The tfrag morph is not in the data block: positions are verbatim in RAM
The morph was recorded as unverifiable because "reading a weight back out of
RAM does not work -- the positions live inside regenerated VU packets". That
is right about the correspondence and wrong about the cause. The cause is
measurable, and it comes back clean:
patches 833
positions 40,600 disc == ram
positions identical 40,600
block bytes differing 166,465
inside a position unpack 0
inside another unpack 49,484
in a VIF command word 116,981
Range: level 0, all 833 tfrag patches, live PINE RAM against the disc. Two
independent measurements agree. Walking the VIF lists on both copies and
comparing parsed positions gives 40,600 identical and zero changed; a raw
per-byte diff of each patch's own extent classifies every differing byte, and
none of them is in a position unpack. The position ranges total 490,224 bytes.
So the blend is not precomputed into the geometry. The rewrites are packet
framing -- 70% command words -- plus other unpack payloads, which is the loader
rebuilding the VIF packet for the tier it selected while leaving the vertices
alone. The morph happens at draw time in the VU stream.
The standing note becomes a sharper one: there was never a correspondence
problem. The values were identical, so there was nothing to read.
gc-trace gains verify-morph, which does both measurements. TfragGeometry
gains position_ranges and other_ranges so the walk needed to classify a diff
is already paid for, and the ranges come from the same walk the rest of the
parse uses rather than a second one that could drift.
Per-patch extents are the gap to the next patch's data pointer. That is exact
and needs no guess about which header field marks a patch's end; the first
attempt at this compared each patch against the whole rest of the region and
reported 65 million differing bytes, which is what an unbounded comparison
looks like.
tfrag_positions_are_verbatim_and_the_rewrites_are_elsewhere is gated on
GC_SSTATE and asserts the four bounds, plus two guards: the rewrites must be
non-trivial (over 100,000 bytes) and mostly command words, so a RAM image that
was a verbatim copy of the disc -- which would make the zero look good for the
wrong reason -- fails instead.
Suite with the disc and GC_SSTATE: 68 disc tests, 156 gc-formats, 22
gc-trace, 23 gc-engine, 19 gc-game. Clippy clean.
This commit is contained in:
@@ -2570,6 +2570,50 @@ 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)
|
||||
|
||||
**Range measured:** level 0, all 833 tfrag patches, live PINE RAM against the
|
||||
disc.
|
||||
|
||||
The morph was recorded as unverifiable because "reading a weight back out of
|
||||
RAM does not work -- the positions live inside regenerated VU packets, so there
|
||||
is no stable per-vertex correspondence". That reading is right about the
|
||||
correspondence and wrong about the cause, and the cause is now measurable.
|
||||
|
||||
**The game rewrites bytes inside the tfrag blocks, and none of them are in a
|
||||
position.**
|
||||
|
||||
patches 833
|
||||
positions 40,600 (disc == ram)
|
||||
positions identical 40,600
|
||||
block bytes differing 166,465
|
||||
inside a position unpack 0
|
||||
inside another unpack 49,484
|
||||
in a VIF command word 116,981
|
||||
|
||||
The position ranges total 490,224 bytes and not one of them differs. The two
|
||||
measurements agree independently: walking the VIF lists and comparing parsed
|
||||
positions gives 40,600 identical and zero changed, and a raw per-byte diff of
|
||||
each patch's own extent classifies every differing byte.
|
||||
|
||||
So the blend is not precomputed into the geometry. It happens at draw time, in
|
||||
the VU stream -- which is the standard PS2 arrangement and is why no weight was
|
||||
ever readable from RAM: there was nothing there to read. The rewrites are
|
||||
mostly packet framing (70% command words) plus some other unpack payloads
|
||||
(30%), which is consistent with the loader rebuilding the VIF packet for the
|
||||
tier it selected while leaving the vertex data alone.
|
||||
|
||||
**Extents.** Each patch's extent is the gap to the next patch's `data` pointer,
|
||||
which is exact and needs no guess about which header field marks a patch's end.
|
||||
The block is located in RAM by the first table record matched under the
|
||||
invariant mask, the same way `gc-trace` locates it.
|
||||
|
||||
Asserted by `tfrag_positions_are_verbatim_and_the_rewrites_are_elsewhere`,
|
||||
gated on `GC_SSTATE`. The test also requires the rewrites to be non-trivial
|
||||
(more than 100,000 bytes) and to be mostly command words, so a RAM image that
|
||||
happened to be a verbatim copy of the disc -- which would make the zero look
|
||||
good for the wrong reason -- fails instead.
|
||||
|
||||
## Most of the chunk container is level-independent
|
||||
|
||||
Comparing every chunk record against the same record index in the other 26
|
||||
@@ -3483,7 +3527,7 @@ the time.
|
||||
the shuffle margin, so a claim that the period is not 64 fails rather than
|
||||
sailing through.
|
||||
|
||||
## Attempting the moby_anim unblock: the class header, and where it stalls## Attempting the moby_anim unblock: the class header, and where it stalls
|
||||
## Attempting the moby_anim unblock: the class header, and where it stalls
|
||||
|
||||
`MobyInstance` names `animSeq`, `animLayers`, `jointCnt` and `jointCache` as
|
||||
runtime pointers. Those pointers are useless on the disc -- they are RAM
|
||||
|
||||
@@ -17,6 +17,7 @@ gc-trace find <src> --bytes HEX # content search (the locators use it
|
||||
gc-trace read <src> --addr HEX --len N
|
||||
gc-trace info <src> # savestate metadata
|
||||
gc-trace compare-tfrag-light <unpacked-dir> [--level N] <src>
|
||||
gc-trace verify-morph <unpacked-dir> [--level N] <src>
|
||||
```
|
||||
|
||||
`<src>` is `--pine [slot]`, `--state FILE` / `--state latest`, or `--ee FILE`.
|
||||
@@ -37,6 +38,26 @@ The comparison:
|
||||
few mismatching vertices' *terms* (`gc_engine::tfrag_light::
|
||||
light_vertex_terms`) printed so a mismatch can be attributed.
|
||||
|
||||
## verify-morph: the morph is not in the tfrag data block (2026-10-14)
|
||||
|
||||
`verify-morph` answers a different question than `compare-tfrag-light`. It
|
||||
walks every patch's packet lists **twice** -- once over the disc copy, once over
|
||||
the RAM copy -- and compares the parsed positions, then diffs the raw bytes of
|
||||
each patch and classifies every differing byte against the walk's own unpack
|
||||
ranges.
|
||||
|
||||
The result on level 0, live over PINE: 40,600 positions, all identical. The
|
||||
game does rewrite the blocks -- 166,465 bytes -- and not one lands inside a
|
||||
position unpack. So the LOD blend is not written back into the geometry.
|
||||
|
||||
That sharpens the standing note in the roadmap. "Reading a weight back out of
|
||||
RAM does not work" was read as the positions being regenerated. They are not
|
||||
regenerated in *value*: the loader rebuilds the packet framing (116,981
|
||||
command-word bytes, 70% of the differences) and some other unpack payloads
|
||||
(49,484), and leaves the vertices alone. The blend happens at draw time in the
|
||||
VU stream. Nothing was there to read, which is a different finding from there
|
||||
being a correspondence problem.
|
||||
|
||||
## Result: levels 0 and 1, verified 2026-10-11
|
||||
|
||||
Level 0 (Aranos), three inputs -- the live emulator over PINE and two PCSX2
|
||||
|
||||
+17
-7
@@ -267,14 +267,24 @@ Phase 1 hands over the extents; this phase works out what each one contains.
|
||||
short structured runs with no common `u32` addend. The loader regenerates
|
||||
each patch's **VU packet stream for the LOD tier it selected**, which
|
||||
happens to every patch regardless of morphing.
|
||||
- [x] **The tfrag morph is not a rewrite of the position arrays.** Across all
|
||||
**833** patches of level 0, live PINE RAM against the disc: **40,600**
|
||||
positions, **40,600 identical, zero changed**. The game does rewrite the
|
||||
blocks -- **166,465** bytes differ -- and **none** of them fall inside a
|
||||
position unpack: 116,981 land in VIF command words and 49,484 in other
|
||||
unpack payloads. The blend is therefore not precomputed into the geometry;
|
||||
it happens at draw time in the VU stream, which is why reading a weight back
|
||||
out of RAM never worked -- there was nothing there to read. This sharpens
|
||||
the earlier "positions live inside regenerated VU packets, so there is no
|
||||
stable per-vertex correspondence": the correspondence was never missing, the
|
||||
values are just identical. `gc-trace verify-morph` and the
|
||||
`GC_SSTATE`-gated test both carry the numbers.
|
||||
- [ ] What tfrag is **not** yet verified is that the morph matches
|
||||
`resolve_vertex`'s formula. Reading a weight back out of RAM does **not**
|
||||
work -- the positions live inside regenerated VU packets, so there is no
|
||||
stable per-vertex correspondence. The LOD switch distances, morph weight and
|
||||
collapse edge remain derived from the data (tiers classified from the four
|
||||
V4_8 parent arrays, weight running zero to one across each band); verifying
|
||||
them needs the **camera distance at the savestate** so the weight can be
|
||||
computed and compared against the rendered result.
|
||||
`resolve_vertex`'s formula. The blend target is still untested against the
|
||||
game itself, and now the search space is bounded: it is in the VU stream,
|
||||
not in the tfrag data block. Verifying it needs the **camera distance at the
|
||||
savestate** so the weight can be computed, or a read of the VU packet stream
|
||||
the game actually submits.
|
||||
- [x] The tie class header's **`+0x0c` word is the three LOD record
|
||||
counts** — LOD 0 at `+0x0c`, LOD 1 at `+0x0d`, LOD 2 at `+0x0e` — verified
|
||||
on **1,864 of 1,864** classes by reading the counts independently from
|
||||
|
||||
Reference in New Issue
Block a user