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:
@@ -0,0 +1,121 @@
|
||||
//! EE RAM: a flat 32 MiB window, plus the content search the locators use.
|
||||
//!
|
||||
//! The harness never hardcodes an address. Every structure the comparison
|
||||
//! needs is found by content, because the addresses differ per level and per
|
||||
//! run, and a pointer read from the wrong base is indistinguishable from a
|
||||
//! right read from the right one. `EeRam` is deliberately dumb: byte-range
|
||||
//! reads and a substring search.
|
||||
|
||||
/// The PlayStation 2's main RAM: 32 MiB at physical 0 (no extended RAM).
|
||||
pub const EE_SIZE: usize = 32 * 1024 * 1024;
|
||||
|
||||
/// A borrowed EE RAM image.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct EeRam<'a>(&'a [u8]);
|
||||
|
||||
impl<'a> EeRam<'a> {
|
||||
/// Wrap a RAM image. The length must be exactly 32 MiB, so a short dump is
|
||||
/// an error rather than a search that silently stops early.
|
||||
pub fn new(bytes: &'a [u8]) -> anyhow::Result<Self> {
|
||||
if bytes.len() != EE_SIZE {
|
||||
anyhow::bail!(
|
||||
"EE RAM image is {} bytes, expected exactly {EE_SIZE}",
|
||||
bytes.len()
|
||||
);
|
||||
}
|
||||
Ok(Self(bytes))
|
||||
}
|
||||
|
||||
pub fn bytes(&self) -> &'a [u8] {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Every offset at which `needle` occurs, scanning 32 MiB.
|
||||
///
|
||||
/// A naive first-byte scan keeps this correct for any needle, including
|
||||
/// one inside a buffer that also contains zeros — which is most of RAM.
|
||||
pub fn find_all(&self, needle: &[u8]) -> Vec<usize> {
|
||||
if needle.is_empty() || needle.len() > self.0.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let first = needle[0];
|
||||
let mut out = Vec::new();
|
||||
let mut i = 0usize;
|
||||
while i + needle.len() <= self.0.len() {
|
||||
if self.0[i] == first && &self.0[i..i + needle.len()] == needle {
|
||||
out.push(i);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The first offset at which `needle` occurs.
|
||||
pub fn find(&self, needle: &[u8]) -> Option<usize> {
|
||||
self.find_all(needle).into_iter().next()
|
||||
}
|
||||
|
||||
pub fn slice(&self, at: usize, len: usize) -> Option<&'a [u8]> {
|
||||
self.0.get(at..at.checked_add(len)?)
|
||||
}
|
||||
|
||||
/// A little-endian `i32` at `at`.
|
||||
pub fn i32(&self, at: usize) -> Option<i32> {
|
||||
self.slice(at, 4)
|
||||
.map(|b| i32::from_le_bytes(b.try_into().unwrap()))
|
||||
}
|
||||
|
||||
/// A little-endian `u32` at `at`.
|
||||
pub fn u32(&self, at: usize) -> Option<u32> {
|
||||
self.slice(at, 4)
|
||||
.map(|b| u32::from_le_bytes(b.try_into().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Fill with a non-zero byte: a zero-filled image makes any needle ending
|
||||
/// in `0x00` match by accident, which is not what this is testing.
|
||||
fn ram_with(needle: &[u8], at: usize) -> Vec<u8> {
|
||||
let mut v = vec![0xaau8; EE_SIZE];
|
||||
v[at..at + needle.len()].copy_from_slice(needle);
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_32mib_image_is_accepted() {
|
||||
EeRam::new(&[0u8; EE_SIZE]).expect("32 MiB is exactly right");
|
||||
assert!(EeRam::new(&[0u8; EE_SIZE - 1]).is_err());
|
||||
assert!(EeRam::new(&[]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_reports_every_occurrence() {
|
||||
let v = ram_with(&[0xde, 0xad, 0xbe, 0xef], 0x1000);
|
||||
let ram = EeRam::new(&v).unwrap();
|
||||
assert_eq!(ram.find(&[0xde, 0xad, 0xbe, 0xef]), Some(0x1000));
|
||||
assert!(ram.find(&[0xde, 0xad, 0xbe, 0xef, 0x00]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_or_oversized_needle_finds_nothing() {
|
||||
let v = vec![7u8; EE_SIZE];
|
||||
let ram = EeRam::new(&v).unwrap();
|
||||
assert!(ram.find(&[]).is_none());
|
||||
// Longer than the image cannot occur inside it, however uniform the
|
||||
// image is.
|
||||
let too_long = vec![7u8; EE_SIZE + 1];
|
||||
assert!(ram.find(&too_long).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_needle_at_the_very_end_is_found() {
|
||||
let mut v = vec![0u8; EE_SIZE];
|
||||
let n = [1u8, 2, 3, 4];
|
||||
v[EE_SIZE - 4..].copy_from_slice(&n);
|
||||
let ram = EeRam::new(&v).unwrap();
|
||||
assert_eq!(ram.find(&n), Some(EE_SIZE - 4));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user