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

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.
This commit is contained in:
Antonio Scandurra
2025-12-19 16:06:28 +01:00
committed by GitHub
parent 7427924405
commit b603372f44

View File

@@ -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,