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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub enum FullCapacityPolicy {
15 RejectIncoming,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub enum LateEventPolicy {
22 Reject,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub enum DuplicateEventPolicy {
29 Reject,
30}
31
32#[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#[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 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 Span {
146 frame_offset: u32,
147 frame_count: u32,
148 control: PlayerControl,
149 },
150 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#[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#[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 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 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 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 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 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 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 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 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}