From a738de6cd8f4ee3d30c3ee55f896461131caeec0 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 16 Jul 2026 17:48:44 +0200 Subject: [PATCH] fix(host): force a CCD mode re-enumeration after UPDATE_MODES (in-place resize) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First on-glass run: the driver accepted every UpdateModes2 (0x0 in the driver log) but the OS never re-enumerated the target's settable modes on its own — 'OS did not advertise 800x1050 within 2s' → re-arrival fallback every time. Re-commit the current config with SDC_FORCE_MODE_ENUMERATION (the same nudge the isolate/layout paths already rely on) before the advertised-wait, re-kick up to 3x, and log the actually-offered resolutions when it still misses. Driver: dbglog the *2 mode-query/parse callbacks so the re-enumeration story is visible in pfvd-driver.log. Co-Authored-By: Claude Fable 5 --- .../src/vdisplay/windows/manager.rs | 30 ++++++++-- .../punktfunk-host/src/windows/win_display.rs | 56 +++++++++++++++++++ .../drivers/pf-vdisplay/src/callbacks.rs | 21 +++++++ 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/crates/punktfunk-host/src/vdisplay/windows/manager.rs b/crates/punktfunk-host/src/vdisplay/windows/manager.rs index 10033f52..98a44e85 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/manager.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/manager.rs @@ -1189,15 +1189,33 @@ impl VirtualDisplayManager { // SAFETY: `dev` is the live control handle (this fn's contract); `update_modes` forwards it // to a synchronous IOCTL with owned/borrowed locals only. unsafe { self.driver.update_modes(dev, &mon.key, mode) }?; - // The OS re-evaluates the target's settable modes asynchronously after UpdateModes2 — wait - // (bounded) for the new resolution to become enumerable before forcing it, else the - // CDS_TEST inside `set_active_mode` would reject it and silently keep the old mode. + // The OS does NOT re-evaluate an indirect display's settable modes on its own after + // UpdateModes2 (on-glass: the new mode never became enumerable within 2 s) — force a mode + // re-enumeration by re-committing the current config (the same SDC_FORCE_MODE_ENUMERATION + // re-commit the isolate/layout paths use), then wait for the new resolution to appear, + // re-kicking a couple of times. Without it the CDS_TEST inside `set_active_mode` would + // reject the mode and silently keep the old one. let t0 = Instant::now(); - if !crate::win_display::wait_mode_advertised(&gdi, mode, Duration::from_millis(2000)) { + let mut advertised = false; + for kick in 0..3u32 { + // SAFETY: CCD query/apply FFI under the held `state` lock (this fn's contract). + unsafe { crate::win_display::force_mode_reenumeration() }; + if crate::win_display::wait_mode_advertised(&gdi, mode, Duration::from_millis(1000)) { + advertised = true; + break; + } + tracing::debug!( + kick, + "in-place resize: new mode not yet enumerable — forcing another mode re-enumeration" + ); + } + if !advertised { anyhow::bail!( - "OS did not advertise {}x{} within 2s of the driver mode-list update", + "OS did not advertise {}x{} within {}ms of the driver mode-list update (offers: {:?})", mode.width, - mode.height + mode.height, + t0.elapsed().as_millis(), + crate::win_display::advertised_resolutions(&gdi) ); } let advertised_ms = t0.elapsed().as_millis() as u64; diff --git a/crates/punktfunk-host/src/windows/win_display.rs b/crates/punktfunk-host/src/windows/win_display.rs index e094364d..91059199 100644 --- a/crates/punktfunk-host/src/windows/win_display.rs +++ b/crates/punktfunk-host/src/windows/win_display.rs @@ -258,6 +258,62 @@ pub(crate) unsafe fn wait_mode_settled( } } +/// Re-commit the CURRENT active config with `SDC_FORCE_MODE_ENUMERATION` — the nudge that makes +/// the OS re-query an indirect display's target modes. Observed on-glass (P2): after +/// `IddCxMonitorUpdateModes2` the OS did NOT re-enumerate on its own within 2 s, so a freshly +/// advertised mode never became settable; the isolate/layout paths already re-commit with this +/// flag for the same "the OS won't re-evaluate unless told" class. Best-effort. +/// +/// # Safety +/// Runs the CCD query/apply FFI; call under the manager `state` lock (sole topology mutator). +pub(crate) unsafe fn force_mode_reenumeration() -> bool { + let Some((paths, modes)) = query_active_config() else { + return false; + }; + 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::debug!("force mode re-enumeration: SetDisplayConfig rc={rc:#x}"); + } + rc == 0 +} + +/// The distinct resolutions `gdi_name` currently advertises (diagnostics for the in-place-resize +/// path: what the OS actually offers when a requested mode never shows up). +pub(crate) fn advertised_resolutions(gdi_name: &str) -> Vec<(u32, u32)> { + let wname: Vec = gdi_name.encode_utf16().chain(std::iter::once(0)).collect(); + let mut set = std::collections::BTreeSet::new(); + let mut i = 0u32; + loop { + let mut dm = DEVMODEW { + dmSize: size_of::() as u16, + ..Default::default() + }; + // SAFETY: `wname` is a live NUL-terminated UTF-16 device name; `&mut dm` is a live, + // size-stamped DEVMODEW the API fills for mode index `i`. Both outlive the call. + let ok = unsafe { + EnumDisplaySettingsW( + PCWSTR(wname.as_ptr()), + ENUM_DISPLAY_SETTINGS_MODE(i), + &mut dm, + ) + } + .as_bool(); + if !ok { + break; + } + set.insert((dm.dmPelsWidth, dm.dmPelsHeight)); + i += 1; + } + set.into_iter().collect() +} + /// Wait (bounded) until `gdi_name` ADVERTISES `mode`'s resolution in its display-mode list — the /// gate between a driver-side mode-list refresh (`IOCTL_UPDATE_MODES`, latency plan P2) and the /// CCD/GDI force-set: the OS re-evaluates an indirect display's settable modes asynchronously after diff --git a/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs b/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs index 7001e45c..c3109b74 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs @@ -129,6 +129,16 @@ pub unsafe extern "C" fn parse_monitor_description2( return STATUS_NOT_FOUND; }; let count = crate::monitor::flatten(&modes).count() as u32; + // Bring-up/diagnostic visibility (P2): does the OS ever RE-parse the description after an + // UPDATE_MODES? The head mode names which list generation this call served. + if let Some(head) = crate::monitor::flatten(&modes).next() { + dbglog!( + "[pf-vd] parse_monitor_description2(id={id}): {count} modes, head {}x{}@{}", + head.width, + head.height, + head.refresh_rate + ); + } out_args.MonitorModeBufferOutputCount = count; if in_args.MonitorModeBufferInputCount < count { // A zero input count is a count-only probe (success); a non-zero too-small buffer is an error. @@ -204,6 +214,17 @@ pub unsafe extern "C" fn monitor_query_modes2( return STATUS_NOT_FOUND; }; let count = crate::monitor::flatten(&modes).count() as u32; + // Diagnostic visibility (P2): shows whether/when the OS re-queries target modes after an + // UPDATE_MODES (the head mode names the list generation this call served). + if let Some(head) = crate::monitor::flatten(&modes).next() { + dbglog!( + "[pf-vd] monitor_query_modes2: {count} modes, head {}x{}@{} (fill={})", + head.width, + head.height, + head.refresh_rate, + in_args.TargetModeBufferInputCount >= count + ); + } out_args.TargetModeBufferOutputCount = count; if in_args.TargetModeBufferInputCount >= count { // SAFETY: `pTargetModes` points to >= `count` IDDCX_TARGET_MODE2 entries.