diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 0467c3e4fd8..6c393565d71 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1568,6 +1568,15 @@ impl App { } if self.pending_effects.is_empty() { + // Wake dirtied windows whose demand-driven render loop (Wayland) is parked. + for window in self.windows.values() { + if let Some(window) = window.as_deref() + && window.invalidator.is_dirty() + { + window.platform_window.schedule_frame(); + } + } + self.event_arena.clear(); break; } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 2a37d0b19b2..ca1ed599b01 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -664,7 +664,8 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn on_appearance_changed(&self, callback: Box); fn on_button_layout_changed(&self, _callback: Box) {} fn draw(&self, scene: &Scene); - fn completed_frame(&self) {} + fn completed_frame(&self, _request_next_frame: bool) {} + fn schedule_frame(&self) {} fn sprite_atlas(&self) -> Arc; fn is_subpixel_rendering_supported(&self) -> bool; diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 7059cf863a6..593ab3b4a65 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1493,23 +1493,20 @@ impl Window { if let Some(last_frame) = last_frame_time.get() && now.duration_since(last_frame) < min_interval { - // Must still complete the frame on platforms that require it. - // On Wayland, `surface.frame()` was already called to request the - // next frame callback, so we must call `surface.commit()` (via - // `complete_frame`) or the compositor won't send another callback. + // Deferred by throttling: ask demand-driven platforms to retry. handle - .update(&mut cx, |_, window, _| window.complete_frame()) + .update(&mut cx, |_, window, _| window.complete_frame(true)) .log_err(); return; } } last_frame_time.set(Some(now)); - let next_frame_callbacks = next_frame_callbacks.take(); - if !next_frame_callbacks.is_empty() { + let current_frame_callbacks = next_frame_callbacks.take(); + if !current_frame_callbacks.is_empty() { handle .update(&mut cx, |_, window, cx| { - for callback in next_frame_callbacks { + for callback in current_frame_callbacks { callback(window, cx); } }) @@ -1544,9 +1541,11 @@ impl Window { .log_err(); } + let request_next_frame = + invalidator.is_dirty() || !next_frame_callbacks.borrow().is_empty(); handle .update(&mut cx, |_, window, _| { - window.complete_frame(); + window.complete_frame(request_next_frame); }) .log_err(); } @@ -2191,6 +2190,9 @@ impl Window { /// Schedule the given closure to be run directly after the current frame is rendered. pub fn on_next_frame(&self, callback: impl FnOnce(&mut Window, &mut App) + 'static) { RefCell::borrow_mut(&self.next_frame_callbacks).push(Box::new(callback)); + if self.invalidator.not_drawing() { + self.platform_window.schedule_frame(); + } } /// Schedule a frame to be drawn on the next animation frame. @@ -2628,8 +2630,8 @@ impl Window { self.capslock } - fn complete_frame(&self) { - self.platform_window.completed_frame(); + fn complete_frame(&self, request_next_frame: bool) { + self.platform_window.completed_frame(request_next_frame); } /// Produces a new frame and assigns it to `rendered_frame`. To actually show diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index a4cc9fde9f1..1c52c25f7f1 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -10,6 +10,7 @@ use std::{ use ashpd::WindowIdentifier; use calloop::{ EventLoop, LoopHandle, + ping::Ping, timer::{TimeoutAction, Timer}, }; use calloop_wayland_source::WaylandSource; @@ -112,6 +113,10 @@ const MIN_KEYCODE: u32 = 8; const UNKNOWN_KEYBOARD_LAYOUT_NAME: SharedString = SharedString::new_static("unknown"); const XDG_ACTIVATION_TOKEN_ENV_VAR: &str = "XDG_ACTIVATION_TOKEN"; +/// Pacing for retry ticks: a fixed 60Hz interval. Retries only occur for throttled or +/// failed-present frames, so matching the output's actual refresh rate wouldn't be observable. +const FRAME_RETRY_INTERVAL: Duration = Duration::from_micros(16_667); + fn take_startup_activation_token_from_environment() -> Option { let startup_activation_token = std::env::var(XDG_ACTIVATION_TOKEN_ENV_VAR) .ok() @@ -147,6 +152,7 @@ pub struct Globals { pub dialog: Option, pub system_bell: Option, pub executor: ForegroundExecutor, + pub frame_ping: Ping, } impl Globals { @@ -155,6 +161,7 @@ impl Globals { executor: ForegroundExecutor, qh: QueueHandle, seat: wl_seat::WlSeat, + frame_ping: Ping, ) -> Self { let dialog_v = XdgWmDialogV1::interface().version; Globals { @@ -190,6 +197,7 @@ impl Globals { system_bell: globals.bind(&qh, 1..=1, ()).ok(), executor, qh, + frame_ping, } } } @@ -336,6 +344,45 @@ impl WaylandClientStatePtr { .expect("The pointer should always be valid when dispatching in wayland") } + pub fn dispatch_scheduled_frames(&self) { + let Some(client) = self.0.upgrade() else { + return; + }; + // Release the client borrow before ticking: the tick re-enters GPUI, which can + // borrow the client again (e.g. IME updates). + let windows = client + .borrow() + .windows + .values() + .cloned() + .collect::>(); + for window in windows { + window.scheduled_frame_fired(); + } + } + + /// Queue a retry tick for `surface_id` one refresh interval from now. An immediate + /// retry would spin against the frame-rate throttle that deferred the draw in the + /// first place. + pub fn schedule_frame_retry(&self, surface_id: &ObjectId) { + let client = self.get_client(); + let state = client.borrow(); + let surface_id = surface_id.clone(); + if let Err(err) = state.loop_handle.insert_source( + Timer::from_duration(FRAME_RETRY_INTERVAL), + move |_, _, this| { + let client = this.get_client(); + let window = get_window(&mut client.borrow_mut(), &surface_id); + if let Some(window) = window { + window.retry_timer_fired(); + } + TimeoutAction::Drop + }, + ) { + log::error!("Failed to schedule frame retry: {err}"); + } + } + pub fn get_serial(&self, kind: SerialKind) -> u32 { self.0.upgrade().unwrap().borrow().serial_tracker.get(kind) } @@ -638,12 +685,24 @@ impl WaylandClient { let compositor_gpu = detect_compositor_gpu(); let gpu_context = Rc::new(RefCell::new(None)); + let (frame_ping, frame_ping_source) = + calloop::ping::make_ping().expect("Failed to create the frame ping"); + handle + .insert_source( + frame_ping_source, + |_, _, client: &mut WaylandClientStatePtr| { + client.dispatch_scheduled_frames(); + }, + ) + .unwrap(); + let seat = seat.unwrap(); let globals = Globals::new( globals, common.foreground_executor.clone(), qh.clone(), seat.clone(), + frame_ping, ); let data_device = globals @@ -1247,7 +1306,7 @@ impl Dispatch for WaylandClientStatePtr { drop(state); if let wl_callback::Event::Done { .. } = event { - window.frame(); + window.frame_callback_fired(); } } } diff --git a/crates/gpui_linux/src/linux/wayland/window.rs b/crates/gpui_linux/src/linux/wayland/window.rs index 675602082bb..c090dd8a16f 100644 --- a/crates/gpui_linux/src/linux/wayland/window.rs +++ b/crates/gpui_linux/src/linux/wayland/window.rs @@ -6,6 +6,7 @@ use std::{ sync::Arc, }; +use calloop::ping::Ping; use collections::{FxHashMap, HashMap}; use futures::channel::oneshot::Receiver; @@ -93,7 +94,6 @@ struct InProgressConfigure { pub struct WaylandWindowState { surface_state: WaylandSurfaceState, - acknowledged_first_configure: bool, parent: Option, /// Child surfaces mapped to whether they block this window's input (dialogs /// block, popups don't). Children are closed before this window closes. @@ -122,6 +122,7 @@ pub struct WaylandWindowState { hovered: bool, pub(crate) force_render_after_recovery: bool, renderer_presented: bool, + present_failed: bool, in_progress_configure: Option, resize_throttle: bool, in_progress_window_controls: Option, @@ -490,6 +491,8 @@ impl WaylandSurfaceState { pub struct WaylandWindowStatePtr { state: Rc>, callbacks: Rc>, + frame_loop: Rc>, + frame_ping: Ping, } impl WaylandWindowState { @@ -547,7 +550,6 @@ impl WaylandWindowState { Ok(Self { surface_state, - acknowledged_first_configure: false, parent, children: FxHashMap::default(), surface, @@ -576,6 +578,7 @@ impl WaylandWindowState { hovered: false, force_render_after_recovery: false, renderer_presented: false, + present_failed: false, in_progress_window_controls: None, window_controls: WindowControls::default(), client_inset: None, @@ -623,6 +626,16 @@ impl WaylandWindowState { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FrameLoop { + Unconfigured, + Ticking, + AwaitingCallback, + Scheduled, + RetryScheduled, + Parked, +} + pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr); pub enum ImeInput { InsertText(String), @@ -633,8 +646,11 @@ pub enum ImeInput { impl Drop for WaylandWindow { fn drop(&mut self) { + self.0.frame_loop.set(FrameLoop::Parked); + let mut state = self.0.state.borrow_mut(); let surface_id = state.surface.id(); + if let Some(parent) = state.parent.as_ref() { parent.state.borrow_mut().children.remove(&surface_id); } @@ -720,6 +736,7 @@ impl WaylandWindow { .as_ref() .map(|viewporter| viewporter.get_viewport(&surface, &globals.qh, ())); + let frame_ping = globals.frame_ping.clone(); let this = Self(WaylandWindowStatePtr { state: Rc::new(RefCell::new(WaylandWindowState::new( handle, @@ -735,6 +752,8 @@ impl WaylandWindow { parent, )?)), callbacks: Rc::new(RefCell::new(Callbacks::default())), + frame_loop: Rc::new(Cell::new(FrameLoop::Unconfigured)), + frame_ping, }); // Kick things off @@ -793,35 +812,72 @@ impl WaylandWindowStatePtr { } pub fn frame(&self) { + self.frame_loop.set(FrameLoop::Ticking); let mut state = self.state.borrow_mut(); - state.surface.frame(&state.globals.qh, state.surface.id()); state.resize_throttle = false; let force_render = state.force_render_after_recovery; state.force_render_after_recovery = false; + let require_presentation = state.present_failed; drop(state); let mut cb = self.callbacks.borrow_mut(); if let Some(fun) = cb.request_frame.as_mut() { fun(RequestFrameOptions { force_render, - ..Default::default() + require_presentation, }); self.update_ime_enabled(); + } else { + self.frame_loop.set(FrameLoop::Parked); } } + pub fn frame_callback_fired(&self) { + if self.frame_loop.get() == FrameLoop::AwaitingCallback { + self.frame(); + } + } + + pub fn scheduled_frame_fired(&self) { + if self.frame_loop.get() == FrameLoop::Scheduled { + self.frame(); + } + } + + pub fn retry_timer_fired(&self) { + if self.frame_loop.get() == FrameLoop::RetryScheduled { + self.frame(); + } + } + + pub fn is_configured(&self) -> bool { + self.frame_loop.get() != FrameLoop::Unconfigured + } + + /// Re-arm a parked render loop because the window has work again. + pub fn schedule_frame(&self) { + if self.frame_loop.get() != FrameLoop::Parked { + return; + } + self.frame_loop.set(FrameLoop::Scheduled); + self.frame_ping.ping(); + } + fn update_ime_enabled(&self) { let mut state = self.state.borrow_mut(); if !state.active { return; } let client = state.client.clone(); - let ime_enabled = state - .input_handler - .as_mut() - .map(|input_handler| input_handler.query_accepts_text_input()) - .unwrap_or(true); - drop(state); + let ime_enabled = if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + let accepts_text_input = input_handler.query_accepts_text_input(); + self.state.borrow_mut().input_handler = Some(input_handler); + accepts_text_input + } else { + drop(state); + true + }; if Some(ime_enabled) == client.ime_enabled() { return; } @@ -881,7 +937,7 @@ impl WaylandWindowStatePtr { } } } - let mut state = self.state.borrow_mut(); + let state = self.state.borrow_mut(); state.surface_state.ack_configure(serial); let window_geometry = inset_by_tiling( @@ -899,9 +955,7 @@ impl WaylandWindowStatePtr { window_geometry.size.height, ); - let request_frame_callback = !state.acknowledged_first_configure; - if request_frame_callback { - state.acknowledged_first_configure = true; + if self.frame_loop.get() == FrameLoop::Unconfigured { drop(state); self.frame(); } @@ -1427,7 +1481,7 @@ impl PlatformWindow for WaylandWindow { // configure reply drives the buffer resize. Before the first configure the popup is // unmapped and cannot reposition, but the initial positioner already carries the size. if matches!(state.surface_state, WaylandSurfaceState::Popup(_)) { - if state.acknowledged_first_configure { + if self.0.is_configured() { let parent_geometry = state .parent .as_ref() @@ -1681,23 +1735,39 @@ impl PlatformWindow for WaylandWindow { return; } + state.surface.frame(&state.globals.qh, state.surface.id()); state.renderer_presented = state.renderer.draw(scene); + state.present_failed = !state.renderer_presented; + if state.renderer_presented { + self.0.frame_loop.set(FrameLoop::AwaitingCallback); + } if state.renderer.needs_redraw() { state.force_render_after_recovery = true; } } - fn completed_frame(&self) { + fn schedule_frame(&self) { + self.0.schedule_frame(); + } + + fn completed_frame(&self, request_next_frame: bool) { let mut state = self.borrow_mut(); - // Work around a bug in old versions of wlroots where committing without a buffer attached - // can cause invalid synchronization that leads to graphical corruption. - if !state.renderer_presented { - state.surface.commit(); + let presented = std::mem::take(&mut state.renderer_presented); + if presented { + return; } - state.renderer_presented = false; + if request_next_frame || state.force_render_after_recovery || state.present_failed { + self.0.frame_loop.set(FrameLoop::RetryScheduled); + let surface_id = state.surface.id(); + let client = state.client.clone(); + drop(state); + client.schedule_frame_retry(&surface_id); + } else { + self.0.frame_loop.set(FrameLoop::Parked); + } } fn sprite_atlas(&self) -> Arc { diff --git a/crates/gpui_web/src/window.rs b/crates/gpui_web/src/window.rs index b9399d32e63..79a976cdd35 100644 --- a/crates/gpui_web/src/window.rs +++ b/crates/gpui_web/src/window.rs @@ -682,7 +682,7 @@ impl PlatformWindow for WebWindow { self.inner.state.borrow_mut().renderer.draw(scene); } - fn completed_frame(&self) { + fn completed_frame(&self, _request_next_frame: bool) { // On web, presentation happens automatically via wgpu surface present }