re(xbg7): a capture names a never-decoding family, and the cause is mixed strides in one pool

capture_ib_truth now proposes an identity for each drawn buffer our decoder cannot
place, by matching (vertex, index) counts against declared-but-not-decoded
resources. That identified n201_01/_02/_03 in Stage_S02.xpr — three of the 63
resources that decode in no container — and the capture pins all four sub-meshes:

  #0 vb 0x32BA71C ib 0x32B39FC 4464 idx 777 v stride 24
  #1 vb 0x32BEFF4 ib 0x32B5CDC 4464 idx 869 v stride 24
  #2 vb 0x32C416C ib 0x32B7FBC  576 idx 192 v stride 24
  #3 vb 0x32C536C ib 0x32B843C 4464 idx 869 v stride 28   <- different

The layout matches our assumptions exactly (tight index packing, last buffer flush
against vb0 so pad 0, span 27936 == align4-summed markers, contiguous vertex
buffers, max index == verts-1 everywhere). The defect is that sub-mesh #3 has a
different stride AND its own vertex shader: anchor_grouped_meshes parses one
declaration per resource and applies its stride to every sub-mesh, so it reads #3
out of phase (372 non-finite position components of 2607), and since the pivot is
the largest index count with ties going to the last marker, the pivot IS that
sub-mesh — so the whole resource is declined.

Also adds examples/miss_targets.rs (which container to aim a capture at): 63
resources decode nowhere, 58 of them in exactly one container, clustering as
Stage_S16 21 (e901_wing_05_*), ptc_pack 12, Base 6, then per-stage n2xx groups.
Flying stage 16 did not draw the e901 wings — the container is resident but the
unit must also be on screen.

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 10:52:35 +00:00
parent 92d36ec10f
commit 27577538cb
5 changed files with 179 additions and 2 deletions

View File

@@ -1550,3 +1550,55 @@ which is a static question (resource names per container × the stage's
`EnumUnit_SNN` roster), not another capture run. That is the cheap way to aim the
next capture, and the first-ever truth rows for a non-`Stage_S02` container (the
five f101 rows above) show the method works the moment the roster does.
### 🔎 ROOT CAUSE for a never-decoding family: a grouped pool can MIX STRIDES (2026-08-13)
A capture can do more than check anchors — it can name a resource we never decode.
`capture_ib_truth` now proposes an identity for every drawn buffer our decoder
cannot place, by matching the buffer's `(vertex, index)` counts against
**declared-but-not-decoded** resources. In the stage-05 and stage-16 mission
captures that fired on one buffer, and it identified a member of the
never-decoding set: **`n201_01` / `_02` / `_03`** in `Stage_S02.xpr` (three of the
63 resources that decode in no container at all).
The capture pins the whole group. Its four declared markers are
`(777,4464) (869,4464) (192,576) (869,4464)`, and the drawn offsets are
| sub | vertex buffer | index buffer | indices | verts | **stride** | shader |
|---|---|---|---|---|---|---|
| #0 | `0x32BA71C` | `0x32B39FC` | 4464 | 777 | **24** | `0xD2D7612373F4AE3D` |
| #1 | `0x32BEFF4` | `0x32B5CDC` | 4464 | 869 | **24** | `0xD742809411292720` |
| #2 | `0x32C416C` | `0x32B7FBC` | 576 | 192 | **24** | `0x57A54C86C90995F8` |
| #3 | `0x32C536C` | `0x32B843C` | 4464 | 869 | **28** | `0x4F5B7E6C2ED3460A` |
Measured against the file, the layout is **exactly what the decoder assumes**: the
index buffers pack tightly (`#0` ends where `#1` starts, …), the last one ends
*flush* at `vb0` (so pad 0, and the pool span 27 936 matches
`align4`-summed markers to the byte), the vertex buffers are contiguous, and every
sub-mesh's max index is `verts 1`. Nothing about the grouping is wrong.
**What is wrong is one stride.** Sub-mesh #3 is stride **28**, not 24 — and each
sub-mesh has its **own vertex shader**, i.e. its own declaration.
`anchor_grouped_meshes` parses a *single* declaration per resource and applies its
stride to the whole pool, so it reads #3 out of phase: dumping that pool at stride
24 gives 372 non-finite position components out of 2 607, and
`mesh::debug_grouped_report` at the capture-proven `vb0` reports exactly that —
`pad 0: position component NaN is not finite/plausible`. The pivot is chosen as the
largest index count, ties going to the last marker, which lands **on the one
sub-mesh whose stride is wrong**, so the entire resource is declined.
So this family is not a threshold problem and not an anchoring problem: **the
descriptor must carry a declaration per sub-mesh**, and reading one for all of them
is the defect. Next step is to look for those per-sub-mesh declarations in the
descriptor (the four distinct shader hashes say the game has four), then let the
grouped path use them.
**Targeting, for the record** (`examples/miss_targets.rs`): of the misses, **63
decode nowhere** and 13 miss in one container while decoding in another. The
never-decoding set clusters as `Stage_S16.xpr` 21 (all `e901_wing_05_*`),
`ptc_pack.xpr` 12 (`.DAT` composites), `Base.xpr` 6 (`g005`, `t170`, `t180`, …),
then per-stage `n2xx` station groups (S02 `n201`, S06 `n202`, S07 `n203`,
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.