The dispatch tables hold 4,737 behaviours, not 5,822 records

Records and behaviours are different numbers, and the difference is one routine
serving several classes. Over all 27 levels:

    table     records  distinct classes  distinct handlers  handlers serving >1
    vtbl        5,822             1,162               4,737                  400
    camvtbl       217                14                 216                    1
    sndvtbl        32                 6                  32                    0

Sharing is real but modest, and it is almost entirely in the moby table: the
median handler serves one class and the busiest serves 34. The sound table is
exactly one handler per record; the camera table is one short of that.

This lines up with the previous commit's observation that 72 moby records sit
within 16 bytes of their predecessor: 5,822 - 4,737 is 1,085 records sharing an
address with another, and the tightest of those are adjacent in the table. Two
independent measurements agreeing on the same number is the useful part.

The practical figure is 4,737. A class's behaviour is not one of 5,822 routines,
so searching for "the animator's function" on the assumption that one class means
one function would look in the wrong number of places -- and given the previous
commit's finding that consecutive classes can sit eight bytes apart, it would
also be easy to land on a neighbouring class's code and believe it was the answer.

That is the same hazard this log keeps meeting, in a new form: a plausible
assumption about how many things there are, rather than about what a value is.

Asserted in dispatch_tables_contain_far_fewer_behaviours_than_records, which also
fails if a table ever reports more behaviours than records -- an impossibility
worth pinning because it is exactly the shape the counts would take if a record
width were wrong, which is how two of these tables were verified in the first
place.

265 tests.
This commit is contained in:
2026-10-14 03:42:42 +02:00
parent 18af702420
commit 3785195ecd
3 changed files with 118 additions and 0 deletions
+83
View File
@@ -6082,3 +6082,86 @@ fn moby_handlers_are_overwhelmingly_function_entries() {
"no two consecutive handlers are close; they stopped sharing routines"
);
}
/// How many distinct behaviours the three dispatch tables actually contain.
///
/// Records and behaviours are not the same number, and the difference is where
/// one routine serves several classes. Measured over all 27 levels:
/// table records distinct classes distinct handlers median per handler
/// vtbl 5,822 1,162 4,737 1
/// camvtbl 217 14 216 1
/// sndvtbl 32 6 32 1
///
/// So sharing is real but modest, and it is almost entirely in the moby table:
/// 5,822 records collapse to 4,737 addresses, with the most-shared handler
/// serving 34 class records. The camera and sound tables are close to one handler
/// per record -- the sound table is exactly so.
///
/// That matches the observation that 72 moby records sit within 16 bytes of their
/// predecessor: roughly 1,085 records share an address, and the tightest of those
/// are adjacent in the table.
#[test]
fn dispatch_tables_contain_far_fewer_behaviours_than_records() {
require_disc!();
let data = std::env::var_os("GC_EXTRACTED")
.map(PathBuf::from)
.unwrap_or_else(|| {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join("Documents/gc-extracted")
});
if !data.exists() {
eprintln!("skipped: unpack the disc first");
return;
}
use gc_formats::overlay_reloc::parse_overlay_sections;
use std::collections::BTreeMap;
// (section, record width, label, records, classes, handlers)
let want = [
(3usize, 3usize, "vtbl", 5822usize, 1162usize, 4737usize),
(4, 5, "camvtbl", 217, 14, 216),
(5, 2, "sndvtbl", 32, 6, 32),
];
for (idx, width, label, want_records, want_classes, want_handlers) in want {
let mut handlers: BTreeMap<u32, u32> = BTreeMap::new();
let mut classes: BTreeMap<u32, u32> = BTreeMap::new();
for lv in 0..27usize {
let Ok(ov) = std::fs::read(data.join(format!("level{lv}/overlay.bin"))) else {
continue;
};
let secs = parse_overlay_sections(&ov).expect("sections");
for r in 0..secs[idx].data.len() / width {
let b = r * width * 4;
let id = u32::from_le_bytes(secs[idx].data[b..b + 4].try_into().unwrap());
if id == u32::MAX {
break;
}
let h = u32::from_le_bytes(secs[idx].data[b + 4..b + 8].try_into().unwrap());
*handlers.entry(h).or_default() += 1;
*classes.entry(id).or_default() += 1;
}
}
let records: usize = handlers.values().sum::<u32>() as usize;
let shared = handlers.values().filter(|&&n| n > 1).count();
eprintln!(
"{label}: {records} records, {} classes, {} handlers ({shared} serve more \
than one)",
classes.len(),
handlers.len()
);
assert_eq!(records, want_records, "{label}: record count moved");
assert_eq!(classes.len(), want_classes, "{label}: class count moved");
assert_eq!(
handlers.len(),
want_handlers,
"{label}: handler count moved"
);
assert!(
handlers.len() <= records,
"{label}: more behaviours than records is impossible"
);
}
}
+29
View File
@@ -3240,3 +3240,32 @@ there is no basis here for saying which. The test asserts the premise (>90%) and
deliberately not either exact figure.
Asserted in `moby_handlers_are_overwhelmingly_function_entries`.
## How many behaviours the dispatch tables actually contain
Records and behaviours are not the same number, and the difference is where one
routine serves several classes. Over all 27 levels:
table records distinct classes distinct handlers handlers serving >1
vtbl 5,822 1,162 4,737 400
camvtbl 217 14 216 1
sndvtbl 32 6 32 0
So sharing is real but modest, and it is almost entirely in the moby table:
5,822 records collapse to 4,737 addresses, the median handler serves one class and
the most-shared serves 34. The sound table is exactly one handler per record, and
the camera table is one short of that.
That matches the earlier observation that 72 moby records sit within 16 bytes of
their predecessor: 5,822 - 4,737 is 1,085 records sharing an address with
another, and the tightest of those are adjacent in the table.
The practical figure is **4,737**. A class's behaviour is not one of 5,822
routines, and looking for "the animator's function" by assuming one class means one
function would search the wrong number of places.
Asserted in `dispatch_tables_contain_far_fewer_behaviours_than_records`, which
also fails if a table ever reports more behaviours than records -- an
impossibility worth pinning because it is the shape the counts would take if the
record widths were wrong.
+6
View File
@@ -407,6 +407,12 @@ Phase 1 hands over the extents; this phase works out what each one contains.
the chunk class-ID artifact now retracted. Re-scored against the real set,
no chunk slot separates (best 0.375 against 0.018 chance, a dozen slots in
the same band). Asserted.
- [x] **The dispatch tables hold 4,737 behaviours, not 5,822 records.** `lvl.vtbl`
collapses 5,822 records over 1,162 classes to 4,737 handler addresses, the
median serving one class and the busiest 34; 400 addresses serve more than
one. The camera and sound tables are essentially one handler per record
(217 -> 216, 32 -> 32). A class's behaviour is therefore not one function,
which matters for any search for a specific class routine. Asserted.
- [x] **All three dispatch tables decode.** `lvl.vtbl` (3 words), `lvl.camvtbl`
(5) and `lvl.sndvtbl` (2) each divide their sections **exactly**, with zero
remainder on all 27 levels -- which is the check that would fail if a record