perf(pf-capture): take three per-frame costs off the Windows HDR hot path (Phase 4)

**4.1 (W10) — the HDR convert allocated two views and mapped a constant buffer per frame, inside
the ring slot's keyed-mutex hold.** So the driver's publisher sat blocked on that slot while the
host created `CreateRenderTargetView`s and Mapped/Unmapped a 16-byte buffer whose contents never
change. Both are lifetime-of-mode facts, not per-frame ones:
  - the two P010 plane RTVs now belong to the out-ring SLOT, built once in `ensure_out_ring`
    (`out_ring` becomes `Vec<OutSlot>`). A driver that rejects planar RTVs — the one hard
    requirement of this path — therefore fails at ring build, where such a failure belongs, instead
    of on the first frame.
  - `inv_src` becomes an IMMUTABLE constant buffer filled in `HdrP010Converter::new(device, w, h)`.
    The converter is already dropped by `recreate_ring` on every mode change, so it cannot go
    stale. This also deletes the `Map`'s silently-ignored failure: it could leave the chroma pass
    sampling at a garbage texel size with no error anywhere.

**4.2 (W15) — `sdr_white_level_scale` ran inside the keyed-mutex hold too.** It is a CCD query that
contends the display-config lock — precisely the contention `DescriptorPoller`'s doc says must stay
off the frame path — and `prepare_blend_scratch` called it while holding the slot. Moved to
`refresh_sdr_white_scale`, called at open and after each ring recreate (where the scratch is
invalidated anyway). "Only on scratch rebuilds" is not the same as harmless when the thing being
blocked is the producer.

**4.3 (W11) — `VideoProcessorSetStreamAutoProcessingMode(vp, 0, false)`.** The comment claimed "no
interpolation/auto-processing" while only setting the frame format; auto-processing's documented
default is ENABLED, so vendor denoise/edge-enhancement was free to run inside every SDR
`VideoProcessorBlt` — altering the pixels we encode and spending video-engine time on the
desktop-capture hot path. Now actually disabled, with each call's purpose stated separately.

**4.4 (the Linux CPU-path map cache) is deliberately NOT done.** The plan gates it on measurement
("measure first, and only if the CPU path still matters") and this box cannot measure a live capture
session; it also needs a frame-buffer return path through `FramePayload::Cpu`, which is a larger
change than the rest of this phase. Left for Phase 7's measured pass.

