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
|
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
|
64 is likewise R&C1's documented value, unverified. Both are noted in the module
|
||||||
doc so they are not mistaken for measured facts.
|
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