Skip to main content

mmd_anim_runtime/reduce/
mod.rs

1use std::cmp::Ordering;
2use std::time::Duration;
3#[cfg(not(target_family = "wasm"))]
4use std::time::Instant;
5
6use glam::{EulerRot, Mat3, Mat4, Quat, Vec3, Vec3A};
7#[cfg(not(target_family = "wasm"))]
8use rayon::prelude::*;
9#[cfg(not(target_family = "wasm"))]
10use rayon::{ThreadPool, ThreadPoolBuilder};
11use thiserror::Error;
12
13use crate::{BoneIndex, InterpolationScalar, ModelArena};
14
15const AFFINE_EPSILON: f32 = 1.0e-4;
16// Below this point the one-time prefit costs more than the global passes it removes.
17const DCC_LOCAL_PREFIT_MIN_FRAMES: usize = 90;
18
19#[cfg(not(target_family = "wasm"))]
20fn reduction_timer_start() -> Instant {
21    Instant::now()
22}
23
24#[cfg(target_family = "wasm")]
25fn reduction_timer_start() {}
26
27#[cfg(not(target_family = "wasm"))]
28fn reduction_timer_elapsed(started: Instant) -> Duration {
29    started.elapsed()
30}
31
32#[cfg(target_family = "wasm")]
33fn reduction_timer_elapsed(_: ()) -> Duration {
34    Duration::ZERO
35}
36
37#[derive(Debug, Clone, Copy)]
38pub struct DensePoseSequenceView<'a> {
39    world_matrices: &'a [Mat4],
40    morph_weights: &'a [f32],
41    frame_count: usize,
42    bone_count: usize,
43    morph_count: usize,
44    start_frame: f32,
45    frame_step: f32,
46}
47
48impl<'a> DensePoseSequenceView<'a> {
49    pub fn new(
50        world_matrices: &'a [Mat4],
51        morph_weights: &'a [f32],
52        frame_count: usize,
53        bone_count: usize,
54        morph_count: usize,
55        start_frame: f32,
56        frame_step: f32,
57    ) -> Result<Self, PoseReductionError> {
58        if frame_count == 0 {
59            return Err(PoseReductionError::EmptySequence);
60        }
61        if bone_count == 0 {
62            return Err(PoseReductionError::EmptySkeleton);
63        }
64        if !start_frame.is_finite() || !frame_step.is_finite() || frame_step <= 0.0 {
65            return Err(PoseReductionError::InvalidTimeBase);
66        }
67        let mut previous_frame = start_frame;
68        for sample_index in 1..frame_count {
69            let frame = start_frame + sample_index as f32 * frame_step;
70            if !frame.is_finite() || frame <= previous_frame {
71                return Err(PoseReductionError::InvalidTimeBase);
72            }
73            previous_frame = frame;
74        }
75        let expected_world = frame_count
76            .checked_mul(bone_count)
77            .ok_or(PoseReductionError::LengthOverflow)?;
78        let expected_morph = frame_count
79            .checked_mul(morph_count)
80            .ok_or(PoseReductionError::LengthOverflow)?;
81        if world_matrices.len() != expected_world {
82            return Err(PoseReductionError::InvalidWorldMatrixCount {
83                actual: world_matrices.len(),
84                expected: expected_world,
85            });
86        }
87        if morph_weights.len() != expected_morph {
88            return Err(PoseReductionError::InvalidMorphWeightCount {
89                actual: morph_weights.len(),
90                expected: expected_morph,
91            });
92        }
93        Ok(Self {
94            world_matrices,
95            morph_weights,
96            frame_count,
97            bone_count,
98            morph_count,
99            start_frame,
100            frame_step,
101        })
102    }
103
104    pub fn frame_count(&self) -> usize {
105        self.frame_count
106    }
107    pub fn bone_count(&self) -> usize {
108        self.bone_count
109    }
110    pub fn morph_count(&self) -> usize {
111        self.morph_count
112    }
113    pub fn start_frame(&self) -> f32 {
114        self.start_frame
115    }
116    pub fn frame_step(&self) -> f32 {
117        self.frame_step
118    }
119
120    fn world_matrix(&self, frame: usize, bone: usize) -> Mat4 {
121        self.world_matrices[frame * self.bone_count + bone]
122    }
123
124    fn morph_weight(&self, frame: usize, morph: usize) -> f32 {
125        self.morph_weights[frame * self.morph_count + morph]
126    }
127
128    fn sample_frame(&self, sample_index: usize) -> f32 {
129        self.start_frame + sample_index as f32 * self.frame_step
130    }
131}
132
133#[derive(Debug, Clone, PartialEq)]
134pub struct SkeletonSnapshot {
135    parent_indices: Box<[i32]>,
136    rest_local_translations: Box<[Vec3A]>,
137    rest_local_rotations: Box<[Quat]>,
138    evaluation_order: Box<[usize]>,
139    morph_count: usize,
140    model_identity: u64,
141}
142
143impl SkeletonSnapshot {
144    pub fn new(
145        parent_indices: Vec<i32>,
146        rest_local_translations: Vec<Vec3A>,
147        rest_local_rotations: Vec<Quat>,
148        morph_count: usize,
149        model_identity: u64,
150    ) -> Result<Self, PoseReductionError> {
151        if parent_indices.is_empty() {
152            return Err(PoseReductionError::EmptySkeleton);
153        }
154        if rest_local_translations.len() != parent_indices.len()
155            || rest_local_rotations.len() != parent_indices.len()
156        {
157            return Err(PoseReductionError::InvalidSkeletonLengths);
158        }
159        for (bone, &parent) in parent_indices.iter().enumerate() {
160            if parent < -1 || parent >= parent_indices.len() as i32 || parent == bone as i32 {
161                return Err(PoseReductionError::InvalidParent { bone, parent });
162            }
163            if !rest_local_translations[bone].is_finite()
164                || !quat_is_finite(rest_local_rotations[bone])
165            {
166                return Err(PoseReductionError::NonFiniteSkeleton { bone });
167            }
168        }
169        let evaluation_order = build_evaluation_order(&parent_indices)?;
170        Ok(Self {
171            parent_indices: parent_indices.into_boxed_slice(),
172            rest_local_translations: rest_local_translations.into_boxed_slice(),
173            rest_local_rotations: rest_local_rotations
174                .into_iter()
175                .map(normalize_quat)
176                .collect::<Vec<_>>()
177                .into_boxed_slice(),
178            evaluation_order: evaluation_order.into_boxed_slice(),
179            morph_count,
180            model_identity,
181        })
182    }
183
184    pub fn from_model(model: &ModelArena, model_identity: u64) -> Result<Self, PoseReductionError> {
185        Self::from_model_with_morph_count(model, model_identity, model.morph_count() as usize)
186    }
187
188    pub fn from_model_with_morph_count(
189        model: &ModelArena,
190        model_identity: u64,
191        morph_count: usize,
192    ) -> Result<Self, PoseReductionError> {
193        let mut parents = Vec::with_capacity(model.bone_count());
194        let mut translations = Vec::with_capacity(model.bone_count());
195        for bone in 0..model.bone_count() {
196            let bone_index = BoneIndex(bone as u32);
197            let parent = model.parent_index(bone_index);
198            parents.push(parent.map_or(-1, |value| value.0 as i32));
199            let parent_position = parent.map_or(Vec3A::ZERO, |value| model.rest_position(value));
200            translations.push(model.rest_position(bone_index) - parent_position);
201        }
202        Self::new(
203            parents,
204            translations,
205            vec![Quat::IDENTITY; model.bone_count()],
206            morph_count,
207            model_identity,
208        )
209    }
210
211    pub fn bone_count(&self) -> usize {
212        self.parent_indices.len()
213    }
214    pub fn morph_count(&self) -> usize {
215        self.morph_count
216    }
217    pub fn model_identity(&self) -> u64 {
218        self.model_identity
219    }
220    pub fn parent_indices(&self) -> &[i32] {
221        &self.parent_indices
222    }
223    pub fn rest_local_translations(&self) -> &[Vec3A] {
224        &self.rest_local_translations
225    }
226    pub fn rest_local_rotations(&self) -> &[Quat] {
227        &self.rest_local_rotations
228    }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232pub enum ReductionTarget {
233    LinearSlerp,
234    VmdBezier,
235    DccCubic,
236}
237
238#[derive(Debug, Clone, Copy, Default, PartialEq)]
239pub struct DccCubicSegment {
240    pub translation_out_tangent: Vec3A,
241    pub translation_in_tangent: Vec3A,
242    pub rotation_start_euler_xyz: Vec3A,
243    pub rotation_end_euler_xyz: Vec3A,
244    pub rotation_out_tangent: Vec3A,
245    pub rotation_in_tangent: Vec3A,
246}
247
248#[derive(Debug, Clone, Copy, Default, PartialEq)]
249pub struct DccScalarSegment {
250    pub out_tangent: f32,
251    pub in_tangent: f32,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub struct QuantizedBezier {
256    pub x1: u8,
257    pub y1: u8,
258    pub x2: u8,
259    pub y2: u8,
260}
261
262impl QuantizedBezier {
263    pub const LINEAR: Self = Self {
264        x1: 20,
265        y1: 20,
266        x2: 107,
267        y2: 107,
268    };
269
270    pub fn evaluate(self, time: f32) -> f32 {
271        InterpolationScalar {
272            x1: self.x1.min(127),
273            y1: self.y1.min(127),
274            x2: self.x2.min(127),
275            y2: self.y2.min(127),
276        }
277        .evaluate(time)
278    }
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub struct VmdBoneInterpolation {
283    pub translation: [QuantizedBezier; 3],
284    pub rotation: QuantizedBezier,
285}
286
287impl VmdBoneInterpolation {
288    pub const LINEAR: Self = Self {
289        translation: [QuantizedBezier::LINEAR; 3],
290        rotation: QuantizedBezier::LINEAR,
291    };
292}
293
294#[derive(Debug, Clone, Copy, PartialEq)]
295pub struct ReductionTolerances {
296    pub local_position: f32,
297    pub local_rotation_radians: f32,
298    pub world_position: f32,
299    pub world_rotation_radians: f32,
300    pub morph_weight: f32,
301}
302
303impl ReductionTolerances {
304    fn validate(self) -> Result<Self, PoseReductionError> {
305        let values = [
306            self.local_position,
307            self.local_rotation_radians,
308            self.world_position,
309            self.world_rotation_radians,
310            self.morph_weight,
311        ];
312        if values
313            .iter()
314            .any(|value| !value.is_finite() || *value < 0.0)
315        {
316            return Err(PoseReductionError::InvalidTolerance);
317        }
318        Ok(self)
319    }
320}
321
322impl Default for ReductionTolerances {
323    fn default() -> Self {
324        Self {
325            local_position: 1.0e-4,
326            local_rotation_radians: 1.0e-4,
327            world_position: 1.0e-4,
328            world_rotation_radians: 1.0e-4,
329            morph_weight: 1.0e-4,
330        }
331    }
332}
333
334#[derive(Debug, Clone, Copy, PartialEq)]
335pub struct ReducedBoneKey {
336    pub sample_index: usize,
337    pub translation: Vec3A,
338    pub rotation: Quat,
339    pub vmd_interpolation: VmdBoneInterpolation,
340    pub dcc_segment: DccCubicSegment,
341}
342
343#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
344struct ReductionKeyProvenance {
345    bits: u8,
346}
347
348impl ReductionKeyProvenance {
349    const ENDPOINT: u8 = 1 << 0;
350    const LOCAL_PREFIT: u8 = 1 << 1;
351    const VALIDATION_FAILURE: u8 = 1 << 2;
352    const ANCESTOR_PROPAGATION: u8 = 1 << 3;
353
354    fn endpoint() -> Self {
355        Self {
356            bits: Self::ENDPOINT,
357        }
358    }
359
360    fn local_prefit() -> Self {
361        Self {
362            bits: Self::LOCAL_PREFIT,
363        }
364    }
365
366    fn validation_failure() -> Self {
367        Self {
368            bits: Self::VALIDATION_FAILURE,
369        }
370    }
371
372    fn ancestor_propagation() -> Self {
373        Self {
374            bits: Self::ANCESTOR_PROPAGATION,
375        }
376    }
377
378    fn is_endpoint(self) -> bool {
379        self.bits & Self::ENDPOINT != 0
380    }
381
382    fn is_local_prefit(self) -> bool {
383        self.bits & Self::LOCAL_PREFIT != 0
384    }
385
386    fn is_validation_failure(self) -> bool {
387        self.bits & Self::VALIDATION_FAILURE != 0
388    }
389
390    fn is_ancestor_propagation(self) -> bool {
391        self.bits & Self::ANCESTOR_PROPAGATION != 0
392    }
393}
394
395#[derive(Debug, Clone, PartialEq)]
396pub struct ReducedBoneTrack {
397    keys: Box<[ReducedBoneKey]>,
398}
399
400impl ReducedBoneTrack {
401    pub fn keys(&self) -> &[ReducedBoneKey] {
402        &self.keys
403    }
404}
405
406#[derive(Debug, Clone, Copy, PartialEq)]
407pub struct ReducedMorphKey {
408    pub sample_index: usize,
409    pub weight: f32,
410    pub dcc_segment: DccScalarSegment,
411}
412
413#[derive(Debug, Clone, PartialEq)]
414pub struct ReducedMorphTrack {
415    keys: Box<[ReducedMorphKey]>,
416}
417
418impl ReducedMorphTrack {
419    pub fn keys(&self) -> &[ReducedMorphKey] {
420        &self.keys
421    }
422}
423
424#[derive(Debug, Clone, Copy, Default, PartialEq)]
425pub struct PoseReductionReport {
426    pub source_bone_key_count: usize,
427    pub reduced_bone_key_count: usize,
428    pub source_morph_key_count: usize,
429    pub reduced_morph_key_count: usize,
430    pub max_local_position_error: f32,
431    pub max_local_rotation_error_radians: f32,
432    pub max_world_position_error: f32,
433    pub max_world_rotation_error_radians: f32,
434    pub max_morph_weight_error: f32,
435}
436
437#[derive(Debug, Clone, Default, PartialEq, Eq)]
438pub struct ReductionWorkStats {
439    pub global_validation_passes: usize,
440    pub candidate_rebuilds: usize,
441    pub candidate_bone_track_rebuilds: usize,
442    pub candidate_morph_track_rebuilds: usize,
443    pub local_prefit_bone_segment_fits: usize,
444    pub local_prefit_morph_segment_fits: usize,
445    pub local_prefit_bone_key_additions: usize,
446    pub local_prefit_morph_key_additions: usize,
447    pub local_prefit_bone_samples: usize,
448    pub local_prefit_morph_samples: usize,
449    pub dcc_bone_segment_fits: usize,
450    pub dcc_morph_segment_fits: usize,
451    pub bone_samples: usize,
452    pub morph_samples: usize,
453    pub world_rebuilds: usize,
454    /// Number of cached candidate world transforms recomputed during validation.
455    pub world_bone_recomputes: usize,
456    pub world_rotation_decompositions: usize,
457    pub normal_key_additions: usize,
458    pub ancestor_key_additions: usize,
459    /// Number of ancestor-propagated key candidates tested by reverse pruning.
460    pub ancestor_prune_attempts: usize,
461    /// Number of ancestor-propagated keys removed after subtree validation.
462    pub ancestor_pruned_keys: usize,
463    pub added_keys_per_pass: Vec<usize>,
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467enum ValidationMode {
468    Incremental,
469    FullScan,
470}
471
472#[derive(Debug, Clone)]
473struct DirtyRanges {
474    bone_local: Vec<Vec<std::ops::RangeInclusive<usize>>>,
475    morph: Vec<Vec<std::ops::RangeInclusive<usize>>>,
476}
477
478impl DirtyRanges {
479    fn full(frame_count: usize, bone_count: usize, morph_count: usize) -> Self {
480        let range = || vec![0..=frame_count.saturating_sub(1)];
481        Self {
482            bone_local: (0..bone_count).map(|_| range()).collect(),
483            morph: (0..morph_count).map(|_| range()).collect(),
484        }
485    }
486
487    fn empty(bone_count: usize, morph_count: usize) -> Self {
488        Self {
489            bone_local: vec![Vec::new(); bone_count],
490            morph: vec![Vec::new(); morph_count],
491        }
492    }
493
494    fn mark_bone(&mut self, bone: usize, range: std::ops::RangeInclusive<usize>) {
495        insert_dirty_range(&mut self.bone_local[bone], range);
496    }
497
498    fn mark_morph(&mut self, morph: usize, range: std::ops::RangeInclusive<usize>) {
499        insert_dirty_range(&mut self.morph[morph], range);
500    }
501}
502
503fn insert_dirty_range(
504    target: &mut Vec<std::ops::RangeInclusive<usize>>,
505    range: std::ops::RangeInclusive<usize>,
506) {
507    target.push(range);
508    target.sort_by_key(|value| *value.start());
509    let mut merged: Vec<std::ops::RangeInclusive<usize>> = Vec::with_capacity(target.len());
510    for current in target.drain(..) {
511        if let Some(previous) = merged.last_mut()
512            && *current.start() <= previous.end().saturating_add(1)
513        {
514            let start = *previous.start();
515            *previous = start..=(*previous.end()).max(*current.end());
516        } else {
517            merged.push(current);
518        }
519    }
520    *target = merged;
521}
522
523#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
524pub struct ReductionTimings {
525    pub local_prefit: Duration,
526    pub candidate_build: Duration,
527    pub error_measure: Duration,
528    pub dcc_fit: Duration,
529    /// Time spent testing and applying ancestor reverse-prune candidates.
530    pub ancestor_prune: Duration,
531}
532
533#[cfg(not(target_family = "wasm"))]
534type ReductionThreadPool = ThreadPool;
535
536#[cfg(target_family = "wasm")]
537struct ReductionThreadPool;
538
539#[derive(Debug, Clone)]
540pub struct ReducedPoseSequence {
541    snapshot: SkeletonSnapshot,
542    target: ReductionTarget,
543    start_frame: f32,
544    frame_step: f32,
545    frame_count: usize,
546    sample_frames: Box<[f32]>,
547    bone_tracks: Box<[ReducedBoneTrack]>,
548    morph_tracks: Box<[ReducedMorphTrack]>,
549    report: PoseReductionReport,
550    work_stats: ReductionWorkStats,
551    timings: ReductionTimings,
552}
553
554impl PartialEq for ReducedPoseSequence {
555    fn eq(&self, other: &Self) -> bool {
556        self.snapshot == other.snapshot
557            && self.target == other.target
558            && self.start_frame == other.start_frame
559            && self.frame_step == other.frame_step
560            && self.frame_count == other.frame_count
561            && self.sample_frames == other.sample_frames
562            && self.bone_tracks == other.bone_tracks
563            && self.morph_tracks == other.morph_tracks
564            && self.report == other.report
565            && self.work_stats == other.work_stats
566    }
567}
568
569impl ReducedPoseSequence {
570    pub fn snapshot(&self) -> &SkeletonSnapshot {
571        &self.snapshot
572    }
573    pub fn target(&self) -> ReductionTarget {
574        self.target
575    }
576    pub fn start_frame(&self) -> f32 {
577        self.start_frame
578    }
579    pub fn frame_step(&self) -> f32 {
580        self.frame_step
581    }
582    pub fn frame_count(&self) -> usize {
583        self.frame_count
584    }
585    pub fn sample_frames(&self) -> &[f32] {
586        &self.sample_frames
587    }
588    pub fn bone_tracks(&self) -> &[ReducedBoneTrack] {
589        &self.bone_tracks
590    }
591    pub fn morph_tracks(&self) -> &[ReducedMorphTrack] {
592        &self.morph_tracks
593    }
594    pub fn report(&self) -> PoseReductionReport {
595        self.report
596    }
597    pub fn work_stats(&self) -> &ReductionWorkStats {
598        &self.work_stats
599    }
600    pub fn timings(&self) -> ReductionTimings {
601        self.timings
602    }
603
604    pub fn sample(&self, frame: f32) -> Result<ReducedPoseSample, PoseReductionError> {
605        let mut scratch = ReducedPoseScratch::default();
606        self.sample_into(frame, &mut scratch)?;
607        Ok(ReducedPoseSample {
608            local_translations: scratch.local_translations,
609            local_rotations: scratch.local_rotations,
610            world_matrices: scratch.world_matrices,
611            morph_weights: scratch.morph_weights,
612        })
613    }
614
615    fn sample_into(
616        &self,
617        frame: f32,
618        scratch: &mut ReducedPoseScratch,
619    ) -> Result<(), PoseReductionError> {
620        if !frame.is_finite() {
621            return Err(PoseReductionError::InvalidSampleTime);
622        }
623        scratch.prepare(self.snapshot.bone_count(), self.snapshot.morph_count());
624        for (bone, track) in self.bone_tracks.iter().enumerate() {
625            let (translation, rotation) =
626                sample_bone_track(track, &self.sample_frames, frame, self.target);
627            scratch.local_translations[bone] = translation;
628            scratch.local_rotations[bone] = rotation;
629        }
630        for (morph, track) in self.morph_tracks.iter().enumerate() {
631            scratch.morph_weights[morph] =
632                sample_morph_track(track, &self.sample_frames, frame, self.target);
633        }
634        build_world_matrices_into(
635            &self.snapshot,
636            &scratch.local_translations,
637            &scratch.local_rotations,
638            &mut scratch.world_matrices,
639        );
640        Ok(())
641    }
642
643    pub fn validate_model(
644        &self,
645        model_identity: u64,
646        bone_count: usize,
647        morph_count: usize,
648    ) -> bool {
649        self.snapshot.model_identity == model_identity
650            && self.snapshot.bone_count() == bone_count
651            && self.snapshot.morph_count == morph_count
652    }
653}
654
655#[derive(Debug, Default)]
656struct ReducedPoseScratch {
657    local_translations: Vec<Vec3A>,
658    local_rotations: Vec<Quat>,
659    world_matrices: Vec<Mat4>,
660    morph_weights: Vec<f32>,
661}
662
663impl ReducedPoseScratch {
664    fn prepare(&mut self, bone_count: usize, morph_count: usize) {
665        self.local_translations.resize(bone_count, Vec3A::ZERO);
666        self.local_rotations.resize(bone_count, Quat::IDENTITY);
667        self.world_matrices.resize(bone_count, Mat4::IDENTITY);
668        self.morph_weights.resize(morph_count, 0.0);
669    }
670}
671
672#[derive(Debug, Clone, PartialEq)]
673pub struct ReducedPoseSample {
674    pub local_translations: Vec<Vec3A>,
675    pub local_rotations: Vec<Quat>,
676    pub world_matrices: Vec<Mat4>,
677    pub morph_weights: Vec<f32>,
678}
679
680#[derive(Debug, Error, Clone, PartialEq)]
681pub enum PoseReductionError {
682    #[error("dense pose sequence must contain at least one frame")]
683    EmptySequence,
684    #[error("skeleton must contain at least one bone")]
685    EmptySkeleton,
686    #[error("invalid or non-positive dense pose time base")]
687    InvalidTimeBase,
688    #[error("dense pose buffer length overflow")]
689    LengthOverflow,
690    #[error("world matrix count {actual} does not match expected {expected}")]
691    InvalidWorldMatrixCount { actual: usize, expected: usize },
692    #[error("morph weight count {actual} does not match expected {expected}")]
693    InvalidMorphWeightCount { actual: usize, expected: usize },
694    #[error("skeleton arrays have inconsistent lengths")]
695    InvalidSkeletonLengths,
696    #[error("bone {bone} has invalid parent index {parent}")]
697    InvalidParent { bone: usize, parent: i32 },
698    #[error("skeleton hierarchy contains a cycle at bone {bone}")]
699    SkeletonCycle { bone: usize },
700    #[error("bone {bone} has non-finite rest data")]
701    NonFiniteSkeleton { bone: usize },
702    #[error("dense pose skeleton counts do not match snapshot")]
703    SnapshotMismatch,
704    #[error("reduction tolerance must be finite and non-negative")]
705    InvalidTolerance,
706    #[error("frame {frame}, bone {bone} contains a non-finite matrix")]
707    NonFiniteMatrix { frame: usize, bone: usize },
708    #[error("frame {frame}, bone {bone} contains scale or shear")]
709    ScaleOrShear { frame: usize, bone: usize },
710    #[error("frame {frame}, bone {bone} has a singular parent transform")]
711    SingularParent { frame: usize, bone: usize },
712    #[error("frame {frame}, morph {morph} contains a non-finite weight")]
713    NonFiniteMorph { frame: usize, morph: usize },
714    #[error("sample time must be finite")]
715    InvalidSampleTime,
716    #[error("requested tolerance cannot be attained at source frame {frame}")]
717    ToleranceUnattainable { frame: usize },
718    #[error("failed to create pose reduction worker pool")]
719    WorkerPool,
720}
721
722pub fn reduce_dense_pose_sequence(
723    input: DensePoseSequenceView<'_>,
724    snapshot: SkeletonSnapshot,
725    tolerances: ReductionTolerances,
726    target: ReductionTarget,
727) -> Result<ReducedPoseSequence, PoseReductionError> {
728    reduce_dense_pose_sequence_with_worker_count(input, snapshot, tolerances, target, 0)
729}
730
731pub fn reduce_dense_pose_sequence_with_worker_count(
732    input: DensePoseSequenceView<'_>,
733    snapshot: SkeletonSnapshot,
734    tolerances: ReductionTolerances,
735    target: ReductionTarget,
736    worker_count: usize,
737) -> Result<ReducedPoseSequence, PoseReductionError> {
738    reduce_dense_pose_sequence_internal(
739        input,
740        snapshot,
741        tolerances,
742        target,
743        worker_count,
744        ValidationMode::Incremental,
745    )
746}
747
748fn reduce_dense_pose_sequence_internal(
749    input: DensePoseSequenceView<'_>,
750    snapshot: SkeletonSnapshot,
751    tolerances: ReductionTolerances,
752    target: ReductionTarget,
753    worker_count: usize,
754    validation_mode: ValidationMode,
755) -> Result<ReducedPoseSequence, PoseReductionError> {
756    let tolerances = tolerances.validate()?;
757    if input.bone_count != snapshot.bone_count() || input.morph_count != snapshot.morph_count() {
758        return Err(PoseReductionError::SnapshotMismatch);
759    }
760
761    let (local_translations, local_rotations) = decompose_dense_pose(input, &snapshot)?;
762    let (world_positions, world_rotations) = cache_dense_world_components(input)?;
763    let mut work_stats = ReductionWorkStats {
764        world_rotation_decompositions: input.frame_count * input.bone_count,
765        ..Default::default()
766    };
767    let mut timings = ReductionTimings::default();
768    let worker_count = resolve_reduction_worker_count(worker_count, input.frame_count);
769    let worker_pool = build_reduction_worker_pool(worker_count)?;
770    let local_euler_xyz = unwrap_euler_xyz(&local_rotations, input.frame_count, input.bone_count);
771    for frame in 0..input.frame_count {
772        for morph in 0..input.morph_count {
773            if !input.morph_weight(frame, morph).is_finite() {
774                return Err(PoseReductionError::NonFiniteMorph { frame, morph });
775            }
776        }
777    }
778
779    let mut bone_key_indices = vec![endpoint_indices(input.frame_count); input.bone_count];
780    let mut morph_key_indices = vec![endpoint_indices(input.frame_count); input.morph_count];
781
782    if target == ReductionTarget::DccCubic && input.frame_count >= DCC_LOCAL_PREFIT_MIN_FRAMES {
783        let dcc_prefit_started = reduction_timer_start();
784        for (bone, keys) in bone_key_indices.iter_mut().enumerate() {
785            let prefit = split_dcc_bone_track(
786                keys,
787                input,
788                bone,
789                &local_translations,
790                &local_rotations,
791                &local_euler_xyz,
792                tolerances,
793            );
794            work_stats.local_prefit_bone_segment_fits += prefit.segment_fits;
795            work_stats.local_prefit_bone_key_additions += prefit.key_additions;
796            work_stats.local_prefit_bone_samples += prefit.samples;
797        }
798        for (morph, keys) in morph_key_indices.iter_mut().enumerate() {
799            let prefit = split_dcc_morph_track(keys, input, morph, tolerances.morph_weight);
800            work_stats.local_prefit_morph_segment_fits += prefit.segment_fits;
801            work_stats.local_prefit_morph_key_additions += prefit.key_additions;
802            work_stats.local_prefit_morph_samples += prefit.samples;
803        }
804        timings.local_prefit += reduction_timer_elapsed(dcc_prefit_started);
805    } else if target == ReductionTarget::LinearSlerp {
806        for (bone, keys) in bone_key_indices.iter_mut().enumerate() {
807            split_bone_track(
808                keys,
809                input,
810                bone,
811                &local_translations,
812                &local_rotations,
813                tolerances,
814            );
815        }
816    }
817    if target != ReductionTarget::DccCubic {
818        for (morph, keys) in morph_key_indices.iter_mut().enumerate() {
819            split_morph_track(keys, input, morph, tolerances.morph_weight);
820        }
821    }
822    let mut bone_key_provenance = bone_key_indices
823        .iter()
824        .map(|indices| initial_key_provenance(indices, input.frame_count))
825        .collect::<Vec<_>>();
826    let mut morph_key_provenance = morph_key_indices
827        .iter()
828        .map(|indices| initial_key_provenance(indices, input.frame_count))
829        .collect::<Vec<_>>();
830
831    let mut candidate: Option<ReducedPoseSequence> = None;
832    // Keep the exact insertion order so reverse-prune remains deterministic even
833    // when several tracks receive a key for the same failing frame.
834    let mut ancestor_insertions = Vec::<(usize, usize)>::new();
835    let mut dirty = DirtyRanges::full(input.frame_count, input.bone_count, input.morph_count);
836    let mut validation_cache =
837        ValidationCache::new(input.frame_count, input.bone_count, input.morph_count);
838    loop {
839        work_stats.global_validation_passes += 1;
840        work_stats.candidate_rebuilds += 1;
841        if candidate.is_some() && matches!(validation_mode, ValidationMode::FullScan) {
842            dirty = DirtyRanges::full(input.frame_count, input.bone_count, input.morph_count);
843        }
844        let candidate_started = reduction_timer_start();
845        let local_pose = DenseLocalPose {
846            translations: &local_translations,
847            rotations: &local_rotations,
848            euler_xyz: &local_euler_xyz,
849        };
850        if let Some(sequence) = candidate.as_mut() {
851            rebuild_dirty_tracks(
852                sequence,
853                target,
854                input,
855                local_pose,
856                &bone_key_indices,
857                &morph_key_indices,
858                &dirty,
859                ReductionInstrumentation {
860                    work_stats: &mut work_stats,
861                    timings: &mut timings,
862                },
863            );
864        } else {
865            candidate = Some(build_sequence(
866                &snapshot,
867                target,
868                input,
869                local_pose,
870                &bone_key_indices,
871                &morph_key_indices,
872                ReductionInstrumentation {
873                    work_stats: &mut work_stats,
874                    timings: &mut timings,
875                },
876            ));
877        }
878        timings.candidate_build += reduction_timer_elapsed(candidate_started);
879        let error_measure_started = reduction_timer_start();
880        let (report, worst_by_track) = measure_error_cached(
881            candidate.as_ref().expect("candidate initialized"),
882            input,
883            DenseValidationPose {
884                translations: &local_translations,
885                rotations: &local_rotations,
886                world_positions: &world_positions,
887                world_rotations: &world_rotations,
888            },
889            tolerances,
890            &mut work_stats,
891            &dirty,
892            &mut validation_cache,
893            worker_pool.as_ref(),
894            worker_count,
895        )?;
896        timings.error_measure += reduction_timer_elapsed(error_measure_started);
897        let mut failing = worst_by_track
898            .into_iter()
899            .flatten()
900            .filter(|worst| worst.normalized_error > 1.0)
901            .collect::<Vec<_>>();
902        failing.sort_by(compare_worst_errors);
903        if failing.is_empty() {
904            work_stats.added_keys_per_pass.push(0);
905            let mut result = candidate.expect("candidate initialized");
906            let mut final_report = report;
907            if !ancestor_insertions.is_empty() {
908                let prune_started = reduction_timer_start();
909                let pruned = prune_ancestor_keys(
910                    &mut result,
911                    input,
912                    local_pose,
913                    DenseValidationPose {
914                        translations: &local_translations,
915                        rotations: &local_rotations,
916                        world_positions: &world_positions,
917                        world_rotations: &world_rotations,
918                    },
919                    tolerances,
920                    &mut bone_key_indices,
921                    &mut bone_key_provenance,
922                    &ancestor_insertions,
923                    &mut validation_cache,
924                    &mut work_stats,
925                )?;
926                timings.ancestor_prune += reduction_timer_elapsed(prune_started);
927                if pruned > 0 {
928                    // Recompute aggregate maxima once after all accepted removals.
929                    // Candidate checks are subtree/range scoped; this final pass is
930                    // intentionally the only full report scan for the prune phase.
931                    let (pruned_report, _) = measure_error_cached(
932                        &result,
933                        input,
934                        DenseValidationPose {
935                            translations: &local_translations,
936                            rotations: &local_rotations,
937                            world_positions: &world_positions,
938                            world_rotations: &world_rotations,
939                        },
940                        tolerances,
941                        &mut work_stats,
942                        &DirtyRanges::full(input.frame_count, input.bone_count, input.morph_count),
943                        &mut validation_cache,
944                        worker_pool.as_ref(),
945                        worker_count,
946                    )?;
947                    final_report = pruned_report;
948                }
949            }
950            result.report = PoseReductionReport {
951                source_bone_key_count: input.frame_count * input.bone_count,
952                reduced_bone_key_count: result
953                    .bone_tracks
954                    .iter()
955                    .map(|track| track.keys.len())
956                    .sum(),
957                source_morph_key_count: input.frame_count * input.morph_count,
958                reduced_morph_key_count: result
959                    .morph_tracks
960                    .iter()
961                    .map(|track| track.keys.len())
962                    .sum(),
963                ..final_report
964            };
965            result.work_stats = work_stats;
966            result.timings = timings;
967            return Ok(result);
968        }
969        let mut inserted = false;
970        let mut added_this_pass = 0;
971        let mut next_dirty = DirtyRanges::empty(input.bone_count, input.morph_count);
972        let first_failure_frame = failing[0].frame;
973        for worst in failing {
974            match worst.track {
975                ErrorTrack::Bone(bone) => {
976                    let mut cursor = Some(bone);
977                    let mut is_origin = true;
978                    while let Some(index) = cursor {
979                        if let Some((range, position)) = insert_key_with_affected_range(
980                            &mut bone_key_indices[index],
981                            worst.frame,
982                        ) {
983                            inserted = true;
984                            added_this_pass += 1;
985                            next_dirty.mark_bone(index, range);
986                            if is_origin {
987                                work_stats.normal_key_additions += 1;
988                                bone_key_provenance[index]
989                                    .insert(position, ReductionKeyProvenance::validation_failure());
990                            } else {
991                                work_stats.ancestor_key_additions += 1;
992                                ancestor_insertions.push((index, worst.frame));
993                                bone_key_provenance[index].insert(
994                                    position,
995                                    ReductionKeyProvenance::ancestor_propagation(),
996                                );
997                            }
998                        }
999                        let parent = snapshot.parent_indices[index];
1000                        cursor = (parent >= 0).then_some(parent as usize);
1001                        is_origin = false;
1002                    }
1003                }
1004                ErrorTrack::Morph(morph) => {
1005                    if let Some((range, position)) =
1006                        insert_key_with_affected_range(&mut morph_key_indices[morph], worst.frame)
1007                    {
1008                        inserted = true;
1009                        added_this_pass += 1;
1010                        next_dirty.mark_morph(morph, range);
1011                        work_stats.normal_key_additions += 1;
1012                        morph_key_provenance[morph]
1013                            .insert(position, ReductionKeyProvenance::validation_failure());
1014                    }
1015                }
1016            }
1017        }
1018        work_stats.added_keys_per_pass.push(added_this_pass);
1019        if !inserted {
1020            return Err(PoseReductionError::ToleranceUnattainable {
1021                frame: first_failure_frame,
1022            });
1023        }
1024        dirty = next_dirty;
1025    }
1026}
1027
1028fn endpoint_indices(frame_count: usize) -> Vec<usize> {
1029    if frame_count == 1 {
1030        vec![0]
1031    } else {
1032        vec![0, frame_count - 1]
1033    }
1034}
1035
1036fn initial_key_provenance(indices: &[usize], frame_count: usize) -> Vec<ReductionKeyProvenance> {
1037    indices
1038        .iter()
1039        .map(|&frame| {
1040            if frame == 0 || frame + 1 == frame_count {
1041                ReductionKeyProvenance::endpoint()
1042            } else {
1043                ReductionKeyProvenance::local_prefit()
1044            }
1045        })
1046        .collect()
1047}
1048
1049fn decompose_dense_pose(
1050    input: DensePoseSequenceView<'_>,
1051    snapshot: &SkeletonSnapshot,
1052) -> Result<(Vec<Vec3A>, Vec<Quat>), PoseReductionError> {
1053    let mut translations = vec![Vec3A::ZERO; input.frame_count * input.bone_count];
1054    let mut rotations = vec![Quat::IDENTITY; input.frame_count * input.bone_count];
1055    for frame in 0..input.frame_count {
1056        for &bone in snapshot.evaluation_order.iter() {
1057            let world = input.world_matrix(frame, bone);
1058            validate_finite_matrix(world, frame, bone)?;
1059            let local = if snapshot.parent_indices[bone] < 0 {
1060                world
1061            } else {
1062                let parent = snapshot.parent_indices[bone] as usize;
1063                let parent_world = input.world_matrix(frame, parent);
1064                validate_finite_matrix(parent_world, frame, parent)?;
1065                let determinant = Mat3::from_mat4(parent_world).determinant();
1066                if !determinant.is_finite() || determinant.abs() <= f32::EPSILON {
1067                    return Err(PoseReductionError::SingularParent { frame, bone });
1068                }
1069                parent_world.inverse() * world
1070            };
1071            let (translation, rotation) = decompose_rigid(local, frame, bone)?;
1072            let index = frame * input.bone_count + bone;
1073            translations[index] = translation;
1074            rotations[index] = if frame > 0 {
1075                let previous = rotations[index - input.bone_count];
1076                if previous.dot(rotation) < 0.0 {
1077                    -rotation
1078                } else {
1079                    rotation
1080                }
1081            } else {
1082                rotation
1083            };
1084        }
1085    }
1086    Ok((translations, rotations))
1087}
1088
1089fn cache_dense_world_components(
1090    input: DensePoseSequenceView<'_>,
1091) -> Result<(Vec<Vec3A>, Vec<Quat>), PoseReductionError> {
1092    let count = input.frame_count * input.bone_count;
1093    let mut positions = Vec::with_capacity(count);
1094    let mut rotations = Vec::with_capacity(count);
1095    for frame in 0..input.frame_count {
1096        for bone in 0..input.bone_count {
1097            let world = input.world_matrix(frame, bone);
1098            positions.push(Vec3A::from(world.w_axis.truncate()));
1099            rotations.push(decompose_rigid(world, frame, bone)?.1);
1100        }
1101    }
1102    Ok((positions, rotations))
1103}
1104
1105fn decompose_rigid(
1106    matrix: Mat4,
1107    frame: usize,
1108    bone: usize,
1109) -> Result<(Vec3A, Quat), PoseReductionError> {
1110    validate_finite_matrix(matrix, frame, bone)?;
1111    let x = matrix.x_axis.truncate();
1112    let y = matrix.y_axis.truncate();
1113    let z = matrix.z_axis.truncate();
1114    let unit = |v: Vec3| (v.length_squared() - 1.0).abs() <= AFFINE_EPSILON;
1115    if !unit(x)
1116        || !unit(y)
1117        || !unit(z)
1118        || x.dot(y).abs() > AFFINE_EPSILON
1119        || x.dot(z).abs() > AFFINE_EPSILON
1120        || y.dot(z).abs() > AFFINE_EPSILON
1121        || Mat3::from_cols(x, y, z).determinant() < 0.0
1122        || matrix.x_axis.w.abs() > AFFINE_EPSILON
1123        || matrix.y_axis.w.abs() > AFFINE_EPSILON
1124        || matrix.z_axis.w.abs() > AFFINE_EPSILON
1125        || (matrix.w_axis.w - 1.0).abs() > AFFINE_EPSILON
1126    {
1127        return Err(PoseReductionError::ScaleOrShear { frame, bone });
1128    }
1129    let rotation = normalize_quat(Quat::from_mat3(&Mat3::from_cols(x, y, z)));
1130    Ok((Vec3A::from(matrix.w_axis.truncate()), rotation))
1131}
1132
1133fn validate_finite_matrix(
1134    matrix: Mat4,
1135    frame: usize,
1136    bone: usize,
1137) -> Result<(), PoseReductionError> {
1138    if matrix
1139        .to_cols_array()
1140        .iter()
1141        .any(|value| !value.is_finite())
1142    {
1143        Err(PoseReductionError::NonFiniteMatrix { frame, bone })
1144    } else {
1145        Ok(())
1146    }
1147}
1148
1149fn split_bone_track(
1150    keys: &mut Vec<usize>,
1151    input: DensePoseSequenceView<'_>,
1152    bone: usize,
1153    translations: &[Vec3A],
1154    rotations: &[Quat],
1155    tolerances: ReductionTolerances,
1156) {
1157    if input.frame_count <= 2 {
1158        return;
1159    }
1160    let bone_count = translations.len() / input.frame_count;
1161    let mut stack = vec![(0usize, input.frame_count - 1)];
1162    while let Some((start, end)) = stack.pop() {
1163        if end <= start + 1 {
1164            continue;
1165        }
1166        let start_index = start * bone_count + bone;
1167        let end_index = end * bone_count + bone;
1168        let mut worst: Option<(f32, usize)> = None;
1169        for frame in start + 1..end {
1170            let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1171            let index = frame * bone_count + bone;
1172            let position_error = translations[index]
1173                .distance(translations[start_index].lerp(translations[end_index], amount));
1174            let rotation_error = quat_angle(
1175                rotations[index],
1176                rotations[start_index].slerp(rotations[end_index], amount),
1177            );
1178            let normalized = normalized_error(position_error, tolerances.local_position).max(
1179                normalized_error(rotation_error, tolerances.local_rotation_radians),
1180            );
1181            if is_worse(worst, normalized, frame) {
1182                worst = Some((normalized, frame));
1183            }
1184        }
1185        if let Some((normalized, frame)) = worst.filter(|value| value.0 > 1.0) {
1186            let _ = normalized;
1187            insert_key(keys, frame);
1188            stack.push((frame, end));
1189            stack.push((start, frame));
1190        }
1191    }
1192}
1193
1194#[derive(Default)]
1195struct LocalPrefitStats {
1196    segment_fits: usize,
1197    key_additions: usize,
1198    samples: usize,
1199}
1200
1201fn split_dcc_bone_track(
1202    keys: &mut Vec<usize>,
1203    input: DensePoseSequenceView<'_>,
1204    bone: usize,
1205    translations: &[Vec3A],
1206    rotations: &[Quat],
1207    euler_xyz: &[Vec3A],
1208    tolerances: ReductionTolerances,
1209) -> LocalPrefitStats {
1210    if input.frame_count <= 2 {
1211        return LocalPrefitStats::default();
1212    }
1213    let bone_count = input.bone_count;
1214    let mut stats = LocalPrefitStats::default();
1215    let mut stack = vec![(0usize, input.frame_count - 1)];
1216    while let Some((start, end)) = stack.pop() {
1217        if end <= start + 1 {
1218            continue;
1219        }
1220        let segment = fit_dcc_bone_segment(input, bone, start, end, translations, euler_xyz);
1221        stats.segment_fits += 1;
1222        let start_index = start * bone_count + bone;
1223        let end_index = end * bone_count + bone;
1224        let duration = input.sample_frame(end) - input.sample_frame(start);
1225        let mut worst: Option<(f32, usize)> = None;
1226        for frame in start + 1..end {
1227            stats.samples += 1;
1228            let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1229            let translation = sample_dcc_vec3(
1230                translations[start_index],
1231                translations[end_index],
1232                segment.translation_out_tangent,
1233                segment.translation_in_tangent,
1234                duration,
1235                amount,
1236            );
1237            let euler = sample_dcc_vec3(
1238                segment.rotation_start_euler_xyz,
1239                segment.rotation_end_euler_xyz,
1240                segment.rotation_out_tangent,
1241                segment.rotation_in_tangent,
1242                duration,
1243                amount,
1244            );
1245            let rotation =
1246                normalize_quat(Quat::from_euler(EulerRot::XYZ, euler.x, euler.y, euler.z));
1247            let index = frame * bone_count + bone;
1248            let normalized = normalized_error(
1249                translations[index].distance(translation),
1250                tolerances.local_position,
1251            )
1252            .max(normalized_error(
1253                quat_angle(rotations[index], rotation),
1254                tolerances.local_rotation_radians,
1255            ));
1256            if is_worse(worst, normalized, frame) {
1257                worst = Some((normalized, frame));
1258            }
1259        }
1260        if let Some((_, frame)) = worst.filter(|value| value.0 > 1.0) {
1261            stats.key_additions += usize::from(insert_key(keys, frame));
1262            stack.push((frame, end));
1263            stack.push((start, frame));
1264        }
1265    }
1266    stats
1267}
1268
1269fn split_dcc_morph_track(
1270    keys: &mut Vec<usize>,
1271    input: DensePoseSequenceView<'_>,
1272    morph: usize,
1273    tolerance: f32,
1274) -> LocalPrefitStats {
1275    if input.frame_count <= 2 {
1276        return LocalPrefitStats::default();
1277    }
1278    let mut stats = LocalPrefitStats::default();
1279    let mut stack = vec![(0usize, input.frame_count - 1)];
1280    while let Some((start, end)) = stack.pop() {
1281        if end <= start + 1 {
1282            continue;
1283        }
1284        let segment = fit_dcc_scalar_segment(
1285            start,
1286            end,
1287            |sample| input.morph_weight(sample, morph),
1288            |sample| input.sample_frame(sample),
1289        );
1290        stats.segment_fits += 1;
1291        let duration = input.sample_frame(end) - input.sample_frame(start);
1292        let mut worst: Option<(f32, usize)> = None;
1293        for frame in start + 1..end {
1294            stats.samples += 1;
1295            let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1296            let expected = sample_hermite(
1297                input.morph_weight(start, morph),
1298                input.morph_weight(end, morph),
1299                segment.out_tangent,
1300                segment.in_tangent,
1301                duration,
1302                amount,
1303            );
1304            let normalized = normalized_error(
1305                (input.morph_weight(frame, morph) - expected).abs(),
1306                tolerance,
1307            );
1308            if is_worse(worst, normalized, frame) {
1309                worst = Some((normalized, frame));
1310            }
1311        }
1312        if let Some((_, frame)) = worst.filter(|value| value.0 > 1.0) {
1313            stats.key_additions += usize::from(insert_key(keys, frame));
1314            stack.push((frame, end));
1315            stack.push((start, frame));
1316        }
1317    }
1318    stats
1319}
1320
1321fn split_morph_track(
1322    keys: &mut Vec<usize>,
1323    input: DensePoseSequenceView<'_>,
1324    morph: usize,
1325    tolerance: f32,
1326) {
1327    if input.frame_count <= 2 {
1328        return;
1329    }
1330    let mut stack = vec![(0usize, input.frame_count - 1)];
1331    while let Some((start, end)) = stack.pop() {
1332        if end <= start + 1 {
1333            continue;
1334        }
1335        let mut worst: Option<(f32, usize)> = None;
1336        for frame in start + 1..end {
1337            let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1338            let expected = input.morph_weight(start, morph)
1339                + (input.morph_weight(end, morph) - input.morph_weight(start, morph)) * amount;
1340            let normalized = normalized_error(
1341                (input.morph_weight(frame, morph) - expected).abs(),
1342                tolerance,
1343            );
1344            if is_worse(worst, normalized, frame) {
1345                worst = Some((normalized, frame));
1346            }
1347        }
1348        if let Some((_, frame)) = worst.filter(|value| value.0 > 1.0) {
1349            insert_key(keys, frame);
1350            stack.push((frame, end));
1351            stack.push((start, frame));
1352        }
1353    }
1354}
1355
1356fn insert_key(keys: &mut Vec<usize>, frame: usize) -> bool {
1357    if let Err(position) = keys.binary_search(&frame) {
1358        keys.insert(position, frame);
1359        true
1360    } else {
1361        false
1362    }
1363}
1364
1365fn insert_key_with_affected_range(
1366    keys: &mut Vec<usize>,
1367    frame: usize,
1368) -> Option<(std::ops::RangeInclusive<usize>, usize)> {
1369    let position = keys.binary_search(&frame).err()?;
1370    let start = keys[position.saturating_sub(1)];
1371    let end = keys[position.min(keys.len() - 1)];
1372    keys.insert(position, frame);
1373    Some((start..=end, position))
1374}
1375
1376#[derive(Clone, Copy)]
1377struct DenseLocalPose<'a> {
1378    translations: &'a [Vec3A],
1379    rotations: &'a [Quat],
1380    euler_xyz: &'a [Vec3A],
1381}
1382
1383struct ReductionInstrumentation<'a> {
1384    work_stats: &'a mut ReductionWorkStats,
1385    timings: &'a mut ReductionTimings,
1386}
1387
1388#[allow(clippy::too_many_arguments)]
1389fn build_sequence(
1390    snapshot: &SkeletonSnapshot,
1391    target: ReductionTarget,
1392    input: DensePoseSequenceView<'_>,
1393    local_pose: DenseLocalPose<'_>,
1394    bone_key_indices: &[Vec<usize>],
1395    morph_key_indices: &[Vec<usize>],
1396    instrumentation: ReductionInstrumentation<'_>,
1397) -> ReducedPoseSequence {
1398    let ReductionInstrumentation {
1399        work_stats,
1400        timings,
1401    } = instrumentation;
1402    if target == ReductionTarget::DccCubic {
1403        work_stats.dcc_bone_segment_fits += bone_key_indices
1404            .iter()
1405            .map(|indices| indices.len().saturating_sub(1))
1406            .sum::<usize>();
1407        work_stats.dcc_morph_segment_fits += morph_key_indices
1408            .iter()
1409            .map(|indices| indices.len().saturating_sub(1))
1410            .sum::<usize>();
1411    }
1412    work_stats.candidate_bone_track_rebuilds += bone_key_indices.len();
1413    work_stats.candidate_morph_track_rebuilds += morph_key_indices.len();
1414    let dcc_bone_started = (target == ReductionTarget::DccCubic).then(reduction_timer_start);
1415    let bone_tracks = bone_key_indices
1416        .iter()
1417        .enumerate()
1418        .map(|(bone, indices)| build_bone_track(target, input, local_pose, bone, indices))
1419        .collect::<Vec<_>>()
1420        .into_boxed_slice();
1421    if let Some(started) = dcc_bone_started {
1422        timings.dcc_fit += reduction_timer_elapsed(started);
1423    }
1424    let dcc_morph_started = (target == ReductionTarget::DccCubic).then(reduction_timer_start);
1425    let morph_tracks = morph_key_indices
1426        .iter()
1427        .enumerate()
1428        .map(|(morph, indices)| build_morph_track(target, input, morph, indices))
1429        .collect::<Vec<_>>()
1430        .into_boxed_slice();
1431    if let Some(started) = dcc_morph_started {
1432        timings.dcc_fit += reduction_timer_elapsed(started);
1433    }
1434    ReducedPoseSequence {
1435        snapshot: snapshot.clone(),
1436        target,
1437        start_frame: input.start_frame,
1438        frame_step: input.frame_step,
1439        frame_count: input.frame_count,
1440        sample_frames: (0..input.frame_count)
1441            .map(|sample| input.sample_frame(sample))
1442            .collect::<Vec<_>>()
1443            .into_boxed_slice(),
1444        bone_tracks,
1445        morph_tracks,
1446        report: PoseReductionReport::default(),
1447        work_stats: ReductionWorkStats::default(),
1448        timings: ReductionTimings::default(),
1449    }
1450}
1451
1452fn build_bone_track(
1453    target: ReductionTarget,
1454    input: DensePoseSequenceView<'_>,
1455    local_pose: DenseLocalPose<'_>,
1456    bone: usize,
1457    indices: &[usize],
1458) -> ReducedBoneTrack {
1459    ReducedBoneTrack {
1460        keys: indices
1461            .iter()
1462            .enumerate()
1463            .map(|(key_position, &frame)| {
1464                let index = frame * input.bone_count + bone;
1465                ReducedBoneKey {
1466                    sample_index: frame,
1467                    translation: local_pose.translations[index],
1468                    rotation: local_pose.rotations[index],
1469                    vmd_interpolation: if target == ReductionTarget::VmdBezier && key_position > 0 {
1470                        fit_vmd_bone_interpolation(
1471                            input,
1472                            bone,
1473                            indices[key_position - 1],
1474                            frame,
1475                            local_pose.translations,
1476                            local_pose.rotations,
1477                        )
1478                    } else {
1479                        VmdBoneInterpolation::LINEAR
1480                    },
1481                    dcc_segment: if target == ReductionTarget::DccCubic && key_position > 0 {
1482                        fit_dcc_bone_segment(
1483                            input,
1484                            bone,
1485                            indices[key_position - 1],
1486                            frame,
1487                            local_pose.translations,
1488                            local_pose.euler_xyz,
1489                        )
1490                    } else {
1491                        DccCubicSegment::default()
1492                    },
1493                }
1494            })
1495            .collect::<Vec<_>>()
1496            .into_boxed_slice(),
1497    }
1498}
1499
1500fn build_morph_track(
1501    target: ReductionTarget,
1502    input: DensePoseSequenceView<'_>,
1503    morph: usize,
1504    indices: &[usize],
1505) -> ReducedMorphTrack {
1506    ReducedMorphTrack {
1507        keys: indices
1508            .iter()
1509            .enumerate()
1510            .map(|(key_position, &frame)| ReducedMorphKey {
1511                sample_index: frame,
1512                weight: input.morph_weight(frame, morph),
1513                dcc_segment: if target == ReductionTarget::DccCubic && key_position > 0 {
1514                    fit_dcc_scalar_segment(
1515                        indices[key_position - 1],
1516                        frame,
1517                        |sample| input.morph_weight(sample, morph),
1518                        |sample| input.sample_frame(sample),
1519                    )
1520                } else {
1521                    DccScalarSegment::default()
1522                },
1523            })
1524            .collect::<Vec<_>>()
1525            .into_boxed_slice(),
1526    }
1527}
1528
1529#[allow(clippy::too_many_arguments)]
1530fn rebuild_dirty_tracks(
1531    sequence: &mut ReducedPoseSequence,
1532    target: ReductionTarget,
1533    input: DensePoseSequenceView<'_>,
1534    local_pose: DenseLocalPose<'_>,
1535    bone_key_indices: &[Vec<usize>],
1536    morph_key_indices: &[Vec<usize>],
1537    dirty: &DirtyRanges,
1538    instrumentation: ReductionInstrumentation<'_>,
1539) {
1540    let ReductionInstrumentation {
1541        work_stats,
1542        timings,
1543    } = instrumentation;
1544    let dcc_started = (target == ReductionTarget::DccCubic).then(reduction_timer_start);
1545    for (bone, range) in dirty.bone_local.iter().enumerate() {
1546        if range.is_empty() {
1547            continue;
1548        }
1549        let indices = &bone_key_indices[bone];
1550        work_stats.candidate_bone_track_rebuilds += 1;
1551        if target == ReductionTarget::DccCubic {
1552            work_stats.dcc_bone_segment_fits += indices.len().saturating_sub(1);
1553        }
1554        sequence.bone_tracks[bone] = build_bone_track(target, input, local_pose, bone, indices);
1555    }
1556    for (morph, range) in dirty.morph.iter().enumerate() {
1557        if range.is_empty() {
1558            continue;
1559        }
1560        let indices = &morph_key_indices[morph];
1561        work_stats.candidate_morph_track_rebuilds += 1;
1562        if target == ReductionTarget::DccCubic {
1563            work_stats.dcc_morph_segment_fits += indices.len().saturating_sub(1);
1564        }
1565        sequence.morph_tracks[morph] = build_morph_track(target, input, morph, indices);
1566    }
1567    if let Some(started) = dcc_started {
1568        timings.dcc_fit += reduction_timer_elapsed(started);
1569    }
1570}
1571
1572/// Remove ancestor-propagated keys after the global refinement pass has
1573/// converged. Each candidate is tested against only the local interval it
1574/// spans and the candidate bone's descendant subtree; the cache is updated
1575/// only when the removal is accepted.
1576#[allow(clippy::too_many_arguments)]
1577fn prune_ancestor_keys(
1578    sequence: &mut ReducedPoseSequence,
1579    input: DensePoseSequenceView<'_>,
1580    local_pose: DenseLocalPose<'_>,
1581    dense: DenseValidationPose<'_>,
1582    tolerances: ReductionTolerances,
1583    bone_key_indices: &mut [Vec<usize>],
1584    bone_key_provenance: &mut [Vec<ReductionKeyProvenance>],
1585    ancestor_insertions: &[(usize, usize)],
1586    cache: &mut ValidationCache,
1587    work_stats: &mut ReductionWorkStats,
1588) -> Result<usize, PoseReductionError> {
1589    let subtrees = build_subtree_lists(&sequence.snapshot);
1590    let mut pruned = 0;
1591    let mut subtree_worlds = vec![Mat4::IDENTITY; sequence.snapshot.bone_count()];
1592
1593    for &(bone, frame) in ancestor_insertions.iter().rev() {
1594        let Some(position) = bone_key_indices[bone].binary_search(&frame).ok() else {
1595            continue;
1596        };
1597        let provenance = bone_key_provenance[bone][position];
1598        // A future provenance combination must not make this pass remove an
1599        // endpoint or a local-prefit/validation key by accident.
1600        if !provenance.is_ancestor_propagation()
1601            || provenance.is_endpoint()
1602            || provenance.is_local_prefit()
1603            || provenance.is_validation_failure()
1604            || position == 0
1605            || position + 1 >= bone_key_indices[bone].len()
1606        {
1607            continue;
1608        }
1609
1610        work_stats.ancestor_prune_attempts += 1;
1611        let start = bone_key_indices[bone][position - 1];
1612        let end = bone_key_indices[bone][position + 1];
1613        let mut replacement_indices = bone_key_indices[bone].clone();
1614        replacement_indices.remove(position);
1615        let mut replacement_provenance = bone_key_provenance[bone].clone();
1616        replacement_provenance.remove(position);
1617        let replacement = build_bone_track_without_key(sequence, input, local_pose, bone, position);
1618        work_stats.candidate_bone_track_rebuilds += 1;
1619        if sequence.target == ReductionTarget::DccCubic {
1620            work_stats.dcc_bone_segment_fits += 1;
1621        }
1622
1623        let interval = start..=end;
1624        if !validate_ancestor_prune_candidate(
1625            sequence,
1626            input,
1627            dense,
1628            tolerances,
1629            bone,
1630            &subtrees[bone],
1631            interval.clone(),
1632            &replacement,
1633            cache,
1634            &mut subtree_worlds,
1635            work_stats,
1636        )? {
1637            continue;
1638        }
1639
1640        bone_key_indices[bone] = replacement_indices;
1641        bone_key_provenance[bone] = replacement_provenance;
1642        sequence.bone_tracks[bone] = replacement;
1643        apply_ancestor_prune_candidate(
1644            sequence,
1645            input,
1646            dense,
1647            bone,
1648            &subtrees[bone],
1649            interval,
1650            cache,
1651            &mut subtree_worlds,
1652            work_stats,
1653        )?;
1654        work_stats.ancestor_pruned_keys += 1;
1655        pruned += 1;
1656    }
1657    Ok(pruned)
1658}
1659
1660fn build_bone_track_without_key(
1661    sequence: &ReducedPoseSequence,
1662    input: DensePoseSequenceView<'_>,
1663    local_pose: DenseLocalPose<'_>,
1664    bone: usize,
1665    position: usize,
1666) -> ReducedBoneTrack {
1667    let mut keys = sequence.bone_tracks[bone].keys.to_vec();
1668    keys.remove(position);
1669    if position < keys.len() {
1670        let start = keys[position - 1].sample_index;
1671        let end = keys[position].sample_index;
1672        keys[position].vmd_interpolation = if sequence.target == ReductionTarget::VmdBezier {
1673            fit_vmd_bone_interpolation(
1674                input,
1675                bone,
1676                start,
1677                end,
1678                local_pose.translations,
1679                local_pose.rotations,
1680            )
1681        } else {
1682            VmdBoneInterpolation::LINEAR
1683        };
1684        keys[position].dcc_segment = if sequence.target == ReductionTarget::DccCubic {
1685            fit_dcc_bone_segment(
1686                input,
1687                bone,
1688                start,
1689                end,
1690                local_pose.translations,
1691                local_pose.euler_xyz,
1692            )
1693        } else {
1694            DccCubicSegment::default()
1695        };
1696    }
1697    ReducedBoneTrack {
1698        keys: keys.into_boxed_slice(),
1699    }
1700}
1701
1702fn build_subtree_lists(snapshot: &SkeletonSnapshot) -> Vec<Vec<usize>> {
1703    (0..snapshot.bone_count())
1704        .map(|root| {
1705            snapshot
1706                .evaluation_order
1707                .iter()
1708                .copied()
1709                .filter(|&bone| is_descendant_or_self(snapshot, bone, root))
1710                .collect()
1711        })
1712        .collect()
1713}
1714
1715fn is_descendant_or_self(snapshot: &SkeletonSnapshot, mut bone: usize, root: usize) -> bool {
1716    loop {
1717        if bone == root {
1718            return true;
1719        }
1720        let parent = snapshot.parent_indices[bone];
1721        if parent < 0 {
1722            return false;
1723        }
1724        bone = parent as usize;
1725    }
1726}
1727
1728#[allow(clippy::too_many_arguments)]
1729fn validate_ancestor_prune_candidate(
1730    sequence: &ReducedPoseSequence,
1731    input: DensePoseSequenceView<'_>,
1732    dense: DenseValidationPose<'_>,
1733    tolerances: ReductionTolerances,
1734    bone: usize,
1735    subtree: &[usize],
1736    interval: std::ops::RangeInclusive<usize>,
1737    replacement: &ReducedBoneTrack,
1738    cache: &ValidationCache,
1739    subtree_worlds: &mut [Mat4],
1740    work_stats: &mut ReductionWorkStats,
1741) -> Result<bool, PoseReductionError> {
1742    for frame in interval {
1743        let (translation, rotation) = sample_bone_track(
1744            replacement,
1745            &sequence.sample_frames,
1746            input.sample_frame(frame),
1747            sequence.target,
1748        );
1749        work_stats.bone_samples += 1;
1750        let index = frame * input.bone_count + bone;
1751        if dense.translations[index].distance(translation) > tolerances.local_position
1752            || quat_angle(dense.rotations[index], rotation) > tolerances.local_rotation_radians
1753        {
1754            return Ok(false);
1755        }
1756
1757        for &current in subtree {
1758            let current_index = frame * input.bone_count + current;
1759            let (local_translation, local_rotation) = if current == bone {
1760                (translation, rotation)
1761            } else {
1762                (
1763                    cache.local_translations[current_index],
1764                    cache.local_rotations[current_index],
1765                )
1766            };
1767            let local = Mat4::from_rotation_translation(local_rotation, local_translation.into());
1768            let parent = sequence.snapshot.parent_indices[current];
1769            let world = if parent < 0 {
1770                local
1771            } else if is_descendant_or_self(&sequence.snapshot, parent as usize, bone) {
1772                subtree_worlds[parent as usize] * local
1773            } else {
1774                cache.world_matrices[frame * input.bone_count + parent as usize] * local
1775            };
1776            subtree_worlds[current] = world;
1777            let world_rotation = decompose_rigid(world, frame, current)?.1;
1778            work_stats.world_bone_recomputes += 1;
1779            work_stats.world_rotation_decompositions += 1;
1780            if dense.world_positions[current_index].distance(Vec3A::from(world.w_axis.truncate()))
1781                > tolerances.world_position
1782                || quat_angle(dense.world_rotations[current_index], world_rotation)
1783                    > tolerances.world_rotation_radians
1784            {
1785                return Ok(false);
1786            }
1787        }
1788        work_stats.world_rebuilds += 1;
1789    }
1790    Ok(true)
1791}
1792
1793#[allow(clippy::too_many_arguments)]
1794fn apply_ancestor_prune_candidate(
1795    sequence: &ReducedPoseSequence,
1796    input: DensePoseSequenceView<'_>,
1797    dense: DenseValidationPose<'_>,
1798    bone: usize,
1799    subtree: &[usize],
1800    interval: std::ops::RangeInclusive<usize>,
1801    cache: &mut ValidationCache,
1802    subtree_worlds: &mut [Mat4],
1803    work_stats: &mut ReductionWorkStats,
1804) -> Result<(), PoseReductionError> {
1805    for frame in interval {
1806        let index = frame * input.bone_count + bone;
1807        let (translation, rotation) = sample_bone_track(
1808            &sequence.bone_tracks[bone],
1809            &sequence.sample_frames,
1810            input.sample_frame(frame),
1811            sequence.target,
1812        );
1813        work_stats.bone_samples += 1;
1814        cache.local_translations[index] = translation;
1815        cache.local_rotations[index] = rotation;
1816        cache.bone_errors[index].local_position = dense.translations[index].distance(translation);
1817        cache.bone_errors[index].local_rotation = quat_angle(dense.rotations[index], rotation);
1818
1819        for &current in subtree {
1820            let current_index = frame * input.bone_count + current;
1821            let (local_translation, local_rotation) = if current == bone {
1822                (translation, rotation)
1823            } else {
1824                (
1825                    cache.local_translations[current_index],
1826                    cache.local_rotations[current_index],
1827                )
1828            };
1829            let local = Mat4::from_rotation_translation(local_rotation, local_translation.into());
1830            let parent = sequence.snapshot.parent_indices[current];
1831            let world = if parent < 0 {
1832                local
1833            } else if is_descendant_or_self(&sequence.snapshot, parent as usize, bone) {
1834                subtree_worlds[parent as usize] * local
1835            } else {
1836                cache.world_matrices[frame * input.bone_count + parent as usize] * local
1837            };
1838            subtree_worlds[current] = world;
1839            let world_rotation = decompose_rigid(world, frame, current)?.1;
1840            work_stats.world_bone_recomputes += 1;
1841            work_stats.world_rotation_decompositions += 1;
1842            cache.world_matrices[current_index] = world;
1843            cache.world_rotations[current_index] = world_rotation;
1844            cache.bone_errors[current_index].world_position =
1845                dense.world_positions[current_index].distance(Vec3A::from(world.w_axis.truncate()));
1846            cache.bone_errors[current_index].world_rotation =
1847                quat_angle(dense.world_rotations[current_index], world_rotation);
1848        }
1849        work_stats.world_rebuilds += 1;
1850    }
1851    Ok(())
1852}
1853
1854#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1855enum ErrorTrack {
1856    Bone(usize),
1857    Morph(usize),
1858}
1859
1860#[derive(Clone, Copy)]
1861struct WorstError {
1862    normalized_error: f32,
1863    frame: usize,
1864    track: ErrorTrack,
1865}
1866
1867#[derive(Clone, Copy)]
1868struct DenseValidationPose<'a> {
1869    translations: &'a [Vec3A],
1870    rotations: &'a [Quat],
1871    world_positions: &'a [Vec3A],
1872    world_rotations: &'a [Quat],
1873}
1874
1875#[derive(Clone, Copy, Default)]
1876struct BoneErrorCell {
1877    local_position: f32,
1878    local_rotation: f32,
1879    world_position: f32,
1880    world_rotation: f32,
1881}
1882
1883struct ValidationCache {
1884    local_translations: Vec<Vec3A>,
1885    local_rotations: Vec<Quat>,
1886    world_matrices: Vec<Mat4>,
1887    world_rotations: Vec<Quat>,
1888    morph_weights: Vec<f32>,
1889    bone_errors: Vec<BoneErrorCell>,
1890    morph_errors: Vec<f32>,
1891}
1892
1893#[cfg(not(target_family = "wasm"))]
1894struct ValidationCacheChunk {
1895    start_frame: usize,
1896    local_translations: Vec<Vec3A>,
1897    local_rotations: Vec<Quat>,
1898    world_matrices: Vec<Mat4>,
1899    world_rotations: Vec<Quat>,
1900    morph_weights: Vec<f32>,
1901    bone_errors: Vec<BoneErrorCell>,
1902    morph_errors: Vec<f32>,
1903}
1904
1905impl ValidationCache {
1906    fn new(frame_count: usize, bone_count: usize, morph_count: usize) -> Self {
1907        Self {
1908            local_translations: vec![Vec3A::ZERO; frame_count * bone_count],
1909            local_rotations: vec![Quat::IDENTITY; frame_count * bone_count],
1910            world_matrices: vec![Mat4::IDENTITY; frame_count * bone_count],
1911            world_rotations: vec![Quat::IDENTITY; frame_count * bone_count],
1912            morph_weights: vec![0.0; frame_count * morph_count],
1913            bone_errors: vec![BoneErrorCell::default(); frame_count * bone_count],
1914            morph_errors: vec![0.0; frame_count * morph_count],
1915        }
1916    }
1917}
1918
1919#[cfg(not(target_family = "wasm"))]
1920#[allow(clippy::too_many_arguments)]
1921fn refresh_full_validation_cache_parallel(
1922    sequence: &ReducedPoseSequence,
1923    input: DensePoseSequenceView<'_>,
1924    dense: DenseValidationPose<'_>,
1925    cache: &mut ValidationCache,
1926    work_stats: &mut ReductionWorkStats,
1927    worker_pool: &ReductionThreadPool,
1928    worker_count: usize,
1929) -> Result<(), PoseReductionError> {
1930    let chunk_size = input.frame_count.div_ceil(worker_count);
1931    let ranges = (0..worker_count)
1932        .map(|worker| {
1933            let start = worker * chunk_size;
1934            start..(start + chunk_size).min(input.frame_count)
1935        })
1936        .filter(|range| !range.is_empty())
1937        .collect::<Vec<_>>();
1938    let chunks = worker_pool.install(|| {
1939        ranges
1940            .par_iter()
1941            .map(|range| {
1942                let frame_count = range.end - range.start;
1943                let mut chunk = ValidationCacheChunk {
1944                    start_frame: range.start,
1945                    local_translations: Vec::with_capacity(frame_count * input.bone_count),
1946                    local_rotations: Vec::with_capacity(frame_count * input.bone_count),
1947                    world_matrices: Vec::with_capacity(frame_count * input.bone_count),
1948                    world_rotations: Vec::with_capacity(frame_count * input.bone_count),
1949                    morph_weights: Vec::with_capacity(frame_count * input.morph_count),
1950                    bone_errors: Vec::with_capacity(frame_count * input.bone_count),
1951                    morph_errors: Vec::with_capacity(frame_count * input.morph_count),
1952                };
1953                let mut scratch = ReducedPoseScratch::default();
1954                scratch.prepare(input.bone_count, input.morph_count);
1955                for frame in range.clone() {
1956                    sequence.sample_into(input.sample_frame(frame), &mut scratch)?;
1957                    chunk
1958                        .local_translations
1959                        .extend_from_slice(&scratch.local_translations);
1960                    chunk
1961                        .local_rotations
1962                        .extend_from_slice(&scratch.local_rotations);
1963                    chunk
1964                        .world_matrices
1965                        .extend_from_slice(&scratch.world_matrices);
1966                    for bone in 0..input.bone_count {
1967                        let index = frame * input.bone_count + bone;
1968                        let world_rotation =
1969                            decompose_rigid(scratch.world_matrices[bone], frame, bone)?.1;
1970                        chunk.world_rotations.push(world_rotation);
1971                        chunk.bone_errors.push(BoneErrorCell {
1972                            local_position: dense.translations[index]
1973                                .distance(scratch.local_translations[bone]),
1974                            local_rotation: quat_angle(
1975                                dense.rotations[index],
1976                                scratch.local_rotations[bone],
1977                            ),
1978                            world_position: dense.world_positions[index].distance(Vec3A::from(
1979                                scratch.world_matrices[bone].w_axis.truncate(),
1980                            )),
1981                            world_rotation: quat_angle(
1982                                dense.world_rotations[index],
1983                                world_rotation,
1984                            ),
1985                        });
1986                    }
1987                    chunk
1988                        .morph_weights
1989                        .extend_from_slice(&scratch.morph_weights);
1990                    for morph in 0..input.morph_count {
1991                        chunk.morph_errors.push(
1992                            (input.morph_weight(frame, morph) - scratch.morph_weights[morph]).abs(),
1993                        );
1994                    }
1995                }
1996                Ok(chunk)
1997            })
1998            .collect::<Result<Vec<_>, PoseReductionError>>()
1999    })?;
2000
2001    for chunk in chunks {
2002        let bone_start = chunk.start_frame * input.bone_count;
2003        let bone_end = bone_start + chunk.local_translations.len();
2004        cache.local_translations[bone_start..bone_end].copy_from_slice(&chunk.local_translations);
2005        cache.local_rotations[bone_start..bone_end].copy_from_slice(&chunk.local_rotations);
2006        cache.world_matrices[bone_start..bone_end].copy_from_slice(&chunk.world_matrices);
2007        cache.world_rotations[bone_start..bone_end].copy_from_slice(&chunk.world_rotations);
2008        cache.bone_errors[bone_start..bone_end].copy_from_slice(&chunk.bone_errors);
2009        let morph_start = chunk.start_frame * input.morph_count;
2010        let morph_end = morph_start + chunk.morph_weights.len();
2011        cache.morph_weights[morph_start..morph_end].copy_from_slice(&chunk.morph_weights);
2012        cache.morph_errors[morph_start..morph_end].copy_from_slice(&chunk.morph_errors);
2013    }
2014    work_stats.bone_samples += input.frame_count * input.bone_count;
2015    work_stats.morph_samples += input.frame_count * input.morph_count;
2016    work_stats.world_rebuilds += input.frame_count;
2017    work_stats.world_bone_recomputes += input.frame_count * input.bone_count;
2018    work_stats.world_rotation_decompositions += input.frame_count * input.bone_count;
2019    Ok(())
2020}
2021
2022#[allow(clippy::too_many_arguments)]
2023fn measure_error_cached(
2024    sequence: &ReducedPoseSequence,
2025    input: DensePoseSequenceView<'_>,
2026    dense: DenseValidationPose<'_>,
2027    tolerances: ReductionTolerances,
2028    work_stats: &mut ReductionWorkStats,
2029    dirty: &DirtyRanges,
2030    cache: &mut ValidationCache,
2031    worker_pool: Option<&ReductionThreadPool>,
2032    worker_count: usize,
2033) -> Result<(PoseReductionReport, Vec<Option<WorstError>>), PoseReductionError> {
2034    let full_dirty = dirty_ranges_are_full(
2035        dirty,
2036        input.frame_count,
2037        input.bone_count,
2038        input.morph_count,
2039    );
2040    #[cfg(not(target_family = "wasm"))]
2041    let refreshed_in_parallel = if full_dirty && worker_count > 1 {
2042        refresh_full_validation_cache_parallel(
2043            sequence,
2044            input,
2045            dense,
2046            cache,
2047            work_stats,
2048            worker_pool.expect("multi-worker reduction has a pool"),
2049            worker_count,
2050        )?;
2051        true
2052    } else {
2053        false
2054    };
2055    #[cfg(target_family = "wasm")]
2056    let refreshed_in_parallel = {
2057        let _ = (full_dirty, worker_pool, worker_count);
2058        false
2059    };
2060
2061    let mut world_dirty = dirty.bone_local.clone();
2062    for &bone in sequence.snapshot.evaluation_order.iter() {
2063        let parent = sequence.snapshot.parent_indices[bone];
2064        if parent >= 0 {
2065            for range in world_dirty[parent as usize].clone() {
2066                insert_dirty_range(&mut world_dirty[bone], range);
2067            }
2068        }
2069    }
2070
2071    if !refreshed_in_parallel {
2072        for (bone, ranges) in dirty.bone_local.iter().enumerate() {
2073            for range in ranges {
2074                for frame in range.clone() {
2075                    let index = frame * input.bone_count + bone;
2076                    let (translation, rotation) = sample_bone_track(
2077                        &sequence.bone_tracks[bone],
2078                        &sequence.sample_frames,
2079                        input.sample_frame(frame),
2080                        sequence.target,
2081                    );
2082                    cache.local_translations[index] = translation;
2083                    cache.local_rotations[index] = rotation;
2084                    let cell = &mut cache.bone_errors[index];
2085                    cell.local_position = dense.translations[index].distance(translation);
2086                    cell.local_rotation = quat_angle(dense.rotations[index], rotation);
2087                    work_stats.bone_samples += 1;
2088                }
2089            }
2090        }
2091        for (morph, ranges) in dirty.morph.iter().enumerate() {
2092            for range in ranges {
2093                for frame in range.clone() {
2094                    let index = frame * input.morph_count + morph;
2095                    let sampled = sample_morph_track(
2096                        &sequence.morph_tracks[morph],
2097                        &sequence.sample_frames,
2098                        input.sample_frame(frame),
2099                        sequence.target,
2100                    );
2101                    cache.morph_weights[index] = sampled;
2102                    cache.morph_errors[index] = (input.morph_weight(frame, morph) - sampled).abs();
2103                    work_stats.morph_samples += 1;
2104                }
2105            }
2106        }
2107
2108        let mut rebuilt_frames = vec![false; input.frame_count];
2109        for &bone in sequence.snapshot.evaluation_order.iter() {
2110            for range in &world_dirty[bone] {
2111                for frame in range.clone() {
2112                    rebuilt_frames[frame] = true;
2113                    let index = frame * input.bone_count + bone;
2114                    let local = Mat4::from_rotation_translation(
2115                        cache.local_rotations[index],
2116                        cache.local_translations[index].into(),
2117                    );
2118                    let parent = sequence.snapshot.parent_indices[bone];
2119                    cache.world_matrices[index] = if parent < 0 {
2120                        local
2121                    } else {
2122                        cache.world_matrices[frame * input.bone_count + parent as usize] * local
2123                    };
2124                    cache.world_rotations[index] =
2125                        decompose_rigid(cache.world_matrices[index], frame, bone)?.1;
2126                    let cell = &mut cache.bone_errors[index];
2127                    cell.world_position = dense.world_positions[index]
2128                        .distance(Vec3A::from(cache.world_matrices[index].w_axis.truncate()));
2129                    cell.world_rotation =
2130                        quat_angle(dense.world_rotations[index], cache.world_rotations[index]);
2131                    work_stats.world_bone_recomputes += 1;
2132                    work_stats.world_rotation_decompositions += 1;
2133                }
2134            }
2135        }
2136        work_stats.world_rebuilds += rebuilt_frames
2137            .into_iter()
2138            .filter(|rebuilt| *rebuilt)
2139            .count();
2140    }
2141
2142    let mut report = PoseReductionReport::default();
2143    let mut worst = vec![None; input.bone_count + input.morph_count];
2144    for frame in 0..input.frame_count {
2145        let (bone_worst, morph_worst) = worst.split_at_mut(input.bone_count);
2146        for (bone, worst) in bone_worst.iter_mut().enumerate() {
2147            let index = frame * input.bone_count + bone;
2148            let cell = cache.bone_errors[index];
2149            report.max_local_position_error =
2150                report.max_local_position_error.max(cell.local_position);
2151            report.max_local_rotation_error_radians = report
2152                .max_local_rotation_error_radians
2153                .max(cell.local_rotation);
2154            report.max_world_position_error =
2155                report.max_world_position_error.max(cell.world_position);
2156            report.max_world_rotation_error_radians = report
2157                .max_world_rotation_error_radians
2158                .max(cell.world_rotation);
2159            for normalized in [
2160                normalized_error(cell.local_position, tolerances.local_position),
2161                normalized_error(cell.local_rotation, tolerances.local_rotation_radians),
2162                normalized_error(cell.world_position, tolerances.world_position),
2163                normalized_error(cell.world_rotation, tolerances.world_rotation_radians),
2164            ] {
2165                update_worst(worst, normalized, frame, ErrorTrack::Bone(bone));
2166            }
2167        }
2168        for (morph, morph_worst) in morph_worst.iter_mut().enumerate() {
2169            let error = cache.morph_errors[frame * input.morph_count + morph];
2170            report.max_morph_weight_error = report.max_morph_weight_error.max(error);
2171            update_worst(
2172                morph_worst,
2173                normalized_error(error, tolerances.morph_weight),
2174                frame,
2175                ErrorTrack::Morph(morph),
2176            );
2177        }
2178    }
2179    Ok((report, worst))
2180}
2181
2182fn dirty_ranges_are_full(
2183    dirty: &DirtyRanges,
2184    frame_count: usize,
2185    bone_count: usize,
2186    morph_count: usize,
2187) -> bool {
2188    let is_full = |ranges: &[std::ops::RangeInclusive<usize>]| {
2189        ranges.len() == 1
2190            && *ranges[0].start() == 0
2191            && *ranges[0].end() == frame_count.saturating_sub(1)
2192    };
2193    dirty.bone_local.len() == bone_count
2194        && dirty.morph.len() == morph_count
2195        && dirty.bone_local.iter().all(|ranges| is_full(ranges))
2196        && dirty.morph.iter().all(|ranges| is_full(ranges))
2197}
2198
2199fn update_worst(worst: &mut Option<WorstError>, normalized: f32, frame: usize, track: ErrorTrack) {
2200    update_worst_candidate(
2201        worst,
2202        WorstError {
2203            normalized_error: normalized,
2204            frame,
2205            track,
2206        },
2207    );
2208}
2209
2210fn update_worst_candidate(worst: &mut Option<WorstError>, candidate: WorstError) {
2211    if worst.is_none_or(|current| compare_worst_errors(&candidate, &current) == Ordering::Less) {
2212        *worst = Some(candidate);
2213    }
2214}
2215
2216fn compare_worst_errors(a: &WorstError, b: &WorstError) -> Ordering {
2217    b.normalized_error
2218        .total_cmp(&a.normalized_error)
2219        .then_with(|| a.frame.cmp(&b.frame))
2220        .then_with(|| error_track_sort_key(a.track).cmp(&error_track_sort_key(b.track)))
2221}
2222
2223fn error_track_sort_key(track: ErrorTrack) -> (u8, usize) {
2224    match track {
2225        ErrorTrack::Bone(index) => (0, index),
2226        ErrorTrack::Morph(index) => (1, index),
2227    }
2228}
2229
2230fn resolve_reduction_worker_count(requested: usize, frame_count: usize) -> usize {
2231    #[cfg(target_family = "wasm")]
2232    {
2233        let _ = (requested, frame_count);
2234        1
2235    }
2236    #[cfg(not(target_family = "wasm"))]
2237    {
2238        let workers = if requested == 0 {
2239            std::thread::available_parallelism()
2240                .map(usize::from)
2241                .unwrap_or(1)
2242        } else {
2243            requested
2244        };
2245        workers.clamp(1, frame_count.max(1))
2246    }
2247}
2248
2249#[cfg(not(target_family = "wasm"))]
2250fn build_reduction_worker_pool(
2251    worker_count: usize,
2252) -> Result<Option<ReductionThreadPool>, PoseReductionError> {
2253    (worker_count > 1)
2254        .then(|| {
2255            ThreadPoolBuilder::new()
2256                .num_threads(worker_count)
2257                .build()
2258                .map_err(|_| PoseReductionError::WorkerPool)
2259        })
2260        .transpose()
2261}
2262
2263#[cfg(target_family = "wasm")]
2264fn build_reduction_worker_pool(
2265    _worker_count: usize,
2266) -> Result<Option<ReductionThreadPool>, PoseReductionError> {
2267    Ok(None)
2268}
2269
2270fn fit_vmd_bone_interpolation(
2271    input: DensePoseSequenceView<'_>,
2272    bone: usize,
2273    start: usize,
2274    end: usize,
2275    translations: &[Vec3A],
2276    rotations: &[Quat],
2277) -> VmdBoneInterpolation {
2278    let bone_count = input.bone_count;
2279    let start_index = start * bone_count + bone;
2280    let end_index = end * bone_count + bone;
2281    let start_translation = translations[start_index];
2282    let end_translation = translations[end_index];
2283    let translation = std::array::from_fn(|axis| {
2284        let start_value = start_translation.to_array()[axis];
2285        let end_value = end_translation.to_array()[axis];
2286        fit_quantized_bezier(
2287            start,
2288            end,
2289            |sample| {
2290                let value = translations[sample * bone_count + bone].to_array()[axis];
2291                normalized_channel_value(start_value, end_value, value)
2292            },
2293            |sample| input.sample_frame(sample),
2294        )
2295    });
2296    let start_rotation = rotations[start_index];
2297    let end_rotation = rotations[end_index];
2298    let total_angle = quat_angle(start_rotation, end_rotation);
2299    let rotation = fit_quantized_bezier(
2300        start,
2301        end,
2302        |sample| {
2303            if total_angle <= f32::EPSILON {
2304                0.0
2305            } else {
2306                (quat_angle(start_rotation, rotations[sample * bone_count + bone]) / total_angle)
2307                    .clamp(0.0, 1.0)
2308            }
2309        },
2310        |sample| input.sample_frame(sample),
2311    );
2312    VmdBoneInterpolation {
2313        translation,
2314        rotation,
2315    }
2316}
2317
2318fn fit_quantized_bezier(
2319    start: usize,
2320    end: usize,
2321    value_at: impl Fn(usize) -> f32,
2322    frame_at: impl Fn(usize) -> f32,
2323) -> QuantizedBezier {
2324    if end <= start + 1 {
2325        return QuantizedBezier::LINEAR;
2326    }
2327    let mut best = QuantizedBezier::LINEAR;
2328    let score = |curve: QuantizedBezier| -> f32 {
2329        (start + 1..end)
2330            .map(|sample| {
2331                let time = segment_amount(start, end, sample, &frame_at);
2332                (curve.evaluate(time) - value_at(sample)).abs()
2333            })
2334            .fold(0.0f32, f32::max)
2335    };
2336    let mut best_score = score(best);
2337    const COARSE: [u8; 9] = [0, 16, 32, 48, 64, 80, 96, 112, 127];
2338    for &x1 in &COARSE {
2339        for &x2 in &COARSE {
2340            if x1 > x2 {
2341                continue;
2342            }
2343            for &y1 in &COARSE {
2344                for &y2 in &COARSE {
2345                    let candidate = QuantizedBezier { x1, y1, x2, y2 };
2346                    let candidate_score = score(candidate);
2347                    if candidate_score.total_cmp(&best_score) == Ordering::Less {
2348                        best = candidate;
2349                        best_score = candidate_score;
2350                    }
2351                }
2352            }
2353        }
2354    }
2355    for step in [32i16, 16, 8, 4, 2, 1] {
2356        loop {
2357            let origin = [best.x1, best.y1, best.x2, best.y2];
2358            let mut next_best = best;
2359            let mut next_score = best_score;
2360            for d0 in [-step, 0, step] {
2361                for d1 in [-step, 0, step] {
2362                    for d2 in [-step, 0, step] {
2363                        for d3 in [-step, 0, step] {
2364                            let offsets = [d0, d1, d2, d3];
2365                            let mut values = [0u8; 4];
2366                            let mut valid = true;
2367                            for coordinate in 0..4 {
2368                                let value = origin[coordinate] as i16 + offsets[coordinate];
2369                                if !(0..=127).contains(&value) {
2370                                    valid = false;
2371                                    break;
2372                                }
2373                                values[coordinate] = value as u8;
2374                            }
2375                            if !valid || values[0] > values[2] {
2376                                continue;
2377                            }
2378                            let candidate = QuantizedBezier {
2379                                x1: values[0],
2380                                y1: values[1],
2381                                x2: values[2],
2382                                y2: values[3],
2383                            };
2384                            let candidate_score = score(candidate);
2385                            if candidate_score.total_cmp(&next_score) == Ordering::Less {
2386                                next_best = candidate;
2387                                next_score = candidate_score;
2388                            }
2389                        }
2390                    }
2391                }
2392            }
2393            if next_best == best {
2394                break;
2395            }
2396            best = next_best;
2397            best_score = next_score;
2398        }
2399    }
2400    best
2401}
2402
2403fn normalized_channel_value(start: f32, end: f32, value: f32) -> f32 {
2404    let range = end - start;
2405    if range.abs() <= f32::EPSILON {
2406        0.0
2407    } else {
2408        ((value - start) / range).clamp(0.0, 1.0)
2409    }
2410}
2411
2412fn unwrap_euler_xyz(rotations: &[Quat], frame_count: usize, bone_count: usize) -> Vec<Vec3A> {
2413    let mut result = vec![Vec3A::ZERO; rotations.len()];
2414    for bone in 0..bone_count {
2415        for frame in 0..frame_count {
2416            let index = frame * bone_count + bone;
2417            let (x, y, z) = rotations[index].to_euler(EulerRot::XYZ);
2418            let mut value = Vec3A::new(x, y, z);
2419            if frame > 0 {
2420                let previous = result[index - bone_count];
2421                value.x = unwrap_angle(previous.x, value.x);
2422                value.y = unwrap_angle(previous.y, value.y);
2423                value.z = unwrap_angle(previous.z, value.z);
2424            }
2425            result[index] = value;
2426        }
2427    }
2428    result
2429}
2430
2431fn unwrap_angle(previous: f32, value: f32) -> f32 {
2432    let turns = ((previous - value) / std::f32::consts::TAU).round();
2433    value + turns * std::f32::consts::TAU
2434}
2435
2436fn fit_dcc_bone_segment(
2437    input: DensePoseSequenceView<'_>,
2438    bone: usize,
2439    start: usize,
2440    end: usize,
2441    translations: &[Vec3A],
2442    euler_xyz: &[Vec3A],
2443) -> DccCubicSegment {
2444    let bone_count = input.bone_count;
2445    let translation = std::array::from_fn::<_, 3, _>(|axis| {
2446        fit_dcc_scalar_segment(
2447            start,
2448            end,
2449            |sample| translations[sample * bone_count + bone].to_array()[axis],
2450            |sample| input.sample_frame(sample),
2451        )
2452    });
2453    let rotation = std::array::from_fn::<_, 3, _>(|axis| {
2454        fit_dcc_scalar_segment(
2455            start,
2456            end,
2457            |sample| euler_xyz[sample * bone_count + bone].to_array()[axis],
2458            |sample| input.sample_frame(sample),
2459        )
2460    });
2461    DccCubicSegment {
2462        translation_out_tangent: Vec3A::new(
2463            translation[0].out_tangent,
2464            translation[1].out_tangent,
2465            translation[2].out_tangent,
2466        ),
2467        translation_in_tangent: Vec3A::new(
2468            translation[0].in_tangent,
2469            translation[1].in_tangent,
2470            translation[2].in_tangent,
2471        ),
2472        rotation_start_euler_xyz: euler_xyz[start * bone_count + bone],
2473        rotation_end_euler_xyz: euler_xyz[end * bone_count + bone],
2474        rotation_out_tangent: Vec3A::new(
2475            rotation[0].out_tangent,
2476            rotation[1].out_tangent,
2477            rotation[2].out_tangent,
2478        ),
2479        rotation_in_tangent: Vec3A::new(
2480            rotation[0].in_tangent,
2481            rotation[1].in_tangent,
2482            rotation[2].in_tangent,
2483        ),
2484    }
2485}
2486
2487fn fit_dcc_scalar_segment(
2488    start: usize,
2489    end: usize,
2490    value_at: impl Fn(usize) -> f32,
2491    frame_at: impl Fn(usize) -> f32,
2492) -> DccScalarSegment {
2493    let duration = frame_at(end) - frame_at(start);
2494    let start_value = value_at(start);
2495    let end_value = value_at(end);
2496    let slope = (end_value - start_value) / duration;
2497    if end <= start + 1 {
2498        return DccScalarSegment {
2499            out_tangent: slope,
2500            in_tangent: slope,
2501        };
2502    }
2503
2504    let mut aa = 0.0;
2505    let mut ab = 0.0;
2506    let mut bb = 0.0;
2507    let mut ar = 0.0;
2508    let mut br = 0.0;
2509    for sample in start + 1..end {
2510        let t = segment_amount(start, end, sample, &frame_at);
2511        let (h00, h10, h01, h11) = hermite_basis(t);
2512        let a = h10 * duration;
2513        let b = h11 * duration;
2514        let residual = value_at(sample) - h00 * start_value - h01 * end_value;
2515        aa += a * a;
2516        ab += a * b;
2517        bb += b * b;
2518        ar += a * residual;
2519        br += b * residual;
2520    }
2521    let determinant = aa * bb - ab * ab;
2522    let (mut out_tangent, mut in_tangent) = if determinant.abs() > f32::EPSILON {
2523        (
2524            (ar * bb - br * ab) / determinant,
2525            (br * aa - ar * ab) / determinant,
2526        )
2527    } else {
2528        (slope, slope)
2529    };
2530    clamp_monotonic_tangents(slope, &mut out_tangent, &mut in_tangent);
2531    DccScalarSegment {
2532        out_tangent,
2533        in_tangent,
2534    }
2535}
2536
2537fn clamp_monotonic_tangents(slope: f32, out_tangent: &mut f32, in_tangent: &mut f32) {
2538    if slope.abs() <= f32::EPSILON {
2539        *out_tangent = 0.0;
2540        *in_tangent = 0.0;
2541        return;
2542    }
2543    if *out_tangent * slope < 0.0 {
2544        *out_tangent = 0.0;
2545    }
2546    if *in_tangent * slope < 0.0 {
2547        *in_tangent = 0.0;
2548    }
2549    let alpha = *out_tangent / slope;
2550    let beta = *in_tangent / slope;
2551    let length = alpha.hypot(beta);
2552    if length > 3.0 {
2553        let scale = 3.0 / length;
2554        *out_tangent = scale * alpha * slope;
2555        *in_tangent = scale * beta * slope;
2556    }
2557}
2558
2559fn hermite_basis(t: f32) -> (f32, f32, f32, f32) {
2560    let t2 = t * t;
2561    let t3 = t2 * t;
2562    (
2563        2.0 * t3 - 3.0 * t2 + 1.0,
2564        t3 - 2.0 * t2 + t,
2565        -2.0 * t3 + 3.0 * t2,
2566        t3 - t2,
2567    )
2568}
2569
2570fn sample_hermite(
2571    start: f32,
2572    end: f32,
2573    out_tangent: f32,
2574    in_tangent: f32,
2575    duration: f32,
2576    t: f32,
2577) -> f32 {
2578    let (h00, h10, h01, h11) = hermite_basis(t);
2579    h00 * start + h10 * duration * out_tangent + h01 * end + h11 * duration * in_tangent
2580}
2581
2582fn sample_dcc_vec3(
2583    start: Vec3A,
2584    end: Vec3A,
2585    out_tangent: Vec3A,
2586    in_tangent: Vec3A,
2587    duration: f32,
2588    t: f32,
2589) -> Vec3A {
2590    Vec3A::new(
2591        sample_hermite(start.x, end.x, out_tangent.x, in_tangent.x, duration, t),
2592        sample_hermite(start.y, end.y, out_tangent.y, in_tangent.y, duration, t),
2593        sample_hermite(start.z, end.z, out_tangent.z, in_tangent.z, duration, t),
2594    )
2595}
2596
2597fn sample_bone_track(
2598    track: &ReducedBoneTrack,
2599    sample_frames: &[f32],
2600    frame: f32,
2601    target: ReductionTarget,
2602) -> (Vec3A, Quat) {
2603    let upper = track
2604        .keys
2605        .partition_point(|key| sample_frames[key.sample_index] <= frame);
2606    if upper == 0 {
2607        return (track.keys[0].translation, track.keys[0].rotation);
2608    }
2609    if upper == track.keys.len() {
2610        let key = track.keys[track.keys.len() - 1];
2611        return (key.translation, key.rotation);
2612    }
2613    let left = track.keys[upper - 1];
2614    let right = track.keys[upper];
2615    let amount = ((frame - sample_frames[left.sample_index])
2616        / (sample_frames[right.sample_index] - sample_frames[left.sample_index]))
2617        .clamp(0.0, 1.0);
2618    if target == ReductionTarget::DccCubic {
2619        let duration = sample_frames[right.sample_index] - sample_frames[left.sample_index];
2620        let segment = right.dcc_segment;
2621        let translation = Vec3A::new(
2622            sample_hermite(
2623                left.translation.x,
2624                right.translation.x,
2625                segment.translation_out_tangent.x,
2626                segment.translation_in_tangent.x,
2627                duration,
2628                amount,
2629            ),
2630            sample_hermite(
2631                left.translation.y,
2632                right.translation.y,
2633                segment.translation_out_tangent.y,
2634                segment.translation_in_tangent.y,
2635                duration,
2636                amount,
2637            ),
2638            sample_hermite(
2639                left.translation.z,
2640                right.translation.z,
2641                segment.translation_out_tangent.z,
2642                segment.translation_in_tangent.z,
2643                duration,
2644                amount,
2645            ),
2646        );
2647        let euler = Vec3A::new(
2648            sample_hermite(
2649                segment.rotation_start_euler_xyz.x,
2650                segment.rotation_end_euler_xyz.x,
2651                segment.rotation_out_tangent.x,
2652                segment.rotation_in_tangent.x,
2653                duration,
2654                amount,
2655            ),
2656            sample_hermite(
2657                segment.rotation_start_euler_xyz.y,
2658                segment.rotation_end_euler_xyz.y,
2659                segment.rotation_out_tangent.y,
2660                segment.rotation_in_tangent.y,
2661                duration,
2662                amount,
2663            ),
2664            sample_hermite(
2665                segment.rotation_start_euler_xyz.z,
2666                segment.rotation_end_euler_xyz.z,
2667                segment.rotation_out_tangent.z,
2668                segment.rotation_in_tangent.z,
2669                duration,
2670                amount,
2671            ),
2672        );
2673        return (
2674            translation,
2675            normalize_quat(Quat::from_euler(EulerRot::XYZ, euler.x, euler.y, euler.z)),
2676        );
2677    }
2678    let translation_amount = if target == ReductionTarget::VmdBezier {
2679        Vec3A::new(
2680            right.vmd_interpolation.translation[0].evaluate(amount),
2681            right.vmd_interpolation.translation[1].evaluate(amount),
2682            right.vmd_interpolation.translation[2].evaluate(amount),
2683        )
2684    } else {
2685        Vec3A::splat(amount)
2686    };
2687    let rotation_amount = if target == ReductionTarget::VmdBezier {
2688        right.vmd_interpolation.rotation.evaluate(amount)
2689    } else {
2690        amount
2691    };
2692    (
2693        Vec3A::new(
2694            left.translation.x + (right.translation.x - left.translation.x) * translation_amount.x,
2695            left.translation.y + (right.translation.y - left.translation.y) * translation_amount.y,
2696            left.translation.z + (right.translation.z - left.translation.z) * translation_amount.z,
2697        ),
2698        normalize_quat(left.rotation.slerp(right.rotation, rotation_amount)),
2699    )
2700}
2701
2702fn sample_morph_track(
2703    track: &ReducedMorphTrack,
2704    sample_frames: &[f32],
2705    frame: f32,
2706    target: ReductionTarget,
2707) -> f32 {
2708    let upper = track
2709        .keys
2710        .partition_point(|key| sample_frames[key.sample_index] <= frame);
2711    if upper == 0 {
2712        return track.keys[0].weight;
2713    }
2714    if upper == track.keys.len() {
2715        return track.keys[track.keys.len() - 1].weight;
2716    }
2717    let left = track.keys[upper - 1];
2718    let right = track.keys[upper];
2719    let amount = ((frame - sample_frames[left.sample_index])
2720        / (sample_frames[right.sample_index] - sample_frames[left.sample_index]))
2721        .clamp(0.0, 1.0);
2722    if target == ReductionTarget::DccCubic {
2723        let duration = sample_frames[right.sample_index] - sample_frames[left.sample_index];
2724        return sample_hermite(
2725            left.weight,
2726            right.weight,
2727            right.dcc_segment.out_tangent,
2728            right.dcc_segment.in_tangent,
2729            duration,
2730            amount,
2731        );
2732    }
2733    left.weight + (right.weight - left.weight) * amount
2734}
2735
2736fn build_world_matrices_into(
2737    snapshot: &SkeletonSnapshot,
2738    translations: &[Vec3A],
2739    rotations: &[Quat],
2740    result: &mut Vec<Mat4>,
2741) {
2742    result.resize(snapshot.bone_count(), Mat4::IDENTITY);
2743    for &bone in snapshot.evaluation_order.iter() {
2744        let local = Mat4::from_rotation_translation(rotations[bone], translations[bone].into());
2745        result[bone] = if snapshot.parent_indices[bone] < 0 {
2746            local
2747        } else {
2748            result[snapshot.parent_indices[bone] as usize] * local
2749        };
2750    }
2751}
2752
2753fn build_evaluation_order(parents: &[i32]) -> Result<Vec<usize>, PoseReductionError> {
2754    fn visit(
2755        bone: usize,
2756        parents: &[i32],
2757        state: &mut [u8],
2758        order: &mut Vec<usize>,
2759    ) -> Result<(), PoseReductionError> {
2760        match state[bone] {
2761            1 => return Err(PoseReductionError::SkeletonCycle { bone }),
2762            2 => return Ok(()),
2763            _ => {}
2764        }
2765        state[bone] = 1;
2766        if parents[bone] >= 0 {
2767            visit(parents[bone] as usize, parents, state, order)?;
2768        }
2769        state[bone] = 2;
2770        order.push(bone);
2771        Ok(())
2772    }
2773    let mut state = vec![0u8; parents.len()];
2774    let mut order = Vec::with_capacity(parents.len());
2775    for bone in 0..parents.len() {
2776        visit(bone, parents, &mut state, &mut order)?;
2777    }
2778    Ok(order)
2779}
2780
2781fn normalize_quat(value: Quat) -> Quat {
2782    if value.length_squared() <= f32::EPSILON {
2783        Quat::IDENTITY
2784    } else {
2785        value.normalize()
2786    }
2787}
2788
2789fn quat_is_finite(value: Quat) -> bool {
2790    value.to_array().iter().all(|value| value.is_finite()) && value.length_squared() > f32::EPSILON
2791}
2792
2793fn quat_angle(a: Quat, b: Quat) -> f32 {
2794    let a = normalize_quat(a);
2795    let mut b = normalize_quat(b);
2796    if a.dot(b) < 0.0 {
2797        b = -b;
2798    }
2799    // For unit quaternions, the hemisphere-aligned chord length is
2800    // `2 * sin(theta / 4)`. Unlike `acos(dot)`, this remains stable for tiny
2801    // angles and returns exactly zero for identical f32 quaternions.
2802    let half_chord = ((a - b).length() * 0.5).clamp(0.0, 1.0);
2803    4.0 * half_chord.asin()
2804}
2805
2806fn normalized_error(error: f32, tolerance: f32) -> f32 {
2807    if tolerance == 0.0 {
2808        if error == 0.0 { 0.0 } else { f32::INFINITY }
2809    } else {
2810        error / tolerance
2811    }
2812}
2813
2814fn segment_amount(start: usize, end: usize, sample: usize, frame_at: impl Fn(usize) -> f32) -> f32 {
2815    let start_frame = frame_at(start);
2816    (frame_at(sample) - start_frame) / (frame_at(end) - start_frame)
2817}
2818
2819fn is_worse(current: Option<(f32, usize)>, error: f32, frame: usize) -> bool {
2820    current.is_none_or(|(best, best_frame)| error > best || (error == best && frame < best_frame))
2821}
2822
2823#[cfg(test)]
2824mod tests;