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:
@@ -45,24 +45,36 @@ impl DesktopWatcher {
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("desktop-watch".into())
|
||||
.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) {
|
||||
let v = if unsafe { is_secure_desktop() } {
|
||||
DESKTOP_SECURE
|
||||
} else {
|
||||
DESKTOP_NORMAL
|
||||
};
|
||||
s.store(v, Ordering::Release);
|
||||
if v != last {
|
||||
if v == candidate {
|
||||
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!(
|
||||
desktop = if v == DESKTOP_SECURE {
|
||||
desktop = if candidate == DESKTOP_SECURE {
|
||||
"Winlogon(secure)"
|
||||
} else {
|
||||
"Default"
|
||||
},
|
||||
"input desktop changed"
|
||||
"input desktop changed (debounced)"
|
||||
);
|
||||
last = v;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
|
||||
@@ -2526,26 +2526,52 @@ fn virtual_stream_relay(
|
||||
"two-process: source switch"
|
||||
);
|
||||
if secure {
|
||||
if dda.is_none() {
|
||||
// SDR-while-secure: drop the SudoVDA out of HDR so the secure (Winlogon) desktop
|
||||
// renders SDR/composed — the HDR fullscreen independent-flip is what made DDA storm
|
||||
// ACCESS_LOST (black). Give the reconfig a moment to settle, then (re)open DDA fresh on
|
||||
// the now-SDR output.
|
||||
let toggled = unsafe {
|
||||
crate::vdisplay::sudovda::set_advanced_color(target.target_id, false)
|
||||
};
|
||||
if toggled {
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
}
|
||||
dda = None; // reopen so we capture the post-toggle (SDR) output
|
||||
match open_dda(&target, cur_mode.width, cur_mode.height, effective_hz) {
|
||||
Ok(p) => dda = Some(p),
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(d) = dda.as_mut() {
|
||||
d.enc.request_keyframe();
|
||||
}
|
||||
next = std::time::Instant::now();
|
||||
} else {
|
||||
// Returning to the helper: drain stale buffered AUs (encoded while we ignored it) and
|
||||
// force a fresh IDR; await_idr then skips the stale deltas until that IDR arrives.
|
||||
// Returning to the normal desktop: restore HDR on the SudoVDA (WGC captures it HDR), then
|
||||
// rebuild the helper fresh so its WGC re-detects the restored colorspace, and resume.
|
||||
unsafe {
|
||||
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 secure {
|
||||
if let Some(d) = dda.as_mut() {
|
||||
|
||||
@@ -22,8 +22,10 @@ use windows::Win32::Devices::DeviceAndDriverInstallation::{
|
||||
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,
|
||||
DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes,
|
||||
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,
|
||||
};
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
|
||||
@@ -216,6 +218,55 @@ pub(crate) unsafe fn resolve_gdi_name(target_id: u32) -> Option<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(&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
|
||||
/// 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
|
||||
|
||||
Reference in New Issue
Block a user