fix(host): activate lid-closed pf-vdisplay targets — explicit CCD path-commit fallback
ci / docs-site (push) Successful in 1m3s
ci / web (push) Successful in 1m3s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
apple / swift (push) Successful in 5m2s
ci / bench (push) Successful in 8m3s
docker / deploy-docs (push) Successful in 26s
windows-host / package (push) Successful in 9m3s
arch / build-publish (push) Successful in 14m56s
android / android (push) Successful in 16m48s
ci / rust (push) Successful in 18m17s
deb / build-publish (push) Successful in 18m31s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m32s
apple / screenshots (push) Successful in 20m40s

A lid-closed laptop defeats both activation stages for a fresh IddCx
target: the clamshell lid policy suppresses the new-monitor
auto-activate, and the SDC_TOPOLOGY_EXTEND preset returns success
without committing a path for the IDD — so every session retry burned
~10s in resolve_target_gdi and the stream died with "not yet an active
display path" after 8 attempts (RDP/Parsec still work there: neither
needs a NEW console display path). Field report: Windows laptop host,
Intel iGPU, lid closed, v0.10.1.

New activate_target_path() (win_display.rs) is the supplied-config
apply Windows' own display Settings uses to turn a monitor on, which
doesn't consult the lid policy: QueryDisplayConfig(QDC_ALL_PATHS), keep
every active path verbatim, append the target's inactive path with a
source no active display is using (never a clone), both mode idxs
DISPLAYCONFIG_PATH_MODE_IDX_INVALID, then SDC_APPLY |
SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES |
SDC_SAVE_TO_DATABASE — SAVE_TO_DATABASE so the next same-identity ADD
auto-activates from the persistence DB and skips the ladder. Wired as
the THIRD stage of resolve_target_gdi; the on-glass-validated
auto-activate → force-EXTEND order is unchanged.

Also sweep stale "SudoVDA" out of logs/errors and current-behavior doc
comments (the backend was removed; pf-vdisplay is the sole one): the
capture error now names pf-vdisplay, the HDR toggle logs
virtual-display, and the not-active warns list the exhausted fallbacks.
Genuinely historical SudoVDA notes stay.

