//! 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 //! [ index buffer : idx_count × u16 big-endian ] triangle list //! [ 12-byte vertex-buffer header (contents undecoded) ] //! [ vertex buffer : vtx_count × stride bytes ] (declaration-driven) //! (pad to 16 bytes → next sub-mesh) //! ``` //! //! 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. //! //! ## Not yet decoded //! //! The hero-ship *body* meshes (`DeltaSaber_*.xpr` `f004`, and ~100 other //! models) use a more complex **multi-stream** layout — separate position / //! attribute streams at descriptor-addressed offsets, quantized positions — //! which is not handled here and is cleanly declined. 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, } 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); } // 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); } // Indices (u16 BE), validated against the sub-mesh vertex count. 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); } } 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). pub fn stage_models(bytes: &[u8]) -> 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, index_count: usize, vtx_count: 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 (mk_rel, index_count) = match find_index_marker(d) { Some(m) => m, None => continue, }; let decl = match parse_vertex_decl(d) { Some(v) => v, None => continue, }; // Total vertex count is stored 32 bytes before the index marker. if mk_rel < 32 { continue; } let vtx_count = be32(d, mk_rel - 32) as usize; if !(3..=400_000).contains(&vtx_count) || index_count < 3 { 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, index_count, vtx_count, 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]; if let Some(mesh) = anchor_pool_mesh( bytes, starts, r.index_count, r.vtx_count, &r.decl, ) { out.push(Xbg7Model { name: r.name.clone(), meshes: vec![mesh], }); } } 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, ) -> Option { let stride = decl.stride; let idx_bytes = index_count * 2; let vtx_bytes = vtx_count.checked_mul(stride)?; for &vb in starts { // The index buffer sits immediately before the vertex buffer. if vb < idx_bytes { continue; } let ib = vb - idx_bytes; if vb + vtx_bytes > bytes.len() { continue; } // ── Full index validation: every index in range, uses ~all vertices. ── let mut max_idx = 0u32; let mut ok = true; for k in 0..index_count { let i = be16(bytes, ib + k * 2) as u32; if i >= vtx_count as u32 { ok = false; break; } max_idx = max_idx.max(i); } if !ok || (max_idx as usize) + 4 < vtx_count { continue; } // ── 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 tris = index_count / 3; let tstep = (tris / 96).max(1); let mut t = 0; let mut bad = false; while t < tris { let mut p = [[0.0f32; 3]; 3]; for (c, pc) in p.iter_mut().enumerate() { let vi = be16(bytes, ib + (3 * t + c) * 2) as usize; 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 { bad = true; break; } *slot = x; lo[a] = lo[a].min(x); hi[a] = hi[a].max(x); } if bad { break; } } if bad { break; } 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; } // 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; } if bad { continue; } 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 { continue; // 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 — try another candidate or decline. 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 { continue; } // ── Accepted: read the full mesh. ── return Some(read_pool_mesh(bytes, ib, vb, index_count, vtx_count, decl)); } None } /// 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; 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 } 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 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) )); } }