refactor(windows-host): §2.5 step 2 — unify both backends behind VirtualDisplayManager (OnceLock)

The two Windows virtual-display backends (sudovda + pf_vdisplay) carried VERBATIM-DUPLICATED
~250-line Idle/Active/Lingering refcount state machines in two `MGR: Mutex<Mgr>` globals, each
smuggling the control HANDLE across the pinger/linger threads as a raw `isize` (HANDLE is !Send).

New `vdisplay/windows/manager.rs`: one host-lifetime `VirtualDisplayManager` (OnceLock singleton,
user-approved) owns the earned state machine + the linger timer + a TYPED `Arc<OwnedHandle>`
control device (the raw-isize smuggle is gone — OwnedHandle is Send+Sync and also CloseHandle's
the device on drop, fixing a latent leak). The only backend-specific code left is the IOCTL
surface behind a small `VdisplayDriver` trait (open/add_monitor/remove_monitor/ping) + the
per-monitor REMOVE key (`MonitorKey::Guid` for sudovda, `::Session(u64)` for pf-vdisplay). The
render-adapter pin decision, the GDI/CCD glue (crate::win_display), and the gen-stamped
MonitorLease are backend-neutral and live once in the manager.

  * sudovda.rs / pf_vdisplay.rs: shrink to a `VdisplayDriver` impl + a thin `VirtualDisplay`
    wrapper (new() -> manager::init(driver); create() -> manager::vdm().acquire(mode)). Their
    IOCTL ops + structs + open_device stay in place (no transcription).
  * MON_GEN -> a manager field; the preempt's wait_for_monitor_released moves onto the manager
    (punktfunk1 calls vdm().wait_for_monitor_released). MonitorLease.drop -> vdm().release(gen),
    with the stale-lease no-op preserved verbatim.

