Slot-4 records (world-map mesh library) Inferred
Each of Legaia's three overworld continents (Drake, Sebucus, Karisto) ships as a "kingdom bundle" - a per-continent asset bundle whose numbered slots hold the map's textures, meshes, and data. Slot 4 is this page's subject: a per-kingdom library of small object-local 3D meshes, rendered as PSX triangles and quads by the world-map renderer. Fair warning before you chase the obvious idea: the tempting reading - that these records are 2D map data, the continent's coastline wireframe or the dev-menu top-view outline - is falsified. Rendering every body on every projection plane produces no map silhouette in any kingdom; see Falsified readings before re-opening that thread.
What the records actually are: 3D vertices. The container layout below is byte-verified
against live RAM, the consumer is pinned, and the per-record semantic is decoded: each 8-byte record is a GTE
vertex (the GTE is the PlayStation's geometry coprocessor - all 3D transforms go through its registers) - the
cluster-A prim handler FUN_80044c14 (a Ghidra-traced function; FUN_80xxxxxx names
are addresses) loads each record's two words straight into the GTE vertex registers (VXYn = x |
y<<16, VZn = z) and RTPT-transforms them, so
x/y/z are model-space coordinates and the 4th i16
(attr) rides in the discarded high half of the VZn word and is not a coordinate. The
slot-4 body is read in place, per frame, by the world-map renderer (ra
0x801F78D4) - there is no record → working-buffer transcode. See Consumer call sites.
The bulk continent terrain itself - the ~4300 POLY_FT4 prims that tile the kingdom continent in the dev-menu top-view - is not sourced from slot 4. It comes from the same kingdom slot-1 TMD pack the landmarks draw from, routed through FUN_80043390's overlay-mode dispatch table at 0x801F8968 whose eight high-mode renderers replace the SCUS variants when the world-map overlay is paged in. See subsystems / world-map - bulk continent terrain emit mechanism.
Overview
Slot 4 of each world-map (kingdom) bundle decompresses to a fixed-size buffer that the runtime loads verbatim into RAM. (PROT.DAT is the disc's single big archive; the "PROT index" is the bundle's entry number in it. CDNAME is the disc's name map, which is where the map01..03 labels come from.) Three carriers:
| Bundle | PROT index | CDNAME label | Decoded size |
|---|---|---|---|
| Drake | 0085 | map01 | 32304 |
| Sebucus | 0244 | map02 | 26964 |
| Karisto | 0391 | map03 | 24444 |
The 7-asset bundle is the standard scene_asset_table shape with type sequence (1, 2, 3, 4, 5, 6, 7). Slot 4's type byte is 0x05 per asset-type - the standard table calls this "MOVE", but the kingdom-bundle consumer interprets each body as an object-local mesh (8-byte header + GTE vertex pool + command stream), not a move table (see Per-record semantic below).
Container layout (confirmed)
Outer pack
+0x00 u32 count ; number of sub-bodies
+0x04 u32 byte_offsets[count] ; absolute byte offset into the
; decoded payload (NOT word offsets,
; unlike the slot-1 TMD pack)
+offset bodies[count] ; contiguous sub-bodies
Sub-body header (8 bytes)
+0x00 u8 count_a ; records per group
+0x01 u8 flag_a ; usually 0; 1 for kind=4 bodies
+0x02 u8 count_b ; number of groups
+0x03 u8 flag_b ; usually 0
+0x04 u16 marker ; constant 0x080C across all bodies
+0x06 u16 kind ; 1, 2, or 4 (body class/scope tag)
Body payload
+0x08 record[count_a * count_b] ; each record is 8 bytes
; ( i16 x, i16 y, i16 z, i16 attr )
+... trailer (8 bytes) ; always 8 zero bytes
Total body size is always 8 + count_a * count_b * 8 + 8. The math fits every body in all three kingdoms exactly.
Per-record semantic - each record is a GTE vertex (decoded)
The 8-byte records are an object-local vertex pool: each record is a 3D vertex the cluster-A prim handlers transform through the GTE. Traced from the handler FUN_80044c14 (one of the per-kind emitters the dispatch table reaches from FUN_80043390). Each per-kind handler receives three arguments; its third (param_3 below) is the base of the body's 8-byte-stride vertex pool - the record region:
// pool word pairs loaded straight into the GTE vertex registers:
puVar = pool + (cmd_index & 0x7ff8); // index = byte offset, 8-byte stride
setCopReg(2, 0x0000, pool_word0); // VXY0 = (i16 X | i16 Y << 16)
setCopReg(2, 0x0800, pool_word1); // VZ0 = (i16 Z) in the low half
// ... V1, V2 the same ...
copFunction(2, 0x280030); // RTPT - perspective-transform 3 verts
copFunction(2, 0x1400006); // NCLIP - backface cull
So a record's first word is the GTE VXYn register (X low, Y high) and its second word is VZn (Z in the low 16 bits). Mapping the 8 bytes back to the parser's i16 fields:
| bytes | field | role |
|---|---|---|
| 0..1 | x | model-space X - GTE VXYn low half |
| 2..3 | y | model-space Y - GTE VXYn high half |
| 4..5 | z | model-space Z - GTE VZn |
| 6..7 | attr | high half of the VZn word - not a coordinate; the GTE vertex load ignores it |
The X/Y/Z ranges bear this out (e.g. Sebucus body 0: X∈[-20224,-3598], Y∈[-6416,4351], Z∈[-17649,20992] - object-local mesh extents). The triangle topology is not in the body: the per-kind handler reads its vertex indices (the & 0x7ff8 byte offsets) from a separate cluster-A command stream and indexes them into this pool, then emits a POLY packet.
kind (1/2/4) - a body class/scope tag (characterized)
kind is the body header's +0x06 field and is not the dispatcher's prim-kind (8..19, computed from a separate command word). Hashing each body's bytes across the three kingdoms shows kind partitions bodies by scope and class:
kind = 1- the three leading bodies (0, 1, 2) are byte-identical across all three kingdoms (sha256-matched): a shared, universal mesh set present on every kingdom map.kind = 2- full-3D kingdom objects. Some are kingdom-specific; a trailing cluster (Drake bodies 9–11 ≡ Sebucus/Karisto bodies 12–14) is byte-identical across all three - another globally shared set. Other kind-2 bodies are shared between adjacent kingdom pairs.kind = 4- always carriesflag_a = 1(the onlyflag_a = 1exception in the corpus is one kind-2 body). Often the widest-extent meshes (Drake body 13 reaches the ±32 K world bounds); shared between kingdom pairs.
So a kingdom's slot 4 is a per-kingdom assembly drawn from a shared mesh library (kind 1 universal + shared kind 2/4) plus kingdom-specific bodies, with kind tagging each body's category. Degenerate bodies (count_a = 1, all-zero records) are empty placeholder slots.
attr - a per-vertex value the render path ignores (characterized)
The 4th i16 is genuinely per-vertex (not constant within a count_a group), is not a function of the vertex position (corr(attr, x/y/z) ≈ 0.1), and is not a copy of a neighbouring vertex's coordinate. It rides in the high half of the VZn word, which the GTE vertex load discards. A full sweep of every cluster-A handler (FUN_80043658..FUN_80045988) confirms none extract the pool word's high half - every >> 0x10 in the family is either a vertex-index extraction or an output-packet / RTPT-screen write, never a read of the pool word1 high half. So attr is render-unused reserved per-vertex data: the 43 >> 0x10 hits in the cluster-A family are all command-word index extraction, and each record word1 is loaded whole into the GTE VZn register with the high 16 bits discarded (grep puVar[1] >> 0x10 = zero). Real per-vertex authoring data (135 distinct values in one Sebucus body) that the engine never reads on any render path.
Per-kingdom body inventory
Drake = 15 bodies; Sebucus = 16; Karisto = 16. The leading three bodies (kind = 1, count_a = 10) are byte-identical templates across all three kingdoms - whatever they encode, the engine ships the same generic shape in every bundle.
Drake (map01, PROT 0085)
| Body | count_a | count_b | kind | flag_a | records | X span | Y span | Z span |
|---|---|---|---|---|---|---|---|---|
| 0 | 10 | 20 | 1 | 0 | 200 | 16626 | 10767 | 38641 |
| 1 | 10 | 20 | 1 | 0 | 200 | byte-identical template (matches Sebucus / Karisto) | ||
| 2 | 10 | 30 | 1 | 0 | 300 | byte-identical template | ||
| 3 | 2 | 30 | 2 | 0 | 60 | pinned plane (degenerate) | ||
| 4 | 2 | 20 | 2 | 0 | 40 | pinned plane (degenerate) | ||
| 5 | 10 | 30 | 2 | 0 | 300 | 25 unique of 30 groups | ||
| 6 | 10 | 26 | 2 | 0 | 260 | |||
| 7 | 10 | 30 | 2 | 0 | 300 | 25 unique of 30 groups | ||
| 8 | 10 | 3 | 2 | 0 | 30 | 3 identical groups (filler / padding) | ||
| 9 | 12 | 30 | 2 | 0 | 360 | 10725 | 25856 | 21248 |
| 10 | 12 | 30 | 2 | 0 | 360 | 13056 | 18432 | 31503 |
| 11 | 12 | 10 | 2 | 0 | 120 | 11492 | 27648 | 24064 |
| 12 | 10 | 120 | 2 | 0 | 1200 | 16118 | 4096 | 31473 |
| 13 | 14 | 15 | 4 | 1 | 210 | 65485 | 14336 | 64512 |
| 14 | 2 | 30 | 2 | 0 | 60 | |||
Bodies 9 / 10 / 11 have Y spans comparable to the X/Z scale - 3D mesh extent, not 2D contour data. Body 12 is nearly flat (Y span 4K). Body 13 reaches the full ±32K world bounds on X and Z and clusters in the corners.
Sebucus (map02, PROT 0244)
16 bodies; same leading three templates as Drake. Bodies 8-11 are all kind = 4 with flag_a = 1 (more boundary-style bodies than Drake's single body 13).
Karisto (map03, PROT 0391)
16 bodies; same leading three templates. Bodies 4-9 + 11 are kind = 4 with flag_a = 1. Karisto body 10 is anomalous: kind = 2, flag_a = 1 (the only non-kind=4 body with flag_a = 1 across all three kingdoms).
RAM layout (confirmed)
Slot 4 is loaded verbatim into RAM with zero per-byte diffs vs disc, and the resident base varies per kingdom - each is pinned by byte-matching the disc-decoded payload against a post-warp full-RAM dump (all bodies agreeing unanimously):
| Kingdom | bundle | resident base | end (excl.) | bytes | bodies matched |
|---|---|---|---|---|---|
| Drake | map01 / 0085 | 0x8011A624 | 0x80122454 | 32304 | 15/15 |
| Sebucus | map02 / 0244 | 0x80119CE4 | 0x80120638 | 26964 | 16/16 |
| Karisto | map03 / 0391 | 0x80108D84 | 0x8010ED00 | 24444 | 16/16 |
Body 0's records start 0x40 past the base (after the 4-byte count and 15–16 × 4-byte offsets). No runtime fixup is applied. Because the base is not constant, a probe that arms breakpoints on the slot-4 RAM window must locate the base for that kingdom first.
Verified by scripts/pcsx-redux/diff_slot4_ram_vs_disc.py against a PCSX-Redux save state (every byte of all Drake bodies matches the disc-side LZS-decoded payload); the per-kingdom base table extends that with a full-RAM dump (autorun_dump_full_ram_hold.lua drives the warp, then dumps post-warp RAM) plus a unanimous body-vote base search (locate_slot4_base.py).
Falsified readings
Three encoding interpretations are falsified by visual inspection (PNG renders of every body × every projection plane × every topology mode) - do not re-open them without new evidence:
- Top-down dev-menu wireframe / continent coastline (body 12 = coastline, body 13 = world boundary frame). Projecting all 15 bodies onto
xzproduces no recognizable map silhouette in any kingdom, and no render matches the dev-menu top-view captured from PCSX-Redux save states. count_a × count_bheightfield grid. Rendering Drake body 12 as a coarse 10 × 120 grid quad-mesh wireframe produces the wrong silhouette; pair-wise edge interpretation ((r0,r1) (r2,r3) ...) likewise yields no coastline-like contour.- A single non-
xzprojection.xysurfaces clean pillar silhouettes for bodies 9 and 11 (25K-27K Y span), but no single axis pair produces a coherent map across all 15 bodies - the recognizablexyshapes are 3D objects seen sideways, not map outlines.
Superseded probe readings, kept as warnings:
- "Slot 4 is transcoded / overwritten by TMD-pack installs." An intermediate model held that cluster A read TMDs at addresses that had once held slot-4 bytes. Wrong on two counts: the register snapshots that placed cluster-A reads in the
0x801BA000working buffer belong to the separate per-actor renderer stream, and the "slot-4 window full of TMDs" snapshot was a field-scene (dolk) capture whose0x8011AxxxTMDs are that scene's field-file pack, not the kingdom slot-4 buffer. The in-place Read-watchpoint capture (see Consumer call sites) retired the transcode framing. - "Sebucus shows no render reads." A wrong-base assumption - re-read against the byte-verified Sebucus base, 171 of 177 reads land inside the slot-4 window.
- "Mid-load table entries past the walker counter are content." Historical "non-TMD entry classification" passes sampled stale pointers past
DAT_8007BB38; those characterisations were out-of-bounds reads, not table contents.
Consumer call sites
Two distinct SCUS-resident reader clusters consume slot 4. Both are byte-identical across all three kingdoms - same PC ranges, same caller RAs - proving the consumer is generic SCUS code, not per-kingdom overlay code.
Slot 4 is read in place - there is no transcode. A Read-watchpoint tiled across the Drake slot-4 RAM window (driving the held-Up warp from the drake_castle_to_worldmap save) captures the slot-4 records being read directly by the renderer: of 365 captured rows, 363 are slot-4 reads by the cluster-A GTE mesh path (none a copy), with the faulting addresses spanning almost the whole window and the live a1/a2 register pointers inside the slot-4 window. The return addresses are 0x801F78D4 (the world-map top-view overlay renderer) and 0x8001BC8C (the SCUS render path). The streaming-chunk dispatcher FUN_8001E54C fired only twice and never with a slot-4 pointer - so it does not copy the slot-4 records. The dispatcher capture corroborates this: of 2153 FUN_80043390 calls during the Drake warp, 762 take their command pointer a0 from inside the slot-4 window, so slot-4 is walked in place as command streams, not just as the vertex source. A Read-watchpoint on body 0's header (the count/kind words) during the warp catches the cluster-A handler chain reading it in place (PC inside FUN_80045584, ra 0x801F78D4), confirming each slot-4 body is a self-contained render packet (8-byte header + indexed vertex records) the renderer walks in place. Sebucus and Karisto are byte-pinned resident at their own bases (see RAM layout) and a Sebucus warp confirms the same renderer drives cluster-A there too, with 171 of 177 captured reads inside the byte-verified Sebucus window.
| Reader | Entry / PC range | Caller RA |
|---|---|---|
Cluster A - TMD-style primitive renderer (FUN_80043390 + per-kind handlers) | dispatcher entry 0x80043390; per-kind handler bodies at 0x80043658..0x80045988 | 0x8001B47C (inside FUN_8001ada4), 0x801F78D4 (world-map overlay) - both in every kingdom |
| Cluster B - secondary mid-body reader | 0x80059DE4 | 0x80059C00 (SCUS) - identical across all three kingdoms |
Cluster-B provenance caveat: the dump file ghidra/scripts/funcs/80059de4.txt is mislabeled - its function entry is FUN_80059BD4, a generic VRAM LoadImage DMA, not a slot-4-specific reader. The Exec-bp hits at 0x80059DE4 fall inside that DMA routine, so “cluster B” is the generic image-upload path incidentally touching the slot-4 RAM window, not a dedicated slot-4 consumer. The real slot-4 render path is cluster A.
FUN_80043390 (712 bytes / 178 instructions) takes three arguments:
void FUN_80043390(struct *display_state, u32 cmd_flags, u32 fade_flags);
// display_state[0] -> vertex pool base (a2 in handlers = param_3)
// display_state[3] -> non-zero gates the color/light-modulation path
// display_state[4] -> command-stream pointer (where slot-4 records feed in)
It reads one u32 command word from display_state[4] (the command stream pointer), extracts a 15-bit kind from bits 17-31 and a 16-bit count from bits 0-15, optionally re-arms the GTE colour registers, and tail-calls a per-kind handler through a jump table. Each handler consumes its own command's primitive batch (count items of a kind-specific stride), emits GP0 packets into the active primitive pool, then chain-calls the next kind handler at the same dispatch point - the renderer is a TMD-style display-list walker, not a fixed-size record loop.
Two parallel handler tables drive the dispatch: SCUS handlers at 0x8007657C (default), or overlay handlers at 0x801F8968 when _DAT_1F800394 & 1 is set (the alternate route for the bulk-terrain pipeline). Within the SCUS table the dispatcher adds a bank offset to the kind*4 index per the literal disassembly of FUN_80043390:
_DAT_1f800028 = 0;
if (fade_flags != 0) { // param_3 != 0
_DAT_1f800028 = 0x50; // baseline
if ((cmd_flags & 0x04000000) != 0) _DAT_1f800028 = 0xA0;
if ((cmd_flags & 0x20000000) != 0) _DAT_1f800028 = 0xF0;
}
So there are four banks, not three. The two ifs are sequential (not else-if), so the 0x20000000 branch wins when both flags are set. And bank 0 / bank 1 are gated by fade_flags, not by cmd_flags bits:
fade_flags | cmd_flags bits | Bank offset | Effect |
|---|---|---|---|
== 0 | (ignored) | 0x00 | bank 0 - kinds 12-19 use the small 0x80043658..0x80043F10 handler set |
!= 0 | neither high bit set | 0x50 | bank 1 - kinds 12-19 swap to 0x800448B0..0x80045584 |
!= 0 | 0x04000000 set, 0x20000000 clear | 0xA0 | bank 2 - kind 18/19 swap to 0x800457C4 / 0x80045988 |
!= 0 | 0x20000000 set | 0xF0 | bank 3 - likely dev / debug mode; never observed in retail world-map render |
Kinds 8-11 are shared across all banks. Empirically (Drake post-warp settled, 19,935 dispatcher-entry hits via autorun_slot4_dispatcher_args.lua), retail world-map render exercises only banks 0x00 (77%) and 0x50 (23%); both 0x04000000 and 0x20000000 cmd_flags bits are never set, so banks 2 and 3 stay dark. The bank distinction in retail play is purely the fade_flags != 0 toggle.
Per-kind primitive types
Every handler has the same shape: read N command-stream words, transform 3-or-4 vertices through the GTE, write an M-byte GP0 packet at the primitive-pool pointer. The strides give away the PSX primitive type:
| Kind | Bank 0 entry | Banks 1,2 entry | cmd stride | GP0 stride | Likely primitive |
|---|---|---|---|---|---|
| 8 | 0x8004409c (shared) | (shared) | 0x14 (20B) | 0x20 (32B) | POLY_G4 (gouraud quad) |
| 9 | 0x8004423c (shared) | (shared) | 0x18 (24B) | 0x28 (40B) | POLY_GT4 (gouraud-textured quad) |
| 10 | 0x80044434 (shared) | (shared) | 0x18 (24B) | 0x28 (40B) | POLY_GT4 variant |
| 11 | 0x800445b0 (shared) | (shared) | 0x1c (28B) | 0x34 (52B) | extended quad (extra per-vert data) |
| 12 | 0x80043658 | 0x800448b0 | 0x0c (12B) | 0x14 (20B) | POLY_F3 (flat triangle) |
| 13 | 0x80043768 | 0x80044a3c | 0x0c (12B) | 0x18 (24B) | POLY_G3 / POLY_FT3 |
| 14 | 0x80043b58 | 0x80044fdc | 0x14 (20B) | 0x1c (28B) | POLY_FT3 |
| 15 | 0x80043c6c | 0x80045194 | 0x18 (24B) | 0x24 (36B) | POLY_GT3 |
| 16 | 0x800438b8 | 0x80044c14 | 0x14 (20B) | 0x20 (32B) | POLY_G4 |
| 17 | 0x800439e4 | 0x80044dc8 | 0x18 (24B) | 0x28 (40B) | POLY_GT4 |
| 18 | 0x80043dd4 | 0x800453bc (b1) / 0x800457c4 (b2) | 0x1c (28B) | 0x28 / 0x20 | POLY_GT4 extended |
| 19 | 0x80043f10 | 0x80045584 (b1) / 0x80045988 (b2) | 0x24 (36B) | 0x34 / 0x28 | POLY_GT4 extended-plus |
Each handler decodes the per-command words as two packed vertex indices per u32 (low-16 & 0x7FF8, high-16 also & 0x7FF8 - a >>3 divisor plus 8-byte vertex stride from param_3 = the vertex pool base).
Cross-kingdom hit-count comparison
Exec-breakpoint hit counts at the eight cluster-A LW PCs + the cluster-B LW PC during a single warp-tile transition. All three kingdoms captured with LEGAIA_PC_CAP=50000 over 1800 vsyncs; no PC saturates the cap, so the totals are exact:
| Kingdom | sstate | Cluster A total | Cluster B | Cluster A RAs observed |
|---|---|---|---|---|
| Drake | already on map01, held UP | 71,331 | 178 | 0x8001B47C, 0x8001BC8C, 0x801F78D4 |
| Sebucus | town → map02, held DOWN | 90,096 | 67 | 0x8001B47C, 0x801F78D4 |
| Karisto | town → map03, held DOWN | 13,593 | 115 | 0x8001B47C, 0x801F78D4 |
Sebucus's cluster-A total is higher than Drake's despite Sebucus's slot-4 being smaller - confirming hit-count tracks scene-render volume, not slot-4 record count. Cluster B's variance is the inverse: Drake walks the most slot-4 bodies, then Karisto, then Sebucus.
Per-kind delta
With the cluster-A LW PCs mapped to specific kind handlers, the per-PC × per-kingdom hit counts surface a clean signal. All three kingdoms captured uncapped (LEGAIA_PC_CAP=50000 over 1800 vsyncs):
| Kind handler | Primitive (likely) | Drake | Sebucus | Karisto |
|---|---|---|---|---|
13 banks 1,2 (0x80044A3C) | POLY_G3 / POLY_FT3 triangle | 9,465 | 2,040 | 49 |
17 banks 1,2 (0x80044DC8) | POLY_GT4 textured quad | 762 | 240 | 147 |
18 bank 1 (0x800453BC, ×4 LW PCs) | POLY_GT4 extended quad | 13,561 (×4) | 20,601 (×4) | 1,820 (×4) |
16 banks 1,2 (0x80044C14) | POLY_G4 quad | 7,688 | 878 | 2,058 |
15 banks 1,2 (0x80045194) | POLY_GT3 textured triangle | 6,860 | 5,412 | 2,059 |
cluster B (0x80059DE4) | mid-body reader | 178 | 67 | 115 |
- Kind 13 scales sharply: Drake (9,465) ≫ Sebucus (2,040) ≫ Karisto (49). Drake / Sebucus have many small triangle primitives; Karisto barely uses them.
- Kind 17 scales with overall scene weight: Drake (762) > Sebucus (240) > Karisto (147). Ratio Drake / Karisto ≈ 5.2.
- Kind 16: Karisto-heavy (2,058) / Drake-heavy (7,688) but Sebucus uses it least (878).
- Kind 18 (extended quad) is the absolute workhorse - Sebucus dispatches 20,601 instances of it (~80% of cluster-A primitive count), Drake 13,561, Karisto 1,820. Dominant per-frame primitive across every kingdom.
- Cluster B (the mid-body reader): Drake (178) > Karisto (115) > Sebucus (67) - Drake's larger slot 4 visits more of the secondary reader's body subset.
Captured CSVs land under captures/slot4_uncapped/ (per-row flushed, safe to inspect mid-run). The dispatcher-entry probe CSV at captures/slot4_dispatcher/ gives the first-kind / cmd_flags / fade_flags per call.
Cluster-A caller (FUN_8001ada4)
FUN_8001ada4 (2456 B / 614 instructions) is the per-actor renderer that walks a linked list of actor records and calls cluster A for each one's meshes. For each actor, the mesh-table at actor+0x44 is [u32 count, u32 mesh_ptr[count]]. Each mesh_ptr is the TMD-style struct passed as param_1 to FUN_80043390. This is the per-actor mesh path; it is a separate caller from the world-map slot-4 render - the warp-transition slot-4 reads return into ra = 0x801F78D4 (the world-map top-view overlay renderer) and 0x8001BC8C (the SCUS render path), reading the slot-4 records in place as documented in Consumer call sites above.
What slot 4 encodes
Slot 4 is a per-kingdom assembly of small object-local 3D meshes, walked in place by the world-map top-view renderer through the cluster-A primitive renderer above. Each body is a self-contained render packet: an 8-byte class header (kind 1/2/4) + a GTE vertex pool + a command stream of packed vertex indices. A full cluster-A sweep finds no reader of the pool word's high half, so the per-vertex attr word is render-unused reserved data. The picture is consistent with:
- bodies 9 / 11 having full 3D mesh-scale Y extents while body 12 is near-flat (different kinds of objects, not 2D contour vs 2D outline)
- the leading three bodies (
kind = 1) being byte-identical templates across all three kingdoms (a shared universal mesh set), plus sharedkind 2/4clusters, with the rest kingdom-specific - the corner-clustered point distribution in body 13 (
kind = 4): four corner-anchored objects, not a single ±32K boundary frame - the in-game-object silhouettes visible in side projections - body-9 features that resemble specific game props
- cluster A's primitive vocabulary (POLY_G4 / POLY_GT4 / POLY_F3 / etc.) is a standard 3D-mesh renderer, not a wireframe / coastline-line driver
The reader doesn't appear as a direct LUI+ADDIU reference to the (per-kingdom) slot-4 base in any captured world-map overlay - the world-map renderer reaches the slot-4 records through a runtime pointer, which is why the static sweep returned empty. Slot 4 is not re-read from a cache; the warp Read-watchpoint shows the renderer reading the records in place, every frame, transforming each through the GTE and emitting GP0 primitive packets into the scene's primitive pool (see Consumer call sites).
Tooling
The wireframe interpretation is falsified, but these tools render and dump the container for inspection:
| Tool | Role |
|---|---|
cargo run -p legaia-asset --bin asset -- slot4-png --input <PROT>.BIN --out <png> | Container PNG renderer. --style row|col|pairs|grid|points toggles between topology interpretations; --axes xz|xy|zy switches projection plane; --only-body N / --frame-body N isolate a single body. --from-raw <bin> renders a previously-dumped slot-4 payload. |
cargo run -p legaia-asset --bin asset -- kingdom-slot <PROT>.BIN --slot 4 | Per-body inventory dump. |
legaia_asset::world_map_overlay::{parse, top_down_lines, wireframe_segments_3d, record_points, body_axis_range} | Rust API. wireframe_segments_3d emits the full-3D segment list (group-polyline topology, all three model-space coordinates kept) the live-engine inspection overlay consumes. |
scripts/pcsx-redux/run_dump_slot4.sh + autorun_dump_slot4.lua | PCSX-Redux closed-loop dumper: loads a save state, dumps the live slot-4 RAM region, quits. |
scripts/pcsx-redux/autorun_dump_full_ram.lua | Full 2 MiB main RAM dump. Use when the load base is unknown. |
scripts/pcsx-redux/diff_slot4_ram_vs_disc.py | Byte-compare a RAM dump against the disc-decoded payload. |
The world-overview web viewer does not expose slot 4 (the wireframe interpretation is falsified). The WASM exports (slot4_wireframe_lines / slot4_wireframe_points / slot4_wireframe_bounds) remain available to re-enable a slot-4 draw, though a faithful render would walk each body as an object-local GTE vertex pool indexed by its command stream rather than as a wireframe.
Live-engine inspection overlay. The clean-room engine decodes slot 4 for every world-map scene onto SceneResources::world_map_slot4 (resolved only for SceneLoadKind::WorldMap; None everywhere else). With LEGAIA_WORLDMAP_SLOT4=1, legaia-engine play-window builds a LineList from wireframe_segments_3d and merges it into the world-map overlay-lines buffer (colour-keyed by body kind), so the decoded vertex pool can be inspected in the live 3D view under the real camera / fog. This is an inspection overlay, not faithful geometry: the segments use the group-polyline topology convention and the records render at their raw object-local coordinates, because the per-object placement transform (and the true triangle topology) live in the unpinned cluster-A command stream described above. Off by default.
Slot-4 loader (loader-hunt probe)
Running autorun_slot4_loader_hunt.lua against Drake (held UP for 60 vsyncs into the warp) with Write bps tiled across slot-4 RAM (0x8011A624 + offset[0..7000]) surfaces the LZS decoder as the sole writer:
| Caller chain | PC of write | Notes |
|---|---|---|
FUN_8001A55C (LZS decoder) | 0x8001A604 (sb v1, 0(s1) literal-byte write) | Dominant: 5-byte bursts at every probed offset |
| same | 0x8001A664 / 0x8001A668 / 0x8001A610 / 0x8001A5AC | Back-reference / literal-run / dictionary-byte paths inside the LZS loop |
Every captured first-write shows pc = 0x8001A604, ra = 0x8001A58C (LZS calling itself internally), with the stack containing 0x8001F194 / 0x8001F0A0 at the call-chain slots - both inside the FUN_8001F05C asset-dispatcher region. The chain is the standard asset-load path: scene loader → FUN_8001F05C (asset dispatcher) → LZS decoder → writes slot 4 at its allocated RAM destination. No special slot-4 transcoder - the asset is just LZS-decoded verbatim into RAM, matching the byte-verified disc-to-RAM finding.
Working-buffer writers (transcoder-hunt probe)
Running autorun_slot4_transcoder_hunt.lua against Drake (held UP for 60 vsyncs into the warp transition) with Write bps tiled across the 0x801BA000 working buffer surfaces two distinct writers, not a single transcoder:
| Offset | First-write PC | RA | Writer function | Role |
|---|---|---|---|---|
+0x7F8 (cluster A's vertex_base) | 0x80028710 / 0x8002871C | 0x8001B160 | FUN_80028158 (5580 B) | per-frame procedural mesh builder, called from FUN_8001ada4 case 4 |
+0x8E4 (cluster A's command_stream) | 0x800293C8 / 0x800296A0 | 0x8001B160 | same FUN_80028158 | per-frame procedural primitive-batch writer (same call) |
+0x6000 (deeper region) | 0x8001A8C8 (memcpy) | 0x8001E758 | FUN_8001E54C (836 B) | scene-load chunk loader - copies [type, size, data] chunks to the buffer |
FUN_80028158 reads only the actor's +0x9C params struct (offsets +0x10..+0x22) and writes the working buffer directly - no slot-4 RAM pointers in its arguments. It is a procedural mesh generator (probably waves / sky / particle-emitter sheets), not a slot-4 transcoder.
FUN_8001E54C is the [type, size, data] streaming chunk dispatcher: switches on *(char*)(chunk + 3) (chunk type byte) and routes each chunk to memcpy (case 0/2), LZS decode (case 1/3), or another decoder (case 12). These writes are other data streams - they never carry slot-4 pointers.
The writers above are not slot-4 transcoders. The finer source-map probe (Read bps tiled across the Drake slot-4 window plus an Exec bp on FUN_8001E54C) settled this: the slot-4 records are read in place as per-frame render geometry by the world-map renderer (0x801F78D4 → cluster-A prim dispatcher 0x80043390 → GTE mesh emit), and FUN_8001E54C never copies them - see Consumer call sites. The FUN_80028158 procedural mesh builder and the FUN_8001E54C chunk copies are separate, non-slot-4 streams (waves / sky / particle sheets and other scene-load chunks); they populate the 0x801BA000-region working buffer the per-actor renderer reads, which is a distinct path from the world-map slot-4 render.
DAT_8007C018 - global TMD pointer table (the actual cluster-A source)
FUN_80043390's display_state arg points at a TMD's group-descriptor array (offset +0xC into a TMD blob whose +0x00 carries the Legaia magic 0x80000002). Those TMD pointers live in a global runtime table:
DAT_8007C018 : array of u32 TMD pointers; entry stride = 4
DAT_8007B774 : install counter (next free index)
DAT_8007BB38 : walk counter (last valid index, used by the table walker)
DAT_8007B824 : per-pack count (set by case 2 to *pack_header[0])
The installer is FUN_80026B4C @ PC 0x80026BA8 (called per-TMD from the asset dispatcher's case 2 TMD-pack handler):
80026b90 lui v1, 0x8008
80026b94 lw v1, -0x488c(v1) ; v1 = *DAT_8007B774 (next free idx)
80026b98 addiu v0, v0, -0x3fe8 ; v0 = 0x8007C018
80026b9c sll v1, v1, 0x2
80026ba0 addu v1, v1, v0 ; v1 = &DAT_8007C018[idx]
80026ba4 jal FUN_800268dc ; build per-group descriptor array at tmd+0xC
80026ba8 _sw a0, 0x0(v1) ; install: DAT_8007C018[idx] = tmd_ptr
Ghidra's static reference-database doesn't surface this store because the addu between the lui+addiu and the sw defeats its constant propagation. The materialisation scan ghidra/scripts/find_addr_materializer_dat_8007c018.py walks every lui+addiu pair that produces 0x8007C018 and looks at the next six instructions; that's how the installer was pinned.
After installation, each pointed-to TMD has the runtime shape:
[+0x00] u32 magic = 0x80000002
[+0x04] u32 flags / version
[+0x08] u32 group_count
[+0x0C] array of group_count × 0x1C-byte group descriptors
each starts with vertex_base_ptr (u32) + vertex_count (u32)
followed by 0x14 bytes of per-group state
Consumers
| Function | Site | Role |
|---|---|---|
FUN_80021B04 (SCUS actor allocator) | reads DAT_8007C018[actor[+0x64].i16] | populates actor[+0x44] = [count, mesh_ptr[count]] from TMD groups |
FUN_80024D78 (SCUS actor allocator - variant) | reads DAT_8007C018[actor[+0x64].i16] | same shape as FUN_80021B04 but also OR-sets actor[+0x10] |= 0x08000000 (a per-actor enable flag) |
FUN_801D77F4 (overlay alt allocator) | reads DAT_8007C018[(i16)param_2] + _DAT_8007B7DC VDF buffer | copies vertex pool from sub-records into actor[+0x90] |
FUN_801D8280 (overlay table walker) | iterates DAT_8007C018[0..DAT_8007BB38] | hands each sub-record to FUN_801D5E20 |
FUN_801F69D8 (world-map top-view dispatcher in world_map_top_ext) | reads DAT_8007C018[(visible_object_kind8 + DAT_8007B6F8) * 4] | walks per-tile visibility scratchpad, calls FUN_80043390(tmd+0xC, color, fog) |
FUN_8001E890 | writes entry[+0x8] = 10 for three consecutive table indices at DAT_8007B824 + 0..2 | per-pack count override (overwrites the installed TMD's group_count field) |
FUN_8001EBEC | reads DAT_8007C018[DAT_8007B824 + 0..2] (3 consecutive party-character TMDs) | per-party-member group-descriptor patch - picks one of two pre-built 0x1C-byte descriptors (TMD+0x124 vs TMD+0x140) based on a per-character flag, then overwrites the indexed group descriptor. Drives equipment-conditional mesh swaps |
Warp-transition caller (FUN_801F69D8)
The Drake warp-into-world-map Read-bp captured ra = 0x801F725C, pointing at the JAL into FUN_80043390 at PC 0x801F7254. The enclosing function is FUN_801F69D8 (2572 B / 643 instr) in overlay_world_map_top_ext.bin. The probe's original label "FUN_801F7088" was inside the function body, not at its entry - the prologue addiu sp, sp, -0x70 sits 1712 bytes earlier at 0x801F69D8. Force-disasm + walk-back + force-set body pinned the true extent; see ghidra/scripts/dump_world_map_top_ext_caller.py.
The function bulk-copies a 0x20-byte camera struct from 0x8007BF10 into scratchpad 0x1F8002CC, nested-loops over visible tile cells in scratchpad table _DAT_1F8003EC + 0x8000 + Y*0x100 + X*2 (padded by ±10), dereferences each 0x20-byte object record at _DAT_1F8003EC + (idx & 0x1FF) * 0x20, applies frustum bounds + visibility flags + GTE RTPT projection, then routes the TMD via DAT_8007C018 and emits the display list. Arg breakdown for the FUN_80043390 call:
arg0= TMD group descriptor array start (tmd + 0xC)arg1= color RGB (0xD0D0D0default;0x40D0D0D0if object record's[+0x1E]flag is set; OR'd with0x10000000ifrecord[+0x12] & 0x800- interactive)arg2= fog cueclamp((GTE_screen_z - 0x5000) >> 3, 0, 0x1000)
How slot-4 bytes reach cluster A
The cluster-A input pointer originates from DAT_8007C018 (the global TMD pointer table). Two parallel call paths funnel into the same dispatcher:
- Top-view dispatcher (
FUN_801F69D8): readsDAT_8007C018[(visible_object_kind8 + DAT_8007B6F8) * 4]per tile and passesentry + 0xCtoFUN_80043390. This is the warp-into-world-map render path the Read-bp probe captured. - Per-actor renderer (
FUN_8001ada4, caller RA0x8001B47C): walksactor+0x44 = [u32 count, u32 mesh_ptr[count]]and passes eachmesh_ptrtoFUN_80043390. The mesh pointers came fromactor+0x44, which is populated byFUN_80021B04/FUN_80024D78fromDAT_8007C018[actor[+0x64].i16]- same table, different actor-allocator path.
This DAT_8007C018 table is the source for the character-mesh and per-tile world-map TMD objects (the [0..4] party meshes from PROT 0874, plus the per-tile kingdom-object TMDs). It is a separate path from the slot-4 render: the kingdom slot-4 body (type = 0x05 = MOVE) is read in place at its per-kingdom resident base by the world-map renderer (ra 0x801F78D4), not routed through DAT_8007C018 - see Consumer call sites (and the superseded transcode/overwrite readings under Falsified readings).
The slot-4 body header kind ∈ {1, 2, 4} is a per-body class/scope tag (see kind (1/2/4) above) and has no link to the cluster-A bank selector (which is driven by the per-call fade_flags / cmd_flags args, not by any slot-4 field).
Live snapshot - settled field scene
Provenance correction: the local dump is named drake_world.bin, but its scene id (0x80084540 = 0x3c), scene name (dolk), and game_mode 0x03 identify it as the dolk field scene, not the Drake world map. DAT_8007C018 is filled identically by every field-scene load (the single FUN_80020224 descriptor-walk), so the layout/counters below are a valid generic field-scene example; the [5..142] entries are this scene's field-file TMD pack (one contiguous 138-entry pack), not a "kingdom bundle". type-0x05 slot-4 does not install into DAT_8007C018 - only dispatcher cases 0x02/0x09 reach FUN_80026B4C.
RAM dump after the scene load has settled (full RAM captured by autorun_dump_full_ram.lua + the classifier at scripts/asset-investigation/classify_dat_8007c018.py):
| Field | Value |
|---|---|
DAT_8007B774 (install counter) | 143 |
DAT_8007BB38 (walker counter) | 142 |
DAT_8007B6F8 (kingdom-TMD prefix) | 5 |
DAT_8007B828 (error bits) | 0x00000000 (no magic mismatches) |
| Index range | Count | Content |
|---|---|---|
[0..4] | 5 | Character-mesh TMDs at 0x8014D554..0x801585C0, group_count 10/10/10/3/2. Disc source: § Disc-side source of [0..4] below. |
[5..142] | 138 | Kingdom-derived TMDs at 0x800F7908..0x80138D44 (group_count 1..10, mixed sizes) |
[143..255] | 113 | Either zero (uninstalled) or stale junk past the walker counter - never read by code because every reader gates on DAT_8007BB38 or an explicit index ≤ install counter |
Every populated entry is a valid Legaia TMD (magic 0x80000002, flags = 1, group_count > 0). The table is homogeneous in the steady state. This is a field-scene (dolk) snapshot, not a world-map one - its 0x8011Axxx-range TMDs are this scene's field-file TMD pack, not the kingdom slot-4 buffer (see Falsified readings for the retired overwrite reading).
Disc-side source of [0..4]
The five character-mesh TMDs at DAT_8007C018[0..4] originate from PROT entry 0874 (befect_data), not from the dev-tree path data\field\player.lzs (whose runtime name maps to PROT 876 - player_data - which actually carries a VAB + TIM_LIST + SEQ streaming-format payload with zero TMDs; see data-field.html for the chunk shape).
PROT 0874 is a parse_player_lzs(buf, 3)-shaped container with three LZS-compressed sections:
| Section | Type byte | Compressed size | File offset | Content |
|---|---|---|---|---|
| 0 | 0x01 | 0xB49C (46 236 B) | 0x20 | 5-TMD pack (LZS decodes to 65 536 B) |
| 1 | 0x02 | 0x41E0 (16 864 B) | 0x5037 | Secondary TMD payload |
| 2 | 0x03 | 0x1D524 (120 100 B) | 0x7055 | MAN-shape data |
Decoding section 0 (LZS-decompress from file offset 0x20) yields a canonical TMD pack - [u32 count][u32 word_offsets[count]][TMD bodies] with word offsets in 4-byte units (same convention as tim-pack / kingdom slot 1):
| Pack slot | Body offset | nobj (disc) | Body bytes (to next slot) |
|---|---|---|---|
| 0 | 0x0018 | 12 | 13 220 |
| 1 | 0x33BC | 12 | 13 800 |
| 2 | 0x69A4 | 12 | 11 656 |
| 3 | 0x972C | 3 | 6 488 |
| 4 | 0xB084 | 2 | 20 348 (trailing padding to pack end) |
Byte-equality check against a Drake post-warp RAM snapshot (local-only captures/ram_dumps/drake_world.bin):
- Pack slot 3 vs RAM
DAT_8007C018[3](un-fixup the runtime's absolute-pointer group descriptors back to disc-form offsets usingdisc_off = abs_ptr - (tmd_base + 0xC)): the full 6488-byte body matches byte-for-byte (0 differences over0x1958bytes compared). - Pack slot 4 vs RAM
DAT_8007C018[4]: the first 1048 bytes match byte-for-byte. The runtime allocates only the in-use prefix; the trailing ~19 KB of disc padding is not copied. - Pack slots 0/1/2 vs RAM
DAT_8007C018[0..2]: the first three group descriptors and groups 4..9 match byte-for-byte. RAM'snobj=10vs disc'snobj=12is a deliberate runtime override (see “10-group cap” below); RAM's slot-3 group descriptor is sourced from disc's group 11 (the FUN_8001EBEC patch).
10-group cap + equipment-conditional group patch
The five disc TMDs ship with nobj=12 (for the three active-party slots) and nobj=3 / 2 (for the trailing two - confirmed nobj from the disc pack matches RAM exactly for those). The active-party post-install loop in FUN_8001E890 overwrites DAT_8007C018[DAT_8007B824 + 0..2]'s entry[+0x08] (TMD group_count) to 10, capping each of the first three TMDs at 10 active groups. The last two disc groups (10 and 11) are equipment-conditional descriptors: FUN_8001EBEC reads two per-character bytes (from 0x80084xxx, equipment slots) and for each of the three active party slots picks either TMD+0x124 (= group 10) or TMD+0x140 (= group 11) and overwrites the indexed live group descriptor with that pre-built 0x1C-byte template. This is the equipment-conditional mesh swap (weapon variant, etc.).
Loader chain
The retail loader is the overlay-resident scene loader FUN_801D6704: it calls FUN_80020118 to install PROT 0874 section 0 into DAT_8007C018[0..4], then FUN_80020224(0) to install the kingdom-derived [5..N] via the generic FUN_8001F05C case 2 → FUN_80026B4C TMD-pack chain - the same descriptor walk every field scene runs; there is no kingdom-specific installer. The static-SCUS dead ends below are retained for provenance.
FUN_8001E890's retail-PROT branch (DAT_8007B8C2 != 0) calls FUN_8003eb98(0x36C, piVar2, 1), which loads PROT 876's raw bytes into piVar2. The downstream LZS calls then interpret piVar2[2..7] as three (size, offset) pairs - but PROT 876's bytes there are streaming-format chunk data (the start of a VABp header inside chunk 0), not LZS descriptors. That branch is therefore incompatible with PROT 876's actual layout in retail and either (a) is gated off by DAT_8007B8C2 == 0 in retail or (b) is dead code. The data\field\player.lzs string and PROT-876 fast path both fall over the same shape mismatch.
Among the static SCUS sites, FUN_800520F0 (the battle scene loader) loads PROT 873+874 contiguously, but its two install loops walk both buffers as flat [count, offsets[], data] packs and process PROT 874's count = 3 entries via FUN_8001fbcc (VDF install). PROT 874 section 0 is gated on the type byte (0x01), so no static SCUS site funnels section 0 through the TMD-pack handler - that dispatch lives in the overlay loader FUN_801D6704 above. The one residual is the exact CDNAME indirection handing FUN_80020118 its PROT-0874 bytes; see Open work below.
Live snapshot - Sebucus mid-warp
The Sebucus dump captures the warp transition partway through the asset install:
| Field | Value |
|---|---|
DAT_8007B774 (install counter) | 92 |
DAT_8007BB38 (walker counter) | 91 |
DAT_8007B6F8 (kingdom-TMD prefix) | 5 |
DAT_8007B828 (error bits) | 0x00000000 |
Entries [0..91] are valid TMDs; the install is in flight, so the TMD-pack handler has not yet completed pushing every member. Entries [92..] carry leftover pointers from a previous game-state's table fill, but DAT_8007BB38 = 91 means no consumer ever reads past index 91. With DAT_8007BB38 as the authoritative bound, nothing past index 91 is table content (see Falsified readings for the retired mid-load classifications).
Open work
- Banks 2 (
0xA0) and 3 (0xF0). Banks reachable in the dispatcher but never observed during retail world-map play. Candidates: dev/debug menu render modes, battle-overlay re-use of the dispatcher, or cutscene render paths. A wide-coveragecmd_flags-capture probe across multiple non-world-map game modes would pin which (if any) caller passes those flags. - PROT 0874 section-0 loader indirection. The producer chain is pinned (see Loader chain and § Disc-side source of [0..4]); the residual is the exact CDNAME indirection handing
FUN_80020118its PROT-0874 bytes - a Write-bp probe onDAT_8007C018[0]would settle it. Full inner-dispatch and caller detail:docs/formats/world-map-overlay.md. - The
_DAT_8007B824"freeze" path. TheFUN_8001F05Ccase-2 sub-path (if (param_3 == 1) { _DAT_8007B704 = size; _DAT_8007B824 = pack_count; }, PC0x8001F2F8) is the sole SCUSswwriter of_DAT_8007B824- the persistent-base indexFUN_8001E1B4reads to reset the install cursor (DAT_8007B774 = _DAT_8007B824). No static SCUS caller ofFUN_8001F05Cpassesparam_3 == 1(the three direct callers -FUN_80020224,FUN_8002541C,overlay_baka_fighter_801d4c50- passs6,0,0), and no dumped overlay caller ofFUN_80020224passesparam_1 == 1. So either the freeze path lives in an uncaptured overlay, or_DAT_8007B824stays at its BSS-init zero throughout retail play and the "persistent slots" semantic is vestigial. A write-bp on_DAT_8007B824would settle which.