From 30e176d6ba5f89cd76a533aa4f1666b232c1132c Mon Sep 17 00:00:00 2001 From: Sam Lantinga 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 | 18 +- 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 | 266 ++++++++++++++++++++++----- test/testffmpeg.c | 52 +++++- 11 files changed, 510 insertions(+), 57 deletions(-) diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index 4cf67b548..6ce79f56e 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 7a0862dcb..ca2b77239 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_WINDOW_POINTER`: the window where rendering is - * displayed - * - `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_WINDOW_POINTER`: the window where rendering is + * 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_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 8b8d280d8..8b935cf2c 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 71ce50133..0196aa1bd 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 2623fe5ef..72599dfc9 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 48bab3bfe..bf27b3546 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 5eb95c3cf..9bf5f3e6b 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 48e87fb44..5ad2126ac 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 6621d598e..2ae567766 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 + /* Windows CE compatibility */ #ifndef CDS_FULLSCREEN #define CDS_FULLSCREEN 0 @@ -334,6 +337,162 @@ WIN_GetDisplayNameVista_failed: 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 c5db03061..3e701a934 100644 --- a/test/testcolorspace.c +++ b/test/testcolorspace.c @@ -18,6 +18,18 @@ #include #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)); - y += 64.0f; - - y += TEXT_LINE_ADVANCE; - y += TEXT_LINE_ADVANCE; - - DrawTextWhite(x, y, "HDR gradient (%d nits)", 400); - 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(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)", 1000); + 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(1000)); + 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 (%g nits)", MAXIMUM_HDR_WHITE_LEVEL); + y += TEXT_LINE_ADVANCE; + 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,28 +599,119 @@ 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; - 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(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; - 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(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; - DrawGradientTexture(x, y, WINDOW_WIDTH - 2 * x, 64.0f, 0.0f, sRGBfromNits(1000)); + 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; + + y += TEXT_LINE_ADVANCE; + + 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 x, float y) +{ + if (stage_index == StageHDRCalibration) { + OnHDRCalibrationMouseHeld(x, y); + } +} + static void loop(void) { SDL_Event event; @@ -565,6 +738,14 @@ static void loop(void) default: break; } + } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + OnMouseHeld(event.button.x, event.button.y); + } else if (event.type == SDL_EVENT_MOUSE_MOTION) { + if (event.motion.state) { + OnMouseHeld(event.button.x, event.button.y); + } + } else if (event.type == SDL_EVENT_DISPLAY_HDR_STATE_CHANGED) { + UpdateSDRWhiteLevel(); } else if (event.type == SDL_EVENT_QUIT) { done = 1; } @@ -575,22 +756,25 @@ static void loop(void) SDL_RenderClear(renderer); switch (stage_index) { - case 0: + case StageClearBackground: RenderClearBackground(); break; - case 1: + case StageDrawBackground: RenderDrawBackground(); break; - case 2: + case StageBlendDrawing: RenderBlendDrawing(); break; - case 3: + case StageBlendTexture: RenderBlendTexture(); break; - case 4: + case StageHDRCalibration: + RenderHDRCalibration(); + break; + case StageGradientDrawing: RenderGradientDrawing(); break; - case 5: + case StageGradientTexture: RenderGradientTexture(); break; } diff --git a/test/testffmpeg.c b/test/testffmpeg.c index 83cda9f67..bea30ab55 100644 --- a/test/testffmpeg.c +++ b/test/testffmpeg.c @@ -57,6 +57,16 @@ #include "icon.h" +/* 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 + + static SDL_Texture *sprite; static SDL_FRect *positions; static SDL_FRect *velocities; @@ -90,6 +100,33 @@ struct SwsContextContainer static const char *SWS_CONTEXT_CONTAINER_PROPERTY = "SWS_CONTEXT_CONTAINER"; static int done; +/* This function isn't Windows specific, but we haven't hooked up HDR video support on other platforms yet */ +#ifdef SDL_PLATFORM_WIN32 +static void GetDisplayHDRProperties(SDL_bool *HDR_display, float *SDR_white_level) +{ + SDL_PropertiesID props; + + 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 */ + *HDR_display = SDL_FALSE; + *SDR_white_level = SDR_DISPLAY_WHITE_LEVEL; + return; + } + + 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 */ + *HDR_display = SDL_FALSE; + *SDR_white_level = SDR_DISPLAY_WHITE_LEVEL; + return; + } + + *HDR_display = SDL_TRUE; + *SDR_white_level = SDL_GetFloatProperty(props, SDL_PROP_DISPLAY_SDR_WHITE_LEVEL_FLOAT, DEFAULT_SDR_WHITE_LEVEL); +} +#endif /* SDL_PLATFORM_WIN32 */ + static SDL_bool CreateWindowAndRenderer(Uint32 window_flags, const char *driver) { SDL_PropertiesID props; @@ -646,14 +683,16 @@ static SDL_bool GetTextureForD3D11Frame(AVFrame *frame, SDL_Texture **texture) SDL_QueryTexture(*texture, NULL, NULL, &texture_width, &texture_height); } if (!*texture || (UINT)texture_width != desc.Width || (UINT)texture_height != desc.Height) { - SDL_bool HDR_display = SDL_TRUE; + float SDR_white_level, video_white_level; + SDL_bool HDR_display = SDL_FALSE; SDL_bool HDR_video = SDL_FALSE; - float display_white_level, video_white_level; if (*texture) { SDL_DestroyTexture(*texture); } + GetDisplayHDRProperties(&HDR_display, &SDR_white_level); + SDL_PropertiesID props = SDL_CreateProperties(); SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, GetFrameColorspace(frame)); switch (desc.Format) { @@ -683,15 +722,12 @@ static SDL_bool GetTextureForD3D11Frame(AVFrame *frame, SDL_Texture **texture) } if (HDR_video != HDR_display) { - /* Use some reasonable assumptions for white levels */ if (HDR_display) { - display_white_level = 200.0f; /* SDR white level */ - video_white_level = 80.0f; + video_white_level = SDR_DISPLAY_WHITE_LEVEL; } else { - display_white_level = 80.0f; - video_white_level = 400.0f; + video_white_level = DEFAULT_HDR_WHITE_LEVEL; } - SDL_SetRenderColorScale(renderer, display_white_level / video_white_level); + SDL_SetRenderColorScale(renderer, SDR_white_level / video_white_level); } else { SDL_SetRenderColorScale(renderer, 1.0f); }