From aff977e8c57d57d3033322e3188642a2b6cd50df Mon Sep 17 00:00:00 2001 From: ReaperOfVeriod Date: Tue, 13 Oct 2026 05:08:08 +0200 Subject: [PATCH] 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. --- docs/plan/pcsx2-verification.md | 41 +++++++++++++++++++++++++++++---- docs/roadmap.md | 27 ++++++++++++++-------- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/docs/plan/pcsx2-verification.md b/docs/plan/pcsx2-verification.md index ad1238c..0ae7454 100644 --- a/docs/plan/pcsx2-verification.md +++ b/docs/plan/pcsx2-verification.md @@ -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. diff --git a/docs/roadmap.md b/docs/roadmap.md index 3175d75..fcb8fe1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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