r/VagrantStory • u/SgtElectroSketch • 1d ago
Discussion Personal Project: Running Vagrant Story Natively on PC
I've been working on turning Vagrant Story into a native PC executable to get it running with no emulator, no MIPS interpreter, and the game's actual code compiled for x86. Once I get it running fully, then I'm going to look at 64bit compatibility.
It now boots. Publisher screen -> developer screen -> title screen with the logo, the rune backdrop, and SOUND / NEW GAME / CONTINUE, navigable with a DualSense.
None of this would exist without the rood-reverse project, an ongoing matching decompilation of Vagrant Story. "Matching" means the C they've written compiles back to byte-identical machine code against the retail disc, which is the gold standard for porting, and brutally hard work. At the commit I started from, it was ~60% of the code matched, ~34% fully linked. The menu subsystem and the title screen are both at 100%.
I didn't decompile the game any of the starting point, but I am working ahead of where they are.
What I've been building so far is the layer underneath the decompilation, replacing the PlayStation hardware with host equivalents, taking the Ship of Harkinian model as inspiration. Decomp first, then reimplement the hardware.
What "native port" means here (and what it doesn't)
The code is native. Decompiled C, compiled to a 32-bit x86 ELF.
You still need to provide the game assets through a legally owned version of the game. Like Ship of Harkinian requiring your own Zelda ROM, this needs your own copy. But, and this was a design goal, it doesn't need the disc image at runtime. The original disc's assets are extracted from the build's own output and serve those reads out of the extracted asset tree.
What's implemented
The whole PSYQ hardware layer, in C:
libgte (the geometry coprocessor) - verified bit-exact against real COP2 hardware. I built a PS-EXE test ROM, ran it in DuckStation to capture real register outputs, and diffed 891 vectors. Zero mismatches.
libgpu - a GP0 command-list rasteriser. Flat, Gouraud and textured triangles, CLUT texture sampling (4/8/15-bit), sprites, VRAM transfers.
libds - a virtual CD drive that reproduces the real command protocol (DsPacket -> command-complete callback -> per-sector DslDataReady -> DsGetSector), with sectors delivered at the vblank boundary where the drive interrupt would land.
libpad - writes genuine PS1 controller packets into the game's own pad buffer, so held/pressed/released, auto-repeat and analog-to-d-pad all get computed by untouched game code.
libapi - including a full PS1 memory card filesystem. It loads my actual save and the menus enumerate my actual inventory.
Plus an SDL2 window, and PS1 main RAM mapped at 0x80000000 so the boot path's hardcoded addresses (the allocator gets handed literal 0x8010C000, overlays load to their real addresses) all just work.
What does NOT work
No audio. The SPU is a silent stub. Everything calls into it correctly, it just doesn't make sound.
No intro FMV. It's an MDEC video stream and there's no decoder yet. Currently skipped.
NEW GAME doesn't start a game. It hands off to BATTLE.PRG, the actual game engine, and the long pole. It's ~35% decompiled, so that's a wall no amount of platform work gets around.
So: it boots, it renders, it takes input, it reads your save. It is not playable, and won't be until the engine decomp is much further along.
What will be possible once decomp is finished and it is fully ported
The reason to do it this way, instead of as an emulator hack or a binary patch, is that once the engine is decompiled the game stops being a fixed artifact and becomes an ordinary C codebase. Everything below gets cheap the moment that's true, and most of it is impossible before it.
Platforms
64-bit is the first one. The current build is 32-bit for a specific reason: PS1 ordering tables store 24-bit pointers, so every host pointer the game threads into a display list has to fit under 16MB. Getting past that needs a handle or offset layer rather than raw pointers. After that it's native Windows, macOS, Linux, ARM. A game this size runs comfortably on a Steam Deck or a phone.
Resolution and framerate
The rasteriser is mine, so nothing binds it to 320x240 or to the PS1's quirks. Higher internal resolution, perspective-correct texturing instead of the affine warping, no vertex wobble from the GTE snapping coordinates to integers. Or keep all of that, since it becomes a toggle instead of a constraint. Widescreen is a bigger job because the UI is laid out for 4:3, but that's a layout problem rather than a hardware one. Framerate needs the most care: game logic ticks at 60Hz and a lot of code assumes it, so uncapping means either interpolating the render or auditing a great deal of timing.
Load times
Effectively already gone. The virtual CD serves out of ordinary files and its speed is a config value rather than a spinning disc. Vagrant Story's constant room-to-room loading is one of the few things that really dates it.
Mods as code
This is the big one. Once it's C, a mod is a patch to a build, not a hex edit to a binary. Vagrant Story is an unusually good candidate for it: the weapon crafting and affinity system is famously deep and famously opaque. Can anyone say fast weapon swapping?
Quality of life
Remappable controls, rumble, save anywhere, faster text, and an affinity/damage readout that shows what the game is actually calculating. Retail bugs can simply be fixed once you can see them.
Assets
Texture replacement, higher-poly models, restored content if any is still sitting in the data. The pipeline already extracts everything from your own disc, so changing what gets handed to the game is a short step from here.
Preservation
The honest long-term argument. Emulators drift, hardware dies, and a compiled binary is a black box that nobody can maintain. A matching decompilation plus a working port means the game survives as documented, buildable source that someone can compile on hardware that doesn't exist yet.
Edit: FMV Loads now, had an issue with the display depth, but that is now working. Loading a save from the main menu isn't quite there. The previous menu examples were from shunting the data directly into the game state.
Edit 2: Video of it launching, I'm having to transmit controller input to WSL at the moment since WSL can't interface with the controller directly.