chore(pf-capture): make the crate verifiable — Windows CI lint + two denies (sweep Phase 0)

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 10:18:48 +02:00
co-authored by Claude Opus 5
parent bf9386ecb2
commit 6a2a153c0b
6 changed files with 749 additions and 628 deletions
+10 -6
View File
@@ -172,12 +172,15 @@ jobs:
# openh264 rebuild), and keeps everything in one C:\t\release tree. Same reason # openh264 rebuild), and keeps everything in one C:\t\release tree. Same reason
# pf-vkhdr-layer's clippy below runs --release. # pf-vkhdr-layer's clippy below runs --release.
# #
# pf-encode is linted SEPARATELY with --all-targets so its Windows `#[cfg(test)]` modules # pf-encode and pf-capture are linted SEPARATELY with --all-targets so their Windows
# are type-checked — the AMF C-ABI layout assertions (`variant_layout_matches_c` and # `#[cfg(test)]` modules are type-checked — pf-encode's AMF C-ABI layout assertions
# friends, which are the only guard on a hand-mirrored vtable ABI), the QSV tests, and the # (`variant_layout_matches_c` and friends, which are the only guard on a hand-mirrored
# PyroWave-Windows smoke test. The host lint above cannot cover them: `-p punktfunk-host` # vtable ABI), the QSV tests, the PyroWave-Windows smoke test; pf-capture's `StallWatch`
# only builds pf-encode as a dependency, so its test targets are never compiled, and that # tests, the DXGI HDR self-tests and the cursor-conversion tables. The host lint above
# blind spot is what let the Linux twin's tests rot to the wrong arity unnoticed. # 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` # 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 # with `nvenc` cannot LINK on MSVC: nvidia-video-codec-sdk link-imports
# NvEncodeAPICreateInstance / NvEncodeAPIGetMaxSupportedVersion, which resolve only against # NvEncodeAPICreateInstance / NvEncodeAPIGetMaxSupportedVersion, which resolve only against
@@ -188,6 +191,7 @@ jobs:
run: | run: |
cargo clippy --release -p punktfunk-host --features nvenc,amf-qsv,qsv -- -D warnings; if ($LASTEXITCODE) { throw "host clippy" } 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-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" } cargo clippy --release -p punktfunk-tray -- -D warnings; if ($LASTEXITCODE) { throw "tray clippy" }
- name: Build + lint the HDR Vulkan layer (pf-vkhdr-layer) - name: Build + lint the HDR Vulkan layer (pf-vkhdr-layer)
+4 -2
View File
@@ -7,10 +7,12 @@
//! [`FrameChannelSender`] closure, so this crate reaches neither the encoder nor the host //! [`FrameChannelSender`] closure, so this crate reaches neither the encoder nor the host
//! orchestrator). //! 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). // Every unsafe block in this crate carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
#![deny(clippy::undocumented_unsafe_blocks)] #![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 anyhow::Result;
use pf_frame::{CapturedFrame, FramePayload, PixelFormat}; use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
+61 -2
View File
@@ -65,7 +65,10 @@ unsafe extern "system" fn hybrid_query_hook(gpu_preference: *mut u32) -> i32 {
if gpu_preference.is_null() { if gpu_preference.is_null() {
return 0xC000_000Du32 as i32; // STATUS_INVALID_PARAMETER 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 0 // STATUS_SUCCESS
} }
@@ -161,7 +164,19 @@ 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<Vec<u8>> { pub(crate) unsafe fn compile_shader(src: &str, entry: PCSTR, target: PCSTR) -> Result<Vec<u8>> {
// 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<ID3DBlob> = None; let mut blob: Option<ID3DBlob> = None;
let mut errs: Option<ID3DBlob> = None; let mut errs: Option<ID3DBlob> = None;
let r = D3DCompile( let r = D3DCompile(
@@ -191,6 +206,7 @@ pub(crate) unsafe fn compile_shader(src: &str, entry: PCSTR, target: PCSTR) -> R
let blob = blob.context("no shader blob")?; let blob = blob.context("no shader blob")?;
let p = blob.GetBufferPointer() as *const u8; let p = blob.GetBufferPointer() as *const u8;
Ok(std::slice::from_raw_parts(p, blob.GetBufferSize()).to_vec()) 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). /// Fullscreen-triangle vertex shader for the HDR conversion pass (3 verts, no input layout).
@@ -322,6 +338,11 @@ pub(crate) struct HdrP010Converter {
impl HdrP010Converter { impl HdrP010Converter {
pub(crate) unsafe fn new(device: &ID3D11Device) -> Result<Self> { pub(crate) unsafe fn new(device: &ID3D11Device) -> 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
// raw pointer outlives the call that produced it.
unsafe {
// Inline the shared HLSL (D3DCompile has no include handler wired here). The two PS sources // Inline the shared HLSL (D3DCompile has no include handler wired here). The two PS sources
// carry a `#include_common` marker we substitute before compiling. // carry a `#include_common` marker we substitute before compiling.
let y_src = HDR_P010_Y_PS.replace("#include_common", HDR_P010_COMMON); let y_src = HDR_P010_Y_PS.replace("#include_common", HDR_P010_COMMON);
@@ -366,6 +387,7 @@ impl HdrP010Converter {
cbuf: cbuf.context("p010 cbuf")?, cbuf: cbuf.context("p010 cbuf")?,
}) })
} }
}
/// 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
@@ -375,6 +397,10 @@ impl HdrP010Converter {
dst: &ID3D11Texture2D, dst: &ID3D11Texture2D,
format: DXGI_FORMAT, format: DXGI_FORMAT,
) -> Result<ID3D11RenderTargetView> { ) -> Result<ID3D11RenderTargetView> {
// 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 { let desc = D3D11_RENDER_TARGET_VIEW_DESC {
Format: format, Format: format,
ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D, ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D,
@@ -394,6 +420,7 @@ impl HdrP010Converter {
})?; })?;
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 /// 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. /// `BIND_RENDER_TARGET`). Two opaque passes: full-res luma → plane 0, half-res chroma → plane 1.
@@ -408,6 +435,13 @@ impl HdrP010Converter {
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
// 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 y_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16_UNORM)?;
let uv_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16G16_UNORM)?; let uv_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16G16_UNORM)?;
@@ -465,6 +499,7 @@ impl HdrP010Converter {
ctx.PSSetShaderResources(0, Some(&[None])); ctx.PSSetShaderResources(0, Some(&[None]));
Ok(()) Ok(())
} }
}
} }
/// PyroWave LUMA pass PS — full-res, writes Y to a separate `R8_UNORM` texture. BT.709 limited from /// PyroWave LUMA pass PS — full-res, writes Y to a separate `R8_UNORM` texture. BT.709 limited from
@@ -604,6 +639,9 @@ pub(crate) struct BgraToYuvPlanes {
impl BgraToYuvPlanes { impl BgraToYuvPlanes {
pub(crate) unsafe fn new(device: &ID3D11Device, hdr: bool, chroma444: bool) -> Result<Self> { pub(crate) unsafe fn new(device: &ID3D11Device, hdr: bool, chroma444: bool) -> Result<Self> {
// 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) { let (y_src, uv_src) = match (hdr, chroma444) {
(false, false) => (PYRO_Y_PS.to_string(), PYRO_UV_PS.to_string()), (false, false) => (PYRO_Y_PS.to_string(), PYRO_UV_PS.to_string()),
(false, true) => (PYRO_Y_PS.to_string(), PYRO_UV444_PS.to_string()), (false, true) => (PYRO_Y_PS.to_string(), PYRO_UV444_PS.to_string()),
@@ -632,6 +670,7 @@ impl BgraToYuvPlanes {
chroma444, chroma444,
}) })
} }
}
/// Convert `src_srv` (BGRA slot for SDR / scRGB FP16 slot for HDR, WxH) → `y_rtv` (full-res Y /// Convert `src_srv` (BGRA slot for SDR / scRGB FP16 slot for HDR, WxH) → `y_rtv` (full-res Y
/// texture) + `cbcr_rtv` (half- or full-res CbCr texture per the constructed mode). Two opaque /// texture) + `cbcr_rtv` (half- or full-res CbCr texture per the constructed mode). Two opaque
@@ -646,6 +685,10 @@ impl BgraToYuvPlanes {
w: u32, w: u32,
h: u32, h: u32,
) -> Result<()> { ) -> Result<()> {
// 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.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite
ctx.VSSetShader(&self.vs, None); ctx.VSSetShader(&self.vs, None);
ctx.PSSetShaderResources(0, Some(&[Some(src_srv.clone())])); ctx.PSSetShaderResources(0, Some(&[Some(src_srv.clone())]));
@@ -688,6 +731,7 @@ impl BgraToYuvPlanes {
ctx.PSSetShaderResources(0, Some(&[None])); ctx.PSSetShaderResources(0, Some(&[None]));
Ok(()) Ok(())
} }
}
} }
/// f64 reference for the P010 colour math — the EXACT analogue of the HLSL in [`HDR_P010_COMMON`]. /// f64 reference for the P010 colour math — the EXACT analogue of the HLSL in [`HDR_P010_COMMON`].
@@ -1264,8 +1308,15 @@ impl VideoConverter {
height: u32, height: u32,
scrgb_input: bool, scrgb_input: bool,
) -> Result<Self> { ) -> Result<Self> {
// 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 vdev: ID3D11VideoDevice = device.cast().context("device -> ID3D11VideoDevice")?;
let vctx: ID3D11VideoContext1 = context.cast().context("context -> ID3D11VideoContext1")?; let vctx: ID3D11VideoContext1 =
context.cast().context("context -> ID3D11VideoContext1")?;
let rate = DXGI_RATIONAL { let rate = DXGI_RATIONAL {
Numerator: 240, Numerator: 240,
Denominator: 1, Denominator: 1,
@@ -1308,6 +1359,7 @@ impl VideoConverter {
vp, vp,
}) })
} }
}
/// Convert `input` (BGRA or scRGB FP16) → `output` (NV12 or P010) on the video engine. Views are /// Convert `input` (BGRA or scRGB FP16) → `output` (NV12 or P010) on the video engine. Views are
/// created per call (cheap relative to the Blt) so the input texture can vary frame to frame. /// created per call (cheap relative to the Blt) so the input texture can vary frame to frame.
@@ -1316,6 +1368,12 @@ impl VideoConverter {
input: &ID3D11Texture2D, input: &ID3D11Texture2D,
output: &ID3D11Texture2D, output: &ID3D11Texture2D,
) -> Result<()> { ) -> Result<()> {
// 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 { let in_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
FourCC: 0, FourCC: 0,
ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D, ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
@@ -1358,6 +1416,7 @@ impl VideoConverter {
drop(std::mem::ManuallyDrop::into_inner(stream.pInputSurface)); drop(std::mem::ManuallyDrop::into_inner(stream.pInputSurface));
blt.context("VideoProcessorBlt") blt.context("VideoProcessorBlt")
} }
}
} }
#[cfg(test)] #[cfg(test)]
+31 -2
View File
@@ -582,7 +582,13 @@ unsafe impl Send for IddPushCapturer {}
/// 2026-07-03), so it cannot dup the handles out either. History: `Global\`-named + world-openable /// 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 /// (`WD`, security-review 2026-06-28 #5) → SY+LS-scoped → nameless → now SY-only. `psd` must outlive
/// `sa`. See `design/idd-push-security.md`. /// `sa`. See `design/idd-push-security.md`.
unsafe fn shared_object_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTOR)> { 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(); let mut psd = PSECURITY_DESCRIPTOR::default();
ConvertStringSecurityDescriptorToSecurityDescriptorW( ConvertStringSecurityDescriptorToSecurityDescriptorW(
w!("D:P(A;;GA;;;SY)"), w!("D:P(A;;GA;;;SY)"),
@@ -597,6 +603,7 @@ unsafe fn shared_object_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTO
bInheritHandle: false.into(), bInheritHandle: false.into(),
}; };
Ok((sa, psd)) Ok((sa, psd))
}
} }
impl IddPushCapturer { impl IddPushCapturer {
@@ -610,6 +617,12 @@ impl IddPushCapturer {
h: u32, h: u32,
format: DXGI_FORMAT, format: DXGI_FORMAT,
) -> Result<Vec<HostSlot>> { ) -> Result<Vec<HostSlot>> {
// 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 (sa, _psd) = shared_object_sa()?;
let mut slots = Vec::new(); let mut slots = Vec::new();
for _ in 0..RING_LEN { for _ in 0..RING_LEN {
@@ -629,7 +642,8 @@ impl IddPushCapturer {
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
CPUAccessFlags: 0, CPUAccessFlags: 0,
MiscFlags: (D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0 MiscFlags: (D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0
| D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0) as u32, | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0)
as u32,
}; };
let mut tex: Option<ID3D11Texture2D> = None; let mut tex: Option<ID3D11Texture2D> = None;
device device
@@ -661,6 +675,7 @@ impl IddPushCapturer {
} }
Ok(slots) Ok(slots)
} }
}
/// Open the IDD-push capturer. On success the caller's `keepalive` is attached (the capturer owns the /// Open the IDD-push capturer. On success the caller's `keepalive` is attached (the capturer owns the
/// virtual display); on FAILURE the keepalive is handed BACK so the caller can fall back to DDA /// virtual display); on FAILURE the keepalive is handed BACK so the caller can fall back to DDA
@@ -1739,6 +1754,11 @@ impl IddPushCapturer {
/// Runs on the owning capture/encode thread that holds the immediate context; forms no lasting /// Runs on the owning capture/encode thread that holds the immediate context; forms no lasting
/// borrow of `self`'s COM objects. /// borrow of `self`'s COM objects.
unsafe fn pyro_fence_signal(&mut self) -> Result<Option<(Option<isize>, u64)>> { unsafe fn pyro_fence_signal(&mut self) -> Result<Option<(Option<isize>, u64)>> {
// 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 { if !self.pyrowave {
return Ok(None); return Ok(None);
} }
@@ -1780,6 +1800,7 @@ impl IddPushCapturer {
// this original stays valid for the next rebuild). // this original stays valid for the next rebuild).
Ok(Some((self.pyro_fence_handle, value))) Ok(Some((self.pyro_fence_handle, value)))
} }
}
/// The (serial, x, y, visible) of the CURRENT polled cursor — the composite-regen change /// The (serial, x, y, visible) of the CURRENT polled cursor — the composite-regen change
/// key. `None` while the poller has no shape yet (or isn't running). /// key. `None` while the poller has no shape yet (or isn't running).
@@ -1803,6 +1824,13 @@ impl IddPushCapturer {
&mut self, &mut self,
slot_tex: &ID3D11Texture2D, slot_tex: &ID3D11Texture2D,
) -> Option<(ID3D11Texture2D, ID3D11ShaderResourceView)> { ) -> Option<(ID3D11Texture2D, ID3D11ShaderResourceView)> {
// 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(); let fmt = self.ring_format();
// (Re)build the scratch at the current ring geometry. // (Re)build the scratch at the current ring geometry.
let stale = self let stale = self
@@ -1910,6 +1938,7 @@ impl IddPushCapturer {
} }
Some((tex, srv)) Some((tex, srv))
} }
}
/// The secure-desktop guard (the 0.18.0 UAC/Winlogon regression). UAC consent and Winlogon /// The secure-desktop guard (the 0.18.0 UAC/Winlogon regression). UAC consent and Winlogon
/// live on the SECURE desktop, which the OS renders through the software-cursor path — its /// live on the SECURE desktop, which the OS renders through the software-cursor path — its
@@ -57,6 +57,10 @@ pub(super) struct CursorBlendPass {
impl CursorBlendPass { impl CursorBlendPass {
pub(super) unsafe fn new(device: &ID3D11Device) -> Result<Self> { pub(super) unsafe fn new(device: &ID3D11Device) -> Result<Self> {
// 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 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 psb = crate::dxgi::compile_shader(CURSOR_PS, s!("main"), s!("ps_5_0"))?;
let mut vs = None; let mut vs = None;
@@ -109,6 +113,7 @@ impl CursorBlendPass {
shape: None, shape: None,
}) })
} }
}
/// Upload `ov`'s bitmap if its serial is new; reuse the cached SRV otherwise. /// Upload `ov`'s bitmap if its serial is new; reuse the cached SRV otherwise.
unsafe fn ensure_shape( unsafe fn ensure_shape(
@@ -116,6 +121,11 @@ impl CursorBlendPass {
device: &ID3D11Device, device: &ID3D11Device,
ov: &pf_frame::CursorOverlay, ov: &pf_frame::CursorOverlay,
) -> Result<()> { ) -> Result<()> {
// 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) { if self.shape.as_ref().is_some_and(|(s, ..)| *s == ov.serial) {
return Ok(()); return Ok(());
} }
@@ -153,6 +163,7 @@ impl CursorBlendPass {
self.shape = Some((ov.serial, srv.context("null cursor shape srv")?, ov.w, ov.h)); self.shape = Some((ov.serial, srv.context("null cursor shape srv")?, ov.w, ov.h));
Ok(()) Ok(())
} }
}
/// Alpha-blend `ov` onto `dst` (frame-sized, RENDER_TARGET-capable — the blend scratch). /// Alpha-blend `ov` onto `dst` (frame-sized, RENDER_TARGET-capable — the blend scratch).
/// `linear_scale`: 0 = SDR passthrough; non-zero = the frame is FP16 scRGB (HDR /// `linear_scale`: 0 = SDR passthrough; non-zero = the frame is FP16 scRGB (HDR
@@ -167,6 +178,13 @@ impl CursorBlendPass {
ov: &pf_frame::CursorOverlay, ov: &pf_frame::CursorOverlay,
linear_scale: f32, linear_scale: f32,
) -> Result<()> { ) -> Result<()> {
// 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)?; self.ensure_shape(device, ov)?;
let (_, srv, w, h) = self.shape.as_ref().expect("shape just ensured"); let (_, srv, w, h) = self.shape.as_ref().expect("shape just ensured");
if self.cbuf_scale != Some(linear_scale) { if self.cbuf_scale != Some(linear_scale) {
@@ -213,4 +231,5 @@ impl CursorBlendPass {
ctx.PSSetShaderResources(0, Some(&none_srv)); ctx.PSSetShaderResources(0, Some(&none_srv));
Ok(()) Ok(())
} }
}
} }
@@ -140,6 +140,9 @@ impl Capturer for SyntheticNv12Capturer {
/// # Safety /// # Safety
/// Calls DXGI factory/adapter enumeration; returns owned COM objects or an error. /// Calls DXGI factory/adapter enumeration; returns owned COM objects or an error.
unsafe fn resolve_render_adapter() -> Result<IDXGIAdapter1> { unsafe fn resolve_render_adapter() -> Result<IDXGIAdapter1> {
// 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")?; let factory: IDXGIFactory4 = CreateDXGIFactory1().context("CreateDXGIFactory1")?;
if let Some(luid) = pf_gpu::resolve_render_adapter_luid() { if let Some(luid) = pf_gpu::resolve_render_adapter_luid() {
if let Ok(a) = factory.EnumAdapterByLuid::<IDXGIAdapter1>(luid) { if let Ok(a) = factory.EnumAdapterByLuid::<IDXGIAdapter1>(luid) {
@@ -147,6 +150,7 @@ unsafe fn resolve_render_adapter() -> Result<IDXGIAdapter1> {
} }
} }
factory.EnumAdapters1(0).context("EnumAdapters1(0)") factory.EnumAdapters1(0).context("EnumAdapters1(0)")
}
} }
/// Create an NV12 `Texture2D` with the given usage/CPU-access/bind flags. /// Create an NV12 `Texture2D` with the given usage/CPU-access/bind flags.
@@ -177,8 +181,12 @@ unsafe fn create_nv12(
..Default::default() ..Default::default()
}; };
let mut tex: Option<ID3D11Texture2D> = None; let mut tex: Option<ID3D11Texture2D> = None;
// 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 device
.CreateTexture2D(&desc, None, Some(&mut tex)) .CreateTexture2D(&desc, None, Some(&mut tex))
.context("CreateTexture2D(NV12)")?; .context("CreateTexture2D(NV12)")?;
}
tex.context("CreateTexture2D returned a null NV12 texture") tex.context("CreateTexture2D returned a null NV12 texture")
} }