An external decomp turns two overlay constants from measured into named

OpenRAC/rac2-gc-decomp is the same disc as ours, verified rather than assumed:

  ours    3,828,350,976  9db2e33e...b1ce5
  theirs  3,828,350,976  9db2e33e...b1ce5

Vendored read-only under vendor/ (already gitignored) and added to PROVENANCE.

It names two constants this parser had measured but never explained:

  * overlay.bin +0x00 = 1,735,808 = 0x1A7C80, the .lit load base. It is constant
    across all 27 levels because every level's literal section loads at the same
    address. So the first header word is the load base of the section the file IS,
    which also establishes that overlay.bin is that section -- it holds both code
    and the debug string pool. $gp = 0x1AEFF0 comes with it, and that closes the
    roadmap's open "$gp for this game" item.

The program around it is seven sections -- .lit .bss .data vtbl camvtbl sndvtbl
.text -- which is precisely the "seven-section parser and relocation" Phase 3
item for this format. Known target now rather than a guess.

I then tested the obvious follow-up and it is wrong: if +0x00 is the .lit load
base, +0x0c ought to be the .lit size. It is SMALLER than the file on 16 of 27
levels, by up to 8.9% (level11: file 3,151,128 vs 2,870,792). Excluded by sign,
not by a near miss. Still unnamed.

Collision: still blocked, but no longer blocked on tooling. The decomp names
Camera_CollPrimTest(void *prim, void *grid) without decompiling it, so the cell
layout is not handed over. What it does supply is $gp, and that is enough to
start. "Camera_CollPrimTest WARNING! - grid out of bounds! (RAR)" is at
overlay+0xd18, the only copy in the file, so:

    string  overlay+0xd18 -> RAM 0x1a8998;  delta to $gp -0x6658;  $lo 0x99a8

MIPS puts the $lo half in bytes 1..3 of a 4-byte instruction. Searching the whole
overlay instruction-aligned gives exactly ONE hit:

    0x354cc8    (overlay+0x1ad048)

One occurrence in 2.75 MB of code is unambiguous: that is the instruction
materialising the assert string, and its enclosing function is
Camera_CollPrimTest. That is the route in, and it needs no symbols.

tools/overlay-anchors does this step, dependency-free. I started this with
capstone for the disassembly proper and dropped it: capstone 0.14 does not
re-export Mode/Endian from a public path, and an excluded-from-the-workspace
native dependency is a poor trade for a step that does not need one yet. The
anchor search is pure byte work, so it is pure Rust. No native dependency is
added anywhere, and the tool is an ordinary workspace member.

Surveyed the rest of that project against our open items: no volume structures,
no tie vertex layouts, no SBlk/DAW fields, no wavelet tables. It is a guide to
the code, not a replacement for reading it.
This commit is contained in:
2026-10-13 07:07:21 +02:00
parent 26a0918178
commit 1f903efe23
7 changed files with 222 additions and 2 deletions
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "gc-overlay-anchors"
description = "Locates MIPS code referencing a string in overlay.bin, via the GP-relative offset"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
publish = false
[dependencies]
anyhow = "1"
+104
View File
@@ -0,0 +1,104 @@
//! Find the MIPS code that references a string in `overlay.bin`.
//!
//! The overlay has no symbol table, but it has a fixed load base and a fixed
//! `$gp`, so every string has a GP-relative offset. MIPS materialises that as a
//! `%hi`/`%lo` pair, and the `%lo` half is a 16-bit immediate that occurs in the
//! instruction stream. Searching for it turns a string into a code address.
//!
//! This is the step that precedes disassembly, and it needs no disassembler.
use anyhow::{anyhow, Context, Result};
use std::collections::BTreeSet;
/// Where a level program is loaded, and its global pointer. Both are stated in
/// OpenRAC/rac2-gc-decomp docs/LEVELS.md and hold for all 27 levels.
const LIT_BASE: u64 = 0x1A7C80;
const GP: u64 = 0x1AEFF0;
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let path = args
.first()
.context("usage: gc-overlay-anchors <overlay.bin> [needle] [--level N]")?;
let overlay = std::fs::read(path).with_context(|| format!("read {path}"))?;
match args.get(1) {
None => {
list_strings(&overlay);
Ok(())
}
Some(needle) => {
let needle = needle.as_bytes();
let hits = find_all(&overlay, needle);
if hits.is_empty() {
return Err(anyhow!(
"{:?} not found in {path}",
String::from_utf8_lossy(needle)
));
}
for off in &hits {
report(&overlay, needle, *off);
}
Ok(())
}
}
}
fn report(overlay: &[u8], needle: &[u8], off: usize) {
let ram = LIT_BASE + off as u64;
let delta = ram as i64 - GP as i64;
let lo = (delta as u16).to_be_bytes();
eprintln!("{:?}", String::from_utf8_lossy(needle));
eprintln!(" overlay+{off:#x} -> RAM {ram:#x}");
eprintln!(
" $gp {GP:#x} -> delta {delta:#x} ({delta}), $lo half {:#06x}",
u16::from_be_bytes(lo)
);
// Count instruction-aligned occurrences of the $lo immediate. MIPS puts it in
// bytes 1..3 of a 4-byte instruction, big-endian regardless of file order.
let mut hits = BTreeSet::new();
for i in (0..overlay.len().saturating_sub(3)).step_by(4) {
if overlay[i + 1] == lo[0] && overlay[i + 2] == lo[1] {
hits.insert(i);
}
}
if hits.is_empty() {
eprintln!(" no instruction-aligned $lo occurrence -- not referenced through $gp\n");
} else {
eprintln!(" {} instruction-aligned occurrence(s):", hits.len());
for i in &hits {
eprintln!(" {:#x} (overlay+{:#x})", LIT_BASE + *i as u64, i);
}
eprintln!();
}
}
fn find_all(h: &[u8], needle: &[u8]) -> Vec<usize> {
if needle.is_empty() || needle.len() > h.len() {
return Vec::new();
}
(0..=h.len() - needle.len())
.filter(|&i| &h[i..i + needle.len()] == needle)
.collect()
}
fn list_strings(d: &[u8]) {
let mut i = 0usize;
while i < d.len() {
if d[i].is_ascii_graphic() || d[i] == b' ' {
let s = i;
while i < d.len() && (d[i].is_ascii_graphic() || d[i] == b' ') {
i += 1
}
if i - s >= 8 {
println!(
"{:#010x} {}",
LIT_BASE + s as u64,
String::from_utf8_lossy(&d[s..i])
);
}
} else {
i += 1;
}
}
}