Skip to content

Commit

Permalink
Add and event and flag to report when a window has been occluded
Browse files Browse the repository at this point in the history
Adds the SDL_EVENT_WINDOW_OCCLUDED events and the window flag SDL_WINDOW_OCCLUDED to report when the window occlusion state has changed, so that the application can take appropriate measures, as it may wish to suspend drawing, throttle, or otherwise behave in a more energy efficient manner when the window is not visible. When the window is no longer occluded, the SDL_EVENT_WINDOW_EXPOSED event is sent and the occlusion flag is cleared.

This is handled on macOS via the window occlusion state event (available as of 10.9), and via the xdg-shell protocol on Wayland (version 6, wayland-protocols 1.32, passed through in libdecor 0.1.2).
  • Loading branch information
Kontrabant committed Jul 18, 2023
1 parent 7aec9ad commit 44536b7
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 89 deletions.
14 changes: 4 additions & 10 deletions cmake/sdlchecks.cmake
Expand Up @@ -571,16 +571,10 @@ macro(CheckWayland)
list(APPEND SDL_EXTRA_LIBS ${PKG_LIBDECOR_LIBRARIES})
endif()

cmake_push_check_state()
list(APPEND CMAKE_REQUIRED_FLAGS ${PKG_LIBDECOR_CFLAGS})
list(APPEND CMAKE_REQUIRED_INCLUDES ${PKG_LIBDECOR_INCLUDE_DIRS})
list(APPEND CMAKE_REQUIRED_LIBRARIES ${PKG_LIBDECOR_LINK_LIBRARIES})
check_symbol_exists(libdecor_frame_get_max_content_size "libdecor.h" HAVE_LIBDECOR_FRAME_GET_MAX_CONTENT_SIZE)
check_symbol_exists(libdecor_frame_get_min_content_size "libdecor.h" HAVE_LIBDECOR_FRAME_GET_MIN_CONTENT_SIZE)
if(HAVE_LIBDECOR_FRAME_GET_MAX_CONTENT_SIZE AND HAVE_LIBDECOR_FRAME_GET_MIN_CONTENT_SIZE)
set(SDL_HAVE_LIBDECOR_GET_MIN_MAX 1)
endif()
cmake_pop_check_state()
# Version 0.1.2 or higher is needed for suspended window state and statically linked min/max getters.
if (PKG_LIBDECOR_VERSION VERSION_GREATER_EQUAL "0.1.2")
set(SDL_HAVE_LIBDECOR_VER_0_1_2 1)
endif ()
endif()
endif()

Expand Down
1 change: 1 addition & 0 deletions include/SDL3/SDL_events.h
Expand Up @@ -121,6 +121,7 @@ typedef enum
SDL_EVENT_WINDOW_ICCPROF_CHANGED, /**< The ICC profile of the window's display has changed */
SDL_EVENT_WINDOW_DISPLAY_CHANGED, /**< Window has been moved to display data1 */
SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED, /**< Window display scale has been changed */
SDL_EVENT_WINDOW_OCCLUDED, /**< The window has been occluded */
SDL_EVENT_WINDOW_DESTROYED, /**< The window with the associated ID is being or has been destroyed. If this message is being handled
in an event watcher, the window handle is still valid and can still be used to retrieve any userdata
associated with the window. Otherwise, the handle has already been destroyed and all resources
Expand Down
1 change: 1 addition & 0 deletions include/SDL3/SDL_video.h
Expand Up @@ -130,6 +130,7 @@ typedef enum
{
SDL_WINDOW_FULLSCREEN = 0x00000001, /**< window is in fullscreen mode */
SDL_WINDOW_OPENGL = 0x00000002, /**< window usable with OpenGL context */
SDL_WINDOW_OCCLUDED = 0x00000004, /**< window is occluded */
SDL_WINDOW_HIDDEN = 0x00000008, /**< window is not visible */
SDL_WINDOW_BORDERLESS = 0x00000010, /**< no window decoration */
SDL_WINDOW_RESIZABLE = 0x00000020, /**< window can be resized */
Expand Down
2 changes: 1 addition & 1 deletion include/build_config/SDL_build_config.h.cmake
Expand Up @@ -557,7 +557,7 @@
#cmakedefine SDL_VIDEO_VITA_PVR @SDL_VIDEO_VITA_PVR@
#cmakedefine SDL_VIDEO_VITA_PVR_OGL @SDL_VIDEO_VITA_PVR_OGL@

