SDL: wayland: Add color manager protocol support

From fadb261b66ee5817008a813a9ea16feb9ec73b31 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sat, 12 Oct 2024 12:10:32 -0400
Subject: [PATCH] wayland: Add color manager protocol support

Support the official wp_color_manager_v1 protocol.
---
 src/video/wayland/SDL_waylandcolor.c      |  224 +++
 src/video/wayland/SDL_waylandcolor.h      |   41 +
 src/video/wayland/SDL_waylandvideo.c      |   69 +-
 src/video/wayland/SDL_waylandvideo.h      |    4 +
 src/video/wayland/SDL_waylandwindow.c     |   55 +-
 src/video/wayland/SDL_waylandwindow.h     |    4 +
 wayland-protocols/color-management-v1.xml | 1631 +++++++++++++++++++++
 7 files changed, 2022 insertions(+), 6 deletions(-)
 create mode 100644 src/video/wayland/SDL_waylandcolor.c
 create mode 100644 src/video/wayland/SDL_waylandcolor.h
 create mode 100644 wayland-protocols/color-management-v1.xml

diff --git a/src/video/wayland/SDL_waylandcolor.c b/src/video/wayland/SDL_waylandcolor.c
new file mode 100644
index 0000000000000..f2bb4552afc2f
--- /dev/null
+++ b/src/video/wayland/SDL_waylandcolor.c
@@ -0,0 +1,224 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+
+#ifdef SDL_VIDEO_DRIVER_WAYLAND
+
+#include "SDL_waylandcolor.h"
+#include "SDL_waylandvideo.h"
+#include "SDL_waylandwindow.h"
+#include "color-management-v1-client-protocol.h"
+
+typedef struct Wayland_ColorInfoState
+{
+    struct wp_image_description_v1 *wp_image_description;
+    struct wp_image_description_info_v1 *wp_image_description_info;
+    Wayland_ColorInfo *info;
+
+    bool result;
+} Wayland_ColorInfoState;
+
+static void image_description_info_handle_done(void *data,
+                                               struct wp_image_description_info_v1 *wp_image_description_info_v1)
+{
+    Wayland_ColorInfoState *state = (Wayland_ColorInfoState *)data;
+
+    if (state->wp_image_description_info) {
+        wp_image_description_info_v1_destroy(state->wp_image_description_info);
+        state->wp_image_description_info = NULL;
+    }
+    if (state->wp_image_description) {
+        wp_image_description_v1_destroy(state->wp_image_description);
+        state->wp_image_description = NULL;
+    }
+
+    state->result = true;
+}
+
+static void image_description_info_handle_icc_file(void *data,
+                                                   struct wp_image_description_info_v1 *wp_image_description_info_v1,
+                                                   int32_t icc, uint32_t icc_size)
+{
+    Wayland_ColorInfoState *state = (Wayland_ColorInfoState *)data;
+
+    state->info->icc_fd = icc;
+    state->info->icc_size = icc_size;
+}
+
+static void image_description_info_handle_primaries(void *data,
+                                                    struct wp_image_description_info_v1 *wp_image_description_info_v1,
+                                                    int32_t r_x, int32_t r_y,
+                                                    int32_t g_x, int32_t g_y,
+                                                    int32_t b_x, int32_t b_y,
+                                                    int32_t w_x, int32_t w_y)
+{
+    // NOP
+}
+
+static void image_description_info_handle_primaries_named(void *data,
+                                                          struct wp_image_description_info_v1 *wp_image_description_info_v1,
+                                                          uint32_t primaries)
+{
+    // NOP
+}
+
+static void image_description_info_handle_tf_power(void *data,
+                                                   struct wp_image_description_info_v1 *wp_image_description_info_v1,
+                                                   uint32_t eexp)
+{
+    // NOP
+}
+
+static void image_description_info_handle_tf_named(void *data,
+                                                   struct wp_image_description_info_v1 *wp_image_description_info_v1,
+                                                   uint32_t tf)
+{
+    // NOP
+}
+
+static void image_description_info_handle_luminances(void *data,
+                                                     struct wp_image_description_info_v1 *wp_image_description_info_v1,
+                                                     uint32_t min_lum,
+                                                     uint32_t max_lum,
+                                                     uint32_t reference_lum)
+{
+    Wayland_ColorInfoState *state = (Wayland_ColorInfoState *)data;
+    state->info->HDR.HDR_headroom = (float)max_lum / (float)reference_lum;
+}
+
+static void image_description_info_handle_target_primaries(void *data,
+                                                           struct wp_image_description_info_v1 *wp_image_description_info_v1,
+                                                           int32_t r_x, int32_t r_y,
+                                                           int32_t g_x, int32_t g_y,
+                                                           int32_t b_x, int32_t b_y,
+                                                           int32_t w_x, int32_t w_y)
+{
+    // NOP
+}
+
+static void image_description_info_handle_target_luminance(void *data,
+                                                           struct wp_image_description_info_v1 *wp_image_description_info_v1,
+                                                           uint32_t min_lum,
+                                                           uint32_t max_lum)
+{
+    // NOP
+}
+
+static void image_description_info_handle_target_max_cll(void *data,
+                                                         struct wp_image_description_info_v1 *wp_image_description_info_v1,
+                                                         uint32_t max_cll)
+{
+    // NOP
+}
+
+static void image_description_info_handle_target_max_fall(void *data,
+                                                          struct wp_image_description_info_v1 *wp_image_description_info_v1,
+                                                          uint32_t max_fall)
+{
+    // NOP
+}
+
+static const struct wp_image_description_info_v1_listener image_description_info_listener = {
+    image_description_info_handle_done,
+    image_description_info_handle_icc_file,
+    image_description_info_handle_primaries,
+    image_description_info_handle_primaries_named,
+    image_description_info_handle_tf_power,
+    image_description_info_handle_tf_named,
+    image_description_info_handle_luminances,
+    image_description_info_handle_target_primaries,
+    image_description_info_handle_target_luminance,
+    image_description_info_handle_target_max_cll,
+    image_description_info_handle_target_max_fall
+};
+
+static void PumpColorspaceEvents(Wayland_ColorInfoState *state)
+{
+    SDL_VideoData *vid = SDL_GetVideoDevice()->internal;
+
+    // Run the image description sequence to completion in its own queue.
+    struct wl_event_queue *queue = WAYLAND_wl_display_create_queue(vid->display);
+    WAYLAND_wl_proxy_set_queue((struct wl_proxy *)state->wp_image_description, queue);
+
+    while (state->wp_image_description) {
+        WAYLAND_wl_display_dispatch_queue(vid->display, queue);
+    }
+
+    WAYLAND_wl_event_queue_destroy(queue);
+}
+
+static void image_description_handle_failed(void *data,
+                                            struct wp_image_description_v1 *wp_image_description_v1,
+                                            uint32_t cause,
+                                            const char *msg)
+{
+    Wayland_ColorInfoState *state = (Wayland_ColorInfoState *)data;
+
+    wp_image_description_v1_destroy(state->wp_image_description);
+    state->wp_image_description = NULL;
+}
+
+static void image_description_handle_ready(void *data,
+                                           struct wp_image_description_v1 *wp_image_description_v1,
+                                           uint32_t identity)
+{
+    Wayland_ColorInfoState *state = (Wayland_ColorInfoState *)data;
+
+    // This will inherit the queue of the factory image description object.
+    state->wp_image_description_info = wp_image_description_v1_get_information(state->wp_image_description);
+    wp_image_description_info_v1_add_listener(state->wp_image_description_info, &image_description_info_listener, data);
+}
+
+static const struct wp_image_description_v1_listener image_description_listener = {
+    image_description_handle_failed,
+    image_description_handle_ready
+};
+
+bool Wayland_GetColorInfoForWindow(SDL_WindowData *window_data, Wayland_ColorInfo *info)
+{
+    Wayland_ColorInfoState state;
+    SDL_zero(state);
+    state.info = info;
+
+    state.wp_image_description = wp_color_management_surface_feedback_v1_get_preferred(window_data->wp_color_management_surface_feedback);
+    wp_image_description_v1_add_listener(state.wp_image_description, &image_description_listener, &state);
+
+    PumpColorspaceEvents(&state);
+
+    return state.result;
+}
+
+bool Wayland_GetColorInfoForOutput(SDL_DisplayData *display_data, Wayland_ColorInfo *info)
+{
+    Wayland_ColorInfoState state;
+    SDL_zero(state);
+    state.info = info;
+
+    state.wp_image_description = wp_color_management_output_v1_get_image_description(display_data->wp_color_management_output);
+    wp_image_description_v1_add_listener(state.wp_image_description, &image_description_listener, &state);
+
+    PumpColorspaceEvents(&state);
+
+    return state.result;
+}
+
+#endif // SDL_VIDEO_DRIVER_WAYLAND
diff --git a/src/video/wayland/SDL_waylandcolor.h b/src/video/wayland/SDL_waylandcolor.h
new file mode 100644
index 0000000000000..ba6d7f00fa5d9
--- /dev/null
+++ b/src/video/wayland/SDL_waylandcolor.h
@@ -0,0 +1,41 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+
+#ifndef SDL_waylandcolor_h_
+#define SDL_waylandcolor_h_
+
+#include "../SDL_sysvideo.h"
+
+typedef struct Wayland_ColorInfo
+{
+    SDL_HDROutputProperties HDR;
+
+    // The ICC fd is only valid if the size is non-zero.
+    int icc_fd;
+    Uint32 icc_size;
+} Wayland_ColorInfo;
+
+extern bool Wayland_GetColorInfoForWindow(SDL_WindowData *window_data, Wayland_ColorInfo *info);
+extern bool Wayland_GetColorInfoForOutput(SDL_DisplayData *display_data, Wayland_ColorInfo *info);
+
+#endif // SDL_waylandcolor_h_
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index 2233ab25ec4fd..15fa01b877398 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -27,6 +27,7 @@
 #include "../../events/SDL_events_c.h"
 
 #include "SDL_waylandclipboard.h"
