Vulkan: HDR support for Windows

This PR adds HDR support for Windows for `VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT`
on `VK_FORMAT_R16G16B16A16_SFLOAT` swapchains .

For nonlinear surface formats (sRGB and extended sRGB) the back buffer is blit into the swapchain,
When VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT is used as surface format a compute shader
is used to flip and invert the gamma.

SDR white level is updated from a few window event changes, but actually
none of them immediately respond to SDR white level changes in the system.
That requires using the WinRT API, which we don't do so far.

Current limitations:
- Intel GPU support
- Dual GPU support

In the future we may add controls inside Blender for absolute HDR nits,
across different platforms. But this makes behavior closer to macOS.

See !144565 for details

Co-authored-by: Brecht Van Lommel <brecht@blender.org>
Pull Request: https://projects.blender.org/blender/blender/pulls/144717
This commit is contained in:
Jeroen Bakker
2025-08-22 10:11:55 +02:00
parent 366eba1d04
commit 0ea1feabd9
16 changed files with 259 additions and 27 deletions

View File

@@ -852,6 +852,18 @@ typedef struct {
float colored_titlebar_bg_color[3];
} GHOST_WindowDecorationStyleSettings;
typedef struct {
/* Is HDR enabled for this Window? */
bool hdr_enabled;
/* Scale factor to display SDR content in HDR. */
float sdr_white_level;
} GHOST_WindowHDRInfo;
#define GHOST_WINDOW_HDR_INFO_NONE \
{ \
/*hdr_enabled*/ false, /*sdr_white_level*/ 1.0f, \
}
#ifdef WITH_VULKAN_BACKEND
typedef struct {
/** Image handle to the image that will be presented to the user. */
@@ -866,6 +878,8 @@ typedef struct {
VkSemaphore present_semaphore;
/** Fence to signal after the image has been updated. */
VkFence submission_fence;
/* Factor to scale SDR content to HDR. */
float sdr_scale;
} GHOST_VulkanSwapChainData;
typedef enum {

View File

@@ -574,7 +574,8 @@ GHOST_ContextVK::GHOST_ContextVK(const GHOST_ContextParams &context_params,
#endif
int contextMajorVersion,
int contextMinorVersion,
const GHOST_GPUDevice &preferred_device)
const GHOST_GPUDevice &preferred_device,
const GHOST_WindowHDRInfo *hdr_info)
: GHOST_Context(context_params),
#ifdef _WIN32
hwnd_(hwnd),
@@ -593,6 +594,7 @@ GHOST_ContextVK::GHOST_ContextVK(const GHOST_ContextParams &context_params,
context_major_version_(contextMajorVersion),
context_minor_version_(contextMinorVersion),
preferred_device_(preferred_device),
hdr_info_(hdr_info),
surface_(VK_NULL_HANDLE),
swapchain_(VK_NULL_HANDLE),
frame_data_(GHOST_FRAMES_IN_FLIGHT),
@@ -644,7 +646,7 @@ GHOST_TSuccess GHOST_ContextVK::swapBuffers()
* has been signaled and waited for. */
vkWaitForFences(device, 1, &submission_frame_data.submission_fence, true, UINT64_MAX);
submission_frame_data.discard_pile.destroy(device);
bool use_hdr_swapchain = false;
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
@@ -720,6 +722,7 @@ GHOST_TSuccess GHOST_ContextVK::swapBuffers()
swap_chain_data.submission_fence = submission_frame_data.submission_fence;
swap_chain_data.acquire_semaphore = submission_frame_data.acquire_semaphore;
swap_chain_data.present_semaphore = swapchain_image.present_semaphore;
swap_chain_data.sdr_scale = (hdr_info_) ? hdr_info_->sdr_white_level : 1.0f;
vkResetFences(device, 1, &submission_frame_data.submission_fence);
if (swap_buffers_pre_callback_) {
@@ -767,6 +770,7 @@ GHOST_TSuccess GHOST_ContextVK::getVulkanSwapChainFormat(
r_swap_chain_data->image = VK_NULL_HANDLE;
r_swap_chain_data->surface_format = surface_format_;
r_swap_chain_data->extent = render_extent_;
r_swap_chain_data->sdr_scale = (hdr_info_) ? hdr_info_->sdr_white_level : 1.0f;
return GHOST_kSuccess;
}
@@ -915,7 +919,7 @@ static bool selectSurfaceFormat(const VkPhysicalDevice physical_device,
vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface, &format_count, formats.data());
array<pair<VkColorSpaceKHR, VkFormat>, 4> selection_order = {
make_pair(VK_COLOR_SPACE_EXTENDED_SRGB_NONLINEAR_EXT, VK_FORMAT_R16G16B16A16_SFLOAT),
make_pair(VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT, VK_FORMAT_R16G16B16A16_SFLOAT),
make_pair(VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, VK_FORMAT_R16G16B16A16_SFLOAT),
make_pair(VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, VK_FORMAT_R8G8B8A8_UNORM),
make_pair(VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, VK_FORMAT_B8G8R8A8_UNORM),
@@ -1119,7 +1123,7 @@ GHOST_TSuccess GHOST_ContextVK::recreateSwapchain(bool use_hdr_swapchain)
create_info.imageColorSpace = surface_format_.colorSpace;
create_info.imageExtent = render_extent_;
create_info.imageArrayLayers = 1;
create_info.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT;
create_info.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
create_info.preTransform = capabilities.currentTransform;
create_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
create_info.presentMode = present_mode;
@@ -1226,6 +1230,7 @@ GHOST_TSuccess GHOST_ContextVK::initializeDrawingContext()
bool use_hdr_swapchain = false;
#ifdef _WIN32
const bool use_window_surface = (hwnd_ != nullptr);
use_hdr_swapchain = true;
#elif defined(__APPLE__)
const bool use_window_surface = (metal_layer_ != nullptr);
#else /* UNIX/Linux */
@@ -1281,6 +1286,13 @@ GHOST_TSuccess GHOST_ContextVK::initializeDrawingContext()
VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME);
optional_device_extensions.push_back(VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME);
}
const bool use_swapchain_colorspace = contains_extension(
extensions_available, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME);
if (use_swapchain_colorspace) {
requireExtension(
extensions_available, extensions_enabled, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME);
}
}
/* External memory extensions. */

View File

@@ -127,7 +127,8 @@ class GHOST_ContextVK : public GHOST_Context {
#endif
int contextMajorVersion,
int contextMinorVersion,
const GHOST_GPUDevice &preferred_device);
const GHOST_GPUDevice &preferred_device,
const GHOST_WindowHDRInfo *hdr_info_ = nullptr);
/**
* Destructor.
@@ -230,6 +231,9 @@ class GHOST_ContextVK : public GHOST_Context {
const int context_minor_version_;
const GHOST_GPUDevice preferred_device_;
/* Optional HDR info updated by window. */
const GHOST_WindowHDRInfo *hdr_info_;
/* For display only. */
VkSurfaceKHR surface_;
VkSwapchainKHR swapchain_;

