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].1, 6, "distinct sound id count moved");
assert_eq!(out[1].2, 0x07, "the largest sound id 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"
);
}
+44
View File
@@ -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. which the working disassembler can read directly.
Asserted in `camera_and_sound_dispatch_tables_decode_at_their_documented_widths`. 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`.