fix(windows-host): IDD activation — resolve-first, force-EXTEND only as fallback
apple / swift (push) Successful in 1m6s
android / android (push) Successful in 4m23s
ci / rust (push) Successful in 5m9s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 57s
apple / screenshots (push) Successful in 5m33s
windows-host / package (push) Successful in 8m46s
deb / build-publish (push) Successful in 3m13s
decky / build-publish (push) Successful in 27s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
ci / bench (push) Successful in 4m51s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m15s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m19s
docker / deploy-docs (push) Successful in 21s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m44s
apple / swift (push) Successful in 1m6s
android / android (push) Successful in 4m23s
ci / rust (push) Successful in 5m9s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 57s
apple / screenshots (push) Successful in 5m33s
windows-host / package (push) Successful in 8m46s
deb / build-publish (push) Successful in 3m13s
decky / build-publish (push) Successful in 27s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
ci / bench (push) Successful in 4m51s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m15s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m19s
docker / deploy-docs (push) Successful in 21s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m44s
force_extend_topology() was added before the resolve loop to de-clone a fresh IDD on integrated-screen boxes (laptops), but its bare SDC_TOPOLOGY_EXTEND preset is ACCESS_DENIED from the Session-0 service context on a HEADLESS box and broke the IDD auto-activation there: resolve_gdi_name stayed None -> "not an active display path" -> black screen. That regressed the headless/primary platform (live RTX box). Revert to the provene2c9bfdflow: resolve FIRST (Windows auto-activates the IDD as its own extended path), and force-EXTEND only as the FALLBACK when resolve returns None (the integrated-screen clone case, observed live to leave resolve None). The success path is byte-identical toe2c9bfd(resolve -> set_active_mode -> isolate_displays_ccd). Validated live: the headless RTX box streams again (probe: frames flow, driver attaches to the ring, host/driver render LUIDs match). Reviewed multi-agent + adversarial: no regression on the validated headless path or the observed Optimus-laptop clone path (a cloned IddCx target resolves to None there, so the is_none() fallback fires + de-clones). Known theoretical caveat, documented inline and unobserved for IddCx but untested across GPU/driver/OS: a CCD clone that manifests as a shared-source ACTIVE path would resolve to Some and bypass the is_none() gate. Follow-up: widen the gate (a target_is_cloned helper) once an integrated-screen box is available to validate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -327,17 +327,17 @@ impl VirtualDisplayManager {
|
||||
}
|
||||
});
|
||||
|
||||
// Windows defaults a new IddCx monitor into CLONE mode when a physical display is already
|
||||
// active (a laptop panel, an attached monitor): the cloned IDD shares that display's source, so
|
||||
// the OS never commits a distinct path for it and capture sees no frames. Force EXTEND first so
|
||||
// the IDD comes up as its OWN active path; the resolve loop below then finds it. Idempotent /
|
||||
// no-op on a sole-display box, so it's safe on the headless single-GPU path too.
|
||||
// SAFETY: `force_extend_topology` only calls `SetDisplayConfig` (a CCD topology apply) with no
|
||||
// borrowed caller memory; it runs under the manager `state` lock, the sole topology mutator.
|
||||
unsafe { force_extend_topology() };
|
||||
|
||||
// Resolve the capture target. May be None on a GPU-less box (target added but not WDDM-activated);
|
||||
// Resolve the capture target — wait for Windows to auto-activate the freshly-ADDed IDD into its
|
||||
// OWN display path (it comes up EXTENDED alongside any existing/basic display; `set_active_mode`
|
||||
// below then promotes it to primary and `isolate_displays_ccd` makes it the sole composited
|
||||
// desktop — the proven flow). May be None on a GPU-less box (target added but not WDDM-activated);
|
||||
// the capture backend re-resolves once a GPU is present.
|
||||
//
|
||||
// We do NOT force a topology change FIRST: the bare `SDC_TOPOLOGY_EXTEND` preset is ACCESS_DENIED
|
||||
// from our Session-0 service context on a headless box and BREAKS this auto-activate (it regressed
|
||||
// the headless path — the IDD then never gets its own path → "not an active display path" → black).
|
||||
// force-EXTEND is only the FALLBACK below, for an integrated-screen box where a fresh IDD is CLONED
|
||||
// onto the panel (shares its source) instead of getting its own path.
|
||||
let mut gdi_name = None;
|
||||
for _ in 0..15 {
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
@@ -349,6 +349,32 @@ impl VirtualDisplayManager {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for an integrated-screen box (e.g. a laptop panel): Windows CLONES a freshly-added
|
||||
// IDD onto the existing display, sharing its source, so it never gets its own committed path. On
|
||||
// the IddCx clone behaviour observed live (commit 8e87e61, an Intel-iGPU + NVIDIA-Optimus laptop)
|
||||
// `resolve_gdi_name` then stays None — so this `is_none()` fallback fires, force-EXTENDs to
|
||||
// de-clone, and the second resolve finds the now-committed path. Headless/extended boxes already
|
||||
// resolved above (the IDD auto-activates with its OWN source) and skip this — which is the whole
|
||||
// point, since force-EXTEND's bare preset is ACCESS_DENIED from our service context there.
|
||||
//
|
||||
// CAVEAT (unobserved for IddCx, untested across GPU/driver/OS): textbook CCD also lets a clone
|
||||
// appear as a *shared-source ACTIVE* path (resolve → Some), which this `is_none()` gate would NOT
|
||||
// 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.
|
||||
if gdi_name.is_none() {
|
||||
// SAFETY: as above — `force_extend_topology` only calls `SetDisplayConfig` (CCD) with no
|
||||
// borrowed caller memory, under the `state` lock.
|
||||
unsafe { force_extend_topology() };
|
||||
for _ in 0..15 {
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
// SAFETY: as the resolve loop above.
|
||||
if let Some(n) = unsafe { resolve_gdi_name(added.target_id) } {
|
||||
gdi_name = Some(n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ccd_saved: Option<SavedConfig> = None;
|
||||
match &gdi_name {
|
||||
Some(n) => {
|
||||
|
||||
Reference in New Issue
Block a user