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:

BundlePROT indexCDNAME labelDecoded size
Drake0085map0132304
Sebucus0244map0226964
Karisto0391map0324444

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:

bytesfieldrole
0..1xmodel-space X - GTE VXYn low half
2..3ymodel-space Y - GTE VXYn high half
4..5zmodel-space Z - GTE VZn
6..7attrhigh 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 carries flag_a = 1 (the only flag_a = 1 exception 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)

Bodycount_acount_bkindflag_arecordsX spanY spanZ span
0102010200166261076738641
1102010200byte-identical template (matches Sebucus / Karisto)
2103010300byte-identical template
32302060pinned plane (degenerate)
42202040pinned plane (degenerate)
510302030025 unique of 30 groups
6102620260
710302030025 unique of 30 groups
810320303 identical groups (filler / padding)
9123020360107252585621248
10123020360130561843231503
11121020120114922764824064
121012020120016118409631473
13141541210654851433664512
142302060

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

Kingdombundleresident baseend (excl.)bytesbodies matched
Drakemap01 / 00850x8011A6240x801224543230415/15
Sebucusmap02 / 02440x80119CE40x801206382696416/16
Karistomap03 / 03910x80108D840x8010ED002444416/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:

  1. Top-down dev-menu wireframe / continent coastline (body 12 = coastline, body 13 = world boundary frame). Projecting all 15 bodies onto xz produces no recognizable map silhouette in any kingdom, and no render matches the dev-menu top-view captured from PCSX-Redux save states.
  2. count_a × count_b heightfield 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.
  3. A single non-xz projection. xy surfaces 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 recognizable xy shapes 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 0x801BA000 working buffer belong to the separate per-actor renderer stream, and the "slot-4 window full of TMDs" snapshot was a field-scene (dolk) capture whose 0x8011Axxx TMDs 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.

