From b603372f44a60f8ed98ae95b36094582ead47b89 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 19 Dec 2025 16:06:28 +0100 Subject: [PATCH] Reduce GPU usage by activating VRR optimization only during high-rate input (#45369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #29073 This PR reduces unnecessary GPU usage by being more selective about when we present frames to prevent display underclocking (VRR optimization). ## Problem Previously, we would keep presenting frames for 1 second after *any* input event, regardless of whether it triggered a re-render. This caused unnecessary GPU work when the user was idle or during low-frequency interactions. ## Solution 1. **Only track input that triggers re-renders**: We now only record input timestamps when the input actually causes the window to become dirty, rather than on every input event. 2. **Rate-based activation**: The VRR optimization now only activates when input arrives at a high rate (≥ 60fps over the last 100ms). This means casual mouse movements or occasional keystrokes won't trigger continuous frame presentation. 3. **Sustained optimization**: Once high-rate input is detected (e.g., during scrolling or dragging), we sustain frame presentation for 1 second to prevent display underclocking, even if input briefly pauses. ## Implementation Added `InputRateTracker` which: - Tracks input timestamps in a 100ms sliding window - Activates when the window contains ≥ 6 events (60fps × 0.1s) - Extends a `sustain_until` timestamp by 1 second each time high rate is detected Release Notes: - Reduced GPU usage when idle by only presenting frames during bursts of high-frequency input. --- crates/gpui/src/window.rs | 70 +++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 2ccd7edac86bced89048cbe5dbf196d8fbcf95f3..8df421feb968677be0abbb642a7127871881bcf3 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -876,7 +876,9 @@ pub struct Window { active: Rc>, hovered: Rc>, pub(crate) needs_present: Rc>, - pub(crate) last_input_timestamp: Rc>, + /// Tracks recent input event timestamps to determine if input is arriving at a high rate. + /// Used to selectively enable VRR optimization only when input rate exceeds 60fps. + pub(crate) input_rate_tracker: Rc>, last_input_modality: InputModality, pub(crate) refreshing: bool, pub(crate) activation_observers: SubscriberSet<(), AnyObserver>, @@ -897,6 +899,51 @@ struct ModifierState { saw_keystroke: bool, } +/// Tracks input event timestamps to determine if input is arriving at a high rate. +/// Used for selective VRR (Variable Refresh Rate) optimization. +#[derive(Clone, Debug)] +pub(crate) struct InputRateTracker { + timestamps: Vec, + window: Duration, + inputs_per_second: u32, + sustain_until: Instant, + sustain_duration: Duration, +} + +impl Default for InputRateTracker { + fn default() -> Self { + Self { + timestamps: Vec::new(), + window: Duration::from_millis(100), + inputs_per_second: 60, + sustain_until: Instant::now(), + sustain_duration: Duration::from_secs(1), + } + } +} + +impl InputRateTracker { + pub fn record_input(&mut self) { + let now = Instant::now(); + self.timestamps.push(now); + self.prune_old_timestamps(now); + + let min_events = self.inputs_per_second as u128 * self.window.as_millis() / 1000; + if self.timestamps.len() as u128 >= min_events { + self.sustain_until = now + self.sustain_duration; + } + } + + pub fn is_high_rate(&self) -> bool { + Instant::now() < self.sustain_until + } + + fn prune_old_timestamps(&mut self, now: Instant) { + self.timestamps + .retain(|&t| now.duration_since(t) <= self.window); + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum DrawPhase { None, @@ -1047,7 +1094,7 @@ impl Window { let hovered = Rc::new(Cell::new(platform_window.is_hovered())); let needs_present = Rc::new(Cell::new(false)); let next_frame_callbacks: Rc>> = Default::default(); - let last_input_timestamp = Rc::new(Cell::new(Instant::now())); + let input_rate_tracker = Rc::new(RefCell::new(InputRateTracker::default())); platform_window .request_decorations(window_decorations.unwrap_or(WindowDecorations::Server)); @@ -1075,7 +1122,7 @@ impl Window { let active = active.clone(); let needs_present = needs_present.clone(); let next_frame_callbacks = next_frame_callbacks.clone(); - let last_input_timestamp = last_input_timestamp.clone(); + let input_rate_tracker = input_rate_tracker.clone(); move |request_frame_options| { let next_frame_callbacks = next_frame_callbacks.take(); if !next_frame_callbacks.is_empty() { @@ -1088,12 +1135,12 @@ impl Window { .log_err(); } - // Keep presenting the current scene for 1 extra second since the - // last input to prevent the display from underclocking the refresh rate. + // Keep presenting if input was recently arriving at a high rate (>= 60fps). + // Once high-rate input is detected, we sustain presentation for 1 second + // to prevent display underclocking during active input. let needs_present = request_frame_options.require_presentation || needs_present.get() - || (active.get() - && last_input_timestamp.get().elapsed() < Duration::from_secs(1)); + || (active.get() && input_rate_tracker.borrow_mut().is_high_rate()); if invalidator.is_dirty() || request_frame_options.force_render { measure("frame duration", || { @@ -1101,7 +1148,6 @@ impl Window { .update(&mut cx, |_, window, cx| { let arena_clear_needed = window.draw(cx); window.present(); - // drop the arena elements after present to reduce latency arena_clear_needed.clear(); }) .log_err(); @@ -1299,7 +1345,7 @@ impl Window { active, hovered, needs_present, - last_input_timestamp, + input_rate_tracker, last_input_modality: InputModality::Mouse, refreshing: false, activation_observers: SubscriberSet::new(), @@ -3691,8 +3737,6 @@ impl Window { /// Dispatch a mouse or keyboard event on the window. #[profiling::function] pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult { - self.last_input_timestamp.set(Instant::now()); - // Track whether this input was keyboard-based for focus-visible styling self.last_input_modality = match &event { PlatformInput::KeyDown(_) | PlatformInput::ModifiersChanged(_) => { @@ -3793,6 +3837,10 @@ impl Window { self.dispatch_key_event(any_key_event, cx); } + if self.invalidator.is_dirty() { + self.input_rate_tracker.borrow_mut().record_input(); + } + DispatchEventResult { propagate: cx.propagate_event, default_prevented: self.default_prevented,