Skip to main content

mmd_anim_runtime/reduce/
mod.rs

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