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 check1aa94eeneeded 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:
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user