#cmakedefine SDL_HAVE_LIBDECOR_GET_MIN_MAX @SDL_HAVE_LIBDECOR_GET_MIN_MAX@
#cmakedefine SDL_HAVE_LIBDECOR_VER_0_1_2 @SDL_HAVE_LIBDECOR_VER_0_1_2@

#if !defined(HAVE_STDINT_H) && !defined(_STDINT_H_)
/* Most everything except Visual Studio 2008 and earlier has stdint.h now */
Expand Down
1 change: 1 addition & 0 deletions src/events/SDL_events.c
Expand Up @@ -253,6 +253,7 @@ static void SDL_LogEvent(const SDL_Event *event)
SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_ICCPROF_CHANGED);
SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_DISPLAY_CHANGED);
SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED);
SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_OCCLUDED);
SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_DESTROYED);
#undef SDL_WINDOWEVENT_CASE

Expand Down
12 changes: 11 additions & 1 deletion src/events/SDL_windowevents.c
Expand Up @@ -62,6 +62,9 @@ int SDL_SendWindowEvent(SDL_Window *window, SDL_EventType windowevent,
}
window->flags |= SDL_WINDOW_HIDDEN;
break;
case SDL_EVENT_WINDOW_EXPOSED:
window->flags &= ~SDL_WINDOW_OCCLUDED;
break;
case SDL_EVENT_WINDOW_MOVED:
window->undefined_x = SDL_FALSE;
window->undefined_y = SDL_FALSE;
Expand Down Expand Up @@ -144,6 +147,12 @@ int SDL_SendWindowEvent(SDL_Window *window, SDL_EventType windowevent,
}
window->last_displayID = (SDL_DisplayID)data1;
break;
case SDL_EVENT_WINDOW_OCCLUDED:
if (window->flags & SDL_WINDOW_OCCLUDED) {
return 0;
}
window->flags |= SDL_WINDOW_OCCLUDED;
break;
default:
break;
}
Expand All @@ -162,7 +171,8 @@ int SDL_SendWindowEvent(SDL_Window *window, SDL_EventType windowevent,
if (windowevent == SDL_EVENT_WINDOW_MOVED ||
windowevent == SDL_EVENT_WINDOW_RESIZED ||
windowevent == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED ||
windowevent == SDL_EVENT_WINDOW_EXPOSED) {
windowevent == SDL_EVENT_WINDOW_EXPOSED ||
windowevent == SDL_EVENT_WINDOW_OCCLUDED) {
SDL_FilterEvents(RemoveSupercededWindowEvents, &event);
}
posted = (SDL_PushEvent(&event) > 0);
Expand Down
25 changes: 22 additions & 3 deletions src/video/cocoa/SDL_cocoawindow.m
Expand Up @@ -562,6 +562,14 @@ static void Cocoa_SetKeyboardFocus(SDL_Window *window)
SDL_SetKeyboardFocus(window);
}

static void Cocoa_SendExposedEventIfVisible(SDL_Window *window)
{
NSWindow *nswindow = ((__bridge SDL_CocoaWindowData*)window->driverdata).nswindow;
if ([nswindow occlusionState] & NSWindowOcclusionStateVisible) {
SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
}
}

@implementation Cocoa_WindowListener

- (void)listen:(SDL_CocoaWindowData *)data
Expand Down Expand Up @@ -600,6 +608,7 @@ - (void)listen:(SDL_CocoaWindowData *)data
[center addObserver:self selector:@selector(windowDidExitFullScreen:) name:NSWindowDidExitFullScreenNotification object:window];
[center addObserver:self selector:@selector(windowDidFailToEnterFullScreen:) name:@"NSWindowDidFailToEnterFullScreenNotification" object:window];
[center addObserver:self selector:@selector(windowDidFailToExitFullScreen:) name:@"NSWindowDidFailToExitFullScreenNotification" object:window];
[center addObserver:self selector:@selector(windowDidChangeOcclusionState:) name:NSWindowDidChangeOcclusionStateNotification object:window];
} else {
[window setDelegate:self];
}
Expand Down Expand Up @@ -733,6 +742,7 @@ - (void)close
[center removeObserver:self name:NSWindowDidExitFullScreenNotification object:window];
[center removeObserver:self name:@"NSWindowDidFailToEnterFullScreenNotification" object:window];
[center removeObserver:self name:@"NSWindowDidFailToExitFullScreenNotification" object:window];
[center removeObserver:self name:NSWindowDidChangeOcclusionStateNotification object:window];
} else {
[window setDelegate:nil];
}
Expand Down Expand Up @@ -825,7 +835,16 @@ - (BOOL)windowShouldClose:(id)sender

