From 203d9a7a950d3cae69938b37086df45dd7af8446 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= <thomas94@gmx.net>
Date: Sun, 25 Jul 2021 15:58:46 +0200
Subject: [PATCH 02/40] Expose opened files via NS api

---
 include/GLFW/glfw3native.h | 14 ++++++++++++++
 src/cocoa_init.m           | 32 ++++++++++++++++++++++++++++++++
 src/cocoa_platform.h       |  1 +
 3 files changed, 47 insertions(+)

diff --git a/include/GLFW/glfw3native.h b/include/GLFW/glfw3native.h
index 011b239c8c..afd0c1989c 100644
--- a/include/GLFW/glfw3native.h
+++ b/include/GLFW/glfw3native.h
@@ -303,6 +303,20 @@ GLFWAPI id glfwGetCocoaWindow(GLFWwindow* window);
  *  @ingroup native
  */
 GLFWAPI id glfwGetCocoaView(GLFWwindow* window);
+
+/*! @brief Returns the list of filenames that opened the application,
+*  such as by dragging files to the app bundle or through file associations.
+*
+*  @return A list of strings, null terminated.
+*
+*  @thread_safety This function may be called from any thread.  Access is not
+*  synchronized.
+*
+*  @since Added in version 3.4.
+*
+*  @ingroup native
+*/
+const char* const* glfwGetOpenedFilenames(void);
 #endif
 
 #if defined(GLFW_EXPOSE_NATIVE_NSGL)
diff --git a/src/cocoa_init.m b/src/cocoa_init.m
index 15dc4ec4c1..802dda8179 100644
--- a/src/cocoa_init.m
+++ b/src/cocoa_init.m
@@ -448,6 +448,20 @@ - (void)applicationDidHide:(NSNotification *)notification
         _glfwRestoreVideoModeCocoa(_glfw.monitors[i]);
 }
 
+- (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)filenames
+{
+    int len = [filenames count];
+    // Last entry is nil
+    _glfw.ns.openedFilenames = _glfw_calloc(len + 1, sizeof(char*));
+
+    for (int i = 0; i < len; i++)
+    {
+        NSString* filename = [filenames objectAtIndex:i];
+        const char* filenameStr = [filename UTF8String];
+        _glfw.ns.openedFilenames[i] = _glfw_strdup(filenameStr);
+    }
+}
+
 @end // GLFWApplicationDelegate
 
 
@@ -682,6 +696,16 @@ void _glfwTerminateCocoa(void)
     if (_glfw.ns.keyUpMonitor)
         [NSEvent removeMonitor:_glfw.ns.keyUpMonitor];
 
+    if (_glfw.ns.openedFilenames)
+    {
+        for (char** p = _glfw.ns.openedFilenames; *p; p++)
+        {
+            _glfw_free(*p);
+        }
+        _glfw_free(_glfw.ns.openedFilenames);
+        _glfw.ns.openedFilenames = nil;
+    }
+
     _glfw_free(_glfw.ns.clipboardString);
 
     _glfwTerminateNSGL();
@@ -693,3 +717,11 @@ void _glfwTerminateCocoa(void)
 
 #endif // _GLFW_COCOA
 
+//////////////////////////////////////////////////////////////////////////
+//////                        GLFW native API                       //////
+//////////////////////////////////////////////////////////////////////////
+
+const char* const* glfwGetOpenedFilenames(void)
+{
+    return (const char* const*) _glfw.ns.openedFilenames;
+}
diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h
index 4d1d66ae03..5b75564b1e 100644
--- a/src/cocoa_platform.h
+++ b/src/cocoa_platform.h
@@ -181,6 +181,7 @@ typedef struct _GLFWlibraryNS
     double              restoreCursorPosX, restoreCursorPosY;
     // The window whose disabled cursor mode is active
     _GLFWwindow*        disabledCursorWindow;
+    char**              openedFilenames;
 
     struct {
         CFBundleRef     bundle;

From 343dbe373efdfe2c950a31c6f652dda94af26c5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= <thomas94@gmx.net>
Date: Tue, 28 Dec 2021 13:05:30 +0100
Subject: [PATCH 03/40] Expose additional opened files by the NS Api

---
 include/GLFW/glfw3native.h |  3 +++
 src/cocoa_init.m           | 10 ++++++++++
 src/cocoa_platform.h       |  3 +++
 3 files changed, 16 insertions(+)

diff --git a/include/GLFW/glfw3native.h b/include/GLFW/glfw3native.h
index afd0c1989c..93421683c2 100644
--- a/include/GLFW/glfw3native.h
+++ b/include/GLFW/glfw3native.h
@@ -253,6 +253,8 @@ GLFWAPI HGLRC glfwGetWGLContext(GLFWwindow* window);
 #endif
 
 #if defined(GLFW_EXPOSE_NATIVE_COCOA)
+typedef void (*GLFWopenedFilenamesFun)(const char*);
+
 /*! @brief Returns the `CGDirectDisplayID` of the specified monitor.
  *
  *  @return The `CGDirectDisplayID` of the specified monitor, or
@@ -317,6 +319,7 @@ GLFWAPI id glfwGetCocoaView(GLFWwindow* window);
 *  @ingroup native
 */
 const char* const* glfwGetOpenedFilenames(void);
+void glfwSetOpenedFilenamesCallback(GLFWopenedFilenamesFun callback);
 #endif
 
 #if defined(GLFW_EXPOSE_NATIVE_NSGL)
diff --git a/src/cocoa_init.m b/src/cocoa_init.m
index 802dda8179..9f7bd285cf 100644
--- a/src/cocoa_init.m
+++ b/src/cocoa_init.m
@@ -459,6 +459,9 @@ - (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)fil
         NSString* filename = [filenames objectAtIndex:i];
         const char* filenameStr = [filename UTF8String];
         _glfw.ns.openedFilenames[i] = _glfw_strdup(filenameStr);
+        if (_glfw.ns.openedFilenamesCallback) {
+            _glfw.ns.openedFilenamesCallback(_glfw.ns.openedFilenames[i]);
+        }
     }
 }
 
