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:
@@ -151,19 +151,34 @@ const MAX_LIGHTS: usize = 64;
|
||||
impl LightBank {
|
||||
/// Parse the bank from a level's `gameplay` extent.
|
||||
///
|
||||
/// The header at +0x04 is an offset to a count followed by the sets; a
|
||||
/// negative count means the level has no lights.
|
||||
/// The extent opens with a table of section offsets in bytes; the word at
|
||||
/// **+0x04** is the directional-light section's offset. That section is an
|
||||
/// `i32` set count, twelve bytes of pad, then the sets. This was read the
|
||||
/// other way round — offset at +0x00, count at +0x04 — until the PCSX2
|
||||
/// comparison showed the pass running on denormal garbage and reporting
|
||||
/// every vertex lit; see `docs/plan/pcsx2-verification.md`.
|
||||
pub fn parse(gameplay: &[u8]) -> Result<LightBank> {
|
||||
let mut b = Buf::new(gameplay);
|
||||
b.skip(4, "gameplay extent header")?;
|
||||
let ofs = b.u32("light bank offset")? as usize;
|
||||
let count = b.i32("directional light count")?;
|
||||
if ofs + 0x10 > gameplay.len() {
|
||||
return Err(Error {
|
||||
offset: ofs,
|
||||
context: "light bank offset runs past the gameplay extent",
|
||||
kind: ErrorKind::OutOfBounds {
|
||||
needed: 0x10,
|
||||
available: gameplay.len().saturating_sub(ofs),
|
||||
},
|
||||
});
|
||||
}
|
||||
let count = i32::from_le_bytes(gameplay[ofs..ofs + 4].try_into().unwrap());
|
||||
if count < 0 {
|
||||
return Ok(LightBank::default());
|
||||
}
|
||||
let n = (count as usize).min(MAX_LIGHTS);
|
||||
let mut sets = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
sets.push(read_set(gameplay, ofs + 0x10 + sets.len() * 0x40)?);
|
||||
for i in 0..n {
|
||||
sets.push(read_set(gameplay, ofs + 0x10 + i * 0x40)?);
|
||||
}
|
||||
Ok(LightBank {
|
||||
sets,
|
||||
@@ -264,8 +279,32 @@ mod tests {
|
||||
#[test]
|
||||
fn a_level_with_no_lights_parses_as_empty() {
|
||||
let mut g = vec![0u8; 32];
|
||||
g[0x04..0x08].copy_from_slice(&(-1i32).to_le_bytes());
|
||||
g[0x04..0x08].copy_from_slice(&8u32.to_le_bytes());
|
||||
g[0x08..0x0c].copy_from_slice(&(-1i32).to_le_bytes());
|
||||
let b = LightBank::parse(&g).expect("should parse");
|
||||
assert!(b.sets.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_bank_offset_is_at_plus_four_not_zero() {
|
||||
// A synthetic extent laid out the way the game writes it: an offset at
|
||||
// +0x04, a count at that offset, then the sets at +0x10.
|
||||
let mut g = vec![0u8; 0x100];
|
||||
let at = 0x40usize;
|
||||
g[0x04..0x08].copy_from_slice(&(at as u32).to_le_bytes());
|
||||
g[at..at + 4].copy_from_slice(&1i32.to_le_bytes());
|
||||
let set = |g: &mut [u8], o: usize, v: [f32; 4]| {
|
||||
for (i, f) in v.iter().enumerate() {
|
||||
g[o + i * 4..o + i * 4 + 4].copy_from_slice(&f.to_bits().to_le_bytes());
|
||||
}
|
||||
};
|
||||
set(&mut g, at + 0x10, [1.0, 2.0, 3.0, 4.0]);
|
||||
set(&mut g, at + 0x20, [0.0, 0.0, -1.0, 0.0]);
|
||||
let b = LightBank::parse(&g).expect("should parse");
|
||||
assert_eq!(b.sets.len(), 1);
|
||||
assert_eq!(b.sets[0].color_a, [1.0, 2.0, 3.0, 4.0]);
|
||||
assert_eq!(b.sets[0].dir_a, [0.0, 0.0, -1.0, 0.0]);
|
||||
// The old reading — offset at +0, count at +4 — must not be what this
|
||||
// does: at +0 the extent is 0, so it would find an empty bank.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user