The method at a glance

Seven steps, listed in the order each one first becomes possible. It is not a waterfall: a subsystem passes through steps 1 to 4 many times, and steps 5 and 6 keep sending work back. The arrows that run backwards do most of the work.

1 Document the disc byte accounting, round trips 2 Port from disassembly port catalog, evidence grades 3 Wire: reach it live live audit, runtime coverage 4 Replace the toolchain layering gate, host drift 5 Play the whole game ladder ratchet, seed set 6 Measure against retail corpus ratchet, ledger 7 Keep iteration quick same output, or it is a bug A B C D E F Beneath every step evidence grades, falsified register

The loops

  1. A
    Corpus to disassembly. A divergence is read for its shape, then the instrument or the engine is at fault. If the engine, the mechanism goes back to porting.
  2. B
    Corpus to formats. The same, when what turned out wrong is a reading of the bytes.
  3. C
    Ladder to wiring. A stall names the parked op and the unset flag, which usually means a port with no caller or a routine never ported.
  4. D
    Wiring to porting. A port that cannot be given a caller exposes what is missing above it.
  5. E
    Corpus to corpus. Capture coverage takes its denominator from the disc and ranks what to capture next.
  6. F
    Everything gets quicker. Performance rounds change how often the other loops can run, and never what they conclude.

Disproved readings are kept. Every loop can end in a reading that turns out wrong, and each newcomer finds the same wrong readings plausible for the same reasons, so the register holds the tempting reading, the decisive instruction, capture or count, and what is true instead. A rising worklist after a dump pass means the measurement got wider, not that the work went backwards (disc coverage).

Measurement gets corrected this way as much as the game does. The Sol Tower stair walks miss on camera by a position-shaped amount, which reads as a defect in the follow compose. The engine's seat equals retail's staging descriptor to the unit, and retail's live pose is short of it, because the ease only runs while the player moves. The answer is in what the channel compares against, and the wrong reading goes in the register.

Three ways to port a game

Each method suits different goals, and none is the best in general. The clearest way to separate them is by what each one starts with and what it still has to earn. The case study takes this further: what executes at play time, how the two Legaia ports developed side by side, and where each route pays.

Decompilation STARTS FROM The executable rewritten as source, function by function PRODUCES Source that rebuilds the game matching: compiles to the retail bytes FIDELITY Fidelity is proved by the byte match, per function GOOD AT Exact source-level truth; source mods; retarget it later Static recompilation STARTS FROM The executable's machine code translated whole into host code PRODUCES The original game, running over a model of the PS1 hardware FIDELITY Fidelity is inherited its code runs, translated GOOD AT Retail behaviour early, with its quirks and timing included Reimplementation this repository STARTS FROM Format docs + traced behaviour the disassembly read, never translated PRODUCES A new engine for the game loads the game data off your disc FIDELITY Fidelity is measured against retail states, ratcheted GOOD AT New renderers, hosts, mechanics; modding by game concept A recomp gets you the game. A reimplementation, plus the oracles that measure it, gets you an engine for the game.
Three routes to a port. The same picture heads the project README.

Decompilation

The executable, rewritten as source. Matching source must compile to the retail bytes; non-matching source gives up that proof for speed.

Starts withA check on every function, when matching. The byte match proves the source says what the game does.
Has to earnCoverage, names and types, the original compiler and flags, then a platform layer for retargeting.
Right whenExact understanding is the goal in itself, or source-level modding is the aim, and the compiler can be pinned down.

Static recompilation

The machine code, translated to host code mechanically, over a runtime for the hardware it used to talk to.

Starts withThe whole game running early, with retail behaviour nearly for free, quirks and timing included.
Has to earnIndirect jumps and overlays, a faithful hardware model, and the freedom to change machine-shaped output, usually through hooks.
Right whenRetail behaviour on modern machines is the product, and the game's code is tractable to translate.

Engine reimplementation this project

A new engine that loads the original's data and behaves like the original. The original's code is reference only.

Starts withFree choice of structure, language and platform, with new rendering, audio and mechanics as ordinary features.
Has to earnEverything else: a long road to first playable, and every behaviour found, written and measured.
Right whenThe goal goes beyond the original's behaviour, the target cannot carry its code, and much of the game is data an engine can load.

A wrong reading of a routine in a reimplementation produces a running game that is quietly different, which is what the measuring half of the method exists to catch. Overlays are a cost for a translation in this game: they share virtual addresses, so an address alone does not identify a function (VA aliasing).

