At a glance

Where on disc
Slot 4 of the kingdom bundles: PROT 0086 (Drake, map01), 0245 (Sebucus, map02), 0392 (Karisto, map03). Type byte 0x05.
Magic
None. Every sub-body carries the constant marker 0x080C at +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; engine SceneResources::world_map_slot4.
Confidence
Inferred - container and RAM placement byte-verified; consumer pinned reading records in place as GTE vertices; kind / attr characterised, 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 0x05 is what asset-type labels “MOVE”. The kingdom-bundle consumer treats each body as a mesh.
BundlePROT indexCDNAME labelDecoded sizeBodies
Drake0086map013230415
Sebucus0245map022696416
Karisto0392map032444416

Layout

one body (8 + 8×count_a×count_b + 8 bytes) header 8 B, marker 0x080C record 0 8 B record 1 8 B … count_a × count_b records, 8-byte stride trailer 8 zero bytes one record = one GTE vertex x +0 i16 y +2 i16 z +4 i16 attr +6 i16, ignored GTE register VXYn GTE register VZn (high half dropped)
A slot-4 body is a header, a run of 8-byte vertex records, and a zero trailer; the renderer loads each record's two words directly into the GTE vertex registers.

Outer pack

OffsetSizeFieldMeaning
+0x00u32countNumber of sub-bodies
+0x04u32 × countbyte_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)

OffsetSizeFieldMeaning
+0x00u8count_aRecords per group
+0x01u8flag_aUsually 0; 1 for kind = 4 bodies
+0x02u8count_bNumber of groups
+0x03u8flag_bUsually 0
+0x04u16markerConstant 0x080C in every body
+0x06u16kind1, 2 or 4 - body class / scope tag

Body payload

OffsetSizeFieldMeaning
+0x088 × count_a × count_brecord[]GTE vertices (i16 x, i16 y, i16 z, i16 attr)
after8trailerAlways 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.

BytesFieldRole
0..1xModel-space X - GTE VXYn low half
2..3yModel-space Y - GTE VXYn high half
4..5zModel-space Z - GTE VZn
6..7attrNot 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:

kindWhat the bodies areShared?
1The three leading bodies (0, 1, 2), count_a = 10Byte-identical in all three kingdoms - a universal mesh set
2Full-3D kingdom objectsSome kingdom-specific; one trailing cluster identical in all three; others shared by kingdom pairs
4Widest-extent meshes (Drake body 13 reaches the ±32 K world bounds); always flag_a = 1Shared 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)

Bodycount_acount_bkindflag_aRecordsNotes / X·Y·Z span
010201020016626 · 10767 · 38641; shared template
1102010200shared template
2103010300shared template
32302060degenerate plane
42202040degenerate plane
510302030025 unique of 30 groups
6102620260
710302030025 unique of 30 groups
810320303 identical groups (filler)
912302036010725 · 25856 · 21248; shared with Sebucus / Karisto 12
1012302036013056 · 18432 · 31503; shared with Sebucus / Karisto 13
1112102012011492 · 27648 · 24064; shared with Sebucus / Karisto 14
121012020120016118 · 4096 · 31473; near-flat
1314154121065485 · 14336 · 64512; corner-clustered
142302060

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.

KingdomBundleResident baseEnd (excl.)Bytes
Drakemap01 / 00860x8011A6240x8012245432304
Sebucusmap02 / 02450x80119CE40x8012063826964
Karistomap03 / 03920x80108D840x8010ED0024444

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.

