//! 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; use std::thread::{self, JoinHandle}; use std::time::Duration; 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, GetDisplayConfigBufferSizes, QueryDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_SOURCE_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS, }; use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID}; 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_GET_WATCHDOG: u32 = ctl(0x803); const IOCTL_DRIVER_PING: u32 = ctl(0x888); const IOCTL_GET_VERSION: u32 = ctl(0x8FF); // A fixed monitor identity. One session at a time today; Windows persists this monitor's layout // across sessions by GUID, and REMOVE keys off it. (TODO: derive per-client when concurrent // sessions land.) const MONITOR_GUID: GUID = GUID::from_u128(0x70756E6B_7466_756E_6B30_000000000001); #[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, } #[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 { let mut returned = 0u32; let inp = (!input.is_empty()).then(|| input.as_ptr() as *const c_void); let outp = (!output.is_empty()).then(|| 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). unsafe fn resolve_gdi_name(target_id: u32) -> Option { 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::() 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 } unsafe fn open_device() -> Result { 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::() 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::() 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) } /// A live SudoVDA control handle. One per host; `create` adds/removes monitors on it. pub struct SudoVdaDisplay { device: HANDLE, watchdog_s: u32, } // The HANDLE is a kernel object usable from any thread; we only ever issue serialized IOCTLs. unsafe impl Send for SudoVdaDisplay {} impl SudoVdaDisplay { pub fn new() -> Result { 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]; 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 {watchdog_s}s"); Ok(Self { device, watchdog_s }) } } impl Drop for SudoVdaDisplay { fn drop(&mut self) { unsafe { let _ = CloseHandle(self.device); } } } impl VirtualDisplay for SudoVdaDisplay { fn name(&self) -> &'static str { "sudovda" } fn create(&mut self, mode: Mode) -> Result { let mut device_name = [0u8; 14]; let nm = b"punktfunk"; device_name[..nm.len()].copy_from_slice(nm); let add = AddParams { width: mode.width, height: mode.height, refresh: mode.refresh_hz, guid: MONITOR_GUID, device_name, serial: [0u8; 14], }; let add_bytes = unsafe { std::slice::from_raw_parts(&add as *const _ as *const u8, size_of::()) }; let mut out = [0u8; size_of::()]; unsafe { ioctl(self.device, 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 ); // Mandatory keepalive: ping inside the watchdog window or the driver tears all displays down. let stop = Arc::new(AtomicBool::new(false)); let device_raw = self.device.0 as isize; let interval = Duration::from_millis(self.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; } } match &gdi_name { Some(n) => tracing::info!("SudoVDA target {} -> {n}", ao.target_id), None => tracing::warn!( "SudoVDA target {} not yet an active display path (needs a WDDM GPU to activate)", ao.target_id ), } Ok(VirtualOutput { node_id: 0, // unused on Windows; the capture target is the GDI name below preferred_mode: Some((mode.width, mode.height, mode.refresh_hz)), keepalive: Box::new(SudoVdaKeepalive { device: device_raw, guid: MONITOR_GUID, stop, pinger: Some(pinger), gdi_name, }), }) } } /// RAII teardown: stop the ping thread, then REMOVE the monitor by its GUID. Does NOT close the /// device handle — that belongs to [`SudoVdaDisplay`], which outlives the output. struct SudoVdaKeepalive { device: isize, guid: GUID, stop: Arc, pinger: Option>, #[allow(dead_code)] // consumed by the Windows capture backend (not yet wired) gdi_name: Option, } impl Drop for SudoVdaKeepalive { fn drop(&mut self) { self.stop.store(true, Ordering::Relaxed); if let Some(j) = self.pinger.take() { let _ = j.join(); } let rp = RemoveParams { guid: self.guid }; let rp_bytes = unsafe { std::slice::from_raw_parts(&rp as *const _ as *const u8, size_of::()) }; let mut none: [u8; 0] = []; let h = HANDLE(self.device as *mut c_void); if let Err(e) = unsafe { ioctl(h, IOCTL_REMOVE, rp_bytes, &mut none) } { tracing::warn!("SudoVDA REMOVE failed: {e:#}"); } else { tracing::info!("SudoVDA monitor removed"); } } } /// 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 } }