From 20a19c7aa4c554aeeaefd45f514b47cca01d5985 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Thu, 28 Aug 2025 11:03:24 +0200 Subject: [PATCH] Vulkan: Tweaks to improve HDR display on Windows * Improve accuracy of warning when HDR display is not supported, taking into account HDR mode on/off on Windows. * When HDR mode is disabled on Windows, don't create a HDR swapchain. This saves memory, and avoids a color difference on NVIDIA. That's because NVIDIA is the only GPU we've tested that allows a HDR swapchain when HDR mode is off, and we don't currently know the expected transforms for that case. * Recreate swapchain when HDR mode on/off switch is detected. * Update HDR info when window gains focus. Note this means there is no wide gamut when Windows HDR is off, but it was already not working. For that we may need to add support for something like 10bit VK_COLOR_SPACE_DISPLAY_P3_NONLINEAR_EXT, or whatever is commonly available outside of HDR mode. Pull Request: https://projects.blender.org/blender/blender/pulls/144959 --- intern/ghost/GHOST_C-api.h | 7 ++++ intern/ghost/GHOST_IWindow.hh | 6 +++ intern/ghost/intern/GHOST_C-api.cc | 6 +++ intern/ghost/intern/GHOST_ContextVK.cc | 41 +++++++++++-------- intern/ghost/intern/GHOST_ContextVK.hh | 2 +- intern/ghost/intern/GHOST_SystemWin32.cc | 1 + intern/ghost/intern/GHOST_Window.hh | 8 ++++ intern/ghost/intern/GHOST_WindowCocoa.mm | 6 ++- intern/ghost/intern/GHOST_WindowWayland.cc | 10 ++++- intern/ghost/intern/GHOST_WindowWin32.cc | 5 ++- intern/ghost/intern/GHOST_WindowWin32.hh | 2 - intern/ghost/intern/GHOST_WindowX11.cc | 3 +- scripts/startup/bl_ui/properties_render.py | 10 ++--- source/blender/makesrna/intern/rna_wm.cc | 14 +++++++ source/blender/windowmanager/WM_api.hh | 5 +++ .../blender/windowmanager/intern/wm_window.cc | 7 ++++ 16 files changed, 103 insertions(+), 30 deletions(-) diff --git a/intern/ghost/GHOST_C-api.h b/intern/ghost/GHOST_C-api.h index 758d65acc01..1b78196fd9f 100644 --- a/intern/ghost/GHOST_C-api.h +++ b/intern/ghost/GHOST_C-api.h @@ -228,6 +228,13 @@ extern GHOST_TSuccess GHOST_DisposeWindow(GHOST_SystemHandle systemhandle, */ extern bool GHOST_ValidWindow(GHOST_SystemHandle systemhandle, GHOST_WindowHandle windowhandle); +/* + ** + * Returns high dynamic range color information about this window. + * \return HDR info. + */ +extern GHOST_WindowHDRInfo GHOST_WindowGetHDRInfo(GHOST_WindowHandle windowhandle); + /** * Get the Window under the cursor. Although coordinates of the mouse are supplied, platform- * specific implementations are free to ignore these and query the mouse location themselves, due diff --git a/intern/ghost/GHOST_IWindow.hh b/intern/ghost/GHOST_IWindow.hh index 3b04edf2471..b7a95bed10b 100644 --- a/intern/ghost/GHOST_IWindow.hh +++ b/intern/ghost/GHOST_IWindow.hh @@ -379,6 +379,12 @@ class GHOST_IWindow { */ virtual uint16_t getDPIHint() = 0; + /** + * Returns high dynamic range color information about this window. + * \return HDR info. + * */ + virtual GHOST_WindowHDRInfo getHDRInfo() = 0; + #ifdef WITH_INPUT_IME /** * Enable IME attached to the given window, i.e. allows user-input diff --git a/intern/ghost/intern/GHOST_C-api.cc b/intern/ghost/intern/GHOST_C-api.cc index e9686336d88..c10e0b20f24 100644 --- a/intern/ghost/intern/GHOST_C-api.cc +++ b/intern/ghost/intern/GHOST_C-api.cc @@ -217,6 +217,12 @@ bool GHOST_ValidWindow(GHOST_SystemHandle systemhandle, GHOST_WindowHandle windo return system->validWindow(window); } +GHOST_WindowHDRInfo GHOST_WindowGetHDRInfo(GHOST_WindowHandle windowhandle) +{ + GHOST_IWindow *window = (GHOST_IWindow *)windowhandle; + return window->getHDRInfo(); +} + GHOST_WindowHandle GHOST_GetWindowUnderCursor(GHOST_SystemHandle systemhandle, int32_t x, int32_t y) diff --git a/intern/ghost/intern/GHOST_ContextVK.cc b/intern/ghost/intern/GHOST_ContextVK.cc index cd088e3e979..9cc79d86a9a 100644 --- a/intern/ghost/intern/GHOST_ContextVK.cc +++ b/intern/ghost/intern/GHOST_ContextVK.cc @@ -598,7 +598,8 @@ GHOST_ContextVK::GHOST_ContextVK(const GHOST_ContextParams &context_params, surface_(VK_NULL_HANDLE), swapchain_(VK_NULL_HANDLE), frame_data_(GHOST_FRAMES_IN_FLIGHT), - render_frame_(0) + render_frame_(0), + use_hdr_swapchain_(false) { } @@ -648,24 +649,31 @@ GHOST_TSuccess GHOST_ContextVK::swapBuffers() vkWaitForFences(device, 1, &submission_frame_data.submission_fence, true, UINT64_MAX); } submission_frame_data.discard_pile.destroy(device); - bool use_hdr_swapchain = true; -#ifdef WITH_GHOST_WAYLAND - /* Wayland doesn't provide a WSI with windowing capabilities, therefore cannot detect whether the - * swap-chain needs to be recreated. But as a side effect we can recreate the swap-chain before - * presenting. */ - if (wayland_window_info_) { - const bool recreate_swapchain = ((wayland_window_info_->size[0] != - std::max(render_extent_.width, render_extent_min_.width)) || - (wayland_window_info_->size[1] != - std::max(render_extent_.height, render_extent_min_.height))); - use_hdr_swapchain = wayland_window_info_->is_color_managed; - if (recreate_swapchain) { - /* Swap-chain is out of date. Recreate swap-chain. */ - recreateSwapchain(use_hdr_swapchain); - } + const bool use_hdr_swapchain = hdr_info_ && hdr_info_->hdr_enabled; + if (use_hdr_swapchain != use_hdr_swapchain_) { + /* Re-create swapchain if HDR mode was toggled in the system settings. */ + recreateSwapchain(use_hdr_swapchain); } + else { +#ifdef WITH_GHOST_WAYLAND + /* Wayland doesn't provide a WSI with windowing capabilities, therefore cannot detect whether + * the swap-chain needs to be recreated. But as a side effect we can recreate the swap-chain + * before presenting. */ + if (wayland_window_info_) { + const bool recreate_swapchain = + ((wayland_window_info_->size[0] != + std::max(render_extent_.width, render_extent_min_.width)) || + (wayland_window_info_->size[1] != + std::max(render_extent_.height, render_extent_min_.height))); + + if (recreate_swapchain) { + /* Swap-chain is out of date. Recreate swap-chain. */ + recreateSwapchain(use_hdr_swapchain); + } + } #endif + } /* There is no valid swapchain as the previous window was minimized. User can have maximized the * window so we need to check if the swapchain can be created. */ if (swapchain_ == VK_NULL_HANDLE) { @@ -1014,6 +1022,7 @@ GHOST_TSuccess GHOST_ContextVK::recreateSwapchain(bool use_hdr_swapchain) vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface_, &capabilities); } + use_hdr_swapchain_ = use_hdr_swapchain; render_extent_ = capabilities.currentExtent; render_extent_min_ = capabilities.minImageExtent; if (render_extent_.width == UINT32_MAX) { diff --git a/intern/ghost/intern/GHOST_ContextVK.hh b/intern/ghost/intern/GHOST_ContextVK.hh index 2582f23c1cc..1b0c1afc51c 100644 --- a/intern/ghost/intern/GHOST_ContextVK.hh +++ b/intern/ghost/intern/GHOST_ContextVK.hh @@ -56,7 +56,6 @@ enum GHOST_TVulkanPlatformType { struct GHOST_ContextVK_WindowInfo { int size[2]; - bool is_color_managed; }; struct GHOST_FrameDiscard { @@ -245,6 +244,7 @@ class GHOST_ContextVK : public GHOST_Context { VkExtent2D render_extent_; VkExtent2D render_extent_min_; VkSurfaceFormatKHR surface_format_; + bool use_hdr_swapchain_; std::function swap_buffers_pre_callback_; std::function swap_buffers_post_callback_; diff --git a/intern/ghost/intern/GHOST_SystemWin32.cc b/intern/ghost/intern/GHOST_SystemWin32.cc index 2a527915d75..5b937f6ef81 100644 --- a/intern/ghost/intern/GHOST_SystemWin32.cc +++ b/intern/ghost/intern/GHOST_SystemWin32.cc @@ -2361,6 +2361,7 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, uint msg, WPARAM wParam, * another window-management function. */ case WM_SETFOCUS: { /* The WM_SETFOCUS message is sent to a window after it has gained the keyboard focus. */ + window->updateHDRInfo(); break; } /* ============ diff --git a/intern/ghost/intern/GHOST_Window.hh b/intern/ghost/intern/GHOST_Window.hh index e6899c45489..aaa8c4fef65 100644 --- a/intern/ghost/intern/GHOST_Window.hh +++ b/intern/ghost/intern/GHOST_Window.hh @@ -250,6 +250,12 @@ class GHOST_Window : public GHOST_IWindow { return 96; } + /** \copydoc #GHOST_IWindow::getHDRInfo */ + GHOST_WindowHDRInfo getHDRInfo() override + { + return hdr_info_; + } + #ifdef WITH_INPUT_IME void beginIME( int32_t /*x*/, int32_t /*y*/, int32_t /*w*/, int32_t /*h*/, bool /*completed*/) override @@ -358,6 +364,8 @@ class GHOST_Window : public GHOST_IWindow { /* OSX only, retina screens */ float native_pixel_size_; + GHOST_WindowHDRInfo hdr_info_ = GHOST_WINDOW_HDR_INFO_NONE; + private: GHOST_Context *context_; }; diff --git a/intern/ghost/intern/GHOST_WindowCocoa.mm b/intern/ghost/intern/GHOST_WindowCocoa.mm index cb47c4b280f..b821fd13c4b 100644 --- a/intern/ghost/intern/GHOST_WindowCocoa.mm +++ b/intern/ghost/intern/GHOST_WindowCocoa.mm @@ -401,6 +401,10 @@ GHOST_WindowCocoa::GHOST_WindowCocoa(GHOST_SystemCocoa *systemCocoa, CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(name); metal_layer_.colorspace = colorspace; CGColorSpaceRelease(colorspace); + + /* For Blender to know if this window supports HDR. */ + hdr_info_.hdr_enabled = true; + hdr_info_.sdr_white_level = 1.0f; } metal_view_ = [[CocoaMetalView alloc] initWithSystemCocoa:systemCocoa @@ -898,7 +902,7 @@ GHOST_Context *GHOST_WindowCocoa::newDrawingContext(GHOST_TDrawingContextType ty #ifdef WITH_VULKAN_BACKEND case GHOST_kDrawingContextTypeVulkan: { GHOST_Context *context = new GHOST_ContextVK( - want_context_params_, metal_layer_, 1, 2, true, preferred_device_); + want_context_params_, metal_layer_, 1, 2, true, preferred_device_, &hdr_info_); if (context->initializeDrawingContext()) { return context; } diff --git a/intern/ghost/intern/GHOST_WindowWayland.cc b/intern/ghost/intern/GHOST_WindowWayland.cc index 0e4d9e1329f..954e812be26 100644 --- a/intern/ghost/intern/GHOST_WindowWayland.cc +++ b/intern/ghost/intern/GHOST_WindowWayland.cc @@ -1991,7 +1991,12 @@ GHOST_WindowWayland::GHOST_WindowWayland(GHOST_SystemWayland *system, window_->backend.vulkan_window_info = new GHOST_ContextVK_WindowInfo; window_->backend.vulkan_window_info->size[0] = window_->frame.size[0]; window_->backend.vulkan_window_info->size[1] = window_->frame.size[1]; - window_->backend.vulkan_window_info->is_color_managed = true; + + /* There is no HDR on/off settings as on Windows, so from the Window side + * consider it always enabled. But may still get disabled if Vulkan has no + * appropriate surface format. */ + hdr_info_.hdr_enabled = true; + hdr_info_.sdr_white_level = 1.0f; } #endif @@ -2472,7 +2477,8 @@ GHOST_Context *GHOST_WindowWayland::newDrawingContext(GHOST_TDrawingContextType window_->backend.vulkan_window_info, 1, 2, - preferred_device_); + preferred_device_, + &hdr_info_); if (context->initializeDrawingContext()) { return context; } diff --git a/intern/ghost/intern/GHOST_WindowWin32.cc b/intern/ghost/intern/GHOST_WindowWin32.cc index 1b92e7d0a4c..2e15690cce6 100644 --- a/intern/ghost/intern/GHOST_WindowWin32.cc +++ b/intern/ghost/intern/GHOST_WindowWin32.cc @@ -1326,7 +1326,10 @@ void GHOST_WindowWin32::updateHDRInfo() color_info.header.id = path.targetInfo.id; if (::DisplayConfigGetDeviceInfo(&color_info.header) == ERROR_SUCCESS) { - info.hdr_enabled = color_info.advancedColorSupported && color_info.advancedColorEnabled; + /* This particular combination indicates HDR mode is enabled. This is undocumented but + * used by WinRT. When wideColorEnforced is true we are in SDR mode with advanced color. */ + info.hdr_enabled = color_info.advancedColorSupported && color_info.advancedColorEnabled && + !color_info.wideColorEnforced; } if (info.hdr_enabled) { diff --git a/intern/ghost/intern/GHOST_WindowWin32.hh b/intern/ghost/intern/GHOST_WindowWin32.hh index 624515fe1ff..76c545bb96a 100644 --- a/intern/ghost/intern/GHOST_WindowWin32.hh +++ b/intern/ghost/intern/GHOST_WindowWin32.hh @@ -415,8 +415,6 @@ class GHOST_WindowWin32 : public GHOST_Window { GHOST_DirectManipulationHelper *direct_manipulation_helper_; - GHOST_WindowHDRInfo hdr_info_ = GHOST_WINDOW_HDR_INFO_NONE; - #ifdef WITH_INPUT_IME /** Handle input method editors event */ GHOST_ImeWin32 ime_input_; diff --git a/intern/ghost/intern/GHOST_WindowX11.cc b/intern/ghost/intern/GHOST_WindowX11.cc index 242237233d8..23197389639 100644 --- a/intern/ghost/intern/GHOST_WindowX11.cc +++ b/intern/ghost/intern/GHOST_WindowX11.cc @@ -1191,7 +1191,8 @@ GHOST_Context *GHOST_WindowX11::newDrawingContext(GHOST_TDrawingContextType type nullptr, 1, 2, - preferred_device_); + preferred_device_, + &hdr_info_); if (context->initializeDrawingContext()) { return context; } diff --git a/scripts/startup/bl_ui/properties_render.py b/scripts/startup/bl_ui/properties_render.py index 19025d5743c..78f56565429 100644 --- a/scripts/startup/bl_ui/properties_render.py +++ b/scripts/startup/bl_ui/properties_render.py @@ -76,12 +76,10 @@ class RENDER_PT_color_management(RenderButtonsPanel, Panel): col.prop(view, "view_transform") col.prop(view, "look") - if view.is_hdr: - import gpu - if not gpu.capabilities.hdr_support_get(): - row = col.split(factor=0.4) - row.label() - row.label(text="HDR display not supported", icon="INFO") + if view.is_hdr and not context.window.support_hdr_color: + row = col.split(factor=0.4) + row.label() + row.label(text="HDR display not supported", icon="INFO") col = flow.column() col.prop(view, "exposure") diff --git a/source/blender/makesrna/intern/rna_wm.cc b/source/blender/makesrna/intern/rna_wm.cc index 7a59666d1bf..f6811f30484 100644 --- a/source/blender/makesrna/intern/rna_wm.cc +++ b/source/blender/makesrna/intern/rna_wm.cc @@ -1049,6 +1049,12 @@ static void rna_Window_view_layer_set(PointerRNA *ptr, PointerRNA value, ReportL WM_window_set_active_view_layer(win, view_layer); } +static bool rna_Window_support_hdr_color_get(PointerRNA *ptr) +{ + wmWindow *win = static_cast(ptr->data); + return WM_window_support_hdr_color(win); +} + static bool rna_Window_modal_handler_skip(CollectionPropertyIterator * /*iter*/, void *data) { const wmEventHandler_Op *handler = (wmEventHandler_Op *)data; @@ -2782,6 +2788,14 @@ static void rna_def_window(BlenderRNA *brna) RNA_def_property_struct_type(prop, "Stereo3dDisplay"); RNA_def_property_ui_text(prop, "Stereo 3D Display", "Settings for stereo 3D display"); + prop = RNA_def_property(srna, "support_hdr_color", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + RNA_def_property_ui_text(prop, + "Support HDR Color", + "The window has a HDR graphics buffer that wide gamut and high dynamic " + "range colors can be written to, in extended sRGB color space."); + RNA_def_property_boolean_funcs(prop, "rna_Window_support_hdr_color_get", nullptr); + prop = RNA_def_property(srna, "modal_operators", PROP_COLLECTION, PROP_NONE); RNA_def_property_struct_type(prop, "Operator"); RNA_def_property_clear_flag(prop, PROP_EDITABLE); diff --git a/source/blender/windowmanager/WM_api.hh b/source/blender/windowmanager/WM_api.hh index aaab582081b..178457aa316 100644 --- a/source/blender/windowmanager/WM_api.hh +++ b/source/blender/windowmanager/WM_api.hh @@ -316,6 +316,11 @@ bool WM_window_is_main_top_level(const wmWindow *win); bool WM_window_is_fullscreen(const wmWindow *win); bool WM_window_is_maximized(const wmWindow *win); +/* + * Support for wide gamut and HDR colors. + */ +bool WM_window_support_hdr_color(const wmWindow *win); + /** * Some editor data may need to be synced with scene data (3D View camera and layers). * This function ensures data is synced for editors diff --git a/source/blender/windowmanager/intern/wm_window.cc b/source/blender/windowmanager/intern/wm_window.cc index d2d5aeaf192..9d5ca7a67a7 100644 --- a/source/blender/windowmanager/intern/wm_window.cc +++ b/source/blender/windowmanager/intern/wm_window.cc @@ -83,6 +83,7 @@ #include "UI_interface_layout.hh" #include "BLF_api.hh" +#include "GPU_capabilities.hh" #include "GPU_context.hh" #include "GPU_framebuffer.hh" #include "GPU_init_exit.hh" @@ -2895,6 +2896,12 @@ bool WM_window_is_main_top_level(const wmWindow *win) return true; } +bool WM_window_support_hdr_color(const wmWindow *win) +{ + return GPU_hdr_support() && win->ghostwin && + GHOST_WindowGetHDRInfo(static_cast(win->ghostwin)).hdr_enabled; +} + /** \} */ /* -------------------------------------------------------------------- */