docs(host): prove the last 3 files + crate-root deny (unsafe-proof program 4/N, final)
Completes the unsafe-proof program now that the parallel WIP has landed: - idd_push.rs (25 sites), nvenc.rs (7), punktfunk1.rs (21): a SAFETY proof on every unsafe block — D3D11/DXGI COM (same-device textures, immediate-context single-thread, keyed-mutex-held convert), the NVENC SDK table (versioned POD, register/map/lock-bitstream pairing), cross-process shm reads (atomic magic/generation handshake), and the C-ABI harness (each call cross-checked against its abi.rs `# Safety` doc). No SUSPECT (UB) blocks. - capture.rs / encode.rs: the parent-module deny is restored (their WIP children are now proven), and main.rs gains a crate-root #![deny(clippy::undocumented_unsafe_blocks)] — the permanent catch-all gate so no future unsafe block anywhere in the crate can land without a proof. - Fixed 4 blocks the agents missed: unsafe blocks nested inside `assert_eq!(...)` macro args (the comment-above-statement didn't associate) — hoisted to a `let`. - rustfmt-canonicalized the Windows files (the agents' SAFETY comments + some pre-existing 1.9.0 drift) so `cargo fmt --all --check` is clean. Verified: cargo clippy -p punktfunk-host --all-targets -- -D warnings AND cargo fmt -p punktfunk-host --check both green with the crate-root deny active. Windows cfg(windows) re-verified on the box next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,10 +2,9 @@
|
||||
//! CPU-copy fallback (the portal delivers a CPU buffer; the encoder uploads it to the GPU
|
||||
//! internally). Zero-copy dmabuf→NVENC import is deferred (plan §9 risk).
|
||||
|
||||
// This file's own unsafe block carries a `// SAFETY:` proof, but the file-level
|
||||
// `#![deny(clippy::undocumented_unsafe_blocks)]` is deliberately NOT set yet: as a parent module it
|
||||
// would propagate the lint to `capture::windows::idd_push` (in-flight parallel work, not yet
|
||||
// proven). The deny lands here once every child module (incl. idd_push.rs) is documented.
|
||||
// Every unsafe block in this module tree carries a `// SAFETY:` proof; enforce it (unsafe-proof
|
||||
// program). As a parent module this also covers the child modules (capture::windows/linux::*).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
//! [`pf_driver_proto::frame`] (which OWNS the contract, with `const` size asserts) — both sides
|
||||
//! `use` it, so drift is a compile error rather than a "must match" comment.
|
||||
|
||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use super::dxgi::{make_device, D3d11Frame, HdrP010Converter, VideoConverter, WinCaptureTarget};
|
||||
use super::{CapturedFrame, Capturer, FramePayload, PixelFormat};
|
||||
use anyhow::{bail, Context, Result};
|
||||
@@ -225,7 +228,12 @@ pub struct IddPushCapturer {
|
||||
status_logged: bool,
|
||||
_keepalive: Box<dyn Send>,
|
||||
}
|
||||
// COM objects used only from the owning (encode) thread.
|
||||
// SAFETY: `IddPushCapturer` is `!Send` only because of its `*mut SharedHeader`/`*mut DebugBlock` raw
|
||||
// pointers (and the COM interfaces). It is created, used, and dropped by a SINGLE thread — the owning
|
||||
// capture/encode thread — never shared: the `ID3D11DeviceContext` is the device's IMMEDIATE context
|
||||
// (single-threaded by D3D11 contract) and is only ever touched from that thread, and the header/
|
||||
// dbg_block pointers (into mappings this struct owns) are only dereferenced there. `Send` transfers
|
||||
// ownership to one thread at a time with NO concurrent access; we do not (and must not) claim `Sync`.
|
||||
unsafe impl Send for IddPushCapturer {}
|
||||
|
||||
/// Build a permissive (Everyone:GenericAll) `SECURITY_ATTRIBUTES` so the restricted WUDFHost driver
|
||||
@@ -343,6 +351,9 @@ impl IddPushCapturer {
|
||||
// a fullscreen game can hold the virtual display at a different mode (esp. across a reconnect), so
|
||||
// matching the actual mode lets the first frame flow instead of being dropped (game-capture bug
|
||||
// GB1). Falls back to the negotiated mode when the CCD read is unavailable.
|
||||
// SAFETY: `active_resolution` is an `unsafe fn` (Win32 CCD `QueryDisplayConfig`) that takes only a
|
||||
// copy of the plain `u32` CCD target id and returns owned `(w, h)` values; it forms no borrows from
|
||||
// us and validates the id internally, returning `None` on any failure (handled by `unwrap_or`).
|
||||
let (w, h) =
|
||||
unsafe { crate::win_display::active_resolution(target.target_id) }.unwrap_or((pw, ph));
|
||||
if (w, h) != (pw, ph) {
|
||||
@@ -361,6 +372,27 @@ impl IddPushCapturer {
|
||||
// PROACTIVELY enable advanced color so HDR streams without the user toggling anything; an
|
||||
// SDR-only client leaves the display alone (and still gets a tone-mapped picture, never a freeze,
|
||||
// if the user does enable HDR).
|
||||
// SAFETY: one block over the whole ring setup; every operation in it is sound:
|
||||
// - `set_advanced_color`/`advanced_color_enabled` are `unsafe fn`s taking only a copy of the plain
|
||||
// `u32` target id; they read/flip CCD display config and return owned values, borrowing nothing.
|
||||
// - `CreateDXGIFactory1`, `EnumAdapterByLuid`, `make_device`, `permissive_sa`, `CreateFileMappingW`,
|
||||
// `MapViewOfFile`, `CreateEventW`, and `create_ring_slots` are all `?`-checked, so every returned
|
||||
// interface/handle/view is non-error before use; `&sa`/`&adapter`/`&device`/the `&HSTRING` names
|
||||
// are live borrows that outlive each synchronous call, and `sa.lpSecurityDescriptor` stays valid
|
||||
// because its backing `_psd` is held in scope for the whole block.
|
||||
// - The header mapping is created AND viewed at `bytes == size_of::<SharedHeader>().max(64)`; the
|
||||
// view's null is checked (`bail!` on failure, after which the owned `map` closes the mapping). The
|
||||
// OS view base is page-aligned, so `section.ptr::<SharedHeader>()` is suitably aligned for a
|
||||
// `SharedHeader`, and `write_bytes(.., 0, bytes)` plus the `(*header).field = ..` writes all stay
|
||||
// within those `bytes` and write THROUGH the raw pointer without forming any `&mut`. The debug
|
||||
// section is the same pattern at `dbg_bytes == size_of::<DebugBlock>()`, only entered when its
|
||||
// own view is non-null.
|
||||
// - The `magic` publish stores through `addr_of!((*header).magic) as *const AtomicU32`: `addr_of!`
|
||||
// takes the field address without a reference; the field is a 4-aligned `u32` (valid for
|
||||
// `AtomicU32`), and the `Release` store after the `Release` fence is the cross-process handshake
|
||||
// that orders all preceding writes before the driver may observe `MAGIC`.
|
||||
// - `header`/`dbg_block` point into the OS mappings, NOT into the `MappedSection` structs, so moving
|
||||
// `section`/`dbg_section` into `me` leaves them valid (see the `MappedSection` doc comment).
|
||||
unsafe {
|
||||
// If we ENABLE advanced color for a 10-bit client, trust it (the driver will compose FP16) and
|
||||
// size the ring FP16 directly — don't race the advanced_color_enabled poll, which may not have
|
||||
@@ -541,10 +573,16 @@ impl IddPushCapturer {
|
||||
fn wait_for_attach(&self) -> Result<()> {
|
||||
let deadline = Instant::now() + Duration::from_secs(4);
|
||||
loop {
|
||||
// Plain read: the driver writes this u32; an aligned u32 read can't tear (same access as
|
||||
// SAFETY: `self.header` points into the live shared-header mapping this capturer owns (sized
|
||||
// `>= size_of::<SharedHeader>()`, page-aligned), so the field read is in-bounds + aligned, and
|
||||
// no reference into the shared region is formed. Plain read: the driver writes this `u32`
|
||||
// cross-process, but an aligned `u32` read can't tear and `driver_status` is best-effort
|
||||
// diagnostics — the real handshake is the atomic `magic`/`latest` (same access as
|
||||
// log_driver_status_once).
|
||||
let st = unsafe { (*self.header).driver_status };
|
||||
if matches!(st, DRV_STATUS_TEX_FAIL | DRV_STATUS_NO_DEVICE1) {
|
||||
// SAFETY: as above — an in-bounds, aligned `u32` read of a best-effort diagnostic field
|
||||
// through the owned, live header mapping; no reference into the shared region is formed.
|
||||
let detail = unsafe { (*self.header).driver_status_detail };
|
||||
bail!(
|
||||
"IDD-push driver failed to attach (driver_status={st} detail=0x{detail:08x} — \
|
||||
@@ -567,6 +605,10 @@ impl IddPushCapturer {
|
||||
|
||||
#[inline]
|
||||
fn latest(&self) -> u64 {
|
||||
// SAFETY: `self.header` is the live, owned shared-header mapping (page-aligned, sized for a
|
||||
// `SharedHeader`). `addr_of!((*self.header).latest)` forms the address of the `latest` field
|
||||
// WITHOUT a reference; it is an 8-aligned `u64` (so valid for `AtomicU64`), and the `Acquire` load
|
||||
// is the consumer half of the cross-process publish handshake (pairs with the driver's `Release`).
|
||||
unsafe {
|
||||
(*(std::ptr::addr_of!((*self.header).latest) as *const AtomicU64))
|
||||
.load(Ordering::Acquire)
|
||||
@@ -578,6 +620,10 @@ impl IddPushCapturer {
|
||||
if self.status_logged {
|
||||
return;
|
||||
}
|
||||
// SAFETY: four in-bounds, aligned reads of the live, owned shared-header mapping. The driver writes
|
||||
// these `u32`/`i32` diagnostic fields cross-process, but aligned word reads can't tear and these are
|
||||
// best-effort status (the real handshake is the atomic `magic`/`latest`); no `&`/`&mut` reference
|
||||
// into the shared region is formed.
|
||||
let (status, detail, lo, hi) = unsafe {
|
||||
(
|
||||
(*self.header).driver_status,
|
||||
@@ -617,6 +663,11 @@ impl IddPushCapturer {
|
||||
tracing::warn!("IDD push DEBUG: no debug block");
|
||||
return;
|
||||
}
|
||||
// SAFETY: `self.dbg_block` was just checked non-null (the early return above); it points into the
|
||||
// owned `dbg_section` mapping sized exactly `size_of::<DebugBlock>()` and page-aligned, so it is
|
||||
// valid + aligned for `DebugBlock`. `d` is a short-lived SHARED reference used only to read the
|
||||
// fields below; we never form `&mut` into this region, and the driver's cross-process writes are
|
||||
// aligned `u32`s that don't tear (best-effort bring-up diagnostics).
|
||||
let d = unsafe { &*self.dbg_block };
|
||||
tracing::error!(
|
||||
run_core_entries = d.run_core_entries,
|
||||
@@ -666,6 +717,10 @@ impl IddPushCapturer {
|
||||
self.height = new_h;
|
||||
let fmt = self.ring_format();
|
||||
let new_gen = IDD_GENERATION.fetch_add(1, Ordering::Relaxed);
|
||||
// SAFETY: `create_ring_slots` is an `unsafe fn` (it makes D3D11/DXGI COM calls); we pass a live
|
||||
// borrow of `self.device` (the capturer's own device, on which the slots are created) plus plain
|
||||
// `u32`/`DXGI_FORMAT` values, and `?` propagates any failure before the slots are used. Every
|
||||
// returned slot's texture + keyed mutex belongs to that same `self.device`.
|
||||
let new_slots = unsafe {
|
||||
Self::create_ring_slots(
|
||||
&self.device,
|
||||
@@ -676,6 +731,12 @@ impl IddPushCapturer {
|
||||
fmt,
|
||||
)?
|
||||
};
|
||||
// SAFETY: `self.header` is the live, owned shared-header mapping (page-aligned, sized for a
|
||||
// `SharedHeader`). The `latest`/`generation` stores go through `addr_of!`-formed field pointers (no
|
||||
// references) of correctly-aligned `u64`/`u32` fields, valid for `AtomicU64`/`AtomicU32`; the
|
||||
// `dxgi_format`/`width`/`height` writes are in-bounds raw writes through the pointer (no `&mut`).
|
||||
// The `Release` fence + the `Release` `generation` store publish all preceding writes so the driver
|
||||
// only re-attaches (`Acquire`) once the new textures + format are in place.
|
||||
unsafe {
|
||||
// Clear `latest` to the 0 sentinel (generation 0, which try_consume rejects). The real guard
|
||||
// against consuming an unwritten new-ring slot is the generation tag in `latest`: a stale
|
||||
@@ -711,9 +772,13 @@ impl IddPushCapturer {
|
||||
return;
|
||||
}
|
||||
self.last_acm_poll = Instant::now();
|
||||
// SAFETY: `advanced_color_enabled` is an `unsafe fn` taking only a copy of the plain `u32` target
|
||||
// id; it performs a read-only CCD query and returns an owned `bool`, borrowing nothing from us.
|
||||
let now_hdr = unsafe { crate::win_display::advanced_color_enabled(self.target_id) };
|
||||
// Follow the display's ACTUAL resolution too — a fullscreen game can mode-set the virtual display
|
||||
// out from under the negotiated size (game-capture bug GB1). Unknown read → keep our current size.
|
||||
// SAFETY: `active_resolution` is an `unsafe fn` taking only a copy of the plain `u32` target id; it
|
||||
// performs a read-only CCD query and returns owned `(w, h)` values, borrowing nothing from us.
|
||||
let (now_w, now_h) = unsafe { crate::win_display::active_resolution(self.target_id) }
|
||||
.unwrap_or((self.width, self.height));
|
||||
if now_hdr == self.display_hdr && now_w == self.width && now_h == self.height {
|
||||
@@ -760,6 +825,10 @@ impl IddPushCapturer {
|
||||
};
|
||||
for _ in 0..OUT_RING {
|
||||
let mut t: Option<ID3D11Texture2D> = None;
|
||||
// SAFETY: `CreateTexture2D` is called on `self.device` (the capturer's live D3D11 device);
|
||||
// `&desc` is a fully-initialized stack `D3D11_TEXTURE2D_DESC`, the data arg is `None` (no
|
||||
// initial data), and `Some(&mut t)` is a live out-parameter the call fills. `?` rejects a failed
|
||||
// HRESULT before `t` is unwrapped, and the created texture belongs to `self.device`.
|
||||
unsafe {
|
||||
self.device
|
||||
.CreateTexture2D(&desc, None, Some(&mut t))
|
||||
@@ -775,9 +844,16 @@ impl IddPushCapturer {
|
||||
fn ensure_converter(&mut self) -> Result<()> {
|
||||
if self.display_hdr {
|
||||
if self.hdr_p010_conv.is_none() {
|
||||
// SAFETY: `HdrP010Converter::new` is `unsafe` (it compiles D3D11 shaders + creates
|
||||
// resources); we pass a live borrow of `self.device`, the device the converter's resources
|
||||
// belong to, and `?` propagates any failure before the converter is stored.
|
||||
self.hdr_p010_conv = Some(unsafe { HdrP010Converter::new(&self.device)? });
|
||||
}
|
||||
} else if self.video_conv.is_none() {
|
||||
// SAFETY: `VideoConverter::new` is `unsafe` (it sets up the D3D11 VIDEO processor); we pass live
|
||||
// borrows of `self.device` + its immediate `self.context` (single-threaded, this thread) plus
|
||||
// plain `u32` dimensions, and `?` propagates any failure before it is stored. The converter's
|
||||
// resources belong to that same device/context.
|
||||
self.video_conv = Some(unsafe {
|
||||
VideoConverter::new(&self.device, &self.context, self.width, self.height, false)?
|
||||
});
|
||||
@@ -942,6 +1018,8 @@ pub fn spawn_observer(target: WinCaptureTarget, preferred: Option<(u32, u32, u32
|
||||
|
||||
/// The discrete render GPU LUID (where NVENC runs), falling back to the monitor's `OsAdapterLuid`.
|
||||
fn resolve_render_adapter_luid_or(fallback_packed: i64) -> LUID {
|
||||
// SAFETY: `resolve_render_adapter_luid` is an `unsafe fn` (it enumerates DXGI adapters) that takes no
|
||||
// arguments and returns an owned `Option<LUID>`, borrowing nothing.
|
||||
if let Some(l) = unsafe { crate::win_adapter::resolve_render_adapter_luid() } {
|
||||
return l;
|
||||
}
|
||||
@@ -955,6 +1033,9 @@ impl Capturer for IddPushCapturer {
|
||||
fn next_frame(&mut self) -> Result<CapturedFrame> {
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
loop {
|
||||
// SAFETY: `self.event` is the live frame-ready `OwnedHandle` this capturer owns; its raw value
|
||||
// (borrowed for the call, so it outlives this synchronous wait) is a valid auto-reset event
|
||||
// handle. `WaitForSingleObject` only reads the handle; the 16 ms timeout bounds the wait.
|
||||
let _ = unsafe { WaitForSingleObject(HANDLE(self.event.as_raw_handle()), 16) };
|
||||
if let Some(f) = self.try_consume()? {
|
||||
return Ok(f);
|
||||
@@ -964,6 +1045,9 @@ impl Capturer for IddPushCapturer {
|
||||
}
|
||||
if Instant::now() > deadline {
|
||||
self.log_debug_block();
|
||||
// SAFETY: four in-bounds, aligned reads of the live, owned shared-header mapping — the same
|
||||
// best-effort diagnostic fields as `log_driver_status_once` (aligned word reads can't tear;
|
||||
// no reference into the shared region is formed).
|
||||
let (st, detail, lo, hi) = unsafe {
|
||||
(
|
||||
(*self.header).driver_status,
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
//! RGB→YUV on the GPU, so no host-side CSC) and VAAPI on AMD/Intel (`*_vaapi`; the CPU-input
|
||||
//! fallback swscales RGB→NV12, the zero-copy path imports the capture dmabuf straight into a
|
||||
//! VA surface). One [`Encoder`] trait, selected in [`open_video`].
|
||||
// This file's own unsafe block carries a `// SAFETY:` proof, but the file-level
|
||||
// `#![deny(clippy::undocumented_unsafe_blocks)]` is deliberately NOT set yet: as a parent module it
|
||||
// would propagate the lint to `encode::windows::nvenc` (in-flight parallel work, not yet proven).
|
||||
// The deny lands here once every child module (incl. nvenc.rs) is documented.
|
||||
// Every unsafe block in this module tree carries a `// SAFETY:` proof; enforce it (unsafe-proof
|
||||
// program). As a parent module this also covers the child modules (encode::windows/linux::*).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::capture::{CapturedFrame, PixelFormat};
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
//! Needs a real NVIDIA GPU at runtime (session creation fails otherwise) — compiles GPU-less, but
|
||||
//! `open`/`submit` only succeed on a GPU box. The software encoder (`super::sw`) is the fallback.
|
||||
|
||||
// Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it.
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use super::{Codec, EncodedFrame, Encoder, EncoderCaps};
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
@@ -88,7 +91,15 @@ pub struct NvencD3d11Encoder {
|
||||
init_device: *mut c_void,
|
||||
}
|
||||
|
||||
// Raw NVENC handle + COM ptrs; confined to the single encode thread (like the Linux encoder).
|
||||
// SAFETY: the `!Send` fields are the raw NVENC session/device handles (`encoder`, `init_device`),
|
||||
// the raw NVENC bitstream/registered/mapped pointers carried in `bitstreams`/`regs`/`pending`, and
|
||||
// the `ID3D11Texture2D` COM refs — none of which may be touched concurrently from two threads. This
|
||||
// encoder is owned by exactly one thread: it is moved onto the host encode thread once at
|
||||
// construction, and every NVENC call and D3D11 access happens only from that thread thereafter
|
||||
// (`submit`/`poll`/`invalidate_ref_frames`/`Drop` all run there, like the Linux encoder). Moving the
|
||||
// handles across that single ownership-transfer boundary is sound because no NVENC/D3D11 call is in
|
||||
// flight during the move and the session and its D3D11 immediate context are never shared (`&`) or
|
||||
// used concurrently — so `Send` introduces no data race on the non-`Send` fields.
|
||||
unsafe impl Send for NvencD3d11Encoder {}
|
||||
|
||||
impl NvencD3d11Encoder {
|
||||
@@ -403,6 +414,17 @@ impl NvencD3d11Encoder {
|
||||
|
||||
/// Lazily create the session on the first frame's D3D11 device (so capture + encode share it).
|
||||
fn init_session(&mut self, device: &ID3D11Device) -> Result<()> {
|
||||
// SAFETY: every call below goes through a function pointer resolved once from the loaded
|
||||
// `nvidia_video_codec_sdk::ENCODE_API` (`nvEncodeAPI`) table, or through this type's own
|
||||
// `unsafe fn`s whose contract is met here. `query_caps`/`try_open_session` receive `device`,
|
||||
// the live `ID3D11Device` the caller pulled off the first frame; each returns either a valid
|
||||
// open NVENC session handle or an `Err`. `destroy_encoder` is only ever called on a handle a
|
||||
// `try_open_session` just returned (and `best` only when `!best.is_null()`), so it never frees
|
||||
// a dangling or null session. `create_bitstream_buffer` is passed `enc` — the one chosen live
|
||||
// session — and `&mut cb`, a `#[repr(C)] NV_ENC_CREATE_BITSTREAM_BUFFER` whose `version` is set
|
||||
// to `NV_ENC_CREATE_BITSTREAM_BUFFER_VER`; `cb` lives across the synchronous call and its
|
||||
// returned `bitstreamBuffer` is copied into `self.bitstreams` before `cb` drops. No handle
|
||||
// escapes the encode thread.
|
||||
unsafe {
|
||||
// Probe real GPU caps first (max dims / 10-bit / custom-VBV / RFI) so the config below is
|
||||
// gated on what this card supports and an out-of-range mode fails with a clear error
|
||||
@@ -589,6 +611,11 @@ impl Encoder for NvencD3d11Encoder {
|
||||
new = format!("{}x{}", captured.width, captured.height),
|
||||
"NVENC: capture device/size/HDR changed — re-initializing session"
|
||||
);
|
||||
// SAFETY: `teardown` (an `unsafe fn`) requires the encode thread with no NVENC call in
|
||||
// flight and a session whose cached regs/bitstreams/pending all belong to `self.encoder`.
|
||||
// All hold: this is the synchronous encode thread, `self.inited` so `self.encoder` is the
|
||||
// live session every cached resource was created against, and the previous frame's encode
|
||||
// has already been polled (synchronous submit→poll), so nothing is mid-encode.
|
||||
unsafe { self.teardown() };
|
||||
}
|
||||
if !self.inited {
|
||||
@@ -625,6 +652,21 @@ impl Encoder for NvencD3d11Encoder {
|
||||
}
|
||||
let slot = self.next % POOL;
|
||||
self.next += 1;
|
||||
// SAFETY: every NVENC call goes through a function pointer from the loaded `ENCODE_API` table
|
||||
// and takes `self.encoder`, the live session `init_session` just established (non-null on the
|
||||
// path that reaches here). `NV_ENC_REGISTER_RESOURCE rr` has `version =
|
||||
// NV_ENC_REGISTER_RESOURCE_VER` and registers `frame.texture` — a D3D11 texture from
|
||||
// `frame.device`, which is the SAME device the session was opened against (any device change
|
||||
// tears down and re-inits above, so `init_device == frame.device.as_raw()` here); the cloned
|
||||
// `ID3D11Texture2D` is kept alive in `regs` so NVENC's registration never outlives the texture.
|
||||
// `mp` (`NV_ENC_MAP_INPUT_RESOURCE`, version set) maps that registration and the map is recorded
|
||||
// in `pending` to be unmapped exactly once in `poll`/`teardown`. `pic` (`NV_ENC_PIC_PARAMS`,
|
||||
// version set) points `inputBuffer` at `mp.mappedResource` and `outputBitstream` at the live
|
||||
// pool bitstream `bitstreams[slot]`; the optional SEI scratch (`mastering_sei`/`cll_sei` and the
|
||||
// `sei` Vec whose `as_mut_ptr()` is written into the codec union) are stack locals that outlive
|
||||
// the synchronous `encode_picture`. Every `#[repr(C)]` param is a live local borrowed `&mut`
|
||||
// for the duration of its one synchronous call. (In-place encode without `CopyResource` is
|
||||
// sound because the encode loop is synchronous, as the module docs state.)
|
||||
unsafe {
|
||||
// Register the capturer's texture with NVENC once (cached by raw pointer), then encode it
|
||||
// IN PLACE — no `CopyResource` into an encoder-owned pool. This is the zero-copy win: the
|
||||
@@ -781,6 +823,12 @@ impl Encoder for NvencD3d11Encoder {
|
||||
// We tag each input with `inputTimeStamp = frame_idx` (0,1,2,…), which is also the client's
|
||||
// frame number (the packetizer numbers frames in submit order), so the client's lost-frame
|
||||
// range maps 1:1 onto the timestamps NVENC invalidates here.
|
||||
// SAFETY: `invalidate_ref_frames` is a function pointer from the loaded `ENCODE_API` table.
|
||||
// `self.encoder` was checked non-null at the top of this fn and is the live session; this runs
|
||||
// on the encode thread (like submit/poll), so there is no concurrent NVENC use. Each `ts` was
|
||||
// clamped to `[oldest_in_dpb, frame_idx - 1]` above, so it names a frame still in the session's
|
||||
// DPB; the call passes only that `u64` timestamp (no struct), so there is no struct-size or
|
||||
// lifetime concern.
|
||||
unsafe {
|
||||
for ts in first..=last {
|
||||
if (API.invalidate_ref_frames)(self.encoder, ts as u64)
|
||||
@@ -799,6 +847,16 @@ impl Encoder for NvencD3d11Encoder {
|
||||
let Some((bs, map, pts_ns)) = self.pending.pop_front() else {
|
||||
return Ok(None);
|
||||
};
|
||||
// SAFETY: a non-empty `pending` implies `submit` ran, so `self.encoder` is the live session
|
||||
// (`teardown` clears `pending` whenever it nulls the handle); all calls below use function
|
||||
// pointers from the loaded `ENCODE_API` table on the encode thread. `NV_ENC_LOCK_BITSTREAM lock`
|
||||
// (version = `NV_ENC_LOCK_BITSTREAM_VER`) locks `bs`, a pool bitstream a prior `encode_picture`
|
||||
// targeted; `lock_bitstream` blocks until that encode finishes, so on success
|
||||
// `lock.bitstreamBufferPtr` is non-null and points at `lock.bitstreamSizeInBytes` bytes of
|
||||
// NVENC-owned, CPU-readable output valid until `unlock_bitstream`. The `from_raw_parts` slice is
|
||||
// only read (copied via `to_vec()`) BEFORE `unlock_bitstream(bs)` — lock and unlock pair on the
|
||||
// same buffer — so it never outlives the lock. `map` (the input resource paired with `bs` in
|
||||
// `pending`) is unmapped here, after the encode completed, exactly once.
|
||||
unsafe {
|
||||
let mut lock = nv::NV_ENC_LOCK_BITSTREAM {
|
||||
version: nv::NV_ENC_LOCK_BITSTREAM_VER,
|
||||
@@ -838,6 +896,11 @@ impl Encoder for NvencD3d11Encoder {
|
||||
|
||||
impl Drop for NvencD3d11Encoder {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: `teardown` (an `unsafe fn`) needs the owning thread with no NVENC call in flight and
|
||||
// a session whose cached resources all belong to `self.encoder`. At Drop this encoder is owned
|
||||
// exclusively (no other reference can exist), runs on the encode thread it was confined to, and
|
||||
// `teardown` early-returns when `self.encoder` is null; otherwise every cached reg/bitstream/
|
||||
// pending was created against that live session. It runs exactly once (here).
|
||||
unsafe { self.teardown() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ pub(super) const SHM_MAGIC: u32 = pf_driver_proto::gamepad::PAD_MAGIC; // "PFDS"
|
||||
pub(super) const OFF_INPUT: usize = core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, input);
|
||||
pub(super) const OFF_OUT_SEQ: usize =
|
||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, out_seq);
|
||||
pub(super) const OFF_OUTPUT: usize = core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, output);
|
||||
pub(super) const OFF_OUTPUT: usize =
|
||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, output);
|
||||
/// Device-type selector the driver reads to choose which HID identity/descriptor it serves: 0 =
|
||||
/// DualSense (the default — the section is zeroed), 1 = DualShock 4.
|
||||
pub(super) const OFF_DEVTYPE: usize =
|
||||
|
||||
@@ -187,8 +187,10 @@ impl XusbWinPad {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn write_state(&mut self, buttons: u16, lt: u8, rt: u8, lx: i16, ly: i16, rx: i16, ry: i16) {
|
||||
self.packet = self.packet.wrapping_add(1);
|
||||
// SAFETY: base points at SHM_SIZE bytes; all offsets are in range.
|
||||
let base = self.shm.base();
|
||||
// SAFETY: `base` is the start of the mapped section (`SHM_SIZE` bytes, owned by `Shm`); every
|
||||
// `OFF_*` is a fixed in-range offset into it and `write_unaligned` handles the unaligned field
|
||||
// writes. Single owner (`&mut self`), so no concurrent writer races these stores.
|
||||
unsafe {
|
||||
std::ptr::write_unaligned(base.add(OFF_BUTTONS) as *mut u16, buttons);
|
||||
*base.add(OFF_LT) = lt;
|
||||
|
||||
@@ -238,7 +238,8 @@ impl InputInjector for SendInputInjector {
|
||||
}
|
||||
InputKind::KeyDown | InputKind::KeyUp => {
|
||||
let down = event.kind == InputKind::KeyDown;
|
||||
let vk = (event.code & 0xff) as u16; // client sends Windows VK
|
||||
// client sends Windows VK
|
||||
let vk = (event.code & 0xff) as u16;
|
||||
// SAFETY: `MapVirtualKeyExW` is a pure value translation (VK → scancode); all three
|
||||
// args are by-value (`u32`, the `MAPVK_VK_TO_VSC_EX` map-type constant, a `None`
|
||||
// HKL). It dereferences no pointer and returns a `u32` — FFI-`unsafe` only.
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
|
||||
// Scaffold: trait methods and config paths are defined ahead of their backends.
|
||||
#![allow(dead_code)]
|
||||
// Unsafe-proof program: every `unsafe {}` / `unsafe impl` in the crate must carry a `// SAFETY:`
|
||||
// proof of why it is sound. This crate-root deny is the permanent, catch-all gate (it also covers
|
||||
// any future module); individual files keep their own `#![deny(...)]` as belt-and-suspenders.
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
mod audio;
|
||||
mod capture;
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
//! Trust: the host serves with its persistent identity (`~/.config/punktfunk/cert.pem`, shared
|
||||
//! with GameStream pairing) and logs the SHA-256 fingerprint clients pin.
|
||||
|
||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use punktfunk_core::config::{CompositorPref, FecConfig, FecScheme, GamepadPref, Role};
|
||||
use punktfunk_core::input::{InputEvent, InputKind};
|
||||
@@ -1965,6 +1968,11 @@ pub(crate) fn boost_thread_priority(critical: bool) {
|
||||
// capture/encode (critical) and send (non-critical).
|
||||
crate::session_tuning::on_hot_thread();
|
||||
#[cfg(target_os = "windows")]
|
||||
// SAFETY: `GetCurrentThread()` returns the constant pseudo-handle for the calling thread — always
|
||||
// valid, thread-local in meaning, and never closed (no leak/double-close). `SetThreadPriority`
|
||||
// takes that handle plus a `THREAD_PRIORITY_*` value the windows crate defines (HIGHEST or
|
||||
// ABOVE_NORMAL here); it only reprioritizes this OS thread, borrows no Rust memory, and its
|
||||
// `Result` is matched (a failure is logged, never UB). No pointers, lifetimes, or aliasing.
|
||||
unsafe {
|
||||
use windows::Win32::System::Threading::{
|
||||
GetCurrentThread, SetThreadPriority, THREAD_PRIORITY_ABOVE_NORMAL,
|
||||
@@ -1992,6 +2000,10 @@ pub(crate) fn boost_thread_priority(critical: bool) {
|
||||
// realtime CPU class can preempt the compositor AND the game's own render thread, adding the
|
||||
// very frame-time we refuse to add (opt-in only — see PUNKTFUNK_SCHED_RR).
|
||||
let nice = if critical { -10 } else { -5 };
|
||||
// SAFETY: `setpriority` takes three by-value integers and no pointers, so there is nothing to
|
||||
// alias or outlive. `PRIO_PROCESS` with `who == 0` targets the calling task on Linux and
|
||||
// `nice` is in range; the call only adjusts this thread's scheduling nice value and returns an
|
||||
// `int` we inspect. No memory is touched.
|
||||
let rc = unsafe { libc::setpriority(libc::PRIO_PROCESS, 0, nice) };
|
||||
if rc == 0 {
|
||||
tracing::debug!(critical, nice, "thread nice raised");
|
||||
@@ -2707,6 +2719,11 @@ fn virtual_stream_relay(ctx: SessionContext) -> Result<()> {
|
||||
// The secure-desktop HDR drop (for the DDA leg) keys off the monitor's real state in the mux loop.
|
||||
#[cfg(target_os = "windows")]
|
||||
if bit_depth >= 10 {
|
||||
// SAFETY: `set_advanced_color` is marked `unsafe` only because it drives the Win32 CCD API
|
||||
// internally; it takes `target_id` by value (Copy `u32` — this session's live SudoVDA
|
||||
// monitor's CCD target id) and sizes + owns every buffer it hands the OS on its own stack.
|
||||
// We pass no pointers, so nothing must outlive the call and there is no aliasing; an
|
||||
// unknown/absent target id simply returns false.
|
||||
unsafe {
|
||||
if crate::win_display::set_advanced_color(target.target_id, true) {
|
||||
// Let the colorspace change settle before WGC creates its capture item / detects HDR.
|
||||
@@ -2942,8 +2959,12 @@ fn virtual_stream_relay(ctx: SessionContext) -> Result<()> {
|
||||
// desktop (the drop just churned + still went black). Instead, if the monitor is in HDR,
|
||||
// open DDA in HDR (FP16 DuplicateOutput1 → BT.2020 PQ Main10); the normal-desktop DDA
|
||||
// overlay/flip issues that drove us to WGC don't apply to the composed Winlogon UI.
|
||||
let hdr =
|
||||
unsafe { crate::win_display::advanced_color_enabled(target.target_id) };
|
||||
// SAFETY: `advanced_color_enabled` is `unsafe` only because it queries the Win32 CCD
|
||||
// API; it takes `target_id` by value (the live SudoVDA monitor's CCD target id) and
|
||||
// allocates + owns every buffer it passes the OS internally. No caller pointer is
|
||||
// involved, so nothing must outlive the call and there is no aliasing; a missing
|
||||
// target id just yields false.
|
||||
let hdr = unsafe { crate::win_display::advanced_color_enabled(target.target_id) };
|
||||
dda = None; // reopen to capture the secure desktop
|
||||
match open_dda(&target, cur_mode.width, cur_mode.height, effective_hz, hdr) {
|
||||
Ok(mut p) => {
|
||||
@@ -3368,12 +3389,27 @@ mod tests {
|
||||
unsafe fn pull_verified(conn: *mut punktfunk_core::abi::PunktfunkConnection, count: u32) {
|
||||
use punktfunk_core::error::PunktfunkStatus;
|
||||
let mut got = 0u32;
|
||||
// SAFETY: the inferred type is the `#[repr(C)]` POD `PunktfunkFrame` (a raw `*const u8`, a
|
||||
// `usize`, and integer fields); all-zero is a valid bit pattern for every field (a null
|
||||
// `data`, `len == 0`). It is only ever read after `next_au` below fully overwrites it on `Ok`,
|
||||
// so the zeroed value is never observed.
|
||||
let mut frame = unsafe { std::mem::zeroed() };
|
||||
while got < count {
|
||||
// SAFETY: `conn` is the live, non-null `*mut PunktfunkConnection` from `punktfunk_connect`
|
||||
// (the caller asserts non-null and does not close it until after this returns), meeting the
|
||||
// ABI's "valid handle". `&mut frame` is an exclusive, writable borrow of the local
|
||||
// `PunktfunkFrame` that outlives this synchronous call. This single test thread is the only
|
||||
// video puller, satisfying the one-video-thread rule.
|
||||
match unsafe {
|
||||
punktfunk_core::abi::punktfunk_connection_next_au(conn, &mut frame, 2000)
|
||||
} {
|
||||
PunktfunkStatus::Ok => {
|
||||
// SAFETY: on `Ok`, `next_au` set `frame.data`/`frame.len` to the reassembled AU
|
||||
// buffer the connection owns; per the ABI contract that borrow stays valid until
|
||||
// the NEXT `next_au` call on this handle. We read the whole slice here (the assert
|
||||
// + length-checked indexing) before the loop's next `next_au`, and `conn` outlives
|
||||
// it — so the pointer is live, exactly `len` bytes, read-only, single-threaded (no
|
||||
// aliasing/use-after-free).
|
||||
let data = unsafe { std::slice::from_raw_parts(frame.data, frame.len) };
|
||||
let idx = u32::from_le_bytes(data[0..4].try_into().unwrap());
|
||||
assert_eq!(
|
||||
@@ -3421,6 +3457,11 @@ mod tests {
|
||||
// Session 1: TOFU (no pin) — observe the host fingerprint.
|
||||
let addr = std::ffi::CString::new("127.0.0.1").unwrap();
|
||||
let mut observed = [0u8; 32];
|
||||
// SAFETY: `addr` is a live `CString` ("127.0.0.1") whose `as_ptr()` is the NUL-terminated
|
||||
// UTF-8 host string the contract requires; `pin_sha256`/cert/key are NULL (all permitted), and
|
||||
// `observed.as_mut_ptr()` is the local `[u8; 32]` — exactly the 32 writable bytes the contract
|
||||
// demands, not aliased during the call. Every pointer references a live local that outlives the
|
||||
// blocking connect.
|
||||
let conn = unsafe {
|
||||
punktfunk_connect(
|
||||
addr.as_ptr(),
|
||||
@@ -3439,26 +3480,28 @@ mod tests {
|
||||
assert_ne!(observed, [0u8; 32], "fingerprint not reported");
|
||||
|
||||
let (mut w, mut h, mut hz) = (0u32, 0u32, 0u32);
|
||||
assert_eq!(
|
||||
unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) },
|
||||
PunktfunkStatus::Ok
|
||||
);
|
||||
// SAFETY: `conn` is the live, non-null connection handle just asserted above; `&mut w/h/hz` are
|
||||
// exclusive, writable borrows of local `u32`s that outlive this synchronous call — the three
|
||||
// writable out-params the contract names.
|
||||
let st = unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) };
|
||||
assert_eq!(st, PunktfunkStatus::Ok);
|
||||
assert_eq!((w, h, hz), (1280, 720, 60));
|
||||
|
||||
// Mid-stream renegotiation: request a new mode, the host acks on the control
|
||||
// stream, and punktfunk_connection_mode reflects the switch.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
punktfunk_core::abi::punktfunk_connection_request_mode(conn, 1920, 1080, 144)
|
||||
},
|
||||
PunktfunkStatus::Ok
|
||||
);
|
||||
// SAFETY: `conn` is the live, non-null connection handle (the only pointer arg); the remaining
|
||||
// arguments are by-value integers. The handle outlives this non-blocking enqueue.
|
||||
let st = unsafe {
|
||||
punktfunk_core::abi::punktfunk_connection_request_mode(conn, 1920, 1080, 144)
|
||||
};
|
||||
assert_eq!(st, PunktfunkStatus::Ok);
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
loop {
|
||||
assert_eq!(
|
||||
unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) },
|
||||
PunktfunkStatus::Ok
|
||||
);
|
||||
// SAFETY: same as the earlier `punktfunk_connection_mode` call — `conn` is the live handle
|
||||
// and `&mut w/h/hz` are exclusive writable borrows of locals that outlive this synchronous
|
||||
// call.
|
||||
let st = unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) };
|
||||
assert_eq!(st, PunktfunkStatus::Ok);
|
||||
if (w, h, hz) == (1920, 1080, 144) {
|
||||
break;
|
||||
}
|
||||
@@ -3469,6 +3512,8 @@ mod tests {
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
|
||||
// SAFETY: `pull_verified` requires a live connection handle it alone pulls video from; `conn` is
|
||||
// the open, non-null handle from `punktfunk_connect` and this is the only thread touching it.
|
||||
unsafe { pull_verified(conn, 25) };
|
||||
|
||||
let ev = punktfunk_core::input::InputEvent {
|
||||
@@ -3479,13 +3524,19 @@ mod tests {
|
||||
y: 2,
|
||||
flags: 0,
|
||||
};
|
||||
assert_eq!(
|
||||
unsafe { punktfunk_connection_send_input(conn, &ev) },
|
||||
PunktfunkStatus::Ok
|
||||
);
|
||||
// SAFETY: `conn` is the live handle; `&ev` borrows the local `InputEvent`, valid and immutable
|
||||
// for this synchronous enqueue — the contract's "valid InputEvent" pointer.
|
||||
let st = unsafe { punktfunk_connection_send_input(conn, &ev) };
|
||||
assert_eq!(st, PunktfunkStatus::Ok);
|
||||
// SAFETY: `conn` was returned by `punktfunk_connect` and is never used after this call (session
|
||||
// 2 below uses a fresh `conn2`); `close` takes ownership and frees the handle exactly once.
|
||||
unsafe { punktfunk_connection_close(conn) };
|
||||
|
||||
// Session 2 (same host process — the listener survived): pin the fingerprint.
|
||||
// SAFETY: as for session 1 — `addr` is the live NUL-terminated host string; here
|
||||
// `observed.as_ptr()` is the 32-byte pin (the fingerprint captured above, a valid `[u8; 32]`),
|
||||
// `observed_sha256_out` is NULL and cert/key are NULL. All pointers reference live locals for
|
||||
// the duration of the blocking connect.
|
||||
let conn2 = unsafe {
|
||||
punktfunk_connect(
|
||||
addr.as_ptr(),
|
||||
@@ -3501,11 +3552,17 @@ mod tests {
|
||||
)
|
||||
};
|
||||
assert!(!conn2.is_null(), "pinned reconnect failed");
|
||||
// SAFETY: `conn2` is the live, non-null pinned handle, pulled only from this thread —
|
||||
// `pull_verified`'s requirement.
|
||||
unsafe { pull_verified(conn2, 25) };
|
||||
// SAFETY: `conn2` came from `punktfunk_connect` and is not used after this; `close` frees it once.
|
||||
unsafe { punktfunk_connection_close(conn2) };
|
||||
|
||||
// Session 3: a wrong pin must be rejected by the handshake.
|
||||
let bad = [0xAAu8; 32];
|
||||
// SAFETY: same shape as the prior connects — `addr` is the live host string, `bad.as_ptr()` is
|
||||
// the 32-byte `[0xAA; 32]` pin, and out/cert/key are NULL; all reference live locals across the
|
||||
// blocking call. (The handshake is expected to fail and return NULL here, which is sound.)
|
||||
let conn3 = unsafe {
|
||||
punktfunk_connect(
|
||||
addr.as_ptr(),
|
||||
@@ -3525,6 +3582,8 @@ mod tests {
|
||||
// The host saw the rejected handshake attempt as session 3? No — a TLS-failed
|
||||
// handshake never yields a connection, so accept() is still waiting. Connect once
|
||||
// more (TOFU) to complete the host's third session and let it exit.
|
||||
// SAFETY: same as session 1's connect — `addr` is the live host string, pin/out/cert/key all
|
||||
// NULL; the pointers reference live locals for the duration of the blocking connect.
|
||||
let conn4 = unsafe {
|
||||
punktfunk_connect(
|
||||
addr.as_ptr(),
|
||||
@@ -3540,7 +3599,9 @@ mod tests {
|
||||
)
|
||||
};
|
||||
assert!(!conn4.is_null());
|
||||
// SAFETY: `conn4` is the live, non-null handle, pulled only from this thread.
|
||||
unsafe { pull_verified(conn4, 25) };
|
||||
// SAFETY: `conn4` came from `punktfunk_connect` and is unused after this; `close` frees it once.
|
||||
unsafe { punktfunk_connection_close(conn4) };
|
||||
|
||||
host.join().unwrap().unwrap();
|
||||
|
||||
@@ -622,12 +622,12 @@ mod gamescope;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "vdisplay/linux/kwin.rs"]
|
||||
mod kwin;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "vdisplay/linux/mutter.rs"]
|
||||
mod mutter;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "vdisplay/windows/manager.rs"]
|
||||
pub(crate) mod manager;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "vdisplay/linux/mutter.rs"]
|
||||
mod mutter;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "vdisplay/windows/pf_vdisplay.rs"]
|
||||
pub(crate) mod pf_vdisplay;
|
||||
|
||||
@@ -63,8 +63,12 @@ pub(crate) trait VdisplayDriver: Send + Sync {
|
||||
///
|
||||
/// # Safety
|
||||
/// `dev` must be the live control handle from [`open`](Self::open).
|
||||
unsafe fn add_monitor(&self, dev: HANDLE, mode: Mode, render_luid: Option<LUID>)
|
||||
-> Result<AddedMonitor>;
|
||||
unsafe fn add_monitor(
|
||||
&self,
|
||||
dev: HANDLE,
|
||||
mode: Mode,
|
||||
render_luid: Option<LUID>,
|
||||
) -> Result<AddedMonitor>;
|
||||
/// REMOVE the monitor identified by `key`.
|
||||
///
|
||||
/// # Safety
|
||||
@@ -150,7 +154,8 @@ pub(crate) fn init(driver: Box<dyn VdisplayDriver>) -> &'static VirtualDisplayMa
|
||||
/// The process-wide manager. Panics if reached before a backend called [`init`] — by construction a
|
||||
/// session is only ever created after `vdisplay::open` constructed the backend (which calls `init`).
|
||||
pub(crate) fn vdm() -> &'static VirtualDisplayManager {
|
||||
VDM.get().expect("VirtualDisplayManager used before a backend initialised it")
|
||||
VDM.get()
|
||||
.expect("VirtualDisplayManager used before a backend initialised it")
|
||||
}
|
||||
|
||||
impl VirtualDisplayManager {
|
||||
@@ -178,9 +183,7 @@ impl VirtualDisplayManager {
|
||||
/// The live control handle for the pinger/linger threads (lock-free: the device never changes once
|
||||
/// opened). `None` only before the first acquire opened it.
|
||||
fn device_handle(&self) -> Option<HANDLE> {
|
||||
self.device
|
||||
.get()
|
||||
.map(|d| HANDLE(d.as_raw_handle()))
|
||||
self.device.get().map(|d| HANDLE(d.as_raw_handle()))
|
||||
}
|
||||
|
||||
/// Open + initialise the backend (validates the driver is present). Mirrors the old
|
||||
@@ -203,8 +206,7 @@ impl VirtualDisplayManager {
|
||||
// client is gone). A REUSED IddCx swap-chain is DEAD, so joining it hands a black screen —
|
||||
// PREEMPT: tear the old monitor down (its key/topology are restored) and create a fresh one. The
|
||||
// old session's lease is gen-stamped, so its later drop is a no-op and can't tear down the new one.
|
||||
if idd_push_mode()
|
||||
&& matches!(*state, MgrState::Active { .. } | MgrState::Lingering { .. })
|
||||
if idd_push_mode() && matches!(*state, MgrState::Active { .. } | MgrState::Lingering { .. })
|
||||
{
|
||||
if let MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } =
|
||||
std::mem::replace(&mut *state, MgrState::Idle)
|
||||
@@ -235,14 +237,21 @@ impl VirtualDisplayManager {
|
||||
// `Active` state, held under the `state` lock, so nothing else reconfigures it concurrently.
|
||||
unsafe { self.reconfigure(mon, mode) };
|
||||
}
|
||||
tracing::info!(refs = *refs, backend = self.driver.name(), "virtual monitor reused (concurrent / reconfigure session)");
|
||||
tracing::info!(
|
||||
refs = *refs,
|
||||
backend = self.driver.name(),
|
||||
"virtual monitor reused (concurrent / reconfigure session)"
|
||||
);
|
||||
return Ok(self.output_for(mon));
|
||||
}
|
||||
|
||||
// Idle or Lingering: repurpose a lingering monitor / create a fresh one → Active{refs:1}.
|
||||
let mon = match std::mem::replace(&mut *state, MgrState::Idle) {
|
||||
MgrState::Lingering { mut mon, .. } => {
|
||||
tracing::info!(backend = self.driver.name(), "virtual monitor reused (reconnect within the linger window)");
|
||||
tracing::info!(
|
||||
backend = self.driver.name(),
|
||||
"virtual monitor reused (reconnect within the linger window)"
|
||||
);
|
||||
if mon.mode != mode {
|
||||
// SAFETY: `reconfigure` needs an exclusive `&mut Monitor` and only touches the live
|
||||
// display topology. `mon` is the local monitor just moved out of the `Lingering`
|
||||
@@ -291,7 +300,8 @@ impl VirtualDisplayManager {
|
||||
// Mandatory keepalive: ping inside the watchdog window or the driver tears all displays down.
|
||||
// The pinger reaches the singleton for both the device + the driver — no raw-handle smuggle.
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let interval = Duration::from_millis(self.watchdog_s.load(Ordering::Relaxed) as u64 * 1000 / 3);
|
||||
let interval =
|
||||
Duration::from_millis(self.watchdog_s.load(Ordering::Relaxed) as u64 * 1000 / 3);
|
||||
let stop_t = stop.clone();
|
||||
let pinger = thread::spawn(move || {
|
||||
let mut warned = false;
|
||||
@@ -374,7 +384,10 @@ impl VirtualDisplayManager {
|
||||
/// Touches the live display topology via the CCD/GDI helpers.
|
||||
unsafe fn reconfigure(&self, mon: &mut Monitor, mode: Mode) {
|
||||
tracing::info!(
|
||||
old = format!("{}x{}@{}", mon.mode.width, mon.mode.height, mon.mode.refresh_hz),
|
||||
old = format!(
|
||||
"{}x{}@{}",
|
||||
mon.mode.width, mon.mode.height, mon.mode.refresh_hz
|
||||
),
|
||||
new = format!("{}x{}@{}", mode.width, mode.height, mode.refresh_hz),
|
||||
"virtual-display: reconfiguring reused monitor to the new client mode"
|
||||
);
|
||||
@@ -408,7 +421,10 @@ impl VirtualDisplayManager {
|
||||
if let Err(e) = unsafe { self.driver.remove_monitor(dev, &mon.key) } {
|
||||
tracing::warn!("virtual-display REMOVE failed: {e:#}");
|
||||
} else {
|
||||
tracing::info!(backend = self.driver.name(), "virtual-display monitor removed");
|
||||
tracing::info!(
|
||||
backend = self.driver.name(),
|
||||
"virtual-display monitor removed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,10 +441,16 @@ impl VirtualDisplayManager {
|
||||
return;
|
||||
}
|
||||
*state = match std::mem::replace(&mut *state, MgrState::Idle) {
|
||||
MgrState::Active { mon, refs } if refs > 1 => MgrState::Active { mon, refs: refs - 1 },
|
||||
MgrState::Active { mon, refs } if refs > 1 => MgrState::Active {
|
||||
mon,
|
||||
refs: refs - 1,
|
||||
},
|
||||
MgrState::Active { mon, .. } => {
|
||||
let ms = linger_ms();
|
||||
tracing::info!(linger_ms = ms, "virtual-display: last session left — lingering before teardown");
|
||||
tracing::info!(
|
||||
linger_ms = ms,
|
||||
"virtual-display: last session left — lingering before teardown"
|
||||
);
|
||||
MgrState::Lingering {
|
||||
mon,
|
||||
until: Instant::now() + Duration::from_millis(ms),
|
||||
|
||||
@@ -238,14 +238,13 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
// borrows the local `AddRequest` (alive across this synchronous call) as the input bytes, and
|
||||
// `out` is a stack `[u8; size_of::<AddReply>()]` whose length bounds the kernel's write — both
|
||||
// buffers outlive the call.
|
||||
unsafe { ioctl(dev, control::IOCTL_ADD, bytemuck::bytes_of(&add), &mut out) }.with_context(
|
||||
|| {
|
||||
unsafe { ioctl(dev, control::IOCTL_ADD, bytemuck::bytes_of(&add), &mut out) }
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"pf-vdisplay ADD {}x{}@{}",
|
||||
mode.width, mode.height, mode.refresh_hz
|
||||
)
|
||||
},
|
||||
)?;
|
||||
})?;
|
||||
// `pod_read_unaligned` (NOT `from_bytes`): `out` is a stack `[u8; N]` with no guaranteed 4-byte
|
||||
// alignment, and `from_bytes` PANICS on a mismatch. This copies into an aligned `AddReply`.
|
||||
let reply: control::AddReply =
|
||||
@@ -291,7 +290,15 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
// SAFETY: per `remove_monitor`'s contract `dev` is the live control handle. `bytes_of(&req)`
|
||||
// borrows the local `RemoveRequest` for the duration of this synchronous call as the input
|
||||
// bytes; `none` is empty, so there is no output buffer.
|
||||
unsafe { ioctl(dev, control::IOCTL_REMOVE, bytemuck::bytes_of(&req), &mut none) }.map(|_| ())
|
||||
unsafe {
|
||||
ioctl(
|
||||
dev,
|
||||
control::IOCTL_REMOVE,
|
||||
bytemuck::bytes_of(&req),
|
||||
&mut none,
|
||||
)
|
||||
}
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
unsafe fn ping(&self, dev: HANDLE) -> Result<()> {
|
||||
|
||||
@@ -19,9 +19,9 @@ use windows::Win32::Devices::Display::{
|
||||
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO,
|
||||
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE,
|
||||
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO,
|
||||
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS,
|
||||
SDC_ALLOW_CHANGES, SDC_APPLY, SDC_FORCE_MODE_ENUMERATION, SDC_SAVE_TO_DATABASE,
|
||||
SDC_USE_SUPPLIED_DISPLAY_CONFIG,
|
||||
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME,
|
||||
QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY, SDC_FORCE_MODE_ENUMERATION,
|
||||
SDC_SAVE_TO_DATABASE, SDC_USE_SUPPLIED_DISPLAY_CONFIG,
|
||||
};
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW,
|
||||
|
||||
Reference in New Issue
Block a user