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);