# 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, GPU-cross-checked) For 36 of the 166 models (weapons, simple props) 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 (prim=4, GPU-confirmed) [ 12-byte vertex-buffer header (contents undecoded) ] [ vertex buffer : vtx_count Γ— stride bytes ] ← declaration-driven (pad to 16 bytes β†’ next sub-mesh) ``` **Vertex declaration.** The layout is **not fixed-stride**. The descriptor holds a declaration table (right after the `(index_bytes, index_count)` marker) of `{offset:u32, format-code:u32, usage<<16:u32}` big-endian triples, terminated by `offset == 0x00FF0000` / `code == 0xFFFFFFFF`: | usage | element | format code | format | size | |-------|----------|-------------|--------|------| | 0x00 | POSITION | `0x2A23B9` | f32Γ—3 | 12 B | | 0x03 | NORMAL | `0x1A2360` | f16Γ—4 (use xyz) | 8 B | | 0x05 | TEXCOORD | `0x2C235F` | f16Γ—2 (u,v) | 4 B | Stride = max element extent. Models **omit elements** β†’ variable stride (20 = pos+normal, no UV; 24 = pos+normal+uv). Each element is read in **naive big-endian component order** (the raw file bytes; see the endianness note below). Assuming a fixed stride-24 was why the old decoder mis-aligned and declined the pos+normal-only models. **Alignment 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). The correct offset is the unique one where recovered normals are exactly unit-length. **Safety gate:** every index is validated `< vtx_count`, and when the declaration has a normal element the mean recovered `|normal|` must be β‰ˆ1 (`[0.5, 2.0]`). A model failing either is rejected (`MeshError::UnsupportedLayout`) rather than emitting garbage. ### Endianness β€” file bytes are naive-BE, `k8in32` is a red herring βœ… The Canary GPU capture reports every vertex stream with fetch `endian = 2` (`k8in32`, each 32-bit word byte-reversed). This describes the **guest-memory** copy the GPU fetches β€” **not** the `.xpr` file bytes. Reading the file with a `k8in32` transform breaks the normals (mean `|normal|` β†’ 1.33); the plain per-element big-endian read yields exactly unit normals. So the game **rearranges** the vertex data between the on-disc `.xpr` and the uploaded buffer; the decoder reads the file directly and must use naive BE. ## Not yet decoded β€” ❔ the complex body layout The hero-ship *body* meshes (`DeltaSaber_A/_T/_W.xpr` resource `f004`, and ~100 other models) still decline. Two open sub-problems: (1) **multi-sub-mesh models** whose *first* sub-mesh decodes but a later one's inter-mesh offset isn't yet handled (the `align16` advance is a guess) β€” these are declined whole; (2) the big body meshes, where the data section does not start with an index buffer and the geometry sits at descriptor-addressed offsets. NB the GPU capture showed **every** rendered mesh is *single-stream* (just wider strides, e.g. 44 bytes = pos + f32Γ—3 + colour + 2Γ—f16Γ—4), so the body is likely single-stream-with-a-richer-declaration rather than the "separate streams" first guessed β€” it was simply not rendered in the captured session (menu only). A capture taken *in a mission* (where `DeltaSaber` renders) would hand over its exact declaration directly. `DeltaSaber_A`'s 5 XBG7 blocks are `f004` (body) + `_rou_f004_mnv01_L/_R`, `_mnv02`, `_turn180` (maneuver / pose). ## 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 (DYNAMIC) β€” added a cvar-gated draw logger to Canary (`command_processor.cc::LogDrawForRE`, cvar `log_draws`), captured the Ready Room / Briefings. **GPU ground truth confirmed the static layout exactly**: primitive `prim=4` = **triangle LIST** (settles the list-vs-strip question), and a stream with `f32x3 @offset0` + `f16x4 @3dw` + `f16x2 @5dw`, stride 6 dwords = 24 bytes β€” matching `POSITION@0, NORMAL@0x0C, TEXCOORD@0x14`. Revealed **stride varies** (24, 20, 28, 44 …) and that all streams are **single-stream** β†’ parsed the declaration for variable stride: coverage **25 β†’ 36** (e.g. `wep_05` is pos+normal, stride 20, no UV β€” previously mis-aligned). Also confirmed the endianness note above: fetch `endian=2` (k8in32) is the *guest* copy; file stays naive-BE. `Stage_S*` now decode (stride 20). - 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**.