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>
152 lines
5.8 KiB
Rust
152 lines
5.8 KiB
Rust
//! 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 <resource3d_dir>
|
|
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.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 || 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.as_chunks::<3>()
|
|
.0
|
|
.iter()
|
|
.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) {
|
|
// Degeneracy is counted over EVERY sub-mesh (grouped pools included —
|
|
// they have their own pad choice); the pad-0 comparison below only
|
|
// applies to single-block resources, whose run starts at `vb - 2n`.
|
|
for sm in &m.meshes {
|
|
let d = degenerate(&sm.indices);
|
|
if d > 0 {
|
|
still_degen += 1;
|
|
worst.push((d, m.name.clone(), where_.clone()));
|
|
}
|
|
}
|
|
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<u32> = (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<u32> = (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()));
|
|
}
|
|
}
|
|
}
|
|
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");
|
|
}
|
|
}
|