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:
@@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
Reference in New Issue
Block a user