feat(xbg7): read the per-sub-mesh vertex declarations (XBG7_SUBMESH_DECLS, off by default)

desc_dump shows each index marker is followed by its OWN element triples: n201_01
declares strides 24, 24, 24 and 28 (the last sub-mesh has a fourth element), which
matches the runtime capture's stride=28 on that draw and the four distinct vertex
shaders. parse_vertex_decl read the first declaration for the whole pool.

all_vertex_decls reads one per marker; anchor_grouped_meshes uses each sub-mesh's
own stride for the pool walk, the pivot validation and the read. debug_grouped_report
follows the same setting so the diagnostic cannot blame the wrong gate — at n201_01's
capture-proven pool start it now reports "pad 0: ACCEPTED" instead of a NaN position.

With XBG7_SUBMESH_DECLS=1: resources that never decode 85 -> 47, resources decoding
in no container 63 -> 30, capture oracles unchanged (93/93 index runs, 42/42 index
counts), consistency unchanged at 96.

Off by default because selection has not caught up: the three n201_0x copies then
settle on one pool (twin_pairs_do_not_share_a_buffer fails), production still picks
a start 4 bytes before sub-mesh #1 rather than the proven one even though the proven
start validates and is unclaimed, and four newly decoded ptc_pack .dat composites
carry degenerate triangles. Format truth is settled; choosing among candidates is
the remaining work.

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 11:45:20 +00:00
parent 27577538cb
commit 4cdbbc2d48
6 changed files with 272 additions and 23 deletions

View File

@@ -1602,3 +1602,55 @@ S09/S25 `n205`, S15 `n207_208`). 58 of the 63 live in exactly one container.
Flying stage 16 did **not** get the `e901` wings drawn (the boss appears later in
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)
Following the `n201` mixed-stride finding: the descriptor was dumped around every
index marker (`examples/desc_dump.rs`), and the layout is unambiguous — **each
marker is followed by its own element triples**, terminated by
`off=0x00FF0000 / code=0xFFFFFFFF`:
| `n201_01` marker | verts / indices | elements (offset, code) | stride |
|---|---|---|---|
| `@0x428` | 777 / 4464 | 0 `2A23B9`, 12 `1A2360`, 20 `182886` | 24 |
| `@0x900` | 869 / 4464 | 0 `2A23B9`, 12 `1A2360`, 20 `2C235F` | 24 |
| `@0xDD8` | 192 / 576 | 0 `2A23B9`, 12 `1A2360`, 20 `182886` | 24 |
| `@0x12B0` | 869 / 4464 | 0 `2A23B9`, 12 `1A2360`, 20 `182886`, **24 `2C235F`** | **28** |
That is exactly the capture's `stride=28` on the fourth draw, and it explains the
four distinct vertex-shader hashes. `parse_vertex_decl` read the **first**
declaration and applied it to the whole pool; `all_vertex_decls` now reads one per
marker, and `anchor_grouped_meshes` uses each sub-mesh's own stride for the vertex
pool walk, the pivot validation and the read.
**Measured with `XBG7_SUBMESH_DECLS=1`:**
| | default | per-sub-mesh declarations |
|---|---|---|
| resources that never decode (disc-wide) | 85 | **47** |
| resources that decode in **no** container | 63 | **30** |
| capture oracles (`Stage_S02`) | 93/93 runs, 42/42 index counts | **unchanged** |
| cross-container minority decodes | 96 | 96 |
| decoded index runs with a degenerate triangle | 1 | 5 |
And `debug_grouped_report` at `n201_01`'s **capture-proven** pool start now reads
`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:
* 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
collapse is the one thing this project has decided never to ship;
* production still picks a pool start 4 bytes before sub-mesh **#1** (`0x32BEFF0`)
instead of the proven `0x32BA71C`, even though that start **is** in the candidate
list, validates at pad 0, and is claimed by nobody in the final output — so the
remaining defect is in *choosing* among candidates, not in the format reading;
* four newly decoded `ptc_pack` `.dat` composites carry degenerate triangles
(`f202_break`, `e108_break`, `f106_break`, `eff_s901_e04`) — new coverage of
imperfect quality rather than a regression, but not clean either.
So the format question is **settled** (and capture-confirmed), the coverage win is
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.