Defer drawing the window until the CoreAnimation displayLayer: method is called (#3592)

GPUI (both 1 and 2) currently performs rendering, layout, and painting
at the end of every effect cycle. This leads to poor performance when
the app receives events more frequently than the display refreshes. Such
rapid events can come from a terminal, an LSP, or a mouse with a high
polling rate.

This PR changes GPUI so that we don't render until the OS notifies us
that the content will be presented, via the `displayLayer:` callback.

Because render, layout, and paint have side effects that are sometimes
relied on in tests, we currently keep the old behavior (drawing after
every effects cycle) in tests.

This is similar to what @ForLoveOfCats explored in
https://github.com/zed-industries/zed/pull/3542, but slightly simpler,
in that we're not using the display link. As a follow-up, we could use
the display link to start rendering earlier, to possibly reduce latency
further.
This commit is contained in:
Max Brunsfeld 2023-12-11 13:03:04 -08:00 committed by GitHub
commit f12510b8b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 66 additions and 42 deletions

View file

@ -9,7 +9,6 @@ use derive_more::{Deref, DerefMut};
pub use entity_map::*; pub use entity_map::*;
pub use model_context::*; pub use model_context::*;
use refineable::Refineable; use refineable::Refineable;
use smallvec::SmallVec;
use smol::future::FutureExt; use smol::future::FutureExt;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub use test_context::*; pub use test_context::*;
@ -597,21 +596,25 @@ impl AppContext {
} }
} }
let dirty_window_ids = self for window in self.windows.values() {
.windows if let Some(window) = window.as_ref() {
.iter()
.filter_map(|(_, window)| {
let window = window.as_ref()?;
if window.dirty { if window.dirty {
Some(window.handle.clone()) window.platform_window.invalidate();
} else {
None
} }
}) }
.collect::<SmallVec<[_; 8]>>(); }
for dirty_window_handle in dirty_window_ids { #[cfg(any(test, feature = "test-support"))]
dirty_window_handle.update(self, |_, cx| cx.draw()).unwrap(); for window in self
.windows
.values()
.filter_map(|window| {
let window = window.as_ref()?;
window.dirty.then_some(window.handle)
})
.collect::<Vec<_>>()
{
self.update_window(window, |_, cx| cx.draw()).unwrap();
} }
} }

View file

