Skip to main content

pylon_replication/
view.rs

1//! What one subscriber was sent, kept so the server can see what that
2//! subscriber drew at an earlier tick (lag compensation).
3//!
4//! A [`ViewHistory`] records the frames sent to one subscriber the way the
5//! clients' entity interpolators record them (`EntityInterpolator` in
6//! packages/realtime and packages/csharp), and [`ViewHistory::position_at`]
7//! places an entity at a fractional tick with the same rule. The two agree
8//! exactly for an interpolator with its default options: `holdWhenQuiet`
9//! on and no `snapDistance`.
10//!
11//! The rules, as in the clients:
12//!
13//! - A spawn starts a life with one sample at the spawn tick. A despawn ends
14//!   the open life at its tick. An entity is drawn from its spawn tick until
15//!   the tick its life ended.
16//! - An update adds a sample when the position or a component changed. A
17//!   second update in the same tick replaces the tick's sample. When the
18//!   entity moved and its last sample is older than the tick before, a copy
19//!   of the last sample is added at the tick before: it stood still until
20//!   then.
21//! - A full frame lists every entity the subscriber has. Open lives of
22//!   entities it does not list end at its tick.
23//! - At a render tick, the entity is between the last sample at or before
24//!   it and the next sample, linearly, and holds at the last sample after
25//!   it. There is no extrapolation.
26//!
27//! Positions are the quantized positions times the frame precision, in
28//! f64, as the clients compute them.
29
30use std::collections::{BTreeMap, VecDeque};
31
32use crate::EntityId;
33
34#[derive(Debug, Clone, Copy, PartialEq)]
35struct Sample {
36    tick: u64,
37    pos: [f64; 3],
38    /// Counts component changes, so a sample knows whether its components
39    /// differ from the one before.
40    components: u64,
41}
42
43#[derive(Debug, Clone)]
44struct Life {
45    spawn_tick: u64,
46    despawn_tick: Option<u64>,
47    samples: VecDeque<Sample>,
48}
49
50/// One entity in a frame sent to the subscriber.
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct Sent {
53    pub id: EntityId,
54    /// The quantized position the subscriber has after the frame.
55    pub q: [i64; 3],
56    /// True when the frame changed the entity's components for the subscriber.
57    pub components_changed: bool,
58}
59
60/// The entities sent to one subscriber over the last ticks, as its
61/// interpolator holds them. See the module docs.
62#[derive(Debug, Clone)]
63pub struct ViewHistory {
64    keep_ticks: u64,
65    lives: BTreeMap<EntityId, Vec<Life>>,
66    last_tick: Option<u64>,
67    /// The tick of the last prune: pruning every few ticks is enough to
68    /// bound what the history holds.
69    pruned_at: u64,
70}
71
72/// How many ticks between prunes.
73const PRUNE_EVERY: u64 = 8;
74
75impl ViewHistory {
76    /// Keep enough to place entities at ticks down to `keep_ticks` before
77    /// the newest frame.
78    pub fn new(keep_ticks: u64) -> Self {
79        Self {
80            keep_ticks,
81            lives: BTreeMap::new(),
82            last_tick: None,
83            pruned_at: 0,
84        }
85    }
86
87    /// How many ticks back [`position_at`](Self::position_at) can see.
88    pub fn keep_ticks(&self) -> u64 {
89        self.keep_ticks
90    }
91
92    /// The newest tick recorded.
93    pub fn last_tick(&self) -> Option<u64> {
94        self.last_tick
95    }
96
97    /// Forget everything (a new connection, or the subscriber's baseline was
98    /// reset).
99    pub fn clear(&mut self) {
100        self.lives.clear();
101        self.last_tick = None;
102        self.pruned_at = 0;
103    }
104
105    /// Record a frame sent for `tick`. `full` is a frame that lists every
106    /// entity the subscriber has, as `spawned`. Positions are quantized; the
107    /// subscriber places them at `q * precision`.
108    pub fn record(
109        &mut self,
110        tick: u64,
111        precision: f32,
112        full: bool,
113        spawned: &[Sent],
114        updated: &[Sent],
115        despawned: &[EntityId],
116    ) {
117        if self.last_tick.is_some_and(|last| tick < last) {
118            // Ticks went back: a restarted or different shard.
119            self.clear();
120        }
121        self.last_tick = Some(tick);
122        let p = precision as f64;
123        let pos = |q: [i64; 3]| [q[0] as f64 * p, q[1] as f64 * p, q[2] as f64 * p];
124
125        if full {
126            let present: std::collections::HashSet<EntityId> =
127                spawned.iter().map(|s| s.id).collect();
128            for (id, lives) in self.lives.iter_mut() {
129                if let Some(open) = lives.last_mut().filter(|l| l.despawn_tick.is_none()) {
130                    if !present.contains(id) {
131                        open.despawn_tick = Some(tick);
132                    }
133                }
134            }
135            for s in spawned {
136                match self.open_life(s.id) {
137                    Some(life) => Self::push(life, tick, pos(s.q), s.components_changed),
138                    None => self.spawn(s.id, tick, pos(s.q)),
139                }
140            }
141        } else {
142            for id in despawned {
143                if let Some(open) = self.open_life(*id) {
144                    open.despawn_tick = Some(tick);
145                }
146            }
147            for s in spawned {
148                self.spawn(s.id, tick, pos(s.q));
149            }
150            // Use lookups for sparse updates. Reserve the map walk for
151            // frames that update at least one in eight stored entities.
152            if updated.len() >= self.lives.len().div_ceil(8)
153                && updated.windows(2).all(|w| w[0].id < w[1].id)
154            {
155                // Dense ascending ids: one walk instead of repeated lookups.
156                let mut lives = self.lives.iter_mut().peekable();
157                for s in updated {
158                    while lives.next_if(|(id, _)| **id < s.id).is_some() {}
159                    if let Some((_, l)) = lives.next_if(|(id, _)| **id == s.id) {
160                        if let Some(life) = l.last_mut().filter(|l| l.despawn_tick.is_none()) {
161                            Self::push(life, tick, pos(s.q), s.components_changed);
162                        }
163                    }
164                }
165            } else {
166                for s in updated {
167                    if let Some(life) = self.open_life(s.id) {
168                        Self::push(life, tick, pos(s.q), s.components_changed);
169                    }
170                }
171            }
172        }
173        if tick >= self.pruned_at + PRUNE_EVERY || tick < self.pruned_at {
174            self.prune(tick);
175            self.pruned_at = tick;
176        }
177    }
178
179    /// Where the subscriber drew `id` at `render_tick` (fractional), or None
180    /// when the subscriber did not draw it then (not spawned yet, despawned,
181    /// or never sent).
182    pub fn position_at(&self, id: EntityId, render_tick: f64) -> Option<[f64; 3]> {
183        place_entity(self.lives.get(&id)?, render_tick)
184    }
185
186    /// Every entity the subscriber drew at `render_tick` within `radius` of
187    /// `center` (3D distance), with its position then, in id order.
188    pub fn entities_near(
189        &self,
190        center: [f64; 3],
191        radius: f64,
192        render_tick: f64,
193    ) -> Vec<(EntityId, [f64; 3])> {
194        let r2 = radius * radius;
195        let mut out = Vec::new();
196        for (id, lives) in &self.lives {
197            if let Some(p) = place_entity(lives, render_tick) {
198                let d = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
199                if d[0] * d[0] + d[1] * d[1] + d[2] * d[2] <= r2 {
200                    out.push((*id, p));
201                }
202            }
203        }
204        out
205    }
206
207    /// Ids the history holds a life for (open or recently ended).
208    pub fn ids(&self) -> impl Iterator<Item = EntityId> + '_ {
209        self.lives.keys().copied()
210    }
211
212    fn open_life(&mut self, id: EntityId) -> Option<&mut Life> {
213        self.lives
214            .get_mut(&id)?
215            .last_mut()
216            .filter(|l| l.despawn_tick.is_none())
217    }
218
219    fn spawn(&mut self, id: EntityId, tick: u64, pos: [f64; 3]) {
220        let lives = self.lives.entry(id).or_default();
221        if let Some(open) = lives.last_mut().filter(|l| l.despawn_tick.is_none()) {
222            open.despawn_tick = Some(tick);
223        }
224        let mut samples = VecDeque::with_capacity(4);
225        samples.push_back(Sample {
226            tick,
227            pos,
228            components: 0,
229        });
230        lives.push(Life {
231            spawn_tick: tick,
232            despawn_tick: None,
233            samples,
234        });
235    }
236
237    /// Add an update to an open life (see the module docs).
238    fn push(life: &mut Life, tick: u64, pos: [f64; 3], components_changed: bool) {
239        let samples = &mut life.samples;
240        let last = *samples.back().expect("a life has a sample");
241        let moved = last.pos != pos;
242        if !moved && !components_changed {
243            return;
244        }
245        let components = if components_changed {
246            last.components + 1
247        } else {
248            last.components
249        };
250        if last.tick == tick {
251            *samples.back_mut().expect("checked") = Sample {
252                tick,
253                pos,
254                components,
255            };
256            return;
257        }
258        if moved && last.tick + 1 < tick {
259            // No update since `last`: it stood still until the tick before.
260            samples.push_back(Sample {
261                tick: tick - 1,
262                ..last
263            });
264        }
265        samples.push_back(Sample {
266            tick,
267            pos,
268            components,
269        });
270    }
271
272    /// Drop what no render tick at or after `tick - keep_ticks` needs: ended
273    /// lives, and samples older than the last one at or before that tick.
274    fn prune(&mut self, tick: u64) {
275        let oldest = tick.saturating_sub(self.keep_ticks);
276        self.lives.retain(|_, lives| {
277            lives.retain(|l| l.despawn_tick.is_none_or(|d| d > oldest));
278            for life in lives.iter_mut() {
279                while life.samples.len() > 1 && life.samples[1].tick <= oldest {
280                    life.samples.pop_front();
281                }
282            }
283            !lives.is_empty()
284        });
285    }
286}
287
288fn place_entity(lives: &[Life], render_tick: f64) -> Option<[f64; 3]> {
289    let life = lives
290        .iter()
291        .find(|l| l.despawn_tick.is_none_or(|d| (d as f64) > render_tick))?;
292    if (life.spawn_tick as f64) > render_tick {
293        return None;
294    }
295    Some(place(&life.samples, render_tick))
296}
297
298/// The client interpolators' rule: between the last sample at or before
299/// the render tick and the next one, linearly; held at the last sample
300/// after it.
301fn place(samples: &VecDeque<Sample>, render_tick: f64) -> [f64; 3] {
302    let mut i = samples.len() - 1;
303    while i > 0 && (samples[i].tick as f64) > render_tick {
304        i -= 1;
305    }
306    let from = samples[i];
307    let to = samples.get(i + 1).copied().unwrap_or(from);
308    let t = if to.tick == from.tick {
309        0.0
310    } else {
311        ((render_tick - from.tick as f64) / (to.tick as f64 - from.tick as f64)).clamp(0.0, 1.0)
312    };
313    let d = [
314        to.pos[0] - from.pos[0],
315        to.pos[1] - from.pos[1],
316        to.pos[2] - from.pos[2],
317    ];
318    [
319        from.pos[0] + d[0] * t,
320        from.pos[1] + d[1] * t,
321        from.pos[2] + d[2] * t,
322    ]
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    fn s(id: EntityId, q: [i64; 3]) -> Sent {
330        Sent {
331            id,
332            q,
333            components_changed: false,
334        }
335    }
336
337    #[test]
338    fn an_entity_is_placed_between_samples_and_held_after_the_last() {
339        let mut v = ViewHistory::new(20);
340        v.record(1, 1.0, true, &[s(7, [0, 0, 0])], &[], &[]);
341        v.record(2, 1.0, false, &[], &[s(7, [10, 0, 0])], &[]);
342        v.record(3, 1.0, false, &[], &[s(7, [30, 0, 0])], &[]);
343        assert_eq!(v.position_at(7, 1.0), Some([0.0, 0.0, 0.0]));
344        assert_eq!(v.position_at(7, 1.5), Some([5.0, 0.0, 0.0]));
345        assert_eq!(v.position_at(7, 2.25), Some([15.0, 0.0, 0.0]));
346        assert_eq!(v.position_at(7, 9.0), Some([30.0, 0.0, 0.0]));
347        // Before its spawn, the subscriber did not draw it.
348        assert_eq!(v.position_at(7, 0.5), None);
349        assert_eq!(v.position_at(8, 2.0), None);
350    }
351
352    #[test]
353    fn a_quiet_entity_stands_still_until_the_tick_before_it_moves() {
354        let mut v = ViewHistory::new(20);
355        v.record(10, 0.5, false, &[s(1, [0, 0, 0])], &[], &[]);
356        // No update for ticks 11-14: deferred, or it did not move.
357        v.record(15, 0.5, false, &[], &[s(1, [20, 0, 0])], &[]);
358        assert_eq!(v.position_at(1, 13.0), Some([0.0, 0.0, 0.0]));
359        assert_eq!(v.position_at(1, 14.0), Some([0.0, 0.0, 0.0]));
360        assert_eq!(v.position_at(1, 14.5), Some([5.0, 0.0, 0.0]));
361        assert_eq!(v.position_at(1, 15.0), Some([10.0, 0.0, 0.0]));
362    }
363
364    #[test]
365    fn a_despawn_ends_the_life_and_a_respawn_starts_another() {
366        let mut v = ViewHistory::new(20);
367        v.record(1, 1.0, false, &[s(4, [1, 1, 1])], &[], &[]);
368        v.record(3, 1.0, false, &[], &[], &[4]);
369        v.record(5, 1.0, false, &[s(4, [9, 9, 9])], &[], &[]);
370        assert_eq!(v.position_at(4, 2.9), Some([1.0, 1.0, 1.0]));
371        assert_eq!(v.position_at(4, 3.0), None);
372        assert_eq!(v.position_at(4, 4.0), None);
373        assert_eq!(v.position_at(4, 5.0), Some([9.0, 9.0, 9.0]));
374    }
375
376    #[test]
377    fn a_full_frame_ends_lives_it_does_not_list() {
378        let mut v = ViewHistory::new(20);
379        v.record(1, 1.0, true, &[s(1, [0, 0, 0]), s(2, [5, 0, 0])], &[], &[]);
380        v.record(4, 1.0, true, &[s(1, [3, 0, 0])], &[], &[]);
381        assert_eq!(v.position_at(2, 3.5), Some([5.0, 0.0, 0.0]));
382        assert_eq!(v.position_at(2, 4.0), None);
383        // Entity 1 moved: it stood still until tick 3, then moved to 3.
384        assert_eq!(v.position_at(1, 3.0), Some([0.0, 0.0, 0.0]));
385        assert_eq!(v.position_at(1, 4.0), Some([3.0, 0.0, 0.0]));
386    }
387
388    #[test]
389    fn a_component_change_without_a_move_adds_a_sample() {
390        let mut v = ViewHistory::new(20);
391        v.record(1, 1.0, false, &[s(1, [0, 0, 0])], &[], &[]);
392        let hp = Sent {
393            components_changed: true,
394            ..s(1, [0, 0, 0])
395        };
396        v.record(5, 1.0, false, &[], &[hp], &[]);
397        v.record(6, 1.0, false, &[], &[s(1, [10, 0, 0])], &[]);
398        // The tick-5 sample holds it still until 5; it moves from there.
399        assert_eq!(v.position_at(1, 5.0), Some([0.0, 0.0, 0.0]));
400        assert_eq!(v.position_at(1, 5.5), Some([5.0, 0.0, 0.0]));
401    }
402
403    #[test]
404    fn old_samples_and_lives_are_pruned() {
405        let mut v = ViewHistory::new(5);
406        v.record(1, 1.0, false, &[s(1, [0, 0, 0]), s(2, [0, 0, 0])], &[], &[]);
407        v.record(2, 1.0, false, &[], &[], &[2]);
408        for t in 3..=30 {
409            v.record(t, 1.0, false, &[], &[s(1, [t as i64, 0, 0])], &[]);
410        }
411        assert!(!v.lives.contains_key(&2), "an ended life long gone");
412        let samples = &v.lives[&1][0].samples;
413        // At most the window, the prune interval, and a sample either side.
414        assert!(
415            samples.len() <= 5 + PRUNE_EVERY as usize + 2,
416            "{} samples",
417            samples.len()
418        );
419        // What the window covers is still exact.
420        assert_eq!(v.position_at(1, 25.5), Some([25.5, 0.0, 0.0]));
421    }
422
423    #[test]
424    fn updates_in_any_order_land_on_their_entities() {
425        let mut v = ViewHistory::new(20);
426        v.record(
427            1,
428            1.0,
429            false,
430            &[s(1, [0, 0, 0]), s(2, [0, 0, 0]), s(3, [0, 0, 0])],
431            &[],
432            &[],
433        );
434        v.record(2, 1.0, false, &[], &[s(3, [3, 0, 0]), s(1, [1, 0, 0])], &[]);
435        v.record(3, 1.0, false, &[], &[s(1, [2, 0, 0]), s(3, [6, 0, 0])], &[]);
436        assert_eq!(v.position_at(1, 3.0), Some([2.0, 0.0, 0.0]));
437        assert_eq!(v.position_at(2, 3.0), Some([0.0, 0.0, 0.0]));
438        assert_eq!(v.position_at(3, 2.0), Some([3.0, 0.0, 0.0]));
439        assert_eq!(v.position_at(3, 3.0), Some([6.0, 0.0, 0.0]));
440    }
441
442    #[test]
443    fn near_finds_the_entities_within_the_radius_at_the_tick() {
444        let mut v = ViewHistory::new(20);
445        v.record(
446            1,
447            1.0,
448            false,
449            &[s(1, [0, 0, 0]), s(2, [100, 0, 0])],
450            &[],
451            &[],
452        );
453        v.record(2, 1.0, false, &[], &[s(2, [10, 0, 0])], &[]);
454        assert_eq!(
455            v.entities_near([0.0, 0.0, 0.0], 20.0, 1.0),
456            vec![(1, [0.0, 0.0, 0.0])]
457        );
458        assert_eq!(
459            v.entities_near([0.0, 0.0, 0.0], 20.0, 2.0),
460            vec![(1, [0.0, 0.0, 0.0]), (2, [10.0, 0.0, 0.0])]
461        );
462    }
463
464    #[test]
465    fn sparse_and_dense_updates_match_individual_lookups() {
466        let spawned: Vec<_> = (0..256).map(|id| s(id, [id as i64, 0, 0])).collect();
467        let mut history = ViewHistory::new(10);
468        history.record(1, 0.5, true, &spawned, &[], &[]);
469        let mut expected = history.clone();
470        for tick in 2..=40 {
471            // Include both sides of the density threshold and a missing id.
472            let count = [1, 2, 31, 32, 33, 128, 256][tick as usize % 7];
473            let updated: Vec<_> = (0..count)
474                .map(|i| Sent {
475                    components_changed: tick % 3 == 0,
476                    ..s(256 - count + i, [i as i64, tick as i64, -1])
477                })
478                .chain([s(999, [0; 3])])
479                .collect();
480            let despawned = if tick == 5 { vec![255] } else { vec![] };
481            let respawned = if tick == 10 {
482                vec![s(255, [0; 3])]
483            } else {
484                vec![]
485            };
486            history.record(tick, 0.5, false, &respawned, &updated, &despawned);
487            // Reverse order forces the lookup path, including for dense frames.
488            let reversed: Vec<_> = updated.iter().rev().copied().collect();
489            expected.record(tick, 0.5, false, &respawned, &reversed, &despawned);
490            assert_eq!(
491                history.ids().collect::<Vec<_>>(),
492                expected.ids().collect::<Vec<_>>()
493            );
494            for (id, lives) in &history.lives {
495                let wanted = &expected.lives[id];
496                assert_eq!(lives.len(), wanted.len());
497                for (actual, expected) in lives.iter().zip(wanted) {
498                    assert_eq!(actual.spawn_tick, expected.spawn_tick);
499                    assert_eq!(actual.despawn_tick, expected.despawn_tick);
500                    assert_eq!(actual.samples, expected.samples);
501                }
502            }
503        }
504    }
505
506    #[test]
507    fn nearby_queries_match_positions_across_entity_lifetimes() {
508        let mut history = ViewHistory::new(20);
509        history.record(1, 1.0, true, &[s(1, [0; 3]), s(2, [10, 0, 0])], &[], &[]);
510        history.record(3, 1.0, false, &[], &[s(2, [0, 10, 0])], &[1]);
511        history.record(5, 1.0, false, &[s(1, [0, 0, 10]), s(3, [20; 3])], &[], &[]);
512        for tick in [0.0, 1.0, 2.5, 3.0, 4.0, 5.0, 6.0] {
513            for radius in [0.0, 10.0, 30.0, -10.0, f64::INFINITY, f64::NAN] {
514                let expected: Vec<_> = history
515                    .ids()
516                    .filter_map(|id| {
517                        let p = history.position_at(id, tick)?;
518                        let distance = p[0] * p[0] + p[1] * p[1] + p[2] * p[2];
519                        (distance <= radius * radius).then_some((id, p))
520                    })
521                    .collect();
522                assert_eq!(history.entities_near([0.0; 3], radius, tick), expected);
523            }
524        }
525    }
526}