feat(host/windows): SDR-while-secure — drop SudoVDA out of HDR on Winlogon so DDA captures it

When the DDA-on-secure path is enabled (PUNKTFUNK_SECURE_DDA=1), the mux now
toggles the SudoVDA's advanced-color (HDR) state via the CCD API
(sudovda::set_advanced_color → DisplayConfigSetDeviceInfo +
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE): on entering the secure (Winlogon)
desktop it disables HDR so the lock/UAC renders SDR/composed (no fullscreen
independent-flip → DDA can duplicate it instead of storming ACCESS_LOST/black),
opens DDA fresh on the now-SDR output; on returning to normal it re-enables HDR
and rebuilds the helper so WGC re-detects the restored colorspace.

Also debounce the DesktopWatcher (publish a Default↔Winlogon change only after it
is stable ~80ms) so transient flaps during the transition don't thrash the mux.

Default (no flag) is unchanged: WGC stays live through a lock, no DDA switch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 10:54:58 +00:00
parent be18797df8
commit 6ea52b0372
3 changed files with 111 additions and 22 deletions
@@ -45,24 +45,36 @@ impl DesktopWatcher {
let _ = std::thread::Builder::new() let _ = std::thread::Builder::new()
.name("desktop-watch".into()) .name("desktop-watch".into())
.spawn(move || { .spawn(move || {
let mut last = initial; // Debounce: only publish a change after the raw reading has been stable for several
// polls. The input desktop flaps Default↔Winlogon transiently during a lock/UAC
// transition; publishing every flap makes the capture mux thrash (rebuild storms).
const STABLE_POLLS: u32 = 4; // ~80ms
let mut published = initial;
let mut candidate = initial;
let mut stable = 0u32;
while !st.load(Ordering::Relaxed) { while !st.load(Ordering::Relaxed) {
let v = if unsafe { is_secure_desktop() } { let v = if unsafe { is_secure_desktop() } {
DESKTOP_SECURE DESKTOP_SECURE
} else { } else {
DESKTOP_NORMAL DESKTOP_NORMAL
}; };
s.store(v, Ordering::Release); if v == candidate {
if v != last { stable = stable.saturating_add(1);
} else {
candidate = v;
stable = 1;
}
if stable >= STABLE_POLLS && candidate != published {
s.store(candidate, Ordering::Release);
published = candidate;
tracing::info!( tracing::info!(
desktop = if v == DESKTOP_SECURE { desktop = if candidate == DESKTOP_SECURE {
"Winlogon(secure)" "Winlogon(secure)"
} else { } else {
"Default" "Default"
}, },
"input desktop changed" "input desktop changed (debounced)"
); );
last = v;
} }
std::thread::sleep(Duration::from_millis(20)); std::thread::sleep(Duration::from_millis(20));
} }
+40 -14
View File
@@ -2526,24 +2526,50 @@ fn virtual_stream_relay(
"two-process: source switch" "two-process: source switch"
); );
if secure { if secure {
if dda.is_none() { // SDR-while-secure: drop the SudoVDA out of HDR so the secure (Winlogon) desktop
match open_dda(&target, cur_mode.width, cur_mode.height, effective_hz) { // renders SDR/composed — the HDR fullscreen independent-flip is what made DDA storm
Ok(p) => dda = Some(p), // ACCESS_LOST (black). Give the reconfig a moment to settle, then (re)open DDA fresh on
Err(e) => { // the now-SDR output.
tracing::error!(error = %format!("{e:#}"), let toggled = unsafe {
"two-process: DDA open failed — secure desktop will freeze on last frame"); crate::vdisplay::sudovda::set_advanced_color(target.target_id, false)
} };
} if toggled {
std::thread::sleep(std::time::Duration::from_millis(250));
} }
if let Some(d) = dda.as_mut() { dda = None; // reopen so we capture the post-toggle (SDR) output
d.enc.request_keyframe(); match open_dda(&target, cur_mode.width, cur_mode.height, effective_hz) {
Ok(mut p) => {
p.enc.request_keyframe();
dda = Some(p);
}
Err(e) => {
tracing::error!(error = %format!("{e:#}"),
"two-process: DDA open failed — secure desktop will freeze on last frame");
}
} }
next = std::time::Instant::now(); next = std::time::Instant::now();
} else { } else {
// Returning to the helper: drain stale buffered AUs (encoded while we ignored it) and // Returning to the normal desktop: restore HDR on the SudoVDA (WGC captures it HDR), then
// force a fresh IDR; await_idr then skips the stale deltas until that IDR arrives. // rebuild the helper fresh so its WGC re-detects the restored colorspace, and resume.
while relay.try_recv().is_ok() {} unsafe {
relay.request_keyframe(); crate::vdisplay::sudovda::set_advanced_color(target.target_id, true);
}
dda = None; // free the secure DDA encoder
match build(&mut vd, cur_mode) {
Ok((ka, rl, tg, hz)) => {
relay = rl;
_keepalive = ka;
target = tg;
effective_hz = hz;
interval = std::time::Duration::from_secs_f64(1.0 / hz.max(1) as f64);
}
Err(e) => {
tracing::error!(error = %format!("{e:#}"),
"two-process: helper rebuild on secure-exit failed");
while relay.try_recv().is_ok() {}
relay.request_keyframe();
}
}
} }
} }
if want_kf { if want_kf {
+53 -2
View File
@@ -22,8 +22,10 @@ use windows::Win32::Devices::DeviceAndDriverInstallation::{
SP_DEVICE_INTERFACE_DATA, SP_DEVICE_INTERFACE_DETAIL_DATA_W, SP_DEVICE_INTERFACE_DATA, SP_DEVICE_INTERFACE_DETAIL_DATA_W,
}; };
use windows::Win32::Devices::Display::{ use windows::Win32::Devices::Display::{
DisplayConfigGetDeviceInfo, GetDisplayConfigBufferSizes, QueryDisplayConfig, DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes,
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO, QueryDisplayConfig, 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, DISPLAYCONFIG_SOURCE_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS,
}; };
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID}; use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
@@ -216,6 +218,55 @@ pub(crate) unsafe fn resolve_gdi_name(target_id: u32) -> Option<String> {
None 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(&mut 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 /// 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 /// 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 /// ACTIVE mode (what DXGI Desktop Duplication captures) must be set explicitly. CDS_TEST first so a