4 Commits

Author SHA1 Message Date
enricobuehler e2c9bfd3d9 feat(windows): pf-vdisplay IDD-push — HDR + pipelined zero-copy capture
apple / swift (push) Successful in 1m4s
windows-host / package (push) Successful in 6m28s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m14s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m10s
release / apple (push) Successful in 7m53s
android / android (push) Successful in 10m33s
ci / web (push) Successful in 44s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 3m4s
ci / docs-site (push) Successful in 53s
ci / rust (push) Successful in 12m22s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m11s
apple / screenshots (push) Successful in 5m24s
deb / build-publish (push) Successful in 3m16s
decky / build-publish (push) Successful in 21s
ci / bench (push) Successful in 4m42s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 27s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m34s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m42s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m13s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 47s
flatpak / build-publish (push) Successful in 4m24s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m5s
docker / deploy-docs (push) Successful in 25s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m44s
HDR (display-driven, matching the WGC path):
- CTA-861.3 HDR EDID (BT.2020 primaries + HDR Static Metadata block) so Windows
  offers "Use HDR" on the virtual display. The host FOLLOWS the display's live
  advanced-color state, recreating the shared ring at the matching format
  (FP16 in HDR / BGRA in SDR) on a toggle — no freeze.
- Always emit Main10/BT.2020-PQ Rgb10a2 while the display is HDR; the client
  auto-detects PQ from the HEVC VUI (clients under-report VIDEO_CAP_10BIT).
  Generic HDR10 mastering SEI on every IDR.
- Generation-tagged `latest` (gen<<40|seq<<8|slot) + driver `is_stale` re-attach
  kill the toggle-time garbage frame and any stale-ring read.

Perf:
- Pipeline the encode loop (Capturer::pipeline_depth; IDD-push = 2): submit N+1
  before polling N so the convert/copy on the 3D engine overlaps the NVENC encode
  of N on the ASIC. PUNKTFUNK_IDD_DEPTH overrides (1 = synchronous).
- Rotating host output ring (OUT_RING) so the in-flight encode and the next
  convert never touch the same texture.
- HDR converts directly from the keyed-mutex slot's SRV into the output ring
  (drops the redundant slot->fp16 scratch copy); SDR copies the BGRA slot in.
  The slot mutex is held only across the convert/copy, not the encode.
  RING_LEN 3->6 for publish headroom.
- Capture-health diagnostic: new_fps vs repeat_fps under PUNKTFUNK_PERF (a low
  new_fps at a high send rate means the source isn't compositing, not an encode
  stall).

Validated live on the RTX box: 5120x1440@240 HDR streams; driver composes
~180 new fps, encode 240 fps @ ~4.3 ms p50.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 00:39:28 +02:00
enricobuehler c5dab484df feat(windows): bundle pf-vdisplay in the host installer; drop SudoVDA
Switch the Inno Setup installer's virtual-display driver from the vendored SudoVDA
C++ binary to our own all-Rust pf-vdisplay (validated streaming at 5120x1440@240).

- packaging/windows/pf-vdisplay/: vendored SIGNED driver (pf_vdisplay.dll/inf/cat +
  punktfunk-driver.cer, the same cert the gamepad drivers ship), built from
  vdisplay-driver/ via deploy-dev.ps1.
- install-pf-vdisplay.ps1 / stage-pf-vdisplay.ps1: mirror the SudoVDA scripts -
  trust cert -> gated ROOT\pf_vdisplay node via nefconc (NEVER devgen) -> pnputil
  /add-driver /install. Idempotent, best-effort (never aborts the install).
- punktfunk-host.iss + pack-host-installer.ps1: install the pf-vdisplay bundle
  under the existing installdriver task.
- Removed the vendored SudoVDA driver + install-sudovda.ps1 + stage-sudovda.ps1.
- README + windows-host.yml: SudoVDA -> pf-vdisplay.

The host's vdisplay/sudovda.rs backend is unchanged - it drives whichever driver
provides the {e5bcc234} interface, now pf-vdisplay. Live installer build/test on
the runner is the remaining step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 00:39:28 +02:00
enricobuehler e27abc065e feat(windows): pf-vdisplay CLEAR_ALL — reap orphaned virtual monitors on startup
The "5-6 stale monitors that never tear down" failure (also seen with SudoVDA):
an orphan from a crashed/killed previous host lingers because the driver watchdog
is kept reset by a still-pinging new session, so it never fires for the orphan.

- Driver (pf-vdisplay control.rs): new IOCTL_CLEAR_ALL (0x804) -> tear down every
  monitor. A pf-vdisplay extension; SudoVDA returns invalid for it (ignored), so
  the host can issue it unconditionally.
- Host (vdisplay/sudovda.rs): send IOCTL_CLEAR_ALL once on startup (best-effort)
  to reap orphans before creating ours; and surface a failing keepalive PING (the
  old `let _ =` swallowed it, masking a lost control handle).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 00:36:21 +02:00
