From 4f10f3439d0cdd57dedd2dd66ec47c5399195c1c Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 24 Jun 2026 16:12:20 +0000 Subject: [PATCH] =?UTF-8?q?feat(windows-drivers):=20pf-vdisplay=20STEP=202?= =?UTF-8?q?=20=E2=80=94=20IddCx=20device=20skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DriverEntry -> driver_add builds the full IDD_CX_CLIENT_CONFIG (14 IddCx callbacks + PnP EvtDeviceD0Entry, all stubs with correct PFN signatures) sized via the ported IDD_STRUCTURE_SIZE! (size.rs), runs IddCxDeviceInitConfig -> WdfDeviceCreate -> WdfDeviceCreateDeviceInterface(the owned pf-vdisplay GUID, not SudoVDA) -> IddCxDeviceInitialize. callbacks.rs has all 14 + device_d0_entry; query_target_info implements HIGH_COLOR_SPACE. edid.rs salvaged verbatim from the oracle. proto gains interface_guid_fields() (u128 -> Windows GUID fields). Links IddCxStub (the CI gate); adapter/monitor/swapchain/IDD-push fill the stubs in STEP 3-6. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/pf-vdisplay-proto/src/lib.rs | 16 ++ .../windows/drivers/pf-vdisplay/build.rs | 40 ++++- .../drivers/pf-vdisplay/src/callbacks.rs | 148 ++++++++++++++++++ .../windows/drivers/pf-vdisplay/src/edid.rs | 118 ++++++++++++++ .../windows/drivers/pf-vdisplay/src/entry.rs | 112 +++++++++++++ .../windows/drivers/pf-vdisplay/src/lib.rs | 81 +++------- .../windows/drivers/pf-vdisplay/src/size.rs | 30 ++++ 7 files changed, 480 insertions(+), 65 deletions(-) create mode 100644 packaging/windows/drivers/pf-vdisplay/src/callbacks.rs create mode 100644 packaging/windows/drivers/pf-vdisplay/src/edid.rs create mode 100644 packaging/windows/drivers/pf-vdisplay/src/entry.rs create mode 100644 packaging/windows/drivers/pf-vdisplay/src/size.rs diff --git a/crates/pf-vdisplay-proto/src/lib.rs b/crates/pf-vdisplay-proto/src/lib.rs index 584aae1..cc40852 100644 --- a/crates/pf-vdisplay-proto/src/lib.rs +++ b/crates/pf-vdisplay-proto/src/lib.rs @@ -25,6 +25,22 @@ extern crate alloc; /// `GUID::from_u128(PF_VDISPLAY_INTERFACE_GUID_U128)`. pub const PF_VDISPLAY_INTERFACE_GUID_U128: u128 = 0x7066_7664_7044_5350_a1b2_c3d4_e5f6_0001; +/// The interface GUID split into Windows `GUID` fields — `(Data1, Data2, Data3, Data4)` — so the driver +/// (and host) can build a `windows`/`wdk_sys` `GUID` without re-deriving the byte layout. Standard GUID +/// layout from the u128: `Data1` = high 32 bits, `Data2`/`Data3` = next two 16-bit groups, `Data4` = +/// the low 64 bits big-endian. (This crate is `no_std` + provider-agnostic, so it returns the fields +/// rather than depend on a `GUID` type.) +#[must_use] +pub const fn interface_guid_fields() -> (u32, u16, u16, [u8; 8]) { + let g = PF_VDISPLAY_INTERFACE_GUID_U128; + ( + (g >> 96) as u32, + (g >> 80) as u16, + (g >> 64) as u16, + (g as u64).to_be_bytes(), + ) +} + /// Bumped on any incompatible change to either plane. Exchanged via [`control::IOCTL_GET_INFO`]; host /// and driver assert a match at startup so a mismatched pair fails loudly instead of corrupting. pub const PROTOCOL_VERSION: u32 = 1; diff --git a/packaging/windows/drivers/pf-vdisplay/build.rs b/packaging/windows/drivers/pf-vdisplay/build.rs index 6ccf3f9..a11bf57 100644 --- a/packaging/windows/drivers/pf-vdisplay/build.rs +++ b/packaging/windows/drivers/pf-vdisplay/build.rs @@ -1,6 +1,38 @@ -//! Emits the WDK link flags for the cdylib (wdk-build). STEP 0 needs only the WDF stub link + the -//! `/INTEGRITYCHECK` that the CI step clears; `IddCxStub` (+ `IddMinimumVersionRequired`) is added in -//! STEP 2 when the driver actually calls IddCx — see wdk-probe/build.rs for the glob recipe. +//! WDK link flags for the cdylib (wdk-build) + `IddCxStub` (the driver calls IddCx DDIs via wdk-iddcx, +//! and exports `IddMinimumVersionRequired`). `/INTEGRITYCHECK` (set by wdk-build) is cleared by the CI +//! packaging step. Glob recipe matches wdk-probe/build.rs. fn main() -> Result<(), wdk_build::ConfigError> { - wdk_build::configure_wdk_binary_build() + wdk_build::configure_wdk_binary_build()?; + link_iddcx_stub(); + Ok(()) +} + +/// Link `IddCxStub.lib`. It ships only under the SDK *version* that includes IddCx, at +/// `Lib\\um\\iddcx\\` — a newer base SDK alongside it lacks the `iddcx` subdir, so +/// glob for the dir that actually contains the lib rather than trusting the max SDK version. x64 only. +fn link_iddcx_stub() { + const ARCH: &str = "x64"; + const ROOTS: [&str; 2] = [ + r"C:\Program Files (x86)\Windows Kits\10\Lib", + r"C:\Program Files\Windows Kits\10\Lib", + ]; + for root in ROOTS { + let Ok(versions) = std::fs::read_dir(root) else { + continue; + }; + for ver in versions.flatten() { + let iddcx = ver.path().join("um").join(ARCH).join("iddcx"); + let Ok(subdirs) = std::fs::read_dir(&iddcx) else { + continue; + }; + for sub in subdirs.flatten() { + if sub.path().join("IddCxStub.lib").is_file() { + println!("cargo:rustc-link-search={}", sub.path().display()); + println!("cargo:rustc-link-lib=static=IddCxStub"); + return; + } + } + } + } + panic!("IddCxStub.lib not found under any Windows Kits Lib\\\\um\\{ARCH}\\iddcx\\\\"); } diff --git a/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs b/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs new file mode 100644 index 0000000..655de42 --- /dev/null +++ b/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs @@ -0,0 +1,148 @@ +//! The IddCx client-config callbacks + the PnP `EvtDeviceD0Entry`. +//! +//! STEP 2: stubs with the correct PFN signatures (so the config wires up + the driver loads); the real +//! mode/EDID logic (STEP 4), adapter init (STEP 3), and swap-chain handoff (STEP 5) fill them in. Every +//! callback is `unsafe extern "C"` to match the wdk-sys `PFN_IDD_CX_*` types; with `panic = "abort"` +//! (workspace profile) a panic across the FFI boundary aborts rather than being UB. `query_target_info` +//! is implemented now because it gates HDR (`HIGH_COLOR_SPACE`) and the adapter (STEP 3) sets FP16. + +use wdk_sys::iddcx; +use wdk_sys::{call_unsafe_wdf_function_binding, NTSTATUS, WDFDEVICE, WDFREQUEST}; + +use crate::{STATUS_NOT_IMPLEMENTED, STATUS_SUCCESS}; + +/// PnP `EvtDeviceD0Entry` (not an IddCx config callback). STEP 3 calls `DeviceContext::init_adapter` +/// here (adapter creation is deferred to first D0, not driver_add). +pub unsafe extern "C" fn device_d0_entry( + _device: WDFDEVICE, + _previous_state: wdk_sys::WDF_POWER_DEVICE_STATE, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// Async completion of `IddCxAdapterInitAsync`. STEP 3: stash the adapter + start the watchdog. +pub unsafe extern "C" fn adapter_init_finished( + _adapter: iddcx::IDDCX_ADAPTER, + _p_in: *const iddcx::IDARG_IN_ADAPTER_INIT_FINISHED, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// SDR mode list for an EDID monitor. STEP 4: EDID-serial lookup + count-then-fill `IDDCX_MONITOR_MODE`. +pub unsafe extern "C" fn parse_monitor_description( + _p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION, + _p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// HDR (`*2`) mode list — writes `IDDCX_MONITOR_MODE2` (+BitsPerComponent). Mandatory under FP16. +pub unsafe extern "C" fn parse_monitor_description2( + _p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION2, + _p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// Only called for EDID-less monitors; ours always carry an EDID, so this stays NOT_IMPLEMENTED. +pub unsafe extern "C" fn monitor_get_default_modes( + _monitor: iddcx::IDDCX_MONITOR, + _p_in: *const iddcx::IDARG_IN_GETDEFAULTDESCRIPTIONMODES, + _p_out: *mut iddcx::IDARG_OUT_GETDEFAULTDESCRIPTIONMODES, +) -> NTSTATUS { + STATUS_NOT_IMPLEMENTED +} + +/// SDR target (scan-out) modes. STEP 4: pointer-match the monitor + fill `IDDCX_TARGET_MODE`. +pub unsafe extern "C" fn monitor_query_modes( + _monitor: iddcx::IDDCX_MONITOR, + _p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES, + _p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// HDR (`*2`) target modes — writes `IDDCX_TARGET_MODE2`. Mandatory under FP16. +pub unsafe extern "C" fn monitor_query_modes2( + _monitor: iddcx::IDDCX_MONITOR, + _p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES2, + _p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// Diagnostic only — assign drives everything. STEP 4 logs the committed paths. +pub unsafe extern "C" fn adapter_commit_modes( + _adapter: iddcx::IDDCX_ADAPTER, + _p_in: *const iddcx::IDARG_IN_COMMITMODES, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// HDR (`*2`) commit over `IDDCX_PATH2`. Mandatory under FP16. +pub unsafe extern "C" fn adapter_commit_modes2( + _adapter: iddcx::IDDCX_ADAPTER, + _p_in: *const iddcx::IDARG_IN_COMMITMODES2, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// Report `HIGH_COLOR_SPACE` so the OS enables the HDR10 wide-gamut/PQ target. Mandatory under FP16. +pub unsafe extern "C" fn query_target_info( + _adapter: iddcx::IDDCX_ADAPTER, + _p_in: *mut iddcx::IDARG_IN_QUERYTARGET_INFO, + p_out: *mut iddcx::IDARG_OUT_QUERYTARGET_INFO, +) -> NTSTATUS { + // SAFETY: p_out is the framework's (uninitialised) out buffer; zero then set the one field we report. + unsafe { + core::ptr::write(p_out, core::mem::zeroed()); + (*p_out).TargetCaps = iddcx::IDDCX_TARGET_CAPS::IDDCX_TARGET_CAPS_HIGH_COLOR_SPACE; + } + STATUS_SUCCESS +} + +/// Accept the OS's default HDR10 static metadata (the host/client own the stream's final metadata). +/// Mandatory under FP16. +pub unsafe extern "C" fn set_default_hdr_metadata( + _monitor: iddcx::IDDCX_MONITOR, + _p_in: *const iddcx::IDARG_IN_MONITOR_SET_DEFAULT_HDR_METADATA, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// Accept (do not apply) the gamma ramp — the client display applies its own transform. MANDATORY once +/// FP16 is set, or the OS rejects the adapter at init ("Failed to get adapter"). +pub unsafe extern "C" fn set_gamma_ramp( + _monitor: iddcx::IDDCX_MONITOR, + _p_in: *const iddcx::IDARG_IN_SET_GAMMARAMP, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// A swap-chain was assigned to the monitor. STEP 5: spawn the `SwapChainProcessor`. +pub unsafe extern "C" fn assign_swap_chain( + _monitor: iddcx::IDDCX_MONITOR, + _p_in: *const iddcx::IDARG_IN_SETSWAPCHAIN, +) -> NTSTATUS { + STATUS_SUCCESS +} + +/// The monitor went inactive. STEP 5: drop the processor (RAII joins the worker thread). +pub unsafe extern "C" fn unassign_swap_chain(_monitor: iddcx::IDDCX_MONITOR) -> NTSTATUS { + STATUS_SUCCESS +} + +/// The pf-vdisplay-proto control plane. Returns `()` and completes the request itself (matches the C +/// `EVT_IDD_CX_DEVICE_IO_CONTROL` shape). STEP 4: dispatch the proto IOCTLs; for now just complete. +pub unsafe extern "C" fn device_io_control( + _device: WDFDEVICE, + request: WDFREQUEST, + _output_len: usize, + _input_len: usize, + _ioctl_code: u32, +) { + // SAFETY: `request` is the framework-provided WDFREQUEST; completing it hands it back to the OS. + unsafe { + call_unsafe_wdf_function_binding!(WdfRequestComplete, request, STATUS_SUCCESS); + } +} diff --git a/packaging/windows/drivers/pf-vdisplay/src/edid.rs b/packaging/windows/drivers/pf-vdisplay/src/edid.rs new file mode 100644 index 0000000..9ee80e4 --- /dev/null +++ b/packaging/windows/drivers/pf-vdisplay/src/edid.rs @@ -0,0 +1,118 @@ +//! The 256-byte EDID the pf-vdisplay driver hands IddCx for each virtual monitor: a 128-byte EDID 1.4 +//! base block + a **CTA-861.3 extension** that advertises HDR — a BT.2020 Colorimetry Data Block and an +//! HDR Static Metadata Data Block declaring the SMPTE ST 2084 (PQ) EOTF. Windows reads a display's HDR +//! capability from this CTA HDR block; without it the monitor is treated as SDR-only regardless of the +//! IddCx adapter's `CAN_PROCESS_FP16` / `HIGH_COLOR_SPACE` / 10-bit mode caps (the missing piece that +//! made "Use HDR" never appear for the virtual display). The base block declares EDID 1.4 + 10-bit +//! digital so the panel's bit depth is unambiguous. +//! +//! Identity: manufacturer "PNK" (bytes 8-9), product name "punktfunk" (the 0xFC display descriptor). The +//! serial-number field (base offset 0x0C, little-endian) encodes the per-monitor index so +//! `parse_monitor_description` can map an EDID the OS hands back to its monitor; [`Edid::generate_with`] +//! patches that serial and recomputes BOTH block checksums (base byte 127 + extension byte 255). The +//! detailed-timing / range-limit descriptors are placeholders — the modes we actually advertise come +//! from the monitor's stored mode list (`monitor.rs` / `callbacks.rs`), not from parsing this EDID. + +use std::array::TryFromSliceError; + +/// Per-monitor serial number, base-block offset 0x0C, little-endian u32. +const SERIAL_OFFSET: usize = 0x0C; + +/// EDID 1.4 base block (128 bytes). Differs from a plain SDR virtual EDID only by: revision 1.4 (byte +/// 19 = 0x04), 10-bit digital video input (byte 20 = 0xB0), and one extension present (byte 126 = 0x01). +/// Byte 127 (checksum) and the serial (0x0C) are filled/patched in [`Edid::generate_with`]. +#[rustfmt::skip] +const BASE: [u8; 128] = [ + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, // fixed header + 0x41, 0xCB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mfr "PNK", product, serial (patched) + 0xFF, 0x21, 0x01, 0x04, 0xB0, 0x32, 0x1F, 0x78, // week/year, EDID 1.4, 10-bit digital, size, gamma + 0x03, 0x78, 0xB1, 0xB5, 0x4A, 0x2B, 0xCC, 0x21, // feature (sRGB-default CLEARED), BT.2020 primaries... + 0x0B, 0x50, 0x54, 0x00, 0x00, 0x00, 0x01, 0x01, // ...BT.2020 primaries, established timings, std timings + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3A, // std timings, DTD 1 (placeholder preferred timing) + 0x80, 0x18, 0x71, 0x38, 0x2D, 0x40, 0x58, 0x2C, + 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x00, 0xFD, 0x00, 0x17, 0xF0, 0x0F, // display range-limits descriptor + 0xFF, 0x0F, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x70, // name descriptor "punktfunk" + 0x75, 0x6E, 0x6B, 0x74, 0x66, 0x75, 0x6E, 0x6B, + 0x0A, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, // empty 4th descriptor... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, // ...byte 126 = 1 extension, byte 127 = checksum +]; + +/// CTA-861.3 extension block (128 bytes), block 1. Header + a Data Block Collection holding the +/// Colorimetry and HDR Static Metadata data blocks; the rest is padding up to the checksum (byte 255). +/// `D` (byte 130) marks where DTDs would start (= end of the data blocks); we carry none. +#[rustfmt::skip] +const CTA_HEADER: [u8; 4] = [ + 0x02, // CTA Extension tag + 0x03, // revision 3 (CTA-861.3 — required for the extended-tag data blocks below) + 0x0F, // D = 15: the (empty) DTD region starts at block byte 15, i.e. data blocks occupy bytes 4..15 + 0x00, // 0 native DTDs; no basic audio; no YCbCr 4:4:4/4:2:2 (RGB-only, matching the wire format) +]; + +/// Colorimetry Data Block (CTA extended tag 0x05): declare BT.2020 RGB (bit 7). YCbCr variants are left +/// clear — the IddCx wire format is RGB-only — and the gamut-metadata flags are 0. +#[rustfmt::skip] +const COLORIMETRY_DB: [u8; 4] = [ + 0xE3, // tag 0b111 (use-extended-tag) | length 3 + 0x05, // extended tag: Colorimetry + 0x80, // BT2020RGB (bit 7); xvYCC/sYCC/opRGB/BT2020 YCC/cYCC all clear + 0x00, // gamut metadata profiles MD0..MD3: none +]; + +/// HDR Static Metadata Data Block (CTA extended tag 0x06): EOTFs = Traditional SDR (ET_0) + SMPTE ST +/// 2084 / PQ (ET_2); Static Metadata Type 1 (SM_0). Plus the optional desired-content luminance hints +/// (~993 nit max, ~400 nit max-frame-average, ~0.05 nit min) so the block is complete. +#[rustfmt::skip] +const HDR_STATIC_METADATA_DB: [u8; 7] = [ + 0xE6, // tag 0b111 (use-extended-tag) | length 6 + 0x06, // extended tag: HDR Static Metadata + 0x05, // Supported EOTFs: ET_0 (traditional SDR) | ET_2 (SMPTE ST 2084 / PQ) + 0x01, // Supported Static Metadata Descriptors: SM_0 (Static Metadata Type 1) + 0x8A, // Desired Content Max Luminance (code 138 ≈ 993 nits) + 0x60, // Desired Content Max Frame-avg Lum. (code 96 = 400 nits) + 0x12, // Desired Content Min Luminance (code 18 ≈ 0.05 nits) +]; + +#[derive(Debug, Clone, Copy)] +pub struct Edid; + +impl Edid { + /// Build the full 256-byte EDID for monitor `serial`, with both block checksums recomputed. + pub fn generate_with(serial: u32) -> Vec { + let mut edid = [0u8; 256]; + // Block 0: base. + edid[..128].copy_from_slice(&BASE); + edid[SERIAL_OFFSET..SERIAL_OFFSET + 4].copy_from_slice(&serial.to_le_bytes()); + // Block 1: CTA-861.3 extension (header + colorimetry + HDR static metadata; rest stays 0). + edid[128..132].copy_from_slice(&CTA_HEADER); + edid[132..136].copy_from_slice(&COLORIMETRY_DB); + edid[136..143].copy_from_slice(&HDR_STATIC_METADATA_DB); + // Each 128-byte block ends in a checksum byte that makes the block sum ≡ 0 (mod 256). + Self::fix_block_checksum(&mut edid, 0); + Self::fix_block_checksum(&mut edid, 128); + edid.to_vec() + } + + /// Read the per-monitor serial (base offset 0x0C, little-endian) from an EDID the OS handed back. + /// Works for the full 256-byte EDID or just the 128-byte base block. Errors (rather than panics) on + /// a too-short buffer so the caller can reject a malformed descriptor. + pub fn get_serial(edid: &[u8]) -> Result { + let bytes: [u8; 4] = edid + .get(SERIAL_OFFSET..SERIAL_OFFSET + 4) + .unwrap_or(&[]) + .try_into()?; + Ok(u32::from_le_bytes(bytes)) + } + + /// Set the trailing byte of the 128-byte block at `start` so the block's bytes sum to 0 (mod 256) — + /// the standard EDID block checksum. + fn fix_block_checksum(edid: &mut [u8], start: usize) { + let sum = edid[start..start + 127] + .iter() + .fold(0u8, |acc, &b| acc.wrapping_add(b)); + edid[start + 127] = 0u8.wrapping_sub(sum); + } +} diff --git a/packaging/windows/drivers/pf-vdisplay/src/entry.rs b/packaging/windows/drivers/pf-vdisplay/src/entry.rs new file mode 100644 index 0000000..4041d47 --- /dev/null +++ b/packaging/windows/drivers/pf-vdisplay/src/entry.rs @@ -0,0 +1,112 @@ +//! DriverEntry + driver_add — the IddCx device bring-up (STEP 2 skeleton). wdk-build links the UMDF +//! `WdfDriverStubUm` whose `FxDriverEntryUm` forwards to the exported `DriverEntry`. Adapter creation is +//! deferred to the first `EvtDeviceD0Entry` (STEP 3); monitors are created on demand by the control +//! plane (STEP 4). + +use wdk_iddcx::nt_success; +use wdk_sys::{ + call_unsafe_wdf_function_binding, iddcx, GUID, NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, + PWDFDEVICE_INIT, ULONG, WDFDEVICE, WDFDRIVER, WDF_DRIVER_CONFIG, WDF_NO_HANDLE, + WDF_NO_OBJECT_ATTRIBUTES, WDF_PNPPOWER_EVENT_CALLBACKS, +}; + +use crate::{callbacks, size, STATUS_NOT_FOUND}; + +#[unsafe(export_name = "DriverEntry")] +pub unsafe extern "system" fn driver_entry( + driver: PDRIVER_OBJECT, + registry_path: PCUNICODE_STRING, +) -> NTSTATUS { + // SAFETY: zeroed then Size + the device-add callback set, per the WDF_DRIVER_CONFIG contract. + let mut config: WDF_DRIVER_CONFIG = unsafe { core::mem::zeroed() }; + config.Size = core::mem::size_of::() as ULONG; + config.EvtDriverDeviceAdd = Some(driver_add); + // SAFETY: driver + registry_path are loader-provided; config is valid for the call. + unsafe { + call_unsafe_wdf_function_binding!( + WdfDriverCreate, + driver, + registry_path, + WDF_NO_OBJECT_ATTRIBUTES, + &mut config, + WDF_NO_HANDLE.cast::() + ) + } +} + +extern "C" fn driver_add(_driver: WDFDRIVER, mut init: PWDFDEVICE_INIT) -> NTSTATUS { + // Defer adapter creation to the first D0 entry. + let mut pnp: WDF_PNPPOWER_EVENT_CALLBACKS = unsafe { core::mem::zeroed() }; + pnp.Size = core::mem::size_of::() as ULONG; + pnp.EvtDeviceD0Entry = Some(callbacks::device_d0_entry); + // SAFETY: init is the framework-provided device-init; pnp is valid for the call. + unsafe { + call_unsafe_wdf_function_binding!(WdfDeviceInitSetPnpPowerEventCallbacks, init, &mut pnp); + } + + // Build + size the IddCx client config (versioned size) and wire the 14 callbacks. + let Some(cfg_size) = size::idd_cx_client_config_size() else { + return STATUS_NOT_FOUND; + }; + let mut cfg: iddcx::IDD_CX_CLIENT_CONFIG = unsafe { core::mem::zeroed() }; + cfg.Size = cfg_size; + cfg.EvtIddCxAdapterInitFinished = Some(callbacks::adapter_init_finished); + cfg.EvtIddCxParseMonitorDescription = Some(callbacks::parse_monitor_description); + cfg.EvtIddCxMonitorGetDefaultDescriptionModes = Some(callbacks::monitor_get_default_modes); + cfg.EvtIddCxMonitorQueryTargetModes = Some(callbacks::monitor_query_modes); + cfg.EvtIddCxAdapterCommitModes = Some(callbacks::adapter_commit_modes); + cfg.EvtIddCxParseMonitorDescription2 = Some(callbacks::parse_monitor_description2); + cfg.EvtIddCxMonitorQueryTargetModes2 = Some(callbacks::monitor_query_modes2); + cfg.EvtIddCxAdapterCommitModes2 = Some(callbacks::adapter_commit_modes2); + cfg.EvtIddCxAdapterQueryTargetInfo = Some(callbacks::query_target_info); + cfg.EvtIddCxMonitorSetDefaultHdrMetaData = Some(callbacks::set_default_hdr_metadata); + cfg.EvtIddCxMonitorSetGammaRamp = Some(callbacks::set_gamma_ramp); + cfg.EvtIddCxMonitorAssignSwapChain = Some(callbacks::assign_swap_chain); + cfg.EvtIddCxMonitorUnassignSwapChain = Some(callbacks::unassign_swap_chain); + cfg.EvtIddCxDeviceIoControl = Some(callbacks::device_io_control); + + // SAFETY: init is the framework device-init; cfg is fully populated + sized. (Links IddCxStub.) + let status = unsafe { wdk_iddcx::IddCxDeviceInitConfig(init, &cfg) }; + if !nt_success(status) { + return status; + } + + let mut device: WDFDEVICE = core::ptr::null_mut(); + // SAFETY: init configured above; no context attributes yet (STEP 3 adds DeviceContext + cleanup). + let status = unsafe { + call_unsafe_wdf_function_binding!( + WdfDeviceCreate, + &mut init, + WDF_NO_OBJECT_ATTRIBUTES, + &mut device + ) + }; + if !nt_success(status) { + return status; + } + + // Expose the owned pf-vdisplay control interface (the host opens this GUID; STEP 4 wires the host + // side in lockstep). NOT SudoVDA's GUID. + let (d1, d2, d3, d4) = pf_vdisplay_proto::interface_guid_fields(); + let guid = GUID { + Data1: d1, + Data2: d2, + Data3: d3, + Data4: d4, + }; + // SAFETY: device is the just-created WDFDEVICE; guid lives for the call; no reference string. + let status = unsafe { + call_unsafe_wdf_function_binding!( + WdfDeviceCreateDeviceInterface, + device, + &guid, + core::ptr::null() + ) + }; + if !nt_success(status) { + return status; + } + + // SAFETY: device is the just-created WDFDEVICE. + unsafe { wdk_iddcx::IddCxDeviceInitialize(device) } +} diff --git a/packaging/windows/drivers/pf-vdisplay/src/lib.rs b/packaging/windows/drivers/pf-vdisplay/src/lib.rs index 8607e78..260d33e 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/lib.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/lib.rs @@ -1,70 +1,29 @@ //! pf-vdisplay — the all-Rust UMDF IddCx virtual-display driver (M1 step-2 rewrite, on wdk-sys + the //! owned pf-vdisplay-proto ABI). See docs/windows-host-rewrite.md §14 for the full port plan. //! -//! STEP 0 (this commit): the workspace scaffold + the std-under-UMDF LINK GATE. DriverEntry → -//! WdfDriverCreate → (EvtDeviceAdd) WdfDeviceCreate, plus a `#[used]` probe that forces `std::thread` -//! + `OwnedHandle` to link — the SwapChainProcessor (STEP 5) depends on both, and the wdk-build UMDF -//! link settings `/NODEFAULTLIB:kernel32.lib`, so std must resolve via OneCoreUAP. If this fails to -//! link, the worker-thread/handle design needs a CreateThread shim BEFORE any callback work (port-plan -//! critique gap #9). The adapter/monitor/swapchain/IDD-push logic lands in STEP 2-6. +//! STEP 2: the IddCx driver SKELETON — DriverEntry → driver_add builds the full `IDD_CX_CLIENT_CONFIG` +//! (14 IddCx callbacks + the PnP `EvtDeviceD0Entry`, all stubs) sized via the versioned +//! [`size::idd_cx_client_config_size`], runs `IddCxDeviceInitConfig` → `WdfDeviceCreate` → +//! `WdfDeviceCreateDeviceInterface`(the owned pf-vdisplay GUID) → `IddCxDeviceInitialize`, and exports +//! `IddMinimumVersionRequired`. This links `IddCxStub` (the CI gate). The real adapter init (STEP 3), +//! control plane + monitor/modes (STEP 4), and swap-chain/IDD-push (STEP 5-6) fill the stubs in. #![allow(non_snake_case, clippy::missing_safety_doc)] -use wdk_sys::{ - call_unsafe_wdf_function_binding, NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, - ULONG, WDFDEVICE, WDFDRIVER, WDF_DRIVER_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES, -}; +mod callbacks; +mod edid; +mod entry; +mod size; -const STATUS_SUCCESS: NTSTATUS = 0; +use wdk_sys::NTSTATUS; -/// STEP-0 link gate (port-plan critique #9). Forces `std::thread` + `std::os::windows::io::OwnedHandle` -/// to be linked into the UMDF cdylib so STEP 0's `cargo build` actually proves the std surface resolves -/// under the wdk-build link settings (kernel32 is `/NODEFAULTLIB`'d → std must come via OneCoreUAP). -/// Never called; `#[used]` keeps the symbol so the linker pulls std. Also touches `wdk-iddcx` to prove -/// the pf-vdisplay → wdk-iddcx → wdk-sys/iddcx dependency graph resolves. -fn _std_link_gate() { - use std::os::windows::io::OwnedHandle; - let t = std::thread::spawn(|| 0u32); - let _ = t.join(); - let _drop_owned: fn(OwnedHandle) = core::mem::drop; - // touch the iddcx surface via wdk-iddcx (forces the feature-enabled dep graph) - let _adapter: Option = None; -} -#[used] -static _STD_LINK_GATE: fn() = _std_link_gate; +// NTSTATUS codes the driver returns (wdk-sys doesn't surface all of these as constants). +pub(crate) const STATUS_SUCCESS: NTSTATUS = 0; +pub(crate) const STATUS_NOT_IMPLEMENTED: NTSTATUS = 0xC000_0002u32 as NTSTATUS; +pub(crate) const STATUS_NOT_FOUND: NTSTATUS = 0xC000_0225u32 as NTSTATUS; -#[unsafe(export_name = "DriverEntry")] -pub unsafe extern "system" fn driver_entry( - driver: PDRIVER_OBJECT, - registry_path: PCUNICODE_STRING, -) -> NTSTATUS { - // SAFETY: zeroed then Size + the device-add callback set, per the WDF_DRIVER_CONFIG contract. - let mut config: WDF_DRIVER_CONFIG = unsafe { core::mem::zeroed() }; - config.Size = core::mem::size_of::() as ULONG; - config.EvtDriverDeviceAdd = Some(evt_device_add); - // SAFETY: driver + registry_path are loader-provided; config is valid for the call. - unsafe { - call_unsafe_wdf_function_binding!( - WdfDriverCreate, - driver, - registry_path, - WDF_NO_OBJECT_ATTRIBUTES, - &mut config, - WDF_NO_HANDLE.cast::() - ) - } -} - -extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INIT) -> NTSTATUS { - let mut device: WDFDEVICE = core::ptr::null_mut(); - // SAFETY: device_init is the framework-provided init; attributes null; device receives the handle. - let _ = unsafe { - call_unsafe_wdf_function_binding!( - WdfDeviceCreate, - &mut device_init, - WDF_NO_OBJECT_ATTRIBUTES, - &mut device - ) - }; - STATUS_SUCCESS -} +/// 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 +/// `IDD_STUB`. `4` matches the proven `wdf-umdf` oracle. +#[unsafe(no_mangle)] +pub static IddMinimumVersionRequired: wdk_sys::ULONG = 4; diff --git a/packaging/windows/drivers/pf-vdisplay/src/size.rs b/packaging/windows/drivers/pf-vdisplay/src/size.rs new file mode 100644 index 0000000..70069fb --- /dev/null +++ b/packaging/windows/drivers/pf-vdisplay/src/size.rs @@ -0,0 +1,30 @@ +//! Versioned IddCx struct sizing — the oracle's `IDD_STRUCTURE_SIZE!` ported to wdk-sys. +//! +//! IddCx structs are versioned: if the running framework is OLDER than the (1.10) headers we built +//! against, our locally-compiled struct may be LARGER than the framework understands, so `.Size` must +//! come from the framework's own size table (`IddStructures[INDEX_]`), not `size_of`. `None` +//! means the struct is unusable on this framework. When the framework is at least our version, +//! `size_of` is correct. (wdk-sys uses ModuleConsts: `_IDDSTRUCTENUM::INDEX_*`, not the oracle's +//! NewType `.0`.) + +use wdk_sys::iddcx; + +/// Correct `.Size` for `IDD_CX_CLIENT_CONFIG`, or `None` if it can't be used on this framework. +#[must_use] +pub fn idd_cx_client_config_size() -> Option { + // SAFETY: read-only access to the stub-provided framework globals. + let higher = unsafe { (&raw const iddcx::IddClientVersionHigherThanFramework).read() } != 0; + if !higher { + return u32::try_from(core::mem::size_of::()).ok(); + } + // SAFETY: read-only. + let count = unsafe { (&raw const iddcx::IddStructureCount).read() }; + let index = iddcx::_IDDSTRUCTENUM::INDEX_IDD_CX_CLIENT_CONFIG as u32; + if index >= count { + return None; // struct cannot be used on this (older) framework + } + // SAFETY: `IddStructures` is the framework's size table; `index` is validated `< count`. + let table = unsafe { (&raw const iddcx::IddStructures).read() }; + let size = unsafe { table.add(index as usize).read() }; + u32::try_from(size).ok() +}