Skip to main content

ponko/net/
snapshots.rs

1//! Snapshot interpolation for remote entities: buffer timestamped states and
2//! show the world a little in the past, so a steady picture can be drawn from
3//! packets that arrive unevenly, out of order or not at all. The helpers
4//! never touch the wire: you decode your own packets and call `push`.
5
6use glam::{Quat, Vec2, Vec3};
7
8/// A state that can be blended between two snapshots and predicted past the
9/// last one. Implemented for `f32`, `Vec2`, `Vec3`, `Quat` and [`Pose`];
10/// implement it for your own state to use [`SnapshotBuffer`] with it.
11///
12/// ```
13/// use ponko::{Interpolate, SnapshotBuffer};
14///
15/// #[derive(Clone, Copy, Debug, PartialEq)]
16/// struct Health(f32);
17/// impl Interpolate for Health {
18///     fn interpolate(a: &Self, b: &Self, _span: f32, t: f32) -> Self {
19///         Health(a.0 + (b.0 - a.0) * t)
20///     }
21///     fn extrapolate(_prev: &Self, last: &Self, _span: f32, _ahead: f32) -> Self {
22///         *last // don't predict
23///     }
24/// }
25/// let mut buffer = SnapshotBuffer::new();
26/// buffer.push(0.0, Health(0.0));
27/// buffer.push(1.0, Health(10.0));
28/// assert_eq!(buffer.sample_at(0.5).unwrap().value, Health(5.0));
29/// ```
30pub trait Interpolate: Copy {
31    /// The state `t` (0..=1) of the way from `a` to `b`, which were `span`
32    /// seconds apart.
33    fn interpolate(a: &Self, b: &Self, span: f32, t: f32) -> Self;
34    /// The state `ahead` seconds after `last`. `prev` came `span` seconds
35    /// before `last` (`span` is 0 when there was no earlier snapshot).
36    fn extrapolate(prev: &Self, last: &Self, span: f32, ahead: f32) -> Self;
37}
38
39impl Interpolate for f32 {
40    fn interpolate(a: &Self, b: &Self, _span: f32, t: f32) -> Self {
41        a + (b - a) * t
42    }
43    fn extrapolate(prev: &Self, last: &Self, span: f32, ahead: f32) -> Self {
44        if span > 0.0 {
45            last + (last - prev) / span * ahead
46        } else {
47            *last
48        }
49    }
50}
51impl Interpolate for Vec2 {
52    fn interpolate(a: &Self, b: &Self, _span: f32, t: f32) -> Self {
53        a.lerp(*b, t)
54    }
55    fn extrapolate(prev: &Self, last: &Self, span: f32, ahead: f32) -> Self {
56        if span > 0.0 {
57            *last + (*last - *prev) / span * ahead
58        } else {
59            *last
60        }
61    }
62}
63impl Interpolate for Vec3 {
64    fn interpolate(a: &Self, b: &Self, _span: f32, t: f32) -> Self {
65        a.lerp(*b, t)
66    }
67    fn extrapolate(prev: &Self, last: &Self, span: f32, ahead: f32) -> Self {
68        if span > 0.0 {
69            *last + (*last - *prev) / span * ahead
70        } else {
71            *last
72        }
73    }
74}
75impl Interpolate for Quat {
76    fn interpolate(a: &Self, b: &Self, _span: f32, t: f32) -> Self {
77        a.slerp(*b, t).normalize()
78    }
79    /// Rotations are held, not predicted.
80    fn extrapolate(_prev: &Self, last: &Self, _span: f32, _ahead: f32) -> Self {
81        *last
82    }
83}
84
85/// A position and rotation, optionally with the velocity the owner was moving
86/// at. Interpolates position linearly, or along a cubic Hermite curve when
87/// both neighbouring snapshots carry a velocity (smoother through curves and
88/// at low send rates), and rotation with slerp. Extrapolation moves the
89/// position on at `velocity`, or at the speed seen between the last two
90/// snapshots when there is none; rotation is held.
91///
92/// ```
93/// use ponko::{Interpolate, Pose, vec3};
94/// let a = Pose::at(vec3(0.0, 0.0, 0.0));
95/// let b = Pose::at(vec3(10.0, 0.0, 0.0));
96/// let mid = Pose::interpolate(&a, &b, 1.0, 0.5);
97/// assert_eq!(mid.position, vec3(5.0, 0.0, 0.0));
98/// ```
99#[derive(Clone, Copy, Debug, PartialEq)]
100pub struct Pose {
101    /// Where, in world units.
102    pub position: Vec3,
103    /// Which way it faces.
104    pub rotation: Quat,
105    /// Units per second, if the sender knows it.
106    pub velocity: Option<Vec3>,
107}
108impl Default for Pose {
109    fn default() -> Self {
110        Self::at(Vec3::ZERO)
111    }
112}
113impl Pose {
114    /// At `position`, not rotated, without a velocity.
115    /// ```
116    /// let p = ponko::Pose::at(ponko::vec3(1.0, 2.0, 3.0));
117    /// assert!(p.velocity.is_none());
118    /// ```
119    pub const fn at(position: Vec3) -> Self {
120        Self {
121            position,
122            rotation: Quat::IDENTITY,
123            velocity: None,
124        }
125    }
126
127    /// The same pose facing `rotation`.
128    /// ```
129    /// use ponko::*;
130    /// let p = Pose::at(Vec3::ZERO).facing(Quat::from_rotation_y(1.0));
131    /// assert_ne!(p.rotation, Quat::IDENTITY);
132    /// ```
133    #[must_use]
134    pub const fn facing(self, rotation: Quat) -> Self {
135        Self { rotation, ..self }
136    }
137
138    /// The same pose moving at `velocity`.
139    /// ```
140    /// use ponko::*;
141    /// let p = Pose::at(Vec3::ZERO).moving(vec3(0.0, 0.0, 5.0));
142    /// assert_eq!(p.velocity, Some(vec3(0.0, 0.0, 5.0)));
143    /// ```
144    #[must_use]
145    pub const fn moving(self, velocity: Vec3) -> Self {
146        Self {
147            velocity: Some(velocity),
148            ..self
149        }
150    }
151}
152impl Interpolate for Pose {
153    fn interpolate(a: &Self, b: &Self, span: f32, t: f32) -> Self {
154        let position = match (a.velocity, b.velocity) {
155            (Some(va), Some(vb)) if span > 0.0 => {
156                // Cubic Hermite: tangents are velocity × the time between.
157                let (t2, t3) = (t * t, t * t * t);
158                let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
159                let h10 = t3 - 2.0 * t2 + t;
160                let h01 = -2.0 * t3 + 3.0 * t2;
161                let h11 = t3 - t2;
162                a.position * h00 + va * (span * h10) + b.position * h01 + vb * (span * h11)
163            }
164            _ => a.position.lerp(b.position, t),
165        };
166        Self {
167            position,
168            rotation: a.rotation.slerp(b.rotation, t).normalize(),
169            velocity: match (a.velocity, b.velocity) {
170                (Some(va), Some(vb)) => Some(va.lerp(vb, t)),
171                _ => None,
172            },
173        }
174    }
175
176    fn extrapolate(prev: &Self, last: &Self, span: f32, ahead: f32) -> Self {
177        let velocity = last.velocity.or_else(|| {
178            // No velocity from the sender: use the speed between snapshots.
179            (span > 0.0).then(|| (last.position - prev.position) / span)
180        });
181        Self {
182            position: last.position + velocity.unwrap_or(Vec3::ZERO) * ahead,
183            ..*last
184        }
185    }
186}
187
188/// How a [`SnapshotSample`] was produced.
189/// ```
190/// assert_ne!(ponko::SnapshotState::Interpolated, ponko::SnapshotState::Held);
191/// ```
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub enum SnapshotState {
194    /// Between two snapshots: the normal case.
195    Interpolated,
196    /// Past the newest snapshot (packets are late), predicted from it for at
197    /// most [`SnapshotConfig::max_extrapolation`].
198    Extrapolated,
199    /// Standing still: before the oldest snapshot, or past the extrapolation
200    /// limit (at where the prediction ended).
201    Held,
202}
203
204/// A state read from a [`SnapshotBuffer`] or [`SnapshotStream`].
205/// ```
206/// use ponko::*;
207/// let mut b = SnapshotBuffer::new();
208/// b.push(1.0, 5.0f32);
209/// let s = b.sample_at(0.0).unwrap();
210/// assert_eq!((s.value, s.state), (5.0, SnapshotState::Held));
211/// ```
212#[derive(Clone, Copy, Debug, PartialEq)]
213pub struct SnapshotSample<T> {
214    /// The state to show.
215    pub value: T,
216    /// How it was made.
217    pub state: SnapshotState,
218}
219
220/// Tuning for [`SnapshotBuffer`] and [`SnapshotStream`].
221/// ```
222/// let c = ponko::SnapshotConfig::default();
223/// assert_eq!((c.delay, c.max_extrapolation, c.capacity), (0.1, 0.25, 64));
224/// ```
225#[derive(Clone, Copy, Debug, PartialEq)]
226pub struct SnapshotConfig {
227    /// How far in the past remote entities are shown, in seconds. It must
228    /// cover the gap between packets plus their jitter, or the buffer runs
229    /// dry and entities are extrapolated. Default 0.1.
230    pub delay: f64,
231    /// How long past the newest snapshot to predict before holding still, in
232    /// seconds. Default 0.25.
233    pub max_extrapolation: f64,
234    /// Snapshots kept (the oldest is dropped first). Make it at least twice
235    /// `delay` × the send rate. Default 64.
236    pub capacity: usize,
237}
238impl Default for SnapshotConfig {
239    fn default() -> Self {
240        Self {
241            delay: 0.1,
242            max_extrapolation: 0.25,
243            capacity: 64,
244        }
245    }
246}
247impl SnapshotConfig {
248    /// Checks the settings: `capacity` at least 2, and the times finite and
249    /// not negative. The constructors panic with this message.
250    /// ```
251    /// use ponko::SnapshotConfig;
252    /// assert!(SnapshotConfig::default().validate().is_ok());
253    /// assert!(SnapshotConfig { capacity: 1, ..Default::default() }.validate().is_err());
254    /// ```
255    pub fn validate(&self) -> Result<(), &'static str> {
256        if self.capacity < 2 {
257            return Err("SnapshotConfig: capacity must be at least 2");
258        }
259        if !(self.delay.is_finite() && self.delay >= 0.0) {
260            return Err("SnapshotConfig: delay must be a finite number of seconds, 0 or more");
261        }
262        if !(self.max_extrapolation.is_finite() && self.max_extrapolation >= 0.0) {
263            return Err("SnapshotConfig: max_extrapolation must be a finite number of seconds, 0 or more");
264        }
265        Ok(())
266    }
267}
268
269/// What happened to a snapshot given to [`SnapshotBuffer::push`].
270/// ```
271/// assert_ne!(ponko::SnapshotPush::Added, ponko::SnapshotPush::Duplicate);
272/// ```
273#[derive(Clone, Copy, Debug, PartialEq, Eq)]
274pub enum SnapshotPush {
275    /// Stored (in time order, even if it arrived late).
276    Added,
277    /// A snapshot with this timestamp is already there: ignored.
278    Duplicate,
279    /// Older than everything kept in a full buffer: ignored.
280    TooOld,
281    /// The timestamp wasn't a finite number: ignored.
282    Invalid,
283}
284
285/// Timestamped states of one entity, in time order, read back at any time:
286/// interpolated between two snapshots, predicted for a bounded time past the
287/// newest, then held. Times are the sender's, in seconds on any timeline you
288/// like (for a remote entity use [`SnapshotStream`], which also converts
289/// your local clock).
290///
291/// Snapshots may arrive in any order and more than once.
292///
293/// ```
294/// use ponko::*;
295/// let mut buffer = SnapshotBuffer::new();
296/// buffer.push(1.0, vec3(10.0, 0.0, 0.0));
297/// buffer.push(0.0, vec3(0.0, 0.0, 0.0)); // arrived late: still placed first
298/// assert_eq!(buffer.push(1.0, vec3(99.0, 0.0, 0.0)), SnapshotPush::Duplicate);
299///
300/// let s = buffer.sample_at(0.25).unwrap();
301/// assert_eq!(s.value, vec3(2.5, 0.0, 0.0));
302/// assert_eq!(s.state, SnapshotState::Interpolated);
303/// // Packets stop: predict for up to 0.25 s, then hold.
304/// assert_eq!(buffer.sample_at(1.1).unwrap().state, SnapshotState::Extrapolated);
305/// let held = buffer.sample_at(5.0).unwrap();
306/// assert_eq!(held.state, SnapshotState::Held);
307/// assert_eq!(held.value, vec3(12.5, 0.0, 0.0));
308/// ```
309#[derive(Clone, Debug)]
310pub struct SnapshotBuffer<T: Interpolate> {
311    config: SnapshotConfig,
312    samples: Vec<(f64, T)>,
313}
314impl<T: Interpolate> Default for SnapshotBuffer<T> {
315    fn default() -> Self {
316        Self::new()
317    }
318}
319impl<T: Interpolate> SnapshotBuffer<T> {
320    /// An empty buffer with the default [`SnapshotConfig`].
321    /// ```
322    /// let b = ponko::SnapshotBuffer::<f32>::new();
323    /// assert!(b.is_empty());
324    /// ```
325    pub fn new() -> Self {
326        Self::with_config(SnapshotConfig::default())
327    }
328
329    /// An empty buffer. Panics if `capacity` is below 2 or a time setting is
330    /// negative or not finite.
331    /// ```
332    /// use ponko::*;
333    /// let b = SnapshotBuffer::<f32>::with_config(SnapshotConfig { delay: 0.2, ..Default::default() });
334    /// assert_eq!(b.config().delay, 0.2);
335    /// ```
336    pub fn with_config(config: SnapshotConfig) -> Self {
337        if let Err(message) = config.validate() {
338            panic!("{message}");
339        }
340        Self {
341            config,
342            samples: Vec::with_capacity(config.capacity),
343        }
344    }
345
346    /// The settings it was made with.
347    /// ```
348    /// assert_eq!(ponko::SnapshotBuffer::<f32>::new().config().delay, 0.1);
349    /// ```
350    pub fn config(&self) -> SnapshotConfig {
351        self.config
352    }
353
354    /// Stores `value`, stamped `time`. Late snapshots go in at their place in
355    /// time; one with the same timestamp as a stored snapshot is ignored.
356    /// When the buffer is full the oldest snapshot is dropped.
357    /// ```
358    /// use ponko::*;
359    /// let mut b = SnapshotBuffer::new();
360    /// assert_eq!(b.push(1.0, 1.0f32), SnapshotPush::Added);
361    /// assert_eq!(b.push(1.0, 1.0f32), SnapshotPush::Duplicate);
362    /// assert_eq!(b.push(f64::NAN, 1.0f32), SnapshotPush::Invalid);
363    /// ```
364    pub fn push(&mut self, time: f64, value: T) -> SnapshotPush {
365        if !time.is_finite() {
366            return SnapshotPush::Invalid;
367        }
368        let at = self.samples.partition_point(|s| s.0 < time);
369        if self.samples.get(at).is_some_and(|s| s.0 == time) {
370            return SnapshotPush::Duplicate;
371        }
372        if self.samples.len() == self.config.capacity {
373            if at == 0 {
374                return SnapshotPush::TooOld;
375            }
376            self.samples.remove(0);
377            self.samples.insert(at - 1, (time, value));
378        } else {
379            self.samples.insert(at, (time, value));
380        }
381        SnapshotPush::Added
382    }
383
384    /// The state at `time` (the sender's timeline): interpolated between the
385    /// snapshots around it; past the newest, predicted up to
386    /// [`max_extrapolation`](SnapshotConfig::max_extrapolation) and then held
387    /// where the prediction ended; before the oldest, the oldest. `None`
388    /// while the buffer is empty. Allocation free.
389    /// ```
390    /// use ponko::*;
391    /// let mut b = SnapshotBuffer::new();
392    /// assert!(b.sample_at(0.0).is_none());
393    /// b.push(0.0, 0.0f32);
394    /// b.push(1.0, 4.0);
395    /// assert_eq!(b.sample_at(0.5).unwrap().value, 2.0);
396    /// ```
397    pub fn sample_at(&self, time: f64) -> Option<SnapshotSample<T>> {
398        let first = self.samples.first()?;
399        let last = self.samples.last()?;
400        if time <= first.0 {
401            return Some(SnapshotSample {
402                value: first.1,
403                state: SnapshotState::Held,
404            });
405        }
406        if time >= last.0 {
407            let (prev, span) = match self.samples.len().checked_sub(2).map(|i| &self.samples[i]) {
408                Some(p) => (&p.1, (last.0 - p.0) as f32),
409                None => (&last.1, 0.0),
410            };
411            let ahead = time - last.0;
412            return Some(if ahead == 0.0 {
413                SnapshotSample {
414                    value: last.1,
415                    state: SnapshotState::Interpolated,
416                }
417            } else if ahead <= self.config.max_extrapolation {
418                SnapshotSample {
419                    value: T::extrapolate(prev, &last.1, span, ahead as f32),
420                    state: SnapshotState::Extrapolated,
421                }
422            } else {
423                SnapshotSample {
424                    value: T::extrapolate(prev, &last.1, span, self.config.max_extrapolation as f32),
425                    state: SnapshotState::Held,
426                }
427            });
428        }
429        let i = self.samples.partition_point(|s| s.0 <= time) - 1;
430        let (a, b) = (&self.samples[i], &self.samples[i + 1]);
431        let span = b.0 - a.0;
432        Some(SnapshotSample {
433            value: T::interpolate(&a.1, &b.1, span as f32, ((time - a.0) / span) as f32),
434            state: SnapshotState::Interpolated,
435        })
436    }
437
438    /// The state to show at `now`, [`delay`](SnapshotConfig::delay) seconds
439    /// in the past; for buffers filled with times from the clock `now` is on.
440    /// ```
441    /// use ponko::*;
442    /// let mut b = SnapshotBuffer::new();
443    /// b.push(0.0, 0.0f32);
444    /// b.push(1.0, 10.0);
445    /// assert_eq!(b.sample(0.6).unwrap().value, 5.0); // 0.6 - 0.1 s delay
446    /// ```
447    pub fn sample(&self, now: f64) -> Option<SnapshotSample<T>> {
448        self.sample_at(now - self.config.delay)
449    }
450
451    /// Timestamp of the newest snapshot.
452    /// ```
453    /// let mut b = ponko::SnapshotBuffer::new();
454    /// b.push(2.0, 0.0f32);
455    /// assert_eq!(b.latest_time(), Some(2.0));
456    /// ```
457    pub fn latest_time(&self) -> Option<f64> {
458        self.samples.last().map(|s| s.0)
459    }
460
461    /// Timestamp of the oldest snapshot kept.
462    /// ```
463    /// let mut b = ponko::SnapshotBuffer::new();
464    /// b.push(2.0, 0.0f32);
465    /// assert_eq!(b.oldest_time(), Some(2.0));
466    /// ```
467    pub fn oldest_time(&self) -> Option<f64> {
468        self.samples.first().map(|s| s.0)
469    }
470
471    /// Number of snapshots kept.
472    /// ```
473    /// assert_eq!(ponko::SnapshotBuffer::<f32>::new().len(), 0);
474    /// ```
475    pub fn len(&self) -> usize {
476        self.samples.len()
477    }
478
479    /// Whether no snapshot is kept.
480    /// ```
481    /// assert!(ponko::SnapshotBuffer::<f32>::new().is_empty());
482    /// ```
483    pub fn is_empty(&self) -> bool {
484        self.samples.is_empty()
485    }
486
487    /// Drops every snapshot. Do this on a teleport, a respawn or a restart
488    /// of the sender's clock, so the entity jumps instead of sliding across.
489    /// ```
490    /// let mut b = ponko::SnapshotBuffer::new();
491    /// b.push(0.0, 0.0f32);
492    /// b.clear();
493    /// assert!(b.is_empty());
494    /// ```
495    pub fn clear(&mut self) {
496        self.samples.clear();
497    }
498
499    /// Drops snapshots that can no longer be shown at `time` or later: all
500    /// but the newest one at or before it.
501    /// ```
502    /// let mut b = ponko::SnapshotBuffer::new();
503    /// for i in 0..5 { b.push(i as f64, 0.0f32); }
504    /// b.trim_before(3.5);
505    /// assert_eq!(b.oldest_time(), Some(3.0));
506    /// ```
507    pub fn trim_before(&mut self, time: f64) {
508        let keep_from = self.samples.partition_point(|s| s.0 <= time).saturating_sub(1);
509        self.samples.drain(..keep_from);
510    }
511}
512
513const CLOCK_WINDOW: usize = 32;
514/// Consecutive samples that disagree with the estimate before the clock is
515/// considered to have jumped and the estimate starts over.
516const CLOCK_JUMP_CONFIRM: u32 = 8;
517
518/// Maps a sender's clock to ours. Every packet carries the sender's time `s`
519/// and arrives at our time `r`; `r - s` is the clock offset plus the trip
520/// time. The trip time only ever adds, so the estimate follows the lower end
521/// of the recent samples (the fastest packets), which makes it robust against
522/// jitter and late packets, then smooths that to avoid steps. Needs no
523/// round trips, and works one way.
524///
525/// The result is a stable mapping, not true time: it includes the shortest
526/// trip time, which is what interpolation wants anyway.
527///
528/// ```
529/// use ponko::ClockSync;
530/// let mut clock = ClockSync::new();
531/// // The sender's clock runs 1000 s ahead of ours; packets take 30–80 ms.
532/// for i in 0..50 {
533///     let sent = 1000.0 + i as f64 * 0.05;
534///     let trip = 0.03 + (i % 5) as f64 * 0.0125;
535///     clock.observe(sent, sent - 1000.0 + trip);
536/// }
537/// let offset = clock.offset().unwrap();
538/// assert!((offset - -1000.0 - 0.03).abs() < 0.005, "{offset}");
539/// assert!((clock.to_local(1010.0).unwrap() - 10.03).abs() < 0.005);
540/// ```
541#[derive(Clone, Debug)]
542pub struct ClockSync {
543    window: [f64; CLOCK_WINDOW],
544    filled: usize,
545    next: usize,
546    offset: Option<f64>,
547    off_streak: u32,
548    rise: f64,
549    fall: f64,
550    jump: f64,
551}
552impl Default for ClockSync {
553    fn default() -> Self {
554        Self::new()
555    }
556}
557impl ClockSync {
558    /// A clock with no samples yet.
559    /// ```
560    /// assert!(ponko::ClockSync::new().offset().is_none());
561    /// ```
562    pub fn new() -> Self {
563        Self {
564            window: [0.0; CLOCK_WINDOW],
565            filled: 0,
566            next: 0,
567            offset: None,
568            off_streak: 0,
569            rise: 0.05,
570            fall: 0.25,
571            jump: 1.0,
572        }
573    }
574
575    /// Records a packet stamped `remote_time` (sender's clock, seconds) that
576    /// arrived at `local_now` (our clock, seconds). Call it for every packet,
577    /// including late and duplicate ones. Ignores non-finite numbers.
578    /// ```
579    /// let mut c = ponko::ClockSync::new();
580    /// c.observe(5.0, 105.0);
581    /// assert_eq!(c.offset(), Some(100.0));
582    /// ```
583    pub fn observe(&mut self, remote_time: f64, local_now: f64) {
584        let sample = local_now - remote_time;
585        if !sample.is_finite() {
586            return;
587        }
588        if let Some(estimate) = self.offset {
589            if (sample - estimate).abs() > self.jump {
590                self.off_streak += 1;
591                if self.off_streak >= CLOCK_JUMP_CONFIRM {
592                    // The sender's clock (or ours) jumped, e.g. a restart.
593                    self.filled = 0;
594                    self.next = 0;
595                    self.offset = None;
596                    self.off_streak = 0;
597                }
598            } else {
599                self.off_streak = 0;
600            }
601        }
602        self.window[self.next] = sample;
603        self.next = (self.next + 1) % CLOCK_WINDOW;
604        self.filled = (self.filled + 1).min(CLOCK_WINDOW);
605
606        // The 4th smallest of 32 (the smallest while there are few): a few
607        // freak low samples can't drag the estimate down.
608        let mut sorted = self.window;
609        let used = &mut sorted[..self.filled];
610        used.sort_unstable_by(f64::total_cmp);
611        let target = used[(self.filled - 1) / 8];
612        self.offset = Some(match self.offset {
613            None => target,
614            Some(o) => {
615                let rate = if target < o { self.fall } else { self.rise };
616                o + (target - o) * rate
617            }
618        });
619    }
620
621    /// Local time minus remote time, once at least one packet was observed.
622    /// ```
623    /// assert!(ponko::ClockSync::new().offset().is_none());
624    /// ```
625    pub fn offset(&self) -> Option<f64> {
626        self.offset
627    }
628
629    /// The local time of a sender timestamp.
630    /// ```
631    /// let mut c = ponko::ClockSync::new();
632    /// c.observe(0.0, 50.0);
633    /// assert_eq!(c.to_local(2.0), Some(52.0));
634    /// ```
635    pub fn to_local(&self, remote_time: f64) -> Option<f64> {
636        self.offset.map(|o| remote_time + o)
637    }
638
639    /// The sender's time at a local time.
640    /// ```
641    /// let mut c = ponko::ClockSync::new();
642    /// c.observe(0.0, 50.0);
643    /// assert_eq!(c.to_remote(52.0), Some(2.0));
644    /// ```
645    pub fn to_remote(&self, local_time: f64) -> Option<f64> {
646        self.offset.map(|o| local_time - o)
647    }
648
649    /// Packets in the estimate (at most 32).
650    /// ```
651    /// assert_eq!(ponko::ClockSync::new().samples(), 0);
652    /// ```
653    pub fn samples(&self) -> usize {
654        self.filled
655    }
656
657    /// Forgets everything, for a new connection.
658    /// ```
659    /// let mut c = ponko::ClockSync::new();
660    /// c.observe(0.0, 1.0);
661    /// c.reset();
662    /// assert!(c.offset().is_none());
663    /// ```
664    pub fn reset(&mut self) {
665        *self = Self::new();
666    }
667}
668
669/// A [`SnapshotBuffer`] plus a [`ClockSync`]: what you need per remote
670/// entity. Push each decoded snapshot with the sender's timestamp and our
671/// clock; sample with our clock, once a frame.
672///
673/// The clock is yours (`Ctx::time`, an `Instant`, ...) but must be the same
674/// for `push` and `sample`. Entities of one sender share its clock, so they
675/// all see the same offset estimate; each stream still keeps its own, which
676/// is cheap.
677///
678/// ```
679/// use ponko::*;
680/// let mut stream = SnapshotStream::new();
681/// // The sender stamps 20 snapshots per second with its own clock (which is
682/// // 500 s ahead); each takes 40 ms to arrive.
683/// for i in 0..10 {
684///     let sent = 500.0 + i as f64 * 0.05;
685///     let arrived = i as f64 * 0.05 + 0.04;
686///     stream.push(sent, arrived, Pose::at(vec3(i as f32, 0.0, 0.0)));
687/// }
688/// // Shown 100 ms in the past: x is 6.0 at local time 0.34 + 0.1 - 0.04.
689/// let s = stream.sample(0.4 + 0.04).unwrap();
690/// assert_eq!(s.state, SnapshotState::Interpolated);
691/// assert!((s.value.position.x - 6.0).abs() < 0.01);
692/// ```
693#[derive(Clone, Debug)]
694pub struct SnapshotStream<T: Interpolate> {
695    clock: ClockSync,
696    buffer: SnapshotBuffer<T>,
697}
698impl<T: Interpolate> Default for SnapshotStream<T> {
699    fn default() -> Self {
700        Self::new()
701    }
702}
703impl<T: Interpolate> SnapshotStream<T> {
704    /// A stream with the default [`SnapshotConfig`].
705    /// ```
706    /// assert!(ponko::SnapshotStream::<f32>::new().sample(0.0).is_none());
707    /// ```
708    pub fn new() -> Self {
709        Self::with_config(SnapshotConfig::default())
710    }
711
712    /// A stream with these settings (panics like [`SnapshotBuffer::with_config`]).
713    /// ```
714    /// use ponko::*;
715    /// let s = SnapshotStream::<f32>::with_config(SnapshotConfig { delay: 0.05, ..Default::default() });
716    /// assert_eq!(s.buffer().config().delay, 0.05);
717    /// ```
718    pub fn with_config(config: SnapshotConfig) -> Self {
719        Self {
720            clock: ClockSync::new(),
721            buffer: SnapshotBuffer::with_config(config),
722        }
723    }
724
725    /// Stores a snapshot stamped `remote_time` by the sender that arrived at
726    /// our time `local_now`, and feeds the pair to the clock estimate.
727    /// ```
728    /// use ponko::*;
729    /// let mut s = SnapshotStream::new();
730    /// assert_eq!(s.push(10.0, 0.5, 1.0f32), SnapshotPush::Added);
731    /// assert_eq!(s.push(10.0, 0.6, 1.0f32), SnapshotPush::Duplicate);
732    /// ```
733    pub fn push(&mut self, remote_time: f64, local_now: f64, value: T) -> SnapshotPush {
734        let result = self.buffer.push(remote_time, value);
735        if matches!(result, SnapshotPush::Added | SnapshotPush::TooOld) {
736            self.clock.observe(remote_time, local_now);
737        }
738        result
739    }
740
741    /// The state to show at our time `now`: the sender's time at
742    /// `now - delay`, read from the buffer. `None` until the first snapshot.
743    /// ```
744    /// use ponko::*;
745    /// let mut s = SnapshotStream::new();
746    /// s.push(0.0, 0.0, 0.0f32);
747    /// s.push(1.0, 1.0, 10.0);
748    /// assert_eq!(s.sample(0.6).unwrap().value, 5.0);
749    /// ```
750    pub fn sample(&self, now: f64) -> Option<SnapshotSample<T>> {
751        let remote = self.clock.to_remote(now - self.buffer.config.delay)?;
752        self.buffer.sample_at(remote)
753    }
754
755    /// Sets how far in the past to show the entity; for example when
756    /// the link gets worse. Takes effect at once (the picture jumps).
757    /// Panics if negative or not finite.
758    /// ```
759    /// let mut s = ponko::SnapshotStream::<f32>::new();
760    /// s.set_delay(0.2);
761    /// assert_eq!(s.buffer().config().delay, 0.2);
762    /// ```
763    pub fn set_delay(&mut self, delay: f64) {
764        assert!(
765            delay.is_finite() && delay >= 0.0,
766            "SnapshotStream::set_delay: delay must be a finite number of seconds, 0 or more"
767        );
768        self.buffer.config.delay = delay;
769    }
770
771    /// The clock estimate.
772    /// ```
773    /// assert!(ponko::SnapshotStream::<f32>::new().clock().offset().is_none());
774    /// ```
775    pub fn clock(&self) -> &ClockSync {
776        &self.clock
777    }
778
779    /// The stored snapshots, in the sender's time.
780    /// ```
781    /// assert!(ponko::SnapshotStream::<f32>::new().buffer().is_empty());
782    /// ```
783    pub fn buffer(&self) -> &SnapshotBuffer<T> {
784        &self.buffer
785    }
786
787    /// Our time of the newest snapshot, if any.
788    /// ```
789    /// let mut s = ponko::SnapshotStream::new();
790    /// s.push(10.0, 0.5, 1.0f32);
791    /// assert_eq!(s.latest_local_time(), Some(0.5));
792    /// ```
793    pub fn latest_local_time(&self) -> Option<f64> {
794        self.clock.to_local(self.buffer.latest_time()?)
795    }
796
797    /// Forgets all snapshots, keeping the clock estimate: a teleport or respawn.
798    /// ```
799    /// let mut s = ponko::SnapshotStream::new();
800    /// s.push(10.0, 0.5, 1.0f32);
801    /// s.clear();
802    /// assert!(s.sample(1.0).is_none());
803    /// ```
804    pub fn clear(&mut self) {
805        self.buffer.clear();
806    }
807
808    /// Forgets snapshots and the clock estimate: a new connection.
809    /// ```
810    /// let mut s = ponko::SnapshotStream::new();
811    /// s.push(10.0, 0.5, 1.0f32);
812    /// s.reset();
813    /// assert!(s.clock().offset().is_none());
814    /// ```
815    pub fn reset(&mut self) {
816        self.buffer.clear();
817        self.clock.reset();
818    }
819}
820
821/// Component: a remote entity's incoming [`Pose`] snapshots, applied to its
822/// [`Transform3D`](crate::Transform3D) by [`apply_net_poses`]. Push each
823/// decoded update from your message handler with `ctx.time()` as the
824/// arrival time.
825///
826/// ```
827/// use ponko::*;
828/// let world = Game::new("remote", 1, 1)
829///     .system(apply_net_poses)
830///     .run_headless(60, |ctx| {
831///         let mut pose = NetPose::default();
832///         pose.stream.push(0.0, 0.0, Pose::at(vec3(0.0, 0.0, 0.0)));
833///         pose.stream.push(1.0, 1.0, Pose::at(vec3(100.0, 0.0, 0.0)));
834///         ctx.world.spawn((Transform3D::default(), pose));
835///     });
836/// # let _ = world;
837/// ```
838#[cfg(feature = "3d")]
839#[derive(Clone, Debug, Default)]
840pub struct NetPose {
841    /// The buffered snapshots and clock estimate.
842    pub stream: SnapshotStream<Pose>,
843}
844
845/// System: sets the [`Transform3D`](crate::Transform3D) position and rotation
846/// of every entity with a [`NetPose`] from its stream, sampled at
847/// `ctx.time()` (scale is left alone). Entities without snapshots stay
848/// where they are. Add it with [`Game::system`](crate::Game::system), so entities
849/// move every frame whatever the simulation does; the stream's timestamps are matched against
850/// `ctx.time()`, so push with that clock.
851///
852/// ```
853/// use ponko::*;
854/// let _ = Game::new("remote", 1, 1).system(apply_net_poses);
855/// ```
856#[cfg(feature = "3d")]
857pub fn apply_net_poses(ctx: &mut crate::Ctx) {
858    let now = ctx.time();
859    ctx.world
860        .query::<(&NetPose, &mut crate::Transform3D)>()
861        .for_each(|(pose, transform)| {
862            if let Some(sample) = pose.stream.sample(now) {
863                transform.position = sample.value.position;
864                transform.rotation = sample.value.rotation;
865            }
866        });
867}
868
869#[cfg(test)]
870mod tests {
871    use super::*;
872    use glam::vec3;
873
874    fn x(sample: Option<SnapshotSample<Vec3>>) -> f32 {
875        sample.unwrap().value.x
876    }
877
878    /// A sender walking along x at 10 units/s, one snapshot every 50 ms.
879    fn walk(t: f64) -> Vec3 {
880        vec3(t as f32 * 10.0, 0.0, 0.0)
881    }
882
883    #[test]
884    fn interpolates_linearly_between_snapshots() {
885        let mut b = SnapshotBuffer::new();
886        b.push(0.0, walk(0.0));
887        b.push(0.1, walk(0.1));
888        let s = b.sample_at(0.025).unwrap();
889        assert!((s.value.x - 0.25).abs() < 1e-5);
890        assert_eq!(s.state, SnapshotState::Interpolated);
891    }
892
893    #[test]
894    fn hermite_follows_a_curve_better_than_a_straight_line() {
895        // Quarter circle of radius 10 in two snapshots: the middle of the
896        // curve is at (7.07, 7.07); a straight line gives (5, 5).
897        let a = Pose::at(vec3(10.0, 0.0, 0.0)).moving(vec3(0.0, 10.0, 0.0));
898        let b = Pose::at(vec3(0.0, 10.0, 0.0)).moving(vec3(-10.0, 0.0, 0.0));
899        let span = std::f32::consts::FRAC_PI_2; // speed 10 over an arc of 5π
900        let hermite = Pose::interpolate(&a, &b, span * 1.0, 0.5).position;
901        let linear = Pose::interpolate(&Pose::at(a.position), &Pose::at(b.position), span, 0.5).position;
902        let truth = vec3(7.071, 7.071, 0.0);
903        assert!((hermite - truth).length() < (linear - truth).length() * 0.5);
904        // Endpoints are exact.
905        assert!((Pose::interpolate(&a, &b, span, 0.0).position - a.position).length() < 1e-5);
906        assert!((Pose::interpolate(&a, &b, span, 1.0).position - b.position).length() < 1e-5);
907    }
908
909    #[test]
910    fn hermite_needs_both_velocities() {
911        let a = Pose::at(vec3(0.0, 0.0, 0.0)).moving(vec3(100.0, 0.0, 0.0));
912        let b = Pose::at(vec3(10.0, 0.0, 0.0));
913        assert_eq!(Pose::interpolate(&a, &b, 1.0, 0.5).position, vec3(5.0, 0.0, 0.0));
914        assert_eq!(Pose::interpolate(&a, &b, 1.0, 0.5).velocity, None);
915    }
916
917    #[test]
918    fn rotation_takes_the_short_way_round() {
919        let a = Pose::at(Vec3::ZERO).facing(Quat::from_rotation_y(0.1));
920        let b = Pose::at(Vec3::ZERO).facing(Quat::from_rotation_y(std::f32::consts::TAU - 0.1));
921        let mid = Pose::interpolate(&a, &b, 1.0, 0.5).rotation;
922        // Halfway between +0.1 and -0.1 rad is 0, not π.
923        assert!(mid.angle_between(Quat::IDENTITY) < 1e-3);
924        assert!((mid.length() - 1.0).abs() < 1e-5);
925    }
926
927    #[test]
928    fn out_of_order_and_duplicate_snapshots_end_up_sorted() {
929        let mut b = SnapshotBuffer::new();
930        let order = [3, 1, 4, 0, 2, 3, 1];
931        let results: Vec<_> = order
932            .iter()
933            .map(|&i| b.push(i as f64 * 0.1, walk(i as f64 * 0.1)))
934            .collect();
935        assert_eq!(
936            results,
937            [
938                SnapshotPush::Added,
939                SnapshotPush::Added,
940                SnapshotPush::Added,
941                SnapshotPush::Added,
942                SnapshotPush::Added,
943                SnapshotPush::Duplicate,
944                SnapshotPush::Duplicate,
945            ]
946        );
947        assert_eq!(b.len(), 5);
948        for i in 0..40 {
949            let t = i as f64 * 0.01;
950            assert!((x(b.sample_at(t)) - walk(t).x).abs() < 1e-4, "t={t}");
951        }
952    }
953
954    #[test]
955    fn a_duplicate_does_not_overwrite_the_first_copy() {
956        let mut b = SnapshotBuffer::new();
957        b.push(1.0, 1.0f32);
958        b.push(1.0, 99.0);
959        assert_eq!(b.sample_at(1.0).unwrap().value, 1.0);
960    }
961
962    #[test]
963    fn extrapolates_for_a_bounded_time_then_holds_where_it_ended() {
964        let mut b = SnapshotBuffer::new();
965        b.push(0.0, walk(0.0));
966        b.push(0.1, walk(0.1));
967        let s = b.sample_at(0.2).unwrap(); // 0.1 s past the last snapshot
968        assert_eq!(s.state, SnapshotState::Extrapolated);
969        assert!((s.value.x - 2.0).abs() < 1e-4, "{}", s.value.x);
970        // Just inside the cap, still extrapolating.
971        let cap = b.sample_at(0.349).unwrap();
972        assert_eq!(cap.state, SnapshotState::Extrapolated);
973        assert!((cap.value.x - 3.49).abs() < 1e-4);
974        // Beyond it: held at the end of the prediction, never flying off.
975        for t in [0.36, 1.0, 100.0] {
976            let h = b.sample_at(t).unwrap();
977            assert_eq!(h.state, SnapshotState::Held, "t={t}");
978            assert!((h.value.x - 3.5).abs() < 1e-4, "t={t}");
979        }
980    }
981
982    #[test]
983    fn velocity_drives_extrapolation_when_given() {
984        let mut b = SnapshotBuffer::new();
985        b.push(0.0, Pose::at(vec3(1.0, 0.0, 0.0)).moving(vec3(0.0, 4.0, 0.0)));
986        let s = b.sample_at(0.1).unwrap();
987        assert_eq!(s.state, SnapshotState::Extrapolated);
988        assert!((s.value.position - vec3(1.0, 0.4, 0.0)).length() < 1e-5);
989        // A single snapshot without velocity stays put.
990        let mut b = SnapshotBuffer::new();
991        b.push(0.0, Pose::at(vec3(1.0, 0.0, 0.0)));
992        assert_eq!(b.sample_at(0.2).unwrap().value.position, vec3(1.0, 0.0, 0.0));
993    }
994
995    #[test]
996    fn zero_extrapolation_holds_at_the_last_snapshot() {
997        let mut b = SnapshotBuffer::with_config(SnapshotConfig {
998            max_extrapolation: 0.0,
999            ..Default::default()
1000        });
1001        b.push(0.0, walk(0.0));
1002        b.push(0.1, walk(0.1));
1003        let s = b.sample_at(0.15).unwrap();
1004        assert_eq!(s.state, SnapshotState::Held);
1005        assert!((s.value.x - 1.0).abs() < 1e-5);
1006    }
1007
1008    #[test]
1009    fn before_the_first_snapshot_the_first_is_held() {
1010        let mut b = SnapshotBuffer::new();
1011        b.push(5.0, walk(5.0));
1012        b.push(5.1, walk(5.1));
1013        let s = b.sample_at(1.0).unwrap();
1014        assert_eq!((s.value, s.state), (walk(5.0), SnapshotState::Held));
1015    }
1016
1017    #[test]
1018    fn full_buffer_drops_the_oldest_and_refuses_older_ones() {
1019        let mut b = SnapshotBuffer::with_config(SnapshotConfig {
1020            capacity: 4,
1021            ..Default::default()
1022        });
1023        for i in 1..=4 {
1024            b.push(i as f64, i as f32);
1025        }
1026        assert_eq!(b.push(0.5, 0.0), SnapshotPush::TooOld);
1027        assert_eq!(b.push(5.0, 5.0), SnapshotPush::Added);
1028        assert_eq!(b.oldest_time(), Some(2.0));
1029        // A late one that fits in the middle pushes the oldest out.
1030        assert_eq!(b.push(2.5, 2.5), SnapshotPush::Added);
1031        assert_eq!((b.oldest_time(), b.len()), (Some(2.5), 4));
1032        assert_eq!(b.sample_at(3.5).unwrap().value, 3.5);
1033    }
1034
1035    #[test]
1036    fn rejects_non_finite_times() {
1037        let mut b = SnapshotBuffer::new();
1038        assert_eq!(b.push(f64::INFINITY, 0.0f32), SnapshotPush::Invalid);
1039        assert_eq!(b.push(f64::NAN, 0.0f32), SnapshotPush::Invalid);
1040        assert!(b.is_empty());
1041    }
1042
1043    #[test]
1044    fn trim_keeps_one_snapshot_before_the_time() {
1045        let mut b = SnapshotBuffer::new();
1046        for i in 0..10 {
1047            b.push(i as f64, i as f32);
1048        }
1049        b.trim_before(4.5);
1050        assert_eq!(b.oldest_time(), Some(4.0));
1051        assert_eq!(b.sample_at(4.5).unwrap().value, 4.5);
1052        b.trim_before(-3.0);
1053        assert_eq!(b.len(), 6);
1054        b.trim_before(100.0);
1055        assert_eq!(b.len(), 1);
1056    }
1057
1058    #[test]
1059    fn clock_sync_ignores_jitter_and_late_packets() {
1060        let mut clock = ClockSync::new();
1061        // Deterministic jitter: trip times 20–120 ms, with an occasional
1062        // packet delayed by a whole second.
1063        let mut worst: f64 = 0.0;
1064        for i in 0..400u32 {
1065            let sent = 7.0 + i as f64 * 0.02;
1066            let mut trip = 0.02 + ((i * 37) % 11) as f64 * 0.01;
1067            if i % 53 == 0 {
1068                trip += 1.0;
1069            }
1070            clock.observe(sent, sent + 100.0 + trip);
1071            if i > 60 {
1072                worst = worst.max((clock.offset().unwrap() - 100.02).abs());
1073            }
1074        }
1075        assert!(worst < 0.025, "estimate strayed {worst} s from the fastest trip");
1076    }
1077
1078    #[test]
1079    fn clock_sync_is_not_dragged_by_one_freakishly_fast_packet() {
1080        let mut clock = ClockSync::new();
1081        for i in 0..40 {
1082            clock.observe(i as f64, i as f64 + 0.05);
1083        }
1084        clock.observe(40.0, 40.0 + 0.005); // 45 ms "faster" than ever
1085        assert!((clock.offset().unwrap() - 0.05).abs() < 1e-9);
1086    }
1087
1088    #[test]
1089    fn clock_sync_follows_slow_drift() {
1090        let mut clock = ClockSync::new();
1091        // The sender's clock runs 100 ppm fast: the offset shrinks 0.1 ms/s.
1092        for i in 0..6000 {
1093            let sent = i as f64 * 0.05;
1094            let local = sent * (1.0 - 1e-4) + 0.04 + (i % 4) as f64 * 0.01;
1095            clock.observe(sent, local);
1096        }
1097        let truth = 0.04 - 299.95e-4; // at the last sample
1098        assert!((clock.offset().unwrap() - truth).abs() < 0.004, "{:?}", clock.offset());
1099    }
1100
1101    #[test]
1102    fn clock_sync_restarts_after_a_clock_jump() {
1103        let mut clock = ClockSync::new();
1104        for i in 0..50 {
1105            clock.observe(i as f64, i as f64 + 0.05);
1106        }
1107        // The sender restarts: its clock now starts from zero again, 5000 s lower.
1108        for i in 0..40 {
1109            clock.observe(i as f64, 5000.0 + i as f64 + 0.05);
1110        }
1111        assert!((clock.offset().unwrap() - 5000.05).abs() < 0.01, "{:?}", clock.offset());
1112        // A single late packet isn't a jump.
1113        let before = clock.offset().unwrap();
1114        clock.observe(41.0, 5041.0 + 20.0);
1115        assert!((clock.offset().unwrap() - before).abs() < 1e-6);
1116    }
1117
1118    #[test]
1119    fn clock_sync_converts_both_ways() {
1120        let mut clock = ClockSync::new();
1121        assert_eq!(clock.to_local(1.0), None);
1122        clock.observe(10.0, 3.0);
1123        assert_eq!(clock.to_local(12.0), Some(5.0));
1124        assert_eq!(clock.to_remote(5.0), Some(12.0));
1125        clock.observe(f64::NAN, 1.0);
1126        assert_eq!(clock.samples(), 1);
1127    }
1128
1129    /// Feeds a stream with a walking sender through a lossy, jittery link
1130    /// and checks how far the picture is from the truth 100 ms ago.
1131    fn run_link(loss_every: u32, jitter_ms: u32, reorder: bool) -> (f32, usize, usize) {
1132        let mut stream = SnapshotStream::new();
1133        let mut inflight: Vec<(f64, f64, Vec3)> = Vec::new(); // (arrival, sent, value)
1134        let (mut worst, mut extrapolated, mut held) = (0.0f32, 0, 0);
1135        for frame in 0..600u32 {
1136            let now = frame as f64 / 60.0;
1137            if frame % 3 == 0 {
1138                // 20 snapshots a second, sender clock 900 s ahead.
1139                let sent = 900.0 + now;
1140                let n = frame / 3;
1141                if loss_every == 0 || n % loss_every != 0 {
1142                    let jitter = ((n * 7919) % (jitter_ms + 1)) as f64 / 1000.0;
1143                    inflight.push((now + 0.03 + jitter, sent, walk(now)));
1144                    if reorder && n % 5 == 0 {
1145                        inflight.push((now + 0.03 + jitter + 0.02, sent, walk(now))); // duplicate
1146                    }
1147                }
1148            }
1149            inflight.sort_by(|a, b| a.0.total_cmp(&b.0));
1150            while inflight.first().is_some_and(|p| p.0 <= now) {
1151                let (_, sent, value) = inflight.remove(0);
1152                stream.push(sent, now, value);
1153            }
1154            if now > 1.0 {
1155                let s = stream.sample(now).unwrap();
1156                // Truth: where the sender was `delay` + the minimum trip ago.
1157                let truth = walk(now - 0.1 - 0.03).x;
1158                worst = worst.max((s.value.x - truth).abs());
1159                match s.state {
1160                    SnapshotState::Extrapolated => extrapolated += 1,
1161                    SnapshotState::Held => held += 1,
1162                    SnapshotState::Interpolated => {}
1163                }
1164            }
1165        }
1166        (worst, extrapolated, held)
1167    }
1168
1169    #[test]
1170    fn stream_is_smooth_on_a_clean_link() {
1171        let (worst, extrapolated, held) = run_link(0, 0, false);
1172        // Packets are only seen at frame boundaries (1/60 s, 0.17 units of
1173        // walking), which the clock estimate takes as trip time.
1174        assert!(worst < 0.1, "{worst}");
1175        assert_eq!((extrapolated, held), (0, 0));
1176    }
1177
1178    #[test]
1179    fn stream_hides_jitter_within_the_delay() {
1180        // Up to 60 ms jitter at 50 ms spacing: the 100 ms delay covers it,
1181        // although the packets arrive out of order and doubled.
1182        let (worst, extrapolated, held) = run_link(0, 60, true);
1183        assert!(worst < 0.5, "{worst}");
1184        assert_eq!(held, 0);
1185        assert!(extrapolated < 60, "{extrapolated}");
1186    }
1187
1188    #[test]
1189    fn stream_rides_out_packet_loss_on_a_straight_walk() {
1190        // Every fourth snapshot lost: interpolation bridges single gaps.
1191        let (worst, extrapolated, held) = run_link(4, 0, false);
1192        assert!(worst < 0.05, "{worst}");
1193        assert_eq!((extrapolated, held), (0, 0));
1194    }
1195
1196    #[test]
1197    fn stream_extrapolates_then_holds_through_a_blackout() {
1198        let mut stream = SnapshotStream::new();
1199        for i in 0..20 {
1200            let t = i as f64 * 0.05;
1201            stream.push(t, t, walk(t));
1202        }
1203        // The last snapshot was at 0.95; nothing arrives after.
1204        let states: Vec<_> = [1.0, 1.1, 1.29, 1.5, 3.0]
1205            .iter()
1206            .map(|&now| stream.sample(now).unwrap())
1207            .collect();
1208        assert_eq!(states[0].state, SnapshotState::Interpolated); // now - 0.1 = 0.9
1209        assert_eq!(states[1].state, SnapshotState::Extrapolated); // 1.0, 0.05 past
1210        assert_eq!(states[2].state, SnapshotState::Extrapolated); // 1.2, 0.25 past
1211        assert_eq!(states[3].state, SnapshotState::Held);
1212        assert_eq!(states[4].state, SnapshotState::Held);
1213        assert!((states[4].value.x - walk(0.95 + 0.25).x).abs() < 1e-3);
1214        assert!(states[1].value.x > states[0].value.x);
1215    }
1216
1217    #[test]
1218    fn stream_is_empty_until_the_first_snapshot_and_after_clear() {
1219        let mut s = SnapshotStream::<Vec3>::new();
1220        assert!(s.sample(1.0).is_none());
1221        s.push(0.0, 0.0, Vec3::ONE);
1222        assert!(s.sample(1.0).is_some());
1223        s.clear();
1224        assert!(s.sample(1.0).is_none());
1225        s.push(0.0, 0.0, Vec3::ONE);
1226        s.reset();
1227        assert_eq!(s.clock().samples(), 0);
1228    }
1229
1230    #[test]
1231    fn late_duplicate_does_not_disturb_the_clock() {
1232        let mut s = SnapshotStream::new();
1233        s.push(10.0, 0.05, 0.0f32);
1234        let before = s.clock().offset();
1235        assert_eq!(s.push(10.0, 3.0, 0.0f32), SnapshotPush::Duplicate);
1236        assert_eq!(s.clock().offset(), before);
1237    }
1238
1239    #[test]
1240    fn delay_can_be_changed_at_runtime() {
1241        let mut s = SnapshotStream::new();
1242        s.push(0.0, 0.0, 0.0f32);
1243        s.push(1.0, 1.0, 10.0);
1244        assert_eq!(s.sample(0.6).unwrap().value, 5.0);
1245        s.set_delay(0.0);
1246        assert_eq!(s.sample(0.6).unwrap().value, 6.0);
1247    }
1248
1249    #[test]
1250    #[should_panic(expected = "capacity must be at least 2")]
1251    fn tiny_capacity_is_refused() {
1252        let _ = SnapshotBuffer::<f32>::with_config(SnapshotConfig {
1253            capacity: 1,
1254            ..Default::default()
1255        });
1256    }
1257
1258    #[test]
1259    #[should_panic(expected = "delay must be a finite number")]
1260    fn negative_delay_is_refused() {
1261        SnapshotStream::<f32>::new().set_delay(-1.0);
1262    }
1263}