Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions crates/gpui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1658,6 +1658,10 @@ impl App {
}

if self.pending_effects.is_empty() {
for window in self.windows.values().filter_map(|window| window.as_deref()) {
window.schedule_pending_platform_frame();
}

self.event_arena.clear();
break;
}
Expand Down Expand Up @@ -3020,9 +3024,38 @@ impl<'a, T> Drop for GpuiBorrow<'a, T> {

#[cfg(test)]
mod test {
use std::{cell::RefCell, rc::Rc};
use std::{
cell::{Cell, RefCell},
rc::Rc,
};

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

use crate::{AppContext, TestAppContext};
#[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() {
Expand Down
4 changes: 3 additions & 1 deletion crates/gpui/src/app/async_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion crates/gpui/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,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;

Expand Down
38 changes: 37 additions & 1 deletion crates/gpui/src/platform/test/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub(crate) struct TestWindowState {
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved_callback: Option<Box<dyn FnMut()>>,
appearance_change_callback: Option<Box<dyn FnMut()>>,
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
frame_scheduled: bool,
input_handler: Option<PlatformInputHandler>,
is_fullscreen: bool,
appearance: WindowAppearance,
Expand Down Expand Up @@ -91,6 +93,8 @@ impl TestWindow {
resize_callback: None,
moved_callback: None,
appearance_change_callback: None,
request_frame_callback: None,
frame_scheduled: false,
input_handler: None,
is_fullscreen: false,
appearance: WindowAppearance::Light,
Expand Down Expand Up @@ -151,6 +155,28 @@ impl TestWindow {
pub fn set_start_external_drag_result(&self, result: bool) {
self.0.lock().start_external_drag_result = result;
}

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

impl PlatformWindow for TestWindow {
Expand Down Expand Up @@ -286,7 +312,9 @@ impl PlatformWindow for TestWindow {
self.0.lock().is_fullscreen
}

fn on_request_frame(&self, _callback: Box<dyn FnMut(RequestFrameOptions)>) {}
fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
self.0.lock().request_frame_callback = Some(callback);
}

fn on_input(&self, callback: Box<dyn FnMut(crate::PlatformInput) -> DispatchEventResult>) {
self.0.lock().input_callback = Some(callback)
Expand All @@ -308,6 +336,14 @@ impl PlatformWindow for TestWindow {
self.0.lock().moved_callback = Some(callback)
}

fn completed_frame(&self, request_next_frame: bool) {
self.0.lock().frame_scheduled = request_next_frame;
}

fn schedule_frame(&self) {
self.0.lock().frame_scheduled = true;
}

fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
self.0.lock().should_close_handler = Some(callback);
}
Expand Down
73 changes: 66 additions & 7 deletions crates/gpui/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1612,23 +1612,24 @@ impl Window {
{
// Don't lose a pending forced render to throttling.
deferred_force_render |= force_render;
// Deferred by throttling: ask demand-driven platforms to retry.
// 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.
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);
}
})
Expand Down Expand Up @@ -1663,9 +1664,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();
}
Expand Down Expand Up @@ -2340,6 +2343,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.
Expand Down Expand Up @@ -2814,8 +2820,19 @@ impl Window {
self.capslock
}

fn complete_frame(&self) {
self.platform_window.completed_frame();
pub(crate) fn schedule_pending_platform_frame(&self) {
// A clean window may still have a scene to present or callbacks that
// were queued while its previous frame was running.
if self.invalidator.is_dirty()
|| self.needs_present.get()
|| !self.next_frame_callbacks.borrow().is_empty()
{
self.platform_window.schedule_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
Expand Down Expand Up @@ -6899,6 +6916,48 @@ mod tests {
}
}

#[gpui::test]
fn parked_window_wakes_for_pending_work(cx: &mut TestAppContext) {
let window = cx.add_window(|_, _| Empty);
let test_window = cx.test_window(window.into());

assert!(test_window.simulate_scheduled_frame());
cx.update_window(window.into(), |_, _, _| {}).unwrap();
assert!(!test_window.frame_scheduled());

cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx))
.unwrap();
assert!(test_window.frame_scheduled());
assert!(test_window.simulate_scheduled_frame());

window.update(cx, |_, _, cx| cx.notify()).unwrap();
assert!(test_window.frame_scheduled());
}

#[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());
assert!(test_window.simulate_scheduled_frame());

let callback_ran = Rc::new(Cell::new(false));
cx.update_window(window.into(), |_, window, _| {
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());

assert!(test_window.simulate_scheduled_frame());
assert!(callback_ran.get());
}

#[test]
fn auto_sized_window_root_fills_the_window() {
let mut cx = TestAppContext::single();
Expand Down
61 changes: 60 additions & 1 deletion crates/gpui_linux/src/linux/wayland/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::{
use ashpd::WindowIdentifier;
use calloop::{
EventLoop, LoopHandle,
ping::Ping,
timer::{TimeoutAction, Timer},
};
use calloop_wayland_source::WaylandSource;
Expand Down Expand Up @@ -186,6 +187,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()
Expand Down Expand Up @@ -221,6 +226,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 {
Expand All @@ -229,6 +235,7 @@ impl Globals {
executor: ForegroundExecutor,
qh: QueueHandle<WaylandClientStatePtr>,
seat: wl_seat::WlSeat,
frame_ping: Ping,
) -> Self {
let dialog_v = XdgWmDialogV1::interface().version;
Globals {
Expand Down Expand Up @@ -264,6 +271,7 @@ impl Globals {
system_bell: globals.bind(&qh, 1..=1, ()).ok(),
executor,
qh,
frame_ping,
}
}
}
Expand Down Expand Up @@ -436,6 +444,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)
}
Expand Down Expand Up @@ -773,12 +820,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
Expand Down Expand Up @@ -1396,7 +1455,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
drop(state);

if let wl_callback::Event::Done { .. } = event {
window.frame();
window.frame_callback_fired();
}
}
}
Expand Down
Loading