Update the decomp; read .text, and fix the disassembler that made it unreadable

Vendored rac2-gc-decomp is 13 commits behind, 12,375 lines added, almost all src/.
It names no collision data structure, so the format is still not handed over. Two
things are usable, and one precondition had to be fixed first.

THE PRECONDITION: the disassembler was stopping after five instructions.

tools/overlay-anchors called disasm_all across the whole window, which halts at
the first word capstone will not decode. In code regions that is almost
immediately, so every window came back with a handful of instructions and I had
concluded there was nothing there. It now walks four bytes at a time, prints
.word for anything undecodable, and reports how many failed. That is the whole
reason the earlier attempts produced nothing -- not that the code was absent.

WHAT THE DECOMP'S ASSERT TABLE ACTUALLY GIVES

assert-message-names.tsv maps each assert name to an address. For level 0,
CameraCollPrimTestGridOutOfBounds is 0x002db350. That address is not the assert
call and not the string. It is:

    0x002db350: jr              $ra
    0x002db354: addiu           $sp, $sp, 0x70

an epilogue. The address lands on the last group of the function before it, and
0x002db35c is a fresh prologue. It is also in .text (0x002a3b80..0x00447900),
five megabytes from the .lit string at 0x001a8998 -- which explains the "$gp search
returns zero" result from the other side. The string and the code are in different
sections, five megabytes apart.

WHAT IS ACTUALLY THERE

Immediately before the epilogue:

    0x002db2dc: lwc1   $f20, 0xc($s0)
    0x002db2ec: add.s   $f20, $f20, $f21
    0x002db2f0: c.olt.s $f0, $f20
    0x002db304: jal     0x2db4b8        <- the assert call
    0x002db314: lw      $v0, ($s1)      <- a count
    0x002db31c: slt     $v0, $s2, $v0
    0x002db320: bnez    $v0, 0x2db2cc   <- loop back
    0x002db324: addiu   $s0, $s0, 0x30  <- stride 48 bytes

A loop over 48-byte records, reading a float at +12 and accumulating, bounded by a
count, with f22 set to 1.0f just above it -- a minimum-height clamp.

NOT CLAIMED to be the collision grid. It is a strided float loop beside an assert
the decomp labels CameraCollPrimTestGridOutOfBounds. A plausible address near a
plausible assert is a lead, not an identification, and that is the exact gap this
log keeps tripping over. Confirming it means tracing the function's arguments back
to the collision payload and showing the grid's leading u16s -- level 0 is 11 x 33
-- drive the count at ($s1). Not done, and not asserted.

The undecodable words are VU1 microcode (COP2 opcodes), which is expected: the
function interleaves VIF-fed VU1 work with FPU arithmetic, like tfrag's.

