re: calibrate the connectivity cap -- +612 resources against 18 consistency regressions
XBG7_EDGE_CAP / XBG7_SMALL_TRIS make the threshold sweepable (defaults unchanged, full suite green). Above 0.417 the capture-proven eng_02_l anchors exactly right and no e106 part regresses, and nothing that decoded at 0.28 is lost -- but ~250 existing anchors move silently and 18 shared resources lose cross-container consistency. Not changed: the movers have no oracle yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
80
crates/sylpheed-formats/examples/edge_cap_sweep.rs
Normal file
80
crates/sylpheed-formats/examples/edge_cap_sweep.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
//! Calibrate the connectivity cap against the whole disc.
|
||||||
|
//!
|
||||||
|
//! `XBG7_EDGE_CAP` sets the cap; this reports, for one setting, how much
|
||||||
|
//! geometry decodes and how self-consistent it is across containers — the two
|
||||||
|
//! numbers any change to the cap has to trade off. Run it once per cap value.
|
||||||
|
use sylpheed_formats::mesh::Xbg7Model;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// name -> (verts, tris) -> spans seen, exactly as mesh_consistency_disc.rs.
|
||||||
|
let mut seen: BTreeMap<String, Vec<([i64; 3], usize, usize)>> = BTreeMap::new();
|
||||||
|
let (mut models, mut verts) = (0usize, 0usize);
|
||||||
|
for f in &files {
|
||||||
|
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||||
|
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||||
|
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||||
|
for s in &m.meshes {
|
||||||
|
for q in &s.positions {
|
||||||
|
for k in 0..3 {
|
||||||
|
lo[k] = lo[k].min(q[k]);
|
||||||
|
hi[k] = hi[k].max(q[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lo[0] == f32::MAX {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let v: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
|
||||||
|
let t: usize = m.meshes.iter().map(|s| s.indices.len() / 3).sum();
|
||||||
|
models += 1;
|
||||||
|
verts += v;
|
||||||
|
if std::env::var("DUMP").is_ok() {
|
||||||
|
// Per-resource signature, so two cap settings can be diffed:
|
||||||
|
// a cap change that silently MOVES an existing anchor is the
|
||||||
|
// risk a coverage count cannot see.
|
||||||
|
println!(
|
||||||
|
"{}|{}|{v}|{t}|{}|{}|{}|{}",
|
||||||
|
f.file_name().unwrap().to_string_lossy(),
|
||||||
|
m.name,
|
||||||
|
m.meshes[0].vbuf_offset.unwrap_or(0),
|
||||||
|
(hi[0] - lo[0]).round() as i64,
|
||||||
|
(hi[1] - lo[1]).round() as i64,
|
||||||
|
(hi[2] - lo[2]).round() as i64
|
||||||
|
);
|
||||||
|
}
|
||||||
|
seen.entry(m.name.clone()).or_default().push((
|
||||||
|
[
|
||||||
|
(hi[0] - lo[0]).round() as i64,
|
||||||
|
(hi[1] - lo[1]).round() as i64,
|
||||||
|
(hi[2] - lo[2]).round() as i64,
|
||||||
|
],
|
||||||
|
v,
|
||||||
|
t,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (mut shared, mut inconsistent) = (0usize, 0usize);
|
||||||
|
for (_, list) in &seen {
|
||||||
|
if list.len() < 2 || !list.iter().all(|e| e.1 == list[0].1 && e.2 == list[0].2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
shared += 1;
|
||||||
|
if list.iter().any(|e| e.0 != list[0].0) {
|
||||||
|
inconsistent += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"cap={} models={models} verts={verts} shared={shared} inconsistent={inconsistent}",
|
||||||
|
std::env::var("XBG7_EDGE_CAP").unwrap_or_else(|_| "0.28 (default)".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -708,6 +708,27 @@ pub fn debug_find_index_buffer(bytes: &[u8], name: &str, vb: usize) -> Vec<(usiz
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The connectivity cap: a searched block whose mean triangle edge exceeds this
|
||||||
|
/// fraction of its bounding-box diagonal is rejected. `0.28` is the shipped
|
||||||
|
/// value. A runtime capture names blocks the engine really draws, and one of them
|
||||||
|
/// (`e106_eng_02_l`, a 24-triangle LOD) measures **0.417** — so the cap has a
|
||||||
|
/// known false positive. `XBG7_EDGE_CAP` overrides it for calibration sweeps;
|
||||||
|
/// it is a diagnostic knob, not a setting (see docs/re/structures/xbg7-mesh.md).
|
||||||
|
fn edge_cap() -> f32 {
|
||||||
|
std::env::var("XBG7_EDGE_CAP").ok().and_then(|v| v.parse().ok()).unwrap_or(0.28)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triangle count below which the looser [`small_cap`] applies. `0` (default)
|
||||||
|
/// disables the split, so the flat [`edge_cap`] governs every block.
|
||||||
|
fn small_tris() -> usize {
|
||||||
|
std::env::var("XBG7_SMALL_TRIS").ok().and_then(|v| v.parse().ok()).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The connectivity cap for blocks below [`small_tris`] triangles.
|
||||||
|
fn small_cap() -> f32 {
|
||||||
|
std::env::var("XBG7_EDGE_CAP_SMALL").ok().and_then(|v| v.parse().ok()).unwrap_or(0.45)
|
||||||
|
}
|
||||||
|
|
||||||
/// Internal: the descriptor parameters the diagnostics need.
|
/// Internal: the descriptor parameters the diagnostics need.
|
||||||
fn decl_of(bytes: &[u8], name: &str) -> Option<(VertexDecl, Vec<(usize, usize)>)> {
|
fn decl_of(bytes: &[u8], name: &str) -> Option<(VertexDecl, Vec<(usize, usize)>)> {
|
||||||
if bytes.len() < 16 || &bytes[..4] != b"XPR2" {
|
if bytes.len() < 16 || &bytes[..4] != b"XPR2" {
|
||||||
@@ -987,7 +1008,13 @@ fn validate_block(
|
|||||||
.sqrt()
|
.sqrt()
|
||||||
.max(1e-6);
|
.max(1e-6);
|
||||||
let mean_edge = edge_sum / (sampled as f32 * 3.0);
|
let mean_edge = edge_sum / (sampled as f32 * 3.0);
|
||||||
if mean_edge / diag > 0.28 {
|
// A COARSE block is coarse by construction: a 24-triangle LOD's edges
|
||||||
|
// are a large fraction of its own size, which is why the flat cap has a
|
||||||
|
// capture-proven false positive (`e106_eng_02_l`, ratio 0.417). Under
|
||||||
|
// `XBG7_SMALL_TRIS` blocks below that triangle count get the looser
|
||||||
|
// `XBG7_EDGE_CAP_SMALL` instead — a targeted relaxation, off by default.
|
||||||
|
let cap = if tris < small_tris() { small_cap() } else { edge_cap() };
|
||||||
|
if mean_edge / diag > cap {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -748,6 +748,39 @@ fix to reach for blind: the threshold trades against false anchors, and now that
|
|||||||
a capture can name true blocks, it can be **calibrated** against them rather than
|
a capture can name true blocks, it can be **calibrated** against them rather than
|
||||||
guessed. Not changed here.
|
guessed. Not changed here.
|
||||||
|
|
||||||
|
**7. Calibrating the cap: a real trade, not a free win.** `XBG7_EDGE_CAP`
|
||||||
|
(and `XBG7_SMALL_TRIS` / `XBG7_EDGE_CAP_SMALL` for a triangle-count-aware
|
||||||
|
variant) make the threshold sweepable without changing the default;
|
||||||
|
`examples/edge_cap_sweep.rs` reports coverage and cross-container consistency per
|
||||||
|
setting. Over the whole `resource3d` directory:
|
||||||
|
|
||||||
|
| cap | resources decoded | anchors moved vs 0.28 | shared | inconsistent | consistent → **inconsistent** | inconsistent → consistent |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **0.28** (shipped) | 5 480 | — | 678 | 125 | — | — |
|
||||||
|
| 0.35 | 5 955 | — | 707 | 140 | — | — |
|
||||||
|
| 0.42 | 6 069 | 254 | 714 | 153 | **18** | 3 |
|
||||||
|
| 0.45 | 6 093 | 254 | 716 | 153 | 18 | 3 |
|
||||||
|
| 0.28, but 0.45 below 64 tris | 6 090 | 236 | 716 | 151 | **16** | 3 |
|
||||||
|
|
||||||
|
**Nothing is ever lost** — every resource that decoded at 0.28 still decodes —
|
||||||
|
and the capture-proven case is fixed: at any cap above 0.417, `e106_eng_02_l`
|
||||||
|
anchors at exactly `0x44a32c`, and the four e106 parts that were already correct
|
||||||
|
stay correct. So on the only ground truth available, relaxing is a strict
|
||||||
|
improvement (4 → 5 of the ship's drawn buffers correct).
|
||||||
|
|
||||||
|
But it is bought: ~590–610 resources that previously decoded not at all now do,
|
||||||
|
**~240–254 existing anchors silently move**, and 16–18 shared resources go from
|
||||||
|
cross-container consistent to inconsistent (against 3 repaired). The
|
||||||
|
triangle-aware variant barely narrows that — almost everything the looser cap
|
||||||
|
admits is a small block anyway.
|
||||||
|
|
||||||
|
**So the cap is not changed here.** The evidence says 0.28 is too tight and that
|
||||||
|
mean-edge-over-diagonal is a weak discriminator for coarse LODs; it does not say
|
||||||
|
0.45 is right, because the 254 movers have no oracle. Deciding needs either a
|
||||||
|
capture covering more ships and stages (the same `--truth` method extends to any
|
||||||
|
container the engine drew from) or a discriminator that does not degrade for
|
||||||
|
coarse geometry. Both are recorded as the next step rather than guessed at.
|
||||||
|
|
||||||
Not settled: `e106_brg_01_b_02` ≡ `e106_brg_01_l` (51 verts). A second 51-vertex
|
Not settled: `e106_brg_01_b_02` ≡ `e106_brg_01_l` (51 verts). A second 51-vertex
|
||||||
`vbase` exists in the logs but is **not** from this container, and the container
|
`vbase` exists in the logs but is **not** from this container, and the container
|
||||||
holds three near-identical 51-vertex runs, so the pair has no oracle yet.
|
holds three near-identical 51-vertex runs, so the pair has no oracle yet.
|
||||||
|
|||||||
Reference in New Issue
Block a user