fix(windows-host): force EXTEND topology so a new IddCx display isn't cloned

A freshly-added IddCx virtual display lands in CLONE/duplicate mode when a
physical display is already active (a laptop panel, an attached monitor): the
cloned output shares that display's source, so the OS never commits a distinct
path for it, never calls ASSIGN_SWAPCHAIN, and capture sees no frames - the
session fails "not an active display path / needs a WDDM GPU to activate" and
tears down with 0 frames (seen live on an Intel-iGPU + NVIDIA-Optimus laptop).

force_extend_topology() applies the EXTEND preset (the programmatic Win+P
"Extend") right after ADD so the IDD comes up as its own active path; the
existing resolve_gdi_name -> set_active_mode -> isolate_displays_ccd bring-up
then proceeds. Idempotent / no-op on a sole-display (headless single-GPU) box,
so it's safe on the path that already worked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 14:33:15 +00:00
parent 5bf787eb2b
commit 8e87e617df
2 changed files with 35 additions and 2 deletions
@@ -27,7 +27,8 @@ use windows::Win32::Foundation::{HANDLE, LUID};
use super::{Mode, VirtualOutput};
use crate::win_display::{
isolate_displays_ccd, resolve_gdi_name, restore_displays_ccd, set_active_mode, SavedConfig,
force_extend_topology, isolate_displays_ccd, resolve_gdi_name, restore_displays_ccd,
set_active_mode, SavedConfig,
};
/// The per-backend REMOVE key the driver stamps on ADD and consumes on REMOVE. SudoVDA keys monitors by
@@ -326,6 +327,15 @@ impl VirtualDisplayManager {
}
});
// Windows defaults a new IddCx monitor into CLONE mode when a physical display is already
// active (a laptop panel, an attached monitor): the cloned IDD shares that display's source, so
// the OS never commits a distinct path for it and capture sees no frames. Force EXTEND first so
// the IDD comes up as its OWN active path; the resolve loop below then finds it. Idempotent /
// no-op on a sole-display box, so it's safe on the headless single-GPU path too.
// SAFETY: `force_extend_topology` only calls `SetDisplayConfig` (a CCD topology apply) with no
// borrowed caller memory; it runs under the manager `state` lock, the sole topology mutator.
unsafe { force_extend_topology() };
// Resolve the capture target. May be None on a GPU-less box (target added but not WDDM-activated);
// the capture backend re-resolves once a GPU is present.
let mut gdi_name = None;