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}