fix(xbg7): grouped selection prefers the candidate explaining the whole pool; per-sub-mesh decls on by default

With per-sub-mesh declarations enabled, n201_01 decoded as a 2-part fragment 4 bytes
off. Both starts validate for the pivot — 0x32BA718 at pad 2 (earlier in file order,
so first-match took it) and the capture-proven 0x32BA71C at pad 0 — so the pivot
alone cannot separate them; at the early one two of four sub-meshes fall out as
out-of-range.

anchor_grouped_meshes now builds each accepted candidate and keeps the one that
explains the most of the declared pool: it returns immediately when a candidate
explains all n sub-meshes, else keeps the best partial, so it can never decode less
than first-match did. n201_01 lands on all four capture-proven offsets
(0x32BA71C / 0x32BEFF4 / 0x32C416C / 0x32C536C) and its two sibling copies take their
own pools, so the twin collapse is gone.

XBG7_SUBMESH_DECLS is therefore on by default (=0 reverts):

  resources that never decode        85 -> 47
  resources decoding in no container 63 -> 30
  degenerate index runs               1 -> 1   (unchanged)
  cross-container minority decodes   96 -> 96  (unchanged)
  captured index runs, stage-02      93/93     (unchanged)
  captured index runs, stage-05     124/128 -> 128/128

The last line is the point: the buffers the capture could not name are the n201
family, and they now decode and match the GPU's indices byte for byte. Suite green
including twin_pairs_do_not_share_a_buffer, 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:
2026-08-13 12:27:56 +00:00
parent 4cdbbc2d48
commit 3b35c48fce
2 changed files with 115 additions and 65 deletions

View File

@@ -1603,7 +1603,7 @@ Flying stage 16 did **not** get the `e901` wings drawn (the boss appears later i
the mission), which is the next lesson: choosing the mission puts a container in
memory, but the unit still has to be **on screen** for a draw to exist.
### 🔎 The descriptor carries a declaration PER SUB-MESH — worth 38 misses, held behind a knob (2026-08-13)
### The descriptor carries a declaration PER SUB-MESH — worth 38 misses (2026-08-13)
Following the `n201` mixed-stride finding: the descriptor was dumped around every
index marker (`examples/desc_dump.rs`), and the layout is unambiguous — **each
@@ -1637,7 +1637,7 @@ And `debug_grouped_report` at `n201_01`'s **capture-proven** pool start now read
`pad 0: ACCEPTED`, where it used to read `position component NaN` — the block the
engine draws from is finally acceptable to the decoder.
**Why it is off by default.** Selection has not caught up:
**Selection had to catch up first** (it now has — see the next section):
* the three `n201_0x` copies all settle on ONE pool, so
`tests/mesh_consistency_disc.rs::twin_pairs_do_not_share_a_buffer` fails — a twin
@@ -1654,3 +1654,49 @@ So the format question is **settled** (and capture-confirmed), the coverage win
real and measured, and what stands between the two is the same
selection/distinct-assignment machinery that the pad work already improved once.
That is the next step, with `n201`'s proven offsets as the acceptance test.
### ✅ …and the selection bug it exposed: prefer the candidate that explains the WHOLE pool
With per-sub-mesh declarations on, `n201_01` decoded as a 2-part fragment **4 bytes**
off. The reason, from `debug_grouped_report` at both offsets:
```
vb0 0x32BA718 (4 bytes early) pad 2: ACCEPTED ← earlier in file order, so first-match took it
vb0 0x32BA71C (capture-proven) pad 0: ACCEPTED
```
Both validate for the **pivot** — the pivot alone cannot separate them. At the early
one, two of the four sub-meshes then fall out as out-of-range, so the resource decoded
as a fragment whose first sub-mesh sat at `0x32BEFF0` (= the early `vb0` + 777·24).
`anchor_grouped_meshes` now **builds** each accepted candidate and keeps the one that
explains the most of the declared pool, returning immediately when a candidate
explains all `n` sub-meshes and falling back to the best partial otherwise (so it can
never decode less than first-match did). The result is exact:
| `n201_01` sub-mesh | decoded vertex offset | capture-proven |
|---|---|---|
| #0 | `0x32BA71C` | `0x32BA71C` ✅ |
| #1 | `0x32BEFF4` | `0x32BEFF4` ✅ |
| #2 | `0x32C416C` | `0x32C416C` ✅ |
| #3 | `0x32C536C` | `0x32C536C` ✅ |
and `n201_02` / `n201_03` take their own distinct pools (`0x3353BEC`, `0x3388BEC`), so
the twin collapse is gone.
**Both changes are now the default** (`XBG7_SUBMESH_DECLS=0` reverts the declaration
reading):
| | before | after |
|---|---|---|
| resources that never decode | 85 | **47** |
| resources decoding in **no** container | 63 | **30** |
| decoded index runs with a degenerate triangle | 1 | **1** |
| cross-container minority decodes | 96 | 96 |
| captured index runs identical, stage-02 capture | 93/93 | **93/93** |
| captured index runs identical, stage-05 mission capture | 124/128 (4 undecoded) | **128/128** |
The last row is the one that matters most: the four buffers that capture could not
name are the `n201` family, and now they decode **and** their index runs match the
GPU byte for byte. Suite green (`twin_pairs_do_not_share_a_buffer` included) apart
from the pre-existing known-failing cross-container consistency test.