Compare commits
10 Commits
dbab1f98ba
...
0a63154293
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a63154293 | |||
| e5057f6cc1 | |||
| a3eefc2374 | |||
| cd591514ad | |||
| a2bd0cd77c | |||
| 48f980ebb1 | |||
| 1cd87066d7 | |||
| 789ad49bc4 | |||
| c87bfe0e7b | |||
| f98ab07dd6 |
@@ -315,8 +315,10 @@ pub fn open_portal_monitor() -> Result<Box<dyn Capturer>> {
|
||||
pub fn capture_virtual_output(
|
||||
vout: crate::vdisplay::VirtualOutput,
|
||||
_want_hdr: bool,
|
||||
_capture: crate::session_plan::CaptureBackend,
|
||||
) -> Result<Box<dyn Capturer>> {
|
||||
// The Linux host stays 8-bit (HDR is blocked upstream), so `want_hdr` is unused here.
|
||||
// The Linux host stays 8-bit (HDR is blocked upstream), so `want_hdr` is unused here; the capture
|
||||
// backend is always the portal (the `CaptureBackend` arg is a Windows-only dispatch — ignored here).
|
||||
linux::PortalCapturer::from_virtual_output(vout).map(|c| Box::new(c) as Box<dyn Capturer>)
|
||||
}
|
||||
|
||||
@@ -327,14 +329,16 @@ pub fn capture_virtual_output(
|
||||
/// compiled and comes back the moment the flag is unset.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn wgc_disabled() -> bool {
|
||||
std::env::var_os("PUNKTFUNK_NO_WGC").is_some()
|
||||
crate::config::config().no_wgc
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn capture_virtual_output(
|
||||
vout: crate::vdisplay::VirtualOutput,
|
||||
want_hdr: bool,
|
||||
capture: crate::session_plan::CaptureBackend,
|
||||
) -> Result<Box<dyn Capturer>> {
|
||||
use crate::session_plan::CaptureBackend;
|
||||
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)"
|
||||
@@ -343,9 +347,10 @@ pub fn capture_virtual_output(
|
||||
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() {
|
||||
// ring — no Desktop Duplication, no win32u reparenting hook. Resolved once in the `SessionPlan`
|
||||
// (was re-derived from `config().idd_push` here); `IddPush` takes the keepalive (owns the virtual
|
||||
// display) so there's no fall-through.
|
||||
if capture == CaptureBackend::IddPush {
|
||||
// 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
|
||||
@@ -370,11 +375,9 @@ pub fn capture_virtual_output(
|
||||
// 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
|
||||
// the secure-desktop (lock/UAC) fallback (WGC can't capture those). `keep` is moved into the
|
||||
// chosen backend (it owns the SudoVDA keepalive), so there's no open-time auto-fallback.
|
||||
let backend = std::env::var("PUNKTFUNK_CAPTURE")
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
if backend == "dda" || backend == "dxgi" || wgc_disabled() {
|
||||
// chosen backend (it owns the SudoVDA keepalive), so there's no open-time auto-fallback. The
|
||||
// backend choice (`dda`/`dxgi`/`PUNKTFUNK_NO_WGC` → DDA, else WGC) is now resolved once in the plan.
|
||||
if capture == CaptureBackend::Dda {
|
||||
return dxgi::DuplCapturer::open(target, pref, keep, false)
|
||||
.map(|c| Box::new(c) as Box<dyn Capturer>);
|
||||
}
|
||||
@@ -420,6 +423,7 @@ pub fn capture_virtual_output(
|
||||
pub fn capture_virtual_output(
|
||||
_vout: crate::vdisplay::VirtualOutput,
|
||||
_want_hdr: bool,
|
||||
_capture: crate::session_plan::CaptureBackend,
|
||||
) -> Result<Box<dyn Capturer>> {
|
||||
anyhow::bail!("virtual-output capture requires Linux or Windows")
|
||||
}
|
||||
|
||||
@@ -136,6 +136,10 @@ pub struct IddPushCapturer {
|
||||
/// Throttle for the `advanced_color_enabled` poll (a CCD `QueryDisplayConfig`, ~ms — too costly per
|
||||
/// frame at 240 Hz).
|
||||
last_acm_poll: Instant,
|
||||
/// Set when a display-descriptor change triggered a ring recreate (recovery, game-capture bug GB1);
|
||||
/// cleared when a fresh frame resumes. If it stays set past the recovery window, `try_consume` drops
|
||||
/// the session (recover-or-drop, no DDA).
|
||||
recovering_since: Option<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
|
||||
@@ -360,8 +364,22 @@ impl IddPushCapturer {
|
||||
preferred: Option<(u32, u32, u32)>,
|
||||
client_10bit: bool,
|
||||
) -> Result<Self> {
|
||||
let (w, h, _hz) = preferred
|
||||
let (pw, ph, _hz) = preferred
|
||||
.context("IDD push needs the negotiated mode (WxH) to size the shared ring")?;
|
||||
// Size the ring to the display's ACTUAL current resolution if it differs from the negotiated mode:
|
||||
// a fullscreen game can hold the virtual display at a different mode (esp. across a reconnect), so
|
||||
// matching the actual mode lets the first frame flow instead of being dropped (game-capture bug
|
||||
// GB1). Falls back to the negotiated mode when the CCD read is unavailable.
|
||||
let (w, h) =
|
||||
unsafe { crate::win_display::active_resolution(target.target_id) }.unwrap_or((pw, ph));
|
||||
if (w, h) != (pw, ph) {
|
||||
tracing::info!(
|
||||
target_id = target.target_id,
|
||||
negotiated = format!("{pw}x{ph}"),
|
||||
actual = format!("{w}x{h}"),
|
||||
"IDD push: sizing the ring to the display's actual mode (differs from negotiated)"
|
||||
);
|
||||
}
|
||||
// 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
|
||||
@@ -496,6 +514,7 @@ impl IddPushCapturer {
|
||||
client_10bit,
|
||||
display_hdr,
|
||||
last_acm_poll: Instant::now(),
|
||||
recovering_since: None,
|
||||
out_ring: Vec::new(),
|
||||
out_idx: 0,
|
||||
hdr_conv: None,
|
||||
@@ -507,37 +526,47 @@ impl IddPushCapturer {
|
||||
// it back to the caller for the DDA fallback (audit §5.1).
|
||||
_keepalive: Box::new(()),
|
||||
};
|
||||
// Bounded wait for the driver to ATTACH to the ring (it writes DRV_STATUS_OPENED). An attach
|
||||
// failure (e.g. the OS rendered the IDD on a different GPU than our ring → DRV_STATUS_TEX_FAIL)
|
||||
// becomes an open failure the caller falls back from, instead of next_frame's 20 s deadline.
|
||||
// Bounded wait for the driver to ATTACH to the ring AND publish a first frame. An attach
|
||||
// failure (DRV_STATUS_TEX_FAIL) or an attach-but-no-frames (a game left the display in a
|
||||
// format/size the ring can't match) becomes an open failure the caller falls back from (→ DDA),
|
||||
// instead of next_frame's 20 s black-then-bail.
|
||||
me.wait_for_attach()?;
|
||||
Ok(me)
|
||||
}
|
||||
}
|
||||
|
||||
/// Block (bounded) until the driver attaches to the host ring, else fail so the caller can fall back
|
||||
/// to DDA (audit §5.1). Checks `driver_status` (NOT frame arrival — an idle desktop may present no
|
||||
/// frame yet), so it never falsely fails on the happy path: the driver writes `DRV_STATUS_OPENED` as
|
||||
/// soon as it opens the ring textures, regardless of whether DWM has composed a frame.
|
||||
/// Block (bounded) until the driver has ATTACHED to the host ring (`DRV_STATUS_OPENED`) **and published
|
||||
/// a first frame**, else fail so the caller can fall back to DDA (audit §5.1 +
|
||||
/// `docs/windows-host-rewrite-game-capture-bug.md` P3/Stage 1).
|
||||
///
|
||||
/// Requiring the first frame — not just the attach — catches the *reconnect-into-a-broken-state* case:
|
||||
/// a fullscreen game can leave the virtual display in a format/size that the driver's `publish()` guard
|
||||
/// rejects, so the driver ATTACHES but silently drops every frame; without this the host sails past
|
||||
/// `open()` and only dies on `next_frame`'s 20 s deadline (the "reconnect = black + audio" symptom). At
|
||||
/// session open the OS activates the virtual display → DWM composites it → a frame arrives within ~1 s,
|
||||
/// so this does not false-fail a normal (even idle) open; no frame within the window = genuinely broken.
|
||||
fn wait_for_attach(&self) -> Result<()> {
|
||||
let deadline = Instant::now() + Duration::from_secs(4);
|
||||
loop {
|
||||
// Plain read: the driver writes this u32; an aligned u32 read can't tear (same access as
|
||||
// log_driver_status_once).
|
||||
let st = unsafe { (*self.header).driver_status };
|
||||
match st {
|
||||
DRV_STATUS_OPENED => return Ok(()),
|
||||
DRV_STATUS_TEX_FAIL | DRV_STATUS_NO_DEVICE1 => {
|
||||
let detail = unsafe { (*self.header).driver_status_detail };
|
||||
bail!(
|
||||
"IDD-push driver failed to attach (driver_status={st} detail=0x{detail:08x} — \
|
||||
render-adapter mismatch?)"
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
if matches!(st, DRV_STATUS_TEX_FAIL | DRV_STATUS_NO_DEVICE1) {
|
||||
let detail = unsafe { (*self.header).driver_status_detail };
|
||||
bail!(
|
||||
"IDD-push driver failed to attach (driver_status={st} detail=0x{detail:08x} — \
|
||||
render-adapter mismatch?)"
|
||||
);
|
||||
}
|
||||
// Attached AND a frame has been published — the publish token's seq advances past 0.
|
||||
if st == DRV_STATUS_OPENED && frame::FrameToken::unpack(self.latest()).seq != 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if Instant::now() > deadline {
|
||||
bail!("IDD-push driver did not attach within 4s (driver_status={st})");
|
||||
bail!(
|
||||
"IDD-push: driver_status={st} but no frame published within 4s — the virtual display \
|
||||
is likely in a format/size the ring can't match (fullscreen game?); falling back"
|
||||
);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
@@ -643,8 +672,10 @@ impl IddPushCapturer {
|
||||
/// 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<()> {
|
||||
fn recreate_ring(&mut self, new_display_hdr: bool, new_w: u32, new_h: u32) -> Result<()> {
|
||||
self.display_hdr = new_display_hdr;
|
||||
self.width = new_w;
|
||||
self.height = new_h;
|
||||
let fmt = self.ring_format();
|
||||
let new_gen = IDD_GENERATION.fetch_add(1, Ordering::Relaxed);
|
||||
let new_slots = unsafe {
|
||||
@@ -665,6 +696,8 @@ impl IddPushCapturer {
|
||||
(*(std::ptr::addr_of!((*self.header).latest) as *const AtomicU64))
|
||||
.store(0, Ordering::Relaxed);
|
||||
(*self.header).dxgi_format = fmt.0 as u32;
|
||||
(*self.header).width = new_w;
|
||||
(*self.header).height = new_h;
|
||||
// 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);
|
||||
@@ -689,16 +722,23 @@ impl IddPushCapturer {
|
||||
}
|
||||
self.last_acm_poll = Instant::now();
|
||||
let now_hdr = unsafe { crate::win_display::advanced_color_enabled(self.target_id) };
|
||||
if now_hdr == self.display_hdr {
|
||||
// Follow the display's ACTUAL resolution too — a fullscreen game can mode-set the virtual display
|
||||
// out from under the negotiated size (game-capture bug GB1). Unknown read → keep our current size.
|
||||
let (now_w, now_h) = unsafe { crate::win_display::active_resolution(self.target_id) }
|
||||
.unwrap_or((self.width, self.height));
|
||||
if now_hdr == self.display_hdr && now_w == self.width && now_h == self.height {
|
||||
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"
|
||||
from = format!("{}x{} hdr={}", self.width, self.height, self.display_hdr),
|
||||
to = format!("{now_w}x{now_h} hdr={now_hdr}"),
|
||||
"IDD push: display descriptor changed — recreating the ring at the new mode"
|
||||
);
|
||||
if let Err(e) = self.recreate_ring(now_hdr) {
|
||||
// Start the recovery clock (if not already running): if a fresh frame doesn't resume within the
|
||||
// window, try_consume drops the session rather than freeze.
|
||||
self.recovering_since.get_or_insert_with(Instant::now);
|
||||
if let Err(e) = self.recreate_ring(now_hdr, now_w, now_h) {
|
||||
tracing::warn!(error = %format!("{e:#}"), "IDD push: ring recreate failed");
|
||||
}
|
||||
}
|
||||
@@ -755,6 +795,17 @@ impl IddPushCapturer {
|
||||
self.log_driver_status_once();
|
||||
// Follow the display: a "Use HDR" flip recreates the ring at the matching format.
|
||||
self.poll_display_hdr();
|
||||
// Recover-or-drop (GB1): if a descriptor change triggered a recreate but no fresh frame has resumed
|
||||
// within the window, the IDD-push path can't follow the display (e.g. an exclusive-flip) — drop the
|
||||
// session cleanly (the loop's `?` ends it → the client reconnects) rather than freeze forever.
|
||||
if let Some(since) = self.recovering_since {
|
||||
if since.elapsed() > Duration::from_secs(3) {
|
||||
bail!(
|
||||
"IDD-push: display descriptor changed and the ring could not recover within 3s — \
|
||||
dropping the session so the client reconnects"
|
||||
);
|
||||
}
|
||||
}
|
||||
let latest = self.latest();
|
||||
// `latest` is the proto publish token `(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
|
||||
@@ -804,6 +855,7 @@ impl IddPushCapturer {
|
||||
self.out_idx = (i + 1) % self.out_ring.len();
|
||||
self.last_seq = seq;
|
||||
self.last_present = Some((out.clone(), pf));
|
||||
self.recovering_since = None; // a fresh frame resumed → recovered
|
||||
Ok(Some(CapturedFrame {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
@@ -942,11 +994,7 @@ impl Capturer for IddPushCapturer {
|
||||
// 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)
|
||||
crate::config::config().idd_depth.clamp(1, OUT_RING)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
//! `HostConfig` — the host's runtime knobs parsed ONCE from the environment, instead of the ~68 scattered
|
||||
//! `env::var` reads recomputed at every call site (some up to 8×, which lets capture + encode silently
|
||||
//! disagree on the resolved backend — plan §2.4). The service / launcher loads `host.env` into the process
|
||||
//! environment before the host starts, and **for the knobs captured here the environment is constant for the
|
||||
//! process lifetime**, so a lazily-parsed global is equivalent to "parsed once at startup".
|
||||
//!
|
||||
//! **Goal-1 stages 1–2** (`docs/windows-host-goal1-plan.md`): stage 1 stood this up; stage 2 migrated the
|
||||
//! genuinely-constant operator/dispatch knobs onto it (the dispatch-disagreement bug class: `idd_push`,
|
||||
//! `capture_backend`, `encoder_pref`, `render_adapter`, `no_wgc`, the vdisplay backend select — plus the
|
||||
//! plan-named `secure_dda`/`idd_depth`/`zerocopy`/`ten_bit` and the multi-site `perf`/`compositor`/
|
||||
//! `video_source`/`gamepad`). `SessionPlan` (stage 3) consumes it as the single owner of the
|
||||
//! capture/topology/encoder decision.
|
||||
//!
|
||||
//! **What is deliberately NOT here (and must stay a live `env::var` read):**
|
||||
//! - **Runtime-mutated session vars.** On Linux, [`crate::vdisplay::apply_session_env`] rewrites the process
|
||||
//! env on *every connect* so one host follows a Bazzite box across Gaming↔Desktop: `WAYLAND_DISPLAY`,
|
||||
//! `XDG_CURRENT_DESKTOP`, `XDG_RUNTIME_DIR`, `DBUS_SESSION_BUS_ADDRESS`, and the *derived* `PUNKTFUNK_*`
|
||||
//! vars `INPUT_BACKEND`, `GAMESCOPE_SESSION`/`GAMESCOPE_NODE`, `KWIN_VIRTUAL_PRIMARY`,
|
||||
//! `MUTTER_VIRTUAL_PRIMARY`, `FORCE_SHM` (+ `GAMESCOPE_APP` on the launch path). Parsing these once would
|
||||
//! freeze them at startup and silently break session-following — they are NOT constant.
|
||||
//! - **Single-use local tuning** read exactly where it is used (no resolve-once benefit, and a parse with a
|
||||
//! call-site-local default/clamp): e.g. `FEC_PCT` (two *different* semantics — GameStream default-20 vs
|
||||
//! punktfunk/1 `Option`/clamp-90), `VIDEO_DROP`, `VBV_FRAMES`, `SPLIT_ENCODE`, `PACE_BURST_KB`, the
|
||||
//! `capture/dxgi.rs` timing knobs, the `*_LIVE` test gates.
|
||||
//! - **Path / genuinely-dynamic reads**: the config-dir resolution, `PATH` executable search, the
|
||||
//! env-forward-to-child loop, `PUNKTFUNK_MGMT_TOKEN`, `PUNKTFUNK_HOST_CMD`, `PUNKTFUNK_RENDER_NODE`.
|
||||
//!
|
||||
//! `PUNKTFUNK_ZEROCOPY` note: this field uses **presence** semantics (`var_os(..).is_some()`) to match the
|
||||
//! Windows `encode/ffmpeg_win.rs` reader. The Linux `zerocopy` module keeps its own *truthy* parser
|
||||
//! (`1|true|yes|on`) — the two are independent features that share a name; do NOT conflate them.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Resolved host configuration. Holds the genuinely-constant operator/dispatch knobs (see module docs for
|
||||
/// what is deliberately excluded). Fields read on only one platform are kept alive cross-platform by the
|
||||
/// derived `Debug` impl, so the parser can stay a single platform-neutral function.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HostConfig {
|
||||
/// `PUNKTFUNK_IDD_PUSH` — use the IDD direct-push capturer (in-process Session-0 capture; no WGC helper).
|
||||
pub idd_push: bool,
|
||||
/// `PUNKTFUNK_ENCODER` — explicit encoder-backend override (lowercased; empty = auto-detect by GPU vendor).
|
||||
pub encoder_pref: String,
|
||||
/// `PUNKTFUNK_NO_HELPER` — never spawn the user-session WGC helper.
|
||||
pub no_helper: bool,
|
||||
/// `PUNKTFUNK_FORCE_HELPER` — force the WGC helper even when not running as SYSTEM.
|
||||
pub force_helper: bool,
|
||||
/// `PUNKTFUNK_NO_WGC` — force the pure single-process DDA path (skip WGC and the two-process relay).
|
||||
pub no_wgc: bool,
|
||||
/// `PUNKTFUNK_CAPTURE` — explicit Windows capture-backend override (lowercased; `dda`/`dxgi` vs the WGC default).
|
||||
pub capture_backend: String,
|
||||
/// `PUNKTFUNK_RENDER_ADAPTER` — discrete render-GPU pin by description substring (`Some` even when empty:
|
||||
/// the empty string still counts as "set" for the presence checks, and the value reader filters it).
|
||||
pub render_adapter: Option<String>,
|
||||
/// `PUNKTFUNK_SECURE_DDA` — enable the experimental DDA-on-secure-desktop (Winlogon/UAC) mux leg.
|
||||
pub secure_dda: bool,
|
||||
/// `PUNKTFUNK_IDD_DEPTH` — IDD-push pipeline depth override (default 2; the call site clamps to its `OUT_RING`).
|
||||
pub idd_depth: usize,
|
||||
/// `PUNKTFUNK_ZEROCOPY` — opt into the Windows D3D11 zero-copy encode path (presence semantics; see module docs).
|
||||
pub zerocopy: bool,
|
||||
/// `PUNKTFUNK_10BIT` — host policy gate for HEVC Main10 (only honored when the client also advertised 10-bit).
|
||||
pub ten_bit: bool,
|
||||
/// `PUNKTFUNK_PERF` — per-stage timing instrumentation.
|
||||
pub perf: bool,
|
||||
/// `PUNKTFUNK_VIDEO_SOURCE` — GameStream video source select (`virtual` / `portal` / unset → synthetic).
|
||||
pub video_source: Option<String>,
|
||||
/// `PUNKTFUNK_COMPOSITOR` — explicit compositor override (operator/CI/test). NOT the runtime-detected
|
||||
/// session — this one is a constant operator knob; `apply_session_env` never writes it.
|
||||
pub compositor: Option<String>,
|
||||
/// `PUNKTFUNK_GAMEPAD` — client/operator virtual-pad backend preference (fed to `pick_gamepad`).
|
||||
pub gamepad: Option<String>,
|
||||
/// `PUNKTFUNK_VDISPLAY` — Windows virtual-display backend select (`pf`/`pfvd` vs `sudovda`; else auto-detect).
|
||||
pub vdisplay: Option<String>,
|
||||
}
|
||||
|
||||
impl HostConfig {
|
||||
fn from_env() -> Self {
|
||||
// Presence flag: set ⇒ true. Matches the original `var_os(k).is_some()` reads (and the few
|
||||
// `var(k).is_ok()` flag reads, which coincide for every real-world value).
|
||||
let flag = |k: &str| std::env::var_os(k).is_some();
|
||||
// String value: `var(k).ok()` — `Some` (possibly empty) when set with valid UTF-8, else `None`.
|
||||
let val = |k: &str| std::env::var(k).ok();
|
||||
Self {
|
||||
idd_push: flag("PUNKTFUNK_IDD_PUSH"),
|
||||
encoder_pref: std::env::var("PUNKTFUNK_ENCODER")
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase(),
|
||||
no_helper: flag("PUNKTFUNK_NO_HELPER"),
|
||||
force_helper: flag("PUNKTFUNK_FORCE_HELPER"),
|
||||
no_wgc: flag("PUNKTFUNK_NO_WGC"),
|
||||
capture_backend: std::env::var("PUNKTFUNK_CAPTURE")
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase(),
|
||||
render_adapter: val("PUNKTFUNK_RENDER_ADAPTER"),
|
||||
secure_dda: flag("PUNKTFUNK_SECURE_DDA"),
|
||||
idd_depth: val("PUNKTFUNK_IDD_DEPTH")
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(2),
|
||||
zerocopy: flag("PUNKTFUNK_ZEROCOPY"),
|
||||
ten_bit: flag("PUNKTFUNK_10BIT"),
|
||||
perf: flag("PUNKTFUNK_PERF"),
|
||||
video_source: val("PUNKTFUNK_VIDEO_SOURCE"),
|
||||
compositor: val("PUNKTFUNK_COMPOSITOR"),
|
||||
gamepad: val("PUNKTFUNK_GAMEPAD"),
|
||||
vdisplay: val("PUNKTFUNK_VDISPLAY"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The process-wide host configuration, parsed once on first access.
|
||||
pub fn config() -> &'static HostConfig {
|
||||
static CFG: OnceLock<HostConfig> = OnceLock::new();
|
||||
CFG.get_or_init(HostConfig::from_env)
|
||||
}
|
||||
@@ -173,14 +173,12 @@ pub fn open_video(
|
||||
// AMD/Intel → VAAPI (one libavcodec backend for both). Auto-detect by default so a single
|
||||
// Linux binary serves any GPU; `PUNKTFUNK_ENCODER` forces a specific backend (and surfaces
|
||||
// its errors crisply instead of silently trying the other).
|
||||
let pref = std::env::var("PUNKTFUNK_ENCODER")
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
let pref = crate::config::config().encoder_pref.as_str();
|
||||
let open_vaapi = || -> Result<Box<dyn Encoder>> {
|
||||
vaapi::VaapiEncoder::open(codec, format, width, height, fps, bitrate_bps, bit_depth)
|
||||
.map(|e| Box::new(e) as Box<dyn Encoder>)
|
||||
};
|
||||
match pref.as_str() {
|
||||
match pref {
|
||||
"nvenc" | "nvidia" | "cuda" => open_nvenc_probed(
|
||||
codec,
|
||||
format,
|
||||
@@ -379,11 +377,7 @@ fn nvidia_present() -> bool {
|
||||
/// passthrough for VAAPI vs the EGL→CUDA import for NVENC).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn linux_zero_copy_is_vaapi() -> bool {
|
||||
match std::env::var("PUNKTFUNK_ENCODER")
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
match crate::config::config().encoder_pref.as_str() {
|
||||
"nvenc" | "nvidia" | "cuda" => false,
|
||||
"vaapi" | "amd" | "intel" => true,
|
||||
_ => !nvidia_present(),
|
||||
@@ -450,10 +444,8 @@ enum GpuVendor {
|
||||
/// vendor). Shared by [`open_video`] and the GameStream codec advertisement so both agree.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn windows_resolved_backend() -> WindowsBackend {
|
||||
let pref = std::env::var("PUNKTFUNK_ENCODER")
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
match pref.as_str() {
|
||||
// Resolved ONCE in HostConfig (Goal-1) — was re-read from PUNKTFUNK_ENCODER on every call.
|
||||
match crate::config::config().encoder_pref.as_str() {
|
||||
"nvenc" | "hw" | "nvidia" | "cuda" => WindowsBackend::Nvenc,
|
||||
"amf" | "amd" => WindowsBackend::Amf,
|
||||
"qsv" | "intel" => WindowsBackend::Qsv,
|
||||
|
||||
@@ -109,7 +109,7 @@ impl WinVendor {
|
||||
/// Is the zero-copy D3D11 path enabled? Opt-in (`PUNKTFUNK_ZEROCOPY=1`) until on-glass validated;
|
||||
/// the default is the robust system-memory readback path.
|
||||
fn zerocopy_enabled() -> bool {
|
||||
std::env::var_os("PUNKTFUNK_ZEROCOPY").is_some()
|
||||
crate::config::config().zerocopy
|
||||
}
|
||||
|
||||
/// The swscale *source* pixel format for a captured packed-RGB/BGR layout (8-bit BGRA fallback only).
|
||||
|
||||
@@ -102,7 +102,7 @@ fn run(
|
||||
// request and capture it (no scaling). Self-contained — deliberately NOT pooled in
|
||||
// `video_cap`, since a reconnect at a different resolution needs a freshly-sized output; the
|
||||
// output is released when this capturer drops at stream end (RAII via its keepalive).
|
||||
if std::env::var("PUNKTFUNK_VIDEO_SOURCE").as_deref() == Ok("virtual") {
|
||||
if crate::config::config().video_source.as_deref() == Some("virtual") {
|
||||
// The launched app picks the compositor (e.g. gamescope for game entries) and the
|
||||
// nested command.
|
||||
let compositor = app
|
||||
@@ -134,8 +134,12 @@ fn run(
|
||||
// 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, false).context("capture virtual output")?;
|
||||
let mut capturer = capture::capture_virtual_output(
|
||||
vout,
|
||||
false,
|
||||
crate::session_plan::CaptureBackend::resolve(),
|
||||
)
|
||||
.context("capture virtual output")?;
|
||||
capturer.set_active(true);
|
||||
return stream_body(&mut *capturer, &sock, cfg, running, force_idr, rfi_range);
|
||||
}
|
||||
@@ -147,7 +151,7 @@ fn run(
|
||||
tracing::info!("video source: reusing capturer");
|
||||
c
|
||||
}
|
||||
None if std::env::var("PUNKTFUNK_VIDEO_SOURCE").is_ok_and(|v| v == "portal") => {
|
||||
None if crate::config::config().video_source.as_deref() == Some("portal") => {
|
||||
tracing::info!("video source: portal desktop capture");
|
||||
capture::open_portal_monitor().context("open portal capturer")?
|
||||
}
|
||||
@@ -358,7 +362,7 @@ fn stream_body(
|
||||
|
||||
// Per-stage timing (PUNKTFUNK_PERF=1): max µs/stage per second + unique vs re-encoded frames,
|
||||
// to pinpoint stalls. `unique` counts genuinely-new captured frames (vs re-encoded holds).
|
||||
let perf = std::env::var_os("PUNKTFUNK_PERF").is_some();
|
||||
let perf = crate::config::config().perf;
|
||||
let (mut mx_cap, mut mx_enc, mut mx_pkt, mut mx_send, mut mx_pkts, mut uniq) =
|
||||
(0u128, 0u128, 0u128, 0u128, 0usize, 0u32);
|
||||
// Absolute next-frame deadline — the single pacing clock for the loop.
|
||||
|
||||
@@ -112,8 +112,10 @@ pub fn default_backend() -> Backend {
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
if std::env::var("PUNKTFUNK_COMPOSITOR")
|
||||
.is_ok_and(|v| v.trim().eq_ignore_ascii_case("gamescope"))
|
||||
if crate::config::config()
|
||||
.compositor
|
||||
.as_deref()
|
||||
.is_some_and(|v| v.trim().eq_ignore_ascii_case("gamescope"))
|
||||
{
|
||||
return Backend::GamescopeEi;
|
||||
}
|
||||
@@ -260,8 +262,10 @@ fn coalesce(events: Vec<InputEvent>) -> Vec<InputEvent> {
|
||||
/// (`org.gnome.Mutter.RemoteDesktop`), the same direct API the Mutter video backend uses.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn libei_ei_source() -> libei::EiSource {
|
||||
let gnome = std::env::var("PUNKTFUNK_COMPOSITOR")
|
||||
.is_ok_and(|v| v.trim().eq_ignore_ascii_case("mutter"))
|
||||
let gnome = crate::config::config()
|
||||
.compositor
|
||||
.as_deref()
|
||||
.is_some_and(|v| v.trim().eq_ignore_ascii_case("mutter"))
|
||||
|| std::env::var("XDG_CURRENT_DESKTOP")
|
||||
.unwrap_or_default()
|
||||
.to_ascii_uppercase()
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
mod audio;
|
||||
mod capture;
|
||||
mod config;
|
||||
mod discovery;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod dmabuf_fence;
|
||||
@@ -34,6 +35,7 @@ mod punktfunk1;
|
||||
mod pwinit;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod service;
|
||||
mod session_plan;
|
||||
mod session_tuning;
|
||||
mod spike;
|
||||
mod vdisplay;
|
||||
|
||||
@@ -599,7 +599,7 @@ async fn serve_session(
|
||||
// opted in (PUNKTFUNK_10BIT). A client that can't decode 10-bit (caps bit clear, or an older
|
||||
// client) always gets the 8-bit stream. PUNKTFUNK_10BIT is the host policy gate until a
|
||||
// mgmt/console toggle replaces it.
|
||||
let host_wants_10bit = std::env::var_os("PUNKTFUNK_10BIT").is_some();
|
||||
let host_wants_10bit = crate::config::config().ten_bit;
|
||||
let client_supports_10bit = hello.video_caps & punktfunk_core::quic::VIDEO_CAP_10BIT != 0;
|
||||
let bit_depth: u8 = if host_wants_10bit && client_supports_10bit {
|
||||
10
|
||||
@@ -1616,7 +1616,7 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
|
||||
/// Resolve the client's gamepad-backend preference (the env/logging shell around
|
||||
/// [`pick_gamepad`]). Always concrete — the `Welcome` reports what the session will drive.
|
||||
fn resolve_gamepad(pref: GamepadPref) -> GamepadPref {
|
||||
let env = std::env::var("PUNKTFUNK_GAMEPAD").ok();
|
||||
let env = crate::config::config().gamepad.clone();
|
||||
let chosen = pick_gamepad(
|
||||
pref,
|
||||
env.as_deref(),
|
||||
@@ -1683,7 +1683,7 @@ fn resolve_compositor(pref: CompositorPref) -> Result<crate::vdisplay::Composito
|
||||
{
|
||||
// Explicit operator override (legacy / CI / forcing a backend for a test) wins and is assumed
|
||||
// to come with a hand-set env — don't retarget the process env in that case.
|
||||
let overridden = std::env::var_os("PUNKTFUNK_COMPOSITOR").is_some();
|
||||
let overridden = crate::config::config().compositor.is_some();
|
||||
let detected = if overridden {
|
||||
crate::vdisplay::detect().ok()
|
||||
} else {
|
||||
@@ -2184,12 +2184,18 @@ fn virtual_stream(
|
||||
// This thread runs the capture+encode loop (single-process: Linux / synthetic / NO_WGC DDA) — or
|
||||
// tail-calls the relay below. Elevate it so a CPU-heavy game can't deschedule our GPU submission.
|
||||
boost_thread_priority(true);
|
||||
// Resolve the per-session capture / topology / encoder decision ONCE (Goal-1 stage 3): the deployed
|
||||
// path now reads this typed `SessionPlan` instead of re-deriving from config at each dispatch site
|
||||
// (the latent "capture and encode disagree on the backend" hazard, plan §2.4). `bit_depth` is the
|
||||
// only per-session input — capture/topology/encoder are otherwise pure functions of `HostConfig`.
|
||||
let plan = crate::session_plan::SessionPlan::resolve(bit_depth);
|
||||
tracing::info!(?plan, "resolved session plan");
|
||||
// Windows two-process secure-desktop path: when the host runs as SYSTEM (required for the secure
|
||||
// desktop + SendInput), WGC can't activate in-process, so we capture the normal desktop via a
|
||||
// helper spawned in the user session and relay its AUs. (Single-process WGC/DDA is used as the
|
||||
// user, and stays the path on Linux.) See docs/windows-secure-desktop.md.
|
||||
#[cfg(target_os = "windows")]
|
||||
if should_use_helper() {
|
||||
if plan.topology == crate::session_plan::SessionTopology::TwoProcessRelay {
|
||||
return virtual_stream_relay(
|
||||
session,
|
||||
mode,
|
||||
@@ -2220,11 +2226,11 @@ fn virtual_stream(
|
||||
// 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());
|
||||
let idd_push_session = plan.capture == crate::session_plan::CaptureBackend::IddPush;
|
||||
#[cfg(target_os = "windows")]
|
||||
if std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() {
|
||||
let idd_setup_guard = idd_push_session.then(|| IDD_SETUP_LOCK.lock().unwrap());
|
||||
#[cfg(target_os = "windows")]
|
||||
if idd_push_session {
|
||||
let prev = IDD_SESSION_STOP.lock().unwrap().replace(stop.clone());
|
||||
if let Some(prev_stop) = prev {
|
||||
prev_stop.store(true, Ordering::SeqCst);
|
||||
@@ -2233,7 +2239,7 @@ fn virtual_stream(
|
||||
}
|
||||
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)?;
|
||||
build_pipeline_with_retry(&mut vd, mode, bitrate_kbps, bit_depth, plan)?;
|
||||
// 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);
|
||||
@@ -2251,7 +2257,7 @@ fn virtual_stream(
|
||||
#[cfg(target_os = "windows")]
|
||||
let _composed_flip = crate::capture::composed_flip::ForceComposedFlip::start();
|
||||
|
||||
let perf = std::env::var("PUNKTFUNK_PERF").is_ok();
|
||||
let perf = crate::config::config().perf;
|
||||
// Microburst cap (applied in send_loop/paced_submit): a frame ≤ this bursts out immediately;
|
||||
// only a bigger frame's overflow is spread. PUNKTFUNK_PACE_BURST_KB overrides the 128 KB default.
|
||||
let burst_cap = std::env::var("PUNKTFUNK_PACE_BURST_KB")
|
||||
@@ -2291,7 +2297,7 @@ fn virtual_stream(
|
||||
let mut compositor = compositor;
|
||||
let (session_tx, session_rx) = std::sync::mpsc::channel::<SessionSwitch>();
|
||||
let watch = std::env::var_os("PUNKTFUNK_SESSION_WATCH").is_some()
|
||||
&& std::env::var_os("PUNKTFUNK_COMPOSITOR").is_none();
|
||||
&& crate::config::config().compositor.is_none();
|
||||
let _watcher = if watch {
|
||||
let stop = stop.clone();
|
||||
std::thread::Builder::new()
|
||||
@@ -2362,6 +2368,7 @@ fn virtual_stream(
|
||||
cur_mode,
|
||||
bitrate_kbps,
|
||||
bit_depth,
|
||||
plan,
|
||||
)?;
|
||||
Ok((new_vd, pipe))
|
||||
})();
|
||||
@@ -2405,7 +2412,7 @@ fn virtual_stream(
|
||||
// Build the new pipeline BEFORE dropping the old one: the host already acked
|
||||
// the switch as accepted, so a rebuild failure must not kill an otherwise
|
||||
// healthy session — keep streaming the current mode and log instead.
|
||||
match build_pipeline(&mut vd, new_mode, bitrate_kbps, bit_depth) {
|
||||
match build_pipeline(&mut vd, new_mode, bitrate_kbps, bit_depth, plan) {
|
||||
Ok(next_pipe) => {
|
||||
(capturer, enc, frame, interval) = next_pipe;
|
||||
cur_mode = new_mode;
|
||||
@@ -2450,7 +2457,7 @@ fn virtual_stream(
|
||||
tracing::warn!(error = %format!("{e:#}"), rebuild = capture_rebuilds,
|
||||
"capture lost — rebuilding pipeline in place");
|
||||
let (new_cap, new_enc, new_frame, new_interval) =
|
||||
build_pipeline_with_retry(&mut vd, cur_mode, bitrate_kbps, bit_depth)
|
||||
build_pipeline_with_retry(&mut vd, cur_mode, bitrate_kbps, bit_depth, plan)
|
||||
.context("rebuild after capture loss")?;
|
||||
capturer = new_cap;
|
||||
enc = new_enc;
|
||||
@@ -2569,29 +2576,6 @@ fn virtual_stream(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Should this host take the two-process (SYSTEM host + user-session WGC helper) path? Yes when it's
|
||||
/// running as SYSTEM — the only account that can capture the secure desktop + drive SendInput on it,
|
||||
/// and the account under which in-process WGC won't activate. `PUNKTFUNK_FORCE_HELPER` forces it on
|
||||
/// (for testing the relay as a normal user); `PUNKTFUNK_NO_HELPER` forces it off. `PUNKTFUNK_NO_WGC`
|
||||
/// also forces it off — that mode runs pure single-process DDA (one capturer for the normal AND secure
|
||||
/// desktop, Apollo-style), which has no WGC helper to relay.
|
||||
#[cfg(target_os = "windows")]
|
||||
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()
|
||||
}
|
||||
|
||||
/// Windows two-process video stream: the SYSTEM host creates the SudoVDA virtual output (and holds
|
||||
/// its keepalive = the sole topology/isolation owner), spawns the WGC helper in the user session to
|
||||
/// capture+encode the NORMAL desktop, and relays the helper's AUs onto the QUIC data plane via the
|
||||
@@ -2731,7 +2715,7 @@ fn virtual_stream_relay(
|
||||
})
|
||||
};
|
||||
|
||||
let perf = std::env::var("PUNKTFUNK_PERF").is_ok();
|
||||
let perf = crate::config::config().perf;
|
||||
let burst_cap = std::env::var("PUNKTFUNK_PACE_BURST_KB")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
@@ -2770,7 +2754,7 @@ fn virtual_stream_relay(
|
||||
// the secure desktop's HDR independent-flip (it storms ACCESS_LOST → black), whereas the WGC helper
|
||||
// STAYS LIVE through a lock/UAC. So by default the mux keeps WGC the whole time (no DesktopWatcher
|
||||
// switch, no overlay). Enable the experimental DDA-on-secure path with PUNKTFUNK_SECURE_DDA=1.
|
||||
let dda_secure = std::env::var("PUNKTFUNK_SECURE_DDA").is_ok() || secure_test_ms.is_some();
|
||||
let dda_secure = crate::config::config().secure_dda || secure_test_ms.is_some();
|
||||
// The authoritative Default↔Winlogon signal (requires SYSTEM to read the Winlogon desktop name);
|
||||
// only needed when the DDA-on-secure path is enabled.
|
||||
let watcher = dda_secure.then(crate::capture::desktop_watch::DesktopWatcher::start);
|
||||
@@ -3041,6 +3025,7 @@ fn build_pipeline_with_retry(
|
||||
mode: punktfunk_core::Mode,
|
||||
bitrate_kbps: u32,
|
||||
bit_depth: u8,
|
||||
plan: crate::session_plan::SessionPlan,
|
||||
) -> Result<Pipeline> {
|
||||
// ~10s first-frame wait per attempt. 8 gives a ~90s budget for the SLOW case: a host-managed
|
||||
// gamescope session cold-starting Steam Big Picture (the SteamOS/Bazzite takeover) can take
|
||||
@@ -3050,7 +3035,7 @@ fn build_pipeline_with_retry(
|
||||
const MAX_ATTEMPTS: u32 = 8;
|
||||
let mut backoff = std::time::Duration::from_millis(500);
|
||||
for attempt in 1..=MAX_ATTEMPTS {
|
||||
match build_pipeline(vd, mode, bitrate_kbps, bit_depth) {
|
||||
match build_pipeline(vd, mode, bitrate_kbps, bit_depth, plan) {
|
||||
Ok(pipe) => {
|
||||
if attempt > 1 {
|
||||
tracing::info!(attempt, "pipeline up after retry");
|
||||
@@ -3109,6 +3094,7 @@ fn build_pipeline(
|
||||
mode: punktfunk_core::Mode,
|
||||
bitrate_kbps: u32,
|
||||
bit_depth: u8,
|
||||
plan: crate::session_plan::SessionPlan,
|
||||
) -> Result<Pipeline> {
|
||||
let vout = vd.create(mode).context("create virtual output")?;
|
||||
// The backend reports the refresh it actually achieved in `preferred_mode.2` (KWin may cap a
|
||||
@@ -3131,7 +3117,7 @@ fn build_pipeline(
|
||||
// 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)
|
||||
let mut capturer = crate::capture::capture_virtual_output(vout, plan.hdr, plan.capture)
|
||||
.context("capture virtual output")?;
|
||||
capturer.set_active(true);
|
||||
let frame = capturer.next_frame().context("first frame")?;
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
//! `SessionPlan` — the per-session capture / topology / encoder decision, resolved **once** from
|
||||
//! [`HostConfig`](crate::config) (+ the handshake-negotiated bit depth) into a typed, logged value.
|
||||
//!
|
||||
//! **Goal-1 stage 3** (`docs/windows-host-goal1-plan.md`): before this, the Windows session decision was
|
||||
//! re-derived at three call sites — the capture backend inside `capture::capture_virtual_output`, the
|
||||
//! process topology in `punktfunk1::should_use_helper`, and the encode backend in
|
||||
//! `encode::windows_resolved_backend` — each reading [`config`](crate::config) independently, with no
|
||||
//! single owner (the latent "capture and encode disagree on the backend" hazard, plan §2.4). `SessionPlan`
|
||||
//! resolves them together, once, so the deployed path reads one typed artifact.
|
||||
//!
|
||||
//! Stage 3 routes the **capture** and **topology** decisions through the plan (see
|
||||
//! `capture::capture_virtual_output` taking [`CaptureBackend`] in, and `virtual_stream` reading
|
||||
//! [`SessionTopology`]). The **encoder** is resolved by `encode::windows_resolved_backend` (config-backed
|
||||
//! and GPU-vendor cached since stage 2, so already a single source) and *recorded* here as
|
||||
//! [`EncoderBackend`]. Threading `encoder`/`input_format` into the encoder + capturer opens — which
|
||||
//! removes the `capture → encode::windows_resolved_backend()` back-reference recomputed in `dxgi.rs` —
|
||||
//! is **stage 5**.
|
||||
//!
|
||||
//! The type is platform-neutral so it threads through the shared `virtual_stream`/`build_pipeline`
|
||||
//! signatures; on Linux it resolves to the single portal/single-process path (the 3-way dispatch is a
|
||||
//! Windows-only concern).
|
||||
|
||||
/// Where a session's frames come from.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CaptureBackend {
|
||||
/// Linux: the xdg ScreenCast portal → PipeWire (the only Linux capture path).
|
||||
Portal,
|
||||
/// Windows: IDD direct-push — frames pulled straight from the pf-vdisplay driver's shared ring
|
||||
/// (in-process, Session 0; no Desktop Duplication, no WGC helper).
|
||||
IddPush,
|
||||
/// Windows: DXGI Desktop Duplication (`PUNKTFUNK_CAPTURE=dda|dxgi` or `PUNKTFUNK_NO_WGC`).
|
||||
Dda,
|
||||
/// Windows: Windows.Graphics.Capture (the composed-desktop default), with a DDA watchdog fallback.
|
||||
Wgc,
|
||||
}
|
||||
|
||||
impl CaptureBackend {
|
||||
/// Resolve the capture backend from [`config`](crate::config). This is the single resolver shared by
|
||||
/// [`SessionPlan::resolve`] and the standalone callers (GameStream / spike), so they can't drift.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn resolve() -> Self {
|
||||
CaptureBackend::Portal
|
||||
}
|
||||
|
||||
/// Windows precedence (identical to the pre-stage-3 `capture_virtual_output` branch order):
|
||||
/// IDD-push wins; else an explicit `dda`/`dxgi` request or `PUNKTFUNK_NO_WGC` selects DDA; else WGC.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn resolve() -> Self {
|
||||
let cfg = crate::config::config();
|
||||
if cfg.idd_push {
|
||||
CaptureBackend::IddPush
|
||||
} else if matches!(cfg.capture_backend.as_str(), "dda" | "dxgi")
|
||||
|| crate::capture::wgc_disabled()
|
||||
{
|
||||
CaptureBackend::Dda
|
||||
} else {
|
||||
CaptureBackend::Wgc
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
pub fn resolve() -> Self {
|
||||
CaptureBackend::Portal
|
||||
}
|
||||
}
|
||||
|
||||
/// How a session is structured across processes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SessionTopology {
|
||||
/// One process captures + encodes (Linux; Windows non-SYSTEM / IDD-push / `NO_WGC`).
|
||||
SingleProcess,
|
||||
/// SYSTEM host + a user-session WGC helper relay (the Windows normal-desktop path under SYSTEM,
|
||||
/// where in-process WGC can't activate). See `virtual_stream_relay`.
|
||||
TwoProcessRelay,
|
||||
}
|
||||
|
||||
/// The resolved encode backend (recorded for logging / stages 4–5; the per-session encoder open still
|
||||
/// resolves via `encode::windows_resolved_backend`, which is config-backed + GPU-vendor cached).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EncoderBackend {
|
||||
/// Linux: NVENC vs VAAPI is auto-detected inside `encode::open_video` (not modeled here).
|
||||
PlatformAuto,
|
||||
Nvenc,
|
||||
Amf,
|
||||
Qsv,
|
||||
Software,
|
||||
}
|
||||
|
||||
/// The per-session decision, resolved once. `Copy` so it threads through the capture/encode chain
|
||||
/// without ceremony (stage 4 folds it, with the rest of the arg soup, into a `SessionContext`).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SessionPlan {
|
||||
pub capture: CaptureBackend,
|
||||
pub topology: SessionTopology,
|
||||
pub encoder: EncoderBackend,
|
||||
/// Handshake-negotiated encode bit depth (8, or 10 = HEVC Main10).
|
||||
pub bit_depth: u8,
|
||||
/// The IDD-push HDR hint (`bit_depth >= 10`) — the want-HDR flag the capturer was passed before.
|
||||
/// Non-IDD-push Windows backends ignore it and auto-detect HDR from the monitor; Linux is 8-bit.
|
||||
pub hdr: bool,
|
||||
}
|
||||
|
||||
impl SessionPlan {
|
||||
/// Resolve the whole plan once from [`config`](crate::config) + the negotiated `bit_depth`.
|
||||
pub fn resolve(bit_depth: u8) -> Self {
|
||||
SessionPlan {
|
||||
capture: CaptureBackend::resolve(),
|
||||
topology: resolve_topology(),
|
||||
encoder: resolve_encoder(),
|
||||
bit_depth,
|
||||
hdr: bit_depth >= 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process topology. On Windows this is the former `punktfunk1::should_use_helper` logic verbatim; on
|
||||
/// every other platform the session is always single-process.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn resolve_topology() -> SessionTopology {
|
||||
let cfg = crate::config::config();
|
||||
// `NO_HELPER`/`NO_WGC` force single-process; IDD-push captures in-process in Session 0 (no helper);
|
||||
// otherwise the helper runs when forced or when we're SYSTEM (in-process WGC can't activate there).
|
||||
let helper = if cfg.no_helper || crate::capture::wgc_disabled() {
|
||||
false
|
||||
} else if cfg.idd_push {
|
||||
false
|
||||
} else {
|
||||
cfg.force_helper || crate::capture::wgc_relay::running_as_system()
|
||||
};
|
||||
if helper {
|
||||
SessionTopology::TwoProcessRelay
|
||||
} else {
|
||||
SessionTopology::SingleProcess
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn resolve_topology() -> SessionTopology {
|
||||
SessionTopology::SingleProcess
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn resolve_encoder() -> EncoderBackend {
|
||||
match crate::encode::windows_resolved_backend() {
|
||||
crate::encode::WindowsBackend::Nvenc => EncoderBackend::Nvenc,
|
||||
crate::encode::WindowsBackend::Amf => EncoderBackend::Amf,
|
||||
crate::encode::WindowsBackend::Qsv => EncoderBackend::Qsv,
|
||||
crate::encode::WindowsBackend::Software => EncoderBackend::Software,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn resolve_encoder() -> EncoderBackend {
|
||||
EncoderBackend::PlatformAuto
|
||||
}
|
||||
@@ -76,7 +76,12 @@ pub fn run(opts: Options) -> Result<()> {
|
||||
refresh_hz: opts.fps,
|
||||
})
|
||||
.context("create virtual output")?;
|
||||
capture::capture_virtual_output(vout, false).context("capture virtual output")?
|
||||
capture::capture_virtual_output(
|
||||
vout,
|
||||
false,
|
||||
crate::session_plan::CaptureBackend::resolve(),
|
||||
)
|
||||
.context("capture virtual output")?
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -479,7 +479,7 @@ pub fn apply_input_env(_chosen: Compositor) {}
|
||||
/// a backend for a test), else the **live session** ([`detect_active_session`] — so a Bazzite box
|
||||
/// follows Gaming↔Desktop switches), else a last-resort `XDG_CURRENT_DESKTOP` read.
|
||||
pub fn detect() -> Result<Compositor> {
|
||||
if let Ok(v) = std::env::var("PUNKTFUNK_COMPOSITOR") {
|
||||
if let Some(v) = crate::config::config().compositor.as_deref() {
|
||||
return match v.trim().to_ascii_lowercase().as_str() {
|
||||
"kwin" | "kde" | "plasma" => Ok(Compositor::Kwin),
|
||||
"wlroots" | "sway" | "hyprland" | "wlr" => Ok(Compositor::Wlroots),
|
||||
@@ -551,11 +551,7 @@ pub fn open(compositor: Compositor) -> Result<Box<dyn VirtualDisplay>> {
|
||||
/// default) auto-detects, preferring pf-vdisplay if its device interface is enumerable.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows_use_pf_vdisplay() -> bool {
|
||||
match std::env::var("PUNKTFUNK_VDISPLAY")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
{
|
||||
match crate::config::config().vdisplay.as_deref().map(str::trim) {
|
||||
Some("pf") | Some("pf-vdisplay") | Some("pfvd") => true,
|
||||
Some("sudovda") | Some("sudo") => false,
|
||||
_ => pf_vdisplay::is_available(),
|
||||
|
||||
@@ -55,7 +55,7 @@ const PF_VDISPLAY_INTERFACE: GUID =
|
||||
/// 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()
|
||||
crate::config::config().idd_push
|
||||
}
|
||||
|
||||
/// Monotonic per-session id keying a pf-vdisplay monitor for `IOCTL_ADD`/`IOCTL_REMOVE`. Unlike
|
||||
@@ -249,9 +249,9 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
|
||||
// the discrete render GPU it pins here). The pf-vdisplay driver now IMPLEMENTS this IOCTL
|
||||
// (IddCxAdapterSetRenderAdapter); a failure is still tolerated (the driver also reports its real
|
||||
// render LUID in the shared header, so the host binds to the right GPU regardless).
|
||||
let pinned = if std::env::var("PUNKTFUNK_RENDER_ADAPTER").is_ok() {
|
||||
let pinned = if crate::config::config().render_adapter.is_some() {
|
||||
unsafe { resolve_render_adapter_luid() }
|
||||
} else if std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() {
|
||||
} else if crate::config::config().idd_push {
|
||||
// P2 direct frame push: the host opens the driver's shared textures AND runs NVENC on the
|
||||
// RENDER adapter, so on a hybrid box (dGPU + iGPU) it MUST be the discrete encoder GPU — an
|
||||
// iGPU-rendered surface is untouchable by NVENC. pf-vdisplay now IMPLEMENTS
|
||||
|
||||
@@ -28,7 +28,7 @@ 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()
|
||||
crate::config::config().idd_push
|
||||
}
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -301,9 +301,9 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
|
||||
// the real source of the perpetual ACCESS_LOST + MODE_CHANGE_IN_PROGRESS storm. So default to
|
||||
// NOT pinning — let the IDD use its natural adapter like Apollo. Opt in with
|
||||
// PUNKTFUNK_RENDER_ADAPTER=<name substring> only on a box that genuinely needs steering.
|
||||
let pinned = if std::env::var("PUNKTFUNK_RENDER_ADAPTER").is_ok() {
|
||||
let pinned = if crate::config::config().render_adapter.is_some() {
|
||||
unsafe { resolve_render_adapter_luid() }
|
||||
} else if std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() {
|
||||
} else if crate::config::config().idd_push {
|
||||
// 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
|
||||
|
||||
@@ -135,7 +135,7 @@ pub fn run(opts: HelperOptions) -> Result<()> {
|
||||
// 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 perf = crate::config::config().perf;
|
||||
let mut frames = 0u64;
|
||||
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)
|
||||
|
||||
@@ -18,8 +18,9 @@ use windows::Win32::Foundation::LUID;
|
||||
/// already satisfy this).
|
||||
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()
|
||||
let want = crate::config::config()
|
||||
.render_adapter
|
||||
.clone()
|
||||
.filter(|s| !s.is_empty());
|
||||
let factory: IDXGIFactory1 = CreateDXGIFactory1().ok()?;
|
||||
let mut best: Option<(LUID, u64, String)> = None;
|
||||
|
||||
@@ -23,7 +23,7 @@ use windows::Win32::Devices::Display::{
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW,
|
||||
DISP_CHANGE_SUCCESSFUL, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, DM_PELSWIDTH,
|
||||
ENUM_DISPLAY_SETTINGS_MODE,
|
||||
ENUM_CURRENT_SETTINGS, ENUM_DISPLAY_SETTINGS_MODE,
|
||||
};
|
||||
|
||||
use crate::vdisplay::Mode;
|
||||
@@ -67,6 +67,27 @@ pub(crate) unsafe fn resolve_gdi_name(target_id: u32) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The virtual display's CURRENT active resolution `(width, height)` via the GDI/CCD API, or `None` if the
|
||||
/// target isn't an active display yet / the query fails. The IDD-push capturer sizes its ring to this
|
||||
/// ACTUAL mode and polls it to recreate the ring when it changes — a fullscreen game can change the
|
||||
/// virtual display's mode out from under the session-negotiated one (game-capture bug GB1).
|
||||
///
|
||||
/// # Safety
|
||||
/// Calls the GDI/CCD APIs; safe to call from any thread.
|
||||
pub(crate) unsafe fn active_resolution(target_id: u32) -> Option<(u32, u32)> {
|
||||
let gdi = resolve_gdi_name(target_id)?;
|
||||
let wname: Vec<u16> = gdi.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let mut dm = DEVMODEW {
|
||||
dmSize: size_of::<DEVMODEW>() as u16,
|
||||
..Default::default()
|
||||
};
|
||||
let ok = EnumDisplaySettingsW(PCWSTR(wname.as_ptr()), ENUM_CURRENT_SETTINGS, &mut dm).as_bool();
|
||||
if !ok || dm.dmPelsWidth == 0 || dm.dmPelsHeight == 0 {
|
||||
return None;
|
||||
}
|
||||
Some((dm.dmPelsWidth, dm.dmPelsHeight))
|
||||
}
|
||||
|
||||
/// Toggle the SudoVDA target's advanced-color (HDR) state via the CCD API. Disabling HDR while on the
|
||||
/// secure (Winlogon) desktop makes it render SDR/composed so DXGI Desktop Duplication can capture it
|
||||
/// (the HDR fullscreen independent-flip otherwise storms `ACCESS_LOST` → black); re-enable on return so
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Goal-1 (clean, layered host architecture) — staged execution plan
|
||||
|
||||
The design is in [`windows-host-rewrite.md`](windows-host-rewrite.md) §2.2–2.4. This file is the **ordered,
|
||||
independently-shippable execution plan**, because the host is **live-validated** (GameStream + punktfunk/1,
|
||||
NVENC + IDD-push on-glass) and Goal-1 rewires its session/config/dispatch flow — so every stage must
|
||||
**preserve behavior**, compile + box-verify on its own, and be committed before the next starts. The plan's
|
||||
own §14 makes the §1 preservation checklist a mandatory per-module assert contract; honour it.
|
||||
|
||||
## Why staged (not one big rewrite)
|
||||
|
||||
`main` is at parity and shipping. A monolithic rewrite would put the validated host in a broken
|
||||
intermediate state for a long window and make a regression impossible to bisect. Each stage below is a
|
||||
behaviour-preserving transform with its own verification, so a regression is caught at the stage that
|
||||
introduced it.
|
||||
|
||||
## Stages (ordered; each = goal · files · risk · verify)
|
||||
|
||||
**Stage 1 — `HostConfig` foundation. ✅ DONE (this commit).**
|
||||
`config.rs`: typed `HostConfig` parsed ONCE from env (`idd_push`/`encoder_pref`/`no_helper`/`force_helper`).
|
||||
Migrated the two highest-churn dispatch reads onto it (`encode::windows_resolved_backend`,
|
||||
`punktfunk1::should_use_helper`). Risk: low (env constant at runtime → identical behaviour). Verify: box
|
||||
`cargo check --features nvenc`.
|
||||
|
||||
**Stage 2 — finish `HostConfig` + resolve-once. ✅ DONE (this commit).**
|
||||
Migrated **31** genuinely-constant operator/dispatch sites onto `HostConfig`: `idd_push` ×7 (the
|
||||
capture/topology disagreement knob), `no_wgc`, `capture_backend`, `render_adapter`, `encoder_pref` (Linux),
|
||||
the Windows vdisplay-backend select, plus the plan-named `secure_dda`/`idd_depth`/`zerocopy`/`ten_bit` and the
|
||||
multi-site `perf` ×4 / `compositor` ×5 / `video_source` ×3 / `gamepad`. Each `HostConfig` field's parser is
|
||||
**byte-identical** to the read it replaced, so `old == new` by construction (the §1 "flipped bool" guard).
|
||||
|
||||
**Scope correction (the plan's "~64 sites / Linux XDG+compositor / grep→0" was unsafe as written):** two
|
||||
classes of `env::var` read are deliberately **kept live** and documented in `config.rs`:
|
||||
- **Runtime-mutated session vars.** On Linux, `vdisplay::apply_session_env` *rewrites the process env on
|
||||
every connect* (the Bazzite Gaming↔Desktop follow): `WAYLAND_DISPLAY`, `XDG_CURRENT_DESKTOP`,
|
||||
`XDG_RUNTIME_DIR`, `DBUS_SESSION_BUS_ADDRESS`, and the derived `PUNKTFUNK_INPUT_BACKEND`,
|
||||
`PUNKTFUNK_GAMESCOPE_SESSION/NODE`, `PUNKTFUNK_KWIN/MUTTER_VIRTUAL_PRIMARY`, `PUNKTFUNK_FORCE_SHM`.
|
||||
Parse-once would freeze them at startup → silent session-following regression. They are NOT constant.
|
||||
- **Single-use local tuning** (no resolve-once benefit, call-site-local default/clamp, and `FEC_PCT` even has
|
||||
*two different* semantics): `FEC_PCT`, `VIDEO_DROP`, `VBV_FRAMES`, `SPLIT_ENCODE`, `PACE_BURST_KB`, the
|
||||
`capture/dxgi.rs` timing knobs, the `*_LIVE`/test gates, plus path/dynamic reads (config-dir, `PATH`
|
||||
search, env-forward-to-child). `PUNKTFUNK_ZEROCOPY` is split on purpose: Windows presence-semantics moved
|
||||
to the field; Linux keeps its own truthy parser.
|
||||
|
||||
Risk: medium (semantics-preservation). Verify: Linux `cargo check`/`clippy`/`fmt` green (the Windows-only
|
||||
edits are 1:1 substitutions, compile-verified on the box as part of Stage 3's build).
|
||||
|
||||
**Stage 3 — `SessionPlan` (the single biggest clarity lever, plan §2.4). ✅ IMPLEMENTED (this commit; on-glass pending).**
|
||||
New `src/session_plan.rs`: a `Copy` `SessionPlan { capture, topology, encoder, bit_depth, hdr }` resolved
|
||||
**once** from `HostConfig` (+ the negotiated `bit_depth`) in `virtual_stream`, logged, and threaded through
|
||||
`build_pipeline_with_retry`/`build_pipeline`. The three dispatch points now read it:
|
||||
- **capture** — `capture::capture_virtual_output` takes a `CaptureBackend` IN (was re-deriving from
|
||||
`config().idd_push`/`capture_backend`/`no_wgc`); `CaptureBackend::resolve()` is the one resolver (also
|
||||
used by the GameStream + spike call sites).
|
||||
- **topology** — `virtual_stream` reads `plan.topology` (`should_use_helper` deleted; its logic is
|
||||
`session_plan::resolve_topology`, verbatim). The IDD-preempt guard reads `plan.capture` too.
|
||||
- **encoder** — recorded as `EncoderBackend` from `encode::windows_resolved_backend` (config-backed +
|
||||
GPU-vendor cached since stage 2, already a single source). Threading `encoder`/`input_format` into the
|
||||
encoder + capturer opens (which removes the `dxgi.rs` back-reference) is **stage 5**.
|
||||
|
||||
Every decision is provably equivalent to the pre-stage-3 scattered reads (same `config()` + cached probes),
|
||||
so it is behavior-preserving. Risk: medium-high (rewires the deployed decision). Verify: box build (Windows
|
||||
compile, which also covers stage 2's Windows-only edits) + on-glass re-test (NVENC + IDD-push + a mode
|
||||
switch) — **pending** (RTX box `192.168.1.173`).
|
||||
|
||||
**Stage 4 — `SessionContext` + `SessionFactory`/`Session`.**
|
||||
Bundle the 12–13-arg `#[allow(too_many_arguments)]` signatures into `SessionContext`; `SessionFactory.build()`
|
||||
owns the RAII chain `vdm.lease(mode) → open_capturer(vout, fmt) → open_encoder(plan) → spawn pipeline`, with
|
||||
`Session::drop` the ONLY teardown path. Risk: high (teardown ordering — the §1 RAII asserts are mandatory).
|
||||
Verify: box build + on-glass (connect/disconnect/reconnect, no leaked monitors/threads).
|
||||
|
||||
**Stage 5 — seam-trait tightenings (plan §2.3).**
|
||||
`Capturer::open_capturer(vout, want: OutputFormat)` takes the format IN (kills the
|
||||
`capture → encode::windows_resolved_backend()` back-reference recomputed in `dxgi.rs`); HDR/release become
|
||||
`VirtualLease` methods (session glue names no concrete backend, contains no `unsafe`); optional encoder
|
||||
features move to `EncoderCaps`. Risk: medium. Verify: box build + on-glass.
|
||||
|
||||
**Stage 6 — `src/windows/` tree (cfg-sprawl confinement, plan §2.2).**
|
||||
Move the Windows backends under `src/windows/` + `capture/windows/`, `encode/windows/`, `inject/windows/`,
|
||||
`audio/windows/`, `vdisplay/windows.rs` behind one `#[cfg(windows)] mod windows;` seam. Pure file move +
|
||||
mod/use-path updates — behaviour-identical. Risk: low-but-huge (dozens of files; compile-verify catches all).
|
||||
Do LAST so the earlier semantic stages don't fight path churn. Verify: Linux + box build.
|
||||
|
||||
## Guardrails (mandatory, plan §14)
|
||||
|
||||
- Each stage is its own commit; box-verify before moving on.
|
||||
- Stages 3–5 touch the deployed path → **on-glass re-test** (NVENC + IDD-push, a mode switch, a
|
||||
connect/disconnect cycle) before the next stage.
|
||||
- Preserve every `PUNKTFUNK_*` var's exact semantics; when in doubt, assert old==new at the call site.
|
||||
@@ -120,10 +120,20 @@ mid-session fallback → 20 s `bail!`.
|
||||
the user-visible fix) → Stage 2 adaptive ring (P1/P2; proto bump + driver re-vendor) → Stage 3 trim
|
||||
advertised modes → Stage S driver resilience (S1/S2). Tracked as GB0–GB3 in the task list.
|
||||
|
||||
**Progress (2026-06-25):** **GB1 landed host-side** — *recover-or-drop, no DDA* (per the owner's call): the
|
||||
ring now tracks the display's ACTUAL mode (CCD `active_resolution`), recreating on a size/HDR change so a
|
||||
game mode-set recovers in-place; if no frame resumes within 3 s it drops the session cleanly (client
|
||||
reconnects). Commits `f98ab07` (first-frame failover) + `c87bfe0`. **Awaiting on-glass Doom validation.**
|
||||
**GB3 groundwork landed** — driver `publish()` width/height guard + descriptor-on-drop logging + a flushed
|
||||
process-lifetime log appender so the swap-chain worker's lines land (commit `789ad49`); **needs a driver
|
||||
rebuild + re-vendor to deploy.** Stage 3 (trim modes) deprioritized; Stage S code-fix gated on these
|
||||
diagnostics showing whether S1/S2 fire on-glass.
|
||||
|
||||
## Verification
|
||||
|
||||
The persistent validator is the **RTX box** `ssh "Enrico Bühler"@192.168.1.158` (ENRICOS-DESKTOP, RTX 4090,
|
||||
PS shell). **EPHEMERAL — boots to Proxmox on reboot; never reboot it, never depend on it surviving.** It has
|
||||
The persistent validator is the **RTX box** `ssh "Enrico Bühler"@<ip>` (ENRICOS-DESKTOP, RTX 4090,
|
||||
PS shell). **The IP FLOATS — DHCP + boots to Proxmox on reboot (new lease each time); recently `.173` /
|
||||
`.158`, confirm the current IP first. EPHEMERAL — never reboot it, never depend on it surviving.** It has
|
||||
WDK 26100 + LLVM 21.1.2 + the Rust toolchain. Build clone: `C:\Users\Public\pf-rewrite`.
|
||||
|
||||
```sh
|
||||
@@ -133,7 +143,7 @@ cargo check -p punktfunk-host # Linux paths; the win_* mod
|
||||
|
||||
# 1. reset the box clone to a clean base, then overlay your changed files
|
||||
# ssh ... "cd C:\Users\Public\pf-rewrite; git fetch -q origin; git reset -q --hard origin/main; git clean -qfd; git checkout -q <rev>"
|
||||
# scp <changed files> "Enrico Bühler@192.168.1.158:C:/Users/Public/pf-rewrite/<same rel path>"
|
||||
# scp <changed files> "Enrico Bühler@<ip>:C:/Users/Public/pf-rewrite/<same rel path>"
|
||||
|
||||
# 2. host clippy (warm target ~4s). NVENC import lib at C:\t\nvenc; no FFmpeg needed (amf-qsv off).
|
||||
ssh ... "cd C:\Users\Public\pf-rewrite; $env:PUNKTFUNK_NVENC_LIB_DIR='C:\t\nvenc'; \
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build-stage-sign-install the NEW-tree pf-vdisplay UMDF IddCx driver (packaging/windows/drivers/) for
|
||||
local dev/test on the RTX box. The wdk-sys / windows-drivers-rs analogue of the superseded
|
||||
vdisplay-driver/deploy-dev.ps1.
|
||||
|
||||
.DESCRIPTION
|
||||
Stages the freshly built pf_vdisplay.dll, CLEARS its FORCE_INTEGRITY PE bit (this tree's wdk-build links
|
||||
/INTEGRITYCHECK, which a self-signed cert can't satisfy — the old wdf-umdf tree didn't), signs it with
|
||||
the self-signed test cert, stamps a STRICTLY-INCREASING DriverVer into the INF, generates + signs the
|
||||
catalog, and (with -Install) pnputil-installs it.
|
||||
|
||||
Build first: from packaging/windows/drivers/, in an MSVC dev shell with LIBCLANG_PATH +
|
||||
Version_Number=10.0.26100.0, run `cargo build`.
|
||||
|
||||
Re-deploying needs a HIGHER DriverVer than the installed one or pnputil silently keeps the old binary —
|
||||
hence the 9.9.MMdd.HHmm scheme (the vendored build is 9.5.*). If the host service is running it holds the
|
||||
driver: `punktfunk-host service stop`, deploy, then start it, for a clean test.
|
||||
.PARAMETER Install
|
||||
Also add the driver package to the store + (if absent) create the Root\pf_vdisplay devnode via nefconc.
|
||||
Needs an ELEVATED shell.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Stage = 'C:\Users\Public\pfvd-stage-deploy',
|
||||
[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'
|
||||
$clear = Join-Path $root '..\clear-force-integrity.ps1'
|
||||
if (-not (Test-Path $dll)) { throw "driver not built: $dll (cargo build in packaging/windows/drivers 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'
|
||||
foreach ($t in @($signtool, $stampinf, $inf2cat)) { if (-not $t) { throw 'a WDK tool (signtool/stampinf/Inf2Cat) was not found' } }
|
||||
|
||||
if (Test-Path $Stage) { Remove-Item $Stage -Recurse -Force }
|
||||
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
|
||||
|
||||
# Clear FORCE_INTEGRITY BEFORE signing (the clear edits the PE, which invalidates any signature).
|
||||
& $clear -Path $stagedDll | Out-Null
|
||||
|
||||
# DriverVer must strictly increase. Installed is 9.5.* — 9.9.MMdd.HHmm always wins on the same day.
|
||||
$now = Get-Date
|
||||
$ver = '9.9.{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 (new tree) DriverVer=$ver -> $Stage"
|
||||
|
||||
if ($Install) {
|
||||
& pnputil /add-driver $stagedInf /install
|
||||
$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
|
||||
}
|
||||
Write-Host "installed pf-vdisplay DriverVer=$ver"
|
||||
}
|
||||
@@ -37,6 +37,15 @@ pub unsafe extern "C" fn adapter_init_finished(
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
/// `EvtCleanupCallback` on the WDFDEVICE (E1): the device is being removed (PnP / driver unload) — drop
|
||||
/// every monitor's swap-chain worker so the worker threads don't linger into teardown. IddCx-free (the
|
||||
/// framework tears the monitors down with the departing device); see
|
||||
/// [`crate::monitor::cleanup_for_device_removal`].
|
||||
pub unsafe extern "C" fn device_cleanup(_object: WDFOBJECT) {
|
||||
dbglog!("[pf-vd] device cleanup — releasing monitors");
|
||||
crate::monitor::cleanup_for_device_removal();
|
||||
}
|
||||
|
||||
/// SDR mode list for an EDID monitor: EDID-serial lookup → count-then-fill `IDDCX_MONITOR_MODE`.
|
||||
pub unsafe extern "C" fn parse_monitor_description(
|
||||
p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION,
|
||||
|
||||
@@ -113,6 +113,9 @@ extern "C" fn driver_add(_driver: WDFDRIVER, mut init: PWDFDEVICE_INIT) -> NTSTA
|
||||
dev_attr.SynchronizationScope =
|
||||
wdk_sys::_WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent;
|
||||
dev_attr.ContextTypeInfo = &DEVICE_CTX.0;
|
||||
// Drop every monitor's swap-chain worker when the device is removed (PnP / unload), so the worker
|
||||
// threads don't linger into teardown (E1 device cleanup). IddCx-free; see callbacks::device_cleanup.
|
||||
dev_attr.EvtCleanupCallback = Some(callbacks::device_cleanup);
|
||||
// SAFETY: init configured above; dev_attr is a valid context-typed attributes block.
|
||||
let status = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfDeviceCreate, &mut init, &mut dev_attr, &mut device)
|
||||
|
||||
@@ -72,6 +72,9 @@ pub struct FramePublisher {
|
||||
/// 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,
|
||||
/// Set when a surface is dropped for a descriptor mismatch (a game mode-set the display), cleared on a
|
||||
/// matched publish — throttles the drop log to once per mismatch episode (game-capture bug GB1).
|
||||
mismatch_logged: bool,
|
||||
}
|
||||
|
||||
// SAFETY: created and used only on the swap-chain processor thread.
|
||||
@@ -246,6 +249,7 @@ impl FramePublisher {
|
||||
// SAFETY: `header` is the mapped host header; `dxgi_format` lives within it.
|
||||
ring_format: unsafe { (*header).dxgi_format },
|
||||
generation,
|
||||
mismatch_logged: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -281,9 +285,28 @@ impl FramePublisher {
|
||||
let mut desc = D3D11_TEXTURE2D_DESC::default();
|
||||
// SAFETY: `surface` is a live ID3D11Texture2D (borrowed from IddCx); `desc` is a valid local out-param.
|
||||
unsafe { surface.GetDesc(&mut desc) };
|
||||
if desc.Format.0 as u32 != self.ring_format {
|
||||
// Descriptor guard: CopyResource needs the surface + ring textures to share format AND dimensions.
|
||||
// A fullscreen game can mode-set the display, changing the surface's format/size before the host
|
||||
// recreates the ring to match (game-capture bug GB1) — drop a mismatched frame (else garbage) and
|
||||
// report the ACTUAL descriptor once per episode so a repro shows exactly what changed.
|
||||
// SAFETY: `self.header` stays mapped for the publisher's lifetime; width/height are plain u32 fields.
|
||||
let (rw, rh) = unsafe { ((*self.header).width, (*self.header).height) };
|
||||
if desc.Format.0 as u32 != self.ring_format || desc.Width != rw || desc.Height != rh {
|
||||
if !self.mismatch_logged {
|
||||
self.mismatch_logged = true;
|
||||
dbglog!(
|
||||
"[pf-vd] frame-push DROP: surface {}x{} fmt={} != ring {}x{} fmt={} — display mode-set? (host should recreate the ring)",
|
||||
desc.Width,
|
||||
desc.Height,
|
||||
desc.Format.0 as u32,
|
||||
rw,
|
||||
rh,
|
||||
self.ring_format
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.mismatch_logged = false;
|
||||
let start = self.next;
|
||||
for attempt in 0..ring_len {
|
||||
let slot = (start + attempt) % ring_len;
|
||||
|
||||
@@ -16,21 +16,40 @@ fn file_log_enabled() -> bool {
|
||||
*ON.get_or_init(|| cfg!(debug_assertions) || std::env::var_os("PFVD_DEBUG_LOG").is_some())
|
||||
}
|
||||
|
||||
/// Process-lifetime append handle to the bring-up log, opened ONCE (by whichever thread logs first) and
|
||||
/// shared via a `Mutex` — so the swap-chain WORKER thread's writes land too. Per-call open/append raced
|
||||
/// the control thread and/or could fail under the worker's restricted token, hiding exactly the
|
||||
/// swap-chain-processor lines a game-break repro needs (game-capture bug S3). `flush` after each line so a
|
||||
/// crash/stall doesn't lose the tail.
|
||||
fn file_appender() -> Option<&'static std::sync::Mutex<std::fs::File>> {
|
||||
use std::sync::OnceLock;
|
||||
static APPENDER: OnceLock<Option<std::sync::Mutex<std::fs::File>>> = OnceLock::new();
|
||||
APPENDER
|
||||
.get_or_init(|| {
|
||||
if !file_log_enabled() {
|
||||
return None;
|
||||
}
|
||||
std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("C:\\Users\\Public\\pfvd-driver.log")
|
||||
.ok()
|
||||
.map(std::sync::Mutex::new)
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub fn log(s: &str) {
|
||||
if let Ok(c) = std::ffi::CString::new(s) {
|
||||
// SAFETY: `c` is a valid NUL-terminated string for the duration of the call.
|
||||
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
||||
}
|
||||
if !file_log_enabled() {
|
||||
return;
|
||||
}
|
||||
use std::io::Write;
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("C:\\Users\\Public\\pfvd-driver.log")
|
||||
{
|
||||
let _ = writeln!(f, "{s}");
|
||||
if let Some(m) = file_appender() {
|
||||
if let Ok(mut f) = m.lock() {
|
||||
let _ = writeln!(f, "{s}");
|
||||
let _ = f.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,14 @@ pub struct MonitorObject {
|
||||
// SAFETY: the raw IddCx monitor handle is framework-managed; access is serialized by MONITOR_MODES.
|
||||
unsafe impl Send for MonitorObject {}
|
||||
|
||||
/// All live monitors. A process-`static` (not a WDFDEVICE-context-owned allocation) BY NECESSITY: the IddCx
|
||||
/// monitor/mode DDIs receive only an IddCx handle — never the WDFDEVICE or its context — so this state must
|
||||
/// be reachable without one (the upstream virtual-display-rs is a process-`static` for the same reason).
|
||||
/// With a single `pf_vdisplay` devnode + `UmdfHostProcessSharing=ProcessSharingDisabled` the host process
|
||||
/// (and this state) die WITH the device, so it is effectively device-scoped already; a `Box` + `AtomicPtr`
|
||||
/// "device-owned" variant (audit §2.5) would only add a use-after-free window — the host-gone watchdog
|
||||
/// thread ([`crate::control::start_watchdog`]) races device cleanup — for no real gain. Cleanup of the
|
||||
/// heavy per-monitor resources on device removal is instead done explicitly ([`cleanup_for_device_removal`]).
|
||||
pub static MONITOR_MODES: Mutex<Vec<MonitorObject>> = Mutex::new(Vec::new());
|
||||
/// Monitor id / EDID-serial counter (unique per created monitor).
|
||||
static NEXT_ID: AtomicU32 = AtomicU32::new(1);
|
||||
@@ -305,6 +313,16 @@ pub fn create_monitor(
|
||||
refresh: u32,
|
||||
) -> Option<(u32, u32, i32)> {
|
||||
let adapter = crate::adapter::adapter()?;
|
||||
// Single identity per session (E1): if the host re-ADDs a still-live `session_id` (it shouldn't), depart
|
||||
// the stale monitor first, so one session maps to exactly one monitor (no duplicate EDID/target lingers).
|
||||
if MONITOR_MODES
|
||||
.lock()
|
||||
.map(|l| l.iter().any(|m| m.session_id == session_id))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
dbglog!("[pf-vd] create_monitor: session {session_id} already live — departing the stale monitor");
|
||||
remove_monitor(session_id);
|
||||
}
|
||||
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let mut modes = vec![Mode {
|
||||
@@ -459,6 +477,27 @@ pub fn clear_all() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `EvtCleanupCallback` (device removal, [`crate::callbacks::device_cleanup`]): drop every monitor's heavy
|
||||
/// resources — the swap-chain processor workers (each RAII-joins its thread + deletes its swap-chain) — and
|
||||
/// clear the list, WITHOUT `IddCxMonitorDeparture` (the framework tears the IddCx monitors down together
|
||||
/// with the departing device; departing here would double-tear). Frees our worker threads promptly even
|
||||
/// though the per-devnode WUDFHost (`ProcessSharingDisabled`) would also reap them when it exits.
|
||||
pub fn cleanup_for_device_removal() {
|
||||
let mut drained: Vec<Option<crate::swap_chain_processor::SwapChainProcessor>> = {
|
||||
let Ok(mut lock) = MONITOR_MODES.lock() else {
|
||||
return;
|
||||
};
|
||||
lock.drain(..)
|
||||
.map(|mut m| m.swap_chain_processor.take())
|
||||
.collect()
|
||||
};
|
||||
// Drop the workers (join their threads) AFTER releasing the lock — joining under MONITOR_MODES would
|
||||
// head-block the control plane (same discipline as remove_monitor / clear_all).
|
||||
for processor in &mut drained {
|
||||
drop(processor.take());
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop a pending entry by id (create failed before arrival).
|
||||
fn remove_by_id(id: u32) {
|
||||
if let Ok(mut lock) = MONITOR_MODES.lock() {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[build]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
@@ -1,3 +0,0 @@
|
||||
/target
|
||||
*.cer
|
||||
*.pfx
|
||||
-510
@@ -1,510 +0,0 @@
|
||||
# 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",
|
||||
]
|
||||
@@ -1,26 +0,0 @@
|
||||
# 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"
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
@@ -1,61 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,77 +0,0 @@
|
||||
#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)'
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
[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",
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
[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",
|
||||
]
|
||||
@@ -1,5 +0,0 @@
|
||||
fn main() {
|
||||
// UMDF includes need the static C runtime linked.
|
||||
println!("cargo::rustc-link-lib=static=ucrt");
|
||||
println!("cargo::rerun-if-changed=build.rs");
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
;/*++
|
||||
; 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
|
||||
@@ -1,532 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,401 +0,0 @@
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
//! 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) };
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//! 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;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
//! 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 ====");
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
//! 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],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#[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}");
|
||||
}));
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2024-07-26"
|
||||
components = ["rustfmt", "clippy"]
|
||||
profile = "minimal"
|
||||
@@ -1,17 +0,0 @@
|
||||
[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"
|
||||
@@ -1,278 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#include <Windows.h>
|
||||
|
||||
/**
|
||||
*
|
||||
* UMDF
|
||||
*
|
||||
*/
|
||||
|
||||
#define WDF_STUB
|
||||
|
||||
#include <wdf.h>
|
||||
|
||||
/**
|
||||
*
|
||||
* IDCXX
|
||||
*
|
||||
*/
|
||||
|
||||
#define IDD_STUB
|
||||
|
||||
// handled in build.rs
|
||||
// #include <iddcx\1.4\IddCx.h>
|
||||
@@ -1,17 +0,0 @@
|
||||
#![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;
|
||||
@@ -1,211 +0,0 @@
|
||||
#![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
@@ -1,12 +0,0 @@
|
||||
[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"
|
||||
@@ -1,344 +0,0 @@
|
||||
#![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
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,667 +0,0 @@
|
||||
#![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