Correct the tfrag pointer-field claim, and show morphing regenerates VU packets

Two corrections to the previous commit, both from continuing the RAM comparison.

1. The previous commit said the RAM comparison "names two fields the disc-side
   work had left as data and an unnamed word". It does not. +0x10 is
   TfragHeader::data and +0x38 is TfragHeader::cube_ofs, both already named in the
   struct. What RAM adds is that both are POINTERS -- relocated by adding the
   block's RAM base -- rather than plain offsets. That is still worth knowing, but
   it is not a naming.

2. The previous commit attributed the 8.6% of rewritten bytes to "morphed
   positions with topology and UVs left verbatim". That is wrong, and patch 1 shows
   why. Comparing 4 KiB, disc 0xdf60 against RAM 0x855f60:

     bytes differing      354 of 4,096 = 8.6%
     contiguous runs      120
     longest run          8 bytes
     extent               +0xc1 through the end of the window

Patch 1 has 83 positions and 83 vertex-info entries, of which 13 morph. If
morphing rewrote position fields alone, the differing bytes would be a 13/83 slice
of a 498-byte position array -- a couple of dozen bytes in one contiguous region.
Instead 354 bytes differ, spread over 120 short runs across the whole window.

So the loader is not patching positions in place: it REGENERATES the VU packet
stream for the current morph state. That is what morphing is on this hardware --
VU1 GIF packets, not a CPU-side vertex array.

Consequence for verification, and the reason this is worth committing rather than
just noting: the plan of "map RAM vertices to disc indices and solve for the weight"
does not work against this memory image. The positions are inside regenerated
packets, so there is no stable per-vertex correspondence to read a weight out of.
Verifying the morph weight against the original needs the CAMERA DISTANCE at the
savestate, so the weight can be computed from the derivation and compared with the
rendered result, rather than read back out of RAM.

Population figures gathered on the way: 833 of 833 patches parse, carrying 43,613
vertex-info entries; 499 of 833 patches have at least one morphing entry, and
patch 0 -- the first record -- is base-only with none.

Also fixed a measurement error mid-run: I compared patch 1 at disc 0xdf60 against
RAM 0x855f00, off by 0x60, and got "every byte differs". The corrected base is
0x855f60.
This commit is contained in:
2026-10-13 05:08:08 +02:00
parent 93591a79de
commit aff977e8c5
2 changed files with 53 additions and 15 deletions
+36 -5
View File
@@ -844,7 +844,11 @@ block's RAM base to each. Every other byte of all 833 records is verbatim. The
remaining six fields differ by 0xF80000 and 0x1F00000 rather than 0x848000 --
three records pointing into a different buffer, recorded rather than averaged.
This names two fields the disc-side work had left as `data` and an unnamed word.
**Correction:** both fields were already named. `+0x10` is
`TfragHeader::data` -- the byte offset of the tfrag's data block, relative to the
table -- and `+0x38` is `TfragHeader::cube_ofs`. The RAM comparison does not name
them; what it adds is that both are **pointers**, relocated by adding the block's
RAM base, rather than plain offsets.
**Result 2 -- behind the pointers, about 8.6% of bytes are rewritten.** Following
record 0's `+0x10` to disc `0xd040` / RAM `0x855040` and comparing 16 KiB:
@@ -858,7 +862,34 @@ command lists stay verbatim while a fraction of the position bytes move. 8.6% is
about half of a 16-byte vertex's 6 position bytes, i.e. consistent with roughly
half the vertices in this patch being mid-morph at the saved camera distance.
**Not established:** that the morphed bytes match `resolve_vertex`'s formula. That
needs mapping RAM vertices to disc vertex indices and solving for the weight, which
is the next step -- but the framing is now known well enough to attempt it, and
the two pointer fields are confirmed rather than inferred.
### How much of a patch actually changes, and where
Patch 1 (13 morphing entries of 83) compared over 4 KiB, disc `0xdf60` against RAM
`0x855f60`:
bytes differing 354 of 4,096 = 8.6%
contiguous runs 120
longest run 8 bytes
extent +0xc1 through the end of the window
83 positions at 6 bytes each is 498 bytes, and only 13 of 83 vertices morph, so
**if morphing rewrote position fields alone** the differing bytes would be a
13/83 slice of a 498-byte array -- a couple of dozen bytes in one contiguous
region. Instead 354 bytes differ, spread over 120 short runs across the whole
window.
So the loader is not patching positions in place. It **regenerates the VU packet
stream** for the current morph state, which on this hardware is what morph
actually is -- VU1 GIF packets, not a CPU-side vertex array.
**Consequence for verification.** The plan of "map RAM vertices to disc indices and
solve for the weight" does not work against this memory image: the positions are
inside regenerated packets, so there is no stable per-vertex correspondence to
read a weight out of. Verifying the morph weight against the original needs the
**camera distance at the savestate** so the weight can be computed from the
derivation and compared with the rendered result, rather than read back out of
RAM.
Along the way, two population figures worth having: **833 of 833 patches parse**,
carrying **43,613** vertex-info entries, and **499 of 833 patches have at least
one morphing entry** -- patch 0, the first record, is base-only with none.
+17 -10
View File
@@ -249,16 +249,23 @@ Phase 1 hands over the extents; this phase works out what each one contains.
`+0x10` and `+0x38`, verified against PCSX2 across all **833** records: the
loader adds the block's RAM base (`0x848000`) to each, every other byte of
every record is verbatim, and 1,665 fields differ where 1,666 were possible.
This names two fields previously left as `data` and an unnamed word.
- [~] Behind those pointers, **8.6% of bytes are rewritten** (14,977 of 16,384
identical), spread across many field positions rather than concentrated --
consistent with morphed positions with topology and UVs left verbatim.
- [ ] What tfrag is **not** yet verified is that the morphed bytes match
`resolve_vertex`'s formula. The LOD switch distances, morph weight and
collapse edge are derived from the data (tiers classified from the four V4_8
parent arrays, weight running zero to one across each band); the framing is
now known well enough to map RAM vertices to disc indices and solve for the
weight, which is the next step.
They are the existing `TfragHeader::data` and `::cube_ofs` -- what RAM adds
is that both are **pointers**, not plain offsets. All **833** patches parse,
carrying **43,613** vertex-info entries; **499 of 833** have at least one
morphing entry.
- [~] Behind those pointers, **8.6% of bytes are rewritten** (354 of 4,096 on
patch 1), spread over 120 short runs across the whole window rather than
concentrated in a position array. Morphing on this hardware is VU1 GIF
packets, and the loader **regenerates the packet stream** rather than
patching position fields in place.
- [ ] 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.
- [x] `tie` — **fully chained.** `resolve_tie_class` walks both indirections: the
class table is in `core_index.bin`, and the entry's `offset_in_asset_wad`
addresses `core_data.bin` (not `core_data.wad`). All **1,864** tie classes