moby_classes.offset is referenced by 394 functions; its neighbours by 1 to 14

First positive result of the loader hunt, and it exists only because of the
control that two commits ago I said was missing.

Across 2,323 level-0 functions of at least 128 bytes -- extents from the decomp's
functions.tsv -- the immediate 0x00c0, which is moby_classes.offset, appears in
394 functions. Every value adjacent to it appears in 1 to 14.

    0xb8:8  0xb9:2  0xba:2  0xbb:2  0xbc:3  0xbd:5  0xbe:5  0xbf:3
    0xc1:3  0xc2:3  0xc3:2  0xc4:1  0xc5:2  0xc6:3  0xc7:1  0xc8:14

This is what 27891ce was asking for. 0xc0 is small and small immediates are
common -- 17,699 occurrences across the program, against 200,784 for 0x0001 -- so
the raw frequency was worthless. The neighbours have the same magnitude and none
of the frequency, which is the only reason 394 means anything rather than being an
artifact of the value being short.

A second control points the other way, so the two bracket it: the other five
level-0 chunk offsets are larger values and are referenced 0 to 2 times each.
Shrub_classes 1, tfrag_textures 2, moby_textures 2, tie_textures 0, shrub_textures
2. The chunk offsets a loader would use are referenced about once. moby_classes is
referenced 394 times. The program leans on this constant heavily.

The previous commit's 410 candidates were not a shortlist. These 394 are, because
the number is now separated from magnitude rather than merely stated.

WHAT THIS DOES NOT SAY

An immediate appearing in a function is not proof the chunk is READ there. It is
proof the constant is used, which is the claim the test asserts and the only claim
it makes. It does not say which of the 394 is the loader, or that the loader is
among them. Stated plainly because the 394 is the kind of number that gets
quoted without its caveat and then built on.

Also measured, negative: zero functions contain both the moby_classes and
tie_classes offsets, so nothing indexes both chunk tables by immediate. The moby
loader does not reach the tie table the same way, if it reaches it at all.

Boundaries: level 0 only, "level 0" meaning the function's first listed place is
level 00, and a 128-byte size floor -- 59957a0 recorded 20-byte "functions" that
are mis-bound extents.