+#include "SDL_waylandcolor.h"
 #include "SDL_waylandevents_c.h"
 #include "SDL_waylandkeyboard.h"
 #include "SDL_waylandmessagebox.h"
@@ -63,6 +64,7 @@
 #include "xdg-output-unstable-v1-client-protocol.h"
 #include "xdg-shell-client-protocol.h"
 #include "xdg-toplevel-icon-v1-client-protocol.h"
+#include "color-management-v1-client-protocol.h"
 
 #ifdef HAVE_LIBDECOR_H
 #include <libdecor.h>
@@ -249,7 +251,7 @@ static int SDLCALL Wayland_DisplayPositionCompare(const void *a, const void *b)
  * The primary is determined by the following criteria, in order:
  * - Landscape is preferred over portrait
  * - The highest native resolution
- * - TODO: A higher HDR range is preferred
+ * - A higher HDR range is preferred
  * - Higher refresh is preferred (ignoring small differences)
  * - Lower scale values are preferred (larger display)
  */
@@ -271,6 +273,7 @@ static int Wayland_GetPrimaryDisplay(SDL_VideoData *vid)
     int best_width = 0;
     int best_height = 0;
     double best_scale = 0.0;
+    float best_headroom = 0.0f;
     int best_refresh = 0;
     bool best_is_landscape = false;
     int best_index = 0;
