Decode the monitor EDID to get HDR metadata on X11

This commit is contained in:
Sam Lantinga
2026-07-20 12:03:33 -07:00
parent f675aeddeb
commit d98661ff7f
3 changed files with 140 additions and 37 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

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