feat(vdisplay/windows): topology=primary — keep physicals active, virtual primary
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) <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,7 @@ use windows::Win32::System::Threading::{
|
|||||||
use super::{Mode, VirtualOutput};
|
use super::{Mode, VirtualOutput};
|
||||||
use crate::win_display::{
|
use crate::win_display::{
|
||||||
force_extend_topology, isolate_displays_ccd, resolve_gdi_name, restore_displays_ccd,
|
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
|
/// The per-backend REMOVE key the driver stamps on ADD and consumes on REMOVE. SudoVDA keys monitors by
|
||||||
@@ -633,20 +633,29 @@ impl VirtualDisplayManager {
|
|||||||
tracing::info!(backend = self.driver.name(), "target {} -> {n}", added.target_id);
|
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.
|
// ADD only advertises the mode; force it active so DXGI captures the requested size.
|
||||||
set_active_mode(n, mode);
|
set_active_mode(n, mode);
|
||||||
// Make the virtual display the SOLE active output (default): an EXTENDED (non-primary) IDD
|
// Apply the display-management topology (Stage 2). `Exclusive` (default) deactivates the
|
||||||
// isn't DWM-composited on this box → Desktop Duplication born-losts. Deactivating the other
|
// other display(s) so the IDD is the SOLE composited primary — an EXTENDED (non-primary)
|
||||||
// display(s) first via the atomic CCD path promotes the IDD to a composited primary with no
|
// IDD isn't DWM-composited on this box → Desktop Duplication born-losts. `Primary` keeps the
|
||||||
// MODE_CHANGE storm. Opt out with PUNKTFUNK_NO_ISOLATE=1.
|
// physical display(s) ACTIVE and makes the IDD primary (repositioned to origin). `Extend`
|
||||||
if should_isolate() {
|
// leaves it a plain extension. Both isolate + primary go through the atomic CCD path (no
|
||||||
// SAFETY: `isolate_displays_ccd` is `unsafe` for its CCD topology FFI; it takes a
|
// MODE_CHANGE storm). Opt out (extend) with PUNKTFUNK_NO_ISOLATE=1 / the console policy.
|
||||||
// `Copy` `u32` by value and returns an owned `SavedConfig` snapshot (no borrowed
|
use crate::vdisplay::policy::Topology;
|
||||||
// memory crosses). It runs under the `state` lock, the sole mutator of the 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) };
|
ccd_saved = unsafe { isolate_displays_ccd(added.target_id) };
|
||||||
} else {
|
}
|
||||||
|
Topology::Primary => {
|
||||||
|
ccd_saved = unsafe { set_virtual_primary_ccd(added.target_id) };
|
||||||
|
}
|
||||||
|
Topology::Extend | Topology::Auto => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"display isolation skipped (topology=extend / PUNKTFUNK_NO_ISOLATE) — IDD stays extended"
|
"display topology=extend — IDD stays extended (no isolate / no primary)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens
|
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens
|
||||||
}
|
}
|
||||||
None => tracing::warn!(
|
None => tracing::warn!(
|
||||||
@@ -997,17 +1006,22 @@ fn linger_ms() -> u64 {
|
|||||||
.unwrap_or(10_000)
|
.unwrap_or(10_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Should a freshly-created monitor isolate the desktop to itself (disable the other displays)? The
|
/// The effective display topology for a freshly-created monitor (never `Auto`): the console policy's
|
||||||
/// console policy's effective topology wins when configured — `Extend` leaves the IDD extended,
|
/// [`effective_topology`](crate::vdisplay::effective_topology) when configured, else the legacy
|
||||||
/// `Exclusive`/`Primary` isolate (Stage 0 treats `Primary` as `Exclusive`); otherwise the legacy
|
/// `PUNKTFUNK_NO_ISOLATE` env knob (`Extend`) / `Exclusive` (today's default). `Extend` leaves the IDD
|
||||||
/// `PUNKTFUNK_NO_ISOLATE` env knob (unset ⇒ isolate, matching today's default).
|
/// extended; `Primary` makes it primary while keeping the physical(s) active; `Exclusive` disables the
|
||||||
fn should_isolate() -> bool {
|
/// physical(s) so the IDD is the sole composited desktop.
|
||||||
|
fn topology_action() -> crate::vdisplay::policy::Topology {
|
||||||
use crate::vdisplay::policy::Topology;
|
use crate::vdisplay::policy::Topology;
|
||||||
if let Some(eff) = crate::vdisplay::policy::prefs().configured_effective() {
|
if crate::vdisplay::policy::prefs()
|
||||||
return !matches!(
|
.configured_effective()
|
||||||
crate::vdisplay::resolve_topology(eff.topology),
|
.is_some()
|
||||||
Topology::Extend
|
{
|
||||||
);
|
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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ use windows::Win32::Devices::Display::{
|
|||||||
DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes,
|
DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes,
|
||||||
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO,
|
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO,
|
||||||
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE,
|
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,
|
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME,
|
||||||
QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY, SDC_FORCE_MODE_ENUMERATION,
|
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,
|
SDC_SAVE_TO_DATABASE, SDC_TOPOLOGY_EXTEND, SDC_USE_SUPPLIED_DISPLAY_CONFIG,
|
||||||
};
|
};
|
||||||
|
use windows::Win32::Foundation::POINTL;
|
||||||
use windows::Win32::Graphics::Gdi::{
|
use windows::Win32::Graphics::Gdi::{
|
||||||
ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW,
|
ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW,
|
||||||
DISP_CHANGE_SUCCESSFUL, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, DM_PELSWIDTH,
|
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<SavedCo
|
|||||||
Some(saved)
|
Some(saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **Primary (topology=primary)** — make the virtual output the PRIMARY display while KEEPING every
|
||||||
|
/// other display ACTIVE (unlike [`isolate_displays_ccd`], which deactivates them). Windows treats the
|
||||||
|
/// display whose source sits at the desktop origin `(0,0)` as primary, so we move the virtual's source
|
||||||
|
/// to `(0,0)` and shift every other active source to its right — all paths stay active. Done as ONE
|
||||||
|
/// atomic CCD `SetDisplayConfig` (NOT GDI `CDS_SET_PRIMARY`, which storms
|
||||||
|
/// `DXGI_ERROR_MODE_CHANGE_IN_PROGRESS` when another display is live — see [`set_active_mode`]).
|
||||||
|
/// Returns the original config to restore on teardown.
|
||||||
|
pub(crate) unsafe fn set_virtual_primary_ccd(keep_target_id: u32) -> Option<SavedConfig> {
|
||||||
|
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
|
/// Restore the topology saved by [`isolate_displays_ccd`] (teardown, before the virtual output is
|
||||||
/// removed), re-activating the displays we deactivated.
|
/// removed), re-activating the displays we deactivated.
|
||||||
// pub(crate) so vdisplay::pf_vdisplay can reuse this backend-neutral CCD restore helper.
|
// pub(crate) so vdisplay::pf_vdisplay can reuse this backend-neutral CCD restore helper.
|
||||||
|
|||||||
Reference in New Issue
Block a user