@@ -286,11 +289,15 @@ static int Wayland_GetPrimaryDisplay(SDL_VideoData *vid)
             if (d->pixel_width > best_width || d->pixel_height > best_height) {
                 have_new_best = true;
             } else if (d->pixel_width == best_width && d->pixel_height == best_height) {
-                if (d->refresh - best_refresh > REFRESH_DELTA) { // Favor a higher refresh rate, but ignore small differences (e.g. 59.97 vs 60.1)
-                    have_new_best = true;
-                } else if (d->scale_factor < best_scale && SDL_abs(d->refresh - best_refresh) <= REFRESH_DELTA) {
-                    // Prefer a lower scale display if the difference in refresh rate is small.
+                if (d->HDR.HDR_headroom > best_headroom) { // Favor a higher HDR luminance range
                     have_new_best = true;
+                } else if (d->HDR.HDR_headroom == best_headroom) {
+                    if (d->refresh - best_refresh > REFRESH_DELTA) { // Favor a higher refresh rate, but ignore small differences (e.g. 59.97 vs 60.1)
+                        have_new_best = true;
+                    } else if (d->scale_factor < best_scale && SDL_abs(d->refresh - best_refresh) <= REFRESH_DELTA) {
+                        // Prefer a lower scale display if the difference in refresh rate is small.
+                        have_new_best = true;
+                    }
                 }
             }
         }
