SDL: Get the correct HDR values under Gamescope

From 326694de32e3243e1d5ec7ce54b5b487729779fa Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 23 Jul 2026 10:32:03 -0700
Subject: [PATCH] Get the correct HDR values under Gamescope

This also switches log messages in SDL_x11modes.c to use SDL_Log()
---
 src/video/x11/SDL_x11events.c |   5 ++
 src/video/x11/SDL_x11modes.c  | 151 ++++++++++++++++++++++++++--------
 src/video/x11/SDL_x11modes.h  |   1 +
 3 files changed, 121 insertions(+), 36 deletions(-)

diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index a500cba637886..7a8654582a86f 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -1455,6 +1455,11 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
                     for (i = 0; i < _this->num_displays; ++i) {
                         SDL_SendDisplayEvent(_this->displays[i], SDL_EVENT_DISPLAY_USABLE_BOUNDS_CHANGED, 0, 0);
                     }
+#ifdef SDL_VIDEO_DRIVER_X11_XRANDR
+                } else if (SDL_strcmp(name_of_atom, "GAMESCOPE_DISPLAY_IS_EXTERNAL") == 0 ||
+                           SDL_strcmp(name_of_atom, "GAMESCOPE_SDR_ON_HDR_CONTENT_BRIGHTNESS") == 0) {
+                    X11_CheckDisplaysMoved(_this, display);
+#endif
                 }
                 X11_XFree(name_of_atom);
             }
diff --git a/src/video/x11/SDL_x11modes.c b/src/video/x11/SDL_x11modes.c
index 6da56b08baeb6..37068b9508b28 100644
--- a/src/video/x11/SDL_x11modes.c
+++ b/src/video/x11/SDL_x11modes.c
@@ -26,6 +26,7 @@
 #include "SDL_x11settings.h"
 #include "edid.h"
 #include "../../events/SDL_displayevents_c.h"
+#include "../SDL_pixels_c.h"
 
 // #define X11MODES_DEBUG
 
@@ -344,14 +345,14 @@ static bool CheckXRandR(Display *display, int *major, int *minor)
 #ifdef XRANDR_DISABLED_BY_DEFAULT
     if (!SDL_GetHintBoolean(SDL_HINT_VIDEO_X11_XRANDR, false)) {
 #ifdef X11MODES_DEBUG
-        printf("XRandR disabled by default due to window manager issues\n");
+        SDL_Log("XRandR disabled by default due to window manager issues\n");
 #endif
         return false;
     }
 #else
     if (!SDL_GetHintBoolean(SDL_HINT_VIDEO_X11_XRANDR, true)) {
 #ifdef X11MODES_DEBUG
-        printf("XRandR disabled due to hint\n");
+        SDL_Log("XRandR disabled due to hint\n");
 #endif
         return false;
     }
@@ -359,7 +360,7 @@ static bool CheckXRandR(Display *display, int *major, int *minor)
 
     if (!SDL_X11_HAVE_XRANDR) {
 #ifdef X11MODES_DEBUG
-        printf("XRandR support not available\n");
+        SDL_Log("XRandR support not available\n");
 #endif
         return false;
     }
@@ -369,13 +370,13 @@ static bool CheckXRandR(Display *display, int *major, int *minor)
     *minor = 3; // we want 1.3
     if (!X11_XRRQueryVersion(display, major, minor)) {
 #ifdef X11MODES_DEBUG
-        printf("XRandR not active on the display\n");
+        SDL_Log("XRandR not active on the display\n");
 #endif
         *major = *minor = 0;
         return false;
     }
 #ifdef X11MODES_DEBUG
-    printf("XRandR available at version %d.%d!\n", *major, *minor);
+    SDL_Log("XRandR available at version %d.%d!\n", *major, *minor);
 #endif
     return true;
 }
@@ -440,8 +441,8 @@ static bool SetXRandRModeInfo(Display *display, XRRScreenResources *res, RRCrtc
             CalculateXRandRRefreshRate(info, &mode->refresh_rate_numerator, &mode->refresh_rate_denominator);
             mode->internal->xrandr_mode = modeID;
 #ifdef X11MODES_DEBUG
-            printf("XRandR mode %d: %dx%d@%d/%dHz\n", (int)modeID,
-                   mode->screen_w, mode->screen_h, mode->refresh_rate_numerator, mode->refresh_rate_denominator);
+            SDL_Log("XRandR mode %d: %dx%d@%d/%dHz\n", (int)modeID,
+                    mode->screen_w, mode->screen_h, mode->refresh_rate_numerator, mode->refresh_rate_denominator);
 #endif
             return true;
         }
@@ -449,7 +450,90 @@ static bool SetXRandRModeInfo(Display *display, XRRScreenResources *res, RRCrtc
     return false;
 }
 
