fix(windows/capture): match the HDR-composited cursor to the target's SDR white level
sRGB→linear alone lands cursor-white at 80 nits (scRGB 1.0) while DWM composes the surrounding SDR desktop at the user's SDR-brightness setting (~200+ nits by default) — the cursor read visibly dark on HDR. Scale by DISPLAYCONFIG_SDR_WHITE_LEVEL (new win_display::sdr_white_level_scale, queried on blend-scratch rebuilds — the CCD query stays off the per-frame path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -429,6 +429,10 @@ pub struct IddPushCapturer {
|
|||||||
last_blend_key: Option<(u64, i32, i32, bool)>,
|
last_blend_key: Option<(u64, i32, i32, bool)>,
|
||||||
/// The ring slot of the last FRESH publish — the regen source.
|
/// The ring slot of the last FRESH publish — the regen source.
|
||||||
last_slot: Option<usize>,
|
last_slot: Option<usize>,
|
||||||
|
/// The target's SDR-white scale (vs 80 nits) for HDR cursor compositing — refreshed on
|
||||||
|
/// each blend-scratch rebuild (first use + ring geometry changes). 2.5 ≈ the Windows
|
||||||
|
/// SDR-brightness default; without it the composited cursor renders visibly dark on HDR.
|
||||||
|
sdr_white_scale: f32,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
slots: Vec<HostSlot>,
|
slots: Vec<HostSlot>,
|
||||||
@@ -1090,6 +1094,7 @@ impl IddPushCapturer {
|
|||||||
blend_scratch: None,
|
blend_scratch: None,
|
||||||
last_blend_key: None,
|
last_blend_key: None,
|
||||||
last_slot: None,
|
last_slot: None,
|
||||||
|
sdr_white_scale: 1.0,
|
||||||
// Held from BEFORE the first-frame gate (the display must not idle off while we
|
// Held from BEFORE the first-frame gate (the display must not idle off while we
|
||||||
// wait for the first compose) until the capturer drops with the session.
|
// wait for the first compose) until the capturer drops with the session.
|
||||||
_display_wake: pf_frame::session_tuning::DisplayWakeRequest::new(),
|
_display_wake: pf_frame::session_tuning::DisplayWakeRequest::new(),
|
||||||
@@ -1819,9 +1824,14 @@ impl IddPushCapturer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(pass) = self.cursor_blend.as_mut() {
|
if let Some(pass) = self.cursor_blend.as_mut() {
|
||||||
// FP16 ring = scRGB linear composition (HDR): linearize the sRGB shape.
|
// FP16 ring = scRGB linear composition (HDR): linearize the sRGB shape and
|
||||||
let is_linear = self.display_hdr;
|
// scale it to the target's SDR white so it matches the desktop around it.
|
||||||
if let Err(e) = pass.blend(&self.device, &self.context, &tex, &ov, is_linear) {
|
let scale = if self.display_hdr {
|
||||||
|
self.sdr_white_scale
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
if let Err(e) = pass.blend(&self.device, &self.context, &tex, &ov, scale) {
|
||||||
if !self.cursor_blend_failed {
|
if !self.cursor_blend_failed {
|
||||||
self.cursor_blend_failed = true;
|
self.cursor_blend_failed = true;
|
||||||
tracing::warn!("cursor blend draw failed — pointer-less frames: {e:#}");
|
tracing::warn!("cursor blend draw failed — pointer-less frames: {e:#}");
|
||||||
|
|||||||
@@ -26,16 +26,18 @@ use windows::Win32::Graphics::Direct3D11::{
|
|||||||
};
|
};
|
||||||
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R8G8B8A8_UNORM;
|
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||||
|
|
||||||
/// Straight-alpha sample of the cursor bitmap; `to_linear` ≠ 0 linearizes sRGB→scRGB for an
|
/// Straight-alpha sample of the cursor bitmap. `linear_scale` = 0 passes sRGB through (SDR
|
||||||
/// FP16 (HDR-composed) frame so the cursor doesn't glow at 8× SDR white.
|
/// ring); non-zero linearizes sRGB→scRGB AND multiplies by the target's SDR-white scale
|
||||||
|
/// (`sdr_white_level_scale` — 1.0 would put cursor-white at 80 nits, visibly DARKER than the
|
||||||
|
/// surrounding SDR desktop content DWM composes at the user's SDR-brightness setting).
|
||||||
const CURSOR_PS: &str = r"
|
const CURSOR_PS: &str = r"
|
||||||
Texture2D<float4> tx : register(t0);
|
Texture2D<float4> tx : register(t0);
|
||||||
SamplerState sm : register(s0);
|
SamplerState sm : register(s0);
|
||||||
cbuffer C : register(b0) { float to_linear; float3 pad; };
|
cbuffer C : register(b0) { float linear_scale; float3 pad; };
|
||||||
float4 main(float4 pos : SV_POSITION, float2 uv : TEXCOORD0) : SV_Target {
|
float4 main(float4 pos : SV_POSITION, float2 uv : TEXCOORD0) : SV_Target {
|
||||||
float4 c = tx.Sample(sm, uv);
|
float4 c = tx.Sample(sm, uv);
|
||||||
if (to_linear != 0.0) {
|
if (linear_scale != 0.0) {
|
||||||
c.rgb = pow(abs(c.rgb), 2.2);
|
c.rgb = pow(abs(c.rgb), 2.2) * linear_scale;
|
||||||
}
|
}
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
@@ -48,7 +50,7 @@ pub(super) struct CursorBlendPass {
|
|||||||
sampler: ID3D11SamplerState,
|
sampler: ID3D11SamplerState,
|
||||||
blend: ID3D11BlendState,
|
blend: ID3D11BlendState,
|
||||||
cbuf: ID3D11Buffer,
|
cbuf: ID3D11Buffer,
|
||||||
cbuf_is_linear: Option<bool>,
|
cbuf_scale: Option<f32>,
|
||||||
/// The uploaded shape (serial-keyed): SRV + dims in host pixels.
|
/// The uploaded shape (serial-keyed): SRV + dims in host pixels.
|
||||||
shape: Option<(u64, ID3D11ShaderResourceView, u32, u32)>,
|
shape: Option<(u64, ID3D11ShaderResourceView, u32, u32)>,
|
||||||
}
|
}
|
||||||
@@ -103,7 +105,7 @@ impl CursorBlendPass {
|
|||||||
sampler: sampler.context("cursor blend sampler")?,
|
sampler: sampler.context("cursor blend sampler")?,
|
||||||
blend: blend.context("cursor blend state")?,
|
blend: blend.context("cursor blend state")?,
|
||||||
cbuf: cbuf.context("cursor blend cbuf")?,
|
cbuf: cbuf.context("cursor blend cbuf")?,
|
||||||
cbuf_is_linear: None,
|
cbuf_scale: None,
|
||||||
shape: None,
|
shape: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -153,21 +155,22 @@ impl CursorBlendPass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Alpha-blend `ov` onto `dst` (frame-sized, RENDER_TARGET-capable — the blend scratch).
|
/// Alpha-blend `ov` onto `dst` (frame-sized, RENDER_TARGET-capable — the blend scratch).
|
||||||
/// `is_linear` = the frame is FP16 scRGB (HDR composition). The quad is placed purely via
|
/// `linear_scale`: 0 = SDR passthrough; non-zero = the frame is FP16 scRGB (HDR
|
||||||
/// the viewport (the fullscreen-triangle VS fills whatever viewport is set), clipped by the
|
/// composition) — linearize and scale to the target's SDR white. The quad is placed purely
|
||||||
/// target automatically.
|
/// via the viewport (the fullscreen-triangle VS fills whatever viewport is set), clipped by
|
||||||
|
/// the target automatically.
|
||||||
pub(super) unsafe fn blend(
|
pub(super) unsafe fn blend(
|
||||||
&mut self,
|
&mut self,
|
||||||
device: &ID3D11Device,
|
device: &ID3D11Device,
|
||||||
ctx: &ID3D11DeviceContext,
|
ctx: &ID3D11DeviceContext,
|
||||||
dst: &ID3D11Texture2D,
|
dst: &ID3D11Texture2D,
|
||||||
ov: &pf_frame::CursorOverlay,
|
ov: &pf_frame::CursorOverlay,
|
||||||
is_linear: bool,
|
linear_scale: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.ensure_shape(device, ov)?;
|
self.ensure_shape(device, ov)?;
|
||||||
let (_, srv, w, h) = self.shape.as_ref().expect("shape just ensured");
|
let (_, srv, w, h) = self.shape.as_ref().expect("shape just ensured");
|
||||||
if self.cbuf_is_linear != Some(is_linear) {
|
if self.cbuf_scale != Some(linear_scale) {
|
||||||
let cb: [f32; 4] = [if is_linear { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0];
|
let cb: [f32; 4] = [linear_scale, 0.0, 0.0, 0.0];
|
||||||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||||||
if ctx
|
if ctx
|
||||||
.Map(&self.cbuf, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut mapped))
|
.Map(&self.cbuf, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut mapped))
|
||||||
@@ -176,7 +179,7 @@ impl CursorBlendPass {
|
|||||||
std::ptr::copy_nonoverlapping(cb.as_ptr(), mapped.pData as *mut f32, cb.len());
|
std::ptr::copy_nonoverlapping(cb.as_ptr(), mapped.pData as *mut f32, cb.len());
|
||||||
ctx.Unmap(&self.cbuf, 0);
|
ctx.Unmap(&self.cbuf, 0);
|
||||||
}
|
}
|
||||||
self.cbuf_is_linear = Some(is_linear);
|
self.cbuf_scale = Some(linear_scale);
|
||||||
}
|
}
|
||||||
let mut rtv: Option<ID3D11RenderTargetView> = None;
|
let mut rtv: Option<ID3D11RenderTargetView> = None;
|
||||||
device
|
device
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ use windows::core::PCWSTR;
|
|||||||
use windows::Win32::Devices::Display::{
|
use windows::Win32::Devices::Display::{
|
||||||
DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes,
|
DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes,
|
||||||
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO,
|
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO,
|
||||||
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME,
|
DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL, DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
|
||||||
DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO,
|
DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME, DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE,
|
||||||
DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE,
|
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_MODE_INFO,
|
||||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPONENT_VIDEO,
|
DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPONENT_VIDEO,
|
||||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPOSITE_VIDEO,
|
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPOSITE_VIDEO,
|
||||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED,
|
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED,
|
||||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DVI,
|
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DVI,
|
||||||
@@ -35,10 +35,11 @@ use windows::Win32::Devices::Display::{
|
|||||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDI, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDTVDONGLE,
|
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDI, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDTVDONGLE,
|
||||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SVIDEO, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED,
|
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SVIDEO, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED,
|
||||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EXTERNAL, DISPLAYCONFIG_PATH_INFO,
|
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EXTERNAL, DISPLAYCONFIG_PATH_INFO,
|
||||||
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME,
|
DISPLAYCONFIG_SDR_WHITE_LEVEL, DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE,
|
||||||
DISPLAYCONFIG_TARGET_DEVICE_NAME, DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY, QDC_ALL_PATHS,
|
DISPLAYCONFIG_SOURCE_DEVICE_NAME, DISPLAYCONFIG_TARGET_DEVICE_NAME,
|
||||||
QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY, SDC_FORCE_MODE_ENUMERATION,
|
DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY, QDC_ALL_PATHS, QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES,
|
||||||
SDC_SAVE_TO_DATABASE, SDC_TOPOLOGY_EXTEND, SDC_USE_SUPPLIED_DISPLAY_CONFIG,
|
SDC_APPLY, SDC_FORCE_MODE_ENUMERATION, SDC_SAVE_TO_DATABASE, SDC_TOPOLOGY_EXTEND,
|
||||||
|
SDC_USE_SUPPLIED_DISPLAY_CONFIG,
|
||||||
};
|
};
|
||||||
use windows::Win32::Foundation::POINTL;
|
use windows::Win32::Foundation::POINTL;
|
||||||
use windows::Win32::Graphics::Gdi::{
|
use windows::Win32::Graphics::Gdi::{
|
||||||
@@ -481,6 +482,52 @@ pub unsafe fn advanced_color_enabled(target_id: u32) -> Option<bool> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The target's SDR white level as a SCALE relative to 80 nits (`1.0` = 80 nits): where DWM
|
||||||
|
/// places SDR-white when composing SDR content onto this HDR desktop. An SDR-authored overlay
|
||||||
|
/// (the composited cursor) must be multiplied by this in scRGB space or it renders visibly
|
||||||
|
/// darker than the surrounding SDR desktop content (the Windows "SDR content brightness"
|
||||||
|
/// slider default alone is ~2.5x). `None` = query failed / target not active (callers keep
|
||||||
|
/// their last value or 1.0).
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// Runs the read-only CCD query FFI over owned locals (same shape as [`advanced_color_enabled`]).
|
||||||
|
pub unsafe fn sdr_white_level_scale(target_id: u32) -> Option<f32> {
|
||||||
|
let mut np = 0u32;
|
||||||
|
let mut nm = 0u32;
|
||||||
|
if GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut np, &mut nm).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut paths = vec![DISPLAYCONFIG_PATH_INFO::default(); np as usize];
|
||||||
|
let mut modes = vec![DISPLAYCONFIG_MODE_INFO::default(); nm as usize];
|
||||||
|
if QueryDisplayConfig(
|
||||||
|
QDC_ONLY_ACTIVE_PATHS,
|
||||||
|
&mut np,
|
||||||
|
paths.as_mut_ptr(),
|
||||||
|
&mut nm,
|
||||||
|
modes.as_mut_ptr(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
for p in paths.iter().take(np as usize) {
|
||||||
|
if p.targetInfo.id == target_id {
|
||||||
|
let mut info = DISPLAYCONFIG_SDR_WHITE_LEVEL::default();
|
||||||
|
info.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
|
||||||
|
info.header.size = size_of::<DISPLAYCONFIG_SDR_WHITE_LEVEL>() as u32;
|
||||||
|
info.header.adapterId = p.targetInfo.adapterId;
|
||||||
|
info.header.id = p.targetInfo.id;
|
||||||
|
if DisplayConfigGetDeviceInfo(&mut info.header) == 0 && info.SDRWhiteLevel > 0 {
|
||||||
|
// Contract: SDRWhiteLevel/1000 * 80 = nits, i.e. the /1000 IS the 80-nit scale.
|
||||||
|
return Some(info.SDRWhiteLevel as f32 / 1000.0);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Force the freshly-added virtual monitor to the client's exact `WxH@Hz`. The ADD IOCTL only
|
/// Force the freshly-added virtual monitor to the client's exact `WxH@Hz`. The ADD IOCTL only
|
||||||
/// ADVERTISES the mode; Windows otherwise activates an IDD target at a 1280x720 default, so the
|
/// ADVERTISES the mode; Windows otherwise activates an IDD target at a 1280x720 default, so the
|
||||||
/// ACTIVE mode (what DXGI Desktop Duplication captures) must be set explicitly. CDS_TEST first so a
|
/// ACTIVE mode (what DXGI Desktop Duplication captures) must be set explicitly. CDS_TEST first so a
|
||||||
|
|||||||
Reference in New Issue
Block a user