At a glance

Static analysis cannot tell a dead function from a hot one nobody has written up; a trace can. Early segments (boot, the first field frame, dialogue) mostly hit code that is already documented, and the unexplained hits concentrate in battle internals, story scripting and NPC interaction - so success is measured as the gap-set shrinking per segment, not as raw coverage.

gap-setdumped, undocumented save-state anchorcatalogued, fingerprinted trace probe1 exec breakpoint each hits CSV + modesattribute by containment read, document, tagcite the address in docs/ leaves the gap-set on regenerate end state becomes the next segment's anchor
One turn of the loop per segment. The only input that cannot be synthesized is a save state at a new place in the story, which is where a human at the pad helps.

The harness under it is PCSX-Redux automation; the gap-set is defined off the port catalog, whose dashboard prints its live size.

The gap-set

GAP-SET = dumped AND NOT documented AND NOT ignored
  • dumped - a Ghidra dump exists, so the address is a function entry that can be broken on.
  • not documented - no file under docs/ cites the address, so breakpoints go only to the unexplained residue.
  • not ignored - not host-replaced PsyQ / BIOS / libgte / libspu code from the catalog's ignore list, which is out of the port's scope and hot every frame.

scripts/pcsx-redux/build_gap_worklist.py regenerates gap_worklist.txt (add --bucket scus for the executable alone) after any new dump or doc lands.

Naming a pending target documents it

"Documented" means the address hex appears in any file under docs/. Naming a still-open target in prose silently drops it from the gap-set, so refer to a pending target by cluster or mode and cite the hex only once the function is characterized.

The harness

autorun_trace_segment.lua arms one non-pausing exec breakpoint per gap-set address, plays a segment, and writes trace_segment.csv (addr, hits, first_frame, first_mode, first_ra, stem) plus a game-mode timeline. It is passive - it records whatever runs - so an input timeline is optional.

EnvironmentMeaning
LEGAIA_NO_SSTATE=1Cold boot from BIOS (segment S1)
LEGAIA_WORKLISTGap-set file (default gap_worklist.txt)
LEGAIA_ADDR_LO / _HIAddress window, e.g. executable-only with HI=0x801C0000
LEGAIA_MAX_BPSCap breakpoints (0 = all) for a smoke run
LEGAIA_INPUTS / LEGAIA_MASHA pad timeline, or a button pulse that advances titles and dialogue
LEGAIA_FRAMESCapture budget in vsyncs

Running it headless. Run under xvfb-run -a (a real display throttles an unfocused window) with LEGAIA_BOOT_DELAY=2, and anchor on a catalogued, fingerprinted save from the immutable library through run_probe.sh --scenario <label> - never a live quicksave slot, which the next save overwrites. About 150 breakpoints arm at once; more stalls the emulator, so trace_scenario.sh captures the gap-set as a union of windowed passes. The probe writes its CSV incrementally, so a late emulator abort still leaves the hits on disk.

Driving from boot. autorun_play_from_boot.lua builds the checkpoint corpus: it skips logos and the intro, confirms NEW GAME, advances the opening dialogue and saves a checkpoint at a target game mode, then resumes from a checkpoint to drive the next segment. Launch with -fastboot (the default boot path stalls on an early CD read headless). It ticks on two per-frame exec breakpoints rather than on vsync, because the title's streamed music stops vsync delivery to the script while the game keeps running. Checkpoint at field run, not field init.

Attribution: executable vs overlay

An executable address (0x800xxxxx) is always resident, so a hit there means that function ran. An overlay address (0x801C0000 and up) is VA-aliased: several overlays occupy the same window, so a hit only means "the resident overlay executed this address".

Attribute by containment, not by stem. The CSV's stem is whichever overlay the dump of that VA came from, usually not the resident one. The right identity is the function of the resident overlay whose range contains the hit; the hit's game mode picks the overlay. attribute_overlay_hits.py does this and flags each function as documented or new.

  • Several hot addresses inside one range are interior PCs of one function, not several functions.
  • A co-resident overlay slot is identified by comparing live RAM with the static image, and its occupant varies by context: the slot at 0x801F69D8 holds the sparring-tutorial overlay in the first fight and the effect-render overlay in a normal one.
  • Matching bytes in RAM prove the bytes are resident, never that they executed there: the same VA can be a data table in one overlay and code in another.

