diff --git a/crates/punktfunk-host/src/capture/desktop_watch.rs b/crates/punktfunk-host/src/capture/desktop_watch.rs index c08a0a0..62590d7 100644 --- a/crates/punktfunk-host/src/capture/desktop_watch.rs +++ b/crates/punktfunk-host/src/capture/desktop_watch.rs @@ -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)); } diff --git a/crates/punktfunk-host/src/m3.rs b/crates/punktfunk-host/src/m3.rs index f0e0306..35c5139 100644 --- a/crates/punktfunk-host/src/m3.rs +++ b/crates/punktfunk-host/src/m3.rs @@ -2526,24 +2526,50 @@ fn virtual_stream_relay( "two-process: source switch" ); if secure { - if dda.is_none() { - match open_dda(&target, cur_mode.width, cur_mode.height, effective_hz) { - Ok(p) => dda = Some(p), - Err(e) => { - tracing::error!(error = %format!("{e:#}"), - "two-process: DDA open failed — secure desktop will freeze on last frame"); - } - } + // 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)); } - if let Some(d) = dda.as_mut() { - d.enc.request_keyframe(); + dda = None; // reopen so we capture the post-toggle (SDR) output + 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(); } 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. - while relay.try_recv().is_ok() {} - relay.request_keyframe(); + // 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 { diff --git a/crates/punktfunk-host/src/vdisplay/sudovda.rs b/crates/punktfunk-host/src/vdisplay/sudovda.rs index 869b721..157ed23 100644 --- a/crates/punktfunk-host/src/vdisplay/sudovda.rs +++ b/crates/punktfunk-host/src/vdisplay/sudovda.rs @@ -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 { 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::() 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