Engine reimplementation
A from-scratch Rust port of Legend of Legaia in the ScummVM / OpenRCT2 mould: the original PlayStation code is traced with a disassembler, documented, and rewritten fresh - never recompiled. You supply your own disc image; the engine reads every asset from it and plays the game. Retail behaviour is the measured ground truth, provable against parity oracles, and enhancements ride on top as toggles with a retail-faithful mode always one flip away. This page is about how the port itself is built.
At a glance
- What it is
- A Rust runtime - boot, field, battle, menus, cutscenes, minigames, audio - reading the original disc for every asset
- What it is not
- A recompile of
SCUS_942.54, a re-authored asset set, or a build that loses the retail-faithful mode - Hosts
- The native
legaia-engine play-window(winit + wgpu), the browser play page (WASM, same engine) and the site’s minigames page - One session
engine-session::BootSessionis the game loop both play hosts tick; hosts own only input, the draw target and audio output- Faithful vs enhanced
- Shading defaults to retail, rasterisation to clean; every enhancement is bit-identical to retail when off
- Proof
- Save-hash scenarios, VRAM diff against save-state captures, mode / audio / sim traces, record-replay determinism, a retail comparison corpus
- Legal posture
- Zero Sony bytes in the repo or any binary; disc-dependent tests skip when no disc is supplied
Where it stands
The port plays the game. From a cold New Game it runs the opening, the towns and dungeons, random and boss battles, the pause menu, shops and inns, the five minigames, the movies and the save screen, on both the native window and the browser page, with sound. A segmented full-game ladder seeds the engine at each story milestone from a retail save and drives it to the next with pad input only, from the opening to the credits crawl; every segment of it clears.
What is still rough:
- It is not frame-identical to retail. The retail comparison corpus scores the engine against every catalogued save state and ranks the gaps worst first. Game state - scene, position, party, flags, bag, enemy roster - matches almost everywhere. The rendered frame is where the distance is: field, world-map and menu frames sit close to retail’s, and the frame in the middle of a battle is the furthest off.
- One New Game reaches the credits, on the hands measured. Every ladder segment starts with a retail player’s party and bag; the ladder’s continuous run plays a single New Game that is never re-seeded and reloads only its own saves, and it reaches the ending on each dealt hand of the rand stream it is measured on. Another deal meets other fights, so the figure on the landing page is a ratchet, not a promise.
- The ladder proves the route, not every room. It drives the critical path. Optional content, rare script branches and long unscripted play are covered by a seeded soak harness that hunts panics, softlocks and stalled scripts.
- Bugs happen. Expect the occasional visual or timing glitch outside the measured states. A report naming the scene and what you did to get there is the fastest way to a fix.
- NA disc first. The port targets the USA release; the Japanese and PAL builds differ in the executable and are not its target yet.
How the crates stack
The port is a stack of small crates with one rule running through it: the lower a crate sits, the fewer things it may link. Format parsers know nothing about engines; the VMs know nothing about GPUs or speakers; the simulation draws nothing; and only the hosts at the top touch a window, a browser or an audio device. That is what lets the browser run the same engine as the native window - it simply links a different top row.
The crate map draws the same stack interactively: every crate in its band, what it uses and what uses it, its scope and modules, and the path a scene takes from the disc to the screen. Its data is generated from the workspace's own manifests, so it cannot drift from them.
engine-session, engine-screens and engine-ui but never engine-render, so it carries no wgpu; parity and engine-render are reached only from the native shell. engine-render and engine-audio never depend on engine-core: the renderer sits on the draw-list kernels and the audio stack on the sound parsers alone. engine-screens also links engine-ui, and the crates above the kernel band link the format crates directly as well as through it. No crate re-exports another, so every path names the crate that owns the code.| Crate | Role |
|---|---|
engine-battle-vm | The battle-side VM kernels: the battle-action state machine, the damage / accuracy / escape formulas, the battle camera script and the battle-overlay leaves around them |
engine-cast | The slot-B cast / summon modules the battle action SM pages in: their tick bodies, each module’s own camera and countdown, the cast-arm kernels. One rank above engine-battle-vm, which never names it |
engine-camera | The shared retail GTE camera below the battle VM, the field and the draw kernels: the camera build and projection, the battle pose and GTE constants, the cutscene camera mover, tween and shake, the camera-relative glide |
engine-vm | The field / event, move and effect VMs and the window-widget VM. Each talks to the game through a Host trait, so the crate has no GPU or audio dependency |
engine-motion-vm | The two per-actor motion VMs: pursue / patrol / face-target, and scripted motion over the scene’s motion bytecode |
engine-battle | The battle kernels that never touch World: monster AI script, death spoils, AP gauge, stat aggregator, spell / monster / move-power catalogs, encounters, level-up, Seru learning. engine-battle-loop holds the battle state and its own steps, engine-core the loop that reaches the rest of World |
engine-core | Where the kernels meet: World, the VM host implementations over it, the tick, the battle loop, saves, and the World side of the sessions whose rules live in the kernel crates. Draws nothing |
engine-scene-host | SceneHost, the owner of a running World and the disc index: scene entry and tick, BGM routing, the FMV hand-off, save resume and the live-scene preview. Directly above engine-core, which never names it |
engine-menu-runtime | The pause menu over a running World: MenuRuntime, the field-menu sub-session dispatcher, the out-of-battle action validator host |
engine-scene | The disc-facing half below engine-core: resolving a scene to its PROT entries, loading what it is built from (textures, meshes, the model bank, environment draws), the virtual filesystem. Names no World, so a tool that only reads scenes links it without the simulation. It is a row of the layering table like the other kernels |
engine-minigames | The minigame rules engines that need no World - slot machine, Baka Fighter, dance, the prize exchange, the Muscle Dome contest ladder. Fishing is the crate below it |
engine-fishing | The fishing rules engine below engine-minigames: the pond session all three hosts run, the rod / lure / line actors, the venue chrome, the hub screen and the venue floor grid |
engine-minigame-scenes | The minigames’ 3D scene surfaces and venues, plus the World’s minigame seat. The Baka Fighter duel and the Muscle Dome arena load through a PROT reader; the dance hall and the fishing venue load a scene through engine-scene |
engine-effects | The effect kernels that need no World: the action effect scripts, the ribbon and render-mode-4 emitters, the summon scene-graph driver, the screen effects and object effects |
engine-system | The World-free runtime system at the bottom of the engine stack: pad input and the retail pad pump, the stream chunk installer, MDEC DMA sync, sound state, fades, mode-entry initialisers, the RAM-cell map |
engine-mode | The game-mode seat over engine-system: the 28-entry mode table, its driver and the minigame door-warp ids |
engine-menus | The World-free menu front end: item and equipment catalogs, the inventory-use and spell-menu sessions, the window-widget host, the memory-card flow, inn, shop, save, status and pause screens |
engine-battle-menus | The World-free battle side of the menu front end: battle command input, the command-flow byte, the open banner, the timed fight and the Muscle Dome match session |
engine-battle-loop | The battle loop’s state below World: BattleState and CastFxState, the round flow and frame clock, and every step that reads or writes that state alone (stat block and buffs, tutorial flow, HUD glides, clip ladder, module stagers). Steps that reach the rest of World stay in engine-core |
engine-title | The World-free boot front end: the publisher-logos phase, the title state machine, the title overlay tick and its SCUS primitive helpers |
engine-dialog | The dialog layer below engine-field and engine-menus: the dialog pager (panel, row window, typewriter pacing, the picker slide) and the text balloon |
engine-field | The World-free field kernels: actor programs, the follow camera’s parameters and eases, player clip playback, the op-0x49 submode, the field event queue, CLUT effects, the world-map controller, the MAN field-script decoders, the inline-dialogue and cutscene-timeline context state and the field camera |
engine-ui | Renderer-agnostic draw lists (TextDraw / SpriteDraw) over the shared render kernels and the battle HUD model - the wgpu-free draw layer both hosts link |
render-kernels | The wgpu-free render kernels both hosts share - GTE math, the screen-primitive builder and its CPU rasteriser, VRAM capture, the effect emitters, the battle-intro transition, enhanced scene lighting. engine-ui builds on it |
engine-render | The wgpu renderer with a software PSX VRAM (1024×512, palette decode in the fragment shader) |
engine-audio | From-scratch 24-voice SPU, SsAPI-shaped sequencer, SFX bank; cpal output natively, WebAudio in the browser |
engine-session | BootSession - the scene host plus the per-frame order around its tick (mode seat, pause menu, camera, BGM and SFX routing) - and the AudioDirector, both generic over the audio output. Both play hosts tick it |
engine-screens | The shop-family screens composed once for both hosts: the gold shop’s windows, the casino prize exchange and coin counter, the inn prompt and Seru-trade panel, the level-up / capture banners |
engine-export | The .glb world exporter behind legaia-engine export-glb: world, NPC, animated-prop, item and party models plus the placement manifest. It steps a scratch World to bake animation, so it sits above engine-core; only the native binary links it |
parity | The engine measured against retail: the VRAM / mode / audio / PCM oracles, the simulation trace, the retail comparison corpus. Tool code no play host ships |
engine-shell | The native host as library code - the play-window window, launcher, replay, scenario runner - plus the legaia-engine binary whose subcommands drive it and the parity tools |
The edges among the World-free kernel crates are a committed table, scripts/ci/crate-layering.toml, and a gate fails any Cargo.toml that departs from it. Each crate’s rank is derived from its row: engine-system, engine-dialog and engine-vm at the bottom, then the shared camera engine-camera, engine-mode and the motion VMs, then the battle VM and render-kernels, then the cast modules engine-cast with the battle, effect and field kernels, then the draw-list kernels, fishing, the other minigames and the scene loader engine-scene, then the menus and the minigame scenes. A kernel crate never reaches engine-core or a host, and the edges a crate’s role alone does not explain are listed in the table with the items that hold them.
Runtime architecture
One session, one world. A host hands the session a pad word and the camera azimuth each frame; the session runs the mode seat and the scene host; the scene host ticks the world; the world drives every VM through the host trait engine-core implements for it. Rendering and audio hang off the side: they consume what the world reports and never reach back into it.
World, which is what keeps engine-vm, engine-motion-vm and engine-battle-vm free of GPU and audio code. The field VM’s FieldHost is an umbrella over fourteen supertraits, one per concern.The frame model
One world tick is one retail vsync, and retail never catches up: a slow frame makes a slow game. The hosts render at the display’s refresh instead, so the engine owns the rule that turns display frames into ticks (engine-field::frame_step): whole 1/60 s ticks with the remainder carried, at most four per display frame, and a backlog past four dropped rather than carried. Both hosts call the same kernel. The pause menu, the name-entry prompt and a movie consume ticks without ticking the world - under the pause menu that is retail’s own shape, since its mode handler runs no frame driver.
Goal and non-goals
Goal: a playable port of the NA release on modern systems via Rust + wgpu, with a WASM target; JP / EU follow once NA is solid. “Playable port” is grounded in retail, not bound by it: the traced dumps and oracles pin what the original does, the faithful mode reproduces it provably, and that is a measuring stick rather than a ceiling.
- Not a decompilation. No byte-matching recompile is attempted and nothing is auto-translated from the MIPS; every line is fresh Rust written from format docs and the traced dumps, which stay reference material. Per-opcode tests use synthetic bytecode.
- Never lose retail. Departures live behind toggles; the oracles keep “faithful” a testable claim. A quirk is preserved in the faithful mode and fair game to improve outside it.
- No re-authored assets. Every texture, mesh, sample and sequence comes off your own disc at runtime.
- Deterministic simulation. Seeded RNG, fixed timestep - the precondition for record / replay.
Modding and translation are shipped tracks, not exceptions: the randomizer (in-browser via the ROM patcher) and the translation tools patch a user-supplied disc without touching the engine, and what they prove out against retail - softlock fixes, tuning sliders - is expected to graduate into engine toggles.
Fidelity and enhancements
In the faithful mode no toggle changes damage, drops, AP costs, encounter rates or story flags, and every parity measurement runs against that mode. Enhancements are explicit toggles, bit-identical to retail when off, so flipping one never touches replays or oracles. Defaults follow the better experience: a knob still defaulting to retail marks an enhanced side that is maturing, not a policy of restraint.
| Knob | Default | Effect |
|---|---|---|
Enhanced lighting (I; F8 cycles time of day) | on | a time-of-day mood over the baked shading, point lights at lamps and glowing props, emissive surfaces, lit windows at night and soft halos - one shared kernel on both hosts |
Lamp shadows (Y) | on | shadow maps for the lighting’s point lights; inert while lighting is off |
Volumetric ground fog (F9) | off while the look matures | a drifting mist bank over misty and night scenes, parted by every character walking through it |
Bloom (F10) | off | bright pixels - lamp halos, glowing props, lit windows, sunlit sky - bleed a soft glow, stronger at night; the HUD never blooms |
Ambient occlusion (Home, --no-ssao) | on | contact shadows over opaque scene geometry from a depth + normal pre-pass, with shading kept forward; character meshes occlude the ground but receive none, and replays, the retail comparison and Retail-faithful force it off |
See-through walls (F4) | on | geometry that completely hides the player dissolves to a dither circle around them |
| Leading-edge wall probes, solid NPCs, live NPCs | on | retail’s three-probe collision footprint, NPC collision and the villagers’ ambient wander |
| Ledge-hop landing guard | on (off under --retail) | refuses a field ledge hop whose landing the walk could not leave; retail tests no landing, and a diagonal step-up can box the player in for good |
| Entry pulse | on (off under --retail) | a rolling vertex-morph envelope at scene entry over the packs it is authored for (jou’s flesh ground, Rim Elm’s shoreline) |
| Retail static-object window | on | a sub-area’s placed props draw only while the camera’s region box holds them, as retail’s windowed object list does; off draws every placement for the whole map |
| Visible-tile crop | on at retail framing | retail draws only the cells the camera’s tile window reaches; any wider or re-aimed view draws the whole map instead of opening black edges |
| Reduce flashing | on | slew-limits the ambient palette cyclers (retail’s casino dance floor strobes at 15 Hz); simulation identical either way |
Music / sound-effect level (bgm_volume / sfx_volume in the options file) | 8 / 8 | two engine-only mixer buses, 0..=10 with 8 the retail mix; voice and movie audio ride neither, and at the default the mix is bit-identical to retail’s single bus |
Master volume (master_volume in the options file, a launcher row) | 25 (percent) | engine-only overall output level of the desktop window, applied to the finished mix, voice and movie audio included; the browser play page has its own volume slider |
Mono output (downmix_mono in the options file; the launcher and play page row) | off | engine-only full downmix of the finished mix, voice and movie audio included; the in-game Monaural row is separate and retail-exact (per-voice fold, movie and voice audio left stereo) |
| Field run button | retail + one | retail’s Cross | R1 plus Square |
Free-angle movement (R) | off | free-angle locomotion instead of retail’s 4 / 8-way quantisation |
Action movement (--action-movement) | off | a platformer field controller in place of the walk: momentum, sprint, Square to jump and double jump, R2 to dash; every pad lock still holds |
Camera distance (T), debug orbit (F3), drag / tilt / zoom | Far / off / identity | framing only; the follow-camera knobs and the distance preset sit out while a scripted shot owns the camera (a cutscene timeline, or a shot a helper record or a talk staged, such as the ending credits’ vignettes or Mei meeting Noa), and the preset eases back in over the follow ease when the shot lets go; a double-click resets the knobs to the retail shot |
| Off-axis battle entry | on (off under --retail) | a fight’s camera opens on the field camera’s azimuth, moved out of the window where the two seat rows overlap on screen; retail opens every fight down the seat axis. Framing only (battle stage and camera) |
| Retail battle cadence | on | the whole battle pass once a battle frame, retail’s cadence, instead of once a vsync; off runs the per-vsync pass |
Heal numbers (--no-heal-numerals) | on (off under --retail) | a battle restore - an item, a Grail, Final Heal, the companion’s heal, a healing Seru - throws a heal-coloured number over its patient; the original throws none and only counts the HP figure up |
| Saved-chain Arts list | off | the command ring’s Arts arm opens the port’s saved-chain list instead of retail’s per-press directional input |
| Smart monsters | off | a monster’s single-target attack goes to the weakest living party member instead of retail’s random pick; the RNG stream is unchanged |
| Shiny Seru | on (off under --retail) | a rare capturable enemy spawns shiny, with +35% stats and a Seru that deals +35% damage - the patcher’s --shiny-seru; off skips the roll and the random draw it takes |
| Run-away EXP | off | a successful escape banks 5% of the fled fight’s EXP into every party member - the patcher’s --flee-exp |
| EXP rate | 1x | scales every monster’s EXP reward, 0.5x to 5x - the patcher’s --exp-scale |
| Fortress battles fix | on (off under --retail) | Jette’s Absolute Fortress ignores the Floating Castle’s stale “no battles” flag; a save without the flag plays the same either way |
| Tetsu spar order fix | on (off under --retail) | a spar fought before the measuring still lets the hunters return to Rim Elm |
| Boss casts respect jewels | off | the boss cinematic casts honour elemental jewels and guards - the patcher’s --jewel-fix |
| Battle freeze guard | on (off under --retail) | a party HP readout stuck short of live HP is re-seeded, so the battle’s wait on it (the state-0x51 softlock) cannot hold |
| Bonus equipment drops | off | a 5% roll after each won battle grants one random piece of equipment on top of the normal drop - the patcher’s --equipment-drops |
| Save anywhere | off | the pause menu’s Save row works in every scene, not only on the world map and at save points as in the original |
| Battle tuning (enemy strength, enemy attack count, Spirit AP, AP from damage, Seru catch rate) | retail | sliders with the patcher’s arithmetic for --enemy-stat-scale, --enemy-attack-count, --spirit-ap, --damage-ap and --seru-catch-rate; --retail holds them at retail |
| Run rules (difficulty, New Game+, ironman, randomizer, Boss Rush) | retail rules; the New Game rules screen on (off under --retail) | run-wide twists chosen per run and carried in its saves; under the default rules a run is bit-identical and writes no extra save block |
PSX rasterisation (--psx-render; the window’s F12 PlayStation pixel look row; the play page’s checkbox) | off | retail vertex snap, GTE register limits, ordering-table draw order and 15-bit dither, on the native 320×240 target magnified nearest-neighbour |
| Semi-transparency blend | on | retail ABE blending - on because it is retail |
| WebXR VR | off | stereo presentation on the site’s WebGL pages |
Play page quality step-down (?autoquality=0 unarms it) | armed | browser only, session only: on a sustained missed frame cadence it turns enhancements off and scales the 3D canvas; never writes an option, and stays out of the way under retail and PSX rasterisation (browser host) |
| Start leaves a minigame | on, not a knob | exits any of the five minigames through that game’s own exit (below) |
One switch pins all of it: play-window --retail turns every enhancement off and sets the persisted engine-only options to their retail values for the session, writing nothing back (OptionsState::pin_retail, WorldToggles::pin_retail). The retail-compare capture runs under it, so a new enhancement added there is off in the comparison without anyone remembering a --no-* flag. Both hosts also take it live: the play page’s Retail-faithful box and the window’s F12 Original game only row pin or release the options mid-game and re-apply the launch’s switch set with retail flipped. Released, the set gives back exactly what the pin lowered - the launch switches their launch values, and the two port guards no switch carries (the hop landing guard, the off-axis battle entry) the engine default.
The gameplay rows from Shiny Seru down to Battle tuning are disc-patcher features the engine runs itself, so a player of the port needs no patched disc. The patcher’s other features either cannot occur in the port (the approach softlock, the Conkram freeze), are still open as engine switches (enemy HP bars, Seru trading without a patched disc), or are disc-content randomizers and mods whose patched disc the engine plays as data.
- Shading defaults to retail. Retail’s textured and colour mesh paths draw the mesh’s baked colour word through the GTE depth cue with no light source, as
texel × colour / 128; the exception is the light-source rows (TMD group flags0x10..=0x17), which shade through the GTE light (shading). The engine draws exactly that; enhanced lighting is layered over it and is an identity when off (every parity capture turns it off). - Rasterisation defaults to clean. The default image is sharper than a PlayStation’s; here faithfulness is opt-in.
- Affine texturing is not gated. The PS1 has no perspective-correct texturing and neither does the mesh path; the swimming textures are the faithful behaviour on both hosts.
The camera knobs stand down for an authored shot. The distance preset, orbit, tilt and wheel zoom shape the follow camera only; while a script that holds the player has an op-0x45 shot staged - a timeline, a helper record or an ordinary talk such as the Water Gate Controller’s - the view reads them at identity. They come back with the follow camera: retail latches no shot, so once the record lets the player go the shot stands until the first step, which eases off it, and the preset blends back in on that ease instead of jumping. The follow ease reads the player’s lock once before the walk, as retail’s field tick does, so brushing a door or stepping onto a stand-and-press tile does not stall the camera. The pause menu freezes the field camera, since retail swaps the field overlay out under it.
Why every minigame must be leavable
A mode the player can enter has to be one the player can leave on every host, or reaching it is a softlock. Retail has no single exit to port: each minigame quits through its own overlay’s state machine (the slot cabinet’s exit row, the duel’s decided-match confirm, the arena’s give-up arm). World::poll_minigame_escape is therefore an engine affordance: Start leaves whichever minigame is live through its own exit, so the cash-out or score matches a deliberate quit, and closes the round trip when the entry came through a door warp. It lives in the world tick, so both hosts inherit it.
The ported VMs
Every interpreter is ported handler by handler: the opcode is read from the disassembly, rewritten in Rust and tested against captured traces. The target is behavioural fidelity per opcode, not byte-exact internals.
| VM | Module | Scope |
|---|---|---|
| Field / event VM | engine-vm::field | 43 opcodes, cross-context dispatch, the op-0x49 tristate, the 0x4C sub-dispatchers |
| Move VM | engine-vm::move_vm | 71 opcodes + the 61-sub-opcode overlay extension |
| Motion VMs | engine-motion-vm::motion_vm | pursue / patrol / face-target; scripted motion and story-flag writes |
| Effect VM | engine-vm::effect_vm | slot pool, init / spawn / tick |
| Battle action SM | engine-battle-vm::battle_action | 47 states in 7 bands; art strikes read from the actor’s chosen art |
| Window-widget VM | engine-vm crate root | 13 opcodes; the menu overlay’s window choreography |
| Title sub-mode dispatcher | engine-title::title_overlay | 25-entry jump table; the title → field launch write |
Gameplay systems
The loop closes end to end: title → save-select → field and encounters → battle → menus → save. BootSession drives all of it through engine-core::World; the rules themselves live in the kernel crates the table names.
| Area | Where | What it covers |
|---|---|---|
| Modes | engine-mode (ModeSeat) | the retail mode word and table; INIT staging and the transition edge |
| Field | engine-scene-host, engine-core, engine-field, engine-scene | the scene’s own MAN bytecode, locomotion, NPC placements, inline field-VM dialogue, door and minigame warps |
| Battle | engine-core round loop over engine-battle-loop, engine-battle and engine-battle-vm | the full round: seats, all six commands, Tactical Arts with Miracle / Super expansion, status ticks, AP gauge, spoils |
| Menus | engine-menus, with the World side in engine-menu-runtime | the pause menu’s two-level stack, items, magic, equip, status, options |
| Shop / inn / level-up | engine-menus (shop, inn), engine-battle (level-up) | stock from the MAN, prices from the item table; inns as scripted dialogue; the XP curve and growth tables read from the executable |
| Title + save | engine-title (title), engine-menus (save select), legaia_save | the real title sheet; the LGSF save container with sentinel-guarded extensions; real memory-card blocks patched in place |
| Audio | engine-session’s BGM director, engine-audio | script-driven BGM with retail’s hard cuts; SFX cues fired against the bank each cue’s category names |
| Minigames | engine-minigames, engine-fishing, engine-minigame-scenes | the warp minigames as suspending scene modes with their own rules engines |
| Cutscenes | play-str, mdec | STR video through the from-scratch MDEC decoder with XA audio in sync (cutscene) |
The browser host
The WASM target runs the engine itself, not a second implementation of it: the play page holds a BootSession over its WebAudio output, built over a scene host it assembles in memory from the disc image. It reaches field and town scenes, live battles, the title and the opening chain, the pause menu and shops, the minigames and audio. Each screen is drawn from engine state through the shared engine-ui / engine-screens builders and the site’s WebGL renderer; per-feature parity between the two hosts is policed by the host-drift gates.
- Seating. The cold-boot spawn is authored for
town01only; every other scene expects a door warp. A host that drops the player straight into a scene gets a resolved seat: the retail one if it stands inside the scene’s largest walkable region, otherwise a door-arrival tile or the region’s centroid. - Pass-through scenes. A scene can be a passage rather than a place: every arrival lands on a walk-on trigger that plays a beat and leaves the scene, and retail never hands the player control there. The scene host recognises that shape from the disc and seats a picked visit on the arrival itself, so the first tick plays the beat. Zora’s Floating Castle’s second form (
taiku2) is the one such scene: its pack replaces every room the beat never frames with a three-vertex stub, so the castle to walk istaiku. - Framing. Both hosts run the engine’s retail follow camera and neither adds a cull of its own - what drops out of a frame is what retail drops, the visible-tile crop and the actor cull at retail framing, which hold under a cutscene’s own shot as retail’s do; a wall between the lens and the player is handled by the see-through-walls fade.
How we know it matches
| Oracle | Command | What it proves |
|---|---|---|
| Engine scenarios | legaia-engine scenarios [--bless] | a headless run for N frames hashes the save bytes against a blessed baseline; an unblessed row fails until reviewed |
| VRAM diff | legaia-engine vram-oracle --runtime-vram <bin> | engine video memory vs a save-state capture; --rows-csv and --clut-regions localise a missed upload to one row |
| Mode / audio / sim traces | mode-trace, audio-trace, pcm-trace, sim-trace | the mode word, SPU state and per-frame simulation channels against retail (differential) |
| Retail comparison corpus | legaia-engine retail-compare | seeds the engine from every walkable and battle save state and scores scene, position, camera, BGM, party, flags, bag and the frame (retail compare) |
| Record / replay | legaia-engine record / replay | the same input file yields bit-identical state traces twice (replay) |
Details: the VRAM static mask
A save state’s VRAM is a live snapshot: two captures of the same scene differ across ~40% of the texture band (animation frames, battle leftovers), so no stateless pre-pass can be byte-exact against one snapshot. The VRAM test asserts against the static mask - the words identical across every same-scene capture - excluding the runtime-managed NPC palette band around row 479. Incompleteness is not flagged; a wrong texel on a static pixel is.
Two refinements the captures forced: the shared effect-texture band is history-dependent (a handful of pixels hold a boot-resident value until a battle re-uploads the disc bytes), so cells inside it must be static across all scenes; and the world map’s ocean palette rows 506 / 508 / 509 animate, so they are excluded for world-map scenes only.