ptr7)The optional model-header pointer at +0x18 is block-relative and can point to facial pose data. This was verified on Birdo's high-detail catherine.gpl. Her low-detail L_catherine.gpl has a null ptr7 and no facial pose section.
The runtime copies selected pose coordinates from this section into the GPL head position and normal arrays. Consequently, editing only the GPL position array is temporary for mapped vertices: blinking and mouth animation restore the original coordinates.
All pointers below are relative to the start of this section. Every fact in this layout holds for all 70 vanilla blocks that have the section (159 attribute records, survey of the input dt_na.dat, 2026-10-10). The runtime facts come from the game's facial code in main.dol (applier 0x805325B8, loader 0x805324E4, type resolver 0x80532294; read 2026-10-09).
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint16 | Maximum pose count advertised by the section |
| 0x2 | uint16 | Facial object count |
| 0x4 | uint16 | Type descriptor count |
| 0x6 | uint16 | Padding (0) |
| 0x8 | uint32 | Facial object table pointer (0x14 + 16 × type count) |
| 0xc | uint32 | Type descriptor array pointer (always 0x14) |
| 0x10 | uint32 | 0 |
One 16-byte descriptor per distinct attribute target, starting at 0x14. Every attribute record's format bytes match one of them.
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint16 | Target index: a GPL submesh, or for kind 0 an SKAcc entry |
| 0x2 | uint8 | Attribute kind (see below) |
| 0x3 | uint8 | component count << 4 | component size (0x32: 3 × int16, 0x62: 6 × int16, 0x61: 6 × int8) |
| 0x4 | byte[12] | Zero in the file. The loader stores the resolved target pointer at +0x8. |
Where the loader points each kind:
SK1[0].srcArrPtr + first_vertex × stride, i.e. the SKN source mirror, with slot 0 assumed at the first SK1 entry. In all 318 vanilla SKN sections with SK1 entries, SK1[0] has the lowest destination and gplVertexArr = 0, so this is the mirror base itself.index. Kinds 4-7 go to UV channels. Vanilla uses only kinds 0, 1 and 2.index (6 × int8 records, written through the same conversion path): a supplement for facial vertices with a third bone. Kritter, King Boo, Peach, Daisy and Diddy have them (11 records).Each object-table entry is 12 bytes:
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint16 | Pose count for this object |
| 0x2 | uint16 | Attribute count for this object |
| 0x4 | uint32 | Attribute record size (0x0C + pose_count * 4) |
| 0x8 | uint32 | Object attribute-data pointer |
An object on a rigid submesh has a kind-1 position attribute of 3 × int16 records and usually a kind-2 normal attribute of 3 × int16 records. An object on the skinned body has one kind-1 attribute with interleaved x y z nx ny nz int16 records and no separate normal attribute (27 models, 40 objects); some add kind-0 SKAcc attributes. The format bytes of an attribute record read xx 01 03 02 for a rigid position record, xx 02 03 02 for a normal record and xx 01 06 02 for a body record, where xx is the target submesh.
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Number of mapped entries per pose |
| 0x4 | byte[4] | Target index, attribute kind, component count, component byte width |
| 0x8 | uint32 | Run-list pointer |
| 0xc | uint32[pose_count] | Pose-array pointers |
Each run is two big-endian uint16 values: first_vertex and run_length. Expanding all runs gives one target-array index for each pose entry. Every run list ends with a (0, 0) pair. The applier reads pairs until it meets a count of 0; the record's entry count is not used as a bound. All 159 vanilla run lists end that way, and their runs add up to the entry count. A list without the terminator makes the game read on into pose zero and use coordinates as vertex indices: a rebuilt Luigi crashed at character select that way (Dolphin, 2026-10-09). BlockValidator fails a list without it.
Position entries contain either XYZ (6 bytes) or interleaved XYZ+normal XYZ (12 bytes), using big-endian signed 16-bit components. Pose zero is an absolute rest array: position pose zero matches the mapped GPL position vertices exactly, and normal pose zero likewise matches the mapped GPL normal vertices. Poses one and later are displacement vectors relative to pose zero, not absolute coordinates. A zero triplet in these poses means that the mapped entry remains at its rest value.
Packing: vanilla sections have no gaps. After the header and type descriptors come the object table, then per object its attribute records, its run lists (in attribute order) and its pose arrays (attribute-major, then pose order). The section is zero-padded to a multiple of 32 bytes. Hammerspace/FacialPoseRebuild.py re-serializes all 75 exported sections byte for byte with these rules.
The section contains two facial objects. Their position records map 199 and 451 head vertices respectively, with five poses per object. The corresponding normal records map 247 and 520 normal entries. The low-detail model contains no section, which explains why ordinary GPL editing worked there.
The section-level maximum pose count is 5 in the surveyed corpus, but each object stores its own count. Observed object counts range from 2 through 6, so record sizes and pointer-array lengths must use the per-object value.
For every mapped position vertex in absolute pose zero:
patched_pose = original_pose + (edited_GPL_position - original_GPL_position)
Poses one and later are already deltas, so a base mesh edit does not alter them. Explicit Blender shape-key edits replace their corresponding delta buffers. --unpatch restores all original pose arrays.
The hammerspace patcher applies the same arrays to a submesh it does not rebuild (since 2026-10-15, facial_pose_edits.py, shared with the in-place patcher): the arrays keep their length, so the section is re-serialized with only those pose bytes changed. Confirmed in game on Birdo's catherine.gpl (2026-10-15); not yet tested on other characters. Before, a hammerspace build dropped them and a slot patch refused the file.
This is separate from SK1/SK2/SKAcc skinning. Birdo's facial head remains a rigid GEO attachment on bone 56; bone 55 has no SK entry or vertex influence in either LOD and is not responsible for the per-vertex overwrite.
Facial objects can also target the skinned submesh. Luigi's two objects (SubmeshIndex 0, interleaved 6-component records) map 64 and 356 vertices: 341 in SK1(51) (head), 12 in SK2(50,51) (neck) and 3 in SK1(18) (the first object's 64 are all SK1(51)). Every one is written by an SK1/SK2 entry, so the skinning deformer overwrites the position buffer each frame. Copying the pose into the GPL position array alone would therefore have no visible effect.
In-game evidence shows the pose data lands in the SKN source data at the mirrored address VAR_DATA_OFF + vertex_index * stride (see the source mirror rule), not through the entry's own source pointer. A rebuilt Luigi SKN with correct per-entry pointers but source arrays 0x20 bytes off the mirror stretched exactly the facial-mapped neck vertices at the back of the head down to the floor. The pose records were written 8 bytes out of step into neighbouring source records. Mirroring the sources fixed it.
No pointers into SKN exist in the facial section (checked for both SKN-relative and block-relative values). The loader resolves the target at load time from SK1[0].srcArrPtr (see Type descriptor), which is the mirror base whenever the first SK1 entry starts at slot 0.
Implications:
BlockValidator enforces this for every skinned model.head, submesh 2, carries both of his objects), and its normal runs name entries of the submesh's normal array. Adding, deleting or reordering that submesh's vertices breaks the poses, so a whole-blob rebuild of such a submesh (RigidRebuild) rebuilds the section too: since 2026-10-10 the exporter writes the mesh's facial shape keys as RigidRebuild.FacialPoses (per object the rebuilt vertices a pose moves with x y z int16 deltas, and the rebuilt normal entries it tilts with nx ny nz deltas; the normal entries of animated vertices are pooled per vertex so each has one delta), and the patcher maps the object's position and normal attributes onto those entries (pose zero copied from the rebuilt arrays, runs from the sorted entries with the terminator pair, other objects unchanged). A rebuild without FacialPoses (older export, or a mesh whose shape keys were deleted) keeps the objects well-formed but neutral: one run on entry 0 with zero deltas. Moving such a submesh to another bone by its GEO ID alone leaves its bytes, and so the runs, unchanged.Survey of the rigid targets (2026-10-10, all 46 exported models with a facial object on a CompCount-3 submesh): every object carries a kind-1 position attribute of 3 x int16 records and, except Mare (mare.gpl), a kind-2 normal attribute of 3 x int16 records; no other attribute kind addresses a rigid submesh. Both pose zeros equal the mapped GPL records (positions and normals), every run list is ascending, unique and terminated.
When the hammerspace patcher rebuilds a submesh that facial objects target, it re-serializes the whole section (Hammerspace/FacialPoseRebuild.py): every object on another submesh is copied unchanged, and each object on the rebuilt submesh gets new run lists and pose arrays. The new section replaces the old one in the block's trailing data, and ptr6/ptr7/ptr8 are recomputed from the new section lengths.
Basis plus facial_object_<o>_pose_<p>) as SkinnedRebuild.FacialPoses: per object the vertices any pose moves, in ascending order, and per pose one x y z nx ny nz int16 delta per vertex. The normal delta is the change of the key's vertex normal against Basis. After the skin layout, the patcher maps each vertex to its new slot. Pose zero is copied from the rebuilt position buffer, the deltas follow in slot order, and the runs come from the sorted slots plus the terminator. Kind-0 attributes and their type descriptors are dropped with a warning, so facial vertices with a third bone lose that supplement.RigidRebuild, since 2026-10-10, Dolphin-confirmed on Mario's head): see the rigid-target note above.+0x18 = 0) when an object targets it; the section bytes stay in the block, unreferenced. The game runs, with no blink or mouth animation (Dolphin, 2026-10-09, Luigi; the Sluggers Characters Beta tool does this for all of its characters). A rebuilt rigid submesh keeps the section and neutralizes its objects instead (one run on entry 0, zero deltas), so the poses of the other submeshes survive.