diff --git a/CHANGELOG.md b/CHANGELOG.md index 1846321a00..0a45783047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased +- On Web, use `Window.requestAnimationFrame()` to throttle `RedrawRequested` events. +- On Wayland, use frame callbacks to throttle `RedrawRequested` events so redraws will align with compositor. +- Add `Window::pre_present_notify` to notify winit before presenting to the windowing system. - On Windows, added `WindowBuilderExtWindows::with_class_name` to customize the internal class name. - **Breaking:** Remove lifetime parameter from `Event` and `WindowEvent`. - **Breaking:** `ScaleFactorChanged` now contains a writer instead of a reference to update inner size. diff --git a/examples/window.rs b/examples/window.rs index fdfd39fa23..0c2729ec85 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -33,6 +33,8 @@ fn main() -> Result<(), impl std::error::Error> { window.request_redraw(); } Event::RedrawRequested(_) => { + // Notify the windowing system that we'll be presenting to the window. + window.pre_present_notify(); fill::fill_window(&window); } _ => (), diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 6d8f1e188f..86cf7f925b 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -813,6 +813,8 @@ impl Window { self.redraw_requester.request_redraw() } + pub fn pre_present_notify(&self) {} + pub fn inner_position(&self) -> Result, error::NotSupportedError> { Err(error::NotSupportedError::new()) } diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 710e08ea52..78927ed9c6 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -74,6 +74,8 @@ impl Inner { } } + pub fn pre_present_notify(&self) {} + pub fn inner_position(&self) -> Result, NotSupportedError> { unsafe { let safe_area = self.safe_area_screen_space(); diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index ae6d762f28..5294789698 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -555,12 +555,7 @@ impl Window { } } pub fn request_user_attention(&self, request_type: Option) { - match self { - #[cfg(x11_platform)] - Window::X(ref w) => w.request_user_attention(request_type), - #[cfg(wayland_platform)] - Window::Wayland(ref w) => w.request_user_attention(request_type), - } + x11_or_wayland!(match self; Window(w) => w.request_user_attention(request_type)) } #[inline] @@ -568,6 +563,11 @@ impl Window { x11_or_wayland!(match self; Window(w) => w.request_redraw()) } + #[inline] + pub fn pre_present_notify(&self) { + x11_or_wayland!(match self; Window(w) => w.pre_present_notify()) + } + #[inline] pub fn current_monitor(&self) -> Option { match self { diff --git a/src/platform_impl/linux/wayland/event_loop/mod.rs b/src/platform_impl/linux/wayland/event_loop/mod.rs index f33cde0f05..0527d6e5f8 100644 --- a/src/platform_impl/linux/wayland/event_loop/mod.rs +++ b/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -32,6 +32,7 @@ pub use proxy::EventLoopProxy; use sink::EventSink; use super::state::{WindowCompositorUpdate, WinitState}; +use super::window::state::FrameCallbackState; use super::{DeviceId, WindowId}; type WaylandDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>; @@ -494,22 +495,29 @@ impl EventLoop { mem::drop(state.windows.get_mut().remove(&window_id)); false } else { - let mut redraw_requested = window_requests - .get(&window_id) - .unwrap() - .take_redraw_requested(); - - // Redraw the frames while at it. - redraw_requested |= state + let mut window = state .windows .get_mut() .get_mut(&window_id) .unwrap() .lock() - .unwrap() - .refresh_frame(); - - redraw_requested + .unwrap(); + + if window.frame_callback_state() == FrameCallbackState::Requested { + false + } else { + // Reset the frame callbacks state. + window.frame_callback_reset(); + let mut redraw_requested = window_requests + .get(&window_id) + .unwrap() + .take_redraw_requested(); + + // Redraw the frame while at it. + redraw_requested |= window.refresh_frame(); + + redraw_requested + } } }); diff --git a/src/platform_impl/linux/wayland/state.rs b/src/platform_impl/linux/wayland/state.rs index f9b7c4afca..849a48f0c4 100644 --- a/src/platform_impl/linux/wayland/state.rs +++ b/src/platform_impl/linux/wayland/state.rs @@ -321,7 +321,15 @@ impl CompositorHandler for WinitState { self.scale_factor_changed(surface, scale_factor as f64, true) } - fn frame(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: u32) {} + fn frame(&mut self, _: &Connection, _: &QueueHandle, surface: &WlSurface, _: u32) { + let window_id = super::make_wid(surface); + let window = match self.windows.get_mut().get(&window_id) { + Some(window) => window, + None => return, + }; + + window.lock().unwrap().frame_callback_received(); + } } impl ProvidesRegistryState for WinitState { diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 3b332aca99..26bb8ba6f0 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -38,7 +38,7 @@ use super::state::WinitState; use super::types::xdg_activation::XdgActivationTokenData; use super::{EventLoopWindowTarget, WindowId}; -mod state; +pub(crate) mod state; pub use state::WindowState; @@ -293,6 +293,11 @@ impl Window { self.event_loop_awakener.ping(); } + #[inline] + pub fn pre_present_notify(&self) { + self.window_state.lock().unwrap().request_frame_callback(); + } + #[inline] pub fn outer_size(&self) -> PhysicalSize { let window_state = self.window_state.lock().unwrap(); diff --git a/src/platform_impl/linux/wayland/window/state.rs b/src/platform_impl/linux/wayland/window/state.rs index 479413e6fc..e0c4920290 100644 --- a/src/platform_impl/linux/wayland/window/state.rs +++ b/src/platform_impl/linux/wayland/window/state.rs @@ -125,6 +125,9 @@ pub struct WindowState { /// sends `None` for the new size in the configure. stateless_size: LogicalSize, + /// The state of the frame callback. + frame_callback_state: FrameCallbackState, + viewport: Option, fractional_scale: Option, @@ -134,26 +137,62 @@ pub struct WindowState { has_pending_move: Option, } -/// The state of the cursor grabs. -#[derive(Clone, Copy)] -struct GrabState { - /// The grab mode requested by the user. - user_grab_mode: CursorGrabMode, - - /// The current grab mode. - current_grab_mode: CursorGrabMode, -} +impl WindowState { + /// Create new window state. + pub fn new( + connection: Connection, + queue_handle: &QueueHandle, + winit_state: &WinitState, + size: LogicalSize, + window: Window, + theme: Option, + ) -> Self { + let compositor = winit_state.compositor_state.clone(); + let pointer_constraints = winit_state.pointer_constraints.clone(); + let viewport = winit_state + .viewporter_state + .as_ref() + .map(|state| state.get_viewport(window.wl_surface(), queue_handle)); + let fractional_scale = winit_state + .fractional_scaling_manager + .as_ref() + .map(|fsm| fsm.fractional_scaling(window.wl_surface(), queue_handle)); -impl GrabState { - fn new() -> Self { Self { - user_grab_mode: CursorGrabMode::None, - current_grab_mode: CursorGrabMode::None, + compositor, + connection, + csd_fails: false, + cursor_grab_mode: GrabState::new(), + cursor_icon: CursorIcon::Default, + cursor_visible: true, + decorate: true, + fractional_scale, + frame: None, + frame_callback_state: FrameCallbackState::None, + has_focus: false, + has_pending_move: None, + ime_allowed: false, + ime_purpose: ImePurpose::Normal, + last_configure: None, + max_inner_size: None, + min_inner_size: MIN_WINDOW_SIZE, + pointer_constraints, + pointers: Default::default(), + queue_handle: queue_handle.clone(), + resizable: true, + scale_factor: 1., + shm: winit_state.shm.wl_shm().clone(), + size, + stateless_size: size, + text_inputs: Vec::new(), + theme, + title: String::default(), + transparent: false, + viewport, + window: ManuallyDrop::new(window), } } -} -impl WindowState { /// Apply closure on the given pointer. fn apply_on_poiner, &WinitPointerData)>( &self, @@ -168,6 +207,33 @@ impl WindowState { }) } + /// Get the current state of the frame callback. + pub fn frame_callback_state(&self) -> FrameCallbackState { + self.frame_callback_state + } + + /// The frame callback was received, but not yet sent to the user. + pub fn frame_callback_received(&mut self) { + self.frame_callback_state = FrameCallbackState::Received; + } + + /// Reset the frame callbacks state. + pub fn frame_callback_reset(&mut self) { + self.frame_callback_state = FrameCallbackState::None; + } + + /// Request a frame callback if we don't have one for this window in flight. + pub fn request_frame_callback(&mut self) { + let surface = self.window.wl_surface(); + match self.frame_callback_state { + FrameCallbackState::None | FrameCallbackState::Received => { + self.frame_callback_state = FrameCallbackState::Requested; + surface.frame(&self.queue_handle, surface.clone()); + } + FrameCallbackState::Requested => (), + } + } + pub fn configure( &mut self, configure: WindowConfigure, @@ -391,60 +457,6 @@ impl WindowState { } } - /// Create new window state. - pub fn new( - connection: Connection, - queue_handle: &QueueHandle, - winit_state: &WinitState, - size: LogicalSize, - window: Window, - theme: Option, - ) -> Self { - let compositor = winit_state.compositor_state.clone(); - let pointer_constraints = winit_state.pointer_constraints.clone(); - let viewport = winit_state - .viewporter_state - .as_ref() - .map(|state| state.get_viewport(window.wl_surface(), queue_handle)); - let fractional_scale = winit_state - .fractional_scaling_manager - .as_ref() - .map(|fsm| fsm.fractional_scaling(window.wl_surface(), queue_handle)); - - Self { - compositor, - connection, - theme, - csd_fails: false, - decorate: true, - cursor_grab_mode: GrabState::new(), - cursor_icon: CursorIcon::Default, - cursor_visible: true, - fractional_scale, - frame: None, - has_focus: false, - ime_allowed: false, - ime_purpose: ImePurpose::Normal, - last_configure: None, - max_inner_size: None, - min_inner_size: MIN_WINDOW_SIZE, - pointer_constraints, - pointers: Default::default(), - queue_handle: queue_handle.clone(), - scale_factor: 1., - shm: winit_state.shm.wl_shm().clone(), - size, - stateless_size: size, - text_inputs: Vec::new(), - title: String::default(), - transparent: false, - resizable: true, - viewport, - window: ManuallyDrop::new(window), - has_pending_move: None, - } - } - /// Get the outer size of the window. #[inline] pub fn outer_size(&self) -> LogicalSize { @@ -892,6 +904,37 @@ impl Drop for WindowState { } } +/// The state of the cursor grabs. +#[derive(Clone, Copy)] +struct GrabState { + /// The grab mode requested by the user. + user_grab_mode: CursorGrabMode, + + /// The current grab mode. + current_grab_mode: CursorGrabMode, +} + +impl GrabState { + fn new() -> Self { + Self { + user_grab_mode: CursorGrabMode::None, + current_grab_mode: CursorGrabMode::None, + } + } +} + +/// The state of the frame callback. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameCallbackState { + /// No frame callback was requsted. + #[default] + None, + /// The frame callback was requested, but not yet arrived, the redraw events are throttled. + Requested, + /// The callback was marked as done, and user could receive redraw requested + Received, +} + impl From for ResizeEdge { fn from(value: ResizeDirection) -> Self { match value { diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 0d0f52fb5c..a0d90d31d0 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1731,6 +1731,11 @@ impl UnownedWindow { .unwrap(); } + #[inline] + pub fn pre_present_notify(&self) { + // TODO timer + } + #[inline] pub fn raw_window_handle(&self) -> RawWindowHandle { let mut window_handle = XlibWindowHandle::empty(); diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index f174753a84..23e9d25539 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -569,6 +569,9 @@ impl WinitWindow { AppState::queue_redraw(RootWindowId(self.id())); } + #[inline] + pub fn pre_present_notify(&self) {} + pub fn outer_position(&self) -> Result, NotSupportedError> { let frame_rect = self.frame(); let position = LogicalPosition::new( diff --git a/src/platform_impl/orbital/window.rs b/src/platform_impl/orbital/window.rs index 11ec6c7ee4..4cbc4fecd0 100644 --- a/src/platform_impl/orbital/window.rs +++ b/src/platform_impl/orbital/window.rs @@ -166,6 +166,9 @@ impl Window { } } + #[inline] + pub fn pre_present_notify(&self) {} + #[inline] pub fn reset_dead_keys(&self) { // TODO? diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 72893f067a..f38b4804bf 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -735,7 +735,10 @@ impl EventLoopWindowTarget { } canvas_clone.borrow_mut().is_intersecting = Some(is_intersecting); - }) + }); + + let runner = self.runner.clone(); + canvas.on_animation_frame(move || runner.request_redraw(RootWindowId(id))); } pub fn available_monitors(&self) -> VecDequeIter { diff --git a/src/platform_impl/web/web_sys/animation_frame.rs b/src/platform_impl/web/web_sys/animation_frame.rs new file mode 100644 index 0000000000..75a0955138 --- /dev/null +++ b/src/platform_impl/web/web_sys/animation_frame.rs @@ -0,0 +1,62 @@ +use std::cell::Cell; +use std::rc::Rc; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; + +pub struct AnimationFrameHandler { + window: web_sys::Window, + closure: Closure, + handle: Rc>>, +} + +impl AnimationFrameHandler { + pub fn new(window: web_sys::Window) -> Self { + let handle = Rc::new(Cell::new(None)); + let closure = Closure::new({ + let handle = handle.clone(); + move || handle.set(None) + }); + + Self { + window, + closure, + handle, + } + } + + pub fn on_animation_frame(&mut self, mut f: F) + where + F: 'static + FnMut(), + { + let handle = self.handle.clone(); + self.closure = Closure::new(move || { + handle.set(None); + f(); + }) + } + + pub fn request(&self) { + if let Some(handle) = self.handle.take() { + self.window + .cancel_animation_frame(handle) + .expect("Failed to cancel animation frame"); + } + + let handle = self + .window + .request_animation_frame(self.closure.as_ref().unchecked_ref()) + .expect("Failed to request animation frame"); + + self.handle.set(Some(handle)); + } +} + +impl Drop for AnimationFrameHandler { + fn drop(&mut self) { + if let Some(handle) = self.handle.take() { + self.window + .cancel_animation_frame(handle) + .expect("Failed to cancel animation frame"); + } + } +} diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 89ad8211f4..0303a0bd81 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -19,6 +19,7 @@ use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes}; use crate::window::{WindowAttributes, WindowId as RootWindowId}; use super::super::WindowId; +use super::animation_frame::AnimationFrameHandler; use super::event_handle::EventListenerHandle; use super::intersection_handle::IntersectionObserverHandle; use super::media_query_handle::MediaQueryListHandle; @@ -42,6 +43,7 @@ pub struct Canvas { pointer_handler: PointerHandler, on_resize_scale: Option, on_intersect: Option, + animation_frame_handler: AnimationFrameHandler, } pub struct Common { @@ -98,7 +100,7 @@ impl Canvas { .expect("Invalid pseudo-element"); let common = Common { - window, + window: window.clone(), document, raw: canvas, style, @@ -151,6 +153,7 @@ impl Canvas { pointer_handler: PointerHandler::new(), on_resize_scale: None, on_intersect: None, + animation_frame_handler: AnimationFrameHandler::new(window), }) } @@ -441,6 +444,13 @@ impl Canvas { self.on_intersect = Some(IntersectionObserverHandle::new(self.raw(), handler)); } + pub(crate) fn on_animation_frame(&mut self, f: F) + where + F: 'static + FnMut(), + { + self.animation_frame_handler.on_animation_frame(f) + } + pub fn request_fullscreen(&self) { self.common.request_fullscreen() } @@ -449,6 +459,10 @@ impl Canvas { self.common.is_fullscreen() } + pub fn request_animation_frame(&self) { + self.animation_frame_handler.request(); + } + pub(crate) fn handle_scale_change( &self, runner: &super::super::event_loop::runner::Shared, diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 06526975b9..7f2abc05e9 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -1,3 +1,4 @@ +mod animation_frame; mod canvas; pub mod event; mod event_handle; diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index c267f8d6ff..bbaaa46604 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -30,7 +30,6 @@ pub struct Inner { document: Document, canvas: Rc>, previous_pointer: RefCell<&'static str>, - register_redraw_request: Box, destroy_fn: Option>, } @@ -40,8 +39,6 @@ impl Window { attr: WindowAttributes, platform_attr: PlatformSpecificWindowBuilderAttributes, ) -> Result { - let runner = target.runner.clone(); - let id = target.generate_id(); let prevent_default = platform_attr.prevent_default; @@ -52,8 +49,6 @@ impl Window { backend::Canvas::create(id, window.clone(), document.clone(), &attr, platform_attr)?; let canvas = Rc::new(RefCell::new(canvas)); - let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); - target.register(&canvas, id, prevent_default); let runner = target.runner.clone(); @@ -68,7 +63,6 @@ impl Window { document: document.clone(), canvas, previous_pointer: RefCell::new("auto"), - register_redraw_request, destroy_fn: Some(destroy_fn), }) .unwrap(), @@ -110,10 +104,13 @@ impl Window { } pub fn request_redraw(&self) { - self.inner - .dispatch(|inner| (inner.register_redraw_request)()); + self.inner.dispatch(move |inner| { + inner.canvas.borrow().request_animation_frame(); + }); } + pub fn pre_present_notify(&self) {} + pub fn outer_position(&self) -> Result, NotSupportedError> { self.inner.queue(|inner| { Ok(inner diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 1a11b2b667..070baa8f43 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -142,6 +142,9 @@ impl Window { } } + #[inline] + pub fn pre_present_notify(&self) {} + #[inline] pub fn outer_position(&self) -> Result, NotSupportedError> { util::WindowArea::Outer.get_rect(self.hwnd()) diff --git a/src/window.rs b/src/window.rs index 017501f6b1..be515a9546 100644 --- a/src/window.rs +++ b/src/window.rs @@ -524,11 +524,12 @@ impl Window { self.window.scale_factor() } - /// Requests a future [`Event::RedrawRequested`] event to be emitted in a way that is - /// synchronized and / or throttled by the windowing system. + /// Queues a [`Event::RedrawRequested`] event to be emitted that aligns with the windowing + /// system drawing loop. /// /// This is the **strongly encouraged** method of redrawing windows, as it can integrate with - /// OS-requested redraws (e.g. when a window gets resized). + /// OS-requested redraws (e.g. when a window gets resized). To improve the event delivery + /// consider using [`Window::pre_present_notify`] as described in docs. /// /// Applications should always aim to redraw whenever they receive a `RedrawRequested` event. /// @@ -536,11 +537,17 @@ impl Window { /// with respect to other events, since the requirements can vary significantly between /// windowing systems. /// + /// However as the event aligns with the windowing system drawing loop, it may not arrive in + /// same or even next event loop iteration. + /// /// ## Platform-specific /// /// - **Windows** This API uses `RedrawWindow` to request a `WM_PAINT` message and `RedrawRequested` /// is emitted in sync with any `WM_PAINT` messages /// - **iOS:** Can only be called on the main thread. + /// - **Wayland:** The events are aligned with the frame callbacks when [`Window::pre_present_notify`] + /// is used. + /// - **Web:** [`Event::RedrawRequested`] will be aligned with the `requestAnimationFrame`. /// /// [`Event::RedrawRequested`]: crate::event::Event::RedrawRequested #[inline] @@ -548,6 +555,43 @@ impl Window { self.window.request_redraw() } + /// Notify the windowing system that you're before presenting to the window. + /// + /// You should call this event after you've done drawing operations, but before you submit + /// the buffer to the display or commit your drawings. Doing so will help winit to properly + /// schedule and do assumptions about its internal state. For example, it could properly + /// throttle [`Event::RedrawRequested`]. + /// + /// ## Example + /// + /// This example illustrates how it looks with OpenGL, but it applies to other graphics + /// APIs and software rendering. + /// + /// ```no_run + /// # use winit::event_loop::EventLoop; + /// # use winit::window::Window; + /// # let mut event_loop = EventLoop::new(); + /// # let window = Window::new(&event_loop).unwrap(); + /// # fn swap_buffers() {} + /// // Do the actual drawing with OpenGL. + /// + /// // Notify winit that we're about to submit buffer to the windowing system. + /// window.pre_present_notify(); + /// + /// // Sumbit buffer to the windowing system. + /// swap_buffers(); + /// ``` + /// + /// ## Platform-specific + /// + /// **Wayland:** - schedules a frame callback to throttle [`Event::RedrawRequested`]. + /// + /// [`Event::RedrawRequested`]: crate::event::Event::RedrawRequested + #[inline] + pub fn pre_present_notify(&self) { + self.window.pre_present_notify(); + } + /// Reset the dead key state of the keyboard. /// /// This is useful when a dead key is bound to trigger an action. Then