This section holds position, normal, color, and uv mapping information for the vertices, as well as draw lists describing the triangles created with the vertices.
| Offset | Type | Description |
|---|---|---|
| 0x0 | Magic (0x00B749E0 for Sluggers) | |
| 0x4 | uint32 | User data length |
| 0x8 | uint32 | User data pointer (relative to header) |
| 0xc | uint32 | Number of meshes |
| 0x10 | uint32 | Pointer to mesh header array (relative to header) |
main.dol reference found no second count to keep in step. A submesh appended to the end of the mesh header array, with an ACT bone naming it (see GEO ID ownership rules), draws in character select, static scenes and gameplay (Dolphin, 2026-09-16). The tested layout grows the mesh header array in place: it stays directly after the GPL header, every existing blob moves back as one unit (by a multiple of 32, see Moving blobs), and only each existing mesh header's two GPL-relative pointers (mesh data header and name) change. A layout that leaves the old array behind as dead bytes and puts a new array elsewhere has never been tested in game.(n, 0x08) pairs, but it does not track the mesh count: identical blobs appear in 2- and 3-submesh models. Keep it verbatim. A 4-submesh Mario with the 3-submesh donor's blob renders correctly (Dolphin, 2026-09-16).Decoded from main.dol and checked against every stadium model (2026-10-18). The user data is a chain of descriptors {u32 length, u16 kind, u16 count, payload}. The GPL wrapper (fn_804FF654) walks it from the start by length alone, with no alignment padding, until the header's user data size is used up, and the actor handles each descriptor by kind:
fn_8053E290, fn_80527CF8), advanced with the frame time and read by the renderer. The payload holds u16 entries slot<<8 | id, not one per mesh; their contents are not decoded.6 x f32 (min XYZ, max XYZ) each, box i for mesh i. Nothing checks the count against the mesh count, so a mesh beyond the table reads whatever follows as its box.The boxes are frustum-culling bounds in bone-local space. Before drawing a mesh, fn_804EEEC0 sets its bone's matrix on the renderer, fn_80540094 transforms the box (fn_8054003C turns the 6 floats into min plus half-size) by that matrix, and fn_8053FEF0 tests it against the frustum planes; a mesh that fails is not drawn. A mesh without bounds skips the test and is always drawn. In all 12 distinct vanilla kind-1 tables every box equals the mesh's bone-local vertex extents exactly; most are symmetric, but sta09/sta09c have asymmetric boxes.
Which stadium models the game culls (25 models, by the descriptor the walk actually reaches):
| User data | Models |
|---|---|
| kind 1 only | sta03, sta03b (Yoshi Park day/night), sta05, sta05b (DK Jungle day/night), sta08b (Daisy Cruiser night), sta09, sta09c (Bowser Jr. Playroom day/variant) |
| kind 0, then kind 1 | sta02b day/night (Bowser Castle), sta06, sta06b (Wario City day/night), sta04b (Peach Ice Garden night), sta07 day/night (Luigi's Mansion) |
| no boxes (kind 0 only, or no user data) | sta01 day/night/variant (Mario Stadium), sta02c, sta05c, sta08 day/variant (Daisy Cruiser), the Bowser Jr. Playroom night model, sta00 (Toy Field) |
| boxes present, never reached | sta04, sta04c (Peach Ice Garden day/variant): the kind-0 length is rounded up to 4 but the kind-1 descriptor follows unpadded, so the walk lands on garbage and stops. sta03c's user data is not a descriptor chain at all. |
When the patcher appends a custom submesh, it walks the chain the same way, extends the kind-1 descriptor the game reaches with one box per new mesh (the largest absolute bone-local coordinate per axis, which always encloses it), and updates the descriptor's length and count and the user data length in the GPL header; every other byte stays the same. Models whose walk reaches no box table are left alone, which keeps sta04/sta04c unculled as in vanilla. Until 2026-10-18 the patcher only extended a table that was the first and only descriptor, so in the "kind 0, then kind 1" stadiums a custom submesh had no box of its own. In Yoshi Park, before boxes were appended at all, custom submeshes did not show (2026-10-01).
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Mesh data header pointer (relative to header) |
| 0x4 | char* | Mesh name pointer (relative to header) |
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Position data pointer (relative to mesh data header) |
| 0x4 | uint32 | Color data pointer (relative to mesh data header) |
| 0x8 | uint32 | UV mapping data pointer (relative to mesh data header) |
| 0xc | uint32 | Normal data pointer (relative to mesh data header) |
| 0x10 | uint32 | Draw list pointer (relative to mesh data header) |
| 0x14 | uint8 | Number of texture channels (0 when the submesh has no UV data) |
| 0x15 | byte[3] | Padding |
Once you analyze the position, color, uv mapping, and normal data referenced by this header, you'll have an array for each of the four data types that the draw list will read from.
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Quantized raw position data ptr (relative to mesh data header) |
| 0x4 | uint16 | Number of positions |
| 0x6 | uint8 | Quantization |
| 0x7 | uint8 | Component count (usually 3, 6 if the mesh is skinned and includes alternating position and normal data) |
Only submesh 0 is ever skinned. In all 172 surveyed player-folder models with an SKN section, the one component-count-6 buffer is submesh 0's, and SKN addresses only submesh 0's positions (see SKN). Every other submesh is rigid: component count 3, stored in the local space of the bone that owns it.
The quantization byte is read per buffer, so each submesh can use its own position format. Donor position buffers use 50, 52-62 and 64: s16 with shift 2 (about ±8192) up to the common 59 (shift 11, about ±16), and float. Rigid submeshes almost always use 59.
A custom submesh far from its bone can flicker. The exporter stores a custom submesh at 59 when it fits, and steps to a coarser format (58 down to 48) when its vertices lie further from the host bone, with a warning. In Yoshi Park (sta03, Dolphin, 2026-10-02), two cut-out planes on root bone 0, about 75 units from the bone and stored at 56 (1/256 unit per step), flickered or vanished for a moment from some camera angles, mostly while the camera moved. Moved close to the bone and stored at 59, the same planes did not flicker. Distance and precision changed together, so which of the two causes it is not known. A cube 15 units from its bone, stored at 58, did not flicker near or far (Daisy Cruiser, 2026-10-18). Treat the exporter's warning as a likely glitch. In a stadium, a new root bone placed next to the mesh avoids it (see GEO ID ownership rules).
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Quantized raw color data ptr (relative to mesh data header) |
| 0x4 | uint16 | Number of colors |
| 0x6 | uint8 | Quantization |
| 0x7 | uint8 | Component count |
Color arrays don't follow the quantization table. Rigid submeshes use (component count 4, quantization 48), (3, 0) or no colors. Quantization 48 is 2-byte RGBA4444, not 4-byte RGBA8: a Dolphin probe (2026-09-26) read a 4-byte ff0000ff entry as ff00, alpha 0, and the surface disappeared.
Vertex colour multiplies the texture, and vertex alpha is honoured (Dolphin, 2026-09-26, Mario's cap). With the cap's two white entries overwritten by red, the white circle behind the M turned red while the red cap and the red M stayed red: white × red = red, red × red = red. An additive blend would have left the circle white, and a replace would have hidden the M. A colour with alpha 0 cuts the surface away as a "hold-out": only the stadium behind it draws there, and an intersecting mesh is cut too. Keep vertex alpha at full unless a surface should vanish (hand-role surfaces use alpha 0 on purpose, see Type 7).
A colour array needs two or more entries when the vertices index it. The per-mesh draw (fn_8052E99C, at 0x8052EBFC) binds a mesh's colour array only when its entry count (+0x4) is greater than 1, and sets a per-vertex-colour flag (r13+0x1E1). With exactly one entry it stores that entry as a material colour instead (fn_80529388, r13+0x1E8). That is what vanilla one-entry arrays are for: they occur only where Type 3 has no color0 (the Mii body), and every colour array a vanilla Type 3 indexes has two or more entries. A mesh whose Type 3 indexes color0 but that has one entry reads the colour array of the mesh drawn before it. Custom submeshes did exactly that until 2026-10-18 (an unpainted mesh exports one white entry): a black cube drawn right after Yoshi Park's field markings came out almost fully transparent, one after Daisy Cruiser's last mesh about 99 % opaque, one drawn first opaque (Dolphin, 2026-10-18). The patcher now writes the entry twice in that case (custom submeshes and rigid rebuilds); with it, both cubes drew solid (same day).
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Quantized raw uv (technically st) coord data ptr (relative to mesh data header) |
| 0x4 | uint16 | Number of coords |
| 0x6 | uint8 | Quantization |
| 0x7 | uint8 | Component count |
| 0x8 | char* | Palette name pointer (relative to mesh data header) |
| 0xc | uint32 | Palette pointer (relative to mesh data header) |
The palette name must be set. It is the model's TPL file name (mario.tpl, L_mario.tpl), and it is per model: every UV channel of every submesh points at the same string. A model with its own TEX section resolves textures in-block and doesn't need the name. A model without one (block header +0x0C is 0, e.g. every L_* model) resolves its textures through this name alone. A submesh with an empty name there crashes the game the first time it is drawn on screen, not at load (Dolphin, 2026-09-19, L_mario). BlockValidator doesn't check this.
Rigid submeshes usually have 2 UV channels (368 of 385 surveyed). Channel 1 feeds the specular layer, and on most rigid surfaces it holds the same coordinates as channel 0.
On player models, UV channel 1 is dead data. Of the 1,076 submeshes in the corpus that declare two UV channels, 949 have a channel 1 that is a byte-for-byte copy of channel 0 (coordinates and per-loop indices). All 127 exceptions are in stadium/environment folders, where channel 1 is a real lightmap. The copy exists only so the vertex descriptor can declare a texture1 stream: the specular shader replaces that texture coordinate with one generated from the vertex normal (see Type 7). Editing channel 1 therefore has no visible effect on a character.
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Quantized raw normal data ptr (relative to mesh data header) |
| 0x4 | uint16 | Number of normals |
| 0x6 | uint8 | Quantization |
| 0x7 | uint8 | Component count |
| 0x8 | float | Ambient percentage (?). 0.0 in every vanilla header. |
The skinned submesh's normals are interleaved with its positions. In all 314 vanilla position buffers with component count 6, the normal header points at position data + 6, has the same count as the position header and component count 6, so every normal sits right after its position in one record. The SKN deformer rewrites these records every frame, normals included. The lighting index of every primitive vertex equals its position index. A separate normal array for such a submesh freezes its lighting in the bind pose, because the deformer never touches it: an early hammerspace normal edit on Tiny Kong's body did exactly that (2026-09-27, High and Low model). The patchers therefore keep one normal per position slot on a component-count-6 submesh; a corner that asks for a different normal than its slot's other corners cannot be stored without splitting the vertex.
Component count 0x83 (131) occurs in 45 vanilla normal headers, all on the ice-block models in folder 142 (ice_block, ice_block_break; quantization 62, positions with component count 3). Its meaning is unknown. Unedited, those headers are copied verbatim; normal edits on them are not exported (the add-on reports an error).
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Primitive data pointer (not used directly) |
| 0x4 | uint32 | Entry array pointer (relative to mesh data header) |
| 0x8 | uint16 | Number of entries in draw list |
| 0xa | byte[2] | Padding |
Once you've analyzed the other data sections, analyze the draw list entry by entry.
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint8 | Entry type |
| 0x1 | byte[3] | Parameter bytes. Not padding: their meaning depends on the entry type. For Type 7 they hold the shader parameters, including specular strength (see Type 7). |
| 0x4 | bitmap32 | Setting |
| 0x8 | uint32 | Primitive list pointer (relative to the mesh data header) (might be null). Must be 32-byte aligned in absolute terms (see Alignment). |
| 0xc | uint32 | Primitive list size. Always a multiple of 32; the list is zero-padded up to it (see Alignment). |
Each time you analyze a draw list entry, do two things: update the draw state, and draw the triangles specified by the entry's primitive list if one exists.
Each triangle drawn by the primitive list has three vertices, each with a combination of a position, a normal, up to two colors, and up to eight texture coordinates. The draw state specifies which of those components are in use and how data will be selected for each component.
How the draw state is changed depends on the entry type. The important entries I've run into are
This type specifies which components are provided to the primitive list for each vertex and how the data is chosen for each. The psuedocode is
state.expected_components = []
components = ['position', 'normal', 'color0', 'color1', 'texture0', 'texture1', 'texture2', 'texture3', 'texture4', 'texture5', 'texture6', 'texture7']
// the first couple bytes have to do with something called the 'position matrix index', I just ignore them
setting = setting >> 2
for component in components:
// the two bits in the setting describing this component
this_setting = setting & 0b11
enabled = this_setting != 0
// if this is set, the value is provided in the primitive list instead of an index into one of the data arrays in the mesh data
// I haven't seen this used
immediate = this_setting == 0b01
indexed = !immediate
index_size = 1 + (this_setting & 0b01)
if enabled:
expected_components.append({'name': component, 'immediate': immediate, 'index_size': index_size})
setting = setting >> 2
You may end up with a list like
[{'name': 'position', 'immediate': 0, 'index_size': 2},
{'name': 'normal', 'immediate': 0, 'index_size': 2},
{'name': 'color0', 'immediate': 0, 'index_size': 1},
{'name': 'texture0', 'immediate': 0, 'index_size': 2}
{'name': 'texture1', 'immediate': 0, 'index_size': 2}]
meaning that a primitive list at this point would expect 9 bytes per vertex: a two-byte index into the position list, a two-byte index into the normal list, a one-byte index into the vertex colors list, a two-byte index into the texture coordinates list to get a coordinate for texture layer 0, and another two-byte index to get a coordinate for texture layer 1, in that order.
This type of entry is simpler, it just describes which texture entry from the texture section of the file is used by a layer. Pseudocode:
texture_index = setting & 0b0001111111111111
setting = setting >> 13
layer = setting & 0b111
setting = setting >> 3
wraps = setting & 0b1111
setting = setting >> 4
wrapt = setting & 0b1111
state.texture_layers[layer] = {'index': texture_index, 'wraps': wraps, 'wrapt': wrapt}
so a setting of 0b00000000 00000000 001 0000000000101 would set texture layer 1 to whatever the 6th texture in the texture section is. wraps and wrapt specify what happens when the s and t (x and y) coordinates extend past the edge of an image, but I just assume the default behavior of whatever modeling software I'm using which is usually fine.
The top byte is not decoded. Above wrapt the word carries a byte that varies without any visible pattern: across the 493 Type 1 entries in force at a Shdw draw it is 11 (245, also the value in the verified built-in Spec list), 00 or 15, while layer and wrap modes are identical. The patcher copies it unchanged.
A Type 1 binding stays in force until the next Type 1 entry for that layer. A draw uses whichever texture is bound when its primitive list runs, and that binding may have been set by an earlier draw state. (Mario's sm0_ds5 uses the texture that sm0_ds0 binds.) So changing one setter changes the texture of every later draw that inherits it. To rebind a surface, the hammerspace patcher changes only the low 13 bits of the setter in force. Layer, wrap modes and the primitive data are kept. It adds no new display-state entries. If the setter also feeds a surface that is not getting the same new texture, the patcher refuses the edit. See Appending textures for binding appended textures.
A Type 7 entry selects a shader for the draws that follow and passes it three parameter bytes. The setting word is an ASCII FourCC naming the shader. For the common Spec family, parameter byte 1 is the surface's specular strength.
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint8 | Entry type (7) |
| 0x1 | uint8 | Specular strength (0-255, linear) for Spec/LhSp/RhSp/GhSp. For Shdw it is a bit index (1 << value). |
| 0x2 | uint8 | Unknown. 0 in 1,478 of 1,484 Spec entries; non-zero only on SpRf. |
| 0x3 | uint8 | Unknown, and no visible effect when patched (see below). Usually 100. |
| 0x4 | char[4] | Shader FourCC: Spec, LhSp, RhSp, Shdw, ... An all-zero word also occurs. |
| 0x8 | uint32 | Primitive list pointer |
| 0xc | uint32 | Primitive list size |
Example: Tiny Kong submesh 0, display state 5 at dt_na.dat offset 0x10394558 reads 07 64 00 64 'S' 'p' 'e' 'c': strength 100, byte 3 = 100. The exporter stores bytes 1-3 as the hex string DisplayStates[i].DisplayStateParamBytes (e.g. "640064"), and the strength byte is at ShaderModeFieldOffset - 3 in the file.
Confirmed in Dolphin, 2026-09-21. Patching Tiny Kong's submesh 0 display states 5/9/15 from strength 100/50/30 to 0/255/255 inverted their visible gloss exactly as predicted: 0 is fully matte (there is no fallback for 0), 255 is maximum, and the highlight's shape is unchanged. The value is per display state, so one submesh can carry several gloss levels. The shipped values also match the visible ranking (100 > 50 > 30).
In main.dol the Spec handler (0x804b441c) stores the byte as the alpha of a TEV konst colour, selects it with GX_TEV_KCSEL_Kn_A, and adds one TEV stage:
colour = previous + specularMap.rgb * (strength / 255)
The konst RGB (the highlight's tint) comes from a global at 0x807937d0, shipped as white FF FF FF. The highlight colour is therefore a scene property, and only its strength is per surface.
A synthesised Type 7 entry with parameter bytes 000000 has no highlight at all. A new glossy surface needs byte 1 set explicitly.
Shipped strength values across all 1,484 Spec entries cluster on round numbers: 150 (325), 100 (302), 50 (264), 60 (162), 0 (82), then 80, 70, 200, 180, 30, 10, 140, 130, 255 and 55, plus one-offs.
The Spec handler divides ±50.0 by one of its parameter bytes (0 falls back to 100) and loads the result into the sphere-map post-transform matrix (GX_PTTEXMTX). That would make it a highlight "spread" control, and byte 3 was the expected source: it is 100 in 777 of 1,484 Spec entries, and another 24 are 0. Patching byte 3 has no visible effect (Dolphin, 2026-09-21, Tiny Kong). In the cleanest test, every other specular surface on the model was set to strength 0, and the one remaining surface was compared at byte 3 = 50 and 200 with the same camera: the screenshots were identical. Which byte reaches the divide is unknown (byte 2, which is 0 on almost every Spec entry and would always take the fallback, is the untested candidate). Leave bytes 2 and 3 at their shipped values.
GXSetTexCoordGen2 with source GX_TG_NRM (normalised), a light-space rotation in GX_TEXMTX, and the standard sphere-map remap (u = 0.5·nx + 0.5, v = -0.5·ny + 0.5, template at 0x806d9398). The target is the model's last texture coordinate slot (GX_TEXCOORD1 on a two-texture character), which is why stored UV channel 1 data is ignored. There is no hardware per-vertex specular lighting.11002004: index 4, layer 1, GX_CLAMP), showing a radially symmetric bright blob that fades to black at the borders. Its descriptor has EdgeLODEnable = False and MinLOD = 0. Normals that scale past the blob clamp to the black border, i.e. no highlight. The blob's falloff is the shininess; its shape can only be changed by editing the image.tex1_64x64_d6da4880cee95b7b_14.png (92 of 686 models), tex1_64x64_8a05f75d65053b44_14.png (59) and tex1_64x64_a09662ae19841ea4_14.png (41). Replacing one through Dolphin's custom textures changes the highlight on every model that uses it. Painting it black removes the highlight regardless of strength.The shader table sits in main.dol at file offset 0x006d4048 (vaddr 0x806d7f48), right after the RTTI string Sys::Scene::CCompositeShader. Rows are 12 bytes: { char name[4]; u32 zero; void* handler; }. Counts are over all 2,407 Type 7 entries in the exported corpus.
| FourCC | Handler | Count | Notes |
|---|---|---|---|
Spec | 0x804b441c | 1484 | Sphere-map specular, described above. |
Shdw | 0x804a8414 | 410 | Shadow. Byte 1 is a bit index; shipped params 000000, 010000, 020000, 3c0000. Stadium and map objects only: on a character cap it crashes on load (Dolphin reports it) and draws black, both as the one-layer map-object list and as the cap's own list with only the mode changed (Mario, Wario; Dolphin, 2026-09-26). To make a character surface matte, set its specular strength to 0 instead. |
| (all zero) | - | 211 | |
RhSp | 0x80307e08 | 136 | Call Spec with the same arguments, then add one TEV alpha stage that picks APREV or RASA * APREV depending on a global flag (hand/equipment state). What the flag does (Dolphin, 2026-09-26, male Mii hands): while that hand wears a mitt, the surface's parts with vertex alpha 0 are cut away (the fingers inside the mitt); without the mitt the surface draws in full. Faces moved out of such a surface draw with their own vertex alpha, so alpha-0 vertices become permanently invisible unless they are repointed to an opaque colour entry. Only a real move of the faces into another record does this: giving Mario's skinned RhSp record the Type 7 state of a Spec surface while it kept its own primitive list did not work in game (recorded before 2026-09-17; cause not analysed). The same state copy between two Spec surfaces with the same texture did work. Hand roles occur on skinned lists and on the male Miis' rigid l_hand/r_hand submeshes (48 surfaces each). The built-in lists captured from those hands draw correctly as new surfaces on Mario's cap. |
LhSp | 0x80307d64 | 135 | |
GhSp | 0x80307eac | 4 | Same shape as the hand roles. Vanilla uses it only on Birdo's ring and diamond; as a new surface on Wario's cap (opaque vertex colours) it drew invisible (Dolphin, 2026-09-26). What makes it visible on Birdo is unknown. |
Audi | 0x800abe24 | 15 | Only byte 1 varies (01/02). |
SpRf | 0x804b4850 | 11 | Specular plus reflection. The only shader with a non-zero byte 2 (params c8323c, ff64ff, c83264). |
Oeka | 0x8044e6b0 | 1 | |
SpRB | 0x804b5050 | 0 | Not decoded. |
CgBt | 0x80256d94 | 0 | |
Foff | 0x804b596c | 0 | |
Bump | 0x801825d4 | 0 | |
Soft | 0x804b1eec | 0 |
Type 4 is not decoded. Observed values (player-folder survey, 2026-09-16):
fffffff0 with 1 channel (15/15), ffffff10 with 2 channels (368/368). Three or more channels occur twice, with inconsistent values.Shdw bind one layer and use fffffff0 on submeshes with two or three UV channels. A surface may therefore bind fewer layers than its submesh has UV channels, never more. The patcher writes Type 4 from the layers a new state group binds.A Type 6 entry sets culling, fog, blending, depth and alpha-test state for the draws that follow. Its setting word is a bit field, decoded from the handler in main.dol (2026-10-02). The handler is 0x8052d148, reached through the draw-state vtable at 0x806e27e0; the other entry types have their own vtables next to it (Type 1 0x8052b7b8, Type 3 0x8052c0ec). It calls the scene's render-state object (vtable 0x806dfe18), which forwards to GXSetCullMode (0x805bd510), GXSetFog (0x805c02c4), GXSetBlendMode (0x805c0614), GXSetAlphaUpdate (0x805c0690), GXSetZMode (0x805c06bc), GXSetZCompLoc (0x805c06f0) and GXSetAlphaCompare (0x805c0080). Bit numbers count from the least significant bit.
| Bits | Meaning |
|---|---|
| 0 | Culling: 0 = back faces culled (GX_CULL_BACK), 1 = double-sided (GX_CULL_NONE). |
| 1 | Fog on for this surface. Used only while a global fog mode at 0x80793da0 is 2; mode 0 forces fog off and mode 1 forces it on. |
| 2-3 | Blending: 0 = none (GX_BM_NONE), 1 = alpha blend (SRCALPHA, INVSRCALPHA), 2 = additive (SRCALPHA, ONE), 3 = subtractive (GX_BM_SUBTRACT). |
| 4 | Ignored by the handler (set in almost every shipped value). |
| 5 | Alpha write to the frame buffer (GXSetAlphaUpdate). |
| 6 | Depth test on. |
| 7 | Depth compare: 0 = GX_LEQUAL, 1 = GX_ALWAYS (passes regardless of depth). |
| 8 | Depth write on. |
| 9 | Depth test location: 1 = before texturing (early), 0 = after texturing. A cut-out needs 0, so that texels failing the alpha test write no depth. |
| 10 | Alpha test threshold: 0 = alpha > 0, 1 = alpha ≥ 0.5. The alpha test is always on; only the threshold changes. |
Common values, decoded:
| Value | Cull | Blend | Depth | Alpha test | Used for |
|---|---|---|---|---|---|
00000374 | back | alpha | LEQUAL, write, early | > 0 | The default on characters and stadiums. |
00000375 | none | alpha | LEQUAL, write, early | > 0 | Double-sided version of 374 (skinned lists only on characters). |
00000174 | back | alpha | LEQUAL, write, late | > 0 | Partly transparent surfaces, stadium audience. |
00000274 | back | alpha | LEQUAL, no write, early | > 0 | Stadium ground (e.g. the last 3,006 faces of the flat sta03 submesh 1) and most of the field markings (sta03 submesh 5). |
00000570 | back | none | LEQUAL, write, late | ≥ 0.5 | One-sided cut-out. |
00000571 | none | none | LEQUAL, write, late | ≥ 0.5 | Double-sided cut-out. |
000003f4, 000005f0 | back | alpha / none | ALWAYS, write | > 0 / ≥ 0.5 | Draws over everything drawn before it. Not suitable as a template. |
Observed values and in-game results:
00000374 or 00000174 with parameter bytes 010000 in both skinned and rigid lists. 00000375 appears only in skinned lists (12). (Player-folder survey, 2026-09-16.)00000374 on 2,633 surfaces, 2,588 of them with opaque textures; 00000570/00000571 on 263, 244 of them with cut-out textures (alpha only 0 or 255); 00000174 on 350, mostly cut-out or partly transparent. All shipped stadium Type 6 entries have parameter bytes 010000. Stadium surfaces bind one texture layer, have no normals (Type 3 pos+col0+tex0, e.g. 00000ccc), use Type 4 fffffff0 and mostly have no Type 7. Stadium CMPR textures never have partial alpha; partial alpha only occurs in C4, C8 and IA8.00000570/00000571, not 00000374 (Yoshi Park custom submesh, Dolphin, 2026-10-01). Cloned from the stadium's own Shdw surface sta03 sm1_ds4 (Type 6 00000374), the mesh draws correctly with an opaque texture, but a cut-out texture's transparent texels come out opaque black, although 63% of the encoded CMPR texels are transparent. The same mesh and texture cloned from sta03 sm1_ds42 / sta03b sm1_ds14 (also Shdw/000000, but Type 6 00000570) draw the cut-out correctly in both models, one-sided as bit 0 predicts. The bits alone don't explain the black: 00000374 still blends and tests alpha > 0, but its depth test runs before texturing.sta03 sm1_ds42 (the Yoshi-head logo, 00000570) is a flat quad at y = 0, on exactly the same plane as grass surfaces drawn before it (sm1_ds14/ds18/ds19). It flickers in the vanilla game when the camera moves (Dolphin, 2026-10-02), which matches z-fighting under GX_LEQUAL. Its Type 6 is an ordinary cut-out, so a copy of the surface that does not touch other geometry should not z-fight.sta03 bones 1 and 6 keeps every mesh in place and only reverses which draws first: the field markings (submesh 5, whose list sets Types 1, 4, 3 and 6 but no Type 7) then drew with their transparent texels opaque black and the white lines grey, from every angle, while the infield ground drawn last instead looked normal (Dolphin, 2026-10-18). A Type 7 020000/Shdw record (the one the markings normally draw with) added to their list, in front of the first draw or as the drawing record, changed nothing. The cause is not traced. Moving a vanilla stadium mesh earlier in the draw order is therefore not known to be safe.Every rigid submesh list starts with the entry types 1, 1, 4, 3, 6, 7, optionally followed by more 1/6/7 entries (4-24 entries in total). The most common list is T1(layer 0), T1(layer 1), T4 ffffff10, T3, T6 00000374, T7 Spec, where only the last entry draws primitives. Longer lists can have several entries that draw: Mario's head has 10 entries (1,1,4,3,6,7,1,1,1,7), 4 of which carry primitives.
00002828, 000028a8, 000028bc, 00003ca8 and 00003cbc.T1 L0, [T1 L1], T4, T3, T6, T7 after the last record. Display states are cumulative, so the group sets every value a draw depends on, cannot inherit a stray value from earlier records, and changes no earlier surface; every earlier state index stays valid. Type 1/6/7 come from the template surface's effective state (any same-model surface, or a built-in list), Type 4 from the number of texture layers the group binds (never more than the submesh has UV channels), and Type 3 is generated from the submesh's own attribute set and index widths, so all surfaces of the submesh index the same arrays. The patcher uses this for new surfaces on a rebuilt donor rigid submesh and on a custom submesh with more than one material. Confirmed in Dolphin (2026-10-07): Mario's cap with half its faces on an appended group from rigid:sm2_ds9, from derived:sm0_ds5 with a new PNG, and from the built-in Spec list with a new PNG all drew correctly. A donor surface whose faces all moved away keeps its record with a null primitive list (Mii body, same day). A custom submesh with three surfaces (its own template plus two appended groups, one on a new PNG) also drew correctly, on Mario's bone 49 and from a Blender export (same day).Spec surface draws, emitted in the order T1, T1, T4, T3, T6, T7, with Type 4 and Type 3 generated for the new attribute set;pony_l3, matching 17 vanilla rigid submeshes: T1 11110000/000008, T1 11002003/000000, T4 ffffff10, T3 000028a8, T6 00000374/010000, T7 Spec/640064) with the texture indices rebound to the host model.T1, T4 fffffff0, T3 (pos+col0+tex0), T6, T7 Shdw/000000, with Type 6 00000374 (opaque) or 00000570 (cut-out). No vanilla rigid submesh's whole list has this shape with the cut-out Type 6, so both are captured as the effective states of one Yoshi Park surface each (sta03 sm1_ds4, sm1_ds42); copies of those surfaces drew correctly (Dolphin, 2026-10-01). They are accepted only in stadium models (dirs 7-16) and only without normals. Avoid templates from hand-role (RhSp/LhSp) surfaces, which carry hand-visibility behaviour, and Type 6 00000375, which never occurs on a rigid submesh.| Offset | Type | Description |
|---|---|---|
| 0x0 | uint8 | Primitive type: 0x98 triangle strip, 0x80 quads, 0x90 triangles. These are the only opcodes in vanilla data (199,515 strips, 11,544 quad blocks and 5,377 triangle blocks across 1,408 blocks), all with vertex format 0 in the low 3 bits. A 0x00 byte ends the list; the rest up to the list size is zero padding. |
| 0x1 | uint16 | Vertex count |
| 0x3 | binary | Vertex data (follows the format set by entry type 3) |
Once you've parsed that, you'll have a bunch of vertices, each looking something like (assuming immediate positioning isn't used){'position': positions[378],
'normal': normals[350],
'color0': colors[5],
'texture0': texture_coords[254],
'texture1': texture_coords[875]}
Now you draw triangular faces from those vertices. The code from my awful python parser looks like:
match primitive:
case 0x90:
# TRIANGLES — winding is reversed on import
for i in range(0, len(vertexes), 3):
faces.append([vertexes[i + 2], vertexes[i + 1], vertexes[i]])
case 0x98:
# TRIANGLESTRIP
order = False
for i in range(len(vertexes) - 2):
if order:
faces.append([vertexes[i], vertexes[i + 1], vertexes[i + 2]])
else:
faces.append([vertexes[i + 2], vertexes[i + 1], vertexes[i]])
order = not order
case 0x80:
# QUADS
for i in range(0, len(vertexes), 4):
if i + 3 < len(vertexes):
faces.append([vertexes[i + 2], vertexes[i + 1], vertexes[i]])
faces.append([vertexes[i], vertexes[i + 3], vertexes[i + 2]])
case _:
print ("primitive type " + hex(primitive) + " not supported")
If you haven't parsed the entire primitive list by now, repeat the entire process. The list may draw a different type of primitive as well.
Winding for new geometry: build each triangle counter-clockwise as seen from outside, then store it reversed (the inverse of the import above). Checked on Mario's head: 1,496 of its 1,498 decoded faces agree with their stored normals under this convention (2026-09-16). A new cube written this way looked closed from every angle in game.
Rebuilt primitive lists are strips. Vanilla lists are almost entirely GX_TRIANGLESTRIP (Mario: 584 of 605 primitives, about 15 bytes per face at the usual 9-byte vertex); plain GX_TRIANGLES cost 27 bytes per face. Since 2026-10-10 every rebuilt blob (custom submesh, rigid rebuild, body rebuild) is encoded by a greedy stripifier (drawlist.stripify): strips are walked along directed edges in the decoded winding (a strip's triangle i is read as [s[i+2], s[i+1], s[i]] for even i and [s[i], s[i+1], s[i+2]] for odd i, so the alternating winding is implicit), each starting from the least connected unused triangle with the best of its three rotations; single triangles, degenerate triangles and triangles on a folded edge (a directed edge owned twice) go into one trailing GX_TRIANGLES block. Two corners are the same strip vertex only when every index the layout draws is equal, so strips break at UV and colour seams as vanilla's do. Re-encoding the vanilla lists of Mario, Luigi and Blue Toad this way gives 48-50 percent of the triangle-list bytes and 87-91 percent of the vanilla bytes, and decodes back to the same triangles with the same winding. A rebuilt Mario head shrank from 54,848 to 34,944 bytes. Confirmed in Dolphin (2026-10-10, Mario head rigid rebuild and Luigi body rebuild): culling is correct on every surface, with no missing or inside-out faces. The in-place list patchers, which move existing faces between records, still write GX_TRIANGLES.
See the overview for the rules across the whole block. Within GPL, "aligned" means 0 mod 32 relative to the model block. The GPL section start is itself aligned, so this is the same as GPL-relative alignment.
| Data | Rule | Evidence |
|---|---|---|
| Skinned position buffer (component count 6) | 32-byte | 189 / 189 |
| Primitive-list pointer | 32-byte | 9,465 / 9,465 |
| Primitive-list size | multiple of 32 | every donor list |
| Non-skinned position (component count 3) | none | 3,055; every mod-4 value observed |
| Color, standalone normal, UV | none | 3,244 / 3,055 / 4,562; every mod-32 value observed |
| Mesh data header (DOLayout blob) start | none, but see Moving blobs | 3,244; every mod-4 value observed |
Vanilla mesh data headers are not aligned. On Mario, blob 0 starts at GPL+0x2C, directly after the 3-entry mesh header array. Every pointer inside a blob is relative to its mesh data header, so the blob's aligned arrays are aligned only because of the blob's own mod-32 offset. Rules for rebuilding:
PatchGPLAppendSubmesh rounded the relocated blob region up to a plain 32-byte boundary. That broke the alignment of submesh 0's skinned positions and of every primitive list in the model; BlockValidator caught it on the first real-donor run. The fix rounds up to the next offset with the same mod-32 residue as the old position (_align32_residue).GX_TRIANGLES block, see strips), keeps the donor's attribute formats, and widens a Type 3's index fields from u8 to u16 only where the new entry counts need it (Mario's cap subdivided to 838 positions: 00003ca8 to 00003cbc; Dolphin, 2026-10-07). A rigid submesh whose Type 3 has no color0 may still point at a one-entry colour array (Mii bodies); the rebuild carries it over unchanged. Confirmed in Dolphin (2026-09-26): Mario's cap blob copied verbatim to a new spot with its descriptor repointed and the original left behind renders identically, so the engine tolerates unreferenced whole blobs and they need no zero-fill or compaction.gplVertexArr, gplDestArr and memClrPtr are relative to submesh 0's position array, so relocating that array as a unit leaves them valid.Since 2026-10-09 the hammerspace patcher rebuilds submesh 0 from any mesh and weights (SkinnedRebuild, the layout of the Sluggers Characters Beta tool's build_body). It is used when the body's vertices or faces change, when it is moved in Object Mode, when faces move to another surface in a way the record-preserving path cannot do (a partial move, a move between shaders, or an Add-material surface), or when a vertex's set of bones changes. Confirmed in Dolphin (2026-10-09): added vertices, removed vertices, and Luigi with and without his facial poses; strips the next day. The new blob is inserted before the GPL user data like a rigid replacement (see Moving blobs), and holds in order:
x y z nx ny nz records and unused gap slots;max(gplVertexArr + align32(vertexOffset + vertexCount × stride))), so no later array lies in bytes the deformer writes (see runtime limits);position + 6 with the slot count, as in vanilla (see Normal data). Position and lighting indices in the primitives are the slot.BuildGPLMeshData has three layout modes:
align32(end of mesh header array), and each blob end is rounded up to 32.In every mode, the GPL user data starts on a 32-byte boundary. Its length isn't a multiple of 32 (40 bytes on Mario), so the section can end unaligned; BuildHEADERModelBlock pads it before the next section.