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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 21:54:50 +02:00
parent 095540efc2
commit d39da4bc06
35 changed files with 7148 additions and 0 deletions
@@ -0,0 +1,2 @@
[build]
rustflags = ["-C", "target-feature=+crt-static"]
@@ -0,0 +1,3 @@
/target
*.cer
*.pfx
+510
View File
@@ -0,0 +1,510 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "bindgen"
version = "0.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pf-vdisplay"
version = "0.1.0"
dependencies = [
"anyhow",
"bytemuck",
"log",
"thiserror",
"wdf-umdf",
"wdf-umdf-sys",
"windows",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wdf-umdf"
version = "0.1.0"
dependencies = [
"paste",
"thiserror",
"wdf-umdf-sys",
]
[[package]]
name = "wdf-umdf-sys"
version = "0.1.0"
dependencies = [
"bindgen",
"bytemuck",
"paste",
"thiserror",
"winreg",
]
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys",
]
@@ -0,0 +1,26 @@
# pf-vdisplay — punktfunk Windows virtual display (IddCx), in Rust.
#
# A self-contained driver workspace (NOT built on windows-drivers-rs like the gamepad drivers — IddCx
# functions are direct IddCxStub exports the WDF function-table macro can't reach, so a unified bindgen
# is the cleaner base). The wdf-umdf-sys / wdf-umdf binding crates are vendored from MolotovCherry's
# MIT-licensed virtual-display-rs (see LICENSE.virtual-display-rs); pf-vdisplay is our driver, swapping
# its named-pipe IPC for the SudoVDA-compatible IOCTL control plane our host already speaks.
[workspace]
resolver = "2"
members = ["wdf-umdf-sys", "wdf-umdf", "pf-vdisplay"]
[profile.release]
strip = true
codegen-units = 1
lto = true
[workspace.lints.rust]
unsafe_op_in_unsafe_fn = "deny"
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
multiple_unsafe_ops_per_block = "deny"
ignored_unit_patterns = "allow"
missing_errors_doc = "allow"
module_inception = "allow"
module_name_repetitions = "allow"
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Cherry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,61 @@
# pf-vdisplay — punktfunk Windows virtual display (Rust IddCx)
P1 of replacing the vendored **SudoVDA** C++ driver with one we own — a pure-Rust UMDF2 **IddCx**
(Indirect Display Driver) virtual display, drop-in compatible with the host's existing
`vdisplay/sudovda.rs` IOCTL control plane. Full rationale + roadmap:
[`docs/windows-virtual-display-rust-port.md`](../../../docs/windows-virtual-display-rust-port.md).
## Layout
```
vdisplay-driver/
wdf-umdf-sys/ VENDORED bindgen FFI to WDF + IddCx (links WdfDriverStubUm + IddCxStub)
wdf-umdf/ VENDORED safe wrappers (IddCx*/Wdf*)
pf-vdisplay/ OUR driver crate (cdylib) + pf_vdisplay.inx
```
`wdf-umdf-sys` / `wdf-umdf` are vendored from [MolotovCherry/virtual-display-rs](https://github.com/MolotovCherry/virtual-display-rs)
(MIT — see `LICENSE.virtual-display-rs`). They're a self-contained bindgen over the WDK, **not**
`windows-drivers-rs` (which the gamepad drivers use): IddCx functions are direct `IddCxStub` exports the
WDF function-table macro can't reach, so a unified bindgen is the cleaner base. Local fix carried in
`wdf-umdf-sys/build.rs`: resolve the `IddCxStub` lib path by the SDK version that actually contains
`um\x64\iddcx\<ver>` (a newer base SDK alongside the WDK has `um\x64` but no `iddcx`).
## Status
- **Scaffold builds** ✅ — workspace + vendored bindings + `pf-vdisplay` compile in-tree to
`pf_vdisplay.dll`. The reference (virtual-display-rs) was separately built + installed + loaded
(Status OK) on the RTX box, proving the IddCx-in-Rust chain end to end.
- **Next (P1 driver logic):** port the IddCx driver — `DriverEntry``IDD_CX_CLIENT_CONFIG`
(adapter-init / parse-monitor-description / query-target-modes / assign-swapchain) → device + monitor
context, generic EDID, no-op swap-chain drain (DDA still captures for P1). Logging via
`OutputDebugString` (no `log`/`driver-logger`/`tokio`).
- **Then (control plane):** the SudoVDA-compatible IOCTL surface on the control device
(`ADD`/`REMOVE`/`PING`/`GET_WATCHDOG`/`GET_VERSION`/`SET_RENDER_ADAPTER`, byte-identical structs +
the `{e5bcc234-…}` interface GUID) so `vdisplay/sudovda.rs` drives it **unchanged**; a default mode
table + the per-`ADD` client mode injected as preferred; the watchdog.
- **Later (P2):** push swap-chain frames straight to the host (skip DDA); HDR via the IddCx 1.11 D3D12
acquire path.
## Build
Needs the WDK (UMDF 2.31 + IddCx stubs), LLVM/clang (`LIBCLANG_PATH`), and the pinned
`nightly-2024-07-26` (auto-selected via `rust-toolchain.toml`). From `pf-vdisplay/`, inside an MSVC dev
shell:
```
set LIBCLANG_PATH=C:\Program Files\LLVM\bin
cargo build # -> ../target/x86_64-pc-windows-msvc/debug/pf_vdisplay.dll
```
## Sign + install (same recipe as the gamepad drivers)
1. (no FORCE_INTEGRITY bit to clear — this crate doesn't set `/INTEGRITYCHECK`)
2. `signtool sign /fd SHA256 /sha1 <punktfunk-ds-test thumbprint>` the renamed `pf_vdisplay.dll`
3. `stampinf -f pf_vdisplay.inf -d * -a amd64 -u 2.15.0 -v <ver>` ; `Inf2Cat /driver:<dir> /os:10_X64` ;
sign the `.cat`
4. `pnputil /add-driver pf_vdisplay.inf` ; create the root devnode (`nefconc --create-device-node
--hardware-id root\pf_vdisplay --class-name Display --class-guid {4d36e968-…}`, mirroring
`install-sudovda.ps1`)
Bundles into the Inno Setup installer the same way as `gamepad-drivers/` once the driver is functional.
@@ -0,0 +1,77 @@
#requires -Version 5.1
<#
.SYNOPSIS
Stage, sign, and (optionally) install the pf-vdisplay UMDF IddCx driver for local dev/test.
.DESCRIPTION
Copies the freshly built pf_vdisplay.dll into the stage dir, signs it with the self-signed test cert,
stamps a STRICTLY INCREASING DriverVer (date.time) into the INF, then generates and signs the catalog.
With -Install it also pnputil /add-driver's the package and creates the Root\pf_vdisplay devnode.
Why the DriverVer bump matters: `pnputil /add-driver /install` only replaces an already-installed
driver binary when the INF DriverVer is higher than the installed one. Re-deploying without a bump
silently keeps the OLD binary loaded — a trap that masks code changes during iteration.
Build first: from pf-vdisplay/, in an MSVC dev shell with LIBCLANG_PATH set, run `cargo build`.
NOTE: pf-vdisplay and SudoVDA register the SAME control-interface GUID, so only one may be active at a
time. On the dev box, disable/remove the SudoVDA devnode before installing pf-vdisplay (and restore it
after). This script does not touch SudoVDA.
.PARAMETER Install
Also add the driver package to the store and create the root devnode.
#>
[CmdletBinding()]
param(
[string]$Stage = 'C:\Users\Public\pfvd-stage',
[string]$Thumbprint = '6A52984E54376C45A1C236B1A2C8A746C5AB6131',
[string]$Nefconc = 'C:\Users\Public\virtual-display-rs\installer\files\nefconc.exe',
[switch]$Install
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$dll = Join-Path $root 'target\x86_64-pc-windows-msvc\debug\pf_vdisplay.dll'
$inx = Join-Path $root 'pf-vdisplay\pf_vdisplay.inx'
if (-not (Test-Path $dll)) { throw "driver not built: $dll (run cargo build in pf-vdisplay/ first)" }
$kits = 'C:\Program Files (x86)\Windows Kits\10\bin'
function Find-Tool([string]$name, [string]$arch) {
(Get-ChildItem "$kits\*\$arch\$name" -EA SilentlyContinue | Sort-Object FullName | Select-Object -Last 1).FullName
}
$signtool = Find-Tool 'signtool.exe' 'x64'
$stampinf = Find-Tool 'stampinf.exe' 'x64'
$inf2cat = Find-Tool 'Inf2Cat.exe' 'x86'
if (-not $signtool) { throw 'signtool.exe not found in the WDK' }
if (-not $stampinf) { throw 'stampinf.exe not found in the WDK' }
if (-not $inf2cat) { throw 'Inf2Cat.exe not found in the WDK' }
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
$stagedDll = Join-Path $Stage 'pf_vdisplay.dll'
$stagedInf = Join-Path $Stage 'pf_vdisplay.inf'
$stagedCat = Join-Path $Stage 'pf_vdisplay.cat'
Copy-Item $dll $stagedDll -Force
Copy-Item $inx $stagedInf -Force # stampinf rewrites this copy in place
# DriverVer date+time — must strictly increase each deploy or pnputil keeps the old binary.
$now = Get-Date
$ver = '1.0.{0}.{1}' -f $now.ToString('MMdd'), $now.ToString('HHmm')
& $signtool sign /fd SHA256 /sha1 $Thumbprint $stagedDll | Out-Null
& $stampinf -f $stagedInf -d '*' -a 'amd64' -u '2.15.0' -v $ver | Out-Null
& $inf2cat /driver:$Stage /os:10_X64 /uselocaltime | Out-Null
& $signtool sign /fd SHA256 /sha1 $Thumbprint $stagedCat | Out-Null
Write-Host "staged + signed pf-vdisplay DriverVer=$ver"
if ($Install) {
& pnputil /add-driver $stagedInf /install | Out-Null
$present = Get-PnpDevice -EA SilentlyContinue |
Where-Object { $_.InstanceId -match 'PF_VDISPLAY' -or $_.FriendlyName -match 'punktfunk Virtual Display' }
if (-not $present) {
if (-not (Test-Path $Nefconc)) { throw "nefconc not found: $Nefconc" }
& $Nefconc --create-device-node --hardware-id 'root\pf_vdisplay' --class-name Display --class-guid '{4d36e968-e325-11ce-bfc1-08002be10318}' | Out-Null
Start-Sleep 2
& pnputil /add-driver $stagedInf /install | Out-Null
}
Write-Host 'installed pf-vdisplay (root\pf_vdisplay devnode)'
}
@@ -0,0 +1,11 @@
[build]
target = "x86_64-pc-windows-msvc"
rustflags = [
"-Zpre-link-arg=/NOLOGO",
"-Zpre-link-arg=/MANIFEST:NO",
"-Zpre-link-arg=/SUBSYSTEM:WINDOWS",
"-Zpre-link-arg=/DYNAMICBASE",
"-Zpre-link-arg=/NXCOMPAT",
"-Clink-arg=/OPT:REF,ICF",
]
@@ -0,0 +1,29 @@
[package]
name = "pf-vdisplay"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wdf-umdf-sys = { path = "../wdf-umdf-sys" }
wdf-umdf = { path = "../wdf-umdf" }
bytemuck = { version = "1.19", features = ["derive"] }
thiserror = "2.0"
anyhow = "1.0"
log = "0.4"
[dependencies.windows]
version = "0.58.0"
features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_Diagnostics_Debug",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
]
@@ -0,0 +1,5 @@
fn main() {
// UMDF includes need the static C runtime linked.
println!("cargo::rustc-link-lib=static=ucrt");
println!("cargo::rerun-if-changed=build.rs");
}
@@ -0,0 +1,77 @@
;/*++
; pf-vdisplay - punktfunk virtual display, UMDF2 IddCx driver INF (template; stampinf -> .inf).
; Adapted from MolotovCherry/virtual-display-rs (MIT) + SudoVDA's control-device security DACL.
;--*/
[Version]
PnpLockdown=1
Signature="$Windows NT$"
ClassGUID={4D36E968-E325-11CE-BFC1-08002BE10318}
Class=Display
ClassVer=2.0
Provider=%ManufacturerName%
CatalogFile=pf_vdisplay.cat
DriverVer=
[Manufacturer]
%ManufacturerName%=Standard,NT$ARCH$
[Standard.NT$ARCH$]
%DeviceName%=pf_vdisplay_Install, Root\pf_vdisplay
[SourceDisksFiles]
pf_vdisplay.dll=1
[SourceDisksNames]
1=%DiskName%
; =================== UMDF IddCx device ====================
[pf_vdisplay_Install.NT]
CopyFiles=UMDriverCopy
[pf_vdisplay_Install.NT.hw]
AddReg=pf_vdisplay_HardwareDeviceSettings
[pf_vdisplay_HardwareDeviceSettings]
HKR, , "UpperFilters", %REG_MULTI_SZ%, "IndirectKmd"
HKR, "WUDF", "DeviceGroupId", %REG_SZ%, "pfVDisplayGroup"
; Let the host (LocalSystem service) + admins open the control device for the ADD/REMOVE/PING IOCTLs.
HKR, , "Security", , "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;WD)"
[pf_vdisplay_Install.NT.Services]
AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall
[pf_vdisplay_Install.NT.Wdf]
UmdfService=pf_vdisplay, pf_vdisplay_Install
UmdfServiceOrder=pf_vdisplay
UmdfKernelModeClientPolicy=AllowKernelModeClients
UmdfHostProcessSharing=ProcessSharingDisabled
[pf_vdisplay_Install]
UmdfLibraryVersion=$UMDFVERSION$
ServiceBinary=%12%\UMDF\pf_vdisplay.dll
UmdfExtensions=IddCx0102
[WUDFRD_ServiceInstall]
DisplayName=%WudfRdDisplayName%
ServiceType=1
StartType=3
ErrorControl=1
ServiceBinary=%12%\WUDFRd.sys
[DestinationDirs]
UMDriverCopy=12,UMDF
[UMDriverCopy]
pf_vdisplay.dll
[Strings]
ManufacturerName="punktfunk"
DiskName="punktfunk Virtual Display Installation Disk"
WudfRdDisplayName="Windows Driver Foundation - User-mode Driver Framework Reflector"
DeviceName="punktfunk Virtual Display"
REG_MULTI_SZ=0x00010000
REG_SZ=0x00000000
REG_EXPAND_SZ=0x00020000
REG_DWORD=0x00010001
@@ -0,0 +1,322 @@
use std::{
mem::{self, MaybeUninit},
ptr::NonNull,
};
use log::{error, info};
use wdf_umdf_sys::{
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1,
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1, __BindgenBitfieldUnit,
DISPLAYCONFIG_2DREGION, DISPLAYCONFIG_RATIONAL, DISPLAYCONFIG_SCANLINE_ORDERING,
DISPLAYCONFIG_TARGET_MODE, DISPLAYCONFIG_VIDEO_SIGNAL_INFO, IDARG_IN_ADAPTER_INIT_FINISHED,
IDARG_IN_COMMITMODES, IDARG_IN_GETDEFAULTDESCRIPTIONMODES, IDARG_IN_PARSEMONITORDESCRIPTION,
IDARG_IN_QUERYTARGETMODES, IDARG_IN_SETSWAPCHAIN, IDARG_OUT_GETDEFAULTDESCRIPTIONMODES,
IDARG_OUT_PARSEMONITORDESCRIPTION, IDARG_OUT_QUERYTARGETMODES, IDDCX_ADAPTER__,
IDDCX_MONITOR_MODE, IDDCX_MONITOR_MODE_ORIGIN, IDDCX_MONITOR__, IDDCX_TARGET_MODE, NTSTATUS,
WDFDEVICE, WDF_POWER_DEVICE_STATE,
};
use crate::{
context::{DeviceContext, MonitorContext},
edid::Edid,
monitor::{AdapterObject, FlattenModes, ADAPTER, MONITOR_MODES},
};
pub extern "C-unwind" fn adapter_init_finished(
adapter_object: *mut IDDCX_ADAPTER__,
_p_in_args: *const IDARG_IN_ADAPTER_INIT_FINISHED,
) -> NTSTATUS {
let Some(adapter_ptr) = NonNull::new(adapter_object) else {
error!("Adapter ptr was null");
return NTSTATUS::STATUS_INVALID_ADDRESS;
};
// store adapter object for the control plane to use
if ADAPTER.set(AdapterObject(adapter_ptr)).is_err() {
error!("Failed to set adapter");
return NTSTATUS::STATUS_ADAPTER_HARDWARE_ERROR;
}
DeviceContext::finish_init();
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn device_d0_entry(
device: WDFDEVICE,
_previous_state: WDF_POWER_DEVICE_STATE,
) -> NTSTATUS {
let status: NTSTATUS = unsafe {
DeviceContext::get_mut(device.cast(), |context| {
if let Err(e) = context.init_adapter() {
error!("Failed to init adapter: {e:?}");
}
})
.into()
};
if !status.is_success() {
return status;
}
NTSTATUS::STATUS_SUCCESS
}
fn display_info(width: u32, height: u32, refresh_rate: u32) -> DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
let clock_rate = refresh_rate * (height + 4) * (height + 4) + 1000;
DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
pixelRate: u64::from(clock_rate),
hSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: clock_rate,
Denominator: height + 4,
},
vSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: clock_rate,
Denominator: (height + 4) * (height + 4),
},
activeSize: DISPLAYCONFIG_2DREGION {
cx: width,
cy: height,
},
totalSize: DISPLAYCONFIG_2DREGION {
cx: width + 4,
cy: height + 4,
},
__bindgen_anon_1: DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1 {
AdditionalSignalInfo: unsafe {
mem::transmute::<
__BindgenBitfieldUnit<[u8; 4]>,
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1,
>(
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1::new_bitfield_1(
255, 0, 0,
),
)
},
},
scanLineOrdering:
DISPLAYCONFIG_SCANLINE_ORDERING::DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
}
}
pub extern "C-unwind" fn parse_monitor_description(
p_in_args: *const IDARG_IN_PARSEMONITORDESCRIPTION,
p_out_args: *mut IDARG_OUT_PARSEMONITORDESCRIPTION,
) -> NTSTATUS {
let in_args = unsafe { &*p_in_args };
let out_args = unsafe { &mut *p_out_args };
let Ok(monitors) = MONITOR_MODES.lock() else {
error!("MONITOR_MODES mutex poisoned");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let edid = unsafe {
std::slice::from_raw_parts(
in_args.MonitorDescription.pData as *const u8,
in_args.MonitorDescription.DataSize as usize,
)
};
let monitor_index = Edid::get_serial(edid);
let Ok(monitor_index) = monitor_index else {
error!(
"We got an edid {} bytes long, but this is incorrect",
edid.len()
);
return NTSTATUS::STATUS_INVALID_VIEW_SIZE;
};
let Some(monitor) = monitors.iter().find(|&m| m.data.id == monitor_index) else {
error!("Failed to find monitor id {monitor_index}");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let number_of_modes: u32 = monitor
.data
.modes
.iter()
.map(|m| u32::try_from(m.refresh_rates.len()).expect("Cannot use > u32::MAX refresh rates"))
.sum();
out_args.MonitorModeBufferOutputCount = number_of_modes;
if in_args.MonitorModeBufferInputCount < number_of_modes {
// Return success if there was no buffer, since the caller was only asking for a count of modes
return if in_args.MonitorModeBufferInputCount > 0 {
NTSTATUS::STATUS_BUFFER_TOO_SMALL
} else {
NTSTATUS::STATUS_SUCCESS
};
}
let monitor_modes = unsafe {
std::slice::from_raw_parts_mut(
in_args
.pMonitorModes
.cast::<MaybeUninit<IDDCX_MONITOR_MODE>>(),
number_of_modes as usize,
)
};
for (mode, out_mode) in monitor.data.modes.flatten().zip(monitor_modes.iter_mut()) {
out_mode.write(IDDCX_MONITOR_MODE {
#[allow(clippy::cast_possible_truncation)]
Size: mem::size_of::<IDDCX_MONITOR_MODE>() as u32,
Origin: IDDCX_MONITOR_MODE_ORIGIN::IDDCX_MONITOR_MODE_ORIGIN_MONITORDESCRIPTOR,
MonitorVideoSignalInfo: display_info(mode.width, mode.height, mode.refresh_rate),
});
}
// Set the preferred mode as represented in the EDID
out_args.PreferredMonitorModeIdx = 0;
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn monitor_get_default_modes(
_monitor_object: *mut IDDCX_MONITOR__,
_p_in_args: *const IDARG_IN_GETDEFAULTDESCRIPTIONMODES,
_p_out_args: *mut IDARG_OUT_GETDEFAULTDESCRIPTIONMODES,
) -> NTSTATUS {
NTSTATUS::STATUS_NOT_IMPLEMENTED
}
pub fn target_mode(width: u32, height: u32, refresh_rate: u32) -> IDDCX_TARGET_MODE {
let total_size = DISPLAYCONFIG_2DREGION {
cx: width,
cy: height,
};
IDDCX_TARGET_MODE {
#[allow(clippy::cast_possible_truncation)]
Size: mem::size_of::<IDDCX_TARGET_MODE>() as u32,
TargetVideoSignalInfo: DISPLAYCONFIG_TARGET_MODE {
targetVideoSignalInfo: DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
pixelRate: u64::from(refresh_rate) * u64::from(width) * u64::from(height),
hSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: refresh_rate * height,
Denominator: 1,
},
vSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: refresh_rate,
Denominator: 1,
},
totalSize: total_size,
activeSize: total_size,
scanLineOrdering:
DISPLAYCONFIG_SCANLINE_ORDERING::DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
__bindgen_anon_1: DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1 {
AdditionalSignalInfo: unsafe {
mem::transmute::<__BindgenBitfieldUnit<[u8; 4]>, DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1>(
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1::new_bitfield_1(
255, 1, 0,
),
)
},
},
},
},
..Default::default()
}
}
pub extern "C-unwind" fn monitor_query_modes(
monitor_object: *mut IDDCX_MONITOR__,
p_in_args: *const IDARG_IN_QUERYTARGETMODES,
p_out_args: *mut IDARG_OUT_QUERYTARGETMODES,
) -> NTSTATUS {
// find out which monitor this belongs too
let Ok(monitors) = MONITOR_MODES.lock() else {
error!("MONITOR_MODES mutex poisoned");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
// we have stored the monitor object per id, so we should be able to compare pointers
let Some(monitor) = monitors
.iter()
.find(|&m| m.object.is_some_and(|p| p.as_ptr() == monitor_object))
else {
error!("Failed to find monitor object in cache for {monitor_object:?}");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let number_of_modes = monitor
.data
.modes
.iter()
.map(|m| u32::try_from(m.refresh_rates.len()).expect("Cannot use > u32::MAX modes"))
.sum();
// Create a set of modes supported for frame processing and scan-out. These are typically not based on the
// monitor's descriptor and instead are based on the static processing capability of the device. The OS will
// report the available set of modes for a given output as the intersection of monitor modes with target modes.
let out_args = unsafe { &mut *p_out_args };
out_args.TargetModeBufferOutputCount = number_of_modes;
let in_args = unsafe { &*p_in_args };
if in_args.TargetModeBufferInputCount >= number_of_modes {
let out_target_modes = unsafe {
std::slice::from_raw_parts_mut(
in_args
.pTargetModes
.cast::<MaybeUninit<IDDCX_TARGET_MODE>>(),
number_of_modes as usize,
)
};
for (mode, out_target) in monitor
.data
.modes
.flatten()
.zip(out_target_modes.iter_mut())
{
let target_mode = target_mode(mode.width, mode.height, mode.refresh_rate);
out_target.write(target_mode);
}
}
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn adapter_commit_modes(
_adapter_object: *mut IDDCX_ADAPTER__,
_p_in_args: *const IDARG_IN_COMMITMODES,
) -> NTSTATUS {
// The swap-chain is managed by IddCx; there is nothing device-specific to reconfigure on a commit.
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn assign_swap_chain(
monitor_object: *mut IDDCX_MONITOR__,
p_in_args: *const IDARG_IN_SETSWAPCHAIN,
) -> NTSTATUS {
let p_in_args = unsafe { &*p_in_args };
unsafe {
MonitorContext::get_mut(monitor_object.cast(), |context| {
context.assign_swap_chain(
p_in_args.hSwapChain,
p_in_args.RenderAdapterLuid,
p_in_args.hNextSurfaceAvailable,
);
})
.into()
}
}
pub extern "C-unwind" fn unassign_swap_chain(monitor_object: *mut IDDCX_MONITOR__) -> NTSTATUS {
info!("swap-chain unassigned (monitor inactive)");
unsafe {
MonitorContext::get_mut(monitor_object.cast(), |context| {
context.unassign_swap_chain();
})
.into()
}
}
@@ -0,0 +1,328 @@
use std::{
mem::{self, size_of},
num::{ParseIntError, TryFromIntError},
ptr::{addr_of_mut, NonNull},
};
use anyhow::anyhow;
use log::{error, info, warn};
use wdf_umdf::{
IddCxAdapterInitAsync, IddCxError, IddCxMonitorArrival, IddCxMonitorCreate,
IddCxMonitorSetupHardwareCursor, WdfError, WdfObjectDelete, WDF_DECLARE_CONTEXT_TYPE,
};
use wdf_umdf_sys::{
DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY, HANDLE, IDARG_IN_ADAPTER_INIT, IDARG_IN_MONITORCREATE,
IDARG_IN_SETUP_HWCURSOR, IDARG_OUT_ADAPTER_INIT, IDARG_OUT_MONITORARRIVAL,
IDARG_OUT_MONITORCREATE, IDDCX_ADAPTER, IDDCX_ADAPTER_CAPS, IDDCX_CURSOR_CAPS,
IDDCX_ENDPOINT_DIAGNOSTIC_INFO, IDDCX_ENDPOINT_VERSION, IDDCX_FEATURE_IMPLEMENTATION,
IDDCX_MONITOR, IDDCX_MONITOR_DESCRIPTION, IDDCX_MONITOR_DESCRIPTION_TYPE, IDDCX_MONITOR_INFO,
IDDCX_SWAPCHAIN, IDDCX_TRANSMISSION_TYPE, IDDCX_XOR_CURSOR_SUPPORT, LUID, NTSTATUS, WDFDEVICE,
WDFOBJECT, WDF_OBJECT_ATTRIBUTES,
};
use windows::{
core::{s, w, GUID},
Win32::{Foundation::TRUE, System::Threading::CreateEventA},
};
use crate::{
direct_3d_device::Direct3DDevice,
edid::Edid,
monitor::MONITOR_MODES,
swap_chain_processor::SwapChainProcessor,
};
// Maximum amount of monitors that can be connected
pub const MAX_MONITORS: u8 = 16;
pub struct DeviceContext {
device: WDFDEVICE,
adapter: Option<IDDCX_ADAPTER>,
}
// SAFETY: Raw ptr is managed by external library
unsafe impl Send for DeviceContext {}
unsafe impl Sync for DeviceContext {}
// for now, `device` is hardcoded into the macro, so it needs to be there even if unused
#[allow(unused)]
pub struct MonitorContext {
device: IDDCX_MONITOR,
swap_chain_processor: Option<SwapChainProcessor>,
}
// SAFETY: Raw ptr is managed by external library
unsafe impl Send for MonitorContext {}
unsafe impl Sync for MonitorContext {}
WDF_DECLARE_CONTEXT_TYPE!(pub DeviceContext);
WDF_DECLARE_CONTEXT_TYPE!(pub MonitorContext);
#[derive(Debug, thiserror::Error)]
pub enum ContextError {
#[error("Failed to parse integer: {0:?}")]
ParseInt(#[from] ParseIntError),
#[error("Failed to convert integer: {0:?}")]
TryFromInt(#[from] TryFromIntError),
#[error("Failed to convert to NTSTATUS: {0:?}")]
Ntstatus(#[from] NTSTATUS),
#[error("Failed to convert to IddCxError: {0:?}")]
IddCx(#[from] IddCxError),
#[error("Failed to convert to WdfError: {0:?}")]
Wdf(#[from] WdfError),
#[error("Windows Error: {0:?}")]
Win(#[from] windows::core::Error),
#[error("{0:?}")]
Other(#[from] anyhow::Error),
}
impl DeviceContext {
pub fn new(device: WDFDEVICE) -> Self {
Self {
device,
adapter: None,
}
}
pub fn init_adapter(&mut self) -> Result<(), ContextError> {
let mut version = IDDCX_ENDPOINT_VERSION {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_ENDPOINT_VERSION>() as u32,
MajorVer: env!("CARGO_PKG_VERSION_MAJOR").parse::<u32>()?,
MinorVer: env!("CARGO_PKG_VERSION_MINOR").parse::<u32>()?,
Build: env!("CARGO_PKG_VERSION_PATCH").parse::<u32>()?,
..Default::default()
};
let mut adapter_caps = IDDCX_ADAPTER_CAPS {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_ADAPTER_CAPS>() as u32,
MaxMonitorsSupported: u32::from(MAX_MONITORS),
EndPointDiagnostics: IDDCX_ENDPOINT_DIAGNOSTIC_INFO {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_ENDPOINT_DIAGNOSTIC_INFO>() as u32,
GammaSupport: IDDCX_FEATURE_IMPLEMENTATION::IDDCX_FEATURE_IMPLEMENTATION_NONE,
TransmissionType: IDDCX_TRANSMISSION_TYPE::IDDCX_TRANSMISSION_TYPE_WIRED_OTHER,
pEndPointFriendlyName: w!("punktfunk Virtual Display Adapter").as_ptr(),
pEndPointManufacturerName: w!("punktfunk").as_ptr(),
pEndPointModelName: w!("Virtual Display").as_ptr(),
pFirmwareVersion: addr_of_mut!(version).cast(),
pHardwareVersion: addr_of_mut!(version).cast(),
},
..Default::default()
};
let mut attr = WDF_OBJECT_ATTRIBUTES::init_context_type(unsafe { Self::get_type_info() });
let adapter_init = IDARG_IN_ADAPTER_INIT {
// this is WdfDevice because that's what we set last
WdfDevice: self.device,
pCaps: addr_of_mut!(adapter_caps).cast(),
ObjectAttributes: addr_of_mut!(attr).cast(),
};
let mut adapter_init_out = IDARG_OUT_ADAPTER_INIT::default();
unsafe { IddCxAdapterInitAsync(&adapter_init, &mut adapter_init_out)? };
self.adapter = Some(adapter_init_out.AdapterObject);
unsafe { self.clone_into(adapter_init_out.AdapterObject as WDFOBJECT)? };
Ok(())
}
pub fn finish_init() -> NTSTATUS {
// Monitors are created on demand by the IOCTL control plane (control::do_add). Start the
// watchdog so a crashed/gone host never leaves a phantom display.
crate::control::start_watchdog();
NTSTATUS::STATUS_SUCCESS
}
pub fn create_monitor(&mut self, index: u32) -> Result<(), ContextError> {
let mut attr =
WDF_OBJECT_ATTRIBUTES::init_context_type(unsafe { MonitorContext::get_type_info() });
// use the edid serial number to represent the monitor index for later identification
let mut edid = Edid::generate_with(index);
let mut monitor_info = IDDCX_MONITOR_INFO {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_MONITOR_INFO>() as u32,
// SAFETY: windows-rs + generated _GUID types are same size, with same fields, and repr C
// see: https://microsoft.github.io/windows-docs-rs/doc/windows/core/struct.GUID.html
// and: wmdf_umdf_sys::_GUID
MonitorContainerId: unsafe {
mem::transmute::<GUID, wdf_umdf_sys::_GUID>(GUID::new()?)
},
MonitorType:
DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY::DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI,
ConnectorIndex: index,
MonitorDescription: IDDCX_MONITOR_DESCRIPTION {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_MONITOR_DESCRIPTION>() as u32,
Type: IDDCX_MONITOR_DESCRIPTION_TYPE::IDDCX_MONITOR_DESCRIPTION_TYPE_EDID,
#[allow(clippy::cast_possible_truncation)]
DataSize: edid.len() as u32,
pData: edid.as_mut_ptr().cast(),
},
};
let monitor_create = IDARG_IN_MONITORCREATE {
ObjectAttributes: &mut attr,
pMonitorInfo: &mut monitor_info,
};
let mut monitor_create_out = IDARG_OUT_MONITORCREATE::default();
unsafe {
IddCxMonitorCreate(
self.adapter.ok_or(anyhow!("Failed to get adapter"))?,
&monitor_create,
&mut monitor_create_out,
)?
};
// store monitor object for later
{
let mut lock = MONITOR_MODES
.lock()
.map_err(|_| anyhow!("Failed to lock mutex"))?;
for monitor in &mut *lock {
if monitor.data.id == index {
monitor.object = Some(
NonNull::new(monitor_create_out.MonitorObject)
.ok_or(anyhow!("MonitorObject was null"))?,
);
}
}
}
unsafe {
let context = MonitorContext::new(monitor_create_out.MonitorObject);
context.init(monitor_create_out.MonitorObject as WDFOBJECT)?;
}
// tell os monitor is plugged in
let mut arrival_out = IDARG_OUT_MONITORARRIVAL::default();
unsafe {
IddCxMonitorArrival(monitor_create_out.MonitorObject, &mut arrival_out)?;
}
// Record the OS target id + render-adapter LUID for the ADD IOCTL reply.
{
let mut lock = MONITOR_MODES
.lock()
.map_err(|_| anyhow!("Failed to lock mutex"))?;
if let Some(mon) = lock.iter_mut().find(|m| m.data.id == index) {
mon.target_id = arrival_out.OsTargetId;
mon.adapter_luid_low = arrival_out.OsAdapterLuid.LowPart;
mon.adapter_luid_high = arrival_out.OsAdapterLuid.HighPart;
}
}
Ok(())
}
}
impl MonitorContext {
pub fn new(device: IDDCX_MONITOR) -> Self {
Self {
device,
swap_chain_processor: None,
}
}
pub fn assign_swap_chain(
&mut self,
swap_chain: IDDCX_SWAPCHAIN,
render_adapter: LUID,
new_frame_event: HANDLE,
) {
// drop processing thread
drop(self.swap_chain_processor.take());
// transmute would work, but one less unsafe block, so why not
let luid = windows::Win32::Foundation::LUID {
LowPart: render_adapter.LowPart,
HighPart: render_adapter.HighPart,
};
// Log which GPU the OS picked to render this virtual monitor (useful on a hybrid iGPU+dGPU box,
// where the render adapter determines which adapter the host's capture must enumerate).
info!(
"swap-chain assigned: OS render adapter LUID = {:08x}:{:08x}",
render_adapter.HighPart, render_adapter.LowPart
);
let device = Direct3DDevice::init(luid);
if let Ok(device) = device {
let mut processor = SwapChainProcessor::new();
processor.run(swap_chain, device, new_frame_event);
self.swap_chain_processor = Some(processor);
self.setup_hw_cursor();
} else {
// It's important to delete the swap-chain if D3D initialization fails, so that the OS knows to generate a new
// swap-chain and try again.
error!("Direct3DDevice::init FAILED on render LUID: {device:?} — deleting swap chain for OS retry");
unsafe {
let _ = WdfObjectDelete(swap_chain.cast());
}
}
}
pub fn unassign_swap_chain(&mut self) {
self.swap_chain_processor.take();
}
pub fn setup_hw_cursor(&mut self) {
let mouse_event = unsafe { CreateEventA(None, false, false, s!("vdd_mouse_event")) };
let Ok(mouse_event) = mouse_event else {
error!("CreateEventA failed: {mouse_event:?}");
return;
};
// setup hardware cursor
let cursor_info = IDDCX_CURSOR_CAPS {
#[allow(clippy::cast_possible_truncation)]
Size: std::mem::size_of::<IDDCX_CURSOR_CAPS>() as u32,
AlphaCursorSupport: TRUE.0,
MaxX: 512,
MaxY: 512,
ColorXorCursorSupport: IDDCX_XOR_CURSOR_SUPPORT::IDDCX_XOR_CURSOR_SUPPORT_NONE,
};
let hw_cursor = IDARG_IN_SETUP_HWCURSOR {
CursorInfo: cursor_info,
hNewCursorDataAvailable: mouse_event.0,
};
let res = unsafe { IddCxMonitorSetupHardwareCursor(self.device, &hw_cursor) };
let Ok(res) = res else {
error!("IddCxMonitorSetupHardwareCursor() failed: {res:?}");
return;
};
if res.is_warning() {
warn!("IddCxMonitorSetupHardwareCursor() warn: {res:?}");
}
if res.is_error() {
error!("IddCxMonitorSetupHardwareCursor() failed: {res:?}");
}
}
}
@@ -0,0 +1,339 @@
//! SudoVDA-compatible IOCTL control plane (`EVT_IDD_CX_DEVICE_IO_CONTROL`). The host's
//! `vdisplay/sudovda.rs` drives this unchanged: ADD a monitor at a requested mode → `{LUID, target_id}`,
//! REMOVE by GUID, PING the watchdog, GET_VERSION/GET_WATCHDOG, SET_RENDER_ADAPTER. Struct layouts are
//! byte-identical to `Common/Include/sudovda-ioctl.h`.
use std::ffi::c_void;
use std::mem::size_of;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use log::{error, info};
use wdf_umdf::{
IddCxAdapterSetRenderAdapter, IddCxMonitorDeparture, WdfRequestCompleteWithInformation,
WdfRequestRetrieveInputBuffer, WdfRequestRetrieveOutputBuffer,
};
use wdf_umdf_sys::{IDARG_IN_ADAPTERSETRENDERADAPTER, LUID, NTSTATUS, WDFDEVICE, WDFREQUEST};
use crate::context::DeviceContext;
use crate::monitor::{
default_modes, Mode, MonitorData, MonitorObject, ADAPTER, MONITOR_MODES, NEXT_ID,
PREFERRED_RENDER_ADAPTER, PROTOCOL_VERSION, WATCHDOG_COUNTDOWN, WATCHDOG_TIMEOUT,
};
// CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, func, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0).
const fn ctl(func: u32) -> u32 {
(0x22u32 << 16) | (func << 2)
}
const IOCTL_ADD: u32 = ctl(0x800);
const IOCTL_REMOVE: u32 = ctl(0x801);
const IOCTL_SET_RENDER_ADAPTER: u32 = ctl(0x802);
const IOCTL_GET_WATCHDOG: u32 = ctl(0x803);
const IOCTL_PING: u32 = ctl(0x888);
const IOCTL_GET_VERSION: u32 = ctl(0x8FF);
#[repr(C)]
struct AddParams {
width: u32,
height: u32,
refresh: u32,
guid: [u8; 16],
device_name: [u8; 14],
serial: [u8; 14],
}
#[repr(C)]
struct AddOut {
luid_low: u32,
luid_high: i32,
target_id: u32,
}
#[repr(C)]
struct RemoveParams {
guid: [u8; 16],
}
#[repr(C)]
struct SetRenderAdapterParams {
luid_low: u32,
luid_high: i32,
}
#[repr(C)]
struct WatchdogOut {
timeout: u32,
countdown: u32,
}
fn guid_key(b: &[u8; 16]) -> u128 {
u128::from_le_bytes(*b)
}
/// SAFETY: `request` valid; returns a pointer to the request's input buffer of at least `min` bytes.
unsafe fn input_buf(request: WDFREQUEST, min: usize) -> Option<*const u8> {
let mut p: *mut c_void = std::ptr::null_mut();
let mut len: usize = 0;
let r = unsafe { WdfRequestRetrieveInputBuffer(request, min, &mut p, &mut len) };
if r.is_err() || p.is_null() || len < min {
return None;
}
Some(p.cast::<u8>())
}
/// SAFETY: `request` valid; returns a pointer to the request's output buffer of at least `min` bytes.
unsafe fn output_buf(request: WDFREQUEST, min: usize) -> Option<*mut u8> {
let mut p: *mut c_void = std::ptr::null_mut();
let mut len: usize = 0;
let r = unsafe { WdfRequestRetrieveOutputBuffer(request, min, &mut p, &mut len) };
if r.is_err() || p.is_null() || len < min {
return None;
}
Some(p.cast::<u8>())
}
/// `EVT_IDD_CX_DEVICE_IO_CONTROL` — IddCx redirects device IOCTLs here. Signature matches SudoVDA's
/// `SudoVDAIoDeviceControl(Device, Request, OutputBufferLength, InputBufferLength, IoControlCode)`.
pub extern "C-unwind" fn device_io_control(
device: WDFDEVICE,
request: WDFREQUEST,
output_len: usize,
input_len: usize,
ioctl_code: u32,
) {
// Reset the watchdog on any IOCTL except the watchdog query (the host PINGs to keep alive).
if ioctl_code != IOCTL_GET_WATCHDOG {
WATCHDOG_COUNTDOWN.store(WATCHDOG_TIMEOUT.load(Ordering::Relaxed), Ordering::Relaxed);
}
let mut bytes: usize = 0;
// SAFETY: dispatch reads/writes the request buffers it validated; `device` is the IddCx device.
let status = unsafe {
match ioctl_code {
IOCTL_ADD => do_add(device, request, input_len, output_len, &mut bytes),
IOCTL_REMOVE => do_remove(request, input_len),
IOCTL_SET_RENDER_ADAPTER => do_set_render_adapter(request, input_len),
IOCTL_GET_WATCHDOG => do_get_watchdog(request, output_len, &mut bytes),
IOCTL_PING => NTSTATUS::STATUS_SUCCESS,
IOCTL_GET_VERSION => do_get_version(request, output_len, &mut bytes),
_ => NTSTATUS::STATUS_INVALID_DEVICE_REQUEST,
}
};
// SAFETY: completing the request we were handed.
let _ = unsafe { WdfRequestCompleteWithInformation(request, status, bytes as u64) };
}
unsafe fn do_add(
device: WDFDEVICE,
request: WDFREQUEST,
input_len: usize,
output_len: usize,
bytes: &mut usize,
) -> NTSTATUS {
if input_len < size_of::<AddParams>() || output_len < size_of::<AddOut>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let (Some(pin), Some(pout)) = (
unsafe { input_buf(request, size_of::<AddParams>()) },
unsafe { output_buf(request, size_of::<AddOut>()) },
) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let params = unsafe { &*pin.cast::<AddParams>() };
let guid = guid_key(&params.guid);
// Dedup: an existing GUID returns its LUID + target id (the host may re-ADD on reconnect).
{
let lock = MONITOR_MODES.lock().unwrap();
if let Some(mon) = lock.iter().find(|m| m.guid == guid) {
let out = AddOut {
luid_low: mon.adapter_luid_low,
luid_high: mon.adapter_luid_high,
target_id: mon.target_id,
};
unsafe { pout.cast::<AddOut>().write_unaligned(out) };
*bytes = size_of::<AddOut>();
return NTSTATUS::STATUS_SUCCESS;
}
}
if params.width == 0 || params.height == 0 || params.refresh == 0 {
return NTSTATUS::STATUS_INVALID_PARAMETER;
}
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
// Requested mode first (preferred), then fallbacks.
let mut modes = vec![Mode {
width: params.width,
height: params.height,
refresh_rates: vec![params.refresh],
}];
modes.extend(default_modes());
MONITOR_MODES.lock().unwrap().push(MonitorObject {
object: None,
data: MonitorData { id, modes },
guid,
target_id: 0,
adapter_luid_low: 0,
adapter_luid_high: 0,
});
// Create the IddCx monitor via the device context (captures target id + LUID into the entry).
let created = unsafe {
DeviceContext::get_mut(device.cast(), |ctx| {
if let Err(e) = ctx.create_monitor(id) {
error!("ADD: create_monitor failed: {e:?}");
}
})
};
let lock = MONITOR_MODES.lock().unwrap();
let mon = lock.iter().find(|m| m.data.id == id);
if created.is_err() || mon.map_or(true, |m| m.object.is_none()) {
drop(lock);
MONITOR_MODES.lock().unwrap().retain(|m| m.data.id != id);
error!("ADD: monitor {id} failed to arrive");
return NTSTATUS::STATUS_UNSUCCESSFUL;
}
let mon = mon.unwrap();
let out = AddOut {
luid_low: mon.adapter_luid_low,
luid_high: mon.adapter_luid_high,
target_id: mon.target_id,
};
unsafe { pout.cast::<AddOut>().write_unaligned(out) };
*bytes = size_of::<AddOut>();
info!(
"ADD {}x{}@{} -> target_id={} luid={:08x}:{:08x}",
params.width, params.height, params.refresh, mon.target_id, mon.adapter_luid_high, mon.adapter_luid_low
);
NTSTATUS::STATUS_SUCCESS
}
unsafe fn do_remove(request: WDFREQUEST, input_len: usize) -> NTSTATUS {
if input_len < size_of::<RemoveParams>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pin) = (unsafe { input_buf(request, size_of::<RemoveParams>()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let params = unsafe { &*pin.cast::<RemoveParams>() };
let guid = guid_key(&params.guid);
let mut lock = MONITOR_MODES.lock().unwrap();
if let Some(pos) = lock.iter().position(|m| m.guid == guid) {
let mon = lock.remove(pos);
if let Some(obj) = mon.object {
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("REMOVE: departure failed: {e:?}");
}
}
info!("REMOVE target_id={}", mon.target_id);
NTSTATUS::STATUS_SUCCESS
} else {
NTSTATUS::STATUS_NOT_FOUND
}
}
unsafe fn do_set_render_adapter(request: WDFREQUEST, input_len: usize) -> NTSTATUS {
if input_len < size_of::<SetRenderAdapterParams>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pin) = (unsafe { input_buf(request, size_of::<SetRenderAdapterParams>()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let params = unsafe { &*pin.cast::<SetRenderAdapterParams>() };
PREFERRED_RENDER_ADAPTER.store(
((params.luid_high as u32 as u64) << 32) | u64::from(params.luid_low),
Ordering::Relaxed,
);
if let Some(adapter) = ADAPTER.get() {
let in_args = IDARG_IN_ADAPTERSETRENDERADAPTER {
PreferredRenderAdapter: LUID {
LowPart: params.luid_low,
HighPart: params.luid_high,
},
};
if let Err(e) = unsafe { IddCxAdapterSetRenderAdapter(adapter.0.as_ptr(), &in_args) } {
error!("SET_RENDER_ADAPTER failed: {e:?}");
}
}
NTSTATUS::STATUS_SUCCESS
}
unsafe fn do_get_watchdog(request: WDFREQUEST, output_len: usize, bytes: &mut usize) -> NTSTATUS {
if output_len < size_of::<WatchdogOut>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pout) = (unsafe { output_buf(request, size_of::<WatchdogOut>()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let out = WatchdogOut {
timeout: WATCHDOG_TIMEOUT.load(Ordering::Relaxed),
countdown: WATCHDOG_COUNTDOWN.load(Ordering::Relaxed),
};
unsafe { pout.cast::<WatchdogOut>().write_unaligned(out) };
*bytes = size_of::<WatchdogOut>();
NTSTATUS::STATUS_SUCCESS
}
unsafe fn do_get_version(request: WDFREQUEST, output_len: usize, bytes: &mut usize) -> NTSTATUS {
if output_len < PROTOCOL_VERSION.len() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pout) = (unsafe { output_buf(request, PROTOCOL_VERSION.len()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
unsafe { std::ptr::copy_nonoverlapping(PROTOCOL_VERSION.as_ptr(), pout, PROTOCOL_VERSION.len()) };
*bytes = PROTOCOL_VERSION.len();
NTSTATUS::STATUS_SUCCESS
}
/// Tear down every monitor (watchdog expiry — the host is gone). Mirrors SudoVDA's DisconnectAllMonitors.
fn disconnect_all_monitors() {
let mut lock = MONITOR_MODES.lock().unwrap();
if lock.is_empty() {
return;
}
for mon in lock.drain(..) {
if let Some(obj) = mon.object {
// SAFETY: `obj` is a live IddCx monitor object.
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("watchdog: monitor departure failed: {e:?}");
}
}
}
}
/// Start the watchdog thread (once). The host reads the timeout via GET_WATCHDOG and PINGs every
/// timeout/3; if it stops, the countdown reaches 0 and every monitor is torn down — so a crashed/gone
/// host never leaves a phantom display. Mirrors SudoVDA's RunWatchdog.
pub fn start_watchdog() {
static STARTED: AtomicBool = AtomicBool::new(false);
if STARTED.swap(true, Ordering::Relaxed) {
return;
}
let timeout = WATCHDOG_TIMEOUT.load(Ordering::Relaxed);
if timeout == 0 {
return;
}
WATCHDOG_COUNTDOWN.store(timeout, Ordering::Relaxed);
thread::spawn(|| loop {
thread::sleep(Duration::from_secs(1));
// Nothing to guard while there are no monitors.
if MONITOR_MODES.lock().unwrap().is_empty() {
continue;
}
let prev = WATCHDOG_COUNTDOWN.load(Ordering::Relaxed);
if prev == 0 {
continue;
}
// Decrement without clobbering a concurrent IOCTL reset (CAS).
if WATCHDOG_COUNTDOWN
.compare_exchange(prev, prev - 1, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
&& prev - 1 == 0
{
error!("watchdog expired (host stopped pinging) — tearing down all monitors");
disconnect_all_monitors();
}
});
}
@@ -0,0 +1,77 @@
use windows::{
core::Error,
Win32::{
Foundation::LUID,
Graphics::{
Direct3D::D3D_DRIVER_TYPE_UNKNOWN,
Direct3D11::{
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext,
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
D3D11_CREATE_DEVICE_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY,
D3D11_CREATE_DEVICE_SINGLETHREADED, D3D11_SDK_VERSION,
},
Dxgi::{CreateDXGIFactory2, IDXGIAdapter1, IDXGIFactory5, DXGI_CREATE_FACTORY_FLAGS},
},
},
};
#[derive(thiserror::Error, Debug)]
pub enum Direct3DError {
#[error("Direct3DError({0:?})")]
Win32(#[from] Error),
#[error("Direct3DError(\"{0}\")")]
Other(&'static str),
}
impl From<&'static str> for Direct3DError {
fn from(value: &'static str) -> Self {
Direct3DError::Other(value)
}
}
#[derive(Debug)]
pub struct Direct3DDevice {
// The following are already refcounted, so they're safe to use directly without additional drop impls
_dxgi_factory: IDXGIFactory5,
_adapter: IDXGIAdapter1,
pub device: ID3D11Device,
_device_context: ID3D11DeviceContext,
}
impl Direct3DDevice {
pub fn init(adapter_luid: LUID) -> Result<Self, Direct3DError> {
let dxgi_factory =
unsafe { CreateDXGIFactory2::<IDXGIFactory5>(DXGI_CREATE_FACTORY_FLAGS(0))? };
let adapter = unsafe { dxgi_factory.EnumAdapterByLuid::<IDXGIAdapter1>(adapter_luid)? };
let mut device = None;
let mut device_context = None;
unsafe {
D3D11CreateDevice(
&adapter,
D3D_DRIVER_TYPE_UNKNOWN,
None,
D3D11_CREATE_DEVICE_BGRA_SUPPORT
| D3D11_CREATE_DEVICE_SINGLETHREADED
| D3D11_CREATE_DEVICE_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut device_context),
)?;
}
let device = device.ok_or("ID3D11Device not found")?;
let device_context = device_context.ok_or("ID3D11DeviceContext not found")?;
Ok(Self {
_dxgi_factory: dxgi_factory,
_adapter: adapter,
device,
_device_context: device_context,
})
}
}
@@ -0,0 +1,114 @@
use std::{array::TryFromSliceError, ops::Deref};
use bytemuck::{Pod, Zeroable};
// A clean, self-contained 128-byte EDID carrying punktfunk's own identity — manufacturer ID "PNK"
// (bytes 8-9) and product name "punktfunk" (the 0xFC display-descriptor). Derived from the
// virtual-display-rs base block (a standard, widely-deployed virtual EDID); it deliberately carries NO
// other driver's bytes or branding. The serial-number field (offset 0x0C) encodes the per-monitor
// index, so `parse_monitor_description` can map an EDID the OS hands back to its monitor;
// `generate_with` patches that serial and `gen_checksum` recomputes byte 127 before the EDID reaches
// IddCx. The detailed-timing / range-limit descriptors are placeholders: the modes we actually
// advertise come from the monitor's stored mode list (`monitor.rs` / `callbacks.rs`), not from parsing
// this EDID.
const _EDID: [u8; 128] = [
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x41, 0xCB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0x21, 0x01, 0x03, 0x80, 0x32, 0x1F, 0x78, 0x07, 0xEE, 0x95, 0xA3, 0x54, 0x4C, 0x99, 0x26,
0x0F, 0x50, 0x54, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3A, 0x80, 0x18, 0x71, 0x38, 0x2D, 0x40, 0x58, 0x2C,
0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xFD, 0x00, 0x17, 0xF0, 0x0F,
0xFF, 0x0F, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x70,
0x75, 0x6E, 0x6B, 0x74, 0x66, 0x75, 0x6E, 0x6B, 0x0A, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
const EDID_LEN: usize = _EDID.len();
static EDID: AlignedEdid<EDID_LEN> = AlignedEdid {
data: _EDID,
_align: [],
};
#[repr(C)]
struct AlignedEdid<const N: usize> {
data: [u8; N],
// required to make this type aligned to Edid
_align: [Edid; 0],
}
impl<const N: usize> AlignedEdid<N> {
fn new(data: &[u8]) -> Result<Self, TryFromSliceError> {
let data: [u8; N] = data.try_into()?;
Ok(Self { data, _align: [] })
}
}
impl<const N: usize> Deref for AlignedEdid<N> {
type Target = Edid;
fn deref(&self) -> &Self::Target {
let header = &self.data[..EDID_SIZE];
bytemuck::from_bytes(header)
}
}
const EDID_SIZE: usize = std::mem::size_of::<Edid>();
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
pub struct Edid {
header: [u8; 8],
manufacturer_id: [u8; 2],
product_code: u16,
serial_number: u32,
manufacture_week: u8,
manufacture_year: u8,
version: u8,
revision: u8,
}
impl Edid {
pub fn generate_with(serial: u32) -> Vec<u8> {
// change serial number in the header
let mut header = *EDID;
header.serial_number = serial;
header.generate()
}
pub fn get_serial(edid: &[u8]) -> Result<u32, TryFromSliceError> {
let edid = AlignedEdid::<EDID_LEN>::new(edid)?;
Ok(edid.serial_number)
}
fn generate(&self) -> Vec<u8> {
let header = bytemuck::bytes_of(self);
// slice of monitor edid minus header
let data = &EDID.data[EDID_SIZE..];
// splice together header and the rest of the EDID
let mut edid: Vec<u8> = header.iter().chain(data).copied().collect();
// regenerate checksum
Self::gen_checksum(&mut edid);
edid
}
fn gen_checksum(data: &mut [u8]) {
// important, this is the bare minimum length
assert!(data.len() >= 128);
// slice to the entire data minus the last checksum byte
let edid_data = &data[..=126];
// do checksum calculation
let sum: u32 = edid_data.iter().copied().map(u32::from).sum();
// this wont ever truncate
#[allow(clippy::cast_possible_truncation)]
let checksum = (256 - (sum % 256)) as u8;
// update last byte with new checksum
data[127] = checksum;
}
}
@@ -0,0 +1,119 @@
//! Driver entry + WDF device-add. Adapted from virtual-display-rs (its event-log/boot-retry logger
//! dance is replaced by the `OutputDebugString` logger in `logger.rs`).
use log::{error, info};
use wdf_umdf::{
IddCxDeviceInitConfig, IddCxDeviceInitialize, WdfDeviceCreate, WdfDeviceCreateDeviceInterface,
WdfDeviceInitSetPnpPowerEventCallbacks, WdfDriverCreate,
};
use wdf_umdf_sys::{
GUID, IDD_CX_CLIENT_CONFIG, NTSTATUS, WDFDEVICE_INIT, WDFDRIVER__, WDFOBJECT, WDF_DRIVER_CONFIG,
WDF_OBJECT_ATTRIBUTES, WDF_PNPPOWER_EVENT_CALLBACKS, _DRIVER_OBJECT, _UNICODE_STRING,
};
use crate::callbacks::{
adapter_commit_modes, adapter_init_finished, assign_swap_chain, device_d0_entry,
monitor_get_default_modes, monitor_query_modes, parse_monitor_description, unassign_swap_chain,
};
use crate::context::DeviceContext;
use crate::control::device_io_control;
// SudoVDA control-interface GUID — the host opens this to drive the ADD/REMOVE/PING IOCTLs.
// {e5bcc234-1e0c-418a-a0d4-ef8b7501414d}
const SUVDA_INTERFACE_GUID: GUID = GUID {
Data1: 0xe5bc_c234,
Data2: 0x1e0c,
Data3: 0x418a,
Data4: [0xa0, 0xd4, 0xef, 0x8b, 0x75, 0x01, 0x41, 0x4d],
};
/// Driver entry point (called by the framework via `FxDriverEntryUm`).
#[no_mangle]
extern "C-unwind" fn DriverEntry(
driver_object: *mut _DRIVER_OBJECT,
registry_path: *mut _UNICODE_STRING,
) -> NTSTATUS {
crate::logger::init();
crate::panic::set_hook();
info!("pf-vdisplay v{} starting", env!("CARGO_PKG_VERSION"));
let mut attributes = WDF_OBJECT_ATTRIBUTES::init();
let mut config = WDF_DRIVER_CONFIG::init(Some(driver_add));
unsafe {
WdfDriverCreate(
driver_object,
registry_path,
Some(&mut attributes),
&mut config,
None,
)
}
.into()
}
extern "C-unwind" fn driver_add(
_driver: *mut WDFDRIVER__,
mut init: *mut WDFDEVICE_INIT,
) -> NTSTATUS {
let mut callbacks = WDF_PNPPOWER_EVENT_CALLBACKS::init();
callbacks.EvtDeviceD0Entry = Some(device_d0_entry);
unsafe {
_ = WdfDeviceInitSetPnpPowerEventCallbacks(init, &mut callbacks);
}
let Some(mut config) = IDD_CX_CLIENT_CONFIG::init() else {
error!("Failed to create IDD_CX_CLIENT_CONFIG");
return NTSTATUS::STATUS_NOT_FOUND;
};
config.EvtIddCxAdapterInitFinished = Some(adapter_init_finished);
config.EvtIddCxParseMonitorDescription = Some(parse_monitor_description);
config.EvtIddCxMonitorGetDefaultDescriptionModes = Some(monitor_get_default_modes);
config.EvtIddCxMonitorQueryTargetModes = Some(monitor_query_modes);
config.EvtIddCxAdapterCommitModes = Some(adapter_commit_modes);
config.EvtIddCxMonitorAssignSwapChain = Some(assign_swap_chain);
config.EvtIddCxMonitorUnassignSwapChain = Some(unassign_swap_chain);
// IddCx redirects device IOCTLs to this callback — our SudoVDA-compatible control plane.
config.EvtIddCxDeviceIoControl = Some(device_io_control);
let init_data = unsafe { &mut *init };
let status = unsafe { IddCxDeviceInitConfig(init_data, &config) };
if let Err(e) = status {
error!("Failed to init iddcx config: {e:?}");
return e.into();
}
let mut attributes =
WDF_OBJECT_ATTRIBUTES::init_context_type(unsafe { DeviceContext::get_type_info() });
attributes.EvtCleanupCallback = Some(event_cleanup);
let mut device = std::ptr::null_mut();
let status = unsafe { WdfDeviceCreate(&mut init, Some(&mut attributes), &mut device) };
if let Err(e) = status {
error!("Failed to create device: {e:?}");
return e.into();
}
// Register the SudoVDA control interface so the host can open it + send the control IOCTLs.
let status =
unsafe { WdfDeviceCreateDeviceInterface(device, &SUVDA_INTERFACE_GUID, std::ptr::null()) };
if let Err(e) = status {
error!("Failed to create control device interface: {e:?}");
return e.into();
}
let status = unsafe { IddCxDeviceInitialize(device) };
if let Err(e) = status {
error!("Failed to init iddcx device: {e:?}");
return e.into();
}
let context = DeviceContext::new(device);
unsafe { context.init(device as WDFOBJECT).into() }
}
unsafe extern "C-unwind" fn event_cleanup(wdf_object: WDFOBJECT) {
_ = unsafe { DeviceContext::drop(wdf_object) };
}
@@ -0,0 +1,38 @@
use std::ops::{Deref, DerefMut};
/// An unsafe wrapper to allow sending across threads
///
/// USE WISELY, IT CAN CAUSE UB OTHERWISE
pub struct Sendable<T>(T);
unsafe impl<T> Send for Sendable<T> {}
unsafe impl<T> Sync for Sendable<T> {}
impl<T> Sendable<T> {
/// `T` must be Send+Sync safe
pub unsafe fn new(t: T) -> Self {
Sendable(t)
}
}
impl<T> Deref for Sendable<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for Sendable<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[macro_export]
macro_rules! debug {
($($tt:tt)*) => {
if cfg!(debug_assertions) {
::log::debug!($($tt)*);
}
};
}
@@ -0,0 +1,33 @@
//! pf-vdisplay — punktfunk Windows virtual display (IddCx), in Rust.
//!
//! P1: a UMDF2 IddCx virtual display. Adapted from MolotovCherry/virtual-display-rs (MIT) — its
//! named-pipe IPC + serde mode config is replaced by an in-tree `monitor` model (and, next, the
//! SudoVDA-compatible IOCTL control plane our host already speaks). Logging goes to
//! `OutputDebugString` (no `log`-eventlog/`tokio`). See `docs/windows-virtual-display-rust-port.md`.
#![allow(non_snake_case)]
mod callbacks;
mod context;
mod control;
mod direct_3d_device;
mod edid;
mod entry;
mod helpers;
mod logger;
mod monitor;
mod panic;
mod swap_chain_processor;
use wdf_umdf_sys::{NTSTATUS, PUNICODE_STRING, PVOID};
// The framework entry point. UMDF's reflector calls this; the `+whole-archive` stub forwards to the
// `DriverEntry` symbol exported from `entry.rs`.
#[link(name = "WdfDriverStubUm", kind = "static", modifiers = "+whole-archive")]
extern "C" {
pub fn FxDriverEntryUm(
LoaderInterface: PVOID,
Context: PVOID,
DriverObject: PVOID,
RegistryPath: PUNICODE_STRING,
) -> NTSTATUS;
}
@@ -0,0 +1,34 @@
//! Minimal `log` backend that writes to `OutputDebugString` — no `driver-logger`/event-log/`tokio`.
//! View with DebugView/WinDbg. Keeping the `log` facade lets the ported callbacks/context use
//! `error!`/`info!`/`debug!` unchanged.
use log::{LevelFilter, Metadata, Record};
use windows::core::PCSTR;
use windows::Win32::System::Diagnostics::Debug::OutputDebugStringA;
struct DbgLogger;
impl log::Log for DbgLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
let msg = format!("[pf-vdisplay] {:<5} {}\0", record.level(), record.args());
// SAFETY: `msg` is a NUL-terminated byte string valid for the call.
unsafe { OutputDebugStringA(PCSTR(msg.as_ptr())) };
}
fn flush(&self) {}
}
static LOGGER: DbgLogger = DbgLogger;
pub fn init() {
let _ = log::set_logger(&LOGGER);
log::set_max_level(if cfg!(debug_assertions) {
LevelFilter::Debug
} else {
LevelFilter::Info
});
}
@@ -0,0 +1,103 @@
//! The monitor + mode model and control-plane state. Replaces virtual-display-rs's `ipc.rs`
//! (named-pipe IPC + serde `driver_ipc` types). Monitors are created on demand by the SudoVDA IOCTL
//! control plane (`control.rs`); each carries the GUID the host keys it by plus the OS target id +
//! render-adapter LUID captured at arrival (the ADD reply).
use std::ptr::NonNull;
use std::sync::atomic::{AtomicU32, AtomicU64};
use std::sync::{Mutex, OnceLock};
use wdf_umdf_sys::{IDDCX_ADAPTER__, IDDCX_MONITOR__};
pub type Dimen = u32;
pub type RefreshRate = u32;
/// One resolution with the refresh rates it supports.
#[derive(Clone, PartialEq, Eq)]
pub struct Mode {
pub width: Dimen,
pub height: Dimen,
pub refresh_rates: Vec<RefreshRate>,
}
/// A monitor's identity (the EDID serial) + advertised modes.
#[derive(Clone)]
pub struct MonitorData {
pub id: u32,
pub modes: Vec<Mode>,
}
/// A live (or pending) monitor.
pub struct MonitorObject {
pub object: Option<NonNull<IDDCX_MONITOR__>>,
pub data: MonitorData,
/// The full GUID the host keys this monitor by (ADD dedup / REMOVE).
pub guid: u128,
/// OS target id + render-adapter LUID, captured from `IDARG_OUT_MONITORARRIVAL` (the ADD reply).
pub target_id: u32,
pub adapter_luid_low: u32,
pub adapter_luid_high: i32,
}
// SAFETY: the raw IddCx object ptr is framework-managed; access is serialized by MONITOR_MODES.
unsafe impl Send for MonitorObject {}
unsafe impl Sync for MonitorObject {}
/// The IddCx adapter object, stashed for the control plane (SET_RENDER_ADAPTER).
pub struct AdapterObject(pub NonNull<IDDCX_ADAPTER__>);
// SAFETY: raw ptr managed by the framework.
unsafe impl Send for AdapterObject {}
unsafe impl Sync for AdapterObject {}
pub static ADAPTER: OnceLock<AdapterObject> = OnceLock::new();
pub static MONITOR_MODES: Mutex<Vec<MonitorObject>> = Mutex::new(Vec::new());
/// Monitor id / EDID-serial counter (unique per created monitor).
pub static NEXT_ID: AtomicU32 = AtomicU32::new(1);
/// Watchdog (seconds). The host reads the timeout via GET_WATCHDOG and PINGs to keep alive.
pub static WATCHDOG_TIMEOUT: AtomicU32 = AtomicU32::new(3);
pub static WATCHDOG_COUNTDOWN: AtomicU32 = AtomicU32::new(3);
/// The preferred render adapter LUID set via SET_RENDER_ADAPTER, packed `(high<<32)|low`. 0 = none.
pub static PREFERRED_RENDER_ADAPTER: AtomicU64 = AtomicU64::new(0);
/// Protocol version reported by GET_VERSION: {major, minor, incremental, testbuild} — matches SudoVDA.
pub const PROTOCOL_VERSION: [u8; 4] = [0, 2, 1, 1];
/// A single (width, height, refresh) tuple — modes flattened across their refresh rates.
#[derive(Copy, Clone)]
pub struct ModeItem {
pub width: Dimen,
pub height: Dimen,
pub refresh_rate: RefreshRate,
}
pub trait FlattenModes {
fn flatten(&self) -> impl Iterator<Item = ModeItem>;
}
impl FlattenModes for Vec<Mode> {
fn flatten(&self) -> impl Iterator<Item = ModeItem> {
self.iter().flat_map(|m| {
m.refresh_rates.iter().map(|&rr| ModeItem {
width: m.width,
height: m.height,
refresh_rate: rr,
})
})
}
}
/// Fallback modes appended after the client's requested mode, so a topology change still has options.
pub fn default_modes() -> Vec<Mode> {
vec![
Mode {
width: 1920,
height: 1080,
refresh_rates: vec![60, 120],
},
Mode {
width: 1280,
height: 720,
refresh_rates: vec![60],
},
]
}
@@ -0,0 +1,20 @@
#[cfg(debug_assertions)]
use std::backtrace::Backtrace;
use std::panic;
use log::error;
pub fn set_hook() {
panic::set_hook(Box::new(|v| {
// debug mode, get full backtrace
#[cfg(debug_assertions)]
{
let backtrace = Backtrace::force_capture();
error!("{v}\n\nstack backtrace:\n{backtrace}");
}
// otherwise just print the panic since we don't have a backtrace
#[cfg(not(debug_assertions))]
error!("{v}");
}));
}
@@ -0,0 +1,158 @@
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread::{self, JoinHandle},
};
use log::{debug, error};
use wdf_umdf::{
IddCxSwapChainFinishedProcessingFrame, IddCxSwapChainReleaseAndAcquireBuffer,
IddCxSwapChainSetDevice, WdfObjectDelete,
};
use wdf_umdf_sys::{
HANDLE, IDARG_IN_SWAPCHAINSETDEVICE, IDARG_OUT_RELEASEANDACQUIREBUFFER, IDDCX_SWAPCHAIN,
NTSTATUS, WAIT_TIMEOUT, WDFOBJECT,
};
use windows::{
core::{w, Interface},
Win32::{
Foundation::HANDLE as WHANDLE,
Graphics::Dxgi::IDXGIDevice,
System::Threading::{
AvRevertMmThreadCharacteristics, AvSetMmThreadCharacteristicsW, WaitForSingleObject,
},
},
};
use crate::{direct_3d_device::Direct3DDevice, helpers::Sendable};
pub struct SwapChainProcessor {
terminate: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
}
unsafe impl Send for SwapChainProcessor {}
unsafe impl Sync for SwapChainProcessor {}
impl SwapChainProcessor {
pub fn new() -> Self {
Self {
terminate: Arc::new(AtomicBool::new(false)),
thread: None,
}
}
pub fn run(
&mut self,
swap_chain: IDDCX_SWAPCHAIN,
device: Direct3DDevice,
available_buffer_event: HANDLE,
) {
let available_buffer_event = unsafe { Sendable::new(available_buffer_event) };
let swap_chain = unsafe { Sendable::new(swap_chain) };
let terminate = self.terminate.clone();
let join_handle = thread::spawn(move || {
// It is very important to prioritize this thread by making use of the Multimedia Scheduler Service.
// It will intelligently prioritize the thread for improved throughput in high CPU-load scenarios.
let mut av_task = 0u32;
let res = unsafe { AvSetMmThreadCharacteristicsW(w!("Distribution"), &mut av_task) };
let Ok(av_handle) = res else {
error!("Failed to prioritize thread: {res:?}");
return;
};
Self::run_core(*swap_chain, &device, *available_buffer_event, &terminate);
let res = unsafe { WdfObjectDelete(*swap_chain as WDFOBJECT) };
if let Err(e) = res {
error!("Failed to delete wdf object: {e:?}");
return;
}
// Revert the thread to normal once it's done
let res = unsafe { AvRevertMmThreadCharacteristics(av_handle) };
if let Err(e) = res {
error!("Failed to revert prioritize thread: {e:?}");
}
});
self.thread = Some(join_handle);
}
fn run_core(
swap_chain: IDDCX_SWAPCHAIN,
device: &Direct3DDevice,
available_buffer_event: HANDLE,
terminate: &AtomicBool,
) {
let dxgi_device = device.device.cast::<IDXGIDevice>();
let Ok(dxgi_device) = dxgi_device else {
error!("Failed to cast ID3D11Device to IDXGIDevice: {dxgi_device:?}");
return;
};
let set_device = IDARG_IN_SWAPCHAINSETDEVICE {
pDevice: dxgi_device.into_raw().cast(),
};
let res = unsafe { IddCxSwapChainSetDevice(swap_chain, &set_device) };
if res.is_err() {
debug!("Failed to set swapchain device: {res:?}");
return;
}
loop {
let mut buffer = IDARG_OUT_RELEASEANDACQUIREBUFFER::default();
let hr: NTSTATUS =
unsafe { IddCxSwapChainReleaseAndAcquireBuffer(swap_chain, &mut buffer).into() };
#[allow(clippy::items_after_statements)]
const E_PENDING: u32 = 0x8000_000A;
if u32::from(hr) == E_PENDING {
let wait_result =
unsafe { WaitForSingleObject(WHANDLE(available_buffer_event.cast()), 16).0 };
// thread requested an end
let should_terminate = terminate.load(Ordering::Relaxed);
if should_terminate {
break;
}
// WAIT_OBJECT_0 | WAIT_TIMEOUT
if matches!(wait_result, 0 | WAIT_TIMEOUT) {
// We have a new buffer, so try the AcquireBuffer again
continue;
}
// The wait was cancelled or something unexpected happened
break;
} else if hr.is_success() {
// This is the most performance-critical section of code in an IddCx driver. It's important that whatever
// is done with the acquired surface be finished as quickly as possible.
let hr = unsafe { IddCxSwapChainFinishedProcessingFrame(swap_chain) };
if hr.is_err() {
break;
}
} else {
// The swap-chain was likely abandoned (e.g. DXGI_ERROR_ACCESS_LOST), so exit the processing loop
break;
}
}
}
}
impl Drop for SwapChainProcessor {
fn drop(&mut self) {
if let Some(handle) = self.thread.take() {
// send signal to end thread
self.terminate.store(true, Ordering::Relaxed);
// wait until thread is finished
_ = handle.join();
}
}
}
@@ -0,0 +1,4 @@
[toolchain]
channel = "nightly-2024-07-26"
components = ["rustfmt", "clippy"]
profile = "minimal"
@@ -0,0 +1,17 @@
[package]
name = "wdf-umdf-sys"
version = "0.1.0"
edition = "2021"
[lints]
workspace = true
[dependencies]
paste = "1.0.15"
bytemuck = "1.19.0"
thiserror = "2.0.3"
[build-dependencies]
bindgen = "0.70.1"
thiserror = "2.0.3"
winreg = "0.52.0"
@@ -0,0 +1,275 @@
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";
const IDDCX_V: &str = "1.4";
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error("cannot find the directory")]
DirectoryNotFound,
}
/// Retrieves the path to the Windows Kits directory. The default should be
/// `C:\Program Files (x86)\Windows Kits\10`.
///
/// # Errors
/// Returns IO error if failed
fn get_windows_kits_dir() -> Result<PathBuf, Error> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let key = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
let dir: String = hklm.open_subkey(key)?.get_value("KitsRoot10")?;
Ok(dir.into())
}
#[derive(Clone, Copy, PartialEq)]
enum DirectoryType {
Include,
Library,
}
#[derive(Clone, Copy, PartialEq)]
enum Target {
X86_64,
ARM64,
}
impl Default for Target {
fn default() -> Self {
let target = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
match &*target {
"x86_64" => Self::X86_64,
"aarch64" => Self::ARM64,
_ => unimplemented!("{target} arch is unsupported"),
}
}
}
impl Display for Target {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Target::X86_64 => f.write_str("x64"),
Target::ARM64 => f.write_str("arm64"),
}
}
}
fn get_base_path<S: AsRef<Path>>(dir_type: DirectoryType, subs: &[S]) -> Result<PathBuf, Error> {
let mut dir = get_windows_kits_dir()?.join(match dir_type {
DirectoryType::Include => "Include",
DirectoryType::Library => "Lib",
});
dir.extend(subs);
if !dir.is_dir() {
return Err(Error::DirectoryNotFound);
}
Ok(dir)
}
fn get_sdk_path<S: AsRef<Path>>(dir_type: DirectoryType, subs: &[S]) -> Result<PathBuf, Error> {
// We first append lib to the path and read the directory..
let dir = get_windows_kits_dir()?
.join(match dir_type {
DirectoryType::Include => "Include",
DirectoryType::Library => "Lib",
})
.read_dir()?;
// In the lib directory we may have one or more directories named after the version of Windows,
// we will be looking for the highest version number.
let mut dir = dir
.filter_map(Result::ok)
.map(|dir| dir.path())
.filter(|dir| {
let is_sdk = dir
.components()
.last()
.and_then(|c| c.as_os_str().to_str())
.map_or(false, |c| c.starts_with("10."));
let mut sub_dir = dir.clone();
sub_dir.extend(subs);
is_sdk && sub_dir.is_dir()
})
.max()
.ok_or_else(|| Error::DirectoryNotFound)?;
dir.extend(subs);
if !dir.is_dir() {
return Err(Error::DirectoryNotFound);
}
// Finally append um to the path to get the path to the user mode libraries.
Ok(dir)
}
/// Retrieves the path to the user mode libraries. The path may look something like:
/// `C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\um`.
///
/// # Errors
/// Returns IO error if failed
fn get_um_dir(dir_type: DirectoryType) -> Result<PathBuf, Error> {
let target = Target::default().to_string();
let binding = &["um", &target];
let subs: &[&str] = match dir_type {
DirectoryType::Include => &["um"],
DirectoryType::Library => binding,
};
let dir = get_sdk_path(dir_type, subs)?;
Ok(dir)
}
/// # Errors
/// Returns IO error if failed
fn get_umdf_dir(dir_type: DirectoryType) -> Result<PathBuf, Error> {
match dir_type {
DirectoryType::Include => get_base_path(dir_type, &["wdf", "umdf", UMDF_V]),
DirectoryType::Library => get_base_path(
dir_type,
&["wdf", "umdf", &Target::default().to_string(), UMDF_V],
),
}
}
/// Retrieves the path to the shared headers. The path may look something like:
/// `C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\shared`.
///
/// # Errors
/// Returns IO error if failed
fn get_shared_dir() -> Result<PathBuf, Error> {
let dir = get_sdk_path(DirectoryType::Include, &["shared"])?;
Ok(dir)
}
fn build_dir() -> PathBuf {
PathBuf::from(
std::env::var_os("OUT_DIR").expect("the environment variable OUT_DIR is undefined"),
)
}
fn generate() {
// Find the include directory containing the user headers.
let include_um_dir = get_um_dir(DirectoryType::Include).unwrap();
let lib_um_dir = get_um_dir(DirectoryType::Library).unwrap();
let shared = get_shared_dir().unwrap();
println!("cargo:rustc-link-search={}", lib_um_dir.display());
// Tell Cargo to re-run this if src/wrapper.h gets changed.
println!("cargo:rerun-if-changed=c/wrapper.h");
//
// UMDF
//
let umdf_lib_dir = get_umdf_dir(DirectoryType::Library).unwrap();
println!("cargo:rustc-link-search={}", umdf_lib_dir.display());
let wdf_include_dir = get_umdf_dir(DirectoryType::Include).unwrap();
// need to link to umdf lib
println!("cargo:rustc-link-lib=static=WdfDriverStubUm");
//
// IDDCX
//
// The IddCx import lib lives only under the WDK's SDK version (e.g. 10.0.26100.0); a newer base
// SDK installed alongside it (e.g. 10.0.28000.0) has um\x64 but no iddcx subdir, so picking the
// max um\x64 version (lib_um_dir) misses it. Resolve by the version that actually contains
// iddcx — the same way the IddCx.h header path is resolved below.
let iddcx_lib_dir = get_sdk_path(
DirectoryType::Library,
&["um", &Target::default().to_string(), "iddcx", IDDCX_V],
)
.unwrap();
println!("cargo:rustc-link-search={}", iddcx_lib_dir.display());
// need to link to iddcx lib
println!("cargo:rustc-link-lib=static=IddCxStub");
//
// REST
//
// Get the build directory.
let out_path = build_dir();
// Generate the bindings
let mut builder = bindgen::Builder::default()
.derive_debug(false)
.layout_tests(false)
.default_enum_style(bindgen::EnumVariation::NewType {
is_bitfield: false,
is_global: false,
})
.merge_extern_blocks(true)
.header("c/wrapper.h")
.header(
get_sdk_path(DirectoryType::Include, &["um", "iddcx", IDDCX_V])
.unwrap()
.join("IddCx.h")
.to_string_lossy()
.to_string(),
)
// general um includes
.clang_arg(format!("-I{}", include_um_dir.display()))
// umdf includes
.clang_arg(format!("-I{}", wdf_include_dir.display()))
.clang_arg(format!("-I{}", shared.display()))
// because aarch64 needs to find excpt.h
.clang_arg(format!(
"-I{}",
get_sdk_path(DirectoryType::Include, &["km", "crt"])
.unwrap()
.display()
))
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.blocklist_type("_?P?IMAGE_TLS_DIRECTORY.*")
// we will use our own custom type
.blocklist_item("NTSTATUS")
.blocklist_item("IddMinimumVersionRequired")
.blocklist_item("WdfMinimumVersionRequired")
.clang_arg("--language=c++")
.clang_arg("-fms-compatibility")
.clang_arg("-fms-extensions")
.override_abi(Abi::CUnwind, ".*")
.generate_cstr(true)
.derive_default(true);
let defines = match Target::default() {
Target::X86_64 => ["AMD64", "_AMD64_"],
Target::ARM64 => ["ARM64", "_ARM64_"],
};
for define in defines {
builder = builder.clang_arg(format!("-D{define}"));
}
// generate
let umdf = builder.generate().unwrap();
// Write the bindings to the $OUT_DIR/bindings.rs file.
umdf.write_to_file(out_path.join("umdf.rs")).unwrap();
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
generate();
}
@@ -0,0 +1,22 @@
#include <Windows.h>
/**
*
* UMDF
*
*/
#define WDF_STUB
#include <wdf.h>
/**
*
* IDCXX
*
*/
#define IDD_STUB
// handled in build.rs
// #include <iddcx\1.4\IddCx.h>
@@ -0,0 +1,17 @@
#![allow(unsafe_op_in_unsafe_fn)]
#![allow(clippy::all)]
#![allow(clippy::pedantic)]
#![allow(clippy::restriction)]
// stand-in type replacing NTSTATUS in the bindings
use crate::NTSTATUS;
include!(concat!(env!("OUT_DIR"), "/umdf.rs"));
// required for some macros
unsafe impl Send for _WDF_OBJECT_CONTEXT_TYPE_INFO {}
unsafe impl Sync for _WDF_OBJECT_CONTEXT_TYPE_INFO {}
// fails to build without this symbol
#[no_mangle]
pub static IddMinimumVersionRequired: ULONG = 4;
@@ -0,0 +1,211 @@
#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals, unused)]
mod bindings;
mod ntstatus;
use std::fmt::{self, Display};
pub use bindings::*;
pub use ntstatus::*;
pub use paste::paste;
#[macro_export]
macro_rules! WdfIsFunctionAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::WdfClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let fn_count = unsafe { $crate::WdfFunctionCount };
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h#L126
$crate::paste! {
// index is always positive, see
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h
const FN_INDEX: u32 = $crate::WDFFUNCENUM::[<$name TableIndex>].0 as u32;
FN_INDEX < $crate::WDF_ALWAYS_AVAILABLE_FUNCTION_COUNT
|| !higher || FN_INDEX < fn_count
}
}};
}
#[macro_export]
macro_rules! WdfIsStructureAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::WdfClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let struct_count = unsafe { $crate::WdfStructureCount };
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h#L141
$crate::paste! {
// index is always positive, see
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h
const STRUCT_INDEX: u32 = $crate::WDFSTRUCTENUM::[<INDEX_ $name>].0 as u32;
!higher || STRUCT_INDEX < struct_count
}
}};
}
#[macro_export]
macro_rules! IddCxIsFunctionAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::IddClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let fn_count = unsafe { $crate::IddFunctionCount };
$crate::paste! {
const FN_INDEX: u32 = $crate::IDDFUNCENUM::[<$name TableIndex>].0 as u32;
FN_INDEX < $crate::IDD_ALWAYS_AVAILABLE_FUNCTION_COUNT
|| !higher || FN_INDEX < fn_count
}
}};
}
#[macro_export]
macro_rules! IddCxIsStructureAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::IddClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let struct_count = unsafe { $crate::IddStructureCount };
$crate::paste! {
const STRUCT_INDEX: u32 = $crate::IDDSTRUCTENUM::[<INDEX_ $name>].0 as u32;
!higher || STRUCT_INDEX < struct_count
}
}};
}
macro_rules! WDF_STRUCTURE_SIZE {
($name:ty) => {
u32::try_from(::core::mem::size_of::<$name>()).expect("size is correct")
};
}
#[macro_export]
macro_rules! WDF_NO_HANDLE {
() => {
::core::ptr::null_mut()
};
}
#[macro_export]
macro_rules! WDF_NO_OBJECT_ATTRIBUTES {
() => {
::core::ptr::null_mut()
};
}
#[macro_export]
macro_rules! WDF_OBJECT_ATTRIBUTES_SET_CONTEXT_TYPE {
($attr:ident, $context_type:ident) => {
$attr.ContextTypeInfo = $context_type;
};
}
impl WDF_OBJECT_ATTRIBUTES {
/// Initializes the [`WDF_OBJECT_ATTRIBUTES`] structure
/// <https://github.com/microsoft/Windows-Driver-Frameworks/blob/a94b8c30dad524352fab90872aefc83920b98e56/src/publicinc/wdf/umdf/2.33/wdfobject.h#L136/>
///
/// Sets
/// - `ExecutionLevel` to [`WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent`]
/// - `SynchronizationScope` to [`WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent`]
#[must_use]
pub fn init() -> Self {
// SAFETY: All fields are zero-able
let mut attributes: Self = unsafe { ::core::mem::zeroed() };
attributes.Size = WDF_STRUCTURE_SIZE!(Self);
attributes.SynchronizationScope =
WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent;
attributes.ExecutionLevel = WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent;
attributes
}
#[must_use]
pub fn init_context_type(context_type: &_WDF_OBJECT_CONTEXT_TYPE_INFO) -> Self {
let mut attr = Self::init();
WDF_OBJECT_ATTRIBUTES_SET_CONTEXT_TYPE!(attr, context_type);
attr
}
}
impl WDF_DRIVER_CONFIG {
/// Initializes the [`WDF_DRIVER_CONFIG`] structure
/// <https://github.com/microsoft/Windows-Driver-Frameworks/blob/a94b8c30dad524352fab90872aefc83920b98e56/src/publicinc/wdf/umdf/2.33/wdfdriver.h#L134/>
#[must_use]
pub fn init(EvtDriverDeviceAdd: PFN_WDF_DRIVER_DEVICE_ADD) -> Self {
// SAFETY: All fields are zero-able
let mut config: Self = unsafe { core::mem::zeroed() };
config.Size = WDF_STRUCTURE_SIZE!(Self);
config.EvtDriverDeviceAdd = EvtDriverDeviceAdd;
config
}
}
impl WDF_PNPPOWER_EVENT_CALLBACKS {
/// Initializes the [`WDF_PNPPOWER_EVENT_CALLBACKS`] structure
/// <https://github.com/microsoft/Windows-Driver-Frameworks/blob/a94b8c30dad524352fab90872aefc83920b98e56/src/publicinc/wdf/umdf/2.33/wdfdevice.h#L1278/>
#[must_use]
pub fn init() -> Self {
// SAFETY: All fields are zero-able
let mut callbacks: Self = unsafe { core::mem::zeroed() };
callbacks.Size = WDF_STRUCTURE_SIZE!(Self);
callbacks
}
}
/// If this returns None, the struct is NOT available to be used
macro_rules! IDD_STRUCTURE_SIZE {
($name:ty) => {{
// SAFETY: We only ever do read access, copy is fine
let higher = unsafe { IddClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access, copy is fine
let struct_count = unsafe { IddStructureCount };
if higher {
// as u32 is fine, since there's no way there's > 4 billion structs
const STRUCT_INDEX: u32 =
$crate::paste! { IDDSTRUCTENUM::[<INDEX_ $name:upper>].0 as u32 };
// SAFETY: A pointer to a [size_t], copying the pointer is ok
let ptr = unsafe { IddStructures };
if STRUCT_INDEX < struct_count {
// SAFETY: we validated struct index is able to be accessed
let ptr = unsafe { ptr.add(STRUCT_INDEX as usize) };
// SAFETY: So it's ok to read
u32::try_from(unsafe { ptr.read() }).ok()
} else {
// struct CANNOT be used
None
}
} else {
u32::try_from(::std::mem::size_of::<$name>()).ok()
}
}};
}
impl IDD_CX_CLIENT_CONFIG {
#[must_use]
pub fn init() -> Option<Self> {
// SAFETY: All fields are zero-able
let mut config: Self = unsafe { core::mem::zeroed() };
config.Size = IDD_STRUCTURE_SIZE!(IDD_CX_CLIENT_CONFIG)?;
Some(config)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
[package]
name = "wdf-umdf"
version = "0.1.0"
edition = "2021"
[lints]
workspace = true
[dependencies]
wdf-umdf-sys = { path = "../wdf-umdf-sys" }
paste = "1.0.15"
thiserror = "2.0.3"
@@ -0,0 +1,319 @@
#![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_OUT_QUERY_HWCURSOR, IDARG_OUT_RELEASEANDACQUIREBUFFER, 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
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxSwapChainFinishedProcessingFrame(
// in
SwapChainObject: IDDCX_SWAPCHAIN
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
true,
IddCxSwapChainFinishedProcessingFrame(
SwapChainObject
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorDeparture(
// in
MonitorObject: IDDCX_MONITOR
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorDeparture(
MonitorObject
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn IddCxAdapterSetRenderAdapter(
// in
AdapterObject: IDDCX_ADAPTER,
// in
pInArgs: *const IDARG_IN_ADAPTERSETRENDERADAPTER,
) -> Result<(), IddCxError> {
IddCxCall!(IddCxAdapterSetRenderAdapter(AdapterObject, pInArgs))
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorSetupHardwareCursor(
// in
MonitorObject: IDDCX_MONITOR,
// in
pInArgs: &IDARG_IN_SETUP_HWCURSOR
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorSetupHardwareCursor(
MonitorObject,
pInArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorQueryHardwareCursor(
// in
MonitorObject: IDDCX_MONITOR,
// in
pInArgs: &IDARG_IN_QUERY_HWCURSOR,
// out
pOutArgs: &mut IDARG_OUT_QUERY_HWCURSOR
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorQueryHardwareCursor(
MonitorObject,
pInArgs,
pOutArgs
)
)
}
@@ -0,0 +1,30 @@
mod iddcx;
mod wdf;
use std::any::Any;
pub use paste::paste;
pub use iddcx::*;
pub use wdf::*;
pub use wdf_umdf_sys;
use wdf_umdf_sys::NTSTATUS;
/// Used for the macros so they can correctly convert a functions result
fn is_nt_error(val: &dyn Any, other_is_error: bool) -> bool {
if let Some(status) = val.downcast_ref::<NTSTATUS>() {
return !status.is_success();
}
// other errors which may not be error codes, but may also be
// such as HRESULT == i32
if other_is_error {
if let Some(status) = val.downcast_ref::<i32>() {
let status = NTSTATUS(*status);
return !status.is_success();
}
}
false
}
@@ -0,0 +1,667 @@
#![allow(non_snake_case)]
#![allow(clippy::missing_errors_doc)]
use std::ffi::c_void;
use std::sync::OnceLock;
use wdf_umdf_sys::{
DEVPROPTYPE, GUID, NTSTATUS, PCUNICODE_STRING, PCWDF_OBJECT_CONTEXT_TYPE_INFO, PDRIVER_OBJECT,
POOL_TYPE, PVOID, PWDFDEVICE_INIT, PWDF_DRIVER_CONFIG, PWDF_OBJECT_ATTRIBUTES, ULONG_PTR,
WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFOBJECT, WDFREQUEST, WDF_DEVICE_FAILED_ACTION, WDF_NO_HANDLE,
WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES, _WDF_DEVICE_PROPERTY_DATA,
_WDF_PNPPOWER_EVENT_CALLBACKS,
};
#[derive(Copy, Clone, Debug, thiserror::Error)]
pub enum WdfError {
#[error("{0}")]
WdfFunctionNotAvailable(&'static str),
#[error("{0}")]
CallFailed(NTSTATUS),
#[error("Failed to upgrade Arc pointer")]
UpgradeFailed,
#[error("Failed to lock")]
LockFailed,
#[error("Unknown")]
Unknown,
// this is required for success status for ()
#[error("This is not an error, ignore it")]
_Success,
}
impl From<()> for WdfError {
fn from(_: ()) -> Self {
WdfError::_Success
}
}
impl From<*mut c_void> for WdfError {
fn from(_: *mut c_void) -> Self {
Self::Unknown
}
}
impl From<WdfError> for NTSTATUS {
fn from(value: WdfError) -> Self {
#[allow(clippy::enum_glob_use)]
use WdfError::*;
match value {
WdfFunctionNotAvailable(_) => Self::STATUS_NOT_FOUND,
CallFailed(status) => status,
UpgradeFailed => Self::STATUS_INVALID_HANDLE,
LockFailed => Self::STATUS_WAS_LOCKED,
Unknown => Self::STATUS_DRIVER_INTERNAL_ERROR,
_Success => 0.into(),
}
}
}
impl From<NTSTATUS> for WdfError {
fn from(value: NTSTATUS) -> Self {
WdfError::CallFailed(value)
}
}
macro_rules! WdfCall {
($name:ident ( $($args:expr),* )) => {
WdfCall!(false, $name($($args),*))
};
($other_is_error:expr, $name:ident ( $($args:expr),* )) => {{
static CACHED_FN: OnceLock<
Result<
::paste::paste!(::wdf_umdf_sys::[<PFN_ $name:upper>]),
WdfError
>
> = OnceLock::new();
let f = CACHED_FN.get_or_init(|| {
::paste::paste! {
const FN_INDEX: usize = ::wdf_umdf_sys::WDFFUNCENUM::[<$name TableIndex>].0 as usize;
// validate that wdf function can be used
let is_available = ::wdf_umdf_sys::WdfIsFunctionAvailable!($name);
if is_available {
// SAFETY: Only immutable accesses are done to this
let fn_table = unsafe { ::wdf_umdf_sys::WdfFunctions_02031 };
// SAFETY: Read-only, initialized by the time we use it, and checked to be in bounds
let f = unsafe {
fn_table
.add(FN_INDEX)
.cast::<::wdf_umdf_sys::[<PFN_ $name:upper>]>()
};
// SAFETY: Ensured that this is present by if condition from `WdfIsFunctionAvailable!`
let f = unsafe { f.read() };
Ok(f)
} else {
Err($crate::WdfError::WdfFunctionNotAvailable(concat!(stringify!($name), " is not available")))
}
}
}).clone()?;
// SAFETY: Above: If it's Ok, then it's guaranteed to be Some(fn)
let f = unsafe { f.unwrap_unchecked() };
// SAFETY: Pointer to globals is always immutable
let globals = unsafe { ::wdf_umdf_sys::WdfDriverGlobals };
// SAFETY: None. User is responsible for safety and must use their own unsafe block
let result = unsafe { f(globals, $($args),*) };
if $crate::is_nt_error(&result, $other_is_error) {
Err(result.into())
} else {
Ok(result.into())
}
}};
}
/// Unlike the official `WDF_DECLARE_CONTEXT_TYPE` macro, you only need to declare this on the actual data struct want to use
/// Safety is maintained through a `RwLock` of the underlying data
///
/// This generates associated fns `init`/`get`/`drop`/`get_type_info` on your `$context_type` with the same visibility
///
/// Example:
/// ```rust
/// pub struct IndirectDeviceContext {
/// device: WDFDEVICE,
/// }
///
/// impl IndirectDeviceContext {
/// pub fn new(device: WDFDEVICE) -> Self {
/// Self { device }
/// }
/// }
///
/// WDF_DECLARE_CONTEXT_TYPE!(pub IndirectDeviceContext);
///
/// // with a `device: WDFDEVICE`
/// let context = IndirectDeviceContext::new(device as WDFOBJECT);
/// IndirectDeviceContext::init(context);
/// // elsewhere
/// let mutable_access = IndirectDeviceContext::get_mut(device).unwrap();
/// ```
#[macro_export]
macro_rules! WDF_DECLARE_CONTEXT_TYPE {
($sv:vis $context_type:ident) => {
$crate::paste! {
// keep it in a mod block to disallow access to private types
#[allow(non_snake_case)]
mod [<WdfObject $context_type>] {
use super::$context_type;
// Require `T: Sync` for safety. User has to uphold the invariant themselves
#[repr(transparent)]
#[allow(non_camel_case_types)]
struct [<_WDF_ $context_type _STATIC_WRAPPER>]<T> {
cell: ::std::cell::UnsafeCell<$crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO>,
_phantom: ::std::marker::PhantomData<T>
}
// SAFETY: `T` impls Sync too
unsafe impl<T: Sync> Sync for [<_WDF_ $context_type _STATIC_WRAPPER>]<T> {}
// Unsure if C mutates this data, but it's in an unsafecell just in case
#[allow(non_upper_case_globals)]
static [<_WDF_ $context_type _TYPE_INFO>]: [<_WDF_ $context_type _STATIC_WRAPPER>]<$context_type> =
[<_WDF_ $context_type _STATIC_WRAPPER>] {
cell: ::std::cell::UnsafeCell::new(
$crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO {
Size: ::std::mem::size_of::<$crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO>() as u32,
ContextName: concat!(stringify!($context_type), "\0")
.as_ptr().cast::<::std::ffi::c_char>(),
ContextSize: ::std::mem::size_of::<[<WdfObject $context_type>]>(),
// SAFETY:
// StaticWrapper and UnsafeCell are both repr(transparent), so cast to underlying _WDF_OBJECT_CONTEXT_TYPE_INFO is ok
UniqueType: &[<_WDF_ $context_type _TYPE_INFO>] as *const _ as *const _,
EvtDriverGetUniqueContextType: ::std::option::Option::None,
}
),
_phantom: ::std::marker::PhantomData
};
/// Allows us to keep ONE main Arc allocation while handing out weak pointers to the rest of the clones.
/// In this way, we can drop the allocation by dropping 1 arc, while letting others still access it
enum ArcPointer<T> {
Strong(::std::sync::Arc<T>),
Weak(::std::sync::Weak<T>)
}
#[repr(transparent)]
struct [<WdfObject $context_type>](ArcPointer<::std::sync::RwLock<$context_type>>);
impl $context_type {
/// Initialize and place context into internal WdfObject
///
/// SAFETY:
/// - handle must be a fresh unused object with no data in its context already
/// - context type must already have been set up for handle
/// - Must be set only once regardless of the object. For all other objects, use clone_into()
$sv unsafe fn init(
self,
handle: $crate::wdf_umdf_sys::WDFOBJECT,
) -> ::std::result::Result<(), $crate::WdfError> {
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle, [<_WDF_ $context_type _TYPE_INFO>].cell.get())?
} as *mut ::std::mem::MaybeUninit<[<WdfObject $context_type>]>;
let context = &mut *context;
// Write to the memory location, making the data in it init
context.write(
[<WdfObject $context_type>](
ArcPointer::Strong(::std::sync::Arc::new(::std::sync::RwLock::new(self)))
)
);
Ok(())
}
/// Initialize handle's context and clone a Weak pointer to self context into it.
/// Internally, these are Arc's, so they will always point to the same data.
/// When the main Arc drops, none of these may access memory any longer
///
/// SAFETY:
/// - handle must be a fresh unused object with no data in its context already
/// - to_handle must have set context_type for this type via WDF_OBJECT_ATTRIBUTES when it was created
/// - to_handle must be a valid T
$sv unsafe fn clone_into(
&self,
handle: $crate::wdf_umdf_sys::WDFOBJECT
) -> ::std::result::Result<(), $crate::WdfError> {
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle, [<_WDF_ $context_type _TYPE_INFO>].cell.get())?
} as *mut ::std::mem::MaybeUninit<[<WdfObject $context_type>]>;
let context = &mut *context;
let from_context = unsafe {
$crate::WdfObjectGetTypedContextWorker(self.device as *mut _, [<_WDF_ $context_type _TYPE_INFO>].cell.get())?
} as *mut [<WdfObject $context_type>];
let from_context = match &(*from_context).0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
// Write to the memory location, making the data in it init
// clones the arc into new handle
context.write(
[<WdfObject $context_type>](ArcPointer::Weak(::std::sync::Arc::downgrade(&from_context)))
);
Ok(())
}
/// NOTE: Dropping memory that was created via `clone_into` will never drop the main allocation.
/// To drop the main allocation, you need to drop the instance made via `init`.
/// That instance can be obtained through the original handle you created it through
///
/// SAFETY:
/// - Data in context is assumed to already be init and a valid T
/// - Therefore, init for the context must already have been done on this handle
/// - No other mutable/non-mutable refs can exist to data when this is called, or it will alias
///
/// This may overwrite data in the handle's context memory, it is UB to read it after drop (e.g. get*)
$sv unsafe fn drop(
handle: $crate::wdf_umdf_sys::WDFOBJECT,
) -> ::std::result::Result<(), $crate::WdfError> {
let context = $crate::WdfObjectGetTypedContextWorker(
handle,
[<_WDF_ $context_type _TYPE_INFO>].cell.get(),
)? as *mut [<WdfObject $context_type>];
// drop the memory
::std::ptr::drop_in_place(context);
Ok(())
}
/// Borrow the context immutably
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn get<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&$context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let guard = context.read().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&*guard);
Ok(())
}
/// Borrow the context mutably
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn get_mut<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&mut $context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let mut guard = context.write().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&mut *guard);
Ok(())
}
/// Borrow the context immutably
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn try_get<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&$context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let guard = context.try_read().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&*guard);
Ok(())
}
/// Try to borrow the context mutably. Immediately returns if it's locked
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn try_get_mut<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&mut $context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let mut guard = context.try_write().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&mut *guard);
Ok(())
}
// SAFETY:
// - No other mutable refs must exist to target type
// - Underlying memory must remain immutable and unchanged until reference is dropped
$sv unsafe fn get_type_info() -> &'static $crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO {
unsafe { &*[<_WDF_ $context_type _TYPE_INFO>].cell.get() }
}
}
}
}
};
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDriverCreate(
// in
DriverObject: PDRIVER_OBJECT,
// in
RegistryPath: PCUNICODE_STRING,
// in, optional
DriverAttributes: Option<PWDF_OBJECT_ATTRIBUTES>,
// in
DriverConfig: PWDF_DRIVER_CONFIG,
// out, optional
Driver: Option<&mut WDFDRIVER>,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDriverCreate(
DriverObject,
RegistryPath,
DriverAttributes.unwrap_or(WDF_NO_OBJECT_ATTRIBUTES!()),
DriverConfig,
Driver
.map(std::ptr::from_mut)
.unwrap_or(WDF_NO_HANDLE!())
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceCreate(
// in, out
DeviceInit: &mut PWDFDEVICE_INIT,
// in, optional
DeviceAttributes: Option<&mut WDF_OBJECT_ATTRIBUTES>,
// out
Device: &mut WDFDEVICE,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDeviceCreate(
DeviceInit,
DeviceAttributes.map_or(WDF_NO_OBJECT_ATTRIBUTES!(), std::ptr::from_mut),
Device
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceInitSetPnpPowerEventCallbacks(
// in
DeviceInit: PWDFDEVICE_INIT,
// in
PnpPowerEventCallbacks: *mut _WDF_PNPPOWER_EVENT_CALLBACKS,
) -> Result<(), WdfError> {
WdfCall! {
WdfDeviceInitSetPnpPowerEventCallbacks(
DeviceInit,
PnpPowerEventCallbacks
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfObjectGetTypedContextWorker(
// in
Handle: WDFOBJECT,
// in
TypeInfo: PCWDF_OBJECT_CONTEXT_TYPE_INFO,
) -> Result<*mut c_void, WdfError> {
WdfCall! {
WdfObjectGetTypedContextWorker(
Handle,
TypeInfo
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfObjectDelete(
// in
Object: WDFOBJECT,
) -> Result<(), WdfError> {
WdfCall! {
WdfObjectDelete(
Object
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceSetFailed(
// in
Device: WDFDEVICE,
// in
FailedAction: WDF_DEVICE_FAILED_ACTION,
) -> Result<(), WdfError> {
WdfCall! {
WdfDeviceSetFailed(
Device,
FailedAction
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceAllocAndQueryPropertyEx(
// in
Device: WDFDEVICE,
// in
DeviceProperty: &mut _WDF_DEVICE_PROPERTY_DATA,
// in
PoolType: POOL_TYPE,
// in, optional
PropertyMemoryAttributes: PWDF_OBJECT_ATTRIBUTES,
// out
PropertyMemory: &mut WDFMEMORY,
// out
Type: &mut DEVPROPTYPE,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDeviceAllocAndQueryPropertyEx(
Device,
DeviceProperty,
PoolType,
PropertyMemoryAttributes,
PropertyMemory,
Type
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfMemoryGetBuffer(
// in
Memory: WDFMEMORY,
// out, optional
BufferSize: Option<&mut usize>,
) -> Result<*mut c_void, WdfError> {
WdfCall! {
WdfMemoryGetBuffer(
Memory,
BufferSize.map_or(std::ptr::null_mut(), std::ptr::from_mut)
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceCreateDeviceInterface(
Device: WDFDEVICE,
InterfaceClassGUID: *const GUID,
ReferenceString: PCUNICODE_STRING,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDeviceCreateDeviceInterface(
Device,
InterfaceClassGUID,
ReferenceString
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfRequestRetrieveInputBuffer(
Request: WDFREQUEST,
MinimumRequiredLength: usize,
Buffer: *mut PVOID,
Length: *mut usize,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfRequestRetrieveInputBuffer(
Request,
MinimumRequiredLength,
Buffer,
Length
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfRequestRetrieveOutputBuffer(
Request: WDFREQUEST,
MinimumRequiredSize: usize,
Buffer: *mut PVOID,
Length: *mut usize,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfRequestRetrieveOutputBuffer(
Request,
MinimumRequiredSize,
Buffer,
Length
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfRequestCompleteWithInformation(
Request: WDFREQUEST,
Status: NTSTATUS,
Information: ULONG_PTR,
) -> Result<(), WdfError> {
WdfCall! {
WdfRequestCompleteWithInformation(
Request,
Status,
Information
)
}
}