Read a moby handler: the chain works, with two caveats and a 110 discrepancy

Followed the chain through -- dispatch table to handler address to disassembly --
and read level 0's class 2 handler at 0x00380a88:

    0x00380a84: jal   0x2ee0d0
    0x00380a88: .word 0x0240202d          <- VU1 microcode, not a failure
    0x00380a8c: lwc1  $f12, 0x40($s1)
    0x00380a98: addiu $a0, $s0, 0x20
    0x00380aa0: swc1  $f0, 0x18($s4)
    0x00380ab0: swc1  $f0, 0x14($s4)

Floats out of one structure, written back into another, with VU1 work alongside --
what a transform update should look like. The chain from an asset observation to the
game's own code is now complete and usable.

TWO CAVEATS, both learned by hitting them:

VU1 microcode interleaves with CPU code. 0x0240202d is a COP2 opcode, not an
undecodable word. A tool that counts .word lines as failures is counting the wrong
thing -- a trap I walked into earlier today with the region walkers, now recorded
so it is not repeated.

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. Consecutive classes share one routine with
per-class entry points inside it, so reading "a handler" gives a fragment. That
matters downstream: a class's behaviour is not one function, and assuming
otherwise would misattribute shared code.

A DISCREPANCY WITH THE REFERENCE

The decomp 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 support the premise, so nothing turns on it, and I am recording it
rather than picking the number that suits. The reference measured on 2026-10-02
against its own pinned overlay set, so a method or version difference is as
plausible as a data difference 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.

264 tests.
This commit is contained in:
2026-10-14 03:40:59 +02:00
parent d611026f07
commit 18af702420
2 changed files with 142 additions and 0 deletions
+98
View File
@@ -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<u32> = 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<u32> = 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"
);
}