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
|
//! 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).
|
//! 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
|
// Every unsafe block in this module tree carries a `// SAFETY:` proof; enforce it (unsafe-proof
|
||||||
// `#![deny(clippy::undocumented_unsafe_blocks)]` is deliberately NOT set yet: as a parent module it
|
// program). As a parent module this also covers the child modules (capture::windows/linux::*).
|
||||||
// would propagate the lint to `capture::windows::idd_push` (in-flight parallel work, not yet
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
// proven). The deny lands here once every child module (incl. idd_push.rs) is documented.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
//! [`pf_driver_proto::frame`] (which OWNS the contract, with `const` size asserts) — both sides
|
//! [`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.
|
//! `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::dxgi::{make_device, D3d11Frame, HdrP010Converter, VideoConverter, WinCaptureTarget};
|
||||||
use super::{CapturedFrame, Capturer, FramePayload, PixelFormat};
|
use super::{CapturedFrame, Capturer, FramePayload, PixelFormat};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
@@ -225,7 +228,12 @@ pub struct IddPushCapturer {
|
|||||||
status_logged: bool,
|
status_logged: bool,
|
||||||
_keepalive: Box<dyn Send>,
|
_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 {}
|
unsafe impl Send for IddPushCapturer {}
|
||||||
|
|
||||||
/// Build a permissive (Everyone:GenericAll) `SECURITY_ATTRIBUTES` so the restricted WUDFHost driver
|
/// 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
|
// 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
|
// 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.
|
// 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) =
|
let (w, h) =
|
||||||
unsafe { crate::win_display::active_resolution(target.target_id) }.unwrap_or((pw, ph));
|
unsafe { crate::win_display::active_resolution(target.target_id) }.unwrap_or((pw, ph));
|
||||||
if (w, h) != (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
|
// 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,
|
// SDR-only client leaves the display alone (and still gets a tone-mapped picture, never a freeze,
|
||||||
// if the user does enable HDR).
|
// 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 {
|
unsafe {
|
||||||
// If we ENABLE advanced color for a 10-bit client, trust it (the driver will compose FP16) and
|
// 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
|
// 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<()> {
|
fn wait_for_attach(&self) -> Result<()> {
|
||||||
let deadline = Instant::now() + Duration::from_secs(4);
|
let deadline = Instant::now() + Duration::from_secs(4);
|
||||||
loop {
|
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).
|
// log_driver_status_once).
|
||||||
let st = unsafe { (*self.header).driver_status };
|
let st = unsafe { (*self.header).driver_status };
|
||||||
if matches!(st, DRV_STATUS_TEX_FAIL | DRV_STATUS_NO_DEVICE1) {
|
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 };
|
let detail = unsafe { (*self.header).driver_status_detail };
|
||||||
bail!(
|
bail!(
|
||||||
"IDD-push driver failed to attach (driver_status={st} detail=0x{detail:08x} — \
|
"IDD-push driver failed to attach (driver_status={st} detail=0x{detail:08x} — \
|
||||||
@@ -567,6 +605,10 @@ impl IddPushCapturer {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn latest(&self) -> u64 {
|
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 {
|
unsafe {
|
||||||
(*(std::ptr::addr_of!((*self.header).latest) as *const AtomicU64))
|
(*(std::ptr::addr_of!((*self.header).latest) as *const AtomicU64))
|
||||||
.load(Ordering::Acquire)
|
.load(Ordering::Acquire)
|
||||||
@@ -578,6 +620,10 @@ impl IddPushCapturer {
|
|||||||
if self.status_logged {
|
if self.status_logged {
|
||||||
return;
|
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 {
|
let (status, detail, lo, hi) = unsafe {
|
||||||
(
|
(
|
||||||
(*self.header).driver_status,
|
(*self.header).driver_status,
|
||||||
@@ -617,6 +663,11 @@ impl IddPushCapturer {
|
|||||||
tracing::warn!("IDD push DEBUG: no debug block");
|
tracing::warn!("IDD push DEBUG: no debug block");
|
||||||
return;
|
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 };
|
let d = unsafe { &*self.dbg_block };
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
run_core_entries = d.run_core_entries,
|
run_core_entries = d.run_core_entries,
|
||||||
@@ -666,6 +717,10 @@ impl IddPushCapturer {
|
|||||||
self.height = new_h;
|
self.height = new_h;
|
||||||
let fmt = self.ring_format();
|
let fmt = self.ring_format();
|
||||||
let new_gen = IDD_GENERATION.fetch_add(1, Ordering::Relaxed);
|
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 {
|
let new_slots = unsafe {
|
||||||
Self::create_ring_slots(
|
Self::create_ring_slots(
|
||||||
&self.device,
|
&self.device,
|
||||||
@@ -676,6 +731,12 @@ impl IddPushCapturer {
|
|||||||
fmt,
|
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 {
|
unsafe {
|
||||||
// Clear `latest` to the 0 sentinel (generation 0, which try_consume rejects). The real guard
|
// 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
|
// against consuming an unwritten new-ring slot is the generation tag in `latest`: a stale
|
||||||
@@ -711,9 +772,13 @@ impl IddPushCapturer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.last_acm_poll = Instant::now();
|
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) };
|
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
|
// 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.
|
// 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) }
|
let (now_w, now_h) = unsafe { crate::win_display::active_resolution(self.target_id) }
|
||||||
.unwrap_or((self.width, self.height));
|
.unwrap_or((self.width, self.height));
|
||||||
if now_hdr == self.display_hdr && now_w == self.width && now_h == 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 {
|
for _ in 0..OUT_RING {
|
||||||
let mut t: Option<ID3D11Texture2D> = None;
|
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 {
|
unsafe {
|
||||||
self.device
|
self.device
|
||||||
.CreateTexture2D(&desc, None, Some(&mut t))
|
.CreateTexture2D(&desc, None, Some(&mut t))
|
||||||
@@ -775,9 +844,16 @@ impl IddPushCapturer {
|
|||||||
fn ensure_converter(&mut self) -> Result<()> {
|
fn ensure_converter(&mut self) -> Result<()> {
|
||||||
if self.display_hdr {
|
if self.display_hdr {
|
||||||
if self.hdr_p010_conv.is_none() {
|
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)? });
|
self.hdr_p010_conv = Some(unsafe { HdrP010Converter::new(&self.device)? });
|
||||||
}
|
}
|
||||||
} else if self.video_conv.is_none() {
|
} 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 {
|
self.video_conv = Some(unsafe {
|
||||||
VideoConverter::new(&self.device, &self.context, self.width, self.height, false)?
|
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`.
|
/// 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 {
|
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() } {
|
if let Some(l) = unsafe { crate::win_adapter::resolve_render_adapter_luid() } {
|
||||||
return l;
|
return l;
|
||||||
}
|
}
|
||||||
@@ -955,6 +1033,9 @@ impl Capturer for IddPushCapturer {
|
|||||||
fn next_frame(&mut self) -> Result<CapturedFrame> {
|
fn next_frame(&mut self) -> Result<CapturedFrame> {
|
||||||
let deadline = Instant::now() + Duration::from_secs(20);
|
let deadline = Instant::now() + Duration::from_secs(20);
|
||||||
loop {
|
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) };
|
let _ = unsafe { WaitForSingleObject(HANDLE(self.event.as_raw_handle()), 16) };
|
||||||
if let Some(f) = self.try_consume()? {
|
if let Some(f) = self.try_consume()? {
|
||||||
return Ok(f);
|
return Ok(f);
|
||||||
@@ -964,6 +1045,9 @@ impl Capturer for IddPushCapturer {
|
|||||||
}
|
}
|
||||||
if Instant::now() > deadline {
|
if Instant::now() > deadline {
|
||||||
self.log_debug_block();
|
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 {
|
let (st, detail, lo, hi) = unsafe {
|
||||||
(
|
(
|
||||||
(*self.header).driver_status,
|
(*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
|
//! 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
|
//! fallback swscales RGB→NV12, the zero-copy path imports the capture dmabuf straight into a
|
||||||
//! VA surface). One [`Encoder`] trait, selected in [`open_video`].
|
//! VA surface). One [`Encoder`] trait, selected in [`open_video`].
|
||||||
// This file's own unsafe block carries a `// SAFETY:` proof, but the file-level
|
// Every unsafe block in this module tree carries a `// SAFETY:` proof; enforce it (unsafe-proof
|
||||||
// `#![deny(clippy::undocumented_unsafe_blocks)]` is deliberately NOT set yet: as a parent module it
|
// program). As a parent module this also covers the child modules (encode::windows/linux::*).
|
||||||
// would propagate the lint to `encode::windows::nvenc` (in-flight parallel work, not yet proven).
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
// The deny lands here once every child module (incl. nvenc.rs) is documented.
|
|
||||||
|
|
||||||
use crate::capture::{CapturedFrame, PixelFormat};
|
use crate::capture::{CapturedFrame, PixelFormat};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
//! Needs a real NVIDIA GPU at runtime (session creation fails otherwise) — compiles GPU-less, but
|
//! 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.
|
//! `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 super::{Codec, EncodedFrame, Encoder, EncoderCaps};
|
||||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
@@ -88,7 +91,15 @@ pub struct NvencD3d11Encoder {
|
|||||||
init_device: *mut c_void,
|
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 {}
|
unsafe impl Send for NvencD3d11Encoder {}
|
||||||
|
|
||||||
impl 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).
|
/// Lazily create the session on the first frame's D3D11 device (so capture + encode share it).
|
||||||
fn init_session(&mut self, device: &ID3D11Device) -> Result<()> {
|
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 {
|
unsafe {
|
||||||
// Probe real GPU caps first (max dims / 10-bit / custom-VBV / RFI) so the config below is
|
// 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
|
// 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),
|
new = format!("{}x{}", captured.width, captured.height),
|
||||||
"NVENC: capture device/size/HDR changed — re-initializing session"
|
"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() };
|
unsafe { self.teardown() };
|
||||||
}
|
}
|
||||||
if !self.inited {
|
if !self.inited {
|
||||||
@@ -625,6 +652,21 @@ impl Encoder for NvencD3d11Encoder {
|
|||||||
}
|
}
|
||||||
let slot = self.next % POOL;
|
let slot = self.next % POOL;
|
||||||
self.next += 1;
|
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 {
|
unsafe {
|
||||||
// Register the capturer's texture with NVENC once (cached by raw pointer), then encode it
|
// 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
|
// 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
|
// 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
|
// 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.
|
// 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 {
|
unsafe {
|
||||||
for ts in first..=last {
|
for ts in first..=last {
|
||||||
if (API.invalidate_ref_frames)(self.encoder, ts as u64)
|
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 {
|
let Some((bs, map, pts_ns)) = self.pending.pop_front() else {
|
||||||
return Ok(None);
|
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 {
|
unsafe {
|
||||||
let mut lock = nv::NV_ENC_LOCK_BITSTREAM {
|
let mut lock = nv::NV_ENC_LOCK_BITSTREAM {
|
||||||
version: nv::NV_ENC_LOCK_BITSTREAM_VER,
|
version: nv::NV_ENC_LOCK_BITSTREAM_VER,
|
||||||
@@ -838,6 +896,11 @@ impl Encoder for NvencD3d11Encoder {
|
|||||||
|
|
||||||
impl Drop for NvencD3d11Encoder {
|
impl Drop for NvencD3d11Encoder {
|
||||||
fn drop(&mut self) {
|
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() };
|
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_INPUT: usize = core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, input);
|
||||||
pub(super) const OFF_OUT_SEQ: usize =
|
pub(super) const OFF_OUT_SEQ: usize =
|
||||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, out_seq);
|
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 =
|
/// 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.
|
/// DualSense (the default — the section is zeroed), 1 = DualShock 4.
|
||||||
pub(super) const OFF_DEVTYPE: usize =
|
pub(super) const OFF_DEVTYPE: usize =
|
||||||
|
|||||||
@@ -187,8 +187,10 @@ impl XusbWinPad {
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn write_state(&mut self, buttons: u16, lt: u8, rt: u8, lx: i16, ly: i16, rx: i16, ry: i16) {
|
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);
|
self.packet = self.packet.wrapping_add(1);
|
||||||
// SAFETY: base points at SHM_SIZE bytes; all offsets are in range.
|
|
||||||
let base = self.shm.base();
|
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 {
|
unsafe {
|
||||||
std::ptr::write_unaligned(base.add(OFF_BUTTONS) as *mut u16, buttons);
|
std::ptr::write_unaligned(base.add(OFF_BUTTONS) as *mut u16, buttons);
|
||||||
*base.add(OFF_LT) = lt;
|
*base.add(OFF_LT) = lt;
|
||||||
|
|||||||
@@ -238,7 +238,8 @@ impl InputInjector for SendInputInjector {
|
|||||||
}
|
}
|
||||||
InputKind::KeyDown | InputKind::KeyUp => {
|
InputKind::KeyDown | InputKind::KeyUp => {
|
||||||
let down = event.kind == InputKind::KeyDown;
|
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
|
// 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`
|
// 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.
|
// 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.
|
// Scaffold: trait methods and config paths are defined ahead of their backends.
|
||||||
#![allow(dead_code)]
|
#![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 audio;
|
||||||
mod capture;
|
mod capture;
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
//! Trust: the host serves with its persistent identity (`~/.config/punktfunk/cert.pem`, shared
|
//! Trust: the host serves with its persistent identity (`~/.config/punktfunk/cert.pem`, shared
|
||||||
//! with GameStream pairing) and logs the SHA-256 fingerprint clients pin.
|
//! 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 anyhow::{anyhow, Context, Result};
|
||||||
use punktfunk_core::config::{CompositorPref, FecConfig, FecScheme, GamepadPref, Role};
|
use punktfunk_core::config::{CompositorPref, FecConfig, FecScheme, GamepadPref, Role};
|
||||||
use punktfunk_core::input::{InputEvent, InputKind};
|
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).
|
// capture/encode (critical) and send (non-critical).
|
||||||
crate::session_tuning::on_hot_thread();
|
crate::session_tuning::on_hot_thread();
|
||||||
#[cfg(target_os = "windows")]
|
#[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 {
|
unsafe {
|
||||||
use windows::Win32::System::Threading::{
|
use windows::Win32::System::Threading::{
|
||||||
GetCurrentThread, SetThreadPriority, THREAD_PRIORITY_ABOVE_NORMAL,
|
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
|
// 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).
|
// very frame-time we refuse to add (opt-in only — see PUNKTFUNK_SCHED_RR).
|
||||||
let nice = if critical { -10 } else { -5 };
|
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) };
|
let rc = unsafe { libc::setpriority(libc::PRIO_PROCESS, 0, nice) };
|
||||||
if rc == 0 {
|
if rc == 0 {
|
||||||
tracing::debug!(critical, nice, "thread nice raised");
|
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.
|
// The secure-desktop HDR drop (for the DDA leg) keys off the monitor's real state in the mux loop.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
if bit_depth >= 10 {
|
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 {
|
unsafe {
|
||||||
if crate::win_display::set_advanced_color(target.target_id, true) {
|
if crate::win_display::set_advanced_color(target.target_id, true) {
|
||||||
// Let the colorspace change settle before WGC creates its capture item / detects HDR.
|
// 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,
|
// 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
|
// 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.
|
// overlay/flip issues that drove us to WGC don't apply to the composed Winlogon UI.
|
||||||
let hdr =
|
// SAFETY: `advanced_color_enabled` is `unsafe` only because it queries the Win32 CCD
|
||||||
unsafe { crate::win_display::advanced_color_enabled(target.target_id) };
|
// 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
|
dda = None; // reopen to capture the secure desktop
|
||||||
match open_dda(&target, cur_mode.width, cur_mode.height, effective_hz, hdr) {
|
match open_dda(&target, cur_mode.width, cur_mode.height, effective_hz, hdr) {
|
||||||
Ok(mut p) => {
|
Ok(mut p) => {
|
||||||
@@ -3368,12 +3389,27 @@ mod tests {
|
|||||||
unsafe fn pull_verified(conn: *mut punktfunk_core::abi::PunktfunkConnection, count: u32) {
|
unsafe fn pull_verified(conn: *mut punktfunk_core::abi::PunktfunkConnection, count: u32) {
|
||||||
use punktfunk_core::error::PunktfunkStatus;
|
use punktfunk_core::error::PunktfunkStatus;
|
||||||
let mut got = 0u32;
|
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() };
|
let mut frame = unsafe { std::mem::zeroed() };
|
||||||
while got < count {
|
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 {
|
match unsafe {
|
||||||
punktfunk_core::abi::punktfunk_connection_next_au(conn, &mut frame, 2000)
|
punktfunk_core::abi::punktfunk_connection_next_au(conn, &mut frame, 2000)
|
||||||
} {
|
} {
|
||||||
PunktfunkStatus::Ok => {
|
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 data = unsafe { std::slice::from_raw_parts(frame.data, frame.len) };
|
||||||
let idx = u32::from_le_bytes(data[0..4].try_into().unwrap());
|
let idx = u32::from_le_bytes(data[0..4].try_into().unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -3421,6 +3457,11 @@ mod tests {
|
|||||||
// Session 1: TOFU (no pin) — observe the host fingerprint.
|
// Session 1: TOFU (no pin) — observe the host fingerprint.
|
||||||
let addr = std::ffi::CString::new("127.0.0.1").unwrap();
|
let addr = std::ffi::CString::new("127.0.0.1").unwrap();
|
||||||
let mut observed = [0u8; 32];
|
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 {
|
let conn = unsafe {
|
||||||
punktfunk_connect(
|
punktfunk_connect(
|
||||||
addr.as_ptr(),
|
addr.as_ptr(),
|
||||||
@@ -3439,26 +3480,28 @@ mod tests {
|
|||||||
assert_ne!(observed, [0u8; 32], "fingerprint not reported");
|
assert_ne!(observed, [0u8; 32], "fingerprint not reported");
|
||||||
|
|
||||||
let (mut w, mut h, mut hz) = (0u32, 0u32, 0u32);
|
let (mut w, mut h, mut hz) = (0u32, 0u32, 0u32);
|
||||||
assert_eq!(
|
// SAFETY: `conn` is the live, non-null connection handle just asserted above; `&mut w/h/hz` are
|
||||||
unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) },
|
// exclusive, writable borrows of local `u32`s that outlive this synchronous call — the three
|
||||||
PunktfunkStatus::Ok
|
// 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));
|
assert_eq!((w, h, hz), (1280, 720, 60));
|
||||||
|
|
||||||
// Mid-stream renegotiation: request a new mode, the host acks on the control
|
// Mid-stream renegotiation: request a new mode, the host acks on the control
|
||||||
// stream, and punktfunk_connection_mode reflects the switch.
|
// stream, and punktfunk_connection_mode reflects the switch.
|
||||||
assert_eq!(
|
// SAFETY: `conn` is the live, non-null connection handle (the only pointer arg); the remaining
|
||||||
unsafe {
|
// 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)
|
punktfunk_core::abi::punktfunk_connection_request_mode(conn, 1920, 1080, 144)
|
||||||
},
|
};
|
||||||
PunktfunkStatus::Ok
|
assert_eq!(st, PunktfunkStatus::Ok);
|
||||||
);
|
|
||||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||||
loop {
|
loop {
|
||||||
assert_eq!(
|
// SAFETY: same as the earlier `punktfunk_connection_mode` call — `conn` is the live handle
|
||||||
unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) },
|
// and `&mut w/h/hz` are exclusive writable borrows of locals that outlive this synchronous
|
||||||
PunktfunkStatus::Ok
|
// 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) {
|
if (w, h, hz) == (1920, 1080, 144) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -3469,6 +3512,8 @@ mod tests {
|
|||||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
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) };
|
unsafe { pull_verified(conn, 25) };
|
||||||
|
|
||||||
let ev = punktfunk_core::input::InputEvent {
|
let ev = punktfunk_core::input::InputEvent {
|
||||||
@@ -3479,13 +3524,19 @@ mod tests {
|
|||||||
y: 2,
|
y: 2,
|
||||||
flags: 0,
|
flags: 0,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
// SAFETY: `conn` is the live handle; `&ev` borrows the local `InputEvent`, valid and immutable
|
||||||
unsafe { punktfunk_connection_send_input(conn, &ev) },
|
// for this synchronous enqueue — the contract's "valid InputEvent" pointer.
|
||||||
PunktfunkStatus::Ok
|
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) };
|
unsafe { punktfunk_connection_close(conn) };
|
||||||
|
|
||||||
// Session 2 (same host process — the listener survived): pin the fingerprint.
|
// 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 {
|
let conn2 = unsafe {
|
||||||
punktfunk_connect(
|
punktfunk_connect(
|
||||||
addr.as_ptr(),
|
addr.as_ptr(),
|
||||||
@@ -3501,11 +3552,17 @@ mod tests {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
assert!(!conn2.is_null(), "pinned reconnect failed");
|
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) };
|
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) };
|
unsafe { punktfunk_connection_close(conn2) };
|
||||||
|
|
||||||
// Session 3: a wrong pin must be rejected by the handshake.
|
// Session 3: a wrong pin must be rejected by the handshake.
|
||||||
let bad = [0xAAu8; 32];
|
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 {
|
let conn3 = unsafe {
|
||||||
punktfunk_connect(
|
punktfunk_connect(
|
||||||
addr.as_ptr(),
|
addr.as_ptr(),
|
||||||
@@ -3525,6 +3582,8 @@ mod tests {
|
|||||||
// The host saw the rejected handshake attempt as session 3? No — a TLS-failed
|
// 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
|
// 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.
|
// 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 {
|
let conn4 = unsafe {
|
||||||
punktfunk_connect(
|
punktfunk_connect(
|
||||||
addr.as_ptr(),
|
addr.as_ptr(),
|
||||||
@@ -3540,7 +3599,9 @@ mod tests {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
assert!(!conn4.is_null());
|
assert!(!conn4.is_null());
|
||||||
|
// SAFETY: `conn4` is the live, non-null handle, pulled only from this thread.
|
||||||
unsafe { pull_verified(conn4, 25) };
|
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) };
|
unsafe { punktfunk_connection_close(conn4) };
|
||||||
|
|
||||||
host.join().unwrap().unwrap();
|
host.join().unwrap().unwrap();
|
||||||
|
|||||||
@@ -622,12 +622,12 @@ mod gamescope;
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[path = "vdisplay/linux/kwin.rs"]
|
#[path = "vdisplay/linux/kwin.rs"]
|
||||||
mod kwin;
|
mod kwin;
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[path = "vdisplay/linux/mutter.rs"]
|
|
||||||
mod mutter;
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
#[path = "vdisplay/windows/manager.rs"]
|
#[path = "vdisplay/windows/manager.rs"]
|
||||||
pub(crate) mod manager;
|
pub(crate) mod manager;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[path = "vdisplay/linux/mutter.rs"]
|
||||||
|
mod mutter;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
#[path = "vdisplay/windows/pf_vdisplay.rs"]
|
#[path = "vdisplay/windows/pf_vdisplay.rs"]
|
||||||
pub(crate) mod pf_vdisplay;
|
pub(crate) mod pf_vdisplay;
|
||||||
|
|||||||
@@ -63,8 +63,12 @@ pub(crate) trait VdisplayDriver: Send + Sync {
|
|||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// `dev` must be the live control handle from [`open`](Self::open).
|
/// `dev` must be the live control handle from [`open`](Self::open).
|
||||||
unsafe fn add_monitor(&self, dev: HANDLE, mode: Mode, render_luid: Option<LUID>)
|
unsafe fn add_monitor(
|
||||||
-> Result<AddedMonitor>;
|
&self,
|
||||||
|
dev: HANDLE,
|
||||||
|
mode: Mode,
|
||||||
|
render_luid: Option<LUID>,
|
||||||
|
) -> Result<AddedMonitor>;
|
||||||
/// REMOVE the monitor identified by `key`.
|
/// REMOVE the monitor identified by `key`.
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # 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
|
/// 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`).
|
/// session is only ever created after `vdisplay::open` constructed the backend (which calls `init`).
|
||||||
pub(crate) fn vdm() -> &'static VirtualDisplayManager {
|
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 {
|
impl VirtualDisplayManager {
|
||||||
@@ -178,9 +183,7 @@ impl VirtualDisplayManager {
|
|||||||
/// The live control handle for the pinger/linger threads (lock-free: the device never changes once
|
/// 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.
|
/// opened). `None` only before the first acquire opened it.
|
||||||
fn device_handle(&self) -> Option<HANDLE> {
|
fn device_handle(&self) -> Option<HANDLE> {
|
||||||
self.device
|
self.device.get().map(|d| HANDLE(d.as_raw_handle()))
|
||||||
.get()
|
|
||||||
.map(|d| HANDLE(d.as_raw_handle()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open + initialise the backend (validates the driver is present). Mirrors the old
|
/// 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 —
|
// 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
|
// 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.
|
// 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()
|
if idd_push_mode() && matches!(*state, MgrState::Active { .. } | MgrState::Lingering { .. })
|
||||||
&& matches!(*state, MgrState::Active { .. } | MgrState::Lingering { .. })
|
|
||||||
{
|
{
|
||||||
if let MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } =
|
if let MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } =
|
||||||
std::mem::replace(&mut *state, MgrState::Idle)
|
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.
|
// `Active` state, held under the `state` lock, so nothing else reconfigures it concurrently.
|
||||||
unsafe { self.reconfigure(mon, mode) };
|
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));
|
return Ok(self.output_for(mon));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Idle or Lingering: repurpose a lingering monitor / create a fresh one → Active{refs:1}.
|
// Idle or Lingering: repurpose a lingering monitor / create a fresh one → Active{refs:1}.
|
||||||
let mon = match std::mem::replace(&mut *state, MgrState::Idle) {
|
let mon = match std::mem::replace(&mut *state, MgrState::Idle) {
|
||||||
MgrState::Lingering { mut mon, .. } => {
|
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 {
|
if mon.mode != mode {
|
||||||
// SAFETY: `reconfigure` needs an exclusive `&mut Monitor` and only touches the live
|
// 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`
|
// 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.
|
// 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.
|
// The pinger reaches the singleton for both the device + the driver — no raw-handle smuggle.
|
||||||
let stop = Arc::new(AtomicBool::new(false));
|
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 stop_t = stop.clone();
|
||||||
let pinger = thread::spawn(move || {
|
let pinger = thread::spawn(move || {
|
||||||
let mut warned = false;
|
let mut warned = false;
|
||||||
@@ -374,7 +384,10 @@ impl VirtualDisplayManager {
|
|||||||
/// Touches the live display topology via the CCD/GDI helpers.
|
/// Touches the live display topology via the CCD/GDI helpers.
|
||||||
unsafe fn reconfigure(&self, mon: &mut Monitor, mode: Mode) {
|
unsafe fn reconfigure(&self, mon: &mut Monitor, mode: Mode) {
|
||||||
tracing::info!(
|
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),
|
new = format!("{}x{}@{}", mode.width, mode.height, mode.refresh_hz),
|
||||||
"virtual-display: reconfiguring reused monitor to the new client mode"
|
"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) } {
|
if let Err(e) = unsafe { self.driver.remove_monitor(dev, &mon.key) } {
|
||||||
tracing::warn!("virtual-display REMOVE failed: {e:#}");
|
tracing::warn!("virtual-display REMOVE failed: {e:#}");
|
||||||
} else {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
*state = match std::mem::replace(&mut *state, MgrState::Idle) {
|
*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, .. } => {
|
MgrState::Active { mon, .. } => {
|
||||||
let ms = linger_ms();
|
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 {
|
MgrState::Lingering {
|
||||||
mon,
|
mon,
|
||||||
until: Instant::now() + Duration::from_millis(ms),
|
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
|
// 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
|
// `out` is a stack `[u8; size_of::<AddReply>()]` whose length bounds the kernel's write — both
|
||||||
// buffers outlive the call.
|
// 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!(
|
format!(
|
||||||
"pf-vdisplay ADD {}x{}@{}",
|
"pf-vdisplay ADD {}x{}@{}",
|
||||||
mode.width, mode.height, mode.refresh_hz
|
mode.width, mode.height, mode.refresh_hz
|
||||||
)
|
)
|
||||||
},
|
})?;
|
||||||
)?;
|
|
||||||
// `pod_read_unaligned` (NOT `from_bytes`): `out` is a stack `[u8; N]` with no guaranteed 4-byte
|
// `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`.
|
// alignment, and `from_bytes` PANICS on a mismatch. This copies into an aligned `AddReply`.
|
||||||
let reply: control::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)`
|
// 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
|
// borrows the local `RemoveRequest` for the duration of this synchronous call as the input
|
||||||
// bytes; `none` is empty, so there is no output buffer.
|
// 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<()> {
|
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,
|
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO,
|
||||||
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE,
|
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_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO,
|
||||||
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS,
|
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME,
|
||||||
SDC_ALLOW_CHANGES, SDC_APPLY, SDC_FORCE_MODE_ENUMERATION, SDC_SAVE_TO_DATABASE,
|
QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY, SDC_FORCE_MODE_ENUMERATION,
|
||||||
SDC_USE_SUPPLIED_DISPLAY_CONFIG,
|
SDC_SAVE_TO_DATABASE, SDC_USE_SUPPLIED_DISPLAY_CONFIG,
|
||||||
};
|
};
|
||||||
use windows::Win32::Graphics::Gdi::{
|
use windows::Win32::Graphics::Gdi::{
|
||||||
ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW,
|
ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW,
|
||||||
|
|||||||
Reference in New Issue
Block a user