Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_asset/
hmd8.rs

1//! HMD8: a merged model container carrying mesh, bone ranges and every clip's
2//! composed bone palettes in one blob.
3//!
4//! Lifted from hl-psx, which developed it to run the whole Half-Life campaign
5//! (103 maps, 237 transitions) inside a 223.7 KB model pool covering mesh AND
6//! animation. It lives here so both games share one format rather than each
7//! carrying a private reader.
8//!
9//! What it does that `Model` + `Animation` do not:
10//!
11//! * one blob per model, with a clip table, so a model's whole animation set
12//!   is a single asset rather than one file per clip;
13//! * vertices stored bone-local and grouped into contiguous RANGES, so each
14//!   range pays one matrix load and feeds the static stream straight to RTPT;
15//! * a 20-byte composed affine per bone per frame (`HMD8_AFFINE_BYTES`), the
16//!   same packed Q11 encoding `.psxanim` v3 already uses;
17//! * optional sections gated by flags (body masks, mouth transforms, studio
18//!   hitboxes, packed or i8 normals, frame times, vertex SoA), so a consumer
19//!   that wants none of the GoldSrc specifics simply does not emit them.
20//!
21//! Extraction note: the only coupling to its old home was a vertex-count guard
22//! reading a game-local budget constant. That is now
23//! [`DEFAULT_MAX_VERTICES`] with [`Model::from_bytes_with_vertex_cap`] for
24//! callers that police their own arena.
25
26// ponytail: the lifted reader names its fields and accessors after the format
27// fields they read (`first`, `count`, `bone`), so per-item docs would only
28// restate the name. The TYPES carry the meaning and are documented below.
29#![allow(missing_docs)]
30
31use core::ptr;
32
33use psx_gte::math::{Mat3I16, Vec3I16};
34
35// ponytail: unchecked reads, same rationale as map.rs -- no D-cache on the
36// R3000, so dropping the bounds branch lets LLVM coalesce these into MIPS
37// unaligned word loads (lwl/lwr). Every caller proves its range first: from
38// the offsets `Model::from_bytes` validated against the blob (private fields only;
39// the public counts can be overwritten by safe code and are never trusted for
40// memory safety), or with an explicit `fits` check.
41
42/// True when `d[o..o + n]` is in bounds.
43#[inline(always)]
44fn fits(d: &[u8], o: usize, n: usize) -> bool {
45    o <= d.len() && n <= d.len() - o
46}
47
48/// `len` bytes at `src` copied to `dst` inside `d` (the ranges may overlap),
49/// or `None` when either range leaves `d`.
50fn move_within(d: &mut [u8], src: usize, dst: usize, len: usize) -> Option<()> {
51    if !fits(d, src, len) || !fits(d, dst, len) {
52        return None;
53    }
54    d.copy_within(src..src + len, dst);
55    Some(())
56}
57
58/// # Safety
59/// `o + 4 <= d.len()`.
60#[inline(always)]
61unsafe fn rd_u32(d: &[u8], o: usize) -> u32 {
62    // SAFETY: the caller guarantees bytes `o..o + 4` are in `d`.
63    unsafe {
64        let p = d.as_ptr().add(o);
65        u32::from_le_bytes([p.read(), p.add(1).read(), p.add(2).read(), p.add(3).read()])
66    }
67}
68/// # Safety
69/// `o + 2 <= d.len()`.
70#[inline(always)]
71unsafe fn rd_u16(d: &[u8], o: usize) -> u16 {
72    // SAFETY: the caller guarantees bytes `o..o + 2` are in `d`.
73    unsafe {
74        let p = d.as_ptr().add(o);
75        u16::from_le_bytes([p.read(), p.add(1).read()])
76    }
77}
78/// # Safety
79/// `o + 2 <= d.len()`.
80#[inline(always)]
81unsafe fn rd_i16(d: &[u8], o: usize) -> i16 {
82    // SAFETY: same contract as this function's.
83    unsafe { rd_u16(d, o) as i16 }
84}
85
86#[inline(always)]
87fn decode_q11(raw: u16) -> i16 {
88    // LLVM lowers the reserved-code select to a branch on MIPS-I. Express the
89    // six-instruction branchless sequence directly: sign-extend and double the
90    // twelve-bit value, then use SLTIU to add two only for the reserved +2047
91    // encoding. Every caller masks the packed field to twelve bits first.
92    #[cfg(target_arch = "mips")]
93    // SAFETY: register-only arithmetic: the asm reads and writes just its two
94    // declared operands (`.set noat` keeps $at out of it), touches no memory
95    // and no stack.
96    let decoded = unsafe {
97        let decoded: u32;
98        core::arch::asm!(
99            ".set push",
100            ".set noat",
101            "xori {scratch}, {decoded}, 0x07ff",
102            "sltiu {scratch}, {scratch}, 1",
103            "sll {decoded}, {decoded}, 20",
104            "sra {decoded}, {decoded}, 19",
105            "sll {scratch}, {scratch}, 1",
106            "addu {decoded}, {decoded}, {scratch}",
107            ".set pop",
108            decoded = inlateout(reg) raw as u32 => decoded,
109            scratch = lateout(reg) _,
110            options(nomem, nostack, preserves_flags),
111        );
112        decoded as i16
113    };
114    #[cfg(not(target_arch = "mips"))]
115    let decoded = {
116        let signed = ((raw << 4) as i16) >> 4;
117        signed
118            .wrapping_shl(1)
119            .wrapping_add(((raw == 0x07ff) as i16) << 1)
120    };
121    decoded
122}
123
124/// # Safety
125/// `o + 4 <= d.len()`: the word load also takes the byte after the pair.
126#[inline(always)]
127unsafe fn rd_q11_pair(d: &[u8], o: usize) -> (i16, i16) {
128    // SAFETY: the caller guarantees bytes `o..o + 4` are in `d`; the load is
129    // unaligned, so `o` needs no alignment.
130    unsafe {
131        // Each pair occupies three bytes. One unaligned word load is cheaper
132        // on MIPS-I than three independent byte loads and exposes both packed
133        // fields directly; byte 3 belongs to the next pair and is masked out.
134        let packed = u32::from_le(d.as_ptr().add(o).cast::<u32>().read_unaligned());
135        (
136            decode_q11((packed & 0x0fff) as u16),
137            decode_q11(((packed >> 12) & 0x0fff) as u16),
138        )
139    }
140}
141#[inline(always)]
142fn unpack_normal_555(packed: u16) -> [i8; 3] {
143    let component = |shift: u32| -> i8 {
144        let bits = ((packed >> shift) & 0x1f) as i16;
145        let signed = if bits & 0x10 != 0 { bits - 32 } else { bits };
146        (signed * 8) as i8
147    };
148    [component(0), component(5), component(10)]
149}
150
151/// A parsed HMD8 blob: mesh, bone ranges, clip table and every clip's composed
152/// bone palettes, all borrowed from one `&'static` slice with no copying.
153///
154/// Construct with [`Model::from_bytes`] or
155/// [`Model::from_bytes_with_vertex_cap`]; a blob that fails validation yields
156/// an empty model rather than panicking, so a bad cook draws nothing instead
157/// of taking the guest down.
158///
159/// Every accessor is memory-safe on any model, including one whose public
160/// counts (the deprecated `n_verts` and `n_tris`) were overwritten:
161/// reads are bounded by the blob and the private offsets `from_bytes`
162/// validated, never by the
163/// public counts. An index past a table reads an inert value (origin vertex,
164/// skipped triangle, identity pose). The `*_unchecked` forms skip that check
165/// for hot loops that already bound their index by the loaded count.
166#[derive(Clone, Copy)]
167pub struct Model {
168    data: &'static [u8],
169    #[deprecated(note = "use `vertex_count()`")]
170    pub n_verts: usize,
171    #[deprecated(note = "use `triangle_count()`")]
172    pub n_tris: usize,
173    n_frames: usize,
174    n_clips: usize,
175    n_bones: usize,
176    n_ranges: usize,
177    n_hitboxes: usize,
178    clips_off: usize,
179    frame_times_off: usize,
180    ranges_off: usize,
181    vertices_off: usize,
182    vertices_z_off: usize,
183    poses_off: usize,
184    tri_off: usize,
185    tri_sz: usize,
186    normal_encoding: u8,
187    local_to_world_q12: u16,
188    mouth_xforms_off: usize,
189    hitboxes_off: usize,
190    has_body_masks: bool,
191    has_mouth: bool,
192    has_frame_times: bool,
193    hma_off: usize,
194    hma_len: usize,
195    #[allow(dead_code)] // read only by the MIPS vertex fast path
196    aligned_vertices: bool,
197    vertex_soa: bool,
198}
199
200/// One HMD8 vertex in the exact two-register layout consumed by the GTE.
201///
202/// Keeping XY packed avoids rebuilding the same word with shifts and ORs
203/// before every model RTPT batch; Z is sign-extended for the paired VZ input.
204#[derive(Clone, Copy)]
205pub struct GteVertexWords {
206    pub xy: u32,
207    pub z: u32,
208}
209
210impl GteVertexWords {
211    /// The vertex as a bone-local position.
212    #[inline(always)]
213    pub const fn position(self) -> Vec3I16 {
214        Vec3I16::new(self.xy as i16, (self.xy >> 16) as i16, self.z as i16)
215    }
216
217    #[inline(always)]
218    const fn from_soa(xy: u32, z: i16) -> Self {
219        GteVertexWords {
220            xy,
221            z: z as i32 as u32,
222        }
223    }
224}
225
226/// A model's vertex stream as GTE register words, bounds-checked once.
227///
228/// [`Model::vertex_words`] builds it from the struct-of-arrays stream current
229/// cooks emit: a word-aligned run of packed XY words and a run of Z halfwords,
230/// one entry per vertex. Both are ordinary slices of the same length, so a
231/// loop over [`Self::triples`] or [`Self::iter`] reads every vertex with
232/// native aligned loads and no per-index check, and a range taken with
233/// [`Self::slice`] is checked once rather than once per vertex.
234#[derive(Clone, Copy)]
235pub struct VertexWords<'a> {
236    xy: &'a [u32],
237    z: &'a [i16],
238}
239
240impl<'a> VertexWords<'a> {
241    /// No vertices.
242    pub const EMPTY: VertexWords<'static> = VertexWords { xy: &[], z: &[] };
243
244    /// Number of vertices in the view.
245    #[inline(always)]
246    pub const fn len(&self) -> usize {
247        self.xy.len()
248    }
249
250    /// True when the view holds no vertices.
251    #[inline(always)]
252    pub const fn is_empty(&self) -> bool {
253        self.xy.is_empty()
254    }
255
256    /// Vertex `index`, or `None` past the end of the view.
257    #[inline]
258    pub fn get(&self, index: usize) -> Option<GteVertexWords> {
259        Some(GteVertexWords::from_soa(
260            *self.xy.get(index)?,
261            *self.z.get(index)?,
262        ))
263    }
264
265    /// The vertices `range` covers, or `None` when it runs past the view.
266    #[inline]
267    pub fn slice(&self, range: core::ops::Range<usize>) -> Option<VertexWords<'a>> {
268        Some(VertexWords {
269            xy: self.xy.get(range.clone())?,
270            z: self.z.get(range)?,
271        })
272    }
273
274    /// Every vertex in order.
275    #[inline]
276    pub fn iter(&self) -> impl Iterator<Item = GteVertexWords> + 'a {
277        self.xy
278            .iter()
279            .zip(self.z)
280            .map(|(&xy, &z)| GteVertexWords::from_soa(xy, z))
281    }
282
283    /// The vertices in consecutive groups of three, one RTPT batch each,
284    /// and the view of the zero to two vertices left over.
285    #[inline]
286    pub fn triples(
287        &self,
288    ) -> (
289        impl Iterator<Item = [GteVertexWords; 3]> + 'a,
290        VertexWords<'a>,
291    ) {
292        let (xy, xy_rest) = self.xy.as_chunks::<3>();
293        let (z, z_rest) = self.z.as_chunks::<3>();
294        let groups = xy.iter().zip(z).map(|(xy, z)| {
295            [
296                GteVertexWords::from_soa(xy[0], z[0]),
297                GteVertexWords::from_soa(xy[1], z[1]),
298                GteVertexWords::from_soa(xy[2], z[2]),
299            ]
300        });
301        (
302            groups,
303            VertexWords {
304                xy: xy_rest,
305                z: z_rest,
306            },
307        )
308    }
309}
310
311/// One frame's bone palette, read lazily: a bone's affine is unpacked from the
312/// blob when asked for, not eagerly into a scratch array.
313#[derive(Clone, Copy)]
314pub struct ModelFrame<'a> {
315    data: &'a [u8],
316    poses_off: usize,
317    n_bones: usize,
318    frame_idx: usize,
319    mouth_xforms_off: usize,
320    has_mouth: bool,
321}
322
323/// Two baked bone palettes prepared for fixed-point affine interpolation.
324#[derive(Clone, Copy)]
325pub struct InterpolatedModelFrame<'a> {
326    a: ModelFrame<'a>,
327    b: ModelFrame<'a>,
328    frac16: i32,
329}
330
331/// A contiguous run of vertices sharing one bone.
332///
333/// This is the reason the format is fast: vertices are stored bone-local and
334/// sorted by bone, so a range pays ONE matrix load and then streams straight
335/// into RTPT, instead of a per-vertex matrix lookup.
336#[derive(Clone, Copy)]
337pub struct BoneRange {
338    pub first: usize,
339    pub count: usize,
340    pub bone: usize,
341    pub body_mask: u8,
342    pub mouth: bool,
343}
344
345/// A bone's composed pose, unpacked from the 20-byte record into GTE registers.
346#[derive(Clone, Copy)]
347pub struct BoneTransform {
348    pub rotation: Mat3I16,
349    pub translation: Vec3I16,
350}
351
352/// A GoldSrc studio hitbox, present only when the cook set the hitbox flag.
353#[derive(Clone, Copy)]
354pub struct StudioHitbox {
355    pub bone: usize,
356    pub bbmin: Vec3I16,
357    pub bbmax: Vec3I16,
358}
359
360const TRI_SZ: usize = 16;
361const TRI_SZ_FULL_NORMALS: usize = 20;
362const HMD7_HEADER_BYTES: usize = 36;
363const HMD7_RANGE_BYTES: usize = 8;
364const HMD8_AFFINE_BYTES: usize = 20;
365const HMD7_HITBOX_BYTES: usize = 14;
366const CLIP_FRAME_COUNT_MASK: u16 = 0x00ff;
367const CLIP_FIRST_FRAME_MASK: u16 = 0x7fff;
368const CLIP_DURATION_EXT_BIT: u16 = 0x8000;
369const DEFAULT_CLIP_HOLD_TICKS: u16 = 40;
370const LOCAL_TO_WORLD_IDENTITY_Q12: u16 = 4096;
371
372#[inline(always)]
373const fn valid_local_to_world_q12(raw: u16) -> bool {
374    matches!(raw, 0 | 256 | 512 | 1024 | 2048 | 4096)
375}
376
377const HMD_FLAG_BODY_MASKS: u16 = 1 << 0;
378const HMD_FLAG_MOUTH: u16 = 1 << 1;
379const HMD_FLAG_PACKED_NORMALS: u16 = 1 << 2;
380const HMD_FLAG_I8_NORMALS: u16 = 1 << 3;
381const HMD_FLAG_HITBOXES: u16 = 1 << 4;
382const HMD_FLAG_FRAME_TIMES: u16 = 1 << 5;
383const HMD_FLAG_ALIGNED_MODEL_DATA: u16 = 1 << 6;
384const HMD_FLAG_VERTEX_SOA: u16 = 1 << 7;
385/// Local-space HMA1 tracks follow the hitboxes: `u32 len`, then `u16 n_used`,
386/// `u8 jaw_bone` (HMA1 bone of the mouth controller, 0xff none), `u8
387/// jaw_flags` (bit 0: post-multiply), `i16 jaw_open[4]` (Q12 quaternion of the
388/// fully open controller), `u8 hma_bone[n_used]` (HMD8 range bone -> HMA1
389/// bone), pad to 2 (absolute), the HMA1 blob, 4 zero bytes, pad to 4. The pose
390/// palette then holds a single bind pose for readers that ignore the tracks.
391const HMD_FLAG_HMA1: u16 = 1 << 8;
392const HMA1_SECTION_HEADER: usize = 4 + 2 + 2 + 8;
393const HMD7_RANGE_MOUTH: u8 = 1 << 0;
394const NORMAL_NONE: u8 = 0;
395const NORMAL_I8X3: u8 = 1;
396const NORMAL_PACKED_555: u8 = 2;
397const MOUTH_XFORM_BYTES: usize = 24;
398const IDENTITY_TRANSFORM: BoneTransform = BoneTransform {
399    rotation: Mat3I16::IDENTITY,
400    translation: Vec3I16::ZERO,
401};
402/// Vertex-count guard used by [`Model::from_bytes`].
403///
404/// A consumer with its own model arena should call
405/// [`Model::from_bytes_with_vertex_cap`] and pass that arena's real capacity:
406/// the guard exists so a cook that outgrew the arena fails to load rather than
407/// scribbling past it.
408pub const DEFAULT_MAX_VERTICES: usize = RENDER_FACE_INDEX_MASK as usize + 1;
409
410/// A decoded triangle. The blob keeps these packed; this is the unpacked view.
411#[derive(Clone, Copy)]
412pub struct Triangle {
413    pub idx: [u16; 3],
414    pub tex: usize,
415    pub uv: [(u8, u8); 3],
416    pub normal: [i8; 3],
417    pub body_mask: u8,
418}
419
420/// Cold UV payload for a projected model face. Vertex indices live in a
421/// separate packed-u32 stream so near/backface rejects touch only four
422/// uncached bytes. Texture ids live once per ordered face run, not once per
423/// face; the streamed actor set keeps those ids out of every face payload.
424#[derive(Clone, Copy)]
425#[repr(C)]
426pub struct RenderFacePayload {
427    pub uv_words: [u16; 3],
428}
429
430impl RenderFacePayload {
431    pub const ZERO: Self = Self { uv_words: [0; 3] };
432}
433
434/// Three ten-bit indices fit one hot word exactly (with the top two bits
435/// unused), so a model whose faces are packed this way can have at most
436/// `RENDER_FACE_INDEX_MASK + 1` (1024) vertices. [`DEFAULT_MAX_VERTICES`] is
437/// that bound, so [`Model::from_bytes`] never admits a model the packing
438/// would mangle; a caller that passes a larger cap to
439/// [`Model::from_bytes_with_vertex_cap`] must not pack its faces.
440pub const RENDER_FACE_INDEX_MASK: u32 = 0x3ff;
441
442#[inline(always)]
443fn uv_word(uv: (u8, u8)) -> u16 {
444    (uv.0 as u16) | ((uv.1 as u16) << 8)
445}
446
447#[inline(always)]
448unsafe fn write_render_face(
449    indices: *mut u32,
450    payloads: *mut RenderFacePayload,
451    t: usize,
452    tri: &Triangle,
453) {
454    // SAFETY: contract is the enclosing fn's; see its doc comment.
455    unsafe {
456        let packed = (tri.idx[0] as u32 & RENDER_FACE_INDEX_MASK)
457            | ((tri.idx[1] as u32 & RENDER_FACE_INDEX_MASK) << 10)
458            | ((tri.idx[2] as u32 & RENDER_FACE_INDEX_MASK) << 20);
459        ptr::write(indices.add(t), packed);
460        ptr::write(
461            payloads.add(t),
462            RenderFacePayload {
463                uv_words: [uv_word(tri.uv[0]), uv_word(tri.uv[1]), uv_word(tri.uv[2])],
464            },
465        );
466    }
467}
468
469impl Model {
470    /// Zero model for static cache slots (never drawn: no triangles).
471    #[allow(deprecated)] // initialises the deprecated public count fields
472    pub const EMPTY: Model = Model {
473        data: &[],
474        n_verts: 0,
475        n_tris: 0,
476        n_frames: 0,
477        n_clips: 0,
478        clips_off: 0,
479        frame_times_off: 0,
480        ranges_off: 0,
481        vertices_off: 0,
482        vertices_z_off: 0,
483        poses_off: 0,
484        n_bones: 0,
485        n_ranges: 0,
486        tri_off: 0,
487        tri_sz: TRI_SZ,
488        normal_encoding: NORMAL_NONE,
489        local_to_world_q12: 4096,
490        mouth_xforms_off: 0,
491        hitboxes_off: 0,
492        n_hitboxes: 0,
493        has_body_masks: false,
494        has_mouth: false,
495        has_frame_times: false,
496        hma_off: 0,
497        hma_len: 0,
498        aligned_vertices: false,
499        vertex_soa: false,
500    };
501
502    /// Parse a HMD8 blob, guarding vertex count with [`DEFAULT_MAX_VERTICES`].
503    pub fn from_bytes(data: &'static [u8]) -> Model {
504        Self::from_bytes_with_vertex_cap(data, DEFAULT_MAX_VERTICES)
505    }
506
507    /// Number of vertices in the stream.
508    #[allow(deprecated)] // reads the deprecated field this accessor replaces
509    #[inline]
510    pub const fn vertex_count(&self) -> usize {
511        self.n_verts
512    }
513
514    /// Number of triangles in the face table.
515    #[allow(deprecated)] // reads the deprecated field this accessor replaces
516    #[inline]
517    pub const fn triangle_count(&self) -> usize {
518        self.n_tris
519    }
520
521    /// Number of baked palette frames across every clip.
522    #[allow(deprecated)] // reads the deprecated field this accessor replaces
523    #[inline]
524    pub const fn frame_count(&self) -> usize {
525        self.n_frames
526    }
527
528    /// Number of clips in the clip table.
529    #[allow(deprecated)] // reads the deprecated field this accessor replaces
530    #[inline]
531    pub const fn clip_count(&self) -> usize {
532        self.n_clips
533    }
534
535    /// Number of bones in each pose palette.
536    #[allow(deprecated)] // reads the deprecated field this accessor replaces
537    #[inline]
538    pub const fn bone_count(&self) -> usize {
539        self.n_bones
540    }
541
542    /// Number of bone ranges (see [`Model::bone_range`]).
543    #[allow(deprecated)] // reads the deprecated field this accessor replaces
544    #[inline]
545    pub const fn bone_range_count(&self) -> usize {
546        self.n_ranges
547    }
548
549    /// Number of studio hitboxes (zero when the cook emitted none).
550    #[allow(deprecated)] // reads the deprecated field this accessor replaces
551    #[inline]
552    pub const fn hitbox_count(&self) -> usize {
553        self.n_hitboxes
554    }
555
556    /// Parse a HMD8 blob, guarding vertex count with the caller's own arena
557    /// capacity.
558    pub fn from_bytes_with_vertex_cap(data: &'static [u8], max_verts: usize) -> Model {
559        let (model, keeps_blob) = Self::layout(data, max_verts);
560        if keeps_blob {
561            Model { data, ..model }
562        } else {
563            model
564        }
565    }
566
567    /// Parse the header and tables of `data` into a model whose own blob is
568    /// empty, plus whether [`Model::from_bytes_with_vertex_cap`] attaches
569    /// `data` to it (every model but [`Model::EMPTY`] does). Borrows `data`
570    /// only for the call, so [`Model::compact_visible_bodies`] can lay out a
571    /// buffer it holds by `&mut`. The detached model never leaves this
572    /// module: its offsets describe a blob it does not hold.
573    #[allow(deprecated)] // initialises the deprecated public count fields
574    fn layout(data: &[u8], max_verts: usize) -> (Model, bool) {
575        if data.len() < HMD7_HEADER_BYTES || data.get(0..4) != Some(b"HMD8") {
576            return (Self::EMPTY, false);
577        }
578        // SAFETY: every read below ends inside the HMD7_HEADER_BYTES (36)
579        // bytes the length check above guarantees.
580        let (
581            n_verts,
582            n_tris,
583            packed_texture_hitbox_counts,
584            n_frames,
585            n_clips,
586            model_data_len,
587            raw_local_to_world_q12,
588            hmd_flags,
589            n_bones,
590            n_ranges,
591        ) = unsafe {
592            (
593                rd_u32(data, 4) as usize,
594                rd_u32(data, 8) as usize,
595                rd_u32(data, 12),
596                (rd_u32(data, 16) as usize).max(1),
597                (rd_u32(data, 20) as usize).max(1),
598                rd_u32(data, 24) as usize,
599                rd_u16(data, 28),
600                rd_u16(data, 30),
601                rd_u16(data, 32) as usize,
602                rd_u16(data, 34) as usize,
603            )
604        };
605        if !valid_local_to_world_q12(raw_local_to_world_q12) {
606            return (Self::EMPTY, false);
607        }
608        let local_to_world_q12 = if raw_local_to_world_q12 == 0 {
609            LOCAL_TO_WORLD_IDENTITY_Q12
610        } else {
611            raw_local_to_world_q12
612        };
613        let clips_off = HMD7_HEADER_BYTES;
614        let requested_frame_times = hmd_flags & HMD_FLAG_FRAME_TIMES != 0;
615        let frame_times_off = clips_off.saturating_add(n_clips.saturating_mul(4));
616        let unaligned_ranges_off =
617            frame_times_off.saturating_add(if requested_frame_times { n_frames } else { 0 });
618        let requested_vertex_soa = hmd_flags & HMD_FLAG_VERTEX_SOA != 0;
619        let ranges_off = if requested_vertex_soa {
620            unaligned_ranges_off.saturating_add(3) & !3
621        } else if hmd_flags & HMD_FLAG_ALIGNED_MODEL_DATA != 0 {
622            unaligned_ranges_off.saturating_add(1) & !1
623        } else {
624            unaligned_ranges_off
625        };
626        let vertices_off = ranges_off.saturating_add(n_ranges.saturating_mul(HMD7_RANGE_BYTES));
627        let vertices_z_off = if requested_vertex_soa {
628            vertices_off.saturating_add(n_verts.saturating_mul(4))
629        } else {
630            vertices_off
631        };
632        let poses_off = vertices_off.saturating_add(n_verts.saturating_mul(6));
633        let poses_len = n_frames
634            .saturating_mul(n_bones)
635            .saturating_mul(HMD8_AFFINE_BYTES);
636        let requested_mouth = hmd_flags & HMD_FLAG_MOUTH != 0;
637        let mouth_xforms_off = poses_off.saturating_add(poses_len);
638        let mouth_xforms_len = if requested_mouth {
639            n_frames.saturating_mul(MOUTH_XFORM_BYTES)
640        } else {
641            0
642        };
643        let requested_hitboxes = hmd_flags & HMD_FLAG_HITBOXES != 0;
644        let n_hitboxes = if requested_hitboxes {
645            (packed_texture_hitbox_counts >> 16) as usize
646        } else {
647            0
648        };
649        let hitboxes_off = mouth_xforms_off.saturating_add(mouth_xforms_len);
650        let hitboxes_len = n_hitboxes.saturating_mul(HMD7_HITBOX_BYTES);
651        let hma_off = hitboxes_off.saturating_add(hitboxes_len);
652        let hma_len = if hmd_flags & HMD_FLAG_HMA1 != 0 && fits(data, hma_off, 4) {
653            // SAFETY: `fits` just checked bytes `hma_off..hma_off + 4`.
654            4usize.saturating_add(unsafe { rd_u32(data, hma_off) } as usize)
655        } else {
656            0
657        };
658        let tri_off = ranges_off.saturating_add(model_data_len);
659        let full_normals = hmd_flags & HMD_FLAG_I8_NORMALS != 0;
660        let tri_sz = if full_normals {
661            TRI_SZ_FULL_NORMALS
662        } else {
663            TRI_SZ
664        };
665        let requested_body_masks = hmd_flags & HMD_FLAG_BODY_MASKS != 0;
666        let requested_packed_normals = hmd_flags & HMD_FLAG_PACKED_NORMALS != 0;
667        let metadata_ok = (!requested_packed_normals || (!requested_body_masks && !full_normals))
668            && hma_off.saturating_add(hma_len) == tri_off;
669
670        // Validate the parsed header against the actual buffer before trusting
671        // any of it. The per-field reads below are unchecked (no D-cache on the
672        // R3000), so a wrong-sized streamed chunk or a missing magic -- which
673        // makes the counts/offsets garbage -- would dereference wild memory and
674        // crash (this was c1a2a: heaviest map, first to hit it). An invalid
675        // model renders as nothing rather than taking the game down.
676        let tri_end = tri_off.saturating_add(n_tris.saturating_mul(tri_sz));
677        let mut valid = metadata_ok
678            && n_bones > 0
679            && n_ranges > 0
680            && n_verts <= max_verts
681            && (!requested_vertex_soa || (data.as_ptr() as usize + vertices_off) & 3 == 0)
682            && poses_off <= tri_off
683            && tri_end <= data.len();
684        let mut range = 0usize;
685        while valid && range < n_ranges {
686            let o = ranges_off + range * HMD7_RANGE_BYTES;
687            // The range table ends before the poses, which `valid` placed
688            // inside the blob; `fits` re-checks it so the reads stand alone.
689            valid = fits(data, o, HMD7_RANGE_BYTES) && {
690                // SAFETY: `fits` checked the whole eight-byte record.
691                let (first, count, bone) = unsafe {
692                    (
693                        rd_u16(data, o) as usize,
694                        rd_u16(data, o + 2) as usize,
695                        rd_u16(data, o + 4) as usize,
696                    )
697                };
698                count > 0 && first.saturating_add(count) <= n_verts && bone < n_bones
699            };
700            range += 1;
701        }
702        let mut hitbox = 0usize;
703        while valid && hitbox < n_hitboxes {
704            let o = hitboxes_off + hitbox * HMD7_HITBOX_BYTES;
705            valid = fits(data, o, HMD7_HITBOX_BYTES)
706                // SAFETY: `fits` checked the whole hitbox record.
707                && (unsafe { rd_u16(data, o) } as usize) < n_bones;
708            hitbox += 1;
709        }
710        if valid && hma_len != 0 {
711            valid = hma1_section_valid(data, hma_off, hma_len, n_bones, n_clips);
712        }
713        if !valid {
714            // ponytail: null model = draws nothing. If a legit model trips this,
715            // fix the cook / raise the cap rather than removing the guard.
716            return (
717                Model {
718                    data: &[],
719                    n_verts: 0,
720                    n_tris: 0,
721                    n_frames: 1,
722                    n_clips: 1,
723                    clips_off: 0,
724                    frame_times_off: 0,
725                    ranges_off: 0,
726                    vertices_off: 0,
727                    vertices_z_off: 0,
728                    poses_off: 0,
729                    n_bones: 0,
730                    n_ranges: 0,
731                    tri_off: 0,
732                    tri_sz,
733                    normal_encoding: NORMAL_NONE,
734                    local_to_world_q12: LOCAL_TO_WORLD_IDENTITY_Q12,
735                    mouth_xforms_off: 0,
736                    hitboxes_off: 0,
737                    n_hitboxes: 0,
738                    has_body_masks: false,
739                    has_mouth: false,
740                    has_frame_times: false,
741                    hma_off: 0,
742                    hma_len: 0,
743                    aligned_vertices: false,
744                    vertex_soa: false,
745                },
746                true,
747            );
748        }
749
750        (
751            Model {
752                data: &[],
753                n_verts,
754                n_tris,
755                n_frames,
756                n_clips,
757                clips_off,
758                frame_times_off,
759                ranges_off,
760                vertices_off,
761                vertices_z_off,
762                poses_off,
763                n_bones,
764                n_ranges,
765                tri_off,
766                tri_sz,
767                normal_encoding: if full_normals {
768                    NORMAL_I8X3
769                } else if requested_packed_normals {
770                    NORMAL_PACKED_555
771                } else {
772                    NORMAL_NONE
773                },
774                local_to_world_q12,
775                mouth_xforms_off,
776                hitboxes_off,
777                n_hitboxes,
778                has_body_masks: requested_body_masks,
779                has_mouth: requested_mouth,
780                has_frame_times: requested_frame_times,
781                hma_off,
782                hma_len,
783                aligned_vertices: (data.as_ptr() as usize + vertices_off) & 1 == 0,
784                vertex_soa: requested_vertex_soa,
785            },
786            true,
787        )
788    }
789
790    /// HMA1 local-space tracks, when the cook emitted them (validated at
791    /// load). Most callers want [`Model::pose`], which picks the right path.
792    #[doc(alias = "hma1")]
793    #[inline]
794    pub fn tracks(&self) -> Option<Tracks> {
795        if self.hma_len == 0 {
796            return None;
797        }
798        let data: &'static [u8] = self.data;
799        let off = self.hma_off;
800        let (map_off, blob_off, end) = hma1_section_layout(data, off, self.hma_len)?;
801        // The section's own bone count, which load matched to the model's.
802        let n_used = u16::from_le_bytes([data[off + 4], data[off + 5]]) as usize;
803        let blob = data.get(blob_off..end)?;
804        let jaw_bone = data[off + 6];
805        let le_i16 = |o: usize| i16::from_le_bytes([data[o], data[o + 1]]);
806        Some(Tracks {
807            // SAFETY: `hma_off`/`hma_len` are private and only set by load
808            // after `hma1_section_valid` passed this blob to `Model::new`.
809            model: unsafe { crate::hma1::Model::new_unchecked(blob) },
810            map: data.get(map_off..map_off + n_used)?,
811            jaw: if jaw_bone == 0xff {
812                None
813            } else {
814                Some(Jaw {
815                    bone: jaw_bone as usize,
816                    post: data[off + 7] & 1 != 0,
817                    open: [
818                        le_i16(off + 8),
819                        le_i16(off + 10),
820                        le_i16(off + 12),
821                        le_i16(off + 14),
822                    ],
823                })
824            },
825        })
826    }
827
828    /// True when the model animates from HMA1 tracks. Its phase functions
829    /// then return `(clip, clip, pos_q8)`: `pos_q8` is the source position in
830    /// frames * 256, and [`Model::pose`] reads the triple the same way.
831    #[inline]
832    pub fn has_tracks(&self) -> bool {
833        self.hma_len != 0
834    }
835
836    /// The bone transforms for one animation sample, whichever animation the
837    /// model carries: a `(frame, frame2, frac16)` palette pair, or for HMA1
838    /// models `(clip, _, pos_q8)` decoded into `scratch` with the mouth
839    /// controller opened `mouth`/64 on the jaw. A model with more HMA1 bones
840    /// than `scratch` holds draws its bind pose.
841    #[inline]
842    pub fn pose<'a>(
843        &'a self,
844        frame: usize,
845        frame2: usize,
846        frac16: u32,
847        mouth: u8,
848        scratch: &'a mut [crate::hma1::Affine],
849    ) -> Pose<'a> {
850        let Some(tracks) = self.tracks() else {
851            return Pose::Palette(self.frame(frame).interpolate(self.frame(frame2), frac16));
852        };
853        if tracks.model.bone_count() > scratch.len() {
854            return Pose::Palette(self.frame(0).interpolate(self.frame(0), 0));
855        }
856        let jaw = match tracks.jaw {
857            Some(j) => crate::hma1::Jaw::open(j.bone, j.post, j.open, mouth),
858            None => crate::hma1::Jaw::NONE,
859        };
860        let clip = frame.min(self.clip_count().saturating_sub(1));
861        tracks.model.decode_with(clip, frac16, &jaw, scratch);
862        Pose::Tracks {
863            map: tracks.map,
864            bones: scratch,
865        }
866    }
867
868    /// The sample holding `clip`'s final pose.
869    #[inline]
870    pub fn clip_end_pose(&self, clip: usize) -> (usize, usize, u32) {
871        let clip = clip.min(self.clip_count().saturating_sub(1));
872        if let Some(tracks) = self.tracks() {
873            return (clip, clip, tracks.model.clip_intervals(clip) * 256);
874        }
875        let frame = self.clip_frame(clip, self.clip_len(clip) - 1);
876        (frame, frame, 0)
877    }
878
879    #[inline]
880    pub fn local_to_world_q12(&self) -> u16 {
881        self.local_to_world_q12
882    }
883
884    /// `(first_frame, frame_count)` words of `clip`'s record, clamped to the
885    /// last clip, or `None` when the model has no readable clip table.
886    #[inline]
887    fn clip_rec(&self, clip: usize) -> Option<(u16, u16)> {
888        if self.clips_off == 0 {
889            return None;
890        }
891        let o = self.clips_off + clip.min(self.clip_count().saturating_sub(1)) * 4;
892        if !fits(self.data, o, 4) {
893            return None;
894        }
895        // SAFETY: `fits` checked the four-byte record.
896        Some(unsafe { (rd_u16(self.data, o), rd_u16(self.data, o + 2)) })
897    }
898
899    #[inline]
900    pub fn clip_len(&self, clip: usize) -> usize {
901        let Some((_, count)) = self.clip_rec(clip) else {
902            return self.frame_count().max(1);
903        };
904        ((count & CLIP_FRAME_COUNT_MASK) as usize).max(1)
905    }
906
907    /// GoldSrc source-sequence duration at the 20 Hz game clock. HMD8 cooks
908    /// pack 100 ms quanta in ClipRec.frame_count's unused high byte and
909    /// duration bit 8 in ClipRec.first_frame's unused high bit. A zero duration
910    /// falls back safely instead of allowing a malformed clip to divide by zero.
911    #[inline]
912    pub fn clip_hold_ticks(&self, clip: usize) -> u16 {
913        let Some((packed_first, count)) = self.clip_rec(clip) else {
914            return DEFAULT_CLIP_HOLD_TICKS;
915        };
916        let quanta = (count >> 8)
917            | if packed_first & CLIP_DURATION_EXT_BIT != 0 {
918                0x0100
919            } else {
920                0
921            };
922        if quanta == 0 {
923            DEFAULT_CLIP_HOLD_TICKS
924        } else {
925            quanta.saturating_mul(2)
926        }
927    }
928
929    #[inline]
930    pub fn clip_frame(&self, clip: usize, local_frame: usize) -> usize {
931        let Some((first, count)) = self.clip_rec(clip) else {
932            return local_frame % self.frame_count().max(1);
933        };
934        let first = (first & CLIP_FIRST_FRAME_MASK) as usize;
935        let count = ((count & CLIP_FRAME_COUNT_MASK) as usize).max(1);
936        (first + (local_frame % count)).min(self.frame_count().saturating_sub(1))
937    }
938
939    /// Select two retained palettes and their interpolation fraction using the
940    /// exact source-frame phases emitted by the cooker. Non-uniform, motion-
941    /// selected poses therefore retain their original timing instead of being
942    /// stretched evenly over the clip.
943    #[inline]
944    fn timed_clip_phase(
945        &self,
946        clip: usize,
947        duration: usize,
948        elapsed: usize,
949        looping: bool,
950    ) -> (usize, usize, u32) {
951        let clip = clip.min(self.clip_count().saturating_sub(1));
952        if let Some(tracks) = self.tracks() {
953            // GoldSrc spreads a cycle over numframes - 1 intervals, looping
954            // or not; HMA1 positions are source frames * 256.
955            let span = tracks.model.clip_intervals(clip) as usize * 256;
956            let duration = duration.max(1);
957            let elapsed = if looping {
958                elapsed % duration
959            } else {
960                elapsed.min(duration)
961            };
962            let pos = elapsed.saturating_mul(span) / duration;
963            return (clip, clip, pos as u32);
964        }
965        let len = self.clip_len(clip);
966        if len <= 1 {
967            let frame = self.clip_frame(clip, 0);
968            return (frame, frame, 0);
969        }
970        if !self.has_frame_times {
971            let duration = duration.max(1);
972            let span16 = if looping { len * 16 } else { (len - 1) * 16 };
973            let phase16 = if looping {
974                (elapsed % duration).saturating_mul(span16) / duration
975            } else {
976                elapsed.min(duration).saturating_mul(span16) / duration
977            };
978            let local = (phase16 / 16).min(len - 1);
979            let next = if looping {
980                (local + 1) % len
981            } else {
982                (local + 1).min(len - 1)
983            };
984            return (
985                self.clip_frame(clip, local),
986                self.clip_frame(clip, next),
987                (phase16 & 15) as u32,
988            );
989        }
990
991        let duration = duration.max(1);
992        let target = if looping {
993            ((elapsed % duration).saturating_mul(256) / duration).min(255)
994        } else {
995            (elapsed.min(duration).saturating_mul(255) / duration).min(255)
996        };
997        let first = self.clip_frame(clip, 0);
998        // A clip record may claim more frames than the cook emitted (`clip_frame` clamps for
999        // the same reason), so the time lookup clamps to the table: frame `n_frames - 1` is
1000        // the last entry, which load placed inside the blob.
1001        let last_frame = self.frame_count().saturating_sub(1);
1002        let time = |local: usize| {
1003            self.data[self.frame_times_off + (first + local).min(last_frame)] as usize
1004        };
1005        let mut left = 0usize;
1006        while left + 1 < len && time(left + 1) <= target {
1007            left += 1;
1008        }
1009        let (right, left_time, right_time, target_time) = if looping && left + 1 == len {
1010            (0, time(left), time(0) + 256, target)
1011        } else {
1012            let right = (left + 1).min(len - 1);
1013            (right, time(left), time(right), target)
1014        };
1015        let denominator = right_time.saturating_sub(left_time);
1016        let frac16 = if denominator == 0 {
1017            0
1018        } else {
1019            target_time
1020                .saturating_sub(left_time)
1021                .saturating_mul(16)
1022                .checked_div(denominator)
1023                .unwrap_or(0)
1024                .min(15) as u32
1025        };
1026        (
1027            self.clip_frame(clip, left),
1028            self.clip_frame(clip, right),
1029            frac16,
1030        )
1031    }
1032
1033    #[inline]
1034    pub fn looped_clip_phase(
1035        &self,
1036        clip: usize,
1037        duration: usize,
1038        elapsed: usize,
1039    ) -> (usize, usize, u32) {
1040        self.timed_clip_phase(clip, duration, elapsed, true)
1041    }
1042
1043    #[inline]
1044    pub fn one_shot_clip_phase(
1045        &self,
1046        clip: usize,
1047        duration: usize,
1048        elapsed: usize,
1049    ) -> (usize, usize, u32) {
1050        self.timed_clip_phase(clip, duration, elapsed, false)
1051    }
1052
1053    #[inline]
1054    pub fn frame(&self, frame: usize) -> ModelFrame<'_> {
1055        let f = if frame < self.frame_count() { frame } else { 0 };
1056        ModelFrame {
1057            data: self.data,
1058            poses_off: self.poses_off,
1059            n_bones: self.bone_count(),
1060            frame_idx: f,
1061            mouth_xforms_off: self.mouth_xforms_off,
1062            has_mouth: self.has_mouth,
1063        }
1064    }
1065
1066    /// Bone range `index`, clamped to the last range. A model without ranges
1067    /// (the empty model) returns an empty range.
1068    #[inline]
1069    pub fn bone_range(&self, index: usize) -> BoneRange {
1070        let index = index.min(self.bone_range_count().saturating_sub(1));
1071        let o = self.ranges_off + index * HMD7_RANGE_BYTES;
1072        if !fits(self.data, o, HMD7_RANGE_BYTES) {
1073            return BoneRange {
1074                first: 0,
1075                count: 0,
1076                bone: 0,
1077                body_mask: 0,
1078                mouth: false,
1079            };
1080        }
1081        // SAFETY: `fits` checked the eight-byte record.
1082        let (first, count, bone) = unsafe {
1083            (
1084                rd_u16(self.data, o) as usize,
1085                rd_u16(self.data, o + 2) as usize,
1086                rd_u16(self.data, o + 4) as usize,
1087            )
1088        };
1089        BoneRange {
1090            first,
1091            count,
1092            bone,
1093            body_mask: self.data[o + 6],
1094            mouth: self.data[o + 7] & HMD7_RANGE_MOUTH != 0,
1095        }
1096    }
1097
1098    /// Studio hitbox `index`, clamped to the last one. A model without
1099    /// hitboxes returns a zero box on bone 0.
1100    #[inline]
1101    pub fn hitbox(&self, index: usize) -> StudioHitbox {
1102        let index = index.min(self.hitbox_count().saturating_sub(1));
1103        let o = self.hitboxes_off + index * HMD7_HITBOX_BYTES;
1104        if self.hitbox_count() == 0 || !fits(self.data, o, HMD7_HITBOX_BYTES) {
1105            return StudioHitbox {
1106                bone: 0,
1107                bbmin: Vec3I16::ZERO,
1108                bbmax: Vec3I16::ZERO,
1109            };
1110        }
1111        // SAFETY: `fits` checked the fourteen-byte record.
1112        unsafe {
1113            StudioHitbox {
1114                bone: rd_u16(self.data, o) as usize,
1115                bbmin: Vec3I16::new(
1116                    rd_i16(self.data, o + 2),
1117                    rd_i16(self.data, o + 4),
1118                    rd_i16(self.data, o + 6),
1119                ),
1120                bbmax: Vec3I16::new(
1121                    rd_i16(self.data, o + 8),
1122                    rd_i16(self.data, o + 10),
1123                    rd_i16(self.data, o + 12),
1124                ),
1125            }
1126        }
1127    }
1128
1129    /// True when vertex `index`'s record lies inside the blob: for every
1130    /// `index < vertex_count()` of a loaded model. Memory safety rests on this
1131    /// check alone, never on the public counts.
1132    #[inline(always)]
1133    fn vert_in_blob(&self, index: usize) -> bool {
1134        if self.vertex_soa {
1135            fits(self.data, self.vertices_off + index * 4, 4)
1136                && fits(self.data, self.vertices_z_off + index * 2, 2)
1137        } else {
1138            fits(self.data, self.vertices_off + index * 6, 6)
1139        }
1140    }
1141
1142    /// Bone-local position of vertex `index`. An index past the vertex
1143    /// stream reads as the origin instead of outside the blob; hot loops that
1144    /// have already bounded the index can use [`Self::vertex_unchecked`].
1145    #[inline]
1146    pub fn vertex(&self, index: usize) -> Vec3I16 {
1147        if !self.vert_in_blob(index) {
1148            return Vec3I16::ZERO;
1149        }
1150        // SAFETY: `vert_in_blob` checked this vertex's record.
1151        unsafe { self.vertex_unchecked(index) }
1152    }
1153
1154    /// [`Self::vertex`] without the bounds check.
1155    ///
1156    /// # Safety
1157    /// `index` must be below the [`Model::vertex_count`] that
1158    /// [`Model::from_bytes`] gave this model. (The public count can be
1159    /// overwritten; the bound is the loaded value.)
1160    #[inline]
1161    pub unsafe fn vertex_unchecked(&self, index: usize) -> Vec3I16 {
1162        if self.vertex_soa {
1163            let xy = self.vertices_off + index * 4;
1164            let z = self.vertices_z_off + index * 2;
1165            // SAFETY: load placed both SoA streams, `vertex_count()` records long,
1166            // inside the blob, and the caller keeps `index` below that.
1167            return unsafe {
1168                Vec3I16::new(
1169                    rd_i16(self.data, xy),
1170                    rd_i16(self.data, xy + 2),
1171                    rd_i16(self.data, z),
1172                )
1173            };
1174        }
1175        #[allow(unused_variables)] // read only by the MIPS path below
1176        let o = self.vertices_off + index * 6;
1177        #[cfg(target_arch = "mips")]
1178        if self.aligned_vertices {
1179            // New HMD8 cooks halfword-align this six-byte stream. Native
1180            // LH loads replace six LBU operations plus their shifts/ORs in
1181            // the per-RTPT model projection loop. The fallback below keeps
1182            // an old cached HMD8 blob playable without requiring a recook.
1183            // SAFETY: the six-byte record is in the blob (as below), and
1184            // `aligned_vertices` means load saw `data + vertices_off` even;
1185            // `index * 6` keeps it even, so the i16 loads are aligned.
1186            unsafe {
1187                let p = self.data.as_ptr().add(o).cast::<i16>();
1188                return Vec3I16::new(p.read(), p.add(1).read(), p.add(2).read());
1189            }
1190        }
1191        // SAFETY: load placed the `vertex_count() * 6` byte stream inside the
1192        // blob, and the caller keeps `index` below `vertex_count()`.
1193        unsafe {
1194            Vec3I16::new(
1195                rd_i16(self.data, o),
1196                rd_i16(self.data, o + 2),
1197                rd_i16(self.data, o + 4),
1198            )
1199        }
1200    }
1201
1202    /// Vertex `index` packed as the GTE's VXY/VZ register words. An index
1203    /// past the vertex stream reads as the origin; see
1204    /// [`Self::vertex_gte_words_unchecked`] for bounded hot loops.
1205    #[inline]
1206    pub fn vertex_gte_words(&self, index: usize) -> GteVertexWords {
1207        if !self.vert_in_blob(index) {
1208            return GteVertexWords { xy: 0, z: 0 };
1209        }
1210        // SAFETY: `vert_in_blob` checked this vertex's record.
1211        unsafe { self.vertex_gte_words_unchecked(index) }
1212    }
1213
1214    /// The whole vertex stream as a bounds-checked-once view; see
1215    /// [`VertexWords`]. Hot loops that walk a bone range read it through
1216    /// [`VertexWords::slice`] and [`VertexWords::triples`] instead of
1217    /// calling [`Self::vertex_gte_words`] per index.
1218    ///
1219    /// Only the struct-of-arrays stream (`HMD_FLAG_VERTEX_SOA`, which every
1220    /// current cook sets) has this shape. An older interleaved cook, and the
1221    /// empty or a rejected model, return an empty view; read those through
1222    /// the per-index accessors.
1223    #[inline]
1224    pub fn vertex_words(&self) -> VertexWords<'static> {
1225        if !self.vertex_soa {
1226            return VertexWords::EMPTY;
1227        }
1228        // The loaded count, from private offsets (the public count can be
1229        // overwritten): `from_bytes` put the Z run right after `count` XY words.
1230        let count = self.vertices_z_off.saturating_sub(self.vertices_off) / 4;
1231        let base = self.data.as_ptr() as usize;
1232        let in_blob = fits(self.data, self.vertices_off, count * 4)
1233            && fits(self.data, self.vertices_z_off, count * 2);
1234        let aligned = base
1235            .wrapping_add(self.vertices_off)
1236            .is_multiple_of(core::mem::align_of::<u32>())
1237            && base
1238                .wrapping_add(self.vertices_z_off)
1239                .is_multiple_of(core::mem::align_of::<i16>());
1240        // `from_bytes` proved both facts for every model with `vertex_soa`
1241        // set; a change to its validation that stops proving them trips here.
1242        debug_assert!(in_blob, "HMD8 SoA vertex stream outside the blob");
1243        debug_assert!(aligned, "HMD8 SoA vertex stream misaligned");
1244        if !(in_blob && aligned) {
1245            return VertexWords::EMPTY;
1246        }
1247        let start = self.data.as_ptr();
1248        // SAFETY: `in_blob` checked that `count` u32s at `vertices_off` and
1249        // `count` i16s at `vertices_z_off` lie inside `data`, a `&'static`
1250        // shared slice nothing can write through, and `aligned` checked both
1251        // starts against the element alignment. Every bit pattern is a valid
1252        // u32 and i16. Neither check can fail for a model `from_bytes`
1253        // accepted with `vertex_soa`: it rejects a SoA blob whose
1254        // `data + vertices_off` is not 4-aligned (the `& 3 == 0` term of
1255        // `valid`), places `vertices_z_off` at `vertices_off + n_verts * 4`
1256        // (so it is 4-aligned too), and requires `poses_off <= tri_off <=
1257        // data.len()` with `poses_off = vertices_off + n_verts * 6`, which
1258        // covers both runs.
1259        unsafe {
1260            VertexWords {
1261                xy: core::slice::from_raw_parts(start.add(self.vertices_off).cast::<u32>(), count),
1262                z: core::slice::from_raw_parts(start.add(self.vertices_z_off).cast::<i16>(), count),
1263            }
1264        }
1265    }
1266
1267    /// [`Self::vertex_gte_words`] without the bounds check.
1268    ///
1269    /// # Safety
1270    /// Same as [`Self::vertex_unchecked`].
1271    #[inline]
1272    pub unsafe fn vertex_gte_words_unchecked(&self, index: usize) -> GteVertexWords {
1273        if self.vertex_soa {
1274            let xy = self.vertices_off + index * 4;
1275            let z = self.vertices_z_off + index * 2;
1276            #[cfg(target_arch = "mips")]
1277            // SAFETY: both records are in the blob (caller's bound on
1278            // `index`). `data` is a byte slice, so a normal raw read can
1279            // retain align=1 in LLVM even after the cast and become LWL/LWR.
1280            // HMD_FLAG_VERTEX_SOA is validated against the actual base
1281            // address at load, making these native aligned loads sound.
1282            unsafe {
1283                return GteVertexWords {
1284                    xy: core::ptr::read_volatile(self.data.as_ptr().add(xy).cast::<u32>()),
1285                    z: core::ptr::read_volatile(self.data.as_ptr().add(z).cast::<i16>()) as i32
1286                        as u32,
1287                };
1288            }
1289            #[cfg(not(target_arch = "mips"))]
1290            // SAFETY: both records are in the blob (caller's bound on `index`).
1291            return unsafe {
1292                GteVertexWords {
1293                    xy: rd_u32(self.data, xy),
1294                    z: rd_i16(self.data, z) as i32 as u32,
1295                }
1296            };
1297        }
1298        #[allow(unused_variables)] // read only by the MIPS path below
1299        let o = self.vertices_off + index * 6;
1300        #[cfg(target_arch = "mips")]
1301        // SAFETY: the six-byte record is in the blob (caller's bound on
1302        // `index`); the XY word load is unaligned, and the Z load is aligned
1303        // only when `aligned_vertices` says load saw an even base.
1304        unsafe {
1305            let p = self.data.as_ptr().add(o);
1306            let xy = p.cast::<u32>().read_unaligned();
1307            let z = if self.aligned_vertices {
1308                p.add(4).cast::<i16>().read()
1309            } else {
1310                rd_i16(self.data, o + 4)
1311            };
1312            GteVertexWords {
1313                xy,
1314                z: z as i32 as u32,
1315            }
1316        }
1317        #[cfg(not(target_arch = "mips"))]
1318        {
1319            // SAFETY: same contract as this function's.
1320            let v = unsafe { self.vertex_unchecked(index) };
1321            GteVertexWords {
1322                xy: ((v.y as u16 as u32) << 16) | v.x as u16 as u32,
1323                z: v.z as i32 as u32,
1324            }
1325        }
1326    }
1327
1328    #[inline]
1329    /// Byte length of the header + clips + ranges + static vertices + palettes --
1330    /// everything the per-frame draw needs. The TriRec/texture tail after it is
1331    /// only read by `fill_render_faces_split_raw`/`triangle()` (load-time or viewmodels),
1332    /// so the enemy pool can drop it once the faces are baked.
1333    pub fn frame_section_len(&self) -> usize {
1334        self.tri_off
1335    }
1336
1337    /// Compact the HMD8 blob in `blob`, in place, to the bodies in
1338    /// `visible_bodies`: whole bone/body ranges and their static vertices are
1339    /// dropped, and the shared pose palette, mouth transforms, hitboxes and
1340    /// HMA1 tracks move down unchanged. The triangle tail is not kept, so bake
1341    /// faces before compacting. Parse the result again with
1342    /// [`Model::from_bytes`] over `blob[..len]`.
1343    ///
1344    /// `remap` receives, for every old vertex index, its new index or
1345    /// `u16::MAX` when it was dropped; it needs one entry per vertex.
1346    ///
1347    /// Returns the compact frame-section length, or `None` when `blob` is not
1348    /// a valid HMD8 model with body masks, `remap` is too short, or no
1349    /// visible body remains. `blob` may be partly rewritten on `None` after
1350    /// the range pass has started.
1351    ///
1352    /// The buffer is borrowed exclusively, so no [`Model`] can read it while
1353    /// it changes: a model parsed from these bytes earlier must not be used
1354    /// again.
1355    pub fn compact_visible_bodies(
1356        blob: &mut [u8],
1357        visible_bodies: u8,
1358        remap: &mut [u16],
1359    ) -> Option<usize> {
1360        let (layout, _) = Self::layout(blob, DEFAULT_MAX_VERTICES);
1361        let old_n = layout.vertex_count();
1362        if !layout.has_body_masks
1363            || old_n == 0
1364            || old_n > remap.len()
1365            || layout.tri_off > blob.len()
1366        {
1367            return None;
1368        }
1369        let remap = &mut remap[..old_n];
1370        remap.fill(u16::MAX);
1371        let mut new_n = 0usize;
1372        let mut new_ranges = 0usize;
1373        for range in 0..layout.bone_range_count() {
1374            let o = layout.ranges_off + range * HMD7_RANGE_BYTES;
1375            let record = blob.get(o..o + HMD7_RANGE_BYTES)?;
1376            let first = u16::from_le_bytes([record[0], record[1]]) as usize;
1377            let count = u16::from_le_bytes([record[2], record[3]]) as usize;
1378            if record[6] & visible_bodies == 0 {
1379                continue;
1380            }
1381            // Ranges only move down, so this record is read before anything
1382            // overwrites it and later records are still in place.
1383            let dst = layout.ranges_off + new_ranges * HMD7_RANGE_BYTES;
1384            blob.copy_within(o..o + HMD7_RANGE_BYTES, dst);
1385            blob[dst..dst + 2].copy_from_slice(&(new_n as u16).to_le_bytes());
1386            for slot in remap.get_mut(first..first + count)? {
1387                *slot = new_n as u16;
1388                new_n += 1;
1389            }
1390            new_ranges += 1;
1391        }
1392        if new_n == 0 || new_ranges == 0 {
1393            return None;
1394        }
1395        let dst = layout.compact_vertices(blob, remap, new_ranges, new_n)?;
1396        layout.compact_tail(blob, dst, new_ranges, new_n)
1397    }
1398
1399    /// Move the kept vertices of `self`'s layout down behind the kept
1400    /// ranges; returns the end of the vertex data.
1401    fn compact_vertices(
1402        &self,
1403        blob: &mut [u8],
1404        remap: &[u16],
1405        new_ranges: usize,
1406        new_n: usize,
1407    ) -> Option<usize> {
1408        let kept = || remap.iter().enumerate().filter(|(_, &new)| new != u16::MAX);
1409        let new_vertices_off = self.ranges_off + new_ranges * HMD7_RANGE_BYTES;
1410        if !self.vertex_soa {
1411            let mut dst = new_vertices_off;
1412            for (old, _) in kept() {
1413                move_within(blob, self.vertices_off + old * 6, dst, 6)?;
1414                dst += 6;
1415            }
1416            return Some(dst);
1417        }
1418        // Move the hot XY stream before the old Z stream can be overwritten,
1419        // then compact Z straight after the kept XY: six bytes per vertex,
1420        // as before.
1421        let new_vertices_z_off = new_vertices_off + new_n * 4;
1422        for (new, (old, _)) in kept().enumerate() {
1423            move_within(
1424                blob,
1425                self.vertices_off + old * 4,
1426                new_vertices_off + new * 4,
1427                4,
1428            )?;
1429        }
1430        for (new, (old, _)) in kept().enumerate() {
1431            move_within(
1432                blob,
1433                self.vertices_z_off + old * 2,
1434                new_vertices_z_off + new * 2,
1435                2,
1436            )?;
1437        }
1438        Some(new_vertices_z_off + new_n * 2)
1439    }
1440
1441    /// Move the pose palette, mouth transforms, hitboxes and HMA1 tracks of
1442    /// `self`'s layout down to `dst`, then rewrite the header's vertex count,
1443    /// model data length and range count. Returns the new frame-section end.
1444    fn compact_tail(
1445        &self,
1446        blob: &mut [u8],
1447        mut dst: usize,
1448        new_ranges: usize,
1449        new_n: usize,
1450    ) -> Option<usize> {
1451        let pose_len = self
1452            .frame_count()
1453            .checked_mul(self.bone_count())?
1454            .checked_mul(HMD8_AFFINE_BYTES)?;
1455        let mouth_len = if self.has_mouth {
1456            self.frame_count().checked_mul(MOUTH_XFORM_BYTES)?
1457        } else {
1458            0
1459        };
1460        let hitbox_len = self.hitbox_count().checked_mul(HMD7_HITBOX_BYTES)?;
1461        for (src, len) in [
1462            (self.poses_off, pose_len),
1463            (self.mouth_xforms_off, mouth_len),
1464            (self.hitboxes_off, hitbox_len),
1465            // HMA1 tracks are per bone, not per vertex: they move intact.
1466            (self.hma_off, self.hma_len),
1467        ] {
1468            if len == 0 {
1469                continue;
1470            }
1471            if src.checked_add(len)? > self.tri_off || dst.checked_add(len)? > blob.len() {
1472                return None;
1473            }
1474            move_within(blob, src, dst, len)?;
1475            dst += len;
1476        }
1477        let model_data_len = u32::try_from(dst.checked_sub(self.ranges_off)?).ok()?;
1478        let new_n = u32::try_from(new_n).ok()?;
1479        blob[4..8].copy_from_slice(&new_n.to_le_bytes());
1480        blob[24..28].copy_from_slice(&model_data_len.to_le_bytes());
1481        blob[34..36].copy_from_slice(&(new_ranges as u16).to_le_bytes());
1482        Some(dst)
1483    }
1484
1485    /// True when triangle `t`'s record lies inside the blob: for every
1486    /// `t < triangle_count()` of a loaded model. Memory safety rests on this
1487    /// check alone, never on the public counts.
1488    #[inline(always)]
1489    fn tri_in_blob(&self, t: usize) -> bool {
1490        // `tri_sz` is TRI_SZ or TRI_SZ_FULL_NORMALS in every model (EMPTY
1491        // included), so this covers every byte `triangle_unchecked` reads.
1492        fits(self.data, self.tri_off + t * self.tri_sz, self.tri_sz)
1493    }
1494
1495    /// Triangle `t`, unpacked. An index past the triangle table reads as a
1496    /// degenerate triangle with an empty body mask, which every renderer
1497    /// skips; hot loops that have already bounded `t` can use
1498    /// [`Self::triangle_unchecked`].
1499    #[inline(always)]
1500    pub fn triangle(&self, t: usize) -> Triangle {
1501        if !self.tri_in_blob(t) {
1502            return Triangle {
1503                idx: [0; 3],
1504                tex: 0,
1505                uv: [(0, 0); 3],
1506                normal: [0; 3],
1507                body_mask: 0,
1508            };
1509        }
1510        // SAFETY: `tri_in_blob` checked this triangle's record.
1511        unsafe { self.triangle_unchecked(t) }
1512    }
1513
1514    /// [`Self::triangle`] without the bounds check.
1515    ///
1516    /// # Safety
1517    /// `t` must be below the [`Model::triangle_count`] that
1518    /// [`Model::from_bytes`] gave this model.
1519    /// (The public count can be overwritten; the bound is the loaded value.)
1520    #[inline(always)]
1521    pub unsafe fn triangle_unchecked(&self, t: usize) -> Triangle {
1522        let o = self.tri_off + t * self.tri_sz;
1523        let d = self.data;
1524        // Raw byte reads avoid Rust's unsafe-precondition guards, which
1525        // current nightly still emits for every `get_unchecked`/slice index
1526        // in this per-face hot loop.
1527        // SAFETY: load placed the `triangle_count() * tri_sz` byte table
1528        // inside the blob and the caller keeps `t` below `triangle_count()`,
1529        // so the whole record is readable. Every offset below stays inside
1530        // it: `tri_sz` is 20 with i8 normals (last byte read: 17) and 16
1531        // otherwise (last: 15).
1532        unsafe {
1533            let p = d.as_ptr().add(o);
1534            Triangle {
1535                idx: [rd_u16(d, o), rd_u16(d, o + 2), rd_u16(d, o + 4)],
1536                tex: rd_u16(d, o + 6) as usize,
1537                uv: [
1538                    (p.add(8).read(), p.add(9).read()),
1539                    (p.add(10).read(), p.add(11).read()),
1540                    (p.add(12).read(), p.add(13).read()),
1541                ],
1542                normal: match self.normal_encoding {
1543                    NORMAL_I8X3 => [
1544                        p.add(14).read() as i8,
1545                        p.add(15).read() as i8,
1546                        p.add(16).read() as i8,
1547                    ],
1548                    NORMAL_PACKED_555 => unpack_normal_555(rd_u16(d, o + 14)),
1549                    _ => [0; 3],
1550                },
1551                body_mask: if self.has_body_masks {
1552                    p.add(if self.normal_encoding == NORMAL_I8X3 {
1553                        17
1554                    } else {
1555                        14
1556                    })
1557                    .read()
1558                } else {
1559                    0xff
1560                },
1561            }
1562        }
1563    }
1564
1565    /// Packet-order UV words for a triangle whose other steady-render fields
1566    /// are already in the viewmodel pose cache. An index past the triangle
1567    /// table reads as zero; see [`Self::triangle_uv_words_unchecked`].
1568    #[inline(always)]
1569    pub fn triangle_uv_words(&self, t: usize) -> [u16; 3] {
1570        if !self.tri_in_blob(t) {
1571            return [0; 3];
1572        }
1573        // SAFETY: `tri_in_blob` checked this triangle's record.
1574        unsafe { self.triangle_uv_words_unchecked(t) }
1575    }
1576
1577    /// [`Self::triangle_uv_words`] without the bounds check.
1578    ///
1579    /// # Safety
1580    /// Same as [`Self::triangle_unchecked`].
1581    #[inline(always)]
1582    pub unsafe fn triangle_uv_words_unchecked(&self, t: usize) -> [u16; 3] {
1583        let o = self.tri_off + t * self.tri_sz + 8;
1584        // SAFETY: bytes 8..14 of a record the caller's bound keeps in the
1585        // blob.
1586        unsafe {
1587            [
1588                rd_u16(self.data, o),
1589                rd_u16(self.data, o + 2),
1590                rd_u16(self.data, o + 4),
1591            ]
1592        }
1593    }
1594
1595    #[inline]
1596    pub fn has_body_masks(&self) -> bool {
1597        self.has_body_masks
1598    }
1599
1600    /// Triangle `t`'s face normal. An index past the triangle table reads as
1601    /// zero; see [`Self::triangle_normal_unchecked`].
1602    #[inline]
1603    pub fn triangle_normal(&self, t: usize) -> [i8; 3] {
1604        if !self.tri_in_blob(t) {
1605            return [0; 3];
1606        }
1607        // SAFETY: `tri_in_blob` checked this triangle's record.
1608        unsafe { self.triangle_normal_unchecked(t) }
1609    }
1610
1611    /// [`Self::triangle_normal`] without the bounds check.
1612    ///
1613    /// # Safety
1614    /// Same as [`Self::triangle_unchecked`].
1615    #[inline]
1616    pub unsafe fn triangle_normal_unchecked(&self, t: usize) -> [i8; 3] {
1617        let o = self.tri_off + t * self.tri_sz + 14;
1618        let p = self.data.as_ptr();
1619        // SAFETY: bytes 14..17 (i8 normals, 20-byte records) or 14..16
1620        // (packed normals) of a record the caller's bound keeps in the blob.
1621        unsafe {
1622            match self.normal_encoding {
1623                NORMAL_I8X3 => [
1624                    p.add(o).read() as i8,
1625                    p.add(o + 1).read() as i8,
1626                    p.add(o + 2).read() as i8,
1627                ],
1628                NORMAL_PACKED_555 => unpack_normal_555(rd_u16(self.data, o)),
1629                _ => [0; 3],
1630            }
1631        }
1632    }
1633
1634    /// Number of consecutive texture/body-mask runs in authored face order.
1635    pub fn render_face_count(&self, visible_bodies: u8) -> usize {
1636        let mut count = 0usize;
1637        let mut t = 0usize;
1638        while t < self.triangle_count() {
1639            if self.triangle(t).body_mask & visible_bodies != 0 {
1640                count += 1;
1641            }
1642            t += 1;
1643        }
1644        count
1645    }
1646
1647    pub fn render_texture_mask(&self, visible_bodies: u8) -> u32 {
1648        let mut mask = 0u32;
1649        let mut t = 0usize;
1650        while t < self.triangle_count() {
1651            let tri = self.triangle(t);
1652            if tri.body_mask & visible_bodies != 0 && tri.tex < 32 {
1653                mask |= 1u32 << tri.tex;
1654            }
1655            t += 1;
1656        }
1657        mask
1658    }
1659
1660    pub fn render_face_run_count(&self, visible_bodies: u8) -> usize {
1661        let mut runs = 0usize;
1662        let mut last_tex = usize::MAX;
1663        let mut last_mask = 0u8;
1664        let mut t = 0usize;
1665        while t < self.triangle_count() {
1666            let tri = self.triangle(t);
1667            if tri.body_mask & visible_bodies == 0 {
1668                t += 1;
1669                continue;
1670            }
1671            if tri.tex != last_tex || tri.body_mask != last_mask {
1672                runs += 1;
1673                last_tex = tri.tex;
1674                last_mask = tri.body_mask;
1675            }
1676            t += 1;
1677        }
1678        runs
1679    }
1680
1681    /// Split GPU face records into a hot packed-index stream, a cold UV stream,
1682    /// and ordered texture runs. Each run word is
1683    /// `end_face:u16 | tex:u8<<16 | body_mask:u8<<24`; `end_face` is relative
1684    /// to this model. The caller guarantees enough face and run storage.
1685    ///
1686    /// # Safety
1687    ///
1688    /// `indices` and `payloads` must each be writable for `out_len` records.
1689    /// `runs` must be writable for the maximum texture/body run count reported
1690    /// by [`Self::render_face_run_count`], and the outputs must not overlap.
1691    pub unsafe fn fill_render_faces_split_raw(
1692        &self,
1693        indices: *mut u32,
1694        payloads: *mut RenderFacePayload,
1695        runs: *mut u32,
1696        out_len: usize,
1697        visible_bodies: u8,
1698    ) -> (usize, usize) {
1699        debug_assert!(
1700            self.vertex_count() <= RENDER_FACE_INDEX_MASK as usize + 1,
1701            "render faces pack 10-bit vertex indices; this model has more vertices"
1702        );
1703        // SAFETY: contract is the enclosing fn's; see its doc comment.
1704        unsafe {
1705            if self.triangle_count() == 0 || out_len == 0 {
1706                return (0, 0);
1707            }
1708            let mut source_t = 0usize;
1709            let mut out_t = 0usize;
1710            let mut run_count = 0usize;
1711            let mut run_tex = usize::MAX;
1712            let mut run_mask = 0u8;
1713            while source_t < self.triangle_count() && out_t < out_len {
1714                let a = self.triangle(source_t);
1715                source_t += 1;
1716                if a.body_mask & visible_bodies == 0 {
1717                    continue;
1718                }
1719                if a.tex != run_tex || a.body_mask != run_mask {
1720                    if run_count != 0 {
1721                        ptr::write(
1722                            runs.add(run_count - 1),
1723                            (out_t as u32)
1724                                | ((run_tex.min(u8::MAX as usize) as u32) << 16)
1725                                | ((run_mask as u32) << 24),
1726                        );
1727                    }
1728                    run_tex = a.tex;
1729                    run_mask = a.body_mask;
1730                    run_count += 1;
1731                }
1732                write_render_face(indices, payloads, out_t, &a);
1733                out_t += 1;
1734            }
1735            if run_count == 0 {
1736                return (0, 0);
1737            }
1738            ptr::write(
1739                runs.add(run_count - 1),
1740                (out_t as u32)
1741                    | ((run_tex.min(u8::MAX as usize) as u32) << 16)
1742                    | ((run_mask as u32) << 24),
1743            );
1744            (out_t, run_count)
1745        }
1746    }
1747
1748    /// Renamed to [`Model::vertex`].
1749    #[deprecated(note = "renamed to `vertex`")]
1750    #[inline(always)]
1751    pub fn vert(&self, index: usize) -> Vec3I16 {
1752        self.vertex(index)
1753    }
1754
1755    /// Renamed to [`Model::triangle_uv_words`].
1756    #[deprecated(note = "renamed to `triangle_uv_words`")]
1757    #[inline(always)]
1758    pub fn tri_uv_words(&self, t: usize) -> [u16; 3] {
1759        self.triangle_uv_words(t)
1760    }
1761}
1762
1763/// A model's HMA1 section (see [`Model::tracks`]).
1764#[derive(Clone, Copy)]
1765pub struct Tracks {
1766    pub model: crate::hma1::Model,
1767    /// HMD8 range/hitbox bone -> HMA1 bone.
1768    pub map: &'static [u8],
1769    pub jaw: Option<Jaw>,
1770}
1771
1772/// The cooked mouth controller: one bone's local rotation, fully open.
1773#[derive(Clone, Copy)]
1774pub struct Jaw {
1775    pub bone: usize,
1776    pub post: bool,
1777    pub open: [i16; 4],
1778}
1779
1780/// One animation sample's bone transforms (see [`Model::pose`]).
1781#[derive(Clone, Copy)]
1782pub enum Pose<'a> {
1783    Palette(InterpolatedModelFrame<'a>),
1784    Tracks {
1785        map: &'a [u8],
1786        bones: &'a [crate::hma1::Affine],
1787    },
1788}
1789
1790impl Pose<'_> {
1791    /// Model-space transform of HMD8 bone `bone`. Palette poses open the
1792    /// mouth ranges by `mouth` here; track poses already folded the mouth
1793    /// into the jaw bone when they were decoded.
1794    #[inline]
1795    pub fn bone(&self, bone: usize, mouth_range: bool, mouth: u8) -> BoneTransform {
1796        match self {
1797            Pose::Palette(frame) => frame.bone(bone, mouth_range, mouth),
1798            Pose::Tracks { map, bones } => {
1799                let a = &bones[map[bone] as usize];
1800                // Tracks keep a quarter unit of translation; the bone
1801                // transform carries whole cooked units, like the palettes.
1802                let t = |v: i32| ((v + 2) >> 2).clamp(i16::MIN as i32, i16::MAX as i32) as i16;
1803                BoneTransform {
1804                    rotation: Mat3I16 { m: a.r },
1805                    translation: Vec3I16::new(t(a.t[0]), t(a.t[1]), t(a.t[2])),
1806                }
1807            }
1808        }
1809    }
1810}
1811
1812/// `(map_off, blob_off, end)` of the HMA1 section at `off`; `None` when its
1813/// header does not fit `data`.
1814#[inline(always)]
1815fn hma1_section_layout(data: &[u8], off: usize, len: usize) -> Option<(usize, usize, usize)> {
1816    let n_used = u16::from_le_bytes([*data.get(off + 4)?, *data.get(off + 5)?]) as usize;
1817    let map_off = off + HMA1_SECTION_HEADER;
1818    Some((map_off, (map_off + n_used + 1) & !1, off + len))
1819}
1820
1821/// Check of an HMA1 section, run once at load: the tracks pass
1822/// [`crate::hma1::Model::new`] (every read in bounds, parents first), every
1823/// range bone maps to a track bone, the jaw exists, and every clip the HMD8
1824/// table names has tracks.
1825fn hma1_section_valid(data: &[u8], off: usize, len: usize, n_bones: usize, n_clips: usize) -> bool {
1826    if len < HMA1_SECTION_HEADER + 4 {
1827        return false;
1828    }
1829    let Some((map_off, blob_off, end)) = hma1_section_layout(data, off, len) else {
1830        return false;
1831    };
1832    if map_off + n_bones > end || blob_off > end {
1833        return false;
1834    }
1835    let (Some(map), Some(blob), Some(&jaw)) = (
1836        data.get(map_off..map_off + n_bones),
1837        data.get(blob_off..end),
1838        data.get(off + 6),
1839    ) else {
1840        return false;
1841    };
1842    if u16::from_le_bytes([data[off + 4], data[off + 5]]) as usize != n_bones {
1843        return false;
1844    }
1845    let Some((hma_bones, hma_clips, _, _)) = crate::hma1::validated_layout(blob) else {
1846        return false;
1847    };
1848    hma_bones != 0
1849        && hma_clips >= n_clips.max(1)
1850        && map.iter().all(|&bone| (bone as usize) < hma_bones)
1851        && (jaw == 0xff || (jaw as usize) < hma_bones)
1852}
1853
1854impl<'a> ModelFrame<'a> {
1855    #[inline]
1856    pub fn interpolate(self, b: Self, frac16: u32) -> InterpolatedModelFrame<'a> {
1857        InterpolatedModelFrame {
1858            a: self,
1859            b,
1860            frac16: frac16 as i32,
1861        }
1862    }
1863    /// Bone `bone`'s palette entry, clamped to the last bone; identity when
1864    /// the record is not inside the blob (the empty model).
1865    #[inline]
1866    fn bone(&self, bone: usize) -> BoneTransform {
1867        let bone = bone.min(self.n_bones.saturating_sub(1));
1868        let o = self.poses_off + (self.frame_idx * self.n_bones + bone) * HMD8_AFFINE_BYTES;
1869        if !fits(self.data, o, HMD8_AFFINE_BYTES) {
1870            return IDENTITY_TRANSFORM;
1871        }
1872        // SAFETY: `fits` checked the 20-byte record; the last word load
1873        // (pair at `o + 9`) ends at `o + 13` and the last halfword at `o + 20`.
1874        unsafe {
1875            let p0 = rd_q11_pair(self.data, o);
1876            let p1 = rd_q11_pair(self.data, o + 3);
1877            let p2 = rd_q11_pair(self.data, o + 6);
1878            let p3 = rd_q11_pair(self.data, o + 9);
1879            let last = decode_q11(rd_u16(self.data, o + 12) & 0x0fff);
1880            BoneTransform {
1881                rotation: Mat3I16 {
1882                    m: [[p0.0, p0.1, p1.0], [p1.1, p2.0, p2.1], [p3.0, p3.1, last]],
1883                },
1884                translation: Vec3I16::new(
1885                    rd_i16(self.data, o + 14),
1886                    rd_i16(self.data, o + 16),
1887                    rd_i16(self.data, o + 18),
1888                ),
1889            }
1890        }
1891    }
1892
1893    /// The frame's open-mouth transform; identity when it is not inside the
1894    /// blob.
1895    #[inline]
1896    fn mouth_xform(&self) -> BoneTransform {
1897        let o = self.mouth_xforms_off + self.frame_idx * MOUTH_XFORM_BYTES;
1898        if !fits(self.data, o, MOUTH_XFORM_BYTES) {
1899            return IDENTITY_TRANSFORM;
1900        }
1901        let d = self.data;
1902        // SAFETY: `fits` checked the 24-byte record.
1903        unsafe {
1904            BoneTransform {
1905                rotation: Mat3I16 {
1906                    m: [
1907                        [rd_i16(d, o), rd_i16(d, o + 2), rd_i16(d, o + 4)],
1908                        [rd_i16(d, o + 6), rd_i16(d, o + 8), rd_i16(d, o + 10)],
1909                        [rd_i16(d, o + 12), rd_i16(d, o + 14), rd_i16(d, o + 16)],
1910                    ],
1911                },
1912                translation: Vec3I16::new(rd_i16(d, o + 18), rd_i16(d, o + 20), rd_i16(d, o + 22)),
1913            }
1914        }
1915    }
1916
1917    fn bone_with_mouth(&self, bone: usize, mouth_range: bool, mouth: u8) -> BoneTransform {
1918        let base = self.bone(bone);
1919        if mouth == 0 || !mouth_range || !self.has_mouth {
1920            return base;
1921        }
1922        let amount = mouth.min(64) as i32;
1923        let open = self.mouth_xform();
1924        let mut relative = BoneTransform {
1925            rotation: Mat3I16::IDENTITY,
1926            translation: Vec3I16::ZERO,
1927        };
1928        for row in 0..3 {
1929            for column in 0..3 {
1930                let identity = if row == column { 4096 } else { 0 };
1931                relative.rotation.m[row][column] =
1932                    (identity + (((open.rotation.m[row][column] as i32 - identity) * amount) >> 6))
1933                        .clamp(i16::MIN as i32, i16::MAX as i32) as i16;
1934            }
1935        }
1936        relative.translation = Vec3I16::new(
1937            ((open.translation.x as i32 * amount) >> 6) as i16,
1938            ((open.translation.y as i32 * amount) >> 6) as i16,
1939            ((open.translation.z as i32 * amount) >> 6) as i16,
1940        );
1941        compose_affine(relative, base)
1942    }
1943}
1944
1945impl InterpolatedModelFrame<'_> {
1946    #[inline(always)]
1947    fn generic_component(a: i16, b: i16, frac16: i32) -> i16 {
1948        a.wrapping_add((((b as i32 - a as i32) * frac16) >> 4) as i16)
1949    }
1950
1951    #[inline(never)]
1952    pub fn bone(&self, bone: usize, mouth_range: bool, mouth: u8) -> BoneTransform {
1953        let a = self.a.bone_with_mouth(bone, mouth_range, mouth);
1954        if self.frac16 == 0 || self.a.frame_idx == self.b.frame_idx {
1955            return a;
1956        }
1957        let b = self.b.bone_with_mouth(bone, mouth_range, mouth);
1958        let mut rotation = Mat3I16::ZERO;
1959        for row in 0..3 {
1960            for column in 0..3 {
1961                rotation.m[row][column] = Self::generic_component(
1962                    a.rotation.m[row][column],
1963                    b.rotation.m[row][column],
1964                    self.frac16,
1965                );
1966            }
1967        }
1968        BoneTransform {
1969            rotation,
1970            translation: Vec3I16::new(
1971                Self::generic_component(a.translation.x, b.translation.x, self.frac16),
1972                Self::generic_component(a.translation.y, b.translation.y, self.frac16),
1973                Self::generic_component(a.translation.z, b.translation.z, self.frac16),
1974            ),
1975        }
1976    }
1977}
1978
1979#[inline(never)]
1980fn compose_affine(parent: BoneTransform, child: BoneTransform) -> BoneTransform {
1981    let rotation = parent.rotation.mul(&child.rotation);
1982    let transformed = parent.rotation.transform(child.translation);
1983    BoneTransform {
1984        rotation,
1985        translation: Vec3I16::new(
1986            transformed[0]
1987                .saturating_add(parent.translation.x as i32)
1988                .clamp(i16::MIN as i32, i16::MAX as i32) as i16,
1989            transformed[1]
1990                .saturating_add(parent.translation.y as i32)
1991                .clamp(i16::MIN as i32, i16::MAX as i32) as i16,
1992            transformed[2]
1993                .saturating_add(parent.translation.z as i32)
1994                .clamp(i16::MIN as i32, i16::MAX as i32) as i16,
1995        ),
1996    }
1997}
1998
1999#[cfg(test)]
2000mod tests {
2001    use super::*;
2002
2003    /// Word-aligned fixture storage: the SoA vertex stream must sit on a
2004    /// 4-byte boundary, which a `Vec<u8>` does not promise.
2005    #[repr(C, align(4))]
2006    struct Blob([u8; 1024]);
2007
2008    /// A body-masked HMD8 model: `ranges` is `(vertex count, body mask)` per
2009    /// range, every byte of vertices, poses and mouth transforms distinct.
2010    /// Returns the storage and the blob length.
2011    fn body_masked(ranges: &[(u16, u8)], soa: bool, hitboxes: usize) -> (Blob, usize) {
2012        const FRAMES: usize = 2;
2013        const BONES: usize = 3;
2014        let n_verts: usize = ranges.iter().map(|&(count, _)| count as usize).sum();
2015        let ranges_off = 40;
2016        let vertices_off = ranges_off + ranges.len() * HMD7_RANGE_BYTES;
2017        let poses_off = vertices_off + n_verts * 6;
2018        let mouth_off = poses_off + FRAMES * BONES * HMD8_AFFINE_BYTES;
2019        let hitboxes_off = mouth_off + FRAMES * MOUTH_XFORM_BYTES;
2020        let end = hitboxes_off + hitboxes * HMD7_HITBOX_BYTES;
2021        let mut blob = Blob([0; 1024]);
2022        let b = &mut blob.0;
2023        for (i, byte) in b[vertices_off..hitboxes_off].iter_mut().enumerate() {
2024            *byte = (i * 7 + 3) as u8;
2025        }
2026        let mut flags = HMD_FLAG_BODY_MASKS | HMD_FLAG_MOUTH;
2027        if soa {
2028            flags |= HMD_FLAG_VERTEX_SOA;
2029        }
2030        if hitboxes != 0 {
2031            flags |= HMD_FLAG_HITBOXES;
2032        }
2033        b[0..4].copy_from_slice(b"HMD8");
2034        b[4..8].copy_from_slice(&(n_verts as u32).to_le_bytes());
2035        b[12..16].copy_from_slice(&((hitboxes as u32) << 16).to_le_bytes());
2036        b[16..20].copy_from_slice(&(FRAMES as u32).to_le_bytes());
2037        b[20..24].copy_from_slice(&1u32.to_le_bytes());
2038        b[24..28].copy_from_slice(&((end - ranges_off) as u32).to_le_bytes());
2039        b[30..32].copy_from_slice(&flags.to_le_bytes());
2040        b[32..34].copy_from_slice(&(BONES as u16).to_le_bytes());
2041        b[34..36].copy_from_slice(&(ranges.len() as u16).to_le_bytes());
2042        let mut first = 0u16;
2043        for (i, &(count, mask)) in ranges.iter().enumerate() {
2044            let o = ranges_off + i * HMD7_RANGE_BYTES;
2045            b[o..o + 2].copy_from_slice(&first.to_le_bytes());
2046            b[o + 2..o + 4].copy_from_slice(&count.to_le_bytes());
2047            b[o + 4..o + 6].copy_from_slice(&((i % BONES) as u16).to_le_bytes());
2048            b[o + 6] = mask;
2049            first += count;
2050        }
2051        for h in 0..hitboxes {
2052            let o = hitboxes_off + h * HMD7_HITBOX_BYTES;
2053            b[o..o + 2].copy_from_slice(&((h % BONES) as u16).to_le_bytes());
2054        }
2055        (blob, end)
2056    }
2057
2058    fn reference_decode_q11(raw: u16) -> i16 {
2059        let code = if raw & 0x0800 != 0 {
2060            (raw | 0xf000) as i16
2061        } else {
2062            raw as i16
2063        };
2064        if code == 2047 {
2065            4096
2066        } else {
2067            code << 1
2068        }
2069    }
2070
2071    #[test]
2072    fn q11_decode_matches_every_packed_code() {
2073        for raw in 0..=0x0fff {
2074            assert_eq!(decode_q11(raw), reference_decode_q11(raw), "{raw:#05x}");
2075        }
2076    }
2077
2078    #[test]
2079    fn unaligned_pair_read_ignores_the_following_byte() {
2080        let a = 0x07ffu32;
2081        let b = 0x0800u32;
2082        let packed = a | (b << 12) | (0xa5 << 24);
2083        let bytes = packed.to_le_bytes();
2084        // SAFETY: `bytes` holds the four bytes the pair read loads.
2085        let pair = unsafe { rd_q11_pair(&bytes, 0) };
2086        assert_eq!(
2087            pair,
2088            (
2089                reference_decode_q11(a as u16),
2090                reference_decode_q11(b as u16)
2091            )
2092        );
2093    }
2094
2095    #[test]
2096    fn model_scale_header_accepts_only_exact_power_of_two_q12_scales() {
2097        for valid in [0, 256, 512, 1024, 2048, 4096] {
2098            assert!(valid_local_to_world_q12(valid));
2099        }
2100        for invalid in [1, 255, 257, 511, 513, 1000, 4095, 4097, u16::MAX] {
2101            assert!(!valid_local_to_world_q12(invalid));
2102        }
2103    }
2104
2105    /// Compaction keeps exactly the visible bodies' ranges and vertices, the
2106    /// whole pose palette, mouth transforms and hitboxes, and the result
2107    /// parses as a model with the kept counts.
2108    #[test]
2109    fn compaction_keeps_the_visible_bodies() {
2110        let ranges: &[(u16, u8)] = &[(3, 1), (2, 2), (4, 1), (1, 3)];
2111        for soa in [false, true] {
2112            let (source, len) = body_masked(ranges, soa, 2);
2113            let leaked: &'static Blob = std::boxed::Box::leak(std::boxed::Box::new(Blob(source.0)));
2114            let before = Model::from_bytes(&leaked.0[..len]);
2115            let mut blob = Blob(source.0);
2116            let mut remap = [0u16; 16];
2117            let kept = Model::compact_visible_bodies(&mut blob.0[..len], 2, &mut remap)
2118                .expect("body 2 is visible");
2119            // Ranges 1 (vertices 3..5) and 3 (vertex 9) carry body 2.
2120            let mut expected = [u16::MAX; 10];
2121            expected[3] = 0;
2122            expected[4] = 1;
2123            expected[9] = 2;
2124            assert_eq!(&remap[..10], &expected);
2125            let compact: &'static Blob = std::boxed::Box::leak(std::boxed::Box::new(Blob(blob.0)));
2126            let after = Model::from_bytes(&compact.0[..kept]);
2127            assert_eq!(after.vertex_count(), 3);
2128            assert_eq!(after.bone_range_count(), 2);
2129            assert_eq!(after.hitbox_count(), 2);
2130            assert!(after.has_body_masks());
2131            for (old, &new) in expected.iter().enumerate() {
2132                if new != u16::MAX {
2133                    assert_eq!(after.vertex(new as usize), before.vertex(old), "soa {soa}");
2134                }
2135            }
2136            assert_eq!(after.bone_range(1).first, 2);
2137            assert_eq!(after.bone_range(1).body_mask, 3);
2138            let pose_bytes =
2139                2 * 3 * HMD8_AFFINE_BYTES + 2 * MOUTH_XFORM_BYTES + 2 * HMD7_HITBOX_BYTES;
2140            assert_eq!(
2141                &compact.0[kept - pose_bytes..kept],
2142                &source.0[len - pose_bytes..len],
2143                "palette, mouth and hitboxes move intact"
2144            );
2145        }
2146        // Nothing visible, or no remap room: refused.
2147        let (source, len) = body_masked(ranges, false, 0);
2148        let mut blob = Blob(source.0);
2149        assert_eq!(
2150            Model::compact_visible_bodies(&mut blob.0[..len], 4, &mut [0; 16]),
2151            None
2152        );
2153        assert_eq!(
2154            Model::compact_visible_bodies(&mut blob.0[..len], 2, &mut [0; 9]),
2155            None
2156        );
2157    }
2158}