Parse tfrag terrain blocks and decode all vertex positions

Adds gc-formats::tfrag and a gated real-disc test that decodes every
terrain patch on all 27 levels.

Two rules decide whether this works at all, and both were wrong on the
first attempt:

The vector format is encoded in the VIF command byte, not the immediate.
Bits 2-3 are vn (component count minus one) and bits 0-1 are vl, giving
element_size = ((32 >> vl) * (vn + 1)) / 8. Terrain positions are vn=2,
vl=1 (command 0x69) at 6 bytes. Reading the format from the immediate's
low nibble -- the more commonly quoted layout -- yields 12 or 16 bytes
and misaligns everything after it.

Unpack payloads pad to 4 bytes, and several non-unpack commands carry
inline data: STROW/STCOL 16, STMASK 4, MPG num*8, DIRECT/DIRECTHL
imm*16. Ignoring those desynchronises the walk, and a desynchronised
walk still finds code words that look like positions.

Positions are also not all in one list. vert_count is the total across
three tiers, the LOD-2 list precedes the common one in offset order, and
the LOD-0-only list starts at shared_ofs + lod_1_size*16 rather than at
an offset of its own. Scanning only the common list matches about a
quarter of patches, each exactly -- which is what made it convincing.

The invariant is that every patch yields exactly its own vert_count. It
caught both errors, so tfrag_positions returns None on a mismatch rather
than partial geometry. All 27 levels: 18,102 patches, 821,439 vertices,
zero mismatches.

Still open: strips, triangle indices and the RGBA vertex-info arrays.
Positions alone are not drawable.
This commit is contained in:
2026-10-10 02:07:02 +02:00
parent 3ec548efe8
commit 36641b4cf8
5 changed files with 782 additions and 5 deletions
+11 -4
View File
@@ -20,7 +20,7 @@ early, while they are still cheap to retire.
header, and the `data` extent is parsed as a container of byte ranges. The
geometry decompresses: level 0 yields 16,969,296 bytes of level content
from a 9.7 MB stream.
- [ ] Next: the geometry directory's class tables, then `tfrag` terrain.
- [ ] Next: tfrag triangle topology, then the geometry directory's class tables.
The disc being the exact build the reference projects pin is the single
biggest favourable fact so far: no disc conversion, no regional timing work,
@@ -114,9 +114,16 @@ Phase 1 hands over the extents; this phase works out what each one contains.
- [x] Region layout of `core_data` established: tfrag, occlusion, sky, collision
and `textures_base` are offsets, strictly increasing on all 27 levels.
Terrain is `[0, occlusion)` and carries inline texels.
- [ ] **tfrag vertex format: not recovered.** The 64-byte record framing and the
scale-8 fixed-point reading were both tested and disproven; see
`plan/level-assets.md` for what was ruled out.
- [x] **tfrag vertex format: recovered.** The tfrags block header gives a table
offset (`0x40`) and a patch count; each patch is a 0x40-byte header whose
`data` offset is relative to the table, not to the block. Positions are
V3_16 payloads inside a VIF1 command list, split across three lists per
patch (common, LOD 0+1, LOD 0). The whole vector format is encoded in the
command byte and every element size follows from it. Verified on all 27
levels: **18,102 patches, 821,439 vertices**, each patch yielding exactly
its own `vert_count`. See `plan/level-assets.md` and `gc-formats::tfrag`.
- [ ] tfrag topology: strips, triangle indices and the RGBA vertex-info arrays,
which is what turns positions into drawable geometry.
- [ ] `gameplay` — begins `WAD` + a per-level byte, and does *not* decode with
the WAD LZ77 decoder. All 27 levels fail identically, so the input is
wrong rather than the decoder. Ruled out: bit order, header length, the