Boot path: internals
The reference tables behind the boot path tour: the init sequence stage by stage, the full mode table, the CD-read stack, the title overlay's state struct and pad masks, where its code sits on the disc, the name-entry grid, and every boot-screen texture with its source rectangle.
The init sequence
main() (FUN_80015E90) is called once by the entry stub FUN_80026C28 with $gp = 0x8007B318. Each stage below is bracketed by a dev-checkpoint print that is compiled in but inert on the shipped disc.
| Init stage | What happens |
|---|---|
| GPU / display | display-mode reset, display mask off, full-frame clear, queue flush |
| CD + XA | CD driver init; the console probe result (gp+0x550) selects the heap size |
| Sound | libsnd init over the sound-driver work area (audio) |
| Heap | 0x134800 bytes retail, 0x200000 on an expanded-RAM dev console |
| Boot scene + names | default scene string opdeene; the dev/retail halfword gets its only write (debug flags); CDNAME primes the name table at 0x80088758 |
| Display env + mode machine | DISPENV / DRAWENV pair, boot mode-init, packet + ordering-table allocator, one priming frame, display on, first overlay pair streamed |
Switching modes. When a handler writes a mode index different from the latched one, the loop stops XA / CD, flushes the GPU queue, re-reads the pad and clears the held-input and frame-state globals before latching the new index. A negative mode index exits the loop - a dev quit path retail never takes.
The full mode table
A 28-entry table at 0x8007078C, 24-byte stride: even rows are INIT handlers, odd rows per-frame handlers. It is static executable data, so legaia_game_tables::mode_table reads it straight off the disc (asset mode-table). The dev names in the table mislead; the third column is what each pair really is.
| Mode | Disc name | What it really is | Overlay loaded | Init handler |
|---|---|---|---|---|
| 0/1 | CONFIG | dev debug menu | 0971 | 0x80025C68 |
| 2/3 | MAIN | field / town gameplay | 0897 | 0x80025B64 |
| 4/5 | MONSTER TEST | dev; INIT bounces back to mode 0 | - | 0x8002611C |
| 6/7 | TMD TEST | dev | - | 0x801CF730 |
| 8/9 | EFECT TEST | dev | 0979 | 0x80025E68 |
| 10/11 | TEST | dev | - | 0x8002B97C |
| 12/13 | MAPDSIP | world-map display | 0981, over 0897’s head | 0x80025DA0 |
| 14/15 | MAP TEST | dev | - | 0x8002B904 |
| 16/17 | READ | publisher logos (init.pak) | slot A already holds 0895 | 0x8002612C |
| 18/19 | GAME OVER | game over | 0902 | 0x80025B30 |
| 20/21 | BATTLE | battle; setup is resident in the executable | none (0898 via the field path) | 0x800565D8 |
| 22/23 | CARD | title screen and the menu - pause menu, save, options | 0899 | 0x8002574C |
| 24/25 | OTHER | minigames, one overlay per sub-id | 0972..0977, 0980 | 0x80025980 |
| 26/27 | STR | FMV movies | 0970 | 0x80025FB4 |
- 12 of 14 per-frame handlers are one shared tick (
0x80025EEC), parameterised by the entry’s+0x14word. Only world map (13) and menu (23) carry their own. - Mode 23 runs no frame driver. Its per-frame handler (
FUN_80025F74→FUN_80017978) calls an 18-instruction stub instead of the master frame driverFUN_80016444, so no actor, effect, move VM or animation advances while the pause menu is up. - World map is a head swap. Mode 12’s INIT saves the first
0x4000bytes of the field overlay, loads PROT 0981 over them and restores them on exit; the world-map controller proper stays in 0897 (world map). - The mode word does not name the minigame. Modes 24/25 host all five warp minigames; the discriminator is the signed halfword
_DAT_8007BA34, written by the field VM’s door-warp arm asop0 − 100and left standing while the minigame runs. Key on the pair, never on0x19alone - and0x18is the one-frame INIT half. Full chain: asset loader. - Overlay index arithmetic. Both overlay loaders resolve
param + 0x381in the raw TOC, which is extraction entryparam + 0x37F(the +2 shift). No call site can produce params 0 or 1, so extraction entries 0895 / 0896 are unreachable from any static loader call.
Entry layout and the bare INIT handlers
Each 24-byte entry is (name_ptr, …, next_mode: i16 at +0x0A, handler_ptr at +0x10, param at +0x14). next_mode is -1 (self-managed) or 0 (fall back to mode 0) - the only two retail values; the word at +0x08 reading 0xFFFF0000 is that -1 over a zero low half, not a sentinel.
Three INIT handlers skip the “reset, wait, load slot A, jump in” wrapper. Mode 4 writes mode 0 and returns. Mode 16 jals 0x801CE9C0 without loading anything, because slot A already holds what it jumps into: PROT 0895 (init.pak) at file +0x1A8, the routine that draws the publisher logos before writing mode 0x11. Mode 20’s FUN_80055B6C is the battle scene setup, resident in the executable.
Slot B’s default (FUN_80025BA0) is the only runtime-decided load: it mirrors the summon-render flag DAT_8007B6A8, picks extraction 901 when set and 900 otherwise, and skips the load when that overlay is already resident. Two other constant loads: the effect-data init uses raw 0x3D5 (= extraction 979) and the CARD-mode init raw 0x37E (= 892, a container-formats::pack of TIMs); legaia_overlay_images::boot_overlay ports the arithmetic and pins each by content.
History: off-by-2 loader arithmetic, and mode 16 as a “stripped dev path”
Reading + 0x381 as an extraction index placed the mode-0 overlay at 973 (the 1-sector OTHER2 dev module), the mode-2 overlay at 899 (the menu) and the minigames at 0896. All three are the same +2 shift; see do-not-re-walk. Mode 16’s jump target was once read as “an entry point in no image, so a retail-stripped dev path”; that was VA aliasing at the shared slot-A base.
Archive directory and CD reads
The TOC is read exactly once: the first three sectors of PROT.DAT land at 0x801C70F0. After that, “give me entry N” is a table lookup, and “give me h:\PROT\FIELD\town01\…” is a CDNAME name lookup followed by the same table lookup. An entry’s size is the sector gap to the next entry, exactly what retail’s own span routine computes (PROT).
| Resolver | Input | Used by |
|---|---|---|
| index-based | PROT entry number → LBA | streaming loader, both overlay loaders |
| path-based | dev-style path → CDNAME index → the index resolver | most retail code paths (data\battle\efect.dat, scene paths) |
Every load ends as “seek to a sector, DMA bytes into RAM” through Legaia’s own layered CD driver - hardware registers and callbacks, not the PsyQ libcd path. Every loader is dual-mode, keyed on the dev/retail flag: the retail branch takes the TOC-index path, the dev branch opens a host path through a break trap real hardware cannot service. The port reads a disc image directly, so the register / interrupt tier is documented rather than ported; the async loaders and the streaming queue above it are ported.
The CD-read API stack, bottom-up
| Function | Role |
|---|---|
FUN_8005D9A0 | CD-DMA-channel-3 synchronous read primitive; the DMA trigger is at 0x8005DA50 inside it |
FUN_8005C42C / FUN_8005C328 | BCD-MSF ↔ LBA, standard (min*60 + sec)*75 + frame − 150 |
FUN_8005DBB4 | ISO9660 directory lookup: filename → {msf[3], size, …} |
FUN_8005E788 / FUN_8005E574 | streaming-read starter + per-IRQ callback; cursor DAT_800796CC, sectors left DAT_800796D8, LBA DAT_800796E4 |
FUN_8005E9A4 | public streaming read (sector_count, dest, mode_flags); sector size 2048 / 2336 (XA) / 0x246 by mode_flags & 0x30 |
FUN_8005EA84 | completion sync; 0x4B0-vsync overall timeout, a 0x3C-vsync sector stall restarts the read |
FUN_8005E4D4 / FUN_8003D3C4 | sync LBA reader; path-based ISO9660 loader for .STR / .XA files |
FUN_8003E4E8 / FUN_8003E68C | boot-time TOC loader; entry span = gap to the next entry |
FUN_8003E8A8 / FUN_8003E6BC | index-based and path-based LBA resolvers |
FUN_8003E800 | async LBA loader; queues via gp+0x97C / gp+0x894, kicked by FUN_8003F128 |
FUN_8003DDA0 | index-based streaming enqueue: 8-byte (idx, byte_offset) descriptors at gp+0x1A8, cursor advanced by size << 11 |
FUN_8003DAA8 | load-kick / completion driver (in-progress flag _DAT_8007B876 & 1) |
FUN_8003EBE4 / FUN_8003EC70 | overlay loaders A / B; differ only in destination pointer and current-id tracker (0x8007BC3C / 0x8007BC4C); 16 static call sites |
FUN_8005DAB0 / FUN_8005D5F8 / FUN_8005D648 | CD response dispatcher (cause 0x4 data-ready, 0x2 completion), re-arm, init / reset |
The BIOS-trampoline stubs in this cluster (FUN_8005BD30 = B(0x07), FUN_8005DB9C = B(0x3F), the FUN_80056648 family) are syscall shims with no game logic.
The title tick
| Piece | Where | Note |
|---|---|---|
| tick function | FUN_801DD35C, in the overlay | 3,026 instructions; the first ~250 fan out through a 25-entry sub-mode jump table at 0x801CF244 |
| state struct | 0x801F0000 (+ a sibling block at 0x801EF014..0x801EF200) | sub-mode selector at +0x204; cursor grid at +0x1F4 / +0x1F8; linear cursor at +0x1FC |
| attract countdown | 0x801EF16C | starts at 0x8000 (a disc byte, not computed), minus the per-frame scalar each tick; underflow writes mode 0x1A and FMV id 0 → MV1.STR (cutscene) |
| menu sub-mode | 0x10 | the Press-Start wait; confirm fades through 0x18 into the two-row cursor state 0x14 (cursor _DAT_8007B820). A cold boot always arrives here: init.pak raises the entry word _DAT_8007BB00 at 0x801CEB84, which routes Init to 0x11 and then 0x10 |
| launch | sub-modes 0x16 fade → 0x06 init_game | writes mode word 2 at 0x801DFC00 and scene id opdeene into 0x8007050C; CONTINUE detours through the card-load path, and its 0x16 writes mode 2 at 0x801DFAFC |
The countdown store is sw v0, -0xe94(a0) at 0x801DDCCC, in the delay slot of a bgez that keeps the attract loop running while the counter is non-negative; the per-frame scalar it subtracts is the byte at _DAT_1F800393. That store is where a write watchpoint on the countdown fires.
The extended state block
| Address | Offset | Use |
|---|---|---|
0x801EF14C | -0xEB4 | slider X; step = scalar × 8. Both arms clamp at 0x2C (floor at 0x801DFC88, ceiling at 0x801DFCB4), so it converges on 0x2C from either side |
0x801EF160 | -0xEA0 | fade / sweep accumulator, clamp 0x1000 |
0x801EF170 | -0xE90 | frame counter, incremented every tick |
0x801EF190 / 194 / 1A0 | -0xE70.. | alpha A / B / C, clamp 0x1000 |
0x801F0204 | +0x204 | sub-mode dispatcher index |
0x801F0230 | +0x230 | top-of-tick early-out guard |
The tick draws through three executable-side helpers: the ClearImage rect-fill queue, the MoveImage VRAM copy, and the sprite-descriptor dispatcher ({ tag, flags, rect, pixel_data_ptr }, alpha-OR pre-pass under flags & 8, width divisor from flags & 3).
The packed pad mask
The per-frame button mask (_DAT_8007B850) and newly-pressed mask (_DAT_8007B874) are packed by the pad builder FUN_8001822C as ~((pad[2] << 8) | pad[3]): the face / shoulder byte in bits 0..7 and the d-pad / system byte in bits 8..15 - not the raw PSX pad word. Anything that ingests retail RAM-side input must translate.
| Bit | Button | Bit | Button |
|---|---|---|---|
| 0 | L2 | 8 | Select |
| 1 | R2 | 9 | L3 |
| 2 | L1 | 10 | R3 |
| 3 | R1 | 11 | Start |
| 4 | Triangle | 12 | Up |
| 5 | Circle | 13 | Right |
| 6 | Cross | 14 | Down |
| 7 | Square | 15 | Left |
Masks the title tick tests: 0x44 = L1|Cross (confirm), 0x21 = L2|Circle (cancel), 0x844 = Start|L1|Cross (press start), 0xF5 = any face button + L1 + L2.
The title overlay on disc
PROT 0899 is streamed raw and uncompressed from its LBA (47227) across its 74 sectors, in five DMA bursts that all land inside the entry.
| PROT.DAT range | Sectors | Contents |
|---|---|---|
0x5C3D800..0x5C44800 | 47227..47241 | 0899 head: options / config-menu strings + small config TIMs |
0x5C44800..0x5C62800 | 47241..47301 | 0899 tail: title-overlay code + data, incl. two embedded UI TIMs |
0x5C62800.. | 47301.. | 0900, the slot-B summon-render pair |
| Burst | RAM | Entry offset |
|---|---|---|
| 1 | 0x801CF818 | +0x1000 |
| 2 | 0x801D4818 | +0x6000 |
| 3 | 0x801D9818 | +0xB000 |
| 4 | 0x801DD018 | +0xE800 |
| 5 | 0x801E4818 | +0x16000 |
The tick body sits at entry offset +0xEB44, byte-identical to the captured title overlay. Two TIMs are embedded in the data segment: 0x801E5120 (entry +0x16908, the save-menu UI atlas) and 0x801EE120 (+0x1F908, the memory-card icon strip - save screen).
History: the “unindexed 60-sector gap”
A superseded entry-size expression gave 0899 only 14 sectors, and the missing 60 became a “gap the TOC misses”. The real size is the gap to the next entry, 74 sectors. The same artifact produced every “hidden overlay after entry N” reading; see PROT.
The opening chain in detail
The intro skip
The skip lives in FUN_801D1344. Each frame it tests three things: no dialog is up (_DAT_8007B868 == 0), scratchpad flag bit 26 is set (_DAT_1F800394 & 0x4000000), and confirm is held (_DAT_8007B850 & 0x100). When all three hold it starts a 60-frame fade-out, writes the town01 entry coordinates (0xEC0, 0x2DC0) into _DAT_80073EF4 / _DAT_80073EF8, clears bit 26 so the skip fires once, and calls the name-based scene change FUN_8001FD44 with the overlay literal town01 at 0x801CE82C.
- The scene-change packet copies the target name into
0x8007050C, syncs it to the active buffer0x80084548and stages the load;s_ERR_CHANGE_PACKETguards re-entry. It is not the map-id door warp (op0x3E), which backs up the active scene name into0x8007BAE8- empty in thetown01opening saves. - The arm is the last record of
opdeene’s third partition (MAN offset0xA47, the2E 1Aat0xA5E);town01carries no such arm. - Each leg’s opening record spawns via op
0x44SPAWN_RECORD (the three op scenes) or the walk-on tile trigger (map01,town01). An exec breakpoint on the spawnerFUN_8003BDE0sees exactly five spawns, one per leg. Subtitle pages roll through the bottom-up crawl roller (FUN_80037174) as a child context, so camera cuts play under the scroll.
History: superseded readings of the skip
The skip was first read as the required hand-off from the prologue into the field; the chain advances by itself and the gate is a skip. A guess that a 0x4C MenuCtrl sub-op in record 0 armed the bit is falsified - record 0 has no 0x2E / 0x2F byte at all.
Name entry
Opened in town01 by field-VM op 0x49 sub-op 3 in the opening timeline record (P2[3], body offset 0x02C6). The grid is 6×17 ASCII cells in three column groups (upper / lower / digits); the cursor walks a 7×17 space whose last row is Backspace / Space / End, and End on a non-empty name opens a Yes/No commit. The committed name lands at record +0x2A7.
| Piece | Where |
|---|---|
| Grid / live buffer | 0x801F29F0 / 0x801F2A6C |
| Cursor | 0x8007BB88, linear 0..0x77; d-pad deltas ±0x11 / ±1, separators skipped; name width capped at 57 px |
| Renderer | FUN_801E6B34 |
| State machine | FUN_801F03F0, 5-entry jump table at 0x801CF71C (init → interactive → three confirm handlers) |
| Parked op state | _DAT_8007B450 holds the op’s RAM address + 1 while the prompt is up |
The fresh-state seed (FUN_80034A6C / FUN_800560B4) sets gold to 500, zeroes a ~0x200-byte story-flag region and expands the starting-party template into the live records.
Boot-screen textures
Publisher logos - PROT 0895 init.pak
| Offset | Logo | Mode | Stored | Drawn as |
|---|---|---|---|---|
+0x021C4 | PROKION | 8 bpp | 176×256 | two 176×126 quads, (144, 165) and (320, 165) |
+0x0D3E4 | Contrail | 8 bpp | 184×256 | one 184×254 quad at (228, 105) |
+0x18E04 | SCEA Presents | 4 bpp | 256×128 | two 252×64 quads, (68, 192) and (320, 192) |
+0x1CE44 | WARNING | 4 bpp | 256×256 | uploaded, drawn by nothing |
Each logo’s quads come from a six-record sprite-descriptor table that follows the fourth TIM, so PROKION and SCEA are vertically packed: the top half and the bottom half draw side by side, meeting on the 640×480 stage’s centre. Pacing: SCEA 16 / 131 / 32 frames (fade in / hold / fade out), Contrail 16 / 121 / 16, PROKION 16 / 91 then a 65-frame ramp to a white screen. The warning’s descriptor (record 1, tpage 0x000B / clut 0x7E80) is the one id none of the five emit calls passes; a cold-boot capture through the logos, the title, the attract movie and back logs 857 quad emits and none for it, and the menu overlay later parks the memory-card kanji page on top of its VRAM rect.
Title wordmark - PROT 0890
One 256×256 8-bpp sheet at file offset 0x14228 (66,080 bytes with its CLUT; byte-matches RAM 0x80170DF8) carries the orb, wordmark, prompt and copyright lines. Retail samples it in bands rather than blitting the quad.
| Source rect | Content | Drawn when |
|---|---|---|
(0, 17, 256, 124) | orb + wordmark | every post-fade phase |
(96, 151, 64, 10) | demo-build label | never - a demo leftover the framebuffer omits |
(60, 178, 196, 16) | PRESS START BUTTON | press-start phase only |
(4, 195, 244, 14) | trademark line | every post-fade phase |
(8, 209, 234, 14) | copyright line | every post-fade phase |
(0, 226, 256, 11) | NEW GAME / CONTINUE footer | menu rows, sampled as two halves; selection is bright vs dim, no arrow cursor |
History: “three multi-bank duplicates” of the title TIM
Three sources (0888 @ 0x1AA28, 0889 @ 0x19A28, 0890 @ 0x14228) all land on the same absolute PROT.DAT offset; they looked like three entries only under the pre-correction entry size. A header-signature scan across the archive returns one hit, inside 0890.
System-UI textures: raw TOC entries 0 and 1
A 118-sector region between the TOC (ends at 0x1800) and extraction entry 0 (0x3C800, sector 121) is skipped by the extraction index, but not by the TOC: it is raw TOC entries 0 and 1, which are exactly CDNAME's two head names, init_data and gameover_data. They have different loaders.
| Raw index | CDNAME | Dev-tree file | Loaded by |
|---|---|---|---|
| 0 (sectors 3..55) | init_data | init\init.pak | the boot image (PROT 0895), state machine FUN_801CECD0 arm 2; arm 3 uploads every TIM |
| 1 (sectors 55..121) | gameover_data | gameover\gameover.pak | the GAME OVER overlay (PROT 0902) only |
2 (extraction 0000) | - | title\title.pak | the boot image, arm 6 |
Each dev-tree name is the operand of the developer-build branch of a load whose retail branch reads the raw index beside it, so the pairing is read straight off the instructions. Raw entry 0 holds the small-caps menu font, the boot cursor and a few sprite strips - 4-bpp TIMs targeting the bottom-right corner of VRAM. Raw entry 1's one page (0x1B80C) is the GAME OVER screen's, and no retail field, battle or menu save state holds its palette; the port's scene upload leaves it out.
| PROT.DAT offset | Dims | VRAM target | Purpose |
|---|---|---|---|
0x01858 | tiny | (896, 256) | boot cursor variant |
0x018E0 | 256×192 | (896, 256) | large UI sprite sheet |
0x07F40 | 256×256 | (896, 0) | dialog-font sheet |
0x10178 | 256×32 | (896, 448) | AP / status-icon strip |
0x11218 | 256×256 | (960, 256) | menu-glyph small-caps font |
0x19438 | 240×24 | (960, 400) | UI sprite strip |
0x1B80C | 256×256 | (640, 0) | raw entry 1: the GAME OVER page |
The atlas at 0x11218 (33,312 bytes) is the font the shop / inventory / status panels sample; its in-RAM copy at 0x80106478 is byte-equal modulo CLUT relocation. Alphabet row at y = 224..238, 26 cells 8 px wide from x = 8; digits at y = 209..220. Retail switches CLUT rows per context (white / gold / dim). The title’s own NEW GAME / CONTINUE rows do not use it - they are the footer band of the wordmark sheet.
How we know
| Function | Address | What it proves |
|---|---|---|
| main loop | FUN_80015E90 | init order, heap sizes, the mode-switch housekeeping, the loader flag’s only write at 0x80015F08 |
| entry stub | FUN_80026C28 | sets $gp = 0x8007B318, calls main once |
| CDNAME primer | FUN_8001D8FC | 16-byte name records at 0x80088758; unbounded copy |
| asset dispatcher | FUN_8001F05C | type byte in the high 8 bits of the size word; handler table 0x80010638 |
| mode table | 0x8007078C | 14 INIT / RUN pairs, 24-byte stride, read straight off the disc |
| boot mode chain | six sh stores to _DAT_8007B83C | 0x8001D5B8, 0x801CEC94, 0x801CF4D4, 0x80025974, 0x801DFC00, 0x80025E50 - one per hand-off |
| minigame init | FUN_80025980 | reads sub-id _DAT_8007BA34 twice, leaves it standing |
| title tick | FUN_801DD35C | countdown store at 0x801DDCCC (write watchpoint); launch write at 0x801DFC00 |
| title overlay source | CD-DMA FUN_8005D9A0 | five bursts from LBA 47227, all inside entry 0899; uncompressed |
| field INIT / scene init | FUN_80025B64 / FUN_801D6704 | loads 0897, reads the scene id, writes mode 3 |
| intro skip | FUN_801D1344 / FUN_8001FD44 | flag-and-pad-gated one-shot; name packet to town01 |
| menu-glyph atlas | RAM 0x80106478 | byte-matches PROT.DAT 0x11218 |
| system-UI loader | FUN_801CECD0 (PROT 0895) | FUN_8003EB98(0, buf, 1) at 0x801CEDA8 / 0x801CEEF8, then a TIM_LIST walk into FUN_800198E0 |
Full write-up: docs/subsystems/boot.md.