This section has a bunch of textures
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint16 | Number of textures |
| 0x2 | uint16 | Number of CLUTs (color lookup tables). No vanilla block sets it: the only vanilla models with a palette (Donkey Kong and Baby DK High, chunks 20 and 83 file 0, one 128x128 C8 texture with 256 entries each) leave it at 0, so the game most likely ignores it. BlockValidator only warns about 0 with palettes present; any other mismatch fails (2026-10-15). The Hammerspace TEX builder writes the real palette count. |
Immediately following the header (at offset 0x4 from the start of the file) is an array of texture descriptors.
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Image Data Pointer (relative to header) |
| 0x4 | uint32 | Palette Data Pointer (relative to header) (might be null) |
| 0x8 | uint16 | Image height |
| 0xa | uint16 | Image width |
| 0xc | bool | Edge LOD enable (?) |
| 0xd | uint8 | Min LOD (?) |
| 0xe | uint8 | Max LOD (?) |
| 0xf | bool | Unpacked? |
| 0x10 | byte[7] | Unknown/padding |
| 0x17 | uint8 | Image format |
| 0x18 | uint16 | Palette entry count |
| 0x1a | uint8 | Palette format |
| 0x1b | byte[5] | Unknown/padding |
The packed image-data region starts at the first 32-byte boundary after the descriptor table, and every image and palette payload must also start on a 32-byte boundary relative to the model block. Pad between payloads with zeros.
The GX texture image-base register stores the physical address shifted right by 5 bits, so the GPU (and Dolphin) can only address a texture on a 32-byte boundary. A payload at 8 mod 32 is sampled 8 bytes early. For CMPR that is one 4x4 sub-block, which shows up as fine pixel noise over a texture that is still recognisable. That makes the error easy to miss: an earlier test judged such an image correct. Across 1,422 unique vanilla model blocks, all 7,011 image payloads and all 654 palette payloads are at 0 mod 32. The TEX section start itself must be aligned too (see overview).
The "judged correct" case was an appended-texture test build (Mario): a 505x505 CMPR payload (129,032 bytes) was followed by an image at TEX-relative 0x5C108 (8 mod 32). On 2026-09-16 that rule was reversed. BuildTEX now pads the start of every image and palette payload to the next 32-byte boundary, which moved that image to 0x5C120; the rebuilt fixture passed in Dolphin. BlockValidator rejects a misaligned payload.
This is probably a version of the TEX0 format. I extract the textures by shuffling around the data to create a valid TPL file and then converting it to a png with wimgt. I don't know where the LODBias, wrapS, wrapT, minFilter, and magFilter are in the header, so I assume they're 0.
def toFile(self, path):
dataLength = self.height * self.width * {0:4,1:8,2:8,3:16,4:16,5:16,6:32,8:4,9:8,10:16,14:4}[self.format] >> 3
hasPalette = self.paletteDataPtr > 0
fname = path + '.tpl'
pngname = path + '.png'
out = open(fname, 'wb')
# TEXPalette
out.write(itb(0x0020AF30, 4))
out.write(itb(1, 4))
out.write(itb(0xC, 4))
# TEXDescriptor
out.write(itb(0x14, 4))
if hasPalette:
out.write(itb(0x38, 4))
else:
out.write(itb(0, 4))
# TEXHeader
out.write(itb(self.height, 2))
out.write(itb(self.width, 2))
out.write(itb(self.format, 4))
out.write(itb(0x40 + hasPalette * 0x20, 4))
out.write(itb(self.wrapS, 4))
out.write(itb(self.wrapT, 4))
out.write(itb(self.minFilter, 4))
out.write(itb(self.magFilter, 4))
out.write(itb(self.LODBias, 4))
out.write(itb(self.edgeLODEnable, 1))
out.write(itb(self.minLOD, 1))
out.write(itb(self.maxLOD, 1))
out.write(itb(self.unpacked, 1))
paletteDataLen = self.paletteEntries * 2
paletteDataOffset = 0
paletteDataPad = 0
if hasPalette:
paletteDataOffset = 0x60 + dataLength
mod = paletteDataOffset % 0x20
if mod != 0:
paletteDataPad = 0x20 - mod
paletteDataOffset += paletteDataPad
# Palette header
out.write(itb(self.paletteEntries, 2))
out.write(itb(1, 1))
out.write(itb(0, 1))
out.write(itb(self.paletteFormat, 4))
out.write(itb(paletteDataOffset, 4))
# Pad to multiple of 0x20 (0x40 or 0x60 here)
out.write(itb(0, 0x8 + hasPalette * 0x14))
self.parent.seek(self.dataPtr)
out.write(self.parent.read(dataLength))
if hasPalette:
out.write(itb(0, paletteDataPad))
self.parent.seek(self.paletteDataPtr)
out.write(self.parent.read(paletteDataLen))
out.close()
os.system('wimgt decode -q -d ' + pngname + ' ' + fname)
os.remove(fname)
A model's TEX section can take more textures than it shipped with. Confirmed in Dolphin with Mario (2026-09-11/12; character select, a static scene and animated gameplay): the TEX count was raised from 5 to 6 and a draw state was pointed at the new texture 5. Everything else in the model rendered unchanged. Later end-to-end runs through the Blender exporter and the hammerspace patcher also passed.
0x1FFF.Spec, RhSp) and different Type 1 setter positions. Both rendered correctly in Dolphin. The image format was the same for both.Tooling limits, not format limits: the patcher writes only the base image level. An addition whose template is mipmapped therefore gets that template's format and remaining sampler bytes, but single-level mip state: +0xE = 1, the float at +0x10 = 0.0 and the mip count at +0x16 = 0. Across the 7,136 descriptors in the exported models (2026-10-01), these three fields are the only ones that differ between mipmapped and plain textures. Mipmapped textures mostly have +0xE = 5 and a float from -3.0 to 0.0; plain ones have +0xE = 0 or 1 and a float from 0.0 to 0.3. That fits +0xE being the minification filter (5 = GX_LIN_MIP_LIN) and +0x10 the LOD bias, but this is not confirmed. The patcher refuses indexed formats (C4, C8, C14X2) for additions, and a direct-colour format never gets a palette. Two additions with the same image (same file name, or identical pixels at the same size) and the same template format/sampler fields are stored once. This keeps the block from growing needlessly: before this merge, four surfaces sharing one 1024x1024 CMPR texture grew a block from 0.47 MB to 2.63 MB.
L_) models bind the High model's texturesAn L_ model has no TEX section. Its Type 1 texture binds are indices into its high-poly partner's TEX (the partner is the neighbouring file index in the same chunk, with the same geo-name stem). A Low model can therefore bind any texture of the High model, including textures appended to it, by index (the High model's n-th appended texture has index (High descriptor count) + n), as long as the High model is patched first.
Retargeting the High model's binds while the Low model keeps its own is harmless: at a distance the character simply shows the Low model's (old) textures. The same Type 1 parameter byte does not always name the same surface in both models, so binds can't be mirrored by parameter byte alone. In Tiny Kong's body, by face count and specular strength, High slot 07 (Spec 0x1E) is the surface Low draws through slot 09, and High 09 (Spec 0x64) the one Low draws through 07.
Evidence (Tiny Kong, Dolphin, 2026-10-07). A probe build from vanilla data, with the High model in hammerspace plus one appended CMPR texture (index 5) and the Low model vanilla in place; only the Type 1 setting words were changed between runs:
| Build | High sub0 binds | Low sub0 binds | Result |
|---|---|---|---|
| 64x64 texture (pair 586 KB) | vanilla | vanilla | Works |
| 64x64 texture | slots 00/04/07/09/0a on 5 | vanilla | Works (several runs, several stadiums and teams) |
| 64x64 texture | same | slots 00/04/07/09 on 5 | Works (several runs) |
| 1024x512 texture (pair 846 KB) | vanilla (texture 5 unbound) | vanilla | 1 crash on load in 3 runs |
The large build crashed with no bind change at all, so it is memory, not the binds: every player on the field has a fixed 870,400-byte heap for its High model, Low model and bat or glove, and 846 KB plus a glove does not fit (found 2026-10-10 in Dolphin RAM dumps; _docs_roster/RosterExpansion.md, Game memory). It crashed in 1 run of 3 most likely because Tiny Kong only has to load as a fielder in some runs; which team bats first is random. An earlier theory (2026-09-27) that some Low binds must follow a High retarget, and others must not, came from single runs of edited Tiny Kong builds of about 0.73 MB (High) and is withdrawn: those builds sat in the size range where crashes are random. The September crashes showed in Dolphin as IntCPU: Unknown instruction at PC 0x400, a jump through a corrupt member-function pointer.
Tooling: the patcher compares the pair's binds and logs the differences at info level; it never changes the Low model's binds on its own. The Blender exporter writes texture reassignments for both models. A Low model's DesiredTextureAssignments are indices into the High model's TEX: the patcher applies them with only the GPL builder (there is no TEX to rebuild), bounded by the 13-bit field, and refuses to write a Low block that binds an index past the High model's current TEX count (2026-10-16).