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:
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user