From 811806409f9ddf52c980c13dd6efdf91a97f72d9 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:27:21 +0800 Subject: [PATCH 1/2] fix(gpui): defer window callbacks AppKit sends while the app is mid-update --- .../patches/zed-idle-frames.patch | 147 +++++++++++++++--- 1 file changed, 127 insertions(+), 20 deletions(-) diff --git a/apps/desktop-gpui/patches/zed-idle-frames.patch b/apps/desktop-gpui/patches/zed-idle-frames.patch index e9f7d7cb714..38cf993725e 100644 --- a/apps/desktop-gpui/patches/zed-idle-frames.patch +++ b/apps/desktop-gpui/patches/zed-idle-frames.patch @@ -35,7 +35,7 @@ index 0294b7d..09adcdd 100644 fn on_active_status_change(&self, callback: Box); fn on_hover_status_change(&self, callback: Box); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs -index 393e517..de95b7d 100644 +index 393e517..bb20879 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -9,7 +9,8 @@ use crate::{ @@ -48,7 +48,7 @@ index 393e517..de95b7d 100644 Priority, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, BackdropBlur, ScaledPixels, Scene, Shadow, SharedString, Size, -@@ -120,6 +121,69 @@ struct WindowInvalidatorInner { +@@ -120,6 +121,109 @@ struct WindowInvalidatorInner { pub dirty_views: FxHashSet, pub update_count: usize, pub frame_dirty: FrameDirtyAccumulator, @@ -92,6 +92,18 @@ index 393e517..de95b7d 100644 + } + } + ++ /// A frame request that arrived while the app was mid-update and could not ++ /// run. While requests are stopped nothing else would draw what woke the ++ /// window, so ask for another single frame. ++ fn missed_frame(&self) { ++ if let Some(toggle) = &self.toggle ++ && self.paused.get() ++ { ++ self.once_pending.set(true); ++ toggle(FrameRequests::Once); ++ } ++ } ++ + /// `busy`: the frame did work. `pending`: work is left for the next frame. + fn frame_finished(&self, busy: bool, pending: bool, now: Instant) { + let Some(toggle) = &self.toggle else { @@ -115,10 +127,38 @@ index 393e517..de95b7d 100644 + toggle(FrameRequests::Stop); + } + } ++} ++ ++/// Applies a platform notification to the window now, or on the next turn of ++/// the run loop when it arrived while the app was already being updated. AppKit ++/// delivers moves, resizes and appearance changes synchronously from inside the ++/// `setFrame:`/`orderFront:` calls an update makes (opening or positioning a ++/// window), and dropping them leaves the window's bounds or appearance stale. ++fn update_window_or_defer( ++ handle: AnyWindowHandle, ++ cx: &mut crate::AsyncApp, ++ update: fn(&mut Window, &mut App), ++) { ++ let Err(error) = handle.update(cx, |_, window, cx| update(window, cx)) else { ++ return; ++ }; ++ if !error.is::() { ++ Result::<(), _>::Err(error).log_err(); ++ return; ++ } ++ let executor = cx.foreground_executor().clone(); ++ let mut cx = cx.clone(); ++ executor ++ .spawn(async move { ++ handle ++ .update(&mut cx, |_, window, cx| update(window, cx)) ++ .log_err(); ++ }) ++ .detach(); } /// Per-frame invalidation bookkeeping, drained at draw time and emitted to the -@@ -138,7 +202,7 @@ pub(crate) struct WindowInvalidator { +@@ -138,7 +242,7 @@ pub(crate) struct WindowInvalidator { } impl WindowInvalidator { @@ -127,7 +167,7 @@ index 393e517..de95b7d 100644 WindowInvalidator { inner: Rc::new(RefCell::new(WindowInvalidatorInner { dirty: true, -@@ -146,6 +210,7 @@ impl WindowInvalidator { +@@ -146,6 +250,7 @@ impl WindowInvalidator { dirty_views: FxHashSet::default(), update_count: 0, frame_dirty: FrameDirtyAccumulator::default(), @@ -135,7 +175,7 @@ index 393e517..de95b7d 100644 })), } } -@@ -157,6 +222,9 @@ impl WindowInvalidator { +@@ -157,6 +262,9 @@ impl WindowInvalidator { if inner.draw_phase == DrawPhase::None { Self::record_frame_dirty(&mut inner); inner.dirty = true; @@ -145,7 +185,7 @@ index 393e517..de95b7d 100644 cx.push_effect(Effect::Notify { emitter: entity }); true } else { -@@ -174,6 +242,9 @@ impl WindowInvalidator { +@@ -174,6 +282,9 @@ impl WindowInvalidator { if dirty { inner.update_count += 1; Self::record_frame_dirty(&mut inner); @@ -155,7 +195,7 @@ index 393e517..de95b7d 100644 } } -@@ -1390,7 +1461,8 @@ impl Window { +@@ -1390,7 +1501,8 @@ impl Window { let scale_factor = platform_window.scale_factor(); let appearance = platform_window.appearance(); let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone())); @@ -165,7 +205,7 @@ index 393e517..de95b7d 100644 let active = Rc::new(Cell::new(platform_window.is_active())); let hovered = Rc::new(Cell::new(platform_window.is_hovered())); let needs_present = Rc::new(Cell::new(false)); -@@ -1507,12 +1579,17 @@ impl Window { +@@ -1507,12 +1619,24 @@ impl Window { let invalidator = invalidator.clone(); let active = active.clone(); let needs_present = needs_present.clone(); @@ -177,17 +217,24 @@ index 393e517..de95b7d 100644 - .update(&mut cx, |_, _, cx| cx.thermal_state()) - .log_err(); + let next_frame_callbacks = &pending_frame_callbacks; -+ // A frame can still arrive for a window that was just closed -+ // (its display link or a pending wake); there is nothing to draw. -+ let Ok(thermal_state) = handle.update(&mut cx, |_, _, cx| cx.thermal_state()) -+ else { -+ return; ++ let thermal = handle.update(&mut cx, |_, _, cx| cx.thermal_state()); ++ let thermal_state = match thermal { ++ Ok(thermal_state) => Some(thermal_state), ++ // AppKit can ask for a frame from inside an update ++ // (`displayLayer:` while a window is shown); draw it once ++ // the update returns. ++ Err(error) if error.is::() => { ++ frame_pacer.missed_frame(); ++ return; ++ } ++ // A frame can still arrive for a window that was just closed ++ // (its display link or a pending wake); there is nothing to draw. ++ Err(_) => return, + }; -+ let thermal_state = Some(thermal_state); // Throttle frame rate based on conditions: // - Thermal pressure (Serious/Critical): cap to ~60fps -@@ -1546,13 +1623,17 @@ impl Window { +@@ -1546,13 +1670,17 @@ impl Window { handle .update(&mut cx, |_, window, _| window.complete_frame()) .log_err(); @@ -205,7 +252,7 @@ index 393e517..de95b7d 100644 handle .update(&mut cx, |_, window, cx| { for callback in next_frame_callbacks { -@@ -1570,6 +1651,7 @@ impl Window { +@@ -1570,6 +1698,7 @@ impl Window { || (active.get() && input_rate_tracker.borrow_mut().is_high_rate()); if invalidator.is_dirty() || request_frame_options.force_render { @@ -213,7 +260,7 @@ index 393e517..de95b7d 100644 measure("frame duration", || { handle .update(&mut cx, |_, window, cx| { -@@ -1585,6 +1667,7 @@ impl Window { +@@ -1585,6 +1714,7 @@ impl Window { .log_err(); }) } else if needs_present { @@ -221,7 +268,7 @@ index 393e517..de95b7d 100644 handle .update(&mut cx, |_, window, _| window.present()) .log_err(); -@@ -1595,6 +1678,9 @@ impl Window { +@@ -1595,38 +1725,41 @@ impl Window { window.complete_frame(); }) .log_err(); @@ -231,7 +278,51 @@ index 393e517..de95b7d 100644 } })); platform_window.on_resize(Box::new({ -@@ -2263,6 +2349,8 @@ impl Window { + let mut cx = cx.to_async(); + move |_, _| { +- handle +- .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) +- .log_err(); ++ update_window_or_defer(handle, &mut cx, |window, cx| { ++ window.bounds_changed(cx) ++ }); + } + })); + platform_window.on_moved(Box::new({ + let mut cx = cx.to_async(); + move || { +- handle +- .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) +- .log_err(); ++ update_window_or_defer(handle, &mut cx, |window, cx| { ++ window.bounds_changed(cx) ++ }); + } + })); + platform_window.on_appearance_changed(Box::new({ + let mut cx = cx.to_async(); + move || { +- handle +- .update(&mut cx, |_, window, cx| window.appearance_changed(cx)) +- .log_err(); ++ update_window_or_defer(handle, &mut cx, |window, cx| { ++ window.appearance_changed(cx) ++ }); + } + })); + platform_window.on_button_layout_changed(Box::new({ + let mut cx = cx.to_async(); + move || { +- handle +- .update(&mut cx, |_, window, cx| window.button_layout_changed(cx)) +- .log_err(); ++ update_window_or_defer(handle, &mut cx, |window, cx| { ++ window.button_layout_changed(cx) ++ }); + } + })); + platform_window.on_active_status_change(Box::new({ +@@ -2263,6 +2396,8 @@ 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)); @@ -240,7 +331,7 @@ index 393e517..de95b7d 100644 } /// Schedule a frame to be drawn on the next animation frame. -@@ -6807,6 +6895,98 @@ pub fn outline( +@@ -6807,6 +6942,114 @@ pub fn outline( } } @@ -326,6 +417,22 @@ index 393e517..de95b7d 100644 + } + + #[test] ++ fn a_frame_missed_while_stopped_asks_for_another() { ++ let (pacer, calls, _) = stopped_pacer(); ++ pacer.missed_frame(); ++ assert_eq!(*calls.borrow(), [FrameRequests::Once]); ++ pacer.wake(); ++ assert_eq!(*calls.borrow(), [FrameRequests::Once]); ++ } ++ ++ #[test] ++ fn a_frame_missed_while_running_asks_for_nothing() { ++ let (pacer, calls, _) = recording_pacer(); ++ pacer.missed_frame(); ++ assert!(calls.borrow().is_empty()); ++ } ++ ++ #[test] + fn without_a_platform_toggle_nothing_is_tracked() { + let pacer = FramePacer::new(None); + let start = pacer.busy_at.get(); From 351864fd79a25a23716e1231b16f20576aa4f465 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:17:10 +0000 Subject: [PATCH 2/2] fix(gpui): keep deferred window callbacks quiet for closed windows --- .../patches/zed-closed-window-events.patch | 80 +++++++------------ 1 file changed, 31 insertions(+), 49 deletions(-) diff --git a/apps/desktop-gpui/patches/zed-closed-window-events.patch b/apps/desktop-gpui/patches/zed-closed-window-events.patch index 4d1903d3fce..5fe5373512a 100644 --- a/apps/desktop-gpui/patches/zed-closed-window-events.patch +++ b/apps/desktop-gpui/patches/zed-closed-window-events.patch @@ -1,5 +1,5 @@ diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs -index c543c6b..9b3e51d 100644 +index 6ac834a..4299e15 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -75,6 +75,24 @@ use crate::util::{ @@ -27,7 +27,25 @@ index c543c6b..9b3e51d 100644 /// Default window size used when no explicit size is provided. pub const DEFAULT_WINDOW_SIZE: Size = size(px(1536.), px(1095.)); -@@ -1624,7 +1642,7 @@ impl Window { +@@ -213,7 +231,7 @@ fn update_window_or_defer( + return; + }; + if !error.is::() { +- Result::<(), _>::Err(error).log_err(); ++ Result::<(), _>::Err(error).log_unless_closed(); + return; + } + let executor = cx.foreground_executor().clone(); +@@ -222,7 +240,7 @@ fn update_window_or_defer( + .spawn(async move { + handle + .update(&mut cx, |_, window, cx| update(window, cx)) +- .log_err(); ++ .log_unless_closed(); + }) + .detach(); + } +@@ -1671,7 +1689,7 @@ impl Window { // `complete_frame`) or the compositor won't send another callback. handle .update(&mut cx, |_, window, _| window.complete_frame()) @@ -36,7 +54,7 @@ index c543c6b..9b3e51d 100644 frame_pacer.frame_finished(true, true, now); return; } -@@ -1642,7 +1660,7 @@ impl Window { +@@ -1689,7 +1707,7 @@ impl Window { callback(window, cx); } }) @@ -45,7 +63,7 @@ index c543c6b..9b3e51d 100644 } // Keep presenting if input was recently arriving at a high rate (>= 60fps). -@@ -1666,20 +1684,20 @@ impl Window { +@@ -1713,20 +1731,20 @@ impl Window { window.present(); arena_clear_needed.clear(); }) @@ -69,43 +87,7 @@ index c543c6b..9b3e51d 100644 let pending = invalidator.is_dirty() || !pending_frame_callbacks.borrow().is_empty(); frame_pacer.frame_finished(busy, pending, now); -@@ -1690,7 +1708,7 @@ impl Window { - move |_, _| { - handle - .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) -- .log_err(); -+ .log_unless_closed(); - } - })); - platform_window.on_moved(Box::new({ -@@ -1698,7 +1716,7 @@ impl Window { - move || { - handle - .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) -- .log_err(); -+ .log_unless_closed(); - } - })); - platform_window.on_appearance_changed(Box::new({ -@@ -1706,7 +1724,7 @@ impl Window { - move || { - handle - .update(&mut cx, |_, window, cx| window.appearance_changed(cx)) -- .log_err(); -+ .log_unless_closed(); - } - })); - platform_window.on_button_layout_changed(Box::new({ -@@ -1714,7 +1732,7 @@ impl Window { - move || { - handle - .update(&mut cx, |_, window, cx| window.button_layout_changed(cx)) -- .log_err(); -+ .log_unless_closed(); - } - })); - platform_window.on_active_status_change(Box::new({ -@@ -1735,7 +1753,7 @@ impl Window { +@@ -1782,7 +1800,7 @@ impl Window { SystemWindowTabController::update_last_active(cx, window.handle.id); }) @@ -114,7 +96,7 @@ index c543c6b..9b3e51d 100644 } })); platform_window.on_hover_status_change(Box::new({ -@@ -1746,7 +1764,7 @@ impl Window { +@@ -1793,7 +1811,7 @@ impl Window { window.hovered.set(active); window.refresh(); }) @@ -123,7 +105,7 @@ index c543c6b..9b3e51d 100644 } })); platform_window.on_input({ -@@ -1754,7 +1772,7 @@ impl Window { +@@ -1801,7 +1819,7 @@ impl Window { Box::new(move |event| { handle .update(&mut cx, |_, window, cx| window.dispatch_event(event, cx)) @@ -132,7 +114,7 @@ index c543c6b..9b3e51d 100644 .unwrap_or(DispatchEventResult::default()) }) }); -@@ -1770,7 +1788,7 @@ impl Window { +@@ -1817,7 +1835,7 @@ impl Window { } None }) @@ -141,7 +123,7 @@ index c543c6b..9b3e51d 100644 .unwrap_or(None) }) }); -@@ -1781,7 +1799,7 @@ impl Window { +@@ -1828,7 +1846,7 @@ impl Window { .update(&mut cx, |_, _window, cx| { SystemWindowTabController::move_tab_to_new_window(cx, handle.window_id()); }) @@ -150,7 +132,7 @@ index c543c6b..9b3e51d 100644 }) }); platform_window.on_merge_all_windows({ -@@ -1791,7 +1809,7 @@ impl Window { +@@ -1838,7 +1856,7 @@ impl Window { .update(&mut cx, |_, _window, cx| { SystemWindowTabController::merge_all_windows(cx, handle.window_id()); }) @@ -159,7 +141,7 @@ index c543c6b..9b3e51d 100644 }) }); platform_window.on_select_next_tab({ -@@ -1801,7 +1819,7 @@ impl Window { +@@ -1848,7 +1866,7 @@ impl Window { .update(&mut cx, |_, _window, cx| { SystemWindowTabController::select_next_tab(cx, handle.window_id()); }) @@ -168,7 +150,7 @@ index c543c6b..9b3e51d 100644 }) }); platform_window.on_select_previous_tab({ -@@ -1811,7 +1829,7 @@ impl Window { +@@ -1858,7 +1876,7 @@ impl Window { .update(&mut cx, |_, _window, cx| { SystemWindowTabController::select_previous_tab(cx, handle.window_id()) }) @@ -177,7 +159,7 @@ index c543c6b..9b3e51d 100644 }) }); platform_window.on_toggle_tab_bar({ -@@ -1822,7 +1840,7 @@ impl Window { +@@ -1869,7 +1887,7 @@ impl Window { let tab_bar_visible = window.platform_window.tab_bar_visible(); SystemWindowTabController::set_visible(cx, tab_bar_visible); })