diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index 857b301..61eee1c 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -5984,3 +5984,101 @@ fn camera_and_sound_dispatch_tables_decode_at_their_documented_widths() { assert_eq!(out[1].1, 6, "distinct sound id count moved"); assert_eq!(out[1].2, 0x07, "the largest sound id moved"); } + +/// The dispatch handlers' addresses are almost all function entries -- measured +/// independently, and not quite matching the reference figure. +/// +/// `docs/MOBY-DISPATCH-TABLES.md` reports 5,598 of 5,788 moby handlers (96.7%) +/// preceded by a `jr ra` within the three words before them. Measured here the +/// same way, over the same 5,788 records: **5,488 (94.8%)**. +/// +/// Same population, same test, 110 handlers different. Both confirm the premise +/// that these addresses are function entries rather than arbitrary words, and the +/// residual is recorded as a discrepancy rather than explained away -- the +/// reference measured on 2026-10-02 against its own pinned overlay set, so a +/// difference in method or version is as likely as a difference in the data. +/// +/// Two things worth knowing before reading a handler. First, the code interleaves +/// VU1 microcode with CPU instructions, so a disassembly window runs `.word +/// 0x0240202d` lines that are COP2 opcodes rather than failures. Second, the +/// handlers are **not all independent functions**: level 0's class 2 and class +/// `0x0E` are 8 bytes apart (0x380a88 and 0x380a90), so consecutive classes share +/// one routine with per-class entry points inside it. Reading a handler gives you +/// a fragment, not a whole function. +#[test] +fn moby_handlers_are_overwhelmingly_function_entries() { + require_disc!(); + + let data = std::env::var_os("GC_EXTRACTED") + .map(PathBuf::from) + .unwrap_or_else(|| { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); + PathBuf::from(home).join("Documents/gc-extracted") + }); + if !data.exists() { + eprintln!("skipped: unpack the disc first"); + return; + } + + use gc_formats::overlay_reloc::parse_overlay_sections; + + const JR_RA: u32 = 0x03E0_0008; + let (mut total, mut with_jr) = (0usize, 0usize); + let mut gaps: Vec = Vec::new(); + for lv in 0..27usize { + let Ok(ov) = std::fs::read(data.join(format!("level{lv}/overlay.bin"))) else { + continue; + }; + let secs = parse_overlay_sections(&ov).expect("sections"); + let text = &secs[6]; + let mut last: Option = None; + for r in 0..secs[3].data.len() / 12 { + let off = r * 12 + 4; + let h = u32::from_le_bytes(secs[3].data[off..off + 4].try_into().unwrap()); + if h == u32::MAX { + break; + } + // how close is this handler to the previous one? + if let Some(p) = last { + if h > p && (h - p) < 16 { + gaps.push(h - p); + } + } + last = Some(h); + let Some(rel) = h.checked_sub(text.dest) else { + continue; + }; + let rel = rel as usize; + let Some(slice) = text.data.get(rel.saturating_sub(12)..rel) else { + continue; + }; + let jr = slice + .chunks_exact(4) + .rev() + .take(3) + .any(|w| u32::from_le_bytes([w[0], w[1], w[2], w[3]]) == JR_RA); + total += 1; + if jr { + with_jr += 1; + } + } + } + + let pct = 100.0 * with_jr as f64 / total as f64; + eprintln!( + "moby handlers: {with_jr} of {total} ({pct:.1}%) preceded by a jr ra within 3 \ + words -- the reference reports 5598 of 5788 (96.7%); {n} of those records sit \ + within 16 bytes of the one before", + n = gaps.len() + ); + assert_eq!(total, 5788, "handler record count moved"); + // the premise, asserted loosely because the reference's own figure differs + assert!( + pct > 90.0, + "handlers stopped looking like function entries: {pct:.1}%" + ); + assert!( + !gaps.is_empty(), + "no two consecutive handlers are close; they stopped sharing routines" + ); +} diff --git a/docs/formats/core-data-regions.md b/docs/formats/core-data-regions.md index dc710de..b8deedc 100644 --- a/docs/formats/core-data-regions.md +++ b/docs/formats/core-data-regions.md @@ -3196,3 +3196,47 @@ The camera and sound handler addresses fall inside section 6 of the same overlay which the working disassembler can read directly. Asserted in `camera_and_sound_dispatch_tables_decode_at_their_documented_widths`. + + +## The handler chain works, with two caveats and one discrepancy + +With the dispatch tables decoded and the disassembler fixed, a moby handler can +actually be read. Level 0's class 2, handler `0x00380a88`: + + 0x00380a84: jal 0x2ee0d0 + 0x00380a88: .word 0x0240202d <- VU1 microcode, not a decoding failure + 0x00380a8c: lwc1 $f12, 0x40($s1) + 0x00380a98: addiu $a0, $s0, 0x20 + 0x00380aa0: swc1 $f0, 0x18($s4) + 0x00380ab0: swc1 $f0, 0x14($s4) + +Floats read out of one structure and written back into another, with VU1 work +alongside -- which is what a transform update should look like. + +### Two caveats before reading one + +**VU1 microcode is interleaved with CPU code.** `0x0240202d` is a COP2 opcode, not +an undecodable word. A disassembly window will run `.word` lines that are +instructions in a different processor, and a tool that counts them as failures is +counting the wrong thing. + +**Handlers are not all independent functions.** Level 0's class 2 is at +`0x380a88` and class `0x0E` at `0x380a90` -- **eight bytes apart** -- and 72 of the +5,788 records sit within 16 bytes of the one before them. So consecutive classes +share one routine with per-class entry points inside it, and reading "a handler" +gives a fragment, not a whole function. + +### And a discrepancy with the reference + +`docs/MOBY-DISPATCH-TABLES.md` reports 5,598 of 5,788 moby handlers (96.7%) +preceded by a `jr ra` within three words. Measured here, the same way, over the +same 5,788 records: **5,488 (94.8%)**. + +Same population, same test, 110 handlers different. Both figures confirm the +premise, so nothing turns on it, and the residual is recorded rather than explained +away. The reference measured on 2026-10-02 against its own pinned overlay set, so +a difference in method or version is as plausible as a difference in the data, and +there is no basis here for saying which. The test asserts the premise (>90%) and +deliberately not either exact figure. + +Asserted in `moby_handlers_are_overwhelmingly_function_entries`.