From c60a05dbe9d717e0984c669d5e47df25b3992106 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 16 Jun 2026 15:12:31 +0000 Subject: [PATCH] fix(host/windows): make SudoVDA the sole display via clean CCD (the IDD needs to be primary/composited) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/punktfunk-host/src/vdisplay/sudovda.rs | 71 ++++++++----------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/crates/punktfunk-host/src/vdisplay/sudovda.rs b/crates/punktfunk-host/src/vdisplay/sudovda.rs index f7d8520..66718e2 100644 --- a/crates/punktfunk-host/src/vdisplay/sudovda.rs +++ b/crates/punktfunk-host/src/vdisplay/sudovda.rs @@ -32,7 +32,7 @@ use windows::Win32::Devices::Display::{ use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID}; use windows::Win32::Graphics::Gdi::{ 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, 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 - // to PRIMARY at the virtual-screen origin (DM_POSITION 0,0) + persisting it GLOBALly contests the - // box's baseline display (e.g. a 1024x768 basic "WinDisc") so the desktop topology never reaches a - // stable fixed point → a perpetual DXGI_ERROR_MODE_CHANGE_IN_PROGRESS storm (the freeze + audible - // PnP chime measured live on the RTX4090+iGPU box). Apollo with an EMPTY config never promotes - // primary and captures the same SudoVDA cleanly (verified live). So default to CDS_UPDATEREGISTRY - // 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 - } + // Set ONLY this output's mode in place (size/refresh/bpp; NO DM_POSITION). Do NOT promote it to + // PRIMARY here and do NOT write a GLOBAL topology: promoting the IDD to primary at (0,0) while the + // box's leftover basic display is still active contests the topology and storms + // DXGI_ERROR_MODE_CHANGE_IN_PROGRESS (measured live). The IDD is made the sole → primary → + // DWM-composited display by the CCD isolation in create() (which deactivates the other display + // first), so a sole display is already primary and needs no CDS_SET_PRIMARY here. let dm = DEVMODEW { dmSize: size_of::() as u16, - dmFields: dm_fields, + dmFields: DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL, dmBitsPerPel: 32, dmPelsWidth: mode.width, dmPelsHeight: mode.height, @@ -376,16 +369,8 @@ fn set_active_mode(gdi_name: &str, mode: Mode) { ); 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 { - 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 { 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. /// /// 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)> { let mut saved = Vec::new(); let mut idx = 0u32; @@ -766,32 +756,29 @@ impl VirtualDisplay for SudoVdaDisplay { 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 = None; match &gdi_name { Some(n) => { tracing::info!("SudoVDA target {} -> {n}", ao.target_id); // ADD only advertises the mode; force it active so DXGI captures the requested size. set_active_mode(n, mode); - // Display isolation (detach all other monitors → SudoVDA becomes the SOLE display) is - // OPT-IN now. On a hybrid GPU box a SOLE active display goes into fullscreen - // independent-flip / MPO, which Desktop Duplication CANNOT capture → the born-lost - // ACCESS_LOST + MODE_CHANGE_IN_PROGRESS storm measured live on the RTX4090+iGPU box - // (hook verified-firing, DPI=2, overlay running — yet still frozen). Apollo stays rock - // solid on this exact box precisely because it KEEPS the physical monitor active and just - // arranges the virtual display alongside it (multi-display is DWM-composited, so the - // output never independent-flips). So default to NOT isolating — match Apollo's topology. - // Set PUNKTFUNK_ISOLATE_DISPLAYS=1 to force the old sole-display behaviour (a truly - // headless box with no attached monitor, where the secure/Winlogon desktop would - // otherwise render on a detached physical output). - if std::env::var("PUNKTFUNK_ISOLATE_DISPLAYS").is_ok() { - isolated = unsafe { isolate_displays(n) }; + // Make the SudoVDA the SOLE active display (default). On this box an EXTENDED + // (non-primary) IDD is NOT DWM-composited → Desktop Duplication gets a born-lost + // ACCESS_LOST (measured live: MODE_CHANGE storm fixed, but the extended IDD then + // born-lost). Apollo reaches the same end state ("Virtual Desktop: WxH" — the IDD is the + // whole desktop, hence primary + composited) via Windows AUTO-promoting the real WDDM + // display over the box's leftover 1024x768 basic display; Windows does NOT auto-promote + // for us, so we deactivate the other display(s) explicitly via the clean atomic CCD path. + // Deactivating FIRST means set_active_mode's primary-promotion has nothing to contest → + // no MODE_CHANGE_IN_PROGRESS storm (that storm came from promoting primary WHILE the + // basic display stayed active). Opt out with PUNKTFUNK_NO_ISOLATE=1 (a box with a real + // second monitor to keep live). The legacy GDI detach is skipped — it misses + // iGPU-attached monitors on a hybrid box and churns per-device; CCD is atomic. + if std::env::var("PUNKTFUNK_NO_ISOLATE").is_err() { ccd_saved = unsafe { isolate_displays_ccd(ao.target_id) }; } else { - tracing::info!( - "display isolation SKIPPED (Apollo-parity multi-display — avoids sole-display \ - independent-flip; set PUNKTFUNK_ISOLATE_DISPLAYS=1 to force sole-display)" - ); + tracing::info!("display isolation skipped (PUNKTFUNK_NO_ISOLATE) — IDD stays extended"); } thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens }