feat(windows): pf-vdisplay — all-Rust IddCx virtual display (replaces SudoVDA)

P1 done: a pure-Rust UMDF2 IddCx driver, drop-in compatible with the host's
existing vdisplay/sudovda.rs control plane (the {e5bcc234} interface + the
SudoVDA IOCTL ABI), so the host drives it unchanged. Validated streaming on
glass at 5120x1440@240 — steady 240 fps, ~2.4 ms encode, clean teardown, full
parity with SudoVDA.

- Vendored wdf-umdf-sys / wdf-umdf bindgen crates (MIT, from virtual-display-rs)
  + the SDK-version build.rs fix that resolves the IddCxStub lib path by the WDK
  version actually containing um\x64\iddcx, not the max base SDK.
- pf-vdisplay crate: entry/callbacks/context/control/monitor/edid/
  swap_chain_processor. Our OWN 128-byte EDID (manufacturer PNK, product
  punktfunk — no SudoVDA bytes), a real swap-chain drain (faithful vdd port,
  required so DWM keeps compositing), the SudoVDA-compatible IOCTL control plane
  (ADD/REMOVE/PING/GET_WATCHDOG/GET_VERSION/SET_RENDER_ADAPTER) + a watchdog that
  tears down orphaned monitors when the host stops pinging.
- deploy-dev.ps1: stage + sign + stampinf (date.time DriverVer) + Inf2Cat +
  install, codifying the "bump DriverVer or pnputil keeps the old binary" gotcha.
