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).

1Play to the momentthe overlay you want is resident (town, battle, menu, minigame)emulator 2Save statea frozen snapshot of the console's full 2 MiB RAMPCSX-Redux / mednafen / Duckstation 3Slice the window0x801C0000..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.py

When 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.

OverlayNamed programDisc sourceKey functions
Title screenoverlay_title.bin-Actor / sprite VM FUN_801D6628; title tick FUN_801DD35C (see boot)
Town / field / dialog / inventoryoverlay_dialog_mc4.bin / overlay_dialog_typing.binPROT 0897 @ 0x801CE818Field/event VM FUN_801DE840, MES renderer FUN_801ED710, inventory hub FUN_801F5748
Field - battle-start transitionoverlay_field_battle_intro.binPROT 0897Partial image captured mid-camera-spin; functions unique to this capture
Battle / battle-actionoverlay_battle_action.bin / overlay_magic_capture.binPROT 0898 @ 0x801CE818Per-actor SM FUN_801E295C, battle dispatcher FUN_801D0748, effect-VM cluster
Menu (options + all pause screens)overlay_menu.binPROT 0899 @ 0x801CE818Items / magic / equipment / status / options UI; equipment stat aggregator FUN_801CF650
Save / load screenoverlay_save_ui_select.bin / _saving.binPROT 0899Save-screen SM FUN_801DC6B4; 33 sub-state handlers at PTR_FUN_801E4F40
Shop / merchantoverlay_shop_save.binPROT 0899A menu-overlay capture taken during a shop session, not a separate entry
Level-upoverlay_magic_level_up.bin / _full.binPROT 0891XP / stat-gain UI; full 256 KB re-capture for the data section
World mapoverlay_world_map.bin / _top.bin / _walk.binPROT 0897World-map controller FUN_801E76D4, dev-menu renderer FUN_801EAD98 - the field overlay resident during overworld
Cutscene / dialogueoverlay_cutscene_dialogue.bin / _mapview.binPROT 0897The 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 siblingsPROT 0972 / 0975 / 0976 / 0980 / 0971Five distinct slot-A overlays that VA-alias one window; controllers in the disclosure below
Muscle Domeoverlay_muscle_dome.binbattle-action overlayMatch 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):

AddressContent
0x801F4B8C4-byte display row-ID array for magic slots
0x801F4B98Magic-type name strings (Spirit / Defense / Meta / Terra / Ozma)
0x801F4C28+Battle-result text strings
0x801F5CF8, 0x801F5D9018-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_up

The 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.bin
Payload 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.

OverlayWhen to save the stateLabel
Town / fieldStanding in any town maptown
BattleDuring the action menu (a clean state, not mid-animation)battle
Level-upWhile the post-battle level-up screen is displayedlevel_up
DialogAs soon as a dialog box appears - the overlay unloads when the box closesdialog
CutsceneWithin 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 pager FUN_801D84D0; per-page glyph uploads to VRAM. Field NPC dialogue has no opener function - it is the actor's inline MES walked by FUN_80039b7c (see script-vm). The renderer-side quads land in crates/engine-render against 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.

See also