diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index 09702ab..be3a6e8 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -7993,3 +7993,70 @@ fn camera_transform_is_orthonormal_with_a_moving_translation() { ); } } + +/// `lvl.camvtbl` role 6 is `UpdateCamera_6`, verified in our overlay. +/// +/// `docs/AUG6-RETAIL-ANCHORS.md` recovers `UpdateCamera_6` and `InitCamera_24` as +/// class/role identifiers in the two dispatch tables, so role 6 of `lvl.camvtbl` +/// *is* the camera update function. The decomp's addresses are in the same space as +/// ours -- its `func_L00_002A3B80` is this overlay's `.text` dest exactly -- so the +/// claim is checkable rather than merely asserted. +/// +/// Three independent checks, because `PROVENANCE.md` records that the decomp's +/// function addresses have failed once here: +/// +/// 1. role 6 of level 0's `lvl.camvtbl` names handler `0x004284a8`; +/// 2. `overlay_bytes_at` reads real code there -- `0x3c014094`, a `lui`; +/// 3. `tools/overlay-anchors` decodes the first six words with **zero** failures and +/// they are floating-point: `mtc1 $at, $f1` after building `0x40947ae1`, then +/// `mtc1 $at, $f3` after `0x3f000000` (0.5f). A camera update doing float work; +/// not what a random address would give. +#[test] +fn camvtbl_role_6_is_the_camera_update_function() { + let Ok(p) = std::env::var("GC_EXTRACTED") else { + eprintln!("skipped: set GC_EXTRACTED"); + return; + }; + let Ok(ov) = std::fs::read(std::path::Path::new(&p).join("level0/overlay.bin")) else { + eprintln!("skipped: no overlay.bin"); + return; + }; + use gc_formats::overlay_reloc::{overlay_bytes_at, parse_overlay_sections}; + let secs = parse_overlay_sections(&ov).expect("sections"); + let cam = &secs[4]; + let mut handler = None; + for r in 0..cam.data.len() / 12 { + let id = u32::from_le_bytes(cam.data[r * 12..r * 12 + 4].try_into().unwrap()); + if id == u32::MAX { + break; + } + let h = u32::from_le_bytes(cam.data[r * 12 + 4..r * 12 + 8].try_into().unwrap()); + if h == u32::MAX { + break; + } + if id == 6 { + handler = Some(h); + break; + } + } + let h = handler.expect("camvtbl has no role 6"); + assert_eq!(h, 0x0042_84A8, "camvtbl role 6 moved"); + let text_end = secs[6].dest + secs[6].data.len() as u32; + assert!( + (secs[6].dest..text_end).contains(&h), + "handler {h:#x} is outside .text" + ); + let w = overlay_bytes_at(&ov, h).expect("in range"); + assert_eq!( + u32::from_le_bytes(w[..4].try_into().unwrap()), + 0x3C01_4094, + "first word at the handler changed" + ); + assert_eq!(u32::from_le_bytes(w[4..8].try_into().unwrap()), 0x3463_7AE1); + assert_eq!( + u32::from_le_bytes(w[8..12].try_into().unwrap()) >> 26, + 0x10, + "third word should be mtc1, got {:#x}", + w[8] + ); +} diff --git a/docs/plan/level-assets.md b/docs/plan/level-assets.md index 9e0dfec..d7008a2 100644 --- a/docs/plan/level-assets.md +++ b/docs/plan/level-assets.md @@ -2986,3 +2986,42 @@ overlay** before following it, not trusted from the name. **This is where the frustum hunt should resume**, and it is a different kind of task from the three searches above: follow a known call site rather than pattern- match a 32 MiB heap. + +### UpdateCamera_6 verified at 0x4284a8 + +The anchor `20655c7` said must be verified before following. It is. + +`docs/AUG6-RETAIL-ANCHORS.md` recovers `UpdateCamera_6` and `InitCamera_24` as +**class/role identifiers in the two dispatch tables**, so role 6 of `lvl.camvtbl` +*is* the camera update function. And the decomp's addresses are in the same space as +ours -- its `func_L00_002A3B80` is this overlay's `.text` dest exactly -- so the +claim is checkable rather than something to take on faith. + +Three independent checks, because `PROVENANCE.md` records that the decomp's function +addresses have failed once here: + +1. Role 6 of level 0's `lvl.camvtbl` names handler **`0x004284a8`**. +2. `overlay_bytes_at` reads real code there: `0x3c014094`, a `lui`. +3. `tools/overlay-anchors` decodes the first six words with **zero** failures: + + 0x4284a8 lui $at, 0x4094 + 0x4284ac ori $at, $at, 0x7ae1 ; $at = 0x40947ae1 + 0x4284b0 mtc1 $at, $f1 ; a float into an FP register + 0x4284b4 lw $v0, 0x2290($s4) + 0x4284b8 lui $at, 0x3f00 + 0x4284bc mtc1 $at, $f3 ; 0x3f000000 = 0.5f + +Two `mtc1` into FP registers, and a struct field at `+0x2290`. That is a function +doing floating-point work on a camera struct -- which is what a camera update +should look like, and not what a random address gives. Three checks agreeing is a +different kind of evidence from any one of them. + +**Level 0's `lvl.camvtbl` is 0xb4 bytes: 13 records, 13 distinct ids.** Note that +several ids read as implausible large values (4,350,904 and similar), so the +table's record layout is **not** uniformly id/handler and role 6 being clean may be +lucky. Worth keeping in mind when the other roles are read. + +**Next, now unblocked:** read the whole of `0x4284a8` and find the store that +writes `0x18aa70`. That confirms `m4` by locating the write site rather than by +inference from input response, and the frustum constants should be whatever the +same function writes nearby.