Reduce GPU usage by activating VRR optimization only during high-rate input (#45369)

Antonio Scandurra created

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.

Change summary

crates/gpui/src/window.rs | 70 ++++++++++++++++++++++++++++++++++------
1 file changed, 59 insertions(+), 11 deletions(-)

Detailed changes

crates/gpui/src/window.rs 🔗

@@ -876,7 +876,9 @@ pub struct Window {
     active: Rc<Cell<bool>>,
     hovered: Rc<Cell<bool>>,
     pub(crate) needs_present: Rc<Cell<bool>>,
-    pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
+    /// 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<RefCell<InputRateTracker>>,
     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<Instant>,
+    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<RefCell<Vec<FrameCallback>>> = 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,