diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index 61eee1c..f17f1e5 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -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 = BTreeMap::new(); + let mut classes: BTreeMap = 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::() 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" + ); + } +} diff --git a/docs/formats/core-data-regions.md b/docs/formats/core-data-regions.md index b8deedc..6ca2f25 100644 --- a/docs/formats/core-data-regions.md +++ b/docs/formats/core-data-regions.md @@ -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. diff --git a/docs/roadmap.md b/docs/roadmap.md index 4277f54..4304909 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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