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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user