The capture and triage loop

  1. AuthorPick the start save, the gap-set window and any pad timeline.
  2. RunProbe headless, retrying past the boot race; produce the CSV and the mode timeline.
  3. TriageIn hit-count order: read the dump, understand the function, document it on its subsystem or format page with a key functions row, and tag the crate that consumes it.
  4. CatalogueBack up the end state as the next segment's start and cite its fingerprint in the ledger.

Segment ledger

The segments chain: each one's end state is the next one's anchor. Every anchor is a retail capture shot on an unpatched image, catalogued by fingerprint (save states are gitignored; only fingerprints are cited).

SegSpanChains from the last byWhat it settled
S1Cold boot, title, NEW GAME, the opening prologueThe boot-onward driverHow to drive the game from boot headless
S2Prologue to Rim ElmMashing on the field tickThe town scene-load callees
S3First free walk in Rim ElmA targeted name-entry driverThe opening parks on the name-entry screen
S4First door warp, out of Vahn's houseA grid-BFS door navigatorReal collision, no position pokes
S5First battleReplaying a recorded human pad timelineIt is the scripted Tetsu spar, not a random encounter
S6First real fight (the Queen Bee ambush)Its own field-mode anchorCommand-menu persistence and the field-to-battle transition
S3: name entry
The opening script suspends on a STATE_RESUME op that hands off to the name-entry state machine, which holds the player with no dialog box until a name is confirmed. Mashing Cross only appends letters; the driver holds the accept option and pulses Cross. The port's mirror of the screen is legaia_engine_menus::name_entry.
S4: door navigation
The controller reads the scene's walkability grid, runs a BFS from the player's tile, visits the boundary tiles where door triggers live nearest first, and learns the pad-to-world mapping online. Read struct fields at their real width: the player's X and Z are 16-bit, and a 32-bit read folds the facing word into the coordinate.
S5: the first battle
Rim Elm has no random encounters at that point in the story. The first fight is the sparring tutorial, started by choosing the third entry of Tetsu's four-item list - which a mash-only driver never selects - so it was captured by recording a person's pad input and replaying it.

Guarding an oracle that reads the library

A test that consumes these anchors has three gitignored inputs: the disc image, extracted/ and the save-state library. The repo's rule is that a disc-gated test skips and passes when its data is missing, and that means every input it reads. A per-anchor skip inside the loop looks guarded, but with no library at all every anchor skips, the count of checked anchors stays zero, and the non-vacuity assertion fails. The fix is a directory-level skip before the loop, which keeps the assertion meaningful for a library that lost one anchor.

Detect it by removing the root rather than reading the source: hide saves/, run every test that reads it, and anything that fails rather than skips has a guard narrower than its inputs.

What the traces resolve

A documented function leaves the gap-set on the next regenerate, so the burndown shows in the worklist. What follows is what the traces settled.

  • Always-resident executable hits are mostly infrastructure: library code the host replaces (libgte, libcd, libgpu, the heap), a developer profiler HUD, no-op stubs. The game logic concentrates in the overlays.
  • Field hits land on the per-actor tick path - the loop that runs each actor's inline-dialogue machine, motion VM and move-table VM - and its hottest function is the per-actor visibility cull. That the trace lands on the central per-frame loop is evidence the harness reaches the main path.
  • Mode 24 hits are the Noa dance overlay, its dance-floor render group (dance minigame).
  • The first battle surfaced the always-resident on-screen-element family (party status HUD, the floating damage-number list, per-actor markers) and new battle-overlay draw functions, among them the Arts announcement banner and an arena scatter routine Ghidra had never made a function.
  • The first real fight surfaced the Arts command-block persistence pair and the enemy target menu builder, showed the 0x801F69D8 slot holding the effect-render overlay, and caught the field-to-battle transition state machine running in the field overlay's battle-intro image.

The functions are catalogued on key functions; the per-hit tables are on the full page.