Behaviour-preserving: the state machine (acquire/release/reconfigure/teardown/linger/preempt) is
the canonical sudovda copy with the IOCTLs routed through the driver seam. Box build to follow
(Windows-only; Linux check is a no-op for these files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 19:50:34 +00:00
parent 15202011c1
commit 4b56c895a0
5 changed files with 671 additions and 981 deletions
+2 -1
View File
@@ -2255,7 +2255,8 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
let prev = IDD_SESSION_STOP.lock().unwrap().replace(stop.clone()); let prev = IDD_SESSION_STOP.lock().unwrap().replace(stop.clone());
if let Some(prev_stop) = prev { if let Some(prev_stop) = prev {
prev_stop.store(true, Ordering::SeqCst); prev_stop.store(true, Ordering::SeqCst);
crate::vdisplay::sudovda::wait_for_monitor_released(std::time::Duration::from_secs(3)); crate::vdisplay::manager::vdm()
.wait_for_monitor_released(std::time::Duration::from_secs(3));
} }
} }
let mut vd = crate::vdisplay::open(compositor)?; let mut vd = crate::vdisplay::open(compositor)?;
+3
View File
@@ -635,6 +635,9 @@ mod kwin;
#[path = "vdisplay/linux/mutter.rs"] #[path = "vdisplay/linux/mutter.rs"]
mod mutter; mod mutter;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[path = "vdisplay/windows/manager.rs"]
pub(crate) mod manager;
#[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;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@@ -0,0 +1,496 @@
//! Host-lifetime virtual-display **ownership model** (Goal-1 §2.5). One reference-counted monitor
//! lifecycle, shared by both Windows backends (SudoVDA + pf-vdisplay) instead of the two verbatim-
//! duplicated `MGR: Mutex<Mgr>` globals each backend used to carry.
//!
//! [`VirtualDisplayManager`] owns the earned Idle/Active/Lingering refcount machine + the linger timer +
//! a **typed** [`OwnedHandle`] control device (no more raw `isize` smuggled across the pinger/linger
//! threads). The backend differences — the IOCTL protocol and the per-monitor REMOVE key — are the only
//! thing behind the [`VdisplayDriver`] seam; the state machine, the render-adapter pin decision, the
//! GDI/CCD glue (`crate::win_display`), and the generation-stamped [`MonitorLease`] are backend-neutral.
//!
//! It's a process-wide singleton ([`vdm`]) initialised once with the chosen backend's driver — the
//! host runs exactly one virtual-display backend per process. The session holds a [`MonitorLease`];
//! its `Drop` releases the refcount (a *stale* lease — its monitor was preempted + recreated under it —
//! is a no-op, so it can never tear down the live monitor).
use std::ffi::c_void;
use std::os::windows::io::{AsRawHandle, OwnedHandle};
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, Once, OnceLock};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use anyhow::Result;
use windows::Win32::Foundation::{HANDLE, LUID};
use super::{Mode, VirtualOutput};
use crate::win_display::{
isolate_displays_ccd, resolve_gdi_name, restore_displays_ccd, set_active_mode, SavedConfig,
};
/// The per-backend REMOVE key the driver stamps on ADD and consumes on REMOVE. SudoVDA keys monitors by
/// a fresh `GUID`; pf-vdisplay keys them by a monotonic `u64` session id.
#[derive(Clone, Copy)]
pub(crate) enum MonitorKey {
Guid(windows::core::GUID),
Session(u64),
}
/// What a backend's `add_monitor` returns: the REMOVE key + the OS target id + the render LUID.
pub(crate) struct AddedMonitor {
pub key: MonitorKey,
pub target_id: u32,
pub luid: LUID,
}
/// The backend-specific IOCTL surface — the *only* thing that differs between SudoVDA and pf-vdisplay.
/// Everything else (the refcount machine, the linger, the pinger, the CCD/GDI glue) is shared in
/// [`VirtualDisplayManager`]. `Send + Sync` because the manager (and so the boxed driver) is a
/// `&'static` singleton reached from the pinger + linger threads.
pub(crate) trait VdisplayDriver: Send + Sync {
fn name(&self) -> &'static str;
/// Find + open the control device, validate it (version handshake), read the watchdog timeout, and
/// reap monitors orphaned by a crashed previous host (`CLEAR_ALL`). Returns the owned handle +
/// watchdog seconds.
///
/// # Safety
/// Issues setup-API + `DeviceIoControl` calls; runs in the caller's apartment.
unsafe fn open(&self) -> Result<(OwnedHandle, u32)>;
/// ADD a virtual monitor at `mode`, pinning the IDD render GPU to `render_luid` first if `Some`.
/// Returns the REMOVE key + target id + the adapter LUID the driver actually used.
///
/// # 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>;
/// REMOVE the monitor identified by `key`.
///
/// # Safety
/// `dev` must be the live control handle.
unsafe fn remove_monitor(&self, dev: HANDLE, key: &MonitorKey) -> Result<()>;
/// Watchdog keepalive PING (issued every `watchdog/3` from the pinger thread).
///
/// # Safety
/// `dev` must be the live control handle.
unsafe fn ping(&self, dev: HANDLE) -> Result<()>;
}
/// The resources backing one live virtual monitor (owned by the [`VirtualDisplayManager`] state, not by
/// any session). No `Drop` impl — [`teardown`](VirtualDisplayManager::teardown) must be called so the
/// REMOVE IOCTL fires (a bare drop would orphan the driver-side monitor).
struct Monitor {
key: MonitorKey,
target_id: u32,
luid: LUID,
gdi_name: Option<String>,
mode: Mode,
stop: Arc<AtomicBool>,
pinger: Option<JoinHandle<()>>,
ccd_saved: Option<SavedConfig>,
/// Generation stamp; a [`MonitorLease`] only releases if its gen still matches (stale-lease no-op).
gen: u64,
}
impl Monitor {
/// The capture target handed to a session (`None` until the GDI name resolves on a WDDM GPU).
fn target(&self) -> Option<crate::capture::dxgi::WinCaptureTarget> {
self.gdi_name
.clone()
.map(|n| crate::capture::dxgi::WinCaptureTarget {
adapter_luid: crate::capture::dxgi::pack_luid(self.luid),
gdi_name: n,
target_id: self.target_id,
})
}
}
enum MgrState {
Idle,
Active { mon: Monitor, refs: u32 },
Lingering { mon: Monitor, until: Instant },
}
/// The host-lifetime virtual-display manager: the single owner of the monitor lifecycle.
pub(crate) struct VirtualDisplayManager {
driver: Box<dyn VdisplayDriver>,
/// Control device, opened once on first acquire. Typed + `Send+Sync`, so the pinger/linger threads
/// share it via the `&'static` singleton with no raw-handle smuggling.
device: OnceLock<Arc<OwnedHandle>>,
watchdog_s: AtomicU32,
/// Monotonic lease-generation counter (was the `MON_GEN` global).
gen: AtomicU64,
state: Mutex<MgrState>,
}
static VDM: OnceLock<VirtualDisplayManager> = OnceLock::new();
/// Initialise the process-wide manager with `driver` (the chosen backend) and return it. Idempotent: the
/// first backend to call wins (the host runs one backend per process), so a later call ignores its driver.
pub(crate) fn init(driver: Box<dyn VdisplayDriver>) -> &'static VirtualDisplayManager {
VDM.get_or_init(|| VirtualDisplayManager {
driver,
device: OnceLock::new(),
watchdog_s: AtomicU32::new(3),
gen: AtomicU64::new(1),
state: Mutex::new(MgrState::Idle),
})
}
/// 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")
}
impl VirtualDisplayManager {
pub(crate) fn backend_name(&self) -> &'static str {
self.driver.name()
}
/// Open + cache the control device (once). Called under the `state` lock so two racing acquires can't
/// double-open.
fn ensure_device(&self) -> Result<HANDLE> {
if let Some(d) = self.device.get() {
return Ok(HANDLE(d.as_raw_handle() as *mut c_void));
}
let (handle, watchdog_s) = unsafe { self.driver.open()? };
self.watchdog_s.store(watchdog_s, Ordering::Relaxed);
let raw = HANDLE(handle.as_raw_handle() as *mut c_void);
let _ = self.device.set(Arc::new(handle));
Ok(raw)
}
/// 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() as *mut c_void))
}
/// Open + initialise the backend (validates the driver is present). Mirrors the old
/// `SudoVdaDisplay::new`/`PfVdisplayDisplay::new`.
pub(crate) fn open_backend(&self) -> Result<()> {
let _ = self.state.lock().unwrap();
self.ensure_device().map(|_| ())
}
/// Acquire the shared monitor for a new session: preempt-recreate under IDD-push, join a live one
/// (refcount++), reuse a lingering one, or create one. The returned [`MonitorLease`] releases the
/// refcount on drop.
pub(crate) fn acquire(&'static self, mode: Mode) -> Result<VirtualOutput> {
self.ensure_linger_timer();
let mut state = self.state.lock().unwrap();
let dev = self.ensure_device()?;
// IDD-push: a new connection while a monitor is live is a single-client RECONNECT (the prior
// 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 let MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } =
std::mem::replace(&mut *state, MgrState::Idle)
{
tracing::info!(
old_target = mon.target_id,
"IDD-push reconnect — preempting the prior session, recreating a fresh monitor"
);
unsafe { self.teardown(dev, mon) };
// Let the OS finish the ASYNC monitor departure before the next ADD; a back-to-back
// REMOVE→ADD races the teardown and the ADD IOCTL is rejected under reconnect churn.
thread::sleep(Duration::from_millis(400));
}
}
// A live monitor already exists — join it (refcount++). Covers concurrent sessions AND the
// build-then-drop overlap of a mid-stream Reconfigure (the new lease is taken while the old is
// still held). Reconfigure the shared monitor if the requested mode differs.
if let MgrState::Active { mon, refs } = &mut *state {
*refs += 1;
if mon.mode != mode {
unsafe { self.reconfigure(mon, mode) };
}
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)");
if mon.mode != mode {
unsafe { self.reconfigure(&mut mon, mode) };
}
mon
}
MgrState::Idle => unsafe { self.create_monitor(dev, mode)? },
MgrState::Active { .. } => unreachable!("handled above"),
};
let out = self.output_for(&mon);
*state = MgrState::Active { mon, refs: 1 };
Ok(out)
}
/// Build the [`VirtualOutput`] (preferred mode + capture target + a fresh gen-stamped lease) for `mon`.
fn output_for(&'static self, mon: &Monitor) -> VirtualOutput {
VirtualOutput {
node_id: 0,
preferred_mode: Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz)),
win_capture: mon.target(),
keepalive: Box::new(MonitorLease {
mgr: self,
gen: mon.gen,
}),
}
}
/// Create a fresh monitor at `mode`: ADD via the driver (pinning the discrete render GPU under the
/// usual conditions), start the watchdog pinger, resolve the GDI name, force the mode + isolate to a
/// sole composited display.
///
/// # Safety
/// `dev` must be the live control handle.
unsafe fn create_monitor(&'static self, dev: HANDLE, mode: Mode) -> Result<Monitor> {
let added = unsafe { self.driver.add_monitor(dev, mode, resolve_render_pin())? };
// 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 stop_t = stop.clone();
let pinger = thread::spawn(move || {
let mut warned = false;
while !stop_t.load(Ordering::Relaxed) {
if let Some(h) = vdm().device_handle() {
match unsafe { vdm().driver.ping(h) } {
Ok(()) => warned = false,
Err(e) => {
if !warned {
tracing::warn!("virtual-display keepalive PING failed (control handle lost?): {e:#}");
warned = true;
}
}
}
}
thread::sleep(interval);
}
});
// Resolve the capture target. May be None on a GPU-less box (target added but not WDDM-activated);
// the capture backend re-resolves once a GPU is present.
let mut gdi_name = None;
for _ in 0..15 {
thread::sleep(Duration::from_millis(200));
if let Some(n) = unsafe { resolve_gdi_name(added.target_id) } {
gdi_name = Some(n);
break;
}
}
let mut ccd_saved: Option<SavedConfig> = None;
match &gdi_name {
Some(n) => {
tracing::info!(backend = self.driver.name(), "target {} -> {n}", added.target_id);
// ADD only advertises the mode; force it active so DXGI captures the requested size.
set_active_mode(n, mode);
// Make the virtual display the SOLE active output (default): an EXTENDED (non-primary) IDD
// isn't DWM-composited on this box → Desktop Duplication born-losts. Deactivating the other
// display(s) first via the atomic CCD path promotes the IDD to a composited primary with no
// MODE_CHANGE storm. Opt out with PUNKTFUNK_NO_ISOLATE=1.
if std::env::var("PUNKTFUNK_NO_ISOLATE").is_err() {
ccd_saved = unsafe { isolate_displays_ccd(added.target_id) };
} else {
tracing::info!("display isolation skipped (PUNKTFUNK_NO_ISOLATE) — IDD stays extended");
}
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens
}
None => tracing::warn!(
"virtual-display target {} not yet an active display path (needs a WDDM GPU to activate)",
added.target_id
),
}
Ok(Monitor {
key: added.key,
target_id: added.target_id,
luid: added.luid,
gdi_name,
mode,
stop,
pinger: Some(pinger),
ccd_saved,
gen: self.gen.fetch_add(1, Ordering::Relaxed),
})
}
/// Re-apply a (possibly new) mode to a reused monitor on reconnect, re-resolving its GDI name.
///
/// # Safety
/// 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),
new = format!("{}x{}@{}", mode.width, mode.height, mode.refresh_hz),
"virtual-display: reconfiguring reused monitor to the new client mode"
);
if let Some(n) = unsafe { resolve_gdi_name(mon.target_id) } {
mon.gdi_name = Some(n);
}
if let Some(n) = &mon.gdi_name {
set_active_mode(n, mode);
}
mon.mode = mode;
}
/// Stop the watchdog ping, re-attach the displays we detached, then REMOVE the monitor. Consumes it.
///
/// # Safety
/// `dev` must be the live control handle.
unsafe fn teardown(&self, dev: HANDLE, mut mon: Monitor) {
mon.stop.store(true, Ordering::Relaxed);
if let Some(j) = mon.pinger.take() {
let _ = j.join();
}
// Re-attach detached display(s) BEFORE the REMOVE so the box is never left with zero displays.
if let Some(saved) = &mon.ccd_saved {
restore_displays_ccd(saved);
}
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");
}
}
/// Release a session's hold (the [`MonitorLease`] `Drop`): refcount-- ; the last session leaving
/// LINGERs before teardown. A STALE lease (its monitor was preempted + recreated under it) is a
/// no-op, so it can't tear down the CURRENT monitor.
fn release(&self, gen: u64) {
let mut state = self.state.lock().unwrap();
let stale = match &*state {
MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } => mon.gen != gen,
MgrState::Idle => true,
};
if stale {
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, .. } => {
let ms = linger_ms();
tracing::info!(linger_ms = ms, "virtual-display: last session left — lingering before teardown");
MgrState::Lingering {
mon,
until: Instant::now() + Duration::from_millis(ms),
}
}
other => other,
};
}
/// Wait (up to `timeout`) for the active monitor to be RELEASED (the MGR is no longer `Active`).
/// Used by the IDD-push reconnect preempt: after signalling the old session to stop, wait here so it
/// tears its monitor down cleanly before we acquire a fresh one.
pub(crate) fn wait_for_monitor_released(&self, timeout: Duration) {
let deadline = Instant::now() + timeout;
loop {
if !matches!(*self.state.lock().unwrap(), MgrState::Active { .. }) {
return;
}
if Instant::now() >= deadline {
tracing::warn!(
"IDD-push preempt: prior session didn't release the monitor within {timeout:?} — proceeding"
);
return;
}
thread::sleep(Duration::from_millis(25));
}
}
/// Background timer (started once): tear down a monitor that has lingered past its deadline (→ Idle),
/// so a physical-screen user gets their screen back after they stop streaming.
fn ensure_linger_timer(&'static self) {
static TIMER: Once = Once::new();
TIMER.call_once(|| {
thread::Builder::new()
.name("vdisplay-linger".into())
.spawn(move || loop {
thread::sleep(Duration::from_millis(500));
let due = {
let g = self.state.lock().unwrap();
matches!(&*g, MgrState::Lingering { until, .. } if Instant::now() >= *until)
};
if !due {
continue;
}
let Some(dev) = self.device_handle() else {
continue;
};
let taken = {
let mut g = self.state.lock().unwrap();
if matches!(&*g, MgrState::Lingering { until, .. } if Instant::now() >= *until) {
if let MgrState::Lingering { mon, .. } =
std::mem::replace(&mut *g, MgrState::Idle)
{
Some(mon)
} else {
None
}
} else {
None
}
};
if let Some(mon) = taken {
unsafe { self.teardown(dev, mon) };
}
})
.ok();
});
}
}
/// The session's refcount handle. `Drop` releases the manager's refcount; a stale lease (its monitor was
/// preempted + recreated under it) is a no-op.
struct MonitorLease {
mgr: &'static VirtualDisplayManager,
gen: u64,
}
impl Drop for MonitorLease {
fn drop(&mut self) {
self.mgr.release(self.gen);
}
}
/// IDD-push mode: a new client connection preempts + recreates the monitor (single-client reconnect),
/// because a REUSED IddCx monitor's swap-chain is dead. Off → monitors are shared across sessions.
fn idd_push_mode() -> bool {
crate::config::config().idd_push
}
/// The render-GPU pin decision (backend-neutral): pin the discrete render GPU when explicitly requested,
/// or under IDD-push (the host runs NVENC on the render adapter, so it MUST be the discrete encoder GPU
/// on a hybrid box). `None` = let the IDD use its natural adapter (Apollo parity — avoids the cross-GPU
/// ACCESS_LOST storm SudoVDA hit when pinned).
fn resolve_render_pin() -> Option<LUID> {
if crate::config::config().render_adapter.is_some() {
unsafe { crate::win_adapter::resolve_render_adapter_luid() }
} else if crate::config::config().idd_push {
tracing::info!("IDD push: pinning the discrete render GPU (SET_RENDER_ADAPTER)");
unsafe { crate::win_adapter::resolve_render_adapter_luid() }
} else {
tracing::info!(
"SET_RENDER_ADAPTER skipped (Apollo-parity: no render pin; set PUNKTFUNK_RENDER_ADAPTER=<name> to force one)"
);
None
}
}
/// Linger window before a session-less monitor is torn down (default 10 s; `PUNKTFUNK_MONITOR_LINGER_MS`).
fn linger_ms() -> u64 {
std::env::var("PUNKTFUNK_MONITOR_LINGER_MS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(10_000)
}
@@ -16,10 +16,8 @@
use std::ffi::c_void; use std::ffi::c_void;
use std::mem::size_of; use std::mem::size_of;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::os::windows::io::{FromRawHandle, OwnedHandle};
use std::sync::{Arc, Mutex, Once}; use std::sync::atomic::{AtomicU64, Ordering};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use windows::core::{GUID, PCWSTR}; use windows::core::{GUID, PCWSTR};
@@ -36,15 +34,8 @@ use windows::Win32::System::IO::DeviceIoControl;
use pf_vdisplay_proto::control; use pf_vdisplay_proto::control;
use super::manager::{AddedMonitor, MonitorKey, VdisplayDriver};
use super::{Mode, VirtualDisplay, VirtualOutput}; use super::{Mode, VirtualDisplay, VirtualOutput};
// Backend-NEUTRAL CCD/DXGI helpers reused from the SudoVDA backend (a pf-vdisplay monitor's target_id
// is a real OS target id, so these operate identically). The shared MON_GEN lease-generation counter is
// reused too, so a stale preempted lease can't tear down the live monitor regardless of which backend is active.
use super::sudovda::MON_GEN;
use crate::win_adapter::resolve_render_adapter_luid;
use crate::win_display::{
isolate_displays_ccd, resolve_gdi_name, restore_displays_ccd, set_active_mode, SavedConfig,
};
// pf-vdisplay device-interface GUID (pf_vdisplay_proto::PF_VDISPLAY_INTERFACE_GUID_U128). Deliberately // pf-vdisplay device-interface GUID (pf_vdisplay_proto::PF_VDISPLAY_INTERFACE_GUID_U128). Deliberately
// NOT SudoVDA's `{e5bcc234-…}` — we own this driver, so a private interface GUID signals it and avoids // NOT SudoVDA's `{e5bcc234-…}` — we own this driver, so a private interface GUID signals it and avoids
@@ -52,12 +43,6 @@ use crate::win_display::{
const PF_VDISPLAY_INTERFACE: GUID = const PF_VDISPLAY_INTERFACE: GUID =
GUID::from_u128(pf_vdisplay_proto::PF_VDISPLAY_INTERFACE_GUID_U128); GUID::from_u128(pf_vdisplay_proto::PF_VDISPLAY_INTERFACE_GUID_U128);
/// IDD-push mode: a new client connection preempts + recreates the monitor (single-client reconnect),
/// because a REUSED IddCx monitor's swap-chain is dead. Off → monitors are shared across sessions.
fn idd_push_mode() -> bool {
crate::config::config().idd_push
}
/// Monotonic per-session id keying a pf-vdisplay monitor for `IOCTL_ADD`/`IOCTL_REMOVE`. Unlike /// Monotonic per-session id keying a pf-vdisplay monitor for `IOCTL_ADD`/`IOCTL_REMOVE`. Unlike
/// SudoVDA's 16-byte GUID + pid-mangling, the proto keys monitors by a plain `u64` — the host-level /// SudoVDA's 16-byte GUID + pid-mangling, the proto keys monitors by a plain `u64` — the host-level
/// refcount manager (MGR) owns collision safety (a stale session can never REMOVE a live one), so a /// refcount manager (MGR) owns collision safety (a stale session can never REMOVE a live one), so a
@@ -149,92 +134,60 @@ unsafe fn open_device() -> Result<HANDLE> {
Ok(handle) Ok(handle)
} }
// ── Host-level reference-counted pf-vdisplay monitor lifecycle ─────────────────────────────────── /// The pf-vdisplay IOCTL surface behind the shared [`VirtualDisplayManager`](super::manager::VirtualDisplayManager)
// /// (Goal-1 §2.5) — the wire contract is owned by `pf_vdisplay_proto::control` (versioned, hard-checked).
// The virtual monitor is created on the first session and REUSED across sessions. When the last pub(crate) struct PfVdisplayDriver;
// session disconnects the monitor LINGERS for a grace window (PUNKTFUNK_MONITOR_LINGER_MS, default
// 10 s): a reconnect within the window reuses it instantly (no new screen, no PnP connect/disconnect
// chime, no teardown/recreate kernel churn); after the window a background timer REMOVEs it so a
// physical-screen user gets their screen back. Overlapping sessions share one monitor via the
// refcount (teardown only at refs==0 + expired grace), so a stale session can never REMOVE a live
// session's monitor. The control-device HANDLE is opened once and kept for the host lifetime — it's a
// handle, not a screen, so it creates no phantom display.
/// The resources backing one live pf-vdisplay monitor (owned by [`MGR`], not by any session). impl VdisplayDriver for PfVdisplayDriver {
struct Monitor {
/// Per-session key for `IOCTL_ADD`/`IOCTL_REMOVE` (the proto keys monitors by a plain `u64`).
session_id: u64,
target_id: u32,
luid: LUID,
gdi_name: Option<String>,
mode: Mode,
stop: Arc<AtomicBool>,
pinger: Option<JoinHandle<()>>,
ccd_saved: Option<SavedConfig>,
/// Generation stamp (shared [`MON_GEN`]); a [`MonitorLease`] only releases if its gen still matches.
gen: u64,
}
enum MgrState {
Idle,
Active { mon: Monitor, refs: u32 },
Lingering { mon: Monitor, until: Instant },
}
struct Mgr {
/// Control-device handle (raw isize; `HANDLE` isn't `Send`). Opened once, kept for the host life.
device: Option<isize>,
watchdog_s: u32,
state: MgrState,
}
static MGR: Mutex<Mgr> = Mutex::new(Mgr {
device: None,
watchdog_s: 10,
state: MgrState::Idle,
});
/// The Windows pf-vdisplay backend. A marker — the monitor lifecycle lives in the global [`MGR`].
pub struct PfVdisplayDisplay;
impl PfVdisplayDisplay {
pub fn new() -> Result<Self> {
// Open the control device once (validates the driver is present + version-matches) + log the
// watchdog timeout.
let mut g = MGR.lock().unwrap();
mgr_ensure_device(&mut g)?;
Ok(Self)
}
}
impl Drop for PfVdisplayDisplay {
fn drop(&mut self) {
// Nothing: the control device + monitor lifecycle are host-level (owned by MGR) and
// deliberately outlive any single session so a reconnect can reuse the monitor.
}
}
impl VirtualDisplay for PfVdisplayDisplay {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"pf-vdisplay" "pf-vdisplay"
} }
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> { unsafe fn open(&self) -> Result<(OwnedHandle, u32)> {
// Delegate to the host-level manager: create the monitor, reuse a lingering one on reconnect, let device = unsafe { open_device()? };
// or join the live one — and hand back a lease whose Drop releases the refcount. // HARD protocol-version check (unlike SudoVDA's best-effort log): a mismatched host/driver pair
mgr_acquire(mode) // fails loudly here rather than corrupting the IOCTL stream.
let mut info_buf = [0u8; size_of::<control::InfoReply>()];
unsafe { ioctl(device, control::IOCTL_GET_INFO, &[], &mut info_buf) }
.context("pf-vdisplay IOCTL_GET_INFO (version handshake)")?;
let info: control::InfoReply =
bytemuck::pod_read_unaligned(&info_buf[..size_of::<control::InfoReply>()]);
if info.protocol_version != pf_vdisplay_proto::PROTOCOL_VERSION {
unsafe {
let _ = CloseHandle(device);
}
anyhow::bail!(
"pf-vdisplay protocol mismatch: host expects {}, driver reports {} — install matching \
host + driver",
pf_vdisplay_proto::PROTOCOL_VERSION,
info.protocol_version
);
}
let watchdog_s = info.watchdog_timeout_s.max(1);
tracing::info!(
"pf-vdisplay protocol {} (watchdog timeout {}s)",
info.protocol_version,
watchdog_s
);
// Reap monitors orphaned by a crashed previous host — a FIRST-CLASS op (driver returns SUCCESS).
let mut none: [u8; 0] = [];
if unsafe { ioctl(device, control::IOCTL_CLEAR_ALL, &[], &mut none) }.is_ok() {
tracing::info!("cleared orphaned virtual monitors on host startup");
} else {
tracing::warn!("pf-vdisplay IOCTL_CLEAR_ALL failed on startup (continuing)");
}
Ok((
unsafe { OwnedHandle::from_raw_handle(device.0 as _) },
watchdog_s,
))
} }
}
/// Create a fresh pf-vdisplay monitor at `mode` on the (host-level) control `device`. ADD the target, unsafe fn add_monitor(
/// start the watchdog ping, resolve the GDI name, force the client mode + (default) isolate to a sole &self,
/// composited display. Returns the [`Monitor`] resources; the manager tracks its lifecycle dev: HANDLE,
/// (refcount + linger). mode: Mode,
unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<Monitor> { render_luid: Option<LUID>,
let dev = HANDLE(device as *mut c_void); ) -> Result<AddedMonitor> {
{
// Fresh session id per created monitor (the manager refcount, not the id, prevents the
// cross-session REMOVE collision).
let session_id = next_session_id(); let session_id = next_session_id();
let add = control::AddRequest { let add = control::AddRequest {
session_id, session_id,
@@ -243,54 +196,30 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
refresh_hz: mode.refresh_hz, refresh_hz: mode.refresh_hz,
_reserved: 0, _reserved: 0,
}; };
// SET_RENDER_ADAPTER is OPT-IN. By default we do NOT pin the render adapter — let the IDD use // SET_RENDER_ADAPTER (opt-in; pf-vdisplay IMPLEMENTS it). Non-fatal on failure: the driver reports
// its natural adapter (Apollo-parity; avoids the cross-GPU mismatch ACCESS_LOST storm). Opt in // its real render LUID in the shared header, so the host binds correctly even if this is ignored.
// with PUNKTFUNK_RENDER_ADAPTER=<name substring> or the IDD-push path (which MUST run NVENC on if let Some(luid) = render_luid {
// the discrete render GPU it pins here). The pf-vdisplay driver now IMPLEMENTS this IOCTL
// (IddCxAdapterSetRenderAdapter); a failure is still tolerated (the driver also reports its real
// render LUID in the shared header, so the host binds to the right GPU regardless).
let pinned = if crate::config::config().render_adapter.is_some() {
unsafe { resolve_render_adapter_luid() }
} else if crate::config::config().idd_push {
// P2 direct frame push: the host opens the driver's shared textures AND runs NVENC on the
// RENDER adapter, so on a hybrid box (dGPU + iGPU) it MUST be the discrete encoder GPU — an
// iGPU-rendered surface is untouchable by NVENC. pf-vdisplay now IMPLEMENTS
// SET_RENDER_ADAPTER, so pin the discrete GPU; the driver also reports the resulting render LUID
// in the shared header, so the host binds correctly even if this is overridden.
tracing::info!("IDD push: pinning the discrete render GPU (SET_RENDER_ADAPTER)");
unsafe { resolve_render_adapter_luid() }
} else {
tracing::info!(
"pf-vdisplay SET_RENDER_ADAPTER skipped (no render pin — avoids cross-GPU mismatch; \
set PUNKTFUNK_RENDER_ADAPTER=<name> to force a specific render GPU)"
);
None
};
if let Some(luid) = pinned {
match unsafe { set_render_adapter(dev, luid) } { match unsafe { set_render_adapter(dev, luid) } {
Ok(()) => tracing::info!( Ok(()) => tracing::info!(
luid = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart), luid = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart),
"pf-vdisplay SET_RENDER_ADAPTER: pinned IDD render GPU" "pf-vdisplay SET_RENDER_ADAPTER: pinned IDD render GPU"
), ),
// Non-fatal: warn + continue (do NOT propagate). The driver reports its real render LUID
// in the shared header and the host binds to that, so the natural-adapter path still works.
Err(e) => tracing::warn!( Err(e) => tracing::warn!(
"pf-vdisplay SET_RENDER_ADAPTER failed (continuing on the natural adapter): {e:#}" "pf-vdisplay SET_RENDER_ADAPTER failed (continuing on the natural adapter): {e:#}"
), ),
} }
} }
let mut out = [0u8; size_of::<control::AddReply>()]; let mut out = [0u8; size_of::<control::AddReply>()];
unsafe { ioctl(dev, control::IOCTL_ADD, bytemuck::bytes_of(&add), &mut out) } unsafe { ioctl(dev, control::IOCTL_ADD, bytemuck::bytes_of(&add), &mut out) }.with_context(
.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 alignment, and `from_bytes` PANICS on an alignment mismatch. This copies the bytes // `pod_read_unaligned` (NOT `from_bytes`): `out` is a stack `[u8; N]` with no guaranteed 4-byte
// into a properly-aligned `AddReply` value. // alignment, and `from_bytes` PANICS on a mismatch. This copies into an aligned `AddReply`.
let reply: control::AddReply = let reply: control::AddReply =
bytemuck::pod_read_unaligned(&out[..size_of::<control::AddReply>()]); bytemuck::pod_read_unaligned(&out[..size_of::<control::AddReply>()]);
let luid = LUID { let luid = LUID {
@@ -305,7 +234,7 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
reply.target_id, reply.target_id,
luid.LowPart luid.LowPart
); );
if let Some(pin) = pinned { if let Some(pin) = render_luid {
if luid.LowPart == pin.LowPart && luid.HighPart == pin.HighPart { if luid.LowPart == pin.LowPart && luid.HighPart == pin.HighPart {
tracing::info!("pf-vdisplay ADD render adapter matches the pinned GPU (pin took)"); tracing::info!("pf-vdisplay ADD render adapter matches the pinned GPU (pin took)");
} else { } else {
@@ -316,370 +245,48 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
); );
} }
} }
Ok(AddedMonitor {
// Mandatory keepalive: ping inside the watchdog window or the driver tears all displays down. key: MonitorKey::Session(session_id),
let stop = Arc::new(AtomicBool::new(false));
let device_raw = device;
let interval = Duration::from_millis(watchdog_s as u64 * 1000 / 3);
let stop_t = stop.clone();
let pinger = thread::spawn(move || {
let h = HANDLE(device_raw as *mut c_void);
let mut warned = false;
while !stop_t.load(Ordering::Relaxed) {
let mut none: [u8; 0] = [];
match unsafe { ioctl(h, control::IOCTL_PING, &[], &mut none) } {
Ok(_) => warned = false,
// A persistently failing PING means the cached control handle went invalid — the
// driver watchdog will then tear the monitor down mid-session. Surface it once.
Err(e) => {
if !warned {
tracing::warn!(
"pf-vdisplay keepalive PING failed (control handle lost?): {e:#}"
);
warned = true;
}
}
}
thread::sleep(interval);
}
});
// Resolve the capture target. May be None on a GPU-less box (target added but not activated
// into a WDDM path); the Windows capture backend will re-resolve once a GPU is present.
let mut gdi_name = None;
for _ in 0..15 {
thread::sleep(Duration::from_millis(200));
if let Some(n) = unsafe { resolve_gdi_name(reply.target_id) } {
gdi_name = Some(n);
break;
}
}
let mut ccd_saved: Option<SavedConfig> = None;
match &gdi_name {
Some(n) => {
tracing::info!("pf-vdisplay target {} -> {n}", reply.target_id);
// ADD only advertises the mode; force it active so DXGI captures the requested size.
set_active_mode(n, mode);
// Make the pf-vdisplay the SOLE active display (default). An EXTENDED (non-primary) IDD
// is NOT DWM-composited → Desktop Duplication gets a born-lost ACCESS_LOST; deactivating
// the other display(s) FIRST (CCD, atomic) leaves the virtual output as the sole →
// primary → composited desktop, so all content (incl. Winlogon) renders to it without a
// MODE_CHANGE_IN_PROGRESS storm. Opt out with PUNKTFUNK_NO_ISOLATE=1 (a box with a real
// second monitor to keep live).
if std::env::var("PUNKTFUNK_NO_ISOLATE").is_err() {
ccd_saved = unsafe { isolate_displays_ccd(reply.target_id) };
} else {
tracing::info!(
"display isolation skipped (PUNKTFUNK_NO_ISOLATE) — IDD stays extended"
);
}
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens
}
None => tracing::warn!(
"pf-vdisplay target {} not yet an active display path (needs a WDDM GPU to activate)",
reply.target_id
),
}
Ok(Monitor {
session_id,
target_id: reply.target_id, target_id: reply.target_id,
luid, luid,
gdi_name,
mode,
stop,
pinger: Some(pinger),
ccd_saved,
gen: MON_GEN.fetch_add(1, Ordering::Relaxed),
}) })
} }
}
impl Monitor { unsafe fn remove_monitor(&self, dev: HANDLE, key: &MonitorKey) -> Result<()> {
/// The capture target handed to a session (`None` until the GDI name resolves). let MonitorKey::Session(session_id) = key else {
fn target(&self) -> Option<crate::capture::dxgi::WinCaptureTarget> { anyhow::bail!("pf-vdisplay: unexpected monitor key kind");
self.gdi_name };
.clone()
.map(|n| crate::capture::dxgi::WinCaptureTarget {
adapter_luid: crate::capture::dxgi::pack_luid(self.luid),
gdi_name: n,
// target_id is stable across secure-desktop topology rebuilds; the GDI name is NOT,
// so capture re-resolves the name from this on every recovery.
target_id: self.target_id,
})
}
/// Stop the watchdog ping, re-attach the displays we detached, then REMOVE the monitor (by session
/// id). `device` is the host-level control handle. Consumes the monitor.
unsafe fn teardown(mut self, device: isize) {
self.stop.store(true, Ordering::Relaxed);
if let Some(j) = self.pinger.take() {
let _ = j.join();
}
// Re-attach detached display(s) BEFORE the REMOVE so the box is never left with zero displays.
if let Some(saved) = &self.ccd_saved {
restore_displays_ccd(saved);
}
let req = control::RemoveRequest { let req = control::RemoveRequest {
session_id: self.session_id, session_id: *session_id,
}; };
let mut none: [u8; 0] = []; let mut none: [u8; 0] = [];
let h = HANDLE(device as *mut c_void); unsafe { ioctl(dev, control::IOCTL_REMOVE, bytemuck::bytes_of(&req), &mut none) }.map(|_| ())
if let Err(e) = ioctl(
h,
control::IOCTL_REMOVE,
bytemuck::bytes_of(&req),
&mut none,
) {
tracing::warn!("pf-vdisplay REMOVE failed: {e:#}");
} else {
tracing::info!("pf-vdisplay monitor removed");
}
} }
}
/// Open the control device once + version/watchdog handshake; cache the handle (raw isize) in `g`. unsafe fn ping(&self, dev: HANDLE) -> Result<()> {
fn mgr_ensure_device(g: &mut Mgr) -> Result<isize> {
if let Some(d) = g.device {
return Ok(d);
}
let device = unsafe { open_device()? };
// Single version+watchdog handshake. The proto intends a HARD protocol-version check (unlike
// SudoVDA's best-effort log) — a mismatched host/driver pair fails loudly here rather than
// corrupting the IOCTL stream.
let mut info_buf = [0u8; size_of::<control::InfoReply>()];
unsafe { ioctl(device, control::IOCTL_GET_INFO, &[], &mut info_buf) }
.context("pf-vdisplay IOCTL_GET_INFO (version handshake)")?;
// `pod_read_unaligned` (see the AddReply note): copies out of the unaligned stack buffer.
let info: control::InfoReply =
bytemuck::pod_read_unaligned(&info_buf[..size_of::<control::InfoReply>()]);
if info.protocol_version != pf_vdisplay_proto::PROTOCOL_VERSION {
// Close the handle before bailing so a retry re-opens cleanly.
unsafe {
let _ = CloseHandle(device);
}
anyhow::bail!(
"pf-vdisplay protocol mismatch: host expects {}, driver reports {} — install matching \
host + driver",
pf_vdisplay_proto::PROTOCOL_VERSION,
info.protocol_version
);
}
g.watchdog_s = info.watchdog_timeout_s.max(1);
tracing::info!(
"pf-vdisplay protocol {} (watchdog timeout {}s)",
info.protocol_version,
g.watchdog_s
);
// Reap monitors orphaned by a crashed/killed previous host instance before we create ours. This is
// a FIRST-CLASS op on pf-vdisplay (the driver returns SUCCESS), NOT a "send-and-hope" hack: without
// it an orphan lingers until the driver watchdog fires — but a still-pinging new session keeps
// resetting that watchdog, so orphans could accumulate.
{
let mut none: [u8; 0] = []; let mut none: [u8; 0] = [];
if unsafe { ioctl(device, control::IOCTL_CLEAR_ALL, &[], &mut none) }.is_ok() { unsafe { ioctl(dev, control::IOCTL_PING, &[], &mut none) }.map(|_| ())
tracing::info!("cleared orphaned virtual monitors on host startup");
} else {
tracing::warn!("pf-vdisplay IOCTL_CLEAR_ALL failed on startup (continuing)");
}
} }
let raw = device.0 as isize;
g.device = Some(raw);
Ok(raw)
} }
/// Linger window before a session-less monitor is torn down. A reconnect within it reuses the /// The Windows pf-vdisplay virtual-display backend. A marker — the lifecycle lives in the shared
/// monitor (no new screen / PnP chime); after it the monitor is REMOVEd so a physical screen returns. /// [`VirtualDisplayManager`](super::manager::VirtualDisplayManager).
fn linger_ms() -> u64 { pub struct PfVdisplayDisplay;
std::env::var("PUNKTFUNK_MONITOR_LINGER_MS")
.ok() impl PfVdisplayDisplay {
.and_then(|s| s.parse().ok()) pub fn new() -> Result<Self> {
.unwrap_or(10_000) super::manager::init(Box::new(PfVdisplayDriver)).open_backend()?;
Ok(Self)
}
} }
/// Acquire the shared monitor for a new session: join the live one (refcount++), reuse a lingering impl VirtualDisplay for PfVdisplayDisplay {
/// one (reconfiguring if the client mode changed), or create one. The returned [`MonitorLease`] fn name(&self) -> &'static str {
/// releases the refcount on drop. "pf-vdisplay"
fn mgr_acquire(mode: Mode) -> Result<VirtualOutput> {
ensure_linger_timer();
let mut g = MGR.lock().unwrap();
let device = mgr_ensure_device(&mut g)?;
let watchdog_s = g.watchdog_s;
// IDD-push: a new connection while a monitor is live = a single-client RECONNECT (the prior client
// is gone — IDD-push is one display, no concurrency). A REUSED IddCx monitor's swap-chain is DEAD,
// so joining it would hand the new client a black screen until the old session times out. PREEMPT:
// tear the old monitor down (its teardown restores topology + IOCTL_REMOVEs) and fall through to
// create a FRESH one. The old session's lease is gen-stamped, so its later drop is ignored
// (mgr_release no-op) and can't tear down the new monitor.
if idd_push_mode()
&& matches!(
g.state,
MgrState::Active { .. } | MgrState::Lingering { .. }
)
{
if let MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } =
std::mem::replace(&mut g.state, MgrState::Idle)
{
tracing::info!(
old_target = mon.target_id,
"IDD-push reconnect — preempting the prior session, recreating a fresh monitor"
);
// teardown() — NOT drop() — sends IOCTL_REMOVE (and restores topology). `Monitor` has NO
// `Drop` impl, so a bare `drop(mon)` would orphan the IddCx monitor in the driver (never
// departed → leaks a live D3D device + a stuck swap-chain processor thread per reconnect).
unsafe { mon.teardown(device) };
// Let the OS finish the ASYNC IddCx monitor departure before the next ADD. A back-to-back
// REMOVE→ADD races the teardown and the ADD IOCTL is rejected under reconnect churn.
thread::sleep(Duration::from_millis(400));
}
} }
// A live monitor already exists — join it (refcount++). This covers a concurrent session AND the fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
// build-then-drop overlap of a mid-stream Reconfigure / secure-return (the new lease is taken while super::manager::vdm().acquire(mode)
// the old is still held). If the requested mode differs, reconfigure the shared monitor to it so a
// Reconfigure actually applies (one shared monitor → sessions necessarily share a mode).
if let MgrState::Active { mon, refs } = &mut g.state {
*refs += 1;
let changed = mon.mode.width != mode.width
|| mon.mode.height != mode.height
|| mon.mode.refresh_hz != mode.refresh_hz;
if changed {
unsafe { mgr_reconfigure(mon, mode) };
}
tracing::info!(
refs = *refs,
"pf-vdisplay monitor reused (concurrent / reconfigure session)"
);
let pm = Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz));
let target = mon.target();
let gen = mon.gen;
return Ok(VirtualOutput {
node_id: 0,
preferred_mode: pm,
win_capture: target,
keepalive: Box::new(MonitorLease { gen }),
});
}
// Idle or Lingering: repurpose/create a monitor → Active{refs:1}.
let mon = match std::mem::replace(&mut g.state, MgrState::Idle) {
MgrState::Lingering { mut mon, .. } => {
tracing::info!("pf-vdisplay monitor reused (reconnect within the linger window)");
let changed = mon.mode.width != mode.width
|| mon.mode.height != mode.height
|| mon.mode.refresh_hz != mode.refresh_hz;
if changed {
unsafe { mgr_reconfigure(&mut mon, mode) };
}
mon
}
MgrState::Idle => unsafe { create_monitor(device, mode, watchdog_s)? },
MgrState::Active { .. } => unreachable!("handled above"),
};
let pm = Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz));
let target = mon.target();
let gen = mon.gen;
g.state = MgrState::Active { mon, refs: 1 };
Ok(VirtualOutput {
node_id: 0,
preferred_mode: pm,
win_capture: target,
keepalive: Box::new(MonitorLease { gen }),
})
}
/// Re-apply a (possibly new) mode to a reused monitor on reconnect, re-resolving its GDI name.
unsafe fn mgr_reconfigure(mon: &mut Monitor, mode: Mode) {
tracing::info!(
old = format!(
"{}x{}@{}",
mon.mode.width, mon.mode.height, mon.mode.refresh_hz
),
new = format!("{}x{}@{}", mode.width, mode.height, mode.refresh_hz),
"pf-vdisplay: reconfiguring reused monitor to the new client mode"
);
if let Some(n) = resolve_gdi_name(mon.target_id) {
mon.gdi_name = Some(n);
}
if let Some(n) = &mon.gdi_name {
set_active_mode(n, mode);
}
mon.mode = mode;
}
/// Release a session's hold: refcount-- ; when the last session leaves, LINGER before teardown.
/// `gen` is the lease's monitor generation: a STALE lease (its monitor was already torn down +
/// recreated under it — the IDD-push reconnect-preempt path) does nothing, so it can't decrement the
/// CURRENT (fresh) monitor's refcount and tear it down.
fn mgr_release(gen: u64) {
let mut g = MGR.lock().unwrap();
let stale = match &g.state {
MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } => mon.gen != gen,
MgrState::Idle => true,
};
if stale {
return;
}
g.state = match std::mem::replace(&mut g.state, MgrState::Idle) {
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,
"pf-vdisplay: last session left — lingering before teardown"
);
MgrState::Lingering {
mon,
until: Instant::now() + Duration::from_millis(ms),
}
}
other => other,
};
}
// NOTE: `wait_for_monitor_released` is NOT redefined here. Its only caller (`punktfunk1.rs`, the
// IDD-push reconnect preempt) reaches it as `crate::vdisplay::sudovda::wait_for_monitor_released`, and
// pf_vdisplay.rs never calls it internally (the preempt is done inline in `mgr_acquire` above), so a
// second copy here would be dead code waiting on the (separate) pf-vdisplay MGR. The two backends keep
// independent MGRs but only one is ever active — see the cross-MGR caveat in the implementation report.
/// Background timer (started once): tear down a monitor that has lingered past its deadline (→ Idle),
/// so a physical-screen user gets their screen back after they stop streaming.
fn ensure_linger_timer() {
static TIMER: Once = Once::new();
TIMER.call_once(|| {
let _ = thread::Builder::new()
.name("pf-vdisplay-linger".into())
.spawn(|| loop {
thread::sleep(Duration::from_millis(500));
let mut g = MGR.lock().unwrap();
let due = matches!(&g.state, MgrState::Lingering { until, .. } if Instant::now() >= *until);
if due {
let device = g.device.unwrap_or(0);
if let MgrState::Lingering { mon, .. } =
std::mem::replace(&mut g.state, MgrState::Idle)
{
drop(g); // release the lock before the REMOVE IOCTL + display restore
unsafe { mon.teardown(device) };
}
}
});
});
}
/// A session's lease on the shared monitor. Drop releases the refcount (→ linger when it hits 0),
/// UNLESS the monitor was already torn down + recreated under it (gen mismatch — the IDD-push
/// reconnect-preempt path), in which case the drop is a no-op so it can't tear down the new monitor.
struct MonitorLease {
gen: u64,
}
impl Drop for MonitorLease {
fn drop(&mut self) {
mgr_release(self.gen);
} }
} }
@@ -700,6 +307,8 @@ pub fn is_available() -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::thread;
use std::time::Duration;
/// Live hardware round trip — skipped unless `PUNKTFUNK_PF_VDISPLAY_LIVE=1` (needs the pf-vdisplay /// Live hardware round trip — skipped unless `PUNKTFUNK_PF_VDISPLAY_LIVE=1` (needs the pf-vdisplay
/// driver installed). Exercises the real trait path: open -> create -> hold -> drop (REMOVE). /// driver installed). Exercises the real trait path: open -> create -> hold -> drop (REMOVE).
@@ -9,23 +9,8 @@
use std::ffi::c_void; use std::ffi::c_void;
use std::mem::size_of; use std::mem::size_of;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::os::windows::io::{FromRawHandle, OwnedHandle};
use std::sync::{Arc, Mutex, Once}; use std::sync::atomic::Ordering;
/// Monotonic monitor generation. Each [`create_monitor`] stamps the next value onto the [`Monitor`]
/// and its [`MonitorLease`]s, so a lease whose monitor was already torn down + recreated (the IDD-push
/// reconnect-preempt path) is ignored on drop instead of decrementing the NEW monitor's refcount.
// pub(crate) so vdisplay::pf_vdisplay can reuse this shared generation counter (one counter across both
// backends keeps the idd_push stale-ring bail working regardless of which backend is active).
pub(crate) static MON_GEN: AtomicU64 = AtomicU64::new(1);
/// IDD-push mode: a new client connection preempts + recreates the monitor (single-client reconnect),
/// because a REUSED IddCx monitor's swap-chain is dead. Off → monitors are shared across sessions.
fn idd_push_mode() -> bool {
crate::config::config().idd_push
}
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use windows::core::{GUID, PCWSTR}; use windows::core::{GUID, PCWSTR};
@@ -41,6 +26,7 @@ use windows::Win32::Storage::FileSystem::{
}; };
use windows::Win32::System::IO::DeviceIoControl; use windows::Win32::System::IO::DeviceIoControl;
use super::manager::{AddedMonitor, MonitorKey, VdisplayDriver};
use super::{Mode, VirtualDisplay, VirtualOutput}; use super::{Mode, VirtualDisplay, VirtualOutput};
// SudoVDA device-interface GUID (Common/Include/sudovda-ioctl.h). // SudoVDA device-interface GUID (Common/Include/sudovda-ioctl.h).
@@ -116,11 +102,6 @@ unsafe fn set_render_adapter(h: HANDLE, luid: LUID) -> Result<()> {
.context("SudoVDA SET_RENDER_ADAPTER") .context("SudoVDA SET_RENDER_ADAPTER")
} }
// `resolve_render_adapter_luid` moved to the backend-neutral `crate::win_adapter` (audit §9 / Goal 2:
// it is display-utility, not SudoVDA-specific). Re-exported so this backend's own callers keep the short
// name; external callers (idd_push, pf_vdisplay) use `crate::win_adapter` directly.
pub(crate) use crate::win_adapter::resolve_render_adapter_luid;
#[repr(C)] #[repr(C)]
struct RemoveParams { struct RemoveParams {
guid: GUID, guid: GUID,
@@ -145,14 +126,6 @@ unsafe fn ioctl(h: HANDLE, code: u32, input: &[u8], output: &mut [u8]) -> Result
Ok(returned) Ok(returned)
} }
// The CCD/GDI display helpers (resolve_gdi_name, set_advanced_color, advanced_color_enabled,
// set_active_mode, isolate/restore_displays_ccd) + SavedConfig moved to the backend-neutral
// `crate::win_display` (audit §9 / Goal 2). Re-exported so this backend's own callers keep the short
// names; external callers use `crate::win_display` directly.
pub(crate) use crate::win_display::{
isolate_displays_ccd, resolve_gdi_name, restore_displays_ccd, set_active_mode, SavedConfig,
};
unsafe fn open_device() -> Result<HANDLE> { unsafe fn open_device() -> Result<HANDLE> {
let hdev = SetupDiGetClassDevsW( let hdev = SetupDiGetClassDevsW(
Some(&SUVDA_INTERFACE), Some(&SUVDA_INTERFACE),
@@ -191,93 +164,65 @@ unsafe fn open_device() -> Result<HANDLE> {
Ok(handle) Ok(handle)
} }
// ── Host-level reference-counted SudoVDA monitor lifecycle ────────────────────────────────────── /// The SudoVDA IOCTL surface behind the shared [`VirtualDisplayManager`](super::manager::VirtualDisplayManager)
// /// (Goal-1 §2.5) — the only SudoVDA-specific code left; the monitor lifecycle is the shared state machine.
// The virtual monitor is created on the first session and REUSED across sessions. When the last pub(crate) struct SudoVdaDriver;
// session disconnects the monitor LINGERS for a grace window (PUNKTFUNK_MONITOR_LINGER_MS, default
// 10 s): a reconnect within the window reuses it instantly (no new screen, no PnP connect/disconnect
// chime, no teardown/recreate kernel churn); after the window a background timer REMOVEs it so a
// physical-screen user gets their screen back. Overlapping sessions share one monitor via the
// refcount (teardown only at refs==0 + expired grace), so a stale session can never REMOVE a live
// session's monitor (the earlier collision). The control-device HANDLE is opened once and kept for
// the host lifetime — it's a handle, not a screen, so it creates no phantom display.
/// The resources backing one live SudoVDA monitor (owned by [`MGR`], not by any session). impl VdisplayDriver for SudoVdaDriver {
struct Monitor {
guid: GUID,
target_id: u32,
luid: LUID,
gdi_name: Option<String>,
mode: Mode,
stop: Arc<AtomicBool>,
pinger: Option<JoinHandle<()>>,
ccd_saved: Option<SavedConfig>,
/// Generation stamp ([`MON_GEN`]); a [`MonitorLease`] only releases if its gen still matches.
gen: u64,
}
enum MgrState {
Idle,
Active { mon: Monitor, refs: u32 },
Lingering { mon: Monitor, until: Instant },
}
struct Mgr {
/// Control-device handle (raw isize; `HANDLE` isn't `Send`). Opened once, kept for the host life.
device: Option<isize>,
watchdog_s: u32,
state: MgrState,
}
static MGR: Mutex<Mgr> = Mutex::new(Mgr {
device: None,
watchdog_s: 3,
state: MgrState::Idle,
});
/// The Windows virtual-display backend. A marker — the monitor lifecycle lives in the global [`MGR`].
pub struct SudoVdaDisplay;
impl SudoVdaDisplay {
pub fn new() -> Result<Self> {
// Open the control device once (validates the driver is present) + log version/watchdog.
let mut g = MGR.lock().unwrap();
mgr_ensure_device(&mut g)?;
Ok(Self)
}
}
impl Drop for SudoVdaDisplay {
fn drop(&mut self) {
// Nothing: the control device + monitor lifecycle are host-level (owned by MGR) and
// deliberately outlive any single session so a reconnect can reuse the monitor.
}
}
impl VirtualDisplay for SudoVdaDisplay {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"sudovda" "sudovda"
} }
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> { unsafe fn open(&self) -> Result<(OwnedHandle, u32)> {
// Delegate to the host-level manager: create the monitor, reuse a lingering one on reconnect, let device = unsafe { open_device()? };
// or join the live one — and hand back a lease whose Drop releases the refcount. let mut ver = [0u8; 4];
mgr_acquire(mode) if unsafe { ioctl(device, IOCTL_GET_VERSION, &[], &mut ver) }.is_ok() {
tracing::info!(
"SudoVDA protocol {}.{}.{} (test={})",
ver[0],
ver[1],
ver[2],
ver[3]
);
}
let mut wd = [0u8; 8];
let watchdog_s = if unsafe { ioctl(device, IOCTL_GET_WATCHDOG, &[], &mut wd) }.is_ok() {
u32::from_le_bytes([wd[0], wd[1], wd[2], wd[3]]).max(1)
} else {
3
};
tracing::info!("SudoVDA watchdog timeout {}s", watchdog_s);
// Reap monitors orphaned by a crashed previous host (SudoVDA returns invalid for CLEAR_ALL —
// ignored; pf-vdisplay honors it).
let mut none: [u8; 0] = [];
if unsafe { ioctl(device, IOCTL_CLEAR_ALL, &[], &mut none) }.is_ok() {
tracing::info!("cleared orphaned virtual monitors on host startup");
}
// Take ownership — the OwnedHandle CloseHandle's the control device on drop (it was leaked before).
Ok((unsafe { OwnedHandle::from_raw_handle(device.0 as _) }, watchdog_s))
} }
}
/// Create a fresh SudoVDA monitor at `mode` on the (host-level) control `device`. The old per-session unsafe fn add_monitor(
/// `create()` body, now owned by the manager: ADD the target, start the watchdog ping, resolve the &self,
/// GDI name, force the client mode + (default) isolate to a sole composited display. Returns the dev: HANDLE,
/// [`Monitor`] resources; the manager tracks its lifecycle (refcount + linger). mode: Mode,
unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<Monitor> { render_luid: Option<LUID>,
let dev = HANDLE(device as *mut c_void); ) -> Result<AddedMonitor> {
{ // SET_RENDER_ADAPTER (opt-in). On this box SudoVDA IGNORES the pin and the IDD lands on a different
// adapter than its DXGI output is enumerated under — the cross-GPU ACCESS_LOST source — so the
// manager only pins under PUNKTFUNK_RENDER_ADAPTER / IDD-push.
if let Some(luid) = render_luid {
match unsafe { set_render_adapter(dev, luid) } {
Ok(()) => tracing::info!(
luid = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart),
"SudoVDA SET_RENDER_ADAPTER: pinned IDD render GPU"
),
Err(e) => tracing::warn!("SudoVDA SET_RENDER_ADAPTER failed (continuing): {e:#}"),
}
}
let mut device_name = [0u8; 14]; let mut device_name = [0u8; 14];
let nm = b"punktfunk"; let nm = b"punktfunk";
device_name[..nm.len()].copy_from_slice(nm); device_name[..nm.len()].copy_from_slice(nm);
// Fresh GUID per created monitor (the manager refcount, not the GUID, prevents the
// cross-session REMOVE collision now).
let session_guid = next_monitor_guid(); let session_guid = next_monitor_guid();
let add = AddParams { let add = AddParams {
width: mode.width, width: mode.width,
@@ -287,41 +232,6 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
device_name, device_name,
serial: [0u8; 14], serial: [0u8; 14],
}; };
// SET_RENDER_ADAPTER is OPT-IN. Apollo runs with an EMPTY config and NEVER pins the render
// adapter, yet captures the SudoVDA cleanly at the client mode on the 4090 (verified live on
// this exact box: no ACCESS_LOST, no MODE_CHANGE storm). On this box our pin is IGNORED by the
// driver AND the IDD lands on a DIFFERENT adapter (0x23664) than the one its DXGI output is
// enumerated under (the 4090, where we make the capture device) — a cross-GPU mismatch that is
// the real source of the perpetual ACCESS_LOST + MODE_CHANGE_IN_PROGRESS storm. So default to
// NOT pinning — let the IDD use its natural adapter like Apollo. Opt in with
// PUNKTFUNK_RENDER_ADAPTER=<name substring> only on a box that genuinely needs steering.
let pinned = if crate::config::config().render_adapter.is_some() {
unsafe { resolve_render_adapter_luid() }
} else if crate::config::config().idd_push {
// P2 direct frame push: the host opens the driver's shared textures AND runs NVENC on the
// RENDER adapter, so on a hybrid box (4090 + iGPU) it MUST be the discrete encoder GPU —
// an iGPU-rendered surface is untouchable by NVENC. pf-vdisplay HONORS SET_RENDER_ADAPTER
// (SudoVDA ignored it), so pin the discrete GPU. The driver also reports the resulting
// render LUID in the shared header, so the host binds correctly even if this is overridden.
tracing::info!("IDD push: pinning the discrete render GPU (SET_RENDER_ADAPTER)");
unsafe { resolve_render_adapter_luid() }
} else {
tracing::info!(
"SudoVDA SET_RENDER_ADAPTER skipped (Apollo-parity: no render pin — avoids cross-GPU \
mismatch; set PUNKTFUNK_RENDER_ADAPTER=<name> to force a specific render GPU)"
);
None
};
if let Some(luid) = pinned {
match unsafe { set_render_adapter(dev, luid) } {
Ok(()) => tracing::info!(
luid = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart),
"SudoVDA SET_RENDER_ADAPTER: pinned IDD render GPU"
),
Err(e) => tracing::warn!("SudoVDA SET_RENDER_ADAPTER failed (continuing): {e:#}"),
}
}
let add_bytes = unsafe { let add_bytes = unsafe {
std::slice::from_raw_parts(&add as *const _ as *const u8, size_of::<AddParams>()) std::slice::from_raw_parts(&add as *const _ as *const u8, size_of::<AddParams>())
}; };
@@ -341,7 +251,7 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
ao.target_id, ao.target_id,
ao.luid.LowPart ao.luid.LowPart
); );
if let Some(luid) = pinned { if let Some(luid) = render_luid {
if ao.luid.LowPart == luid.LowPart && ao.luid.HighPart == luid.HighPart { if ao.luid.LowPart == luid.LowPart && ao.luid.HighPart == luid.HighPart {
tracing::info!("SudoVDA ADD render adapter matches the pinned GPU (pin took)"); tracing::info!("SudoVDA ADD render adapter matches the pinned GPU (pin took)");
} else { } else {
@@ -352,380 +262,49 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
); );
} }
} }
Ok(AddedMonitor {
// Mandatory keepalive: ping inside the watchdog window or the driver tears all displays down. key: MonitorKey::Guid(session_guid),
let stop = Arc::new(AtomicBool::new(false));
let device_raw = device;
let interval = Duration::from_millis(watchdog_s as u64 * 1000 / 3);
let stop_t = stop.clone();
let pinger = thread::spawn(move || {
let h = HANDLE(device_raw as *mut c_void);
let mut warned = false;
while !stop_t.load(Ordering::Relaxed) {
let mut none: [u8; 0] = [];
match unsafe { ioctl(h, IOCTL_DRIVER_PING, &[], &mut none) } {
Ok(_) => warned = false,
// A persistently failing PING means the cached control handle went invalid — the
// driver watchdog will then tear the monitor down mid-session. Surface it once
// (the old `let _ =` swallowed it, which masked exactly this during the bad-state churn).
Err(e) => {
if !warned {
tracing::warn!(
"SudoVDA keepalive PING failed (control handle lost?): {e:#}"
);
warned = true;
}
}
}
thread::sleep(interval);
}
});
// Resolve the capture target. May be None on a GPU-less box (target added but not activated
// into a WDDM path); the Windows capture backend will re-resolve once a GPU is present.
let mut gdi_name = None;
for _ in 0..15 {
thread::sleep(Duration::from_millis(200));
if let Some(n) = unsafe { resolve_gdi_name(ao.target_id) } {
gdi_name = Some(n);
break;
}
}
let mut ccd_saved: Option<SavedConfig> = None;
match &gdi_name {
Some(n) => {
tracing::info!("SudoVDA target {} -> {n}", ao.target_id);
// ADD only advertises the mode; force it active so DXGI captures the requested size.
set_active_mode(n, mode);
// Make the SudoVDA the SOLE active display (default). On this box an EXTENDED
// (non-primary) IDD is NOT DWM-composited → Desktop Duplication gets a born-lost
// ACCESS_LOST (measured live: MODE_CHANGE storm fixed, but the extended IDD then
// born-lost). Apollo reaches the same end state ("Virtual Desktop: WxH" — the IDD is the
// whole desktop, hence primary + composited) via Windows AUTO-promoting the real WDDM
// display over the box's leftover 1024x768 basic display; Windows does NOT auto-promote
// for us, so we deactivate the other display(s) explicitly via the clean atomic CCD path.
// Deactivating FIRST means set_active_mode's primary-promotion has nothing to contest →
// no MODE_CHANGE_IN_PROGRESS storm (that storm came from promoting primary WHILE the
// basic display stayed active). Opt out with PUNKTFUNK_NO_ISOLATE=1 (a box with a real
// second monitor to keep live). The legacy GDI detach is skipped — it misses
// iGPU-attached monitors on a hybrid box and churns per-device; CCD is atomic.
if std::env::var("PUNKTFUNK_NO_ISOLATE").is_err() {
ccd_saved = unsafe { isolate_displays_ccd(ao.target_id) };
} else {
tracing::info!(
"display isolation skipped (PUNKTFUNK_NO_ISOLATE) — IDD stays extended"
);
}
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens
}
None => tracing::warn!(
"SudoVDA target {} not yet an active display path (needs a WDDM GPU to activate)",
ao.target_id
),
}
Ok(Monitor {
guid: session_guid,
target_id: ao.target_id, target_id: ao.target_id,
luid: ao.luid, luid: ao.luid,
gdi_name,
mode,
stop,
pinger: Some(pinger),
ccd_saved,
gen: MON_GEN.fetch_add(1, Ordering::Relaxed),
}) })
} }
}
impl Monitor { unsafe fn remove_monitor(&self, dev: HANDLE, key: &MonitorKey) -> Result<()> {
/// The capture target handed to a session (`None` until the GDI name resolves). let MonitorKey::Guid(guid) = key else {
fn target(&self) -> Option<crate::capture::dxgi::WinCaptureTarget> { anyhow::bail!("sudovda: unexpected monitor key kind");
self.gdi_name };
.clone() let rp = RemoveParams { guid: *guid };
.map(|n| crate::capture::dxgi::WinCaptureTarget { let rp_bytes = unsafe {
adapter_luid: crate::capture::dxgi::pack_luid(self.luid), std::slice::from_raw_parts(&rp as *const _ as *const u8, size_of::<RemoveParams>())
gdi_name: n, };
// target_id is stable across secure-desktop topology rebuilds; the GDI name is NOT,
// so capture re-resolves the name from this on every recovery.
target_id: self.target_id,
})
}
/// Stop the watchdog ping, re-attach the displays we detached, then REMOVE the monitor (by GUID).
/// `device` is the host-level control handle. Consumes the monitor.
unsafe fn teardown(mut self, device: isize) {
self.stop.store(true, Ordering::Relaxed);
if let Some(j) = self.pinger.take() {
let _ = j.join();
}
// Re-attach detached display(s) BEFORE the REMOVE so the box is never left with zero displays.
if let Some(saved) = &self.ccd_saved {
restore_displays_ccd(saved);
}
let rp = RemoveParams { guid: self.guid };
let rp_bytes =
std::slice::from_raw_parts(&rp as *const _ as *const u8, size_of::<RemoveParams>());
let mut none: [u8; 0] = []; let mut none: [u8; 0] = [];
let h = HANDLE(device as *mut c_void); unsafe { ioctl(dev, IOCTL_REMOVE, rp_bytes, &mut none) }.map(|_| ())
if let Err(e) = ioctl(h, IOCTL_REMOVE, rp_bytes, &mut none) {
tracing::warn!("SudoVDA REMOVE failed: {e:#}");
} else {
tracing::info!("SudoVDA monitor removed");
}
} }
}
/// Open the control device once + read version/watchdog; cache the handle (raw isize) in `g`. unsafe fn ping(&self, dev: HANDLE) -> Result<()> {
fn mgr_ensure_device(g: &mut Mgr) -> Result<isize> {
if let Some(d) = g.device {
return Ok(d);
}
let device = unsafe { open_device()? };
let mut ver = [0u8; 4];
if unsafe { ioctl(device, IOCTL_GET_VERSION, &[], &mut ver) }.is_ok() {
tracing::info!(
"SudoVDA protocol {}.{}.{} (test={})",
ver[0],
ver[1],
ver[2],
ver[3]
);
}
let mut wd = [0u8; 8];
g.watchdog_s = if unsafe { ioctl(device, IOCTL_GET_WATCHDOG, &[], &mut wd) }.is_ok() {
u32::from_le_bytes([wd[0], wd[1], wd[2], wd[3]]).max(1)
} else {
3
};
tracing::info!("SudoVDA watchdog timeout {}s", g.watchdog_s);
// Reap monitors orphaned by a crashed/killed previous host instance before we create ours.
// pf-vdisplay honors IOCTL_CLEAR_ALL; SudoVDA returns invalid (ignored). Without it an orphan
// lingers until the driver watchdog fires — but a still-pinging new session keeps resetting that
// watchdog, so orphans could accumulate (the "5-6 stale monitors that never tear down" failure).
{
let mut none: [u8; 0] = []; let mut none: [u8; 0] = [];
if unsafe { ioctl(device, IOCTL_CLEAR_ALL, &[], &mut none) }.is_ok() { unsafe { ioctl(dev, IOCTL_DRIVER_PING, &[], &mut none) }.map(|_| ())
tracing::info!("cleared orphaned virtual monitors on host startup");
}
}
let raw = device.0 as isize;
g.device = Some(raw);
Ok(raw)
}
/// Linger window before a session-less monitor is torn down. A reconnect within it reuses the
/// monitor (no new screen / PnP chime); after it the monitor is REMOVEd so a physical screen returns.
fn linger_ms() -> u64 {
std::env::var("PUNKTFUNK_MONITOR_LINGER_MS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(10_000)
}
/// Acquire the shared monitor for a new session: join the live one (refcount++), reuse a lingering
/// one (reconfiguring if the client mode changed), or create one. The returned [`MonitorLease`]
/// releases the refcount on drop.
fn mgr_acquire(mode: Mode) -> Result<VirtualOutput> {
ensure_linger_timer();
let mut g = MGR.lock().unwrap();
let device = mgr_ensure_device(&mut g)?;
let watchdog_s = g.watchdog_s;
// IDD-push: a new connection while a monitor is live = a single-client RECONNECT (the prior client
// is gone — IDD-push is one display, no concurrency). A REUSED IddCx monitor's swap-chain is DEAD,
// so joining it would hand the new client a black screen until the old session times out. PREEMPT:
// tear the old monitor down (its Drop restores topology + IOCTL_REMOVEs) and fall through to create
// a FRESH one. The old session's lease is gen-stamped, so its later drop is ignored (mgr_release
// no-op) and can't tear down the new monitor.
if idd_push_mode()
&& matches!(
g.state,
MgrState::Active { .. } | MgrState::Lingering { .. }
)
{
if let MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } =
std::mem::replace(&mut g.state, MgrState::Idle)
{
tracing::info!(
old_target = mon.target_id,
"IDD-push reconnect — preempting the prior session, recreating a fresh monitor"
);
// teardown() — NOT drop() — sends IOCTL_REMOVE (and restores topology). `Monitor` has NO
// `Drop` impl, so a bare `drop(mon)` orphaned the IddCx monitor in the driver: it was never
// departed, so it kept a live D3D device + a stuck swap-chain processor thread, and these
// accumulated every reconnect (the driver-side churn leak: +1 device, ~36 nvwgf2umx threads,
// ~50 MB VRAM per session, until it choked). teardown frees it via the driver's do_remove.
unsafe { mon.teardown(device) };
// Let the OS finish the ASYNC IddCx monitor departure before the next ADD. A back-to-back
// REMOVE→ADD races the teardown and the ADD IOCTL is rejected (`DeviceIoControl failed`)
// under reconnect churn. Held under the MGR lock, but IDD-push setup is already serialized
// (IDD_SETUP_LOCK), so this only paces the recreate — exactly what a reconnect flood needs.
thread::sleep(Duration::from_millis(400));
}
}
// A live monitor already exists — join it (refcount++). This covers a concurrent session AND the
// build-then-drop overlap of a mid-stream Reconfigure / secure-return (the new lease is taken while
// the old is still held). If the requested mode differs, reconfigure the shared monitor to it so a
// Reconfigure actually applies (one shared monitor → sessions necessarily share a mode).
if let MgrState::Active { mon, refs } = &mut g.state {
*refs += 1;
let changed = mon.mode.width != mode.width
|| mon.mode.height != mode.height
|| mon.mode.refresh_hz != mode.refresh_hz;
if changed {
unsafe { mgr_reconfigure(mon, mode) };
}
tracing::info!(
refs = *refs,
"SudoVDA monitor reused (concurrent / reconfigure session)"
);
let pm = Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz));
let target = mon.target();
let gen = mon.gen;
return Ok(VirtualOutput {
node_id: 0,
preferred_mode: pm,
win_capture: target,
keepalive: Box::new(MonitorLease { gen }),
});
}
// Idle or Lingering: repurpose/create a monitor → Active{refs:1}.
let mon = match std::mem::replace(&mut g.state, MgrState::Idle) {
MgrState::Lingering { mut mon, .. } => {
tracing::info!("SudoVDA monitor reused (reconnect within the linger window)");
let changed = mon.mode.width != mode.width
|| mon.mode.height != mode.height
|| mon.mode.refresh_hz != mode.refresh_hz;
if changed {
unsafe { mgr_reconfigure(&mut mon, mode) };
}
mon
}
MgrState::Idle => unsafe { create_monitor(device, mode, watchdog_s)? },
MgrState::Active { .. } => unreachable!("handled above"),
};
let pm = Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz));
let target = mon.target();
let gen = mon.gen;
g.state = MgrState::Active { mon, refs: 1 };
Ok(VirtualOutput {
node_id: 0,
preferred_mode: pm,
win_capture: target,
keepalive: Box::new(MonitorLease { gen }),
})
}
/// Re-apply a (possibly new) mode to a reused monitor on reconnect, re-resolving its GDI name.
unsafe fn mgr_reconfigure(mon: &mut Monitor, mode: Mode) {
tracing::info!(
old = format!(
"{}x{}@{}",
mon.mode.width, mon.mode.height, mon.mode.refresh_hz
),
new = format!("{}x{}@{}", mode.width, mode.height, mode.refresh_hz),
"SudoVDA: reconfiguring reused monitor to the new client mode"
);
if let Some(n) = resolve_gdi_name(mon.target_id) {
mon.gdi_name = Some(n);
}
if let Some(n) = &mon.gdi_name {
set_active_mode(n, mode);
}
mon.mode = mode;
}
/// Release a session's hold: refcount-- ; when the last session leaves, LINGER before teardown.
/// `gen` is the lease's monitor generation: a STALE lease (its monitor was already torn down +
/// recreated under it — the IDD-push reconnect-preempt path) does nothing, so it can't decrement the
/// CURRENT (fresh) monitor's refcount and tear it down.
fn mgr_release(gen: u64) {
let mut g = MGR.lock().unwrap();
let stale = match &g.state {
MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } => mon.gen != gen,
MgrState::Idle => true,
};
if stale {
return;
}
g.state = match std::mem::replace(&mut g.state, MgrState::Idle) {
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,
"SudoVDA: last session left — lingering before teardown"
);
MgrState::Lingering {
mon,
until: Instant::now() + Duration::from_millis(ms),
}
}
other => other,
};
}
/// Wait (up to `timeout`) for the active monitor to be RELEASED — i.e. the MGR is no longer `Active`
/// (the prior session dropped its lease → `Lingering`/`Idle`). Used by the IDD-push reconnect preempt:
/// after signalling the old session to stop, we wait here so it tears its monitor down CLEANLY (while
/// frames still flow) before we acquire a fresh one — instead of dropping the monitor out from under a
/// still-live session, which churns the driver's ADD/REMOVE path and wedges it under rapid reconnects.
pub(crate) fn wait_for_monitor_released(timeout: Duration) {
let deadline = Instant::now() + timeout;
loop {
if !matches!(MGR.lock().unwrap().state, MgrState::Active { .. }) {
return;
}
if Instant::now() >= deadline {
tracing::warn!(
"IDD-push preempt: prior session didn't release the monitor within {timeout:?} — \
proceeding (mgr_acquire will preempt it)"
);
return;
}
thread::sleep(Duration::from_millis(25));
} }
} }
/// Background timer (started once): tear down a monitor that has lingered past its deadline (→ Idle), /// The Windows SudoVDA virtual-display backend. A marker — the lifecycle lives in the shared
/// so a physical-screen user gets their screen back after they stop streaming. /// [`VirtualDisplayManager`](super::manager::VirtualDisplayManager).
fn ensure_linger_timer() { pub struct SudoVdaDisplay;
static TIMER: Once = Once::new();
TIMER.call_once(|| { impl SudoVdaDisplay {
let _ = thread::Builder::new() pub fn new() -> Result<Self> {
.name("sudovda-linger".into()) super::manager::init(Box::new(SudoVdaDriver)).open_backend()?;
.spawn(|| loop { Ok(Self)
thread::sleep(Duration::from_millis(500)); }
let mut g = MGR.lock().unwrap();
let due = matches!(&g.state, MgrState::Lingering { until, .. } if Instant::now() >= *until);
if due {
let device = g.device.unwrap_or(0);
if let MgrState::Lingering { mon, .. } =
std::mem::replace(&mut g.state, MgrState::Idle)
{
drop(g); // release the lock before the REMOVE IOCTL + display restore
unsafe { mon.teardown(device) };
}
}
});
});
} }
/// A session's lease on the shared monitor. Drop releases the refcount (→ linger when it hits 0), impl VirtualDisplay for SudoVdaDisplay {
/// UNLESS the monitor was already torn down + recreated under it (gen mismatch — the IDD-push fn name(&self) -> &'static str {
/// reconnect-preempt path), in which case the drop is a no-op so it can't tear down the new monitor. "sudovda"
struct MonitorLease { }
gen: u64,
} fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
impl Drop for MonitorLease { super::manager::vdm().acquire(mode)
fn drop(&mut self) {
mgr_release(self.gen);
} }
} }
@@ -746,6 +325,8 @@ pub fn is_available() -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::thread;
use std::time::Duration;
/// Live hardware round trip — skipped unless `PUNKTFUNK_SUDOVDA_LIVE=1` (needs the SudoVDA /// Live hardware round trip — skipped unless `PUNKTFUNK_SUDOVDA_LIVE=1` (needs the SudoVDA
/// driver installed). Exercises the real trait path: open -> create -> hold -> drop (REMOVE). /// driver installed). Exercises the real trait path: open -> create -> hold -> drop (REMOVE).