From d359a2048f1ac96a388c4c8f3b3193b7fe93bfce Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" Date: Wed, 25 Mar 2026 09:38:07 +0000 Subject: [PATCH] smooth scroll speed estimation to reduce red-dot spikes --- daemon/src/main.rs | 57 +++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 0d98024..84ca94b 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -40,11 +40,29 @@ struct Args { no_grab: bool, } +#[derive(Debug, Default)] +struct AxisRuntime { + last_event_at: Option, + remainder: f64, + smoothed_speed: f64, +} + #[derive(Debug, Default)] struct ScrollState { - last_scroll_event_at: Option, - remainder_v: f64, - remainder_h: f64, + vertical: AxisRuntime, + horizontal: AxisRuntime, +} + +const SPEED_EMA_ALPHA: f64 = 0.25; +const MIN_DT_SECONDS: f64 = 0.004; +const SPEED_IDLE_RESET_SECONDS: f64 = 0.2; + +fn axis_runtime_mut(state: &mut ScrollState, axis: RelativeAxisCode) -> &mut AxisRuntime { + if axis == RelativeAxisCode::REL_HWHEEL || axis == RelativeAxisCode::REL_HWHEEL_HI_RES { + &mut state.horizontal + } else { + &mut state.vertical + } } fn main() -> anyhow::Result<()> { @@ -269,13 +287,14 @@ fn transform_event( { return None; } + let axis_state = axis_runtime_mut(state, axis); let now = Instant::now(); - let dt = state - .last_scroll_event_at + let dt = axis_state + .last_event_at .map(|t| now.saturating_duration_since(t).as_secs_f64()) .unwrap_or(0.016) - .max(0.001); - state.last_scroll_event_at = Some(now); + .max(MIN_DT_SECONDS); + axis_state.last_event_at = Some(now); let axis_unit = if axis == RelativeAxisCode::REL_WHEEL_HI_RES || axis == RelativeAxisCode::REL_HWHEEL_HI_RES @@ -288,7 +307,17 @@ fn transform_event( // Normalize hi-res wheel units (120 per detent) to "detent-equivalent" // speed so tuning behaves similarly across REL_WHEEL and REL_WHEEL_HI_RES. let normalized_value = (value as f64) / axis_unit; - let speed = normalized_value.abs() / dt; + let raw_speed = normalized_value.abs() / dt; + + // Smooth out bursty evdev timing so the live indicator and accel input + // don't jump wildly on single tightly-packed events. + let speed = if dt > SPEED_IDLE_RESET_SECONDS { + raw_speed + } else { + SPEED_EMA_ALPHA * raw_speed + (1.0 - SPEED_EMA_ALPHA) * axis_state.smoothed_speed + }; + axis_state.smoothed_speed = speed; + let (gain_x, gain_y) = sensitivity(speed, *mode, params); let gain = if axis == RelativeAxisCode::REL_HWHEEL || axis == RelativeAxisCode::REL_HWHEEL_HI_RES @@ -298,17 +327,9 @@ fn transform_event( gain_y }; - let remainder = if axis == RelativeAxisCode::REL_HWHEEL - || axis == RelativeAxisCode::REL_HWHEEL_HI_RES - { - &mut state.remainder_h - } else { - &mut state.remainder_v - }; - - let scaled = (value as f64) * gain + *remainder; + let scaled = (value as f64) * gain + axis_state.remainder; let quantized = scaled.trunc() as i32; - *remainder = scaled - (quantized as f64); + axis_state.remainder = scaled - (quantized as f64); let _ = write_speed_stat(speed);