Playthrough coverage
Turn what the game executes into a documentation worklist. A scripted segment of the opening is played under PCSX-Redux with a breakpoint on every function that has a disassembly dump but no explanation in docs/. The functions that fire are reachable by construction, and they are what to document next.
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.
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.
| Environment | Meaning |
|---|---|
LEGAIA_NO_SSTATE=1 | Cold boot from BIOS (segment S1) |
LEGAIA_WORKLIST | Gap-set file (default gap_worklist.txt) |
LEGAIA_ADDR_LO / _HI | Address window, e.g. executable-only with HI=0x801C0000 |
LEGAIA_MAX_BPS | Cap breakpoints (0 = all) for a smoke run |
LEGAIA_INPUTS / LEGAIA_MASH | A pad timeline, or a button pulse that advances titles and dialogue |
LEGAIA_FRAMES | Capture 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
0x801F69D8holds 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
- AuthorPick the start save, the gap-set window and any pad timeline.
- RunProbe headless, retrying past the boot race; produce the CSV and the mode timeline.
- 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.
- 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).
| Seg | Span | Chains from the last by | What it settled |
|---|---|---|---|
| S1 | Cold boot, title, NEW GAME, the opening prologue | The boot-onward driver | How to drive the game from boot headless |
| S2 | Prologue to Rim Elm | Mashing on the field tick | The town scene-load callees |
| S3 | First free walk in Rim Elm | A targeted name-entry driver | The opening parks on the name-entry screen |
| S4 | First door warp, out of Vahn's house | A grid-BFS door navigator | Real collision, no position pokes |
| S5 | First battle | Replaying a recorded human pad timeline | It is the scripted Tetsu spar, not a random encounter |
| S6 | First real fight (the Queen Bee ambush) | Its own field-mode anchor | Command-menu persistence and the field-to-battle transition |
- S3: name entry
- The opening script suspends on a
STATE_RESUMEop 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 islegaia_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
0x801F69D8slot 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.