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
@@ -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
),
}