//! XBG7 mesh geometry decoder (geometry resources inside XPR2 containers). //! //! ## Clean-room note //! //! This format was reverse-engineered **purely by static observation of the //! retail disc's `hidden/resource3d/*.xpr` files** (hex inspection + geometric //! validation of the recovered triangles). No game code was decompiled or //! copied. See `docs/re/structures/xbg7-mesh.md` for the evidence log. //! //! ## Where XBG7 lives //! //! Ship / weapon / prop models are `XPR2` containers (see [`crate::texture`]). //! Their resource directory holds `TX2D` texture resources **and** one or more //! `XBG7` geometry resources. The `XBG7` *descriptor* (at the resource's //! `data_offset`) is a scene/material graph; the actual vertex and index //! buffers live in the container's shared data section (from `header_size`). //! //! ## The "simple" layout decoded here (CONFIRMED) //! //! For single-stream models (weapons, simple props — 36 of the 166 disc models) //! the data section is a straight sequence of sub-meshes, each: //! //! ```text //! [ 12-byte vertex-buffer header (contents undecoded) ] //! [ index buffer : idx_count × u16 big-endian ] triangle LIST //! [ vertex buffer : vtx_count × stride bytes ] (declaration-driven) //! (pad to 16 bytes → next sub-mesh) //! ``` //! //! The index buffer is a triangle **list**: `idx_count` is a multiple of 3 and //! each consecutive triple is one triangle. This is confirmed two ways — the //! Canary GPU draw capture logs `prim=4` (triangle list), and the objective //! `XVERIFY` diagnostic shows stored-normal agreement of **1.000** under the list //! reading (every face normal points the way its vertices' normals do — only //! possible with correct topology + winding) versus **~0.49** (random) under a //! strip reading. A brief 2026-07 attempt to read these as triangle strips was a //! mistake: it over-generated ~2.5× the triangles, filling holes with an //! overlapping garbage soup that *looked* solid but had random normals. //! //! Correctness is enforced by a **winding-consistency gate**: a correctly-carved //! sub-mesh agrees with its stored normals either ≈always (≈1.0) or ≈never (≈0.0, //! inverted winding — still a real single-sided mesh); a mis-carve wires arbitrary //! vertices and scatters to the ≈0.5 middle. Sub-meshes with `max(na, 1-na) < //! 0.90` are declined rather than emitted as a spike-mess. //! //! The vertex layout is **not fixed** — it comes from a **vertex declaration** //! in the descriptor: a table of `{offset, format-code, usage}` triples (usage //! `0x00` POSITION `f32×3`, `0x03` NORMAL `f16×4`, `0x05` TEXCOORD `f16×2`). //! Models omit UV or use fewer elements, so stride varies (20 = pos+normal, //! 24 = pos+normal+uv, …). Each element is read in naive big-endian component //! order. Correct alignment is pinned by the recovered normals being exactly //! unit-length. **Cross-checked against a Canary GPU vertex-fetch capture**, //! which confirmed the primitive type (triangle list), formats, and offsets — //! see `docs/re/structures/xbg7-mesh.md`. //! //! `vtx_count` / `idx_count` come from per-sub-mesh records in the descriptor: //! a `[vtx_count:u32][0:u32][idx_count:u32][tail:u32]` tuple (big-endian), read //! in file order. Every index is validated to be `< vtx_count`; if any //! sub-mesh fails to carve cleanly the whole model is rejected //! ([`MeshError::UnsupportedLayout`]) rather than emitting garbage. //! //! ## The grouped-pool layout (CONFIRMED — hero ships) //! //! Detailed models (`DeltaSaber_*.xpr` and ~100 others) don't interleave each //! sub-mesh's index buffer with its vertex buffer. Instead a resource's *several* //! sub-meshes share **two grouped pools**: //! //! ```text //! index pool : [ ib0 | ib1 | … ] each buffer 4-byte aligned, in marker order //! vertex pool : [ vb0 | vb1 | … ] each pool `vtx_count × stride`, same order //! ``` //! //! with the index pool ending exactly where the vertex pool begins. This is the //! **same vertex format** as weapons (stride 24, triangle list) — it was declined //! only for *location*, not format. [`Xbg7Model::anchor_models`] decodes it: it //! pivots on the one unknown, `vb0` (found by the unit-normal vertex-run scan), //! and derives every other index/vertex offset (see [`anchor_grouped_meshes`]). //! Reversed statically and cross-checked against a Canary GPU draw-log capture of //! `DeltaSaber_T.xpr` (`f001` = body + 7 detail parts = 8650 tris, every sub-mesh //! at 0 degenerate / full coverage). See `docs/re/structures/xbg7-mesh.md`. //! //! Resources whose grouped pivot cannot be validated (a few multi-stream / //! quantized bodies remain) are still cleanly declined rather than emitted as //! garbage. use crate::texture::{Xpr2Header, Xpr2ResourceEntry}; use binrw::BinRead; use std::io::Cursor; use thiserror::Error; #[derive(Debug, Error)] pub enum MeshError { #[error("Not an XPR2 container")] NotXpr2, #[error("No XBG7 geometry resource in container")] NoGeometry, #[error("Mesh layout not supported (multi-stream / quantized body mesh)")] UnsupportedLayout, #[error("Parse error: {0}")] Parse(#[from] binrw::Error), } /// A decoded 3D mesh ready for Bevy. #[derive(Debug, Default, Clone)] pub struct GameMesh { /// Vertex positions in model space `[x, y, z]`. pub positions: Vec<[f32; 3]>, /// Vertex normals `[nx, ny, nz]` — empty when not stored (compute smooth /// normals from geometry instead). pub normals: Vec<[f32; 3]>, /// Texture coordinates `[u, v]`. **Best-guess channel** (attr halves 0 & 2) /// pending in-game visual confirmation — see the module doc. pub uvs: Vec<[f32; 2]>, /// Triangle-list indices (3 per triangle). pub indices: Vec, /// Sub-mesh / node name from the descriptor, when available. pub name: Option, } /// A model = the set of sub-meshes recovered from one XPR2 container's first /// XBG7 resource, plus the resource's name. #[derive(Debug, Default, Clone)] pub struct Xbg7Model { pub name: String, pub meshes: Vec, } /// Count the `XBG7` geometry resources in an XPR2 container. Single-model files /// (weapons / props) have exactly one; the multi-resource `Stage_*.xpr` /// collections have many. Used to route decoding: one → the validated /// records-based list decode ([`Xbg7Model::from_xpr2`]); many → the /// content-anchored [`Xbg7Model::stage_models`]. (Content-anchoring a /// single-model file fabricates phantom / duplicate blocks — see `decode`.) pub fn count_xbg7(bytes: &[u8]) -> usize { if bytes.len() < 16 || &bytes[..4] != b"XPR2" { return 0; } let mut cur = Cursor::new(bytes); let header = match Xpr2Header::read(&mut cur) { Ok(h) => h, Err(_) => return 0, }; let mut n = 0usize; for _ in 0..header.num_resources { match Xpr2ResourceEntry::read(&mut cur) { Ok(e) if &e.type_tag == b"XBG7" => n += 1, Ok(_) => {} Err(_) => break, } } n } impl Xbg7Model { /// Total vertex / triangle counts across all sub-meshes. pub fn totals(&self) -> (usize, usize) { let v = self.meshes.iter().map(|m| m.positions.len()).sum(); let t = self.meshes.iter().map(|m| m.indices.len() / 3).sum(); (v, t) } /// Decode the geometry of the first XBG7 resource in an XPR2 container. /// /// Returns [`MeshError::UnsupportedLayout`] for models whose data section /// does not carve cleanly under the simple single-stream layout (the /// complex body meshes) — never partial / garbage geometry. pub fn from_xpr2(bytes: &[u8]) -> Result { if bytes.len() < 16 || &bytes[..4] != b"XPR2" { return Err(MeshError::NotXpr2); } let mut cur = Cursor::new(bytes); let header = Xpr2Header::read(&mut cur)?; let mut xbg: Option = None; for _ in 0..header.num_resources { let e = Xpr2ResourceEntry::read(&mut cur)?; if &e.type_tag == b"XBG7" { xbg = Some(e); break; } } let xbg = xbg.ok_or(MeshError::NoGeometry)?; const DIR_BASE: usize = 0x10; let desc = xbg.data_offset as usize + DIR_BASE; let desc_end = (desc + xbg.descriptor_size as usize).min(bytes.len()); if desc >= bytes.len() { return Err(MeshError::UnsupportedLayout); } // Resource name (for labelling). let name = read_cstr(bytes, xbg.name_offset as usize + DIR_BASE) .unwrap_or_else(|| "XBG7".to_string()); // Parse the vertex declaration (element offsets/formats + stride). The // XBG7 layout is NOT fixed-stride — models omit UV or use fewer elements // (stride 20 = pos+normal, stride 24 = pos+normal+uv, …). Confirmed // against a Canary GPU vertex-fetch capture (see docs/re/xbg7-mesh.md). let decl = parse_vertex_decl(&bytes[desc..desc_end]) .ok_or(MeshError::UnsupportedLayout)?; // Extract the ordered list of sub-mesh (vtx_count, idx_count) records. let records = submesh_records(&bytes[desc..desc_end]); if records.is_empty() { return Err(MeshError::UnsupportedLayout); } if std::env::var("XMESHDBG").is_ok() { let d = &bytes[desc..desc_end]; eprintln!("[{name}] stride={} records={records:?}", decl.stride); let mut rel = 0usize; while rel + 8 <= d.len() { let (a, c) = (be32(d, rel), be32(d, rel + 4)); if c >= 3 && c % 3 == 0 && c < 400_000 && a == c * 2 { eprintln!(" idx-marker @0x{rel:03x}: idx_count={c}"); } rel += 4; } } // Carve the data section sequentially. let base = header.header_size as usize; let mut off = 0usize; // relative to `base` let mut meshes = Vec::new(); for (vtx_count, idx_count) in records { // Each sub-mesh block is `[12-byte header][index buffer][vertex // buffer]` — the SAME layout as stage resources (see // `stage_models`). The header must be skipped: reading indices from // the block start instead treats the 12 header bytes as 6 junk // indices (2 leading degenerate triangles — the stray-triangle // artifact) and drops the last 6 real indices. Skipping it leaves the // vertex buffer at the identical offset (`+12 + idx_bytes`), so // coverage is unchanged; only the triangle list is corrected. let ib = base + off + VERTEX_BUFFER_GAP; let ie = ib + idx_count * 2; let vb = ie; let ve = vb + vtx_count * decl.stride; if ve > bytes.len() { return Err(MeshError::UnsupportedLayout); } // The index buffer is a triangle LIST (u16 BE): `idx_count` is a // multiple of 3, and each consecutive triple is one triangle. This is // the format the game actually draws (Canary GPU capture: prim=4 = // triangle list), and the objective proof is `XVERIFY`: reading it as // a list gives stored-normal agreement 1.000 (every face normal points // the way its vertices' normals do — only possible with correct // topology + winding), whereas a strip reading gives ~0.49 (random). let mut indices = Vec::with_capacity(idx_count); for k in 0..idx_count { let i = be16(bytes, ib + k * 2) as u32; if i >= vtx_count as u32 { return Err(MeshError::UnsupportedLayout); } indices.push(i); } // Vertices per the declaration. The .xpr stores each element in // naive big-endian component order (f32 / f16 read at consecutive // offsets) — the GPU's `k8in32` fetch endianness applies to the // rearranged guest-memory copy, not to these file bytes. let mut positions = Vec::with_capacity(vtx_count); let mut normals = Vec::with_capacity(vtx_count); let mut uvs = Vec::with_capacity(vtx_count); let mut normal_len_sum = 0.0f32; for v in 0..vtx_count { let o = vb + v * decl.stride; // POSITION: f32×3 big-endian. let p = o + decl.pos_offset; let x = bef(bytes, p); let y = bef(bytes, p + 4); let z = bef(bytes, p + 8); if !(x.is_finite() && y.is_finite() && z.is_finite()) { return Err(MeshError::UnsupportedLayout); } positions.push([x, y, z]); // NORMAL: f16×4 (use xyz). if let Some(no) = decl.normal_offset { let nb = o + no; let nx = half(bytes, nb); let ny = half(bytes, nb + 2); let nz = half(bytes, nb + 4); normal_len_sum += (nx * nx + ny * ny + nz * nz).sqrt(); normals.push([nx, ny, nz]); } // TEXCOORD: f16×2. if let Some(uo) = decl.uv_offset { let ub = o + uo; uvs.push([half(bytes, ub), half(bytes, ub + 2)]); } } // Sanity gate: when the declaration has a normal element, a correctly // aligned vertex buffer yields unit-length normals. A mean far from 1 // means the layout does not fit (wrong stride / offset) — decline // rather than emit garbage. if decl.normal_offset.is_some() { let mean = normal_len_sum / vtx_count.max(1) as f32; if !(0.5..=2.0).contains(&mean) { return Err(MeshError::UnsupportedLayout); } } // ── Decisive list-vs-strip diagnostic (env XVERIFY) ────────────── // For each sub-mesh compute, under BOTH interpretations, two // topology-correctness metrics that do NOT depend on which is "nicer // looking": stored-normal agreement (a triangle's cross-product face // normal should point the same way as its vertices' stored normals — // correct topology ⇒ ~all agree; wrong topology wires arbitrary // vertices ⇒ ~50%) and edge-manifoldness (a closed surface shares // almost every edge between exactly 2 triangles). if std::env::var("XVERIFY").is_ok() { let (lt, ld, ln, le) = topology_report(&indices, &positions, &normals); let stripped = expand_triangle_strip(&indices); let (st, sd, sn, se) = topology_report(&stripped, &positions, &normals); eprintln!( " submesh v={vtx_count} idx={idx_count}\n\ \x20 LIST : {lt:>5} tris, degen {ld:>4}, normal-agree {ln:.3}, edge2 {le:.3}\n\ \x20 STRIP: {st:>5} tris, degen {sd:>4}, normal-agree {sn:.3}, edge2 {se:.3}" ); } // Objective quality gate on winding CONSISTENCY. Each triangle's // cross-product face normal is compared to its vertices' stored // normals; `na` is the fraction that agree. A correctly-carved, // consistently-wound mesh sits at either ≈1.0 (winding matches the // normals) OR ≈0.0 (winding is inverted relative to them — still a // real, single-sided mesh, just authored the other way). A mis-carve // wires arbitrary vertices, so agreement collapses to the ≈0.5 middle. // Gate on `max(na, 1-na)` so both consistent windings pass and only // the random middle is declined (e.g. `rou_f001_wep_23` na=0.398 → // consistency 0.602 → declined; the clean weapons are all 1.000). if !normals.is_empty() { let (_, _, na, _) = topology_report(&indices, &positions, &normals); if na.max(1.0 - na) < 0.90 { return Err(MeshError::UnsupportedLayout); } } meshes.push(GameMesh { positions, normals, uvs, indices, name: None, }); off = align16(ve) - base; } Ok(Xbg7Model { name, meshes }) } /// Decode **every** locatable XBG7 geometry resource in a container. /// /// "Stage" containers (`hidden/resource3d/Stage_*.xpr`) are collections of /// many enemy / prop sub-models, each an independent XBG7 resource. Unlike /// the single-stream weapon layout (index buffer immediately followed by its /// vertex buffer), a stage's index buffers and vertex buffers live in /// **separate grouped pools**, and the container stores each resource's /// buffer *sizes* (index count via the marker, vertex count 32 bytes before /// it) but **not** an explicit data offset — the on-disc block layout is a /// separate allocation order we have not reversed. /// /// Rather than guess that order, each resource's `[index buffer][vertex /// buffer]` block is located by **content**: the unique offset in the data /// section where (a) all `index_count` indices are `< vertex_count`, (b) the /// stored normals are unit length, and (c) the resulting triangles are /// non-degenerate with a real spatial extent. This signature is strong /// enough to pin a block unambiguously in a multi-megabyte file. Resources /// that cannot be located and validated this way are **skipped** (never /// emitted as garbage) — including the biggest hero bodies, which use the /// same quantized/complex layout that [`Xbg7Model::from_xpr2`] declines. /// /// Returns one [`Xbg7Model`] per decoded resource (empty if none decode). /// /// Multi-resource stage files use `min_consistency = 0.0` (no winding gate): /// the stage corpus is large and its enemy meshes span a continuous /// consistency range (0.5–1.0), so a hard gate would drop many legitimate /// blocks. The single-model **weapon fallback** ([`decode` in the CLI]) calls /// [`Xbg7Model::anchor_models`] with a strict `0.85`, where a mis-anchor is an /// obvious phantom / spike-mess that must be declined. pub fn stage_models(bytes: &[u8]) -> Vec { Self::anchor_models(bytes, 0.0) } /// Content-anchor every XBG7 resource, rejecting any block whose winding /// consistency (`max(na, 1-na)`) is below `min_consistency`. See /// [`Xbg7Model::stage_models`]. pub fn anchor_models(bytes: &[u8], min_consistency: f32) -> Vec { let mut out = Vec::new(); if bytes.len() < 16 || &bytes[..4] != b"XPR2" { return out; } let mut cur = Cursor::new(bytes); let header = match Xpr2Header::read(&mut cur) { Ok(h) => h, Err(_) => return out, }; let data_base = header.header_size as usize; if data_base >= bytes.len() { return out; } // ── Collect every XBG7 resource's parameters up front. ── const DIR_BASE: usize = 0x10; struct Res { name: String, /// `(vtx_count, idx_count)` for every sub-mesh, in file order. One /// entry → the simple adjacency layout ([`anchor_pool_mesh`]); several /// → the grouped-pool layout ([`anchor_grouped_meshes`]). markers: Vec<(usize, usize)>, decl: VertexDecl, } let mut resources: Vec = Vec::new(); for _ in 0..header.num_resources { let e = match Xpr2ResourceEntry::read(&mut cur) { Ok(e) => e, Err(_) => break, }; if &e.type_tag != b"XBG7" { continue; } let desc = e.data_offset as usize + DIR_BASE; let desc_end = (desc + e.descriptor_size as usize).min(bytes.len()); if desc >= bytes.len() || desc_end <= desc { continue; } let d = &bytes[desc..desc_end]; let markers = all_index_markers(d); if markers.is_empty() { continue; } let decl = match parse_vertex_decl(d) { Some(v) => v, None => continue, }; // Anchoring relies on the unit-normal signature; skip resources with // no NORMAL element (too ambiguous to pin safely). if decl.normal_offset.is_none() { continue; } let name = read_cstr(bytes, e.name_offset as usize + DIR_BASE) .unwrap_or_else(|| "XBG7".to_string()); resources.push(Res { name, markers, decl, }); } if resources.is_empty() { return out; } // ── One O(file) pass per distinct stride: find vertex-block *starts*. ── // // Each geometry block is `[12B header][index buffer][vertex buffer]`, and // the blocks are scattered among texture data with no stored offset. But // a vertex buffer is a run of stride-sized records whose NORMAL (f16×4 at // +12) is unit length; a *block start* is the unique offset where that // run begins — the previous stride slot is NOT a unit-normal vertex (it's // index bytes / header). Collecting those starts turns the per-resource // search from O(file) into a scan of a few hundred candidates. let mut strides: Vec = resources.iter().map(|r| r.decl.stride).collect(); strides.sort_unstable(); strides.dedup(); let mut starts_by_stride: std::collections::BTreeMap> = std::collections::BTreeMap::new(); for &s in &strides { starts_by_stride.insert(s, vertex_run_starts(bytes, data_base, s)); } for r in &resources { let starts = &starts_by_stride[&r.decl.stride]; let meshes = if r.markers.len() == 1 { // Single sub-mesh → the proven per-block adjacency anchor // (index buffer immediately before its vertex buffer). Stages and // simple props take this path; `min_consistency` behaviour is // exactly as before. let (vtx_count, index_count) = r.markers[0]; anchor_pool_mesh(bytes, starts, index_count, vtx_count, &r.decl, min_consistency) .into_iter() .collect() } else { // Several sub-meshes sharing grouped index/vertex pools → the // deterministic grouped-pool decode (hero ships et al.). let grouped = anchor_grouped_meshes(bytes, data_base, starts, &r.markers, &r.decl); if !grouped.is_empty() { grouped } else { // The grouped pivot didn't validate (the extra markers were a // coincidence, or this isn't a grouped-pool model). Fall back // to the original single-block adjacency anchor on the first // marker so coverage is never *below* the pre-grouped decode. let (vtx_count, index_count) = r.markers[0]; anchor_pool_mesh(bytes, starts, index_count, vtx_count, &r.decl, min_consistency) .into_iter() .collect() } }; if !meshes.is_empty() { out.push(Xbg7Model { name: r.name.clone(), meshes, }); } } out } } /// Scan the data section for offsets that begin a `stride`-sized unit-normal /// vertex run (NORMAL is `f16×4` at vertex offset +12). A run *start* is an /// offset whose normal is unit while the preceding stride slot's is not — i.e. /// the first vertex of a buffer, not a mid-buffer position. Returns the sorted /// candidate starts (block vertex-buffer offsets). fn vertex_run_starts(bytes: &[u8], data_base: usize, stride: usize) -> Vec { const NRM: usize = 12; // POSITION f32×3 occupies [0,12); NORMAL f16×4 follows let mut starts = Vec::new(); if stride < NRM + 8 { return starts; } let is_unit = |o: usize| -> bool { if o + NRM + 6 > bytes.len() { return false; } let nx = half(bytes, o + NRM); let ny = half(bytes, o + NRM + 2); let nz = half(bytes, o + NRM + 4); let l = (nx * nx + ny * ny + nz * nz).sqrt(); (0.85..=1.15).contains(&l) }; // Vertex buffers begin on 4-byte boundaries in practice; step 4. let end = bytes.len().saturating_sub(NRM + 6); let mut o = data_base; while o <= end { if is_unit(o) && (o < data_base + stride || !is_unit(o - stride)) { starts.push(o); } o += 4; } starts } /// Locate a stage resource's `[index buffer][vertex buffer]` block among the /// precomputed vertex-run `starts` (see [`vertex_run_starts`]) and decode it, or /// return `None` if no candidate validates. A candidate `vb` is accepted when /// the `index_count` indices ending just before it are all `< vtx_count`, /// reference (nearly) all vertices, and produce non-degenerate triangles with a /// real spatial extent — a signature strong enough to pin the block. fn anchor_pool_mesh( bytes: &[u8], starts: &[usize], index_count: usize, vtx_count: usize, decl: &VertexDecl, min_consistency: f32, ) -> Option { let idx_bytes = index_count * 2; for &vb in starts { // The index buffer sits just before the vertex buffer, which is 4-byte // aligned — so 0..=3 bytes of padding may separate them (`ib = vb − // idx_bytes − pad`). pad 0 is the immediate-adjacency case (all stages so // far); some weapons need pad 2. A shifted pad reads garbled indices, so // pad>0 is gated at a strict 0.85 consistency to avoid a false anchor in // the ungated (`min_consistency == 0`) stage path — pad 0 keeps its exact // prior behaviour. for pad in 0..=3usize { if vb < idx_bytes + pad { continue; } let ib = vb - idx_bytes - pad; let mc = if pad == 0 { min_consistency } else { min_consistency.max(0.85) }; if validate_block(bytes, ib, vb, vtx_count, index_count, decl, mc, true) { // ── Accepted: read the full mesh. ── return Some(read_pool_mesh(bytes, ib, vb, index_count, vtx_count, decl)); } } } None } /// Check whether the `index_count` big-endian `u16` indices at `ib`, read against /// the `vtx_count`-vertex pool at `vb`, form a coherent triangle-list sub-mesh: /// every index in range, ~all vertices referenced, non-degenerate triangles with /// a real spatial extent, connected edges, and (when `min_consistency > 0`) a /// winding consistent with the stored normals. This is the shared acceptance test /// for both the adjacency anchor ([`anchor_pool_mesh`]) and the grouped-pool /// anchor ([`anchor_grouped_meshes`]); the two differ only in how they *place* /// `ib`/`vb`, not in how they validate a placement. fn validate_block( bytes: &[u8], ib: usize, vb: usize, vtx_count: usize, index_count: usize, decl: &VertexDecl, min_consistency: f32, strict_connectivity: bool, ) -> bool { let stride = decl.stride; let idx_bytes = index_count * 2; if ib + idx_bytes > bytes.len() { return false; } let vtx_bytes = match vtx_count.checked_mul(stride) { Some(v) => v, None => return false, }; if vb + vtx_bytes > bytes.len() { return false; } // ── Full index validation: every index in range, uses ~all vertices. ── let mut max_idx = 0u32; for k in 0..index_count { let i = be16(bytes, ib + k * 2) as u32; if i >= vtx_count as u32 { return false; } max_idx = max_idx.max(i); } if (max_idx as usize) + 4 < vtx_count { return false; } // ── Triangle quality: finite, non-degenerate, real spatial extent. ── let pos = decl.pos_offset; let mut lo = [f32::MAX; 3]; let mut hi = [f32::MIN; 3]; let mut degenerate = 0usize; let mut sampled = 0usize; let mut edge_sum = 0.0f32; let mut nrm_agree = 0usize; let mut nrm_counted = 0usize; let tris = index_count / 3; let tstep = (tris / 96).max(1); let mut t = 0; while t < tris { let mut p = [[0.0f32; 3]; 3]; let mut tri_idx = [0usize; 3]; for (c, pc) in p.iter_mut().enumerate() { let vi = be16(bytes, ib + (3 * t + c) * 2) as usize; tri_idx[c] = vi; let base = vb + vi * stride + pos; for (a, slot) in pc.iter_mut().enumerate() { let x = bef(bytes, base + a * 4); if !x.is_finite() || x.abs() > 1.0e6 { return false; } *slot = x; lo[a] = lo[a].min(x); hi[a] = hi[a].max(x); } } let u = [p[1][0] - p[0][0], p[1][1] - p[0][1], p[1][2] - p[0][2]]; let w = [p[2][0] - p[0][0], p[2][1] - p[0][1], p[2][2] - p[0][2]]; let cx = [ u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0], ]; if 0.5 * (cx[0] * cx[0] + cx[1] * cx[1] + cx[2] * cx[2]).sqrt() < 1.0e-9 { degenerate += 1; } else if let Some(no) = decl.normal_offset { // Stored-normal agreement: the face normal should point the way // the three vertices' stored normals do. A wrong anchor wires // arbitrary vertices → this collapses toward 50%. let mut sn = [0.0f32; 3]; for &vi in &tri_idx { let nb = vb + vi * stride + no; sn[0] += half(bytes, nb); sn[1] += half(bytes, nb + 2); sn[2] += half(bytes, nb + 4); } if cx[0] * sn[0] + cx[1] * sn[1] + cx[2] * sn[2] > 0.0 { nrm_agree += 1; } nrm_counted += 1; } // Sum the triangle's three edge lengths (for the connectivity check). let e3 = [p[2][0] - p[1][0], p[2][1] - p[1][1], p[2][2] - p[1][2]]; edge_sum += (u[0] * u[0] + u[1] * u[1] + u[2] * u[2]).sqrt() + (w[0] * w[0] + w[1] * w[1] + w[2] * w[2]).sqrt() + (e3[0] * e3[0] + e3[1] * e3[1] + e3[2] * e3[2]).sqrt(); sampled += 1; t += tstep; } let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(hi[2] - lo[2]); if extent < 0.5 || sampled == 0 || degenerate * 10 > sampled * 3 { return false; // too flat, or >30% degenerate → not this block } // Connectivity check: a correctly-anchored mesh has triangle edges that // are SMALL relative to its overall size (~0.05–0.15 of the bbox // diagonal). A wrong anchor / cross-wired index buffer connects distant // vertices, so its mean edge spans a large fraction of the model (a spiky // mess). Reject those. Only applied when the placement was *searched* // ([`anchor_pool_mesh`] / grouped-pool pivot): a small flat sub-mesh (a fin, // an antenna) legitimately has large edges relative to its own diagonal, so // the check is skipped for *derived* grouped-pool parts, whose in-range + // consistency signature already pins them unambiguously. if strict_connectivity { let diag = ((hi[0] - lo[0]).powi(2) + (hi[1] - lo[1]).powi(2) + (hi[2] - lo[2]).powi(2)) .sqrt() .max(1e-6); let mean_edge = edge_sum / (sampled as f32 * 3.0); if mean_edge / diag > 0.28 { return false; } } // Winding-consistency gate (same as `from_xpr2`): a correctly-anchored // block is internally consistent — face normals agree with stored normals // either almost always (≈1.0) or almost never (≈0.0, inverted winding); // a false anchor / cross-wired buffer scatters to the ≈0.5 middle. Gate on // `max(na,1-na)` so both real windings pass (stage meshes use both) and // only the random middle is rejected. 0.85 tolerates small-block sampling. if nrm_counted > 0 && min_consistency > 0.0 { let na = nrm_agree as f32 / nrm_counted as f32; if na.max(1.0 - na) < min_consistency { return false; } } true } /// Decode a **grouped-pool** XBG7 resource: one whose descriptor holds *several* /// index markers (sub-meshes), all sharing one contiguous index pool and one /// contiguous vertex pool. This is the hero-ship / detailed-model layout /// (`DeltaSaber_*.xpr` and ~100 others) that the per-block adjacency anchor /// ([`anchor_pool_mesh`]) cannot decode, because a sub-mesh's index buffer is /// **not** immediately before its vertex buffer. /// /// The layout, reversed from the retail disc + cross-checked against a Canary GPU /// draw-log capture of `DeltaSaber_T.xpr` (see `docs/re/structures/xbg7-mesh.md`): /// /// ```text /// index pool : [ ib0 | ib1 | … ] each buffer 4-byte aligned, in marker order /// vertex pool : [ vb0 | vb1 | … ] each pool `vtx_count × stride` bytes, same order /// ``` /// /// and crucially **the index pool ends exactly where the vertex pool begins**. /// So the whole resource pivots on a single unknown — the first vertex pool start /// `vb0` (= index-pool end). Everything else is derived: /// `ib0 = vb0 − span`, `ib[i] = align4(ib[i-1] + idx_count[i-1]·2)`, /// `vb[i] = vb[i-1] + vtx_count[i-1]·stride`. `vb0` is found by scanning the /// unit-normal vertex-run `starts` and accepting the first for which sub-mesh 0 /// validates ([`validate_block`], strict 0.85 consistency — the pivot must be /// unambiguous). Each derived sub-mesh is validated too; the chain stops at the /// first that fails (emit what validated, never unvalidated geometry). fn anchor_grouped_meshes( bytes: &[u8], data_base: usize, starts: &[usize], markers: &[(usize, usize)], // (vtx_count, idx_count) in descriptor/file order decl: &VertexDecl, ) -> Vec { let n = markers.len(); if n == 0 { return Vec::new(); } let stride = decl.stride; // Relative index-buffer offsets (ib0 = 0), 4-byte aligned between buffers, // and cumulative vertex offsets (vb0 = 0) — both derived from the marker list. let mut rel_ib = Vec::with_capacity(n); let mut off_v = Vec::with_capacity(n); let (mut acc_i, mut acc_v) = (0usize, 0usize); for &(vc, ic) in markers { rel_ib.push(acc_i); off_v.push(acc_v); acc_i = align4(acc_i + ic * 2); acc_v += vc * stride; } // The index pool spans ib0 .. end-of-last-buffer. The vertex pool that // follows is **4-byte aligned**, so up to 3 bytes of padding can sit between // the last index buffer and vb0: `vb0 = align4(ib0 + span)`, i.e. // `ib0 = vb0 − span − pad` with `pad ∈ 0..=3`. (DeltaSaber's index pool ended // already-aligned → pad 0; many weapons need pad 2.) We try each pad and let // `validate_block` pick the one that yields a coherent sub-mesh. let span = rel_ib[n - 1] + markers[n - 1].1 * 2; // Pivot on the **largest** sub-mesh, not `markers[0]`: it's the one whose // triangle-quality + connectivity signature most reliably confirms the // (ib0, vb0) alignment. Some weapons lead with a tiny, elongated bracket that // fails the connectivity gate even when perfectly placed, which would reject // an otherwise-correct pivot. let kmax = (0..n).max_by_key(|&i| markers[i].1).unwrap_or(0); let (vck, ick) = markers[kmax]; for &vb0 in starts { for pad in 0..=3usize { if vb0 < span + pad { continue; } let ib0 = vb0 - span - pad; if ib0 < data_base { continue; } // Strict anchor on the pivot sub-mesh: require high winding // consistency AND connectivity here regardless of the stage-wide // `min_consistency`. A wrong pad reads shifted indices → agreement // collapses well below 0.85, so only the true pad/pivot passes. let ib_k = ib0 + rel_ib[kmax]; let vb_k = vb0 + off_v[kmax]; if !validate_block(bytes, ib_k, vb_k, vck, ick, decl, 0.85, true) { continue; } // Pivot confirmed the exact alignment ⇒ every marker up to the pivot // is correctly placed; read those unconditionally (a legitimately // tiny/flat lead part may fail the quality gates yet still be real). // Markers after the pivot are validated so a stray trailing marker // ends the chain instead of appending garbage. let mut meshes = Vec::with_capacity(n); let mut vb = vb0; for i in 0..n { let (vc, ic) = markers[i]; let ib = ib0 + rel_ib[i]; if ib + ic * 2 > bytes.len() || vc.checked_mul(stride).map_or(true, |b| vb + b > bytes.len()) { break; } // Parts are placed deterministically; in-range + consistency pins // them, so the connectivity heuristic (which mis-rejects small // flat fins) is relaxed here. let ok = validate_block(bytes, ib, vb, vc, ic, decl, 0.85, false); if !ok && i > kmax { break; // chain diverged — emit the validated prefix, no garbage } meshes.push(read_pool_mesh(bytes, ib, vb, ic, vc, decl)); vb += vc * stride; } return meshes; } } Vec::new() } /// Read positions / normals / uvs / indices for an anchored stage block. fn read_pool_mesh( bytes: &[u8], ib: usize, vb: usize, index_count: usize, vtx_count: usize, decl: &VertexDecl, ) -> GameMesh { let stride = decl.stride; // Triangle LIST (see `from_xpr2`): each consecutive index triple is a // triangle. `anchor_pool_mesh` already validated this block as a list. let indices: Vec = (0..index_count) .map(|k| be16(bytes, ib + k * 2) as u32) .collect(); let mut positions = Vec::with_capacity(vtx_count); let mut normals = Vec::with_capacity(vtx_count); let mut uvs = Vec::with_capacity(vtx_count); for v in 0..vtx_count { let o = vb + v * stride; let p = o + decl.pos_offset; positions.push([bef(bytes, p), bef(bytes, p + 4), bef(bytes, p + 8)]); if let Some(no) = decl.normal_offset { let nb = o + no; normals.push([half(bytes, nb), half(bytes, nb + 2), half(bytes, nb + 4)]); } if let Some(uo) = decl.uv_offset { let ub = o + uo; uvs.push([half(bytes, ub), half(bytes, ub + 2)]); } } GameMesh { positions, normals, uvs, indices, name: None, } } // ── Layout constants ──────────────────────────────────────────────────────── /// Size of the header that precedes each sub-mesh block's index buffer /// (`[12-byte header][index buffer][vertex buffer]`). Contents not yet decoded. const VERTEX_BUFFER_GAP: usize = 12; // ── Vertex declaration ─────────────────────────────────────────────────────── /// The vertex layout for one XBG7 resource, parsed from the descriptor's /// declaration table (shared by all its sub-meshes). struct VertexDecl { /// Bytes per vertex. stride: usize, /// Byte offset of the POSITION element (`f32×3`) within a vertex. pos_offset: usize, /// Byte offset of the NORMAL element (`f16×4`), if present. normal_offset: Option, /// Byte offset of the TEXCOORD element (`f16×2`), if present. uv_offset: Option, } /// Known element format codes → element size in bytes (from the GPU capture: /// POSITION `f32×3`, NORMAL `f16×4`, TEXCOORD `f16×2`). fn decl_code_size(code: u32) -> Option { match code { 0x2A_23B9 => Some(12), // f32×3 (POSITION) 0x1A_2360 => Some(8), // f16×4 (NORMAL) 0x2C_235F => Some(4), // f16×2 (TEXCOORD) _ => None, } } /// Parse the XBG7 vertex declaration: a table of `{offset:u32, code:u32, /// usage<<16:u32}` big-endian triples that follows the `(index_bytes, /// index_count)` marker, terminated by an `offset == 0x00FF0000` / /// `code == 0xFFFFFFFF` sentinel. Usage codes: `0` POSITION, `3` NORMAL, /// `5` TEXCOORD. Stride is the max element extent; unknown element sizes are /// inferred from the next element's offset. /// Locate the `(index_bytes, index_count)` marker in a descriptor: the first /// big-endian pair where `index_bytes == index_count * 2` and `index_count` is a /// positive multiple of 3. Returns `(rel_offset, index_count)`. fn find_index_marker(desc: &[u8]) -> Option<(usize, usize)> { let mut rel = 0usize; while rel + 40 <= desc.len() { let a = be32(desc, rel); let c = be32(desc, rel + 4); if c >= 3 && c % 3 == 0 && c < 400_000 && a == c * 2 { return Some((rel, c as usize)); } rel += 4; } None } /// Collect **all** `(vtx_count, idx_count)` sub-mesh records in a descriptor, in /// file order, for the grouped-pool layout (see [`anchor_grouped_meshes`]). /// /// Each record is an `(index_bytes, index_count)` marker (as in /// [`find_index_marker`]) whose vertex count sits 32 bytes earlier — the same /// `(mk_rel − 32)` convention the single-marker anchor uses. Records with an /// implausible vertex count are skipped so a stray `a == c·2` coincidence cannot /// corrupt the derived index/vertex chain. fn all_index_markers(desc: &[u8]) -> Vec<(usize, usize)> { let mut out = Vec::new(); let mut rel = 0usize; while rel + 8 <= desc.len() { let a = be32(desc, rel); let c = be32(desc, rel + 4); if c >= 3 && c % 3 == 0 && c < 400_000 && a == c * 2 && rel >= 32 { let vc = be32(desc, rel - 32) as usize; if (3..=200_000).contains(&vc) { out.push((vc, c as usize)); rel += 8; continue; } } rel += 4; } out } fn parse_vertex_decl(desc: &[u8]) -> Option { let mk = find_index_marker(desc)?.0; // Read declaration triples. let mut elems: Vec<(usize, u32, u32)> = Vec::new(); // (offset, code, usage) let mut r = mk + 8; for _ in 0..16 { if r + 12 > desc.len() { break; } let off = be32(desc, r); let code = be32(desc, r + 4); let usage = be32(desc, r + 8) >> 16; if off == 0x00FF_0000 || code == 0xFFFF_FFFF { break; } if off as usize > 0x1000 { break; // out-of-range offset — not a real element } elems.push((off as usize, code & 0x00FF_FFFF, usage)); r += 12; } if elems.is_empty() { return None; } let mut stride = 0usize; for (i, &(off, code, _)) in elems.iter().enumerate() { let size = decl_code_size(code).unwrap_or_else(|| { if i + 1 < elems.len() { elems[i + 1].0.saturating_sub(off) } else { 4 } }); stride = stride.max(off + size); } if stride == 0 || stride > 256 { return None; } let pos_offset = elems .iter() .find(|&&(_, c, u)| c == 0x2A_23B9 || u == 0) .map(|&(o, _, _)| o) .unwrap_or(0); let normal_offset = elems.iter().find(|&&(_, _, u)| u == 3).map(|&(o, _, _)| o); let uv_offset = elems.iter().find(|&&(_, _, u)| u == 5).map(|&(o, _, _)| o); Some(VertexDecl { stride, pos_offset, normal_offset, uv_offset, }) } // ── Descriptor sub-mesh record scan ───────────────────────────────────────── /// Scan an XBG7 descriptor for the ordered list of per-sub-mesh /// `(vtx_count, idx_count)` records. /// /// The record is a big-endian tuple `[vtx:u32][0:u32][idx:u32][tail:u32]` with /// `3 ≤ vtx ≤ 65535`, the second word zero, `idx` a positive multiple of 3, and /// a small non-zero `tail`. Found by a sliding 4-byte scan (records are not on /// a fixed stride in the scene graph). fn submesh_records(desc: &[u8]) -> Vec<(usize, usize)> { let mut out = Vec::new(); if desc.len() < 16 { return out; } let mut rel = 0usize; while rel + 16 <= desc.len() { let a = be32(desc, rel); let z = be32(desc, rel + 4); let c = be32(desc, rel + 8); let t = be32(desc, rel + 12); if (3..=65535).contains(&a) && z == 0 && c >= 3 && c <= 200_000 && c % 3 == 0 && (1..=64).contains(&t) { out.push((a as usize, c as usize)); rel += 16; // consume the record } else { rel += 4; } } out } // ── Little primitive readers ──────────────────────────────────────────────── #[inline] fn align16(x: usize) -> usize { (x + 15) & !15 } #[inline] fn align4(x: usize) -> usize { (x + 3) & !3 } /// Expand a triangle-**strip** index buffer into a triangle list (alternating /// winding, degenerates dropped). **Diagnostic only** — XBG7 index buffers are /// triangle LISTS, not strips (proven by `XVERIFY`: normal-agree 1.000 as a list /// vs ~0.49 as a strip, plus the GPU capture's `prim=4`). Retained so `XVERIFY` /// can keep demonstrating that the strip reading is wrong; NOT used in decode. fn expand_triangle_strip(strip: &[u32]) -> Vec { let mut out = Vec::with_capacity(strip.len().saturating_sub(2) * 3); for w in 0..strip.len().saturating_sub(2) { let (a, b, c) = if w % 2 == 0 { (strip[w], strip[w + 1], strip[w + 2]) } else { (strip[w + 1], strip[w], strip[w + 2]) }; if a != b && b != c && a != c { out.extend_from_slice(&[a, b, c]); } } out } /// Topology-correctness metrics for a flat triangle-list index buffer, used to /// decide list-vs-strip objectively. Returns `(tris, degenerate, normal_agree, /// edge2_frac)`: /// - `normal_agree`: fraction of non-degenerate triangles whose geometric face /// normal (cross product) dots positively with the sum of its three vertices' /// stored normals. Correct topology + consistent winding ⇒ near 1.0; a /// mis-interpreted buffer wires arbitrary vertices ⇒ near 0.5. /// - `edge2_frac`: fraction of distinct undirected edges shared by exactly 2 /// triangles. A closed manifold surface ⇒ near 1.0. fn topology_report( indices: &[u32], positions: &[[f32; 3]], normals: &[[f32; 3]], ) -> (usize, usize, f32, f32) { use std::collections::HashMap; let tris = indices.len() / 3; let mut degen = 0usize; let mut agree = 0usize; let mut counted = 0usize; let mut edges: HashMap<(u32, u32), u32> = HashMap::new(); for t in indices.chunks_exact(3) { let (a, b, c) = (t[0] as usize, t[1] as usize, t[2] as usize); if a == b || b == c || a == c { degen += 1; continue; } for &(i, j) in &[(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] { let e = if i < j { (i, j) } else { (j, i) }; *edges.entry(e).or_insert(0) += 1; } let (p0, p1, p2) = (positions[a], positions[b], positions[c]); let u = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]]; let w = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]]; let fx = u[1] * w[2] - u[2] * w[1]; let fy = u[2] * w[0] - u[0] * w[2]; let fz = u[0] * w[1] - u[1] * w[0]; if !normals.is_empty() { let sn = [ normals[a][0] + normals[b][0] + normals[c][0], normals[a][1] + normals[b][1] + normals[c][1], normals[a][2] + normals[b][2] + normals[c][2], ]; let dot = fx * sn[0] + fy * sn[1] + fz * sn[2]; if dot > 0.0 { agree += 1; } counted += 1; } } let two = edges.values().filter(|&&c| c == 2).count(); let edge2 = if edges.is_empty() { 0.0 } else { two as f32 / edges.len() as f32 }; let na = if counted == 0 { f32::NAN } else { agree as f32 / counted as f32 }; (tris, degen, na, edge2) } #[inline] fn be16(b: &[u8], o: usize) -> u16 { u16::from_be_bytes([b[o], b[o + 1]]) } #[inline] fn be32(b: &[u8], o: usize) -> u32 { u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) } #[inline] fn bef(b: &[u8], o: usize) -> f32 { f32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) } /// Big-endian IEEE-754 half → f32. #[inline] fn half(b: &[u8], o: usize) -> f32 { f16_to_f32(be16(b, o)) } /// Minimal IEEE-754 binary16 → binary32 (no external dep). fn f16_to_f32(h: u16) -> f32 { let sign = (h >> 15) & 1; let exp = (h >> 10) & 0x1F; let mant = h & 0x3FF; let bits: u32 = match exp { 0 if mant == 0 => (sign as u32) << 31, // ±0 0 => { // subnormal → normalize let mut e: i32 = -1; let mut m = mant as u32; loop { e += 1; m <<= 1; if m & 0x400 != 0 { break; } } let exp32 = (127 - 15 - e) as u32; ((sign as u32) << 31) | (exp32 << 23) | ((m & 0x3FF) << 13) } 0x1F => ((sign as u32) << 31) | (0xFF << 23) | ((mant as u32) << 13), // Inf/NaN _ => { let exp32 = (exp as i32 - 15 + 127) as u32; ((sign as u32) << 31) | (exp32 << 23) | ((mant as u32) << 13) } }; f32::from_bits(bits) } fn read_cstr(b: &[u8], o: usize) -> Option { if o >= b.len() { return None; } let end = b[o..].iter().position(|&c| c == 0).map(|p| o + p)?; if end == o { return None; } Some(String::from_utf8_lossy(&b[o..end]).into_owned()) } // ── Tests ─────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; #[test] fn half_roundtrip_known_values() { assert_eq!(f16_to_f32(0x3C00), 1.0); // 1.0 assert_eq!(f16_to_f32(0x0000), 0.0); // +0 assert_eq!(f16_to_f32(0xBC00), -1.0); // -1.0 assert_eq!(f16_to_f32(0x4000), 2.0); // 2.0 assert!((f16_to_f32(0x3800) - 0.5).abs() < 1e-6); // 0.5 } #[test] fn submesh_record_scan_finds_tuple() { // [vtx=215][0][idx=1092][tail=4] let mut d = vec![0u8; 32]; d[0..4].copy_from_slice(&215u32.to_be_bytes()); d[8..12].copy_from_slice(&1092u32.to_be_bytes()); d[12..16].copy_from_slice(&4u32.to_be_bytes()); let recs = submesh_records(&d); assert_eq!(recs, vec![(215, 1092)]); } #[test] fn rejects_non_xpr2() { assert!(matches!( Xbg7Model::from_xpr2(b"NOPEnotacontainerXXXXXXXX"), Err(MeshError::NotXpr2) )); } }