Skip to main content

record_player/
timed_control.rs

1use crate::mechanics::DeckMechanicalControl;
2use crate::scratch_gate::{ScratchPreset, MAX_SCRATCH_CLICKS, MIN_SCRATCH_CLICKS};
3use serde::{Deserialize, Serialize};
4use std::error::Error;
5use std::fmt;
6use std::sync::atomic::{AtomicUsize, Ordering};
7
8const SNAPSHOT_VERSION: u32 = 3;
9static NEXT_TIMELINE_ID: AtomicUsize = AtomicUsize::new(1);
10
11/// The timeline rejects an incoming event when all slots are in use.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub enum FullCapacityPolicy {
15    RejectIncoming,
16}
17
18/// The timeline rejects an event before the current render frame.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub enum LateEventPolicy {
22    Reject,
23}
24
25/// The timeline rejects a repeated frame and sequence key.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub enum DuplicateEventPolicy {
29    Reject,
30}
31
32/// These policies do not discard, replace, or combine accepted transitions.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct ControlTimelinePolicy {
36    pub full_capacity: FullCapacityPolicy,
37    pub late_event: LateEventPolicy,
38    pub duplicate_event: DuplicateEventPolicy,
39}
40
41impl ControlTimelinePolicy {
42    pub const fn lossless() -> Self {
43        Self {
44            full_capacity: FullCapacityPolicy::RejectIncoming,
45            late_event: LateEventPolicy::Reject,
46            duplicate_event: DuplicateEventPolicy::Reject,
47        }
48    }
49}
50
51impl Default for ControlTimelinePolicy {
52    fn default() -> Self {
53        Self::lossless()
54    }
55}
56
57/// One complete player control state at an exact render frame.
58#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct PlayerControl {
61    pub deck: DeckMechanicalControl,
62    pub stylus_lowered: bool,
63    #[serde(default)]
64    pub scratch_preset: ScratchPreset,
65    #[serde(default = "default_scratch_clicks")]
66    pub scratch_clicks: u8,
67    #[serde(default = "default_manual_crossfader_gain")]
68    pub manual_crossfader_gain: f64,
69}
70
71impl PlayerControl {
72    pub const fn new(deck: DeckMechanicalControl, stylus_lowered: bool) -> Self {
73        Self {
74            deck,
75            stylus_lowered,
76            scratch_preset: ScratchPreset::Baby,
77            scratch_clicks: ScratchPreset::Baby.default_clicks(),
78            manual_crossfader_gain: 1.0,
79        }
80    }
81
82    /// Sets the complete scratch helper control for this sample-timed state.
83    pub const fn with_scratch(
84        mut self,
85        preset: ScratchPreset,
86        clicks: u8,
87        manual_crossfader_gain: f64,
88    ) -> Self {
89        self.scratch_preset = preset;
90        self.scratch_clicks = clicks;
91        self.manual_crossfader_gain = manual_crossfader_gain;
92        self
93    }
94}
95
96const fn default_scratch_clicks() -> u8 {
97    ScratchPreset::Baby.default_clicks()
98}
99
100const fn default_manual_crossfader_gain() -> f64 {
101    1.0
102}
103
104impl Default for PlayerControl {
105    fn default() -> Self {
106        Self::new(DeckMechanicalControl::default(), false)
107    }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct TimedPlayerControl {
113    pub absolute_frame: u64,
114    pub sequence: u64,
115    pub control: PlayerControl,
116}
117
118impl TimedPlayerControl {
119    pub const fn new(absolute_frame: u64, sequence: u64, control: PlayerControl) -> Self {
120        Self {
121            absolute_frame,
122            sequence,
123            control,
124        }
125    }
126
127    const fn key(self) -> TimedControlKey {
128        TimedControlKey {
129            absolute_frame: self.absolute_frame,
130            sequence: self.sequence,
131        }
132    }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137struct TimedControlKey {
138    absolute_frame: u64,
139    sequence: u64,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq)]
143pub enum ControlBlockItem {
144    /// Render these frames with one unchanged control value.
145    Span {
146        frame_offset: u32,
147        frame_count: u32,
148        control: PlayerControl,
149    },
150    /// Apply this transition before rendering the frame at `frame_offset`.
151    Transition {
152        frame_offset: u32,
153        event: TimedPlayerControl,
154    },
155}
156
157impl ControlBlockItem {
158    pub const fn frame_offset(self) -> u32 {
159        match self {
160            Self::Span { frame_offset, .. } | Self::Transition { frame_offset, .. } => frame_offset,
161        }
162    }
163}
164
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct ControlTimelineSnapshot {
168    version: u32,
169    capacity: usize,
170    policy: ControlTimelinePolicy,
171    render_frame: u64,
172    current_control: PlayerControl,
173    pending_events: Vec<TimedPlayerControl>,
174    last_submitted: Option<TimedControlKey>,
175}
176
177impl ControlTimelineSnapshot {
178    pub const fn capacity(&self) -> usize {
179        self.capacity
180    }
181
182    pub const fn render_frame(&self) -> u64 {
183        self.render_frame
184    }
185
186    pub fn pending_events(&self) -> &[TimedPlayerControl] {
187        &self.pending_events
188    }
189}
190
191/// A fixed-size marker for one timeline's logical render state.
192///
193/// A checkpoint remains valid after event consumption. A successful enqueue or
194/// snapshot restore invalidates every earlier checkpoint.
195#[derive(Debug, Clone, Copy, PartialEq)]
196#[must_use]
197pub struct PlayerControlTimelineCheckpoint {
198    timeline_id: usize,
199    slot_revision: u64,
200    head: usize,
201    len: usize,
202    render_frame: u64,
203    current_control: PlayerControl,
204    last_submitted: Option<TimedControlKey>,
205}
206
207/// This queue allocates its complete event storage during construction.
208#[derive(Debug)]
209pub struct PlayerControlTimeline {
210    slots: Box<[Option<TimedPlayerControl>]>,
211    timeline_id: usize,
212    slot_revision: u64,
213    head: usize,
214    len: usize,
215    policy: ControlTimelinePolicy,
216    render_frame: u64,
217    current_control: PlayerControl,
218    last_submitted: Option<TimedControlKey>,
219}
220
221impl PlayerControlTimeline {
222    pub fn new(
223        capacity: usize,
224        render_frame: u64,
225        initial_control: PlayerControl,
226    ) -> Result<Self, ControlTimelineCreateError> {
227        Self::with_policy(
228            capacity,
229            render_frame,
230            initial_control,
231            ControlTimelinePolicy::lossless(),
232        )
233    }
234
235    pub fn with_policy(
236        capacity: usize,
237        render_frame: u64,
238        initial_control: PlayerControl,
239        policy: ControlTimelinePolicy,
240    ) -> Result<Self, ControlTimelineCreateError> {
241        if capacity == 0 {
242            return Err(ControlTimelineCreateError::ZeroCapacity);
243        }
244        validate_control(initial_control).map_err(ControlTimelineCreateError::InvalidControl)?;
245
246        Ok(Self {
247            slots: vec![None; capacity].into_boxed_slice(),
248            timeline_id: NEXT_TIMELINE_ID.fetch_add(1, Ordering::Relaxed),
249            slot_revision: 0,
250            head: 0,
251            len: 0,
252            policy,
253            render_frame,
254            current_control: initial_control,
255            last_submitted: None,
256        })
257    }
258
259    pub const fn capacity(&self) -> usize {
260        self.slots.len()
261    }
262
263    pub const fn len(&self) -> usize {
264        self.len
265    }
266
267    pub const fn is_empty(&self) -> bool {
268        self.len == 0
269    }
270
271    pub const fn is_full(&self) -> bool {
272        self.len == self.slots.len()
273    }
274
275    pub const fn policy(&self) -> ControlTimelinePolicy {
276        self.policy
277    }
278
279    pub const fn render_frame(&self) -> u64 {
280        self.render_frame
281    }
282
283    pub const fn current_control(&self) -> PlayerControl {
284        self.current_control
285    }
286
287    pub fn next_event(&self) -> Option<TimedPlayerControl> {
288        self.peek()
289    }
290
291    /// Captures the logical queue and render state in constant time.
292    ///
293    /// This function does not allocate. Event consumption does not invalidate
294    /// the result.
295    pub const fn checkpoint(&self) -> PlayerControlTimelineCheckpoint {
296        PlayerControlTimelineCheckpoint {
297            timeline_id: self.timeline_id,
298            slot_revision: self.slot_revision,
299            head: self.head,
300            len: self.len,
301            render_frame: self.render_frame,
302            current_control: self.current_control,
303            last_submitted: self.last_submitted,
304        }
305    }
306
307    /// Restores a checkpoint in constant time.
308    ///
309    /// This function does not allocate. It rejects a marker from another
310    /// timeline. It also rejects a marker after a slot write.
311    pub fn restore_checkpoint(
312        &mut self,
313        checkpoint: PlayerControlTimelineCheckpoint,
314    ) -> Result<(), ControlTimelineCheckpointRestoreError> {
315        if checkpoint.timeline_id != self.timeline_id {
316            return Err(ControlTimelineCheckpointRestoreError::TimelineMismatch);
317        }
318        if checkpoint.slot_revision != self.slot_revision {
319            return Err(ControlTimelineCheckpointRestoreError::SlotsChanged);
320        }
321
322        self.head = checkpoint.head;
323        self.len = checkpoint.len;
324        self.render_frame = checkpoint.render_frame;
325        self.current_control = checkpoint.current_control;
326        self.last_submitted = checkpoint.last_submitted;
327        Ok(())
328    }
329
330    /// Returns the next transition inside the half-open render block.
331    pub fn next_transition_offset(&self, block_frame_count: u32) -> Option<u32> {
332        let event = self.peek()?;
333        let offset = event.absolute_frame.checked_sub(self.render_frame)?;
334        (offset < u64::from(block_frame_count)).then_some(offset as u32)
335    }
336
337    /// Adds one event without allocating or changing an accepted event.
338    pub fn enqueue(&mut self, event: TimedPlayerControl) -> Result<(), ControlTimelinePushError> {
339        validate_control(event.control).map_err(ControlTimelinePushError::InvalidControl)?;
340
341        let key = event.key();
342        if self.last_submitted == Some(key) {
343            return Err(ControlTimelinePushError::Duplicate {
344                absolute_frame: key.absolute_frame,
345                sequence: key.sequence,
346            });
347        }
348        if event.absolute_frame < self.render_frame {
349            return Err(ControlTimelinePushError::Late {
350                absolute_frame: event.absolute_frame,
351                render_frame: self.render_frame,
352            });
353        }
354        if let Some(previous) = self.last_submitted {
355            if event.absolute_frame < previous.absolute_frame {
356                return Err(ControlTimelinePushError::NonMonotonicFrame {
357                    previous: previous.absolute_frame,
358                    incoming: event.absolute_frame,
359                });
360            }
361            if event.sequence <= previous.sequence {
362                return Err(ControlTimelinePushError::NonMonotonicSequence {
363                    previous: previous.sequence,
364                    incoming: event.sequence,
365                });
366            }
367        }
368        if self.is_full() {
369            return Err(ControlTimelinePushError::Full {
370                capacity: self.capacity(),
371            });
372        }
373
374        let tail = (self.head + self.len) % self.capacity();
375        // A consumed event can remain here. The logical tail owns this slot.
376        self.slots[tail] = Some(event);
377        self.slot_revision = self.slot_revision.wrapping_add(1);
378        self.len += 1;
379        self.last_submitted = Some(key);
380        Ok(())
381    }
382
383    /// Visits every transition and render span in one half-open block.
384    ///
385    /// Events at the block end remain pending for the next block.
386    pub fn visit_block(
387        &mut self,
388        block_frame_count: u32,
389        mut visit: impl FnMut(ControlBlockItem),
390    ) -> Result<(), ControlTimelineAdvanceError> {
391        let block_start = self.render_frame;
392        let block_end = block_start
393            .checked_add(u64::from(block_frame_count))
394            .ok_or(ControlTimelineAdvanceError::FrameOverflow)?;
395        let mut span_start = block_start;
396
397        while let Some(event) = self.peek() {
398            if event.absolute_frame >= block_end {
399                break;
400            }
401            debug_assert!(event.absolute_frame >= span_start);
402
403            if event.absolute_frame > span_start {
404                visit(ControlBlockItem::Span {
405                    frame_offset: (span_start - block_start) as u32,
406                    frame_count: (event.absolute_frame - span_start) as u32,
407                    control: self.current_control,
408                });
409                span_start = event.absolute_frame;
410            }
411
412            let event = self.pop().expect("the queue head must exist");
413            self.current_control = event.control;
414            visit(ControlBlockItem::Transition {
415                frame_offset: (event.absolute_frame - block_start) as u32,
416                event,
417            });
418        }
419
420        if span_start < block_end {
421            visit(ControlBlockItem::Span {
422                frame_offset: (span_start - block_start) as u32,
423                frame_count: (block_end - span_start) as u32,
424                control: self.current_control,
425            });
426        }
427        self.render_frame = block_end;
428        Ok(())
429    }
430
431    pub fn snapshot(&self) -> ControlTimelineSnapshot {
432        let mut pending_events = Vec::with_capacity(self.len);
433        for logical_index in 0..self.len {
434            let slot = (self.head + logical_index) % self.capacity();
435            pending_events
436                .push(self.slots[slot].expect("an occupied queue slot must contain an event"));
437        }
438        ControlTimelineSnapshot {
439            version: SNAPSHOT_VERSION,
440            capacity: self.capacity(),
441            policy: self.policy,
442            render_frame: self.render_frame,
443            current_control: self.current_control,
444            pending_events,
445            last_submitted: self.last_submitted,
446        }
447    }
448
449    /// Restore validates all data before it changes the current timeline.
450    pub fn restore(
451        &mut self,
452        snapshot: &ControlTimelineSnapshot,
453    ) -> Result<(), ControlTimelineRestoreError> {
454        validate_snapshot(snapshot, self.capacity())?;
455
456        self.slots.fill(None);
457        self.head = 0;
458        self.len = snapshot.pending_events.len();
459        for (slot, event) in self
460            .slots
461            .iter_mut()
462            .zip(snapshot.pending_events.iter().copied())
463        {
464            *slot = Some(event);
465        }
466        self.slot_revision = self.slot_revision.wrapping_add(1);
467        self.policy = snapshot.policy;
468        self.render_frame = snapshot.render_frame;
469        self.current_control = snapshot.current_control;
470        self.last_submitted = snapshot.last_submitted;
471        Ok(())
472    }
473
474    pub fn from_snapshot(
475        snapshot: &ControlTimelineSnapshot,
476    ) -> Result<Self, ControlTimelineRestoreError> {
477        validate_snapshot(snapshot, snapshot.capacity)?;
478        let mut timeline = Self::with_policy(
479            snapshot.capacity,
480            snapshot.render_frame,
481            snapshot.current_control,
482            snapshot.policy,
483        )
484        .map_err(|_| ControlTimelineRestoreError::InvalidSnapshot)?;
485        timeline.restore(snapshot)?;
486        Ok(timeline)
487    }
488
489    fn peek(&self) -> Option<TimedPlayerControl> {
490        (self.len > 0).then(|| self.slots[self.head]).flatten()
491    }
492
493    fn pop(&mut self) -> Option<TimedPlayerControl> {
494        if self.len == 0 {
495            return None;
496        }
497        // Keep the value so a render checkpoint can restore this queue view.
498        let event = self.slots[self.head];
499        self.head = (self.head + 1) % self.capacity();
500        self.len -= 1;
501        event
502    }
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
506pub enum ControlValueRequirement {
507    Finite,
508    Nonnegative,
509    UnitInterval,
510    ScratchClickCount,
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
514pub struct InvalidControlValue {
515    pub field: &'static str,
516    pub requirement: ControlValueRequirement,
517}
518
519#[derive(Debug, Clone, Copy, PartialEq, Eq)]
520pub enum ControlTimelineCreateError {
521    ZeroCapacity,
522    InvalidControl(InvalidControlValue),
523}
524
525#[derive(Debug, Clone, Copy, PartialEq, Eq)]
526pub enum ControlTimelinePushError {
527    InvalidControl(InvalidControlValue),
528    Late {
529        absolute_frame: u64,
530        render_frame: u64,
531    },
532    Duplicate {
533        absolute_frame: u64,
534        sequence: u64,
535    },
536    NonMonotonicFrame {
537        previous: u64,
538        incoming: u64,
539    },
540    NonMonotonicSequence {
541        previous: u64,
542        incoming: u64,
543    },
544    Full {
545        capacity: usize,
546    },
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub enum ControlTimelineAdvanceError {
551    FrameOverflow,
552}
553
554#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555pub enum ControlTimelineCheckpointRestoreError {
556    TimelineMismatch,
557    SlotsChanged,
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub enum ControlTimelineRestoreError {
562    UnsupportedVersion { version: u32 },
563    CapacityMismatch { expected: usize, actual: usize },
564    InvalidSnapshot,
565}
566
567impl fmt::Display for ControlTimelineCreateError {
568    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
569        match self {
570            Self::ZeroCapacity => formatter.write_str("control timeline capacity must be positive"),
571            Self::InvalidControl(value) => write_invalid_control(formatter, *value),
572        }
573    }
574}
575
576impl Error for ControlTimelineCreateError {}
577
578impl fmt::Display for ControlTimelinePushError {
579    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
580        match self {
581            Self::InvalidControl(value) => write_invalid_control(formatter, *value),
582            Self::Late {
583                absolute_frame,
584                render_frame,
585            } => write!(
586                formatter,
587                "control event frame {absolute_frame} is before render frame {render_frame}"
588            ),
589            Self::Duplicate {
590                absolute_frame,
591                sequence,
592            } => write!(
593                formatter,
594                "control event frame {absolute_frame} and sequence {sequence} are duplicates"
595            ),
596            Self::NonMonotonicFrame { previous, incoming } => write!(
597                formatter,
598                "control event frame {incoming} is before submitted frame {previous}"
599            ),
600            Self::NonMonotonicSequence { previous, incoming } => write!(
601                formatter,
602                "control event sequence {incoming} does not follow sequence {previous}"
603            ),
604            Self::Full { capacity } => {
605                write!(formatter, "control timeline capacity {capacity} is full")
606            }
607        }
608    }
609}
610
611impl Error for ControlTimelinePushError {}
612
613impl fmt::Display for ControlTimelineAdvanceError {
614    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
615        match self {
616            Self::FrameOverflow => formatter.write_str("control timeline frame overflow"),
617        }
618    }
619}
620
621impl Error for ControlTimelineAdvanceError {}
622
623impl fmt::Display for ControlTimelineCheckpointRestoreError {
624    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
625        match self {
626            Self::TimelineMismatch => {
627                formatter.write_str("checkpoint belongs to a different control timeline")
628            }
629            Self::SlotsChanged => {
630                formatter.write_str("control timeline slots changed after the checkpoint")
631            }
632        }
633    }
634}
635
636impl Error for ControlTimelineCheckpointRestoreError {}
637
638impl fmt::Display for ControlTimelineRestoreError {
639    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
640        match self {
641            Self::UnsupportedVersion { version } => {
642                write!(
643                    formatter,
644                    "unsupported control timeline snapshot version {version}"
645                )
646            }
647            Self::CapacityMismatch { expected, actual } => write!(
648                formatter,
649                "control timeline snapshot capacity {actual} does not match {expected}"
650            ),
651            Self::InvalidSnapshot => formatter.write_str("invalid control timeline snapshot"),
652        }
653    }
654}
655
656impl Error for ControlTimelineRestoreError {}
657
658fn write_invalid_control(
659    formatter: &mut fmt::Formatter<'_>,
660    value: InvalidControlValue,
661) -> fmt::Result {
662    let requirement = match value.requirement {
663        ControlValueRequirement::Finite => "finite",
664        ControlValueRequirement::Nonnegative => "nonnegative",
665        ControlValueRequirement::UnitInterval => "between zero and one inclusive",
666        ControlValueRequirement::ScratchClickCount => "a supported scratch click count",
667    };
668    write!(
669        formatter,
670        "control field {} must be {requirement}",
671        value.field
672    )
673}
674
675fn validate_control(control: PlayerControl) -> Result<(), InvalidControlValue> {
676    let deck = control.deck;
677    validate_finite(
678        "motorTargetAngularVelocityRadS",
679        deck.motor_target_angular_velocity_rad_s,
680    )?;
681    if let Some(angle) = deck.hand_target_angle_rad {
682        validate_finite("handTargetAngleRad", angle)?;
683    }
684    validate_finite(
685        "handTargetAngularVelocityRadS",
686        deck.hand_target_angular_velocity_rad_s,
687    )?;
688    validate_nonnegative("handNormalForceN", deck.hand_normal_force_n)?;
689    validate_nonnegative("handContactRadiusM", deck.hand_contact_radius_m)?;
690    validate_finite("stylusTorqueNm", deck.stylus_torque_nm)?;
691    if !(MIN_SCRATCH_CLICKS..=MAX_SCRATCH_CLICKS).contains(&control.scratch_clicks) {
692        return Err(InvalidControlValue {
693            field: "scratchClicks",
694            requirement: ControlValueRequirement::ScratchClickCount,
695        });
696    }
697    validate_unit_interval("manualCrossfaderGain", control.manual_crossfader_gain)?;
698    Ok(())
699}
700
701fn validate_finite(field: &'static str, value: f64) -> Result<(), InvalidControlValue> {
702    if value.is_finite() {
703        Ok(())
704    } else {
705        Err(InvalidControlValue {
706            field,
707            requirement: ControlValueRequirement::Finite,
708        })
709    }
710}
711
712fn validate_nonnegative(field: &'static str, value: f64) -> Result<(), InvalidControlValue> {
713    validate_finite(field, value)?;
714    if value >= 0.0 {
715        Ok(())
716    } else {
717        Err(InvalidControlValue {
718            field,
719            requirement: ControlValueRequirement::Nonnegative,
720        })
721    }
722}
723
724fn validate_unit_interval(field: &'static str, value: f64) -> Result<(), InvalidControlValue> {
725    validate_finite(field, value)?;
726    if (0.0..=1.0).contains(&value) {
727        Ok(())
728    } else {
729        Err(InvalidControlValue {
730            field,
731            requirement: ControlValueRequirement::UnitInterval,
732        })
733    }
734}
735
736fn validate_snapshot(
737    snapshot: &ControlTimelineSnapshot,
738    expected_capacity: usize,
739) -> Result<(), ControlTimelineRestoreError> {
740    if snapshot.version != SNAPSHOT_VERSION {
741        return Err(ControlTimelineRestoreError::UnsupportedVersion {
742            version: snapshot.version,
743        });
744    }
745    if snapshot.capacity != expected_capacity {
746        return Err(ControlTimelineRestoreError::CapacityMismatch {
747            expected: expected_capacity,
748            actual: snapshot.capacity,
749        });
750    }
751    if snapshot.capacity == 0 || snapshot.pending_events.len() > snapshot.capacity {
752        return Err(ControlTimelineRestoreError::InvalidSnapshot);
753    }
754    if validate_control(snapshot.current_control).is_err() {
755        return Err(ControlTimelineRestoreError::InvalidSnapshot);
756    }
757
758    let mut previous: Option<TimedControlKey> = None;
759    for event in snapshot.pending_events.iter().copied() {
760        if validate_control(event.control).is_err() || event.absolute_frame < snapshot.render_frame
761        {
762            return Err(ControlTimelineRestoreError::InvalidSnapshot);
763        }
764        if let Some(previous) = previous {
765            if event.absolute_frame < previous.absolute_frame || event.sequence <= previous.sequence
766            {
767                return Err(ControlTimelineRestoreError::InvalidSnapshot);
768            }
769        }
770        previous = Some(event.key());
771    }
772
773    match (previous, snapshot.last_submitted) {
774        (Some(tail), Some(last)) if tail == last => {}
775        (None, Some(last)) if last.absolute_frame < snapshot.render_frame => {}
776        (None, None) => {}
777        _ => return Err(ControlTimelineRestoreError::InvalidSnapshot),
778    }
779    Ok(())
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785    use crate::mechanics::MotorMode;
786
787    fn control(rate: f64) -> PlayerControl {
788        PlayerControl::new(
789            DeckMechanicalControl {
790                motor_mode: MotorMode::Servo,
791                motor_target_angular_velocity_rad_s: rate,
792                hand_contact: rate != 0.0,
793                hand_target_angle_rad: None,
794                hand_target_angular_velocity_rad_s: rate,
795                hand_normal_force_n: rate.abs(),
796                hand_contact_radius_m: 0.12,
797                stylus_torque_nm: 0.0,
798            },
799            rate.is_sign_positive(),
800        )
801    }
802
803    #[test]
804    fn partitions_block_at_exact_event_frames() {
805        let mut timeline = PlayerControlTimeline::new(8, 1_000, control(0.0)).unwrap();
806        timeline
807            .enqueue(TimedPlayerControl::new(1_003, 1, control(1.0)))
808            .unwrap();
809        timeline
810            .enqueue(TimedPlayerControl::new(1_007, 2, control(-1.0)))
811            .unwrap();
812
813        assert_eq!(timeline.next_transition_offset(8), Some(3));
814        let mut items = Vec::new();
815        timeline.visit_block(10, |item| items.push(item)).unwrap();
816
817        assert_eq!(
818            items,
819            vec![
820                ControlBlockItem::Span {
821                    frame_offset: 0,
822                    frame_count: 3,
823                    control: control(0.0),
824                },
825                ControlBlockItem::Transition {
826                    frame_offset: 3,
827                    event: TimedPlayerControl::new(1_003, 1, control(1.0)),
828                },
829                ControlBlockItem::Span {
830                    frame_offset: 3,
831                    frame_count: 4,
832                    control: control(1.0),
833                },
834                ControlBlockItem::Transition {
835                    frame_offset: 7,
836                    event: TimedPlayerControl::new(1_007, 2, control(-1.0)),
837                },
838                ControlBlockItem::Span {
839                    frame_offset: 7,
840                    frame_count: 3,
841                    control: control(-1.0),
842                },
843            ]
844        );
845        assert_eq!(timeline.render_frame(), 1_010);
846        assert_eq!(timeline.current_control(), control(-1.0));
847    }
848
849    #[test]
850    fn preserves_several_transitions_in_one_quantum() {
851        let mut timeline = PlayerControlTimeline::new(8, 200, control(0.0)).unwrap();
852        for (frame, sequence, rate) in [
853            (200, 10, 1.0),
854            (200, 11, -1.0),
855            (201, 12, 0.5),
856            (205, 13, -0.5),
857        ] {
858            timeline
859                .enqueue(TimedPlayerControl::new(frame, sequence, control(rate)))
860                .unwrap();
861        }
862
863        let mut transitions = Vec::new();
864        timeline
865            .visit_block(8, |item| {
866                if let ControlBlockItem::Transition {
867                    frame_offset,
868                    event,
869                } = item
870                {
871                    transitions.push((frame_offset, event.sequence));
872                }
873            })
874            .unwrap();
875
876        assert_eq!(transitions, vec![(0, 10), (0, 11), (1, 12), (5, 13)]);
877        assert!(timeline.is_empty());
878    }
879
880    #[test]
881    fn preserves_stylus_transitions_at_one_frame() {
882        let initial = PlayerControl::new(control(0.0).deck, true);
883        let mut lifted = initial;
884        lifted.stylus_lowered = false;
885        let mut lowered = lifted;
886        lowered.stylus_lowered = true;
887        let mut timeline = PlayerControlTimeline::new(4, 300, initial).unwrap();
888        timeline
889            .enqueue(TimedPlayerControl::new(302, 1, lifted))
890            .unwrap();
891        timeline
892            .enqueue(TimedPlayerControl::new(302, 2, lowered))
893            .unwrap();
894
895        let mut observed = Vec::new();
896        timeline
897            .visit_block(4, |item| {
898                if let ControlBlockItem::Transition { event, .. } = item {
899                    observed.push(event.control.stylus_lowered);
900                }
901            })
902            .unwrap();
903
904        assert_eq!(observed, [false, true]);
905        assert!(timeline.current_control().stylus_lowered);
906    }
907
908    #[test]
909    fn event_at_block_end_starts_the_next_block() {
910        let mut timeline = PlayerControlTimeline::new(2, 50, control(0.0)).unwrap();
911        timeline
912            .enqueue(TimedPlayerControl::new(58, 1, control(1.0)))
913            .unwrap();
914        assert_eq!(timeline.next_transition_offset(8), None);
915
916        let mut first = Vec::new();
917        timeline.visit_block(8, |item| first.push(item)).unwrap();
918        assert_eq!(timeline.len(), 1);
919        assert_eq!(timeline.next_transition_offset(1), Some(0));
920
921        let mut second = Vec::new();
922        timeline.visit_block(1, |item| second.push(item)).unwrap();
923        assert!(matches!(
924            second.first(),
925            Some(ControlBlockItem::Transition {
926                frame_offset: 0,
927                ..
928            })
929        ));
930    }
931
932    #[test]
933    fn rejects_late_event_without_changing_queue() {
934        let mut timeline = PlayerControlTimeline::new(2, 500, control(0.0)).unwrap();
935        let error = timeline
936            .enqueue(TimedPlayerControl::new(499, 1, control(1.0)))
937            .unwrap_err();
938        assert_eq!(
939            error,
940            ControlTimelinePushError::Late {
941                absolute_frame: 499,
942                render_frame: 500,
943            }
944        );
945        assert!(timeline.is_empty());
946    }
947
948    #[test]
949    fn rejects_new_event_at_full_capacity() {
950        let mut timeline = PlayerControlTimeline::new(2, 0, control(0.0)).unwrap();
951        timeline
952            .enqueue(TimedPlayerControl::new(1, 1, control(1.0)))
953            .unwrap();
954        timeline
955            .enqueue(TimedPlayerControl::new(2, 2, control(-1.0)))
956            .unwrap();
957
958        assert_eq!(
959            timeline
960                .enqueue(TimedPlayerControl::new(3, 3, control(0.5)))
961                .unwrap_err(),
962            ControlTimelinePushError::Full { capacity: 2 }
963        );
964        assert_eq!(timeline.len(), 2);
965        assert_eq!(timeline.next_event().unwrap().sequence, 1);
966    }
967
968    #[test]
969    fn validates_timestamp_sequence_and_duplicate_order() {
970        let mut timeline = PlayerControlTimeline::new(8, 0, control(0.0)).unwrap();
971        timeline
972            .enqueue(TimedPlayerControl::new(10, 10, control(1.0)))
973            .unwrap();
974
975        assert!(matches!(
976            timeline.enqueue(TimedPlayerControl::new(10, 10, control(1.0))),
977            Err(ControlTimelinePushError::Duplicate { .. })
978        ));
979        assert_eq!(
980            timeline
981                .enqueue(TimedPlayerControl::new(9, 11, control(-1.0)))
982                .unwrap_err(),
983            ControlTimelinePushError::NonMonotonicFrame {
984                previous: 10,
985                incoming: 9,
986            }
987        );
988        assert_eq!(
989            timeline
990                .enqueue(TimedPlayerControl::new(11, 9, control(-1.0)))
991                .unwrap_err(),
992            ControlTimelinePushError::NonMonotonicSequence {
993                previous: 10,
994                incoming: 9,
995            }
996        );
997        timeline
998            .enqueue(TimedPlayerControl::new(10, 11, control(-1.0)))
999            .unwrap();
1000    }
1001
1002    #[test]
1003    fn rapid_reversals_remain_distinct_and_ordered() {
1004        let mut timeline = PlayerControlTimeline::new(32, 8_000, control(0.0)).unwrap();
1005        for index in 0..24_u64 {
1006            let rate = if index % 2 == 0 { 18.0 } else { -18.0 };
1007            timeline
1008                .enqueue(TimedPlayerControl::new(
1009                    8_000 + index,
1010                    100 + index,
1011                    control(rate),
1012                ))
1013                .unwrap();
1014        }
1015
1016        let mut observed = Vec::new();
1017        timeline
1018            .visit_block(24, |item| {
1019                if let ControlBlockItem::Transition { event, .. } = item {
1020                    observed.push(event.control.deck.hand_target_angular_velocity_rad_s);
1021                }
1022            })
1023            .unwrap();
1024
1025        assert_eq!(observed.len(), 24);
1026        assert!(observed
1027            .windows(2)
1028            .all(|rates| rates[0].is_sign_positive() != rates[1].is_sign_positive()));
1029    }
1030
1031    #[test]
1032    fn rejects_invalid_control_values() {
1033        let mut timeline = PlayerControlTimeline::new(2, 0, control(0.0)).unwrap();
1034        let mut invalid = control(1.0);
1035        invalid.deck.hand_target_angle_rad = Some(f64::NAN);
1036        assert_eq!(
1037            timeline
1038                .enqueue(TimedPlayerControl::new(0, 1, invalid))
1039                .unwrap_err(),
1040            ControlTimelinePushError::InvalidControl(InvalidControlValue {
1041                field: "handTargetAngleRad",
1042                requirement: ControlValueRequirement::Finite,
1043            })
1044        );
1045
1046        invalid = control(1.0);
1047        invalid.deck.hand_normal_force_n = -0.1;
1048        assert!(matches!(
1049            timeline.enqueue(TimedPlayerControl::new(0, 1, invalid)),
1050            Err(ControlTimelinePushError::InvalidControl(
1051                InvalidControlValue {
1052                    requirement: ControlValueRequirement::Nonnegative,
1053                    ..
1054                }
1055            ))
1056        ));
1057
1058        for clicks in [0, MAX_SCRATCH_CLICKS.saturating_add(1)] {
1059            invalid = control(1.0);
1060            invalid.scratch_clicks = clicks;
1061            assert_eq!(
1062                timeline
1063                    .enqueue(TimedPlayerControl::new(0, 1, invalid))
1064                    .unwrap_err(),
1065                ControlTimelinePushError::InvalidControl(InvalidControlValue {
1066                    field: "scratchClicks",
1067                    requirement: ControlValueRequirement::ScratchClickCount,
1068                })
1069            );
1070        }
1071
1072        for gain in [f64::NAN, -0.1, 1.1] {
1073            invalid = control(1.0);
1074            invalid.manual_crossfader_gain = gain;
1075            let expected_requirement = if gain.is_nan() {
1076                ControlValueRequirement::Finite
1077            } else {
1078                ControlValueRequirement::UnitInterval
1079            };
1080            assert_eq!(
1081                timeline
1082                    .enqueue(TimedPlayerControl::new(0, 1, invalid))
1083                    .unwrap_err(),
1084                ControlTimelinePushError::InvalidControl(InvalidControlValue {
1085                    field: "manualCrossfaderGain",
1086                    requirement: expected_requirement,
1087                })
1088            );
1089        }
1090    }
1091
1092    #[test]
1093    fn snapshot_restore_replays_identically_after_ring_wrap() {
1094        let mut timeline = PlayerControlTimeline::new(4, 1_000, control(0.0)).unwrap();
1095        timeline
1096            .enqueue(TimedPlayerControl::new(1_001, 1, control(1.0)))
1097            .unwrap();
1098        timeline
1099            .enqueue(TimedPlayerControl::new(1_002, 2, control(-1.0)))
1100            .unwrap();
1101        timeline.visit_block(3, |_| {}).unwrap();
1102        timeline
1103            .enqueue(TimedPlayerControl::new(1_004, 3, control(0.5)))
1104            .unwrap();
1105        timeline
1106            .enqueue(TimedPlayerControl::new(1_006, 4, control(-0.5)))
1107            .unwrap();
1108        timeline
1109            .enqueue(TimedPlayerControl::new(1_007, 5, control(0.25)))
1110            .unwrap();
1111
1112        let snapshot = timeline.snapshot();
1113        assert_eq!(snapshot.version, 3);
1114        let mut restored = PlayerControlTimeline::from_snapshot(&snapshot).unwrap();
1115        assert_eq!(restored.snapshot(), snapshot);
1116
1117        let mut original_items = Vec::new();
1118        let mut restored_items = Vec::new();
1119        timeline
1120            .visit_block(5, |item| original_items.push(item))
1121            .unwrap();
1122        restored
1123            .visit_block(5, |item| restored_items.push(item))
1124            .unwrap();
1125        assert_eq!(restored_items, original_items);
1126        assert_eq!(restored.snapshot(), timeline.snapshot());
1127    }
1128
1129    #[test]
1130    fn restore_rejects_the_previous_control_snapshot_schema() {
1131        let mut timeline = PlayerControlTimeline::new(2, 0, control(0.0)).unwrap();
1132        let before = timeline.snapshot();
1133        let mut previous = before.clone();
1134        previous.version = SNAPSHOT_VERSION - 1;
1135
1136        assert_eq!(
1137            timeline.restore(&previous),
1138            Err(ControlTimelineRestoreError::UnsupportedVersion { version: 2 })
1139        );
1140        assert_eq!(timeline.snapshot(), before);
1141    }
1142
1143    #[test]
1144    fn checkpoint_restores_consumed_events_and_render_state() {
1145        let initial = control(0.0);
1146        let mut timeline = PlayerControlTimeline::new(4, 100, initial).unwrap();
1147        for (frame, sequence, rate) in [(101, 1, 1.0), (103, 2, -1.0), (110, 3, 0.5)] {
1148            timeline
1149                .enqueue(TimedPlayerControl::new(frame, sequence, control(rate)))
1150                .unwrap();
1151        }
1152        let checkpoint = timeline.checkpoint();
1153
1154        let mut first_render = Vec::new();
1155        timeline
1156            .visit_block(5, |item| first_render.push(item))
1157            .unwrap();
1158        assert_eq!(timeline.render_frame(), 105);
1159        assert_eq!(timeline.len(), 1);
1160        assert_eq!(timeline.current_control(), control(-1.0));
1161
1162        timeline.restore_checkpoint(checkpoint).unwrap();
1163        assert_eq!(timeline.render_frame(), 100);
1164        assert_eq!(timeline.len(), 3);
1165        assert_eq!(timeline.current_control(), initial);
1166        assert_eq!(timeline.next_event().unwrap().sequence, 1);
1167
1168        let mut repeated_render = Vec::new();
1169        timeline
1170            .visit_block(5, |item| repeated_render.push(item))
1171            .unwrap();
1172        assert_eq!(repeated_render, first_render);
1173
1174        timeline.restore_checkpoint(checkpoint).unwrap();
1175        assert_eq!(timeline.next_event().unwrap().sequence, 1);
1176    }
1177
1178    #[test]
1179    fn checkpoint_restores_wrapped_full_queue() {
1180        let mut timeline = PlayerControlTimeline::new(3, 0, control(0.0)).unwrap();
1181        for (frame, sequence, rate) in [(1, 1, 1.0), (2, 2, -1.0), (9, 3, 0.5)] {
1182            timeline
1183                .enqueue(TimedPlayerControl::new(frame, sequence, control(rate)))
1184                .unwrap();
1185        }
1186        timeline.visit_block(3, |_| {}).unwrap();
1187        timeline
1188            .enqueue(TimedPlayerControl::new(10, 4, control(-0.5)))
1189            .unwrap();
1190        timeline
1191            .enqueue(TimedPlayerControl::new(11, 5, control(0.25)))
1192            .unwrap();
1193        assert!(timeline.is_full());
1194
1195        let checkpoint = timeline.checkpoint();
1196        let before = timeline.snapshot();
1197        timeline.visit_block(9, |_| {}).unwrap();
1198        assert!(timeline.is_empty());
1199        timeline.restore_checkpoint(checkpoint).unwrap();
1200        assert_eq!(timeline.snapshot(), before);
1201        assert_eq!(
1202            timeline
1203                .enqueue(TimedPlayerControl::new(12, 6, control(-0.25)))
1204                .unwrap_err(),
1205            ControlTimelinePushError::Full { capacity: 3 }
1206        );
1207
1208        let mut sequences = Vec::new();
1209        timeline
1210            .visit_block(9, |item| {
1211                if let ControlBlockItem::Transition { event, .. } = item {
1212                    sequences.push(event.sequence);
1213                }
1214            })
1215            .unwrap();
1216        assert_eq!(sequences, [3, 4, 5]);
1217    }
1218
1219    #[test]
1220    fn checkpoint_rejects_enqueue_that_overwrites_consumed_slot() {
1221        let mut timeline = PlayerControlTimeline::new(2, 0, control(0.0)).unwrap();
1222        timeline
1223            .enqueue(TimedPlayerControl::new(1, 1, control(1.0)))
1224            .unwrap();
1225        timeline
1226            .enqueue(TimedPlayerControl::new(10, 2, control(-1.0)))
1227            .unwrap();
1228        let checkpoint = timeline.checkpoint();
1229
1230        timeline.visit_block(2, |_| {}).unwrap();
1231        timeline
1232            .enqueue(TimedPlayerControl::new(20, 3, control(0.5)))
1233            .unwrap();
1234        let before_failed_restore = timeline.snapshot();
1235
1236        assert_eq!(
1237            timeline.restore_checkpoint(checkpoint),
1238            Err(ControlTimelineCheckpointRestoreError::SlotsChanged)
1239        );
1240        assert_eq!(timeline.snapshot(), before_failed_restore);
1241    }
1242
1243    #[test]
1244    fn checkpoint_rejects_another_timeline() {
1245        let first = PlayerControlTimeline::new(2, 0, control(0.0)).unwrap();
1246        let checkpoint = first.checkpoint();
1247        let mut second = PlayerControlTimeline::new(2, 0, control(0.0)).unwrap();
1248
1249        assert_eq!(
1250            second.restore_checkpoint(checkpoint),
1251            Err(ControlTimelineCheckpointRestoreError::TimelineMismatch)
1252        );
1253    }
1254
1255    #[test]
1256    fn snapshot_restore_invalidates_checkpoint() {
1257        let mut timeline = PlayerControlTimeline::new(2, 0, control(0.0)).unwrap();
1258        timeline
1259            .enqueue(TimedPlayerControl::new(1, 1, control(1.0)))
1260            .unwrap();
1261        let checkpoint = timeline.checkpoint();
1262        let snapshot = timeline.snapshot();
1263        timeline.restore(&snapshot).unwrap();
1264
1265        assert_eq!(
1266            timeline.restore_checkpoint(checkpoint),
1267            Err(ControlTimelineCheckpointRestoreError::SlotsChanged)
1268        );
1269    }
1270
1271    #[test]
1272    fn restore_rejects_capacity_change_without_mutation() {
1273        let source = PlayerControlTimeline::new(3, 0, control(0.0)).unwrap();
1274        let snapshot = source.snapshot();
1275        let mut destination = PlayerControlTimeline::new(2, 50, control(1.0)).unwrap();
1276        let before = destination.snapshot();
1277
1278        assert_eq!(
1279            destination.restore(&snapshot).unwrap_err(),
1280            ControlTimelineRestoreError::CapacityMismatch {
1281                expected: 2,
1282                actual: 3,
1283            }
1284        );
1285        assert_eq!(destination.snapshot(), before);
1286    }
1287
1288    #[test]
1289    fn block_advance_reports_frame_overflow_without_mutation() {
1290        let mut timeline = PlayerControlTimeline::new(2, u64::MAX - 1, control(0.0)).unwrap();
1291        assert_eq!(
1292            timeline.visit_block(2, |_| {}).unwrap_err(),
1293            ControlTimelineAdvanceError::FrameOverflow
1294        );
1295        assert_eq!(timeline.render_frame(), u64::MAX - 1);
1296    }
1297}