diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index f8d1a56..3205b6e 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -6321,3 +6321,96 @@ fn level_table_has_twenty_seven_keys_with_twenty_one_absent() { assert_eq!(ids, expected, "the level-id set moved"); assert_eq!(*ids.last().unwrap(), 30, "the highest level id is 30"); } + +/// `bfc_distance` is not the length of the control region, partitioned by +/// `shader_count`. +/// +/// `bfc_distance == 5 * shader_count` holds universally, but reading it as +/// "quadwords of AD-GIF data" makes it the length of the leading region -- and it +/// cannot be. `bfc_distance` runs 5..20 quadwords and `vert_ofs` only 3..8. +/// +/// Pooled, the two overlap on 9,315 of 15,219 records and that number describes +/// nothing. Partitioned by `shader_count` it is exact: for two or more shaders +/// `bfc_distance` exceeds `vert_ofs` on **every** record, and for one shader on +/// 32%, which is just the share whose `vert_ofs` is below 5. +/// +/// The AD-GIF is therefore not stored in the tie packet. `bfc_distance` is a +/// per-record quantity relating to the shader count, and the packet holds control, +/// vertices and scissor in that order. +#[test] +fn tie_bfc_distance_is_not_the_control_region_length() { + 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(|_| ".".into()); + PathBuf::from(home).join("Documents/gc-extracted") + }); + if !data.exists() { + eprintln!("skipped: unpack the disc first"); + return; + } + use gc_formats::level_core::{ClassEntry, LevelCoreHeader}; + use gc_formats::tie::{parse_tie_block, TieClassHeader, TieRecord}; + use std::collections::BTreeMap; + + let mut by_sc: BTreeMap = BTreeMap::new(); + let mut total = 0usize; + + for lv in 0..27usize { + let (Ok(idx), Ok(core)) = ( + std::fs::read(data.join(format!("level{lv}/core_index.bin"))), + std::fs::read(data.join(format!("level{lv}/core_data.bin"))), + ) else { + continue; + }; + let t = LevelCoreHeader::parse(&idx).expect("hdr").tie_classes; + for c in 0..t.count as usize { + let o = t.offset as usize + c * 0x20; + let Ok(e) = ClassEntry::parse(&idx[o..o + 0x20]) else { + continue; + }; + let base = e.offset_in_asset_wad as usize; + if base == 0 || base + 0x48 > core.len() { + continue; + } + let Ok(hdr) = TieClassHeader::parse(&core[base..base + 0x48]) else { + continue; + }; + for lod in 1..=2usize { + let off = hdr.packets[lod]; + if off <= 0 { + continue; + } + let s = base + off as usize; + let Some(blk) = parse_tie_block(&core, s) else { + continue; + }; + for r in blk.header.records(&core[s..]) { + let Ok(r) = TieRecord::parse(r) else { continue }; + total += 1; + let e = by_sc.entry(r.shader_count).or_insert((0, 0)); + e.0 += 1; + e.1 += usize::from(r.bfc_distance > r.vert_ofs); + } + } + } + } + + assert!(total > 15_000, "only {total} tie records read"); + eprintln!("bfc_distance > vert_ofs, by shader_count: {by_sc:?}"); + for (sc, (n, over)) in &by_sc { + if *sc >= 2 { + assert_eq!( + *n, *over, + "shader_count {sc}: expected every record to exceed, got {over}/{n}" + ); + } + } + // One shader: exceeds exactly when vert_ofs < 5, i.e. control < 5 quadwords. + let (n1, over1) = by_sc[&1]; + assert!( + over1 < n1, + "shader_count 1 should not exceed on every record" + ); +} diff --git a/crates/gc-formats/src/tie.rs b/crates/gc-formats/src/tie.rs index d8df87d..a50b052 100644 --- a/crates/gc-formats/src/tie.rs +++ b/crates/gc-formats/src/tie.rs @@ -246,7 +246,19 @@ pub struct TieRecord { pub pointer: u32, /// `+0x04`: AD-GIF (material) blocks this packet references; 1..4 on disc. pub shader_count: u8, - /// `+0x05`: quadwords of ad-gif data. Equals `5 * shader_count`. + /// `+0x05`: equals `5 * shader_count` on every record measured. + /// + /// **This is not the length of the control region**, despite what the field + /// is usually described as. It runs 5..20 quadwords and the control region is + /// `vert_ofs`, 3..8, so for two or more shaders the value exceeds the region + /// on *every* record -- 9,315 of 15,219 pooled, which is a number describing + /// nothing; partitioned by `shader_count` it is 100% for 2, 3 and 4 and 32% + /// for 1, which is exactly the share whose `vert_ofs` is below 5. + /// + /// So the AD-GIF is **not stored in the tie packet**. What this value means + /// is not established; the `5 * shader_count` relation is solid and the + /// "quadwords of ad-gif data" reading of it is not. See + /// `tie_bfc_distance_is_not_the_control_region_length`. pub bfc_distance: u8, /// `+0x06`: 4..18 on disc. pub control_count: u8, diff --git a/docs/plan/level-assets.md b/docs/plan/level-assets.md index 15c1ee3..97b8408 100644 --- a/docs/plan/level-assets.md +++ b/docs/plan/level-assets.md @@ -1807,6 +1807,45 @@ the packet. The field name is left alone because the on-disc order is fixed and everything already written about this record says `vert_ofs`, but the doc now says what the value is. -**Still not known:** what the control region actually contains. It is not -standard VIF or GIF DMA tags. It may be pre-merged GS register writes in a -different framing, or per-class state rather than per-primitive draw data. +**What the control region is:** per-record, not shared. 12,294 distinct blobs +across 13,042 records, mixed within 2,410 of 3,549 classes, and different between +LOD 0 and LOD 1 on all 500 classes that have both. It varies per primitive. + +Partitioned by its own length it is *not* uniform in character -- the pooled +count hid this: + + vert_ofs 48 bytes (3 qw) 5 distinct blobs + vert_ofs 64 bytes (4 qw) 3,175 + vert_ofs 80 bytes (5 qw) 4,309 + vert_ofs 96 bytes (6 qw) 3,281 + vert_ofs 112 bytes (7 qw) 1,523 + vert_ofs 128 bytes (8 qw) 1 + +The shortest and longest are effectively constant -- five blobs and one -- and +everything in between is per-record unique. That is a shape, not a noise floor. + +## bfc_distance is not the control region's length + +The other half of the answer. `bfc_distance == 5 * shader_count` holds on every +record and has always been documented as "quadwords of ad-gif data" -- which, read +as a length within the packet, would make it the control region's size. + +It cannot be. `bfc_distance` is 5..20 quadwords; `vert_ofs`, the control region, +is 3..8. + +Pooled, the two overlap on 9,315 of 15,219 records (61%) -- a number describing +nothing. Partitioned by `shader_count`, as rule 3a requires, it is exact: + + shader_count 1: 2,772 of 8,676 exceed (32%) + shader_count 2: 4,649 of 4,649 exceed (100%) + shader_count 3: 1,729 of 1,729 exceed (100%) + shader_count 4: 165 of 165 exceed (100%) + +For two or more shaders it exceeds on every record, without exception. For one +shader it exceeds exactly when `vert_ofs < 5`, which is the 32%. + +**So the AD-GIF is not stored in the tie packet.** The packet is control, +vertices, scissor. `bfc_distance` is a per-record quantity relating to the shader +count whose meaning is not established, and the "quadwords of ad-gif data" reading +of it is wrong. The field doc said so in terms before this measurement; it does +not now. diff --git a/docs/roadmap.md b/docs/roadmap.md index e620415..6e3ed0f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -747,7 +747,9 @@ lookup, and it is the main thing inherited from ReRAC. the GS's draw packets are not stored there in standard DMA form. What the probe did establish is the packet partition (control / vertices / scissor) and a universal invariant, `scissor_ofs == vert_ofs + vert_size`, on 15,219 - of 15,219 records. See `docs/plan/level-assets.md`. + of 15,219 records. **`bfc_distance` is also not the control region's length** -- + 100% of records exceed it for 2+ shaders, 32% for one -- so the AD-GIF is not + stored in the tie packet at all. See `docs/plan/level-assets.md`. **Gate:** CPU and GPU paths agree on every vertex of a reference level — the same identity check ReRAC runs.