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"
);
}
}
+48
View File
@@ -2773,3 +2773,51 @@ the game does. The `tan` scale in particular -- 1024 for 1.0 -- is my reading of
the fixed-point convention and is **not** established, and the clip-cube scale of
64 is likewise R&C1's documented value, unverified. Both are noted in the module
doc so they are not mistaken for measured facts.
## The camera transform is at 0x18aa70 -- found live, verified by orthonormality
With PCSX2 on Aranos, the savestate problem went away: **PINE reads EE RAM live**,
so two dumps seconds apart are consecutive frames by construction, which is exactly
what the offline savestates could not provide.
Two live dumps 1.5s apart. Of every 16-byte-aligned triple of finite plausible
floats, **1,654 differ**; 1,501 are singletons and 153 repeat a neighbouring
triple (those are the per-object arrays). Most of the singletons are mob positions
moving 4-5 units. Two shapes stood out:
0x18aaa0 [247.299, 193.133, 50.453] -> ... moved 0.26 over 2s
0x18aa70..0x18ab2c unit vectors that reoriented
The second region is **three 4x4 matrices at 64-byte stride**:
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 (same shape, t = 247.239, 193.102, 50.497)
0x18aaf0 (same shape, t = 246.940, 193.155, 50.151)
**The control that makes this an identification:** all three upper 3x3 blocks are
orthonormal to within 1e-6 -- row norms 0.999995..0.999998, pairwise dot products
1e-6 and below. Three consecutive float triples in a random 32 MiB region being
unit length *and* mutually perpendicular is not a coincidence anyone gets twice.
The residual is f32 rounding, which is the expected result on an FPU that is not
IEEE-754 (AGENTS rule 2).
Each fourth row is `(x, y, z, 1.0)` -- affine, and the translation is the camera
position, shared across all three to within 0.4 units. Three matrices with one
position is what a view / modelview / lighting family looks like.
**Camera position on Aranos: (247.3, 193.1, 50.5).** Test asserts orthonormality,
`w == 1`, and that the translation moves smoothly but not wildly -- the second half
is the check `1aa94ee` needed and the savestates could not supply.
**What this unblocks.** `guard_band` step 1 is `R * (bsphere - camera * 1024)`.
Both operands are now available: `R` is rows 0-2 of `0x18aa70`, and the camera is
its fourth row. The clip test can be run against a real frame for the first time.
**Still to establish**, and deliberately not assumed: which of the three matrices is
the one step 1 wants, whether the translation is world or camera space (the
guard-band algorithm multiplies by 1024, which implies the disc's own 12.4 fixed
point, so the float translation may need converting first), and the frustum
planes, which the guard band needs and which are not in this structure.