283 passing with the disc present (23 + 19 + 63 + 156 + 22; disc binary 35.23s),
clippy clean.
This commit is contained in:
2026-10-14 05:28:34 +02:00
parent 59957a02a2
commit 3ea5ab799f
2 changed files with 155 additions and 0 deletions
+108
View File
@@ -6710,3 +6710,111 @@ fn tie_tail_offsets_are_not_more_structured_than_control() {
{mc:.5}: they may select real content after all"
);
}
/// `moby_classes.offset` is a load-bearing constant, proved by its neighbours.
///
/// The immediate `0x00c0` appears in **394 of 2,323** level-0 functions of at
/// least 128 bytes. Every value immediately adjacent to it appears in 1 to 14.
///
/// That is the control 27891ce lacked. `0xc0` is small, and small immediates are
/// common, so its raw frequency proved nothing on its own. The neighbours share
/// its magnitude exactly and share none of its frequency, which is what makes the
/// 394 mean something rather than being an artifact of the value being short.
///
/// Boundaries: level 0 only; function extents from the decomp's functions.tsv,
/// where "level 0" means the function's first listed place is level 00; size floor
/// of 128 bytes, since 59957a0 recorded 20-byte "functions" that are mis-bound
/// extents. An immediate alone does not prove the chunk is read -- it proves the
/// constant is used, and that is the claim being made.
#[test]
fn moby_classes_offset_is_referenced_far_more_than_its_neighbours() {
require_disc!();
let data = std::env::var_os("GC_EXTRACTED")
.map(PathBuf::from)
.unwrap_or_else(|| {
PathBuf::from(std::env::var("HOME").unwrap()).join("Documents/gc-extracted")
});
let dcomp = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../vendor/rac2-gc-decomp/config/levels/functions.tsv");
if !data.exists() || !dcomp.exists() {
eprintln!("skipped");
return;
}
let txt = std::fs::read_to_string(dcomp).unwrap();
let mut funcs: Vec<(u32, u32)> = Vec::new();
for line in txt.lines() {
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
let f: Vec<&str> = line.split('\t').collect();
if f.len() < 6 {
continue;
}
let size: u32 = match f[2].parse() {
Ok(v) => v,
Err(_) => continue,
};
for pl in f[5].split(',') {
let mut it = pl.split(':');
if it.next() == Some("00") {
if let Some(a) = it.next().and_then(|x| u32::from_str_radix(x, 16).ok()) {
funcs.push((a, size));
break;
}
}
}
}
funcs.sort_unstable();
let idx = std::fs::read(data.join("level0/core_index.bin")).unwrap();
let ov = std::fs::read(data.join("level0/overlay.bin")).unwrap();
let h = gc_formats::level_core::LevelCoreHeader::parse(&idx).unwrap();
let secs = gc_formats::overlay_reloc::parse_overlay_sections(&ov).unwrap();
let text = &secs[6].data;
let dest = secs[6].dest;
let has_imm = |op: u8| matches!(op, 0x09 | 0x0d | 0x0f | 0x0c | 0x05 | 0x06);
const FLOOR: u32 = 128;
let mut sets: Vec<std::collections::BTreeSet<u16>> = Vec::new();
for (start, size) in &funcs {
if *size < FLOOR {
continue;
}
let off = (*start).wrapping_sub(dest) as usize;
if off >= text.len() {
continue;
}
let n = (*size as usize).min(text.len() - off) & !3;
let mut set = std::collections::BTreeSet::new();
for w in text[off..off + n].chunks_exact(4) {
let i = u32::from_le_bytes([w[0], w[1], w[2], w[3]]);
if has_imm((i >> 26) as u8) {
set.insert((i & 0xffff) as u16);
}
}
sets.push(set);
}
let hit = |v: u16| sets.iter().filter(|s| s.contains(&v)).count();
let base = h.moby_classes.offset as u16;
assert!(sets.len() > 2_000, "only {} functions kept", sets.len());
let self_hits = hit(base);
let mut neighbour_max = 0usize;
let mut detail = Vec::new();
for d in -8i32..=8i32 {
if d == 0 {
continue;
}
let v = (base as i32 + d) as u16;
let c = hit(v);
neighbour_max = neighbour_max.max(c);
detail.push(format!("{v:#06x}:{c}"));
}
eprintln!("moby_classes {base:#06x} -> {self_hits} functions; neighbours {detail:?}");
assert!(self_hits > 100, "only {self_hits} functions reference it");
assert!(
self_hits > neighbour_max * 10,
"only {self_hits} vs neighbours up to {neighbour_max}: no longer special"
);
}
+47
View File
@@ -2003,3 +2003,50 @@ where this should resume.
Note the one caution this measurement earns: a 20-byte "function" containing the
immediate is not a real candidate, it is a function whose extent ends just after
some other function's instruction. Sizes need a floor before the set is used.
## moby_classes.offset is a load-bearing constant
First positive result of the loader hunt, and it turns entirely on the control.
Across 2,323 level-0 functions of at least 128 bytes (extents from the decomp's
`functions.tsv`), the immediate `0x00c0` -- `moby_classes.offset` -- appears in
**394 functions**
and every value immediately adjacent to it appears in 1 to 14:
0xb8:8 0xb9:2 0xba:2 0xbb:2 0xbc:3 0xbd:5 0xbe:5 0xbf:3
0xc1:3 0xc2:3 0xc3:2 0xc4:1 0xc5:2 0xc6:3 0xc7:1 0xc8:14
That is the control 27891ce said was missing. `0xc0` is small and small
immediates are common, so its raw frequency proved nothing -- 17,699 occurrences
across the program, with `0x0001` appearing 200,784 times. The neighbours have
the *same magnitude* and none of the frequency, which is what makes 394 mean
something instead of being an artifact of the value being short.
Against the five other level-0 chunk offsets used as a second control, all of
which are larger and so would be biased in the *other* direction:
shrub_classes 0x27c0 1 tfrag_textures 0x2910 2
moby_textures 0x2ee0 2 tie_textures 0x3960 0
shrub_textures 0x3e20 2
So the chunk offsets the loader would use are referenced about once each, and
`moby_classes` is referenced 394 times. The program uses this constant heavily.
**What this does not say.** An immediate appearing in a function is not proof the
chunk is *read* there -- it is proof the constant is used, which is the claim
being asserted and all the test asserts. It does not say which of the 394 is the
loader, or that the loader is among them.
**Also measured, and negative:** zero functions contain both the `moby_classes`
and `tie_classes` offsets, so there is no single function that indexes both chunk
tables by immediate. The moby loader, if it exists in this form, does not reach
the tie table the same way.
**Next step, now concrete:** the 394 is a real shortlist. Take the ones whose
immediates also include chunk offsets that *do* look like class-table references,
or the largest ones -- the 34,316-byte outlier is the most promising single
target -- and disassemble those. That is bounded work, unlike the four probes
that produced nothing.