The tfrag clip cube is at +0x38, and +0x2e is the mini-sphere array

TfragHeader carried a field named `cube_ofs` at +0x2e. R&C1 documents that
slot as `msphere_ofs` and puts the 8-corner clip cube at +0x38. The disc is on
R&C1's side for both.

Reading +0x2e as an array of `msphere_count` (+0x2c) Vec4f entries gives 833 of
833 well-formed arrays: four finite floats per entry, every radius
non-negative. +0x2c takes 4, 3, 5, 6, 16 and others. Reading that same slot as
eight s16 clip corners gives data that is not corner-shaped, which is how the
error was visible at all.

The real cube at +0x38 is non-zero on 833 of 833 headers and its eight points
sit inside the patch's own block on every one.

So: `cube_ofs` at +0x2e is renamed to `msphere_ofs`, the count it pairs with is
parsed and the array is read into TfragGeometry as `mini_spheres`, and
`clip_cube_ofs` at +0x38 is new, read into `clip_corners`.

What the corners are, measured over all 833:

    corners, all with 8 distinct x values       833 of 833
    |centroid - bsphere centre| / r             median 0.147, max 0.595
    corner distance from centroid / r           median 1.000

The median is exactly the bounding radius, so the eight points lie on the
bounding sphere. They are not an axis-aligned box: every tfrag has eight
distinct x values, so if it is a box it is oriented. Whether they form a cube
needs the corner ordering, which is not recoverable from the data -- assuming
opposite pairs are (0,7)(1,6)(2,5)(3,4) matches body-diagonal == 2r on only 99
of 833, so either that pairing is wrong or the array is not a cube. Recorded as
unknown.

This was picked to unblock the guard-band clip test from the roadmap. The
algorithm is from the vendored R&C1 reference and is recorded with its
attribution: camera-space sphere, near/far truncation, side-plane cull, then
the 8-corner re-test against the frustum unscaled and against a volume with xy
x (0.25, 0.25) -- the 4x guard band -- with any corner outside the guard volume
sending the tfrag to the clipping path at LOD 0 with no morphing. draw_mode
already matches that table. What the test still needs is the camera matrix and
the frustum as a clip volume, both per-frame state, so the camera has to be
pinned at a savestate. The geometry side is now parsed and asserted, which
leaves only the camera side outstanding.

The s16 x 64 corner scale is R&C1's documented one and is not verified against
the game; the assertion covers that the parsed corners sit inside the patch's
own block and lie on the bounding sphere, not that the scale is right.

Suite with the disc: 69 disc tests, 156 gc-formats, 22 gc-trace, 23
gc-engine, 19 gc-game. Clippy clean.
This commit is contained in:
2026-10-14 13:09:54 +02:00
parent 3a1156d8e1
commit 66e83f1665
4 changed files with 291 additions and 6 deletions
+63 -1
View File
@@ -2570,7 +2570,69 @@ 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 morph is not a rewrite of the position arrays (2026-10-14)
## The tfrag clip cube is at `+0x38`, and `+0x2e` is the mini-sphere array
**Range measured:** level 0, all 833 tfrag headers.
The parser had a field named `cube_ofs` at header `+0x2e`. R&C1 documents that
slot as `msphere_ofs` and puts the 8-corner clip cube at `+0x38`, and the disc
agrees with R&C1 on both:
* reading `+0x2e` as an array of `msphere_count` (`+0x2c`) `Vec4f` entries
gives **833 of 833** well-formed arrays -- four finite floats per entry, every
radius non-negative. `+0x2c` takes 4, 3, 5, 6 and 16 among others;
* reading it as eight `s16` clip corners gave data that was not corner-shaped,
which is how the mistake was visible in the first place;
* the real cube, at `+0x38`, is **non-zero on 833 of 833** and its eight points
sit inside the patch's own block on all of them.
So `TfragHeader::cube_ofs` at `+0x2e` is now `msphere_ofs`, the count it pairs
with is parsed, and `clip_cube_ofs` at `+0x38` is new.
### What the cube's corners are
Eight points, each read as four `s16` at 8-byte stride and scaled by 64 -- the
scale is R&C1's documented one and is **not** verified against the game. What
is measured:
corners, all with 8 distinct x values 833 of 833
|centroid - bsphere centre| / r median 0.147, max 0.595
corner distance from centroid / r median 1.000
The median is exactly the radius, so the eight points lie on the bounding
sphere. They are **not** an axis-aligned box -- every tfrag has eight distinct x
values, so the box, if it is one, is oriented. Testing whether the eight points
form a cube needs the corner *ordering*, which is not recoverable from the data:
assuming opposite pairs are `(0,7) (1,6) (2,5) (3,4)` gives a body-diagonal
equals `2r` match on only 99 of 833, so either that assumption is wrong or the
array is not a cube. Recorded as unknown rather than guessed at.
### The guard-band clip test, and what it now needs
The algorithm is documented in the vendored R&C1 reference (`tfrag_rac1.md`
§3b.2), attributed to it rather than verified here:
1. `d = R * (bsphere.xyz - camera * 1024)` in camera space;
2. `far = int(d.z + r)`, `near = int(d.z - r)`, truncated;
3. cull when `near > 512000` or `far <= n`, or on the side planes
`tan*z - |x| +- r*sqrt(1 + tan^2)`;
4. "wholly inside" is both side margins non-negative, `far < f`, `near >= n`;
5. otherwise re-test the 8 corners from `clip_cube_ofs`, against the frustum
unscaled and against a volume with xy scaled by `(0.25, 0.25)` -- the 4x
guard band;
6. all corners outside one unscaled plane culls; any corner outside the guard
volume sends the tfrag to the clipping program at LOD 0 with no morphing;
7. otherwise `draw_mode` decides.
`draw_mode` itself already matches the reference's table in this repository
(`gc_formats::tfrag::TfragGeometry::draw_mode`). What step 1 needs and what we
still do not have is the **camera matrix**, and what step 5 needs is the view
frustum planes as a clip volume. Both are per-frame game state, so this needs
the camera at a savestate -- the same blocker the morph verification had before
the PINE session made it measurable. The difference now is that the geometry
side is parsed and tested, so only the camera side is outstanding.
## The tfrag morph is not a rewrite of the position arrays (2026-10-14)## The tfrag morph is not a rewrite of the position arrays (2026-10-14)
**Range measured:** level 0, all 833 tfrag patches, live PINE RAM against the
disc.
+6 -1
View File
@@ -713,7 +713,12 @@ the fidelity rule.
(TBP0/TBP1 and so on), for a renderer that emulates the GS rather than
resolving texture addresses on the host.
- **The guard-band clip test** that sends patches crossing the guard band to
the clipping program.
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
clip-cube section). What is outstanding is the **camera matrix** and the
view frustum as a clip volume, both per-frame game state, so the test itself
needs a savestate with the camera pinned. `draw_mode` is unchanged and already
matches the reference's mode table.
- ~~**Lighting magnitudes.**~~ **Settled 2026-10-11** by the PCSX2
comparison loop (`docs/plan/pcsx2-verification.md`): level 0's 162,400
colour bytes match the game's EE RAM exactly. Two bugs the comparison found