use std::collections::VecDeque;
use std::time::Duration;
use frust_core::event::{PointerButton, PointerEvent, PointerId, PointerPhase};
use kurbo::Point;
pub const NO_RESAMPLE_VAR: &str = "FRUST_NO_RESAMPLE";
pub const SAMPLE_OFFSET_NANOS: u64 = 5_000_000;
pub const PREDICTION_WINDOW_NANOS: u64 = 8_333_333;
pub const DEFAULT_REFRESH_INTERVAL_NANOS: u64 = 16_666_667;
pub const MIN_PLAUSIBLE_INTERVAL_NANOS: u64 = 1_000_000;
pub const MAX_PLAUSIBLE_INTERVAL_NANOS: u64 = 100_000_000;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RawPointerSample {
pub pointer_id: PointerId,
pub phase: PointerPhase,
pub position: Point,
pub button: PointerButton,
pub time_nanos: u64,
}
impl RawPointerSample {
fn as_event(&self) -> PointerEvent {
PointerEvent {
phase: self.phase,
position: self.position,
button: self.button,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ResampledPointer {
pub pointer_id: PointerId,
pub event: PointerEvent,
}
#[derive(Debug, Clone, Copy)]
struct Queued {
seq: u64,
sample: RawPointerSample,
}
#[derive(Debug)]
struct Lane {
pointer_id: PointerId,
queue: VecDeque<Queued>,
last_emitted: Option<Point>,
}
type Keyed = ((u64, u64), ResampledPointer);
#[derive(Debug)]
pub struct PointerResampler {
enabled: bool,
lanes: Vec<Lane>,
next_seq: u64,
merge: Vec<Keyed>,
}
impl PointerResampler {
pub fn new() -> Self {
Self::with_enabled(!kill_switch_engaged())
}
pub fn disabled() -> Self {
Self::with_enabled(false)
}
pub fn with_enabled(enabled: bool) -> Self {
Self {
enabled,
lanes: Vec::new(),
next_seq: 0,
merge: Vec::new(),
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn has_pending(&self) -> bool {
self.lanes.iter().any(|lane| !lane.queue.is_empty())
}
pub fn push(&mut self, sample: RawPointerSample) {
let seq = self.next_seq;
self.next_seq = self.next_seq.wrapping_add(1);
let queued = Queued { seq, sample };
match self
.lanes
.iter_mut()
.find(|lane| lane.pointer_id == sample.pointer_id)
{
Some(lane) => lane.queue.push_back(queued),
None => self.lanes.push(Lane {
pointer_id: sample.pointer_id,
queue: VecDeque::from([queued]),
last_emitted: None,
}),
}
}
pub fn resample(&mut self, frame_time_nanos: u64, out: &mut Vec<ResampledPointer>) {
if !self.enabled {
self.merge.clear();
for lane in &mut self.lanes {
self.merge.extend(lane.queue.drain(..).map(|queued| {
(
(0, queued.seq),
ResampledPointer {
pointer_id: queued.sample.pointer_id,
event: queued.sample.as_event(),
},
)
}));
}
self.merge.sort_by_key(|(key, _)| *key);
out.extend(self.merge.drain(..).map(|(_, event)| event));
self.lanes.clear();
return;
}
let sample_time = frame_time_nanos.saturating_sub(SAMPLE_OFFSET_NANOS);
self.merge.clear();
for lane in &mut self.lanes {
lane.resample(sample_time, &mut self.merge);
}
if self.lanes.len() > 1 {
self.merge.sort_by_key(|(key, _)| *key);
}
out.extend(self.merge.drain(..).map(|(_, event)| event));
self.lanes
.retain(|lane| !lane.queue.is_empty() || lane.last_emitted.is_some());
}
}
impl Lane {
fn resample(&mut self, sample_time: u64, out: &mut Vec<Keyed>) {
let sample_pos = self.position_at(sample_time);
let mut pending_move: Option<Queued> = None;
while let Some(time_nanos) = self.queue.front().map(|q| q.sample.time_nanos) {
if time_nanos > sample_time {
break; }
let queued = self.queue.pop_front().expect("front was just observed");
match queued.sample.phase {
PointerPhase::Move => {
pending_move = Some(queued);
}
PointerPhase::Down => {
self.flush_move(pending_move.take(), sample_pos, out);
self.emit(queued, out);
self.last_emitted = Some(queued.sample.position);
}
PointerPhase::Up | PointerPhase::Cancel => {
self.flush_move(pending_move.take(), sample_pos, out);
self.emit(queued, out);
self.last_emitted = None; }
}
}
self.flush_move(pending_move, sample_pos, out);
}
fn emit(&self, queued: Queued, out: &mut Vec<Keyed>) {
out.push((
(queued.sample.time_nanos, queued.seq),
ResampledPointer {
pointer_id: self.pointer_id,
event: queued.sample.as_event(),
},
));
}
fn flush_move(
&mut self,
last: Option<Queued>,
sample_pos: Option<Point>,
out: &mut Vec<Keyed>,
) {
let Some(last) = last else {
return;
};
if let Some(pos) = sample_pos
&& self.last_emitted != Some(pos)
{
out.push((
(last.sample.time_nanos, last.seq),
ResampledPointer {
pointer_id: self.pointer_id,
event: PointerEvent {
phase: PointerPhase::Move,
position: pos,
button: last.sample.button,
},
},
));
self.last_emitted = Some(pos);
}
}
fn position_at(&self, sample_time: u64) -> Option<Point> {
if self.queue.is_empty() {
return self.last_emitted;
}
let mut before: Option<&RawPointerSample> = None;
let mut after: Option<&RawPointerSample> = None;
for queued in &self.queue {
let sample = &queued.sample;
if sample.time_nanos <= sample_time {
before = Some(sample);
} else {
after = Some(sample);
break;
}
}
match (before, after) {
(Some(a), Some(b)) => Some(lerp_point(
a.position,
b.position,
fraction(a.time_nanos, b.time_nanos, sample_time),
)),
(Some(a), None) => Some(self.predict_forward(a, sample_time)),
(None, Some(b)) => self.last_emitted.or(Some(b.position)),
(None, None) => self.last_emitted,
}
}
fn predict_forward(&self, newest: &RawPointerSample, sample_time: u64) -> Point {
match self.queue.iter().rev().nth(1).map(|queued| &queued.sample) {
Some(prior) if newest.time_nanos > prior.time_nanos => {
let ahead = (sample_time - newest.time_nanos).min(PREDICTION_WINDOW_NANOS);
let span = newest.time_nanos - prior.time_nanos;
let t = ahead as f64 / span as f64;
Point::new(
newest.position.x + (newest.position.x - prior.position.x) * t,
newest.position.y + (newest.position.y - prior.position.y) * t,
)
}
_ => newest.position,
}
}
}
impl Default for PointerResampler {
fn default() -> Self {
Self::new()
}
}
fn fraction(a: u64, b: u64, t: u64) -> f64 {
let span = b.saturating_sub(a);
if span == 0 {
return 1.0;
}
(t.saturating_sub(a)) as f64 / span as f64
}
fn lerp_point(a: Point, b: Point, t: f64) -> Point {
Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
}
pub fn frame_interval_nanos(prev_tick: Option<u64>, cur_tick: u64) -> u64 {
match prev_tick {
Some(prev) if cur_tick > prev => {
let delta = cur_tick - prev;
if (MIN_PLAUSIBLE_INTERVAL_NANOS..=MAX_PLAUSIBLE_INTERVAL_NANOS).contains(&delta) {
delta
} else {
DEFAULT_REFRESH_INTERVAL_NANOS
}
}
_ => DEFAULT_REFRESH_INTERVAL_NANOS,
}
}
pub fn deadline_overrun(work: Duration, budget_nanos: u64) -> bool {
(work.as_nanos() as u64) > budget_nanos
}
fn kill_switch_engaged() -> bool {
kill_switch(
option_env!("FRUST_NO_RESAMPLE"),
std::env::var(NO_RESAMPLE_VAR).ok().as_deref(),
)
}
fn kill_switch(compile_time: Option<&str>, runtime: Option<&str>) -> bool {
fn is_set_non_zero(value: Option<&str>) -> bool {
matches!(value, Some(v) if v != "0")
}
is_set_non_zero(compile_time) || is_set_non_zero(runtime)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(phase: PointerPhase, x: f64, y: f64, time_nanos: u64) -> RawPointerSample {
sample_for(PointerId::touch(0), phase, x, y, time_nanos)
}
fn sample_for(
pointer_id: PointerId,
phase: PointerPhase,
x: f64,
y: f64,
time_nanos: u64,
) -> RawPointerSample {
RawPointerSample {
pointer_id,
phase,
position: Point::new(x, y),
button: PointerButton::Primary,
time_nanos,
}
}
fn frame_time_for(sample_time: u64) -> u64 {
sample_time + SAMPLE_OFFSET_NANOS
}
fn drain_tagged(resampler: &mut PointerResampler, sample_time: u64) -> Vec<ResampledPointer> {
let mut out = Vec::new();
resampler.resample(frame_time_for(sample_time), &mut out);
out
}
fn drain(resampler: &mut PointerResampler, sample_time: u64) -> Vec<PointerEvent> {
drain_tagged(resampler, sample_time)
.into_iter()
.map(|r| r.event)
.collect()
}
#[test]
fn kill_switch_off_when_neither_set() {
assert!(!kill_switch(None, None));
}
#[test]
fn kill_switch_on_when_either_source_wins() {
assert!(kill_switch(Some("1"), None));
assert!(kill_switch(None, Some("1")));
assert!(kill_switch(Some("0"), Some("1")));
assert!(kill_switch(Some("1"), Some("0")));
}
#[test]
fn kill_switch_off_when_either_is_literal_zero_and_other_unset() {
assert!(!kill_switch(Some("0"), None));
assert!(!kill_switch(None, Some("0")));
}
#[test]
fn disabled_resampler_delivers_every_sample_verbatim_in_order() {
let mut r = PointerResampler::disabled();
assert!(!r.is_enabled());
r.push(sample(PointerPhase::Down, 1.0, 2.0, 0));
r.push(sample(PointerPhase::Move, 3.0, 4.0, 5));
r.push(sample(PointerPhase::Up, 5.0, 6.0, 10));
let out = drain(&mut r, 0);
assert_eq!(out.len(), 3);
assert_eq!(out[0].phase, PointerPhase::Down);
assert_eq!(out[0].position, Point::new(1.0, 2.0));
assert_eq!(out[1].phase, PointerPhase::Move);
assert_eq!(out[1].position, Point::new(3.0, 4.0));
assert_eq!(out[2].phase, PointerPhase::Up);
assert_eq!(out[2].position, Point::new(5.0, 6.0));
assert!(!r.has_pending());
}
#[test]
fn move_position_interpolated_between_bracketing_samples() {
let mut r = PointerResampler::with_enabled(true);
r.push(sample(PointerPhase::Move, 0.0, 0.0, 0));
r.push(sample(PointerPhase::Move, 10.0, 0.0, 10_000_000));
let out = drain(&mut r, 5_000_000);
assert_eq!(out.len(), 1);
assert_eq!(out[0].phase, PointerPhase::Move);
assert_eq!(out[0].position, Point::new(5.0, 0.0));
assert!(r.has_pending());
}
#[test]
fn interpolation_fraction_is_time_weighted() {
let mut r = PointerResampler::with_enabled(true);
r.push(sample(PointerPhase::Move, 0.0, 0.0, 0));
r.push(sample(PointerPhase::Move, 100.0, 40.0, 10_000_000));
let out = drain(&mut r, 2_500_000);
assert_eq!(out[0].position, Point::new(25.0, 10.0));
}
#[test]
fn down_move_up_pass_transitions_through_losslessly() {
let mut r = PointerResampler::with_enabled(true);
r.push(sample(PointerPhase::Down, 0.0, 0.0, 0));
r.push(sample(PointerPhase::Move, 4.0, 0.0, 4_000_000));
r.push(sample(PointerPhase::Up, 8.0, 0.0, 8_000_000));
let out = drain(&mut r, 8_000_000);
let phases: Vec<PointerPhase> = out.iter().map(|e| e.phase).collect();
assert_eq!(
phases,
vec![PointerPhase::Down, PointerPhase::Move, PointerPhase::Up]
);
assert_eq!(out.first().unwrap().position, Point::new(0.0, 0.0));
assert_eq!(out.last().unwrap().position, Point::new(8.0, 0.0));
assert!(!r.has_pending());
}
#[test]
fn cancel_passes_through_and_ends_the_gesture() {
let mut r = PointerResampler::with_enabled(true);
r.push(sample(PointerPhase::Down, 1.0, 1.0, 0));
r.push(sample(PointerPhase::Cancel, 1.0, 1.0, 2_000_000));
let out = drain(&mut r, 2_000_000);
assert_eq!(out.len(), 2);
assert_eq!(out[1].phase, PointerPhase::Cancel);
assert_eq!(out[1].position, Point::new(1.0, 1.0));
assert!(!r.has_pending());
}
#[test]
fn too_new_transition_stays_buffered_until_its_instant_arrives() {
let mut r = PointerResampler::with_enabled(true);
r.push(sample(PointerPhase::Down, 0.0, 0.0, 0));
r.push(sample(PointerPhase::Up, 0.0, 0.0, 10_000_000));
let out = drain(&mut r, 0);
assert_eq!(out.len(), 1);
assert_eq!(out[0].phase, PointerPhase::Down);
assert!(r.has_pending());
let out = drain(&mut r, 10_000_000);
assert_eq!(out.len(), 1);
assert_eq!(out[0].phase, PointerPhase::Up);
assert!(!r.has_pending());
}
#[test]
fn prediction_extrapolates_within_the_window() {
let mut r = PointerResampler::with_enabled(true);
r.push(sample(PointerPhase::Move, 0.0, 0.0, 0));
r.push(sample(PointerPhase::Move, 10.0, 0.0, 10_000_000));
let out = drain(&mut r, 14_000_000);
assert_eq!(out.len(), 1);
assert_eq!(out[0].position, Point::new(14.0, 0.0));
assert!(!r.has_pending(), "both samples were at/before the instant");
}
#[test]
fn prediction_is_clamped_to_the_half_frame_window() {
let mut r = PointerResampler::with_enabled(true);
r.push(sample(PointerPhase::Move, 0.0, 0.0, 0));
r.push(sample(PointerPhase::Move, 10.0, 0.0, 10_000_000));
let out = drain(&mut r, 60_000_000);
assert_eq!(out.len(), 1);
let predicted_x = out[0].position.x;
let expected = 10.0 + PREDICTION_WINDOW_NANOS as f64 / 1_000_000.0;
assert!(
(predicted_x - expected).abs() < 1e-6,
"clamped prediction {predicted_x} should be {expected}"
);
}
#[test]
fn empty_queue_resamples_to_nothing() {
let mut r = PointerResampler::with_enabled(true);
let out = drain(&mut r, 1_000_000);
assert!(out.is_empty());
assert!(!r.has_pending());
}
#[test]
fn single_move_sample_holds_its_position() {
let mut r = PointerResampler::with_enabled(true);
r.push(sample(PointerPhase::Move, 7.0, 3.0, 0));
let out = drain(&mut r, 5_000_000);
assert_eq!(out.len(), 1);
assert_eq!(out[0].position, Point::new(7.0, 3.0));
}
#[test]
fn move_before_first_sample_instant_stays_buffered() {
let mut r = PointerResampler::with_enabled(true);
r.push(sample(PointerPhase::Move, 2.0, 2.0, 10_000_000));
let out = drain(&mut r, 0);
assert!(out.is_empty());
assert!(r.has_pending());
}
#[test]
fn stationary_finger_does_not_re_emit_the_same_move() {
let mut r = PointerResampler::with_enabled(true);
r.push(sample(PointerPhase::Down, 5.0, 5.0, 0));
r.push(sample(PointerPhase::Move, 5.0, 5.0, 2_000_000));
r.push(sample(PointerPhase::Move, 5.0, 5.0, 4_000_000));
let out = drain(&mut r, 4_000_000);
assert_eq!(out.len(), 1, "only the Down; the no-op moves are deduped");
assert_eq!(out[0].phase, PointerPhase::Down);
}
#[test]
fn two_interleaved_contacts_resample_in_separate_lanes() {
let a = PointerId::touch(0);
let b = PointerId::touch(1);
let mut r = PointerResampler::with_enabled(true);
r.push(sample_for(a, PointerPhase::Down, 0.0, 0.0, 0));
r.push(sample_for(b, PointerPhase::Down, 100.0, 0.0, 1_000_000));
r.push(sample_for(a, PointerPhase::Move, 10.0, 0.0, 10_000_000));
r.push(sample_for(b, PointerPhase::Move, 90.0, 0.0, 11_000_000));
r.push(sample_for(a, PointerPhase::Move, 20.0, 0.0, 20_000_000));
r.push(sample_for(b, PointerPhase::Move, 80.0, 0.0, 21_000_000));
let out = drain_tagged(&mut r, 15_000_000);
let ids: Vec<PointerId> = out.iter().map(|e| e.pointer_id).collect();
let phases: Vec<PointerPhase> = out.iter().map(|e| e.event.phase).collect();
assert_eq!(ids, vec![a, b, a, b], "merged back into arrival order");
assert_eq!(
phases,
vec![
PointerPhase::Down,
PointerPhase::Down,
PointerPhase::Move,
PointerPhase::Move
]
);
assert_eq!(out[0].event.position, Point::new(0.0, 0.0));
assert_eq!(out[1].event.position, Point::new(100.0, 0.0));
assert_eq!(out[2].event.position, Point::new(15.0, 0.0));
let b_x = out[3].event.position.x;
assert!((b_x - 86.0).abs() < 1e-9, "lane b moved to {b_x}, not 86");
assert!(r.has_pending(), "both lanes still hold a too-new sample");
}
#[test]
fn a_lane_ends_on_its_own_up_without_ending_the_other() {
let a = PointerId::touch(0);
let b = PointerId::touch(1);
let mut r = PointerResampler::with_enabled(true);
r.push(sample_for(a, PointerPhase::Down, 0.0, 0.0, 0));
r.push(sample_for(b, PointerPhase::Down, 50.0, 50.0, 1_000_000));
r.push(sample_for(b, PointerPhase::Up, 50.0, 50.0, 2_000_000));
let out = drain_tagged(&mut r, 2_000_000);
assert_eq!(out.len(), 3);
assert_eq!(
(out[2].pointer_id, out[2].event.phase),
(b, PointerPhase::Up)
);
assert_eq!(r.lanes.len(), 1, "b's lane retired on its Up; a's lives on");
assert_eq!(r.lanes[0].pointer_id, a);
r.push(sample_for(a, PointerPhase::Move, 10.0, 0.0, 10_000_000));
let out = drain_tagged(&mut r, 10_000_000);
assert_eq!(out.len(), 1);
assert_eq!(out[0].pointer_id, a);
assert_eq!(out[0].event.position, Point::new(10.0, 0.0));
r.push(sample_for(a, PointerPhase::Cancel, 10.0, 0.0, 12_000_000));
let out = drain_tagged(&mut r, 12_000_000);
assert_eq!(out[0].event.phase, PointerPhase::Cancel);
assert!(r.lanes.is_empty(), "a's lane retired on its Cancel");
}
#[test]
fn disabled_resampler_keeps_arrival_order_across_contacts() {
let a = PointerId::touch(0);
let b = PointerId::touch(1);
let mut r = PointerResampler::disabled();
r.push(sample_for(a, PointerPhase::Down, 0.0, 0.0, 0));
r.push(sample_for(b, PointerPhase::Down, 9.0, 9.0, 1));
r.push(sample_for(a, PointerPhase::Move, 1.0, 0.0, 2));
r.push(sample_for(b, PointerPhase::Up, 9.0, 9.0, 3));
let out = drain_tagged(&mut r, 0);
let order: Vec<(PointerId, PointerPhase)> =
out.iter().map(|e| (e.pointer_id, e.event.phase)).collect();
assert_eq!(
order,
vec![
(a, PointerPhase::Down),
(b, PointerPhase::Down),
(a, PointerPhase::Move),
(b, PointerPhase::Up),
]
);
assert!(!r.has_pending());
}
#[test]
fn frame_interval_uses_plausible_tick_delta() {
assert_eq!(frame_interval_nanos(Some(0), 16_666_667), 16_666_667);
}
#[test]
fn frame_interval_falls_back_on_no_prior_or_implausible_delta() {
assert_eq!(
frame_interval_nanos(None, 1_000),
DEFAULT_REFRESH_INTERVAL_NANOS
);
assert_eq!(
frame_interval_nanos(Some(100), 100),
DEFAULT_REFRESH_INTERVAL_NANOS
);
assert_eq!(
frame_interval_nanos(Some(0), 500),
DEFAULT_REFRESH_INTERVAL_NANOS
);
assert_eq!(
frame_interval_nanos(Some(0), 200_000_000),
DEFAULT_REFRESH_INTERVAL_NANOS
);
}
#[test]
fn deadline_overrun_compares_work_to_budget() {
let budget = 16_666_667;
assert!(deadline_overrun(Duration::from_millis(20), budget));
assert!(!deadline_overrun(Duration::from_millis(10), budget));
assert!(!deadline_overrun(Duration::from_nanos(budget), budget));
}
}