Mesh Data Section

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.

Header

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)

GPL user data: descriptor chain and culling boxes

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:

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 dataModels
kind 1 onlysta03, 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 1sta02b 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 reachedsta04, 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).

Mesh Header

Offset Type Description
0x0 uint32 Mesh data header pointer (relative to header)
0x4 char* Mesh name pointer (relative to header)

Mesh Data 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.

Position Data

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).

Color Data

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).

UV Map Data

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.

Normal Data

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).

Draw List

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.

Draw List 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.

Update the draw state

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

Type 3

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.

Type 1

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.

Type 7 (shader / specular)

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
0x0uint8Entry type (7)
0x1uint8Specular strength (0-255, linear) for Spec/LhSp/RhSp/GhSp. For Shdw it is a bit index (1 << value).
0x2uint8Unknown. 0 in 1,478 of 1,484 Spec entries; non-zero only on SpRf.
0x3uint8Unknown, and no visible effect when patched (see below). Usually 100.
0x4char[4]Shader FourCC: Spec, LhSp, RhSp, Shdw, ... An all-zero word also occurs.
0x8uint32Primitive list pointer
0xcuint32Primitive 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.

Specular strength (byte 1)

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.

Byte 3 (no known effect)

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.

How the highlight is drawn
Shader FourCCs

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.

FourCCHandlerCountNotes
Spec0x804b441c1484Sphere-map specular, described above.
Shdw0x804a8414410Shadow. 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
RhSp0x80307e08136Call 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.
LhSp0x80307d64135
GhSp0x80307eac4Same 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.
Audi0x800abe2415Only byte 1 varies (01/02).
SpRf0x804b485011Specular plus reflection. The only shader with a non-zero byte 2 (params c8323c, ff64ff, c83264).
Oeka0x8044e6b01
SpRB0x804b50500Not decoded.
CgBt0x80256d940
Foff0x804b596c0
Bump0x801825d40
Soft0x804b1eec0

Types 4 and 6

Type 4 is not decoded. Observed values (player-folder survey, 2026-09-16):

Type 6 (pixel state)

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.

BitsMeaning
0Culling: 0 = back faces culled (GX_CULL_BACK), 1 = double-sided (GX_CULL_NONE).
1Fog 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-3Blending: 0 = none (GX_BM_NONE), 1 = alpha blend (SRCALPHA, INVSRCALPHA), 2 = additive (SRCALPHA, ONE), 3 = subtractive (GX_BM_SUBTRACT).
4Ignored by the handler (set in almost every shipped value).
5Alpha write to the frame buffer (GXSetAlphaUpdate).
6Depth test on.
7Depth compare: 0 = GX_LEQUAL, 1 = GX_ALWAYS (passes regardless of depth).
8Depth write on.
9Depth test location: 1 = before texturing (early), 0 = after texturing. A cut-out needs 0, so that texels failing the alpha test write no depth.
10Alpha test threshold: 0 = alpha > 0, 1 = alpha ≥ 0.5. The alpha test is always on; only the threshold changes.

Common values, decoded:

ValueCullBlendDepthAlpha testUsed for
00000374backalphaLEQUAL, write, early> 0The default on characters and stadiums.
00000375nonealphaLEQUAL, write, early> 0Double-sided version of 374 (skinned lists only on characters).
00000174backalphaLEQUAL, write, late> 0Partly transparent surfaces, stadium audience.
00000274backalphaLEQUAL, no write, early> 0Stadium ground (e.g. the last 3,006 faces of the flat sta03 submesh 1) and most of the field markings (sta03 submesh 5).
00000570backnoneLEQUAL, write, late≥ 0.5One-sided cut-out.
00000571nonenoneLEQUAL, write, late≥ 0.5Double-sided cut-out.
000003f4, 000005f0backalpha / noneALWAYS, write> 0 / ≥ 0.5Draws over everything drawn before it. Not suitable as a template.

Observed values and in-game results:

Rigid submesh state lists

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.

Draw the primitives

A draw list entry won't always have a primitive list - if the primitive list pointer is 0 you skip this step. If there is a primitive list, then the structure is as follows:
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.

Alignment

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

Moving blobs

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:

Rebuilding the skinned body

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:

  1. the mesh data header, UV, normal and draw-list headers, the mesh name and the UV palette names;
  2. the position buffer on a 32-byte boundary, in the slot order of the new SKN (see canonical layout), with interleaved x y z nx ny nz records and unused gap slots;
  3. a gap up to the aligned write-back end of the SK1/SK2 entries (max(gplVertexArr + align32(vertexOffset + vertexCount × stride))), so no later array lies in bytes the deformer writes (see runtime limits);
  4. the colour array and one array per UV channel, each pooled from the exported per-loop values;
  5. the display-state records, then the primitive lists, each on a 32-byte boundary and padded to a multiple of 32.

BuildGPLMeshData has three layout modes:

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.