Put shortly: a recomp gets you the game; a reimplementation, plus the oracles that measure it, gets you an engine for the game. Neither is the better route in general. A recomp is the faster way to retail behaviour on a new machine, and a matching decompilation is the only one that proves what the code says. A reimplementation trades both away for an engine whose structure is its own, and then has to buy fidelity back by measurement.

Why this project reimplements

The project has two coordinated tracks: preserving and documenting every asset, and a port the user runs over their own disc. Format documentation is a deliverable here regardless of the porting method, and the disc patcher and language packs are built on it. The port aims to go past the original with retail one toggle away (fidelity and enhancements). And Legaia is heavily data-driven: field scripts, effects and moves are bytecode on the disc, so porting a VM brings along every script the disc holds for it.

What it costs

  • Accuracy has to be earned. A recomp inherits retail behaviour; this port has to reach it, one mechanism at a time, and prove it.
  • A long road to first playable. Formats, VMs, a renderer, an audio stack and a battle system all have to exist first.
  • The instruments are a project of their own. Seeding from retail RAM, scoring frames and keeping baselines honest take ongoing engineering.
  • Being faithful is a discipline. An enhancement must leave the faithful mode bit-identical when off.
  • Fidelity is a long tail, paid continuously. The first playable game is far from the last divergence, and parity on what the corpus has seen says nothing about what it has not.
  • It rests on decompilation-grade analysis; it does not replace it. Every port is read from the disassembly with the care a decompilation takes. Reimplementing changes what gets written from that reading, not how much has to be understood.

What it buys

  • New rendering. Enhanced lighting, fog, the occlusion fade and SSAO in the wgpu renderer, beside a PSX mode on the native target the corpus scores.
  • Its own audio stack. A from-scratch SPU and sequencer on both hosts, with pack music mixed beside them.
  • More hosts. One session in a native window and in the browser, with VR on the site's 3D pages.
  • Modding by game concept. An expansion pack is TOML naming monsters, maps, quests, arts and items, not addresses; run rules, game modes and netplay are the port's own. Patcher features proved against retail graduate into engine toggles.
  • Rules you can read. A damage formula, an AI pick or a script opcode is a named Rust kernel tagged // PORT: with the retail address it was read from, so the retail routine stays its provenance and the port catalog derives what is ported from the tree.
  • No original code executing. The engine parses data tables out of the user's SCUS_942.54 and overlays, and reads a cast module's image to recover which presentation calls each arm makes, but no MIPS executes.

Fidelity is measured, not inherited

A recomp is faithful because the original's code runs. Here nothing of the original runs, so fidelity is a measurement, and each instrument scores one axis of it against retail and commits the result as a ratchet that may only rise.

Stills
The retail comparison corpus seats the engine where each of several hundred retail save states stood and scores frame, camera, player, placements, music, flags, party and enemies per state.
Film
The cast-sequence oracle records a spell, summon or monster cast on both sides and scores it frame by frame on the game's own state-machine clock, where a still sees one instant.
Sound
The audio-state sweep and real retail PCM compare music and cues against what the emulator actually played.
Routines
The retail kernel differential runs a retail routine off your disc in the crates/retail-exec R3000 interpreter, GTE included, beside the Rust kernel that ports it, on generated inputs, with no hand reading between the two. Every test declares and proves the routines it runs, and the count it covers is a ratchet.
Whole game
The full-game ladder plays New Game to credits on pad input, segment by segment and as one continuous run.

The verification ladder stacks these with the format docs and port provenance, rung by rung, with what each one cannot see. None of these is a proof in the sense a matching decompilation's byte match is. Each measures what it samples, which is why the corpus takes its denominator from the disc (capture coverage) and its gates refuse a silent loosening.

The boundary, stated honestly

The project does not call this "clean-room". The same people read the Ghidra output and write the Rust, which is not the two-team firewall that term means. The boundary actually enforced is narrower and checkable: the dumps in ghidra/scripts/funcs/ stay reference material and are never committed, no Sony-derived bytes are committed, and no code is mechanically translated. The user supplies the disc.

The methods complement each other

Choosing one method does not rule out using the others as tools, and this project does so.

Recomp
A differential oracle. The recomp differential runs a local recompiled build beside the engine, emits one state-trace shape from both, and reports the first frame each channel diverges. A save state holds one instant; a recomp gives a continuous run, down to the stream of key-ons a sequencer asks for. The workspace and its traces stay out of the repository.
Decompiler
A pointer. Ghidra's C says where to look, and the disassembly says what is true. Any source-level truth, from whatever method, makes a stronger reference.
Emulator
The judge. Its save states are the retail corpus itself.
Disc patcher
A proving ground. Features proved against retail with MIPS hooks are expected to graduate into engine toggles.

