From 2f5979e652b237304de82096e6e329ae7972ce6b Mon Sep 17 00:00:00 2001 From: ReaperOfVeriod Date: Wed, 14 Oct 2026 03:16:17 +0200 Subject: [PATCH] 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. --- docs/formats/core-data-regions.md | 71 +++++++++++++++++++++++++++++++ tools/overlay-anchors/src/main.rs | 43 +++++++++++++------ 2 files changed, 102 insertions(+), 12 deletions(-) diff --git a/docs/formats/core-data-regions.md b/docs/formats/core-data-regions.md index bbbf2c2..f5dd199 100644 --- a/docs/formats/core-data-regions.md +++ b/docs/formats/core-data-regions.md @@ -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. diff --git a/tools/overlay-anchors/src/main.rs b/tools/overlay-anchors/src/main.rs index def00f3..ddbcc41 100644 --- a/tools/overlay-anchors/src/main.rs +++ b/tools/overlay-anchors/src/main.rs @@ -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(()) }