Support output to HDR monitors

Co-authored-by: Alvin Wong <alvinhochun@gmail.com>
Co-authored-by: Allen Pestaluky <allenpestaluky@gmail.com>
This commit is contained in:
Josh Jones 2025-02-02 18:12:45 -05:00
parent 40448082ab
commit b8389cc76b
65 changed files with 1579 additions and 233 deletions

View file

@ -57,6 +57,7 @@
#endif
#if defined(D3D12_ENABLED)
#include "drivers/d3d12/rendering_context_driver_d3d12.h"
#include <dxgi1_6.h>
#endif
#if defined(GLES3_ENABLED)
#include "drivers/gles3/rasterizer_gles3.h"
@ -150,6 +151,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const {
case FEATURE_STATUS_INDICATOR:
case FEATURE_WINDOW_EMBEDDING:
case FEATURE_WINDOW_DRAG:
case FEATURE_HDR_OUTPUT:
return true;
case FEATURE_SCREEN_EXCLUDE_FROM_CAPTURE:
return (os_ver.dwBuildNumber >= 19041); // Fully supported on Windows 10 Vibranium R1 (2004)+ only, captured as black rect on older versions.
@ -1192,6 +1194,24 @@ static BOOL CALLBACK _MonitorEnumProcCount(HMONITOR hMonitor, HDC hdcMonitor, LP
return TRUE;
}
static BOOL CALLBACK _MonitorEnumProcMonitor(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData) {
EnumScreenData *data = (EnumScreenData *)dwData;
if (data->screen == data->count) {
data->monitor = hMonitor;
return FALSE;
}
data->count++;
return TRUE;
}
static HMONITOR _get_hmonitor_of_screen(int p_screen) {
EnumScreenData data = { 0, p_screen, nullptr };
EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcMonitor, (LPARAM)&data);
return data.monitor;
}
int DisplayServerWindows::get_screen_count() const {
_THREAD_SAFE_METHOD_
@ -1565,6 +1585,101 @@ Ref<Image> DisplayServerWindows::screen_get_image_rect(const Rect2i &p_rect) con
return img;
}
#ifdef D3D12_ENABLED
static bool _get_monitor_desc(HMONITOR p_monitor, IDXGIFactory2 *dxgi_factory, DXGI_OUTPUT_DESC1 &r_Desc) {
r_Desc = {};
// Note: As of August, 2025 Microsoft's sample code only checks the default
// adapter, but sometimes p_monitor may belong to another adapter.
Microsoft::WRL::ComPtr<IDXGIAdapter1> dxgiAdapter;
UINT adapter_i = 0;
while (dxgi_factory->EnumAdapters1(adapter_i, &dxgiAdapter) == S_OK) {
Microsoft::WRL::ComPtr<IDXGIOutput> dxgiOutput;
DXGI_OUTPUT_DESC1 desc1;
UINT output_i = 0;
while (dxgiAdapter->EnumOutputs(output_i, &dxgiOutput) != DXGI_ERROR_NOT_FOUND) {
Microsoft::WRL::ComPtr<IDXGIOutput6> output6;
if (FAILED(dxgiOutput.As(&output6))) {
continue;
}
if (FAILED(output6->GetDesc1(&desc1))) {
continue;
}
if (desc1.Monitor == p_monitor) {
r_Desc = desc1;
return true;
}
output_i++;
}
adapter_i++;
}
return false;
}
#endif // D3D12_ENABLED
// Store a list of displays that have failed to get their SDR white level so we don't spam the error log.
static HashSet<Vector3i> displays_with_white_level_error;
static float _get_sdr_white_level_for_hmonitor(HMONITOR p_monitor, const LocalVector<DISPLAYCONFIG_PATH_INFO> &p_paths) {
if (!p_monitor) {
return 0.0f;
}
MONITORINFOEXW minfo;
memset(&minfo, 0, sizeof(minfo));
minfo.cbSize = sizeof(minfo);
if (!GetMonitorInfoW(p_monitor, &minfo)) {
ERR_FAIL_V_MSG(0.0f, vformat("Failed to get monitor info for HMONITOR: 0x%X", (intptr_t)p_monitor));
}
for (const DISPLAYCONFIG_PATH_INFO &path : p_paths) {
const Vector3i key = Vector3i((int32_t)path.sourceInfo.adapterId.HighPart, (int32_t)path.sourceInfo.adapterId.LowPart, (int32_t)path.sourceInfo.id);
DISPLAYCONFIG_SOURCE_DEVICE_NAME source_name;
memset(&source_name, 0, sizeof(source_name));
source_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
source_name.header.size = sizeof(source_name);
source_name.header.adapterId = path.sourceInfo.adapterId;
source_name.header.id = path.sourceInfo.id;
if (DisplayConfigGetDeviceInfo(&source_name.header) != ERROR_SUCCESS) {
if (!displays_with_white_level_error.has(key)) {
WARN_PRINT(vformat("Failed to get source name for adapterId: 0x%08X%08X, id: %d", (uint32_t)path.sourceInfo.adapterId.HighPart, (uint32_t)path.sourceInfo.adapterId.LowPart, path.sourceInfo.id));
displays_with_white_level_error.insert(key);
}
continue;
}
if (wcscmp(minfo.szDevice, source_name.viewGdiDeviceName) != 0) {
continue;
}
// Query the SDR white level.
DISPLAYCONFIG_SDR_WHITE_LEVEL sdr_white_level;
memset(&sdr_white_level, 0, sizeof(sdr_white_level));
sdr_white_level.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
sdr_white_level.header.size = sizeof(sdr_white_level);
sdr_white_level.header.adapterId = path.targetInfo.adapterId;
sdr_white_level.header.id = path.targetInfo.id;
if (DisplayConfigGetDeviceInfo(&sdr_white_level.header) != ERROR_SUCCESS) {
if (!displays_with_white_level_error.has(key)) {
WARN_PRINT(vformat("Failed to get SDR white level for adapterId: 0x%08X%08X, id: %d", (uint32_t)path.targetInfo.adapterId.HighPart, (uint32_t)path.targetInfo.adapterId.LowPart, path.targetInfo.id));
displays_with_white_level_error.insert(key);
}
continue;
}
return (float)sdr_white_level.SDRWhiteLevel / 1000.0f * 80.0f;
}
return 0.0f;
}
float DisplayServerWindows::screen_get_refresh_rate(int p_screen) const {
_THREAD_SAFE_METHOD_
@ -3196,6 +3311,96 @@ HWND DisplayServerWindows::_find_window_from_process_id(OS::ProcessID p_pid, HWN
return NULL;
}
// Get screen HDR capabilities for internal use only.
DisplayServerWindows::ScreenHdrData DisplayServerWindows::_get_screen_hdr_data(int p_screen) const {
ScreenHdrData data;
HMONITOR monitor = _get_hmonitor_of_screen(p_screen);
if (!monitor) {
return data;
}
#ifdef D3D12_ENABLED
// A dynamic cast is used here because the rendering context is not an Object and Object:cast is not supported.
RenderingContextDriverD3D12 *rendering_context_d3d12 = dynamic_cast<RenderingContextDriverD3D12 *>(rendering_context);
if (rendering_context_d3d12) {
IDXGIFactory2 *dxgi_factory = rendering_context_d3d12->dxgi_factory_get();
DXGI_OUTPUT_DESC1 desc;
if (_get_monitor_desc(monitor, dxgi_factory, desc)) {
data.hdr_supported = desc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020;
data.min_luminance = desc.MinLuminance;
data.max_luminance = desc.MaxLuminance;
data.max_average_luminance = desc.MaxFullFrameLuminance;
}
}
#endif // D3D12_ENABLED
uint32_t path_count = 0;
uint32_t mode_count = 0;
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &path_count, &mode_count) == ERROR_SUCCESS) {
LocalVector<DISPLAYCONFIG_PATH_INFO> paths;
LocalVector<DISPLAYCONFIG_MODE_INFO> modes;
paths.resize(path_count);
modes.resize(mode_count);
if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &path_count, paths.ptr(), &mode_count, modes.ptr(), nullptr) == ERROR_SUCCESS) {
data.sdr_white_level = _get_sdr_white_level_for_hmonitor(monitor, paths);
}
}
return data;
}
void DisplayServerWindows::_update_hdr_output_for_window(WindowID p_window, const WindowData &p_window_data, ScreenHdrData p_screen_data) {
#ifdef RD_ENABLED
if (rendering_context) {
bool current_hdr_enabled = rendering_context->window_get_hdr_output_enabled(p_window);
bool desired_hdr_enabled = p_window_data.hdr_output_requested && p_screen_data.hdr_supported;
if (current_hdr_enabled != desired_hdr_enabled) {
rendering_context->window_set_hdr_output_enabled(p_window, desired_hdr_enabled);
rendering_context->window_set_hdr_output_linear_luminance_scale(p_window, 80.0f);
}
// If auto reference luminance is enabled, update it based on the current SDR white level.
if (p_window_data.hdr_output_reference_luminance < 0.0f) {
if (p_screen_data.sdr_white_level > 0.0f) {
rendering_context->window_set_hdr_output_reference_luminance(p_window, p_screen_data.sdr_white_level);
}
// If we cannot get the SDR white level, leave the previous value unchanged.
}
// If auto max luminance is enabled, update it based on the screen's max luminance.
if (p_window_data.hdr_output_max_luminance < 0.0f) {
if (p_screen_data.max_luminance > 0.0f) {
rendering_context->window_set_hdr_output_max_luminance(p_window, p_screen_data.max_luminance);
}
// If we cannot get the screen's max luminance, leave the previous value unchanged.
}
}
#endif // RD_ENABLED
}
void DisplayServerWindows::_update_hdr_output_for_tracked_windows() {
hdr_output_cache.clear();
for (const KeyValue<WindowID, WindowData> &E : windows) {
if (E.value.hdr_output_requested) {
int screen = window_get_current_screen(E.key);
ScreenHdrData data;
if (!hdr_output_cache.has(screen)) {
data = _get_screen_hdr_data(screen);
hdr_output_cache.insert(screen, data);
} else {
data = hdr_output_cache[screen];
}
_update_hdr_output_for_window(E.key, E.value, data);
}
}
}
Error DisplayServerWindows::embed_process(WindowID p_window, OS::ProcessID p_pid, const Rect2i &p_rect, bool p_visible, bool p_grab_focus) {
_THREAD_SAFE_METHOD_
@ -3881,6 +4086,10 @@ void DisplayServerWindows::process_events() {
Input::get_singleton()->flush_buffered_events();
}
// Poll HDR output state for windows that have requested it.
// Needed to detect changes in luminance values due to a lack of Windows events for such changes.
_update_hdr_output_for_tracked_windows();
LocalVector<List<FileDialogData *>::Element *> to_remove;
for (List<FileDialogData *>::Element *E = file_dialogs.front(); E; E = E->next()) {
FileDialogData *fd = E->get();
@ -4340,6 +4549,157 @@ DisplayServer::VSyncMode DisplayServerWindows::window_get_vsync_mode(WindowID p_
return DisplayServer::VSYNC_ENABLED;
}
bool DisplayServerWindows::window_is_hdr_output_supported(WindowID p_window) const {
_THREAD_SAFE_METHOD_
#if defined(RD_ENABLED)
if (rendering_device && !rendering_device->has_feature(RenderingDevice::Features::SUPPORTS_HDR_OUTPUT)) {
return false; // HDR output is not supported by the rendering device.
}
#endif
// The window supports HDR if the screen it is on supports HDR.
int screen = window_get_current_screen(p_window);
DisplayServerWindows::ScreenHdrData data = _get_screen_hdr_data(screen);
return data.hdr_supported;
}
void DisplayServerWindows::window_request_hdr_output(const bool p_enable, WindowID p_window) {
_THREAD_SAFE_METHOD_
#if defined(RD_ENABLED)
ERR_FAIL_COND_EDMSG(p_enable && (rendering_device && rendering_device->has_feature(RenderingDevice::Features::SUPPORTS_HDR_OUTPUT)) == false, "HDR output is not supported by the rendering device.");
#endif
WindowData &wd = windows[p_window];
wd.hdr_output_requested = p_enable;
int screen = window_get_current_screen(p_window);
DisplayServerWindows::ScreenHdrData data = _get_screen_hdr_data(screen);
_update_hdr_output_for_window(p_window, wd, data);
}
bool DisplayServerWindows::window_is_hdr_output_requested(WindowID p_window) const {
_THREAD_SAFE_METHOD_
const WindowData &wd = windows[p_window];
return wd.hdr_output_requested;
}
bool DisplayServerWindows::window_is_hdr_output_enabled(WindowID p_window) const {
_THREAD_SAFE_METHOD_
#if defined(RD_ENABLED)
if (rendering_context) {
return rendering_context->window_get_hdr_output_enabled(p_window);
}
#endif
return false;
}
void DisplayServerWindows::window_set_hdr_output_reference_luminance(const float p_reference_luminance, WindowID p_window) {
_THREAD_SAFE_METHOD_
WindowData &wd = windows[p_window];
if (Math::is_equal_approx(wd.hdr_output_reference_luminance, p_reference_luminance)) {
return;
}
wd.hdr_output_reference_luminance = p_reference_luminance;
// Negative luminance means auto-adjust
if (wd.hdr_output_reference_luminance < 0.0f) {
int screen = window_get_current_screen(p_window);
DisplayServerWindows::ScreenHdrData data = _get_screen_hdr_data(screen);
_update_hdr_output_for_window(p_window, wd, data);
} else {
// Otherwise, apply the requested luminance
#if defined(RD_ENABLED)
if (rendering_context) {
rendering_context->window_set_hdr_output_reference_luminance(p_window, p_reference_luminance);
}
#endif
}
}
float DisplayServerWindows::window_get_hdr_output_reference_luminance(WindowID p_window) const {
_THREAD_SAFE_METHOD_
const WindowData &wd = windows[p_window];
return wd.hdr_output_reference_luminance;
}
float DisplayServerWindows::window_get_hdr_output_current_reference_luminance(WindowID p_window) const {
_THREAD_SAFE_METHOD_
#if defined(RD_ENABLED)
if (rendering_context) {
return rendering_context->window_get_hdr_output_reference_luminance(p_window);
}
#endif
return 0.0f;
}
void DisplayServerWindows::window_set_hdr_output_max_luminance(const float p_max_luminance, WindowID p_window) {
_THREAD_SAFE_METHOD_
WindowData &wd = windows[p_window];
if (Math::is_equal_approx(wd.hdr_output_max_luminance, p_max_luminance)) {
return;
}
wd.hdr_output_max_luminance = p_max_luminance;
// Negative luminance means auto-adjust
if (wd.hdr_output_max_luminance < 0.0f) {
int screen = window_get_current_screen(p_window);
DisplayServerWindows::ScreenHdrData data = _get_screen_hdr_data(screen);
_update_hdr_output_for_window(p_window, wd, data);
} else {
// Otherwise, apply the requested luminance
#if defined(RD_ENABLED)
if (rendering_context) {
rendering_context->window_set_hdr_output_max_luminance(p_window, p_max_luminance);
}
#endif
}
}
float DisplayServerWindows::window_get_hdr_output_max_luminance(WindowID p_window) const {
_THREAD_SAFE_METHOD_
const WindowData &wd = windows[p_window];
return wd.hdr_output_max_luminance;
}
float DisplayServerWindows::window_get_hdr_output_current_max_luminance(WindowID p_window) const {
_THREAD_SAFE_METHOD_
#if defined(RD_ENABLED)
if (rendering_context) {
return rendering_context->window_get_hdr_output_max_luminance(p_window);
}
#endif
return 0.0f;
}
float DisplayServerWindows::window_get_output_max_linear_value(WindowID p_window) const {
_THREAD_SAFE_METHOD_
#if defined(RD_ENABLED)
if (rendering_context) {
return rendering_context->window_get_output_max_linear_value(p_window);
}
#endif
return 1.0f; // SDR
}
void DisplayServerWindows::window_start_drag(WindowID p_window) {
_THREAD_SAFE_METHOD_
@ -5818,6 +6178,11 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
}
} break;
case WM_DISPLAYCHANGE: {
// Update HDR capabilities and reference luminance when display changes.
_update_hdr_output_for_tracked_windows();
} break;
case WM_WINDOWPOSCHANGED: {
WindowData &window = windows[window_id];
@ -5913,6 +6278,9 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
window.rect_changed_callback.call(Rect2i(window.last_pos.x, window.last_pos.y, window.width, window.height));
}
// Update HDR capabilities and reference luminance when window moves to different screen.
_update_hdr_output_for_tracked_windows();
// Update cursor clip region after window rect has changed.
if (mouse_mode == MOUSE_MODE_CAPTURED || mouse_mode == MOUSE_MODE_CONFINED || mouse_mode == MOUSE_MODE_CONFINED_HIDDEN) {
RECT crect;