In the other direction, the format docs, disc and save-state readers and measurement method are written so that other projects can reuse them, whichever method they chose.

Step 1: document the disc and parse its formats

Done
Walk the disc from sectors to sub-assets. Trace each format to the routine that reads it in Ghidra, write the byte-level page, and write a parser with a way back to bytes where a tool needs one. Every format carries a confidence level: Confirmed, Inferred or Unknown.
Produces
Format pages, one parser per format, the extraction pipeline, viewers, and the disc patcher on the same parsers. All of it is useful before any engine exists.
Gate
The denominator comes from the disc. Disc coverage counts the code bytes inside dumped functions and the entries with a known format class; byte accounting counts the bytes a parser actually consumes, so an unwalked format surfaces as a large residue. Both are ratchets. The patcher's disc-gated oracles patch a scratch copy and re-decode it to prove the round trip.
Example
The bytes 0x01059B84 at the start of a scene's texture entry look like a format's magic number, and read that way they invent a third pack format. They are a DATA_FIELD chunk header wrapping an ordinary pack (field-pack). And an LZS stream that "decompresses without error" proves nothing: the ring buffer starts as zeros, so most inputs decode to something plausible (LZS).

Step 2: port functions from the disassembly

Done
Dump the function, read the instruction stream, write a fresh Rust function that does the same thing, and tag it // PORT: FUN_<addr>. The decompiled C is only a hint about where to look: register arguments vanish from calls, stores get reordered or dropped, labels print as calls, and a module imported at the wrong base prints every address shifted by a constant (decompiler artifacts).
Produces
World-free kernels in the engine's crates, a citation in the docs, and a row in the port catalog, which derives each function's status from the tree on every run.
Gate
Three checks, each on its own question. port-catalog.py --check ratchets what is ported. Port provenance asks whether a // PORT: address names the routine the Rust implements; its signals rank suspicion, so --check gates the delta: a commit that adds an unwaived finding fails. The retail kernel differential asks whether the Rust computes what the routine computes, by running the routine off your disc beside its kernel; each test declares and proves the routines it runs, and differential-coverage.py --check ratchets how many tagged routines are covered. A differential settles provenance for the addresses it runs (the verification ladder).
Example
A sweep for writers of _DAT_8007B8C2 searched the absolute lui + offset form and found none, which was read as "BSS zero-init sets the value", then as "retail runs with the flag at 0", inverting the flag's polarity. Its writer is 0x80015F08 sh v0,0x5aa(gp), a gp-relative store the scan could not see. A negative result is held to the same evidence bar as a positive one.

Step 3: wire them

Dumpedits instructions are on file
Documenteda page cites it
PortedRust exists and its tests passlooks finished
Wireda host can reach it
Entereda replay actually ran it
The catalog tracks each stage separately, so a ported routine with no caller cannot count as done.
Done
Give each port a caller that a host can reach. An unwired port compiles, passes its own unit tests, reads as progress and does nothing for a player, so it is not done until something live reaches it.
Produces
Ports a host reaches, and for the rest an explicit statement of why not: // NOT WIRED: names what must exist first, // REPLACED-BY: names the mechanism that does the routine's job.
Gate
Static and runtime. port-catalog.py --live builds a call graph from the hosts' real entry points, with every ambiguity resolved toward reachability, so --not-live is a hard floor. Live-audit triage settles each row: WIRE, DISCLOSE, DELETE, REPLACE or FALSE INERT. The runtime denominator asks what a replay actually entered, and host-drift gates catch a feature wired into one host only.
Example
Three field effect handlers stay unconverted across coverage runs, which reads as "a composed-overworld ladder would reach them". That reading is falsified: no production path constructs an actor on any of the three, so the gap is a constructor, not a test. Sharper still: an anchor tagged NOT WIRED that a passing oracle executed has the highest priority, since an oracle traversing stub code can certify behaviour nothing implements.

Step 4: replace the toolchain under the game

Find the line between the game's own logic and the PlayStation toolchain it was built on, keep the first and replace the second with new foundational libraries. Sony's PsyQ libraries are statically linked into SCUS_942.54, where they appear as recognisable address clusters. The game's own logic is above them and mostly lives in overlays.

