Assert the tie vertex layout finding in a test, and audit which claims are asserted

The i16[3] high-byte finding -- the most recent and most subtle result of this
stretch, and the one I corrected twice -- lived only in prose. Two commits is
exactly how long a claim can sit unasserted before it drifts. It now has a test:

  tie_vertex_layout_is_selected_by_i16_3_high_byte

which reproduces the figures exactly and, importantly, asserts both directions:
(4,5) and (6,7) are above 0.85 and 0.80 in the ordinary group, (6,7) is BELOW 0.99
so it cannot silently become the constant I once mistook it for, (0,1) and (2,3)
are under 0.20, and every pair in the non-zero group is under 0.15. A future
change in either layout now fails loudly.

Also audited the rest of this session's claims against the test suite and recorded
the result in the format doc:

  tie vertex element is 16 bytes             asserted
  layout selected by i16[3] HIGH byte       asserted (this commit)
  blocks self-delimiting on 0xf6            asserted per-block
  packets == 0 means no block at that LOD   asserted
  packets[0] == 256 on 1,864 of 1,864       asserted
  chunk record length uniform per chunk      asserted
  moby class IDs in one body slot per type  asserted

  collision payload verbatim in RAM         NOT assertable here -- needs a savestate
  tfrag patch bytes all rewritten by LOD    same
  morphing is unobservable in that image    same

