Merge perf/first-frame-latency: driver proto v4 + first-frame/resize latency (P0-P2)
Brings the first-frame-latency branch (P0.1 transition tracing, P1.1/P1.2 Welcome-time display prep, P2 in-place resize; pf-driver-proto v3 -> v4 with IOCTL_UPDATE_MODES) onto current main. The branch predates the W6.2/W7 splits, so git's rename detection carried most of it into the moved crates (pf-capture idd_push, pf-vdisplay manager/pf_vdisplay, pf-win-display, pf-driver-proto, the driver workspace) and the punktfunk1.rs remainder was re-homed by hand: - native/handshake.rs: welcome/start trace marks + the Welcome-time display prep spawn (the prep thread BECOMES the stream thread; hand-off via a SyncSender<SessionContext>). negotiate() gains bringup/quit/stop and returns the PrepHandle. - native.rs: bringup/resize_ms creation + the stop/quit flags hoisted BEFORE the handshake (the close watcher splits: flags pre-handshake, lifecycle events post-handshake where `hello` exists); punch_done stamp; the data plane adopts the prep thread's result or builds inline. - native/stream.rs: SessionContext/SendStats carry the trace; send_loop finishes it on the first video packet; the resize path gains the in-place fast path (try_inplace_resize) with the full rebuild as fallback, restructured so both share the post-rebuild bookkeeping; prepare_display/PreparedDisplay/ PrepHandle; build_pipeline(+retry) thread the stage marks. - session_status/mgmt: ttff_ms + last_resize_ms per session (union with the lifecycle-events fields main added to the same spots). - pf-capture: Capturer gains capture_target_id() + resize_output() defaults. - pf-vdisplay manager: perf's faster activation poll (60x50ms) + the settle floor before the PnP sweep, on main's knobs/no-trait shape. Also: packaging/windows/build-gamepad-drivers.ps1 is ASCII again (an em-dash from the pf-mouse work tripped windows-host.yml's locale-safety gate on main). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,8 @@ use windows::Win32::System::Threading::{
|
||||
use super::{DisplayOwnership, Mode, VirtualOutput};
|
||||
use pf_win_display::win_display::{
|
||||
count_other_active, force_extend_topology, isolate_displays_ccd, resolve_gdi_name,
|
||||
restore_displays_ccd, set_active_mode, set_virtual_primary_ccd, SavedConfig,
|
||||
restore_displays_ccd, set_active_mode, set_virtual_primary_ccd, wait_mode_settled,
|
||||
wait_target_departed, SavedConfig,
|
||||
};
|
||||
|
||||
#[path = "manager/driver.rs"]
|
||||
@@ -204,6 +205,16 @@ pub 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 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<MgrInner>,
|
||||
@@ -234,6 +245,8 @@ 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),
|
||||
update_modes_futile: AtomicBool::new(false),
|
||||
gen: AtomicU64::new(1),
|
||||
state: Mutex::new(MgrInner::default()),
|
||||
setup_lock: Mutex::new(()),
|
||||
@@ -321,9 +334,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 {
|
||||
@@ -418,9 +432,10 @@ impl VirtualDisplayManager {
|
||||
if let Some(SlotState::Lingering { mon, .. } | SlotState::Pinned { mon }) =
|
||||
inner.slots.remove(&slot)
|
||||
{
|
||||
let old_target = mon.target_id;
|
||||
tracing::info!(
|
||||
slot,
|
||||
old_target = mon.target_id,
|
||||
old_target,
|
||||
"IDD-push reconnect — preempting the kept (lingering/pinned) monitor, recreating a fresh one"
|
||||
);
|
||||
// SAFETY: `teardown_removed` requires `dev` to be a valid control handle; `dev` is the
|
||||
@@ -430,7 +445,16 @@ impl VirtualDisplayManager {
|
||||
unsafe { self.teardown_removed(dev, &mut inner, mon) };
|
||||
// Let the OS finish the ASYNC monitor departure before the next ADD; a back-to-back
|
||||
// REMOVE→ADD races the teardown and the ADD IOCTL is rejected under reconnect churn.
|
||||
thread::sleep(Duration::from_millis(400));
|
||||
// Verified-state wait, ceiling = the old fixed 400 ms settle (latency plan P0.3).
|
||||
// SAFETY: CCD query FFI over a `Copy` target id, under the held `state` lock.
|
||||
let departed =
|
||||
unsafe { wait_target_departed(old_target, Duration::from_millis(400)) };
|
||||
if !departed {
|
||||
tracing::debug!(
|
||||
old_target,
|
||||
"preempted monitor still in the active CCD set after the departure ceiling"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,9 +470,10 @@ impl VirtualDisplayManager {
|
||||
if matches!(inner.slots.get(&slot), Some(SlotState::Active { mon, .. }) if !wudf_alive(mon.wudf_pid))
|
||||
{
|
||||
if let Some(SlotState::Active { mon, .. }) = inner.slots.remove(&slot) {
|
||||
let old_target = mon.target_id;
|
||||
tracing::warn!(
|
||||
slot,
|
||||
old_target = mon.target_id,
|
||||
old_target,
|
||||
wudf_pid = mon.wudf_pid,
|
||||
"virtual monitor's WUDFHost is gone — preempting the dead monitor, recreating"
|
||||
);
|
||||
@@ -457,8 +482,9 @@ impl VirtualDisplayManager {
|
||||
// retired, kept alive; see `DeviceSlot`). `mon` was just removed from the map, so it
|
||||
// is exclusively owned here — no aliasing.
|
||||
unsafe { self.teardown_removed(dev, &mut inner, mon) };
|
||||
// Same async-departure settle as the reconnect preempt above.
|
||||
thread::sleep(Duration::from_millis(400));
|
||||
// Same async-departure settle as the reconnect preempt above (verified wait, P0.3).
|
||||
// SAFETY: CCD query FFI over a `Copy` target id, under the held `state` lock.
|
||||
let _ = unsafe { wait_target_departed(old_target, Duration::from_millis(400)) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,6 +501,57 @@ impl VirtualDisplayManager {
|
||||
_ => unreachable!("just matched Active"),
|
||||
};
|
||||
if cur_mode != mode {
|
||||
// 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 {
|
||||
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) => {
|
||||
// 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,
|
||||
reason = %format!("{e:#}"),
|
||||
"in-place resize not possible — 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");
|
||||
};
|
||||
@@ -736,8 +813,12 @@ impl VirtualDisplayManager {
|
||||
/// # Safety
|
||||
/// Runs the CCD (QueryDisplayConfig / SetDisplayConfig) FFI; call under the `state` lock.
|
||||
unsafe fn resolve_target_gdi(&self, target_id: u32) -> Option<String> {
|
||||
for _ in 0..15 {
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
// 50 ms sampling (latency plan P0.5): the SAME 3 s per-stage ceilings — the 3-stage ladder
|
||||
// structure encodes real failure modes (headless auto-activate, integrated-panel clone,
|
||||
// lid-closed path activation) and is untouched — but a typical activation resolves on an
|
||||
// early poll, so finer sampling shaves ~150 ms off every stage crossing.
|
||||
for _ in 0..60 {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
// SAFETY: `resolve_gdi_name` is `unsafe` for its CCD FFI; it takes a plain `Copy` `u32`
|
||||
// target id by value and returns an owned `String`, so no caller memory is borrowed.
|
||||
if let Some(n) = unsafe { resolve_gdi_name(target_id) } {
|
||||
@@ -746,8 +827,8 @@ impl VirtualDisplayManager {
|
||||
}
|
||||
// SAFETY: `force_extend_topology` only calls `SetDisplayConfig` (CCD) with no borrowed memory.
|
||||
unsafe { force_extend_topology() };
|
||||
for _ in 0..15 {
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
for _ in 0..60 {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
// SAFETY: as the resolve loop above.
|
||||
if let Some(n) = unsafe { resolve_gdi_name(target_id) } {
|
||||
return Some(n);
|
||||
@@ -756,8 +837,8 @@ impl VirtualDisplayManager {
|
||||
// SAFETY: `activate_target_path` runs the CCD query/apply FFI with owned local buffers; the
|
||||
// `Copy` target id is passed by value, under the `state` lock — the sole topology mutator.
|
||||
if unsafe { pf_win_display::win_display::activate_target_path(target_id) } {
|
||||
for _ in 0..15 {
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
for _ in 0..60 {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
// SAFETY: as the resolve loops above.
|
||||
if let Some(n) = unsafe { resolve_gdi_name(target_id) } {
|
||||
return Some(n);
|
||||
@@ -919,19 +1000,40 @@ impl VirtualDisplayManager {
|
||||
);
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture opens
|
||||
// Topology settle before capture opens: verified-state wait (latency plan P0.2) —
|
||||
// poll until the target's path + active mode are committed, ceiling = the old fixed
|
||||
// 1500 ms sleep (a rejected mode / slow third-party CCD-lock holder burns the
|
||||
// ceiling and proceeds, exactly like the sleep it replaces).
|
||||
let settle_start = std::time::Instant::now();
|
||||
// SAFETY: CCD/GDI query FFI over a `Copy` target id, under the held `state` lock.
|
||||
let settled = unsafe {
|
||||
wait_mode_settled(added.target_id, mode, Duration::from_millis(1500))
|
||||
};
|
||||
tracing::info!(
|
||||
settle_ms = settle_start.elapsed().as_millis() as u64,
|
||||
verified = settled,
|
||||
"topology settle (verified-state wait)"
|
||||
);
|
||||
|
||||
// EXPERIMENTAL `pnp_disable_monitors`, second selector (ANY topology): monitors
|
||||
// that are connected but NOT part of the desktop — the standby TV/monitor the
|
||||
// deactivated-set selector above structurally misses (it never had an active path
|
||||
// to deactivate), yet whose periodic standby wake events drive the same Windows
|
||||
// reaction cascade (rationale in `windows/monitor_devnode.rs`). Runs AFTER the
|
||||
// settle sleep so the active flags it reads are the committed ones (a display
|
||||
// still mid-activation from the primary topology's force-EXTEND must not read as
|
||||
// inactive and get disabled); in Extend the active physical panels are untouched
|
||||
// by construction. First member only — the sweep is group-scoped like the
|
||||
// isolate; later members join an already-swept desktop.
|
||||
// settle so the active flags it reads are the committed ones (a display still
|
||||
// mid-activation from the primary topology's force-EXTEND must not read as
|
||||
// inactive and get disabled) — and since the verified wait above only confirms
|
||||
// OUR target (not a physical still lighting up from force-EXTEND), this opt-in
|
||||
// sweep keeps the old FULL settle as its floor before reading those flags.
|
||||
// In Extend the active physical panels are untouched by construction. First
|
||||
// member only — the sweep is group-scoped like the isolate; later members join
|
||||
// an already-swept desktop.
|
||||
if first_member && crate::policy::prefs().pnp_disable_monitors() {
|
||||
if let Some(rest) =
|
||||
Duration::from_millis(1500).checked_sub(settle_start.elapsed())
|
||||
{
|
||||
thread::sleep(rest);
|
||||
}
|
||||
let mut keep = inner.target_ids();
|
||||
keep.push(added.target_id);
|
||||
for id in pf_win_display::monitor_devnode::disable_connected_inactive(&keep) {
|
||||
@@ -962,6 +1064,101 @@ 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")?;
|
||||
let t0 = Instant::now();
|
||||
// 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(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)
|
||||
);
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -1013,9 +1210,17 @@ impl VirtualDisplayManager {
|
||||
);
|
||||
}
|
||||
// Let the OS finish the ASYNC monitor departure before the ADD — a back-to-back REMOVE→ADD
|
||||
// races the teardown and the ADD is rejected under churn (same 400 ms settle as the reconnect
|
||||
// preempt path).
|
||||
thread::sleep(Duration::from_millis(400));
|
||||
// races the teardown and the ADD is rejected under churn. Verified departure wait, ceiling =
|
||||
// the old fixed 400 ms settle (latency plan P0.3); the driver's ghost-reap ADD retry remains
|
||||
// the backstop for a departure the CCD reports early.
|
||||
let depart_start = std::time::Instant::now();
|
||||
// SAFETY: CCD query FFI over a `Copy` target id, under the held `state` lock.
|
||||
let departed = unsafe { wait_target_departed(old.target_id, Duration::from_millis(400)) };
|
||||
tracing::info!(
|
||||
depart_ms = depart_start.elapsed().as_millis() as u64,
|
||||
verified = departed,
|
||||
"re-arrival: old monitor departure settle"
|
||||
);
|
||||
// 2. ADD a fresh monitor at the NEW mode, reusing the slot as the preferred (stable) id.
|
||||
let render_pin = resolve_render_pin();
|
||||
// SAFETY: `dev` is the live control handle; `render_pin`/`client_hdr` are owned `Copy`/`Option`
|
||||
@@ -1042,7 +1247,18 @@ impl VirtualDisplayManager {
|
||||
// the group's first-member restore snapshot.
|
||||
// SAFETY: CCD FFI over borrowed Copy target ids, under the `state` lock.
|
||||
unsafe { self.reisolate_after_swap(inner, added.target_id) };
|
||||
thread::sleep(Duration::from_millis(1500)); // let the topology settle before capture reopens
|
||||
// Topology settle before capture reopens: verified-state wait, ceiling = the old
|
||||
// fixed 1500 ms sleep (latency plan P0.2 — the re-arrival twin).
|
||||
let settle_start = std::time::Instant::now();
|
||||
// SAFETY: CCD/GDI query FFI over a `Copy` target id, under the held `state` lock.
|
||||
let settled = unsafe {
|
||||
wait_mode_settled(added.target_id, mode, Duration::from_millis(1500))
|
||||
};
|
||||
tracing::info!(
|
||||
settle_ms = settle_start.elapsed().as_millis() as u64,
|
||||
verified = settled,
|
||||
"re-arrival topology settle (verified-state wait)"
|
||||
);
|
||||
}
|
||||
None => tracing::warn!(
|
||||
"re-arrival target {} not yet an active display path (auto-activate, EXTEND preset \
|
||||
|
||||
@@ -341,7 +341,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.
|
||||
@@ -397,27 +397,43 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
}
|
||||
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
|
||||
@@ -434,7 +450,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(
|
||||
@@ -592,6 +608,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");
|
||||
@@ -740,4 +788,80 @@ mod tests {
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
drop(vout); // triggers REMOVE + stops the pinger
|
||||
}
|
||||
|
||||
/// Live in-place resize spike — skipped unless `PUNKTFUNK_PF_VDISPLAY_LIVE=1` (needs a v4
|
||||
/// pf-vdisplay driver installed + the host service STOPPED, single-instance guard). Answers the
|
||||
/// P2 open questions on real glass with no streaming client: create at one mode, then acquire
|
||||
/// the SAME session's slot at a DIFFERENT mode — the manager's resize branch runs UPDATE_MODES
|
||||
/// → mode-advertised wait → set_active_mode → verified settle. In-place success is visible as
|
||||
/// the SAME OS target id on the second output (a re-arrival fallback mints a new one) plus the
|
||||
/// committed active resolution; the test reports which path ran and asserts the mode landed.
|
||||
#[test]
|
||||
fn live_inplace_resize() {
|
||||
if std::env::var("PUNKTFUNK_PF_VDISPLAY_LIVE").is_err() {
|
||||
return;
|
||||
}
|
||||
// Live-run diagnostics: surface the manager/backend tracing (activation ladder, settle
|
||||
// waits, UPDATE_MODES) on stdout — a bare test harness has no subscriber, which made the
|
||||
// first on-glass run blind.
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "debug".into()),
|
||||
)
|
||||
.try_init();
|
||||
// Context probe: can this process see the CCD active-path set at all? (`None` = the query
|
||||
// itself fails in this session/window-station — the whole ladder would be blind, and a
|
||||
// "monitor never activated" verdict would be an artifact of the test context.)
|
||||
// SAFETY: CCD query over an owned empty slice (test-only diagnostics).
|
||||
let active0 = unsafe { crate::win_display::count_other_active(&[]) };
|
||||
println!("spike: CCD active paths visible before create: {active0:?}");
|
||||
let mut vd = PfVdisplayDisplay::new().expect("open pf-vdisplay");
|
||||
let first = vd
|
||||
.create(Mode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
refresh_hz: 60,
|
||||
})
|
||||
.expect("create virtual display");
|
||||
let t1 = first
|
||||
.win_capture
|
||||
.as_ref()
|
||||
.expect("no capture target")
|
||||
.target_id;
|
||||
thread::sleep(Duration::from_secs(2)); // let the activation/settle fully quiesce
|
||||
// A deliberately arbitrary (window-drag-shaped) mode the ADD never advertised.
|
||||
let t0 = std::time::Instant::now();
|
||||
let second = vd
|
||||
.create(Mode {
|
||||
width: 2356,
|
||||
height: 1332,
|
||||
refresh_hz: 60,
|
||||
})
|
||||
.expect("in-place resize acquire");
|
||||
let resize_ms = t0.elapsed().as_millis();
|
||||
let t2 = second
|
||||
.win_capture
|
||||
.as_ref()
|
||||
.expect("no capture target")
|
||||
.target_id;
|
||||
let in_place = t1 == t2;
|
||||
// SAFETY: CCD query over a Copy target id (test-only diagnostics).
|
||||
let active = unsafe { crate::win_display::active_resolution(t2) };
|
||||
println!(
|
||||
"in-place resize spike: in_place={in_place} (target {t1} -> {t2}) took {resize_ms} ms, \
|
||||
active resolution now {active:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
active,
|
||||
Some((2356, 1332)),
|
||||
"the new mode did not become the active resolution"
|
||||
);
|
||||
assert!(
|
||||
in_place,
|
||||
"the resize fell back to re-arrival (target id changed) — UPDATE_MODES path not taken"
|
||||
);
|
||||
drop(second);
|
||||
drop(first);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user