cargo check + clippy green on the Windows box; on-glass lid-closed
repro still owed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 16:44:41 +02:00
parent 7b9337c7d0
commit e1d7fa2a30
6 changed files with 150 additions and 17 deletions
+2 -1
View File
@@ -405,7 +405,8 @@ pub fn capture_virtual_output(
) -> Result<Box<dyn Capturer>> {
let target = vout.win_capture.clone().ok_or_else(|| {
anyhow::anyhow!(
"SudoVDA target not yet an active display (needs a WDDM GPU to activate it)"
"pf-vdisplay target not yet an active display path (activation failed — see the \
virtual-display warnings above)"
)
})?;
let pref = vout.preferred_mode;
@@ -40,7 +40,7 @@ pub struct WinCaptureTarget {
pub adapter_luid: i64,
/// The output's GDI device name, e.g. `\\.\DISPLAY3`. Can CHANGE across a secure-desktop switch.
pub gdi_name: String,
/// Stable SudoVDA target id — re-resolved to the current GDI name on every recovery.
/// Stable virtual-display (IddCx) target id — re-resolved to the current GDI name on every recovery.
pub target_id: u32,
/// The pf-vdisplay driver's WUDFHost pid (from the ADD reply) — the process the IDD-push capturer
/// duplicates the sealed frame channel's handles INTO (`idd_push::ChannelBroker`). `0` = unknown
+1 -1
View File
@@ -3122,7 +3122,7 @@ fn resolve_compositor(
dedicated_launch: bool,
) -> Result<crate::vdisplay::Compositor> {
use crate::vdisplay::Compositor;
// Windows has a single virtual-display backend (SudoVDA); vdisplay::open ignores the compositor
// Windows has a single virtual-display backend (pf-vdisplay); vdisplay::open ignores the compositor
// arg there, so short-circuit the Linux session-detection state machine with a placeholder.
#[cfg(target_os = "windows")]
{
+1 -1
View File
@@ -61,7 +61,7 @@ pub struct VirtualOutput {
/// gamescope outputs are created at the exact size, so this just confirms it; **Mutter sizes
/// its virtual monitor FROM the negotiation**, so here it's what makes the client's mode real.
pub preferred_mode: Option<(u32, u32, u32)>,
/// Windows capture identity (DXGI adapter LUID + GDI output name) for the SudoVDA backend —
/// Windows capture identity (DXGI adapter LUID + GDI output name) for the pf-vdisplay backend —
/// what [`crate::capture::capture_virtual_output`] needs to duplicate the right output.
#[cfg(target_os = "windows")]
pub win_capture: Option<crate::capture::dxgi::WinCaptureTarget>,
@@ -822,6 +822,13 @@ impl VirtualDisplayManager {
/// catch. If that ever shows up, widen the gate to also fire when the IDD target's source is shared
/// with another active path (a `target_is_cloned` helper) — needs on-laptop validation first.
///
/// LAST RESORT — explicit path activation: a lid-closed laptop (field report, Intel iGPU) defeats
/// BOTH stages above — the clamshell lid policy suppresses the new-monitor auto-activation, and
/// the `SDC_TOPOLOGY_EXTEND` preset "succeeds" without committing a path for the IDD — so the
/// target stays connected-but-inactive for the session's whole retry budget. `activate_target_path`
/// commits the target's path directly (supplied-config apply, the same thing display Settings
/// does), which doesn't consult the lid policy at all.
///
/// # Safety
/// Runs the CCD (QueryDisplayConfig / SetDisplayConfig) FFI; call under the `state` lock.
unsafe fn resolve_target_gdi(&self, target_id: u32) -> Option<String> {
@@ -842,6 +849,17 @@ impl VirtualDisplayManager {
return Some(n);
}
}
// SAFETY: `activate_target_path` runs the CCD query/apply FFI with owned local buffers; the
// `Copy` target id is passed by value, under the `state` lock — the sole topology mutator.
if unsafe { crate::win_display::activate_target_path(target_id) } {
for _ in 0..15 {
thread::sleep(Duration::from_millis(200));
// SAFETY: as the resolve loops above.
if let Some(n) = unsafe { resolve_gdi_name(target_id) } {
return Some(n);
}
}
}
None
}
@@ -886,7 +904,11 @@ impl VirtualDisplayManager {
let gdi_name = unsafe { self.resolve_target_gdi(added.target_id) };
match &gdi_name {
Some(n) => {
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.
set_active_mode(n, mode);
// Apply the display-management topology (Stage 2, GROUP-scoped since Stage W2).
@@ -977,8 +999,7 @@ impl VirtualDisplayManager {
}
// SAFETY: `set_virtual_primary_ccd` takes the `Copy` target id by value and returns
// an owned `SavedConfig` (no borrowed memory crosses), under the `state` lock.
inner.group.ccd_saved =
unsafe { set_virtual_primary_ccd(added.target_id) };
inner.group.ccd_saved = unsafe { set_virtual_primary_ccd(added.target_id) };
}
Topology::Primary => {
// A sibling already holds primary (the group's designated member) — the new
@@ -996,7 +1017,8 @@ impl VirtualDisplayManager {
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens
}
None => tracing::warn!(
"virtual-display target {} not yet an active display path (needs a WDDM GPU to activate)",
"virtual-display target {} not yet an active display path (auto-activate, EXTEND \
preset and explicit path activation all failed — GPU-less box?)",
added.target_id
),
}
@@ -1081,7 +1103,11 @@ impl VirtualDisplayManager {
let gdi_name = unsafe { self.resolve_target_gdi(added.target_id) };
match &gdi_name {
Some(n) => {
tracing::info!(backend = self.driver.name(), "re-arrival target {} -> {n}", added.target_id);
tracing::info!(
backend = self.driver.name(),
"re-arrival target {} -> {n}",
added.target_id
);
// ADD only advertises the mode; force it active so DXGI/IDD captures the new size.
set_active_mode(n, mode);
// 4. Re-isolate the composited set with the NEW target replacing the old — preserving
@@ -1091,7 +1117,8 @@ impl VirtualDisplayManager {
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture reopens
}
None => tracing::warn!(
"re-arrival target {} not yet an active display path (needs a WDDM GPU to activate)",
"re-arrival target {} not yet an active display path (auto-activate, EXTEND preset \
and explicit path activation all failed — GPU-less box?)",
added.target_id
),
}
@@ -20,7 +20,7 @@ use windows::Win32::Devices::Display::{
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE,
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_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,
};
@@ -56,7 +56,107 @@ pub(crate) unsafe fn force_extend_topology() {
}
}
/// Resolve the `\\.\DisplayN` GDI name for a SudoVDA target id via the CCD API. Returns `None`
/// 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> {
@@ -116,7 +216,7 @@ pub(crate) unsafe fn active_resolution(target_id: u32) -> Option<(u32, u32)> {
Some((dm.dmPelsWidth, dm.dmPelsHeight))
}
/// Toggle the SudoVDA target's advanced-color (HDR) state via the CCD API. Disabling HDR while on the
/// 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`.
@@ -153,7 +253,7 @@ pub(crate) unsafe fn set_advanced_color(target_id: u32, enable: bool) -> bool {
target_id,
enable,
rc,
"SudoVDA set advanced-color (HDR) state"
"virtual-display set advanced-color (HDR) state"
);
return rc == 0;
}
@@ -165,7 +265,7 @@ pub(crate) unsafe fn set_advanced_color(target_id: u32, enable: bool) -> bool {
false
}
/// Read the SudoVDA target's CURRENT advanced-color (HDR) state via the CCD API — i.e. whether HDR is
/// 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
@@ -210,7 +310,7 @@ pub(crate) unsafe fn advanced_color_enabled(target_id: u32) -> Option<bool> {
None
}
/// Force the freshly-added SudoVDA monitor to the client's exact `WxH@Hz`. The ADD IOCTL only
/// 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.
@@ -221,7 +321,7 @@ pub(crate) fn set_active_mode(gdi_name: &str, mode: Mode) {
// 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 SudoVDA ADD IOCTL advertises
// <= 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.
@@ -358,6 +458,11 @@ pub(crate) type SavedConfig = (Vec<DISPLAYCONFIG_PATH_INFO>, Vec<DISPLAYCONFIG_M
/// 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`].