SDL: wayland: Implement SetWindowOpacity via the alpha modifier protocol

From 716dc0e1bfdf854f4956430c543e6eeb2967b743 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Tue, 14 May 2024 13:10:09 -0400
Subject: [PATCH] wayland: Implement SetWindowOpacity via the alpha modifier
 protocol

The wp_alpha_modifier_v1 protocol allows for a global blending factor to be specified for an entire surface. Use this to add support for SDL_SetWindowOpacity().
---
 src/video/wayland/SDL_waylandvideo.c    |   9 +++
 src/video/wayland/SDL_waylandvideo.h    |   1 +
 src/video/wayland/SDL_waylandwindow.c   |  50 +++++++++---
 src/video/wayland/SDL_waylandwindow.h   |   2 +
 wayland-protocols/alpha-modifier-v1.xml | 103 ++++++++++++++++++++++++
 5 files changed, 155 insertions(+), 10 deletions(-)
 create mode 100644 wayland-protocols/alpha-modifier-v1.xml

diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index 5f50c79a90e58..0567ae5bda20d 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -43,6 +43,7 @@
 
 #include <wayland-util.h>
 
+#include "alpha-modifier-v1-client-protocol.h"
 #include "cursor-shape-v1-client-protocol.h"
 #include "fractional-scale-v1-client-protocol.h"
 #include "idle-inhibit-unstable-v1-client-protocol.h"
@@ -487,6 +488,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(void)
     device->SetWindowMinimumSize = Wayland_SetWindowMinimumSize;
     device->SetWindowMaximumSize = Wayland_SetWindowMaximumSize;
     device->SetWindowModalFor = Wayland_SetWindowModalFor;
+    device->SetWindowOpacity = Wayland_SetWindowOpacity;
     device->SetWindowTitle = Wayland_SetWindowTitle;
     device->GetWindowSizeInPixels = Wayland_GetWindowSizeInPixels;
     device->GetDisplayForWindow = Wayland_GetDisplayForWindow;
@@ -1107,6 +1109,8 @@ static void display_handle_global(void *data, struct wl_registry *registry, uint
         d->zxdg_exporter_v2 = wl_registry_bind(d->registry, id, &zxdg_exporter_v2_interface, 1);
     } else if (SDL_strcmp(interface, "xdg_wm_dialog_v1") == 0) {
         d->xdg_wm_dialog_v1 = wl_registry_bind(d->registry, id, &xdg_wm_dialog_v1_interface, 1);
+    } else if (SDL_strcmp(interface, "wp_alpha_modifier_v1") == 0) {
+        d->wp_alpha_modifier_v1 = wl_registry_bind(d->registry, id, &wp_alpha_modifier_v1_interface, 1);
     } else if (SDL_strcmp(interface, "kde_output_order_v1") == 0) {
         d->kde_output_order = wl_registry_bind(d->registry, id, &kde_output_order_v1_interface, 1);
         kde_output_order_v1_add_listener(d->kde_output_order, &kde_output_order_listener, d);
@@ -1369,6 +1373,11 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this)
         data->xdg_wm_dialog_v1 = NULL;
     }
 
+    if (data->wp_alpha_modifier_v1) {
+        wp_alpha_modifier_v1_destroy(data->wp_alpha_modifier_v1);
+        data->wp_alpha_modifier_v1 = NULL;
+    }
+
     if (data->kde_output_order) {
         Wayland_FlushOutputOrder(data);
         kde_output_order_v1_destroy(data->kde_output_order);
diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h
index abf33d614b818..f5cc778b9e78e 100644
--- a/src/video/wayland/SDL_waylandvideo.h
+++ b/src/video/wayland/SDL_waylandvideo.h
@@ -81,6 +81,7 @@ struct SDL_VideoData
     struct zwp_input_timestamps_manager_v1 *input_timestamps_manager;
     struct zxdg_exporter_v2 *zxdg_exporter_v2;
     struct xdg_wm_dialog_v1 *xdg_wm_dialog_v1;
+    struct wp_alpha_modifier_v1 *wp_alpha_modifier_v1;
     struct kde_output_order_v1 *kde_output_order;
 
     struct xkb_context *xkb_context;
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index c26ab779bc9d1..61d8b64c1402d 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -32,6 +32,7 @@
 #include "SDL_waylandvideo.h"
 #include "../../SDL_hints_c.h"
 
+#include "alpha-modifier-v1-client-protocol.h"
 #include "xdg-shell-client-protocol.h"
 #include "xdg-decoration-unstable-v1-client-protocol.h"
 #include "idle-inhibit-unstable-v1-client-protocol.h"
@@ -279,10 +280,24 @@ static void RepositionPopup(SDL_Window *window, SDL_bool use_current_position)
     }
 }
 