RETAIL THE PORT Game logic damage rules, script opcodes, culling choices Observable hardware behaviour SPU voices and reverb, GTE limits, dither Toolchain and devices libsnd, libspu, libcd, memory card, GPU interface port model replace Same decisions, new Rust named kernels tagged with the retail address Reproduced where a player can tell SPU model, GTE model, PSX mode behind a toggle New foundations sequencer, Vfs, save writer, wgpu and WebGL2
The question at each layer is whether a player could notice the difference. The table below names the replacements.
RetailThe port
libsnd / SsAPI sequencerSequencer in engine-audio
libspu and the SPU hardwarespu::Spu, with AudioOut (cpal) and WebAudioOut
GTE and GPUThe GTE model in render-kernels; engine-render on wgpu natively, WebGL2 in the browser
CD streamingThe Vfs backends and CD streaming host traits in engine-scene
Memory cardThe card flow in engine-menus, the writer in engine-menu-runtime, legaia_save
MDEC moviesmdec, with XA audio in sync

The line is not simply where a library ends. It depends on what a player can observe:

  • Game decisions stay game logic, even beside the hardware. The visible-tile crop, the actor cull and the per-primitive rejects are retail's choices, so the port makes the same ones.
  • Observable hardware behaviour is modelled, not swapped away. Voices, ADSR, the Gaussian resampler and reverb are in the SPU model. The GTE's register limits, vertex snap and dither sit behind PSX mode.
  • A library's documented format may not be the game's. Legaia's SEQ departs from stock PsyQ SEQ, and reading it as stock plays music about three times too fast (SEQ).
Produces
A foundation that runs on hosts the original never knew, with instruction-level reference kernels such as seq_calc kept beside the practical implementation to pin the behaviour.
Gate
The layering gate keeps kernel crates from reaching the session, renderer or a host, and a wasm32 type-check holds the shared session free of native dependencies. What the replacement does is measured in step 6: the image channel at the native target, and audio through the audio-trace oracle and real retail PCM.
Example
The audio stack runs both hosts from one core, with only the output differing. Its scene-entry behaviour is a sequence every host owes, and the docs record what each missing step sounds like.

Step 5: play the whole game on pad input

loadsthe seeded save lands in its scene
entersthe entry script settles
progressesthe next milestone is reached by seated traversal
padthe next milestone is reached on buttons alone
The four tiers a story segment climbs. A separate continuous run plays one New Game with no re-seeding.
Done
Play New Game to credits with no human, on pad input alone. A scripted hand walks, talks, fights, heals and shops by pressing the buttons a player would. Story and battle defects no unit test sees appear here, and a game that cannot be finished is not a port.
Produces
The full-game ladder: story segments between milestones anchored to retail saves, each climbing loads, enters, progresses and pad. The continuous run plays one session from New Game with no re-seeding, so it arrives at each stretch with the levels it earned. The soak harness throws seeded random input at every scene and minigame.
Gate
The tiers are ratcheted, and a red run may not be turned green by lowering them. One random stream can hide a fight won only by the deal, so the seed set repeats the whole run on other streams, each asserting its own row. Record and replay keeps it reproducible: the same input gives bit-identical traces.
Example
A stall is a diagnosis, not a red light. The report names the script instruction that parked, the door gate that refused, the story flag never set and the disc records that set it (how to read a stall). The page also says what the ladder cannot claim.

Step 6: measure against a retail corpus

Done
Capture moments of the original as emulator save states. For each, read what retail was doing out of the state's own memory, put the engine in the same situation through its own load path, and score the difference per channel, worst state first. The channels cover what a player notices: the frame on screen, player position, camera, NPC placement, facing, the music track, story flags, party, enemies and more. The retail-oracle method writes it up engine-neutral.
Produces
The retail comparison corpus and its siblings: the cast-sequence oracle for what unfolds over time, the audio sweep and real PCM comparison for sound, the retail kernel differential for single routines run off the disc, the recomp differential for continuous traces. Its reports are a worklist, with a catalogue of divergence shapes.
Gate
Per-state, per-channel scores may only rise. Two more gates protect the ratchet: a baselined value that moved the wrong way needs a reasoned entry in a ledger that is never pruned, and a test that skipped for want of its data is not counted as passed (ratchet integrity).
Keeping it wide
A corpus grows where capturing is easy, and parity on those moments says nothing about the rest. Capture coverage lists every scene, monster, enemy attack, player cast and menu screen the disc defines, and ranks what no state shows yet. As parity is reached on what the corpus holds, the corpus is extended into what it does not hold.
Example
On noaru_field_card_walk, the engine's frame is black where retail shows a floor, with every other channel exact. That pattern points at a mesh-bearing ambient part seated off its anchor: Noaru's ice floor rides the camera focus through move-VM extension op 0x24. The frame pointed at the gap, and the mechanism itself is read from the move VM's overlay extension.

