diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index 2fcb7dc3bbb..78f283475cb 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -232,6 +232,13 @@ namespace platf::dxgi { virtual bool is_hdr() override; virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) override; + // Returns true when the capture source delivers FP16 storage with already + // gamma-encoded (sRGB) values, as happens with Windows 11 Auto Color + // Management (ACM) when an SDR application is displayed on an SDR monitor. + // In this case the linear shader path must NOT apply ApplySRGBCurve again + // (otherwise highlights are doubly encoded → white-out). + // Distinct from is_hdr() which checks for PQ HDR (G2084). + bool is_source_gamma_encoded_fp16(); const char *dxgi_format_to_string(DXGI_FORMAT format); const char *colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index e42bb8efa4b..eaae31c378f 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -766,6 +766,27 @@ namespace platf::dxgi { return desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; } + bool display_base_t::is_source_gamma_encoded_fp16() { + dxgi::output6_t output6 {}; + + auto status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); + if (FAILED(status)) { + return false; + } + + DXGI_OUTPUT_DESC1 desc1; + output6->GetDesc1(&desc1); + + // Windows 11 Auto Color Management (ACM) composes SDR desktops into an + // FP16 framebuffer but the pixel values remain gamma-encoded (G22). + // The "linear" shader path assumes linear scRGB FP16 (G10) and applies + // ApplySRGBCurve, which double-encodes the already-sRGB G22 values, + // crushing highlights to white. Use this method to route G22+FP16 input + // through the non-linear shader path (saturate only, no curve). + return desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709 + || desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020; + } + bool display_base_t::get_hdr_metadata(SS_HDR_METADATA &metadata) { dxgi::output6_t output6 {}; diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index d659c4bbed7..b169b69d1ab 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -507,15 +507,28 @@ namespace platf::dxgi { // Semi-planar 8-bit YUV 4:2:0 create_vertex_shader_helper(convert_yuv420_planar_y_vs_hlsl, convert_Y_or_YUV_vs); create_pixel_shader_helper(convert_yuv420_planar_y_ps_hlsl, convert_Y_or_YUV_ps); - create_pixel_shader_helper(convert_yuv420_planar_y_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); + if (display->is_source_gamma_encoded_fp16()) { + // ACM-SDR FP16: input is already sRGB-encoded; reuse non-linear shader to avoid double sRGB encoding (would cause highlight white-out) + create_pixel_shader_helper(convert_yuv420_planar_y_ps_hlsl, convert_Y_or_YUV_fp16_ps); + } else { + create_pixel_shader_helper(convert_yuv420_planar_y_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); + } if (downscaling) { create_vertex_shader_helper(convert_yuv420_packed_uv_type0s_vs_hlsl, convert_UV_vs); create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hlsl, convert_UV_ps); - create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear_hlsl, convert_UV_fp16_ps); + if (display->is_source_gamma_encoded_fp16()) { + create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hlsl, convert_UV_fp16_ps); + } else { + create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear_hlsl, convert_UV_fp16_ps); + } } else { create_vertex_shader_helper(convert_yuv420_packed_uv_type0_vs_hlsl, convert_UV_vs); create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hlsl, convert_UV_ps); - create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear_hlsl, convert_UV_fp16_ps); + if (display->is_source_gamma_encoded_fp16()) { + create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hlsl, convert_UV_fp16_ps); + } else { + create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear_hlsl, convert_UV_fp16_ps); + } } break; @@ -525,6 +538,9 @@ namespace platf::dxgi { create_pixel_shader_helper(convert_yuv420_planar_y_ps_hlsl, convert_Y_or_YUV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps); + } else if (display->is_source_gamma_encoded_fp16()) { + // ACM-SDR FP16: input is already sRGB-encoded; reuse non-linear shader to avoid double sRGB encoding + create_pixel_shader_helper(convert_yuv420_planar_y_ps_hlsl, convert_Y_or_YUV_fp16_ps); } else { create_pixel_shader_helper(convert_yuv420_planar_y_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); } @@ -533,6 +549,8 @@ namespace platf::dxgi { create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hlsl, convert_UV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer_hlsl, convert_UV_fp16_ps); + } else if (display->is_source_gamma_encoded_fp16()) { + create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hlsl, convert_UV_fp16_ps); } else { create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear_hlsl, convert_UV_fp16_ps); } @@ -541,6 +559,8 @@ namespace platf::dxgi { create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hlsl, convert_UV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl, convert_UV_fp16_ps); + } else if (display->is_source_gamma_encoded_fp16()) { + create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hlsl, convert_UV_fp16_ps); } else { create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear_hlsl, convert_UV_fp16_ps); } @@ -553,6 +573,9 @@ namespace platf::dxgi { create_pixel_shader_helper(convert_yuv444_planar_ps_hlsl, convert_Y_or_YUV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv444_planar_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps); + } else if (display->is_source_gamma_encoded_fp16()) { + // ACM-SDR FP16: input is already sRGB-encoded; reuse non-linear shader to avoid double sRGB encoding + create_pixel_shader_helper(convert_yuv444_planar_ps_hlsl, convert_Y_or_YUV_fp16_ps); } else { create_pixel_shader_helper(convert_yuv444_planar_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); } @@ -562,7 +585,12 @@ namespace platf::dxgi { // Packed 8-bit YUV 4:4:4 create_vertex_shader_helper(convert_yuv444_packed_vs_hlsl, convert_Y_or_YUV_vs); create_pixel_shader_helper(convert_yuv444_packed_ayuv_ps_hlsl, convert_Y_or_YUV_ps); - create_pixel_shader_helper(convert_yuv444_packed_ayuv_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); + if (display->is_source_gamma_encoded_fp16()) { + // ACM-SDR FP16: input is already sRGB-encoded; reuse non-linear shader to avoid double sRGB encoding + create_pixel_shader_helper(convert_yuv444_packed_ayuv_ps_hlsl, convert_Y_or_YUV_fp16_ps); + } else { + create_pixel_shader_helper(convert_yuv444_packed_ayuv_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); + } break; case DXGI_FORMAT_Y410: @@ -571,6 +599,9 @@ namespace platf::dxgi { create_pixel_shader_helper(convert_yuv444_packed_y410_ps_hlsl, convert_Y_or_YUV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv444_packed_y410_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps); + } else if (display->is_source_gamma_encoded_fp16()) { + // ACM-SDR FP16: input is already sRGB-encoded; reuse non-linear shader to avoid double sRGB encoding + create_pixel_shader_helper(convert_yuv444_packed_y410_ps_hlsl, convert_Y_or_YUV_fp16_ps); } else { create_pixel_shader_helper(convert_yuv444_packed_y410_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); }