bfc_distance is not the control region's length; the AD-GIF is not in the packet

Follows 9bb04cc. The control region is now characterised, and the second half of
the answer is a correction to a field doc that has been wrong since the record
was first parsed.

WHAT THE CONTROL REGION IS

Per-record, not shared state. 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, and the pooled
count hid that:

    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 -- while
everything between is per-record unique. That is a shape, not a noise floor, and
it is invisible unless the measurement is partitioned by length.

THE CORRECTION

bfc_distance == 5 * shader_count holds on every record, and the field doc has
always read "quadwords of ad-gif data" -- which read as a packet-relative length
would make it the control region's size.

It cannot be that. bfc_distance is 5..20 quadwords; vert_ofs, the control region,
is 3..8.

Pooled, they overlap on 9,315 of 15,219 records -- 61%, a number describing
nothing. Partitioned by shader_count, as AGENTS 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%)

Without exception for two or more shaders. For one shader it exceeds exactly when
vert_ofs is below 5, which is that 32%.

**The AD-GIF is not stored in the tie packet.** The packet is control, vertices
and scissor, in that order. bfc_distance is a per-record quantity tied to the
shader count whose meaning is still not established, and the "quadwords of ad-gif
data" reading of it is wrong. The doc now says so and says why, rather than
carrying a plausible description of a field whose meaning nobody has established.

This is the fourth retraction today and the second today found by partitioning
rather than by a test failing. The pooled number was not merely less precise than
the partitioned ones -- it pointed the other way, because 61% looks like noise and
100% looks like a boundary. Had I accepted the pooled figure I would have written
"sometimes" and moved on.

59 disc tests, 278 total across 23 + 19 + 59 + 156 + 22 with the disc present
(disc binary 35.31s), clippy clean.
This commit is contained in:
2026-10-14 04:57:41 +02:00
parent 9bb04cca7b
commit f43a1c2e6e
4 changed files with 151 additions and 5 deletions
+93
View File
@@ -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<u8, (usize, usize)> = 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"
);
}
+13 -1
View File
@@ -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,