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,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;
}
}