View File

@@ -1434,6 +1434,8 @@ GHOST_Event *GHOST_SystemWin32::processWindowSizeEvent(GHOST_WindowWin32 *window
system->dispatchEvents();
return nullptr;
}
window->updateHDRInfo();
return sizeEvent;
}
@@ -2159,6 +2161,9 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, uint msg, WPARAM wParam,
if (LOWORD(wParam) == WA_INACTIVE) {
window->lostMouseCapture();
}
else {
window->updateHDRInfo();
}
lResult = ::DefWindowProc(hwnd, msg, wParam, lParam);
break;
@@ -2176,6 +2181,7 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, uint msg, WPARAM wParam,
}
case WM_EXITSIZEMOVE: {
window->in_live_resize_ = 0;
window->updateHDRInfo();
break;
}
case WM_PAINT: {
@@ -2240,6 +2246,7 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, uint msg, WPARAM wParam,
}
else {
event = processWindowEvent(GHOST_kEventWindowMove, window);
window->updateHDRInfo();
}
break;
@@ -2275,6 +2282,7 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, uint msg, WPARAM wParam,
if (wt) {
wt->remapCoordinates();
}
window->updateHDRInfo();
break;
}
case WM_KILLFOCUS: {
@@ -2293,6 +2301,7 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, uint msg, WPARAM wParam,
{
window->ThemeRefresh();
}
window->updateHDRInfo();
break;
}
/* ======================

View File

@@ -218,6 +218,9 @@ GHOST_WindowWin32::GHOST_WindowWin32(GHOST_SystemWin32 *system,
/* Initialize Direct Manipulation. */
direct_manipulation_helper_ = GHOST_DirectManipulationHelper::create(h_wnd_, getDPIHint());
/* Initialize HDR info. */
updateHDRInfo();
}
void GHOST_WindowWin32::updateDirectManipulation()
@@ -619,7 +622,7 @@ GHOST_Context *GHOST_WindowWin32::newDrawingContext(GHOST_TDrawingContextType ty
#ifdef WITH_VULKAN_BACKEND
case GHOST_kDrawingContextTypeVulkan: {
GHOST_Context *context = new GHOST_ContextVK(
want_context_params_, h_wnd_, 1, 2, preferred_device_);
want_context_params_, h_wnd_, 1, 2, preferred_device_, &hdr_info_);
if (context->initializeDrawingContext()) {
return context;
}
@@ -1262,3 +1265,87 @@ void GHOST_WindowWin32::unregisterWindowAppUserModelProperties()
pstore->Release();
}
}
/* We call this from a few window event changes, but actually none of them immediately
* respond to SDR white level changes in the system. That requires using the WinRT API,
* which we don't do so far. */
void GHOST_WindowWin32::updateHDRInfo()
{
/* Get monitor from window. */
HMONITOR hmonitor = ::MonitorFromWindow(h_wnd_, MONITOR_DEFAULTTONEAREST);
if (!hmonitor) {
return;
}
MONITORINFOEXW monitor_info = {};
monitor_info.cbSize = sizeof(MONITORINFOEXW);
if (!::GetMonitorInfoW(hmonitor, &monitor_info)) {
return;
}
/* Get active display paths and modes. */
UINT32 path_count = 0;
UINT32 mode_count = 0;
if (::GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &path_count, &mode_count) !=
ERROR_SUCCESS)
{
return;
}
std::vector<DISPLAYCONFIG_PATH_INFO> paths(path_count);
std::vector<DISPLAYCONFIG_MODE_INFO> modes(mode_count);
if (::QueryDisplayConfig(
QDC_ONLY_ACTIVE_PATHS, &path_count, paths.data(), &mode_count, modes.data(), nullptr) !=
ERROR_SUCCESS)
{
return;
}
GHOST_WindowHDRInfo info = GHOST_WINDOW_HDR_INFO_NONE;
/* Find the display path matching the monitor. */
for (const DISPLAYCONFIG_PATH_INFO &path : paths) {
DISPLAYCONFIG_SOURCE_DEVICE_NAME device_name = {};
device_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
device_name.header.size = sizeof(device_name);
device_name.header.adapterId = path.sourceInfo.adapterId;
device_name.header.id = path.sourceInfo.id;
if (::DisplayConfigGetDeviceInfo(&device_name.header) != ERROR_SUCCESS) {
continue;
}
if (wcscmp(monitor_info.szDevice, device_name.viewGdiDeviceName) != 0) {
continue;
}
/* Query HDR status. */
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO color_info = {};
color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
color_info.header.size = sizeof(color_info);
color_info.header.adapterId = path.targetInfo.adapterId;
color_info.header.id = path.targetInfo.id;
if (::DisplayConfigGetDeviceInfo(&color_info.header) == ERROR_SUCCESS) {
info.hdr_enabled = color_info.advancedColorSupported && color_info.advancedColorEnabled;
}
if (info.hdr_enabled) {
/* Query SDR white level. */
DISPLAYCONFIG_SDR_WHITE_LEVEL 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.targetInfo.adapterId;
white_level.header.id = path.targetInfo.id;
if (::DisplayConfigGetDeviceInfo(&white_level.header) == ERROR_SUCCESS) {
if (white_level.SDRWhiteLevel > 0) {
/* Windows assumes 1.0 = 80 nits, so multipley by that to get the absolute
* value in nits if we need it in the future. */
info.sdr_white_level = static_cast<float>(white_level.SDRWhiteLevel) / 1000.0f;
}
}
}
hdr_info_ = info;
}
}

View File

@@ -326,6 +326,9 @@ class GHOST_WindowWin32 : public GHOST_Window {
GHOST_TTrackpadInfo getTrackpadInfo();
/* Update HDR info on initialization and window changes. */
void updateHDRInfo();
private:
/**
* \param type: The type of rendering context create.
@@ -412,6 +415,8 @@ 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_;