Decompilation
The executable, rewritten as source. Matching source must compile to the retail bytes; non-matching source gives up that proof for speed.
How this project ports Legend of Legaia, and why it goes about it this way: a playable port and modding hub standing on Ghidra-traced reverse engineering, fresh Rust written from format docs with the decompiled output as reference. That is the ScummVM / OpenRCT2 model, not a decompilation and not a static recompilation of SCUS_942.54. This page is the method; the case study sets it beside the other routes, using a static recompilation of this same game built over the same months.
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.
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.
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.
The executable, rewritten as source. Matching source must compile to the retail bytes; non-matching source gives up that proof for speed.
The machine code, translated to host code mechanically, over a runtime for the hardware it used to talk to.
A new engine that loads the original's data and behaves like the original. The original's code is reference only.
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.
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.
// 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.SCUS_942.54 and overlays, and reads a cast module's image to recover which presentation calls each arm makes, but no MIPS executes.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.
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.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 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.
Choosing one method does not rule out using the others as tools, and this project does so.
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.
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).// 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).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.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)._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.// NOT WIRED: names what must exist first, // REPLACED-BY: names the mechanism that does the routine's job.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.NOT WIRED that a passing oracle executed has the highest priority, since an oracle traversing stub code can certify behaviour nothing implements.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.
Sequencer in engine-audiospu::Spu, with AudioOut (cpal) and WebAudioOutrender-kernels; engine-render on wgpu natively, WebGL2 in the browserVfs backends and CD streaming host traits in engine-sceneengine-menus, the writer in engine-menu-runtime, legaia_savemdec, with XA audio in syncThe line is not simply where a library ends. It depends on what a player can observe:
seq_calc kept beside the practical implementation to pin the behaviour.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.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.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.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..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.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).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).
disassemblyInstructions at stated addresses, branch and store sequences. The strongest grade, and only as good as the addresses it cites.captureA save state, a probe log, a disc-derived oracle.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.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.
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.
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 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).
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.
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).
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.
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:
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.