From c9c98cf942ae31e63134ea4493adde2065ad5c1c Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Wed, 12 Aug 2026 04:22:43 +0000 Subject: [PATCH] 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) --- .../examples/gate_histogram.rs | 60 ++++++++++++++++++ crates/sylpheed-formats/examples/undecoded.rs | 56 +++++++++++++++++ crates/sylpheed-formats/src/mesh.rs | 61 +++++++++++++++++++ docs/re/structures/xbg7-mesh.md | 32 ++++++++++ 4 files changed, 209 insertions(+) create mode 100644 crates/sylpheed-formats/examples/gate_histogram.rs create mode 100644 crates/sylpheed-formats/examples/undecoded.rs diff --git a/crates/sylpheed-formats/examples/gate_histogram.rs b/crates/sylpheed-formats/examples/gate_histogram.rs new file mode 100644 index 0000000..e08d258 --- /dev/null +++ b/crates/sylpheed-formats/examples/gate_histogram.rs @@ -0,0 +1,60 @@ +//! Which gate stops the resources that never decode? +//! Usage: gate_histogram [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 = 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 = 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)); + } +} diff --git a/crates/sylpheed-formats/examples/undecoded.rs b/crates/sylpheed-formats/examples/undecoded.rs new file mode 100644 index 0000000..c81f09a --- /dev/null +++ b/crates/sylpheed-formats/examples/undecoded.rs @@ -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 = 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 = 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}"); + } + } +} diff --git a/crates/sylpheed-formats/src/mesh.rs b/crates/sylpheed-formats/src/mesh.rs index a093ecf..563deec 100644 --- a/crates/sylpheed-formats/src/mesh.rs +++ b/crates/sylpheed-formats/src/mesh.rs @@ -755,6 +755,67 @@ pub fn debug_try_anchor( 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 /// 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 — diff --git a/docs/re/structures/xbg7-mesh.md b/docs/re/structures/xbg7-mesh.md index d7fe6ca..652a72d 100644 --- a/docs/re/structures/xbg7-mesh.md +++ b/docs/re/structures/xbg7-mesh.md @@ -965,6 +965,38 @@ container holds two direct copies **and** two mirrored ones (`0x33b7754`, `0x342e284`); our twins take the two direct copies, which is 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 capture gave a rule that needs no capture to apply: a `…_01`/`…_02` pair of