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(()) }