@@ -589,6 +592,8 @@ int _glfwInitCocoa(void)
 {
     @autoreleasepool {
 
+    _glfw.ns.openedFilenamesCallback = NULL;
+
     _glfw.ns.helper = [[GLFWHelper alloc] init];
 
     [NSThread detachNewThreadSelector:@selector(doNothing:)
@@ -725,3 +730,8 @@ void _glfwTerminateCocoa(void)
 {
     return (const char* const*) _glfw.ns.openedFilenames;
 }
+
+void glfwSetOpenedFilenamesCallback(GLFWopenedFilenamesFun callback)
+{
+    _glfw.ns.openedFilenamesCallback = callback;
+}
diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h
index 5b75564b1e..236af78fbd 100644
--- a/src/cocoa_platform.h
+++ b/src/cocoa_platform.h
@@ -158,6 +158,8 @@ typedef struct _GLFWwindowNS
     double          cursorWarpDeltaX, cursorWarpDeltaY;
 } _GLFWwindowNS;
 
+typedef void (*GLFWopenedFilenamesFun)(const char*);
+
 // Cocoa-specific global data
 //
 typedef struct _GLFWlibraryNS
@@ -182,6 +184,7 @@ typedef struct _GLFWlibraryNS
     // The window whose disabled cursor mode is active
     _GLFWwindow*        disabledCursorWindow;
     char**              openedFilenames;
+    GLFWopenedFilenamesFun openedFilenamesCallback;
 
     struct {
         CFBundleRef     bundle;

From 41c333ff2fa3621808361678d1cae874eb9aee7c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Tue, 15 Jul 2025 15:15:48 +0200
Subject: [PATCH 04/40] Support float buffers (in analogy to 87f11f8)

---
 docs/window.md       | 7 +++++++
 include/GLFW/glfw3.h | 6 ++++++
 src/context.c        | 6 ++++++
 src/glx_context.c    | 5 ++++-
 src/internal.h       | 1 +
 src/wgl_context.c    | 4 +++-
 src/win32_platform.h | 1 +
 src/window.c         | 5 +++++
 src/x11_platform.h   | 2 ++
 9 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/docs/window.md b/docs/window.md
index 371baa5620..46633d525a 100644
--- a/docs/window.md
+++ b/docs/window.md
@@ -168,6 +168,7 @@ resulting context and framebuffer may differ from what these hints requested.
 The following hints are always hard constraints:
 - @ref GLFW_STEREO
 - @ref GLFW_DOUBLEBUFFER
+- @ref GLFW_FLOATBUFFER
 - [GLFW_CLIENT_API](@ref GLFW_CLIENT_API_hint)
 - [GLFW_CONTEXT_CREATION_API](@ref GLFW_CONTEXT_CREATION_API_hint)
 
@@ -336,6 +337,12 @@ __GLFW_DOUBLEBUFFER__ specifies whether the framebuffer should be double
 buffered.  You nearly always want to use double buffering.  This is a hard
 constraint.  Possible values are `GLFW_TRUE` and `GLFW_FALSE`.
 
+@anchor GLFW_FLOATBUFFER
+__GLFW_FLOATBUFFER__ specifies whether a floating point framebuffer should
+be used. This is needed for high dynamic range and large-gamut rendering (e.g.
+with an extendes sRGB gamut), where pixel values are allowed to be negative.
+This is a hard constraint. Possible values are `GLFW_TRUE` and `GLFW_FALSE`.
+The default is `GLFW_FALSE`.
 
 #### Monitor related hints {#window_hints_mtr}
 
diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h
index 79b0628849..3f5df73da4 100644
--- a/include/GLFW/glfw3.h
+++ b/include/GLFW/glfw3.h
@@ -1023,6 +1023,12 @@ extern "C" {
  */
 #define GLFW_DOUBLEBUFFER           0x00021010
 
+/*! @brief Floating point framebuffer hint.
+ *
+ *  Floating point framebuffer [hint](@ref GLFW_FLOATBUFFER).
+ */
+#define GLFW_FLOATBUFFER           0x00021011
+
 /*! @brief Context client API hint and attribute.
  *
  *  Context client API [hint](@ref GLFW_CLIENT_API_hint) and
diff --git a/src/context.c b/src/context.c
index f4fa63ad7f..7a88f2d2cb 100644
--- a/src/context.c
+++ b/src/context.c
@@ -201,6 +201,12 @@ const _GLFWfbconfig* _glfwChooseFBConfig(const _GLFWfbconfig* desired,
             continue;
         }
 
+        if (desired->floatbuffer != current->floatbuffer)
+        {
+            // Floating point buffer is a hard constraint
+            continue;
+        }
+
         // Count number of missing buffers
         {
             missing = 0;
diff --git a/src/glx_context.c b/src/glx_context.c
index a2464a9d69..993ee83d75 100644
--- a/src/glx_context.c
+++ b/src/glx_context.c
@@ -82,7 +82,8 @@ static GLFWbool chooseGLXFBConfig(const _GLFWfbconfig* desired,
         _GLFWfbconfig* u = usableConfigs + usableCount;
 
         // Only consider RGBA GLXFBConfigs
-        if (!(getGLXFBConfigAttrib(n, GLX_RENDER_TYPE) & GLX_RGBA_BIT))
+        if (!(getGLXFBConfigAttrib(n, GLX_RENDER_TYPE) & GLX_RGBA_BIT) &&
+            !(getGLXFBConfigAttrib(n, GLX_RENDER_TYPE) & GLX_RGBA_FLOAT_BIT))
             continue;
 
         // Only consider window GLXFBConfigs
@@ -123,6 +124,8 @@ static GLFWbool chooseGLXFBConfig(const _GLFWfbconfig* desired,
         if (getGLXFBConfigAttrib(n, GLX_STEREO))
             u->stereo = GLFW_TRUE;
 
+        u->floatbuffer = (getGLXFBConfigAttrib(n, GLX_RENDER_TYPE) & GLX_RGBA_FLOAT_BIT) != 0;
+
         if (_glfw.glx.ARB_multisample)
             u->samples = getGLXFBConfigAttrib(n, GLX_SAMPLES);
 
diff --git a/src/internal.h b/src/internal.h
index 4f097aa82b..01322381c3 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -480,6 +480,7 @@ struct _GLFWfbconfig
     int         samples;
     GLFWbool    sRGB;
     GLFWbool    doublebuffer;
+    GLFWbool    floatbuffer;
     GLFWbool    transparent;
     uintptr_t   handle;
 };
diff --git a/src/wgl_context.c b/src/wgl_context.c
index 3c7d71c236..4b477f3995 100644
--- a/src/wgl_context.c
+++ b/src/wgl_context.c
@@ -163,7 +163,8 @@ static int choosePixelFormatWGL(_GLFWwindow* window,
                 continue;
             }
 
-            if (FIND_ATTRIB_VALUE(WGL_PIXEL_TYPE_ARB) != WGL_TYPE_RGBA_ARB)
+            if (FIND_ATTRIB_VALUE(WGL_PIXEL_TYPE_ARB) != WGL_TYPE_RGBA_ARB &&
+                FIND_ATTRIB_VALUE(WGL_PIXEL_TYPE_ARB) != WGL_TYPE_RGBA_FLOAT_ARB)
                 continue;
 
             if (FIND_ATTRIB_VALUE(WGL_ACCELERATION_ARB) == WGL_NO_ACCELERATION_ARB)
@@ -186,6 +187,7 @@ static int choosePixelFormatWGL(_GLFWwindow* window,
             u->accumAlphaBits = FIND_ATTRIB_VALUE(WGL_ACCUM_ALPHA_BITS_ARB);
 
             u->auxBuffers = FIND_ATTRIB_VALUE(WGL_AUX_BUFFERS_ARB);
+            u->floatbuffer = FIND_ATTRIB_VALUE(WGL_PIXEL_TYPE_ARB) == WGL_TYPE_RGBA_FLOAT_ARB;
 
             if (FIND_ATTRIB_VALUE(WGL_STEREO_ARB))
                 u->stereo = GLFW_TRUE;
diff --git a/src/win32_platform.h b/src/win32_platform.h
index 49cceba6d4..7b3ecaff0c 100644
--- a/src/win32_platform.h
+++ b/src/win32_platform.h
@@ -170,6 +170,7 @@ typedef enum
 #define WGL_DRAW_TO_WINDOW_ARB 0x2001
 #define WGL_PIXEL_TYPE_ARB 0x2013
 #define WGL_TYPE_RGBA_ARB 0x202b
+#define WGL_TYPE_RGBA_FLOAT_ARB 0x21a0
 #define WGL_ACCELERATION_ARB 0x2003
 #define WGL_NO_ACCELERATION_ARB 0x2025
 #define WGL_RED_BITS_ARB 0x2015
diff --git a/src/window.c b/src/window.c
index e03121a46d..d3b2869d69 100644
--- a/src/window.c
+++ b/src/window.c
@@ -26,6 +26,7 @@
 //
 //========================================================================
 
+#include "GLFW/glfw3.h"
 #include "internal.h"
 
 #include <assert.h>
@@ -287,6 +288,7 @@ void glfwDefaultWindowHints(void)
     _glfw.hints.framebuffer.depthBits    = 24;
     _glfw.hints.framebuffer.stencilBits  = 8;
     _glfw.hints.framebuffer.doublebuffer = GLFW_TRUE;
+    _glfw.hints.framebuffer.floatbuffer  = GLFW_FALSE;
 
     // The default is to select the highest available refresh rate
     _glfw.hints.refreshRate = GLFW_DONT_CARE;
@@ -337,6 +339,9 @@ GLFWAPI void glfwWindowHint(int hint, int value)
         case GLFW_DOUBLEBUFFER:
             _glfw.hints.framebuffer.doublebuffer = value ? GLFW_TRUE : GLFW_FALSE;
             return;
+        case GLFW_FLOATBUFFER:
+            _glfw.hints.framebuffer.floatbuffer = value ? GLFW_TRUE : GLFW_FALSE;
+            return;
         case GLFW_TRANSPARENT_FRAMEBUFFER:
             _glfw.hints.framebuffer.transparent = value ? GLFW_TRUE : GLFW_FALSE;
             return;
diff --git a/src/x11_platform.h b/src/x11_platform.h
index 30326c5be0..cb34acb4d7 100644
--- a/src/x11_platform.h
+++ b/src/x11_platform.h
@@ -52,10 +52,12 @@
 
 #define GLX_VENDOR 1
 #define GLX_RGBA_BIT 0x00000001
+#define GLX_RGBA_FLOAT_BIT 0x00000004
 #define GLX_WINDOW_BIT 0x00000001
 #define GLX_DRAWABLE_TYPE 0x8010
 #define GLX_RENDER_TYPE 0x8011
 #define GLX_RGBA_TYPE 0x8014
+#define GLX_RGBA_FLOAT_TYPE 0x20b9
 #define GLX_DOUBLEBUFFER 5
 #define GLX_STEREO 6
 #define GLX_AUX_BUFFERS 7

From f443d9072831a060178b9e743165bcfc16f85281 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Tue, 15 Jul 2025 18:36:42 +0200
Subject: [PATCH 05/40] feat: basic wayland & Windows color management support

API was partially inspired and some code taken from
https://github.com/glfw/glfw/compare/master...IMS212:glfw:hdrWindows
---
 deps/wayland/color-management-v1.xml | 1638 ++++++++++++++++++++++++++
 include/GLFW/glfw3.h                 |   49 +
 src/CMakeLists.txt                   |    1 +
 src/cocoa_init.m                     |    1 +
 src/cocoa_platform.h                 |    1 +
 src/cocoa_window.m                   |    5 +
 src/egl_context.c                    |    5 +
 src/internal.h                       |    2 +
 src/null_init.c                      |    1 +
 src/null_platform.h                  |    1 +
 src/null_window.c                    |    6 +
 src/win32_platform.h                 |    1 +
 src/win32_window.c                   |  119 ++
 src/window.c                         |   10 +
 src/wl_init.c                        |  101 ++
 src/wl_platform.h                    |   21 +
 src/wl_window.c                      |  321 +++++
 src/x11_platform.h                   |    1 +
 src/x11_window.c                     |    5 +
 19 files changed, 2289 insertions(+)
 create mode 100644 deps/wayland/color-management-v1.xml

diff --git a/deps/wayland/color-management-v1.xml b/deps/wayland/color-management-v1.xml
new file mode 100644
index 0000000000..8d2a219a22
--- /dev/null
+++ b/deps/wayland/color-management-v1.xml
@@ -0,0 +1,1638 @@
+<?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 summary="destroy the color manager">
+        Destroy the wp_color_manager_v1 object. This does not affect any other
+        objects in any way.
+      </description>
+    </request>
+
+    <enum name="error">
+      <entry name="unsupported_feature" value="0"
+             summary="request not supported"/>
+      <entry name="surface_exists" value="1"
+             summary="color management surface exists already"/>
+    </enum>
+
+    <enum name="render_intent">
+      <description summary="rendering intents">
+        See the ICC.1:2022 specification from the International Color Consortium
+        for more details about rendering intents.
+
+        The principles of ICC defined rendering intents apply with all types of
+        image descriptions, not only those with ICC file profiles.
+
+        Compositors must support the perceptual rendering intent. Other
+        rendering intents are optional.
+      </description>
+
+      <entry name="perceptual" value="0"
+             summary="perceptual"/>
+      <entry name="relative" value="1"
+             summary="media-relative colorimetric"/>
+      <entry name="saturation" value="2"
+             summary="saturation"/>
+      <entry name="absolute" value="3"
+             summary="ICC-absolute colorimetric"/>
+      <entry name="relative_bpc" value="4"
+             summary="media-relative colorimetric + black point compensation"/>
+    </enum>
+
+    <enum name="feature">
+      <description summary="compositor supported features"/>
+
+      <entry name="icc_v2_v4" value="0"
+             summary="create_icc_creator request"/>
+      <entry name="parametric" value="1"
+             summary="create_parametric_creator request"/>
+      <entry name="set_primaries" value="2"
+             summary="parametric set_primaries request"/>
+      <entry name="set_tf_power" value="3"
+             summary="parametric set_tf_power request"/>
+      <entry name="set_luminances" value="4"
+             summary="parametric set_luminances request"/>
+      <entry name="set_mastering_display_primaries" value="5">
+        <description summary="parametric set_mastering_display_primaries request">
+          The compositor supports set_mastering_display_primaries request with a
+          target color volume fully contained inside the primary color volume.
+        </description>
+      </entry>
+      <entry name="extended_target_volume" value="6">
+        <description summary="parametric target exceeds primary color volume">
+          The compositor additionally supports target color volumes that
+          extend outside of the primary color volume.
+
+          This can only be advertised if feature set_mastering_display_primaries
+          is supported as well.
+        </description>
+      </entry>
+      <entry name="windows_scrgb" value="7"
+             summary="create_windows_scrgb request"/>
+    </enum>
+
+    <enum name="primaries">
+      <description summary="named color primaries">
+        Named color primaries used to encode well-known sets of primaries. H.273
+        is the authority, when it comes to the exact values of primaries and
+        authoritative specifications, where an equivalent code point exists.
+
+        A value of 0 is invalid and will never be present in the list of enums.
+
+        Descriptions do list the specifications for convenience.
+      </description>
+
+      <entry name="srgb" value="1">
+        <description summary="Color primaries for the sRGB color space as defined by the BT.709 standard">
+          Color primaries as defined by
+          - Rec. ITU-R BT.709-6
+          - Rec. ITU-R BT.1361-0 conventional colour gamut system and extended
+            colour gamut system (historical)
+          - IEC 61966-2-1 sRGB or sYCC
+          - IEC 61966-2-4
+          - Society of Motion Picture and Television Engineers (SMPTE) RP 177
+            (1993) Annex B
+          Equivalent to H.273 ColourPrimaries code point 1.
+        </description>
+      </entry>
+      <entry name="pal_m" value="2">
+        <description summary="Color primaries for PAL-M as defined by the BT.470 standard">
+          Color primaries as defined by
+          - Rec. ITU-R BT.470-6 System M (historical)
+          - United States National Television System Committee 1953
+            Recommendation for transmission standards for color television
+          - United States Federal Communications Commission (2003) Title 47 Code
+            of Federal Regulations 73.682 (a)(20)
+          Equivalent to H.273 ColourPrimaries code point 4.
+        </description>
+      </entry>
+      <entry name="pal" value="3">
+        <description summary="Color primaries for PAL as defined by the BT.601 standard">
+          Color primaries as defined by
+          - Rec. ITU-R BT.470-6 System B, G (historical)
+          - Rec. ITU-R BT.601-7 625
+          - Rec. ITU-R BT.1358-0 625 (historical)
+          - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
+          Equivalent to H.273 ColourPrimaries code point 5.
+        </description>
+      </entry>
+      <entry name="ntsc" value="4">
+        <description summary="Color primaries for NTSC as defined by the BT.601 standard">
+          Color primaries as defined by
+          - Rec. ITU-R BT.601-7 525
+          - Rec. ITU-R BT.1358-1 525 or 625 (historical)
+          - Rec. ITU-R BT.1700-0 NTSC
+          - SMPTE 170M (2004)
+          - SMPTE 240M (1999) (historical)
+          Equivalent to H.273 ColourPrimaries code point 6 and 7.
+        </description>
+      </entry>
+      <entry name="generic_film" value="5">
+        <description summary="Generic film with colour filters using Illuminant C">
+          Color primaries as defined by H.273 for generic film.
+          Equivalent to H.273 ColourPrimaries code point 8.
+        </description>
+      </entry>
+      <entry name="bt2020" value="6">
+        <description summary="Color primaries as defined by the BT.2020 and BT.2100 standard">
+          Color primaries as defined by
+          - Rec. ITU-R BT.2020-2
+          - Rec. ITU-R BT.2100-0
+          Equivalent to H.273 ColourPrimaries code point 9.
+        </description>
+      </entry>
+      <entry name="cie1931_xyz" value="7">
+        <description summary="Color primaries of the full CIE 1931 XYZ color space">
+          Color primaries as defined as the maximum of the CIE 1931 XYZ color
+          space by
+          - SMPTE ST 428-1
+          - (CIE 1931 XYZ as in ISO 11664-1)
+          Equivalent to H.273 ColourPrimaries code point 10.
+        </description>
+      </entry>
+      <entry name="dci_p3" value="8">
+        <description summary="Color primaries of the DCI P3 color space as defined by the SMPTE RP 431 standard">
+          Color primaries as defined by Digital Cinema System and published in
+          SMPTE RP 431-2 (2011). Equivalent to H.273 ColourPrimaries code point
+          11.
+        </description>
+      </entry>
+      <entry name="display_p3" value="9">
+        <description summary="Color primaries of Display P3 variant of the DCI-P3 color space as defined by the SMPTE EG 432 standard">
+          Color primaries as defined by Digital Cinema System and published in
+          SMPTE EG 432-1 (2010).
+          Equivalent to H.273 ColourPrimaries code point 12.
+        </description>
+      </entry>
+      <entry name="adobe_rgb" value="10">
+        <description summary="Color primaries of the Adobe RGB color space as defined by the ISO 12640 standard">
+          Color primaries as defined by Adobe as "Adobe RGB" and later published
+          by ISO 12640-4 (2011).
+        </description>
+      </entry>
+    </enum>
+
+    <enum name="transfer_function">
+      <description summary="named transfer functions">
+        Named transfer functions used to represent well-known transfer
+        characteristics. H.273 is the authority, when it comes to the exact
+        formulas and authoritative specifications, where an equivalent code
+        point exists.
+
+        A value of 0 is invalid and will never be present in the list of enums.
+
+        Descriptions do list the specifications for convenience.
+      </description>
+
+      <entry name="bt1886" value="1">
+        <description summary="BT.1886 display transfer characteristic">
+          Rec. ITU-R BT.1886 is the display transfer characteristic assumed by
+          - Rec. ITU-R BT.601-7 525 and 625
+          - Rec. ITU-R BT.709-6
+          - Rec. ITU-R BT.2020-2
+          These recommendations are referred to by H.273 TransferCharacteristics
+          code points 1, 6, 14, and 15, which are all equivalent.
+
+          This TF implies these default luminances from Rec. ITU-R BT.2035:
+          - primary color volume minimum: 0.01 cd/m²
+          - primary color volume maximum: 100 cd/m²
+          - reference white: 100 cd/m²
+        </description>
+      </entry>
+      <entry name="gamma22" value="2">
+        <description summary="Assumed display gamma 2.2 transfer function">
+          Transfer characteristics as defined by
+          - Rec. ITU-R BT.470-6 System M (historical)
+          - United States National Television System Committee 1953
+            Recommendation for transmission standards for color television
+          - United States Federal Communications Commission (2003) Title 47 Code
+            of Federal Regulations 73.682 (a) (20)
+          - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
+          Equivalent to H.273 TransferCharacteristics code point 4.
+
+          Note: an sRGB display (IEC 61966-2-1) uses this transfer function.
+        </description>
+      </entry>
+      <entry name="gamma28" value="3">
+        <description summary="Assumed display gamma 2.8 transfer function">
+          Transfer characteristics as defined by
+          - Rec. ITU-R BT.470-6 System B, G (historical)
+          Equivalent to H.273 TransferCharacteristics code point 5.
+        </description>
+      </entry>
+      <entry name="st240" value="4">
+        <description summary="SMPTE ST 240 transfer function">
+          Transfer characteristics as defined by
+          - SMPTE ST 240 (1999)
+          Equivalent to H.273 TransferCharacteristics code point 7.
+        </description>
+      </entry>
+      <entry name="ext_linear" value="5">
+        <description summary="extended linear transfer function">
+          Linear transfer function defined over all real numbers.
+          Normalised electrical values are equal the normalised optical values.
+
+          The differences to H.273 TransferCharacteristics code point 8 are
+          the definition over all real numbers.
+        </description>
+      </entry>
+      <entry name="log_100" value="6">
+        <description summary="logarithmic 100:1 transfer function">
+          Logarithmic transfer characteristic (100:1 range).
+          Equivalent to H.273 TransferCharacteristics code point 9.
+        </description>
+      </entry>
+      <entry name="log_316" value="7">
+        <description summary="logarithmic (100*Sqrt(10) : 1) transfer function">
+          Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range).
+          Equivalent to H.273 TransferCharacteristics code point 10.
+        </description>
+      </entry>
+      <entry name="xvycc" value="8">
+        <description summary="IEC 61966-2-4 transfer function">
+          Transfer characteristics as defined by
+          - IEC 61966-2-4
+          Equivalent to H.273 TransferCharacteristics code point 11.
+        </description>
+      </entry>
+      <entry name="srgb" value="9">
+        <description summary="sRGB piece-wise transfer function">
+          Transfer characteristics as defined by
+          - IEC 61966-2-1 sRGB
+          Equivalent to H.273 TransferCharacteristics code point 13 with
+          MatrixCoefficients set to 0.
+
+          Note: This is not appropriate for describing sRGB material.
+          sRGB material is intended to be viewed on an sRGB display, and
+          that is described by gamma22.
+        </description>
+      </entry>
+      <entry name="ext_srgb" value="10">
+        <description summary="Extended sRGB piece-wise transfer function">
+          Transfer characteristics as defined by
+          - IEC 61966-2-1 sYCC
+          Equivalent to H.273 TransferCharacteristics code point 13 with
+          MatrixCoefficients set to anything but 0.
+        </description>
+      </entry>
+      <entry name="st2084_pq" value="11">
+        <description summary="perceptual quantizer transfer function">
+          Transfer characteristics as defined by
+          - SMPTE ST 2084 (2014) for 10-, 12-, 14- and 16-bit systems
+          - Rec. ITU-R BT.2100-2 perceptual quantization (PQ) system
+          Equivalent to H.273 TransferCharacteristics code point 16.
+
+          This TF implies these default luminances
+          - primary color volume minimum: 0.005 cd/m²
+          - primary color volume maximum: 10000 cd/m²
+          - reference white: 203 cd/m²
+
+          The difference between the primary color volume minimum and maximum
+          must be approximately 10000 cd/m² as that is the swing of the EOTF
+          defined by ST 2084 and BT.2100. The default value for the
+          reference white is a protocol addition: it is suggested by
+          Report ITU-R BT.2408-7 and is not part of ST 2084 or BT.2100.
+        </description>
+      </entry>
+      <entry name="st428" value="12">
+        <description summary="SMPTE ST 428 transfer function">
+          Transfer characteristics as defined by
+          - SMPTE ST 428-1 (2019)
+          Equivalent to H.273 TransferCharacteristics code point 17.
+        </description>
+      </entry>
+      <entry name="hlg" value="13">
+        <description summary="hybrid log-gamma transfer function">
+          Transfer characteristics as defined by
+          - ARIB STD-B67 (2015)
+          - Rec. ITU-R BT.2100-2 hybrid log-gamma (HLG) system
+          Equivalent to H.273 TransferCharacteristics code point 18.
+
+          This TF implies these default luminances
+          - primary color volume minimum: 0.005 cd/m²
+          - primary color volume maximum: 1000 cd/m²
+          - reference white: 203 cd/m²
+
+          HLG is a relative display-referred signal with a specified
+          non-linear mapping to the display peak luminance (the HLG OOTF).
+          All absolute luminance values used here for HLG assume a 1000 cd/m²
+          peak display.
+
+          The default value for the reference white is a protocol addition:
+          it is suggested by Report ITU-R BT.2408-7 and is not part of
+          ARIB STD-B67 or BT.2100.
+        </description>
+      </entry>
+    </enum>
+
+    <request name="get_output">
+      <description summary="create a color management interface for a wl_output">
+        This creates a new wp_color_management_output_v1 object for the
+        given wl_output.
+
+        See the wp_color_management_output_v1 interface for more details.
+      </description>
+
+      <arg name="id" type="new_id" interface="wp_color_management_output_v1"/>
+      <arg name="output" type="object" interface="wl_output"/>
+    </request>
+
+    <request name="get_surface">
+      <description summary="create a color management interface for a wl_surface">
+        If a wp_color_management_surface_v1 object already exists for the given
+        wl_surface, the protocol error surface_exists is raised.
+
+        This creates a new color wp_color_management_surface_v1 object for the
+        given wl_surface.
+
+        See the wp_color_management_surface_v1 interface for more details.
+      </description>
+
+      <arg name="id" type="new_id" interface="wp_color_management_surface_v1"/>
+      <arg name="surface" type="object" interface="wl_surface"/>
+    </request>
+
+    <request name="get_surface_feedback">
+      <description summary="create a color management feedback interface">
+        This creates a new color wp_color_management_surface_feedback_v1 object
+        for the given wl_surface.
+
+        See the wp_color_management_surface_feedback_v1 interface for more
+        details.
+      </description>
+
+      <arg name="id" type="new_id"
+           interface="wp_color_management_surface_feedback_v1"/>
+      <arg name="surface" type="object" interface="wl_surface"/>
+    </request>
+
+    <request name="create_icc_creator">
+      <description summary="make a new ICC-based image description creator object">
+        Makes a new ICC-based image description creator object with all
+        properties initially unset. The client can then use the object's
+        interface to define all the required properties for an image description
+        and finally create a wp_image_description_v1 object.
+
+        This request can be used when the compositor advertises
+        wp_color_manager_v1.feature.icc_v2_v4.
+        Otherwise this request raises the protocol error unsupported_feature.
+      </description>
+
+      <arg name="obj"
+           type="new_id" interface="wp_image_description_creator_icc_v1"
+           summary="the new creator object"/>
+    </request>
+
+    <request name="create_parametric_creator">
+      <description summary="make a new parametric image description creator object">
+        Makes a new parametric image description creator object with all
+        properties initially unset. The client can then use the object's
+        interface to define all the required properties for an image description
+        and finally create a wp_image_description_v1 object.
+
+        This request can be used when the compositor advertises
+        wp_color_manager_v1.feature.parametric.
+        Otherwise this request raises the protocol error unsupported_feature.
+      </description>
+
+      <arg name="obj"
+           type="new_id" interface="wp_image_description_creator_params_v1"
+           summary="the new creator object"/>
+    </request>
+
+    <request name="create_windows_scrgb">
+      <description summary="create Windows-scRGB image description object">
+        This creates a pre-defined image description for the so-called
+        Windows-scRGB stimulus encoding. This comes from the Windows 10 handling
+        of its own definition of an scRGB color space for an HDR screen
+        driven in BT.2100/PQ signalling mode.
+
+        Windows-scRGB uses sRGB (BT.709) color primaries and white point.
+        The transfer characteristic is extended linear.
+
+        The nominal color channel value range is extended, meaning it includes
+        negative and greater than 1.0 values. Negative values are used to
+        escape the sRGB color gamut boundaries. To make use of the extended
+        range, the client needs to use a pixel format that can represent those
+        values, e.g. floating-point 16 bits per channel.
+
+        Nominal color value R=G=B=0.0 corresponds to BT.2100/PQ system
+        0 cd/m², and R=G=B=1.0 corresponds to BT.2100/PQ system 80 cd/m².
+        The maximum is R=G=B=125.0 corresponding to 10k cd/m².
+
+        Windows-scRGB is displayed by Windows 10 by converting it to
+        BT.2100/PQ, maintaining the CIE 1931 chromaticity and mapping the
+        luminance as above. No adjustment is made to the signal to account
+        for the viewing conditions.
+
+        The reference white level of Windows-scRGB is unknown. If a
+        reference white level must be assumed for compositor processing, it
+        should be R=G=B=2.5375 corresponding to 203 cd/m² of Report ITU-R
+        BT.2408-7.
+
+        The target color volume of Windows-scRGB is unknown. The color gamut
+        may be anything between sRGB and BT.2100.
+
+        Note: EGL_EXT_gl_colorspace_scrgb_linear definition differs from
+        Windows-scRGB by using R=G=B=1.0 as the reference white level, while
+        Windows-scRGB reference white level is unknown or varies. However,
+        it seems probable that Windows implements both
+        EGL_EXT_gl_colorspace_scrgb_linear and Vulkan
+        VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT as Windows-scRGB.
+
+        This request can be used when the compositor advertises
+        wp_color_manager_v1.feature.windows_scrgb.
+        Otherwise this request raises the protocol error unsupported_feature.
+
+        The resulting image description object does not allow get_information
+        request. The wp_image_description_v1.ready event shall be sent.
+      </description>
+
+      <arg name="image_description"
+           type="new_id" interface="wp_image_description_v1"/>
+    </request>
+
+    <event name="supported_intent">
+      <description summary="supported rendering intent">
+        When this object is created, it shall immediately send this event once
+        for each rendering intent the compositor supports.
+      </description>
+
+      <arg name="render_intent" type="uint" enum="render_intent"
+           summary="rendering intent"/>
+    </event>
+
+    <event name="supported_feature">
+      <description summary="supported features">
+        When this object is created, it shall immediately send this event once
+        for each compositor supported feature listed in the enumeration.
+      </description>
+
+      <arg name="feature" type="uint" enum="feature"
+           summary="supported feature"/>
+    </event>
+
+    <event name="supported_tf_named">
+      <description summary="supported named transfer characteristic">
+        When this object is created, it shall immediately send this event once
+        for each named transfer function the compositor supports with the
+        parametric image description creator.
+      </description>
+
+      <arg name="tf" type="uint" enum="transfer_function"
+           summary="Named transfer function"/>
+    </event>
+
+    <event name="supported_primaries_named">
+      <description summary="supported named primaries">
+        When this object is created, it shall immediately send this event once
+        for each named set of primaries the compositor supports with the
+        parametric image description creator.
+      </description>
+
+      <arg name="primaries" type="uint" enum="primaries"
+           summary="Named color primaries"/>
+    </event>
+
+    <event name="done">
+      <description summary="all features have been sent">
+        This event is sent when all supported rendering intents, features,
+        transfer functions and named primaries have been sent.
+      </description>
+    </event>
+  </interface>
+
+  <interface name="wp_color_management_output_v1" version="1">
+    <description summary="output color properties">
+      A wp_color_management_output_v1 describes the color properties of an
+      output.
+
+      The wp_color_management_output_v1 is associated with the wl_output global
+      underlying the wl_output object. Therefore the client destroying the
+      wl_output object has no impact, but the compositor removing the output
+      global makes the wp_color_management_output_v1 object inert.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the color management output">
+        Destroy the color wp_color_management_output_v1 object. This does not
+        affect any remaining protocol objects.
+      </description>
+    </request>
+
+    <event name="image_description_changed">
+      <description summary="image description changed">
+        This event is sent whenever the image description of the output changed,
+        followed by one wl_output.done event common to output events across all
+        extensions.
+
+        If the client wants to use the updated image description, it needs to do
+        get_image_description again, because image description objects are
+        immutable.
+      </description>
+    </event>
+
+    <request name="get_image_description">
+      <description summary="get the image description of the output">
+        This creates a new wp_image_description_v1 object for the current image
+        description of the output. There always is exactly one image description
+        active for an output so the client should destroy the image description
+        created by earlier invocations of this request. This request is usually
+        sent as a reaction to the image_description_changed event or when
+        creating a wp_color_management_output_v1 object.
+
+        The image description of an output represents the color encoding the
+        output expects. There might be performance and power advantages, as well
+        as improved color reproduction, if a content update matches the image
+        description of the output it is being shown on. If a content update is
+        shown on any other output than the one it matches the image description
+        of, then the color reproduction on those outputs might be considerably
+        worse.
+
+        The created wp_image_description_v1 object preserves the image
+        description of the output from the time the object was created.
+
+        The resulting image description object allows get_information request.
+
+        If this protocol object is inert, the resulting image description object
+        shall immediately deliver the wp_image_description_v1.failed event with
+        the no_output cause.
+
+        If the interface version is inadequate for the output's image
+        description, meaning that the client does not support all the events
+        needed to deliver the crucial information, the resulting image
+        description object shall immediately deliver the
+        wp_image_description_v1.failed event with the low_version cause.
+
+        Otherwise the object shall immediately deliver the ready event.
+      </description>
+
+      <arg name="image_description"
+           type="new_id" interface="wp_image_description_v1"/>
+    </request>
+  </interface>
+
+  <interface name="wp_color_management_surface_v1" version="1">
+    <description summary="color management extension to a surface">
+        A wp_color_management_surface_v1 allows the client to set the color
+        space and HDR properties of a surface.
+
+        If the wl_surface associated with the wp_color_management_surface_v1 is
+        destroyed, the wp_color_management_surface_v1 object becomes inert.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the color management interface for a surface">
+        Destroy the wp_color_management_surface_v1 object and do the same as
+        unset_image_description.
+      </description>
+    </request>
+
+    <enum name="error">
+      <description summary="protocol errors"/>
+      <entry name="render_intent" value="0"
+             summary="unsupported rendering intent"/>
+      <entry name="image_description" value="1"
+             summary="invalid image description"/>
+      <entry name="inert" value="2"
+             summary="forbidden request on inert object"/>
+    </enum>
+
+    <request name="set_image_description">
+      <description summary="set the surface image description">
+        If this protocol object is inert, the protocol error inert is raised.
+
+        Set the image description of the underlying surface. The image
+        description and rendering intent are double-buffered state, see
+        wl_surface.commit.
+
+        It is the client's responsibility to understand the image description
+        it sets on a surface, and to provide content that matches that image
+        description. Compositors might convert images to match their own or any
+        other image descriptions.
+
+        Image descriptions which are not ready (see wp_image_description_v1)
+        are forbidden in this request, and in such case the protocol error
+        image_description is raised.
+
+        All image descriptions which are ready (see wp_image_description_v1)
+        are allowed and must always be accepted by the compositor.
+
+        A rendering intent provides the client's preference on how content
+        colors should be mapped to each output. The render_intent value must
+        be one advertised by the compositor with
+        wp_color_manager_v1.render_intent event, otherwise the protocol error
+        render_intent is raised.
+
+        When an image description is set on a surface, the Transfer
+        Characteristics of the image description defines the valid range of
+        the nominal (real-valued) color channel values. The processing of
+        out-of-range color channel values is undefined, but compositors are
+        recommended to clamp the values to the valid range when possible.
+
+        By default, a surface does not have an associated image description
+        nor a rendering intent. The handling of color on such surfaces is
+        compositor implementation defined. Compositors should handle such
+        surfaces as sRGB, but may handle them differently if they have specific
+        requirements.
+
+        Setting the image description has copy semantics; after this request,
+        the image description can be immediately destroyed without affecting
+        the pending state of the surface.
+      </description>
+
+      <arg name="image_description"
+           type="object" interface="wp_image_description_v1"/>
+      <arg name="render_intent"
+           type="uint" enum="wp_color_manager_v1.render_intent"
+           summary="rendering intent"/>
+    </request>
+
+    <request name="unset_image_description">
+      <description summary="remove the surface image description">
+        If this protocol object is inert, the protocol error inert is raised.
+
+        This request removes any image description from the surface. See
+        set_image_description for how a compositor handles a surface without
+        an image description. This is double-buffered state, see
+        wl_surface.commit.
+      </description>
+    </request>
+  </interface>
+
+  <interface name="wp_color_management_surface_feedback_v1" version="1">
+    <description summary="color management extension to a surface">
+        A wp_color_management_surface_feedback_v1 allows the client to get the
+        preferred image description of a surface.
+
+        If the wl_surface associated with this object is destroyed, the
+        wp_color_management_surface_feedback_v1 object becomes inert.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the color management interface for a surface">
+        Destroy the wp_color_management_surface_feedback_v1 object.
+      </description>
+    </request>
+
+    <enum name="error">
+      <description summary="protocol errors"/>
+      <entry name="inert" value="0"
+             summary="forbidden request on inert object"/>
+      <entry name="unsupported_feature" value="1"
+             summary="attempted to use an unsupported feature"/>
+    </enum>
+
+    <event name="preferred_changed">
+      <description summary="the preferred image description changed">
+        The preferred image description is the one which likely has the most
+        performance and/or quality benefits for the compositor if used by the
+        client for its wl_surface contents. This event is sent whenever the
+        compositor changes the wl_surface's preferred image description.
+
+        This event sends the identity of the new preferred state as the argument,
+        so clients who are aware of the image description already can reuse it.
+        Otherwise, if the client client wants to know what the preferred image
+        description is, it shall use the get_preferred request.
+
+        The preferred image description is not automatically used for anything.
+        It is only a hint, and clients may set any valid image description with
+        set_image_description, but there might be performance and color accuracy
+        improvements by providing the wl_surface contents in the preferred
+        image description. Therefore clients that can, should render according
+        to the preferred image description
+      </description>
+
+      <arg name="identity" type="uint" summary="image description id number"/>
+    </event>
+
+    <request name="get_preferred">
+      <description summary="get the preferred image description">
+        If this protocol object is inert, the protocol error inert is raised.
+
+        The preferred image description represents the compositor's preferred
+        color encoding for this wl_surface at the current time. There might be
+        performance and power advantages, as well as improved color
+        reproduction, if the image description of a content update matches the
+        preferred image description.
+
+        This creates a new wp_image_description_v1 object for the currently
+        preferred image description for the wl_surface. The client should
+        stop using and destroy the image descriptions created by earlier
+        invocations of this request for the associated wl_surface.
+        This request is usually sent as a reaction to the preferred_changed
+        event or when creating a wp_color_management_surface_feedback_v1 object
+        if the client is capable of adapting to image descriptions.
+
+        The created wp_image_description_v1 object preserves the preferred image
+        description of the wl_surface from the time the object was created.
+
+        The resulting image description object allows get_information request.
+
+        If the image description is parametric, the client should set it on its
+        wl_surface only if the image description is an exact match with the
+        client content. Particularly if everything else matches, but the target
+        color volume is greater than what the client needs, the client should
+        create its own parameric image description with its exact parameters.
+
+        If the interface version is inadequate for the preferred image
+        description, meaning that the client does not support all the
+        events needed to deliver the crucial information, the resulting image
+        description object shall immediately deliver the
+        wp_image_description_v1.failed event with the low_version cause,
+        otherwise the object shall immediately deliver the ready event.
+      </description>
+
+      <arg name="image_description"
+           type="new_id" interface="wp_image_description_v1"/>
+    </request>
+
+    <request name="get_preferred_parametric">
+      <description summary="get the preferred image description">
+        The same description as for get_preferred applies, except the returned
+        image description is guaranteed to be parametric. This is meant for
+        clients that can only deal with parametric image descriptions.
+
+        If the compositor doesn't support parametric image descriptions, the
+        unsupported_feature error is emitted.
+      </description>
+
+      <arg name="image_description"
+           type="new_id" interface="wp_image_description_v1"/>
+    </request>
+  </interface>
+
+  <interface name="wp_image_description_creator_icc_v1" version="1">
+    <description summary="holder of image description ICC information">
+      This type of object is used for collecting all the information required
+      to create a wp_image_description_v1 object from an ICC file. A complete
+      set of required parameters consists of these properties:
+      - ICC file
+
+      Each required property must be set exactly once if the client is to create
+      an image description. The set requests verify that a property was not
+      already set. The create request verifies that all required properties are
+      set. There may be several alternative requests for setting each property,
+      and in that case the client must choose one of them.
+
+      Once all properties have been set, the create request must be used to
+      create the image description object, destroying the creator in the
+      process.
+    </description>
+
+    <enum name="error">
+      <description summary="protocol errors"/>
+
+      <entry name="incomplete_set" value="0"
+             summary="incomplete parameter set"/>
+      <entry name="already_set" value="1"
+             summary="property already set"/>
+      <entry name="bad_fd" value="2"
+             summary="fd not seekable and readable"/>
+      <entry name="bad_size" value="3"
+             summary="no or too much data"/>
+      <entry name="out_of_file" value="4"
+             summary="offset + length exceeds file size"/>
+    </enum>
+
+    <request name="create" type="destructor">
+      <description summary="Create the image description object from ICC data">
+        Create an image description object based on the ICC information
+        previously set on this object. A compositor must parse the ICC data in
+        some undefined but finite amount of time.
+
+        The completeness of the parameter set is verified. If the set is not
+        complete, the protocol error incomplete_set is raised. For the
+        definition of a complete set, see the description of this interface.
+
+        If the particular combination of the information is not supported
+        by the compositor, the resulting image description object shall
+        immediately deliver the wp_image_description_v1.failed event with the
+        'unsupported' cause. If a valid image description was created from the
+        information, the wp_image_description_v1.ready event will eventually
+        be sent instead.
+
+        This request destroys the wp_image_description_creator_icc_v1 object.
+
+        The resulting image description object does not allow get_information
+        request.
+      </description>
+
+      <arg name="image_description"
+           type="new_id" interface="wp_image_description_v1"/>
+    </request>
+
+    <request name="set_icc_file">
+      <description summary="set the ICC profile file">
+        Sets the ICC profile file to be used as the basis of the image
+        description.
+
+        The data shall be found through the given fd at the given offset, having
+        the given length. The fd must be seekable and readable. Violating these
+        requirements raises the bad_fd protocol error.
+
+        If reading the data fails due to an error independent of the client, the
+        compositor shall send the wp_image_description_v1.failed event on the
+        created wp_image_description_v1 with the 'operating_system' cause.
+
+        The maximum size of the ICC profile is 32 MB. If length is greater than
+        that or zero, the protocol error bad_size is raised. If offset + length
+        exceeds the file size, the protocol error out_of_file is raised.
+
+        A compositor may read the file at any time starting from this request
+        and only until whichever happens first:
+        - If create request was issued, the wp_image_description_v1 object
+          delivers either failed or ready event; or
+        - if create request was not issued, this
+          wp_image_description_creator_icc_v1 object is destroyed.
+
+        A compositor shall not modify the contents of the file, and the fd may
+        be sealed for writes and size changes. The client must ensure to its
+        best ability that the data does not change while the compositor is
+        reading it.
+
+        The data must represent a valid ICC profile. The ICC profile version
+        must be 2 or 4, it must be a 3 channel profile and the class must be
+        Display or ColorSpace. Violating these requirements will not result in a
+        protocol error, but will eventually send the
+        wp_image_description_v1.failed event on the created
+        wp_image_description_v1 with the 'unsupported' cause.
+
+        See the International Color Consortium specification ICC.1:2022 for more
+        details about ICC profiles.
+
+        If ICC file has already been set on this object, the protocol error
+        already_set is raised.
+      </description>
+
+      <arg name="icc_profile" type="fd"
+           summary="ICC profile"/>
+      <arg name="offset" type="uint"
+           summary="byte offset in fd to start of ICC data"/>
+      <arg name="length" type="uint"
+           summary="length of ICC data in bytes"/>
+    </request>
+  </interface>
+
+  <interface name="wp_image_description_creator_params_v1" version="1">
+    <description summary="holder of image description parameters">
+      This type of object is used for collecting all the parameters required
+      to create a wp_image_description_v1 object. A complete set of required
+      parameters consists of these properties:
+      - transfer characteristic function (tf)
+      - chromaticities of primaries and white point (primary color volume)
+
+      The following properties are optional and have a well-defined default
+      if not explicitly set:
+      - primary color volume luminance range
+      - reference white luminance level
+      - mastering display primaries and white point (target color volume)
+      - mastering luminance range
+
+      The following properties are optional and will be ignored
+      if not explicitly set:
+      - maximum content light level
+      - maximum frame-average light level
+
+      Each required property must be set exactly once if the client is to create
+      an image description. The set requests verify that a property was not
+      already set. The create request verifies that all required properties are
+      set. There may be several alternative requests for setting each property,
+      and in that case the client must choose one of them.
+
+      Once all properties have been set, the create request must be used to
+      create the image description object, destroying the creator in the
+      process.
+    </description>
+
+    <enum name="error">
+      <description summary="protocol errors"/>
+
+      <entry name="incomplete_set" value="0"
+             summary="incomplete parameter set"/>
+      <entry name="already_set" value="1"
+             summary="property already set"/>
+      <entry name="unsupported_feature" value="2"
+             summary="request not supported"/>
+      <entry name="invalid_tf" value="3"
+             summary="invalid transfer characteristic"/>
+      <entry name="invalid_primaries_named" value="4"
+             summary="invalid primaries named"/>
+      <entry name="invalid_luminance" value="5"
+             summary="invalid luminance value or range"/>
+    </enum>
+
+    <request name="create" type="destructor">
+      <description summary="Create the image description object using params">
+        Create an image description object based on the parameters previously
+        set on this object.
+
+        The completeness of the parameter set is verified. If the set is not
+        complete, the protocol error incomplete_set is raised. For the
+        definition of a complete set, see the description of this interface.
+
+        The protocol error invalid_luminance is raised if any of the following
+        requirements is not met:
+        - When max_cll is set, it must be greater than min L and less or equal
+          to max L of the mastering luminance range.
+        - When max_fall is set, it must be greater than min L and less or equal
+          to max L of the mastering luminance range.
+        - When both max_cll and max_fall are set, max_fall must be less or equal
+          to max_cll.
+
+        If the particular combination of the parameter set is not supported
+        by the compositor, the resulting image description object shall
+        immediately deliver the wp_image_description_v1.failed event with the
+        'unsupported' cause. If a valid image description was created from the
+        parameter set, the wp_image_description_v1.ready event will eventually
+        be sent instead.
+
+        This request destroys the wp_image_description_creator_params_v1
+        object.
+
+        The resulting image description object does not allow get_information
+        request.
+      </description>
+
+      <arg name="image_description"
+           type="new_id" interface="wp_image_description_v1"/>
+    </request>
+
+    <request name="set_tf_named">
+      <description summary="named transfer characteristic">
+        Sets the transfer characteristic using explicitly enumerated named
+        functions.
+
+        When the resulting image description is attached to an image, the
+        content should be encoded and decoded according to the industry standard
+        practices for the transfer characteristic.
+
+        Only names advertised with wp_color_manager_v1 event supported_tf_named
+        are allowed. Other values shall raise the protocol error invalid_tf.
+
+        If transfer characteristic has already been set on this object, the
+        protocol error already_set is raised.
+      </description>
+
+      <arg name="tf" type="uint" enum="wp_color_manager_v1.transfer_function"
+           summary="named transfer function"/>
+    </request>
+
+    <request name="set_tf_power">
+      <description summary="transfer characteristic as a power curve">
+        Sets the color component transfer characteristic to a power curve with
+        the given exponent. Negative values are handled by mirroring the
+        positive half of the curve through the origin. The valid domain and
+        range of the curve are all finite real numbers. This curve represents
+        the conversion from electrical to optical color channel values.
+
+        When the resulting image description is attached to an image, the
+        content should be encoded with the inverse of the power curve.
+
+        The curve exponent shall be multiplied by 10000 to get the argument eexp
+        value to carry the precision of 4 decimals.
+
+        The curve exponent must be at least 1.0 and at most 10.0. Otherwise the
+        protocol error invalid_tf is raised.
+
+        If transfer characteristic has already been set on this object, the
+        protocol error already_set is raised.
+
+        This request can be used when the compositor advertises
+        wp_color_manager_v1.feature.set_tf_power. Otherwise this request raises
+        the protocol error unsupported_feature.
+      </description>
+
+      <arg name="eexp" type="uint" summary="the exponent * 10000"/>
+    </request>
+
+    <request name="set_primaries_named">
+      <description summary="named primaries">
+        Sets the color primaries and white point using explicitly named sets.
+        This describes the primary color volume which is the basis for color
+        value encoding.
+
+        Only names advertised with wp_color_manager_v1 event
+        supported_primaries_named are allowed. Other values shall raise the
+        protocol error invalid_primaries_named.
+
+        If primaries have already been set on this object, the protocol error
+        already_set is raised.
+      </description>
+
+      <arg name="primaries" type="uint" enum="wp_color_manager_v1.primaries"
+           summary="named primaries"/>
+    </request>
+
+    <request name="set_primaries">
+      <description summary="primaries as chromaticity coordinates">
+        Sets the color primaries and white point using CIE 1931 xy chromaticity
+        coordinates. This describes the primary color volume which is the basis
+        for color value encoding.
+
+        Each coordinate value is multiplied by 1 million to get the argument
+        value to carry precision of 6 decimals.
+
+        If primaries have already been set on this object, the protocol error
+        already_set is raised.
+
+        This request can be used if the compositor advertises
+        wp_color_manager_v1.feature.set_primaries. Otherwise this request raises
+        the protocol error unsupported_feature.
+      </description>
+
+      <arg name="r_x" type="int" summary="Red x * 1M"/>
+      <arg name="r_y" type="int" summary="Red y * 1M"/>
+      <arg name="g_x" type="int" summary="Green x * 1M"/>
+      <arg name="g_y" type="int" summary="Green y * 1M"/>
+      <arg name="b_x" type="int" summary="Blue x * 1M"/>
+      <arg name="b_y" type="int" summary="Blue y * 1M"/>
+      <arg name="w_x" type="int" summary="White x * 1M"/>
+      <arg name="w_y" type="int" summary="White y * 1M"/>
+    </request>
+
+    <request name="set_luminances">
+      <description summary="primary color volume luminance range and reference white">
+        Sets the primary color volume luminance range and the reference white
+        luminance level. These values include the minimum display emission
+        and ambient flare luminances, assumed to be optically additive and have
+        the chromaticity of the primary color volume white point.
+
+        The default luminances from
+        https://www.color.org/chardata/rgb/srgb.xalter are
+        - primary color volume minimum: 0.2 cd/m²
+        - primary color volume maximum: 80 cd/m²
+        - reference white: 80 cd/m²
+
+        Setting a named transfer characteristic can imply other default
+        luminances.
+
+        The default luminances get overwritten when this request is used.
+        With transfer_function.st2084_pq the given 'max_lum' value is ignored,
+        and 'max_lum' is taken as 'min_lum' + 10000 cd/m².
+
+        'min_lum' and 'max_lum' specify the minimum and maximum luminances of
+        the primary color volume as reproduced by the targeted display.
+
+        'reference_lum' specifies the luminance of the reference white as
+        reproduced by the targeted display, and reflects the targeted viewing
+        environment.
+
+        Compositors should make sure that all content is anchored, meaning that
+        an input signal level of 'reference_lum' on one image description and
+        another input signal level of 'reference_lum' on another image
+        description should produce the same output level, even though the
+        'reference_lum' on both image representations can be different.
+
+        'reference_lum' may be higher than 'max_lum'. In that case reaching
+        the reference white output level in image content requires the
+        'extended_target_volume' feature support.
+
+        If 'max_lum' or 'reference_lum' are less than or equal to 'min_lum',
+        the protocol error invalid_luminance is raised.
+
+        The minimum luminance is multiplied by 10000 to get the argument
+        'min_lum' value and carries precision of 4 decimals. The maximum
+        luminance and reference white luminance values are unscaled.
+
+        If the primary color volume luminance range and the reference white
+        luminance level have already been set on this object, the protocol error
+        already_set is raised.
+
+        This request can be used if the compositor advertises
+        wp_color_manager_v1.feature.set_luminances. Otherwise this request
+        raises the protocol error unsupported_feature.
+      </description>
+
+      <arg name="min_lum" type="uint"
+           summary="minimum luminance (cd/m²) * 10000"/>
+      <arg name="max_lum" type="uint"
+           summary="maximum luminance (cd/m²)"/>
+      <arg name="reference_lum" type="uint"
+           summary="reference white luminance (cd/m²)"/>
+    </request>
+
+    <request name="set_mastering_display_primaries">
+      <description summary="mastering display primaries as chromaticity coordinates">
+        Provides the color primaries and white point of the mastering display
+        using CIE 1931 xy chromaticity coordinates. This is compatible with the
+        SMPTE ST 2086 definition of HDR static metadata.
+
+        The mastering display primaries and mastering display luminances define
+        the target color volume.
+
+        If mastering display primaries are not explicitly set, the target color
+        volume is assumed to have the same primaries as the primary color volume.
+
+        The target color volume is defined by all tristimulus values between 0.0
+        and 1.0 (inclusive) of the color space defined by the given mastering
+        display primaries and white point. The colorimetry is identical between
+        the container color space and the mastering display color space,
+        including that no chromatic adaptation is applied even if the white
+        points differ.
+
+        The target color volume can exceed the primary color volume to allow for
+        a greater color volume with an existing color space definition (for
+        example scRGB). It can be smaller than the primary color volume to
+        minimize gamut and tone mapping distances for big color spaces (HDR
+        metadata).
+
+        To make use of the entire target color volume a suitable pixel format
+        has to be chosen (e.g. floating point to exceed the primary color
+        volume, or abusing limited quantization range as with xvYCC).
+
+        Each coordinate value is multiplied by 1 million to get the argument
+        value to carry precision of 6 decimals.
+
+        If mastering display primaries have already been set on this object, the
+        protocol error already_set is raised.
+
+        This request can be used if the compositor advertises
+        wp_color_manager_v1.feature.set_mastering_display_primaries. Otherwise
+        this request raises the protocol error unsupported_feature. The
+        advertisement implies support only for target color volumes fully
+        contained within the primary color volume.
+
+        If a compositor additionally supports target color volume exceeding the
+        primary color volume, it must advertise
+        wp_color_manager_v1.feature.extended_target_volume. If a client uses
+        target color volume exceeding the primary color volume and the
+        compositor does not support it, the result is implementation defined.
+        Compositors are recommended to detect this case and fail the image
+        description gracefully, but it may as well result in color artifacts.
+      </description>
+
+      <arg name="r_x" type="int" summary="Red x * 1M"/>
+      <arg name="r_y" type="int" summary="Red y * 1M"/>
+      <arg name="g_x" type="int" summary="Green x * 1M"/>
+      <arg name="g_y" type="int" summary="Green y * 1M"/>
+      <arg name="b_x" type="int" summary="Blue x * 1M"/>
+      <arg name="b_y" type="int" summary="Blue y * 1M"/>
+      <arg name="w_x" type="int" summary="White x * 1M"/>
+      <arg name="w_y" type="int" summary="White y * 1M"/>
+    </request>
+
+    <request name="set_mastering_luminance">
+      <description summary="display mastering luminance range">
+        Sets the luminance range that was used during the content mastering
+        process as the minimum and maximum absolute luminance L. These values
+        include the minimum display emission and ambient flare luminances,
+        assumed to be optically additive and have the chromaticity of the
+        primary color volume white point. This should be
+        compatible with the SMPTE ST 2086 definition of HDR static metadata.
+
+        The mastering display primaries and mastering display luminances define
+        the target color volume.
+
+        If mastering luminances are not explicitly set, the target color volume
+        is assumed to have the same min and max luminances as the primary color
+        volume.
+
+        If max L is less than or equal to min L, the protocol error
+        invalid_luminance is raised.
+
+        Min L value is multiplied by 10000 to get the argument min_lum value
+        and carry precision of 4 decimals. Max L value is unscaled for max_lum.
+
+        This request can be used if the compositor advertises
+        wp_color_manager_v1.feature.set_mastering_display_primaries. Otherwise
+        this request raises the protocol error unsupported_feature. The
+        advertisement implies support only for target color volumes fully
+        contained within the primary color volume.
+
+        If a compositor additionally supports target color volume exceeding the
+        primary color volume, it must advertise
+        wp_color_manager_v1.feature.extended_target_volume. If a client uses
+        target color volume exceeding the primary color volume and the
+        compositor does not support it, the result is implementation defined.
+        Compositors are recommended to detect this case and fail the image
+        description gracefully, but it may as well result in color artifacts.
+      </description>
+
+      <arg name="min_lum" type="uint" summary="min L (cd/m²) * 10000"/>
+      <arg name="max_lum" type="uint" summary="max L (cd/m²)"/>
+    </request>
+
+    <request name="set_max_cll">
+      <description summary="maximum content light level">
+        Sets the maximum content light level (max_cll) as defined by CTA-861-H.
+
+        max_cll is undefined by default.
+      </description>
+
+      <arg name="max_cll" type="uint" summary="Maximum content light level (cd/m²)"/>
+    </request>
+
+    <request name="set_max_fall">
+      <description summary="maximum frame-average light level">
+        Sets the maximum frame-average light level (max_fall) as defined by
+        CTA-861-H.
+
+        max_fall is undefined by default.
+      </description>
+
+      <arg name="max_fall" type="uint" summary="Maximum frame-average light level (cd/m²)"/>
+    </request>
+  </interface>
+
+  <interface name="wp_image_description_v1" version="1">
+    <description summary="Colorimetric image description">
+      An image description carries information about the color encoding used on
+      a surface when attached to a wl_surface via
+      wp_color_management_surface_v1.set_image_description. A compositor can use
+      this information to decode pixel values into colorimetrically meaningful
+      quantities.
+
+      Note, that the wp_image_description_v1 object is not ready to be used
+      immediately after creation. The object eventually delivers either the
+      'ready' or the 'failed' event, specified in all requests creating it. The
+      object is deemed "ready" after receiving the 'ready' event.
+
+      An object which is not ready is illegal to use, it can only be destroyed.
+      Any other request in this interface shall result in the 'not_ready'
+      protocol error. Attempts to use an object which is not ready through other
+      interfaces shall raise protocol errors defined there.
+
+      Once created and regardless of how it was created, a
+      wp_image_description_v1 object always refers to one fixed image
+      description. It cannot change after creation.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the image description">
+        Destroy this object. It is safe to destroy an object which is not ready.
+
+        Destroying a wp_image_description_v1 object has no side-effects, not
+        even if a wp_color_management_surface_v1.set_image_description has not
+        yet been followed by a wl_surface.commit.
+      </description>
+    </request>
+
+    <enum name="error">
+      <description summary="protocol errors"/>
+
+      <entry name="not_ready" value="0"
+             summary="attempted to use an object which is not ready"/>
+      <entry name="no_information" value="1"
+             summary="get_information not allowed"/>
+    </enum>
+
+    <enum name="cause">
+      <description summary="generic reason for failure"/>
+
+      <entry name="low_version" value="0"
+             summary="interface version too low"/>
+      <entry name="unsupported" value="1"
+             summary="unsupported image description data"/>
+      <entry name="operating_system" value="2"
+             summary="error independent of the client"/>
+      <entry name="no_output" value="3"
+             summary="the relevant output no longer exists"/>
+    </enum>
+
+    <event name="failed">
+      <description summary="graceful error on creating the image description">
+        If creating a wp_image_description_v1 object fails for a reason that is
+        not defined as a protocol error, this event is sent.
+
+        The requests that create image description objects define whether and
+        when this can occur. Only such creation requests can trigger this event.
+        This event cannot be triggered after the image description was
+        successfully formed.
+
+        Once this event has been sent, the wp_image_description_v1 object will
+        never become ready and it can only be destroyed.
+      </description>
+
+      <arg name="cause" type="uint" enum="cause"
+           summary="generic reason"/>
+      <arg name="msg" type="string"
+           summary="ad hoc human-readable explanation"/>
+    </event>
+
+    <event name="ready">
+      <description summary="indication that the object is ready to be used">
+        Once this event has been sent, the wp_image_description_v1 object is
+        deemed "ready". Ready objects can be used to send requests and can be
+        used through other interfaces.
+
+        Every ready wp_image_description_v1 protocol object refers to an
+        underlying image description record in the compositor. Multiple protocol
+        objects may end up referring to the same record. Clients may identify
+        these "copies" by comparing their id numbers: if the numbers from two
+        protocol objects are identical, the protocol objects refer to the same
+        image description record. Two different image description records
+        cannot have the same id number simultaneously. The id number does not
+        change during the lifetime of the image description record.
+
+        The id number is valid only as long as the protocol object is alive. If
+        all protocol objects referring to the same image description record are
+        destroyed, the id number may be recycled for a different image
+        description record.
+
+        Image description id number is not a protocol object id. Zero is
+        reserved as an invalid id number. It shall not be possible for a client
+        to refer to an image description by its id number in protocol. The id
+        numbers might not be portable between Wayland connections. A compositor
+        shall not send an invalid id number.
+
+        This identity allows clients to de-duplicate image description records
+        and avoid get_information request if they already have the image
+        description information.
+      </description>
+
+      <arg name="identity" type="uint" summary="image description id number"/>
+    </event>
+
+    <request name="get_information">
+      <description summary="get information about the image description">
+        Creates a wp_image_description_info_v1 object which delivers the
+        information that makes up the image description.
+
+        Not all image description protocol objects allow get_information
+        request. Whether it is allowed or not is defined by the request that
+        created the object. If get_information is not allowed, the protocol
+        error no_information is raised.
+      </description>
+
+      <arg name="information"
+           type="new_id" interface="wp_image_description_info_v1"/>
+    </request>
+  </interface>
+
+  <interface name="wp_image_description_info_v1" version="1">
+    <description summary="Colorimetric image description information">
+      Sends all matching events describing an image description object exactly
+      once and finally sends the 'done' event.
+
+      This means
+      - if the image description is parametric, it must send
+        - primaries
+        - named_primaries, if applicable
+        - at least one of tf_power and tf_named, as applicable
+        - luminances
+        - target_primaries
+        - target_luminance
+      - if the image description is parametric, it may send, if applicable,
+        - target_max_cll
+        - target_max_fall
+      - if the image description contains an ICC profile, it must send the
+        icc_file event
+
+      Once a wp_image_description_info_v1 object has delivered a 'done' event it
+      is automatically destroyed.
+
+      Every wp_image_description_info_v1 created from the same
+      wp_image_description_v1 shall always return the exact same data.
+    </description>
+
+    <event name="done" type="destructor">
+      <description summary="end of information">
+        Signals the end of information events and destroys the object.
+      </description>
+    </event>
+
+    <event name="icc_file">
+      <description summary="ICC profile matching the image description">
+        The icc argument provides a file descriptor to the client which may be
+        memory-mapped to provide the ICC profile matching the image description.
+        The fd is read-only, and if mapped then it must be mapped with
+        MAP_PRIVATE by the client.
+
+        The ICC profile version and other details are determined by the
+        compositor. There is no provision for a client to ask for a specific
+        kind of a profile.
+      </description>
+
+      <arg name="icc" type="fd" summary="ICC profile file descriptor"/>
+      <arg name="icc_size" type="uint" summary="ICC profile size, in bytes"/>
+      <!-- Offset always 0, compositor must not expose unnecessary data. -->
+    </event>
+
+    <event name="primaries">
+      <description summary="primaries as chromaticity coordinates">
+        Delivers the primary color volume primaries and white point using CIE
+        1931 xy chromaticity coordinates.
+
+        Each coordinate value is multiplied by 1 million to get the argument
+        value to carry precision of 6 decimals.
+      </description>
+
+      <arg name="r_x" type="int" summary="Red x * 1M"/>
+      <arg name="r_y" type="int" summary="Red y * 1M"/>
+      <arg name="g_x" type="int" summary="Green x * 1M"/>
+      <arg name="g_y" type="int" summary="Green y * 1M"/>
+      <arg name="b_x" type="int" summary="Blue x * 1M"/>
+      <arg name="b_y" type="int" summary="Blue y * 1M"/>
+      <arg name="w_x" type="int" summary="White x * 1M"/>
+      <arg name="w_y" type="int" summary="White y * 1M"/>
+    </event>
+
+    <event name="primaries_named">
+      <description summary="named primaries">
+        Delivers the primary color volume primaries and white point using an
+        explicitly enumerated named set.
+      </description>
+
+      <arg name="primaries" type="uint" enum="wp_color_manager_v1.primaries"
+           summary="named primaries"/>
+    </event>
+
+    <event name="tf_power">
+      <description summary="transfer characteristic as a power curve">
+        The color component transfer characteristic of this image description is
+        a pure power curve. This event provides the exponent of the power
+        function. This curve represents the conversion from electrical to
+        optical pixel or color values.
+
+        The curve exponent has been multiplied by 10000 to get the argument eexp
+        value to carry the precision of 4 decimals.
+      </description>
+
+      <arg name="eexp" type="uint" summary="the exponent * 10000"/>
+    </event>
+
+    <event name="tf_named">
+      <description summary="named transfer characteristic">
+        Delivers the transfer characteristic using an explicitly enumerated
+        named function.
+      </description>
+
+      <arg name="tf" type="uint" enum="wp_color_manager_v1.transfer_function"
+           summary="named transfer function"/>
+    </event>
+
+    <event name="luminances">
+      <description summary="primary color volume luminance range and reference white">
+        Delivers the primary color volume luminance range and the reference
+        white luminance level. These values include the minimum display emission
+        and ambient flare luminances, assumed to be optically additive and have
+        the chromaticity of the primary color volume white point.
+
+        The minimum luminance is multiplied by 10000 to get the argument
+        'min_lum' value and carries precision of 4 decimals. The maximum
+        luminance and reference white luminance values are unscaled.
+      </description>
+
+      <arg name="min_lum" type="uint"
+           summary="minimum luminance (cd/m²) * 10000"/>
+      <arg name="max_lum" type="uint"
+           summary="maximum luminance (cd/m²)"/>
+      <arg name="reference_lum" type="uint"
+           summary="reference white luminance (cd/m²)"/>
+    </event>
+
+    <event name="target_primaries">
+      <description summary="target primaries as chromaticity coordinates">
+        Provides the color primaries and white point of the target color volume
+        using CIE 1931 xy chromaticity coordinates. This is compatible with the
+        SMPTE ST 2086 definition of HDR static metadata for mastering displays.
+
+        While primary color volume is about how color is encoded, the target
+        color volume is the actually displayable color volume. If target color
+        volume is equal to the primary color volume, then this event is not
+        sent.
+
+        Each coordinate value is multiplied by 1 million to get the argument
+        value to carry precision of 6 decimals.
+      </description>
+
+      <arg name="r_x" type="int" summary="Red x * 1M"/>
+      <arg name="r_y" type="int" summary="Red y * 1M"/>
+      <arg name="g_x" type="int" summary="Green x * 1M"/>
+      <arg name="g_y" type="int" summary="Green y * 1M"/>
+      <arg name="b_x" type="int" summary="Blue x * 1M"/>
+      <arg name="b_y" type="int" summary="Blue y * 1M"/>
+      <arg name="w_x" type="int" summary="White x * 1M"/>
+      <arg name="w_y" type="int" summary="White y * 1M"/>
+    </event>
+
+    <event name="target_luminance">
+      <description summary="target luminance range">
+        Provides the luminance range that the image description is targeting as
+        the minimum and maximum absolute luminance L. These values include the
+        minimum display emission and ambient flare luminances, assumed to be
+        optically additive and have the chromaticity of the primary color
+        volume white point. This should be compatible with the SMPTE ST 2086
+        definition of HDR static metadata.
+
+        This luminance range is only theoretical and may not correspond to the
+        luminance of light emitted on an actual display.
+
+        Min L value is multiplied by 10000 to get the argument min_lum value and
+        carry precision of 4 decimals. Max L value is unscaled for max_lum.
+      </description>
+
+      <arg name="min_lum" type="uint" summary="min L (cd/m²) * 10000"/>
+      <arg name="max_lum" type="uint" summary="max L (cd/m²)"/>
+    </event>
+
+    <event name="target_max_cll">
+      <description summary="target maximum content light level">
+        Provides the targeted max_cll of the image description. max_cll is
+        defined by CTA-861-H.
+
+        This luminance is only theoretical and may not correspond to the
+        luminance of light emitted on an actual display.
+      </description>
+
+      <arg name="max_cll" type="uint"
+           summary="Maximum content light-level (cd/m²)"/>
+    </event>
+
+    <event name="target_max_fall">
+      <description summary="target maximum frame-average light level">
+        Provides the targeted max_fall of the image description. max_fall is
+        defined by CTA-861-H.
+
+        This luminance is only theoretical and may not correspond to the
+        luminance of light emitted on an actual display.
+      </description>
+
+      <arg name="max_fall" type="uint"
+           summary="Maximum frame-average light level (cd/m²)"/>
+    </event>
+  </interface>
+</protocol>
+
diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h
index 3f5df73da4..81d409a873 100644
--- a/include/GLFW/glfw3.h
+++ b/include/GLFW/glfw3.h
@@ -2080,6 +2080,33 @@ typedef struct GLFWgammaramp
     unsigned int size;
 } GLFWgammaramp;
 
+ /*! @brief HDR config.
+  *
+  *  This describes the display properties.
+  *
+  *  @sa @ref glfwGetHDRConfig
+  *
+  *  @since Added in version 3.5.
+  *
+  *  @ingroup window
+  */
+typedef struct GLFWhdrconfig {
+    uint32_t primaries;
+    uint32_t transfer_function;
+    float output_display_primary_red_x;
+    float output_display_primary_red_y;
+    float output_display_primary_green_x;
+    float output_display_primary_green_y;
+    float output_display_primary_blue_x;
+    float output_display_primary_blue_y;
+    float output_white_point_x;
+    float output_white_point_y;
+    float max_luminance;
+    float min_luminance;
+    float max_full_frame_luminance;
+    float sdr_white_level;
+} GLFWhdrconfig;
+
 /*! @brief Image data.
  *
  *  This describes a single 2D image.  See the documentation for each related
@@ -3486,6 +3513,28 @@ GLFWAPI void glfwGetWindowPos(GLFWwindow* window, int* xpos, int* ypos);
  */
 GLFWAPI void glfwSetWindowPos(GLFWwindow* window, int xpos, int ypos);
 
+/*! @brief Retrieves the HDR configuration of the specified window.
+ *
+ *  This function retrieves the HDR configuration of the specified window.  If
+ *  the window does not support HDR, this function returns `NULL`.
+ *
+ *  @param[in] window The window to query.
+ *  @return The HDR configuration of the window, or `NULL` if it does not
+ *  support HDR or an [error](@ref error_handling) occurred.
+ *
+ *  @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref
+ *  GLFW_PLATFORM_ERROR.
+ *
+ *  @thread_safety This function must only be called from the main thread.
+ *
+ *  @sa @ref window_hdr
+ *
+ *  @since Added in version 3.4.
+ *
+ *  @ingroup window
+ */
+GLFWAPI const GLFWhdrconfig* glfwGetHDRConfig(GLFWwindow* window);
+
 /*! @brief Retrieves the size of the content area of the specified window.
  *
  *  This function retrieves the size, in screen coordinates, of the content area
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2cbe8a7335..f2d0c9447f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -103,6 +103,7 @@ if (GLFW_BUILD_WAYLAND)
     generate_wayland_protocol("pointer-constraints-unstable-v1.xml")
     generate_wayland_protocol("relative-pointer-unstable-v1.xml")
     generate_wayland_protocol("fractional-scale-v1.xml")
+    generate_wayland_protocol("color-management-v1.xml")
     generate_wayland_protocol("xdg-activation-v1.xml")
     generate_wayland_protocol("xdg-decoration-unstable-v1.xml")
 endif()
diff --git a/src/cocoa_init.m b/src/cocoa_init.m
index 9f7bd285cf..2daa759408 100644
--- a/src/cocoa_init.m
+++ b/src/cocoa_init.m
@@ -545,6 +545,7 @@ GLFWbool _glfwConnectCocoa(int platformID, _GLFWplatform* platform)
         .setWindowIcon = _glfwSetWindowIconCocoa,
         .getWindowPos = _glfwGetWindowPosCocoa,
         .setWindowPos = _glfwSetWindowPosCocoa,
+        .getHDRConfig = _glfwGetHDRConfigCocoa,
         .getWindowSize = _glfwGetWindowSizeCocoa,
         .setWindowSize = _glfwSetWindowSizeCocoa,
         .setWindowSizeLimits = _glfwSetWindowSizeLimitsCocoa,
diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h
index 236af78fbd..780598f119 100644
--- a/src/cocoa_platform.h
+++ b/src/cocoa_platform.h
@@ -224,6 +224,7 @@ void _glfwSetWindowTitleCocoa(_GLFWwindow* window, const char* title);
 void _glfwSetWindowIconCocoa(_GLFWwindow* window, int count, const GLFWimage* images);
 void _glfwGetWindowPosCocoa(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosCocoa(_GLFWwindow* window, int xpos, int ypos);
+GLFWhdrconfig* _glfwGetHDRConfigCocoa(_GLFWwindow* window);
 void _glfwGetWindowSizeCocoa(_GLFWwindow* window, int* width, int* height);
 void _glfwSetWindowSizeCocoa(_GLFWwindow* window, int width, int height);
 void _glfwSetWindowSizeLimitsCocoa(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight);
diff --git a/src/cocoa_window.m b/src/cocoa_window.m
index e69b5fe0c3..063e740bc5 100644
--- a/src/cocoa_window.m
+++ b/src/cocoa_window.m
@@ -1063,6 +1063,11 @@ void _glfwSetWindowPosCocoa(_GLFWwindow* window, int x, int y)
     } // autoreleasepool
 }
 
+GLFWhdrconfig* _glfwGetHDRConfigCocoa(_GLFWwindow* window)
+{
+    return NULL; // Cocoa does not support HDR via GLFW. Use Metal APIs directly.
+}
+
 void _glfwGetWindowSizeCocoa(_GLFWwindow* window, int* width, int* height)
 {
     @autoreleasepool {
diff --git a/src/egl_context.c b/src/egl_context.c
index 517c64cb2b..078134b885 100644
--- a/src/egl_context.c
+++ b/src/egl_context.c
@@ -26,6 +26,7 @@
 //========================================================================
 
 #include "internal.h"
+#include "color-management-v1-client-protocol.h"
 
 #include <stdio.h>
 #include <string.h>
@@ -196,6 +197,10 @@ static GLFWbool chooseEGLConfig(const _GLFWctxconfig* ctxconfig,
 
         u->samples = getEGLConfigAttrib(n, EGL_SAMPLES);
         u->doublebuffer = fbconfig->doublebuffer;
+        u->floatbuffer = (u->redBits == 0 || u->redBits >= 16) &&
+                        (u->greenBits == 0 || u->greenBits >= 16) &&
+                        (u->blueBits == 0 || u->blueBits >= 16) &&
+                        (u->alphaBits == 0 || u->alphaBits >= 16);
 
         u->handle = (uintptr_t) n;
         usableCount++;
diff --git a/src/internal.h b/src/internal.h
index 01322381c3..756c0c4cb3 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -66,6 +66,7 @@ typedef void (*GLFWproc)(void);
 typedef struct _GLFWerror       _GLFWerror;
 typedef struct _GLFWinitconfig  _GLFWinitconfig;
 typedef struct _GLFWwndconfig   _GLFWwndconfig;
+typedef struct _GLFWhdrconfig   _GLFWhdrconfig;
 typedef struct _GLFWctxconfig   _GLFWctxconfig;
 typedef struct _GLFWfbconfig    _GLFWfbconfig;
 typedef struct _GLFWcontext     _GLFWcontext;
@@ -721,6 +722,7 @@ struct _GLFWplatform
     void (*setWindowIcon)(_GLFWwindow*,int,const GLFWimage*);
     void (*getWindowPos)(_GLFWwindow*,int*,int*);
     void (*setWindowPos)(_GLFWwindow*,int,int);
+    GLFWhdrconfig* (*getHDRConfig)(_GLFWwindow*);
     void (*getWindowSize)(_GLFWwindow*,int*,int*);
     void (*setWindowSize)(_GLFWwindow*,int,int);
     void (*setWindowSizeLimits)(_GLFWwindow*,int,int,int,int);
diff --git a/src/null_init.c b/src/null_init.c
index 8c10f5e67c..0575ea4d5c 100644
--- a/src/null_init.c
+++ b/src/null_init.c
@@ -74,6 +74,7 @@ GLFWbool _glfwConnectNull(int platformID, _GLFWplatform* platform)
         .setWindowIcon = _glfwSetWindowIconNull,
         .getWindowPos = _glfwGetWindowPosNull,
         .setWindowPos = _glfwSetWindowPosNull,
+        .getHDRConfig = _glfwGetHDRConfigNull,
         .getWindowSize = _glfwGetWindowSizeNull,
         .setWindowSize = _glfwSetWindowSizeNull,
         .setWindowSizeLimits = _glfwSetWindowSizeLimitsNull,
diff --git a/src/null_platform.h b/src/null_platform.h
index dbcb835b83..13b062e2de 100644
--- a/src/null_platform.h
+++ b/src/null_platform.h
@@ -226,6 +226,7 @@ void _glfwSetWindowIconNull(_GLFWwindow* window, int count, const GLFWimage* ima
 void _glfwSetWindowMonitorNull(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
 void _glfwGetWindowPosNull(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosNull(_GLFWwindow* window, int xpos, int ypos);
+GLFWhdrconfig* _glfwGetHDRConfigNull(_GLFWwindow* window);
 void _glfwGetWindowSizeNull(_GLFWwindow* window, int* width, int* height);
 void _glfwSetWindowSizeNull(_GLFWwindow* window, int width, int height);
 void _glfwSetWindowSizeLimitsNull(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight);
diff --git a/src/null_window.c b/src/null_window.c
index f0e1dcc9a9..32d0d1835e 100644
--- a/src/null_window.c
+++ b/src/null_window.c
@@ -242,6 +242,12 @@ void _glfwSetWindowPosNull(_GLFWwindow* window, int xpos, int ypos)
     }
 }
 
+GLFWhdrconfig* _glfwGetHDRConfigNull(_GLFWwindow* window)
+{
+    // Null does not support HDR
+    return NULL;
+}
+
 void _glfwGetWindowSizeNull(_GLFWwindow* window, int* width, int* height)
 {
     if (width)
diff --git a/src/win32_platform.h b/src/win32_platform.h
index 7b3ecaff0c..128f72a2d5 100644
--- a/src/win32_platform.h
+++ b/src/win32_platform.h
@@ -489,6 +489,7 @@ void _glfwSetWindowTitleWin32(_GLFWwindow* window, const char* title);
 void _glfwSetWindowIconWin32(_GLFWwindow* window, int count, const GLFWimage* images);
 void _glfwGetWindowPosWin32(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosWin32(_GLFWwindow* window, int xpos, int ypos);
+GLFWhdrconfig* _glfwGetHDRConfigWin32(_GLFWwindow* window);
 void _glfwGetWindowSizeWin32(_GLFWwindow* window, int* width, int* height);
 void _glfwSetWindowSizeWin32(_GLFWwindow* window, int width, int height);
 void _glfwSetWindowSizeLimitsWin32(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight);
diff --git a/src/win32_window.c b/src/win32_window.c
index 26f9684b79..42dcf5b10f 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -1651,6 +1651,125 @@ void _glfwSetWindowPosWin32(_GLFWwindow* window, int xpos, int ypos)
                  SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOSIZE);
 }
 
+float GetDisplayWhitePoint() {
+    UINT32 numPaths, numModes;
+    LONG result;
+
+    // Get the number of paths and modes
+    result = GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &numPaths, &numModes);
+    if (result != ERROR_SUCCESS) {
+        return FALSE;
+    }
+
+    // Allocate memory for the paths and modes
+    DISPLAYCONFIG_PATH_INFO* paths = _glfw_calloc(numPaths * sizeof(DISPLAYCONFIG_PATH_INFO));
+    DISPLAYCONFIG_MODE_INFO* modes = _glfw_calloc(numModes * sizeof(DISPLAYCONFIG_MODE_INFO));
+
+    if (!paths || !modes) {
+        _glfw_free(paths);
+        _glfw_free(modes);
+        return FALSE;
+    }
+
+    // Query the display configuration
+    result = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &numPaths, paths, &numModes, modes, NULL);
+    if (result != ERROR_SUCCESS) {
+        _glfw_free(paths);
+        _glfw_free(modes);
+        return FALSE;
+    }
+
+    // Find the first active path
+    for (UINT32 i = 0; i < numPaths; i++) {
+        if (paths[i].flags & DISPLAYCONFIG_PATH_ACTIVE) {
+            DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName;
+            sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
+            sourceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME);
+            sourceName.header.adapterId = paths[i].sourceInfo.adapterId;
+            sourceName.header.id = paths[i].sourceInfo.id;
+
+            result = DisplayConfigGetDeviceInfo(&sourceName.header);
+            if (result == ERROR_SUCCESS) {
+                DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO advanced_color_info = {};
+                advanced_color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
+                advanced_color_info.header.size = sizeof(advanced_color_info);
+                advanced_color_info.header.adapterId = paths[i].targetInfo.adapterId;
+                advanced_color_info.header.id = paths[i].targetInfo.id;
+                result = DisplayConfigGetDeviceInfo(&advanced_color_info.header);
+
+                if (result == ERROR_SUCCESS && advanced_color_info.advancedColorEnabled > 0) {
+                    DISPLAYCONFIG_SDR_WHITE_LEVEL white_level = {};
+                    white_level.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
+                    white_level.header.size = sizeof(white_level);
+                    white_level.header.adapterId = paths[i].targetInfo.adapterId;
+                    white_level.header.id = paths[i].targetInfo.id;
+                    if (DisplayConfigGetDeviceInfo(&white_level.header) == ERROR_SUCCESS)
+                        return white_level.SDRWhiteLevel;  // From wingdi.h.
+                }
+            }
+        }
+    }
+
+    _glfw_free(paths);
+    _glfw_free(modes);
+
+    memset(0, 0, 0);
+    return 80.0f; // sRGB standard white level
+}
+
+GLFWhdrconfig* _glfwGetHDRConfigWin32(_GLFWwindow* window)
+{
+    IDXGIFactory6* factory = NULL;
+    IDXGIAdapter4* adapter = NULL;
+    IDXGIOutput6* output = NULL;
+    DXGI_OUTPUT_DESC1 desc;
+
+    GLFWhdrconfig* config = NULL;
+
+    HRESULT hr = CreateDXGIFactory2(0, &IID_IDXGIFactory6, (void**)&factory);
+    if (SUCCEEDED(hr))
+    {
+        // TODO: loop instead of hardcoding 0
+        hr = factory->lpVtbl->EnumAdapters(factory, 0, (IDXGIAdapter**)&adapter);
+        if (SUCCEEDED(hr))
+        {
+            hr = adapter->lpVtbl->EnumOutputs(adapter, 0, (IDXGIOutput**)&output);
+            if (SUCCEEDED(hr))
+            {
+                hr = output->lpVtbl->GetDesc1(output, &desc);
+                if (SUCCEEDED(hr))
+                {
+                    config = _glfw_calloc(sizeof(GLFWhdrconfig));
+
+                    // Surface colorspace is scRGB, i.e. sRGB primaries with linear transfer
+                    // See https://learn.microsoft.com/en-us/windows/win32/direct3darticles/high-dynamic-range
+                    config->primaries = 1; // sRGB H.273
+                    config->transfer_function = 8; // linear H.273
+                    
+                    config->output_display_primary_red_x = desc.RedPrimary[0];
+                    config->output_display_primary_red_y = desc.RedPrimary[1];
+                    config->output_display_primary_green_x = desc.GreenPrimary[0];
+                    config->output_display_primary_green_y = desc.GreenPrimary[1];
+                    config->output_display_primary_blue_x = desc.BluePrimary[0];
+                    config->output_display_primary_blue_y = desc.BluePrimary[1];
+                    config->output_white_point_x = desc.WhitePoint[0];
+                    config->output_white_point_y = desc.WhitePoint[1];
+                    config->max_luminance = desc.MaxLuminance;
+                    config->min_luminance = desc.MinLuminance;
+                    config->max_full_frame_luminance = desc.MaxFullFrameLuminance;
+                    config->sdr_white_level = GetDisplayWhitePoint();
+                }
+            }
+        }
+    }
+
+    if (output) output->lpVtbl->Release(output);
+    if (adapter) adapter->lpVtbl->Release(adapter);
+    if (factory) factory->lpVtbl->Release(factory);
+
+    return config;
+}
+
 void _glfwGetWindowSizeWin32(_GLFWwindow* window, int* width, int* height)
 {
     RECT area;
diff --git a/src/window.c b/src/window.c
index d3b2869d69..b1e370f1b0 100644
--- a/src/window.c
+++ b/src/window.c
@@ -612,6 +612,16 @@ GLFWAPI void glfwSetWindowPos(GLFWwindow* handle, int xpos, int ypos)
     _glfw.platform.setWindowPos(window, xpos, ypos);
 }
 
+GLFWAPI const GLFWhdrconfig* glfwGetHDRConfig(GLFWwindow* handle)
+{
+    _GLFW_REQUIRE_INIT_OR_RETURN(NULL);
+
+    _GLFWwindow* window = (_GLFWwindow*) handle;
+    assert(window != NULL);
+
+    return _glfw.platform.getHDRConfig(window);
+}
+
 GLFWAPI void glfwGetWindowSize(GLFWwindow* handle, int* width, int* height)
 {
     if (width)
diff --git a/src/wl_init.c b/src/wl_init.c
index ef9e450360..ba7c25747d 100644
--- a/src/wl_init.c
+++ b/src/wl_init.c
@@ -49,6 +49,7 @@
 #include "fractional-scale-v1-client-protocol.h"
 #include "xdg-activation-v1-client-protocol.h"
 #include "idle-inhibit-unstable-v1-client-protocol.h"
+#include "color-management-v1-client-protocol.h"
 
 // NOTE: Versions of wayland-scanner prior to 1.17.91 named every global array of
 //       wl_interface pointers 'types', making it impossible to combine several unmodified
@@ -83,6 +84,10 @@
 #include "fractional-scale-v1-client-protocol-code.h"
 #undef types
 
+#define types _glfw_color_management_types
+#include "color-management-v1-client-protocol-code.h"
+#undef types
+
 #define types _glfw_xdg_activation_types
 #include "xdg-activation-v1-client-protocol-code.h"
 #undef types
@@ -103,6 +108,89 @@ static const struct xdg_wm_base_listener wmBaseListener =
     wmBaseHandlePing
 };
 
+void colorManagerHandleSupportedIntent(void *userData, struct wp_color_manager_v1 *color_manager, uint32_t render_intent)
+{
+    _GLFWlibraryWayland* wl = userData;
+    if (render_intent >= sizeof(wl->colorManagerSupport.intents) / sizeof(wl->colorManagerSupport.intents[0]))
+    {
+        printf("Wayland: Unsupported render intent %d\n", render_intent);
+        return;
+    }
+
+    wl->colorManagerSupport.intents[render_intent] = GLFW_TRUE;
+}
+
+void colorManagerHandleSupportedFeature(void *userData, struct wp_color_manager_v1 *color_manager, uint32_t feature)
+{
+    _GLFWlibraryWayland* wl = userData;
+
+    switch (feature) {
+    case WP_COLOR_MANAGER_V1_FEATURE_ICC_V2_V4:
+        wl->colorManagerSupport.icc = GLFW_TRUE;
+        break;
+    case WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC:
+        wl->colorManagerSupport.parametric = GLFW_TRUE;
+        break;
+    case WP_COLOR_MANAGER_V1_FEATURE_SET_PRIMARIES:
+        wl->colorManagerSupport.setPrimaries = GLFW_TRUE;
+        break;
+    case WP_COLOR_MANAGER_V1_FEATURE_SET_TF_POWER:
+        wl->colorManagerSupport.setTfPower = GLFW_TRUE;
+        break;
+    case WP_COLOR_MANAGER_V1_FEATURE_SET_LUMINANCES:
+        wl->colorManagerSupport.setLuminance = GLFW_TRUE;
+        break;
+    case WP_COLOR_MANAGER_V1_FEATURE_SET_MASTERING_DISPLAY_PRIMARIES:
+        wl->colorManagerSupport.setMasteringDisplayPrimaries = GLFW_TRUE;
+        break;
+    case WP_COLOR_MANAGER_V1_FEATURE_EXTENDED_TARGET_VOLUME:
+        wl->colorManagerSupport.setExtendedTargetVolume = GLFW_TRUE;
+        break;
+    case WP_COLOR_MANAGER_V1_FEATURE_WINDOWS_SCRGB:
+        wl->colorManagerSupport.windowsScrgb = GLFW_TRUE;
+        break;
+    default:
+        printf("Wayland: Unsupported color manager feature %d\n", feature);
+        break;
+    }
+}
+
+void colorManagerHandleSupportedTransferFunction(void *userData, struct wp_color_manager_v1 *color_manager, uint32_t tf)
+{
+    _GLFWlibraryWayland* wl = userData;
+    if (tf >= sizeof(wl->colorManagerSupport.tfs) / sizeof(wl->colorManagerSupport.tfs[0]))
+    {
+        printf("Wayland: Unsupported transfer function %d\n", tf);
+        return;
+    }
+
+    wl->colorManagerSupport.tfs[tf] = GLFW_TRUE;
+}
+
+void colorManagerHandleSupportedPrimaries(void *userData, struct wp_color_manager_v1 *color_manager, uint32_t primaries)
+{
+    _GLFWlibraryWayland* wl = userData;
+    if (primaries >= sizeof(wl->colorManagerSupport.primaries) / sizeof(wl->colorManagerSupport.primaries[0]))
+    {
+        printf("Wayland: Unsupported primaries %d\n", primaries);
+        return;
+    }
+
+    wl->colorManagerSupport.primaries[primaries] = GLFW_TRUE;
+}
+
+void colorManagerHandleDone(void *userData, struct wp_color_manager_v1 *color_manager)
+{
+}
+
+const struct wp_color_manager_v1_listener colorManagerListener = {
+    colorManagerHandleSupportedIntent,
+    colorManagerHandleSupportedFeature,
+    colorManagerHandleSupportedTransferFunction,
+    colorManagerHandleSupportedPrimaries,
+    colorManagerHandleDone,
+};
+
 static void registryHandleGlobal(void* userData,
                                  struct wl_registry* registry,
                                  uint32_t name,
@@ -208,6 +296,16 @@ static void registryHandleGlobal(void* userData,
                              &wp_fractional_scale_manager_v1_interface,
                              1);
     }
+    else if (strcmp(interface, "wp_color_manager_v1") == 0)
+    {
+        _glfw.wl.colorManager =
+            wl_registry_bind(registry, name,
+                             &wp_color_manager_v1_interface,
+                             1);
+
+        memset(&_glfw.wl.colorManagerSupport, 0, sizeof(_glfw.wl.colorManagerSupport));
+        wp_color_manager_v1_add_listener(_glfw.wl.colorManager, &colorManagerListener, &_glfw.wl);
+    }
 }
 
 static void registryHandleGlobalRemove(void* userData,
@@ -479,6 +577,7 @@ GLFWbool _glfwConnectWayland(int platformID, _GLFWplatform* platform)
         .setWindowIcon = _glfwSetWindowIconWayland,
         .getWindowPos = _glfwGetWindowPosWayland,
         .setWindowPos = _glfwSetWindowPosWayland,
+        .getHDRConfig = _glfwGetHDRConfigWayland,
         .getWindowSize = _glfwGetWindowSizeWayland,
         .setWindowSize = _glfwSetWindowSizeWayland,
         .setWindowSizeLimits = _glfwSetWindowSizeLimitsWayland,
@@ -988,6 +1087,8 @@ void _glfwTerminateWayland(void)
         xdg_activation_v1_destroy(_glfw.wl.activationManager);
     if (_glfw.wl.fractionalScaleManager)
         wp_fractional_scale_manager_v1_destroy(_glfw.wl.fractionalScaleManager);
+    if (_glfw.wl.colorManager)
+        wp_color_manager_v1_destroy(_glfw.wl.colorManager);
     if (_glfw.wl.registry)
         wl_registry_destroy(_glfw.wl.registry);
     if (_glfw.wl.display)
diff --git a/src/wl_platform.h b/src/wl_platform.h
index afa6f50af5..7a2b620206 100644
--- a/src/wl_platform.h
+++ b/src/wl_platform.h
@@ -385,6 +385,8 @@ typedef struct _GLFWwindowWayland
         struct libdecor_frame*  frame;
     } libdecor;
 
+    GLFWhdrconfig*              hdrConfig;
+
     _GLFWcursor*                currentCursor;
     double                      cursorPosX, cursorPosY;
 
@@ -400,6 +402,8 @@ typedef struct _GLFWwindowWayland
     struct wp_viewport*             scalingViewport;
     uint32_t                        scalingNumerator;
     struct wp_fractional_scale_v1*  fractionalScale;
+    struct wp_color_management_surface_v1*  colorSurface;
+    struct wp_color_management_surface_feedback_v1*  colorSurfaceFeedback;
 
     struct zwp_relative_pointer_v1* relativePointer;
     struct zwp_locked_pointer_v1*   lockedPointer;
@@ -438,6 +442,22 @@ typedef struct _GLFWlibraryWayland
     struct zwp_idle_inhibit_manager_v1*     idleInhibitManager;
     struct xdg_activation_v1*               activationManager;
     struct wp_fractional_scale_manager_v1*  fractionalScaleManager;
+    struct wp_color_manager_v1*             colorManager;
+
+    struct {
+        GLFWbool parametric;
+        GLFWbool icc;
+        GLFWbool setPrimaries;
+        GLFWbool setTfPower;
+        GLFWbool setLuminance;
+        GLFWbool setMasteringDisplayPrimaries;
+        GLFWbool setExtendedTargetVolume;
+        GLFWbool windowsScrgb;
+
+        GLFWbool primaries[11];
+        GLFWbool tfs[14];
+        GLFWbool intents[4];
+    } colorManagerSupport;
 
     _GLFWofferWayland*          offers;
     unsigned int                offerCount;
@@ -621,6 +641,7 @@ void _glfwSetWindowTitleWayland(_GLFWwindow* window, const char* title);
 void _glfwSetWindowIconWayland(_GLFWwindow* window, int count, const GLFWimage* images);
 void _glfwGetWindowPosWayland(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosWayland(_GLFWwindow* window, int xpos, int ypos);
+GLFWhdrconfig* _glfwGetHDRConfigWayland(_GLFWwindow* window);
 void _glfwGetWindowSizeWayland(_GLFWwindow* window, int* width, int* height);
 void _glfwSetWindowSizeWayland(_GLFWwindow* window, int width, int height);
 void _glfwSetWindowSizeLimitsWayland(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight);
diff --git a/src/wl_window.c b/src/wl_window.c
index 6457f31e04..6e9eac91a0 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -51,6 +51,7 @@
 #include "xdg-activation-v1-client-protocol.h"
 #include "idle-inhibit-unstable-v1-client-protocol.h"
 #include "fractional-scale-v1-client-protocol.h"
+#include "color-management-v1-client-protocol.h"
 
 #define GLFW_BORDER_SIZE    4
 #define GLFW_CAPTION_HEIGHT 24
@@ -555,6 +556,251 @@ const struct wp_fractional_scale_v1_listener fractionalScaleListener =
     fractionalScaleHandlePreferredScale,
 };
 
+#define WAYLAND_COLOR_FACTOR 1000000
+#define WAYLAND_MIN_LUMINANCE_FACTOR 10000
+
+void imageDescriptionHandleDone(void *userData, struct wp_image_description_info_v1 *image_description_info)
+{
+    wp_image_description_info_v1_destroy(image_description_info);
+}
+
+void imageDescriptionHandleIccFile(void *userData, struct wp_image_description_info_v1 *image_description_info, int32_t icc, uint32_t icc_size)
+{
+    printf("Wayland: ICC profile support is not implemented yet.\n");
+}
+
+void imageDescriptionHandlePrimaries(void *userData, struct wp_image_description_info_v1 *image_description_info, 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)
+{
+    printf("Wayland: Primaries support is not implemented yet.\n");
+    _GLFWwindow* window = userData;
+    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
+    if (!hdrConfig)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: No HDR config available for window");
+        return;
+    }
+
+    float surface_primary_red_x = (float)r_x / WAYLAND_COLOR_FACTOR;
+    float surface_primary_red_y = (float)r_y / WAYLAND_COLOR_FACTOR;
+    float surface_primary_green_x = (float)g_x / WAYLAND_COLOR_FACTOR;
+    float surface_primary_green_y = (float)g_y / WAYLAND_COLOR_FACTOR;
+    float surface_primary_blue_x = (float)b_x / WAYLAND_COLOR_FACTOR;
+    float surface_primary_blue_y = (float)b_y / WAYLAND_COLOR_FACTOR;
+    float surface_white_point_x = (float)w_x / WAYLAND_COLOR_FACTOR;
+    float surface_white_point_y = (float)w_y / WAYLAND_COLOR_FACTOR;
+
+    printf("Wayland: Surface primaries set to R(%f, %f), G(%f, %f), B(%f, %f), W(%f, %f)\n",
+          surface_primary_red_x, surface_primary_red_y,
+          surface_primary_green_x, surface_primary_green_y,
+          surface_primary_blue_x, surface_primary_blue_y,
+          surface_white_point_x, surface_white_point_y);
+}
+
+void imageDescriptionHandlePrimariesNamed(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t primaries)
+{
+    _GLFWwindow* window = userData;
+    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
+    if (!hdrConfig)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: No HDR config available for window");
+        return;
+    }
+
+    // Translate Wayland transfer function to H.273 code points 
+    switch (primaries)
+    {
+        case WP_COLOR_MANAGER_V1_PRIMARIES_SRGB: hdrConfig->primaries = 1; break;
+        case WP_COLOR_MANAGER_V1_PRIMARIES_PAL_M: hdrConfig->primaries = 4; break;
+        case WP_COLOR_MANAGER_V1_PRIMARIES_PAL: hdrConfig->primaries = 5; break;
+        case WP_COLOR_MANAGER_V1_PRIMARIES_NTSC: hdrConfig->primaries = 6; break;
+        case WP_COLOR_MANAGER_V1_PRIMARIES_GENERIC_FILM: hdrConfig->primaries = 8; break;
+        case WP_COLOR_MANAGER_V1_PRIMARIES_BT2020: hdrConfig->primaries = 9; break;
+        case WP_COLOR_MANAGER_V1_PRIMARIES_CIE1931_XYZ: hdrConfig->primaries = 10; break;
+        case WP_COLOR_MANAGER_V1_PRIMARIES_DCI_P3: hdrConfig->primaries = 11; break;
+        case WP_COLOR_MANAGER_V1_PRIMARIES_DISPLAY_P3: hdrConfig->primaries = 12; break;
+        case WP_COLOR_MANAGER_V1_PRIMARIES_ADOBE_RGB: hdrConfig->primaries = 256; break; // Adobe RGB is not defined in H.273, use 256 as a placeholder
+        default:
+            _glfwInputError(GLFW_PLATFORM_ERROR,
+                            "Wayland: Unknown primaries %d", primaries);
+            return;
+    }
+}
+
+void imageDescriptionHandlePower(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t eexp)
+{
+    printf("Wayland: Tf power set to %d.\n", eexp);
+}
+
+void imageDescriptionHandleTransferFunctionNamed(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t tf)
+{
+    _GLFWwindow* window = userData;
+    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
+    if (!hdrConfig)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: No HDR config available for window");
+        return;
+    }
+
+    // Translate Wayland transfer function to H.273 code points 
+    switch (tf)
+    {
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_BT1886: hdrConfig->transfer_function = 1; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22: hdrConfig->transfer_function = 4; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA28: hdrConfig->transfer_function = 5; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST240: hdrConfig->transfer_function = 7; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR: hdrConfig->transfer_function = 8; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_100: hdrConfig->transfer_function = 9; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_316: hdrConfig->transfer_function = 10; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_XVYCC: hdrConfig->transfer_function = 11; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB: hdrConfig->transfer_function = 13; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB: hdrConfig->transfer_function = 13; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ: hdrConfig->transfer_function = 16; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST428: hdrConfig->transfer_function = 17; break;
+        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG: hdrConfig->transfer_function = 18; break;
+        default:
+            _glfwInputError(GLFW_PLATFORM_ERROR,
+                            "Wayland: Unknown transfer function %d", tf);
+            return;
+    }
+}
+
+void imageDescriptionHandleLuminances(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum)
+{
+    _GLFWwindow* window = userData;
+    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
+    if (!hdrConfig)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: No HDR config available for window");
+        return;
+    }
+
+    float surface_min_luminance = (float)min_lum / WAYLAND_MIN_LUMINANCE_FACTOR;
+    float surface_max_luminance = (float)max_lum;
+    float surface_reference_luminance = (float)reference_lum;
+
+    printf("Wayland: Surface min luminance: %f, Max luminance: %f, Reference luminance: %f\n", surface_min_luminance, surface_max_luminance, surface_reference_luminance);
+}
+
+void imageDescriptionHandleTargetPrimaries(void *userData, struct wp_image_description_info_v1 *image_description_info, 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)
+{
+    _GLFWwindow* window = userData;
+    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
+    if (!hdrConfig)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: No HDR config available for window");
+        return;
+    }
+
+    hdrConfig->output_display_primary_red_x = (float)r_x / WAYLAND_COLOR_FACTOR;
+    hdrConfig->output_display_primary_red_y = (float)r_y / WAYLAND_COLOR_FACTOR;
+    hdrConfig->output_display_primary_green_x = (float)g_x / WAYLAND_COLOR_FACTOR;
+    hdrConfig->output_display_primary_green_y = (float)g_y / WAYLAND_COLOR_FACTOR;
+    hdrConfig->output_display_primary_blue_x = (float)b_x / WAYLAND_COLOR_FACTOR;
+    hdrConfig->output_display_primary_blue_y = (float)b_y / WAYLAND_COLOR_FACTOR;
+    hdrConfig->output_white_point_x = (float)w_x / WAYLAND_COLOR_FACTOR;
+    hdrConfig->output_white_point_y = (float)w_y / WAYLAND_COLOR_FACTOR;
+
+    printf("Wayland: Target primaries set to R(%f, %f), G(%f, %f), B(%f, %f), W(%f, %f)\n",
+           hdrConfig->output_display_primary_red_x, hdrConfig->output_display_primary_red_y,
+           hdrConfig->output_display_primary_green_x, hdrConfig->output_display_primary_green_y,
+           hdrConfig->output_display_primary_blue_x, hdrConfig->output_display_primary_blue_y,
+           hdrConfig->output_white_point_x, hdrConfig->output_white_point_y);
+}
+
+void imageDescriptionHandleTargetLuminances(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t min_lum, uint32_t max_lum)
+{
+    _GLFWwindow* window = userData;
+    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
+    if (!hdrConfig)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: No HDR config available for window");
+        return;
+    }
+
+    hdrConfig->min_luminance = (float)min_lum / WAYLAND_MIN_LUMINANCE_FACTOR;
+    hdrConfig->max_luminance = (float)max_lum;
+
+    printf("Wayland: Target min luminance: %f, Max luminance: %f\n", hdrConfig->min_luminance, hdrConfig->max_luminance);
+}
+
+void imageDescriptionHandleTargetMaxCll(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t max_cll)
+{
+    printf("Wayland: Max CLL: %d\n", max_cll);
+}
+
+void imageDescriptionHandleTargetMaxFall(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t max_fall)
+{
+    _GLFWwindow* window = userData;
+    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
+    if (!hdrConfig)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: No HDR config available for window");
+        return;
+    }
+
+    hdrConfig->max_full_frame_luminance = (float)max_fall;
+    printf("Wayland: Max FALL: %d\n", max_fall);
+}
+
+const struct wp_image_description_info_v1_listener imageDescriptionListener = {
+    imageDescriptionHandleDone,
+    imageDescriptionHandleIccFile,
+    imageDescriptionHandlePrimaries,
+    imageDescriptionHandlePrimariesNamed,
+    imageDescriptionHandlePower,
+    imageDescriptionHandleTransferFunctionNamed,
+    imageDescriptionHandleLuminances,
+    imageDescriptionHandleTargetPrimaries,
+    imageDescriptionHandleTargetLuminances,
+    imageDescriptionHandleTargetMaxCll,
+    imageDescriptionHandleTargetMaxFall,
+};
+
+void getPreferredImageDescription(_GLFWwindow* window)
+{
+    struct wp_image_description_v1* preferred;
+    // Strangely, Hyprland does not support getting the preferred surface feedback in forced parametric mode, even
+    // if the parametric creator mode is enabled.
+    // if (_glfw.wl.colorManagerSupport.parametric)
+    //     preferred = wp_color_management_surface_feedback_v1_get_preferred_parametric(window->wl.colorSurfaceFeedback);
+    // else
+    preferred = wp_color_management_surface_feedback_v1_get_preferred(window->wl.colorSurfaceFeedback);
+
+    if (!preferred) {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: Color management surface feedback received without preferred image description");
+        return;
+    }
+
+    struct wp_image_description_info_v1 *preferredInfo = wp_image_description_v1_get_information(preferred);
+    wp_image_description_v1_destroy(preferred);
+    if (!preferredInfo) {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: Color management surface feedback received with preferred image description without information");
+        return;
+    }
+
+    window->wl.hdrConfig = _glfw_realloc(window->wl.hdrConfig, sizeof(GLFWhdrconfig));
+
+    wp_image_description_info_v1_add_listener(preferredInfo, &imageDescriptionListener, window);
+}
+
+void colorFeedbackListenerHandleFeedback(void *userData, struct wp_color_management_surface_feedback_v1* feedback, uint32_t identity) {
+    getPreferredImageDescription(userData);
+}
+
+const struct wp_color_management_surface_feedback_v1_listener colorFeedbackListener =
+{
+    colorFeedbackListenerHandleFeedback,
+};
+
 static void xdgToplevelHandleConfigure(void* userData,
                                        struct xdg_toplevel* toplevel,
                                        int32_t width,
@@ -1072,6 +1318,70 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
         }
     }
 
+    window->wl.colorSurface = NULL;
+    window->wl.colorSurfaceFeedback = NULL;
+    window->wl.hdrConfig = NULL;
+
+    enum wp_color_manager_v1_primaries primaries = WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
+    
+    // TODO: we should be using EXR_SRGB here (mirrors sRGB about 0), but it turns out that some compositors (e.g. Hyprland) do not support it.
+    enum wp_color_manager_v1_transfer_function tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
+    enum wp_color_manager_v1_render_intent intent = -1;
+    for (int i = 0; i < 4; ++i)
+    {
+        if (_glfw.wl.colorManagerSupport.intents[i])
+        {
+            intent = i;
+            break;
+        }
+    }
+
+    GLFWbool supportsHdr = _glfw.wl.colorManager &&
+                          _glfw.wl.colorManagerSupport.parametric &&
+                          _glfw.wl.colorManagerSupport.primaries[primaries] &&
+                          _glfw.wl.colorManagerSupport.tfs[tf] &&
+                          intent != -1;
+
+    // HDR / color management config
+    if (supportsHdr) {
+        // Set up color surface & get its preferred image description (populated GLFWhdrconfig)
+        {
+            window->wl.colorSurface = wp_color_manager_v1_get_surface(_glfw.wl.colorManager, window->wl.surface);
+            window->wl.colorSurfaceFeedback = wp_color_manager_v1_get_surface_feedback(_glfw.wl.colorManager, window->wl.surface);
+            if (window->wl.colorSurfaceFeedback) {
+                wp_color_management_surface_feedback_v1_add_listener(window->wl.colorSurfaceFeedback, &colorFeedbackListener, window);
+            }
+
+            getPreferredImageDescription(window);
+
+            wl_display_roundtrip(_glfw.wl.display);
+        }
+        
+        // Configure the color surface to take scRGB (sRGB primaries & linear transfer) from us
+        struct wp_image_description_creator_params_v1* creator = wp_color_manager_v1_create_parametric_creator(_glfw.wl.colorManager);
+        if (!creator)
+        {
+            _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to create color management creator");
+            return GLFW_FALSE;
+        }
+
+        wp_image_description_creator_params_v1_set_primaries_named(creator, primaries);
+        wp_image_description_creator_params_v1_set_tf_named(creator, tf);
+
+        struct wp_image_description_v1* image_description = wp_image_description_creator_params_v1_create(creator);
+
+        if (!image_description)
+        {
+            _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to create image description");
+            wp_image_description_creator_params_v1_destroy(creator);
+            return GLFW_FALSE;
+        }
+
+        wp_color_management_surface_v1_set_image_description(window->wl.colorSurface, image_description, intent);
+
+        wp_image_description_v1_destroy(image_description);
+    }
+
     return GLFW_TRUE;
 }
 
@@ -2192,6 +2502,12 @@ void _glfwDestroyWindowWayland(_GLFWwindow* window)
     if (window->wl.fractionalScale)
         wp_fractional_scale_v1_destroy(window->wl.fractionalScale);
 
+    if (window->wl.colorSurfaceFeedback)
+        wp_color_management_surface_feedback_v1_destroy(window->wl.colorSurfaceFeedback);
+
+    if (window->wl.colorSurface)
+        wp_color_management_surface_v1_destroy(window->wl.colorSurface);
+
     if (window->wl.scalingViewport)
         wp_viewport_destroy(window->wl.scalingViewport);
 
@@ -2260,6 +2576,11 @@ void _glfwSetWindowPosWayland(_GLFWwindow* window, int xpos, int ypos)
                     "Wayland: The platform does not support setting the window position");
 }
 
+GLFWhdrconfig* _glfwGetHDRConfigWayland(_GLFWwindow* window)
+{
+    return window->wl.hdrConfig;
+}
+
 void _glfwGetWindowSizeWayland(_GLFWwindow* window, int* width, int* height)
 {
     if (width)
diff --git a/src/x11_platform.h b/src/x11_platform.h
index cb34acb4d7..22541f5fda 100644
--- a/src/x11_platform.h
+++ b/src/x11_platform.h
@@ -909,6 +909,7 @@ void _glfwSetWindowTitleX11(_GLFWwindow* window, const char* title);
 void _glfwSetWindowIconX11(_GLFWwindow* window, int count, const GLFWimage* images);
 void _glfwGetWindowPosX11(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosX11(_GLFWwindow* window, int xpos, int ypos);
+GLFWhdrconfig* _glfwGetHDRConfigX11(_GLFWwindow* window);
 void _glfwGetWindowSizeX11(_GLFWwindow* window, int* width, int* height);
 void _glfwSetWindowSizeX11(_GLFWwindow* window, int width, int height);
 void _glfwSetWindowSizeLimitsX11(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight);
diff --git a/src/x11_window.c b/src/x11_window.c
index 322349f08b..9e82841602 100644
--- a/src/x11_window.c
+++ b/src/x11_window.c
@@ -2190,6 +2190,11 @@ void _glfwSetWindowPosX11(_GLFWwindow* window, int xpos, int ypos)
     XFlush(_glfw.x11.display);
 }
 
+GLFWhdrconfig* _glfwGetHDRConfigX11(_GLFWwindow* window)
+{
+    return NULL; // X11 does not support HDR
+}
+
 void _glfwGetWindowSizeX11(_GLFWwindow* window, int* width, int* height)
 {
     XWindowAttributes attribs;

From a160eeb2945c12906dc19d643a11a8266bbf826d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Thu, 17 Jul 2025 13:53:04 +0200
Subject: [PATCH 06/40] feat(wayland): preliminary HDR support

---
 src/egl_context.c | 10 ++++++++++
 src/wl_window.c   | 26 ++++++++++++++++++--------
 2 files changed, 28 insertions(+), 8 deletions(-)

diff --git a/src/egl_context.c b/src/egl_context.c
index 078134b885..dc31ccb805 100644
--- a/src/egl_context.c
+++ b/src/egl_context.c
@@ -202,6 +202,16 @@ static GLFWbool chooseEGLConfig(const _GLFWctxconfig* ctxconfig,
                         (u->blueBits == 0 || u->blueBits >= 16) &&
                         (u->alphaBits == 0 || u->alphaBits >= 16);
 
+        // printf("Found EGLConfig %d: %d bits red, %d bits green, %d bits blue, "
+        //        "%d bits alpha, %d bits depth, %d bits stencil, %d samples, "
+        //        "%s double buffered, %s float buffer\n",
+        //        usableCount + 1,
+        //        u->redBits, u->greenBits, u->blueBits,
+        //        u->alphaBits, u->depthBits, u->stencilBits,
+        //        u->samples,
+        //        u->doublebuffer ? "is" : "is not",
+        //        u->floatbuffer ? "is" : "is not");
+
         u->handle = (uintptr_t) n;
         usableCount++;
     }
diff --git a/src/wl_window.c b/src/wl_window.c
index 6e9eac91a0..8d4d3a2ee6 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -571,7 +571,6 @@ void imageDescriptionHandleIccFile(void *userData, struct wp_image_description_i
 
 void imageDescriptionHandlePrimaries(void *userData, struct wp_image_description_info_v1 *image_description_info, 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)
 {
-    printf("Wayland: Primaries support is not implemented yet.\n");
     _GLFWwindow* window = userData;
     GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
     if (!hdrConfig)
@@ -590,7 +589,7 @@ void imageDescriptionHandlePrimaries(void *userData, struct wp_image_description
     float surface_white_point_x = (float)w_x / WAYLAND_COLOR_FACTOR;
     float surface_white_point_y = (float)w_y / WAYLAND_COLOR_FACTOR;
 
-    printf("Wayland: Surface primaries set to R(%f, %f), G(%f, %f), B(%f, %f), W(%f, %f)\n",
+    printf("Wayland: Surface primaries: R(%f, %f), G(%f, %f), B(%f, %f), W(%f, %f)\n",
           surface_primary_red_x, surface_primary_red_y,
           surface_primary_green_x, surface_primary_green_y,
           surface_primary_blue_x, surface_primary_blue_y,
@@ -630,7 +629,7 @@ void imageDescriptionHandlePrimariesNamed(void *userData, struct wp_image_descri
 
 void imageDescriptionHandlePower(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t eexp)
 {
-    printf("Wayland: Tf power set to %d.\n", eexp);
+    printf("Wayland: tfPower: %d.\n", eexp);
 }
 
 void imageDescriptionHandleTransferFunctionNamed(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t tf)
@@ -705,7 +704,7 @@ void imageDescriptionHandleTargetPrimaries(void *userData, struct wp_image_descr
     hdrConfig->output_white_point_x = (float)w_x / WAYLAND_COLOR_FACTOR;
     hdrConfig->output_white_point_y = (float)w_y / WAYLAND_COLOR_FACTOR;
 
-    printf("Wayland: Target primaries set to R(%f, %f), G(%f, %f), B(%f, %f), W(%f, %f)\n",
+    printf("Wayland: Target primaries: R(%f, %f), G(%f, %f), B(%f, %f), W(%f, %f)\n",
            hdrConfig->output_display_primary_red_x, hdrConfig->output_display_primary_red_y,
            hdrConfig->output_display_primary_green_x, hdrConfig->output_display_primary_green_y,
            hdrConfig->output_display_primary_blue_x, hdrConfig->output_display_primary_blue_y,
@@ -1324,9 +1323,14 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
 
     enum wp_color_manager_v1_primaries primaries = WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
     
-    // TODO: we should be using EXR_SRGB here (mirrors sRGB about 0), but it turns out that some compositors (e.g. Hyprland) do not support it.
     enum wp_color_manager_v1_transfer_function tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
-    enum wp_color_manager_v1_render_intent intent = -1;
+    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB])
+    {
+        // Use the extended sRGB transfer function if available
+        tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB;
+    }
+
+    enum wp_color_manager_v1_render_intent intent = WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL;
     for (int i = 0; i < 4; ++i)
     {
         if (_glfw.wl.colorManagerSupport.intents[i])
@@ -1340,9 +1344,15 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
                           _glfw.wl.colorManagerSupport.parametric &&
                           _glfw.wl.colorManagerSupport.primaries[primaries] &&
                           _glfw.wl.colorManagerSupport.tfs[tf] &&
-                          intent != -1;
+                          _glfw.wl.colorManagerSupport.intents[intent];
+
+    if (fbconfig->floatbuffer && !supportsHdr)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: Float buffers are not supported without HDR support");
+        return GLFW_FALSE;
+    }
 
-    // HDR / color management config
     if (supportsHdr) {
         // Set up color surface & get its preferred image description (populated GLFWhdrconfig)
         {

From 313e7002e271ee7e97836adef15b155a4f42b12a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Thu, 17 Jul 2025 16:44:41 +0200
Subject: [PATCH 07/40] fix(wayland): initialize hdrconfig to zero

---
 src/wl_window.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/wl_window.c b/src/wl_window.c
index 8d4d3a2ee6..0ce39b6fc9 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -787,6 +787,7 @@ void getPreferredImageDescription(_GLFWwindow* window)
     }
 
     window->wl.hdrConfig = _glfw_realloc(window->wl.hdrConfig, sizeof(GLFWhdrconfig));
+    memset(window->wl.hdrConfig, 0, sizeof(GLFWhdrconfig));
 
     wp_image_description_info_v1_add_listener(preferredInfo, &imageDescriptionListener, window);
 }

From 271228e38dbdfc1a9c746ab4946d9200acd221f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Thu, 17 Jul 2025 17:59:34 +0200
Subject: [PATCH 08/40] feat: remove HDRConfig in favor of only exposing SDR
 white level

---
 include/GLFW/glfw3.h |  51 +----------
 src/cocoa_init.m     |   2 +-
 src/cocoa_platform.h |   2 +-
 src/cocoa_window.m   |   5 +-
 src/internal.h       |   3 +-
 src/null_init.c      |   2 +-
 src/null_platform.h  |   2 +-
 src/null_window.c    |   5 +-
 src/win32_platform.h |   2 +-
 src/win32_window.c   |  57 +------------
 src/window.c         |   6 +-
 src/wl_init.c        |   2 +-
 src/wl_platform.h    |   4 +-
 src/wl_window.c      | 196 ++++++++++---------------------------------
 src/x11_platform.h   |   2 +-
 src/x11_window.c     |   4 +-
 16 files changed, 68 insertions(+), 277 deletions(-)

diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h
index 81d409a873..3674cae9bd 100644
--- a/include/GLFW/glfw3.h
+++ b/include/GLFW/glfw3.h
@@ -2080,33 +2080,6 @@ typedef struct GLFWgammaramp
     unsigned int size;
 } GLFWgammaramp;
 
- /*! @brief HDR config.
-  *
-  *  This describes the display properties.
-  *
-  *  @sa @ref glfwGetHDRConfig
-  *
-  *  @since Added in version 3.5.
-  *
-  *  @ingroup window
-  */
-typedef struct GLFWhdrconfig {
-    uint32_t primaries;
-    uint32_t transfer_function;
-    float output_display_primary_red_x;
-    float output_display_primary_red_y;
-    float output_display_primary_green_x;
-    float output_display_primary_green_y;
-    float output_display_primary_blue_x;
-    float output_display_primary_blue_y;
-    float output_white_point_x;
-    float output_white_point_y;
-    float max_luminance;
-    float min_luminance;
-    float max_full_frame_luminance;
-    float sdr_white_level;
-} GLFWhdrconfig;
-
 /*! @brief Image data.
  *
  *  This describes a single 2D image.  See the documentation for each related
@@ -3513,28 +3486,6 @@ GLFWAPI void glfwGetWindowPos(GLFWwindow* window, int* xpos, int* ypos);
  */
 GLFWAPI void glfwSetWindowPos(GLFWwindow* window, int xpos, int ypos);
 
-/*! @brief Retrieves the HDR configuration of the specified window.
- *
- *  This function retrieves the HDR configuration of the specified window.  If
- *  the window does not support HDR, this function returns `NULL`.
- *
- *  @param[in] window The window to query.
- *  @return The HDR configuration of the window, or `NULL` if it does not
- *  support HDR or an [error](@ref error_handling) occurred.
- *
- *  @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref
- *  GLFW_PLATFORM_ERROR.
- *
- *  @thread_safety This function must only be called from the main thread.
- *
- *  @sa @ref window_hdr
- *
- *  @since Added in version 3.4.
- *
- *  @ingroup window
- */
-GLFWAPI const GLFWhdrconfig* glfwGetHDRConfig(GLFWwindow* window);
-
 /*! @brief Retrieves the size of the content area of the specified window.
  *
  *  This function retrieves the size, in screen coordinates, of the content area
@@ -3788,6 +3739,8 @@ GLFWAPI void glfwGetWindowFrameSize(GLFWwindow* window, int* left, int* top, int
  */
 GLFWAPI void glfwGetWindowContentScale(GLFWwindow* window, float* xscale, float* yscale);
 
+GLFWAPI float glfwGetWindowSdrWhiteLevel(GLFWwindow* window);
+
 /*! @brief Returns the opacity of the whole window.
  *
  *  This function returns the opacity of the window, including any decorations.
diff --git a/src/cocoa_init.m b/src/cocoa_init.m
index 2daa759408..bacf6f8be8 100644
--- a/src/cocoa_init.m
+++ b/src/cocoa_init.m
@@ -545,7 +545,6 @@ GLFWbool _glfwConnectCocoa(int platformID, _GLFWplatform* platform)
         .setWindowIcon = _glfwSetWindowIconCocoa,
         .getWindowPos = _glfwGetWindowPosCocoa,
         .setWindowPos = _glfwSetWindowPosCocoa,
-        .getHDRConfig = _glfwGetHDRConfigCocoa,
         .getWindowSize = _glfwGetWindowSizeCocoa,
         .setWindowSize = _glfwSetWindowSizeCocoa,
         .setWindowSizeLimits = _glfwSetWindowSizeLimitsCocoa,
@@ -553,6 +552,7 @@ GLFWbool _glfwConnectCocoa(int platformID, _GLFWplatform* platform)
         .getFramebufferSize = _glfwGetFramebufferSizeCocoa,
         .getWindowFrameSize = _glfwGetWindowFrameSizeCocoa,
         .getWindowContentScale = _glfwGetWindowContentScaleCocoa,
+        .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelCocoa,
         .iconifyWindow = _glfwIconifyWindowCocoa,
         .restoreWindow = _glfwRestoreWindowCocoa,
         .maximizeWindow = _glfwMaximizeWindowCocoa,
diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h
index 780598f119..067234bcff 100644
--- a/src/cocoa_platform.h
+++ b/src/cocoa_platform.h
@@ -224,7 +224,6 @@ void _glfwSetWindowTitleCocoa(_GLFWwindow* window, const char* title);
 void _glfwSetWindowIconCocoa(_GLFWwindow* window, int count, const GLFWimage* images);
 void _glfwGetWindowPosCocoa(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosCocoa(_GLFWwindow* window, int xpos, int ypos);
-GLFWhdrconfig* _glfwGetHDRConfigCocoa(_GLFWwindow* window);
 void _glfwGetWindowSizeCocoa(_GLFWwindow* window, int* width, int* height);
 void _glfwSetWindowSizeCocoa(_GLFWwindow* window, int width, int height);
 void _glfwSetWindowSizeLimitsCocoa(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight);
@@ -232,6 +231,7 @@ void _glfwSetWindowAspectRatioCocoa(_GLFWwindow* window, int numer, int denom);
 void _glfwGetFramebufferSizeCocoa(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeCocoa(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleCocoa(_GLFWwindow* window, float* xscale, float* yscale);
+float _glfwGetWindowSdrWhiteLevelCocoa(_GLFWwindow* window);
 void _glfwIconifyWindowCocoa(_GLFWwindow* window);
 void _glfwRestoreWindowCocoa(_GLFWwindow* window);
 void _glfwMaximizeWindowCocoa(_GLFWwindow* window);
diff --git a/src/cocoa_window.m b/src/cocoa_window.m
index 063e740bc5..0385bbccf9 100644
--- a/src/cocoa_window.m
+++ b/src/cocoa_window.m
@@ -1063,9 +1063,10 @@ void _glfwSetWindowPosCocoa(_GLFWwindow* window, int x, int y)
     } // autoreleasepool
 }
 
-GLFWhdrconfig* _glfwGetHDRConfigCocoa(_GLFWwindow* window)
+float _glfwGetWindowSdrWhiteLevelCocoa(_GLFWwindow* window)
 {
-    return NULL; // Cocoa does not support HDR via GLFW. Use Metal APIs directly.
+    // On Cocoa, we'll render via metal configured to the sRGB color space, which'll give us a white level of 80 nits.
+    return 80.0f;
 }
 
 void _glfwGetWindowSizeCocoa(_GLFWwindow* window, int* width, int* height)
diff --git a/src/internal.h b/src/internal.h
index 756c0c4cb3..71a76f9006 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -66,7 +66,6 @@ typedef void (*GLFWproc)(void);
 typedef struct _GLFWerror       _GLFWerror;
 typedef struct _GLFWinitconfig  _GLFWinitconfig;
 typedef struct _GLFWwndconfig   _GLFWwndconfig;
-typedef struct _GLFWhdrconfig   _GLFWhdrconfig;
 typedef struct _GLFWctxconfig   _GLFWctxconfig;
 typedef struct _GLFWfbconfig    _GLFWfbconfig;
 typedef struct _GLFWcontext     _GLFWcontext;
@@ -722,7 +721,7 @@ struct _GLFWplatform
     void (*setWindowIcon)(_GLFWwindow*,int,const GLFWimage*);
     void (*getWindowPos)(_GLFWwindow*,int*,int*);
     void (*setWindowPos)(_GLFWwindow*,int,int);
-    GLFWhdrconfig* (*getHDRConfig)(_GLFWwindow*);
+    float (*getWindowSdrWhiteLevel)(_GLFWwindow*);
     void (*getWindowSize)(_GLFWwindow*,int*,int*);
     void (*setWindowSize)(_GLFWwindow*,int,int);
     void (*setWindowSizeLimits)(_GLFWwindow*,int,int,int,int);
diff --git a/src/null_init.c b/src/null_init.c
index 0575ea4d5c..b4757dfb59 100644
--- a/src/null_init.c
+++ b/src/null_init.c
@@ -74,7 +74,6 @@ GLFWbool _glfwConnectNull(int platformID, _GLFWplatform* platform)
         .setWindowIcon = _glfwSetWindowIconNull,
         .getWindowPos = _glfwGetWindowPosNull,
         .setWindowPos = _glfwSetWindowPosNull,
-        .getHDRConfig = _glfwGetHDRConfigNull,
         .getWindowSize = _glfwGetWindowSizeNull,
         .setWindowSize = _glfwSetWindowSizeNull,
         .setWindowSizeLimits = _glfwSetWindowSizeLimitsNull,
@@ -82,6 +81,7 @@ GLFWbool _glfwConnectNull(int platformID, _GLFWplatform* platform)
         .getFramebufferSize = _glfwGetFramebufferSizeNull,
         .getWindowFrameSize = _glfwGetWindowFrameSizeNull,
         .getWindowContentScale = _glfwGetWindowContentScaleNull,
+        .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelNull,
         .iconifyWindow = _glfwIconifyWindowNull,
         .restoreWindow = _glfwRestoreWindowNull,
         .maximizeWindow = _glfwMaximizeWindowNull,
diff --git a/src/null_platform.h b/src/null_platform.h
index 13b062e2de..08daebb4f6 100644
--- a/src/null_platform.h
+++ b/src/null_platform.h
@@ -226,7 +226,6 @@ void _glfwSetWindowIconNull(_GLFWwindow* window, int count, const GLFWimage* ima
 void _glfwSetWindowMonitorNull(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
 void _glfwGetWindowPosNull(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosNull(_GLFWwindow* window, int xpos, int ypos);
-GLFWhdrconfig* _glfwGetHDRConfigNull(_GLFWwindow* window);
 void _glfwGetWindowSizeNull(_GLFWwindow* window, int* width, int* height);
 void _glfwSetWindowSizeNull(_GLFWwindow* window, int width, int height);
 void _glfwSetWindowSizeLimitsNull(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight);
@@ -234,6 +233,7 @@ void _glfwSetWindowAspectRatioNull(_GLFWwindow* window, int n, int d);
 void _glfwGetFramebufferSizeNull(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeNull(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleNull(_GLFWwindow* window, float* xscale, float* yscale);
+float _glfwGetWindowSdrWhiteLevelNull(_GLFWwindow* window);
 void _glfwIconifyWindowNull(_GLFWwindow* window);
 void _glfwRestoreWindowNull(_GLFWwindow* window);
 void _glfwMaximizeWindowNull(_GLFWwindow* window);
diff --git a/src/null_window.c b/src/null_window.c
index 32d0d1835e..37ae0af45e 100644
--- a/src/null_window.c
+++ b/src/null_window.c
@@ -242,10 +242,9 @@ void _glfwSetWindowPosNull(_GLFWwindow* window, int xpos, int ypos)
     }
 }
 
-GLFWhdrconfig* _glfwGetHDRConfigNull(_GLFWwindow* window)
+float _glfwGetWindowSdrWhiteLevelNull(_GLFWwindow* window)
 {
-    // Null does not support HDR
-    return NULL;
+    return 80.0f;
 }
 
 void _glfwGetWindowSizeNull(_GLFWwindow* window, int* width, int* height)
diff --git a/src/win32_platform.h b/src/win32_platform.h
index 128f72a2d5..d9daefaa6d 100644
--- a/src/win32_platform.h
+++ b/src/win32_platform.h
@@ -489,7 +489,6 @@ void _glfwSetWindowTitleWin32(_GLFWwindow* window, const char* title);
 void _glfwSetWindowIconWin32(_GLFWwindow* window, int count, const GLFWimage* images);
 void _glfwGetWindowPosWin32(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosWin32(_GLFWwindow* window, int xpos, int ypos);
-GLFWhdrconfig* _glfwGetHDRConfigWin32(_GLFWwindow* window);
 void _glfwGetWindowSizeWin32(_GLFWwindow* window, int* width, int* height);
 void _glfwSetWindowSizeWin32(_GLFWwindow* window, int width, int height);
 void _glfwSetWindowSizeLimitsWin32(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight);
@@ -497,6 +496,7 @@ void _glfwSetWindowAspectRatioWin32(_GLFWwindow* window, int numer, int denom);
 void _glfwGetFramebufferSizeWin32(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeWin32(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleWin32(_GLFWwindow* window, float* xscale, float* yscale);
+float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window);
 void _glfwIconifyWindowWin32(_GLFWwindow* window);
 void _glfwRestoreWindowWin32(_GLFWwindow* window);
 void _glfwMaximizeWindowWin32(_GLFWwindow* window);
diff --git a/src/win32_window.c b/src/win32_window.c
index 42dcf5b10f..85b5ec0c92 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -1651,7 +1651,7 @@ void _glfwSetWindowPosWin32(_GLFWwindow* window, int xpos, int ypos)
                  SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOSIZE);
 }
 
-float GetDisplayWhitePoint() {
+float _glfwGetWindowSdrWhiteLevel(_GLFWwindow* window) {
     UINT32 numPaths, numModes;
     LONG result;
 
@@ -1679,7 +1679,7 @@ float GetDisplayWhitePoint() {
         return FALSE;
     }
 
-    // Find the first active path
+    // Find the first active path TODO: find path for the current window
     for (UINT32 i = 0; i < numPaths; i++) {
         if (paths[i].flags & DISPLAYCONFIG_PATH_ACTIVE) {
             DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName;
@@ -1717,59 +1717,6 @@ float GetDisplayWhitePoint() {
     return 80.0f; // sRGB standard white level
 }
 
-GLFWhdrconfig* _glfwGetHDRConfigWin32(_GLFWwindow* window)
-{
-    IDXGIFactory6* factory = NULL;
-    IDXGIAdapter4* adapter = NULL;
-    IDXGIOutput6* output = NULL;
-    DXGI_OUTPUT_DESC1 desc;
-
-    GLFWhdrconfig* config = NULL;
-
-    HRESULT hr = CreateDXGIFactory2(0, &IID_IDXGIFactory6, (void**)&factory);
-    if (SUCCEEDED(hr))
-    {
-        // TODO: loop instead of hardcoding 0
-        hr = factory->lpVtbl->EnumAdapters(factory, 0, (IDXGIAdapter**)&adapter);
-        if (SUCCEEDED(hr))
-        {
-            hr = adapter->lpVtbl->EnumOutputs(adapter, 0, (IDXGIOutput**)&output);
-            if (SUCCEEDED(hr))
-            {
-                hr = output->lpVtbl->GetDesc1(output, &desc);
-                if (SUCCEEDED(hr))
-                {
-                    config = _glfw_calloc(sizeof(GLFWhdrconfig));
-
-                    // Surface colorspace is scRGB, i.e. sRGB primaries with linear transfer
-                    // See https://learn.microsoft.com/en-us/windows/win32/direct3darticles/high-dynamic-range
-                    config->primaries = 1; // sRGB H.273
-                    config->transfer_function = 8; // linear H.273
-                    
-                    config->output_display_primary_red_x = desc.RedPrimary[0];
-                    config->output_display_primary_red_y = desc.RedPrimary[1];
-                    config->output_display_primary_green_x = desc.GreenPrimary[0];
-                    config->output_display_primary_green_y = desc.GreenPrimary[1];
-                    config->output_display_primary_blue_x = desc.BluePrimary[0];
-                    config->output_display_primary_blue_y = desc.BluePrimary[1];
-                    config->output_white_point_x = desc.WhitePoint[0];
-                    config->output_white_point_y = desc.WhitePoint[1];
-                    config->max_luminance = desc.MaxLuminance;
-                    config->min_luminance = desc.MinLuminance;
-                    config->max_full_frame_luminance = desc.MaxFullFrameLuminance;
-                    config->sdr_white_level = GetDisplayWhitePoint();
-                }
-            }
-        }
-    }
-
-    if (output) output->lpVtbl->Release(output);
-    if (adapter) adapter->lpVtbl->Release(adapter);
-    if (factory) factory->lpVtbl->Release(factory);
-
-    return config;
-}
-
 void _glfwGetWindowSizeWin32(_GLFWwindow* window, int* width, int* height)
 {
     RECT area;
diff --git a/src/window.c b/src/window.c
index b1e370f1b0..b5d3faf8db 100644
--- a/src/window.c
+++ b/src/window.c
@@ -612,14 +612,14 @@ GLFWAPI void glfwSetWindowPos(GLFWwindow* handle, int xpos, int ypos)
     _glfw.platform.setWindowPos(window, xpos, ypos);
 }
 
-GLFWAPI const GLFWhdrconfig* glfwGetHDRConfig(GLFWwindow* handle)
+GLFWAPI float glfwGetWindowSdrWhiteLevel(GLFWwindow* handle)
 {
-    _GLFW_REQUIRE_INIT_OR_RETURN(NULL);
+    _GLFW_REQUIRE_INIT_OR_RETURN(0.f);
 
     _GLFWwindow* window = (_GLFWwindow*) handle;
     assert(window != NULL);
 
-    return _glfw.platform.getHDRConfig(window);
+    return _glfw.platform.getWindowSdrWhiteLevel(window);
 }
 
 GLFWAPI void glfwGetWindowSize(GLFWwindow* handle, int* width, int* height)
diff --git a/src/wl_init.c b/src/wl_init.c
index ba7c25747d..228385a07a 100644
--- a/src/wl_init.c
+++ b/src/wl_init.c
@@ -577,7 +577,6 @@ GLFWbool _glfwConnectWayland(int platformID, _GLFWplatform* platform)
         .setWindowIcon = _glfwSetWindowIconWayland,
         .getWindowPos = _glfwGetWindowPosWayland,
         .setWindowPos = _glfwSetWindowPosWayland,
-        .getHDRConfig = _glfwGetHDRConfigWayland,
         .getWindowSize = _glfwGetWindowSizeWayland,
         .setWindowSize = _glfwSetWindowSizeWayland,
         .setWindowSizeLimits = _glfwSetWindowSizeLimitsWayland,
@@ -585,6 +584,7 @@ GLFWbool _glfwConnectWayland(int platformID, _GLFWplatform* platform)
         .getFramebufferSize = _glfwGetFramebufferSizeWayland,
         .getWindowFrameSize = _glfwGetWindowFrameSizeWayland,
         .getWindowContentScale = _glfwGetWindowContentScaleWayland,
+        .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelWayland,
         .iconifyWindow = _glfwIconifyWindowWayland,
         .restoreWindow = _glfwRestoreWindowWayland,
         .maximizeWindow = _glfwMaximizeWindowWayland,
diff --git a/src/wl_platform.h b/src/wl_platform.h
index 7a2b620206..1d0dc7df8b 100644
--- a/src/wl_platform.h
+++ b/src/wl_platform.h
@@ -385,8 +385,6 @@ typedef struct _GLFWwindowWayland
         struct libdecor_frame*  frame;
     } libdecor;
 
-    GLFWhdrconfig*              hdrConfig;
-
     _GLFWcursor*                currentCursor;
     double                      cursorPosX, cursorPosY;
 
@@ -641,7 +639,6 @@ void _glfwSetWindowTitleWayland(_GLFWwindow* window, const char* title);
 void _glfwSetWindowIconWayland(_GLFWwindow* window, int count, const GLFWimage* images);
 void _glfwGetWindowPosWayland(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosWayland(_GLFWwindow* window, int xpos, int ypos);
-GLFWhdrconfig* _glfwGetHDRConfigWayland(_GLFWwindow* window);
 void _glfwGetWindowSizeWayland(_GLFWwindow* window, int* width, int* height);
 void _glfwSetWindowSizeWayland(_GLFWwindow* window, int width, int height);
 void _glfwSetWindowSizeLimitsWayland(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight);
@@ -649,6 +646,7 @@ void _glfwSetWindowAspectRatioWayland(_GLFWwindow* window, int numer, int denom)
 void _glfwGetFramebufferSizeWayland(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeWayland(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleWayland(_GLFWwindow* window, float* xscale, float* yscale);
+float _glfwGetWindowSdrWhiteLevelWayland(_GLFWwindow* window);
 void _glfwIconifyWindowWayland(_GLFWwindow* window);
 void _glfwRestoreWindowWayland(_GLFWwindow* window);
 void _glfwMaximizeWindowWayland(_GLFWwindow* window);
diff --git a/src/wl_window.c b/src/wl_window.c
index 0ce39b6fc9..5066c64a21 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -564,188 +564,85 @@ void imageDescriptionHandleDone(void *userData, struct wp_image_description_info
     wp_image_description_info_v1_destroy(image_description_info);
 }
 
+// TODO: consider using the following callbacks to get information about the displayable color volume of the display such that
+// users can clip their colors to the displayable color volume. (E.g. to avoid the display itself clipping the colors or renormalizing them unintentionally)
 void imageDescriptionHandleIccFile(void *userData, struct wp_image_description_info_v1 *image_description_info, int32_t icc, uint32_t icc_size)
 {
-    printf("Wayland: ICC profile support is not implemented yet.\n");
 }
 
 void imageDescriptionHandlePrimaries(void *userData, struct wp_image_description_info_v1 *image_description_info, 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)
 {
-    _GLFWwindow* window = userData;
-    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
-    if (!hdrConfig)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Wayland: No HDR config available for window");
-        return;
-    }
-
-    float surface_primary_red_x = (float)r_x / WAYLAND_COLOR_FACTOR;
-    float surface_primary_red_y = (float)r_y / WAYLAND_COLOR_FACTOR;
-    float surface_primary_green_x = (float)g_x / WAYLAND_COLOR_FACTOR;
-    float surface_primary_green_y = (float)g_y / WAYLAND_COLOR_FACTOR;
-    float surface_primary_blue_x = (float)b_x / WAYLAND_COLOR_FACTOR;
-    float surface_primary_blue_y = (float)b_y / WAYLAND_COLOR_FACTOR;
-    float surface_white_point_x = (float)w_x / WAYLAND_COLOR_FACTOR;
-    float surface_white_point_y = (float)w_y / WAYLAND_COLOR_FACTOR;
-
-    printf("Wayland: Surface primaries: R(%f, %f), G(%f, %f), B(%f, %f), W(%f, %f)\n",
-          surface_primary_red_x, surface_primary_red_y,
-          surface_primary_green_x, surface_primary_green_y,
-          surface_primary_blue_x, surface_primary_blue_y,
-          surface_white_point_x, surface_white_point_y);
 }
 
 void imageDescriptionHandlePrimariesNamed(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t primaries)
 {
-    _GLFWwindow* window = userData;
-    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
-    if (!hdrConfig)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Wayland: No HDR config available for window");
-        return;
-    }
-
-    // Translate Wayland transfer function to H.273 code points 
-    switch (primaries)
-    {
-        case WP_COLOR_MANAGER_V1_PRIMARIES_SRGB: hdrConfig->primaries = 1; break;
-        case WP_COLOR_MANAGER_V1_PRIMARIES_PAL_M: hdrConfig->primaries = 4; break;
-        case WP_COLOR_MANAGER_V1_PRIMARIES_PAL: hdrConfig->primaries = 5; break;
-        case WP_COLOR_MANAGER_V1_PRIMARIES_NTSC: hdrConfig->primaries = 6; break;
-        case WP_COLOR_MANAGER_V1_PRIMARIES_GENERIC_FILM: hdrConfig->primaries = 8; break;
-        case WP_COLOR_MANAGER_V1_PRIMARIES_BT2020: hdrConfig->primaries = 9; break;
-        case WP_COLOR_MANAGER_V1_PRIMARIES_CIE1931_XYZ: hdrConfig->primaries = 10; break;
-        case WP_COLOR_MANAGER_V1_PRIMARIES_DCI_P3: hdrConfig->primaries = 11; break;
-        case WP_COLOR_MANAGER_V1_PRIMARIES_DISPLAY_P3: hdrConfig->primaries = 12; break;
-        case WP_COLOR_MANAGER_V1_PRIMARIES_ADOBE_RGB: hdrConfig->primaries = 256; break; // Adobe RGB is not defined in H.273, use 256 as a placeholder
-        default:
-            _glfwInputError(GLFW_PLATFORM_ERROR,
-                            "Wayland: Unknown primaries %d", primaries);
-            return;
-    }
+    // // Translate Wayland transfer function to H.273 code points 
+    // switch (primaries)
+    // {
+    //     case WP_COLOR_MANAGER_V1_PRIMARIES_SRGB: hdrConfig->primaries = 1; break;
+    //     case WP_COLOR_MANAGER_V1_PRIMARIES_PAL_M: hdrConfig->primaries = 4; break;
+    //     case WP_COLOR_MANAGER_V1_PRIMARIES_PAL: hdrConfig->primaries = 5; break;
+    //     case WP_COLOR_MANAGER_V1_PRIMARIES_NTSC: hdrConfig->primaries = 6; break;
+    //     case WP_COLOR_MANAGER_V1_PRIMARIES_GENERIC_FILM: hdrConfig->primaries = 8; break;
+    //     case WP_COLOR_MANAGER_V1_PRIMARIES_BT2020: hdrConfig->primaries = 9; break;
+    //     case WP_COLOR_MANAGER_V1_PRIMARIES_CIE1931_XYZ: hdrConfig->primaries = 10; break;
+    //     case WP_COLOR_MANAGER_V1_PRIMARIES_DCI_P3: hdrConfig->primaries = 11; break;
+    //     case WP_COLOR_MANAGER_V1_PRIMARIES_DISPLAY_P3: hdrConfig->primaries = 12; break;
+    //     case WP_COLOR_MANAGER_V1_PRIMARIES_ADOBE_RGB: hdrConfig->primaries = 256; break; // Adobe RGB is not defined in H.273, use 256 as a placeholder
+    //     default:
+    //         _glfwInputError(GLFW_PLATFORM_ERROR,
+    //                         "Wayland: Unknown primaries %d", primaries);
+    //         return;
+    // }
 }
 
 void imageDescriptionHandlePower(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t eexp)
 {
-    printf("Wayland: tfPower: %d.\n", eexp);
 }
 
 void imageDescriptionHandleTransferFunctionNamed(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t tf)
 {
-    _GLFWwindow* window = userData;
-    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
-    if (!hdrConfig)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Wayland: No HDR config available for window");
-        return;
-    }
-
-    // Translate Wayland transfer function to H.273 code points 
-    switch (tf)
-    {
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_BT1886: hdrConfig->transfer_function = 1; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22: hdrConfig->transfer_function = 4; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA28: hdrConfig->transfer_function = 5; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST240: hdrConfig->transfer_function = 7; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR: hdrConfig->transfer_function = 8; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_100: hdrConfig->transfer_function = 9; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_316: hdrConfig->transfer_function = 10; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_XVYCC: hdrConfig->transfer_function = 11; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB: hdrConfig->transfer_function = 13; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB: hdrConfig->transfer_function = 13; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ: hdrConfig->transfer_function = 16; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST428: hdrConfig->transfer_function = 17; break;
-        case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG: hdrConfig->transfer_function = 18; break;
-        default:
-            _glfwInputError(GLFW_PLATFORM_ERROR,
-                            "Wayland: Unknown transfer function %d", tf);
-            return;
-    }
+    // // Translate Wayland transfer function to H.273 code points 
+    // switch (tf)
+    // {
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_BT1886: hdrConfig->transfer_function = 1; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22: hdrConfig->transfer_function = 4; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA28: hdrConfig->transfer_function = 5; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST240: hdrConfig->transfer_function = 7; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR: hdrConfig->transfer_function = 8; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_100: hdrConfig->transfer_function = 9; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_316: hdrConfig->transfer_function = 10; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_XVYCC: hdrConfig->transfer_function = 11; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB: hdrConfig->transfer_function = 13; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB: hdrConfig->transfer_function = 13; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ: hdrConfig->transfer_function = 16; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST428: hdrConfig->transfer_function = 17; break;
+    //     case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG: hdrConfig->transfer_function = 18; break;
+    //     default:
+    //         _glfwInputError(GLFW_PLATFORM_ERROR,
+    //                         "Wayland: Unknown transfer function %d", tf);
+    //         return;
+    // }
 }
 
 void imageDescriptionHandleLuminances(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum)
 {
-    _GLFWwindow* window = userData;
-    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
-    if (!hdrConfig)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Wayland: No HDR config available for window");
-        return;
-    }
-
-    float surface_min_luminance = (float)min_lum / WAYLAND_MIN_LUMINANCE_FACTOR;
-    float surface_max_luminance = (float)max_lum;
-    float surface_reference_luminance = (float)reference_lum;
-
-    printf("Wayland: Surface min luminance: %f, Max luminance: %f, Reference luminance: %f\n", surface_min_luminance, surface_max_luminance, surface_reference_luminance);
 }
 
 void imageDescriptionHandleTargetPrimaries(void *userData, struct wp_image_description_info_v1 *image_description_info, 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)
 {
-    _GLFWwindow* window = userData;
-    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
-    if (!hdrConfig)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Wayland: No HDR config available for window");
-        return;
-    }
-
-    hdrConfig->output_display_primary_red_x = (float)r_x / WAYLAND_COLOR_FACTOR;
-    hdrConfig->output_display_primary_red_y = (float)r_y / WAYLAND_COLOR_FACTOR;
-    hdrConfig->output_display_primary_green_x = (float)g_x / WAYLAND_COLOR_FACTOR;
-    hdrConfig->output_display_primary_green_y = (float)g_y / WAYLAND_COLOR_FACTOR;
-    hdrConfig->output_display_primary_blue_x = (float)b_x / WAYLAND_COLOR_FACTOR;
-    hdrConfig->output_display_primary_blue_y = (float)b_y / WAYLAND_COLOR_FACTOR;
-    hdrConfig->output_white_point_x = (float)w_x / WAYLAND_COLOR_FACTOR;
-    hdrConfig->output_white_point_y = (float)w_y / WAYLAND_COLOR_FACTOR;
-
-    printf("Wayland: Target primaries: R(%f, %f), G(%f, %f), B(%f, %f), W(%f, %f)\n",
-           hdrConfig->output_display_primary_red_x, hdrConfig->output_display_primary_red_y,
-           hdrConfig->output_display_primary_green_x, hdrConfig->output_display_primary_green_y,
-           hdrConfig->output_display_primary_blue_x, hdrConfig->output_display_primary_blue_y,
-           hdrConfig->output_white_point_x, hdrConfig->output_white_point_y);
 }
 
 void imageDescriptionHandleTargetLuminances(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t min_lum, uint32_t max_lum)
 {
-    _GLFWwindow* window = userData;
-    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
-    if (!hdrConfig)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Wayland: No HDR config available for window");
-        return;
-    }
-
-    hdrConfig->min_luminance = (float)min_lum / WAYLAND_MIN_LUMINANCE_FACTOR;
-    hdrConfig->max_luminance = (float)max_lum;
-
-    printf("Wayland: Target min luminance: %f, Max luminance: %f\n", hdrConfig->min_luminance, hdrConfig->max_luminance);
 }
 
 void imageDescriptionHandleTargetMaxCll(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t max_cll)
 {
-    printf("Wayland: Max CLL: %d\n", max_cll);
 }
 
 void imageDescriptionHandleTargetMaxFall(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t max_fall)
 {
-    _GLFWwindow* window = userData;
-    GLFWhdrconfig* hdrConfig = window->wl.hdrConfig;
-    if (!hdrConfig)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Wayland: No HDR config available for window");
-        return;
-    }
-
-    hdrConfig->max_full_frame_luminance = (float)max_fall;
-    printf("Wayland: Max FALL: %d\n", max_fall);
 }
 
 const struct wp_image_description_info_v1_listener imageDescriptionListener = {
@@ -786,9 +683,6 @@ void getPreferredImageDescription(_GLFWwindow* window)
         return;
     }
 
-    window->wl.hdrConfig = _glfw_realloc(window->wl.hdrConfig, sizeof(GLFWhdrconfig));
-    memset(window->wl.hdrConfig, 0, sizeof(GLFWhdrconfig));
-
     wp_image_description_info_v1_add_listener(preferredInfo, &imageDescriptionListener, window);
 }
 
@@ -1320,7 +1214,6 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
 
     window->wl.colorSurface = NULL;
     window->wl.colorSurfaceFeedback = NULL;
-    window->wl.hdrConfig = NULL;
 
     enum wp_color_manager_v1_primaries primaries = WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
     
@@ -1355,7 +1248,7 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
     }
 
     if (supportsHdr) {
-        // Set up color surface & get its preferred image description (populated GLFWhdrconfig)
+        // Set up color surface & get its preferred image description (currently nothing is done with the preferred image description)
         {
             window->wl.colorSurface = wp_color_manager_v1_get_surface(_glfw.wl.colorManager, window->wl.surface);
             window->wl.colorSurfaceFeedback = wp_color_manager_v1_get_surface_feedback(_glfw.wl.colorManager, window->wl.surface);
@@ -2587,9 +2480,10 @@ void _glfwSetWindowPosWayland(_GLFWwindow* window, int xpos, int ypos)
                     "Wayland: The platform does not support setting the window position");
 }
 
-GLFWhdrconfig* _glfwGetHDRConfigWayland(_GLFWwindow* window)
+float _glfwGetWindowSdrWhiteLevelWayland(_GLFWwindow* window)
 {
-    return window->wl.hdrConfig;
+    // TODO: when enabling non-sRGB transfer functions, like EXT_LINEAR, this should return the display's white level in nits
+    return 80.0f;
 }
 
 void _glfwGetWindowSizeWayland(_GLFWwindow* window, int* width, int* height)
diff --git a/src/x11_platform.h b/src/x11_platform.h
index 22541f5fda..622ac618ae 100644
--- a/src/x11_platform.h
+++ b/src/x11_platform.h
@@ -909,7 +909,6 @@ void _glfwSetWindowTitleX11(_GLFWwindow* window, const char* title);
 void _glfwSetWindowIconX11(_GLFWwindow* window, int count, const GLFWimage* images);
 void _glfwGetWindowPosX11(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosX11(_GLFWwindow* window, int xpos, int ypos);
-GLFWhdrconfig* _glfwGetHDRConfigX11(_GLFWwindow* window);
 void _glfwGetWindowSizeX11(_GLFWwindow* window, int* width, int* height);
 void _glfwSetWindowSizeX11(_GLFWwindow* window, int width, int height);
 void _glfwSetWindowSizeLimitsX11(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight);
@@ -917,6 +916,7 @@ void _glfwSetWindowAspectRatioX11(_GLFWwindow* window, int numer, int denom);
 void _glfwGetFramebufferSizeX11(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeX11(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleX11(_GLFWwindow* window, float* xscale, float* yscale);
+float _glfwGetWindowSdrWhiteLevelX11(_GLFWwindow* window);
 void _glfwIconifyWindowX11(_GLFWwindow* window);
 void _glfwRestoreWindowX11(_GLFWwindow* window);
 void _glfwMaximizeWindowX11(_GLFWwindow* window);
diff --git a/src/x11_window.c b/src/x11_window.c
index 9e82841602..f2479363af 100644
--- a/src/x11_window.c
+++ b/src/x11_window.c
@@ -2190,9 +2190,9 @@ void _glfwSetWindowPosX11(_GLFWwindow* window, int xpos, int ypos)
     XFlush(_glfw.x11.display);
 }
 
-GLFWhdrconfig* _glfwGetHDRConfigX11(_GLFWwindow* window)
+float _glfwGetWindowSdrWhiteLevelX11(_GLFWwindow* window)
 {
-    return NULL; // X11 does not support HDR
+    return 80.0f;
 }
 
 void _glfwGetWindowSizeX11(_GLFWwindow* window, int* width, int* height)

From 2da94c4390d169083fb3e817ca18ceac563a2df8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <tom@94.me>
Date: Thu, 17 Jul 2025 17:15:46 +0200
Subject: [PATCH 09/40] fix(windows): compilation, mapping, return sdr white
 only for active monitor

---
 src/egl_context.c  |  1 -
 src/win32_init.c   |  1 +
 src/win32_window.c | 84 ++++++++++++++++++++++++++++------------------
 3 files changed, 53 insertions(+), 33 deletions(-)

diff --git a/src/egl_context.c b/src/egl_context.c
index dc31ccb805..406fa17e66 100644
--- a/src/egl_context.c
+++ b/src/egl_context.c
@@ -26,7 +26,6 @@
 //========================================================================
 
 #include "internal.h"
-#include "color-management-v1-client-protocol.h"
 
 #include <stdio.h>
 #include <string.h>
diff --git a/src/win32_init.c b/src/win32_init.c
index 6b6e9d08ea..968934708f 100644
--- a/src/win32_init.c
+++ b/src/win32_init.c
@@ -640,6 +640,7 @@ GLFWbool _glfwConnectWin32(int platformID, _GLFWplatform* platform)
         .getFramebufferSize = _glfwGetFramebufferSizeWin32,
         .getWindowFrameSize = _glfwGetWindowFrameSizeWin32,
         .getWindowContentScale = _glfwGetWindowContentScaleWin32,
+        .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelWin32,
         .iconifyWindow = _glfwIconifyWindowWin32,
         .restoreWindow = _glfwRestoreWindowWin32,
         .maximizeWindow = _glfwMaximizeWindowWin32,
diff --git a/src/win32_window.c b/src/win32_window.c
index 85b5ec0c92..3a2a4e8343 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -1651,24 +1651,33 @@ void _glfwSetWindowPosWin32(_GLFWwindow* window, int xpos, int ypos)
                  SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOSIZE);
 }
 
-float _glfwGetWindowSdrWhiteLevel(_GLFWwindow* window) {
+float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window) {
     UINT32 numPaths, numModes;
     LONG result;
 
+    const TCHAR* monitorName = NULL;
+
+    HMONITOR hMonitor = MonitorFromWindow(window->win32.handle, MONITOR_DEFAULTTONEAREST);
+    MONITORINFOEX monitorInfo;
+    monitorInfo.cbSize = sizeof(MONITORINFOEX);
+    if (GetMonitorInfo(hMonitor, &monitorInfo)) {
+        monitorName = monitorInfo.szDevice;
+    }
+
     // Get the number of paths and modes
     result = GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &numPaths, &numModes);
     if (result != ERROR_SUCCESS) {
-        return FALSE;
+        return 80.0f;
     }
 
     // Allocate memory for the paths and modes
-    DISPLAYCONFIG_PATH_INFO* paths = _glfw_calloc(numPaths * sizeof(DISPLAYCONFIG_PATH_INFO));
-    DISPLAYCONFIG_MODE_INFO* modes = _glfw_calloc(numModes * sizeof(DISPLAYCONFIG_MODE_INFO));
+    DISPLAYCONFIG_PATH_INFO* paths = _glfw_calloc(numPaths, sizeof(DISPLAYCONFIG_PATH_INFO));
+    DISPLAYCONFIG_MODE_INFO* modes = _glfw_calloc(numModes, sizeof(DISPLAYCONFIG_MODE_INFO));
 
     if (!paths || !modes) {
         _glfw_free(paths);
         _glfw_free(modes);
-        return FALSE;
+        return 80.0f;
     }
 
     // Query the display configuration
@@ -1676,38 +1685,49 @@ float _glfwGetWindowSdrWhiteLevel(_GLFWwindow* window) {
     if (result != ERROR_SUCCESS) {
         _glfw_free(paths);
         _glfw_free(modes);
-        return FALSE;
+        return 80.0f;
     }
 
     // Find the first active path TODO: find path for the current window
     for (UINT32 i = 0; i < numPaths; i++) {
-        if (paths[i].flags & DISPLAYCONFIG_PATH_ACTIVE) {
-            DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName;
-            sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
-            sourceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME);
-            sourceName.header.adapterId = paths[i].sourceInfo.adapterId;
-            sourceName.header.id = paths[i].sourceInfo.id;
-
-            result = DisplayConfigGetDeviceInfo(&sourceName.header);
-            if (result == ERROR_SUCCESS) {
-                DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO advanced_color_info = {};
-                advanced_color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
-                advanced_color_info.header.size = sizeof(advanced_color_info);
-                advanced_color_info.header.adapterId = paths[i].targetInfo.adapterId;
-                advanced_color_info.header.id = paths[i].targetInfo.id;
-                result = DisplayConfigGetDeviceInfo(&advanced_color_info.header);
-
-                if (result == ERROR_SUCCESS && advanced_color_info.advancedColorEnabled > 0) {
-                    DISPLAYCONFIG_SDR_WHITE_LEVEL white_level = {};
-                    white_level.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
-                    white_level.header.size = sizeof(white_level);
-                    white_level.header.adapterId = paths[i].targetInfo.adapterId;
-                    white_level.header.id = paths[i].targetInfo.id;
-                    if (DisplayConfigGetDeviceInfo(&white_level.header) == ERROR_SUCCESS)
-                        return white_level.SDRWhiteLevel;  // From wingdi.h.
-                }
-            }
+        if (!(paths[i].flags & DISPLAYCONFIG_PATH_ACTIVE)) {
+            continue;
         }
+
+        DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName;
+        sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
+        sourceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME);
+        sourceName.header.adapterId = paths[i].sourceInfo.adapterId;
+        sourceName.header.id = paths[i].sourceInfo.id;
+
+        result = DisplayConfigGetDeviceInfo(&sourceName.header);
+
+        // If we have a monitor name, only check for sdr white level on the monitor that it matches
+        if (result != ERROR_SUCCESS || (monitorName && wcscmp(sourceName.viewGdiDeviceName, monitorName) != 0)) {
+            continue;
+        }
+
+        DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO advanced_color_info = {};
+        advanced_color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
+        advanced_color_info.header.size = sizeof(advanced_color_info);
+        advanced_color_info.header.adapterId = paths[i].targetInfo.adapterId;
+        advanced_color_info.header.id = paths[i].targetInfo.id;
+        result = DisplayConfigGetDeviceInfo(&advanced_color_info.header);
+
+        if (result != ERROR_SUCCESS || advanced_color_info.advancedColorEnabled == 0) {
+            continue;
+        }
+
+        DISPLAYCONFIG_SDR_WHITE_LEVEL white_level = {};
+        white_level.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
+        white_level.header.size = sizeof(white_level);
+        white_level.header.adapterId = paths[i].targetInfo.adapterId;
+        white_level.header.id = paths[i].targetInfo.id;
+        if (DisplayConfigGetDeviceInfo(&white_level.header) != ERROR_SUCCESS) {
+            continue;
+        }
+
+        return white_level.SDRWhiteLevel / 1000.0f * 80.0f;
     }
 
     _glfw_free(paths);

From bda6331e047de3a16cdc9118e990b1f6bd107aa6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <tom@94.me>
Date: Thu, 17 Jul 2025 17:15:57 +0200
Subject: [PATCH 10/40] fix(x11): routing of sdr white

---
 src/x11_init.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/x11_init.c b/src/x11_init.c
index 6b34c2639a..a250db9e04 100644
--- a/src/x11_init.c
+++ b/src/x11_init.c
@@ -1217,6 +1217,7 @@ GLFWbool _glfwConnectX11(int platformID, _GLFWplatform* platform)
         .getFramebufferSize = _glfwGetFramebufferSizeX11,
         .getWindowFrameSize = _glfwGetWindowFrameSizeX11,
         .getWindowContentScale = _glfwGetWindowContentScaleX11,
+        .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelX11,
         .iconifyWindow = _glfwIconifyWindowX11,
         .restoreWindow = _glfwRestoreWindowX11,
         .maximizeWindow = _glfwMaximizeWindowX11,

From 5f10deb86c7dfaf8ec7bb67a3f5045e8efb9a88e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Fri, 18 Jul 2025 10:00:35 +0200
Subject: [PATCH 11/40] fix(wayland): support 5th rendering intent

---
 src/wl_platform.h | 2 +-
 src/wl_window.c   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/wl_platform.h b/src/wl_platform.h
index 1d0dc7df8b..7d13285b74 100644
--- a/src/wl_platform.h
+++ b/src/wl_platform.h
@@ -454,7 +454,7 @@ typedef struct _GLFWlibraryWayland
 
         GLFWbool primaries[11];
         GLFWbool tfs[14];
-        GLFWbool intents[4];
+        GLFWbool intents[5];
     } colorManagerSupport;
 
     _GLFWofferWayland*          offers;
diff --git a/src/wl_window.c b/src/wl_window.c
index 5066c64a21..81081fbdf8 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1225,7 +1225,7 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
     }
 
     enum wp_color_manager_v1_render_intent intent = WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL;
-    for (int i = 0; i < 4; ++i)
+    for (int i = 0; i < 5; ++i)
     {
         if (_glfw.wl.colorManagerSupport.intents[i])
         {

From 1df03b964962741db330fdcd1c7df70d7fcfbbb9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <tmueller@nvidia.com>
Date: Fri, 18 Jul 2025 09:06:08 +0200
Subject: [PATCH 12/40] fix(windows): compilation on older MSVC

---
 src/win32_window.c | 31 +++++++++++++++++--------------
 1 file changed, 17 insertions(+), 14 deletions(-)

diff --git a/src/win32_window.c b/src/win32_window.c
index 3a2a4e8343..46bc1a4f98 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -1695,6 +1695,7 @@ float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window) {
         }
 
         DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName;
+        memset(&sourceName, 0, sizeof(sourceName));
         sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
         sourceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME);
         sourceName.header.adapterId = paths[i].sourceInfo.adapterId;
@@ -1707,27 +1708,29 @@ float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window) {
             continue;
         }
 
-        DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO advanced_color_info = {};
-        advanced_color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
-        advanced_color_info.header.size = sizeof(advanced_color_info);
-        advanced_color_info.header.adapterId = paths[i].targetInfo.adapterId;
-        advanced_color_info.header.id = paths[i].targetInfo.id;
-        result = DisplayConfigGetDeviceInfo(&advanced_color_info.header);
+        DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO advancedColorInfo;
+        memset(&advancedColorInfo, 0, sizeof(advancedColorInfo));
+        advancedColorInfo.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
+        advancedColorInfo.header.size = sizeof(advancedColorInfo);
+        advancedColorInfo.header.adapterId = paths[i].targetInfo.adapterId;
+        advancedColorInfo.header.id = paths[i].targetInfo.id;
+        result = DisplayConfigGetDeviceInfo(&advancedColorInfo.header);
 
-        if (result != ERROR_SUCCESS || advanced_color_info.advancedColorEnabled == 0) {
+        if (result != ERROR_SUCCESS || advancedColorInfo.advancedColorEnabled == 0) {
             continue;
         }
 
-        DISPLAYCONFIG_SDR_WHITE_LEVEL white_level = {};
-        white_level.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
-        white_level.header.size = sizeof(white_level);
-        white_level.header.adapterId = paths[i].targetInfo.adapterId;
-        white_level.header.id = paths[i].targetInfo.id;
-        if (DisplayConfigGetDeviceInfo(&white_level.header) != ERROR_SUCCESS) {
+        DISPLAYCONFIG_SDR_WHITE_LEVEL whiteLevel;
+        memset(&whiteLevel, 0, sizeof(whiteLevel));
+        whiteLevel.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
+        whiteLevel.header.size = sizeof(whiteLevel);
+        whiteLevel.header.adapterId = paths[i].targetInfo.adapterId;
+        whiteLevel.header.id = paths[i].targetInfo.id;
+        if (DisplayConfigGetDeviceInfo(&whiteLevel.header) != ERROR_SUCCESS) {
             continue;
         }
 
-        return white_level.SDRWhiteLevel / 1000.0f * 80.0f;
+        return whiteLevel.SDRWhiteLevel / 1000.0f * 80.0f;
     }
 
     _glfw_free(paths);

From 15c129575dce3f0832b618ecfbac1c0420864ae5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Sun, 20 Jul 2025 09:08:52 +0200
Subject: [PATCH 13/40] feat: expose display surface primaries and transfer
 function

---
 include/GLFW/glfw3.h |  2 ++
 src/cocoa_init.m     |  2 ++
 src/cocoa_platform.h |  2 ++
 src/cocoa_window.m   | 10 ++++++++++
 src/internal.h       |  2 ++
 src/null_init.c      |  2 ++
 src/null_platform.h  |  2 ++
 src/null_window.c    | 10 ++++++++++
 src/win32_init.c     |  2 ++
 src/win32_platform.h |  2 ++
 src/win32_window.c   | 10 ++++++++++
 src/window.c         | 21 +++++++++++++++++++++
 src/wl_init.c        |  2 ++
 src/wl_platform.h    |  2 ++
 src/wl_window.c      | 29 +++++++++++++++++++++--------
 src/x11_init.c       |  2 ++
 src/x11_platform.h   |  2 ++
 src/x11_window.c     | 10 ++++++++++
 18 files changed, 106 insertions(+), 8 deletions(-)

diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h
index 3674cae9bd..fbae45b5b8 100644
--- a/include/GLFW/glfw3.h
+++ b/include/GLFW/glfw3.h
@@ -3740,6 +3740,8 @@ GLFWAPI void glfwGetWindowFrameSize(GLFWwindow* window, int* left, int* top, int
 GLFWAPI void glfwGetWindowContentScale(GLFWwindow* window, float* xscale, float* yscale);
 
 GLFWAPI float glfwGetWindowSdrWhiteLevel(GLFWwindow* window);
+GLFWAPI uint32_t glfwGetWindowPrimaries(GLFWwindow* window);
+GLFWAPI uint32_t glfwGetWindowTransfer(GLFWwindow* window);
 
 /*! @brief Returns the opacity of the whole window.
  *
diff --git a/src/cocoa_init.m b/src/cocoa_init.m
index bacf6f8be8..71904b1612 100644
--- a/src/cocoa_init.m
+++ b/src/cocoa_init.m
@@ -553,6 +553,8 @@ GLFWbool _glfwConnectCocoa(int platformID, _GLFWplatform* platform)
         .getWindowFrameSize = _glfwGetWindowFrameSizeCocoa,
         .getWindowContentScale = _glfwGetWindowContentScaleCocoa,
         .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelCocoa,
+        .getWindowPrimaries = _glfwGetWindowPrimariesCocoa,
+        .getWindowTransfer = _glfwGetWindowTransferCocoa,
         .iconifyWindow = _glfwIconifyWindowCocoa,
         .restoreWindow = _glfwRestoreWindowCocoa,
         .maximizeWindow = _glfwMaximizeWindowCocoa,
diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h
index 067234bcff..136e77076a 100644
--- a/src/cocoa_platform.h
+++ b/src/cocoa_platform.h
@@ -232,6 +232,8 @@ void _glfwGetFramebufferSizeCocoa(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeCocoa(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleCocoa(_GLFWwindow* window, float* xscale, float* yscale);
 float _glfwGetWindowSdrWhiteLevelCocoa(_GLFWwindow* window);
+uint32_t _glfwGetWindowPrimariesCocoa(_GLFWwindow* window);
+uint32_t _glfwGetWindowTransferCocoa(_GLFWwindow* window);
 void _glfwIconifyWindowCocoa(_GLFWwindow* window);
 void _glfwRestoreWindowCocoa(_GLFWwindow* window);
 void _glfwMaximizeWindowCocoa(_GLFWwindow* window);
diff --git a/src/cocoa_window.m b/src/cocoa_window.m
index 0385bbccf9..6aea32f07d 100644
--- a/src/cocoa_window.m
+++ b/src/cocoa_window.m
@@ -1069,6 +1069,16 @@ float _glfwGetWindowSdrWhiteLevelCocoa(_GLFWwindow* window)
     return 80.0f;
 }
 
+uint32_t _glfwGetWindowPrimariesCocoa(_GLFWwindow* window)
+{
+    return 1; // sRGB
+}
+
+uint32_t _glfwGetWindowTransferCocoa(_GLFWwindow* window)
+{
+    return 10; // EXT sRGB
+}
+
 void _glfwGetWindowSizeCocoa(_GLFWwindow* window, int* width, int* height)
 {
     @autoreleasepool {
diff --git a/src/internal.h b/src/internal.h
index 71a76f9006..41a54aa560 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -722,6 +722,8 @@ struct _GLFWplatform
     void (*getWindowPos)(_GLFWwindow*,int*,int*);
     void (*setWindowPos)(_GLFWwindow*,int,int);
     float (*getWindowSdrWhiteLevel)(_GLFWwindow*);
+    uint32_t (*getWindowPrimaries)(_GLFWwindow*);
+    uint32_t (*getWindowTransfer)(_GLFWwindow*);
     void (*getWindowSize)(_GLFWwindow*,int*,int*);
     void (*setWindowSize)(_GLFWwindow*,int,int);
     void (*setWindowSizeLimits)(_GLFWwindow*,int,int,int,int);
diff --git a/src/null_init.c b/src/null_init.c
index b4757dfb59..74f4f0bfd8 100644
--- a/src/null_init.c
+++ b/src/null_init.c
@@ -82,6 +82,8 @@ GLFWbool _glfwConnectNull(int platformID, _GLFWplatform* platform)
         .getWindowFrameSize = _glfwGetWindowFrameSizeNull,
         .getWindowContentScale = _glfwGetWindowContentScaleNull,
         .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelNull,
+        .getWindowPrimaries = _glfwGetWindowPrimariesNull,
+        .getWindowTransfer = _glfwGetWindowTransferNull,
         .iconifyWindow = _glfwIconifyWindowNull,
         .restoreWindow = _glfwRestoreWindowNull,
         .maximizeWindow = _glfwMaximizeWindowNull,
diff --git a/src/null_platform.h b/src/null_platform.h
index 08daebb4f6..aa1bae0531 100644
--- a/src/null_platform.h
+++ b/src/null_platform.h
@@ -234,6 +234,8 @@ void _glfwGetFramebufferSizeNull(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeNull(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleNull(_GLFWwindow* window, float* xscale, float* yscale);
 float _glfwGetWindowSdrWhiteLevelNull(_GLFWwindow* window);
+uint32_t _glfwGetWindowPrimariesNull(_GLFWwindow* window);
+uint32_t _glfwGetWindowTransferNull(_GLFWwindow* window);
 void _glfwIconifyWindowNull(_GLFWwindow* window);
 void _glfwRestoreWindowNull(_GLFWwindow* window);
 void _glfwMaximizeWindowNull(_GLFWwindow* window);
diff --git a/src/null_window.c b/src/null_window.c
index 37ae0af45e..054b134676 100644
--- a/src/null_window.c
+++ b/src/null_window.c
@@ -247,6 +247,16 @@ float _glfwGetWindowSdrWhiteLevelNull(_GLFWwindow* window)
     return 80.0f;
 }
 
+uint32_t _glfwGetWindowPrimariesNull(_GLFWwindow* window)
+{
+    return 1; // sRGB
+}
+
+uint32_t _glfwGetWindowTransferNull(_GLFWwindow* window)
+{
+    return 10; // EXT sRGB
+}
+
 void _glfwGetWindowSizeNull(_GLFWwindow* window, int* width, int* height)
 {
     if (width)
diff --git a/src/win32_init.c b/src/win32_init.c
index 968934708f..dd1045e4c8 100644
--- a/src/win32_init.c
+++ b/src/win32_init.c
@@ -641,6 +641,8 @@ GLFWbool _glfwConnectWin32(int platformID, _GLFWplatform* platform)
         .getWindowFrameSize = _glfwGetWindowFrameSizeWin32,
         .getWindowContentScale = _glfwGetWindowContentScaleWin32,
         .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelWin32,
+        .getWindowPrimaries = _glfwGetWindowPrimariesWin32,
+        .getWindowTransfer = _glfwGetWindowTransferWin32,
         .iconifyWindow = _glfwIconifyWindowWin32,
         .restoreWindow = _glfwRestoreWindowWin32,
         .maximizeWindow = _glfwMaximizeWindowWin32,
diff --git a/src/win32_platform.h b/src/win32_platform.h
index d9daefaa6d..bbf28a3672 100644
--- a/src/win32_platform.h
+++ b/src/win32_platform.h
@@ -497,6 +497,8 @@ void _glfwGetFramebufferSizeWin32(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeWin32(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleWin32(_GLFWwindow* window, float* xscale, float* yscale);
 float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window);
+uint32_t _glfwGetWindowPrimariesWin32(_GLFWwindow* window);
+uint32_t _glfwGetWindowTransferWin32(_GLFWwindow* window);
 void _glfwIconifyWindowWin32(_GLFWwindow* window);
 void _glfwRestoreWindowWin32(_GLFWwindow* window);
 void _glfwMaximizeWindowWin32(_GLFWwindow* window);
diff --git a/src/win32_window.c b/src/win32_window.c
index 46bc1a4f98..93774059aa 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -1740,6 +1740,16 @@ float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window) {
     return 80.0f; // sRGB standard white level
 }
 
+uint32_t _glfwGetWindowPrimariesWin32(_GLFWwindow* window)
+{
+    return 1; // sRGB
+}
+
+uint32_t _glfwGetWindowTransferWin32(_GLFWwindow* window)
+{
+    return 5; // linear
+}
+
 void _glfwGetWindowSizeWin32(_GLFWwindow* window, int* width, int* height)
 {
     RECT area;
diff --git a/src/window.c b/src/window.c
index b5d3faf8db..7da2d43b6e 100644
--- a/src/window.c
+++ b/src/window.c
@@ -622,6 +622,27 @@ GLFWAPI float glfwGetWindowSdrWhiteLevel(GLFWwindow* handle)
     return _glfw.platform.getWindowSdrWhiteLevel(window);
 }
 
+GLFWAPI uint32_t glfwGetWindowPrimaries(GLFWwindow* handle)
+{
+    _GLFW_REQUIRE_INIT_OR_RETURN(1); // sRGB
+
+    _GLFWwindow* window = (_GLFWwindow*) handle;
+    assert(window != NULL);
+
+    return _glfw.platform.getWindowPrimaries(window);
+}
+
+GLFWAPI uint32_t glfwGetWindowTransfer(GLFWwindow* handle)
+{
+    _GLFW_REQUIRE_INIT_OR_RETURN(10); // EXT sRGB
+
+    _GLFWwindow* window = (_GLFWwindow*) handle;
+    assert(window != NULL);
+
+    return _glfw.platform.getWindowTransfer(window);
+}
+
+
 GLFWAPI void glfwGetWindowSize(GLFWwindow* handle, int* width, int* height)
 {
     if (width)
diff --git a/src/wl_init.c b/src/wl_init.c
index 228385a07a..ac9a2d2de6 100644
--- a/src/wl_init.c
+++ b/src/wl_init.c
@@ -585,6 +585,8 @@ GLFWbool _glfwConnectWayland(int platformID, _GLFWplatform* platform)
         .getWindowFrameSize = _glfwGetWindowFrameSizeWayland,
         .getWindowContentScale = _glfwGetWindowContentScaleWayland,
         .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelWayland,
+        .getWindowPrimaries = _glfwGetWindowPrimariesWayland,
+        .getWindowTransfer = _glfwGetWindowTransferWayland,
         .iconifyWindow = _glfwIconifyWindowWayland,
         .restoreWindow = _glfwRestoreWindowWayland,
         .maximizeWindow = _glfwMaximizeWindowWayland,
diff --git a/src/wl_platform.h b/src/wl_platform.h
index 7d13285b74..9e393d8e2c 100644
--- a/src/wl_platform.h
+++ b/src/wl_platform.h
@@ -647,6 +647,8 @@ void _glfwGetFramebufferSizeWayland(_GLFWwindow* window, int* width, int* height
 void _glfwGetWindowFrameSizeWayland(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleWayland(_GLFWwindow* window, float* xscale, float* yscale);
 float _glfwGetWindowSdrWhiteLevelWayland(_GLFWwindow* window);
+uint32_t _glfwGetWindowPrimariesWayland(_GLFWwindow* window);
+uint32_t _glfwGetWindowTransferWayland(_GLFWwindow* window);
 void _glfwIconifyWindowWayland(_GLFWwindow* window);
 void _glfwRestoreWindowWayland(_GLFWwindow* window);
 void _glfwMaximizeWindowWayland(_GLFWwindow* window);
diff --git a/src/wl_window.c b/src/wl_window.c
index 81081fbdf8..874ef89a9c 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1215,14 +1215,9 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
     window->wl.colorSurface = NULL;
     window->wl.colorSurfaceFeedback = NULL;
 
-    enum wp_color_manager_v1_primaries primaries = WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
-    
-    enum wp_color_manager_v1_transfer_function tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
-    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB])
-    {
-        // Use the extended sRGB transfer function if available
-        tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB;
-    }
+    // These functions take into account the color management support of the compositor
+    enum wp_color_manager_v1_primaries primaries = _glfwGetWindowPrimariesWayland(window);
+    enum wp_color_manager_v1_transfer_function tf = _glfwGetWindowTransferWayland(window);
 
     enum wp_color_manager_v1_render_intent intent = WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL;
     for (int i = 0; i < 5; ++i)
@@ -2486,6 +2481,24 @@ float _glfwGetWindowSdrWhiteLevelWayland(_GLFWwindow* window)
     return 80.0f;
 }
 
+uint32_t _glfwGetWindowPrimariesWayland(_GLFWwindow* window)
+{
+    enum wp_color_manager_v1_primaries primaries = WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
+    if (_glfw.wl.colorManagerSupport.primaries[WP_COLOR_MANAGER_V1_PRIMARIES_BT2020])
+        primaries = WP_COLOR_MANAGER_V1_PRIMARIES_BT2020;
+    return primaries;
+}
+
+uint32_t _glfwGetWindowTransferWayland(_GLFWwindow* window)
+{
+    enum wp_color_manager_v1_transfer_function tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
+    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB])
+        tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB;
+    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ])
+        tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ;
+    return tf;
+}
+
 void _glfwGetWindowSizeWayland(_GLFWwindow* window, int* width, int* height)
 {
     if (width)
diff --git a/src/x11_init.c b/src/x11_init.c
index a250db9e04..97bac0bac5 100644
--- a/src/x11_init.c
+++ b/src/x11_init.c
@@ -1218,6 +1218,8 @@ GLFWbool _glfwConnectX11(int platformID, _GLFWplatform* platform)
         .getWindowFrameSize = _glfwGetWindowFrameSizeX11,
         .getWindowContentScale = _glfwGetWindowContentScaleX11,
         .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelX11,
+        .getWindowPrimaries = _glfwGetWindowPrimariesX11,
+        .getWindowTransfer = _glfwGetWindowTransferX11,
         .iconifyWindow = _glfwIconifyWindowX11,
         .restoreWindow = _glfwRestoreWindowX11,
         .maximizeWindow = _glfwMaximizeWindowX11,
diff --git a/src/x11_platform.h b/src/x11_platform.h
index 622ac618ae..42ad9df05b 100644
--- a/src/x11_platform.h
+++ b/src/x11_platform.h
@@ -917,6 +917,8 @@ void _glfwGetFramebufferSizeX11(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeX11(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleX11(_GLFWwindow* window, float* xscale, float* yscale);
 float _glfwGetWindowSdrWhiteLevelX11(_GLFWwindow* window);
+uint32_t _glfwGetWindowPrimariesX11(_GLFWwindow* window);
+uint32_t _glfwGetWindowTransferX11(_GLFWwindow* window);
 void _glfwIconifyWindowX11(_GLFWwindow* window);
 void _glfwRestoreWindowX11(_GLFWwindow* window);
 void _glfwMaximizeWindowX11(_GLFWwindow* window);
diff --git a/src/x11_window.c b/src/x11_window.c
index f2479363af..d06be39838 100644
--- a/src/x11_window.c
+++ b/src/x11_window.c
@@ -2195,6 +2195,16 @@ float _glfwGetWindowSdrWhiteLevelX11(_GLFWwindow* window)
     return 80.0f;
 }
 
+uint32_t _glfwGetWindowPrimariesX11(_GLFWwindow* window)
+{
+    return 1; // sRGB
+}
+
+uint32_t _glfwGetWindowTransferX11(_GLFWwindow* window)
+{
+    return 10; // EXT sRGB
+}
+
 void _glfwGetWindowSizeX11(_GLFWwindow* window, int* width, int* height)
 {
     XWindowAttributes attribs;

From 0d09565c9eb094337a3a1245b372e16dba5df173 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Sun, 20 Jul 2025 14:33:44 +0200
Subject: [PATCH 14/40] feat: prefer HLG over SRGB

---
 src/wl_window.c | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/wl_window.c b/src/wl_window.c
index 874ef89a9c..990b15253a 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -2483,20 +2483,20 @@ float _glfwGetWindowSdrWhiteLevelWayland(_GLFWwindow* window)
 
 uint32_t _glfwGetWindowPrimariesWayland(_GLFWwindow* window)
 {
-    enum wp_color_manager_v1_primaries primaries = WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
     if (_glfw.wl.colorManagerSupport.primaries[WP_COLOR_MANAGER_V1_PRIMARIES_BT2020])
-        primaries = WP_COLOR_MANAGER_V1_PRIMARIES_BT2020;
-    return primaries;
+        return WP_COLOR_MANAGER_V1_PRIMARIES_BT2020;
+    return WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
 }
 
 uint32_t _glfwGetWindowTransferWayland(_GLFWwindow* window)
 {
-    enum wp_color_manager_v1_transfer_function tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
-    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB])
-        tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB;
     if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ])
-        tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ;
-    return tf;
+        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ;
+    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG])
+        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG;
+    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB])
+        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB;
+    return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
 }
 
 void _glfwGetWindowSizeWayland(_GLFWwindow* window, int* width, int* height)

From 88574a6d3d7d0462e23c30b4c8cc3e7113a19c8f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Sun, 20 Jul 2025 16:09:51 +0200
Subject: [PATCH 15/40] feat: expose number of bits per sample

---
 src/egl_context.c    | 2 ++
 src/glx_context.c    | 2 ++
 src/internal.h       | 1 +
 src/nsgl_context.m   | 2 ++
 src/osmesa_context.c | 2 ++
 src/wgl_context.c    | 1 +
 src/window.c         | 2 ++
 7 files changed, 12 insertions(+)

diff --git a/src/egl_context.c b/src/egl_context.c
index 406fa17e66..a19c6ad0ec 100644
--- a/src/egl_context.c
+++ b/src/egl_context.c
@@ -607,6 +607,8 @@ GLFWbool _glfwCreateContextEGL(_GLFWwindow* window,
     if (!chooseEGLConfig(ctxconfig, fbconfig, &config))
         return GLFW_FALSE;
 
+    window->bitsPerSample = getEGLConfigAttrib(config, EGL_RED_SIZE);
+
     if (ctxconfig->client == GLFW_OPENGL_ES_API)
     {
         if (!eglBindAPI(EGL_OPENGL_ES_API))
diff --git a/src/glx_context.c b/src/glx_context.c
index 993ee83d75..b9947eb1f8 100644
--- a/src/glx_context.c
+++ b/src/glx_context.c
@@ -470,6 +470,8 @@ GLFWbool _glfwCreateContextGLX(_GLFWwindow* window,
         return GLFW_FALSE;
     }
 
+    window->bitsPerSample = getGLXFBConfigAttrib(native, GLX_RED_SIZE);
+
     if (ctxconfig->client == GLFW_OPENGL_ES_API)
     {
         if (!_glfw.glx.ARB_create_context ||
diff --git a/src/internal.h b/src/internal.h
index 41a54aa560..6fede8f286 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -547,6 +547,7 @@ struct _GLFWwindow
     _GLFWcursor*        cursor;
     char*               title;
 
+    int                 bitsPerSample;
     int                 minwidth, minheight;
     int                 maxwidth, maxheight;
     int                 numer, denom;
diff --git a/src/nsgl_context.m b/src/nsgl_context.m
index df72980005..6e97baf7f1 100644
--- a/src/nsgl_context.m
+++ b/src/nsgl_context.m
@@ -263,6 +263,8 @@ GLFWbool _glfwCreateContextNSGL(_GLFWwindow* window,
         SET_ATTRIB(NSOpenGLPFAColorSize, colorBits);
     }
 
+    window->bitsPerSample = fbconfig->redBits;
+
     if (fbconfig->alphaBits != GLFW_DONT_CARE)
         SET_ATTRIB(NSOpenGLPFAAlphaSize, fbconfig->alphaBits);
 
diff --git a/src/osmesa_context.c b/src/osmesa_context.c
index 0bb375a8bd..31587ae43c 100644
--- a/src/osmesa_context.c
+++ b/src/osmesa_context.c
@@ -274,6 +274,8 @@ GLFWbool _glfwCreateContextOSMesa(_GLFWwindow* window,
         return GLFW_FALSE;
     }
 
+    window->bitsPerSample = 8;
+
     window->context.makeCurrent = makeContextCurrentOSMesa;
     window->context.swapBuffers = swapBuffersOSMesa;
     window->context.swapInterval = swapIntervalOSMesa;
diff --git a/src/wgl_context.c b/src/wgl_context.c
index 4b477f3995..2a55d389d3 100644
--- a/src/wgl_context.c
+++ b/src/wgl_context.c
@@ -291,6 +291,7 @@ static int choosePixelFormatWGL(_GLFWwindow* window,
         return 0;
     }
 
+    window->bitsPerSample = closest->redBits;
     pixelFormat = (int) closest->handle;
     _glfw_free(usableConfigs);
 
diff --git a/src/window.c b/src/window.c
index 7da2d43b6e..b060c3daeb 100644
--- a/src/window.c
+++ b/src/window.c
@@ -938,6 +938,8 @@ GLFWAPI int glfwGetWindowAttrib(GLFWwindow* handle, int attrib)
             return window->mousePassthrough;
         case GLFW_TRANSPARENT_FRAMEBUFFER:
             return _glfw.platform.framebufferTransparent(window);
+        case GLFW_RED_BITS:
+            return window->bitsPerSample;
         case GLFW_RESIZABLE:
             return window->resizable;
         case GLFW_DECORATED:

From 98e27c23a8f4d6be0149063674a52da10c0e6b44 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Sun, 20 Jul 2025 17:13:30 +0200
Subject: [PATCH 16/40] fix: handle color management support more cleanly

---
 src/wl_window.c | 43 +++++++++++++++++++++++++++++++++++--------
 1 file changed, 35 insertions(+), 8 deletions(-)

diff --git a/src/wl_window.c b/src/wl_window.c
index 990b15253a..0cec05006c 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1158,6 +1158,30 @@ static void destroyShellObjects(_GLFWwindow* window)
     window->wl.xdg.surface = NULL;
 }
 
+static GLFWbool supportsColorManagement(_GLFWwindow* window)
+{
+    if (!_glfw.wl.colorManager)
+        return GLFW_FALSE;
+
+    if (!_glfw.wl.colorManagerSupport.parametric)
+        return GLFW_FALSE;
+
+    if (!_glfw.wl.colorManagerSupport.intents[WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL])
+        return GLFW_FALSE;
+
+    if (!_glfw.wl.colorManagerSupport.primaries[WP_COLOR_MANAGER_V1_PRIMARIES_SRGB] &&
+        !_glfw.wl.colorManagerSupport.primaries[WP_COLOR_MANAGER_V1_PRIMARIES_BT2020])
+        return GLFW_FALSE;
+
+    if (!_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB] &&
+        !_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ] &&
+        !_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG] &&
+        !_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB])
+        return GLFW_FALSE;
+
+    return GLFW_TRUE;
+}
+
 static GLFWbool createNativeSurface(_GLFWwindow* window,
                                     const _GLFWwndconfig* wndconfig,
                                     const _GLFWfbconfig* fbconfig)
@@ -1229,20 +1253,17 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
         }
     }
 
-    GLFWbool supportsHdr = _glfw.wl.colorManager &&
-                          _glfw.wl.colorManagerSupport.parametric &&
-                          _glfw.wl.colorManagerSupport.primaries[primaries] &&
-                          _glfw.wl.colorManagerSupport.tfs[tf] &&
-                          _glfw.wl.colorManagerSupport.intents[intent];
+    GLFWbool supportsCm = supportsColorManagement(window);
 
-    if (fbconfig->floatbuffer && !supportsHdr)
+    if (fbconfig->floatbuffer && !supportsCm)
     {
         _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Wayland: Float buffers are not supported without HDR support");
+                        "Wayland: Float buffers are not supported without color management support");
         return GLFW_FALSE;
     }
 
-    if (supportsHdr) {
+    if (supportsCm)
+    {
         // Set up color surface & get its preferred image description (currently nothing is done with the preferred image description)
         {
             window->wl.colorSurface = wp_color_manager_v1_get_surface(_glfw.wl.colorManager, window->wl.surface);
@@ -2483,6 +2504,9 @@ float _glfwGetWindowSdrWhiteLevelWayland(_GLFWwindow* window)
 
 uint32_t _glfwGetWindowPrimariesWayland(_GLFWwindow* window)
 {
+    if (!supportsColorManagement(window))
+        return WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
+
     if (_glfw.wl.colorManagerSupport.primaries[WP_COLOR_MANAGER_V1_PRIMARIES_BT2020])
         return WP_COLOR_MANAGER_V1_PRIMARIES_BT2020;
     return WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
@@ -2490,6 +2514,9 @@ uint32_t _glfwGetWindowPrimariesWayland(_GLFWwindow* window)
 
 uint32_t _glfwGetWindowTransferWayland(_GLFWwindow* window)
 {
+    if (!supportsColorManagement(window))
+        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
+
     if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ])
         return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ;
     if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG])

From d9d200fc574a54105340de453934e9d4f498ebf3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Sun, 20 Jul 2025 19:58:17 +0200
Subject: [PATCH 17/40] feat: add glfwInitHint to turn off wayland color
 management

---
 include/GLFW/glfw3.h |  4 ++++
 src/init.c           |  4 ++++
 src/internal.h       |  1 +
 src/wl_init.c        | 15 +++++++++------
 4 files changed, 18 insertions(+), 6 deletions(-)

diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h
index fbae45b5b8..7fc47be671 100644
--- a/include/GLFW/glfw3.h
+++ b/include/GLFW/glfw3.h
@@ -1143,6 +1143,10 @@ extern "C" {
 #define GLFW_WAYLAND_APP_ID         0x00026001
 /*! @} */
 
+#define GLFW_WAYLAND_COLOR_MANAGEMENT   0x00026002
+
+
+
 #define GLFW_NO_API                          0
 #define GLFW_OPENGL_API             0x00030001
 #define GLFW_OPENGL_ES_API          0x00030002
diff --git a/src/init.c b/src/init.c
index dbd5a900c6..4f4b4a54d2 100644
--- a/src/init.c
+++ b/src/init.c
@@ -462,6 +462,10 @@ GLFWAPI void glfwInitHint(int hint, int value)
         case GLFW_WAYLAND_LIBDECOR:
             _glfwInitHints.wl.libdecorMode = value;
             return;
+        case GLFW_WAYLAND_COLOR_MANAGEMENT:
+            _glfwInitHints.wl.colorManagement = value ? GLFW_TRUE : GLFW_FALSE;
+            return;
+
     }
 
     _glfwInputError(GLFW_INVALID_ENUM,
diff --git a/src/internal.h b/src/internal.h
index 6fede8f286..bf361b35e5 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -387,6 +387,7 @@ struct _GLFWinitconfig
     } x11;
     struct {
         int       libdecorMode;
+        GLFWbool  colorManagement;
     } wl;
 };
 
diff --git a/src/wl_init.c b/src/wl_init.c
index ac9a2d2de6..d31e6c5795 100644
--- a/src/wl_init.c
+++ b/src/wl_init.c
@@ -298,13 +298,16 @@ static void registryHandleGlobal(void* userData,
     }
     else if (strcmp(interface, "wp_color_manager_v1") == 0)
     {
-        _glfw.wl.colorManager =
-            wl_registry_bind(registry, name,
-                             &wp_color_manager_v1_interface,
-                             1);
+        if (_glfw.hints.init.wl.colorManagement == GLFW_TRUE)
+        {
+            _glfw.wl.colorManager =
+                wl_registry_bind(registry, name,
+                                 &wp_color_manager_v1_interface,
+                                 1);
 
-        memset(&_glfw.wl.colorManagerSupport, 0, sizeof(_glfw.wl.colorManagerSupport));
-        wp_color_manager_v1_add_listener(_glfw.wl.colorManager, &colorManagerListener, &_glfw.wl);
+            memset(&_glfw.wl.colorManagerSupport, 0, sizeof(_glfw.wl.colorManagerSupport));
+            wp_color_manager_v1_add_listener(_glfw.wl.colorManager, &colorManagerListener, &_glfw.wl);
+        }
     }
 }
 

From c7cfeb25c8bb65567f7d3a440c027c775f6e5a44 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Mon, 21 Jul 2025 07:13:47 +0200
Subject: [PATCH 18/40] fix(windows): report sRGB transfer if not fp16 frame
 buffer

---
 src/win32_window.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/win32_window.c b/src/win32_window.c
index 93774059aa..6a99294f24 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -1747,7 +1747,9 @@ uint32_t _glfwGetWindowPrimariesWin32(_GLFWwindow* window)
 
 uint32_t _glfwGetWindowTransferWin32(_GLFWwindow* window)
 {
-    return 5; // linear
+    // If we managed to get a fp16 frame buffer on Windows, we need to output scRGB
+    // i.e. linear colors w/ sRGB primaries.
+    return window->bitsPerSample == 16 ? 5 : 10; // 5 == linear, 10 == EXT sRGB
 }
 
 void _glfwGetWindowSizeWin32(_GLFWwindow* window, int* width, int* height)

From e662df7a2ff73302b561f66d8251e48ccb83e7c1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Mon, 21 Jul 2025 07:14:07 +0200
Subject: [PATCH 19/40] fix(egl): don't print when failed to get float buffer

---
 src/egl_context.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/egl_context.c b/src/egl_context.c
index a19c6ad0ec..1cb08e9701 100644
--- a/src/egl_context.c
+++ b/src/egl_context.c
@@ -243,8 +243,9 @@ static GLFWbool chooseEGLConfig(const _GLFWctxconfig* ctxconfig,
         }
         else
         {
-            _glfwInputError(GLFW_FORMAT_UNAVAILABLE,
-                            "EGL: Failed to find a suitable EGLConfig");
+            // No need for this error message -- the user can simply try again.
+            // _glfwInputError(GLFW_FORMAT_UNAVAILABLE,
+            //                 "EGL: Failed to find a suitable EGLConfig");
         }
     }
 

