Reverse-engineer the single-stream XBG7 geometry layout (clean-room: hex
inspection + geometric validation of the retail disc's
hidden/resource3d/*.xpr, no game code copied) and present models as
textured 3D meshes in the explorer.
Format (docs/re/structures/xbg7-mesh.md): XBG7 geometry resources sit
inside XPR2 containers alongside TX2D textures. For ~25 single-stream
models (weapons, simple props) the data section is a sequence of
sub-meshes, each an u16-BE triangle-list index buffer followed (after a
fixed 12-byte header) by a stride-24 vertex buffer whose declaration is
in the descriptor: POSITION f32x3 @0x00, NORMAL f16x4 @0x0C, TEXCOORD
f16x2 @0x14. Sub-mesh (vtx,idx) counts come from descriptor tuples.
The +12 vertex offset is pinned by the recovered normals being exactly
unit-length (align16 lands 4 bytes early and silently corrupts every
field). A safety gate rejects any model whose indices are out of range
or whose mean |normal| is not ~1, declining garbage (Stage_S*
placeholders, the complex multi-stream hero-ship body) rather than
mis-decoding it.
- mesh.rs: Xbg7Model::from_xpr2 -> GameMesh { positions, normals, uvs,
indices }; standalone f16->f32; unit + real-disc tests (weapon decodes
to 215v/364t with unit normals + in-range UVs; DeltaSaber body
declined).
- texture.rs: from_xpr2_index / texture_names so the viewer can pick a
model's _col albedo map.
- viewer: loose .xpr with decodable XBG7 spawns Bevy meshes (real normals,
double-sided) textured with the albedo, framed by the orbit camera; the
central egui panel goes transparent so the 3D scene shows through.
Complex multi-stream body meshes (DeltaSaber f004, other vertex layouts)
remain undecoded and are cleanly declined — next target is dynamic RE.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.7 KiB
XBG7 — mesh geometry (inside XPR2 model containers)
- Confidence: 🟡
PROBABLEfor the single-stream layout (below); ❔HYPOTHESIS/ undecoded for the complex multi-stream body layout. - Parser in:
sylpheed-formats/src/mesh.rs(Xbg7Model::from_xpr2), teststests/mesh_disc.rs. Container parsing reused fromsrc/texture.rs(Xpr2Header/Xpr2ResourceEntry). - Applies to: ship / weapon / prop models in
hidden/resource3d/*.xpr(166 files). - Method: clean-room — static hex inspection of the retail disc + geometric validation of the recovered triangles (non-degenerate area, indices in range, bbox matches the descriptor's stored size). No game code decompiled or copied.
Where XBG7 lives
Models are ordinary XPR2 containers (see the XPR2 texture doc / texture.rs). The 16-byte
resource-directory entries (from file offset 0x10) carry TX2D texture resources and one or
more XBG7 geometry resources:
entry = [ tag:4 ][ data_offset:u32 ][ descriptor_size:u32 ][ name_offset:u32 ] (big-endian)
Offsets are relative to the directory base 0x10. The XBG7 descriptor (at data_offset+0x10,
descriptor_size bytes) is a scene / material / node graph — it holds node names
(rou_f001_mnt1_root, Light, …), a bounding value (0x41F00000 = 30.0 ≈ the ship's ~30-unit
length), material names matching the TX2D channels (_col albedo, _spc specular, _gls gloss,
_lum luminance), and per-sub-mesh records. The vertex / index buffers live in the container's
shared data section (from header_size).
Sub-mesh records (in the descriptor)
Read in file order by a sliding 4-byte scan; each is a big-endian tuple:
[ vtx_count:u32 ][ 0:u32 ][ idx_count:u32 ][ tail:u32 ]
3..=65535 ==0 mult. of 3 1..=64
(For rou_f001_wep_00: vtx_count=215, idx_count=1092 — matches the recovered geometry exactly.)
The single-stream data layout — 🟡 PROBABLE (decoded)
For ~25 of the 166 models (weapons rou_f001_wep_00/02/03/04/09/…) the data section is a straight
sequence of sub-meshes, carved from header_size in record order:
per sub-mesh:
[ index buffer : idx_count × u16 BE ] triangle list
[ 12-byte vertex-buffer header (contents undecoded) ]
[ vertex buffer : vtx_count × 24 bytes ] ← declaration below
(pad to 16 bytes → next sub-mesh)
Vertex declaration (from the descriptor; usage codes 0x00 POSITION, 0x03 NORMAL,
0x05 TEXCOORD — identical across decoded models):
+0x00 POSITION f32 × 3 BE model units
+0x0C NORMAL f16 × 4 BE (x, y, z, w); use xyz — unit length
+0x14 TEXCOORD f16 × 2 BE (u, v)
Alignment is pinned by the normals. The vertex buffer starts at index_end + 12 (a fixed
12-byte header — NOT align16, which lands 4 bytes early on most models and silently corrupts every
field). The correct offset is the unique one where the recovered normals are exactly unit-length; an
align-based guess produced the small stray-triangle artefacts seen in the first viewer pass.
Safety gate: every index is validated < vtx_count, and the mean recovered |normal| must be
≈1 (in [0.5, 2.0]). A model failing either is rejected (MeshError::UnsupportedLayout) rather than
emitting garbage — this correctly declines Stage_S* placeholders and models using a different
vertex layout.
Not yet decoded — ❔ the complex body layout
The hero-ship body meshes (DeltaSaber_A/_T/_W.xpr resource f004, and ~100 other models) use a
multi-stream layout: the data section does not start with an index buffer; the real
geometry sits at descriptor-addressed offsets deep in a multi-MB data section, as separate
position / attribute streams (a plain f32×3 position stream at stride 12 was located at a high
offset, with attributes in other streams), and positions appear partly quantized. DeltaSaber_A's
5 XBG7 blocks are f004 (body) + _rou_f004_mnv01_L/_R, _mnv02, _turn180 (maneuver / pose
variants). Decoding this requires parsing the descriptor's vertex-declaration + stream table
(offsets 0x0C/0x14 with format codes were seen but not resolved). These models are cleanly
declined today.
Evidence log
- 2026-07-12 —
rou_f001_wep_00.xpr: XPR2 dir = 1×XBG7 (f001_wep_00) + 3×TX2D (_col/_gls/_spc). Data section starts with a u16-BE index run (max 214), then stride-24 vertices. Descriptor record[215,0,1092,4]at desc+0x2B0; index-buffer byte size0x888(=2184=1092×2) at desc+0x1A0. Recovered mesh = 215 v / 364 t, 362 non-degenerate, median tri area 0.015 — coherent. → single-stream layout PROBABLE. - 2026-07-12 — descriptor-driven sequential carving over all 166 models: 42 carve under the
index-only check. Real 3D extent confirmed on
rou_f001_wep_04(bbox 3.7×1.2×3.5). - 2026-07-12 (refine) — attribute ranges differed per model (
wep_00UV≈attr0/2,wep_04attr4/5,wep_03attr1≈±60000 = garbage) → refuted the fixed "6 half attrs, UV=attr0/2" reading. Found the descriptor vertex declaration (offsets 0x0C/0x14, usages 0x03 NORMAL / 0x05 TEXCOORD). Solved the vertex-base offset with a unit-normal validator:index_end + 12gives median|normal| = 1.000on every weapon model (wep_00/02/03/04), vsalign16landing 4 bytes early. UVs then land in[0,1]. Adding the normal gate: 25 models decode clean + normal-valid (the rest — incl.Stage_S*degenerate blobs — correctly declined). - 2026-07-12 —
DeltaSaber_A.xprbody: data does not begin with indices; plain-f32×3runs with ship-scale extent (span ≈27–34, matching bbox 30.0) found only at high offsets (data+0x28634C, …) → multi-stream, undecoded.