Files
punktfunk/crates/punktfunk-host/src/vdisplay/sudovda.rs
T
enricobuehler 0ce2e37faf refactor(host/windows): clean up DDA path + add a proper Windows service
Final cleanup after the DDA-parity work, plus an end-user service to replace the
PsExec/VBS/scheduled-task launch chain.

Cleanup (behavior-preserving):
- sudovda.rs: drop the dead legacy GDI isolate_displays/restore_displays (CCD is
  the sole isolation path), the always-empty Monitor.isolated field, and the
  vestigial reassert_isolation + PUNKTFUNK_ISOLATE_DISPLAYS knob; fix stale comments.
- dxgi.rs: downgrade leftover debug warns/infos (DuplicateOutput1 retry, FALLBACKS,
  hook-hits, AcquireNextFrame idle timeout) to debug!; remove the PUNKTFUNK_NO_CURSOR
  per-frame test knob.

Windows service (src/service.rs, `punktfunk-host service`):
- SCM supervisor (windows-service crate) that duplicates its LocalSystem token,
  retargets it to the active console session, and CreateProcessAsUserW's the host
  there (Sunshine/Apollo model) — relaunching on exit and console session switch,
  inside a kill-on-close job object so a service crash never orphans the host.
- install/uninstall/start/stop/status subcommands: one elevated `service install`
  registers an auto-start LocalSystem service + firewall rules + a default host.env.
- Config moves to %ProgramData%\punktfunk\host.env; config_dir() now resolves to
  %ProgramData%\punktfunk on Windows (replacing the APPDATA=C:\Users\Public hack),
  with a PUNKTFUNK_CONFIG_DIR override. Logs land in %ProgramData%\punktfunk\logs\.
- merged_env_block (shared with the WGC helper) now also carries RUST_LOG.
- docs/windows-service.md + scripts/windows/host.env.example; windows-host.md updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 18:44:15 +00:00

996 lines
40 KiB
Rust