enricobuehler d39da4bc06 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>
2026-06-24 00:36:21 +02:00
60 changed files with 10136 additions and 218 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Build the punktfunk Windows HOST as a signed Inno Setup installer and publish it to Gitea's generic
# package registry, so a Windows GPU box can install the streaming host (SYSTEM service + bundled
# SudoVDA virtual-display driver + the web management console, run by a scheduled task on a bundled
# pf-vdisplay virtual-display driver + the web management console, run by a scheduled task on a bundled
# bun) from one signed setup.exe. Runs on the self-hosted Windows runner
# (host mode; scripts/ci/setup-windows-runner.ps1) — same MSVC/Windows-SDK/LLVM env as windows.yml.
#
+7 -2
View File
@@ -1454,11 +1454,16 @@ pub mod endpoint {
/// close, while a genuinely dead peer is still detected within `MAX_IDLE`.
fn stream_transport() -> Arc<quinn::TransportConfig> {
use std::time::Duration;
const MAX_IDLE: Duration = Duration::from_secs(20);
// 8s idle (was 20s): a vanished client is declared dead within 8s instead of 20, so its
// session tears down promptly — which the Windows IDD-push path needs so a RECONNECT recreates
// a fresh virtual monitor (a reused monitor's IddCx swap-chain dies) instead of joining the
// still-lingering old session. Active sessions are unaffected: video keeps the connection live,
// and the 4s keep-alive holds it open through quiet control periods.
const MAX_IDLE: Duration = Duration::from_secs(8);
const KEEP_ALIVE: Duration = Duration::from_secs(4);
let mut t = quinn::TransportConfig::default();
t.max_idle_timeout(Some(
quinn::IdleTimeout::try_from(MAX_IDLE).expect("20s is a valid QUIC idle timeout"),
quinn::IdleTimeout::try_from(MAX_IDLE).expect("8s is a valid QUIC idle timeout"),
));
t.keep_alive_interval(Some(KEEP_ALIVE));
Arc::new(t)
+37 -3
View File
@@ -142,6 +142,16 @@ pub trait Capturer: Send {
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
None
}
/// How many frames the encode loop may keep in flight (submitted but not yet polled) before it
/// blocks. `1` (the default) is the synchronous loop: capture → submit → poll-blocks, so the
/// per-frame wall time is `capture+convert + encode`. A capturer that hands a fresh output texture
/// per frame (so the encode of N reads a different texture than the convert of N+1 writes) can return
/// `>1` to PIPELINE: the loop submits N+1 before polling N, overlapping the convert/copy on the 3D
/// engine with the NVENC-ASIC encode of the prior frame, dropping per-frame wall toward `max(...)`.
fn pipeline_depth(&self) -> usize {
1
}
}
/// A deterministic moving test pattern (BGRx). Lets the spike exercise the encode → file →
@@ -302,7 +312,11 @@ pub fn open_portal_monitor() -> Result<Box<dyn Capturer>> {
/// [`crate::vdisplay::VirtualDisplay`] backend. The captured size is the size the output was
/// created at — native, no scaling.
#[cfg(target_os = "linux")]
pub fn capture_virtual_output(vout: crate::vdisplay::VirtualOutput) -> Result<Box<dyn Capturer>> {
pub fn capture_virtual_output(
vout: crate::vdisplay::VirtualOutput,
_want_hdr: bool,
) -> Result<Box<dyn Capturer>> {
// The Linux host stays 8-bit (HDR is blocked upstream), so `want_hdr` is unused here.
linux::PortalCapturer::from_virtual_output(vout).map(|c| Box::new(c) as Box<dyn Capturer>)
}
@@ -317,7 +331,10 @@ pub(crate) fn wgc_disabled() -> bool {
}
#[cfg(target_os = "windows")]
pub fn capture_virtual_output(vout: crate::vdisplay::VirtualOutput) -> Result<Box<dyn Capturer>> {
pub fn capture_virtual_output(
vout: crate::vdisplay::VirtualOutput,
want_hdr: bool,
) -> Result<Box<dyn Capturer>> {
let target = vout.win_capture.clone().ok_or_else(|| {
anyhow::anyhow!(
"SudoVDA target not yet an active display (needs a WDDM GPU to activate it)"
@@ -325,6 +342,18 @@ pub fn capture_virtual_output(vout: crate::vdisplay::VirtualOutput) -> Result<Bo
})?;
let pref = vout.preferred_mode;
let keep = vout.keepalive;
// P2 direct frame push (kill DDA): consume frames straight from the pf-vdisplay driver's shared
// ring — no Desktop Duplication, no win32u reparenting hook. Opt-in while it's A/B'd against DDA;
// `idd_push` takes the keepalive (owns the virtual display) so there's no fall-through.
if std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() {
// Recreate the monitor + ring per session (fix-teardown): a FRESH monitor reliably gets a
// working IddCx swap-chain, whereas a REUSED monitor's swap-chain dies after ~2 sessions and
// the host can't revive it. The driver's recreate crash (target id resolved to 0) is fixed by
// stamping target_id onto the monitor context. The ring is always FP16 (the driver composes
// the IDD in FP16); `want_hdr` selects the per-frame conversion (FP16 → Rgb10a2 vs Bgra).
return idd_push::IddPushCapturer::open(target, pref, want_hdr, keep)
.map(|c| Box::new(c) as Box<dyn Capturer>);
}
// WGC (Windows.Graphics.Capture) is the default: it captures the COMPOSED desktop including the
// overlay/independent-flip planes DXGI Desktop Duplication misses (the frozen-HDR-animation bug),
// and has no ACCESS_LOST-on-overlay churn. DDA stays available via PUNKTFUNK_CAPTURE=dda and is
@@ -376,7 +405,10 @@ pub fn capture_virtual_output(vout: crate::vdisplay::VirtualOutput) -> Result<Bo
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
pub fn capture_virtual_output(_vout: crate::vdisplay::VirtualOutput) -> Result<Box<dyn Capturer>> {
pub fn capture_virtual_output(
_vout: crate::vdisplay::VirtualOutput,
_want_hdr: bool,
) -> Result<Box<dyn Capturer>> {
anyhow::bail!("virtual-output capture requires Linux or Windows")
}
@@ -386,6 +418,8 @@ pub mod composed_flip;
pub mod desktop_watch;
#[cfg(target_os = "windows")]
pub mod dxgi;
#[cfg(target_os = "windows")]
pub mod idd_push;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "windows")]
+130 -74
View File
@@ -202,6 +202,87 @@ pub(crate) unsafe fn make_device(
Ok((device, context))
}
/// Resolve the configured GPU scheduling-priority class from `PUNKTFUNK_GPU_PRIORITY_CLASS`
/// (`off|normal|high|realtime`, default high). `None` = leave it at the OS default (the `off` opt-out).
/// D3DKMT_SCHEDULINGPRIORITYCLASS: IDLE 0, BELOW_NORMAL 1, NORMAL 2, ABOVE_NORMAL 3, HIGH 4, REALTIME 5.
fn configured_gpu_priority_class() -> Option<i32> {
match std::env::var("PUNKTFUNK_GPU_PRIORITY_CLASS")
.ok()
.as_deref()
{
Some("off") => None,
Some("normal") => Some(2),
Some("realtime") => Some(5),
_ => Some(4), // HIGH — safe on NVIDIA+HAGS (realtime can freeze NVENC)
}
}
/// Enable SE_INC_BASE_PRIORITY on the CURRENT process token (best-effort) — the kernel gates the
/// HIGH/REALTIME GPU scheduling-priority bump on it. Held by SYSTEM/Administrators; a UAC-FILTERED
/// token (what `CreateProcessAsUserW` hands the WGC helper) does NOT have it, which is why the helper
/// can't elevate itself and the SYSTEM host stamps the class onto it cross-process instead (see
/// [`set_child_gpu_priority_class`]).
unsafe fn enable_inc_base_priority() {
use windows::core::PCWSTR;
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
use windows::Win32::Security::{
AdjustTokenPrivileges, LookupPrivilegeValueW, LUID_AND_ATTRIBUTES,
SE_INC_BASE_PRIORITY_NAME, SE_PRIVILEGE_ENABLED, TOKEN_ADJUST_PRIVILEGES, TOKEN_PRIVILEGES,
TOKEN_QUERY,
};
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
let mut token = HANDLE::default();
if OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&mut token,
)
.is_ok()
{
let mut luid = LUID::default();
if LookupPrivilegeValueW(PCWSTR::null(), SE_INC_BASE_PRIORITY_NAME, &mut luid).is_ok() {
let tp = TOKEN_PRIVILEGES {
PrivilegeCount: 1,
Privileges: [LUID_AND_ATTRIBUTES {
Luid: luid,
Attributes: SE_PRIVILEGE_ENABLED,
}],
};
if AdjustTokenPrivileges(
token,
false,
Some(&tp as *const TOKEN_PRIVILEGES),
0,
None,
None,
)
.is_err()
{
tracing::warn!("could not enable SE_INC_BASE_PRIORITY for GPU priority");
}
}
let _ = CloseHandle(token);
}
}
/// Call `gdi32!D3DKMTSetProcessSchedulingPriorityClass(process, prio)` (no stable windows-rs binding —
/// loaded by name). Returns the NTSTATUS (0 = success) or `None` if the export can't be resolved. The
/// CALLING process must hold SE_INC_BASE_PRIORITY ([`enable_inc_base_priority`]) for HIGH/REALTIME; the
/// kernel checks the caller's privilege whether the target is self or a child we created.
unsafe fn d3dkmt_set_scheduling_priority_class(
process: windows::Win32::Foundation::HANDLE,
prio: i32,
) -> Option<i32> {
use windows::core::s;
use windows::Win32::Foundation::HANDLE;
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA};
let gdi32 = LoadLibraryA(s!("gdi32.dll")).ok()?;
let p = GetProcAddress(gdi32, s!("D3DKMTSetProcessSchedulingPriorityClass"))?;
type SetPrio = unsafe extern "system" fn(HANDLE, i32) -> i32;
let f: SetPrio = std::mem::transmute(p);
Some(f(process, prio))
}
/// Apollo-style GPU scheduling-priority hardening (Sunshine `display_base.cpp:599-709`). On a
/// GPU-saturated game our capture+encode process is starved of GPU time slices — NVENC sits ~idle but
/// `lock_bitstream` waits ~20 ms for our context to be scheduled. Elevating the PROCESS GPU scheduling
@@ -209,89 +290,64 @@ pub(crate) unsafe fn make_device(
/// alone, which we measured as no help) lets our brief encode preempt the game. Uses HIGH, NOT
/// realtime: realtime on NVIDIA + HAGS can freeze/crash NVENC (Apollo downgrades it for exactly this).
/// Runs once per process; best-effort. `PUNKTFUNK_GPU_PRIORITY_CLASS = off|normal|high|realtime`
/// (default high).
/// (default high). NOTE: in the SYSTEM-host + user-session-helper deployment this self-set NO-OPs in
/// the helper (filtered token), so the host also sets it on the helper via [`set_child_gpu_priority_class`].
fn elevate_process_gpu_priority() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| unsafe {
use windows::core::{s, PCWSTR};
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
use windows::Win32::Security::{
AdjustTokenPrivileges, LookupPrivilegeValueW, LUID_AND_ATTRIBUTES,
SE_INC_BASE_PRIORITY_NAME, SE_PRIVILEGE_ENABLED, TOKEN_ADJUST_PRIVILEGES,
TOKEN_PRIVILEGES, TOKEN_QUERY,
use windows::Win32::System::Threading::GetCurrentProcess;
let Some(prio) = configured_gpu_priority_class() else {
tracing::info!("GPU process scheduling priority class left at default (off)");
return;
};
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA};
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
// D3DKMT_SCHEDULINGPRIORITYCLASS: IDLE 0, BELOW_NORMAL 1, NORMAL 2, ABOVE_NORMAL 3, HIGH 4,
// REALTIME 5.
let prio: i32 = match std::env::var("PUNKTFUNK_GPU_PRIORITY_CLASS").ok().as_deref() {
Some("off") => {
tracing::info!("GPU process scheduling priority class left at default (off)");
return;
}
Some("normal") => 2,
Some("realtime") => 5,
_ => 4, // HIGH — safe on NVIDIA+HAGS (realtime can freeze NVENC)
};
// 1. Enable SE_INC_BASE_PRIORITY so the kernel permits the GPU priority bump.
let mut token = HANDLE::default();
if OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&mut token,
)
.is_ok()
{
let mut luid = LUID::default();
if LookupPrivilegeValueW(PCWSTR::null(), SE_INC_BASE_PRIORITY_NAME, &mut luid).is_ok() {
let tp = TOKEN_PRIVILEGES {
PrivilegeCount: 1,
Privileges: [LUID_AND_ATTRIBUTES {
Luid: luid,
Attributes: SE_PRIVILEGE_ENABLED,
}],
};
if AdjustTokenPrivileges(
token,
false,
Some(&tp as *const TOKEN_PRIVILEGES),
0,
None,
None,
)
.is_err()
{
tracing::warn!("could not enable SE_INC_BASE_PRIORITY for GPU priority");
}
}
let _ = CloseHandle(token);
}
// 2. D3DKMTSetProcessSchedulingPriorityClass via gdi32 (no stable windows-rs binding).
if let Ok(gdi32) = LoadLibraryA(s!("gdi32.dll")) {
if let Some(p) = GetProcAddress(gdi32, s!("D3DKMTSetProcessSchedulingPriorityClass")) {
type SetPrio = unsafe extern "system" fn(HANDLE, i32) -> i32;
let f: SetPrio = std::mem::transmute(p);
let st = f(GetCurrentProcess(), prio);
if st == 0 {
tracing::info!(
priority_class = prio,
"GPU process scheduling priority class set (2=normal 4=high 5=realtime)"
);
} else {
tracing::warn!(
status = format!("0x{st:08X}"),
"D3DKMTSetProcessSchedulingPriorityClass failed (run as admin/SYSTEM for GPU priority)"
);
}
}
enable_inc_base_priority();
match d3dkmt_set_scheduling_priority_class(GetCurrentProcess(), prio) {
Some(0) => tracing::info!(
priority_class = prio,
"GPU process scheduling priority class set (2=normal 4=high 5=realtime)"
),
Some(st) => tracing::warn!(
status = format!("0x{st:08X}"),
"D3DKMTSetProcessSchedulingPriorityClass failed (run as admin/SYSTEM for GPU priority)"
),
None => tracing::warn!("D3DKMTSetProcessSchedulingPriorityClass export not found"),
}
});
}
/// Set the GPU scheduling-priority class of ANOTHER process we created — the WGC capture+encode helper
/// in the interactive user session. The helper is spawned with the user's UAC-FILTERED token, which
/// lacks SE_INC_BASE_PRIORITY, so its own [`elevate_process_gpu_priority`] silently no-ops and NVENC
/// gets starved under a GPU-saturating game (the "240→40 fps in-game collapse"). The SYSTEM host DOES
/// hold the privilege, so it stamps the class onto the child's process handle right after spawn — the
/// process-level class applies to GPU contexts the child creates afterwards. Best-effort; logged.
/// `PUNKTFUNK_GPU_PRIORITY_CLASS=off` disables it (same knob as the self path).
///
/// # Safety
/// `process` must be a valid handle to a process we own with at least PROCESS_SET_INFORMATION access
/// (the just-created helper, `PROCESS_INFORMATION::hProcess`).
pub(crate) unsafe fn set_child_gpu_priority_class(process: windows::Win32::Foundation::HANDLE) {
let Some(prio) = configured_gpu_priority_class() else {
return;
};
enable_inc_base_priority(); // the SYSTEM host holds SE_INC_BASE_PRIORITY; the helper does not
match d3dkmt_set_scheduling_priority_class(process, prio) {
Some(0) => tracing::info!(
priority_class = prio,
"WGC helper GPU scheduling priority class set cross-process from the SYSTEM host \
(2=normal 4=high 5=realtime)"
),
Some(st) => tracing::warn!(
status = format!("0x{st:08X}"),
"cross-process D3DKMTSetProcessSchedulingPriorityClass on the WGC helper failed"
),
None => tracing::warn!(
"D3DKMTSetProcessSchedulingPriorityClass export not found — WGC helper has no GPU priority"
),
}
}
/// Re-find the output, make a fresh device on its adapter, and duplicate it. Used by the ACCESS_LOST
/// recovery to rebuild the whole capture on the current (possibly secure) input desktop.
unsafe fn reopen_duplication(
@@ -0,0 +1,922 @@
//! P2 direct frame push (kill DDA) — HOST side. The pf-vdisplay driver runs in a restricted WUDFHost
//! token that canNOT create named kernel objects, so — exactly like the gamepad UMDF drivers
//! (`inject/dualsense_windows.rs`) — the HOST (privileged) CREATES the shared header + frame-ready
//! event + ring of keyed-mutex textures (`Global\` names, permissive `D:(A;;GA;;;WD)` SDDL) on the
//! discrete render GPU, and the driver only OPENS them and copies frames in. We then consume the ring
//! straight into the zero-copy NVENC path — no DXGI Desktop Duplication, no `win32u` hook. Gated by
//! `PUNKTFUNK_IDD_PUSH`. Driver counterpart: `packaging/windows/vdisplay-driver/pf-vdisplay/src/
//! frame_transport.rs` — [`SharedHeader`], [`MAGIC`], [`RING_LEN`], the status codes and the `Global\`
//! name scheme are DUPLICATED byte-identically there.
use super::dxgi::{make_device, D3d11Frame, HdrConverter, WinCaptureTarget};
use super::{CapturedFrame, Capturer, FramePayload, PixelFormat};
use anyhow::{bail, Context, Result};
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use windows::core::{w, Interface, HSTRING};
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE, LUID};
use windows::Win32::Graphics::Direct3D11::{
ID3D11Device, ID3D11DeviceContext, ID3D11RenderTargetView, ID3D11ShaderResourceView,
ID3D11Texture2D, D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE,
D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX, D3D11_RESOURCE_MISC_SHARED_NTHANDLE,
D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM,
DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::{
CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory4, IDXGIKeyedMutex, IDXGIResource1,
};
use windows::Win32::Security::Authorization::{
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
};
use windows::Win32::Security::{PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES};
use windows::Win32::System::Memory::{
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS,
MEMORY_MAPPED_VIEW_ADDRESS, PAGE_READWRITE,
};
use windows::Win32::System::Threading::{CreateEventW, WaitForSingleObject};
// --- kept byte-identical with the driver (frame_transport.rs) ---
pub const MAGIC: u32 = 0x4456_4650;
pub const VERSION: u32 = 1;
/// Ring slots — MUST equal the driver's `RING_LEN` (frame_transport.rs). 6 (was 3) gives ample headroom
/// so the driver's 0 ms-timeout publish always finds a free slot while the host briefly holds one across
/// the convert/copy into its output ring and the depth-2 pipelined encode runs on the rest.
pub const RING_LEN: u32 = 6;
const DXGI_SHARED_RESOURCE_RW: u32 = 0x8000_0000 | 0x1;
// driver_status codes (the driver writes these; we read+log them).
const DRV_STATUS_OPENED: u32 = 1;
const DRV_STATUS_TEX_FAIL: u32 = 2;
const DRV_STATUS_NO_DEVICE1: u32 = 3;
/// Host-owned output-ring depth: distinct NVENC-input textures rotated per frame so the in-flight
/// encode of frame N and the convert/copy of frame N+1 never touch the same texture. 3 covers a
/// pipeline depth of 2 with one slot of margin.
const OUT_RING: usize = 3;
#[repr(C)]
struct SharedHeader {
magic: u32,
version: u32,
generation: u32,
ring_len: u32,
width: u32,
height: u32,
dxgi_format: u32,
_pad: u32,
latest: u64,
qpc_pts: u64,
driver_render_luid_low: u32,
driver_render_luid_high: i32,
driver_status: u32,
driver_status_detail: u32,
}
/// Bring-up debug block (fixed name) — the host creates it; the driver writes diagnostics into it
/// independent of the per-target header. Byte-identical with the driver's `DebugBlock`.
#[repr(C)]
struct DebugBlock {
magic: u32,
run_core_entries: u32,
resolved_target_id: u32,
header_open_attempts: u32,
last_open_error: u32,
header_opened: u32,
render_luid_low: u32,
render_luid_high: i32,
frames_acquired: u32,
_pad: u32,
}
const DBG_NAME: &str = "Global\\pfvd-dbg";
const DBG_MAGIC: u32 = 0x4742_4450;
fn hdr_name(target_id: u32) -> String {
format!("Global\\pfvd-hdr-{target_id}")
}
fn evt_name(target_id: u32) -> String {
format!("Global\\pfvd-evt-{target_id}")
}
fn tex_name(target_id: u32, generation: u32, slot: u32) -> String {
format!("Global\\pfvd-tex-{target_id}-{generation}-{slot}")
}
// ----------------------------------------------------------------
/// Monotonic per-process generation: each capturer instance stamps its ring-texture names with a
/// fresh value so a retried/overlapping `open()` never collides with a previous attempt's not-yet-
/// released shared-handle names (`DXGI_ERROR_NAME_ALREADY_EXISTS`). The driver reads it from the header.
static IDD_GENERATION: AtomicU32 = AtomicU32::new(1);
fn now_ns() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
}
struct HostSlot {
tex: ID3D11Texture2D,
mutex: IDXGIKeyedMutex,
shared: HANDLE,
/// SRV on the slot texture so the HDR path samples the FP16 slot DIRECTLY (no slot→scratch copy);
/// the convert pass writes the output ring while holding the slot's keyed mutex. Unused for SDR
/// (which CopyResource's the BGRA slot straight to the output).
srv: ID3D11ShaderResourceView,
}
impl Drop for HostSlot {
fn drop(&mut self) {
unsafe {
let _ = CloseHandle(self.shared);
}
}
}
/// Creates + owns the shared ring; yields the driver's frames as [`FramePayload::D3d11`].
pub struct IddPushCapturer {
device: ID3D11Device,
context: ID3D11DeviceContext,
target_id: u32,
map: HANDLE,
header: *mut SharedHeader,
event: HANDLE,
dbg_map: HANDLE,
dbg_block: *mut DebugBlock,
width: u32,
height: u32,
slots: Vec<HostSlot>,
/// The ring/texture generation, bumped every time the ring is recreated at a new format (the
/// display's HDR mode flipped). Stamped into the texture names + the header so the driver re-attaches.
generation: u32,
/// The CLIENT's advertised 10-bit capability (= negotiated `bit_depth >= 10`). Only used at `open`
/// to PROACTIVELY enable advanced color (so a 10-bit client gets HDR without a manual toggle); it
/// does NOT gate the per-frame conversion — that follows the display, like the WGC path (clients
/// under-report 10-bit yet all decode Main10 + auto-detect PQ from the VUI).
client_10bit: bool,
/// The DISPLAY's CURRENT HDR state (from `advanced_color_enabled`) — the user can flip "Use HDR" in
/// Windows mid-session. Drives the ring format (HDR → FP16 surfaces, SDR → BGRA) and the conversion.
/// Polled in the capture loop; a change recreates the ring (see [`Self::recreate_ring`]).
display_hdr: bool,
/// Throttle for the `advanced_color_enabled` poll (a CCD `QueryDisplayConfig`, ~ms — too costly per
/// frame at 240 Hz).
last_acm_poll: Instant,
/// Host-owned ROTATING output ring NVENC encodes (texture + RTV per slot). Rotating it per frame is
/// the precondition for pipelining the encode loop: while NVENC encodes frame N's texture on the
/// ASIC, frame N+1's convert/copy writes a DIFFERENT texture on the 3D engine — the two overlap. The
/// HDR convert and the SDR copy both write into the current slot. Format = `out_format()` (Rgb10a2 in
/// HDR, Bgra in SDR); rebuilt on a display-mode flip. Built lazily.
out_ring: Vec<(ID3D11Texture2D, ID3D11RenderTargetView)>,
out_idx: usize,
/// FP16 scRGB → `Rgb10a2` BT.2020 PQ converter, used while the display is HDR. Built lazily.
hdr_conv: Option<HdrConverter>,
last_seq: u64,
last_present: Option<(ID3D11Texture2D, PixelFormat)>,
status_logged: bool,
/// The monitor generation this capturer was opened for. When the active monitor gen changes (a
/// reconnect preempted + recreated the monitor), `next_frame` bails immediately so this session
/// releases its NVENC encoder instead of lingering on the dead ring's 20s deadline.
my_gen: u64,
_keepalive: Box<dyn Send>,
}
// COM objects used only from the owning (encode) thread.
unsafe impl Send for IddPushCapturer {}
/// The persistent IDD-push capturer, kept alive for the host lifetime and SHARED across client
/// sessions. The driver's per-session monitor TEARDOWN→RECREATE path is unstable (on session 2 the
/// target-id resolves to 0, `IddCxSwapChainSetDevice` fails `0x80070057`, then an access violation),
/// while the FIRST-session path is solid. So we create the monitor + ring + swap-chain ONCE and hand
/// every later session a thin handle delegating to this one. The persistent capturer holds a monitor
/// lease for the host lifetime, so `VirtualDisplay::create` always JOINs the same live monitor (same
/// target id) and the reuse match always hits — no recreate, no driver crash. Prototype scope:
/// single-client, single-mode (a different mode would need a recreate, the unstable path).
static IDD_PERSIST: Mutex<Option<IddPushCapturer>> = Mutex::new(None);
/// Open the IDD-push capturer, reusing the persistent one across sessions (see [`IDD_PERSIST`]).
pub fn open_or_reuse(
target: WinCaptureTarget,
preferred: Option<(u32, u32, u32)>,
client_10bit: bool,
keepalive: Box<dyn Send>,
) -> Result<Box<dyn Capturer>> {
let (w, h, _) =
preferred.context("IDD push needs the negotiated mode (WxH) to size the ring")?;
let mut slot = IDD_PERSIST.lock().unwrap();
let reuse = matches!(slot.as_ref(), Some(c) if c.target_id == target.target_id && c.width == w && c.height == h);
match slot.as_mut() {
Some(c) if reuse => {
// Reuse: the persistent capturer already owns the monitor + ring + driver attach. Drop the
// new per-session monitor lease (the persistent capturer's lease keeps the monitor live).
// The ring tracks the display, not the client; only the client's 10-bit cap can differ.
drop(keepalive);
c.set_client_10bit(client_10bit);
tracing::info!(
target_id = target.target_id,
client_10bit,
"IDD push: reusing the persistent capturer (no monitor/ring recreate)"
);
}
Some(c) => bail!(
"IDD-push persistent capturer is {}x{} target {}, this session wants {}x{} target {} — a \
mode/target change needs a recreate (the driver's recreate path is unstable); not \
supported in the persistent prototype",
c.width,
c.height,
c.target_id,
w,
h,
target.target_id
),
None => {
tracing::info!(
target_id = target.target_id,
client_10bit,
"IDD push: creating the persistent capturer (first session)"
);
*slot = Some(IddPushCapturer::open(target, preferred, client_10bit, keepalive)?);
}
}
Ok(Box::new(IddReuseHandle))
}
/// Thin per-session handle: every method delegates to the single persistent [`IddPushCapturer`].
/// Dropping it (session end) does NOT tear down the ring/monitor — that's the whole point.
struct IddReuseHandle;
impl Capturer for IddReuseHandle {
fn next_frame(&mut self) -> Result<CapturedFrame> {
IDD_PERSIST
.lock()
.unwrap()
.as_mut()
.context("IDD-push persistent capturer missing")?
.next_frame()
}
fn try_latest(&mut self) -> Result<Option<CapturedFrame>> {
IDD_PERSIST
.lock()
.unwrap()
.as_mut()
.context("IDD-push persistent capturer missing")?
.try_latest()
}
fn set_active(&self, active: bool) {
if let Some(c) = IDD_PERSIST.lock().unwrap().as_ref() {
c.set_active(active);
}
}
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
IDD_PERSIST
.lock()
.unwrap()
.as_ref()
.and_then(|c| c.hdr_meta())
}
}
/// Build a permissive (Everyone:GenericAll) `SECURITY_ATTRIBUTES` so the restricted WUDFHost driver
/// can OPEN the host-created objects — the same `D:(A;;GA;;;WD)` SDDL the gamepad shared section uses.
/// The returned `psd` backing must outlive `sa`; both are dropped when the process exits.
unsafe fn permissive_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTOR)> {
let mut psd = PSECURITY_DESCRIPTOR::default();
ConvertStringSecurityDescriptorToSecurityDescriptorW(
w!("D:(A;;GA;;;WD)"),
SDDL_REVISION_1,
&mut psd,
None,
)
.context("build SDDL for IDD-push shared objects")?;
let sa = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: psd.0,
bInheritHandle: false.into(),
};
Ok((sa, psd))
}
impl IddPushCapturer {
/// Create the `RING_LEN` shared keyed-mutex textures for one ring generation, at `format` (matched
/// to the display's composition format — FP16 in HDR, BGRA in SDR). Each is shared by the name
/// `pfvd-tex-<target>-<generation>-<k>` so the driver opens it; a fresh generation gives fresh names
/// (so a recreate never collides with the old ring's not-yet-released handles).
unsafe fn create_ring_slots(
device: &ID3D11Device,
target_id: u32,
generation: u32,
w: u32,
h: u32,
format: DXGI_FORMAT,
) -> Result<Vec<HostSlot>> {
let (sa, _psd) = permissive_sa()?;
let mut slots = Vec::new();
for k in 0..RING_LEN {
let desc = D3D11_TEXTURE2D_DESC {
Width: w,
Height: h,
MipLevels: 1,
ArraySize: 1,
// Match the OS-composed swap-chain surfaces so the driver's CopyResource into the slot +
// its format-guard both succeed.
Format: format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
CPUAccessFlags: 0,
MiscFlags: (D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0
| D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0) as u32,
};
let mut tex: Option<ID3D11Texture2D> = None;
device
.CreateTexture2D(&desc, None, Some(&mut tex))
.context("CreateTexture2D(IDD-push ring slot)")?;
let tex = tex.context("null ring texture")?;
let res1: IDXGIResource1 = tex.cast()?;
let shared = res1
.CreateSharedHandle(
Some(&sa as *const SECURITY_ATTRIBUTES),
DXGI_SHARED_RESOURCE_RW,
&HSTRING::from(tex_name(target_id, generation, k)),
)
.context("CreateSharedHandle(IDD-push ring slot)")?;
let mutex: IDXGIKeyedMutex = tex.cast()?;
let mut srv: Option<ID3D11ShaderResourceView> = None;
device
.CreateShaderResourceView(&tex, None, Some(&mut srv))
.context("CreateShaderResourceView(IDD-push ring slot)")?;
let srv = srv.context("null slot srv")?;
slots.push(HostSlot {
tex,
mutex,
shared,
srv,
});
}
Ok(slots)
}
pub fn open(
target: WinCaptureTarget,
preferred: Option<(u32, u32, u32)>,
client_10bit: bool,
keepalive: Box<dyn Send>,
) -> Result<Self> {
let (w, h, _hz) = preferred
.context("IDD push needs the negotiated mode (WxH) to size the shared ring")?;
// The driver composes the virtual display in FP16 (R16G16B16A16_FLOAT scRGB) when the display is
// in advanced-color (HDR) mode, and 8-bit BGRA otherwise (per swap_chain_processor.rs + the
// COMMIT_MODES2 colorspace/rgb_bpc log). The user can flip "Use HDR" in Windows at any time, so
// the ring format must TRACK the display's ACTUAL mode (the driver's format-guard drops a
// mismatch). We poll the live state here and on every recreate. For a 10-bit-capable client we
// PROACTIVELY enable advanced color so HDR streams without the user toggling anything; an
// SDR-only client leaves the display alone (and still gets a tone-mapped picture, never a freeze,
// if the user does enable HDR).
unsafe {
if client_10bit && crate::vdisplay::sudovda::set_advanced_color(target.target_id, true)
{
// Let the colorspace change settle before the driver composes + we size the ring.
std::thread::sleep(Duration::from_millis(250));
}
let display_hdr = crate::vdisplay::sudovda::advanced_color_enabled(target.target_id);
let ring_fmt = if display_hdr {
DXGI_FORMAT_R16G16B16A16_FLOAT
} else {
DXGI_FORMAT_B8G8R8A8_UNORM
};
// Create our device on the discrete render GPU (where NVENC runs); the driver must render
// the swap-chain on the SAME adapter for the shared textures to open (it reports its actual
// render LUID into the header so we can detect a mismatch).
let luid = resolve_render_adapter_luid_or(target.adapter_luid);
let factory: IDXGIFactory4 = CreateDXGIFactory1().context("CreateDXGIFactory1")?;
let adapter: IDXGIAdapter1 = factory
.EnumAdapterByLuid(luid)
.context("EnumAdapterByLuid(render adapter) for IDD push")?;
let (device, context) = make_device(&adapter).context("make_device for IDD push")?;
let (sa, _psd) = permissive_sa()?;
let bytes = std::mem::size_of::<SharedHeader>().max(64);
// Header.
let map = CreateFileMappingW(
INVALID_HANDLE_VALUE,
Some(&sa),
PAGE_READWRITE,
0,
bytes as u32,
&HSTRING::from(hdr_name(target.target_id)),
)
.context("CreateFileMapping(IDD-push header)")?;
let view = MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, bytes);
if view.Value.is_null() {
let _ = CloseHandle(map);
bail!("MapViewOfFile failed for IDD-push header");
}
let generation = IDD_GENERATION.fetch_add(1, Ordering::Relaxed);
let header = view.Value.cast::<SharedHeader>();
std::ptr::write_bytes(header.cast::<u8>(), 0, bytes);
(*header).version = VERSION;
(*header).generation = generation;
(*header).ring_len = RING_LEN;
(*header).width = w;
(*header).height = h;
// Ring format = the display's composition format (FP16 in HDR, BGRA in SDR). The driver
// reads this into its `ring_format` and drops any surface that doesn't match.
(*header).dxgi_format = ring_fmt.0 as u32;
// Frame-ready event (auto-reset).
let event = CreateEventW(
Some(&sa),
false,
false,
&HSTRING::from(evt_name(target.target_id)),
)
.context("CreateEvent(IDD-push)")?;
// Ring of shared keyed-mutex textures, format matched to the display's current mode.
let slots =
Self::create_ring_slots(&device, target.target_id, generation, w, h, ring_fmt)?;
// Bring-up debug block (fixed name) — the driver writes diagnostics here. Best-effort.
let dbg_bytes = std::mem::size_of::<DebugBlock>();
let (dbg_map, dbg_block) = match CreateFileMappingW(
INVALID_HANDLE_VALUE,
Some(&sa),
PAGE_READWRITE,
0,
dbg_bytes as u32,
&HSTRING::from(DBG_NAME),
) {
Ok(dm) => {
let dv = MapViewOfFile(dm, FILE_MAP_ALL_ACCESS, 0, 0, dbg_bytes);
if dv.Value.is_null() {
let _ = CloseHandle(dm);
(HANDLE::default(), std::ptr::null_mut())
} else {
let p = dv.Value.cast::<DebugBlock>();
std::ptr::write_bytes(p.cast::<u8>(), 0, dbg_bytes);
(*p).magic = DBG_MAGIC;
(dm, p)
}
}
Err(_) => (HANDLE::default(), std::ptr::null_mut()),
};
// Publish: magic LAST (Release) — signals the driver the ring is ready to open.
std::sync::atomic::fence(Ordering::Release);
(*(std::ptr::addr_of!((*header).magic) as *const AtomicU32))
.store(MAGIC, Ordering::Release);
tracing::info!(
target_id = target.target_id,
render_luid = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart),
mode = format!("{w}x{h}"),
display_hdr,
client_10bit,
ring_fp16 = display_hdr,
"IDD push(host): created shared ring; waiting for the driver to attach + publish"
);
Ok(Self {
device,
context,
target_id: target.target_id,
map,
header,
event,
dbg_map,
dbg_block,
width: w,
height: h,
slots,
generation,
client_10bit,
display_hdr,
last_acm_poll: Instant::now(),
out_ring: Vec::new(),
out_idx: 0,
hdr_conv: None,
last_seq: 0,
last_present: None,
status_logged: false,
my_gen: crate::vdisplay::sudovda::CURRENT_MON_GEN.load(Ordering::Relaxed),
_keepalive: keepalive,
})
}
}
#[inline]
fn latest(&self) -> u64 {
unsafe {
(*(std::ptr::addr_of!((*self.header).latest) as *const AtomicU64))
.load(Ordering::Acquire)
}
}
/// Log the driver's status once it first reports (the only driver-visibility channel we have).
fn log_driver_status_once(&mut self) {
if self.status_logged {
return;
}
let (status, detail, lo, hi) = unsafe {
(
(*self.header).driver_status,
(*self.header).driver_status_detail,
(*self.header).driver_render_luid_low,
(*self.header).driver_render_luid_high,
)
};
if status == 0 {
return;
}
self.status_logged = true;
let render_luid = format!("{hi:08x}:{lo:08x}");
match status {
DRV_STATUS_OPENED => tracing::info!(
render_luid,
"IDD push: driver attached to the shared ring"
),
DRV_STATUS_TEX_FAIL => tracing::error!(
render_luid,
detail = format!("0x{detail:08x}"),
"IDD push: driver could NOT open our textures — render-adapter mismatch (it renders on \
a different GPU than where we created the ring)"
),
DRV_STATUS_NO_DEVICE1 => {
tracing::error!("IDD push: driver has no ID3D11Device1 to open shared resources")
}
other => tracing::warn!(other, render_luid, "IDD push: driver reported an unknown status"),
}
}
/// Log the driver's bring-up diagnostics (the fixed-name debug block) — independent of the
/// per-target header, so it tells us whether the swap-chain processor ran, what target_id it
/// resolved, whether the header opened (+ error), and whether frames flowed.
fn log_debug_block(&self) {
if self.dbg_block.is_null() {
tracing::warn!("IDD push DEBUG: no debug block");
return;
}
let d = unsafe { &*self.dbg_block };
tracing::error!(
run_core_entries = d.run_core_entries,
resolved_target_id = d.resolved_target_id,
header_open_attempts = d.header_open_attempts,
last_open_error = format!("0x{:08x}", d.last_open_error),
header_opened = d.header_opened,
driver_render_luid = format!("{:08x}:{:08x}", d.render_luid_high, d.render_luid_low),
frames_acquired = d.frames_acquired,
"IDD push DEBUG: driver-reported diagnostics (run_core_entries=0 ⇒ swap-chain processor \
never ran; resolved_target_id≠ours ⇒ name mismatch; last_open_error 0x80070002 ⇒ header \
not found; frames_acquired=0 ⇒ idle display)"
);
}
/// The output texture format + the [`PixelFormat`] it presents as, driven SOLELY by the DISPLAY's
/// HDR state (like the WGC path): HDR → `Rgb10a2` BT.2020 PQ → NVENC Main10, and the client
/// auto-detects PQ from the HEVC VUI; SDR → 8-bit `Bgra`. We do NOT gate HDR on the client's
/// advertised `VIDEO_CAP_10BIT` — clients under-report it (e.g. the Mac advertises 10-bit only when
/// its OWN display is HDR), yet all decode Main10 + auto-switch, exactly as on the WGC path.
fn out_format(&self) -> (DXGI_FORMAT, PixelFormat) {
if self.display_hdr {
(DXGI_FORMAT_R10G10B10A2_UNORM, PixelFormat::Rgb10a2)
} else {
(DXGI_FORMAT_B8G8R8A8_UNORM, PixelFormat::Bgra)
}
}
/// The ring (shared-texture) format, matched to the display's composition format: FP16 when the
/// display is HDR, BGRA when SDR.
fn ring_format(&self) -> DXGI_FORMAT {
if self.display_hdr {
DXGI_FORMAT_R16G16B16A16_FLOAT
} else {
DXGI_FORMAT_B8G8R8A8_UNORM
}
}
/// Update the client's 10-bit capability (the reuse path). Only affects whether a fresh `open`
/// proactively enables advanced color; the per-frame conversion follows the display, not the client.
fn set_client_10bit(&mut self, client_10bit: bool) {
self.client_10bit = client_10bit;
}
/// Recreate the ring at the format for `new_display_hdr` (the user flipped "Use HDR"). Bumps the
/// generation so the driver re-attaches ([`is_stale`]) to the new-format textures; clears the
/// header's `latest` so we don't consume a stale slot from the old ring; drops the conversion
/// textures so they rebuild at the new format.
fn recreate_ring(&mut self, new_display_hdr: bool) -> Result<()> {
self.display_hdr = new_display_hdr;
let fmt = self.ring_format();
let new_gen = IDD_GENERATION.fetch_add(1, Ordering::Relaxed);
let new_slots = unsafe {
Self::create_ring_slots(
&self.device,
self.target_id,
new_gen,
self.width,
self.height,
fmt,
)?
};
unsafe {
// Clear `latest` to the 0 sentinel (generation 0, which try_consume rejects). The real guard
// against consuming an unwritten new-ring slot is the generation tag in `latest`: a stale
// old-ring publish racing this recreate carries the OLD generation and is rejected. We wait
// for the driver's first NEW-generation publish.
(*(std::ptr::addr_of!((*self.header).latest) as *const AtomicU64))
.store(0, Ordering::Relaxed);
(*self.header).dxgi_format = fmt.0 as u32;
// Publish the new generation LAST (Release): when the driver observes it (Acquire) the new
// textures already exist and the format is already updated.
std::sync::atomic::fence(Ordering::Release);
(*(std::ptr::addr_of!((*self.header).generation) as *const AtomicU32))
.store(new_gen, Ordering::Release);
}
self.slots = new_slots; // drops the old slots → closes their shared handles + SRVs
self.generation = new_gen;
self.last_seq = 0;
self.out_ring.clear(); // the output format changed → rebuild lazily at the new format
self.out_idx = 0;
self.last_present = None;
Ok(())
}
/// Throttled poll of the display's live HDR state; recreate the ring if the user flipped "Use HDR".
/// Called from the capture loop (incl. while frozen on a format mismatch) so a toggle recovers within
/// a poll interval.
fn poll_display_hdr(&mut self) {
if self.last_acm_poll.elapsed() < Duration::from_millis(250) {
return;
}
self.last_acm_poll = Instant::now();
let now_hdr = unsafe { crate::vdisplay::sudovda::advanced_color_enabled(self.target_id) };
if now_hdr == self.display_hdr {
return;
}
tracing::info!(
target_id = self.target_id,
display_hdr = now_hdr,
client_10bit = self.client_10bit,
"IDD push: display HDR mode flipped — recreating the ring at the new format"
);
if let Err(e) = self.recreate_ring(now_hdr) {
tracing::warn!(error = %format!("{e:#}"), "IDD push: ring recreate failed");
}
}
/// Build the host-owned output ring (`OUT_RING` textures at [`Self::out_format`] + RTVs) if not yet
/// built. Rotated per frame so the in-flight encode of N and the convert/copy of N+1 touch different
/// textures. Rebuilt (cleared) when the display-mode flip changes the output format.
fn ensure_out_ring(&mut self) -> Result<()> {
if !self.out_ring.is_empty() {
return Ok(());
}
let (format, _) = self.out_format();
let desc = D3D11_TEXTURE2D_DESC {
Width: self.width,
Height: self.height,
MipLevels: 1,
ArraySize: 1,
Format: format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
CPUAccessFlags: 0,
MiscFlags: 0,
};
for _ in 0..OUT_RING {
let mut t: Option<ID3D11Texture2D> = None;
let mut rtv: Option<ID3D11RenderTargetView> = None;
unsafe {
self.device
.CreateTexture2D(&desc, None, Some(&mut t))
.context("CreateTexture2D(IDD out ring)")?;
let t = t.context("null out-ring texture")?;
self.device
.CreateRenderTargetView(&t, None, Some(&mut rtv))
.context("CreateRenderTargetView(IDD out ring)")?;
self.out_ring.push((t, rtv.context("null out-ring rtv")?));
}
}
Ok(())
}
/// Build the HDR converter if not already built (HDR-display path only — an SDR display is a copy).
fn ensure_converter(&mut self) -> Result<()> {
if self.hdr_conv.is_none() {
self.hdr_conv = Some(unsafe { HdrConverter::new(&self.device)? });
}
Ok(())
}
fn try_consume(&mut self) -> Result<Option<CapturedFrame>> {
self.log_driver_status_once();
// Follow the display: a "Use HDR" flip recreates the ring at the matching format.
self.poll_display_hdr();
let latest = self.latest();
// `latest` = (generation << 40) | (seq << 8) | slot. Reject any publish whose generation isn't
// our CURRENT ring (a stale old-ring publish racing a recreate, or the 0 sentinel we reset to) so
// we never consume an unwritten new-ring slot — eliminating the toggle-time garbage frame.
if (latest >> 40) as u32 != self.generation {
return Ok(None);
}
let seq = (latest >> 8) & 0xFFFF_FFFF;
let slot = (latest & 0xff) as usize;
if seq == self.last_seq || slot >= self.slots.len() {
return Ok(None);
}
self.ensure_out_ring()?;
// Build the HDR converter BEFORE acquiring the slot so nothing between Acquire and Release can
// `?`-return and leak the keyed-mutex lock (which would stall the driver on that slot).
if self.display_hdr {
self.ensure_converter()?;
}
let i = self.out_idx;
let (out, out_rtv) = {
let (t, rtv) = &self.out_ring[i];
(t.clone(), rtv.clone())
};
let (_, pf) = self.out_format();
// Hold the slot's keyed mutex only across the convert/copy into the host out-ring (NOT across the
// ~3 ms encode — NVENC reads the host out-ring slot, not the keyed-mutex slot), so the driver gets
// the slot back immediately and the encode of the PREVIOUS frame overlaps this convert.
let s = &self.slots[slot];
if unsafe { s.mutex.AcquireSync(0, 8) }.is_err() {
return Ok(None);
}
unsafe {
if self.display_hdr {
// Sample the FP16 slot's SRV directly (no scratch copy) → BT.2020 PQ Rgb10a2.
if let Some(conv) = self.hdr_conv.as_ref() {
conv.convert(&self.context, &s.srv, &out_rtv, self.width, self.height);
}
} else {
// SDR: the slot is already 8-bit BGRA — one copy into the out-ring (hidden by pipelining).
self.context.CopyResource(&out, &s.tex);
}
let _ = s.mutex.ReleaseSync(0);
}
self.out_idx = (i + 1) % self.out_ring.len();
self.last_seq = seq;
self.last_present = Some((out.clone(), pf));
Ok(Some(CapturedFrame {
width: self.width,
height: self.height,
pts_ns: now_ns(),
format: pf,
payload: FramePayload::D3d11(D3d11Frame {
texture: out,
device: self.device.clone(),
}),
}))
}
fn repeat_last(&self) -> Option<CapturedFrame> {
self.last_present.as_ref().map(|(tex, pf)| CapturedFrame {
width: self.width,
height: self.height,
pts_ns: now_ns(),
format: *pf,
payload: FramePayload::D3d11(D3d11Frame {
texture: tex.clone(),
device: self.device.clone(),
}),
})
}
}
/// Diagnostic observer (O3.1): create the IDD-push ring + debug block as the SYSTEM host (LocalSystem
/// — proper privileges, the gamepad pattern) ALONGSIDE the normal WGC path, which provides the
/// presentation trigger. Logs whether the driver's `run_core` ran and pushed frames into a
/// host-created ring — resolving the `run_core=0` ambiguity (a user-created ring may be unwritable by
/// the driver). Gated by `PUNKTFUNK_IDD_PUSH_OBSERVE`; spawns a short-lived sampling thread.
pub fn spawn_observer(target: WinCaptureTarget, preferred: Option<(u32, u32, u32)>) {
std::thread::spawn(move || {
let tid = target.target_id;
tracing::info!(
target_id = tid,
"IDD push OBSERVER: creating host ring (LocalSystem) + debug block alongside WGC"
);
match IddPushCapturer::open(target, preferred, false, Box::new(())) {
Ok(mut cap) => {
let mut frames = 0u32;
for _ in 0..40 {
match cap.try_consume() {
Ok(Some(_)) => frames += 1,
Ok(None) => {}
Err(e) => tracing::warn!("IDD push OBSERVER: consume error: {e:#}"),
}
std::thread::sleep(Duration::from_millis(750));
}
tracing::info!(
target_id = tid,
frames_from_ring = frames,
"IDD push OBSERVER: sampling done"
);
cap.log_debug_block();
}
Err(e) => tracing::warn!(
target_id = tid,
"IDD push OBSERVER: ring open failed: {e:#}"
),
}
});
}
/// The discrete render GPU LUID (where NVENC runs), falling back to the monitor's `OsAdapterLuid`.
fn resolve_render_adapter_luid_or(fallback_packed: i64) -> LUID {
if let Some(l) = unsafe { crate::vdisplay::sudovda::resolve_render_adapter_luid() } {
return l;
}
LUID {
LowPart: (fallback_packed & 0xffff_ffff) as u32,
HighPart: (fallback_packed >> 32) as i32,
}
}
impl Capturer for IddPushCapturer {
fn next_frame(&mut self) -> Result<CapturedFrame> {
let deadline = Instant::now() + Duration::from_secs(20);
loop {
let _ = unsafe { WaitForSingleObject(self.event, 16) };
if let Some(f) = self.try_consume()? {
return Ok(f);
}
if let Some(f) = self.repeat_last() {
return Ok(f);
}
if Instant::now() > deadline {
self.log_debug_block();
let (st, detail, lo, hi) = unsafe {
(
(*self.header).driver_status,
(*self.header).driver_status_detail,
(*self.header).driver_render_luid_low,
(*self.header).driver_render_luid_high,
)
};
bail!(
"no IDD-push frame within 20s (target {}) — driver_status={st} detail=0x{detail:08x} \
driver_render_luid={hi:08x}:{lo:08x}. 0=driver never attached (swap-chain not \
assigned / driver not active), 1=attached but no frames (idle desktop?), 2=driver \
couldn't open our textures (render-adapter mismatch).",
self.target_id
);
}
}
}
fn try_latest(&mut self) -> Result<Option<CapturedFrame>> {
self.try_consume()
}
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
// While the display is HDR we emit BT.2020 PQ (Rgb10a2) → the encoder forces HEVC Main10 + the
// PQ VUI; pair that with a mastering-display SEI so any decoder tone-maps from a real grade. The
// driver doesn't (yet) forward the OS's IDDCX_HDR10_METADATA, so use the generic HDR10 baseline
// (the same metadata the native HDR path sends on the 0xCE datagram).
self.display_hdr.then(crate::hdr::generic_hdr10)
}
fn pipeline_depth(&self) -> usize {
// 2 = one frame deferred: submit N+1 (capture + convert/copy into a fresh out-ring texture) while
// NVENC encodes N on the ASIC. We hand a rotating `OUT_RING` of output textures, so this is safe.
// `PUNKTFUNK_IDD_DEPTH` overrides (1 disables pipelining; clamp to ≤ OUT_RING so a frame in flight
// always has its own texture).
std::env::var("PUNKTFUNK_IDD_DEPTH")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(2)
.clamp(1, OUT_RING)
}
}
impl Drop for IddPushCapturer {
fn drop(&mut self) {
self.slots.clear();
unsafe {
if !self.dbg_block.is_null() {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: self.dbg_block.cast(),
});
}
if !self.dbg_map.is_invalid() {
let _ = CloseHandle(self.dbg_map);
}
if !self.header.is_null() {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: self.header.cast(),
});
}
let _ = CloseHandle(self.event);
let _ = CloseHandle(self.map);
}
// _keepalive drops after, REMOVEing the virtual display.
}
}
@@ -278,6 +278,13 @@ unsafe fn spawn_inner(cmdline: &str, w: u32, h: u32, hz: u32) -> Result<HelperRe
}
tracing::info!(pid = pi.dwProcessId, mode = %format!("{w}x{h}@{hz}"), "WGC helper spawned");
// The helper does the WGC capture + NVENC encode, but it runs under the user's UAC-FILTERED token
// (no SE_INC_BASE_PRIORITY), so it can't raise its OWN GPU scheduling-priority class — under a
// GPU-saturating game NVENC then gets starved (the "240→40 fps in-game collapse"). The SYSTEM host
// holds the privilege, so stamp the HIGH GPU priority class onto the child here, right after spawn
// (the process-level class applies to the GPU contexts the helper creates afterwards).
crate::capture::dxgi::set_child_gpu_priority_class(pi.hProcess);
// stderr → host tracing, line by line.
let err_handle = HandleReader(err_r);
std::thread::Builder::new()
@@ -127,8 +127,15 @@ fn run(
refresh_hz: cfg.fps,
})
.context("create virtual output at client resolution")?;
// `want_hdr=false`: the IDD-push backend (opt-in PUNKTFUNK_IDD_PUSH) has no monitor-HDR
// auto-detection — it converts its always-FP16 ring per this flag — and GameStream HDR is not
// negotiated into StreamConfig here, so an IDD-push GameStream session streams SDR even on an
// HDR desktop. (The default WGC backend DOES auto-detect HDR from the output colorspace, but
// IDD-push bypasses WGC.) Acceptable for the experimental IDD-push A/B path; HDR over IDD-push
// is wired only for punktfunk/1 (want_hdr = negotiated bit_depth >= 10). TODO: derive want_hdr
// from a GameStream HDR flag once StreamConfig carries one.
let mut capturer =
capture::capture_virtual_output(vout).context("capture virtual output")?;
capture::capture_virtual_output(vout, false).context("capture virtual output")?;
capturer.set_active(true);
return stream_body(&mut *capturer, &sock, cfg, running, force_idr, rfi_range);
}
+126 -9
View File
@@ -2149,6 +2149,22 @@ fn session_watcher_loop(tx: std::sync::mpsc::Sender<SessionSwitch>, stop: Arc<At
/// keepalive, the virtual output) while the data-plane `session` continues untouched —
/// the rebuilt encoder opens with an IDR + in-band parameter sets. `probe_rx`/`probe_result_tx`
/// carry speed-test bursts (see [`service_probes`]).
/// The stop flag of the current in-process IDD-push session, so a NEW connection can PREEMPT it.
/// A fresh connection means the prior client is gone (a reconnect) and a reused IddCx monitor's
/// swap-chain is dead — so we stop the prior session (it releases its monitor cleanly while frames
/// still flow), then build a fresh one, instead of joining a dying session or tearing its monitor out
/// from under it (which churns the driver's ADD/REMOVE path and wedges it under rapid reconnects).
#[cfg(target_os = "windows")]
static IDD_SESSION_STOP: std::sync::Mutex<Option<Arc<AtomicBool>>> = std::sync::Mutex::new(None);
/// Serializes IDD-push session SETUP (preempt + monitor create + first frame). Held across setup,
/// released before the encode loop — so a reconnect FLOOD can never run concurrent monitor
/// create/teardown (the churn that fails the ADD IOCTL and wedges the driver). Each session finishes
/// setup before the next acquires this and preempts it, by which point the preempted session is in its
/// encode loop and releases its monitor promptly.
#[cfg(target_os = "windows")]
static IDD_SETUP_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[allow(clippy::too_many_arguments)]
fn virtual_stream(
session: Session,
@@ -2197,9 +2213,30 @@ fn virtual_stream(
bit_depth,
"punktfunk/1 virtual display"
);
// IDD-push reconnect preempt: a fresh connection means the prior client is gone. Hold IDD_SETUP_LOCK
// across the preempt + pipeline build so a reconnect FLOOD can't run concurrent monitor
// create/teardown. Then STOP the prior session (it ends cleanly while its monitor still composites
// frames) and WAIT for it to release its monitor, before building a FRESH one — instead of the
// driver-churning teardown of a monitor under a still-live session. Register THIS session's stop so
// the next reconnect preempts it.
#[cfg(target_os = "windows")]
let idd_setup_guard = std::env::var_os("PUNKTFUNK_IDD_PUSH")
.is_some()
.then(|| IDD_SETUP_LOCK.lock().unwrap());
#[cfg(target_os = "windows")]
if std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() {
let prev = IDD_SESSION_STOP.lock().unwrap().replace(stop.clone());
if let Some(prev_stop) = prev {
prev_stop.store(true, Ordering::SeqCst);
crate::vdisplay::sudovda::wait_for_monitor_released(std::time::Duration::from_secs(3));
}
}
let mut vd = crate::vdisplay::open(compositor)?;
let (mut capturer, mut enc, mut frame, mut interval) =
build_pipeline_with_retry(&mut vd, mode, bitrate_kbps, bit_depth)?;
// Setup done — release the IDD-push setup lock so the next reconnect can begin (and preempt us).
#[cfg(target_os = "windows")]
drop(idd_setup_guard);
// Windows single-process DDA path (PUNKTFUNK_NO_WGC=1): the SudoVDA virtual display, isolated as the
// SOLE active output, goes into fullscreen independent-flip (one plane on one display) which Desktop
@@ -2276,6 +2313,17 @@ fn virtual_stream(
let mut capture_rebuilds: u32 = 0;
// Last HDR mastering metadata we forwarded — re-sent as 0xCE on change/keyframe (see below).
let mut last_hdr_meta: Option<punktfunk_core::quic::HdrMeta> = None;
// Frames submitted to NVENC but not yet polled (capture_ns, pacing deadline). With a capturer that
// hands a fresh output texture per frame, the loop submits N+1 before polling N (pipeline depth > 1),
// overlapping the convert/copy of N+1 on the 3D engine with the encode of N on the NVENC ASIC.
let mut inflight: std::collections::VecDeque<(u64, std::time::Instant)> =
std::collections::VecDeque::new();
// Diagnostic: distinguish NEW captured frames (the source produced a fresh frame) from REPEATS (the
// loop re-encoded the last frame because `try_latest` had nothing). A low new-frame rate at a high
// send rate ⇒ the capture source isn't producing frames (e.g. an IDD virtual display DWM isn't
// compositing), NOT an encoder problem. Logged every 2 s when `PUNKTFUNK_PERF`.
let (mut diag_new, mut diag_repeat) = (0u64, 0u64);
let mut diag_at = std::time::Instant::now();
while !stop.load(Ordering::SeqCst) && std::time::Instant::now() < deadline {
// Mid-stream session switch (the box flipped Gaming↔Desktop): rebuild the WHOLE backend in
// place — a different compositor at the SAME client mode — keeping the Session + send thread
@@ -2384,9 +2432,10 @@ fn virtual_stream(
match capturer.try_latest() {
Ok(Some(f)) => {
frame = f;
diag_new += 1;
capture_rebuilds = 0; // a delivered frame clears the consecutive-loss counter
}
Ok(None) => {} // no new frame (static desktop / mid-rebuild) — repeat the last frame
Ok(None) => diag_repeat += 1, // no new frame (static desktop / mid-rebuild) — repeat the last
// The capture source died (PipeWire/compositor thread ended, virtual output gone). Rather
// than tear the whole session down — the client has no reconnect path and would have to
// cold-restart the handshake — rebuild the pipeline IN PLACE at the current mode, exactly
@@ -2411,6 +2460,18 @@ fn virtual_stream(
next = std::time::Instant::now();
}
}
if perf && diag_at.elapsed() >= std::time::Duration::from_secs(2) {
let secs = diag_at.elapsed().as_secs_f64();
tracing::info!(
new_fps = format!("{:.0}", diag_new as f64 / secs),
repeat_fps = format!("{:.0}", diag_repeat as f64 / secs),
"capture diag: NEW frames from the source vs REPEATS (low new_fps at high send rate ⇒ \
the source isn't producing frames, not an encode stall)"
);
diag_new = 0;
diag_repeat = 0;
diag_at = std::time::Instant::now();
}
// The source's static HDR mastering metadata (Windows GetDesc1; None on Linux/SDR) is the
// single source of truth: hand it to the encoder (in-band SEI on keyframes) and, when it
// changes, to the client (0xCE). Re-sent on each keyframe below so a dropped best-effort
@@ -2421,13 +2482,26 @@ fn virtual_stream(
if resend_meta {
last_hdr_meta = hdr_meta;
}
// How deep to pipeline (1 = synchronous submit→poll, the original behaviour). The IDD-push
// capturer hands a rotating ring of output textures, so it returns >1; other capturers default 1.
let depth = capturer.pipeline_depth().max(1);
let capture_ns = now_ns();
enc.submit(&frame).context("encoder submit")?;
// The deadline for this frame's packets (the next frame's due time); the send thread paces
// up to here so a high-bitrate frame spreads over the interval instead of bursting.
// This frame's pacing deadline (the next frame's due time); the send thread spreads a big frame
// up to here. Each in-flight frame carries its own (capture_ns, deadline) for when it's polled.
next += interval;
inflight.push_back((capture_ns, next));
// Drain the OLDEST in-flight frames, keeping at most depth-1 deferred. At depth 1 this polls
// immediately after every submit (synchronous); at depth 2 it polls N right after submitting N+1,
// so the encode of N overlaps the convert/copy of N+1. NVENC's `pending` is FIFO, so poll() returns
// the oldest submitted frame's AU — matching `inflight.pop_front()`.
let mut send_gone = false;
while let Some(au) = enc.poll().context("encoder poll")? {
while inflight.len() >= depth {
let au = match enc.poll().context("encoder poll")? {
Some(au) => au,
None => break, // no AU ready for a submitted frame (shouldn't happen — poll blocks)
};
let (cap_ns, deadline) = inflight.pop_front().expect("inflight non-empty");
let flags = if au.keyframe {
(FLAG_PIC | FLAG_SOF) as u32
} else {
@@ -2442,12 +2516,12 @@ fn virtual_stream(
resend_meta = false;
}
}
let encode_us = (now_ns().saturating_sub(capture_ns) / 1000) as u32;
let encode_us = (now_ns().saturating_sub(cap_ns) / 1000) as u32;
let msg = FrameMsg {
data: au.data,
capture_ns,
capture_ns: cap_ns,
flags,
deadline: next,
deadline,
encode_us,
};
// Hand to the send thread; this blocks (backpressure) if it's behind. An Err means it
@@ -2466,6 +2540,28 @@ fn virtual_stream(
None => next = std::time::Instant::now(),
}
}
// Drain the in-flight tail (the depth-1 frames submitted but not yet polled) so the last frames still
// reach the client instead of being dropped on the way out.
while let Some((cap_ns, deadline)) = inflight.pop_front() {
let Ok(Some(au)) = enc.poll() else { break };
let flags = if au.keyframe {
(FLAG_PIC | FLAG_SOF) as u32
} else {
FLAG_PIC as u32
};
let encode_us = (now_ns().saturating_sub(cap_ns) / 1000) as u32;
let msg = FrameMsg {
data: au.data,
capture_ns: cap_ns,
flags,
deadline,
encode_us,
};
if frame_tx.send(msg).is_err() {
break;
}
sent += 1;
}
// Signal the send thread to drain + exit (drop the channel), then join it.
drop(frame_tx);
let _ = send_thread.join();
@@ -2484,6 +2580,14 @@ fn should_use_helper() -> bool {
if std::env::var_os("PUNKTFUNK_NO_HELPER").is_some() || crate::capture::wgc_disabled() {
return false;
}
// IDD direct-push captures IN-PROCESS in Session 0: the pf-vdisplay driver delivers frames to the
// SYSTEM host's session via shared memory and NVENC is headless, so no user-session WGC helper is
// needed for VIDEO (and a Session-1 helper couldn't open the Session-0 shared textures anyway).
// NOTE: input injection (SendInput) from Session 0 can't reach the user's Session-1 desktop yet —
// a known follow-up; this path validates the video transport. See docs/windows-virtual-display-rust-port.md.
if std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() {
return false;
}
std::env::var_os("PUNKTFUNK_FORCE_HELPER").is_some()
|| crate::capture::wgc_relay::running_as_system()
}
@@ -2576,6 +2680,15 @@ fn virtual_stream_relay(
let (mut _keepalive, mut relay, mut target, mut effective_hz) = build(&mut vd, mode)?;
let mut cur_mode = mode;
// O3.1: optionally observe the IDD-push ring alongside WGC (WGC = the presentation trigger) to
// confirm the 0257 driver pushes frames into a HOST-created ring. Diagnostic only; gated.
if std::env::var_os("PUNKTFUNK_IDD_PUSH_OBSERVE").is_some() {
crate::capture::idd_push::spawn_observer(
target.clone(),
Some((cur_mode.width, cur_mode.height, effective_hz)),
);
}
// The host's own DDA capturer+encoder for the SECURE (Winlogon) desktop, which WGC — and thus the
// helper — cannot capture. Opened lazily on the first secure transition (so a session that never
// hits a UAC/lock screen never pays for a second NVENC session), then kept for fast re-switch.
@@ -3014,8 +3127,12 @@ fn build_pipeline(
"compositor did not honor the requested refresh — encoding at the achieved rate"
);
}
let mut capturer =
crate::capture::capture_virtual_output(vout).context("capture virtual output")?;
// HDR vs SDR for the IDD-push conversion: a negotiated 10-bit session (client advertised
// VIDEO_CAP_10BIT + host opted in via PUNKTFUNK_10BIT) is our HDR path → BT.2020 PQ Rgb10a2;
// otherwise the FP16 IDD frames are converted to 8-bit SDR. (Ignored by non-IDD-push backends,
// which auto-detect HDR from the monitor state.)
let mut capturer = crate::capture::capture_virtual_output(vout, bit_depth >= 10)
.context("capture virtual output")?;
capturer.set_active(true);
let frame = capturer.next_frame().context("first frame")?;
// `bit_depth` is the handshake-negotiated value (8, or 10 = HEVC Main10 when the client
+1 -1
View File
@@ -76,7 +76,7 @@ pub fn run(opts: Options) -> Result<()> {
refresh_hz: opts.fps,
})
.context("create virtual output")?;
capture::capture_virtual_output(vout).context("capture virtual output")?
capture::capture_virtual_output(vout, false).context("capture virtual output")?
}
};
+161 -13
View File
@@ -9,8 +9,25 @@
use std::ffi::c_void;
use std::mem::size_of;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, Once};
/// Monotonic monitor generation. Each [`create_monitor`] stamps the next value onto the [`Monitor`]
/// and its [`MonitorLease`]s, so a lease whose monitor was already torn down + recreated (the IDD-push
/// reconnect-preempt path) is ignored on drop instead of decrementing the NEW monitor's refcount.
static MON_GEN: AtomicU64 = AtomicU64::new(1);
/// The gen of the CURRENTLY-active monitor. A session capturer captures this at open and re-checks it
/// each frame; when it changes (a reconnect preempted + recreated the monitor), the old session bails
/// IMMEDIATELY instead of lingering on the dead ring's 20s frame deadline — which would otherwise hold
/// its NVENC encoder open and exhaust the GPU's encode-session limit under rapid reconnects.
pub(crate) static CURRENT_MON_GEN: AtomicU64 = AtomicU64::new(0);
/// IDD-push mode: a new client connection preempts + recreates the monitor (single-client reconnect),
/// because a REUSED IddCx monitor's swap-chain is dead. Off → monitors are shared across sessions.
fn idd_push_mode() -> bool {
std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some()
}
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
@@ -27,7 +44,8 @@ use windows::Win32::Devices::Display::{
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE,
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO,
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME,
QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY, SDC_USE_SUPPLIED_DISPLAY_CONFIG,
QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY, SDC_FORCE_MODE_ENUMERATION,
SDC_SAVE_TO_DATABASE, SDC_USE_SUPPLIED_DISPLAY_CONFIG,
};
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
use windows::Win32::Graphics::Gdi::{
@@ -53,6 +71,9 @@ const IOCTL_ADD: u32 = ctl(0x800);
const IOCTL_REMOVE: u32 = ctl(0x801);
const IOCTL_SET_RENDER_ADAPTER: u32 = ctl(0x802); // == 0x0022_2008
const IOCTL_GET_WATCHDOG: u32 = ctl(0x803);
/// pf-vdisplay extension (NOT in SudoVDA): tear down every virtual monitor. Sent once on host startup
/// to reap monitors orphaned by a crashed/killed previous host. SudoVDA returns invalid (ignored).
const IOCTL_CLEAR_ALL: u32 = ctl(0x804);
const IOCTL_DRIVER_PING: u32 = ctl(0x888);
const IOCTL_GET_VERSION: u32 = ctl(0x8FF);
@@ -116,7 +137,9 @@ unsafe fn set_render_adapter(h: HANDLE, luid: LUID) -> Result<()> {
/// Desktop Duplication (e.g. the RTX 4090). Default: the discrete adapter with the most
/// `DedicatedVideoMemory`, skipping WARP / Basic-Render and the SudoVDA software adapter (≈0 VRAM).
/// `PUNKTFUNK_RENDER_ADAPTER=<substring>` forces a match by Description (Apollo's `adapter_name`).
unsafe fn resolve_render_adapter_luid() -> Option<LUID> {
/// `pub(crate)` so the IDD direct-push capturer can create its shared textures on the same discrete
/// GPU it pins here (and where NVENC runs).
pub(crate) unsafe fn resolve_render_adapter_luid() -> Option<LUID> {
use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIFactory1};
let want = std::env::var("PUNKTFUNK_RENDER_ADAPTER")
.ok()
@@ -494,13 +517,32 @@ unsafe fn isolate_displays_ccd(keep_target_id: u32) -> Option<SavedConfig> {
}
}
if others == 0 {
tracing::info!("display isolate (CCD): SudoVDA target {keep_target_id} already the only active display");
// The virtual path shows active in the CCD database (from set_active_mode's legacy
// ChangeDisplaySettingsExW), but a legacy mode-set does NOT drive the IddCx adapter's
// EVT_IDD_CX_ADAPTER_COMMIT_MODES — and without COMMIT_MODES the OS never calls
// ASSIGN_SWAPCHAIN, so the driver never receives composed frames. Force an explicit CCD
// SetDisplayConfig commit of the (sole) virtual path so the IddCx path actually activates.
// SDC_FORCE_MODE_ENUMERATION makes the OS re-enumerate + re-commit even though the CCD DB
// already lists the path active.
let rc = SetDisplayConfig(
Some(paths.as_slice()),
Some(modes.as_slice()),
SDC_APPLY
| SDC_USE_SUPPLIED_DISPLAY_CONFIG
| SDC_ALLOW_CHANGES
| SDC_SAVE_TO_DATABASE
| SDC_FORCE_MODE_ENUMERATION,
);
tracing::info!("display isolate (CCD): forced CCD re-commit of sole virtual path {keep_target_id} rc={rc:#x} (drives IddCx COMMIT_MODES → ASSIGN_SWAPCHAIN)");
return Some(saved);
}
let rc = SetDisplayConfig(
Some(paths.as_slice()),
Some(modes.as_slice()),
SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES,
SDC_APPLY
| SDC_USE_SUPPLIED_DISPLAY_CONFIG
| SDC_ALLOW_CHANGES
| SDC_FORCE_MODE_ENUMERATION,
);
if rc == 0 {
tracing::info!("display isolate (CCD): deactivated {others} other display(s) — SudoVDA target {keep_target_id} is now the sole desktop");
@@ -584,6 +626,8 @@ struct Monitor {
stop: Arc<AtomicBool>,
pinger: Option<JoinHandle<()>>,
ccd_saved: Option<SavedConfig>,
/// Generation stamp ([`MON_GEN`]); a [`MonitorLease`] only releases if its gen still matches.
gen: u64,
}
enum MgrState {
@@ -667,6 +711,14 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
// PUNKTFUNK_RENDER_ADAPTER=<name substring> only on a box that genuinely needs steering.
let pinned = if std::env::var("PUNKTFUNK_RENDER_ADAPTER").is_ok() {
unsafe { resolve_render_adapter_luid() }
} else if std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() {
// P2 direct frame push: the host opens the driver's shared textures AND runs NVENC on the
// RENDER adapter, so on a hybrid box (4090 + iGPU) it MUST be the discrete encoder GPU —
// an iGPU-rendered surface is untouchable by NVENC. pf-vdisplay HONORS SET_RENDER_ADAPTER
// (SudoVDA ignored it), so pin the discrete GPU. The driver also reports the resulting
// render LUID in the shared header, so the host binds correctly even if this is overridden.
tracing::info!("IDD push: pinning the discrete render GPU (SET_RENDER_ADAPTER)");
unsafe { resolve_render_adapter_luid() }
} else {
tracing::info!(
"SudoVDA SET_RENDER_ADAPTER skipped (Apollo-parity: no render pin — avoids cross-GPU \
@@ -722,10 +774,22 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
let stop_t = stop.clone();
let pinger = thread::spawn(move || {
let h = HANDLE(device_raw as *mut c_void);
let mut warned = false;
while !stop_t.load(Ordering::Relaxed) {
let mut none: [u8; 0] = [];
unsafe {
let _ = ioctl(h, IOCTL_DRIVER_PING, &[], &mut none);
match unsafe { ioctl(h, IOCTL_DRIVER_PING, &[], &mut none) } {
Ok(_) => warned = false,
// A persistently failing PING means the cached control handle went invalid — the
// driver watchdog will then tear the monitor down mid-session. Surface it once
// (the old `let _ =` swallowed it, which masked exactly this during the bad-state churn).
Err(e) => {
if !warned {
tracing::warn!(
"SudoVDA keepalive PING failed (control handle lost?): {e:#}"
);
warned = true;
}
}
}
thread::sleep(interval);
}
@@ -783,6 +847,7 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
stop,
pinger: Some(pinger),
ccd_saved,
gen: MON_GEN.fetch_add(1, Ordering::Relaxed),
})
}
}
@@ -848,6 +913,16 @@ fn mgr_ensure_device(g: &mut Mgr) -> Result<isize> {
3
};
tracing::info!("SudoVDA watchdog timeout {}s", g.watchdog_s);
// Reap monitors orphaned by a crashed/killed previous host instance before we create ours.
// pf-vdisplay honors IOCTL_CLEAR_ALL; SudoVDA returns invalid (ignored). Without it an orphan
// lingers until the driver watchdog fires — but a still-pinging new session keeps resetting that
// watchdog, so orphans could accumulate (the "5-6 stale monitors that never tear down" failure).
{
let mut none: [u8; 0] = [];
if unsafe { ioctl(device, IOCTL_CLEAR_ALL, &[], &mut none) }.is_ok() {
tracing::info!("cleared orphaned virtual monitors on host startup");
}
}
let raw = device.0 as isize;
g.device = Some(raw);
Ok(raw)
@@ -871,6 +946,39 @@ fn mgr_acquire(mode: Mode) -> Result<VirtualOutput> {
let device = mgr_ensure_device(&mut g)?;
let watchdog_s = g.watchdog_s;
// IDD-push: a new connection while a monitor is live = a single-client RECONNECT (the prior client
// is gone — IDD-push is one display, no concurrency). A REUSED IddCx monitor's swap-chain is DEAD,
// so joining it would hand the new client a black screen until the old session times out. PREEMPT:
// tear the old monitor down (its Drop restores topology + IOCTL_REMOVEs) and fall through to create
// a FRESH one. The old session's lease is gen-stamped, so its later drop is ignored (mgr_release
// no-op) and can't tear down the new monitor.
if idd_push_mode()
&& matches!(
g.state,
MgrState::Active { .. } | MgrState::Lingering { .. }
)
{
if let MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } =
std::mem::replace(&mut g.state, MgrState::Idle)
{
tracing::info!(
old_target = mon.target_id,
"IDD-push reconnect — preempting the prior session, recreating a fresh monitor"
);
// teardown() — NOT drop() — sends IOCTL_REMOVE (and restores topology). `Monitor` has NO
// `Drop` impl, so a bare `drop(mon)` orphaned the IddCx monitor in the driver: it was never
// departed, so it kept a live D3D device + a stuck swap-chain processor thread, and these
// accumulated every reconnect (the driver-side churn leak: +1 device, ~36 nvwgf2umx threads,
// ~50 MB VRAM per session, until it choked). teardown frees it via the driver's do_remove.
unsafe { mon.teardown(device) };
// Let the OS finish the ASYNC IddCx monitor departure before the next ADD. A back-to-back
// REMOVE→ADD races the teardown and the ADD IOCTL is rejected (`DeviceIoControl failed`)
// under reconnect churn. Held under the MGR lock, but IDD-push setup is already serialized
// (IDD_SETUP_LOCK), so this only paces the recreate — exactly what a reconnect flood needs.
thread::sleep(Duration::from_millis(400));
}
}
// A live monitor already exists — join it (refcount++). This covers a concurrent session AND the
// build-then-drop overlap of a mid-stream Reconfigure / secure-return (the new lease is taken while
// the old is still held). If the requested mode differs, reconfigure the shared monitor to it so a
@@ -889,11 +997,13 @@ fn mgr_acquire(mode: Mode) -> Result<VirtualOutput> {
);
let pm = Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz));
let target = mon.target();
let gen = mon.gen;
CURRENT_MON_GEN.store(gen, Ordering::Relaxed);
return Ok(VirtualOutput {
node_id: 0,
preferred_mode: pm,
win_capture: target,
keepalive: Box::new(MonitorLease),
keepalive: Box::new(MonitorLease { gen }),
});
}
@@ -914,12 +1024,14 @@ fn mgr_acquire(mode: Mode) -> Result<VirtualOutput> {
};
let pm = Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz));
let target = mon.target();
let gen = mon.gen;
CURRENT_MON_GEN.store(gen, Ordering::Relaxed);
g.state = MgrState::Active { mon, refs: 1 };
Ok(VirtualOutput {
node_id: 0,
preferred_mode: pm,
win_capture: target,
keepalive: Box::new(MonitorLease),
keepalive: Box::new(MonitorLease { gen }),
})
}
@@ -943,8 +1055,18 @@ unsafe fn mgr_reconfigure(mon: &mut Monitor, mode: Mode) {
}
/// Release a session's hold: refcount-- ; when the last session leaves, LINGER before teardown.
fn mgr_release() {
/// `gen` is the lease's monitor generation: a STALE lease (its monitor was already torn down +
/// recreated under it — the IDD-push reconnect-preempt path) does nothing, so it can't decrement the
/// CURRENT (fresh) monitor's refcount and tear it down.
fn mgr_release(gen: u64) {
let mut g = MGR.lock().unwrap();
let stale = match &g.state {
MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } => mon.gen != gen,
MgrState::Idle => true,
};
if stale {
return;
}
g.state = match std::mem::replace(&mut g.state, MgrState::Idle) {
MgrState::Active { mon, refs } if refs > 1 => MgrState::Active {
mon,
@@ -965,6 +1087,28 @@ fn mgr_release() {
};
}
/// Wait (up to `timeout`) for the active monitor to be RELEASED — i.e. the MGR is no longer `Active`
/// (the prior session dropped its lease → `Lingering`/`Idle`). Used by the IDD-push reconnect preempt:
/// after signalling the old session to stop, we wait here so it tears its monitor down CLEANLY (while
/// frames still flow) before we acquire a fresh one — instead of dropping the monitor out from under a
/// still-live session, which churns the driver's ADD/REMOVE path and wedges it under rapid reconnects.
pub(crate) fn wait_for_monitor_released(timeout: Duration) {
let deadline = Instant::now() + timeout;
loop {
if !matches!(MGR.lock().unwrap().state, MgrState::Active { .. }) {
return;
}
if Instant::now() >= deadline {
tracing::warn!(
"IDD-push preempt: prior session didn't release the monitor within {timeout:?} — \
proceeding (mgr_acquire will preempt it)"
);
return;
}
thread::sleep(Duration::from_millis(25));
}
}
/// Background timer (started once): tear down a monitor that has lingered past its deadline (→ Idle),
/// so a physical-screen user gets their screen back after they stop streaming.
fn ensure_linger_timer() {
@@ -989,11 +1133,15 @@ fn ensure_linger_timer() {
});
}
/// A session's lease on the shared monitor. Drop releases the refcount (→ linger when it hits 0).
struct MonitorLease;
/// A session's lease on the shared monitor. Drop releases the refcount (→ linger when it hits 0),
/// UNLESS the monitor was already torn down + recreated under it (gen mismatch — the IDD-push
/// reconnect-preempt path), in which case the drop is a no-op so it can't tear down the new monitor.
struct MonitorLease {
gen: u64,
}
impl Drop for MonitorLease {
fn drop(&mut self) {
mgr_release();
mgr_release(self.gen);
}
}
+178 -40
View File
@@ -63,6 +63,22 @@ pub fn run(opts: HelperOptions) -> Result<()> {
WgcCapturer::open(target, Some((opts.width, opts.height, opts.fps))).context("WGC open")?;
cap.set_active(true);
// O3 present-trigger experiment: spawn a thread that PRESENTS a D3D swapchain to the virtual
// display (a present SOURCE), testing whether that — unlike WGC's READ — makes the OS assign the
// driver's IddCx swap-chain (so the driver's run_core runs + can push). Gated; diagnostic.
if std::env::var_os("PUNKTFUNK_PRESENT_TRIGGER").is_some() {
let (w, h) = (opts.width, opts.height);
std::thread::Builder::new()
.name("pf-present-trigger".into())
.spawn(move || {
tracing::info!("present-trigger: starting D3D present loop on the virtual display");
if let Err(e) = unsafe { present_trigger(w, h) } {
tracing::warn!("present-trigger error: {e:#}");
}
})
.ok();
}
// First frame establishes the real dimensions + whether the desktop is HDR (the encoder derives
// Main10/HDR from the frame's PixelFormat::Rgb10a2). Then open NVENC on the capture device.
let first = cap.next_frame().context("first WGC frame")?;
@@ -107,47 +123,55 @@ pub fn run(opts: HelperOptions) -> Result<()> {
let stdout = std::io::stdout();
let mut out = stdout.lock();
// Encode pipeline depth. The loop keeps DEPTH frames in flight so per-frame GPU-scheduling waits
// can overlap. NOTE: depth > 1 was measured to REGRESS under a GPU-saturating game — the encodes
// serialize on the contended GPU anyway, so a deeper queue just stacks latency (≈ depth × frame
// time) without raising throughput. Default 1 (the validated-best); `PUNKTFUNK_ENCODE_DEPTH` (1..=6)
// can raise it if a future workload is genuinely encode-throughput-bound rather than scheduling-bound.
let depth: usize = std::env::var("PUNKTFUNK_ENCODE_DEPTH")
.ok()
.and_then(|s| s.trim().parse::<usize>().ok())
.filter(|&d| (1..=6).contains(&d))
.unwrap_or(1);
tracing::info!(depth, "WGC helper: encode pipeline depth");
// FIXED-CADENCE encode loop (mirrors the single-process `punktfunk1::virtual_stream` loop). The
// host runs as SYSTEM and relays our AUs; to deliver a STEADY `fps` to the client (the "fixed 240"
// goal) we must NOT gate on WGC's content-driven FrameArrived — `WgcCapturer::next_frame` blocks up
// to its ~8 ms static-repeat timeout when the desktop is quiet, capping a barely-changing desktop
// ~125 fps regardless of the GPU. Instead we pace to `1/fps` and take the FRESHEST frame with the
// non-blocking `try_latest`, repeating the last one when nothing newer arrived. Depth-1: NVENC's
// `poll` (lock_bitstream) blocks until the just-submitted frame is encoded, so exactly one frame is
// in flight per iteration. A deeper pipeline was measured to only stack latency under a
// GPU-saturating game (the encodes serialize on the contended GPU anyway) — the in-game lever is
// the GPU scheduling priority the SYSTEM host stamps on us, not pipeline depth.
let interval = std::time::Duration::from_secs_f64(1.0 / opts.fps.max(1) as f64);
let perf = std::env::var_os("PUNKTFUNK_PERF").is_some();
let mut frames = 0u64;
let mut cap_wait_ns = 0u64;
let mut encode_ns = 0u64; // time blocked in lock_bitstream (the oldest in-flight encode)
let mut write_ns = 0u64; // time blocked writing the AU to the stdout pipe (relay backpressure)
let mut repeats = 0u64; // frames where no newer capture had arrived (duplicate re-encode)
let mut cap_ns = 0u64; // time in try_latest (capture + video-processor convert)
let mut encode_ns = 0u64; // time blocked in lock_bitstream
let mut write_ns = 0u64; // time writing the AU to the stdout pipe (relay backpressure)
let mut window = std::time::Instant::now();
// Prime: submit `depth` frames before the first poll so NVENC has that many encodes in flight.
// We don't hold the `CapturedFrame`s past `submit`: NVENC keeps its own registered texture clone
// and the capturer's ring/held-set own the canonical refs (sized for `depth`), so the in-flight
// inputs stay valid after our clones drop.
enc.submit(&first).context("first encoder submit")?;
drop(first);
for _ in 1..depth {
let f = cap.next_frame().context("WGC prime frame")?;
enc.submit(&f).context("prime encoder submit")?;
}
// `frame` is held across iterations and repeated when `try_latest` has nothing newer, so a static
// desktop still clocks `fps`. The capturer's held-set / output ring keep its texture alive across
// the repeat; reassigning `frame` on a fresh capture drops the prior one (already drained by poll).
let mut frame = first;
let mut next = std::time::Instant::now();
loop {
if kf.swap(false, Ordering::Relaxed) {
enc.request_keyframe();
}
// Pop + forward the OLDEST in-flight frame (FIFO). With `depth` outstanding it has had
// depth-1 frames' worth of GPU slots to finish, so this rarely blocks under load.
let p0 = std::time::Instant::now();
let polled = enc.poll().context("encoder poll")?;
if perf {
encode_ns += p0.elapsed().as_nanos() as u64;
// Freshest captured frame, or repeat the last (no new composition: static desktop / between a
// game's presents). Non-blocking, so the cadence is OURS, not WGC's event rate.
let t0 = std::time::Instant::now();
match cap.try_latest().context("WGC try_latest")? {
Some(f) => frame = f,
None => repeats += 1,
}
if let Some(au) = polled {
if perf {
cap_ns += t0.elapsed().as_nanos() as u64;
}
enc.submit(&frame).context("encoder submit")?;
// Drain the just-submitted frame. NVENC's poll blocks in lock_bitstream until it's encoded, so
// this returns exactly one AU (then None) — depth-1, no accumulation.
loop {
let p0 = std::time::Instant::now();
let polled = enc.poll().context("encoder poll")?;
if perf {
encode_ns += p0.elapsed().as_nanos() as u64;
}
let Some(au) = polled else { break };
let w0 = std::time::Instant::now();
let wrote = write_au(&mut out, &au);
if perf {
@@ -158,13 +182,13 @@ pub fn run(opts: HelperOptions) -> Result<()> {
return Ok(());
}
}
// Refill: capture + submit to keep `depth` frames in flight.
let t0 = std::time::Instant::now();
let next = cap.next_frame().context("WGC next frame")?;
if perf {
cap_wait_ns += t0.elapsed().as_nanos() as u64;
// Pace to this frame's due time. If we're already past it (encode couldn't keep up under a
// GPU-saturating game), skip the sleep and re-baseline so we don't spiral into catch-up.
next += interval;
match next.checked_duration_since(std::time::Instant::now()) {
Some(d) => std::thread::sleep(d),
None => next = std::time::Instant::now(),
}
enc.submit(&next).context("encoder submit")?;
if perf {
frames += 1;
@@ -174,13 +198,15 @@ pub fn run(opts: HelperOptions) -> Result<()> {
let per = |ns: u64| format!("{:.2}", ns as f64 / frames as f64 / 1e6);
tracing::info!(
fps = format!("{:.1}", frames as f64 / secs),
cap_wait_ms = per(cap_wait_ns),
repeats,
cap_ms = per(cap_ns),
encode_ms = per(encode_ns),
write_ms = per(write_ns),
"WGC helper perf (depth-pipelined; encode_ms=lock_bitstream on the oldest)"
"WGC helper perf (fixed-cadence depth-1; encode_ms=lock_bitstream; repeats=duplicated frames)"
);
frames = 0;
cap_wait_ns = 0;
repeats = 0;
cap_ns = 0;
encode_ns = 0;
write_ns = 0;
window = std::time::Instant::now();
@@ -197,3 +223,115 @@ fn write_au(out: &mut impl Write, au: &encode::EncodedFrame) -> std::io::Result<
out.write_all(&au.data)?;
out.flush()
}
/// O3 present-trigger experiment (see the gated call in `run`). Creates a small swapchain-backed
/// window on the virtual display (the CCD-isolated primary) and presents continuously — an active
/// present SOURCE on the display — to test whether that makes the OS assign the driver's IddCx
/// swap-chain (which WGC's read does not). Runs forever on its own thread.
///
/// # Safety
/// Win32/D3D11 FFI; called once on a dedicated helper thread.
unsafe fn present_trigger(disp_w: u32, disp_h: u32) -> Result<()> {
use windows::core::{w, Interface};
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
use windows::Win32::Graphics::Direct3D11::{
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11RenderTargetView,
ID3D11Texture2D, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION,
};
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC};
use windows::Win32::Graphics::Dxgi::{
IDXGIAdapter, IDXGIDevice, IDXGIFactory2, DXGI_PRESENT, DXGI_SWAP_CHAIN_DESC1,
DXGI_SWAP_EFFECT_FLIP_DISCARD, DXGI_USAGE_RENDER_TARGET_OUTPUT,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DispatchMessageW, PeekMessageW, RegisterClassW,
ShowWindow, MSG, PM_REMOVE, SW_SHOWNOACTIVATE, WNDCLASSW, WS_EX_NOACTIVATE, WS_EX_TOPMOST,
WS_POPUP, WS_VISIBLE,
};
unsafe extern "system" fn wndproc(h: HWND, m: u32, wp: WPARAM, lp: LPARAM) -> LRESULT {
DefWindowProcW(h, m, wp, lp)
}
let hinst: HMODULE = GetModuleHandleW(None)?;
let cls = w!("pfPresentTrigger");
let wc = WNDCLASSW {
lpfnWndProc: Some(wndproc),
hInstance: hinst.into(),
lpszClassName: cls,
..Default::default()
};
RegisterClassW(&wc);
// Small window at the top-left of the (primary = virtual) display so it barely obscures the
// captured desktop; topmost + no-activate so it doesn't steal focus.
let win_w = disp_w.min(96) as i32;
let win_h = disp_h.min(96) as i32;
let hwnd: HWND = CreateWindowExW(
WS_EX_TOPMOST | WS_EX_NOACTIVATE,
cls,
w!("pf-present"),
WS_POPUP | WS_VISIBLE,
0,
0,
win_w,
win_h,
None,
None,
Some(hinst.into()),
None,
)?;
let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)?;
let device = device.context("present-trigger d3d11 device")?;
let context = context.context("present-trigger d3d11 context")?;
let dxgi_dev: IDXGIDevice = device.cast()?;
let adapter: IDXGIAdapter = dxgi_dev.GetAdapter()?;
let factory: IDXGIFactory2 = adapter.GetParent()?;
let scd = DXGI_SWAP_CHAIN_DESC1 {
Width: win_w as u32,
Height: win_h as u32,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
BufferCount: 2,
SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD,
..Default::default()
};
let swapchain = factory.CreateSwapChainForHwnd(&device, hwnd, &scd, None, None)?;
tracing::info!("present-trigger: swapchain created on the virtual display; presenting");
let mut frame = 0u32;
loop {
let mut msg = MSG::default();
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
let _ = DispatchMessageW(&msg);
}
let back: ID3D11Texture2D = swapchain.GetBuffer(0)?;
let mut rtv: Option<ID3D11RenderTargetView> = None;
device.CreateRenderTargetView(&back, None, Some(&mut rtv))?;
let rtv = rtv.context("present-trigger rtv")?;
let c = (frame % 120) as f32 / 120.0;
context.ClearRenderTargetView(&rtv, &[c, 0.1, 0.2, 1.0]);
let _ = swapchain.Present(1, DXGI_PRESENT(0));
frame = frame.wrapping_add(1);
}
}
+497
View File
@@ -0,0 +1,497 @@
# Windows virtual display — a Rust port of SudoVDA (investigation & plan)
Status: **P1 done — `pf-vdisplay` validated streaming on glass at 5120×1440@240** (2026-06-22). The
all-Rust IddCx driver replaces the vendored **SudoVDA** C++ driver, matching the "all-Rust UMDF, zero
external driver deps" direction we finished for gamepads (ViGEmBus gone; DualSense/DS4/XUSB shipped).
The investigation/plan below is kept for context; see **Validated on-box** for the result.
## TL;DR
A Rust port is **feasible, low-on-blockers, and strategically aligned** — and there's an unexpected
architectural prize beyond "same thing, in Rust."
- **Signing is not a blocker.** An IddCx driver is UMDF *user-mode*; it needs **no WHQL, no
attestation, no test-signing**. A self-signed cert in LocalMachine `Root` + `TrustedPublisher`
loads it — **exactly the model our gamepad drivers already ship** (and exactly what SudoVDA and the
other forks do). ([Do UMDF drivers require signing?](https://learn.microsoft.com/en-us/archive/blogs/peterwie/do-umdf-drivers-require-signing))
- **We would not be first in Rust.** [`MolotovCherry/virtual-display-rs`](https://github.com/MolotovCherry/virtual-display-rs)
is a complete, shipping **IddCx driver written in Rust** (MIT), with hand-rolled IddCx/WDF bindgen
bindings (`wdf-umdf-sys` + `wdf-umdf`) and a reference swap-chain processor. This turns "greenfield
FFI" into "adapt a proven reference."
- **The prize: we can stop using DXGI Desktop Duplication.** An IddCx driver already *receives* the
composited desktop frames in its swap-chain. [Looking Glass](https://deepwiki.com/gnif/LookingGlass/2.5-indirect-display-driver-(idd))
ships exactly this in production — driver consumes the swap-chain, hands frames to a separate
process, "operates entirely independently of DDA." Doing the same would **delete an entire class of
multi-GPU bugs** the current `capture/dxgi.rs` is built to survive (ACCESS_LOST storms,
MODE_CHANGE_IN_PROGRESS, the `win32u.dll` reparenting patch).
Recommendation: **yes, build it in Rust**, in phases — a drop-in DDA-compatible driver first (own the
stack at low risk), then the direct-frame-push path (the real cleanup). Keep vendoring SudoVDA as the
safe interim until the Rust driver is on-glass-validated on the RTX box.
## Validated on-box (2026-06-22)
Before committing, the toolchain + load path were proven on the RTX box (Win11 26200, WDK 26100):
- **A Rust IddCx driver builds with our toolchain.** Cloned [`virtual-display-rs`](https://github.com/MolotovCherry/virtual-display-rs)
and built its driver `.dll` against our WDK (UMDF 2.31 + IddCx 1.4 stubs, bindgen over `IddCx.h` via
our LLVM, nightly-2024-07-26). One fix needed: its `build.rs` picked the **max** SDK Lib version
(`10.0.28000.0`, a base SDK with no IddCx) for the `IddCxStub` search path; resolving it by the
version that actually contains `um\x64\iddcx\1.4` (`10.0.26100.0`, the WDK) fixed the link.
- **It installs self-signed and loads.** Signed `.dll`/`.cat` with our existing driver cert (the
gamepad `punktfunk-ds-test`), `pnputil /add-driver`, root devnode via `devgen`. The device came up
**Status OK / CM_PROB_NONE**, Class Display, hosted by `WUDFRd` — a Rust IddCx adapter initialized
cleanly. (SudoVDA, already live here, independently confirms IddCx + self-signed UMDF work on this
box.) Test artifacts removed afterward; SudoVDA untouched.
**Conclusion:** the central risk ("can we build + load a Rust IddCx driver here?") is retired. The
binding question (D2) resolves toward **reusing `virtual-display-rs`'s self-contained `wdf-umdf-sys` +
`wdf-umdf` bindgen crates** (now proven to build + load on our box) rather than extending
`windows-drivers-rs` — IddCx functions are direct `IddCxStub` exports the WDF function-table macro
can't reach anyway, so a unified bindgen is the cleaner base for `pf-vdisplay`. Reference clone kept at
`C:\Users\Public\virtual-display-rs`.
**Scaffold + driver logic landed + on-glass:** `packaging/windows/vdisplay-driver/` — vendored
`wdf-umdf-sys`/`wdf-umdf` (MIT, + the SDK-version build.rs fix) + the `pf-vdisplay` driver crate. The
full IddCx driver is ported (entry → `IDD_CX_CLIENT_CONFIG` with all 7 callbacks → device/monitor
context → our own EDID → a real swap-chain drain), with the IPC/serde/`tokio` stack replaced by an
in-tree `monitor` model and `OutputDebugString` logging. **Validated on the RTX box:** built, signed
(our `punktfunk-ds-test` cert), installed, loaded **Status OK**, and **arrived a real virtual monitor**
("VirtuDisplay+", `DISPLAY\CHY0000`) — i.e. an OURS, all-Rust IddCx virtual display creating a monitor.
**IOCTL control plane done + on-glass (P1 functionally complete):** the SudoVDA-compatible control
plane is implemented (`EVT_IDD_CX_DEVICE_IO_CONTROL` + the `{e5bcc234-…}` interface registered via
`WdfDeviceCreateDeviceInterface`; `control.rs` with byte-identical structs) — `ADD` a monitor at a
requested mode → `{LUID, target_id}` (target id + adapter LUID captured from `IDARG_OUT_MONITORARRIVAL`),
`REMOVE` by GUID, `PING`/`GET_WATCHDOG` watchdog, `GET_VERSION`, `SET_RENDER_ADAPTER`
(`IddCxAdapterSetRenderAdapter`); per-`ADD` mode injection (requested mode preferred + fallbacks). Added
the five missing FFI wrappers to the vendored `wdf-umdf`. **Validated on the RTX box** with a probe
that mimics `vdisplay/sudovda.rs` exactly: `GET_VERSION → 0.2.1`, `GET_WATCHDOG → timeout=3`,
`ADD 1920×1080@60 → target_id=257 + adapter LUID`, a real "VirtuDisplay+" monitor arrived at the
requested mode, `REMOVE` ok. **Constraint:** pf-vdisplay can't coexist with SudoVDA — they register the
same interface GUID, so two IddCx adapters claiming it → `FAILED_POST_START`; pf-vdisplay *replaces*
SudoVDA (validated by disabling SudoVDA first).
**Watchdog + real-host drive validated:** added the watchdog thread (1 Hz countdown reset by any IOCTL;
tears down all monitors at 0 so a gone host never leaves a phantom display; mirrors SudoVDA's
`RunWatchdog`). Pointed the **real host** at it — removed SudoVDA's devnode so pf-vdisplay is the sole
`{e5bcc234}` provider, then ran the host's `vdisplay::sudovda::tests::live_create_drop`
(`PUNKTFUNK_SUDOVDA_LIVE=1`): **test passed**, and the pf-vdisplay log shows the host's IOCTLs landing —
`ADD 1920x1080@60 → target_id=258, luid=…02619823`, then the watchdog correctly tore the monitor down
when the test process exited without a final REMOVE. So `vdisplay/sudovda.rs` drives pf-vdisplay
unchanged through the full control contract.
**Validated streaming end-to-end on glass (2026-06-22) — P1 complete.** pf-vdisplay is a working
SudoVDA replacement. Driven by the **real host** (`serve`, the LocalSystem service) with a stock client
at **5120×1440@240**: the monitor arrives, `resolve_gdi_name → \\.\DISPLAY10`, `set_active_mode` +
CCD-isolate succeed, the DXGI output resolves **under the RTX 4090**, WGC capture + NVENC run at
**steady 240 fps, ~2.4 ms encode**, 6512 AUs sent, clean teardown (`isolate restored rc=0x0`). Same
`vdisplay/sudovda.rs` path, unchanged — full parity with SudoVDA.
**The earlier "monitor arrives but never gets a swap-chain / no DXGI output" symptoms were a
measurement + state artifact, not a driver bug.** Two traps cost a lot of time:
1. **Session 0.** Every standalone probe (`vdtest`, the host's `live_create_drop` test) ran in
**Session 0** — the services session, whose desktop is a throwaway **1024×768** basic display. IddCx
activation happens in the **console Session 1**, where the 4090 drives the real desktop. So
`Screen.AllScreens`/CCD queries from Session 0 *can never* see the virtual monitor activate — they
report the wrong desktop. The only valid way to drive + observe it is the **host service** (SYSTEM,
which targets Session 1) plus the driver's own `OutputDebugString` (system-wide, session-agnostic).
2. **Accumulated device-state damage.** Repeated reinstalls + `Disable`/`Enable-PnpDevice` cycles +
a control handle the host **cached across all of it** left the device tree wedged (stale handle →
the host's PINGs fail → the 3 s watchdog tears the monitor down mid-session → capture opens a dying
display → "no DXGI output"). **A reboot cleared it and it worked on the first connect.** Lesson:
after device churn, restart the host service (fresh handle) — and when in doubt, reboot.
The swap-chain processor is a **faithful port of virtual-display-rs's** (it drains correctly via
`ReleaseAndAcquireBuffer` + `FinishedProcessingFrame` — the drain is *required*; a true no-op would
stall DWM and freeze the captured image). The EDID is our **own clean 128-byte block** (manufacturer
`PNK`, product `punktfunk`) — no SudoVDA bytes.
**Build gotcha (important for iterating):** updating an installed UMDF driver only takes if the INF
**DriverVer changes**`deploy-dev.ps1` stamps a date.time `-v` on every run; without a bump the old
binary keeps running (silently). **Devnode hygiene:** create the root devnode with
`nefconc --create-device-node` (a clean `ROOT\DISPLAY` node), NOT `devgen /add` — devgen makes
**persistent `SWD\DEVGEN` software devices** that survive reboot *and* registry deletion and resurrect
on every `pnputil /add-driver` (they have `hwid root\pf_vdisplay`, so the driver install re-materializes
them). The production installer must use a single `nefconc`/INF-created node and never `devgen`.
## P2 — direct frame push (kill DDA): design & decision record
Status: **in progress.** P1 ships frames the old way (the driver drains its swap-chain and DDA/WGC
re-captures the composited desktop). P2 makes the driver *publish* each swap-chain frame to the host
directly, so we can retire Desktop Duplication and its multi-GPU survival code. Built behind
`PUNKTFUNK_IDD_PUSH`, A/B'd against DDA, and only then made the default.
### The decisive finding: producer and consumer are both in Session 0
The whole transport design hinged on one unknown — same-session or cross-session? **Measured on the
RTX box (2026-06-22):** the pf-vdisplay host process is `WUDFHost.exe` with
`-DeviceGroupId:pfVDisplayGroup`, running in **Session 0**; the punktfunk host service is `LocalSystem`,
also **Session 0**. So the swap-chain processor thread (spawned by our own `thread::spawn` inside the
driver, i.e. in `WUDFHost`) and the encoder live in the **same session**. This is the easy case:
- A D3D11 **shared keyed-mutex texture** created in the driver can be opened by name in the host with
`ID3D11Device1::OpenSharedResourceByName` — both devices created on the **same render-adapter LUID**
(which the driver already reports out of the `ADD` IOCTL via `OsAdapterLuid`, surfaced as
`WinCaptureTarget::adapter_luid`).
- Named kernel objects resolve through Session 0's shared `\BaseNamedObjects`, so **no `Global\`
prefix / `SeCreateGlobalPrivilege` gymnastics** are needed (kept the names unprefixed; documented
that this relies on both processes being Session 0). The Looking-Glass cross-*VM* shared-memory
device is unnecessary — this is cross-*process*, same-session, on one GPU.
This collapses the "Session-0 cross-process transport is the long pole" risk from the original plan.
### Transport: a ring of shared keyed-mutex textures + a metadata header + an event
A single ping-pong keyed mutex would couple the driver's present rate to the host's consume rate — and
**the swap-chain thread must never block** (a stalled `IddCxSwapChainReleaseAndAcquire`/processing loop
freezes DWM compositing system-wide). So, the Looking-Glass shape — multiple frame buffers, newest
wins:
- **Ring** of `N` (default 3) shared textures, `RESOURCE_MISC_SHARED_NTHANDLE |
SHARED_KEYEDMUTEX`, fixed size for the session. A **generation** counter bumps on a mode change
(resize): the driver tears down + recreates the ring at the new size, the host notices the
generation change and re-opens.
- **Named metadata header** (`CreateFileMapping`): `{magic, version, generation, width, height,
dxgi_format, ring_len, latest}` where `latest` packs `{write_index, monotonic sequence}` published
*after* the copy completes. Plain (unprefixed) names — Session-0 shared namespace.
- **Frame-ready auto-reset event** so the consumer waits instead of spinning.
- **Producer (driver, per acquired frame):** pick `(latest_index + 1) % N`; **try**-acquire that
slot's keyed mutex with a 0 ms timeout (if the host still holds it — rare with 3 slots — reuse the
current slot or skip, **never block**); `CopyResource` the acquired `MetaData.pSurface` into the
slot; release the mutex; publish `{index, ++seq}`; `SetEvent`. Then `FinishedProcessingFrame` as
today.
- **Consumer (host `IddPushCapturer`):** `WaitForSingleObject(event, timeout)`; read `latest`; if `seq`
advanced, acquire that slot's mutex, `CopyResource` into an owned NVENC-input texture, release, yield
`FramePayload::D3d11{texture, device}` — straight into the existing zero-copy NVENC path. No DDA, no
CPU readback.
### What P2 removes vs. keeps
- **Removes:** `capture/dxgi.rs`'s `DXGI_ERROR_ACCESS_LOST`/`MODE_CHANGE_IN_PROGRESS` re-duplication
churn, the legacy-`DuplicateOutput` fallback, and **`install_gpu_pref_hook()` (the `win32u.dll`
patch)** — by **pinning the render adapter to the encoder GPU** (`IddCxAdapterSetRenderAdapter`, the
existing `SET_RENDER_ADAPTER` IOCTL, driven before `ADD`), so the OS never reparents the output and
the shared texture + NVENC share one device by construction.
- **Keeps:** display **topology** (making the virtual display the composited desktop) and the
**watchdog** (now ours). The **two-process WGC secure-desktop relay** stays until we confirm the IDD
push also delivers the secure (Winlogon) desktop; if it does, that retires too.
### On-glass attempt 2026-06-22 — code complete, blocked at driver load
The full transport (driver publisher + host `IddPushCapturer` + render-LUID robustness + in-process
routing) is written and compiles clean. The first on-glass A/B exposed several real things and one
hard blocker:
- **The service captures in a Session-1 WGC helper, not in-process.** `should_use_helper()` returns
true for a SYSTEM service, so it spawns a user-session helper that does capture **and input
injection**. IDD-push must capture **in-process in Session 0** (where the driver publishes) — wired
via `should_use_helper()` returning false for `PUNKTFUNK_IDD_PUSH`. **Caveat:** `SendInput` from
Session 0 can't reach the user's Session-1 desktop, so in-process IDD-push has **no working input**
yet. Production needs either a Session-1 input-only helper, or `Global\`-namespaced shared textures
so a Session-1 helper consumes IDD-push for both video + input.
- **`SET_RENDER_ADAPTER` is ignored by the driver** (the IDD lands on a different adapter than pinned:
observed IDD adapter `0xd60722` vs pinned 4090 `0x15de1`). The render-LUID-in-header path makes the
host bind correctly regardless, but the driver should be made to actually honor the pin (or the host
must copy across adapters) so NVENC gets a 4090 surface.
- **Cursor is included** in the IddCx composited frame (DDA strips it) — so the host-side cursor
compositor (P2.5) is likely unnecessary for this path.
- **`FAILED_POST_START` was a red herring (churn, not the binary).** Comparing the 2157 (works) and
the `frame_transport` DLL import tables: **identical** (same 8 DLLs; the size/hash delta is just the
Authenticode signature). A clean install **+ reboot** (no `restart-device`/`disable-enable`/kill in
between) loads the `frame_transport` driver to **`OK`**. The earlier `FAILED_POST_START` was the
device wedging from the hot-reload churn (the deploy gotchas above). **Lesson: deploy = install +
reboot, full stop.**
- **THE REAL BLOCKER — the driver can't CREATE the shared objects.** With the driver loaded clean and
the monitor active, the host's `IddPushCapturer` still times out: `pfvd-hdr-<target> never appeared`.
The driver's own `OutputDebugString` is invisible (UMDF redirects it to ETW, not DebugView — verified
with a working DBWIN self-test), so a **file-logging** driver build was tried — and it wrote **no
file at all**, even though `init()` runs in `DriverEntry`, the device is `OK`, WUDFHost runs as
`LocalService`, and `C:\Users\Public` is world-writable. **WUDFHost runs with a restricted token: it
can neither write the filesystem nor create named kernel objects** (`CreateFileMappingW`/`CreateEventW`/
`CreateSharedHandle`), so `FramePublisher::new` fails silently. This is exactly why the **gamepad UMDF
drivers invert it**: `inject/dualsense_windows.rs` — *"the host creates the section (privileged → a
permissive SDDL so the WUDFHost can open it); the driver maps it"* — `Global\pfds-shm-<idx>` + SDDL
`D:(A;;GA;;;WD)`. **Fix: invert frame-push to match.** The HOST creates the header + event + ring
textures (`Global\` names, `D:(A;;GA;;;WD)` SDDL); the DRIVER only OPENS them, writes its actual
render LUID + a status code back into the host-created header (so we get driver visibility through the
host log), and runs the copy loop. The host creates the textures on the render adapter the driver
reports.
- **Also unresolved: `SET_RENDER_ADAPTER` appears ignored** (the host's pin to the 4090 vs the ADD-reply
adapter differ every time). The inverted header carries the driver's *actual* render LUID so the host
can create textures + run NVENC on the right adapter — but if that's the iGPU, NVENC (NVIDIA) can't
encode it, so the driver must be made to honor the pin (or the host must cross-adapter copy). Needs its
own investigation.
**Driver deploy gotchas learned (this box):** hot-reloading a UMDF display driver is unreliable —
`pnputil /restart-device` does NOT restart WUDFHost (old image stays mapped), `Disable/Enable-PnpDevice`
errors on the root-enumerated IDD, and **killing WUDFHost invalidates the host's cached `{e5bcc234}`
control handle** (every ADD then fails `0x80070006`, and the device can wedge to `FAILED_POST_START`).
A **reboot** loads a freshly-installed build cleanly. **Recovery** from a broken build is clean and
reboot-free: `pnputil /delete-driver <oemNN>.inf /uninstall` removes the bad package and the device
rebinds the previous (validated) package in the DriverStore — restored 2157 → `OK` immediately.
### On-glass attempt 2 (2026-06-23) — inversion works; in-process Session-0 path is a dead end
Implemented the **inversion** (host creates the header + event + ring textures with the
`D:(A;;GA;;;WD)` SDDL, driver only opens them) + a per-attempt **generation** (kills the
`DXGI_ERROR_NAME_ALREADY_EXISTS` retry collisions) + a fixed-name **`Global\pfvd-dbg` debug channel**
(structured counters the driver writes, since UMDF/ETW + the restricted token block its other logs).
Results on the RTX box:
- ✅ The host **creates the shared ring every time** (`created shared ring … render_luid=…`) — the
privileged-create / restricted-open split is sound.
- ✅ No more name collisions (generation fix).
-**The driver writes NOTHING** — debug block all zeros, crucially `run_core_entries=0`. The
swap-chain processor **never runs**, i.e. the OS **never assigns a swap-chain** to the virtual
monitor in this path.
**Root cause: an IddCx monitor only gets a swap-chain when something PRESENTS to it, and the in-process
path has no presenter.** The host + the CCD topology-isolate run in **Session 0, which has no DWM /
compositor**. The WGC path works because its capture helper lives in **Session 1**, where DWM composes
the desktop onto the display (that composition is the swap-chain trigger). So in-process Session-0
IDD-push gets no frames to push, full stop — a **fundamental** barrier, not a fixable bug. The original
plan's "Session-0 transport is the long pole" was right, but the long pole turned out to be *triggering
presentation*, not the shared-memory mechanics (those work).
**Consequence:** the only viable IDD-push shape is **option 3 — a Session-1 helper drives presentation +
consumes the `Global\` ring** (the inversion built here is exactly what it needs). But it carries an
unretired risk: it's still unproven whether the swap-chain gets assigned even with a Session-1 consumer
that isn't WGC. Until that's answered, **DDA/WGC stays the shipping Windows capture path** — it works.
All the IDD-push code (driver open-side + host create-side + debug channel) is written, compiles, and is
gated behind `PUNKTFUNK_IDD_PUSH` (off), so it's dormant and harmless.
### CONCLUSION (2026-06-23): IDD-push is not viable for bare-metal capture — the swap-chain is never assigned
After the inversion + a fixed-name debug channel + a host-created-ring observer + an autonomous
loopback test harness (`punktfunk-probe` → the SYSTEM service, paired via the mgmt API), the question
"does the driver's swap-chain processor ever run?" was answered **definitively: no.** The driver's
`run_core` is **never entered**`run_core_entries=0` in *every* configuration tested:
- in-process (Session 0) and WGC-triggered (Session 1 helper) sessions,
- a user-created ring AND a host-created (LocalSystem) ring with a permissive `D:(A;;GA;;;WD)` SDDL,
- with and without a Low-IL (`S:(ML;;NW;;;LW)`) mandatory label,
- with WUDFHost confirmed **not** an AppContainer (`IsAppContainer=0`),
— even while WGC simultaneously captured the same virtual monitor's composition and streamed multi-MB
of HEVC. The gamepad UMDF drivers prove a UMDF driver *can* open + write a host-created `Global\`
section on this box, so the driver writing nothing is **not** an access problem — `run_core` simply
does not run.
**Root cause (researched + ecosystem-confirmed):** an IddCx virtual monitor only receives a swap-chain
(`EVT_IDD_CX_MONITOR_ASSIGN_SWAPCHAIN`) when the OS **presents/scans-out** to it, which requires a real
presentation consumer. **WGC/DDA capture of the composed desktop does NOT count** — it reads DWM's
composition, bypassing the driver's swap-chain. With no physical scanout and no consumer that routes
*through the driver*, the path stays inactive (`IDDCX_PATH_FLAGS=0`) and `ASSIGN_SWAPCHAIN` never fires.
Confirming evidence:
- **Every bare-metal virtual-display capture project uses WGC/DDA, not the driver swap-chain:** SudoVDA
(its swap-chain loop acquires-and-discards), Apollo/Sunshine (DDA + WGC backends), virtual-display-rs
(discards), parsec-vdd (no frame path). Only **Looking Glass** consumes the driver swap-chain — and
only because a **VM guest scans out** the display (the consumer). We have no equivalent on bare metal.
- Microsoft's own unanswered Q&A (learn.microsoft.com/answers 4096179) reports the identical symptom for
the IddSampleDriver: virtual display "always inactive," `ASSIGN_SWAPCHAIN` never runs.
**Verdict:** the "driver consumes its swap-chain and pushes frames" architecture (P2 / Looking-Glass
style) **cannot get frames** for punktfunk's bare-metal, whole-desktop, capture-only use case. The
shared-memory transport machinery (host-creates / driver-opens, the gamepad pattern) is all sound and
proven to *create*, but there is nothing for the driver to publish. **DDA/WGC remains the only viable
Windows capture path**, which is exactly what the entire ecosystem does. The IDD-push code stays
in-tree, compiles, and is gated `off` (`PUNKTFUNK_IDD_PUSH`) — dormant and harmless — documenting the
attempt so it isn't re-tried. "Better performance/lower overhead" must come from optimizing the WGC/DDA
path (e.g. trimming the Session-0↔Session-1 relay, zero-copy encode), not from IDD-push.
The only unexplored avenue is **driver-side** (a different adapter/monitor/path setup that might make the
OS treat the virtual display as a presentation target) — but it needs a reboot to test, the MS Q&A
suggests it's unsolved, and the unanimous ecosystem choice of WGC/DDA argues it's a dead end.
**Final exhaustion (2026-06-23, follow-up): both remaining avenues closed.**
- **Option 3 (present source) — TESTED, failed.** Added a present-trigger to the Session-1 WGC helper:
it successfully created a D3D11 swapchain on the virtual display and presented continuously (WGC even
captured the flashing window). The driver stayed `run_core_entries=0` / `frames_acquired=0`. So an
active *present source* on the display does NOT make the OS assign the driver's swap-chain either —
DWM composes the present onto the display (capturable) without routing it through the driver's
swap-chain.
- **Option 2 (driver flag) — closed by analysis.** The present-trigger succeeding proves the **path is
already active** (a swapchain presents to the display fine); the missing piece is **scanout routed
through the driver**, which the OS does only for a real consumer (physical display / VM guest / RDP).
The one IddCx flag for that — `IDDCX_ADAPTER_FLAGS_REMOTE_SESSION_DRIVER` — requires the **RDP
protocol stack** as the consumer, which bare-metal console capture has no equivalent of.
**Verdict is final:** IDD-push needs a presentation consumer (scanout / VM guest / RDP) that bare-metal
console desktop-capture fundamentally cannot provide. No host-side capture, no in-process path, no
present source, and no available driver flag overcomes it. WGC (normal desktop) + DDA (secure desktop)
is the only viable Windows capture path — as the entire ecosystem already does. The IDD-push +
present-trigger code stays in-tree, gated off, as the documented record of the attempt.
### Known gaps the build-out must close (tracked as P2.* tasks)
- **Cursor.** DDA/WGC composite the HW cursor host-side from frame-info; the IDD path delivers the
cursor separately (`IddCxMonitorSetupHardwareCursor` event → `QueryHardwareCursor`). The prototype
may ship cursor-less; the build-out wires the IDD cursor into the existing `CursorCompositor`.
- **HDR.** The default IddCx swap-chain surface is 8-bit `B8G8R8A8`; FP16/HDR needs the **IddCx 1.11
D3D12 acquire path** (`SetDevice2`/`ReleaseAndAcquireBuffer2``ID3D12Resource`). Build against
1.10, runtime-gate 1.11. SDR-only for the prototype.
## Why we'd do this
The user's goals, mapped to outcomes:
| Goal | Outcome |
| --- | --- |
| Drop external deps | No more vendored prebuilt SudoVDA `.dll`/`.cat` (third-party, C++, single upstream). |
| Increase Rust coverage | The display driver joins the gamepad drivers as in-tree Rust UMDF. |
| Own the stack / easier display management | We control the IOCTL protocol, the EDID, the mode list, the watchdog — and can fold the topology/mode logic that's currently scattered in `vdisplay/sudovda.rs` into the driver. |
| Cleaner code | Phase 2 retires `capture/dxgi.rs`'s DDA workarounds + the `win32u.dll` patch. |
## What we'd be replacing (current architecture)
- **Driver:** SudoVDA — UMDF2 IddCx, `Class=Display`, `UmdfExtensions=IddCx0102`,
`UpperFilters=IndirectKmd`, root-enumerated `Root\SudoMaker\SudoVDA`. Vendored prebuilt under
`packaging/windows/sudovda/`, installed by `install-sudovda.ps1` (cert → `nefconc` devnode →
`pnputil`). Source is public ([SudoMaker/SudoVDA](https://github.com/SudoMaker/SudoVDA), README-only
MIT/CC0 grant over the MS sample, ~1,900 LOC C++).
- **Host contract:** `crates/punktfunk-host/src/vdisplay/sudovda.rs` opens the control device by
interface GUID `{e5bcc234-…}` and drives a tiny `METHOD_BUFFERED` IOCTL protocol — byte-identical to
SudoVDA's `Common/Include/sudovda-ioctl.h`:
- `ADD (0x800)` `{w,h,refresh,GUID,name[14],serial[14]}``{LUID, target_id}`
- `REMOVE (0x801)` `{GUID}` · `SET_RENDER_ADAPTER (0x802)` `{LUID}` · `GET_WATCHDOG (0x803)` ·
`PING (0x888)` (mandatory keepalive) · `GET_VERSION (0x8FF)`
- **Capture:** `capture/dxgi.rs` finds the virtual monitor's GDI output **across all adapters** (it's
enumerated under the *rendering* GPU, not SudoVDA's LUID) and runs **DXGI Desktop Duplication**
(`DuplicateOutput1`, FP16 for HDR). This file is **dominated by virtual-display-over-DDA survival
code**: `DXGI_ERROR_ACCESS_LOST` re-duplication with retries, `MODE_CHANGE_IN_PROGRESS` backoff,
legacy-`DuplicateOutput` fallback, CCD display isolation to make the IDD the sole composited
desktop, and an **`install_gpu_pref_hook()` that patches `win32u.dll!NtGdiDdDDIGetCachedHybridQueryValue`**
to stop DXGI reparenting the output across GPUs. Most of that exists *because* we capture a virtual
display via DDA on a multi-GPU box.
## Feasibility findings
### Signing — green (the make-or-break)
UMDF user-mode ⇒ Code-Integrity signing rules don't apply to our binary (the only kernel piece is
Microsoft's inbox `IndirectKmd`). Self-signed cert in `Root` + `TrustedPublisher` is sufficient on a
normal Secure-Boot Win11 box — no `bcdedit /set testsigning`. SudoVDA and `virtual-display-rs` both
ship this way. This is the **same** model as our DualSense/DS4/XUSB drivers. (The only thing that
breaks install is a botched cert placement, not a signing *tier*.)
### Rust prior art — exists, MIT, reusable
`virtual-display-rs` proves an all-Rust IddCx driver runs in production and gives us:
`wdf-umdf-sys` (bindgen over WDF **and** `iddcx.h`, links `IddCxStub`), `wdf-umdf` (safe wrappers —
`iddcx.rs` ~300 LOC, with an `IddCxIsFunctionAvailable!` version-gate macro), and a reference driver
(`swap_chain_processor.rs` ~158 LOC, `direct_3d_device.rs`, `edid.rs`). **Caveat:** it uses its *own*
bindgen stack, **not** `microsoft/windows-drivers-rs` — see Decision D2.
### windows-drivers-rs IddCx support — absent, but a bounded extension
Our `wdk-sys` (m0) binds Base + WDF + feature-gated subsets (hid/gpio/spb/…). **Zero IddCx symbols.**
Adding it is the same shape as the existing subsets: an `ApiSubset::Iddcx` variant + `iddcx` feature →
`iddcx_headers()` returning `iddcx.h` for bindgen, and linking `IddCx.lib`. IddCx functions are **not**
WDF-table functions, so the `call_unsafe_wdf_function_binding!` macro doesn't apply — they're direct
`IddCx.lib` exports we'd `#[link(name="IddCx")] extern` (or bindgen) and wrap ourselves.
`windows` 0.58 (already in the tree) provides the Direct3D11/Dxgi APIs the swap-chain loop needs.
### The IddCx driver itself — well-understood, ~12k LOC
Required callbacks (baselined on the MS [IddSampleDriver](https://github.com/microsoft/Windows-driver-samples/blob/main/video/IndirectDisplay/IddSampleDriver/Driver.cpp), ~1,100 LOC, IddCx 1.4):
`EVT_IDD_CX_ADAPTER_INIT_FINISHED`, `ADAPTER_COMMIT_MODES`, `PARSE_MONITOR_DESCRIPTION`,
`MONITOR_GET_DEFAULT_DESCRIPTION_MODES`, `MONITOR_QUERY_TARGET_MODES`, `MONITOR_ASSIGN_SWAPCHAIN`
(the only callback with real D3D work), `MONITOR_UNASSIGN_SWAPCHAIN`, and `DEVICE_IO_CONTROL` (where
our ADD/REMOVE/PING IOCTLs live). Init flow: `WdfDeviceCreate → IddCxDeviceInitConfig →
IddCxDeviceInitialize → IddCxAdapterInitAsync → IddCxMonitorCreate → IddCxMonitorArrival`.
**Arbitrary resolutions don't need EDID timings:** ship one generic ~128/256-byte EDID base block to
make Windows treat the target as a real monitor, then advertise modes programmatically from the
mode-list callbacks — a static table **plus the runtime-requested client mode injected as preferred**
(exactly SudoVDA's `s_DefaultModes[]` + per-ADD preferred-mode approach). 5120×1440@240 just gets
added at ADD time.
**HDR/10-bit:** supported, but it's the one place IddCx is *harder* than today. The default swap-chain
surface is **8-bit `A8R8G8B8`**; FP16/HDR requires the IddCx **1.11 D3D12 acquire path**
(`SetDevice2`/`ReleaseAndAcquireBuffer2``ID3D12Resource`, with a stricter sync model). Our box is
Win11 26200 (IddCx ≥ 1.10), so this is reachable, but it's real work — and our current WGC/DDA path
gives FP16 HDR "for free." Build against 1.10 and runtime-gate the newer DDIs (SudoVDA's pattern).
## The architectural prize: skip DDA (Phase 2)
An IddCx driver gets each presented frame from `IddCxSwapChainReleaseAndAcquireBuffer` as an
`IDXGIResource` on a device **we** bind via `IddCxSwapChainSetDevice`. We can copy it into a shared
texture / shared section and hand it to the host's encoder process directly — **no Desktop
Duplication**. Why this is the real win, not just a detour:
- **It's the *intended* IddCx use case.** IddCx exists for remote/wireless/USB displays that ship
swap-chain frames over a wire; consuming frames in the driver is the designed path, and **Looking
Glass already does exactly this** (driver → shared memory → separate consumer, no DDA).
- **It kills the multi-GPU bug class.** We call `IddCxAdapterSetRenderAdapter` to pin the swap-chain to
the **same GPU as our NVENC encoder before adding the monitor**, and the OS honors it. No more DXGI
reparenting the output onto the wrong GPU, no ACCESS_LOST storms, and we can **retire
`install_gpu_pref_hook()` (the `win32u.dll` patch)** and most of `capture/dxgi.rs`. Swap-chain
re-creation becomes a documented, in-band event (`ABANDON_SWAPCHAIN`) instead of an undocumented
failure we fight with retries.
What it does **not** remove (be honest): display **topology** management — making the virtual display
the sole/primary composited desktop so the game (and Winlogon) render to it — is independent of how we
*get* frames and stays (though we can integrate it more cleanly). And the watchdog stays, now ours.
The cost: a **Session-0 → service cross-process frame transport** (the driver host is `WUDFHost` in
Session 0 / LocalService; our host is a LocalSystem service). A `Global\`-named, explicitly-ACL'd
shared section + keyed-mutex texture (Looking Glass's shape) is where the engineering actually goes —
prototype this first, it's the only genuinely new risk. Plus the HDR D3D12 path above.
## Decisions to make at kickoff
- **D1 — Own the driver?** Recommend **yes, in Rust.** (Alternatives: fork SudoVDA's C++ — fastest to a
known-good HDR driver but reintroduces a C++ toolchain and README-only license provenance; or keep
vendoring — zero cost, but none of the goals.)
- **D2 — Binding stack?** The main implementation fork.
- **(a)** Extend our `windows-drivers-rs` (m0) with an `iddcx` subset — **one toolchain across all
our drivers**, our build env, but we write the IddCx bindings ourselves (+~35 wk), using
`virtual-display-rs`'s `iddcx.rs` as the 1:1 guide. *Preferred for consistency.*
- **(b)** Vendor `virtual-display-rs`'s `wdf-umdf*` crates (MIT) — fastest to first light, but a
*second* WDK-binding stack in-tree.
- Suggested sequence: **prototype on (b) to prove IddCx-on-our-box in days**, then build production on
**(a)** for consistency.
- **D3 — Frame transport?** Phase it: **DDA-compatible first** (zero capture-side change), **direct
push second** (the cleanup). Don't couple the driver rewrite to the transport rewrite.
## Recommended plan
- **P0 — now:** keep vendoring SudoVDA. No change. (The gamepad-driver installer work just shipped;
this is independent.)
- **P1 — drop-in Rust IddCx driver (`pf-vdisplay`).** Replicate SudoVDA's IOCTL contract **exactly**
(same struct layouts; reuse or re-issue the control interface GUID) so `vdisplay/sudovda.rs` needs
**~zero change** (at most a GUID constant). Class=Display + IddCx INF, our own EDID + programmatic
mode list incl. the per-ADD client mode, the watchdog, a real swap-chain drain (the vdd port — the
drain is required so DWM keeps compositing; DDA/WGC still captures the desktop). Bundle + self-sign +
`pnputil`-install via the installer, identical to the gamepad-driver path we just built. **Outcome:** all-Rust, SudoVDA dependency dropped, DDA capture
unchanged. Effort ≈ **24 wk to first light**, **57 wk to parity** (HDR, multi-monitor, CI).
- **P2 — direct frame push (kill DDA).** Add a swap-chain processor that copies each frame into a
shared section/texture; new `capture` backend reads it directly; pin the render adapter to the
encoder GPU. Gate behind a flag, validate against DDA, then retire the DDA path + the `win32u.dll`
patch. HDR via the IddCx 1.11 D3D12 acquire path. **Outcome:** the real "owning the stack pays off"
cleanup. Effort: additional; the Session-0 transport is the long pole.
## Risks
1. **D3-in-a-driver swap-chain loop** — the one genuinely new piece; bugs here = black screens/TDR.
Mitigated by `virtual-display-rs`'s `swap_chain_processor.rs` + the MS sample as references.
2. **Session-0 cross-process transport** (P2) — the actual hard part; prototype it first.
3. **HDR = the harder D3D12 1.11 path** — our current WGC/DDA HDR is free; the IddCx HDR path is not.
4. **Two binding stacks** if we go D2(b) — a maintenance cost cutting against "clean/consistent."
5. **No WHQL ⇒ no Windows Update / Dev-Center distribution** — same constraint our gamepad drivers
already accept (bundle + self-sign + import cert).
## References
- IddCx model + signing: [IDD model overview](https://learn.microsoft.com/en-us/windows-hardware/drivers/display/indirect-display-driver-model-overview) ·
[IddCx versions](https://learn.microsoft.com/en-us/windows-hardware/drivers/display/iddcx-versions) ·
[1.10+ updates](https://learn.microsoft.com/en-us/windows-hardware/drivers/display/iddcx1.10-updates) ·
[UMDF signing](https://learn.microsoft.com/en-us/archive/blogs/peterwie/do-umdf-drivers-require-signing)
- Swap-chain / frames: [IDDCX_METADATA](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/iddcx/ns-iddcx-iddcx_metadata) ·
[SetDevice](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/iddcx/nf-iddcx-iddcxswapchainsetdevice) ·
[SetRenderAdapter](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/iddcx/nf-iddcx-iddcxadaptersetrenderadapter) ·
[ASSIGN_SWAPCHAIN](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/iddcx/nc-iddcx-evt_idd_cx_monitor_assign_swapchain)
- Prior art: [microsoft IddSampleDriver](https://github.com/microsoft/Windows-driver-samples/tree/main/video/IndirectDisplay) ·
[SudoMaker/SudoVDA](https://github.com/SudoMaker/SudoVDA) ([ioctl.h](https://github.com/SudoMaker/SudoVDA/blob/master/Common/Include/sudovda-ioctl.h)) ·
**[MolotovCherry/virtual-display-rs (Rust, MIT)](https://github.com/MolotovCherry/virtual-display-rs)** ·
[Looking Glass IDD (swap-chain → shm, no DDA)](https://deepwiki.com/gnif/LookingGlass/2.5-indirect-display-driver-(idd)) ·
[itsmikethetech/Virtual-Display-Driver](https://github.com/itsmikethetech/Virtual-Display-Driver)
+29 -24
View File
@@ -6,16 +6,16 @@ generic package registry (`punktfunk-host-windows`) by `.gitea/workflows/windows
## x64 only (no ARM64)
Unlike the client (which ships x64 + ARM64 MSIX), the host is **x64-only by design**. It is coupled to
an NVIDIA GPU (NVENC, via `nvEncodeAPI64.dll` from the driver) and the **SudoVDA** virtual-display
driver — neither exists on Windows ARM64 (no ARM64 NVIDIA driver; the vendored SudoVDA is x64-only). An
an NVIDIA GPU (NVENC, via `nvEncodeAPI64.dll` from the driver) and the **pf-vdisplay** virtual-display
driver — neither exists on Windows ARM64 (no ARM64 NVIDIA driver; the driver builds x64-only). An
ARM64 host would install but couldn't encode or create a virtual display, so we don't build one.
Revisit if NVIDIA-ARM Windows PCs + an ARM64 SudoVDA ever ship.
Revisit if NVIDIA-ARM Windows PCs ever ship.
## Why not MSIX (like the client)
The host installs a **`LocalSystem` SCM service** that `CreateProcessAsUserW`'s from Session 0 into the
interactive session for secure-desktop (UAC / lock screen) capture, adds firewall rules, and depends
on the **SudoVDA** kernel/IDD virtual-display driver. MSIX's sandbox can install **neither** a SYSTEM
on the **pf-vdisplay** UMDF/IDD virtual-display driver. MSIX's sandbox can install **neither** a SYSTEM
service of this kind **nor** a driver. So the host ships as a classic elevated installer.
The installer is deliberately thin: the real install logic — SCM registration, firewall rules, the
@@ -26,9 +26,10 @@ exe into `C:\Program Files\punktfunk\` and calls that subcommand, elevated.
## What the installer does
- Installs `punktfunk-host.exe` (+ `host.env.example`, this README) to `{app}` (`C:\Program Files\punktfunk`).
- **Optional task** *Install the SudoVDA virtual display driver* — imports the driver's self-signed
cert (machine `Root` + `TrustedPublisher`), creates the `root\sudomaker\sudovda` device node (only
if absent — `install-sudovda.ps1`), and stages the driver with `pnputil /add-driver /install`.
- **Optional task** *Install the pf-vdisplay virtual display driver* — imports the driver's self-signed
cert (machine `Root` + `TrustedPublisher`), creates the `root\pf_vdisplay` device node (only
if absent, via nefconc — never devgen`install-pf-vdisplay.ps1`), and stages the driver with
`pnputil /add-driver /install`.
Best-effort: a driver failure warns but never aborts the install (the host degrades to a physical
display without it).
- Runs `punktfunk-host service install` (idempotent; writes a default `host.env` only if absent, so
@@ -45,7 +46,7 @@ exe into `C:\Program Files\punktfunk\` and calls that subcommand, elevated.
(otherwise the locked exe / respawning supervisor would block the copy), then re-points the service;
the existing console password is kept (the wizard page is skipped).
- **Uninstall** (Add/Remove Programs): runs `service uninstall` (stop + delete service + remove
firewall rules) and removes the `PunktfunkWeb` task + its firewall rule. The SudoVDA driver and the
firewall rules) and removes the `PunktfunkWeb` task + its firewall rule. The pf-vdisplay driver and the
`%ProgramData%\punktfunk` config (incl. `web-password`) are intentionally left in place.
Silent install: `punktfunk-host-setup-<ver>.exe /VERYSILENT` (omit the driver with
@@ -54,10 +55,11 @@ read it from `%ProgramData%\punktfunk\web-password`.
## Prerequisites on the target box
- An **NVIDIA GPU + driver** — the installer's exe is built `--features nvenc` and load-depends on the
driver's `nvEncodeAPI64.dll`.
- **ViGEmBus** (optional) for virtual gamepads — still a manual prerequisite (not bundled yet):
<https://github.com/nefarius/ViGEmBus/releases>.
- A **GPU for hardware encode**: an NVIDIA GPU + driver (NVENC), or an AMD/Intel GPU (AMF/QSV) — the
exe is built `--features nvenc,amf-qsv`. Software H.264 is the GPU-less fallback.
- **Virtual gamepads need no prerequisite.** The DualSense / DualShock 4 / Xbox 360 (XUSB) UMDF drivers
are **bundled** in the installer (the *Install the virtual gamepad drivers* task) and
`pnputil`-installed. **ViGEmBus is no longer used.**
## Files here
@@ -65,21 +67,24 @@ read it from `%ProgramData%\punktfunk\web-password`.
|------|------|
| `punktfunk-host.iss` | Inno Setup script (the installer definition). |
| `pack-host-installer.ps1` | Orchestrator: cert + sign, stage the driver + FFmpeg + **web console** (`.output` + bun) bundles, run ISCC, sign setup.exe, emit registry paths. |
| `stage-sudovda.ps1` | Stage the **vendored** SudoVDA driver + fetch/verify the **pinned** nefcon release into the bundle. |
| `install-sudovda.ps1` | Runs at install time (elevated): trust cert → gated device-node create → `pnputil` install. |
| `stage-pf-vdisplay.ps1` | Stage the **vendored** pf-vdisplay driver + fetch/verify the **pinned** nefcon release into the bundle. |
| `install-pf-vdisplay.ps1` | Runs at install time (elevated): trust cert → gated device-node create (nefconc)`pnputil` install. |
| `../../scripts/windows/web-run.cmd` | The `PunktfunkWeb` task action: loads the mgmt token + login password env, runs the bundled `bun` on the Nitro server (`:3000`). |
| `../../scripts/windows/web-setup.ps1` | Install-time (elevated): write the ACL'd console password, register the `PunktfunkWeb` task + firewall rule, start it. |
| `sudovda/` | **Vendored** prebuilt SudoVDA driver: `SudoVDA.inf` / `sudovda.cat` / `SudoVDA.dll` / `sudovda.cer`. |
| `pf-vdisplay/` | **Vendored** signed pf-vdisplay driver: `pf_vdisplay.inf` / `pf_vdisplay.cat` / `pf_vdisplay.dll` / `punktfunk-driver.cer`. Built from `vdisplay-driver/`. |
| `vdisplay-driver/` | The all-Rust IddCx **driver source** (`pf-vdisplay` crate + vendored `wdf-umdf*` bindings) + `deploy-dev.ps1` (build/sign/install for dev). |
| `nvenc/nvenc.def`, `nvenc/gen-nvenc-importlib.ps1` | Synthesise `nvencodeapi.lib` for the `--features nvenc` link (llvm-dlltool / lib.exe). |
> **Vendored driver:** SudoVDA has no upstream release (its repo is a source-only VS solution; Apollo
> embeds the driver in its own installer), so the prebuilt **signed** driver is checked in under
> `sudovda/` (MIT/CC0; v1.10.9.289, signer `CN=sudovda@su.mk`, Class=Display, HWID
> `Root\SudoMaker\SudoVDA`). To refresh it, copy the four files out of a box's driver store
> (`C:\Windows\System32\DriverStore\FileRepository\sudovda.inf_amd64_*`) and re-derive `sudovda.cer`
> from the `.cat` signer (`(Get-AuthenticodeSignature sudovda.cat).SignerCertificate | Export-Certificate`).
> nefcon (the device-node tool) **is** fetched + SHA-256-verified from its pinned release in
> `stage-sudovda.ps1`.
> **Vendored driver:** pf-vdisplay is our **all-Rust IddCx** virtual display (UMDF2), built from
> `packaging/windows/vdisplay-driver/`. It replaced the vendored SudoVDA C++ driver — full story in
> [`docs/windows-virtual-display-rust-port.md`](../../docs/windows-virtual-display-rust-port.md). The
> **signed** output (`pf_vdisplay.dll`/`.inf`/`.cat` + `punktfunk-driver.cer`; signer
> `punktfunk-ds-test` — the same cert the gamepad drivers ship, Class=Display, HWID `root\pf_vdisplay`)
> is checked in under `pf-vdisplay/`. To refresh it after a driver-source change, rebuild + re-sign with
> `vdisplay-driver/deploy-dev.ps1` and copy the staged `pf_vdisplay.{dll,inf,cat}` over the vendored
> copies. nefcon (the device-node tool — the install creates the node with it, **never** `devgen`, which
> leaves persistent phantom devices) **is** fetched + SHA-256-verified from its pinned release in
> `stage-pf-vdisplay.ps1`.
## Build locally (Windows, MSVC + Windows SDK + Inno Setup)
@@ -91,7 +96,7 @@ $env:PUNKTFUNK_NVENC_LIB_DIR = 'C:\t\nvenc'
# 2. build the host
cargo build --release -p punktfunk-host --features nvenc
# 3. pack (self-signed unless MSIX_CERT_PFX_B64/MSIX_CERT_PASSWORD are set; -NoDriver to skip SudoVDA)
# 3. pack (self-signed unless MSIX_CERT_PFX_B64/MSIX_CERT_PASSWORD are set; -NoDriver to skip pf-vdisplay)
pwsh -File packaging\windows\pack-host-installer.ps1 -Version 0.0.0-dev -TargetDir C:\t\release -OutDir C:\t\out
```
@@ -1,32 +1,35 @@
<#
.SYNOPSIS
Install the bundled SudoVDA virtual-display driver. Runs ELEVATED at setup time (invoked from the
installer's [Run] section). Best-effort: warns and exits 0 on any failure the host degrades to a
physical display without SudoVDA, so a driver hiccup must never abort the whole install.
Install the bundled pf-vdisplay (punktfunk) virtual-display driver our all-Rust IddCx replacement
for SudoVDA. Runs ELEVATED at setup time (invoked from the installer's [Run] section). Best-effort:
warns and exits 0 on any failure the host degrades to a physical display without a virtual display,
so a driver hiccup must never abort the whole install.
.DESCRIPTION
-Dir holds the staged payload from fetch-sudovda.ps1 (the .inf/.cat/.dll + signing .cer + nefconc.exe).
Steps:
1. Trust the self-signed driver cert (machine Root + TrustedPublisher) so PnP installs it silently.
2. Create the root device node IF ABSENT (gated a blind re-create spawns a phantom duplicate, and
-Dir holds the staged payload (pf_vdisplay.inf/.cat/.dll + signing .cer + nefconc.exe). Steps:
1. Trust the self-signed driver cert (machine Root + TrustedPublisher) so PnP installs it silently
(the same punktfunk-ds-test cert the gamepad drivers ship).
2. Create the ROOT device node IF ABSENT (gated a blind re-create spawns a phantom duplicate, and
the host's open_device() binds interface index 0; crates/punktfunk-host/src/vdisplay/sudovda.rs).
ALWAYS via nefconc (a clean ROOT\DISPLAY node) NEVER devgen, which makes persistent SWD\DEVGEN
software devices that survive reboot + registry deletion and resurrect on every driver install.
3. Stage + bind the driver (pnputil /add-driver /install modern, in-box, idempotent).
Class/ClassGuid are read from the .inf so they always match the shipped driver.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File install-sudovda.ps1 -Dir C:\path\to\sudovda
powershell -ExecutionPolicy Bypass -File install-pf-vdisplay.ps1 -Dir C:\path\to\pf-vdisplay
#>
[CmdletBinding()]
param(
[string]$Dir = $PSScriptRoot,
[string]$HardwareId = 'root\sudomaker\sudovda' # verified live (docs/windows-host.md)
[string]$HardwareId = 'root\pf_vdisplay' # matches pf_vdisplay.inf [Standard.NTamd64]
)
# Never abort the installer on a driver failure.
$ErrorActionPreference = 'Continue'
trap { Write-Warning "SudoVDA install error: $_"; exit 0 }
trap { Write-Warning "pf-vdisplay install error: $_"; exit 0 }
function Test-SudoVdaPresent {
function Test-PfVdisplayPresent {
$devs = Get-PnpDevice -Class Display -PresentOnly -ErrorAction SilentlyContinue
foreach ($d in $devs) {
$hw = (Get-PnpDeviceProperty -InstanceId $d.InstanceId -KeyName 'DEVPKEY_Device_HardwareIds' `
@@ -36,14 +39,14 @@ function Test-SudoVdaPresent {
return $false
}
$inf = Get-ChildItem -Path $Dir -Filter *.inf -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
$inf = Get-ChildItem -Path $Dir -Filter pf_vdisplay.inf -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
$cer = Get-ChildItem -Path $Dir -Filter *.cer -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
$nef = Get-ChildItem -Path $Dir -Filter 'nefconc.exe' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $inf) { Write-Warning "no SudoVDA .inf in $Dir; skipping driver install."; exit 0 }
Write-Host "SudoVDA inf: $($inf.FullName)"
if (-not $inf) { Write-Warning "no pf_vdisplay.inf in $Dir; skipping driver install."; exit 0 }
Write-Host "pf-vdisplay inf: $($inf.FullName)"
# 1) Trust the self-signed driver cert (a self-signed driver needs the cert in BOTH the machine
# Root store, so the chain validates, and TrustedPublisher, so PnP installs without a prompt).
# 1) Trust the self-signed driver cert (a self-signed driver needs the cert in BOTH the machine Root
# store, so the chain validates, and TrustedPublisher, so PnP installs without a prompt).
if ($cer) {
Write-Host "==> importing $($cer.Name) to Root + TrustedPublisher"
certutil.exe -addstore -f Root "$($cer.FullName)" | Out-Null
@@ -51,9 +54,9 @@ if ($cer) {
}
else { Write-Warning "no .cer in $Dir — driver may not install silently (untrusted publisher)" }
# 2) Create the root device node only if it isn't already there.
if (Test-SudoVdaPresent) {
Write-Host "SudoVDA device node already present — leaving it as-is."
# 2) Create the root device node only if it isn't already there. nefconc, NEVER devgen.
if (Test-PfVdisplayPresent) {
Write-Host "pf-vdisplay device node already present — leaving it as-is."
}
elseif ($nef) {
$infText = Get-Content -Raw $inf.FullName
@@ -66,7 +69,7 @@ elseif ($nef) {
Write-Warning "nefconc --create-device-node returned $LASTEXITCODE"
}
}
else { Write-Warning "nefconc.exe not found in $Dir — cannot create the SudoVDA device node." }
else { Write-Warning "nefconc.exe not found in $Dir — cannot create the pf-vdisplay device node." }
# 3) Stage + bind the driver (idempotent; re-staging the same .inf is harmless).
Write-Host "==> pnputil /add-driver $($inf.Name) /install"
+9 -7
View File
@@ -7,7 +7,7 @@
1. resolves a code-signing cert (supplied stable .pfx from CI secrets OR an ephemeral self-signed
CN=unom — same scheme as the client's pack-msix.ps1) and exports the public .cer,
2. signs the inner punktfunk-host.exe,
3. fetches + stages the SudoVDA driver bundle (unless -NoDriver),
3. stages the pf-vdisplay virtual-display driver bundle (unless -NoDriver),
4. runs ISCC to build punktfunk-host-setup-<ver>.exe,
5. signs the setup.exe (timestamp best-effort),
6. emits HOST_SETUP_PATH / HOST_CER_PATH to GITHUB_ENV for the publish step.
@@ -28,7 +28,7 @@ param(
[string]$FfmpegDir = $env:FFMPEG_DIR, # bundle its bin\*.dll (amf-qsv build)
[string]$WebDir = $env:WEB_OUTPUT_DIR, # built web .output tree -> bundle the mgmt console
[string]$BunExe = $env:BUN_EXE, # portable bun.exe runtime for the console
[switch]$NoDriver, # build without the bundled SudoVDA driver
[switch]$NoDriver, # build without the bundled pf-vdisplay driver
[switch]$NoSign # skip signing (local debug)
)
$ErrorActionPreference = 'Stop'
@@ -140,17 +140,19 @@ $defines = @(
"/DReadme=$readme"
)
# --- stage the SudoVDA driver bundle ----------------------------------------------------------
# --- stage the pf-vdisplay virtual-display driver bundle --------------------------------------
# pf-vdisplay is our all-Rust IddCx driver (packaging/windows/vdisplay-driver/), vendored signed under
# packaging/windows/pf-vdisplay/. It replaced the vendored SudoVDA C++ driver.
if (-not $NoDriver) {
$stage = Join-Path $OutDir 'stage'
& (Join-Path $here 'stage-sudovda.ps1') -OutDir $stage
Copy-Item (Join-Path $here 'install-sudovda.ps1') (Join-Path $stage 'install-sudovda.ps1') -Force
& (Join-Path $here 'stage-pf-vdisplay.ps1') -OutDir $stage
Copy-Item (Join-Path $here 'install-pf-vdisplay.ps1') (Join-Path $stage 'install-pf-vdisplay.ps1') -Force
$defines += "/DStageDir=$stage"
}
else { Write-Host "-NoDriver: building installer WITHOUT the bundled SudoVDA driver" }
else { Write-Host "-NoDriver: building installer WITHOUT the bundled pf-vdisplay driver" }
# --- stage the punktfunk virtual-gamepad UMDF drivers (DualSense/DS4 + Xbox 360 XUSB) ----------
# Vendored, pre-signed under packaging/windows/gamepad-drivers/ (like SudoVDA). Rebuild + re-vendor
# Vendored, pre-signed under packaging/windows/gamepad-drivers/ (like pf-vdisplay). Rebuild + re-vendor
# from packaging/windows/{dualsense,xusb}-driver/ when the driver source changes (see their READMEs).
if (-not $NoDriver) {
$gpVendor = Join-Path $here 'gamepad-drivers'
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,78 @@
;/*++
; 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 = 06/22/2026,1.0.0622.2210
[Manufacturer]
%ManufacturerName%=Standard,NTamd64
[Standard.NTamd64]
%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=2.15.0
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
Binary file not shown.
+7 -7
View File
@@ -1,7 +1,7 @@
; punktfunk host installer (Inno Setup 6).
;
; Produces a signed setup.exe that lays the host into Program Files, optionally installs the bundled
; SudoVDA virtual-display driver, and DELEGATES service registration to `punktfunk-host service
; pf-vdisplay virtual-display driver, and DELEGATES service registration to `punktfunk-host service
; install`. The real, idempotent install logic (SCM registration, firewall rules, default host.env,
; the SYSTEM->interactive-session CreateProcessAsUserW supervisor for secure-desktop capture) lives in
; crates/punktfunk-host/src/service.rs - this script does NOT duplicate it. That SYSTEM service model
@@ -36,7 +36,7 @@
#ifndef WebSetup
#define WebSetup "..\..\scripts\windows\web-setup.ps1"
#endif
; StageDir (the staged SudoVDA payload + nefconc.exe + install-sudovda.ps1) is optional.
; StageDir (the staged pf-vdisplay payload + nefconc.exe + install-pf-vdisplay.ps1) is optional.
#ifdef StageDir
#define WithDriver
#endif
@@ -84,7 +84,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
#ifdef WithDriver
Name: "installdriver"; Description: "Install the SudoVDA virtual display driver (required for native-resolution streaming)"
Name: "installdriver"; Description: "Install the pf-vdisplay virtual display driver (required for native-resolution streaming)"
#endif
#ifdef WithGamepad
Name: "installgamepad"; Description: "Install the virtual gamepad drivers (DualSense / DualShock 4 / Xbox 360 — no ViGEmBus needed)"
@@ -112,8 +112,8 @@ Source: "{#WebRunCmd}"; DestDir: "{app}\web"; DestName: "web-run.cmd"; Flags: ig
Source: "{#WebSetup}"; DestDir: "{tmp}"; DestName: "web-setup.ps1"; Flags: deleteafterinstall
#endif
#ifdef WithDriver
; The driver payload + nefconc.exe + install-sudovda.ps1, extracted to {tmp} and removed after install.
Source: "{#StageDir}\*"; DestDir: "{tmp}\sudovda"; Flags: deleteafterinstall recursesubdirs createallsubdirs; Tasks: installdriver
; The driver payload + nefconc.exe + install-pf-vdisplay.ps1, extracted to {tmp} and removed after install.
Source: "{#StageDir}\*"; DestDir: "{tmp}\pfvdisplay"; Flags: deleteafterinstall recursesubdirs createallsubdirs; Tasks: installdriver
#endif
#ifdef WithGamepad
; The vendored UMDF gamepad drivers + install-gamepad-drivers.ps1, extracted to {tmp}, removed after.
@@ -123,8 +123,8 @@ Source: "{#GamepadStageDir}\*"; DestDir: "{tmp}\gamepad"; Flags: deleteafterinst
[Run]
#ifdef WithDriver
Filename: "powershell.exe"; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{tmp}\sudovda\install-sudovda.ps1"" -Dir ""{tmp}\sudovda"""; \
StatusMsg: "Installing the SudoVDA virtual display driver..."; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{tmp}\pfvdisplay\install-pf-vdisplay.ps1"" -Dir ""{tmp}\pfvdisplay"""; \
StatusMsg: "Installing the pf-vdisplay virtual display driver..."; \
Flags: runhidden waituntilterminated; Tasks: installdriver
#endif
#ifdef WithGamepad
@@ -1,25 +1,28 @@
<#
.SYNOPSIS
Stage the driver bundle the installer ships into -OutDir: the VENDORED SudoVDA driver + the
fetched nefcon device tool.
Stage the pf-vdisplay driver bundle the installer ships into -OutDir: the VENDORED signed pf-vdisplay
driver + the fetched nefcon device tool.
.DESCRIPTION
SudoVDA has no upstream release (its repo is a source-only VS solution; Apollo embeds the driver in
its single installer), so the prebuilt, signed driver is VENDORED in this repo under
packaging/windows/sudovda/ (MIT/CC0; SudoVDA v1.10.9.289, signer CN=sudovda@su.mk, Class=Display,
HWID Root\SudoMaker\SudoVDA). nefcon DOES publish a pinned release, so we fetch + SHA-256-verify it
(it provides nefconc.exe, used to create the root-enumerated device node pnputil can't).
pf-vdisplay (our all-Rust IddCx virtual display) is built from packaging/windows/vdisplay-driver/, and
the SIGNED output (pf_vdisplay.dll/.inf/.cat + punktfunk-driver.cer) is VENDORED under
packaging/windows/pf-vdisplay/ (signer punktfunk-ds-test shared with the gamepad drivers Class=
Display, HWID root\pf_vdisplay). Rebuild + re-vendor with
packaging/windows/vdisplay-driver/deploy-dev.ps1 when the driver source changes, then copy the staged
pf_vdisplay.{dll,inf,cat} over the vendored copies. nefcon publishes a pinned release, so we fetch +
SHA-256-verify it (it provides nefconc.exe, used to create the root-enumerated device node pnputil
can't).
Output (consumed by punktfunk-host.iss): -OutDir gets SudoVDA.inf/.cat/.dll + sudovda.cer and
nefconc.exe (x64). pack-host-installer.ps1 also drops install-sudovda.ps1 in.
Output (consumed by punktfunk-host.iss): -OutDir gets pf_vdisplay.inf/.cat/.dll + punktfunk-driver.cer
and nefconc.exe (x64). pack-host-installer.ps1 also drops install-pf-vdisplay.ps1 in.
.EXAMPLE
pwsh -File stage-sudovda.ps1 -OutDir C:\t\out\stage
pwsh -File stage-pf-vdisplay.ps1 -OutDir C:\t\out\stage
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$OutDir,
[string]$VendorDir = (Join-Path $PSScriptRoot 'sudovda'),
[string]$VendorDir = (Join-Path $PSScriptRoot 'pf-vdisplay'),
# PINNED nefcon release (https://github.com/nefarius/nefcon/releases). MIT-licensed.
[string]$NefconUrl = 'https://github.com/nefarius/nefcon/releases/download/v1.17.40/nefcon_v1.17.40.zip',
[string]$NefconSha256 = '812bae7ed7dfb7d6d2284bc7de2f8ccebc92ed2a0b1ae893c53b337096e50c1a'
@@ -31,11 +34,11 @@ $PSNativeCommandUseErrorActionPreference = $false
if (Test-Path $OutDir) { Remove-Item -Recurse -Force $OutDir }
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
# --- vendored SudoVDA driver ------------------------------------------------------------------
$inf = Get-ChildItem -Path $VendorDir -Filter *.inf -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $inf) { throw "no vendored SudoVDA .inf under $VendorDirsee packaging/windows/README.md" }
# --- vendored pf-vdisplay driver --------------------------------------------------------------
$inf = Get-ChildItem -Path $VendorDir -Filter pf_vdisplay.inf -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $inf) { throw "no vendored pf_vdisplay.inf under $VendorDirre-vendor via vdisplay-driver/deploy-dev.ps1" }
Copy-Item (Join-Path $VendorDir '*') $OutDir -Force
Write-Host "==> vendored SudoVDA staged from $VendorDir"
Write-Host "==> vendored pf-vdisplay staged from $VendorDir"
# --- nefcon (fetched + verified) --------------------------------------------------------------
$work = Join-Path ([IO.Path]::GetTempPath()) ('nefcon-' + [IO.Path]::GetRandomFileName())
@@ -51,7 +54,7 @@ try {
}
Write-Host " sha256 ok ($got)"
}
else { Write-Warning "no pinned nefcon SHA-256 — computed $got (PIN THIS in stage-sudovda.ps1)" }
else { Write-Warning "no pinned nefcon SHA-256 — computed $got (PIN THIS in stage-pf-vdisplay.ps1)" }
Expand-Archive -Path $zip -DestinationPath $work -Force
$nefc = Get-ChildItem -Path $work -Recurse -Filter 'nefconc.exe' |
Where-Object { $_.FullName -match '(?i)\\x64\\' } | Select-Object -First 1
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
[build]
rustflags = ["-C", "target-feature=+crt-static"]
@@ -0,0 +1,3 @@
/target
*.cer
*.pfx
+510
View File
@@ -0,0 +1,510 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "bindgen"
version = "0.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pf-vdisplay"
version = "0.1.0"
dependencies = [
"anyhow",
"bytemuck",
"log",
"thiserror",
"wdf-umdf",
"wdf-umdf-sys",
"windows",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wdf-umdf"
version = "0.1.0"
dependencies = [
"paste",
"thiserror",
"wdf-umdf-sys",
]
[[package]]
name = "wdf-umdf-sys"
version = "0.1.0"
dependencies = [
"bindgen",
"bytemuck",
"paste",
"thiserror",
"winreg",
]
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys",
]
@@ -0,0 +1,26 @@
# pf-vdisplay — punktfunk Windows virtual display (IddCx), in Rust.
#
# A self-contained driver workspace (NOT built on windows-drivers-rs like the gamepad drivers — IddCx
# functions are direct IddCxStub exports the WDF function-table macro can't reach, so a unified bindgen
# is the cleaner base). The wdf-umdf-sys / wdf-umdf binding crates are vendored from MolotovCherry's
# MIT-licensed virtual-display-rs (see LICENSE.virtual-display-rs); pf-vdisplay is our driver, swapping
# its named-pipe IPC for the SudoVDA-compatible IOCTL control plane our host already speaks.
[workspace]
resolver = "2"
members = ["wdf-umdf-sys", "wdf-umdf", "pf-vdisplay"]
[profile.release]
strip = true
codegen-units = 1
lto = true
[workspace.lints.rust]
unsafe_op_in_unsafe_fn = "deny"
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
multiple_unsafe_ops_per_block = "deny"
ignored_unit_patterns = "allow"
missing_errors_doc = "allow"
module_inception = "allow"
module_name_repetitions = "allow"
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Cherry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,61 @@
# pf-vdisplay — punktfunk Windows virtual display (Rust IddCx)
P1 of replacing the vendored **SudoVDA** C++ driver with one we own — a pure-Rust UMDF2 **IddCx**
(Indirect Display Driver) virtual display, drop-in compatible with the host's existing
`vdisplay/sudovda.rs` IOCTL control plane. Full rationale + roadmap:
[`docs/windows-virtual-display-rust-port.md`](../../../docs/windows-virtual-display-rust-port.md).
## Layout
```
vdisplay-driver/
wdf-umdf-sys/ VENDORED bindgen FFI to WDF + IddCx (links WdfDriverStubUm + IddCxStub)
wdf-umdf/ VENDORED safe wrappers (IddCx*/Wdf*)
pf-vdisplay/ OUR driver crate (cdylib) + pf_vdisplay.inx
```
`wdf-umdf-sys` / `wdf-umdf` are vendored from [MolotovCherry/virtual-display-rs](https://github.com/MolotovCherry/virtual-display-rs)
(MIT — see `LICENSE.virtual-display-rs`). They're a self-contained bindgen over the WDK, **not**
`windows-drivers-rs` (which the gamepad drivers use): IddCx functions are direct `IddCxStub` exports the
WDF function-table macro can't reach, so a unified bindgen is the cleaner base. Local fix carried in
`wdf-umdf-sys/build.rs`: resolve the `IddCxStub` lib path by the SDK version that actually contains
`um\x64\iddcx\<ver>` (a newer base SDK alongside the WDK has `um\x64` but no `iddcx`).
## Status
- **Scaffold builds** ✅ — workspace + vendored bindings + `pf-vdisplay` compile in-tree to
`pf_vdisplay.dll`. The reference (virtual-display-rs) was separately built + installed + loaded
(Status OK) on the RTX box, proving the IddCx-in-Rust chain end to end.
- **Next (P1 driver logic):** port the IddCx driver — `DriverEntry``IDD_CX_CLIENT_CONFIG`
(adapter-init / parse-monitor-description / query-target-modes / assign-swapchain) → device + monitor
context, generic EDID, no-op swap-chain drain (DDA still captures for P1). Logging via
`OutputDebugString` (no `log`/`driver-logger`/`tokio`).
- **Then (control plane):** the SudoVDA-compatible IOCTL surface on the control device
(`ADD`/`REMOVE`/`PING`/`GET_WATCHDOG`/`GET_VERSION`/`SET_RENDER_ADAPTER`, byte-identical structs +
the `{e5bcc234-…}` interface GUID) so `vdisplay/sudovda.rs` drives it **unchanged**; a default mode
table + the per-`ADD` client mode injected as preferred; the watchdog.
- **Later (P2):** push swap-chain frames straight to the host (skip DDA); HDR via the IddCx 1.11 D3D12
acquire path.
## Build
Needs the WDK (UMDF 2.31 + IddCx stubs), LLVM/clang (`LIBCLANG_PATH`), and the pinned
`nightly-2024-07-26` (auto-selected via `rust-toolchain.toml`). From `pf-vdisplay/`, inside an MSVC dev
shell:
```
set LIBCLANG_PATH=C:\Program Files\LLVM\bin
cargo build # -> ../target/x86_64-pc-windows-msvc/debug/pf_vdisplay.dll
```
## Sign + install (same recipe as the gamepad drivers)
1. (no FORCE_INTEGRITY bit to clear — this crate doesn't set `/INTEGRITYCHECK`)
2. `signtool sign /fd SHA256 /sha1 <punktfunk-ds-test thumbprint>` the renamed `pf_vdisplay.dll`
3. `stampinf -f pf_vdisplay.inf -d * -a amd64 -u 2.15.0 -v <ver>` ; `Inf2Cat /driver:<dir> /os:10_X64` ;
sign the `.cat`
4. `pnputil /add-driver pf_vdisplay.inf` ; create the root devnode (`nefconc --create-device-node
--hardware-id root\pf_vdisplay --class-name Display --class-guid {4d36e968-…}`, mirroring
`install-sudovda.ps1`)
Bundles into the Inno Setup installer the same way as `gamepad-drivers/` once the driver is functional.
@@ -0,0 +1,77 @@
#requires -Version 5.1
<#
.SYNOPSIS
Stage, sign, and (optionally) install the pf-vdisplay UMDF IddCx driver for local dev/test.
.DESCRIPTION
Copies the freshly built pf_vdisplay.dll into the stage dir, signs it with the self-signed test cert,
stamps a STRICTLY INCREASING DriverVer (date.time) into the INF, then generates and signs the catalog.
With -Install it also pnputil /add-driver's the package and creates the Root\pf_vdisplay devnode.
Why the DriverVer bump matters: `pnputil /add-driver /install` only replaces an already-installed
driver binary when the INF DriverVer is higher than the installed one. Re-deploying without a bump
silently keeps the OLD binary loaded — a trap that masks code changes during iteration.
Build first: from pf-vdisplay/, in an MSVC dev shell with LIBCLANG_PATH set, run `cargo build`.
NOTE: pf-vdisplay and SudoVDA register the SAME control-interface GUID, so only one may be active at a
time. On the dev box, disable/remove the SudoVDA devnode before installing pf-vdisplay (and restore it
after). This script does not touch SudoVDA.
.PARAMETER Install
Also add the driver package to the store and create the root devnode.
#>
[CmdletBinding()]
param(
[string]$Stage = 'C:\Users\Public\pfvd-stage',
[string]$Thumbprint = '6A52984E54376C45A1C236B1A2C8A746C5AB6131',
[string]$Nefconc = 'C:\Users\Public\virtual-display-rs\installer\files\nefconc.exe',
[switch]$Install
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$dll = Join-Path $root 'target\x86_64-pc-windows-msvc\debug\pf_vdisplay.dll'
$inx = Join-Path $root 'pf-vdisplay\pf_vdisplay.inx'
if (-not (Test-Path $dll)) { throw "driver not built: $dll (run cargo build in pf-vdisplay/ first)" }
$kits = 'C:\Program Files (x86)\Windows Kits\10\bin'
function Find-Tool([string]$name, [string]$arch) {
(Get-ChildItem "$kits\*\$arch\$name" -EA SilentlyContinue | Sort-Object FullName | Select-Object -Last 1).FullName
}
$signtool = Find-Tool 'signtool.exe' 'x64'
$stampinf = Find-Tool 'stampinf.exe' 'x64'
$inf2cat = Find-Tool 'Inf2Cat.exe' 'x86'
if (-not $signtool) { throw 'signtool.exe not found in the WDK' }
if (-not $stampinf) { throw 'stampinf.exe not found in the WDK' }
if (-not $inf2cat) { throw 'Inf2Cat.exe not found in the WDK' }
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
$stagedDll = Join-Path $Stage 'pf_vdisplay.dll'
$stagedInf = Join-Path $Stage 'pf_vdisplay.inf'
$stagedCat = Join-Path $Stage 'pf_vdisplay.cat'
Copy-Item $dll $stagedDll -Force
Copy-Item $inx $stagedInf -Force # stampinf rewrites this copy in place
# DriverVer date+time — must strictly increase each deploy or pnputil keeps the old binary.
$now = Get-Date
$ver = '1.0.{0}.{1}' -f $now.ToString('MMdd'), $now.ToString('HHmm')
& $signtool sign /fd SHA256 /sha1 $Thumbprint $stagedDll | Out-Null
& $stampinf -f $stagedInf -d '*' -a 'amd64' -u '2.15.0' -v $ver | Out-Null
& $inf2cat /driver:$Stage /os:10_X64 /uselocaltime | Out-Null
& $signtool sign /fd SHA256 /sha1 $Thumbprint $stagedCat | Out-Null
Write-Host "staged + signed pf-vdisplay DriverVer=$ver"
if ($Install) {
& pnputil /add-driver $stagedInf /install | Out-Null
$present = Get-PnpDevice -EA SilentlyContinue |
Where-Object { $_.InstanceId -match 'PF_VDISPLAY' -or $_.FriendlyName -match 'punktfunk Virtual Display' }
if (-not $present) {
if (-not (Test-Path $Nefconc)) { throw "nefconc not found: $Nefconc" }
& $Nefconc --create-device-node --hardware-id 'root\pf_vdisplay' --class-name Display --class-guid '{4d36e968-e325-11ce-bfc1-08002be10318}' | Out-Null
Start-Sleep 2
& pnputil /add-driver $stagedInf /install | Out-Null
}
Write-Host 'installed pf-vdisplay (root\pf_vdisplay devnode)'
}
@@ -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,30 @@
[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_Memory",
"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,532 @@
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_PATH,
IDDCX_MONITOR_MODE, IDDCX_MONITOR_MODE_ORIGIN, IDDCX_MONITOR__, IDDCX_TARGET_MODE, NTSTATUS,
WDFDEVICE, WDF_POWER_DEVICE_STATE,
};
// IddCx 1.10 *2 DDIs (HDR-capable). For B1 we advertise SDR (8 bpc) so behaviour is unchanged; B2
// flips the bit depth + adapter flag to enable HDR.
use wdf_umdf_sys::{
IDARG_IN_COMMITMODES2, IDARG_IN_PARSEMONITORDESCRIPTION2, IDARG_IN_QUERYTARGETMODES2,
IDARG_IN_QUERYTARGET_INFO, IDARG_OUT_QUERYTARGET_INFO, IDDCX_BITS_PER_COMPONENT, IDDCX_MONITOR_MODE2,
IDDCX_PATH2, IDDCX_TARGET_CAPS, IDDCX_TARGET_MODE2, IDDCX_WIRE_BITS_PER_COMPONENT,
};
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 {
info!("GET_DEFAULT_MODES called (we return NOT_IMPLEMENTED — only valid for a monitor with NO EDID)");
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 {
// DIAGNOSTIC: does the OS commit an ACTIVE path for our monitor? IDDCX_PATH_FLAGS_ACTIVE = 2. If
// no active path is ever committed, the OS never calls ASSIGN_SWAPCHAIN (the bug we're chasing).
let in_args = unsafe { &*p_in_args };
info!("COMMIT_MODES: path_count={}", in_args.PathCount);
for i in 0..in_args.PathCount {
let path: &IDDCX_PATH = unsafe { &*in_args.pPaths.add(i as usize) };
let active = (path.Flags.0 & 2) != 0;
info!(
" path[{i}] monitor={:p} flags=0x{:x} active={active}",
path.MonitorObject, path.Flags.0
);
}
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()
}
}
// ===== IddCx 1.10 *2 DDIs (HDR-capable path) ============================================
// These mirror the 1.x callbacks above but advertise per-mode wire bit-depth. B1 reports SDR (8 bpc);
// B2 bumps `wire_bits()` to add 10 bpc + sets CAN_PROCESS_FP16 to actually enable HDR.
/// Wire bit-depth advertised per mode. B2: advertise BOTH 8 and 10 bpc RGB so the OS offers HDR10
/// modes (the bitfield: 8 = 0x2, 10 = 0x4).
fn wire_bits() -> IDDCX_WIRE_BITS_PER_COMPONENT {
let rgb = IDDCX_BITS_PER_COMPONENT(
IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_8.0
| IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_10.0,
);
IDDCX_WIRE_BITS_PER_COMPONENT {
Rgb: rgb,
YCbCr444: IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE,
YCbCr422: IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE,
YCbCr420: IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE,
}
}
/// 1.10 variant of [`parse_monitor_description`] — writes `IDDCX_MONITOR_MODE2` (adds bit-depth).
pub extern "C-unwind" fn parse_monitor_description2(
p_in_args: *const IDARG_IN_PARSEMONITORDESCRIPTION2,
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 Ok(monitor_index) = Edid::get_serial(edid) else {
error!("bad edid ({} bytes)", 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 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_MODE2>>(),
number_of_modes as usize,
)
};
for (mode, out_mode) in monitor.data.modes.flatten().zip(monitor_modes.iter_mut()) {
out_mode.write(IDDCX_MONITOR_MODE2 {
#[allow(clippy::cast_possible_truncation)]
Size: mem::size_of::<IDDCX_MONITOR_MODE2>() as u32,
Origin: IDDCX_MONITOR_MODE_ORIGIN::IDDCX_MONITOR_MODE_ORIGIN_MONITORDESCRIPTOR,
MonitorVideoSignalInfo: display_info(mode.width, mode.height, mode.refresh_rate),
BitsPerComponent: wire_bits(),
});
}
out_args.PreferredMonitorModeIdx = 0;
NTSTATUS::STATUS_SUCCESS
}
fn target_mode2(width: u32, height: u32, refresh_rate: u32) -> IDDCX_TARGET_MODE2 {
let m1 = target_mode(width, height, refresh_rate);
IDDCX_TARGET_MODE2 {
#[allow(clippy::cast_possible_truncation)]
Size: mem::size_of::<IDDCX_TARGET_MODE2>() as u32,
TargetVideoSignalInfo: m1.TargetVideoSignalInfo,
BitsPerComponent: wire_bits(),
..Default::default()
}
}
/// 1.10 variant of [`monitor_query_modes`] — writes `IDDCX_TARGET_MODE2`.
pub extern "C-unwind" fn monitor_query_modes2(
monitor_object: *mut IDDCX_MONITOR__,
p_in_args: *const IDARG_IN_QUERYTARGETMODES2,
p_out_args: *mut IDARG_OUT_QUERYTARGETMODES,
) -> NTSTATUS {
let Ok(monitors) = MONITOR_MODES.lock() else {
error!("MONITOR_MODES mutex poisoned");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
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();
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_MODE2>>(),
number_of_modes as usize,
)
};
for (mode, out_target) in monitor.data.modes.flatten().zip(out_target_modes.iter_mut()) {
out_target.write(target_mode2(mode.width, mode.height, mode.refresh_rate));
}
}
NTSTATUS::STATUS_SUCCESS
}
/// 1.10 variant of [`adapter_commit_modes`] — `IDDCX_PATH2` carries the committed wire format.
pub extern "C-unwind" fn adapter_commit_modes2(
_adapter_object: *mut IDDCX_ADAPTER__,
p_in_args: *const IDARG_IN_COMMITMODES2,
) -> NTSTATUS {
let in_args = unsafe { &*p_in_args };
info!("COMMIT_MODES2: path_count={}", in_args.PathCount);
for i in 0..in_args.PathCount {
let path: &IDDCX_PATH2 = unsafe { &*in_args.pPaths.add(i as usize) };
let active = (path.Flags.0 & 2) != 0;
info!(
" path2[{i}] monitor={:p} flags=0x{:x} active={active} colorspace={} rgb_bpc=0x{:x}",
path.MonitorObject,
path.Flags.0,
path.WireFormatInfo.ColorSpace.0,
path.WireFormatInfo.BitsPerComponent.Rgb.0
);
}
NTSTATUS::STATUS_SUCCESS
}
/// 1.10 NEW: per-target capabilities. B2 reports `HIGH_COLOR_SPACE` so the OS enables HDR10 (transfer
/// curve + wide gamut) on this target.
pub extern "C-unwind" fn query_target_info(
_adapter_object: *mut IDDCX_ADAPTER__,
_p_in_args: *mut IDARG_IN_QUERYTARGET_INFO,
p_out_args: *mut IDARG_OUT_QUERYTARGET_INFO,
) -> NTSTATUS {
let out_args = unsafe { &mut *p_out_args };
out_args.TargetCaps = IDDCX_TARGET_CAPS::IDDCX_TARGET_CAPS_HIGH_COLOR_SPACE;
out_args.DitheringSupport = IDDCX_WIRE_BITS_PER_COMPONENT::default();
NTSTATUS::STATUS_SUCCESS
}
/// 1.10 NEW (HDR): the OS hands us the default HDR10 static metadata for the monitor. B2 accepts it
/// (the host/client own the final HDR metadata for the stream); B3 will forward it to the host for the
/// HEVC mastering-display SEI. Stub keeps the OS's HDR setup happy.
pub extern "C-unwind" fn set_default_hdr_metadata(
_monitor_object: *mut IDDCX_MONITOR__,
_p_in_args: *const wdf_umdf_sys::IDARG_IN_MONITOR_SET_DEFAULT_HDR_METADATA,
) -> NTSTATUS {
NTSTATUS::STATUS_SUCCESS
}
/// 1.10 HDR: the OS hands us the gamma ramp (a 3x4 colour-space matrix in HDR mode). We do NOT apply it
/// server-side — the host streams the scRGB FP16 and the CLIENT's display applies its own transform —
/// so we accept it. Wiring this is OBLIGATED once CAN_PROCESS_FP16 is set; without it the OS rejects
/// the adapter at init (`IddCxAdapterInitAsync` → "Failed to get adapter").
pub extern "C-unwind" fn set_gamma_ramp(
_monitor_object: *mut IDDCX_MONITOR__,
_p_in_args: *const wdf_umdf_sys::IDARG_IN_SET_GAMMARAMP,
) -> NTSTATUS {
NTSTATUS::STATUS_SUCCESS
}
@@ -0,0 +1,401 @@
use std::{
mem::{self, size_of},
num::{ParseIntError, TryFromIntError},
ptr::{addr_of_mut, NonNull},
sync::{Arc, Mutex},
};
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_ADAPTER_FLAGS, 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;
/// ONE shared D3D render device, reused across every swap-chain assignment (keyed by render LUID).
/// Creating a fresh `Direct3DDevice` per assign — and the swap-chain flap fires several assigns per
/// session — spawned a new NVIDIA UMD worker-thread set each time that was NEVER reclaimed on release
/// (proven on the RTX box: ~70 `nvwgf2umx` threads + ~50 MB VRAM leaked per reconnect, permanently,
/// even though our `Direct3DDevice` refcount dropped to 0). Pooling one device keeps a single, stable
/// thread set: the processors borrow an `Arc`, so the device outlives them and is never re-created.
static DEVICE_POOL: Mutex<Option<(i64, Arc<Direct3DDevice>)>> = Mutex::new(None);
/// Get-or-create the pooled D3D device for `luid`. Re-creates only if the render adapter changes
/// (e.g. a GPU hot-swap), which drops the old `Arc` once its last processor releases it.
fn pooled_device(luid: windows::Win32::Foundation::LUID) -> Option<Arc<Direct3DDevice>> {
let key = (i64::from(luid.HighPart) << 32) | i64::from(luid.LowPart as u32);
let mut pool = DEVICE_POOL.lock().ok()?;
if let Some((k, dev)) = pool.as_ref() {
if *k == key {
return Some(dev.clone());
}
}
match Direct3DDevice::init(luid) {
Ok(d) => {
let a = Arc::new(d);
*pool = Some((key, a.clone()));
Some(a)
}
Err(e) => {
error!("pooled Direct3DDevice::init failed: {e:?}");
None
}
}
}
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>,
/// OS target id (from IddCxMonitorArrival), stamped on this context at creation. assign_swap_chain
/// uses THIS instead of a MONITOR_MODES pointer lookup — the lookup returns 0 for a recreated
/// (session-2+) monitor, which broke the shared-ring naming and cascaded into SetDevice
/// E_INVALIDARG + an access violation (the fix-teardown crash).
target_id: u32,
}
// 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,
// B2 HDR: declare we can process FP16 (scRGB) desktop surfaces — enables HDR10 / SDR WCG.
// This OBLIGATES the *2 mode DDIs (done) + ReleaseAndAcquireBuffer2 (done in run_core).
Flags: IDDCX_ADAPTER_FLAGS::IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16,
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;
}
}
// Stamp the OS target id onto the monitor's CONTEXT so assign_swap_chain reads it directly
// (no MONITOR_MODES pointer lookup, which returns 0 for a recreated monitor).
unsafe {
let _ = MonitorContext::get_mut(monitor_create_out.MonitorObject.cast(), |ctx| {
ctx.target_id = arrival_out.OsTargetId;
});
}
Ok(())
}
}
impl MonitorContext {
pub fn new(device: IDDCX_MONITOR) -> Self {
Self {
device,
swap_chain_processor: None,
target_id: 0,
}
}
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
);
// The OS target id keys the per-monitor shared frame-push objects (header/event/textures) the
// host opens. Read it from THIS context (stamped at creation after IddCxMonitorArrival) — the
// old MONITOR_MODES pointer lookup returned 0 for a recreated (session-2+) monitor, which broke
// the ring naming and cascaded into SetDevice E_INVALIDARG + an access violation.
let target_id = self.target_id;
let device = pooled_device(luid);
if let Some(device) = device {
let mut processor = SwapChainProcessor::new();
processor.run(
swap_chain,
device,
new_frame_event,
target_id,
render_adapter.LowPart,
render_adapter.HighPart,
);
self.swap_chain_processor = Some(processor);
// Cursor is BAKED into the captured video: for IDD-push we deliberately do NOT advertise a
// hardware cursor, so DWM software-composites the mouse cursor into the swapchain surface we
// capture — the client then sees the cursor in the stream. (A future separate-plane cursor
// would re-enable setup_hw_cursor + IddCxMonitorQueryHardwareCursor.) Not advertising one
// also stops leaking a CreateEventA handle per assign.
} else {
// It's important to delete the swap-chain if D3D init fails, so the OS generates a fresh
// swap-chain and retries.
error!("pooled Direct3DDevice unavailable for render LUID — deleting swap chain for OS retry");
unsafe {
let _ = WdfObjectDelete(swap_chain.cast());
}
}
}
pub fn unassign_swap_chain(&mut self) {
let had = self.swap_chain_processor.take().is_some();
error!("unassign_swap_chain (target={}) — dropped live processor: {had}", self.target_id);
}
/// Advertise a HARDWARE cursor. NOT called for IDD-push — we bake the cursor into the video
/// instead (see `assign_swap_chain`). Kept for a future separate-plane cursor (which would pair it
/// with `IddCxMonitorQueryHardwareCursor`). Leaks a `CreateEventA` handle per call, so only wire it
/// back up alongside a real cursor-plane consumer.
#[allow(dead_code)]
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,413 @@
//! 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::sync::Mutex;
use std::thread;
use std::time::{Duration, Instant};
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, MonitorContext};
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);
/// pf-vdisplay extension (NOT in SudoVDA): tear down every monitor. The host issues this on startup to
/// reap monitors orphaned by a crashed/killed previous host instance. SudoVDA returns invalid for it
/// (harmlessly ignored), so the host can send it unconditionally.
const IOCTL_CLEAR_ALL: u32 = ctl(0x804);
const IOCTL_PING: u32 = ctl(0x888);
const IOCTL_GET_VERSION: u32 = ctl(0x8FF);
/// Serializes monitor lifecycle ops — ADD / REMOVE / watchdog-teardown — against each other. Without
/// it, a watchdog expiry can drain an entry out from under an in-flight `do_add` (which releases the
/// `MONITOR_MODES` lock before the slow `create_monitor`), leaving `do_add` to return
/// `STATUS_UNSUCCESSFUL` → the host sees `ERROR_GEN_FAILURE`. This was the reconnect-churn fault.
static MONITOR_OP_LOCK: Mutex<()> = Mutex::new(());
/// A monitor created less than this ago is still in its host-side setup window (CCD commit + GDI-name
/// resolve + topology settle, ~5 s) and is never reaped by the watchdog — only by an explicit
/// CLEAR_ALL. Protects a freshly-born monitor from a transient PING gap during reconnect churn.
const MONITOR_GRACE: Duration = Duration::from_secs(6);
#[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_CLEAR_ALL => {
disconnect_all_monitors(true);
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 {
// Serialize the whole ADD (push entry → create_monitor → verify) against the watchdog teardown +
// REMOVE, so an expiry can never drain this entry mid-flight. `create_monitor` is fast (the slow
// CCD/GDI work is host-side, after this returns), and PING/GET_WATCHDOG don't take this lock, so
// the host keeps the watchdog reset while we hold it.
let _op = MONITOR_OP_LOCK.lock().unwrap();
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,
created_at: Instant::now(),
});
// 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);
// Serialize against ADD + watchdog teardown (lock order: OP_LOCK → MONITOR_MODES).
let _op = MONITOR_OP_LOCK.lock().unwrap();
let mon = {
let mut lock = MONITOR_MODES.lock().unwrap();
match lock.iter().position(|m| m.guid == guid) {
Some(pos) => lock.remove(pos),
None => return NTSTATUS::STATUS_NOT_FOUND,
}
// MONITOR_MODES released here — the processor-join + departure below must not hold it.
};
if let Some(obj) = mon.object {
free_swap_chain_processor(obj.as_ptr());
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("REMOVE: departure failed: {e:?}");
}
}
info!("REMOVE target_id={}", mon.target_id);
NTSTATUS::STATUS_SUCCESS
}
/// Drop a monitor's live swap-chain processor BEFORE departure. The WDF context is an
/// `Arc<RwLock<MonitorContext>>` that WDF frees WITHOUT running Rust `Drop` (no `EvtCleanupCallback`
/// is wired), and the OS does not reliably call UNASSIGN on a host-initiated departure — so the
/// streaming `Direct3DDevice` (its ~dozens of D3D worker threads + tens of MB of VRAM) was orphaned
/// once per session, the dominant reconnect-churn leak. `get_mut` takes the context `RwLock`, so this
/// is safe against a concurrent OS unassign callback (whichever runs second sees `None`).
fn free_swap_chain_processor(monitor: *mut wdf_umdf_sys::IDDCX_MONITOR__) {
// SAFETY: `monitor` is a live IddCx monitor object whose context was init'd at creation.
let r = unsafe { MonitorContext::get_mut(monitor.cast(), |ctx| ctx.unassign_swap_chain()) };
if let Err(e) = r {
error!("free_swap_chain_processor: get_mut FAILED: {e:?}");
}
}
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 monitors. `force` (CLEAR_ALL) reaps EVERYTHING — orphans from a crashed previous host;
/// the watchdog passes `false`, which spares any monitor still inside its creation grace
/// (`MONITOR_GRACE`) so a freshly-born monitor is never reaped mid-setup. Caller MUST hold
/// `MONITOR_OP_LOCK` (lock order: OP_LOCK → MONITOR_MODES). Mirrors SudoVDA's DisconnectAllMonitors.
fn disconnect_all_monitors_locked(force: bool) {
// Drain under the lock (fast); free processors + depart OUTSIDE it (the processor-join blocks).
let to_depart: Vec<MonitorObject> = {
let mut lock = MONITOR_MODES.lock().unwrap();
if lock.is_empty() {
return;
}
let mut keep: Vec<MonitorObject> = Vec::new();
let mut depart: Vec<MonitorObject> = Vec::new();
for mon in lock.drain(..) {
if !force && mon.created_at.elapsed() < MONITOR_GRACE {
keep.push(mon); // still in its host-side setup window — leave it alone
} else {
depart.push(mon);
}
}
*lock = keep;
depart
};
for mon in to_depart {
if let Some(obj) = mon.object {
free_swap_chain_processor(obj.as_ptr());
// SAFETY: `obj` is a live IddCx monitor object.
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("teardown: monitor departure failed: {e:?}");
}
}
}
}
/// Public entry: takes `MONITOR_OP_LOCK`, then tears down. Used by CLEAR_ALL (`force = true`).
fn disconnect_all_monitors(force: bool) {
let _op = MONITOR_OP_LOCK.lock().unwrap();
disconnect_all_monitors_locked(force);
}
/// 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
{
// About to fire. Serialize against do_add/do_remove (so we never tear an entry out from
// under an in-flight ADD), then RE-CHECK the countdown under the lock: if a concurrent
// IOCTL (PING/ADD) reset it while we were acquiring the lock, the host is alive — abort.
let _op = MONITOR_OP_LOCK.lock().unwrap();
if WATCHDOG_COUNTDOWN.load(Ordering::Relaxed) == 0 {
error!("watchdog expired (host stopped pinging) — tearing down stale monitors");
disconnect_all_monitors_locked(false);
}
}
});
}
@@ -0,0 +1,95 @@
use std::sync::atomic::{AtomicI32, Ordering};
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)
}
}
/// DIAGNOSTIC: live `Direct3DDevice` count. Each one holds an `ID3D11Device` whose NVIDIA UMD spawns
/// ~dozens of worker threads; if this climbs without bound across reconnects, devices are leaking.
pub static LIVE_DEVICES: AtomicI32 = AtomicI32::new(0);
#[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,
/// The single (SINGLETHREADED) immediate context — used by the frame-push publisher's
/// `CopyResource` on the swap-chain processor thread (the one thread this device is touched from).
pub 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")?;
let live = LIVE_DEVICES.fetch_add(1, Ordering::Relaxed) + 1;
log::error!("Direct3DDevice::init OK — live D3D devices = {live}");
Ok(Self {
_dxgi_factory: dxgi_factory,
_adapter: adapter,
device,
device_context,
})
}
}
impl Drop for Direct3DDevice {
fn drop(&mut self) {
let live = LIVE_DEVICES.fetch_sub(1, Ordering::Relaxed) - 1;
log::error!("Direct3DDevice::drop — live D3D devices = {live}");
}
}
@@ -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<u8> {
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<u32, TryFromSliceError> {
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);
}
}
@@ -0,0 +1,130 @@
//! 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_commit_modes2, adapter_init_finished, assign_swap_chain,
device_d0_entry, monitor_get_default_modes, monitor_query_modes, monitor_query_modes2,
parse_monitor_description, parse_monitor_description2, query_target_info,
set_default_hdr_metadata, set_gamma_ramp, 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);
// IddCx 1.10 *2 mode DDIs (HDR-capable path). The OS prefers these on 1.10; the 1.x callbacks
// above stay as the down-level fallback. B1 advertises SDR through them (so behaviour is unchanged);
// B2 enables HDR by adding 10 bpc in `wire_bits()`, HIGH_COLOR_SPACE caps, and CAN_PROCESS_FP16.
config.EvtIddCxParseMonitorDescription2 = Some(parse_monitor_description2);
config.EvtIddCxMonitorQueryTargetModes2 = Some(monitor_query_modes2);
config.EvtIddCxAdapterCommitModes2 = Some(adapter_commit_modes2);
config.EvtIddCxAdapterQueryTargetInfo = Some(query_target_info);
config.EvtIddCxMonitorSetDefaultHdrMetaData = Some(set_default_hdr_metadata);
config.EvtIddCxMonitorSetGammaRamp = Some(set_gamma_ramp);
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,424 @@
//! P2 direct frame push — DRIVER side. The restricted WUDFHost token canNOT create named kernel
//! objects (proven on the RTX box: it can't even write a world-writable file), so — exactly like the
//! gamepad UMDF drivers (`crates/punktfunk-host/src/inject/dualsense_windows.rs`: *"the host creates
//! the section, privileged, with a permissive SDDL so the WUDFHost can open it; the driver maps it"*)
//! — the **host** creates the shared header + frame-ready event + ring of keyed-mutex textures, and
//! the driver only **OPENS** them. The driver writes its actual render-adapter LUID + a status code
//! back into the host-created header (our only driver-visibility channel: UMDF hides OutputDebugString
//! in ETW and the token can't write files), then copies each acquired swap-chain surface into the next
//! ring slot and signals the host.
//!
//! Host counterpart: `crates/punktfunk-host/src/capture/idd_push.rs` — [`SharedHeader`], [`MAGIC`],
//! [`RING_LEN`], the driver-status codes and the `Global\` object-name scheme are DUPLICATED
//! byte-identically there.
use std::sync::atomic::{AtomicPtr, AtomicU32, AtomicU64, Ordering};
use log::info;
use windows::core::{Interface, HSTRING};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Graphics::Direct3D11::{
ID3D11Device, ID3D11Device1, ID3D11DeviceContext, ID3D11Texture2D, D3D11_TEXTURE2D_DESC,
};
use windows::Win32::Graphics::Dxgi::IDXGIKeyedMutex;
use windows::Win32::System::Memory::{
MapViewOfFile, OpenFileMappingW, UnmapViewOfFile, FILE_MAP_ALL_ACCESS,
MEMORY_MAPPED_VIEW_ADDRESS,
};
use windows::Win32::System::Threading::{OpenEventW, SetEvent, SYNCHRONIZATION_ACCESS_RIGHTS};
// --- kept byte-identical with the host (idd_push.rs) ---
pub const MAGIC: u32 = 0x4456_4650;
/// Kept for parity with the host's duplicated protocol header (the host writes it).
#[allow(dead_code)]
pub const VERSION: u32 = 1;
/// Ring slots. 6 (was 3) gives ample headroom so this 0 ms-timeout publish always finds a free slot
/// while the host briefly holds one across the convert/copy into its output ring and the depth-2
/// pipelined encode runs. MUST equal the host's `RING_LEN` (idd_push.rs) — both are rebuilt together;
/// a mismatch corrupts the slot mapping.
pub const RING_LEN: u32 = 6;
const DXGI_SHARED_RESOURCE_RW: u32 = 0x8000_0000 | 0x1;
/// SYNCHRONIZE | EVENT_MODIFY_STATE — the driver waits on (no) and SIGNALS the event.
const EVENT_ACCESS: u32 = 0x0010_0000 | 0x0002;
const WAIT_TIMEOUT_HRESULT: i32 = 0x0000_0102;
/// `driver_status` values the driver writes into the host header (the host logs them on a timeout).
/// `NONE` is the host's initial value (kept for parity).
#[allow(dead_code)]
pub const DRV_STATUS_NONE: u32 = 0;
pub const DRV_STATUS_OPENED: u32 = 1;
pub const DRV_STATUS_TEX_FAIL: u32 = 2;
pub const DRV_STATUS_NO_DEVICE1: u32 = 3;
#[repr(C)]
pub struct SharedHeader {
pub magic: u32,
pub version: u32,
pub generation: u32,
pub ring_len: u32,
pub width: u32,
pub height: u32,
pub dxgi_format: u32,
pub _pad: u32,
/// `(seq << 8) | slot` — DRIVER-written after each copy; host loads it `Acquire`.
pub latest: u64,
pub qpc_pts: u64,
/// DRIVER-written: the adapter the swap-chain actually renders on (so the host can detect a
/// mismatch with the textures it created and report it).
pub driver_render_luid_low: u32,
pub driver_render_luid_high: i32,
/// DRIVER-written status (visibility channel).
pub driver_status: u32,
pub driver_status_detail: u32,
}
pub fn hdr_name(target_id: u32) -> String {
format!("Global\\pfvd-hdr-{target_id}")
}
pub fn evt_name(target_id: u32) -> String {
format!("Global\\pfvd-evt-{target_id}")
}
pub fn tex_name(target_id: u32, generation: u32, slot: u32) -> String {
format!("Global\\pfvd-tex-{target_id}-{generation}-{slot}")
}
// --------------------------------------------------------
// ===== Bring-up debug channel (fixed-name, host-created) =====
// UMDF hides the driver's OutputDebugString (ETW) and the restricted token can't write files, so this
// fixed-name `Global\pfvd-dbg` block — created by the host with the permissive SDDL — is how the driver
// reports what it's doing, INDEPENDENT of the per-target header (which is the thing under test). The
// host reads + logs these counters. Duplicated in `idd_push.rs`.
#[repr(C)]
pub struct DebugBlock {
pub magic: u32,
/// ++ each `run_core` entry — proves the swap-chain processor runs at all.
pub run_core_entries: u32,
/// The `target_id` the driver resolved for naming (mismatch vs the host = the bug).
pub resolved_target_id: u32,
/// ++ each header-open attempt.
pub header_open_attempts: u32,
/// Last header-open error (win32/HRESULT).
pub last_open_error: u32,
/// 1 once the driver opened the per-target header.
pub header_opened: u32,
pub render_luid_low: u32,
pub render_luid_high: i32,
/// ++ each acquired swap-chain frame — proves frames flow (or the display is idle).
pub frames_acquired: u32,
pub _pad: u32,
}
static DBG_PTR: AtomicPtr<DebugBlock> = AtomicPtr::new(std::ptr::null_mut());
/// Map the host-created debug block on first use (fixed name). Returns null until the host creates it.
fn dbg_block() -> *mut DebugBlock {
let p = DBG_PTR.load(Ordering::Acquire);
if !p.is_null() {
return p;
}
let Ok(map) = (unsafe {
OpenFileMappingW(FILE_MAP_ALL_ACCESS.0, false, &HSTRING::from("Global\\pfvd-dbg"))
}) else {
return std::ptr::null_mut();
};
let view = unsafe { MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, std::mem::size_of::<DebugBlock>()) };
if view.Value.is_null() {
unsafe {
let _ = CloseHandle(map);
}
return std::ptr::null_mut();
}
let np = view.Value.cast::<DebugBlock>();
match DBG_PTR.compare_exchange(std::ptr::null_mut(), np, Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => np, // we win; intentionally leak the handle (diagnostic, process-lifetime)
Err(existing) => {
unsafe {
let _ = UnmapViewOfFile(view);
let _ = CloseHandle(map);
}
existing
}
}
}
pub fn dbg_run_core_entry() {
let p = dbg_block();
if !p.is_null() {
unsafe {
(*(std::ptr::addr_of_mut!((*p).run_core_entries) as *const AtomicU32))
.fetch_add(1, Ordering::Relaxed);
}
}
}
pub fn dbg_frame() {
let p = dbg_block();
if !p.is_null() {
unsafe {
(*(std::ptr::addr_of_mut!((*p).frames_acquired) as *const AtomicU32))
.fetch_add(1, Ordering::Relaxed);
}
}
}
/// Record the target id + render LUID the driver will use to name the shared objects.
pub fn dbg_set_target(target_id: u32, render_luid_low: u32, render_luid_high: i32) {
let p = dbg_block();
if !p.is_null() {
unsafe {
(*p).resolved_target_id = target_id;
(*p).render_luid_low = render_luid_low;
(*p).render_luid_high = render_luid_high;
}
}
}
/// Record a header-open attempt + its error (0 = success).
pub fn dbg_header_attempt(error: u32, opened: bool) {
let p = dbg_block();
if !p.is_null() {
unsafe {
(*(std::ptr::addr_of_mut!((*p).header_open_attempts) as *const AtomicU32))
.fetch_add(1, Ordering::Relaxed);
(*p).last_open_error = error;
if opened {
(*p).header_opened = 1;
}
}
}
}
struct Slot {
tex: ID3D11Texture2D,
mutex: IDXGIKeyedMutex,
}
/// Publishes acquired swap-chain surfaces into the HOST-created ring. Owned by the swap-chain
/// processor thread; attached lazily once the host has created the shared objects.
pub struct FramePublisher {
context: ID3D11DeviceContext,
map: HANDLE,
header: *mut SharedHeader,
event: HANDLE,
slots: Vec<Slot>,
next: u32,
seq: u64,
/// The host-created ring textures' DXGI format (from the shared header). A swap-chain surface whose
/// format differs (e.g. an FP16 HDR frame vs a BGRA ring) is dropped in `publish` — CopyResource
/// needs matching formats.
ring_format: u32,
/// The ring generation this publisher attached to. The host BUMPS the header generation when it
/// recreates the ring at a new format mid-session (the display's HDR mode flipped) — [`Self::is_stale`]
/// detects that so `run_core` re-attaches to the new-format textures instead of dropping every frame.
generation: u32,
}
// SAFETY: created and used only on the swap-chain processor thread.
unsafe impl Send for FramePublisher {}
impl FramePublisher {
/// Try ONCE to attach to the host-created shared objects. Returns `Err` cheaply if the host hasn't
/// created/published them yet — the drain loop retries periodically, so a non-IDD-push session
/// just keeps draining with no stall.
pub fn try_open(
target_id: u32,
render_luid_low: u32,
render_luid_high: i32,
device: &ID3D11Device,
context: &ID3D11DeviceContext,
) -> windows::core::Result<Self> {
// 1. Open the host-created header (RW). Err if the host hasn't created it yet.
let map = unsafe {
OpenFileMappingW(
FILE_MAP_ALL_ACCESS.0,
false,
&HSTRING::from(hdr_name(target_id)),
)?
};
let view =
unsafe { MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, std::mem::size_of::<SharedHeader>()) };
if view.Value.is_null() {
unsafe {
let _ = CloseHandle(map);
}
return Err(windows::core::Error::from_win32());
}
let header = view.Value.cast::<SharedHeader>();
// 2. Report our render adapter to the host immediately (lets it detect a mismatch).
unsafe {
(*header).driver_render_luid_low = render_luid_low;
(*header).driver_render_luid_high = render_luid_high;
}
// 3. The host sets magic==MAGIC only once the ring textures exist. Not ready → retry later.
let magic =
unsafe { (*(std::ptr::addr_of!((*header).magic) as *const AtomicU32)).load(Ordering::Acquire) };
if magic != MAGIC {
unsafe {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: header.cast() });
let _ = CloseHandle(map);
}
return Err(windows::core::Error::from_win32());
}
let (generation, ring_len) =
unsafe { ((*header).generation, (*header).ring_len.min(RING_LEN)) };
// 4. Open the event (SYNCHRONIZE | EVENT_MODIFY_STATE so we can SetEvent).
let event = match unsafe {
OpenEventW(
SYNCHRONIZATION_ACCESS_RIGHTS(EVENT_ACCESS),
false,
&HSTRING::from(evt_name(target_id)),
)
} {
Ok(e) => e,
Err(e) => {
unsafe {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: header.cast() });
let _ = CloseHandle(map);
}
return Err(e);
}
};
// 5. Open device1 + the ring textures the host created (same render adapter required).
let device1: ID3D11Device1 = match device.cast() {
Ok(d) => d,
Err(e) => {
unsafe {
(*header).driver_status = DRV_STATUS_NO_DEVICE1;
let _ = CloseHandle(event);
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: header.cast() });
let _ = CloseHandle(map);
}
return Err(e);
}
};
let mut slots = Vec::new();
for k in 0..ring_len {
let name = HSTRING::from(tex_name(target_id, generation, k));
let opened: windows::core::Result<ID3D11Texture2D> =
unsafe { device1.OpenSharedResourceByName(&name, DXGI_SHARED_RESOURCE_RW) };
match opened {
Ok(tex) => match tex.cast::<IDXGIKeyedMutex>() {
Ok(mutex) => slots.push(Slot { tex, mutex }),
Err(e) => {
unsafe {
(*header).driver_status = DRV_STATUS_TEX_FAIL;
(*header).driver_status_detail = e.code().0 as u32;
let _ = CloseHandle(event);
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: header.cast() });
let _ = CloseHandle(map);
}
return Err(e);
}
},
Err(e) => {
// Most likely a render-adapter mismatch (the host made the textures on a different
// GPU than the swap-chain renders on). Tell the host so it can report it.
unsafe {
(*header).driver_status = DRV_STATUS_TEX_FAIL;
(*header).driver_status_detail = e.code().0 as u32;
let _ = CloseHandle(event);
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: header.cast() });
let _ = CloseHandle(map);
}
return Err(e);
}
}
}
unsafe {
(*header).driver_status = DRV_STATUS_OPENED;
}
info!("frame-push(driver): attached to host ring gen {generation} ({ring_len} slots)");
Ok(Self {
context: context.clone(),
map,
header,
event,
slots,
next: 0,
seq: 0,
ring_format: unsafe { (*header).dxgi_format },
generation,
})
}
#[inline]
fn latest_cell(&self) -> &AtomicU64 {
unsafe { &*(std::ptr::addr_of!((*self.header).latest) as *const AtomicU64) }
}
/// True once the host has recreated the ring (bumped the header generation) — e.g. the display's
/// HDR mode flipped, so the ring format changed (FP16 ⇄ BGRA) and the texture names now carry a new
/// generation. `run_core` drops the publisher on this so it re-attaches to the new ring.
pub fn is_stale(&self) -> bool {
let cur = unsafe {
(*(std::ptr::addr_of!((*self.header).generation) as *const AtomicU32))
.load(Ordering::Acquire)
};
cur != self.generation
}
/// Copy `surface` into the next free ring slot and signal the host. Never blocks (0 ms try-acquire).
pub fn publish(&mut self, surface: &ID3D11Texture2D) {
let ring_len = self.slots.len() as u32;
if ring_len == 0 {
return;
}
// B2 format guard: CopyResource needs the surface + ring textures to share a DXGI format. Drop
// a frame that doesn't match (e.g. an FP16 HDR surface arriving while the ring is still BGRA,
// before B3 makes the ring FP16) instead of corrupting / failing the copy.
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { surface.GetDesc(&mut desc) };
if desc.Format.0 as u32 != self.ring_format {
return;
}
let start = self.next;
for attempt in 0..ring_len {
let slot = (start + attempt) % ring_len;
let s = &self.slots[slot as usize];
match unsafe { s.mutex.AcquireSync(0, 0) } {
Ok(()) => {
unsafe {
self.context.CopyResource(&s.tex, surface);
let _ = s.mutex.ReleaseSync(0);
}
self.seq = self.seq.wrapping_add(1);
// `latest` = (generation << 40) | (seq << 8) | slot. Stamping the generation lets the
// host REJECT a publish from a stale ring (an old-generation publisher racing the
// host's mid-session ring recreate) so it never consumes an unwritten new-ring slot.
let latest = (u64::from(self.generation) << 40)
| ((self.seq & 0xFFFF_FFFF) << 8)
| u64::from(slot & 0xff);
self.latest_cell().store(latest, Ordering::Release);
unsafe {
let _ = SetEvent(self.event);
}
self.next = (slot + 1) % ring_len;
return;
}
Err(e) if e.code().0 == WAIT_TIMEOUT_HRESULT => continue,
Err(_) => return,
}
}
// All slots busy — drop this frame (never block the swap-chain thread).
}
}
impl Drop for FramePublisher {
fn drop(&mut self) {
self.slots.clear();
unsafe {
if !self.header.is_null() {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: self.header.cast(),
});
}
let _ = CloseHandle(self.event);
let _ = CloseHandle(self.map);
}
}
}
@@ -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,34 @@
//! 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 frame_transport;
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,55 @@
//! Minimal `log` backend that writes to `OutputDebugString` AND tees to a file — UMDF redirects a
//! hosted driver's `OutputDebugString` to ETW (invisible to DebugView), so the file tee is how we
//! actually read driver logs during bring-up. Keeping the `log` facade lets the ported
//! callbacks/context use `error!`/`info!`/`debug!` unchanged.
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::Mutex;
use log::{LevelFilter, Metadata, Record};
use windows::core::PCSTR;
use windows::Win32::System::Diagnostics::Debug::OutputDebugStringA;
/// World-writable so the restricted WUDFHost token can append. Read it during bring-up.
const LOG_PATH: &str = r"C:\Users\Public\pfvd-driver.log";
struct DbgLogger {
file: Mutex<()>,
}
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())) };
// Tee to the file (best-effort): the real channel during bring-up.
let _guard = self.file.lock();
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(LOG_PATH) {
let _ = writeln!(f, "{:<5} {}", record.level(), record.args());
}
}
fn flush(&self) {}
}
static LOGGER: DbgLogger = DbgLogger {
file: Mutex::new(()),
};
pub fn init() {
let _ = log::set_logger(&LOGGER);
log::set_max_level(if cfg!(debug_assertions) {
LevelFilter::Debug
} else {
LevelFilter::Info
});
// Boot marker so each load is distinguishable in the file.
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(LOG_PATH) {
let _ = writeln!(f, "==== pf-vdisplay logger init ====");
}
}
@@ -0,0 +1,111 @@
//! 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 std::time::Instant;
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,
/// When the entry was pushed (`do_add`). The watchdog skips monitors younger than the host's
/// setup window (CCD commit + GDI-name resolve + settle) so a still-initializing monitor is never
/// torn down mid-birth during reconnect churn.
pub created_at: Instant,
}
// 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. 8 s (was
/// 3) gives the host's between-session teardown gap — stop old pinger → CCD display re-attach (a slow
/// `SetDisplayConfig`) → REMOVE — headroom, so the watchdog doesn't spuriously fire during reconnect
/// churn. The host derives its PING interval from this (timeout/3), so it auto-adjusts.
pub static WATCHDOG_TIMEOUT: AtomicU32 = AtomicU32::new(8);
pub static WATCHDOG_COUNTDOWN: AtomicU32 = AtomicU32::new(8);
/// 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,311 @@
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread::{self, JoinHandle},
time::Duration,
};
use log::{debug, error};
use wdf_umdf::{
IddCxSwapChainFinishedProcessingFrame, IddCxSwapChainReleaseAndAcquireBuffer2,
IddCxSwapChainSetDevice, WdfObjectDelete,
};
use wdf_umdf_sys::{
HANDLE, IDARG_IN_RELEASEANDACQUIREBUFFER2, IDARG_IN_SWAPCHAINSETDEVICE,
IDARG_OUT_RELEASEANDACQUIREBUFFER2, IDDCX_SWAPCHAIN, NTSTATUS, WAIT_TIMEOUT, WDFOBJECT,
};
use windows::{
core::{w, Interface},
Win32::{
Foundation::HANDLE as WHANDLE,
Graphics::{
Direct3D11::ID3D11Texture2D,
Dxgi::{IDXGIDevice, IDXGIResource},
},
System::Threading::{
AvRevertMmThreadCharacteristics, AvSetMmThreadCharacteristicsW, WaitForSingleObject,
},
},
};
use crate::{
direct_3d_device::Direct3DDevice,
frame_transport::{
dbg_frame, dbg_header_attempt, dbg_run_core_entry, dbg_set_target, FramePublisher,
},
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: Arc<Direct3DDevice>,
available_buffer_event: HANDLE,
target_id: u32,
render_luid_low: u32,
render_luid_high: i32,
) {
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,
target_id,
render_luid_low,
render_luid_high,
);
error!("run_core RETURNED (target={target_id}) — deleting swap-chain, device drops next");
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,
target_id: u32,
render_luid_low: u32,
render_luid_high: i32,
) {
// P2 direct frame push: lazily ATTACH to the HOST-created shared ring. The restricted UMDF
// token can't create named objects, so the host creates the header + event + textures and we
// only OPEN them once they appear (`try_open`). Until then we just drain — exactly the P1
// behaviour — so a non-IDD-push session never stalls. Retried every ~30 frames.
let mut publisher: Option<FramePublisher> = None;
let mut frames_since_try: u32 = u32::MAX; // attach attempt on the first acquired frame
// Bring-up debug: prove run_core ran + record the target/render LUID we'll name objects with.
dbg_run_core_entry();
dbg_set_target(target_id, render_luid_low, render_luid_high);
// SetDevice fails (0x887A0026, FACILITY_DXGI) when the monitor briefly flaps INACTIVE during
// topology activation — the OS unassigns + re-assigns the swap-chain, and a fresh run_core thread
// can lose the race to the unassign. Retry briefly so a stable re-assign binds the device instead
// of giving up on the first transient failure. `terminate` (set when the OS unassigns + drops the
// processor) breaks us out promptly.
// Cast to IDXGIDevice ONCE and BORROW it to the swap-chain across all retries. The previous
// code re-cast + `into_raw()`'d on EVERY attempt — and a flapping monitor fails several
// attempts per session — so each failure orphaned one IDXGIDevice reference, pinning the D3D
// device so it (and its ~dozen D3D worker threads + tens of MB of VRAM) was NEVER freed when
// the processor dropped. That leaked ~71 threads / ~57 MB VRAM per reconnect until the driver
// choked and sessions fell to 0 bytes. `as_raw()` keeps our single reference (released right
// after the loop); IddCx AddRefs its own on success, and `device` keeps the object alive for
// the drain loop regardless.
let dxgi_device = match device.device.cast::<IDXGIDevice>() {
Ok(d) => d,
Err(e) => {
error!("Failed to cast ID3D11Device to IDXGIDevice: {e:?}");
return;
}
};
let set_device = IDARG_IN_SWAPCHAINSETDEVICE {
pDevice: dxgi_device.as_raw().cast(),
};
let mut set_ok = false;
let mut terminated = false;
for attempt in 0..60u32 {
if terminate.load(Ordering::Relaxed) {
error!("run_core: terminated during SetDevice (attempt {attempt}, target={target_id})");
terminated = true;
break;
}
let res = unsafe { IddCxSwapChainSetDevice(swap_chain, &set_device) };
if res.is_ok() {
set_ok = true;
error!("run_core: SetDevice OK (target={target_id}, attempt={attempt}) — entering drain loop");
break;
}
if attempt == 0 {
debug!("run_core: SetDevice attempt 0 failed ({res:?}) — retrying up to 60x@50ms (monitor may be flapping)");
}
thread::sleep(Duration::from_millis(50));
}
// Release our borrowed device reference — IddCx holds its own now, or we gave up. (Explicit
// drop so NLL can't release it mid-loop while the swap-chain still references the raw ptr.)
drop(dxgi_device);
if !set_ok {
if !terminated {
error!("run_core: SetDevice never succeeded after retries (target={target_id}) — giving up");
}
return;
}
let mut logged_pending = false;
let mut logged_frame = false;
loop {
// Check terminate at the TOP, every iteration. The success branch below does NOT re-check
// it, so during a CONTINUOUS frame burst (DWM rendering the freshly-activated desktop) a
// thread that the OS unassigns — or that `free_swap_chain_processor` is dropping — never
// sees the flag and loops on, pinning its D3D device (and ~36 NVIDIA worker threads). That
// is THE reconnect leak: it only reproduced at full speed, because cdb's pacing forced
// E_PENDING gaps (which DO check terminate) and masked it. Without this, `SwapChainProcessor::drop`'s
// join can also block until the burst ends.
if terminate.load(Ordering::Relaxed) {
break;
}
// The host recreates the shared ring (new format) mid-session when the display's HDR mode
// flips — it bumps the header generation. Detect that and drop the publisher so we re-attach
// to the new-format textures below; otherwise we'd keep CopyResource'ing into the stale ring,
// whose format now mismatches the surface → the publish() format-guard drops every frame and
// the stream freezes until the next swap-chain recreate.
if publisher.as_ref().is_some_and(FramePublisher::is_stale) {
publisher = None;
frames_since_try = u32::MAX; // re-attach immediately
}
// Lazy-attach (rate-limited) at the loop TOP so we keep trying even while the display is
// idle (E_PENDING / no frames presented yet), not only when a frame is acquired. `try_open`
// is a cheap OpenFileMapping that fails fast until the host has created the ring.
if publisher.is_none() {
if frames_since_try >= 30 {
frames_since_try = 0;
match FramePublisher::try_open(
target_id,
render_luid_low,
render_luid_high,
&device.device,
&device.device_context,
) {
Ok(p) => {
dbg_header_attempt(0, true);
publisher = Some(p);
}
Err(e) => dbg_header_attempt(e.code().0 as u32, false),
}
} else {
frames_since_try += 1;
}
}
// B2: ...Buffer2 is required once CAN_PROCESS_FP16 is set. AcquireSystemMemoryBuffer=FALSE
// keeps the GPU surface (out.MetaData.pSurface). The surface format varies per-frame —
// FP16 (R16G16B16A16_FLOAT) in HDR, BGRA in SDR — and the publisher's format guard handles
// a frame that doesn't match the ring until B3 makes the ring FP16.
let mut in_args = IDARG_IN_RELEASEANDACQUIREBUFFER2 {
#[allow(clippy::cast_possible_truncation)]
Size: std::mem::size_of::<IDARG_IN_RELEASEANDACQUIREBUFFER2>() as u32,
AcquireSystemMemoryBuffer: 0,
};
let mut buffer = IDARG_OUT_RELEASEANDACQUIREBUFFER2::default();
let hr: NTSTATUS = unsafe {
IddCxSwapChainReleaseAndAcquireBuffer2(swap_chain, &mut in_args, &mut buffer).into()
};
#[allow(clippy::items_after_statements)]
const E_PENDING: u32 = 0x8000_000A;
if u32::from(hr) == E_PENDING {
if !logged_pending {
error!("run_core: E_PENDING (target={target_id}) — swap-chain valid but DWM has composed NO frame yet");
logged_pending = true;
}
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() {
if !logged_frame {
error!("run_core: FIRST FRAME acquired (target={target_id}) — DWM IS compositing the virtual display!");
logged_frame = true;
}
dbg_frame(); // bring-up: prove frames actually flow (vs an idle display)
// 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.
//
// P2: copy the acquired surface into the shared ring BEFORE FinishedProcessingFrame
// (the surface is valid until the next ReleaseAndAcquire). The pointer is BORROWED —
// `from_raw_borrowed` does not take IddCx's refcount — and the GPU-side copy is ordered
// before the consumer via the slot keyed mutex. (Attach happens at the loop top.)
if let Some(pub_) = publisher.as_mut() {
let raw = buffer.MetaData.pSurface as *mut core::ffi::c_void;
if !raw.is_null() {
if let Some(res) = unsafe { IDXGIResource::from_raw_borrowed(&raw) } {
if let Ok(tex) = res.cast::<ID3D11Texture2D>() {
pub_.publish(&tex);
}
}
}
}
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();
}
}
}
@@ -0,0 +1,4 @@
[toolchain]
channel = "nightly-2024-07-26"
components = ["rustfmt", "clippy"]
profile = "minimal"
@@ -0,0 +1,17 @@
[package]
name = "wdf-umdf-sys"
version = "0.1.0"
edition = "2021"
[lints]
workspace = true
[dependencies]
paste = "1.0.15"
bytemuck = "1.19.0"
thiserror = "2.0.3"
[build-dependencies]
bindgen = "0.70.1"
thiserror = "2.0.3"
winreg = "0.52.0"
@@ -0,0 +1,278 @@
use std::env;
use std::fmt::{self, Display};
use std::path::{Path, PathBuf};
use bindgen::Abi;
use winreg::enums::HKEY_LOCAL_MACHINE;
use winreg::RegKey;
const UMDF_V: &str = "2.31";
// Bumped 1.4 -> 1.10 for HDR/FP16 support (IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16,
// IddCxSwapChainReleaseAndAcquireBuffer2, the *2 mode/metadata DDIs). 1.10 is a superset of 1.4, so
// existing call sites keep working; the new HDR DDIs become available to bind.
const IDDCX_V: &str = "1.10";
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error("cannot find the directory")]
DirectoryNotFound,
}
/// Retrieves the path to the Windows Kits directory. The default should be
/// `C:\Program Files (x86)\Windows Kits\10`.
///
/// # Errors
/// Returns IO error if failed
fn get_windows_kits_dir() -> Result<PathBuf, Error> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let key = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
let dir: String = hklm.open_subkey(key)?.get_value("KitsRoot10")?;
Ok(dir.into())
}
#[derive(Clone, Copy, PartialEq)]
enum DirectoryType {
Include,
Library,
}
#[derive(Clone, Copy, PartialEq)]
enum Target {
X86_64,
ARM64,
}
impl Default for Target {
fn default() -> Self {
let target = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
match &*target {
"x86_64" => Self::X86_64,
"aarch64" => Self::ARM64,
_ => unimplemented!("{target} arch is unsupported"),
}
}
}
impl Display for Target {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Target::X86_64 => f.write_str("x64"),
Target::ARM64 => f.write_str("arm64"),
}
}
}
fn get_base_path<S: AsRef<Path>>(dir_type: DirectoryType, subs: &[S]) -> Result<PathBuf, Error> {
let mut dir = get_windows_kits_dir()?.join(match dir_type {
DirectoryType::Include => "Include",
DirectoryType::Library => "Lib",
});
dir.extend(subs);
if !dir.is_dir() {
return Err(Error::DirectoryNotFound);
}
Ok(dir)
}
fn get_sdk_path<S: AsRef<Path>>(dir_type: DirectoryType, subs: &[S]) -> Result<PathBuf, Error> {
// We first append lib to the path and read the directory..
let dir = get_windows_kits_dir()?
.join(match dir_type {
DirectoryType::Include => "Include",
DirectoryType::Library => "Lib",
})
.read_dir()?;
// In the lib directory we may have one or more directories named after the version of Windows,
// we will be looking for the highest version number.
let mut dir = dir
.filter_map(Result::ok)
.map(|dir| dir.path())
.filter(|dir| {
let is_sdk = dir
.components()
.last()
.and_then(|c| c.as_os_str().to_str())
.map_or(false, |c| c.starts_with("10."));
let mut sub_dir = dir.clone();
sub_dir.extend(subs);
is_sdk && sub_dir.is_dir()
})
.max()
.ok_or_else(|| Error::DirectoryNotFound)?;
dir.extend(subs);
if !dir.is_dir() {
return Err(Error::DirectoryNotFound);
}
// Finally append um to the path to get the path to the user mode libraries.
Ok(dir)
}
/// Retrieves the path to the user mode libraries. The path may look something like:
/// `C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\um`.
///
/// # Errors
/// Returns IO error if failed
fn get_um_dir(dir_type: DirectoryType) -> Result<PathBuf, Error> {
let target = Target::default().to_string();
let binding = &["um", &target];
let subs: &[&str] = match dir_type {
DirectoryType::Include => &["um"],
DirectoryType::Library => binding,
};
let dir = get_sdk_path(dir_type, subs)?;
Ok(dir)
}
/// # Errors
/// Returns IO error if failed
fn get_umdf_dir(dir_type: DirectoryType) -> Result<PathBuf, Error> {
match dir_type {
DirectoryType::Include => get_base_path(dir_type, &["wdf", "umdf", UMDF_V]),
DirectoryType::Library => get_base_path(
dir_type,
&["wdf", "umdf", &Target::default().to_string(), UMDF_V],
),
}
}
/// Retrieves the path to the shared headers. The path may look something like:
/// `C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\shared`.
///
/// # Errors
/// Returns IO error if failed
fn get_shared_dir() -> Result<PathBuf, Error> {
let dir = get_sdk_path(DirectoryType::Include, &["shared"])?;
Ok(dir)
}
fn build_dir() -> PathBuf {
PathBuf::from(
std::env::var_os("OUT_DIR").expect("the environment variable OUT_DIR is undefined"),
)
}
fn generate() {
// Find the include directory containing the user headers.
let include_um_dir = get_um_dir(DirectoryType::Include).unwrap();
let lib_um_dir = get_um_dir(DirectoryType::Library).unwrap();
let shared = get_shared_dir().unwrap();
println!("cargo:rustc-link-search={}", lib_um_dir.display());
// Tell Cargo to re-run this if src/wrapper.h gets changed.
println!("cargo:rerun-if-changed=c/wrapper.h");
//
// UMDF
//
let umdf_lib_dir = get_umdf_dir(DirectoryType::Library).unwrap();
println!("cargo:rustc-link-search={}", umdf_lib_dir.display());
let wdf_include_dir = get_umdf_dir(DirectoryType::Include).unwrap();
// need to link to umdf lib
println!("cargo:rustc-link-lib=static=WdfDriverStubUm");
//
// IDDCX
//
// The IddCx import lib lives only under the WDK's SDK version (e.g. 10.0.26100.0); a newer base
// SDK installed alongside it (e.g. 10.0.28000.0) has um\x64 but no iddcx subdir, so picking the
// max um\x64 version (lib_um_dir) misses it. Resolve by the version that actually contains
// iddcx — the same way the IddCx.h header path is resolved below.
let iddcx_lib_dir = get_sdk_path(
DirectoryType::Library,
&["um", &Target::default().to_string(), "iddcx", IDDCX_V],
)
.unwrap();
println!("cargo:rustc-link-search={}", iddcx_lib_dir.display());
// need to link to iddcx lib
println!("cargo:rustc-link-lib=static=IddCxStub");
//
// REST
//
// Get the build directory.
let out_path = build_dir();
// Generate the bindings
let mut builder = bindgen::Builder::default()
.derive_debug(false)
.layout_tests(false)
.default_enum_style(bindgen::EnumVariation::NewType {
is_bitfield: false,
is_global: false,
})
.merge_extern_blocks(true)
.header("c/wrapper.h")
.header(
get_sdk_path(DirectoryType::Include, &["um", "iddcx", IDDCX_V])
.unwrap()
.join("IddCx.h")
.to_string_lossy()
.to_string(),
)
// general um includes
.clang_arg(format!("-I{}", include_um_dir.display()))
// umdf includes
.clang_arg(format!("-I{}", wdf_include_dir.display()))
.clang_arg(format!("-I{}", shared.display()))
// because aarch64 needs to find excpt.h
.clang_arg(format!(
"-I{}",
get_sdk_path(DirectoryType::Include, &["km", "crt"])
.unwrap()
.display()
))
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.blocklist_type("_?P?IMAGE_TLS_DIRECTORY.*")
// we will use our own custom type
.blocklist_item("NTSTATUS")
.blocklist_item("IddMinimumVersionRequired")
.blocklist_item("WdfMinimumVersionRequired")
.clang_arg("--language=c++")
.clang_arg("-fms-compatibility")
.clang_arg("-fms-extensions")
.override_abi(Abi::CUnwind, ".*")
.generate_cstr(true)
.derive_default(true);
let defines = match Target::default() {
Target::X86_64 => ["AMD64", "_AMD64_"],
Target::ARM64 => ["ARM64", "_ARM64_"],
};
for define in defines {
builder = builder.clang_arg(format!("-D{define}"));
}
// generate
let umdf = builder.generate().unwrap();
// Write the bindings to the $OUT_DIR/bindings.rs file.
umdf.write_to_file(out_path.join("umdf.rs")).unwrap();
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
generate();
}
@@ -0,0 +1,22 @@
#include <Windows.h>
/**
*
* UMDF
*
*/
#define WDF_STUB
#include <wdf.h>
/**
*
* IDCXX
*
*/
#define IDD_STUB
// handled in build.rs
// #include <iddcx\1.4\IddCx.h>
@@ -0,0 +1,17 @@
#![allow(unsafe_op_in_unsafe_fn)]
#![allow(clippy::all)]
#![allow(clippy::pedantic)]
#![allow(clippy::restriction)]
// stand-in type replacing NTSTATUS in the bindings
use crate::NTSTATUS;
include!(concat!(env!("OUT_DIR"), "/umdf.rs"));
// required for some macros
unsafe impl Send for _WDF_OBJECT_CONTEXT_TYPE_INFO {}
unsafe impl Sync for _WDF_OBJECT_CONTEXT_TYPE_INFO {}
// fails to build without this symbol
#[no_mangle]
pub static IddMinimumVersionRequired: ULONG = 4;
@@ -0,0 +1,211 @@
#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals, unused)]
mod bindings;
mod ntstatus;
use std::fmt::{self, Display};
pub use bindings::*;
pub use ntstatus::*;
pub use paste::paste;
#[macro_export]
macro_rules! WdfIsFunctionAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::WdfClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let fn_count = unsafe { $crate::WdfFunctionCount };
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h#L126
$crate::paste! {
// index is always positive, see
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h
const FN_INDEX: u32 = $crate::WDFFUNCENUM::[<$name TableIndex>].0 as u32;
FN_INDEX < $crate::WDF_ALWAYS_AVAILABLE_FUNCTION_COUNT
|| !higher || FN_INDEX < fn_count
}
}};
}
#[macro_export]
macro_rules! WdfIsStructureAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::WdfClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let struct_count = unsafe { $crate::WdfStructureCount };
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h#L141
$crate::paste! {
// index is always positive, see
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h
const STRUCT_INDEX: u32 = $crate::WDFSTRUCTENUM::[<INDEX_ $name>].0 as u32;
!higher || STRUCT_INDEX < struct_count
}
}};
}
#[macro_export]
macro_rules! IddCxIsFunctionAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::IddClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let fn_count = unsafe { $crate::IddFunctionCount };
$crate::paste! {
const FN_INDEX: u32 = $crate::IDDFUNCENUM::[<$name TableIndex>].0 as u32;
FN_INDEX < $crate::IDD_ALWAYS_AVAILABLE_FUNCTION_COUNT
|| !higher || FN_INDEX < fn_count
}
}};
}
#[macro_export]
macro_rules! IddCxIsStructureAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::IddClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let struct_count = unsafe { $crate::IddStructureCount };
$crate::paste! {
const STRUCT_INDEX: u32 = $crate::IDDSTRUCTENUM::[<INDEX_ $name>].0 as u32;
!higher || STRUCT_INDEX < struct_count
}
}};
}
macro_rules! WDF_STRUCTURE_SIZE {
($name:ty) => {
u32::try_from(::core::mem::size_of::<$name>()).expect("size is correct")
};
}
#[macro_export]
macro_rules! WDF_NO_HANDLE {
() => {
::core::ptr::null_mut()
};
}
#[macro_export]
macro_rules! WDF_NO_OBJECT_ATTRIBUTES {
() => {
::core::ptr::null_mut()
};
}
#[macro_export]
macro_rules! WDF_OBJECT_ATTRIBUTES_SET_CONTEXT_TYPE {
($attr:ident, $context_type:ident) => {
$attr.ContextTypeInfo = $context_type;
};
}
impl WDF_OBJECT_ATTRIBUTES {
/// Initializes the [`WDF_OBJECT_ATTRIBUTES`] structure
/// <https://github.com/microsoft/Windows-Driver-Frameworks/blob/a94b8c30dad524352fab90872aefc83920b98e56/src/publicinc/wdf/umdf/2.33/wdfobject.h#L136/>
///
/// Sets
/// - `ExecutionLevel` to [`WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent`]
/// - `SynchronizationScope` to [`WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent`]
#[must_use]
pub fn init() -> Self {
// SAFETY: All fields are zero-able
let mut attributes: Self = unsafe { ::core::mem::zeroed() };
attributes.Size = WDF_STRUCTURE_SIZE!(Self);
attributes.SynchronizationScope =
WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent;
attributes.ExecutionLevel = WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent;
attributes
}
#[must_use]
pub fn init_context_type(context_type: &_WDF_OBJECT_CONTEXT_TYPE_INFO) -> Self {
let mut attr = Self::init();
WDF_OBJECT_ATTRIBUTES_SET_CONTEXT_TYPE!(attr, context_type);
attr
}
}
impl WDF_DRIVER_CONFIG {
/// Initializes the [`WDF_DRIVER_CONFIG`] structure
/// <https://github.com/microsoft/Windows-Driver-Frameworks/blob/a94b8c30dad524352fab90872aefc83920b98e56/src/publicinc/wdf/umdf/2.33/wdfdriver.h#L134/>
#[must_use]
pub fn init(EvtDriverDeviceAdd: PFN_WDF_DRIVER_DEVICE_ADD) -> Self {
// SAFETY: All fields are zero-able
let mut config: Self = unsafe { core::mem::zeroed() };
config.Size = WDF_STRUCTURE_SIZE!(Self);
config.EvtDriverDeviceAdd = EvtDriverDeviceAdd;
config
}
}
impl WDF_PNPPOWER_EVENT_CALLBACKS {
/// Initializes the [`WDF_PNPPOWER_EVENT_CALLBACKS`] structure
/// <https://github.com/microsoft/Windows-Driver-Frameworks/blob/a94b8c30dad524352fab90872aefc83920b98e56/src/publicinc/wdf/umdf/2.33/wdfdevice.h#L1278/>
#[must_use]
pub fn init() -> Self {
// SAFETY: All fields are zero-able
let mut callbacks: Self = unsafe { core::mem::zeroed() };
callbacks.Size = WDF_STRUCTURE_SIZE!(Self);
callbacks
}
}
/// If this returns None, the struct is NOT available to be used
macro_rules! IDD_STRUCTURE_SIZE {
($name:ty) => {{
// SAFETY: We only ever do read access, copy is fine
let higher = unsafe { IddClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access, copy is fine
let struct_count = unsafe { IddStructureCount };
if higher {
// as u32 is fine, since there's no way there's > 4 billion structs
const STRUCT_INDEX: u32 =
$crate::paste! { IDDSTRUCTENUM::[<INDEX_ $name:upper>].0 as u32 };
// SAFETY: A pointer to a [size_t], copying the pointer is ok
let ptr = unsafe { IddStructures };
if STRUCT_INDEX < struct_count {
// SAFETY: we validated struct index is able to be accessed
let ptr = unsafe { ptr.add(STRUCT_INDEX as usize) };
// SAFETY: So it's ok to read
u32::try_from(unsafe { ptr.read() }).ok()
} else {
// struct CANNOT be used
None
}
} else {
u32::try_from(::std::mem::size_of::<$name>()).ok()
}
}};
}
impl IDD_CX_CLIENT_CONFIG {
#[must_use]
pub fn init() -> Option<Self> {
// SAFETY: All fields are zero-able
let mut config: Self = unsafe { core::mem::zeroed() };
config.Size = IDD_STRUCTURE_SIZE!(IDD_CX_CLIENT_CONFIG)?;
Some(config)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
[package]
name = "wdf-umdf"
version = "0.1.0"
edition = "2021"
[lints]
workspace = true
[dependencies]
wdf-umdf-sys = { path = "../wdf-umdf-sys" }
paste = "1.0.15"
thiserror = "2.0.3"
@@ -0,0 +1,344 @@
#![allow(non_snake_case)]
#![allow(clippy::missing_errors_doc)]
use std::sync::OnceLock;
use wdf_umdf_sys::{
IDARG_IN_ADAPTERSETRENDERADAPTER, IDARG_IN_ADAPTER_INIT, IDARG_IN_MONITORCREATE,
IDARG_IN_QUERY_HWCURSOR, IDARG_IN_SETUP_HWCURSOR, IDARG_IN_SWAPCHAINSETDEVICE,
IDARG_OUT_ADAPTER_INIT, IDARG_OUT_MONITORARRIVAL, IDARG_OUT_MONITORCREATE,
IDARG_IN_RELEASEANDACQUIREBUFFER2, IDARG_OUT_QUERY_HWCURSOR, IDARG_OUT_RELEASEANDACQUIREBUFFER,
IDARG_OUT_RELEASEANDACQUIREBUFFER2, IDDCX_ADAPTER, IDDCX_MONITOR,
IDDCX_SWAPCHAIN, IDD_CX_CLIENT_CONFIG, NTSTATUS, WDFDEVICE, WDFDEVICE_INIT,
};
#[derive(Copy, Clone, Debug, thiserror::Error)]
pub enum IddCxError {
#[error("{0}")]
IddCxFunctionNotAvailable(&'static str),
#[error("{0}")]
CallFailed(NTSTATUS),
#[error("{0}")]
NtStatus(NTSTATUS),
}
impl From<IddCxError> for NTSTATUS {
fn from(value: IddCxError) -> Self {
#[allow(clippy::enum_glob_use)]
use IddCxError::*;
match value {
IddCxFunctionNotAvailable(_) => Self::STATUS_NOT_FOUND,
CallFailed(status) => status,
NtStatus(n) => n,
}
}
}
impl From<NTSTATUS> for IddCxError {
fn from(value: NTSTATUS) -> Self {
IddCxError::CallFailed(value)
}
}
impl From<i32> for IddCxError {
fn from(val: i32) -> Self {
IddCxError::NtStatus(NTSTATUS(val))
}
}
// void IddCx functions return () on success; required by the call macro's error arm but never an error.
impl From<()> for IddCxError {
fn from(_: ()) -> Self {
IddCxError::NtStatus(NTSTATUS(0))
}
}
macro_rules! IddCxCall {
($name:ident ( $($args:expr),* )) => {
IddCxCall!(false, $name($($args),*))
};
($other_is_error:expr, $name:ident ( $($args:expr),* )) => {{
static CACHED_FN: OnceLock<
Result<
::paste::paste!(::wdf_umdf_sys::[<PFN_ $name:upper>]),
IddCxError
>
> = OnceLock::new();
let f = CACHED_FN.get_or_init(|| {
::paste::paste! {
const FN_INDEX: usize = ::wdf_umdf_sys::IDDFUNCENUM::[<$name TableIndex>].0 as usize;
// validate that wdf function can be used
let is_available = ::wdf_umdf_sys::IddCxIsFunctionAvailable!($name);
if is_available {
// SAFETY: Only immutable accesses are done to this
// The underlying array is Copy, so we call as_ptr() directly on it inside block
let fn_table = unsafe { ::wdf_umdf_sys::IddFunctions.as_ptr() };
// SAFETY: Ensured that this is present by if condition from `WdfIsFunctionAvailable!`
let f = unsafe {
fn_table.add(FN_INDEX)
.cast::<::wdf_umdf_sys::[<PFN_ $name:upper>]>()
};
// SAFETY: Ensured that this is present by if condition from `IddIsFunctionAvailable!`
let f = unsafe { f.read() };
Ok(f)
} else {
Err($crate::IddCxError::IddCxFunctionNotAvailable(concat!(stringify!($name), " is not available")))
}
}
}).clone()?;
// SAFETY: Above: If it's Ok, then it's guaranteed to be Some(fn)
let f = unsafe { f.unwrap_unchecked() };
// SAFETY: Pointer to globals is always immutable
let globals = unsafe { ::wdf_umdf_sys::IddDriverGlobals };
// SAFETY: None. User is responsible for safety and must use their own unsafe block
let result = unsafe { f(globals, $($args),*) };
if $crate::is_nt_error(&result, $other_is_error) {
Err(result.into())
} else {
Ok(result.into())
}
}};
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn IddCxDeviceInitConfig(
// in, out
DeviceInit: &mut WDFDEVICE_INIT,
// in
Config: &IDD_CX_CLIENT_CONFIG,
) -> Result<NTSTATUS, IddCxError> {
IddCxCall! {
IddCxDeviceInitConfig(
DeviceInit,
Config
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn IddCxDeviceInitialize(
// in
Device: WDFDEVICE,
) -> Result<NTSTATUS, IddCxError> {
IddCxCall! {
IddCxDeviceInitialize(
Device
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn IddCxAdapterInitAsync(
// in
pInArgs: &IDARG_IN_ADAPTER_INIT,
// out
pOutArgs: &mut IDARG_OUT_ADAPTER_INIT,
) -> Result<NTSTATUS, IddCxError> {
IddCxCall! {
IddCxAdapterInitAsync(
pInArgs,
pOutArgs
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorCreate(
// in
AdapterObject: IDDCX_ADAPTER,
// in
pInArgs: &IDARG_IN_MONITORCREATE,
// out
pOutArgs: &mut IDARG_OUT_MONITORCREATE,
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorCreate(
AdapterObject,
pInArgs,
pOutArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorArrival(
// in
MonitorObject: IDDCX_MONITOR,
// out
pOutArgs: &mut IDARG_OUT_MONITORARRIVAL,
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorArrival(
MonitorObject,
pOutArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxSwapChainSetDevice(
// in
SwapChainObject: IDDCX_SWAPCHAIN,
// in
pInArgs: &IDARG_IN_SWAPCHAINSETDEVICE
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
true,
IddCxSwapChainSetDevice(
SwapChainObject,
pInArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxSwapChainReleaseAndAcquireBuffer(
// in
SwapChainObject: IDDCX_SWAPCHAIN,
// out
pOutArgs: &mut IDARG_OUT_RELEASEANDACQUIREBUFFER
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
true,
IddCxSwapChainReleaseAndAcquireBuffer(
SwapChainObject,
pOutArgs
)
)
}
/// IddCx 1.10 HDR variant — required once the adapter sets `CAN_PROCESS_FP16`. Provides per-frame
/// `IDDCX_METADATA2` (surface colour space, HDR metadata, SDR white level).
///
/// # Safety
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxSwapChainReleaseAndAcquireBuffer2(
// in
SwapChainObject: IDDCX_SWAPCHAIN,
// in
pInArgs: &mut IDARG_IN_RELEASEANDACQUIREBUFFER2,
// out
pOutArgs: &mut IDARG_OUT_RELEASEANDACQUIREBUFFER2
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
true,
IddCxSwapChainReleaseAndAcquireBuffer2(
SwapChainObject,
pInArgs,
pOutArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxSwapChainFinishedProcessingFrame(
// in
SwapChainObject: IDDCX_SWAPCHAIN
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
true,
IddCxSwapChainFinishedProcessingFrame(
SwapChainObject
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorDeparture(
// in
MonitorObject: IDDCX_MONITOR
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorDeparture(
MonitorObject
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn IddCxAdapterSetRenderAdapter(
// in
AdapterObject: IDDCX_ADAPTER,
// in
pInArgs: *const IDARG_IN_ADAPTERSETRENDERADAPTER,
) -> Result<(), IddCxError> {
IddCxCall!(IddCxAdapterSetRenderAdapter(AdapterObject, pInArgs))
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorSetupHardwareCursor(
// in
MonitorObject: IDDCX_MONITOR,
// in
pInArgs: &IDARG_IN_SETUP_HWCURSOR
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorSetupHardwareCursor(
MonitorObject,
pInArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorQueryHardwareCursor(
// in
MonitorObject: IDDCX_MONITOR,
// in
pInArgs: &IDARG_IN_QUERY_HWCURSOR,
// out
pOutArgs: &mut IDARG_OUT_QUERY_HWCURSOR
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorQueryHardwareCursor(
MonitorObject,
pInArgs,
pOutArgs
)
)
}
@@ -0,0 +1,30 @@
mod iddcx;
mod wdf;
use std::any::Any;
pub use paste::paste;
pub use iddcx::*;
pub use wdf::*;
pub use wdf_umdf_sys;
use wdf_umdf_sys::NTSTATUS;
/// Used for the macros so they can correctly convert a functions result
fn is_nt_error(val: &dyn Any, other_is_error: bool) -> bool {
if let Some(status) = val.downcast_ref::<NTSTATUS>() {
return !status.is_success();
}
// other errors which may not be error codes, but may also be
// such as HRESULT == i32
if other_is_error {
if let Some(status) = val.downcast_ref::<i32>() {
let status = NTSTATUS(*status);
return !status.is_success();
}
}
false
}
@@ -0,0 +1,667 @@
#![allow(non_snake_case)]
#![allow(clippy::missing_errors_doc)]
use std::ffi::c_void;
use std::sync::OnceLock;
use wdf_umdf_sys::{
DEVPROPTYPE, GUID, NTSTATUS, PCUNICODE_STRING, PCWDF_OBJECT_CONTEXT_TYPE_INFO, PDRIVER_OBJECT,
POOL_TYPE, PVOID, PWDFDEVICE_INIT, PWDF_DRIVER_CONFIG, PWDF_OBJECT_ATTRIBUTES, ULONG_PTR,
WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFOBJECT, WDFREQUEST, WDF_DEVICE_FAILED_ACTION, WDF_NO_HANDLE,
WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES, _WDF_DEVICE_PROPERTY_DATA,
_WDF_PNPPOWER_EVENT_CALLBACKS,
};
#[derive(Copy, Clone, Debug, thiserror::Error)]
pub enum WdfError {
#[error("{0}")]
WdfFunctionNotAvailable(&'static str),
#[error("{0}")]
CallFailed(NTSTATUS),
#[error("Failed to upgrade Arc pointer")]
UpgradeFailed,
#[error("Failed to lock")]
LockFailed,
#[error("Unknown")]
Unknown,
// this is required for success status for ()
#[error("This is not an error, ignore it")]
_Success,
}
impl From<()> for WdfError {
fn from(_: ()) -> Self {
WdfError::_Success
}
}
impl From<*mut c_void> for WdfError {
fn from(_: *mut c_void) -> Self {
Self::Unknown
}
}
impl From<WdfError> for NTSTATUS {
fn from(value: WdfError) -> Self {
#[allow(clippy::enum_glob_use)]
use WdfError::*;
match value {
WdfFunctionNotAvailable(_) => Self::STATUS_NOT_FOUND,
CallFailed(status) => status,
UpgradeFailed => Self::STATUS_INVALID_HANDLE,
LockFailed => Self::STATUS_WAS_LOCKED,
Unknown => Self::STATUS_DRIVER_INTERNAL_ERROR,
_Success => 0.into(),
}
}
}
impl From<NTSTATUS> for WdfError {
fn from(value: NTSTATUS) -> Self {
WdfError::CallFailed(value)
}
}
macro_rules! WdfCall {
($name:ident ( $($args:expr),* )) => {
WdfCall!(false, $name($($args),*))
};
($other_is_error:expr, $name:ident ( $($args:expr),* )) => {{
static CACHED_FN: OnceLock<
Result<
::paste::paste!(::wdf_umdf_sys::[<PFN_ $name:upper>]),
WdfError
>
> = OnceLock::new();
let f = CACHED_FN.get_or_init(|| {
::paste::paste! {
const FN_INDEX: usize = ::wdf_umdf_sys::WDFFUNCENUM::[<$name TableIndex>].0 as usize;
// validate that wdf function can be used
let is_available = ::wdf_umdf_sys::WdfIsFunctionAvailable!($name);
if is_available {
// SAFETY: Only immutable accesses are done to this
let fn_table = unsafe { ::wdf_umdf_sys::WdfFunctions_02031 };
// SAFETY: Read-only, initialized by the time we use it, and checked to be in bounds
let f = unsafe {
fn_table
.add(FN_INDEX)
.cast::<::wdf_umdf_sys::[<PFN_ $name:upper>]>()
};
// SAFETY: Ensured that this is present by if condition from `WdfIsFunctionAvailable!`
let f = unsafe { f.read() };
Ok(f)
} else {
Err($crate::WdfError::WdfFunctionNotAvailable(concat!(stringify!($name), " is not available")))
}
}
}).clone()?;
// SAFETY: Above: If it's Ok, then it's guaranteed to be Some(fn)
let f = unsafe { f.unwrap_unchecked() };
// SAFETY: Pointer to globals is always immutable
let globals = unsafe { ::wdf_umdf_sys::WdfDriverGlobals };
// SAFETY: None. User is responsible for safety and must use their own unsafe block
let result = unsafe { f(globals, $($args),*) };
if $crate::is_nt_error(&result, $other_is_error) {
Err(result.into())
} else {
Ok(result.into())
}
}};
}
/// Unlike the official `WDF_DECLARE_CONTEXT_TYPE` macro, you only need to declare this on the actual data struct want to use
/// Safety is maintained through a `RwLock` of the underlying data
///
/// This generates associated fns `init`/`get`/`drop`/`get_type_info` on your `$context_type` with the same visibility
///
/// Example:
/// ```rust
/// pub struct IndirectDeviceContext {
/// device: WDFDEVICE,
/// }
///
/// impl IndirectDeviceContext {
/// pub fn new(device: WDFDEVICE) -> Self {
/// Self { device }
/// }
/// }
///
/// WDF_DECLARE_CONTEXT_TYPE!(pub IndirectDeviceContext);
///
/// // with a `device: WDFDEVICE`
/// let context = IndirectDeviceContext::new(device as WDFOBJECT);
/// IndirectDeviceContext::init(context);
/// // elsewhere
/// let mutable_access = IndirectDeviceContext::get_mut(device).unwrap();
/// ```
#[macro_export]
macro_rules! WDF_DECLARE_CONTEXT_TYPE {
($sv:vis $context_type:ident) => {
$crate::paste! {
// keep it in a mod block to disallow access to private types
#[allow(non_snake_case)]
mod [<WdfObject $context_type>] {
use super::$context_type;
// Require `T: Sync` for safety. User has to uphold the invariant themselves
#[repr(transparent)]
#[allow(non_camel_case_types)]
struct [<_WDF_ $context_type _STATIC_WRAPPER>]<T> {
cell: ::std::cell::UnsafeCell<$crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO>,
_phantom: ::std::marker::PhantomData<T>
}
// SAFETY: `T` impls Sync too
unsafe impl<T: Sync> Sync for [<_WDF_ $context_type _STATIC_WRAPPER>]<T> {}
// Unsure if C mutates this data, but it's in an unsafecell just in case
#[allow(non_upper_case_globals)]
static [<_WDF_ $context_type _TYPE_INFO>]: [<_WDF_ $context_type _STATIC_WRAPPER>]<$context_type> =
[<_WDF_ $context_type _STATIC_WRAPPER>] {
cell: ::std::cell::UnsafeCell::new(
$crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO {
Size: ::std::mem::size_of::<$crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO>() as u32,
ContextName: concat!(stringify!($context_type), "\0")
.as_ptr().cast::<::std::ffi::c_char>(),
ContextSize: ::std::mem::size_of::<[<WdfObject $context_type>]>(),
// SAFETY:
// StaticWrapper and UnsafeCell are both repr(transparent), so cast to underlying _WDF_OBJECT_CONTEXT_TYPE_INFO is ok
UniqueType: &[<_WDF_ $context_type _TYPE_INFO>] as *const _ as *const _,
EvtDriverGetUniqueContextType: ::std::option::Option::None,
}
),
_phantom: ::std::marker::PhantomData
};
/// Allows us to keep ONE main Arc allocation while handing out weak pointers to the rest of the clones.
/// In this way, we can drop the allocation by dropping 1 arc, while letting others still access it
enum ArcPointer<T> {
Strong(::std::sync::Arc<T>),
Weak(::std::sync::Weak<T>)
}
#[repr(transparent)]
struct [<WdfObject $context_type>](ArcPointer<::std::sync::RwLock<$context_type>>);
impl $context_type {
/// Initialize and place context into internal WdfObject
///
/// SAFETY:
/// - handle must be a fresh unused object with no data in its context already
/// - context type must already have been set up for handle
/// - Must be set only once regardless of the object. For all other objects, use clone_into()
$sv unsafe fn init(
self,
handle: $crate::wdf_umdf_sys::WDFOBJECT,
) -> ::std::result::Result<(), $crate::WdfError> {
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle, [<_WDF_ $context_type _TYPE_INFO>].cell.get())?
} as *mut ::std::mem::MaybeUninit<[<WdfObject $context_type>]>;
let context = &mut *context;
// Write to the memory location, making the data in it init
context.write(
[<WdfObject $context_type>](
ArcPointer::Strong(::std::sync::Arc::new(::std::sync::RwLock::new(self)))
)
);
Ok(())
}
/// Initialize handle's context and clone a Weak pointer to self context into it.
/// Internally, these are Arc's, so they will always point to the same data.
/// When the main Arc drops, none of these may access memory any longer
///
/// SAFETY:
/// - handle must be a fresh unused object with no data in its context already
/// - to_handle must have set context_type for this type via WDF_OBJECT_ATTRIBUTES when it was created
/// - to_handle must be a valid T
$sv unsafe fn clone_into(
&self,
handle: $crate::wdf_umdf_sys::WDFOBJECT
) -> ::std::result::Result<(), $crate::WdfError> {
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle, [<_WDF_ $context_type _TYPE_INFO>].cell.get())?
} as *mut ::std::mem::MaybeUninit<[<WdfObject $context_type>]>;
let context = &mut *context;
let from_context = unsafe {
$crate::WdfObjectGetTypedContextWorker(self.device as *mut _, [<_WDF_ $context_type _TYPE_INFO>].cell.get())?
} as *mut [<WdfObject $context_type>];
let from_context = match &(*from_context).0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
// Write to the memory location, making the data in it init
// clones the arc into new handle
context.write(
[<WdfObject $context_type>](ArcPointer::Weak(::std::sync::Arc::downgrade(&from_context)))
);
Ok(())
}
/// NOTE: Dropping memory that was created via `clone_into` will never drop the main allocation.
/// To drop the main allocation, you need to drop the instance made via `init`.
/// That instance can be obtained through the original handle you created it through
///
/// SAFETY:
/// - Data in context is assumed to already be init and a valid T
/// - Therefore, init for the context must already have been done on this handle
/// - No other mutable/non-mutable refs can exist to data when this is called, or it will alias
///
/// This may overwrite data in the handle's context memory, it is UB to read it after drop (e.g. get*)
$sv unsafe fn drop(
handle: $crate::wdf_umdf_sys::WDFOBJECT,
) -> ::std::result::Result<(), $crate::WdfError> {
let context = $crate::WdfObjectGetTypedContextWorker(
handle,
[<_WDF_ $context_type _TYPE_INFO>].cell.get(),
)? as *mut [<WdfObject $context_type>];
// drop the memory
::std::ptr::drop_in_place(context);
Ok(())
}
/// Borrow the context immutably
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn get<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&$context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let guard = context.read().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&*guard);
Ok(())
}
/// Borrow the context mutably
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn get_mut<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&mut $context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let mut guard = context.write().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&mut *guard);
Ok(())
}
/// Borrow the context immutably
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn try_get<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&$context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let guard = context.try_read().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&*guard);
Ok(())
}
/// Try to borrow the context mutably. Immediately returns if it's locked
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn try_get_mut<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&mut $context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let mut guard = context.try_write().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&mut *guard);
Ok(())
}
// SAFETY:
// - No other mutable refs must exist to target type
// - Underlying memory must remain immutable and unchanged until reference is dropped
$sv unsafe fn get_type_info() -> &'static $crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO {
unsafe { &*[<_WDF_ $context_type _TYPE_INFO>].cell.get() }
}
}
}
}
};
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDriverCreate(
// in
DriverObject: PDRIVER_OBJECT,
// in
RegistryPath: PCUNICODE_STRING,
// in, optional
DriverAttributes: Option<PWDF_OBJECT_ATTRIBUTES>,
// in
DriverConfig: PWDF_DRIVER_CONFIG,
// out, optional
Driver: Option<&mut WDFDRIVER>,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDriverCreate(
DriverObject,
RegistryPath,
DriverAttributes.unwrap_or(WDF_NO_OBJECT_ATTRIBUTES!()),
DriverConfig,
Driver
.map(std::ptr::from_mut)
.unwrap_or(WDF_NO_HANDLE!())
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceCreate(
// in, out
DeviceInit: &mut PWDFDEVICE_INIT,
// in, optional
DeviceAttributes: Option<&mut WDF_OBJECT_ATTRIBUTES>,
// out
Device: &mut WDFDEVICE,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDeviceCreate(
DeviceInit,
DeviceAttributes.map_or(WDF_NO_OBJECT_ATTRIBUTES!(), std::ptr::from_mut),
Device
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceInitSetPnpPowerEventCallbacks(
// in
DeviceInit: PWDFDEVICE_INIT,
// in
PnpPowerEventCallbacks: *mut _WDF_PNPPOWER_EVENT_CALLBACKS,
) -> Result<(), WdfError> {
WdfCall! {
WdfDeviceInitSetPnpPowerEventCallbacks(
DeviceInit,
PnpPowerEventCallbacks
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfObjectGetTypedContextWorker(
// in
Handle: WDFOBJECT,
// in
TypeInfo: PCWDF_OBJECT_CONTEXT_TYPE_INFO,
) -> Result<*mut c_void, WdfError> {
WdfCall! {
WdfObjectGetTypedContextWorker(
Handle,
TypeInfo
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfObjectDelete(
// in
Object: WDFOBJECT,
) -> Result<(), WdfError> {
WdfCall! {
WdfObjectDelete(
Object
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceSetFailed(
// in
Device: WDFDEVICE,
// in
FailedAction: WDF_DEVICE_FAILED_ACTION,
) -> Result<(), WdfError> {
WdfCall! {
WdfDeviceSetFailed(
Device,
FailedAction
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceAllocAndQueryPropertyEx(
// in
Device: WDFDEVICE,
// in
DeviceProperty: &mut _WDF_DEVICE_PROPERTY_DATA,
// in
PoolType: POOL_TYPE,
// in, optional
PropertyMemoryAttributes: PWDF_OBJECT_ATTRIBUTES,
// out
PropertyMemory: &mut WDFMEMORY,
// out
Type: &mut DEVPROPTYPE,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDeviceAllocAndQueryPropertyEx(
Device,
DeviceProperty,
PoolType,
PropertyMemoryAttributes,
PropertyMemory,
Type
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfMemoryGetBuffer(
// in
Memory: WDFMEMORY,
// out, optional
BufferSize: Option<&mut usize>,
) -> Result<*mut c_void, WdfError> {
WdfCall! {
WdfMemoryGetBuffer(
Memory,
BufferSize.map_or(std::ptr::null_mut(), std::ptr::from_mut)
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceCreateDeviceInterface(
Device: WDFDEVICE,
InterfaceClassGUID: *const GUID,
ReferenceString: PCUNICODE_STRING,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDeviceCreateDeviceInterface(
Device,
InterfaceClassGUID,
ReferenceString
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfRequestRetrieveInputBuffer(
Request: WDFREQUEST,
MinimumRequiredLength: usize,
Buffer: *mut PVOID,
Length: *mut usize,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfRequestRetrieveInputBuffer(
Request,
MinimumRequiredLength,
Buffer,
Length
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfRequestRetrieveOutputBuffer(
Request: WDFREQUEST,
MinimumRequiredSize: usize,
Buffer: *mut PVOID,
Length: *mut usize,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfRequestRetrieveOutputBuffer(
Request,
MinimumRequiredSize,
Buffer,
Length
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfRequestCompleteWithInformation(
Request: WDFREQUEST,
Status: NTSTATUS,
Information: ULONG_PTR,
) -> Result<(), WdfError> {
WdfCall! {
WdfRequestCompleteWithInformation(
Request,
Status,
Information
)
}
}