//! Is our index list one element late? A capture-free check of the `pad` choice. //! //! `anchor_pool_mesh` tries `ib = vb − idx_count*2 − pad` for `pad` in `0..=3`, //! **pad 0 first and with the looser winding gate** (`XBG7_PAD0_CONSISTENCY`, //! 0.70) than the pad ≥ 1 path (0.85). The 2026-08-13 index-byte comparison //! against the runtime showed 17 draw batches whose captured indices equal our //! decoded list shifted by exactly one element — all of them on buffers whose //! real index data sits at pad 2. A one-element shift re-wires every triangle, //! and it is invisible to every count-based metric (the count, the coverage and //! the positions are all still right). //! //! This reproduces that finding from the container alone: for each resource it //! reads the index run at pad 0 and at pad 2 and scores both on two properties a //! correct triangle list has — **no degenerate triangles** (two equal indices) //! and consistent winding against the stored normals. //! //! Usage: index_pad_check [name-substring] use sylpheed_formats::mesh::Xbg7Model; fn be16(b: &[u8], at: usize) -> u32 { ((b[at] as u32) << 8) | b[at + 1] as u32 } /// Degenerate triangles (a repeated index) and the fraction of triangles whose /// face normal agrees with their vertices' stored normals. fn score(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> (usize, f32, usize) { let mut degen = 0usize; let mut agree = 0usize; let mut counted = 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 { degen += 1; continue; } if a >= pos.len() || b >= pos.len() || c >= pos.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], ]; if nrm.len() <= a { continue; } let n = nrm[a]; let dot = f[0] * n[0] + f[1] * n[1] + f[2] * n[2]; counted += 1; if dot > 0.0 { agree += 1; } } let frac = if counted == 0 { 0.0 } else { agree as f32 / counted as f32 }; (degen, frac.max(1.0 - frac), counted) } fn main() { let a: Vec = std::env::args().collect(); let bytes = std::fs::read(&a[1]).expect("container"); let filter = a.get(2).cloned().unwrap_or_default(); let models = Xbg7Model::stage_models(&bytes); println!( "{:<22} {:>6} {:>6} {:<26} {:<26} verdict", "resource", "verts", "idx", "pad 0 (what we decode)", "pad 2 (2 bytes earlier)" ); let (mut better_p2, mut better_p0, mut tie) = (0usize, 0usize, 0usize); let mut hist_ok: Vec<(usize, String, usize)> = Vec::new(); for m in &models { if !filter.is_empty() && !m.name.contains(&filter) { continue; } if m.meshes.len() != 1 { continue; // single-block path only; grouped pools use another formula } let sm = &m.meshes[0]; let Some(vb) = sm.vbuf_offset else { continue }; let n = sm.indices.len(); if n < 6 || sm.normals.is_empty() || vb < n * 2 + 2 { continue; } let read_at = |start: usize| -> Vec { (0..n).map(|k| be16(&bytes, start + k * 2)).collect() }; let l0 = read_at(vb - n * 2); let l2 = read_at(vb - n * 2 - 2); // Sanity: l0 must be what the decoder emitted, or the assumption that we // took pad 0 is wrong for this resource and the comparison is meaningless. let took_pad0 = l0 == sm.indices; if !took_pad0 { continue; } let (d0, c0, _) = score(&l0, &sm.positions, &sm.normals); let (d2, c2, _) = score(&l2, &sm.positions, &sm.normals); let max_i0 = l0.iter().max().copied().unwrap_or(0); let max_i2 = l2.iter().max().copied().unwrap_or(0); let v = sm.positions.len() as u32; // A shifted list typically also overruns the pool by one vertex or leaves // the last vertex unreferenced, so carry the coverage as evidence too. let verdict = if d2 < d0 && c2 >= c0 { better_p2 += 1; "pad 2 is the real one" } else if d0 < d2 || c0 > c2 { better_p0 += 1; "pad 0 fine" } else { tie += 1; "indistinguishable" }; if verdict == "pad 0 fine" { hist_ok.push((d0, m.name.clone(), n)); } if verdict != "pad 0 fine" || !filter.is_empty() { println!( "{:<22} {:>6} {:>6} degen {:>5} wind {:.3} max {:<5} degen {:>5} wind {:.3} max {:<5} {}", m.name, v, n, d0, c0, max_i0, d2, c2, max_i2, verdict ); } } println!("\npad 2 wins: {better_p2} · pad 0 wins: {better_p0} · indistinguishable: {tie}"); // Is "no degenerate triangle" a safe invariant for a correctly anchored // block? Distribution over the resources where pad 0 is the better choice. let zero = hist_ok.iter().filter(|(d, _, _)| *d == 0).count(); println!( "of the {} pad-0-correct resources, {zero} have ZERO degenerate triangles", hist_ok.len() ); let mut worst: Vec<_> = hist_ok.iter().filter(|(d, _, _)| *d > 0).collect(); worst.sort_by_key(|(d, _, _)| std::cmp::Reverse(*d)); for (d, n, idx) in worst.iter().take(10) { println!(" {n}: {d} degenerate of {} triangles", idx / 3); } }