diff --git a/packaging/windows/drivers/pf-vdisplay/build.rs b/packaging/windows/drivers/pf-vdisplay/build.rs index a11bf57..bc7bdd4 100644 --- a/packaging/windows/drivers/pf-vdisplay/build.rs +++ b/packaging/windows/drivers/pf-vdisplay/build.rs @@ -7,15 +7,18 @@ fn main() -> Result<(), wdk_build::ConfigError> { 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. +/// Link `IddCxStub.lib`. It ships under `Lib\\um\\iddcx\\`, and the iddcx +/// versions are NOT equivalent: the `1.0`/`1.2` stubs lack the versioned-struct-size table symbols +/// (`IddStructures`/`IddStructureCount`/`IddClientVersionHigherThanFramework`) that `size.rs` needs — +/// `1.3`+ and `1.10` have them. So pick the HIGHEST `iddcx\` dir that has the lib (version-aware, +/// since "1.10" < "1.2" lexically). 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", ]; + let mut best: Option<((u32, u32), std::path::PathBuf)> = None; for root in ROOTS { let Ok(versions) = std::fs::read_dir(root) else { continue; @@ -26,13 +29,24 @@ fn link_iddcx_stub() { 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; + if !sub.path().join("IddCxStub.lib").is_file() { + continue; + } + let name = sub.file_name().to_string_lossy().into_owned(); + let mut parts = name.split('.'); + let v = ( + parts.next().and_then(|x| x.parse().ok()).unwrap_or(0u32), + parts.next().and_then(|x| x.parse().ok()).unwrap_or(0u32), + ); + if best.as_ref().is_none_or(|(bv, _)| v > *bv) { + best = Some((v, sub.path())); } } } } - panic!("IddCxStub.lib not found under any Windows Kits Lib\\\\um\\{ARCH}\\iddcx\\\\"); + let Some((_, dir)) = best else { + panic!("IddCxStub.lib not found under any Windows Kits Lib\\\\um\\{ARCH}\\iddcx\\\\"); + }; + println!("cargo:rustc-link-search={}", dir.display()); + println!("cargo:rustc-link-lib=static=IddCxStub"); } diff --git a/packaging/windows/drivers/pf-vdisplay/src/adapter.rs b/packaging/windows/drivers/pf-vdisplay/src/adapter.rs index 58e7285..55ada6c 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/adapter.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/adapter.rs @@ -43,31 +43,81 @@ pub fn init_adapter(device: WDFDEVICE) -> NTSTATUS { if ADAPTER.get().is_some() { return STATUS_SUCCESS; } + dbglog!("[pf-vd] init_adapter"); - // Endpoint diagnostics (telemetry only — not used for OS runtime decisions). `pEndPointModelName` - // must be a non-empty string; the rest are optional. GammaSupport stays NONE (zeroed). + // The framework binds an IddCx version (the INF's UmdfExtensions) that may be OLDER than our 1.10 + // headers, so use ITS expected struct sizes — newer fields (e.g. IDDCX_ADAPTER_CAPS's + // StaticDesktopReencodeFrameCount) make size_of too big and IddCxAdapterInitAsync rejects it. The + // table is readable now (post-IddCxDeviceInitialize). Falls back to size_of if unavailable. + let ver_size = crate::size::framework_struct_size( + iddcx::_IDDSTRUCTENUM::INDEX_IDDCX_ENDPOINT_VERSION as u32, + ) + .unwrap_or(core::mem::size_of::() as u32); + let diag_size = crate::size::framework_struct_size( + iddcx::_IDDSTRUCTENUM::INDEX_IDDCX_ENDPOINT_DIAGNOSTIC_INFO as u32, + ) + .unwrap_or(core::mem::size_of::() as u32); + let caps_size = crate::size::framework_struct_size( + iddcx::_IDDSTRUCTENUM::INDEX_IDDCX_ADAPTER_CAPS as u32, + ) + .unwrap_or(core::mem::size_of::() as u32); + dbglog!("[pf-vd] fw sizes: caps={caps_size} diag={diag_size} ver={ver_size}"); + + // Firmware/hardware version (telemetry). The oracle points BOTH at one IDDCX_ENDPOINT_VERSION. + // `version` is a stack local read synchronously by IddCxAdapterInitAsync (same as the oracle). + let mut version: iddcx::IDDCX_ENDPOINT_VERSION = unsafe { core::mem::zeroed() }; + version.Size = ver_size; + version.MajorVer = env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap_or(0); + version.MinorVer = env!("CARGO_PKG_VERSION_MINOR").parse().unwrap_or(0); + version.Build = env!("CARGO_PKG_VERSION_PATCH").parse().unwrap_or(0); + + // Endpoint diagnostics. `pEndPointModelName` must be a non-empty string. GammaSupport stays NONE. let mut diag: iddcx::IDDCX_ENDPOINT_DIAGNOSTIC_INFO = unsafe { core::mem::zeroed() }; - diag.Size = core::mem::size_of::() as u32; + diag.Size = diag_size; diag.TransmissionType = iddcx::IDDCX_TRANSMISSION_TYPE::IDDCX_TRANSMISSION_TYPE_WIRED_OTHER; diag.pEndPointFriendlyName = wstr!("punktfunk Virtual Display Adapter"); diag.pEndPointManufacturerName = wstr!("punktfunk"); diag.pEndPointModelName = wstr!("Virtual Display"); + diag.pFirmwareVersion = (&raw mut version).cast(); + diag.pHardwareVersion = (&raw mut version).cast(); let mut caps: iddcx::IDDCX_ADAPTER_CAPS = unsafe { core::mem::zeroed() }; - caps.Size = core::mem::size_of::() as u32; + caps.Size = caps_size; + // CAN_PROCESS_FP16 must be CONSISTENT with the config's callbacks: the config registers the *2 / gamma + // / set-default-hdr-metadata callbacks (FP16-obligated), so the adapter MUST advertise FP16 or the + // framework rejects the mismatch at IddCxAdapterInitAsync. The oracle sets both. caps.Flags = iddcx::IDDCX_ADAPTER_FLAGS::IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16; caps.MaxMonitorsSupported = 16; caps.EndPointDiagnostics = diag; + dbglog!( + "[pf-vd] caps Size={} Flags={:#x} MaxMon={} diagSize={} cfgSizeOf={} capsSizeOf={}", + caps.Size, + caps.Flags, + caps.MaxMonitorsSupported, + caps.EndPointDiagnostics.Size, + core::mem::size_of::(), + core::mem::size_of::(), + ); + // The adapter WDF object's attributes. The oracle passes an init'd WDF_OBJECT_ATTRIBUTES (Size + + // Synchronization/Execution = InheritFromParent — NOT zeroed, since zero = *Invalid*); a null/zeroed + // one is what IddCxAdapterInitAsync rejected. No context type yet (STEP 3). + let mut attr: wdk_sys::WDF_OBJECT_ATTRIBUTES = unsafe { core::mem::zeroed() }; + attr.Size = core::mem::size_of::() as u32; + attr.ExecutionLevel = wdk_sys::_WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent; + attr.SynchronizationScope = + wdk_sys::_WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent; let init = iddcx::IDARG_IN_ADAPTER_INIT { WdfDevice: device, pCaps: &raw mut caps, - ObjectAttributes: core::ptr::null_mut(), + ObjectAttributes: &raw mut attr, }; let mut out: iddcx::IDARG_OUT_ADAPTER_INIT = unsafe { core::mem::zeroed() }; // SAFETY: `init`/`out` are valid local storage; IddCxAdapterInitAsync reads the caps synchronously // (the adapter object itself is delivered later via adapter_init_finished). Called once per device. - unsafe { wdk_iddcx::IddCxAdapterInitAsync(&init, &mut out) } + let st = unsafe { wdk_iddcx::IddCxAdapterInitAsync(&init, &mut out) }; + dbglog!("[pf-vd] IddCxAdapterInitAsync -> {st:#x}"); + st } /// Stash the adapter object delivered by `EvtIddCxAdapterInitFinished` (STEP 4 reads it). diff --git a/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs b/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs index 1f76f6f..db05fde 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs @@ -17,6 +17,7 @@ pub unsafe extern "C" fn device_d0_entry( device: WDFDEVICE, _previous_state: wdk_sys::WDF_POWER_DEVICE_STATE, ) -> NTSTATUS { + dbglog!("[pf-vd] device_d0_entry"); crate::adapter::init_adapter(device) } @@ -26,6 +27,7 @@ pub unsafe extern "C" fn adapter_init_finished( adapter: iddcx::IDDCX_ADAPTER, _p_in: *const iddcx::IDARG_IN_ADAPTER_INIT_FINISHED, ) -> NTSTATUS { + dbglog!("[pf-vd] adapter_init_finished"); crate::adapter::set_adapter(adapter); STATUS_SUCCESS } diff --git a/packaging/windows/drivers/pf-vdisplay/src/entry.rs b/packaging/windows/drivers/pf-vdisplay/src/entry.rs index 4041d47..80cfa2f 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/entry.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/entry.rs @@ -1,7 +1,7 @@ -//! DriverEntry + driver_add — the IddCx device bring-up (STEP 2 skeleton). wdk-build links the UMDF +//! DriverEntry + driver_add — the IddCx device bring-up (STEP 2/3). 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). +//! plane (STEP 4). Instrumented with `dbglog!` for on-glass bring-up. use wdk_iddcx::nt_success; use wdk_sys::{ @@ -17,12 +17,13 @@ pub unsafe extern "system" fn driver_entry( driver: PDRIVER_OBJECT, registry_path: PCUNICODE_STRING, ) -> NTSTATUS { + dbglog!("[pf-vd] DriverEntry"); // 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 { + let st = unsafe { call_unsafe_wdf_function_binding!( WdfDriverCreate, driver, @@ -31,10 +32,13 @@ pub unsafe extern "system" fn driver_entry( &mut config, WDF_NO_HANDLE.cast::() ) - } + }; + dbglog!("[pf-vd] WdfDriverCreate -> {st:#x}"); + st } extern "C" fn driver_add(_driver: WDFDRIVER, mut init: PWDFDEVICE_INIT) -> NTSTATUS { + dbglog!("[pf-vd] driver_add"); // 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; @@ -46,6 +50,7 @@ extern "C" fn driver_add(_driver: WDFDRIVER, mut init: PWDFDEVICE_INIT) -> NTSTA // Build + size the IddCx client config (versioned size) and wire the 14 callbacks. let Some(cfg_size) = size::idd_cx_client_config_size() else { + dbglog!("[pf-vd] config size unavailable"); return STATUS_NOT_FOUND; }; let mut cfg: iddcx::IDD_CX_CLIENT_CONFIG = unsafe { core::mem::zeroed() }; @@ -67,6 +72,7 @@ extern "C" fn driver_add(_driver: WDFDRIVER, mut init: PWDFDEVICE_INIT) -> NTSTA // SAFETY: init is the framework device-init; cfg is fully populated + sized. (Links IddCxStub.) let status = unsafe { wdk_iddcx::IddCxDeviceInitConfig(init, &cfg) }; + dbglog!("[pf-vd] IddCxDeviceInitConfig -> {status:#x}"); if !nt_success(status) { return status; } @@ -81,6 +87,7 @@ extern "C" fn driver_add(_driver: WDFDRIVER, mut init: PWDFDEVICE_INIT) -> NTSTA &mut device ) }; + dbglog!("[pf-vd] WdfDeviceCreate -> {status:#x}"); if !nt_success(status) { return status; } @@ -103,10 +110,13 @@ extern "C" fn driver_add(_driver: WDFDRIVER, mut init: PWDFDEVICE_INIT) -> NTSTA core::ptr::null() ) }; + dbglog!("[pf-vd] WdfDeviceCreateDeviceInterface -> {status:#x}"); if !nt_success(status) { return status; } // SAFETY: device is the just-created WDFDEVICE. - unsafe { wdk_iddcx::IddCxDeviceInitialize(device) } + let status = unsafe { wdk_iddcx::IddCxDeviceInitialize(device) }; + dbglog!("[pf-vd] IddCxDeviceInitialize -> {status:#x}"); + status } diff --git a/packaging/windows/drivers/pf-vdisplay/src/lib.rs b/packaging/windows/drivers/pf-vdisplay/src/lib.rs index e2d2c38..7ad0442 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/lib.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/lib.rs @@ -10,6 +10,8 @@ #![allow(non_snake_case, clippy::missing_safety_doc)] +#[macro_use] +mod log; mod adapter; mod callbacks; #[allow(dead_code)] // salvaged verbatim; wired into the mode callbacks in STEP 4 diff --git a/packaging/windows/drivers/pf-vdisplay/src/log.rs b/packaging/windows/drivers/pf-vdisplay/src/log.rs new file mode 100644 index 0000000..add0975 --- /dev/null +++ b/packaging/windows/drivers/pf-vdisplay/src/log.rs @@ -0,0 +1,26 @@ +//! Minimal driver logger (matches the DualSense driver). DebugView can't capture the UMDF host across +//! session 0, so besides `OutputDebugStringA` we append to a world-writable file readable over SSH. Used +//! only for bring-up/diagnostics; cheap and best-effort (ignores all errors). + +unsafe extern "system" { + fn OutputDebugStringA(s: *const u8); +} + +pub fn log(s: &str) { + if let Ok(c) = std::ffi::CString::new(s) { + // SAFETY: `c` is a valid NUL-terminated string for the duration of the call. + unsafe { OutputDebugStringA(c.as_ptr().cast()) }; + } + use std::io::Write; + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("C:\\Users\\Public\\pfvd-driver.log") + { + let _ = writeln!(f, "{s}"); + } +} + +macro_rules! dbglog { + ($($a:tt)*) => { $crate::log::log(&::std::format!($($a)*)) }; +} diff --git a/packaging/windows/drivers/pf-vdisplay/src/size.rs b/packaging/windows/drivers/pf-vdisplay/src/size.rs index 2cf126e..ac5193f 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/size.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/size.rs @@ -1,19 +1,59 @@ -//! `.Size` for `IDD_CX_CLIENT_CONFIG`. +//! `.Size` for `IDD_CX_CLIENT_CONFIG` — the oracle's `IDD_STRUCTURE_SIZE!`, ported. //! -//! The oracle uses a *versioned* size — `IddStructures[INDEX]` when the running framework is OLDER than -//! the (1.10) headers we built against (`IddClientVersionHigherThanFramework != 0`). That machinery -//! (`IddClientVersionHigherThanFramework` / `IddStructureCount` / `IddStructures`) only exists in the -//! iddcx ≥1.4 `IddCxStub`; the WDK on the runner/box links the **1.0** stub (the only `IddCxStub.lib` -//! present), which does NOT export those symbols — referencing them is an LNK2019. We target IddCx 1.10 -//! against a current framework (framework ≥ client ⇒ `higher == false`), where `size_of` is exactly what -//! the versioned path returns. So use `size_of` directly. (Revisit the versioned path — with a ≥1.4 -//! `IddCxStub` linked — only if pre-1.10 Windows must ever be supported, which the punktfunk Windows -//! host does not target.) +//! IddCx structs are versioned. If the client (our headers) is NEWER than the running framework +//! (`IddClientVersionHigherThanFramework != 0`), `size_of` of our struct is too big, so the framework's +//! own `IddStructures[INDEX]` size must be used. Otherwise `size_of` is correct. `IddStructures` is null +//! until IddCx initialises, so it must ONLY be read in the `higher` branch (the framework populates it +//! exactly then). build.rs links the `iddcx>=1.3` stub that exports these symbols. Logged so we can see +//! which branch + value the box actually takes. use wdk_sys::iddcx; -/// Correct `.Size` for `IDD_CX_CLIENT_CONFIG` on a framework at least as new as our headers. +/// The correct `.Size` for `IDD_CX_CLIENT_CONFIG`, or `None` if the struct is unusable on this framework. #[must_use] pub fn idd_cx_client_config_size() -> Option { - u32::try_from(core::mem::size_of::()).ok() + let local = core::mem::size_of::(); + // SAFETY: BOOLEAN static, read-only. + let higher = unsafe { (&raw const iddcx::IddClientVersionHigherThanFramework).read() } != 0; + dbglog!("[pf-vd] cfg size: higher={higher} size_of={local}"); + if !higher { + return u32::try_from(local).ok(); + } + // SAFETY: read-only; the framework populates the size table exactly when `higher` is true. + let count = unsafe { (&raw const iddcx::IddStructureCount).read() }; + let index = iddcx::_IDDSTRUCTENUM::INDEX_IDD_CX_CLIENT_CONFIG as u32; + if index >= count { + dbglog!("[pf-vd] cfg size: index {index} >= count {count} -> None"); + return None; + } + // SAFETY: `IddStructures` is the framework's size table; `index` validated `< count`. + let table = unsafe { (&raw const iddcx::IddStructures).read() }; + let fw = unsafe { table.add(index as usize).read() }; + dbglog!("[pf-vd] cfg size: framework={fw}"); + u32::try_from(fw).ok() +} + +/// The framework's expected `.Size` for the `_IDDSTRUCTENUM` struct at `index`, or `None` if the size +/// table isn't usable (index out of range / `IddStructures` not yet populated). The framework binds a +/// specific IddCx version (the INF's `UmdfExtensions`), which can be OLDER than our headers — newer +/// fields (e.g. `IDDCX_ADAPTER_CAPS::StaticDesktopReencodeFrameCount`) make `size_of` too big and +/// `IddCxAdapterInitAsync` rejects it; this returns the size the framework actually expects. +/// +/// Only call once the IddCx context is initialised (e.g. from `EvtDeviceD0Entry`, after +/// `IddCxDeviceInitialize`) — `IddStructures` may be null before that. +#[must_use] +pub fn framework_struct_size(index: u32) -> Option { + // SAFETY: read-only. + let count = unsafe { (&raw const iddcx::IddStructureCount).read() }; + if index >= count { + return None; + } + // SAFETY: read-only; `IddStructures` is the framework size table. + let table = unsafe { (&raw const iddcx::IddStructures).read() }; + if table.is_null() { + return None; + } + // SAFETY: `index` validated `< count`; `table` non-null. + let size = unsafe { table.add(index as usize).read() }; + u32::try_from(size).ok() }