SDL: Added HDR display properties and related event

From 30e176d6ba5f89cd76a533aa4f1666b232c1132c Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 6 Feb 2024 01:53:03 -0800
Subject: [PATCH] Added HDR display properties and related event

Also added an HDR calibration stage to testcolorspace
---
 include/SDL3/SDL_events.h            |   3 +-
 include/SDL3/SDL_render.h            |  16 +-
 include/SDL3/SDL_video.h             |   8 +
 src/events/SDL_events.c              |   1 +
 src/render/SDL_render.c              |  11 ++
 src/test/SDL_test_common.c           |   6 +
 src/video/SDL_sysvideo.h             |   8 +
 src/video/SDL_video.c                |  31 +++-
 src/video/windows/SDL_windowsmodes.c | 163 +++++++++++++++++
 test/testcolorspace.c                | 254 +++++++++++++++++++++++----
 test/testffmpeg.c                    |  52 +++++-
 11 files changed, 503 insertions(+), 50 deletions(-)

diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index 4cf67b5480f3..6ce79f56e7e6 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -97,8 +97,9 @@ typedef enum
     SDL_EVENT_DISPLAY_REMOVED,             /**< Display has been removed from the system */
     SDL_EVENT_DISPLAY_MOVED,               /**< Display has changed position */
     SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED, /**< Display has changed content scale */
+    SDL_EVENT_DISPLAY_HDR_STATE_CHANGED,   /**< Display HDR properties have changed */
     SDL_EVENT_DISPLAY_FIRST = SDL_EVENT_DISPLAY_ORIENTATION,
-    SDL_EVENT_DISPLAY_LAST = SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED,
+    SDL_EVENT_DISPLAY_LAST = SDL_EVENT_DISPLAY_HDR_STATE_CHANGED,
 
     /* Window events */
     /* 0x200 was SDL_WINDOWEVENT, reserve the number for sdl2-compat */
diff --git a/include/SDL3/SDL_render.h b/include/SDL3/SDL_render.h
index 7a0862dcb083..ca2b772395e4 100644
--- a/include/SDL3/SDL_render.h
+++ b/include/SDL3/SDL_render.h
@@ -234,12 +234,12 @@ extern DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window *window, co
  *
  * These are the supported properties:
  *
+ * - `SDL_PROP_RENDERER_CREATE_NAME_STRING`: the name of the rendering driver
+ *   to use, if a specific one is desired
  * - `SDL_PROP_RENDERER_CREATE_WINDOW_POINTER`: the window where rendering is
- *   displayed
+ *   displayed, required if this isn't a software renderer using a surface
  * - `SDL_PROP_RENDERER_CREATE_SURFACE_POINTER`: the surface where rendering
  *   is displayed, if you want a software renderer without a window
- * - `SDL_PROP_RENDERER_CREATE_NAME_STRING`: the name of the rendering driver
- *   to use, if a specific one is desired
  * - `SDL_PROP_RENDERER_CREATE_OUTPUT_COLORSPACE_NUMBER`: an SDL_ColorSpace
  *   value describing the colorspace for output to the display, defaults to
  *   SDL_COLORSPACE_SRGB. The direct3d11 and direct3d12 renderers support
@@ -263,9 +263,9 @@ extern DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window *window, co
  */
 extern DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRendererWithProperties(SDL_PropertiesID props);
 
+#define SDL_PROP_RENDERER_CREATE_NAME_STRING                    "name"
 #define SDL_PROP_RENDERER_CREATE_WINDOW_POINTER                 "window"
 #define SDL_PROP_RENDERER_CREATE_SURFACE_POINTER                "surface"
-#define SDL_PROP_RENDERER_CREATE_NAME_STRING                    "name"
 #define SDL_PROP_RENDERER_CREATE_OUTPUT_COLORSPACE_NUMBER       "output_colorspace"
 #define SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_BOOLEAN          "present_vsync"
 
