refactor(windows-packaging): delete the superseded vdisplay-driver/ tree (M6)

The old all-Rust IddCx driver tree (packaging/windows/vdisplay-driver/ — the wdf-umdf-sys 'oracle', 7896 lines) is fully superseded by packaging/windows/drivers/ (wdk-sys / windows-drivers-rs + the owned pf-vdisplay-proto ABI), which is the source of the vendored + installed driver. It was in NO cargo workspace (never built) and NO CI workflow; only stale doc/script refs pointed at it (the confusion the audit + game-capture-bug doc both flagged).

Delete it + repoint the build-relevant refs (packaging/windows/README.md, stage-pf-vdisplay.ps1, pack-host-installer.ps1) at drivers/ + drivers/deploy-dev.ps1. The vendored driver (packaging/windows/pf-vdisplay/) is unaffected; docs/windows-virtual-display-rust-port.md keeps its historical mentions as narrative.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 16:37:00 +00:00
parent 48f980ebb1
commit a2bd0cd77c
35 changed files with 0 additions and 7896 deletions
@@ -1,2 +0,0 @@
[build]
rustflags = ["-C", "target-feature=+crt-static"]
@@ -1,3 +0,0 @@
/target
*.cer
*.pfx
-510
View File
@@ -1,510 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "bindgen"
version = "0.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pf-vdisplay"
version = "0.1.0"
dependencies = [
"anyhow",
"bytemuck",
"log",
"thiserror",
"wdf-umdf",
"wdf-umdf-sys",
"windows",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wdf-umdf"
version = "0.1.0"
dependencies = [
"paste",
"thiserror",
"wdf-umdf-sys",
]
[[package]]
name = "wdf-umdf-sys"
version = "0.1.0"
dependencies = [
"bindgen",
"bytemuck",
"paste",
"thiserror",
"winreg",
]
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys",
]
@@ -1,26 +0,0 @@
# pf-vdisplay — punktfunk Windows virtual display (IddCx), in Rust.
#
# A self-contained driver workspace (NOT built on windows-drivers-rs like the gamepad drivers — IddCx
# functions are direct IddCxStub exports the WDF function-table macro can't reach, so a unified bindgen
# is the cleaner base). The wdf-umdf-sys / wdf-umdf binding crates are vendored from MolotovCherry's
# MIT-licensed virtual-display-rs (see LICENSE.virtual-display-rs); pf-vdisplay is our driver, swapping
# its named-pipe IPC for the SudoVDA-compatible IOCTL control plane our host already speaks.
[workspace]
resolver = "2"
members = ["wdf-umdf-sys", "wdf-umdf", "pf-vdisplay"]
[profile.release]
strip = true
codegen-units = 1
lto = true
[workspace.lints.rust]
unsafe_op_in_unsafe_fn = "deny"
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
multiple_unsafe_ops_per_block = "deny"
ignored_unit_patterns = "allow"
missing_errors_doc = "allow"
module_inception = "allow"
module_name_repetitions = "allow"
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Cherry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,61 +0,0 @@
# pf-vdisplay — punktfunk Windows virtual display (Rust IddCx)
P1 of replacing the vendored **SudoVDA** C++ driver with one we own — a pure-Rust UMDF2 **IddCx**
(Indirect Display Driver) virtual display, drop-in compatible with the host's existing
`vdisplay/sudovda.rs` IOCTL control plane. Full rationale + roadmap:
[`docs/windows-virtual-display-rust-port.md`](../../../docs/windows-virtual-display-rust-port.md).
## Layout
```
vdisplay-driver/
wdf-umdf-sys/ VENDORED bindgen FFI to WDF + IddCx (links WdfDriverStubUm + IddCxStub)
wdf-umdf/ VENDORED safe wrappers (IddCx*/Wdf*)
pf-vdisplay/ OUR driver crate (cdylib) + pf_vdisplay.inx
```
`wdf-umdf-sys` / `wdf-umdf` are vendored from [MolotovCherry/virtual-display-rs](https://github.com/MolotovCherry/virtual-display-rs)
(MIT — see `LICENSE.virtual-display-rs`). They're a self-contained bindgen over the WDK, **not**
`windows-drivers-rs` (which the gamepad drivers use): IddCx functions are direct `IddCxStub` exports the
WDF function-table macro can't reach, so a unified bindgen is the cleaner base. Local fix carried in
`wdf-umdf-sys/build.rs`: resolve the `IddCxStub` lib path by the SDK version that actually contains
`um\x64\iddcx\<ver>` (a newer base SDK alongside the WDK has `um\x64` but no `iddcx`).
## Status
- **Scaffold builds** ✅ — workspace + vendored bindings + `pf-vdisplay` compile in-tree to
`pf_vdisplay.dll`. The reference (virtual-display-rs) was separately built + installed + loaded
(Status OK) on the RTX box, proving the IddCx-in-Rust chain end to end.
- **Next (P1 driver logic):** port the IddCx driver — `DriverEntry``IDD_CX_CLIENT_CONFIG`
(adapter-init / parse-monitor-description / query-target-modes / assign-swapchain) → device + monitor
context, generic EDID, no-op swap-chain drain (DDA still captures for P1). Logging via
`OutputDebugString` (no `log`/`driver-logger`/`tokio`).
- **Then (control plane):** the SudoVDA-compatible IOCTL surface on the control device
(`ADD`/`REMOVE`/`PING`/`GET_WATCHDOG`/`GET_VERSION`/`SET_RENDER_ADAPTER`, byte-identical structs +
the `{e5bcc234-…}` interface GUID) so `vdisplay/sudovda.rs` drives it **unchanged**; a default mode
table + the per-`ADD` client mode injected as preferred; the watchdog.
- **Later (P2):** push swap-chain frames straight to the host (skip DDA); HDR via the IddCx 1.11 D3D12
acquire path.
## Build
Needs the WDK (UMDF 2.31 + IddCx stubs), LLVM/clang (`LIBCLANG_PATH`), and the pinned
`nightly-2024-07-26` (auto-selected via `rust-toolchain.toml`). From `pf-vdisplay/`, inside an MSVC dev
shell:
```
set LIBCLANG_PATH=C:\Program Files\LLVM\bin
cargo build # -> ../target/x86_64-pc-windows-msvc/debug/pf_vdisplay.dll
```
## Sign + install (same recipe as the gamepad drivers)
1. (no FORCE_INTEGRITY bit to clear — this crate doesn't set `/INTEGRITYCHECK`)
2. `signtool sign /fd SHA256 /sha1 <punktfunk-ds-test thumbprint>` the renamed `pf_vdisplay.dll`
3. `stampinf -f pf_vdisplay.inf -d * -a amd64 -u 2.15.0 -v <ver>` ; `Inf2Cat /driver:<dir> /os:10_X64` ;
sign the `.cat`
4. `pnputil /add-driver pf_vdisplay.inf` ; create the root devnode (`nefconc --create-device-node
--hardware-id root\pf_vdisplay --class-name Display --class-guid {4d36e968-…}`, mirroring
`install-sudovda.ps1`)
Bundles into the Inno Setup installer the same way as `gamepad-drivers/` once the driver is functional.
@@ -1,77 +0,0 @@
#requires -Version 5.1
<#
.SYNOPSIS
Stage, sign, and (optionally) install the pf-vdisplay UMDF IddCx driver for local dev/test.
.DESCRIPTION
Copies the freshly built pf_vdisplay.dll into the stage dir, signs it with the self-signed test cert,
stamps a STRICTLY INCREASING DriverVer (date.time) into the INF, then generates and signs the catalog.
With -Install it also pnputil /add-driver's the package and creates the Root\pf_vdisplay devnode.
Why the DriverVer bump matters: `pnputil /add-driver /install` only replaces an already-installed
driver binary when the INF DriverVer is higher than the installed one. Re-deploying without a bump
silently keeps the OLD binary loaded — a trap that masks code changes during iteration.
Build first: from pf-vdisplay/, in an MSVC dev shell with LIBCLANG_PATH set, run `cargo build`.
NOTE: pf-vdisplay and SudoVDA register the SAME control-interface GUID, so only one may be active at a
time. On the dev box, disable/remove the SudoVDA devnode before installing pf-vdisplay (and restore it
after). This script does not touch SudoVDA.
.PARAMETER Install
Also add the driver package to the store and create the root devnode.
#>
[CmdletBinding()]
param(
[string]$Stage = 'C:\Users\Public\pfvd-stage',
[string]$Thumbprint = '6A52984E54376C45A1C236B1A2C8A746C5AB6131',
[string]$Nefconc = 'C:\Users\Public\virtual-display-rs\installer\files\nefconc.exe',
[switch]$Install
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$dll = Join-Path $root 'target\x86_64-pc-windows-msvc\debug\pf_vdisplay.dll'
$inx = Join-Path $root 'pf-vdisplay\pf_vdisplay.inx'
if (-not (Test-Path $dll)) { throw "driver not built: $dll (run cargo build in pf-vdisplay/ first)" }
$kits = 'C:\Program Files (x86)\Windows Kits\10\bin'
function Find-Tool([string]$name, [string]$arch) {
(Get-ChildItem "$kits\*\$arch\$name" -EA SilentlyContinue | Sort-Object FullName | Select-Object -Last 1).FullName
}
$signtool = Find-Tool 'signtool.exe' 'x64'
$stampinf = Find-Tool 'stampinf.exe' 'x64'
$inf2cat = Find-Tool 'Inf2Cat.exe' 'x86'
if (-not $signtool) { throw 'signtool.exe not found in the WDK' }
if (-not $stampinf) { throw 'stampinf.exe not found in the WDK' }
if (-not $inf2cat) { throw 'Inf2Cat.exe not found in the WDK' }
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
$stagedDll = Join-Path $Stage 'pf_vdisplay.dll'
$stagedInf = Join-Path $Stage 'pf_vdisplay.inf'
$stagedCat = Join-Path $Stage 'pf_vdisplay.cat'
Copy-Item $dll $stagedDll -Force
Copy-Item $inx $stagedInf -Force # stampinf rewrites this copy in place
# DriverVer date+time — must strictly increase each deploy or pnputil keeps the old binary.
$now = Get-Date
$ver = '1.0.{0}.{1}' -f $now.ToString('MMdd'), $now.ToString('HHmm')
& $signtool sign /fd SHA256 /sha1 $Thumbprint $stagedDll | Out-Null
& $stampinf -f $stagedInf -d '*' -a 'amd64' -u '2.15.0' -v $ver | Out-Null
& $inf2cat /driver:$Stage /os:10_X64 /uselocaltime | Out-Null
& $signtool sign /fd SHA256 /sha1 $Thumbprint $stagedCat | Out-Null
Write-Host "staged + signed pf-vdisplay DriverVer=$ver"
if ($Install) {
& pnputil /add-driver $stagedInf /install | Out-Null
$present = Get-PnpDevice -EA SilentlyContinue |
Where-Object { $_.InstanceId -match 'PF_VDISPLAY' -or $_.FriendlyName -match 'punktfunk Virtual Display' }
if (-not $present) {
if (-not (Test-Path $Nefconc)) { throw "nefconc not found: $Nefconc" }
& $Nefconc --create-device-node --hardware-id 'root\pf_vdisplay' --class-name Display --class-guid '{4d36e968-e325-11ce-bfc1-08002be10318}' | Out-Null
Start-Sleep 2
& pnputil /add-driver $stagedInf /install | Out-Null
}
Write-Host 'installed pf-vdisplay (root\pf_vdisplay devnode)'
}
@@ -1,11 +0,0 @@
[build]
target = "x86_64-pc-windows-msvc"
rustflags = [
"-Zpre-link-arg=/NOLOGO",
"-Zpre-link-arg=/MANIFEST:NO",
"-Zpre-link-arg=/SUBSYSTEM:WINDOWS",
"-Zpre-link-arg=/DYNAMICBASE",
"-Zpre-link-arg=/NXCOMPAT",
"-Clink-arg=/OPT:REF,ICF",
]
@@ -1,30 +0,0 @@
[package]
name = "pf-vdisplay"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wdf-umdf-sys = { path = "../wdf-umdf-sys" }
wdf-umdf = { path = "../wdf-umdf" }
bytemuck = { version = "1.19", features = ["derive"] }
thiserror = "2.0"
anyhow = "1.0"
log = "0.4"
[dependencies.windows]
version = "0.58.0"
features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_Memory",
"Win32_System_Diagnostics_Debug",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
]
@@ -1,5 +0,0 @@
fn main() {
// UMDF includes need the static C runtime linked.
println!("cargo::rustc-link-lib=static=ucrt");
println!("cargo::rerun-if-changed=build.rs");
}
@@ -1,77 +0,0 @@
;/*++
; pf-vdisplay - punktfunk virtual display, UMDF2 IddCx driver INF (template; stampinf -> .inf).
; Adapted from MolotovCherry/virtual-display-rs (MIT) + SudoVDA's control-device security DACL.
;--*/
[Version]
PnpLockdown=1
Signature="$Windows NT$"
ClassGUID={4D36E968-E325-11CE-BFC1-08002BE10318}
Class=Display
ClassVer=2.0
Provider=%ManufacturerName%
CatalogFile=pf_vdisplay.cat
DriverVer=
[Manufacturer]
%ManufacturerName%=Standard,NT$ARCH$
[Standard.NT$ARCH$]
%DeviceName%=pf_vdisplay_Install, Root\pf_vdisplay
[SourceDisksFiles]
pf_vdisplay.dll=1
[SourceDisksNames]
1=%DiskName%
; =================== UMDF IddCx device ====================
[pf_vdisplay_Install.NT]
CopyFiles=UMDriverCopy
[pf_vdisplay_Install.NT.hw]
AddReg=pf_vdisplay_HardwareDeviceSettings
[pf_vdisplay_HardwareDeviceSettings]
HKR, , "UpperFilters", %REG_MULTI_SZ%, "IndirectKmd"
HKR, "WUDF", "DeviceGroupId", %REG_SZ%, "pfVDisplayGroup"
; Let the host (LocalSystem service) + admins open the control device for the ADD/REMOVE/PING IOCTLs.
HKR, , "Security", , "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;WD)"
[pf_vdisplay_Install.NT.Services]
AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall
[pf_vdisplay_Install.NT.Wdf]
UmdfService=pf_vdisplay, pf_vdisplay_Install
UmdfServiceOrder=pf_vdisplay
UmdfKernelModeClientPolicy=AllowKernelModeClients
UmdfHostProcessSharing=ProcessSharingDisabled
[pf_vdisplay_Install]
UmdfLibraryVersion=$UMDFVERSION$
ServiceBinary=%12%\UMDF\pf_vdisplay.dll
UmdfExtensions=IddCx0102
[WUDFRD_ServiceInstall]
DisplayName=%WudfRdDisplayName%
ServiceType=1
StartType=3
ErrorControl=1
ServiceBinary=%12%\WUDFRd.sys
[DestinationDirs]
UMDriverCopy=12,UMDF
[UMDriverCopy]
pf_vdisplay.dll
[Strings]
ManufacturerName="punktfunk"
DiskName="punktfunk Virtual Display Installation Disk"
WudfRdDisplayName="Windows Driver Foundation - User-mode Driver Framework Reflector"
DeviceName="punktfunk Virtual Display"
REG_MULTI_SZ=0x00010000
REG_SZ=0x00000000
REG_EXPAND_SZ=0x00020000
REG_DWORD=0x00010001
@@ -1,532 +0,0 @@
use std::{
mem::{self, MaybeUninit},
ptr::NonNull,
};
use log::{error, info};
use wdf_umdf_sys::{
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1,
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1, __BindgenBitfieldUnit,
DISPLAYCONFIG_2DREGION, DISPLAYCONFIG_RATIONAL, DISPLAYCONFIG_SCANLINE_ORDERING,
DISPLAYCONFIG_TARGET_MODE, DISPLAYCONFIG_VIDEO_SIGNAL_INFO, IDARG_IN_ADAPTER_INIT_FINISHED,
IDARG_IN_COMMITMODES, IDARG_IN_GETDEFAULTDESCRIPTIONMODES, IDARG_IN_PARSEMONITORDESCRIPTION,
IDARG_IN_QUERYTARGETMODES, IDARG_IN_SETSWAPCHAIN, IDARG_OUT_GETDEFAULTDESCRIPTIONMODES,
IDARG_OUT_PARSEMONITORDESCRIPTION, IDARG_OUT_QUERYTARGETMODES, IDDCX_ADAPTER__, IDDCX_PATH,
IDDCX_MONITOR_MODE, IDDCX_MONITOR_MODE_ORIGIN, IDDCX_MONITOR__, IDDCX_TARGET_MODE, NTSTATUS,
WDFDEVICE, WDF_POWER_DEVICE_STATE,
};
// IddCx 1.10 *2 DDIs (HDR-capable). For B1 we advertise SDR (8 bpc) so behaviour is unchanged; B2
// flips the bit depth + adapter flag to enable HDR.
use wdf_umdf_sys::{
IDARG_IN_COMMITMODES2, IDARG_IN_PARSEMONITORDESCRIPTION2, IDARG_IN_QUERYTARGETMODES2,
IDARG_IN_QUERYTARGET_INFO, IDARG_OUT_QUERYTARGET_INFO, IDDCX_BITS_PER_COMPONENT, IDDCX_MONITOR_MODE2,
IDDCX_PATH2, IDDCX_TARGET_CAPS, IDDCX_TARGET_MODE2, IDDCX_WIRE_BITS_PER_COMPONENT,
};
use crate::{
context::{DeviceContext, MonitorContext},
edid::Edid,
monitor::{AdapterObject, FlattenModes, ADAPTER, MONITOR_MODES},
};
pub extern "C-unwind" fn adapter_init_finished(
adapter_object: *mut IDDCX_ADAPTER__,
_p_in_args: *const IDARG_IN_ADAPTER_INIT_FINISHED,
) -> NTSTATUS {
let Some(adapter_ptr) = NonNull::new(adapter_object) else {
error!("Adapter ptr was null");
return NTSTATUS::STATUS_INVALID_ADDRESS;
};
// store adapter object for the control plane to use
if ADAPTER.set(AdapterObject(adapter_ptr)).is_err() {
error!("Failed to set adapter");
return NTSTATUS::STATUS_ADAPTER_HARDWARE_ERROR;
}
DeviceContext::finish_init();
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn device_d0_entry(
device: WDFDEVICE,
_previous_state: WDF_POWER_DEVICE_STATE,
) -> NTSTATUS {
let status: NTSTATUS = unsafe {
DeviceContext::get_mut(device.cast(), |context| {
if let Err(e) = context.init_adapter() {
error!("Failed to init adapter: {e:?}");
}
})
.into()
};
if !status.is_success() {
return status;
}
NTSTATUS::STATUS_SUCCESS
}
fn display_info(width: u32, height: u32, refresh_rate: u32) -> DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
let clock_rate = refresh_rate * (height + 4) * (height + 4) + 1000;
DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
pixelRate: u64::from(clock_rate),
hSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: clock_rate,
Denominator: height + 4,
},
vSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: clock_rate,
Denominator: (height + 4) * (height + 4),
},
activeSize: DISPLAYCONFIG_2DREGION {
cx: width,
cy: height,
},
totalSize: DISPLAYCONFIG_2DREGION {
cx: width + 4,
cy: height + 4,
},
__bindgen_anon_1: DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1 {
AdditionalSignalInfo: unsafe {
mem::transmute::<
__BindgenBitfieldUnit<[u8; 4]>,
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1,
>(
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1::new_bitfield_1(
255, 0, 0,
),
)
},
},
scanLineOrdering:
DISPLAYCONFIG_SCANLINE_ORDERING::DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
}
}
pub extern "C-unwind" fn parse_monitor_description(
p_in_args: *const IDARG_IN_PARSEMONITORDESCRIPTION,
p_out_args: *mut IDARG_OUT_PARSEMONITORDESCRIPTION,
) -> NTSTATUS {
let in_args = unsafe { &*p_in_args };
let out_args = unsafe { &mut *p_out_args };
let Ok(monitors) = MONITOR_MODES.lock() else {
error!("MONITOR_MODES mutex poisoned");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let edid = unsafe {
std::slice::from_raw_parts(
in_args.MonitorDescription.pData as *const u8,
in_args.MonitorDescription.DataSize as usize,
)
};
let monitor_index = Edid::get_serial(edid);
let Ok(monitor_index) = monitor_index else {
error!(
"We got an edid {} bytes long, but this is incorrect",
edid.len()
);
return NTSTATUS::STATUS_INVALID_VIEW_SIZE;
};
let Some(monitor) = monitors.iter().find(|&m| m.data.id == monitor_index) else {
error!("Failed to find monitor id {monitor_index}");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let number_of_modes: u32 = monitor
.data
.modes
.iter()
.map(|m| u32::try_from(m.refresh_rates.len()).expect("Cannot use > u32::MAX refresh rates"))
.sum();
out_args.MonitorModeBufferOutputCount = number_of_modes;
if in_args.MonitorModeBufferInputCount < number_of_modes {
// Return success if there was no buffer, since the caller was only asking for a count of modes
return if in_args.MonitorModeBufferInputCount > 0 {
NTSTATUS::STATUS_BUFFER_TOO_SMALL
} else {
NTSTATUS::STATUS_SUCCESS
};
}
let monitor_modes = unsafe {
std::slice::from_raw_parts_mut(
in_args
.pMonitorModes
.cast::<MaybeUninit<IDDCX_MONITOR_MODE>>(),
number_of_modes as usize,
)
};
for (mode, out_mode) in monitor.data.modes.flatten().zip(monitor_modes.iter_mut()) {
out_mode.write(IDDCX_MONITOR_MODE {
#[allow(clippy::cast_possible_truncation)]
Size: mem::size_of::<IDDCX_MONITOR_MODE>() as u32,
Origin: IDDCX_MONITOR_MODE_ORIGIN::IDDCX_MONITOR_MODE_ORIGIN_MONITORDESCRIPTOR,
MonitorVideoSignalInfo: display_info(mode.width, mode.height, mode.refresh_rate),
});
}
// Set the preferred mode as represented in the EDID
out_args.PreferredMonitorModeIdx = 0;
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn monitor_get_default_modes(
_monitor_object: *mut IDDCX_MONITOR__,
_p_in_args: *const IDARG_IN_GETDEFAULTDESCRIPTIONMODES,
_p_out_args: *mut IDARG_OUT_GETDEFAULTDESCRIPTIONMODES,
) -> NTSTATUS {
info!("GET_DEFAULT_MODES called (we return NOT_IMPLEMENTED — only valid for a monitor with NO EDID)");
NTSTATUS::STATUS_NOT_IMPLEMENTED
}
pub fn target_mode(width: u32, height: u32, refresh_rate: u32) -> IDDCX_TARGET_MODE {
let total_size = DISPLAYCONFIG_2DREGION {
cx: width,
cy: height,
};
IDDCX_TARGET_MODE {
#[allow(clippy::cast_possible_truncation)]
Size: mem::size_of::<IDDCX_TARGET_MODE>() as u32,
TargetVideoSignalInfo: DISPLAYCONFIG_TARGET_MODE {
targetVideoSignalInfo: DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
pixelRate: u64::from(refresh_rate) * u64::from(width) * u64::from(height),
hSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: refresh_rate * height,
Denominator: 1,
},
vSyncFreq: DISPLAYCONFIG_RATIONAL {
Numerator: refresh_rate,
Denominator: 1,
},
totalSize: total_size,
activeSize: total_size,
scanLineOrdering:
DISPLAYCONFIG_SCANLINE_ORDERING::DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
__bindgen_anon_1: DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1 {
AdditionalSignalInfo: unsafe {
mem::transmute::<__BindgenBitfieldUnit<[u8; 4]>, DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1>(
DISPLAYCONFIG_VIDEO_SIGNAL_INFO__bindgen_ty_1__bindgen_ty_1::new_bitfield_1(
255, 1, 0,
),
)
},
},
},
},
..Default::default()
}
}
pub extern "C-unwind" fn monitor_query_modes(
monitor_object: *mut IDDCX_MONITOR__,
p_in_args: *const IDARG_IN_QUERYTARGETMODES,
p_out_args: *mut IDARG_OUT_QUERYTARGETMODES,
) -> NTSTATUS {
// find out which monitor this belongs too
let Ok(monitors) = MONITOR_MODES.lock() else {
error!("MONITOR_MODES mutex poisoned");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
// we have stored the monitor object per id, so we should be able to compare pointers
let Some(monitor) = monitors
.iter()
.find(|&m| m.object.is_some_and(|p| p.as_ptr() == monitor_object))
else {
error!("Failed to find monitor object in cache for {monitor_object:?}");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let number_of_modes = monitor
.data
.modes
.iter()
.map(|m| u32::try_from(m.refresh_rates.len()).expect("Cannot use > u32::MAX modes"))
.sum();
// Create a set of modes supported for frame processing and scan-out. These are typically not based on the
// monitor's descriptor and instead are based on the static processing capability of the device. The OS will
// report the available set of modes for a given output as the intersection of monitor modes with target modes.
let out_args = unsafe { &mut *p_out_args };
out_args.TargetModeBufferOutputCount = number_of_modes;
let in_args = unsafe { &*p_in_args };
if in_args.TargetModeBufferInputCount >= number_of_modes {
let out_target_modes = unsafe {
std::slice::from_raw_parts_mut(
in_args
.pTargetModes
.cast::<MaybeUninit<IDDCX_TARGET_MODE>>(),
number_of_modes as usize,
)
};
for (mode, out_target) in monitor
.data
.modes
.flatten()
.zip(out_target_modes.iter_mut())
{
let target_mode = target_mode(mode.width, mode.height, mode.refresh_rate);
out_target.write(target_mode);
}
}
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn adapter_commit_modes(
_adapter_object: *mut IDDCX_ADAPTER__,
p_in_args: *const IDARG_IN_COMMITMODES,
) -> NTSTATUS {
// DIAGNOSTIC: does the OS commit an ACTIVE path for our monitor? IDDCX_PATH_FLAGS_ACTIVE = 2. If
// no active path is ever committed, the OS never calls ASSIGN_SWAPCHAIN (the bug we're chasing).
let in_args = unsafe { &*p_in_args };
info!("COMMIT_MODES: path_count={}", in_args.PathCount);
for i in 0..in_args.PathCount {
let path: &IDDCX_PATH = unsafe { &*in_args.pPaths.add(i as usize) };
let active = (path.Flags.0 & 2) != 0;
info!(
" path[{i}] monitor={:p} flags=0x{:x} active={active}",
path.MonitorObject, path.Flags.0
);
}
NTSTATUS::STATUS_SUCCESS
}
pub extern "C-unwind" fn assign_swap_chain(
monitor_object: *mut IDDCX_MONITOR__,
p_in_args: *const IDARG_IN_SETSWAPCHAIN,
) -> NTSTATUS {
let p_in_args = unsafe { &*p_in_args };
unsafe {
MonitorContext::get_mut(monitor_object.cast(), |context| {
context.assign_swap_chain(
p_in_args.hSwapChain,
p_in_args.RenderAdapterLuid,
p_in_args.hNextSurfaceAvailable,
);
})
.into()
}
}
pub extern "C-unwind" fn unassign_swap_chain(monitor_object: *mut IDDCX_MONITOR__) -> NTSTATUS {
info!("swap-chain unassigned (monitor inactive)");
unsafe {
MonitorContext::get_mut(monitor_object.cast(), |context| {
context.unassign_swap_chain();
})
.into()
}
}
// ===== IddCx 1.10 *2 DDIs (HDR-capable path) ============================================
// These mirror the 1.x callbacks above but advertise per-mode wire bit-depth. B1 reports SDR (8 bpc);
// B2 bumps `wire_bits()` to add 10 bpc + sets CAN_PROCESS_FP16 to actually enable HDR.
/// Wire bit-depth advertised per mode. B2: advertise BOTH 8 and 10 bpc RGB so the OS offers HDR10
/// modes (the bitfield: 8 = 0x2, 10 = 0x4).
fn wire_bits() -> IDDCX_WIRE_BITS_PER_COMPONENT {
let rgb = IDDCX_BITS_PER_COMPONENT(
IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_8.0
| IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_10.0,
);
IDDCX_WIRE_BITS_PER_COMPONENT {
Rgb: rgb,
YCbCr444: IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE,
YCbCr422: IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE,
YCbCr420: IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE,
}
}
/// 1.10 variant of [`parse_monitor_description`] — writes `IDDCX_MONITOR_MODE2` (adds bit-depth).
pub extern "C-unwind" fn parse_monitor_description2(
p_in_args: *const IDARG_IN_PARSEMONITORDESCRIPTION2,
p_out_args: *mut IDARG_OUT_PARSEMONITORDESCRIPTION,
) -> NTSTATUS {
let in_args = unsafe { &*p_in_args };
let out_args = unsafe { &mut *p_out_args };
let Ok(monitors) = MONITOR_MODES.lock() else {
error!("MONITOR_MODES mutex poisoned");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let edid = unsafe {
std::slice::from_raw_parts(
in_args.MonitorDescription.pData as *const u8,
in_args.MonitorDescription.DataSize as usize,
)
};
let Ok(monitor_index) = Edid::get_serial(edid) else {
error!("bad edid ({} bytes)", edid.len());
return NTSTATUS::STATUS_INVALID_VIEW_SIZE;
};
let Some(monitor) = monitors.iter().find(|&m| m.data.id == monitor_index) else {
error!("Failed to find monitor id {monitor_index}");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let number_of_modes: u32 = monitor
.data
.modes
.iter()
.map(|m| u32::try_from(m.refresh_rates.len()).expect("Cannot use > u32::MAX refresh rates"))
.sum();
out_args.MonitorModeBufferOutputCount = number_of_modes;
if in_args.MonitorModeBufferInputCount < number_of_modes {
return if in_args.MonitorModeBufferInputCount > 0 {
NTSTATUS::STATUS_BUFFER_TOO_SMALL
} else {
NTSTATUS::STATUS_SUCCESS
};
}
let monitor_modes = unsafe {
std::slice::from_raw_parts_mut(
in_args.pMonitorModes.cast::<MaybeUninit<IDDCX_MONITOR_MODE2>>(),
number_of_modes as usize,
)
};
for (mode, out_mode) in monitor.data.modes.flatten().zip(monitor_modes.iter_mut()) {
out_mode.write(IDDCX_MONITOR_MODE2 {
#[allow(clippy::cast_possible_truncation)]
Size: mem::size_of::<IDDCX_MONITOR_MODE2>() as u32,
Origin: IDDCX_MONITOR_MODE_ORIGIN::IDDCX_MONITOR_MODE_ORIGIN_MONITORDESCRIPTOR,
MonitorVideoSignalInfo: display_info(mode.width, mode.height, mode.refresh_rate),
BitsPerComponent: wire_bits(),
});
}
out_args.PreferredMonitorModeIdx = 0;
NTSTATUS::STATUS_SUCCESS
}
fn target_mode2(width: u32, height: u32, refresh_rate: u32) -> IDDCX_TARGET_MODE2 {
let m1 = target_mode(width, height, refresh_rate);
IDDCX_TARGET_MODE2 {
#[allow(clippy::cast_possible_truncation)]
Size: mem::size_of::<IDDCX_TARGET_MODE2>() as u32,
TargetVideoSignalInfo: m1.TargetVideoSignalInfo,
BitsPerComponent: wire_bits(),
..Default::default()
}
}
/// 1.10 variant of [`monitor_query_modes`] — writes `IDDCX_TARGET_MODE2`.
pub extern "C-unwind" fn monitor_query_modes2(
monitor_object: *mut IDDCX_MONITOR__,
p_in_args: *const IDARG_IN_QUERYTARGETMODES2,
p_out_args: *mut IDARG_OUT_QUERYTARGETMODES,
) -> NTSTATUS {
let Ok(monitors) = MONITOR_MODES.lock() else {
error!("MONITOR_MODES mutex poisoned");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let Some(monitor) = monitors
.iter()
.find(|&m| m.object.is_some_and(|p| p.as_ptr() == monitor_object))
else {
error!("Failed to find monitor object in cache for {monitor_object:?}");
return NTSTATUS::STATUS_DRIVER_INTERNAL_ERROR;
};
let number_of_modes = monitor
.data
.modes
.iter()
.map(|m| u32::try_from(m.refresh_rates.len()).expect("Cannot use > u32::MAX modes"))
.sum();
let out_args = unsafe { &mut *p_out_args };
out_args.TargetModeBufferOutputCount = number_of_modes;
let in_args = unsafe { &*p_in_args };
if in_args.TargetModeBufferInputCount >= number_of_modes {
let out_target_modes = unsafe {
std::slice::from_raw_parts_mut(
in_args.pTargetModes.cast::<MaybeUninit<IDDCX_TARGET_MODE2>>(),
number_of_modes as usize,
)
};
for (mode, out_target) in monitor.data.modes.flatten().zip(out_target_modes.iter_mut()) {
out_target.write(target_mode2(mode.width, mode.height, mode.refresh_rate));
}
}
NTSTATUS::STATUS_SUCCESS
}
/// 1.10 variant of [`adapter_commit_modes`] — `IDDCX_PATH2` carries the committed wire format.
pub extern "C-unwind" fn adapter_commit_modes2(
_adapter_object: *mut IDDCX_ADAPTER__,
p_in_args: *const IDARG_IN_COMMITMODES2,
) -> NTSTATUS {
let in_args = unsafe { &*p_in_args };
info!("COMMIT_MODES2: path_count={}", in_args.PathCount);
for i in 0..in_args.PathCount {
let path: &IDDCX_PATH2 = unsafe { &*in_args.pPaths.add(i as usize) };
let active = (path.Flags.0 & 2) != 0;
info!(
" path2[{i}] monitor={:p} flags=0x{:x} active={active} colorspace={} rgb_bpc=0x{:x}",
path.MonitorObject,
path.Flags.0,
path.WireFormatInfo.ColorSpace.0,
path.WireFormatInfo.BitsPerComponent.Rgb.0
);
}
NTSTATUS::STATUS_SUCCESS
}
/// 1.10 NEW: per-target capabilities. B2 reports `HIGH_COLOR_SPACE` so the OS enables HDR10 (transfer
/// curve + wide gamut) on this target.
pub extern "C-unwind" fn query_target_info(
_adapter_object: *mut IDDCX_ADAPTER__,
_p_in_args: *mut IDARG_IN_QUERYTARGET_INFO,
p_out_args: *mut IDARG_OUT_QUERYTARGET_INFO,
) -> NTSTATUS {
let out_args = unsafe { &mut *p_out_args };
out_args.TargetCaps = IDDCX_TARGET_CAPS::IDDCX_TARGET_CAPS_HIGH_COLOR_SPACE;
out_args.DitheringSupport = IDDCX_WIRE_BITS_PER_COMPONENT::default();
NTSTATUS::STATUS_SUCCESS
}
/// 1.10 NEW (HDR): the OS hands us the default HDR10 static metadata for the monitor. B2 accepts it
/// (the host/client own the final HDR metadata for the stream); B3 will forward it to the host for the
/// HEVC mastering-display SEI. Stub keeps the OS's HDR setup happy.
pub extern "C-unwind" fn set_default_hdr_metadata(
_monitor_object: *mut IDDCX_MONITOR__,
_p_in_args: *const wdf_umdf_sys::IDARG_IN_MONITOR_SET_DEFAULT_HDR_METADATA,
) -> NTSTATUS {
NTSTATUS::STATUS_SUCCESS
}
/// 1.10 HDR: the OS hands us the gamma ramp (a 3x4 colour-space matrix in HDR mode). We do NOT apply it
/// server-side — the host streams the scRGB FP16 and the CLIENT's display applies its own transform —
/// so we accept it. Wiring this is OBLIGATED once CAN_PROCESS_FP16 is set; without it the OS rejects
/// the adapter at init (`IddCxAdapterInitAsync` → "Failed to get adapter").
pub extern "C-unwind" fn set_gamma_ramp(
_monitor_object: *mut IDDCX_MONITOR__,
_p_in_args: *const wdf_umdf_sys::IDARG_IN_SET_GAMMARAMP,
) -> NTSTATUS {
NTSTATUS::STATUS_SUCCESS
}
@@ -1,401 +0,0 @@
use std::{
mem::{self, size_of},
num::{ParseIntError, TryFromIntError},
ptr::{addr_of_mut, NonNull},
sync::{Arc, Mutex},
};
use anyhow::anyhow;
use log::{error, info, warn};
use wdf_umdf::{
IddCxAdapterInitAsync, IddCxError, IddCxMonitorArrival, IddCxMonitorCreate,
IddCxMonitorSetupHardwareCursor, WdfError, WdfObjectDelete, WDF_DECLARE_CONTEXT_TYPE,
};
use wdf_umdf_sys::{
DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY, HANDLE, IDARG_IN_ADAPTER_INIT, IDARG_IN_MONITORCREATE,
IDARG_IN_SETUP_HWCURSOR, IDARG_OUT_ADAPTER_INIT, IDARG_OUT_MONITORARRIVAL,
IDARG_OUT_MONITORCREATE, IDDCX_ADAPTER, IDDCX_ADAPTER_CAPS, IDDCX_ADAPTER_FLAGS, IDDCX_CURSOR_CAPS,
IDDCX_ENDPOINT_DIAGNOSTIC_INFO, IDDCX_ENDPOINT_VERSION, IDDCX_FEATURE_IMPLEMENTATION,
IDDCX_MONITOR, IDDCX_MONITOR_DESCRIPTION, IDDCX_MONITOR_DESCRIPTION_TYPE, IDDCX_MONITOR_INFO,
IDDCX_SWAPCHAIN, IDDCX_TRANSMISSION_TYPE, IDDCX_XOR_CURSOR_SUPPORT, LUID, NTSTATUS, WDFDEVICE,
WDFOBJECT, WDF_OBJECT_ATTRIBUTES,
};
use windows::{
core::{s, w, GUID},
Win32::{Foundation::TRUE, System::Threading::CreateEventA},
};
use crate::{
direct_3d_device::Direct3DDevice,
edid::Edid,
monitor::MONITOR_MODES,
swap_chain_processor::SwapChainProcessor,
};
// Maximum amount of monitors that can be connected
pub const MAX_MONITORS: u8 = 16;
/// ONE shared D3D render device, reused across every swap-chain assignment (keyed by render LUID).
/// Creating a fresh `Direct3DDevice` per assign — and the swap-chain flap fires several assigns per
/// session — spawned a new NVIDIA UMD worker-thread set each time that was NEVER reclaimed on release
/// (proven on the RTX box: ~70 `nvwgf2umx` threads + ~50 MB VRAM leaked per reconnect, permanently,
/// even though our `Direct3DDevice` refcount dropped to 0). Pooling one device keeps a single, stable
/// thread set: the processors borrow an `Arc`, so the device outlives them and is never re-created.
static DEVICE_POOL: Mutex<Option<(i64, Arc<Direct3DDevice>)>> = Mutex::new(None);
/// Get-or-create the pooled D3D device for `luid`. Re-creates only if the render adapter changes
/// (e.g. a GPU hot-swap), which drops the old `Arc` once its last processor releases it.
fn pooled_device(luid: windows::Win32::Foundation::LUID) -> Option<Arc<Direct3DDevice>> {
let key = (i64::from(luid.HighPart) << 32) | i64::from(luid.LowPart as u32);
let mut pool = DEVICE_POOL.lock().ok()?;
if let Some((k, dev)) = pool.as_ref() {
if *k == key {
return Some(dev.clone());
}
}
match Direct3DDevice::init(luid) {
Ok(d) => {
let a = Arc::new(d);
*pool = Some((key, a.clone()));
Some(a)
}
Err(e) => {
error!("pooled Direct3DDevice::init failed: {e:?}");
None
}
}
}
pub struct DeviceContext {
device: WDFDEVICE,
adapter: Option<IDDCX_ADAPTER>,
}
// SAFETY: Raw ptr is managed by external library
unsafe impl Send for DeviceContext {}
unsafe impl Sync for DeviceContext {}
// for now, `device` is hardcoded into the macro, so it needs to be there even if unused
#[allow(unused)]
pub struct MonitorContext {
device: IDDCX_MONITOR,
swap_chain_processor: Option<SwapChainProcessor>,
/// OS target id (from IddCxMonitorArrival), stamped on this context at creation. assign_swap_chain
/// uses THIS instead of a MONITOR_MODES pointer lookup — the lookup returns 0 for a recreated
/// (session-2+) monitor, which broke the shared-ring naming and cascaded into SetDevice
/// E_INVALIDARG + an access violation (the fix-teardown crash).
target_id: u32,
}
// SAFETY: Raw ptr is managed by external library
unsafe impl Send for MonitorContext {}
unsafe impl Sync for MonitorContext {}
WDF_DECLARE_CONTEXT_TYPE!(pub DeviceContext);
WDF_DECLARE_CONTEXT_TYPE!(pub MonitorContext);
#[derive(Debug, thiserror::Error)]
pub enum ContextError {
#[error("Failed to parse integer: {0:?}")]
ParseInt(#[from] ParseIntError),
#[error("Failed to convert integer: {0:?}")]
TryFromInt(#[from] TryFromIntError),
#[error("Failed to convert to NTSTATUS: {0:?}")]
Ntstatus(#[from] NTSTATUS),
#[error("Failed to convert to IddCxError: {0:?}")]
IddCx(#[from] IddCxError),
#[error("Failed to convert to WdfError: {0:?}")]
Wdf(#[from] WdfError),
#[error("Windows Error: {0:?}")]
Win(#[from] windows::core::Error),
#[error("{0:?}")]
Other(#[from] anyhow::Error),
}
impl DeviceContext {
pub fn new(device: WDFDEVICE) -> Self {
Self {
device,
adapter: None,
}
}
pub fn init_adapter(&mut self) -> Result<(), ContextError> {
let mut version = IDDCX_ENDPOINT_VERSION {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_ENDPOINT_VERSION>() as u32,
MajorVer: env!("CARGO_PKG_VERSION_MAJOR").parse::<u32>()?,
MinorVer: env!("CARGO_PKG_VERSION_MINOR").parse::<u32>()?,
Build: env!("CARGO_PKG_VERSION_PATCH").parse::<u32>()?,
..Default::default()
};
let mut adapter_caps = IDDCX_ADAPTER_CAPS {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_ADAPTER_CAPS>() as u32,
// B2 HDR: declare we can process FP16 (scRGB) desktop surfaces — enables HDR10 / SDR WCG.
// This OBLIGATES the *2 mode DDIs (done) + ReleaseAndAcquireBuffer2 (done in run_core).
Flags: IDDCX_ADAPTER_FLAGS::IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16,
MaxMonitorsSupported: u32::from(MAX_MONITORS),
EndPointDiagnostics: IDDCX_ENDPOINT_DIAGNOSTIC_INFO {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_ENDPOINT_DIAGNOSTIC_INFO>() as u32,
GammaSupport: IDDCX_FEATURE_IMPLEMENTATION::IDDCX_FEATURE_IMPLEMENTATION_NONE,
TransmissionType: IDDCX_TRANSMISSION_TYPE::IDDCX_TRANSMISSION_TYPE_WIRED_OTHER,
pEndPointFriendlyName: w!("punktfunk Virtual Display Adapter").as_ptr(),
pEndPointManufacturerName: w!("punktfunk").as_ptr(),
pEndPointModelName: w!("Virtual Display").as_ptr(),
pFirmwareVersion: addr_of_mut!(version).cast(),
pHardwareVersion: addr_of_mut!(version).cast(),
},
..Default::default()
};
let mut attr = WDF_OBJECT_ATTRIBUTES::init_context_type(unsafe { Self::get_type_info() });
let adapter_init = IDARG_IN_ADAPTER_INIT {
// this is WdfDevice because that's what we set last
WdfDevice: self.device,
pCaps: addr_of_mut!(adapter_caps).cast(),
ObjectAttributes: addr_of_mut!(attr).cast(),
};
let mut adapter_init_out = IDARG_OUT_ADAPTER_INIT::default();
unsafe { IddCxAdapterInitAsync(&adapter_init, &mut adapter_init_out)? };
self.adapter = Some(adapter_init_out.AdapterObject);
unsafe { self.clone_into(adapter_init_out.AdapterObject as WDFOBJECT)? };
Ok(())
}
pub fn finish_init() -> NTSTATUS {
// Monitors are created on demand by the IOCTL control plane (control::do_add). Start the
// watchdog so a crashed/gone host never leaves a phantom display.
crate::control::start_watchdog();
NTSTATUS::STATUS_SUCCESS
}
pub fn create_monitor(&mut self, index: u32) -> Result<(), ContextError> {
let mut attr =
WDF_OBJECT_ATTRIBUTES::init_context_type(unsafe { MonitorContext::get_type_info() });
// use the edid serial number to represent the monitor index for later identification
let mut edid = Edid::generate_with(index);
let mut monitor_info = IDDCX_MONITOR_INFO {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_MONITOR_INFO>() as u32,
// SAFETY: windows-rs + generated _GUID types are same size, with same fields, and repr C
// see: https://microsoft.github.io/windows-docs-rs/doc/windows/core/struct.GUID.html
// and: wmdf_umdf_sys::_GUID
MonitorContainerId: unsafe {
mem::transmute::<GUID, wdf_umdf_sys::_GUID>(GUID::new()?)
},
MonitorType:
DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY::DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI,
ConnectorIndex: index,
MonitorDescription: IDDCX_MONITOR_DESCRIPTION {
#[allow(clippy::cast_possible_truncation)]
Size: size_of::<IDDCX_MONITOR_DESCRIPTION>() as u32,
Type: IDDCX_MONITOR_DESCRIPTION_TYPE::IDDCX_MONITOR_DESCRIPTION_TYPE_EDID,
#[allow(clippy::cast_possible_truncation)]
DataSize: edid.len() as u32,
pData: edid.as_mut_ptr().cast(),
},
};
let monitor_create = IDARG_IN_MONITORCREATE {
ObjectAttributes: &mut attr,
pMonitorInfo: &mut monitor_info,
};
let mut monitor_create_out = IDARG_OUT_MONITORCREATE::default();
unsafe {
IddCxMonitorCreate(
self.adapter.ok_or(anyhow!("Failed to get adapter"))?,
&monitor_create,
&mut monitor_create_out,
)?
};
// store monitor object for later
{
let mut lock = MONITOR_MODES
.lock()
.map_err(|_| anyhow!("Failed to lock mutex"))?;
for monitor in &mut *lock {
if monitor.data.id == index {
monitor.object = Some(
NonNull::new(monitor_create_out.MonitorObject)
.ok_or(anyhow!("MonitorObject was null"))?,
);
}
}
}
unsafe {
let context = MonitorContext::new(monitor_create_out.MonitorObject);
context.init(monitor_create_out.MonitorObject as WDFOBJECT)?;
}
// tell os monitor is plugged in
let mut arrival_out = IDARG_OUT_MONITORARRIVAL::default();
unsafe {
IddCxMonitorArrival(monitor_create_out.MonitorObject, &mut arrival_out)?;
}
// Record the OS target id + render-adapter LUID for the ADD IOCTL reply.
{
let mut lock = MONITOR_MODES
.lock()
.map_err(|_| anyhow!("Failed to lock mutex"))?;
if let Some(mon) = lock.iter_mut().find(|m| m.data.id == index) {
mon.target_id = arrival_out.OsTargetId;
mon.adapter_luid_low = arrival_out.OsAdapterLuid.LowPart;
mon.adapter_luid_high = arrival_out.OsAdapterLuid.HighPart;
}
}
// Stamp the OS target id onto the monitor's CONTEXT so assign_swap_chain reads it directly
// (no MONITOR_MODES pointer lookup, which returns 0 for a recreated monitor).
unsafe {
let _ = MonitorContext::get_mut(monitor_create_out.MonitorObject.cast(), |ctx| {
ctx.target_id = arrival_out.OsTargetId;
});
}
Ok(())
}
}
impl MonitorContext {
pub fn new(device: IDDCX_MONITOR) -> Self {
Self {
device,
swap_chain_processor: None,
target_id: 0,
}
}
pub fn assign_swap_chain(
&mut self,
swap_chain: IDDCX_SWAPCHAIN,
render_adapter: LUID,
new_frame_event: HANDLE,
) {
// drop processing thread
drop(self.swap_chain_processor.take());
// transmute would work, but one less unsafe block, so why not
let luid = windows::Win32::Foundation::LUID {
LowPart: render_adapter.LowPart,
HighPart: render_adapter.HighPart,
};
// Log which GPU the OS picked to render this virtual monitor (useful on a hybrid iGPU+dGPU box,
// where the render adapter determines which adapter the host's capture must enumerate).
info!(
"swap-chain assigned: OS render adapter LUID = {:08x}:{:08x}",
render_adapter.HighPart, render_adapter.LowPart
);
// The OS target id keys the per-monitor shared frame-push objects (header/event/textures) the
// host opens. Read it from THIS context (stamped at creation after IddCxMonitorArrival) — the
// old MONITOR_MODES pointer lookup returned 0 for a recreated (session-2+) monitor, which broke
// the ring naming and cascaded into SetDevice E_INVALIDARG + an access violation.
let target_id = self.target_id;
let device = pooled_device(luid);
if let Some(device) = device {
let mut processor = SwapChainProcessor::new();
processor.run(
swap_chain,
device,
new_frame_event,
target_id,
render_adapter.LowPart,
render_adapter.HighPart,
);
self.swap_chain_processor = Some(processor);
// Cursor is BAKED into the captured video: for IDD-push we deliberately do NOT advertise a
// hardware cursor, so DWM software-composites the mouse cursor into the swapchain surface we
// capture — the client then sees the cursor in the stream. (A future separate-plane cursor
// would re-enable setup_hw_cursor + IddCxMonitorQueryHardwareCursor.) Not advertising one
// also stops leaking a CreateEventA handle per assign.
} else {
// It's important to delete the swap-chain if D3D init fails, so the OS generates a fresh
// swap-chain and retries.
error!("pooled Direct3DDevice unavailable for render LUID — deleting swap chain for OS retry");
unsafe {
let _ = WdfObjectDelete(swap_chain.cast());
}
}
}
pub fn unassign_swap_chain(&mut self) {
let had = self.swap_chain_processor.take().is_some();
error!("unassign_swap_chain (target={}) — dropped live processor: {had}", self.target_id);
}
/// Advertise a HARDWARE cursor. NOT called for IDD-push — we bake the cursor into the video
/// instead (see `assign_swap_chain`). Kept for a future separate-plane cursor (which would pair it
/// with `IddCxMonitorQueryHardwareCursor`). Leaks a `CreateEventA` handle per call, so only wire it
/// back up alongside a real cursor-plane consumer.
#[allow(dead_code)]
pub fn setup_hw_cursor(&mut self) {
let mouse_event = unsafe { CreateEventA(None, false, false, s!("vdd_mouse_event")) };
let Ok(mouse_event) = mouse_event else {
error!("CreateEventA failed: {mouse_event:?}");
return;
};
// setup hardware cursor
let cursor_info = IDDCX_CURSOR_CAPS {
#[allow(clippy::cast_possible_truncation)]
Size: std::mem::size_of::<IDDCX_CURSOR_CAPS>() as u32,
AlphaCursorSupport: TRUE.0,
MaxX: 512,
MaxY: 512,
ColorXorCursorSupport: IDDCX_XOR_CURSOR_SUPPORT::IDDCX_XOR_CURSOR_SUPPORT_NONE,
};
let hw_cursor = IDARG_IN_SETUP_HWCURSOR {
CursorInfo: cursor_info,
hNewCursorDataAvailable: mouse_event.0,
};
let res = unsafe { IddCxMonitorSetupHardwareCursor(self.device, &hw_cursor) };
let Ok(res) = res else {
error!("IddCxMonitorSetupHardwareCursor() failed: {res:?}");
return;
};
if res.is_warning() {
warn!("IddCxMonitorSetupHardwareCursor() warn: {res:?}");
}
if res.is_error() {
error!("IddCxMonitorSetupHardwareCursor() failed: {res:?}");
}
}
}
@@ -1,413 +0,0 @@
//! SudoVDA-compatible IOCTL control plane (`EVT_IDD_CX_DEVICE_IO_CONTROL`). The host's
//! `vdisplay/sudovda.rs` drives this unchanged: ADD a monitor at a requested mode → `{LUID, target_id}`,
//! REMOVE by GUID, PING the watchdog, GET_VERSION/GET_WATCHDOG, SET_RENDER_ADAPTER. Struct layouts are
//! byte-identical to `Common/Include/sudovda-ioctl.h`.
use std::ffi::c_void;
use std::mem::size_of;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::thread;
use std::time::{Duration, Instant};
use log::{error, info};
use wdf_umdf::{
IddCxAdapterSetRenderAdapter, IddCxMonitorDeparture, WdfRequestCompleteWithInformation,
WdfRequestRetrieveInputBuffer, WdfRequestRetrieveOutputBuffer,
};
use wdf_umdf_sys::{IDARG_IN_ADAPTERSETRENDERADAPTER, LUID, NTSTATUS, WDFDEVICE, WDFREQUEST};
use crate::context::{DeviceContext, MonitorContext};
use crate::monitor::{
default_modes, Mode, MonitorData, MonitorObject, ADAPTER, MONITOR_MODES, NEXT_ID,
PREFERRED_RENDER_ADAPTER, PROTOCOL_VERSION, WATCHDOG_COUNTDOWN, WATCHDOG_TIMEOUT,
};
// CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, func, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0).
const fn ctl(func: u32) -> u32 {
(0x22u32 << 16) | (func << 2)
}
const IOCTL_ADD: u32 = ctl(0x800);
const IOCTL_REMOVE: u32 = ctl(0x801);
const IOCTL_SET_RENDER_ADAPTER: u32 = ctl(0x802);
const IOCTL_GET_WATCHDOG: u32 = ctl(0x803);
/// pf-vdisplay extension (NOT in SudoVDA): tear down every monitor. The host issues this on startup to
/// reap monitors orphaned by a crashed/killed previous host instance. SudoVDA returns invalid for it
/// (harmlessly ignored), so the host can send it unconditionally.
const IOCTL_CLEAR_ALL: u32 = ctl(0x804);
const IOCTL_PING: u32 = ctl(0x888);
const IOCTL_GET_VERSION: u32 = ctl(0x8FF);
/// Serializes monitor lifecycle ops — ADD / REMOVE / watchdog-teardown — against each other. Without
/// it, a watchdog expiry can drain an entry out from under an in-flight `do_add` (which releases the
/// `MONITOR_MODES` lock before the slow `create_monitor`), leaving `do_add` to return
/// `STATUS_UNSUCCESSFUL` → the host sees `ERROR_GEN_FAILURE`. This was the reconnect-churn fault.
static MONITOR_OP_LOCK: Mutex<()> = Mutex::new(());
/// A monitor created less than this ago is still in its host-side setup window (CCD commit + GDI-name
/// resolve + topology settle, ~5 s) and is never reaped by the watchdog — only by an explicit
/// CLEAR_ALL. Protects a freshly-born monitor from a transient PING gap during reconnect churn.
const MONITOR_GRACE: Duration = Duration::from_secs(6);
#[repr(C)]
struct AddParams {
width: u32,
height: u32,
refresh: u32,
guid: [u8; 16],
device_name: [u8; 14],
serial: [u8; 14],
}
#[repr(C)]
struct AddOut {
luid_low: u32,
luid_high: i32,
target_id: u32,
}
#[repr(C)]
struct RemoveParams {
guid: [u8; 16],
}
#[repr(C)]
struct SetRenderAdapterParams {
luid_low: u32,
luid_high: i32,
}
#[repr(C)]
struct WatchdogOut {
timeout: u32,
countdown: u32,
}
fn guid_key(b: &[u8; 16]) -> u128 {
u128::from_le_bytes(*b)
}
/// SAFETY: `request` valid; returns a pointer to the request's input buffer of at least `min` bytes.
unsafe fn input_buf(request: WDFREQUEST, min: usize) -> Option<*const u8> {
let mut p: *mut c_void = std::ptr::null_mut();
let mut len: usize = 0;
let r = unsafe { WdfRequestRetrieveInputBuffer(request, min, &mut p, &mut len) };
if r.is_err() || p.is_null() || len < min {
return None;
}
Some(p.cast::<u8>())
}
/// SAFETY: `request` valid; returns a pointer to the request's output buffer of at least `min` bytes.
unsafe fn output_buf(request: WDFREQUEST, min: usize) -> Option<*mut u8> {
let mut p: *mut c_void = std::ptr::null_mut();
let mut len: usize = 0;
let r = unsafe { WdfRequestRetrieveOutputBuffer(request, min, &mut p, &mut len) };
if r.is_err() || p.is_null() || len < min {
return None;
}
Some(p.cast::<u8>())
}
/// `EVT_IDD_CX_DEVICE_IO_CONTROL` — IddCx redirects device IOCTLs here. Signature matches SudoVDA's
/// `SudoVDAIoDeviceControl(Device, Request, OutputBufferLength, InputBufferLength, IoControlCode)`.
pub extern "C-unwind" fn device_io_control(
device: WDFDEVICE,
request: WDFREQUEST,
output_len: usize,
input_len: usize,
ioctl_code: u32,
) {
// Reset the watchdog on any IOCTL except the watchdog query (the host PINGs to keep alive).
if ioctl_code != IOCTL_GET_WATCHDOG {
WATCHDOG_COUNTDOWN.store(WATCHDOG_TIMEOUT.load(Ordering::Relaxed), Ordering::Relaxed);
}
let mut bytes: usize = 0;
// SAFETY: dispatch reads/writes the request buffers it validated; `device` is the IddCx device.
let status = unsafe {
match ioctl_code {
IOCTL_ADD => do_add(device, request, input_len, output_len, &mut bytes),
IOCTL_REMOVE => do_remove(request, input_len),
IOCTL_SET_RENDER_ADAPTER => do_set_render_adapter(request, input_len),
IOCTL_GET_WATCHDOG => do_get_watchdog(request, output_len, &mut bytes),
IOCTL_PING => NTSTATUS::STATUS_SUCCESS,
IOCTL_CLEAR_ALL => {
disconnect_all_monitors(true);
NTSTATUS::STATUS_SUCCESS
}
IOCTL_GET_VERSION => do_get_version(request, output_len, &mut bytes),
_ => NTSTATUS::STATUS_INVALID_DEVICE_REQUEST,
}
};
// SAFETY: completing the request we were handed.
let _ = unsafe { WdfRequestCompleteWithInformation(request, status, bytes as u64) };
}
unsafe fn do_add(
device: WDFDEVICE,
request: WDFREQUEST,
input_len: usize,
output_len: usize,
bytes: &mut usize,
) -> NTSTATUS {
// Serialize the whole ADD (push entry → create_monitor → verify) against the watchdog teardown +
// REMOVE, so an expiry can never drain this entry mid-flight. `create_monitor` is fast (the slow
// CCD/GDI work is host-side, after this returns), and PING/GET_WATCHDOG don't take this lock, so
// the host keeps the watchdog reset while we hold it.
let _op = MONITOR_OP_LOCK.lock().unwrap();
if input_len < size_of::<AddParams>() || output_len < size_of::<AddOut>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let (Some(pin), Some(pout)) = (
unsafe { input_buf(request, size_of::<AddParams>()) },
unsafe { output_buf(request, size_of::<AddOut>()) },
) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let params = unsafe { &*pin.cast::<AddParams>() };
let guid = guid_key(&params.guid);
// Dedup: an existing GUID returns its LUID + target id (the host may re-ADD on reconnect).
{
let lock = MONITOR_MODES.lock().unwrap();
if let Some(mon) = lock.iter().find(|m| m.guid == guid) {
let out = AddOut {
luid_low: mon.adapter_luid_low,
luid_high: mon.adapter_luid_high,
target_id: mon.target_id,
};
unsafe { pout.cast::<AddOut>().write_unaligned(out) };
*bytes = size_of::<AddOut>();
return NTSTATUS::STATUS_SUCCESS;
}
}
if params.width == 0 || params.height == 0 || params.refresh == 0 {
return NTSTATUS::STATUS_INVALID_PARAMETER;
}
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
// Requested mode first (preferred), then fallbacks.
let mut modes = vec![Mode {
width: params.width,
height: params.height,
refresh_rates: vec![params.refresh],
}];
modes.extend(default_modes());
MONITOR_MODES.lock().unwrap().push(MonitorObject {
object: None,
data: MonitorData { id, modes },
guid,
target_id: 0,
adapter_luid_low: 0,
adapter_luid_high: 0,
created_at: Instant::now(),
});
// Create the IddCx monitor via the device context (captures target id + LUID into the entry).
let created = unsafe {
DeviceContext::get_mut(device.cast(), |ctx| {
if let Err(e) = ctx.create_monitor(id) {
error!("ADD: create_monitor failed: {e:?}");
}
})
};
let lock = MONITOR_MODES.lock().unwrap();
let mon = lock.iter().find(|m| m.data.id == id);
if created.is_err() || mon.map_or(true, |m| m.object.is_none()) {
drop(lock);
MONITOR_MODES.lock().unwrap().retain(|m| m.data.id != id);
error!("ADD: monitor {id} failed to arrive");
return NTSTATUS::STATUS_UNSUCCESSFUL;
}
let mon = mon.unwrap();
let out = AddOut {
luid_low: mon.adapter_luid_low,
luid_high: mon.adapter_luid_high,
target_id: mon.target_id,
};
unsafe { pout.cast::<AddOut>().write_unaligned(out) };
*bytes = size_of::<AddOut>();
info!(
"ADD {}x{}@{} -> target_id={} luid={:08x}:{:08x}",
params.width, params.height, params.refresh, mon.target_id, mon.adapter_luid_high, mon.adapter_luid_low
);
NTSTATUS::STATUS_SUCCESS
}
unsafe fn do_remove(request: WDFREQUEST, input_len: usize) -> NTSTATUS {
if input_len < size_of::<RemoveParams>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pin) = (unsafe { input_buf(request, size_of::<RemoveParams>()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let params = unsafe { &*pin.cast::<RemoveParams>() };
let guid = guid_key(&params.guid);
// Serialize against ADD + watchdog teardown (lock order: OP_LOCK → MONITOR_MODES).
let _op = MONITOR_OP_LOCK.lock().unwrap();
let mon = {
let mut lock = MONITOR_MODES.lock().unwrap();
match lock.iter().position(|m| m.guid == guid) {
Some(pos) => lock.remove(pos),
None => return NTSTATUS::STATUS_NOT_FOUND,
}
// MONITOR_MODES released here — the processor-join + departure below must not hold it.
};
if let Some(obj) = mon.object {
free_swap_chain_processor(obj.as_ptr());
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("REMOVE: departure failed: {e:?}");
}
}
info!("REMOVE target_id={}", mon.target_id);
NTSTATUS::STATUS_SUCCESS
}
/// Drop a monitor's live swap-chain processor BEFORE departure. The WDF context is an
/// `Arc<RwLock<MonitorContext>>` that WDF frees WITHOUT running Rust `Drop` (no `EvtCleanupCallback`
/// is wired), and the OS does not reliably call UNASSIGN on a host-initiated departure — so the
/// streaming `Direct3DDevice` (its ~dozens of D3D worker threads + tens of MB of VRAM) was orphaned
/// once per session, the dominant reconnect-churn leak. `get_mut` takes the context `RwLock`, so this
/// is safe against a concurrent OS unassign callback (whichever runs second sees `None`).
fn free_swap_chain_processor(monitor: *mut wdf_umdf_sys::IDDCX_MONITOR__) {
// SAFETY: `monitor` is a live IddCx monitor object whose context was init'd at creation.
let r = unsafe { MonitorContext::get_mut(monitor.cast(), |ctx| ctx.unassign_swap_chain()) };
if let Err(e) = r {
error!("free_swap_chain_processor: get_mut FAILED: {e:?}");
}
}
unsafe fn do_set_render_adapter(request: WDFREQUEST, input_len: usize) -> NTSTATUS {
if input_len < size_of::<SetRenderAdapterParams>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pin) = (unsafe { input_buf(request, size_of::<SetRenderAdapterParams>()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let params = unsafe { &*pin.cast::<SetRenderAdapterParams>() };
PREFERRED_RENDER_ADAPTER.store(
((params.luid_high as u32 as u64) << 32) | u64::from(params.luid_low),
Ordering::Relaxed,
);
if let Some(adapter) = ADAPTER.get() {
let in_args = IDARG_IN_ADAPTERSETRENDERADAPTER {
PreferredRenderAdapter: LUID {
LowPart: params.luid_low,
HighPart: params.luid_high,
},
};
if let Err(e) = unsafe { IddCxAdapterSetRenderAdapter(adapter.0.as_ptr(), &in_args) } {
error!("SET_RENDER_ADAPTER failed: {e:?}");
}
}
NTSTATUS::STATUS_SUCCESS
}
unsafe fn do_get_watchdog(request: WDFREQUEST, output_len: usize, bytes: &mut usize) -> NTSTATUS {
if output_len < size_of::<WatchdogOut>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pout) = (unsafe { output_buf(request, size_of::<WatchdogOut>()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
let out = WatchdogOut {
timeout: WATCHDOG_TIMEOUT.load(Ordering::Relaxed),
countdown: WATCHDOG_COUNTDOWN.load(Ordering::Relaxed),
};
unsafe { pout.cast::<WatchdogOut>().write_unaligned(out) };
*bytes = size_of::<WatchdogOut>();
NTSTATUS::STATUS_SUCCESS
}
unsafe fn do_get_version(request: WDFREQUEST, output_len: usize, bytes: &mut usize) -> NTSTATUS {
if output_len < PROTOCOL_VERSION.len() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
let Some(pout) = (unsafe { output_buf(request, PROTOCOL_VERSION.len()) }) else {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
};
unsafe { std::ptr::copy_nonoverlapping(PROTOCOL_VERSION.as_ptr(), pout, PROTOCOL_VERSION.len()) };
*bytes = PROTOCOL_VERSION.len();
NTSTATUS::STATUS_SUCCESS
}
/// Tear down monitors. `force` (CLEAR_ALL) reaps EVERYTHING — orphans from a crashed previous host;
/// the watchdog passes `false`, which spares any monitor still inside its creation grace
/// (`MONITOR_GRACE`) so a freshly-born monitor is never reaped mid-setup. Caller MUST hold
/// `MONITOR_OP_LOCK` (lock order: OP_LOCK → MONITOR_MODES). Mirrors SudoVDA's DisconnectAllMonitors.
fn disconnect_all_monitors_locked(force: bool) {
// Drain under the lock (fast); free processors + depart OUTSIDE it (the processor-join blocks).
let to_depart: Vec<MonitorObject> = {
let mut lock = MONITOR_MODES.lock().unwrap();
if lock.is_empty() {
return;
}
let mut keep: Vec<MonitorObject> = Vec::new();
let mut depart: Vec<MonitorObject> = Vec::new();
for mon in lock.drain(..) {
if !force && mon.created_at.elapsed() < MONITOR_GRACE {
keep.push(mon); // still in its host-side setup window — leave it alone
} else {
depart.push(mon);
}
}
*lock = keep;
depart
};
for mon in to_depart {
if let Some(obj) = mon.object {
free_swap_chain_processor(obj.as_ptr());
// SAFETY: `obj` is a live IddCx monitor object.
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("teardown: monitor departure failed: {e:?}");
}
}
}
}
/// Public entry: takes `MONITOR_OP_LOCK`, then tears down. Used by CLEAR_ALL (`force = true`).
fn disconnect_all_monitors(force: bool) {
let _op = MONITOR_OP_LOCK.lock().unwrap();
disconnect_all_monitors_locked(force);
}
/// Start the watchdog thread (once). The host reads the timeout via GET_WATCHDOG and PINGs every
/// timeout/3; if it stops, the countdown reaches 0 and every monitor is torn down — so a crashed/gone
/// host never leaves a phantom display. Mirrors SudoVDA's RunWatchdog.
pub fn start_watchdog() {
static STARTED: AtomicBool = AtomicBool::new(false);
if STARTED.swap(true, Ordering::Relaxed) {
return;
}
let timeout = WATCHDOG_TIMEOUT.load(Ordering::Relaxed);
if timeout == 0 {
return;
}
WATCHDOG_COUNTDOWN.store(timeout, Ordering::Relaxed);
thread::spawn(|| loop {
thread::sleep(Duration::from_secs(1));
// Nothing to guard while there are no monitors.
if MONITOR_MODES.lock().unwrap().is_empty() {
continue;
}
let prev = WATCHDOG_COUNTDOWN.load(Ordering::Relaxed);
if prev == 0 {
continue;
}
// Decrement without clobbering a concurrent IOCTL reset (CAS).
if WATCHDOG_COUNTDOWN
.compare_exchange(prev, prev - 1, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
&& prev - 1 == 0
{
// About to fire. Serialize against do_add/do_remove (so we never tear an entry out from
// under an in-flight ADD), then RE-CHECK the countdown under the lock: if a concurrent
// IOCTL (PING/ADD) reset it while we were acquiring the lock, the host is alive — abort.
let _op = MONITOR_OP_LOCK.lock().unwrap();
if WATCHDOG_COUNTDOWN.load(Ordering::Relaxed) == 0 {
error!("watchdog expired (host stopped pinging) — tearing down stale monitors");
disconnect_all_monitors_locked(false);
}
}
});
}
@@ -1,95 +0,0 @@
use std::sync::atomic::{AtomicI32, Ordering};
use windows::{
core::Error,
Win32::{
Foundation::LUID,
Graphics::{
Direct3D::D3D_DRIVER_TYPE_UNKNOWN,
Direct3D11::{
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext,
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
D3D11_CREATE_DEVICE_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY,
D3D11_CREATE_DEVICE_SINGLETHREADED, D3D11_SDK_VERSION,
},
Dxgi::{CreateDXGIFactory2, IDXGIAdapter1, IDXGIFactory5, DXGI_CREATE_FACTORY_FLAGS},
},
},
};
#[derive(thiserror::Error, Debug)]
pub enum Direct3DError {
#[error("Direct3DError({0:?})")]
Win32(#[from] Error),
#[error("Direct3DError(\"{0}\")")]
Other(&'static str),
}
impl From<&'static str> for Direct3DError {
fn from(value: &'static str) -> Self {
Direct3DError::Other(value)
}
}
/// DIAGNOSTIC: live `Direct3DDevice` count. Each one holds an `ID3D11Device` whose NVIDIA UMD spawns
/// ~dozens of worker threads; if this climbs without bound across reconnects, devices are leaking.
pub static LIVE_DEVICES: AtomicI32 = AtomicI32::new(0);
#[derive(Debug)]
pub struct Direct3DDevice {
// The following are already refcounted, so they're safe to use directly without additional drop impls
_dxgi_factory: IDXGIFactory5,
_adapter: IDXGIAdapter1,
pub device: ID3D11Device,
/// The single (SINGLETHREADED) immediate context — used by the frame-push publisher's
/// `CopyResource` on the swap-chain processor thread (the one thread this device is touched from).
pub device_context: ID3D11DeviceContext,
}
impl Direct3DDevice {
pub fn init(adapter_luid: LUID) -> Result<Self, Direct3DError> {
let dxgi_factory =
unsafe { CreateDXGIFactory2::<IDXGIFactory5>(DXGI_CREATE_FACTORY_FLAGS(0))? };
let adapter = unsafe { dxgi_factory.EnumAdapterByLuid::<IDXGIAdapter1>(adapter_luid)? };
let mut device = None;
let mut device_context = None;
unsafe {
D3D11CreateDevice(
&adapter,
D3D_DRIVER_TYPE_UNKNOWN,
None,
D3D11_CREATE_DEVICE_BGRA_SUPPORT
| D3D11_CREATE_DEVICE_SINGLETHREADED
| D3D11_CREATE_DEVICE_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut device_context),
)?;
}
let device = device.ok_or("ID3D11Device not found")?;
let device_context = device_context.ok_or("ID3D11DeviceContext not found")?;
let live = LIVE_DEVICES.fetch_add(1, Ordering::Relaxed) + 1;
log::error!("Direct3DDevice::init OK — live D3D devices = {live}");
Ok(Self {
_dxgi_factory: dxgi_factory,
_adapter: adapter,
device,
device_context,
})
}
}
impl Drop for Direct3DDevice {
fn drop(&mut self) {
let live = LIVE_DEVICES.fetch_sub(1, Ordering::Relaxed) - 1;
log::error!("Direct3DDevice::drop — live D3D devices = {live}");
}
}
@@ -1,118 +0,0 @@
//! The 256-byte EDID the pf-vdisplay driver hands IddCx for each virtual monitor: a 128-byte EDID 1.4
//! base block + a **CTA-861.3 extension** that advertises HDR — a BT.2020 Colorimetry Data Block and an
//! HDR Static Metadata Data Block declaring the SMPTE ST 2084 (PQ) EOTF. Windows reads a display's HDR
//! capability from this CTA HDR block; without it the monitor is treated as SDR-only regardless of the
//! IddCx adapter's `CAN_PROCESS_FP16` / `HIGH_COLOR_SPACE` / 10-bit mode caps (the missing piece that
//! made "Use HDR" never appear for the virtual display). The base block declares EDID 1.4 + 10-bit
//! digital so the panel's bit depth is unambiguous.
//!
//! Identity: manufacturer "PNK" (bytes 8-9), product name "punktfunk" (the 0xFC display descriptor). The
//! serial-number field (base offset 0x0C, little-endian) encodes the per-monitor index so
//! `parse_monitor_description` can map an EDID the OS hands back to its monitor; [`Edid::generate_with`]
//! patches that serial and recomputes BOTH block checksums (base byte 127 + extension byte 255). The
//! detailed-timing / range-limit descriptors are placeholders — the modes we actually advertise come
//! from the monitor's stored mode list (`monitor.rs` / `callbacks.rs`), not from parsing this EDID.
use std::array::TryFromSliceError;
/// Per-monitor serial number, base-block offset 0x0C, little-endian u32.
const SERIAL_OFFSET: usize = 0x0C;
/// EDID 1.4 base block (128 bytes). Differs from a plain SDR virtual EDID only by: revision 1.4 (byte
/// 19 = 0x04), 10-bit digital video input (byte 20 = 0xB0), and one extension present (byte 126 = 0x01).
/// Byte 127 (checksum) and the serial (0x0C) are filled/patched in [`Edid::generate_with`].
#[rustfmt::skip]
const BASE: [u8; 128] = [
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, // fixed header
0x41, 0xCB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mfr "PNK", product, serial (patched)
0xFF, 0x21, 0x01, 0x04, 0xB0, 0x32, 0x1F, 0x78, // week/year, EDID 1.4, 10-bit digital, size, gamma
0x03, 0x78, 0xB1, 0xB5, 0x4A, 0x2B, 0xCC, 0x21, // feature (sRGB-default CLEARED), BT.2020 primaries...
0x0B, 0x50, 0x54, 0x00, 0x00, 0x00, 0x01, 0x01, // ...BT.2020 primaries, established timings, std timings
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3A, // std timings, DTD 1 (placeholder preferred timing)
0x80, 0x18, 0x71, 0x38, 0x2D, 0x40, 0x58, 0x2C,
0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E,
0x00, 0x00, 0x00, 0xFD, 0x00, 0x17, 0xF0, 0x0F, // display range-limits descriptor
0xFF, 0x0F, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x70, // name descriptor "punktfunk"
0x75, 0x6E, 0x6B, 0x74, 0x66, 0x75, 0x6E, 0x6B,
0x0A, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, // empty 4th descriptor...
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, // ...byte 126 = 1 extension, byte 127 = checksum
];
/// CTA-861.3 extension block (128 bytes), block 1. Header + a Data Block Collection holding the
/// Colorimetry and HDR Static Metadata data blocks; the rest is padding up to the checksum (byte 255).
/// `D` (byte 130) marks where DTDs would start (= end of the data blocks); we carry none.
#[rustfmt::skip]
const CTA_HEADER: [u8; 4] = [
0x02, // CTA Extension tag
0x03, // revision 3 (CTA-861.3 — required for the extended-tag data blocks below)
0x0F, // D = 15: the (empty) DTD region starts at block byte 15, i.e. data blocks occupy bytes 4..15
0x00, // 0 native DTDs; no basic audio; no YCbCr 4:4:4/4:2:2 (RGB-only, matching the wire format)
];
/// Colorimetry Data Block (CTA extended tag 0x05): declare BT.2020 RGB (bit 7). YCbCr variants are left
/// clear — the IddCx wire format is RGB-only — and the gamut-metadata flags are 0.
#[rustfmt::skip]
const COLORIMETRY_DB: [u8; 4] = [
0xE3, // tag 0b111 (use-extended-tag) | length 3
0x05, // extended tag: Colorimetry
0x80, // BT2020RGB (bit 7); xvYCC/sYCC/opRGB/BT2020 YCC/cYCC all clear
0x00, // gamut metadata profiles MD0..MD3: none
];
/// HDR Static Metadata Data Block (CTA extended tag 0x06): EOTFs = Traditional SDR (ET_0) + SMPTE ST
/// 2084 / PQ (ET_2); Static Metadata Type 1 (SM_0). Plus the optional desired-content luminance hints
/// (~993 nit max, ~400 nit max-frame-average, ~0.05 nit min) so the block is complete.
#[rustfmt::skip]
const HDR_STATIC_METADATA_DB: [u8; 7] = [
0xE6, // tag 0b111 (use-extended-tag) | length 6
0x06, // extended tag: HDR Static Metadata
0x05, // Supported EOTFs: ET_0 (traditional SDR) | ET_2 (SMPTE ST 2084 / PQ)
0x01, // Supported Static Metadata Descriptors: SM_0 (Static Metadata Type 1)
0x8A, // Desired Content Max Luminance (code 138 ≈ 993 nits)
0x60, // Desired Content Max Frame-avg Lum. (code 96 = 400 nits)
0x12, // Desired Content Min Luminance (code 18 ≈ 0.05 nits)
];
#[derive(Debug, Clone, Copy)]
pub struct Edid;
impl Edid {
/// Build the full 256-byte EDID for monitor `serial`, with both block checksums recomputed.
pub fn generate_with(serial: u32) -> Vec<u8> {
let mut edid = [0u8; 256];
// Block 0: base.
edid[..128].copy_from_slice(&BASE);
edid[SERIAL_OFFSET..SERIAL_OFFSET + 4].copy_from_slice(&serial.to_le_bytes());
// Block 1: CTA-861.3 extension (header + colorimetry + HDR static metadata; rest stays 0).
edid[128..132].copy_from_slice(&CTA_HEADER);
edid[132..136].copy_from_slice(&COLORIMETRY_DB);
edid[136..143].copy_from_slice(&HDR_STATIC_METADATA_DB);
// Each 128-byte block ends in a checksum byte that makes the block sum ≡ 0 (mod 256).
Self::fix_block_checksum(&mut edid, 0);
Self::fix_block_checksum(&mut edid, 128);
edid.to_vec()
}
/// Read the per-monitor serial (base offset 0x0C, little-endian) from an EDID the OS handed back.
/// Works for the full 256-byte EDID or just the 128-byte base block. Errors (rather than panics) on
/// a too-short buffer so the caller can reject a malformed descriptor.
pub fn get_serial(edid: &[u8]) -> Result<u32, TryFromSliceError> {
let bytes: [u8; 4] = edid
.get(SERIAL_OFFSET..SERIAL_OFFSET + 4)
.unwrap_or(&[])
.try_into()?;
Ok(u32::from_le_bytes(bytes))
}
/// Set the trailing byte of the 128-byte block at `start` so the block's bytes sum to 0 (mod 256) —
/// the standard EDID block checksum.
fn fix_block_checksum(edid: &mut [u8], start: usize) {
let sum = edid[start..start + 127]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_add(b));
edid[start + 127] = 0u8.wrapping_sub(sum);
}
}
@@ -1,130 +0,0 @@
//! Driver entry + WDF device-add. Adapted from virtual-display-rs (its event-log/boot-retry logger
//! dance is replaced by the `OutputDebugString` logger in `logger.rs`).
use log::{error, info};
use wdf_umdf::{
IddCxDeviceInitConfig, IddCxDeviceInitialize, WdfDeviceCreate, WdfDeviceCreateDeviceInterface,
WdfDeviceInitSetPnpPowerEventCallbacks, WdfDriverCreate,
};
use wdf_umdf_sys::{
GUID, IDD_CX_CLIENT_CONFIG, NTSTATUS, WDFDEVICE_INIT, WDFDRIVER__, WDFOBJECT, WDF_DRIVER_CONFIG,
WDF_OBJECT_ATTRIBUTES, WDF_PNPPOWER_EVENT_CALLBACKS, _DRIVER_OBJECT, _UNICODE_STRING,
};
use crate::callbacks::{
adapter_commit_modes, adapter_commit_modes2, adapter_init_finished, assign_swap_chain,
device_d0_entry, monitor_get_default_modes, monitor_query_modes, monitor_query_modes2,
parse_monitor_description, parse_monitor_description2, query_target_info,
set_default_hdr_metadata, set_gamma_ramp, unassign_swap_chain,
};
use crate::context::DeviceContext;
use crate::control::device_io_control;
// SudoVDA control-interface GUID — the host opens this to drive the ADD/REMOVE/PING IOCTLs.
// {e5bcc234-1e0c-418a-a0d4-ef8b7501414d}
const SUVDA_INTERFACE_GUID: GUID = GUID {
Data1: 0xe5bc_c234,
Data2: 0x1e0c,
Data3: 0x418a,
Data4: [0xa0, 0xd4, 0xef, 0x8b, 0x75, 0x01, 0x41, 0x4d],
};
/// Driver entry point (called by the framework via `FxDriverEntryUm`).
#[no_mangle]
extern "C-unwind" fn DriverEntry(
driver_object: *mut _DRIVER_OBJECT,
registry_path: *mut _UNICODE_STRING,
) -> NTSTATUS {
crate::logger::init();
crate::panic::set_hook();
info!("pf-vdisplay v{} starting", env!("CARGO_PKG_VERSION"));
let mut attributes = WDF_OBJECT_ATTRIBUTES::init();
let mut config = WDF_DRIVER_CONFIG::init(Some(driver_add));
unsafe {
WdfDriverCreate(
driver_object,
registry_path,
Some(&mut attributes),
&mut config,
None,
)
}
.into()
}
extern "C-unwind" fn driver_add(
_driver: *mut WDFDRIVER__,
mut init: *mut WDFDEVICE_INIT,
) -> NTSTATUS {
let mut callbacks = WDF_PNPPOWER_EVENT_CALLBACKS::init();
callbacks.EvtDeviceD0Entry = Some(device_d0_entry);
unsafe {
_ = WdfDeviceInitSetPnpPowerEventCallbacks(init, &mut callbacks);
}
let Some(mut config) = IDD_CX_CLIENT_CONFIG::init() else {
error!("Failed to create IDD_CX_CLIENT_CONFIG");
return NTSTATUS::STATUS_NOT_FOUND;
};
config.EvtIddCxAdapterInitFinished = Some(adapter_init_finished);
config.EvtIddCxParseMonitorDescription = Some(parse_monitor_description);
config.EvtIddCxMonitorGetDefaultDescriptionModes = Some(monitor_get_default_modes);
config.EvtIddCxMonitorQueryTargetModes = Some(monitor_query_modes);
config.EvtIddCxAdapterCommitModes = Some(adapter_commit_modes);
// IddCx 1.10 *2 mode DDIs (HDR-capable path). The OS prefers these on 1.10; the 1.x callbacks
// above stay as the down-level fallback. B1 advertises SDR through them (so behaviour is unchanged);
// B2 enables HDR by adding 10 bpc in `wire_bits()`, HIGH_COLOR_SPACE caps, and CAN_PROCESS_FP16.
config.EvtIddCxParseMonitorDescription2 = Some(parse_monitor_description2);
config.EvtIddCxMonitorQueryTargetModes2 = Some(monitor_query_modes2);
config.EvtIddCxAdapterCommitModes2 = Some(adapter_commit_modes2);
config.EvtIddCxAdapterQueryTargetInfo = Some(query_target_info);
config.EvtIddCxMonitorSetDefaultHdrMetaData = Some(set_default_hdr_metadata);
config.EvtIddCxMonitorSetGammaRamp = Some(set_gamma_ramp);
config.EvtIddCxMonitorAssignSwapChain = Some(assign_swap_chain);
config.EvtIddCxMonitorUnassignSwapChain = Some(unassign_swap_chain);
// IddCx redirects device IOCTLs to this callback — our SudoVDA-compatible control plane.
config.EvtIddCxDeviceIoControl = Some(device_io_control);
let init_data = unsafe { &mut *init };
let status = unsafe { IddCxDeviceInitConfig(init_data, &config) };
if let Err(e) = status {
error!("Failed to init iddcx config: {e:?}");
return e.into();
}
let mut attributes =
WDF_OBJECT_ATTRIBUTES::init_context_type(unsafe { DeviceContext::get_type_info() });
attributes.EvtCleanupCallback = Some(event_cleanup);
let mut device = std::ptr::null_mut();
let status = unsafe { WdfDeviceCreate(&mut init, Some(&mut attributes), &mut device) };
if let Err(e) = status {
error!("Failed to create device: {e:?}");
return e.into();
}
// Register the SudoVDA control interface so the host can open it + send the control IOCTLs.
let status =
unsafe { WdfDeviceCreateDeviceInterface(device, &SUVDA_INTERFACE_GUID, std::ptr::null()) };
if let Err(e) = status {
error!("Failed to create control device interface: {e:?}");
return e.into();
}
let status = unsafe { IddCxDeviceInitialize(device) };
if let Err(e) = status {
error!("Failed to init iddcx device: {e:?}");
return e.into();
}
let context = DeviceContext::new(device);
unsafe { context.init(device as WDFOBJECT).into() }
}
unsafe extern "C-unwind" fn event_cleanup(wdf_object: WDFOBJECT) {
_ = unsafe { DeviceContext::drop(wdf_object) };
}
@@ -1,424 +0,0 @@
//! P2 direct frame push — DRIVER side. The restricted WUDFHost token canNOT create named kernel
//! objects (proven on the RTX box: it can't even write a world-writable file), so — exactly like the
//! gamepad UMDF drivers (`crates/punktfunk-host/src/inject/dualsense_windows.rs`: *"the host creates
//! the section, privileged, with a permissive SDDL so the WUDFHost can open it; the driver maps it"*)
//! — the **host** creates the shared header + frame-ready event + ring of keyed-mutex textures, and
//! the driver only **OPENS** them. The driver writes its actual render-adapter LUID + a status code
//! back into the host-created header (our only driver-visibility channel: UMDF hides OutputDebugString
//! in ETW and the token can't write files), then copies each acquired swap-chain surface into the next
//! ring slot and signals the host.
//!
//! Host counterpart: `crates/punktfunk-host/src/capture/idd_push.rs` — [`SharedHeader`], [`MAGIC`],
//! [`RING_LEN`], the driver-status codes and the `Global\` object-name scheme are DUPLICATED
//! byte-identically there.
use std::sync::atomic::{AtomicPtr, AtomicU32, AtomicU64, Ordering};
use log::info;
use windows::core::{Interface, HSTRING};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Graphics::Direct3D11::{
ID3D11Device, ID3D11Device1, ID3D11DeviceContext, ID3D11Texture2D, D3D11_TEXTURE2D_DESC,
};
use windows::Win32::Graphics::Dxgi::IDXGIKeyedMutex;
use windows::Win32::System::Memory::{
MapViewOfFile, OpenFileMappingW, UnmapViewOfFile, FILE_MAP_ALL_ACCESS,
MEMORY_MAPPED_VIEW_ADDRESS,
};
use windows::Win32::System::Threading::{OpenEventW, SetEvent, SYNCHRONIZATION_ACCESS_RIGHTS};
// --- kept byte-identical with the host (idd_push.rs) ---
pub const MAGIC: u32 = 0x4456_4650;
/// Kept for parity with the host's duplicated protocol header (the host writes it).
#[allow(dead_code)]
pub const VERSION: u32 = 1;
/// Ring slots. 6 (was 3) gives ample headroom so this 0 ms-timeout publish always finds a free slot
/// while the host briefly holds one across the convert/copy into its output ring and the depth-2
/// pipelined encode runs. MUST equal the host's `RING_LEN` (idd_push.rs) — both are rebuilt together;
/// a mismatch corrupts the slot mapping.
pub const RING_LEN: u32 = 6;
const DXGI_SHARED_RESOURCE_RW: u32 = 0x8000_0000 | 0x1;
/// SYNCHRONIZE | EVENT_MODIFY_STATE — the driver waits on (no) and SIGNALS the event.
const EVENT_ACCESS: u32 = 0x0010_0000 | 0x0002;
const WAIT_TIMEOUT_HRESULT: i32 = 0x0000_0102;
/// `driver_status` values the driver writes into the host header (the host logs them on a timeout).
/// `NONE` is the host's initial value (kept for parity).
#[allow(dead_code)]
pub const DRV_STATUS_NONE: u32 = 0;
pub const DRV_STATUS_OPENED: u32 = 1;
pub const DRV_STATUS_TEX_FAIL: u32 = 2;
pub const DRV_STATUS_NO_DEVICE1: u32 = 3;
#[repr(C)]
pub struct SharedHeader {
pub magic: u32,
pub version: u32,
pub generation: u32,
pub ring_len: u32,
pub width: u32,
pub height: u32,
pub dxgi_format: u32,
pub _pad: u32,
/// `(seq << 8) | slot` — DRIVER-written after each copy; host loads it `Acquire`.
pub latest: u64,
pub qpc_pts: u64,
/// DRIVER-written: the adapter the swap-chain actually renders on (so the host can detect a
/// mismatch with the textures it created and report it).
pub driver_render_luid_low: u32,
pub driver_render_luid_high: i32,
/// DRIVER-written status (visibility channel).
pub driver_status: u32,
pub driver_status_detail: u32,
}
pub fn hdr_name(target_id: u32) -> String {
format!("Global\\pfvd-hdr-{target_id}")
}
pub fn evt_name(target_id: u32) -> String {
format!("Global\\pfvd-evt-{target_id}")
}
pub fn tex_name(target_id: u32, generation: u32, slot: u32) -> String {
format!("Global\\pfvd-tex-{target_id}-{generation}-{slot}")
}
// --------------------------------------------------------
// ===== Bring-up debug channel (fixed-name, host-created) =====
// UMDF hides the driver's OutputDebugString (ETW) and the restricted token can't write files, so this
// fixed-name `Global\pfvd-dbg` block — created by the host with the permissive SDDL — is how the driver
// reports what it's doing, INDEPENDENT of the per-target header (which is the thing under test). The
// host reads + logs these counters. Duplicated in `idd_push.rs`.
#[repr(C)]
pub struct DebugBlock {
pub magic: u32,
/// ++ each `run_core` entry — proves the swap-chain processor runs at all.
pub run_core_entries: u32,
/// The `target_id` the driver resolved for naming (mismatch vs the host = the bug).
pub resolved_target_id: u32,
/// ++ each header-open attempt.
pub header_open_attempts: u32,
/// Last header-open error (win32/HRESULT).
pub last_open_error: u32,
/// 1 once the driver opened the per-target header.
pub header_opened: u32,
pub render_luid_low: u32,
pub render_luid_high: i32,
/// ++ each acquired swap-chain frame — proves frames flow (or the display is idle).
pub frames_acquired: u32,
pub _pad: u32,
}
static DBG_PTR: AtomicPtr<DebugBlock> = AtomicPtr::new(std::ptr::null_mut());
/// Map the host-created debug block on first use (fixed name). Returns null until the host creates it.
fn dbg_block() -> *mut DebugBlock {
let p = DBG_PTR.load(Ordering::Acquire);
if !p.is_null() {
return p;
}
let Ok(map) = (unsafe {
OpenFileMappingW(FILE_MAP_ALL_ACCESS.0, false, &HSTRING::from("Global\\pfvd-dbg"))
}) else {
return std::ptr::null_mut();
};
let view = unsafe { MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, std::mem::size_of::<DebugBlock>()) };
if view.Value.is_null() {
unsafe {
let _ = CloseHandle(map);
}
return std::ptr::null_mut();
}
let np = view.Value.cast::<DebugBlock>();
match DBG_PTR.compare_exchange(std::ptr::null_mut(), np, Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => np, // we win; intentionally leak the handle (diagnostic, process-lifetime)
Err(existing) => {
unsafe {
let _ = UnmapViewOfFile(view);
let _ = CloseHandle(map);
}
existing
}
}
}
pub fn dbg_run_core_entry() {
let p = dbg_block();
if !p.is_null() {
unsafe {
(*(std::ptr::addr_of_mut!((*p).run_core_entries) as *const AtomicU32))
.fetch_add(1, Ordering::Relaxed);
}
}
}
pub fn dbg_frame() {
let p = dbg_block();
if !p.is_null() {
unsafe {
(*(std::ptr::addr_of_mut!((*p).frames_acquired) as *const AtomicU32))
.fetch_add(1, Ordering::Relaxed);
}
}
}
/// Record the target id + render LUID the driver will use to name the shared objects.
pub fn dbg_set_target(target_id: u32, render_luid_low: u32, render_luid_high: i32) {
let p = dbg_block();
if !p.is_null() {
unsafe {
(*p).resolved_target_id = target_id;
(*p).render_luid_low = render_luid_low;
(*p).render_luid_high = render_luid_high;
}
}
}
/// Record a header-open attempt + its error (0 = success).
pub fn dbg_header_attempt(error: u32, opened: bool) {
let p = dbg_block();
if !p.is_null() {
unsafe {
(*(std::ptr::addr_of_mut!((*p).header_open_attempts) as *const AtomicU32))
.fetch_add(1, Ordering::Relaxed);
(*p).last_open_error = error;
if opened {
(*p).header_opened = 1;
}
}
}
}
struct Slot {
tex: ID3D11Texture2D,
mutex: IDXGIKeyedMutex,
}
/// Publishes acquired swap-chain surfaces into the HOST-created ring. Owned by the swap-chain
/// processor thread; attached lazily once the host has created the shared objects.
pub struct FramePublisher {
context: ID3D11DeviceContext,
map: HANDLE,
header: *mut SharedHeader,
event: HANDLE,
slots: Vec<Slot>,
next: u32,
seq: u64,
/// The host-created ring textures' DXGI format (from the shared header). A swap-chain surface whose
/// format differs (e.g. an FP16 HDR frame vs a BGRA ring) is dropped in `publish` — CopyResource
/// needs matching formats.
ring_format: u32,
/// The ring generation this publisher attached to. The host BUMPS the header generation when it
/// recreates the ring at a new format mid-session (the display's HDR mode flipped) — [`Self::is_stale`]
/// detects that so `run_core` re-attaches to the new-format textures instead of dropping every frame.
generation: u32,
}
// SAFETY: created and used only on the swap-chain processor thread.
unsafe impl Send for FramePublisher {}
impl FramePublisher {
/// Try ONCE to attach to the host-created shared objects. Returns `Err` cheaply if the host hasn't
/// created/published them yet — the drain loop retries periodically, so a non-IDD-push session
/// just keeps draining with no stall.
pub fn try_open(
target_id: u32,
render_luid_low: u32,
render_luid_high: i32,
device: &ID3D11Device,
context: &ID3D11DeviceContext,
) -> windows::core::Result<Self> {
// 1. Open the host-created header (RW). Err if the host hasn't created it yet.
let map = unsafe {
OpenFileMappingW(
FILE_MAP_ALL_ACCESS.0,
false,
&HSTRING::from(hdr_name(target_id)),
)?
};
let view =
unsafe { MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, std::mem::size_of::<SharedHeader>()) };
if view.Value.is_null() {
unsafe {
let _ = CloseHandle(map);
}
return Err(windows::core::Error::from_win32());
}
let header = view.Value.cast::<SharedHeader>();
// 2. Report our render adapter to the host immediately (lets it detect a mismatch).
unsafe {
(*header).driver_render_luid_low = render_luid_low;
(*header).driver_render_luid_high = render_luid_high;
}
// 3. The host sets magic==MAGIC only once the ring textures exist. Not ready → retry later.
let magic =
unsafe { (*(std::ptr::addr_of!((*header).magic) as *const AtomicU32)).load(Ordering::Acquire) };
if magic != MAGIC {
unsafe {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: header.cast() });
let _ = CloseHandle(map);
}
return Err(windows::core::Error::from_win32());
}
let (generation, ring_len) =
unsafe { ((*header).generation, (*header).ring_len.min(RING_LEN)) };
// 4. Open the event (SYNCHRONIZE | EVENT_MODIFY_STATE so we can SetEvent).
let event = match unsafe {
OpenEventW(
SYNCHRONIZATION_ACCESS_RIGHTS(EVENT_ACCESS),
false,
&HSTRING::from(evt_name(target_id)),
)
} {
Ok(e) => e,
Err(e) => {
unsafe {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: header.cast() });
let _ = CloseHandle(map);
}
return Err(e);
}
};
// 5. Open device1 + the ring textures the host created (same render adapter required).
let device1: ID3D11Device1 = match device.cast() {
Ok(d) => d,
Err(e) => {
unsafe {
(*header).driver_status = DRV_STATUS_NO_DEVICE1;
let _ = CloseHandle(event);
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: header.cast() });
let _ = CloseHandle(map);
}
return Err(e);
}
};
let mut slots = Vec::new();
for k in 0..ring_len {
let name = HSTRING::from(tex_name(target_id, generation, k));
let opened: windows::core::Result<ID3D11Texture2D> =
unsafe { device1.OpenSharedResourceByName(&name, DXGI_SHARED_RESOURCE_RW) };
match opened {
Ok(tex) => match tex.cast::<IDXGIKeyedMutex>() {
Ok(mutex) => slots.push(Slot { tex, mutex }),
Err(e) => {
unsafe {
(*header).driver_status = DRV_STATUS_TEX_FAIL;
(*header).driver_status_detail = e.code().0 as u32;
let _ = CloseHandle(event);
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: header.cast() });
let _ = CloseHandle(map);
}
return Err(e);
}
},
Err(e) => {
// Most likely a render-adapter mismatch (the host made the textures on a different
// GPU than the swap-chain renders on). Tell the host so it can report it.
unsafe {
(*header).driver_status = DRV_STATUS_TEX_FAIL;
(*header).driver_status_detail = e.code().0 as u32;
let _ = CloseHandle(event);
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: header.cast() });
let _ = CloseHandle(map);
}
return Err(e);
}
}
}
unsafe {
(*header).driver_status = DRV_STATUS_OPENED;
}
info!("frame-push(driver): attached to host ring gen {generation} ({ring_len} slots)");
Ok(Self {
context: context.clone(),
map,
header,
event,
slots,
next: 0,
seq: 0,
ring_format: unsafe { (*header).dxgi_format },
generation,
})
}
#[inline]
fn latest_cell(&self) -> &AtomicU64 {
unsafe { &*(std::ptr::addr_of!((*self.header).latest) as *const AtomicU64) }
}
/// True once the host has recreated the ring (bumped the header generation) — e.g. the display's
/// HDR mode flipped, so the ring format changed (FP16 ⇄ BGRA) and the texture names now carry a new
/// generation. `run_core` drops the publisher on this so it re-attaches to the new ring.
pub fn is_stale(&self) -> bool {
let cur = unsafe {
(*(std::ptr::addr_of!((*self.header).generation) as *const AtomicU32))
.load(Ordering::Acquire)
};
cur != self.generation
}
/// Copy `surface` into the next free ring slot and signal the host. Never blocks (0 ms try-acquire).
pub fn publish(&mut self, surface: &ID3D11Texture2D) {
let ring_len = self.slots.len() as u32;
if ring_len == 0 {
return;
}
// B2 format guard: CopyResource needs the surface + ring textures to share a DXGI format. Drop
// a frame that doesn't match (e.g. an FP16 HDR surface arriving while the ring is still BGRA,
// before B3 makes the ring FP16) instead of corrupting / failing the copy.
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { surface.GetDesc(&mut desc) };
if desc.Format.0 as u32 != self.ring_format {
return;
}
let start = self.next;
for attempt in 0..ring_len {
let slot = (start + attempt) % ring_len;
let s = &self.slots[slot as usize];
match unsafe { s.mutex.AcquireSync(0, 0) } {
Ok(()) => {
unsafe {
self.context.CopyResource(&s.tex, surface);
let _ = s.mutex.ReleaseSync(0);
}
self.seq = self.seq.wrapping_add(1);
// `latest` = (generation << 40) | (seq << 8) | slot. Stamping the generation lets the
// host REJECT a publish from a stale ring (an old-generation publisher racing the
// host's mid-session ring recreate) so it never consumes an unwritten new-ring slot.
let latest = (u64::from(self.generation) << 40)
| ((self.seq & 0xFFFF_FFFF) << 8)
| u64::from(slot & 0xff);
self.latest_cell().store(latest, Ordering::Release);
unsafe {
let _ = SetEvent(self.event);
}
self.next = (slot + 1) % ring_len;
return;
}
Err(e) if e.code().0 == WAIT_TIMEOUT_HRESULT => continue,
Err(_) => return,
}
}
// All slots busy — drop this frame (never block the swap-chain thread).
}
}
impl Drop for FramePublisher {
fn drop(&mut self) {
self.slots.clear();
unsafe {
if !self.header.is_null() {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: self.header.cast(),
});
}
let _ = CloseHandle(self.event);
let _ = CloseHandle(self.map);
}
}
}
@@ -1,38 +0,0 @@
use std::ops::{Deref, DerefMut};
/// An unsafe wrapper to allow sending across threads
///
/// USE WISELY, IT CAN CAUSE UB OTHERWISE
pub struct Sendable<T>(T);
unsafe impl<T> Send for Sendable<T> {}
unsafe impl<T> Sync for Sendable<T> {}
impl<T> Sendable<T> {
/// `T` must be Send+Sync safe
pub unsafe fn new(t: T) -> Self {
Sendable(t)
}
}
impl<T> Deref for Sendable<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for Sendable<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[macro_export]
macro_rules! debug {
($($tt:tt)*) => {
if cfg!(debug_assertions) {
::log::debug!($($tt)*);
}
};
}
@@ -1,34 +0,0 @@
//! pf-vdisplay — punktfunk Windows virtual display (IddCx), in Rust.
//!
//! P1: a UMDF2 IddCx virtual display. Adapted from MolotovCherry/virtual-display-rs (MIT) — its
//! named-pipe IPC + serde mode config is replaced by an in-tree `monitor` model (and, next, the
//! SudoVDA-compatible IOCTL control plane our host already speaks). Logging goes to
//! `OutputDebugString` (no `log`-eventlog/`tokio`). See `docs/windows-virtual-display-rust-port.md`.
#![allow(non_snake_case)]
mod callbacks;
mod context;
mod control;
mod direct_3d_device;
mod edid;
mod entry;
mod frame_transport;
mod helpers;
mod logger;
mod monitor;
mod panic;
mod swap_chain_processor;
use wdf_umdf_sys::{NTSTATUS, PUNICODE_STRING, PVOID};
// The framework entry point. UMDF's reflector calls this; the `+whole-archive` stub forwards to the
// `DriverEntry` symbol exported from `entry.rs`.
#[link(name = "WdfDriverStubUm", kind = "static", modifiers = "+whole-archive")]
extern "C" {
pub fn FxDriverEntryUm(
LoaderInterface: PVOID,
Context: PVOID,
DriverObject: PVOID,
RegistryPath: PUNICODE_STRING,
) -> NTSTATUS;
}
@@ -1,55 +0,0 @@
//! Minimal `log` backend that writes to `OutputDebugString` AND tees to a file — UMDF redirects a
//! hosted driver's `OutputDebugString` to ETW (invisible to DebugView), so the file tee is how we
//! actually read driver logs during bring-up. Keeping the `log` facade lets the ported
//! callbacks/context use `error!`/`info!`/`debug!` unchanged.
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::Mutex;
use log::{LevelFilter, Metadata, Record};
use windows::core::PCSTR;
use windows::Win32::System::Diagnostics::Debug::OutputDebugStringA;
/// World-writable so the restricted WUDFHost token can append. Read it during bring-up.
const LOG_PATH: &str = r"C:\Users\Public\pfvd-driver.log";
struct DbgLogger {
file: Mutex<()>,
}
impl log::Log for DbgLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
let msg = format!("[pf-vdisplay] {:<5} {}\0", record.level(), record.args());
// SAFETY: `msg` is a NUL-terminated byte string valid for the call.
unsafe { OutputDebugStringA(PCSTR(msg.as_ptr())) };
// Tee to the file (best-effort): the real channel during bring-up.
let _guard = self.file.lock();
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(LOG_PATH) {
let _ = writeln!(f, "{:<5} {}", record.level(), record.args());
}
}
fn flush(&self) {}
}
static LOGGER: DbgLogger = DbgLogger {
file: Mutex::new(()),
};
pub fn init() {
let _ = log::set_logger(&LOGGER);
log::set_max_level(if cfg!(debug_assertions) {
LevelFilter::Debug
} else {
LevelFilter::Info
});
// Boot marker so each load is distinguishable in the file.
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(LOG_PATH) {
let _ = writeln!(f, "==== pf-vdisplay logger init ====");
}
}
@@ -1,111 +0,0 @@
//! The monitor + mode model and control-plane state. Replaces virtual-display-rs's `ipc.rs`
//! (named-pipe IPC + serde `driver_ipc` types). Monitors are created on demand by the SudoVDA IOCTL
//! control plane (`control.rs`); each carries the GUID the host keys it by plus the OS target id +
//! render-adapter LUID captured at arrival (the ADD reply).
use std::ptr::NonNull;
use std::sync::atomic::{AtomicU32, AtomicU64};
use std::sync::{Mutex, OnceLock};
use std::time::Instant;
use wdf_umdf_sys::{IDDCX_ADAPTER__, IDDCX_MONITOR__};
pub type Dimen = u32;
pub type RefreshRate = u32;
/// One resolution with the refresh rates it supports.
#[derive(Clone, PartialEq, Eq)]
pub struct Mode {
pub width: Dimen,
pub height: Dimen,
pub refresh_rates: Vec<RefreshRate>,
}
/// A monitor's identity (the EDID serial) + advertised modes.
#[derive(Clone)]
pub struct MonitorData {
pub id: u32,
pub modes: Vec<Mode>,
}
/// A live (or pending) monitor.
pub struct MonitorObject {
pub object: Option<NonNull<IDDCX_MONITOR__>>,
pub data: MonitorData,
/// The full GUID the host keys this monitor by (ADD dedup / REMOVE).
pub guid: u128,
/// OS target id + render-adapter LUID, captured from `IDARG_OUT_MONITORARRIVAL` (the ADD reply).
pub target_id: u32,
pub adapter_luid_low: u32,
pub adapter_luid_high: i32,
/// When the entry was pushed (`do_add`). The watchdog skips monitors younger than the host's
/// setup window (CCD commit + GDI-name resolve + settle) so a still-initializing monitor is never
/// torn down mid-birth during reconnect churn.
pub created_at: Instant,
}
// SAFETY: the raw IddCx object ptr is framework-managed; access is serialized by MONITOR_MODES.
unsafe impl Send for MonitorObject {}
unsafe impl Sync for MonitorObject {}
/// The IddCx adapter object, stashed for the control plane (SET_RENDER_ADAPTER).
pub struct AdapterObject(pub NonNull<IDDCX_ADAPTER__>);
// SAFETY: raw ptr managed by the framework.
unsafe impl Send for AdapterObject {}
unsafe impl Sync for AdapterObject {}
pub static ADAPTER: OnceLock<AdapterObject> = OnceLock::new();
pub static MONITOR_MODES: Mutex<Vec<MonitorObject>> = Mutex::new(Vec::new());
/// Monitor id / EDID-serial counter (unique per created monitor).
pub static NEXT_ID: AtomicU32 = AtomicU32::new(1);
/// Watchdog (seconds). The host reads the timeout via GET_WATCHDOG and PINGs to keep alive. 8 s (was
/// 3) gives the host's between-session teardown gap — stop old pinger → CCD display re-attach (a slow
/// `SetDisplayConfig`) → REMOVE — headroom, so the watchdog doesn't spuriously fire during reconnect
/// churn. The host derives its PING interval from this (timeout/3), so it auto-adjusts.
pub static WATCHDOG_TIMEOUT: AtomicU32 = AtomicU32::new(8);
pub static WATCHDOG_COUNTDOWN: AtomicU32 = AtomicU32::new(8);
/// The preferred render adapter LUID set via SET_RENDER_ADAPTER, packed `(high<<32)|low`. 0 = none.
pub static PREFERRED_RENDER_ADAPTER: AtomicU64 = AtomicU64::new(0);
/// Protocol version reported by GET_VERSION: {major, minor, incremental, testbuild} — matches SudoVDA.
pub const PROTOCOL_VERSION: [u8; 4] = [0, 2, 1, 1];
/// A single (width, height, refresh) tuple — modes flattened across their refresh rates.
#[derive(Copy, Clone)]
pub struct ModeItem {
pub width: Dimen,
pub height: Dimen,
pub refresh_rate: RefreshRate,
}
pub trait FlattenModes {
fn flatten(&self) -> impl Iterator<Item = ModeItem>;
}
impl FlattenModes for Vec<Mode> {
fn flatten(&self) -> impl Iterator<Item = ModeItem> {
self.iter().flat_map(|m| {
m.refresh_rates.iter().map(|&rr| ModeItem {
width: m.width,
height: m.height,
refresh_rate: rr,
})
})
}
}
/// Fallback modes appended after the client's requested mode, so a topology change still has options.
pub fn default_modes() -> Vec<Mode> {
vec![
Mode {
width: 1920,
height: 1080,
refresh_rates: vec![60, 120],
},
Mode {
width: 1280,
height: 720,
refresh_rates: vec![60],
},
]
}
@@ -1,20 +0,0 @@
#[cfg(debug_assertions)]
use std::backtrace::Backtrace;
use std::panic;
use log::error;
pub fn set_hook() {
panic::set_hook(Box::new(|v| {
// debug mode, get full backtrace
#[cfg(debug_assertions)]
{
let backtrace = Backtrace::force_capture();
error!("{v}\n\nstack backtrace:\n{backtrace}");
}
// otherwise just print the panic since we don't have a backtrace
#[cfg(not(debug_assertions))]
error!("{v}");
}));
}
@@ -1,311 +0,0 @@
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread::{self, JoinHandle},
time::Duration,
};
use log::{debug, error};
use wdf_umdf::{
IddCxSwapChainFinishedProcessingFrame, IddCxSwapChainReleaseAndAcquireBuffer2,
IddCxSwapChainSetDevice, WdfObjectDelete,
};
use wdf_umdf_sys::{
HANDLE, IDARG_IN_RELEASEANDACQUIREBUFFER2, IDARG_IN_SWAPCHAINSETDEVICE,
IDARG_OUT_RELEASEANDACQUIREBUFFER2, IDDCX_SWAPCHAIN, NTSTATUS, WAIT_TIMEOUT, WDFOBJECT,
};
use windows::{
core::{w, Interface},
Win32::{
Foundation::HANDLE as WHANDLE,
Graphics::{
Direct3D11::ID3D11Texture2D,
Dxgi::{IDXGIDevice, IDXGIResource},
},
System::Threading::{
AvRevertMmThreadCharacteristics, AvSetMmThreadCharacteristicsW, WaitForSingleObject,
},
},
};
use crate::{
direct_3d_device::Direct3DDevice,
frame_transport::{
dbg_frame, dbg_header_attempt, dbg_run_core_entry, dbg_set_target, FramePublisher,
},
helpers::Sendable,
};
pub struct SwapChainProcessor {
terminate: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
}
unsafe impl Send for SwapChainProcessor {}
unsafe impl Sync for SwapChainProcessor {}
impl SwapChainProcessor {
pub fn new() -> Self {
Self {
terminate: Arc::new(AtomicBool::new(false)),
thread: None,
}
}
pub fn run(
&mut self,
swap_chain: IDDCX_SWAPCHAIN,
device: Arc<Direct3DDevice>,
available_buffer_event: HANDLE,
target_id: u32,
render_luid_low: u32,
render_luid_high: i32,
) {
let available_buffer_event = unsafe { Sendable::new(available_buffer_event) };
let swap_chain = unsafe { Sendable::new(swap_chain) };
let terminate = self.terminate.clone();
let join_handle = thread::spawn(move || {
// It is very important to prioritize this thread by making use of the Multimedia Scheduler Service.
// It will intelligently prioritize the thread for improved throughput in high CPU-load scenarios.
let mut av_task = 0u32;
let res = unsafe { AvSetMmThreadCharacteristicsW(w!("Distribution"), &mut av_task) };
let Ok(av_handle) = res else {
error!("Failed to prioritize thread: {res:?}");
return;
};
Self::run_core(
*swap_chain,
&device,
*available_buffer_event,
&terminate,
target_id,
render_luid_low,
render_luid_high,
);
error!("run_core RETURNED (target={target_id}) — deleting swap-chain, device drops next");
let res = unsafe { WdfObjectDelete(*swap_chain as WDFOBJECT) };
if let Err(e) = res {
error!("Failed to delete wdf object: {e:?}");
return;
}
// Revert the thread to normal once it's done
let res = unsafe { AvRevertMmThreadCharacteristics(av_handle) };
if let Err(e) = res {
error!("Failed to revert prioritize thread: {e:?}");
}
});
self.thread = Some(join_handle);
}
fn run_core(
swap_chain: IDDCX_SWAPCHAIN,
device: &Direct3DDevice,
available_buffer_event: HANDLE,
terminate: &AtomicBool,
target_id: u32,
render_luid_low: u32,
render_luid_high: i32,
) {
// P2 direct frame push: lazily ATTACH to the HOST-created shared ring. The restricted UMDF
// token can't create named objects, so the host creates the header + event + textures and we
// only OPEN them once they appear (`try_open`). Until then we just drain — exactly the P1
// behaviour — so a non-IDD-push session never stalls. Retried every ~30 frames.
let mut publisher: Option<FramePublisher> = None;
let mut frames_since_try: u32 = u32::MAX; // attach attempt on the first acquired frame
// Bring-up debug: prove run_core ran + record the target/render LUID we'll name objects with.
dbg_run_core_entry();
dbg_set_target(target_id, render_luid_low, render_luid_high);
// SetDevice fails (0x887A0026, FACILITY_DXGI) when the monitor briefly flaps INACTIVE during
// topology activation — the OS unassigns + re-assigns the swap-chain, and a fresh run_core thread
// can lose the race to the unassign. Retry briefly so a stable re-assign binds the device instead
// of giving up on the first transient failure. `terminate` (set when the OS unassigns + drops the
// processor) breaks us out promptly.
// Cast to IDXGIDevice ONCE and BORROW it to the swap-chain across all retries. The previous
// code re-cast + `into_raw()`'d on EVERY attempt — and a flapping monitor fails several
// attempts per session — so each failure orphaned one IDXGIDevice reference, pinning the D3D
// device so it (and its ~dozen D3D worker threads + tens of MB of VRAM) was NEVER freed when
// the processor dropped. That leaked ~71 threads / ~57 MB VRAM per reconnect until the driver
// choked and sessions fell to 0 bytes. `as_raw()` keeps our single reference (released right
// after the loop); IddCx AddRefs its own on success, and `device` keeps the object alive for
// the drain loop regardless.
let dxgi_device = match device.device.cast::<IDXGIDevice>() {
Ok(d) => d,
Err(e) => {
error!("Failed to cast ID3D11Device to IDXGIDevice: {e:?}");
return;
}
};
let set_device = IDARG_IN_SWAPCHAINSETDEVICE {
pDevice: dxgi_device.as_raw().cast(),
};
let mut set_ok = false;
let mut terminated = false;
for attempt in 0..60u32 {
if terminate.load(Ordering::Relaxed) {
error!("run_core: terminated during SetDevice (attempt {attempt}, target={target_id})");
terminated = true;
break;
}
let res = unsafe { IddCxSwapChainSetDevice(swap_chain, &set_device) };
if res.is_ok() {
set_ok = true;
error!("run_core: SetDevice OK (target={target_id}, attempt={attempt}) — entering drain loop");
break;
}
if attempt == 0 {
debug!("run_core: SetDevice attempt 0 failed ({res:?}) — retrying up to 60x@50ms (monitor may be flapping)");
}
thread::sleep(Duration::from_millis(50));
}
// Release our borrowed device reference — IddCx holds its own now, or we gave up. (Explicit
// drop so NLL can't release it mid-loop while the swap-chain still references the raw ptr.)
drop(dxgi_device);
if !set_ok {
if !terminated {
error!("run_core: SetDevice never succeeded after retries (target={target_id}) — giving up");
}
return;
}
let mut logged_pending = false;
let mut logged_frame = false;
loop {
// Check terminate at the TOP, every iteration. The success branch below does NOT re-check
// it, so during a CONTINUOUS frame burst (DWM rendering the freshly-activated desktop) a
// thread that the OS unassigns — or that `free_swap_chain_processor` is dropping — never
// sees the flag and loops on, pinning its D3D device (and ~36 NVIDIA worker threads). That
// is THE reconnect leak: it only reproduced at full speed, because cdb's pacing forced
// E_PENDING gaps (which DO check terminate) and masked it. Without this, `SwapChainProcessor::drop`'s
// join can also block until the burst ends.
if terminate.load(Ordering::Relaxed) {
break;
}
// The host recreates the shared ring (new format) mid-session when the display's HDR mode
// flips — it bumps the header generation. Detect that and drop the publisher so we re-attach
// to the new-format textures below; otherwise we'd keep CopyResource'ing into the stale ring,
// whose format now mismatches the surface → the publish() format-guard drops every frame and
// the stream freezes until the next swap-chain recreate.
if publisher.as_ref().is_some_and(FramePublisher::is_stale) {
publisher = None;
frames_since_try = u32::MAX; // re-attach immediately
}
// Lazy-attach (rate-limited) at the loop TOP so we keep trying even while the display is
// idle (E_PENDING / no frames presented yet), not only when a frame is acquired. `try_open`
// is a cheap OpenFileMapping that fails fast until the host has created the ring.
if publisher.is_none() {
if frames_since_try >= 30 {
frames_since_try = 0;
match FramePublisher::try_open(
target_id,
render_luid_low,
render_luid_high,
&device.device,
&device.device_context,
) {
Ok(p) => {
dbg_header_attempt(0, true);
publisher = Some(p);
}
Err(e) => dbg_header_attempt(e.code().0 as u32, false),
}
} else {
frames_since_try += 1;
}
}
// B2: ...Buffer2 is required once CAN_PROCESS_FP16 is set. AcquireSystemMemoryBuffer=FALSE
// keeps the GPU surface (out.MetaData.pSurface). The surface format varies per-frame —
// FP16 (R16G16B16A16_FLOAT) in HDR, BGRA in SDR — and the publisher's format guard handles
// a frame that doesn't match the ring until B3 makes the ring FP16.
let mut in_args = IDARG_IN_RELEASEANDACQUIREBUFFER2 {
#[allow(clippy::cast_possible_truncation)]
Size: std::mem::size_of::<IDARG_IN_RELEASEANDACQUIREBUFFER2>() as u32,
AcquireSystemMemoryBuffer: 0,
};
let mut buffer = IDARG_OUT_RELEASEANDACQUIREBUFFER2::default();
let hr: NTSTATUS = unsafe {
IddCxSwapChainReleaseAndAcquireBuffer2(swap_chain, &mut in_args, &mut buffer).into()
};
#[allow(clippy::items_after_statements)]
const E_PENDING: u32 = 0x8000_000A;
if u32::from(hr) == E_PENDING {
if !logged_pending {
error!("run_core: E_PENDING (target={target_id}) — swap-chain valid but DWM has composed NO frame yet");
logged_pending = true;
}
let wait_result =
unsafe { WaitForSingleObject(WHANDLE(available_buffer_event.cast()), 16).0 };
// thread requested an end
let should_terminate = terminate.load(Ordering::Relaxed);
if should_terminate {
break;
}
// WAIT_OBJECT_0 | WAIT_TIMEOUT
if matches!(wait_result, 0 | WAIT_TIMEOUT) {
// We have a new buffer, so try the AcquireBuffer again
continue;
}
// The wait was cancelled or something unexpected happened
break;
} else if hr.is_success() {
if !logged_frame {
error!("run_core: FIRST FRAME acquired (target={target_id}) — DWM IS compositing the virtual display!");
logged_frame = true;
}
dbg_frame(); // bring-up: prove frames actually flow (vs an idle display)
// This is the most performance-critical section of code in an IddCx driver. It's important that whatever
// is done with the acquired surface be finished as quickly as possible.
//
// P2: copy the acquired surface into the shared ring BEFORE FinishedProcessingFrame
// (the surface is valid until the next ReleaseAndAcquire). The pointer is BORROWED —
// `from_raw_borrowed` does not take IddCx's refcount — and the GPU-side copy is ordered
// before the consumer via the slot keyed mutex. (Attach happens at the loop top.)
if let Some(pub_) = publisher.as_mut() {
let raw = buffer.MetaData.pSurface as *mut core::ffi::c_void;
if !raw.is_null() {
if let Some(res) = unsafe { IDXGIResource::from_raw_borrowed(&raw) } {
if let Ok(tex) = res.cast::<ID3D11Texture2D>() {
pub_.publish(&tex);
}
}
}
}
let hr = unsafe { IddCxSwapChainFinishedProcessingFrame(swap_chain) };
if hr.is_err() {
break;
}
} else {
// The swap-chain was likely abandoned (e.g. DXGI_ERROR_ACCESS_LOST), so exit the processing loop
break;
}
}
}
}
impl Drop for SwapChainProcessor {
fn drop(&mut self) {
if let Some(handle) = self.thread.take() {
// send signal to end thread
self.terminate.store(true, Ordering::Relaxed);
// wait until thread is finished
_ = handle.join();
}
}
}
@@ -1,4 +0,0 @@
[toolchain]
channel = "nightly-2024-07-26"
components = ["rustfmt", "clippy"]
profile = "minimal"
@@ -1,17 +0,0 @@
[package]
name = "wdf-umdf-sys"
version = "0.1.0"
edition = "2021"
[lints]
workspace = true
[dependencies]
paste = "1.0.15"
bytemuck = "1.19.0"
thiserror = "2.0.3"
[build-dependencies]
bindgen = "0.70.1"
thiserror = "2.0.3"
winreg = "0.52.0"
@@ -1,278 +0,0 @@
use std::env;
use std::fmt::{self, Display};
use std::path::{Path, PathBuf};
use bindgen::Abi;
use winreg::enums::HKEY_LOCAL_MACHINE;
use winreg::RegKey;
const UMDF_V: &str = "2.31";
// Bumped 1.4 -> 1.10 for HDR/FP16 support (IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16,
// IddCxSwapChainReleaseAndAcquireBuffer2, the *2 mode/metadata DDIs). 1.10 is a superset of 1.4, so
// existing call sites keep working; the new HDR DDIs become available to bind.
const IDDCX_V: &str = "1.10";
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error("cannot find the directory")]
DirectoryNotFound,
}
/// Retrieves the path to the Windows Kits directory. The default should be
/// `C:\Program Files (x86)\Windows Kits\10`.
///
/// # Errors
/// Returns IO error if failed
fn get_windows_kits_dir() -> Result<PathBuf, Error> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let key = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
let dir: String = hklm.open_subkey(key)?.get_value("KitsRoot10")?;
Ok(dir.into())
}
#[derive(Clone, Copy, PartialEq)]
enum DirectoryType {
Include,
Library,
}
#[derive(Clone, Copy, PartialEq)]
enum Target {
X86_64,
ARM64,
}
impl Default for Target {
fn default() -> Self {
let target = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
match &*target {
"x86_64" => Self::X86_64,
"aarch64" => Self::ARM64,
_ => unimplemented!("{target} arch is unsupported"),
}
}
}
impl Display for Target {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Target::X86_64 => f.write_str("x64"),
Target::ARM64 => f.write_str("arm64"),
}
}
}
fn get_base_path<S: AsRef<Path>>(dir_type: DirectoryType, subs: &[S]) -> Result<PathBuf, Error> {
let mut dir = get_windows_kits_dir()?.join(match dir_type {
DirectoryType::Include => "Include",
DirectoryType::Library => "Lib",
});
dir.extend(subs);
if !dir.is_dir() {
return Err(Error::DirectoryNotFound);
}
Ok(dir)
}
fn get_sdk_path<S: AsRef<Path>>(dir_type: DirectoryType, subs: &[S]) -> Result<PathBuf, Error> {
// We first append lib to the path and read the directory..
let dir = get_windows_kits_dir()?
.join(match dir_type {
DirectoryType::Include => "Include",
DirectoryType::Library => "Lib",
})
.read_dir()?;
// In the lib directory we may have one or more directories named after the version of Windows,
// we will be looking for the highest version number.
let mut dir = dir
.filter_map(Result::ok)
.map(|dir| dir.path())
.filter(|dir| {
let is_sdk = dir
.components()
.last()
.and_then(|c| c.as_os_str().to_str())
.map_or(false, |c| c.starts_with("10."));
let mut sub_dir = dir.clone();
sub_dir.extend(subs);
is_sdk && sub_dir.is_dir()
})
.max()
.ok_or_else(|| Error::DirectoryNotFound)?;
dir.extend(subs);
if !dir.is_dir() {
return Err(Error::DirectoryNotFound);
}
// Finally append um to the path to get the path to the user mode libraries.
Ok(dir)
}
/// Retrieves the path to the user mode libraries. The path may look something like:
/// `C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\um`.
///
/// # Errors
/// Returns IO error if failed
fn get_um_dir(dir_type: DirectoryType) -> Result<PathBuf, Error> {
let target = Target::default().to_string();
let binding = &["um", &target];
let subs: &[&str] = match dir_type {
DirectoryType::Include => &["um"],
DirectoryType::Library => binding,
};
let dir = get_sdk_path(dir_type, subs)?;
Ok(dir)
}
/// # Errors
/// Returns IO error if failed
fn get_umdf_dir(dir_type: DirectoryType) -> Result<PathBuf, Error> {
match dir_type {
DirectoryType::Include => get_base_path(dir_type, &["wdf", "umdf", UMDF_V]),
DirectoryType::Library => get_base_path(
dir_type,
&["wdf", "umdf", &Target::default().to_string(), UMDF_V],
),
}
}
/// Retrieves the path to the shared headers. The path may look something like:
/// `C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\shared`.
///
/// # Errors
/// Returns IO error if failed
fn get_shared_dir() -> Result<PathBuf, Error> {
let dir = get_sdk_path(DirectoryType::Include, &["shared"])?;
Ok(dir)
}
fn build_dir() -> PathBuf {
PathBuf::from(
std::env::var_os("OUT_DIR").expect("the environment variable OUT_DIR is undefined"),
)
}
fn generate() {
// Find the include directory containing the user headers.
let include_um_dir = get_um_dir(DirectoryType::Include).unwrap();
let lib_um_dir = get_um_dir(DirectoryType::Library).unwrap();
let shared = get_shared_dir().unwrap();
println!("cargo:rustc-link-search={}", lib_um_dir.display());
// Tell Cargo to re-run this if src/wrapper.h gets changed.
println!("cargo:rerun-if-changed=c/wrapper.h");
//
// UMDF
//
let umdf_lib_dir = get_umdf_dir(DirectoryType::Library).unwrap();
println!("cargo:rustc-link-search={}", umdf_lib_dir.display());
let wdf_include_dir = get_umdf_dir(DirectoryType::Include).unwrap();
// need to link to umdf lib
println!("cargo:rustc-link-lib=static=WdfDriverStubUm");
//
// IDDCX
//
// The IddCx import lib lives only under the WDK's SDK version (e.g. 10.0.26100.0); a newer base
// SDK installed alongside it (e.g. 10.0.28000.0) has um\x64 but no iddcx subdir, so picking the
// max um\x64 version (lib_um_dir) misses it. Resolve by the version that actually contains
// iddcx — the same way the IddCx.h header path is resolved below.
let iddcx_lib_dir = get_sdk_path(
DirectoryType::Library,
&["um", &Target::default().to_string(), "iddcx", IDDCX_V],
)
.unwrap();
println!("cargo:rustc-link-search={}", iddcx_lib_dir.display());
// need to link to iddcx lib
println!("cargo:rustc-link-lib=static=IddCxStub");
//
// REST
//
// Get the build directory.
let out_path = build_dir();
// Generate the bindings
let mut builder = bindgen::Builder::default()
.derive_debug(false)
.layout_tests(false)
.default_enum_style(bindgen::EnumVariation::NewType {
is_bitfield: false,
is_global: false,
})
.merge_extern_blocks(true)
.header("c/wrapper.h")
.header(
get_sdk_path(DirectoryType::Include, &["um", "iddcx", IDDCX_V])
.unwrap()
.join("IddCx.h")
.to_string_lossy()
.to_string(),
)
// general um includes
.clang_arg(format!("-I{}", include_um_dir.display()))
// umdf includes
.clang_arg(format!("-I{}", wdf_include_dir.display()))
.clang_arg(format!("-I{}", shared.display()))
// because aarch64 needs to find excpt.h
.clang_arg(format!(
"-I{}",
get_sdk_path(DirectoryType::Include, &["km", "crt"])
.unwrap()
.display()
))
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.blocklist_type("_?P?IMAGE_TLS_DIRECTORY.*")
// we will use our own custom type
.blocklist_item("NTSTATUS")
.blocklist_item("IddMinimumVersionRequired")
.blocklist_item("WdfMinimumVersionRequired")
.clang_arg("--language=c++")
.clang_arg("-fms-compatibility")
.clang_arg("-fms-extensions")
.override_abi(Abi::CUnwind, ".*")
.generate_cstr(true)
.derive_default(true);
let defines = match Target::default() {
Target::X86_64 => ["AMD64", "_AMD64_"],
Target::ARM64 => ["ARM64", "_ARM64_"],
};
for define in defines {
builder = builder.clang_arg(format!("-D{define}"));
}
// generate
let umdf = builder.generate().unwrap();
// Write the bindings to the $OUT_DIR/bindings.rs file.
umdf.write_to_file(out_path.join("umdf.rs")).unwrap();
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
generate();
}
@@ -1,22 +0,0 @@
#include <Windows.h>
/**
*
* UMDF
*
*/
#define WDF_STUB
#include <wdf.h>
/**
*
* IDCXX
*
*/
#define IDD_STUB
// handled in build.rs
// #include <iddcx\1.4\IddCx.h>
@@ -1,17 +0,0 @@
#![allow(unsafe_op_in_unsafe_fn)]
#![allow(clippy::all)]
#![allow(clippy::pedantic)]
#![allow(clippy::restriction)]
// stand-in type replacing NTSTATUS in the bindings
use crate::NTSTATUS;
include!(concat!(env!("OUT_DIR"), "/umdf.rs"));
// required for some macros
unsafe impl Send for _WDF_OBJECT_CONTEXT_TYPE_INFO {}
unsafe impl Sync for _WDF_OBJECT_CONTEXT_TYPE_INFO {}
// fails to build without this symbol
#[no_mangle]
pub static IddMinimumVersionRequired: ULONG = 4;
@@ -1,211 +0,0 @@
#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals, unused)]
mod bindings;
mod ntstatus;
use std::fmt::{self, Display};
pub use bindings::*;
pub use ntstatus::*;
pub use paste::paste;
#[macro_export]
macro_rules! WdfIsFunctionAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::WdfClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let fn_count = unsafe { $crate::WdfFunctionCount };
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h#L126
$crate::paste! {
// index is always positive, see
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h
const FN_INDEX: u32 = $crate::WDFFUNCENUM::[<$name TableIndex>].0 as u32;
FN_INDEX < $crate::WDF_ALWAYS_AVAILABLE_FUNCTION_COUNT
|| !higher || FN_INDEX < fn_count
}
}};
}
#[macro_export]
macro_rules! WdfIsStructureAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::WdfClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let struct_count = unsafe { $crate::WdfStructureCount };
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h#L141
$crate::paste! {
// index is always positive, see
// https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h
const STRUCT_INDEX: u32 = $crate::WDFSTRUCTENUM::[<INDEX_ $name>].0 as u32;
!higher || STRUCT_INDEX < struct_count
}
}};
}
#[macro_export]
macro_rules! IddCxIsFunctionAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::IddClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let fn_count = unsafe { $crate::IddFunctionCount };
$crate::paste! {
const FN_INDEX: u32 = $crate::IDDFUNCENUM::[<$name TableIndex>].0 as u32;
FN_INDEX < $crate::IDD_ALWAYS_AVAILABLE_FUNCTION_COUNT
|| !higher || FN_INDEX < fn_count
}
}};
}
#[macro_export]
macro_rules! IddCxIsStructureAvailable {
($name:ident) => {{
// SAFETY: We only ever do read access
let higher = unsafe { $crate::IddClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access
let struct_count = unsafe { $crate::IddStructureCount };
$crate::paste! {
const STRUCT_INDEX: u32 = $crate::IDDSTRUCTENUM::[<INDEX_ $name>].0 as u32;
!higher || STRUCT_INDEX < struct_count
}
}};
}
macro_rules! WDF_STRUCTURE_SIZE {
($name:ty) => {
u32::try_from(::core::mem::size_of::<$name>()).expect("size is correct")
};
}
#[macro_export]
macro_rules! WDF_NO_HANDLE {
() => {
::core::ptr::null_mut()
};
}
#[macro_export]
macro_rules! WDF_NO_OBJECT_ATTRIBUTES {
() => {
::core::ptr::null_mut()
};
}
#[macro_export]
macro_rules! WDF_OBJECT_ATTRIBUTES_SET_CONTEXT_TYPE {
($attr:ident, $context_type:ident) => {
$attr.ContextTypeInfo = $context_type;
};
}
impl WDF_OBJECT_ATTRIBUTES {
/// Initializes the [`WDF_OBJECT_ATTRIBUTES`] structure
/// <https://github.com/microsoft/Windows-Driver-Frameworks/blob/a94b8c30dad524352fab90872aefc83920b98e56/src/publicinc/wdf/umdf/2.33/wdfobject.h#L136/>
///
/// Sets
/// - `ExecutionLevel` to [`WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent`]
/// - `SynchronizationScope` to [`WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent`]
#[must_use]
pub fn init() -> Self {
// SAFETY: All fields are zero-able
let mut attributes: Self = unsafe { ::core::mem::zeroed() };
attributes.Size = WDF_STRUCTURE_SIZE!(Self);
attributes.SynchronizationScope =
WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent;
attributes.ExecutionLevel = WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent;
attributes
}
#[must_use]
pub fn init_context_type(context_type: &_WDF_OBJECT_CONTEXT_TYPE_INFO) -> Self {
let mut attr = Self::init();
WDF_OBJECT_ATTRIBUTES_SET_CONTEXT_TYPE!(attr, context_type);
attr
}
}
impl WDF_DRIVER_CONFIG {
/// Initializes the [`WDF_DRIVER_CONFIG`] structure
/// <https://github.com/microsoft/Windows-Driver-Frameworks/blob/a94b8c30dad524352fab90872aefc83920b98e56/src/publicinc/wdf/umdf/2.33/wdfdriver.h#L134/>
#[must_use]
pub fn init(EvtDriverDeviceAdd: PFN_WDF_DRIVER_DEVICE_ADD) -> Self {
// SAFETY: All fields are zero-able
let mut config: Self = unsafe { core::mem::zeroed() };
config.Size = WDF_STRUCTURE_SIZE!(Self);
config.EvtDriverDeviceAdd = EvtDriverDeviceAdd;
config
}
}
impl WDF_PNPPOWER_EVENT_CALLBACKS {
/// Initializes the [`WDF_PNPPOWER_EVENT_CALLBACKS`] structure
/// <https://github.com/microsoft/Windows-Driver-Frameworks/blob/a94b8c30dad524352fab90872aefc83920b98e56/src/publicinc/wdf/umdf/2.33/wdfdevice.h#L1278/>
#[must_use]
pub fn init() -> Self {
// SAFETY: All fields are zero-able
let mut callbacks: Self = unsafe { core::mem::zeroed() };
callbacks.Size = WDF_STRUCTURE_SIZE!(Self);
callbacks
}
}
/// If this returns None, the struct is NOT available to be used
macro_rules! IDD_STRUCTURE_SIZE {
($name:ty) => {{
// SAFETY: We only ever do read access, copy is fine
let higher = unsafe { IddClientVersionHigherThanFramework } != 0;
// SAFETY: We only ever do read access, copy is fine
let struct_count = unsafe { IddStructureCount };
if higher {
// as u32 is fine, since there's no way there's > 4 billion structs
const STRUCT_INDEX: u32 =
$crate::paste! { IDDSTRUCTENUM::[<INDEX_ $name:upper>].0 as u32 };
// SAFETY: A pointer to a [size_t], copying the pointer is ok
let ptr = unsafe { IddStructures };
if STRUCT_INDEX < struct_count {
// SAFETY: we validated struct index is able to be accessed
let ptr = unsafe { ptr.add(STRUCT_INDEX as usize) };
// SAFETY: So it's ok to read
u32::try_from(unsafe { ptr.read() }).ok()
} else {
// struct CANNOT be used
None
}
} else {
u32::try_from(::std::mem::size_of::<$name>()).ok()
}
}};
}
impl IDD_CX_CLIENT_CONFIG {
#[must_use]
pub fn init() -> Option<Self> {
// SAFETY: All fields are zero-able
let mut config: Self = unsafe { core::mem::zeroed() };
config.Size = IDD_STRUCTURE_SIZE!(IDD_CX_CLIENT_CONFIG)?;
Some(config)
}
}
File diff suppressed because it is too large Load Diff
@@ -1,12 +0,0 @@
[package]
name = "wdf-umdf"
version = "0.1.0"
edition = "2021"
[lints]
workspace = true
[dependencies]
wdf-umdf-sys = { path = "../wdf-umdf-sys" }
paste = "1.0.15"
thiserror = "2.0.3"
@@ -1,344 +0,0 @@
#![allow(non_snake_case)]
#![allow(clippy::missing_errors_doc)]
use std::sync::OnceLock;
use wdf_umdf_sys::{
IDARG_IN_ADAPTERSETRENDERADAPTER, IDARG_IN_ADAPTER_INIT, IDARG_IN_MONITORCREATE,
IDARG_IN_QUERY_HWCURSOR, IDARG_IN_SETUP_HWCURSOR, IDARG_IN_SWAPCHAINSETDEVICE,
IDARG_OUT_ADAPTER_INIT, IDARG_OUT_MONITORARRIVAL, IDARG_OUT_MONITORCREATE,
IDARG_IN_RELEASEANDACQUIREBUFFER2, IDARG_OUT_QUERY_HWCURSOR, IDARG_OUT_RELEASEANDACQUIREBUFFER,
IDARG_OUT_RELEASEANDACQUIREBUFFER2, IDDCX_ADAPTER, IDDCX_MONITOR,
IDDCX_SWAPCHAIN, IDD_CX_CLIENT_CONFIG, NTSTATUS, WDFDEVICE, WDFDEVICE_INIT,
};
#[derive(Copy, Clone, Debug, thiserror::Error)]
pub enum IddCxError {
#[error("{0}")]
IddCxFunctionNotAvailable(&'static str),
#[error("{0}")]
CallFailed(NTSTATUS),
#[error("{0}")]
NtStatus(NTSTATUS),
}
impl From<IddCxError> for NTSTATUS {
fn from(value: IddCxError) -> Self {
#[allow(clippy::enum_glob_use)]
use IddCxError::*;
match value {
IddCxFunctionNotAvailable(_) => Self::STATUS_NOT_FOUND,
CallFailed(status) => status,
NtStatus(n) => n,
}
}
}
impl From<NTSTATUS> for IddCxError {
fn from(value: NTSTATUS) -> Self {
IddCxError::CallFailed(value)
}
}
impl From<i32> for IddCxError {
fn from(val: i32) -> Self {
IddCxError::NtStatus(NTSTATUS(val))
}
}
// void IddCx functions return () on success; required by the call macro's error arm but never an error.
impl From<()> for IddCxError {
fn from(_: ()) -> Self {
IddCxError::NtStatus(NTSTATUS(0))
}
}
macro_rules! IddCxCall {
($name:ident ( $($args:expr),* )) => {
IddCxCall!(false, $name($($args),*))
};
($other_is_error:expr, $name:ident ( $($args:expr),* )) => {{
static CACHED_FN: OnceLock<
Result<
::paste::paste!(::wdf_umdf_sys::[<PFN_ $name:upper>]),
IddCxError
>
> = OnceLock::new();
let f = CACHED_FN.get_or_init(|| {
::paste::paste! {
const FN_INDEX: usize = ::wdf_umdf_sys::IDDFUNCENUM::[<$name TableIndex>].0 as usize;
// validate that wdf function can be used
let is_available = ::wdf_umdf_sys::IddCxIsFunctionAvailable!($name);
if is_available {
// SAFETY: Only immutable accesses are done to this
// The underlying array is Copy, so we call as_ptr() directly on it inside block
let fn_table = unsafe { ::wdf_umdf_sys::IddFunctions.as_ptr() };
// SAFETY: Ensured that this is present by if condition from `WdfIsFunctionAvailable!`
let f = unsafe {
fn_table.add(FN_INDEX)
.cast::<::wdf_umdf_sys::[<PFN_ $name:upper>]>()
};
// SAFETY: Ensured that this is present by if condition from `IddIsFunctionAvailable!`
let f = unsafe { f.read() };
Ok(f)
} else {
Err($crate::IddCxError::IddCxFunctionNotAvailable(concat!(stringify!($name), " is not available")))
}
}
}).clone()?;
// SAFETY: Above: If it's Ok, then it's guaranteed to be Some(fn)
let f = unsafe { f.unwrap_unchecked() };
// SAFETY: Pointer to globals is always immutable
let globals = unsafe { ::wdf_umdf_sys::IddDriverGlobals };
// SAFETY: None. User is responsible for safety and must use their own unsafe block
let result = unsafe { f(globals, $($args),*) };
if $crate::is_nt_error(&result, $other_is_error) {
Err(result.into())
} else {
Ok(result.into())
}
}};
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn IddCxDeviceInitConfig(
// in, out
DeviceInit: &mut WDFDEVICE_INIT,
// in
Config: &IDD_CX_CLIENT_CONFIG,
) -> Result<NTSTATUS, IddCxError> {
IddCxCall! {
IddCxDeviceInitConfig(
DeviceInit,
Config
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn IddCxDeviceInitialize(
// in
Device: WDFDEVICE,
) -> Result<NTSTATUS, IddCxError> {
IddCxCall! {
IddCxDeviceInitialize(
Device
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn IddCxAdapterInitAsync(
// in
pInArgs: &IDARG_IN_ADAPTER_INIT,
// out
pOutArgs: &mut IDARG_OUT_ADAPTER_INIT,
) -> Result<NTSTATUS, IddCxError> {
IddCxCall! {
IddCxAdapterInitAsync(
pInArgs,
pOutArgs
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorCreate(
// in
AdapterObject: IDDCX_ADAPTER,
// in
pInArgs: &IDARG_IN_MONITORCREATE,
// out
pOutArgs: &mut IDARG_OUT_MONITORCREATE,
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorCreate(
AdapterObject,
pInArgs,
pOutArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorArrival(
// in
MonitorObject: IDDCX_MONITOR,
// out
pOutArgs: &mut IDARG_OUT_MONITORARRIVAL,
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorArrival(
MonitorObject,
pOutArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxSwapChainSetDevice(
// in
SwapChainObject: IDDCX_SWAPCHAIN,
// in
pInArgs: &IDARG_IN_SWAPCHAINSETDEVICE
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
true,
IddCxSwapChainSetDevice(
SwapChainObject,
pInArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxSwapChainReleaseAndAcquireBuffer(
// in
SwapChainObject: IDDCX_SWAPCHAIN,
// out
pOutArgs: &mut IDARG_OUT_RELEASEANDACQUIREBUFFER
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
true,
IddCxSwapChainReleaseAndAcquireBuffer(
SwapChainObject,
pOutArgs
)
)
}
/// IddCx 1.10 HDR variant — required once the adapter sets `CAN_PROCESS_FP16`. Provides per-frame
/// `IDDCX_METADATA2` (surface colour space, HDR metadata, SDR white level).
///
/// # Safety
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxSwapChainReleaseAndAcquireBuffer2(
// in
SwapChainObject: IDDCX_SWAPCHAIN,
// in
pInArgs: &mut IDARG_IN_RELEASEANDACQUIREBUFFER2,
// out
pOutArgs: &mut IDARG_OUT_RELEASEANDACQUIREBUFFER2
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
true,
IddCxSwapChainReleaseAndAcquireBuffer2(
SwapChainObject,
pInArgs,
pOutArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxSwapChainFinishedProcessingFrame(
// in
SwapChainObject: IDDCX_SWAPCHAIN
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
true,
IddCxSwapChainFinishedProcessingFrame(
SwapChainObject
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorDeparture(
// in
MonitorObject: IDDCX_MONITOR
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorDeparture(
MonitorObject
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn IddCxAdapterSetRenderAdapter(
// in
AdapterObject: IDDCX_ADAPTER,
// in
pInArgs: *const IDARG_IN_ADAPTERSETRENDERADAPTER,
) -> Result<(), IddCxError> {
IddCxCall!(IddCxAdapterSetRenderAdapter(AdapterObject, pInArgs))
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorSetupHardwareCursor(
// in
MonitorObject: IDDCX_MONITOR,
// in
pInArgs: &IDARG_IN_SETUP_HWCURSOR
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorSetupHardwareCursor(
MonitorObject,
pInArgs
)
)
}
/// # Safety
///
/// None. User is responsible for safety.
#[rustfmt::skip]
pub unsafe fn IddCxMonitorQueryHardwareCursor(
// in
MonitorObject: IDDCX_MONITOR,
// in
pInArgs: &IDARG_IN_QUERY_HWCURSOR,
// out
pOutArgs: &mut IDARG_OUT_QUERY_HWCURSOR
) -> Result<NTSTATUS, IddCxError> {
IddCxCall!(
IddCxMonitorQueryHardwareCursor(
MonitorObject,
pInArgs,
pOutArgs
)
)
}
@@ -1,30 +0,0 @@
mod iddcx;
mod wdf;
use std::any::Any;
pub use paste::paste;
pub use iddcx::*;
pub use wdf::*;
pub use wdf_umdf_sys;
use wdf_umdf_sys::NTSTATUS;
/// Used for the macros so they can correctly convert a functions result
fn is_nt_error(val: &dyn Any, other_is_error: bool) -> bool {
if let Some(status) = val.downcast_ref::<NTSTATUS>() {
return !status.is_success();
}
// other errors which may not be error codes, but may also be
// such as HRESULT == i32
if other_is_error {
if let Some(status) = val.downcast_ref::<i32>() {
let status = NTSTATUS(*status);
return !status.is_success();
}
}
false
}
@@ -1,667 +0,0 @@
#![allow(non_snake_case)]
#![allow(clippy::missing_errors_doc)]
use std::ffi::c_void;
use std::sync::OnceLock;
use wdf_umdf_sys::{
DEVPROPTYPE, GUID, NTSTATUS, PCUNICODE_STRING, PCWDF_OBJECT_CONTEXT_TYPE_INFO, PDRIVER_OBJECT,
POOL_TYPE, PVOID, PWDFDEVICE_INIT, PWDF_DRIVER_CONFIG, PWDF_OBJECT_ATTRIBUTES, ULONG_PTR,
WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFOBJECT, WDFREQUEST, WDF_DEVICE_FAILED_ACTION, WDF_NO_HANDLE,
WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES, _WDF_DEVICE_PROPERTY_DATA,
_WDF_PNPPOWER_EVENT_CALLBACKS,
};
#[derive(Copy, Clone, Debug, thiserror::Error)]
pub enum WdfError {
#[error("{0}")]
WdfFunctionNotAvailable(&'static str),
#[error("{0}")]
CallFailed(NTSTATUS),
#[error("Failed to upgrade Arc pointer")]
UpgradeFailed,
#[error("Failed to lock")]
LockFailed,
#[error("Unknown")]
Unknown,
// this is required for success status for ()
#[error("This is not an error, ignore it")]
_Success,
}
impl From<()> for WdfError {
fn from(_: ()) -> Self {
WdfError::_Success
}
}
impl From<*mut c_void> for WdfError {
fn from(_: *mut c_void) -> Self {
Self::Unknown
}
}
impl From<WdfError> for NTSTATUS {
fn from(value: WdfError) -> Self {
#[allow(clippy::enum_glob_use)]
use WdfError::*;
match value {
WdfFunctionNotAvailable(_) => Self::STATUS_NOT_FOUND,
CallFailed(status) => status,
UpgradeFailed => Self::STATUS_INVALID_HANDLE,
LockFailed => Self::STATUS_WAS_LOCKED,
Unknown => Self::STATUS_DRIVER_INTERNAL_ERROR,
_Success => 0.into(),
}
}
}
impl From<NTSTATUS> for WdfError {
fn from(value: NTSTATUS) -> Self {
WdfError::CallFailed(value)
}
}
macro_rules! WdfCall {
($name:ident ( $($args:expr),* )) => {
WdfCall!(false, $name($($args),*))
};
($other_is_error:expr, $name:ident ( $($args:expr),* )) => {{
static CACHED_FN: OnceLock<
Result<
::paste::paste!(::wdf_umdf_sys::[<PFN_ $name:upper>]),
WdfError
>
> = OnceLock::new();
let f = CACHED_FN.get_or_init(|| {
::paste::paste! {
const FN_INDEX: usize = ::wdf_umdf_sys::WDFFUNCENUM::[<$name TableIndex>].0 as usize;
// validate that wdf function can be used
let is_available = ::wdf_umdf_sys::WdfIsFunctionAvailable!($name);
if is_available {
// SAFETY: Only immutable accesses are done to this
let fn_table = unsafe { ::wdf_umdf_sys::WdfFunctions_02031 };
// SAFETY: Read-only, initialized by the time we use it, and checked to be in bounds
let f = unsafe {
fn_table
.add(FN_INDEX)
.cast::<::wdf_umdf_sys::[<PFN_ $name:upper>]>()
};
// SAFETY: Ensured that this is present by if condition from `WdfIsFunctionAvailable!`
let f = unsafe { f.read() };
Ok(f)
} else {
Err($crate::WdfError::WdfFunctionNotAvailable(concat!(stringify!($name), " is not available")))
}
}
}).clone()?;
// SAFETY: Above: If it's Ok, then it's guaranteed to be Some(fn)
let f = unsafe { f.unwrap_unchecked() };
// SAFETY: Pointer to globals is always immutable
let globals = unsafe { ::wdf_umdf_sys::WdfDriverGlobals };
// SAFETY: None. User is responsible for safety and must use their own unsafe block
let result = unsafe { f(globals, $($args),*) };
if $crate::is_nt_error(&result, $other_is_error) {
Err(result.into())
} else {
Ok(result.into())
}
}};
}
/// Unlike the official `WDF_DECLARE_CONTEXT_TYPE` macro, you only need to declare this on the actual data struct want to use
/// Safety is maintained through a `RwLock` of the underlying data
///
/// This generates associated fns `init`/`get`/`drop`/`get_type_info` on your `$context_type` with the same visibility
///
/// Example:
/// ```rust
/// pub struct IndirectDeviceContext {
/// device: WDFDEVICE,
/// }
///
/// impl IndirectDeviceContext {
/// pub fn new(device: WDFDEVICE) -> Self {
/// Self { device }
/// }
/// }
///
/// WDF_DECLARE_CONTEXT_TYPE!(pub IndirectDeviceContext);
///
/// // with a `device: WDFDEVICE`
/// let context = IndirectDeviceContext::new(device as WDFOBJECT);
/// IndirectDeviceContext::init(context);
/// // elsewhere
/// let mutable_access = IndirectDeviceContext::get_mut(device).unwrap();
/// ```
#[macro_export]
macro_rules! WDF_DECLARE_CONTEXT_TYPE {
($sv:vis $context_type:ident) => {
$crate::paste! {
// keep it in a mod block to disallow access to private types
#[allow(non_snake_case)]
mod [<WdfObject $context_type>] {
use super::$context_type;
// Require `T: Sync` for safety. User has to uphold the invariant themselves
#[repr(transparent)]
#[allow(non_camel_case_types)]
struct [<_WDF_ $context_type _STATIC_WRAPPER>]<T> {
cell: ::std::cell::UnsafeCell<$crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO>,
_phantom: ::std::marker::PhantomData<T>
}
// SAFETY: `T` impls Sync too
unsafe impl<T: Sync> Sync for [<_WDF_ $context_type _STATIC_WRAPPER>]<T> {}
// Unsure if C mutates this data, but it's in an unsafecell just in case
#[allow(non_upper_case_globals)]
static [<_WDF_ $context_type _TYPE_INFO>]: [<_WDF_ $context_type _STATIC_WRAPPER>]<$context_type> =
[<_WDF_ $context_type _STATIC_WRAPPER>] {
cell: ::std::cell::UnsafeCell::new(
$crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO {
Size: ::std::mem::size_of::<$crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO>() as u32,
ContextName: concat!(stringify!($context_type), "\0")
.as_ptr().cast::<::std::ffi::c_char>(),
ContextSize: ::std::mem::size_of::<[<WdfObject $context_type>]>(),
// SAFETY:
// StaticWrapper and UnsafeCell are both repr(transparent), so cast to underlying _WDF_OBJECT_CONTEXT_TYPE_INFO is ok
UniqueType: &[<_WDF_ $context_type _TYPE_INFO>] as *const _ as *const _,
EvtDriverGetUniqueContextType: ::std::option::Option::None,
}
),
_phantom: ::std::marker::PhantomData
};
/// Allows us to keep ONE main Arc allocation while handing out weak pointers to the rest of the clones.
/// In this way, we can drop the allocation by dropping 1 arc, while letting others still access it
enum ArcPointer<T> {
Strong(::std::sync::Arc<T>),
Weak(::std::sync::Weak<T>)
}
#[repr(transparent)]
struct [<WdfObject $context_type>](ArcPointer<::std::sync::RwLock<$context_type>>);
impl $context_type {
/// Initialize and place context into internal WdfObject
///
/// SAFETY:
/// - handle must be a fresh unused object with no data in its context already
/// - context type must already have been set up for handle
/// - Must be set only once regardless of the object. For all other objects, use clone_into()
$sv unsafe fn init(
self,
handle: $crate::wdf_umdf_sys::WDFOBJECT,
) -> ::std::result::Result<(), $crate::WdfError> {
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle, [<_WDF_ $context_type _TYPE_INFO>].cell.get())?
} as *mut ::std::mem::MaybeUninit<[<WdfObject $context_type>]>;
let context = &mut *context;
// Write to the memory location, making the data in it init
context.write(
[<WdfObject $context_type>](
ArcPointer::Strong(::std::sync::Arc::new(::std::sync::RwLock::new(self)))
)
);
Ok(())
}
/// Initialize handle's context and clone a Weak pointer to self context into it.
/// Internally, these are Arc's, so they will always point to the same data.
/// When the main Arc drops, none of these may access memory any longer
///
/// SAFETY:
/// - handle must be a fresh unused object with no data in its context already
/// - to_handle must have set context_type for this type via WDF_OBJECT_ATTRIBUTES when it was created
/// - to_handle must be a valid T
$sv unsafe fn clone_into(
&self,
handle: $crate::wdf_umdf_sys::WDFOBJECT
) -> ::std::result::Result<(), $crate::WdfError> {
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle, [<_WDF_ $context_type _TYPE_INFO>].cell.get())?
} as *mut ::std::mem::MaybeUninit<[<WdfObject $context_type>]>;
let context = &mut *context;
let from_context = unsafe {
$crate::WdfObjectGetTypedContextWorker(self.device as *mut _, [<_WDF_ $context_type _TYPE_INFO>].cell.get())?
} as *mut [<WdfObject $context_type>];
let from_context = match &(*from_context).0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
// Write to the memory location, making the data in it init
// clones the arc into new handle
context.write(
[<WdfObject $context_type>](ArcPointer::Weak(::std::sync::Arc::downgrade(&from_context)))
);
Ok(())
}
/// NOTE: Dropping memory that was created via `clone_into` will never drop the main allocation.
/// To drop the main allocation, you need to drop the instance made via `init`.
/// That instance can be obtained through the original handle you created it through
///
/// SAFETY:
/// - Data in context is assumed to already be init and a valid T
/// - Therefore, init for the context must already have been done on this handle
/// - No other mutable/non-mutable refs can exist to data when this is called, or it will alias
///
/// This may overwrite data in the handle's context memory, it is UB to read it after drop (e.g. get*)
$sv unsafe fn drop(
handle: $crate::wdf_umdf_sys::WDFOBJECT,
) -> ::std::result::Result<(), $crate::WdfError> {
let context = $crate::WdfObjectGetTypedContextWorker(
handle,
[<_WDF_ $context_type _TYPE_INFO>].cell.get(),
)? as *mut [<WdfObject $context_type>];
// drop the memory
::std::ptr::drop_in_place(context);
Ok(())
}
/// Borrow the context immutably
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn get<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&$context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let guard = context.read().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&*guard);
Ok(())
}
/// Borrow the context mutably
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn get_mut<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&mut $context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let mut guard = context.write().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&mut *guard);
Ok(())
}
/// Borrow the context immutably
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn try_get<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&$context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let guard = context.try_read().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&*guard);
Ok(())
}
/// Try to borrow the context mutably. Immediately returns if it's locked
/// Function returns with error and won't call cb if it failed to lock
///
/// SAFETY:
/// - Must have initialized WdfObject first
/// - Data must not have been dropped
/// - Object must not have been destroyed
$sv unsafe fn try_get_mut<F>(
handle: *mut $crate::wdf_umdf_sys::WDFDEVICE__,
cb: F
) -> ::std::result::Result<(), $crate::WdfError>
where
F: ::std::ops::FnOnce(&mut $context_type)
{
let context = unsafe {
$crate::WdfObjectGetTypedContextWorker(handle as *mut _,
// SAFETY: Reading is always fine, since user cannot obtain mutable reference
(&*[<_WDF_ $context_type _TYPE_INFO>].cell.get()).UniqueType
)?
} as *mut [<WdfObject $context_type>];
let context = &*context;
let context = match &context.0 {
ArcPointer::Strong(a) => a.clone(),
ArcPointer::Weak(a) => a.upgrade().ok_or($crate::WdfError::UpgradeFailed)?.clone(),
};
let mut guard = context.try_write().map_err(|_| $crate::WdfError::LockFailed)?;
cb(&mut *guard);
Ok(())
}
// SAFETY:
// - No other mutable refs must exist to target type
// - Underlying memory must remain immutable and unchanged until reference is dropped
$sv unsafe fn get_type_info() -> &'static $crate::wdf_umdf_sys::_WDF_OBJECT_CONTEXT_TYPE_INFO {
unsafe { &*[<_WDF_ $context_type _TYPE_INFO>].cell.get() }
}
}
}
}
};
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDriverCreate(
// in
DriverObject: PDRIVER_OBJECT,
// in
RegistryPath: PCUNICODE_STRING,
// in, optional
DriverAttributes: Option<PWDF_OBJECT_ATTRIBUTES>,
// in
DriverConfig: PWDF_DRIVER_CONFIG,
// out, optional
Driver: Option<&mut WDFDRIVER>,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDriverCreate(
DriverObject,
RegistryPath,
DriverAttributes.unwrap_or(WDF_NO_OBJECT_ATTRIBUTES!()),
DriverConfig,
Driver
.map(std::ptr::from_mut)
.unwrap_or(WDF_NO_HANDLE!())
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceCreate(
// in, out
DeviceInit: &mut PWDFDEVICE_INIT,
// in, optional
DeviceAttributes: Option<&mut WDF_OBJECT_ATTRIBUTES>,
// out
Device: &mut WDFDEVICE,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDeviceCreate(
DeviceInit,
DeviceAttributes.map_or(WDF_NO_OBJECT_ATTRIBUTES!(), std::ptr::from_mut),
Device
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceInitSetPnpPowerEventCallbacks(
// in
DeviceInit: PWDFDEVICE_INIT,
// in
PnpPowerEventCallbacks: *mut _WDF_PNPPOWER_EVENT_CALLBACKS,
) -> Result<(), WdfError> {
WdfCall! {
WdfDeviceInitSetPnpPowerEventCallbacks(
DeviceInit,
PnpPowerEventCallbacks
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfObjectGetTypedContextWorker(
// in
Handle: WDFOBJECT,
// in
TypeInfo: PCWDF_OBJECT_CONTEXT_TYPE_INFO,
) -> Result<*mut c_void, WdfError> {
WdfCall! {
WdfObjectGetTypedContextWorker(
Handle,
TypeInfo
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfObjectDelete(
// in
Object: WDFOBJECT,
) -> Result<(), WdfError> {
WdfCall! {
WdfObjectDelete(
Object
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceSetFailed(
// in
Device: WDFDEVICE,
// in
FailedAction: WDF_DEVICE_FAILED_ACTION,
) -> Result<(), WdfError> {
WdfCall! {
WdfDeviceSetFailed(
Device,
FailedAction
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceAllocAndQueryPropertyEx(
// in
Device: WDFDEVICE,
// in
DeviceProperty: &mut _WDF_DEVICE_PROPERTY_DATA,
// in
PoolType: POOL_TYPE,
// in, optional
PropertyMemoryAttributes: PWDF_OBJECT_ATTRIBUTES,
// out
PropertyMemory: &mut WDFMEMORY,
// out
Type: &mut DEVPROPTYPE,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDeviceAllocAndQueryPropertyEx(
Device,
DeviceProperty,
PoolType,
PropertyMemoryAttributes,
PropertyMemory,
Type
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfMemoryGetBuffer(
// in
Memory: WDFMEMORY,
// out, optional
BufferSize: Option<&mut usize>,
) -> Result<*mut c_void, WdfError> {
WdfCall! {
WdfMemoryGetBuffer(
Memory,
BufferSize.map_or(std::ptr::null_mut(), std::ptr::from_mut)
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfDeviceCreateDeviceInterface(
Device: WDFDEVICE,
InterfaceClassGUID: *const GUID,
ReferenceString: PCUNICODE_STRING,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfDeviceCreateDeviceInterface(
Device,
InterfaceClassGUID,
ReferenceString
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfRequestRetrieveInputBuffer(
Request: WDFREQUEST,
MinimumRequiredLength: usize,
Buffer: *mut PVOID,
Length: *mut usize,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfRequestRetrieveInputBuffer(
Request,
MinimumRequiredLength,
Buffer,
Length
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfRequestRetrieveOutputBuffer(
Request: WDFREQUEST,
MinimumRequiredSize: usize,
Buffer: *mut PVOID,
Length: *mut usize,
) -> Result<NTSTATUS, WdfError> {
WdfCall! {
WdfRequestRetrieveOutputBuffer(
Request,
MinimumRequiredSize,
Buffer,
Length
)
}
}
/// # Safety
///
/// None. User is responsible for safety.
pub unsafe fn WdfRequestCompleteWithInformation(
Request: WDFREQUEST,
Status: NTSTATUS,
Information: ULONG_PTR,
) -> Result<(), WdfError> {
WdfCall! {
WdfRequestCompleteWithInformation(
Request,
Status,
Information
)
}
}