From 6a2a153c0be79b785739ee4064ed9e585436fedb Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 26 Jul 2026 10:18:48 +0200 Subject: [PATCH] =?UTF-8?q?chore(pf-capture):=20make=20the=20crate=20verif?= =?UTF-8?q?iable=20=E2=80=94=20Windows=20CI=20lint=20+=20two=20denies=20(s?= =?UTF-8?q?weep=20Phase=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate for the rest of design/pf-capture-sweep.md: today half this crate is compiled by no CI job at all, so the Windows-side findings can't even be type-checked. 0.1 (X1) — windows-host.yml lints `-p pf-capture --all-targets`. The host lint above it builds pf-capture only as a DEPENDENCY, so its `#[cfg(test)]` modules were never compiled anywhere: the 5 StallWatch tests, the DXGI HDR self-tests and the cursor-conversion tables were dead code in CI. The workflow's own comment already diagnosed this blind spot and fixed it for pf-encode; pf-capture never got the same treatment. No features on this crate, so no feature juggling and no extra dep tree. 0.2 (X2) — `#![deny(unsafe_op_in_unsafe_fn)]` beside the existing `deny(clippy::undocumented_unsafe_blocks)`. The crate declares "every unsafe block carries a SAFETY proof; enforce it" in ten file headers, but in edition 2021 that lint has nothing to fire on inside an `unsafe fn` — so the hardest FFI in the crate was exempt from its own program: the ring/slot construction, the fence signal, the blend scratch, and every D3D converter ctor/convert. 119 operations across 16 functions now carry a proof. `shared_object_sa` loses its `unsafe` marker instead (it states no precondition — only its RESULT is a raw pointer, which is the caller's business). 0.3 (X4) — drop the crate-wide `#![allow(dead_code)]`. Verified: zero warnings on either target without it, so it was hiding nothing the lint can see (W16's dead items are `pub`/written-once, so they need Phase 1.7's deletion instead). No behaviour change: only lint attributes, `unsafe { }` scoping and comments. Linux `cargo test -p pf-capture` 6/6, clippy --all-targets clean on both targets (Windows verified by cross-checking x86_64-pc-windows-msvc locally). Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/windows-host.yml | 16 +- crates/pf-capture/src/lib.rs | 6 +- crates/pf-capture/src/windows/dxgi.rs | 619 ++++++++++-------- crates/pf-capture/src/windows/idd_push.rs | 435 ++++++------ .../src/windows/idd_push/cursor_blend.rs | 277 ++++---- .../pf-capture/src/windows/synthetic_nv12.rs | 24 +- 6 files changed, 749 insertions(+), 628 deletions(-) diff --git a/.gitea/workflows/windows-host.yml b/.gitea/workflows/windows-host.yml index 6788ae4a..ac1a1887 100644 --- a/.gitea/workflows/windows-host.yml +++ b/.gitea/workflows/windows-host.yml @@ -172,12 +172,15 @@ jobs: # openh264 rebuild), and keeps everything in one C:\t\release tree. Same reason # pf-vkhdr-layer's clippy below runs --release. # - # pf-encode is linted SEPARATELY with --all-targets so its Windows `#[cfg(test)]` modules - # are type-checked — the AMF C-ABI layout assertions (`variant_layout_matches_c` and - # friends, which are the only guard on a hand-mirrored vtable ABI), the QSV tests, and the - # PyroWave-Windows smoke test. The host lint above cannot cover them: `-p punktfunk-host` - # only builds pf-encode as a dependency, so its test targets are never compiled, and that - # blind spot is what let the Linux twin's tests rot to the wrong arity unnoticed. + # pf-encode and pf-capture are linted SEPARATELY with --all-targets so their Windows + # `#[cfg(test)]` modules are type-checked — pf-encode's AMF C-ABI layout assertions + # (`variant_layout_matches_c` and friends, which are the only guard on a hand-mirrored + # vtable ABI), the QSV tests, the PyroWave-Windows smoke test; pf-capture's `StallWatch` + # tests, the DXGI HDR self-tests and the cursor-conversion tables. The host lint above + # cannot cover them: `-p punktfunk-host` only builds those crates as dependencies, so their + # test targets are never compiled anywhere, and that blind spot is what let the Linux twin's + # tests rot to the wrong arity unnoticed. pf-capture has no cargo features, so it needs no + # feature juggling and pulls in no extra dep tree. # NOTE: clippy (a check, no link step) is deliberately the vehicle here — `cargo test` # with `nvenc` cannot LINK on MSVC: nvidia-video-codec-sdk link-imports # NvEncodeAPICreateInstance / NvEncodeAPIGetMaxSupportedVersion, which resolve only against @@ -188,6 +191,7 @@ jobs: run: | cargo clippy --release -p punktfunk-host --features nvenc,amf-qsv,qsv -- -D warnings; if ($LASTEXITCODE) { throw "host clippy" } cargo clippy --release -p pf-encode --all-targets --features nvenc,amf-qsv,qsv -- -D warnings; if ($LASTEXITCODE) { throw "pf-encode clippy" } + cargo clippy --release -p pf-capture --all-targets -- -D warnings; if ($LASTEXITCODE) { throw "pf-capture clippy" } cargo clippy --release -p punktfunk-tray -- -D warnings; if ($LASTEXITCODE) { throw "tray clippy" } - name: Build + lint the HDR Vulkan layer (pf-vkhdr-layer) diff --git a/crates/pf-capture/src/lib.rs b/crates/pf-capture/src/lib.rs index 3fc198d4..fba08b52 100644 --- a/crates/pf-capture/src/lib.rs +++ b/crates/pf-capture/src/lib.rs @@ -7,10 +7,12 @@ //! [`FrameChannelSender`] closure, so this crate reaches neither the encoder nor the host //! orchestrator). -// Scaffold: trait defaults + synthetic sources are defined ahead of the backends that use them. -#![allow(dead_code)] // Every unsafe block in this crate carries a `// SAFETY:` proof; enforce it (unsafe-proof program). #![deny(clippy::undocumented_unsafe_blocks)] +// …and that program only covers a whole `unsafe fn` body once the body needs its own block: in +// edition 2021 `unsafe_op_in_unsafe_fn` is allow-by-default, which exempted the crate's hardest FFI +// (the ring/slot construction, the channel broker, every D3D converter ctor) from the deny above. +#![deny(unsafe_op_in_unsafe_fn)] use anyhow::Result; use pf_frame::{CapturedFrame, FramePayload, PixelFormat}; diff --git a/crates/pf-capture/src/windows/dxgi.rs b/crates/pf-capture/src/windows/dxgi.rs index 1200b802..fd69c8bf 100644 --- a/crates/pf-capture/src/windows/dxgi.rs +++ b/crates/pf-capture/src/windows/dxgi.rs @@ -65,7 +65,10 @@ unsafe extern "system" fn hybrid_query_hook(gpu_preference: *mut u32) -> i32 { if gpu_preference.is_null() { return 0xC000_000Du32 as i32; // STATUS_INVALID_PARAMETER } - *gpu_preference = 3; // D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED + // SAFETY: win32u's contract for this export — the caller (DXGI) passes a writable `*mut u32` + // out-param — and the null case has just been rejected above, so this is an in-bounds, + // 4-aligned single-word store into the caller's live local. + unsafe { *gpu_preference = 3 }; // D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED 0 // STATUS_SUCCESS } @@ -161,36 +164,49 @@ pub fn install_gpu_pref_hook() { }); } +/// Compile one HLSL entry point. Returns the bytecode blob's bytes. +/// +/// # Safety +/// `entry` and `target` must be valid NUL-terminated ASCII pointers (an `s!()` literal at every +/// call site); `src` is a plain Rust `&str`, read only for the duration of the call. pub(crate) unsafe fn compile_shader(src: &str, entry: PCSTR, target: PCSTR) -> Result> { - let mut blob: Option = None; - let mut errs: Option = None; - let r = D3DCompile( - src.as_ptr() as *const c_void, - src.len(), - PCSTR::null(), - None, - None, - entry, - target, - 0, - 0, - &mut blob, - Some(&mut errs), - ); - if r.is_err() { - let msg = errs - .as_ref() - .map(|e| { - let p = e.GetBufferPointer() as *const u8; - String::from_utf8_lossy(std::slice::from_raw_parts(p, e.GetBufferSize())) - .to_string() - }) - .unwrap_or_default(); - bail!("D3DCompile failed: {msg}"); + // SAFETY: `D3DCompile` reads `src.as_ptr()` for exactly `src.len()` bytes (a live `&str` that + // outlives the synchronous call) plus the two caller-supplied NUL-terminated `PCSTR`s (per the + // contract above); `&mut blob` / `Some(&mut errs)` are live out-params. Both + // `slice::from_raw_parts` calls pair a blob's OWN `GetBufferPointer` with its OWN + // `GetBufferSize` while that blob is still alive, and the slice is copied + // (`to_string` / `to_vec`) before it goes out of scope. + unsafe { + let mut blob: Option = None; + let mut errs: Option = None; + let r = D3DCompile( + src.as_ptr() as *const c_void, + src.len(), + PCSTR::null(), + None, + None, + entry, + target, + 0, + 0, + &mut blob, + Some(&mut errs), + ); + if r.is_err() { + let msg = errs + .as_ref() + .map(|e| { + let p = e.GetBufferPointer() as *const u8; + String::from_utf8_lossy(std::slice::from_raw_parts(p, e.GetBufferSize())) + .to_string() + }) + .unwrap_or_default(); + bail!("D3DCompile failed: {msg}"); + } + let blob = blob.context("no shader blob")?; + let p = blob.GetBufferPointer() as *const u8; + Ok(std::slice::from_raw_parts(p, blob.GetBufferSize()).to_vec()) } - let blob = blob.context("no shader blob")?; - let p = blob.GetBufferPointer() as *const u8; - Ok(std::slice::from_raw_parts(p, blob.GetBufferSize()).to_vec()) } /// Fullscreen-triangle vertex shader for the HDR conversion pass (3 verts, no input layout). @@ -322,49 +338,55 @@ pub(crate) struct HdrP010Converter { impl HdrP010Converter { pub(crate) unsafe fn new(device: &ID3D11Device) -> Result { - // Inline the shared HLSL (D3DCompile has no include handler wired here). The two PS sources - // carry a `#include_common` marker we substitute before compiling. - let y_src = HDR_P010_Y_PS.replace("#include_common", HDR_P010_COMMON); - let uv_src = HDR_P010_UV_PS.replace("#include_common", HDR_P010_COMMON); - let vsb = compile_shader(HDR_VS, s!("main"), s!("vs_5_0"))?; - let yb = compile_shader(&y_src, s!("main"), s!("ps_5_0"))?; - let uvb = compile_shader(&uv_src, s!("main"), s!("ps_5_0"))?; - let mut vs = None; - device.CreateVertexShader(&vsb, None, Some(&mut vs))?; - let mut ps_y = None; - device.CreatePixelShader(&yb, None, Some(&mut ps_y))?; - let mut ps_uv = None; - device.CreatePixelShader(&uvb, None, Some(&mut ps_uv))?; - let sd = D3D11_SAMPLER_DESC { - // POINT: the Y pass samples a single texel centre exactly, and the UV pass does its OWN - // 2x2 box average via 4 explicit taps at texel centres (offset half a texel). Point - // sampling keeps each tap exact; the averaging is in the shader, not the sampler. - Filter: D3D11_FILTER_MIN_MAG_MIP_POINT, - AddressU: D3D11_TEXTURE_ADDRESS_CLAMP, - AddressV: D3D11_TEXTURE_ADDRESS_CLAMP, - AddressW: D3D11_TEXTURE_ADDRESS_CLAMP, - ComparisonFunc: D3D11_COMPARISON_NEVER, - MaxLOD: f32::MAX, - ..Default::default() - }; - let mut sampler = None; - device.CreateSamplerState(&sd, Some(&mut sampler))?; - let cbd = D3D11_BUFFER_DESC { - ByteWidth: 16, // float2 inv_src + float2 pad - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - ..Default::default() - }; - let mut cbuf = None; - device.CreateBuffer(&cbd, None, Some(&mut cbuf))?; - Ok(Self { - vs: vs.context("p010 vs")?, - ps_y: ps_y.context("p010 y ps")?, - ps_uv: ps_uv.context("p010 uv ps")?, - sampler: sampler.context("p010 sampler")?, - cbuf: cbuf.context("p010 cbuf")?, - }) + // 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 + // raw pointer outlives the call that produced it. + unsafe { + // Inline the shared HLSL (D3DCompile has no include handler wired here). The two PS sources + // carry a `#include_common` marker we substitute before compiling. + let y_src = HDR_P010_Y_PS.replace("#include_common", HDR_P010_COMMON); + let uv_src = HDR_P010_UV_PS.replace("#include_common", HDR_P010_COMMON); + let vsb = compile_shader(HDR_VS, s!("main"), s!("vs_5_0"))?; + let yb = compile_shader(&y_src, s!("main"), s!("ps_5_0"))?; + let uvb = compile_shader(&uv_src, s!("main"), s!("ps_5_0"))?; + let mut vs = None; + device.CreateVertexShader(&vsb, None, Some(&mut vs))?; + let mut ps_y = None; + device.CreatePixelShader(&yb, None, Some(&mut ps_y))?; + let mut ps_uv = None; + device.CreatePixelShader(&uvb, None, Some(&mut ps_uv))?; + let sd = D3D11_SAMPLER_DESC { + // POINT: the Y pass samples a single texel centre exactly, and the UV pass does its OWN + // 2x2 box average via 4 explicit taps at texel centres (offset half a texel). Point + // sampling keeps each tap exact; the averaging is in the shader, not the sampler. + Filter: D3D11_FILTER_MIN_MAG_MIP_POINT, + AddressU: D3D11_TEXTURE_ADDRESS_CLAMP, + AddressV: D3D11_TEXTURE_ADDRESS_CLAMP, + AddressW: D3D11_TEXTURE_ADDRESS_CLAMP, + ComparisonFunc: D3D11_COMPARISON_NEVER, + MaxLOD: f32::MAX, + ..Default::default() + }; + let mut sampler = None; + device.CreateSamplerState(&sd, Some(&mut sampler))?; + let cbd = D3D11_BUFFER_DESC { + ByteWidth: 16, // float2 inv_src + float2 pad + Usage: D3D11_USAGE_DYNAMIC, + BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + ..Default::default() + }; + let mut cbuf = None; + device.CreateBuffer(&cbd, None, Some(&mut cbuf))?; + Ok(Self { + vs: vs.context("p010 vs")?, + ps_y: ps_y.context("p010 y ps")?, + ps_uv: ps_uv.context("p010 uv ps")?, + sampler: sampler.context("p010 sampler")?, + cbuf: cbuf.context("p010 cbuf")?, + }) + } } /// Create a per-plane RTV of the P010 texture `dst` with the given single-plane `format` @@ -375,15 +397,19 @@ impl HdrP010Converter { dst: &ID3D11Texture2D, format: DXGI_FORMAT, ) -> Result { - let desc = D3D11_RENDER_TARGET_VIEW_DESC { - Format: format, - ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D, - Anonymous: D3D11_RENDER_TARGET_VIEW_DESC_0 { - Texture2D: D3D11_TEX2D_RTV { MipSlice: 0 }, - }, - }; - let mut rtv: Option = None; - device + // SAFETY: one `?`-checked `CreateRenderTargetView` on the live `device` borrow, with a + // fully-initialized `D3D11_RENDER_TARGET_VIEW_DESC` local whose address is taken only for the + // duration of the synchronous call, plus a live `Option` out-param. + unsafe { + let desc = D3D11_RENDER_TARGET_VIEW_DESC { + Format: format, + ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D, + Anonymous: D3D11_RENDER_TARGET_VIEW_DESC_0 { + Texture2D: D3D11_TEX2D_RTV { MipSlice: 0 }, + }, + }; + let mut rtv: Option = None; + device .CreateRenderTargetView( dst, Some(&desc as *const D3D11_RENDER_TARGET_VIEW_DESC), @@ -392,7 +418,8 @@ impl HdrP010Converter { .with_context(|| { format!("CreateRenderTargetView(P010 plane, format={format:?}) — driver may not support planar RTVs") })?; - rtv.context("p010 plane rtv null") + rtv.context("p010 plane rtv null") + } } /// Convert `src_srv` (FP16 scRGB, WxH) into `dst` (a `DXGI_FORMAT_P010` texture with @@ -408,62 +435,70 @@ impl HdrP010Converter { w: u32, h: u32, ) -> Result<()> { - let y_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16_UNORM)?; - let uv_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16G16_UNORM)?; + // 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. + 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); + // 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); + ctx.PSSetShaderResources(0, Some(&[Some(src_srv.clone())])); + ctx.PSSetSamplers(0, Some(&[Some(self.sampler.clone())])); + ctx.IASetInputLayout(None); + ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + // --- LUMA pass: full-res, plane 0 --- + let vp_y = D3D11_VIEWPORT { + TopLeftX: 0.0, + TopLeftY: 0.0, + Width: w as f32, + Height: h as f32, + MinDepth: 0.0, + MaxDepth: 1.0, + }; + ctx.RSSetViewports(Some(&[vp_y])); + ctx.OMSetRenderTargets(Some(&[Some(y_rtv.clone())]), None); + ctx.PSSetShader(&self.ps_y, None); + ctx.Draw(3, 0); + ctx.OMSetRenderTargets(Some(&[None]), None); + + // --- CHROMA pass: half-res, plane 1 --- + let vp_uv = D3D11_VIEWPORT { + TopLeftX: 0.0, + TopLeftY: 0.0, + Width: (w / 2) as f32, + Height: (h / 2) as f32, + MinDepth: 0.0, + MaxDepth: 1.0, + }; + ctx.RSSetViewports(Some(&[vp_uv])); + ctx.OMSetRenderTargets(Some(&[Some(uv_rtv.clone())]), None); + ctx.PSSetShader(&self.ps_uv, None); + ctx.PSSetConstantBuffers(0, Some(&[Some(self.cbuf.clone())])); + ctx.Draw(3, 0); + + // Unbind for the next frame's re-RTV / NVENC read. + ctx.OMSetRenderTargets(Some(&[None]), None); + ctx.PSSetShaderResources(0, Some(&[None])); + Ok(()) } - - // Shared pipeline state. - ctx.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite - ctx.VSSetShader(&self.vs, None); - ctx.PSSetShaderResources(0, Some(&[Some(src_srv.clone())])); - ctx.PSSetSamplers(0, Some(&[Some(self.sampler.clone())])); - ctx.IASetInputLayout(None); - ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - - // --- LUMA pass: full-res, plane 0 --- - let vp_y = D3D11_VIEWPORT { - TopLeftX: 0.0, - TopLeftY: 0.0, - Width: w as f32, - Height: h as f32, - MinDepth: 0.0, - MaxDepth: 1.0, - }; - ctx.RSSetViewports(Some(&[vp_y])); - ctx.OMSetRenderTargets(Some(&[Some(y_rtv.clone())]), None); - ctx.PSSetShader(&self.ps_y, None); - ctx.Draw(3, 0); - ctx.OMSetRenderTargets(Some(&[None]), None); - - // --- CHROMA pass: half-res, plane 1 --- - let vp_uv = D3D11_VIEWPORT { - TopLeftX: 0.0, - TopLeftY: 0.0, - Width: (w / 2) as f32, - Height: (h / 2) as f32, - MinDepth: 0.0, - MaxDepth: 1.0, - }; - ctx.RSSetViewports(Some(&[vp_uv])); - ctx.OMSetRenderTargets(Some(&[Some(uv_rtv.clone())]), None); - ctx.PSSetShader(&self.ps_uv, None); - ctx.PSSetConstantBuffers(0, Some(&[Some(self.cbuf.clone())])); - ctx.Draw(3, 0); - - // Unbind for the next frame's re-RTV / NVENC read. - ctx.OMSetRenderTargets(Some(&[None]), None); - ctx.PSSetShaderResources(0, Some(&[None])); - Ok(()) } } @@ -604,33 +639,37 @@ pub(crate) struct BgraToYuvPlanes { impl BgraToYuvPlanes { pub(crate) unsafe fn new(device: &ID3D11Device, hdr: bool, chroma444: bool) -> Result { - let (y_src, uv_src) = match (hdr, chroma444) { - (false, false) => (PYRO_Y_PS.to_string(), PYRO_UV_PS.to_string()), - (false, true) => (PYRO_Y_PS.to_string(), PYRO_UV444_PS.to_string()), - (true, false) => ( - PYRO_HDR_Y_PS.replace("#include_common", PYRO_HDR_COMMON), - PYRO_HDR_UV_PS.replace("#include_common", PYRO_HDR_COMMON), - ), - (true, true) => ( - PYRO_HDR_Y_PS.replace("#include_common", PYRO_HDR_COMMON), - PYRO_HDR_UV444_PS.replace("#include_common", PYRO_HDR_COMMON), - ), - }; - let vsb = compile_shader(HDR_VS, s!("main"), s!("vs_5_0"))?; - let yb = compile_shader(&y_src, s!("main"), s!("ps_5_0"))?; - let uvb = compile_shader(&uv_src, s!("main"), s!("ps_5_0"))?; - let mut vs = None; - device.CreateVertexShader(&vsb, None, Some(&mut vs))?; - let mut ps_y = None; - device.CreatePixelShader(&yb, None, Some(&mut ps_y))?; - let mut ps_uv = None; - device.CreatePixelShader(&uvb, None, Some(&mut ps_uv))?; - Ok(Self { - vs: vs.context("pyro vs")?, - ps_y: ps_y.context("pyro y ps")?, - ps_uv: ps_uv.context("pyro uv ps")?, - chroma444, - }) + // SAFETY: as `HdrP010Converter::new` — `?`-checked D3D11 shader creation on the live + // `device` borrow, with `s!()` literals into `compile_shader` and live out-params. + unsafe { + let (y_src, uv_src) = match (hdr, chroma444) { + (false, false) => (PYRO_Y_PS.to_string(), PYRO_UV_PS.to_string()), + (false, true) => (PYRO_Y_PS.to_string(), PYRO_UV444_PS.to_string()), + (true, false) => ( + PYRO_HDR_Y_PS.replace("#include_common", PYRO_HDR_COMMON), + PYRO_HDR_UV_PS.replace("#include_common", PYRO_HDR_COMMON), + ), + (true, true) => ( + PYRO_HDR_Y_PS.replace("#include_common", PYRO_HDR_COMMON), + PYRO_HDR_UV444_PS.replace("#include_common", PYRO_HDR_COMMON), + ), + }; + let vsb = compile_shader(HDR_VS, s!("main"), s!("vs_5_0"))?; + let yb = compile_shader(&y_src, s!("main"), s!("ps_5_0"))?; + let uvb = compile_shader(&uv_src, s!("main"), s!("ps_5_0"))?; + let mut vs = None; + device.CreateVertexShader(&vsb, None, Some(&mut vs))?; + let mut ps_y = None; + device.CreatePixelShader(&yb, None, Some(&mut ps_y))?; + let mut ps_uv = None; + device.CreatePixelShader(&uvb, None, Some(&mut ps_uv))?; + Ok(Self { + vs: vs.context("pyro vs")?, + ps_y: ps_y.context("pyro y ps")?, + ps_uv: ps_uv.context("pyro uv ps")?, + chroma444, + }) + } } /// Convert `src_srv` (BGRA slot for SDR / scRGB FP16 slot for HDR, WxH) → `y_rtv` (full-res Y @@ -646,47 +685,52 @@ impl BgraToYuvPlanes { w: u32, h: u32, ) -> Result<()> { - ctx.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite - ctx.VSSetShader(&self.vs, None); - ctx.PSSetShaderResources(0, Some(&[Some(src_srv.clone())])); - ctx.IASetInputLayout(None); - ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + // SAFETY: D3D11 state-setting plus two `Draw`s on the caller's live immediate-context + // borrow, 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 { + ctx.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite + ctx.VSSetShader(&self.vs, None); + ctx.PSSetShaderResources(0, Some(&[Some(src_srv.clone())])); + ctx.IASetInputLayout(None); + ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - // LUMA pass: full-res → the R8 Y texture. - ctx.RSSetViewports(Some(&[D3D11_VIEWPORT { - TopLeftX: 0.0, - TopLeftY: 0.0, - Width: w as f32, - Height: h as f32, - MinDepth: 0.0, - MaxDepth: 1.0, - }])); - ctx.OMSetRenderTargets(Some(&[Some(y_rtv.clone())]), None); - ctx.PSSetShader(&self.ps_y, None); - ctx.Draw(3, 0); - ctx.OMSetRenderTargets(Some(&[None]), None); + // LUMA pass: full-res → the R8 Y texture. + ctx.RSSetViewports(Some(&[D3D11_VIEWPORT { + TopLeftX: 0.0, + TopLeftY: 0.0, + Width: w as f32, + Height: h as f32, + MinDepth: 0.0, + MaxDepth: 1.0, + }])); + ctx.OMSetRenderTargets(Some(&[Some(y_rtv.clone())]), None); + ctx.PSSetShader(&self.ps_y, None); + ctx.Draw(3, 0); + ctx.OMSetRenderTargets(Some(&[None]), None); - // CHROMA pass: half-res (4:2:0) or full-res (4:4:4) → the CbCr texture. - let (cw, ch) = if self.chroma444 { - (w, h) - } else { - (w / 2, h / 2) - }; - ctx.RSSetViewports(Some(&[D3D11_VIEWPORT { - TopLeftX: 0.0, - TopLeftY: 0.0, - Width: cw as f32, - Height: ch as f32, - MinDepth: 0.0, - MaxDepth: 1.0, - }])); - ctx.OMSetRenderTargets(Some(&[Some(cbcr_rtv.clone())]), None); - ctx.PSSetShader(&self.ps_uv, None); - ctx.Draw(3, 0); + // CHROMA pass: half-res (4:2:0) or full-res (4:4:4) → the CbCr texture. + let (cw, ch) = if self.chroma444 { + (w, h) + } else { + (w / 2, h / 2) + }; + ctx.RSSetViewports(Some(&[D3D11_VIEWPORT { + TopLeftX: 0.0, + TopLeftY: 0.0, + Width: cw as f32, + Height: ch as f32, + MinDepth: 0.0, + MaxDepth: 1.0, + }])); + ctx.OMSetRenderTargets(Some(&[Some(cbcr_rtv.clone())]), None); + ctx.PSSetShader(&self.ps_uv, None); + ctx.Draw(3, 0); - ctx.OMSetRenderTargets(Some(&[None]), None); - ctx.PSSetShaderResources(0, Some(&[None])); - Ok(()) + ctx.OMSetRenderTargets(Some(&[None]), None); + ctx.PSSetShaderResources(0, Some(&[None])); + Ok(()) + } } } @@ -1264,49 +1308,57 @@ impl VideoConverter { height: u32, scrgb_input: bool, ) -> Result { - let vdev: ID3D11VideoDevice = device.cast().context("device -> ID3D11VideoDevice")?; - let vctx: ID3D11VideoContext1 = context.cast().context("context -> ID3D11VideoContext1")?; - let rate = DXGI_RATIONAL { - Numerator: 240, - Denominator: 1, - }; - let desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC { - InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, - InputFrameRate: rate, - InputWidth: width, - InputHeight: height, - OutputFrameRate: rate, - OutputWidth: width, - OutputHeight: height, - Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL, - }; - let enumr = vdev - .CreateVideoProcessorEnumerator(&desc) - .context("CreateVideoProcessorEnumerator")?; - let vp = vdev - .CreateVideoProcessor(&enumr, 0) - .context("CreateVideoProcessor")?; + // SAFETY: the `cast()`s and the `?`-checked video-device factory calls run on the caller's + // live `device`/`context` borrows; `&desc` is a fully-initialized stack + // `D3D11_VIDEO_PROCESSOR_CONTENT_DESC` read only for the duration of the call, and the + // colour-space/frame-format setters take the just-created processor by borrow plus plain + // enum values. + unsafe { + let vdev: ID3D11VideoDevice = device.cast().context("device -> ID3D11VideoDevice")?; + let vctx: ID3D11VideoContext1 = + context.cast().context("context -> ID3D11VideoContext1")?; + let rate = DXGI_RATIONAL { + Numerator: 240, + Denominator: 1, + }; + let desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC { + InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, + InputFrameRate: rate, + InputWidth: width, + InputHeight: height, + OutputFrameRate: rate, + OutputWidth: width, + OutputHeight: height, + Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL, + }; + let enumr = vdev + .CreateVideoProcessorEnumerator(&desc) + .context("CreateVideoProcessorEnumerator")?; + let vp = vdev + .CreateVideoProcessor(&enumr, 0) + .context("CreateVideoProcessor")?; - // Full-range RGB in → studio-range BT.709 NV12 out. Input gamma follows the ring format: - // scRGB linear (G10) for the FP16 HDR ring, sRGB (G22) for the 8-bit BGRA SDR ring. The - // output is always BT.709 SDR (the video processor tone-maps the scRGB case). - let in_cs = if scrgb_input { - DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 - } else { - DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709 - }; - 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. - vctx.VideoProcessorSetStreamFrameFormat(&vp, 0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE); + // Full-range RGB in → studio-range BT.709 NV12 out. Input gamma follows the ring format: + // scRGB linear (G10) for the FP16 HDR ring, sRGB (G22) for the 8-bit BGRA SDR ring. The + // output is always BT.709 SDR (the video processor tone-maps the scRGB case). + let in_cs = if scrgb_input { + DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 + } else { + DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709 + }; + 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. + vctx.VideoProcessorSetStreamFrameFormat(&vp, 0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE); - Ok(Self { - vdev, - vctx, - enumr, - vp, - }) + Ok(Self { + vdev, + vctx, + enumr, + vp, + }) + } } /// Convert `input` (BGRA or scRGB FP16) → `output` (NV12 or P010) on the video engine. Views are @@ -1316,47 +1368,54 @@ impl VideoConverter { input: &ID3D11Texture2D, output: &ID3D11Texture2D, ) -> Result<()> { - let in_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC { - FourCC: 0, - ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D, - Anonymous: D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0 { - Texture2D: D3D11_TEX2D_VPIV { - MipSlice: 0, - ArraySlice: 0, + // SAFETY: both view creations are `?`-checked calls on `self.vdev` with fully-initialized + // stack descriptors and live out-params. `stream.pInputSurface` is a `ManuallyDrop` of the + // input view just created: `VideoProcessorBlt` only BORROWS it (a COM in-param never transfers + // ownership), and the explicit `into_inner` drop below releases that reference exactly once on + // both the success and the failure path. `slice::from_ref(&stream)` borrows the live local. + unsafe { + let in_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC { + FourCC: 0, + ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D, + Anonymous: D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0 { + Texture2D: D3D11_TEX2D_VPIV { + MipSlice: 0, + ArraySlice: 0, + }, }, - }, - }; - let mut in_view: Option = None; - self.vdev - .CreateVideoProcessorInputView(input, &self.enumr, &in_desc, Some(&mut in_view)) - .context("CreateVideoProcessorInputView")?; + }; + let mut in_view: Option = None; + self.vdev + .CreateVideoProcessorInputView(input, &self.enumr, &in_desc, Some(&mut in_view)) + .context("CreateVideoProcessorInputView")?; - let out_desc = D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC { - ViewDimension: D3D11_VPOV_DIMENSION_TEXTURE2D, - Anonymous: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0 { - Texture2D: D3D11_TEX2D_VPOV { MipSlice: 0 }, - }, - }; - let mut out_view: Option = None; - self.vdev - .CreateVideoProcessorOutputView(output, &self.enumr, &out_desc, Some(&mut out_view)) - .context("CreateVideoProcessorOutputView")?; - let out_view = out_view.context("null output view")?; + let out_desc = D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC { + ViewDimension: D3D11_VPOV_DIMENSION_TEXTURE2D, + Anonymous: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0 { + Texture2D: D3D11_TEX2D_VPOV { MipSlice: 0 }, + }, + }; + let mut out_view: Option = None; + self.vdev + .CreateVideoProcessorOutputView(output, &self.enumr, &out_desc, Some(&mut out_view)) + .context("CreateVideoProcessorOutputView")?; + let out_view = out_view.context("null output view")?; - let stream = D3D11_VIDEO_PROCESSOR_STREAM { - Enable: true.into(), - pInputSurface: std::mem::ManuallyDrop::new(in_view), - ..Default::default() - }; - let blt = - self.vctx - .VideoProcessorBlt(&self.vp, &out_view, 0, std::slice::from_ref(&stream)); - // COM in-params never transfer ownership: the Blt only borrowed the input view, and the - // struct's `ManuallyDrop` field suppressed its release — drop it by hand, success or not. - // (Skipping this leaked one view + its UMD allocation PER CONVERTED FRAME — the SDR hot - // path; D3D11 defers the actual destruction until the GPU is done with the blit.) - drop(std::mem::ManuallyDrop::into_inner(stream.pInputSurface)); - blt.context("VideoProcessorBlt") + let stream = D3D11_VIDEO_PROCESSOR_STREAM { + Enable: true.into(), + pInputSurface: std::mem::ManuallyDrop::new(in_view), + ..Default::default() + }; + let blt = + self.vctx + .VideoProcessorBlt(&self.vp, &out_view, 0, std::slice::from_ref(&stream)); + // COM in-params never transfer ownership: the Blt only borrowed the input view, and the + // struct's `ManuallyDrop` field suppressed its release — drop it by hand, success or not. + // (Skipping this leaked one view + its UMD allocation PER CONVERTED FRAME — the SDR hot + // path; D3D11 defers the actual destruction until the GPU is done with the blit.) + drop(std::mem::ManuallyDrop::into_inner(stream.pInputSurface)); + blt.context("VideoProcessorBlt") + } } } diff --git a/crates/pf-capture/src/windows/idd_push.rs b/crates/pf-capture/src/windows/idd_push.rs index e4b06ed4..706ad5d6 100644 --- a/crates/pf-capture/src/windows/idd_push.rs +++ b/crates/pf-capture/src/windows/idd_push.rs @@ -582,21 +582,28 @@ unsafe impl Send for IddPushCapturer {} /// 2026-07-03), so it cannot dup the handles out either. History: `Global\`-named + world-openable /// (`WD`, security-review 2026-06-28 #5) → SY+LS-scoped → nameless → now SY-only. `psd` must outlive /// `sa`. See `design/idd-push-security.md`. -unsafe fn shared_object_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTOR)> { - let mut psd = PSECURITY_DESCRIPTOR::default(); - ConvertStringSecurityDescriptorToSecurityDescriptorW( - w!("D:P(A;;GA;;;SY)"), - SDDL_REVISION_1, - &mut psd, - None, - ) - .context("build SDDL for IDD-push shared objects")?; - let sa = SECURITY_ATTRIBUTES { - nLength: std::mem::size_of::() as u32, - lpSecurityDescriptor: psd.0, - bInheritHandle: false.into(), - }; - Ok((sa, psd)) +fn shared_object_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTOR)> { + // SAFETY: `ConvertStringSecurityDescriptorToSecurityDescriptorW` reads the `w!()` literal + // and writes the descriptor it allocates into the live local `psd`; `?` rejects a failure + // before `psd` is read. The `SECURITY_ATTRIBUTES` returned alongside merely CARRIES `psd.0` as + // a raw pointer — keeping the two paired (and freeing the descriptor) is the caller's unsafe + // business, so this fn itself has no precondition to state and needs no `unsafe` marker. + unsafe { + let mut psd = PSECURITY_DESCRIPTOR::default(); + ConvertStringSecurityDescriptorToSecurityDescriptorW( + w!("D:P(A;;GA;;;SY)"), + SDDL_REVISION_1, + &mut psd, + None, + ) + .context("build SDDL for IDD-push shared objects")?; + let sa = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: psd.0, + bInheritHandle: false.into(), + }; + Ok((sa, psd)) + } } impl IddPushCapturer { @@ -610,56 +617,64 @@ impl IddPushCapturer { h: u32, format: DXGI_FORMAT, ) -> Result> { - let (sa, _psd) = shared_object_sa()?; - let mut slots = Vec::new(); - for _ in 0..RING_LEN { - let desc = D3D11_TEXTURE2D_DESC { - Width: w, - Height: h, - MipLevels: 1, - ArraySize: 1, - // Match the OS-composed swap-chain surfaces so the driver's CopyResource into the slot + - // its format-guard both succeed. - Format: format, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, - CPUAccessFlags: 0, - MiscFlags: (D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0 - | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0) as u32, - }; - let mut tex: Option = None; - device - .CreateTexture2D(&desc, None, Some(&mut tex)) - .context("CreateTexture2D(IDD-push ring slot)")?; - let tex = tex.context("null ring texture")?; - let res1: IDXGIResource1 = tex.cast()?; - let shared = res1 - .CreateSharedHandle( - Some(&sa as *const SECURITY_ATTRIBUTES), - DXGI_SHARED_RESOURCE_RW, - PCWSTR::null(), // UNNAMED — reachable only through the broker's duplicate - ) - .context("CreateSharedHandle(IDD-push ring slot)")?; - // Own the shared handle so the slot's `Drop` closes it via RAII (was a manual `CloseHandle`). - let shared = OwnedHandle::from_raw_handle(shared.0 as _); - let mutex: IDXGIKeyedMutex = tex.cast()?; - let mut srv: Option = None; - device - .CreateShaderResourceView(&tex, None, Some(&mut srv)) - .context("CreateShaderResourceView(IDD-push ring slot)")?; - let srv = srv.context("null slot srv")?; - slots.push(HostSlot { - tex, - mutex, - shared, - srv, - }); + // SAFETY: every D3D11/DXGI call is `?`-checked on the live `device` borrow, over + // fully-initialized stack descriptors and live out-params; `&sa` stays valid for the whole loop + // because `_psd`, the security descriptor backing it, is held in scope alongside. + // `OwnedHandle::from_raw_handle` adopts the handle `CreateSharedHandle` JUST minted for this + // slot — a unique, still-open NT handle owned by this process — making the slot its sole owner. + unsafe { + let (sa, _psd) = shared_object_sa()?; + let mut slots = Vec::new(); + for _ in 0..RING_LEN { + let desc = D3D11_TEXTURE2D_DESC { + Width: w, + Height: h, + MipLevels: 1, + ArraySize: 1, + // Match the OS-composed swap-chain surfaces so the driver's CopyResource into the slot + + // its format-guard both succeed. + Format: format, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, + CPUAccessFlags: 0, + MiscFlags: (D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0 + | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0) + as u32, + }; + let mut tex: Option = None; + device + .CreateTexture2D(&desc, None, Some(&mut tex)) + .context("CreateTexture2D(IDD-push ring slot)")?; + let tex = tex.context("null ring texture")?; + let res1: IDXGIResource1 = tex.cast()?; + let shared = res1 + .CreateSharedHandle( + Some(&sa as *const SECURITY_ATTRIBUTES), + DXGI_SHARED_RESOURCE_RW, + PCWSTR::null(), // UNNAMED — reachable only through the broker's duplicate + ) + .context("CreateSharedHandle(IDD-push ring slot)")?; + // Own the shared handle so the slot's `Drop` closes it via RAII (was a manual `CloseHandle`). + let shared = OwnedHandle::from_raw_handle(shared.0 as _); + let mutex: IDXGIKeyedMutex = tex.cast()?; + let mut srv: Option = None; + device + .CreateShaderResourceView(&tex, None, Some(&mut srv)) + .context("CreateShaderResourceView(IDD-push ring slot)")?; + let srv = srv.context("null slot srv")?; + slots.push(HostSlot { + tex, + mutex, + shared, + srv, + }); + } + Ok(slots) } - Ok(slots) } /// Open the IDD-push capturer. On success the caller's `keepalive` is attached (the capturer owns the @@ -1739,46 +1754,52 @@ impl IddPushCapturer { /// Runs on the owning capture/encode thread that holds the immediate context; forms no lasting /// borrow of `self`'s COM objects. unsafe fn pyro_fence_signal(&mut self) -> Result, u64)>> { - if !self.pyrowave { - return Ok(None); - } - if self.pyro_fence.is_none() { - let dev5: ID3D11Device5 = self - .device + // SAFETY: per the contract above this runs on the owning capture/encode thread, which holds + // the immediate context. Every call is a `?`-checked COM method on `self`'s live device or + // context (or on a `cast()` of one), and `CreateSharedHandle` yields a fresh NT handle whose + // raw value is only STORED here — never dereferenced, and never closed on this path. + unsafe { + if !self.pyrowave { + return Ok(None); + } + if self.pyro_fence.is_none() { + let dev5: ID3D11Device5 = self + .device + .cast() + .context("ID3D11Device -> ID3D11Device5 (shared fence)")?; + // windows-rs returns COM interfaces via an out-param (unlike the HANDLE-returning + // CreateSharedHandle below). + let mut fence_out: Option = None; + dev5.CreateFence(0, D3D11_FENCE_FLAG_SHARED, &mut fence_out) + .context("CreateFence(D3D11_FENCE_FLAG_SHARED)")?; + let fence = fence_out.context("null D3D11 fence")?; + // GENERIC_ALL (0x1000_0000) — the access the pyrowave interop test hands the handle. + let handle: HANDLE = fence + .CreateSharedHandle(None, 0x1000_0000, PCWSTR::null()) + .context("ID3D11Fence::CreateSharedHandle")?; + self.pyro_fence = Some(fence); + self.pyro_fence_handle = Some(handle.0 as isize); + self.pyro_fence_value = 0; + } + self.pyro_fence_value += 1; + let value = self.pyro_fence_value; + let ctx4: ID3D11DeviceContext4 = self + .context .cast() - .context("ID3D11Device -> ID3D11Device5 (shared fence)")?; - // windows-rs returns COM interfaces via an out-param (unlike the HANDLE-returning - // CreateSharedHandle below). - let mut fence_out: Option = None; - dev5.CreateFence(0, D3D11_FENCE_FLAG_SHARED, &mut fence_out) - .context("CreateFence(D3D11_FENCE_FLAG_SHARED)")?; - let fence = fence_out.context("null D3D11 fence")?; - // GENERIC_ALL (0x1000_0000) — the access the pyrowave interop test hands the handle. - let handle: HANDLE = fence - .CreateSharedHandle(None, 0x1000_0000, PCWSTR::null()) - .context("ID3D11Fence::CreateSharedHandle")?; - self.pyro_fence = Some(fence); - self.pyro_fence_handle = Some(handle.0 as isize); - self.pyro_fence_value = 0; + .context("ID3D11DeviceContext -> ID3D11DeviceContext4 (fence signal)")?; + { + let fence = self.pyro_fence.as_ref().expect("fence just created"); + ctx4.Signal(fence, value) + .context("ID3D11 fence Signal after convert")?; + } + // Submit the queued convert + signal so the encoder's Vulkan timeline wait can resolve. + self.context.Flush(); + // Pass the persistent shared handle EVERY frame (not once): the encoder can be rebuilt on a + // client mode-switch, and a rebuilt encoder needs to re-import the fence into its fresh Vulkan + // device. The encoder imports only when it has no timeline yet (and DUPLICATES the handle so + // this original stays valid for the next rebuild). + Ok(Some((self.pyro_fence_handle, value))) } - self.pyro_fence_value += 1; - let value = self.pyro_fence_value; - let ctx4: ID3D11DeviceContext4 = self - .context - .cast() - .context("ID3D11DeviceContext -> ID3D11DeviceContext4 (fence signal)")?; - { - let fence = self.pyro_fence.as_ref().expect("fence just created"); - ctx4.Signal(fence, value) - .context("ID3D11 fence Signal after convert")?; - } - // Submit the queued convert + signal so the encoder's Vulkan timeline wait can resolve. - self.context.Flush(); - // Pass the persistent shared handle EVERY frame (not once): the encoder can be rebuilt on a - // client mode-switch, and a rebuilt encoder needs to re-import the fence into its fresh Vulkan - // device. The encoder imports only when it has no timeline yet (and DUPLICATES the handle so - // this original stays valid for the next rebuild). - Ok(Some((self.pyro_fence_handle, value))) } /// The (serial, x, y, visible) of the CURRENT polled cursor — the composite-regen change @@ -1803,112 +1824,120 @@ impl IddPushCapturer { &mut self, slot_tex: &ID3D11Texture2D, ) -> Option<(ID3D11Texture2D, ID3D11ShaderResourceView)> { - let fmt = self.ring_format(); - // (Re)build the scratch at the current ring geometry. - let stale = self - .blend_scratch - .as_ref() - .is_none_or(|(_, _, w, h, f)| (*w, *h, *f) != (self.width, self.height, fmt)); - if stale { - self.blend_scratch = None; - let desc = D3D11_TEXTURE2D_DESC { - Width: self.width, - Height: self.height, - MipLevels: 1, - ArraySize: 1, - Format: fmt, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, - ..Default::default() - }; - let mut tex: Option = None; - let built = self - .device - .CreateTexture2D(&desc, None, Some(&mut tex)) - .ok() - .and(tex) - .and_then(|t| { - let mut srv: Option = None; - self.device - .CreateShaderResourceView(&t, None, Some(&mut srv)) - .ok() - .and(srv) - .map(|v| (t, v)) - }); - match built { - Some((t, v)) => { - 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 { - self.cursor_blend_failed = true; - tracing::warn!( - "cursor blend scratch creation failed — capture-model frames stay \ - pointer-less this session" - ); - } - return None; - } - } - } - let (tex, srv, ..) = self.blend_scratch.as_ref().expect("just ensured"); - let (tex, srv) = (tex.clone(), srv.clone()); - self.context.CopyResource(&tex, slot_tex); - // Blend the pointer (visible shapes only; hidden = the copy alone is the frame). - let overlay = self.cursor_poll.as_ref().and_then(|p| p.read()); - self.last_blend_key = overlay.as_ref().map(|o| (o.serial, o.x, o.y, o.visible)); - if let Some(ov) = overlay.filter(|o| o.visible) { - if self.cursor_blend.is_none() && !self.cursor_blend_failed { - match cursor_blend::CursorBlendPass::new(&self.device) { - Ok(p) => self.cursor_blend = Some(p), - Err(e) => { - self.cursor_blend_failed = true; - tracing::warn!( - "cursor blend pass build failed — capture-model frames stay \ - pointer-less this session: {e:#}" - ); - } - } - } - if let Some(pass) = self.cursor_blend.as_mut() { - // FP16 ring = scRGB linear composition (HDR): linearize the sRGB shape and - // scale it to the target's SDR white so it matches the desktop around it. - let scale = if self.display_hdr { - self.sdr_white_scale - } else { - 0.0 + // SAFETY: per the contract above, D3D11 calls on the owning thread's device + immediate + // context while the slot's keyed mutex is held. `CreateTexture2D`/`CreateShaderResourceView` + // 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. + unsafe { + let fmt = self.ring_format(); + // (Re)build the scratch at the current ring geometry. + let stale = self + .blend_scratch + .as_ref() + .is_none_or(|(_, _, w, h, f)| (*w, *h, *f) != (self.width, self.height, fmt)); + if stale { + self.blend_scratch = None; + let desc = D3D11_TEXTURE2D_DESC { + Width: self.width, + Height: self.height, + MipLevels: 1, + ArraySize: 1, + Format: fmt, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, + ..Default::default() }; - if let Err(e) = pass.blend(&self.device, &self.context, &tex, &ov, scale) { - if !self.cursor_blend_failed { - self.cursor_blend_failed = true; - tracing::warn!("cursor blend draw failed — pointer-less frames: {e:#}"); + let mut tex: Option = None; + let built = self + .device + .CreateTexture2D(&desc, None, Some(&mut tex)) + .ok() + .and(tex) + .and_then(|t| { + let mut srv: Option = None; + self.device + .CreateShaderResourceView(&t, None, Some(&mut srv)) + .ok() + .and(srv) + .map(|v| (t, v)) + }); + match built { + Some((t, v)) => { + 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 { + self.cursor_blend_failed = true; + tracing::warn!( + "cursor blend scratch creation failed — capture-model frames stay \ + pointer-less this session" + ); + } + return None; } } } + let (tex, srv, ..) = self.blend_scratch.as_ref().expect("just ensured"); + let (tex, srv) = (tex.clone(), srv.clone()); + self.context.CopyResource(&tex, slot_tex); + // Blend the pointer (visible shapes only; hidden = the copy alone is the frame). + let overlay = self.cursor_poll.as_ref().and_then(|p| p.read()); + self.last_blend_key = overlay.as_ref().map(|o| (o.serial, o.x, o.y, o.visible)); + if let Some(ov) = overlay.filter(|o| o.visible) { + if self.cursor_blend.is_none() && !self.cursor_blend_failed { + match cursor_blend::CursorBlendPass::new(&self.device) { + Ok(p) => self.cursor_blend = Some(p), + Err(e) => { + self.cursor_blend_failed = true; + tracing::warn!( + "cursor blend pass build failed — capture-model frames stay \ + pointer-less this session: {e:#}" + ); + } + } + } + if let Some(pass) = self.cursor_blend.as_mut() { + // FP16 ring = scRGB linear composition (HDR): linearize the sRGB shape and + // scale it to the target's SDR white so it matches the desktop around it. + 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 { + self.cursor_blend_failed = true; + tracing::warn!("cursor blend draw failed — pointer-less frames: {e:#}"); + } + } + } + } + Some((tex, srv)) } - Some((tex, srv)) } /// The secure-desktop guard (the 0.18.0 UAC/Winlogon regression). UAC consent and Winlogon diff --git a/crates/pf-capture/src/windows/idd_push/cursor_blend.rs b/crates/pf-capture/src/windows/idd_push/cursor_blend.rs index 6c253a4d..c1dfb50d 100644 --- a/crates/pf-capture/src/windows/idd_push/cursor_blend.rs +++ b/crates/pf-capture/src/windows/idd_push/cursor_blend.rs @@ -57,57 +57,62 @@ pub(super) struct CursorBlendPass { impl CursorBlendPass { pub(super) unsafe fn new(device: &ID3D11Device) -> Result { - let vsb = crate::dxgi::compile_shader(crate::dxgi::HDR_VS, s!("main"), s!("vs_5_0"))?; - let psb = crate::dxgi::compile_shader(CURSOR_PS, s!("main"), s!("ps_5_0"))?; - let mut vs = None; - device.CreateVertexShader(&vsb, None, Some(&mut vs))?; - let mut ps = None; - device.CreatePixelShader(&psb, None, Some(&mut ps))?; - let sd = D3D11_SAMPLER_DESC { - // LINEAR: the quad is drawn 1:1 in frame pixels, so this only matters at the - // half-texel edges; linear keeps them soft instead of ringing. - Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR, - AddressU: D3D11_TEXTURE_ADDRESS_CLAMP, - AddressV: D3D11_TEXTURE_ADDRESS_CLAMP, - AddressW: D3D11_TEXTURE_ADDRESS_CLAMP, - ComparisonFunc: D3D11_COMPARISON_NEVER, - MaxLOD: f32::MAX, - ..Default::default() - }; - let mut sampler = None; - device.CreateSamplerState(&sd, Some(&mut sampler))?; - // Straight-alpha over: dst.rgb = src.rgb*a + dst.rgb*(1-a); keep dst alpha. - let mut bd = D3D11_BLEND_DESC::default(); - bd.RenderTarget[0] = D3D11_RENDER_TARGET_BLEND_DESC { - BlendEnable: true.into(), - SrcBlend: D3D11_BLEND_SRC_ALPHA, - DestBlend: D3D11_BLEND_INV_SRC_ALPHA, - BlendOp: D3D11_BLEND_OP_ADD, - SrcBlendAlpha: D3D11_BLEND_ONE, - DestBlendAlpha: D3D11_BLEND_ONE, - BlendOpAlpha: D3D11_BLEND_OP_ADD, - RenderTargetWriteMask: 0x0F, - }; - let mut blend = None; - device.CreateBlendState(&bd, Some(&mut blend))?; - let cbd = D3D11_BUFFER_DESC { - ByteWidth: 16, // float to_linear + float3 pad - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - ..Default::default() - }; - let mut cbuf = None; - device.CreateBuffer(&cbd, None, Some(&mut cbuf))?; - Ok(Self { - vs: vs.context("cursor blend vs")?, - ps: ps.context("cursor blend ps")?, - sampler: sampler.context("cursor blend sampler")?, - blend: blend.context("cursor blend state")?, - cbuf: cbuf.context("cursor blend cbuf")?, - cbuf_scale: None, - shape: None, - }) + // SAFETY: `?`-checked D3D11 resource creation on the live `device` borrow, over + // fully-initialized stack descriptors and live out-params; `compile_shader` receives `s!()` + // literals (its contract). + unsafe { + let vsb = crate::dxgi::compile_shader(crate::dxgi::HDR_VS, s!("main"), s!("vs_5_0"))?; + let psb = crate::dxgi::compile_shader(CURSOR_PS, s!("main"), s!("ps_5_0"))?; + let mut vs = None; + device.CreateVertexShader(&vsb, None, Some(&mut vs))?; + let mut ps = None; + device.CreatePixelShader(&psb, None, Some(&mut ps))?; + let sd = D3D11_SAMPLER_DESC { + // LINEAR: the quad is drawn 1:1 in frame pixels, so this only matters at the + // half-texel edges; linear keeps them soft instead of ringing. + Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR, + AddressU: D3D11_TEXTURE_ADDRESS_CLAMP, + AddressV: D3D11_TEXTURE_ADDRESS_CLAMP, + AddressW: D3D11_TEXTURE_ADDRESS_CLAMP, + ComparisonFunc: D3D11_COMPARISON_NEVER, + MaxLOD: f32::MAX, + ..Default::default() + }; + let mut sampler = None; + device.CreateSamplerState(&sd, Some(&mut sampler))?; + // Straight-alpha over: dst.rgb = src.rgb*a + dst.rgb*(1-a); keep dst alpha. + let mut bd = D3D11_BLEND_DESC::default(); + bd.RenderTarget[0] = D3D11_RENDER_TARGET_BLEND_DESC { + BlendEnable: true.into(), + SrcBlend: D3D11_BLEND_SRC_ALPHA, + DestBlend: D3D11_BLEND_INV_SRC_ALPHA, + BlendOp: D3D11_BLEND_OP_ADD, + SrcBlendAlpha: D3D11_BLEND_ONE, + DestBlendAlpha: D3D11_BLEND_ONE, + BlendOpAlpha: D3D11_BLEND_OP_ADD, + RenderTargetWriteMask: 0x0F, + }; + let mut blend = None; + device.CreateBlendState(&bd, Some(&mut blend))?; + let cbd = D3D11_BUFFER_DESC { + ByteWidth: 16, // float to_linear + float3 pad + Usage: D3D11_USAGE_DYNAMIC, + BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + ..Default::default() + }; + let mut cbuf = None; + device.CreateBuffer(&cbd, None, Some(&mut cbuf))?; + Ok(Self { + vs: vs.context("cursor blend vs")?, + ps: ps.context("cursor blend ps")?, + sampler: sampler.context("cursor blend sampler")?, + blend: blend.context("cursor blend state")?, + cbuf: cbuf.context("cursor blend cbuf")?, + cbuf_scale: None, + shape: None, + }) + } } /// Upload `ov`'s bitmap if its serial is new; reuse the cached SRV otherwise. @@ -116,42 +121,48 @@ impl CursorBlendPass { device: &ID3D11Device, ov: &pf_frame::CursorOverlay, ) -> Result<()> { - if self.shape.as_ref().is_some_and(|(s, ..)| *s == ov.serial) { - return Ok(()); + // SAFETY: `CreateTexture2D`/`CreateShaderResourceView` are `?`-checked calls on the live + // `device` borrow. `init.pSysMem` points into `ov.rgba`, which the length check above proves + // holds at least `ov.w * ov.h * 4` bytes for the declared `SysMemPitch = ov.w * 4`, and which + // outlives the synchronous upload. + unsafe { + if self.shape.as_ref().is_some_and(|(s, ..)| *s == ov.serial) { + return Ok(()); + } + if ov.rgba.len() < (ov.w as usize) * (ov.h as usize) * 4 || ov.w == 0 || ov.h == 0 { + bail!("malformed cursor overlay ({}x{})", ov.w, ov.h); + } + let desc = D3D11_TEXTURE2D_DESC { + Width: ov.w, + Height: ov.h, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_R8G8B8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, + ..Default::default() + }; + let init = D3D11_SUBRESOURCE_DATA { + pSysMem: ov.rgba.as_ptr().cast(), + SysMemPitch: ov.w * 4, + SysMemSlicePitch: 0, + }; + let mut tex: Option = None; + device + .CreateTexture2D(&desc, Some(&init), Some(&mut tex)) + .context("CreateTexture2D(cursor shape)")?; + let tex = tex.context("null cursor shape texture")?; + let mut srv: Option = None; + device + .CreateShaderResourceView(&tex, None, Some(&mut srv)) + .context("CreateShaderResourceView(cursor shape)")?; + self.shape = Some((ov.serial, srv.context("null cursor shape srv")?, ov.w, ov.h)); + Ok(()) } - if ov.rgba.len() < (ov.w as usize) * (ov.h as usize) * 4 || ov.w == 0 || ov.h == 0 { - bail!("malformed cursor overlay ({}x{})", ov.w, ov.h); - } - let desc = D3D11_TEXTURE2D_DESC { - Width: ov.w, - Height: ov.h, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_R8G8B8A8_UNORM, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, - ..Default::default() - }; - let init = D3D11_SUBRESOURCE_DATA { - pSysMem: ov.rgba.as_ptr().cast(), - SysMemPitch: ov.w * 4, - SysMemSlicePitch: 0, - }; - let mut tex: Option = None; - device - .CreateTexture2D(&desc, Some(&init), Some(&mut tex)) - .context("CreateTexture2D(cursor shape)")?; - let tex = tex.context("null cursor shape texture")?; - let mut srv: Option = None; - device - .CreateShaderResourceView(&tex, None, Some(&mut srv)) - .context("CreateShaderResourceView(cursor shape)")?; - self.shape = Some((ov.serial, srv.context("null cursor shape srv")?, ov.w, ov.h)); - Ok(()) } /// Alpha-blend `ov` onto `dst` (frame-sized, RENDER_TARGET-capable — the blend scratch). @@ -167,50 +178,58 @@ impl CursorBlendPass { ov: &pf_frame::CursorOverlay, linear_scale: f32, ) -> Result<()> { - self.ensure_shape(device, ov)?; - let (_, srv, w, h) = self.shape.as_ref().expect("shape just ensured"); - if self.cbuf_scale != Some(linear_scale) { - let cb: [f32; 4] = [linear_scale, 0.0, 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); + // SAFETY: all D3D11 work on the caller's live `device`/`ctx` borrows. The + // `copy_nonoverlapping` writes `cb.len()` `f32`s into the pointer the immediately preceding + // `Map` of `self.cbuf` (16 bytes = 4×`f32`, DYNAMIC/WRITE_DISCARD) returned, inside the + // `is_ok()` arm and before the paired `Unmap`. `ensure_shape` forwards this fn's `device` + // borrow, and every `*Set*`/`Draw` takes borrowed slices of live locals or clones of live COM + // interfaces. + unsafe { + self.ensure_shape(device, ov)?; + let (_, srv, w, h) = self.shape.as_ref().expect("shape just ensured"); + if self.cbuf_scale != Some(linear_scale) { + let cb: [f32; 4] = [linear_scale, 0.0, 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); + } + self.cbuf_scale = Some(linear_scale); } - self.cbuf_scale = Some(linear_scale); - } - let mut rtv: Option = None; - device - .CreateRenderTargetView(dst, None, Some(&mut rtv)) - .context("CreateRenderTargetView(cursor blend scratch)")?; - let rtv = rtv.context("null cursor blend rtv")?; + let mut rtv: Option = None; + device + .CreateRenderTargetView(dst, None, Some(&mut rtv)) + .context("CreateRenderTargetView(cursor blend scratch)")?; + let rtv = rtv.context("null cursor blend rtv")?; - ctx.OMSetRenderTargets(Some(&[Some(rtv)]), None); - ctx.OMSetBlendState(&self.blend, None, 0xffff_ffff); - ctx.VSSetShader(&self.vs, None); - ctx.PSSetShader(&self.ps, None); - ctx.PSSetShaderResources(0, Some(&[Some(srv.clone())])); - ctx.PSSetSamplers(0, Some(&[Some(self.sampler.clone())])); - ctx.PSSetConstantBuffers(0, Some(&[Some(self.cbuf.clone())])); - ctx.IASetInputLayout(None); - ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - // Placement IS the viewport: the VS fills it, the OS clips it to the target. - let vp = D3D11_VIEWPORT { - TopLeftX: ov.x as f32, - TopLeftY: ov.y as f32, - Width: *w as f32, - Height: *h as f32, - MinDepth: 0.0, - MaxDepth: 1.0, - }; - ctx.RSSetViewports(Some(&[vp])); - ctx.Draw(3, 0); - // Unbind so the scratch can be bound as a conversion INPUT without a hazard warning. - ctx.OMSetRenderTargets(None, None); - let none_srv: [Option; 1] = [None]; - ctx.PSSetShaderResources(0, Some(&none_srv)); - Ok(()) + ctx.OMSetRenderTargets(Some(&[Some(rtv)]), None); + ctx.OMSetBlendState(&self.blend, None, 0xffff_ffff); + ctx.VSSetShader(&self.vs, None); + ctx.PSSetShader(&self.ps, None); + ctx.PSSetShaderResources(0, Some(&[Some(srv.clone())])); + ctx.PSSetSamplers(0, Some(&[Some(self.sampler.clone())])); + ctx.PSSetConstantBuffers(0, Some(&[Some(self.cbuf.clone())])); + ctx.IASetInputLayout(None); + ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + // Placement IS the viewport: the VS fills it, the OS clips it to the target. + let vp = D3D11_VIEWPORT { + TopLeftX: ov.x as f32, + TopLeftY: ov.y as f32, + Width: *w as f32, + Height: *h as f32, + MinDepth: 0.0, + MaxDepth: 1.0, + }; + ctx.RSSetViewports(Some(&[vp])); + ctx.Draw(3, 0); + // Unbind so the scratch can be bound as a conversion INPUT without a hazard warning. + ctx.OMSetRenderTargets(None, None); + let none_srv: [Option; 1] = [None]; + ctx.PSSetShaderResources(0, Some(&none_srv)); + Ok(()) + } } } diff --git a/crates/pf-capture/src/windows/synthetic_nv12.rs b/crates/pf-capture/src/windows/synthetic_nv12.rs index f3276d3a..ae2b41e8 100644 --- a/crates/pf-capture/src/windows/synthetic_nv12.rs +++ b/crates/pf-capture/src/windows/synthetic_nv12.rs @@ -140,13 +140,17 @@ impl Capturer for SyntheticNv12Capturer { /// # Safety /// Calls DXGI factory/adapter enumeration; returns owned COM objects or an error. unsafe fn resolve_render_adapter() -> Result { - let factory: IDXGIFactory4 = CreateDXGIFactory1().context("CreateDXGIFactory1")?; - if let Some(luid) = pf_gpu::resolve_render_adapter_luid() { - if let Ok(a) = factory.EnumAdapterByLuid::(luid) { - return Ok(a); + // SAFETY: three `?`/`Ok`-checked DXGI enumeration calls over owned locals — the factory is + // created here and the adapters it returns own their own COM references. No raw pointers. + unsafe { + let factory: IDXGIFactory4 = CreateDXGIFactory1().context("CreateDXGIFactory1")?; + if let Some(luid) = pf_gpu::resolve_render_adapter_luid() { + if let Ok(a) = factory.EnumAdapterByLuid::(luid) { + return Ok(a); + } } + factory.EnumAdapters1(0).context("EnumAdapters1(0)") } - factory.EnumAdapters1(0).context("EnumAdapters1(0)") } /// Create an NV12 `Texture2D` with the given usage/CPU-access/bind flags. @@ -177,8 +181,12 @@ unsafe fn create_nv12( ..Default::default() }; let mut tex: Option = None; - device - .CreateTexture2D(&desc, None, Some(&mut tex)) - .context("CreateTexture2D(NV12)")?; + // SAFETY: one `?`-checked `CreateTexture2D` on the live `device` borrow (per the contract + // above), with a fully-initialized stack descriptor and a live `Option` out-param. + unsafe { + device + .CreateTexture2D(&desc, None, Some(&mut tex)) + .context("CreateTexture2D(NV12)")?; + } tex.context("CreateTexture2D returned a null NV12 texture") }