Overlay capture
When you walk into a town, open the pause menu or start a battle, Legaia quietly swaps a different chunk of program code into the same 256 KB of console memory. Those chunks are overlays, and they hold most of what makes the game a game: the script interpreter behind every NPC conversation, the battle state machine, the whole menu system - none of it in the executable Ghidra sees on the disc. This page is the workflow for getting that code anyway: freeze an emulator at the right moment, slice the overlay window out of its save state, and import the slice into Ghidra.
At a glance
- The window
0x801C0000-0x80200000, 256 KB, shared by every overlay; only one is resident at a time- Slot A
- Field (PROT 0897), battle (0898) and menu (0899) all load at base
0x801CE818; smaller overlays load elsewhere in the window - One-command tool
scripts/ghidra-analysis/auto-name-overlay.py- slices, fingerprints the resident overlay, writes a stub dump script- Emulators
- PCSX-Redux (recommended), mednafen (gzip states), Duckstation (
.sav, zstd) - equivalent dumps - Output
- Function dumps
ghidra/scripts/funcs/overlay_<label>_<addr>.txt(gitignored, Sony-derived) - Static complement
- Static overlay pipeline - clean bytes with disc-pinned identity, no emulator; this page stays authoritative for runtime values
What this solves
Disassembling SCUS_942.54 alone gives you the loader, the renderer wrappers and the sound driver - and a wall of jal 0x801Dxxxx calls into code that is not on the disc as a file Ghidra can open. Overlay capture is the dynamic answer. Reach for it when a function you are tracing lives at 0x801C0000+ and no static image contains it, when you need the runtime values around that code (initialised globals, filled tables), or when you want the exact list of archive entries a scene requested from the loader (the asset-loader CSV).
0x801C0000..0x80200000 out of the state; fingerprint which overlay it isauto-name-overlay.py
4Import at basea named program at 0x801C0000, MIPS LE, auto-analysedimport-overlay-named.sh
5Dump functionsdisassembly + decompiled C per entry point into ghidra/scripts/funcs/dump_<label>_overlay.py
Quick start
auto-name-overlay.py detects the save-state format from the magic bytes (mednafen gzip+MDFNSVST or Duckstation DUCCS+zstd), extracts the overlay window, fingerprints which overlay is loaded against an anchor-function table, and emits both the binary slice and a stub dump_<label>_overlay.py Ghidra script with the largest function entry points pre-seeded.
scripts/ghidra-analysis/auto-name-overlay.py "$HOME/.mednafen/mcs/Legend of Legaia (USA).<HASH>.mc0"# [info] format: mednafen; sliced 262,144 bytes
# [info] auto-detected label: world_map (world_map=4, field=3)
# [ok] /tmp/overlay_world_map.bin
# [ok] ghidra/scripts/dump_world_map_overlay.py
Then import the slice (slice path first, label second - both required) and run the stub:
scripts/ghidra-analysis/import-overlay-named.sh /tmp/overlay_<label>.bin <label>docker compose exec ghidra /ghidra/support/analyzeHeadless /projects legaia -process overlay_<label>.bin -noanalysis -postScript /scripts/dump_<label>_overlay.pyWhen the auto-detection picks the wrong label (the anchor table has no function exclusive to the shop, cutscene or level-up subset), pass --label name; an existing stub is preserved (--force overwrites). To grow the anchor table, add any function you confirm exclusive to one overlay to ANCHOR_FUNCTIONS in the same script.
What's in the overlay window
Two terms recur. Overlays that load at the same virtual address VA-alias: one address names different code depending on which overlay is resident, so a bare FUN_801xxxxx is ambiguous until you say which overlay you mean. Slot A is the primary shared slot the runtime streams the active overlay into - field, battle and menu are VA-alias siblings there, and the mode-24 minigame overlays stream into the same slot.
Capture status
Committed function dumps land under ghidra/scripts/funcs/ as overlay_<label>_<addr>.txt. Several rows share one on-disc source: the save screen and the shop are the menu overlay (0899) captured in different states; the world map and scripted dialogue are the field overlay (0897) resident in different modes.
| Overlay | Named program | Disc source | Key functions |
|---|---|---|---|
| Title screen | overlay_title.bin | - | Actor / sprite VM FUN_801D6628; title tick FUN_801DD35C (see boot) |
| Town / field / dialog / inventory | overlay_dialog_mc4.bin / overlay_dialog_typing.bin | PROT 0897 @ 0x801CE818 | Field/event VM FUN_801DE840, MES renderer FUN_801ED710, inventory hub FUN_801F5748 |
| Field - battle-start transition | overlay_field_battle_intro.bin | PROT 0897 | Partial image captured mid-camera-spin; functions unique to this capture |
| Battle / battle-action | overlay_battle_action.bin / overlay_magic_capture.bin | PROT 0898 @ 0x801CE818 | Per-actor SM FUN_801E295C, battle dispatcher FUN_801D0748, effect-VM cluster |
| Menu (options + all pause screens) | overlay_menu.bin | PROT 0899 @ 0x801CE818 | Items / magic / equipment / status / options UI; equipment stat aggregator FUN_801CF650 |
| Save / load screen | overlay_save_ui_select.bin / _saving.bin | PROT 0899 | Save-screen SM FUN_801DC6B4; 33 sub-state handlers at PTR_FUN_801E4F40 |
| Shop / merchant | overlay_shop_save.bin | PROT 0899 | A menu-overlay capture taken during a shop session, not a separate entry |
| Level-up | overlay_magic_level_up.bin / _full.bin | PROT 0891 | XP / stat-gain UI; full 256 KB re-capture for the data section |
| World map | overlay_world_map.bin / _top.bin / _walk.bin | PROT 0897 | World-map controller FUN_801E76D4, dev-menu renderer FUN_801EAD98 - the field overlay resident during overworld |
| Cutscene / dialogue | overlay_cutscene_dialogue.bin / _mapview.bin | PROT 0897 | The field overlay during actor-scripted dialogue - not the FMV decoder, which is PROT 0970 (cutscene_str) |
| Minigame hub (fishing, slot, Baka Fighter, dance, debug menu) | overlay_fishing.bin and siblings | PROT 0972 / 0975 / 0976 / 0980 / 0971 | Five distinct slot-A overlays that VA-alias one window; controllers in the disclosure below |
| Muscle Dome | overlay_muscle_dome.bin | battle-action overlay | Match controller FUN_801D0748 - the dome is a battle mode, not a card game |
Minigame-hub and Muscle Dome controllers
All five hub minigames are variants of one overlay binary sharing a library core, so a given address may host a different function per minigame - always read the overlay-qualified dump. Per-frame controllers (each a switch-on-state-byte state machine, documented in the per-minigame pages under docs/subsystems/): fishing FUN_801CF3BC, slot machine FUN_801CF0D8, Baka Fighter FUN_801D3468, dance FUN_801CF470. The addresses FUN_801D63B0 / FUN_801D2CC0 / FUN_801D5ED0 / FUN_801D2F38 are the shared textured-quad sprite/HUD emitter the minigames reuse for every draw - their high caller counts reflect that, not control flow.
Muscle Dome: the per-frame match controller is FUN_801D0748 (pad read, phase dispatch on ctx+6). The score tally belongs to the arena hub (FUN_801CF074 / FUN_801D1184, PROT 0977), which runs only between fights, never between turns. FUN_801D5854 is the camera/view director, FUN_801D8DE8 the HUD renderer, FUN_801D388C the presentation driver. See minigame-muscle-dome.md.
Level-up overlay data section
A level-up state re-extracted at the full 256 KB window and imported as overlay_magic_level_up_full.bin gives the data section (dump_levelup_data_section.py):
| Address | Content |
|---|---|
0x801F4B8C | 4-byte display row-ID array for magic slots |
0x801F4B98 | Magic-type name strings (Spirit / Defense / Meta / Terra / Ozma) |
0x801F4C28+ | Battle-result text strings |
0x801F5CF8, 0x801F5D90 | 18-byte move-VM trigger programs, one per burst arm - they reach the particle spawner only via the 0x17 escape inside them |
0x801F6000+ | Live animation state globals (zero at rest) |
Per-character growth is not in this display overlay - it is in static SCUS_942.54 (DAT_800769CC curves + DAT_80076918 param block), applied by FUN_801E9504. See level-up. The "HP grant at Seru +0x74" reading is falsified: +0x74 is the actor's colour word.
History: attributions the capture table used to carry
- The menu overlay was once attributed to PROT
0896; that entry is not a slot-A overlay at all - its once-recovered base was an over-read artifact of the superseded PROT entry-size expression (see prot). - An old PROT 0973 attribution for a hub minigame was its image in 0973's over-read tail; the pinned sources are 0972 / 0975 / 0976 / 0980.
- Earlier readings of the minigame "main entries" and the Muscle Dome "round dispatcher" are collected on do-not-re-walk.
Capturing with PCSX-Redux
Boot with the disc, play to the scene, open File → Show Lua Console, and run ghidra/scripts/dump_overlay.lua - it writes 0x801C0000-0x801EFFFF to /tmp/legaia_overlay_<TIMESTAMP>.bin. Then ghidra/scripts/import_overlay.sh <dump> copies the file into the container and imports it at base 0x801C0000 (MIPS LE, auto-analysed). Copy dumps to a writable container path: /data is bind-mounted read-only; /tmp is what the committed importers use.
For closed-loop probing (breakpoints, watch addresses, per-VSync CSV) rather than a one-off slice, see PCSX-Redux automation.
Mednafen pipeline with asset-loader CSV
scripts/ghidra-analysis/analyze-overlay.sh is the older mednafen flow; use it when you specifically need the asset-loader CSV. It slices the state, re-imports it as overlay.bin (overwriting the previous import - keep separate labels per scene), then scans every jal to a known SCUS asset loader and const-tracks the $a0 argument into /tmp/overlay_loads_<label>.csv.
scripts/ghidra-analysis/analyze-overlay.sh "$HOME/.mednafen/mcs/Legend of Legaia (USA).<HASH>.mc0" --label level_upThe CSV gives the exact PROT entries the runtime loader requests for that scene - it replaces the iterative --vram-extra-dir guesswork in the asset viewer. For the broader save-state toolkit (diff, bisect, scenario manifest), see mednafen automation.
Capturing with Duckstation
Duckstation .sav states are DUCCS magic plus a zstd stream. scripts/ghidra-analysis/extract-duckstation-overlay.py decompresses with the system zstd and locates main RAM by the same anchor-string approach; default slice is the full 256 KB.
scripts/ghidra-analysis/extract-duckstation-overlay.py SCUS-94254_1.sav --out /tmp/legaia_overlay_fishing.binPayload layout, and the VRAM-base trap
The decompressed payload carries more than main RAM at fixed offsets: the cue path string in the uncompressed header, a 256x192 RGBA thumbnail in the first zstd frame, main RAM at payload +0x1A62 in the second, and VRAM (1 MiB) at RAM_end + 0x396 - a GPU-register block sits between. Pin the VRAM base against a landmark before reading coordinates out of it: a base short by that block makes every located CLUT colour appear at one uniform halfword shift, which reads as a "relocation" when it is only an extraction error.
Seven Duckstation saves cover the minigame overlays: fishing, slot machine, Baka Fighter, dance and the debug menu are variants of one binary (2-34 unique prologues between pairs; debug_menu is the superset capture); the two Muscle Dome saves share identical code and only one is imported.
Capture protocol per overlay
The rule everywhere: the overlay is only resident while its screen is on - a dialog box, a level-up banner, a cutscene - so save during it, not after.
| Overlay | When to save the state | Label |
|---|---|---|
| Town / field | Standing in any town map | town |
| Battle | During the action menu (a clean state, not mid-animation) | battle |
| Level-up | While the post-battle level-up screen is displayed | level_up |
| Dialog | As soon as a dialog box appears - the overlay unloads when the box closes | dialog |
| Cutscene | Within the first seconds of playback (XA audio audible, fullscreen) | cutscene |
What to look for after a dialog or cutscene import
- Dialog: glyph-table strings near the base; functions taking a
MES container ptr + msg_id + (x, y)shape feeding the dialog pagerFUN_801D84D0; per-page glyph uploads to VRAM. Field NPC dialogue has no opener function - it is the actor's inline MES walked byFUN_80039b7c(see script-vm). The renderer-side quads land incrates/engine-renderagainst the extracted font atlas (dialog font). - Cutscene: XA-DMA writes to the SPU register region; a mode table indexed by cutscene ID (the cutscene equivalent of the game-mode table at
0x8007078C); per-chapter XA filename strings. The mode-26/27 STR dispatcher is described in cutscene.
Extracting TIMs from a RAM snapshot
A captured RAM dump often contains transient textures (TIMs) staged in main RAM before upload to video memory. When the on-disc source is uncompressed, the staged copy is byte-identical except the RECT fields (VRAM target coordinates), so a texture can be traced back to its source PROT entry: sweep the dump for the TIM magic plus valid flags, decode each hit to PNG to identify it visually, fingerprint 16 bytes of the first CLUT row (skipping the runtime-relocated RECT), and grep the PROT corpus for that fingerprint.
Worked example: the boot publisher logos
A full 2 MiB snapshot taken during the publisher-logo phase contains four TIMs at 0x801D09DC, 0x801DBBFC, 0x801E761C, 0x801EB65C - the publisher / warning screens. All four CLUT fingerprints match 0895_bat_back_dat.BIN at well-separated offsets: PROT 0895 is the boot init.pak bundle, and the bat_back_dat filename label is the CDNAME +2 numbering shift. Documented in boot.
The method works for any transient TIM (battle backgrounds, menu chrome, terrain textures) whose source is stored uncompressed; LZS-compressed sources will not match by direct byte search - decompress first or use a different signature.
Bulk import of static overlay candidates
The find-overlay heuristic ranks PROT entries by likelihood-of-being-overlay-code via addiu sp, sp, -X prologue density (see MIPS overlay detection). scripts/ghidra-analysis/bulk-import-overlays.sh --score 3.5 imports the top candidates at base 0x801C0000 and runs auto-analysis + the inventory dumper; per-overlay function inventories land in ghidra/scripts/inventory_overlay_<stem>.bin.csv. Bulk import only gives function lists - the static overlay pipeline is the successor, importing each overlay at its recovered base with the PROT entry as identity.