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:
@@ -246,6 +246,24 @@ pub trait Capturer: Send {
|
||||
fn pipeline_depth(&self) -> usize {
|
||||
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 →
|
||||
|
||||
@@ -1903,6 +1903,39 @@ impl Capturer for IddPushCapturer {
|
||||
// always has its own texture).
|
||||
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
|
||||
|
||||
@@ -4616,81 +4616,111 @@ fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDisplay>) -> Res
|
||||
} else {
|
||||
bitrate_kbps
|
||||
};
|
||||
// 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
|
||||
// IN-PLACE fast path first (latency plan P2.3, Windows IDD-push): keep the capturer +
|
||||
// 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.
|
||||
match build_pipeline(
|
||||
&mut vd,
|
||||
new_mode,
|
||||
mode_bitrate,
|
||||
bit_depth,
|
||||
plan,
|
||||
&quit,
|
||||
Some(resize_trace.as_ref()),
|
||||
) {
|
||||
Ok(next_pipe) => {
|
||||
if mode_bitrate != bitrate_kbps {
|
||||
tracing::info!(
|
||||
from_kbps = bitrate_kbps,
|
||||
to_kbps = mode_bitrate,
|
||||
"pinned PyroWave bitrate re-resolved for the new mode"
|
||||
);
|
||||
bitrate_kbps = mode_bitrate;
|
||||
live_bitrate.store(mode_bitrate, Ordering::Relaxed);
|
||||
let rebuilt = fast_done
|
||||
|| match build_pipeline(
|
||||
&mut vd,
|
||||
new_mode,
|
||||
mode_bitrate,
|
||||
bit_depth,
|
||||
plan,
|
||||
&quit,
|
||||
Some(resize_trace.as_ref()),
|
||||
) {
|
||||
Ok(next_pipe) => {
|
||||
let old_display_gen = cur_display_gen;
|
||||
// The destructuring assignment drops the OLD capturer (→ its display lease)
|
||||
// as each binding is replaced — the new pipeline is already up
|
||||
// (create-before-drop).
|
||||
(capturer, enc, frame, interval, cur_node_id, cur_display_gen) = next_pipe;
|
||||
// H4: the old display's lease drop above is indistinguishable from a
|
||||
// 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;
|
||||
// The destructuring assignment drops the OLD capturer (→ its display lease) as
|
||||
// each binding is replaced — the new pipeline is already up (create-before-drop).
|
||||
(capturer, enc, frame, interval, cur_node_id, cur_display_gen) = next_pipe;
|
||||
cur_mode = new_mode;
|
||||
next = std::time::Instant::now();
|
||||
// H4: the old display's lease drop above is indistinguishable from a 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).
|
||||
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 {
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), ?new_mode,
|
||||
"mode-switch rebuild failed — staying on the current mode");
|
||||
// H2 rollback: the control task acked the switch BEFORE this rebuild, so the
|
||||
// client's mode slot already flipped to `new_mode`. A second accepted ack
|
||||
// carrying the still-live mode corrects it (any accepted ack means "the
|
||||
// active mode is now X" client-side; old clients just log it). `frame` is
|
||||
// untouched here (the fast path returned false before swapping anything and
|
||||
// the destructure only runs on the Ok arm), so it's still the OLD
|
||||
// pipeline's frame — its real dims + interval are what's still on glass.
|
||||
let _ = reconfig_result_tx.send(Reconfigured {
|
||||
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.
|
||||
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");
|
||||
};
|
||||
if rebuilt {
|
||||
if mode_bitrate != bitrate_kbps {
|
||||
tracing::info!(
|
||||
from_kbps = bitrate_kbps,
|
||||
to_kbps = mode_bitrate,
|
||||
"pinned PyroWave bitrate re-resolved for the new mode"
|
||||
);
|
||||
bitrate_kbps = mode_bitrate;
|
||||
live_bitrate.store(mode_bitrate, Ordering::Relaxed);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), ?new_mode,
|
||||
"mode-switch rebuild failed — staying on the current mode");
|
||||
// H2 rollback: the control task acked the switch BEFORE this rebuild, so the
|
||||
// client's mode slot already flipped to `new_mode`. A second accepted ack
|
||||
// carrying the still-live mode corrects it (any accepted ack means "the active
|
||||
// mode is now X" client-side; old clients just log it). `frame` is untouched
|
||||
// here (the destructure only runs on the Ok arm), so it's still the OLD
|
||||
// pipeline's frame — its real dims + interval are exactly what's still on glass.
|
||||
cur_mode = new_mode;
|
||||
next = std::time::Instant::now();
|
||||
// H2/H3: the backend may have honored a different mode than requested — KWin caps
|
||||
// a virtual output's refresh, or Windows pf-vdisplay rejects 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 {
|
||||
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
|
||||
@@ -5346,6 +5376,115 @@ type Pipeline = (
|
||||
/// 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
|
||||
/// 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 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
|
||||
|
||||
@@ -68,11 +68,12 @@ pub(crate) trait VdisplayDriver: Send + Sync {
|
||||
/// 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)
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -90,6 +91,17 @@ pub(crate) trait VdisplayDriver: Send + Sync {
|
||||
preferred_monitor_id: u32,
|
||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
) -> 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`.
|
||||
///
|
||||
/// # Safety
|
||||
@@ -255,6 +267,9 @@ pub(crate) struct VirtualDisplayManager {
|
||||
/// `&'static` singleton with no raw-handle smuggling.
|
||||
device: Mutex<DeviceSlot>,
|
||||
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).
|
||||
gen: AtomicU64,
|
||||
state: Mutex<MgrInner>,
|
||||
@@ -285,6 +300,7 @@ pub(crate) fn init(driver: Box<dyn VdisplayDriver>) -> &'static VirtualDisplayMa
|
||||
driver,
|
||||
device: Mutex::new(DeviceSlot::default()),
|
||||
watchdog_s: AtomicU32::new(3),
|
||||
driver_proto: AtomicU32::new(0),
|
||||
gen: AtomicU64::new(1),
|
||||
state: Mutex::new(MgrInner::default()),
|
||||
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
|
||||
// concurrent open. `open` has no handle precondition to uphold, and the `OwnedHandle` it
|
||||
// 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;
|
||||
self.watchdog_s.store(watchdog_s, Ordering::Relaxed);
|
||||
self.driver_proto.store(driver_proto, Ordering::Relaxed);
|
||||
let raw = HANDLE(handle.as_raw_handle());
|
||||
slot.current = Some(Arc::new(handle));
|
||||
if !reap {
|
||||
@@ -581,6 +598,53 @@ 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 {
|
||||
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 {
|
||||
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).
|
||||
///
|
||||
/// 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"
|
||||
}
|
||||
|
||||
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`
|
||||
// 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.
|
||||
@@ -390,27 +390,43 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
.context("pf-vdisplay IOCTL_GET_INFO (version handshake)")?;
|
||||
let info: 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!(
|
||||
"pf-vdisplay protocol mismatch: host expects {}, driver reports {} — install matching \
|
||||
host + driver",
|
||||
"pf-vdisplay protocol mismatch: host drives {}..={}, driver reports {} — install \
|
||||
matching host + driver",
|
||||
pf_driver_proto::MIN_DRIVER_PROTOCOL_VERSION,
|
||||
pf_driver_proto::PROTOCOL_VERSION,
|
||||
info.protocol_version
|
||||
);
|
||||
}
|
||||
let watchdog_s = info.watchdog_timeout_s.max(1);
|
||||
tracing::info!(
|
||||
"pf-vdisplay protocol {} (watchdog timeout {}s)",
|
||||
info.protocol_version,
|
||||
watchdog_s
|
||||
);
|
||||
if info.protocol_version < pf_driver_proto::PROTOCOL_VERSION {
|
||||
tracing::warn!(
|
||||
"pf-vdisplay protocol {} (host supports {}): driver lacks the in-place resize — \
|
||||
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
|
||||
// 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
|
||||
// an unconditional CLEAR_ALL there would raze them.
|
||||
if !reap_orphans {
|
||||
reap_ghost_monitors();
|
||||
return Ok((device, watchdog_s));
|
||||
return Ok((device, watchdog_s, info.protocol_version));
|
||||
}
|
||||
let mut none: [u8; 0] = [];
|
||||
// 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
|
||||
// restarts (the reason a restart's CLEAR_ALL alone never recovered it before).
|
||||
reap_ghost_monitors();
|
||||
Ok((device, watchdog_s))
|
||||
Ok((device, watchdog_s, info.protocol_version))
|
||||
}
|
||||
|
||||
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<()> {
|
||||
let MonitorKey::Session(session_id) = key else {
|
||||
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
|
||||
/// 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
|
||||
|
||||
Reference in New Issue
Block a user