From d98661ff7f018ae3e1c6f60c92df09b3f7f785c9 Mon Sep 17 00:00:00 2001 From: Sam Lantinga 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 | 106 +++++++++++++++++++++++++---------- src/video/x11/edid-parse.c | 65 +++++++++++++++++++-- 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 1fbbc86876..23dcaf90ff 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; - 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 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); } - X11_XFree(prop); } - break; + if (propdata) { + X11_XFree(propdata); + } } } - if (props) { - X11_XFree(props); + 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; + } + } + } } - inches = (int)((SDL_sqrtf(widthmm * widthmm + heightmm * heightmm) / 25.4f) + 0.5f); +#ifdef X11MODES_DEBUG + if (info) { + SDL_Log("Found EDID data for %s", name); + dump_monitor_info(info); + } +#endif + return info; +} + +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 eca187b600..6760c83404 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; } -MonitorInfo * -decode_edid (const uchar *edid) +static void +decode_HDR_metadata_block (const uchar *data, int length, MonitorInfo *info) { - MonitorInfo *info = SDL_calloc (1, sizeof (MonitorInfo)); + 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, size_t size) +{ + 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 4581291896..7cbdcdae12 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);