diff --git a/crates/punktfunk-host/src/vdisplay/windows/manager.rs b/crates/punktfunk-host/src/vdisplay/windows/manager.rs index 98a44e85..a8ba2e30 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/manager.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/manager.rs @@ -268,8 +268,15 @@ pub(crate) struct VirtualDisplayManager { device: Mutex, watchdog_s: AtomicU32, /// The driver's handshake-reported protocol version (0 until the first open). The in-place - /// resize (latency plan P2) gates on `>= 4`; a v3 driver keeps the re-arrival path. + /// resize (latency plan P2) gates its UPDATE_MODES attempt on `>= 4`; a v3 driver keeps the + /// already-advertised fast path + the re-arrival fallback. driver_proto: AtomicU32, + /// Latched `true` after an UPDATE_MODES round-trip failed to make the new mode settable — + /// on-glass (build 26200) the OS pins a monitor's settable set at ARRIVAL (it re-parses our + /// description + re-queries target modes, then ignores both), so every further attempt for an + /// out-of-arrival-list mode would only waste ~1 s per resize before the same re-arrival + /// fallback. One attempt per process, in case a future OS build honors the refresh. + update_modes_futile: AtomicBool, /// Monotonic lease-generation counter (was the `MON_GEN` global). gen: AtomicU64, state: Mutex, @@ -301,6 +308,7 @@ pub(crate) fn init(driver: Box) -> &'static VirtualDisplayMa device: Mutex::new(DeviceSlot::default()), watchdog_s: AtomicU32::new(3), driver_proto: AtomicU32::new(0), + update_modes_futile: AtomicBool::new(false), gen: AtomicU64::new(1), state: Mutex::new(MgrInner::default()), setup_lock: Mutex::new(()), @@ -598,13 +606,14 @@ impl VirtualDisplayManager { _ => unreachable!("just matched Active"), }; if cur_mode != mode { - // IN-PLACE mode set first (latency plan P2, driver protocol >= 4): refresh the - // live monitor's advertised modes (IOCTL_UPDATE_MODES) + CCD-force the new mode — - // no REMOVE→ADD, so the monitor's OS identity (saved per-monitor DPI), the - // driver-side swap-chain machinery and the retained frame stash all survive, and - // the whole hotplug cost (departure settle + activation ladder + re-isolate) - // disappears. Any failure falls through to the proven re-arrival below. - if self.driver_proto.load(Ordering::Relaxed) >= 4 { + // IN-PLACE mode set first (latency plan P2): an already-advertised resolution + // (arrival list + the driver's same-id mode history) is CCD-forced on the SAME + // monitor — no REMOVE→ADD, so the monitor's OS identity (saved per-monitor DPI), + // the driver-side swap-chain machinery and the retained frame stash all survive, + // and the whole hotplug cost (departure settle + activation ladder + re-isolate) + // disappears. An out-of-list mode fails FAST (see `resize_in_place`) and falls + // through to the proven re-arrival below. + { let in_place = { let Some(SlotState::Active { mon, refs }) = inner.slots.get_mut(&slot) else { @@ -629,10 +638,13 @@ impl VirtualDisplayManager { Some(out) } Err(e) => { - tracing::warn!( + // Expected-normal for a first-seen arbitrary size (the OS pins + // settable modes at arrival; the re-arrival teaches it) — info, + // not warn. + tracing::info!( slot, - error = %format!("{e:#}"), - "in-place resize failed — falling back to monitor re-arrival" + reason = %format!("{e:#}"), + "in-place resize not possible — monitor re-arrival" ); None } @@ -1177,46 +1189,61 @@ impl VirtualDisplayManager { .gdi_name .clone() .context("in-place resize needs a resolved GDI name")?; - tracing::info!( - old = format!( - "{}x{}@{}", - mon.mode.width, mon.mode.height, mon.mode.refresh_hz - ), - new = format!("{}x{}@{}", mode.width, mode.height, mode.refresh_hz), - target = mon.target_id, - "virtual-display: updating the live monitor's modes for an in-place resize" - ); - // 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 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(); - let mut advertised = false; - for kick in 0..3u32 { + // FAST PATH (driver-independent): the OS already offers this resolution — the monitor's + // arrival list, which since the driver's mode-history union contains every size this + // identity ever served — so a plain CCD mode set reaches it with no driver round-trip. + let already = crate::win_display::wait_mode_advertised(&gdi, mode, Duration::ZERO); + if !already { + // Out-of-arrival-list mode. On-glass (build 26200) the OS re-parses our description + // AND re-queries target modes after UpdateModes2 — our callbacks served the fresh + // list — yet the SETTABLE set stays pruned to the arrival list: the monitor + // source-mode set is pinned at arrival. So one bounded UPDATE_MODES attempt per + // process (in case a future build honors the refresh), then latch it futile and fail + // fast to the re-arrival — whose same-id history union makes THIS size settable in + // place from then on. + if self.driver_proto.load(Ordering::Relaxed) < 4 { + anyhow::bail!( + "{}x{} is not in the advertised mode set (v3 driver: in-place reaches only \ + arrival-list modes)", + mode.width, + mode.height + ); + } + if self.update_modes_futile.load(Ordering::Relaxed) { + anyhow::bail!( + "{}x{} is not in the advertised mode set (UPDATE_MODES latched futile — the \ + OS pins settable modes at monitor arrival; the re-arrival teaches this size \ + to the identity's history)", + mode.width, + mode.height + ); + } + tracing::info!( + old = format!( + "{}x{}@{}", + mon.mode.width, mon.mode.height, mon.mode.refresh_hz + ), + new = format!("{}x{}@{}", mode.width, mode.height, mode.refresh_hz), + target = mon.target_id, + "virtual-display: updating the live monitor's modes for an in-place resize" + ); + // 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) }?; // 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; + if !crate::win_display::wait_mode_advertised(&gdi, mode, Duration::from_millis(800)) { + self.update_modes_futile.store(true, Ordering::Relaxed); + anyhow::bail!( + "OS did not advertise {}x{} within {}ms of the driver mode-list update \ + (offers: {:?}) — latching UPDATE_MODES off for this process", + mode.width, + mode.height, + t0.elapsed().as_millis(), + crate::win_display::advertised_resolutions(&gdi) + ); } - 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 {}ms of the driver mode-list update (offers: {:?})", - mode.width, - mode.height, - t0.elapsed().as_millis(), - crate::win_display::advertised_resolutions(&gdi) - ); } let advertised_ms = t0.elapsed().as_millis() as u64; set_active_mode(&gdi, mode); diff --git a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs index a92ced32..31c72542 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs @@ -146,6 +146,41 @@ pub fn reap_orphaned(grace: Duration) -> usize { n } +/// Append `from`'s modes to `into`, skipping resolutions already present, capped at +/// [`MODE_LIST_CAP`] — the accumulate half of the union semantics (see [`update_monitor_modes`]). +fn union_modes(into: &mut Vec, from: &[Mode]) { + for m in from { + if into.len() >= MODE_LIST_CAP { + break; + } + if !into + .iter() + .any(|e| (e.width, e.height) == (m.width, m.height)) + { + into.push(m.clone()); + } + } +} + +/// The last advertised mode list of a DEPARTED monitor, per monitor id — consumed by the next +/// same-id [`create_monitor`] so a re-arrived monitor's ARRIVAL list already contains every mode +/// its predecessor ever served. The OS pins a monitor's settable set at arrival (see +/// [`update_monitor_modes`]), so this is what makes a windowed↔fullscreen cycle (or any return to +/// a previously-used size) an IN-PLACE mode set instead of another hotplug. In-process only (a +/// WUDFHost restart forgets it — harmless, the next resizes re-teach it); bounded: ≤ 16 ids × +/// [`MODE_LIST_CAP`] modes. +static MODE_HISTORY: Mutex)>> = Mutex::new(Vec::new()); + +/// Record a departing monitor's advertised list for its id ([`MODE_HISTORY`]). +fn remember_modes(id: u32, modes: &[Mode]) { + let mut hist = MODE_HISTORY.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(slot) = hist.iter_mut().find(|(i, _)| *i == id) { + slot.1 = modes.to_vec(); + } else { + hist.push((id, modes.to_vec())); + } +} + /// Fallback modes appended after the requested mode, so a topology change still has options. fn default_modes() -> Vec { vec![ @@ -494,6 +529,15 @@ pub fn create_monitor( let id = { let mut lock = lock_monitors(); let id = resolve_id(&lock, preferred_id); + // Same-id mode history (P2 union semantics): a RE-ARRIVED monitor advertises every mode + // its departed predecessor served, so the OS's arrival-pinned settable set already + // contains them — a return to any previously-used size is then an IN-PLACE mode set. + { + let hist = MODE_HISTORY.lock().unwrap_or_else(|e| e.into_inner()); + if let Some((_, prev)) = hist.iter().find(|(i, _)| *i == id) { + union_modes(&mut modes, prev); + } + } lock.push(MonitorObject { object: None, id, @@ -598,28 +642,34 @@ pub fn create_monitor( Some((id, target_id, luid_low, luid_high)) } +/// How many distinct resolutions a monitor's advertised list may accumulate (the requested head + +/// history + the built-in fallbacks). Bounds the union growth across many resizes; the OLDEST +/// history entries fall off first. +const MODE_LIST_CAP: usize = 12; + /// `IOCTL_UPDATE_MODES` (v4): refresh the LIVE monitor's advertised mode list to lead with a new -/// preferred mode (+ the same [`default_modes`] fallbacks ADD produces) and push the new TARGET -/// mode list to the OS via `IddCxMonitorUpdateModes2` — the in-place mid-stream resize -/// (`design/first-frame-and-resize-latency.md` P2). No departure: the monitor's OS identity, its -/// swap-chain worker and the retained frame stash all survive; the OS re-evaluates the target's -/// settable modes and the HOST then CCD-forces the new mode active. The `*2` (HDR) DDI matches the -/// `*2` mode/buffer family this driver already requires (IddCx 1.10), so it adds no new OS floor. +/// preferred mode and push the new TARGET mode list to the OS via `IddCxMonitorUpdateModes2` — +/// the in-place mid-stream resize (`design/first-frame-and-resize-latency.md` P2). No departure: +/// the monitor's OS identity, its swap-chain worker and the retained frame stash all survive. +/// The `*2` (HDR) DDI matches the `*2` mode/buffer family this driver already requires +/// (IddCx 1.10), so it adds no new OS floor. +/// +/// UNION semantics (on-glass finding, build 26200): the OS re-parses the description AND +/// re-queries target modes after `UpdateModes2` — our callbacks served the fresh list — yet the +/// SETTABLE set stays pruned to the modes known at monitor ARRIVAL (the monitor source-mode set +/// is pinned then). So replacing the list can only ever LOSE settable modes (v1 of this op +/// dropped the arrival mode from the target list, breaking even a resize BACK to it); the update +/// therefore accumulates — new mode first, every previously-advertised mode kept (deduped by +/// resolution, capped at [`MODE_LIST_CAP`]) — and the real payoff is at the NEXT re-arrival, +/// where [`create_monitor`]'s same-id history union makes every previously-used mode settable. /// /// The stored list is updated FIRST (under the lock) so any OS re-query through the mode DDIs /// ([`modes_for_object`]/[`modes_for_id`]) sees the new list, and REVERTED if the DDI fails — the /// OS then still holds the old list and the two stay coherent. The DDI itself is called OUTSIDE /// the lock (it may re-enter the mode-query callbacks, which lock [`MONITOR_MODES`]). pub fn update_monitor_modes(session_id: u64, width: u32, height: u32, refresh: u32) -> NTSTATUS { - let mut new_modes = vec![Mode { - width, - height, - refresh_rates: vec![refresh], - }]; - new_modes.extend(default_modes()); - - // Swap the stored list + grab the live handle under the lock. - let (object, old_modes) = { + // Swap the stored list (union — see above) + grab the live handle under the lock. + let (object, old_modes, new_modes) = { let mut lock = lock_monitors(); let Some(m) = lock.iter_mut().find(|m| m.session_id == session_id) else { return crate::STATUS_NOT_FOUND; @@ -627,8 +677,14 @@ pub fn update_monitor_modes(session_id: u64, width: u32, height: u32, refresh: u let Some(object) = m.object else { return crate::STATUS_NOT_FOUND; // created but not yet arrived — nothing to update }; + let mut new_modes = vec![Mode { + width, + height, + refresh_rates: vec![refresh], + }]; + union_modes(&mut new_modes, &m.modes); let old = core::mem::replace(&mut m.modes, new_modes.clone()); - (object, old) + (object, old, new_modes) }; // The OS's target-mode list for this monitor (the `*2`/HDR shape, like `monitor_query_modes2`). @@ -668,6 +724,9 @@ pub fn remove_monitor(session_id: u64) -> bool { return false; }; let mut entry = lock.remove(pos); + // Keep the departing monitor's advertised list for its id — the next same-id create + // unions it back in (P2 mode history; see MODE_HISTORY). + remember_modes(entry.id, &entry.modes); (entry.object, entry.swap_chain_processor.take()) }; // Drop the worker FIRST (it joins + deletes the swap-chain), THEN depart the monitor.