mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
gpui: Make the Wayland render loop demand-driven
Closes #55345 Wayland is the only platform whose frame ticks are conditional: a wl_surface frame callback only arrives after a commit the compositor goes on to repaint. GPUI assumed unconditional ticks (any previously our Wayland backend faked them), so an idle fullscreen window stopped receiving callbacks and froze until external damage arrived. This PR makes the Wayland render loop demand-driven, instead parking when there's nothing to draw, and stops Zed commiting empty frames on every tick as the artificial heartbeat from the compositor. Release Notes: - Fixed the UI freezing in fullscreen on some Wayland compositors - Fixed idle windows waking at the display's refresh rate on Wayland
This commit is contained in:
parent
717e1c0590
commit
dd16384970
6 changed files with 176 additions and 35 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -664,7 +664,8 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
|||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
|
||||
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<dyn PlatformAtlas>;
|
||||
fn is_subpixel_rendering_supported(&self) -> bool;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
let startup_activation_token = std::env::var(XDG_ACTIVATION_TOKEN_ENV_VAR)
|
||||
.ok()
|
||||
|
|
@ -147,6 +152,7 @@ pub struct Globals {
|
|||
pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
|
||||
pub system_bell: Option<xdg_system_bell_v1::XdgSystemBellV1>,
|
||||
pub executor: ForegroundExecutor,
|
||||
pub frame_ping: Ping,
|
||||
}
|
||||
|
||||
impl Globals {
|
||||
|
|
@ -155,6 +161,7 @@ impl Globals {
|
|||
executor: ForegroundExecutor,
|
||||
qh: QueueHandle<WaylandClientStatePtr>,
|
||||
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::<Vec<WaylandWindowStatePtr>>();
|
||||
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<WlCallback, ObjectId> for WaylandClientStatePtr {
|
|||
drop(state);
|
||||
|
||||
if let wl_callback::Event::Done { .. } = event {
|
||||
window.frame();
|
||||
window.frame_callback_fired();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WaylandWindowStatePtr>,
|
||||
/// 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<InProgressConfigure>,
|
||||
resize_throttle: bool,
|
||||
in_progress_window_controls: Option<WindowControls>,
|
||||
|
|
@ -490,6 +491,8 @@ impl WaylandSurfaceState {
|
|||
pub struct WaylandWindowStatePtr {
|
||||
state: Rc<RefCell<WaylandWindowState>>,
|
||||
callbacks: Rc<RefCell<Callbacks>>,
|
||||
frame_loop: Rc<Cell<FrameLoop>>,
|
||||
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<dyn PlatformAtlas> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue