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:
2026-08-13 07:26:52 +00:00
parent 6d92e6c114
commit 41b59faf5f
9 changed files with 655 additions and 3 deletions

View File

@@ -1396,3 +1396,55 @@ vs our 246)"*. Not a disagreement: `21` was the first of two batches, and
Mixing the index range into the de-dup key (same commit) makes every batch
appear. **Any conclusion drawn from a pre-2026-08-13 capture's `indices=` value,
or from `vbase ibase`, is about one batch and not about the block.**
### ✅ FIXED, and it is the biggest silent defect found so far: the index run was one element late (2026-08-13)
Comparing captured index VALUES (not just counts) against our decode turned the
layout check above into a byte-level oracle — `examples/capture_index_bytes.rs`
lines each draw batch up against `GameMesh::indices` at the batch's own offset.
Result on `Stage_S02`: **76 of 93 index runs identical, 17 differing — and every
difference was a shift by exactly one element**, on buffers whose real index data
sits at **pad 2**.
Cause: `anchor_pool_mesh` took the **first** pad that validated, and pad 0 is
tried first *with the looser gate* (`XBG7_PAD0_CONSISTENCY` 0.70, against 0.85 for
pad ≥ 1). For a pad-2 block, reading at pad 0 yields `[true[1], true[2], …,
garbage]` — every index still in range, the pool still covered, the positions
untouched, and the winding often just above 0.70. So it validated, and every
triangle came out mis-wired.
**The signature is decidable offline** (`examples/index_pad_check.rs`): a shifted
run wires arbitrary vertices, so triangles come out **degenerate** (a repeated
index). In `Stage_S02`, 32 resources read at pad 0 with 12 156 degenerate
triangles and winding 0.630.76, while the same blocks at pad 2 give **zero**
degenerate triangles and winding 0.981.00. And degeneracy is near-perfectly
clean as an invariant: **282 of 283** correctly anchored blocks in that container
have zero degenerate triangles.
**Fix:** score every validating pad by `(degenerate triangles, then winding)` and
keep the best, instead of returning the first. Revert knob
`XBG7_PAD_FIRST_MATCH=1` restores the old behaviour, which is how the before/after
below was measured.
| measurement | first-match (old) | scored (new) |
|---|---|---|
| captured index runs identical to ours (`Stage_S02`, 2 025 elements) | 76 / 93 | **93 / 93** |
| decoded sub-meshes whose index run holds a degenerate triangle (disc-wide) | **579** | **16** |
| sub-meshes whose index run changed | — | **575** of 8 850 |
| resources decoded · vertex anchors · cross-container minority decodes | 6 209 · — · 89 | **unchanged** (6 209 · identical `vb` · 89) |
So 575 sub-meshes — 6.5 % of the disc's geometry — were being decoded with
mis-wired triangles under a completely correct-looking decode: right resource,
right buffer, right vertex count, right coverage. **No count-based metric could
see it**; only the captured index values, and then the degeneracy signature they
pointed at. Locked in by
`tests/mesh_disc.rs::decoded_index_runs_have_almost_no_degenerate_triangles`.
Honest limits: of the moved runs, **92** had a pad-0 reading with no degenerate
triangle and moved on the winding tie-break alone (83 such cases existed before
the fix, so the tie-break newly decides 9) — weaker evidence than the degeneracy
signature, and unverified by the capture. The **16** remaining degenerate runs are
the grouped `.dat` break composites in `ptc_pack` (`f102`/`f104`/`e107`, whose
marker lists are documented not to map onto the stored blocks), `e201_bdy_03_m`
(2 containers) and `_rou_f402_dead` (9) — each already suspect on other grounds,
and now the concrete next targets.