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
+40 -3
View File
@@ -357,8 +357,45 @@ element size from the immediate, 2-byte payload padding, ignoring non-unpack
payloads, and banking strips by arrival order. Every one produced parseable,
plausible geometry; only the header's own triangle count caught them.
### Texture bindings, colours and lights
Also settled. Adds `level_core.rs` for the index header and the texture table,
which is the join between tfrags and texel data.
A tfrag's **ad-gifs** are five A+D quadwords (0x50 bytes) 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 the two wrap modes. The texture table is a count/offset pair at
index offset 0x30, entries of 16 bytes.
The table checks out on its own terms: on level 0 it is 93 entries at 0x2900,
and 0x2900 + 93 x 16 lands exactly on the next range at 0x38.
Three details worth writing down:
* **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, which is -8.56..-5.56
mip levels. Reading the field as unsigned gives 1911 instead of -137.
* **`data_hi` of TEX1_1 is MMIN, and 4 is `LinearMipmapNearest`**, not
`LinearMipmapLinear` as the bit layout alone suggests.
* **RGBA and the light/normal records are not in the VIF lists.** They sit at
fixed offsets in the patch's data block: `rgba_size * 4` four-byte colours at
`rgba_ofs`, and `vert_count` eight-byte records at `light_ofs + 0x10`. The
origin also lives there — if no STROW row supplied one, the quadword at
`light_ofs` is the origin.
### Verification
Every ad-gif must index a texture that exists, or the geometry and the texture
data have been misread independently of each other. Across all 27 levels:
**47,950 ad-gifs over 18,102 patches, all indices in range**, referencing
1429 of the 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
The ad-gif texture bindings (which texture each primitive samples), the RGBA
per-vertex colours and light records, and the LOD switch and morph rules in
`TfragProc`. Positions and topology are drawable but not yet textured.
The LOD switch and morph rules in `TfragProc`, the texel data itself, and the
GS register values the level load builds from the texture entry. A tfrag can
now be turned into positioned, UV-mapped, textured triangles; what is missing
is choosing which LOD to draw and decoding pixels.
+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