//! Windows virtual-display backend driving **SudoVDA** (the SudoMaker Virtual Display Adapter —
//! the Indirect Display Driver the Apollo Sunshine-fork ships). The Windows analogue of the
//! Linux per-compositor backends: [`create`](VirtualDisplay::create) adds a virtual monitor at the
//! client's exact `WxH@Hz` (the mode is baked into the ADD IOCTL — no EDID seeding), starts the
//! mandatory watchdog ping, and the returned [`VirtualOutput`]'s keepalive `Drop` removes it (RAII).
//!
//! Control surface (verified live against SudoVDA 0.2.1): a device-interface-GUID + `CreateFileW`
//! + `DeviceIoControl` IOCTL protocol. No DLL, no named pipe. See `docs/windows-host.md`.
use std::ffi::c_void;
use std::mem::size_of;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, Once};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use windows::core::{GUID, PCWSTR};
use windows::Win32::Devices::DeviceAndDriverInstallation::{
SetupDiDestroyDeviceInfoList, SetupDiEnumDeviceInterfaces, SetupDiGetClassDevsW,
SetupDiGetDeviceInterfaceDetailW, DIGCF_DEVICEINTERFACE, DIGCF_PRESENT,
SP_DEVICE_INTERFACE_DATA, SP_DEVICE_INTERFACE_DETAIL_DATA_W,
};
use windows::Win32::Devices::Display::{
DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes,
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_MODE_INFO,
DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE,
DISPLAYCONFIG_SOURCE_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY,
SDC_USE_SUPPLIED_DISPLAY_CONFIG,
};
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
use windows::Win32::Graphics::Gdi::{
ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW,
DISP_CHANGE_SUCCESSFUL, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, DM_PELSWIDTH,
ENUM_DISPLAY_SETTINGS_MODE,
};
use windows::Win32::Storage::FileSystem::{
CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
};
use windows::Win32::System::IO::DeviceIoControl;
use super::{Mode, VirtualDisplay, VirtualOutput};
// SudoVDA device-interface GUID (Common/Include/sudovda-ioctl.h).
const SUVDA_INTERFACE: GUID = GUID::from_u128(0xE5BC_C234_1E0C_418A_A0D4_EF8B_7501_414D);
// CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, func, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0).
const fn ctl(func: u32) -> u32 {
(0x22u32 << 16) | (func << 2)
}
const IOCTL_ADD: u32 = ctl(0x800);
const IOCTL_REMOVE: u32 = ctl(0x801);
const IOCTL_SET_RENDER_ADAPTER: u32 = ctl(0x802); // == 0x0022_2008
const IOCTL_GET_WATCHDOG: u32 = ctl(0x803);
const IOCTL_DRIVER_PING: u32 = ctl(0x888);
const IOCTL_GET_VERSION: u32 = ctl(0x8FF);
/// A UNIQUE-per-session SudoVDA monitor GUID. The monitor is keyed by GUID for IOCTL_ADD/REMOVE, so a
/// FIXED GUID makes overlapping sessions (a client reconnecting after a freeze before the old session
/// has torn down, or genuine concurrent sessions) all map to the SAME monitor — then one session's
/// IOCTL_REMOVE on teardown tears the monitor down OUT FROM UNDER a still-live session ("display
/// disconnected" sound + freeze, even with no context change — observed live). Make it unique per
/// (process, session): base GUID with the low 48-bit node = (pid << 16 | session#).
fn next_monitor_guid() -> GUID {
use std::sync::atomic::AtomicU32;
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::Relaxed) as u128;
let pid = std::process::id() as u128;
GUID::from_u128(0x70756E6B_7466_756E_6B30_000000000000u128 | (pid << 16) | (n & 0xFFFF))
}
#[repr(C)]
#[derive(Clone, Copy)]
struct AddParams {
width: u32,
height: u32,
refresh: u32,
guid: GUID,
device_name: [u8; 14],
serial: [u8; 14],
}
#[repr(C)]
#[derive(Clone, Copy)]
struct AddOut {
luid: LUID,
target_id: u32,
}
// SET_RENDER_ADAPTER input — byte-identical to SudoVDA's `{ LUID AdapterLuid; }` (8 bytes). The
// windows `LUID` is `{ LowPart: u32, HighPart: i32 }` == the C `LUID`, so `#[repr(C)]` is exact.
#[repr(C)]
#[derive(Clone, Copy)]
struct SetRenderAdapterParams {
luid: LUID,
}
/// Pin the SudoVDA IDD's RENDER GPU to `luid` (Apollo's `SetRenderAdapter`). No output buffer. MUST be
/// issued on the driver handle BEFORE `IOCTL_ADD` to steer which GPU the new target renders on — on a
/// multi-adapter box (SudoVDA IDD + a discrete GPU) this stops DXGI from reparenting the virtual
/// output onto a different adapter than the one we duplicate/encode on (the ACCESS_LOST storm).
unsafe fn set_render_adapter(h: HANDLE, luid: LUID) -> Result<()> {
let p = SetRenderAdapterParams { luid };
let bytes = std::slice::from_raw_parts(
&p as *const _ as *const u8,
size_of::<SetRenderAdapterParams>(),
);
let mut none: [u8; 0] = [];
ioctl(h, IOCTL_SET_RENDER_ADAPTER, bytes, &mut none)
.map(|_| ())
.context("SudoVDA SET_RENDER_ADAPTER")
}
/// Resolve the LUID of the GPU that should RENDER the virtual display = the GPU that drives NVENC +
/// Desktop Duplication (e.g. the RTX 4090). Default: the discrete adapter with the most
/// `DedicatedVideoMemory`, skipping WARP / Basic-Render and the SudoVDA software adapter (≈0 VRAM).
/// `PUNKTFUNK_RENDER_ADAPTER=<substring>` forces a match by Description (Apollo's `adapter_name`).
unsafe fn resolve_render_adapter_luid() -> Option<LUID> {
use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIFactory1};
let want = std::env::var("PUNKTFUNK_RENDER_ADAPTER")
.ok()
.filter(|s| !s.is_empty());
let factory: IDXGIFactory1 = CreateDXGIFactory1().ok()?;
let mut best: Option<(LUID, u64, String)> = None;
let mut i = 0u32;
while let Ok(a) = factory.EnumAdapters1(i) {
i += 1;
let Ok(d) = a.GetDesc1() else { continue };
let name = String::from_utf16_lossy(&d.Description);
let name = name.trim_end_matches('\u{0}').to_string();
let lname = name.to_ascii_lowercase();
if lname.contains("basic render") || lname.contains("warp") {
continue; // never pin to the software rasterizer
}
if let Some(w) = &want {
if lname.contains(&w.to_ascii_lowercase()) {
tracing::info!(
adapter = name,
"render adapter chosen by PUNKTFUNK_RENDER_ADAPTER"
);
return Some(d.AdapterLuid);
}
continue;
}
let vram = d.DedicatedVideoMemory as u64; // SudoVDA software adapter ≈ 0 → loses to the dGPU
if best.as_ref().is_none_or(|(_, v, _)| vram > *v) {
best = Some((d.AdapterLuid, vram, name));
}
}
match best {
Some((luid, vram, name)) => {
tracing::info!(
adapter = name,
vram_mb = vram / (1024 * 1024),
"render adapter chosen (max VRAM)"
);
Some(luid)
}
None => {
tracing::warn!("no suitable render adapter found for SET_RENDER_ADAPTER");
None
}
}
}
#[repr(C)]
struct RemoveParams {
guid: GUID,
}
/// One `DeviceIoControl` round trip (METHOD_BUFFERED). `input`/`output` may be empty.
unsafe fn ioctl(h: HANDLE, code: u32, input: &[u8], output: &mut [u8]) -> Result<u32> {
let mut returned = 0u32;
let inp = (!input.is_empty()).then_some(input.as_ptr() as *const c_void);
let outp = (!output.is_empty()).then_some(output.as_mut_ptr() as *mut c_void);
DeviceIoControl(
h,
code,
inp,
input.len() as u32,
outp,
output.len() as u32,
Some(&mut returned),
None,
)
.with_context(|| format!("DeviceIoControl(code={code:#x})"))?;
Ok(returned)
}
/// Resolve the `\\.\DisplayN` GDI name for a SudoVDA target id via the CCD API. Returns `None`
/// until the OS activates the target into the desktop topology (needs a real WDDM GPU; on a
/// GPU-less box this stays `None` even though ADD succeeded).
pub(crate) unsafe fn resolve_gdi_name(target_id: u32) -> Option<String> {
let mut np = 0u32;
let mut nm = 0u32;
if GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut np, &mut nm).is_err() {
return None;
}
let mut paths = vec![DISPLAYCONFIG_PATH_INFO::default(); np as usize];
let mut modes = vec![DISPLAYCONFIG_MODE_INFO::default(); nm as usize];
if QueryDisplayConfig(
QDC_ONLY_ACTIVE_PATHS,
&mut np,
paths.as_mut_ptr(),
&mut nm,
modes.as_mut_ptr(),
None,
)
.is_err()
{
return None;
}
for p in paths.iter().take(np as usize) {
if p.targetInfo.id == target_id {
let mut src = DISPLAYCONFIG_SOURCE_DEVICE_NAME::default();
src.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
src.header.size = size_of::<DISPLAYCONFIG_SOURCE_DEVICE_NAME>() as u32;
src.header.adapterId = p.sourceInfo.adapterId;
src.header.id = p.sourceInfo.id;
if DisplayConfigGetDeviceInfo(&mut src.header) == 0 {
let name = String::from_utf16_lossy(&src.viewGdiDeviceName);
return Some(name.trim_end_matches('\u{0}').to_string());
}
}
}
None
}
/// Toggle the SudoVDA target's advanced-color (HDR) state via the CCD API. Disabling HDR while on the
/// secure (Winlogon) desktop makes it render SDR/composed so DXGI Desktop Duplication can capture it
/// (the HDR fullscreen independent-flip otherwise storms `ACCESS_LOST` → black); re-enable on return so
/// WGC keeps HDR on the normal desktop. Returns true on a successful `DisplayConfigSetDeviceInfo`.
pub(crate) unsafe fn set_advanced_color(target_id: u32, enable: bool) -> bool {
let mut np = 0u32;
let mut nm = 0u32;
if GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut np, &mut nm).is_err() {
return false;
}
let mut paths = vec![DISPLAYCONFIG_PATH_INFO::default(); np as usize];
let mut modes = vec![DISPLAYCONFIG_MODE_INFO::default(); nm as usize];
if QueryDisplayConfig(
QDC_ONLY_ACTIVE_PATHS,
&mut np,
paths.as_mut_ptr(),
&mut nm,
modes.as_mut_ptr(),
None,
)
.is_err()
{
return false;
}
for p in paths.iter().take(np as usize) {
if p.targetInfo.id == target_id {
let mut s = DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE::default();
s.header.r#type = DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE;
s.header.size = size_of::<DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE>() as u32;
s.header.adapterId = p.targetInfo.adapterId;
s.header.id = p.targetInfo.id;
s.Anonymous.value = enable as u32; // bit 0 = enableAdvancedColor
let rc = DisplayConfigSetDeviceInfo(&s.header);
tracing::info!(
target_id,
enable,
rc,
"SudoVDA set advanced-color (HDR) state"
);
return rc == 0;
}
}
tracing::warn!(
target_id,
"set_advanced_color: target not found in active paths"
);
false
}
/// Force the freshly-added SudoVDA monitor to the client's exact `WxH@Hz`. The ADD IOCTL only
/// ADVERTISES the mode; Windows otherwise activates an IDD target at a 1280x720 default, so the
/// ACTIVE mode (what DXGI Desktop Duplication captures) must be set explicitly. CDS_TEST first so a
/// mode the driver didn't advertise just leaves the default instead of erroring the session.
fn set_active_mode(gdi_name: &str, mode: Mode) {
let wname: Vec<u16> = gdi_name.encode_utf16().chain(std::iter::once(0)).collect();
// Enumerate the modes the driver actually advertises for this output and pick the best match for
// the requested RESOLUTION: the exact refresh if present, else the highest advertised refresh
// <= requested, else the highest available at that resolution. The SudoVDA ADD IOCTL advertises
// the client mode, but a very high pixel rate (e.g. 5120x1440@240 = 1.77 Gpix/s) can be clamped
// or absent — falling back to a lower refresh AT THE SAME RESOLUTION keeps the client's
// resolution (what the user sees) instead of collapsing to the 1280x720/1920x1080 OS default.
let mut at_res: Vec<u32> = Vec::new();
let mut res_set: std::collections::BTreeSet<(u32, u32)> = std::collections::BTreeSet::new();
let mut i = 0u32;
loop {
let mut dm = DEVMODEW {
dmSize: size_of::<DEVMODEW>() as u16,
..Default::default()
};
let ok = unsafe {
EnumDisplaySettingsW(
PCWSTR(wname.as_ptr()),
ENUM_DISPLAY_SETTINGS_MODE(i),
&mut dm,
)
}
.as_bool();
if !ok {
break;
}
i += 1;
res_set.insert((dm.dmPelsWidth, dm.dmPelsHeight));
if dm.dmPelsWidth == mode.width && dm.dmPelsHeight == mode.height {
at_res.push(dm.dmDisplayFrequency);
}
}
let chosen_hz = if at_res.contains(&mode.refresh_hz) {
mode.refresh_hz
} else if let Some(hz) = at_res
.iter()
.copied()
.filter(|&hz| hz <= mode.refresh_hz)
.max()
{
hz
} else if let Some(hz) = at_res.iter().copied().max() {
hz
} else {
mode.refresh_hz // resolution not advertised at all; attempt anyway (likely -> OS default)
};
if at_res.is_empty() {
tracing::warn!(
"{gdi_name}: driver advertises no {}x{} mode (top advertised: {:?}); attempting @{} anyway",
mode.width,
mode.height,
res_set.iter().rev().take(8).collect::<Vec<_>>(),
mode.refresh_hz
);
} else if chosen_hz != mode.refresh_hz {
tracing::info!(
"{gdi_name}: {}x{}@{} not advertised; using {}x{}@{} (advertised refreshes here: {:?})",
mode.width,
mode.height,
mode.refresh_hz,
mode.width,
mode.height,
chosen_hz,
at_res
);
}
// Set ONLY this output's mode in place (size/refresh/bpp; NO DM_POSITION). Do NOT promote it to
// PRIMARY here and do NOT write a GLOBAL topology: promoting the IDD to primary at (0,0) while the
// box's leftover basic display is still active contests the topology and storms
// DXGI_ERROR_MODE_CHANGE_IN_PROGRESS (measured live). The IDD is made the sole → primary →
// DWM-composited display by the CCD isolation in create() (which deactivates the other display
// first), so a sole display is already primary and needs no CDS_SET_PRIMARY here.
let dm = DEVMODEW {
dmSize: size_of::<DEVMODEW>() as u16,
dmFields: DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL,
dmBitsPerPel: 32,
dmPelsWidth: mode.width,
dmPelsHeight: mode.height,
dmDisplayFrequency: chosen_hz,
..Default::default()
};
let test = unsafe {
ChangeDisplaySettingsExW(PCWSTR(wname.as_ptr()), Some(&dm), None, CDS_TEST, None)
};
if test != DISP_CHANGE_SUCCESSFUL {
tracing::warn!(
result = test.0,
"{gdi_name}: driver rejected {}x{}@{} (mode not advertised?) — leaving OS default",
mode.width,
mode.height,
chosen_hz
);
return;
}
let apply = unsafe {
ChangeDisplaySettingsExW(
PCWSTR(wname.as_ptr()),
Some(&dm),
None,
CDS_UPDATEREGISTRY,
None,
)
};
if apply == DISP_CHANGE_SUCCESSFUL {
tracing::info!(
"{gdi_name}: active mode set to {}x{}@{}",
mode.width,
mode.height,
chosen_hz
);
} else {
tracing::warn!(
result = apply.0,
"{gdi_name}: failed to apply {}x{}@{}",
mode.width,
mode.height,
chosen_hz
);
}
}
/// Saved active display topology, for restoring on teardown.
type SavedConfig = (Vec<DISPLAYCONFIG_PATH_INFO>, Vec<DISPLAYCONFIG_MODE_INFO>);
/// `DISPLAYCONFIG_PATH_ACTIVE` (wingdi.h) — the `flags` bit marking a path active. The `windows` crate
/// doesn't export it, so define it here.
const DISPLAYCONFIG_PATH_ACTIVE: u32 = 0x0000_0001;
/// Robust display isolation via the CCD API. The naive GDI approach (EnumDisplayDevices +
/// ChangeDisplaySettings) MISSES displays on a hybrid box — an iGPU-attached physical monitor isn't
/// flagged `ATTACHED_TO_DESKTOP` in the GDI enum, so it's never detached and the secure desktop /
/// lock screen lands on IT while our virtual output freezes. `QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS)`
/// sees every active path; we deactivate all of them EXCEPT the SudoVDA target's, leaving the virtual
/// display as the sole desktop so ALL content (incl. Winlogon) renders to it. Apollo isolates the same
/// way (CCD). Returns the original active config to restore on teardown.
unsafe fn isolate_displays_ccd(keep_target_id: u32) -> Option<SavedConfig> {
let mut np = 0u32;
let mut nm = 0u32;
if GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut np, &mut nm).is_err() {
return None;
}
let mut paths = vec![DISPLAYCONFIG_PATH_INFO::default(); np as usize];
let mut modes = vec![DISPLAYCONFIG_MODE_INFO::default(); nm as usize];
if QueryDisplayConfig(
QDC_ONLY_ACTIVE_PATHS,
&mut np,
paths.as_mut_ptr(),
&mut nm,
modes.as_mut_ptr(),
None,
)
.is_err()
{
return None;
}
paths.truncate(np as usize);
modes.truncate(nm as usize);
let saved = (paths.clone(), modes.clone());
let mut others = 0u32;
for p in paths.iter_mut() {
if p.targetInfo.id == keep_target_id {
continue;
}
if p.flags & DISPLAYCONFIG_PATH_ACTIVE != 0 {
p.flags &= !DISPLAYCONFIG_PATH_ACTIVE; // mark this path inactive
others += 1;
}
}
if others == 0 {
tracing::info!("display isolate (CCD): SudoVDA target {keep_target_id} already the only active display");
return Some(saved);
}
let rc = SetDisplayConfig(
Some(paths.as_slice()),
Some(modes.as_slice()),
SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES,
);
if rc == 0 {
tracing::info!("display isolate (CCD): deactivated {others} other display(s) — SudoVDA target {keep_target_id} is now the sole desktop");
} else {
tracing::warn!("display isolate (CCD): SetDisplayConfig failed rc={rc:#x} (tried to deactivate {others} path(s))");
}
Some(saved)
}
/// Restore the topology saved by [`isolate_displays_ccd`] (teardown, before the virtual output is
/// removed), re-activating the displays we deactivated.
unsafe fn restore_displays_ccd(saved: &SavedConfig) {
let (paths, modes) = saved;
if paths.is_empty() {
return;
}
let rc = SetDisplayConfig(
Some(paths.as_slice()),
Some(modes.as_slice()),
SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES,
);
tracing::info!("display isolate (CCD): restored original topology rc={rc:#x}");
}
unsafe fn open_device() -> Result<HANDLE> {
let hdev = SetupDiGetClassDevsW(
Some(&SUVDA_INTERFACE),
PCWSTR::null(),
None,
DIGCF_DEVICEINTERFACE | DIGCF_PRESENT,
)
.context("SetupDiGetClassDevsW(SudoVDA) — is the SudoVDA driver installed?")?;
let mut idata = SP_DEVICE_INTERFACE_DATA {
cbSize: size_of::<SP_DEVICE_INTERFACE_DATA>() as u32,
..Default::default()
};
SetupDiEnumDeviceInterfaces(hdev, None, &SUVDA_INTERFACE, 0, &mut idata)
.context("SetupDiEnumDeviceInterfaces(SudoVDA)")?;
let mut required = 0u32;
let _ = SetupDiGetDeviceInterfaceDetailW(hdev, &idata, None, 0, Some(&mut required), None);
let mut buf = vec![0u8; required as usize];
let detail = buf.as_mut_ptr() as *mut SP_DEVICE_INTERFACE_DETAIL_DATA_W;
(*detail).cbSize = size_of::<SP_DEVICE_INTERFACE_DETAIL_DATA_W>() as u32;
SetupDiGetDeviceInterfaceDetailW(hdev, &idata, Some(detail), required, None, None)
.context("SetupDiGetDeviceInterfaceDetailW(SudoVDA)")?;
let handle = CreateFileW(
PCWSTR((*detail).DevicePath.as_ptr()),
0xC000_0000, // GENERIC_READ | GENERIC_WRITE
FILE_SHARE_READ | FILE_SHARE_WRITE,
None,
OPEN_EXISTING,
FILE_FLAGS_AND_ATTRIBUTES(0),
None,
)
.context("CreateFileW(SudoVDA device)")?;
let _ = SetupDiDestroyDeviceInfoList(hdev);
Ok(handle)
}
// ── Host-level reference-counted SudoVDA monitor lifecycle ──────────────────────────────────────
//
// The virtual monitor is created on the first session and REUSED across sessions. When the last
// 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).
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>,
}
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 {
"sudovda"
}
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
// Delegate to the host-level manager: create the monitor, reuse a lingering one on reconnect,
// or join the live one — and hand back a lease whose Drop releases the refcount.
mgr_acquire(mode)
}
}
/// Create a fresh SudoVDA monitor at `mode` on the (host-level) control `device`. The old per-session
/// `create()` body, now owned by the manager: ADD the target, start the watchdog ping, resolve the
/// GDI name, force the client mode + (default) isolate to a sole composited display. Returns the
/// [`Monitor`] resources; the manager tracks its lifecycle (refcount + linger).
unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<Monitor> {
let dev = HANDLE(device as *mut c_void);
{
let mut device_name = [0u8; 14];
let nm = b"punktfunk";
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 add = AddParams {
width: mode.width,
height: mode.height,
refresh: mode.refresh_hz,
guid: session_guid,
device_name,
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 std::env::var("PUNKTFUNK_RENDER_ADAPTER").is_ok() {
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 {
std::slice::from_raw_parts(&add as *const _ as *const u8, size_of::<AddParams>())
};
let mut out = [0u8; size_of::<AddOut>()];
unsafe { ioctl(dev, IOCTL_ADD, add_bytes, &mut out) }.with_context(|| {
format!(
"SudoVDA ADD {}x{}@{}",
mode.width, mode.height, mode.refresh_hz
)
})?;
let ao = unsafe { *(out.as_ptr() as *const AddOut) };
tracing::info!(
"SudoVDA created {}x{}@{} (target_id={}, adapter_luid={:#x})",
mode.width,
mode.height,
mode.refresh_hz,
ao.target_id,
ao.luid.LowPart
);
if let Some(luid) = pinned {
if ao.luid.LowPart == luid.LowPart && ao.luid.HighPart == luid.HighPart {
tracing::info!("SudoVDA ADD render adapter matches the pinned GPU (pin took)");
} else {
tracing::warn!(
add = format!("{:08x}:{:08x}", ao.luid.HighPart, ao.luid.LowPart),
pinned = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart),
"SudoVDA ADD render adapter DIFFERS from pinned — driver ignored SET_RENDER_ADAPTER?"
);
}
}
// Mandatory keepalive: ping inside the watchdog window or the driver tears all displays down.
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);
while !stop_t.load(Ordering::Relaxed) {
let mut none: [u8; 0] = [];
unsafe {
let _ = ioctl(h, IOCTL_DRIVER_PING, &[], &mut none);
}
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,
luid: ao.luid,
gdi_name,
mode,
stop,
pinger: Some(pinger),
ccd_saved,
})
}
}
impl Monitor {
/// The capture target handed to a session (`None` until the GDI name resolves).
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 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 h = HANDLE(device as *mut c_void);
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`.
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);
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;
// 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();
return Ok(VirtualOutput {
node_id: 0,
preferred_mode: pm,
win_capture: target,
keepalive: Box::new(MonitorLease),
});
}
// 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();
g.state = MgrState::Active { mon, refs: 1 };
Ok(VirtualOutput {
node_id: 0,
preferred_mode: pm,
win_capture: target,
keepalive: Box::new(MonitorLease),
})
}
/// 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.
fn mgr_release() {
let mut g = MGR.lock().unwrap();
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,
};
}
/// 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("sudovda-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).
struct MonitorLease;
impl Drop for MonitorLease {
fn drop(&mut self) {
mgr_release();
}
}
/// Readiness probe: can we open the SudoVDA control device?
pub fn probe() -> Result<()> {
let h = unsafe { open_device()? };
unsafe {
let _ = CloseHandle(h);
}
Ok(())
}
/// Is the SudoVDA driver present (device interface enumerable)?
pub fn is_available() -> bool {
unsafe { open_device().map(|h| CloseHandle(h)).is_ok() }
}
#[cfg(test)]
mod tests {
use super::*;
/// 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).
#[test]
fn live_create_drop() {
if std::env::var("PUNKTFUNK_SUDOVDA_LIVE").is_err() {
return;
}
let mut vd = SudoVdaDisplay::new().expect("open SudoVDA");
let vout = vd
.create(Mode {
width: 1920,
height: 1080,
refresh_hz: 60,
})
.expect("create virtual display");
assert_eq!(vout.preferred_mode, Some((1920, 1080, 60)));
thread::sleep(Duration::from_secs(3));
drop(vout); // triggers REMOVE + stops the pinger
}
}