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

@@ -29,6 +29,7 @@ set(INC
../draw/intern
../draw/intern/shaders
metal/kernels
vulkan/shaders
shaders/infos
@@ -674,6 +675,10 @@ set(MSL_SRC
shaders/metal/mtl_shader_common.msl
)
set(VULKAN_BACKEND_GLSL_SRC
vulkan/shaders/vk_backbuffer_blit_comp.glsl
)
if(WITH_GTESTS)
if(WITH_GPU_BACKEND_TESTS)
list(APPEND GLSL_SRC ${GLSL_SRC_TEST})
@@ -689,6 +694,10 @@ if(WITH_METAL_BACKEND)
endforeach()
endif()
if(WITH_VULKAN_BACKEND)
list(APPEND GLSL_SRC ${VULKAN_BACKEND_GLSL_SRC})
endif()
set(GLSL_C)
foreach(GLSL_FILE ${GLSL_SRC})
glsl_to_c(${GLSL_FILE} GLSL_C)

View File

@@ -50,6 +50,10 @@
# include "gpu_shader_fullscreen_blit_info.hh"
#endif
#ifdef WITH_VULKAN_BACKEND
# include "vk_backbuffer_blit_info.hh"
#endif
/* Compositor. */
#include "compositor_alpha_crop_info.hh"
#include "compositor_bilateral_blur_info.hh"

View File

@@ -83,6 +83,14 @@ set(SRC_GLSL_COMP
# gpu_shader_index_2d_array_tris.glsl
)
set(SRC_VULKAN_GLSL_COMP
vk_backbuffer_blit_comp.glsl
)
if(WITH_VULKAN_BACKEND)
list(APPEND SRC_GLSL_COMP ${SRC_VULKAN_GLSL_COMP})
endif()
set(SRC_GLSL_LIB
common/gpu_shader_print_lib.glsl
)

View File

@@ -0,0 +1,25 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "vk_backbuffer_blit_info.hh"
COMPUTE_SHADER_CREATE_INFO(vk_backbuffer_blit)
void main()
{
ivec2 dst_texel = ivec2(gl_GlobalInvocationID.xy);
ivec2 src_size = ivec2(imageSize(src_img));
ivec2 src_texel = ivec2(dst_texel.x, src_size.y - dst_texel.y - 1);
vec4 color = imageLoad(src_img, ivec2(src_texel));
/*
* Convert from extended sRGB non-linear to linear.
*
* Preserves negative wide gamut values with sign/abs.
* Gamma 2.2 is used instead of the sRGB piecewise transfer function, because
* most SDR sRGB displays decode with gamma 2.2, and that's what we are trying
* to match.
*/
color.rgb = sign(color.rgb) * pow(abs(color.rgb), vec3(2.2f)) * sdr_scale;
imageStore(dst_img, ivec2(dst_texel), color);
}

View File

@@ -0,0 +1,15 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "gpu_shader_create_info.hh"
GPU_SHADER_CREATE_INFO(vk_backbuffer_blit)
LOCAL_GROUP_SIZE(16, 16)
IMAGE(0, SFLOAT_16_16_16_16, read, Float2D, src_img)
IMAGE(1, SFLOAT_16_16_16_16, write, Float2D, dst_img)
PUSH_CONSTANT(float, sdr_scale)
COMPUTE_SOURCE("vk_backbuffer_blit_comp.glsl")
ADDITIONAL_INFO(gpu_srgb_to_framebuffer_space)
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()

View File

@@ -89,7 +89,7 @@ void VKContext::sync_backbuffer(bool cycle_resource_pool)
swap_chain_data.extent.height,
1,
to_gpu_format(swap_chain_data.surface_format.format),
GPU_TEXTURE_USAGE_ATTACHMENT,
GPU_TEXTURE_USAGE_ATTACHMENT | GPU_TEXTURE_USAGE_SHADER_READ,
nullptr);
back_left->attachment_set(GPU_FB_COLOR_ATTACHMENT0,
@@ -102,7 +102,7 @@ void VKContext::sync_backbuffer(bool cycle_resource_pool)
swap_chain_format_ = swap_chain_data.surface_format;
GCaps.hdr_viewport_support = (swap_chain_format_.format == VK_FORMAT_R16G16B16A16_SFLOAT) &&
ELEM(swap_chain_format_.colorSpace,
VK_COLOR_SPACE_EXTENDED_SRGB_NONLINEAR_EXT,
VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT,
VK_COLOR_SPACE_SRGB_NONLINEAR_KHR);
}
}
@@ -372,6 +372,8 @@ void VKContext::swap_buffers_post_callback()
void VKContext::swap_buffers_pre_handler(const GHOST_VulkanSwapChainData &swap_chain_data)
{
const bool do_blit_to_swapchain = swap_chain_data.image != VK_NULL_HANDLE;
const bool use_shader = swap_chain_data.surface_format.colorSpace ==
VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT;
/* When swapchain is invalid/minimized we only flush the render graph to free GPU resources. */
if (!do_blit_to_swapchain) {
@@ -387,28 +389,43 @@ void VKContext::swap_buffers_pre_handler(const GHOST_VulkanSwapChainData &swap_c
device.resources.add_image(swap_chain_data.image, 1, "SwapchainImage");
GPU_debug_group_begin("BackBuffer.Blit");
if (use_shader) {
VKTexture swap_chain_texture("swap_chain_texture");
swap_chain_texture.init_swapchain(swap_chain_data.image,
to_gpu_format(swap_chain_data.surface_format.format));
Shader *shader = device.vk_backbuffer_blit_sh_get();
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "sdr_scale", swap_chain_data.sdr_scale);
VKStateManager &state_manager = state_manager_get();
state_manager.image_bind(color_attachment, 0);
state_manager.image_bind(&swap_chain_texture, 1);
int2 dispatch_size = math::divide_ceil(
int2(swap_chain_data.extent.width, swap_chain_data.extent.height), int2(16));
VKBackend::get().compute_dispatch(UNPACK2(dispatch_size), 1);
}
else {
render_graph::VKBlitImageNode::CreateInfo blit_image = {};
blit_image.src_image = color_attachment->vk_image_handle();
blit_image.dst_image = swap_chain_data.image;
blit_image.filter = VK_FILTER_LINEAR;
render_graph::VKBlitImageNode::CreateInfo blit_image = {};
blit_image.src_image = color_attachment->vk_image_handle();
blit_image.dst_image = swap_chain_data.image;
blit_image.filter = VK_FILTER_LINEAR;
VkImageBlit &region = blit_image.region;
region.srcOffsets[0] = {0, 0, 0};
region.srcOffsets[1] = {color_attachment->width_get(), color_attachment->height_get(), 1};
region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.srcSubresource.mipLevel = 0;
region.srcSubresource.baseArrayLayer = 0;
region.srcSubresource.layerCount = 1;
VkImageBlit &region = blit_image.region;
region.srcOffsets[0] = {0, 0, 0};
region.srcOffsets[1] = {color_attachment->width_get(), color_attachment->height_get(), 1};
region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.srcSubresource.mipLevel = 0;
region.srcSubresource.baseArrayLayer = 0;
region.srcSubresource.layerCount = 1;
region.dstOffsets[0] = {0, int32_t(swap_chain_data.extent.height), 0};
region.dstOffsets[1] = {int32_t(swap_chain_data.extent.width), 0, 1};
region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.dstSubresource.mipLevel = 0;
region.dstSubresource.baseArrayLayer = 0;
region.dstSubresource.layerCount = 1;
region.dstOffsets[0] = {0, int32_t(swap_chain_data.extent.height), 0};
region.dstOffsets[1] = {int32_t(swap_chain_data.extent.width), 0, 1};
region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.dstSubresource.mipLevel = 0;
region.dstSubresource.baseArrayLayer = 0;
region.dstSubresource.layerCount = 1;
render_graph.add_node(blit_image);
render_graph.add_node(blit_image);
}
render_graph::VKSynchronizationNode::CreateInfo synchronization = {};
synchronization.vk_image = swap_chain_data.image;

View File

@@ -75,6 +75,7 @@ void VKDevice::deinit()
dummy_buffer.free();
samplers_.free();
GPU_SHADER_FREE_SAFE(vk_backbuffer_blit_sh_);
{
while (!thread_data_.is_empty()) {

View File

@@ -237,6 +237,8 @@ class VKDevice : public NonCopyable {
std::string glsl_comp_patch_;
Vector<VKThreadData *> thread_data_;
Shader *vk_backbuffer_blit_sh_ = nullptr;
public:
render_graph::VKResourceStateTracker resources;
VKDiscardPool orphaned_data;
@@ -479,6 +481,14 @@ class VKDevice : public NonCopyable {
/** \} */
Shader *vk_backbuffer_blit_sh_get()
{
if (vk_backbuffer_blit_sh_ == nullptr) {
vk_backbuffer_blit_sh_ = GPU_shader_create_from_info_name("vk_backbuffer_blit");
}
return vk_backbuffer_blit_sh_;
}
private:
void init_physical_device_properties();
void init_physical_device_memory_properties();

View File

@@ -480,6 +480,15 @@ bool VKTexture::init_internal(gpu::Texture *src,
return true;
}
void VKTexture::init_swapchain(VkImage vk_image, TextureFormat format)
{
device_format_ = format_ = format;
format_flag_ = to_format_flag(format);
vk_image_ = vk_image;
type_ = GPU_TEXTURE_2D;
usage_set(GPU_TEXTURE_USAGE_ATTACHMENT | GPU_TEXTURE_USAGE_SHADER_WRITE);
}
bool VKTexture::is_texture_view() const
{
return source_texture_ != nullptr;

View File

@@ -30,6 +30,7 @@ ENUM_OPERATORS(VKImageViewFlags, VKImageViewFlags::NO_SWIZZLING)
class VKTexture : public Texture {
friend class VKDescriptorSetUpdator;
friend class VKContext;
/**
* Texture format how the texture is stored on the device.
@@ -148,6 +149,8 @@ class VKTexture : public Texture {
int mip_offset,
int layer_offset,
bool use_stencil) override;
/* Initialize VKTexture with a swapchain image. */
void init_swapchain(VkImage vk_image, TextureFormat gpu_format);
private:
/** Is this texture a view of another texture. */