diff --git a/packaging/windows/drivers/pf-vdisplay/src/adapter.rs b/packaging/windows/drivers/pf-vdisplay/src/adapter.rs index 08c571d..2e9b198 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/adapter.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/adapter.rs @@ -88,10 +88,14 @@ pub fn init_adapter(device: WDFDEVICE) -> NTSTATUS { let mut caps: iddcx::IDDCX_ADAPTER_CAPS = unsafe { core::mem::zeroed() }; caps.Size = core::mem::size_of::() as u32; - // Flags = NONE (SDR). The working upstream virtual-display-rs sets NO Flags. CAN_PROCESS_FP16 requires a - // newer IddCx contract than the INF's UmdfExtensions=IddCx0102 grants, so the framework's adapter-caps - // Validate (ddivalidation.cpp:797) rejects it with STATUS_INVALID_PARAMETER. HDR/FP16 is deferred to - // STEP 7 (needs a higher UmdfExtensions binding + the obligated *2/HDR DDIs). + // STEP 7 (HDR): declare we can process FP16 (scRGB) desktop surfaces — this is what marks the virtual + // monitor advanced-color-capable (→ the host sees display_hdr=true → the "Use HDR" toggle appears). The + // ONLY reason STEP 3 rejected this flag was setting it WITHOUT the obligated *2/HDR DDIs; those are now + // registered in entry.rs (parse_monitor_description2/monitor_query_modes2/adapter_commit_modes2 + + // query_target_info/set_default_hdr_metadata/set_gamma_ramp). The proven oracle sets exactly this flag + // with the INF still at UmdfExtensions=IddCx0102. GammaSupport stays NONE (set above). Enum is bindgen + // ModuleConsts — the variant is a plain-int const assignable straight to the `Flags` field. + caps.Flags = iddcx::IDDCX_ADAPTER_FLAGS::IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16; caps.MaxMonitorsSupported = 16; caps.EndPointDiagnostics = diag; diff --git a/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs b/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs index 05e5f19..917cc1d 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/callbacks.rs @@ -81,11 +81,53 @@ pub unsafe extern "C" fn parse_monitor_description( STATUS_SUCCESS } -/// HDR (`*2`) mode list — writes `IDDCX_MONITOR_MODE2` (+BitsPerComponent). Mandatory under FP16. +/// HDR (`*2`) mode list — writes `IDDCX_MONITOR_MODE2` (+BitsPerComponent). Mandatory under FP16. Mirrors +/// the v1 `parse_monitor_description` exactly (EDID-serial lookup → count-then-fill, same +/// BUFFER_TOO_SMALL/SUCCESS logic), but emits `IDDCX_MONITOR_MODE2` with the per-mode wire bit-depth so the +/// OS offers HDR10 modes. `IDARG_OUT_PARSEMONITORDESCRIPTION` is the SAME out struct as v1 (shared by the C +/// header); only the in-args / mode struct are the `*2` variants. pub unsafe extern "C" fn parse_monitor_description2( - _p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION2, - _p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION, + p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION2, + p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION, ) -> 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::(), + 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_MODE2 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_MODE2 = unsafe { core::mem::zeroed() }; + mode.Size = core::mem::size_of::() 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); + mode.BitsPerComponent = crate::monitor::wire_bits(); + *slot = mode; + } + out_args.PreferredMonitorModeIdx = 0; STATUS_SUCCESS } @@ -122,12 +164,30 @@ pub unsafe extern "C" fn monitor_query_modes( STATUS_SUCCESS } -/// HDR (`*2`) target modes — writes `IDDCX_TARGET_MODE2`. Mandatory under FP16. +/// HDR (`*2`) target modes — writes `IDDCX_TARGET_MODE2`. Mandatory under FP16. Mirrors the v1 +/// `monitor_query_modes` exactly (pointer-match the monitor → count → fill), but emits `IDDCX_TARGET_MODE2` +/// (with per-mode wire bit-depth) via `monitor::target_mode2`. `IDARG_OUT_QUERYTARGETMODES` is the SAME out +/// struct as v1. 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, + monitor: iddcx::IDDCX_MONITOR, + p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES2, + p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES, ) -> 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_MODE2 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_mode2(item.width, item.height, item.refresh_rate); + } + } STATUS_SUCCESS } diff --git a/packaging/windows/drivers/pf-vdisplay/src/entry.rs b/packaging/windows/drivers/pf-vdisplay/src/entry.rs index 1b86b23..d342069 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/entry.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/entry.rs @@ -76,10 +76,18 @@ extern "C" fn driver_add(_driver: WDFDRIVER, mut init: PWDFDEVICE_INIT) -> NTSTA cfg.EvtIddCxMonitorGetDefaultDescriptionModes = Some(callbacks::monitor_get_default_modes); cfg.EvtIddCxMonitorQueryTargetModes = Some(callbacks::monitor_query_modes); cfg.EvtIddCxAdapterCommitModes = Some(callbacks::adapter_commit_modes); - // SDR config — matches the working upstream virtual-display-rs. The *2 / gamma / HDR-metadata / - // query-target-info callbacks are FP16-OBLIGATED: registering them while caps declare no FP16 makes the - // framework's adapter Validate reject (ddivalidation.cpp:797 -> INVALID_PARAMETER). They return with - // FP16 caps under a higher UmdfExtensions binding in STEP 7 (HDR). + // STEP 7 (HDR): the *2 mode DDIs + the gamma/HDR-metadata/query-target-info callbacks. The adapter + // caps now set CAN_PROCESS_FP16 (adapter.rs), which OBLIGATES this whole set — without them the OS + // rejects the adapter at init ("Failed to get adapter"). The proven oracle (entry.rs) registers the *2 + // variants ALONGSIDE the v1 callbacks above (NOT instead of them) — the OS prefers the *2 on IddCx + // 1.10 and falls back to v1 down-level — so we replicate exactly: keep both. The framework no longer + // rejects the *2 set because the FP16 cap is now present (the only reason STEP 3 had to drop them). + 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); diff --git a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs index 9c0a03c..a5a4645 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs @@ -142,6 +142,35 @@ pub fn target_mode(width: u32, height: u32, refresh_rate: u32) -> iddcx::IDDCX_T tm } +/// Wire bit-depth advertised per mode in the `*2` (HDR) mode DDIs. STEP 7: advertise BOTH 8 and 10 bpc +/// RGB (so the OS offers HDR10 modes), no YCbCr. The wdk-sys bindgen enum is `ModuleConsts`, so each +/// `IDDCX_BITS_PER_COMPONENT_*` is a plain-int const and the `IDDCX_WIRE_BITS_PER_COMPONENT` fields are +/// plain ints — OR the constants directly (NO newtype `.0` like the oracle's wdf-umdf-sys binding). Field +/// names (Rgb/YCbCr444/YCbCr422/YCbCr420, IDDCX_BITS_PER_COMPONENT_8/_10/_NONE) are the verbatim C header +/// names, identical across both bindings. +pub fn wire_bits() -> iddcx::IDDCX_WIRE_BITS_PER_COMPONENT { + let rgb = iddcx::IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_8 + | iddcx::IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_10; + let mut w: iddcx::IDDCX_WIRE_BITS_PER_COMPONENT = unsafe { core::mem::zeroed() }; + w.Rgb = rgb; + w.YCbCr444 = iddcx::IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE; + w.YCbCr422 = iddcx::IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE; + w.YCbCr420 = iddcx::IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE; + w +} + +/// `IDDCX_TARGET_MODE2` for a scan-out mode (HDR `*2` path): builds the v1 [`target_mode`] and copies its +/// `TargetVideoSignalInfo`, then stamps the `*2` Size + per-mode wire bit-depth ([`wire_bits`]). Rest +/// zeroed. +pub fn target_mode2(width: u32, height: u32, refresh_rate: u32) -> iddcx::IDDCX_TARGET_MODE2 { + let m1 = target_mode(width, height, refresh_rate); + let mut tm: iddcx::IDDCX_TARGET_MODE2 = unsafe { core::mem::zeroed() }; + tm.Size = core::mem::size_of::() as u32; + tm.TargetVideoSignalInfo = m1.TargetVideoSignalInfo; + tm.BitsPerComponent = wire_bits(); + 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> { MONITOR_MODES