From 0fb68dccd2a40c7935898686f1bb8770700f76c0 Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Fri, 16 Jun 2023 19:11:44 +0400 Subject: [PATCH 1/3] Add `FrameThrottled` event and a way to request it Add a way to request a frame throttling hint from the windowing system. This is essential for platforms like Wayland where you don't have real vsync in OpenGL/Vulkan and you're advised to use 'frame callbacks' to throttle your rendering. `Window::request_frame_throttling_hint` is a an end point for users to request `FrameThrottled` event, which returns error when it can't be done, so the users could fallback to their own scheduling strategies. The API doesn't try to address all sort of frame presentation cases, like how much the frame took, etc. Such cases could be covered later, with different events in addition to the throttling hint. For now implemented only for Wayland. Fixes #2412. --- CHANGELOG.md | 1 + src/event.rs | 10 ++++++++++ src/platform_impl/android/mod.rs | 4 ++++ src/platform_impl/ios/window.rs | 4 ++++ src/platform_impl/linux/mod.rs | 11 +++++----- src/platform_impl/linux/wayland/state.rs | 7 ++++++- src/platform_impl/linux/wayland/window/mod.rs | 6 ++++++ src/platform_impl/linux/x11/window.rs | 5 +++++ src/platform_impl/macos/window.rs | 4 ++++ src/platform_impl/orbital/window.rs | 5 +++++ src/platform_impl/web/window.rs | 5 +++++ src/platform_impl/windows/window.rs | 5 +++++ src/window.rs | 20 +++++++++++++++++++ 13 files changed, 80 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9229c5f24c..124b88eeea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased +- **Breaking:** Add `WindowEvent::FrameThrottled` event and `Window::request_frame_throttling_hint` to request it. - On Android, changed default behavior of Android to ignore volume keys letting the operating system handle them. - On Android, added `EventLoopBuilderExtAndroid::handle_volume_keys` to indicate that the application will handle the volume keys manually. - **Breaking:** Rename `DeviceEventFilter` to `DeviceEvents` reversing the behavior of variants. diff --git a/src/event.rs b/src/event.rs index 4111e1f64a..b37015d831 100644 --- a/src/event.rs +++ b/src/event.rs @@ -366,6 +366,14 @@ pub enum WindowEvent<'a> { /// hovered. HoveredFileCancelled, + /// An event from windowing system when it is a good time to start drawing a new frame. + /// + /// This is useful for throttling redrawing operation and driving animations. The event + /// is delivered in reaction to [`Window::request_frame_throttling_hint`]. + /// + /// For platform specifics see [`Window::request_frame_throttling_hint`] request. + FrameThrottled, + /// The window gained or lost focus. /// /// The parameter is true if the window has gained focus, and false if it has lost focus. @@ -553,6 +561,7 @@ impl Clone for WindowEvent<'static> { Moved(pos) => Moved(*pos), CloseRequested => CloseRequested, Destroyed => Destroyed, + FrameThrottled => FrameThrottled, DroppedFile(file) => DroppedFile(file.clone()), HoveredFile(file) => HoveredFile(file.clone()), HoveredFileCancelled => HoveredFileCancelled, @@ -656,6 +665,7 @@ impl<'a> WindowEvent<'a> { Moved(position) => Some(Moved(position)), CloseRequested => Some(CloseRequested), Destroyed => Some(Destroyed), + FrameThrottled => Some(FrameThrottled), DroppedFile(file) => Some(DroppedFile(file)), HoveredFile(file) => Some(HoveredFile(file)), HoveredFileCancelled => Some(HoveredFileCancelled), diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 1f8dd8ea91..311be549db 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -808,6 +808,10 @@ impl Window { self.redraw_requester.request_redraw() } + pub fn request_frame_throttling_hint(&self) -> Result<(), error::NotSupportedError> { + Err(error::NotSupportedError::new()) + } + 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 f773db5e6b..79881bc308 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -75,6 +75,10 @@ impl Inner { } } + pub fn request_frame_throttling_hint(&self) -> Result<(), NotSupportedError> { + Err(NotSupportedError::new()) + } + 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 03db21ed3e..d93102f4fd 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -541,12 +541,11 @@ 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)) + } + + pub fn request_frame_throttling_hint(&self) -> Result<(), NotSupportedError> { + x11_or_wayland!(match self; Window(w) => w.request_frame_throttling_hint()) } #[inline] diff --git a/src/platform_impl/linux/wayland/state.rs b/src/platform_impl/linux/wayland/state.rs index f9b7c4afca..de2300a0b8 100644 --- a/src/platform_impl/linux/wayland/state.rs +++ b/src/platform_impl/linux/wayland/state.rs @@ -23,6 +23,7 @@ use sctk::shm::{Shm, ShmHandler}; use sctk::subcompositor::SubcompositorState; use crate::dpi::LogicalSize; +use crate::event::WindowEvent; use super::event_loop::sink::EventSink; use super::output::MonitorHandle; @@ -321,7 +322,11 @@ 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); + self.events_sink + .push_window_event(WindowEvent::FrameThrottled, window_id); + } } 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 76168f3620..e59e65796e 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -489,6 +489,12 @@ impl Window { xdg_activation_token.commit(); } + pub fn request_frame_throttling_hint(&self) -> Result<(), NotSupportedError> { + let surface = self.window.wl_surface(); + surface.frame(&self.queue_handle, surface.clone()); + Ok(()) + } + #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { self.window_state.lock().unwrap().set_cursor_grab(mode) diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index adbcf34ebc..304c873c8a 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1583,6 +1583,11 @@ impl UnownedWindow { .expect("Failed to set urgency hint"); } + #[inline] + pub fn request_frame_throttling_hint(&self) -> Result<(), NotSupportedError> { + Err(NotSupportedError::new()) + } + #[inline] pub fn id(&self) -> WindowId { WindowId(self.xwindow as _) diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 05aad7b191..91ef0d151c 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -536,6 +536,10 @@ impl WinitWindow { AppState::queue_redraw(RootWindowId(self.id())); } + pub fn request_frame_throttling_hint(&self) -> Result<(), NotSupportedError> { + Err(NotSupportedError::new()) + } + 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 1afe5cdd94..5053117921 100644 --- a/src/platform_impl/orbital/window.rs +++ b/src/platform_impl/orbital/window.rs @@ -166,6 +166,11 @@ impl Window { } } + #[inline] + pub fn request_frame_throttling_hint(&self) -> Result<(), error::NotSupportedError> { + Err(error::NotSupportedError::new()) + } + #[inline] pub fn reset_dead_keys(&self) { // TODO? diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 6a379921b5..df361ca008 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -351,6 +351,11 @@ impl Window { // Currently an intentional no-op } + #[inline] + pub fn request_frame_throttling_hint(&self) -> Result<(), NotSupportedError> { + Err(NotSupportedError::new()) + } + #[inline] pub fn current_monitor(&self) -> Option { Some(MonitorHandle) diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 1556c1bbf1..b24dd44e31 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -141,6 +141,11 @@ impl Window { } } + #[inline] + pub fn request_frame_throttling_hint(&self) -> Result<(), NotSupportedError> { + Err(NotSupportedError::new()) + } + #[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 55d9f704f5..84c4c6afe0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1127,6 +1127,26 @@ impl Window { self.window.request_user_attention(request_type) } + /// Request a frame throttling hint from the windowing system. + /// + /// The error is returned when frame couldn't be requested and the user should use their + /// fallback renderer scheduling logic, like using timer based on monitor refresh rate or + /// graphics API extensions (VSYNC). + /// + /// For more see [`FrameThrottled`] event. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Uses frame callbacks. The user must perform drawing operation resulting in + /// `wl_surface.commit`(eglSwapBuffers, etc) after issueing a request. + /// - ** macOS / Windows / iOS / Android / Web / X11 / Orbital:** Not supported. + /// + /// [`FrameThrottled`]: crate::event::WindowEvent::FrameThrottled. + #[inline] + pub fn request_frame_throttling_hint(&self) -> Result<(), NotSupportedError> { + self.window.request_frame_throttling_hint() + } + /// Sets the current window theme. Use `None` to fallback to system default. /// /// ## Platform-specific From 02afd228eecf73279129344d7fa7e9895fd0f25f Mon Sep 17 00:00:00 2001 From: dAxpeDDa Date: Sat, 17 Jun 2023 11:52:29 +0200 Subject: [PATCH 2/3] Implement `FrameThrottled` for Web --- .../web/event_loop/window_target.rs | 8 +++ src/platform_impl/web/web_sys/canvas.rs | 17 ++++- .../web/web_sys/frame_throttling.rs | 62 +++++++++++++++++++ src/platform_impl/web/web_sys/mod.rs | 2 + src/platform_impl/web/window.rs | 5 +- src/window.rs | 2 +- 6 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 src/platform_impl/web/web_sys/frame_throttling.rs diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 8cfbb42756..a4c62a8313 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -719,6 +719,14 @@ impl EventLoopWindowTarget { } }, ); + + let runner = self.runner.clone(); + canvas.on_frame_throttle(move || { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::FrameThrottled, + }) + }) } pub fn available_monitors(&self) -> VecDequeIter { diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 67a383e8a5..07f2db27ea 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -2,7 +2,7 @@ use super::super::WindowId; use super::event_handle::EventListenerHandle; use super::media_query_handle::MediaQueryListHandle; use super::pointer::PointerHandler; -use super::{event, ButtonsState, ResizeScaleHandle}; +use super::{event, ButtonsState, FrameThrottlingHandler, ResizeScaleHandle}; use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::error::OsError as RootOE; use crate::event::{Force, MouseButton, MouseScrollDelta}; @@ -37,6 +37,7 @@ pub struct Canvas { on_dark_mode: Option, pointer_handler: PointerHandler, on_resize_scale: Option, + frame_throttling_handler: FrameThrottlingHandler, } pub struct Common { @@ -87,7 +88,7 @@ impl Canvas { Ok(Canvas { common: Common { - window, + window: window.clone(), raw: canvas, old_size: Rc::default(), current_size: Rc::default(), @@ -105,6 +106,7 @@ impl Canvas { on_dark_mode: None, pointer_handler: PointerHandler::new(), on_resize_scale: None, + frame_throttling_handler: FrameThrottlingHandler::new(window), }) } @@ -365,6 +367,13 @@ impl Canvas { )); } + pub(crate) fn on_frame_throttle(&mut self, f: F) + where + F: 'static + FnMut(), + { + self.frame_throttling_handler.on_frame_throttle(f) + } + pub fn request_fullscreen(&self) { self.common.request_fullscreen() } @@ -373,6 +382,10 @@ impl Canvas { self.common.is_fullscreen() } + pub fn request_frame_throttling_hint(&self) { + self.frame_throttling_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/frame_throttling.rs b/src/platform_impl/web/web_sys/frame_throttling.rs new file mode 100644 index 0000000000..c289fa146e --- /dev/null +++ b/src/platform_impl/web/web_sys/frame_throttling.rs @@ -0,0 +1,62 @@ +use std::cell::Cell; +use std::rc::Rc; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; + +pub struct FrameThrottlingHandler { + window: web_sys::Window, + closure: Closure, + handle: Rc>>, +} + +impl FrameThrottlingHandler { + 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_frame_throttle(&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 FrameThrottlingHandler { + 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/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 367680d201..c7f78c586f 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -1,6 +1,7 @@ mod canvas; pub mod event; mod event_handle; +mod frame_throttling; mod media_query_handle; mod pointer; mod resize_scaling; @@ -9,6 +10,7 @@ mod timeout; pub use self::canvas::Canvas; pub use self::event::ButtonsState; pub use self::event_handle::EventListenerHandle; +pub use self::frame_throttling::FrameThrottlingHandler; pub use self::resize_scaling::ResizeScaleHandle; pub use self::timeout::{IdleCallback, Timeout}; diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index df361ca008..ccc4bb3568 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -353,7 +353,10 @@ impl Window { #[inline] pub fn request_frame_throttling_hint(&self) -> Result<(), NotSupportedError> { - Err(NotSupportedError::new()) + self.inner.dispatch(move |inner| { + inner.canvas.borrow().request_frame_throttling_hint(); + }); + Ok(()) } #[inline] diff --git a/src/window.rs b/src/window.rs index 84c4c6afe0..d3058f01cb 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1139,7 +1139,7 @@ impl Window { /// /// - **Wayland:** Uses frame callbacks. The user must perform drawing operation resulting in /// `wl_surface.commit`(eglSwapBuffers, etc) after issueing a request. - /// - ** macOS / Windows / iOS / Android / Web / X11 / Orbital:** Not supported. + /// - ** macOS / Windows / iOS / Android / X11 / Orbital:** Not supported. /// /// [`FrameThrottled`]: crate::event::WindowEvent::FrameThrottled. #[inline] From 9084605b7a014946597280937fa2474c70021b62 Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Sat, 17 Jun 2023 12:41:01 +0000 Subject: [PATCH 3/3] Update src/window.rs Co-authored-by: daxpedda --- src/window.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/window.rs b/src/window.rs index d3058f01cb..9592daa8a6 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1137,8 +1137,8 @@ impl Window { /// /// ## Platform-specific /// - /// - **Wayland:** Uses frame callbacks. The user must perform drawing operation resulting in - /// `wl_surface.commit`(eglSwapBuffers, etc) after issueing a request. + /// - **Wayland:** `wl_surface.commit` must be called for this event to fire. + /// This is usually done by drawing operations, e.g. `eglSwapBuffers`. /// - ** macOS / Windows / iOS / Android / X11 / Orbital:** Not supported. /// /// [`FrameThrottled`]: crate::event::WindowEvent::FrameThrottled.