Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2c9bfd3d9 | |||
| c5dab484df | |||
| e27abc065e | |||
| d39da4bc06 |
@@ -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.
|
||||
#
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")?
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ~1–2k 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 (+~3–5 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 ≈ **2–4 wk to first light**, **5–7 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
@@ -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"
|
||||
@@ -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.
@@ -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 $VendorDir — see 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 $VendorDir — re-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
@@ -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(¶ms.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(¶ms.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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user