@ -46,6 +46,8 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
Rc::new(MacPlatform::new()) Rc::new(MacPlatform::new())
} }
pub type DrawWindow = Box<dyn FnMut() -> Result<Scene>>;
pub(crate) trait Platform: 'static { pub(crate) trait Platform: 'static {
fn background_executor(&self) -> BackgroundExecutor; fn background_executor(&self) -> BackgroundExecutor;
fn foreground_executor(&self) -> ForegroundExecutor; fn foreground_executor(&self) -> ForegroundExecutor;
@ -66,6 +68,7 @@ pub(crate) trait Platform: 'static {
&self, &self,
handle: AnyWindowHandle, handle: AnyWindowHandle,
options: WindowOptions, options: WindowOptions,
draw: DrawWindow,
) -> Box<dyn PlatformWindow>; ) -> Box<dyn PlatformWindow>;
fn set_display_link_output_callback( fn set_display_link_output_callback(
@ -163,7 +166,7 @@ pub trait PlatformWindow {
fn on_close(&self, callback: Box<dyn FnOnce()>); fn on_close(&self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>); fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool; fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
fn draw(&self, scene: Scene); fn invalidate(&self);
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>; fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;

View file

@ -3,7 +3,8 @@ use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker,
MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, PlatformTextSystem, PlatformWindow, Result, Scene, SemanticVersion, VideoTimestamp,
WindowOptions,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use block::ConcreteBlock; use block::ConcreteBlock;
@ -490,8 +491,14 @@ impl Platform for MacPlatform {
&self, &self,
handle: AnyWindowHandle, handle: AnyWindowHandle,
options: WindowOptions, options: WindowOptions,
draw: Box<dyn FnMut() -> Result<Scene>>,
) -> Box<dyn PlatformWindow> { ) -> Box<dyn PlatformWindow> {
Box::new(MacWindow::open(handle, options, self.foreground_executor())) Box::new(MacWindow::open(
handle,
options,
draw,
self.foreground_executor(),
))
} }
fn set_display_link_output_callback( fn set_display_link_output_callback(

View file

@ -1,10 +1,10 @@
use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange}; use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
use crate::{ use crate::{
display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths, display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, DrawWindow, ExternalPaths,
FileDropEvent, ForegroundExecutor, GlobalPixels, InputEvent, KeyDownEvent, Keystroke, FileDropEvent, ForegroundExecutor, GlobalPixels, InputEvent, KeyDownEvent, Keystroke,
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
PromptLevel, Scene, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions, PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions,
}; };
use block::ConcreteBlock; use block::ConcreteBlock;
use cocoa::{ use cocoa::{
@ -45,6 +45,7 @@ use std::{
sync::{Arc, Weak}, sync::{Arc, Weak},
time::Duration, time::Duration,
}; };
use util::ResultExt;
const WINDOW_STATE_IVAR: &str = "windowState"; const WINDOW_STATE_IVAR: &str = "windowState";
@ -318,7 +319,7 @@ struct MacWindowState {
executor: ForegroundExecutor, executor: ForegroundExecutor,
native_window: id, native_window: id,
renderer: MetalRenderer, renderer: MetalRenderer,
scene_to_render: Option<Scene>, draw: Option<DrawWindow>,
kind: WindowKind, kind: WindowKind,
event_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>, event_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
activate_callback: Option<Box<dyn FnMut(bool)>>, activate_callback: Option<Box<dyn FnMut(bool)>>,
@ -454,6 +455,7 @@ impl MacWindow {
pub fn open( pub fn open(
handle: AnyWindowHandle, handle: AnyWindowHandle,
options: WindowOptions, options: WindowOptions,
draw: DrawWindow,
executor: ForegroundExecutor, executor: ForegroundExecutor,
) -> Self { ) -> Self {
unsafe { unsafe {
@ -545,7 +547,7 @@ impl MacWindow {
executor, executor,
native_window, native_window,
renderer: MetalRenderer::new(true), renderer: MetalRenderer::new(true),
scene_to_render: None, draw: Some(draw),
kind: options.kind, kind: options.kind,
event_callback: None, event_callback: None,
activate_callback: None, activate_callback: None,
@ -958,9 +960,8 @@ impl PlatformWindow for MacWindow {
} }
} }
fn draw(&self, scene: Scene) { fn invalidate(&self) {
let mut this = self.0.lock(); let this = self.0.lock();
this.scene_to_render = Some(scene);
unsafe { unsafe {
let _: () = msg_send![this.native_window.contentView(), setNeedsDisplay: YES]; let _: () = msg_send![this.native_window.contentView(), setNeedsDisplay: YES];
} }
@ -1442,8 +1443,11 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
extern "C" fn display_layer(this: &Object, _: Sel, _: id) { extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
unsafe { unsafe {
let window_state = get_window_state(this); let window_state = get_window_state(this);
let mut window_state = window_state.as_ref().lock(); let mut draw = window_state.lock().draw.take().unwrap();
if let Some(scene) = window_state.scene_to_render.take() { let scene = draw().log_err();
let mut window_state = window_state.lock();
window_state.draw = Some(draw);
if let Some(scene) = scene {
window_state.renderer.draw(&scene); window_state.renderer.draw(&scene);
} }
} }

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
Keymap, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, Keymap, Platform, PlatformDisplay, PlatformTextSystem, Scene, TestDisplay, TestWindow,
WindowOptions,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::VecDeque; use collections::VecDeque;
@ -135,6 +136,7 @@ impl Platform for TestPlatform {
&self, &self,
handle: AnyWindowHandle, handle: AnyWindowHandle,
options: WindowOptions, options: WindowOptions,
_draw: Box<dyn FnMut() -> Result<Scene>>,
) -> Box<dyn crate::PlatformWindow> { ) -> Box<dyn crate::PlatformWindow> {
*self.active_window.lock() = Some(handle); *self.active_window.lock() = Some(handle);
Box::new(TestWindow::new( Box::new(TestWindow::new(

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
PlatformInputHandler, PlatformWindow, Point, Scene, Size, TestPlatform, TileId, PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance,
WindowAppearance, WindowBounds, WindowOptions, WindowBounds, WindowOptions,
}; };
use collections::HashMap; use collections::HashMap;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -20,7 +20,6 @@ pub(crate) struct TestWindowHandlers {
pub struct TestWindow { pub struct TestWindow {
pub(crate) bounds: WindowBounds, pub(crate) bounds: WindowBounds,
current_scene: Mutex<Option<Scene>>,
display: Rc<dyn PlatformDisplay>, display: Rc<dyn PlatformDisplay>,
pub(crate) window_title: Option<String>, pub(crate) window_title: Option<String>,
pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>, pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>,
@ -37,7 +36,6 @@ impl TestWindow {
) -> Self { ) -> Self {
Self { Self {
bounds: options.bounds, bounds: options.bounds,
current_scene: Default::default(),
display, display,
platform, platform,
input_handler: None, input_handler: None,
@ -166,8 +164,8 @@ impl PlatformWindow for TestWindow {
unimplemented!() unimplemented!()
} }
fn draw(&self, scene: crate::Scene) { fn invalidate(&self) {
self.current_scene.lock().replace(scene); // (self.draw.lock())().unwrap();
} }
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> { fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {

View file

@ -7,9 +7,9 @@ use crate::{
ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, Path, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, Path,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString, Size, Style,
Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
}; };
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use collections::HashMap; use collections::HashMap;
@ -223,8 +223,8 @@ pub struct Window {
bounds: WindowBounds, bounds: WindowBounds,
bounds_observers: SubscriberSet<(), AnyObserver>, bounds_observers: SubscriberSet<(), AnyObserver>,
active: bool, active: bool,
activation_observers: SubscriberSet<(), AnyObserver>,
pub(crate) dirty: bool, pub(crate) dirty: bool,
activation_observers: SubscriberSet<(), AnyObserver>,
pub(crate) last_blur: Option<Option<FocusId>>, pub(crate) last_blur: Option<Option<FocusId>>,
pub(crate) focus: Option<FocusId>, pub(crate) focus: Option<FocusId>,
} }
@ -278,7 +278,14 @@ impl Window {
options: WindowOptions, options: WindowOptions,
cx: &mut AppContext, cx: &mut AppContext,
) -> Self { ) -> Self {
let platform_window = cx.platform.open_window(handle, options); let platform_window = cx.platform.open_window(
handle,
options,
Box::new({
let mut cx = cx.to_async();
move || handle.update(&mut cx, |_, cx| cx.draw())
}),
);
let display_id = platform_window.display().id(); let display_id = platform_window.display().id();
let sprite_atlas = platform_window.sprite_atlas(); let sprite_atlas = platform_window.sprite_atlas();
let mouse_position = platform_window.mouse_position(); let mouse_position = platform_window.mouse_position();
@ -349,8 +356,8 @@ impl Window {
bounds, bounds,
bounds_observers: SubscriberSet::new(), bounds_observers: SubscriberSet::new(),
active: false, active: false,
dirty: false,
activation_observers: SubscriberSet::new(), activation_observers: SubscriberSet::new(),
dirty: true,
last_blur: None, last_blur: None,
focus: None, focus: None,
} }
@ -673,7 +680,7 @@ impl<'a> WindowContext<'a> {
self.window.viewport_size = self.window.platform_window.content_size(); self.window.viewport_size = self.window.platform_window.content_size();
self.window.bounds = self.window.platform_window.bounds(); self.window.bounds = self.window.platform_window.bounds();
self.window.display_id = self.window.platform_window.display().id(); self.window.display_id = self.window.platform_window.display().id();
self.window.dirty = true; self.notify();
self.window self.window
.bounds_observers .bounds_observers
@ -1174,7 +1181,7 @@ impl<'a> WindowContext<'a> {
} }
/// Draw pixels to the display for this window based on the contents of its scene. /// Draw pixels to the display for this window based on the contents of its scene.
pub(crate) fn draw(&mut self) { pub(crate) fn draw(&mut self) -> Scene {
self.text_system().start_frame(); self.text_system().start_frame();
self.window.platform_window.clear_input_handler(); self.window.platform_window.clear_input_handler();
self.window.layout_engine.as_mut().unwrap().clear(); self.window.layout_engine.as_mut().unwrap().clear();
@ -1226,15 +1233,15 @@ impl<'a> WindowContext<'a> {
mem::swap(&mut window.rendered_frame, &mut window.next_frame); mem::swap(&mut window.rendered_frame, &mut window.next_frame);
let scene = self.window.rendered_frame.scene_builder.build(); let scene = self.window.rendered_frame.scene_builder.build();
self.window.platform_window.draw(scene);
let cursor_style = self let cursor_style = self
.window .window
.requested_cursor_style .requested_cursor_style
.take() .take()
.unwrap_or(CursorStyle::Arrow); .unwrap_or(CursorStyle::Arrow);
self.platform.set_cursor_style(cursor_style); self.platform.set_cursor_style(cursor_style);
self.window.dirty = false; self.window.dirty = false;
scene
} }
/// Dispatch a mouse or keyboard event on the window. /// Dispatch a mouse or keyboard event on the window.