//! How much geometry on the disc was being read one index element late? //! //! The pad-scoring fix (2026-08-13) makes `anchor_pool_mesh` choose the pad whose //! index run is cleanest instead of the first that validates. This measures the //! blast radius: per container, how many single-block resources now decode from a //! run that is NOT the old first-match (`pad 0`) one, and how many decoded runs //! still contain degenerate triangles (the shift signature) afterwards. //! //! Usage: pad_shift_audit use sylpheed_formats::mesh::Xbg7Model; fn be16(b: &[u8], at: usize) -> u32 { ((b[at] as u32) << 8) | b[at + 1] as u32 } /// Fraction of triangles whose face normal agrees with the stored normals, /// folded to `max(na, 1-na)` so both authored windings read as ≈1. fn winding(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> f32 { let (mut agree, mut n) = (0usize, 0usize); for t in idx.chunks_exact(3) { let (a, b, c) = (t[0] as usize, t[1] as usize, t[2] as usize); if a == b || b == c || a == c || a.max(b).max(c) >= pos.len() || a >= nrm.len() { continue; } let e1 = [pos[b][0] - pos[a][0], pos[b][1] - pos[a][1], pos[b][2] - pos[a][2]]; let e2 = [pos[c][0] - pos[a][0], pos[c][1] - pos[a][1], pos[c][2] - pos[a][2]]; let f = [ e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0], ]; let nv = nrm[a]; n += 1; if f[0] * nv[0] + f[1] * nv[1] + f[2] * nv[2] > 0.0 { agree += 1; } } if n == 0 { return 1.0; } let fr = agree as f32 / n as f32; fr.max(1.0 - fr) } fn degenerate(idx: &[u32]) -> usize { idx.chunks_exact(3) .filter(|t| t[0] == t[1] || t[1] == t[2] || t[0] == t[2]) .count() } fn main() { let dir = std::env::args().nth(1).expect("resource3d dir"); let mut files: Vec<_> = std::fs::read_dir(&dir) .unwrap() .flatten() .map(|e| e.path()) .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("xpr")) .collect(); files.sort(); let (mut total, mut moved, mut still_degen) = (0usize, 0usize, 0usize); let mut weak = 0usize; let mut weak_wind: Vec<(f32, f32, String)> = Vec::new(); let mut worst: Vec<(usize, String, String)> = Vec::new(); for f in &files { let Ok(bytes) = std::fs::read(f) else { continue }; let where_ = f.file_name().unwrap().to_string_lossy().to_string(); let mut c_moved = 0usize; for m in Xbg7Model::stage_models(&bytes) { if m.meshes.len() != 1 { continue; } let sm = &m.meshes[0]; let Some(vb) = sm.vbuf_offset else { continue }; let n = sm.indices.len(); if n < 6 || vb < n * 2 { continue; } total += 1; let pad0: Vec = (0..n).map(|k| be16(&bytes, vb - n * 2 + k * 2)).collect(); if pad0 != sm.indices { moved += 1; c_moved += 1; // How strong was the evidence for moving? A pad-0 run with // degenerate triangles is a decisive shift signature; one with // none moved on winding alone, which is weaker. if degenerate(&pad0) == 0 { weak += 1; // Record how far apart the two runs' winding is, to see // whether these look like the same shift signature as the // decisive cases (pad-0 winding drifting toward 0.5) or like // noise between two equally clean readings. let w0 = winding(&pad0, &sm.positions, &sm.normals); let w2: f32 = { let l2: Vec = (0..n).map(|k| be16(&bytes, vb - n * 2 - 2 + k * 2)).collect(); winding(&l2, &sm.positions, &sm.normals) }; weak_wind.push((w0, w2, m.name.clone())); } } let d = degenerate(&sm.indices); if d > 0 { still_degen += 1; worst.push((d, m.name.clone(), where_.clone())); } } if c_moved > 0 { println!("{where_:<24} {c_moved} resources read from a non-pad-0 index run"); } } println!("\n{moved} of {total} single-block resources moved off the pad-0 run"); println!("{still_degen} decoded runs still contain a degenerate triangle"); println!("of the moved, {weak} had a pad-0 run with no degenerate triangle (moved on winding alone)"); let clear = weak_wind.iter().filter(|(w0, w2, _)| *w2 - *w0 > 0.15).count(); let close = weak_wind.iter().filter(|(w0, w2, _)| (*w2 - *w0).abs() <= 0.05).count(); println!(" of those: {clear} show the shift signature (pad-2 winding > pad-0 by >0.15), {close} are within 0.05 (noise)"); for (w0, w2, n) in weak_wind.iter().take(8) { println!(" {n}: pad0 wind {w0:.3} -> pad2 {w2:.3}"); } worst.sort_by_key(|(d, _, _)| std::cmp::Reverse(*d)); for (d, n, w) in worst.iter().take(15) { println!(" {n} ({w}): {d} degenerate triangles"); } }