# Handoff — XBG7 stage-container meshes, unified layout, mesh renderer (2026-07-12)

## Summary

This session extended the XBG7 mesh work from single weapons/props to the big
**stage containers**, fixed a long-standing weapon decode artifact, added a
headless mesh renderer for self-verification, and reworked the viewer's stage
presentation. Companion Xenia-Canary GPU instrumentation (the `log_draws`
draw-logger used as the ground-truth oracle) is on the `instrument-current`
branch of the Canary fork.

## What landed (Reborn — `feature/ipfb-idxd-parser`)

### 1. Stage containers decode — `Xbg7Model::stage_models`
`hidden/resource3d/Stage_S*.xpr` are **collections of up to ~450 enemy/prop
sub-models**, not single meshes. Each resource is a block
`[12-byte header][index buffer][vertex buffer]`; the index count is the
descriptor's `(index_bytes, index_count)` marker and the vertex count is the
`u32` 32 bytes before it. The blocks are **scattered among the container's
texture data with no stored offset**, so each is located by **content**: one
`O(file)` pass per stride finds vertex-buffer starts (unit NORMAL at vertex +12
whose previous stride slot isn't — a block boundary), then each resource is
pinned to the candidate where its indices validate and produce non-degenerate,
well-connected triangles. Fast (≤ ~4 s on a 70 MB stage), unambiguous.

- **Coverage: ~4993 sub-models across the 22 stages** (content-anchored).
- A **connectivity gate** (mean triangle edge ≤ 0.28× bbox diagonal) rejects
  mis-anchored blocks that would render as spiky messes.

### 2. Weapon layout fix (unified with stages)
Weapons use the **same** `[12-byte header][index][vertex]` block layout. The old
decoder read the index buffer 12 bytes too early → 2 leading degenerate
triangles (the recurring "stray white triangle" artifact) + a lost tail of 6
real indices. Now the header is skipped; vertex offsets are unchanged, so
coverage stays 36/166 and the triangle lists are correct. Verified on
wep_00/03/04 via the renderer.

### 3. Headless mesh renderer — `sylpheed-cli mesh {info,render}`
A software rasterizer (orthographic, z-buffered, two-sided shading) that writes a
PNG. Lets anyone (including the assistant) verify recovered geometry without the
GUI. `--only <substr>` filters sub-models; `--yaw/--pitch/--size/--row` control
the view. Stage containers render as a normalised thumbnail grid.

### 4. Viewer UX
- Stage files show a **thumbnail grid**: each sub-model recentred + uniformly
  scaled to a fixed cell (a 1-unit prop and a 3000-unit structure are equally
  visible), skybox-plane / stray-anchor models (>20000 u) culled.
- Stage sub-models are **textured** by matching name → `_col` albedo.
- **Camera fixed**: zoom was hard-capped at 50 u (camera trapped inside big
  stages); now zoom limits + clip planes scale to the framed model.
- Dispatch decodes both single-model and stage paths and keeps whichever yields
  more geometry (fixes stages previously showing the dummy-root "black cube").

## State / how to verify

```bash
# formats + disc tests (needs the extracted disc)
SYLPHEED_RES3D=/path/to/hidden/resource3d \
  cargo test -p sylpheed-formats --test mesh_disc -- --ignored --nocapture

# render any model / stage to a PNG
cargo run --release -p sylpheed-cli -- mesh render \
  .../resource3d/Stage_S10.xpr /tmp/s10.png --size 1000
cargo run --release -p sylpheed-cli -- mesh render \
  .../resource3d/rou_f001_wep_00.xpr /tmp/wep.png

# viewer
cargo run -p sylpheed-viewer     # File → Open Directory → the extracted disc
```

## Known limitations / next steps

- **Multi-submesh main bodies with separate vertex pools** still mis-decode
  (e.g. `Stage_S10` `e005`: a coherent fighter overlaid with wrong spike
  triangles). `e003` works only because its two submeshes *share* one pool. Same
  unsolved class as the **DeltaSaber hero body** (`f004`, still declined). Fix
  needs per-submesh vertex-base RE. The LODs of these bodies (e.g. `e005_l`)
  decode cleanly.
- **The per-resource data offset is not stored** — blocks are found by content.
  Reversing the container's allocation order would give exact offsets + 100%
  coverage but is a larger effort.
- **Texture colour correctness** remains the known "unverified" dynamic-RE item
  (channel order / sRGB) for both 2D textures and model albedos.
- The viewer's stage decode runs on the main thread (~10 s on a 70 MB stage in a
  debug build); move off-thread if the load hitch matters.

## Canary oracle (`instrument-current` on the Xenia-Canary fork)

`command_processor.cc::LogDrawForRE` (cvar `log_draws`) dumps each distinct
draw's primitive type, index buffer, vertex declaration, and sample vertex
positions to `xenia_re_draws.log`. Used to confirm the XBG7 layout (triangle
list, f32×3 position + f16×4 normal + f16×2 uv). Build with the memory-safe
flags (`CC=clang-19 CXX=clang++-19 CMAKE_BUILD_PARALLEL_LEVEL=3 … -j 3`); run one
emulator process at a time.
