80 findings, not the 14 the first run showed -- clippy stops at the first failing compilation unit, so `--keep-going` is what makes the list complete. 60 were machine-applicable (`cargo clippy --fix`). The rest by hand: * five descending `sort_by` -> `sort_by_key(Reverse(..))` * `chunks_exact(4)` on both sides of four zips, so the compared items stay `[u8; 4]` rather than one array against one slice * three `type` aliases for the census maps and the captured-quad tuple * `&PathBuf` -> `&Path` in two disc tests * two range loops; one of them keeps `#[allow(needless_range_loop)]` with the reason -- the index is into a map's value, which changes each iteration * the module doc list in `invert_capture` re-indented to markdown's rules * `blit`'s eight arguments get `#[allow(too_many_arguments)]`, not a struct One dead `let off = b.len();` in a `ratc` test is dropped rather than renamed. The sibling test at :162 is the one that asserts an offset; if this one was meant to as well, that is a test change and not a lint fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
5.8 KiB
Rust
148 lines
5.8 KiB
Rust
//! 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 <container.xpr> [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.as_chunks::<3>().0 {
|
||
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<String> = 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<u32> { (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);
|
||
}
|
||
}
|