The split is deliberate: disc-readable claims are asserted against the disc,
savestate-dependent ones live in plan/pcsx2-verification.md with their method.
What is not acceptable is a disc-readable claim living only in prose -- which is
how the tie i16[3] finding spent two commits before it had a test, and how several
older claims drifted before the roadmap reconciliation caught them.
This commit is contained in:
2026-10-13 06:54:52 +02:00
parent 922248a4ec
commit 9f938f47de
2 changed files with 192 additions and 0 deletions
+161
View File
@@ -3531,3 +3531,164 @@ fn tie_vertex_element_is_16_bytes_with_fixed_texcoords() {
r12 * 100.0
);
}
/// The tie vertex element's layout is selected by the HIGH byte of `i16[3]`.
///
/// Measured over the tie vertices of levels 0-2, grouped by that high byte:
///
/// - high byte zero (the ordinary case, 89,341 vertices): slot pairs `(4,5)` and
/// `(6,7)` are both 12-bit fixed, at 92.9% and 89.5% for a divisor of 4, while
/// `(0,1)` and `(2,3)` sit at baseline.
/// - high byte non-zero (6,271 vertices): **no** pair is fixed; the best is 7.5%
/// against a 2-5% baseline.
///
/// A constant would score 100%, so `(6,7)` at 89.5% is a real pair rather than
/// the `4096, 0` artifact it looks like. See `docs/formats/core-data-regions.md`.
#[test]
fn tie_vertex_layout_is_selected_by_i16_3_high_byte() {
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::level_core::{ClassEntry, LevelCoreHeader};
use gc_formats::tie::{parse_tie_block, TieClassHeader, TieRecord};
// [pair][k] where k indexes divisor 1, 4, 16, 256
let divisors = [1i32, 4, 16, 256];
let mut zero = [[0u64; 4]; 4];
let mut nonzero = [[0u64; 4]; 4];
let (mut n_zero, mut n_nonzero) = (0u64, 0u64);
for lv in 0..3usize {
let idx =
std::fs::read(data.join(format!("level{lv}/core_index.bin"))).expect("core_index");
let core = std::fs::read(data.join(format!("level{lv}/core_data.bin"))).expect("core_data");
let h = LevelCoreHeader::parse(&idx).expect("hdr");
let t = h.tie_classes;
for c in 0..t.count as usize {
let o = t.offset as usize + c * 0x20;
let e = ClassEntry::parse(&idx[o..o + 0x20]).expect("entry");
let base = e.offset_in_asset_wad as usize;
if base == 0 || base + 0x48 > core.len() {
continue;
}
let Ok(hd) = TieClassHeader::parse(&core[base..base + 0x48]) else {
continue;
};
for lod in [1usize, 2] {
let off = hd.packets[lod] as usize;
if off == 0 {
continue;
}
let s = base + off;
let Some(blk) = parse_tie_block(&core, s) else {
continue;
};
let recs: Vec<&[u8]> = blk.header.records(&core[s..]).collect();
for (ri, rec) in recs.iter().enumerate() {
let Ok(r) = TieRecord::parse(rec) else {
continue;
};
let start = s + 16 + ri * 16 + r.pointer as usize + r.vert_ofs as usize * 16;
for v in 0..r.vert_size as usize {
let q = start + v * 16;
if q + 16 > core.len() {
break;
}
let hi = (i16::from_le_bytes(core[q + 6..q + 8].try_into().unwrap())
as u32
>> 8)
!= 0;
if hi {
n_nonzero += 1
} else {
n_zero += 1
}
let tbl = if hi { &mut nonzero } else { &mut zero };
for pair in 0..4usize {
let a = i16::from_le_bytes(
core[q + pair * 4..q + pair * 4 + 2].try_into().unwrap(),
) as i32;
let b = i16::from_le_bytes(
core[q + pair * 4 + 2..q + pair * 4 + 4].try_into().unwrap(),
) as i32;
for (k, dv) in divisors.iter().enumerate() {
if a % dv == 0 && b % dv == 0 {
tbl[pair][k] += 1
}
}
}
}
}
}
}
}
assert!(n_zero > 80_000, "only {n_zero} ordinary tie vertices");
assert!(
n_nonzero > 5_000,
"only {n_nonzero} vertices with a non-zero i16[3] high byte"
);
let rate = |t: &[[u64; 4]; 4], pair: usize, k: usize, n: u64| t[pair][k] as f64 / n as f64;
// The ordinary case: (4,5) and (6,7) are both fixed-point pairs.
assert!(
rate(&zero, 2, 1, n_zero) > 0.85,
"(4,5) fell to {:.2}",
rate(&zero, 2, 1, n_zero)
);
assert!(
rate(&zero, 3, 1, n_zero) > 0.80,
"(6,7) fell to {:.2}",
rate(&zero, 3, 1, n_zero)
);
// A constant would be 1.0; below 1.0 means it is not one.
assert!(
rate(&zero, 3, 1, n_zero) < 0.99,
"(6,7) is exactly constant again; revisit the pair claim"
);
// (0,1) and (2,3) are baseline.
assert!(
rate(&zero, 0, 1, n_zero) < 0.20,
"(0,1) rose to {:.2}",
rate(&zero, 0, 1, n_zero)
);
assert!(
rate(&zero, 1, 1, n_zero) < 0.20,
"(2,3) rose to {:.2}",
rate(&zero, 1, 1, n_zero)
);
// The non-zero case: nothing is fixed.
for pair in 0..4usize {
assert!(
rate(&nonzero, pair, 1, n_nonzero) < 0.15,
"pair ({},{}) rose to {:.2} in the non-zero group",
pair * 2,
pair * 2 + 1,
rate(&nonzero, pair, 1, n_nonzero)
);
}
eprintln!(
"tie vertex layout by i16[3] high byte, levels 0-2: \
zero {n_zero} -> (4,5) {:.1}%, (6,7) {:.1}%, (0,1) {:.1}%, (2,3) {:.1}%; \
non-zero {n_nonzero} -> best pair {:.1}%",
rate(&zero, 2, 1, n_zero) * 100.0,
rate(&zero, 3, 1, n_zero) * 100.0,
rate(&zero, 0, 1, n_zero) * 100.0,
rate(&zero, 1, 1, n_zero) * 100.0,
(0..4)
.map(|p| rate(&nonzero, p, 1, n_nonzero))
.fold(0.0f64, f64::max)
* 100.0
);
}
+31
View File
@@ -1804,3 +1804,34 @@ fixed at `i16[4]`/`i16[5]`, 73.6% of the time, unexplained". The field position
was right; the 73.6% was a population average over three encodings, which is
exactly the pooled-number problem rule 3a describes -- partitioning by the one
variable that separates them turns a mystery into a lookup.
### Which of these findings are asserted
Not every claim here has a test behind it, and the recent ones are the ones most
likely to be lost. Audit against `crates/gc-extract/tests/disc.rs`:
claim asserted in
------------------------------------------- -----------------------------------
tie vertex element is 16 bytes tie_vertex_element_is_16_bytes_
with_fixed_texcoords
layout selected by i16[3] HIGH byte tie_vertex_layout_is_selected_by_
i16_3_high_byte
blocks self-delimiting on 0xf6 tie_classes_resolve_through_both_
indirections (per-block assert)
packets == 0 means no block at that LOD same test
packets[0] == 256 on 1,864 of 1,864 same test
chunk record length uniform per chunk tie-classes test + chunk_records_
start_16_byte_aligned
moby class IDs in one body slot per type moby_class_ids_sit_in_one_slot_
per_chunk_type
collision payload verbatim in RAM NOT assertable here -- needs a
savestate, so it lives in
plan/pcsx2-verification.md
tfrag patch bytes all rewritten by LOD same -- savestate-dependent
morphing is unobservable in that image same -- savestate-dependent
The split is deliberate: anything readable from the disc is asserted against the
disc, and anything that needs a running PCSX2 is documented in the plan file with
its method. What is *not* acceptable is a disc-readable claim living only in
prose, which is how the tie `i16[3]` finding spent two commits before it had a
test.