@@ -299,6 +306,7 @@ static int Wayland_GetPrimaryDisplay(SDL_VideoData *vid)
             best_width = d->pixel_width;
             best_height = d->pixel_height;
             best_scale = d->scale_factor;
+            best_headroom = d->HDR.HDR_headroom;
             best_refresh = d->refresh;
             best_is_landscape = is_landscape;
             best_index = i;
@@ -630,6 +638,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols)
     device->SetWindowIcon = Wayland_SetWindowIcon;
     device->GetWindowSizeInPixels = Wayland_GetWindowSizeInPixels;
     device->GetWindowContentScale = Wayland_GetWindowContentScale;
+    device->GetWindowICCProfile = Wayland_GetWindowICCProfile;
     device->GetDisplayForWindow = Wayland_GetDisplayForWindow;
     device->DestroyWindow = Wayland_DestroyWindow;
     device->SetWindowHitTest = Wayland_SetWindowHitTest;
@@ -1050,6 +1059,8 @@ static void display_handle_done(void *data,
         AddEmulatedModes(internal, native_mode.w, native_mode.h);
     }
 
+    SDL_SetDisplayHDRProperties(dpy, &internal->HDR);
+
     if (internal->display == 0) {
         // First time getting display info, initialize the VideoDisplay
         if (internal->physical_width_mm >= internal->physical_height_mm) {
@@ -1107,6 +1118,27 @@ static const struct wl_output_listener output_listener = {
     display_handle_description // Version 4
 };
 
+static void Wayland_GetOutputColorInfo(SDL_DisplayData *display)
+{
+    Wayland_ColorInfo info;
+    SDL_zero(info);
+
+    if (Wayland_GetColorInfoForOutput(display, &info)) {
+        SDL_copyp(&display->HDR, &info.HDR);
+    }
+}
+
+static void handle_output_image_description_changed(void *data,
+                                                    struct wp_color_management_output_v1 *wp_color_management_output_v1)
+{
+    // wl_display.done is called after this event, so the display HDR status will be updated there.
+    Wayland_GetOutputColorInfo(data);
+}
+
+static const struct wp_color_management_output_v1_listener wp_color_management_output_listener = {
+    handle_output_image_description_changed
+};
+
 static bool Wayland_add_display(SDL_VideoData *d, uint32_t id, uint32_t version)
 {
     struct wl_output *output;
@@ -1136,6 +1168,11 @@ static bool Wayland_add_display(SDL_VideoData *d, uint32_t id, uint32_t version)
         data->xdg_output = zxdg_output_manager_v1_get_xdg_output(data->videodata->xdg_output_manager, output);
         zxdg_output_v1_add_listener(data->xdg_output, &xdg_output_listener, data);
     }
+    if (data->videodata->wp_color_manager_v1) {
+        data->wp_color_management_output = wp_color_manager_v1_get_output(data->videodata->wp_color_manager_v1, output);
+        wp_color_management_output_v1_add_listener(data->wp_color_management_output, &wp_color_management_output_listener, data);
+        Wayland_GetOutputColorInfo(data);
+    }
     return true;
 }
 
@@ -1153,6 +1190,10 @@ static void Wayland_free_display(SDL_VideoDisplay *display, bool send_event)
 
         SDL_free(display_data->wl_output_name);
 
+        if (display_data->wp_color_management_output) {
+            wp_color_management_output_v1_destroy(display_data->wp_color_management_output);
+        }
+
         if (display_data->xdg_output) {
             zxdg_output_v1_destroy(display_data->xdg_output);
         }
@@ -1187,6 +1228,16 @@ static void Wayland_init_xdg_output(SDL_VideoData *d)
     }
 }
 
