gpui: Make the Wayland render loop demand-driven (#60690)

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 committing 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

---------

Co-authored-by: Philipp Schaffrath <philipp.schaffrath@gmail.com>
Co-authored-by: Daan De Meyer <daan@amutable.com>
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
Neel 2026-08-21 09:43:16 +00:00 committed by GitHub
parent ef50ad95b5
commit eb354c8d50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 465 additions and 62 deletions

View file

@ -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<Cell<usize>>);
impl Render for RenderCounter {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> 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() {

View file

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

View file

@ -861,7 +861,7 @@ 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 schedule_frame(&self) {}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
fn is_subpixel_rendering_supported(&self) -> bool;

View file

@ -38,6 +38,8 @@ pub(crate) struct TestWindowState {
appearance_change_callback: Option<Box<dyn FnMut()>>,
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
frame_wake_count: Rc<Cell<usize>>,
frame_scheduled: bool,
frame_callback_pending: bool,
input_handler: Option<PlatformInputHandler>,
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<Pixels>) {
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<dyn FnMut(crate::PlatformInput) -> 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<DevicePixels> = state.bounds.size.to_device_pixels(scale_factor);
if let Some(renderer) = &mut state.renderer {
renderer.render_scene(scene, device_size).warn_on_err();

View file

@ -1162,7 +1162,7 @@ pub struct Window {
next_hitbox_id: HitboxId,
pub(crate) next_tooltip_id: TooltipId,
pub(crate) tooltip_bounds: Option<TooltipBounds>,
next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>,
pub(crate) next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>,
pub(crate) dirty_views: FxHashSet<EntityId>,
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<Cell<Bounds<Pixels>>>,

View file

@ -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<String> {
let startup_activation_token = std::env::var(XDG_ACTIVATION_TOKEN_ENV_VAR)
.ok()
@ -222,6 +227,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 {
@ -230,6 +236,7 @@ impl Globals {
executor: ForegroundExecutor,
qh: QueueHandle<WaylandClientStatePtr>,
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::<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) -> 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<WlCallback, ObjectId> for WaylandClientStatePtr {
drop(state);
if let wl_callback::Event::Done { .. } = event {
window.frame();
window.frame_callback_fired();
}
}
}

View file

@ -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<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,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<wl_callback::WlCallback>,
in_progress_configure: Option<InProgressConfigure>,
resize_throttle: bool,
in_progress_window_controls: Option<WindowControls>,
@ -536,6 +537,8 @@ impl WaylandSurfaceState {
pub struct WaylandWindowStatePtr {
state: Rc<RefCell<WaylandWindowState>>,
callbacks: Rc<RefCell<Callbacks>>,
frame_loop: Rc<Cell<FrameLoop>>,
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<dyn PlatformAtlas> {
@ -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();
}
}

View file

@ -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<dyn PlatformAtlas> {
self.inner.state.borrow().renderer.sprite_atlas().clone()
}