- (void)windowDidExpose:(NSNotification *)aNotification
{
SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
Cocoa_SendExposedEventIfVisible(_data.window);
}

- (void)windowDidChangeOcclusionState:(NSNotification *)aNotification
{
if ([_data.nswindow occlusionState] & NSWindowOcclusionStateVisible) {
SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
} else {
SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_OCCLUDED, 0, 0);
}
}

- (void)windowWillMove:(NSNotification *)aNotification
Expand Down Expand Up @@ -1669,7 +1688,7 @@ - (void)drawRect:(NSRect)dirtyRect
self.layer.backgroundColor = CGColorGetConstantColor(color);
}

SDL_SendWindowEvent(_sdlWindow, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
Cocoa_SendExposedEventIfVisible(_sdlWindow);
}

- (BOOL)wantsUpdateLayer
Expand All @@ -1687,7 +1706,7 @@ - (void)updateLayer
CFStringRef color = transparent ? kCGColorClear : kCGColorBlack;
self.layer.backgroundColor = CGColorGetConstantColor(color);
ScheduleContextUpdates((__bridge SDL_CocoaWindowData *)_sdlWindow->driverdata);
SDL_SendWindowEvent(_sdlWindow, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
Cocoa_SendExposedEventIfVisible(_sdlWindow);
}

- (void)rightMouseDown:(NSEvent *)theEvent
Expand Down
2 changes: 1 addition & 1 deletion src/video/wayland/SDL_waylandsym.h
Expand Up @@ -218,7 +218,7 @@ SDL_WAYLAND_SYM(bool, libdecor_configuration_get_window_state, (struct libdecor_
enum libdecor_window_state *))
SDL_WAYLAND_SYM(int, libdecor_dispatch, (struct libdecor *, int))

#if defined(SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR) || defined(SDL_HAVE_LIBDECOR_GET_MIN_MAX)
#if defined(SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR) || defined(SDL_HAVE_LIBDECOR_VER_0_1_2)
/* Only found in libdecor 0.1.1 or higher, so failure to load them is not fatal. */
SDL_WAYLAND_SYM_OPT(void, libdecor_frame_get_min_content_size, (struct libdecor_frame *,\
int *,\
Expand Down
2 changes: 1 addition & 1 deletion src/video/wayland/SDL_waylandvideo.c
Expand Up @@ -752,7 +752,7 @@ static void display_handle_global(void *data, struct wl_registry *registry, uint
} else if (SDL_strcmp(interface, "wl_seat") == 0) {
Wayland_display_add_input(d, id, version);
} else if (SDL_strcmp(interface, "xdg_wm_base") == 0) {
d->shell.xdg = wl_registry_bind(d->registry, id, &xdg_wm_base_interface, SDL_min(version, 3));
d->shell.xdg = wl_registry_bind(d->registry, id, &xdg_wm_base_interface, SDL_min(version, 6));
xdg_wm_base_add_listener(d->shell.xdg, &shell_listener_xdg, NULL);
} else if (SDL_strcmp(interface, "wl_shm") == 0) {
d->shm = wl_registry_bind(registry, id, &wl_shm_interface, 1);
Expand Down
85 changes: 71 additions & 14 deletions src/video/wayland/SDL_waylandwindow.c
Expand Up @@ -376,6 +376,23 @@ static void ConfigureWindowGeometry(SDL_Window *window)
/* Unconditionally send the window and drawable size, the video core will deduplicate when required. */
SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, window_width, window_height);
SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED, data->drawable_width, data->drawable_height);

/* Send an exposure event if the window is in the shown state and the size has changed,
* even if the window is occluded, as the client needs to commit a new frame for the
* changes to take effect.
*
* The occlusion state is immediately set again afterward, if necessary.
*/
if (data->surface_status == WAYLAND_SURFACE_STATUS_SHOWN) {
if ((drawable_size_changed || window_size_changed) ||
(!data->suspended && (window->flags & SDL_WINDOW_OCCLUDED))) {
SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
}

if (data->suspended) {
SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_OCCLUDED, 0, 0);
}
}
}

static void EnsurePopupPositionIsValid(SDL_Window *window)
Expand Down Expand Up @@ -535,14 +552,13 @@ static const struct wl_callback_listener surface_frame_listener;