KindCommand strideGP0 packetPrimitive
820 B32 BPOLY_G4 gouraud quad
9, 1024 B40 BPOLY_GT4 gouraud-textured quad
1128 B52 BExtended quad
1212 B20 BPOLY_F3 flat triangle
1312 B24 BPOLY_G3 / POLY_FT3
1420 B28 BPOLY_FT3
1524 B36 BPOLY_GT3
1620 B32 BPOLY_G4
1724 B40 BPOLY_GT4
1828 B40 / 32 BPOLY_GT4 extended - the per-frame workhorse in every kingdom
1936 B52 / 40 BPOLY_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_flagscmd_flags bitsBankEffect
== 0ignored0 (+0x00)Kinds 12-19 use the small handler set
!= 0neither high bit1 (+0x50)Kinds 12-19 swap to the lit handler set
!= 00x040000002 (+0xA0)Kinds 18 / 19 swap again
!= 00x200000003 (+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.

  1. 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 colour 0xD0D0D0 (variants for the record's +0x1E flag and the interactive bit +0x12 & 0x800) and a fog cue clamp((screen_z - 0x5000) >> 3, 0, 0x1000). This is the path that reads slot 4 in place.
  2. Per-actor renderer FUN_8001ADA4 (SCUS, 2456 bytes). Walks a linked list of actors; each actor's +0x44 mesh 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
GlobalRole
DAT_8007C018Array 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_8007B774Install counter (next free index)
DAT_8007BB38Walk counter (last valid index) - every reader gates on it, so entries past it are never content
DAT_8007B824Per-pack count / persistent-base index
DAT_8007B6F8Kingdom-TMD prefix (5 in every snapshot)
FunctionRole
FUN_80026B4CInstaller, 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_80024D78SCUS actor allocators - fill actor+0x44 from DAT_8007C018[actor+0x64]; the variant also sets actor+0x10 |= 0x08000000
FUN_801D77F4, FUN_801D8280Overlay allocator and table walker
FUN_801F69D8Top-view dispatcher - indexes [(object_kind + DAT_8007B6F8)] per tile
FUN_8001E890, FUN_8001EBECCap the three party TMDs at 10 groups and patch one equipment-conditional group descriptor per member (disc group 10 or 11)
FUN_801D6704Overlay 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

ToolRole
asset kingdom-slot <PROT>.BIN --slot 4Per-body inventory dump
asset slot4-png --input <PROT>.BIN --out <png>Container renderer (--style, --axes, --only-body)
legaia_asset::world_map_overlayRust API: parse, wireframe_segments_3d, record_points
LEGAIA_WORLDMAP_SLOT4=1 legaia-engine play-windowInspection overlay of the decoded pools under the live camera and fog - not faithful geometry
scripts/pcsx-redux/ slot-4 dump / base-locate / diff scriptsDump live RAM, find the per-kingdom base, byte-compare against the disc

How we know

Function / siteAddressWhat it provesEvidence
Primitive dispatcher FUN_800433900x80043390; SCUS jump table 0x8007657C, overlay table 0x801F8968Command-word decode, four-bank handler selectionDisassembly; captures/slot4_dispatcher/
Kind-16 handler FUN_80044C140x80044C14 (family 0x80043658..0x80045988)Each 8-byte record is loaded as a GTE vertex; attr unreadDisassembly sweep of every handler
World-map top-view rendererreturn address 0x801F78D4; entry FUN_801F69D8Slot 4 read in place, per frame, 363 / 365 captured reads; 762 of 2153 dispatcher calls take their command pointer from the windowautorun_slot4_source_map.lua, drake_castle_to_worldmap state
LZS decoder FUN_8001A55Cwrites at 0x8001A604 etc.Sole writer of the slot-4 window: decoded verbatim, no transcoderautorun_slot4_loader_hunt.lua
Working-buffer writers FUN_80028158, FUN_8001E54C0x801BA000 bufferProcedural mesh builder and chunk loader - other streams, never slot-4 pointersautorun_slot4_transcoder_hunt.lua
Resident basesDrake 0x8011A624, Sebucus 0x80119CE4, Karisto 0x80108D84Byte-identical to the disc payload for every bodydiff_slot4_ram_vs_disc.py, locate_slot4_base.py
TMD installer FUN_80026B4Cstore at 0x80026BA8How DAT_8007C018 is filledfind_addr_materializer_dat_8007c018.py
Party-mesh sourcePROT 0874 section 0DAT_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 xy silhouettes of bodies 9 and 11 are 3D props seen sideways. A count_a × count_b heightfield grid fails the same test.
  • “Slot 4 is transcoded into the working buffer.” The 0x801BA000 writes belong to the per-actor procedural stream, and the “window full of TMDs” snapshot was a dolk field 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.

See also