Compile-verified for windows-msvc locally; pf-capture 20/20 on Linux; workspace clippy
--all-targets clean on both targets. The GPU-capture check and the measured `cap_us` delta are owed
to Phase 7.3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 11:11:46 +02:00
co-authored by Claude Opus 5
parent bc45287dc6
commit 530909a154
2 changed files with 138 additions and 80 deletions
+60 -43
View File
@@ -28,12 +28,11 @@ use windows::Win32::Graphics::Direct3D11::{
ID3D11RenderTargetView, ID3D11SamplerState, ID3D11ShaderResourceView, ID3D11Texture2D, ID3D11RenderTargetView, ID3D11SamplerState, ID3D11ShaderResourceView, ID3D11Texture2D,
ID3D11VertexShader, D3D11_BIND_CONSTANT_BUFFER, D3D11_BIND_RENDER_TARGET, ID3D11VertexShader, D3D11_BIND_CONSTANT_BUFFER, D3D11_BIND_RENDER_TARGET,
D3D11_BIND_SHADER_RESOURCE, D3D11_BUFFER_DESC, D3D11_COMPARISON_NEVER, D3D11_CPU_ACCESS_READ, D3D11_BIND_SHADER_RESOURCE, D3D11_BUFFER_DESC, D3D11_COMPARISON_NEVER, D3D11_CPU_ACCESS_READ,
D3D11_CPU_ACCESS_WRITE, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_FILTER_MIN_MAG_MIP_POINT, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_FILTER_MIN_MAG_MIP_POINT, D3D11_MAPPED_SUBRESOURCE,
D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_READ, D3D11_MAP_WRITE_DISCARD, D3D11_MAP_READ, D3D11_RENDER_TARGET_VIEW_DESC, D3D11_RENDER_TARGET_VIEW_DESC_0,
D3D11_RENDER_TARGET_VIEW_DESC, D3D11_RENDER_TARGET_VIEW_DESC_0, D3D11_RTV_DIMENSION_TEXTURE2D, D3D11_RTV_DIMENSION_TEXTURE2D, D3D11_SAMPLER_DESC, D3D11_SDK_VERSION, D3D11_SUBRESOURCE_DATA,
D3D11_SAMPLER_DESC, D3D11_SDK_VERSION, D3D11_SUBRESOURCE_DATA, D3D11_TEX2D_RTV, D3D11_TEX2D_RTV, D3D11_TEXTURE2D_DESC, D3D11_TEXTURE_ADDRESS_CLAMP, D3D11_USAGE_DEFAULT,
D3D11_TEXTURE2D_DESC, D3D11_TEXTURE_ADDRESS_CLAMP, D3D11_USAGE_DEFAULT, D3D11_USAGE_DYNAMIC, D3D11_USAGE_IMMUTABLE, D3D11_USAGE_STAGING, D3D11_VIEWPORT,
D3D11_USAGE_STAGING, D3D11_VIEWPORT,
}; };
use windows::Win32::Graphics::Dxgi::Common::{ use windows::Win32::Graphics::Dxgi::Common::{
DXGI_FORMAT, DXGI_FORMAT_P010, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_R16G16_UNORM, DXGI_FORMAT, DXGI_FORMAT_P010, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_R16G16_UNORM,
@@ -371,12 +370,19 @@ pub(crate) struct HdrP010Converter {
ps_y: ID3D11PixelShader, ps_y: ID3D11PixelShader,
ps_uv: ID3D11PixelShader, ps_uv: ID3D11PixelShader,
sampler: ID3D11SamplerState, sampler: ID3D11SamplerState,
/// Constant buffer for the chroma pass (inv_src texel size). 16 bytes. /// Constant buffer for the chroma pass: `inv_src` = (1/srcW, 1/srcH), 16 bytes. IMMUTABLE and
/// filled at construction — it depends only on the source size, and the converter is already
/// rebuilt whenever the mode changes. It used to be a DYNAMIC buffer that `convert` Mapped,
/// wrote and Unmapped on EVERY HDR frame, inside the ring slot's keyed-mutex hold, with the
/// `Map`'s failure silently ignored (leaving the pass reading a stale/garbage texel size).
cbuf: ID3D11Buffer, cbuf: ID3D11Buffer,
} }
impl HdrP010Converter { impl HdrP010Converter {
pub(crate) unsafe fn new(device: &ID3D11Device) -> Result<Self> { /// `w`/`h` are the SOURCE dimensions this converter's chroma pass will sample, baked into the
/// immutable constant buffer. Rebuild the converter if they change (the IDD capturer's
/// `recreate_ring` already drops it).
pub(crate) unsafe fn new(device: &ID3D11Device, w: u32, h: u32) -> Result<Self> {
// SAFETY: every call is a `?`-checked D3D11 method on the live `device` borrow, over // SAFETY: every call is a `?`-checked D3D11 method on the live `device` borrow, over
// fully-initialized stack descriptors and live `Option` out-params; `compile_shader` receives // fully-initialized stack descriptors and live `Option` out-params; `compile_shader` receives
// `s!()` literals (its contract). Each created COM interface owns its own reference, and no // `s!()` literals (its contract). Each created COM interface owns its own reference, and no
@@ -409,15 +415,23 @@ impl HdrP010Converter {
}; };
let mut sampler = None; let mut sampler = None;
device.CreateSamplerState(&sd, Some(&mut sampler))?; device.CreateSamplerState(&sd, Some(&mut sampler))?;
// `inv_src` never changes for a given source size — build the buffer IMMUTABLE with its
// contents, so the chroma pass needs no per-frame Map (and has no unchecked failure).
let inv_src: [f32; 4] = [1.0 / w.max(1) as f32, 1.0 / h.max(1) as f32, 0.0, 0.0];
let cbd = D3D11_BUFFER_DESC { let cbd = D3D11_BUFFER_DESC {
ByteWidth: 16, // float2 inv_src + float2 pad ByteWidth: 16, // float2 inv_src + float2 pad
Usage: D3D11_USAGE_DYNAMIC, Usage: D3D11_USAGE_IMMUTABLE,
BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, CPUAccessFlags: 0,
..Default::default() ..Default::default()
}; };
let init = D3D11_SUBRESOURCE_DATA {
pSysMem: inv_src.as_ptr().cast(),
SysMemPitch: 0,
SysMemSlicePitch: 0,
};
let mut cbuf = None; let mut cbuf = None;
device.CreateBuffer(&cbd, None, Some(&mut cbuf))?; device.CreateBuffer(&cbd, Some(&init), Some(&mut cbuf))?;
Ok(Self { Ok(Self {
vs: vs.context("p010 vs")?, vs: vs.context("p010 vs")?,
ps_y: ps_y.context("p010 y ps")?, ps_y: ps_y.context("p010 y ps")?,
@@ -431,7 +445,11 @@ impl HdrP010Converter {
/// Create a per-plane RTV of the P010 texture `dst` with the given single-plane `format` /// Create a per-plane RTV of the P010 texture `dst` with the given single-plane `format`
/// (`R16_UNORM` for plane 0 luma, `R16G16_UNORM` for plane 1 chroma). The plane is selected by the /// (`R16_UNORM` for plane 0 luma, `R16G16_UNORM` for plane 1 chroma). The plane is selected by the
/// view format (planar-RTV semantics); MipSlice 0. /// view format (planar-RTV semantics); MipSlice 0.
unsafe fn plane_rtv( ///
/// Called ONCE PER OUT-RING SLOT by the owner of the P010 textures, not per frame — see
/// [`Self::convert`]. Fails when the driver rejects a planar RTV, which is the one hard
/// requirement of this whole path (a D3D11.3+ runtime plus driver support).
pub(crate) unsafe fn plane_rtv(
device: &ID3D11Device, device: &ID3D11Device,
dst: &ID3D11Texture2D, dst: &ID3D11Texture2D,
format: DXGI_FORMAT, format: DXGI_FORMAT,
@@ -461,40 +479,29 @@ impl HdrP010Converter {
} }
} }
/// Convert `src_srv` (FP16 scRGB, WxH) into `dst` (a `DXGI_FORMAT_P010` texture with /// Convert `src_srv` (FP16 scRGB, WxH) into a `DXGI_FORMAT_P010` texture through the two plane
/// `BIND_RENDER_TARGET`). Two opaque passes: full-res luma → plane 0, half-res chroma → plane 1. /// RTVs the CALLER built for it ([`Self::plane_rtv`]): full-res luma → `y_rtv` (plane 0),
/// `w`/`h` are the full luma dimensions (must be even). Returns `Err` if a plane RTV can't be /// half-res chroma → `uv_rtv` (plane 1). `w`/`h` are the full luma dimensions (must be even) and
/// created (driver) so the caller can fall back to the R10 path. /// must match the `w`/`h` this converter was constructed with.
///
/// Takes the views rather than creating them: two `CreateRenderTargetView`s per frame — plus a
/// Map/Unmap of a never-changing 16-byte constant buffer — was pure per-frame cost inside the
/// ring slot's keyed-mutex hold, i.e. time the DRIVER spent blocked on that slot. Both are
/// lifetime-of-mode facts: the views belong to the out-ring slot (built in `ensure_out_ring`),
/// the buffer to this converter, which is already rebuilt on every mode change.
pub(crate) unsafe fn convert( pub(crate) unsafe fn convert(
&self, &self,
device: &ID3D11Device,
ctx: &ID3D11DeviceContext, ctx: &ID3D11DeviceContext,
src_srv: &ID3D11ShaderResourceView, src_srv: &ID3D11ShaderResourceView,
dst: &ID3D11Texture2D, y_rtv: &ID3D11RenderTargetView,
uv_rtv: &ID3D11RenderTargetView,
w: u32, w: u32,
h: u32, h: u32,
) -> Result<()> { ) -> Result<()> {
// SAFETY: all D3D11 work runs on the caller's live `device`/`ctx` borrows (`ctx` is the // SAFETY: all D3D11 work runs on the caller's live `ctx` borrow (the owning capture thread's
// owning capture thread's immediate context), and `plane_rtv` receives that same device. The // immediate context) over borrowed slices of fully-initialized locals (the viewports) and
// `copy_nonoverlapping` writes `cb.len()` `f32`s into `mapped.pData` — the pointer the // clones of the caller's live SRV/RTVs. No raw pointers and no mapping on this path.
// immediately preceding `Map` of `self.cbuf` returned, a 16-byte (4×`f32`) DYNAMIC constant
// buffer — inside the `is_ok()` arm only, before the paired `Unmap`. Every `OMSet*`/`PSSet*`/
// `RSSetViewports`/`Draw` takes borrowed slices of live locals or clones of live interfaces.
unsafe { unsafe {
let y_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16_UNORM)?;
let uv_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16G16_UNORM)?;
// Update the chroma constant buffer (inverse source texel size).
let cb: [f32; 4] = [1.0 / w as f32, 1.0 / h as f32, 0.0, 0.0];
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
if ctx
.Map(&self.cbuf, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut mapped))
.is_ok()
{
std::ptr::copy_nonoverlapping(cb.as_ptr(), mapped.pData as *mut f32, cb.len());
ctx.Unmap(&self.cbuf, 0);
}
// Shared pipeline state. // Shared pipeline state.
ctx.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite ctx.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite
ctx.VSSetShader(&self.vs, None); ctx.VSSetShader(&self.vs, None);
@@ -937,8 +944,10 @@ pub fn hdr_p010_convert_bars_on_luid(
.context("CreateTexture2D(P010 bars dst)")?; .context("CreateTexture2D(P010 bars dst)")?;
let p010 = p010.context("null p010 tex")?; let p010 = p010.context("null p010 tex")?;
let conv = HdrP010Converter::new(&device)?; let conv = HdrP010Converter::new(&device, w, h)?;
conv.convert(&device, &context, &src_srv, &p010, w, h)?; let y_rtv = HdrP010Converter::plane_rtv(&device, &p010, DXGI_FORMAT_R16_UNORM)?;
let uv_rtv = HdrP010Converter::plane_rtv(&device, &p010, DXGI_FORMAT_R16G16_UNORM)?;
conv.convert(&context, &src_srv, &y_rtv, &uv_rtv, w, h)?;
Ok((device, p010)) Ok((device, p010))
} }
} }
@@ -1130,8 +1139,10 @@ pub fn hdr_p010_selftest_at(w: u32, h: u32, vendor: Option<u32>) -> Result<()> {
.context("CreateTexture2D(P010 dst)")?; .context("CreateTexture2D(P010 dst)")?;
let p010 = p010.context("null p010 tex")?; let p010 = p010.context("null p010 tex")?;
let conv = HdrP010Converter::new(&device)?; let conv = HdrP010Converter::new(&device, W, H)?;
conv.convert(&device, &context, &src_srv, &p010, W, H)?; let y_rtv = HdrP010Converter::plane_rtv(&device, &p010, DXGI_FORMAT_R16_UNORM)?;
let uv_rtv = HdrP010Converter::plane_rtv(&device, &p010, DXGI_FORMAT_R16G16_UNORM)?;
conv.convert(&context, &src_srv, &y_rtv, &uv_rtv, W, H)?;
// Staging copy of the whole P010 texture (both planes), MAP_READ. // Staging copy of the whole P010 texture (both planes), MAP_READ.
let stage_desc = D3D11_TEXTURE2D_DESC { let stage_desc = D3D11_TEXTURE2D_DESC {
@@ -1399,8 +1410,14 @@ impl VideoConverter {
let out_cs = DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709; let out_cs = DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709;
vctx.VideoProcessorSetStreamColorSpace1(&vp, 0, in_cs); vctx.VideoProcessorSetStreamColorSpace1(&vp, 0, in_cs);
vctx.VideoProcessorSetOutputColorSpace1(&vp, out_cs); vctx.VideoProcessorSetOutputColorSpace1(&vp, out_cs);
// One frame in, one frame out — no interpolation/auto-processing. // Progressive: one frame in, one frame out — no deinterlace, no frame-rate conversion.
vctx.VideoProcessorSetStreamFrameFormat(&vp, 0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE); vctx.VideoProcessorSetStreamFrameFormat(&vp, 0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE);
// …and no vendor "auto processing" either. Its documented DEFAULT is ENABLED, so until
// now the comment above claimed something only this call delivers: denoise, edge
// enhancement and whatever else the driver folds in were free to run inside every SDR
// `VideoProcessorBlt` — on the desktop-capture hot path, altering the pixels we encode
// and costing video-engine time nobody asked for.
vctx.VideoProcessorSetStreamAutoProcessingMode(&vp, 0, false);
Ok(Self { Ok(Self {
vdev, vdev,
+78 -37
View File
@@ -165,6 +165,19 @@ struct HostSlot {
srv: ID3D11ShaderResourceView, srv: ID3D11ShaderResourceView,
} }
/// One host out-ring slot: the NVENC/AMF/QSV encode-input texture, plus — when the output format is
/// P010 — the two per-plane RTVs the HDR shader passes render through.
///
/// The views live HERE, built once with the slot, because they are a property of the texture and the
/// mode, not of the frame: `HdrP010Converter::convert` used to create both on EVERY HDR frame, inside
/// the ring slot's keyed-mutex hold, so the driver sat blocked on that slot while we allocated views.
struct OutSlot {
tex: ID3D11Texture2D,
/// `(luma R16_UNORM, chroma R16G16_UNORM)` plane views. `None` for NV12/BGRA outputs, which the
/// video processor or a plain `CopyResource` writes without an RTV of ours.
p010: Option<(ID3D11RenderTargetView, ID3D11RenderTargetView)>,
}
/// One PyroWave output-ring slot: the two SEPARATE shareable plane textures the wavelet encoder /// One PyroWave output-ring slot: the two SEPARATE shareable plane textures the wavelet encoder
/// imports (design/pyrowave-windows-host-zerocopy.md) plus their RTVs (the [`BgraToYuvPlanes`] CSC /// imports (design/pyrowave-windows-host-zerocopy.md) plus their RTVs (the [`BgraToYuvPlanes`] CSC
/// renders into them). Y is full-res `R8_UNORM`, CbCr is half-res `R8G8_UNORM`; both are /// renders into them). Y is full-res `R8_UNORM`, CbCr is half-res `R8G8_UNORM`; both are
@@ -572,7 +585,7 @@ pub struct IddPushCapturer {
/// NV12 (SDR, BT.709 limited) or P010 (HDR, BT.2020 PQ limited), so NVENC takes native YUV and skips /// NV12 (SDR, BT.709 limited) or P010 (HDR, BT.2020 PQ limited), so NVENC takes native YUV and skips
/// its internal RGB→YUV CSC on the SM/3D engine the game saturates (plan §5.A). Rebuilt on a /// its internal RGB→YUV CSC on the SM/3D engine the game saturates (plan §5.A). Rebuilt on a
/// display-mode flip. Built lazily. /// display-mode flip. Built lazily.
out_ring: Vec<ID3D11Texture2D>, out_ring: Vec<OutSlot>,
out_idx: usize, out_idx: usize,
/// BGRA slot → NV12 (BT.709 limited) on the dedicated D3D11 VIDEO engine, used while the display is /// BGRA slot → NV12 (BT.709 limited) on the dedicated D3D11 VIDEO engine, used while the display is
/// SDR — keeps the colour-convert OFF the contended 3D/compute engine. Built lazily; rebuilt on a /// SDR — keeps the colour-convert OFF the contended 3D/compute engine. Built lazily; rebuilt on a
@@ -1188,7 +1201,7 @@ impl IddPushCapturer {
"IDD push(host): created sealed ring + delivered the channel; waiting for the driver \ "IDD push(host): created sealed ring + delivered the channel; waiting for the driver \
to attach + publish" to attach + publish"
); );
let me = Self { let mut me = Self {
device, device,
context, context,
target_id: target.target_id, target_id: target.target_id,
@@ -1256,6 +1269,10 @@ impl IddPushCapturer {
// it back to the caller for the DDA fallback (audit §5.1). // it back to the caller for the DDA fallback (audit §5.1).
_keepalive: Box::new(()), _keepalive: Box::new(()),
}; };
// The HDR SDR-white reference for the composited cursor, queried ONCE here rather than
// from the blend (which holds the ring slot's keyed mutex — see
// `refresh_sdr_white_scale`). No-op on an SDR composition.
me.refresh_sdr_white_scale();
// Bounded wait for the driver to ATTACH to the ring AND publish a first frame. An attach // Bounded wait for the driver to ATTACH to the ring AND publish a first frame. An attach
// failure (DRV_STATUS_TEX_FAIL) or an attach-but-no-frames (a game left the display in a // failure (DRV_STATUS_TEX_FAIL) or an attach-but-no-frames (a game left the display in a
// format/size the ring can't match) becomes an open failure the caller falls back from (→ DDA), // format/size the ring can't match) becomes an open failure the caller falls back from (→ DDA),
@@ -1626,6 +1643,9 @@ impl IddPushCapturer {
let _ = deliver_cursor_channel(&self.broker, self.target_id, cs, send); let _ = deliver_cursor_channel(&self.broker, self.target_id, cs, send);
} }
self.blend_scratch = None; // ring geometry/format changed — rebuild at next blend self.blend_scratch = None; // ring geometry/format changed — rebuild at next blend
// The HDR SDR-white reference belongs to the mode we just moved to. Queried HERE, not from
// the blend, which holds the ring slot's keyed mutex (see `refresh_sdr_white_scale`).
self.refresh_sdr_white_scale();
self.last_slot = None; // old-ring slot indices are meaningless now self.last_slot = None; // old-ring slot indices are meaningless now
self.last_seq = 0; self.last_seq = 0;
self.out_ring.clear(); // the output format changed → rebuild lazily at the new format self.out_ring.clear(); // the output format changed → rebuild lazily at the new format
@@ -1768,11 +1788,23 @@ impl IddPushCapturer {
// `&desc` is a fully-initialized stack `D3D11_TEXTURE2D_DESC`, the data arg is `None` (no // `&desc` is a fully-initialized stack `D3D11_TEXTURE2D_DESC`, the data arg is `None` (no
// initial data), and `Some(&mut t)` is a live out-parameter the call fills. `?` rejects a failed // initial data), and `Some(&mut t)` is a live out-parameter the call fills. `?` rejects a failed
// HRESULT before `t` is unwrapped, and the created texture belongs to `self.device`. // HRESULT before `t` is unwrapped, and the created texture belongs to `self.device`.
// `plane_rtv` is `unsafe` for the same COM reason and takes that same device plus the
// texture we just made; a driver that rejects planar RTVs fails HERE, at ring build, which
// is where such a failure belongs (it used to surface on the first frame).
unsafe { unsafe {
self.device self.device
.CreateTexture2D(&desc, None, Some(&mut t)) .CreateTexture2D(&desc, None, Some(&mut t))
.context("CreateTexture2D(IDD out ring)")?; .context("CreateTexture2D(IDD out ring)")?;
self.out_ring.push(t.context("null out-ring texture")?); let tex = t.context("null out-ring texture")?;
let p010 = if format == DXGI_FORMAT_P010 {
Some((
HdrP010Converter::plane_rtv(&self.device, &tex, DXGI_FORMAT_R16_UNORM)?,
HdrP010Converter::plane_rtv(&self.device, &tex, DXGI_FORMAT_R16G16_UNORM)?,
))
} else {
None
};
self.out_ring.push(OutSlot { tex, p010 });
} }
} }
Ok(()) Ok(())
@@ -1872,7 +1904,8 @@ impl IddPushCapturer {
// SAFETY: `HdrP010Converter::new` is `unsafe` (it compiles D3D11 shaders + creates // SAFETY: `HdrP010Converter::new` is `unsafe` (it compiles D3D11 shaders + creates
// resources); we pass a live borrow of `self.device`, the device the converter's resources // resources); we pass a live borrow of `self.device`, the device the converter's resources
// belong to, and `?` propagates any failure before the converter is stored. // belong to, and `?` propagates any failure before the converter is stored.
self.hdr_p010_conv = Some(unsafe { HdrP010Converter::new(&self.device)? }); self.hdr_p010_conv =
Some(unsafe { HdrP010Converter::new(&self.device, self.width, self.height)? });
} }
} else if self.want_444 { } else if self.want_444 {
// Full-chroma passthrough — no conversion resources to build. // Full-chroma passthrough — no conversion resources to build.
@@ -1995,6 +2028,32 @@ impl IddPushCapturer {
self.cursor_shared.as_mut().and_then(|c| c.read()) self.cursor_shared.as_mut().and_then(|c| c.read())
} }
/// Refresh [`Self::sdr_white_scale`] — where DWM places SDR white on this HDR desktop, which the
/// composited cursor must match or it renders visibly dark (~2.5× at the Windows default).
///
/// Called at open and after every ring recreate, NEVER from the blend. `sdr_white_level_scale` is
/// a CCD query that contends the display-config lock — the exact contention `DescriptorPoller`'s
/// doc says must stay off the frame path — and it used to run inside `prepare_blend_scratch`,
/// which holds the ring slot's KEYED MUTEX: the driver's publisher sat blocked on that slot for
/// the duration of a display-config round-trip. "Only on scratch rebuilds" is not the same as
/// harmless when the thing being blocked is the producer. A failed query keeps the prior value.
fn refresh_sdr_white_scale(&mut self) {
if !self.display_hdr {
return;
}
// SAFETY: `sdr_white_level_scale` is an `unsafe fn` running a read-only CCD query over owned
// local buffers; the `Copy` target id crosses by value and it returns an owned value.
let queried = unsafe { pf_win_display::win_display::sdr_white_level_scale(self.target_id) };
self.sdr_white_scale = queried.unwrap_or(self.sdr_white_scale);
tracing::info!(
target_id = self.target_id,
queried = ?queried,
applied = self.sdr_white_scale,
"cursor composite: HDR SDR-white scale (1.0 = 80 nits; None = query failed — keeping \
the prior value)"
);
}
/// The (serial, x, y, visible) of the CURRENT live cursor — the composite-regen change key. /// The (serial, x, y, visible) of the CURRENT live cursor — the composite-regen change key.
/// `None` while no source has a shape yet. /// `None` while no source has a shape yet.
fn cursor_blend_key(&mut self) -> Option<(u64, i32, i32, bool)> { fn cursor_blend_key(&mut self) -> Option<(u64, i32, i32, bool)> {
@@ -2019,7 +2078,7 @@ impl IddPushCapturer {
// take a fully-initialized stack descriptor plus live out-params and are `.ok()`-checked before // take a fully-initialized stack descriptor plus live out-params and are `.ok()`-checked before
// use; `CopyResource` moves between our own scratch and the caller's live slot texture, which // use; `CopyResource` moves between our own scratch and the caller's live slot texture, which
// share format and size by construction (the scratch is rebuilt whenever the ring geometry // share format and size by construction (the scratch is rebuilt whenever the ring geometry
// changes). `sdr_white_level_scale` is a read-only CCD query over owned locals. // changes).
unsafe { unsafe {
let fmt = self.ring_format(); let fmt = self.ring_format();
// (Re)build the scratch at the current ring geometry. // (Re)build the scratch at the current ring geometry.
@@ -2059,25 +2118,10 @@ impl IddPushCapturer {
}); });
match built { match built {
Some((t, v)) => { Some((t, v)) => {
// `sdr_white_scale` is NOT queried here — see `refresh_sdr_white_scale`:
// this runs while the ring slot's keyed mutex is held, and a CCD
// display-config round-trip has no business blocking the driver's publisher.
self.blend_scratch = Some((t, v, self.width, self.height, fmt)); self.blend_scratch = Some((t, v, self.width, self.height, fmt));
if self.display_hdr {
// Where DWM places SDR white on this HDR desktop — the composited
// cursor must match or it reads dark (~2.5x at the Windows default).
// Queried only here: scratch rebuilds are rare, and the CCD query
// contends on the display-config lock, which must stay OFF the
// per-frame path.
// Safety: read-only CCD query over owned locals (within unsafe fn).
let queried =
pf_win_display::win_display::sdr_white_level_scale(self.target_id);
self.sdr_white_scale = queried.unwrap_or(self.sdr_white_scale);
tracing::info!(
target_id = self.target_id,
queried = ?queried,
applied = self.sdr_white_scale,
"cursor composite: HDR SDR-white scale (1.0 = 80 nits; None = \
query failed keeping the prior value)"
);
}
} }
None => { None => {
if !self.cursor_blend_failed { if !self.cursor_blend_failed {
@@ -2309,7 +2353,8 @@ impl IddPushCapturer {
} else { } else {
self.ensure_out_ring()?; self.ensure_out_ring()?;
self.ensure_converter()?; self.ensure_converter()?;
(Some(self.out_ring[i].clone()), None) let s = &self.out_ring[i];
(Some((s.tex.clone(), s.p010.clone())), None)
}; };
let (_, pf) = self.out_format(); let (_, pf) = self.out_format();
let ring_len = if self.pyrowave { let ring_len = if self.pyrowave {
@@ -2360,14 +2405,10 @@ impl IddPushCapturer {
// HDR: FP16 slot SRV → P010 (BT.2020 PQ) via the shader; NVENC takes native P010. // HDR: FP16 slot SRV → P010 (BT.2020 PQ) via the shader; NVENC takes native P010.
if let Some(conv) = self.hdr_p010_conv.as_ref() { if let Some(conv) = self.hdr_p010_conv.as_ref() {
let src = blended.as_ref().map(|(_, srv)| srv).unwrap_or(&slot_srv); let src = blended.as_ref().map(|(_, srv)| srv).unwrap_or(&slot_srv);
conv.convert( let (_, rtvs) = out.as_ref().expect("out ring");
&self.device, // The slot's P010 plane views, built once in `ensure_out_ring`.
&self.context, let (y_rtv, uv_rtv) = rtvs.as_ref().expect("P010 out slot has plane RTVs");
src, conv.convert(&self.context, src, y_rtv, uv_rtv, self.width, self.height)?;
out.as_ref().expect("out ring"),
self.width,
self.height,
)?;
} }
} else if self.want_444 { } else if self.want_444 {
// SDR 4:4:4: pass the BGRA slot through untouched — NVENC ingests full-chroma // SDR 4:4:4: pass the BGRA slot through untouched — NVENC ingests full-chroma
@@ -2375,12 +2416,12 @@ impl IddPushCapturer {
// copy-engine move; the slot releases back to the driver immediately. // copy-engine move; the slot releases back to the driver immediately.
let src = blended.as_ref().map(|(t, _)| t).unwrap_or(&slot_tex); let src = blended.as_ref().map(|(t, _)| t).unwrap_or(&slot_tex);
self.context self.context
.CopyResource(out.as_ref().expect("out ring"), src); .CopyResource(&out.as_ref().expect("out ring").0, src);
} else { } else {
// SDR: BGRA slot → NV12 on the VIDEO engine; NVENC takes native NV12, no SM-side CSC. // SDR: BGRA slot → NV12 on the VIDEO engine; NVENC takes native NV12, no SM-side CSC.
if let Some(conv) = self.video_conv.as_ref() { if let Some(conv) = self.video_conv.as_ref() {
let src = blended.as_ref().map(|(t, _)| t).unwrap_or(&slot_tex); let src = blended.as_ref().map(|(t, _)| t).unwrap_or(&slot_tex);
conv.convert(src, out.as_ref().expect("out ring"))?; conv.convert(src, &out.as_ref().expect("out ring").0)?;
} }
} }
} }
@@ -2394,7 +2435,7 @@ impl IddPushCapturer {
if let Some((y, _, cbcr, _)) = pyro_slot.as_ref() { if let Some((y, _, cbcr, _)) = pyro_slot.as_ref() {
self.pyro_last = Some((y.clone(), cbcr.clone())); self.pyro_last = Some((y.clone(), cbcr.clone()));
} else { } else {
self.last_present = Some((out.as_ref().expect("out ring").clone(), pf)); self.last_present = Some((out.as_ref().expect("out ring").0.clone(), pf));
} }
let now = Instant::now(); let now = Instant::now();
if regen { if regen {
@@ -2493,7 +2534,7 @@ impl IddPushCapturer {
}), }),
) )
} else { } else {
(out.expect("out ring texture"), None) (out.expect("out ring texture").0, None)
}; };
Ok(Some(CapturedFrame { Ok(Some(CapturedFrame {
width: self.width, width: self.width,
@@ -2555,7 +2596,7 @@ impl IddPushCapturer {
}); });
} }
let (src, pf) = self.last_present.clone()?; let (src, pf) = self.last_present.clone()?;
let dst = self.out_ring.get(i)?.clone(); let dst = self.out_ring.get(i)?.tex.clone();
// SAFETY: GPU copy on the owning thread's immediate context; src/dst are our out-ring textures of // SAFETY: GPU copy on the owning thread's immediate context; src/dst are our out-ring textures of
// identical format/size (src is a previous out-ring slot; dst the next). // identical format/size (src is a previous out-ring slot; dst the next).
unsafe { unsafe {