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.9 KiB
Rust
148 lines
5.9 KiB
Rust
//! Does a resource have a CLEANER home in its container than the one we picked?
|
|
//!
|
|
//! After the pad-scoring fix, only two resources on the disc still decode to an
|
|
//! index run with degenerate triangles — `e201_bdy_03_m` (2 containers) and
|
|
//! `_rou_f402_dead` (9). Degeneracy says the run does not fit the pool, so either
|
|
//! the vertex block is wrong or the candidate list never offered the right one.
|
|
//! This walks every candidate vertex-run start for the resource's declaration and
|
|
//! scores each `(start, pad)` the way the anchor now does — degenerate triangles
|
|
//! first, then winding, plus coverage — so the answer is one of:
|
|
//! * a strictly cleaner candidate exists (the selection is at fault),
|
|
//! * several are equally clean (genuinely ambiguous), or
|
|
//! * nothing is clean (the block is not in the candidate list at all).
|
|
//!
|
|
//! Usage: better_home <container.xpr> <resource-name>
|
|
use sylpheed_formats::mesh::{debug_resource_params, debug_vertex_run_starts, Xbg7Model};
|
|
|
|
fn be16(b: &[u8], at: usize) -> u32 {
|
|
((b[at] as u32) << 8) | b[at + 1] as u32
|
|
}
|
|
|
|
fn main() {
|
|
let a: Vec<String> = std::env::args().collect();
|
|
let bytes = std::fs::read(&a[1]).expect("container");
|
|
let name = &a[2];
|
|
|
|
let Some((markers, stride)) = debug_resource_params(&bytes, name) else {
|
|
eprintln!("no such XBG7 resource: {name}");
|
|
std::process::exit(1);
|
|
};
|
|
let (vc, ic) = markers[0];
|
|
println!(
|
|
"{name}: {} marker(s), first = {vc} verts / {ic} indices, stride {stride}",
|
|
markers.len()
|
|
);
|
|
|
|
// Where did the decoder put it?
|
|
let ours = Xbg7Model::stage_models(&bytes)
|
|
.into_iter()
|
|
.find(|m| m.name == *name)
|
|
.and_then(|m| m.meshes.first().and_then(|s| s.vbuf_offset));
|
|
println!("our anchor: {ours:?}");
|
|
|
|
let starts = debug_vertex_run_starts(&bytes, stride);
|
|
println!(
|
|
"{} candidate vertex-run starts for stride {stride}",
|
|
starts.len()
|
|
);
|
|
|
|
// Score every (start, pad): degenerate triangles, winding against the stored
|
|
// normals, and whether the run covers the pool exactly.
|
|
let mut rows: Vec<(usize, f32, usize, usize, usize, bool)> = Vec::new(); // degen, wind, start, pad, max_idx, covered
|
|
for &vb in &starts {
|
|
for pad in 0..=3usize {
|
|
if vb < ic * 2 + pad {
|
|
continue;
|
|
}
|
|
let ib = vb - ic * 2 - pad;
|
|
if ib + ic * 2 > bytes.len() || vb + vc * stride > bytes.len() {
|
|
continue;
|
|
}
|
|
let idx: Vec<u32> = (0..ic).map(|k| be16(&bytes, ib + k * 2)).collect();
|
|
let max_idx = *idx.iter().max().unwrap_or(&0) as usize;
|
|
if max_idx >= vc {
|
|
continue; // out of range — not a candidate at all
|
|
}
|
|
let pos: Vec<[f32; 3]> = (0..vc)
|
|
.map(|v| {
|
|
let at = vb + v * stride;
|
|
[
|
|
f32::from_be_bytes(bytes[at..at + 4].try_into().unwrap()),
|
|
f32::from_be_bytes(bytes[at + 4..at + 8].try_into().unwrap()),
|
|
f32::from_be_bytes(bytes[at + 8..at + 12].try_into().unwrap()),
|
|
]
|
|
})
|
|
.collect();
|
|
if pos
|
|
.iter()
|
|
.any(|p| p.iter().any(|c| !c.is_finite() || c.abs() > 1e6))
|
|
{
|
|
continue;
|
|
}
|
|
let mut degen = 0usize;
|
|
let (mut agree, mut counted) = (0usize, 0usize);
|
|
for t in idx.as_chunks::<3>().0 {
|
|
let (x, y, z) = (t[0] as usize, t[1] as usize, t[2] as usize);
|
|
if x == y || y == z || x == z {
|
|
degen += 1;
|
|
continue;
|
|
}
|
|
// Winding needs normals; use the geometric centroid normal as a
|
|
// stand-in so this stays declaration-agnostic: a consistent mesh
|
|
// has all faces pointing away from the centroid on a convex-ish
|
|
// hull. Weak, so degeneracy leads the sort.
|
|
let e1 = [
|
|
pos[y][0] - pos[x][0],
|
|
pos[y][1] - pos[x][1],
|
|
pos[y][2] - pos[x][2],
|
|
];
|
|
let e2 = [
|
|
pos[z][0] - pos[x][0],
|
|
pos[z][1] - pos[x][1],
|
|
pos[z][2] - pos[x][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 cx: [f32; 3] = {
|
|
let mut c = [0.0f32; 3];
|
|
for p in &pos {
|
|
for k in 0..3 {
|
|
c[k] += p[k] / pos.len() as f32;
|
|
}
|
|
}
|
|
c
|
|
};
|
|
let out = [pos[x][0] - cx[0], pos[x][1] - cx[1], pos[x][2] - cx[2]];
|
|
counted += 1;
|
|
if f[0] * out[0] + f[1] * out[1] + f[2] * out[2] > 0.0 {
|
|
agree += 1;
|
|
}
|
|
}
|
|
let w = if counted == 0 {
|
|
0.0
|
|
} else {
|
|
agree as f32 / counted as f32
|
|
};
|
|
rows.push((degen, w.max(1.0 - w), vb, pad, max_idx, max_idx + 1 == vc));
|
|
}
|
|
}
|
|
rows.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.total_cmp(&a.1)));
|
|
println!(
|
|
"\n{} in-range candidates; best 12 by (degenerate, winding):",
|
|
rows.len()
|
|
);
|
|
for (d, w, vb, pad, mx, cov) in rows.iter().take(12) {
|
|
let mark = if Some(*vb) == ours { " <-- ours" } else { "" };
|
|
println!(
|
|
" vb 0x{vb:07X} pad {pad} degen {d:>4} wind {w:.3} max_idx {mx}/{} {}{mark}",
|
|
vc - 1,
|
|
if *cov { "covers" } else { "SHORT" }
|
|
);
|
|
}
|
|
let clean = rows.iter().filter(|r| r.0 == 0 && r.5).count();
|
|
println!("\n{clean} candidates are degenerate-free AND cover the pool exactly");
|
|
}
|