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

psx_asset/
hma1.rs

1//! HMA1: per-bone local quaternion animation tracks (the HMD8 pose-palette
2//! replacement cooked by `psx-anim-cook`).
3//!
4//! Each clip stores, per bone, a rotation track and a translation code:
5//!
6//! * rotation: one key, or keys every 1/2/3/4/6/8/16 source frames (the
7//!   clip header maps the playback position to each rate with one multiply,
8//!   so a bone's key lookup is an index, not a search);
9//! * 8-bit keys are range-reduced (i8 base + 4/6/8-bit offsets per
10//!   component), 12-bit keys are packed; the cooker picks per bone;
11//! * translation: the bind value, one per-clip value, or range-reduced Q2
12//!   keys (quarter cooked units, so hierarchy rounding stays small).
13//!
14//! [`Model::decode`] evaluates every bone parents-first into model-space
15//! affines. Composition runs on the GTE (MVMVA sf=0, rounded on the CPU) so
16//! it matches the host reference decoder bit for bit; on other targets the
17//! same arithmetic runs in Rust.
18//!
19//! Layout (little endian): u16 n_bones, u16 n_clips, u8 parent[n] (0xff
20//! root), pad to 2, i16 bind_t[n][3] (Q2), pad to 4, u32 clip_off[clip_count];
21//! clip: u16 n_int, u8 flags, u8 qfmt(=3), u16 seg[7], u16 factor_q15[7],
22//! u8 mode[n], pad to 2, then byte-packed tracks.
23
24#![allow(missing_docs, clippy::needless_range_loop)]
25
26use core::ptr;
27
28pub const RATE_COUNT: usize = 7;
29const CLIP_HEADER: usize = 4 + 2 * RATE_COUNT * 2;
30
31#[derive(Clone, Copy)]
32#[repr(C)]
33pub struct Affine {
34    pub r: [[i16; 3]; 3],
35    pub _pad: i16,
36    pub t: [i32; 3],
37}
38
39impl Affine {
40    pub const ZERO: Affine = Affine {
41        r: [[0; 3]; 3],
42        _pad: 0,
43        t: [0; 3],
44    };
45}
46
47// The readers below take a raw pointer into a blob `Model::new` validated.
48// Each has the same contract: every byte it reads, `d + o` up to the width it
49// names, lies inside that blob.
50
51#[inline(always)]
52unsafe fn u8_at(d: *const u8, o: usize) -> u32 {
53    // SAFETY: the caller guarantees the bytes read are inside the blob.
54    unsafe { *d.add(o) as u32 }
55}
56#[inline(always)]
57unsafe fn i8_at(d: *const u8, o: usize) -> i32 {
58    // SAFETY: the caller guarantees the bytes read are inside the blob.
59    unsafe { *d.add(o) as i8 as i32 }
60}
61#[inline(always)]
62unsafe fn u16_at(d: *const u8, o: usize) -> u32 {
63    // track data is byte-packed: odd offsets are legal
64    // SAFETY: the caller guarantees the bytes read are inside the blob.
65    unsafe { *d.add(o) as u32 | (*d.add(o + 1) as u32) << 8 }
66}
67#[inline(always)]
68unsafe fn i16_at(d: *const u8, o: usize) -> i32 {
69    // SAFETY: the caller guarantees the bytes read are inside the blob.
70    unsafe { (*d.add(o) as u32 | (*d.add(o + 1) as u32) << 8) as i16 as i32 }
71}
72#[inline(always)]
73unsafe fn u32_unaligned(d: *const u8, o: usize) -> u32 {
74    // SAFETY: the caller guarantees the bytes read are inside the blob.
75    unsafe { ptr::read_unaligned(d.add(o).cast::<u32>()) }
76}
77
78#[inline(always)]
79unsafe fn read8(d: *const u8, o: usize) -> [i32; 4] {
80    // SAFETY: the caller guarantees the bytes read are inside the blob.
81    unsafe {
82        [
83            i8_at(d, o) << 5,
84            i8_at(d, o + 1) << 5,
85            i8_at(d, o + 2) << 5,
86            i8_at(d, o + 3) << 5,
87        ]
88    }
89}
90#[inline(always)]
91unsafe fn read12(d: *const u8, o: usize) -> [i32; 4] {
92    // SAFETY: the caller guarantees the bytes read are inside the blob.
93    unsafe {
94        let packed = u16_at(d, o) | (u16_at(d, o + 2) << 16);
95        let w2 = u16_at(d, o + 4);
96        let s = |v: u32| (((v << 20) as i32) >> 20) << 1;
97        [
98            s(packed),
99            s(packed >> 12),
100            s((packed >> 24) | (w2 << 8)),
101            s(w2 >> 4),
102        ]
103    }
104}
105
106/// Segment index, Q8 fraction and key count of rate `r` for the clip at
107/// `off`: the position maps to each rate with one multiply.
108#[inline(never)]
109unsafe fn seg(d: *const u8, off: usize, r: usize, pos_q8: u32) -> (usize, i32, usize) {
110    // SAFETY: `off` is a validated clip and `r < RATE_COUNT`, so both reads are
111    // inside its 32-byte header.
112    unsafe {
113        let s = u16_at(d, off + 4 + r * 2) as usize;
114        let factor = u16_at(d, off + 4 + RATE_COUNT * 2 + r * 2);
115        let sp = (pos_q8 * factor) >> 15;
116        let i = (sp >> 8) as usize;
117        if i >= s {
118            (s.saturating_sub(1), 256, s + 1)
119        } else {
120            (i, (sp & 255) as i32, s + 1)
121        }
122    }
123}
124
125#[inline(always)]
126fn lerp4(a: [i32; 4], b: [i32; 4], f: i32) -> [i32; 4] {
127    [
128        a[0] + (((b[0] - a[0]) * f) >> 8),
129        a[1] + (((b[1] - a[1]) * f) >> 8),
130        a[2] + (((b[2] - a[2]) * f) >> 8),
131        a[3] + (((b[3] - a[3]) * f) >> 8),
132    ]
133}
134
135#[inline(always)]
136fn quat_to_mat(q: [i32; 4]) -> [[i16; 3]; 3] {
137    let [mut x, mut y, mut z, mut w] = q;
138    let n = (x * x + y * y + z * z + w * w + 2048) >> 12;
139    // The renormalising series is only meaningful while the length error is at most one unit;
140    // clamping keeps a damaged key (components up to 12224) from overflowing `3 * e * e`,
141    // and leaves every in-range key as it was.
142    let e = (4096 - n).clamp(-4096, 4096);
143    let r = 4096 + (e >> 1) + ((3 * e * e) >> 15);
144    x = (x * r + 2048) >> 12;
145    y = (y * r + 2048) >> 12;
146    z = (z * r + 2048) >> 12;
147    w = (w * r + 2048) >> 12;
148    let m2 = |a: i32, b: i32| (a * b + 1024) >> 11;
149    let (xx, yy, zz) = (m2(x, x), m2(y, y), m2(z, z));
150    let (xy, xz, yz) = (m2(x, y), m2(x, z), m2(y, z));
151    let (wx, wy, wz) = (m2(w, x), m2(w, y), m2(w, z));
152    [
153        [
154            (4096 - (yy + zz)) as i16,
155            (xy - wz) as i16,
156            (xz + wy) as i16,
157        ],
158        [
159            (xy + wz) as i16,
160            (4096 - (xx + zz)) as i16,
161            (yz - wx) as i16,
162        ],
163        [
164            (xz - wy) as i16,
165            (yz + wx) as i16,
166            (4096 - (xx + yy)) as i16,
167        ],
168    ]
169}
170
171/// MVMVA(RT, V0, cv=none, sf=0): raw 32-bit MAC1..3 for one column.
172#[cfg(target_arch = "mips")]
173#[inline(always)]
174fn mvmva_raw(xy: u32, z: u32) -> [i32; 3] {
175    // SAFETY: GTE-only asm: it writes VXY0/VZ0, runs MVMVA on the rotation
176    // already loaded, and reads MAC1..3 into $8..$10, all declared operands.
177    // No memory or stack access.
178    unsafe {
179        let m1: u32;
180        let m2: u32;
181        let m3: u32;
182        core::arch::asm!(
183            ".word {w0}", // mtc2 $8, VXY0
184            ".word {w1}", // mtc2 $9, VZ0
185            ".word 0", // VXY0 commit gap (HWB-010/011)
186            ".word 0",
187            ".word {w2}", // MVMVA RT,V0,none,sf=0
188            ".word {w3}", // mfc2 $8, MAC1
189            ".word {w4}", // mfc2 $9, MAC2
190            ".word {w5}", // mfc2 $10, MAC3
191            ".word 0",
192            w0 = const psx_hw::gte::mtc2(8, 0),
193            w1 = const psx_hw::gte::mtc2(9, 1),
194            w2 = const psx_hw::gte::mvmva(psx_hw::gte::Matrix::Rotation, psx_hw::gte::Vector::V0, psx_hw::gte::Translation::None),
195            w3 = const psx_hw::gte::mfc2(8, 25),
196            w4 = const psx_hw::gte::mfc2(9, 26),
197            w5 = const psx_hw::gte::mfc2(10, 27),
198            inlateout("$8") xy => m1,
199            inlateout("$9") z => m2,
200            lateout("$10") m3,
201            options(nostack, nomem, preserves_flags),
202        );
203        [m1 as i32, m2 as i32, m3 as i32]
204    }
205}
206
207#[inline(always)]
208fn pack(a: i32, b: i32) -> u32 {
209    (a as u32 & 0xffff) | ((b as u32) << 16)
210}
211
212/// parent * (r, t), rounded. On the PS1 the parent's rotation must already
213/// be in the GTE (see `decode`); elsewhere the same sums run in Rust.
214#[inline(always)]
215fn compose_loaded(p: &Affine, r: &[[i16; 3]; 3], t: [i32; 3], out: &mut Affine) {
216    let rd = |v: i32| (v + 2048) >> 12;
217    let cols = |xy: u32, z: u32| -> [i32; 3] {
218        #[cfg(target_arch = "mips")]
219        {
220            mvmva_raw(xy, z)
221        }
222        #[cfg(not(target_arch = "mips"))]
223        {
224            let v = [
225                (xy & 0xffff) as i16 as i32,
226                (xy >> 16) as i16 as i32,
227                z as i16 as i32,
228            ];
229            let mut o = [0i32; 3];
230            for i in 0..3 {
231                o[i] = p.r[i][0] as i32 * v[0] + p.r[i][1] as i32 * v[1] + p.r[i][2] as i32 * v[2];
232            }
233            o
234        }
235    };
236    let c0 = cols(pack(r[0][0] as i32, r[1][0] as i32), r[2][0] as i32 as u32);
237    let c1 = cols(pack(r[0][1] as i32, r[1][1] as i32), r[2][1] as i32 as u32);
238    let c2 = cols(pack(r[0][2] as i32, r[1][2] as i32), r[2][2] as i32 as u32);
239    let ct = cols(pack(t[0], t[1]), t[2] as u32);
240    for i in 0..3 {
241        out.r[i][0] = rd(c0[i]) as i16;
242        out.r[i][1] = rd(c1[i]) as i16;
243        out.r[i][2] = rd(c2[i]) as i16;
244        out.t[i] = rd(ct[i]) + p.t[i];
245    }
246}
247
248#[inline(always)]
249fn load_rot(_r: &[[i16; 3]; 3]) {
250    #[cfg(target_arch = "mips")]
251    psx_gte::scene::load_rotation(&psx_gte::math::Mat3I16 { m: *_r });
252}
253
254/// A local rotation folded into one bone before its children compose: the
255/// GoldSrc mouth controller, which adds to one Euler angle of the jaw bone.
256/// For a Z-rotation controller that is `delta * local` (`post == false`); for
257/// an X-rotation controller it is `local * delta` (`post == true`).
258#[derive(Clone, Copy)]
259pub struct Jaw {
260    /// HMA1 bone index, or `usize::MAX` for none.
261    pub bone: usize,
262    pub post: bool,
263    /// Q12 quaternion (x, y, z, w); need not be unit length (the decoder
264    /// renormalises the product).
265    pub q: [i32; 4],
266}
267
268impl Jaw {
269    pub const NONE: Jaw = Jaw {
270        bone: usize::MAX,
271        post: false,
272        q: [0, 0, 0, 4096],
273    };
274
275    /// The controller opened `amount`/64 of the way from closed to `full`
276    /// (a Q12 quaternion): a linear blend from identity, renormalised by the
277    /// decoder, exact in direction and close in angle for mouth-sized turns.
278    pub fn open(bone: usize, post: bool, full: [i16; 4], amount: u8) -> Jaw {
279        if amount == 0 {
280            return Jaw::NONE;
281        }
282        let a = amount.min(64) as i32;
283        let id = [0, 0, 0, 4096];
284        let mut q = [0i32; 4];
285        for k in 0..4 {
286            q[k] = id[k] + (((full[k] as i32 - id[k]) * a) >> 6);
287        }
288        Jaw { bone, post, q }
289    }
290}
291
292/// Q12 quaternion product a * b.
293#[inline(always)]
294pub fn quat_mul(a: [i32; 4], b: [i32; 4]) -> [i32; 4] {
295    let [ax, ay, az, aw] = a;
296    let [bx, by, bz, bw] = b;
297    let r = |v: i32| (v + 2048) >> 12;
298    [
299        r(aw * bx + ax * bw + ay * bz - az * by),
300        r(aw * by - ax * bz + ay * bw + az * bx),
301        r(aw * bz + ax * by - ay * bx + az * bw),
302        r(aw * bw - ax * bx - ay * by - az * bz),
303    ]
304}
305
306/// A validated HMA1 blob.
307///
308/// The decoder reads keys without bounds checks, so the only way to build
309/// one is [`Model::new`], which walks every clip and bone once and proves
310/// that no playback position can read outside the blob. The fields are
311/// private so safe code cannot widen what was proven.
312#[derive(Clone, Copy)]
313pub struct Model {
314    d: *const u8,
315    n_bones: usize,
316    n_clips: usize,
317    bind_off: usize,
318    clips_off: usize,
319}
320
321/// Fixed layout of a blob: `(n_bones, n_clips, bind_off, clips_off)`, when
322/// the header, parents, bind translations and clip offset table fit `d`.
323fn layout(d: &[u8]) -> Option<(usize, usize, usize, usize)> {
324    let n_bones = u16::from_le_bytes([*d.first()?, *d.get(1)?]) as usize;
325    let n_clips = u16::from_le_bytes([*d.get(2)?, *d.get(3)?]) as usize;
326    let bind_off = (4 + n_bones + 1) & !1;
327    let clips_off = (bind_off + n_bones * 6 + 3) & !3;
328    (clips_off + n_clips * 4 <= d.len()).then_some((n_bones, n_clips, bind_off, clips_off))
329}
330
331/// One past the last byte a keyed track can read: keys `i` and `i + 1` of
332/// `key_bytes` each from `keys`, `read` bytes per key load, where `i` is at
333/// most `max(segments, 1) - 1` (see `seg`).
334fn track_end(keys: usize, segments: usize, key_bytes: usize, read: usize) -> usize {
335    keys.saturating_add(segments.max(1).saturating_mul(key_bytes))
336        .saturating_add(read)
337}
338
339/// Walk every clip exactly as `decode_with` does and check each read against
340/// `d`, for every playback position at once.
341fn tracks_fit(d: &[u8], n_bones: usize, n_clips: usize, clips_off: usize) -> bool {
342    let len = d.len();
343    let u16_at = |o: usize| u16::from_le_bytes([d[o], d[o + 1]]) as usize;
344    for clip in 0..n_clips {
345        let o = clips_off + clip * 4;
346        let off = u32::from_le_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]]) as usize;
347        let modes = off.saturating_add(CLIP_HEADER);
348        if modes.saturating_add(n_bones) > len {
349            return false;
350        }
351        let segments = |rate: usize| u16_at(off + 4 + rate * 2);
352        let mut p = (modes + n_bones + 1) & !1;
353        for b in 0..n_bones {
354            let mode = d[modes + b] as usize;
355            let (rc, pc, hi) = (mode & 7, (mode >> 3) & 7, mode & 0x40 != 0);
356            // Rotation.
357            let end = if rc == 7 {
358                p = p.saturating_add(if hi { 6 } else { 4 });
359                p
360            } else if !hi {
361                if p.saturating_add(6) > len {
362                    return false;
363                }
364                let kb = d[p + 4] as usize;
365                // 8-bit keys pack 4, 6 or 8 bits per component (2..=4 bytes);
366                // wider would overflow the decoder's 32-bit field shifts.
367                if kb > 4 {
368                    return false;
369                }
370                let ks = p + 6;
371                p = ks.saturating_add((segments(rc) + 1).saturating_mul(kb));
372                track_end(ks, segments(rc), kb, 4)
373            } else {
374                let keys = p;
375                p = p.saturating_add((segments(rc) + 1).saturating_mul(6));
376                track_end(keys, segments(rc), 6, 6)
377            };
378            if end > len {
379                return false;
380            }
381            // Translation (pc == 7 reads the bind table `layout` checked).
382            let end = if pc == 7 {
383                0
384            } else if pc == 6 {
385                p = p.saturating_add(6);
386                p
387            } else {
388                if p.saturating_add(7) > len {
389                    return false;
390                }
391                let kb = d[p + 6] as usize;
392                let ks = p + 8;
393                p = ks.saturating_add((segments(pc) + 1).saturating_mul(kb));
394                if kb == 6 {
395                    track_end(ks, segments(pc), 6, 6)
396                } else {
397                    track_end(ks, segments(pc), kb, 4)
398                }
399            };
400            if end > len {
401                return false;
402            }
403        }
404    }
405    true
406}
407
408/// [`layout`] of a blob that passes every check [`Model::new`] makes, for
409/// callers that only validate and may not hold `d` for `'static`.
410pub(crate) fn validated_layout(d: &[u8]) -> Option<(usize, usize, usize, usize)> {
411    let (n_bones, n_clips, bind_off, clips_off) = layout(d)?;
412    if n_clips == 0 {
413        return None;
414    }
415    for b in 0..n_bones {
416        let parent = d[4 + b] as usize;
417        if parent != 0xff && parent >= b {
418            return None;
419        }
420    }
421    if !tracks_fit(d, n_bones, n_clips, clips_off) {
422        return None;
423    }
424    Some((n_bones, n_clips, bind_off, clips_off))
425}
426
427impl Model {
428    /// Validate and wrap an HMA1 blob. `None` when any clip, at any playback
429    /// position, would read outside `d`, when a bone's parent does not come
430    /// before it (the decoder composes parents first), or when there are no
431    /// clips.
432    pub fn new(d: &'static [u8]) -> Option<Model> {
433        let (n_bones, n_clips, bind_off, clips_off) = validated_layout(d)?;
434        Some(Model {
435            d: d.as_ptr(),
436            n_bones,
437            n_clips,
438            bind_off,
439            clips_off,
440        })
441    }
442
443    /// [`Model::new`] without the walk, for a blob that already passed it.
444    ///
445    /// # Safety
446    /// `Model::new(d)` must return `Some`.
447    #[inline]
448    pub(crate) unsafe fn new_unchecked(d: &'static [u8]) -> Model {
449        // `new` returned Some for these bytes, so `layout` does too.
450        let (n_bones, n_clips, bind_off, clips_off) = layout(d).unwrap_or((0, 1, 0, 0));
451        Model {
452            d: d.as_ptr(),
453            n_bones,
454            n_clips,
455            bind_off,
456            clips_off,
457        }
458    }
459
460    /// Number of bones; `decode` needs this many `Affine` slots.
461    #[inline]
462    pub fn bone_count(&self) -> usize {
463        self.n_bones
464    }
465
466    /// Number of clips in the blob (at least one).
467    #[inline]
468    pub fn clip_count(&self) -> usize {
469        self.n_clips
470    }
471
472    /// Byte offset of `clip`'s record, clamped to the last clip.
473    #[inline(always)]
474    fn clip_off(&self, clip: usize) -> usize {
475        let clip = clip.min(self.n_clips - 1);
476        // SAFETY: `layout` checked the clip offset table, and `clip` is below
477        // `n_clips`; the load is unaligned.
478        unsafe { ptr::read_unaligned(self.d.add(self.clips_off + clip * 4).cast::<u32>()) as usize }
479    }
480
481    /// Source frame intervals of `clip` (numframes - 1, at least 1): the
482    /// clip's position runs 0..=`clip_intervals * 256`. Clips past the last
483    /// read the last.
484    pub fn clip_intervals(&self, clip: usize) -> u32 {
485        // SAFETY: `tracks_fit` checked every clip's header is in the blob.
486        unsafe { u16_at(self.d, self.clip_off(clip)).max(1) }
487    }
488
489    /// Decode every bone of `clip` at `pos_q8` into model-space affines.
490    /// Clips past the last decode the last; an `out` shorter than
491    /// [`Model::bone_count`] is left untouched.
492    #[inline(always)]
493    pub fn decode(&self, clip: usize, pos_q8: u32, out: &mut [Affine]) {
494        self.decode_with(clip, pos_q8, &Jaw::NONE, out)
495    }
496
497    /// [`Model::decode`] with a local controller rotation on one bone.
498    /// `out` must hold at least `n_bones` entries.
499    #[inline(never)]
500    pub fn decode_with(&self, clip: usize, pos_q8: u32, jaw: &Jaw, out: &mut [Affine]) {
501        let off = self.clip_off(clip);
502        // SAFETY: `Model::new` ran `tracks_fit`, which follows this same walk
503        // for every clip and bounds each read below for any `pos_q8` (`seg`
504        // clamps the key index to the segment count it checked). Parents
505        // precede children (checked by `new`) and `out` holds `nb` bones
506        // (checked below), so the unchecked `out` accesses are in bounds.
507        unsafe {
508            let d = self.d;
509            let nb = self.n_bones;
510            if out.len() < nb {
511                return;
512            }
513            let modes = off + CLIP_HEADER;
514            let mut p = (modes + nb + 1) & !1;
515            let mut loaded = usize::MAX;
516            for b in 0..nb {
517                let mode = u8_at(d, modes + b);
518                let rc = (mode & 7) as usize;
519                let pc = ((mode >> 3) & 7) as usize;
520                let hi = mode & 0x40 != 0;
521                let q = if rc == 7 {
522                    if hi {
523                        let q = read12(d, p);
524                        p += 6;
525                        q
526                    } else {
527                        let q = read8(d, p);
528                        p += 4;
529                        q
530                    }
531                } else if !hi {
532                    let (i, f, nk) = seg(d, off, rc, pos_q8);
533                    let kb = u8_at(d, p + 4) as usize;
534                    // `tracks_fit` rejects `kb > 4`, so a model that got this far
535                    // has `w <= 8` and the field shifts below cannot overflow.
536                    debug_assert!(kb <= 4, "a validated rotation track has 0..=4 key bytes");
537                    let w = (kb as u32) << 1;
538                    let mask = (1u32 << w) - 1;
539                    let (b0, b1, b2, b3) = (
540                        i8_at(d, p),
541                        i8_at(d, p + 1),
542                        i8_at(d, p + 2),
543                        i8_at(d, p + 3),
544                    );
545                    let ks = p + 6;
546                    let wa = u32_unaligned(d, ks + i * kb);
547                    let wb = u32_unaligned(d, ks + (i + 1) * kb);
548                    let ex = |word: u32| -> [i32; 4] {
549                        [
550                            (b0 + (word & mask) as i32) << 5,
551                            (b1 + ((word >> w) & mask) as i32) << 5,
552                            (b2 + ((word >> (2 * w)) & mask) as i32) << 5,
553                            (b3 + ((word >> (3 * w)) & mask) as i32) << 5,
554                        ]
555                    };
556                    p = ks + nk * kb;
557                    lerp4(ex(wa), ex(wb), f)
558                } else {
559                    let (i, f, nk) = seg(d, off, rc, pos_q8);
560                    let q = lerp4(read12(d, p + i * 6), read12(d, p + i * 6 + 6), f);
561                    p += nk * 6;
562                    q
563                };
564                let t = if pc == 7 {
565                    let o = self.bind_off + b * 6;
566                    [i16_at(d, o), i16_at(d, o + 2), i16_at(d, o + 4)]
567                } else if pc == 6 {
568                    let v = [i16_at(d, p), i16_at(d, p + 2), i16_at(d, p + 4)];
569                    p += 6;
570                    v
571                } else {
572                    let base = [i16_at(d, p), i16_at(d, p + 2), i16_at(d, p + 4)];
573                    let kb = u8_at(d, p + 6) as usize;
574                    let ks = p + 8;
575                    let (i, f, nk) = seg(d, off, pc, pos_q8);
576                    let v = if kb == 6 {
577                        let a = [
578                            i16_at(d, ks + i * 6) & 0xffff,
579                            i16_at(d, ks + i * 6 + 2) & 0xffff,
580                            i16_at(d, ks + i * 6 + 4) & 0xffff,
581                        ];
582                        let c = [
583                            i16_at(d, ks + i * 6 + 6) & 0xffff,
584                            i16_at(d, ks + i * 6 + 8) & 0xffff,
585                            i16_at(d, ks + i * 6 + 10) & 0xffff,
586                        ];
587                        [
588                            base[0] + a[0] + (((c[0] - a[0]) * f) >> 8),
589                            base[1] + a[1] + (((c[1] - a[1]) * f) >> 8),
590                            base[2] + a[2] + (((c[2] - a[2]) * f) >> 8),
591                        ]
592                    } else {
593                        let w: u32 = if kb == 2 {
594                            5
595                        } else if kb == 3 {
596                            8
597                        } else {
598                            10
599                        };
600                        let mask = (1u32 << w) - 1;
601                        let wa = u32_unaligned(d, ks + i * kb);
602                        let wb = u32_unaligned(d, ks + (i + 1) * kb);
603                        let ex = |word: u32| {
604                            [
605                                (word & mask) as i32,
606                                ((word >> w) & mask) as i32,
607                                ((word >> (2 * w)) & mask) as i32,
608                            ]
609                        };
610                        let (a, c) = (ex(wa), ex(wb));
611                        [
612                            base[0] + a[0] + (((c[0] - a[0]) * f) >> 8),
613                            base[1] + a[1] + (((c[1] - a[1]) * f) >> 8),
614                            base[2] + a[2] + (((c[2] - a[2]) * f) >> 8),
615                        ]
616                    };
617                    p = ks + nk * kb;
618                    v
619                };
620                let q = if b == jaw.bone {
621                    if jaw.post {
622                        quat_mul(q, jaw.q)
623                    } else {
624                        quat_mul(jaw.q, q)
625                    }
626                } else {
627                    q
628                };
629                let r = quat_to_mat(q);
630                let parent = u8_at(d, 4 + b) as usize;
631                // Parents precede children (checked when the HMD8 loads) and
632                // `out` holds `nb` bones (checked above).
633                if parent == 0xff {
634                    *out.get_unchecked_mut(b) = Affine { r, _pad: 0, t };
635                } else {
636                    let pa = *out.get_unchecked(parent);
637                    if loaded != parent {
638                        load_rot(&pa.r);
639                        loaded = parent;
640                    }
641                    compose_loaded(&pa, &r, t, out.get_unchecked_mut(b));
642                }
643            }
644        }
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use core::f64::consts::FRAC_1_SQRT_2;
652
653    /// Q12 rotation matrix of a unit quaternion given as floats (the
654    /// standard formula, in f64, normalised first).
655    fn reference(q: [f64; 4]) -> [[f64; 3]; 3] {
656        let n = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
657        let [x, y, z, w] = [q[0] / n, q[1] / n, q[2] / n, q[3] / n];
658        [
659            [
660                1.0 - 2.0 * (y * y + z * z),
661                2.0 * (x * y - w * z),
662                2.0 * (x * z + w * y),
663            ],
664            [
665                2.0 * (x * y + w * z),
666                1.0 - 2.0 * (x * x + z * z),
667                2.0 * (y * z - w * x),
668            ],
669            [
670                2.0 * (x * z - w * y),
671                2.0 * (y * z + w * x),
672                1.0 - 2.0 * (x * x + y * y),
673            ],
674        ]
675    }
676
677    #[test]
678    fn quat_to_mat_matches_the_float_formula_for_near_unit_input() {
679        // Rotations about each axis and a diagonal one, with the quaternion
680        // scaled up to 5% off unit length, which the renormalise step absorbs.
681        let axes: [[f64; 4]; 5] = [
682            [0.0, 0.0, 0.0, 1.0],
683            [0.5, 0.5, 0.5, 0.5],
684            [0.0, 0.0, FRAC_1_SQRT_2, FRAC_1_SQRT_2],
685            [0.3, -0.5, 0.2, 0.78],
686            [-0.6, 0.1, 0.0, 0.79],
687        ];
688        for axis in axes {
689            for scale in [0.95, 1.0, 1.05] {
690                let q = axis.map(|c| (c * 4096.0 * scale) as i32);
691                let got = quat_to_mat(q);
692                let want = reference(axis);
693                for r in 0..3 {
694                    for c in 0..3 {
695                        let expected = (want[r][c] * 4096.0).round() as i32;
696                        let diff = (got[r][c] as i32 - expected).abs();
697                        assert!(diff <= 24, "{axis:?} x{scale} [{r}][{c}] off by {diff}");
698                    }
699                }
700            }
701        }
702    }
703
704    #[test]
705    fn quat_to_mat_survives_the_largest_components_a_key_can_hold() {
706        // An 8-bit key is an i8 base plus an 8-bit offset, shifted left by 5:
707        // components from -4096 to 12224. Their squared length is far from
708        // 4096, and `3 * e * e` in i32 overflowed (a panic in debug, garbage
709        // in release) once the length error passed about 26,000.
710        let extremes = [-4096, 0, 4096, 12224];
711        for x in extremes {
712            for y in extremes {
713                for z in extremes {
714                    for w in extremes {
715                        let m = quat_to_mat([x, y, z, w]);
716                        // Garbage rotation is allowed; unbounded work is not.
717                        let _ = m;
718                    }
719                }
720            }
721        }
722    }
723}