├── .github ├── FUNDING.yml ├── workflows │ ├── build_res │ │ └── pr.pfx │ ├── clippy.yml │ ├── stale.yml │ ├── cla.yml │ └── build.yml ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md └── dependabot.yml ├── .vscode ├── settings.json └── launch.json ├── Virtual Display Driver Control ├── .editorconfig ├── GitVersion.yml ├── Assets │ ├── icon.ico │ ├── icon.png │ ├── StoreLogo.backup.png │ ├── LargeTile.scale-100.png │ ├── LargeTile.scale-125.png │ ├── LargeTile.scale-150.png │ ├── LargeTile.scale-200.png │ ├── LargeTile.scale-400.png │ ├── SmallTile.scale-100.png │ ├── SmallTile.scale-125.png │ ├── SmallTile.scale-150.png │ ├── SmallTile.scale-200.png │ ├── SmallTile.scale-400.png │ ├── StoreLogo.scale-100.png │ ├── StoreLogo.scale-125.png │ ├── StoreLogo.scale-150.png │ ├── StoreLogo.scale-200.png │ ├── StoreLogo.scale-400.png │ ├── SplashScreen.scale-100.png │ ├── SplashScreen.scale-125.png │ ├── SplashScreen.scale-150.png │ ├── SplashScreen.scale-200.png │ ├── SplashScreen.scale-400.png │ ├── LockScreenLogo.scale-200.png │ ├── Square44x44Logo.scale-100.png │ ├── Square44x44Logo.scale-125.png │ ├── Square44x44Logo.scale-150.png │ ├── Square44x44Logo.scale-200.png │ ├── Square44x44Logo.scale-400.png │ ├── Wide310x150Logo.scale-100.png │ ├── Wide310x150Logo.scale-125.png │ ├── Wide310x150Logo.scale-150.png │ ├── Wide310x150Logo.scale-200.png │ ├── Wide310x150Logo.scale-400.png │ ├── Square150x150Logo.scale-100.png │ ├── Square150x150Logo.scale-125.png │ ├── Square150x150Logo.scale-150.png │ ├── Square150x150Logo.scale-200.png │ ├── Square150x150Logo.scale-400.png │ ├── Square44x44Logo.targetsize-16.png │ ├── Square44x44Logo.targetsize-24.png │ ├── Square44x44Logo.targetsize-256.png │ ├── Square44x44Logo.targetsize-32.png │ ├── Square44x44Logo.targetsize-48.png │ ├── Square44x44Logo.altform-unplated_targetsize-16.png │ ├── Square44x44Logo.altform-unplated_targetsize-256.png │ ├── Square44x44Logo.altform-unplated_targetsize-32.png │ ├── Square44x44Logo.altform-unplated_targetsize-48.png │ ├── Square44x44Logo.targetsize-24_altform-unplated.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-16.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-24.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-256.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-32.png │ └── Square44x44Logo.altform-lightunplated_targetsize-48.png ├── Properties │ ├── launchSettings.json │ └── PublishProfiles │ │ └── win10-x64.pubxml ├── app.manifest ├── Common │ ├── Logging.cs │ └── AppSettings.cs ├── WindowTools.cs ├── Virtual Display Driver Control.sln ├── Virtual Display Driver Control.csproj.user ├── Views │ ├── MonitorsView.xaml.cs │ ├── MonitorsView.xaml │ └── SettingsView.xaml ├── MainWindow.xaml.cs ├── Package.appxmanifest ├── MainWindow.xaml ├── Helpers │ ├── MaterialHelper.cs │ └── ThemeHelper.cs ├── App.xaml ├── App.xaml.cs └── Virtual Display Driver Control.csproj ├── rust ├── .cargo │ └── config.toml ├── driver-logger │ ├── res │ │ ├── MSG00409.bin │ │ ├── eventmsgs.rc │ │ └── eventmsgs.mc │ ├── build.rs │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── win_debug.rs │ │ └── win_logger.rs ├── rust-toolchain.toml ├── virtual-display-driver │ ├── res │ │ └── MSG00409.bin │ ├── VirtualDisplayDriver.inf │ ├── .cargo │ │ └── config.toml │ ├── src │ │ ├── panic.rs │ │ ├── lib.rs │ │ ├── helpers.rs │ │ ├── direct_3d_device.rs │ │ ├── edid.rs │ │ ├── swap_chain_processor.rs │ │ └── entry.rs │ ├── Cargo.toml │ ├── build.rs │ └── Makefile.toml ├── wdf-umdf │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── iddcx.rs ├── driver-ipc │ ├── src │ │ ├── lib.rs │ │ ├── sync.rs │ │ ├── core.rs │ │ └── mock.rs │ └── Cargo.toml ├── wdf-umdf-sys │ ├── c │ │ └── wrapper.h │ ├── Cargo.toml │ ├── src │ │ ├── bindings.rs │ │ └── lib.rs │ └── build.rs ├── bindings │ └── python │ │ ├── Cargo.toml │ │ ├── src │ │ └── utils.rs │ │ └── Makefile.toml ├── virtual-display-driver-cli │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── mode.rs ├── vdd-user-session-service │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ ├── set_privileges.rs │ │ ├── main.rs │ │ └── service.rs ├── Cargo.toml └── Makefile.toml ├── installer ├── files │ ├── icon.ico │ ├── install.reg │ ├── nefconc.exe │ ├── nefconw.exe │ └── uninstall.reg └── install-cert.bat ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md ├── examples └── monitor_control.py ├── CLA.md └── CODE-OF-CONDUCT.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: MolotovCherry 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.check.command": "clippy", 3 | } 4 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | end_of_line = crlf -------------------------------------------------------------------------------- /rust/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "target-feature=+crt-static"] 3 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/GitVersion.yml: -------------------------------------------------------------------------------- 1 | assembly-informational-format: '{Major}.{Minor}.{Patch}-{ShortSha}' -------------------------------------------------------------------------------- /installer/files/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/installer/files/icon.ico -------------------------------------------------------------------------------- /installer/files/install.reg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/installer/files/install.reg -------------------------------------------------------------------------------- /installer/files/nefconc.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/installer/files/nefconc.exe -------------------------------------------------------------------------------- /installer/files/nefconw.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/installer/files/nefconw.exe -------------------------------------------------------------------------------- /.github/workflows/build_res/pr.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/.github/workflows/build_res/pr.pfx -------------------------------------------------------------------------------- /rust/driver-logger/res/MSG00409.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/rust/driver-logger/res/MSG00409.bin -------------------------------------------------------------------------------- /installer/install-cert.bat: -------------------------------------------------------------------------------- 1 | certutil -addstore -f root "DriverCertificate.cer" 2 | certutil -addstore -f TrustedPublisher "DriverCertificate.cer" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.cer 3 | Virtual Display Driver Control/bin/* 4 | Virtual Display Driver Control/obj/* 5 | Virtual Display Driver Control/.vs/* 6 | -------------------------------------------------------------------------------- /rust/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2024-07-26" 3 | components = ["rustfmt", "clippy"] 4 | targets = [] 5 | profile = "minimal" 6 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/res/MSG00409.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/rust/virtual-display-driver/res/MSG00409.bin -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/icon.ico -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/icon.png -------------------------------------------------------------------------------- /rust/virtual-display-driver/VirtualDisplayDriver.inf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/rust/virtual-display-driver/VirtualDisplayDriver.inf -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/StoreLogo.backup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/StoreLogo.backup.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/LargeTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/LargeTile.scale-100.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/LargeTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/LargeTile.scale-125.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/LargeTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/LargeTile.scale-150.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/LargeTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/LargeTile.scale-200.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/LargeTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/LargeTile.scale-400.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/SmallTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/SmallTile.scale-100.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/SmallTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/SmallTile.scale-125.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/SmallTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/SmallTile.scale-150.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/SmallTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/SmallTile.scale-200.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/SmallTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/SmallTile.scale-400.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/StoreLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/StoreLogo.scale-100.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/StoreLogo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/StoreLogo.scale-125.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/StoreLogo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/StoreLogo.scale-150.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/StoreLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/StoreLogo.scale-200.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/StoreLogo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/StoreLogo.scale-400.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/SplashScreen.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/SplashScreen.scale-100.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/SplashScreen.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/SplashScreen.scale-125.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/SplashScreen.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/SplashScreen.scale-150.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/SplashScreen.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/SplashScreen.scale-400.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/LockScreenLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/LockScreenLogo.scale-200.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.scale-100.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.scale-125.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.scale-150.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.scale-400.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Wide310x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Wide310x150Logo.scale-100.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Wide310x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Wide310x150Logo.scale-125.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Wide310x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Wide310x150Logo.scale-150.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Wide310x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Wide310x150Logo.scale-400.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square150x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square150x150Logo.scale-100.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square150x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square150x150Logo.scale-125.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square150x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square150x150Logo.scale-150.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square150x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square150x150Logo.scale-400.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-16.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-24.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-256.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-32.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-48.png -------------------------------------------------------------------------------- /installer/files/uninstall.reg: -------------------------------------------------------------------------------- 1 | Windows Registry Editor Version 5.00 2 | 3 | [-HKEY_LOCAL_MACHINE\SOFTWARE\VirtualDisplayDriver] 4 | [-HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\System\VirtualDisplayDriver] 5 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.altform-unplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.altform-unplated_targetsize-16.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.altform-unplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.altform-unplated_targetsize-256.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.altform-unplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.altform-unplated_targetsize-32.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.altform-unplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.altform-unplated_targetsize-48.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-24_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.targetsize-24_altform-unplated.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png -------------------------------------------------------------------------------- /Virtual Display Driver Control/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MolotovCherry/virtual-display-rs/HEAD/Virtual Display Driver Control/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png -------------------------------------------------------------------------------- /rust/wdf-umdf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wdf-umdf" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [dependencies] 10 | wdf-umdf-sys = { path = "../wdf-umdf-sys" } 11 | paste = "1.0.15" 12 | thiserror = "2.0.3" 13 | -------------------------------------------------------------------------------- /rust/driver-logger/res/eventmsgs.rc: -------------------------------------------------------------------------------- 1 | /* Do not edit this file manually. 2 | This file is autogenerated by windmc. */ 3 | 4 | 5 | // Country: United States 6 | // Language: English 7 | #pragma code_page(437) 8 | LANGUAGE 0x9, 0x1 9 | 1 MESSAGETABLE "MSG00409.bin" 10 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Virtual Display Driver Control (Package)": { 4 | "commandName": "MsixPackage" 5 | }, 6 | "Virtual Display Driver Control (Unpackaged)": { 7 | "commandName": "Project" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /rust/driver-ipc/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod core; 3 | mod driver_client; 4 | pub mod sync; 5 | 6 | pub use client::Client; 7 | pub use core::*; 8 | pub use driver_client::DriverClient; 9 | 10 | #[cfg(test)] 11 | mod mock; 12 | 13 | pub static DEFAULT_PIPE_NAME: &str = "virtualdisplaydriver"; 14 | -------------------------------------------------------------------------------- /rust/wdf-umdf-sys/c/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /** 4 | * 5 | * UMDF 6 | * 7 | */ 8 | 9 | #define WDF_STUB 10 | 11 | #include 12 | 13 | /** 14 | * 15 | * IDCXX 16 | * 17 | */ 18 | 19 | #define IDD_STUB 20 | 21 | // handled in build.rs 22 | // #include 23 | -------------------------------------------------------------------------------- /rust/bindings/python/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vdd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | pyo3 = { version = "0.22.6", features = ["abi3", "abi3-py37"] } 11 | driver-ipc = { path = "../../driver-ipc" } 12 | 13 | [lints] 14 | workspace = true 15 | -------------------------------------------------------------------------------- /rust/wdf-umdf-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wdf-umdf-sys" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [dependencies] 10 | paste = "1.0.15" 11 | bytemuck = "1.19.0" 12 | thiserror = "2.0.3" 13 | 14 | [build-dependencies] 15 | bindgen = "0.70.1" 16 | thiserror = "2.0.3" 17 | winreg = "0.52.0" 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: A question you have 4 | title: '' 5 | labels: invalid 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please ask questions in the [discord server](https://discord.gg/pDDt78wYQy) or [discussion section](https://github.com/MolotovCherry/virtual-display-rs/discussions), not on issues. Thanks! 11 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-pc-windows-msvc" 3 | 4 | rustflags = [ 5 | "-Zpre-link-arg=/NOLOGO", 6 | "-Zpre-link-arg=/MANIFEST:NO", 7 | "-Zpre-link-arg=/SUBSYSTEM:WINDOWS", 8 | "-Zpre-link-arg=/DYNAMICBASE", 9 | "-Zpre-link-arg=/NXCOMPAT", 10 | "-Clink-arg=/OPT:REF,ICF", 11 | ] 12 | -------------------------------------------------------------------------------- /rust/driver-logger/build.rs: -------------------------------------------------------------------------------- 1 | //use winresource::WindowsResource; 2 | 3 | fn main() { 4 | // This is commented out, because we can't add winres to another crate 5 | // and this one at the same time 6 | // 7 | // WindowsResource::new() 8 | // .set_resource_file("res/eventmsgs.rc") 9 | // .compile() 10 | // .unwrap(); 11 | } 12 | -------------------------------------------------------------------------------- /rust/bindings/python/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use pyo3::{exceptions::PyRuntimeError, prelude::PyErr}; 4 | 5 | pub trait IntoPyErr { 6 | fn into_py_err(self) -> Result; 7 | } 8 | 9 | impl IntoPyErr for Result { 10 | fn into_py_err(self) -> Result { 11 | self.map_err(|err| PyRuntimeError::new_err(err.to_string())) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rust/driver-ipc/src/sync.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod driver_client; 3 | 4 | use std::sync::LazyLock; 5 | 6 | pub use client::{Client, EventsSubscription}; 7 | pub use driver_client::DriverClient; 8 | 9 | use tokio::runtime::{Builder, Runtime}; 10 | 11 | static RUNTIME: LazyLock = LazyLock::new(|| { 12 | Builder::new_multi_thread() 13 | .worker_threads(1) 14 | .enable_all() 15 | .build() 16 | .unwrap() 17 | }); 18 | -------------------------------------------------------------------------------- /rust/virtual-display-driver-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "virtual-display-driver-cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [dependencies] 10 | clap = { version = "4.5.21", features = ["derive"] } 11 | color-eyre = "0.6.3" 12 | driver-ipc = { path = "../driver-ipc" } 13 | eyre = "0.6.12" 14 | owo-colors = "4.1.0" 15 | serde_json = "1.0.133" 16 | lazy_format = "2.0.3" 17 | joinery = "3.1.0" 18 | serde = "1.0.215" 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "attach", 10 | "name": "Attach", 11 | "pid": "${command:pickMyProcess}" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/rust" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /rust/wdf-umdf-sys/src/bindings.rs: -------------------------------------------------------------------------------- 1 | #![allow(unsafe_op_in_unsafe_fn)] 2 | #![allow(clippy::all)] 3 | #![allow(clippy::pedantic)] 4 | #![allow(clippy::restriction)] 5 | 6 | // stand-in type replacing NTSTATUS in the bindings 7 | use crate::NTSTATUS; 8 | 9 | include!(concat!(env!("OUT_DIR"), "/umdf.rs")); 10 | 11 | // required for some macros 12 | unsafe impl Send for _WDF_OBJECT_CONTEXT_TYPE_INFO {} 13 | unsafe impl Sync for _WDF_OBJECT_CONTEXT_TYPE_INFO {} 14 | 15 | // fails to build without this symbol 16 | #[no_mangle] 17 | pub static IddMinimumVersionRequired: ULONG = 4; 18 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/src/panic.rs: -------------------------------------------------------------------------------- 1 | #[cfg(debug_assertions)] 2 | use std::backtrace::Backtrace; 3 | use std::panic; 4 | 5 | use log::error; 6 | 7 | pub fn set_hook() { 8 | panic::set_hook(Box::new(|v| { 9 | // debug mode, get full backtrace 10 | #[cfg(debug_assertions)] 11 | { 12 | let backtrace = Backtrace::force_capture(); 13 | error!("{v}\n\nstack backtrace:\n{backtrace}"); 14 | } 15 | 16 | // otherwise just print the panic since we don't have a backtrace 17 | #[cfg(not(debug_assertions))] 18 | error!("{v}"); 19 | })); 20 | } 21 | -------------------------------------------------------------------------------- /rust/vdd-user-session-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vdd-user-session-service" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | windows-service = "0.7.0" 8 | driver-ipc = { path = "../driver-ipc" } 9 | clap = { version = "4.5.21", features = ["derive"] } 10 | winreg = "0.52.0" 11 | serde_json = "1.0.133" 12 | 13 | [dependencies.windows] 14 | version = "0.58.0" 15 | features = [ 16 | "Win32_Foundation", 17 | "Win32_System_RemoteDesktop", 18 | "Win32_System_Services", 19 | "Win32_UI_WindowsAndMessaging", 20 | "Win32_System_Threading", 21 | "Win32_Security", 22 | ] 23 | 24 | [lints] 25 | workspace = true 26 | -------------------------------------------------------------------------------- /rust/driver-logger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "driver-logger" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | log = { version = "0.4.22", features = ["std"] } 8 | env_filter = { version = "0.1.2", default-features = false, optional = true } 9 | widestring = "1.1.0" 10 | winreg = "0.52.0" 11 | thiserror = "2.0.3" 12 | 13 | [dependencies.windows] 14 | version = "0.58.0" 15 | features = ["Win32_System_Diagnostics_Debug"] 16 | 17 | [dependencies.windows-sys] 18 | version = "0.52.0" 19 | features = ["Win32_Foundation", "Win32_System_EventLog"] 20 | 21 | [build-dependencies] 22 | winresource = "0.1.19" 23 | 24 | [lints] 25 | workspace = true 26 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "virtual-display-driver", 5 | "wdf-umdf-sys", 6 | "wdf-umdf", 7 | "driver-ipc", 8 | "driver-logger", 9 | "virtual-display-driver-cli", 10 | "bindings/python", 11 | "vdd-user-session-service", 12 | ] 13 | 14 | [profile.release] 15 | strip = true 16 | codegen-units = 1 17 | lto = true 18 | 19 | [workspace.lints.rust] 20 | unsafe_op_in_unsafe_fn = "deny" 21 | 22 | [workspace.lints.clippy] 23 | pedantic = { level = "warn", priority = -1 } 24 | multiple_unsafe_ops_per_block = "deny" 25 | ignored_unit_patterns = "allow" 26 | missing_errors_doc = "allow" 27 | module_inception = "allow" 28 | module_name_repetitions = "allow" 29 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | PerMonitorV2 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: Clippy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "master" ] 7 | pull_request: 8 | branches: [ "master" ] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | clippy: 15 | runs-on: windows-2022 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Rust cache 22 | uses: Swatinem/rust-cache@v2 23 | with: 24 | workspaces: "rust -> target" 25 | 26 | - name: Add Clippy Component 27 | working-directory: rust 28 | run: rustup component add clippy 29 | 30 | - name: Clippy 31 | working-directory: rust 32 | run: cargo clippy --all-features -- --deny warnings 33 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Common/Logging.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | using System.IO; 4 | 5 | namespace Virtual_Display_Driver_Control.Common; 6 | class Logging { 7 | public static void Initialize() { 8 | var log = new LoggerConfiguration() 9 | // Always log to debug regardless 10 | .WriteTo.Debug(); 11 | 12 | // Write output log if not in debug mode 13 | #if !DEBUG 14 | log.WriteTo.File(Path.Combine(SettingsProvider.AppDir, "app.log"), 15 | rollingInterval: RollingInterval.Day, 16 | rollOnFileSizeLimit: true); 17 | #endif 18 | 19 | Log.Logger = log.CreateLogger(); 20 | } 21 | 22 | public static void Dispose() { 23 | Log.CloseAndFlush(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rust/vdd-user-session-service/Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | TARGET_PATH = "debug" 3 | 4 | [env.prod] 5 | TARGET_PATH = "release" 6 | BUILD_FLAGS = "--release" 7 | 8 | [tasks.set-build-path] 9 | env = { "BUILD_TARGET_PATH" = { script = [''' 10 | for /f "tokens=*" %%a in ('cargo target-dir') do set target_dir=%%a 11 | 12 | echo %target_dir%\%TARGET_PATH% 13 | '''] } } 14 | 15 | [tasks.copy] 16 | dependencies = ["set-build-path"] 17 | script = [ 18 | ''' 19 | if not exist "..\\target\\output" ( 20 | echo Directory not found, creating it... 21 | mkdir ..\\target\\output 22 | ) 23 | ''', 24 | # copy output files to it 25 | ''' 26 | copy %BUILD_TARGET_PATH%\*.exe ..\target\output 27 | ''', 28 | ] 29 | 30 | [tasks.build] 31 | clear = true 32 | script = ["cargo b %BUILD_FLAGS%"] 33 | -------------------------------------------------------------------------------- /rust/virtual-display-driver-cli/Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | TARGET_PATH = "debug" 3 | 4 | [env.prod] 5 | TARGET_PATH = "release" 6 | BUILD_FLAGS = "--release" 7 | 8 | [tasks.set-build-path] 9 | env = { "BUILD_TARGET_PATH" = { script = [''' 10 | for /f "tokens=*" %%a in ('cargo target-dir') do set target_dir=%%a 11 | 12 | echo %target_dir%\%TARGET_PATH% 13 | '''] } } 14 | 15 | [tasks.copy] 16 | dependencies = ["set-build-path"] 17 | script = [ 18 | ''' 19 | if not exist "..\\target\\output" ( 20 | echo Directory not found, creating it... 21 | mkdir ..\\target\\output 22 | ) 23 | ''', 24 | # copy output files to it 25 | ''' 26 | copy %BUILD_TARGET_PATH%\*.exe ..\target\output 27 | ''', 28 | ] 29 | 30 | [tasks.build] 31 | clear = true 32 | script = ["cargo b %BUILD_FLAGS%"] 33 | -------------------------------------------------------------------------------- /rust/wdf-umdf/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod iddcx; 2 | mod wdf; 3 | 4 | use std::any::Any; 5 | 6 | pub use paste::paste; 7 | 8 | pub use iddcx::*; 9 | pub use wdf::*; 10 | pub use wdf_umdf_sys; 11 | 12 | use wdf_umdf_sys::NTSTATUS; 13 | 14 | /// Used for the macros so they can correctly convert a functions result 15 | fn is_nt_error(val: &dyn Any, other_is_error: bool) -> bool { 16 | if let Some(status) = val.downcast_ref::() { 17 | return !status.is_success(); 18 | } 19 | 20 | // other errors which may not be error codes, but may also be 21 | // such as HRESULT == i32 22 | if other_is_error { 23 | if let Some(status) = val.downcast_ref::() { 24 | let status = NTSTATUS(*status); 25 | return !status.is_success(); 26 | } 27 | } 28 | 29 | false 30 | } 31 | -------------------------------------------------------------------------------- /rust/driver-ipc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "driver-ipc" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.80" 6 | 7 | [dependencies] 8 | log = "0.4.22" 9 | serde = { version = "1.0.215", features = ["derive"] } 10 | thiserror = "2.0.3" 11 | owo-colors = "4.1.0" 12 | serde_json = "1.0.133" 13 | windows = { version = "0.58.0", features = ["Win32_Foundation"] } 14 | lazy_format = "2.0.3" 15 | joinery = "3.1.0" 16 | winreg = "0.52.0" 17 | tokio = { version = "1.42.0", features = [ 18 | "rt-multi-thread", 19 | "sync", 20 | "time", 21 | "net", 22 | "macros", 23 | ] } 24 | tokio-stream = { version = "0.1.17", features = ["sync"] } 25 | 26 | [dev-dependencies] 27 | tokio = { version = "1.42.0", features = [ 28 | "rt-multi-thread", 29 | "sync", 30 | "time", 31 | "net", 32 | "macros", 33 | "io-util", 34 | ] } 35 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod helpers; 2 | 3 | mod callbacks; 4 | mod context; 5 | mod direct_3d_device; 6 | mod edid; 7 | mod entry; 8 | mod ipc; 9 | mod panic; 10 | mod swap_chain_processor; 11 | 12 | use wdf_umdf_sys::{NTSTATUS, PUNICODE_STRING, PVOID}; 13 | 14 | // 15 | // This exports the framework entry point function. 16 | // This is the first thing called when the driver loads. 17 | // After it finishes, it calls the exported, 18 | // DriverEntry: DRIVER_INITIALIZE 19 | // which is defined in `entry.rs` 20 | // 21 | #[link( 22 | name = "WdfDriverStubUm", 23 | kind = "static", 24 | modifiers = "+whole-archive" 25 | )] 26 | extern "C" { 27 | pub fn FxDriverEntryUm( 28 | LoaderInterface: PVOID, 29 | Context: PVOID, 30 | DriverObject: PVOID, 31 | RegistryPath: PUNICODE_STRING, 32 | ) -> NTSTATUS; 33 | } 34 | -------------------------------------------------------------------------------- /rust/bindings/python/Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | TARGET_PATH = "debug" 3 | 4 | [env.prod] 5 | TARGET_PATH = "release" 6 | BUILD_FLAGS = "--release" 7 | 8 | [tasks.set-build-path] 9 | env = { "BUILD_TARGET_PATH" = { script = [''' 10 | for /f "tokens=*" %%a in ('cargo target-dir') do set target_dir=%%a 11 | 12 | echo %target_dir%\%TARGET_PATH% 13 | '''] } } 14 | 15 | [tasks.copy] 16 | dependencies = ["set-build-path"] 17 | script = [ 18 | ''' 19 | if not exist "..\\..\\target\\output" ( 20 | echo Directory not found, creating it... 21 | mkdir ..\\..\\target\\output 22 | ) 23 | ''', 24 | ''' 25 | ren %BUILD_TARGET_PATH%\vdd.dll vdd.pyd 26 | ''', 27 | # copy output files to it 28 | ''' 29 | copy %BUILD_TARGET_PATH%\*.pyd ..\..\target\output 30 | ''', 31 | ] 32 | 33 | [tasks.build] 34 | clear = true 35 | script = ["cargo b %BUILD_FLAGS%"] 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'My Idea' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Prerequisites 11 | 12 | Please answer the following questions for yourself before submitting an issue. 13 | 14 | - [ ] I checked to make sure that this request has not already been filed 15 | 16 | ## Description 17 | 18 | **Is your feature request related to a problem? Please describe.** 19 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 20 | 21 | **Describe the solution you'd like** 22 | A clear and concise description of what you want to happen. 23 | 24 | **Describe alternatives you've considered** 25 | A clear and concise description of any alternative solutions or features you've considered. 26 | 27 | **Additional context** 28 | Add any other context or screenshots about the feature request here. 29 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | /// An unsafe wrapper to allow sending across threads 4 | /// 5 | /// USE WISELY, IT CAN CAUSE UB OTHERWISE 6 | pub struct Sendable(T); 7 | unsafe impl Send for Sendable {} 8 | unsafe impl Sync for Sendable {} 9 | 10 | impl Sendable { 11 | /// `T` must be Send+Sync safe 12 | pub unsafe fn new(t: T) -> Self { 13 | Sendable(t) 14 | } 15 | } 16 | 17 | impl Deref for Sendable { 18 | type Target = T; 19 | 20 | fn deref(&self) -> &Self::Target { 21 | &self.0 22 | } 23 | } 24 | 25 | impl DerefMut for Sendable { 26 | fn deref_mut(&mut self) -> &mut Self::Target { 27 | &mut self.0 28 | } 29 | } 30 | 31 | #[macro_export] 32 | macro_rules! debug { 33 | ($($tt:tt)*) => { 34 | if cfg!(debug_assertions) { 35 | ::log::debug!($($tt)*); 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /rust/driver-logger/res/eventmsgs.mc: -------------------------------------------------------------------------------- 1 | 2 | ; // Set of 5 event log resource messages as used by the winlog crate. 3 | 4 | 5 | ; // This is the header section. 6 | 7 | LanguageNames=(English=0x409:MSG00409) 8 | 9 | SeverityNames=( 10 | Informational=0x1:STATUS_SEVERITY_INFORMATIONAL 11 | Warning=0x2:STATUS_SEVERITY_WARNING 12 | Error=0x3:STATUS_SEVERITY_ERROR 13 | ) 14 | 15 | 16 | ; // The following are the message definitions. 17 | 18 | MessageIdTypedef=DWORD 19 | 20 | MessageId=0x1 21 | Severity=Error 22 | SymbolicName=MSG_ERROR 23 | Language=English 24 | %1 25 | . 26 | 27 | MessageId=0x2 28 | Severity=Warning 29 | SymbolicName=MSG_WARNING 30 | Language=English 31 | %1 32 | . 33 | 34 | MessageId=0x3 35 | Severity=Informational 36 | SymbolicName=MSG_INFO 37 | Language=English 38 | %1 39 | . 40 | 41 | MessageId=0x4 42 | Severity=Informational 43 | SymbolicName=MSG_DEBUG 44 | Language=English 45 | %1 46 | . 47 | 48 | MessageId=0x5 49 | Severity=Informational 50 | SymbolicName=MSG_TRACE 51 | Language=English 52 | %1 53 | . 54 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Properties/PublishProfiles/win10-x64.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | FileSystem 8 | x64 9 | win10-x64 10 | bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ 11 | true 12 | False 13 | False 14 | True 15 | 16 | true 17 | 18 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Cherry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/WindowTools.cs: -------------------------------------------------------------------------------- 1 | using CSharpFunctionalExtensions; 2 | using System; 3 | using System.Drawing; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace Virtual_Display_Driver_Control; 7 | 8 | internal static class WindowTools { 9 | public const int ICON_SMALL = 0; 10 | public const int ICON_BIG = 1; 11 | public const int ICON_SMALL2 = 2; 12 | 13 | public const int WM_GETICON = 0x007F; 14 | public const int WM_SETICON = 0x0080; 15 | 16 | [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)] 17 | public static extern int SendMessage(IntPtr hWnd, uint msg, int wParam, IntPtr lParam); 18 | 19 | public static Maybe GetIcon() { 20 | string? sExe = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName; 21 | if (sExe is not null) { 22 | return Icon.ExtractAssociatedIcon(sExe)!; 23 | } else { 24 | return Maybe.None; 25 | } 26 | } 27 | 28 | public static void SetWindowIcon(object target) { 29 | IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(target); 30 | GetIcon().Execute(icon => { 31 | SendMessage(hWnd, WM_SETICON, ICON_BIG, icon.Handle); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "virtual-display-driver" 3 | version = "0.4.0" 4 | edition = "2021" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | thiserror = "2.0.3" 14 | anyhow = "1.0.93" 15 | wdf-umdf-sys = { path = "../wdf-umdf-sys" } 16 | wdf-umdf = { path = "../wdf-umdf" } 17 | log = "0.4.22" 18 | bytemuck = { version = "1.19.0", features = ["derive"] } 19 | serde_json = "1.0.133" 20 | driver-ipc = { path = "../driver-ipc" } 21 | driver-logger = { path = "../driver-logger" } 22 | tokio = { version = "1.42.0", features = [ 23 | "macros", 24 | "net", 25 | "rt-multi-thread", 26 | "io-util", 27 | "sync", 28 | ] } 29 | 30 | [dependencies.windows] 31 | version = "0.58.0" 32 | features = [ 33 | "Win32_Foundation", 34 | "Win32_Security", 35 | "Win32_System_SystemServices", 36 | "Win32_System_Threading", 37 | "Win32_Graphics_Direct3D11", 38 | "Win32_Graphics_Direct3D", 39 | "Win32_Graphics_Dxgi", 40 | ] 41 | 42 | [build-dependencies] 43 | winres = "0.1.12" 44 | vergen-gix = { version = "1.0.2", features = ["build"] } 45 | 46 | [package.metadata.winres] 47 | OriginalFilename = "VirtualDisplayDriver.dll" 48 | ProductName = "Virtual Display Driver" 49 | FileDescription = "Adds virtual displays" 50 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/build.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use vergen_gix::{Emitter, GixBuilder}; 4 | 5 | fn main() -> Result<(), Box> { 6 | let mut winres = winres::WindowsResource::new(); 7 | 8 | // as much as I don't like setting this here, because it belongs to the logger crate 9 | // I want my own resource info for this crate. There's a linker error unless we do it 10 | // all in one fell swoop. This is the only option for now 11 | // 12 | // For location of the content, see winlog/res/eventmsgs.rc 13 | winres 14 | .append_rc_content( 15 | r#" 16 | /* Do not edit this file manually. 17 | This file is autogenerated by windmc. */ 18 | 19 | 20 | // Country: United States 21 | // Language: English 22 | #pragma code_page(437) 23 | LANGUAGE 0x9, 0x1 24 | 1 MESSAGETABLE "res/MSG00409.bin" 25 | "#, 26 | ) 27 | .compile() 28 | .unwrap(); 29 | 30 | // need linked c runtime for umdf includes 31 | println!("cargo::rustc-link-lib=static=ucrt"); 32 | //println!("cargo:rustc-link-lib=static=vcruntime"); 33 | 34 | println!("cargo::rerun-if-changed=build.rs"); 35 | 36 | // emit vergen build instructions 37 | Emitter::default() 38 | .add_instructions(&GixBuilder::all_git()?)? 39 | .emit()?; 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Virtual Display Driver Control.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.34221.43 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Virtual Display Driver Control", "Virtual Display Driver Control.csproj", "{D3A9D705-7E41-49EE-A5F8-8F5F549E6394}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Release|x64 = Release|x64 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {D3A9D705-7E41-49EE-A5F8-8F5F549E6394}.Debug|x64.ActiveCfg = Debug|x64 15 | {D3A9D705-7E41-49EE-A5F8-8F5F549E6394}.Debug|x64.Build.0 = Debug|x64 16 | {D3A9D705-7E41-49EE-A5F8-8F5F549E6394}.Debug|x64.Deploy.0 = Debug|x64 17 | {D3A9D705-7E41-49EE-A5F8-8F5F549E6394}.Release|x64.ActiveCfg = Release|x64 18 | {D3A9D705-7E41-49EE-A5F8-8F5F549E6394}.Release|x64.Build.0 = Release|x64 19 | {D3A9D705-7E41-49EE-A5F8-8F5F549E6394}.Release|x64.Deploy.0 = Release|x64 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(ExtensibilityGlobals) = postSolution 25 | SolutionGuid = {4F69884B-9DCB-429B-88A5-3B436BC6430A} 26 | EndGlobalSection 27 | EndGlobal 28 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '00 0 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | 10 | permissions: 11 | contents: write 12 | issues: write 13 | pull-requests: write 14 | 15 | steps: 16 | - uses: actions/stale@v8 17 | with: 18 | days-before-issue-stale: 30 19 | days-before-pr-stale: -1 20 | stale-issue-label: 'stale' 21 | stale-pr-label: 'stale' 22 | any-of-labels: 'duplicate,invalid,awaiting feedback,wontfix' 23 | exempt-all-milestones: true 24 | exempt-all-assignees: true 25 | labels-to-remove-when-unstale: stale 26 | stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. It will be closed in 7 days if there is no activity within that time.' 27 | stale-pr-message: 'This pr is stale because it has been open for 30 days with no activity. It will be closed in 7 days if there is no activity within that time. If it gets closed, but you wish to still do the pr, either send a new one, or comment on the pr stating your wishes.' 28 | close-issue-message: 'This issue has been closed because it was stale for 30 days, and there was no activity within the 7 day grace period. You may still comment on your issue after it is closed if you wish for it to receive attention or be re-opened.' 29 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Virtual Display Driver Control.csproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ProjectDebugger 5 | 6 | 7 | Virtual Display Driver Control (Unpackaged) 8 | Assets\icon.png 9 | Assets\icon.png 10 | Assets\icon.png 11 | Assets\icon.png 12 | Assets\icon.png 13 | Assets\icon.png 14 | Assets\icon.png 15 | Assets\icon.png 16 | 17 | 18 | ProjectDebugger 19 | 20 | 21 | 22 | Designer 23 | 24 | 25 | Designer 26 | 27 | 28 | Designer 29 | 30 | 31 | Designer 32 | 33 | 34 | Designer 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA Assistant" 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened,closed,synchronize] 7 | 8 | # explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings 9 | permissions: 10 | actions: write 11 | contents: write 12 | pull-requests: write 13 | statuses: write 14 | 15 | jobs: 16 | CLAAssistant: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: "CLA Assistant" 20 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' 21 | uses: contributor-assistant/github-action@v2.3.0 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | path-to-signatures: 'signatures/version1/cla.json' 26 | path-to-document: 'https://github.com/MolotovCherry/virtual-display-rs/blob/master/CLA.md' # e.g. a CLA or a DCO document 27 | # branch should not be protected 28 | branch: 'signatures' 29 | allowlist: MolotovCherry,bot* 30 | create-file-commit-message: 'Creating file for storing CLA Signatures' 31 | signed-commit-message: '$contributorName has signed the CLA in $owner/$repo#$pullRequestNo' 32 | #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' 33 | #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' 34 | custom-allsigned-prcomment: 'All Contributors have signed the CLA.' 35 | lock-pullrequest-aftermerge: false 36 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Views/MonitorsView.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml.Controls; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Virtual_Display_Driver_Control.Views; 7 | 8 | public sealed partial class MonitorsView : Page { 9 | private static Ipc? _ipc = null; 10 | 11 | static bool s_IsConnected { 12 | get { 13 | return _ipc != null && Ipc.IsConnected; 14 | } 15 | } 16 | 17 | static Ipc? s_Ipc { 18 | get { 19 | if (s_IsConnected) { 20 | return _ipc; 21 | } else { 22 | return null; 23 | } 24 | } 25 | } 26 | 27 | static List? savedData = null; 28 | static List? monitorList = null; 29 | 30 | public MonitorsView() { 31 | InitializeComponent(); 32 | 33 | // Now initialize Ipc since we have defined the callback 34 | Ipc.GetOrCreateIpc((Ipc ipc) => { 35 | System.Diagnostics.Debug.WriteLine("Init view"); 36 | savedData = ipc.RequestState(); 37 | // an actual clone, we don't want to touch the original savedData 38 | monitorList = savedData.Select(monitor => (Monitor)monitor.Clone()).ToList(); 39 | _ipc = ipc; 40 | }); 41 | } 42 | 43 | // callback is fired once it reconnects (if it does). can be null if not desired 44 | public void ReconnectIpc(Action callback) { 45 | if (!s_IsConnected) { 46 | Ipc.GetOrCreateIpc((Ipc ipc) => { 47 | _ipc = ipc; 48 | 49 | if (callback != null) { 50 | callback(ipc); 51 | } 52 | }, null); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Windowing; 2 | using Microsoft.UI.Xaml; 3 | using Microsoft.UI.Xaml.Controls; 4 | using Virtual_Display_Driver_Control.Views; 5 | 6 | namespace Virtual_Display_Driver_Control { 7 | public sealed partial class MainWindow : Window { 8 | public MainWindow() { 9 | InitializeComponent(); 10 | 11 | // set window icon 12 | WindowTools.SetWindowIcon(this); 13 | 14 | // only supported on windows 11 15 | if (AppWindowTitleBar.IsCustomizationSupported()) { 16 | ExtendsContentIntoTitleBar = true; 17 | SetTitleBar(AppTitleBar); 18 | } 19 | } 20 | 21 | private void NavView_Loaded(object sender, RoutedEventArgs e) { 22 | foreach (NavigationViewItemBase item in NavView.MenuItems) { 23 | if (item is NavigationViewItem && item.Tag.ToString() == "MonitorsView") { 24 | NavView.SelectedItem = item; 25 | break; 26 | } 27 | } 28 | 29 | ContentFrame.Navigate(typeof(MonitorsView)); 30 | } 31 | 32 | private void NavView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) { 33 | if (args.IsSettingsSelected) { 34 | ContentFrame.Navigate(typeof(SettingsView)); 35 | } else if (args.SelectedItem is NavigationViewItem item) { 36 | switch (item.Tag) { 37 | case "MonitorsView": 38 | ContentFrame.Navigate(typeof(MonitorsView)); 39 | break; 40 | 41 | default: 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rust/Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | BUILD_TARGET_PATH = { script = [''' 3 | for /f "tokens=*" %%a in ('cargo target-dir') do set target_dir=%%a 4 | 5 | echo %target_dir% 6 | '''] } 7 | 8 | [env.prod] 9 | PROD = "1" 10 | 11 | [tasks.build] 12 | clear = true 13 | dependencies = [ 14 | { name = "build", path = "virtual-display-driver" }, 15 | { name = "copy", path = "virtual-display-driver" }, 16 | { name = "build", path = "virtual-display-driver-cli" }, 17 | { name = "copy", path = "virtual-display-driver-cli" }, 18 | { name = "build", path = "vdd-user-session-service" }, 19 | { name = "copy", path = "vdd-user-session-service" }, 20 | { name = "build", path = "bindings/python" }, 21 | { name = "copy", path = "bindings/python" }, 22 | ] 23 | 24 | [tasks.build-installer] 25 | dependencies = ["build"] 26 | script = [ 27 | ''' 28 | if defined RELEASE_VERSION ( 29 | set release=-i %RELEASE_VERSION% 30 | ) 31 | 32 | if not defined PROD ( 33 | set mode=-d -D 34 | ) 35 | 36 | del /S /Q /F target\output\*.msi 37 | 38 | cargo wix -p virtual-display-driver %release% %mode% --nocapture -I installer/main.wxs -o target\output -C -ext -C WixDifxAppExtension -L -ext -L WixDifxAppExtension -L "C:\Program Files (x86)\WiX Toolset v3.11\bin\difxapp_x64.wixlib" 39 | ''', 40 | # load env 41 | ''' 42 | if defined CI ( 43 | call "%ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" 44 | ) else ( 45 | call "%ProgramFiles%\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat" 46 | ) 47 | ''', 48 | # sign installer 49 | ''' 50 | if defined CI ( 51 | set sign_options=/f private.pfx /p %PRIVATE_KEY_PASSWORD% 52 | ) else ( 53 | set sign_options=/sm /n DriverCertificate 54 | ) 55 | 56 | signtool sign /a /fd SHA256 /v %sign_options% /t http://timestamp.digicert.com target/output/*.msi 57 | ''', 58 | ] 59 | 60 | [config] 61 | default_to_workspace = false 62 | -------------------------------------------------------------------------------- /rust/vdd-user-session-service/src/set_privileges.rs: -------------------------------------------------------------------------------- 1 | use windows::{ 2 | core::PCWSTR, 3 | Win32::{ 4 | Foundation::{CloseHandle, HANDLE, LUID}, 5 | Security::{ 6 | AdjustTokenPrivileges, LookupPrivilegeValueW, SE_PRIVILEGE_ENABLED, 7 | TOKEN_ADJUST_PRIVILEGES, TOKEN_PRIVILEGES, TOKEN_PRIVILEGES_ATTRIBUTES, 8 | }, 9 | System::Threading::{OpenProcess, OpenProcessToken, PROCESS_QUERY_INFORMATION}, 10 | }, 11 | }; 12 | 13 | pub fn set_privilege(name: PCWSTR, state: bool) -> bool { 14 | let Ok(handle) = (unsafe { OpenProcess(PROCESS_QUERY_INFORMATION, false, std::process::id()) }) 15 | else { 16 | return false; 17 | }; 18 | 19 | let mut token_handle = HANDLE::default(); 20 | if unsafe { OpenProcessToken(handle, TOKEN_ADJUST_PRIVILEGES, &mut token_handle).is_err() } { 21 | return false; 22 | } 23 | 24 | let mut luid = LUID::default(); 25 | 26 | if unsafe { LookupPrivilegeValueW(PCWSTR::null(), name, &mut luid).is_err() } { 27 | return false; 28 | } 29 | 30 | let mut tp = TOKEN_PRIVILEGES { 31 | PrivilegeCount: 1, 32 | ..Default::default() 33 | }; 34 | 35 | tp.Privileges[0].Luid = luid; 36 | 37 | if state { 38 | tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 39 | } else { 40 | tp.Privileges[0].Attributes = TOKEN_PRIVILEGES_ATTRIBUTES(0u32); 41 | } 42 | 43 | if unsafe { 44 | #[allow(clippy::cast_possible_truncation)] 45 | AdjustTokenPrivileges( 46 | token_handle, 47 | false, 48 | Some(&tp), 49 | std::mem::size_of::() as u32, 50 | None, 51 | None, 52 | ) 53 | .is_err() 54 | } { 55 | return false; 56 | } 57 | 58 | if unsafe { CloseHandle(handle).is_err() } { 59 | return false; 60 | } 61 | 62 | if unsafe { CloseHandle(token_handle).is_err() } { 63 | return false; 64 | } 65 | 66 | true 67 | } 68 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Package.appxmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | Virtual Display Driver Control 19 | Cherry 20 | Assets\StoreLogo.png 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /rust/driver-logger/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::missing_errors_doc)] 2 | 3 | mod win_debug; 4 | mod win_logger; 5 | 6 | use std::error::Error; 7 | 8 | use log::{Level, Log}; 9 | 10 | use crate::win_debug::WinDebugLogger; 11 | use crate::win_logger::WinLogger; 12 | 13 | // A logger which logs to multiple logger implementations 14 | pub struct DriverLogger { 15 | pub level: Level, 16 | win_debug: Option, 17 | win_logger: Option, 18 | } 19 | 20 | impl DriverLogger { 21 | #[must_use] 22 | pub fn new(level: Level) -> Self { 23 | Self { 24 | level, 25 | win_logger: None, 26 | win_debug: None, 27 | } 28 | } 29 | 30 | pub fn debug(&mut self) -> &mut Self { 31 | self.win_debug = Some(WinDebugLogger { level: self.level }); 32 | self 33 | } 34 | 35 | pub fn name(&mut self, name: &str) -> Result<&mut Self, Box> { 36 | self.win_logger = Some(WinLogger::new(name)?); 37 | Ok(self) 38 | } 39 | 40 | pub fn init(self) -> Result<(), Box> { 41 | let level = self.level; 42 | 43 | match log::set_boxed_logger(Box::new(self)) { 44 | Ok(()) => { 45 | log::set_max_level(level.to_level_filter()); 46 | Ok(()) 47 | } 48 | Err(e) => Err(e.into()), 49 | } 50 | } 51 | } 52 | 53 | impl Log for DriverLogger { 54 | fn enabled(&self, metadata: &log::Metadata) -> bool { 55 | metadata.level() <= self.level 56 | } 57 | 58 | fn log(&self, record: &log::Record) { 59 | if !self.enabled(record.metadata()) { 60 | return; 61 | } 62 | 63 | if let Some(debug) = self.win_debug.as_ref() { 64 | debug.log(record); 65 | } 66 | 67 | if let Some(logger) = self.win_logger.as_ref() { 68 | logger.log(record); 69 | } 70 | } 71 | 72 | fn flush(&self) { 73 | if let Some(debug) = self.win_debug.as_ref() { 74 | debug.flush(); 75 | } 76 | 77 | if let Some(logger) = self.win_logger.as_ref() { 78 | logger.flush(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /rust/driver-logger/src/win_debug.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::fmt::Write; 4 | 5 | use log::{Level, Log, SetLoggerError}; 6 | use windows::{core::PCWSTR, Win32::System::Diagnostics::Debug::OutputDebugStringW}; 7 | 8 | #[derive(Debug)] 9 | pub struct WinDebugLogger { 10 | pub level: Level, 11 | } 12 | 13 | impl Log for WinDebugLogger { 14 | fn enabled(&self, metadata: &log::Metadata) -> bool { 15 | metadata.level() <= self.level 16 | } 17 | 18 | fn log(&self, record: &log::Record) { 19 | if !self.enabled(record.metadata()) { 20 | return; 21 | } 22 | 23 | // Silently ignore errors 24 | let _ = log(record); 25 | } 26 | 27 | fn flush(&self) {} 28 | } 29 | 30 | fn log(record: &log::Record) -> Option<()> { 31 | let target = if record.target().is_empty() { 32 | record.module_path().unwrap_or_default() 33 | } else { 34 | record.target() 35 | }; 36 | 37 | let level = format!("[{}]", record.level()); 38 | 39 | // Everything except the timestamp 40 | let mut base = String::new(); 41 | write!(&mut base, "{level:<7} [{target}").ok()?; 42 | 43 | if let Some(line) = record.line() { 44 | write!(&mut base, ":{line}").ok()?; 45 | } 46 | 47 | write!(&mut base, "]").ok()?; 48 | 49 | for line in record.args().to_string().lines() { 50 | let full = format!("{base} {line}\0"); 51 | let full = full.encode_utf16().collect::>(); 52 | 53 | // Write the output 54 | unsafe { 55 | OutputDebugStringW(PCWSTR(full.as_ptr())); 56 | } 57 | } 58 | 59 | Some(()) 60 | } 61 | 62 | /// Initialize the global logger with a specific log level. 63 | /// 64 | /// ``` 65 | /// # use log::{warn, info}; 66 | /// # fn main() { 67 | /// windebug_logger::init_with_level(log::Level::Warn).unwrap(); 68 | /// 69 | /// warn!("This is an example message."); 70 | /// info!("This message will not be logged."); 71 | /// # } 72 | /// ``` 73 | pub fn init_with_level(level: Level) -> Result<(), SetLoggerError> { 74 | let logger = WinDebugLogger { level }; 75 | match log::set_boxed_logger(Box::new(logger)) { 76 | Ok(()) => { 77 | log::set_max_level(level.to_level_filter()); 78 | Ok(()) 79 | } 80 | Err(e) => Err(e), 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 27 | 28 | 33 | 34 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'My bug' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Prerequisites 11 | 12 | Please answer the following questions for yourself before submitting an issue. 13 | 14 | - [ ] I have checked and am running the latest version of the driver and the control app software 15 | - [ ] I am using a supported version of Windows, as stated in the README on the front page 16 | - [ ] I checked and carefully read the documentation, making sure to follow any troubleshooting problems 17 | - [ ] I have tried to solve the problem myself, but could not (or this is a unsolvable problem on my end) 18 | - [ ] I understand driver installation will fail if I didn't install the certificate properly. I have made sure the certificate is installed and exists in my local computer's root and TrustedPublisher stores 19 | - [ ] I have uninstalled and reinstalled, and the problem is still present 20 | - [ ] I checked to make sure that this issue has not already been filed 21 | 22 | ## Context 23 | 24 | Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. Please include as much information as you possibly can; it may be crucial to reproduce the problem 25 | 26 | * Operating System Version + Build Number: 27 | * Device Manager Logs/Details/Problem Code: 28 | * Any other relevant information: 29 | 30 | ## Expected Behavior 31 | 32 | Please describe the behavior you are expecting 33 | 34 | ## Current Behavior 35 | 36 | What is the current behavior? 37 | 38 | ## Failure Information 39 | 40 | Please help provide information about the failure. 41 | 42 | ### Steps to Reproduce 43 | 44 | Please provide, as best you can, detailed steps for reproducing the issue. 45 | 46 | 1. step 1 47 | 2. step 2 48 | 3. you get it... 49 | 50 | ### Failure Logs 51 | 52 | 53 | 54 |
Crash Log 55 |

56 | 57 | ``` 58 | 59 | ``` 60 | 61 |

62 |
63 | -------------------------------------------------------------------------------- /rust/driver-ipc/src/core.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub type Id = u32; 4 | pub type Dimen = u32; 5 | pub type RefreshRate = u32; 6 | 7 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, PartialOrd)] 8 | pub struct Monitor { 9 | // identifier 10 | pub id: Id, 11 | pub name: Option, 12 | pub enabled: bool, 13 | pub modes: Vec, 14 | } 15 | 16 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, PartialOrd)] 17 | pub struct Mode { 18 | pub width: Dimen, 19 | pub height: Dimen, 20 | pub refresh_rates: Vec, 21 | } 22 | 23 | #[non_exhaustive] 24 | #[derive(Debug, Clone, Deserialize, Serialize)] 25 | pub enum DriverCommand { 26 | // Single line of communication client->server 27 | // Driver commands 28 | // 29 | // Notify of monitor changes (whether adding or updating) 30 | Notify(Vec), 31 | // Remove a monitor from system 32 | Remove(Vec), 33 | // Remove all monitors from system 34 | RemoveAll, 35 | } 36 | 37 | /// Request command sent from client->server 38 | #[non_exhaustive] 39 | #[derive(Debug, Clone, Deserialize, Serialize)] 40 | pub enum RequestCommand { 41 | // Request information on the current system monitor state 42 | State, 43 | } 44 | 45 | /// Reply command sent from server->client 46 | #[non_exhaustive] 47 | #[derive(Debug, Clone, Deserialize, Serialize)] 48 | pub enum ReplyCommand { 49 | // Reply to previous current system monitor state request 50 | State(Vec), 51 | } 52 | 53 | /// An event happened 54 | #[non_exhaustive] 55 | #[derive(Debug, Clone, Deserialize, Serialize)] 56 | pub enum EventCommand { 57 | // Monitor state was changed while client was connected 58 | Changed(Vec), 59 | } 60 | 61 | /// An untagged enum of commands to be used with deserialization. 62 | /// This makes the deserialization process much easier to handle 63 | /// when a received command could be of multiple types 64 | #[non_exhaustive] 65 | #[derive(Debug, Clone, Serialize, Deserialize)] 66 | #[serde(untagged)] 67 | pub enum ServerCommand { 68 | Driver(DriverCommand), 69 | Request(RequestCommand), 70 | } 71 | 72 | /// An untagged enum of commands to be used with deserialization. 73 | /// This makes the deserialization process much easier to handle 74 | /// when a received command could be of multiple types 75 | #[non_exhaustive] 76 | #[derive(Debug, Clone, Serialize, Deserialize)] 77 | #[serde(untagged)] 78 | pub enum ClientCommand { 79 | Reply(ReplyCommand), 80 | Event(EventCommand), 81 | } 82 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/src/direct_3d_device.rs: -------------------------------------------------------------------------------- 1 | use windows::{ 2 | core::Error, 3 | Win32::{ 4 | Foundation::LUID, 5 | Graphics::{ 6 | Direct3D::D3D_DRIVER_TYPE_UNKNOWN, 7 | Direct3D11::{ 8 | D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, 9 | D3D11_CREATE_DEVICE_BGRA_SUPPORT, 10 | D3D11_CREATE_DEVICE_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY, 11 | D3D11_CREATE_DEVICE_SINGLETHREADED, D3D11_SDK_VERSION, 12 | }, 13 | Dxgi::{CreateDXGIFactory2, IDXGIAdapter1, IDXGIFactory5, DXGI_CREATE_FACTORY_FLAGS}, 14 | }, 15 | }, 16 | }; 17 | 18 | #[derive(thiserror::Error, Debug)] 19 | pub enum Direct3DError { 20 | #[error("Direct3DError({0:?})")] 21 | Win32(#[from] Error), 22 | #[error("Direct3DError(\"{0}\")")] 23 | Other(&'static str), 24 | } 25 | 26 | impl From<&'static str> for Direct3DError { 27 | fn from(value: &'static str) -> Self { 28 | Direct3DError::Other(value) 29 | } 30 | } 31 | 32 | #[derive(Debug)] 33 | pub struct Direct3DDevice { 34 | // The following are already refcounted, so they're safe to use directly without additional drop impls 35 | _dxgi_factory: IDXGIFactory5, 36 | _adapter: IDXGIAdapter1, 37 | pub device: ID3D11Device, 38 | _device_context: ID3D11DeviceContext, 39 | } 40 | 41 | impl Direct3DDevice { 42 | pub fn init(adapter_luid: LUID) -> Result { 43 | let dxgi_factory = 44 | unsafe { CreateDXGIFactory2::(DXGI_CREATE_FACTORY_FLAGS(0))? }; 45 | 46 | let adapter = unsafe { dxgi_factory.EnumAdapterByLuid::(adapter_luid)? }; 47 | 48 | let mut device = None; 49 | let mut device_context = None; 50 | 51 | unsafe { 52 | D3D11CreateDevice( 53 | &adapter, 54 | D3D_DRIVER_TYPE_UNKNOWN, 55 | None, 56 | D3D11_CREATE_DEVICE_BGRA_SUPPORT 57 | | D3D11_CREATE_DEVICE_SINGLETHREADED 58 | | D3D11_CREATE_DEVICE_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY, 59 | None, 60 | D3D11_SDK_VERSION, 61 | Some(&mut device), 62 | None, 63 | Some(&mut device_context), 64 | )?; 65 | } 66 | 67 | let device = device.ok_or("ID3D11Device not found")?; 68 | let device_context = device_context.ok_or("ID3D11DeviceContext not found")?; 69 | 70 | Ok(Self { 71 | _dxgi_factory: dxgi_factory, 72 | _adapter: adapter, 73 | device, 74 | _device_context: device_context, 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /rust/vdd-user-session-service/src/main.rs: -------------------------------------------------------------------------------- 1 | mod service; 2 | mod set_privileges; 3 | 4 | use clap::Parser; 5 | use std::ffi::OsString; 6 | use windows_service::{ 7 | service::{ 8 | ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState, 9 | ServiceType, 10 | }, 11 | service_manager::{ServiceManager, ServiceManagerAccess}, 12 | }; 13 | 14 | use self::service::start_service; 15 | 16 | const SERVICE_NAME: &str = "vdd-user-session-initializer"; 17 | const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; 18 | 19 | #[derive(Parser, Debug)] 20 | #[command(version, about, long_about = None)] 21 | struct Args { 22 | /// Install the service 23 | #[arg(short, long, conflicts_with = "uninstall")] 24 | install: bool, 25 | 26 | /// Uninstall the service 27 | #[arg(short, long, conflicts_with = "install")] 28 | uninstall: bool, 29 | } 30 | 31 | fn main() -> Result<(), windows_service::Error> { 32 | let args = Args::parse(); 33 | 34 | if args.install { 35 | let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; 36 | let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; 37 | 38 | let service_binary_path = ::std::env::current_exe().unwrap(); 39 | 40 | let service_info = ServiceInfo { 41 | name: OsString::from(SERVICE_NAME), 42 | display_name: OsString::from("Virtual Display Driver User Session Initializer Service"), 43 | service_type: SERVICE_TYPE, 44 | start_type: ServiceStartType::AutoStart, 45 | error_control: ServiceErrorControl::Normal, 46 | executable_path: service_binary_path, 47 | launch_arguments: vec![], 48 | dependencies: vec![], 49 | account_name: None, // run as System 50 | account_password: None, 51 | }; 52 | 53 | let service = 54 | service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?; 55 | service.set_description("Watches for log on and log off events and starts/stops virtual monitors based on user persisted data")?; 56 | 57 | return Ok(()); 58 | } else if args.uninstall { 59 | let manager_access = ServiceManagerAccess::CONNECT; 60 | let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; 61 | 62 | let service_access = 63 | ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; 64 | let service = service_manager.open_service(SERVICE_NAME, service_access)?; 65 | 66 | // The service will be marked for deletion as long as this function call succeeds. 67 | // However, it will not be deleted from the database until it is stopped and all open handles to it are closed. 68 | service.delete()?; 69 | // Our handle to it is not closed yet. So we can still query it. 70 | if service.query_status()?.current_state != ServiceState::Stopped { 71 | // If the service cannot be stopped, it will be deleted when the system restarts. 72 | service.stop()?; 73 | } 74 | 75 | return Ok(()); 76 | } 77 | 78 | start_service()?; 79 | 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Views/MonitorsView.xaml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 3 13 | 14 | 15 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | My block 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 67 | 68 | 69 | My block 70 | 71 | 72 | 73 | 74 | 79 | 80 | 81 | 82 | 83 | My block 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Helpers/MaterialHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Composition.SystemBackdrops; 2 | using Microsoft.UI.Xaml.Media; 3 | using Virtual_Display_Driver_Control.Common; 4 | using WinUIEx; 5 | 6 | namespace Virtual_Display_Driver_Control.Helpers; 7 | 8 | class MaterialHelper { 9 | private static MicaBackdrop micaBackdrop = new MicaBackdrop() { Kind = MicaKind.Base }; 10 | private static MicaBackdrop micaBackdropAlt = new MicaBackdrop() { Kind = MicaKind.BaseAlt }; 11 | private static DesktopAcrylicBackdrop acrylicBackdrop = new DesktopAcrylicBackdrop(); 12 | private static TransparentTintBackdrop transparentTintBackdrop = new TransparentTintBackdrop(); 13 | private static BlurredBackdrop blurredBackdrop = new BlurredBackdrop(); 14 | 15 | private class BlurredBackdrop : CompositionBrushBackdrop { 16 | protected override Windows.UI.Composition.CompositionBrush CreateBrush(Windows.UI.Composition.Compositor compositor) 17 | => compositor.CreateHostBackdropBrush(); 18 | } 19 | 20 | public static void Initialize() { 21 | SetMaterial(App.Settings.Material); 22 | } 23 | 24 | public static void SetMaterial(Material material) { 25 | AppSettings Settings = App.Settings; 26 | 27 | if (material == Material.Mica && MicaController.IsSupported()) { 28 | App.Window.SystemBackdrop = micaBackdrop; 29 | Settings.Material = material; 30 | } else if (material == Material.MicaAlt && MicaController.IsSupported()) { 31 | App.Window.SystemBackdrop = micaBackdropAlt; 32 | Settings.Material = material; 33 | } else if (material == Material.Acrylic && DesktopAcrylicController.IsSupported()) { 34 | App.Window.SystemBackdrop = acrylicBackdrop; 35 | Settings.Material = material; 36 | } else if (material == Material.Blurred && DesktopAcrylicController.IsSupported()) { 37 | App.Window.SystemBackdrop = blurredBackdrop; 38 | Settings.Material = material; 39 | } else if (material == Material.Transparent) { 40 | App.Window.SystemBackdrop = transparentTintBackdrop; 41 | Settings.Material = material; 42 | } else { 43 | App.Window.SystemBackdrop = null; 44 | Settings.Material = Material.None; 45 | } 46 | 47 | ThemeHelper.ApplyBackground(Settings.Theme); 48 | } 49 | 50 | // Checks if material is supported 51 | public static bool isSupported(Material material) { 52 | if (material == Material.Mica || material == Material.MicaAlt) { 53 | return MicaController.IsSupported(); 54 | } else if (material == Material.Acrylic || material == Material.Blurred) { 55 | return DesktopAcrylicController.IsSupported(); 56 | } else { 57 | return true; 58 | } 59 | } 60 | 61 | public static bool isMicaSupported(Material material) { 62 | if (material == Material.Mica || material == Material.MicaAlt) { 63 | return MicaController.IsSupported(); 64 | } 65 | 66 | return false; 67 | } 68 | 69 | public static bool isAcrylicSupported(Material material) { 70 | if (material == Material.Acrylic || material == Material.Blurred) { 71 | return DesktopAcrylicController.IsSupported(); 72 | } 73 | 74 | return false; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/App.xaml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | Virtual Display Driver Control 20 | 21 | Transparent 22 | Transparent 23 | 24 | 25 | 26 | 27 | 30 | Black 31 | Black 32 | Black 33 | #E9E9E9 34 | #EDEDED 35 | Black 36 | 37 | 40 | 41 | 44 | 45 | 46 | 47 | 50 | White 51 | White 52 | White 53 | #191919 54 | #1D1D1D 55 | White 56 | 57 | 60 | 61 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | using Serilog; 3 | using System; 4 | using System.Diagnostics; 5 | using System.Text; 6 | using Virtual_Display_Driver_Control.Common; 7 | using Virtual_Display_Driver_Control.Helpers; 8 | 9 | namespace Virtual_Display_Driver_Control; 10 | 11 | public partial class App : Application { 12 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 13 | public static Window Window { get; private set; } 14 | public static AppSettings Settings { get; private set; } 15 | #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 16 | 17 | public App() { 18 | Logging.Initialize(); 19 | 20 | UnhandledException += App_UnhandledException; 21 | AppDomain.CurrentDomain.UnhandledException += Domain_UnhandledException; 22 | InitializeComponent(); 23 | Settings = SettingsProvider.Initialize(); 24 | } 25 | 26 | protected override void OnLaunched(LaunchActivatedEventArgs args) { 27 | Window = new MainWindow(); 28 | 29 | ThemeHelper.Initialize(); 30 | MaterialHelper.Initialize(); 31 | 32 | Window.Activate(); 33 | Window.Closed += OnClosed; 34 | } 35 | 36 | private void OnClosed(object sender, WindowEventArgs e) { 37 | // cleanup ops 38 | Log.CloseAndFlush(); 39 | } 40 | 41 | void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) { 42 | _UnhandledException(sender, e.Exception); 43 | } 44 | 45 | void Domain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e) { 46 | _UnhandledException(sender, (Exception)e.ExceptionObject); 47 | } 48 | 49 | async void _UnhandledException(object sender, Exception ex) { 50 | StringBuilder formattedException = new StringBuilder() { Capacity = 200 }; 51 | 52 | formattedException.Append("\n--------- UNHANDLED EXCEPTION ---------"); 53 | 54 | if (ex is not null) { 55 | formattedException.Append($"\n>>>> HRESULT: {ex.HResult}\n"); 56 | if (ex.Message is not null) { 57 | formattedException.Append("\n--- MESSAGE ---\n"); 58 | formattedException.Append(ex.Message); 59 | } 60 | if (ex.StackTrace is not null) { 61 | formattedException.Append("\n--- STACKTRACE ---\n"); 62 | formattedException.Append(ex.StackTrace); 63 | } 64 | if (ex.Source is not null) { 65 | formattedException.Append("\n--- SOURCE ---\n"); 66 | formattedException.Append(ex.Source); 67 | } 68 | if (ex.InnerException is not null) { 69 | formattedException.Append("\n--- INNER ---\n"); 70 | formattedException.Append(ex.InnerException); 71 | } 72 | } else { 73 | formattedException.Append("\nException is null!\n"); 74 | } 75 | 76 | formattedException.Append("\n---------------------------------------\n"); 77 | 78 | Log.Fatal(formattedException.ToString()); 79 | 80 | Log.CloseAndFlush(); 81 | 82 | // Please check "Output Window" for exception details (View -> Output Window) (CTRL + ALT + O) 83 | Debugger.Break(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/src/edid.rs: -------------------------------------------------------------------------------- 1 | use std::{array::TryFromSliceError, ops::Deref}; 2 | 3 | use bytemuck::{Pod, Zeroable}; 4 | 5 | const _EDID: [u8; 128] = [ 6 | 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x0D, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 7 | 0xFF, 0x21, 0x01, 0x03, 0x80, 0x32, 0x1F, 0x78, 0x07, 0xEE, 0x95, 0xA3, 0x54, 0x4C, 0x99, 0x26, 8 | 0x0F, 0x50, 0x54, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 9 | 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3A, 0x80, 0x18, 0x71, 0x38, 0x2D, 0x40, 0x58, 0x2C, 10 | 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xFD, 0x00, 0x17, 0xF0, 0x0F, 11 | 0xFF, 0x0F, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x56, 12 | 0x69, 0x72, 0x74, 0x75, 0x44, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x2B, 0x00, 0x00, 0x00, 0x00, 13 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14 | ]; 15 | 16 | const EDID_LEN: usize = _EDID.len(); 17 | 18 | static EDID: AlignedEdid = AlignedEdid { 19 | data: _EDID, 20 | _align: [], 21 | }; 22 | 23 | #[repr(C)] 24 | struct AlignedEdid { 25 | data: [u8; N], 26 | // required to make this type aligned to Edid 27 | _align: [Edid; 0], 28 | } 29 | 30 | impl AlignedEdid { 31 | fn new(data: &[u8]) -> Result { 32 | let data: [u8; N] = data.try_into()?; 33 | Ok(Self { data, _align: [] }) 34 | } 35 | } 36 | 37 | impl Deref for AlignedEdid { 38 | type Target = Edid; 39 | 40 | fn deref(&self) -> &Self::Target { 41 | let header = &self.data[..EDID_SIZE]; 42 | bytemuck::from_bytes(header) 43 | } 44 | } 45 | 46 | const EDID_SIZE: usize = std::mem::size_of::(); 47 | 48 | #[repr(C)] 49 | #[derive(Debug, Copy, Clone, Pod, Zeroable)] 50 | pub struct Edid { 51 | header: [u8; 8], 52 | manufacturer_id: [u8; 2], 53 | product_code: u16, 54 | serial_number: u32, 55 | manufacture_week: u8, 56 | manufacture_year: u8, 57 | version: u8, 58 | revision: u8, 59 | } 60 | 61 | impl Edid { 62 | pub fn generate_with(serial: u32) -> Vec { 63 | // change serial number in the header 64 | let mut header = *EDID; 65 | header.serial_number = serial; 66 | 67 | header.generate() 68 | } 69 | 70 | pub fn get_serial(edid: &[u8]) -> Result { 71 | let edid = AlignedEdid::::new(edid)?; 72 | Ok(edid.serial_number) 73 | } 74 | 75 | fn generate(&self) -> Vec { 76 | let header = bytemuck::bytes_of(self); 77 | 78 | // slice of monitor edid minus header 79 | let data = &EDID.data[EDID_SIZE..]; 80 | 81 | // splice together header and the rest of the EDID 82 | let mut edid: Vec = header.iter().chain(data).copied().collect(); 83 | // regenerate checksum 84 | Self::gen_checksum(&mut edid); 85 | 86 | edid 87 | } 88 | 89 | fn gen_checksum(data: &mut [u8]) { 90 | // important, this is the bare minimum length 91 | assert!(data.len() >= 128); 92 | 93 | // slice to the entire data minus the last checksum byte 94 | let edid_data = &data[..=126]; 95 | 96 | // do checksum calculation 97 | let sum: u32 = edid_data.iter().copied().map(u32::from).sum(); 98 | // this wont ever truncate 99 | #[allow(clippy::cast_possible_truncation)] 100 | let checksum = (256 - (sum % 256)) as u8; 101 | 102 | // update last byte with new checksum 103 | data[127] = checksum; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Before contributing, please read carefully our [Contributor Licence Agreement](CLA.md). When you make a PR for the first time, you will automatically be asked to agree to and sign the CLA by making a comment affirming your agreement. If you are contributing in another way and still need to sign the agreement, you may open a new PR to sign it. 4 | 5 | When contributing to this repository, please first discuss the change you wish to make via issue or any other available method with the owners of this repository before making a change. 6 | 7 | Please note we have a [code of conduct](CODE-OF-CONDUCT.md), please follow it in all your interactions with the project. 8 | 9 | ## Ways to contribute 10 | - [Help others](#help-others) 11 | - [Analyzing issues](#analyzing-issues) 12 | - [Reporting issues](#reporting-issues) 13 | - [Working on issues](#working-on-issues) 14 | - [Contributing code](#contributing-code) 15 | - [Open pull request](#pull-request-process) 16 | - [Sponsoring](https://github.com/sponsors/MolotovCherry) the project. Every little bit helps! 17 | 18 | ## Help others 19 | Helping out with support in the [discord server](https://discord.gg/pDDt78wYQy) or [discussions](https://github.com/MolotovCherry/virtual-display-rs/discussions). 20 | 21 | ## Analyzing issues 22 | Analyzing issues involves helping to solve issues. 23 | 24 | This could involve such things as figuring out how to reproduce it, finding the fix to it, giving additional information, or a hint which helps to understand it more. 25 | 26 | ## Reporting issues 27 | If you find a bug, you are welcome to report it. 28 | We can only handle well-reported, actual bugs, so please follow the guidelines below. 29 | 30 | Once you have familiarized with the guidelines, you can go to the [issue tracker](https://github.com/MolotovCherry/virtual-display-rs/issues/new/choose) to report the issue. 31 | 32 | ### Quick Checklist for Bug Reports: 33 | 34 | Issue report checklist: 35 | * Real, current bug 36 | * No duplicate 37 | * Reproducible 38 | * Good summary 39 | * Well-documented 40 | * Minimal example 41 | 42 | ## Working on issues 43 | Find or create an issue you are interested in working on. On the issue, let the project members know you want to work on that particular issue, and if agreed, you will be assigned to the issue. Please also discuss what changes you want to make, design decisions, or any other pertinent details with project members so you can ensure that none of your or our time is wasted. 44 | 45 | ## Contributing code 46 | These are some of the rules we try to follow: 47 | 48 | - Apply a clean coding style adapted to the surrounding code 49 | - Use same indentation as surrounding project/code 50 | - Use variable naming conventions like in the other files you are seeing (camelcase) 51 | - Comment your code where it gets non-trivial 52 | - Keep an eye on performance and memory consumption 53 | - Keep a careful eye out for compatibility with previous code. Especially if it has to do with public apis. 54 | - Maintain high code quality. Please ensure your code is - to the best of your ability - readable, well-formed, and documented. 55 | - Please ensure you have handled any lint warnings in the appropriate project(s). If you have a reason the lint should be allowed, then please explicitly allow the lint(s) so they no longer trigger a warning. Note that allowing a lint should only be done when there's a very good reason. 56 | 57 | ## Pull Request Process 58 | 59 | 1. Ensure any extraneous files such as generated files from install, build dependencies, or other kinds of temporary project files are removed from the PR. 60 | 2. If applicable, update the README.md with any appropriate changes you've made. 61 | 62 | # Contact 63 | - [Discord server](https://discord.gg/pDDt78wYQy) 64 | - [Discussions](https://github.com/MolotovCherry/virtual-display-rs/discussions) 65 | - Project leadership: /molotov/cherry/ (Discord) 66 | - Remove all forward slashes in names. They are there to prevent bots from harvesting names. 67 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Common/AppSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text.Json; 5 | using CSharpFunctionalExtensions; 6 | using Humanizer; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.UI.Xaml; 9 | 10 | namespace Virtual_Display_Driver_Control.Common; 11 | 12 | public sealed class AppSettings { 13 | public ElementTheme Theme { get; set; } = ElementTheme.Default; 14 | public Material Material { get; set; } = Material.Mica; 15 | public UpdateVersion UpdateVersion { get; set; } = new UpdateVersion(); 16 | 17 | public async void Save() { 18 | try { 19 | string json = JsonSerializer.Serialize(this); 20 | await File.WriteAllTextAsync(SettingsProvider.SettingsPath, json); 21 | } catch { } 22 | } 23 | } 24 | 25 | public sealed class UpdateVersion { 26 | public Version Version { get; set; } = new Version(); 27 | public string ReleaseUrl { get; set; } = ""; 28 | public List Assets { get; set; } = new List(); 29 | public DateTime? LastUpdate { get; set; } 30 | 31 | public string LastUpdateHuman() { 32 | return LastUpdate.Humanize(); 33 | } 34 | } 35 | 36 | public sealed class Version : IComparable, IEquatable { 37 | public int Major { get; set; } 38 | public int Minor { get; set; } 39 | public int Patch { get; set; } 40 | 41 | public static bool operator <(Version version1, Version version2) { 42 | return version1.CompareTo(version2) < 0; 43 | } 44 | 45 | public static bool operator >(Version version1, Version version2) { 46 | return version1.CompareTo(version2) > 0; 47 | } 48 | 49 | public int CompareTo(Version? version) { 50 | if (version == null) 51 | return 1; 52 | 53 | if (ReferenceEquals(version, this)) 54 | return 0; 55 | 56 | var MajorCmp = Major.CompareTo(version.Major); 57 | if (MajorCmp != 0) 58 | return MajorCmp; 59 | 60 | var MinorCmp = Minor.CompareTo(version.Minor); 61 | if (MinorCmp != 0) 62 | return MinorCmp; 63 | 64 | var PatchCmp = Patch.CompareTo(version.Patch); 65 | if (PatchCmp != 0) 66 | return PatchCmp; 67 | 68 | return 0; 69 | } 70 | 71 | public static Maybe Parse(string version) { 72 | try { 73 | System.Version parsedVersion; 74 | if (System.Version.TryParse(version, out parsedVersion!)) { 75 | return new Version { 76 | Major = parsedVersion.Major, 77 | Minor = parsedVersion.Minor, 78 | Patch = parsedVersion.Build 79 | }; 80 | } 81 | 82 | return Maybe.None; 83 | } catch { 84 | return Maybe.None; 85 | } 86 | } 87 | 88 | public bool Equals(Version? version) { 89 | return CompareTo(version) == 0; 90 | } 91 | } 92 | 93 | public sealed class Asset { 94 | public string Name { get; set; } = ""; 95 | public string Url { get; set; } = ""; 96 | } 97 | 98 | public enum Material { 99 | Mica, 100 | MicaAlt, 101 | Acrylic, 102 | Blurred, 103 | Transparent, 104 | None 105 | } 106 | 107 | public static class SettingsProvider { 108 | public static string AppDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "VirtualDisplayDriver"); 109 | public static string Settings = "appsettings.json"; 110 | public static string SettingsPath = Path.Combine(AppDir, Settings); 111 | 112 | public static AppSettings Initialize() { 113 | if (!Directory.Exists(AppDir)) { 114 | Directory.CreateDirectory(AppDir); 115 | } 116 | 117 | IConfiguration configuration; 118 | try { 119 | configuration = new ConfigurationBuilder() 120 | .SetBasePath(AppDir) 121 | .AddJsonFile(Settings) 122 | .Build(); 123 | } catch { 124 | return new AppSettings(); 125 | } 126 | 127 | var appSettings = new AppSettings(); 128 | configuration.Bind(appSettings); 129 | 130 | return appSettings; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/Makefile.toml: -------------------------------------------------------------------------------- 1 | env_scripts = [ 2 | # set the visual studio dev console environment for everyone 3 | ''' 4 | #!@duckscript 5 | CI = get_env CI 6 | Program_Files = get_env ProgramFiles 7 | BAT_PATH = set "${Program_Files}\\Microsoft Visual Studio\\2022\\Community\\Common7\\Tools\\VsDevCmd.bat" 8 | if not is_empty ${CI} 9 | BAT_PATH = set "${Program_Files}\\Microsoft Visual Studio\\2022\\Enterprise\\Common7\\Tools\\VsDevCmd.bat" 10 | end 11 | 12 | output = exec cmd /c call "${BAT_PATH}" && set 13 | stdout = set ${output.stdout} 14 | 15 | env_vars = split ${stdout} \r\n 16 | # remove the first 4 since they are only a header 17 | array_remove ${env_vars} 0 18 | array_remove ${env_vars} 0 19 | array_remove ${env_vars} 0 20 | array_remove ${env_vars} 0 21 | 22 | for var in ${env_vars} 23 | handle = split ${var} = 24 | 25 | key = array_get ${handle} 0 26 | array_remove ${handle} 0 27 | 28 | value = array_join ${handle} = 29 | 30 | if not is_empty ${key} 31 | if not is_empty ${value} 32 | set_env ${key} ${value} 33 | end 34 | end 35 | 36 | release ${handle} 37 | end 38 | 39 | release ${env_vars} 40 | ''', 41 | ] 42 | 43 | [env] 44 | BUILD_DLL_OLD_NAME = "virtual_display_driver.dll" 45 | BUILD_DLL_NAME = "VirtualDisplayDriver.dll" 46 | SIGN_OPTIONS = { script = [''' 47 | if defined CI ( 48 | echo /f ../../private.pfx /p %PRIVATE_KEY_PASSWORD% 49 | ) else ( 50 | echo /sm /n DriverCertificate 51 | ) 52 | '''] } 53 | # default development env settings (set by default) 54 | TARGET_PATH = "x86_64-pc-windows-msvc\\debug" 55 | CAT_FILE = "delta.cat" 56 | 57 | [env.dev] 58 | # for developer inf stamping mode 59 | PRIVATE_DRIVER_PACKAGE = "true" 60 | 61 | [env.development] 62 | # for developer inf stamping mode 63 | PRIVATE_DRIVER_PACKAGE = "true" 64 | 65 | [env.prod] 66 | TARGET_PATH = "x86_64-pc-windows-msvc\\release" 67 | BUILD_FLAGS = "--release" 68 | CAT_FILE = "VirtualDisplayDriver.cat" 69 | 70 | [tasks.set-build-path] 71 | env = { "BUILD_TARGET_PATH" = { script = [''' 72 | for /f "tokens=*" %%a in ('cargo target-dir') do set target_dir=%%a 73 | 74 | echo %target_dir%\%TARGET_PATH% 75 | '''] } } 76 | 77 | [tasks.build-driver] 78 | dependencies = ["set-build-path"] 79 | script = ["cargo b %BUILD_FLAGS%"] 80 | 81 | [tasks.rename] 82 | dependencies = ["build-driver"] 83 | script = [''' 84 | cd /D %BUILD_TARGET_PATH% 85 | del /f /q "%BUILD_DLL_NAME%" 86 | ren "%BUILD_DLL_OLD_NAME%" "%BUILD_DLL_NAME%" 87 | '''] 88 | 89 | [tasks.stamp-inf] 90 | dependencies = ["build-driver"] 91 | script = [ 92 | # copy inf to target dir 93 | "copy /y VirtualDisplayDriver.inf \"%BUILD_TARGET_PATH%/VirtualDisplayDriver.inf\"", 94 | 95 | # Stamp inf 96 | "stampinf.exe -v \"%CARGO_MAKE_PROJECT_VERSION%\" -d * -a amd64 -u 2.15.0 -f \"%BUILD_TARGET_PATH%/VirtualDisplayDriver.inf\"", 97 | ] 98 | 99 | [tasks.gen-cat] 100 | dependencies = ["build-driver", "rename", "stamp-inf", "sign"] 101 | script = [ 102 | # generate and sign cat file 103 | ''' 104 | inf2cat /driver:%BUILD_TARGET_PATH% /os:10_x64,10_AU_X64,10_RS2_X64,10_RS3_X64,10_RS4_X64,10_RS5_X64,10_19H1_X64,10_VB_X64,10_CO_X64,10_NI_X64 105 | 106 | signtool sign /a /fd SHA256 /v %SIGN_OPTIONS% /t http://timestamp.digicert.com "%BUILD_TARGET_PATH%/%CAT_FILE%" 107 | ''', 108 | ] 109 | 110 | [tasks.sign] 111 | dependencies = ["build-driver", "rename", "stamp-inf"] 112 | script = [ 113 | # Create a self signed certificate (only if not already done) 114 | ''' 115 | certutil -store "My" | findstr /i "DriverCertificate" >NUL 2>NUL 116 | if not defined CI ( 117 | if %errorlevel%==1 ( 118 | pwsh -Command Start-Process cmd -ArgumentList '/c "\"%ProgramFiles%\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat\" && makecert -r -pe -ss my -sr localmachine -n CN=DriverCertificate DriverCertificate.cer"' -Verb RunAs 119 | ) 120 | ) 121 | ''', 122 | 123 | # Sign the driver. If this fails, you probably installed the cert before this. Run it again 124 | "signtool sign /a /fd SHA256 /v %SIGN_OPTIONS% /t http://timestamp.digicert.com \"%BUILD_TARGET_PATH%/%BUILD_DLL_NAME%\"", 125 | ] 126 | 127 | # copy all files to an output folder of artifacts 128 | [tasks.copy] 129 | dependencies = ["set-build-path"] 130 | script = [ 131 | ''' 132 | if not exist "..\\target\\output" ( 133 | echo Directory not found, creating it... 134 | mkdir ..\\target\\output 135 | ) else ( 136 | echo Directory found, deleting files... 137 | del /S /Q /F ..\\target\\output\\*.dll 138 | del /S /Q /F ..\\target\\output\\*.inf 139 | del /S /Q /F ..\\target\\output\\*.cat 140 | ) 141 | ''', 142 | # copy output files to it 143 | ''' 144 | copy %BUILD_TARGET_PATH%\*.dll ..\target\output 145 | copy %BUILD_TARGET_PATH%\*.inf ..\target\output 146 | copy %BUILD_TARGET_PATH%\*.cat ..\target\output 147 | ''', 148 | ] 149 | 150 | [tasks.build] 151 | dependencies = ["build-driver", "rename", "sign", "stamp-inf", "gen-cat"] 152 | clear = true 153 | -------------------------------------------------------------------------------- /rust/driver-ipc/src/mock.rs: -------------------------------------------------------------------------------- 1 | use std::{io, sync::Arc}; 2 | 3 | use tokio::{ 4 | io::{AsyncReadExt, AsyncWriteExt}, 5 | net::windows::named_pipe, 6 | sync::{broadcast, Notify}, 7 | task, 8 | }; 9 | 10 | use crate::*; 11 | 12 | use self::client::EOF; 13 | 14 | pub struct MockServer { 15 | server: Arc, 16 | state: Vec, 17 | command_rx: broadcast::Receiver, 18 | command_tx: broadcast::Sender, 19 | notify_closed: Arc, 20 | } 21 | 22 | impl MockServer { 23 | pub fn new(name: &str) -> Self { 24 | let server = named_pipe::ServerOptions::new() 25 | .access_inbound(true) 26 | .access_outbound(true) 27 | .reject_remote_clients(true) 28 | .create(format!(r"\\.\pipe\{}", name)) 29 | .unwrap(); 30 | let server = Arc::new(server); 31 | 32 | let notify_closed = Arc::new(Notify::new()); 33 | 34 | let (command_tx, command_rx) = broadcast::channel(64); 35 | 36 | { 37 | let server = server.clone(); 38 | let command_tx = command_tx.clone(); 39 | let notify_closed = notify_closed.clone(); 40 | task::spawn(async move { 41 | let server = unsafe { 42 | (server.as_ref() as *const _ as *mut named_pipe::NamedPipeServer) 43 | .as_mut() 44 | .unwrap() 45 | }; 46 | 47 | server.connect().await.expect("Failed to connect to server"); 48 | 49 | loop { 50 | let mut buf = vec![]; 51 | loop { 52 | let byte = tokio::select! { 53 | _ = notify_closed.notified() => return, 54 | r = server.read_u8() => r, 55 | }; 56 | 57 | let v = match byte { 58 | Ok(v) => v, 59 | Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return, // Client disconnected 60 | Err(_) => continue, 61 | }; 62 | if v == EOF { 63 | break; 64 | } 65 | buf.push(v); 66 | } 67 | 68 | let cmd = serde_json::from_slice::(&buf) 69 | .expect("Failed to deserialize request"); 70 | 71 | command_tx.send(cmd).expect("Failed to send command"); 72 | } 73 | }); 74 | } 75 | 76 | Self { 77 | server, 78 | state: vec![], 79 | command_rx, 80 | command_tx, 81 | notify_closed, 82 | } 83 | } 84 | 85 | pub fn state(&self) -> &[Monitor] { 86 | &self.state 87 | } 88 | 89 | pub fn check_next(&mut self, cb: impl FnOnce(ServerCommand) + Send + 'static) { 90 | let mut rx = self.command_tx.subscribe(); 91 | 92 | task::spawn(async move { 93 | loop { 94 | match rx.recv().await { 95 | Ok(cmd) => { 96 | cb(cmd); 97 | break; 98 | } 99 | Err(_) => continue, 100 | } 101 | } 102 | }); 103 | } 104 | 105 | pub async fn pump(&mut self) { 106 | let cmd = self.command_rx.recv().await.unwrap(); 107 | 108 | let server = unsafe { 109 | (self.server.as_ref() as *const _ as *mut named_pipe::NamedPipeServer) 110 | .as_mut() 111 | .unwrap() 112 | }; 113 | 114 | let changed = match cmd { 115 | ServerCommand::Request(RequestCommand::State) => { 116 | let reply = ReplyCommand::State(self.state.clone()); 117 | let mut reply = serde_json::to_vec(&reply).unwrap(); 118 | reply.push(EOF); 119 | 120 | server 121 | .write_all(&reply) 122 | .await 123 | .expect("Failed to write reply"); 124 | false 125 | } 126 | ServerCommand::Driver(DriverCommand::Notify(monitors)) => { 127 | self.state = monitors; 128 | true 129 | } 130 | ServerCommand::Driver(DriverCommand::Remove(ids)) => { 131 | self.state.retain(|m| !ids.contains(&m.id)); 132 | true 133 | } 134 | ServerCommand::Driver(DriverCommand::RemoveAll) => { 135 | self.state.clear(); 136 | true 137 | } 138 | }; 139 | 140 | if changed { 141 | let event = EventCommand::Changed(self.state.clone()); 142 | let mut event = serde_json::to_vec(&event).unwrap(); 143 | event.push(EOF); 144 | 145 | server 146 | .write_all(&event) 147 | .await 148 | .expect("Failed to write event"); 149 | } 150 | } 151 | } 152 | 153 | impl Drop for MockServer { 154 | fn drop(&mut self) { 155 | self.notify_closed.notify_waiters(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /examples/monitor_control.py: -------------------------------------------------------------------------------- 1 | from vdd import * 2 | 3 | # 4 | # Before you begin, please note the following: 5 | # 6 | # 1. All Monitors must have a unique Id 7 | # 2. All Modes under a Monitor must have a unique width/height 8 | # 3. All refresh rates under a Mode must be unique 9 | # 10 | # Every class, attribute, and function is annotated with a __doc__, which also shows the type's 11 | # type signature. 12 | # 13 | # Final note: It is possible to have stale data in memory, and this can cause duplicate Ids. 14 | # However, if it is sent to the driver, the driver will simply ignore the duplicates. 15 | # When notify() is done, it DOES NOT check the latest data! You must reconcile differences via 16 | # either get_state(), or set up a receiver() to be notified of new changes 17 | # 18 | 19 | # make the client 20 | client = DriverClient() 21 | # you can see what's in it 22 | print(client) 23 | # DriverClient { monitors: [Monitor { id: 0, name: None, enabled: true, modes: [Mode { width: 1920, height: 1080, refresh_rates: [90, 120] }] }] } 24 | 25 | # monitors are stored at 26 | print(client.monitors) 27 | # [Monitor { id: 0, name: None, enabled: true, modes: [Mode { width: 1920, height: 1080, refresh_rates: [90, 120] }] }] 28 | 29 | # 30 | # Monitor functionality 31 | # 32 | 33 | # to get a monitor, just index 34 | client.monitors[0] 35 | # set id 36 | client.monitors[0].id = 0 37 | # set name 38 | client.monitors[0].name = "MyName" 39 | # you can unset the name 40 | client.monitors[0].name = None 41 | # enable or disable monitor 42 | client.monitors[0].enabled = False 43 | 44 | # delete a monitor we don't want 45 | del client.monitors[0] 46 | 47 | # create new monitor (set it up as you want) 48 | new_mon = Monitor() 49 | client.monitors[0] = new_mon 50 | 51 | # add a new monitor to list 52 | client.monitors += Monitor() 53 | # or add multiple 54 | client.monitors += [Monitor(), Monitor()] 55 | 56 | # you can iterate over them 57 | for mon in client.monitors: 58 | print(mon) 59 | # Monitor { id: 0, name: None, enabled: true, modes: [Mode { width: 1920, height: 1080, refresh_rates: [90, 120] }] } 60 | print(mon.modes) 61 | # [Mode { width: 1920, height: 1080, refresh_rates: [90, 120] }] 62 | 63 | # 64 | # Modes 65 | # 66 | 67 | # access a mode by index 68 | print(client.monitors[0].modes[0]) 69 | # Mode { width: 1920, height: 1080, refresh_rates: [90, 120] } 70 | 71 | # set width 72 | client.monitors[0].modes[0].width = 1000 73 | # set height 74 | client.monitors[0].modes[0].height = 1000 75 | # check out refresh rates 76 | print(client.monitors[0].modes[0].refresh_rates) 77 | # [90, 120] 78 | 79 | # add a new mode 80 | new_mode = Mode() 81 | # set up properties like normal 82 | # add mode to list 83 | client.monitors[0].modes += new_mode 84 | # or add multiple 85 | client.monitors[0].modes += [Mode(), Mode()] 86 | # delete a mode we don't want 87 | del client.monitors[0].modes[0] 88 | 89 | # 90 | # Refresh Rates 91 | # 92 | 93 | # add a refresh rate 94 | client.monitors[0].modes[0].modes[0].refresh_rates += 90 95 | 96 | # add multiple refresh rates 97 | client.monitors[0].modes[0].modes[0].refresh_rates += [90, 120, 240] 98 | 99 | # delete a refresh rate 100 | del client.monitors[0].modes[0].modes[0].refresh_rates[0] 101 | 102 | # set a refresh rate 103 | client.monitors[0].modes[0].modes[0].refresh_rates[0] = 90 104 | 105 | # 106 | # DriverClient functions 107 | # 108 | 109 | # find a Monitor by id or name 110 | # 111 | # DriverClient.find(int | str) -> Optional[Monitor] 112 | client.find(5) 113 | client.find("name") 114 | 115 | # Get the closest available free ID. Note that if internal state is stale, this may result in a duplicate ID 116 | # which the driver will ignore when you notify it of changes 117 | # 118 | # DriverClient.new_id(id: Optional[int] = None) -> Optional[int] 119 | client.new_id() 120 | # you can ask for a preferred id, and it'll give it to you if available. 121 | # if the id you asked for is a duplicate, None gets returned 122 | client.new_id(5) 123 | 124 | # send changes to driver. all changes are done in-memory until you notify 125 | client.notify() 126 | 127 | # save (persist) current in-memory changes to user across reboots 128 | client.persist() 129 | 130 | # if any other clients elsewhere modify client while your script is running 131 | # you can ask to be notified. 132 | # this represents the complete current state of the driver 133 | # 134 | # once the subscriber is dropped, the function will no longer receive updates 135 | # 136 | # DriverClient.receive(Callable[list[Monitor], None]) 137 | subscriber = client.receive(lambda d: print(d)) 138 | # one way to use this might be to auto update your driver instance 139 | def set_monitors(data): 140 | client.monitors = data 141 | # calling it on a new callback will cancel the old one and set the new one 142 | client.receive(set_monitors) 143 | # calling it with no args will cancel the current receiver 144 | client.receive() 145 | 146 | # gets latest states from driver 147 | # 148 | # DriverClient.get_state() -> list[Monitor] 149 | client.get_state() 150 | 151 | # remove monitors by id 152 | # 153 | # DriverClient.remove(list[int]) 154 | client.remove([1,2,3]) 155 | 156 | # set enable status on monitors by id or name 157 | # 158 | # DriverClient.set_enabled(list[int | str], bool) 159 | client.set_enabled([1,2,3,"name"], true) 160 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Helpers/ThemeHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI; 2 | using Microsoft.UI.Composition.SystemBackdrops; 3 | using Microsoft.UI.Windowing; 4 | using Microsoft.UI.Xaml; 5 | using Microsoft.UI.Xaml.Controls; 6 | using Microsoft.UI.Xaml.Media; 7 | using System.Runtime.InteropServices; 8 | using Virtual_Display_Driver_Control.Common; 9 | using Windows.UI; 10 | using Windows.UI.ViewManagement; 11 | 12 | namespace Virtual_Display_Driver_Control.Helpers; 13 | 14 | public static class ThemeHelper { 15 | [DllImport("UXTheme.dll", SetLastError = true, EntryPoint = "#138")] 16 | public static extern bool ShouldSystemUseDarkMode(); 17 | 18 | private static UISettings uiSettings = new UISettings(); 19 | 20 | public static ElementTheme GetTheme() { 21 | if (App.Window.Content is FrameworkElement frameworkElement) { 22 | return frameworkElement.ActualTheme; 23 | } 24 | 25 | return ElementTheme.Default; 26 | } 27 | 28 | public static void SetTheme(ElementTheme theme) { 29 | if (App.Window.Content is FrameworkElement frameworkElement) { 30 | App.Settings.Theme = theme; 31 | 32 | SetThemeTitlebar(theme); 33 | 34 | uiSettings.ColorValuesChanged -= ColorChangedCb; 35 | frameworkElement.RequestedTheme = theme; 36 | 37 | if (theme == ElementTheme.Default) { 38 | uiSettings.ColorValuesChanged += ColorChangedCb; 39 | } 40 | 41 | ApplyBackground(theme); 42 | } 43 | } 44 | 45 | private static void ColorChangedCb(UISettings sender, object args) { 46 | var dispatcher = App.Window.DispatcherQueue; 47 | 48 | // run it on the main window thread 49 | dispatcher?.TryEnqueue(() => { 50 | ElementTheme theme; 51 | if (ShouldSystemUseDarkMode()) { 52 | theme = ElementTheme.Dark; 53 | } else { 54 | theme = ElementTheme.Light; 55 | } 56 | 57 | SetThemeTitlebar(theme); 58 | ApplyBackground(theme); 59 | }); 60 | } 61 | 62 | private static void SetThemeTitlebar(ElementTheme theme) { 63 | if (App.Window.AppWindow.TitleBar is AppWindowTitleBar titleBar && AppWindowTitleBar.IsCustomizationSupported()) { 64 | var resources = (ResourceDictionary)Application.Current.Resources.ThemeDictionaries; 65 | 66 | ResourceDictionary resourceTheme; 67 | if (theme == ElementTheme.Light) { 68 | resourceTheme = (ResourceDictionary)resources["Light"]; 69 | } else if (theme == ElementTheme.Dark) { 70 | resourceTheme = (ResourceDictionary)resources["Dark"]; 71 | // The rest are ElementTheme.Default 72 | } else if (ShouldSystemUseDarkMode()) { 73 | resourceTheme = (ResourceDictionary)resources["Dark"]; 74 | } else { 75 | resourceTheme = (ResourceDictionary)resources["Light"]; 76 | } 77 | 78 | titleBar.ButtonForegroundColor = (Color)resourceTheme["ButtonForegroundColor"]; 79 | titleBar.ButtonInactiveForegroundColor = (Color)resourceTheme["ButtonInactiveForegroundColor"]; 80 | titleBar.ButtonHoverForegroundColor = (Color)resourceTheme["ButtonHoverForegroundColor"]; 81 | titleBar.ButtonHoverBackgroundColor = (Color)resourceTheme["ButtonHoverBackgroundColor"]; 82 | titleBar.ButtonPressedBackgroundColor = (Color)resourceTheme["ButtonPressedBackgroundColor"]; 83 | titleBar.ButtonPressedForegroundColor = (Color)resourceTheme["ButtonPressedForegroundColor"]; 84 | } 85 | } 86 | 87 | public static void ApplyBackground(ElementTheme theme) { 88 | var appResources = (ResourceDictionary)Application.Current.Resources.ThemeDictionaries; 89 | 90 | ResourceDictionary resourceTheme; 91 | if (theme == ElementTheme.Dark) { 92 | resourceTheme = (ResourceDictionary)appResources["Dark"]; 93 | } else if (theme == ElementTheme.Light) { 94 | resourceTheme = (ResourceDictionary)appResources["Light"]; 95 | // The rest are ElementTheme.Default 96 | } else if (ShouldSystemUseDarkMode()) { 97 | resourceTheme = (ResourceDictionary)appResources["Dark"]; 98 | } else { 99 | resourceTheme = (ResourceDictionary)appResources["Light"]; 100 | } 101 | 102 | var material = App.Settings.Material; 103 | SolidColorBrush brush; 104 | if (MaterialHelper.isMicaSupported(material)) { 105 | brush = new SolidColorBrush(Colors.Transparent); 106 | } else if (material == Material.Acrylic && DesktopAcrylicController.IsSupported()) { 107 | brush = new SolidColorBrush(Colors.Transparent); 108 | } else if (material == Material.Blurred && DesktopAcrylicController.IsSupported()) { 109 | brush = (SolidColorBrush)resourceTheme["BackgroundBlurred"]; 110 | } else if (material == Material.Transparent) { 111 | brush = (SolidColorBrush)resourceTheme["BackgroundTransparent"]; 112 | } else { 113 | brush = (SolidColorBrush)resourceTheme["Background"]; 114 | } 115 | 116 | Grid rootGrid = (Grid)App.Window.Content; 117 | 118 | rootGrid.Background = brush; 119 | } 120 | 121 | public static void Initialize() { 122 | SetTheme(App.Settings.Theme); 123 | } 124 | 125 | public static bool IsEnabled() { 126 | // high contrast theme does not allow themes to be changed 127 | var accessibilitySettings = new AccessibilitySettings(); 128 | return !accessibilitySettings.HighContrast; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /rust/driver-logger/src/win_logger.rs: -------------------------------------------------------------------------------- 1 | //! A simple [Rust log](https://docs.rs/log/latest/log/) backend 2 | //! to send messages to [Windows event log](https://docs.microsoft.com/en-us/windows/desktop/eventlog/event-logging). 3 | 4 | #![warn(missing_docs)] 5 | #![allow(unused, clippy::unused_self)] 6 | 7 | use std::path::Path; 8 | 9 | use log::{Level, LevelFilter, Metadata, Record, SetLoggerError}; 10 | use widestring::U16CString; 11 | use windows_sys::Win32::{ 12 | Foundation::HANDLE, 13 | System::EventLog::{ 14 | DeregisterEventSource, RegisterEventSourceW, ReportEventW, EVENTLOG_ERROR_TYPE, 15 | EVENTLOG_INFORMATION_TYPE, EVENTLOG_WARNING_TYPE, 16 | }, 17 | }; 18 | use winreg::{enums::HKEY_LOCAL_MACHINE, RegKey}; 19 | 20 | // Generated from MC. 21 | const MSG_ERROR: u32 = 0xC000_0001; 22 | const MSG_WARNING: u32 = 0x8000_0002; 23 | const MSG_INFO: u32 = 0x4000_0003; 24 | const MSG_DEBUG: u32 = 0x4000_0004; 25 | const MSG_TRACE: u32 = 0x4000_0005; 26 | 27 | const REG_BASEKEY: &str = "SYSTEM\\CurrentControlSet\\Services\\EventLog\\System"; 28 | 29 | /// Error type of methods in this crate. 30 | #[derive(Debug, thiserror::Error)] 31 | pub enum Error { 32 | /// System error. 33 | #[error("IO error: {0}")] 34 | Io(#[from] std::io::Error), 35 | /// String convertion error. 36 | #[error("String convertion failed")] 37 | StringConvertionFailed, 38 | /// Calling [`log::set_boxed_logger`] failed. 39 | #[error("Set logger failed: {0}")] 40 | SetLoggerFailed(#[from] SetLoggerError), 41 | } 42 | 43 | #[cfg(not(feature = "env_filter"))] 44 | struct Filter {} 45 | #[cfg(not(feature = "env_filter"))] 46 | impl Filter { 47 | fn enabled(&self, _metadata: &Metadata) -> bool { 48 | true 49 | } 50 | fn matches(&self, _record: &Record) -> bool { 51 | true 52 | } 53 | } 54 | #[cfg(not(feature = "env_filter"))] 55 | fn make_filter() -> Filter { 56 | Filter {} 57 | } 58 | 59 | #[cfg(feature = "env_filter")] 60 | use env_filter::Filter; 61 | #[cfg(feature = "env_filter")] 62 | fn make_filter() -> Filter { 63 | let mut builder = env_filter::Builder::from_env("RUST_LOG"); 64 | builder.build() 65 | } 66 | 67 | pub struct WinLogger { 68 | handle: HANDLE, 69 | filter: Filter, 70 | } 71 | 72 | /// Initialize the global logger as the windows event logger. 73 | /// See document of [`register`]. 74 | pub fn init(name: &str, level: LevelFilter) -> Result<(), Error> { 75 | log::set_boxed_logger(Box::new(WinLogger::new(name)?))?; 76 | log::set_max_level(level); 77 | Ok(()) 78 | } 79 | 80 | /// Attempt to remove the event source registry. 81 | /// See document of [`register`]. 82 | pub fn deregister(name: &str) -> Result<(), Error> { 83 | let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); 84 | let cur_ver = hklm.open_subkey(REG_BASEKEY)?; 85 | cur_ver.delete_subkey(name).map_err(From::from) 86 | } 87 | 88 | /// Attempt to add the event source registry. 89 | /// 90 | /// Any event source sould be registried first. 91 | /// You need to call [`register`] when installing the program, 92 | /// and call [`deregister`] when uninstalling the program. 93 | pub fn register(name: &str, register_exe: &Path) -> Result<(), Error> { 94 | let exe_path = register_exe.to_str().ok_or(Error::StringConvertionFailed)?; 95 | 96 | let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); 97 | let cur_ver = hklm.open_subkey(REG_BASEKEY)?; 98 | let (app_key, _) = cur_ver.create_subkey(name)?; 99 | app_key.set_value("EventMessageFile", &exe_path)?; 100 | app_key.set_value("TypesSupported", &7u32)?; 101 | Ok(()) 102 | } 103 | 104 | impl WinLogger { 105 | pub fn new(name: &str) -> Result { 106 | let name = U16CString::from_str(name).map_err(|_| Error::StringConvertionFailed)?; 107 | let handle = unsafe { RegisterEventSourceW(std::ptr::null_mut(), name.as_ptr()) }; 108 | 109 | if handle == 0 { 110 | Err(Error::Io(std::io::Error::last_os_error())) 111 | } else { 112 | Ok(WinLogger { 113 | handle, 114 | filter: make_filter(), 115 | }) 116 | } 117 | } 118 | } 119 | 120 | impl Drop for WinLogger { 121 | fn drop(&mut self) { 122 | unsafe { DeregisterEventSource(self.handle) }; 123 | } 124 | } 125 | 126 | impl log::Log for WinLogger { 127 | fn enabled(&self, metadata: &Metadata) -> bool { 128 | self.filter.enabled(metadata) 129 | } 130 | 131 | fn log(&self, record: &Record) { 132 | if self.filter.matches(record) { 133 | let level = record.level(); 134 | let (wtype, dweventid) = match level { 135 | Level::Error => (EVENTLOG_ERROR_TYPE, MSG_ERROR), 136 | Level::Warn => (EVENTLOG_WARNING_TYPE, MSG_WARNING), 137 | Level::Info => (EVENTLOG_INFORMATION_TYPE, MSG_INFO), 138 | Level::Debug => (EVENTLOG_INFORMATION_TYPE, MSG_DEBUG), 139 | Level::Trace => (EVENTLOG_INFORMATION_TYPE, MSG_TRACE), 140 | }; 141 | 142 | let msg = U16CString::from_str_truncate(format!("{}", record.args())); 143 | let msg_ptr = msg.as_ptr(); 144 | 145 | unsafe { 146 | ReportEventW( 147 | self.handle, 148 | wtype, // type 149 | 0, // category 150 | dweventid, // event id == resource msg id 151 | std::ptr::null_mut(), 152 | 1, 153 | 0, 154 | &msg_ptr, 155 | std::ptr::null_mut(), 156 | ) 157 | }; 158 | } 159 | } 160 | 161 | fn flush(&self) {} 162 | } 163 | -------------------------------------------------------------------------------- /CLA.md: -------------------------------------------------------------------------------- 1 | # Virtual Display Driver Individual Contributor License Agreement 2 | 3 | ### *Adapted from http://www.apache.org/licenses/ © Apache Software Foundation.* 4 | 5 | In order to clarify the intellectual property license granted with Contributions from any person or entity, Virtual Display Driver must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This CLA is for your protection as a Contributor as well as the protection of Virtual Display Driver and its users; it does not change your rights to use your own Contributions for any other purpose. 6 | 7 | Please read this document carefully before signing and keep a copy for your records. 8 | 9 | You accept and agree to the following terms and conditions for your present and future Contributions Submitted to Virtual Display Driver. In return, Virtual Display Driver shall not use your Contributions in a way that is contrary to the public benefit or inconsistent with its nonprofit status and bylaws in effect at the time of the Contribution. Except for the license granted herein to Virtual Display Driver and recipients of software distributed by Virtual Display Driver, You reserve all right, title, and interest in and to your Contributions. 10 | 11 | 1. Definitions. "You" (or "Contributor") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this CLA with Virtual Display Driver. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. 12 | 13 | For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 14 | 15 | "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally Submitted by You to Virtual Display Driver for inclusion in, or documentation of, any of the products owned or managed by Virtual Display Driver (the "Work"). For the purposes of this definition, "Submitted" means any form of electronic, verbal, or written communication sent to Virtual Display Driver or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Virtual Display Driver for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 16 | 17 | 2. Grant of Copyright License. Subject to the terms and conditions of this CLA, You hereby grant to Virtual Display Driver and to recipients of software distributed by Virtual Display Driver a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your Contributions and such derivative works. 18 | 19 | 3. Grant of Patent License. Subject to the terms and conditions of this CLA, You hereby grant to Virtual Display Driver and to recipients of software distributed by Virtual Display Driver a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by your Contribution(s) alone or by combination of your Contribution(s) with the Work to which such Contribution(s) was Submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this CLA for that Contribution or Work shall terminate as of the date such litigation is filed. 20 | 21 | 4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Virtual Display Driver, or that your employer has executed a separate Corporate CLA with Virtual Display Driver. 22 | 23 | 5. You represent that each of your Contributions is your original creation (see section 7 for submissions on behalf of others). You represent that your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of your Contributions. 24 | 25 | 6. You are not expected to provide support for your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 26 | 27 | 7. You commit not to copy code from another project which license does not allow the duplication / reuse / modification of their source code and / or license is not compatible with the project you are contributing to. As a reminder, a project without an explicit license must be considered as a project with a copyrighted license. 28 | 29 | 8. You agree to notify Virtual Display Driver of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. 30 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Virtual Display Driver Control.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net6.0-windows10.0.22621.0 5 | 10.0.17763.0 6 | Virtual_Display_Driver_Control 7 | x64 8 | win10-x86;win10-x64;win10-arm64 9 | win10-$(Platform).pubxml 10 | true 11 | true 12 | 10.0.19041.0 13 | False 14 | Virtual Display Driver Control 15 | Virtual Display Driver Control manages your virutal monitors for the Virtual Display driver 16 | https://github.com/MolotovCherry/virtual-display-rs 17 | icon.png 18 | https://github.com/MolotovCherry/virtual-display-rs 19 | windows,rust,privacy,monitor,display,vr,oculus,driver,remote,desktop,screen,windows-10,virtual-reality,private,virtual,dummy,virtual-desktop,obs,windows-11,virtual-monitor 20 | MSIX 21 | Assets\icon.ico 22 | app.manifest 23 | enable 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | all 59 | runtime; build; native; contentfiles; analyzers; buildtransitive 60 | 61 | 62 | 63 | 64 | 65 | 70 | 71 | 72 | 73 | 74 | 75 | PreserveNewest 76 | 77 | 78 | 79 | 80 | True 81 | \ 82 | 83 | 84 | MSBuild:Compile 85 | 86 | 87 | MSBuild:Compile 88 | 89 | 90 | 91 | 96 | 97 | true 98 | 99 | 100 | full 101 | $(DefineConstants);DEBUG 102 | 103 | 104 | full 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/src/swap_chain_processor.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::{ 3 | atomic::{AtomicBool, Ordering}, 4 | Arc, 5 | }, 6 | thread::{self, JoinHandle}, 7 | }; 8 | 9 | use log::{debug, error}; 10 | use wdf_umdf::{ 11 | IddCxSwapChainFinishedProcessingFrame, IddCxSwapChainReleaseAndAcquireBuffer, 12 | IddCxSwapChainSetDevice, WdfObjectDelete, 13 | }; 14 | use wdf_umdf_sys::{ 15 | HANDLE, IDARG_IN_SWAPCHAINSETDEVICE, IDARG_OUT_RELEASEANDACQUIREBUFFER, IDDCX_SWAPCHAIN, 16 | NTSTATUS, WAIT_TIMEOUT, WDFOBJECT, 17 | }; 18 | use windows::{ 19 | core::{w, Interface}, 20 | Win32::{ 21 | Foundation::HANDLE as WHANDLE, 22 | Graphics::Dxgi::IDXGIDevice, 23 | System::Threading::{ 24 | AvRevertMmThreadCharacteristics, AvSetMmThreadCharacteristicsW, WaitForSingleObject, 25 | }, 26 | }, 27 | }; 28 | 29 | use crate::{direct_3d_device::Direct3DDevice, helpers::Sendable}; 30 | 31 | pub struct SwapChainProcessor { 32 | terminate: Arc, 33 | thread: Option>, 34 | } 35 | 36 | unsafe impl Send for SwapChainProcessor {} 37 | unsafe impl Sync for SwapChainProcessor {} 38 | 39 | impl SwapChainProcessor { 40 | pub fn new() -> Self { 41 | Self { 42 | terminate: Arc::new(AtomicBool::new(false)), 43 | thread: None, 44 | } 45 | } 46 | 47 | pub fn run( 48 | &mut self, 49 | swap_chain: IDDCX_SWAPCHAIN, 50 | device: Direct3DDevice, 51 | available_buffer_event: HANDLE, 52 | ) { 53 | let available_buffer_event = unsafe { Sendable::new(available_buffer_event) }; 54 | let swap_chain = unsafe { Sendable::new(swap_chain) }; 55 | let terminate = self.terminate.clone(); 56 | 57 | let join_handle = thread::spawn(move || { 58 | // It is very important to prioritize this thread by making use of the Multimedia Scheduler Service. 59 | // It will intelligently prioritize the thread for improved throughput in high CPU-load scenarios. 60 | let mut av_task = 0u32; 61 | let res = unsafe { AvSetMmThreadCharacteristicsW(w!("Distribution"), &mut av_task) }; 62 | let Ok(av_handle) = res else { 63 | error!("Failed to prioritize thread: {res:?}"); 64 | return; 65 | }; 66 | 67 | Self::run_core(*swap_chain, &device, *available_buffer_event, &terminate); 68 | 69 | let res = unsafe { WdfObjectDelete(*swap_chain as WDFOBJECT) }; 70 | if let Err(e) = res { 71 | error!("Failed to delete wdf object: {e:?}"); 72 | return; 73 | } 74 | 75 | // Revert the thread to normal once it's done 76 | let res = unsafe { AvRevertMmThreadCharacteristics(av_handle) }; 77 | if let Err(e) = res { 78 | error!("Failed to revert prioritize thread: {e:?}"); 79 | } 80 | }); 81 | 82 | self.thread = Some(join_handle); 83 | } 84 | 85 | fn run_core( 86 | swap_chain: IDDCX_SWAPCHAIN, 87 | device: &Direct3DDevice, 88 | available_buffer_event: HANDLE, 89 | terminate: &AtomicBool, 90 | ) { 91 | let dxgi_device = device.device.cast::(); 92 | let Ok(dxgi_device) = dxgi_device else { 93 | error!("Failed to cast ID3D11Device to IDXGIDevice: {dxgi_device:?}"); 94 | return; 95 | }; 96 | 97 | let set_device = IDARG_IN_SWAPCHAINSETDEVICE { 98 | pDevice: dxgi_device.into_raw().cast(), 99 | }; 100 | 101 | let res = unsafe { IddCxSwapChainSetDevice(swap_chain, &set_device) }; 102 | if res.is_err() { 103 | debug!("Failed to set swapchain device: {res:?}"); 104 | return; 105 | } 106 | 107 | loop { 108 | let mut buffer = IDARG_OUT_RELEASEANDACQUIREBUFFER::default(); 109 | let hr: NTSTATUS = 110 | unsafe { IddCxSwapChainReleaseAndAcquireBuffer(swap_chain, &mut buffer).into() }; 111 | 112 | #[allow(clippy::items_after_statements)] 113 | const E_PENDING: u32 = 0x8000_000A; 114 | if u32::from(hr) == E_PENDING { 115 | let wait_result = 116 | unsafe { WaitForSingleObject(WHANDLE(available_buffer_event.cast()), 16).0 }; 117 | 118 | // thread requested an end 119 | let should_terminate = terminate.load(Ordering::Relaxed); 120 | if should_terminate { 121 | break; 122 | } 123 | 124 | // WAIT_OBJECT_0 | WAIT_TIMEOUT 125 | if matches!(wait_result, 0 | WAIT_TIMEOUT) { 126 | // We have a new buffer, so try the AcquireBuffer again 127 | continue; 128 | } 129 | 130 | // The wait was cancelled or something unexpected happened 131 | break; 132 | } else if hr.is_success() { 133 | // This is the most performance-critical section of code in an IddCx driver. It's important that whatever 134 | // is done with the acquired surface be finished as quickly as possible. 135 | let hr = unsafe { IddCxSwapChainFinishedProcessingFrame(swap_chain) }; 136 | 137 | if hr.is_err() { 138 | break; 139 | } 140 | } else { 141 | // The swap-chain was likely abandoned (e.g. DXGI_ERROR_ACCESS_LOST), so exit the processing loop 142 | break; 143 | } 144 | } 145 | } 146 | } 147 | 148 | impl Drop for SwapChainProcessor { 149 | fn drop(&mut self) { 150 | if let Some(handle) = self.thread.take() { 151 | // send signal to end thread 152 | self.terminate.store(true, Ordering::Relaxed); 153 | 154 | // wait until thread is finished 155 | _ = handle.join(); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release: 7 | description: 'Release' 8 | required: true 9 | default: false 10 | type: boolean 11 | push: 12 | branches: [ "master" ] 13 | pull_request: 14 | branches: [ "master" ] 15 | release: 16 | types: [created] 17 | 18 | env: 19 | CARGO_TERM_COLOR: always 20 | 21 | jobs: 22 | build: 23 | permissions: write-all 24 | runs-on: windows-2022 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Rust cache 31 | uses: Swatinem/rust-cache@v2 32 | with: 33 | workspaces: "rust -> target" 34 | key: ${{ (github.event_name == 'release' || inputs.release) && 'prod' || 'dev' }} 35 | 36 | - name: Install cargo-make 37 | uses: baptiste0928/cargo-install@v3 38 | with: 39 | crate: cargo-make 40 | 41 | - name: Install cargo-target-dir 42 | uses: baptiste0928/cargo-install@v3 43 | with: 44 | crate: cargo-target-dir 45 | git: https://github.com/MolotovCherry/cargo-target-dir 46 | 47 | - name: Install cargo-wix 48 | if: github.event_name == 'release' 49 | uses: baptiste0928/cargo-install@v3 50 | with: 51 | crate: cargo-wix 52 | 53 | - name: Export Private Certificate 54 | if: github.event_name != 'pull_request' 55 | env: 56 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 57 | run: | 58 | $env:PRIVATE_KEY | Out-File -FilePath private.txt 59 | certutil -decode private.txt private.pfx 60 | 61 | - name: Export PR Private Certificate 62 | if: github.event_name == 'pull_request' 63 | run: cp .github/workflows/build_res/pr.pfx private.pfx 64 | 65 | - name: Export Public Certificate 66 | env: 67 | PRIVATE_KEY_PASSWORD: ${{ github.event_name == 'pull_request' && '1234' || secrets.PRIVATE_KEY_PASSWORD }} 68 | run: | 69 | Get-PfxCertificate -FilePath private.pfx -Password (ConvertTo-SecureString -String "${{ env.PRIVATE_KEY_PASSWORD }}" -AsPlainText -Force) | Export-Certificate -FilePath DriverCertificate.cer -type CERT 70 | 71 | - name: Setup python version 72 | uses: actions/setup-python@v5 73 | with: 74 | python-version: '3.12' 75 | 76 | - name: Build 77 | env: 78 | PRIVATE_KEY_PASSWORD: ${{ github.event_name == 'pull_request' && '1234' || secrets.PRIVATE_KEY_PASSWORD }} 79 | RELEASE: ${{ (github.event_name == 'release' || inputs.release) && 'prod' || 'dev' }} 80 | working-directory: rust 81 | run: cargo make -p $env:RELEASE build 82 | 83 | - name: Prepare Artifact Upload 84 | if: github.event_name != 'release' 85 | run: | 86 | # copy any required files to . so artifact upload files are in top level 87 | Get-ChildItem -Path ` 88 | "rust/target/output/*.exe", ` 89 | "rust/target/output/*.cat", ` 90 | "rust/target/output/*.dll", ` 91 | "rust/target/output/*.inf", ` 92 | "rust/target/output/*.pyd", ` 93 | "installer/install-cert.bat", ` 94 | "installer/files/*.reg" ` 95 | | ForEach-Object { Copy-Item -Path $_.FullName -Destination "." } 96 | 97 | - name: Upload Artifacts 98 | if: github.event_name != 'release' 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: driver 102 | path: | 103 | *.exe 104 | *.cat 105 | *.dll 106 | *.inf 107 | *.pyd 108 | DriverCertificate.cer 109 | install-cert.bat 110 | 111 | - name: Set release version 112 | if: github.event_name == 'release' 113 | run: | 114 | $tagName = "${{ github.event.release.tag_name }}" 115 | $version = $tagName.TrimStart('v') 116 | echo "RELEASE_VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append 117 | 118 | # package with wix 119 | - name: Create installer 120 | if: github.event_name == 'release' 121 | working-directory: rust 122 | run: | 123 | cargo wix -p virtual-display-driver -i ${{ env.RELEASE_VERSION }} --nocapture -I ../installer/main.wxs -o target\output -C -ext -C WixDifxAppExtension -L -ext -L WixDifxAppExtension -L "C:\Program Files (x86)\WiX Toolset v3.11\bin\difxapp_x64.wixlib" 124 | 125 | - name: Sign installer 126 | if: github.event_name == 'release' 127 | shell: cmd 128 | env: 129 | PRIVATE_KEY_PASSWORD: ${{ github.event_name == 'pull_request' && '1234' || secrets.PRIVATE_KEY_PASSWORD }} 130 | run: | 131 | call "%ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" 132 | signtool sign /a /fd SHA256 /v /f private.pfx /p ${{ env.PRIVATE_KEY_PASSWORD }} /t http://timestamp.digicert.com rust/target/output/*.msi 133 | 134 | - name: Zip up install package 135 | if: github.event_name == 'release' 136 | run: | 137 | Get-ChildItem -Path DriverCertificate.cer, install-cert.bat, rust/target/output/*.msi | Compress-Archive -CompressionLevel Optimal -DestinationPath "virtual-desktop-driver-installer-x64.zip" 138 | 139 | - name: Zip up portable package 140 | if: github.event_name == 'release' 141 | run: | 142 | Get-ChildItem -Path DriverCertificate.cer, install-cert.bat, *.inf, *.dll, *.cat, *.exe, *.pyd, *.reg | Compress-Archive -CompressionLevel Optimal -DestinationPath "virtual-desktop-driver-portable-x64.zip" 143 | 144 | - name: Attach assets to release 145 | if: github.event_name == 'release' 146 | uses: xresloader/upload-to-github-release@v1 147 | env: 148 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 149 | with: 150 | file: "*.zip" 151 | draft: false 152 | release_id: ${{ github.event.release.id }} 153 | -------------------------------------------------------------------------------- /rust/vdd-user-session-service/src/service.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsString, io::ErrorKind, sync::mpsc, time::Duration}; 2 | 3 | use driver_ipc::{ 4 | sync::{Client, DriverClient}, 5 | Monitor, 6 | }; 7 | use windows::Win32::{ 8 | Foundation::{CloseHandle, HANDLE}, 9 | Security::{ImpersonateLoggedOnUser, SE_TCB_NAME}, 10 | System::RemoteDesktop::{WTSGetActiveConsoleSessionId, WTSQueryUserToken}, 11 | }; 12 | use windows_service::{ 13 | define_windows_service, 14 | service::{ 15 | ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, 16 | SessionChangeReason, 17 | }, 18 | service_control_handler::{self, ServiceControlHandlerResult}, 19 | service_dispatcher, 20 | }; 21 | use winreg::{ 22 | enums::{HKEY_CURRENT_USER, KEY_READ}, 23 | RegKey, 24 | }; 25 | 26 | use crate::{set_privileges::set_privilege, SERVICE_NAME, SERVICE_TYPE}; 27 | 28 | define_windows_service!(ffi_service_main, service_main); 29 | 30 | pub fn start_service() -> windows_service::Result<()> { 31 | service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; 32 | Ok(()) 33 | } 34 | 35 | #[allow(clippy::needless_pass_by_value)] 36 | fn service_main(arguments: Vec) { 37 | if let Err(_e) = run_service(&arguments) { 38 | // error handling 39 | } 40 | } 41 | 42 | #[allow(clippy::too_many_lines)] 43 | fn run_service(_arguments: &[OsString]) -> windows_service::Result<()> { 44 | // escalate privileges so we can get the logged on user token 45 | if !set_privilege(SE_TCB_NAME, true) { 46 | let io = std::io::Error::new(ErrorKind::Other, "Failed to grant SE_TCB_NAME"); 47 | return Err(windows_service::Error::Winapi(io)); 48 | } 49 | 50 | let (shutdown_tx, shutdown_rx) = mpsc::channel(); 51 | 52 | let mut latest_session = 0; 53 | 54 | let event_handler = move |control_event| -> ServiceControlHandlerResult { 55 | match control_event { 56 | ServiceControl::Stop => { 57 | shutdown_tx.send(()).unwrap(); 58 | ServiceControlHandlerResult::NoError 59 | } 60 | 61 | ServiceControl::SessionChange(param) => { 62 | match param.reason { 63 | SessionChangeReason::SessionLogon 64 | | SessionChangeReason::RemoteConnect 65 | | SessionChangeReason::SessionUnlock => { 66 | // skip if this was already ran for a particular session 67 | if latest_session == param.notification.session_id { 68 | return ServiceControlHandlerResult::NoError; 69 | } 70 | 71 | latest_session = param.notification.session_id; 72 | 73 | if let Err(e) = notify(latest_session) { 74 | return e; 75 | } 76 | } 77 | 78 | SessionChangeReason::SessionLogoff => { 79 | let Ok(client) = Client::connect() else { 80 | return ServiceControlHandlerResult::Other(0x3); 81 | }; 82 | 83 | _ = client.remove_all(); 84 | } 85 | 86 | _ => (), 87 | } 88 | 89 | ServiceControlHandlerResult::NoError 90 | } 91 | 92 | ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, 93 | 94 | _ => ServiceControlHandlerResult::NotImplemented, 95 | } 96 | }; 97 | 98 | let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; 99 | 100 | // service running 101 | status_handle.set_service_status(ServiceStatus { 102 | service_type: SERVICE_TYPE, 103 | current_state: ServiceState::Running, 104 | controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SESSION_CHANGE, 105 | exit_code: ServiceExitCode::Win32(0), 106 | checkpoint: 0, 107 | wait_hint: Duration::default(), 108 | process_id: None, 109 | })?; 110 | 111 | if let Ok(session) = get_current_session() { 112 | latest_session = session; 113 | _ = notify(latest_session); 114 | } 115 | 116 | // blocking wait for shutdown signal 117 | _ = shutdown_rx.recv(); 118 | 119 | // service stopped 120 | status_handle.set_service_status(ServiceStatus { 121 | service_type: SERVICE_TYPE, 122 | current_state: ServiceState::Stopped, 123 | controls_accepted: ServiceControlAccept::empty(), 124 | exit_code: ServiceExitCode::Win32(0), 125 | checkpoint: 0, 126 | wait_hint: Duration::default(), 127 | process_id: None, 128 | })?; 129 | 130 | Ok(()) 131 | } 132 | 133 | fn get_current_session() -> Result { 134 | let session = unsafe { WTSGetActiveConsoleSessionId() }; 135 | 136 | if session == 0xFFFF_FFFF { 137 | Err(()) 138 | } else { 139 | Ok(session) 140 | } 141 | } 142 | 143 | fn notify(session_id: u32) -> Result<(), ServiceControlHandlerResult> { 144 | impersonate_user(session_id, || { 145 | let hklm = RegKey::predef(HKEY_CURRENT_USER); 146 | let key = r"SOFTWARE\VirtualDisplayDriver"; 147 | 148 | let Ok(driver_settings) = hklm.open_subkey_with_flags(key, KEY_READ) else { 149 | return Err(ServiceControlHandlerResult::NoError); 150 | }; 151 | 152 | let monitors = driver_settings 153 | .get_value::("data") 154 | .map(|data| serde_json::from_str::>(&data).unwrap_or_default()) 155 | .unwrap_or_default(); 156 | 157 | let Ok(mut client) = DriverClient::new() else { 158 | return Err(ServiceControlHandlerResult::NoError); 159 | }; 160 | 161 | if client.set_monitors(&monitors).is_err() { 162 | return Err(ServiceControlHandlerResult::NoError); 163 | } 164 | 165 | _ = client.notify(); 166 | 167 | Ok(()) 168 | }) 169 | } 170 | 171 | fn impersonate_user( 172 | session_id: u32, 173 | cb: impl FnOnce() -> Result<(), ServiceControlHandlerResult>, 174 | ) -> Result<(), ServiceControlHandlerResult> { 175 | let mut token = HANDLE::default(); 176 | if unsafe { WTSQueryUserToken(session_id, &mut token).is_err() } { 177 | return Err(ServiceControlHandlerResult::NoError); 178 | } 179 | 180 | // impersonate user for current user reg call 181 | if unsafe { ImpersonateLoggedOnUser(token).is_err() } { 182 | return Err(ServiceControlHandlerResult::NoError); 183 | } 184 | 185 | cb()?; 186 | 187 | _ = unsafe { CloseHandle(token) }; 188 | 189 | Ok(()) 190 | } 191 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As with many Open Source projects, and the greater FLOSS community, the Virtual Display Driver community is comprised of a diverse group of contributors and consumers from around the globe. We find the contributions, collaborations, and mentorships within our community to be the essential lifeblood of our project and appreciate the efforts of those who participate to nurture and grow those, and all other aspects of our community. 4 | 5 | However, when a large and sufficiently diverse group of people work together, there are often cultural, communication, and compatibility issues. In order to minimize conflict, and provide a framework for resolution, we have a brief code of conduct that we ask all participants in the Virtual Display Driver community adhere to. These rules should apply to everyone, regardless of station within the community, and anyone can serve to remind, or ask the project members to help resolve issues. 6 | 7 | No list is ever exhaustive, so we encourage members of the Virtual Display Driver community to adhere to the spirit, rather than the letter, of this code, as that is how it will be enforced. Places where this code may be particularly applicable are GitHub, IRC, mailing lists, conferences, meetups, online events, discord servers, and other direct interactions within the community. Any violations, especially continued or flagrant offenses, may affect an individual’s (or organization’s) ability to participate within the Virtual Display Driver community. 8 | 9 | If you feel that someone is in violation of the code of conduct, whether in letter or in spirit, we request that you contact us with as detailed a description as possible of the offense and offending party/parties, or any other inquiries please feel free to contact us. Contact details can be found at the end of this document. 10 | 11 | ## Rules 12 | 1. **Be friendly and patient.** We were all new or suffered from a lack of knowledge at one point in time. Please try to remember what it felt like to be on that end, and treat people accordingly. 13 | 2. **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 14 | 3. **Be helpful.** By helping others to learn our entire ecosystem is enriched. We encourage members of the Virtual Display Driver community to mentor each other and help to raise the general level of knowledge in the community whenever possible. 15 | 4. **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language. 16 | 5. **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Virtual Display Driver community should be respectful when dealing with other members as well as with people outside the Virtual Display Driver community. 17 | 6. **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren’t acceptable. This includes, but is not limited to: 18 |
    19 |
  1. Violent threats or language directed against another person.
  2. 20 |
  3. Discriminatory jokes and language.
  4. 21 |
  5. Posting sexually explicit or violent material.
  6. 22 |
  7. Posting (or threatening to post) other people’s personally identifying information (“doxing”).
  8. 23 |
  9. Personal insults, especially those using racist or sexist terms.
  10. 24 |
  11. Unwelcome sexual attention.
  12. 25 |
  13. Advocating for, or encouraging, any of the above behavior.
  14. 26 |
  15. Repeated harassment of others. In general, if someone asks you to stop, then stop.
  16. 27 |
28 | 8. **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Virtual Display Driver is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of Virtual Display Driver comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. 29 | 30 | ## Consequences 31 | 1. Except in flagrant or otherwise egregious cases, the first infraction will result in a verbal warning. Everyone slips up or acts out of frustration at times, we just ask that you work to not repeat the behavior. 32 | 2. A second infraction (or more flagrant first offense, as determined by the Virtual Display Driver project members) will have an enforced temporary ban of 2 days from the communication platform. 33 | 3. A third infraction (or especially egregious first offense, as determined by the Virtual Display Driver project members) will result in temporary suspension from all avenues of Virtual Display Driver community participation for 4 weeks. This will include, but is not limited to, GitHub, IRC, mailing lists, conferences, meetups, online events, discord servers, and other direct interactions within the community. 34 | 4. Continued infractions will be dealt with on a case-by-case basis, but could result in permanent suspension from the Virtual Display Driver community. 35 | 36 | _This text is adapted from the [Ceph Code of Conduct](https://ceph.io/community/code-of-conduct/), which itself credits [Django Project](https://www.djangoproject.com/conduct/) for the original inspiration of this document and the [Ada Initiative](https://adainitiative.org/) for expanding the fight for equality and civility within FLOSS communities and beyond._ 37 | 38 | # Contact 39 | - [Discord server](https://discord.gg/pDDt78wYQy) 40 | - [Discussions](https://github.com/MolotovCherry/virtual-display-rs/discussions) 41 | - Project leadership: /molotov/cherry/ (Discord) 42 | - Remove all forward slashes in names. They are there to prevent bots from harvesting names. 43 | -------------------------------------------------------------------------------- /rust/virtual-display-driver-cli/src/mode.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeSet, HashMap}; 2 | 3 | use eyre::Context as _; 4 | use joinery::JoinableIterator as _; 5 | 6 | const DEFAULT_REFRESH_RATE: driver_ipc::RefreshRate = 60; 7 | 8 | /// Represent a mode as specified by the user as a CLI argument. Can be parsed 9 | /// from a string such as `1920x1080` or `3840x2160@60/120`, or converted 10 | /// from/to the type [`driver_ipc::Mode`]. 11 | /// 12 | /// This type is very similar to [`driver_ipc::Mode`], but with a few key 13 | /// differences: 14 | /// 15 | /// - The list of refresh rates can be empty by design. When converting to 16 | /// [`driver_ipc::Mode`], the refresh rate is set to [`DEFAULT_REFRESH_RATE`] 17 | /// if one isn't specified. See [`remove`] for a use-case where the empty list 18 | /// of refresh rates is used. 19 | /// - It implements [`std::str::FromStr`], with a convenient user-facing error 20 | /// message using `eyre`. 21 | /// - It implements [`std::fmt::Display`] with a nice output format. 22 | /// - The list of resolutions is represented with a set, meaning that duplicate 23 | /// resolutions will be ignored. 24 | #[derive(Debug, Clone)] 25 | pub struct Mode { 26 | pub width: driver_ipc::Dimen, 27 | pub height: driver_ipc::Dimen, 28 | pub refresh_rates: BTreeSet, 29 | } 30 | 31 | impl Mode { 32 | /// Add the default refresh rate if the list of refresh rates is empty. 33 | fn ensure_refresh_rate(&mut self) { 34 | if self.refresh_rates.is_empty() { 35 | self.refresh_rates.insert(DEFAULT_REFRESH_RATE); 36 | } 37 | } 38 | } 39 | 40 | impl From for Mode { 41 | fn from(value: driver_ipc::Mode) -> Self { 42 | Self { 43 | width: value.width, 44 | height: value.height, 45 | refresh_rates: value.refresh_rates.into_iter().collect(), 46 | } 47 | } 48 | } 49 | 50 | impl From for driver_ipc::Mode { 51 | fn from(mut value: Mode) -> Self { 52 | value.ensure_refresh_rate(); 53 | 54 | Self { 55 | width: value.width, 56 | height: value.height, 57 | refresh_rates: value.refresh_rates.into_iter().collect(), 58 | } 59 | } 60 | } 61 | 62 | impl std::fmt::Display for Mode { 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | if self.refresh_rates.is_empty() { 65 | write!(f, "{}x{}", self.width, self.height)?; 66 | } else { 67 | write!( 68 | f, 69 | "{}x{}@{}", 70 | self.width, 71 | self.height, 72 | self.refresh_rates.iter().join_with("/"), 73 | )?; 74 | } 75 | 76 | Ok(()) 77 | } 78 | } 79 | 80 | impl std::str::FromStr for Mode { 81 | type Err = eyre::Error; 82 | 83 | fn from_str(s: &str) -> Result { 84 | let (resolution, refresh_rate_list) = match s.split_once('@') { 85 | Some((resolution, refresh_rate_list)) => (resolution, Some(refresh_rate_list)), 86 | None => (s, None), 87 | }; 88 | 89 | let (width, height) = resolution.split_once('x').ok_or_else(|| { 90 | eyre::eyre!("invalid resolution in {s:?}, expected a string like \"1920x1080\"",) 91 | })?; 92 | let width = width 93 | .parse() 94 | .with_context(|| format!("invalid width in {s:?}, expected a number"))?; 95 | let height = height 96 | .parse() 97 | .with_context(|| format!("invalid height in {s:?}, expected a number"))?; 98 | 99 | let refresh_rates = match refresh_rate_list { 100 | Some(refresh_rate_list) => refresh_rate_list 101 | .split('/') 102 | .map(|s| { 103 | s.parse().with_context(|| { 104 | format!("failed to parse refresh rate in {s:?}, expected a number") 105 | }) 106 | }) 107 | .collect::>()?, 108 | None => BTreeSet::new(), 109 | }; 110 | 111 | Ok(Self { 112 | width, 113 | height, 114 | refresh_rates, 115 | }) 116 | } 117 | } 118 | 119 | /// Merge together a list of modes. Multiple modes with the same resolution 120 | /// will be merged into one, and the sets of refresh rates will be combined. 121 | pub fn merge(modes: impl IntoIterator) -> Vec { 122 | let mut resolutions = 123 | HashMap::<(driver_ipc::Dimen, driver_ipc::Dimen), BTreeSet>::new(); 124 | 125 | for mode in modes { 126 | let refresh_rates = resolutions.entry((mode.width, mode.height)).or_default(); 127 | refresh_rates.extend(&mode.refresh_rates); 128 | } 129 | 130 | resolutions 131 | .into_iter() 132 | .map(|((width, height), refresh_rates)| Mode { 133 | width, 134 | height, 135 | refresh_rates, 136 | }) 137 | .collect() 138 | } 139 | 140 | /// Remove a mode from a list of modes. If `remove_mode` includes a refresh 141 | /// rate, then only that refresh rate will be removed from the mode; otherwise, 142 | /// the entire mode will be removed. Returns an error if no mode matches 143 | /// `remove_mode`. 144 | /// 145 | /// This function will also implicitly merge together the list of provided 146 | /// modes, see [`merge`] for more details. 147 | pub fn remove( 148 | modes: impl IntoIterator, 149 | remove_mode: &Mode, 150 | ) -> eyre::Result> { 151 | let mut resolutions = 152 | HashMap::<(driver_ipc::Dimen, driver_ipc::Dimen), BTreeSet>::new(); 153 | 154 | for mut mode in modes { 155 | mode.ensure_refresh_rate(); 156 | 157 | let refresh_rates = resolutions.entry((mode.width, mode.height)).or_default(); 158 | refresh_rates.extend(&mode.refresh_rates); 159 | } 160 | 161 | if remove_mode.refresh_rates.is_empty() { 162 | let removed = resolutions.remove(&(remove_mode.width, remove_mode.height)); 163 | if removed.is_none() { 164 | eyre::bail!("mode {remove_mode} not found"); 165 | } 166 | } else { 167 | let Some(refresh_rates) = resolutions.get_mut(&(remove_mode.width, remove_mode.height)) 168 | else { 169 | eyre::bail!("mode {remove_mode} not found"); 170 | }; 171 | for refresh_rate in &remove_mode.refresh_rates { 172 | let removed = refresh_rates.remove(refresh_rate); 173 | if !removed { 174 | eyre::bail!("mode {remove_mode} not found"); 175 | } 176 | } 177 | } 178 | 179 | let modes = resolutions 180 | .into_iter() 181 | .filter(|(_, refresh_rates)| !refresh_rates.is_empty()) 182 | .map(|((width, height), refresh_rates)| Mode { 183 | width, 184 | height, 185 | refresh_rates, 186 | }) 187 | .collect(); 188 | Ok(modes) 189 | } 190 | -------------------------------------------------------------------------------- /rust/virtual-display-driver/src/entry.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use driver_logger::DriverLogger; 4 | use log::{error, info, Level}; 5 | use wdf_umdf::{ 6 | IddCxDeviceInitConfig, IddCxDeviceInitialize, WdfDeviceCreate, 7 | WdfDeviceInitSetPnpPowerEventCallbacks, WdfDeviceSetFailed, WdfDriverCreate, 8 | }; 9 | use wdf_umdf_sys::{ 10 | IDD_CX_CLIENT_CONFIG, NTSTATUS, WDFDEVICE_INIT, WDFDRIVER__, WDFOBJECT, 11 | WDF_DEVICE_FAILED_ACTION, WDF_DRIVER_CONFIG, WDF_OBJECT_ATTRIBUTES, 12 | WDF_PNPPOWER_EVENT_CALLBACKS, _DRIVER_OBJECT, _UNICODE_STRING, 13 | }; 14 | 15 | use crate::callbacks::{ 16 | adapter_commit_modes, adapter_init_finished, assign_swap_chain, device_d0_entry, 17 | monitor_get_default_modes, monitor_query_modes, parse_monitor_description, unassign_swap_chain, 18 | }; 19 | use crate::{context::DeviceContext, helpers::Sendable}; 20 | 21 | // 22 | // Our driver's entry point 23 | // See windows::Wdk::System::SystemServices::DRIVER_INITIALIZE 24 | // 25 | #[no_mangle] 26 | extern "C-unwind" fn DriverEntry( 27 | driver_object: *mut _DRIVER_OBJECT, 28 | registry_path: *mut _UNICODE_STRING, 29 | ) -> NTSTATUS { 30 | // During system bootup, `RegisterEventSourceW` fails and causes the driver to not bootup 31 | // Pretty unfortunate, therefore, we will run this on a thread until it succeeds and let the rest of 32 | // the driver start. I know this is suboptimal considering it's our main code to catch panics. 33 | // 34 | // It always starts immediately when the computer is already booted up. 35 | // If you have a better solution, please by all means open an issue report 36 | let init_log = || { 37 | let mut logger = DriverLogger::new(if cfg!(debug_assertions) { 38 | Level::Debug 39 | } else { 40 | Level::Info 41 | }); 42 | 43 | if cfg!(debug_assertions) { 44 | logger.debug(); 45 | } else if logger.name("VirtualDisplayDriver").is_err() { 46 | return NTSTATUS::STATUS_UNSUCCESSFUL; 47 | } 48 | 49 | let status = logger 50 | .init() 51 | .map_err(|_| NTSTATUS::STATUS_FAILED_DRIVER_ENTRY) 52 | .into(); 53 | 54 | if status == NTSTATUS::STATUS_SUCCESS { 55 | info!( 56 | "Initialized Virtual Display Driver v{} @ {}", 57 | env!("CARGO_PKG_VERSION"), 58 | env!("VERGEN_GIT_SHA") 59 | ); 60 | } 61 | 62 | status 63 | }; 64 | 65 | let status = init_log(); 66 | 67 | if !status.is_success() { 68 | // Okay, let's try another method then 69 | let device = unsafe { Sendable::new(driver_object) }; 70 | 71 | std::thread::spawn(move || { 72 | #[allow(clippy::redundant_locals)] 73 | let device = device; 74 | let time_waited = Instant::now(); 75 | // 5 minutes 76 | let timeout_duration = Duration::from_secs(60 * 5); 77 | // in ms 78 | let sleep_for = 500; 79 | 80 | loop { 81 | let status = init_log(); 82 | std::thread::sleep(Duration::from_millis(sleep_for)); 83 | 84 | // if it succeeds, great. if it didn't conclude after 5 minutes 85 | // Surely a users system is booted up before then? 86 | let timedout = time_waited.elapsed() >= timeout_duration; 87 | if status.is_success() || timedout { 88 | if timedout { 89 | // Service took too long to start. Unfortunately, there is no way to log this failure 90 | unsafe { 91 | _ = WdfDeviceSetFailed( 92 | device.cast(), 93 | WDF_DEVICE_FAILED_ACTION::WdfDeviceFailedNoRestart, 94 | ); 95 | } 96 | } else { 97 | info!( 98 | "Service took {} seconds to start", 99 | time_waited.elapsed().as_secs() 100 | ); 101 | } 102 | 103 | break; 104 | } 105 | } 106 | }); 107 | } 108 | 109 | // set the panic hook to capture and log panics 110 | crate::panic::set_hook(); 111 | 112 | let mut attributes = WDF_OBJECT_ATTRIBUTES::init(); 113 | 114 | let mut config = WDF_DRIVER_CONFIG::init(Some(driver_add)); 115 | 116 | unsafe { 117 | WdfDriverCreate( 118 | driver_object, 119 | registry_path, 120 | Some(&mut attributes), 121 | &mut config, 122 | None, 123 | ) 124 | } 125 | .into() 126 | } 127 | 128 | extern "C-unwind" fn driver_add( 129 | _driver: *mut WDFDRIVER__, 130 | mut init: *mut WDFDEVICE_INIT, 131 | ) -> NTSTATUS { 132 | let mut callbacks = WDF_PNPPOWER_EVENT_CALLBACKS::init(); 133 | 134 | callbacks.EvtDeviceD0Entry = Some(device_d0_entry); 135 | 136 | unsafe { 137 | _ = WdfDeviceInitSetPnpPowerEventCallbacks(init, &mut callbacks); 138 | } 139 | 140 | let Some(mut config) = IDD_CX_CLIENT_CONFIG::init() else { 141 | error!("Failed to create IDD_CX_CLIENT_CONFIG"); 142 | return NTSTATUS::STATUS_NOT_FOUND; 143 | }; 144 | 145 | config.EvtIddCxAdapterInitFinished = Some(adapter_init_finished); 146 | 147 | config.EvtIddCxParseMonitorDescription = Some(parse_monitor_description); 148 | config.EvtIddCxMonitorGetDefaultDescriptionModes = Some(monitor_get_default_modes); 149 | config.EvtIddCxMonitorQueryTargetModes = Some(monitor_query_modes); 150 | config.EvtIddCxAdapterCommitModes = Some(adapter_commit_modes); 151 | config.EvtIddCxMonitorAssignSwapChain = Some(assign_swap_chain); 152 | config.EvtIddCxMonitorUnassignSwapChain = Some(unassign_swap_chain); 153 | 154 | let init_data = unsafe { &mut *init }; 155 | let status = unsafe { IddCxDeviceInitConfig(init_data, &config) }; 156 | if let Err(e) = status { 157 | error!("Failed to init iddcx config: {e:?}"); 158 | return e.into(); 159 | } 160 | 161 | let mut attributes = 162 | WDF_OBJECT_ATTRIBUTES::init_context_type(unsafe { DeviceContext::get_type_info() }); 163 | 164 | attributes.EvtCleanupCallback = Some(event_cleanup); 165 | 166 | let mut device = std::ptr::null_mut(); 167 | 168 | let status = unsafe { WdfDeviceCreate(&mut init, Some(&mut attributes), &mut device) }; 169 | if let Err(e) = status { 170 | error!("Failed to create device: {e:?}"); 171 | return e.into(); 172 | } 173 | 174 | let status = unsafe { IddCxDeviceInitialize(device) }; 175 | if let Err(e) = status { 176 | error!("Failed to init iddcx device: {e:?}"); 177 | return e.into(); 178 | } 179 | 180 | let context = DeviceContext::new(device); 181 | 182 | unsafe { context.init(device as WDFOBJECT).into() } 183 | } 184 | 185 | unsafe extern "C-unwind" fn event_cleanup(wdf_object: WDFOBJECT) { 186 | _ = unsafe { DeviceContext::drop(wdf_object) }; 187 | } 188 | -------------------------------------------------------------------------------- /Virtual Display Driver Control/Views/SettingsView.xaml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 3 16 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 76 | 79 | 80 | 81 | 82 | 83 | 87 | 88 | 89 | 98 | 99 | 104 | 105 | 106 | 114 | 115 | 123 | 124 | 132 | 133 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /rust/wdf-umdf-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case, non_camel_case_types, non_upper_case_globals, unused)] 2 | 3 | mod bindings; 4 | mod ntstatus; 5 | 6 | use std::fmt::{self, Display}; 7 | 8 | pub use bindings::*; 9 | pub use ntstatus::*; 10 | pub use paste::paste; 11 | 12 | #[macro_export] 13 | macro_rules! WdfIsFunctionAvailable { 14 | ($name:ident) => {{ 15 | // SAFETY: We only ever do read access 16 | let higher = unsafe { $crate::WdfClientVersionHigherThanFramework } != 0; 17 | // SAFETY: We only ever do read access 18 | let fn_count = unsafe { $crate::WdfFunctionCount }; 19 | 20 | // https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h#L126 21 | $crate::paste! { 22 | // index is always positive, see 23 | // https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h 24 | const FN_INDEX: u32 = $crate::WDFFUNCENUM::[<$name TableIndex>].0 as u32; 25 | 26 | FN_INDEX < $crate::WDF_ALWAYS_AVAILABLE_FUNCTION_COUNT 27 | || !higher || FN_INDEX < fn_count 28 | } 29 | }}; 30 | } 31 | 32 | #[macro_export] 33 | macro_rules! WdfIsStructureAvailable { 34 | ($name:ident) => {{ 35 | // SAFETY: We only ever do read access 36 | let higher = unsafe { $crate::WdfClientVersionHigherThanFramework } != 0; 37 | // SAFETY: We only ever do read access 38 | let struct_count = unsafe { $crate::WdfStructureCount }; 39 | 40 | // https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h#L141 41 | $crate::paste! { 42 | // index is always positive, see 43 | // https://github.com/microsoft/Windows-Driver-Frameworks/blob/main/src/publicinc/wdf/umdf/2.33/wdffuncenum.h 44 | const STRUCT_INDEX: u32 = $crate::WDFSTRUCTENUM::[].0 as u32; 45 | 46 | !higher || STRUCT_INDEX < struct_count 47 | } 48 | }}; 49 | } 50 | 51 | #[macro_export] 52 | macro_rules! IddCxIsFunctionAvailable { 53 | ($name:ident) => {{ 54 | // SAFETY: We only ever do read access 55 | let higher = unsafe { $crate::IddClientVersionHigherThanFramework } != 0; 56 | // SAFETY: We only ever do read access 57 | let fn_count = unsafe { $crate::IddFunctionCount }; 58 | 59 | $crate::paste! { 60 | const FN_INDEX: u32 = $crate::IDDFUNCENUM::[<$name TableIndex>].0 as u32; 61 | 62 | FN_INDEX < $crate::IDD_ALWAYS_AVAILABLE_FUNCTION_COUNT 63 | || !higher || FN_INDEX < fn_count 64 | } 65 | }}; 66 | } 67 | 68 | #[macro_export] 69 | macro_rules! IddCxIsStructureAvailable { 70 | ($name:ident) => {{ 71 | // SAFETY: We only ever do read access 72 | let higher = unsafe { $crate::IddClientVersionHigherThanFramework } != 0; 73 | // SAFETY: We only ever do read access 74 | let struct_count = unsafe { $crate::IddStructureCount }; 75 | 76 | $crate::paste! { 77 | const STRUCT_INDEX: u32 = $crate::IDDSTRUCTENUM::[].0 as u32; 78 | 79 | !higher || STRUCT_INDEX < struct_count 80 | } 81 | }}; 82 | } 83 | 84 | macro_rules! WDF_STRUCTURE_SIZE { 85 | ($name:ty) => { 86 | u32::try_from(::core::mem::size_of::<$name>()).expect("size is correct") 87 | }; 88 | } 89 | 90 | #[macro_export] 91 | macro_rules! WDF_NO_HANDLE { 92 | () => { 93 | ::core::ptr::null_mut() 94 | }; 95 | } 96 | 97 | #[macro_export] 98 | macro_rules! WDF_NO_OBJECT_ATTRIBUTES { 99 | () => { 100 | ::core::ptr::null_mut() 101 | }; 102 | } 103 | 104 | #[macro_export] 105 | macro_rules! WDF_OBJECT_ATTRIBUTES_SET_CONTEXT_TYPE { 106 | ($attr:ident, $context_type:ident) => { 107 | $attr.ContextTypeInfo = $context_type; 108 | }; 109 | } 110 | 111 | impl WDF_OBJECT_ATTRIBUTES { 112 | /// Initializes the [`WDF_OBJECT_ATTRIBUTES`] structure 113 | /// 114 | /// 115 | /// Sets 116 | /// - `ExecutionLevel` to [`WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent`] 117 | /// - `SynchronizationScope` to [`WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent`] 118 | #[must_use] 119 | pub fn init() -> Self { 120 | // SAFETY: All fields are zero-able 121 | let mut attributes: Self = unsafe { ::core::mem::zeroed() }; 122 | 123 | attributes.Size = WDF_STRUCTURE_SIZE!(Self); 124 | attributes.SynchronizationScope = 125 | WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent; 126 | attributes.ExecutionLevel = WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent; 127 | 128 | attributes 129 | } 130 | 131 | #[must_use] 132 | pub fn init_context_type(context_type: &_WDF_OBJECT_CONTEXT_TYPE_INFO) -> Self { 133 | let mut attr = Self::init(); 134 | 135 | WDF_OBJECT_ATTRIBUTES_SET_CONTEXT_TYPE!(attr, context_type); 136 | 137 | attr 138 | } 139 | } 140 | 141 | impl WDF_DRIVER_CONFIG { 142 | /// Initializes the [`WDF_DRIVER_CONFIG`] structure 143 | /// 144 | #[must_use] 145 | pub fn init(EvtDriverDeviceAdd: PFN_WDF_DRIVER_DEVICE_ADD) -> Self { 146 | // SAFETY: All fields are zero-able 147 | let mut config: Self = unsafe { core::mem::zeroed() }; 148 | 149 | config.Size = WDF_STRUCTURE_SIZE!(Self); 150 | 151 | config.EvtDriverDeviceAdd = EvtDriverDeviceAdd; 152 | 153 | config 154 | } 155 | } 156 | 157 | impl WDF_PNPPOWER_EVENT_CALLBACKS { 158 | /// Initializes the [`WDF_PNPPOWER_EVENT_CALLBACKS`] structure 159 | /// 160 | #[must_use] 161 | pub fn init() -> Self { 162 | // SAFETY: All fields are zero-able 163 | let mut callbacks: Self = unsafe { core::mem::zeroed() }; 164 | callbacks.Size = WDF_STRUCTURE_SIZE!(Self); 165 | 166 | callbacks 167 | } 168 | } 169 | 170 | /// If this returns None, the struct is NOT available to be used 171 | macro_rules! IDD_STRUCTURE_SIZE { 172 | ($name:ty) => {{ 173 | // SAFETY: We only ever do read access, copy is fine 174 | let higher = unsafe { IddClientVersionHigherThanFramework } != 0; 175 | // SAFETY: We only ever do read access, copy is fine 176 | let struct_count = unsafe { IddStructureCount }; 177 | 178 | if higher { 179 | // as u32 is fine, since there's no way there's > 4 billion structs 180 | const STRUCT_INDEX: u32 = 181 | $crate::paste! { IDDSTRUCTENUM::[].0 as u32 }; 182 | 183 | // SAFETY: A pointer to a [size_t], copying the pointer is ok 184 | let ptr = unsafe { IddStructures }; 185 | 186 | if STRUCT_INDEX < struct_count { 187 | // SAFETY: we validated struct index is able to be accessed 188 | let ptr = unsafe { ptr.add(STRUCT_INDEX as usize) }; 189 | // SAFETY: So it's ok to read 190 | u32::try_from(unsafe { ptr.read() }).ok() 191 | } else { 192 | // struct CANNOT be used 193 | None 194 | } 195 | } else { 196 | u32::try_from(::std::mem::size_of::<$name>()).ok() 197 | } 198 | }}; 199 | } 200 | 201 | impl IDD_CX_CLIENT_CONFIG { 202 | #[must_use] 203 | pub fn init() -> Option { 204 | // SAFETY: All fields are zero-able 205 | let mut config: Self = unsafe { core::mem::zeroed() }; 206 | 207 | config.Size = IDD_STRUCTURE_SIZE!(IDD_CX_CLIENT_CONFIG)?; 208 | 209 | Some(config) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /rust/wdf-umdf/src/iddcx.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![allow(clippy::missing_errors_doc)] 3 | 4 | use std::sync::OnceLock; 5 | 6 | use wdf_umdf_sys::{ 7 | IDARG_IN_ADAPTER_INIT, IDARG_IN_MONITORCREATE, IDARG_IN_QUERY_HWCURSOR, 8 | IDARG_IN_SETUP_HWCURSOR, IDARG_IN_SWAPCHAINSETDEVICE, IDARG_OUT_ADAPTER_INIT, 9 | IDARG_OUT_MONITORARRIVAL, IDARG_OUT_MONITORCREATE, IDARG_OUT_QUERY_HWCURSOR, 10 | IDARG_OUT_RELEASEANDACQUIREBUFFER, IDDCX_ADAPTER, IDDCX_MONITOR, IDDCX_SWAPCHAIN, 11 | IDD_CX_CLIENT_CONFIG, NTSTATUS, WDFDEVICE, WDFDEVICE_INIT, 12 | }; 13 | 14 | #[derive(Copy, Clone, Debug, thiserror::Error)] 15 | pub enum IddCxError { 16 | #[error("{0}")] 17 | IddCxFunctionNotAvailable(&'static str), 18 | #[error("{0}")] 19 | CallFailed(NTSTATUS), 20 | #[error("{0}")] 21 | NtStatus(NTSTATUS), 22 | } 23 | 24 | impl From for NTSTATUS { 25 | fn from(value: IddCxError) -> Self { 26 | #[allow(clippy::enum_glob_use)] 27 | use IddCxError::*; 28 | match value { 29 | IddCxFunctionNotAvailable(_) => Self::STATUS_NOT_FOUND, 30 | CallFailed(status) => status, 31 | NtStatus(n) => n, 32 | } 33 | } 34 | } 35 | 36 | impl From for IddCxError { 37 | fn from(value: NTSTATUS) -> Self { 38 | IddCxError::CallFailed(value) 39 | } 40 | } 41 | 42 | impl From for IddCxError { 43 | fn from(val: i32) -> Self { 44 | IddCxError::NtStatus(NTSTATUS(val)) 45 | } 46 | } 47 | 48 | macro_rules! IddCxCall { 49 | ($name:ident ( $($args:expr),* )) => { 50 | IddCxCall!(false, $name($($args),*)) 51 | }; 52 | 53 | ($other_is_error:expr, $name:ident ( $($args:expr),* )) => {{ 54 | static CACHED_FN: OnceLock< 55 | Result< 56 | ::paste::paste!(::wdf_umdf_sys::[]), 57 | IddCxError 58 | > 59 | > = OnceLock::new(); 60 | 61 | let f = CACHED_FN.get_or_init(|| { 62 | ::paste::paste! { 63 | const FN_INDEX: usize = ::wdf_umdf_sys::IDDFUNCENUM::[<$name TableIndex>].0 as usize; 64 | 65 | // validate that wdf function can be used 66 | let is_available = ::wdf_umdf_sys::IddCxIsFunctionAvailable!($name); 67 | 68 | if is_available { 69 | // SAFETY: Only immutable accesses are done to this 70 | // The underlying array is Copy, so we call as_ptr() directly on it inside block 71 | let fn_table = unsafe { ::wdf_umdf_sys::IddFunctions.as_ptr() }; 72 | 73 | // SAFETY: Ensured that this is present by if condition from `WdfIsFunctionAvailable!` 74 | let f = unsafe { 75 | fn_table.add(FN_INDEX) 76 | .cast::<::wdf_umdf_sys::[]>() 77 | }; 78 | 79 | // SAFETY: Ensured that this is present by if condition from `IddIsFunctionAvailable!` 80 | let f = unsafe { f.read() }; 81 | 82 | Ok(f) 83 | } else { 84 | Err($crate::IddCxError::IddCxFunctionNotAvailable(concat!(stringify!($name), " is not available"))) 85 | } 86 | } 87 | }).clone()?; 88 | 89 | // SAFETY: Above: If it's Ok, then it's guaranteed to be Some(fn) 90 | let f = unsafe { f.unwrap_unchecked() }; 91 | 92 | // SAFETY: Pointer to globals is always immutable 93 | let globals = unsafe { ::wdf_umdf_sys::IddDriverGlobals }; 94 | 95 | // SAFETY: None. User is responsible for safety and must use their own unsafe block 96 | let result = unsafe { f(globals, $($args),*) }; 97 | 98 | if $crate::is_nt_error(&result, $other_is_error) { 99 | Err(result.into()) 100 | } else { 101 | Ok(result.into()) 102 | } 103 | 104 | }}; 105 | } 106 | 107 | /// # Safety 108 | /// 109 | /// None. User is responsible for safety. 110 | pub unsafe fn IddCxDeviceInitConfig( 111 | // in, out 112 | DeviceInit: &mut WDFDEVICE_INIT, 113 | // in 114 | Config: &IDD_CX_CLIENT_CONFIG, 115 | ) -> Result { 116 | IddCxCall! { 117 | IddCxDeviceInitConfig( 118 | DeviceInit, 119 | Config 120 | ) 121 | } 122 | } 123 | 124 | /// # Safety 125 | /// 126 | /// None. User is responsible for safety. 127 | pub unsafe fn IddCxDeviceInitialize( 128 | // in 129 | Device: WDFDEVICE, 130 | ) -> Result { 131 | IddCxCall! { 132 | IddCxDeviceInitialize( 133 | Device 134 | ) 135 | } 136 | } 137 | 138 | /// # Safety 139 | /// 140 | /// None. User is responsible for safety. 141 | pub unsafe fn IddCxAdapterInitAsync( 142 | // in 143 | pInArgs: &IDARG_IN_ADAPTER_INIT, 144 | // out 145 | pOutArgs: &mut IDARG_OUT_ADAPTER_INIT, 146 | ) -> Result { 147 | IddCxCall! { 148 | IddCxAdapterInitAsync( 149 | pInArgs, 150 | pOutArgs 151 | ) 152 | } 153 | } 154 | 155 | /// # Safety 156 | /// 157 | /// None. User is responsible for safety. 158 | #[rustfmt::skip] 159 | pub unsafe fn IddCxMonitorCreate( 160 | // in 161 | AdapterObject: IDDCX_ADAPTER, 162 | // in 163 | pInArgs: &IDARG_IN_MONITORCREATE, 164 | // out 165 | pOutArgs: &mut IDARG_OUT_MONITORCREATE, 166 | ) -> Result { 167 | IddCxCall!( 168 | IddCxMonitorCreate( 169 | AdapterObject, 170 | pInArgs, 171 | pOutArgs 172 | ) 173 | ) 174 | } 175 | 176 | /// # Safety 177 | /// 178 | /// None. User is responsible for safety. 179 | #[rustfmt::skip] 180 | pub unsafe fn IddCxMonitorArrival( 181 | // in 182 | MonitorObject: IDDCX_MONITOR, 183 | // out 184 | pOutArgs: &mut IDARG_OUT_MONITORARRIVAL, 185 | ) -> Result { 186 | IddCxCall!( 187 | IddCxMonitorArrival( 188 | MonitorObject, 189 | pOutArgs 190 | ) 191 | ) 192 | } 193 | 194 | /// # Safety 195 | /// 196 | /// None. User is responsible for safety. 197 | #[rustfmt::skip] 198 | pub unsafe fn IddCxSwapChainSetDevice( 199 | // in 200 | SwapChainObject: IDDCX_SWAPCHAIN, 201 | // in 202 | pInArgs: &IDARG_IN_SWAPCHAINSETDEVICE 203 | ) -> Result { 204 | IddCxCall!( 205 | true, 206 | IddCxSwapChainSetDevice( 207 | SwapChainObject, 208 | pInArgs 209 | ) 210 | ) 211 | } 212 | 213 | /// # Safety 214 | /// 215 | /// None. User is responsible for safety. 216 | #[rustfmt::skip] 217 | pub unsafe fn IddCxSwapChainReleaseAndAcquireBuffer( 218 | // in 219 | SwapChainObject: IDDCX_SWAPCHAIN, 220 | // out 221 | pOutArgs: &mut IDARG_OUT_RELEASEANDACQUIREBUFFER 222 | ) -> Result { 223 | IddCxCall!( 224 | true, 225 | IddCxSwapChainReleaseAndAcquireBuffer( 226 | SwapChainObject, 227 | pOutArgs 228 | ) 229 | ) 230 | } 231 | 232 | /// # Safety 233 | /// 234 | /// None. User is responsible for safety. 235 | #[rustfmt::skip] 236 | pub unsafe fn IddCxSwapChainFinishedProcessingFrame( 237 | // in 238 | SwapChainObject: IDDCX_SWAPCHAIN 239 | ) -> Result { 240 | IddCxCall!( 241 | true, 242 | IddCxSwapChainFinishedProcessingFrame( 243 | SwapChainObject 244 | ) 245 | ) 246 | } 247 | 248 | /// # Safety 249 | /// 250 | /// None. User is responsible for safety. 251 | #[rustfmt::skip] 252 | pub unsafe fn IddCxMonitorDeparture( 253 | // in 254 | MonitorObject: IDDCX_MONITOR 255 | ) -> Result { 256 | IddCxCall!( 257 | IddCxMonitorDeparture( 258 | MonitorObject 259 | ) 260 | ) 261 | } 262 | 263 | /// # Safety 264 | /// 265 | /// None. User is responsible for safety. 266 | #[rustfmt::skip] 267 | pub unsafe fn IddCxMonitorSetupHardwareCursor( 268 | // in 269 | MonitorObject: IDDCX_MONITOR, 270 | // in 271 | pInArgs: &IDARG_IN_SETUP_HWCURSOR 272 | ) -> Result { 273 | IddCxCall!( 274 | IddCxMonitorSetupHardwareCursor( 275 | MonitorObject, 276 | pInArgs 277 | ) 278 | ) 279 | } 280 | 281 | /// # Safety 282 | /// 283 | /// None. User is responsible for safety. 284 | #[rustfmt::skip] 285 | pub unsafe fn IddCxMonitorQueryHardwareCursor( 286 | // in 287 | MonitorObject: IDDCX_MONITOR, 288 | // in 289 | pInArgs: &IDARG_IN_QUERY_HWCURSOR, 290 | // out 291 | pOutArgs: &mut IDARG_OUT_QUERY_HWCURSOR 292 | ) -> Result { 293 | IddCxCall!( 294 | IddCxMonitorQueryHardwareCursor( 295 | MonitorObject, 296 | pInArgs, 297 | pOutArgs 298 | ) 299 | ) 300 | } 301 | -------------------------------------------------------------------------------- /rust/wdf-umdf-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fmt::{self, Display}; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use bindgen::Abi; 6 | use winreg::enums::HKEY_LOCAL_MACHINE; 7 | use winreg::RegKey; 8 | 9 | const UMDF_V: &str = "2.31"; 10 | const IDDCX_V: &str = "1.4"; 11 | 12 | #[derive(Debug, thiserror::Error)] 13 | enum Error { 14 | #[error(transparent)] 15 | IoError(#[from] std::io::Error), 16 | #[error("cannot find the directory")] 17 | DirectoryNotFound, 18 | } 19 | 20 | /// Retrieves the path to the Windows Kits directory. The default should be 21 | /// `C:\Program Files (x86)\Windows Kits\10`. 22 | /// 23 | /// # Errors 24 | /// Returns IO error if failed 25 | fn get_windows_kits_dir() -> Result { 26 | let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); 27 | let key = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots"; 28 | let dir: String = hklm.open_subkey(key)?.get_value("KitsRoot10")?; 29 | 30 | Ok(dir.into()) 31 | } 32 | 33 | #[derive(Clone, Copy, PartialEq)] 34 | enum DirectoryType { 35 | Include, 36 | Library, 37 | } 38 | 39 | #[derive(Clone, Copy, PartialEq)] 40 | enum Target { 41 | X86_64, 42 | ARM64, 43 | } 44 | 45 | impl Default for Target { 46 | fn default() -> Self { 47 | let target = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); 48 | match &*target { 49 | "x86_64" => Self::X86_64, 50 | "aarch64" => Self::ARM64, 51 | _ => unimplemented!("{target} arch is unsupported"), 52 | } 53 | } 54 | } 55 | 56 | impl Display for Target { 57 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 58 | match self { 59 | Target::X86_64 => f.write_str("x64"), 60 | Target::ARM64 => f.write_str("arm64"), 61 | } 62 | } 63 | } 64 | 65 | fn get_base_path>(dir_type: DirectoryType, subs: &[S]) -> Result { 66 | let mut dir = get_windows_kits_dir()?.join(match dir_type { 67 | DirectoryType::Include => "Include", 68 | DirectoryType::Library => "Lib", 69 | }); 70 | 71 | dir.extend(subs); 72 | if !dir.is_dir() { 73 | return Err(Error::DirectoryNotFound); 74 | } 75 | 76 | Ok(dir) 77 | } 78 | 79 | fn get_sdk_path>(dir_type: DirectoryType, subs: &[S]) -> Result { 80 | // We first append lib to the path and read the directory.. 81 | let dir = get_windows_kits_dir()? 82 | .join(match dir_type { 83 | DirectoryType::Include => "Include", 84 | DirectoryType::Library => "Lib", 85 | }) 86 | .read_dir()?; 87 | 88 | // In the lib directory we may have one or more directories named after the version of Windows, 89 | // we will be looking for the highest version number. 90 | let mut dir = dir 91 | .filter_map(Result::ok) 92 | .map(|dir| dir.path()) 93 | .filter(|dir| { 94 | let is_sdk = dir 95 | .components() 96 | .last() 97 | .and_then(|c| c.as_os_str().to_str()) 98 | .map_or(false, |c| c.starts_with("10.")); 99 | 100 | let mut sub_dir = dir.clone(); 101 | sub_dir.extend(subs); 102 | 103 | is_sdk && sub_dir.is_dir() 104 | }) 105 | .max() 106 | .ok_or_else(|| Error::DirectoryNotFound)?; 107 | 108 | dir.extend(subs); 109 | if !dir.is_dir() { 110 | return Err(Error::DirectoryNotFound); 111 | } 112 | 113 | // Finally append um to the path to get the path to the user mode libraries. 114 | Ok(dir) 115 | } 116 | 117 | /// Retrieves the path to the user mode libraries. The path may look something like: 118 | /// `C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\um`. 119 | /// 120 | /// # Errors 121 | /// Returns IO error if failed 122 | fn get_um_dir(dir_type: DirectoryType) -> Result { 123 | let target = Target::default().to_string(); 124 | 125 | let binding = &["um", &target]; 126 | let subs: &[&str] = match dir_type { 127 | DirectoryType::Include => &["um"], 128 | DirectoryType::Library => binding, 129 | }; 130 | 131 | let dir = get_sdk_path(dir_type, subs)?; 132 | Ok(dir) 133 | } 134 | 135 | /// # Errors 136 | /// Returns IO error if failed 137 | fn get_umdf_dir(dir_type: DirectoryType) -> Result { 138 | match dir_type { 139 | DirectoryType::Include => get_base_path(dir_type, &["wdf", "umdf", UMDF_V]), 140 | DirectoryType::Library => get_base_path( 141 | dir_type, 142 | &["wdf", "umdf", &Target::default().to_string(), UMDF_V], 143 | ), 144 | } 145 | } 146 | 147 | /// Retrieves the path to the shared headers. The path may look something like: 148 | /// `C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\shared`. 149 | /// 150 | /// # Errors 151 | /// Returns IO error if failed 152 | fn get_shared_dir() -> Result { 153 | let dir = get_sdk_path(DirectoryType::Include, &["shared"])?; 154 | Ok(dir) 155 | } 156 | 157 | fn build_dir() -> PathBuf { 158 | PathBuf::from( 159 | std::env::var_os("OUT_DIR").expect("the environment variable OUT_DIR is undefined"), 160 | ) 161 | } 162 | 163 | fn generate() { 164 | // Find the include directory containing the user headers. 165 | let include_um_dir = get_um_dir(DirectoryType::Include).unwrap(); 166 | let lib_um_dir = get_um_dir(DirectoryType::Library).unwrap(); 167 | let shared = get_shared_dir().unwrap(); 168 | 169 | println!("cargo:rustc-link-search={}", lib_um_dir.display()); 170 | 171 | // Tell Cargo to re-run this if src/wrapper.h gets changed. 172 | println!("cargo:rerun-if-changed=c/wrapper.h"); 173 | 174 | // 175 | // UMDF 176 | // 177 | 178 | let umdf_lib_dir = get_umdf_dir(DirectoryType::Library).unwrap(); 179 | 180 | println!("cargo:rustc-link-search={}", umdf_lib_dir.display()); 181 | 182 | let wdf_include_dir = get_umdf_dir(DirectoryType::Include).unwrap(); 183 | 184 | // need to link to umdf lib 185 | println!("cargo:rustc-link-lib=static=WdfDriverStubUm"); 186 | 187 | // 188 | // IDDCX 189 | // 190 | 191 | let mut iddcx_lib_dir = lib_um_dir.clone(); 192 | iddcx_lib_dir.push("iddcx"); 193 | iddcx_lib_dir.push(IDDCX_V); 194 | 195 | println!("cargo:rustc-link-search={}", iddcx_lib_dir.display()); 196 | 197 | // need to link to iddcx lib 198 | println!("cargo:rustc-link-lib=static=IddCxStub"); 199 | 200 | // 201 | // REST 202 | // 203 | 204 | // Get the build directory. 205 | let out_path = build_dir(); 206 | 207 | // Generate the bindings 208 | let mut builder = bindgen::Builder::default() 209 | .derive_debug(false) 210 | .layout_tests(false) 211 | .default_enum_style(bindgen::EnumVariation::NewType { 212 | is_bitfield: false, 213 | is_global: false, 214 | }) 215 | .merge_extern_blocks(true) 216 | .header("c/wrapper.h") 217 | .header( 218 | get_sdk_path(DirectoryType::Include, &["um", "iddcx", IDDCX_V]) 219 | .unwrap() 220 | .join("IddCx.h") 221 | .to_string_lossy() 222 | .to_string(), 223 | ) 224 | // general um includes 225 | .clang_arg(format!("-I{}", include_um_dir.display())) 226 | // umdf includes 227 | .clang_arg(format!("-I{}", wdf_include_dir.display())) 228 | .clang_arg(format!("-I{}", shared.display())) 229 | // because aarch64 needs to find excpt.h 230 | .clang_arg(format!( 231 | "-I{}", 232 | get_sdk_path(DirectoryType::Include, &["km", "crt"]) 233 | .unwrap() 234 | .display() 235 | )) 236 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) 237 | .blocklist_type("_?P?IMAGE_TLS_DIRECTORY.*") 238 | // we will use our own custom type 239 | .blocklist_item("NTSTATUS") 240 | .blocklist_item("IddMinimumVersionRequired") 241 | .blocklist_item("WdfMinimumVersionRequired") 242 | .clang_arg("--language=c++") 243 | .clang_arg("-fms-compatibility") 244 | .clang_arg("-fms-extensions") 245 | .override_abi(Abi::CUnwind, ".*") 246 | .generate_cstr(true) 247 | .derive_default(true); 248 | 249 | let defines = match Target::default() { 250 | Target::X86_64 => ["AMD64", "_AMD64_"], 251 | Target::ARM64 => ["ARM64", "_ARM64_"], 252 | }; 253 | 254 | for define in defines { 255 | builder = builder.clang_arg(format!("-D{define}")); 256 | } 257 | 258 | // generate 259 | let umdf = builder.generate().unwrap(); 260 | 261 | // Write the bindings to the $OUT_DIR/bindings.rs file. 262 | umdf.write_to_file(out_path.join("umdf.rs")).unwrap(); 263 | } 264 | 265 | fn main() { 266 | println!("cargo:rerun-if-changed=build.rs"); 267 | 268 | generate(); 269 | } 270 | --------------------------------------------------------------------------------