fix(xbg7): the index run was one element late for 575 sub-meshes
Extending the capture comparison from index COUNTS to index VALUES (`examples/capture_index_bytes.rs`, using the batch offsets the new ib logging gives) showed 76 of 93 Stage_S02 index runs identical to the GPU's and 17 differing — every difference a shift by exactly one element, on buffers whose index data sits at pad 2. `anchor_pool_mesh` returned the FIRST pad that validated, and pad 0 is tried first with the looser winding gate (0.70 vs 0.85). Read at pad 0, a pad-2 block yields [true[1], true[2], …, garbage]: every index in range, the pool covered, the positions right, the winding often just above 0.70 — so it validated, and every triangle was mis-wired. Nothing count-based could see it. The signature is decidable without the capture: a shifted run wires arbitrary vertices, so triangles come out degenerate. 282 of 283 correctly anchored Stage_S02 blocks have zero degenerate triangles, while the shifted readings carry 1–2 156. So score every validating pad by (degenerate triangles, then winding) and keep the best. `XBG7_PAD_FIRST_MATCH=1` restores the old behaviour. captured index runs identical: 76/93 -> 93/93 (2 025 elements) decoded runs with a degenerate triangle: 579 -> 16 (disc-wide) sub-meshes whose index run changed: 575 of 8 850 resources decoded / vertex anchors / consistency: unchanged (6 209 / same vb / 89) Locked in by tests/mesh_disc.rs::decoded_index_runs_have_almost_no_degenerate_triangles. Suite green with --include-ignored apart from the pre-existing known-failing cross-container consistency test (the 24-vertex bounding-box class). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
This commit is contained in:
@@ -1089,6 +1089,14 @@ fn pad0_consistency() -> f32 {
|
||||
std::env::var("XBG7_PAD0_CONSISTENCY").ok().and_then(|v| v.parse().ok()).unwrap_or(0.70)
|
||||
}
|
||||
|
||||
/// Revert knob for the 2026-08-13 pad scoring: with `XBG7_PAD_FIRST_MATCH=1`,
|
||||
/// [`anchor_pool_mesh`] takes the FIRST pad that validates (the pre-fix
|
||||
/// behaviour) instead of the pad whose index run is cleanest. Kept so the two
|
||||
/// behaviours can be diffed on the disc; see docs/re/structures/xbg7-mesh.md.
|
||||
fn pad_first_match() -> bool {
|
||||
std::env::var("XBG7_PAD_FIRST_MATCH").map(|v| v == "1").unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -1293,6 +1301,17 @@ fn anchor_pool_mesh(
|
||||
// pad>0 is gated at a strict 0.85 consistency to avoid a false anchor in
|
||||
// the ungated (`min_consistency == 0`) stage path — pad 0 keeps its exact
|
||||
// prior behaviour.
|
||||
// Do NOT take the first pad that validates. A list read one element late
|
||||
// still validates — every index is in range, the pool is still covered,
|
||||
// and the winding can squeak past 0.70 — but it re-wires every triangle.
|
||||
// The runtime capture caught it (17 draw batches whose captured indices
|
||||
// equal ours shifted by one, all on pad-2 buffers), and the signature is
|
||||
// decidable offline: a shift wires vertices arbitrarily, so triangles come
|
||||
// out DEGENERATE (a repeated index). 282 of 283 correctly anchored
|
||||
// `Stage_S02` blocks have zero degenerate triangles, against 1–2 156 for
|
||||
// their shifted readings. So score every validating pad and keep the
|
||||
// cleanest. See docs/re/structures/xbg7-mesh.md.
|
||||
let mut best: Option<(usize, f32, usize)> = None; // (degenerate, -winding, pad)
|
||||
for pad in 0..=3usize {
|
||||
if vb < idx_bytes + pad {
|
||||
continue;
|
||||
@@ -1310,10 +1329,20 @@ fn anchor_pool_mesh(
|
||||
min_consistency.max(0.85)
|
||||
};
|
||||
if validate_block(bytes, ib, vb, vtx_count, index_count, decl, mc, true) {
|
||||
// ── Accepted: read the full mesh. ──
|
||||
return Some(read_pool_mesh(bytes, ib, vb, index_count, vtx_count, decl));
|
||||
if pad_first_match() {
|
||||
return Some(read_pool_mesh(bytes, ib, vb, index_count, vtx_count, decl));
|
||||
}
|
||||
let (degen, wind) = index_run_quality(bytes, ib, vb, index_count, decl);
|
||||
let cand = (degen, -wind, pad);
|
||||
if best.map_or(true, |b| cand < b) {
|
||||
best = Some(cand);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((_, _, pad)) = best {
|
||||
// ── Accepted: read the full mesh. ──
|
||||
return Some(read_pool_mesh(bytes, vb - idx_bytes - pad, vb, index_count, vtx_count, decl));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -1659,6 +1688,38 @@ fn anchor_grouped_meshes(
|
||||
}
|
||||
|
||||
/// Read positions / normals / uvs / indices for an anchored stage block.
|
||||
/// How clean is the triangle list at `ib` against the pool at `vb`?
|
||||
///
|
||||
/// Returns `(degenerate triangles, winding agreement)` — the two properties that
|
||||
/// separate a correctly located index run from one read a couple of bytes off.
|
||||
/// A shifted run wires arbitrary vertices, which shows up as **degenerate**
|
||||
/// triangles (a repeated index) and a winding agreement drifting toward the 0.5
|
||||
/// middle; a real block has zero degenerate triangles and agreement ≈1 or ≈0.
|
||||
/// Used by [`anchor_pool_mesh`] to choose between pads that all validate.
|
||||
fn index_run_quality(
|
||||
bytes: &[u8],
|
||||
ib: usize,
|
||||
vb: usize,
|
||||
index_count: usize,
|
||||
decl: &VertexDecl,
|
||||
) -> (usize, f32) {
|
||||
// The pool only needs as many vertices as the run references.
|
||||
let mut max_idx = 0usize;
|
||||
for k in 0..index_count {
|
||||
let at = ib + k * 2;
|
||||
if at + 2 > bytes.len() {
|
||||
return (usize::MAX, 0.0);
|
||||
}
|
||||
max_idx = max_idx.max(be16(bytes, at) as usize);
|
||||
}
|
||||
let m = read_pool_mesh(bytes, ib, vb, index_count, max_idx + 1, decl);
|
||||
if m.normals.is_empty() {
|
||||
return (0, 1.0); // no normals: degeneracy alone decides
|
||||
}
|
||||
let (_, degen, na, _) = topology_report(&m.indices, &m.positions, &m.normals);
|
||||
(degen, na.max(1.0 - na))
|
||||
}
|
||||
|
||||
fn read_pool_mesh(
|
||||
bytes: &[u8],
|
||||
ib: usize,
|
||||
|
||||
Reference in New Issue
Block a user