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:
@@ -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)?;
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
Reference in New Issue
Block a user