From dcd4dd62463b894e79cfb2a67c0f6c64819ee109 Mon Sep 17 00:00:00 2001 From: ReaperOfVeriod Date: Thu, 15 Oct 2026 03:23:02 +0200 Subject: [PATCH] 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. --- crates/gc-extract/tests/disc.rs | 71 +++++++++++++++++++++++++++++++++ docs/plan/level-assets.md | 48 ++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index 2c7f3b6..f903d69 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -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" + ); + } +} diff --git a/docs/plan/level-assets.md b/docs/plan/level-assets.md index a65c11d..2007652 100644 --- a/docs/plan/level-assets.md +++ b/docs/plan/level-assets.md @@ -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.