diff --git a/crates/pf-driver-proto/src/lib.rs b/crates/pf-driver-proto/src/lib.rs index 283f585f..56cb18de 100644 --- a/crates/pf-driver-proto/src/lib.rs +++ b/crates/pf-driver-proto/src/lib.rs @@ -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 /// `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. -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)`. 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 /// 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); + /// 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 /// 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, } + /// `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). #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)] @@ -253,6 +288,12 @@ pub mod control { assert!(size_of::() == 8); assert!(offset_of!(RemoveRequest, session_id) == 0); + assert!(size_of::() == 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::() == 8); assert!(offset_of!(SetRenderAdapterRequest, luid_low) == 0); assert!(offset_of!(SetRenderAdapterRequest, luid_high) == 4); @@ -889,6 +930,28 @@ mod tests { 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::(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] fn gamepad_names_and_magics_are_stable() { assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0"); @@ -930,6 +993,7 @@ mod tests { control::IOCTL_GET_INFO, control::IOCTL_CLEAR_ALL, control::IOCTL_SET_FRAME_CHANNEL, + control::IOCTL_UPDATE_MODES, ]; for (i, a) in all.iter().enumerate() { for b in &all[i + 1..] { diff --git a/packaging/windows/drivers/pf-vdisplay/src/control.rs b/packaging/windows/drivers/pf-vdisplay/src/control.rs index 414c5a97..8c59796f 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/control.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/control.rs @@ -95,6 +95,8 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) { control::IOCTL_SET_RENDER_ADAPTER => unsafe { set_render_adapter(request) }, // SAFETY: `request` is the framework WDFREQUEST. 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), } } @@ -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::(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. /// /// # Safety diff --git a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs index 513f4426..a92ced32 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs @@ -7,7 +7,7 @@ use std::sync::Mutex; 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. #[derive(Clone)] @@ -365,9 +365,7 @@ pub fn preserve_publisher( /// 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 /// it and waits for a fresh channel delivery instead. `None` until a worker has stashed one. -pub fn take_preserved_publisher( - target_id: u32, -) -> Option { +pub fn take_preserved_publisher(target_id: u32) -> Option { if target_id == 0 { return None; } @@ -600,6 +598,65 @@ pub fn create_monitor( 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 = 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. 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 diff --git a/packaging/windows/drivers/wdk-iddcx/src/lib.rs b/packaging/windows/drivers/wdk-iddcx/src/lib.rs index 600bd6a9..ac8325fb 100644 --- a/packaging/windows/drivers/wdk-iddcx/src/lib.rs +++ b/packaging/windows/drivers/wdk-iddcx/src/lib.rs @@ -140,6 +140,16 @@ iddcx_ddi!( in_args: *const iddcx::IDARG_IN_ADAPTERSETRENDERADAPTER, ) @ 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!( /// Bind a D3D device to an assigned swap-chain. HRESULT-shaped (0x887A0026 → retry on monitor flap). IddCxSwapChainSetDevice(