From 8abb15f5f9a11acd066deba16700f64bfd7a922f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Mon, 21 Jul 2025 09:06:39 +0200
Subject: [PATCH 20/40] fix(cocoa): compiler warning

---
 src/cocoa_platform.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h
index 136e77076a..227b3c12c1 100644
--- a/src/cocoa_platform.h
+++ b/src/cocoa_platform.h
@@ -158,7 +158,7 @@ typedef struct _GLFWwindowNS
     double          cursorWarpDeltaX, cursorWarpDeltaY;
 } _GLFWwindowNS;
 
-typedef void (*GLFWopenedFilenamesFun)(const char*);
+typedef void (*_GLFWopenedFilenamesFun)(const char*);
 
 // Cocoa-specific global data
 //
@@ -184,7 +184,7 @@ typedef struct _GLFWlibraryNS
     // The window whose disabled cursor mode is active
     _GLFWwindow*        disabledCursorWindow;
     char**              openedFilenames;
-    GLFWopenedFilenamesFun openedFilenamesCallback;
+    _GLFWopenedFilenamesFun openedFilenamesCallback;
 
     struct {
         CFBundleRef     bundle;

From b1461ff61cadf8d894cb4d391754e0138466fde1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Mon, 21 Jul 2025 10:08:12 +0200
Subject: [PATCH 21/40] fix(windows): report SDR white level of 80 when
 rendering sRGB

---
 src/win32_window.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/win32_window.c b/src/win32_window.c
index 6a99294f24..99f9649657 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -1652,6 +1652,15 @@ void _glfwSetWindowPosWin32(_GLFWwindow* window, int xpos, int ypos)
 }
 
 float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window) {
+    if (window->bitsPerSample != 16) {
+        // If we don't have a fp16 frame buffer, Windows does not expect scRGB
+        // with proper SDR white level scaling, it instead expects standard
+        // sRGB whose reference white level should be 80 nits. (Even though the
+        // screen's reference white level -- obtained by the bottom code --
+        // might be different. In that case Windows does the remapping for us.)
+        return 80.0f;
+    }
+
     UINT32 numPaths, numModes;
     LONG result;
 

From 8f4b49f63b7e0efb246dabfdfd3e881828b906fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Wed, 23 Jul 2025 07:52:21 +0200
Subject: [PATCH 22/40] fix(wayland): use absolute colorimetric rendering
 intent if available

---
 src/wl_platform.h |  1 +
 src/wl_window.c   | 35 +++++++++++++++++++++++++----------
 2 files changed, 26 insertions(+), 10 deletions(-)

diff --git a/src/wl_platform.h b/src/wl_platform.h
index 9e393d8e2c..01c6561f81 100644
--- a/src/wl_platform.h
+++ b/src/wl_platform.h
@@ -649,6 +649,7 @@ void _glfwGetWindowContentScaleWayland(_GLFWwindow* window, float* xscale, float
 float _glfwGetWindowSdrWhiteLevelWayland(_GLFWwindow* window);
 uint32_t _glfwGetWindowPrimariesWayland(_GLFWwindow* window);
 uint32_t _glfwGetWindowTransferWayland(_GLFWwindow* window);
+uint32_t _glfwGetWindowRenderingIntentWayland(_GLFWwindow* window);
 void _glfwIconifyWindowWayland(_GLFWwindow* window);
 void _glfwRestoreWindowWayland(_GLFWwindow* window);
 void _glfwMaximizeWindowWayland(_GLFWwindow* window);
diff --git a/src/wl_window.c b/src/wl_window.c
index 0cec05006c..d8629e7053 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1242,16 +1242,7 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
     // These functions take into account the color management support of the compositor
     enum wp_color_manager_v1_primaries primaries = _glfwGetWindowPrimariesWayland(window);
     enum wp_color_manager_v1_transfer_function tf = _glfwGetWindowTransferWayland(window);
-
-    enum wp_color_manager_v1_render_intent intent = WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL;
-    for (int i = 0; i < 5; ++i)
-    {
-        if (_glfw.wl.colorManagerSupport.intents[i])
-        {
-            intent = i;
-            break;
-        }
-    }
+    enum wp_color_manager_v1_render_intent intent = _glfwGetWindowRenderingIntentWayland(window);
 
     GLFWbool supportsCm = supportsColorManagement(window);
 
@@ -2526,6 +2517,30 @@ uint32_t _glfwGetWindowTransferWayland(_GLFWwindow* window)
     return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
 }
 
+uint32_t _glfwGetWindowRenderingIntentWayland(_GLFWwindow* window)
+{
+    if (!supportsColorManagement(window))
+        return WP_COLOR_MANAGER_V1_RENDER_INTENT_ABSOLUTE;
+
+    // Our goal is to be as colorimetrically accurate as possible. The absolute
+    // rendering intent preserves colors exactly (including white point) at the
+    // cost of gamut clipping. If that's not available, we fall back to the
+    // relative intent which rescales colors to the nearest white point in the
+    // display's gamut, at the cost of some color accuracy. Lastly, the
+    // perceptual intent is our fallback when neither absolute nor relative are
+    // available, as it is guaranteed to be supported when color management is
+    // supported. the perceptual intent may lead to the most pleasing image in
+    // terms of smooth color transitions, but that's at the cost of color
+    // accuracy / saturation.
+    if (_glfw.wl.colorManagerSupport.intents[WP_COLOR_MANAGER_V1_RENDER_INTENT_ABSOLUTE])
+        return WP_COLOR_MANAGER_V1_RENDER_INTENT_ABSOLUTE;
+    if (_glfw.wl.colorManagerSupport.intents[WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE])
+        return WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE;
+
+     // Perceptual is guaranteed to be supported when color management is supported
+    return WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL;
+}
+
 void _glfwGetWindowSizeWayland(_GLFWwindow* window, int* width, int* height)
 {
     if (width)

From 189c59eb9be030ea6edcd0bd7459ebc4444ab0a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Thu, 24 Jul 2025 11:39:47 +0200
Subject: [PATCH 23/40] feat(wayland): hook up SDR white reporting

---
 src/internal.h  | 1 +
 src/wl_window.c | 5 ++++-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/internal.h b/src/internal.h
index bf361b35e5..8147edfb31 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -548,6 +548,7 @@ struct _GLFWwindow
     _GLFWcursor*        cursor;
     char*               title;
 
+    float               sdrWhiteLevel;
     int                 bitsPerSample;
     int                 minwidth, minheight;
     int                 maxwidth, maxheight;
diff --git a/src/wl_window.c b/src/wl_window.c
index d8629e7053..f2be72efc8 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -627,6 +627,8 @@ void imageDescriptionHandleTransferFunctionNamed(void *userData, struct wp_image
 
 void imageDescriptionHandleLuminances(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum)
 {
+    _GLFWwindow* window = userData;
+    window->sdrWhiteLevel = reference_lum;
 }
 
 void imageDescriptionHandleTargetPrimaries(void *userData, struct wp_image_description_info_v1 *image_description_info, 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)
@@ -2489,7 +2491,8 @@ void _glfwSetWindowPosWayland(_GLFWwindow* window, int xpos, int ypos)
 
 float _glfwGetWindowSdrWhiteLevelWayland(_GLFWwindow* window)
 {
-    // TODO: when enabling non-sRGB transfer functions, like EXT_LINEAR, this should return the display's white level in nits
+    if (window->sdrWhiteLevel != 0.0f)
+        return window->sdrWhiteLevel;
     return 80.0f;
 }
 

From 2c996be2d9a45d57f3abe9dffc1a0bfd153d2dd6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Fri, 25 Jul 2025 10:32:40 +0200
Subject: [PATCH 24/40] feat(wayland): hook up max and min luminances

TODO: do the same for Windows
---
 include/GLFW/glfw3.h |  3 +++
 src/cocoa_init.m     |  2 ++
 src/cocoa_platform.h |  2 ++
 src/cocoa_window.m   | 10 ++++++++++
 src/internal.h       |  3 ++-
 src/null_init.c      |  2 ++
 src/null_platform.h  |  2 ++
 src/null_window.c    | 10 ++++++++++
 src/win32_init.c     |  2 ++
 src/win32_platform.h |  2 ++
 src/win32_window.c   |  8 ++++++++
 src/window.c         | 20 ++++++++++++++++++++
 src/wl_init.c        |  2 ++
 src/wl_platform.h    |  5 +++++
 src/wl_window.c      | 19 ++++++++++++++++---
 src/x11_init.c       |  2 ++
 src/x11_platform.h   |  2 ++
 src/x11_window.c     | 10 ++++++++++
 18 files changed, 102 insertions(+), 4 deletions(-)

diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h
index 7fc47be671..5328d5aa68 100644
--- a/include/GLFW/glfw3.h
+++ b/include/GLFW/glfw3.h
@@ -3744,6 +3744,9 @@ GLFWAPI void glfwGetWindowFrameSize(GLFWwindow* window, int* left, int* top, int
 GLFWAPI void glfwGetWindowContentScale(GLFWwindow* window, float* xscale, float* yscale);
 
 GLFWAPI float glfwGetWindowSdrWhiteLevel(GLFWwindow* window);
+GLFWAPI float glfwGetWindowMinLuminance(GLFWwindow* window);
+GLFWAPI float glfwGetWindowMaxLuminance(GLFWwindow* window);
+
 GLFWAPI uint32_t glfwGetWindowPrimaries(GLFWwindow* window);
 GLFWAPI uint32_t glfwGetWindowTransfer(GLFWwindow* window);
 
diff --git a/src/cocoa_init.m b/src/cocoa_init.m
index 71904b1612..f7825de862 100644
--- a/src/cocoa_init.m
+++ b/src/cocoa_init.m
@@ -553,6 +553,8 @@ GLFWbool _glfwConnectCocoa(int platformID, _GLFWplatform* platform)
         .getWindowFrameSize = _glfwGetWindowFrameSizeCocoa,
         .getWindowContentScale = _glfwGetWindowContentScaleCocoa,
         .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelCocoa,
+        .getWindowMinLuminance = _glfwGetWindowMinLuminanceCocoa,
+        .getWindowMaxLuminance = _glfwGetWindowMaxLuminanceCocoa,
         .getWindowPrimaries = _glfwGetWindowPrimariesCocoa,
         .getWindowTransfer = _glfwGetWindowTransferCocoa,
         .iconifyWindow = _glfwIconifyWindowCocoa,
diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h
index 227b3c12c1..7e12c48f16 100644
--- a/src/cocoa_platform.h
+++ b/src/cocoa_platform.h
@@ -232,6 +232,8 @@ void _glfwGetFramebufferSizeCocoa(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeCocoa(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleCocoa(_GLFWwindow* window, float* xscale, float* yscale);
 float _glfwGetWindowSdrWhiteLevelCocoa(_GLFWwindow* window);
+float _glfwGetWindowMinLuminanceCocoa(_GLFWwindow* window);
+float _glfwGetWindowMaxLuminanceCocoa(_GLFWwindow* window);
 uint32_t _glfwGetWindowPrimariesCocoa(_GLFWwindow* window);
 uint32_t _glfwGetWindowTransferCocoa(_GLFWwindow* window);
 void _glfwIconifyWindowCocoa(_GLFWwindow* window);
diff --git a/src/cocoa_window.m b/src/cocoa_window.m
index 6aea32f07d..7671c7835b 100644
--- a/src/cocoa_window.m
+++ b/src/cocoa_window.m
@@ -1069,6 +1069,16 @@ float _glfwGetWindowSdrWhiteLevelCocoa(_GLFWwindow* window)
     return 80.0f;
 }
 
+float _glfwGetWindowMinLuminanceCocoa(_GLFWwindow* window)
+{
+    return 0.0f;
+}
+
+float _glfwGetWindowMaxLuminanceCocoa(_GLFWwindow* window)
+{
+    return 0.0f;
+}
+
 uint32_t _glfwGetWindowPrimariesCocoa(_GLFWwindow* window)
 {
     return 1; // sRGB
diff --git a/src/internal.h b/src/internal.h
index 8147edfb31..ff37b82e40 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -548,7 +548,6 @@ struct _GLFWwindow
     _GLFWcursor*        cursor;
     char*               title;
 
-    float               sdrWhiteLevel;
     int                 bitsPerSample;
     int                 minwidth, minheight;
     int                 maxwidth, maxheight;
@@ -725,6 +724,8 @@ struct _GLFWplatform
     void (*getWindowPos)(_GLFWwindow*,int*,int*);
     void (*setWindowPos)(_GLFWwindow*,int,int);
     float (*getWindowSdrWhiteLevel)(_GLFWwindow*);
+    float (*getWindowMinLuminance)(_GLFWwindow*);
+    float (*getWindowMaxLuminance)(_GLFWwindow*);
     uint32_t (*getWindowPrimaries)(_GLFWwindow*);
     uint32_t (*getWindowTransfer)(_GLFWwindow*);
     void (*getWindowSize)(_GLFWwindow*,int*,int*);
diff --git a/src/null_init.c b/src/null_init.c
index 74f4f0bfd8..d384008602 100644
--- a/src/null_init.c
+++ b/src/null_init.c
@@ -82,6 +82,8 @@ GLFWbool _glfwConnectNull(int platformID, _GLFWplatform* platform)
         .getWindowFrameSize = _glfwGetWindowFrameSizeNull,
         .getWindowContentScale = _glfwGetWindowContentScaleNull,
         .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelNull,
+        .getWindowMinLuminance = _glfwGetWindowMinLuminanceNull,
+        .getWindowMaxLuminance = _glfwGetWindowMaxLuminanceNull,
         .getWindowPrimaries = _glfwGetWindowPrimariesNull,
         .getWindowTransfer = _glfwGetWindowTransferNull,
         .iconifyWindow = _glfwIconifyWindowNull,
diff --git a/src/null_platform.h b/src/null_platform.h
index aa1bae0531..438fe37410 100644
--- a/src/null_platform.h
+++ b/src/null_platform.h
@@ -234,6 +234,8 @@ void _glfwGetFramebufferSizeNull(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeNull(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleNull(_GLFWwindow* window, float* xscale, float* yscale);
 float _glfwGetWindowSdrWhiteLevelNull(_GLFWwindow* window);
+float _glfwGetWindowMinLuminanceNull(_GLFWwindow* window);
+float _glfwGetWindowMaxLuminanceNull(_GLFWwindow* window);
 uint32_t _glfwGetWindowPrimariesNull(_GLFWwindow* window);
 uint32_t _glfwGetWindowTransferNull(_GLFWwindow* window);
 void _glfwIconifyWindowNull(_GLFWwindow* window);
diff --git a/src/null_window.c b/src/null_window.c
index 054b134676..52b828650e 100644
--- a/src/null_window.c
+++ b/src/null_window.c
@@ -247,6 +247,16 @@ float _glfwGetWindowSdrWhiteLevelNull(_GLFWwindow* window)
     return 80.0f;
 }
 
+float _glfwGetWindowMinLuminanceNull(_GLFWwindow* window)
+{
+    return 0.0f;
+}
+
+float _glfwGetWindowMaxLuminanceNull(_GLFWwindow* window)
+{
+    return 0.0f;
+}
+
 uint32_t _glfwGetWindowPrimariesNull(_GLFWwindow* window)
 {
     return 1; // sRGB
diff --git a/src/win32_init.c b/src/win32_init.c
index dd1045e4c8..cafc6ef416 100644
--- a/src/win32_init.c
+++ b/src/win32_init.c
@@ -641,6 +641,8 @@ GLFWbool _glfwConnectWin32(int platformID, _GLFWplatform* platform)
         .getWindowFrameSize = _glfwGetWindowFrameSizeWin32,
         .getWindowContentScale = _glfwGetWindowContentScaleWin32,
         .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelWin32,
+        .getWindowMinLuminance = _glfwGetWindowMinLuminanceWin32,
+        .getWindowMaxLuminance = _glfwGetWindowMaxLuminanceWin32,
         .getWindowPrimaries = _glfwGetWindowPrimariesWin32,
         .getWindowTransfer = _glfwGetWindowTransferWin32,
         .iconifyWindow = _glfwIconifyWindowWin32,
diff --git a/src/win32_platform.h b/src/win32_platform.h
index bbf28a3672..6fd05947c6 100644
--- a/src/win32_platform.h
+++ b/src/win32_platform.h
@@ -497,6 +497,8 @@ void _glfwGetFramebufferSizeWin32(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeWin32(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleWin32(_GLFWwindow* window, float* xscale, float* yscale);
 float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window);
+float _glfwGetWindowMinLuminanceWin32(_GLFWwindow* window);
+float _glfwGetWindowMaxLuminanceWin32(_GLFWwindow* window);
 uint32_t _glfwGetWindowPrimariesWin32(_GLFWwindow* window);
 uint32_t _glfwGetWindowTransferWin32(_GLFWwindow* window);
 void _glfwIconifyWindowWin32(_GLFWwindow* window);
diff --git a/src/win32_window.c b/src/win32_window.c
index 99f9649657..e9428d3b8c 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -1749,6 +1749,14 @@ float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window) {
     return 80.0f; // sRGB standard white level
 }
 
+float _glfwGetWindowMinLuminanceWin32(_GLFWwindow* window) {
+    return 0.0f;
+}
+
+float _glfwGetWindowMaxLuminanceWin32(_GLFWwindow* window) {
+    return 0.0f;
+}
+
 uint32_t _glfwGetWindowPrimariesWin32(_GLFWwindow* window)
 {
     return 1; // sRGB
diff --git a/src/window.c b/src/window.c
index b060c3daeb..9b76b7c432 100644
--- a/src/window.c
+++ b/src/window.c
@@ -622,6 +622,26 @@ GLFWAPI float glfwGetWindowSdrWhiteLevel(GLFWwindow* handle)
     return _glfw.platform.getWindowSdrWhiteLevel(window);
 }
 
+GLFWAPI float glfwGetWindowMinLuminance(GLFWwindow* handle)
+{
+    _GLFW_REQUIRE_INIT_OR_RETURN(0.f);
+
+    _GLFWwindow* window = (_GLFWwindow*) handle;
+    assert(window != NULL);
+
+    return _glfw.platform.getWindowMinLuminance(window);
+}
+
+GLFWAPI float glfwGetWindowMaxLuminance(GLFWwindow* handle)
+{
+    _GLFW_REQUIRE_INIT_OR_RETURN(0.f);
+
+    _GLFWwindow* window = (_GLFWwindow*) handle;
+    assert(window != NULL);
+
+    return _glfw.platform.getWindowMaxLuminance(window);
+}
+
 GLFWAPI uint32_t glfwGetWindowPrimaries(GLFWwindow* handle)
 {
     _GLFW_REQUIRE_INIT_OR_RETURN(1); // sRGB
diff --git a/src/wl_init.c b/src/wl_init.c
index d31e6c5795..0d3f05eb45 100644
--- a/src/wl_init.c
+++ b/src/wl_init.c
@@ -588,6 +588,8 @@ GLFWbool _glfwConnectWayland(int platformID, _GLFWplatform* platform)
         .getWindowFrameSize = _glfwGetWindowFrameSizeWayland,
         .getWindowContentScale = _glfwGetWindowContentScaleWayland,
         .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelWayland,
+        .getWindowMinLuminance = _glfwGetWindowMinLuminanceWayland,
+        .getWindowMaxLuminance = _glfwGetWindowMaxLuminanceWayland,
         .getWindowPrimaries = _glfwGetWindowPrimariesWayland,
         .getWindowTransfer = _glfwGetWindowTransferWayland,
         .iconifyWindow = _glfwIconifyWindowWayland,
diff --git a/src/wl_platform.h b/src/wl_platform.h
index 01c6561f81..0992ea5241 100644
--- a/src/wl_platform.h
+++ b/src/wl_platform.h
@@ -359,6 +359,9 @@ typedef struct _GLFWwindowWayland
     GLFWbool                    hovered;
     GLFWbool                    transparent;
     GLFWbool                    scaleFramebuffer;
+    float                       sdrWhiteLevel;
+    float                       minLuminance;
+    float                       maxLuminance;
     struct wl_surface*          surface;
     struct wl_callback*         callback;
 
@@ -647,6 +650,8 @@ void _glfwGetFramebufferSizeWayland(_GLFWwindow* window, int* width, int* height
 void _glfwGetWindowFrameSizeWayland(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleWayland(_GLFWwindow* window, float* xscale, float* yscale);
 float _glfwGetWindowSdrWhiteLevelWayland(_GLFWwindow* window);
+float _glfwGetWindowMinLuminanceWayland(_GLFWwindow* window);
+float _glfwGetWindowMaxLuminanceWayland(_GLFWwindow* window);
 uint32_t _glfwGetWindowPrimariesWayland(_GLFWwindow* window);
 uint32_t _glfwGetWindowTransferWayland(_GLFWwindow* window);
 uint32_t _glfwGetWindowRenderingIntentWayland(_GLFWwindow* window);
diff --git a/src/wl_window.c b/src/wl_window.c
index f2be72efc8..dc2398f260 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -627,8 +627,11 @@ void imageDescriptionHandleTransferFunctionNamed(void *userData, struct wp_image
 
 void imageDescriptionHandleLuminances(void *userData, struct wp_image_description_info_v1 *image_description_info, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum)
 {
+    // printf("Wayland: min_lum=%d, max_lum=%d, reference_lum=%d\n", min_lum, max_lum, reference_lum);
     _GLFWwindow* window = userData;
-    window->sdrWhiteLevel = reference_lum;
+    window->wl.sdrWhiteLevel = reference_lum;
+    window->wl.minLuminance = (float)min_lum / WAYLAND_MIN_LUMINANCE_FACTOR;
+    window->wl.maxLuminance = (float)max_lum;
 }
 
 void imageDescriptionHandleTargetPrimaries(void *userData, struct wp_image_description_info_v1 *image_description_info, 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)
@@ -2491,11 +2494,21 @@ void _glfwSetWindowPosWayland(_GLFWwindow* window, int xpos, int ypos)
 
 float _glfwGetWindowSdrWhiteLevelWayland(_GLFWwindow* window)
 {
-    if (window->sdrWhiteLevel != 0.0f)
-        return window->sdrWhiteLevel;
+    if (window->wl.sdrWhiteLevel != 0.0f)
+        return window->wl.sdrWhiteLevel;
     return 80.0f;
 }
 
+float _glfwGetWindowMinLuminanceWayland(_GLFWwindow* window)
+{
+    return window->wl.minLuminance;
+}
+
+float _glfwGetWindowMaxLuminanceWayland(_GLFWwindow* window)
+{
+    return window->wl.maxLuminance;
+}
+
 uint32_t _glfwGetWindowPrimariesWayland(_GLFWwindow* window)
 {
     if (!supportsColorManagement(window))
diff --git a/src/x11_init.c b/src/x11_init.c
index 97bac0bac5..f73e25a944 100644
--- a/src/x11_init.c
+++ b/src/x11_init.c
@@ -1218,6 +1218,8 @@ GLFWbool _glfwConnectX11(int platformID, _GLFWplatform* platform)
         .getWindowFrameSize = _glfwGetWindowFrameSizeX11,
         .getWindowContentScale = _glfwGetWindowContentScaleX11,
         .getWindowSdrWhiteLevel = _glfwGetWindowSdrWhiteLevelX11,
+        .getWindowMinLuminance = _glfwGetWindowMinLuminanceX11,
+        .getWindowMaxLuminance = _glfwGetWindowMaxLuminanceX11,
         .getWindowPrimaries = _glfwGetWindowPrimariesX11,
         .getWindowTransfer = _glfwGetWindowTransferX11,
         .iconifyWindow = _glfwIconifyWindowX11,
diff --git a/src/x11_platform.h b/src/x11_platform.h
index 42ad9df05b..a4fc749df5 100644
--- a/src/x11_platform.h
+++ b/src/x11_platform.h
@@ -917,6 +917,8 @@ void _glfwGetFramebufferSizeX11(_GLFWwindow* window, int* width, int* height);
 void _glfwGetWindowFrameSizeX11(_GLFWwindow* window, int* left, int* top, int* right, int* bottom);
 void _glfwGetWindowContentScaleX11(_GLFWwindow* window, float* xscale, float* yscale);
 float _glfwGetWindowSdrWhiteLevelX11(_GLFWwindow* window);
+float _glfwGetWindowMinLuminanceX11(_GLFWwindow* window);
+float _glfwGetWindowMaxLuminanceX11(_GLFWwindow* window);
 uint32_t _glfwGetWindowPrimariesX11(_GLFWwindow* window);
 uint32_t _glfwGetWindowTransferX11(_GLFWwindow* window);
 void _glfwIconifyWindowX11(_GLFWwindow* window);
diff --git a/src/x11_window.c b/src/x11_window.c
index d06be39838..8f6b565614 100644
--- a/src/x11_window.c
+++ b/src/x11_window.c
@@ -2195,6 +2195,16 @@ float _glfwGetWindowSdrWhiteLevelX11(_GLFWwindow* window)
     return 80.0f;
 }
 
+float _glfwGetWindowMinLuminanceX11(_GLFWwindow* window)
+{
+    return 0.0f;
+}
+
+float _glfwGetWindowMaxLuminanceX11(_GLFWwindow* window)
+{
+    return 0.0f;
+}
+
 uint32_t _glfwGetWindowPrimariesX11(_GLFWwindow* window)
 {
     return 1; // sRGB

From 6c1b203dfab8adfdf6184070cc3dd68c72d369fd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller=20H=C3=B6hne?= <thomas94@gmx.net>
Date: Fri, 25 Jul 2025 16:03:24 +0200
Subject: [PATCH 25/40] fix(wayland): remove obsolete restriction of fp16
 buffers

---
 src/wl_window.c | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/src/wl_window.c b/src/wl_window.c
index dc2398f260..0858aae1a9 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1251,13 +1251,6 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
 
     GLFWbool supportsCm = supportsColorManagement(window);
 
-    if (fbconfig->floatbuffer && !supportsCm)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Wayland: Float buffers are not supported without color management support");
-        return GLFW_FALSE;
-    }
-
     if (supportsCm)
     {
         // Set up color surface & get its preferred image description (currently nothing is done with the preferred image description)

From 58f79531427e9e40c11dec97a313218a00cf62f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Wed, 30 Jul 2025 11:23:04 +0200
Subject: [PATCH 26/40] fix(wayland): propagate compositor luminances to
 surface

---
 src/wl_window.c | 69 ++++++++++++++++++++++++++++++-------------------
 1 file changed, 43 insertions(+), 26 deletions(-)

diff --git a/src/wl_window.c b/src/wl_window.c
index 0858aae1a9..2f5ebc61df 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -559,6 +559,46 @@ const struct wp_fractional_scale_v1_listener fractionalScaleListener =
 #define WAYLAND_COLOR_FACTOR 1000000
 #define WAYLAND_MIN_LUMINANCE_FACTOR 10000
 
+static GLFWbool updateColorManagedSurface(_GLFWwindow* window)
+{
+    // These functions take into account the color management support of the compositor
+    enum wp_color_manager_v1_primaries primaries = _glfwGetWindowPrimariesWayland(window);
+    enum wp_color_manager_v1_transfer_function tf = _glfwGetWindowTransferWayland(window);
+    enum wp_color_manager_v1_render_intent intent = _glfwGetWindowRenderingIntentWayland(window);
+
+    struct wp_image_description_creator_params_v1* creator = wp_color_manager_v1_create_parametric_creator(_glfw.wl.colorManager);
+    if (!creator)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to create color management creator");
+        return GLFW_FALSE;
+    }
+
+    wp_image_description_creator_params_v1_set_primaries_named(creator, primaries);
+    wp_image_description_creator_params_v1_set_tf_named(creator, tf);
+
+    if (_glfw.wl.colorManagerSupport.setLuminance && (window->wl.sdrWhiteLevel != 0.0f || window->wl.minLuminance != 0.0f || window->wl.maxLuminance != 0.0f))
+    {
+        wp_image_description_creator_params_v1_set_luminances(creator,
+                                                              (uint32_t)(window->wl.minLuminance * WAYLAND_MIN_LUMINANCE_FACTOR),
+                                                              (uint32_t)window->wl.maxLuminance,
+                                                              (uint32_t)window->wl.sdrWhiteLevel);
+    }
+
+    struct wp_image_description_v1* image_description = wp_image_description_creator_params_v1_create(creator);
+
+    if (!image_description)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to create image description");
+        wp_image_description_creator_params_v1_destroy(creator);
+        return GLFW_FALSE;
+    }
+
+    wp_color_management_surface_v1_set_image_description(window->wl.colorSurface, image_description, intent);
+
+    wp_image_description_v1_destroy(image_description);
+    return GLFW_TRUE;
+}
+
 void imageDescriptionHandleDone(void *userData, struct wp_image_description_info_v1 *image_description_info)
 {
     wp_image_description_info_v1_destroy(image_description_info);
@@ -632,6 +672,8 @@ void imageDescriptionHandleLuminances(void *userData, struct wp_image_descriptio
     window->wl.sdrWhiteLevel = reference_lum;
     window->wl.minLuminance = (float)min_lum / WAYLAND_MIN_LUMINANCE_FACTOR;
     window->wl.maxLuminance = (float)max_lum;
+
+    updateColorManagedSurface(window);
 }
 
 void imageDescriptionHandleTargetPrimaries(void *userData, struct wp_image_description_info_v1 *image_description_info, 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)
@@ -1244,11 +1286,6 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
     window->wl.colorSurface = NULL;
     window->wl.colorSurfaceFeedback = NULL;
 
-    // These functions take into account the color management support of the compositor
-    enum wp_color_manager_v1_primaries primaries = _glfwGetWindowPrimariesWayland(window);
-    enum wp_color_manager_v1_transfer_function tf = _glfwGetWindowTransferWayland(window);
-    enum wp_color_manager_v1_render_intent intent = _glfwGetWindowRenderingIntentWayland(window);
-
     GLFWbool supportsCm = supportsColorManagement(window);
 
     if (supportsCm)
@@ -1265,30 +1302,10 @@ static GLFWbool createNativeSurface(_GLFWwindow* window,
 
             wl_display_roundtrip(_glfw.wl.display);
         }
-        
-        // Configure the color surface to take scRGB (sRGB primaries & linear transfer) from us
-        struct wp_image_description_creator_params_v1* creator = wp_color_manager_v1_create_parametric_creator(_glfw.wl.colorManager);
-        if (!creator)
-        {
-            _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to create color management creator");
-            return GLFW_FALSE;
-        }
-
-        wp_image_description_creator_params_v1_set_primaries_named(creator, primaries);
-        wp_image_description_creator_params_v1_set_tf_named(creator, tf);
 
-        struct wp_image_description_v1* image_description = wp_image_description_creator_params_v1_create(creator);
-
-        if (!image_description)
-        {
-            _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to create image description");
-            wp_image_description_creator_params_v1_destroy(creator);
+        if (!updateColorManagedSurface(window)) {
             return GLFW_FALSE;
         }
-
-        wp_color_management_surface_v1_set_image_description(window->wl.colorSurface, image_description, intent);
-
-        wp_image_description_v1_destroy(image_description);
     }
 
     return GLFW_TRUE;

From df2f2bb9d26a413d2d60151990b7584871dcb9cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Wed, 6 Aug 2025 10:43:23 +0200
Subject: [PATCH 27/40] fix(wayland): relative render intent for more
 perceptually accurate colors

---
 src/wl_window.c | 26 +++++++++++---------------
 1 file changed, 11 insertions(+), 15 deletions(-)

diff --git a/src/wl_window.c b/src/wl_window.c
index 2f5ebc61df..c438cf0474 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -2546,24 +2546,20 @@ uint32_t _glfwGetWindowTransferWayland(_GLFWwindow* window)
 uint32_t _glfwGetWindowRenderingIntentWayland(_GLFWwindow* window)
 {
     if (!supportsColorManagement(window))
-        return WP_COLOR_MANAGER_V1_RENDER_INTENT_ABSOLUTE;
-
-    // Our goal is to be as colorimetrically accurate as possible. The absolute
-    // rendering intent preserves colors exactly (including white point) at the
-    // cost of gamut clipping. If that's not available, we fall back to the
-    // relative intent which rescales colors to the nearest white point in the
-    // display's gamut, at the cost of some color accuracy. Lastly, the
-    // perceptual intent is our fallback when neither absolute nor relative are
-    // available, as it is guaranteed to be supported when color management is
-    // supported. the perceptual intent may lead to the most pleasing image in
-    // terms of smooth color transitions, but that's at the cost of color
-    // accuracy / saturation.
-    if (_glfw.wl.colorManagerSupport.intents[WP_COLOR_MANAGER_V1_RENDER_INTENT_ABSOLUTE])
-        return WP_COLOR_MANAGER_V1_RENDER_INTENT_ABSOLUTE;
+        return WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE;
+
+    // Our goal is to be as colorimetrically accurate as possible without
+    // falsifying how the image was intended to be displayed. The relative
+    // rendering intent preserves colors exactly while adapting to the white
+    // point of the display's gamut. Lastly, the perceptual intent is our
+    // fallback when relative colorimetric isn't available, as it is guaranteed
+    // to be supported when color management is supported. the perceptual
+    // intent may lead to the most pleasing image in terms of smooth color
+    // transitions, but that's at the cost of color accuracy / saturation.
     if (_glfw.wl.colorManagerSupport.intents[WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE])
         return WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE;
 
-     // Perceptual is guaranteed to be supported when color management is supported
+    // Perceptual is guaranteed to be supported when color management is supported
     return WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL;
 }
 

From 56b6cc8be6c4be58a0d1ec00e6527b22eba9a198 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Wed, 13 Aug 2025 13:23:14 +0200
Subject: [PATCH 28/40] feat(wayland): generalize clipboard to other data than
 strings

---
 include/GLFW/glfw3native.h |   4 +
 src/wl_platform.h          |   2 +
 src/wl_window.c            | 148 +++++++++++++++++++++----------------
 3 files changed, 91 insertions(+), 63 deletions(-)

diff --git a/include/GLFW/glfw3native.h b/include/GLFW/glfw3native.h
index 93421683c2..0dc99bca01 100644
--- a/include/GLFW/glfw3native.h
+++ b/include/GLFW/glfw3native.h
@@ -548,6 +548,10 @@ GLFWAPI struct wl_output* glfwGetWaylandMonitor(GLFWmonitor* monitor);
  *  @ingroup native
  */
 GLFWAPI struct wl_surface* glfwGetWaylandWindow(GLFWwindow* window);
+
+GLFWAPI void glfwSetWaylandClipboardData(const char* data, const char* type, size_t length);
+
+GLFWAPI const char* glfwGetWaylandClipboardData(const char* type, size_t* length);
 #endif
 
 #if defined(GLFW_EXPOSE_NATIVE_EGL)
diff --git a/src/wl_platform.h b/src/wl_platform.h
index 0992ea5241..333e4fa320 100644
--- a/src/wl_platform.h
+++ b/src/wl_platform.h
@@ -486,6 +486,8 @@ typedef struct _GLFWlibraryWayland
     int                         keyRepeatScancode;
 
     char*                       clipboardString;
+    size_t                      clipboardLength;
+
     short int                   keycodes[256];
     short int                   scancodes[GLFW_KEY_LAST + 1];
     char                        keynames[GLFW_KEY_LAST + 1][5];
diff --git a/src/wl_window.c b/src/wl_window.c
index c438cf0474..06c890d7c4 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1539,7 +1539,7 @@ static void handleEvents(double* timeout)
 
 // Reads the specified data offer as the specified MIME type
 //
-static char* readDataOfferAsString(struct wl_data_offer* offer, const char* mimeType)
+static char* readDataOffer(struct wl_data_offer* offer, const char* mimeType, size_t* length)
 {
     int fds[2];
 
@@ -1555,17 +1555,18 @@ static char* readDataOfferAsString(struct wl_data_offer* offer, const char* mime
     flushDisplay();
     close(fds[1]);
 
-    char* string = NULL;
+    char* data = NULL;
     size_t size = 0;
-    size_t length = 0;
+    *length = 0;
+
+    size_t readSize = 4096;
 
     for (;;)
     {
-        const size_t readSize = 4096;
-        const size_t requiredSize = length + readSize + 1;
+        const size_t requiredSize = *length + readSize + 1;
         if (requiredSize > size)
         {
-            char* longer = _glfw_realloc(string, requiredSize);
+            char* longer = _glfw_realloc(data, requiredSize);
             if (!longer)
             {
                 _glfwInputError(GLFW_OUT_OF_MEMORY, NULL);
@@ -1573,11 +1574,11 @@ static char* readDataOfferAsString(struct wl_data_offer* offer, const char* mime
                 return NULL;
             }
 
-            string = longer;
+            data = longer;
             size = requiredSize;
         }
 
-        const ssize_t result = read(fds[0], string + length, readSize);
+        const ssize_t result = read(fds[0], data + *length, readSize);
         if (result == 0)
             break;
         else if (result == -1)
@@ -1592,13 +1593,14 @@ static char* readDataOfferAsString(struct wl_data_offer* offer, const char* mime
             return NULL;
         }
 
-        length += result;
+        *length += result;
+        readSize *= 2;
     }
 
     close(fds[0]);
+    data[*length] = '\0'; // Null-terminate in case we hold a string
 
-    string[length] = '\0';
-    return string;
+    return data;
 }
 
 static void pointerHandleEnter(void* userData,
@@ -2276,7 +2278,8 @@ static void dataDeviceHandleDrop(void* userData,
     if (!_glfw.wl.dragOffer)
         return;
 
-    char* string = readDataOfferAsString(_glfw.wl.dragOffer, "text/uri-list");
+    size_t length;
+    char* string = readDataOffer(_glfw.wl.dragOffer, "text/uri-list", &length);
     if (string)
     {
         int count;
@@ -2309,10 +2312,10 @@ static void dataDeviceHandleSelection(void* userData,
     {
         if (_glfw.wl.offers[i].offer == offer)
         {
-            if (_glfw.wl.offers[i].text_plain_utf8)
+            // if (_glfw.wl.offers[i].text_plain_utf8)
                 _glfw.wl.selectionOffer = offer;
-            else
-                wl_data_offer_destroy(offer);
+            // else
+            //     wl_data_offer_destroy(offer);
 
             _glfw.wl.offers[i] = _glfw.wl.offers[_glfw.wl.offerCount - 1];
             _glfw.wl.offerCount--;
@@ -3402,15 +3405,18 @@ static void dataSourceHandleSend(void* userData,
                                  int fd)
 {
     // Ignore it if this is an outdated or invalid request
-    if (_glfw.wl.selectionSource != source ||
-        strcmp(mimeType, "text/plain;charset=utf-8") != 0)
+    if (
+        _glfw.wl.selectionSource != source
+        // || strcmp(mimeType, "text/plain;charset=utf-8") != 0
+    )
     {
+        printf("Wayland: Ignoring clipboard send request for mime type '%s'\n", mimeType);
         close(fd);
         return;
     }
 
     char* string = _glfw.wl.clipboardString;
-    size_t length = strlen(string);
+    size_t length = _glfw.wl.clipboardLength;
 
     while (length > 0)
     {
@@ -3453,55 +3459,13 @@ static const struct wl_data_source_listener dataSourceListener =
 
 void _glfwSetClipboardStringWayland(const char* string)
 {
-    if (_glfw.wl.selectionSource)
-    {
-        wl_data_source_destroy(_glfw.wl.selectionSource);
-        _glfw.wl.selectionSource = NULL;
-    }
-
-    char* copy = _glfw_strdup(string);
-    if (!copy)
-    {
-        _glfwInputError(GLFW_OUT_OF_MEMORY, NULL);
-        return;
-    }
-
-    _glfw_free(_glfw.wl.clipboardString);
-    _glfw.wl.clipboardString = copy;
-
-    _glfw.wl.selectionSource =
-        wl_data_device_manager_create_data_source(_glfw.wl.dataDeviceManager);
-    if (!_glfw.wl.selectionSource)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Wayland: Failed to create clipboard data source");
-        return;
-    }
-    wl_data_source_add_listener(_glfw.wl.selectionSource,
-                                &dataSourceListener,
-                                NULL);
-    wl_data_source_offer(_glfw.wl.selectionSource, "text/plain;charset=utf-8");
-    wl_data_device_set_selection(_glfw.wl.dataDevice,
-                                 _glfw.wl.selectionSource,
-                                 _glfw.wl.serial);
+    glfwSetWaylandClipboardData(string, "text/plain;charset=utf-8", strlen(string) + 1);
 }
 
 const char* _glfwGetClipboardStringWayland(void)
 {
-    if (!_glfw.wl.selectionOffer)
-    {
-        _glfwInputError(GLFW_FORMAT_UNAVAILABLE,
-                        "Wayland: No clipboard data available");
-        return NULL;
-    }
-
-    if (_glfw.wl.selectionSource)
-        return _glfw.wl.clipboardString;
-
-    _glfw_free(_glfw.wl.clipboardString);
-    _glfw.wl.clipboardString =
-        readDataOfferAsString(_glfw.wl.selectionOffer, "text/plain;charset=utf-8");
-    return _glfw.wl.clipboardString;
+    size_t tmp;
+    return glfwGetWaylandClipboardData("text/plain;charset=utf-8", &tmp);
 }
 
 EGLenum _glfwGetEGLPlatformWayland(EGLint** attribs)
@@ -3621,5 +3585,63 @@ GLFWAPI struct wl_surface* glfwGetWaylandWindow(GLFWwindow* handle)
     return window->wl.surface;
 }
 
+GLFWAPI void glfwSetWaylandClipboardData(const char* data, const char* type, size_t length) {
+    if (_glfw.wl.selectionSource)
+    {
+        wl_data_source_destroy(_glfw.wl.selectionSource);
+        _glfw.wl.selectionSource = NULL;
+    }
+
+    char* copy = _glfw_calloc(length, 1);
+    memcpy(copy, data, length);
+    if (!copy)
+    {
+        _glfwInputError(GLFW_OUT_OF_MEMORY, NULL);
+        return;
+    }
+
+    _glfw_free(_glfw.wl.clipboardString);
+    _glfw.wl.clipboardString = copy;
+    _glfw.wl.clipboardLength = length;
+
+    _glfw.wl.selectionSource =
+        wl_data_device_manager_create_data_source(_glfw.wl.dataDeviceManager);
+    if (!_glfw.wl.selectionSource)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: Failed to create clipboard data source");
+        return;
+    }
+    wl_data_source_add_listener(_glfw.wl.selectionSource,
+                                &dataSourceListener,
+                                NULL);
+    wl_data_source_offer(_glfw.wl.selectionSource, type);
+    wl_data_device_set_selection(_glfw.wl.dataDevice,
+                                 _glfw.wl.selectionSource,
+                                 _glfw.wl.serial);
+
+}
+
+GLFWAPI const char* glfwGetWaylandClipboardData(const char* type, size_t* length) {
+    if (_glfw.wl.selectionSource) {
+        *length = _glfw.wl.clipboardLength;
+        return _glfw.wl.clipboardString;
+    }
+
+    if (!_glfw.wl.selectionOffer)
+    {
+        _glfwInputError(GLFW_FORMAT_UNAVAILABLE,
+                        "Wayland: No clipboard data available");
+        *length = 0;
+        return NULL;
+    }
+
+    _glfw_free(_glfw.wl.clipboardString);
+    _glfw.wl.clipboardString = readDataOffer(_glfw.wl.selectionOffer, type, &_glfw.wl.clipboardLength);
+
+    *length = _glfw.wl.clipboardLength;
+    return _glfw.wl.clipboardString;
+}
+
 #endif // _GLFW_WAYLAND
 

From 76c2c55b2b5dbca90b6b1edd2dd9676017fc3589 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Thu, 21 Aug 2025 20:44:18 +0200
Subject: [PATCH 29/40] fix(wayland): oom in c&p realloc

---
 src/wl_window.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/wl_window.c b/src/wl_window.c
index 06c890d7c4..f66b9ed5a9 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1594,7 +1594,7 @@ static char* readDataOffer(struct wl_data_offer* offer, const char* mimeType, si
         }
 
         *length += result;
-        readSize *= 2;
+        // readSize *= 2;
     }
 
     close(fds[0]);

From ee44c8284fcd9b24b26501fedd24b3c1f7185d6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Fri, 22 Aug 2025 08:11:15 +0200
Subject: [PATCH 30/40] fix(wayland): make pasting less allocation-heavy

---
 src/wl_window.c | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/wl_window.c b/src/wl_window.c
index f66b9ed5a9..19ba0cd49d 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1559,14 +1559,17 @@ static char* readDataOffer(struct wl_data_offer* offer, const char* mimeType, si
     size_t size = 0;
     *length = 0;
 
-    size_t readSize = 4096;
+    const size_t readSize = 1024 * 64;
+    size_t allocSize = readSize;
 
     for (;;)
     {
         const size_t requiredSize = *length + readSize + 1;
         if (requiredSize > size)
         {
-            char* longer = _glfw_realloc(data, requiredSize);
+            const size_t newSize = *length + allocSize + 1;
+
+            char* longer = _glfw_realloc(data, newSize);
             if (!longer)
             {
                 _glfwInputError(GLFW_OUT_OF_MEMORY, NULL);
@@ -1575,7 +1578,8 @@ static char* readDataOffer(struct wl_data_offer* offer, const char* mimeType, si
             }
 
             data = longer;
-            size = requiredSize;
+            size = newSize;
+            allocSize *= 2;
         }
 
         const ssize_t result = read(fds[0], data + *length, readSize);
@@ -1594,7 +1598,6 @@ static char* readDataOffer(struct wl_data_offer* offer, const char* mimeType, si
         }
 
         *length += result;
-        // readSize *= 2;
     }
 
     close(fds[0]);

From 84b650fc197ba6b863b95a30db1dc03a34a3718d Mon Sep 17 00:00:00 2001
From: Wenzel Jakob <wenzel.jakob@epfl.ch>
Date: Mon, 25 Aug 2025 22:47:32 +0200
Subject: [PATCH 31/40] silence compilation warnings on macOS and on Linux

---
 src/internal.h | 24 ++++++++++++++++++------
 2 files changed, 20 insertions(+), 7 deletions(-)

diff --git a/src/internal.h b/src/internal.h
index ff37b82e40..df0822b23c 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -80,13 +80,21 @@ typedef struct _GLFWjoystick    _GLFWjoystick;
 typedef struct _GLFWtls         _GLFWtls;
 typedef struct _GLFWmutex       _GLFWmutex;
 
-#define GL_VERSION 0x1f02
+#if !defined(GL_VERSION)
+#  define GL_VERSION 0x1f02
+#endif
 #define GL_NONE 0
 #define GL_COLOR_BUFFER_BIT 0x00004000
 #define GL_UNSIGNED_BYTE 0x1401
-#define GL_EXTENSIONS 0x1f03
-#define GL_NUM_EXTENSIONS 0x821d
-#define GL_CONTEXT_FLAGS 0x821e
+#if !defined(GL_EXTENSIONS)
+#  define GL_EXTENSIONS 0x1f03
+#endif
+#if !defined(GL_NUM_EXTENSIONS)
+#  define GL_NUM_EXTENSIONS 0x821d
+#endif
+#if !defined(GL_CONTEXT_FLAGS)
+#  define GL_CONTEXT_FLAGS 0x821e
+#endif
 #define GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT 0x00000001
 #define GL_CONTEXT_FLAG_DEBUG_BIT 0x00000002
 #define GL_CONTEXT_PROFILE_MASK 0x9126
@@ -95,8 +103,12 @@ typedef struct _GLFWmutex       _GLFWmutex;
 #define GL_RESET_NOTIFICATION_STRATEGY_ARB 0x8256
 #define GL_LOSE_CONTEXT_ON_RESET_ARB 0x8252
 #define GL_NO_RESET_NOTIFICATION_ARB 0x8261
-#define GL_CONTEXT_RELEASE_BEHAVIOR 0x82fb
-#define GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH 0x82fc
+#if !defined (GL_CONTEXT_RELEASE_BEHAVIOR)
+#  define GL_CONTEXT_RELEASE_BEHAVIOR 0x82fb
+#endif
+#if !defined(GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH)
+#  define GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH 0x82fc
+#endif
 #define GL_CONTEXT_FLAG_NO_ERROR_BIT_KHR 0x00000008
 
 typedef int GLint;

From be307b31d18346ca378de0512713fc1d52c715d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tau=20G=C3=A4rtli?= <git@tau.garden>
Date: Sun, 31 Aug 2025 15:00:03 +0200
Subject: [PATCH 32/40] Wayland: Add support for dropping files when sandboxed

Adds support for receiving drops that make use
of the [File Transfer][1] portal. This is the case
in sandboxed environments such as Flatpak or Snap.

[1]: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html
---
 .github/workflows/build.yml |   2 +-
 src/CMakeLists.txt          |   4 ++
 src/wl_platform.h           |   2 +
 src/wl_window.c             | 129 +++++++++++++++++++++++++++++++++++-
 4 files changed, 134 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8ef9ed7931..d7e3c5303f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,7 +21,7 @@ jobs:
             - name: Install dependencies
               run: |
                   sudo apt update
-                  sudo apt install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev libwayland-dev libxkbcommon-dev
+                  sudo apt install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev libwayland-dev libxkbcommon-dev libdbus-1-dev
 
             - name: Configure Null shared library
               run: cmake -B build-null-shared -D GLFW_BUILD_WAYLAND=OFF -D GLFW_BUILD_X11=OFF -D BUILD_SHARED_LIBS=ON
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f2d0c9447f..4f261baa42 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -162,6 +162,10 @@ if (GLFW_BUILD_WAYLAND)
         wayland-egl>=0.2.7
         xkbcommon>=0.5.0)
 
+    pkg_check_modules(DBUS REQUIRED dbus-1)
+    target_include_directories(glfw PRIVATE ${DBUS_INCLUDE_DIRS})
+    target_link_libraries(glfw PRIVATE ${DBUS_LIBRARIES})
+
     target_include_directories(glfw PRIVATE ${Wayland_INCLUDE_DIRS})
 
     if (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
diff --git a/src/wl_platform.h b/src/wl_platform.h
index 333e4fa320..1b68a19251 100644
--- a/src/wl_platform.h
+++ b/src/wl_platform.h
@@ -338,6 +338,7 @@ typedef struct _GLFWofferWayland
     struct wl_data_offer*       offer;
     GLFWbool                    text_plain_utf8;
     GLFWbool                    text_uri_list;
+    GLFWbool                    portal_file_transfer;
 } _GLFWofferWayland;
 
 typedef struct _GLFWscaleWayland
@@ -469,6 +470,7 @@ typedef struct _GLFWlibraryWayland
     struct wl_data_offer*       dragOffer;
     _GLFWwindow*                dragFocus;
     uint32_t                    dragSerial;
+    GLFWbool                    dragUsePortal;
 
     const char*                 tag;
 
diff --git a/src/wl_window.c b/src/wl_window.c
index 19ba0cd49d..d565f56a92 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -41,6 +41,7 @@
 #include <sys/timerfd.h>
 #include <poll.h>
 #include <linux/input-event-codes.h>
+#include <dbus/dbus.h>
 
 #include "wayland-client-protocol.h"
 #include "xdg-shell-client-protocol.h"
@@ -55,6 +56,7 @@
 
 #define GLFW_BORDER_SIZE    4
 #define GLFW_CAPTION_HEIGHT 24
+#define FILE_TRANSFER_PORTAL_MIME_TYPE "application/vnd.portal.filetransfer"
 
 static int createTmpfileCloexec(char* tmpname)
 {
@@ -2173,6 +2175,8 @@ static void dataOfferHandleOffer(void* userData,
                 _glfw.wl.offers[i].text_plain_utf8 = GLFW_TRUE;
             else if (strcmp(mimeType, "text/uri-list") == 0)
                 _glfw.wl.offers[i].text_uri_list = GLFW_TRUE;
+            else if (strcmp(mimeType, FILE_TRANSFER_PORTAL_MIME_TYPE) == 0)
+                _glfw.wl.offers[i].portal_file_transfer = GLFW_TRUE;
 
             break;
         }
@@ -2235,13 +2239,19 @@ static void dataDeviceHandleEnter(void* userData,
         _GLFWwindow* window = wl_surface_get_user_data(surface);
         if (window->wl.surface == surface)
         {
-            if (_glfw.wl.offers[i].text_uri_list)
+            GLFWbool portal = _glfw.wl.offers[i].portal_file_transfer;
+            if (_glfw.wl.offers[i].text_uri_list || portal)
             {
                 _glfw.wl.dragOffer = offer;
                 _glfw.wl.dragFocus = window;
                 _glfw.wl.dragSerial = serial;
+                _glfw.wl.dragUsePortal = portal;
 
-                wl_data_offer_accept(offer, serial, "text/uri-list");
+                if (portal) {
+                    wl_data_offer_accept(offer, serial, FILE_TRANSFER_PORTAL_MIME_TYPE);
+                } else {
+                    wl_data_offer_accept(offer, serial, "text/uri-list");
+                }
             }
         }
     }
@@ -2275,12 +2285,127 @@ static void dataDeviceHandleMotion(void* userData,
 {
 }
 
+// Receives a dropped file that was sent using the
+// [File Transfer](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html) portal.
+// This enables us to receive files when running as a Flatpak or Snap.
+static void dataDeviceHandleFileTransferPortalDrop(void* userData,
+                                                   struct wl_data_device* device)
+{
+    size_t length;
+    char* key = readDataOffer(_glfw.wl.dragOffer, FILE_TRANSFER_PORTAL_MIME_TYPE, &length);
+    if (!key)
+        return;
+
+    DBusError error;
+    dbus_error_init(&error);
+    DBusConnection* connection = dbus_bus_get(DBUS_BUS_SESSION, &error);
+    if (dbus_error_is_set(&error))
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR, "DBus: %s", error.message);
+        dbus_error_free(&error);
+        _glfw_free(key);
+        return;
+    }
+    dbus_connection_set_exit_on_disconnect(connection, FALSE);
+
+    DBusMessage* message = dbus_message_new_method_call(
+        "org.freedesktop.portal.Documents",
+        "/org/freedesktop/portal/documents",
+        "org.freedesktop.portal.FileTransfer",
+        "RetrieveFiles"
+    );
+    if (!message)
+    {
+        _glfwInputError(GLFW_OUT_OF_MEMORY, NULL);
+        _glfw_free(key);
+        return;
+    }
+    DBusMessageIter args, options;
+    dbus_message_iter_init_append(message, &args);
+    if (!dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &key))
+    {
+        _glfwInputError(GLFW_OUT_OF_MEMORY, NULL);
+        dbus_message_unref(message);
+        _glfw_free(key);
+        return;
+    }
+    if (!dbus_message_iter_open_container(&args, DBUS_TYPE_ARRAY, "{sv}", &options))
+    {
+        _glfwInputError(GLFW_OUT_OF_MEMORY, NULL);
+        dbus_message_unref(message);
+        _glfw_free(key);
+        return;
+    }
+    if (!dbus_message_iter_close_container(&args, &options))
+    {
+        _glfwInputError(GLFW_OUT_OF_MEMORY, NULL);
+        dbus_message_unref(message);
+        _glfw_free(key);
+        return;
+    }
+
+    DBusMessage* reply = dbus_connection_send_with_reply_and_block(connection, message, DBUS_TIMEOUT_INFINITE, &error);
+    if (dbus_error_is_set(&error))
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR, "DBus: %s", error.message);
+        dbus_error_free(&error);
+        dbus_message_unref(message);
+        _glfw_free(key);
+        return;
+    }
+
+    DBusMessageIter out, array;
+    if (!dbus_message_iter_init(reply, &out))
+    {
+        _glfwInputError(GLFW_OUT_OF_MEMORY, NULL);
+        dbus_message_unref(reply);
+        dbus_message_unref(message);
+        _glfw_free(key);
+        return;
+    }
+    if (dbus_message_iter_get_arg_type(&out) != DBUS_TYPE_ARRAY
+        || dbus_message_iter_get_element_type(&out) != DBUS_TYPE_STRING) {
+        _glfwInputError(GLFW_PLATFORM_ERROR, "DBus: Reply is not an array of strings");
+        dbus_message_unref(reply);
+        dbus_message_unref(message);
+        _glfw_free(key);
+        return;
+    }
+    dbus_message_iter_recurse(&out, &array);
+    int elements = dbus_message_iter_get_element_count(&out);
+    char** paths = _glfw_calloc(elements, sizeof(char*));
+    if (!paths) {
+        _glfwInputError(GLFW_OUT_OF_MEMORY, NULL);
+        dbus_message_unref(reply);
+        dbus_message_unref(message);
+        _glfw_free(key);
+        return;
+    }
+    int i = 0;
+    do {
+        dbus_message_iter_get_basic(&array, &paths[i++]);
+    } while (dbus_message_iter_next(&array));
+
+    _glfwInputDrop(_glfw.wl.dragFocus, elements, (const char**) paths);
+
+    _glfw_free(paths);
+    dbus_message_unref(reply);
+    dbus_message_unref(message);
+    _glfw_free(key);
+}
+
 static void dataDeviceHandleDrop(void* userData,
                                  struct wl_data_device* device)
 {
     if (!_glfw.wl.dragOffer)
         return;
 
+    if (_glfw.wl.dragUsePortal)
+    {
+        dataDeviceHandleFileTransferPortalDrop(userData, device);
+        return;
+    }
+
     size_t length;
     char* string = readDataOffer(_glfw.wl.dragOffer, "text/uri-list", &length);
     if (string)

From b89327f035d602fccc3f0391bafb3401c09c0ea2 Mon Sep 17 00:00:00 2001
From: Wenzel Jakob <wenzel.jakob@epfl.ch>
Date: Mon, 1 Sep 2025 16:59:34 +0200
Subject: [PATCH 33/40] silence compilation warning on windows

---
 src/win32_window.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/win32_window.c b/src/win32_window.c
index e9428d3b8c..191f8e75ee 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -1669,7 +1669,7 @@ float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window) {
     HMONITOR hMonitor = MonitorFromWindow(window->win32.handle, MONITOR_DEFAULTTONEAREST);
     MONITORINFOEX monitorInfo;
     monitorInfo.cbSize = sizeof(MONITORINFOEX);
-    if (GetMonitorInfo(hMonitor, &monitorInfo)) {
+    if (GetMonitorInfo(hMonitor, (LPMONITORINFO)&monitorInfo)) {
         monitorName = monitorInfo.szDevice;
     }
 

From 1673c5f63744aef7ec9c52dad70b28d16806d62e Mon Sep 17 00:00:00 2001
From: Wenzel Jakob <wenzel.jakob@epfl.ch>
Date: Mon, 1 Sep 2025 17:39:25 +0200
Subject: [PATCH 34/40] object library build fixes

---
 CMakeLists.txt     |  2 ++
 src/CMakeLists.txt | 25 ++++++++++++++++---------
 2 files changed, 18 insertions(+), 9 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 398b36eb1c..6abbad7831 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -40,6 +40,8 @@ set(GLFW_LIBRARY_TYPE "${GLFW_LIBRARY_TYPE}" CACHE STRING
 if (GLFW_LIBRARY_TYPE)
     if (GLFW_LIBRARY_TYPE STREQUAL "SHARED")
         set(GLFW_BUILD_SHARED_LIBRARY TRUE)
+    elseif (GLFW_LIBRARY_TYPE STREQUAL "OBJECT")
+        set(GLFW_BUILD_SHARED_LIBRARY ${BUILD_SHARED_LIBS})
     else()
         set(GLFW_BUILD_SHARED_LIBRARY FALSE)
     endif()
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4f261baa42..fc0022c26c 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -121,15 +121,22 @@ else()
 endif()
 set(GLFW_LIB_NAME_SUFFIX "")
 
-set_target_properties(glfw PROPERTIES
-                      OUTPUT_NAME ${GLFW_LIB_NAME}
-                      VERSION ${GLFW_VERSION_MAJOR}.${GLFW_VERSION_MINOR}
-                      SOVERSION ${GLFW_VERSION_MAJOR}
-                      POSITION_INDEPENDENT_CODE ON
-                      C_STANDARD 99
-                      C_EXTENSIONS OFF
-                      DEFINE_SYMBOL _GLFW_BUILD_DLL
-                      FOLDER "GLFW3")
+if (GLFW_BUILD_SHARED_LIBRARY)
+    set_target_properties(glfw PROPERTIES
+                          OUTPUT_NAME ${GLFW_LIB_NAME}
+                          VERSION ${GLFW_VERSION_MAJOR}.${GLFW_VERSION_MINOR}
+                          SOVERSION ${GLFW_VERSION_MAJOR}
+                          POSITION_INDEPENDENT_CODE ON
+                          C_STANDARD 99
+                          C_EXTENSIONS OFF
+                          FOLDER "GLFW3")
+
+    if (GLFW_LIBRARY_TYPE STREQUAL "OBJECT")
+        target_compile_definitions(glfw PUBLIC _GLFW_BUILD_DLL)
+    else()
+        target_compile_definitions(glfw PRIVATE _GLFW_BUILD_DLL)
+    endif()
+endif()
 
 target_include_directories(glfw PUBLIC
                            "$<BUILD_INTERFACE:${GLFW_SOURCE_DIR}/include>"

From 9742816433a6e9d7a4809871b579cf73520f7d14 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <git@tom94.net>
Date: Fri, 5 Sep 2025 11:08:32 +0200
Subject: [PATCH 35/40] fix(cocoa): possible input event stall

---
 src/cocoa_window.m | 87 +++++++++++++++++++++++++++-------------------
 1 file changed, 52 insertions(+), 35 deletions(-)

diff --git a/src/cocoa_window.m b/src/cocoa_window.m
index 7671c7835b..7afef77bcc 100644
--- a/src/cocoa_window.m
+++ b/src/cocoa_window.m
@@ -1540,58 +1540,75 @@ GLFWbool _glfwRawMouseMotionSupportedCocoa(void)
     return GLFW_FALSE;
 }
 
-void _glfwPollEventsCocoa(void)
+void _glfwProcessEventsCocoa(GLFWbool wait, double timeout)
 {
     @autoreleasepool {
 
-    for (;;)
+    size_t size = 16; // Initial size is a 128 byte cacheline on Apple Silicon
+    NSEvent** events = _glfw_calloc(size, sizeof(NSEvent*));
+    int count = 0;
+
+    if (wait)
     {
-        NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny
-                                            untilDate:[NSDate distantPast]
-                                               inMode:NSDefaultRunLoopMode
-                                              dequeue:YES];
-        if (event == nil)
-            break;
+        NSDate* date;
+        if (timeout == 0.0)
+            date = [NSDate distantFuture];
+        else
+            date = [NSDate dateWithTimeIntervalSinceNow:timeout];
 
-        [NSApp sendEvent:event];
+        // I wanted to pass NO to dequeue:, and rely on PollEvents to
+        // dequeue and send.  For reasons not at all clear to me, passing
+        // NO to dequeue: causes this method never to return.
+        events[count] = [NSApp nextEventMatchingMask:NSEventMaskAny
+                                           untilDate:date
+                                              inMode:NSDefaultRunLoopMode
+                                             dequeue:YES];
+
+        if (events[count++] == nil)
+        {
+            _glfw_free(events);
+            return;
+        }
     }
 
-    } // autoreleasepool
-}
+    for (;; count++)
+    {
+        if (count == size)
+        {
+            size *= 2;
+            events = _glfw_realloc(events, size * sizeof(NSEvent*));
+        }
 
-void _glfwWaitEventsCocoa(void)
-{
-    @autoreleasepool {
+        events[count] = [NSApp nextEventMatchingMask:NSEventMaskAny
+                                           untilDate:[NSDate distantPast]
+                                              inMode:NSDefaultRunLoopMode
+                                             dequeue:YES];
 
-    // I wanted to pass NO to dequeue:, and rely on PollEvents to
-    // dequeue and send.  For reasons not at all clear to me, passing
-    // NO to dequeue: causes this method never to return.
-    NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
-                                        untilDate:[NSDate distantFuture]
-                                           inMode:NSDefaultRunLoopMode
-                                          dequeue:YES];
-    [NSApp sendEvent:event];
+        if (events[count] == nil)
+            break;
+    }
 
-    _glfwPollEventsCocoa();
+    for (size_t i = 0; i < count; i++)
+        [NSApp sendEvent:events[i]];
+
+    _glfw_free(events);
 
     } // autoreleasepool
 }
 
-void _glfwWaitEventsTimeoutCocoa(double timeout)
+void _glfwPollEventsCocoa(void)
 {
-    @autoreleasepool {
-
-    NSDate* date = [NSDate dateWithTimeIntervalSinceNow:timeout];
-    NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny
-                                        untilDate:date
-                                           inMode:NSDefaultRunLoopMode
-                                          dequeue:YES];
-    if (event)
-        [NSApp sendEvent:event];
+    _glfwProcessEventsCocoa(false, 0.0);
+}
 
-    _glfwPollEventsCocoa();
+void _glfwWaitEventsCocoa(void)
+{
+    _glfwProcessEventsCocoa(true, 0.0);
+}
 
-    } // autoreleasepool
+void _glfwWaitEventsTimeoutCocoa(double timeout)
+{
+    _glfwProcessEventsCocoa(true, timeout);
 }
 
 void _glfwPostEmptyEventCocoa(void)

From 8cfb77ba95390ea70ead58688de9071ea6befe25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <tom@94.me>
Date: Wed, 12 Nov 2025 12:25:58 +0100
Subject: [PATCH 36/40] fix(wayland): broken window resize on some compositors
 (#9)

Gnome and KDE worked previously already. This commit fixes floating
windows in Hyprland and potentially other Wayland tiling compositors
(not tested).
---
 src/wl_window.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/wl_window.c b/src/wl_window.c
index d565f56a92..4d10bbe890 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -2721,6 +2721,26 @@ void _glfwSetWindowSizeWayland(_GLFWwindow* window, int width, int height)
             libdecor_state_free(frameState);
         }
 
+        if (window->wl.xdg.toplevel)
+        {
+            // Some Wayland compositors (e.g. Hyprland) require setting both
+            // min and max size to the same values to effectively resize a
+            // floating window. Hence, when resizing under wayland, set min/max
+            // sizes to the newly desired window size for a moment, then
+            // restore the limits.
+            // https://github.com/hyprwm/Hyprland/discussions/11723
+            xdg_toplevel_set_min_size(window->wl.xdg.toplevel,
+                                      window->wl.width,
+                                      window->wl.height);
+            xdg_toplevel_set_max_size(window->wl.xdg.toplevel,
+                                      window->wl.width,
+                                      window->wl.height);
+
+            wl_surface_commit(window->wl.surface);
+
+            updateXdgSizeLimits(window);
+        }
+
         if (window->wl.visible)
             _glfwInputWindowDamage(window);
     }

From 49bf38196ebe630a5a4f67db2c9d7dbdf246812f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <tom@94.me>
Date: Wed, 12 Nov 2025 12:26:48 +0100
Subject: [PATCH 37/40] feat(wayland): use `axis_value120` instead of `axis`
 events if supported (#7)

`axis_value120` events are precise while, unlike `axis` events, having a
clear correspondence to discrete scroll events (detents in the mouse
wheel). This makes for more precise scroll handling overall.

This commit also adjusts the default scale factor of `axis` events to 15
(from 10) to make it more in line with typical detent scale.
---
 src/wl_init.c   |  2 +-
 src/wl_window.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 53 insertions(+), 4 deletions(-)

diff --git a/src/wl_init.c b/src/wl_init.c
index 0d3f05eb45..5d60254904 100644
--- a/src/wl_init.c
+++ b/src/wl_init.c
@@ -223,7 +223,7 @@ static void registryHandleGlobal(void* userData,
         {
             _glfw.wl.seat =
                 wl_registry_bind(registry, name, &wl_seat_interface,
-                                 _glfw_min(4, version));
+                                 _glfw_min(8, version));
             _glfwAddSeatListenerWayland(_glfw.wl.seat);
 
             if (wl_seat_get_version(_glfw.wl.seat) >=
diff --git a/src/wl_window.c b/src/wl_window.c
index 4d10bbe890..45a38b475f 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1862,11 +1862,55 @@ static void pointerHandleAxis(void* userData,
     if (!window)
         return;
 
-    // NOTE: 10 units of motion per mouse wheel step seems to be a common ratio
+    if (wl_pointer_get_version(pointer) >= WL_POINTER_AXIS_VALUE120_SINCE_VERSION)
+        // Ignore this event, as we will get a more precise axis_value120 event
+        return;
+
+    // NOTE: 15 units of motion per mouse wheel step seems to be a common ratio to wheel detents
+    if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL)
+        _glfwInputScroll(window, -wl_fixed_to_double(value) / 15.0, 0.0);
+    else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL)
+        _glfwInputScroll(window, 0.0, -wl_fixed_to_double(value) / 15.0);
+}
+
+static void pointerHandleFrame(void* userData,
+                               struct wl_pointer* pointer)
+{
+}
+
+static void pointerHandleAxisSource(void* userData,
+                                    struct wl_pointer* pointer,
+                                    uint32_t axisSource)
+{
+}
+
+static void pointerHandleAxisStop(void* userData,
+                                  struct wl_pointer* pointer,
+                                  uint32_t time,
+                                  uint32_t axis)
+{
+}
+
+static void pointerHandleAxisDiscrete(void* userData,
+                                      struct wl_pointer* pointer,
+                                      uint32_t axis,
+                                      int32_t discrete)
+{
+}
+
+static void pointerHandleAxisValue120(void* userData,
+                                     struct wl_pointer* pointer,
+                                     uint32_t axis,
+                                     int32_t value120)
+{
+    _GLFWwindow* window = _glfw.wl.pointerFocus;
+    if (!window)
+        return;
+
     if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL)
-        _glfwInputScroll(window, -wl_fixed_to_double(value) / 10.0, 0.0);
+        _glfwInputScroll(window, -value120 / 120.0f, 0.0);
     else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL)
-        _glfwInputScroll(window, 0.0, -wl_fixed_to_double(value) / 10.0);
+        _glfwInputScroll(window, 0.0, -value120 / 120.0f);
 }
 
 static const struct wl_pointer_listener pointerListener =
@@ -1876,6 +1920,11 @@ static const struct wl_pointer_listener pointerListener =
     pointerHandleMotion,
     pointerHandleButton,
     pointerHandleAxis,
+    pointerHandleFrame,
+    pointerHandleAxisSource,
+    pointerHandleAxisStop,
+    pointerHandleAxisDiscrete,
+    pointerHandleAxisValue120,
 };
 
 static void keyboardHandleKeymap(void* userData,

From ef18903873c56ed576406dcb51795630a2493e6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <tom@94.me>
Date: Wed, 12 Nov 2025 12:27:17 +0100
Subject: [PATCH 38/40] Add ability to query the monitor the window is
 currently (mostly) on (#8)

* feat(wayland): keep track of current monitor of the window

* feat(cocoa): implement glfwGetWindowCurrentMonitor

* feat(x11): implement glfwGetWindowCurrentMonitor

The implementation checks which monitor's work area overlaps the center
of the window.

* feat(windows): implement glfwGetWindowCurrentMonitor
---
 include/GLFW/glfw3.h |  2 ++
 src/cocoa_init.m     |  1 +
 src/cocoa_platform.h |  1 +
 src/cocoa_window.m   | 21 +++++++++++++++++++++
 src/internal.h       |  1 +
 src/null_init.c      |  1 +
 src/null_platform.h  |  1 +
 src/null_window.c    |  5 +++++
 src/win32_init.c     |  1 +
 src/win32_platform.h |  1 +
 src/win32_window.c   | 17 +++++++++++++++++
 src/window.c         | 10 ++++++++++
 src/wl_init.c        |  1 +
 src/wl_platform.h    |  1 +
 src/wl_window.c      | 10 ++++++++++
 src/x11_init.c       |  1 +
 src/x11_platform.h   |  1 +
 src/x11_window.c     | 27 +++++++++++++++++++++++++++
 18 files changed, 103 insertions(+)

diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h
index 5328d5aa68..88cdee19a3 100644
--- a/include/GLFW/glfw3.h
+++ b/include/GLFW/glfw3.h
@@ -4035,6 +4035,8 @@ GLFWAPI void glfwRequestWindowAttention(GLFWwindow* window);
  */
 GLFWAPI GLFWmonitor* glfwGetWindowMonitor(GLFWwindow* window);
 
+GLFWAPI GLFWmonitor* glfwGetWindowCurrentMonitor(GLFWwindow* window);
+
 /*! @brief Sets the mode, monitor, video mode and placement of a window.
  *
  *  This function sets the monitor that the window uses for full screen mode or,
diff --git a/src/cocoa_init.m b/src/cocoa_init.m
index f7825de862..0f9715f4b2 100644
--- a/src/cocoa_init.m
+++ b/src/cocoa_init.m
@@ -565,6 +565,7 @@ GLFWbool _glfwConnectCocoa(int platformID, _GLFWplatform* platform)
         .requestWindowAttention = _glfwRequestWindowAttentionCocoa,
         .focusWindow = _glfwFocusWindowCocoa,
         .setWindowMonitor = _glfwSetWindowMonitorCocoa,
+        .getWindowCurrentMonitor = _glfwGetWindowCurrentMonitorCocoa,
         .windowFocused = _glfwWindowFocusedCocoa,
         .windowIconified = _glfwWindowIconifiedCocoa,
         .windowVisible = _glfwWindowVisibleCocoa,
diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h
index 7e12c48f16..fa07fd308f 100644
--- a/src/cocoa_platform.h
+++ b/src/cocoa_platform.h
@@ -244,6 +244,7 @@ void _glfwHideWindowCocoa(_GLFWwindow* window);
 void _glfwRequestWindowAttentionCocoa(_GLFWwindow* window);
 void _glfwFocusWindowCocoa(_GLFWwindow* window);
 void _glfwSetWindowMonitorCocoa(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
+GLFWmonitor* _glfwGetWindowCurrentMonitorCocoa(_GLFWwindow* window);
 GLFWbool _glfwWindowFocusedCocoa(_GLFWwindow* window);
 GLFWbool _glfwWindowIconifiedCocoa(_GLFWwindow* window);
 GLFWbool _glfwWindowVisibleCocoa(_GLFWwindow* window);
diff --git a/src/cocoa_window.m b/src/cocoa_window.m
index 7afef77bcc..8194cbc05a 100644
--- a/src/cocoa_window.m
+++ b/src/cocoa_window.m
@@ -1394,6 +1394,27 @@ void _glfwSetWindowMonitorCocoa(_GLFWwindow* window,
     } // autoreleasepool
 }
 
+GLFWmonitor* _glfwGetWindowCurrentMonitorCocoa(_GLFWwindow* window)
+{
+    @autoreleasepool {
+
+    const NSScreen* screen = [window->ns.object screen] ?: [NSScreen mainScreen];
+
+    int monitorCount;
+    GLFWmonitor **monitors = glfwGetMonitors(&monitorCount);
+
+    for (int i = 0;  i < monitorCount;  i++)
+    {
+        _GLFWmonitor* monitor = (_GLFWmonitor*) monitors[i];
+        if (monitor->ns.screen == screen)
+            return monitors[i];
+    }
+
+    return NULL;
+
+    } // autoreleasepool
+}
+
 GLFWbool _glfwWindowFocusedCocoa(_GLFWwindow* window)
 {
     @autoreleasepool {
diff --git a/src/internal.h b/src/internal.h
index df0822b23c..e7895b3e92 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -755,6 +755,7 @@ struct _GLFWplatform
     void (*requestWindowAttention)(_GLFWwindow*);
     void (*focusWindow)(_GLFWwindow*);
     void (*setWindowMonitor)(_GLFWwindow*,_GLFWmonitor*,int,int,int,int,int);
+    GLFWmonitor* (*getWindowCurrentMonitor)(_GLFWwindow*);
     GLFWbool (*windowFocused)(_GLFWwindow*);
     GLFWbool (*windowIconified)(_GLFWwindow*);
     GLFWbool (*windowVisible)(_GLFWwindow*);
diff --git a/src/null_init.c b/src/null_init.c
index d384008602..4cc5846ecd 100644
--- a/src/null_init.c
+++ b/src/null_init.c
@@ -94,6 +94,7 @@ GLFWbool _glfwConnectNull(int platformID, _GLFWplatform* platform)
         .requestWindowAttention = _glfwRequestWindowAttentionNull,
         .focusWindow = _glfwFocusWindowNull,
         .setWindowMonitor = _glfwSetWindowMonitorNull,
+        .getWindowCurrentMonitor = _glfwGetWindowCurrentMonitorNull,
         .windowFocused = _glfwWindowFocusedNull,
         .windowIconified = _glfwWindowIconifiedNull,
         .windowVisible = _glfwWindowVisibleNull,
diff --git a/src/null_platform.h b/src/null_platform.h
index 438fe37410..199f7c80f4 100644
--- a/src/null_platform.h
+++ b/src/null_platform.h
@@ -224,6 +224,7 @@ void _glfwDestroyWindowNull(_GLFWwindow* window);
 void _glfwSetWindowTitleNull(_GLFWwindow* window, const char* title);
 void _glfwSetWindowIconNull(_GLFWwindow* window, int count, const GLFWimage* images);
 void _glfwSetWindowMonitorNull(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
+GLFWmonitor* _glfwGetWindowCurrentMonitorNull(_GLFWwindow* window);
 void _glfwGetWindowPosNull(_GLFWwindow* window, int* xpos, int* ypos);
 void _glfwSetWindowPosNull(_GLFWwindow* window, int xpos, int ypos);
 void _glfwGetWindowSizeNull(_GLFWwindow* window, int* width, int* height);
diff --git a/src/null_window.c b/src/null_window.c
index 52b828650e..5bc63a5089 100644
--- a/src/null_window.c
+++ b/src/null_window.c
@@ -221,6 +221,11 @@ void _glfwSetWindowMonitorNull(_GLFWwindow* window,
     }
 }
 
+GLFWmonitor* _glfwGetWindowCurrentMonitorNull(_GLFWwindow* window)
+{
+    return NULL;
+}
+
 void _glfwGetWindowPosNull(_GLFWwindow* window, int* xpos, int* ypos)
 {
     if (xpos)
diff --git a/src/win32_init.c b/src/win32_init.c
index cafc6ef416..5bb99a74b4 100644
--- a/src/win32_init.c
+++ b/src/win32_init.c
@@ -653,6 +653,7 @@ GLFWbool _glfwConnectWin32(int platformID, _GLFWplatform* platform)
         .requestWindowAttention = _glfwRequestWindowAttentionWin32,
         .focusWindow = _glfwFocusWindowWin32,
         .setWindowMonitor = _glfwSetWindowMonitorWin32,
+        .getWindowCurrentMonitor = _glfwGetWindowCurrentMonitorWin32,
         .windowFocused = _glfwWindowFocusedWin32,
         .windowIconified = _glfwWindowIconifiedWin32,
         .windowVisible = _glfwWindowVisibleWin32,
diff --git a/src/win32_platform.h b/src/win32_platform.h
index 6fd05947c6..e47ae801c5 100644
--- a/src/win32_platform.h
+++ b/src/win32_platform.h
@@ -509,6 +509,7 @@ void _glfwHideWindowWin32(_GLFWwindow* window);
 void _glfwRequestWindowAttentionWin32(_GLFWwindow* window);
 void _glfwFocusWindowWin32(_GLFWwindow* window);
 void _glfwSetWindowMonitorWin32(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
+GLFWmonitor* _glfwGetWindowCurrentMonitorWin32(_GLFWwindow* window);
 GLFWbool _glfwWindowFocusedWin32(_GLFWwindow* window);
 GLFWbool _glfwWindowIconifiedWin32(_GLFWwindow* window);
 GLFWbool _glfwWindowVisibleWin32(_GLFWwindow* window);
diff --git a/src/win32_window.c b/src/win32_window.c
index 191f8e75ee..391fac5e06 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -2056,6 +2056,23 @@ void _glfwSetWindowMonitorWin32(_GLFWwindow* window,
     }
 }
 
+GLFWmonitor* _glfwGetWindowCurrentMonitorWin32(_GLFWwindow* window)
+{
+    HMONITOR hMonitor = MonitorFromWindow(window->win32.handle, MONITOR_DEFAULTTONEAREST);
+
+    int monitorCount;
+    GLFWmonitor **monitors = glfwGetMonitors(&monitorCount);
+
+    for (int i = 0;  i < monitorCount;  i++)
+    {
+        _GLFWmonitor* monitor = (_GLFWmonitor*) monitors[i];
+        if (monitor->win32.handle == hMonitor)
+            return monitors[i];
+    }
+
+    return NULL;
+}
+
 GLFWbool _glfwWindowFocusedWin32(_GLFWwindow* window)
 {
     return window->win32.handle == GetActiveWindow();
diff --git a/src/window.c b/src/window.c
index 9b76b7c432..22fa2285b4 100644
--- a/src/window.c
+++ b/src/window.c
@@ -1054,6 +1054,16 @@ GLFWAPI GLFWmonitor* glfwGetWindowMonitor(GLFWwindow* handle)
     return (GLFWmonitor*) window->monitor;
 }
 
+GLFWAPI GLFWmonitor* glfwGetWindowCurrentMonitor(GLFWwindow* handle)
+{
+    _GLFW_REQUIRE_INIT_OR_RETURN(NULL);
+
+    _GLFWwindow* window = (_GLFWwindow*) handle;
+    assert(window != NULL);
+
+    return _glfw.platform.getWindowCurrentMonitor(window);
+}
+
 GLFWAPI void glfwSetWindowMonitor(GLFWwindow* wh,
                                   GLFWmonitor* mh,
                                   int xpos, int ypos,
diff --git a/src/wl_init.c b/src/wl_init.c
index 5d60254904..0e6a3113ce 100644
--- a/src/wl_init.c
+++ b/src/wl_init.c
@@ -600,6 +600,7 @@ GLFWbool _glfwConnectWayland(int platformID, _GLFWplatform* platform)
         .requestWindowAttention = _glfwRequestWindowAttentionWayland,
         .focusWindow = _glfwFocusWindowWayland,
         .setWindowMonitor = _glfwSetWindowMonitorWayland,
+        .getWindowCurrentMonitor = _glfwGetWindowCurrentMonitorWayland,
         .windowFocused = _glfwWindowFocusedWayland,
         .windowIconified = _glfwWindowIconifiedWayland,
         .windowVisible = _glfwWindowVisibleWayland,
diff --git a/src/wl_platform.h b/src/wl_platform.h
index 1b68a19251..d56565e4a7 100644
--- a/src/wl_platform.h
+++ b/src/wl_platform.h
@@ -667,6 +667,7 @@ void _glfwHideWindowWayland(_GLFWwindow* window);
 void _glfwRequestWindowAttentionWayland(_GLFWwindow* window);
 void _glfwFocusWindowWayland(_GLFWwindow* window);
 void _glfwSetWindowMonitorWayland(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
+GLFWmonitor* _glfwGetWindowCurrentMonitorWayland(_GLFWwindow* window);
 GLFWbool _glfwWindowFocusedWayland(_GLFWwindow* window);
 GLFWbool _glfwWindowIconifiedWayland(_GLFWwindow* window);
 GLFWbool _glfwWindowVisibleWayland(_GLFWwindow* window);
diff --git a/src/wl_window.c b/src/wl_window.c
index 45a38b475f..f648ffe6cf 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -3034,6 +3034,16 @@ void _glfwSetWindowMonitorWayland(_GLFWwindow* window,
         _glfwSetWindowSizeWayland(window, width, height);
 }
 
+GLFWmonitor* _glfwGetWindowCurrentMonitorWayland(_GLFWwindow* window)
+{
+    if (window->wl.outputScaleCount > 0)
+    {
+        return wl_output_get_user_data(window->wl.outputScales[0].output);
+    }
+
+    return NULL;
+}
+
 GLFWbool _glfwWindowFocusedWayland(_GLFWwindow* window)
 {
     return _glfw.wl.keyboardFocus == window;
diff --git a/src/x11_init.c b/src/x11_init.c
index f73e25a944..062eea1bb8 100644
--- a/src/x11_init.c
+++ b/src/x11_init.c
@@ -1230,6 +1230,7 @@ GLFWbool _glfwConnectX11(int platformID, _GLFWplatform* platform)
         .requestWindowAttention = _glfwRequestWindowAttentionX11,
         .focusWindow = _glfwFocusWindowX11,
         .setWindowMonitor = _glfwSetWindowMonitorX11,
+        .getWindowCurrentMonitor = _glfwGetWindowCurrentMonitorX11,
         .windowFocused = _glfwWindowFocusedX11,
         .windowIconified = _glfwWindowIconifiedX11,
         .windowVisible = _glfwWindowVisibleX11,
diff --git a/src/x11_platform.h b/src/x11_platform.h
index a4fc749df5..2935ce74c0 100644
--- a/src/x11_platform.h
+++ b/src/x11_platform.h
@@ -929,6 +929,7 @@ void _glfwHideWindowX11(_GLFWwindow* window);
 void _glfwRequestWindowAttentionX11(_GLFWwindow* window);
 void _glfwFocusWindowX11(_GLFWwindow* window);
 void _glfwSetWindowMonitorX11(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
+GLFWmonitor* _glfwGetWindowCurrentMonitorX11(_GLFWwindow* window);
 GLFWbool _glfwWindowFocusedX11(_GLFWwindow* window);
 GLFWbool _glfwWindowIconifiedX11(_GLFWwindow* window);
 GLFWbool _glfwWindowVisibleX11(_GLFWwindow* window);
diff --git a/src/x11_window.c b/src/x11_window.c
index 8f6b565614..3489a4e8a1 100644
--- a/src/x11_window.c
+++ b/src/x11_window.c
@@ -2548,6 +2548,33 @@ void _glfwSetWindowMonitorX11(_GLFWwindow* window,
     XFlush(_glfw.x11.display);
 }
 
+GLFWmonitor* _glfwGetWindowCurrentMonitorX11(_GLFWwindow* window)
+{
+    int sizeX, sizeY;
+    _glfwGetWindowSizeX11(window, &sizeX, &sizeY);
+    int centerX, centerY;
+    _glfwGetWindowPosX11(window, &centerX, &centerY);
+    centerX += sizeX / 2;
+    centerY += sizeY / 2;
+
+    int monitorCount = 0;
+    GLFWmonitor** monitors = glfwGetMonitors(&monitorCount);
+    for (int i = 0;  i < monitorCount;  i++)
+    {
+        GLFWmonitor* monitor = monitors[i];
+        int x, y, width, height;
+        glfwGetMonitorWorkarea(monitor, &x, &y, &width, &height);
+
+        if (centerX >= x && centerX < x + width &&
+            centerY >= y && centerY < y + height)
+        {
+            return monitor;
+        }
+    }
+
+    return NULL;
+}
+
 GLFWbool _glfwWindowFocusedX11(_GLFWwindow* window)
 {
     Window focused;

From 15cc1e01adcde2ab69347382d5291d8f22638b58 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <tom@94.me>
Date: Mon, 8 Dec 2025 12:21:16 +0100
Subject: [PATCH 39/40] Windows: improve SDR/HDR detection (#10)

* fix(windows): minor correctness bug in SDR white level query

* feat(windows): detect capped max luminance (wide color but SDR)
---
 src/win32_window.c | 106 ++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 101 insertions(+), 5 deletions(-)

diff --git a/src/win32_window.c b/src/win32_window.c
index 391fac5e06..a4baf3d8d7 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -1697,7 +1697,6 @@ float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window) {
         return 80.0f;
     }
 
-    // Find the first active path TODO: find path for the current window
     for (UINT32 i = 0; i < numPaths; i++) {
         if (!(paths[i].flags & DISPLAYCONFIG_PATH_ACTIVE)) {
             continue;
@@ -1725,10 +1724,16 @@ float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window) {
         advancedColorInfo.header.id = paths[i].targetInfo.id;
         result = DisplayConfigGetDeviceInfo(&advancedColorInfo.header);
 
-        if (result != ERROR_SUCCESS || advancedColorInfo.advancedColorEnabled == 0) {
+        if (result != ERROR_SUCCESS) {
             continue;
         }
 
+        if (advancedColorInfo.advancedColorEnabled == 0) {
+            _glfw_free(paths);
+            _glfw_free(modes);
+            return 80.0f;
+        }
+
         DISPLAYCONFIG_SDR_WHITE_LEVEL whiteLevel;
         memset(&whiteLevel, 0, sizeof(whiteLevel));
         whiteLevel.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
@@ -1739,13 +1744,13 @@ float _glfwGetWindowSdrWhiteLevelWin32(_GLFWwindow* window) {
             continue;
         }
 
+        _glfw_free(paths);
+        _glfw_free(modes);
         return whiteLevel.SDRWhiteLevel / 1000.0f * 80.0f;
     }
 
     _glfw_free(paths);
     _glfw_free(modes);
-
-    memset(0, 0, 0);
     return 80.0f; // sRGB standard white level
 }
 
@@ -1753,8 +1758,99 @@ float _glfwGetWindowMinLuminanceWin32(_GLFWwindow* window) {
     return 0.0f;
 }
 
+GLFWbool _glfwGetWindowAdvancedColorEnabledWin32(_GLFWwindow* window) {
+    if (window->bitsPerSample != 16) {
+        // If we don't have a fp16 frame buffer, Windows does not expect scRGB
+        // with proper SDR white level scaling, it instead expects standard
+        // sRGB whose reference white level should be 80 nits. (Even though the
+        // screen's reference white level -- obtained by the bottom code --
+        // might be different. In that case Windows does the remapping for us.)
+        return GLFW_FALSE;
+    }
+
+    UINT32 numPaths, numModes;
+    LONG result;
+
+    const TCHAR* monitorName = NULL;
+
+    HMONITOR hMonitor = MonitorFromWindow(window->win32.handle, MONITOR_DEFAULTTONEAREST);
+    MONITORINFOEX monitorInfo;
+    monitorInfo.cbSize = sizeof(MONITORINFOEX);
+    if (GetMonitorInfo(hMonitor, (LPMONITORINFO)&monitorInfo)) {
+        monitorName = monitorInfo.szDevice;
+    }
+
+    // Get the number of paths and modes
+    result = GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &numPaths, &numModes);
+    if (result != ERROR_SUCCESS) {
+        return GLFW_FALSE;
+    }
+
+    // Allocate memory for the paths and modes
+    DISPLAYCONFIG_PATH_INFO* paths = _glfw_calloc(numPaths, sizeof(DISPLAYCONFIG_PATH_INFO));
+    DISPLAYCONFIG_MODE_INFO* modes = _glfw_calloc(numModes, sizeof(DISPLAYCONFIG_MODE_INFO));
+
+    if (!paths || !modes) {
+        _glfw_free(paths);
+        _glfw_free(modes);
+        return GLFW_FALSE;
+    }
+
+    // Query the display configuration
+    result = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &numPaths, paths, &numModes, modes, NULL);
+    if (result != ERROR_SUCCESS) {
+        _glfw_free(paths);
+        _glfw_free(modes);
+        return GLFW_FALSE;
+    }
+
+    for (UINT32 i = 0; i < numPaths; i++) {
+        if (!(paths[i].flags & DISPLAYCONFIG_PATH_ACTIVE)) {
+            continue;
+        }
+
+        DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName;
+        memset(&sourceName, 0, sizeof(sourceName));
+        sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
+        sourceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME);
+        sourceName.header.adapterId = paths[i].sourceInfo.adapterId;
+        sourceName.header.id = paths[i].sourceInfo.id;
+
+        result = DisplayConfigGetDeviceInfo(&sourceName.header);
+
+        // If we have a monitor name, only check for sdr white level on the monitor that it matches
+        if (result != ERROR_SUCCESS || (monitorName && wcscmp(sourceName.viewGdiDeviceName, monitorName) != 0)) {
+            continue;
+        }
+
+        DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO advancedColorInfo;
+        memset(&advancedColorInfo, 0, sizeof(advancedColorInfo));
+        advancedColorInfo.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
+        advancedColorInfo.header.size = sizeof(advancedColorInfo);
+        advancedColorInfo.header.adapterId = paths[i].targetInfo.adapterId;
+        advancedColorInfo.header.id = paths[i].targetInfo.id;
+        result = DisplayConfigGetDeviceInfo(&advancedColorInfo.header);
+
+        if (result != ERROR_SUCCESS) {
+            continue;
+        }
+
+        _glfw_free(paths);
+        _glfw_free(modes);
+
+        return advancedColorInfo.advancedColorEnabled != 0;
+    }
+
+    _glfw_free(paths);
+    _glfw_free(modes);
+
+    return GLFW_FALSE;
+}
+
 float _glfwGetWindowMaxLuminanceWin32(_GLFWwindow* window) {
-    return 0.0f;
+    // If advanced color is not enabled, return standard sRGB max luminance (not HDR).
+    // Otherwise return 0.0 to indicate no known limit.
+    return _glfwGetWindowAdvancedColorEnabledWin32(window) ? 0.0f : 80.0f;
 }
 
 uint32_t _glfwGetWindowPrimariesWin32(_GLFWwindow* window)

From 38f86be2c9495a4aaacbf5360c0f79f729576a9d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <tom@94.me>
Date: Tue, 6 Jan 2026 14:18:35 +0100
Subject: [PATCH 40/40] feat(wayland): use sRGB instead of PQ transfer function
 when <=80 nits (#11)

This change works around a newly introduces bug in Hyprland (starting
with v0.53) where PQ is rendered with incorrect brightness in SDR mode.
The displayed colors are equivalent either way (up to
quantization/dithering artifacts) and PQ's extra headroom is unnecessary
in SDR environments. KDE and Gnome's tev rendering is unaffected by this
change.

feat(wayland): improve transfer function/primaries selection

Prefer extended sRGB+rec709 primaries over HLG for improved
compatibility. (Not observed in practice, but more of a just-in-case
change after having encountered issues with HLG in image formats.)
---
 src/wl_window.c | 33 ++++++++++++++++++---------------
 1 file changed, 18 insertions(+), 15 deletions(-)

diff --git a/src/wl_window.c b/src/wl_window.c
index f648ffe6cf..882a8568a2 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1223,8 +1223,8 @@ static GLFWbool supportsColorManagement(_GLFWwindow* window)
         return GLFW_FALSE;
 
     if (!_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB] &&
+        !_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22] &&
         !_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ] &&
-        !_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG] &&
         !_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB])
         return GLFW_FALSE;
 
@@ -2699,28 +2699,31 @@ float _glfwGetWindowMaxLuminanceWayland(_GLFWwindow* window)
     return window->wl.maxLuminance;
 }
 
-uint32_t _glfwGetWindowPrimariesWayland(_GLFWwindow* window)
+uint32_t _glfwGetWindowTransferWayland(_GLFWwindow* window)
 {
     if (!supportsColorManagement(window))
-        return WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
+        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22;
 
-    if (_glfw.wl.colorManagerSupport.primaries[WP_COLOR_MANAGER_V1_PRIMARIES_BT2020])
-        return WP_COLOR_MANAGER_V1_PRIMARIES_BT2020;
-    return WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
+    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ]
+        && (window->wl.maxLuminance == 0.0f || window->wl.maxLuminance > 80.0f))
+        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ;
+
+    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB])
+        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB;
+    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB])
+        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
+    return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22;
 }
 
-uint32_t _glfwGetWindowTransferWayland(_GLFWwindow* window)
+uint32_t _glfwGetWindowPrimariesWayland(_GLFWwindow* window)
 {
     if (!supportsColorManagement(window))
-        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
+        return WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
 
-    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ])
-        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ;
-    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG])
-        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG;
-    if (_glfw.wl.colorManagerSupport.tfs[WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB])
-        return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB;
-    return WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB;
+    if (_glfw.wl.colorManagerSupport.primaries[WP_COLOR_MANAGER_V1_PRIMARIES_BT2020])
+        return WP_COLOR_MANAGER_V1_PRIMARIES_BT2020;
+
+    return WP_COLOR_MANAGER_V1_PRIMARIES_SRGB;
 }
 
 uint32_t _glfwGetWindowRenderingIntentWayland(_GLFWwindow* window)