- docs/windows-virtual-display-rust-port.md: investigation, the on-glass
  validation, and the two traps that cost time (Session-0 measurement +
  accumulated device-state needing a reboot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 21:54:50 +02:00
parent 095540efc2
commit d39da4bc06
35 changed files with 7148 additions and 0 deletions
@@ -0,0 +1,11 @@
[build]
target = "x86_64-pc-windows-msvc"
rustflags = [
"-Zpre-link-arg=/NOLOGO",
"-Zpre-link-arg=/MANIFEST:NO",
"-Zpre-link-arg=/SUBSYSTEM:WINDOWS",
"-Zpre-link-arg=/DYNAMICBASE",
"-Zpre-link-arg=/NXCOMPAT",
"-Clink-arg=/OPT:REF,ICF",
]
@@ -0,0 +1,29 @@
[package]
name = "pf-vdisplay"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wdf-umdf-sys = { path = "../wdf-umdf-sys" }
wdf-umdf = { path = "../wdf-umdf" }
bytemuck = { version = "1.19", features = ["derive"] }
thiserror = "2.0"
anyhow = "1.0"
log = "0.4"
[dependencies.windows]
version = "0.58.0"
features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_Diagnostics_Debug",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
]
@@ -0,0 +1,5 @@
fn main() {
// UMDF includes need the static C runtime linked.
println!("cargo::rustc-link-lib=static=ucrt");
println!("cargo::rerun-if-changed=build.rs");
}
@@ -0,0 +1,77 @@
;/*++
; pf-vdisplay - punktfunk virtual display, UMDF2 IddCx driver INF (template; stampinf -> .inf).
; Adapted from MolotovCherry/virtual-display-rs (MIT) + SudoVDA's control-device security DACL.
;--*/
[Version]
PnpLockdown=1
Signature="$Windows NT$"
ClassGUID={4D36E968-E325-11CE-BFC1-08002BE10318}
Class=Display
ClassVer=2.0
Provider=%ManufacturerName%
CatalogFile=pf_vdisplay.cat
DriverVer=
[Manufacturer]
%ManufacturerName%=Standard,NT$ARCH$
[Standard.NT$ARCH$]
%DeviceName%=pf_vdisplay_Install, Root\pf_vdisplay
[SourceDisksFiles]
pf_vdisplay.dll=1
[SourceDisksNames]
1=%DiskName%
; =================== UMDF IddCx device ====================
[pf_vdisplay_Install.NT]
CopyFiles=UMDriverCopy
[pf_vdisplay_Install.NT.hw]
AddReg=pf_vdisplay_HardwareDeviceSettings
[pf_vdisplay_HardwareDeviceSettings]
HKR, , "UpperFilters", %REG_MULTI_SZ%, "IndirectKmd"
HKR, "WUDF", "DeviceGroupId", %REG_SZ%, "pfVDisplayGroup"
; Let the host (LocalSystem service) + admins open the control device for the ADD/REMOVE/PING IOCTLs.
HKR, , "Security", , "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;WD)"
[pf_vdisplay_Install.NT.Services]
AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall
[pf_vdisplay_Install.NT.Wdf]
UmdfService=pf_vdisplay, pf_vdisplay_Install
UmdfServiceOrder=pf_vdisplay
UmdfKernelModeClientPolicy=AllowKernelModeClients
UmdfHostProcessSharing=ProcessSharingDisabled
[pf_vdisplay_Install]
UmdfLibraryVersion=$UMDFVERSION$
ServiceBinary=%12%\UMDF\pf_vdisplay.dll
UmdfExtensions=IddCx0102
[WUDFRD_ServiceInstall]
DisplayName=%WudfRdDisplayName%
ServiceType=1
StartType=3
ErrorControl=1
ServiceBinary=%12%\WUDFRd.sys
[DestinationDirs]
UMDriverCopy=12,UMDF
[UMDriverCopy]
pf_vdisplay.dll
[Strings]
ManufacturerName="punktfunk"
DiskName="punktfunk Virtual Display Installation Disk"
WudfRdDisplayName="Windows Driver Foundation - User-mode Driver Framework Reflector"
DeviceName="punktfunk Virtual Display"
REG_MULTI_SZ=0x00010000
REG_SZ=0x00000000
REG_EXPAND_SZ=0x00020000
REG_DWORD=0x00010001
@@ -0,0 +1,322 @@
use std::{
mem::{self, MaybeUninit},
ptr::NonNull,
};
use log::{error, info};
use wdf_umdf_sys::{
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1,
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1, __BindgenBitfieldUnit,
DISPLAYCONFIG_2DREGION, DISPLAYCONFIG_RATIONAL, DISPLAYCONFIG_SCANLINE_ORDERING,
DISPLAYCONFIG_TARGET_MODE, DISPLAYCONFIG_VIDEO_SIGNAL_INFO, IDARG_IN_ADAPTER_INIT_FINISHED,
IDARG_IN_COMMITMODES, IDARG_IN_GETDEFAULTDESCRIPTIONMODES, IDARG_IN_PARSEMONITORDESCRIPTION,
IDARG_IN_QUERYTARGETMODES, IDARG_IN_SETSWAPCHAIN, IDARG_OUT_GETDEFAULTDESCRIPTIONMODES,
IDARG_OUT_PARSEMONITORDESCRIPTION, IDARG_OUT_QUERYTARGETMODES, IDDCX_ADAPTER__,
IDDCX_MONITOR_MODE, IDDCX_MONITOR_MODE_ORIGIN, IDDCX_MONITOR__, IDDCX_TARGET_MODE, NTSTATUS,
WDFDEVICE, WDF_POWER_DEVICE_STATE,
};
use crate::{
context::{DeviceContext, MonitorContext},
edid::Edid,
monitor::{AdapterObject, FlattenModes, ADAPTER, MONITOR_MODES},
};
pub extern "C-unwind" fn adapter_init_finished(
adapter_object: *mut IDDCX_ADAPTER__,
_p_in_args: *const IDARG_IN_ADAPTER_INIT_FINISHED,
) -> NTSTATUS {
let Some(adapter_ptr) = NonNull::new(adapter_object) else {
error!("Adapter ptr was null");
return NTSTATUS::STATUS_INVALID_ADDRESS;
};
// store adapter object for the control plane to use
if ADAPTER.set(AdapterObject(adapter_ptr)).is_err() {
error!("Failed to set adapter");
return NTSTATUS::STATUS_ADAPTER_HARDWARE_ERROR;
}
DeviceContext::finish_init();
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn device_d0_entry(
device: WDFDEVICE,
_previous_state: WDF_POWER_DEVICE_STATE,
) -> NTSTATUS {
let status: NTSTATUS = unsafe {
DeviceContext::get_mut(device.cast(), |context| {
if let Err(e) = context.init_adapter() {
error!("Failed to init adapter: {e:?}");
}
})
.into()
};
if !status.is_success() {
return status;
}
NTSTATUS::STATUS_SUCCESS
}
fn display_info(width: u32, height: u32, refresh_rate: u32) -> DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
let clock_rate = refresh_rate * (height + 4) * (height + 4) + 1000;
DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
pixelRate: u64::from(clock_rate),
hSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: clock_rate,
Denominator: height + 4,
},
vSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: clock_rate,
Denominator: (height + 4) * (height + 4),
},
activeSize: DISPLAYCONFIG_2DREGION {
cx: width,
cy: height,
},
totalSize: DISPLAYCONFIG_2DREGION {
cx: width + 4,
cy: height + 4,
},
__bindgen_anon_1: DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1 {
AdditionalSignalInfo: unsafe {
mem::transmute::<
__BindgenBitfieldUnit<[u8; 4]>,
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1,
>(
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1::new_bitfield_1(
255, 0, 0,
),
)
},
},
scanLineOrdering:
DISPLAYCONFIG_SCANLINE_ORDERING::DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
}
}
pub extern "C-unwind" fn parse_monitor_description(
p_in_args: *const IDARG_IN_PARSEMONITORDESCRIPTION,
p_out_args: *mut IDARG_OUT_PARSEMONITORDESCRIPTION,
) -> NTSTATUS {
let in_args = unsafe { &*p_in_args };
let out_args = unsafe { &mut *p_out_args };
let Ok(monitors) = MONITOR_MODES.lock() else {
error!("MONITOR_MODES mutex poisoned");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let edid = unsafe {
std::slice::from_raw_parts(
in_args.MonitorDescription.pData as *const u8,
in_args.MonitorDescription.DataSize as usize,
)
};
let monitor_index = Edid::get_serial(edid);
let Ok(monitor_index) = monitor_index else {
error!(
"We got an edid {} bytes long, but this is incorrect",
edid.len()
);
return NTSTATUS::STATUS_INVALID_VIEW_SIZE;
};
let Some(monitor) = monitors.iter().find(|&m| m.data.id == monitor_index) else {
error!("Failed to find monitor id {monitor_index}");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let number_of_modes: u32 = monitor
.data
.modes
.iter()
.map(|m| u32::try_from(m.refresh_rates.len()).expect("Cannot use > u32::MAX refresh rates"))
.sum();
out_args.MonitorModeBufferOutputCount = number_of_modes;
if in_args.MonitorModeBufferInputCount < number_of_modes {
// Return success if there was no buffer, since the caller was only asking for a count of modes
return if in_args.MonitorModeBufferInputCount > 0 {
NTSTATUS::STATUS_BUFFER_TOO_SMALL
} else {
NTSTATUS::STATUS_SUCCESS
};
}
let monitor_modes = unsafe {
std::slice::from_raw_parts_mut(
in_args
.pMonitorModes
.cast::<MaybeUninit<IDDCX_MONITOR_MODE>>(),
number_of_modes as usize,
)
};
for (mode, out_mode) in monitor.data.modes.flatten().zip(monitor_modes.iter_mut()) {
out_mode.write(IDDCX_MONITOR_MODE {
#[allow(clippy::cast_possible_truncation)]
Size: mem::size_of::<IDDCX_MONITOR_MODE>() as u32,
Origin: IDDCX_MONITOR_MODE_ORIGIN::IDDCX_MONITOR_MODE_ORIGIN_MONITORDESCRIPTOR,
MonitorVideoSignalInfo: display_info(mode.width, mode.height, mode.refresh_rate),
});
}
// Set the preferred mode as represented in the EDID
out_args.PreferredMonitorModeIdx = 0;
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn monitor_get_default_modes(
_monitor_object: *mut IDDCX_MONITOR__,
_p_in_args: *const IDARG_IN_GETDEFAULTDESCRIPTIONMODES,
_p_out_args: *mut IDARG_OUT_GETDEFAULTDESCRIPTIONMODES,
) -> NTSTATUS {
NTSTATUS::STATUS_NOT_IMPLEMENTED
}
pub fn target_mode(width: u32, height: u32, refresh_rate: u32) -> IDDCX_TARGET_MODE {
let total_size = DISPLAYCONFIG_2DREGION {
cx: width,
cy: height,
};
IDDCX_TARGET_MODE {
#[allow(clippy::cast_possible_truncation)]
Size: mem::size_of::<IDDCX_TARGET_MODE>() as u32,
TargetVideoSignalInfo: DISPLAYCONFIG_TARGET_MODE {
targetVideoSignalInfo: DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
pixelRate: u64::from(refresh_rate) * u64::from(width) * u64::from(height),
hSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: refresh_rate * height,
Denominator: 1,
},
vSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: refresh_rate,
Denominator: 1,
},
totalSize: total_size,
activeSize: total_size,
scanLineOrdering:
DISPLAYCONFIG_SCANLINE_ORDERING::DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
__bindgen_anon_1: DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1 {
AdditionalSignalInfo: unsafe {
mem::transmute::<__BindgenBitfieldUnit<[u8; 4]>, DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1>(
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1::new_bitfield_1(
255, 1, 0,
),
)
},
},
},
},
..Default::default()
}
}
pub extern "C-unwind" fn monitor_query_modes(
monitor_object: *mut IDDCX_MONITOR__,
p_in_args: *const IDARG_IN_QUERYTARGETMODES,
p_out_args: *mut IDARG_OUT_QUERYTARGETMODES,
) -> NTSTATUS {
// find out which monitor this belongs too
let Ok(monitors) = MONITOR_MODES.lock() else {
error!("MONITOR_MODES mutex poisoned");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
// we have stored the monitor object per id, so we should be able to compare pointers
let Some(monitor) = monitors
.iter()
.find(|&m| m.object.is_some_and(|p| p.as_ptr() == monitor_object))
else {
error!("Failed to find monitor object in cache for {monitor_object:?}");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let number_of_modes = monitor
.data
.modes
.iter()
.map(|m| u32::try_from(m.refresh_rates.len()).expect("Cannot use > u32::MAX modes"))
.sum();
// Create a set of modes supported for frame processing and scan-out. These are typically not based on the
// monitor's descriptor and instead are based on the static processing capability of the device. The OS will
// report the available set of modes for a given output as the intersection of monitor modes with target modes.
let out_args = unsafe { &mut *p_out_args };
out_args.TargetModeBufferOutputCount = number_of_modes;
let in_args = unsafe { &*p_in_args };
if in_args.TargetModeBufferInputCount >= number_of_modes {
let out_target_modes = unsafe {
std::slice::from_raw_parts_mut(
in_args
.pTargetModes
.cast::<MaybeUninit<IDDCX_TARGET_MODE>>(),
number_of_modes as usize,
)
};
for (mode, out_target) in monitor
.data
.modes
.flatten()
.zip(out_target_modes.iter_mut())
{
let target_mode = target_mode(mode.width, mode.height, mode.refresh_rate);
out_target.write(target_mode);
}
}
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn adapter_commit_modes(
_adapter_object: *mut IDDCX_ADAPTER__,
_p_in_args: *const IDARG_IN_COMMITMODES,
) -> NTSTATUS {
// The swap-chain is managed by IddCx; there is nothing device-specific to reconfigure on a commit.
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn assign_swap_chain(
monitor_object: *mut IDDCX_MONITOR__,
p_in_args: *const IDARG_IN_SETSWAPCHAIN,
) -> NTSTATUS {
let p_in_args = unsafe { &*p_in_args };
unsafe {
MonitorContext::get_mut(monitor_object.cast(), |context| {
context.assign_swap_chain(
p_in_args.hSwapChain,
p_in_args.RenderAdapterLuid,
p_in_args.hNextSurfaceAvailable,
);
})
.into()
}
}
pub extern "C-unwind" fn unassign_swap_chain(monitor_object: *mut IDDCX_MONITOR__) -> NTSTATUS {
info!("swap-chain unassigned (monitor inactive)");
unsafe {
MonitorContext::get_mut(monitor_object.cast(), |context| {
context.unassign_swap_chain();
})
.into()
}
}
@@ -0,0 +1,328 @@
use std::{
mem::{self, size_of},
num::{ParseIntError, TryFromIntError},
ptr::{addr_of_mut, NonNull},
};
use anyhow::anyhow;
use log::{error, info, warn};
use wdf_umdf::{
IddCxAdapterInitAsync, IddCxError, IddCxMonitorArrival, IddCxMonitorCreate,
IddCxMonitorSetupHardwareCursor, WdfError, WdfObjectDelete, WDF_DECLARE_CONTEXT_TYPE,
};
use wdf_umdf_sys::{
DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY, HANDLE, IDARG_IN_ADAPTER_INIT, IDARG_IN_MONITORCREATE,
IDARG_IN_SETUP_HWCURSOR, IDARG_OUT_ADAPTER_INIT, IDARG_OUT_MONITORARRIVAL,
IDARG_OUT_MONITORCREATE, IDDCX_ADAPTER, IDDCX_ADAPTER_CAPS, IDDCX_CURSOR_CAPS,
IDDCX_ENDPOINT_DIAGNOSTIC_INFO, IDDCX_ENDPOINT_VERSION, IDDCX_FEATURE_IMPLEMENTATION,
IDDCX_MONITOR, IDDCX_MONITOR_DESCRIPTION, IDDCX_MONITOR_DESCRIPTION_TYPE, IDDCX_MONITOR_INFO,
IDDCX_SWAPCHAIN, IDDCX_TRANSMISSION_TYPE, IDDCX_XOR_CURSOR_SUPPORT, LUID, NTSTATUS, WDFDEVICE,
WDFOBJECT, WDF_OBJECT_ATTRIBUTES,
};
use windows::{
core::{s, w, GUID},
Win32::{Foundation::TRUE, System::Threading::CreateEventA},
};
use crate::{
direct_3d_device::Direct3DDevice,
edid::Edid,
monitor::MONITOR_MODES,
swap_chain_processor::SwapChainProcessor,
};
// Maximum amount of monitors that can be connected
pub const MAX_MONITORS: u8 = 16;
pub struct DeviceContext {
device: WDFDEVICE,
adapter: Option<IDDCX_ADAPTER>,
}
// SAFETY: Raw ptr is managed by external library
unsafe impl Send for DeviceContext {}
unsafe impl Sync for DeviceContext {}
// for now, `device` is hardcoded into the macro, so it needs to be there even if unused
#[allow(unused)]
pub struct MonitorContext {
device: IDDCX_MONITOR,
swap_chain_processor: Option<SwapChainProcessor>,
}
// SAFETY: Raw ptr is managed by external library
unsafe impl Send for MonitorContext {}
unsafe impl Sync for MonitorContext {}
WDF_DECLARE_CONTEXT_TYPE!(pub DeviceContext);
WDF_DECLARE_CONTEXT_TYPE!(pub MonitorContext);
#[derive(Debug, thiserror::Error)]
pub enum ContextError {
#[error("Failed to parse integer: {0:?}")]
ParseInt(#[from] ParseIntError),
#[error("Failed to convert integer: {0:?}")]
TryFromInt(#[from] TryFromIntError),
#[error("Failed to convert to NTSTATUS: {0:?}")]
Ntstatus(#[from] NTSTATUS),
#[error("Failed to convert to IddCxError: {0:?}")]
IddCx(#[from] IddCxError),
#[error("Failed to convert to WdfError: {0:?}")]
Wdf(#[from] WdfError),
#[error("Windows Error: {0:?}")]
Win(#[from] windows::core::Error),
#[error("{0:?}")]
Other(#[from] anyhow::Error),
}
impl DeviceContext {
pub fn new(device: WDFDEVICE) -> Self {
Self {
device,
adapter: None,
}
}
pub fn init_adapter(&mut self) -> Result<(), ContextError> {
let mut version = IDDCX_ENDPOINT_VERSION {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_ENDPOINT_VERSION>() as u32,
MajorVer: env!("CARGO_PKG_VERSION_MAJOR").parse::<u32>()?,
MinorVer: env!("CARGO_PKG_VERSION_MINOR").parse::<u32>()?,
Build: env!("CARGO_PKG_VERSION_PATCH").parse::<u32>()?,
..Default::default()
};
let mut adapter_caps = IDDCX_ADAPTER_CAPS {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_ADAPTER_CAPS>() as u32,
MaxMonitorsSupported: u32::from(MAX_MONITORS),
EndPointDiagnostics: IDDCX_ENDPOINT_DIAGNOSTIC_INFO {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_ENDPOINT_DIAGNOSTIC_INFO>() as u32,
GammaSupport: IDDCX_FEATURE_IMPLEMENTATION::IDDCX_FEATURE_IMPLEMENTATION_NONE,
TransmissionType: IDDCX_TRANSMISSION_TYPE::IDDCX_TRANSMISSION_TYPE_WIRED_OTHER,
pEndPointFriendlyName: w!("punktfunk Virtual Display Adapter").as_ptr(),
pEndPointManufacturerName: w!("punktfunk").as_ptr(),
pEndPointModelName: w!("Virtual Display").as_ptr(),
pFirmwareVersion: addr_of_mut!(version).cast(),
pHardwareVersion: addr_of_mut!(version).cast(),
},
..Default::default()
};
let mut attr = WDF_OBJECT_ATTRIBUTES::init_context_type(unsafe { Self::get_type_info() });
let adapter_init = IDARG_IN_ADAPTER_INIT {
// this is WdfDevice because that's what we set last
WdfDevice: self.device,
pCaps: addr_of_mut!(adapter_caps).cast(),
ObjectAttributes: addr_of_mut!(attr).cast(),
};
let mut adapter_init_out = IDARG_OUT_ADAPTER_INIT::default();
unsafe { IddCxAdapterInitAsync(&adapter_init, &mut adapter_init_out)? };
self.adapter = Some(adapter_init_out.AdapterObject);
unsafe { self.clone_into(adapter_init_out.AdapterObject as WDFOBJECT)? };
Ok(())
}
pub fn finish_init() -> NTSTATUS {
// Monitors are created on demand by the IOCTL control plane (control::do_add). Start the
// watchdog so a crashed/gone host never leaves a phantom display.
crate::control::start_watchdog();
NTSTATUS::STATUS_SUCCESS
}
pub fn create_monitor(&mut self, index: u32) -> Result<(), ContextError> {
let mut attr =
WDF_OBJECT_ATTRIBUTES::init_context_type(unsafe { MonitorContext::get_type_info() });
// use the edid serial number to represent the monitor index for later identification
let mut edid = Edid::generate_with(index);
let mut monitor_info = IDDCX_MONITOR_INFO {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_MONITOR_INFO>() as u32,
// SAFETY: windows-rs + generated _GUID types are same size, with same fields, and repr C
// see: https://microsoft.github.io/windows-docs-rs/doc/windows/core/struct.GUID.html
// and: wmdf_umdf_sys::_GUID
MonitorContainerId: unsafe {
mem::transmute::<GUID, wdf_umdf_sys::_GUID>(GUID::new()?)
},
MonitorType:
DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY::DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI,
ConnectorIndex: index,
MonitorDescription: IDDCX_MONITOR_DESCRIPTION {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_MONITOR_DESCRIPTION>() as u32,
Type: IDDCX_MONITOR_DESCRIPTION_TYPE::IDDCX_MONITOR_DESCRIPTION_TYPE_EDID,
#[allow(clippy::cast_possible_truncation)]
DataSize: edid.len() as u32,
pData: edid.as_mut_ptr().cast(),
},
};
let monitor_create = IDARG_IN_MONITORCREATE {
ObjectAttributes: &mut attr,
pMonitorInfo: &mut monitor_info,
};
let mut monitor_create_out = IDARG_OUT_MONITORCREATE::default();
unsafe {
IddCxMonitorCreate(
self.adapter.ok_or(anyhow!("Failed to get adapter"))?,
&monitor_create,
&mut monitor_create_out,
)?
};
// store monitor object for later
{
let mut lock = MONITOR_MODES
.lock()
.map_err(|_| anyhow!("Failed to lock mutex"))?;
for monitor in &mut *lock {
if monitor.data.id == index {
monitor.object = Some(
NonNull::new(monitor_create_out.MonitorObject)
.ok_or(anyhow!("MonitorObject was null"))?,
);
}
}
}
unsafe {
let context = MonitorContext::new(monitor_create_out.MonitorObject);
context.init(monitor_create_out.MonitorObject as WDFOBJECT)?;
}
// tell os monitor is plugged in
let mut arrival_out = IDARG_OUT_MONITORARRIVAL::default();
unsafe {
IddCxMonitorArrival(monitor_create_out.MonitorObject, &mut arrival_out)?;
}
// Record the OS target id + render-adapter LUID for the ADD IOCTL reply.
{
let mut lock = MONITOR_MODES
.lock()
.map_err(|_| anyhow!("Failed to lock mutex"))?;
if let Some(mon) = lock.iter_mut().find(|m| m.data.id == index) {
mon.target_id = arrival_out.OsTargetId;
mon.adapter_luid_low = arrival_out.OsAdapterLuid.LowPart;
mon.adapter_luid_high = arrival_out.OsAdapterLuid.HighPart;
}
}
Ok(())
}
}
impl MonitorContext {
pub fn new(device: IDDCX_MONITOR) -> Self {
Self {
device,
swap_chain_processor: None,
}
}
pub fn assign_swap_chain(
&mut self,
swap_chain: IDDCX_SWAPCHAIN,
render_adapter: LUID,
new_frame_event: HANDLE,
) {
// drop processing thread
drop(self.swap_chain_processor.take());
// transmute would work, but one less unsafe block, so why not
let luid = windows::Win32::Foundation::LUID {
LowPart: render_adapter.LowPart,
HighPart: render_adapter.HighPart,
};
// Log which GPU the OS picked to render this virtual monitor (useful on a hybrid iGPU+dGPU box,
// where the render adapter determines which adapter the host's capture must enumerate).
info!(
"swap-chain assigned: OS render adapter LUID = {:08x}:{:08x}",
render_adapter.HighPart, render_adapter.LowPart
);
let device = Direct3DDevice::init(luid);
if let Ok(device) = device {
let mut processor = SwapChainProcessor::new();
processor.run(swap_chain, device, new_frame_event);
self.swap_chain_processor = Some(processor);
self.setup_hw_cursor();
} else {
// It's important to delete the swap-chain if D3D initialization fails, so that the OS knows to generate a new
// swap-chain and try again.
error!("Direct3DDevice::init FAILED on render LUID: {device:?} — deleting swap chain for OS retry");
unsafe {
let _ = WdfObjectDelete(swap_chain.cast());
}
}
}
pub fn unassign_swap_chain(&mut self) {
self.swap_chain_processor.take();
}
pub fn setup_hw_cursor(&mut self) {
let mouse_event = unsafe { CreateEventA(None, false, false, s!("vdd_mouse_event")) };
let Ok(mouse_event) = mouse_event else {
error!("CreateEventA failed: {mouse_event:?}");
return;
};
// setup hardware cursor
let cursor_info = IDDCX_CURSOR_CAPS {
#[allow(clippy::cast_possible_truncation)]
Size: std::mem::size_of::<IDDCX_CURSOR_CAPS>() as u32,
AlphaCursorSupport: TRUE.0,
MaxX: 512,
MaxY: 512,
ColorXorCursorSupport: IDDCX_XOR_CURSOR_SUPPORT::IDDCX_XOR_CURSOR_SUPPORT_NONE,
};
let hw_cursor = IDARG_IN_SETUP_HWCURSOR {
CursorInfo: cursor_info,
hNewCursorDataAvailable: mouse_event.0,
};
let res = unsafe { IddCxMonitorSetupHardwareCursor(self.device, &hw_cursor) };
let Ok(res) = res else {
error!("IddCxMonitorSetupHardwareCursor() failed: {res:?}");
return;
};
if res.is_warning() {
warn!("IddCxMonitorSetupHardwareCursor() warn: {res:?}");
}
if res.is_error() {
error!("IddCxMonitorSetupHardwareCursor() failed: {res:?}");
}
}
}
@@ -0,0 +1,339 @@
//! SudoVDA-compatible IOCTL control plane (`EVT_IDD_CX_DEVICE_IO_CONTROL`). The host's
//! `vdisplay/sudovda.rs` drives this unchanged: ADD a monitor at a requested mode → `{LUID, target_id}`,
//! REMOVE by GUID, PING the watchdog, GET_VERSION/GET_WATCHDOG, SET_RENDER_ADAPTER. Struct layouts are
//! byte-identical to `Common/Include/sudovda-ioctl.h`.
use std::ffi::c_void;
use std::mem::size_of;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use log::{error, info};
use wdf_umdf::{
IddCxAdapterSetRenderAdapter, IddCxMonitorDeparture, WdfRequestCompleteWithInformation,
WdfRequestRetrieveInputBuffer, WdfRequestRetrieveOutputBuffer,
};
use wdf_umdf_sys::{IDARG_IN_ADAPTERSETRENDERADAPTER, LUID, NTSTATUS, WDFDEVICE, WDFREQUEST};
use crate::context::DeviceContext;
use crate::monitor::{
default_modes, Mode, MonitorData, MonitorObject, ADAPTER, MONITOR_MODES, NEXT_ID,
PREFERRED_RENDER_ADAPTER, PROTOCOL_VERSION, WATCHDOG_COUNTDOWN, WATCHDOG_TIMEOUT,
};
// CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, func, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0).
const fn ctl(func: u32) -> u32 {
(0x22u32 << 16) | (func << 2)
}
const IOCTL_ADD: u32 = ctl(0x800);
const IOCTL_REMOVE: u32 = ctl(0x801);
const IOCTL_SET_RENDER_ADAPTER: u32 = ctl(0x802);
const IOCTL_GET_WATCHDOG: u32 = ctl(0x803);
const IOCTL_PING: u32 = ctl(0x888);
const IOCTL_GET_VERSION: u32 = ctl(0x8FF);
#[repr(C)]
struct AddParams {
width: u32,
height: u32,
refresh: u32,
guid: [u8; 16],
device_name: [u8; 14],
serial: [u8; 14],
}
#[repr(C)]
struct AddOut {
luid_low: u32,
luid_high: i32,
target_id: u32,
}
#[repr(C)]
struct RemoveParams {
guid: [u8; 16],
}
#[repr(C)]
struct SetRenderAdapterParams {
luid_low: u32,
luid_high: i32,
}
#[repr(C)]
struct WatchdogOut {
timeout: u32,
countdown: u32,
}
fn guid_key(b: &[u8; 16]) -> u128 {
u128::from_le_bytes(*b)
}
/// SAFETY: `request` valid; returns a pointer to the request's input buffer of at least `min` bytes.
unsafe fn input_buf(request: WDFREQUEST, min: usize) -> Option<*const u8> {
let mut p: *mut c_void = std::ptr::null_mut();
let mut len: usize = 0;
let r = unsafe { WdfRequestRetrieveInputBuffer(request, min, &mut p, &mut len) };
if r.is_err() || p.is_null() || len < min {
return None;
}
Some(p.cast::<u8>())
}
/// SAFETY: `request` valid; returns a pointer to the request's output buffer of at least `min` bytes.
unsafe fn output_buf(request: WDFREQUEST, min: usize) -> Option<*mut u8> {
let mut p: *mut c_void = std::ptr::null_mut();
let mut len: usize = 0;
let r = unsafe { WdfRequestRetrieveOutputBuffer(request, min, &mut p, &mut len) };
if r.is_err() || p.is_null() || len < min {
return None;
}
Some(p.cast::<u8>())
}
/// `EVT_IDD_CX_DEVICE_IO_CONTROL` — IddCx redirects device IOCTLs here. Signature matches SudoVDA's
/// `SudoVDAIoDeviceControl(Device, Request, OutputBufferLength, InputBufferLength, IoControlCode)`.
pub extern "C-unwind" fn device_io_control(
device: WDFDEVICE,
request: WDFREQUEST,
output_len: usize,
input_len: usize,
ioctl_code: u32,
) {
// Reset the watchdog on any IOCTL except the watchdog query (the host PINGs to keep alive).
if ioctl_code != IOCTL_GET_WATCHDOG {
WATCHDOG_COUNTDOWN.store(WATCHDOG_TIMEOUT.load(Ordering::Relaxed), Ordering::Relaxed);
}
let mut bytes: usize = 0;
// SAFETY: dispatch reads/writes the request buffers it validated; `device` is the IddCx device.
let status = unsafe {
match ioctl_code {
IOCTL_ADD => do_add(device, request, input_len, output_len, &mut bytes),
IOCTL_REMOVE => do_remove(request, input_len),
IOCTL_SET_RENDER_ADAPTER => do_set_render_adapter(request, input_len),
IOCTL_GET_WATCHDOG => do_get_watchdog(request, output_len, &mut bytes),
IOCTL_PING => NTSTATUS::STATUS_SUCCESS,
IOCTL_GET_VERSION => do_get_version(request, output_len, &mut bytes),
_ => NTSTATUS::STATUS_INVALID_DEVICE_REQUEST,
}
};
// SAFETY: completing the request we were handed.
let _ = unsafe { WdfRequestCompleteWithInformation(request, status, bytes as u64) };
}
unsafe fn do_add(
device: WDFDEVICE,
request: WDFREQUEST,
input_len: usize,
output_len: usize,
bytes: &mut usize,
) -> NTSTATUS {
if input_len < size_of::<AddParams>() || output_len < size_of::<AddOut>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let (Some(pin), Some(pout)) = (
unsafe { input_buf(request, size_of::<AddParams>()) },
unsafe { output_buf(request, size_of::<AddOut>()) },
) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let params = unsafe { &*pin.cast::<AddParams>() };
let guid = guid_key(&params.guid);
// Dedup: an existing GUID returns its LUID + target id (the host may re-ADD on reconnect).
{
let lock = MONITOR_MODES.lock().unwrap();
if let Some(mon) = lock.iter().find(|m| m.guid == guid) {
let out = AddOut {
luid_low: mon.adapter_luid_low,
luid_high: mon.adapter_luid_high,
target_id: mon.target_id,
};
unsafe { pout.cast::<AddOut>().write_unaligned(out) };
*bytes = size_of::<AddOut>();
return NTSTATUS::STATUS_SUCCESS;
}
}
if params.width == 0 || params.height == 0 || params.refresh == 0 {
return NTSTATUS::STATUS_INVALID_PARAMETER;
}
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
// Requested mode first (preferred), then fallbacks.
let mut modes = vec![Mode {
width: params.width,
height: params.height,
refresh_rates: vec![params.refresh],
}];
modes.extend(default_modes());
MONITOR_MODES.lock().unwrap().push(MonitorObject {
object: None,
data: MonitorData { id, modes },
guid,
target_id: 0,
adapter_luid_low: 0,
adapter_luid_high: 0,
});
// Create the IddCx monitor via the device context (captures target id + LUID into the entry).
let created = unsafe {
DeviceContext::get_mut(device.cast(), |ctx| {
if let Err(e) = ctx.create_monitor(id) {
error!("ADD: create_monitor failed: {e:?}");
}
})
};
let lock = MONITOR_MODES.lock().unwrap();
let mon = lock.iter().find(|m| m.data.id == id);
if created.is_err() || mon.map_or(true, |m| m.object.is_none()) {
drop(lock);
MONITOR_MODES.lock().unwrap().retain(|m| m.data.id != id);
error!("ADD: monitor {id} failed to arrive");
return NTSTATUS::STATUS_UNSUCCESSFUL;
}
let mon = mon.unwrap();
let out = AddOut {
luid_low: mon.adapter_luid_low,
luid_high: mon.adapter_luid_high,
target_id: mon.target_id,
};
unsafe { pout.cast::<AddOut>().write_unaligned(out) };
*bytes = size_of::<AddOut>();
info!(
"ADD {}x{}@{} -> target_id={} luid={:08x}:{:08x}",
params.width, params.height, params.refresh, mon.target_id, mon.adapter_luid_high, mon.adapter_luid_low
);
NTSTATUS::STATUS_SUCCESS
}
unsafe fn do_remove(request: WDFREQUEST, input_len: usize) -> NTSTATUS {
if input_len < size_of::<RemoveParams>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pin) = (unsafe { input_buf(request, size_of::<RemoveParams>()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let params = unsafe { &*pin.cast::<RemoveParams>() };
let guid = guid_key(&params.guid);
let mut lock = MONITOR_MODES.lock().unwrap();
if let Some(pos) = lock.iter().position(|m| m.guid == guid) {
let mon = lock.remove(pos);
if let Some(obj) = mon.object {
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("REMOVE: departure failed: {e:?}");
}
}
info!("REMOVE target_id={}", mon.target_id);
NTSTATUS::STATUS_SUCCESS
} else {
NTSTATUS::STATUS_NOT_FOUND
}
}
unsafe fn do_set_render_adapter(request: WDFREQUEST, input_len: usize) -> NTSTATUS {
if input_len < size_of::<SetRenderAdapterParams>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pin) = (unsafe { input_buf(request, size_of::<SetRenderAdapterParams>()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let params = unsafe { &*pin.cast::<SetRenderAdapterParams>() };
PREFERRED_RENDER_ADAPTER.store(
((params.luid_high as u32 as u64) << 32) | u64::from(params.luid_low),
Ordering::Relaxed,
);
if let Some(adapter) = ADAPTER.get() {
let in_args = IDARG_IN_ADAPTERSETRENDERADAPTER {
PreferredRenderAdapter: LUID {
LowPart: params.luid_low,
HighPart: params.luid_high,
},
};
if let Err(e) = unsafe { IddCxAdapterSetRenderAdapter(adapter.0.as_ptr(), &in_args) } {
error!("SET_RENDER_ADAPTER failed: {e:?}");
}
}
NTSTATUS::STATUS_SUCCESS
}
unsafe fn do_get_watchdog(request: WDFREQUEST, output_len: usize, bytes: &mut usize) -> NTSTATUS {
if output_len < size_of::<WatchdogOut>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pout) = (unsafe { output_buf(request, size_of::<WatchdogOut>()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let out = WatchdogOut {
timeout: WATCHDOG_TIMEOUT.load(Ordering::Relaxed),
countdown: WATCHDOG_COUNTDOWN.load(Ordering::Relaxed),
};
unsafe { pout.cast::<WatchdogOut>().write_unaligned(out) };
*bytes = size_of::<WatchdogOut>();
NTSTATUS::STATUS_SUCCESS
}
unsafe fn do_get_version(request: WDFREQUEST, output_len: usize, bytes: &mut usize) -> NTSTATUS {
if output_len < PROTOCOL_VERSION.len() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pout) = (unsafe { output_buf(request, PROTOCOL_VERSION.len()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
unsafe { std::ptr::copy_nonoverlapping(PROTOCOL_VERSION.as_ptr(), pout, PROTOCOL_VERSION.len()) };
*bytes = PROTOCOL_VERSION.len();
NTSTATUS::STATUS_SUCCESS
}
/// Tear down every monitor (watchdog expiry — the host is gone). Mirrors SudoVDA's DisconnectAllMonitors.
fn disconnect_all_monitors() {
let mut lock = MONITOR_MODES.lock().unwrap();
if lock.is_empty() {
return;
}
for mon in lock.drain(..) {
if let Some(obj) = mon.object {
// SAFETY: `obj` is a live IddCx monitor object.
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("watchdog: monitor departure failed: {e:?}");
}
}
}
}
/// Start the watchdog thread (once). The host reads the timeout via GET_WATCHDOG and PINGs every
/// timeout/3; if it stops, the countdown reaches 0 and every monitor is torn down — so a crashed/gone
/// host never leaves a phantom display. Mirrors SudoVDA's RunWatchdog.
pub fn start_watchdog() {
static STARTED: AtomicBool = AtomicBool::new(false);
if STARTED.swap(true, Ordering::Relaxed) {
return;
}
let timeout = WATCHDOG_TIMEOUT.load(Ordering::Relaxed);
if timeout == 0 {
return;
}
WATCHDOG_COUNTDOWN.store(timeout, Ordering::Relaxed);
thread::spawn(|| loop {
thread::sleep(Duration::from_secs(1));
// Nothing to guard while there are no monitors.
if MONITOR_MODES.lock().unwrap().is_empty() {
continue;
}
let prev = WATCHDOG_COUNTDOWN.load(Ordering::Relaxed);
if prev == 0 {
continue;
}
// Decrement without clobbering a concurrent IOCTL reset (CAS).
if WATCHDOG_COUNTDOWN
.compare_exchange(prev, prev - 1, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
&& prev - 1 == 0
{
error!("watchdog expired (host stopped pinging) — tearing down all monitors");
disconnect_all_monitors();
}
});
}
@@ -0,0 +1,77 @@
use windows::{
core::Error,
Win32::{
Foundation::LUID,
Graphics::{
Direct3D::D3D_DRIVER_TYPE_UNKNOWN,
Direct3D11::{
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext,
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
D3D11_CREATE_DEVICE_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY,
D3D11_CREATE_DEVICE_SINGLETHREADED, D3D11_SDK_VERSION,
},
Dxgi::{CreateDXGIFactory2, IDXGIAdapter1, IDXGIFactory5, DXGI_CREATE_FACTORY_FLAGS},
},
},
};
#[derive(thiserror::Error, Debug)]
pub enum Direct3DError {
#[error("Direct3DError({0:?})")]
Win32(#[from] Error),
#[error("Direct3DError(\"{0}\")")]
Other(&'static str),
}
impl From<&'static str> for Direct3DError {
fn from(value: &'static str) -> Self {
Direct3DError::Other(value)
}
}
#[derive(Debug)]
pub struct Direct3DDevice {
// The following are already refcounted, so they're safe to use directly without additional drop impls
_dxgi_factory: IDXGIFactory5,
_adapter: IDXGIAdapter1,
pub device: ID3D11Device,
_device_context: ID3D11DeviceContext,
}
impl Direct3DDevice {
pub fn init(adapter_luid: LUID) -> Result<Self, Direct3DError> {
let dxgi_factory =
unsafe { CreateDXGIFactory2::<IDXGIFactory5>(DXGI_CREATE_FACTORY_FLAGS(0))? };
let adapter = unsafe { dxgi_factory.EnumAdapterByLuid::<IDXGIAdapter1>(adapter_luid)? };
let mut device = None;
let mut device_context = None;
unsafe {
D3D11CreateDevice(
&adapter,
D3D_DRIVER_TYPE_UNKNOWN,
None,
D3D11_CREATE_DEVICE_BGRA_SUPPORT
| D3D11_CREATE_DEVICE_SINGLETHREADED
| D3D11_CREATE_DEVICE_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut device_context),
)?;
}
let device = device.ok_or("ID3D11Device not found")?;
let device_context = device_context.ok_or("ID3D11DeviceContext not found")?;
Ok(Self {
_dxgi_factory: dxgi_factory,
_adapter: adapter,
device,
_device_context: device_context,
})
}
}
@@ -0,0 +1,114 @@
use std::{array::TryFromSliceError, ops::Deref};
use bytemuck::{Pod, Zeroable};
// A clean, self-contained 128-byte EDID carrying punktfunk's own identity — manufacturer ID "PNK"
// (bytes 8-9) and product name "punktfunk" (the 0xFC display-descriptor). Derived from the
// virtual-display-rs base block (a standard, widely-deployed virtual EDID); it deliberately carries NO
// other driver's bytes or branding. The serial-number field (offset 0x0C) encodes the per-monitor
// index, so `parse_monitor_description` can map an EDID the OS hands back to its monitor;
// `generate_with` patches that serial and `gen_checksum` recomputes byte 127 before the EDID reaches
// IddCx. 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.
const _EDID: [u8; 128] = [
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x41, 0xCB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0x21, 0x01, 0x03, 0x80, 0x32, 0x1F, 0x78, 0x07, 0xEE, 0x95, 0xA3, 0x54, 0x4C, 0x99, 0x26,
0x0F, 0x50, 0x54, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3A, 0x80, 0x18, 0x71, 0x38, 0x2D, 0x40, 0x58, 0x2C,
0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xFD, 0x00, 0x17, 0xF0, 0x0F,
0xFF, 0x0F, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x70,
0x75, 0x6E, 0x6B, 0x74, 0x66, 0x75, 0x6E, 0x6B, 0x0A, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
const EDID_LEN: usize = _EDID.len();
static EDID: AlignedEdid<EDID_LEN> = AlignedEdid {
data: _EDID,
_align: [],
};
#[repr(C)]
struct AlignedEdid<const N: usize> {
data: [u8; N],
// required to make this type aligned to Edid
_align: [Edid; 0],
}
impl<const N: usize> AlignedEdid<N> {
fn new(data: &[u8]) -> Result<Self, TryFromSliceError> {
let data: [u8; N] = data.try_into()?;
Ok(Self { data, _align: [] })
}
}
impl<const N: usize> Deref for AlignedEdid<N> {
type Target = Edid;
fn deref(&self) -> &Self::Target {
let header = &self.data[..EDID_SIZE];
bytemuck::from_bytes(header)
}
}
const EDID_SIZE: usize = std::mem::size_of::<Edid>();
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
pub struct Edid {
header: [u8; 8],
manufacturer_id: [u8; 2],
product_code: u16,
serial_number: u32,
manufacture_week: u8,
manufacture_year: u8,
version: u8,
revision: u8,
}
impl Edid {
pub fn generate_with(serial: u32) -> Vec<u8> {
// change serial number in the header
let mut header = *EDID;
header.serial_number = serial;
header.generate()
}
pub fn get_serial(edid: &[u8]) -> Result<u32, TryFromSliceError> {
let edid = AlignedEdid::<EDID_LEN>::new(edid)?;
Ok(edid.serial_number)
}
fn generate(&self) -> Vec<u8> {
let header = bytemuck::bytes_of(self);
// slice of monitor edid minus header
let data = &EDID.data[EDID_SIZE..];
// splice together header and the rest of the EDID
let mut edid: Vec<u8> = header.iter().chain(data).copied().collect();
// regenerate checksum
Self::gen_checksum(&mut edid);
edid
}
fn gen_checksum(data: &mut [u8]) {
// important, this is the bare minimum length
assert!(data.len() >= 128);
// slice to the entire data minus the last checksum byte
let edid_data = &data[..=126];
// do checksum calculation
let sum: u32 = edid_data.iter().copied().map(u32::from).sum();
// this wont ever truncate
#[allow(clippy::cast_possible_truncation)]
let checksum = (256 - (sum % 256)) as u8;
// update last byte with new checksum
data[127] = checksum;
}
}
@@ -0,0 +1,119 @@
//! Driver entry + WDF device-add. Adapted from virtual-display-rs (its event-log/boot-retry logger
//! dance is replaced by the `OutputDebugString` logger in `logger.rs`).
use log::{error, info};
use wdf_umdf::{
IddCxDeviceInitConfig, IddCxDeviceInitialize, WdfDeviceCreate, WdfDeviceCreateDeviceInterface,
WdfDeviceInitSetPnpPowerEventCallbacks, WdfDriverCreate,
};
use wdf_umdf_sys::{
GUID, IDD_CX_CLIENT_CONFIG, NTSTATUS, WDFDEVICE_INIT, WDFDRIVER__, WDFOBJECT, WDF_DRIVER_CONFIG,
WDF_OBJECT_ATTRIBUTES, WDF_PNPPOWER_EVENT_CALLBACKS, _DRIVER_OBJECT, _UNICODE_STRING,
};
use crate::callbacks::{
adapter_commit_modes, adapter_init_finished, assign_swap_chain, device_d0_entry,
monitor_get_default_modes, monitor_query_modes, parse_monitor_description, unassign_swap_chain,
};
use crate::context::DeviceContext;
use crate::control::device_io_control;
// SudoVDA control-interface GUID — the host opens this to drive the ADD/REMOVE/PING IOCTLs.
// {e5bcc234-1e0c-418a-a0d4-ef8b7501414d}
const SUVDA_INTERFACE_GUID: GUID = GUID {
Data1: 0xe5bc_c234,
Data2: 0x1e0c,
Data3: 0x418a,
Data4: [0xa0, 0xd4, 0xef, 0x8b, 0x75, 0x01, 0x41, 0x4d],
};
/// Driver entry point (called by the framework via `FxDriverEntryUm`).
#[no_mangle]
extern "C-unwind" fn DriverEntry(
driver_object: *mut _DRIVER_OBJECT,
registry_path: *mut _UNICODE_STRING,
) -> NTSTATUS {
crate::logger::init();
crate::panic::set_hook();
info!("pf-vdisplay v{} starting", env!("CARGO_PKG_VERSION"));
let mut attributes = WDF_OBJECT_ATTRIBUTES::init();
let mut config = WDF_DRIVER_CONFIG::init(Some(driver_add));
unsafe {
WdfDriverCreate(
driver_object,
registry_path,
Some(&mut attributes),
&mut config,
None,
)
}
.into()
}
extern "C-unwind" fn driver_add(
_driver: *mut WDFDRIVER__,
mut init: *mut WDFDEVICE_INIT,
) -> NTSTATUS {
let mut callbacks = WDF_PNPPOWER_EVENT_CALLBACKS::init();
callbacks.EvtDeviceD0Entry = Some(device_d0_entry);
unsafe {
_ = WdfDeviceInitSetPnpPowerEventCallbacks(init, &mut callbacks);
}
let Some(mut config) = IDD_CX_CLIENT_CONFIG::init() else {
error!("Failed to create IDD_CX_CLIENT_CONFIG");
return NTSTATUS::STATUS_NOT_FOUND;
};
config.EvtIddCxAdapterInitFinished = Some(adapter_init_finished);
config.EvtIddCxParseMonitorDescription = Some(parse_monitor_description);
config.EvtIddCxMonitorGetDefaultDescriptionModes = Some(monitor_get_default_modes);
config.EvtIddCxMonitorQueryTargetModes = Some(monitor_query_modes);
config.EvtIddCxAdapterCommitModes = Some(adapter_commit_modes);
config.EvtIddCxMonitorAssignSwapChain = Some(assign_swap_chain);
config.EvtIddCxMonitorUnassignSwapChain = Some(unassign_swap_chain);
// IddCx redirects device IOCTLs to this callback — our SudoVDA-compatible control plane.
config.EvtIddCxDeviceIoControl = Some(device_io_control);
let init_data = unsafe { &mut *init };
let status = unsafe { IddCxDeviceInitConfig(init_data, &config) };
if let Err(e) = status {
error!("Failed to init iddcx config: {e:?}");
return e.into();
}
let mut attributes =
WDF_OBJECT_ATTRIBUTES::init_context_type(unsafe { DeviceContext::get_type_info() });
attributes.EvtCleanupCallback = Some(event_cleanup);
let mut device = std::ptr::null_mut();
let status = unsafe { WdfDeviceCreate(&mut init, Some(&mut attributes), &mut device) };
if let Err(e) = status {
error!("Failed to create device: {e:?}");
return e.into();
}
// Register the SudoVDA control interface so the host can open it + send the control IOCTLs.
let status =
unsafe { WdfDeviceCreateDeviceInterface(device, &SUVDA_INTERFACE_GUID, std::ptr::null()) };
if let Err(e) = status {
error!("Failed to create control device interface: {e:?}");
return e.into();
}
let status = unsafe { IddCxDeviceInitialize(device) };
if let Err(e) = status {
error!("Failed to init iddcx device: {e:?}");
return e.into();
}
let context = DeviceContext::new(device);
unsafe { context.init(device as WDFOBJECT).into() }
}
unsafe extern "C-unwind" fn event_cleanup(wdf_object: WDFOBJECT) {
_ = unsafe { DeviceContext::drop(wdf_object) };
}
@@ -0,0 +1,38 @@
use std::ops::{Deref, DerefMut};
/// An unsafe wrapper to allow sending across threads
///
/// USE WISELY, IT CAN CAUSE UB OTHERWISE
pub struct Sendable<T>(T);
unsafe impl<T> Send for Sendable<T> {}
unsafe impl<T> Sync for Sendable<T> {}
impl<T> Sendable<T> {
/// `T` must be Send+Sync safe
pub unsafe fn new(t: T) -> Self {
Sendable(t)
}
}
impl<T> Deref for Sendable<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for Sendable<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[macro_export]
macro_rules! debug {
($($tt:tt)*) => {
if cfg!(debug_assertions) {
::log::debug!($($tt)*);
}
};
}
@@ -0,0 +1,33 @@
//! pf-vdisplay — punktfunk Windows virtual display (IddCx), in Rust.
//!
//! P1: a UMDF2 IddCx virtual display. Adapted from MolotovCherry/virtual-display-rs (MIT) — its
//! named-pipe IPC + serde mode config is replaced by an in-tree `monitor` model (and, next, the
//! SudoVDA-compatible IOCTL control plane our host already speaks). Logging goes to
//! `OutputDebugString` (no `log`-eventlog/`tokio`). See `docs/windows-virtual-display-rust-port.md`.
#![allow(non_snake_case)]
mod callbacks;
mod context;
mod control;
mod direct_3d_device;
mod edid;
mod entry;
mod helpers;
mod logger;
mod monitor;
mod panic;
mod swap_chain_processor;
use wdf_umdf_sys::{NTSTATUS, PUNICODE_STRING, PVOID};
// The framework entry point. UMDF's reflector calls this; the `+whole-archive` stub forwards to the
// `DriverEntry` symbol exported from `entry.rs`.
#[link(name = "WdfDriverStubUm", kind = "static", modifiers = "+whole-archive")]
extern "C" {
pub fn FxDriverEntryUm(
LoaderInterface: PVOID,
Context: PVOID,
DriverObject: PVOID,
RegistryPath: PUNICODE_STRING,
) -> NTSTATUS;
}
@@ -0,0 +1,34 @@
//! Minimal `log` backend that writes to `OutputDebugString` — no `driver-logger`/event-log/`tokio`.
//! View with DebugView/WinDbg. Keeping the `log` facade lets the ported callbacks/context use
//! `error!`/`info!`/`debug!` unchanged.
use log::{LevelFilter, Metadata, Record};
use windows::core::PCSTR;
use windows::Win32::System::Diagnostics::Debug::OutputDebugStringA;
struct DbgLogger;
impl log::Log for DbgLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
let msg = format!("[pf-vdisplay] {:<5} {}\0", record.level(), record.args());
// SAFETY: `msg` is a NUL-terminated byte string valid for the call.
unsafe { OutputDebugStringA(PCSTR(msg.as_ptr())) };
}
fn flush(&self) {}
}
static LOGGER: DbgLogger = DbgLogger;
pub fn init() {
let _ = log::set_logger(&LOGGER);
log::set_max_level(if cfg!(debug_assertions) {
LevelFilter::Debug
} else {
LevelFilter::Info
});
}
@@ -0,0 +1,103 @@
//! The monitor + mode model and control-plane state. Replaces virtual-display-rs's `ipc.rs`
//! (named-pipe IPC + serde `driver_ipc` types). Monitors are created on demand by the SudoVDA IOCTL
//! control plane (`control.rs`); each carries the GUID the host keys it by plus the OS target id +
//! render-adapter LUID captured at arrival (the ADD reply).
use std::ptr::NonNull;
use std::sync::atomic::{AtomicU32, AtomicU64};
use std::sync::{Mutex, OnceLock};
use wdf_umdf_sys::{IDDCX_ADAPTER__, IDDCX_MONITOR__};
pub type Dimen = u32;
pub type RefreshRate = u32;
/// One resolution with the refresh rates it supports.
#[derive(Clone, PartialEq, Eq)]
pub struct Mode {
pub width: Dimen,
pub height: Dimen,
pub refresh_rates: Vec<RefreshRate>,
}
/// A monitor's identity (the EDID serial) + advertised modes.
#[derive(Clone)]
pub struct MonitorData {
pub id: u32,
pub modes: Vec<Mode>,
}
/// A live (or pending) monitor.
pub struct MonitorObject {
pub object: Option<NonNull<IDDCX_MONITOR__>>,
pub data: MonitorData,
/// The full GUID the host keys this monitor by (ADD dedup / REMOVE).
pub guid: u128,
/// OS target id + render-adapter LUID, captured from `IDARG_OUT_MONITORARRIVAL` (the ADD reply).
pub target_id: u32,
pub adapter_luid_low: u32,
pub adapter_luid_high: i32,
}
// SAFETY: the raw IddCx object ptr is framework-managed; access is serialized by MONITOR_MODES.
unsafe impl Send for MonitorObject {}
unsafe impl Sync for MonitorObject {}
/// The IddCx adapter object, stashed for the control plane (SET_RENDER_ADAPTER).
pub struct AdapterObject(pub NonNull<IDDCX_ADAPTER__>);
// SAFETY: raw ptr managed by the framework.
unsafe impl Send for AdapterObject {}
unsafe impl Sync for AdapterObject {}
pub static ADAPTER: OnceLock<AdapterObject> = OnceLock::new();
pub static MONITOR_MODES: Mutex<Vec<MonitorObject>> = Mutex::new(Vec::new());
/// Monitor id / EDID-serial counter (unique per created monitor).
pub static NEXT_ID: AtomicU32 = AtomicU32::new(1);
/// Watchdog (seconds). The host reads the timeout via GET_WATCHDOG and PINGs to keep alive.
pub static WATCHDOG_TIMEOUT: AtomicU32 = AtomicU32::new(3);
pub static WATCHDOG_COUNTDOWN: AtomicU32 = AtomicU32::new(3);
/// The preferred render adapter LUID set via SET_RENDER_ADAPTER, packed `(high<<32)|low`. 0 = none.
pub static PREFERRED_RENDER_ADAPTER: AtomicU64 = AtomicU64::new(0);
/// Protocol version reported by GET_VERSION: {major, minor, incremental, testbuild} — matches SudoVDA.
pub const PROTOCOL_VERSION: [u8; 4] = [0, 2, 1, 1];
/// A single (width, height, refresh) tuple — modes flattened across their refresh rates.
#[derive(Copy, Clone)]
pub struct ModeItem {
pub width: Dimen,
pub height: Dimen,
pub refresh_rate: RefreshRate,
}
pub trait FlattenModes {
fn flatten(&self) -> impl Iterator<Item = ModeItem>;
}
impl FlattenModes for Vec<Mode> {
fn flatten(&self) -> impl Iterator<Item = ModeItem> {
self.iter().flat_map(|m| {
m.refresh_rates.iter().map(|&rr| ModeItem {
width: m.width,
height: m.height,
refresh_rate: rr,
})
})
}
}
/// Fallback modes appended after the client's requested mode, so a topology change still has options.
pub 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],
},
]
}
@@ -0,0 +1,20 @@
#[cfg(debug_assertions)]
use std::backtrace::Backtrace;
use std::panic;
use log::error;
pub fn set_hook() {
panic::set_hook(Box::new(|v| {
// debug mode, get full backtrace
#[cfg(debug_assertions)]
{
let backtrace = Backtrace::force_capture();
error!("{v}\n\nstack backtrace:\n{backtrace}");
}
// otherwise just print the panic since we don't have a backtrace
#[cfg(not(debug_assertions))]
error!("{v}");
}));
}
@@ -0,0 +1,158 @@
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread::{self, JoinHandle},
};
use log::{debug, error};
use wdf_umdf::{
IddCxSwapChainFinishedProcessingFrame, IddCxSwapChainReleaseAndAcquireBuffer,
IddCxSwapChainSetDevice, WdfObjectDelete,
};
use wdf_umdf_sys::{
HANDLE, IDARG_IN_SWAPCHAINSETDEVICE, IDARG_OUT_RELEASEANDACQUIREBUFFER, IDDCX_SWAPCHAIN,
NTSTATUS, WAIT_TIMEOUT, WDFOBJECT,
};
use windows::{
core::{w, Interface},
Win32::{
Foundation::HANDLE as WHANDLE,
Graphics::Dxgi::IDXGIDevice,
System::Threading::{
AvRevertMmThreadCharacteristics, AvSetMmThreadCharacteristicsW, WaitForSingleObject,
},
},
};
use crate::{direct_3d_device::Direct3DDevice, helpers::Sendable};
pub struct SwapChainProcessor {
terminate: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
}
unsafe impl Send for SwapChainProcessor {}
unsafe impl Sync for SwapChainProcessor {}
impl SwapChainProcessor {
pub fn new() -> Self {
Self {
terminate: Arc::new(AtomicBool::new(false)),
thread: None,
}
}
pub fn run(
&mut self,
swap_chain: IDDCX_SWAPCHAIN,
device: Direct3DDevice,
available_buffer_event: HANDLE,
) {
let available_buffer_event = unsafe { Sendable::new(available_buffer_event) };
let swap_chain = unsafe { Sendable::new(swap_chain) };
let terminate = self.terminate.clone();
let join_handle = thread::spawn(move || {
// It is very important to prioritize this thread by making use of the Multimedia Scheduler Service.
// It will intelligently prioritize the thread for improved throughput in high CPU-load scenarios.
let mut av_task = 0u32;
let res = unsafe { AvSetMmThreadCharacteristicsW(w!("Distribution"), &mut av_task) };
let Ok(av_handle) = res else {
error!("Failed to prioritize thread: {res:?}");
return;
};
Self::run_core(*swap_chain, &device, *available_buffer_event, &terminate);
let res = unsafe { WdfObjectDelete(*swap_chain as WDFOBJECT) };
if let Err(e) = res {
error!("Failed to delete wdf object: {e:?}");
return;
}
// Revert the thread to normal once it's done
let res = unsafe { AvRevertMmThreadCharacteristics(av_handle) };
if let Err(e) = res {
error!("Failed to revert prioritize thread: {e:?}");
}
});
self.thread = Some(join_handle);
}
fn run_core(
swap_chain: IDDCX_SWAPCHAIN,
device: &Direct3DDevice,
available_buffer_event: HANDLE,
terminate: &AtomicBool,
) {
let dxgi_device = device.device.cast::<IDXGIDevice>();
let Ok(dxgi_device) = dxgi_device else {
error!("Failed to cast ID3D11Device to IDXGIDevice: {dxgi_device:?}");
return;
};
let set_device = IDARG_IN_SWAPCHAINSETDEVICE {
pDevice: dxgi_device.into_raw().cast(),
};
let res = unsafe { IddCxSwapChainSetDevice(swap_chain, &set_device) };
if res.is_err() {
debug!("Failed to set swapchain device: {res:?}");
return;
}
loop {
let mut buffer = IDARG_OUT_RELEASEANDACQUIREBUFFER::default();
let hr: NTSTATUS =
unsafe { IddCxSwapChainReleaseAndAcquireBuffer(swap_chain, &mut buffer).into() };
#[allow(clippy::items_after_statements)]
const E_PENDING: u32 = 0x8000_000A;
if u32::from(hr) == E_PENDING {
let wait_result =
unsafe { WaitForSingleObject(WHANDLE(available_buffer_event.cast()), 16).0 };
// thread requested an end
let should_terminate = terminate.load(Ordering::Relaxed);
if should_terminate {
break;
}
// WAIT_OBJECT_0 | WAIT_TIMEOUT
if matches!(wait_result, 0 | WAIT_TIMEOUT) {
// We have a new buffer, so try the AcquireBuffer again
continue;
}
// The wait was cancelled or something unexpected happened
break;
} else if hr.is_success() {
// This is the most performance-critical section of code in an IddCx driver. It's important that whatever
// is done with the acquired surface be finished as quickly as possible.
let hr = unsafe { IddCxSwapChainFinishedProcessingFrame(swap_chain) };
if hr.is_err() {
break;
}
} else {
// The swap-chain was likely abandoned (e.g. DXGI_ERROR_ACCESS_LOST), so exit the processing loop
break;
}
}
}
}
impl Drop for SwapChainProcessor {
fn drop(&mut self) {
if let Some(handle) = self.thread.take() {
// send signal to end thread
self.terminate.store(true, Ordering::Relaxed);
// wait until thread is finished
_ = handle.join();
}
}
}