fix(mesh): pre-pivot sub-meshes must cover their pool, not just index in range
The last two blocks that under-covered their vertex pool were f102_break.dat and f104_break.dat in ptc_pack.xpr, each reading a neighbouring block's index buffer against the wrong declaration (414 verts indexed to 404; 160 indexed to 79). Their marker lists do not map onto the stored blocks -- only 2 of 9 and 4 of 10 sub-meshes decoded at all. Requiring coverage (max_idx + 4 >= vtx_count) for pre-pivot sub-meshes drops exactly the mismatched pieces. Every decoded sub-mesh disc-wide now covers its pool: 8580 at slack 0, 49 within tolerance, none beyond, none negative. Coverage 6069/6294, inconsistency 56, truth table 46/46 -- all unchanged. Suite green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
24
crates/sylpheed-formats/examples/submesh_dump.rs
Normal file
24
crates/sylpheed-formats/examples/submesh_dump.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//! Per-sub-mesh vertex/index/coverage dump for one resource.
|
||||||
|
//! Usage: submesh_dump <container.xpr> <resource>...
|
||||||
|
use sylpheed_formats::mesh::{debug_resource_params, Xbg7Model};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
fn main() {
|
||||||
|
let a: Vec<String> = std::env::args().collect();
|
||||||
|
let bytes = std::fs::read(&a[1]).expect("container");
|
||||||
|
let want: HashSet<String> = a[2..].iter().cloned().collect();
|
||||||
|
for m in Xbg7Model::models_named(&bytes, &want, &|| false) {
|
||||||
|
let markers = debug_resource_params(&bytes, &m.name).map(|(mk, _)| mk).unwrap_or_default();
|
||||||
|
println!("{} — {} sub-meshes decoded, {} markers declared", m.name, m.meshes.len(), markers.len());
|
||||||
|
for (i, s) in m.meshes.iter().enumerate() {
|
||||||
|
let max_idx = s.indices.iter().max().copied().unwrap_or(0) as usize;
|
||||||
|
println!(
|
||||||
|
" #{i:<2} at 0x{:<9x} verts {:<6} idx {:<6} max_idx {:<6} slack {}",
|
||||||
|
s.vbuf_offset.unwrap_or(0),
|
||||||
|
s.positions.len(),
|
||||||
|
s.indices.len(),
|
||||||
|
max_idx,
|
||||||
|
s.positions.len() as i64 - 1 - max_idx as i64
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1397,9 +1397,20 @@ fn anchor_grouped_meshes(
|
|||||||
// quality question — it is unusable. Measured 2026-08-12: 18
|
// quality question — it is unusable. Measured 2026-08-12: 18
|
||||||
// sub-meshes disc-wide carried indices up to 364 vertices past
|
// sub-meshes disc-wide carried indices up to 364 vertices past
|
||||||
// the end (`coverage_audit`), which any renderer would fault on.
|
// the end (`coverage_audit`), which any renderer would fault on.
|
||||||
let in_range =
|
// Same two structural requirements the searched path enforces:
|
||||||
(0..ic).all(|k| (be16(bytes, ib + k * 2) as usize) < vc);
|
// every index inside the buffer, and the indices reaching the
|
||||||
if in_range {
|
// end of it. Real geometry covers its pool exactly — 8 586 of
|
||||||
|
// 8 636 decoded sub-meshes reference their last vertex, none
|
||||||
|
// more than 3 short (`coverage_audit`) — so a sub-mesh whose
|
||||||
|
// indices stop well short is reading the wrong block, not a
|
||||||
|
// sparse one.
|
||||||
|
let mut max_idx = 0usize;
|
||||||
|
let in_range = (0..ic).all(|k| {
|
||||||
|
let i = be16(bytes, ib + k * 2) as usize;
|
||||||
|
max_idx = max_idx.max(i);
|
||||||
|
i < vc
|
||||||
|
});
|
||||||
|
if in_range && max_idx + 4 >= vc {
|
||||||
meshes.push(read_pool_mesh(bytes, ib, vb, ic, vc, decl));
|
meshes.push(read_pool_mesh(bytes, ib, vb, ic, vc, decl));
|
||||||
}
|
}
|
||||||
vb += vc * stride;
|
vb += vc * stride;
|
||||||
|
|||||||
@@ -995,6 +995,20 @@ Everything else holds: 6 069/6 294 decoded, cross-container inconsistency 56, th
|
|||||||
capture truth table still 46/46 claimed with 0 unclaimed, suite green. The vertex
|
capture truth table still 46/46 claimed with 0 unclaimed, suite green. The vertex
|
||||||
total falls by 1 546 — exactly the garbage that is no longer emitted.
|
total falls by 1 546 — exactly the garbage that is no longer emitted.
|
||||||
|
|
||||||
|
**The same reasoning finished the job.** The two remaining outliers were the only
|
||||||
|
decoded blocks that did not cover their pool: `f102_break.dat` had a sub-mesh
|
||||||
|
declared 414 vertices whose indices stopped at 404, and `f104_break.dat` one
|
||||||
|
declared 160 whose indices stopped at 79 — both reading a *neighbouring* block's
|
||||||
|
index buffer against the wrong declaration. These are grouped `.dat` composites
|
||||||
|
in `ptc_pack.xpr` whose marker lists (9 and 10 entries) clearly do not map 1:1
|
||||||
|
onto the stored blocks; only 2 of 9 and 4 of 10 sub-meshes ever decoded. Applying
|
||||||
|
the **coverage** requirement to pre-pivot sub-meshes as well drops exactly those
|
||||||
|
two mismatched pieces and keeps the rest.
|
||||||
|
|
||||||
|
Every decoded sub-mesh on the disc now covers its own vertex pool: **8 580 at
|
||||||
|
slack 0, 49 within the ±4 tolerance, none beyond it, none negative.** Coverage,
|
||||||
|
consistency and the capture oracle are all unchanged by the tightening.
|
||||||
|
|
||||||
### Coverage has a denominator now, and the misses have a cause breakdown
|
### Coverage has a denominator now, and the misses have a cause breakdown
|
||||||
|
|
||||||
Coverage has been quoted as "resources decoded" with no total. `examples/undecoded.rs`
|
Coverage has been quoted as "resources decoded" with no total. `examples/undecoded.rs`
|
||||||
|
|||||||
Reference in New Issue
Block a user