Skip to main content

frust_shell_common/
resample.rs

1//! Pointer-event resampling + deadline-aware pacing helpers shared by the
2//! mobile shells.
3//!
4//! # What lives here
5//!
6//! - [`PointerResampler`] — a pure-logic, host-testable buffer of raw pointer
7//!   samples (logical coords + a shell-supplied monotonic timestamp) that emits
8//!   an interpolated `Move` position at each frame boundary
9//!   (`frame_time − `[`SAMPLE_OFFSET_NANOS`]) with a Flutter-parity
10//!   [half-frame prediction window](PREDICTION_WINDOW_NANOS). `Down`/`Up`/
11//!   `Cancel` phase transitions pass through **losslessly** — never synthesized,
12//!   never dropped, never repositioned — so only `Move` positions are ever
13//!   resampled (Flutter's `PointerEventResampler` contract, adapted). Samples
14//!   are kept in one lane per [`PointerId`](frust_core::event::PointerId), so
15//!   simultaneous contacts are each resampled along their own path and never
16//!   mix.
17//! - [`frame_interval_nanos`] / [`deadline_overrun`] — the deadline-aware
18//!   scheduling helpers: estimate a frame-target budget from the
19//!   tick-to-tick timestamp delta, and decide whether a frame's measured work
20//!   overran it. **Instrumentation only** — no work-dropping heuristics live
21//!   here.
22//!
23//! # Layering choice
24//!
25//! Like [`crate::frame_gate`] and [`crate::perf`], this is shell-owned by
26//! design and lives in `frust-shell-common`: it is platform-agnostic, contains
27//! no `unsafe`, no FFI, and no clock read of its own — every timestamp is
28//! handed in by the shell (which owns the monotonic clock), keeping this whole
29//! module deterministically unit-testable on the host. It compiles unchanged on
30//! every target (host / `aarch64-linux-android` / iOS), preserving the crate's
31//! zero-`unsafe`, compiles-everywhere charter (see `docs/ARCHITECTURE.md`'s
32//! Layer Dependencies).
33//!
34//! # Clock domain
35//!
36//! The resampler is domain-agnostic: it only ever *differences* two timestamps,
37//! so a shell may stamp both the raw samples ([`PointerResampler::push`]) and
38//! the per-frame sample query ([`PointerResampler::resample`]) from any single
39//! monotonic source of its choosing, as long as **both come from the same
40//! source**. The mobile shells use a per-handle `Instant` epoch for this
41//! (decoupled from the vsync `FrameTime` clock that drives animation), so a
42//! sample stamped at touch arrival and the frame's sample-time are always
43//! comparable.
44
45use std::collections::VecDeque;
46use std::time::Duration;
47
48use frust_core::event::{PointerButton, PointerEvent, PointerId, PointerPhase};
49use kurbo::Point;
50
51/// The kill-switch environment/compile-time variable: when set to any
52/// non-`"0"` value, [`PointerResampler::new`] yields a **disabled** resampler
53/// that delivers every raw sample straight through in arrival order (pre-
54/// resampling behavior verbatim). Mirrors
55/// [`FRUST_NO_FRAME_GATE`](crate::frame_gate::NO_FRAME_GATE_VAR)'s compile-time-
56/// or-runtime parsing exactly.
57pub const NO_RESAMPLE_VAR: &str = "FRUST_NO_RESAMPLE";
58
59/// How far behind the frame deadline pointer positions are sampled, in
60/// nanoseconds: a `Move` is emitted at `frame_time − SAMPLE_OFFSET`, slightly
61/// in the past so the two raw samples bracketing that instant are usually
62/// already in hand (**interpolation**, not extrapolation) at the common
63/// touch/display cadence.
64///
65/// **Community-approximate** (see `docs/CODE_STANDARDS.md`): Flutter's
66/// `GestureBinding` resamples at a negative `samplingOffset`, but the exact
67/// default has drifted across engine versions and is not a published constant.
68/// ~5ms is the modest interpolate-slightly-in-the-past value community
69/// reimplementations converge on — small enough to keep input latency
70/// imperceptible, large enough to bracket a newer sample most frames.
71pub const SAMPLE_OFFSET_NANOS: u64 = 5_000_000;
72
73/// The forward-prediction clamp, in nanoseconds: when the sample instant runs
74/// *past* the newest buffered sample (the finger paused, or its samples lag the
75/// display), the position is extrapolated along the last segment's velocity but
76/// never more than this far ahead of the newest sample.
77///
78/// **Community-approximate**: Flutter caps pointer prediction at roughly one
79/// half-refresh window to keep a paused/again-moving finger from overshooting;
80/// half of a 60Hz frame (~8.33ms) is that Flutter-parity half-frame window.
81pub const PREDICTION_WINDOW_NANOS: u64 = 8_333_333;
82
83/// Fallback frame-target interval (60Hz) used by [`frame_interval_nanos`] when
84/// there is no prior tick or the tick-to-tick delta is implausible.
85pub const DEFAULT_REFRESH_INTERVAL_NANOS: u64 = 16_666_667;
86
87/// Lower plausibility bound for a tick-to-tick interval (1ms ≈ a 1000Hz
88/// ceiling): a smaller delta is treated as a clock glitch and replaced by
89/// [`DEFAULT_REFRESH_INTERVAL_NANOS`].
90pub const MIN_PLAUSIBLE_INTERVAL_NANOS: u64 = 1_000_000;
91
92/// Upper plausibility bound for a tick-to-tick interval (100ms ≈ a 10Hz floor):
93/// a larger delta (a long idle across skipped ticks, a resumed app) is treated
94/// as non-representative and replaced by [`DEFAULT_REFRESH_INTERVAL_NANOS`].
95pub const MAX_PLAUSIBLE_INTERVAL_NANOS: u64 = 100_000_000;
96
97/// One raw pointer contact as delivered by a platform touch entry point, before
98/// resampling: which contact it is, the phase transition, the **logical**
99/// (density-independent) position the shell already converted, the button
100/// (always [`PointerButton::Primary`] for touch), and a shell-supplied monotonic
101/// timestamp (see the module's *Clock domain* note).
102#[derive(Debug, Clone, Copy, PartialEq)]
103pub struct RawPointerSample {
104    /// Which contact the sample belongs to — selects its resampling lane.
105    pub pointer_id: PointerId,
106    pub phase: PointerPhase,
107    pub position: Point,
108    pub button: PointerButton,
109    pub time_nanos: u64,
110}
111
112impl RawPointerSample {
113    /// The raw sample as a [`PointerEvent`] with its reported position (the
114    /// verbatim form used on the disabled/direct-delivery path and for phase
115    /// transitions).
116    fn as_event(&self) -> PointerEvent {
117        PointerEvent {
118            phase: self.phase,
119            position: self.position,
120            button: self.button,
121        }
122    }
123}
124
125/// One resampled event and the contact it belongs to — what
126/// [`PointerResampler::resample`] emits, so the shell can rebuild the
127/// [`InputEvent::PointerContact`](frust_core::event::InputEvent::PointerContact)
128/// carrier for it.
129#[derive(Debug, Clone, Copy, PartialEq)]
130pub struct ResampledPointer {
131    /// The contact the event belongs to.
132    pub pointer_id: PointerId,
133    /// The (possibly resampled) event.
134    pub event: PointerEvent,
135}
136
137/// A buffered raw sample plus its global arrival sequence number — the
138/// tiebreak that keeps the merged output of several lanes in arrival order.
139#[derive(Debug, Clone, Copy)]
140struct Queued {
141    seq: u64,
142    sample: RawPointerSample,
143}
144
145/// One contact's resampling state: its own buffered samples and the position it
146/// last emitted. Lanes never share samples or positions, so two fingers moving
147/// at once are each interpolated along their own path.
148#[derive(Debug)]
149struct Lane {
150    pointer_id: PointerId,
151    /// This contact's raw samples in arrival (== timestamp) order, drained up to
152    /// each frame's sample instant.
153    queue: VecDeque<Queued>,
154    /// The position of the last event this lane emitted, so a `Move`
155    /// interpolation dedups a no-op re-emit and a query with no bracketing pair
156    /// can hold the pointer where it was. Cleared to `None` on an `Up`/`Cancel`
157    /// (the contact ended — no position to hold), which is also what lets the
158    /// lane be retired once its queue is empty.
159    last_emitted: Option<Point>,
160}
161
162/// One emitted event awaiting the cross-lane merge, keyed by when it happened:
163/// `(time, seq)` of the raw sample it came from (for a coalesced `Move`, the
164/// last move sample of its run).
165type Keyed = ((u64, u64), ResampledPointer);
166
167/// Buffers raw pointer samples and emits frame-boundary-resampled events, one
168/// independent **lane per [`PointerId`]**. See the module docs for the
169/// interpolation/prediction contract; construct one per app handle and drive it
170/// from the shell's touch and frame paths.
171///
172/// A lane opens with its contact's first sample and ends once its `Up`/`Cancel`
173/// has been emitted. Each lane resamples exactly as a single-pointer resampler
174/// would; the events of several lanes are merged back into arrival order.
175#[derive(Debug)]
176pub struct PointerResampler {
177    /// When `false`, [`resample`](Self::resample) drains every buffered sample
178    /// verbatim in arrival order — the [`NO_RESAMPLE_VAR`] kill switch and
179    /// [`disabled`](Self::disabled) path (pre-resampling behavior verbatim).
180    enabled: bool,
181    /// The live lanes, in the order their contacts first appeared.
182    lanes: Vec<Lane>,
183    /// The next arrival sequence number [`push`](Self::push) hands out.
184    next_seq: u64,
185    /// The lanes' keyed output before the merge, reused across frames (cleared,
186    /// not reallocated) so a drag's per-frame resample allocates nothing.
187    merge: Vec<Keyed>,
188}
189
190impl PointerResampler {
191    /// A resampler honoring the [`NO_RESAMPLE_VAR`] kill switch — what every
192    /// shell constructs. When the variable is set (compile-time `--define` or
193    /// runtime env, any non-`"0"` value), this is equivalent to
194    /// [`disabled`](Self::disabled).
195    pub fn new() -> Self {
196        Self::with_enabled(!kill_switch_engaged())
197    }
198
199    /// A resampler that always delivers raw samples straight through — the
200    /// explicit disabled/kill-switch form (and a test seam bypassing the env
201    /// read). Mirrors [`Self::new`]'s behavior when [`NO_RESAMPLE_VAR`] is set.
202    pub fn disabled() -> Self {
203        Self::with_enabled(false)
204    }
205
206    /// Construct with an explicit enabled flag, bypassing the env read — the
207    /// test/advanced seam (mirrors [`crate::frame_gate::FrameGate::with_enabled`]).
208    pub fn with_enabled(enabled: bool) -> Self {
209        Self {
210            enabled,
211            lanes: Vec::new(),
212            next_seq: 0,
213            merge: Vec::new(),
214        }
215    }
216
217    /// Whether resampling is active. `false` for a [`disabled`](Self::disabled)
218    /// resampler or when the kill switch is engaged — in which case the shell
219    /// should deliver touches directly rather than buffering them here.
220    pub fn is_enabled(&self) -> bool {
221        self.enabled
222    }
223
224    /// Whether any raw sample is still buffered, in any lane. The shell ORs
225    /// this into its frame-gate input (`events_since_last_frame`) so a frame
226    /// that could not yet drain a too-new sample still runs on the next tick —
227    /// the "pending buffered input never starves the gate" contract (see
228    /// `docs/CODE_STANDARDS.md`'s default-to-run rule).
229    pub fn has_pending(&self) -> bool {
230        self.lanes.iter().any(|lane| !lane.queue.is_empty())
231    }
232
233    /// Buffer one raw platform sample in its contact's lane (a touch entry
234    /// point calls this per contact). Each contact's samples must be pushed in
235    /// nondecreasing timestamp order (the natural arrival order of one
236    /// pointer's stream).
237    pub fn push(&mut self, sample: RawPointerSample) {
238        let seq = self.next_seq;
239        self.next_seq = self.next_seq.wrapping_add(1);
240        let queued = Queued { seq, sample };
241        match self
242            .lanes
243            .iter_mut()
244            .find(|lane| lane.pointer_id == sample.pointer_id)
245        {
246            Some(lane) => lane.queue.push_back(queued),
247            None => self.lanes.push(Lane {
248                pointer_id: sample.pointer_id,
249                queue: VecDeque::from([queued]),
250                last_emitted: None,
251            }),
252        }
253    }
254
255    /// Drain the buffered samples up to this frame's sample instant into `out`,
256    /// appending the resampled events — each tagged with its contact — that the
257    /// shell should feed into the tree this frame, in order. `out` is appended
258    /// to, not cleared — the caller owns/reuses the buffer.
259    ///
260    /// `frame_time_nanos` is the frame's sample query time in the shell's chosen
261    /// monotonic domain (the same domain [`push`](Self::push) stamped with).
262    ///
263    /// On a **disabled** resampler every buffered sample is emitted verbatim in
264    /// arrival order (direct delivery). On an enabled one, each lane
265    /// independently:
266    /// - emits `Down`/`Up`/`Cancel` whose timestamp has reached the sample
267    ///   instant **losslessly** in order, each at its raw reported position;
268    /// - coalesces a run of `Move` samples up to the sample instant into a
269    ///   single `Move` at the position interpolated at
270    ///   `frame_time − `[`SAMPLE_OFFSET_NANOS`] (or extrapolated within the
271    ///   [`PREDICTION_WINDOW_NANOS`] clamp when the finger has outrun its
272    ///   samples);
273    /// - leaves samples still ahead of the sample instant buffered (see
274    ///   [`has_pending`](Self::has_pending)).
275    ///
276    /// The lanes' events are then merged by the arrival order of the samples
277    /// they came from, so a second finger's `Down` lands after the first
278    /// finger's `Down` that preceded it. A lane whose `Up`/`Cancel` was emitted
279    /// and that has nothing left buffered is retired.
280    pub fn resample(&mut self, frame_time_nanos: u64, out: &mut Vec<ResampledPointer>) {
281        if !self.enabled {
282            // Verbatim, in arrival order across every lane.
283            self.merge.clear();
284            for lane in &mut self.lanes {
285                self.merge.extend(lane.queue.drain(..).map(|queued| {
286                    (
287                        (0, queued.seq),
288                        ResampledPointer {
289                            pointer_id: queued.sample.pointer_id,
290                            event: queued.sample.as_event(),
291                        },
292                    )
293                }));
294            }
295            self.merge.sort_by_key(|(key, _)| *key);
296            out.extend(self.merge.drain(..).map(|(_, event)| event));
297            self.lanes.clear();
298            return;
299        }
300
301        let sample_time = frame_time_nanos.saturating_sub(SAMPLE_OFFSET_NANOS);
302        self.merge.clear();
303        for lane in &mut self.lanes {
304            lane.resample(sample_time, &mut self.merge);
305        }
306        // Stable, and each lane's own events are already in key order, so this
307        // only interleaves lanes — it never reorders within one (and a single
308        // lane, every gesture today, comes out exactly as it went in).
309        if self.lanes.len() > 1 {
310            self.merge.sort_by_key(|(key, _)| *key);
311        }
312        out.extend(self.merge.drain(..).map(|(_, event)| event));
313        self.lanes
314            .retain(|lane| !lane.queue.is_empty() || lane.last_emitted.is_some());
315    }
316}
317
318impl Lane {
319    /// This lane's half of [`PointerResampler::resample`]: drain its samples up
320    /// to `sample_time` into `out`, each keyed for the cross-lane merge.
321    fn resample(&mut self, sample_time: u64, out: &mut Vec<Keyed>) {
322        // Position at the sample instant, computed from the pre-drain queue
323        // snapshot so every coalesced Move this frame lands on the same point.
324        let sample_pos = self.position_at(sample_time);
325
326        // The last move sample of the run being coalesced, if any.
327        let mut pending_move: Option<Queued> = None;
328
329        // Copy the front's timestamp out so the immutable `front()` borrow
330        // ends before the body pops/mutates.
331        while let Some(time_nanos) = self.queue.front().map(|q| q.sample.time_nanos) {
332            if time_nanos > sample_time {
333                break; // not yet reached — leave it (and everything after) buffered
334            }
335            let queued = self.queue.pop_front().expect("front was just observed");
336            match queued.sample.phase {
337                PointerPhase::Move => {
338                    pending_move = Some(queued);
339                }
340                PointerPhase::Down => {
341                    // A transition ends any coalesced Move run before it (a
342                    // well-formed stream never nests one, but keep ordering
343                    // lossless regardless).
344                    self.flush_move(pending_move.take(), sample_pos, out);
345                    self.emit(queued, out);
346                    self.last_emitted = Some(queued.sample.position);
347                }
348                PointerPhase::Up | PointerPhase::Cancel => {
349                    // Bring the pointer to its resampled position first, then
350                    // lift/cancel at the raw reported position.
351                    self.flush_move(pending_move.take(), sample_pos, out);
352                    self.emit(queued, out);
353                    self.last_emitted = None; // contact ended — nothing to hold
354                }
355            }
356        }
357
358        // Trailing coalesced Moves become one interpolated Move at the sample
359        // position.
360        self.flush_move(pending_move, sample_pos, out);
361    }
362
363    /// Emit one raw transition verbatim.
364    fn emit(&self, queued: Queued, out: &mut Vec<Keyed>) {
365        out.push((
366            (queued.sample.time_nanos, queued.seq),
367            ResampledPointer {
368                pointer_id: self.pointer_id,
369                event: queued.sample.as_event(),
370            },
371        ));
372    }
373
374    /// Emit the single coalesced `Move` for a run of buffered move samples
375    /// (`last` is the run's final sample), at the frame's resampled position —
376    /// skipped when there was no move, no resolvable position, or the position
377    /// is unchanged from the last emit.
378    fn flush_move(
379        &mut self,
380        last: Option<Queued>,
381        sample_pos: Option<Point>,
382        out: &mut Vec<Keyed>,
383    ) {
384        let Some(last) = last else {
385            return;
386        };
387        if let Some(pos) = sample_pos
388            && self.last_emitted != Some(pos)
389        {
390            out.push((
391                (last.sample.time_nanos, last.seq),
392                ResampledPointer {
393                    pointer_id: self.pointer_id,
394                    event: PointerEvent {
395                        phase: PointerPhase::Move,
396                        position: pos,
397                        button: last.sample.button,
398                    },
399                },
400            ));
401            self.last_emitted = Some(pos);
402        }
403    }
404
405    /// The interpolated (or clamped-extrapolated) pointer position at
406    /// `sample_time`, from the current queue snapshot. `None` only when there is
407    /// no sample and no prior emit to hold onto.
408    fn position_at(&self, sample_time: u64) -> Option<Point> {
409        if self.queue.is_empty() {
410            return self.last_emitted;
411        }
412
413        // The last sample at/before the instant, and the first strictly after.
414        let mut before: Option<&RawPointerSample> = None;
415        let mut after: Option<&RawPointerSample> = None;
416        for queued in &self.queue {
417            let sample = &queued.sample;
418            if sample.time_nanos <= sample_time {
419                before = Some(sample);
420            } else {
421                after = Some(sample);
422                break;
423            }
424        }
425
426        match (before, after) {
427            // Bracketed: linear interpolation between the two.
428            (Some(a), Some(b)) => Some(lerp_point(
429                a.position,
430                b.position,
431                fraction(a.time_nanos, b.time_nanos, sample_time),
432            )),
433            // The instant is past the newest sample: predict forward, clamped.
434            (Some(a), None) => Some(self.predict_forward(a, sample_time)),
435            // The instant precedes the first sample: hold at the last emit (or
436            // the first sample if nothing was ever emitted).
437            (None, Some(b)) => self.last_emitted.or(Some(b.position)),
438            (None, None) => self.last_emitted,
439        }
440    }
441
442    /// Extrapolate past `newest` along the last segment's velocity, clamped so
443    /// the prediction never runs more than [`PREDICTION_WINDOW_NANOS`] ahead of
444    /// `newest`. Falls back to holding at `newest.position` when there is no
445    /// prior sample to derive a velocity from (a single-sample queue).
446    fn predict_forward(&self, newest: &RawPointerSample, sample_time: u64) -> Point {
447        // The sample immediately before `newest` (the second-to-last element).
448        match self.queue.iter().rev().nth(1).map(|queued| &queued.sample) {
449            Some(prior) if newest.time_nanos > prior.time_nanos => {
450                let ahead = (sample_time - newest.time_nanos).min(PREDICTION_WINDOW_NANOS);
451                let span = newest.time_nanos - prior.time_nanos;
452                let t = ahead as f64 / span as f64;
453                Point::new(
454                    newest.position.x + (newest.position.x - prior.position.x) * t,
455                    newest.position.y + (newest.position.y - prior.position.y) * t,
456                )
457            }
458            _ => newest.position,
459        }
460    }
461}
462
463impl Default for PointerResampler {
464    fn default() -> Self {
465        Self::new()
466    }
467}
468
469/// The normalized position of `t` within `[a, b]` (`0.0` at `a`, `1.0` at `b`),
470/// guarding a zero-width span (two samples at the same timestamp) by returning
471/// `1.0` so the later sample wins.
472fn fraction(a: u64, b: u64, t: u64) -> f64 {
473    let span = b.saturating_sub(a);
474    if span == 0 {
475        return 1.0;
476    }
477    (t.saturating_sub(a)) as f64 / span as f64
478}
479
480/// Linear interpolation between two points at normalized `t`.
481fn lerp_point(a: Point, b: Point, t: f64) -> Point {
482    Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
483}
484
485/// Estimate this frame's deadline budget (the frame-target interval) from two
486/// consecutive tick timestamps. Returns the tick-to-tick
487/// delta when it is plausible (`[`[`MIN_PLAUSIBLE_INTERVAL_NANOS`]`,
488/// `[`MAX_PLAUSIBLE_INTERVAL_NANOS`]`]`), else [`DEFAULT_REFRESH_INTERVAL_NANOS`]
489/// (60Hz) — covering the first tick (no prior), a clock glitch, and a long idle
490/// across skipped ticks.
491pub fn frame_interval_nanos(prev_tick: Option<u64>, cur_tick: u64) -> u64 {
492    match prev_tick {
493        Some(prev) if cur_tick > prev => {
494            let delta = cur_tick - prev;
495            if (MIN_PLAUSIBLE_INTERVAL_NANOS..=MAX_PLAUSIBLE_INTERVAL_NANOS).contains(&delta) {
496                delta
497            } else {
498                DEFAULT_REFRESH_INTERVAL_NANOS
499            }
500        }
501        _ => DEFAULT_REFRESH_INTERVAL_NANOS,
502    }
503}
504
505/// Whether a frame's measured `work` overran its `budget_nanos` deadline.
506/// **Instrumentation only** — the shell records the overrun
507/// (a counter, gated behind `perf::enabled()`); it never drops or reshapes work
508/// on the strength of this.
509pub fn deadline_overrun(work: Duration, budget_nanos: u64) -> bool {
510    (work.as_nanos() as u64) > budget_nanos
511}
512
513/// Reads the [`NO_RESAMPLE_VAR`] kill switch from the compile-time define and
514/// the process environment, mirroring [`crate::frame_gate`]'s
515/// `kill_switch_engaged`: either source set to a non-`"0"` value engages it.
516fn kill_switch_engaged() -> bool {
517    kill_switch(
518        option_env!("FRUST_NO_RESAMPLE"),
519        std::env::var(NO_RESAMPLE_VAR).ok().as_deref(),
520    )
521}
522
523/// The pure decision [`kill_switch_engaged`] wraps: a non-empty, non-`"0"`
524/// value from either the compile-time or runtime source engages the switch.
525/// Split out so it is directly unit-testable without touching the process
526/// environment (see [`crate::frame_gate`]'s `kill_switch`).
527fn kill_switch(compile_time: Option<&str>, runtime: Option<&str>) -> bool {
528    fn is_set_non_zero(value: Option<&str>) -> bool {
529        matches!(value, Some(v) if v != "0")
530    }
531    is_set_non_zero(compile_time) || is_set_non_zero(runtime)
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    fn sample(phase: PointerPhase, x: f64, y: f64, time_nanos: u64) -> RawPointerSample {
539        sample_for(PointerId::touch(0), phase, x, y, time_nanos)
540    }
541
542    fn sample_for(
543        pointer_id: PointerId,
544        phase: PointerPhase,
545        x: f64,
546        y: f64,
547        time_nanos: u64,
548    ) -> RawPointerSample {
549        RawPointerSample {
550            pointer_id,
551            phase,
552            position: Point::new(x, y),
553            button: PointerButton::Primary,
554            time_nanos,
555        }
556    }
557
558    /// A frame time whose sample instant (`frame_time − SAMPLE_OFFSET`) is
559    /// exactly `sample_time` — the tests reason in sample-instant terms.
560    fn frame_time_for(sample_time: u64) -> u64 {
561        sample_time + SAMPLE_OFFSET_NANOS
562    }
563
564    /// The resampled events of one frame, with their contact ids.
565    fn drain_tagged(resampler: &mut PointerResampler, sample_time: u64) -> Vec<ResampledPointer> {
566        let mut out = Vec::new();
567        resampler.resample(frame_time_for(sample_time), &mut out);
568        out
569    }
570
571    /// The resampled events of one frame — the single-contact tests' view.
572    fn drain(resampler: &mut PointerResampler, sample_time: u64) -> Vec<PointerEvent> {
573        drain_tagged(resampler, sample_time)
574            .into_iter()
575            .map(|r| r.event)
576            .collect()
577    }
578
579    // -----------------------------------------------------------------
580    // kill_switch (pure) — mirrors frame_gate's coverage
581    // -----------------------------------------------------------------
582
583    #[test]
584    fn kill_switch_off_when_neither_set() {
585        assert!(!kill_switch(None, None));
586    }
587
588    #[test]
589    fn kill_switch_on_when_either_source_wins() {
590        assert!(kill_switch(Some("1"), None));
591        assert!(kill_switch(None, Some("1")));
592        assert!(kill_switch(Some("0"), Some("1")));
593        assert!(kill_switch(Some("1"), Some("0")));
594    }
595
596    #[test]
597    fn kill_switch_off_when_either_is_literal_zero_and_other_unset() {
598        assert!(!kill_switch(Some("0"), None));
599        assert!(!kill_switch(None, Some("0")));
600    }
601
602    // -----------------------------------------------------------------
603    // Kill-switch off path: direct verbatim delivery
604    // -----------------------------------------------------------------
605
606    #[test]
607    fn disabled_resampler_delivers_every_sample_verbatim_in_order() {
608        let mut r = PointerResampler::disabled();
609        assert!(!r.is_enabled());
610        r.push(sample(PointerPhase::Down, 1.0, 2.0, 0));
611        r.push(sample(PointerPhase::Move, 3.0, 4.0, 5));
612        r.push(sample(PointerPhase::Up, 5.0, 6.0, 10));
613
614        // Sample instant is irrelevant when disabled — everything drains.
615        let out = drain(&mut r, 0);
616        assert_eq!(out.len(), 3);
617        assert_eq!(out[0].phase, PointerPhase::Down);
618        assert_eq!(out[0].position, Point::new(1.0, 2.0));
619        assert_eq!(out[1].phase, PointerPhase::Move);
620        assert_eq!(out[1].position, Point::new(3.0, 4.0));
621        assert_eq!(out[2].phase, PointerPhase::Up);
622        assert_eq!(out[2].position, Point::new(5.0, 6.0));
623        assert!(!r.has_pending());
624    }
625
626    // -----------------------------------------------------------------
627    // Interpolation math
628    // -----------------------------------------------------------------
629
630    #[test]
631    fn move_position_interpolated_between_bracketing_samples() {
632        let mut r = PointerResampler::with_enabled(true);
633        // Two moves 10ms apart along x; sample the midpoint (5ms).
634        r.push(sample(PointerPhase::Move, 0.0, 0.0, 0));
635        r.push(sample(PointerPhase::Move, 10.0, 0.0, 10_000_000));
636
637        let out = drain(&mut r, 5_000_000);
638        assert_eq!(out.len(), 1);
639        assert_eq!(out[0].phase, PointerPhase::Move);
640        assert_eq!(out[0].position, Point::new(5.0, 0.0));
641        // The later sample is still ahead of the instant → buffered.
642        assert!(r.has_pending());
643    }
644
645    #[test]
646    fn interpolation_fraction_is_time_weighted() {
647        let mut r = PointerResampler::with_enabled(true);
648        r.push(sample(PointerPhase::Move, 0.0, 0.0, 0));
649        r.push(sample(PointerPhase::Move, 100.0, 40.0, 10_000_000));
650        // 25% of the way through the segment.
651        let out = drain(&mut r, 2_500_000);
652        assert_eq!(out[0].position, Point::new(25.0, 10.0));
653    }
654
655    // -----------------------------------------------------------------
656    // Phase-transition passthrough (lossless, raw positions)
657    // -----------------------------------------------------------------
658
659    #[test]
660    fn down_move_up_pass_transitions_through_losslessly() {
661        let mut r = PointerResampler::with_enabled(true);
662        r.push(sample(PointerPhase::Down, 0.0, 0.0, 0));
663        r.push(sample(PointerPhase::Move, 4.0, 0.0, 4_000_000));
664        r.push(sample(PointerPhase::Up, 8.0, 0.0, 8_000_000));
665
666        // Sample instant reaches the Up.
667        let out = drain(&mut r, 8_000_000);
668        let phases: Vec<PointerPhase> = out.iter().map(|e| e.phase).collect();
669        assert_eq!(
670            phases,
671            vec![PointerPhase::Down, PointerPhase::Move, PointerPhase::Up]
672        );
673        // Down and Up keep their raw positions (only Move is resampled).
674        assert_eq!(out.first().unwrap().position, Point::new(0.0, 0.0));
675        assert_eq!(out.last().unwrap().position, Point::new(8.0, 0.0));
676        assert!(!r.has_pending());
677    }
678
679    #[test]
680    fn cancel_passes_through_and_ends_the_gesture() {
681        let mut r = PointerResampler::with_enabled(true);
682        r.push(sample(PointerPhase::Down, 1.0, 1.0, 0));
683        r.push(sample(PointerPhase::Cancel, 1.0, 1.0, 2_000_000));
684        let out = drain(&mut r, 2_000_000);
685        assert_eq!(out.len(), 2);
686        assert_eq!(out[1].phase, PointerPhase::Cancel);
687        assert_eq!(out[1].position, Point::new(1.0, 1.0));
688        assert!(!r.has_pending());
689    }
690
691    #[test]
692    fn too_new_transition_stays_buffered_until_its_instant_arrives() {
693        let mut r = PointerResampler::with_enabled(true);
694        r.push(sample(PointerPhase::Down, 0.0, 0.0, 0));
695        r.push(sample(PointerPhase::Up, 0.0, 0.0, 10_000_000));
696
697        // Instant only reaches the Down — the Up is not yet due, never dropped.
698        let out = drain(&mut r, 0);
699        assert_eq!(out.len(), 1);
700        assert_eq!(out[0].phase, PointerPhase::Down);
701        assert!(r.has_pending());
702
703        // A later frame whose instant reaches the Up delivers it.
704        let out = drain(&mut r, 10_000_000);
705        assert_eq!(out.len(), 1);
706        assert_eq!(out[0].phase, PointerPhase::Up);
707        assert!(!r.has_pending());
708    }
709
710    // -----------------------------------------------------------------
711    // Prediction clamp
712    // -----------------------------------------------------------------
713
714    #[test]
715    fn prediction_extrapolates_within_the_window() {
716        let mut r = PointerResampler::with_enabled(true);
717        // 10px over 10ms → 1px/ms. Sample 4ms past the newest → +4px (< window).
718        r.push(sample(PointerPhase::Move, 0.0, 0.0, 0));
719        r.push(sample(PointerPhase::Move, 10.0, 0.0, 10_000_000));
720        let out = drain(&mut r, 14_000_000);
721        assert_eq!(out.len(), 1);
722        assert_eq!(out[0].position, Point::new(14.0, 0.0));
723        assert!(!r.has_pending(), "both samples were at/before the instant");
724    }
725
726    #[test]
727    fn prediction_is_clamped_to_the_half_frame_window() {
728        let mut r = PointerResampler::with_enabled(true);
729        // 10px/10ms again, but sample far past the newest (50ms ahead): the
730        // extrapolation clamps at PREDICTION_WINDOW_NANOS (~8.33ms → +8.33px),
731        // NOT the full 50px an unclamped predictor would give.
732        r.push(sample(PointerPhase::Move, 0.0, 0.0, 0));
733        r.push(sample(PointerPhase::Move, 10.0, 0.0, 10_000_000));
734        let out = drain(&mut r, 60_000_000);
735        assert_eq!(out.len(), 1);
736        let predicted_x = out[0].position.x;
737        let expected = 10.0 + PREDICTION_WINDOW_NANOS as f64 / 1_000_000.0;
738        assert!(
739            (predicted_x - expected).abs() < 1e-6,
740            "clamped prediction {predicted_x} should be {expected}"
741        );
742    }
743
744    // -----------------------------------------------------------------
745    // Empty / one-sample edge cases
746    // -----------------------------------------------------------------
747
748    #[test]
749    fn empty_queue_resamples_to_nothing() {
750        let mut r = PointerResampler::with_enabled(true);
751        let out = drain(&mut r, 1_000_000);
752        assert!(out.is_empty());
753        assert!(!r.has_pending());
754    }
755
756    #[test]
757    fn single_move_sample_holds_its_position() {
758        let mut r = PointerResampler::with_enabled(true);
759        // One move; the instant is past it → predict_forward with no prior
760        // sample holds at the sample's own position.
761        r.push(sample(PointerPhase::Move, 7.0, 3.0, 0));
762        let out = drain(&mut r, 5_000_000);
763        assert_eq!(out.len(), 1);
764        assert_eq!(out[0].position, Point::new(7.0, 3.0));
765    }
766
767    #[test]
768    fn move_before_first_sample_instant_stays_buffered() {
769        let mut r = PointerResampler::with_enabled(true);
770        // The only sample is newer than the instant → nothing drains yet.
771        r.push(sample(PointerPhase::Move, 2.0, 2.0, 10_000_000));
772        let out = drain(&mut r, 0);
773        assert!(out.is_empty());
774        assert!(r.has_pending());
775    }
776
777    #[test]
778    fn stationary_finger_does_not_re_emit_the_same_move() {
779        let mut r = PointerResampler::with_enabled(true);
780        r.push(sample(PointerPhase::Down, 5.0, 5.0, 0));
781        // Two moves at the same position → the coalesced move equals the Down's
782        // recorded position, so no redundant Move is emitted.
783        r.push(sample(PointerPhase::Move, 5.0, 5.0, 2_000_000));
784        r.push(sample(PointerPhase::Move, 5.0, 5.0, 4_000_000));
785        let out = drain(&mut r, 4_000_000);
786        assert_eq!(out.len(), 1, "only the Down; the no-op moves are deduped");
787        assert_eq!(out[0].phase, PointerPhase::Down);
788    }
789
790    // -----------------------------------------------------------------
791    // Per-contact lanes
792    // -----------------------------------------------------------------
793
794    #[test]
795    fn two_interleaved_contacts_resample_in_separate_lanes() {
796        let a = PointerId::touch(0);
797        let b = PointerId::touch(1);
798        let mut r = PointerResampler::with_enabled(true);
799        // Two fingers moving in opposite directions, samples interleaved in
800        // arrival order. A shared lane would interpolate between the two
801        // fingers' positions; separate lanes keep each on its own path.
802        r.push(sample_for(a, PointerPhase::Down, 0.0, 0.0, 0));
803        r.push(sample_for(b, PointerPhase::Down, 100.0, 0.0, 1_000_000));
804        r.push(sample_for(a, PointerPhase::Move, 10.0, 0.0, 10_000_000));
805        r.push(sample_for(b, PointerPhase::Move, 90.0, 0.0, 11_000_000));
806        r.push(sample_for(a, PointerPhase::Move, 20.0, 0.0, 20_000_000));
807        r.push(sample_for(b, PointerPhase::Move, 80.0, 0.0, 21_000_000));
808
809        // Sample at 15ms: lane a interpolates halfway between 10 and 20; lane b
810        // between its own 11ms/21ms samples (90 → 80, 40% of the way).
811        let out = drain_tagged(&mut r, 15_000_000);
812        let ids: Vec<PointerId> = out.iter().map(|e| e.pointer_id).collect();
813        let phases: Vec<PointerPhase> = out.iter().map(|e| e.event.phase).collect();
814        assert_eq!(ids, vec![a, b, a, b], "merged back into arrival order");
815        assert_eq!(
816            phases,
817            vec![
818                PointerPhase::Down,
819                PointerPhase::Down,
820                PointerPhase::Move,
821                PointerPhase::Move
822            ]
823        );
824        assert_eq!(out[0].event.position, Point::new(0.0, 0.0));
825        assert_eq!(out[1].event.position, Point::new(100.0, 0.0));
826        assert_eq!(out[2].event.position, Point::new(15.0, 0.0));
827        let b_x = out[3].event.position.x;
828        assert!((b_x - 86.0).abs() < 1e-9, "lane b moved to {b_x}, not 86");
829        assert!(r.has_pending(), "both lanes still hold a too-new sample");
830    }
831
832    #[test]
833    fn a_lane_ends_on_its_own_up_without_ending_the_other() {
834        let a = PointerId::touch(0);
835        let b = PointerId::touch(1);
836        let mut r = PointerResampler::with_enabled(true);
837        r.push(sample_for(a, PointerPhase::Down, 0.0, 0.0, 0));
838        r.push(sample_for(b, PointerPhase::Down, 50.0, 50.0, 1_000_000));
839        r.push(sample_for(b, PointerPhase::Up, 50.0, 50.0, 2_000_000));
840        let out = drain_tagged(&mut r, 2_000_000);
841        assert_eq!(out.len(), 3);
842        assert_eq!(
843            (out[2].pointer_id, out[2].event.phase),
844            (b, PointerPhase::Up)
845        );
846        assert_eq!(r.lanes.len(), 1, "b's lane retired on its Up; a's lives on");
847        assert_eq!(r.lanes[0].pointer_id, a);
848
849        // Lane a still holds its own position: a later move interpolates from
850        // a's Down, never from b's.
851        r.push(sample_for(a, PointerPhase::Move, 10.0, 0.0, 10_000_000));
852        let out = drain_tagged(&mut r, 10_000_000);
853        assert_eq!(out.len(), 1);
854        assert_eq!(out[0].pointer_id, a);
855        assert_eq!(out[0].event.position, Point::new(10.0, 0.0));
856
857        r.push(sample_for(a, PointerPhase::Cancel, 10.0, 0.0, 12_000_000));
858        let out = drain_tagged(&mut r, 12_000_000);
859        assert_eq!(out[0].event.phase, PointerPhase::Cancel);
860        assert!(r.lanes.is_empty(), "a's lane retired on its Cancel");
861    }
862
863    #[test]
864    fn disabled_resampler_keeps_arrival_order_across_contacts() {
865        let a = PointerId::touch(0);
866        let b = PointerId::touch(1);
867        let mut r = PointerResampler::disabled();
868        r.push(sample_for(a, PointerPhase::Down, 0.0, 0.0, 0));
869        r.push(sample_for(b, PointerPhase::Down, 9.0, 9.0, 1));
870        r.push(sample_for(a, PointerPhase::Move, 1.0, 0.0, 2));
871        r.push(sample_for(b, PointerPhase::Up, 9.0, 9.0, 3));
872        let out = drain_tagged(&mut r, 0);
873        let order: Vec<(PointerId, PointerPhase)> =
874            out.iter().map(|e| (e.pointer_id, e.event.phase)).collect();
875        assert_eq!(
876            order,
877            vec![
878                (a, PointerPhase::Down),
879                (b, PointerPhase::Down),
880                (a, PointerPhase::Move),
881                (b, PointerPhase::Up),
882            ]
883        );
884        assert!(!r.has_pending());
885    }
886
887    // -----------------------------------------------------------------
888    // Deadline helpers
889    // -----------------------------------------------------------------
890
891    #[test]
892    fn frame_interval_uses_plausible_tick_delta() {
893        // A clean 60Hz tick delta passes through.
894        assert_eq!(frame_interval_nanos(Some(0), 16_666_667), 16_666_667);
895    }
896
897    #[test]
898    fn frame_interval_falls_back_on_no_prior_or_implausible_delta() {
899        assert_eq!(
900            frame_interval_nanos(None, 1_000),
901            DEFAULT_REFRESH_INTERVAL_NANOS
902        );
903        // Non-monotonic / equal ticks → fallback.
904        assert_eq!(
905            frame_interval_nanos(Some(100), 100),
906            DEFAULT_REFRESH_INTERVAL_NANOS
907        );
908        // Too small (sub-1ms) and too large (>100ms idle) → fallback.
909        assert_eq!(
910            frame_interval_nanos(Some(0), 500),
911            DEFAULT_REFRESH_INTERVAL_NANOS
912        );
913        assert_eq!(
914            frame_interval_nanos(Some(0), 200_000_000),
915            DEFAULT_REFRESH_INTERVAL_NANOS
916        );
917    }
918
919    #[test]
920    fn deadline_overrun_compares_work_to_budget() {
921        let budget = 16_666_667;
922        assert!(deadline_overrun(Duration::from_millis(20), budget));
923        assert!(!deadline_overrun(Duration::from_millis(10), budget));
924        // Exactly at budget is not an overrun.
925        assert!(!deadline_overrun(Duration::from_nanos(budget), budget));
926    }
927}