The header pointers found in the previous commit are live, so they can be
dereferenced. Two records 0x30 apart in the level 0 table at 0x22a6b8 were
followed.
Range measured: savestate slot 1, reading 0x00936a70 (from the record at
0x22a6b8) and 0x009376f0 (from the record at 0x22a6e8), 64 bytes each.
The two payloads have nothing in common.
0x00936a70 is fixed-point vertex data:
8d 8c 80 80 89 87 7c 80 8d 8c 80 80 89 87 7c 80
4e 4e 4c 80 4e 4e 4c 80 4e 4e 4c 80 4e 4e 4c 80 repeating
Every high byte is 0x80 or 0x7c and the pattern repeats on an 8-byte boundary.
As s16 these are large negatives in a narrow band around -32500, consistent
with Q16.15 fixed point rather than f32, and with a stride of 4 s16 per vertex.
0x009376f0 is a tagged sub-list, not vertex data:
00 00 00 30 cd 00 00 00 cd 00 00 00 cd 00 00 00
cd 00 00 00 01 00 00 05 22 c1 01 6e 00 00 00 00
Structurally different: small integers, a repeating 0xcd, 0x30 tags in the high
byte. Nothing here is position data.
This supports the type reading of the header: the table is a tagged collection
where the tag selects the payload kind, not a uniform array. It also rules out
the alternative that the 16-byte tiling was one uniform record format with
varying fields -- two adjacent records with unrelated payloads is not that.
Still open: the mapping from tag value to payload kind. The 0x30 appearing in
record bytes 8-11 in RAM also heads the second payload, which is suggestive
but is one coincidence so far and is deliberately not written up as a link.
Answering the "what is the tag byte" question needed an anchor. Searching live
RAM from scratch was the wrong way to get one: 04 04 00 01 matches unrelated
data at 0x1ac988 and there is no way to tell a real hit from a coincidental
one. The savestates are the anchor -- slot 1 is level 0, and searching one
gives candidates that can be checked rather than guessed at.
Range measured: savestate slot 1,
~/.config/PCSX2/sstates/SCUS-97268 (38996035).01.p2s, records at 0x22a6b8
onward, read with gc-trace read --state.
Result: 40+ records beginning 04 04 00 01 00 00 00 00 at 0x30 spacing -- the
packet framing identified on disc -- with the second half of each header
rewritten:
disc: 04 04 00 01 | 00 00 00 00 | 00 00 00 05 | 00 80 0e 6c
RAM: 04 04 00 01 | 00 00 00 00 | 13 00 00 30 | 70 6a 93 00
Bytes 0-7 unchanged; bytes 8-15 are not. On disc bytes 12-13 are the constant
00 80 and byte 15 the constant 6c; in RAM bytes 12-15 hold a pointer --
0x00936a70, 0x009376f0, 0x00937850, 0x00939c50 in consecutive records, all
inside EE RAM and ascending across the table as a walk of consecutive objects
would be. Byte 11 becomes 0x30 in every record seen, against a constant 0x05 on
disc, and bytes 8-9 hold a varying small value (0x13, 0x16, 0x24).
So the packet table at +0x40 is what the loader walks: span-relative on disc,
absolute in RAM. This was inferred from one class before; these 40+ records
show it directly.
Consequence for the tag bytes: the vocabularies at record bytes 7 and 15 are
real, but byte 15 sits in a field the loader overwrites with a pointer. Its
disc values are read as data by something rather than stored as a tag.
Not settled: whether byte 7's vocabulary is also rewritten, and whether 0x6d
marks payload the loader expands. That needs these records compared one by one
against their disc spans, which is cheap now that the anchor exists.
PCSX2 was set up and working but existed only in this machine's filesystem
and in people's heads, so every comparison looked like a one-off observation
rather than something re-runnable. The setup section in
docs/plan/pcsx2-verification.md now records where the emulator, the BIOS, the
disc, the savestates and the PINE socket are, how a state is captured, and
which command the disc integration tests need -- and AGENTS.md and README.md
point at it from their orientation sections. One operational detail worth
spelling out: the emulator's directory is lowercase (pcsx2), which is not
what people type.
Savestate slot 2 is the second planet, so the comparison runs on a level with
a different geometry extent, light bank and runtime-set population:
level1: 401/401 headers, 401/401 pointers, bank byte-identical
vertices compared: 26470 (105880 colour bytes) -- equal: 100.00%
The live emulator agrees with the savestate byte for byte. Level 1 is a
different planet, so the two fixes in the previous commit are not level-0
coincidences: the normal's z sign and the unlit-record base colour both hold
across 268,280 colour bytes on two levels.
The harness that compares the port against the game's own memory existed but
had never been compiled: tools/trace did not build (27 errors, all in the
argument plumbing), and the tfrag_light.rs light-bank correction it motivated
was unverified. This finishes it and runs it, and the run finds two real bugs.
Result, on level 0 against three inputs -- the live emulator over PINE and two
PCSX2 savestates from the same level, agreeing byte for byte:
headers matching the disc on invariant bytes: 833/833
data pointers relocated as table + disc offset: 833/833
light bank in RAM at 0x50ab20, byte-identical to the disc's
vertices compared: 40600 (162400 colour bytes)
equal: 162400 (100.00%)
The locator finds the level's tfrags block in RAM by tfrag 0's bounding
sphere and accepts a candidate only when every header matches the disc on the
bytes the game leaves alone and every data pointer is the relocated form, so
"the block was found" is itself a verification rather than an assumption. The
bank is searched in RAM by its exact section bytes, which is what settles its
framing: offset at +0x04, an i32 set count, twelve bytes of pad, then the sets.
The previous reading (offset at +0x00, count at +0x04) was wrong.
Two bugs the comparison found, both of which produced plausible output:
1. vertex_normal_bits negated x and y and left z positive, while
NormalTable::normal negates all three. dA came out -0.707 where the game
had +0.707, so max(dA, dA*wA) took the wrong branch and every vertex lit
near its base colour: dark, but not wrong-dark. 95.88% agreement with the
bug, 100.00% without.
2. A record whose select names a set the bank lacks drew transparent black.
The game writes the record's base colour; 3,130 of level 0's 40,600
vertices do this (select 0x000f, set 15 -- a runtime slot the level's
directional bank does not have). The vertex is unlit, not invisible.
Both are now regression tests in the engine. The comparison prints the pass's
per-vertex terms (light_vertex_terms) for the first mismatches, because a
colour mismatch alone cannot be attributed and the comparison is worthless
without attribution -- that is how the second bug was identified from a
three-line read rather than guessed at.
Docs: docs/plan/pcsx2-verification.md records the harness, the result, the
two bugs and what to compare next; roadmap and level-assets.md stop carrying
lighting magnitudes as an open item. Findings reach tests only as numbers --
no test reads a savestate, a dump or a live emulator.