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:
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user