Header (12 bytes)

u32 id        // ALWAYS 0x80000002 in Legaia. Bit 31 = FLIST_BIT
              // (pointers are byte offsets relative to header end);
              // low byte 0x02 = "Legaia TMD format version 2"
u32 flags     // 0 on disc; runtime sets to 1 after pointer fixup
u32 nobj      // number of objects

The 0x80000002 magic was confirmed via the dev string "Model Version Err: %x" in FUN_80026B4C - the registration function bails when *tmd != 0x80000002.

Object table (28 bytes per object × nobj)

u32 vert_top      // byte offset from end of header (0x0C)
u32 n_vert
u32 normal_top    // byte offset from end of header
u32 n_normal
u32 prim_top      // byte offset from end of header
u32 n_primitive   // SUM of all primitives across primitive-section groups
i32 scale         // ALWAYS 0x00808080 in Legaia (Legaia-custom; standard
                  // PSX uses signed log2 scale)

After FUN_800268DC runs at load time, vert_top / normal_top / prim_top are patched in-place to absolute RAM addresses. Static tools should NOT do this patch - they should use the offsets as (ptr_base + offset) where ptr_base = 12 (HEADER_SIZE).

Vertex / normal data

Both are arrays of SVECTOR { i16 x, y, z, pad } = 8 bytes each.

pub struct Vector { pub x: i16, pub y: i16, pub z: i16, pub _pad: i16 }

Primitive section

The primitive section is a sequence of groups. Each group has an 8-byte header followed by a fixed-stride array of prim data.

group header (8 bytes):
  +0  u16 count          // how many primitives in this group
  +2  u16 flags          // selects entry in per-mode table (see below)
  +4  u8  olen           // PSX SDK "output length" (packet word count)
  +5  u8  ilen           // PSX SDK "input length" -- per-prim WORD stride
                         // per-prim byte stride = ilen * 4
  +6  u8  flag           // PSX SDK flag byte (lighting / shading / etc)
  +7  u8  mode           // PSX SDK mode byte (FT3/FT4/GT3/GT4/etc)
prim data (count × ilen*4 bytes):
  count × [ilen u32 words]

n_primitive in the OBJECT header is the sum of count across all groups in the object's primitive section.

The per-prim layout depends on the prim type. The renderer (FUN_8002735C) indexes into an 8-byte-stride table at 0x8007326C using ((flags >> 1) - 8) >> 1) * 8:

flags table idx byte 0 byte 4 meaning of byte 4
0x10/11 0 0x04 0x05 vertex-index byte offset / 2 within prim
0x12/13 1 0x09 0x07 (varies per type)
0x14/15 2 0x04 0x00
0x16/17 3 0x06 0x06
0x18/19 4 0x07 0x07 FT3-style prims (uVar3 = 7 → vertex idx at byte 14 of 20-byte prim)
0x1A/1B 5 0x09 0x0B FT4-style prims
0x20-23 4 (same as above)

Each entry's first u32 has bytes [?, ?, ?, type_bits] where the low 2 bits of byte 3 select the OT packet shape (0/1/2/3 → different DrawPolyXX variants). Each entry's second u32 has the vertex-index offset (in u16 units) within the prim in its low byte.

Worked example

Small TMD 0001.tmd (5 prims, 168-byte section):

Section offset Bytes Meaning
0 04 00 20 00 07 05 01 27 Group 1 header: count=4, flags=0x20, olen=7, ilen=5, flag=0x01, mode=0x27
8 (20 bytes) Prim 0 (5 words = 20 bytes; FT3-style)
28 (20 bytes) Prim 1
48 (20 bytes) Prim 2
68 (20 bytes) Prim 3
88 (zeros) Padding
108 01 00 22 00 09 06 01 2f Group 2 header: count=1, flags=0x22, olen=9, ilen=6, flag=0x01, mode=0x2F
116 (24 bytes) Prim 4 (6 words = 24 bytes; FT4-style)
140 (zeros) Trailing padding

Big TMD 0000.tmd (760 prims, 15232-byte section):

Section offset Bytes Meaning
0 f8 02 10 00 07 05 00 26 Group header: count=0x02F8=760, flags=0x0010, olen=7, ilen=5, flag=0x00, mode=0x26
8 (20 bytes per prim) Prim 0 - start of uniform 20-byte stride
8 + 760×20 = 15208 Trailing 24 bytes of padding

The per-prim data IS uniform 20 bytes (= ilen*4) - there are no per-prim sub-headers. Walker: legaia_tmd::legaia_prims::iter_groups.

TMD pointer table

FUN_80026B4C writes registered TMDs to *(int **)(idx * 4 + 0x8007C018). Confirmed readers in retail (4 functions, all setup-not-render):

Function Role
FUN_80021B04 Actor-spawn helper; builds per-actor OBJECT pointer table at actor[0x44]+4
FUN_80024D78 Per-actor OBJECT-table rebuild
FUN_8001EBEC Per-frame OBJECT[10/11] swap (pose select for player TMDs)
FUN_8001E890 “DATA_FIELD player loader” — calls FUN_8003eb98(0x36C, …) (PROT 876 = player_data) and the dev paths data\field\player.lzs / h:\prot\all\data\field\player.lz. The retail bytes the loader reads (PROT 876 = streaming-format VAB+TIM_LIST+SEQ; the dev data\field\player.lzs file is absent from the ISO9660 walk) do not carry the [0..4] character TMDs. Those come from PROT 0874 (befect_data) section 0 — see world-map-overlay.html § Disc-side source of [0..4]. What this function does do that's still consumed at DAT_8007C018[0..2] is the post-install group-count cap (entry[+0x08] = 10) and the equipment-conditional patch dispatch into FUN_8001EBEC.

The per-actor OBJECT[i] is a 28-byte struct copied into actor[0x44][i+1] from tmd + 12 + i*28 - sizeof(OBJECT) = 28.

The renderer itself is documented separately under renderer subsystem.

See also