Mario Sluggers Model Format — Structure Overview
Blocks appear in the order they are typically laid out in the file. Pointers in the header are relative to the start of the file; pointers within sections are generally relative to the start of that section.
File Header
0x00first word = 0
0x14unknown pointer (ptr6) (block-relative; null on every character model, set in 578 of 1,408 vanilla blocks: props, map objects, effects)
0x18→ facial pose data (ptr7) (optional, 70 vanilla blocks; block-relative; position and normal pose arrays)
0x1cunknown pointer (ptr8) (null in all 1,408 vanilla blocks)
0x00version magic (0x00B749E0)
0x04user data length
0x08user data pointer
0x0cnumber of GEO descriptors
0x10→ GEO descriptor array
GEO Descriptor ×N
0x00→ DOLayout
0x04→ mesh name string
DOLayout
0x00→ Position Data
0x04→ Color Data
0x08→ UV Data (first of N channels)
0x0c→ Normal/Lighting Data
0x10→ Display Header
0x14texture channel count (uint8)
0x15padding (3 bytes)
Position / Color / UV / Normal Data
0x00→ raw quantized data array
0x04count (uint16)
0x07component count (3 usual; 6 if skinned pos+normal)
UV headers also carry palette name ptr (0x08) and palette ptr (0x0c).
Normal header also carries ambient percentage float (0x08).
Display Header
0x00primitive data pointer (not used directly)
0x04→ display state array
0x08number of display states (uint16)
Display State ×N (16 bytes each)
0x00type (uint8): 1 = texture bind, 3 = vertex format, 4 = texture layers (not decoded), 6 = pixel state, 7 = shader (no other types in vanilla)
0x01parameter bytes ×3 (type 7: byte 1 = specular strength)
0x04setting (bitmap32) (type 7: shader FourCC)
0x08→ primitive list (may be null; 32-byte aligned)
0x0cprimitive list size (multiple of 32)
Primitive List (GX draw calls)
Repeating blocks until a 0x00 byte; zero-padded to the list size:
+0x00primitive type: 0x98 tri-strip, 0x80 quads, 0x90 triangles (the only opcodes in vanilla)
+0x01vertex count (uint16 BE)
+0x03vertex data — per-attribute indices, widths set by type-3 state
0x00version magic (0x007B7960)
0x04actor ID (uint16)
0x06bone count (uint16)
0x08bone tree — unknown word
0x0cbone tree — root node pointer
0x10→ geo name string
0x14skin file ID (uint16)
0x18user data size
0x1c→ user data (bone → animation track ID map)
Bone ×N (0x1c bytes each, traversed via tree)
0x00→ SRT (null if no transform)
0x04–0x13tree navigation: prev / next / parent / first child pointers
0x14GEO file ID (uint16) — submesh drawn rigidly by this bone; 0xFFFF means no mesh
0x16bone ID (uint16)
0x18inheritance flag (bool) — usually true
0x19draw priority (uint8)
SRT (0x34 bytes)
0x00type byte (0x4, 0x8, or 0xc)
0x04scale (float[3]: x y z)
0x10quaternion (float[4]: x y z w) — apply as (-w, x, y, z)
0x20translation (float[3]: x y z)
0x2cpadding (8 bytes)
0x00number of textures (uint16)
0x02number of CLUTs (uint16)
Texture Descriptor ×N (0x20 bytes each, starts at 0x04)
0x00→ image data
0x04→ palette data (null if none)
0x08image height (uint16)
0x0aimage width (uint16)
0x0c–0x0fedge LOD enable · min LOD · max LOD · unpacked
0x10–0x16unknown / padding (may contain wrap/filter settings)
0x17image format (uint8)
0x18palette entry count (uint16)
0x1apalette format (uint8)
0x1b–0x1funknown / padding
0x00SK1 count (uint16)
0x02SK2 count (uint16)
0x04SKAcc count (uint16)
0x06quantize info (high nibble must be 0)
0x08→ SK1 array
0x0c→ SK2 array
0x10→ SKAcc array
0x14memClr start (offset into submesh 0 positions; cleared each frame)
0x18memClr size
0x1c→ flush index array
0x20flush index count
SK1 ×N (0x40 bytes each — 1 bone, sequential vertices)
0x00runtime matrix placeholder (0x30 bytes)
0x30→ source position/normal array
0x34→ destination in GPL position data
0x38bone index (uint16)
0x3avertex count (uint16)
0x3cposition/normal offset (uint8)
SK2 ×N (0x74 bytes each — 2 bones, weight array)
0x00runtime matrix placeholders (2 × 0x30 bytes)
0x60→ source position/normal array
0x64→ weight array (uint8 pairs; logical records use a 256-unit divisor)
0x68→ destination in GPL position data
0x6cbone index 1 (uint16)
0x6ebone index 2 (uint16)
0x70vertex count (uint16)
0x72position/normal offset (uint8)
SKAcc ×N (0x44 bytes each — 1 bone, indexed dest, accumulates)
0x00runtime matrix placeholder (0x30 bytes)
0x30→ source position/normal array
0x34→ destination index array (uint16 per vertex)
0x38→ destination base in GPL position data
0x3c→ weight array (uint8 per vertex)
0x40bone index (uint16)
0x42vertex count (uint16)
0x00maximum pose count (uint16)
0x02facial object count (uint16)
0x04attribute type count (uint16)
0x06padding (uint16, 0)
0x08→ facial object table (= 0x14 + 16 × type count)
0x0c→ type descriptor array (always 0x14)
0x100
Type Descriptor ×N (16 bytes each, from 0x14)
0x00target index (uint16): submesh, or SKAcc entry for kind 0
0x02kind (uint8): 0 SKAcc source, 1 position, 2 normal, 3 colour, 4-7 UV
0x03component count << 4 | component size
0x04zero (runtime: resolved target pointer at +0x08)
Facial Object Table Entry ×N (12 bytes each)
0x00object pose count (uint16)
0x02object attribute count (uint16)
0x04attribute record size (0x0C + object pose count × 4)
0x08→ facial object data
Facial Object Data
0x00attribute records × object attribute count
Attribute Record
0x00mapped entry count (uint32)
0x04target index / kind / component count / component size (uint8 each; matches a type descriptor)
0x08→ vertex run list
0x0c→ pose pointer array (uint32 × object pose count)
Vertex Run List
+0x00first GPL vertex index (uint16)
+0x02consecutive vertex count (uint16)
Ends with a (0, 0) pair; the game reads runs until a count of 0. The runs add up to the mapped entry count.
Pose Array × Pose Count
+0x00quantized components (XYZ or interleaved XYZ+normal XYZ)
Pose zero is absolute; later position poses are displacement vectors.
Editing notes
Facts that matter when a model is rebuilt rather than read, each documented where its structure is:
- An unreferenced whole mesh blob is harmless, so a submesh can be replaced by a new blob before the GPL user data with its descriptor repointed (Moving blobs).
- A new surface is one canonical state group appended to a rigid list; earlier records never change (Rigid submesh state lists).
- Colour arrays with quantization 48 hold 2-byte RGBA4444 entries, not RGBA8 (Color data).
- Which Type 7 shaders work on a character surface, and what the hand roles do, is in the Type 7 catalogue.
- The skinned body (submesh 0) can be rebuilt from any mesh and weights: a new blob with interleaved normals and room for the skinning write-back (Rebuilding the skinned body), a new SKN in canonical layout (Canonical layout) and remapped facial poses (Rebuilding the section).
- Rebuilt primitive lists are triangle strips, about half the bytes of plain triangles (Draw the primitives). Block size counts against a scene-wide memory budget, not against the DAT.
32-Byte Alignment
The Wii Broadway CPU has 32-byte cache lines, the GX command processor fetches display lists in 32-byte bursts, and the GX texture image-base register drops the low 5 address bits. Some offsets in a model block must therefore be 32-byte aligned; many others are routinely unaligned in vanilla data. Do not pad every variable-length array: add zero padding only where the table below requires it.
All alignments are relative to the start of the model block. Counts come from a survey of all 1,422 unique vanilla model blocks (337 skinned) and from scans of the 361 skinned .sluggie exports. "100%" means every observed instance was at 0 mod 32.
| Data |
Rule |
Evidence |
Details |
| Section starts (GPL, ACT, TEX, SKN, ptr6-ptr8) |
32-byte |
1,422 blocks, 100%; Dolphin |
below |
| TEX image and palette payloads |
32-byte |
7,011 images, 654 palettes, 100%; Dolphin |
TEX |
| Skinned position buffer (submesh 0) |
32-byte |
189, 100% |
GPL |
| Primitive-list pointer |
32-byte |
9,465, 100% |
GPL |
| Primitive-list size |
multiple of 32 (zero-padded) |
every donor list |
GPL |
| DOLayout blob start |
none, but a blob may only move by a multiple of 32 |
3,244, unaligned; builder bug |
GPL |
| Non-skinned position, standalone normal, color, UV |
none |
every mod-4 value observed |
GPL |
| SKN source, weight, dest-index, flush arrays |
32-byte |
14,436 / 9,703 / 3,323 / 173, 100% |
SKN |
| SK1/SK2 source arrays |
exact position: VAR_DATA_OFF + gplVertexArr |
361/361 models; Dolphin |
SKN |
SK1/SK2 gplVertexArr |
multiple of 32 (relative to the position buffer) |
12,364 entries, 100% |
SKN |
| SKN memClrPtr |
none (memClrSize is a multiple of 32) |
165 non-zero ranges |
SKN |
| ACT internal tail padding |
no single formula |
185 player models |
ACT |
Failure modes differ. A misaligned texture renders with fine pixel noise, not a hang. A misaligned or wrongly sized primitive list, a misaligned skinned position buffer or a misaligned SKN array can stall the GPU or CPU, or corrupt rendering. An aligned but off-mirror SKN source produces vertex explosions.
The required kinds are listed in ModelFormat.ARRAY_ALIGNMENT: skinned_position, primitive_list, skn_source, skn_weight, skn_destination_index, skn_flush_index, section_start and texture_payload. BlockValidator checks all of them, plus primitive-list sizes, the SKN source mirror and gplVertexArr. It reports its totals in facts['alignment'].
Section starts
Relative to the model block, every section start (GPL, ACT, TEX, SKN, ptr6-ptr8) is at 0 mod 32 in all 1,422 unique vanilla blocks. Moving a section moves everything after it. A GPL section that grows to a length that isn't a multiple of 32 therefore misaligns TEX, and every texture in the model with it (confirmed in Dolphin, 2026-09-16). The full BuildGPLMeshData serializer produces exactly such a length: it ends on a 32-byte boundary, then appends the 40-byte GPLUserData.
BuildHEADERModelBlock zero-pads each section to the next 32-byte boundary. When the new GPL still fits the donor span, it keeps the older rule of padding to the donor span. ptr6-ptr8 are recomputed from the aligned positions, keeping the donor spacing between trailing sections. Because every donor section is already aligned, this padding changes nothing in a byte-identical rebuild.
aligned_start = (end_of_previous_section + 31) & ~31
See index for links to detailed per-section documentation.