fix(xbg7): degenerate index runs 582 -> 1 (grouped path + prefer a clean candidate)
Two follow-ups to the pad-scoring fix, both driven by the same invariant (a correctly located index run has no degenerate triangles): - anchor_grouped_meshes picked its pad by first-match too; scoring the pivot run the same way cleared every remaining ptc_pack composite (f102/f104/e107). - anchor_pool_mesh now prefers a degenerate-free candidate over an earlier dirty one. examples/better_home.rs showed the last two resources each had exactly one degenerate-free, pool-covering block, sitting later in file order than the lookalike we took. First-match order is kept for every clean hit, and a dirty block is still used if nothing clean exists, so coverage cannot regress. degenerate index runs, disc-wide: 582 -> 11 -> 1 captured index runs identical: 93/93 (unchanged) resources decoded / misses: 6 209 / 85 (unchanged) index runs changed / anchors moved: 590 / 10 (_rou_f402_dead x8, e201_bdy_03_m x2) Cross-container minority decodes 89 -> 96, and that is progress: all seven new rows are _rou_f402_dead, which now has a majority (32x25x8) for the first time, so its seven wrong copies are named instead of hidden behind "no majority". The last dirty run (_rou_f402_dead in Stage_S09) is blocked by distinct assignment — its clean block is claimed by e_rou_f003_Near, both 24-vertex bounding boxes. A winding-floor escalation for that case was written, measured to fire for nothing, and reverted; the reasoning is kept as a comment. Regression threshold tightened to 1. Suite green with --include-ignored apart from the pre-existing known-failing cross-container consistency test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
This commit is contained in:
@@ -1281,6 +1281,9 @@ fn anchor_pool_mesh(
|
||||
min_vb: usize,
|
||||
) -> Option<GameMesh> {
|
||||
let idx_bytes = index_count * 2;
|
||||
// First accepted candidate whose index run still has degenerate triangles —
|
||||
// used only if no clean candidate exists anywhere (see the end of the loop).
|
||||
let mut dirty: Option<(usize, usize)> = None;
|
||||
for &vb in starts {
|
||||
// Monotone assignment (opt-in): resources of one signature are laid out
|
||||
// in descriptor order, so a later one may not take an earlier block.
|
||||
@@ -1339,11 +1342,36 @@ fn anchor_pool_mesh(
|
||||
}
|
||||
}
|
||||
}
|
||||
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));
|
||||
if let Some((degen, _, pad)) = best {
|
||||
// A candidate whose triangles are degenerate-free is preferred over an
|
||||
// earlier one that is not. This keeps first-match order for every
|
||||
// clean hit (the overwhelming majority) and only searches on when the
|
||||
// first accepted block is provably mis-fitted — the two resources that
|
||||
// survived the pad fix (`e201_bdy_03_m`, `_rou_f402_dead`) each have
|
||||
// exactly ONE degenerate-free block in their container, sitting later
|
||||
// in file order than the lookalike we were taking.
|
||||
if degen == 0 || pad_first_match() {
|
||||
return Some(read_pool_mesh(bytes, vb - idx_bytes - pad, vb, index_count, vtx_count, decl));
|
||||
}
|
||||
if dirty.is_none() {
|
||||
dirty = Some((vb, pad));
|
||||
}
|
||||
}
|
||||
}
|
||||
// NOT DONE, deliberately: when every validating candidate is degenerate one
|
||||
// could re-scan without the pad-0 winding floor, since degeneracy is the
|
||||
// stronger witness. Written and measured 2026-08-13 — it fires for **nothing**
|
||||
// on the disc. The single remaining dirty resource (`_rou_f402_dead` in
|
||||
// `Stage_S09`) does have a degenerate-free block that validates at
|
||||
// `XBG7_PAD0_CONSISTENCY=0`, but it is CLAIMED by `e_rou_f003_Near`, so
|
||||
// distinct assignment — not the winding floor — is what blocks it. Both are
|
||||
// 24-vertex bounding boxes, i.e. the box-identity class that needs
|
||||
// descriptor-level data rather than another anchoring heuristic (see the docs).
|
||||
// Nothing clean anywhere: keep the first accepted block, so this can never
|
||||
// cost coverage relative to first-match.
|
||||
if let Some((vb, pad)) = dirty {
|
||||
return Some(read_pool_mesh(bytes, vb - idx_bytes - pad, vb, index_count, vtx_count, decl));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1617,6 +1645,12 @@ fn anchor_grouped_meshes(
|
||||
if taken.contains(&vb0) {
|
||||
continue;
|
||||
}
|
||||
// Which pad? Not the first that validates — the same trap the searched
|
||||
// path had until 2026-08-13: a pool read one index element late still
|
||||
// validates but wires every triangle wrongly. Score the pivot's index run
|
||||
// (degenerate triangles first, then winding) and keep the cleanest pad.
|
||||
// `XBG7_PAD_FIRST_MATCH=1` restores first-match here too.
|
||||
let mut best: Option<(usize, f32, usize)> = None;
|
||||
for pad in 0..=3usize {
|
||||
if vb0 < span + pad {
|
||||
continue;
|
||||
@@ -1634,6 +1668,17 @@ fn anchor_grouped_meshes(
|
||||
if !validate_block(bytes, ib_k, vb_k, vck, ick, decl, grouped_consistency(), true) {
|
||||
continue;
|
||||
}
|
||||
let (degen, wind) = index_run_quality(bytes, ib_k, vb_k, ick, decl);
|
||||
let cand = (degen, -wind, pad);
|
||||
if best.map_or(true, |b| cand < b) {
|
||||
best = Some(cand);
|
||||
}
|
||||
if pad_first_match() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some((_, _, pad)) = best {
|
||||
let ib0 = vb0 - span - pad;
|
||||
|
||||
// Pivot confirmed the exact alignment ⇒ every marker up to the pivot
|
||||
// is correctly placed; read those unconditionally (a legitimately
|
||||
@@ -1687,7 +1732,6 @@ fn anchor_grouped_meshes(
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -1720,6 +1764,7 @@ fn index_run_quality(
|
||||
(degen, na.max(1.0 - na))
|
||||
}
|
||||
|
||||
/// Read positions / normals / uvs / indices for an anchored stage block.
|
||||
fn read_pool_mesh(
|
||||
bytes: &[u8],
|
||||
ib: usize,
|
||||
|
||||
Reference in New Issue
Block a user