PSX RAM map
The PlayStation has exactly 2 MB of main RAM, and everything the running game is - its code, the party's stats, the loaded scene, the story flags, the item bag - lives somewhere in that window. This page walks it bottom to top: the executable, the save block, the heap, the two overlay slots the per-mode code is streamed into, and the stack at the very top - then the 1 KB scratchpad that sits outside the 2 MB altogether.
At a glance
- Main RAM
- 2 MB at
0x80000000..0x801FFFFF. Nothing above it:0x80200000+only mirrors RAM on a retail console. - Executable
SCUS_942.54loads at0x80010000and is0x6B800bytes (430 KB): tables, then code frommain()at0x80015E90, then data. It ends at0x8007B800.gp0x8007B318- most globals are reached asimm(gp), which an absolute-address search never sees.- Save block
0x80084140..0x80085B57, the0x1A18bytes a save copies: character records at0x80084708 + n*0x414, flag bank0x80085758, item bag0x80085958.- Heap
0x80091800..0x801C5FFF(InitHeap(0x80091800, 0x134800)): scene bundles, models, the battle context and actor pool.- Overlays
- Slot A at
0x801CE818(field, battle, menu, minigames - one at a time); slot B at0x801F69D8(summon / cast modules, world-map render). - Stack
- Starts at
0x80200000(the startup code replaces the header's0x801FFFF0) and grows down. - Full table
docs/reference/memory-map.md- every row, in the same region order; grep it for your address
The address space
Drawn to scale, low addresses on the left. Click a region to jump to its section.
0x800000002 MB main RAM0x801FFFFF
- Kernel RAM
0x80000000 SCUS_942.54head + code0x80010000- Data segment
0x8006F180 - Static work area + save block
0x8007B800 - Heap
0x80091800 - Resident buffers
0x801C6000 - Overlay slot A
0x801CE818 - Overlay slot B
0x801F69D8 - Free RAM + stack
0x801FA9D8 - Scratchpad (not main RAM)
0x1F800000
0x801C6000top 232 KB, zoomed0x80200000
0x801CE818, which is why an address there means nothing without the mode - and the battle image's extent runs 0x640 bytes into slot B, so a module paged during battle and the battle overlay share that sliver.| Starts at | Size | Holds |
|---|---|---|
0x80000000 | 64 KB | Kernel RAM: exception vectors and the BIOS's own tables. The game never addresses it. |
0x80010000 | 24 KB | SCUS_942.54 image head: jump tables, constant matrices, strings - no code. |
0x80015E90 | 357 KB | SCUS_942.54 code: the game plus the statically linked PsyQ libraries. |
0x8006F180 | 50 KB | SCUS_942.54 data segment: static game tables, then the gp small-data block. |
0x8007B800 | 88 KB | Static work area: uninitialised globals, the save block, the sound-driver work area. |
0x80091800 | 1234 KB | Heap. |
0x801C6000 | 34 KB | Resident buffers: PROT TOC, XA clip table, libcd / SsAPI state, battle actor table. |
0x801CE818 | ≤ 162 KB | Overlay slot A - one per-mode image at a time. |
0x801F69D8 | ≤ 16 KB | Overlay slot B - small paged modules. |
0x801FA9D8 | 22 KB | Free RAM, then the stack from 0x80200000 down. |
0x1F800000 | 1 KB | Scratchpad - separate on-chip RAM. |
0x1F801000+ | - | Hardware I/O registers. |
Where these boundaries come from
The executable's own header fixes the bottom half: t_addr = 0x80010000, t_size = 0x6B800, initial PC 0x80026C28, initial sp = 0x801FFFF0 (which the startup code at 0x80026C74 replaces with 0x80200000), and no BSS (b_addr = b_size = 0) - so nothing above the image end is cleared by the loader. The first jr ra in the image is at 0x80016228 and the last at 0x8006F178, which is what places the code between the table head and the data. main() sets the heap from DAT_8007B414 = 0x80091800 with size 0x134800 (0x200000 on the 8 MB development console), so the heap ends at 0x801C6000. The overlay bases are recovered from each image's own call graph (static overlay pipeline).
The previous version of this map split the executable at 0x80060000, gave it ~960 KB, placed a 256 KB overlay window at 0x801C0000 with a title overlay at its base and an "extended overlay region" past 0x80200000. None of that survives the header and the overlay bases: the title screen is PROT 0899 code at 0x801CE818, 0x801C0000..0x801C5FFF is heap, and there is no RAM past 0x801FFFFF.
How to read an address
- Every address is a PSX virtual address. Main RAM starts at
0x80000000, so0x80084708is byte0x84708into the 2 MB.DAT_8007B83Cis Ghidra's automatic label for data at that address, the wayFUN_8003E4E8labels a function (key functions). - The executable is only the bottom of the picture. Its data holds the persistent state, but most game logic lives in overlays streamed off the disc per game mode.
- An overlay address is ambiguous on its own. Every slot-A image links at
0x801CE818and only one is resident, so the same address means different things in field, battle and menu mode. The overlay rows below say which image they belong to. - A heap address is a snapshot. It depends on the allocation order; the ones listed are those that stay put across the captured states.
0x1F800000is not main RAM. It is the scratchpad, and a flag a script tests may live there.
Kernel and executable
The first 64 KB belong to the BIOS - exception vectors at 0x80000080, the kernel's tables and work area - and the game only reaches them through the BIOS call vectors. One kernel word is game state all the same: 0x80009010 is the BIOS rand() seed every random draw in the game steps, so a save state's word there is where the game's one random stream stands. SCUS_942.54 follows at 0x80010000: 24 KB of tables, the code from main() up, then the data segment.
| Address | Type | Purpose |
|---|---|---|
0x80010778 | u32[71] | Move-VM opcode jump table (move VM). |
0x80010B84 | MATRIX | The constant base matrix the non-field view build folds in: 16384 * I, a 4x scale (the field uses 0x8007BF10, 6x). |
0x800111C4 | u32 | CD / XA transport state word - the 11 states FUN_8003D764 walks. |
0x80014FA0 | u32[132] | Battle-applier selector jump table: 116 of 132 slots point at the shared epilogue; slot 0x82 is the Incense top-up. |
0x80015E90 | code | main() - the first function in the image; the entry stub FUN_80026C28 sets gp and calls it. |
Cheat-code patch sites in the code
A GameShark code that writes 0x2400 to an instruction's upper halfword (address % 4 == 2) turns it into addiu $zero, $zero, imm - a no-op. These make useful Ghidra anchors.
| Address | Effect | Cheat |
|---|---|---|
0x800422F4 | Writes 0001 over the branch inside the inventory-add clamp, shortening it so the clamp always runs: every add stores 99 | Bought Any Item / Find Items You Will Get 99 Quantity |
0x8004309E | No-ops the inventory count decrement | Infinite Items All Slots |
0x8004390E | No-ops a draw-call site | Remove Vahn's Chest |
The fourth code-patch-shaped cheat, Maxed HP for All Characters, writes 0x2400 to 0x8007EA96 - past the image's end, so whatever it patches is not executable code. The field overlay's Walk Thru Walls sites are below.
Data segment and gp block
The executable's initialised data: the static game tables, then the gp small-data block from 0x8007B318, which runs past the image end into the work area. These mean the same thing in every mode.
| Address | Type | Purpose |
|---|---|---|
0x8006F4C8 | i16[2049] | Arctangent table for the bearing resolver FUN_80019B28. |
0x8007050C | char[] | Resident scene-name string - an output of the scene loader, not an input. |
0x8007078C | 24 B × 28 | Game-mode dispatch table (boot). |
0x80070A2C | i16[5120] | Shared trig LUT, 4096 * sin - one revolution plus the quarter turn the cosine view needs. |
0x8007326C | 8 B × 6 | TMD per-mode descriptor table (TMD). |
0x80073F00 | i16 | Dialog pager auto-press countdown: op 4C 89 sets it, and the waiting box presses confirm when it runs out. |
0x80073F14 | u8[8] | Compass axis bitmasks for the motion VM's eight-direction walk: 1 +Z, 2 -Z, 4 +X, 8 -X, combined for diagonals. |
0x80074358 | u32[4] | Global ability bitmask; the party's per-character ability blocks are ORed into it. |
0x80076C10 | u8[0x18] × 103 | Screen-element placement table - the content boxes every battle plate, party panel and Muscle Dome layout derives its chrome from. One table, three names. |
0x80077828 | u8[2] per monster | Per-monster steal table, 1-based id, each entry [chance, item] - chance first. Not in the monster record (steal table). |
0x80077998 | u8[5] per character | Rot limb object ranges: which mesh objects the battle draw dims for each Rot bit. |
0x80078630 | u8[6] per character | Rot CLUT windows: three [lo, hi) palette ranges per character that the status CLUT pass tints blue for each Rot limb bit (battle). |
0x800788B8 | u16[0x110] | Streamed-voice clip durations (arts shout, Seru cast voice); neither reader bounds the index. |
0x8007B450 | ptr / u32 | Op-0x49 submode cell: 0 idle, 1 done, otherwise the active submode descriptor's address. The cheat database's "menu-request register" - poking a record pointer opens a submode. |
0x8007B5FC | u32 | Encounter step counter, one word across doors. Outside the saved window: it holds its SCUS_942.54 file value 0x3CE (974) after power-on and a card load, so the first battle after a cold load in a rate-18 region is rolled on step 55. |
0x8007B6A8 | u8 | Save-anywhere allow flag (scene header bit); the same bit raises the field-fog cap on every overworld. |
0x8007B6F4 | u16 | GTE H projection register (focal length / zoom). The cheat database's "small maps" / "camera mode" labels are this word. |
0x8007B790 | u16[3] | Live camera rotation trio - pitch, yaw, roll (12-bit angles). |
0x8007B7C0 | u32 | Previous frame's held pad mask (the pad reader keeps it to form the edge masks). |
0x80076C10 - one table, three names
Three subsystems index this array and each named it after itself - a battle "pose-slot array", the party-panel "publish target", the Muscle Dome "element layout table". They cite the same base and the same 0x18 stride; a record is a screen-element placement. The clone helper FUN_801D5778 writes dst[+0xA] = src[+0xA] - 0x140, and 0x140 is 320, the display width: the field is a screen X and the operation is "stage this element one screen to the left".
| Offset | Field |
|---|---|
+0x00 / +0x01 | element id, seat A / seat B (usually equal; records 41 / 42 carry them byte-swapped) |
+0x02 / +0x04 | seat A x / y |
+0x06 / +0x08 | content width / box height, shared by both seats (0x0C for the plate-run family; roster panels 50, framed windows 120 / 42 / 28 / 26) |
+0x0A / +0x0C | seat B x / y - the pair FUN_801D5778 pushes by a screen width; which seat is parked is per record, so "from / to" is the safe name |
+0x0E / +0x0F | kind, seat A / seat B |
+0x10 / +0x12 | 13 on the framed-window and roster rows, else 0, and no spawn arm reads it / zero in all 103 records |
+0x14 | content string pointer, measured by the rendered-width kernel FUN_80035F04 into +0x06; the battle name plaque points it at the actor's display-name buffer actor+0x1BC |
103 initialised records run 0x80076C10..0x800775B8; index 129 would be the steal table, and the placement shape stops at 103 (every coordinate within ±416). A record is a content box; the chrome is derived, not stored - pen = (x, y - 2), plate = (x - 8, y - 6) sized (w + 16, 20). Parser legaia_ui_formats::screen_elements; packet evidence on battle.
Static work area and the save block
From the image end at 0x8007B800 to the heap base is uninitialised global storage - the executable declares no BSS, so a cell here holds only what the code stores. It holds, in order, the rest of the gp block, the actor and mesh tables, the save block and the work buffers above it.
Mode, pad and battle globals
| Address | Type | Purpose |
|---|---|---|
0x8007B83C | u16 | Next game-mode index, the master mode selector: 0x02/0x03 field INIT/MAIN, 0x14/0x15 battle, 0x16/0x17 the menu and title (CARD), 0x1A STR FMV. The front end writes it six times on the way from the logo to the field, each store in the handler that hands off. |
0x8007B850 | u32 | Per-frame held button mask, packed (face/shoulder byte in bits 0-7, d-pad/system byte in bits 8-15). |
0x8007B868 | u32 | Dev/dual-mode gate (retail 0); the developer screens it gates are unreachable on a retail disc. |
0x8007B874 | u32 | Newly-pressed edge mask, held & ~prev - what most menus test. |
0x8007B8A0 | s16 | Failed actor-spawn counter behind WORK ERR%d: cleared by the per-stage init, incremented when the actor pool is empty, printed every frame it is non-zero (WORK ERR on Mt. Rikuroa). |
0x8007B8C2 | u16 | Dev/retail loader-path selector: 40 reads, written once at cold boot to 1. Details. |
0x8007B98F | u8 | In-game debug menu enable - byte 3 of the debug-mode word 0x8007B98C. Poking it to 1 brings up the debug menu on SELECT+Triangle. |
0x8007BAC0 | u32 | Special-battle restriction word: bit 0x100 bars Item, 0x200 forbids Ra-Seru; any set bit suppresses the spoils. |
0x8007BAC8 | u16 | BGM request id; the scene loader clears it and the script installs it (audio). |
0x8007BD0C | u8[4] | Formation cell - per-slot monster id (monster m is actor slot m + 3); the AI picker switches on it. |
0x8007BD10 | u8[] | Per-seat character id, 1-based (1 Vahn, 2 Noa, 3 Gala, 4 the AI companion). |
0x8007BD24 | ptr | Battle context pointer (0x800EB654 in battle, 0 in the field): +0x07 action-state cursor, +0x13 active slot, +0x276 the side-band applier's stage byte, +0x279 cast-module phase, +0x28A battle-mode counter. |
0x8007BD71 | u8 | Battle run state: 0xFF running, 0xFE ending, low values the loader's step counter. |
0x8007BD74 | ptr | Side-band streaming buffer: one summon.dat / readef.DAT slot at a time (summon / readef). |
0x8007B8C2 - the build-mode selector
Every one of its 40 read sites splits the same way: non-zero opens assets by PROT-TOC index through the in-RAM table; zero opens by filename through FUN_800608F0, whose whole body is break 0x103 - a PsyQ dev-station host trap retail hardware cannot service. main() writes the halfword once at cold boot:
80015f00 jal 0x8003F084 ; two-instruction leaf: returns 1
80015f04 nop
80015f08 sh v0,0x5aa(gp) ; gp = 0x8007B318, so EA = 0x8007B8C2
The store is gp-relative, which is why sweeps for the absolute lui 0x8008 form reported no writer. BSS zero-init does not establish it - the header declares no BSS. The field-VM flag ops cannot reach it either: their index window is [0x80085758, 0x80086957] and the move VM's signed operand reaches down only to 0x80084758, both above it. A from-scratch engine treats both flags as build-time constants. JP retail uses build-shifted addresses (+0x1B90 for the debug-menu enable).
Actor and mesh tables
| Address | Type | Purpose |
|---|---|---|
0x8007C018 | ptr[N] | Global TMD pointer table, one writer. Entries [0..4] are the five character meshes; the rest is the scene's TMD pack. Details. Its counters and bases (0x8007B774, 0x8007BB38, 0x8007B824, 0x8007B6F8) sit in the gp block. |
0x8007C348 | u32 | Actor allocator free-stack top; seven list sentinels follow. |
0x8007C34C | u32[7] | Actor-list heads the per-frame walk dispatches through node[+0x0C]; five are separate render passes. |
0x8007C364 | ptr | Player field ctx pointer (0x80083794 in the captures; +0x10 carries the encounter-active flag). |
0x80083E58 | ptr[N] | VDF sub-entry pointer table, consumed for actor bring-up. |
0x800840B8 | i32[3] | Camera eye-space translation trio, uploaded as the GTE TR. |
The global TMD pointer table
Every populated entry [0..DAT_8007BB38] is a post-fixup Legaia TMD (magic 0x80000002, group descriptors at +0xC, stride 0x1C). The sole writer is the installer at FUN_80026B4C @ 0x80026BA8; its store through gp[+0x820] is what hides the walker counter from Ghidra's xref database. A settled field-scene snapshot (dolk) holds 143 entries: five character meshes from PROT 0874 section 0, then a contiguous 138-entry scene pack installed by the single descriptor walk. Type-0x05 slot-4 bodies never install here - only dispatcher cases 0x02 / 0x09 reach the installer. Consumers: the world-map top-view renderer, the SCUS and overlay actor allocators, the table walker and the per-party-member equipment-conditional group patch (character mesh).
The save block (0x80084140..0x80085B57)
The live game state a save is composed from - the save write copies these 0x1A18 bytes, so a save-block offset is VA - 0x80084140 (save screen).
| Address | Type | Purpose |
|---|---|---|
0x8008444C | u32 | Persistent fishing points; the lure row, rod stat and point-exchange purchase mask sit beside it. |
0x80084540 | u16 | The loaded scene's raw CDNAME define - two above the extraction-frame index (town01 3). The cheat database's "Map Modifier". |
0x80084570 | u32 | Play-time counter - advances per frame (~60/s), not per second. |
0x800845DC | u32 | Records-page play time: zero until the ending scene's op 4C EE copies the live counter into it. |
0x80084594 / 0x80084598 | u8 / u8[] | Party member count / member ids (sorted insertion, cap 4). |
0x8008459C / 0x800845A4 | u32 | Party gold / casino coin bank. |
0x800845A0 | u32 | 4C D3 clock, remaining ticks; its duration (0x800845B8, non-zero = running), threshold (0x800845BC) and flag pair (0x800845C0: expiry flag high half, below-threshold flag low half) follow (MENU_CTRL). |
0x800845B4 | u32 | Point Card counter (unmapped by every public cheat archive): the shop buy commit accrues price/20 * qty while the card is held, capped at 9,999,999. |
0x800846D0 | u32[4] | Button-mask config words; 0x48 (Cross | R1) is the field run button. |
0x80084708 + n*0x414 | u8[0x414] | Character records, four slots; display name at +0x2A7. Slot 3 (Terra) ends exactly where the flag bank begins (save record). |
0x80085758 | bits | Fourth flag bank (save block +0x1618) - the bitfield field-VM SET / CLEAR / TEST address as (idx >> 3, 0x80 >> (idx & 7)); indices run past 256, so it is not a fixed 256-bit array. |
0x80085958 | (id, count)[256] | Item bag (save block +0x1818); the active window is 128 or 256 slots - never 72, which is a cheat's page span (inventory). |
Work buffers
| Address | Type | Purpose |
|---|---|---|
0x80085B58 | buffer | libsnd sound-driver work area, handed over at boot - the first byte past the save block. |
0x80088758 | record[] | Resident CDNAME define table (16-byte records), what the travel arts scan for a destination. |
0x80089118 | i32[3] | Camera focus trio, stored negated; in the field only X and Z are written. |
0x80089128 | u32[32] | Held-mask history ring, one entry per vsync; its AND drives the menus' auto-repeat. |
0x800917B0 | u32 per VAB id | The fixed SPU address each VAB slot opens at (audio). |
Heap
main() hands 0x80091800..0x801C5FFF to InitHeap, and the allocator serves scene bundles, model packs, sound banks and the battle structures out of it. A heap address belongs to the allocation order, not to the program; these are the ones that stay put across the captured states.
| Address | Type | Purpose |
|---|---|---|
0x800EB654 | struct | The battle context 0x8007BD24 points at while a battle is resident. |
0x800EC9E8 + n*0x2D4 | u8[0x2D4] | Battle actor pool; HP at +0x14C. The actors are reached through the pointer table at 0x801C9370 (cheat citations). |
Resident buffers
The 34 KB between the heap and the overlay base are fixed buffers that survive every mode change - which is why an overlay can hand data to its successor here.
| Address | Type | Purpose |
|---|---|---|
0x801C6220 | rows | Shop buy-list staging array, written by the SCUS row builder; the hoisted band is the Platinum Card stock (shop). |
0x801C6EA4 | ptr | Current world / scene struct pointer; +0x40 is a write-only parked return state. |
0x801C6ED8 | [CdlLOC, u32] × 34 | CD-XA clip table, slot i = file XA<i+1>.XA; built at boot by ISO9660 lookup, so it survives disc relayout (cutscene). |
0x801C70F0 | TOC | In-RAM PROT.DAT TOC, filled at boot (PROT.DAT). |
0x801C8980 | table | Resident monster.snd bank index (PROT 0891), staged by init.pak. |
0x801C8FE0 | u32[] | Steal result band and element-cycling counter - scratch two slot-B cast modules address independently. |
0x801C9370 | ptr[8] | Battle actor pointer table: party in slots 0-2, monsters in 3-7. |
0x801CB408 | record[128] | libcd directory-entry cache. |
0x801CD2B8 / 0x801CD2C0 | u16 / ptr[16] | SsAPI slot-allocation bitmap / per-slot sequence-state pointers. |
Overlay slot A (0x801CE818)
The per-mode code window. Every image links at 0x801CE818, each mode's INIT handler streams its PROT entry in, and only one is resident - the same address is a dialog routine in the field and the battle state machine in battle. The pause menu replaces the field overlay at the same base.
| PROT | Ends at | Image | Loaded by |
|---|---|---|---|
| 0895 | 0x801F4017 | Boot init.pak - publisher logos, front-end hand-off | boot, mode 16 |
| 0897 | 0x801F3817 | Field / town / dialog / world map | mode 2 |
| 0898 | 0x801F7017 | Battle | mode 20 |
| 0899 | 0x801F3817 | Menu: title screen, pause menu, save, shop, casino prizes | mode 22 |
| 0970 | 0x801F3017 | STR / MDEC movie player | mode 26 |
| 0971 | 0x801D0017 | Debug menu | mode 0 |
| 0972..0980 | ≤ 0x801DC817 | Minigames: fishing, slot machine, Baka Fighter, Muscle Dome hub, dance; dev modules | mode 24 warp |
| 0902 / 0979 / 0981 | small | Game over (unreachable), field-to-battle intro, world-map top-view debug | modes 18 / 8 / 12 |
Field overlay (PROT 0897)
| Address | Type | Purpose |
|---|---|---|
0x801D065C … 0x801D078C | code | Walk Thru Walls cheat sites: four beq instructions in the player movement controller's collision path, whose branch displacement the cheat rewrites - a code patch, not collision-state bytes. |
0x801F21B4 | tables | Head of the overlay's data: the actor-collision, wall and interact-facing probe tables. |
0x801F2738 | cells | The dialog pager's row window - scroll, reveal, collapse, rows on the page, pending text (MES). |
0x801F2B98 | record[17] | Panel-window descriptors the world-map panel sizer and dev-menu panels index. |
0x801F3518 | u32 | World-map batch emitter state: running angle, one-shot gate 0x801F351C, scale, step, OT layer. Field-overlay data - reloaded with the overlay, not preserved across a swap (world map). |
0x801F3580 | struct | Camera staging descriptor - the composer's output and the ease's input. |
Battle overlay (PROT 0898)
| Address | Type | Purpose |
|---|---|---|
0x801CED44 | u32[] | Battle action-SM jump table, indexed by ctx[+0x07] (battle action). |
0x801F6950 | u32 | Overlay PRNG state - its draws never perturb the SCUS rand() stream. |
0x801F6980 | u8[8] | Multi-cast value-readout window; PROT 0915 writes it from slot B, reaching down into the battle image. |
0x801F6990 | u32[16] | The arts queue-builder's per-token marks (accepted starter, Super tail-replace). |
Menu and title overlay (PROT 0899)
| Address | Type | Purpose |
|---|---|---|
0x801E46B0 | i32 | Selected item id for the description box (<= 0 draws nothing). |
0x801E46B4 / 0x801E46B8 | u32 | Shop quantity under edit and its cap, min(gold / price, 99, 99 - held). |
0x801E46D0 | u32 | Packed toggle-state word for the options window. |
0x801EF018 | struct | Title state: +0x154 attract countdown (underflow → mode 0x1A → MV1.STR), +0x158 frame counter; +0x204 / +0x208 the sub-mode and previous sub-mode. |
Boot, movies and minigames
| Address | Image | Purpose |
|---|---|---|
0x801F369C | 0895 boot | init.pak's own globals: logo sprite descriptors, memory-card driver state, fade level. |
0x801D0D58 / 0x801D0E60 | 0970 movies | The two MDEC command packets and the hardware-register pointer block (DMA, MDEC, DPCR). |
0x801D9168 / 0x801D9298 | 0972 fishing | Tension gauge / fish life (cheat-pinned); the lure triple is at 0x801D9174. |
0x801D3CAC | 0975 slot machine | Punch-mode unlock (cheat-pinned). |
0x801DBFC4 / 0x801DC06C | 0976 Baka Fighter | Player / computer life (cheat-pinned). |
0x801D1A78 / 0x801D1AE0 | 0977 Muscle Dome | Contest hub arm word / ringside-still latch. |
0x801D53CC | 0980 dance | Dance-points counter (cheat-pinned). |
PROT 0896 links at 0x801D4DF0, not at the slot base, and nothing on this disc loads it.
Overlay slot B (0x801F69D8)
One small module at a time above slot A: PROT 0900 (screen-effect widgets), 0901 (world-map render), 0967..0969 (battle tutorial and boss stages), 0978 (the staged background loader) and the 64 summon / cast images 0903..0966, the largest 0x4000 bytes. The band starts inside the battle image's extent, so a module loaded in the field has its head overwritten when the battle overlay comes in (cast module).
| Address | Type | Purpose |
|---|---|---|
0x801F69D8 | u32 + code | Slot-B link base: the resident module's first word (0x001000E2 when empty). |
0x801F6AA8 / 0x801F6AD8 | tables | PROT 0978's two dispatchers - field restore and int.tim panel still. |
0x801F7A9C | code | PROT 0900's sprite, iris-mask and letterbox widgets that field-VM op 0x43 drives (move VM). |
0x801F9D28 | u32 | PROT 0955's per-arm frame countdown - one example of the module-local cells each cast image keeps. |
Free RAM and the stack
Above the largest slot-B image nothing is loaded. The stack starts at 0x80200000 and grows down; interrupt callbacks run on their own stack at 0x8007A7F0, and the render pass moves onto the scratchpad, so only the main thread's own calls land here. A canary fill over the band measures how deep they go: 0x801FE420 (7,136 bytes) once at boot, while FUN_8001D8FC reads CDNAME.TXT into a 4 KB stack buffer, and never below 0x801FF138 after it - the FMV decoder, which reserves a per-frame macroblock table on the stack. Battles stay above 0x801FFD30, scene loads and card saves above 0x801FFA20, the minigames above 0x801FFA88 bar the dance contest, whose hall load reaches 0x801FF9E0. 0x801FD000..0x801FE000 is therefore free, which is why the disc patcher parks injected code there (randomizer).
No code on the disc builds an address from 0x801FA9D8 up; the stack top comes from a data word. The loader reads exactly as many sectors as the PROT table of contents gives, so a larger PROT 0900 / 0901 would carry its extra bytes into this space on every field load, and they would stay until power-off. The catch is that those two entries have no spare room on the disc, so growing them means moving every later PROT entry, which a same-size patch cannot do. In the same way, while the field or menu overlay is loaded, the range 0x801F3818..0x801F69D7 only holds leftovers from whichever image last used it.
Scratchpad (0x1F800000..0x1F8003FF)
1 KB of fast on-chip RAM outside the 2 MB. Most cells from 0x1F80035C up are displacements off 0x1F800314 (lui 0x1F80; ori 0x314), so an absolute-address scan never sees them.
| Address | Type | Purpose |
|---|---|---|
0x1F800038 | u32 | IR0 the world-map decoration handlers load before their depth cue. |
0x1F800314 | base | Base the cells below are reached from. |
0x1F80035C | i16[16] | Scene floor-elevation ladder - the sixteen heights a collision byte's low nibble indexes (field locomotion). |
0x1F80037C | u8 | Current walk-region kind; 0 disables the camera scroll clamp. The region's AABB follows at 0x1F800384..87. |
0x1F80037D | u8 | Game-speed rate scalar; elapsed time is always the product of this and the tick byte. |
0x1F80037E | u16 | Near cutoff - primitives with an OTZ below it are dropped. |
0x1F800388 | u16[4] | Draw-context 2-D clip rect. |
0x1F800393 | u8 | Per-frame tick byte - how many display frames the last loop took; every timer subtracts it. |
0x1F800394 | u32 | Field-VM transient flag word - not persisted. Ops 0x2E/0x2F/0x30 set/clear/test it; the low 16 bits are re-seeded on every mode switch. Bit 0x40 is scene-transition-pending; bit 0 is set exactly on the kingdom overworlds. |
0x1F8003A0 | ptr | Active primitive / packet write cursor the POLY_* emitters allocate from. |
0x1F8003E8..EB | i8[4] | Camera visible tile window [nearX, nearZ, farX, farZ], set per camera region and by op 0x46. |
0x1F8003EC | ptr | Tile-flag bitmap base; the walkability grid is at +0x4000. |
0x1F8003F4 | ptr | Ordering-table base the overworld ground and fog emitters index. |
Above the scratchpad are the hardware registers - DMA at 0x1F801080..0x1F8010F0, GPU at 0x1F801810, MDEC at 0x1F801820, SPU from 0x1F801C00. The game reaches them through the PsyQ libraries; only the movie player keeps a pointer block to them in its own data.
How we know
Ghidra's reference manager does not resolve MIPS lui+addiu pairs or gp-relative stores, so most rows were pinned by a writer hunt (Ghidra tooling), a save-state diff, or a cheat code that names the cell. The region boundaries come from the executable's header and the overlays' own call graphs.
| Evidence | Where | What it proves |
|---|---|---|
| PS-X EXE header | SCUS_942.54 +0x10 | Load address, image size, initial PC and sp, no BSS |
| Heap init | FUN_8002B3D4(2, 0x80091800, 0x134800) | The heap's base and end |
| Static overlay map | crates/asset/data/static-overlays.toml | Every overlay's base and extent, recovered from its own jal graph |
main() | FUN_80015E90 @ 0x80015F08 | The one gp-relative store that sets the build-mode selector to 1 |
| Pad reader | FUN_8001822C | The held / previous / changed / edge masks |
| Mode-switch prologue | FUN_8001DCF8 @ 0x8001E17C | Sole non-RMW writer of the field-VM flag word's low 16 bits |
| TMD installer | FUN_80026B4C @ 0x80026BA8 | Sole writer of the global TMD table |
| Flag ops | FUN_8003CE08 / _CE34 / _CE64 | The flag bank's addressing |
| Bag window installer | FUN_8004313C | The only writer of the bag's active window |
| Save write | 0x801DFA98 | The save block is 0x1A18 bytes from 0x80084140 |
| Cheat corpus | 557 GameShark / PAR codes | Names the game-state cells directly (cheats) |