+static void Wayland_InitColorManager(SDL_VideoData *d)
+{
+    for (int i = 0; i < d->output_count; ++i) {
+        SDL_DisplayData *disp = d->output_list[i];
+        disp->wp_color_management_output = wp_color_manager_v1_get_output(disp->videodata->wp_color_manager_v1, disp->output);
+        wp_color_management_output_v1_add_listener(disp->wp_color_management_output, &wp_color_management_output_listener, disp);
+        Wayland_GetOutputColorInfo(disp);
+    }
+}
+
 static void handle_ping_xdg_wm_base(void *data, struct xdg_wm_base *xdg, uint32_t serial)
 {
     xdg_wm_base_pong(xdg, serial);
@@ -1280,6 +1331,9 @@ static void display_handle_global(void *data, struct wl_registry *registry, uint
         d->xdg_toplevel_icon_manager_v1 = wl_registry_bind(d->registry, id, &xdg_toplevel_icon_manager_v1_interface, 1);
     } else if (SDL_strcmp(interface, "frog_color_management_factory_v1") == 0) {
         d->frog_color_management_factory_v1 = wl_registry_bind(d->registry, id, &frog_color_management_factory_v1_interface, 1);
+    } else if (SDL_strcmp(interface, "xx_color_manager_v4") == 0) {
+        d->wp_color_manager_v1 = wl_registry_bind(d->registry, id, &wp_color_manager_v1_interface, 1);
+        Wayland_InitColorManager(d);
     }
 }
 
@@ -1567,6 +1621,11 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this)
         data->frog_color_management_factory_v1 = NULL;
     }
 
+    if (data->wp_color_manager_v1) {
+        wp_color_manager_v1_destroy(data->wp_color_manager_v1);
+        data->wp_color_manager_v1 = 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 44f5f35980e43..5b5ee24c0fa2c 100644
--- a/src/video/wayland/SDL_waylandvideo.h
+++ b/src/video/wayland/SDL_waylandvideo.h
@@ -83,6 +83,7 @@ struct SDL_VideoData
     struct wp_alpha_modifier_v1 *wp_alpha_modifier_v1;
     struct xdg_toplevel_icon_manager_v1 *xdg_toplevel_icon_manager_v1;
     struct frog_color_management_factory_v1 *frog_color_management_factory_v1;
+    struct wp_color_manager_v1 *wp_color_manager_v1;
     struct zwp_tablet_manager_v2 *tablet_manager;
 
     struct xkb_context *xkb_context;
@@ -102,6 +103,7 @@ struct SDL_DisplayData
     SDL_VideoData *videodata;
     struct wl_output *output;
     struct zxdg_output_v1 *xdg_output;
+    struct wp_color_management_output_v1 *wp_color_management_output;
     char *wl_output_name;
     double scale_factor;
     uint32_t registry_id;
@@ -111,6 +113,8 @@ struct SDL_DisplayData
     SDL_DisplayOrientation orientation;
     int physical_width_mm, physical_height_mm;
     bool has_logical_position, has_logical_size;
+    bool running_colorspace_event_queue;
+    SDL_HDROutputProperties HDR;
     SDL_DisplayID display;
     SDL_VideoDisplay placeholder;
     int wl_output_done_count;
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index 50bed340f4b60..8dbc5e090a9d1 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -23,6 +23,8 @@
 
 #ifdef SDL_VIDEO_DRIVER_WAYLAND
 
+#include <sys/mman.h>
+
 #include "../SDL_sysvideo.h"
 #include "../../events/SDL_events_c.h"
 #include "../../core/unix/SDL_appid.h"
@@ -31,6 +33,7 @@
 #include "SDL_waylandwindow.h"
 #include "SDL_waylandvideo.h"
 #include "../../SDL_hints_c.h"
+#include "SDL_waylandcolor.h"
 
 #include "alpha-modifier-v1-client-protocol.h"
 #include "xdg-shell-client-protocol.h"
@@ -43,6 +46,7 @@
 #include "xdg-dialog-v1-client-protocol.h"
 #include "frog-color-management-v1-client-protocol.h"
 #include "xdg-toplevel-icon-v1-client-protocol.h"
+#include "color-management-v1-client-protocol.h"
 
 #ifdef HAVE_LIBDECOR_H
 #include <libdecor.h>
@@ -1647,6 +1651,28 @@ static const struct frog_color_managed_surface_listener frog_surface_listener =
     frog_preferred_metadata_handler
 };
 