@@ -334,6 +334,10 @@ extern DECLSPEC int SDLCALL SDL_GetRendererInfo(SDL_Renderer *renderer, SDL_Rend
  *
  * The following read-only properties are provided by SDL:
  *
+ * - `SDL_PROP_RENDERER_NAME_STRING`: the name of the rendering driver
+ * - `SDL_PROP_RENDERER_WINDOW_POINTER`: the window where rendering is displayed, if any
+ * - `SDL_PROP_RENDERER_SURFACE_POINTER`: the surface where rendering is displayed, if this is a software renderer without a window
+ * - `SDL_PROP_RENDERER_OUTPUT_COLORSPACE_NUMBER`: an SDL_ColorSpace value describing the colorspace for output to the display, defaults to SDL_COLORSPACE_SRGB.
  * - `SDL_PROP_RENDERER_D3D9_DEVICE_POINTER`: the IDirect3DDevice9 associated
  *   with the renderer
  * - `SDL_PROP_RENDERER_D3D11_DEVICE_POINTER`: the ID3D11Device associated
@@ -354,6 +358,10 @@ extern DECLSPEC int SDLCALL SDL_GetRendererInfo(SDL_Renderer *renderer, SDL_Rend
  */
 extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetRendererProperties(SDL_Renderer *renderer);
 
+#define SDL_PROP_RENDERER_NAME_STRING                   "SDL.renderer.name"
+#define SDL_PROP_RENDERER_WINDOW_POINTER                "SDL.renderer.window"
+#define SDL_PROP_RENDERER_SURFACE_POINTER               "SDL.renderer.surface"
+#define SDL_PROP_RENDERER_OUTPUT_COLORSPACE_NUMBER      "SDL.renderer.output_colorspace"
 #define SDL_PROP_RENDERER_D3D9_DEVICE_POINTER           "SDL.renderer.d3d9.device"
 #define SDL_PROP_RENDERER_D3D11_DEVICE_POINTER          "SDL.renderer.d3d11.device"
 #define SDL_PROP_RENDERER_D3D12_DEVICE_POINTER          "SDL.renderer.d3d12.device"
diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index 8b8d280d82b7..8b935cf2c839 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -353,6 +353,11 @@ extern DECLSPEC SDL_DisplayID SDLCALL SDL_GetPrimaryDisplay(void);
 /**
  * Get the properties associated with a display.
  *
+ * The following read-only properties are provided by SDL:
+ *
+ * - `SDL_PROP_DISPLAY_HDR_ENABLED_BOOLEAN`: true if the display has High Dynamic Range enabled
+ * - `SDL_PROP_DISPLAY_SDR_WHITE_LEVEL_FLOAT`: the luminance, in nits, that SDR white is rendered on this display. If this value is not set or is zero, the value 200 is a reasonable default when HDR is enabled.
+ *
  * \param displayID the instance ID of the display to query
  * \returns a valid property ID on success or 0 on failure; call
  *          SDL_GetError() for more information.
@@ -364,6 +369,9 @@ extern DECLSPEC SDL_DisplayID SDLCALL SDL_GetPrimaryDisplay(void);
  */
 extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetDisplayProperties(SDL_DisplayID displayID);
 
+#define SDL_PROP_DISPLAY_HDR_ENABLED_BOOLEAN            "SDL.display.HDR_enabled"
+#define SDL_PROP_DISPLAY_SDR_WHITE_LEVEL_FLOAT          "SDL.display.SDR_white_level"
+
 /**
  * Get the name of a display in UTF-8 encoding.
  *
diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c
index 71ce501330f3..0196aa1bdd68 100644
--- a/src/events/SDL_events.c
+++ b/src/events/SDL_events.c
@@ -282,6 +282,7 @@ static void SDL_LogEvent(const SDL_Event *event)
         SDL_DISPLAYEVENT_CASE(SDL_EVENT_DISPLAY_REMOVED);
         SDL_DISPLAYEVENT_CASE(SDL_EVENT_DISPLAY_MOVED);
         SDL_DISPLAYEVENT_CASE(SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED);
+        SDL_DISPLAYEVENT_CASE(SDL_EVENT_DISPLAY_HDR_STATE_CHANGED);
 #undef SDL_DISPLAYEVENT_CASE
 
 #define SDL_WINDOWEVENT_CASE(x)                \
diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c
index 2623fe5ef905..72599dfc9c62 100644
--- a/src/render/SDL_render.c
+++ b/src/render/SDL_render.c
@@ -854,6 +854,7 @@ SDL_Renderer *SDL_CreateRendererWithProperties(SDL_PropertiesID props)
     const int n = SDL_GetNumRenderDrivers();
     const char *hint;
     int i, attempted = 0;
+    SDL_PropertiesID new_props;
 
     if (!window && surface) {
         return SDL_CreateSoftwareRenderer(surface);
@@ -964,6 +965,16 @@ SDL_Renderer *SDL_CreateRendererWithProperties(SDL_PropertiesID props)
         renderer->hidden = SDL_FALSE;
     }
 
+    new_props = SDL_GetRendererProperties(renderer);
+    SDL_SetStringProperty(new_props, SDL_PROP_RENDERER_NAME_STRING, renderer->info.name);
+    if (window) {
+        SDL_SetProperty(new_props, SDL_PROP_RENDERER_WINDOW_POINTER, window);
+    }
+    if (surface) {
+        SDL_SetProperty(new_props, SDL_PROP_RENDERER_SURFACE_POINTER, surface);
+    }
+    SDL_SetNumberProperty(new_props, SDL_PROP_RENDERER_OUTPUT_COLORSPACE_NUMBER, renderer->output_colorspace);
+
     SDL_SetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_RENDERER, renderer);
 
     SDL_SetRenderViewport(renderer, NULL);
diff --git a/src/test/SDL_test_common.c b/src/test/SDL_test_common.c
index 48bab3bfe402..bf27b3546eca 100644
--- a/src/test/SDL_test_common.c
+++ b/src/test/SDL_test_common.c
@@ -1666,6 +1666,12 @@ static void SDLTest_PrintEvent(const SDL_Event *event)
                     event->display.displayID, (int)(scale * 100.0f));
         }
         break;
+    case SDL_EVENT_DISPLAY_HDR_STATE_CHANGED:
+        {
+            SDL_Log("SDL EVENT: Display %" SDL_PRIu32 " HDR %s",
+                    event->display.displayID, event->display.data1 ? "enabled" : "disabled");
+        }
+        break;
     case SDL_EVENT_DISPLAY_MOVED:
         SDL_Log("SDL EVENT: Display %" SDL_PRIu32 " changed position",
                 event->display.displayID);
diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index 5eb95c3cfaf3..9bf5f3e6b9dd 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -117,6 +117,12 @@ struct SDL_Window
 #define SDL_WINDOW_IS_POPUP(W) \
     (((W)->flags & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_POPUP_MENU)) != 0)
 
+typedef struct
+{
+    SDL_bool enabled;
+    float SDR_whitelevel;
+} SDL_HDRDisplayProperties;
+
 /*
  * Define the SDL display structure.
  * This corresponds to physical monitors attached to the system.
@@ -133,6 +139,7 @@ struct SDL_VideoDisplay
     SDL_DisplayOrientation natural_orientation;
     SDL_DisplayOrientation current_orientation;
     float content_scale;
+    SDL_HDRDisplayProperties HDR;
 
     SDL_Window *fullscreen_window;
 
@@ -489,6 +496,7 @@ extern void SDL_ResetFullscreenDisplayModes(SDL_VideoDisplay *display);
 extern void SDL_SetDesktopDisplayMode(SDL_VideoDisplay *display, const SDL_DisplayMode *mode);
 extern void SDL_SetCurrentDisplayMode(SDL_VideoDisplay *display, const SDL_DisplayMode *mode);
 extern void SDL_SetDisplayContentScale(SDL_VideoDisplay *display, float scale);
+extern void SDL_SetDisplayHDRProperties(SDL_VideoDisplay *display, const SDL_HDRDisplayProperties *HDR);
 extern int SDL_SetDisplayModeForDisplay(SDL_VideoDisplay *display, SDL_DisplayMode *mode);
 extern SDL_VideoDisplay *SDL_GetVideoDisplay(SDL_DisplayID display);
 extern SDL_VideoDisplay *SDL_GetVideoDisplayForWindow(SDL_Window *window);
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 48e87fb44f59..5ad2126acd62 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -671,6 +671,7 @@ SDL_DisplayID SDL_AddVideoDisplay(const SDL_VideoDisplay *display, SDL_bool send
 {
     SDL_VideoDisplay **displays, *new_display;
     SDL_DisplayID id;
+    SDL_PropertiesID props;
     int i;
 
     new_display = (SDL_VideoDisplay *)SDL_malloc(sizeof(*new_display));
@@ -710,9 +711,15 @@ SDL_DisplayID SDL_AddVideoDisplay(const SDL_VideoDisplay *display, SDL_bool send
         new_display->fullscreen_modes[i].displayID = id;
     }
 
-    if (send_event) {
-        SDL_SendDisplayEvent(new_display, SDL_EVENT_DISPLAY_ADDED, 0);
+    props = SDL_GetDisplayProperties(id);
+
+    if (display->HDR.enabled) {
+        SDL_SetBooleanProperty(props, SDL_PROP_DISPLAY_HDR_ENABLED_BOOLEAN, SDL_TRUE);
+    }
+    if (display->HDR.SDR_whitelevel != 0.0f) {
+        SDL_SetFloatProperty(props, SDL_PROP_DISPLAY_SDR_WHITE_LEVEL_FLOAT, display->HDR.SDR_whitelevel);
     }
+
     return id;
 }
 
@@ -976,6 +983,26 @@ float SDL_GetDisplayContentScale(SDL_DisplayID displayID)
     return display->content_scale;
 }
 
+void SDL_SetDisplayHDRProperties(SDL_VideoDisplay *display, const SDL_HDRDisplayProperties *HDR)
+{
+    SDL_PropertiesID props = SDL_GetDisplayProperties(display->id);
+    SDL_bool changed = SDL_FALSE;
+
+    if (HDR->enabled != display->HDR.enabled) {
+        SDL_SetBooleanProperty(props, SDL_PROP_DISPLAY_HDR_ENABLED_BOOLEAN, HDR->enabled);
+        changed = SDL_TRUE;
+    }
+    if (HDR->SDR_whitelevel != display->HDR.SDR_whitelevel) {
+        SDL_SetFloatProperty(props, SDL_PROP_DISPLAY_SDR_WHITE_LEVEL_FLOAT, HDR->SDR_whitelevel);
+        changed = SDL_TRUE;
+    }
+    SDL_copyp(&display->HDR, HDR);
+
+    if (changed) {
+        SDL_SendDisplayEvent(display, SDL_EVENT_DISPLAY_HDR_STATE_CHANGED, HDR->enabled);
+    }
+}
+
 static const SDL_DisplayMode *SDL_GetFullscreenModeMatch(const SDL_DisplayMode *mode)
 {
     const SDL_DisplayMode **modes;
diff --git a/src/video/windows/SDL_windowsmodes.c b/src/video/windows/SDL_windowsmodes.c
index 6621d598e36f..2ae5677664c6 100644
--- a/src/video/windows/SDL_windowsmodes.c
+++ b/src/video/windows/SDL_windowsmodes.c
@@ -25,6 +25,9 @@
 #include "SDL_windowsvideo.h"
 #include "../../events/SDL_displayevents_c.h"
 
+#define COBJMACROS
+#include <dxgi1_6.h>
+
 /* Windows CE compatibility */
 #ifndef CDS_FULLSCREEN
 #define CDS_FULLSCREEN 0
@@ -334,6 +337,162 @@ static char *WIN_GetDisplayNameVista(const WCHAR *deviceName)
     return NULL;
 }
 
+static SDL_bool WIN_GetMonitorDESC1(HMONITOR hMonitor, DXGI_OUTPUT_DESC1 *desc)
+{
+    typedef HRESULT(WINAPI * PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory);
+    PFN_CREATE_DXGI_FACTORY CreateDXGIFactoryFunc = NULL;
+    void *hDXGIMod = NULL;
+    SDL_bool found = SDL_FALSE;
+
+#ifdef SDL_PLATFORM_WINRT
+    CreateDXGIFactoryFunc = CreateDXGIFactory1;
+#else
+    hDXGIMod = SDL_LoadObject("dxgi.dll");
+    if (hDXGIMod) {
+        CreateDXGIFactoryFunc = (PFN_CREATE_DXGI_FACTORY)SDL_LoadFunction(hDXGIMod, "CreateDXGIFactory");
+    }
+#endif
+    if (CreateDXGIFactoryFunc) {
+        static const GUID SDL_IID_IDXGIFactory2 = { 0x50c83a1c, 0xe072, 0x4c48, { 0x87, 0xb0, 0x36, 0x30, 0xfa, 0x36, 0xa6, 0xd0 } };
+        static const GUID SDL_IID_IDXGIOutput6 = { 0x068346e8, 0xaaec, 0x4b84, { 0xad, 0xd7, 0x13, 0x7f, 0x51, 0x3f, 0x77, 0xa1 } };
+        IDXGIFactory2 *dxgiFactory;
+
+        if (SUCCEEDED(CreateDXGIFactoryFunc(&SDL_IID_IDXGIFactory2, (void **)&dxgiFactory))) {
+            IDXGIAdapter1 *dxgiAdapter;
+            UINT adapter = 0;
+            while (!found && SUCCEEDED(IDXGIFactory2_EnumAdapters1(dxgiFactory, adapter, &dxgiAdapter))) {
+                IDXGIOutput *dxgiOutput;
+                UINT output = 0;
+                while (!found && SUCCEEDED(IDXGIAdapter1_EnumOutputs(dxgiAdapter, output, &dxgiOutput))) {
+                    IDXGIOutput6 *dxgiOutput6;
+                    if (SUCCEEDED(IDXGIOutput_QueryInterface(dxgiOutput, &SDL_IID_IDXGIOutput6, (void **)&dxgiOutput6))) {
+                        if (SUCCEEDED(IDXGIOutput6_GetDesc1(dxgiOutput6, desc))) {
+                            if (desc->Monitor == hMonitor) {
+                                found = SDL_TRUE;
+                            }
+                        }
+                        IDXGIOutput6_Release(dxgiOutput6);
+                    }
+                    IDXGIOutput_Release(dxgiOutput);
+                    ++output;
+                }
+                IDXGIAdapter1_Release(dxgiAdapter);
+                ++adapter;
+            }
+            IDXGIFactory2_Release(dxgiFactory);
+        }
+    }
+    if (hDXGIMod) {
+        SDL_UnloadObject(hDXGIMod);
+    }
+    return found;
+}
+
+static SDL_bool WIN_GetMonitorPathInfo(HMONITOR hMonitor, DISPLAYCONFIG_PATH_INFO *path_info)
+{
+    LONG result;
+    MONITORINFOEXW view_info;
+    UINT32 i;
+    UINT32 num_path_array_elements = 0;
+    UINT32 num_mode_info_array_elements = 0;
+    DISPLAYCONFIG_PATH_INFO *path_infos = NULL, *new_path_infos;
+    DISPLAYCONFIG_MODE_INFO *mode_infos = NULL, *new_mode_infos;
+    SDL_bool found = SDL_FALSE;
+
+    SDL_zero(view_info);
+    view_info.cbSize = sizeof(view_info);
+    if (!GetMonitorInfoW(hMonitor, (MONITORINFO *)&view_info)) {
+        goto done;
+    }
+
+    do {
+        if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &num_path_array_elements, &num_mode_info_array_elements) != ERROR_SUCCESS) {
+            return -1;
+        }
+
+        new_path_infos = (DISPLAYCONFIG_PATH_INFO *)SDL_realloc(path_infos, num_path_array_elements * sizeof(*path_infos));
+        if (!new_path_infos) {
+            goto done;
+        }
+        path_infos = new_path_infos;
+
+        new_mode_infos = (DISPLAYCONFIG_MODE_INFO *)SDL_realloc(mode_infos, num_mode_info_array_elements * sizeof(*mode_infos));
+        if (!new_mode_infos) {
+            goto done;
+        }
+        mode_infos = new_mode_infos;
+
+        result = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &num_path_array_elements, path_infos, &num_mode_info_array_elements, mode_infos, NULL);
+
+    } while (result == ERROR_INSUFFICIENT_BUFFER);
+
+    if (result == ERROR_SUCCESS) {
+        for (i = 0; i < num_path_array_elements; ++i) {
+            DISPLAYCONFIG_SOURCE_DEVICE_NAME device_name;
+
+            SDL_zero(device_name);
+            device_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
+            device_name.header.size = sizeof(device_name);
+            device_name.header.adapterId = path_infos[i].sourceInfo.adapterId;
+            device_name.header.id = path_infos[i].sourceInfo.id;
+            if (DisplayConfigGetDeviceInfo(&device_name.header) == ERROR_SUCCESS) {
+                if (SDL_wcscmp(view_info.szDevice, device_name.viewGdiDeviceName) == 0) {
+                    SDL_copyp(path_info, &path_infos[i]);
+                    found = SDL_TRUE;
+                    break;
+                }
+            }
+        }
+    }
+
+done:
+    SDL_free(path_infos);
+    SDL_free(mode_infos);
+
+    return found;
+}
+
+static float WIN_GetSDRWhiteLevel(HMONITOR hMonitor)
+{
+    DISPLAYCONFIG_PATH_INFO path_info;
+    float SDR_whitelevel = 200.0f;
+
+    if (WIN_GetMonitorPathInfo(hMonitor, &path_info)) {
+        DISPLAYCONFIG_SDR_WHITE_LEVEL white_level;
+
+        SDL_zero(white_level);
+        white_level.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
+        white_level.header.size = sizeof(white_level);
+        white_level.header.adapterId = path_info.targetInfo.adapterId;
+        white_level.header.id = path_info.targetInfo.id;
+        if (DisplayConfigGetDeviceInfo(&white_level.header) == ERROR_SUCCESS) {
+            SDR_whitelevel = (white_level.SDRWhiteLevel / 1000.0f) * 80.0f;
+        }
+    }
+    return SDR_whitelevel;
+}
+
+static void WIN_GetHDRProperties(SDL_VideoDevice *_this, HMONITOR hMonitor, SDL_HDRDisplayProperties *HDR)
+{
+    DXGI_OUTPUT_DESC1 desc;
+
+    SDL_zerop(HDR);
+
+    if (WIN_GetMonitorDESC1(hMonitor, &desc)) {
+        if (desc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020) {
+            HDR->enabled = SDL_TRUE;
+            HDR->SDR_whitelevel = WIN_GetSDRWhiteLevel(hMonitor);
+
+            /* In theory you can get the maximum luminence from desc.MaxLuminance, but this value is 80
+             * on my system regardless of whether HDR is enabled. Because the value isn't reliable games
+             * will typically have a calibration step where they show you a white image at high luminence
+             * and slowly lower the brightness until you can see it as distinct from the background and
+             * then use that as the calibrated maximum luminence. The value 400 is a reasonable default.
+             */
+        }
+    }
+}
+
 static void WIN_AddDisplay(SDL_VideoDevice *_this, HMONITOR hMonitor, const MONITORINFOEXW *info, int *display_index)
 {
     int i, index = *display_index;
@@ -386,6 +545,7 @@ static void WIN_AddDisplay(SDL_VideoDevice *_this, HMONITOR hMonitor, const MONI
             if (!_this->setting_display_mode) {
                 SDL_VideoDisplay *existing_display = _this->displays[i];
                 SDL_Rect bounds;
+                SDL_HDRDisplayProperties HDR;
 
                 SDL_ResetFullscreenDisplayModes(existing_display);
                 SDL_SetDesktopDisplayMode(existing_display, &mode);
@@ -399,6 +559,8 @@ static void WIN_AddDisplay(SDL_VideoDevice *_this, HMONITOR hMonitor, const MONI
                 }
                 SDL_SendDisplayEvent(existing_display, SDL_EVENT_DISPLAY_ORIENTATION, current_orientation);
                 SDL_SetDisplayContentScale(existing_display, content_scale);
+                WIN_GetHDRProperties(_this, hMonitor, &HDR);
+                SDL_SetDisplayHDRProperties(existing_display, &HDR);
             }
             goto done;
         }
@@ -430,6 +592,7 @@ static void WIN_AddDisplay(SDL_VideoDevice *_this, HMONITOR hMonitor, const MONI
     display.device = _this;
     display.driverdata = displaydata;
     WIN_GetDisplayBounds(_this, &display, &displaydata->bounds);
+    WIN_GetHDRProperties(_this, hMonitor, &display.HDR);
     SDL_AddVideoDisplay(&display, SDL_FALSE);
     SDL_free(display.name);
 
diff --git a/test/testcolorspace.c b/test/testcolorspace.c
index c5db0306125f..3e701a93438e 100644
--- a/test/testcolorspace.c
+++ b/test/testcolorspace.c
@@ -18,6 +18,18 @@
 #include <emscripten/emscripten.h>
 #endif
 
+/* The value for the SDR white level on an SDR display, scRGB 1.0 */
+#define SDR_DISPLAY_WHITE_LEVEL  80.0f
+
+/* The default value for the SDR white level on an HDR display */
+#define DEFAULT_SDR_WHITE_LEVEL 200.0f
+
+/* The default value for the HDR white level on an HDR display */
+#define DEFAULT_HDR_WHITE_LEVEL 400.0f
+
+/* The maximum value for the HDR white level on an HDR display */
+#define MAXIMUM_HDR_WHITE_LEVEL 1000.0f
+
 #define WINDOW_WIDTH  640
 #define WINDOW_HEIGHT 480
 
@@ -32,11 +44,27 @@ static SDL_Colorspace colorspace = SDL_COLORSPACE_SRGB;
 static const char *colorspace_name = "sRGB";
 static int renderer_count = 0;
 static int renderer_index = 0;
-static int stage_count = 6;
 static int stage_index = 0;
 static int done;
+static SDL_bool HDR_enabled = SDL_FALSE;
+static float SDR_white_level = SDR_DISPLAY_WHITE_LEVEL;
 static float SDR_color_scale = 1.0f;
-static float HDR_color_scale = 1.0f;
+static SDL_FRect SDR_calibration_rect;
+static float HDR_white_level = DEFAULT_HDR_WHITE_LEVEL;
+static float HDR_color_scale = DEFAULT_HDR_WHITE_LEVEL / SDR_DISPLAY_WHITE_LEVEL;
+static SDL_FRect HDR_calibration_rect;
+
+enum
+{
+    StageClearBackground,
+    StageDrawBackground,
+    StageBlendDrawing,
+    StageBlendTexture,
+    StageHDRCalibration,
+    StageGradientDrawing,
+    StageGradientTexture,
+    StageCount
+};
 
 static void FreeRenderer(void)
 {
@@ -45,11 +73,61 @@ static void FreeRenderer(void)
     renderer = NULL;
 }
 
+static float GetDisplaySDRWhiteLevel(void)
+{
+    SDL_PropertiesID props;
+
+    HDR_enabled = SDL_FALSE;
+
+    props = SDL_GetRendererProperties(renderer);
+    if (SDL_GetNumberProperty(props, SDL_PROP_RENDERER_OUTPUT_COLORSPACE_NUMBER, SDL_COLORSPACE_SRGB) != SDL_COLORSPACE_SCRGB) {
+        /* We're not displaying in HDR, use the SDR white level */
+        return SDR_DISPLAY_WHITE_LEVEL;
+    }
+
+    props = SDL_GetDisplayProperties(SDL_GetDisplayForWindow(window));
+    if (!SDL_GetBooleanProperty(props, SDL_PROP_DISPLAY_HDR_ENABLED_BOOLEAN, SDL_FALSE)) {
+        /* HDR is not enabled on the display */
+        return SDR_DISPLAY_WHITE_LEVEL;
+    }
+
+    HDR_enabled = SDL_TRUE;
+
+    return SDL_GetFloatProperty(props, SDL_PROP_DISPLAY_SDR_WHITE_LEVEL_FLOAT, DEFAULT_SDR_WHITE_LEVEL);
+}
+
+static void SetSDRWhiteLevel(float value)
+{
+    if (value == SDR_white_level) {
+        return;
+    }
+
+    SDL_Log("SDR white level set to %g nits\n", value);
+    SDR_white_level = value;
+    SDR_color_scale = SDR_white_level / SDR_DISPLAY_WHITE_LEVEL;
+    SDL_SetRenderColorScale(renderer, SDR_color_scale);
+}
+
+static void UpdateSDRWhiteLevel(void)
+{
+    SetSDRWhiteLevel(GetDisplaySDRWhiteLevel());
+}
+
+static void SetHDRWhiteLevel(float value)
+{
+    if (value == HDR_white_level) {
+        return;
+    }
+
+    SDL_Log("HDR white level set to %g nits\n", value);
+    HDR_white_level = value;
+    HDR_color_scale = HDR_white_level / SDR_DISPLAY_WHITE_LEVEL;
+}
+
 static void CreateRenderer(void)
 {
     SDL_PropertiesID props;
     SDL_RendererInfo info;
-    float SDR_white_level;
 
     props = SDL_CreateProperties();
     SDL_SetProperty(props, SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, window);
@@ -66,15 +144,7 @@ static void CreateRenderer(void)
     SDL_Log("Created renderer %s\n", info.name);
     renderer_name = info.name;
 
-    /* If HDR is enabled... */
-    if (colorspace == SDL_COLORSPACE_SCRGB) {
-        SDR_white_level = 200.0f;
-    } else {
-        SDR_white_level = 80.0f;
-    }
-    SDR_color_scale = SDR_white_level / 80.0f;
-
-    SDL_SetRenderColorScale(renderer, SDR_color_scale);
+    UpdateSDRWhiteLevel();
 }
 
 static void NextRenderer( void )
@@ -107,12 +177,12 @@ static void PrevRenderer(void)
 
 static void NextStage(void)
 {
-    if (stage_count <= 0) {
+    if (StageCount <= 0) {
         return;
     }
 
     ++stage_index;
-    if (stage_index == stage_count) {
+    if (stage_index == StageCount) {
         stage_index = 0;
     }
 }
@@ -121,7 +191,7 @@ static void PrevStage(void)
 {
     --stage_index;
     if (stage_index == -1) {
-        stage_index += stage_count;
+        stage_index += StageCount;
     }
 }
 
@@ -388,6 +458,11 @@ static void DrawGradient(float x, float y, float width, float height, float star
     SDL_RenderGeometryRaw(renderer, NULL, xy, xy_stride, color, color_stride, NULL, 0, num_vertices, indices, num_indices, size_indices);
 }
 
+static float scRGBtoNits(float v)
+{
+    return v * 80.0f;
+}
+
 static float scRGBfromNits(float v)
 {
     return v / 80.0f;
@@ -413,6 +488,11 @@ static float sRGBFromLinear(float v)
     return v;
 }
 
+static float sRGBtoNits(float v)
+{
+    return scRGBtoNits(sRGBtoLinear(v));
+}
+
 static float sRGBfromNits(float v)
 {
     return sRGBFromLinear(scRGBfromNits(v));
@@ -432,28 +512,30 @@ static void RenderGradientDrawing(void)
 
     y += TEXT_LINE_ADVANCE;
 
-    DrawTextWhite(x, y, "SDR gradient (%d nits)", 80);
+    DrawTextWhite(x, y, "SDR gradient (%g nits)", SDR_white_level);
     y += TEXT_LINE_ADVANCE;
-    DrawGradient(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(80.0f));
+    SDL_SetRenderColorScale(renderer, 1.0f);
+    DrawGradient(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(SDR_white_level));
+    SDL_SetRenderColorScale(renderer, SDR_color_scale);
     y += 64.0f;
 
     y += TEXT_LINE_ADVANCE;
     y += TEXT_LINE_ADVANCE;
 
-    DrawTextWhite(x, y, "HDR gradient (%d nits)", 400);
+    DrawTextWhite(x, y, "HDR gradient (%g nits)", HDR_white_level);
     y += TEXT_LINE_ADVANCE;
-    SDL_SetRenderColorScale(renderer, HDR_color_scale);
-    DrawGradient(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(400.0f));
+    SDL_SetRenderColorScale(renderer, 1.0f);
+    DrawGradient(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(HDR_white_level));
     SDL_SetRenderColorScale(renderer, SDR_color_scale);
     y += 64.0f;
 
     y += TEXT_LINE_ADVANCE;
     y += TEXT_LINE_ADVANCE;
 
-    DrawTextWhite(x, y, "HDR gradient (%d nits)", 1000);
+    DrawTextWhite(x, y, "HDR gradient (%g nits)", MAXIMUM_HDR_WHITE_LEVEL);
     y += TEXT_LINE_ADVANCE;
-    SDL_SetRenderColorScale(renderer, HDR_color_scale);
-    DrawGradient(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(1000));
+    SDL_SetRenderColorScale(renderer, 1.0f);
+    DrawGradient(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(MAXIMUM_HDR_WHITE_LEVEL));
     SDL_SetRenderColorScale(renderer, SDR_color_scale);
     y += 64.0f;
 }
@@ -517,26 +599,117 @@ static void RenderGradientTexture(void)
 
     y += TEXT_LINE_ADVANCE;
 
-    DrawTextWhite(x, y, "SDR gradient (%d nits)", 80);
+    DrawTextWhite(x, y, "SDR gradient (%g nits)", SDR_white_level);
+    y += TEXT_LINE_ADVANCE;
+    SDL_SetRenderColorScale(renderer, 1.0f);
+    DrawGradientTexture(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(SDR_white_level));
+    SDL_SetRenderColorScale(renderer, SDR_color_scale);
+    y += 64.0f;
+
+    y += TEXT_LINE_ADVANCE;
+    y += TEXT_LINE_ADVANCE;
+
+    DrawTextWhite(x, y, "HDR gradient (%g nits)", HDR_white_level);
     y += TEXT_LINE_ADVANCE;
-    DrawGradientTexture(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(80));
+    SDL_SetRenderColorScale(renderer, 1.0f);
+    DrawGradientTexture(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(HDR_white_level));
+    SDL_SetRenderColorScale(renderer, SDR_color_scale);
     y += 64.0f;
 
     y += TEXT_LINE_ADVANCE;
     y += TEXT_LINE_ADVANCE;
 
-    DrawTextWhite(x, y, "HDR gradient (%d nits)", 400);
+    DrawTextWhite(x, y, "HDR gradient (%g nits)", MAXIMUM_HDR_WHITE_LEVEL);
     y += TEXT_LINE_ADVANCE;
-    DrawGradientTexture(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(400));
+    SDL_SetRenderColorScale(renderer, 1.0f);
+    DrawGradientTexture(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(MAXIMUM_HDR_WHITE_LEVEL));
+    SDL_SetRenderColorScale(renderer, SDR_color_scale);
     y += 64.0f;
+}
+
+static void RenderHDRCalibration(void)
+{
+    SDL_FRect rect;
 
+    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+    SDL_RenderClear(renderer);
+
+    float x = TEXT_START_X;
+    float y = TEXT_START_Y;
+    DrawTextWhite(x, y, "%s %s", renderer_name, colorspace_name);
     y += TEXT_LINE_ADVANCE;
+    DrawTextWhite(x, y, "Test: HDR calibration");
     y += TEXT_LINE_ADVANCE;
 
-    DrawTextWhite(x, y, "HDR gradient (%d nits)", 1000);
     y += TEXT_LINE_ADVANCE;
-    DrawGradientTexture(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(1000));
+
+    if (!HDR_enabled) {
+        DrawTextWhite(x, y, "HDR not enabled");
+        return;
+    }
+
+    DrawTextWhite(x, y, "Select HDR maximum brightness (%g nits)", HDR_white_level);
+    y += TEXT_LINE_ADVANCE;
+    DrawTextWhite(x, y, "The square in the middle should just barely be visible");
+    y += TEXT_LINE_ADVANCE;
+    HDR_calibration_rect.x = x;
+    HDR_calibration_rect.y = y;
+    HDR_calibration_rect.w = WINDOW_WIDTH - 2 * x;
+    HDR_calibration_rect.h = 64.0f;
+    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
+    SDL_SetRenderColorScale(renderer, MAXIMUM_HDR_WHITE_LEVEL / SDR_DISPLAY_WHITE_LEVEL);
+    SDL_RenderFillRect(renderer, &HDR_calibration_rect);
+    SDL_SetRenderColorScale(renderer, HDR_color_scale);
+    rect = HDR_calibration_rect;
+    rect.h -= 4.0f;
+    rect.w = 60.0f;
+    rect.x = (WINDOW_WIDTH - rect.w) / 2.0f;
+    rect.y += 2.0f;
+    SDL_RenderFillRect(renderer, &rect);
+    SDL_SetRenderColorScale(renderer, SDR_color_scale);
     y += 64.0f;
+
+    y += TEXT_LINE_ADVANCE;
+    y += TEXT_LINE_ADVANCE;
+
+    DrawTextWhite(x, y, "Select SDR maximum brightness (%g nits)", SDR_white_level);
+    y += TEXT_LINE_ADVANCE;
+    SDR_calibration_rect.x = x;
+    SDR_calibration_rect.y = y;
+    SDR_calibration_rect.w = WINDOW_WIDTH - 2 * x;
+    SDR_calibration_rect.h = 64.0f;
+    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
+    SDL_RenderFillRect(renderer, &SDR_calibration_rect);
+}
+
+static void OnHDRCalibrationMouseHeld(float x, float y)
+{
+    SDL_FPoint mouse = { x, y };
+
+    if (SDL_PointInRectFloat(&mouse, &HDR_calibration_rect)) {
+        float v = (x - HDR_calibration_rect.x) / HDR_calibration_rect.w;
+        v *= (sRGBfromNits(MAXIMUM_HDR_WHITE_LEVEL) - 1.0f);
+        v += 1.0f;
+        v = sRGBtoNits(v);
+        SetHDRWhiteLevel(v);
+        return;
+    }
+
+    if (SDL_PointInRectFloat(&mouse, &SDR_calibration_rect)) {
+        float v = (x - SDR_calibration_rect.x) / SDR_calibration_rect.w;
+        v *= (sRGBfromNits(MAXIMUM_HDR_WHITE_LEVEL) - 1.0f);
+        v += 1.0f;
+        v = sRGBtoNits(v);
+        SetSDRWhiteLevel(v);
+        return;
+    }
+}
+
+static void OnMouseHeld(float 

(Patch may be truncated, please check the link at the top of this post.)