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:
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user