fix(host/windows): make SudoVDA the sole display via clean CCD (the IDD needs to be primary/composited)
Live result of the previous build: the MODE_CHANGE_IN_PROGRESS storm was FIXED
(0 occurrences) by dropping primary-promotion — but it exposed the regression
the review predicted: a non-primary EXTENDED SudoVDA is NOT DWM-composited on
this box, so DDA gets born-lost ACCESS_LOST (0x887a0026) + black frames. The
IDD genuinely must be the sole/primary/composited display here.
Apollo reaches that end state ('Virtual Desktop: 5120x1440', sole display) via
Windows AUTO-promoting the real WDDM display over the box's leftover 1024x768
basic display — but Windows does NOT auto-promote for us, leaving the IDD
extended. So make it sole explicitly, the clean way:
- create(): deactivate the other display(s) via the atomic CCD path
(isolate_displays_ccd) by DEFAULT (opt out with PUNKTFUNK_NO_ISOLATE). Drop
the legacy per-device GDI detach from the path (it misses iGPU-attached
monitors and churns; kept #[allow(dead_code)] for reference).
- set_active_mode(): CDS_UPDATEREGISTRY only — set the mode in place, NO
CDS_SET_PRIMARY / CDS_GLOBAL / DM_POSITION. A sole display is already primary,
so there's nothing to contest → no MODE_CHANGE storm (that storm came from
promoting primary at (0,0) WHILE the basic display was still active).
Net: sole SudoVDA → primary → composited → capturable, with no topology
contest. Keeps the prior MODE_CHANGE-as-transient handling + removed born-lost
escape as backstops.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ use windows::Win32::Devices::Display::{
|
|||||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
|
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
|
||||||
use windows::Win32::Graphics::Gdi::{
|
use windows::Win32::Graphics::Gdi::{
|
||||||
ChangeDisplaySettingsExW, EnumDisplayDevicesW, EnumDisplaySettingsW, CDS_GLOBAL, CDS_NORESET,
|
ChangeDisplaySettingsExW, EnumDisplayDevicesW, EnumDisplaySettingsW, CDS_GLOBAL, CDS_NORESET,
|
||||||
CDS_SET_PRIMARY, CDS_TEST, CDS_TYPE, CDS_UPDATEREGISTRY, DEVMODEW, DISPLAY_DEVICEW,
|
CDS_TEST, CDS_TYPE, CDS_UPDATEREGISTRY, DEVMODEW, DISPLAY_DEVICEW,
|
||||||
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, DISP_CHANGE_SUCCESSFUL, DM_BITSPERPEL, DM_DISPLAYFREQUENCY,
|
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,
|
DM_PELSHEIGHT, DM_PELSWIDTH, DM_POSITION, ENUM_CURRENT_SETTINGS, ENUM_DISPLAY_SETTINGS_MODE,
|
||||||
};
|
};
|
||||||
@@ -341,22 +341,15 @@ fn set_active_mode(gdi_name: &str, mode: Mode) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default (multi-display, Apollo-parity): set ONLY this output's mode in place. Promoting the IDD
|
// Set ONLY this output's mode in place (size/refresh/bpp; NO DM_POSITION). Do NOT promote it to
|
||||||
// to PRIMARY at the virtual-screen origin (DM_POSITION 0,0) + persisting it GLOBALly contests the
|
// PRIMARY here and do NOT write a GLOBAL topology: promoting the IDD to primary at (0,0) while the
|
||||||
// box's baseline display (e.g. a 1024x768 basic "WinDisc") so the desktop topology never reaches a
|
// box's leftover basic display is still active contests the topology and storms
|
||||||
// stable fixed point → a perpetual DXGI_ERROR_MODE_CHANGE_IN_PROGRESS storm (the freeze + audible
|
// DXGI_ERROR_MODE_CHANGE_IN_PROGRESS (measured live). The IDD is made the sole → primary →
|
||||||
// PnP chime measured live on the RTX4090+iGPU box). Apollo with an EMPTY config never promotes
|
// DWM-composited display by the CCD isolation in create() (which deactivates the other display
|
||||||
// primary and captures the same SudoVDA cleanly (verified live). So default to CDS_UPDATEREGISTRY
|
// first), so a sole display is already primary and needs no CDS_SET_PRIMARY here.
|
||||||
// only. ONLY when isolating to a SOLE display does the IDD genuinely need to be primary — a blank
|
|
||||||
// EXTENDED IDD may not be DWM-composited and would yield no duplication frames.
|
|
||||||
let isolating = std::env::var("PUNKTFUNK_ISOLATE_DISPLAYS").is_ok();
|
|
||||||
let mut dm_fields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL;
|
|
||||||
if isolating {
|
|
||||||
dm_fields |= DM_POSITION; // pin to origin, but only as the sole/primary display
|
|
||||||
}
|
|
||||||
let dm = DEVMODEW {
|
let dm = DEVMODEW {
|
||||||
dmSize: size_of::<DEVMODEW>() as u16,
|
dmSize: size_of::<DEVMODEW>() as u16,
|
||||||
dmFields: dm_fields,
|
dmFields: DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL,
|
||||||
dmBitsPerPel: 32,
|
dmBitsPerPel: 32,
|
||||||
dmPelsWidth: mode.width,
|
dmPelsWidth: mode.width,
|
||||||
dmPelsHeight: mode.height,
|
dmPelsHeight: mode.height,
|
||||||
@@ -376,16 +369,8 @@ fn set_active_mode(gdi_name: &str, mode: Mode) {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Default: CDS_UPDATEREGISTRY only — set this output's mode WITHOUT promoting it to primary or
|
|
||||||
// rewriting the global topology (which storms MODE_CHANGE_IN_PROGRESS). Promote to primary only when
|
|
||||||
// isolating to a sole display.
|
|
||||||
let apply_flags = if isolating {
|
|
||||||
CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_SET_PRIMARY
|
|
||||||
} else {
|
|
||||||
CDS_UPDATEREGISTRY
|
|
||||||
};
|
|
||||||
let apply = unsafe {
|
let apply = unsafe {
|
||||||
ChangeDisplaySettingsExW(PCWSTR(wname.as_ptr()), Some(&dm), None, apply_flags, None)
|
ChangeDisplaySettingsExW(PCWSTR(wname.as_ptr()), Some(&dm), None, CDS_UPDATEREGISTRY, None)
|
||||||
};
|
};
|
||||||
if apply == DISP_CHANGE_SUCCESSFUL {
|
if apply == DISP_CHANGE_SUCCESSFUL {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -414,6 +399,11 @@ fn set_active_mode(gdi_name: &str, mode: Mode) {
|
|||||||
/// is attached" and the secure desktop has nowhere to render but the output we capture.
|
/// 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.
|
/// Returns the displays we detached plus their saved modes so teardown can restore them.
|
||||||
|
///
|
||||||
|
/// Superseded by the atomic CCD [`isolate_displays_ccd`] (the legacy per-device GDI detach misses
|
||||||
|
/// iGPU-attached monitors on a hybrid box and churns the topology). Retained for reference / a
|
||||||
|
/// possible fallback.
|
||||||
|
#[allow(dead_code)]
|
||||||
unsafe fn isolate_displays(keep_gdi_name: &str) -> Vec<(String, DEVMODEW)> {
|
unsafe fn isolate_displays(keep_gdi_name: &str) -> Vec<(String, DEVMODEW)> {
|
||||||
let mut saved = Vec::new();
|
let mut saved = Vec::new();
|
||||||
let mut idx = 0u32;
|
let mut idx = 0u32;
|
||||||
@@ -766,32 +756,29 @@ impl VirtualDisplay for SudoVdaDisplay {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut isolated: Vec<(String, DEVMODEW)> = Vec::new();
|
let isolated: Vec<(String, DEVMODEW)> = Vec::new(); // legacy GDI detach unused (CCD path below)
|
||||||
let mut ccd_saved: Option<SavedConfig> = None;
|
let mut ccd_saved: Option<SavedConfig> = None;
|
||||||
match &gdi_name {
|
match &gdi_name {
|
||||||
Some(n) => {
|
Some(n) => {
|
||||||
tracing::info!("SudoVDA target {} -> {n}", ao.target_id);
|
tracing::info!("SudoVDA target {} -> {n}", ao.target_id);
|
||||||
// ADD only advertises the mode; force it active so DXGI captures the requested size.
|
// ADD only advertises the mode; force it active so DXGI captures the requested size.
|
||||||
set_active_mode(n, mode);
|
set_active_mode(n, mode);
|
||||||
// Display isolation (detach all other monitors → SudoVDA becomes the SOLE display) is
|
// Make the SudoVDA the SOLE active display (default). On this box an EXTENDED
|
||||||
// OPT-IN now. On a hybrid GPU box a SOLE active display goes into fullscreen
|
// (non-primary) IDD is NOT DWM-composited → Desktop Duplication gets a born-lost
|
||||||
// independent-flip / MPO, which Desktop Duplication CANNOT capture → the born-lost
|
// ACCESS_LOST (measured live: MODE_CHANGE storm fixed, but the extended IDD then
|
||||||
// ACCESS_LOST + MODE_CHANGE_IN_PROGRESS storm measured live on the RTX4090+iGPU box
|
// born-lost). Apollo reaches the same end state ("Virtual Desktop: WxH" — the IDD is the
|
||||||
// (hook verified-firing, DPI=2, overlay running — yet still frozen). Apollo stays rock
|
// whole desktop, hence primary + composited) via Windows AUTO-promoting the real WDDM
|
||||||
// solid on this exact box precisely because it KEEPS the physical monitor active and just
|
// display over the box's leftover 1024x768 basic display; Windows does NOT auto-promote
|
||||||
// arranges the virtual display alongside it (multi-display is DWM-composited, so the
|
// for us, so we deactivate the other display(s) explicitly via the clean atomic CCD path.
|
||||||
// output never independent-flips). So default to NOT isolating — match Apollo's topology.
|
// Deactivating FIRST means set_active_mode's primary-promotion has nothing to contest →
|
||||||
// Set PUNKTFUNK_ISOLATE_DISPLAYS=1 to force the old sole-display behaviour (a truly
|
// no MODE_CHANGE_IN_PROGRESS storm (that storm came from promoting primary WHILE the
|
||||||
// headless box with no attached monitor, where the secure/Winlogon desktop would
|
// basic display stayed active). Opt out with PUNKTFUNK_NO_ISOLATE=1 (a box with a real
|
||||||
// otherwise render on a detached physical output).
|
// second monitor to keep live). The legacy GDI detach is skipped — it misses
|
||||||
if std::env::var("PUNKTFUNK_ISOLATE_DISPLAYS").is_ok() {
|
// iGPU-attached monitors on a hybrid box and churns per-device; CCD is atomic.
|
||||||
isolated = unsafe { isolate_displays(n) };
|
if std::env::var("PUNKTFUNK_NO_ISOLATE").is_err() {
|
||||||
ccd_saved = unsafe { isolate_displays_ccd(ao.target_id) };
|
ccd_saved = unsafe { isolate_displays_ccd(ao.target_id) };
|
||||||
} else {
|
} else {
|
||||||
tracing::info!(
|
tracing::info!("display isolation skipped (PUNKTFUNK_NO_ISOLATE) — IDD stays extended");
|
||||||
"display isolation SKIPPED (Apollo-parity multi-display — avoids sole-display \
|
|
||||||
independent-flip; set PUNKTFUNK_ISOLATE_DISPLAYS=1 to force sole-display)"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens
|
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user