Tools build, clippy clean, 260 tests.
This commit is contained in:
2026-10-14 03:16:17 +02:00
parent 508a03fa4a
commit 2f5979e652
2 changed files with 102 additions and 12 deletions
+71
View File
@@ -2937,3 +2937,74 @@ appeared to offer, and it closes it for a checkable reason rather than a vague
one. The route that remains is the one already recorded: find how the code
addresses the `Camera_CollPrimTest` assert string, which is not by a `$gp`-relative
load.
## A route into `.text`, and one thing the decomp's assert table does not mean
The decomp gained 13 commits (12,375 lines, almost all `src/`). Nothing in it
names a collision data structure, so the format is not handed over. Two things
are usable.
### `assert-message-names.tsv` addresses are code, in `.text`
The table maps each assert's name to an address, 25 overlays each. For level 0,
`CameraCollPrimTestGridOutOfBounds` is `0x002db350`.
That address is **not** the assert call and not the string. It is:
0x002db350: jr $ra
0x002db354: addiu $sp, $sp, 0x70
0x002db358: nop
an **epilogue** -- the address lands on the last instruction group of the
function before it. The `0x70` is the frame being torn down; `0x002db35c`
immediately after is a *new* prologue (`addiu $sp, $sp, -0x80`).
So the table's addresses are function-ish locations in `.text` (level 0's
`0x002db350` is well past `.lit`, which ends at `0x001b165c`, and inside
`0x002a3b80..0x00447900`). They are not string references, which is why the
earlier "$gp-relative search returns zero" result is now explained from the other
side: the string is in `.lit` at `0x001a8998` and the code is in `.text` five
megabytes away.
### What is actually in that function
Immediately before the epilogue:
0x002db2dc: lwc1 $f20, 0xc($s0)
0x002db2ec: add.s $f20, $f20, $f21
0x002db2f0: c.olt.s $f0, $f20
0x002db304: jal 0x2db4b8 <- the assert call
0x002db308: mov.s $f12, $f21
0x002db30c: bnez $v0, 0x2db32c
0x002db314: lw $v0, ($s1) <- a count
0x002db318: addiu $s2, $s2, 1
0x002db31c: slt $v0, $s2, $v0
0x002db320: bnez $v0, 0x2db2cc <- loop back
0x002db324: addiu $s0, $s0, 0x30 <- stride 48 bytes
A loop over records of **48 bytes**, reading a float at `+12` and accumulating,
guarded by a count at `($s1)`. `lui $at, 0x3f80` / `mtc1 $at, $f22` sets `f22` to
1.0f, which is a minimum-height clamp.
**This is not claimed to be the collision grid.** It is a strided float loop
next to an assert the decomp labels `CameraCollPrimTestGridOutOfBounds`, and that
is a lead, not an identification. The gap between them is exactly the gap this
log keeps tripping over: a plausible address near a plausible assert is not a
named function. Confirming it needs the function's arguments traced back to the
collision payload, and the grid's two leading `u16` dimensions (level 0 is
11 x 33) have not yet been shown to drive the count at `($s1)`.
Recorded as the most concrete route currently available, with its verification
gap stated rather than skipped.
### The disassembler needed fixing first
`tools/overlay-anchors` called `disasm_all` over the whole window, which stops at
the first word capstone will not decode. In code regions that is almost
immediately, so every window came back five instructions long -- which is why
earlier attempts at this produced nothing. It now walks four bytes at a time,
prints `.word` for anything undecodable, and reports the failure count.
The undecodable words here are VU1 microcode (`0x0260202d` and friends are COP2
opcodes), which is expected and is itself informative: this function interleaves
VIF-fed VU1 work with FPU arithmetic, like `tfrag`'s.
+31 -12
View File
@@ -111,20 +111,39 @@ fn disasm(overlay: &[u8], ram: u64, before: usize, after: usize) -> Result<()> {
.build()
.context("capstone mips32")?;
// Disassemble word by word rather than one `disasm_all` over the window: a
// single undecodable word stops the whole run, and code regions have data
// mixed in. Skipping four bytes and continuing is the point of the tool --
// without it a window around an interesting address is usually five
// instructions long.
let start = off.saturating_sub(before);
let chunk = &overlay[start..off + after];
let insns = cs
.disasm_all(chunk, LIT_BASE + start as u64)
.context("disassemble")?;
for i in insns.iter() {
let at = start + usize::try_from(i.address() - LIT_BASE).unwrap();
eprintln!(
"{} {:#010x} {}",
if at == off { ">>" } else { " " },
i.address(),
i
);
let end = (off + after).min(overlay.len());
let mut at = start;
let mut bad = 0usize;
while at + 4 <= end {
let word = u32::from_le_bytes(overlay[at..at + 4].try_into().unwrap());
match cs.disasm_all(&overlay[at..at + 4], LIT_BASE + at as u64) {
Ok(v) if !v.is_empty() => {
for i in v.iter() {
eprintln!(
"{} {:#010x} {i}",
if i.address() == LIT_BASE + off as u64 {
">>"
} else {
" "
},
i.address()
);
}
}
_ => {
eprintln!(" {:#010x} .word {word:#010x}", LIT_BASE + at as u64);
bad += 1;
}
}
at += 4;
}
eprintln!("({bad} of {} words did not decode)", (end - start) / 4);
Ok(())
}