Skip to content
Open
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
15 changes: 14 additions & 1 deletion winit-core/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,10 +502,23 @@ pub enum WindowEvent {
/// [`applicationDidEnterBackground`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground
/// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle
///
/// ### Wayland
///
/// Emitted when the compositor adds or removes the xdg-shell `suspended` state (xdg_wm_base
/// version 6 or later), for example while the window is on a hidden workspace or the outputs
/// are off. A suspended window receives no frame callbacks, so a frame presented with vsync
/// may block until the window is shown again.
///
/// Winit still asks for a redraw after the configure that suspends the window, as for any
/// other configure, since the application's next commit is what applies it. An application
/// that stops presenting on `Occluded(true)`, or only presents in ways that can't block,
/// should call `WindowExtWayland::notify_presentation_paused` once it has; winit then commits
/// the window's configures itself until `Occluded(false)`, when it asks for a redraw.
///
/// ### Others
///
/// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`].
/// - **Android / Wayland / Windows / Orbital:** Unsupported.
/// - **Android / Windows / Orbital:** Unsupported.
///
/// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border
/// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
Expand Down
8 changes: 8 additions & 0 deletions winit-wayland/src/event_loop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,14 @@ impl EventLoop {
app.window_event(&self.active_event_loop, window_id, event);
}

if let Some(occluded) = compositor_update.occluded {
app.window_event(
&self.active_event_loop,
window_id,
WindowEvent::Occluded(occluded),
);
}

if compositor_update.close_window {
app.window_event(&self.active_event_loop, window_id, WindowEvent::CloseRequested);
}
Expand Down
34 changes: 34 additions & 0 deletions winit-wayland/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,47 @@ pub trait EventLoopBuilderExtWayland {
pub trait WindowExtWayland {
/// Returns `xdg_toplevel` of the window or [`None`] if the window is X11 window.
fn xdg_toplevel(&self) -> Option<NonNull<c_void>>;

/// Tells winit that the application has stopped presenting after
/// [`WindowEvent::Occluded(true)`][occluded], so winit may commit the surface on its behalf.
///
/// Every configure has to be committed to take effect, and some compositors wait for that
/// commit, up to a timeout, before they finish rearranging windows. Winit normally asks for a
/// redraw so the application's next frame commits it, but a suspended window gets no frame
/// callbacks, so a frame presented with vsync may block until the window is shown again.
///
/// Once called, winit commits the surface right away and after every later configure while
/// the window stays suspended, without asking for a redraw. This ends when the window stops
/// being suspended: winit then emits `Occluded(false)` and asks for a redraw as usual, so
/// call this again after the next `Occluded(true)`. It does nothing while the window isn't
/// suspended.
///
/// The application may keep presenting while occluded as long as presenting can't block, for
/// example at a low rate with a swap interval of 0 or the mailbox present mode. It shouldn't
/// call [`Window::pre_present_notify`] for those frames: the frame callback it requests isn't
/// sent while the window is suspended, and winit holds back [`RedrawRequested`] until it is.
///
/// An application that never calls this behaves as before: winit asks it to redraw after
/// each configure, including the ones that suspend the window.
///
/// [occluded]: winit_core::event::WindowEvent::Occluded
/// [`RedrawRequested`]: winit_core::event::WindowEvent::RedrawRequested
/// [`Window::pre_present_notify`]: winit_core::window::Window::pre_present_notify
fn notify_presentation_paused(&self);
}

impl WindowExtWayland for dyn CoreWindow + '_ {
#[inline]
fn xdg_toplevel(&self) -> Option<NonNull<c_void>> {
self.cast_ref::<Window>()?.xdg_toplevel()
}

#[inline]
fn notify_presentation_paused(&self) {
if let Some(window) = self.cast_ref::<Window>() {
window.notify_presentation_paused();
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
54 changes: 38 additions & 16 deletions winit-wayland/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,23 +326,36 @@ impl WindowHandler for WinitState {
};

// Populate the configure to the window.
self.window_compositor_updates[index].resized |= self
.windows
.get_mut()
.get_mut(&window_id)
.expect("got configure for dead window.")
.lock()
.unwrap()
.configure_window(configure, &self.shm, &self.subcompositor_state);
let (resized, was_suspended, suspended, committed) = {
let mut window = self
.windows
.get_mut()
.get_mut(&window_id)
.expect("got configure for dead window.")
.lock()
.unwrap();
let was_suspended = window.is_suspended();
let resized = window.configure_window(configure, &self.shm, &self.subcompositor_state);
let committed = window.commit_paused_configure();
(resized, was_suspended, window.is_suspended(), committed)
};
self.window_compositor_updates[index].resized |= resized;
if suspended != was_suspended {
self.window_compositor_updates[index].occluded = Some(suspended);
}

// NOTE: configure demands wl_surface::commit, however winit doesn't commit on behalf of the
// users, since it can break a lot of things, thus it'll ask users to redraw instead.
self.window_requests
.get_mut()
.get(&window_id)
.unwrap()
.redraw_requested
.store(true, Ordering::Relaxed);
// users, since it can break a lot of things, thus it'll ask users to redraw instead. The
// exception is a suspended window whose application said it stopped presenting, see
// `WindowExtWayland::notify_presentation_paused`.
if !committed {
self.window_requests
.get_mut()
.get(&window_id)
.unwrap()
.redraw_requested
.store(true, Ordering::Relaxed);
}

// Manually mark that we've got an event, since configure may not generate a resize.
self.dispatched_events = true;
Expand Down Expand Up @@ -512,11 +525,20 @@ pub struct WindowCompositorUpdate {

/// Close the window.
pub close_window: bool,

/// The window became suspended (`Some(true)`) or stopped being suspended (`Some(false)`).
pub occluded: Option<bool>,
}

impl WindowCompositorUpdate {
fn new(window_id: WindowId) -> Self {
Self { window_id, resized: false, scale_changed: false, close_window: false }
Self {
window_id,
resized: false,
scale_changed: false,
close_window: false,
occluded: None,
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions winit-wayland/src/window/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ impl Window {
pub(crate) fn xdg_toplevel(&self) -> Option<NonNull<c_void>> {
NonNull::new(self.window.xdg_toplevel().id().as_ptr().cast())
}

pub(crate) fn notify_presentation_paused(&self) {
let Some(state) = self.common.state.upgrade() else { return };
state.lock().unwrap().pause_presentation();
}
}

impl Window {
Expand Down
48 changes: 47 additions & 1 deletion winit-wayland/src/window/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use sctk::reexports::client::backend::ObjectId;
use sctk::reexports::client::protocol::wl_seat::WlSeat;
use sctk::reexports::client::protocol::wl_shm::WlShm;
use sctk::reexports::client::{Proxy, QueueHandle};
use sctk::reexports::csd_frame::DecorationsFrame;
use sctk::reexports::csd_frame::{DecorationsFrame, WindowState as XdgWindowState};
use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1;
use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3;
use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport;
Expand Down Expand Up @@ -154,6 +154,10 @@ pub struct WindowState {
/// The state of the frame callback.
frame_callback_state: FrameCallbackState,

/// The scale factor at the time the application said it stopped presenting while the window
/// is suspended, or `None` if it didn't. Cleared when the window stops being suspended.
presentation_paused: Option<f64>,

viewport: Option<WpViewport>,
fractional_scale: Option<WpFractionalScaleV1>,
blur: Option<SurfaceBlurEffect>,
Expand Down Expand Up @@ -228,6 +232,7 @@ impl WindowState {
fractional_scale,
frame: None,
frame_callback_state: FrameCallbackState::None,
presentation_paused: None,
seat_focus: Default::default(),
has_pending_move: None,
text_input_state: None,
Expand Down Expand Up @@ -506,6 +511,47 @@ impl WindowState {
if maximized { xdg_toplevel.set_maximized() } else { xdg_toplevel.unset_maximized() }
}

/// Whether the compositor has suspended the window, as of the last configure.
pub(crate) fn is_suspended(&self) -> bool {
match &self.window {
WindowType::Window { last_configure, .. } => last_configure
.as_ref()
.is_some_and(|configure| configure.state.contains(XdgWindowState::SUSPENDED)),
_ => false,
}
}

/// The application stopped presenting after the window was suspended, so commit the
/// configures it acknowledges on its behalf until the window stops being suspended.
pub(crate) fn pause_presentation(&mut self) {
if !self.is_suspended() {
return;
}

self.presentation_paused = Some(self.scale_factor);
self.commit_paused_configure();
}

/// Called after a configure was acknowledged. Returns `true` if the configure was committed
/// on behalf of the application, which then doesn't need to redraw for it.
pub(crate) fn commit_paused_configure(&mut self) -> bool {
if !self.is_suspended() {
self.presentation_paused = None;
return false;
}

let Some(scale_factor) = self.presentation_paused else { return false };

// A new integer buffer scale would apply to the last buffer the application presented,
// whose size need not be a multiple of it, so leave that commit to the application.
if self.fractional_scale.is_none() && scale_factor as i32 != self.scale_factor as i32 {
return false;
}

self.window.wl_surface().commit();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

winit should never commit on behalf of the user when there's a possibility of user rendering, which there's, so such approach doesn't work.

What works, perhaps, is once you get Suspended, do RequestRedraw and after that request submit Occluded, so the event is kind of delayed with the hope that user redrawn because they got asked to do so, so delivering Occluded would be once we know that user kind-of committed the occluded state.

true
}

pub(crate) fn fullscreen(&self) -> Option<Fullscreen> {
let is_fullscreen = match &self.window {
WindowType::Window { last_configure, .. } => last_configure
Expand Down
5 changes: 5 additions & 0 deletions winit/src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ changelog entry.

## Unreleased

### Added

- On Wayland, emit `WindowEvent::Occluded` when the compositor suspends or resumes the window (xdg-shell v6 `suspended` state).
- On Wayland, add `WindowExtWayland::notify_presentation_paused`, which lets winit commit a suspended window's configures while the application isn't presenting.

### Fixed

- On Windows, fix a crash occurring when trying to create a DXGI surface on a window that is both fullscreen and always on top.
Expand Down
Loading