feat(host/windows): native res, cursor, secure-desktop capture, windowless SYSTEM launch
apple / swift (push) Successful in 52s
ci / rust (push) Failing after 36s
ci / web (push) Successful in 31s
android / android (push) Successful in 1m52s
ci / docs-site (push) Successful in 29s
ci / bench (push) Successful in 1m39s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 3m19s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m15s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m57s
docker / deploy-docs (push) Successful in 17s
apple / swift (push) Successful in 52s
ci / rust (push) Failing after 36s
ci / web (push) Successful in 31s
android / android (push) Successful in 1m52s
ci / docs-site (push) Successful in 29s
ci / bench (push) Successful in 1m39s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 3m19s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m15s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m57s
docker / deploy-docs (push) Successful in 17s
Live-validated Mac <-> RTX 4090 at the display's native 5120x1440@240: - Resolution: set_active_mode enumerates the IDD's advertised modes and sets the requested resolution at the best supported refresh (keeps 5120x1440@240; no more silent fallback to the 1080p OS default when an exact mode is briefly unavailable). - Bitrate auto-cap: NVENC init probes and steps the average bitrate down to the GPU's codec-level max so a high client bitrate connects (matches the Linux host; we do not split NVENC sessions). - Mouse cursor: DXGI duplication excludes the HW cursor; capture the pointer shape/position (GetFramePointerShape) and GPU-composite it before NVENC. Color cursors alpha-blend; masked-color (the text I-beam) uses an INV_DEST_COLOR inversion blend so the caret inverts the screen and shows on any background (no black box); monochrome handled too. - Secure desktop (lock / login / UAC): run as SYSTEM in the interactive session, follow the input desktop via SetThreadDesktop, and on the WinSta switch recreate the D3D11 device and re-resolve the virtual output's GDI name from the stable SudoVDA target id (the name changes across the topology rebuild; the old failure hunted the stale \\.\DISPLAYn and dropped). ACCESS_LOST / INVALID_CALL / device-removed are recoverable, and a mid-stream resolution change is followed (capturer + NVENC re-init at the new size). isolate_displays detaches other monitors so Winlogon renders to the virtual output. One real session recovered 1012 desktop switches and completed cleanly. Windows-only backends; Linux/macOS unaffected. Builds clean on x86_64-pc-windows-msvc. Deployment (windowless SYSTEM launch via PsExec + hidden VBScript) documented in docs/windows-host.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//! Windows virtual-display backend driving **SudoVDA** (the SudoMaker Virtual Display Adapter —
|
||||
//! 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
|
||||
@@ -27,6 +27,12 @@ use windows::Win32::Devices::Display::{
|
||||
DISPLAYCONFIG_SOURCE_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS,
|
||||
};
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
ChangeDisplaySettingsExW, EnumDisplayDevicesW, EnumDisplaySettingsW, CDS_GLOBAL, CDS_NORESET,
|
||||
CDS_SET_PRIMARY, CDS_TEST, CDS_TYPE, CDS_UPDATEREGISTRY, DEVMODEW, DISPLAY_DEVICEW,
|
||||
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, DISP_CHANGE_SUCCESSFUL, DM_BITSPERPEL, DM_DISPLAYFREQUENCY,
|
||||
DM_PELSHEIGHT, DM_PELSWIDTH, DM_POSITION, ENUM_CURRENT_SETTINGS, ENUM_DISPLAY_SETTINGS_MODE,
|
||||
};
|
||||
use windows::Win32::Storage::FileSystem::{
|
||||
CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
|
||||
};
|
||||
@@ -97,7 +103,7 @@ unsafe fn ioctl(h: HANDLE, code: u32, input: &[u8], output: &mut [u8]) -> Result
|
||||
/// 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<String> {
|
||||
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() {
|
||||
@@ -133,6 +139,204 @@ unsafe fn resolve_gdi_name(target_id: u32) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// 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
|
||||
);
|
||||
}
|
||||
|
||||
let dm = DEVMODEW {
|
||||
dmSize: size_of::<DEVMODEW>() as u16,
|
||||
dmFields: DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL | DM_POSITION,
|
||||
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,
|
||||
// Make it the PRIMARY display: a blank *extended* IDD output isn't composited by the DWM,
|
||||
// so it produces no duplication frames. As primary it carries the shell/cursor → frames
|
||||
// flow (this is what Apollo does). Position is (0,0) via DM_POSITION (zeroed by default).
|
||||
CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_SET_PRIMARY,
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Detach every display except `keep_gdi_name`, leaving the SudoVDA virtual output as the ONLY
|
||||
/// display. This is the SudoVDA/Apollo "isolate the virtual display" move and the key to capturing
|
||||
/// the secure desktop: Windows renders the login / UAC (Winlogon) desktop on the physical/primary
|
||||
/// display and resets the topology when it switches there — with a physical monitor still attached
|
||||
/// (e.g. an LG TV), the login lands on it and our virtual output goes perpetually ACCESS_LOST. With
|
||||
/// the physical detached and the change PERSISTED to the registry, Winlogon reads "only the virtual
|
||||
/// is attached" and the secure desktop has nowhere to render but the output we capture.
|
||||
///
|
||||
/// Returns the displays we detached plus their saved modes so teardown can restore them.
|
||||
unsafe fn isolate_displays(keep_gdi_name: &str) -> Vec<(String, DEVMODEW)> {
|
||||
let mut saved = Vec::new();
|
||||
let mut idx = 0u32;
|
||||
loop {
|
||||
let mut dd = DISPLAY_DEVICEW {
|
||||
cb: size_of::<DISPLAY_DEVICEW>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
if !EnumDisplayDevicesW(PCWSTR::null(), idx, &mut dd, 0).as_bool() {
|
||||
break;
|
||||
}
|
||||
idx += 1;
|
||||
if (dd.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP).0 == 0 {
|
||||
continue; // not part of the desktop — nothing to detach
|
||||
}
|
||||
let name = String::from_utf16_lossy(&dd.DeviceName);
|
||||
let name = name.trim_end_matches('\u{0}').to_string();
|
||||
if name == keep_gdi_name {
|
||||
continue; // the virtual output we want to keep
|
||||
}
|
||||
// Save the current mode so the teardown can re-attach this display where it was.
|
||||
let mut cur = DEVMODEW {
|
||||
dmSize: size_of::<DEVMODEW>() as u16,
|
||||
..Default::default()
|
||||
};
|
||||
let wname: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
if EnumDisplaySettingsW(PCWSTR(wname.as_ptr()), ENUM_CURRENT_SETTINGS, &mut cur).as_bool() {
|
||||
saved.push((name.clone(), cur));
|
||||
}
|
||||
// A 0x0 mode removes the display from the desktop. NORESET batches; we commit once below.
|
||||
let off = DEVMODEW {
|
||||
dmSize: size_of::<DEVMODEW>() as u16,
|
||||
dmFields: DM_POSITION | DM_PELSWIDTH | DM_PELSHEIGHT,
|
||||
..Default::default()
|
||||
};
|
||||
let r = ChangeDisplaySettingsExW(
|
||||
PCWSTR(wname.as_ptr()),
|
||||
Some(&off),
|
||||
None,
|
||||
CDS_UPDATEREGISTRY | CDS_NORESET | CDS_GLOBAL,
|
||||
None,
|
||||
);
|
||||
tracing::info!("display isolate: detaching {name} (result={})", r.0);
|
||||
}
|
||||
if !saved.is_empty() {
|
||||
// Commit the batched detaches (NULL device + 0 flags applies the pending registry changes).
|
||||
let _ = ChangeDisplaySettingsExW(PCWSTR::null(), None, None, CDS_TYPE(0), None);
|
||||
tracing::info!(
|
||||
"display isolate: {} display(s) detached — only {keep_gdi_name} remains",
|
||||
saved.len()
|
||||
);
|
||||
}
|
||||
saved
|
||||
}
|
||||
|
||||
/// Re-attach the displays [`isolate_displays`] detached, restoring each to its saved mode. Called on
|
||||
/// teardown BEFORE the virtual output is removed, so there is always at least one display.
|
||||
unsafe fn restore_displays(saved: &[(String, DEVMODEW)]) {
|
||||
for (name, dm) in saved {
|
||||
let wname: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let _ = ChangeDisplaySettingsExW(
|
||||
PCWSTR(wname.as_ptr()),
|
||||
Some(dm),
|
||||
None,
|
||||
CDS_UPDATEREGISTRY | CDS_NORESET | CDS_GLOBAL,
|
||||
None,
|
||||
);
|
||||
}
|
||||
if !saved.is_empty() {
|
||||
let _ = ChangeDisplaySettingsExW(PCWSTR::null(), None, None, CDS_TYPE(0), None);
|
||||
tracing::info!("display isolate: restored {} display(s)", saved.len());
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn open_device() -> Result<HANDLE> {
|
||||
let hdev = SetupDiGetClassDevsW(
|
||||
Some(&SUVDA_INTERFACE),
|
||||
@@ -275,8 +479,16 @@ impl VirtualDisplay for SudoVdaDisplay {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut isolated: Vec<(String, DEVMODEW)> = Vec::new();
|
||||
match &gdi_name {
|
||||
Some(n) => tracing::info!("SudoVDA target {} -> {n}", ao.target_id),
|
||||
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);
|
||||
// Detach every other display so the secure desktop (Winlogon/UAC) renders here too.
|
||||
isolated = unsafe { isolate_displays(n) };
|
||||
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
|
||||
@@ -291,6 +503,9 @@ impl VirtualDisplay for SudoVdaDisplay {
|
||||
.map(|n| crate::capture::dxgi::WinCaptureTarget {
|
||||
adapter_luid: crate::capture::dxgi::pack_luid(ao.luid),
|
||||
gdi_name: n,
|
||||
// The SudoVDA 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: ao.target_id,
|
||||
}),
|
||||
keepalive: Box::new(SudoVdaKeepalive {
|
||||
device: device_raw,
|
||||
@@ -298,6 +513,7 @@ impl VirtualDisplay for SudoVdaDisplay {
|
||||
stop,
|
||||
pinger: Some(pinger),
|
||||
gdi_name,
|
||||
isolated,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -312,6 +528,8 @@ struct SudoVdaKeepalive {
|
||||
pinger: Option<JoinHandle<()>>,
|
||||
#[allow(dead_code)] // consumed by the Windows capture backend (not yet wired)
|
||||
gdi_name: Option<String>,
|
||||
/// Displays detached by [`isolate_displays`], restored here on teardown.
|
||||
isolated: Vec<(String, DEVMODEW)>,
|
||||
}
|
||||
|
||||
impl Drop for SudoVdaKeepalive {
|
||||
@@ -320,6 +538,9 @@ impl Drop for SudoVdaKeepalive {
|
||||
if let Some(j) = self.pinger.take() {
|
||||
let _ = j.join();
|
||||
}
|
||||
// Re-attach the physical display(s) we detached BEFORE removing the virtual output, so the
|
||||
// box is never left with zero displays.
|
||||
unsafe { restore_displays(&self.isolated) };
|
||||
let rp = RemoveParams { guid: self.guid };
|
||||
let rp_bytes = unsafe {
|
||||
std::slice::from_raw_parts(&rp as *const _ as *const u8, size_of::<RemoveParams>())
|
||||
|
||||
Reference in New Issue
Block a user