re: coverage is 96.4% (6069/6294) -- and the 225 misses get a gate breakdown
undecoded.rs supplies the denominator the coverage numbers never had; the disc holds 6294 XBG7 resources, 6069 decode, 225 are searched and missed, 0 lack a descriptor. gate_histogram.rs attributes each miss to the furthest gate its best candidate reached: 120 connectivity, 74 grouped-pool (different path), 15 degenerate/extent, 9 winding, 7 buffer-not-covered. Recorded as a work-list, not a verdict -- a wrong candidate can pass more gates than the true block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
60
crates/sylpheed-formats/examples/gate_histogram.rs
Normal file
60
crates/sylpheed-formats/examples/gate_histogram.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
//! Which gate stops the resources that never decode?
|
||||||
|
//! Usage: gate_histogram <resource3d_dir> [max_resources]
|
||||||
|
use sylpheed_formats::mesh::{debug_best_rejection, xbg7_resource_names, Xbg7Model};
|
||||||
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||||
|
let cap: usize = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(usize::MAX);
|
||||||
|
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 hist: BTreeMap<usize, (usize, String)> = BTreeMap::new();
|
||||||
|
let mut done = 0usize;
|
||||||
|
for f in &files {
|
||||||
|
if done >= cap {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||||
|
let names = xbg7_resource_names(&bytes);
|
||||||
|
if names.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let got: HashSet<String> = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false)
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| m.name)
|
||||||
|
.collect();
|
||||||
|
for n in names.iter().filter(|n| !got.contains(*n)) {
|
||||||
|
if done >= cap {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
done += 1;
|
||||||
|
if let Some((rank, why)) = debug_best_rejection(&bytes, n) {
|
||||||
|
let e = hist.entry(rank).or_insert((0, String::new()));
|
||||||
|
e.0 += 1;
|
||||||
|
if e.1.is_empty() {
|
||||||
|
e.1 = format!("{n}: {why}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("furthest gate reached, over {done} resources that never decode:");
|
||||||
|
let label = |r: usize| match r {
|
||||||
|
0 => "no gate reached",
|
||||||
|
1 => "index out of range",
|
||||||
|
2 => "buffer not covered by indices",
|
||||||
|
3 => "degenerate / implausible positions",
|
||||||
|
4 => "connectivity (mean edge / diagonal)",
|
||||||
|
5 => "winding consistency",
|
||||||
|
9 => "grouped pool (different path)",
|
||||||
|
_ => "?",
|
||||||
|
};
|
||||||
|
for (r, (n, ex)) in &hist {
|
||||||
|
println!(" {:<38} {n:>5} e.g. {ex}", label(*r));
|
||||||
|
}
|
||||||
|
}
|
||||||
56
crates/sylpheed-formats/examples/undecoded.rs
Normal file
56
crates/sylpheed-formats/examples/undecoded.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
//! Which XBG7 resources never decode, and how big is that population?
|
||||||
|
//!
|
||||||
|
//! Coverage has been reported as "resources decoded" without a denominator. This
|
||||||
|
//! prints both, per container and in total, and names the misses so the gate
|
||||||
|
//! attribution (`why_rejected`) has a work list.
|
||||||
|
use sylpheed_formats::mesh::{debug_resource_params, xbg7_resource_names, Xbg7Model};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||||
|
let show = std::env::args().nth(2).is_some();
|
||||||
|
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 decoded, mut no_decl) = (0usize, 0usize, 0usize);
|
||||||
|
let mut misses: Vec<String> = Vec::new();
|
||||||
|
for f in &files {
|
||||||
|
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||||
|
let names = xbg7_resource_names(&bytes);
|
||||||
|
if names.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let got: HashSet<String> = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false)
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| m.name)
|
||||||
|
.collect();
|
||||||
|
for n in &names {
|
||||||
|
total += 1;
|
||||||
|
if got.contains(n) {
|
||||||
|
decoded += 1;
|
||||||
|
} else if debug_resource_params(&bytes, n).is_none() {
|
||||||
|
// No vertex declaration / no index markers: the anchor scan
|
||||||
|
// never even considers these, so they are a different question
|
||||||
|
// from "searched and not found".
|
||||||
|
no_decl += 1;
|
||||||
|
} else {
|
||||||
|
misses.push(format!("{}|{n}", f.file_name().unwrap().to_string_lossy()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"XBG7 resources: {total} total, {decoded} decoded ({:.1}%), {no_decl} without a usable descriptor, {} searched-and-missed",
|
||||||
|
100.0 * decoded as f64 / total as f64,
|
||||||
|
misses.len()
|
||||||
|
);
|
||||||
|
if show {
|
||||||
|
for m in &misses {
|
||||||
|
println!("{m}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -755,6 +755,67 @@ pub fn debug_try_anchor(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How far did the anchor scan get for a resource it failed to place?
|
||||||
|
///
|
||||||
|
/// Runs the same candidate loop the decoder runs and keeps the **furthest**
|
||||||
|
/// rejection — the candidate that passed the most gates before failing. Over the
|
||||||
|
/// resources that never decode, the distribution of these says which gate to
|
||||||
|
/// work on, instead of tuning one threshold and re-measuring.
|
||||||
|
pub fn debug_best_rejection(bytes: &[u8], name: &str) -> Option<(usize, String)> {
|
||||||
|
let (decl, markers) = decl_of(bytes, name)?;
|
||||||
|
let starts = debug_vertex_run_starts(bytes, decl.stride);
|
||||||
|
let rank = |why: &str| -> usize {
|
||||||
|
if why.contains("out of range") {
|
||||||
|
1
|
||||||
|
} else if why.contains("buffer not covered") {
|
||||||
|
2
|
||||||
|
} else if why.contains("not finite") || why.contains("degenerate") {
|
||||||
|
3
|
||||||
|
} else if why.contains("connectivity") {
|
||||||
|
4
|
||||||
|
} else if why.contains("winding") {
|
||||||
|
5
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Only the single-block path is modelled here. A grouped-pool resource is
|
||||||
|
// placed by its pivot, so running the single-block loop on `markers[0]`
|
||||||
|
// would report a gate the decoder never consulted.
|
||||||
|
if markers.len() > 1 {
|
||||||
|
return Some((9, format!("grouped pool ({} sub-meshes) — not analysed here", markers.len())));
|
||||||
|
}
|
||||||
|
let (vtx_count, index_count) = *markers.first()?;
|
||||||
|
let idx_bytes = index_count * 2;
|
||||||
|
let mut best = (0usize, String::from("no candidate reached any gate"));
|
||||||
|
for &vb in &starts {
|
||||||
|
for pad in 0..=3usize {
|
||||||
|
if vb < idx_bytes + pad {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mc = if pad == 0 { 0.0 } else { 0.85 };
|
||||||
|
if let Err(why) = validate_block_report(
|
||||||
|
bytes,
|
||||||
|
vb - idx_bytes - pad,
|
||||||
|
vb,
|
||||||
|
vtx_count,
|
||||||
|
index_count,
|
||||||
|
&decl,
|
||||||
|
mc,
|
||||||
|
true,
|
||||||
|
) {
|
||||||
|
let r = rank(&why);
|
||||||
|
if r > best.0 {
|
||||||
|
best = (r, why);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return None; // it would have decoded — not a miss
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(best)
|
||||||
|
}
|
||||||
|
|
||||||
/// Diagnostic: why does the decoder refuse a grouped-pool resource at a given
|
/// Diagnostic: why does the decoder refuse a grouped-pool resource at a given
|
||||||
/// pool start? Recomputes the pool layout exactly as [`anchor_grouped_meshes`]
|
/// pool start? Recomputes the pool layout exactly as [`anchor_grouped_meshes`]
|
||||||
/// does and reports the pivot sub-mesh's verdict for each index/vertex pad —
|
/// does and reports the pivot sub-mesh's verdict for each index/vertex pad —
|
||||||
|
|||||||
@@ -965,6 +965,38 @@ container holds two direct copies **and** two mirrored ones
|
|||||||
(`0x33b7754`, `0x342e284`); our twins take the two direct copies, which is
|
(`0x33b7754`, `0x342e284`); our twins take the two direct copies, which is
|
||||||
self-consistent but unverified — `n206` appears in no captured stage.
|
self-consistent but unverified — `n206` appears in no captured stage.
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
supplies both by enumerating the XBG7 directory of every container:
|
||||||
|
|
||||||
|
**6 294 XBG7 resources on the disc — 6 069 decode (96.4 %), 225 are searched and
|
||||||
|
missed, 0 lack a usable descriptor.**
|
||||||
|
|
||||||
|
`examples/gate_histogram.rs` then asks, for each miss, **which gate the best
|
||||||
|
candidate reached** before being rejected (`mesh::debug_best_rejection`):
|
||||||
|
|
||||||
|
| furthest gate reached | count | example |
|
||||||
|
|---|---|---|
|
||||||
|
| connectivity (mean edge / diagonal) | **120** | `g004`: 0.724 > cap 0.42 |
|
||||||
|
| grouped pool — placed by a different path, not analysed here | 74 | `t170` (2 sub-meshes) |
|
||||||
|
| degenerate / implausible positions | 15 | `g005`: extent 0.346 (min 0.5), 7/8 degenerate |
|
||||||
|
| winding consistency | 9 | `e007_bdy_01_l`: 0.667 < 0.85 |
|
||||||
|
| buffer not covered by indices | 7 | `e101_bdy_02_d`: indices reach 13 171 of 15 430 |
|
||||||
|
|
||||||
|
⚠️ **Read this as a work-list, not a verdict.** "Furthest gate reached" is taken
|
||||||
|
over *all* candidates, and a wrong candidate can pass more gates than the true
|
||||||
|
block — so this says where to look, not what is broken. What it does establish is
|
||||||
|
that after the cap move to 0.42, **connectivity is still the single largest
|
||||||
|
blocker** (53 % of single-block misses), and that a third of the misses are
|
||||||
|
grouped-pool resources that need the pivot path analysed on its own terms.
|
||||||
|
|
||||||
|
The two smallest buckets are the interesting ones for a fix that cannot go wrong:
|
||||||
|
`extent < 0.5` rejects genuinely tiny props (`g005` spans 0.346), and "buffer not
|
||||||
|
covered" fires when the index buffer addresses only part of a large vertex pool —
|
||||||
|
which is exactly what a **sub-range draw** looks like, and the capture's
|
||||||
|
`indices=` field already showed the engine issuing those.
|
||||||
|
|
||||||
### The twin invariant, checked disc-wide (2026-08-12)
|
### The twin invariant, checked disc-wide (2026-08-12)
|
||||||
|
|
||||||
The capture gave a rule that needs no capture to apply: a `…_01`/`…_02` pair of
|
The capture gave a rule that needs no capture to apply: a `…_01`/`…_02` pair of
|
||||||
|
|||||||
Reference in New Issue
Block a user