Camera transform found at 0x18aa70: three orthonormal matrices, one position

With PCSX2 on Aranos the savestate problem went away. PINE reads EE RAM live, so
two dumps seconds apart are consecutive frames by construction -- exactly what the
offline savestates could not provide (1aa94ee).

Of 1,654 changed 16-byte float triples between two live dumps, most singletons are
mob positions moving 4-5 units. One region is three 4x4 matrices at 64-byte
stride, each with an affine fourth row:

    0x18aa70   0.0338 -0.1983  0.9796  0.0000
              -0.0275 -0.9799 -0.1974  0.0000
               0.9990 -0.0203 -0.0386  0.0000
              247.2990 193.1330 50.4526  1.0000
    0x18aab0   t = 247.239, 193.102,  50.497
    0x18aaf0   t = 246.940, 193.155,  50.151

The control that makes this an identification rather than a coincidence: all three
upper 3x3 blocks are orthonormal to within 1e-6 -- row norms 0.999995..0.999998,
pairwise dots at or below 1e-6. Three consecutive float triples in a random 32 MiB
region being unit length AND mutually perpendicular does not happen twice. The
residual is f32 rounding, expected on an FPU that is not IEEE-754.

Camera position on Aranos: (247.3, 193.1, 50.5).

camera_transform_is_orthonormal_with_a_moving_translation asserts orthonormality,
w == 1, and that the translation moves smoothly but not wildly. That last half is
the check 1aa94ee needed and the savestates could not supply: consecutive frames
give 0.002-0.005 of movement, against ~230 between unrelated savestates.

What this unblocks: guard_band step 1 is R * (bsphere - camera * 1024). Both
operands now exist. R is rows 0-2 of 0x18aa70 and the camera is its fourth row, so
the clip test can run against a real frame for the first time.

Not assumed, and recorded as open: which of the three matrices step 1 wants; whether
the float translation needs converting to the disc's 12.4 fixed point first (the
algorithm multiplies by 1024, which implies it does); and the frustum planes, which
the guard band needs and which are not in this structure.
This commit is contained in:
2026-10-15 03:23:02 +02:00
parent b4d605a2f4
commit dcd4dd6246
2 changed files with 119 additions and 0 deletions
+71
View File
@@ -7913,3 +7913,74 @@ fn moby_classes_enrichment_survives_the_correct_mapping() {
);
}
}
/// The camera transform is three orthonormal matrices with a shared translation.
///
/// Read live over PINE: two EE RAM dumps taken seconds apart, then the region at
/// `0x18aa70` is checked to be a 4x4 matrix whose upper 3x3 is orthonormal and
/// whose fourth row is a translation that moves smoothly between the two reads.
///
/// Orthonormality is the control, and it is the part that makes this an
/// identification rather than a coincidence: three consecutive float triples
/// happening to be unit length and mutually perpendicular is not something a
/// random region of 32 MiB does.
#[test]
fn camera_transform_is_orthonormal_with_a_moving_translation() {
let Ok(p) = gc_trace::pine::Pine::connect() else {
eprintln!("skipped: PINE not available (is PCSX2 running?)");
return;
};
let a = p.dump_ee().expect("dump");
std::thread::sleep(std::time::Duration::from_millis(1500));
let b = p.dump_ee().expect("dump");
let f = |r: &[u8], o: usize| f32::from_le_bytes(r[o..o + 4].try_into().unwrap());
let m = |r: &[u8], base: usize, row: usize| {
[
f(r, base + row * 16),
f(r, base + row * 16 + 4),
f(r, base + row * 16 + 8),
f(r, base + row * 16 + 12),
]
};
for (name, base) in [
("m1", 0x0018_AA70usize),
("m2", 0x0018_AA_B0),
("m3", 0x0018_AA_F0),
] {
for row in 0..3 {
let r = m(&a, base, row);
let norm = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt();
assert!(
(norm - 1.0).abs() < 1e-4,
"{name} row {row} is not a unit vector: |r| = {norm}"
);
}
for (i, j) in [(0usize, 1usize), (0, 2), (1, 2)] {
let x = m(&a, base, i);
let y = m(&a, base, j);
let d = x[0] * y[0] + x[1] * y[1] + x[2] * y[2];
assert!(
d.abs() < 1e-4,
"{name} rows {i},{j} are not orthogonal: {d}"
);
}
let ta = m(&a, base, 3);
let tb = m(&b, base, 3);
assert_eq!(ta[3], 1.0, "{name} fourth row is not affine (w != 1)");
let moved = (ta[0] - tb[0]).hypot(ta[1] - tb[1]).hypot(ta[2] - tb[2]);
eprintln!(
"camera {name} at [{:.3}, {:.3}, {:.3}] moved {:.4} in 1.5s",
ta[0], ta[1], ta[2], moved
);
assert!(
moved > 0.0,
"{name} translation did not move; the game is paused?"
);
assert!(
moved < 50.0,
"{name} translation jumped {moved}; frames are not adjacent"
);
}
}