Verify the tfrag lighting magnitudes against the running game

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.
This commit is contained in:
2026-10-11 19:51:25 +02:00
parent 2c7555fe16
commit 69a8398f6e
15 changed files with 2144 additions and 25 deletions
+9 -3
View File
@@ -714,9 +714,15 @@ the fidelity rule.
resolving texture addresses on the host.
- **The guard-band clip test** that sends patches crossing the guard band to
the clipping program.
- **Lighting magnitudes.** The pass runs and the output is plausible; whether
it matches the game needs comparison against reference frames, which needs
the PCSX2 loop that is still to come. Nothing in this file establishes it.
- ~~**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
and fixed, both of which produced output that looked plausible:
`vertex_normal_bits` negated x and y but not z (so `max(dA, dA*wA)` took the
wrong branch and every vertex lit near its base colour -- 95.88% agreement
instead of 100%), and a record whose `select` names a set the bank lacks
drew transparent black where the game writes the record's base colour (3,130
of level 0's 40,600 vertices, `select 0x000f`).
- **tie, moby and shrub geometry.** The class tables parse and the container
they point into is identified (below), but none of the three is decoded yet.
Moby is the largest of the three and is skeletal, with an animation evaluator