+static void feedback_surface_preferred_changed(void *data,
+                                               struct wp_color_management_surface_feedback_v1 *wp_color_management_surface_feedback_v1,
+                                               uint32_t identity)
+{
+    SDL_WindowData *wind = (SDL_WindowData *)data;
+    Wayland_ColorInfo info;
+    SDL_zero(info);
+
+    if (Wayland_GetColorInfoForWindow(wind, &info)) {
+        SDL_SetWindowHDRProperties(wind->sdlwindow, &info.HDR, true);
+        if (info.icc_size) {
+            wind->icc_fd = info.icc_fd;
+            wind->icc_size = info.icc_size;
+            SDL_SendWindowEvent(wind->sdlwindow, SDL_EVENT_WINDOW_ICCPROF_CHANGED, 0, 0);
+        }
+    }
+}
+
+static const struct wp_color_management_surface_feedback_v1_listener color_management_surface_feedback_listener = {
+    feedback_surface_preferred_changed
+};
+
 static void SetKeyboardFocus(SDL_Window *window, bool set_focus)
 {
     SDL_Window *toplevel = window;
@@ -2593,7 +2619,10 @@ bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Proper
     }
 
     if (!custom_surface_role) {
-        if (c->frog_color_management_factory_v1) {
+        if (c->wp_color_manager_v1) {
+            data->wp_color_management_surface_feedback = wp_color_manager_v1_get_surface_feedback(c->wp_color_manager_v1, data->surface);
+            wp_color_management_surface_feedback_v1_add_listener(data->wp_color_management_surface_feedback, &color_management_surface_feedback_listener, data);
+        } else if (c->frog_color_management_factory_v1) {
             data->frog_color_managed_surface = frog_color_management_factory_v1_get_color_managed_surface(c->frog_color_management_factory_v1, data->surface);
             frog_color_managed_surface_add_listener(data->frog_color_managed_surface, &frog_surface_listener, data);
         }
@@ -2875,6 +2904,26 @@ bool Wayland_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surfa
     return true;
 }
 
+void *Wayland_GetWindowICCProfile(SDL_VideoDevice *_this, SDL_Window *window, size_t *size)
+{
+    SDL_WindowData *wind = window->internal;
+    void *ret = NULL;
+
+    if (wind->icc_size > 0) {
+        void *icc_map = mmap(NULL, wind->icc_size, PROT_READ, MAP_PRIVATE, wind->icc_fd, 0);
+        if (icc_map != MAP_FAILED) {
+            ret = SDL_malloc(wind->icc_size);
+            if (ret) {
+                *size = wind->icc_size;
+                SDL_memcpy(ret, icc_map, *size);
+            }
+            munmap(icc_map, wind->icc_size);
+        }
+    }
+
+    return ret;
+}
+
 bool Wayland_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window)
 {
     SDL_WindowData *wind = window->internal;
@@ -2994,6 +3043,10 @@ void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window)
             frog_color_managed_surface_destroy(wind->frog_color_managed_surface);
         }
 
+        if (wind->wp_color_management_surface_feedback) {
+            wp_color_management_surface_feedback_v1_destroy(wind->wp_color_management_surface_feedback);
+        }
+
         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 906c5e448dbe5..bd854226f1149 100644
