diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 972e0b7e782a1..957155b144050 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -3805,7 +3805,7 @@ extern "C" { #define SDL_HINT_VIDEO_WAYLAND_MODE_EMULATION "SDL_VIDEO_WAYLAND_MODE_EMULATION" /** - * A variable controlling how modes with a non-native aspect ratio are + * A variable controlling how video modes with a non-native aspect ratio are * displayed under Wayland. * * When this hint is set, the requested scaling will be used when displaying @@ -3815,9 +3815,8 @@ extern "C" { * The variable can be set to the following values: * * - "aspect" - Video modes will be displayed scaled, in their proper aspect - * ratio, with black bars. + * ratio, with black bars. (default) * - "stretch" - Video modes will be scaled to fill the entire display. - * (default) * - "none" - Video modes will be displayed as 1:1 with no scaling. * * This hint should be set before creating a window. diff --git a/src/video/wayland/SDL_waylandshmbuffer.c b/src/video/wayland/SDL_waylandshmbuffer.c index 7b274c700abbf..fe16287d0d48b 100644 --- a/src/video/wayland/SDL_waylandshmbuffer.c +++ b/src/video/wayland/SDL_waylandshmbuffer.c @@ -32,6 +32,7 @@ #include "SDL_waylandshmbuffer.h" #include "SDL_waylandvideo.h" +#include "single-pixel-buffer-v1-client-protocol.h" static bool SetTempFileSize(int fd, off_t size) { @@ -186,4 +187,28 @@ void Wayland_ReleaseSHMPool(Wayland_SHMPool *shmPool) } } +struct wl_buffer *Wayland_CreateSinglePixelBuffer(Uint32 r, Uint32 g, Uint32 b, Uint32 a) +{ + SDL_VideoData *viddata = SDL_GetVideoDevice()->internal; + + // THe single-pixel buffer protocol is preferred, as the compositor can choose an optimal format. + if (viddata->single_pixel_buffer_manager) { + return wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer(viddata->single_pixel_buffer_manager, r, g, b, a); + } else { + Wayland_SHMPool *pool = Wayland_AllocSHMPool(4); + if (!pool) { + return NULL; + } + + void *mem; + struct wl_buffer *wl_buffer = Wayland_AllocBufferFromPool(pool, 1, 1, &mem); + + const Uint8 pixel[4] = { r >> 24, g >> 24, b >> 24, a >> 24 }; + SDL_memcpy(mem, pixel, sizeof(pixel)); + + Wayland_ReleaseSHMPool(pool); + return wl_buffer; + } +} + #endif diff --git a/src/video/wayland/SDL_waylandshmbuffer.h b/src/video/wayland/SDL_waylandshmbuffer.h index e5a54a25d065c..d368af5dd93ab 100644 --- a/src/video/wayland/SDL_waylandshmbuffer.h +++ b/src/video/wayland/SDL_waylandshmbuffer.h @@ -30,4 +30,6 @@ extern Wayland_SHMPool *Wayland_AllocSHMPool(int size); extern struct wl_buffer *Wayland_AllocBufferFromPool(Wayland_SHMPool *shmPool, int width, int height, void **data); extern void Wayland_ReleaseSHMPool(Wayland_SHMPool *shmPool); +extern struct wl_buffer *Wayland_CreateSinglePixelBuffer(Uint32 r, Uint32 g, Uint32 b, Uint32 a); + #endif diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index 848e43740a835..f269d20b0a627 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -68,6 +68,7 @@ #include "color-management-v1-client-protocol.h" #include "pointer-warp-v1-client-protocol.h" #include "pointer-gestures-unstable-v1-client-protocol.h" +#include "single-pixel-buffer-v1-client-protocol.h" #ifdef HAVE_LIBDECOR_H #include @@ -651,6 +652,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols) device->SetWindowResizable = Wayland_SetWindowResizable; device->SetWindowPosition = Wayland_SetWindowPosition; device->SetWindowSize = Wayland_SetWindowSize; + device->SetWindowAspectRatio = Wayland_SetWindowAspectRatio; device->SetWindowMinimumSize = Wayland_SetWindowMinimumSize; device->SetWindowMaximumSize = Wayland_SetWindowMaximumSize; device->SetWindowParent = Wayland_SetWindowParent; @@ -1272,6 +1274,8 @@ static void handle_registry_global(void *data, struct wl_registry *registry, uin if (SDL_strcmp(interface, "wl_compositor") == 0) { d->compositor = wl_registry_bind(d->registry, id, &wl_compositor_interface, SDL_min(SDL_WL_COMPOSITOR_VERSION, version)); + } else if (SDL_strcmp(interface, "wl_subcompositor") == 0) { + d->subcompositor = wl_registry_bind(d->registry, id, &wl_subcompositor_interface, 1); } else if (SDL_strcmp(interface, "wl_output") == 0) { Wayland_add_display(d, id, SDL_min(version, SDL_WL_OUTPUT_VERSION)); } else if (SDL_strcmp(interface, "wl_seat") == 0) { @@ -1337,6 +1341,8 @@ static void handle_registry_global(void *data, struct wl_registry *registry, uin } else if (SDL_strcmp(interface, "zwp_pointer_gestures_v1") == 0) { d->zwp_pointer_gestures = wl_registry_bind(d->registry, id, &zwp_pointer_gestures_v1_interface, SDL_min(version, 3)); Wayland_DisplayInitPointerGestureManager(d); + } else if (SDL_strcmp(interface, "wp_single_pixel_buffer_manager_v1") == 0) { + d->single_pixel_buffer_manager = wl_registry_bind(d->registry, id, &wp_single_pixel_buffer_manager_v1_interface, 1); } #ifdef SDL_WL_FIXES_VERSION else if (SDL_strcmp(interface, "wl_fixes") == 0) { @@ -1665,6 +1671,16 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this) data->zwp_pointer_gestures = NULL; } + if (data->single_pixel_buffer_manager) { + wp_single_pixel_buffer_manager_v1_destroy(data->single_pixel_buffer_manager); + data->single_pixel_buffer_manager = NULL; + } + + if (data->subcompositor) { + wl_subcompositor_destroy(data->subcompositor); + data->subcompositor = NULL; + } + if (data->compositor) { wl_compositor_destroy(data->compositor); data->compositor = NULL; diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h index bab5d51d9235c..5f87970e10f11 100644 --- a/src/video/wayland/SDL_waylandvideo.h +++ b/src/video/wayland/SDL_waylandvideo.h @@ -61,6 +61,7 @@ struct SDL_VideoData struct libdecor *libdecor; #endif } shell; + struct wl_subcompositor *subcompositor; struct zwp_relative_pointer_manager_v1 *relative_pointer_manager; struct zwp_pointer_constraints_v1 *pointer_constraints; struct wp_pointer_warp_v1 *wp_pointer_warp_v1; @@ -85,6 +86,7 @@ struct SDL_VideoData struct zwp_tablet_manager_v2 *tablet_manager; struct wl_fixes *wl_fixes; struct zwp_pointer_gestures_v1 *zwp_pointer_gestures; + struct wp_single_pixel_buffer_manager_v1 *single_pixel_buffer_manager; struct xkb_context *xkb_context; diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index 5fddec7221fda..5e7e020f2edb6 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -72,20 +72,6 @@ static int PixelToPoint(SDL_Window *window, int pixel) return pixel ? SDL_max((int)SDL_lround((double)pixel / GetWindowScale(window)), 1) : 0; } -/* According to the Wayland spec: - * - * "If the [fullscreen] surface doesn't cover the whole output, the compositor will - * position the surface in the center of the output and compensate with border fill - * covering the rest of the output. The content of the border fill is undefined, but - * should be assumed to be in some way that attempts to blend into the surrounding area - * (e.g. solid black)." - * - * - KDE, as of 5.27, still doesn't do this - * - GNOME prior to 43 didn't do this (older versions are still found in many LTS distros) - * - * Default to 'stretch' for now, until things have moved forward enough that the default - * can be changed to 'aspect'. - */ enum WaylandModeScale { WAYLAND_MODE_SCALE_UNDEFINED, @@ -102,54 +88,30 @@ static enum WaylandModeScale GetModeScaleMethod(void) const char *scale_hint = SDL_GetHint(SDL_HINT_VIDEO_WAYLAND_MODE_SCALING); if (scale_hint) { - if (!SDL_strcasecmp(scale_hint, "aspect")) { - scale_mode = WAYLAND_MODE_SCALE_ASPECT; + if (!SDL_strcasecmp(scale_hint, "stretch")) { + scale_mode = WAYLAND_MODE_SCALE_STRETCH; } else if (!SDL_strcasecmp(scale_hint, "none")) { scale_mode = WAYLAND_MODE_SCALE_NONE; } else { - scale_mode = WAYLAND_MODE_SCALE_STRETCH; + scale_mode = WAYLAND_MODE_SCALE_ASPECT; } } else { - scale_mode = WAYLAND_MODE_SCALE_STRETCH; + scale_mode = WAYLAND_MODE_SCALE_ASPECT; } } return scale_mode; } -static void GetBufferSize(SDL_Window *window, int *width, int *height) -{ - SDL_WindowData *data = window->internal; - int buf_width; - int buf_height; - - // Exclusive fullscreen modes always have a pixel density of 1 - if (data->is_fullscreen && window->fullscreen_exclusive) { - buf_width = window->current_fullscreen_mode.w; - buf_height = window->current_fullscreen_mode.h; - } else if (!data->scale_to_display) { - // Round fractional backbuffer sizes halfway away from zero. - buf_width = PointToPixel(window, data->requested.logical_width); - buf_height = PointToPixel(window, data->requested.logical_height); - } else { - buf_width = data->requested.pixel_width; - buf_height = data->requested.pixel_height; - } - - if (width) { - *width = buf_width; - } - if (height) { - *height = buf_height; - } -} - static void SetMinMaxDimensions(SDL_Window *window) { SDL_WindowData *wind = window->internal; int min_width, min_height, max_width, max_height; - if ((window->flags & SDL_WINDOW_FULLSCREEN) || wind->fullscreen_deadline_count) { + /* Keep the limits off while the window is in a fixed-size state, or the controls + * to exit that state may be disabled. + */ + if (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) { min_width = 0; min_height = 0; max_width = 0; @@ -184,6 +146,13 @@ static void SetMinMaxDimensions(SDL_Window *window) if (!wind->shell_surface.libdecor.frame) { return; // Can't do anything yet, wait for ShowWindow } + + if (min_width && min_height && min_width == max_width && min_height == max_height) { + libdecor_frame_unset_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE); + } else { + libdecor_frame_set_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE); + } + /* No need to change these values if the window is non-resizable, * as libdecor will just overwrite them internally. */ @@ -286,18 +255,17 @@ static void RepositionPopup(SDL_Window *window, bool use_current_position) } } -static void SetSurfaceOpaqueRegion(SDL_WindowData *wind, bool is_opaque) +static void SetSurfaceOpaqueRegion(struct wl_surface *surface, int width, int height) { - SDL_VideoData *viddata = wind->waylandData; + SDL_VideoData *viddata = SDL_GetVideoDevice()->internal; - if (is_opaque) { + if (width && height) { struct wl_region *region = wl_compositor_create_region(viddata->compositor); - wl_region_add(region, 0, 0, - wind->current.logical_width, wind->current.logical_height); - wl_surface_set_opaque_region(wind->surface, region); + wl_region_add(region, 0, 0, width, height); + wl_surface_set_opaque_region(surface, region); wl_region_destroy(region); } else { - wl_surface_set_opaque_region(wind->surface, NULL); + wl_surface_set_opaque_region(surface, NULL); } } @@ -307,29 +275,19 @@ static void ConfigureWindowGeometry(SDL_Window *window) const double scale_factor = GetWindowScale(window); const int old_pixel_width = data->current.pixel_width; const int old_pixel_height = data->current.pixel_height; - int window_width, window_height; + int window_width = 0; + int window_height = 0; + int viewport_width, viewport_height; bool window_size_changed; - - // Set the drawable backbuffer size. - GetBufferSize(window, &data->current.pixel_width, &data->current.pixel_height); - const bool buffer_size_changed = data->current.pixel_width != old_pixel_width || - data->current.pixel_height != old_pixel_height; - - if (data->egl_window && buffer_size_changed) { - WAYLAND_wl_egl_window_resize(data->egl_window, - data->current.pixel_width, - data->current.pixel_height, - 0, 0); - } + bool buffer_size_changed; + const bool is_opaque = !(window->flags & SDL_WINDOW_TRANSPARENT) && window->opacity == 1.0f; if (data->is_fullscreen && window->fullscreen_exclusive) { - int output_width; - int output_height; window_width = window->current_fullscreen_mode.w; window_height = window->current_fullscreen_mode.h; - output_width = data->requested.logical_width; - output_height = data->requested.logical_height; + viewport_width = data->requested.logical_width; + viewport_height = data->requested.logical_height; switch (GetModeScaleMethod()) { case WAYLAND_MODE_SCALE_NONE: @@ -337,40 +295,50 @@ static void ConfigureWindowGeometry(SDL_Window *window) * Windows can request a smaller size, but exceeding these dimensions is a protocol violation, * thus, modes that exceed the output size still need to be scaled with a viewport. */ - if (window_width <= output_width && window_height <= output_height) { - output_width = window_width; - output_height = window_height; + if (window_width <= viewport_width && window_height <= viewport_height) { + viewport_width = window_width; + viewport_height = window_height; break; } SDL_FALLTHROUGH; case WAYLAND_MODE_SCALE_ASPECT: { - const float output_ratio = (float)output_width / (float)output_height; + const float output_ratio = (float)viewport_width / (float)viewport_height; const float mode_ratio = (float)window_width / (float)window_height; if (output_ratio > mode_ratio) { - output_width = SDL_lroundf((float)window_width * ((float)output_height / (float)window_height)); + viewport_width = SDL_lroundf((float)window_width * ((float)viewport_height / (float)window_height)); } else if (output_ratio < mode_ratio) { - output_height = SDL_lroundf((float)window_height * ((float)output_width / (float)window_width)); + viewport_height = SDL_lroundf((float)window_height * ((float)viewport_width / (float)window_width)); } } break; default: break; } - window_size_changed = window_width != window->w || window_height != window->h || - data->current.logical_width != output_width || data->current.logical_height != output_height; + window_size_changed = window_width != window->w || + window_height != window->h || + data->current.viewport_width != viewport_width || + data->current.viewport_height != viewport_height; + + // Exclusive fullscreen window sizes are always in pixel units. + data->current.pixel_width = window_width; + data->current.pixel_height = window_height; + buffer_size_changed = data->current.pixel_width != old_pixel_width || + data->current.pixel_height != old_pixel_height; if (window_size_changed || buffer_size_changed) { if (data->viewport) { - wp_viewport_set_destination(data->viewport, output_width, output_height); + wp_viewport_set_destination(data->viewport, viewport_width, viewport_height); - data->current.logical_width = output_width; - data->current.logical_height = output_height; + data->current.logical_width = data->requested.logical_width; + data->current.logical_height = data->requested.logical_height; + data->current.viewport_width = viewport_width; + data->current.viewport_height = viewport_height; } else { // Calculate the integer scale from the mode and output. - const int32_t int_scale = SDL_max(window->current_fullscreen_mode.w / output_width, 1); + const int32_t int_scale = SDL_max(window->current_fullscreen_mode.w / viewport_width, 1); wl_surface_set_buffer_scale(data->surface, int_scale); data->current.logical_width = window->current_fullscreen_mode.w; @@ -381,31 +349,167 @@ static void ConfigureWindowGeometry(SDL_Window *window) data->pointer_scale.y = (double)window_height / (double)data->current.logical_height; } } else { - window_width = data->requested.logical_width; - window_height = data->requested.logical_height; + if (!data->scale_to_display) { + viewport_width = data->requested.logical_width; + viewport_height = data->requested.logical_height; + } else { + viewport_width = data->requested.pixel_width; + viewport_height = data->requested.pixel_height; + } + + if (data->viewport && data->waylandData->subcompositor && !data->is_fullscreen) { + if (window->min_w) { + window_width = viewport_width = SDL_max(viewport_width, window->min_w); + } + if (window->min_h) { + window_height = viewport_height = SDL_max(viewport_height, window->min_h); + } + if (window->max_w) { + window_width = viewport_width = SDL_min(viewport_width, window->max_w); + } + if (window->max_h) { + window_height = viewport_height = SDL_min(viewport_height, window->max_h); + } + + float aspect = (float)viewport_width / (float)viewport_height; + if (window->min_aspect != 0.f && aspect < window->min_aspect) { + viewport_height = SDL_lroundf((float)viewport_width / window->min_aspect); + } else if (window->max_aspect != 0.f && aspect > window->max_aspect) { + viewport_width = SDL_lroundf((float)viewport_height * window->max_aspect); + } - window_size_changed = window_width != data->current.logical_width || window_height != data->current.logical_height; + // At this point, the viewport matches the window dimensions, but the viewport might be clamped to window dimensions beyond here. + window_width = viewport_width; + window_height = viewport_height; + + // If the viewport bounds exceed the window size, scale them while maintaining the aspect ratio. + if (viewport_width > data->requested.logical_width || viewport_height > data->requested.logical_height) { + aspect = (float)viewport_width / (float)viewport_height; + const float window_ratio = (float)data->requested.logical_width / (float)data->requested.logical_height; + if (aspect >= window_ratio) { + viewport_width = data->requested.logical_width; + viewport_height = SDL_lroundf((float)viewport_width / aspect); + } else if (aspect < window_ratio) { + viewport_height = data->requested.logical_height; + viewport_width = SDL_lroundf((float)viewport_height * aspect); + } + } + } else { + window_width = viewport_width; + window_height = viewport_height; + } + + if (!data->scale_to_display) { + data->current.pixel_width = PointToPixel(window, window_width); + data->current.pixel_height = PointToPixel(window, window_height); + } else { + // The viewport size is in pixels at this point; convert it to logical units. + data->current.pixel_width = window_width; + data->current.pixel_height = window_height; + viewport_width = PixelToPoint(window, viewport_width); + viewport_height = PixelToPoint(window, viewport_height); + } + + // Clamp the physical window size to the system minimum required size. + data->requested.logical_width = SDL_max(data->requested.logical_width, data->system_limits.min_width); + data->requested.logical_height = SDL_max(data->requested.logical_height, data->system_limits.min_height); + + window_size_changed = data->requested.logical_width != data->current.logical_width || + data->requested.logical_height != data->current.logical_height || + viewport_width != data->current.viewport_width || + viewport_height != data->current.viewport_height; + + buffer_size_changed = data->current.pixel_width != old_pixel_width || + data->current.pixel_height != old_pixel_height; if (window_size_changed || buffer_size_changed) { if (data->viewport) { - wp_viewport_set_destination(data->viewport, window_width, window_height); + wp_viewport_set_destination(data->viewport, viewport_width, viewport_height); } else if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { // Don't change this if the DPI awareness flag is unset, as an application may have set this manually on a custom or external surface. wl_surface_set_buffer_scale(data->surface, (int32_t)scale_factor); } - // Clamp the physical window size to the system minimum required size. - data->current.logical_width = SDL_max(window_width, data->system_limits.min_width); - data->current.logical_height = SDL_max(window_height, data->system_limits.min_height); + data->current.logical_width = data->requested.logical_width; + data->current.logical_height = data->requested.logical_height; + data->current.viewport_width = viewport_width; + data->current.viewport_height = viewport_height; - if (!data->scale_to_display) { - data->pointer_scale.x = 1.0; - data->pointer_scale.y = 1.0; - } else { - data->pointer_scale.x = scale_factor; - data->pointer_scale.y = scale_factor; + data->pointer_scale.x = (double)window_width / (double)viewport_width; + data->pointer_scale.y = (double)window_height / (double)viewport_height; + } + } + + if (data->egl_window && buffer_size_changed) { + WAYLAND_wl_egl_window_resize(data->egl_window, + data->current.pixel_width, + data->current.pixel_height, + 0, 0); + } + + /* Calculate the mask size and offset. + * + * Libdecor needs some extra functionality to position the mask correctly that is not yet upstream, + * however, it turns out that the compositors that actually need libdecor also center and mask + * fullscreen windows automatically, so it happens to work out. + */ + if (data->viewport && data->waylandData->subcompositor && + (viewport_width != data->current.logical_width || viewport_height != data->current.logical_height) && + (!data->is_fullscreen || data->shell_surface_type != WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR)) { + if (!data->mask.surface) { + data->mask.surface = wl_compositor_create_surface(data->waylandData->compositor); + } + if (!data->mask.subsurface) { + data->mask.subsurface = wl_subcompositor_get_subsurface(data->waylandData->subcompositor, data->mask.surface, data->surface); + } + if (!data->mask.viewport) { + data->mask.viewport = wp_viewporter_get_viewport(data->waylandData->viewporter, data->mask.surface); + } + if (!data->mask.buffer || data->mask.opaque != is_opaque) { + struct wl_buffer *new_buffer = NULL; + new_buffer = Wayland_CreateSinglePixelBuffer(0, 0, 0, is_opaque ? SDL_MAX_UINT32 : 0); + + wl_surface_attach(data->mask.surface, new_buffer, 0, 0); + + if (data->mask.buffer) { + wl_buffer_destroy(data->mask.buffer); } + data->mask.buffer = new_buffer; + data->mask.opaque = is_opaque; + } else { + wl_surface_attach(data->mask.surface, data->mask.buffer, 0, 0); + } + + if (wl_surface_get_version(data->mask.surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) { + wl_surface_damage_buffer(data->mask.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); + } else { + wl_surface_damage(data->mask.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); + } + + wl_subsurface_place_below(data->mask.subsurface, data->surface); + wp_viewport_set_destination(data->mask.viewport, data->current.logical_width, data->current.logical_height); + + if (is_opaque) { + SetSurfaceOpaqueRegion(data->mask.surface, data->current.logical_width, data->current.logical_height); + } else { + SetSurfaceOpaqueRegion(data->mask.surface, 0, 0); + } + + if (data->shell_surface_type != WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) { + data->mask.offset_x = -(data->current.logical_width - viewport_width) / 2; + data->mask.offset_y = -(data->current.logical_height - viewport_height) / 2; + wl_subsurface_set_position(data->mask.subsurface, data->mask.offset_x, data->mask.offset_y); } + + wl_surface_commit(data->mask.surface); + + data->mask.mapped = true; + } else if (data->mask.mapped) { + wl_surface_attach(data->mask.surface, NULL, 0, 0); + wl_surface_commit(data->mask.surface); + wl_subsurface_set_position(data->mask.subsurface, 0, 0); + + data->mask.mapped = false; } /* @@ -421,7 +525,11 @@ static void ConfigureWindowGeometry(SDL_Window *window) xdg_surface_set_window_geometry(data->shell_surface.xdg.surface, 0, 0, data->current.logical_width, data->current.logical_height); } - SetSurfaceOpaqueRegion(data, !(window->flags & SDL_WINDOW_TRANSPARENT) && window->opacity == 1.0f); + if (is_opaque) { + SetSurfaceOpaqueRegion(data->surface, viewport_width, viewport_height); + } else { + SetSurfaceOpaqueRegion(data->surface, 0, 0); + } // Ensure that child popup windows are still in bounds. for (SDL_Window *child = window->first_child; child; child = child->next_sibling) { @@ -473,38 +581,21 @@ static void CommitLibdecorFrame(SDL_Window *window) #endif } -static void fullscreen_deadline_handler(void *data, struct wl_callback *callback, uint32_t callback_data) +static void pending_state_deadline_handler(void *data, struct wl_callback *callback, uint32_t callback_data) { // Get the window from the ID as it may have been destroyed SDL_WindowID windowID = (SDL_WindowID)((uintptr_t)data); SDL_Window *window = SDL_GetWindowFromID(windowID); if (window && window->internal) { - window->internal->fullscreen_deadline_count--; + window->internal->pending_state_deadline_count--; } wl_callback_destroy(callback); } -static struct wl_callback_listener fullscreen_deadline_listener = { - fullscreen_deadline_handler -}; - -static void maximized_restored_deadline_handler(void *data, struct wl_callback *callback, uint32_t callback_data) -{ - // Get the window from the ID as it may have been destroyed - SDL_WindowID windowID = (SDL_WindowID)((uintptr_t)data); - SDL_Window *window = SDL_GetWindowFromID(windowID); - - if (window && window->internal) { - window->internal->maximized_restored_deadline_count--; - } - - wl_callback_destroy(callback); -} - -static struct wl_callback_listener maximized_restored_deadline_listener = { - maximized_restored_deadline_handler +static struct wl_callback_listener pending_state_deadline_listener = { + pending_state_deadline_handler }; static void FlushPendingEvents(SDL_Window *window) @@ -513,7 +604,7 @@ static void FlushPendingEvents(SDL_Window *window) const bool last_position_pending = window->last_position_pending; const bool last_size_pending = window->last_size_pending; - while (window->internal->fullscreen_deadline_count || window->internal->maximized_restored_deadline_count) { + while (window->internal->pending_state_deadline_count) { WAYLAND_wl_display_roundtrip(window->internal->waylandData->display); } @@ -588,11 +679,7 @@ static void SetFullscreen(SDL_Window *window, struct wl_output *output, bool ful } wind->fullscreen_exclusive = output ? window->fullscreen_exclusive : false; - ++wind->fullscreen_deadline_count; if (fullscreen) { - Wayland_SetWindowResizable(SDL_GetVideoDevice(), window, true); - wl_surface_commit(wind->surface); - libdecor_frame_set_fullscreen(wind->shell_surface.libdecor.frame, output); } else { libdecor_frame_unset_fullscreen(wind->shell_surface.libdecor.frame); @@ -605,11 +692,7 @@ static void SetFullscreen(SDL_Window *window, struct wl_output *output, bool ful } wind->fullscreen_exclusive = output ? window->fullscreen_exclusive : false; - ++wind->fullscreen_deadline_count; if (fullscreen) { - Wayland_SetWindowResizable(SDL_GetVideoDevice(), window, true); - wl_surface_commit(wind->surface); - xdg_toplevel_set_fullscreen(wind->shell_surface.xdg.toplevel.xdg_toplevel, output); } else { xdg_toplevel_unset_fullscreen(wind->shell_surface.xdg.toplevel.xdg_toplevel); @@ -617,8 +700,9 @@ static void SetFullscreen(SDL_Window *window, struct wl_output *output, bool ful } // Queue a deadline event + ++wind->pending_state_deadline_count; struct wl_callback *cb = wl_display_sync(viddata->display); - wl_callback_add_listener(cb, &fullscreen_deadline_listener, (void *)((uintptr_t)window->id)); + wl_callback_add_listener(cb, &pending_state_deadline_listener, (void *)((uintptr_t)window->id)); } static void UpdateWindowFullscreen(SDL_Window *window, bool fullscreen) @@ -677,6 +761,8 @@ static void surface_frame_done(void *data, struct wl_callback *cb, uint32_t time wl_surface_damage(wind->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); } + wind->pending_state_commit = false; + if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) { if (wind->pending_config_ack) { wind->pending_config_ack = false; @@ -824,6 +910,9 @@ static void handle_xdg_toplevel_configure(void *data, } } + // When resizing, dimensions other than 0 are a maximum. + const bool new_configure_size = width != wind->last_configure.width || height != wind->last_configure.height; + UpdateWindowFullscreen(window, fullscreen); /* Always send a maximized/restore event; if the event is redundant it will @@ -848,51 +937,72 @@ static void handle_xdg_toplevel_configure(void *data, /* xdg_toplevel spec states that this is a suggestion. * Ignore if less than or greater than max/min size. */ - if (window->flags & SDL_WINDOW_RESIZABLE) { - if (width == 0 || height == 0) { + if ((window->flags & SDL_WINDOW_RESIZABLE) || maximized) { + if (!width) { /* This happens when the compositor indicates that the size is * up to the client, so use the cached window size here. */ if (floating) { width = window->floating.w; - height = window->floating.h; // Clamp the window to the toplevel bounds, if any are set. - if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && - wind->toplevel_bounds.width && wind->toplevel_bounds.height) { + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && wind->toplevel_bounds.width) { width = SDL_min(wind->toplevel_bounds.width, width); - height = SDL_min(wind->toplevel_bounds.height, height); } } else { width = window->windowed.w; - height = window->windowed.h; } if (!wind->scale_to_display) { wind->requested.logical_width = width; - wind->requested.logical_height = height; } else { wind->requested.pixel_width = width; - wind->requested.pixel_height = height; width = wind->requested.logical_width = PixelToPoint(window, width); - height = wind->requested.logical_height = PixelToPoint(window, height); } - } else { + } else if (new_configure_size) { /* Don't apply the supplied dimensions if they haven't changed from the last configuration * event, or a newer size set programmatically can be overwritten by old data. */ - if (width != wind->last_configure.width || height != wind->last_configure.height) { - wind->requested.logical_width = width; - wind->requested.logical_height = height; - if (wind->scale_to_display) { - wind->requested.pixel_width = PointToPixel(window, width); - wind->requested.pixel_height = PointToPixel(window, height); + wind->requested.logical_width = width; + + if (wind->scale_to_display) { + wind->requested.pixel_width = PointToPixel(window, width); + } + } + if (!height) { + /* This happens when the compositor indicates that the size is + * up to the client, so use the cached window size here. + */ + if (floating) { + height = window->floating.h; + + // Clamp the window to the toplevel bounds, if any are set. + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && wind->toplevel_bounds.height) { + height = SDL_min(wind->toplevel_bounds.height, height); } + } else { + height = window->windowed.h; + } + + if (!wind->scale_to_display) { + wind->requested.logical_height = height; + } else { + wind->requested.pixel_height = height; + height = wind->requested.logical_height = PixelToPoint(window, height); + } + } else if (new_configure_size) { + /* Don't apply the supplied dimensions if they haven't changed from the last configuration + * event, or a newer size set programmatically can be overwritten by old data. + */ + wind->requested.logical_height = height; + + if (wind->scale_to_display) { + wind->requested.pixel_height = PointToPixel(window, height); } } } else { - /* If we're a fixed-size window, we know our size for sure. + /* If we're a fixed-size, non-maximized window, we know our size for sure. * Always assume the configure is wrong. */ if (!wind->scale_to_display) { @@ -906,20 +1016,24 @@ static void handle_xdg_toplevel_configure(void *data, } } - /* Notes on the spec: + /* Notes on the spec and implementations: * * - The content limits are only a hint, which the compositor is free to ignore, * so apply them manually when appropriate. * - * - Maximized windows must have their exact dimensions respected, thus they must - * not be resized, or a protocol violation can occur. + * - Only floating windows are truly safe to resize: maximized windows must have + * their exact dimensions respected, or a protocol violation can occur, and tiled + * windows can technically use dimensions smaller than the ones supplied by the + * compositor, but doing so can cause odd behavior. In these cases it's best to use + * the supplied dimensions and use a viewport + mask to enforce the size limits and/or + * aspect ratio. * * - When resizing a window, the width/height are maximum values, so aspect ratio * correction can't resize beyond the existing dimensions, or a protocol violation - * can occur. In practice, nothing seems to kill clients that do this, but doing - * so causes GNOME to glitch out. + * can occur. However, in practice, nothing seems to kill clients that do this, but + * doing so can cause certain compositors to glitch out. */ - if (!maximized) { + if (floating) { if (!wind->scale_to_display) { if (window->max_w > 0) { wind->requested.logical_width = SDL_min(wind->requested.logical_width, window->max_w); @@ -1195,7 +1309,6 @@ static void decoration_frame_configure(struct libdecor_frame *frame, enum libdecor_window_state window_state; int width, height; - bool prev_fullscreen = wind->is_fullscreen; bool active = false; bool fullscreen = false; bool maximized = false; @@ -1207,6 +1320,10 @@ static void decoration_frame_configure(struct libdecor_frame *frame, static const enum libdecor_window_state tiled_states = (LIBDECOR_WINDOW_STATE_TILED_LEFT | LIBDECOR_WINDOW_STATE_TILED_RIGHT | LIBDECOR_WINDOW_STATE_TILED_TOP | LIBDECOR_WINDOW_STATE_TILED_BOTTOM); + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) { + LibdecorGetMinContentSize(frame, &wind->system_limits.min_width, &wind->system_limits.min_height); + } + // Window State if (libdecor_configuration_get_window_state(configuration, &window_state)) { fullscreen = (window_state & LIBDECOR_WINDOW_STATE_FULLSCREEN) != 0; @@ -1273,8 +1390,8 @@ static void decoration_frame_configure(struct libdecor_frame *frame, } } } else { - if (!(window->flags & SDL_WINDOW_RESIZABLE)) { - /* If we're a fixed-size window, we know our size for sure. + if (!(window->flags & SDL_WINDOW_RESIZABLE) && !maximized) { + /* If we're a fixed-size, non-maximized window, we know our size for sure. * Always assume the configure is wrong. */ if (!wind->scale_to_display) { @@ -1299,33 +1416,33 @@ static void decoration_frame_configure(struct libdecor_frame *frame, */ if ((floating && (!wind->floating && !(window->flags & SDL_WINDOW_BORDERLESS))) || !libdecor_configuration_get_content_size(configuration, frame, &width, &height)) { + width = 0; + height = 0; + } + + const bool new_configure_size = width != wind->last_configure.width || height != wind->last_configure.height; + + if (!width) { /* This happens when we're being restored from a non-floating state, * or the compositor indicates that the size is up to the client, so * used the cached window size here. */ if (floating) { width = window->floating.w; - height = window->floating.h; // Clamp the window to the toplevel bounds, if any are set. - if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && - wind->toplevel_bounds.width && wind->toplevel_bounds.height) { + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && wind->toplevel_bounds.width) { width = SDL_min(wind->toplevel_bounds.width, width); - height = SDL_min(wind->toplevel_bounds.height, height); } } else { width = window->windowed.w; - height = window->windowed.h; } if (!wind->scale_to_display) { wind->requested.logical_width = width; - wind->requested.logical_height = height; } else { wind->requested.pixel_width = width; - wind->requested.pixel_height = height; width = wind->requested.logical_width = PixelToPoint(window, width); - height = wind->requested.logical_height = PixelToPoint(window, height); } } else { /* Don't apply the supplied dimensions if they haven't changed from the last configuration @@ -1335,33 +1452,75 @@ static void decoration_frame_configure(struct libdecor_frame *frame, * configure event to set the suspended state may arrive with the content size increased * by the decoration dimensions, which should also be ignored. */ - if ((width != wind->last_configure.width || height != wind->last_configure.height) && + if (new_configure_size && !(wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME && wind->suspended != suspended)) { wind->requested.logical_width = width; - wind->requested.logical_height = height; if (wind->scale_to_display) { wind->requested.pixel_width = PointToPixel(window, width); + } + } + } + + if (!height) { + /* This happens when we're being restored from a non-floating state, + * or the compositor indicates that the size is up to the client, so + * used the cached window size here. + */ + if (floating) { + height = window->floating.h; + + // Clamp the window to the toplevel bounds, if any are set. + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && wind->toplevel_bounds.height) { + height = SDL_min(wind->toplevel_bounds.height, height); + } + } else { + height = window->windowed.h; + } + + if (!wind->scale_to_display) { + wind->requested.logical_height = height; + } else { + wind->requested.pixel_height = height; + height = wind->requested.logical_height = PixelToPoint(window, height); + } + } else { + /* Don't apply the supplied dimensions if they haven't changed from the last configuration + * event, or a newer size set programmatically can be overwritten by old data. + * + * If a client takes a long time to present the first frame after creating the window, a + * configure event to set the suspended state may arrive with the content size increased + * by the decoration dimensions, which should also be ignored. + */ + if (new_configure_size && + !(wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME && wind->suspended != suspended)) { + wind->requested.logical_height = height; + + if (wind->scale_to_display) { wind->requested.pixel_height = PointToPixel(window, height); } } } } - /* Notes on the spec: + /* Notes on the spec and implementations: * * - The content limits are only a hint, which the compositor is free to ignore, * so apply them manually when appropriate. * - * - Maximized windows must have their exact dimensions respected, thus they must - * not be resized, or a protocol violation can occur. + * - Only floating windows are truly safe to resize: maximized windows must have + * their exact dimensions respected, or a protocol violation can occur, and tiled + * windows can technically use dimensions smaller than the ones supplied by the + * compositor, but doing so can cause odd behavior. In these cases it's best to use + * the supplied dimensions and use a viewport + mask to enforce the size limits and/or + * aspect ratio. * * - When resizing a window, the width/height are maximum values, so aspect ratio * correction can't resize beyond the existing dimensions, or a protocol violation - * can occur. In practice, nothing seems to kill clients that do this, but doing - * so causes GNOME to glitch out. + * can occur. However, in practice, nothing seems to kill clients that do this, but + * doing so can cause certain compositors to glitch out. */ - if (!maximized) { + if (floating) { if (!wind->scale_to_display) { if (window->max_w > 0) { wind->requested.logical_width = SDL_min(wind->requested.logical_width, window->max_w); @@ -1450,20 +1609,8 @@ static void decoration_frame_configure(struct libdecor_frame *frame, } if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) { - LibdecorGetMinContentSize(frame, &wind->system_limits.min_width, &wind->system_limits.min_height); wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME; } - - /* Update the resize capability if this config event was the result of the - * compositor taking a window out of fullscreen. Since this will change the - * capabilities and commit a new frame state with the last known content - * dimension, this has to be called after the new state has been committed - * and the new content dimensions were updated. - */ - if (prev_fullscreen && !wind->is_fullscreen) { - Wayland_SetWindowResizable(SDL_GetVideoDevice(), window, - !!(window->flags & SDL_WINDOW_RESIZABLE)); - } } static void decoration_frame_close(struct libdecor_frame *frame, void *user_data) @@ -2224,6 +2371,10 @@ void Wayland_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) } // Attach a null buffer to unmap the surface. + if (wind->mask.surface) { + wl_surface_attach(wind->mask.surface, NULL, 0, 0); + wl_surface_commit(wind->mask.surface); + } wl_surface_attach(wind->surface, NULL, 0, 0); wl_surface_commit(wind->surface); @@ -2383,6 +2534,11 @@ SDL_FullscreenResult Wayland_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Win output = NULL; } } + + // Commit to set any pending size or limit data. + if (fullscreen && wind->pending_state_commit) { + wl_surface_commit(wind->surface); + } SetFullscreen(window, output, !!fullscreen); } else if (wind->is_fullscreen) { /* @@ -2417,7 +2573,7 @@ void Wayland_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window) // Not currently fullscreen or maximized, and no state pending; nothing to do. if (!(window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) && - !wind->fullscreen_deadline_count && !wind->maximized_restored_deadline_count) { + !wind->pending_state_deadline_count) { return; } @@ -2428,9 +2584,9 @@ void Wayland_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window) } libdecor_frame_unset_maximized(wind->shell_surface.libdecor.frame); - ++wind->maximized_restored_deadline_count; + ++wind->pending_state_deadline_count; struct wl_callback *cb = wl_display_sync(_this->internal->display); - wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id)); + wl_callback_add_listener(cb, &pending_state_deadline_listener, (void *)((uintptr_t)window->id)); } else #endif // Note that xdg-shell does NOT provide a way to unset minimize! @@ -2440,9 +2596,9 @@ void Wayland_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window) } xdg_toplevel_unset_maximized(wind->shell_surface.xdg.toplevel.xdg_toplevel); - ++wind->maximized_restored_deadline_count; + ++wind->pending_state_deadline_count; struct wl_callback *cb = wl_display_sync(_this->internal->display); - wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id)); + wl_callback_add_listener(cb, &pending_state_deadline_listener, (void *)((uintptr_t)window->id)); } } @@ -2468,28 +2624,17 @@ void Wayland_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool void Wayland_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable) { -#ifdef HAVE_LIBDECOR_H - const SDL_WindowData *wind = window->internal; - - if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) { - if (!wind->shell_surface.libdecor.frame) { - return; // Can't do anything yet, wait for ShowWindow - } - if (libdecor_frame_has_capability(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE)) { - if (!resizable) { - libdecor_frame_unset_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE); - } - } else if (resizable) { - libdecor_frame_set_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE); - } - } -#endif + SDL_WindowData *wind = window->internal; /* When changing the resize capability on libdecor windows, the limits must always * be reapplied, as when libdecor changes states, it overwrites the values internally. */ SetMinMaxDimensions(window); CommitLibdecorFrame(window); + + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_SHOWN) { + wind->pending_state_commit = true; + } } void Wayland_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window) @@ -2503,7 +2648,7 @@ void Wayland_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window) // Not fullscreen, already maximized, and no state pending; nothing to do. if (!(window->flags & SDL_WINDOW_FULLSCREEN) && (window->flags & SDL_WINDOW_MAXIMIZED) && - !wind->fullscreen_deadline_count && !wind->maximized_restored_deadline_count) { + !wind->pending_state_deadline_count) { return; } @@ -2513,13 +2658,15 @@ void Wayland_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window) return; // Can't do anything yet, wait for ShowWindow } - // Commit to preserve any pending size data. - wl_surface_commit(wind->surface); + // Commit to set any pending size or limit data. + if (wind->pending_state_commit) { + wl_surface_commit(wind->surface); + } libdecor_frame_set_maximized(wind->shell_surface.libdecor.frame); - ++wind->maximized_restored_deadline_count; + ++wind->pending_state_deadline_count; struct wl_callback *cb = wl_display_sync(viddata->display); - wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id)); + wl_callback_add_listener(cb, &pending_state_deadline_listener, (void *)((uintptr_t)window->id)); } else #endif if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) { @@ -2527,13 +2674,15 @@ void Wayland_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window) return; // Can't do anything yet, wait for ShowWindow } - // Commit to preserve any pending size data. - wl_surface_commit(wind->surface); + // Commit to set any pending size or limit data. + if (wind->pending_state_commit) { + wl_surface_commit(wind->surface); + } xdg_toplevel_set_maximized(wind->shell_surface.xdg.toplevel.xdg_toplevel); - ++wind->maximized_restored_deadline_count; + ++wind->pending_state_deadline_count; struct wl_callback *cb = wl_display_sync(viddata->display); - wl_callback_add_listener(cb, &maximized_restored_deadline_listener, (void *)((uintptr_t)window->id)); + wl_callback_add_listener(cb, &pending_state_deadline_listener, (void *)((uintptr_t)window->id)); } } @@ -2838,13 +2987,19 @@ bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Proper void Wayland_SetWindowMinimumSize(SDL_VideoDevice *_this, SDL_Window *window) { // Will be committed when Wayland_SetWindowSize() is called by the video core. - SetMinMaxDimensions(window); + window->internal->limits_changed = true; } void Wayland_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window) { // Will be committed when Wayland_SetWindowSize() is called by the video core. - SetMinMaxDimensions(window); + window->internal->limits_changed = true; +} + +void Wayland_SetWindowAspectRatio(SDL_VideoDevice *_this, SDL_Window *window) +{ + // Will be committed when Wayland_SetWindowSize() is called by the video core. + window->internal->limits_changed = true; } bool Wayland_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window) @@ -2894,8 +3049,10 @@ void Wayland_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window) */ FlushPendingEvents(window); - // Maximized and fullscreen windows don't get resized. - if (!(window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) || + /* Maximized and fullscreen windows don't get resized, and the new size is ignored + * if this is just to recalculate the min/max or aspect limits on a tiled window. + */ + if (wind->floating || (window->tiled && !wind->limits_changed) || wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_CUSTOM) { if (!wind->scale_to_display) { wind->requested.logical_width = window->pending.w; @@ -2906,14 +3063,18 @@ void Wayland_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window) wind->requested.pixel_width = window->pending.w; wind->requested.pixel_height = window->pending.h; } - - ConfigureWindowGeometry(window); } else { // Can't resize the window. window->last_size_pending = false; } - // Always commit, as this may be in response to a min/max limit change. + wind->limits_changed = false; + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_SHOWN) { + wind->pending_state_commit = true; + } + + // Always recalculate the geometry, as this may be in response to a min/max limit change. + ConfigureWindowGeometry(window); CommitLibdecorFrame(window); } @@ -2952,7 +3113,41 @@ bool Wayland_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float SDL_WindowData *wind = window->internal; if (wind->wp_alpha_modifier_surface_v1) { - SetSurfaceOpaqueRegion(wind, !(window->flags & SDL_WINDOW_TRANSPARENT) && opacity == 1.0f); + const bool is_opaque = !(window->flags & SDL_WINDOW_TRANSPARENT) && opacity == 1.0f; + + if (wind->mask.mapped && wind->mask.opaque != is_opaque) { + struct wl_buffer *new_buffer = NULL; + new_buffer = Wayland_CreateSinglePixelBuffer(0, 0, 0, is_opaque ? SDL_MAX_UINT32 : 0); + + wl_surface_attach(wind->mask.surface, new_buffer, 0, 0); + if (wl_surface_get_version(wind->mask.surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) { + wl_surface_damage_buffer(wind->mask.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); + } else { + wl_surface_damage(wind->mask.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); + } + + if (is_opaque) { + SetSurfaceOpaqueRegion(wind->mask.surface, wind->current.logical_width, wind->current.logical_height); + } else { + SetSurfaceOpaqueRegion(wind->mask.surface, 0, 0); + } + + wl_surface_commit(wind->mask.surface); + + if (wind->mask.buffer) { + wl_buffer_destroy(wind->mask.buffer); + } + + wind->mask.buffer = new_buffer; + wind->mask.opaque = is_opaque; + } + + if (is_opaque) { + SetSurfaceOpaqueRegion(wind->surface, wind->current.viewport_width, wind->current.viewport_height); + } else { + SetSurfaceOpaqueRegion(wind->surface, 0, 0); + } + wp_alpha_modifier_surface_v1_set_multiplier(wind->wp_alpha_modifier_surface_v1, (Uint32)((double)SDL_MAX_UINT32 * (double)opacity)); return true; @@ -3145,7 +3340,7 @@ bool Wayland_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window) do { WAYLAND_wl_display_roundtrip(_this->internal->display); - } while (wind->fullscreen_deadline_count || wind->maximized_restored_deadline_count); + } while (wind->pending_state_deadline_count); return true; } @@ -3257,6 +3452,18 @@ void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) */ Wayland_DisplayRemoveWindowReferencesFromSeats(data, wind); + if (wind->mask.viewport) { + wp_viewport_destroy(wind->mask.viewport); + } + if (wind->mask.buffer) { + wl_buffer_destroy(wind->mask.buffer); + } + if (wind->mask.subsurface) { + wl_subsurface_destroy(wind->mask.subsurface); + } + if (wind->mask.surface) { + wl_surface_destroy(wind->mask.surface); + } #ifdef SDL_VIDEO_OPENGL_EGL if (wind->egl_surface) { SDL_EGL_DestroySurface(_this, wind->egl_surface); diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h index eeec232b7a9cc..8495e99eec75e 100644 --- a/src/video/wayland/SDL_waylandwindow.h +++ b/src/video/wayland/SDL_waylandwindow.h @@ -165,6 +165,10 @@ struct SDL_WindowData // The size of the window backbuffer in pixels. int pixel_width; int pixel_height; + + // The dimensions of the active viewport, in logical units. + int viewport_width; + int viewport_height; } current; // The last compositor requested parameters; used for deduplication of window geometry configuration. @@ -188,6 +192,20 @@ struct SDL_WindowData int height; } toplevel_bounds; + struct + { + struct wl_surface *surface; + struct wl_subsurface *subsurface; + struct wl_buffer *buffer; + struct wp_viewport *viewport; + + int offset_x; + int offset_y; + + bool mapped; + bool opaque; + } mask; + struct { int hint; @@ -196,8 +214,7 @@ struct SDL_WindowData } text_input_props; SDL_DisplayID last_displayID; - int fullscreen_deadline_count; - int maximized_restored_deadline_count; + int pending_state_deadline_count; Uint64 last_focus_event_time_ns; int icc_fd; Uint32 icc_size; @@ -206,6 +223,8 @@ struct SDL_WindowData bool resizing; bool active; bool pending_config_ack; + bool pending_state_commit; + bool limits_changed; bool is_fullscreen; bool fullscreen_exclusive; bool drop_fullscreen_requests; @@ -236,6 +255,7 @@ extern void Wayland_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *windo extern bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props); extern bool Wayland_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window); extern void Wayland_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window); +extern void Wayland_SetWindowAspectRatio(SDL_VideoDevice *_this, SDL_Window *window); extern void Wayland_SetWindowMinimumSize(SDL_VideoDevice *_this, SDL_Window *window); extern void Wayland_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window); extern void Wayland_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h); diff --git a/wayland-protocols/single-pixel-buffer-v1.xml b/wayland-protocols/single-pixel-buffer-v1.xml new file mode 100644 index 0000000000000..bcbd0382eb633 --- /dev/null +++ b/wayland-protocols/single-pixel-buffer-v1.xml @@ -0,0 +1,76 @@ + + + + Copyright © 2022 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol extension allows clients to create single-pixel buffers. + + Compositors supporting this protocol extension should also support the + viewporter protocol extension. Clients may use viewporter to scale a + single-pixel buffer to a desired size. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + The wp_single_pixel_buffer_manager_v1 interface is a factory for + single-pixel buffers. + + + + + Destroy the wp_single_pixel_buffer_manager_v1 object. + + The child objects created via this interface are unaffected. + + + + + + Create a single-pixel buffer from four 32-bit RGBA values. + + Unless specified in another protocol extension, the RGBA values use + pre-multiplied alpha. + + The width and height of the buffer are 1. + + The r, g, b and a arguments valid range is from UINT32_MIN (0) + to UINT32_MAX (0xffffffff). + + These arguments should be interpreted as a percentage, i.e. + - UINT32_MIN = 0% of the given color component + - UINT32_MAX = 100% of the given color component + + + + + + + + +