Why static extraction works

PSX overlays are normally clean copies of a fixed-VA-linked blob: the loader DMAs the bytes into the overlay window, runs FlushCache, and jumps in - there is no per-load relocation. Legaia's overlay code ships as MIPS-code entries inside PROT.DAT (the mips_overlay / overlay_ptr_table detectors flag the small ones; the big scene overlays are raw too, just data-section-first). So the on-disc entry is the loaded code, modulo the runtime-written .bss.

This is proved two ways:

  • Static reproducibility. The as-loaded bytes extracted from any copy of the disc hash to a committed sha256 (asset overlay verify). No Sony bytes are committed - only the hash.
  • Runtime byte-match (disc + save-state gated). The on-disc bytes are byte-identical to the resident RAM image over the entire .text+.rodata region. For the battle overlay (PROT 0898) the first 0x28800 of 0x29800 bytes match - 100 % of code+rodata - with only the trailing .bss diverging. For the menu overlay (PROT 0899) the clean prefix is 0x15e8c bytes across six menu-open save states.

What it buys (and the limit)

  • Solves the VA-aliasing identity problem structurally. Many overlays link to the same VA (virtual address) range - 0x801DD864 is a battle-action function in one overlay and a muscle-dome function in another - which is why the repo disambiguates with overlay_<label>_<addr> naming. Statically, an overlay is “PROT entry N at base X”: identity from the source entry, not a guessed label.
  • Reproducible from the user's disc, with no curated save state - including overlays nobody ever captured.
  • It does not unblock runtime-value captures. Values an overlay only holds at run time - e.g. gp[0x754] (a $gp-relative global: the base index where the effect-model library starts in the mesh pointer table DAT_8007C018; party_count + 2 in battle), watchpoint results, ctx[+0x274] bytes - still need live probes. This is a workflow + coverage + identity win; the dynamic captures stay authoritative for runtime values.

Base recovery

The load base is recovered statically from the overlay's own internal jal call graph - the graph of MIPS jal (jump-and-link, i.e. function-call) instructions recovered from the overlay bytes. For the true base B, every internal call target T maps to file offset T - B, which begins a function prologue (addiu sp, sp, -X). Tallying B = T - prologue_offset over every (distinct-call-target, prologue-offset) pair, the true base wins by a landslide: the field overlay recovers 0x801CE818 with 60 corroborating call targets; battle with 44.

The recovery is decisive enough to pin overlay identity. The options/menu overlay is PROT 0899 at 0x801CE818 - found by byte-searching the corpus for FUN_801CF650's instruction signature, RAM-verified across six menu-open save states. PROT 0899 and the field overlay (0897) are VA-alias siblings in slot A: both load at 0x801CE818 at different times, so 0x801CF650 is a "Give" string in 0897 but the equip aggregator in 0899. That is the exact aliasing this pipeline exists to disambiguate.

Two guard facts bound what the recovery can claim:

  • Subtract aliased regions first. When an entry's on-disc footprint over-reads a known overlay, the carried-in bytes are self-consistent at their own base and can fix the vote by construction. PROT 0896's whole-file recovery returns a convincing base for exactly this reason (the votes come from field-overlay bytes in its over-read tail); restricted to its own head, the recovery yields no landslide, so 0896's true link base is unrecovered.
  • Static loader census. A full-image scan of both overlay loaders' jal sites finds no call site that can produce param 0 or 1, so extraction entries 0895/0896 are unreachable from any static loader call. (An extraction entry is the extraction-output index: the raw in-RAM PROT TOC index minus 2.) Extraction 0895's content is the boot init.pak bundle; 0896 remains an unidentified blob - see boot · game-mode state machine and the open-threads 0896 row.

Slot A vs slot B

The overlay loaders manage two independently swappable slots.

Slot A - the big scene overlays

