diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b99da408226..08f4be87f5f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1706,6 +1706,15 @@ impl App { } if self.pending_effects.is_empty() { + for window in self.windows.values().filter_map(|window| window.as_deref()) { + if window.invalidator.is_dirty() + || window.needs_present.get() + || !window.next_frame_callbacks.borrow().is_empty() + { + window.platform_window.schedule_frame(); + } + } + self.event_arena.clear(); break; } @@ -3075,12 +3084,43 @@ impl<'a, T> Drop for GpuiBorrow<'a, T> { #[cfg(test)] mod test { - use std::{cell::RefCell, ffi::OsString, path::PathBuf, rc::Rc}; + use std::{ + cell::{Cell, RefCell}, + ffi::OsString, + path::PathBuf, + rc::Rc, + }; #[cfg(unix)] use std::os::unix::ffi::OsStringExt; - use crate::{AppContext, TestAppContext}; + use crate::{AppContext, Context, Empty, IntoElement, Render, TestAppContext, Window}; + + struct RenderCounter(Rc>); + + impl Render for RenderCounter { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + self.0.set(self.0.get() + 1); + Empty + } + } + + #[gpui::test] + fn async_app_refresh_flushes_refresh_effect(cx: &mut TestAppContext) { + let render_count = Rc::new(Cell::new(0)); + + let _window = cx.add_window({ + let render_count = render_count.clone(); + move |_, _| RenderCounter(render_count) + }); + + cx.run_until_parked(); + let render_count_before_refresh = render_count.get(); + + cx.to_async().refresh(); + + assert_eq!(render_count.get(), render_count_before_refresh + 1); + } #[test] fn test_gpui_borrow() { diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index 6c5246f33d0..48c084a66f3 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -146,7 +146,9 @@ impl AsyncApp { pub fn refresh(&self) { let app = self.app(); let mut lock = app.borrow_mut(); - lock.refresh_windows(); + // A direct call would leave the refresh effect queued, which cannot wake + // a platform render loop that has already parked. + lock.update(|cx| cx.refresh_windows()); } /// Get an executor which can be used to spawn futures in the background. diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 51c24da0480..ab4b500825a 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -861,7 +861,7 @@ 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 schedule_frame(&self) {} fn sprite_atlas(&self) -> Arc; fn is_subpixel_rendering_supported(&self) -> bool; diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index a3ae2ba20ba..939c8a8bf87 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -38,6 +38,8 @@ pub(crate) struct TestWindowState { appearance_change_callback: Option>, request_frame_callback: Option>, frame_wake_count: Rc>, + frame_scheduled: bool, + frame_callback_pending: bool, input_handler: Option, is_fullscreen: bool, appearance: WindowAppearance, @@ -98,6 +100,8 @@ impl TestWindow { appearance_change_callback: None, request_frame_callback: None, frame_wake_count: Rc::new(Cell::new(0)), + frame_scheduled: false, + frame_callback_pending: false, input_handler: None, is_fullscreen: false, appearance: WindowAppearance::Light, @@ -105,6 +109,28 @@ impl TestWindow { start_external_drag_result: false, }))) } + pub fn simulate_scheduled_frame(&self) -> bool { + let callback = { + let mut state = self.0.lock(); + if !std::mem::take(&mut state.frame_scheduled) { + return false; + } + state.frame_callback_pending = false; + state.request_frame_callback.take() + }; + let Some(mut callback) = callback else { + self.0.lock().frame_scheduled = true; + return false; + }; + + callback(RequestFrameOptions::default()); + self.0.lock().request_frame_callback = Some(callback); + true + } + + pub fn frame_scheduled(&self) -> bool { + self.0.lock().frame_scheduled + } pub fn simulate_resize(&mut self, size: Size) { let scale_factor = self.scale_factor(); @@ -324,6 +350,13 @@ impl PlatformWindow for TestWindow { self.0.lock().request_frame_callback = Some(callback); } + fn schedule_frame(&self) { + let mut state = self.0.lock(); + if !state.frame_callback_pending { + state.frame_scheduled = true; + } + } + fn on_input(&self, callback: Box DispatchEventResult>) { self.0.lock().input_callback = Some(callback) } @@ -361,6 +394,8 @@ impl PlatformWindow for TestWindow { fn draw(&self, scene: &Scene) { let scale_factor = self.scale_factor(); let mut state = self.0.lock(); + state.frame_callback_pending = true; + state.frame_scheduled = true; let device_size: Size = state.bounds.size.to_device_pixels(scale_factor); if let Some(renderer) = &mut state.renderer { renderer.render_scene(scene, device_size).warn_on_err(); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index bddeda62782..50d3d915107 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1162,7 +1162,7 @@ pub struct Window { next_hitbox_id: HitboxId, pub(crate) next_tooltip_id: TooltipId, pub(crate) tooltip_bounds: Option, - next_frame_callbacks: Rc>>, + pub(crate) next_frame_callbacks: Rc>>, pub(crate) dirty_views: FxHashSet, focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, pub(crate) focus_lost_listeners: SubscriberSet<(), AnyObserver>, @@ -1594,12 +1594,11 @@ impl Window { { // Don't lose a pending forced render to throttling. deferred_force_render |= force_render; - // 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.platform_window.schedule_frame(); + }) .log_err(); // The demand that entered this branch (a deferred forced // render or pending next-frame callbacks) is still @@ -1652,7 +1651,11 @@ impl Window { handle .update(&mut cx, |_, window, _| { - window.complete_frame(); + if window.invalidator.is_dirty() + || !window.next_frame_callbacks.borrow().is_empty() + { + window.platform_window.schedule_frame(); + } }) .log_err(); @@ -2351,6 +2354,7 @@ 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)); + self.platform_window.schedule_frame(); // Next-frame callbacks create frame demand without dirtying the // window, so the platform's frame source must be woken explicitly. self.invalidator.wake_platform(); @@ -2844,10 +2848,6 @@ impl Window { self.capslock } - fn complete_frame(&self) { - self.platform_window.completed_frame(); - } - /// Produces a new frame and assigns it to `rendered_frame`. To actually show /// the contents of the new [`Scene`], use [`Self::present`]. #[profiling::function] @@ -7089,6 +7089,80 @@ mod tests { assert_eq!(observed_appearance.get(), Some(WindowAppearance::Dark)); } + #[gpui::test] + fn queued_frame_callback_wakes_a_parked_render_loop(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| Empty); + let test_window = cx.test_window(window.into()); + + assert!(test_window.simulate_scheduled_frame()); + assert!(test_window.simulate_scheduled_frame()); + assert!(!test_window.frame_scheduled()); + + cx.update_window(window.into(), |_, window, _| { + window.active.set(true); + window.on_next_frame(|_, _| {}); + }) + .unwrap(); + assert!( + test_window.frame_scheduled(), + "queuing work on a parked window must wake the render loop" + ); + + assert!(test_window.simulate_scheduled_frame()); + assert!( + test_window.frame_scheduled(), + "presenting the frame must await one compositor callback" + ); + assert!(test_window.simulate_scheduled_frame()); + assert!(!test_window.frame_scheduled()); + } + + #[gpui::test] + fn pending_presentation_wakes_a_parked_render_loop(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| Empty); + let test_window = cx.test_window(window.into()); + + assert!(test_window.simulate_scheduled_frame()); + assert!(test_window.simulate_scheduled_frame()); + assert!(!test_window.frame_scheduled()); + + cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + + assert!( + test_window.frame_scheduled(), + "a rendered scene awaiting presentation must wake the render loop" + ); + } + + #[gpui::test] + fn callback_queued_during_a_frame_requests_a_follow_up(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| Empty); + let test_window = cx.test_window(window.into()); + + let callback_ran = Rc::new(Cell::new(false)); + cx.update_window(window.into(), |_, window, _| { + // Inactive windows are frame-rate throttled, which would defer the + // ticks this test drives manually. + window.active.set(true); + let callback_ran = callback_ran.clone(); + window.on_next_frame(move |window, _| { + window.on_next_frame(move |_, _| callback_ran.set(true)); + }); + }) + .unwrap(); + + assert!(test_window.simulate_scheduled_frame()); + assert!(!callback_ran.get()); + assert!( + test_window.frame_scheduled(), + "a callback queued mid-frame must schedule a follow-up before the loop parks" + ); + + assert!(test_window.simulate_scheduled_frame()); + assert!(callback_ran.get()); + } + struct RootView { explicit_size: bool, child_bounds: Rc>>, diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 01863989cf0..a5c6dc89258 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; @@ -187,6 +188,10 @@ fn set_ime_cursor_rectangle_after_done( } } +/// 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() @@ -222,6 +227,7 @@ pub struct Globals { pub dialog: Option, pub system_bell: Option, pub executor: ForegroundExecutor, + pub frame_ping: Ping, } impl Globals { @@ -230,6 +236,7 @@ impl Globals { executor: ForegroundExecutor, qh: QueueHandle, seat: wl_seat::WlSeat, + frame_ping: Ping, ) -> Self { let dialog_v = XdgWmDialogV1::interface().version; Globals { @@ -265,6 +272,7 @@ impl Globals { system_bell: globals.bind(&qh, 1..=1, ()).ok(), executor, qh, + frame_ping, } } } @@ -437,6 +445,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) -> Serial { self.0.upgrade().unwrap().borrow().serial_tracker.get(kind) } @@ -774,12 +821,21 @@ 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| { + 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 @@ -1397,7 +1453,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 75315710f6b..eb84b7f7fd5 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; @@ -14,7 +15,7 @@ use wayland_backend::client::ObjectId; use wayland_client::WEnum; use wayland_client::{ Proxy, - protocol::{wl_output, wl_seat, wl_surface}, + protocol::{wl_callback, wl_output, wl_seat, wl_surface}, }; use wayland_protocols::wp::viewporter::client::wp_viewport; use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1; @@ -95,7 +96,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,8 +122,9 @@ pub struct WaylandWindowState { handle: AnyWindowHandle, active: bool, hovered: bool, - pub(crate) force_render_after_recovery: bool, - renderer_presented: bool, + redraw_requested: bool, + presentation: PresentationState, + pending_frame_callback: Option, in_progress_configure: Option, resize_throttle: bool, in_progress_window_controls: Option, @@ -536,6 +537,8 @@ impl WaylandSurfaceState { pub struct WaylandWindowStatePtr { state: Rc>, callbacks: Rc>, + frame_loop: Rc>, + frame_ping: Ping, } impl WaylandWindowState { @@ -593,7 +596,6 @@ impl WaylandWindowState { Ok(Self { surface_state, - acknowledged_first_configure: false, parent, children: FxHashMap::default(), surface, @@ -620,8 +622,9 @@ impl WaylandWindowState { handle, active: false, hovered: false, - force_render_after_recovery: false, - renderer_presented: false, + redraw_requested: false, + presentation: PresentationState::Unpresented, + pending_frame_callback: None, in_progress_window_controls: None, window_controls: WindowControls::default(), client_inset: None, @@ -669,6 +672,75 @@ impl WaylandWindowState { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PresentationState { + Unpresented, + Presented, + RetryBeforeFirstPresent, + RetryAfterPresent, +} + +impl PresentationState { + fn requires_presentation(self) -> bool { + matches!( + self, + Self::RetryBeforeFirstPresent | Self::RetryAfterPresent + ) + } + + fn failed(self) -> Self { + match self { + Self::Unpresented | Self::RetryBeforeFirstPresent => Self::RetryBeforeFirstPresent, + Self::Presented | Self::RetryAfterPresent => Self::RetryAfterPresent, + } + } +} + +#[cfg(test)] +mod presentation_state_tests { + use super::PresentationState; + + #[test] + fn failure_tracks_whether_the_surface_has_presented() { + assert_eq!( + PresentationState::Unpresented.failed(), + PresentationState::RetryBeforeFirstPresent + ); + assert_eq!( + PresentationState::RetryBeforeFirstPresent.failed(), + PresentationState::RetryBeforeFirstPresent + ); + assert_eq!( + PresentationState::Presented.failed(), + PresentationState::RetryAfterPresent + ); + assert_eq!( + PresentationState::RetryAfterPresent.failed(), + PresentationState::RetryAfterPresent + ); + } + + #[test] + fn only_retry_states_require_presentation() { + assert!(!PresentationState::Unpresented.requires_presentation()); + assert!(!PresentationState::Presented.requires_presentation()); + assert!(PresentationState::RetryBeforeFirstPresent.requires_presentation()); + assert!(PresentationState::RetryAfterPresent.requires_presentation()); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FrameLoop { + Unconfigured, + Ticking, + RescheduleRequested, + PresentationFailed, + AwaitingCallback, + Scheduled, + RetryScheduled, + Parked, +} + pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr); pub enum ImeInput { InsertText(String), @@ -679,8 +751,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); } @@ -766,6 +841,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, @@ -781,6 +857,8 @@ impl WaylandWindow { parent, )?)), callbacks: Rc::new(RefCell::new(Callbacks::default())), + frame_loop: Rc::new(Cell::new(FrameLoop::Unconfigured)), + frame_ping, }); // Kick things off @@ -839,21 +917,117 @@ 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; + // GPUI may throttle this tick without calling draw, so leave the request + // latched until a draw actually reaches the renderer. + let force_render = state.redraw_requested; + let require_presentation = state.presentation.requires_presentation(); drop(state); - let mut cb = self.callbacks.borrow_mut(); - if let Some(fun) = cb.request_frame.as_mut() { - fun(RequestFrameOptions { - force_render, - ..Default::default() - }); - self.update_ime_enabled(); + let mut callbacks = self.callbacks.borrow_mut(); + let Some(request_frame_callback) = callbacks.request_frame.as_mut() else { + self.frame_loop.set(FrameLoop::Parked); + return; + }; + request_frame_callback(RequestFrameOptions { + force_render, + require_presentation, + }); + self.update_ime_enabled(); + drop(callbacks); + + self.complete_frame(); + } + + fn complete_frame(&self) { + let mut state = self.state.borrow_mut(); + + let frame_loop = self.frame_loop.get(); + if frame_loop == FrameLoop::AwaitingCallback { + return; } + + if state.presentation.requires_presentation() { + // Before the first present, or when throttling skipped draw, a + // callback may never arrive. Otherwise let the compositor pace + // retries so an occluded window does not keep polling. + if frame_loop == FrameLoop::PresentationFailed + && state.presentation == PresentationState::RetryAfterPresent + { + if state.pending_frame_callback.is_none() { + let callback = state.surface.frame(&state.globals.qh, state.surface.id()); + state.pending_frame_callback = Some(callback); + } + state.surface.commit(); + self.frame_loop.set(FrameLoop::AwaitingCallback); + return; + } + + self.frame_loop.set(FrameLoop::RetryScheduled); + let surface_id = state.surface.id(); + let client = state.client.clone(); + drop(state); + client.schedule_frame_retry(&surface_id); + return; + } + + if frame_loop == FrameLoop::RescheduleRequested || state.redraw_requested { + self.frame_loop.set(FrameLoop::RetryScheduled); + let surface_id = state.surface.id(); + let client = state.client.clone(); + drop(state); + client.schedule_frame_retry(&surface_id); + return; + } + + self.frame_loop.set(FrameLoop::Parked); + } + + pub fn frame_callback_fired(&self) { + // Another wl_surface commit may have carried this callback while a retry + // timer owned the render-loop wakeup. + self.state.borrow_mut().pending_frame_callback = None; + 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 + } + + pub fn schedule_frame(&self) { + match self.frame_loop.get() { + FrameLoop::Parked => { + self.frame_loop.set(FrameLoop::Scheduled); + self.frame_ping.ping(); + } + FrameLoop::Ticking => { + self.frame_loop.set(FrameLoop::RescheduleRequested); + } + // A wake is already armed: a ping or retry timer is in flight, or a + // presented buffer guarantees a compositor frame callback. + _ => {} + } + } + + fn request_redraw(&self) { + self.state.borrow_mut().redraw_requested = true; + self.schedule_frame(); } fn update_ime_enabled(&self) { @@ -862,12 +1036,15 @@ impl WaylandWindowStatePtr { 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; } @@ -927,7 +1104,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( @@ -945,11 +1122,12 @@ impl WaylandWindowStatePtr { window_geometry.size.height, ); - let request_frame_callback = !state.acknowledged_first_configure; - if request_frame_callback { - state.acknowledged_first_configure = true; - drop(state); + let initial_configure = self.frame_loop.get() == FrameLoop::Unconfigured; + drop(state); + if initial_configure { self.frame(); + } else { + self.request_redraw(); } } } @@ -976,17 +1154,22 @@ impl WaylandWindowStatePtr { } WEnum::Value(_) => { log::warn!("Unknown decoration mode"); + return; } WEnum::Unknown(v) => { log::warn!("Unknown decoration mode: {}", v); + return; } } + update_window(self.state.borrow_mut()); + self.request_redraw(); } } pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) { if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event { self.rescale(scale as f32 / 120.0); + self.request_redraw(); } } @@ -1193,7 +1376,10 @@ impl WaylandWindowStatePtr { state.surface.set_buffer_scale(scale); drop(state); self.rescale(scale as f32); + } else { + drop(state); } + self.request_redraw(); } wl_surface::Event::Leave { output } => { state.outputs.remove(&output.id()); @@ -1206,7 +1392,10 @@ impl WaylandWindowStatePtr { state.surface.set_buffer_scale(scale); drop(state); self.rescale(scale as f32); + } else { + drop(state); } + self.request_redraw(); } wl_surface::Event::PreferredBufferScale { factor } => { // We use `WpFractionalScale` instead to set the scale if it's available @@ -1214,6 +1403,7 @@ impl WaylandWindowStatePtr { state.surface.set_buffer_scale(factor); drop(state); self.rescale(factor as f32); + self.request_redraw(); } } _ => {} @@ -1473,7 +1663,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() @@ -1611,8 +1801,12 @@ impl PlatformWindow for WaylandWindow { fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { let mut state = self.borrow_mut(); + if state.background_appearance == background_appearance { + return; + } state.background_appearance = background_appearance; update_window(state); + self.0.request_redraw(); } fn background_appearance(&self) -> WindowBackgroundAppearance { @@ -1725,27 +1919,31 @@ impl PlatformWindow for WaylandWindow { } } - state.force_render_after_recovery = true; + state.redraw_requested = true; return; } - state.renderer_presented = state.renderer.draw(scene); + // Surface state changed during this GPUI tick is included in this presentation. + state.redraw_requested = false; + if state.pending_frame_callback.is_none() { + let callback = state.surface.frame(&state.globals.qh, state.surface.id()); + state.pending_frame_callback = Some(callback); + } + if state.renderer.draw(scene) { + state.presentation = PresentationState::Presented; + self.0.frame_loop.set(FrameLoop::AwaitingCallback); + } else { + state.presentation = state.presentation.failed(); + self.0.frame_loop.set(FrameLoop::PresentationFailed); + } if state.renderer.needs_redraw() { - state.force_render_after_recovery = true; + state.redraw_requested = true; } } - fn completed_frame(&self) { - 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(); - } - - state.renderer_presented = false; + fn schedule_frame(&self) { + self.0.schedule_frame(); } fn sprite_atlas(&self) -> Arc { @@ -1875,6 +2073,7 @@ impl PlatformWindow for WaylandWindow { update_window(state); } } + self.0.request_redraw(); } fn window_controls(&self) -> WindowControls { @@ -1886,6 +2085,7 @@ impl PlatformWindow for WaylandWindow { if Some(inset) != state.client_inset { state.client_inset = Some(inset); update_window(state); + self.0.request_redraw(); } } diff --git a/crates/gpui_web/src/window.rs b/crates/gpui_web/src/window.rs index 6261b954d3f..4bb2bc94269 100644 --- a/crates/gpui_web/src/window.rs +++ b/crates/gpui_web/src/window.rs @@ -790,10 +790,6 @@ impl PlatformWindow for WebWindow { self.inner.state.borrow_mut().renderer.draw(scene); } - fn completed_frame(&self) { - // On web, presentation happens automatically via wgpu surface present - } - fn sprite_atlas(&self) -> Arc { self.inner.state.borrow().renderer.sprite_atlas().clone() }