--- a/src/video/wayland/SDL_waylandwindow.h
+++ b/src/video/wayland/SDL_waylandwindow.h
@@ -116,6 +116,7 @@ struct SDL_WindowData
     struct wp_alpha_modifier_surface_v1 *wp_alpha_modifier_surface_v1;
     struct xdg_toplevel_icon_v1 *xdg_toplevel_icon_v1;
     struct frog_color_managed_surface *frog_color_managed_surface;
+    struct wp_color_management_surface_feedback_v1 *wp_color_management_surface_feedback;
 
     SDL_AtomicInt swap_interval_ready;
 
@@ -184,6 +185,8 @@ struct SDL_WindowData
     int fullscreen_deadline_count;
     int maximized_restored_deadline_count;
     Uint64 last_focus_event_time_ns;
+    int icc_fd;
+    Uint32 icc_size;
     bool floating;
     bool suspended;
     bool resizing;
@@ -231,6 +234,7 @@ extern void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window);
 extern bool Wayland_SuspendScreenSaver(SDL_VideoDevice *_this);
 extern bool Wayland_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon);
 extern float Wayland_GetWindowContentScale(SDL_VideoDevice *_this, SDL_Window *window);
+extern void *Wayland_GetWindowICCProfile(SDL_VideoDevice *_this, SDL_Window *window, size_t *size);
 
 extern bool Wayland_SetWindowHitTest(SDL_Window *window, bool enabled);
 extern bool Wayland_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation);
diff --git a/wayland-protocols/color-management-v1.xml b/wayland-protocols/color-management-v1.xml
new file mode 100644
index 0000000000000..7f8da78f1238d
--- /dev/null
+++ b/wayland-protocols/color-management-v1.xml
@@ -0,0 +1,1631 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="color_management_v1">
+  <copyright>
+    Copyright 2019 Sebastian Wick
+    Copyright 2019 Erwin Burema
+    Copyright 2020 AMD
+    Copyright 2020-2024 Collabora, Ltd.
+    Copyright 2024 Xaver Hugl
+    Copyright 2022-2025 Red Hat, Inc.
+
+    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>
+
+  <description summary="color management protocol">
+    The aim of the color management extension is to allow clients to know
+    the color properties of outputs, and to tell the compositor about the color
+    properties of their content on surfaces. Doing this enables a compositor
+    to perform automatic color management of content for different outputs
+    according to how content is intended to look like.
+
+    The color properties are represented as an image description object which
+    is immutable after it has been created. A wl_output always has an
+    associated image description that clients can observe. A wl_surface
+    always has an associated preferred image description as a hint chosen by
+    the compositor that clients can also observe. Clients can set an image
+    description on a wl_surface to denote the color characteristics of the
+    surface contents.
+
+    An image description includes SDR and HDR colorimetry and encoding, HDR
+    metadata, and viewing environment parameters. An image description does
+    not include the properties set through color-representation extension.
+    It is expected that the color-representation extension is used in
+    conjunction with the color management extension when necessary,
+    particularly with the YUV family of pixel formats.
+
+    Recommendation ITU-T H.273
+    "Coding-independent code points for video signal type identification"
+    shall be referred to as simply H.273 here.
+
+    The color-and-hdr repository
+    (https://gitlab.freedesktop.org/pq/color-and-hdr) contains
+    background information on the protocol design and legacy color management.
+    It also contains a glossary, learning resources for digital color, tools,
+    samples and more.
+
+    The terminology used in this protocol is based on common color science and
+    color encoding terminology where possible. The glossary in the color-and-hdr
+    repository shall be the authority on the definition of terms in this
+    protocol.
+
+    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>
+
+  <interface name="wp_color_manager_v1" version="1">
+    <description summary="color manager singleton">
+      A singleton global interface used for getting color management extensions
+      for wl_surface and wl_output objects, and for creating client defined
+      image description objects. The extension interfaces allow
+      getting the image description of outputs and setting the image
+      description of surfaces.
+
+      Compositors should never remove this global.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description sum

(Patch may be truncated, please check the link at the top of this post.)