Step 7: keep iteration quick

Done
Focused performance rounds, run now and then rather than continuously, on two costs: what the engine costs a player, and what a change costs a developer. An oracle that takes too long gets run less, and defects survive longer.
Produces
Frame benchmarks on both hosts, and build and test profiles with an edit-time dev-fast path, a result cache on the corpus and a parallel full gate. The numbers are before-and-after comparisons on one machine, deliberately not committed: a frame-time gate on a shared machine can only flake.
Gate
A speedup is proved by the output, not by the timing. The ladder's trace with its wall-clock fields stripped must match line for line, and a patcher feature's .ppf from a fixed seed must hash the same (profiling). A cache has to key on everything its result depends on, and a bless or a check never reads the corpus's.
Example
The readPixels on the battle transition's arming frame is expensive, and an asynchronous read would remove the wait. It would also land the capture frames later, which moves the transition, so that is not an optimisation available here (what the numbers have shown).

Evidence grades

A claim about the original carries a grade naming what it rests on, and it takes the grade of its weakest load-bearing piece (the evidence column).

  1. disassemblyInstructions at stated addresses, branch and store sequences. The strongest grade, and only as good as the addresses it cites.
  2. captureA save state, a probe log, a disc-derived oracle.
  3. decompiled-CGhidra's rendering, an inferred signature, a label someone typed into the database. A claim nobody has confirmed against instructions, which makes this a re-audit worklist and not a verdict of wrong.
  4. inferenceReasoning from surrounding facts or from absence, usually stated openly.

A docs page is not a grade of its own. A statement on one carries the grade of what it cites, and a statement with no citation is a hypothesis until someone re-reads the listing. Grades let a reader see how much weight a claim will bear, and they make the audit cheap: the re-audits that falsify claims here find them in the decompiled-C bucket.

Traps a newcomer hits

Green tests are not parity

A test can only fail on what its author thought to check, and three quieter failures sit beside that one. A disc-gated test that finds its input missing reports ok (a skipped test is not a passed test). An oracle can have no way to disagree: pcm_oracle's "retail" window is the engine's own mixer seeded from a snapshot, which is why the real retail PCM check records the emulator's actual output. And a channel may measure less than its name suggests: party and inventory are seeded from the retail window, so on their own they check the seeding.

A still frame cannot prove a cast

The corpus scores one frame per state, and a cast runs for hundreds of vsyncs. A high image score on a mid-cast state says that frame matches, and nothing about the order, timing or length around it. On most summons the state's one instant is the white flash, which the metric cannot see under. The cast-sequence oracle records both sides and pairs frames on the game's own state-machine clock.

A save state replays old RAM

A state holds the RAM of the disc that booted it, so loading it onto a freshly patched disc keeps the old executable, overlays and models resident. A fix can look absent, and a fixed defect can look present. PCSX-Redux will also silently apply a .ppf sitting beside the image. Verify disc edits from a cold boot or a memory-card load (patched-disc taint).

"No static caller" is not dead code

Most game logic lives in overlays loaded at runtime, and much of it is reached through pointer tables and actor templates. "No caller found" becomes "no reference exists" only after a search of every image's bytes in every encoding an address can take (address reference scan), and a search can fail in its own ways: a grep through | head reads as empty when the pipe cuts it off before the hit.

A speedup that changes a result is a bug

The optimiser's choices must not move a simulation that runs on integers, and a cache that misses one input hands back an old answer for a new question. Prove a speedup by comparing the output (step 7).

A score can fall because a fix is right

The 0x2A chain-exit fix drops two Tail Fire frames, which reads as a regression. The old scores rested on the error, and the state's own RAM shows it. Read why a score moved before reverting or blessing it, and record the reason in the ledger.

Applying this to another game

Most of the method transfers, and a good part of the code does too. Reusing for other PS1 games sorts the crates and scripts by how far they carry and names each Legaia assumption that would trip a second title. The retail-oracle method writes the measurement half engine-neutral, with an adoption order.

Before starting, three questions decide much of the shape:

  • How much of the game is data? The more of its logic is bytecode and tables, the more a ported VM carries, and the stronger the case for reimplementing.
  • Does an emulator give you states you can read? Without readable machine states there is no corpus, and every accuracy claim stays an impression.
  • What is the actual goal? If it is retail behaviour on modern machines and nothing more, another method may reach it sooner.

Then start small on each axis instead of finishing one step at a time: one format with a round trip, one VM wired into a host, one state channel over a handful of states, and the evidence grades and falsified register from day one, since those cost nothing to start.

See also