From 3785195ecd21072f40257e15e3a36342fd95794f Mon Sep 17 00:00:00 2001 From: ReaperOfVeriod Date: Wed, 14 Oct 2026 03:42:42 +0200 Subject: [PATCH] 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. --- crates/gc-extract/tests/disc.rs | 83 +++++++++++++++++++++++++++++++ docs/formats/core-data-regions.md | 29 +++++++++++ docs/roadmap.md | 6 +++ 3 files changed, 118 insertions(+) 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