SDL: Decode the monitor EDID to get HDR metadata on X11

From d98661ff7f018ae3e1c6f60c92df09b3f7f785c9 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 20 Jul 2026 12:03:33 -0700
Subject: [PATCH] Decode the monitor EDID to get HDR metadata on X11

---
 src/video/x11/SDL_x11modes.c | 108 ++++++++++++++++++++++++-----------
 src/video/x11/edid-parse.c   |  63 ++++++++++++++++++--
 src/video/x11/edid.h         |   6 +-
 3 files changed, 140 insertions(+), 37 deletions(-)

diff --git a/src/video/x11/SDL_x11modes.c b/src/video/x11/SDL_x11modes.c
index 1fbbc868766d1..23dcaf90ff653 100644
--- a/src/video/x11/SDL_x11modes.c
+++ b/src/video/x11/SDL_x11modes.c
@@ -449,45 +449,74 @@ static bool SetXRandRModeInfo(Display *display, XRRScreenResources *res, RRCrtc
     return false;
 }
 
-static void SetXRandRDisplayName(Display *dpy, Atom EDID, char *name, const size_t namelen, RROutput output, const unsigned long widthmm, const unsigned long heightmm)
+static MonitorInfo *GetMonitorInfo(Display *dpy, int screen, RROutput output, const char *name)
 {
-    // See if we can get the EDID data for the real monitor name
-    int inches;
-    int nprop;
-    Atom *props = X11_XRRListOutputProperties(dpy, output, &nprop);
-    int i;
+    MonitorInfo *info = NULL;
+    int real_format;
+    Atom real_type;
+    unsigned long items_read = 0, items_left = 0;
+    unsigned char *propdata = NULL;
+    int status;
+
+
+    if (!info) {
+        Atom GAMESCOPE_DISPLAY_EDID_PATH = X11_XInternAtom(dpy, "GAMESCOPE_DISPLAY_EDID_PATH", False);
+        if (GAMESCOPE_DISPLAY_EDID_PATH != None) {
+            status = X11_XGetWindowProperty(dpy, RootWindow(dpy, screen),
+                                            GAMESCOPE_DISPLAY_EDID_PATH, 0L, 1024L, False, AnyPropertyType,
+                                            &real_type, &real_format, &items_read, &items_left, &propdata);
+            if (status == Success && propdata) {
+                size_t size = 0;
+                void *data = SDL_LoadFile((char *)propdata, &size);
+                if (data) {
+                    info = decode_edid((uchar *)data, size);
+                    SDL_free(data);
+                }
+            }
+            if (propdata) {
+                X11_XFree(propdata);
+            }
+        }
+    }
 
-    for (i = 0; i < nprop; ++i) {
-        unsigned char *prop;
-        int actual_format;
-        unsigned long nitems, bytes_after;
-        Atom actual_type;
-
-        if (props[i] == EDID) {
-            if (X11_XRRGetOutputProperty(dpy, output, props[i], 0, 100, False,
-                                         False, AnyPropertyType, &actual_type,
-                                         &actual_format, &nitems, &bytes_after,
-                                         &prop) == Success) {
-                MonitorInfo *info = decode_edid(prop);
-                if (info) {
-#ifdef X11MODES_DEBUG
-                    printf("Found EDID data for %s\n", name);
-                    dump_monitor_info(info);
-#endif
-                    SDL_strlcpy(name, info->dsc_product_name, namelen);
-                    SDL_free(info);
+    if (!info) {
+        Atom EDID = X11_XInternAtom(dpy, "EDID", False);
+        if (EDID != None) {
+            int nprop = 0;
+            Atom *props = X11_XRRListOutputProperties(dpy, output, &nprop);
+            for (int i = 0; i < nprop; ++i) {
+                if (props[i] == EDID) {
+                    status = X11_XRRGetOutputProperty(dpy, output, props[i], 0L, 128L, False,
+                                                       False, AnyPropertyType, &real_type,
+                                                       &real_format, &items_read, &items_left, &propdata);
+                    if (status == Success && propdata) {
+                        info = decode_edid(propdata, items_read);
+                    }
+                    if (propdata) {
+                        X11_XFree(propdata);
+                    }
+                    break;
                 }
-                X11_XFree(prop);
             }
-            break;
         }
     }
 
-    if (props) {
-        X11_XFree(props);
+#ifdef X11MODES_DEBUG
+    if (info) {
+        SDL_Log("Found EDID data for %s", name);
+        dump_monitor_info(info);
     }
+#endif
+    return info;
+}
 
-    inches = (int)((SDL_sqrtf(widthmm * widthmm + heightmm * heightmm) / 25.4f) + 0.5f);
+static void SetXRandRDisplayName(MonitorInfo *info, char *name, size_t namelen, unsigned long widthmm, unsigned long heightmm)
+{
+    if (info) {
+        SDL_strlcpy(name, info->dsc_product_name, namelen);
+    }
+
+    int inches = (int)((SDL_sqrtf(widthmm * widthmm + heightmm * heightmm) / 25.4f) + 0.5f);
     if (*name && inches) {
         const size_t len = SDL_strlen(name);
         (void)SDL_snprintf(&name[len], namelen - len, " %d\"", inches);
@@ -500,7 +529,6 @@ static void SetXRandRDisplayName(Display *dpy, Atom EDID, char *name, const size
 
 static bool X11_FillXRandRDisplayInfo(SDL_VideoDevice *_this, Display *dpy, int screen, RROutput outputid, XRRScreenResources *res, SDL_VideoDisplay *display, char *display_name, size_t display_name_size)
 {
-    Atom EDID = X11_XInternAtom(dpy, "EDID", False);
     XRROutputInfo *output_info;
     int display_x, display_y;
     unsigned long display_mm_width, display_mm_height;
@@ -593,8 +621,10 @@ 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);
+
     SetXRandRModeInfo(dpy, res, output_crtc, modeID, &mode);
-    SetXRandRDisplayName(dpy, EDID, display_name, display_name_size, outputid, display_mm_width, display_mm_height);
+    SetXRandRDisplayName(info, display_name, display_name_size, display_mm_width, display_mm_height);
 
     SDL_zero(*display);
     if (*display_name) {
@@ -604,6 +634,20 @@ static bool X11_FillXRandRDisplayInfo(SDL_VideoDevice *_this, Display *dpy, int
     display->content_scale = X11_GetGlobalContentScaleForDevice(_this);
     display->internal = displaydata;
 
+    if (info) {
+        if (info->max_luminance > 0.0) {
+            /* 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.
+             */
+            display->HDR.HDR_headroom = (float)info->max_luminance / 203.0f;
+            display->HDR.SDR_white_level = 1.0f;
+        }
+
+        SDL_free(info);
+    }
+
     return true;
 }
 
diff --git a/src/video/x11/edid-parse.c b/src/video/x11/edid-parse.c
index eca187b60049e..6760c83404900 100644
--- a/src/video/x11/edid-parse.c
+++ b/src/video/x11/edid-parse.c
@@ -518,10 +518,54 @@ decode_check_sum (const uchar *edid,
     info->checksum = check;
 }
 
+static void
+decode_HDR_metadata_block (const uchar *data, int length, MonitorInfo *info)
+{
+    if (length >= 3)
+	info->max_luminance = 50.0 * SDL_pow(2, data[2] / 32.0);
+
+    if (length >= 4)
+	info->max_frame_average_luminance = 50.0 * SDL_pow(2, data[3] / 32.0);
+
+    if (length >= 5)
+	info->min_luminance = 50.0 * SDL_pow(2, data[4] / 32.0);
+}
+
+
+static void
+decode_cta_block (const uchar *edid,
+		  MonitorInfo *info)
+{
+    int offset = 4;
+    while (offset < 128) {
+	int length = edid[offset] & 0x1f;
+	int type = edid[offset] >> 5;
+	if (length == 0) {
+	    break;
+	}
+	if (type == 7) {
+	    type <<= 8;
+	    type |= edid[offset + 1];
+	}
+	if (type == 0x706) {
+	    decode_HDR_metadata_block (&edid[offset + 2], length - 1, info);
+	}
+	offset += (1 + length);
+    }
+}
+
 MonitorInfo *
-decode_edid (const uchar *edid)
+decode_edid (const uchar *edid, size_t size)
 {
-    MonitorInfo *info = SDL_calloc (1, sizeof (MonitorInfo));
+    const int EDID_PAGE_SIZE = 128;
+    int num_blocks = (size / EDID_PAGE_SIZE);
+    MonitorInfo *info;
+
+    if ((size % EDID_PAGE_SIZE) != 0) {
+	return NULL;
+    }
+
+    info = SDL_calloc (1, sizeof (MonitorInfo));
 
     decode_check_sum (edid, info);
     
@@ -533,10 +577,21 @@ decode_edid (const uchar *edid)
         !decode_established_timings (edid, info) ||
         !decode_standard_timings (edid, info) ||
         !decode_descriptors (edid, info)) {
-        SDL_free(info);
-        return NULL;
+	SDL_free(info);
+	return NULL;
     }
     
+    if (num_blocks > 1) {
+	for (int i = 1; i < num_blocks; ++i) {
+	    int offset = i * EDID_PAGE_SIZE;
+	    uchar extension = edid[offset];
+	    if (extension == 0x02) {
+		// CTA-861 Extension Block
+		decode_cta_block (&edid[offset], info);
+	    }
+	}
+    }
+
     return info;
 }
 
diff --git a/src/video/x11/edid.h b/src/video/x11/edid.h
index 458129189623b..7cbdcdae12264 100644
--- a/src/video/x11/edid.h
+++ b/src/video/x11/edid.h
@@ -167,6 +167,10 @@ struct MonitorInfo
     double white_x;
     double white_y;
 
+    double min_luminance;
+    double max_luminance;
+    double max_frame_average_luminance;
+
     Timing established[24]; // Terminated by 0x0x0
     Timing standard[8];
 
@@ -185,7 +189,7 @@ struct MonitorInfo
     char dsc_string[14]; // Unspecified ASCII data
 };
 
-MonitorInfo *decode_edid(const uchar *data);
+MonitorInfo *decode_edid(const uchar *data, size_t size);
 void dump_monitor_info(MonitorInfo *info);
 char *make_display_name(const char *output_name,
                         const MonitorInfo *info);