Skip to main content

unity_formats/
clip.rs

1//! Unity AnimationClips: the curves of a Mecanim clip (`m_MuscleClip`) decoded to keys with tangents.
2//!
3//! A clip stores its curves in three blocks, numbered in this order: the **streamed** curves (`m_StreamedClip`), the
4//! **dense** curves (`m_DenseClip`, one sample per frame at a fixed rate) and the **constant** curves
5//! (`m_ConstantClip`, one value each). `m_ClipBindingConstant.genericBindings` lists what the curves drive, in curve
6//! order: a binding takes one curve (a float property), or for a Transform the curves of its attribute, 1 position (3),
7//! 2 rotation, a quaternion (4), 3 scale (3), 4 Euler angles (3). A binding names its target by `path`, the CRC-32 of the
8//! path of GameObject names below the Animator's own (the Animator's GameObject itself is the empty path, hash 0).
9//!
10//! The streamed block is a sequence of frames, each `time: f32`, `count: i32` and `count` keys of `index: i32` and four
11//! `f32` coefficients; a key opens a cubic segment of its curve that lasts until the curve's next key:
12//! `value(dt) = ((c0 dt + c1) dt + c2) dt + c3` for `dt` seconds after the key, so `c3` is the key's value and `c2` its
13//! outgoing slope; the incoming slope of the next key is the derivative at the segment's end. The first frame is at
14//! `-f32::MAX` and gives every curve's initial value; the last is at `+inf` with no keys. A segment whose coefficients are
15//! not finite is a step.
16
17use crate::player::{ObjRef, Player};
18use crate::value::{Array, Value};
19use crate::{Error, Result, invalid};
20
21/// One curve binding of a clip (`genericBindings`).
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct GenericBinding {
24    /// The CRC-32 of the target's path below the Animator's GameObject.
25    pub path: u32,
26    /// The attribute: for a Transform (`type_id` 4) 1 position, 2 rotation, 3 scale, 4 Euler angles; else the
27    /// property's hash.
28    pub attribute: u32,
29    /// The target's class id (4 Transform, 95 Animator, 137 SkinnedMeshRenderer, ...).
30    pub type_id: i64,
31    /// Whether the curve drives an object reference (`isPPtrCurve`); such clips are refused by [`ClipData::read`].
32    pub is_pptr: bool,
33}
34
35impl GenericBinding {
36    /// How many curves this binding takes.
37    pub fn width(&self) -> usize {
38        match (self.type_id, self.attribute) {
39            (4, 1 | 3 | 4) => 3,
40            (4, 2) => 4,
41            _ => 1,
42        }
43    }
44}
45
46/// Where a curve was stored.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum Storage {
49    /// The streamed block (`m_StreamedClip`).
50    Streamed,
51    /// The dense block (`m_DenseClip`).
52    Dense,
53    /// The constant block (`m_ConstantClip`).
54    Constant,
55}
56
57/// A key of a curve: its time (seconds), value and the slopes (per second) into and out of it. An infinite outgoing
58/// slope is a step to the next key.
59#[derive(Clone, Copy, Debug, PartialEq)]
60pub struct Key {
61    /// Seconds from the clip's start.
62    pub time: f32,
63    /// The curve's value at `time`.
64    pub value: f32,
65    /// The slope arriving at the key, per second.
66    pub in_slope: f32,
67    /// The slope leaving the key, per second; infinite for a step.
68    pub out_slope: f32,
69}
70
71/// One scalar curve: keys in increasing time, evaluated as a cubic Hermite spline between them (a dense curve's slopes
72/// are its secants, so it is piecewise linear).
73#[derive(Clone, Debug, PartialEq)]
74pub struct Curve {
75    /// Which block the curve was stored in.
76    pub storage: Storage,
77    /// The keys, in increasing time.
78    pub keys: Vec<Key>,
79}
80
81impl Curve {
82    /// The value at `t` and the slopes into and out of it: a key's own at its time, a segment's derivative inside it,
83    /// the end values held with no slope outside. A step segment holds its key's value. A curve without keys is 0; a
84    /// `t` that is NaN has the value NaN.
85    pub fn sample(&self, t: f32) -> (f32, f32, f32) {
86        let k = &self.keys;
87        let (Some(first), Some(last)) = (k.first(), k.last()) else {
88            return (0.0, 0.0, 0.0);
89        };
90        if t.is_nan() {
91            return (f32::NAN, 0.0, 0.0);
92        }
93        if t <= first.time {
94            return (
95                first.value,
96                0.0,
97                if t == first.time { first.out_slope } else { 0.0 },
98            );
99        }
100        if t >= last.time {
101            return (last.value, if t == last.time { last.in_slope } else { 0.0 }, 0.0);
102        }
103        // the first key after `t`, and the one before it; `t` is strictly between the first and last keys
104        let after = k.partition_point(|key| key.time <= t);
105        let (Some(a), Some(b)) = (after.checked_sub(1).and_then(|i| k.get(i)), k.get(after)) else {
106            return (last.value, 0.0, 0.0);
107        };
108        if t == a.time {
109            return (a.value, a.in_slope, a.out_slope);
110        }
111        if !a.out_slope.is_finite() {
112            return (a.value, 0.0, 0.0);
113        }
114        let (dt, s) = (
115            f64::from(b.time - a.time),
116            f64::from(t - a.time) / f64::from(b.time - a.time),
117        );
118        let (p0, p1) = (f64::from(a.value), f64::from(b.value));
119        let (m0, m1) = (f64::from(a.out_slope) * dt, f64::from(b.in_slope) * dt);
120        let (s2, s3) = (s * s, s * s * s);
121        let value = (2.0 * s3 - 3.0 * s2 + 1.0) * p0
122            + (s3 - 2.0 * s2 + s) * m0
123            + (-2.0 * s3 + 3.0 * s2) * p1
124            + (s3 - s2) * m1;
125        let slope = ((6.0 * s2 - 6.0 * s) * p0
126            + (3.0 * s2 - 4.0 * s + 1.0) * m0
127            + (-6.0 * s2 + 6.0 * s) * p1
128            + (3.0 * s2 - 2.0 * s) * m1)
129            / dt;
130        (crate::narrow(value), crate::narrow(slope), crate::narrow(slope))
131    }
132
133    /// Whether any slope is a step.
134    pub fn has_steps(&self) -> bool {
135        self.keys.iter().any(|k| !k.out_slope.is_finite())
136    }
137
138    /// Whether the curve has a single key.
139    pub fn is_constant(&self) -> bool {
140        self.keys.len() == 1
141    }
142}
143
144/// A binding with the curves it takes, in the order of its attribute's components.
145#[derive(Clone, Debug, PartialEq)]
146pub struct Track {
147    /// What the curves drive.
148    pub binding: GenericBinding,
149    /// [`GenericBinding::width`] curves.
150    pub curves: Vec<Curve>,
151}
152
153/// An AnimationClip's curves as stored.
154#[derive(Clone, Debug, PartialEq)]
155pub struct ClipData {
156    /// `m_Name`.
157    pub name: String,
158    /// Seconds, `m_StopTime - m_StartTime`.
159    pub duration: f32,
160    /// `m_LoopTime`.
161    pub looping: bool,
162    /// The bindings with their curves, in binding order.
163    pub tracks: Vec<Track>,
164    /// Animation events (`m_Events`): not read.
165    pub events: usize,
166    /// How far a streamed segment's end was from the next key's value (relative), the worst over the segments of a
167    /// millisecond or more: the decode's own consistency (the segment is the cubic the next key continues).
168    pub worst_continuity_error: f32,
169}
170
171/// Words a key takes in the streamed block: its curve index and four coefficients.
172const KEY_WORDS: usize = 5;
173
174fn int(v: &Value, k: &str) -> Result<i64> {
175    v.get(k)
176        .and_then(Value::as_i64)
177        .ok_or_else(|| invalid(format!("clip: no integer {k}")))
178}
179
180fn float(v: &Value, k: &str) -> Result<f32> {
181    v.get(k)
182        .and_then(Value::as_f32)
183        .ok_or_else(|| invalid(format!("clip: no float {k}")))
184}
185
186fn list<'a>(v: &'a Value, k: &str) -> Result<&'a [Value]> {
187    match v.get(k) {
188        Some(Value::Array(Array::Values(items))) => Ok(items),
189        Some(Value::Array(a)) if a.is_empty() => Ok(&[]),
190        _ => Err(invalid(format!("clip: no list {k}"))),
191    }
192}
193
194fn floats(v: &Value, k: &str) -> Result<Vec<f32>> {
195    match v.get(k) {
196        Some(Value::Array(Array::F32(f))) => Ok(f.clone()),
197        Some(Value::Array(a)) if a.is_empty() => Ok(Vec::new()),
198        _ => Err(invalid(format!("clip: no float list {k}"))),
199    }
200}
201
202/// The streamed block's curves: `curve_count` curves from the frames in `words`. Every curve stores at least its
203/// initial key, five words, so a count the data cannot hold is refused before anything is allocated for it.
204fn read_streamed(name: &str, words: &[u32], curve_count: usize) -> Result<(Vec<Curve>, f32)> {
205    let bad = |why: String| invalid(format!("{name}: streamed clip: {why}"));
206    let most = words.len() / KEY_WORDS;
207    if curve_count > most {
208        return Err(bad(format!(
209            "{curve_count} curves, but {} bytes of frames hold at most {most} keys, and every curve has one",
210            words.len().saturating_mul(4)
211        )));
212    }
213    let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
214    // each curve's entries: the time and the four coefficients of the cubic that starts there
215    let mut entries: Vec<Vec<(f32, [f32; 4])>> = vec![Vec::new(); curve_count];
216    let mut at = 0;
217    let take = |at: &mut usize, n: usize| -> Result<&[u8]> {
218        let end = at.checked_add(n);
219        let part = end.and_then(|end| bytes.get(*at..end)).ok_or_else(|| {
220            bad(format!(
221                "frames past the data at byte {at} of {}",
222                bytes.len(),
223                at = *at
224            ))
225        })?;
226        *at = end.unwrap_or(*at);
227        Ok(part)
228    };
229    let mut last_time = f32::NEG_INFINITY;
230    let mut ended = false;
231    while at < bytes.len() {
232        let frame_at = at;
233        let time = f32::from_le_bytes(take(&mut at, 4)?.try_into().expect("4 bytes"));
234        let count = i32::from_le_bytes(take(&mut at, 4)?.try_into().expect("4 bytes"));
235        if time.is_nan() || time < last_time {
236            return Err(bad(format!(
237                "frame time {time} after {last_time} (the frame at byte {frame_at}; times must not decrease)"
238            )));
239        }
240        last_time = time;
241        let count = usize::try_from(count).map_err(|_| {
242            bad(format!(
243                "{count} keys in the frame at byte {frame_at} (at least 0)"
244            ))
245        })?;
246        for _ in 0..count {
247            let key_at = at;
248            let index = i32::from_le_bytes(take(&mut at, 4)?.try_into().expect("4 bytes"));
249            let mut c = [0.0f32; 4];
250            for x in &mut c {
251                *x = f32::from_le_bytes(take(&mut at, 4)?.try_into().expect("4 bytes"));
252            }
253            usize::try_from(index)
254                .ok()
255                .and_then(|i| entries.get_mut(i))
256                .ok_or_else(|| {
257                    bad(format!(
258                        "a key of curve {index} of {curve_count} at byte {key_at}"
259                    ))
260                })?
261                .push((time, c));
262        }
263        if time == f32::INFINITY {
264            ended = true;
265            break;
266        }
267    }
268    if !ended || at != bytes.len() {
269        return Err(bad(format!(
270            "the frames do not end with the +inf frame at the end of the data (frames end at byte {at} of {})",
271            bytes.len()
272        )));
273    }
274    let mut worst = 0.0f32;
275    let mut out = Vec::with_capacity(curve_count);
276    for (i, list) in entries.iter().enumerate() {
277        // the initial entry (at -f32::MAX) is the value before the first key, which the first key repeats
278        let real: Vec<&(f32, [f32; 4])> = list
279            .iter()
280            .filter(|(t, _)| *t > f32::MIN / 2.0 && t.is_finite())
281            .collect();
282        let mut keys: Vec<Key> = Vec::new();
283        if real.is_empty() {
284            let (_, c) = list.first().ok_or_else(|| bad(format!("curve {i} has no key")))?;
285            keys.push(Key {
286                time: 0.0,
287                value: c[3],
288                in_slope: 0.0,
289                out_slope: 0.0,
290            });
291        }
292        let mut previous: Option<(f32, [f32; 4])> = None;
293        for &&(time, c) in &real {
294            let in_slope = match previous {
295                Some((t0, p)) => {
296                    let dt = time - t0;
297                    // the cubic of the previous key at this key; a segment under a millisecond (a step made of two
298                    // keys) is not checked, its large coefficients cancel past f32 (17 of 135,385 in SkateBIRD's clips)
299                    let end = ((p[0] * dt + p[1]) * dt + p[2]) * dt + p[3];
300                    if dt >= 1e-3 && end.is_finite() && c[3].is_finite() {
301                        worst = worst.max((end - c[3]).abs() / (1.0 + c[3].abs()));
302                    }
303                    (3.0 * p[0] * dt + 2.0 * p[1]) * dt + p[2]
304                }
305                None => 0.0,
306            };
307            keys.push(Key {
308                time,
309                value: c[3],
310                in_slope,
311                out_slope: c[2],
312            });
313            previous = Some((time, c));
314        }
315        // a key whose segment is a step has non-finite coefficients: its slopes are a step
316        for k in &mut keys {
317            if !k.in_slope.is_finite() {
318                k.in_slope = f32::INFINITY;
319            }
320        }
321        out.push(Curve {
322            storage: Storage::Streamed,
323            keys,
324        });
325    }
326    Ok((out, worst))
327}
328
329/// The dense block's curves (`m_DenseClip`): frame `f` of curve `c` is `m_SampleArray[f * curves + c]`, at
330/// `m_BeginTime + f / m_SampleRate`. The sample array must hold exactly `frames * curves` values, and a block that
331/// states curves needs frames: a count nothing stored backs is refused before a curve is made for it.
332fn read_dense(name: &str, dense: &Value) -> Result<Vec<Curve>> {
333    let bad = |why: String| invalid(format!("{name}: dense clip: {why}"));
334    let dense_curves =
335        usize::try_from(int(dense, "m_CurveCount")?).map_err(|_| bad("a negative curve count".into()))?;
336    let frames =
337        usize::try_from(int(dense, "m_FrameCount")?).map_err(|_| bad("a negative frame count".into()))?;
338    let samples = floats(dense, "m_SampleArray")?;
339    if frames.checked_mul(dense_curves) != Some(samples.len()) {
340        return Err(bad(format!(
341            "{} samples for {frames} frames of {dense_curves} curves",
342            samples.len()
343        )));
344    }
345    if frames == 0 && dense_curves > 0 {
346        return Err(bad(format!("{dense_curves} curves of no frames")));
347    }
348    let (rate, begin) = (float(dense, "m_SampleRate")?, float(dense, "m_BeginTime")?);
349    if frames > 1 && !(rate.is_finite() && rate > 0.0) {
350        return Err(bad(format!(
351            "sample rate {rate} for {frames} frames (a positive rate is needed)"
352        )));
353    }
354    let times: Vec<f32> = (0..frames).map(|f| begin + f as f32 / rate).collect();
355    let mut curves = Vec::with_capacity(dense_curves);
356    for c in 0..dense_curves {
357        let values: Vec<f32> = samples.iter().skip(c).step_by(dense_curves).copied().collect();
358        let slope = |a: usize, b: usize| match (values.get(a..=b), times.get(a..=b)) {
359            (Some([v0, .., v1]), Some([t0, .., t1])) => (v1 - v0) / (t1 - t0),
360            _ => 0.0,
361        };
362        curves.push(Curve {
363            storage: Storage::Dense,
364            keys: values
365                .iter()
366                .zip(&times)
367                .enumerate()
368                .map(|(f, (&value, &time))| Key {
369                    time,
370                    value,
371                    in_slope: f.checked_sub(1).map_or(0.0, |p| slope(p, f)),
372                    out_slope: if f < frames.saturating_sub(1) {
373                        slope(f, f.saturating_add(1))
374                    } else {
375                        0.0
376                    },
377                })
378                .collect(),
379        });
380    }
381    Ok(curves)
382}
383
384impl ClipData {
385    /// The clip object `r`. Legacy clips (`m_Legacy`, curves stored per kind) are not read.
386    pub fn read(player: &Player, r: &ObjRef) -> Result<ClipData> {
387        let c = player.decode(r)?;
388        let name = c.get("m_Name").and_then(Value::as_str).unwrap_or("").to_owned();
389        if c.get("m_Legacy")
390            .is_some_and(|l| matches!(l, Value::Bool(true)) || l.as_i64().is_some_and(|v| v != 0))
391        {
392            return Err(Error::Unsupported(format!("{name}: legacy animation clip")));
393        }
394        let muscle = c
395            .get("m_MuscleClip")
396            .ok_or_else(|| invalid(format!("{name}: no m_MuscleClip")))?;
397        let data = muscle
398            .at(&["m_Clip", "data"])
399            .ok_or_else(|| invalid(format!("{name}: no m_MuscleClip.m_Clip.data")))?;
400        let streamed = data
401            .get("m_StreamedClip")
402            .ok_or_else(|| invalid(format!("{name}: no m_StreamedClip")))?;
403        let dense = data
404            .get("m_DenseClip")
405            .ok_or_else(|| invalid(format!("{name}: no m_DenseClip")))?;
406        let constant = data
407            .get("m_ConstantClip")
408            .ok_or_else(|| invalid(format!("{name}: no m_ConstantClip")))?;
409        let start = float(muscle, "m_StartTime")?;
410        let stop = float(muscle, "m_StopTime")?;
411        if start != 0.0 {
412            return Err(Error::Unsupported(format!(
413                "{name}: a clip starting at {start} s"
414            )));
415        }
416        let streamed_words = match streamed.get("data") {
417            Some(Value::Array(Array::U32(w))) => w.as_slice(),
418            Some(Value::Array(a)) if a.is_empty() => &[],
419            _ => return Err(invalid(format!("{name}: streamed clip data is not 32-bit words"))),
420        };
421        let streamed_count = usize::try_from(int(streamed, "curveCount")?)
422            .map_err(|_| invalid(format!("{name}: negative curve count")))?;
423        let (mut curves, worst) = if streamed_count == 0 && streamed_words.is_empty() {
424            (Vec::new(), 0.0)
425        } else {
426            read_streamed(&name, streamed_words, streamed_count)?
427        };
428        curves.extend(read_dense(&name, dense)?);
429        for v in floats(constant, "data")? {
430            curves.push(Curve {
431                storage: Storage::Constant,
432                keys: vec![Key {
433                    time: 0.0,
434                    value: v,
435                    in_slope: 0.0,
436                    out_slope: 0.0,
437                }],
438            });
439        }
440        // the bindings take the curves in order
441        let mut bindings = Vec::new();
442        let constant_bindings = c
443            .get("m_ClipBindingConstant")
444            .ok_or_else(|| invalid(format!("{name}: no m_ClipBindingConstant")))?;
445        for b in list(constant_bindings, "genericBindings")? {
446            bindings.push(GenericBinding {
447                path: u32::try_from(int(b, "path")?)
448                    .map_err(|_| invalid(format!("{name}: a path hash beyond 32 bits")))?,
449                attribute: u32::try_from(int(b, "attribute")?)
450                    .map_err(|_| invalid(format!("{name}: an attribute beyond 32 bits")))?,
451                type_id: int(b, "typeID")?,
452                is_pptr: int(b, "isPPtrCurve")? != 0,
453            });
454        }
455        if let Some(b) = bindings.iter().find(|b| b.is_pptr) {
456            return Err(Error::Unsupported(format!(
457                "{name}: a PPtr curve (attribute {})",
458                b.attribute
459            )));
460        }
461        let wanted: usize = bindings.iter().map(GenericBinding::width).sum();
462        if wanted != curves.len() {
463            return Err(invalid(format!(
464                "{name}: the bindings take {wanted} curves, the clip stores {}",
465                curves.len()
466            )));
467        }
468        let mut rest = curves.into_iter();
469        let tracks = bindings
470            .into_iter()
471            .map(|binding| {
472                let curves = rest.by_ref().take(binding.width()).collect();
473                Track { binding, curves }
474            })
475            .collect();
476        let looping = muscle
477            .get("m_LoopTime")
478            .is_some_and(|l| matches!(l, Value::Bool(true)) || l.as_i64().is_some_and(|v| v != 0));
479        Ok(ClipData {
480            name,
481            duration: stop - start,
482            looping,
483            tracks,
484            events: list(&c, "m_Events")?.len(),
485            worst_continuity_error: worst,
486        })
487    }
488}
489
490/// The hash a clip's binding names a path by: the CRC-32 (the zlib one) of the path of GameObject names below the
491/// Animator's GameObject, empty for the GameObject itself.
492pub fn path_hash(path: &str) -> u32 {
493    crate::animator::name_hash(path)
494}
495
496/// The clips an AnimatorController lists (`m_AnimationClips`), in order, null entries left out.
497pub fn controller_clips(player: &Player, controller: &ObjRef) -> Result<Vec<ObjRef>> {
498    let c = player.decode(controller)?;
499    let mut out = Vec::new();
500    for p in list(&c, "m_AnimationClips")? {
501        if let Some(clip) = player.resolve(&controller.file, p)? {
502            out.push(clip);
503        }
504    }
505    Ok(out)
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    fn key(time: f32, value: f32, in_slope: f32, out_slope: f32) -> Key {
513        Key {
514            time,
515            value,
516            in_slope,
517            out_slope,
518        }
519    }
520
521    fn frame(time: f32, keys: &[(i32, [f32; 4])]) -> Vec<u32> {
522        let mut words = vec![time.to_bits(), keys.len() as u32];
523        for (index, c) in keys {
524            words.push(*index as u32);
525            words.extend(c.iter().map(|x| x.to_bits()));
526        }
527        words
528    }
529
530    /// The initial frame, two keys of one curve and the end frame: the keys' values, outgoing slopes, the incoming
531    /// slope of the second as the first cubic's derivative at its end, and the continuity of the decode.
532    #[test]
533    fn a_streamed_curve_decodes_to_keys_with_slopes() {
534        // a cubic from 1 at t=0 over 2 s: v(dt) = 0.5 dt^3 - dt^2 + 3 dt + 1; its end value 1+6-4+4=7... at dt = 2
535        let (c0, c1, c2, c3) = (0.5f32, -1.0, 3.0, 1.0);
536        let end = ((c0 * 2.0 + c1) * 2.0 + c2) * 2.0 + c3;
537        let mut words = frame(-f32::MAX, &[(0, [0.0, 0.0, 0.0, 1.0])]);
538        words.extend(frame(0.0, &[(0, [c0, c1, c2, c3])]));
539        words.extend(frame(2.0, &[(0, [0.0, 0.0, 0.5, end])]));
540        words.extend(frame(f32::INFINITY, &[]));
541        let (curves, worst) = read_streamed("t", &words, 1).unwrap();
542        assert_eq!(worst, 0.0);
543        let k = &curves[0].keys;
544        assert_eq!(k.len(), 2);
545        assert_eq!((k[0].time, k[0].value, k[0].out_slope), (0.0, 1.0, 3.0));
546        // the derivative at the end of the first segment: 3 c0 dt^2 + 2 c1 dt + c2 = 6 - 4 + 3
547        assert_eq!(
548            (k[1].time, k[1].value, k[1].in_slope, k[1].out_slope),
549            (2.0, end, 5.0, 0.5)
550        );
551        // the Hermite spline through these keys is the stored cubic
552        let (v, s, _) = curves[0].sample(1.0);
553        assert!((v - (0.5 - 1.0 + 3.0 + 1.0)).abs() < 1e-5, "{v}");
554        assert!((s - (1.5 - 2.0 + 3.0)).abs() < 1e-5, "{s}");
555        assert_eq!(curves[0].sample(0.0), (1.0, 0.0, 3.0));
556        assert_eq!(curves[0].sample(2.0), (end, 5.0, 0.0));
557    }
558
559    /// A curve with only its initial entry is constant; frames that do not end at +inf, a key of a curve beyond the
560    /// count and times going back are refused.
561    #[test]
562    fn streamed_clips_refuse_what_is_not_the_format() {
563        let mut words = frame(-f32::MAX, &[(0, [0.0, 0.0, 0.0, 4.0])]);
564        words.extend(frame(f32::INFINITY, &[]));
565        let (curves, _) = read_streamed("t", &words, 1).unwrap();
566        assert_eq!(curves[0].keys, vec![key(0.0, 4.0, 0.0, 0.0)]);
567        let refused = |words: &[u32], n: usize| read_streamed("t", words, n).unwrap_err().to_string();
568        assert!(refused(&words[..words.len() - 2], 1).contains("do not end"));
569        assert!(refused(&words, 0).contains("curve 0 of 0"));
570        let mut back = frame(1.0, &[]);
571        back.extend(frame(0.5, &[]));
572        back.extend(frame(f32::INFINITY, &[]));
573        assert!(refused(&back, 1).contains("after"));
574    }
575
576    /// A curve count nothing in the data backs is refused before a vector is made for it, with the offset of the bad
577    /// bytes where there is one.
578    #[test]
579    fn a_streamed_curve_count_is_bounded_by_the_data() {
580        let mut words = frame(-f32::MAX, &[(0, [0.0, 0.0, 0.0, 4.0])]);
581        words.extend(frame(f32::INFINITY, &[]));
582        for n in [2, 1 << 40, usize::MAX] {
583            let e = read_streamed("t", &words, n).unwrap_err().to_string();
584            assert!(e.contains("hold at most 1 keys"), "{e}");
585        }
586        // a key naming curve 5 of 1, at the byte the key starts (the frame header takes 8)
587        let mut words = frame(-f32::MAX, &[(5, [0.0; 4])]);
588        words.extend(frame(f32::INFINITY, &[]));
589        let e = read_streamed("t", &words, 1).unwrap_err().to_string();
590        assert!(e.contains("curve 5 of 1 at byte 8"), "{e}");
591        let mut back = frame(1.0, &[]);
592        back.extend(frame(0.5, &[]));
593        let e = read_streamed("t", &back, 0).unwrap_err().to_string();
594        assert!(e.contains("byte 8"), "{e}");
595    }
596
597    fn dense(curves: i64, frames: i64, samples: Vec<f32>) -> Value {
598        let f = |k: &str, v: Value| (std::sync::Arc::<str>::from(k), v);
599        Value::Struct(vec![
600            f("m_CurveCount", Value::Int(curves)),
601            f("m_FrameCount", Value::Int(frames)),
602            f("m_SampleRate", Value::Float(2.0)),
603            f("m_BeginTime", Value::Float(0.0)),
604            f("m_SampleArray", Value::Array(Array::F32(samples))),
605        ])
606    }
607
608    /// The dense block's frame and curve counts are multiplied without overflow and must be backed by samples: a huge
609    /// count with no samples makes no curves.
610    #[test]
611    fn dense_counts_are_checked_against_the_samples() {
612        for (curves, frames) in [(i64::MAX, 3), (3, i64::MAX), (i64::MAX, i64::MAX), (1 << 40, 0)] {
613            let e = read_dense("t", &dense(curves, frames, vec![]))
614                .unwrap_err()
615                .to_string();
616            assert!(e.contains("dense clip"), "{e}");
617        }
618        let curves = read_dense("t", &dense(2, 2, vec![0.0, 10.0, 1.0, 20.0])).unwrap();
619        assert_eq!(curves.len(), 2);
620        assert_eq!(
621            curves[1].keys,
622            vec![key(0.0, 10.0, 0.0, 20.0), key(0.5, 20.0, 20.0, 0.0)]
623        );
624        assert_eq!(read_dense("t", &dense(0, 0, vec![])).unwrap(), vec![]);
625    }
626
627    /// A time that is not a number samples to NaN instead of indexing before the first key.
628    #[test]
629    fn sampling_at_nan_does_not_panic() {
630        let curve = Curve {
631            storage: Storage::Constant,
632            keys: vec![key(0.0, 1.0, 0.0, 0.0), key(1.0, 2.0, 0.0, 0.0)],
633        };
634        assert!(curve.sample(f32::NAN).0.is_nan());
635    }
636
637    /// A Hermite curve samples to its keys, and a transform track of three curves with different key times becomes
638    /// one Hermite channel at the union of the times whose pieces are the same cubics; constants give one key, dense
639    /// curves a Linear channel.
640    #[test]
641    fn path_hashes_are_the_crc32_of_the_path() {
642        assert_eq!(path_hash(""), 0);
643        assert_eq!(path_hash("scarf"), 2_702_068_705);
644        assert_eq!(path_hash("root/Lchain1/Lchain2/Lchain3"), 3_634_936_137);
645        assert_eq!(path_hash("root/Rchain1/Rchain2"), 356_844_446);
646    }
647}