+static void SetSurfaceOpaqueRegion(SDL_WindowData *wind, SDL_bool is_opaque)
+{
+    SDL_VideoData *viddata = wind->waylandData;
+
+    if (is_opaque) {
+        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_destroy(region);
+    } else {
+        wl_surface_set_opaque_region(wind->surface, NULL);
+    }
+}
+
 static void ConfigureWindowGeometry(SDL_Window *window)
 {
     SDL_WindowData *data = window->driverdata;
-    SDL_VideoData *viddata = data->waylandData;
     const int old_pixel_width = data->current.pixel_width;
     const int old_pixel_height = data->current.pixel_height;
     int window_width, window_height;
@@ -391,20 +406,12 @@ static void ConfigureWindowGeometry(SDL_Window *window)
      * need to be recalculated if the output size has changed.
      */
     if (window_size_changed) {
-        struct wl_region *region;
-
         /* libdecor does this internally on frame commits, so it's only needed for xdg surfaces. */
         if (data->shell_surface_type != WAYLAND_SURFACE_LIBDECOR && data->shell_surface.xdg.surface) {
             xdg_surface_set_window_geometry(data->shell_surface.xdg.surface, 0, 0, data->current.logical_width, data->current.logical_height);
         }
 
-        if (!(window->flags & SDL_WINDOW_TRANSPARENT)) {
-            region = wl_compositor_create_region(viddata->compositor);
-            wl_region_add(region, 0, 0,
-                          data->current.logical_width, data->current.logical_height);
-            wl_surface_set_opaque_region(data->surface, region);
-            wl_region_destroy(region);
-        }
+        SetSurfaceOpaqueRegion(data, !(window->flags & SDL_WINDOW_TRANSPARENT) && window->opacity == 1.0f);
 
         /* Ensure that child popup windows are still in bounds. */
         for (SDL_Window *child = window->first_child; child; child = child->next_sibling) {
@@ -2302,6 +2309,11 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
         }
     }
 
+    if (!custom_surface_role && c->wp_alpha_modifier_v1) {
+        data->wp_alpha_modifier_surface_v1 = wp_alpha_modifier_v1_get_surface(c->wp_alpha_modifier_v1, data->surface);
+        wp_alpha_modifier_surface_v1_set_multiplier(data->wp_alpha_modifier_surface_v1, SDL_MAX_UINT32);
+    }
+
     /* Must be called before EGL configuration to set the drawable backbuffer size. */
     ConfigureWindowGeometry(window);
 
@@ -2494,6 +2506,20 @@ SDL_DisplayID Wayland_GetDisplayForWindow(SDL_VideoDevice *_this, SDL_Window *wi
     return 0;
 }
 
+int Wayland_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float opacity)
+{
+    SDL_WindowData *wind = window->driverdata;
+
+    if (wind->wp_alpha_modifier_surface_v1) {
+        SetSurfaceOpaqueRegion(wind, !(window->flags & SDL_WINDOW_TRANSPARENT) && opacity == 1.0f);
+        wp_alpha_modifier_surface_v1_set_multiplier(wind->wp_alpha_modifier_surface_v1, (Uint32)((double)SDL_MAX_UINT32 * (double)opacity));
+
+        return 0;
+    }
+
+    return SDL_SetError("wayland: set window opacity failed; compositor lacks support for the required wp_alpha_modifier_v1 protocol");
+}
+
 void Wayland_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window)
 {
     SDL_WindowData *wind = window->driverdata;
@@ -2614,6 +2640,10 @@ void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window)
             wp_fractional_scale_v1_destroy(wind->fractional_scale);
         }
 
+        if (wind->wp_alpha_modifier_surface_v1) {
+            wp_alpha_modifier_surface_v1_destroy(wind->wp_alpha_modifier_surface_v1);
+        }
+
         SDL_free(wind->outputs);
         SDL_free(wind->app_id);
 
diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h
index 2469a5f394006..21c7e5b8680d1 100644
--- a/src/video/wayland/SDL_waylandwindow.h
+++ b/src/video/wayland/SDL_waylandwindow.h
@@ -97,6 +97,7 @@ struct SDL_WindowData
     struct wp_fractional_scale_v1 *fractional_scale;
     struct zxdg_exported_v2 *exported;
     struct xdg_dialog_v1 *xdg_dialog_v1;
+    struct wp_alpha_modifier_surface_v1 *wp_alpha_modifier_surface_v1;
 
     SDL_AtomicInt swap_interval_ready;
 
@@ -193,6 +194,7 @@ extern void Wayland_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *win
 extern void Wayland_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h);
 extern SDL_DisplayID Wayland_GetDisplayForWindow(SDL_VideoDevice *_this, SDL_Window *window);
 extern int Wayland_SetWindowModalFor(SDL_VideoDevice *_this, SDL_Window *modal_window, SDL_Window *parent_window);
+extern int Wayland_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float opacity);
 extern void Wayland_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window);
 extern void Wayland_ShowWindowSystemMenu(SDL_Window *window, int x, int y);
 extern void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window);
diff --git a/wayland-protocols/alpha-modifier-v1.xml b/wayland-protocols/alpha-modifier-v1.xml
new file mode 100644
index 0000000000000..932fa6f2a94cc
--- /dev/null
+++ b/wayland-protocols/alpha-modifier-v1.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="alpha_modifier_v1">
+  <copyright>
+    Copyright © 2024 Xaver Hugl
+
+    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.
+  </copyright>
+
+  <interface name="wp_alpha_modifier_v1" version="1">
+    <description summary="surface alpha modifier manager">
+      This interface allows a client to set a factor for the alpha values on a
+      surface, which can be used to offload such operations to the compositor,
+      which can in turn for example offload them to KMS.
+
+      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.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the alpha modifier manager object">
+        Destroy the alpha modifier manager. This doesn't destroy objects
+        created with the manager.
+      </description>
+    </request>
+
+    <enum name="error">
+      <entry name="already_constructed" value="0"
+             summary="wl_surface already has a alpha modifier object"/>
+    </enum>
+
+    <request name="get_surface">
+      <description summary="create a new toplevel decoration object">
+        Create a new alpha modifier surface object associated with the
+        given wl_surface. If there is already such an object associated with
+        the wl_surface, the already_constructed error will be raised.
+      </description>
+      <arg name="id" type="new_id" interface="wp_alpha_modifier_surface_v1"/>
+      <arg name="surface" type="object" interface="wl_surface"/>
+    </request>
+  </interface>
+
+  <interface name="wp_alpha_modifier_surface_v1" version="1">
+    <description summary="alpha modifier object for a surface">
+      This interface allows the client to set a factor for the alpha values on
+      a surface, which can be used to offload such operations to the compositor.
+      The default factor is UINT32_MAX.
+
+      This object has to be destroyed before the associated wl_surface. Once the
+      wl_surface is destroyed, all request on this object will raise the
+      no_surface error.
+    </description>
+
+    <enum name="error">
+      <entry name="no_surface" value="0" summary="wl_surface was destroyed"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the alpha modifier object">
+        This destroys the object, and is equivalent to set_multiplier with
+        a value of UINT32_MAX, with the same double-buffered semantics as
+        set_multiplier.
+      </description>
+    </request>
+
+    <request name="set_multiplier">
+      <description summary="specify the alpha multiplier">
+        Sets the alpha multiplier for the surface. The alpha multiplier is
+        double-buffered state, see wl_surface.commit for details.
+
+        This factor is applied in the compositor's blending space, as an
+        additional step after the processing of per-pixel alpha values for the
+        wl_surface. The exact meaning of the factor is thus undefined, unless
+        the blending space is specified in a different extension.
+
+        This multiplier is applied even if the buffer attached to the
+        wl_surface doesn't have an alpha channel; in that case an alpha value
+        of one is used instead.
+
+        Zero means completely transparent, UINT32_MAX means completely opaque.
+      </description>
+      <arg name="factor" type="uint"/>
+    </request>
+  </interface>
+</protocol>