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 a library of small 3D meshes - the props and landmarks the overworld renderer draws around the walking party. Each mesh is a pool of 8-byte vertices the PlayStation's geometry coprocessor reads straight off the loaded bytes, every frame, with no conversion in between.
At a glance
- Where on disc
- Slot 4 of the kingdom bundles: PROT
0086(Drake,map01),0245(Sebucus,map02),0392(Karisto,map03). Type byte0x05. - Magic
- None. Every sub-body carries the constant marker
0x080Cat+4. - Size
- 15-16 bodies per kingdom; 24-32 KB decoded; 8 bytes per vertex record.
- In RAM
- Loaded verbatim (LZS-decoded, no fixup) at a per-kingdom base - see RAM layout.
- Parser
legaia_asset::world_map_overlay; engineSceneResources::world_map_slot4.- Confidence
- Inferred - container and RAM placement byte-verified; consumer pinned reading records in place as GTE vertices;
kind/attrcharacterised, not decoded. - Used by
- World map, World-overview viewer
What it is
A kingdom's slot 4 is a small mesh library: 15-16 self-contained bodies, each an 8-byte header plus a pool of 3D vertices. The world-map renderer walks the bodies in place each frame, pushes vertices through the GTE (the geometry coprocessor), and emits ordinary triangle and quad packets. The triangle topology is not in the body - a separate command stream indexes into the vertex pool.
- Not the continent itself. The bulk terrain (~4300 textured quads in the top view) comes from the kingdom's slot-1 TMD pack, drawn through the overlay-mode dispatch table - see world map - bulk terrain.
- Not a coastline or map outline. That reading is falsified; see History.
- Not a move table, although the type byte
0x05is what asset-type labels “MOVE”. The kingdom-bundle consumer treats each body as a mesh.
| Bundle | PROT index | CDNAME label | Decoded size | Bodies |
|---|---|---|---|---|
| Drake | 0086 | map01 | 32304 | 15 |
| Sebucus | 0245 | map02 | 26964 | 16 |
| Karisto | 0392 | map03 | 24444 | 16 |
Layout
Outer pack
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | u32 | count | Number of sub-bodies |
+0x04 | u32 × count | byte_offsets[] | Absolute byte offsets into the decoded payload (word offsets in the slot-1 TMD pack - a different rule) |
+offset | - | bodies[] | Contiguous sub-bodies |
Sub-body header (8 bytes)
| Offset | Size | Field | Meaning |
|---|---|---|---|
+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 in every body |
+0x06 | u16 | kind | 1, 2 or 4 - body class / scope tag |
Body payload
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x08 | 8 × count_a × count_b | record[] | GTE vertices (i16 x, i16 y, i16 z, i16 attr) |
| after | 8 | trailer | Always 8 zero bytes |
Body size is always 8 + count_a × count_b × 8 + 8; the arithmetic fits every body in all three kingdoms exactly.
Each record is a GTE vertex
The per-kind handler receives the record region as its vertex pool and indexes it with byte offsets (index & 0x7FF8, an 8-byte stride). It loads the first word into VXYn (X low half, Y high half) and the second into VZn (Z in the low half), then runs RTPT (perspective-transform three vertices) and NCLIP (backface cull). The fourth i16 rides in the discarded high half of the VZn word.
| 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 | Not a coordinate; the GTE vertex load ignores it |
Coordinate ranges are object-local mesh extents (Sebucus body 0: X in [-20224, -3598], Y in [-6416, 4351], Z in [-17649, 20992]).
kind - a class / scope tag
kind is not the renderer's primitive kind (that comes from a separate command word). Hashing bodies across the three kingdoms shows what it partitions:
kind | What the bodies are | Shared? |
|---|---|---|
| 1 | The three leading bodies (0, 1, 2), count_a = 10 | Byte-identical in all three kingdoms - a universal mesh set |
| 2 | Full-3D kingdom objects | Some kingdom-specific; one trailing cluster identical in all three; others shared by kingdom pairs |
| 4 | Widest-extent meshes (Drake body 13 reaches the ±32 K world bounds); always flag_a = 1 | Shared by kingdom pairs |
So each kingdom's slot 4 is an assembly from a shared library plus kingdom-specific bodies. Degenerate bodies (count_a = 1, all-zero records) are empty placeholder slots.
attr - present but unread
The fourth i16 varies per vertex, is uncorrelated with position, and is not a neighbour's coordinate. A sweep of every cluster-A handler finds no reader of the pool word's high half, so it is reserved data the render path never uses.
Per-kingdom body inventory
Drake = 15 bodies, Sebucus = 16, Karisto = 16. Bodies 0-2 are the shared kind = 1 templates in every kingdom.
Drake (map01, PROT 0086)
| Body | count_a | count_b | kind | flag_a | Records | Notes / X·Y·Z span |
|---|---|---|---|---|---|---|
| 0 | 10 | 20 | 1 | 0 | 200 | 16626 · 10767 · 38641; shared template |
| 1 | 10 | 20 | 1 | 0 | 200 | shared template |
| 2 | 10 | 30 | 1 | 0 | 300 | shared template |
| 3 | 2 | 30 | 2 | 0 | 60 | degenerate plane |
| 4 | 2 | 20 | 2 | 0 | 40 | degenerate plane |
| 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) |
| 9 | 12 | 30 | 2 | 0 | 360 | 10725 · 25856 · 21248; shared with Sebucus / Karisto 12 |
| 10 | 12 | 30 | 2 | 0 | 360 | 13056 · 18432 · 31503; shared with Sebucus / Karisto 13 |
| 11 | 12 | 10 | 2 | 0 | 120 | 11492 · 27648 · 24064; shared with Sebucus / Karisto 14 |
| 12 | 10 | 120 | 2 | 0 | 1200 | 16118 · 4096 · 31473; near-flat |
| 13 | 14 | 15 | 4 | 1 | 210 | 65485 · 14336 · 64512; corner-clustered |
| 14 | 2 | 30 | 2 | 0 | 60 |
Sebucus (map02, PROT 0245)
16 bodies. Bodies 8-11 are kind = 4, flag_a = 1.
Karisto (map03, PROT 0392)
16 bodies. Bodies 4-9 and 11 are kind = 4, flag_a = 1. Body 10 is the corpus's one kind = 2 body with flag_a = 1.
Full dump: asset kingdom-slot <PROT>.BIN --slot 4.
RAM layout
The LZS decoder writes slot 4 straight to its destination - byte-identical to the disc, no fixup - and the resident base differs per kingdom, so a RAM probe must locate the base first. Body 0's records start 0x40 past the base.
| Kingdom | Bundle | Resident base | End (excl.) | Bytes |
|---|---|---|---|---|
| Drake | map01 / 0086 | 0x8011A624 | 0x80122454 | 32304 |
| Sebucus | map02 / 0245 | 0x80119CE4 | 0x80120638 | 26964 |
| Karisto | map03 / 0392 | 0x80108D84 | 0x8010ED00 | 24444 |
How it is drawn
The world-map top-view renderer (in the world-map overlay) walks the slot-4 bodies in place every frame and hands each to the SCUS-resident primitive dispatcher - a TMD-style display-list walker. The dispatcher reads one command word (a 15-bit primitive kind plus a 16-bit count), optionally re-arms the GTE colour registers, and tail-calls a per-kind handler through a jump table. Each handler transforms 3-4 pool vertices through the GTE, writes one GP0 packet into the scene's primitive pool, and chains to the next command.
- In place. A read-watchpoint tiled across the Drake window catches only renderer reads (363 of 365 captures); no copy into a working buffer exists.
- Generic code. One dispatcher serves all three kingdoms and the field-character renderer; the world-map overlay swaps in its own handler table for the bulk-terrain route.
- Four handler banks, selected per call by fade and command flags; retail world-map play uses only banks 0 and 1.
Primitive kinds
Kinds 8-19 map onto the standard PlayStation polygon packets - flat, gouraud and textured triangles and quads. Kind 18 (an extended textured quad) is the per-frame workhorse in every kingdom. Each command word packs two vertex indices per u32, both masked & 0x7FF8.
Primitive kinds, bank selection and per-kingdom render load
Every handler has the same shape; the command and packet strides give away the primitive type. Kinds 8-11 share one handler across all banks; kinds 12-19 have a bank-0 and a bank-1/2 handler each.
| Kind | Command stride | GP0 packet | Primitive |
|---|---|---|---|
| 8 | 20 B | 32 B | POLY_G4 gouraud quad |
| 9, 10 | 24 B | 40 B | POLY_GT4 gouraud-textured quad |
| 11 | 28 B | 52 B | Extended quad |
| 12 | 12 B | 20 B | POLY_F3 flat triangle |
| 13 | 12 B | 24 B | POLY_G3 / POLY_FT3 |
| 14 | 20 B | 28 B | POLY_FT3 |
| 15 | 24 B | 36 B | POLY_GT3 |
| 16 | 20 B | 32 B | POLY_G4 |
| 17 | 24 B | 40 B | POLY_GT4 |
| 18 | 28 B | 40 / 32 B | POLY_GT4 extended - the per-frame workhorse in every kingdom |
| 19 | 36 B | 52 / 40 B | POLY_GT4 extended-plus |
The dispatcher computes a bank offset added to the kind × 4 jump-table index. The two flag tests are sequential, so the 0x20000000 branch wins when both are set.
fade_flags | cmd_flags bits | Bank | Effect |
|---|---|---|---|
== 0 | ignored | 0 (+0x00) | Kinds 12-19 use the small handler set |
!= 0 | neither high bit | 1 (+0x50) | Kinds 12-19 swap to the lit handler set |
!= 0 | 0x04000000 | 2 (+0xA0) | Kinds 18 / 19 swap again |
!= 0 | 0x20000000 | 3 (+0xF0) | Never observed in retail world-map render |
Over 19,935 dispatcher entries on Drake, 77% take bank 0 and 23% bank 1; neither high flag bit is ever set. Exec-breakpoint totals over one warp transition (1800 vsyncs, uncapped): Drake 71,331 cluster-A hits, Sebucus 90,096, Karisto 13,593 - tracking scene render volume, not slot-4 size. Kind 18 dominates in every kingdom (Sebucus 20,601 instances, about 80% of its primitives); kind 13 triangles are heavy on Drake (9,465) and nearly absent on Karisto (49).
A second reader, “cluster B” at 0x80059DE4, is the generic VRAM LoadImage DMA (FUN_80059BD4) incidentally touching the window - not a slot-4 consumer. The dump file named 80059de4.txt is that DMA routine.
The two callers: top-view dispatcher and per-actor renderer
Two call paths reach the primitive dispatcher with the same three arguments (display_state, cmd_flags, fade_flags): display_state[0] is the vertex pool, [3] gates colour / light modulation, [4] is the command stream.
- Top-view dispatcher
FUN_801F69D8(world-map overlay, 2572 bytes). Copies a 0x20-byte camera struct into scratchpad, loops over visible tile cells (±10 padding) in the scratchpad tile table, dereferences each 0x20-byte object record, applies frustum bounds and visibility flags, projects through the GTE, then calls the dispatcher with colour0xD0D0D0(variants for the record's+0x1Eflag and the interactive bit+0x12 & 0x800) and a fog cueclamp((screen_z - 0x5000) >> 3, 0, 0x1000). This is the path that reads slot 4 in place. - Per-actor renderer
FUN_8001ADA4(SCUS, 2456 bytes). Walks a linked list of actors; each actor's+0x44mesh table is[u32 count, u32 mesh_ptr[count]]and each pointer is a TMD group-descriptor array. This is the field-character path, sharing the dispatcher but not the slot-4 buffer.
The neighbouring TMD table
The same dispatcher draws field characters and per-tile kingdom objects from a global table of TMD pointers, DAT_8007C018. That is a separate path: slot 4 is walked at its resident base and never installed into the table, even though a field-scene RAM snapshot fills the same address range with that scene's TMD pack.
DAT_8007C018 - installer, consumers, party-mesh source
| Global | Role |
|---|---|
DAT_8007C018 | Array of u32 TMD pointers, stride 4; each target is a Legaia TMD (magic 0x80000002) with +0x08 group count and 0x1C-byte group descriptors from +0x0C |
DAT_8007B774 | Install counter (next free index) |
DAT_8007BB38 | Walk counter (last valid index) - every reader gates on it, so entries past it are never content |
DAT_8007B824 | Per-pack count / persistent-base index |
DAT_8007B6F8 | Kingdom-TMD prefix (5 in every snapshot) |
| Function | Role |
|---|---|
FUN_80026B4C | Installer, called per TMD from the asset dispatcher's TMD-pack case; builds the group-descriptor array at tmd + 0xC and stores the pointer. Ghidra misses the store because an addu sits between the lui+addiu and the sw. |
FUN_80021B04, FUN_80024D78 | SCUS actor allocators - fill actor+0x44 from DAT_8007C018[actor+0x64]; the variant also sets actor+0x10 |= 0x08000000 |
FUN_801D77F4, FUN_801D8280 | Overlay allocator and table walker |
FUN_801F69D8 | Top-view dispatcher - indexes [(object_kind + DAT_8007B6F8)] per tile |
FUN_8001E890, FUN_8001EBEC | Cap the three party TMDs at 10 groups and patch one equipment-conditional group descriptor per member (disc group 10 or 11) |
FUN_801D6704 | Overlay scene loader: installs PROT 0874 section 0 into [0..4] via FUN_80020118, then the scene's own TMD pack via FUN_80020224 - the same walk every field scene runs |
Entries [0..4] are the party character meshes from PROT 0874 section 0 (a 5-TMD pack, LZS at file offset 0x20; slots ship with 12 / 12 / 12 / 3 / 2 groups). A settled dolk field snapshot shows 143 installed entries, [5..142] being that scene's field TMD pack; a mid-warp Sebucus snapshot shows 92 with the install still in flight. The dev-tree path data\field\player.lzs maps to PROT 876, which carries a VAB + TIM list + SEQ stream with no TMDs; the SCUS branch that would read it as three LZS descriptors is incompatible with those bytes and is either gated off or dead in retail.
Tooling
| Tool | Role |
|---|---|
asset kingdom-slot <PROT>.BIN --slot 4 | Per-body inventory dump |
asset slot4-png --input <PROT>.BIN --out <png> | Container renderer (--style, --axes, --only-body) |
legaia_asset::world_map_overlay | Rust API: parse, wireframe_segments_3d, record_points |
LEGAIA_WORLDMAP_SLOT4=1 legaia-engine play-window | Inspection overlay of the decoded pools under the live camera and fog - not faithful geometry |
scripts/pcsx-redux/ slot-4 dump / base-locate / diff scripts | Dump live RAM, find the per-kingdom base, byte-compare against the disc |
How we know
| Function / site | Address | What it proves | Evidence |
|---|---|---|---|
Primitive dispatcher FUN_80043390 | 0x80043390; SCUS jump table 0x8007657C, overlay table 0x801F8968 | Command-word decode, four-bank handler selection | Disassembly; captures/slot4_dispatcher/ |
Kind-16 handler FUN_80044C14 | 0x80044C14 (family 0x80043658..0x80045988) | Each 8-byte record is loaded as a GTE vertex; attr unread | Disassembly sweep of every handler |
| World-map top-view renderer | return address 0x801F78D4; entry FUN_801F69D8 | Slot 4 read in place, per frame, 363 / 365 captured reads; 762 of 2153 dispatcher calls take their command pointer from the window | autorun_slot4_source_map.lua, drake_castle_to_worldmap state |
LZS decoder FUN_8001A55C | writes at 0x8001A604 etc. | Sole writer of the slot-4 window: decoded verbatim, no transcoder | autorun_slot4_loader_hunt.lua |
Working-buffer writers FUN_80028158, FUN_8001E54C | 0x801BA000 buffer | Procedural mesh builder and chunk loader - other streams, never slot-4 pointers | autorun_slot4_transcoder_hunt.lua |
| Resident bases | Drake 0x8011A624, Sebucus 0x80119CE4, Karisto 0x80108D84 | Byte-identical to the disc payload for every body | diff_slot4_ram_vs_disc.py, locate_slot4_base.py |
TMD installer FUN_80026B4C | store at 0x80026BA8 | How DAT_8007C018 is filled | find_addr_materializer_dat_8007c018.py |
| Party-mesh source | PROT 0874 section 0 | DAT_8007C018[0..4] byte-match the disc pack (6488 bytes on slot 3, 1048-byte prefix on slot 4) | Drake post-warp RAM dump vs LZS-decoded section |
Full capture transcripts and reproduction steps: docs/formats/world-map-overlay.md.
History
History: the coastline reading and other retired hypotheses
- Coastline wireframe / dev-menu outline. Rendering every body on every projection plane and topology mode produces no map silhouette in any kingdom; the clean
xysilhouettes of bodies 9 and 11 are 3D props seen sideways. Acount_a × count_bheightfield grid fails the same test. - “Slot 4 is transcoded into the working buffer.” The
0x801BA000writes belong to the per-actor procedural stream, and the “window full of TMDs” snapshot was adolkfield scene, not a world map. - “Sebucus shows no render reads.” A wrong-base assumption; against the verified base, 171 of 177 reads land in the window.
- Table entries past the walk counter are stale pointers, not content.
- PROT 0085 / 0244 / 0391 named the entry an over-read window started in; the bundle is the next entry. See PROT.
Catalogued on do-not-re-walk.
Open questions
- Which caller, if any, selects handler banks 2 and 3 - candidates are debug render modes, battle reuse of the dispatcher, or cutscenes.
- The exact CDNAME indirection that hands the PROT 0874 party-mesh bytes to the installer; a write breakpoint on
DAT_8007C018[0]would settle it. - Who calls the asset dispatcher with the “freeze” argument that latches
DAT_8007B824- no static SCUS caller does.