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:
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user