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:
@@ -28,12 +28,11 @@ use windows::Win32::Graphics::Direct3D11::{
|
||||
ID3D11RenderTargetView, ID3D11SamplerState, ID3D11ShaderResourceView, ID3D11Texture2D,
|
||||
ID3D11VertexShader, D3D11_BIND_CONSTANT_BUFFER, D3D11_BIND_RENDER_TARGET,
|
||||
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_MAPPED_SUBRESOURCE, D3D11_MAP_READ, D3D11_MAP_WRITE_DISCARD,
|
||||
D3D11_RENDER_TARGET_VIEW_DESC, D3D11_RENDER_TARGET_VIEW_DESC_0, D3D11_RTV_DIMENSION_TEXTURE2D,
|
||||
D3D11_SAMPLER_DESC, D3D11_SDK_VERSION, D3D11_SUBRESOURCE_DATA, D3D11_TEX2D_RTV,
|
||||
D3D11_TEXTURE2D_DESC, D3D11_TEXTURE_ADDRESS_CLAMP, D3D11_USAGE_DEFAULT, D3D11_USAGE_DYNAMIC,
|
||||
D3D11_USAGE_STAGING, D3D11_VIEWPORT,
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_FILTER_MIN_MAG_MIP_POINT, D3D11_MAPPED_SUBRESOURCE,
|
||||
D3D11_MAP_READ, D3D11_RENDER_TARGET_VIEW_DESC, D3D11_RENDER_TARGET_VIEW_DESC_0,
|
||||
D3D11_RTV_DIMENSION_TEXTURE2D, D3D11_SAMPLER_DESC, D3D11_SDK_VERSION, D3D11_SUBRESOURCE_DATA,
|
||||
D3D11_TEX2D_RTV, D3D11_TEXTURE2D_DESC, D3D11_TEXTURE_ADDRESS_CLAMP, D3D11_USAGE_DEFAULT,
|
||||
D3D11_USAGE_IMMUTABLE, D3D11_USAGE_STAGING, D3D11_VIEWPORT,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::Common::{
|
||||
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_uv: ID3D11PixelShader,
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
@@ -409,15 +415,23 @@ impl HdrP010Converter {
|
||||
};
|
||||
let mut sampler = None;
|
||||
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 {
|
||||
ByteWidth: 16, // float2 inv_src + float2 pad
|
||||
Usage: D3D11_USAGE_DYNAMIC,
|
||||
Usage: D3D11_USAGE_IMMUTABLE,
|
||||
BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,
|
||||
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
|
||||
CPUAccessFlags: 0,
|
||||
..Default::default()
|
||||
};
|
||||
let init = D3D11_SUBRESOURCE_DATA {
|
||||
pSysMem: inv_src.as_ptr().cast(),
|
||||
SysMemPitch: 0,
|
||||
SysMemSlicePitch: 0,
|
||||
};
|
||||
let mut cbuf = None;
|
||||
device.CreateBuffer(&cbd, None, Some(&mut cbuf))?;
|
||||
device.CreateBuffer(&cbd, Some(&init), Some(&mut cbuf))?;
|
||||
Ok(Self {
|
||||
vs: vs.context("p010 vs")?,
|
||||
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`
|
||||
/// (`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.
|
||||
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,
|
||||
dst: &ID3D11Texture2D,
|
||||
format: DXGI_FORMAT,
|
||||
@@ -461,40 +479,29 @@ impl HdrP010Converter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert `src_srv` (FP16 scRGB, WxH) into `dst` (a `DXGI_FORMAT_P010` texture with
|
||||
/// `BIND_RENDER_TARGET`). Two opaque passes: full-res luma → plane 0, half-res chroma → plane 1.
|
||||
/// `w`/`h` are the full luma dimensions (must be even). Returns `Err` if a plane RTV can't be
|
||||
/// created (driver) so the caller can fall back to the R10 path.
|
||||
/// Convert `src_srv` (FP16 scRGB, WxH) into a `DXGI_FORMAT_P010` texture through the two plane
|
||||
/// RTVs the CALLER built for it ([`Self::plane_rtv`]): full-res luma → `y_rtv` (plane 0),
|
||||
/// half-res chroma → `uv_rtv` (plane 1). `w`/`h` are the full luma dimensions (must be even) and
|
||||
/// 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(
|
||||
&self,
|
||||
device: &ID3D11Device,
|
||||
ctx: &ID3D11DeviceContext,
|
||||
src_srv: &ID3D11ShaderResourceView,
|
||||
dst: &ID3D11Texture2D,
|
||||
y_rtv: &ID3D11RenderTargetView,
|
||||
uv_rtv: &ID3D11RenderTargetView,
|
||||
w: u32,
|
||||
h: u32,
|
||||
) -> Result<()> {
|
||||
// SAFETY: all D3D11 work runs on the caller's live `device`/`ctx` borrows (`ctx` is the
|
||||
// owning capture thread's immediate context), and `plane_rtv` receives that same device. The
|
||||
// `copy_nonoverlapping` writes `cb.len()` `f32`s into `mapped.pData` — the pointer the
|
||||
// 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.
|
||||
// SAFETY: all D3D11 work runs on the caller's live `ctx` borrow (the owning capture thread's
|
||||
// immediate context) over borrowed slices of fully-initialized locals (the viewports) and
|
||||
// clones of the caller's live SRV/RTVs. No raw pointers and no mapping on this path.
|
||||
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.
|
||||
ctx.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite
|
||||
ctx.VSSetShader(&self.vs, None);
|
||||
@@ -937,8 +944,10 @@ pub fn hdr_p010_convert_bars_on_luid(
|
||||
.context("CreateTexture2D(P010 bars dst)")?;
|
||||
let p010 = p010.context("null p010 tex")?;
|
||||
|
||||
let conv = HdrP010Converter::new(&device)?;
|
||||
conv.convert(&device, &context, &src_srv, &p010, w, h)?;
|
||||
let conv = HdrP010Converter::new(&device, 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))
|
||||
}
|
||||
}
|
||||
@@ -1130,8 +1139,10 @@ pub fn hdr_p010_selftest_at(w: u32, h: u32, vendor: Option<u32>) -> Result<()> {
|
||||
.context("CreateTexture2D(P010 dst)")?;
|
||||
let p010 = p010.context("null p010 tex")?;
|
||||
|
||||
let conv = HdrP010Converter::new(&device)?;
|
||||
conv.convert(&device, &context, &src_srv, &p010, W, H)?;
|
||||
let conv = HdrP010Converter::new(&device, 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.
|
||||
let stage_desc = D3D11_TEXTURE2D_DESC {
|
||||
@@ -1399,8 +1410,14 @@ impl VideoConverter {
|
||||
let out_cs = DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709;
|
||||
vctx.VideoProcessorSetStreamColorSpace1(&vp, 0, in_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);
|
||||
// …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 {
|
||||
vdev,
|
||||
|
||||
@@ -165,6 +165,19 @@ struct HostSlot {
|
||||
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
|
||||
/// 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
|
||||
@@ -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
|
||||
/// its internal RGB→YUV CSC on the SM/3D engine the game saturates (plan §5.A). Rebuilt on a
|
||||
/// display-mode flip. Built lazily.
|
||||
out_ring: Vec<ID3D11Texture2D>,
|
||||
out_ring: Vec<OutSlot>,
|
||||
out_idx: usize,
|
||||
/// 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
|
||||
@@ -1188,7 +1201,7 @@ impl IddPushCapturer {
|
||||
"IDD push(host): created sealed ring + delivered the channel; waiting for the driver \
|
||||
to attach + publish"
|
||||
);
|
||||
let me = Self {
|
||||
let mut me = Self {
|
||||
device,
|
||||
context,
|
||||
target_id: target.target_id,
|
||||
@@ -1256,6 +1269,10 @@ impl IddPushCapturer {
|
||||
// it back to the caller for the DDA fallback (audit §5.1).
|
||||
_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
|
||||
// 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),
|
||||
@@ -1626,6 +1643,9 @@ impl IddPushCapturer {
|
||||
let _ = deliver_cursor_channel(&self.broker, self.target_id, cs, send);
|
||||
}
|
||||
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_seq = 0;
|
||||
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
|
||||
// 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`.
|
||||
// `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 {
|
||||
self.device
|
||||
.CreateTexture2D(&desc, None, Some(&mut t))
|
||||
.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(())
|
||||
@@ -1872,7 +1904,8 @@ impl IddPushCapturer {
|
||||
// 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
|
||||
// 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 {
|
||||
// Full-chroma passthrough — no conversion resources to build.
|
||||
@@ -1995,6 +2028,32 @@ impl IddPushCapturer {
|
||||
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.
|
||||
/// `None` while no source has a shape yet.
|
||||
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
|
||||
// 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
|
||||
// changes). `sdr_white_level_scale` is a read-only CCD query over owned locals.
|
||||
// changes).
|
||||
unsafe {
|
||||
let fmt = self.ring_format();
|
||||
// (Re)build the scratch at the current ring geometry.
|
||||
@@ -2059,25 +2118,10 @@ impl IddPushCapturer {
|
||||
});
|
||||
match built {
|
||||
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));
|
||||
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 => {
|
||||
if !self.cursor_blend_failed {
|
||||
@@ -2309,7 +2353,8 @@ impl IddPushCapturer {
|
||||
} else {
|
||||
self.ensure_out_ring()?;
|
||||
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 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.
|
||||
if let Some(conv) = self.hdr_p010_conv.as_ref() {
|
||||
let src = blended.as_ref().map(|(_, srv)| srv).unwrap_or(&slot_srv);
|
||||
conv.convert(
|
||||
&self.device,
|
||||
&self.context,
|
||||
src,
|
||||
out.as_ref().expect("out ring"),
|
||||
self.width,
|
||||
self.height,
|
||||
)?;
|
||||
let (_, rtvs) = out.as_ref().expect("out ring");
|
||||
// The slot's P010 plane views, built once in `ensure_out_ring`.
|
||||
let (y_rtv, uv_rtv) = rtvs.as_ref().expect("P010 out slot has plane RTVs");
|
||||
conv.convert(&self.context, src, y_rtv, uv_rtv, self.width, self.height)?;
|
||||
}
|
||||
} else if self.want_444 {
|
||||
// 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.
|
||||
let src = blended.as_ref().map(|(t, _)| t).unwrap_or(&slot_tex);
|
||||
self.context
|
||||
.CopyResource(out.as_ref().expect("out ring"), src);
|
||||
.CopyResource(&out.as_ref().expect("out ring").0, src);
|
||||
} else {
|
||||
// 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() {
|
||||
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() {
|
||||
self.pyro_last = Some((y.clone(), cbcr.clone()));
|
||||
} 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();
|
||||
if regen {
|
||||
@@ -2493,7 +2534,7 @@ impl IddPushCapturer {
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
(out.expect("out ring texture"), None)
|
||||
(out.expect("out ring texture").0, None)
|
||||
};
|
||||
Ok(Some(CapturedFrame {
|
||||
width: self.width,
|
||||
@@ -2555,7 +2596,7 @@ impl IddPushCapturer {
|
||||
});
|
||||
}
|
||||
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
|
||||
// identical format/size (src is a previous out-ring slot; dst the next).
|
||||
unsafe {
|
||||
|
||||
Reference in New Issue
Block a user