ReaderEntry / PC rangeCaller RA
Cluster A - TMD-style primitive renderer (FUN_80043390 + per-kind handlers)dispatcher entry 0x80043390; per-kind handler bodies at 0x80043658..0x800459880x8001B47C (inside FUN_8001ada4), 0x801F78D4 (world-map overlay) - both in every kingdom
Cluster B - secondary mid-body reader0x80059DE40x80059C00 (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_flagscmd_flags bitsBank offsetEffect
== 0(ignored)0x00bank 0 - kinds 12-19 use the small 0x80043658..0x80043F10 handler set
!= 0neither high bit set0x50bank 1 - kinds 12-19 swap to 0x800448B0..0x80045584
!= 00x04000000 set, 0x20000000 clear0xA0bank 2 - kind 18/19 swap to 0x800457C4 / 0x80045988
!= 00x20000000 set0xF0bank 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:

KindBank 0 entryBanks 1,2 entrycmd strideGP0 strideLikely primitive
80x8004409c (shared)(shared)0x14 (20B)0x20 (32B)POLY_G4 (gouraud quad)
90x8004423c (shared)(shared)0x18 (24B)0x28 (40B)POLY_GT4 (gouraud-textured quad)
100x80044434 (shared)(shared)0x18 (24B)0x28 (40B)POLY_GT4 variant
110x800445b0 (shared)(shared)0x1c (28B)0x34 (52B)extended quad (extra per-vert data)
120x800436580x800448b00x0c (12B)0x14 (20B)POLY_F3 (flat triangle)
130x800437680x80044a3c0x0c (12B)0x18 (24B)POLY_G3 / POLY_FT3
140x80043b580x80044fdc0x14 (20B)0x1c (28B)POLY_FT3
150x80043c6c0x800451940x18 (24B)0x24 (36B)POLY_GT3
160x800438b80x80044c140x14 (20B)0x20 (32B)POLY_G4
170x800439e40x80044dc80x18 (24B)0x28 (40B)POLY_GT4
180x80043dd40x800453bc (b1) / 0x800457c4 (b2)0x1c (28B)0x28 / 0x20POLY_GT4 extended
190x80043f100x80045584 (b1) / 0x80045988 (b2)0x24 (36B)0x34 / 0x28POLY_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:

KingdomsstateCluster A totalCluster BCluster A RAs observed
Drakealready on map01, held UP71,3311780x8001B47C, 0x8001BC8C, 0x801F78D4
Sebucustown → map02, held DOWN90,096670x8001B47C, 0x801F78D4
Karistotown → map03, held DOWN13,5931150x8001B47C, 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 handlerPrimitive (likely)DrakeSebucusKaristo
13 banks 1,2 (0x80044A3C)POLY_G3 / POLY_FT3 triangle9,4652,04049
17 banks 1,2 (0x80044DC8)POLY_GT4 textured quad762240147
18 bank 1 (0x800453BC, ×4 LW PCs)POLY_GT4 extended quad13,561 (×4)20,601 (×4)1,820 (×4)
16 banks 1,2 (0x80044C14)POLY_G4 quad7,6888782,058
15 banks 1,2 (0x80045194)POLY_GT3 textured triangle6,8605,4122,059
cluster B (0x80059DE4)mid-body reader17867115
  • 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 shared kind 2/4 clusters, 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:

ToolRole
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 4Per-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.luaPCSX-Redux closed-loop dumper: loads a save state, dumps the live slot-4 RAM region, quits.
scripts/pcsx-redux/autorun_dump_full_ram.luaFull 2 MiB main RAM dump. Use when the load base is unknown.
scripts/pcsx-redux/diff_slot4_ram_vs_disc.pyByte-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 chainPC of writeNotes
FUN_8001A55C (LZS decoder)0x8001A604 (sb v1, 0(s1) literal-byte write)Dominant: 5-byte bursts at every probed offset
same0x8001A664 / 0x8001A668 / 0x8001A610 / 0x8001A5ACBack-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:

OffsetFirst-write PCRAWriter functionRole
+0x7F8 (cluster A's vertex_base)0x80028710 / 0x8002871C0x8001B160FUN_80028158 (5580 B)per-frame procedural mesh builder, called from FUN_8001ada4 case 4
+0x8E4 (cluster A's command_stream)0x800293C8 / 0x800296A00x8001B160same FUN_80028158per-frame procedural primitive-batch writer (same call)
+0x6000 (deeper region)0x8001A8C8 (memcpy)0x8001E758FUN_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

FunctionSiteRole
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 buffercopies 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_8001E890writes entry[+0x8] = 10 for three consecutive table indices at DAT_8007B824 + 0..2per-pack count override (overwrites the installed TMD's group_count field)
FUN_8001EBECreads 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 (0xD0D0D0 default; 0x40D0D0D0 if object record's [+0x1E] flag is set; OR'd with 0x10000000 if record[+0x12] & 0x800 - interactive)
  • arg2 = fog cue clamp((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:

  1. Top-view dispatcher (FUN_801F69D8): reads DAT_8007C018[(visible_object_kind8 + DAT_8007B6F8) * 4] per tile and passes entry + 0xC to FUN_80043390. This is the warp-into-world-map render path the Read-bp probe captured.
  2. Per-actor renderer (FUN_8001ada4, caller RA 0x8001B47C): walks actor+0x44 = [u32 count, u32 mesh_ptr[count]] and passes each mesh_ptr to FUN_80043390. The mesh pointers came from actor+0x44, which is populated by FUN_80021B04/FUN_80024D78 from DAT_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):

FieldValue
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 rangeCountContent
[0..4]5Character-mesh TMDs at 0x8014D554..0x801585C0, group_count 10/10/10/3/2. Disc source: § Disc-side source of [0..4] below.
[5..142]138Kingdom-derived TMDs at 0x800F7908..0x80138D44 (group_count 1..10, mixed sizes)
[143..255]113Either 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:

SectionType byteCompressed sizeFile offsetContent
00x010xB49C (46 236 B)0x205-TMD pack (LZS decodes to 65 536 B)
10x020x41E0 (16 864 B)0x5037Secondary TMD payload
20x030x1D524 (120 100 B)0x7055MAN-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 slotBody offsetnobj (disc)Body bytes (to next slot)
00x00181213 220
10x33BC1213 800
20x69A41211 656
30x972C3 6 488
40xB0842 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 using disc_off = abs_ptr - (tmd_base + 0xC)): the full 6488-byte body matches byte-for-byte (0 differences over 0x1958 bytes 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's nobj=10 vs disc's nobj=12 is 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 2FUN_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:

FieldValue
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

  1. 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-coverage cmd_flags-capture probe across multiple non-world-map game modes would pin which (if any) caller passes those flags.
  2. 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_80020118 its PROT-0874 bytes - a Write-bp probe on DAT_8007C018[0] would settle it. Full inner-dispatch and caller detail: docs/formats/world-map-overlay.md.
  3. The _DAT_8007B824 "freeze" path. The FUN_8001F05C case-2 sub-path (if (param_3 == 1) { _DAT_8007B704 = size; _DAT_8007B824 = pack_count; }, PC 0x8001F2F8) is the sole SCUS sw writer of _DAT_8007B824 - the persistent-base index FUN_8001E1B4 reads to reset the install cursor (DAT_8007B774 = _DAT_8007B824). No static SCUS caller of FUN_8001F05C passes param_3 == 1 (the three direct callers - FUN_80020224, FUN_8002541C, overlay_baka_fighter_801d4c50 - pass s6, 0, 0), and no dumped overlay caller of FUN_80020224 passes param_1 == 1. So either the freeze path lives in an uncaptured overlay, or _DAT_8007B824 stays at its BSS-init zero throughout retail play and the "persistent slots" semantic is vestigial. A write-bp on _DAT_8007B824 would settle which.

See also