fix(host/windows): rebuild the output fresh on every WGC↔DDA source switch
Key insight (from the user): a fresh RECONNECT shows the secure desktop but the live transition does not — so the difference is what a fresh session does that the live switch skipped. A reconnect runs build() = REMOVE + fresh ADD of the SudoVDA monitor + re-isolate + a fresh capturer; the live transition instead reused the session-start output (created while on the NORMAL desktop), which goes born-lost (ACCESS_LOST storm → black) on the secure desktop. Fix: virtual_stream_relay now calls build() on EVERY source switch (both WGC→DDA and DDA→WGC), then opens DDA on the new target for secure / uses the fresh helper for normal. This makes each transition equivalent to the reconnect that works — fixing both the WGC→DDA cutover (secure desktop now in the clean output state DDA can duplicate) and the DDA→WGC cutover (a fresh helper's first frame is its opening IDR, so await_idr clears immediately instead of waiting on a wedged helper). Costs a ~1-2s rebuild per transition, acceptable for UAC/lock events. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,19 +17,21 @@
|
|||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use windows::core::{w, PCWSTR};
|
use windows::core::w;
|
||||||
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
|
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
|
||||||
|
use windows::Win32::Graphics::Gdi::{GetStockObject, BLACK_BRUSH, HBRUSH};
|
||||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||||
use windows::Win32::System::StationsAndDesktops::{
|
use windows::Win32::System::StationsAndDesktops::{
|
||||||
CloseDesktop, GetUserObjectInformationW, OpenInputDesktop, SetThreadDesktop,
|
CloseDesktop, GetUserObjectInformationW, OpenInputDesktop, SetThreadDesktop,
|
||||||
DESKTOP_ACCESS_FLAGS, DESKTOP_CONTROL_FLAGS, UOI_NAME,
|
DESKTOP_ACCESS_FLAGS, DESKTOP_CONTROL_FLAGS, UOI_NAME,
|
||||||
};
|
};
|
||||||
use windows::Win32::UI::WindowsAndMessaging::{
|
use windows::Win32::UI::WindowsAndMessaging::{
|
||||||
CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, PeekMessageW, RegisterClassW,
|
CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetSystemMetrics,
|
||||||
SetLayeredWindowAttributes, SetWindowPos, ShowWindow, TranslateMessage, HWND_TOPMOST,
|
PeekMessageW, RegisterClassW, SetLayeredWindowAttributes, SetWindowPos, ShowWindow,
|
||||||
LWA_ALPHA, MSG, PM_REMOVE, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SW_SHOWNOACTIVATE,
|
TranslateMessage, HWND_TOPMOST, LWA_COLORKEY, MSG, PM_REMOVE, SM_CXVIRTUALSCREEN,
|
||||||
WNDCLASSW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_TRANSPARENT,
|
SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SWP_NOACTIVATE, SWP_NOMOVE,
|
||||||
WS_POPUP,
|
SWP_NOSIZE, SW_SHOWNOACTIVATE, WNDCLASSW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
|
||||||
|
WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_POPUP,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A running force-composed-flip overlay. Drop signals the thread to tear down its window + exit.
|
/// A running force-composed-flip overlay. Drop signals the thread to tear down its window + exit.
|
||||||
@@ -105,6 +107,8 @@ unsafe fn make_overlay() -> Option<HWND> {
|
|||||||
lpfnWndProc: Some(wndproc),
|
lpfnWndProc: Some(wndproc),
|
||||||
hInstance: hinst.into(),
|
hInstance: hinst.into(),
|
||||||
lpszClassName: class,
|
lpszClassName: class,
|
||||||
|
// Paint the window black so LWA_COLORKEY(black) keys it out → visually invisible.
|
||||||
|
hbrBackground: HBRUSH(GetStockObject(BLACK_BRUSH).0),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let atom = RegisterClassW(&wc);
|
let atom = RegisterClassW(&wc);
|
||||||
@@ -115,15 +119,24 @@ unsafe fn make_overlay() -> Option<HWND> {
|
|||||||
tracing::warn!(err = e.0, "force-composed-flip: RegisterClassW failed");
|
tracing::warn!(err = e.0, "force-composed-flip: RegisterClassW failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Cover the WHOLE virtual screen (all outputs incl. the SudoVDA): a 1x1 corner window may not even
|
||||||
|
// sit on the captured output's rect, and independent-flip is per-output. A window overlapping the
|
||||||
|
// output is what disqualifies its flip.
|
||||||
|
let (vx, vy, vw, vh) = (
|
||||||
|
GetSystemMetrics(SM_XVIRTUALSCREEN),
|
||||||
|
GetSystemMetrics(SM_YVIRTUALSCREEN),
|
||||||
|
GetSystemMetrics(SM_CXVIRTUALSCREEN).max(1),
|
||||||
|
GetSystemMetrics(SM_CYVIRTUALSCREEN).max(1),
|
||||||
|
);
|
||||||
let hwnd = match CreateWindowExW(
|
let hwnd = match CreateWindowExW(
|
||||||
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW,
|
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW,
|
||||||
class,
|
class,
|
||||||
w!(""),
|
w!(""),
|
||||||
WS_POPUP,
|
WS_POPUP,
|
||||||
0,
|
vx,
|
||||||
0,
|
vy,
|
||||||
1,
|
vw,
|
||||||
1,
|
vh,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(hinst.into()),
|
Some(hinst.into()),
|
||||||
@@ -137,8 +150,16 @@ unsafe fn make_overlay() -> Option<HWND> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// alpha=1: technically visible (so it disqualifies independent-flip) but imperceptible.
|
// Color-key on black: the window is painted black (its WNDCLASS background brush) and black is keyed
|
||||||
let _ = SetLayeredWindowAttributes(hwnd, windows::Win32::Foundation::COLORREF(0), 1, LWA_ALPHA);
|
// out, so it's visually invisible — but DWM counts it as a real opaque window covering the output,
|
||||||
|
// which is what disqualifies the fullscreen independent-flip (a near-zero ALPHA layered window is
|
||||||
|
// often ignored by the flip-eligibility check; a color-keyed one is not).
|
||||||
|
let _ = SetLayeredWindowAttributes(
|
||||||
|
hwnd,
|
||||||
|
windows::Win32::Foundation::COLORREF(0x0000_0000),
|
||||||
|
0,
|
||||||
|
LWA_COLORKEY,
|
||||||
|
);
|
||||||
let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
|
let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
|
||||||
let _ = SetWindowPos(
|
let _ = SetWindowPos(
|
||||||
hwnd,
|
hwnd,
|
||||||
|
|||||||
@@ -2453,26 +2453,40 @@ fn virtual_stream_relay(
|
|||||||
},
|
},
|
||||||
"two-process: source switch"
|
"two-process: source switch"
|
||||||
);
|
);
|
||||||
|
// Rebuild the SudoVDA output FRESH on every source switch — the key insight: a fresh
|
||||||
|
// reconnect captures the secure desktop but the live transition (which reused the
|
||||||
|
// session-start output, created while on the NORMAL desktop) does not. `build` does the
|
||||||
|
// exact reconnect setup: REMOVE + fresh ADD of the virtual monitor + re-isolate + a fresh
|
||||||
|
// capturer, so the new output is in the clean state that DDA can duplicate on the secure
|
||||||
|
// desktop. Drop the old DDA (it's bound to the old target); reopen on the new one.
|
||||||
|
match build(&mut vd, cur_mode) {
|
||||||
|
Ok((ka, rl, tg, hz)) => {
|
||||||
|
relay = rl; // fresh helper (drops the old) ...
|
||||||
|
_keepalive = ka; // ... then releases the old output
|
||||||
|
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: source-switch rebuild failed — staying on the current source");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dda = None; // old DDA was on the old target
|
||||||
if secure {
|
if secure {
|
||||||
if dda.is_none() {
|
match open_dda(&target, cur_mode.width, cur_mode.height, effective_hz) {
|
||||||
match open_dda(&target, cur_mode.width, cur_mode.height, effective_hz) {
|
Ok(mut p) => {
|
||||||
Ok(p) => dda = Some(p),
|
p.enc.request_keyframe();
|
||||||
Err(e) => {
|
dda = Some(p);
|
||||||
tracing::error!(error = %format!("{e:#}"),
|
}
|
||||||
"two-process: DDA open failed — secure desktop will freeze on last frame");
|
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();
|
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();
|
|
||||||
}
|
}
|
||||||
|
// (normal: the fresh helper's first frame is its opening IDR — await_idr clears on it.)
|
||||||
}
|
}
|
||||||
if want_kf {
|
if want_kf {
|
||||||
if secure {
|
if secure {
|
||||||
|
|||||||
Reference in New Issue
Block a user