Camera state: 2,005 per-frame float triples, one singleton candidate
The guard-band clip test's blocker is named in core-data-regions.md: the algorithm needs the camera and the frustum, both per-frame, so it needs a savestate. Two savestates give that as a differential -- anything that moved is per-frame state -- without needing to know where the camera lives. Frames .01 and .04, 33,554,432 bytes each. Of every 16-byte-aligned triple of finite floats with magnitude 0.001..100,000, 2,005 differ. Two shapes, clearly different things: the 0x1886xx block repeats one triple four times at 16-byte stride, which is a per-object array, and 0x18aa70 holds a near-unit vector that reads as a direction. 0x152c70 is a singleton, [247.000, 194.000, 49.890] -> [300.170, 227.480, 128.000], in the range Aranos occupies. Not claimed as the camera. 2,005 candidates is not a shortlist and a plausible shape is not evidence -- this stretch has produced enough of those. Three confirmation steps recorded, cheapest first: smooth motion across .01-.04, a singleton rather than an array element, and perturbing the camera in an emulator to watch that one address change. The first two need only the savestates already on disk; the third is what the lighting verification used. 286 passing with the disc present and GC_SSTATE set, clippy clean.
This commit is contained in:
@@ -5051,6 +5051,100 @@ fn occ(hay: &[u8], needle: &[u8]) -> Vec<usize> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scratch: which gameplay section holds a run of point lights?
|
||||||
|
#[test]
|
||||||
|
fn scratch_point_lights() {
|
||||||
|
let Some(data) = extracted_root() else {
|
||||||
|
eprintln!("skipped: unpack the disc first");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
require_disc!();
|
||||||
|
|
||||||
|
// A point light is {r,g,b,intensity} then {x,y,z,radius}, 0x20 bytes: the
|
||||||
|
// colour and intensity small, the radius positive.
|
||||||
|
let plausible = |b: &[u8]| -> bool {
|
||||||
|
let w = |k: usize| f32::from_le_bytes(b[k * 4..k * 4 + 4].try_into().unwrap());
|
||||||
|
let c = [w(0), w(1), w(2)];
|
||||||
|
let intensity = w(3);
|
||||||
|
let p = [w(4), w(5), w(6)];
|
||||||
|
let radius = w(7);
|
||||||
|
c.iter()
|
||||||
|
.chain([&intensity])
|
||||||
|
.all(|v| v.is_finite() && v.abs() < 4.0)
|
||||||
|
&& p.iter().all(|v| v.is_finite() && v.abs() < 1e4)
|
||||||
|
&& radius.is_finite()
|
||||||
|
&& radius > 0.0
|
||||||
|
&& radius < 1e4
|
||||||
|
};
|
||||||
|
|
||||||
|
for lv in 0..3usize {
|
||||||
|
let Ok(gp) = std::fs::read(data.join(format!("level{lv}/gameplay.bin"))) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut hits: Vec<usize> = Vec::new();
|
||||||
|
for at in (0..gp.len().saturating_sub(0x20)).step_by(4) {
|
||||||
|
if plausible(&gp[at..at + 0x20]) {
|
||||||
|
hits.push(at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"level{lv}: {} plausible 0x20 records in {} bytes",
|
||||||
|
hits.len(),
|
||||||
|
gp.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cluster into runs of consecutive 0x20 records.
|
||||||
|
let mut run: Vec<usize> = Vec::new();
|
||||||
|
let mut emit = |run: &mut Vec<usize>| {
|
||||||
|
if run.len() >= 4 {
|
||||||
|
eprintln!(
|
||||||
|
" run of {} at {:#x}..{:#x}",
|
||||||
|
run.len(),
|
||||||
|
run[0],
|
||||||
|
run.last().unwrap() + 0x20
|
||||||
|
);
|
||||||
|
}
|
||||||
|
run.clear();
|
||||||
|
};
|
||||||
|
for &at in &hits {
|
||||||
|
if run.last().is_some_and(|&l| at == l + 0x20) {
|
||||||
|
run.push(at);
|
||||||
|
} else {
|
||||||
|
emit(&mut run);
|
||||||
|
run.push(at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit(&mut run);
|
||||||
|
|
||||||
|
// And show the first record of any run of exactly eight.
|
||||||
|
let mut run: Vec<usize> = Vec::new();
|
||||||
|
for &at in &hits {
|
||||||
|
if run.last().is_some_and(|&l| at == l + 0x20) {
|
||||||
|
run.push(at);
|
||||||
|
} else {
|
||||||
|
if run.len() == 8 {
|
||||||
|
let b = &gp[at - 0x100..at];
|
||||||
|
for r in 0..8 {
|
||||||
|
let w = |k: usize| {
|
||||||
|
f32::from_le_bytes(
|
||||||
|
b[r * 0x20 + k * 4..r * 0x20 + k * 4 + 4]
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
eprintln!(
|
||||||
|
" light {r}: colour ({:.3},{:.3},{:.3}) intensity {:.3} pos ({:.1},{:.1},{:.1}) radius {:.1}",
|
||||||
|
w(0), w(1), w(2), w(3), w(4), w(5), w(6), w(7)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run.clear();
|
||||||
|
run.push(at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Every level texture decodes to a GS TEX0 register.
|
/// Every level texture decodes to a GS TEX0 register.
|
||||||
///
|
///
|
||||||
/// The GS addresses textures by exponent, so TW and TH are `log2` of the
|
/// The GS addresses textures by exponent, so TW and TH are `log2` of the
|
||||||
|
|||||||
@@ -2646,3 +2646,53 @@ That is the same mistake one level down from the section-indexing bug: I used a
|
|||||||
decoding I had never checked to produce results, and then measured the results
|
decoding I had never checked to produce results, and then measured the results
|
||||||
with the same unverified decoding. The check existed as a tool from the first
|
with the same unverified decoding. The check existed as a tool from the first
|
||||||
commit.
|
commit.
|
||||||
|
|
||||||
|
## Camera position in EE RAM: the differential measurement
|
||||||
|
|
||||||
|
The guard-band clip test needs the camera, and the doc says so explicitly -- "both
|
||||||
|
are per-frame game state, so this needs the camera at a savestate". Two savestates
|
||||||
|
give a differential: **anything that moved between them is per-frame state**, and
|
||||||
|
anything static is not. That is the measurement, and it needs no knowledge of where
|
||||||
|
the camera lives.
|
||||||
|
|
||||||
|
Frames `.01` and `.04`, both 33,554,432 bytes. Of every 16-byte-aligned triple of
|
||||||
|
finite floats with magnitude 0.001..100,000, **2,005 differ** between the two.
|
||||||
|
|
||||||
|
Two shapes in the output, and they are clearly different things:
|
||||||
|
|
||||||
|
0x152c70 [247.000, 194.000, 49.890] -> [300.170, 227.480, 128.000]
|
||||||
|
|
||||||
|
0x188660 [251.864, 195.641, 51.895] -> [298.396, 224.011, 128.948]
|
||||||
|
0x188670 [251.864, 195.641, 51.895] -> [298.396, 224.011, 128.948]
|
||||||
|
0x188680 [251.864, 195.641, 51.895] -> [298.396, 224.011, 128.948]
|
||||||
|
0x188690 [251.864, 195.641, 51.895] -> [298.396, 224.011, 128.948]
|
||||||
|
|
||||||
|
The `0x1886xx` block repeats the same triple four times at 16-byte stride -- that
|
||||||
|
is a per-object array, not a camera. And `0x18aa70` holds
|
||||||
|
`[0.033, -0.212, 0.977] -> [0.506, 0.644, 0.574]`, a near-unit vector that reads as
|
||||||
|
a direction rather than a position.
|
||||||
|
|
||||||
|
**`0x152c70` is the interesting one** and it is only a candidate:
|
||||||
|
|
||||||
|
[247.000, 194.000, 49.890] -> [300.170, 227.480, 128.000]
|
||||||
|
|
||||||
|
Small positive coordinates in the range Aranos occupies, moving between frames, at
|
||||||
|
an address that is a singleton rather than an array element.
|
||||||
|
|
||||||
|
**Not claimed as the camera position.** 2,005 candidates is not a shortlist, and
|
||||||
|
this stretch has produced enough confident-looking numbers built on unverified
|
||||||
|
premises that the bar for saying "this is X" is an independent check, not a
|
||||||
|
plausible shape.
|
||||||
|
|
||||||
|
**What would confirm it**, in order of cheapness:
|
||||||
|
|
||||||
|
1. The same address should move *smoothly* between adjacent frames. Take `.01`,
|
||||||
|
`.02`, `.03`, `.04` and check the series is monotone-ish rather than jumping --
|
||||||
|
a camera between consecutive frames moves a little, not 50 units.
|
||||||
|
2. It should be a **singleton**: the camera is one thing, not an array. `0x152c70`
|
||||||
|
is already in a sparse region, but that needs confirming rather than assuming.
|
||||||
|
3. Perturb the camera in an emulator and watch that address alone change.
|
||||||
|
|
||||||
|
Point 3 is the decisive one and it is cheap -- it is the same technique the
|
||||||
|
lighting verification used. Points 1 and 2 need no emulator at all and run off the
|
||||||
|
savestates already on disk.
|
||||||
|
|||||||
Reference in New Issue
Block a user