From eda7cac78ea78f6f2a9111da05e1eed7e35b9d7b Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 5 Jul 2026 09:17:41 +0000 Subject: [PATCH] =?UTF-8?q?feat(vdisplay/windows):=20topology=3Dprimary=20?= =?UTF-8?q?=E2=80=94=20keep=20physicals=20active,=20virtual=20primary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the deferred Windows primary-only CCD (Stage 2). set_virtual_primary_ccd repositions the virtual output's source to (0,0) = primary and shifts the physical display(s) to its right, ALL kept active — one atomic CCD SetDisplayConfig (not GDI CDS_SET_PRIMARY, which storms MODE_CHANGE_IN_PROGRESS with another display live). The manager's should_isolate() becomes topology_action() (3-way): extend (skip), primary (set_virtual_primary_ccd), exclusive (isolate_displays_ccd). Restore-on-teardown covers both. Validates the user's two scenarios on a physical-monitor .173. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/vdisplay/windows/manager.rs | 64 ++++++++------ .../punktfunk-host/src/windows/win_display.rs | 84 ++++++++++++++++++- 2 files changed, 122 insertions(+), 26 deletions(-) diff --git a/crates/punktfunk-host/src/vdisplay/windows/manager.rs b/crates/punktfunk-host/src/vdisplay/windows/manager.rs index 23e6198..e60aa20 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/manager.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/manager.rs @@ -34,7 +34,7 @@ use windows::Win32::System::Threading::{ use super::{Mode, VirtualOutput}; use crate::win_display::{ force_extend_topology, isolate_displays_ccd, resolve_gdi_name, restore_displays_ccd, - set_active_mode, SavedConfig, + set_active_mode, set_virtual_primary_ccd, SavedConfig, }; /// The per-backend REMOVE key the driver stamps on ADD and consumes on REMOVE. SudoVDA keys monitors by @@ -633,19 +633,28 @@ impl VirtualDisplayManager { tracing::info!(backend = self.driver.name(), "target {} -> {n}", added.target_id); // ADD only advertises the mode; force it active so DXGI captures the requested size. set_active_mode(n, mode); - // Make the virtual display the SOLE active output (default): an EXTENDED (non-primary) IDD - // isn't DWM-composited on this box → Desktop Duplication born-losts. Deactivating the other - // display(s) first via the atomic CCD path promotes the IDD to a composited primary with no - // MODE_CHANGE storm. Opt out with PUNKTFUNK_NO_ISOLATE=1. - if should_isolate() { - // SAFETY: `isolate_displays_ccd` is `unsafe` for its CCD topology FFI; it takes a - // `Copy` `u32` by value and returns an owned `SavedConfig` snapshot (no borrowed - // memory crosses). It runs under the `state` lock, the sole mutator of the topology. - ccd_saved = unsafe { isolate_displays_ccd(added.target_id) }; - } else { - tracing::info!( - "display isolation skipped (topology=extend / PUNKTFUNK_NO_ISOLATE) — IDD stays extended" - ); + // Apply the display-management topology (Stage 2). `Exclusive` (default) deactivates the + // other display(s) so the IDD is the SOLE composited primary — an EXTENDED (non-primary) + // IDD isn't DWM-composited on this box → Desktop Duplication born-losts. `Primary` keeps the + // physical display(s) ACTIVE and makes the IDD primary (repositioned to origin). `Extend` + // leaves it a plain extension. Both isolate + primary go through the atomic CCD path (no + // MODE_CHANGE storm). Opt out (extend) with PUNKTFUNK_NO_ISOLATE=1 / the console policy. + use crate::vdisplay::policy::Topology; + match topology_action() { + // SAFETY (both arms): the CCD helper is `unsafe` for its topology FFI; it takes a + // `Copy` `u32` by value and returns an owned `SavedConfig` (no borrowed memory crosses), + // and runs under the `state` lock, the sole mutator of the topology. + Topology::Exclusive => { + ccd_saved = unsafe { isolate_displays_ccd(added.target_id) }; + } + Topology::Primary => { + ccd_saved = unsafe { set_virtual_primary_ccd(added.target_id) }; + } + Topology::Extend | Topology::Auto => { + tracing::info!( + "display topology=extend — IDD stays extended (no isolate / no primary)" + ); + } } thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens } @@ -997,17 +1006,22 @@ fn linger_ms() -> u64 { .unwrap_or(10_000) } -/// Should a freshly-created monitor isolate the desktop to itself (disable the other displays)? The -/// console policy's effective topology wins when configured — `Extend` leaves the IDD extended, -/// `Exclusive`/`Primary` isolate (Stage 0 treats `Primary` as `Exclusive`); otherwise the legacy -/// `PUNKTFUNK_NO_ISOLATE` env knob (unset ⇒ isolate, matching today's default). -fn should_isolate() -> bool { +/// The effective display topology for a freshly-created monitor (never `Auto`): the console policy's +/// [`effective_topology`](crate::vdisplay::effective_topology) when configured, else the legacy +/// `PUNKTFUNK_NO_ISOLATE` env knob (`Extend`) / `Exclusive` (today's default). `Extend` leaves the IDD +/// extended; `Primary` makes it primary while keeping the physical(s) active; `Exclusive` disables the +/// physical(s) so the IDD is the sole composited desktop. +fn topology_action() -> crate::vdisplay::policy::Topology { use crate::vdisplay::policy::Topology; - if let Some(eff) = crate::vdisplay::policy::prefs().configured_effective() { - return !matches!( - crate::vdisplay::resolve_topology(eff.topology), - Topology::Extend - ); + if crate::vdisplay::policy::prefs() + .configured_effective() + .is_some() + { + return crate::vdisplay::effective_topology(); + } + if std::env::var("PUNKTFUNK_NO_ISOLATE").is_ok() { + Topology::Extend + } else { + Topology::Exclusive } - std::env::var("PUNKTFUNK_NO_ISOLATE").is_err() } diff --git a/crates/punktfunk-host/src/windows/win_display.rs b/crates/punktfunk-host/src/windows/win_display.rs index 7b4ae93..2e6560f 100644 --- a/crates/punktfunk-host/src/windows/win_display.rs +++ b/crates/punktfunk-host/src/windows/win_display.rs @@ -18,11 +18,13 @@ use windows::Win32::Devices::Display::{ DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes, QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE, - DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO, + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_MODE_INFO, + DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE, 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_TOPOLOGY_EXTEND, SDC_USE_SUPPLIED_DISPLAY_CONFIG, }; +use windows::Win32::Foundation::POINTL; use windows::Win32::Graphics::Gdi::{ ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW, DISP_CHANGE_SUCCESSFUL, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, DM_PELSWIDTH, @@ -431,6 +433,86 @@ pub(crate) unsafe fn isolate_displays_ccd(keep_target_id: u32) -> Option Option { + let mut np = 0u32; + let mut nm = 0u32; + if GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut np, &mut nm).is_err() { + return None; + } + let mut paths = vec![DISPLAYCONFIG_PATH_INFO::default(); np as usize]; + let mut modes = vec![DISPLAYCONFIG_MODE_INFO::default(); nm as usize]; + if QueryDisplayConfig( + QDC_ONLY_ACTIVE_PATHS, + &mut np, + paths.as_mut_ptr(), + &mut nm, + modes.as_mut_ptr(), + None, + ) + .is_err() + { + return None; + } + paths.truncate(np as usize); + modes.truncate(nm as usize); + let saved = (paths.clone(), modes.clone()); + + // The virtual output's source width, to shift the physicals past it. + let virt_width = paths.iter().find_map(|p| { + if p.targetInfo.id != keep_target_id { + return None; + } + let idx = p.sourceInfo.modeInfoIdx as usize; + let m = modes.get(idx)?; + (m.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE) + .then(|| m.Anonymous.sourceMode.width as i32) + })?; + + // Reposition each active path's SOURCE once: the virtual to (0,0) (= primary), the rest shifted + // right by the virtual's width (kept active, no overlap). Dedup source-mode indices (a cloned + // group would share one). + let mut done = std::collections::HashSet::new(); + for p in paths.iter() { + let idx = p.sourceInfo.modeInfoIdx as usize; + if !done.insert(idx) { + continue; + } + let Some(m) = modes.get_mut(idx) else { + continue; + }; + if m.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE { + continue; + } + if p.targetInfo.id == keep_target_id { + m.Anonymous.sourceMode.position = POINTL { x: 0, y: 0 }; + } else { + m.Anonymous.sourceMode.position.x += virt_width; + } + } + + let rc = SetDisplayConfig( + Some(paths.as_slice()), + Some(modes.as_slice()), + SDC_APPLY + | SDC_USE_SUPPLIED_DISPLAY_CONFIG + | SDC_ALLOW_CHANGES + | SDC_FORCE_MODE_ENUMERATION, + ); + if rc == 0 { + tracing::info!("display primary (CCD): virtual target {keep_target_id} set PRIMARY at (0,0); physical display(s) kept ACTIVE, shifted right by {virt_width}px"); + } else { + tracing::warn!("display primary (CCD): SetDisplayConfig failed rc={rc:#x} (virtual {keep_target_id} primary, physicals kept)"); + } + Some(saved) +} + /// Restore the topology saved by [`isolate_displays_ccd`] (teardown, before the virtual output is /// removed), re-activating the displays we deactivated. // pub(crate) so vdisplay::pf_vdisplay can reuse this backend-neutral CCD restore helper.