Facial Pose Section (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.

Verified layout

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

Header

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

Type descriptor

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:

Object table entry

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.

Attribute record

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.

Birdo high-detail model

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.

In-place patch rule

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 poses on skinned vertices (Luigi)

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:

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.

Rebuilding the section

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.