perf(host): in-place mid-stream resize — mode-set the live monitor, keep the capturer

Latency plan P2.2/P2.3: against a v4 driver the manager's resize branch now
runs UPDATE_MODES -> wait-mode-advertised (the OS re-enumerates async) ->
set_active_mode -> verified-state settle (P0.2) on the SAME monitor — no
REMOVE->ADD hotplug, no departure settle, no activation ladder, no re-isolate;
Windows keeps the per-monitor DPI (identity preserved). Any failure (v3
driver, mode never advertised, settle miss) falls back to the proven
re-arrival path unchanged.

On top of that the session's resize handler keeps the WHOLE capture pipeline:
the IDD-push capturer re-sizes its ring immediately (Capturer::resize_output —
no DescriptorPoller two-strike debounce, which stays for EXTERNAL changes),
the driver re-attaches and the mode-set full redraw provides the first frame;
only the encoder is swapped once the first new-size frame arrives
(open_video is ms-scale — P2.4 deliberately skipped). The capturer, send
thread and session transport all survive; every decline routes to the full
rebuild. Resize-trace stages (display_resized, ring_recreated,
first_new_frame, encoder_open) extend the P0.1 timeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 17:12:14 +02:00
parent 0899e53903
commit c2b9b32904
6 changed files with 487 additions and 78 deletions
+18
View File
@@ -246,6 +246,24 @@ pub trait Capturer: Send {
fn pipeline_depth(&self) -> usize { fn pipeline_depth(&self) -> usize {
1 1
} }
/// The OS display-target id this capturer is bound to (Windows IDD-push), so the resize path
/// can verify the display it just reconfigured is STILL the one this capturer serves (an
/// in-place resize keeps the target; a re-arrival fallback mints a new one, which needs a
/// fresh capturer). `None` = the backend has no such identity (every non-IDD backend).
fn capture_target_id(&self) -> Option<u32> {
None
}
/// HOST-INITIATED output resize (latency plan P2.3): the session's resize handler has ALREADY
/// committed the display's new mode (the manager's in-place mode set), so a capable capturer
/// re-sizes its capture surface NOW — no descriptor-poll debounce (that machinery stays, for
/// EXTERNAL changes only) and no teardown: the capture pipeline and its send thread survive;
/// only the encoder is swapped by the caller once the first new-size frame arrives. Returns
/// `true` when handled; `false` (the default) routes the caller to the full-rebuild path.
fn resize_output(&mut self, _width: u32, _height: u32) -> bool {
false
}
} }
/// A deterministic moving test pattern (BGRx). Lets the spike exercise the encode → file → /// A deterministic moving test pattern (BGRx). Lets the spike exercise the encode → file →
@@ -1903,6 +1903,39 @@ impl Capturer for IddPushCapturer {
// always has its own texture). // always has its own texture).
crate::config::config().idd_depth.clamp(1, OUT_RING) crate::config::config().idd_depth.clamp(1, OUT_RING)
} }
fn capture_target_id(&self) -> Option<u32> {
Some(self.target_id)
}
fn resize_output(&mut self, width: u32, height: u32) -> bool {
// Host-initiated resize (latency plan P2.3): the session's resize handler has already
// committed the display's new mode (the manager's in-place mode set), so recreate the ring
// at the new size NOW — no DescriptorPoller two-strike debounce (that stays, unchanged,
// for EXTERNAL changes: HDR flips, game mode-sets). The driver re-attaches to the fresh
// ring and republishes; on an in-place mode set the OS's mode-set full redraw gives the
// stash/first frame within the recover window. Same recover-or-drop arming as the
// poller-driven recreate, so a ring that can't re-attach still fails the session cleanly
// instead of freezing.
if (width, height) == (self.width, self.height) {
return true; // already at the requested size (refresh-only change) — nothing to do
}
tracing::info!(
target_id = self.target_id,
from = format!("{}x{}", self.width, self.height),
to = format!("{width}x{height}"),
"IDD push: host-initiated resize — recreating the ring at the new mode"
);
self.recovering_since.get_or_insert_with(Instant::now);
if let Err(e) = self.recreate_ring(self.display_hdr, width, height) {
tracing::warn!(
error = %format!("{e:#}"),
"IDD push: host-initiated ring recreate failed — falling back to a full rebuild"
);
return false;
}
true
}
} }
/// A 4:4:4 session while the display is HDR: there is no 10-bit full-chroma source (the FP16 /// A 4:4:4 session while the display is HDR: there is no 10-bit full-chroma source (the FP16
+203 -64
View File
@@ -4616,81 +4616,111 @@ fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDisplay>) -> Res
} else { } else {
bitrate_kbps bitrate_kbps
}; };
// Build the new pipeline BEFORE dropping the old one: the host already acked // IN-PLACE fast path first (latency plan P2.3, Windows IDD-push): keep the capturer +
// the switch as accepted, so a rebuild failure must not kill an otherwise // send thread, mode-set the SAME monitor in place (P2.1/P2.2), resize the ring, swap
// only the encoder. Any decline (v3 driver → the manager re-arrived, ring recreate
// failed, no new-size frame) falls through to the full rebuild below.
#[cfg(target_os = "windows")]
let fast_done = plan.capture == crate::session_plan::CaptureBackend::IddPush
&& try_inplace_resize(
&mut vd,
&mut capturer,
&mut enc,
&mut frame,
&mut interval,
new_mode,
mode_bitrate,
bit_depth,
plan,
&quit,
resize_trace.as_ref(),
);
#[cfg(not(target_os = "windows"))]
let fast_done = false;
// Full rebuild — build the new pipeline BEFORE dropping the old one: the host already
// acked the switch as accepted, so a rebuild failure must not kill an otherwise
// healthy session — keep streaming the current mode and log instead. // healthy session — keep streaming the current mode and log instead.
match build_pipeline( let rebuilt = fast_done
&mut vd, || match build_pipeline(
new_mode, &mut vd,
mode_bitrate, new_mode,
bit_depth, mode_bitrate,
plan, bit_depth,
&quit, plan,
Some(resize_trace.as_ref()), &quit,
) { Some(resize_trace.as_ref()),
Ok(next_pipe) => { ) {
if mode_bitrate != bitrate_kbps { Ok(next_pipe) => {
tracing::info!( let old_display_gen = cur_display_gen;
from_kbps = bitrate_kbps, // The destructuring assignment drops the OLD capturer (→ its display lease)
to_kbps = mode_bitrate, // as each binding is replaced — the new pipeline is already up
"pinned PyroWave bitrate re-resolved for the new mode" // (create-before-drop).
); (capturer, enc, frame, interval, cur_node_id, cur_display_gen) = next_pipe;
bitrate_kbps = mode_bitrate; // H4: the old display's lease drop above is indistinguishable from a
live_bitrate.store(mode_bitrate, Ordering::Relaxed); // disconnect to the keep-alive machinery — under linger/forever policies
// every resize would ACCUMULATE kept monitors at stale modes. Retire the
// superseded entry now (a no-op when it was already torn down under
// `immediate`, or off Linux; the in-place fast path keeps the SAME display,
// so it has nothing to retire).
if let Some(g) = old_display_gen.filter(|g| cur_display_gen != Some(*g)) {
crate::vdisplay::registry::retire(g);
}
true
} }
let old_display_gen = cur_display_gen; Err(e) => {
// The destructuring assignment drops the OLD capturer (→ its display lease) as tracing::warn!(error = %format!("{e:#}"), ?new_mode,
// each binding is replaced — the new pipeline is already up (create-before-drop). "mode-switch rebuild failed — staying on the current mode");
(capturer, enc, frame, interval, cur_node_id, cur_display_gen) = next_pipe; // H2 rollback: the control task acked the switch BEFORE this rebuild, so the
cur_mode = new_mode; // client's mode slot already flipped to `new_mode`. A second accepted ack
next = std::time::Instant::now(); // carrying the still-live mode corrects it (any accepted ack means "the
// H4: the old display's lease drop above is indistinguishable from a disconnect // active mode is now X" client-side; old clients just log it). `frame` is
// to the keep-alive machinery — under linger/forever policies every resize would // untouched here (the fast path returned false before swapping anything and
// ACCUMULATE kept monitors at stale modes. Retire the superseded entry now (a // the destructure only runs on the Ok arm), so it's still the OLD
// no-op when it was already torn down under `immediate`, or off Linux). // pipeline's frame — its real dims + interval are what's still on glass.
if let Some(g) = old_display_gen.filter(|g| cur_display_gen != Some(*g)) {
crate::vdisplay::registry::retire(g);
}
// H2/H3: the backend may have honored a different mode than requested — KWin
// caps a virtual output's refresh, or Windows pf-vdisplay rejects an in-place
// SetMode to a resolution its running monitor doesn't advertise and the host
// falls back to the actual display mode. `frame` is the NEW pipeline's first
// frame (just rebound above), so its dims are what the client actually decodes.
// Publish that ACTUAL mode to the live stats slot, and correct the client's mode
// slot when it differs from the accept ack it already got.
let actual = delivered_mode(frame.width, frame.height, interval);
live_mode.store(
pack_mode(actual.width, actual.height, actual.refresh_hz),
Ordering::Relaxed,
);
if actual != new_mode {
let _ = reconfig_result_tx.send(Reconfigured { let _ = reconfig_result_tx.send(Reconfigured {
accepted: true, accepted: true,
mode: actual, mode: delivered_mode(frame.width, frame.height, interval),
}); });
false
} }
// The owed AUs died with the old encoder — drop their in-flight records };
// and restart the encode-stall clock for the fresh one. if rebuilt {
inflight.clear(); if mode_bitrate != bitrate_kbps {
last_au_at = std::time::Instant::now(); tracing::info!(
encoder_resets = 0; from_kbps = bitrate_kbps,
last_forced_idr = Some(std::time::Instant::now()); // fresh encoder opens on an IDR — anchor the cooldown to_kbps = mode_bitrate,
resize_trace.finish("pipeline_rebuilt"); "pinned PyroWave bitrate re-resolved for the new mode"
);
bitrate_kbps = mode_bitrate;
live_bitrate.store(mode_bitrate, Ordering::Relaxed);
} }
Err(e) => { cur_mode = new_mode;
tracing::warn!(error = %format!("{e:#}"), ?new_mode, next = std::time::Instant::now();
"mode-switch rebuild failed — staying on the current mode"); // H2/H3: the backend may have honored a different mode than requested — KWin caps
// H2 rollback: the control task acked the switch BEFORE this rebuild, so the // a virtual output's refresh, or Windows pf-vdisplay rejects a resolution its
// client's mode slot already flipped to `new_mode`. A second accepted ack // running monitor doesn't advertise and the host falls back to the actual display
// carrying the still-live mode corrects it (any accepted ack means "the active // mode. `frame` is the NEW pipeline's first frame (just rebound above), so its
// mode is now X" client-side; old clients just log it). `frame` is untouched // dims are what the client actually decodes. Publish that ACTUAL mode to the live
// here (the destructure only runs on the Ok arm), so it's still the OLD // stats slot, and correct the client's mode slot when it differs from the accept
// pipeline's frame — its real dims + interval are exactly what's still on glass. // ack it already got.
let actual = delivered_mode(frame.width, frame.height, interval);
live_mode.store(
pack_mode(actual.width, actual.height, actual.refresh_hz),
Ordering::Relaxed,
);
if actual != new_mode {
let _ = reconfig_result_tx.send(Reconfigured { let _ = reconfig_result_tx.send(Reconfigured {
accepted: true, accepted: true,
mode: delivered_mode(frame.width, frame.height, interval), mode: actual,
}); });
} }
// The owed AUs died with the old encoder — drop their in-flight records
// and restart the encode-stall clock for the fresh one.
inflight.clear();
last_au_at = std::time::Instant::now();
encoder_resets = 0;
last_forced_idr = Some(std::time::Instant::now()); // fresh encoder opens on an IDR — anchor the cooldown
resize_trace.finish("pipeline_rebuilt");
} }
} }
// Adaptive bitrate: drain to the NEWEST requested rate (the client's controller may step // Adaptive bitrate: drain to the NEWEST requested rate (the client's controller may step
@@ -5346,6 +5376,115 @@ type Pipeline = (
/// error chain is classified and permanent ones short-circuit. Each failed attempt drops its /// error chain is classified and permanent ones short-circuit. Each failed attempt drops its
/// capturer, which (via `PortalCapturer::Drop`) tears the PipeWire thread + virtual output down /// capturer, which (via `PortalCapturer::Drop`) tears the PipeWire thread + virtual output down
/// before the next attempt — no leak across retries. /// before the next attempt — no leak across retries.
/// The in-place resize fast path (latency plan P2.3, Windows IDD-push): the manager mode-sets the
/// SAME monitor in place (driver protocol v4 — `IOCTL_UPDATE_MODES`; internally falls back to
/// re-arrival against an older driver), then the existing capturer re-sizes its ring immediately
/// (no descriptor-poll debounce) and only the ENCODER is swapped once the first new-size frame
/// arrives — the capture pipeline, its send thread and the whole session transport survive.
/// Returns `true` when the stream is now delivering the new mode on the same capturer; `false`
/// routes the caller to the full rebuild (which is also the correct path when the manager had to
/// re-arrive a fresh monitor — this capturer's ring/broker are bound to the departed target).
#[cfg(target_os = "windows")]
#[allow(clippy::too_many_arguments)]
fn try_inplace_resize(
vd: &mut Box<dyn crate::vdisplay::VirtualDisplay>,
capturer: &mut Box<dyn crate::capture::Capturer>,
enc: &mut Box<dyn crate::encode::Encoder>,
frame: &mut crate::capture::CapturedFrame,
interval: &mut std::time::Duration,
new_mode: punktfunk_core::Mode,
bitrate_kbps: u32,
bit_depth: u8,
plan: crate::session_plan::SessionPlan,
quit: &Arc<AtomicBool>,
trace: &crate::bringup::Trace,
) -> bool {
let Some(cur_target) = capturer.capture_target_id() else {
return false; // not an IDD-push capturer — nothing to reuse
};
// Acquire at the new mode: the manager's resize branch runs the in-place mode set (or its
// re-arrival fallback) and returns a +1-ref lease, released again when `vout` drops below —
// the capturer keeps holding its own original lease (`gen` is preserved by both paths).
let vout = match crate::vdisplay::registry::acquire(vd, new_mode, quit.clone()) {
Ok(v) => v,
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), "in-place resize: acquire failed");
return false;
}
};
trace.mark("display_resized");
let effective_hz = vout
.preferred_mode
.map(|(_, _, hz)| hz)
.filter(|&hz| hz > 0)
.unwrap_or(new_mode.refresh_hz);
if vout.win_capture.as_ref().map(|t| t.target_id) != Some(cur_target) {
// The manager re-arrived a fresh monitor (old driver / in-place failure): this capturer is
// bound to the departed target. The full rebuild re-acquires (JOINing the already-resized
// monitor) with a fresh capturer.
tracing::info!(
"resize: monitor re-arrived (no in-place support) — running the full pipeline rebuild"
);
return false;
}
if !capturer.resize_output(new_mode.width, new_mode.height) {
return false;
}
trace.mark("ring_recreated");
// Bounded wait for the first frame at the new size (the driver re-attaches to the fresh ring;
// the mode-set full redraw composes promptly). Mirrors the capturer's own 3 s recover-or-drop.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
let new_frame = loop {
match capturer.try_latest() {
Ok(Some(f)) if (f.width, f.height) == (new_mode.width, new_mode.height) => break f,
Ok(_) => {
if std::time::Instant::now() >= deadline {
tracing::warn!(
"resize: no new-size frame within 3s of the in-place mode set — running \
the full pipeline rebuild"
);
return false;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
Err(e) => {
tracing::warn!(error = %format!("{e:#}"),
"resize: capture failed after the in-place mode set — running the full rebuild");
return false;
}
}
};
trace.mark("first_new_frame");
// Fresh encoder at the delivered size — the one component that can't follow a resolution
// change in place today (P2.4 stays unimplemented: `open_video` is ms-scale, measured).
let mut new_enc = match crate::encode::open_video(
plan.codec,
new_frame.format,
new_frame.width,
new_frame.height,
effective_hz,
bitrate_kbps as u64 * 1000,
new_frame.is_cuda(),
bit_depth,
plan.chroma,
) {
Ok(e) => e,
Err(e) => {
tracing::warn!(error = %format!("{e:#}"),
"resize: encoder open failed after the in-place mode set — running the full rebuild");
return false;
}
};
if let Some(c) = plan.wire_chunk {
new_enc.set_wire_chunking(c);
}
*enc = new_enc;
*frame = new_frame;
*interval = std::time::Duration::from_secs_f64(1.0 / effective_hz.max(1) as f64);
trace.mark("encoder_open");
true
}
/// The Welcome-time display-prep hand-off (latency plan P1.1/P1.2): the opened vdisplay backend + /// The Welcome-time display-prep hand-off (latency plan P1.1/P1.2): the opened vdisplay backend +
/// the fully built pipeline — monitor create, activation, settle, capture attach, first frame, /// the fully built pipeline — monitor create, activation, settle, capture attach, first frame,
/// encoder open — produced on the prep/stream thread while the client's Start round-trip and the /// encoder open — produced on the prep/stream thread while the client's Start round-trip and the
@@ -68,11 +68,12 @@ pub(crate) trait VdisplayDriver: Send + Sync {
/// timeout. `reap_orphans` (the FIRST open of the process only) additionally `CLEAR_ALL`s /// timeout. `reap_orphans` (the FIRST open of the process only) additionally `CLEAR_ALL`s
/// monitors orphaned by a crashed previous host — a REOPEN (after a dead handle was retired) /// monitors orphaned by a crashed previous host — a REOPEN (after a dead handle was retired)
/// must NOT, since sessions this process still considers live may be racing it. Returns the /// must NOT, since sessions this process still considers live may be racing it. Returns the
/// owned handle + watchdog seconds. /// owned handle + watchdog seconds + the driver's reported protocol version (the in-place
/// resize gates on it).
/// ///
/// # Safety /// # Safety
/// Issues setup-API + `DeviceIoControl` calls; runs in the caller's apartment. /// Issues setup-API + `DeviceIoControl` calls; runs in the caller's apartment.
unsafe fn open(&self, reap_orphans: bool) -> Result<(OwnedHandle, u32)>; unsafe fn open(&self, reap_orphans: bool) -> Result<(OwnedHandle, u32, u32)>;
/// ADD a virtual monitor at `mode`, pinning the IDD render GPU to `render_luid` first if `Some`, and /// ADD a virtual monitor at `mode`, pinning the IDD render GPU to `render_luid` first if `Some`, and
/// requesting `preferred_monitor_id` (the host's per-client stable id; `0` = auto). `client_hdr` /// requesting `preferred_monitor_id` (the host's per-client stable id; `0` = auto). `client_hdr`
/// is the CLIENT display's HDR volume for the monitor's EDID CTA HDR block (`None` = the /// is the CLIENT display's HDR volume for the monitor's EDID CTA HDR block (`None` = the
@@ -90,6 +91,17 @@ pub(crate) trait VdisplayDriver: Send + Sync {
preferred_monitor_id: u32, preferred_monitor_id: u32,
client_hdr: Option<punktfunk_core::quic::HdrMeta>, client_hdr: Option<punktfunk_core::quic::HdrMeta>,
) -> Result<AddedMonitor>; ) -> Result<AddedMonitor>;
/// Refresh the LIVE monitor `key`'s advertised mode list to lead with `mode` (the in-place
/// mid-stream resize, latency plan P2 — pf-vdisplay `IOCTL_UPDATE_MODES`, driver protocol v4).
/// The monitor is NOT departed; the caller CCD-forces the freshly-advertised mode afterwards.
/// The default errs so a backend without support routes to the re-arrival fallback.
///
/// # Safety
/// `dev` must be the live control handle.
unsafe fn update_modes(&self, dev: HANDLE, key: &MonitorKey, mode: Mode) -> Result<()> {
let _ = (dev, key, mode);
anyhow::bail!("backend does not support in-place mode updates")
}
/// REMOVE the monitor identified by `key`. /// REMOVE the monitor identified by `key`.
/// ///
/// # Safety /// # Safety
@@ -255,6 +267,9 @@ pub(crate) struct VirtualDisplayManager {
/// `&'static` singleton with no raw-handle smuggling. /// `&'static` singleton with no raw-handle smuggling.
device: Mutex<DeviceSlot>, device: Mutex<DeviceSlot>,
watchdog_s: AtomicU32, 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.
driver_proto: AtomicU32,
/// Monotonic lease-generation counter (was the `MON_GEN` global). /// Monotonic lease-generation counter (was the `MON_GEN` global).
gen: AtomicU64, gen: AtomicU64,
state: Mutex<MgrInner>, state: Mutex<MgrInner>,
@@ -285,6 +300,7 @@ pub(crate) fn init(driver: Box<dyn VdisplayDriver>) -> &'static VirtualDisplayMa
driver, driver,
device: Mutex::new(DeviceSlot::default()), device: Mutex::new(DeviceSlot::default()),
watchdog_s: AtomicU32::new(3), watchdog_s: AtomicU32::new(3),
driver_proto: AtomicU32::new(0),
gen: AtomicU64::new(1), gen: AtomicU64::new(1),
state: Mutex::new(MgrInner::default()), state: Mutex::new(MgrInner::default()),
setup_lock: Mutex::new(()), setup_lock: Mutex::new(()),
@@ -431,9 +447,10 @@ impl VirtualDisplayManager {
// FFI in the caller's apartment; the `device` mutex (held here) serializes it, so there is no // FFI in the caller's apartment; the `device` mutex (held here) serializes it, so there is no
// concurrent open. `open` has no handle precondition to uphold, and the `OwnedHandle` it // concurrent open. `open` has no handle precondition to uphold, and the `OwnedHandle` it
// returns is the sole owner of the device. // returns is the sole owner of the device.
let (handle, watchdog_s) = unsafe { self.driver.open(reap)? }; let (handle, watchdog_s, driver_proto) = unsafe { self.driver.open(reap)? };
slot.opened_once = true; slot.opened_once = true;
self.watchdog_s.store(watchdog_s, Ordering::Relaxed); self.watchdog_s.store(watchdog_s, Ordering::Relaxed);
self.driver_proto.store(driver_proto, Ordering::Relaxed);
let raw = HANDLE(handle.as_raw_handle()); let raw = HANDLE(handle.as_raw_handle());
slot.current = Some(Arc::new(handle)); slot.current = Some(Arc::new(handle));
if !reap { if !reap {
@@ -581,6 +598,53 @@ impl VirtualDisplayManager {
_ => unreachable!("just matched Active"), _ => unreachable!("just matched Active"),
}; };
if cur_mode != mode { 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 {
let in_place = {
let Some(SlotState::Active { mon, refs }) = inner.slots.get_mut(&slot)
else {
unreachable!("just matched Active");
};
// SAFETY: `dev` is the handle `ensure_device()` returned above; the CCD
// waits inside run under the held `state` lock (this fn's discipline).
match unsafe { self.resize_in_place(dev, mon, mode) } {
Ok(()) => {
// Same join semantics as the re-arrival: +1 ref for the new
// (build-then-drop overlap) lease; `gen` untouched, so the old
// session's lease stays valid.
*refs += 1;
let refs = *refs;
let out = self.output_for(slot, mon, quit.clone());
tracing::info!(
slot,
refs,
backend = self.driver.name(),
"virtual monitor resized IN PLACE (identity + swap-chain kept)"
);
Some(out)
}
Err(e) => {
tracing::warn!(
slot,
error = %format!("{e:#}"),
"in-place resize failed — falling back to monitor re-arrival"
);
None
}
}
};
if let Some(out) = in_place {
// The width changed — re-arrange the group so auto-row siblings don't
// overlap the resized display (no-op for a single member).
self.apply_group_layout(&mut inner);
return Ok(out);
}
}
let Some(SlotState::Active { mon, refs }) = inner.slots.remove(&slot) else { let Some(SlotState::Active { mon, refs }) = inner.slots.remove(&slot) else {
unreachable!("just matched Active"); unreachable!("just matched Active");
}; };
@@ -1096,6 +1160,68 @@ impl VirtualDisplayManager {
}) })
} }
/// Mid-stream resize IN PLACE (latency plan P2): the driver refreshes the LIVE monitor's
/// advertised target-mode list to lead with `mode` (`IOCTL_UPDATE_MODES` →
/// `IddCxMonitorUpdateModes2`, protocol v4), the OS re-enumerates the target's settable modes
/// (waited on, bounded), and the usual CCD/GDI force-set + verified settle (P0.2) commit it —
/// on the SAME monitor: target id, GDI name, saved per-monitor DPI, the driver's swap-chain
/// worker and its retained frame stash all survive (the OS reassigns the swap-chain across a
/// mode set; the preserved-publisher/stash hand-off covers that flap — what it was built for).
/// On success `mon.mode` is updated in place; any failure leaves `mon` untouched (still at the
/// old mode) and the caller falls back to [`re_add`](Self::re_add).
///
/// # Safety
/// `dev` must be the live control handle; runs the CCD/GDI FFI under the `state` lock.
unsafe fn resize_in_place(&self, dev: HANDLE, mon: &mut Monitor, mode: Mode) -> Result<()> {
let gdi = mon
.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 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.
let t0 = Instant::now();
if !crate::win_display::wait_mode_advertised(&gdi, mode, Duration::from_millis(2000)) {
anyhow::bail!(
"OS did not advertise {}x{} within 2s of the driver mode-list update",
mode.width,
mode.height
);
}
let advertised_ms = t0.elapsed().as_millis() as u64;
set_active_mode(&gdi, mode);
// Verified-state settle (P0.2): the same committed-state predicate as the create paths. A
// mode set that did not commit within the ceiling routes to the re-arrival fallback.
let settle_start = Instant::now();
// SAFETY: CCD/GDI query FFI over a `Copy` target id, under the held `state` lock.
let settled =
unsafe { wait_mode_settled(mon.target_id, mode, Duration::from_millis(1500)) };
if !settled {
anyhow::bail!(
"in-place mode set did not commit within 1.5s (advertised after {advertised_ms} ms)"
);
}
tracing::info!(
advertised_ms,
settle_ms = settle_start.elapsed().as_millis() as u64,
"in-place resize committed (verified-state wait)"
);
mon.mode = mode;
Ok(())
}
/// Mid-stream resize by monitor RE-ARRIVAL (`design/midstream-resolution-resize.md` Fix 1). /// Mid-stream resize by monitor RE-ARRIVAL (`design/midstream-resolution-resize.md` Fix 1).
/// ///
/// The pf-vdisplay driver freezes a monitor's advertised mode list at `IOCTL_ADD` time (the /// The pf-vdisplay driver freezes a monitor's advertised mode list at `IOCTL_ADD` time (the
@@ -344,7 +344,7 @@ impl VdisplayDriver for PfVdisplayDriver {
"pf-vdisplay" "pf-vdisplay"
} }
unsafe fn open(&self, reap_orphans: bool) -> Result<(OwnedHandle, u32)> { unsafe fn open(&self, reap_orphans: bool) -> Result<(OwnedHandle, u32, u32)> {
// SAFETY: `open_device` is `unsafe` only because it issues SetupAPI enumeration + `CreateFileW` // SAFETY: `open_device` is `unsafe` only because it issues SetupAPI enumeration + `CreateFileW`
// FFI; it takes no arguments and returns an owned raw `HANDLE` (or `Err`). Called here on the // FFI; it takes no arguments and returns an owned raw `HANDLE` (or `Err`). Called here on the
// backend-init thread, with no precondition beyond a valid thread context. // backend-init thread, with no precondition beyond a valid thread context.
@@ -390,27 +390,43 @@ impl VdisplayDriver for PfVdisplayDriver {
.context("pf-vdisplay IOCTL_GET_INFO (version handshake)")?; .context("pf-vdisplay IOCTL_GET_INFO (version handshake)")?;
let info: control::InfoReply = let info: control::InfoReply =
bytemuck::pod_read_unaligned(&info_buf[..size_of::<control::InfoReply>()]); bytemuck::pod_read_unaligned(&info_buf[..size_of::<control::InfoReply>()]);
if info.protocol_version != pf_driver_proto::PROTOCOL_VERSION { // HARD floor/ceiling instead of strict equality since v4: v4 is ADDITIVE over v3
// (IOCTL_UPDATE_MODES — the in-place resize), so this host still drives a v3 driver and
// simply gates the in-place path on the reported version (re-arrival fallback). Anything
// below the floor or ABOVE this host's own version stays a loud failure.
if info.protocol_version < pf_driver_proto::MIN_DRIVER_PROTOCOL_VERSION
|| info.protocol_version > pf_driver_proto::PROTOCOL_VERSION
{
anyhow::bail!( anyhow::bail!(
"pf-vdisplay protocol mismatch: host expects {}, driver reports {} — install matching \ "pf-vdisplay protocol mismatch: host drives {}..={}, driver reports {} — install \
host + driver", matching host + driver",
pf_driver_proto::MIN_DRIVER_PROTOCOL_VERSION,
pf_driver_proto::PROTOCOL_VERSION, pf_driver_proto::PROTOCOL_VERSION,
info.protocol_version info.protocol_version
); );
} }
let watchdog_s = info.watchdog_timeout_s.max(1); let watchdog_s = info.watchdog_timeout_s.max(1);
tracing::info!( if info.protocol_version < pf_driver_proto::PROTOCOL_VERSION {
"pf-vdisplay protocol {} (watchdog timeout {}s)", tracing::warn!(
info.protocol_version, "pf-vdisplay protocol {} (host supports {}): driver lacks the in-place resize — \
watchdog_s mid-stream resizes use the monitor re-arrival path until the driver is updated",
); info.protocol_version,
pf_driver_proto::PROTOCOL_VERSION
);
} else {
tracing::info!(
"pf-vdisplay protocol {} (watchdog timeout {}s)",
info.protocol_version,
watchdog_s
);
}
// Reap monitors orphaned by a crashed previous host — a FIRST-CLASS op (driver returns // Reap monitors orphaned by a crashed previous host — a FIRST-CLASS op (driver returns
// SUCCESS). FIRST open of the process only: a REOPEN (the manager retired a dead handle after // SUCCESS). FIRST open of the process only: a REOPEN (the manager retired a dead handle after
// a driver upgrade / WUDFHost restart) can race sessions that still believe they are live, and // a driver upgrade / WUDFHost restart) can race sessions that still believe they are live, and
// an unconditional CLEAR_ALL there would raze them. // an unconditional CLEAR_ALL there would raze them.
if !reap_orphans { if !reap_orphans {
reap_ghost_monitors(); reap_ghost_monitors();
return Ok((device, watchdog_s)); return Ok((device, watchdog_s, info.protocol_version));
} }
let mut none: [u8; 0] = []; let mut none: [u8; 0] = [];
// SAFETY: `raw` borrows the live `OwnedHandle` above. `IOCTL_CLEAR_ALL` has no input and no // SAFETY: `raw` borrows the live `OwnedHandle` above. `IOCTL_CLEAR_ALL` has no input and no
@@ -427,7 +443,7 @@ impl VdisplayDriver for PfVdisplayDriver {
// monitor-slot budget — prevents the 0x80070490 slot-exhaustion wedge from carrying across // monitor-slot budget — prevents the 0x80070490 slot-exhaustion wedge from carrying across
// restarts (the reason a restart's CLEAR_ALL alone never recovered it before). // restarts (the reason a restart's CLEAR_ALL alone never recovered it before).
reap_ghost_monitors(); reap_ghost_monitors();
Ok((device, watchdog_s)) Ok((device, watchdog_s, info.protocol_version))
} }
unsafe fn add_monitor( unsafe fn add_monitor(
@@ -577,6 +593,38 @@ impl VdisplayDriver for PfVdisplayDriver {
}) })
} }
unsafe fn update_modes(&self, dev: HANDLE, key: &MonitorKey, mode: Mode) -> Result<()> {
let MonitorKey::Session(session_id) = key else {
anyhow::bail!("pf-vdisplay: unexpected monitor key kind");
};
let req = control::UpdateModesRequest {
session_id: *session_id,
width: mode.width,
height: mode.height,
refresh_hz: mode.refresh_hz,
_reserved: 0,
};
let mut none: [u8; 0] = [];
// SAFETY: per `update_modes`'s contract `dev` is the live control handle. `bytes_of(&req)`
// borrows the local `UpdateModesRequest` for the duration of this synchronous call as the
// input bytes; `none` is empty, so there is no output buffer.
unsafe {
ioctl(
dev,
control::IOCTL_UPDATE_MODES,
bytemuck::bytes_of(&req),
&mut none,
)
}
.map(|_| ())
.with_context(|| {
format!(
"pf-vdisplay UPDATE_MODES {}x{}@{}",
mode.width, mode.height, mode.refresh_hz
)
})
}
unsafe fn remove_monitor(&self, dev: HANDLE, key: &MonitorKey) -> Result<()> { unsafe fn remove_monitor(&self, dev: HANDLE, key: &MonitorKey) -> Result<()> {
let MonitorKey::Session(session_id) = key else { let MonitorKey::Session(session_id) = key else {
anyhow::bail!("pf-vdisplay: unexpected monitor key kind"); anyhow::bail!("pf-vdisplay: unexpected monitor key kind");
@@ -258,6 +258,51 @@ pub(crate) unsafe fn wait_mode_settled(
} }
} }
/// 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
/// `IddCxMonitorUpdateModes2`, so an immediate `set_active_mode` could race the re-enumeration and
/// silently leave the old mode. Returns `true` once any refresh at the requested WxH is enumerable.
pub(crate) fn wait_mode_advertised(
gdi_name: &str,
mode: Mode,
ceiling: std::time::Duration,
) -> bool {
let wname: Vec<u16> = gdi_name.encode_utf16().chain(std::iter::once(0)).collect();
let deadline = std::time::Instant::now() + ceiling;
loop {
let mut i = 0u32;
loop {
let mut dm = DEVMODEW {
dmSize: size_of::<DEVMODEW>() as u16,
..Default::default()
};
// SAFETY: `wname` is a live NUL-terminated UTF-16 device name whose pointer stays valid
// for the call; `&mut dm` is a live, size-stamped DEVMODEW the API fills for mode index
// `i`. Both outlive this synchronous call.
let ok = unsafe {
EnumDisplaySettingsW(
PCWSTR(wname.as_ptr()),
ENUM_DISPLAY_SETTINGS_MODE(i),
&mut dm,
)
}
.as_bool();
if !ok {
break;
}
if dm.dmPelsWidth == mode.width && dm.dmPelsHeight == mode.height {
return true;
}
i += 1;
}
if std::time::Instant::now() >= deadline {
return false;
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
}
/// Monitor-departure wait (latency plan P0.3): after a REMOVE, poll until the target has left the /// Monitor-departure wait (latency plan P0.3): after a REMOVE, poll until the target has left the
/// ACTIVE CCD set — two consecutive absent samples, so one transient query failure mid-teardown /// ACTIVE CCD set — two consecutive absent samples, so one transient query failure mid-teardown
/// can't read as "gone" — instead of sleeping the fixed departure settle. `ceiling` (the old fixed /// can't read as "gone" — instead of sleeping the fixed departure settle. `ceiling` (the old fixed