feat(driver): pf-vdisplay IOCTL_UPDATE_MODES — live monitor mode-list refresh (proto v4)

Latency plan P2.1 (design/first-frame-and-resize-latency.md): a new additive
control-plane op lets the host refresh a LIVE monitor's advertised target-mode
list to lead with an arbitrary new mode (IddCxMonitorUpdateModes2 — the same
IddCx 1.10 *2 family this driver already requires, so no new OS floor). This
removes the 'mode list frozen at ADD' constraint that forced the mid-stream
resize through a REMOVE->ADD monitor hotplug: the monitor's OS identity, its
swap-chain worker and the retained FrameStash all survive an in-place mode set.

Protocol v4 is ADDITIVE over v3: the host's handshake floor stays at v3
(MIN_DRIVER_PROTOCOL_VERSION) and gates the in-place path on the reported
version, keeping re-arrival as the permanent fallback. The driver's stored
mode list is swapped before the DDI and reverted if it fails, so the OS and
the mode-DDI callbacks always agree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 17:12:13 +02:00
parent 32ffe7d634
commit 0899e53903
4 changed files with 160 additions and 5 deletions
+65 -1
View File
@@ -59,7 +59,19 @@ pub const fn interface_guid_fields() -> (u32, u16, u16, [u8; 8]) {
/// attach a ring naming a different monitor ([`frame::DRV_STATUS_BIND_FAIL`], the gamepad channel's /// attach a ring naming a different monitor ([`frame::DRV_STATUS_BIND_FAIL`], the gamepad channel's
/// `pad_index` validation applied to frames). A v2 host never stamps the field, so a v3 driver /// `pad_index` validation applied to frames). A v2 host never stamps the field, so a v3 driver
/// against a v2 host would refuse every attach — lockstep by the handshake, as ever. /// against a v2 host would refuse every attach — lockstep by the handshake, as ever.
pub const PROTOCOL_VERSION: u32 = 3; /// v4: ADDITIVE — [`control::IOCTL_UPDATE_MODES`] (the in-place mid-stream resize,
/// `design/first-frame-and-resize-latency.md` P2): the driver refreshes a LIVE monitor's advertised
/// target-mode list (`IddCxMonitorUpdateModes2`) so the OS can mode-set to an arbitrary new mode
/// without a REMOVE→ADD monitor hotplug. Nothing existing changed, so the host accepts a v3 driver
/// too ([`MIN_DRIVER_PROTOCOL_VERSION`]) and simply falls back to the re-arrival resize against it;
/// a v4 driver serving an older (v3-asserting) host fails that host's strict handshake — ship
/// driver+host together, as ever.
pub const PROTOCOL_VERSION: u32 = 4;
/// The OLDEST driver protocol this host still drives (v4 is additive over v3 — see the v4 note on
/// [`PROTOCOL_VERSION`]): a v3 driver lacks only `IOCTL_UPDATE_MODES`, which the host gates on the
/// handshake-reported version and covers with the re-arrival fallback.
pub const MIN_DRIVER_PROTOCOL_VERSION: u32 = 3;
/// `CTL_CODE(FILE_DEVICE_UNKNOWN = 0x22, func, METHOD_BUFFERED = 0, FILE_ANY_ACCESS = 0)`. /// `CTL_CODE(FILE_DEVICE_UNKNOWN = 0x22, func, METHOD_BUFFERED = 0, FILE_ANY_ACCESS = 0)`.
pub const fn ctl_code(func: u32) -> u32 { pub const fn ctl_code(func: u32) -> u32 {
@@ -91,6 +103,13 @@ pub mod control {
/// host duplicated into the driver's WUDFHost process. Input [`SetFrameChannelRequest`]. Sent once /// host duplicated into the driver's WUDFHost process. Input [`SetFrameChannelRequest`]. Sent once
/// after the ring is created and again on every mid-session ring recreate (HDR-mode flip). /// after the ring is created and again on every mid-session ring recreate (HDR-mode flip).
pub const IOCTL_SET_FRAME_CHANNEL: u32 = ctl_code(0x906); pub const IOCTL_SET_FRAME_CHANNEL: u32 = ctl_code(0x906);
/// Refresh a LIVE monitor's advertised target-mode list to a new preferred mode (+ the built-in
/// fallbacks) via `IddCxMonitorUpdateModes2` — the in-place mid-stream resize (v4,
/// `design/first-frame-and-resize-latency.md` P2). Input [`UpdateModesRequest`]. The host then
/// CCD-forces the new mode active on the SAME monitor: no REMOVE→ADD hotplug, the monitor's OS
/// identity (saved per-monitor DPI) and the driver's swap-chain/stash machinery survive. A v3
/// driver fails this unknown IOCTL → the host falls back to the re-arrival resize.
pub const IOCTL_UPDATE_MODES: u32 = ctl_code(0x907);
/// `IOCTL_ADD` input. A monotonic `session_id` keys the monitor (the host's refcount manager owns /// `IOCTL_ADD` input. A monotonic `session_id` keys the monitor (the host's refcount manager owns
/// collision safety — no more SudoVDA's 16-byte GUID + pid-mangling). The driver advertises this /// collision safety — no more SudoVDA's 16-byte GUID + pid-mangling). The driver advertises this
@@ -164,6 +183,22 @@ pub mod control {
pub session_id: u64, pub session_id: u64,
} }
/// `IOCTL_UPDATE_MODES` input (v4): the live monitor (by its ADD `session_id`) and the new
/// preferred mode its target-mode list should lead with. The driver replaces the stored list
/// (new mode first, then its built-in fallbacks — the same shape ADD produces) and pushes it to
/// the OS via `IddCxMonitorUpdateModes2`; success means the OS accepted the new list, after
/// which the host force-sets the mode via CCD/GDI as usual.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
pub struct UpdateModesRequest {
pub session_id: u64,
pub width: u32,
pub height: u32,
pub refresh_hz: u32,
/// Pads the `u64`-aligned struct to a multiple of 8 (Pod forbids implicit tail padding).
pub _reserved: u32,
}
/// `IOCTL_SET_RENDER_ADAPTER` input (the GPU the IddCx swap-chain should render on). /// `IOCTL_SET_RENDER_ADAPTER` input (the GPU the IddCx swap-chain should render on).
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
@@ -253,6 +288,12 @@ pub mod control {
assert!(size_of::<RemoveRequest>() == 8); assert!(size_of::<RemoveRequest>() == 8);
assert!(offset_of!(RemoveRequest, session_id) == 0); assert!(offset_of!(RemoveRequest, session_id) == 0);
assert!(size_of::<UpdateModesRequest>() == 24);
assert!(offset_of!(UpdateModesRequest, session_id) == 0);
assert!(offset_of!(UpdateModesRequest, width) == 8);
assert!(offset_of!(UpdateModesRequest, height) == 12);
assert!(offset_of!(UpdateModesRequest, refresh_hz) == 16);
assert!(size_of::<SetRenderAdapterRequest>() == 8); assert!(size_of::<SetRenderAdapterRequest>() == 8);
assert!(offset_of!(SetRenderAdapterRequest, luid_low) == 0); assert!(offset_of!(SetRenderAdapterRequest, luid_low) == 0);
assert!(offset_of!(SetRenderAdapterRequest, luid_high) == 4); assert!(offset_of!(SetRenderAdapterRequest, luid_high) == 4);
@@ -889,6 +930,28 @@ mod tests {
assert_eq!(bytes[32..40], 0x2000u64.to_le_bytes()); assert_eq!(bytes[32..40], 0x2000u64.to_le_bytes());
} }
#[test]
fn update_modes_request_roundtrips_and_versions_cohere() {
let req = control::UpdateModesRequest {
session_id: 42,
width: 2560,
height: 1409, // deliberately arbitrary — the in-place path serves window-drag modes
refresh_hz: 120,
_reserved: 0,
};
let bytes = bytemuck::bytes_of(&req);
assert_eq!(bytes.len(), 24);
assert_eq!(
*bytemuck::from_bytes::<control::UpdateModesRequest>(bytes),
req
);
assert_eq!(bytes[8..12], 2560u32.to_le_bytes());
// The compat window: v4 is additive over v3, so the host floor stays one below.
assert_eq!(PROTOCOL_VERSION, 4);
assert_eq!(MIN_DRIVER_PROTOCOL_VERSION, 3);
assert!(MIN_DRIVER_PROTOCOL_VERSION <= PROTOCOL_VERSION);
}
#[test] #[test]
fn gamepad_names_and_magics_are_stable() { fn gamepad_names_and_magics_are_stable() {
assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0"); assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0");
@@ -930,6 +993,7 @@ mod tests {
control::IOCTL_GET_INFO, control::IOCTL_GET_INFO,
control::IOCTL_CLEAR_ALL, control::IOCTL_CLEAR_ALL,
control::IOCTL_SET_FRAME_CHANNEL, control::IOCTL_SET_FRAME_CHANNEL,
control::IOCTL_UPDATE_MODES,
]; ];
for (i, a) in all.iter().enumerate() { for (i, a) in all.iter().enumerate() {
for b in &all[i + 1..] { for b in &all[i + 1..] {
@@ -95,6 +95,8 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
control::IOCTL_SET_RENDER_ADAPTER => unsafe { set_render_adapter(request) }, control::IOCTL_SET_RENDER_ADAPTER => unsafe { set_render_adapter(request) },
// SAFETY: `request` is the framework WDFREQUEST. // SAFETY: `request` is the framework WDFREQUEST.
control::IOCTL_SET_FRAME_CHANNEL => unsafe { set_frame_channel(request) }, control::IOCTL_SET_FRAME_CHANNEL => unsafe { set_frame_channel(request) },
// SAFETY: `request` is the framework WDFREQUEST.
control::IOCTL_UPDATE_MODES => unsafe { update_modes(request) },
_ => complete(request, STATUS_NOT_FOUND), _ => complete(request, STATUS_NOT_FOUND),
} }
} }
@@ -198,6 +200,28 @@ unsafe fn set_frame_channel(request: WDFREQUEST) {
} }
} }
/// `IOCTL_UPDATE_MODES` (v4): refresh a LIVE monitor's target-mode list to a new preferred mode —
/// the in-place mid-stream resize (`design/first-frame-and-resize-latency.md` P2). The monitor is
/// NOT departed: its OS identity, swap-chain machinery and retained frame stash all survive; the
/// host force-sets the freshly-advertised mode afterwards.
///
/// # Safety
/// `request` is the framework `WDFREQUEST`.
unsafe fn update_modes(request: WDFREQUEST) {
// SAFETY: `request` is the framework WDFREQUEST.
let Some(req) = (unsafe { read_input::<control::UpdateModesRequest>(request) }) else {
complete(request, STATUS_INVALID_PARAMETER);
return;
};
if !valid_mode(req.width, req.height, req.refresh_hz) {
complete(request, STATUS_INVALID_PARAMETER);
return;
}
let st =
crate::monitor::update_monitor_modes(req.session_id, req.width, req.height, req.refresh_hz);
complete(request, st);
}
/// `IOCTL_REMOVE`: depart + drop the monitor for the given session id. /// `IOCTL_REMOVE`: depart + drop the monitor for the given session id.
/// ///
/// # Safety /// # Safety
@@ -7,7 +7,7 @@
use std::sync::Mutex; use std::sync::Mutex;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use wdk_sys::{WDFOBJECT, call_unsafe_wdf_function_binding, iddcx}; use wdk_sys::{NTSTATUS, WDFOBJECT, call_unsafe_wdf_function_binding, iddcx};
/// One resolution with the refresh rates it supports. /// One resolution with the refresh rates it supports.
#[derive(Clone)] #[derive(Clone)]
@@ -365,9 +365,7 @@ pub fn preserve_publisher(
/// swap-chain's render adapter matches the publisher's ([`FramePublisher::render_adapter`]) — same /// swap-chain's render adapter matches the publisher's ([`FramePublisher::render_adapter`]) — same
/// pooled device, so its context + opened ring textures are still valid; on a mismatch the caller drops /// pooled device, so its context + opened ring textures are still valid; on a mismatch the caller drops
/// it and waits for a fresh channel delivery instead. `None` until a worker has stashed one. /// it and waits for a fresh channel delivery instead. `None` until a worker has stashed one.
pub fn take_preserved_publisher( pub fn take_preserved_publisher(target_id: u32) -> Option<crate::frame_transport::FramePublisher> {
target_id: u32,
) -> Option<crate::frame_transport::FramePublisher> {
if target_id == 0 { if target_id == 0 {
return None; return None;
} }
@@ -600,6 +598,65 @@ pub fn create_monitor(
Some((id, target_id, luid_low, luid_high)) Some((id, target_id, luid_low, luid_high))
} }
/// `IOCTL_UPDATE_MODES` (v4): refresh the LIVE monitor's advertised mode list to lead with a new
/// preferred mode (+ the same [`default_modes`] fallbacks ADD produces) and push the new TARGET
/// mode list to the OS via `IddCxMonitorUpdateModes2` — the in-place mid-stream resize
/// (`design/first-frame-and-resize-latency.md` P2). No departure: the monitor's OS identity, its
/// swap-chain worker and the retained frame stash all survive; the OS re-evaluates the target's
/// settable modes and the HOST then CCD-forces the new mode active. The `*2` (HDR) DDI matches the
/// `*2` mode/buffer family this driver already requires (IddCx 1.10), so it adds no new OS floor.
///
/// The stored list is updated FIRST (under the lock) so any OS re-query through the mode DDIs
/// ([`modes_for_object`]/[`modes_for_id`]) sees the new list, and REVERTED if the DDI fails — the
/// OS then still holds the old list and the two stay coherent. The DDI itself is called OUTSIDE
/// the lock (it may re-enter the mode-query callbacks, which lock [`MONITOR_MODES`]).
pub fn update_monitor_modes(session_id: u64, width: u32, height: u32, refresh: u32) -> NTSTATUS {
let mut new_modes = vec![Mode {
width,
height,
refresh_rates: vec![refresh],
}];
new_modes.extend(default_modes());
// Swap the stored list + grab the live handle under the lock.
let (object, old_modes) = {
let mut lock = lock_monitors();
let Some(m) = lock.iter_mut().find(|m| m.session_id == session_id) else {
return crate::STATUS_NOT_FOUND;
};
let Some(object) = m.object else {
return crate::STATUS_NOT_FOUND; // created but not yet arrived — nothing to update
};
let old = core::mem::replace(&mut m.modes, new_modes.clone());
(object, old)
};
// The OS's target-mode list for this monitor (the `*2`/HDR shape, like `monitor_query_modes2`).
let mut targets: Vec<iddcx::IDDCX_TARGET_MODE2> = flatten(&new_modes)
.map(|item| target_mode2(item.width, item.height, item.refresh_rate))
.collect();
let mut in_args = pod_init!(iddcx::IDARG_IN_UPDATEMODES2);
in_args.Reason = iddcx::IDDCX_UPDATE_REASON::IDDCX_UPDATE_REASON_OTHER;
in_args.TargetModeCount = targets.len() as u32;
in_args.pTargetModes = targets.as_mut_ptr();
// SAFETY: `object` is a live IddCx monitor handle (arrived — checked above; a concurrent REMOVE
// is serialized by the host, which only ever resizes a monitor its own session holds a lease
// on). `in_args` points at valid local storage (`targets` outlives the synchronous DDI call).
let st = unsafe { wdk_iddcx::IddCxMonitorUpdateModes2(object, &in_args) };
dbglog!(
"[pf-vd] IddCxMonitorUpdateModes2(session={session_id}, {width}x{height}@{refresh}) -> {st:#x}"
);
if !wdk_iddcx::nt_success(st) {
// Keep the stored list coherent with what the OS actually holds (the old one).
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.session_id == session_id) {
m.modes = old_modes;
}
return st;
}
crate::STATUS_SUCCESS
}
/// `IOCTL_REMOVE`: depart + drop the monitor for `session_id`. Returns true if one was removed. /// `IOCTL_REMOVE`: depart + drop the monitor for `session_id`. Returns true if one was removed.
pub fn remove_monitor(session_id: u64) -> bool { pub fn remove_monitor(session_id: u64) -> bool {
// Pull out the IddCx handle AND the swap-chain processor under the lock, but drop the processor // Pull out the IddCx handle AND the swap-chain processor under the lock, but drop the processor
@@ -140,6 +140,16 @@ iddcx_ddi!(
in_args: *const iddcx::IDARG_IN_ADAPTERSETRENDERADAPTER, in_args: *const iddcx::IDARG_IN_ADAPTERSETRENDERADAPTER,
) @ IddCxAdapterSetRenderAdapterTableIndex as PFN_IDDCXADAPTERSETRENDERADAPTER -> () ) @ IddCxAdapterSetRenderAdapterTableIndex as PFN_IDDCXADAPTERSETRENDERADAPTER -> ()
); );
iddcx_ddi!(
/// Refresh a LIVE monitor's target-mode list (the HDR `*2` variant, IddCx 1.10 — the same API
/// family as the `*2` mode/buffer DDIs this driver already requires): the OS re-evaluates which
/// modes the target supports WITHOUT a monitor departure, so the host can then mode-set to a
/// freshly-advertised mode in place (the mid-stream resize, latency plan P2).
IddCxMonitorUpdateModes2(
monitor: iddcx::IDDCX_MONITOR,
in_args: *const iddcx::IDARG_IN_UPDATEMODES2,
) @ IddCxMonitorUpdateModes2TableIndex as PFN_IDDCXMONITORUPDATEMODES2
);
iddcx_ddi!( iddcx_ddi!(
/// Bind a D3D device to an assigned swap-chain. HRESULT-shaped (0x887A0026 → retry on monitor flap). /// Bind a D3D device to an assigned swap-chain. HRESULT-shaped (0x887A0026 → retry on monitor flap).
IddCxSwapChainSetDevice( IddCxSwapChainSetDevice(