fix(mesh): winding consistency replaces the connectivity heuristic

Gate attribution put connectivity behind 153 of 225 misses -- the largest
blocker, and the gate already known to reject a capture-proven block. Testing it
against winding consistency (an objective topology signal: ~1.0 or ~0.0 for a
real mesh, ~0.5 for a mis-carve) shows a floor of 0.70 with connectivity inert
dominates the previous default on both axes: 6212 resources decoded (+143) with
39 shared inconsistencies (-17), capture oracle unchanged at 46/46, twin
invariant clean, coverage invariant intact. Suite green.

The 0.80 cliff (5770 resources, 0 inconsistent) is documented, not taken --
consistency is the weaker witness, and both points are one env var apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 05:47:01 +00:00
parent ee2b98341d
commit f835705b48
3 changed files with 116 additions and 8 deletions

View File

@@ -764,6 +764,7 @@ pub fn debug_try_anchor(
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);
#[allow(clippy::type_complexity)]
let rank = |why: &str| -> usize {
if why.contains("out of range") {
1
@@ -779,11 +780,48 @@ pub fn debug_best_rejection(bytes: &[u8], name: &str) -> Option<(usize, String)>
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.
// A grouped-pool resource is placed by its PIVOT sub-mesh, so it needs the
// grouped candidate loop; running the single-block loop on `markers[0]`
// would report a gate the decoder never consulted for it.
if markers.len() > 1 {
return Some((9, format!("grouped pool ({} sub-meshes) — not analysed here", markers.len())));
let n = markers.len();
let (mut rel_ib, mut acc_i) = (Vec::with_capacity(n), 0usize);
for &(_, ic) in &markers {
rel_ib.push(acc_i);
acc_i = align4(acc_i + ic * 2);
}
let span = rel_ib[n - 1] + markers[n - 1].1 * 2;
let kmax = (0..n).max_by_key(|&i| markers[i].1).unwrap_or(0);
let (vck, ick) = markers[kmax];
let off_v: usize = markers.iter().take(kmax).map(|&(vc, _)| vc * decl.stride).sum();
let mut best = (0usize, String::from("no pool start reached any gate"));
for &vb0 in &starts {
for pad in 0..=3usize {
if vb0 < span + pad {
continue;
}
let ib0 = vb0 - span - pad;
match validate_block_report(
bytes,
ib0 + rel_ib[kmax],
vb0 + off_v,
vck,
ick,
&decl,
0.85,
true,
) {
Ok(()) => return None, // the pivot would have anchored
Err(why) => {
let r = rank(&why);
if r > best.0 {
best = (r, why);
}
}
}
}
}
return Some(best);
}
let (vtx_count, index_count) = *markers.first()?;
let idx_bytes = index_count * 2;
@@ -908,7 +946,14 @@ pub fn debug_find_index_buffer(bytes: &[u8], name: &str, vb: usize) -> Vec<(usiz
/// The connectivity cap: a searched block whose mean triangle edge exceeds this
/// fraction of its bounding-box diagonal is rejected.
///
/// **0.42 since 2026-08-12**, raised from 0.28 on runtime evidence. A capture
/// **1.0 since 2026-08-12 — effectively inert.** The winding-consistency gate
/// ([`pad0_consistency`]) replaced this as the primary structural test: it is an
/// objective topology signal rather than a shape heuristic, and swapping them
/// decodes **143 more** resources with **17 fewer** cross-container
/// inconsistencies while the capture oracle stays at 46/46. The cap is kept as a
/// knob and a backstop against absurd blocks. History below.
///
/// **0.42 from 2026-08-12**, raised from 0.28 on runtime evidence. A capture
/// names the blocks the engine really draws, and the old cap rejected one of them
/// outright — `e106_eng_02_l`, a 24-triangle LOD, measures **0.417**, because a
/// coarse mesh's edges *are* a large fraction of its own size. Swept against the
@@ -918,7 +963,31 @@ pub fn debug_find_index_buffer(bytes: &[u8], name: &str, vb: usize) -> Vec<(usiz
/// value that captures the whole measured gain. `XBG7_EDGE_CAP` overrides it (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.42)
std::env::var("XBG7_EDGE_CAP").ok().and_then(|v| v.parse().ok()).unwrap_or(1.0)
}
/// Winding-consistency floor for the pad-0 single-block anchor.
///
/// **0.70 since 2026-08-12.** A triangle's face normal should agree with its
/// vertices' stored normals almost always (≈1.0) or almost never (≈0.0, inverted
/// winding); a mis-carve wires arbitrary vertices and lands near 0.5. Gating on
/// `max(na, 1na)` is therefore an objective topology test, where the
/// connectivity cap it replaces is a shape heuristic that provably rejected a
/// capture-proven block. Measured over the disc, with connectivity inert:
///
/// | floor | resources decoded | shared inconsistent |
/// |---|---|---|
/// | 0.60 | 6 214 | 53 |
/// | **0.70** | **6 212** | **39** |
/// | 0.80 | 5 770 | 1 |
/// | 0.85 | 5 770 | 0 |
///
/// 0.70 dominates the previous connectivity-only default (6 069 / 56) on both
/// axes with the capture oracle unchanged, so it ships. The cliff at 0.80 buys
/// perfect cross-container consistency for 442 resources — recorded rather than
/// taken, since consistency is the weaker witness (see the docs).
fn pad0_consistency() -> f32 {
std::env::var("XBG7_PAD0_CONSISTENCY").ok().and_then(|v| v.parse().ok()).unwrap_or(0.70)
}
/// Triangle count below which the looser [`small_cap`] applies. `0` (default)
@@ -1081,8 +1150,14 @@ fn anchor_pool_mesh(
continue;
}
let ib = vb - idx_bytes - pad;
// `XBG7_PAD0_CONSISTENCY` adds a winding requirement to the pad-0
// path, which has none by default. Winding agreement is an objective
// topology signal (≈1.0 or ≈0.0 for a real mesh, ≈0.5 for a
// mis-carve) where the connectivity cap is a shape heuristic with a
// capture-proven false positive — so it is the candidate replacement
// for that cap. Off by default; see docs/re/structures/xbg7-mesh.md.
let mc = if pad == 0 {
min_consistency
min_consistency.max(pad0_consistency())
} else {
min_consistency.max(0.85)
};

View File

@@ -6,7 +6,7 @@ file offset vcount candidate claimed by our decode ou
0x1a0befc 2296 yes f101_bdy_03(2296) f101_bdy_03 f101_bdy_03 @ -0x0
0x1a22c1c 7694 yes f101_eng_01(7694) f101_eng_01 f101_eng_01 @ -0x0
0x1be5df4 628 yes f101_wep_01_m(628) f101_wep_01_m f101_wep_01_m @ -0x0
0x1bf40f4 261 yes e_rob_f001(24), _rou_f105_break(261), e_rou_f106(24), e_rou_f302_barrel(24), e_rou_f302_base(24), e_rou_f303_barrel(24), e_rou_f303_base(24), e_rou_e007_Far(24), e_rou_e007_Near(24), e_rou_e010_Far(24), e_rou_e010_Near(24), e_rou_e105(24), e_rou_e105_wep_01(24), e_rou_e106(24), e_rou_e106_eng(24), e_rou_e106_wep_02_01(24), e_rou_e106_wep_02_joint(24), e_rou_e108_Missile_open(24), e_rou_e201(24), e_rou_e302_barrel(24), e_rou_e302_base(24), e_rou_e303_barrel_Near(24), e_rou_e303_base_Near(24), e_rou_e501(24) f106_eng_01_l e_rob_f001 @ -0x0
0x1bf40f4 261 yes e_rob_f001(24), e_rou_f003_Near(24), e_rou_f101_wep_01(24), e_rou_f105(24), _rou_f105_break(261), e_rou_f106(24), e_rou_f302_barrel(24), e_rou_f302_base(24), e_rou_f303_barrel(24), e_rou_f303_base(24), e_rou_e007_Far(24), e_rou_e007_Near(24), e_rou_e010_Far(24), e_rou_e010_Near(24), e_rou_e105(24), e_rou_e105_wep_01(24), e_rou_e106(24), e_rou_e106_eng(24), e_rou_e106_wep_02_01(24), e_rou_e106_wep_02_joint(24), e_rou_e108_Missile_open(24), e_rou_e201(24), e_rou_e302_barrel(24), e_rou_e302_base(24), e_rou_e303_barrel_Near(24), e_rou_e303_base_Near(24), e_rou_e501(24) f106_eng_01_l e_rob_f001 @ -0x0
0x1bf596c 2446 NO _rou_f105_break(2446) f105_bdy_01_m _rou_f105_break @ -0x0
0x1c03ebc 1960 NO _rou_f105_break(1960) f105_bdy_02_m _rou_f105_break @ -0x0
0x1c0f67c 82 NO _rou_f105_break(82) e106_wep_02_01_l _rou_f105_break @ -0x0

View File

@@ -1009,6 +1009,39 @@ 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.
### ✅ Fixed: winding consistency replaces the connectivity heuristic
With every miss attributed (below), **connectivity accounted for 153 of 225**
by far the largest blocker, and the one gate already known to reject a
capture-proven block. So it was tested against the alternative the decoder
already trusts elsewhere: **winding consistency**, `max(na, 1na)`, where a real
mesh sits at ≈1.0 or ≈0.0 and a mis-carve lands near 0.5. It is an objective
topology test; the edge-ratio is a shape heuristic.
Swapping them (connectivity inert, winding gating the pad-0 path):
| winding floor | resources decoded | shared inconsistent | capture oracle |
|---|---|---|---|
| — (connectivity 0.42, previous default) | 6 069 | 56 | 46/46 |
| 0.60 | 6 214 | 53 | — |
| **0.70 (now)** | **6 212** | **39** | **46/46** |
| 0.80 | 5 770 | 1 | — |
| 0.85 | 5 770 | 0 | 46/46 |
**0.70 dominates the previous default on both axes** — 143 more resources decode
*and* 17 fewer shared resources disagree across containers — with the capture
oracle unchanged at 46/46 claimed, 0 unclaimed, the twin invariant still clean
(18 exact mirrors, 22 related, 0 collapses, 0 unrelated across 40 pairs, up from
34), and every decoded sub-mesh still covering its pool. So it ships, and the
connectivity cap drops to an inert 1.0, kept as a knob and a backstop.
The cliff at 0.80 is recorded rather than taken: it buys **perfect** cross-container
consistency (0 inconsistent) for 442 resources. Consistency is the weaker witness
— a systematic mis-anchor is consistent — so paying that much coverage for it is
not obviously right, and both points are one env var apart
(`XBG7_PAD0_CONSISTENCY`, `XBG7_EDGE_CAP`) for anyone who wants the conservative
end.
### 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`