refactor(host/W6.2): extract the Windows display-topology cluster into the pf-win-display leaf crate
windows/{win_display,monitor_devnode,display_events}.rs move into
crates/pf-win-display: the CCD/GDI path-activation + mode-set + HDR
advanced-colour + source-rect helpers, the PnP monitor-devnode enable/disable
lever, and the WM_DISPLAYCHANGE / device-arrival watch. The coming pf-capture
crate's IDD-push capturer consumes all three; the host's pf-vdisplay backend
consumes win_display + monitor_devnode. A leaf lets both depend on them as a
PEER instead of the capturer reaching back into the orchestrator (plan §W6).
win_display's one external tie (crate::vdisplay::Mode) becomes the underlying
punktfunk_core::Mode; the cluster is otherwise self-contained (pf-paths for the
state file, serde_json for it, windows). pub(crate) items bump to pub at the
boundary; win_display carries a module-level allow(missing_safety_doc) to keep
the pre-carve behavior (the FFI helpers were pub(crate) unsafe fn with prose
safety docs — the lint only fires once they're pub, and this is an internal
publish=false leaf). The host imports the three modules at its crate root, so
every crate::{win_display,monitor_devnode,display_events}::* path is unchanged.
Verified: Linux clippy -D warnings (leaf empty + host
nvenc,vulkan-encode,pyrowave --all-targets); Windows clippy -D warnings
(pf-win-display --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,9 +32,6 @@ mod crash;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/ddc.rs"]
|
||||
mod ddc;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/display_events.rs"]
|
||||
mod display_events;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "linux/drm_sync.rs"]
|
||||
mod drm_sync;
|
||||
@@ -56,9 +53,6 @@ mod library;
|
||||
mod log_capture;
|
||||
mod mgmt;
|
||||
mod mgmt_token;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/monitor_devnode.rs"]
|
||||
mod monitor_devnode;
|
||||
mod native;
|
||||
mod native_pairing;
|
||||
mod pipeline;
|
||||
@@ -76,9 +70,11 @@ mod vdisplay;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/win_adapter.rs"]
|
||||
mod win_adapter;
|
||||
// The Windows display-topology cluster (CCD/GDI mode-set, PnP monitor devnodes, the display-change
|
||||
// watch) lives in the `pf-win-display` leaf crate (plan §W6); import the modules at the crate root
|
||||
// so every existing `crate::{win_display,monitor_devnode,display_events}::*` path stays valid.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/win_display.rs"]
|
||||
mod win_display;
|
||||
use pf_win_display::{display_events, monitor_devnode, win_display};
|
||||
// The zero-copy GPU plumbing lives in the `pf-zerocopy` leaf crate (plan §W6); this shim keeps
|
||||
// every existing `crate::zerocopy::*` path valid. `drm_fourcc` consumes the frame vocabulary, so
|
||||
// it sits with `capture` and is re-exported here for its old callers.
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
//! OS display-event listener — the attribution sensor for the periodic-stutter disturbance class.
|
||||
//!
|
||||
//! The capture-stall watch ([`crate::capture::windows::idd_push`]) can SAY "DWM stopped composing
|
||||
//! on a stable period", but not WHY. Field evidence (Apollo's Stuttering Clinic, Apollo #384,
|
||||
//! Tom's HW "stutter from disabled-but-connected monitors") points at a connected-but-idle sink
|
||||
//! (standby TV/monitor, active HDMI cable, KVM/AVR) re-probing the link every few seconds; the GPU
|
||||
//! driver services each probe below the topology layer, and on some boxes Windows additionally
|
||||
//! tears down + re-arrives the monitor's devnode each time. This module timestamps everything
|
||||
//! Windows lets user mode see of that reaction so the stall log can name the disturbance instead
|
||||
//! of guessing:
|
||||
//!
|
||||
//! - `WM_DEVICECHANGE` + `RegisterDeviceNotificationW(GUID_DEVINTERFACE_MONITOR)`: monitor device
|
||||
//! interface arrival/removal — fires on devnode churn even when the final topology is unchanged
|
||||
//! (the "reaction cascade" class `pnp_disable_monitors` suppresses), with the interface path
|
||||
//! naming WHICH monitor pulsed.
|
||||
//! - `DBT_DEVNODES_CHANGED`: the broadcast catch-all for PnP tree churn (no payload).
|
||||
//! - `WM_DISPLAYCHANGE`: an actual mode/topology commit reached the desktop (this one does NOT
|
||||
//! fire for a pure probe with no mode delta — its absence is itself a signal).
|
||||
//!
|
||||
//! A pure driver-internal probe (EDID/DDC read, DP link retrain) emits NONE of these — that
|
||||
//! absence, paired with metronomic stalls, is what discriminates "driver services a standby sink
|
||||
//! below the OS" from "Windows re-enumerates the monitor". Kernel-precise attribution (DxgKrnl ETW
|
||||
//! event 272 `DxgkCbIndicateChildStatus`) is a possible follow-up; this listener is the cheap
|
||||
//! always-on first stage.
|
||||
//!
|
||||
//! The listener thread also keeps a cached CCD target inventory (refreshed on each event + a slow
|
||||
//! timer), so the capture thread can name connected-but-inactive external displays — the prime
|
||||
//! suspects — without ever touching the CCD lock itself (the display-config lock is exactly what
|
||||
//! stalls during churn; the capture thread must never block on it).
|
||||
|
||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Mutex, Once, OnceLock};
|
||||
use std::time::Instant;
|
||||
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_MONITOR;
|
||||
use windows::Win32::Foundation::{HANDLE, HWND, LPARAM, LRESULT, WPARAM};
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, RegisterClassW,
|
||||
RegisterDeviceNotificationW, SetTimer, DBT_DEVICEARRIVAL, DBT_DEVICEREMOVECOMPLETE,
|
||||
DBT_DEVNODES_CHANGED, DBT_DEVTYP_DEVICEINTERFACE, DEVICE_NOTIFY_WINDOW_HANDLE,
|
||||
DEV_BROADCAST_DEVICEINTERFACE_W, DEV_BROADCAST_HDR, MSG, WINDOW_EX_STYLE, WM_DEVICECHANGE,
|
||||
WM_DISPLAYCHANGE, WM_TIMER, WNDCLASSW, WS_OVERLAPPED,
|
||||
};
|
||||
|
||||
/// One OS-visible display event, timestamped at receipt.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DisplayEvent {
|
||||
pub at: Instant,
|
||||
pub kind: DisplayEventKind,
|
||||
/// Monitor device instance id for arrival/removal (e.g. `DISPLAY\GSM83CD\...`), else `None`.
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum DisplayEventKind {
|
||||
/// A monitor device interface ARRIVED — a sink (re)connected as Windows sees it.
|
||||
MonitorArrival,
|
||||
/// A monitor device interface was REMOVED — a sink dropped as Windows sees it.
|
||||
MonitorRemoval,
|
||||
/// PnP device-tree churn (broadcast, no payload) — re-enumeration passed through.
|
||||
DevNodesChanged,
|
||||
/// A mode/topology commit reached the desktop (resolution/layout actually changed).
|
||||
DisplayChange,
|
||||
}
|
||||
|
||||
impl DisplayEventKind {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::MonitorArrival => "monitor-arrival",
|
||||
Self::MonitorRemoval => "monitor-removal",
|
||||
Self::DevNodesChanged => "devnodes-changed",
|
||||
Self::DisplayChange => "display-change",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
/// Recent events, oldest-first, capped at [`RING_CAP`].
|
||||
events: VecDeque<DisplayEvent>,
|
||||
/// Cached CCD target inventory (see module docs for why the cache exists).
|
||||
inventory: Vec<crate::win_display::TargetInventory>,
|
||||
}
|
||||
|
||||
/// Ring depth: at the observed worst case (a probe cycle every ~2 s, ≤4 events per cycle) this
|
||||
/// holds well over a minute of history — the stall correlator only ever asks about the last gap.
|
||||
const RING_CAP: usize = 128;
|
||||
|
||||
fn state() -> &'static Mutex<State> {
|
||||
static STATE: OnceLock<Mutex<State>> = OnceLock::new();
|
||||
STATE.get_or_init(|| {
|
||||
Mutex::new(State {
|
||||
events: VecDeque::with_capacity(RING_CAP),
|
||||
inventory: Vec::new(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Start the listener thread (idempotent). Degraded-not-fatal: if window/registration creation
|
||||
/// fails the ring just stays empty — the stall log then reports "listener unavailable" naturally
|
||||
/// via empty summaries, and streaming is unaffected.
|
||||
pub(crate) fn spawn_once() {
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
let spawned = std::thread::Builder::new()
|
||||
.name("pf-display-events".into())
|
||||
.spawn(pump);
|
||||
if let Err(e) = spawned {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
"display-event listener thread failed to spawn — stall logs won't carry OS event attribution"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Events with `from <= at <= to`, oldest-first.
|
||||
pub(crate) fn events_between(from: Instant, to: Instant) -> Vec<DisplayEvent> {
|
||||
let st = state().lock().unwrap();
|
||||
st.events
|
||||
.iter()
|
||||
.filter(|e| e.at >= from && e.at <= to)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Compact one-line summary for log fields: `"monitor-removal x2 (DISPLAY\GSM83CD\...),
|
||||
/// devnodes-changed x1"`; `"none"` when empty.
|
||||
pub(crate) fn summarize(events: &[DisplayEvent]) -> String {
|
||||
if events.is_empty() {
|
||||
return "none".into();
|
||||
}
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for kind in [
|
||||
DisplayEventKind::MonitorArrival,
|
||||
DisplayEventKind::MonitorRemoval,
|
||||
DisplayEventKind::DevNodesChanged,
|
||||
DisplayEventKind::DisplayChange,
|
||||
] {
|
||||
let hits: Vec<&DisplayEvent> = events.iter().filter(|e| e.kind == kind).collect();
|
||||
if hits.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let detail = hits
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|e| e.detail.as_deref())
|
||||
.map(|d| format!(" ({d})"))
|
||||
.unwrap_or_default();
|
||||
out.push(format!("{} x{}{}", kind.label(), hits.len(), detail));
|
||||
}
|
||||
out.join(", ")
|
||||
}
|
||||
|
||||
/// The prime suspects for link-probe disturbances, from the cached inventory: external physical
|
||||
/// displays that are CONNECTED but not part of the desktop (standby TV / input-switched monitor).
|
||||
/// Rendered as `"<friendly> (<connector>)"`. Never blocks on the CCD lock.
|
||||
pub(crate) fn connected_inactive_externals() -> Vec<String> {
|
||||
let st = state().lock().unwrap();
|
||||
st.inventory
|
||||
.iter()
|
||||
.filter(|t| t.external_physical && !t.active)
|
||||
.map(|t| format!("{} ({})", t.friendly, t.tech))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn push_event(kind: DisplayEventKind, detail: Option<String>) {
|
||||
let mut st = state().lock().unwrap();
|
||||
if st.events.len() >= RING_CAP {
|
||||
st.events.pop_front();
|
||||
}
|
||||
st.events.push_back(DisplayEvent {
|
||||
at: Instant::now(),
|
||||
kind,
|
||||
detail,
|
||||
});
|
||||
}
|
||||
|
||||
fn refresh_inventory() {
|
||||
// SAFETY: `target_inventory` only runs read-only CCD queries over locals (see its SAFETY doc);
|
||||
// called from the listener thread, never the capture thread.
|
||||
let inv = unsafe { crate::win_display::target_inventory() };
|
||||
if !inv.is_empty() {
|
||||
state().lock().unwrap().inventory = inv;
|
||||
}
|
||||
}
|
||||
|
||||
/// Inventory refresh timer: 15 s keeps the "connected-but-inactive" suspect list fresh enough for
|
||||
/// a warn that rate-limits to 30 s, without measurable CCD traffic.
|
||||
const INVENTORY_TIMER_MS: u32 = 15_000;
|
||||
|
||||
/// `DBT_DEVNODES_CHANGED` arrives as `wParam` on `WM_DEVICECHANGE` without a registration; the
|
||||
/// interface arrivals/removals need the `RegisterDeviceNotificationW` below.
|
||||
unsafe extern "system" fn wnd_proc(
|
||||
hwnd: HWND,
|
||||
msg: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
match msg {
|
||||
WM_DISPLAYCHANGE => {
|
||||
// lParam packs the new primary resolution — worth carrying: it distinguishes a real
|
||||
// mode change from a same-mode re-commit when reading a field log.
|
||||
let (w, h) = (
|
||||
(lparam.0 & 0xffff) as u32,
|
||||
((lparam.0 >> 16) & 0xffff) as u32,
|
||||
);
|
||||
push_event(DisplayEventKind::DisplayChange, Some(format!("{w}x{h}")));
|
||||
refresh_inventory();
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DEVICECHANGE => {
|
||||
let event = wparam.0 as u32;
|
||||
if event == DBT_DEVNODES_CHANGED {
|
||||
push_event(DisplayEventKind::DevNodesChanged, None);
|
||||
refresh_inventory();
|
||||
} else if event == DBT_DEVICEARRIVAL || event == DBT_DEVICEREMOVECOMPLETE {
|
||||
let kind = if event == DBT_DEVICEARRIVAL {
|
||||
DisplayEventKind::MonitorArrival
|
||||
} else {
|
||||
DisplayEventKind::MonitorRemoval
|
||||
};
|
||||
// SAFETY: for these two events lParam is documented to point at a
|
||||
// DEV_BROADCAST_HDR (or be 0 — checked). We only read the header fields, and only
|
||||
// reinterpret as DEV_BROADCAST_DEVICEINTERFACE_W after checking dbch_devicetype,
|
||||
// reading at most dbch_size bytes — the size the sender declared.
|
||||
let detail = unsafe {
|
||||
let hdr = lparam.0 as *const DEV_BROADCAST_HDR;
|
||||
if hdr.is_null() || (*hdr).dbch_devicetype != DBT_DEVTYP_DEVICEINTERFACE {
|
||||
None
|
||||
} else {
|
||||
let di = hdr as *const DEV_BROADCAST_DEVICEINTERFACE_W;
|
||||
let head = std::mem::offset_of!(DEV_BROADCAST_DEVICEINTERFACE_W, dbcc_name);
|
||||
let bytes = ((*hdr).dbch_size as usize).saturating_sub(head);
|
||||
let name = std::slice::from_raw_parts(
|
||||
std::ptr::addr_of!((*di).dbcc_name).cast::<u16>(),
|
||||
(bytes / 2).min(512),
|
||||
);
|
||||
let end = name.iter().position(|&c| c == 0).unwrap_or(name.len());
|
||||
crate::monitor_devnode::instance_id_from_interface_path(
|
||||
&String::from_utf16_lossy(&name[..end]),
|
||||
)
|
||||
}
|
||||
};
|
||||
push_event(kind, detail);
|
||||
refresh_inventory();
|
||||
}
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_TIMER => {
|
||||
refresh_inventory();
|
||||
LRESULT(0)
|
||||
}
|
||||
// SAFETY: default handling for everything else — the standard wndproc tail call.
|
||||
_ => unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) },
|
||||
}
|
||||
}
|
||||
|
||||
/// The listener thread: a hidden TOP-LEVEL window (message-ONLY windows receive neither
|
||||
/// `WM_DISPLAYCHANGE` nor broadcast `WM_DEVICECHANGE` — the classic pitfall) + a device-interface
|
||||
/// registration for monitors + a blocking message pump. Runs for the process lifetime.
|
||||
fn pump() {
|
||||
refresh_inventory(); // baseline before any event arrives
|
||||
|
||||
let class: Vec<u16> = "pf-display-events\0".encode_utf16().collect();
|
||||
// SAFETY: straight-line Win32 window bring-up on this thread. `class` outlives every use of
|
||||
// its pointer (it lives to the end of the fn, the pump loops forever). All handles passed on
|
||||
// are the ones the preceding calls returned; failure of any step returns out of the thread
|
||||
// (degraded mode, see `spawn_once`). The DEV_BROADCAST_DEVICEINTERFACE_W filter is a fully
|
||||
// initialised local read synchronously by RegisterDeviceNotificationW.
|
||||
unsafe {
|
||||
let Ok(hinstance) = GetModuleHandleW(None) else {
|
||||
tracing::warn!(
|
||||
"display-event listener: GetModuleHandleW failed — no OS event attribution"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let wc = WNDCLASSW {
|
||||
lpfnWndProc: Some(wnd_proc),
|
||||
hInstance: hinstance.into(),
|
||||
lpszClassName: PCWSTR(class.as_ptr()),
|
||||
..Default::default()
|
||||
};
|
||||
if RegisterClassW(&wc) == 0 {
|
||||
tracing::warn!(
|
||||
"display-event listener: RegisterClassW failed — no OS event attribution"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let hwnd = match CreateWindowExW(
|
||||
WINDOW_EX_STYLE(0),
|
||||
PCWSTR(class.as_ptr()),
|
||||
PCWSTR(class.as_ptr()),
|
||||
WS_OVERLAPPED, // hidden: never shown, never gets focus — exists only to receive broadcasts
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
Some(wc.hInstance),
|
||||
None,
|
||||
) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "display-event listener: CreateWindowExW failed — no OS event attribution");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let filter = DEV_BROADCAST_DEVICEINTERFACE_W {
|
||||
dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
|
||||
dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
|
||||
dbcc_classguid: GUID_DEVINTERFACE_MONITOR,
|
||||
..Default::default()
|
||||
};
|
||||
if let Err(e) = RegisterDeviceNotificationW(
|
||||
HANDLE(hwnd.0),
|
||||
std::ptr::from_ref(&filter).cast(),
|
||||
DEVICE_NOTIFY_WINDOW_HANDLE,
|
||||
) {
|
||||
// DBT_DEVNODES_CHANGED and WM_DISPLAYCHANGE still arrive — partial attribution.
|
||||
tracing::warn!(error = %e, "display-event listener: monitor-interface registration failed — arrival/removal detail unavailable");
|
||||
}
|
||||
SetTimer(Some(hwnd), 1, INVENTORY_TIMER_MS, None);
|
||||
tracing::debug!(
|
||||
"display-event listener running (monitor hot-plug + display-change attribution)"
|
||||
);
|
||||
let mut msg = MSG::default();
|
||||
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
//! PnP monitor-devnode disable — the EXPERIMENTAL `pnp_disable_monitors` display-policy axis.
|
||||
//!
|
||||
//! An `Exclusive` isolate removes the physical monitors from the desktop TOPOLOGY (CCD), but their
|
||||
//! PnP device nodes stay live — so a standby monitor/TV that periodically wakes its connection
|
||||
//! (auto input scan, Instant-On HPD cycling, DP link events) still triggers the full Windows
|
||||
//! reaction each time: PnP arrival/removal, CCD re-evaluation, DWM invalidation. Field evidence
|
||||
//! (Apollo #368's Device-Manager refresh at every hitch; our own reporter's TV where unplugging the
|
||||
//! cable removes a metronomic ~4 s double-jolt) says this reaction cascade is the expensive part.
|
||||
//!
|
||||
//! This module disables physical monitors' devnodes for the stream's duration
|
||||
//! (`CM_Disable_DevNode` with `CM_DISABLE_PERSIST`, so a devnode that hot-plug re-arrives STAYS
|
||||
//! disabled — that persistence is the whole point) and re-enables them at teardown before the CCD
|
||||
//! restore. Two precise selectors, never "every monitor but ours" (co-installed third-party
|
||||
//! virtual displays are untouched): [`disable_for_deactivated`] — monitors on targets the
|
||||
//! `Exclusive` isolate actually deactivated, mapped CCD target → monitor device interface path
|
||||
//! (`DISPLAYCONFIG_TARGET_DEVICE_NAME`) → PnP instance id; and [`disable_connected_inactive`] —
|
||||
//! external physical monitors that are connected but not part of the desktop in ANY topology (the
|
||||
//! standby-TV case: never active, so the first selector can't see it, yet it probes the link all
|
||||
//! the same — the exact class in the field reports above).
|
||||
//!
|
||||
//! Crash safety: instance ids are journaled to `<config>/pnp-disabled-monitors.json` BEFORE the
|
||||
//! disable and cleared after a successful re-enable; [`startup_recover`] re-enables leftovers when
|
||||
//! the host starts after a crash/kill. Worst case (host dies AND never restarts) the monitor stays
|
||||
//! disabled until the user re-enables it in Device Manager — the web-console help text says so.
|
||||
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Devices::DeviceAndDriverInstallation::{
|
||||
CM_Disable_DevNode, CM_Enable_DevNode, CM_Locate_DevNodeW, CM_DISABLE_PERSIST,
|
||||
CM_LOCATE_DEVNODE_NORMAL, CM_LOCATE_DEVNODE_PHANTOM, CR_SUCCESS,
|
||||
};
|
||||
use windows::Win32::Devices::Display::{
|
||||
DisplayConfigGetDeviceInfo, DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME,
|
||||
DISPLAYCONFIG_TARGET_DEVICE_NAME,
|
||||
};
|
||||
use windows::Win32::Foundation::LUID;
|
||||
|
||||
/// The crash-recovery journal: PnP instance ids we disabled and have not yet re-enabled.
|
||||
fn journal_path() -> std::path::PathBuf {
|
||||
pf_paths::config_dir().join("pnp-disabled-monitors.json")
|
||||
}
|
||||
|
||||
fn read_journal() -> Vec<String> {
|
||||
match std::fs::read(journal_path()) {
|
||||
Ok(bytes) => serde_json::from_slice(&bytes).unwrap_or_default(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist `ids` as the outstanding-disable set (union semantics handled by the callers). Failure
|
||||
/// is logged, not fatal — the feature degrades to "no crash journal", not "no feature".
|
||||
fn write_journal(ids: &[String]) {
|
||||
let path = journal_path();
|
||||
if ids.is_empty() {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
return;
|
||||
}
|
||||
if let Some(dir) = path.parent() {
|
||||
let _ = pf_paths::create_private_dir(dir);
|
||||
}
|
||||
if let Err(e) = std::fs::write(&path, serde_json::to_vec_pretty(&ids).unwrap_or_default()) {
|
||||
tracing::warn!(error = %e, "PnP-disable: could not write the crash-recovery journal");
|
||||
}
|
||||
}
|
||||
|
||||
/// `\\?\DISPLAY#GSM83CD#5&367fb4cb&0&UID4352#{guid}` → `DISPLAY\GSM83CD\5&367fb4cb&0&UID4352`.
|
||||
/// The standard device-interface-path → instance-id transform: strip the `\\?\` prefix and the
|
||||
/// trailing `#{interface-class-guid}`, then `#` separators become `\`.
|
||||
// pub(crate): `display_events` applies the same transform to DBT_DEVICEARRIVAL interface paths.
|
||||
pub(crate) fn instance_id_from_interface_path(path: &str) -> Option<String> {
|
||||
let rest = path.strip_prefix(r"\\?\")?;
|
||||
let cut = rest.rfind("#{")?;
|
||||
Some(rest[..cut].replace('#', "\\"))
|
||||
}
|
||||
|
||||
fn utf16z(buf: &[u16]) -> String {
|
||||
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||
String::from_utf16_lossy(&buf[..len])
|
||||
}
|
||||
|
||||
/// Resolve a CCD target to its monitor's (instance id, friendly name).
|
||||
fn monitor_instance(adapter: LUID, target_id: u32) -> Option<(String, String)> {
|
||||
let mut req = DISPLAYCONFIG_TARGET_DEVICE_NAME::default();
|
||||
req.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
|
||||
req.header.size = std::mem::size_of::<DISPLAYCONFIG_TARGET_DEVICE_NAME>() as u32;
|
||||
req.header.adapterId = adapter;
|
||||
req.header.id = target_id;
|
||||
// SAFETY: `req` is a properly-sized DISPLAYCONFIG_TARGET_DEVICE_NAME local whose header
|
||||
// (type/size/adapterId/id) is fully initialised; the API writes only within the struct.
|
||||
let rc = unsafe { DisplayConfigGetDeviceInfo(&mut req.header) };
|
||||
if rc != 0 {
|
||||
return None;
|
||||
}
|
||||
let id = instance_id_from_interface_path(&utf16z(&req.monitorDevicePath))?;
|
||||
Some((id, utf16z(&req.monitorFriendlyDeviceName)))
|
||||
}
|
||||
|
||||
/// Apply enable/disable to one PnP instance id. Returns whether the config action succeeded.
|
||||
fn set_devnode(id: &str, disable: bool) -> bool {
|
||||
let wide: Vec<u16> = id.encode_utf16().chain([0]).collect();
|
||||
let mut devinst = 0u32;
|
||||
// A disabled (or currently-departed) devnode may not be in the live tree — locate PHANTOM for
|
||||
// the enable path so recovery still finds it; the disable path requires a present device.
|
||||
let flags = if disable {
|
||||
CM_LOCATE_DEVNODE_NORMAL
|
||||
} else {
|
||||
CM_LOCATE_DEVNODE_PHANTOM
|
||||
};
|
||||
// SAFETY: `wide` is a live NUL-terminated UTF-16 instance id outliving the call; `devinst` is
|
||||
// a valid out-param.
|
||||
let cr = unsafe { CM_Locate_DevNodeW(&mut devinst, PCWSTR(wide.as_ptr()), flags) };
|
||||
if cr != CR_SUCCESS {
|
||||
tracing::warn!(id, cr = cr.0, "PnP-disable: CM_Locate_DevNodeW failed");
|
||||
return false;
|
||||
}
|
||||
// SAFETY: `devinst` is the devnode the locate above resolved; plain value flags.
|
||||
let cr = unsafe {
|
||||
if disable {
|
||||
// PERSIST is the point: the standby monitor's hot-plug RE-ARRIVAL must stay disabled,
|
||||
// otherwise every wake event recreates an enabled devnode and the churn is back.
|
||||
CM_Disable_DevNode(devinst, CM_DISABLE_PERSIST)
|
||||
} else {
|
||||
CM_Enable_DevNode(devinst, 0)
|
||||
}
|
||||
};
|
||||
if cr != CR_SUCCESS {
|
||||
tracing::warn!(
|
||||
id,
|
||||
cr = cr.0,
|
||||
disable,
|
||||
"PnP-disable: CM_{}_DevNode failed",
|
||||
if disable { "Disable" } else { "Enable" }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Disable the devnodes of every monitor the `Exclusive` isolate deactivated: all ACTIVE paths in
|
||||
/// the pre-isolate snapshot whose target is not `keep_target_id`. Journals BEFORE disabling.
|
||||
/// Returns the instance ids to re-enable at teardown.
|
||||
pub fn disable_for_deactivated(
|
||||
saved: &crate::win_display::SavedConfig,
|
||||
keep_target_id: u32,
|
||||
) -> Vec<String> {
|
||||
const DISPLAYCONFIG_PATH_ACTIVE: u32 = 0x0000_0001;
|
||||
let mut targets: Vec<(String, String)> = Vec::new();
|
||||
for p in &saved.0 {
|
||||
if p.targetInfo.id == keep_target_id || p.flags & DISPLAYCONFIG_PATH_ACTIVE == 0 {
|
||||
continue;
|
||||
}
|
||||
match monitor_instance(p.targetInfo.adapterId, p.targetInfo.id) {
|
||||
Some(hit) => {
|
||||
if !targets.contains(&hit) {
|
||||
targets.push(hit);
|
||||
}
|
||||
}
|
||||
None => tracing::debug!(
|
||||
target_id = p.targetInfo.id,
|
||||
"PnP-disable: no monitor device name for deactivated target — skipping"
|
||||
),
|
||||
}
|
||||
}
|
||||
journal_and_disable(targets)
|
||||
}
|
||||
|
||||
/// Disable the devnodes of every EXTERNAL PHYSICAL monitor that is connected but NOT part of the
|
||||
/// desktop — regardless of who deactivated it. This is the standby-TV case the deactivated-set
|
||||
/// selection above structurally misses: a TV that was never active has no pre-isolate active path,
|
||||
/// yet its standby wake events (auto input scan, Instant-On HPD cycling) drive the same Windows
|
||||
/// reaction cascade. Selection stays allowlist-precise via
|
||||
/// [`crate::win_display::TargetInventory::external_physical`] — internal panels and
|
||||
/// indirect/virtual targets (ours or third-party) can never be picked, and `keep_target_ids`
|
||||
/// (the managed virtual set) is excluded belt-and-braces. Runs AFTER the topology action so the
|
||||
/// active flags it reads are the settled ones. Journals like [`disable_for_deactivated`]; the
|
||||
/// caller merges the returned ids into the same teardown list.
|
||||
pub fn disable_connected_inactive(keep_target_ids: &[u32]) -> Vec<String> {
|
||||
// SAFETY: `target_inventory` only runs read-only CCD queries over local buffers (see its
|
||||
// docs); no borrowed memory crosses the call.
|
||||
let inventory = unsafe { crate::win_display::target_inventory() };
|
||||
let mut targets: Vec<(String, String)> = Vec::new();
|
||||
for t in &inventory {
|
||||
if t.active || !t.external_physical || keep_target_ids.contains(&t.target_id) {
|
||||
continue;
|
||||
}
|
||||
let Some(id) = instance_id_from_interface_path(&t.monitor_device_path) else {
|
||||
continue;
|
||||
};
|
||||
let hit = (id, format!("{} ({})", t.friendly, t.tech));
|
||||
if !targets.contains(&hit) {
|
||||
targets.push(hit);
|
||||
}
|
||||
}
|
||||
journal_and_disable(targets)
|
||||
}
|
||||
|
||||
/// Shared tail of the two selectors: crash-journal FIRST, then disable, returning what actually
|
||||
/// got disabled (the teardown re-enable list).
|
||||
fn journal_and_disable(targets: Vec<(String, String)>) -> Vec<String> {
|
||||
if targets.is_empty() {
|
||||
tracing::debug!("PnP-disable: no physical monitor devnodes to disable");
|
||||
return Vec::new();
|
||||
}
|
||||
// Journal FIRST (union with any outstanding ids), so a crash between here and the disable
|
||||
// over-recovers instead of leaking a disabled monitor.
|
||||
let mut journal = read_journal();
|
||||
for (id, _) in &targets {
|
||||
if !journal.contains(id) {
|
||||
journal.push(id.clone());
|
||||
}
|
||||
}
|
||||
write_journal(&journal);
|
||||
let mut disabled = Vec::new();
|
||||
for (id, name) in targets {
|
||||
if set_devnode(&id, true) {
|
||||
tracing::info!(id, monitor = name, "PnP-disable: monitor devnode disabled");
|
||||
disabled.push(id);
|
||||
}
|
||||
}
|
||||
disabled
|
||||
}
|
||||
|
||||
/// Re-enable `ids` (teardown / recovery) and clear them from the journal.
|
||||
pub fn enable_instances(ids: &[String]) -> u32 {
|
||||
let mut ok = 0u32;
|
||||
for id in ids {
|
||||
if set_devnode(id, false) {
|
||||
tracing::info!(id, "PnP-disable: monitor devnode re-enabled");
|
||||
ok += 1;
|
||||
}
|
||||
}
|
||||
let journal: Vec<String> = read_journal()
|
||||
.into_iter()
|
||||
.filter(|j| !ids.contains(j))
|
||||
.collect();
|
||||
write_journal(&journal);
|
||||
ok
|
||||
}
|
||||
|
||||
/// Host-startup crash recovery: re-enable any devnodes a previous host disabled but never
|
||||
/// restored (crash, kill, power loss). Call once, early in `serve`.
|
||||
pub fn startup_recover() {
|
||||
let leftovers = read_journal();
|
||||
if leftovers.is_empty() {
|
||||
return;
|
||||
}
|
||||
tracing::warn!(
|
||||
count = leftovers.len(),
|
||||
"PnP-disable: found monitor devnodes a previous host left disabled — re-enabling"
|
||||
);
|
||||
enable_instances(&leftovers);
|
||||
}
|
||||
@@ -1,890 +0,0 @@
|
||||
//! Backend-neutral Windows display utilities — the CCD (QueryDisplayConfig) + GDI helpers shared by the
|
||||
//! virtual-display backends (pf-vdisplay, SudoVDA) and the capturers (IDD-push, WGC, DDA): GDI-name
|
||||
//! resolution, advanced-color (HDR) get/set, active-mode set, and CCD topology isolate/restore.
|
||||
//!
|
||||
//! These are display-utility, NOT SudoVDA-specific (a pf-vdisplay monitor's target_id is a real OS target
|
||||
//! id, so they operate identically), so they live here rather than in the SudoVDA backend — breaking the
|
||||
//! circular reach-in where the capturers + the pf-vdisplay backend reached into `vdisplay::sudovda` for
|
||||
//! them, which let the SudoVDA backend be dropped without losing them (audit §9 / Goal 2 — done). The
|
||||
//! plan's `windows/display_ccd.rs`. Extracted verbatim from the former SudoVDA backend before its removal.
|
||||
|
||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use std::mem::size_of;
|
||||
|
||||
use windows::core::PCWSTR;
|
||||
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_GET_TARGET_NAME,
|
||||
DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO,
|
||||
DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE,
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPONENT_VIDEO,
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPOSITE_VIDEO,
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED,
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DVI,
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HD15, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI,
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_LVDS,
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDI, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDTVDONGLE,
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SVIDEO, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED,
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EXTERNAL, DISPLAYCONFIG_PATH_INFO,
|
||||
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME,
|
||||
DISPLAYCONFIG_TARGET_DEVICE_NAME, DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY, QDC_ALL_PATHS,
|
||||
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,
|
||||
ENUM_CURRENT_SETTINGS, ENUM_DISPLAY_SETTINGS_MODE,
|
||||
};
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// EXPLICITLY activate `target_id` into its own display path — the last-resort fallback when neither
|
||||
/// the OS auto-activate nor the EXTEND topology preset lights a freshly-ADDed IDD target. Observed on
|
||||
/// a lid-closed laptop (field report, Intel iGPU): the clamshell lid policy makes Windows skip the
|
||||
/// new-monitor auto-activation AND the `SDC_TOPOLOGY_EXTEND` preset returns success without ever
|
||||
/// committing a path for the IDD, so the target sits connected-but-inactive for the whole retry
|
||||
/// budget (RDP/Parsec don't need a new console display path, which is why they still work there).
|
||||
///
|
||||
/// This is the supplied-config apply Windows' own display Settings uses to turn a monitor on: query
|
||||
/// ALL paths, keep every currently-active path verbatim, and append the target's inactive path with a
|
||||
/// source not already driving another display — mode indices invalidated so `SDC_ALLOW_CHANGES` lets
|
||||
/// the OS pick modes for the new path. Returns `true` when the apply reports success; the caller
|
||||
/// still re-polls [`resolve_gdi_name`] to confirm the path actually committed.
|
||||
pub(crate) unsafe fn activate_target_path(target_id: u32) -> bool {
|
||||
let mut np = 0u32;
|
||||
let mut nm = 0u32;
|
||||
if GetDisplayConfigBufferSizes(QDC_ALL_PATHS, &mut np, &mut nm).is_err() {
|
||||
return false;
|
||||
}
|
||||
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_ALL_PATHS,
|
||||
&mut np,
|
||||
paths.as_mut_ptr(),
|
||||
&mut nm,
|
||||
modes.as_mut_ptr(),
|
||||
None,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
paths.truncate(np as usize);
|
||||
modes.truncate(nm as usize);
|
||||
|
||||
// Keep the currently-active paths verbatim — their mode indices stay valid because the queried
|
||||
// modes array is passed through unchanged.
|
||||
let mut supplied: Vec<DISPLAYCONFIG_PATH_INFO> = paths
|
||||
.iter()
|
||||
.filter(|p| p.flags & DISPLAYCONFIG_PATH_ACTIVE != 0)
|
||||
.copied()
|
||||
.collect();
|
||||
if supplied.iter().any(|p| p.targetInfo.id == target_id) {
|
||||
return true; // already active — we raced the OS auto-activate
|
||||
}
|
||||
|
||||
// Pick an inactive path for our target whose SOURCE isn't already driving an active display on
|
||||
// the same adapter (sharing one would make the IDD a clone — exactly the no-frames state this
|
||||
// fallback exists to break out of).
|
||||
let Some(cand) = paths.iter().find(|p| {
|
||||
p.targetInfo.id == target_id
|
||||
&& p.flags & DISPLAYCONFIG_PATH_ACTIVE == 0
|
||||
&& !supplied.iter().any(|a| {
|
||||
(
|
||||
a.sourceInfo.adapterId.LowPart,
|
||||
a.sourceInfo.adapterId.HighPart,
|
||||
a.sourceInfo.id,
|
||||
) == (
|
||||
p.sourceInfo.adapterId.LowPart,
|
||||
p.sourceInfo.adapterId.HighPart,
|
||||
p.sourceInfo.id,
|
||||
)
|
||||
})
|
||||
}) else {
|
||||
tracing::warn!(
|
||||
target_id,
|
||||
"explicit path activation: no inactive path with a free source for this target"
|
||||
);
|
||||
return false;
|
||||
};
|
||||
let mut new_path = *cand;
|
||||
new_path.flags |= DISPLAYCONFIG_PATH_ACTIVE;
|
||||
new_path.sourceInfo.Anonymous.modeInfoIdx = DISPLAYCONFIG_PATH_MODE_IDX_INVALID;
|
||||
new_path.targetInfo.Anonymous.modeInfoIdx = DISPLAYCONFIG_PATH_MODE_IDX_INVALID;
|
||||
supplied.push(new_path);
|
||||
|
||||
// SAVE_TO_DATABASE so Windows remembers the arrangement — the next same-identity ADD (the driver
|
||||
// reuses the slot's EDID serial/ConnectorIndex) then auto-activates from the persistence DB and
|
||||
// skips this whole fallback ladder.
|
||||
let rc = SetDisplayConfig(
|
||||
Some(supplied.as_slice()),
|
||||
Some(modes.as_slice()),
|
||||
SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES | SDC_SAVE_TO_DATABASE,
|
||||
);
|
||||
if rc == 0 {
|
||||
tracing::info!(
|
||||
target_id,
|
||||
"explicit path activation: supplied-config apply succeeded (target committed alongside {} active path(s))",
|
||||
supplied.len() - 1
|
||||
);
|
||||
true
|
||||
} else {
|
||||
tracing::warn!(
|
||||
target_id,
|
||||
"explicit path activation: SetDisplayConfig rc={rc:#x}"
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the `\\.\DisplayN` GDI name for a virtual-display 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).
|
||||
pub(crate) unsafe fn resolve_gdi_name(target_id: u32) -> Option<String> {
|
||||
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;
|
||||
}
|
||||
for p in paths.iter().take(np as usize) {
|
||||
if p.targetInfo.id == target_id {
|
||||
let mut src = DISPLAYCONFIG_SOURCE_DEVICE_NAME::default();
|
||||
src.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
|
||||
src.header.size = size_of::<DISPLAYCONFIG_SOURCE_DEVICE_NAME>() as u32;
|
||||
src.header.adapterId = p.sourceInfo.adapterId;
|
||||
src.header.id = p.sourceInfo.id;
|
||||
if DisplayConfigGetDeviceInfo(&mut src.header) == 0 {
|
||||
let name = String::from_utf16_lossy(&src.viewGdiDeviceName);
|
||||
return Some(name.trim_end_matches('\u{0}').to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The virtual display's CURRENT active resolution `(width, height)` via the GDI/CCD API, or `None` if the
|
||||
/// target isn't an active display yet / the query fails. The IDD-push capturer sizes its ring to this
|
||||
/// ACTUAL mode and polls it to recreate the ring when it changes — a fullscreen game can change the
|
||||
/// virtual display's mode out from under the session-negotiated one (game-capture bug GB1).
|
||||
///
|
||||
/// # Safety
|
||||
/// Calls the GDI/CCD APIs; safe to call from any thread.
|
||||
pub(crate) unsafe fn active_resolution(target_id: u32) -> Option<(u32, u32)> {
|
||||
let gdi = resolve_gdi_name(target_id)?;
|
||||
let wname: Vec<u16> = gdi.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let mut dm = DEVMODEW {
|
||||
dmSize: size_of::<DEVMODEW>() as u16,
|
||||
..Default::default()
|
||||
};
|
||||
let ok = EnumDisplaySettingsW(PCWSTR(wname.as_ptr()), ENUM_CURRENT_SETTINGS, &mut dm).as_bool();
|
||||
if !ok || dm.dmPelsWidth == 0 || dm.dmPelsHeight == 0 {
|
||||
return None;
|
||||
}
|
||||
Some((dm.dmPelsWidth, dm.dmPelsHeight))
|
||||
}
|
||||
|
||||
/// Toggle the virtual-display target's advanced-color (HDR) state via the CCD API. Disabling HDR while on the
|
||||
/// secure (Winlogon) desktop makes it render SDR/composed so DXGI Desktop Duplication can capture it
|
||||
/// (the HDR fullscreen independent-flip otherwise storms `ACCESS_LOST` → black); re-enable on return so
|
||||
/// WGC keeps HDR on the normal desktop. Returns true on a successful `DisplayConfigSetDeviceInfo`.
|
||||
pub(crate) unsafe fn set_advanced_color(target_id: u32, enable: bool) -> bool {
|
||||
let mut np = 0u32;
|
||||
let mut nm = 0u32;
|
||||
if GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut np, &mut nm).is_err() {
|
||||
return false;
|
||||
}
|
||||
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 false;
|
||||
}
|
||||
for p in paths.iter().take(np as usize) {
|
||||
if p.targetInfo.id == target_id {
|
||||
let mut s = DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE::default();
|
||||
s.header.r#type = DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE;
|
||||
s.header.size = size_of::<DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE>() as u32;
|
||||
s.header.adapterId = p.targetInfo.adapterId;
|
||||
s.header.id = p.targetInfo.id;
|
||||
s.Anonymous.value = enable as u32; // bit 0 = enableAdvancedColor
|
||||
let rc = DisplayConfigSetDeviceInfo(&s.header);
|
||||
tracing::debug!(
|
||||
target_id,
|
||||
enable,
|
||||
rc,
|
||||
"virtual-display set advanced-color (HDR) state"
|
||||
);
|
||||
return rc == 0;
|
||||
}
|
||||
}
|
||||
tracing::warn!(
|
||||
target_id,
|
||||
"virtual-display advanced-color: target not in active paths"
|
||||
);
|
||||
false
|
||||
}
|
||||
|
||||
/// Read the virtual-display target's CURRENT advanced-color (HDR) state via the CCD API — i.e. whether HDR is
|
||||
/// actually ON for the virtual display right now (e.g. because the user toggled it in Windows display
|
||||
/// settings). The capture/encode pipeline follows the monitor's real colorspace (WGC → FP16 → NVENC
|
||||
/// Main10 BT.2020 PQ), so this is the authoritative "is this an HDR session" signal — NOT the
|
||||
/// handshake-negotiated bit depth. `None` when the query fails or the target isn't in the active-path
|
||||
/// list (both happen transiently during a display-topology re-probe): the caller decides the fallback —
|
||||
/// the capture loop's poller keeps the last known value, since reading a blip as "HDR off" used to cost
|
||||
/// an HDR session TWO spurious ring recreates (false, then true again a poll later).
|
||||
pub(crate) unsafe fn advanced_color_enabled(target_id: u32) -> Option<bool> {
|
||||
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;
|
||||
}
|
||||
for p in paths.iter().take(np as usize) {
|
||||
if p.targetInfo.id == target_id {
|
||||
let mut info = DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO::default();
|
||||
info.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
|
||||
info.header.size = size_of::<DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO>() as u32;
|
||||
info.header.adapterId = p.targetInfo.adapterId;
|
||||
info.header.id = p.targetInfo.id;
|
||||
if DisplayConfigGetDeviceInfo(&mut info.header) == 0 {
|
||||
// value bit 1 = advancedColorEnabled (bit 0 = advancedColorSupported).
|
||||
return Some((info.Anonymous.value & 0x2) != 0);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Force the freshly-added virtual monitor to the client's exact `WxH@Hz`. The ADD IOCTL only
|
||||
/// ADVERTISES the mode; Windows otherwise activates an IDD target at a 1280x720 default, so the
|
||||
/// ACTIVE mode (what DXGI Desktop Duplication captures) must be set explicitly. CDS_TEST first so a
|
||||
/// mode the driver didn't advertise just leaves the default instead of erroring the session.
|
||||
// pub(crate) so vdisplay::pf_vdisplay can reuse this backend-neutral CCD/GDI mode-set helper
|
||||
// (a pf-vdisplay monitor's GDI name is a real OS device name, so it works unchanged).
|
||||
pub(crate) fn set_active_mode(gdi_name: &str, mode: Mode) {
|
||||
let wname: Vec<u16> = gdi_name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
|
||||
// Enumerate the modes the driver actually advertises for this output and pick the best match for
|
||||
// the requested RESOLUTION: the exact refresh if present, else the highest advertised refresh
|
||||
// <= requested, else the highest available at that resolution. The pf-vdisplay ADD IOCTL advertises
|
||||
// the client mode, but a very high pixel rate (e.g. 5120x1440@240 = 1.77 Gpix/s) can be clamped
|
||||
// or absent — falling back to a lower refresh AT THE SAME RESOLUTION keeps the client's
|
||||
// resolution (what the user sees) instead of collapsing to the 1280x720/1920x1080 OS default.
|
||||
let mut at_res: Vec<u32> = Vec::new();
|
||||
let mut res_set: std::collections::BTreeSet<(u32, u32)> = std::collections::BTreeSet::new();
|
||||
let mut i = 0u32;
|
||||
loop {
|
||||
let mut dm = DEVMODEW {
|
||||
dmSize: size_of::<DEVMODEW>() as u16,
|
||||
..Default::default()
|
||||
};
|
||||
// SAFETY: `wname` is a live NUL-terminated UTF-16 device name (built above) whose pointer stays
|
||||
// valid for the call; `&mut dm` is a live DEVMODEW with `dmSize` set that EnumDisplaySettingsW
|
||||
// fills in for mode index `i`. Both outlive this synchronous call; the API only reads the name
|
||||
// and writes `dm`, so nothing aliases.
|
||||
let ok = unsafe {
|
||||
EnumDisplaySettingsW(
|
||||
PCWSTR(wname.as_ptr()),
|
||||
ENUM_DISPLAY_SETTINGS_MODE(i),
|
||||
&mut dm,
|
||||
)
|
||||
}
|
||||
.as_bool();
|
||||
if !ok {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
res_set.insert((dm.dmPelsWidth, dm.dmPelsHeight));
|
||||
if dm.dmPelsWidth == mode.width && dm.dmPelsHeight == mode.height {
|
||||
at_res.push(dm.dmDisplayFrequency);
|
||||
}
|
||||
}
|
||||
let chosen_hz = if at_res.contains(&mode.refresh_hz) {
|
||||
mode.refresh_hz
|
||||
} else if let Some(hz) = at_res
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&hz| hz <= mode.refresh_hz)
|
||||
.max()
|
||||
{
|
||||
hz
|
||||
} else if let Some(hz) = at_res.iter().copied().max() {
|
||||
hz
|
||||
} else {
|
||||
mode.refresh_hz // resolution not advertised at all; attempt anyway (likely -> OS default)
|
||||
};
|
||||
if at_res.is_empty() {
|
||||
tracing::warn!(
|
||||
"{gdi_name}: driver advertises no {}x{} mode (top advertised: {:?}); attempting @{} anyway",
|
||||
mode.width,
|
||||
mode.height,
|
||||
res_set.iter().rev().take(8).collect::<Vec<_>>(),
|
||||
mode.refresh_hz
|
||||
);
|
||||
} else if chosen_hz != mode.refresh_hz {
|
||||
tracing::info!(
|
||||
"{gdi_name}: {}x{}@{} not advertised; using {}x{}@{} (advertised refreshes here: {:?})",
|
||||
mode.width,
|
||||
mode.height,
|
||||
mode.refresh_hz,
|
||||
mode.width,
|
||||
mode.height,
|
||||
chosen_hz,
|
||||
at_res
|
||||
);
|
||||
}
|
||||
|
||||
// Set ONLY this output's mode in place (size/refresh/bpp; NO DM_POSITION). Do NOT promote it to
|
||||
// PRIMARY here and do NOT write a GLOBAL topology: promoting the IDD to primary at (0,0) while the
|
||||
// box's leftover basic display is still active contests the topology and storms
|
||||
// DXGI_ERROR_MODE_CHANGE_IN_PROGRESS (measured live). The IDD is made the sole → primary →
|
||||
// DWM-composited display by the CCD isolation in create() (which deactivates the other display
|
||||
// first), so a sole display is already primary and needs no CDS_SET_PRIMARY here.
|
||||
let dm = DEVMODEW {
|
||||
dmSize: size_of::<DEVMODEW>() as u16,
|
||||
dmFields: DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL,
|
||||
dmBitsPerPel: 32,
|
||||
dmPelsWidth: mode.width,
|
||||
dmPelsHeight: mode.height,
|
||||
dmDisplayFrequency: chosen_hz,
|
||||
..Default::default()
|
||||
};
|
||||
// SAFETY: `wname` is a live NUL-terminated UTF-16 device name and `&dm` is a live DEVMODEW describing
|
||||
// the requested mode; both outlive the call. CDS_TEST only validates the mode (no apply), the two
|
||||
// trailing args are null, and the API only reads its inputs.
|
||||
let test = unsafe {
|
||||
ChangeDisplaySettingsExW(PCWSTR(wname.as_ptr()), Some(&dm), None, CDS_TEST, None)
|
||||
};
|
||||
if test != DISP_CHANGE_SUCCESSFUL {
|
||||
tracing::warn!(
|
||||
result = test.0,
|
||||
"{gdi_name}: driver rejected {}x{}@{} (mode not advertised?) — leaving OS default",
|
||||
mode.width,
|
||||
mode.height,
|
||||
chosen_hz
|
||||
);
|
||||
return;
|
||||
}
|
||||
// SAFETY: same inputs as the CDS_TEST call above — `wname` (live NUL-terminated device name) and
|
||||
// `&dm` (live DEVMODEW) both outlive the call; CDS_UPDATEREGISTRY applies the already-validated mode,
|
||||
// and the API only reads its inputs.
|
||||
let apply = unsafe {
|
||||
ChangeDisplaySettingsExW(
|
||||
PCWSTR(wname.as_ptr()),
|
||||
Some(&dm),
|
||||
None,
|
||||
CDS_UPDATEREGISTRY,
|
||||
None,
|
||||
)
|
||||
};
|
||||
if apply == DISP_CHANGE_SUCCESSFUL {
|
||||
tracing::info!(
|
||||
"{gdi_name}: active mode set to {}x{}@{}",
|
||||
mode.width,
|
||||
mode.height,
|
||||
chosen_hz
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
result = apply.0,
|
||||
"{gdi_name}: failed to apply {}x{}@{}",
|
||||
mode.width,
|
||||
mode.height,
|
||||
chosen_hz
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Saved active display topology, for restoring on teardown.
|
||||
// pub(crate) so vdisplay::pf_vdisplay's Monitor can hold the same saved-topology type.
|
||||
pub(crate) type SavedConfig = (Vec<DISPLAYCONFIG_PATH_INFO>, Vec<DISPLAYCONFIG_MODE_INFO>);
|
||||
|
||||
/// `DISPLAYCONFIG_PATH_ACTIVE` (wingdi.h) — the `flags` bit marking a path active. The `windows` crate
|
||||
/// doesn't export it, so define it here.
|
||||
const DISPLAYCONFIG_PATH_ACTIVE: u32 = 0x0000_0001;
|
||||
|
||||
/// `DISPLAYCONFIG_PATH_MODE_IDX_INVALID` (wingdi.h) — "no mode pinned" for a path's source/target
|
||||
/// mode index; with `SDC_ALLOW_CHANGES` the OS picks the modes itself. Not exported by the `windows`
|
||||
/// crate either.
|
||||
const DISPLAYCONFIG_PATH_MODE_IDX_INVALID: u32 = 0xffff_ffff;
|
||||
|
||||
/// Query the current ACTIVE display config (paths + modes), truncated to the real counts. `None` on
|
||||
/// API failure. Shared by [`isolate_displays_ccd`] (snapshot + per-attempt re-query) and
|
||||
/// [`count_other_active`].
|
||||
unsafe fn query_active_config() -> 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);
|
||||
Some((paths, modes))
|
||||
}
|
||||
|
||||
/// Count currently-ACTIVE display paths whose target id is not in `keep_target_ids` — i.e. displays
|
||||
/// that would still be lit besides the managed virtual set. `None` on query failure. Used to VERIFY
|
||||
/// isolation actually took, and (in the `primary` topology) to detect a physical that is ALREADY
|
||||
/// active so we can skip a force-EXTEND that would reset its refresh.
|
||||
pub(crate) unsafe fn count_other_active(keep_target_ids: &[u32]) -> Option<u32> {
|
||||
let (paths, _) = query_active_config()?;
|
||||
Some(
|
||||
paths
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
!keep_target_ids.contains(&p.targetInfo.id)
|
||||
&& p.flags & DISPLAYCONFIG_PATH_ACTIVE != 0
|
||||
})
|
||||
.count() as u32,
|
||||
)
|
||||
}
|
||||
|
||||
/// One CONNECTED display target from a full (`QDC_ALL_PATHS`) CCD sweep — the disturbance-
|
||||
/// attribution inventory. `external_physical` is the load-bearing bit: a standby TV/monitor on a
|
||||
/// real connector is the prime suspect for the periodic link-probe stutter class, while internal
|
||||
/// panels and indirect/virtual targets (our own IDD included) are not.
|
||||
pub(crate) struct TargetInventory {
|
||||
pub target_id: u32,
|
||||
/// Whether any active path drives this target (part of the desktop right now).
|
||||
pub active: bool,
|
||||
/// External physical connector (HDMI/DP/DVI/…): candidate for standby link-probe churn.
|
||||
pub external_physical: bool,
|
||||
/// Short connector label for logs (`"HDMI"`, `"DisplayPort"`, `"internal-panel"`, …).
|
||||
pub tech: &'static str,
|
||||
/// The monitor's friendly name (`"LG TV SSCR2"`); empty when the EDID carries none.
|
||||
pub friendly: String,
|
||||
/// Monitor device interface path — maps to the PnP instance id (`monitor_devnode`).
|
||||
pub monitor_device_path: String,
|
||||
}
|
||||
|
||||
/// Classify a CCD output technology: `(external physical?, log label)`. Allowlist, not blocklist:
|
||||
/// new/unknown/indirect technologies read as non-external, so a co-installed third-party virtual
|
||||
/// display can never be mistaken for a physical suspect (same precision rule as `monitor_devnode`).
|
||||
fn output_tech_class(tech: DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY) -> (bool, &'static str) {
|
||||
match tech {
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI => (true, "HDMI"),
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL => (true, "DisplayPort"),
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DVI => (true, "DVI"),
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HD15 => (true, "VGA"),
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EXTERNAL => (true, "UDI"),
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDI => (true, "SDI"),
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPONENT_VIDEO => (true, "component"),
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPOSITE_VIDEO => (true, "composite"),
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SVIDEO => (true, "S-Video"),
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDTVDONGLE => (true, "TV-dongle"),
|
||||
DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL
|
||||
| DISPLAYCONFIG_OUTPUT_TECHNOLOGY_LVDS
|
||||
| DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED
|
||||
| DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED => (false, "internal-panel"),
|
||||
_ => (false, "virtual/other"),
|
||||
}
|
||||
}
|
||||
|
||||
fn utf16z_str(buf: &[u16]) -> String {
|
||||
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||
String::from_utf16_lossy(&buf[..len])
|
||||
}
|
||||
|
||||
/// Sweep EVERY connected display target (`QDC_ALL_PATHS`, deduped from the source×target path
|
||||
/// matrix to unique targets) with its name, connector class and active state. Read-only CCD; can
|
||||
/// briefly serialize on the display-config lock during topology churn — callers must keep it OFF
|
||||
/// the capture thread (`display_events` runs it on its own listener thread and caches).
|
||||
pub(crate) unsafe fn target_inventory() -> Vec<TargetInventory> {
|
||||
let mut np = 0u32;
|
||||
let mut nm = 0u32;
|
||||
if GetDisplayConfigBufferSizes(QDC_ALL_PATHS, &mut np, &mut nm).is_err() {
|
||||
return Vec::new();
|
||||
}
|
||||
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_ALL_PATHS,
|
||||
&mut np,
|
||||
paths.as_mut_ptr(),
|
||||
&mut nm,
|
||||
modes.as_mut_ptr(),
|
||||
None,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
paths.truncate(np as usize);
|
||||
// Targets driven by an ACTIVE path. `(LUID parts, target id)` keys: target ids are only
|
||||
// unique per adapter.
|
||||
let active: Vec<(u32, i32, u32)> = paths
|
||||
.iter()
|
||||
.filter(|p| p.flags & DISPLAYCONFIG_PATH_ACTIVE != 0)
|
||||
.map(|p| {
|
||||
(
|
||||
p.targetInfo.adapterId.LowPart,
|
||||
p.targetInfo.adapterId.HighPart,
|
||||
p.targetInfo.id,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut seen: Vec<(u32, i32, u32)> = Vec::new();
|
||||
let mut out = Vec::new();
|
||||
for p in &paths {
|
||||
let t = &p.targetInfo;
|
||||
let key = (t.adapterId.LowPart, t.adapterId.HighPart, t.id);
|
||||
// `targetAvailable` == a monitor is connected; an ACTIVE target is included regardless
|
||||
// (the flag reads FALSE transiently right after a removal).
|
||||
if (!t.targetAvailable.as_bool() && !active.contains(&key)) || seen.contains(&key) {
|
||||
continue;
|
||||
}
|
||||
seen.push(key);
|
||||
let mut req = DISPLAYCONFIG_TARGET_DEVICE_NAME::default();
|
||||
req.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
|
||||
req.header.size = size_of::<DISPLAYCONFIG_TARGET_DEVICE_NAME>() as u32;
|
||||
req.header.adapterId = t.adapterId;
|
||||
req.header.id = t.id;
|
||||
// `req` is a properly-sized DISPLAYCONFIG_TARGET_DEVICE_NAME local whose header
|
||||
// (type/size/adapterId/id) is fully initialised; the API writes only within the struct.
|
||||
if DisplayConfigGetDeviceInfo(&mut req.header) != 0 {
|
||||
continue; // target with no queryable monitor — nothing to attribute to
|
||||
}
|
||||
let (external_physical, tech) = output_tech_class(req.outputTechnology);
|
||||
out.push(TargetInventory {
|
||||
target_id: t.id,
|
||||
active: active.contains(&key),
|
||||
external_physical,
|
||||
tech,
|
||||
friendly: utf16z_str(&req.monitorFriendlyDeviceName),
|
||||
monitor_device_path: utf16z_str(&req.monitorDevicePath),
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Robust display isolation via the CCD API. The naive GDI approach (EnumDisplayDevices +
|
||||
/// ChangeDisplaySettings) MISSES displays on a hybrid box — an iGPU-attached physical monitor isn't
|
||||
/// flagged `ATTACHED_TO_DESKTOP` in the GDI enum, so it's never detached and the secure desktop /
|
||||
/// lock screen lands on IT while our virtual output freezes. `QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS)`
|
||||
/// sees every active path; we deactivate all of them EXCEPT the managed virtual target **set**
|
||||
/// (`design/display-management.md` §6.1: "exclusive" means the managed set stays active — with
|
||||
/// parallel displays a sibling slot is never deactivated), leaving the virtual display(s) as the sole
|
||||
/// desktop so ALL content (incl. Winlogon) renders to them. Apollo isolates the same way (CCD).
|
||||
/// Re-issued with the grown/shrunk set on each slot add/remove while the group lives; the FIRST call's
|
||||
/// returned config is what teardown restores (the caller keeps it on the group record and discards
|
||||
/// later returns). Returns the original active config to restore on teardown.
|
||||
// pub(crate) so vdisplay::pf_vdisplay can reuse this backend-neutral CCD isolation helper
|
||||
// (it operates on real OS target ids — a pf-vdisplay monitor's target_id qualifies).
|
||||
pub(crate) unsafe fn isolate_displays_ccd(keep_target_ids: &[u32]) -> Option<SavedConfig> {
|
||||
// Snapshot the ORIGINAL active config ONCE for restore-on-teardown, before any changes.
|
||||
let saved = query_active_config()?;
|
||||
|
||||
// Deactivate every non-keep display, then VERIFY and RETRY. A field-reported bug had a physical
|
||||
// monitor STAY ACTIVE in exclusive mode, so we don't trust a single SetDisplayConfig: re-query the
|
||||
// live topology each attempt and re-apply until ONLY the keep set is active. Secure-desktop
|
||||
// correctness depends on this — the lock screen must not land on a stray panel while we stream.
|
||||
for attempt in 1..=4u32 {
|
||||
let (mut paths, modes) = query_active_config()?;
|
||||
let mut others = 0u32;
|
||||
for p in paths.iter_mut() {
|
||||
if keep_target_ids.contains(&p.targetInfo.id) {
|
||||
continue;
|
||||
}
|
||||
if p.flags & DISPLAYCONFIG_PATH_ACTIVE != 0 {
|
||||
p.flags &= !DISPLAYCONFIG_PATH_ACTIVE; // mark this path inactive
|
||||
others += 1;
|
||||
}
|
||||
}
|
||||
// Commit the config. Even when nothing needed deactivating we re-commit: a legacy mode-set does
|
||||
// NOT drive the IddCx adapter's EVT_IDD_CX_ADAPTER_COMMIT_MODES, and without COMMIT_MODES the OS
|
||||
// never calls ASSIGN_SWAPCHAIN, so the driver receives no frames. SDC_FORCE_MODE_ENUMERATION
|
||||
// forces the re-commit; SAVE_TO_DATABASE only in the sole-path case (matches prior behavior —
|
||||
// don't permanently rewrite the user's multi-display layout; the teardown restore handles it).
|
||||
let mut flags = SDC_APPLY
|
||||
| SDC_USE_SUPPLIED_DISPLAY_CONFIG
|
||||
| SDC_ALLOW_CHANGES
|
||||
| SDC_FORCE_MODE_ENUMERATION;
|
||||
if others == 0 {
|
||||
flags |= SDC_SAVE_TO_DATABASE;
|
||||
}
|
||||
let rc = SetDisplayConfig(Some(paths.as_slice()), Some(modes.as_slice()), flags);
|
||||
|
||||
// VERIFY the OUTCOME (rc alone lies — a "successful" apply can leave a panel active): re-query
|
||||
// and confirm no non-keep display survived. Only then is the virtual set truly the sole desktop.
|
||||
let survivors = count_other_active(keep_target_ids).unwrap_or(0);
|
||||
if survivors == 0 {
|
||||
tracing::info!("display isolate (CCD): target set {keep_target_ids:?} is the SOLE active desktop (attempt {attempt}/4, deactivated {others}, rc={rc:#x})");
|
||||
return Some(saved);
|
||||
}
|
||||
tracing::warn!("display isolate (CCD): {survivors} display(s) STILL active after attempt {attempt}/4 (deactivated {others}, rc={rc:#x}) — re-querying + retrying");
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
}
|
||||
tracing::error!("display isolate (CCD): failed to isolate target set {keep_target_ids:?} after 4 attempts — a non-virtual display stayed active (field-reported exclusive-mode bug)");
|
||||
Some(saved)
|
||||
}
|
||||
|
||||
/// The desktop-space rectangle `(x, y, w, h)` of `target_id`'s SOURCE — where this display's
|
||||
/// region lives in the desktop coordinate space. `None` while the target isn't an active path.
|
||||
/// Used by the IDD-push compose kick to dirty THE TARGET display: with parallel displays the
|
||||
/// cursor sits on ONE of them, and a cursor wiggle only dirties that one — a sibling display's
|
||||
/// kick must first know where to send the cursor (Stage W3 on-glass finding).
|
||||
pub(crate) unsafe fn source_desktop_rect(target_id: u32) -> Option<(i32, i32, i32, i32)> {
|
||||
let (paths, modes) = query_active_config()?;
|
||||
for p in &paths {
|
||||
if p.targetInfo.id != target_id || p.flags & DISPLAYCONFIG_PATH_ACTIVE == 0 {
|
||||
continue;
|
||||
}
|
||||
let idx = p.sourceInfo.Anonymous.modeInfoIdx as usize;
|
||||
let m = modes.get(idx)?;
|
||||
if m.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE {
|
||||
return None;
|
||||
}
|
||||
let sm = m.Anonymous.sourceMode;
|
||||
return Some((
|
||||
sm.position.x,
|
||||
sm.position.y,
|
||||
sm.width as i32,
|
||||
sm.height as i32,
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Place each managed virtual target's SOURCE at the given desktop-space origin, as ONE atomic CCD
|
||||
/// `SetDisplayConfig` (design `display-management.md` §6.2 — the Windows arm of the pure
|
||||
/// `vdisplay/layout.rs` arrangement; positions come from `arrange`, this only commits them). Windows
|
||||
/// treats the source at `(0,0)` as primary, so auto-row's first member lands primary — the group's
|
||||
/// designated member. Paths not named stay where they are. Best-effort: a failure leaves the OS
|
||||
/// placement (mouse crossing may not match the layout table until the next apply).
|
||||
pub(crate) unsafe fn apply_source_positions(positions: &[(u32, i32, i32)]) {
|
||||
if positions.len() < 2 {
|
||||
return; // a single (or no) member sits at the origin — nothing to arrange
|
||||
}
|
||||
let Some((paths, mut modes)) = query_active_config() else {
|
||||
return;
|
||||
};
|
||||
// Dedup source-mode indices (a cloned group shares one) — same discipline as
|
||||
// `set_virtual_primary_ccd`.
|
||||
let mut done = std::collections::HashSet::new();
|
||||
let mut moved = 0u32;
|
||||
for p in paths.iter() {
|
||||
let Some(&(_, x, y)) = positions.iter().find(|(t, _, _)| *t == p.targetInfo.id) else {
|
||||
continue;
|
||||
};
|
||||
let idx = p.sourceInfo.Anonymous.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;
|
||||
}
|
||||
m.Anonymous.sourceMode.position = POINTL { x, y };
|
||||
moved += 1;
|
||||
}
|
||||
if moved == 0 {
|
||||
return;
|
||||
}
|
||||
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!(
|
||||
?positions,
|
||||
"display layout (CCD): group source origins applied"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
?positions,
|
||||
"display layout (CCD): SetDisplayConfig rc={rc:#x}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **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 lay the other displays out to its right.
|
||||
let virt_width = paths.iter().find_map(|p| {
|
||||
if p.targetInfo.id != keep_target_id {
|
||||
return None;
|
||||
}
|
||||
let idx = p.sourceInfo.Anonymous.modeInfoIdx as usize;
|
||||
let m = modes.get(idx)?;
|
||||
// `then_some` (eager): `sourceMode.width` is a POD `u32` union read, discarded when the arm is
|
||||
// false — no lazy guard needed. (`then(|| …)` here trips clippy::unnecessary_lazy_evaluations.)
|
||||
(m.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE)
|
||||
.then_some(m.Anonymous.sourceMode.width as i32)
|
||||
})?;
|
||||
let others = paths.len().saturating_sub(1);
|
||||
|
||||
// Reposition each active path's SOURCE once: the virtual to (0,0) (= primary), the other
|
||||
// displays PACKED left-to-right from the virtual's right edge — kept active, no overlap and no
|
||||
// gap (vs. blindly shifting each by virt_width, which leaves a dead gap when EXTEND already
|
||||
// placed them to the right). Dedup source-mode indices (a cloned group shares one).
|
||||
let mut next_x = virt_width;
|
||||
let mut done = std::collections::HashSet::new();
|
||||
for p in paths.iter() {
|
||||
let idx = p.sourceInfo.Anonymous.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 {
|
||||
let w = m.Anonymous.sourceMode.width as i32;
|
||||
m.Anonymous.sourceMode.position = POINTL { x: next_x, y: 0 };
|
||||
next_x += w;
|
||||
}
|
||||
}
|
||||
|
||||
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); {others} other display(s) kept ACTIVE + packed to its right");
|
||||
} 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.
|
||||
pub(crate) unsafe fn restore_displays_ccd(saved: &SavedConfig) {
|
||||
let (paths, modes) = saved;
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
let rc = SetDisplayConfig(
|
||||
Some(paths.as_slice()),
|
||||
Some(modes.as_slice()),
|
||||
SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES,
|
||||
);
|
||||
if rc == 0 {
|
||||
tracing::info!("display isolate (CCD): restored original topology");
|
||||
} else {
|
||||
tracing::warn!("display isolate (CCD): topology restore failed rc={rc:#x} — physical displays may be left deactivated");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user