feat(windows-drivers): STEP 4 (2/n) — create_monitor + real mode DDIs + ADD/REMOVE
windows-drivers / probe-and-proto (push) Successful in 33s
windows-drivers / driver-build (push) Successful in 1m10s
android / android (push) Successful in 4m2s
ci / rust (push) Successful in 4m39s
ci / web (push) Successful in 44s
ci / docs-site (push) Successful in 52s
deb / build-publish (push) Successful in 2m17s
windows-host / package (push) Successful in 6m16s
decky / build-publish (push) Successful in 25s
ci / bench (push) Successful in 4m44s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 27s
apple / swift (push) Successful in 1m13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m51s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m51s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m18s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 47s
apple / screenshots (push) Successful in 5m45s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m49s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m47s
docker / deploy-docs (push) Successful in 21s
windows-drivers / probe-and-proto (push) Successful in 33s
windows-drivers / driver-build (push) Successful in 1m10s
android / android (push) Successful in 4m2s
ci / rust (push) Successful in 4m39s
ci / web (push) Successful in 44s
ci / docs-site (push) Successful in 52s
deb / build-publish (push) Successful in 2m17s
windows-host / package (push) Successful in 6m16s
decky / build-publish (push) Successful in 25s
ci / bench (push) Successful in 4m44s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 27s
apple / swift (push) Successful in 1m13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m51s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m51s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m18s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 47s
apple / screenshots (push) Successful in 5m45s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m49s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m47s
docker / deploy-docs (push) Successful in 21s
The virtual-monitor lifecycle is now code-complete on the driver side (CI-green; deployed — no load/adapter-init regression, Status=OK): - new monitor.rs: the monitor/mode model (Mode/MonitorObject/MONITOR_MODES), ported from upstream virtual-display-rs with guid:u128 -> session_id:u64. create_monitor builds an EDID (serial=id) -> IddCxMonitorCreate -> IddCxMonitorArrival, stores the monitor, and returns the OS target id + adapter LUID for AddReply. remove_monitor / clear_all depart + drop. display_info/target_mode build the DISPLAYCONFIG timing (the union videoStandard u32 set directly — bindgen-API-agnostic, vs the oracle new_bitfield_1 transmute). - callbacks.rs: parse_monitor_description (EDID-serial lookup -> count-then-fill IDDCX_MONITOR_MODE) + monitor_query_modes (pointer-match -> IDDCX_TARGET_MODE) are real. - control.rs: IOCTL_ADD -> create_monitor + AddReply, REMOVE -> remove_monitor, CLEAR_ALL -> clear_all, via read_input/write_output_complete WDF buffer helpers. SET_RENDER_ADAPTER still stubbed (hybrid-GPU pin, next) + the watchdog thread (next). - DISPLAYCONFIG_* resolve at the wdk_sys root (pub use types::*), not iddcx. Warnings are the STEP-7 *2/HDR stubs + created_at (read by the watchdog, next). The on-glass "monitor appears at WxH@Hz" gate awaits the host switch to pf_vdisplay_proto. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,3 +120,9 @@ pub fn init_adapter(device: WDFDEVICE) -> NTSTATUS {
|
|||||||
pub fn set_adapter(adapter: iddcx::IDDCX_ADAPTER) {
|
pub fn set_adapter(adapter: iddcx::IDDCX_ADAPTER) {
|
||||||
let _ = ADAPTER.set(SendAdapter(adapter));
|
let _ = ADAPTER.set(SendAdapter(adapter));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The created adapter handle, once `EvtIddCxAdapterInitFinished` has fired — for `create_monitor`
|
||||||
|
/// (`IddCxMonitorCreate`) and SET_RENDER_ADAPTER. `None` before adapter init completes.
|
||||||
|
pub(crate) fn adapter() -> Option<iddcx::IDDCX_ADAPTER> {
|
||||||
|
ADAPTER.get().map(|a| a.0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
use wdk_sys::iddcx;
|
use wdk_sys::iddcx;
|
||||||
use wdk_sys::{NTSTATUS, WDFDEVICE, WDFREQUEST};
|
use wdk_sys::{NTSTATUS, WDFDEVICE, WDFREQUEST};
|
||||||
|
|
||||||
use crate::{STATUS_NOT_IMPLEMENTED, STATUS_SUCCESS};
|
use crate::{
|
||||||
|
STATUS_BUFFER_TOO_SMALL, STATUS_INVALID_PARAMETER, STATUS_NOT_FOUND, STATUS_NOT_IMPLEMENTED,
|
||||||
|
STATUS_SUCCESS,
|
||||||
|
};
|
||||||
|
|
||||||
/// PnP `EvtDeviceD0Entry` (not an IddCx config callback). Adapter creation is deferred to the first D0
|
/// PnP `EvtDeviceD0Entry` (not an IddCx config callback). Adapter creation is deferred to the first D0
|
||||||
/// (the adapter object is only valid after D0), not driver_add.
|
/// (the adapter object is only valid after D0), not driver_add.
|
||||||
@@ -32,11 +35,48 @@ pub unsafe extern "C" fn adapter_init_finished(
|
|||||||
STATUS_SUCCESS
|
STATUS_SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SDR mode list for an EDID monitor. STEP 4: EDID-serial lookup + count-then-fill `IDDCX_MONITOR_MODE`.
|
/// SDR mode list for an EDID monitor: EDID-serial lookup → count-then-fill `IDDCX_MONITOR_MODE`.
|
||||||
pub unsafe extern "C" fn parse_monitor_description(
|
pub unsafe extern "C" fn parse_monitor_description(
|
||||||
_p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION,
|
p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION,
|
||||||
_p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION,
|
p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION,
|
||||||
) -> NTSTATUS {
|
) -> NTSTATUS {
|
||||||
|
// SAFETY: framework-provided in/out args, valid for the call.
|
||||||
|
let in_args = unsafe { &*p_in };
|
||||||
|
let out_args = unsafe { &mut *p_out };
|
||||||
|
// SAFETY: the framework supplies a valid EDID buffer of `DataSize` bytes.
|
||||||
|
let edid = unsafe {
|
||||||
|
core::slice::from_raw_parts(
|
||||||
|
in_args.MonitorDescription.pData.cast::<u8>(),
|
||||||
|
in_args.MonitorDescription.DataSize as usize,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let Ok(id) = crate::edid::Edid::get_serial(edid) else {
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
|
};
|
||||||
|
let Some(modes) = crate::monitor::modes_for_id(id) else {
|
||||||
|
return STATUS_NOT_FOUND;
|
||||||
|
};
|
||||||
|
let count = crate::monitor::flatten(&modes).count() as u32;
|
||||||
|
out_args.MonitorModeBufferOutputCount = count;
|
||||||
|
if in_args.MonitorModeBufferInputCount < count {
|
||||||
|
// A zero input count is a count-only probe (success); a non-zero too-small buffer is an error.
|
||||||
|
return if in_args.MonitorModeBufferInputCount > 0 {
|
||||||
|
STATUS_BUFFER_TOO_SMALL
|
||||||
|
} else {
|
||||||
|
STATUS_SUCCESS
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SAFETY: `pMonitorModes` points to >= `count` IDDCX_MONITOR_MODE entries (validated above).
|
||||||
|
let out = unsafe { core::slice::from_raw_parts_mut(in_args.pMonitorModes, count as usize) };
|
||||||
|
for (item, slot) in crate::monitor::flatten(&modes).zip(out.iter_mut()) {
|
||||||
|
let mut mode: iddcx::IDDCX_MONITOR_MODE = unsafe { core::mem::zeroed() };
|
||||||
|
mode.Size = core::mem::size_of::<iddcx::IDDCX_MONITOR_MODE>() as u32;
|
||||||
|
mode.Origin = iddcx::IDDCX_MONITOR_MODE_ORIGIN::IDDCX_MONITOR_MODE_ORIGIN_MONITORDESCRIPTOR;
|
||||||
|
mode.MonitorVideoSignalInfo =
|
||||||
|
crate::monitor::display_info(item.width, item.height, item.refresh_rate);
|
||||||
|
*slot = mode;
|
||||||
|
}
|
||||||
|
out_args.PreferredMonitorModeIdx = 0;
|
||||||
STATUS_SUCCESS
|
STATUS_SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,12 +97,27 @@ pub unsafe extern "C" fn monitor_get_default_modes(
|
|||||||
STATUS_NOT_IMPLEMENTED
|
STATUS_NOT_IMPLEMENTED
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SDR target (scan-out) modes. STEP 4: pointer-match the monitor + fill `IDDCX_TARGET_MODE`.
|
/// SDR target (scan-out) modes: pointer-match the monitor → fill `IDDCX_TARGET_MODE`.
|
||||||
pub unsafe extern "C" fn monitor_query_modes(
|
pub unsafe extern "C" fn monitor_query_modes(
|
||||||
_monitor: iddcx::IDDCX_MONITOR,
|
monitor: iddcx::IDDCX_MONITOR,
|
||||||
_p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES,
|
p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES,
|
||||||
_p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES,
|
p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES,
|
||||||
) -> NTSTATUS {
|
) -> NTSTATUS {
|
||||||
|
// SAFETY: framework-provided in/out args, valid for the call.
|
||||||
|
let in_args = unsafe { &*p_in };
|
||||||
|
let out_args = unsafe { &mut *p_out };
|
||||||
|
let Some(modes) = crate::monitor::modes_for_object(monitor) else {
|
||||||
|
return STATUS_NOT_FOUND;
|
||||||
|
};
|
||||||
|
let count = crate::monitor::flatten(&modes).count() as u32;
|
||||||
|
out_args.TargetModeBufferOutputCount = count;
|
||||||
|
if in_args.TargetModeBufferInputCount >= count {
|
||||||
|
// SAFETY: `pTargetModes` points to >= `count` IDDCX_TARGET_MODE entries.
|
||||||
|
let out = unsafe { core::slice::from_raw_parts_mut(in_args.pTargetModes, count as usize) };
|
||||||
|
for (item, slot) in crate::monitor::flatten(&modes).zip(out.iter_mut()) {
|
||||||
|
*slot = crate::monitor::target_mode(item.width, item.height, item.refresh_rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
STATUS_SUCCESS
|
STATUS_SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
//! The `pf-vdisplay-proto` control plane (`EvtIddCxDeviceIoControl`). The host opens the device interface
|
//! The `pf-vdisplay-proto` control plane (`EvtIddCxDeviceIoControl`). The host opens the device interface
|
||||||
//! (`PF_VDISPLAY_INTERFACE_GUID`) and drives the low-frequency IOCTLs: GET_INFO (version handshake),
|
//! (`PF_VDISPLAY_INTERFACE_GUID`) and drives the low-frequency IOCTLs: GET_INFO (version handshake), PING
|
||||||
//! PING (watchdog keepalive), and — STEP 4 (next) — ADD/REMOVE/SET_RENDER_ADAPTER/CLEAR_ALL for virtual
|
//! (watchdog keepalive), ADD/REMOVE/CLEAR_ALL (virtual monitors), and SET_RENDER_ADAPTER (next). Every
|
||||||
//! monitors. Every path completes the `WDFREQUEST` exactly once (the `EVT_IDD_CX_DEVICE_IO_CONTROL` shape
|
//! path completes the `WDFREQUEST` exactly once (the `EVT_IDD_CX_DEVICE_IO_CONTROL` shape returns `()`).
|
||||||
//! returns `()`).
|
|
||||||
|
|
||||||
use core::sync::atomic::{AtomicU64, Ordering};
|
use core::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@ use pf_vdisplay_proto::control;
|
|||||||
use wdk_iddcx::nt_success;
|
use wdk_iddcx::nt_success;
|
||||||
use wdk_sys::{call_unsafe_wdf_function_binding, NTSTATUS, WDFREQUEST};
|
use wdk_sys::{call_unsafe_wdf_function_binding, NTSTATUS, WDFREQUEST};
|
||||||
|
|
||||||
use crate::{STATUS_NOT_FOUND, STATUS_NOT_IMPLEMENTED, STATUS_SUCCESS};
|
use crate::{STATUS_INVALID_PARAMETER, STATUS_NOT_FOUND, STATUS_NOT_IMPLEMENTED, STATUS_SUCCESS};
|
||||||
|
|
||||||
/// The host must PING within this window or the watchdog reaps all monitors (STEP 4: the watchdog thread).
|
/// The host must PING within this window or the watchdog reaps all monitors (STEP 4: the watchdog thread).
|
||||||
const WATCHDOG_TIMEOUT_S: u32 = 10;
|
const WATCHDOG_TIMEOUT_S: u32 = 10;
|
||||||
@@ -24,38 +23,109 @@ static WATCHDOG_PINGS: AtomicU64 = AtomicU64::new(0);
|
|||||||
/// `request` is the framework-provided `WDFREQUEST` for an `EvtIddCxDeviceIoControl` call.
|
/// `request` is the framework-provided `WDFREQUEST` for an `EvtIddCxDeviceIoControl` call.
|
||||||
pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
|
pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
|
||||||
match ioctl_code {
|
match ioctl_code {
|
||||||
control::IOCTL_GET_INFO => unsafe { get_info(request) },
|
control::IOCTL_GET_INFO => {
|
||||||
control::IOCTL_PING => {
|
|
||||||
WATCHDOG_PINGS.fetch_add(1, Ordering::Relaxed);
|
|
||||||
complete(request, STATUS_SUCCESS);
|
|
||||||
}
|
|
||||||
// STEP 4 (next): ADD -> create_monitor, REMOVE, SET_RENDER_ADAPTER, CLEAR_ALL.
|
|
||||||
control::IOCTL_ADD
|
|
||||||
| control::IOCTL_REMOVE
|
|
||||||
| control::IOCTL_SET_RENDER_ADAPTER
|
|
||||||
| control::IOCTL_CLEAR_ALL => complete(request, STATUS_NOT_IMPLEMENTED),
|
|
||||||
_ => complete(request, STATUS_NOT_FOUND),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `IOCTL_GET_INFO`: write [`control::InfoReply`] (protocol version + watchdog timeout). The host asserts
|
|
||||||
/// `protocol_version == PROTOCOL_VERSION` and fails loudly on a mismatch.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
/// `request` is the framework `WDFREQUEST`.
|
|
||||||
unsafe fn get_info(request: WDFREQUEST) {
|
|
||||||
let reply = control::InfoReply {
|
let reply = control::InfoReply {
|
||||||
protocol_version: pf_vdisplay_proto::PROTOCOL_VERSION,
|
protocol_version: pf_vdisplay_proto::PROTOCOL_VERSION,
|
||||||
watchdog_timeout_s: WATCHDOG_TIMEOUT_S,
|
watchdog_timeout_s: WATCHDOG_TIMEOUT_S,
|
||||||
};
|
};
|
||||||
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
|
unsafe { write_output_complete(request, &reply) };
|
||||||
|
}
|
||||||
|
control::IOCTL_PING => {
|
||||||
|
WATCHDOG_PINGS.fetch_add(1, Ordering::Relaxed);
|
||||||
|
complete(request, STATUS_SUCCESS);
|
||||||
|
}
|
||||||
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
|
control::IOCTL_ADD => unsafe { add(request) },
|
||||||
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
|
control::IOCTL_REMOVE => unsafe { remove(request) },
|
||||||
|
control::IOCTL_CLEAR_ALL => {
|
||||||
|
crate::monitor::clear_all();
|
||||||
|
complete(request, STATUS_SUCCESS);
|
||||||
|
}
|
||||||
|
// SET_RENDER_ADAPTER (hybrid-GPU render pin): STEP 4 (next).
|
||||||
|
control::IOCTL_SET_RENDER_ADAPTER => complete(request, STATUS_NOT_IMPLEMENTED),
|
||||||
|
_ => complete(request, STATUS_NOT_FOUND),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `IOCTL_ADD`: create a virtual monitor at the requested mode → reply with the OS target id + LUID.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `request` is the framework `WDFREQUEST`.
|
||||||
|
unsafe fn add(request: WDFREQUEST) {
|
||||||
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
|
let Some(req) = (unsafe { read_input::<control::AddRequest>(request) }) else {
|
||||||
|
complete(request, STATUS_INVALID_PARAMETER);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some((target_id, luid_low, luid_high)) =
|
||||||
|
crate::monitor::create_monitor(req.session_id, req.width, req.height, req.refresh_hz)
|
||||||
|
else {
|
||||||
|
complete(request, STATUS_NOT_FOUND);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let reply = control::AddReply {
|
||||||
|
adapter_luid_low: luid_low,
|
||||||
|
adapter_luid_high: luid_high,
|
||||||
|
target_id,
|
||||||
|
_reserved: 0,
|
||||||
|
};
|
||||||
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
|
unsafe { write_output_complete(request, &reply) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `IOCTL_REMOVE`: depart + drop the monitor for the given session id.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `request` is the framework `WDFREQUEST`.
|
||||||
|
unsafe fn remove(request: WDFREQUEST) {
|
||||||
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
|
let Some(req) = (unsafe { read_input::<control::RemoveRequest>(request) }) else {
|
||||||
|
complete(request, STATUS_INVALID_PARAMETER);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
crate::monitor::remove_monitor(req.session_id);
|
||||||
|
complete(request, STATUS_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a `Copy`/`Pod` input struct from the request's input buffer (None if too small / unavailable).
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `request` is the framework `WDFREQUEST`.
|
||||||
|
unsafe fn read_input<T: Copy>(request: WDFREQUEST) -> Option<T> {
|
||||||
let mut buf: *mut core::ffi::c_void = core::ptr::null_mut();
|
let mut buf: *mut core::ffi::c_void = core::ptr::null_mut();
|
||||||
let mut len: usize = 0;
|
let mut len: usize = 0;
|
||||||
// SAFETY: `request` is valid; `buf`/`len` are out-params written by the framework.
|
// SAFETY: `request` valid; `buf`/`len` are out-params written by the framework.
|
||||||
|
let st = unsafe {
|
||||||
|
call_unsafe_wdf_function_binding!(
|
||||||
|
WdfRequestRetrieveInputBuffer,
|
||||||
|
request,
|
||||||
|
core::mem::size_of::<T>(),
|
||||||
|
&mut buf,
|
||||||
|
&mut len
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if !nt_success(st) || buf.is_null() || len < core::mem::size_of::<T>() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// SAFETY: `buf` has >= size_of::<T>() bytes; T is a Pod control struct.
|
||||||
|
Some(unsafe { buf.cast::<T>().read_unaligned() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a `Copy`/`Pod` reply to the request's output buffer + complete with its byte count.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `request` is the framework `WDFREQUEST`.
|
||||||
|
unsafe fn write_output_complete<T: Copy>(request: WDFREQUEST, value: &T) {
|
||||||
|
let mut buf: *mut core::ffi::c_void = core::ptr::null_mut();
|
||||||
|
let mut len: usize = 0;
|
||||||
|
// SAFETY: `request` valid; `buf`/`len` are out-params written by the framework.
|
||||||
let st = unsafe {
|
let st = unsafe {
|
||||||
call_unsafe_wdf_function_binding!(
|
call_unsafe_wdf_function_binding!(
|
||||||
WdfRequestRetrieveOutputBuffer,
|
WdfRequestRetrieveOutputBuffer,
|
||||||
request,
|
request,
|
||||||
core::mem::size_of::<control::InfoReply>(),
|
core::mem::size_of::<T>(),
|
||||||
&mut buf,
|
&mut buf,
|
||||||
&mut len
|
&mut len
|
||||||
)
|
)
|
||||||
@@ -64,9 +134,9 @@ unsafe fn get_info(request: WDFREQUEST) {
|
|||||||
complete(request, st);
|
complete(request, st);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// SAFETY: `buf` has >= size_of::<InfoReply>() writable bytes (validated above); InfoReply is Pod.
|
// SAFETY: `buf` has >= size_of::<T>() writable bytes; T is a Pod control struct.
|
||||||
unsafe { buf.cast::<control::InfoReply>().write_unaligned(reply) };
|
unsafe { buf.cast::<T>().write_unaligned(*value) };
|
||||||
complete_info(request, STATUS_SUCCESS, core::mem::size_of::<control::InfoReply>());
|
complete_info(request, STATUS_SUCCESS, core::mem::size_of::<T>());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Complete a request with just a status (no output).
|
/// Complete a request with just a status (no output).
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ mod log;
|
|||||||
mod adapter;
|
mod adapter;
|
||||||
mod callbacks;
|
mod callbacks;
|
||||||
mod control;
|
mod control;
|
||||||
#[allow(dead_code)] // salvaged verbatim; wired into the mode callbacks in STEP 4
|
|
||||||
mod edid;
|
mod edid;
|
||||||
mod entry;
|
mod entry;
|
||||||
|
mod monitor;
|
||||||
|
|
||||||
use wdk_sys::NTSTATUS;
|
use wdk_sys::NTSTATUS;
|
||||||
|
|
||||||
@@ -25,6 +25,8 @@ use wdk_sys::NTSTATUS;
|
|||||||
pub(crate) const STATUS_SUCCESS: NTSTATUS = 0;
|
pub(crate) const STATUS_SUCCESS: NTSTATUS = 0;
|
||||||
pub(crate) const STATUS_NOT_IMPLEMENTED: NTSTATUS = 0xC000_0002u32 as NTSTATUS;
|
pub(crate) const STATUS_NOT_IMPLEMENTED: NTSTATUS = 0xC000_0002u32 as NTSTATUS;
|
||||||
pub(crate) const STATUS_NOT_FOUND: NTSTATUS = 0xC000_0225u32 as NTSTATUS;
|
pub(crate) const STATUS_NOT_FOUND: NTSTATUS = 0xC000_0225u32 as NTSTATUS;
|
||||||
|
pub(crate) const STATUS_INVALID_PARAMETER: NTSTATUS = 0xC000_000Du32 as NTSTATUS;
|
||||||
|
pub(crate) const STATUS_BUFFER_TOO_SMALL: NTSTATUS = 0xC000_0023u32 as NTSTATUS;
|
||||||
|
|
||||||
/// IddCx (stub mode) requires the driver to export the minimum IddCx framework version it needs — the
|
/// IddCx (stub mode) requires the driver to export the minimum IddCx framework version it needs — the
|
||||||
/// `#ifndef IDD_STUB` branch of `IddCxFuncEnum.h` that normally emits it is compiled out under
|
/// `#ifndef IDD_STUB` branch of `IddCxFuncEnum.h` that normally emits it is compiled out under
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
//! Virtual-monitor model + lifecycle (STEP 4). Monitors are created on demand by the control plane
|
||||||
|
//! ([`crate::control`], `IOCTL_ADD`): each carries the requested mode (advertised as preferred) plus the
|
||||||
|
//! `session_id` the host keys it by and the OS target id + render-adapter LUID captured at arrival. Ported
|
||||||
|
//! from the working upstream virtual-display-rs (`monitor.rs` + `context.rs::create_monitor`), with
|
||||||
|
//! `guid: u128` → `session_id: u64` for the owned `pf_vdisplay_proto` control plane.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use wdk_sys::iddcx;
|
||||||
|
|
||||||
|
/// One resolution with the refresh rates it supports.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Mode {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub refresh_rates: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single (width, height, refresh) tuple — modes flattened across their refresh rates.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct ModeItem {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub refresh_rate: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flatten a mode list into per-refresh-rate tuples (the order the mode DDIs emit).
|
||||||
|
pub fn flatten(modes: &[Mode]) -> impl Iterator<Item = ModeItem> + '_ {
|
||||||
|
modes.iter().flat_map(|m| {
|
||||||
|
m.refresh_rates.iter().map(|&rr| ModeItem {
|
||||||
|
width: m.width,
|
||||||
|
height: m.height,
|
||||||
|
refresh_rate: rr,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A live (or pending) virtual monitor.
|
||||||
|
pub struct MonitorObject {
|
||||||
|
/// The IddCx monitor handle, set once `IddCxMonitorCreate` returns (None while pending).
|
||||||
|
pub object: Option<iddcx::IDDCX_MONITOR>,
|
||||||
|
/// EDID serial / connector index — the key the mode DDIs match on.
|
||||||
|
pub id: u32,
|
||||||
|
/// Advertised modes (requested mode first, then [`default_modes`]).
|
||||||
|
pub modes: Vec<Mode>,
|
||||||
|
/// The host's monotonic key (ADD/REMOVE).
|
||||||
|
pub session_id: u64,
|
||||||
|
/// OS target id + render-adapter LUID from `IDARG_OUT_MONITORARRIVAL` (the ADD reply).
|
||||||
|
pub target_id: u32,
|
||||||
|
pub adapter_luid_low: u32,
|
||||||
|
pub adapter_luid_high: i32,
|
||||||
|
/// When the entry was created — the watchdog skips still-initializing monitors.
|
||||||
|
pub created_at: Instant,
|
||||||
|
}
|
||||||
|
// SAFETY: the raw IddCx monitor handle is framework-managed; access is serialized by MONITOR_MODES.
|
||||||
|
unsafe impl Send for MonitorObject {}
|
||||||
|
|
||||||
|
pub static MONITOR_MODES: Mutex<Vec<MonitorObject>> = Mutex::new(Vec::new());
|
||||||
|
/// Monitor id / EDID-serial counter (unique per created monitor).
|
||||||
|
static NEXT_ID: AtomicU32 = AtomicU32::new(1);
|
||||||
|
|
||||||
|
/// Fallback modes appended after the requested mode, so a topology change still has options.
|
||||||
|
fn default_modes() -> Vec<Mode> {
|
||||||
|
vec![
|
||||||
|
Mode { width: 1920, height: 1080, refresh_rates: vec![60, 120] },
|
||||||
|
Mode { width: 1280, height: 720, refresh_rates: vec![60] },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `DISPLAYCONFIG_VIDEO_SIGNAL_INFO` for a monitor mode (vSyncFreqDivider = 0, per the DDI contract).
|
||||||
|
pub fn display_info(width: u32, height: u32, refresh_rate: u32) -> wdk_sys::DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
|
||||||
|
let clock_rate = refresh_rate * (height + 4) * (height + 4) + 1000;
|
||||||
|
let mut si: wdk_sys::DISPLAYCONFIG_VIDEO_SIGNAL_INFO = unsafe { core::mem::zeroed() };
|
||||||
|
si.pixelRate = u64::from(clock_rate);
|
||||||
|
si.hSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL { Numerator: clock_rate, Denominator: height + 4 };
|
||||||
|
si.vSyncFreq =
|
||||||
|
wdk_sys::DISPLAYCONFIG_RATIONAL { Numerator: clock_rate, Denominator: (height + 4) * (height + 4) };
|
||||||
|
si.activeSize = wdk_sys::DISPLAYCONFIG_2DREGION { cx: width, cy: height };
|
||||||
|
si.totalSize = wdk_sys::DISPLAYCONFIG_2DREGION { cx: width + 4, cy: height + 4 };
|
||||||
|
// union { AdditionalSignalInfo bitfield | videoStandard:u32 }: videoStandard=255, vSyncFreqDivider=0.
|
||||||
|
si.__bindgen_anon_1.videoStandard = 255;
|
||||||
|
si.scanLineOrdering =
|
||||||
|
wdk_sys::DISPLAYCONFIG_SCANLINE_ORDERING::DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE;
|
||||||
|
si
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `IDDCX_TARGET_MODE` for a scan-out mode (vSyncFreqDivider = 1, per the DDI contract).
|
||||||
|
pub fn target_mode(width: u32, height: u32, refresh_rate: u32) -> iddcx::IDDCX_TARGET_MODE {
|
||||||
|
let region = wdk_sys::DISPLAYCONFIG_2DREGION { cx: width, cy: height };
|
||||||
|
let mut si: wdk_sys::DISPLAYCONFIG_VIDEO_SIGNAL_INFO = unsafe { core::mem::zeroed() };
|
||||||
|
si.pixelRate = u64::from(refresh_rate) * u64::from(width) * u64::from(height);
|
||||||
|
si.hSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL { Numerator: refresh_rate * height, Denominator: 1 };
|
||||||
|
si.vSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL { Numerator: refresh_rate, Denominator: 1 };
|
||||||
|
si.totalSize = region;
|
||||||
|
si.activeSize = region;
|
||||||
|
si.scanLineOrdering =
|
||||||
|
wdk_sys::DISPLAYCONFIG_SCANLINE_ORDERING::DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE;
|
||||||
|
// videoStandard=255, vSyncFreqDivider=1 (bits 16..21) => 255 | (1<<16).
|
||||||
|
si.__bindgen_anon_1.videoStandard = 255 | (1 << 16);
|
||||||
|
let mut tm: iddcx::IDDCX_TARGET_MODE = unsafe { core::mem::zeroed() };
|
||||||
|
tm.Size = core::mem::size_of::<iddcx::IDDCX_TARGET_MODE>() as u32;
|
||||||
|
tm.TargetVideoSignalInfo = wdk_sys::DISPLAYCONFIG_TARGET_MODE { targetVideoSignalInfo: si };
|
||||||
|
tm
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A monitor's advertised modes (the looked-up entry returns a clone for lock-free mode-DDI fill).
|
||||||
|
pub fn modes_for_id(id: u32) -> Option<Vec<Mode>> {
|
||||||
|
MONITOR_MODES.lock().ok()?.iter().find(|m| m.id == id).map(|m| m.modes.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modes for the monitor whose handle matches (used by `monitor_query_modes`).
|
||||||
|
pub fn modes_for_object(object: iddcx::IDDCX_MONITOR) -> Option<Vec<Mode>> {
|
||||||
|
MONITOR_MODES
|
||||||
|
.lock()
|
||||||
|
.ok()?
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.object == Some(object))
|
||||||
|
.map(|m| m.modes.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `IOCTL_ADD`: create + arrive a virtual monitor at `width`x`height`@`refresh`. Returns the OS
|
||||||
|
/// `(target_id, adapter_luid_low, adapter_luid_high)` for the [`AddReply`](pf_vdisplay_proto::control::AddReply),
|
||||||
|
/// or `None` on failure (no adapter yet / IddCx error).
|
||||||
|
pub fn create_monitor(session_id: u64, width: u32, height: u32, refresh: u32) -> Option<(u32, u32, i32)> {
|
||||||
|
let adapter = crate::adapter::adapter()?;
|
||||||
|
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
|
let mut modes = vec![Mode { width, height, refresh_rates: vec![refresh] }];
|
||||||
|
modes.extend(default_modes());
|
||||||
|
|
||||||
|
// Register the (pending) monitor so the mode DDIs can find it by EDID-serial id before arrival.
|
||||||
|
if let Ok(mut lock) = MONITOR_MODES.lock() {
|
||||||
|
lock.push(MonitorObject {
|
||||||
|
object: None,
|
||||||
|
id,
|
||||||
|
modes,
|
||||||
|
session_id,
|
||||||
|
target_id: 0,
|
||||||
|
adapter_luid_low: 0,
|
||||||
|
adapter_luid_high: 0,
|
||||||
|
created_at: Instant::now(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EDID (serial = id) describes the monitor; the OS calls back into parse_monitor_description.
|
||||||
|
let mut edid = crate::edid::Edid::generate_with(id);
|
||||||
|
let mut desc: iddcx::IDDCX_MONITOR_DESCRIPTION = unsafe { core::mem::zeroed() };
|
||||||
|
desc.Size = core::mem::size_of::<iddcx::IDDCX_MONITOR_DESCRIPTION>() as u32;
|
||||||
|
desc.Type = iddcx::IDDCX_MONITOR_DESCRIPTION_TYPE::IDDCX_MONITOR_DESCRIPTION_TYPE_EDID;
|
||||||
|
desc.DataSize = edid.len() as u32;
|
||||||
|
desc.pData = edid.as_mut_ptr().cast();
|
||||||
|
|
||||||
|
let mut info: iddcx::IDDCX_MONITOR_INFO = unsafe { core::mem::zeroed() };
|
||||||
|
info.Size = core::mem::size_of::<iddcx::IDDCX_MONITOR_INFO>() as u32;
|
||||||
|
info.MonitorContainerId = container_guid(id);
|
||||||
|
info.MonitorType =
|
||||||
|
wdk_sys::DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY::DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
|
||||||
|
info.ConnectorIndex = id;
|
||||||
|
info.MonitorDescription = desc;
|
||||||
|
|
||||||
|
let mut attr: wdk_sys::WDF_OBJECT_ATTRIBUTES = unsafe { core::mem::zeroed() };
|
||||||
|
attr.Size = core::mem::size_of::<wdk_sys::WDF_OBJECT_ATTRIBUTES>() as u32;
|
||||||
|
attr.ExecutionLevel = wdk_sys::_WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent;
|
||||||
|
attr.SynchronizationScope =
|
||||||
|
wdk_sys::_WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent;
|
||||||
|
|
||||||
|
let create_in = iddcx::IDARG_IN_MONITORCREATE { ObjectAttributes: &raw mut attr, pMonitorInfo: &raw mut info };
|
||||||
|
let mut create_out: iddcx::IDARG_OUT_MONITORCREATE = unsafe { core::mem::zeroed() };
|
||||||
|
// SAFETY: adapter is a valid IddCx adapter; create_in points to valid local storage read synchronously.
|
||||||
|
let st = unsafe { wdk_iddcx::IddCxMonitorCreate(adapter, &create_in, &mut create_out) };
|
||||||
|
dbglog!("[pf-vd] IddCxMonitorCreate(id={id}) -> {st:#x}");
|
||||||
|
if !wdk_iddcx::nt_success(st) {
|
||||||
|
remove_by_id(id);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let monitor = create_out.MonitorObject;
|
||||||
|
if let Ok(mut lock) = MONITOR_MODES.lock() {
|
||||||
|
if let Some(m) = lock.iter_mut().find(|m| m.id == id) {
|
||||||
|
m.object = Some(monitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the OS the monitor is plugged in.
|
||||||
|
let mut arrival_out: iddcx::IDARG_OUT_MONITORARRIVAL = unsafe { core::mem::zeroed() };
|
||||||
|
// SAFETY: `monitor` is the just-created IddCx monitor handle.
|
||||||
|
let st = unsafe { wdk_iddcx::IddCxMonitorArrival(monitor, &mut arrival_out) };
|
||||||
|
dbglog!("[pf-vd] IddCxMonitorArrival(id={id}) -> {st:#x}");
|
||||||
|
if !wdk_iddcx::nt_success(st) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (target_id, luid_low, luid_high) = (
|
||||||
|
arrival_out.OsTargetId,
|
||||||
|
arrival_out.OsAdapterLuid.LowPart,
|
||||||
|
arrival_out.OsAdapterLuid.HighPart,
|
||||||
|
);
|
||||||
|
if let Ok(mut lock) = MONITOR_MODES.lock() {
|
||||||
|
if let Some(m) = lock.iter_mut().find(|m| m.id == id) {
|
||||||
|
m.target_id = target_id;
|
||||||
|
m.adapter_luid_low = luid_low;
|
||||||
|
m.adapter_luid_high = luid_high;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((target_id, luid_low, luid_high))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `IOCTL_REMOVE`: depart + drop the monitor for `session_id`. Returns true if one was removed.
|
||||||
|
pub fn remove_monitor(session_id: u64) -> bool {
|
||||||
|
let monitor = {
|
||||||
|
let Ok(mut lock) = MONITOR_MODES.lock() else { return false };
|
||||||
|
let Some(pos) = lock.iter().position(|m| m.session_id == session_id) else { return false };
|
||||||
|
let entry = lock.remove(pos);
|
||||||
|
entry.object
|
||||||
|
};
|
||||||
|
if let Some(m) = monitor {
|
||||||
|
// SAFETY: `m` is a live IddCx monitor handle; departure tears it down.
|
||||||
|
unsafe { wdk_iddcx::IddCxMonitorDeparture(m) };
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `IOCTL_CLEAR_ALL`: depart + drop every monitor (host-startup orphan reap).
|
||||||
|
pub fn clear_all() {
|
||||||
|
let monitors: Vec<iddcx::IDDCX_MONITOR> = {
|
||||||
|
let Ok(mut lock) = MONITOR_MODES.lock() else { return };
|
||||||
|
lock.drain(..).filter_map(|m| m.object).collect()
|
||||||
|
};
|
||||||
|
for m in monitors {
|
||||||
|
// SAFETY: `m` is a live IddCx monitor handle.
|
||||||
|
unsafe { wdk_iddcx::IddCxMonitorDeparture(m) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop a pending entry by id (create failed before arrival).
|
||||||
|
fn remove_by_id(id: u32) {
|
||||||
|
if let Ok(mut lock) = MONITOR_MODES.lock() {
|
||||||
|
lock.retain(|m| m.id != id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A deterministic, monitor-unique container GUID (groups targets into a physical device). Derived from
|
||||||
|
/// `id` so it is stable + collision-free without a random source.
|
||||||
|
fn container_guid(id: u32) -> wdk_sys::GUID {
|
||||||
|
wdk_sys::GUID {
|
||||||
|
Data1: 0x7066_7664u32.wrapping_add(id),
|
||||||
|
Data2: 0x7044,
|
||||||
|
Data3: 0x5350,
|
||||||
|
Data4: [0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, (id >> 8) as u8, id as u8],
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user