Static overlay pipeline
Most of Legaia's gameplay code lives in RAM overlays - chunks of MIPS code the game pages into the
0x801C0000+ window per game mode (title / field / battle / menu / world-map / cutscene / minigames). The dynamic capture workflow reverses them from emulator save states; this page is the static
complement - extract each overlay straight from PROT.DAT (the disc's main archive, where the overlay code ships as ordinary
numbered entries) and disassemble it at its load address, with identity attached from the first byte. No emulator, no save state,
reproducible from any copy of the disc. It complements the dynamic captures; it does not replace them - and like them, it is maintainer
infrastructure: you only need it if you are extending the reverse-engineering itself.
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+.rodataregion. For the battle overlay (PROT 0898) the first0x28800of0x29800bytes match - 100 % of code+rodata - with only the trailing.bssdiverging. For the menu overlay (PROT 0899) the clean prefix is0x15e8cbytes 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 -
0x801DD864is a battle-action function in one overlay and a muscle-dome function in another - which is why the repo disambiguates withoverlay_<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 tableDAT_8007C018;party_count + 2in 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'
jalsites 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 bootinit.pakbundle; 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
jalalone. - 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 atbase + N×0x800, sojal-recovery can latch a phantom base. The canonical entry is the one recovering0x801CE818- 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
0x4000over-read prefix at0x801CA818” - a phantom base: the same image matched inside 0973's over-read tail. The canonical entry is 0975 (recovers0x801CE818, and is what the warp streams). - “Gimard = 0905” - an off-by-2 from reading the loader index math as
param + 0x381. The correct math isparam + 0x37Fin extraction space; Gimard's stager is 0903, and 0905 is the0x83slot. - “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
ineligibleand keep it on the dynamic path - don't force it static. - The fingerprint covers the
read_entryfootprint. 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.