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>
4.9 KiB
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 →
_colalbedo. - 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
# 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_S10e005: a coherent fighter overlaid with wrong spike triangles).e003works 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.