Decode tfrag texture bindings, vertex colours and light records

Adds level_core.rs (the uncompressed core index header and texture table)
and extends tfrag.rs with the data a draw call needs beyond geometry.

An ad-gif is five A+D quadwords holding packed fields rather than GS
registers; the level load rewrites them in place before drawing. On disc
TEX0_1.data_lo indexes the level's texture table, TEX1_1.data_lo is the
LOD bias K and data_hi the minification filter, and CLAMP_1 holds both
wrap modes.

Three things are easy to get wrong here:

* K is a signed 12-bit value in 1/16 units, sign-extended by shifting the
  field up 4 and back down. Retail values are -137..-89 mip-scaled;
  reading it unsigned gives 1911 instead of -137.
* TEX1_1.data_hi is MMIN, and 4 decodes as LinearMipmapNearest, not
  LinearMipmapLinear as the raw bit layout suggests.
* RGBA and the light/normal records are not in the VIF lists at all. They
  sit at fixed offsets in the patch data block: rgba_size*4 colours at
  rgba_ofs, vert_count light records at light_ofs+0x10. The tfrag origin
  lives there too -- if no STROW row supplied one, the quadword at
  light_ofs is it.

The texture table checks out on its own terms: 93 entries at index offset
0x2900 on level 0, and 0x2900 + 93*16 lands exactly on the next range.

Verification: an ad-gif index outside the level's table would mean the
geometry and the textures were misread independently, so every one is
range-checked against real entries. All 27 levels: 47,950 ad-gifs over
18,102 patches, all in range, referencing 1429 of 1595 level texture
entries (90%). Each patch's light array is exactly vert_count records and
its RGBA array exactly rgba_size*4.

Still open: LOD selection and morphing, and the texel data itself.
This commit is contained in:
2026-10-10 02:22:51 +02:00
parent 9459f15329
commit 48d9506cf1
6 changed files with 829 additions and 15 deletions
+8 -3
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: tfrag texture bindings, then the geometry directory's class tables.
- [ ] Next: tfrag LOD selection and morphing, then the first drawable frame.
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,
@@ -128,8 +128,13 @@ Phase 1 hands over the extents; this phase works out what each one contains.
rather than positions. Every patch's strips expand to exactly its own
`tri_count`: **18,102 patches, 809,879 triangles**, all indices in range.
See `plan/level-assets.md` and `gc-formats::tfrag`.
- [ ] tfrag shading inputs: the ad-gif texture bindings, per-vertex RGBA and
light records, then the LOD switch and morph rules in `TfragProc`.
- [x] **tfrag texture bindings and per-vertex data.** The ad-gifs decode to a
texture index, wrap modes, minification filter and LOD bias; the level
index gained a texture table (`level_core.rs`) for those indices to land
in. Per-vertex RGBA and light/normal records recovered from their fixed
offsets. All 27 levels: 47,950 ad-gifs, every index in range, 90% of the
level texture tables referenced.
- [ ] tfrag LOD selection: the switch and morph rules in `TfragProc`.
- [ ] `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