SDL: Added SDL_PROP_DISPLAY_HDR_WHITE_LEVEL_FLOAT

From cb3864949055aac24a464ebe561904328a088424 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 16 Feb 2024 17:36:11 -0800
Subject: [PATCH] Added SDL_PROP_DISPLAY_HDR_WHITE_LEVEL_FLOAT

---
 include/SDL3/SDL_video.h             |  2 +
 src/video/SDL_sysvideo.h             |  1 +
 src/video/SDL_video.c                |  7 ++++
 src/video/cocoa/SDL_cocoamodes.m     |  1 +
 src/video/uikit/SDL_uikitmodes.m     |  1 +
 src/video/windows/SDL_windowsmodes.c | 20 ++++-----
 test/testcolorspace.c                | 63 ++++++++++++++++++++--------
 7 files changed, 64 insertions(+), 31 deletions(-)

diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index e3b995583170..730f10fc4300 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -360,6 +360,7 @@ extern DECLSPEC SDL_DisplayID SDLCALL SDL_GetPrimaryDisplay(void);
  * - `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.
+ * - `SDL_PROP_DISPLAY_HDR_WHITE_LEVEL_FLOAT`: the maximum luminance, in nits, of HDR content on this display. If this value is not set or is zero, the value 400 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
@@ -374,6 +375,7 @@ extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetDisplayProperties(SDL_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"
+#define SDL_PROP_DISPLAY_HDR_WHITE_LEVEL_FLOAT          "SDL.display.HDR_white_level"
 
 /**
  * Get the name of a display in UTF-8 encoding.
diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index 4eb861c3f37f..ac112cbd31a9 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -122,6 +122,7 @@ typedef struct
 {
     SDL_bool enabled;
     float SDR_whitelevel;
+    float HDR_whitelevel;
 } SDL_HDRDisplayProperties;
 
 /*
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 8fe9c9a0bef5..3cfa955db8b1 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -715,6 +715,9 @@ SDL_DisplayID SDL_AddVideoDisplay(const SDL_VideoDisplay *display, SDL_bool send
     if (display->HDR.SDR_whitelevel != 0.0f) {
         SDL_SetFloatProperty(props, SDL_PROP_DISPLAY_SDR_WHITE_LEVEL_FLOAT, display->HDR.SDR_whitelevel);
     }
+    if (display->HDR.HDR_whitelevel != 0.0f) {
+        SDL_SetFloatProperty(props, SDL_PROP_DISPLAY_HDR_WHITE_LEVEL_FLOAT, display->HDR.HDR_whitelevel);
+    }
 
     return id;
 }
@@ -992,6 +995,10 @@ void SDL_SetDisplayHDRProperties(SDL_VideoDisplay *display, const SDL_HDRDisplay
         SDL_SetFloatProperty(props, SDL_PROP_DISPLAY_SDR_WHITE_LEVEL_FLOAT, HDR->SDR_whitelevel);
         changed = SDL_TRUE;
     }
+    if (HDR->HDR_whitelevel != display->HDR.HDR_whitelevel) {
+        SDL_SetFloatProperty(props, SDL_PROP_DISPLAY_HDR_WHITE_LEVEL_FLOAT, HDR->HDR_whitelevel);
+        changed = SDL_TRUE;
+    }
     SDL_copyp(&display->HDR, HDR);
 
     if (changed) {
diff --git a/src/video/cocoa/SDL_cocoamodes.m b/src/video/cocoa/SDL_cocoamodes.m
index fa6215e6a813..66bb3a72e3f2 100644
--- a/src/video/cocoa/SDL_cocoamodes.m
+++ b/src/video/cocoa/SDL_cocoamodes.m
@@ -302,6 +302,7 @@ static void Cocoa_GetHDRProperties(CGDirectDisplayID displayID, SDL_HDRDisplayPr
         if (screen && screen.maximumPotentialExtendedDynamicRangeColorComponentValue > 1.0f) {
             HDR->enabled = SDL_TRUE;
             HDR->SDR_whitelevel = 80.0f; /* SDR content is always at scRGB 1.0 */
+            HDR->HDR_whitelevel = HDR->SDR_whitelevel * screen.maximumExtendedDynamicRangeColorComponentValue;
         }
     }
 #endif
diff --git a/src/video/uikit/SDL_uikitmodes.m b/src/video/uikit/SDL_uikitmodes.m
index d9ca17f2c987..e655c7c39346 100644
--- a/src/video/uikit/SDL_uikitmodes.m
+++ b/src/video/uikit/SDL_uikitmodes.m
@@ -247,6 +247,7 @@ int UIKit_AddDisplay(UIScreen *uiscreen, SDL_bool send_event)
         if (uiscreen.potentialEDRHeadroom > 1.0f) {
             display.HDR.enabled = SDL_TRUE;
             display.HDR.SDR_whitelevel = 80.0f; /* SDR content is always at scRGB 1.0 */
+            display.HDR.HDR_whitelevel = display.HDR.SDR_whitelevel * uiscreen.currentEDRHeadroom;
         }
     }
 #endif /* !SDL_PLATFORM_TVOS */
diff --git a/src/video/windows/SDL_windowsmodes.c b/src/video/windows/SDL_windowsmodes.c
index 2ae5677664c6..c417420e9c18 100644
--- a/src/video/windows/SDL_windowsmodes.c
+++ b/src/video/windows/SDL_windowsmodes.c
@@ -339,7 +339,7 @@ static char *WIN_GetDisplayNameVista(const WCHAR *deviceName)
 
 static SDL_bool WIN_GetMonitorDESC1(HMONITOR hMonitor, DXGI_OUTPUT_DESC1 *desc)
 {
-    typedef HRESULT(WINAPI * PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory);
+    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;
@@ -349,18 +349,18 @@ static SDL_bool WIN_GetMonitorDESC1(HMONITOR hMonitor, DXGI_OUTPUT_DESC1 *desc)
 #else
     hDXGIMod = SDL_LoadObject("dxgi.dll");
     if (hDXGIMod) {
-        CreateDXGIFactoryFunc = (PFN_CREATE_DXGI_FACTORY)SDL_LoadFunction(hDXGIMod, "CreateDXGIFactory");
+        CreateDXGIFactoryFunc = (PFN_CREATE_DXGI_FACTORY)SDL_LoadFunction(hDXGIMod, "CreateDXGIFactory1");
     }
 #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_IDXGIFactory1 = { 0x770aae78, 0xf26f, 0x4dba, { 0xa8, 0x29, 0x25, 0x3c, 0x83, 0xd1, 0xb3, 0x87 } }; 
         static const GUID SDL_IID_IDXGIOutput6 = { 0x068346e8, 0xaaec, 0x4b84, { 0xad, 0xd7, 0x13, 0x7f, 0x51, 0x3f, 0x77, 0xa1 } };
-        IDXGIFactory2 *dxgiFactory;
+        IDXGIFactory1 *dxgiFactory;
 
-        if (SUCCEEDED(CreateDXGIFactoryFunc(&SDL_IID_IDXGIFactory2, (void **)&dxgiFactory))) {
+        if (SUCCEEDED(CreateDXGIFactoryFunc(&SDL_IID_IDXGIFactory1, (void **)&dxgiFactory))) {
             IDXGIAdapter1 *dxgiAdapter;
             UINT adapter = 0;
-            while (!found && SUCCEEDED(IDXGIFactory2_EnumAdapters1(dxgiFactory, adapter, &dxgiAdapter))) {
+            while (!found && SUCCEEDED(IDXGIFactory1_EnumAdapters1(dxgiFactory, adapter, &dxgiAdapter))) {
                 IDXGIOutput *dxgiOutput;
                 UINT output = 0;
                 while (!found && SUCCEEDED(IDXGIAdapter1_EnumOutputs(dxgiAdapter, output, &dxgiOutput))) {
@@ -482,13 +482,7 @@ static void WIN_GetHDRProperties(SDL_VideoDevice *_this, HMONITOR hMonitor, SDL_
         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.
-             */
+            HDR->HDR_whitelevel = desc.MaxLuminance;
         }
     }
 }
diff --git a/test/testcolorspace.c b/test/testcolorspace.c
index f3b6b3a9b127..3b479ae09d97 100644
--- a/test/testcolorspace.c
+++ b/test/testcolorspace.c
@@ -50,8 +50,8 @@ static SDL_bool HDR_enabled = SDL_FALSE;
 static float SDR_white_level = SDR_DISPLAY_WHITE_LEVEL;
 static float SDR_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 float HDR_white_level = SDR_DISPLAY_WHITE_LEVEL;
+static float HDR_color_scale = 1.0f;
 static SDL_FRect HDR_calibration_rect;
 
 enum
@@ -77,7 +77,10 @@ static float GetDisplaySDRWhiteLevel(void)
 {
     SDL_PropertiesID props;
 
-    HDR_enabled = SDL_FALSE;
+    if (!HDR_enabled) {
+        /* HDR is not enabled, use the SDR white level */
+        return SDR_DISPLAY_WHITE_LEVEL;
+    }
 
     props = SDL_GetRendererProperties(renderer);
     if (SDL_GetNumberProperty(props, SDL_PROP_RENDERER_OUTPUT_COLORSPACE_NUMBER, SDL_COLORSPACE_SRGB) != SDL_COLORSPACE_SCRGB) {
@@ -86,13 +89,6 @@ static float GetDisplaySDRWhiteLevel(void)
     }
 
     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);
 }
 
@@ -108,9 +104,23 @@ static void SetSDRWhiteLevel(float value)
     SDL_SetRenderColorScale(renderer, SDR_color_scale);
 }
 
-static void UpdateSDRWhiteLevel(void)
+static float GetDisplayHDRWhiteLevel(void)
 {
-    SetSDRWhiteLevel(GetDisplaySDRWhiteLevel());
+    SDL_PropertiesID props;
+
+    if (!HDR_enabled) {
+        /* HDR is not enabled, use the SDR white level */
+        return SDR_DISPLAY_WHITE_LEVEL;
+    }
+
+    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));
+    return SDL_GetFloatProperty(props, SDL_PROP_DISPLAY_HDR_WHITE_LEVEL_FLOAT, DEFAULT_HDR_WHITE_LEVEL);
 }
 
 static void SetHDRWhiteLevel(float value)
@@ -124,6 +134,26 @@ static void SetHDRWhiteLevel(float value)
     HDR_color_scale = HDR_white_level / SDR_DISPLAY_WHITE_LEVEL;
 }
 
+static void UpdateHDRState(void)
+{
+    SDL_PropertiesID props;
+
+    props = SDL_GetDisplayProperties(SDL_GetDisplayForWindow(window));
+    HDR_enabled = SDL_GetBooleanProperty(props, SDL_PROP_DISPLAY_HDR_ENABLED_BOOLEAN, SDL_FALSE);
+
+    SetHDRWhiteLevel(GetDisplayHDRWhiteLevel());
+    SetSDRWhiteLevel(GetDisplaySDRWhiteLevel());
+
+    SDL_Log("HDR %s\n", HDR_enabled ? "enabled" : "disabled");
+
+    if (HDR_enabled) {
+        props = SDL_GetRendererProperties(renderer);
+        if (SDL_GetNumberProperty(props, SDL_PROP_RENDERER_OUTPUT_COLORSPACE_NUMBER, SDL_COLORSPACE_SRGB) != SDL_COLORSPACE_SCRGB) {
+            SDL_Log("Run with --colorspace scRGB to display HDR colors\n");
+        }
+    }
+}
+
 static void CreateRenderer(void)
 {
     SDL_PropertiesID props;
@@ -144,9 +174,7 @@ static void CreateRenderer(void)
     SDL_Log("Created renderer %s\n", info.name);
     renderer_name = info.name;
 
-    UpdateSDRWhiteLevel();
-
-    SDL_Log("HDR is %s\n", HDR_enabled ? "enabled" : "disabled");
+    UpdateHDRState();
 }
 
 static void NextRenderer( void )
@@ -661,7 +689,7 @@ static void RenderHDRCalibration(void)
     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);
+    SDL_SetRenderColorScale(renderer, HDR_color_scale * 0.90f);
     rect = HDR_calibration_rect;
     rect.h -= 4.0f;
     rect.w = 60.0f;
@@ -747,8 +775,7 @@ static void loop(void)
                 OnMouseHeld(event.button.x, event.button.y);
             }
         } else if (event.type == SDL_EVENT_DISPLAY_HDR_STATE_CHANGED) {
-            SDL_Log("HDR %s\n", event.display.data1 ? "enabled" : "disabled");
-            UpdateSDRWhiteLevel();
+            UpdateHDRState();
         } else if (event.type == SDL_EVENT_QUIT) {
             done = 1;
         }