From 8e87e617dfac4ac8cb314d907b99f5ed1f666820 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 26 Jun 2026 14:33:15 +0000 Subject: [PATCH] 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) --- .../src/vdisplay/windows/manager.rs | 12 ++++++++- .../punktfunk-host/src/windows/win_display.rs | 25 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/punktfunk-host/src/vdisplay/windows/manager.rs b/crates/punktfunk-host/src/vdisplay/windows/manager.rs index 0d579f0..b3dfe39 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/manager.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/manager.rs @@ -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; diff --git a/crates/punktfunk-host/src/windows/win_display.rs b/crates/punktfunk-host/src/windows/win_display.rs index ac34952..7b4ae93 100644 --- a/crates/punktfunk-host/src/windows/win_display.rs +++ b/crates/punktfunk-host/src/windows/win_display.rs @@ -21,7 +21,7 @@ use windows::Win32::Devices::Display::{ DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY, SDC_FORCE_MODE_ENUMERATION, - SDC_SAVE_TO_DATABASE, SDC_USE_SUPPLIED_DISPLAY_CONFIG, + SDC_SAVE_TO_DATABASE, SDC_TOPOLOGY_EXTEND, SDC_USE_SUPPLIED_DISPLAY_CONFIG, }; use windows::Win32::Graphics::Gdi::{ ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW, @@ -31,6 +31,29 @@ use windows::Win32::Graphics::Gdi::{ use crate::vdisplay::Mode; +/// Force the desktop into EXTEND topology - the programmatic equivalent of the Win+P / DisplaySwitch +/// "Extend" shortcut. Windows defaults a FRESHLY-ADDED monitor into CLONE/duplicate mode when a +/// physical display is already active (e.g. a laptop panel): a cloned IddCx output shares the panel's +/// source, so the OS never commits a distinct path for it, never calls ASSIGN_SWAPCHAIN, and capture +/// sees no frames (`resolve_gdi_name` stays `None` and the session fails "not an active display path"). +/// Applying the EXTEND preset across the live set of connected displays makes the new IddCx monitor its +/// OWN active path, so the rest of bring-up (`resolve_gdi_name` -> `set_active_mode` -> +/// `isolate_displays_ccd`) proceeds. Best-effort + idempotent: a no-op on a single-display (already +/// sole/extended) box, so it is safe to call unconditionally. `rc == 0` is success. +pub(crate) unsafe fn force_extend_topology() { + // A topology flag with no supplied path/mode arrays tells the OS to recompute + apply that preset + // for the currently-connected displays (the same code path DisplaySwitch.exe drives). + let rc = SetDisplayConfig(None, None, SDC_APPLY | SDC_TOPOLOGY_EXTEND); + if rc == 0 { + tracing::info!( + "display topology forced to EXTEND (a new IddCx monitor would otherwise be CLONED onto the \ + existing panel -> no distinct source -> no frames)" + ); + } else { + tracing::warn!("display force-EXTEND topology: SetDisplayConfig rc={rc:#x}"); + } +} + /// Resolve the `\\.\DisplayN` GDI name for a SudoVDA target id via the CCD API. Returns `None` /// until the OS activates the target into the desktop topology (needs a real WDDM GPU; on a /// GPU-less box this stays `None` even though ADD succeeded).