static void surface_frame_done(void *data, struct wl_callback *cb, uint32_t time)
{
SDL_Window *w;
SDL_WindowData *wind = (SDL_WindowData *)data;

/*
* wl_surface.damage_buffer is the preferred method of setting the damage region
* on compositor version 4 and above.
*/
if (wl_compositor_get_version(wind->waylandData->compositor) >= 4) {
if (wl_compositor_get_version(wind->waylandData->compositor) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) {
wl_surface_damage_buffer(wind->surface, 0, 0,
wind->drawable_width, wind->drawable_height);
} else {
Expand All @@ -554,11 +570,18 @@ static void surface_frame_done(void *data, struct wl_callback *cb, uint32_t time
wind->surface_status = WAYLAND_SURFACE_STATUS_SHOWN;

/* If any child windows are waiting on this window to be shown, show them now */
for (w = wind->sdlwindow->first_child; w != NULL; w = w->next_sibling) {
for (SDL_Window *w = wind->sdlwindow->first_child; w != NULL; w = w->next_sibling) {
if (w->driverdata->surface_status == WAYLAND_SURFACE_STATUS_SHOW_PENDING) {
Wayland_ShowWindow(SDL_GetVideoDevice(), w);
}
}

/* If the window was initially set to the suspended state, send the occluded event now,
* as we don't want to mark the window as occluded until at least one frame has been submitted.
*/
if (wind->suspended) {
SDL_SendWindowEvent(wind->sdlwindow, SDL_EVENT_WINDOW_OCCLUDED, 0, 0);
}
}

wl_callback_destroy(cb);
Expand Down Expand Up @@ -616,6 +639,7 @@ static void handle_configure_xdg_toplevel(void *data,
SDL_bool maximized = SDL_FALSE;
SDL_bool floating = SDL_TRUE;
SDL_bool focused = SDL_FALSE;
SDL_bool suspended = SDL_FALSE;
wl_array_for_each (state, states) {
switch (*state) {
case XDG_TOPLEVEL_STATE_FULLSCREEN:
Expand All @@ -635,6 +659,8 @@ static void handle_configure_xdg_toplevel(void *data,
case XDG_TOPLEVEL_STATE_TILED_BOTTOM:
floating = SDL_FALSE;
break;
case XDG_TOPLEVEL_STATE_SUSPENDED:
suspended = SDL_TRUE;
default:
break;
}
Expand Down Expand Up @@ -721,6 +747,7 @@ static void handle_configure_xdg_toplevel(void *data,
wind->requested_window_width = width;
wind->requested_window_height = height;
wind->floating = floating;
wind->suspended = suspended;
if (wind->surface_status == WAYLAND_SURFACE_STATUS_WAITING_FOR_CONFIGURE) {
wind->surface_status = WAYLAND_SURFACE_STATUS_WAITING_FOR_FRAME;
}
Expand All @@ -732,9 +759,25 @@ static void handle_close_xdg_toplevel(void *data, struct xdg_toplevel *xdg_tople
SDL_SendWindowEvent(window->sdlwindow, SDL_EVENT_WINDOW_CLOSE_REQUESTED, 0, 0);
}

static void handle_xdg_configure_toplevel_bounds(void *data,
struct xdg_toplevel *xdg_toplevel,
int32_t width, int32_t height)
{
/* NOP */
}

void handle_xdg_toplevel_wm_capabilities(void *data,
struct xdg_toplevel *xdg_toplevel,
struct wl_array *capabilities)
{
/* NOP */
}

static const struct xdg_toplevel_listener toplevel_listener_xdg = {
handle_configure_xdg_toplevel,
handle_close_xdg_toplevel
handle_close_xdg_toplevel,
handle_xdg_configure_toplevel_bounds, /* Version 4 */
handle_xdg_toplevel_wm_capabilities /* Version 5 */
};

static void handle_configure_xdg_popup(void *data,
Expand Down Expand Up @@ -824,19 +867,19 @@ static const struct zxdg_toplevel_decoration_v1_listener decoration_listener = {
* minimum content size limit. The internal limits must always be overridden
* to ensure that very small windows don't cause errors or crashes.
*
* On versions of libdecor that expose the function to get the minimum content
* On libdecor >= 0.1.2, which exposes the function to get the minimum content
* size limit, this function is a no-op.
*
* Can be removed if the minimum required version of libdecor is raised
* to a version that guarantees the availability of this function.
* Can be removed if the minimum required version of libdecor is raised to
* 0.1.2 or higher.
*/
static void OverrideLibdecorLimits(SDL_Window *window)
{
#ifdef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR
if (libdecor_frame_get_min_content_size == NULL) {
libdecor_frame_set_min_content_size(window->driverdata->shell_surface.libdecor.frame, window->min_w, window->min_h);
}
#elif !defined(SDL_HAVE_LIBDECOR_GET_MIN_MAX)
#elif !defined(SDL_HAVE_LIBDECOR_VER_0_1_2)
libdecor_frame_set_min_content_size(window->driverdata->shell_surface.libdecor.frame, window->min_w, window->min_h);
#endif
}
Expand All @@ -847,15 +890,15 @@ static void OverrideLibdecorLimits(SDL_Window *window)
* function is a no-op.
*
* Can be replaced with a direct call if the minimum required version of libdecor is raised
* to a version that guarantees the availability of this function.
* to 0.1.2 or higher.
*/
static void LibdecorGetMinContentSize(struct libdecor_frame *frame, int *min_w, int *min_h)
{
#ifdef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR
if (libdecor_frame_get_min_content_size != NULL) {
libdecor_frame_get_min_content_size(frame, min_w, min_h);
}
#elif defined(SDL_HAVE_LIBDECOR_GET_MIN_MAX)
#elif defined(SDL_HAVE_LIBDECOR_VER_0_1_2)
libdecor_frame_get_min_content_size(frame, min_w, min_h);
#endif
}
Expand All @@ -876,6 +919,7 @@ static void decoration_frame_configure(struct libdecor_frame *frame,
SDL_bool fullscreen = SDL_FALSE;
SDL_bool maximized = SDL_FALSE;
SDL_bool tiled = SDL_FALSE;
SDL_bool suspended = SDL_FALSE;
SDL_bool floating;

static const enum libdecor_window_state tiled_states = (LIBDECOR_WINDOW_STATE_TILED_LEFT | LIBDECOR_WINDOW_STATE_TILED_RIGHT |
Expand All @@ -887,6 +931,9 @@ static void decoration_frame_configure(struct libdecor_frame *frame,
maximized = (window_state & LIBDECOR_WINDOW_STATE_MAXIMIZED) != 0;
focused = (window_state & LIBDECOR_WINDOW_STATE_ACTIVE) != 0;
tiled = (window_state & tiled_states) != 0;
#ifdef SDL_HAVE_LIBDECOR_VER_0_1_2
suspended = (window_state & LIBDECOR_WINDOW_STATE_SUSPENDED) != 0;
#endif
}
floating = !(fullscreen || maximized || tiled);

Expand Down Expand Up @@ -999,8 +1046,9 @@ static void decoration_frame_configure(struct libdecor_frame *frame,
wind->floating_height = height;
}

/* Store the new floating state. */
/* Store the new state. */
wind->floating = floating;
wind->suspended = suspended;

/* Calculate the new window geometry */
wind->requested_window_width = width;
Expand Down Expand Up @@ -1039,9 +1087,13 @@ static void decoration_frame_close(struct libdecor_frame *frame, void *user_data

static void decoration_frame_commit(struct libdecor_frame *frame, void *user_data)
{
SDL_WindowData *wind = user_data;

SDL_SendWindowEvent(wind->sdlwindow, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
/* libdecor decoration subsurfaces are synchronous, so the client needs to
* commit a frame to trigger an update of the decoration surfaces.
*/
SDL_WindowData *wind = (SDL_WindowData *)user_data;
if (!wind->suspended && wind->surface_status == WAYLAND_SURFACE_STATUS_SHOWN) {
SDL_SendWindowEvent(wind->sdlwindow, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
}
}

static struct libdecor_frame_interface libdecor_frame_interface = {
Expand Down Expand Up @@ -1565,6 +1617,11 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
* HideWindow was called immediately before ShowWindow.
*/
WAYLAND_wl_display_roundtrip(c->display);

/* Send an exposure event to signal that the client should draw. */
if (data->surface_status == WAYLAND_SURFACE_STATUS_WAITING_FOR_FRAME) {
SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
}
}

static void Wayland_ReleasePopup(SDL_VideoDevice *_this, SDL_Window *popup)
Expand Down
1 change: 1 addition & 0 deletions src/video/wayland/SDL_waylandwindow.h
Expand Up @@ -122,6 +122,7 @@ struct SDL_WindowData
int system_min_required_height;
SDL_DisplayID last_displayID;
SDL_bool floating;
SDL_bool suspended;
SDL_bool is_fullscreen;
SDL_bool in_fullscreen_transition;
SDL_bool fullscreen_was_positioned;
Expand Down

0 comments on commit 44536b7

Please sign in to comment.