feat(viewer): stage thumbnail grid, per-model albedo, camera zoom fix

Route stage containers to stage_models (decode both single + stage paths, keep
whichever yields more geometry) — fixes stages showing the dummy-root "black
cube". Present a stage's sub-models as a thumbnail grid: each recentred and
uniformly scaled to a fixed cell so a 1-unit prop and a 3000-unit structure are
equally visible; skybox-plane / stray-anchor models (>20000u) are culled. Each
sub-model is textured by matching its name to the container's `_col` albedo.

Fix the orbit camera: zoom radius was hard-capped at 50u with fixed clip planes,
trapping the camera inside multi-thousand-unit stages. Zoom limits and near/far
planes now scale to the framed model.

Adds docs/HANDOFF-stage-mesh-2026-07-12.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-12 20:56:45 +02:00
parent 2053f31d17
commit d23339a3aa
3 changed files with 375 additions and 17 deletions

View File

@@ -0,0 +1,94 @@
# 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.