Slot A (~0x801CE818) holds field (0897), battle (0898), menu (0899), the STR/MDEC cutscene overlay (0970), and the minigame overlays (fishing 0972, slot machine 0975, baka fighter 0976, dance 0980 - the mode-24 door-warp sub-id slots, see script-VM § 0x3E WARP). All are VA-alias siblings: same base, resident at different times.

  • The field / battle / menu / cutscene rows recover from jal alone.
  • The minigame rows are cross-checked instead by a documented minigame function landing on a prologue at the base (anchor_va), because their footprints over-read each other: one minigame's code is duplicated across consecutive entries at base + N×0x800, so jal-recovery can latch a phantom base. The canonical entry is the one recovering 0x801CE818 - which is also the entry the warp actually streams.
  • The “world-map”, “save”, and “shop” UIs are not separate entries: the overworld controller lives in field 0897, and the save + shop sessions live in menu 0899 (each function's signature byte-matches only that entry via find-sig).

Slot B - summon / effect / minigame-data blobs

Slot B (link base 0x801F69D8) holds the player-summon / effect / minigame-data blobs from the 0900..0969 PROT cluster. These timeshare one buffer, so a save state catches an inseparable mix of two overlays - there is no clean whole-overlay RAM prefix, and most have too sparse a call graph to recover. Their base is cross-checked the slot-B way: a high fraction of the overlay's internal absolute self-pointers (lui 0x801f/0x8020 ; addiu) resolve in-file only at the committed base. This is precisely where static extraction earns its keep: the disc entry disassembles cleanly at the link base even though the runtime buffer is unusable.

The cluster is heterogeneous. The summon-stager entries follow one loader arithmetic: extraction entry = loader param + 0x37F, in extraction space (the extraction-output index space defined above). Both spell blocks are capture-pinned per spell id, zero exceptions:

Spell id(s) Cast Extraction entries Identity
0x81..=0x8B Player Seru magic (Gimard = 0x81, …) 0903..=0913 Capture-pinned per spell id. The deep-dived 38-spawn-call stager file is 0905 (the 0x83 slot). 0907 is Nighto's stager - its head title “Hell's Music” is the attack's display name (the SCUS spell table carries the same string).
0x99 Evil Seru Magic (Juggernaut) 0927 “Dark Eclipse” head title = the attack's display name, the same pattern as Nighto's.
0x9A..=0x9D Sim-Seru summons Palma / Mule / Horn / Jedo 0928..=0931 Pre-linked pointer-table heads, no title.
0x9E..=0xA0 Ra-Seru trio Meta / Terra / Ozma 0932..=0934 Pre-linked pointer-table heads, no title.

Non-spell residents of the cluster: summon-effect data (0957 - a summon string table, not a dance song) and the rare-Seru flute summons 0924/0925 (Lippian / Spikefish, capture-pinned; “Ultimate Rave” is Lippian's failed-kill banner), with 0926 the unused 0x98 one-sector stub.

The GAME OVER overlay (0902) is not slot B - its old slot-B row was a pointer_resolution false positive: 44/48 of its pointers are external references to fixed battle-band structures that happen to fall inside the slot-B window, scoring 91% at a base it never loads to. Its true base is slot A 0x801CE818, pinned by the mode-18 loader chain and by in-file string anchors (the gameover.pak path, the GAME OVER caption). The hardened check, string_anchor_votes, counts pointers that decode to the start of one of the file's own string literals - a vote only the true base can win - and the reproducibility test fails any anchor-less row the rival slot out-votes.

How the labels were corrected (do not re-walk)
  • “PROT 0896 = options/menu overlay” - wrong. 0896 (bat_back_dat) is not a menu overlay; the real one is PROT 0899. 0896's convincing 60-vote recovered base came from the field overlay's bytes carried in its over-read tail, and a live mode-24 entry capture ruled out the “mode-24 OTHER overlay” reading - the SCUS-resident OTHER INIT streams each minigame's own overlay directly into slot A.
  • “Slot machine = 0973 with a 0x4000 over-read prefix at 0x801CA818 - a phantom base: the same image matched inside 0973's over-read tail. The canonical entry is 0975 (recovers 0x801CE818, and is what the warp streams).
  • “Gimard = 0905” - an off-by-2 from reading the loader index math as param + 0x381. The correct math is param + 0x37F in extraction space; Gimard's stager is 0903, and 0905 is the 0x83 slot.
  • “0907 = Disco King dance song” - refuted. “Hell's Music” is Nighto's attack display name; the dance overlay (0980) has no slot-B loader callsite - its music is sequenced BGM.

The committed map

crates/asset/data/static-overlays.toml is the entry→base map - one record per overlay:

Field Meaning
prot_index PROT.DAT entry the overlay is extracted from (the identity).
base_va Load base inside the overlay window; statically recovered, RAM-confirmed where a capture exists.
form raw (entry bytes are the as-loaded bytes) or lzs (decompress; needs decompressed_size).
clean_copy_bytes Length of the RAM-verified .text+.rodata prefix (for verified rows).
eligibility verified (RAM byte-matched) / static (base-recovered + function-anchored, not RAM-prefix-verified) / ineligible (runtime-relocated - keep on the dynamic path).
base_source How base_va was determined: jal (internal call-graph recovery - default; the reproducibility test asserts the recovery agrees), capture (byte-matched a resident RAM anchor/region), cross_ref (taken from another pinned RE result in-tree).
anchor_va Optional known function VA that must land on a function prologue (addiu sp, sp, -X) at base_va - a capture-free, disc-reproducible base cross-check. Decisive for cross_ref/capture rows where the jal-recovery assertion is skipped (e.g. a slot-A minigame sibling anchored by a documented minigame function).
fingerprint_sha256 sha256 of the as-loaded bytes - the disc-derived reproducibility anchor.
notes Which subsystems / entry points live here.

The map spans the slot-A scene family (field / battle / menu + cutscene + the minigames) and the pinned slot-B entries (summon render + the capture-pinned stager blocks 903..913 and 927..934, GAME OVER, the attack-titled 0924, summon-effect data).

CLI

# Inspect the map.
asset overlay list

# Reconnaissance sweep: recover each entry's base + print its leading dev
# string (the identity tell). --base filters to one overlay slot.
asset overlay scan extracted/PROT.DAT --from 895 --to 985 --base 0x801CE818

# Locate a function-head signature across the corpus and infer the host
# overlay's base (the capture-free byte-search that pins an entry).
asset overlay find-sig extracted/PROT.DAT "1e80043c a046838c" --anchor-va 0x801DC6B4

# Re-extract from your PROT.DAT and assert every committed fingerprint
# reproduces (bit-for-bit, from any copy of the disc).
asset overlay verify extracted/PROT.DAT

# Extract each eligible overlay's as-loaded bytes to a gitignored dir.
asset overlay extract extracted/PROT.DAT --out extracted/overlays

# Emit Ghidra import helpers: a per-overlay Jython rename script + a shell
# driver that imports each overlay at its base, program named overlay_<label>.
asset overlay ghidra --out extracted/overlays

# Regenerate map rows (recover bases + hash bytes); review before committing.
asset overlay generate extracted/PROT.DAT --index 897 --index 898

The extracted .bins are Sony overlay code (gitignored); only the map (PROT index → base → label + sha256 hashes) and the docs are committed. A statically-extracted-and-disassembled overlay reproduces the same functions at the same addresses as the captured overlay_<label>_<addr>.txt dumps - asserted against the disc bytes and against live RAM in disc-gated tests.

Scope + limits

This is a complement to the dynamic captures, not a replacement. Three limits define where it stops:

  • Only clean-copy overlays qualify. The byte-match catches the exceptions: an overlay whose on-disc bytes do not match the resident image is runtime-relocated or runtime-constructed. Mark it ineligible and keep it on the dynamic path - don't force it static.
  • The fingerprint covers the read_entry footprint. For a few entries that footprint over-reads adjacent shared sectors past the real overlay, because the on-disc entries overlap. The over-read tail is harmless noise in the disassembly; the real functions still land at their real addresses.
  • It does not address runtime values. For gp-relative globals, watchpoint results, and anything the overlay constructs at run time, the dynamic-capture workflow and the PCSX-Redux probes remain essential and authoritative.

See also