The GS TEX0 register set, and why TBP0 is region-relative

Picking up "the GS register set the level load builds from the texture entry"
from level-assets.md, for a renderer that emulates the GS rather than resolving
texture addresses on the host.

gc_formats::gs gains tex0_of (TBP0, TBW, TW, TH, PSM), texture_slot_bytes and
clut_base. Three measurements back them, over all 27 levels and 1,595 textures.

Every dimension is a power of two: 1,595 textures, zero exceptions, in four
classes -- 128x128 (1,249), 64x64 (303), 256x256 (22), 32x32 (21). The GS
addresses textures by exponent, so TW and TH are log2 of the dimensions, and a
texture that was not a power of two could not be addressed at all. That the
whole population is one is the measurand for the decode.

TBP0 is region-relative. 19 of 27 levels have a textures_base only 64-byte
aligned -- mod 256 reads 0, 64, 128 or 192 -- while every entry's own
data_offset is 256-aligned on all 1,595. A base pointer computed from core_data
would therefore not be a whole number of 256-byte blocks on those levels. The
first version of tex0_of took textures_base + data_offset and rejected 1,168 of
1,595 textures for exactly this reason, which is how the finding surfaced.

The slot is level 0 plus one mip level: the stride between consecutive entries
is w*h + (w/2)*(h/2) on 1,560 of the 1,564 ascending gaps -- 32x32 gives 1,280,
64x64 gives 5,120, 128x128 gives 20,480, 256x256 gives 81,920. One byte per
texel. `ty` reads 3 on all 1,595, so it is not the stored level count.

Four gaps break the rule and four more run backwards. Both are reported as
bounded exceptions rather than folded into an average: the four ascending
outliers are 712,960 to 1,064,960 bytes, two orders of magnitude past any slot
the game allocates, so they are boundaries the slot rule does not describe
rather than slots that came out different. The four reverse gaps mean the table
is not sorted by data_offset, which a consumer walking the region in table order
would trip over.

The CLUT address is the established palette * 0x100, restated as clut_base
because a GS emulator asks for it as TEXCLUT's CBP.

Still open after this: TEXCLUT's own fields, MIPTBP1/2 wiring, and how the
ad-gifs' A+D values relate to this base.

Suite with the disc: 70 disc tests, 160 gc-formats, 22 gc-trace, 23
gc-engine, 19 gc-game. Clippy clean.
This commit is contained in:
2026-10-14 13:36:16 +02:00
parent 66e83f1665
commit e3e0a30030
4 changed files with 331 additions and 4 deletions
+40 -1
View File
@@ -2570,7 +2570,46 @@ correct stride, which is not yet known. The LOD1/LOD2-sharing test
(96.1% same class against 0.4% different) remains the strongest evidence that
the first slots are positional, and it is insensitive to stride.
## The tfrag clip cube is at `+0x38`, and `+0x2e` is the mini-sphere array
## The GS texture register set, and why TBP0 is region-relative
**Range measured:** all 27 levels, **1,595 level textures**.
`gc_formats::gs` gains the TEX0 register a GS emulator needs, decoded from a
texture entry: `tbp0`, `tbw`, `tw`, `th`, `psm`, plus `texture_slot_bytes` and
`clut_base`. Three measurements back the decode.
**Every dimension is a power of two.** 1,595 textures, **zero** exceptions, in
four classes: 128x128 (1,249), 64x64 (303), 256x256 (22), 32x32 (21). The GS
addresses textures by exponent, so TW and TH are `log2` of the dimensions and a
texture that was not a power of two could not be addressed at all. That the
whole population is one is the measurand for the decode.
**TBP0 is region-relative, not data-relative.** 19 of the 27 levels have a
`textures_base` that is only 64-byte aligned -- mod 256 reads 0, 64, 128 or 192
-- while every entry's own `data_offset` is 256-aligned on all 1,595. So a base
pointer computed from `core_data` would not be a whole number of 256-byte blocks
on those levels. The loader uploads the texture region to a GS-aligned base and
the pointer counts from there.
**The slot is level 0 plus one mip level.** The stride between consecutive
entries is `w*h + (w/2)*(h/2)` on 1,560 of the 1,564 ascending gaps: 32x32 gives
1,280, 64x64 gives 5,120, 128x128 gives 20,480, 256x256 gives 81,920. The
game stores the base image and one mip, at one byte per texel. `ty` reads **3 on
all 1,595**, so it is not the stored level count -- naming what it is instead of
calling it a mip count is the point.
Four gaps break the rule, and four more run backwards. They are reported as a
bounded exception rather than folded into an average: the four ascending
outliers are 712,960 to 1,064,960 bytes, two orders of magnitude past any slot
the game allocates, so they are boundaries the slot rule does not describe
rather than slots that came out different. The four reverse gaps mean the table
is not sorted by `data_offset`, which is itself worth knowing -- a consumer that
walks the region by table order would visit it out of sequence.
The CLUT address is the established `palette * 0x100`, restated as
`clut_base` because a GS emulator asks for it as TEXCLUT's CBP.
## The tfrag clip cube is at `+0x38`, and `+0x2e` is the mini-sphere array## The tfrag clip cube is at `+0x38`, and `+0x2e` is the mini-sphere array
**Range measured:** level 0, all 833 tfrag headers.
+9 -3
View File
@@ -709,9 +709,15 @@ the fidelity rule.
- **Point lights.** The directional pass runs; `point_light_vertex` and its
`r^2 - |p - L|^2` cull test are not written, and they are the one part of
the lighting that *is* distance-dependent.
- **The GS register set** the level load builds from the texture entry
(TBP0/TBP1 and so on), for a renderer that emulates the GS rather than
resolving texture addresses on the host.
- ~~**The GS register set**~~ **Partly settled 2026-10-14.**
`gc_formats::gs::tex0_of` decodes TEX0 (TBP0, TBW, TW, TH, PSM) from a texture
entry, verified over all 1,595 textures on all 27 levels: every dimension is a
power of two, so the exponent fields are exact, and TBP0 is **region-relative**
because 19 of 27 levels have a `textures_base` only 64-byte aligned.
`texture_slot_bytes` is `w*h + (w/2)*(h/2)` -- level 0 plus one mip -- on
1,560 of 1,564 ascending gaps, with four bounded exceptions. Still open: the
**TEXCLUT** register (the CLUT's CBP/CSA/CSM), MIPTBP1/2 wiring, and the
A+D values the ad-gifs carry relative to this base.
- **The guard-band clip test** that sends patches crossing the guard band to
the clipping program. The geometry side is now in place -- the 8-corner clip
cube at header `+0x38` is parsed and tested (`core-data-regions.md`, the