# XBG7 β€” mesh geometry (inside XPR2 model containers) - **Confidence:** 🟑 `PROBABLE` for the single-stream layout (below); ❔ `HYPOTHESIS` / undecoded for the complex multi-stream body layout. - **Parser in:** `sylpheed-formats/src/mesh.rs` (`Xbg7Model::from_xpr2`), tests `tests/mesh_disc.rs`. Container parsing reused from `src/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 size `0x888` (=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_00` UVβ‰ˆattr0/2, `wep_04` attr4/5, `wep_03` attr1β‰ˆΒ±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 + 12` gives median `|normal| = 1.000` on every weapon model (`wep_00/02/03/04`), vs `align16` landing 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.xpr` body: data does **not** begin with indices; plain-`f32Γ—3` runs with ship-scale extent (span β‰ˆ27–34, matching bbox 30.0) found only at high offsets (`data+0x28634C`, …) β†’ multi-stream, **undecoded**.