-static MonitorInfo *GetMonitorInfo(Display *dpy, int screen, RROutput output, const char *name)
+static bool GetRootWindowCardinalProperty(Display *dpy, int screen, const char *name, Uint32 *value)
+{
+    Atom atom = X11_XInternAtom(dpy, name, False);
+    if (atom == None) {
+        return false;
+    }
+
+    int real_format;
+    Atom real_type;
+    unsigned long items_read = 0, items_left = 0;
+    unsigned char *propdata = NULL;
+    int status;
+    bool result = false;
+
+    status = X11_XGetWindowProperty(dpy, RootWindow(dpy, screen),
+                                    atom, 0L, 1024L, False, AnyPropertyType,
+                                    &real_type, &real_format, &items_read, &items_left, &propdata);
+    if (status == Success && propdata) {
+        if (real_type == XA_CARDINAL && items_read > 0) {
+            switch (real_format) {
+            case 8:
+                *value = propdata[0];
+                break;
+            case 16:
+                *value = propdata[0] | (propdata[1] << 8);
+                break;
+            default:
+                *value = propdata[0] | (propdata[1] << 8) | (propdata[2] << 16) | (propdata[3] << 24);
+                break;
+            }
+            result = true;
+        }
+        X11_XFree(propdata);
+    }
+    return result;
+}
+
+static bool GetRootWindowBoolProperty(Display *dpy, int screen, const char *name, bool *value)
+{
+    Uint32 v;
+    if (GetRootWindowCardinalProperty(dpy, screen, name, &v)) {
+        *value = (v != 0);
+        return true;
+    }
+    return false;
+}
+
+static bool GetRootWindowFloatProperty(Display *dpy, int screen, const char *name, float *value)
+{
+    Uint32 v;
+    if (GetRootWindowCardinalProperty(dpy, screen, name, &v)) {
+        SDL_memcpy(value, &v, sizeof(v));
+        return true;
+    }
+    return false;
+}
+
+static float GetGamescopeSDRWhiteLevel(Display *dpy, int screen)
+{
+    // These properties are currently only available on X display :0
+    // If this changes, please remove the XCloseDisplay() call below.
+    dpy = X11_XOpenDisplay(":0");
+    screen = 0;
+    if (!dpy) {
+        return 0.0f;
+    }
+
+    float SDR_white_level = 0.0f;
+    bool external_display = false;
+    if (GetRootWindowBoolProperty(dpy, screen, "GAMESCOPE_DISPLAY_IS_EXTERNAL", &external_display) &&
+        external_display) {
+        GetRootWindowFloatProperty(dpy, screen, "GAMESCOPE_SDR_ON_HDR_CONTENT_BRIGHTNESS", &SDR_white_level);
+    } else {
+        // We're using the Steam Deck internal display, which has an SDR white level of 500 nits
+        SDR_white_level = 500.0f;
+    }
+
+    // Closing the display opened above
+    X11_XCloseDisplay(dpy);
+
+    return SDR_white_level;
+}
+
+static MonitorInfo *GetMonitorInfo(Display *dpy, int screen, RROutput output, bool *gamescope)
 {
     MonitorInfo *info = NULL;
     int real_format;
@@ -458,6 +542,7 @@ static MonitorInfo *GetMonitorInfo(Display *dpy, int screen, RROutput output, co
     unsigned char *propdata = NULL;
     int status;
 
+    *gamescope = false;
 
     if (!info) {
         Atom GAMESCOPE_DISPLAY_EDID_PATH = X11_XInternAtom(dpy, "GAMESCOPE_DISPLAY_EDID_PATH", False);
@@ -470,6 +555,9 @@ static MonitorInfo *GetMonitorInfo(Display *dpy, int screen, RROutput output, co
                 void *data = SDL_LoadFile((char *)propdata, &size);
                 if (data) {
                     info = decode_edid((uchar *)data, size);
+                    if (info) {
+                        *gamescope = true;
+                    }
                     SDL_free(data);
                 }
             }
@@ -503,7 +591,6 @@ static MonitorInfo *GetMonitorInfo(Display *dpy, int screen, RROutput output, co
 
 #ifdef X11MODES_DEBUG
     if (info) {
-        SDL_Log("Found EDID data for %s", name);
         dump_monitor_info(info);
     }
 #endif
@@ -523,7 +610,7 @@ static void SetXRandRDisplayName(MonitorInfo *info, char *name, size_t namelen,
     }
 
 #ifdef X11MODES_DEBUG
-    printf("Display name: %s\n", name);
+    SDL_Log("Display name: %s\n", name);
 #endif
 }
 
@@ -621,7 +708,8 @@ static bool X11_FillXRandRDisplayInfo(SDL_VideoDevice *_this, Display *dpy, int
     displaydata->xrandr_output = outputid;
     SDL_strlcpy(displaydata->connector_name, display_name, sizeof(displaydata->connector_name));
 
-    MonitorInfo *info = GetMonitorInfo(dpy, screen, outputid, display_name);
+    bool gamescope = false;
+    MonitorInfo *info = GetMonitorInfo(dpy, screen, outputid, &gamescope);
 
     SetXRandRModeInfo(dpy, res, output_crtc, modeID, &mode);
     SetXRandRDisplayName(info, display_name, display_name_size, display_mm_width, display_mm_height);
@@ -635,32 +723,20 @@ static bool X11_FillXRandRDisplayInfo(SDL_VideoDevice *_this, Display *dpy, int
     display->internal = displaydata;
 
     if (info) {
-        /* ITU-R BT.2408-7 (Sept 2023) has the reference PQ white level at 203 nits,
-         * while older Dolby documentation claims a reference level of 100 nits.
-         *
-         * Use 203 nits for now.
-         */
-        float SDR_white_level = 203.0f;
-
-        bool steam_deck = SDL_GetHintBoolean("SteamDeck", false);
-        if (steam_deck) {
-            // The SDR white level is dynamic, but typically is 400 nits
-            // We'll need some other way to get this for external displays
-            SDR_white_level = 400.0f;
+        float SDR_white_level;
+        if (gamescope) {
+            SDR_white_level = GetGamescopeSDRWhiteLevel(dpy, screen);
         } else {
             // Support for HDR on X11 seems spotty, let's disable this for now
             SDR_white_level = 0.0f;
         }
 
-        // Allow a hint override for the SDR white level
-        const char *hint = SDL_GetHint("SDL_X11_SDR_WHITE_LEVEL");
-        if (hint && *hint) {
-            SDR_white_level = (float)SDL_atof(hint);
-        }
-
-        if (info->max_luminance > 0.0 && SDR_white_level > 0.0f) {
+#ifdef X11MODES_DEBUG
+        SDL_Log("HDR values: %f / %f", SDR_white_level, info->max_luminance);
+#endif
+        if (SDR_white_level > 0.0f && info->max_luminance > SDR_white_level) {
             display->HDR.HDR_headroom = (float)info->max_luminance / SDR_white_level;
-            display->HDR.SDR_white_level = SDR_white_level / 80.0f;
+            display->HDR.SDR_white_level = SDR_white_level / SCRGB_NITS;
         }
 
         SDL_free(info);
@@ -716,8 +792,11 @@ static bool X11_UpdateXRandRDisplay(SDL_VideoDevice *_this, Display *dpy, int sc
     // update scale
     SDL_SetDisplayContentScale(existing_display, display.content_scale);
 
+    // update HDR properties
+    SDL_SetDisplayHDRProperties(existing_display, &display.HDR);
+
     // SDL_DisplayData is updated piece-meal above, free our local copy of this data
-    SDL_free( display.internal );
+    SDL_free(display.internal);
 
     return true;
 }
@@ -734,7 +813,7 @@ static XRRScreenResources *X11_GetScreenResources(Display *dpy, int screen)
     return res;
 }
 
-static void X11_CheckDisplaysMoved(SDL_VideoDevice *_this, Display *dpy)
+void X11_CheckDisplaysMoved(SDL_VideoDevice *_this, Display *dpy)
 {
     const int screencount = ScreenCount(dpy);
 
@@ -816,7 +895,7 @@ static void X11_HandleXRandROutputChange(SDL_VideoDevice *_this, const XRROutput
     int i;
 
 #if 0
-    printf("XRROutputChangeNotifyEvent! [output=%u, crtc=%u, mode=%u, rotation=%u, connection=%u]\n", (unsigned int) ev->output, (unsigned int) ev->crtc, (unsigned int) ev->mode, (unsigned int) ev->rotation, (unsigned int) ev->connection);
+    SDL_Log("XRROutputChangeNotifyEvent! [output=%u, crtc=%u, mode=%u, rotation=%u, connection=%u]\n", (unsigned int) ev->output, (unsigned int) ev->crtc, (unsigned int) ev->mode, (unsigned int) ev->rotation, (unsigned int) ev->connection);
 #endif
 
     // XWayland doesn't always send output disconnected events
@@ -1125,8 +1204,8 @@ bool X11_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *sdl_display, S
 
         if (crtc->mode == modedata->xrandr_mode) {
 #ifdef X11MODES_DEBUG
-            printf("already in desired mode 0x%lx (%ux%u), nothing to do\n",
-                   crtc->mode, crtc->width, crtc->height);
+            SDL_Log("already in desired mode 0x%lx (%ux%u), nothing to do\n",
+                    crtc->mode, crtc->width, crtc->height);
 #endif
             status = Success;
             goto freeInfo;
diff --git a/src/video/x11/SDL_x11modes.h b/src/video/x11/SDL_x11modes.h
index b11b2d18571b3..82e67676afcb7 100644
--- a/src/video/x11/SDL_x11modes.h
+++ b/src/video/x11/SDL_x11modes.h
@@ -68,6 +68,7 @@ extern float X11_GetGlobalContentScale(Display *display, XSettingsClient *client
 extern float X11_GetGlobalContentScaleForDevice(SDL_VideoDevice *_this);
 
 #ifdef SDL_VIDEO_DRIVER_X11_XRANDR
+extern void X11_CheckDisplaysMoved(SDL_VideoDevice *_this, Display *dpy);
 extern void X11_HandleXRandREvent(SDL_VideoDevice *_this, const XEvent *xevent);
 #endif