diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index d497430..45dfab8 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -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 + ); +} diff --git a/docs/formats/core-data-regions.md b/docs/formats/core-data-regions.md index 5f9d89c..27083b9 100644 --- a/docs/formats/core-data-regions.md +++ b/docs/formats/core-data-regions.md @@ -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.