Add native self-hosted instance connection to fluxer_desktop

Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 +
tools/ci) with a "Connect to a Different Server" menu item and popout
that lets the desktop app switch to any self-hosted Fluxer instance,
plus fixes for well-known discovery on single-domain self-hosted
deployments and a false-positive ERR_ABORTED on same-origin client
redirects during the switch. Defaults to chat.fluxr.chat and uses an
isolated userData directory from the official build.
This commit is contained in:
2026-07-01 18:22:43 -04:00
commit 682afacd30
1763 changed files with 613720 additions and 0 deletions
+478
View File
@@ -0,0 +1,478 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "convert_case"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "ctor"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a"
[[package]]
name = "fluxer_encoder_ring"
version = "0.0.0"
dependencies = [
"libloading",
"windows",
]
[[package]]
name = "fluxer_gpu_rebuild"
version = "0.0.0"
dependencies = [
"parking_lot",
]
[[package]]
name = "fluxer_screen_frame_bus"
version = "0.0.0"
dependencies = [
"fluxer_gpu_rebuild",
"parking_lot",
]
[[package]]
name = "fluxer_win_game_capture"
version = "0.0.0"
dependencies = [
"fluxer_encoder_ring",
"fluxer_screen_frame_bus",
"napi",
"napi-build",
"napi-derive",
"parking_lot",
"windows",
"windows-core",
"windows-sys",
]
[[package]]
name = "futures"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"slab",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "memchr"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "napi"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad513ff22558f1830b595ea6eb4091da48145d09a222ce157e781896f78be0b9"
dependencies = [
"bitflags",
"ctor",
"futures",
"napi-build",
"napi-sys",
"nohash-hasher",
"rustc-hash",
]
[[package]]
name = "napi-build"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
[[package]]
name = "napi-derive"
version = "3.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b3f766e04667e6da0e181e2da4f85475d5a6513b7cf6a80bea184e224a5b42"
dependencies = [
"convert_case",
"ctor",
"napi-derive-backend",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "napi-derive-backend"
version = "5.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d5af30503edf933ce7377cf6d4c877a62b0f1107ea05585f1b5e430e88d5baf"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"semver",
"syn",
]
[[package]]
name = "napi-sys"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f5bcdf71abd3a50d00b49c1c2c75251cb3c913777d6139cd37dabc093a5e400"
dependencies = [
"libloading",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
@@ -0,0 +1,56 @@
[package]
name = "fluxer_win_game_capture"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
[workspace]
resolver = "2"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
napi = {version = "3.9.1", default-features = false, features = ["dyn-symbols", "napi8"]}
napi-derive = "3.5.6"
fluxer_screen_frame_bus = {path = "../screen-frame-bus"}
fluxer_encoder_ring = {path = "../encoder-ring"}
parking_lot = "0.12"
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.62.2", features = [
"Graphics_Capture",
"Graphics_DirectX_Direct3D11",
"Win32_Foundation",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_System_WinRT",
"Win32_System_WinRT_Direct3D11",
"Win32_System_WinRT_Graphics_Capture",
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
"Win32_System_Performance",
]}
windows-core = "0.62.2"
windows-sys = {version = "0.61.2", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_Security",
"Win32_System_Diagnostics_Debug",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Performance",
"Win32_System_ProcessStatus",
"Win32_System_Registry",
"Win32_System_SystemInformation",
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
]}
[build-dependencies]
napi-build = "2.3.2"
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
extern crate napi_build;
fn main() {
napi_build::setup();
}
@@ -0,0 +1,6 @@
{
"$comment": "Optional per-process game-capture injection policy override. Placed next to the native addon, it augments the deny/force-cpu lists compiled into the addon. Entries are matched case-insensitively by executable file name (e.g. game.exe); paths are reduced to their final component. 'deny' refuses injection; 'allow' opts a process back in past the built-in deny list; 'forceCpu' keeps injection but prefers CPU readback over the shared-texture fast path. A missing or malformed file falls back to the embedded defaults.",
"deny": [],
"allow": [],
"forceCpu": []
}
+375
View File
@@ -0,0 +1,375 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "fluxer_game_hook"
version = "0.0.0"
dependencies = [
"retour",
"windows",
"windows-sys 0.61.2",
]
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "iced-x86"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b"
dependencies = [
"lazy_static",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "mach2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
[[package]]
name = "mmap-fixed-fixed"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0681853891801e4763dc252e843672faf32bcfee27a0aa3b19733902af450acc"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "region"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7"
dependencies = [
"bitflags",
"libc",
"mach2",
"windows-sys 0.52.0",
]
[[package]]
name = "retour"
version = "0.4.0-alpha.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ead4bc8e12d553ff70769c5f5c21f5f4f0e73c0018068a6bb5a3d7d3b9e57ec7"
dependencies = [
"cfg-if",
"generic-array",
"iced-x86",
"libc",
"mmap-fixed-fixed",
"once_cell",
"region",
"slice-pool2",
]
[[package]]
name = "slice-pool2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a3d689654af89bdfeba29a914ab6ac0236d382eb3b764f7454dde052f2821f8"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
@@ -0,0 +1,46 @@
[package]
name = "fluxer_game_hook"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
[workspace]
resolver = "2"
[lib]
crate-type = ["cdylib"]
# `retour` only ships x86/x86_64 trampoline + patcher backends (its `arch`
# module has no aarch64 variant), so it does not even compile for
# aarch64-pc-windows-msvc. Gate it to the architectures it supports; the
# aarch64 hook uses the in-crate `inline_hook::aarch64` backend instead.
[target.'cfg(all(target_os = "windows", any(target_arch = "x86", target_arch = "x86_64")))'.dependencies]
retour = "0.4.0-alpha.4"
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D9",
"Win32_Graphics_Direct3D10",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Direct3D11on12",
"Win32_Graphics_Direct3D12",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
]}
windows-sys = {version = "0.61.2", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_Graphics_OpenGL",
"Win32_Security",
"Win32_System_Diagnostics_Debug",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Performance",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
]}
@@ -0,0 +1,248 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
pub const STOLEN_BYTES: usize = 16;
pub const NOP: u32 = 0xD503_201F;
pub const LDR_X16_PC8: u32 = 0x5800_0050;
pub const BR_X16: u32 = 0xD61F_0200;
pub const BLR_X16: u32 = 0xD63F_0200;
pub fn is_b(insn: u32) -> bool {
(insn & 0xFC00_0000) == 0x1400_0000
}
pub fn is_bl(insn: u32) -> bool {
(insn & 0xFC00_0000) == 0x9400_0000
}
pub fn needs_absolute_island(insn: u32) -> bool {
is_b(insn) || is_bl(insn)
}
pub fn branch_target(insn: u32, src_pc: u64) -> Option<u64> {
if !is_b(insn) && !is_bl(insn) {
return None;
}
let imm26 = (insn & 0x03FF_FFFF) as i32;
let off = ((imm26 << 6) >> 6) as i64 * 4;
Some((src_pc as i64 + off) as u64)
}
pub fn encode_imm26(byte_off: i64) -> Option<u32> {
if byte_off & 0b11 != 0 {
return None;
}
let words = byte_off >> 2;
if !(-(1 << 25)..(1 << 25)).contains(&words) {
return None;
}
Some((words as u32) & 0x03FF_FFFF)
}
pub fn append_abs_branch(out: &mut Vec<u8>, addr: u64, link: bool) {
let branch = if link { BLR_X16 } else { BR_X16 };
out.extend_from_slice(&LDR_X16_PC8.to_le_bytes());
out.extend_from_slice(&branch.to_le_bytes());
out.extend_from_slice(&addr.to_le_bytes());
}
pub fn adrp_target(insn: u32, src_pc: u64) -> Option<u64> {
if (insn & 0x9F00_0000) != 0x9000_0000 {
return None;
}
let immlo = ((insn >> 29) & 0x3) as i64;
let immhi = ((insn >> 5) & 0x7FFFF) as i64;
let raw = (immhi << 2) | immlo;
let imm21 = (raw << 43) >> 43;
let page = (src_pc & !0xFFF) as i64 + imm21 * 4096;
Some(page as u64)
}
fn ldr_unsigned_64(insn: u32) -> Option<(u32, u32, u64)> {
if (insn & 0xFFC0_0000) != 0xF940_0000 {
return None;
}
let imm12 = ((insn >> 10) & 0xFFF) as u64;
let rn = (insn >> 5) & 0x1F;
let rt = insn & 0x1F;
Some((rt, rn, imm12 * 8))
}
fn br_register(insn: u32) -> Option<u32> {
if (insn & 0xFFFF_FC1F) != 0xD61F_0000 {
return None;
}
Some((insn >> 5) & 0x1F)
}
pub unsafe fn import_thunk_target(prologue: &[u8], src_base: u64) -> Option<u64> {
if prologue.len() < 12 {
return None;
}
let adrp = u32::from_le_bytes(prologue[0..4].try_into().ok()?);
let ldr = u32::from_le_bytes(prologue[4..8].try_into().ok()?);
let br = u32::from_le_bytes(prologue[8..12].try_into().ok()?);
let adrp_reg = adrp & 0x1F;
let page = adrp_target(adrp, src_base)?;
let (ldr_rt, ldr_rn, offset) = ldr_unsigned_64(ldr)?;
let br_rn = br_register(br)?;
if adrp_reg != ldr_rn || ldr_rt != br_rn {
return None;
}
let pointer_addr = page.checked_add(offset)?;
let target = unsafe { core::ptr::read_unaligned(pointer_addr as *const u64) };
(target != 0).then_some(target)
}
pub fn emit_branch_to_island(insn: u32, dst_pc: u64, island_addr: u64) -> Option<u32> {
let link = is_bl(insn);
let off = island_addr as i64 - dst_pc as i64;
let imm = encode_imm26(off)?;
let opc = if link { 0x9400_0000 } else { 0x1400_0000 };
Some(opc | imm)
}
pub fn island_for_branch(insn: u32, src_pc: u64) -> Option<Vec<u8>> {
let target = branch_target(insn, src_pc)?;
let mut bytes = Vec::new();
append_abs_branch(&mut bytes, target, is_bl(insn));
Some(bytes)
}
pub fn relocate_instruction(insn: u32, src_pc: u64, dst_pc: u64) -> Option<u32> {
if (insn & 0x9F00_0000) == 0x9000_0000 {
return relocate_adr(insn, src_pc, dst_pc, true);
}
if (insn & 0x9F00_0000) == 0x1000_0000 {
return relocate_adr(insn, src_pc, dst_pc, false);
}
if is_b(insn) || is_bl(insn) {
let target = branch_target(insn, src_pc)?;
let off = target as i64 - dst_pc as i64;
let imm = encode_imm26(off)?;
return Some((insn & 0xFC00_0000) | imm);
}
if (insn & 0xFF00_0010) == 0x5400_0000 {
return relocate_imm19_at5(insn, src_pc, dst_pc);
}
if (insn & 0x7F00_0000) == 0x3400_0000 {
return relocate_imm19_at5(insn, src_pc, dst_pc);
}
if (insn & 0x7F00_0000) == 0x3600_0000 {
return relocate_tbz(insn, src_pc, dst_pc);
}
if (insn & 0x3B00_0000) == 0x1800_0000 {
return relocate_imm19_at5(insn, src_pc, dst_pc);
}
Some(insn)
}
fn relocate_adr(insn: u32, src_pc: u64, dst_pc: u64, page: bool) -> Option<u32> {
let immlo = ((insn >> 29) & 0x3) as i64;
let immhi = ((insn >> 5) & 0x7FFFF) as i64;
let raw = (immhi << 2) | immlo;
let imm21 = (raw << 43) >> 43;
let (src_ref, dst_ref, scale) = if page {
(src_pc & !0xFFF, dst_pc & !0xFFF, 4096i64)
} else {
(src_pc, dst_pc, 1i64)
};
let target = src_ref as i64 + imm21 * scale;
let new_off = target - dst_ref as i64;
if scale != 1 && new_off & 0xFFF != 0 {
return None;
}
let scaled = new_off / scale;
if !(-(1 << 20)..(1 << 20)).contains(&scaled) {
return None;
}
let new_raw = (scaled as u32) & 0x1F_FFFF;
let new_immlo = (new_raw & 0x3) << 29;
let new_immhi = ((new_raw >> 2) & 0x7FFFF) << 5;
Some((insn & 0x9F00_001F) | new_immlo | new_immhi)
}
fn relocate_imm19_at5(insn: u32, src_pc: u64, dst_pc: u64) -> Option<u32> {
let imm19 = ((insn >> 5) & 0x7FFFF) as i64;
let off = ((imm19 << 45) >> 45) * 4;
let target = src_pc as i64 + off;
let new_off = target - dst_pc as i64;
if new_off & 0b11 != 0 {
return None;
}
let words = new_off >> 2;
if !(-(1 << 18)..(1 << 18)).contains(&words) {
return None;
}
let new_imm19 = ((words as u32) & 0x7FFFF) << 5;
Some((insn & !(0x7FFFF << 5)) | new_imm19)
}
fn relocate_tbz(insn: u32, src_pc: u64, dst_pc: u64) -> Option<u32> {
let imm14 = ((insn >> 5) & 0x3FFF) as i64;
let off = ((imm14 << 50) >> 50) * 4;
let target = src_pc as i64 + off;
let new_off = target - dst_pc as i64;
if new_off & 0b11 != 0 {
return None;
}
let words = new_off >> 2;
if !(-(1 << 13)..(1 << 13)).contains(&words) {
return None;
}
let new_imm14 = ((words as u32) & 0x3FFF) << 5;
Some((insn & !(0x3FFF << 5)) | new_imm14)
}
pub fn assemble_trampoline(
prologue: &[u8],
src_base: u64,
dst_base: u64,
resume: u64,
) -> Option<Vec<u8>> {
if !prologue.len().is_multiple_of(4) {
return None;
}
let count = prologue.len() / 4;
const RETURN_BRANCH_BYTES: usize = 16;
const ISLAND_BYTES: usize = 16;
let islands_base = dst_base + (count * 4) as u64 + RETURN_BRANCH_BYTES as u64;
let mut prologue_out: Vec<u8> = Vec::with_capacity(count * 4);
let mut islands_out: Vec<u8> = Vec::new();
let mut next_island = islands_base;
for i in 0..count {
let insn = u32::from_le_bytes(prologue[i * 4..i * 4 + 4].try_into().ok()?);
let src_pc = src_base + (i * 4) as u64;
let dst_pc = dst_base + (i * 4) as u64;
if needs_absolute_island(insn) {
let island_addr = next_island;
next_island += ISLAND_BYTES as u64;
let relocated = emit_branch_to_island(insn, dst_pc, island_addr)?;
prologue_out.extend_from_slice(&relocated.to_le_bytes());
let island = island_for_branch(insn, src_pc)?;
debug_assert_eq!(island.len(), ISLAND_BYTES);
islands_out.extend_from_slice(&island);
} else {
let relocated = relocate_instruction(insn, src_pc, dst_pc)?;
prologue_out.extend_from_slice(&relocated.to_le_bytes());
}
}
let mut out = prologue_out;
append_abs_branch(&mut out, resume, false);
out.extend_from_slice(&islands_out);
Some(out)
}
pub fn relocated_prologue(prologue: &[u8], src_base: u64, dst_base: u64) -> Option<Vec<u8>> {
let count = prologue.len() / 4;
let body = assemble_trampoline(prologue, src_base, dst_base, src_base + STOLEN_BYTES as u64)?;
Some(body[..count * 4].to_vec())
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,584 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::{
GAME_CAPTURE_API_OPENGL, GAME_CAPTURE_FALLBACK_NONE,
GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED, HookState, mark_present,
publish_shared_texture_frame, set_capture_flags, set_fallback_reason, verbose_log,
};
use std::{
ffi::c_void,
ptr::null_mut,
sync::atomic::{AtomicBool, Ordering},
};
use windows::{
Win32::{
Foundation::{HMODULE as WinHmodule, HWND as WinHwnd},
Graphics::{
Direct3D::D3D_DRIVER_TYPE_HARDWARE,
Direct3D11::{
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE,
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_RESOURCE_MISC_SHARED, D3D11_SDK_VERSION,
D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11CreateDeviceAndSwapChain,
ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
},
Dxgi::{
Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_MODE_DESC,
DXGI_MODE_SCALING_UNSPECIFIED, DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED,
DXGI_RATIONAL, DXGI_SAMPLE_DESC,
},
DXGI_PRESENT, DXGI_SWAP_CHAIN_DESC, DXGI_SWAP_EFFECT_DISCARD,
DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIResource, IDXGISwapChain,
},
},
},
core::{BOOL as WinBool, Interface},
};
use windows_sys::Win32::{
Foundation::HWND as SysHwnd,
Graphics::OpenGL::{
GL_COLOR_BUFFER_BIT, GL_LINEAR, GL_NEAREST, GL_NO_ERROR, GL_TEXTURE_2D,
GL_TEXTURE_BINDING_2D, glBindTexture, glDeleteTextures, glFinish, glGenTextures,
glGetError, glGetIntegerv, wglGetCurrentContext, wglGetProcAddress,
},
UI::WindowsAndMessaging::DestroyWindow,
};
const WGL_ACCESS_READ_ONLY_NV: u32 = 0x0000;
const WGL_ACCESS_READ_WRITE_NV: u32 = 0x0001;
const WGL_ACCESS_WRITE_DISCARD_NV: u32 = 0x0002;
const GL_READ_FRAMEBUFFER: u32 = 0x8CA8;
const GL_DRAW_FRAMEBUFFER: u32 = 0x8CA9;
const GL_FRAMEBUFFER: u32 = 0x8D40;
const GL_COLOR_ATTACHMENT0: u32 = 0x8CE0;
const GL_FRAMEBUFFER_COMPLETE: u32 = 0x8CD5;
const GL_READ_FRAMEBUFFER_BINDING: u32 = 0x8CAA;
const GL_DRAW_FRAMEBUFFER_BINDING: u32 = 0x8CA6;
type DxOpenDeviceNvFn = unsafe extern "system" fn(dx_device: *mut c_void) -> *mut c_void;
type DxCloseDeviceNvFn = unsafe extern "system" fn(device: *mut c_void) -> i32;
type DxRegisterObjectNvFn = unsafe extern "system" fn(
device: *mut c_void,
dx_object: *mut c_void,
name: u32,
object_type: u32,
access: u32,
) -> *mut c_void;
type DxUnregisterObjectNvFn =
unsafe extern "system" fn(device: *mut c_void, object: *mut c_void) -> i32;
type DxLockObjectsNvFn =
unsafe extern "system" fn(device: *mut c_void, count: i32, objects: *const *mut c_void) -> i32;
type DxUnlockObjectsNvFn =
unsafe extern "system" fn(device: *mut c_void, count: i32, objects: *const *mut c_void) -> i32;
type GlGenFramebuffersFn = unsafe extern "system" fn(n: i32, framebuffers: *mut u32);
type GlDeleteFramebuffersFn = unsafe extern "system" fn(n: i32, framebuffers: *const u32);
type GlBindFramebufferFn = unsafe extern "system" fn(target: u32, framebuffer: u32);
type GlFramebufferTexture2DFn = unsafe extern "system" fn(
target: u32,
attachment: u32,
textarget: u32,
texture: u32,
level: i32,
);
type GlCheckFramebufferStatusFn = unsafe extern "system" fn(target: u32) -> u32;
type GlBlitFramebufferFn = unsafe extern "system" fn(
src_x0: i32,
src_y0: i32,
src_x1: i32,
src_y1: i32,
dst_x0: i32,
dst_y0: i32,
dst_x1: i32,
dst_y1: i32,
mask: u32,
filter: u32,
);
struct InteropProcs {
open_device: DxOpenDeviceNvFn,
close_device: DxCloseDeviceNvFn,
register_object: DxRegisterObjectNvFn,
unregister_object: DxUnregisterObjectNvFn,
lock_objects: DxLockObjectsNvFn,
unlock_objects: DxUnlockObjectsNvFn,
gen_framebuffers: GlGenFramebuffersFn,
delete_framebuffers: GlDeleteFramebuffersFn,
bind_framebuffer: GlBindFramebufferFn,
framebuffer_texture_2d: GlFramebufferTexture2DFn,
check_framebuffer_status: GlCheckFramebufferStatusFn,
blit_framebuffer: GlBlitFramebufferFn,
}
pub(crate) struct GlInteropState {
procs: InteropProcs,
_device: ID3D11Device,
_context: ID3D11DeviceContext,
swap_chain: IDXGISwapChain,
_texture: ID3D11Texture2D,
dummy_hwnd: SysHwnd,
shared_handle: u64,
dx_device: *mut c_void,
dx_object: *mut c_void,
gl_texture: u32,
draw_fbo: u32,
width: u32,
height: u32,
}
unsafe impl Send for GlInteropState {}
static GL_GPU_DISABLED: AtomicBool = AtomicBool::new(false);
static GL_GPU_UNAVAILABLE_LOGGED: AtomicBool = AtomicBool::new(false);
static GL_DUMMY_PRESENT_ACTIVE: AtomicBool = AtomicBool::new(false);
struct DummyPresentGuard;
impl DummyPresentGuard {
fn enter() -> Self {
GL_DUMMY_PRESENT_ACTIVE.store(true, Ordering::Release);
Self
}
}
impl Drop for DummyPresentGuard {
fn drop(&mut self) {
GL_DUMMY_PRESENT_ACTIVE.store(false, Ordering::Release);
}
}
fn latch_disable(reason: &str) {
if !GL_GPU_DISABLED.swap(true, Ordering::AcqRel) {
verbose_log(&format!(
"opengl interop: latch-disabling GPU path, falling back to glReadPixels CPU path ({reason})"
));
}
}
pub(crate) fn gpu_path_disabled() -> bool {
GL_GPU_DISABLED.load(Ordering::Acquire)
}
pub(crate) fn dummy_present_active() -> bool {
GL_DUMMY_PRESENT_ACTIVE.load(Ordering::Acquire)
}
unsafe fn load_proc<T>(name: &[u8]) -> Option<T> {
debug_assert_eq!(
name.last(),
Some(&0),
"wglGetProcAddress name must be NUL-terminated"
);
let proc = wglGetProcAddress(name.as_ptr());
match proc {
Some(proc) => Some(std::mem::transmute_copy::<_, T>(&proc)),
None => None,
}
}
impl InteropProcs {
unsafe fn load() -> Option<Self> {
if wglGetCurrentContext().is_null() {
return None;
}
let open_device = load_proc::<DxOpenDeviceNvFn>(b"wglDXOpenDeviceNV\0")?;
let close_device = load_proc::<DxCloseDeviceNvFn>(b"wglDXCloseDeviceNV\0")?;
let register_object = load_proc::<DxRegisterObjectNvFn>(b"wglDXRegisterObjectNV\0")?;
let unregister_object = load_proc::<DxUnregisterObjectNvFn>(b"wglDXUnregisterObjectNV\0")?;
let lock_objects = load_proc::<DxLockObjectsNvFn>(b"wglDXLockObjectsNV\0")?;
let unlock_objects = load_proc::<DxUnlockObjectsNvFn>(b"wglDXUnlockObjectsNV\0")?;
let gen_framebuffers = load_proc::<GlGenFramebuffersFn>(b"glGenFramebuffers\0")?;
let delete_framebuffers = load_proc::<GlDeleteFramebuffersFn>(b"glDeleteFramebuffers\0")?;
let bind_framebuffer = load_proc::<GlBindFramebufferFn>(b"glBindFramebuffer\0")?;
let framebuffer_texture_2d =
load_proc::<GlFramebufferTexture2DFn>(b"glFramebufferTexture2D\0")?;
let check_framebuffer_status =
load_proc::<GlCheckFramebufferStatusFn>(b"glCheckFramebufferStatus\0")?;
let blit_framebuffer = load_proc::<GlBlitFramebufferFn>(b"glBlitFramebuffer\0")?;
Some(Self {
open_device,
close_device,
register_object,
unregister_object,
lock_objects,
unlock_objects,
gen_framebuffers,
delete_framebuffers,
bind_framebuffer,
framebuffer_texture_2d,
check_framebuffer_status,
blit_framebuffer,
})
}
}
unsafe fn create_interop_d3d11_device()
-> Option<(ID3D11Device, ID3D11DeviceContext, IDXGISwapChain, SysHwnd)> {
let dummy_hwnd = crate::create_dummy_window();
if dummy_hwnd.is_null() {
verbose_log("opengl interop: failed to create dummy D3D11 flush window");
return None;
}
let desc = DXGI_SWAP_CHAIN_DESC {
BufferDesc: DXGI_MODE_DESC {
Width: 2,
Height: 2,
RefreshRate: DXGI_RATIONAL {
Numerator: 60,
Denominator: 1,
},
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
ScanlineOrdering: DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED,
Scaling: DXGI_MODE_SCALING_UNSPECIFIED,
},
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
BufferCount: 2,
OutputWindow: WinHwnd(dummy_hwnd),
Windowed: WinBool(1),
SwapEffect: DXGI_SWAP_EFFECT_DISCARD,
Flags: 0,
};
let mut swap_chain = None;
let mut device = None;
let mut context = None;
let result = D3D11CreateDeviceAndSwapChain(
None,
D3D_DRIVER_TYPE_HARDWARE,
WinHmodule(null_mut()),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&desc),
Some(&mut swap_chain),
Some(&mut device),
None,
Some(&mut context),
);
if result.is_err() {
let _ = DestroyWindow(dummy_hwnd);
return None;
}
match (device, context, swap_chain) {
(Some(device), Some(context), Some(swap_chain)) => {
Some((device, context, swap_chain, dummy_hwnd))
}
_ => {
let _ = DestroyWindow(dummy_hwnd);
None
}
}
}
unsafe fn create_shared_texture(
device: &ID3D11Device,
width: u32,
height: u32,
) -> Option<(ID3D11Texture2D, u64)> {
let desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: height,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
CPUAccessFlags: 0,
MiscFlags: D3D11_RESOURCE_MISC_SHARED.0 as u32,
};
let mut texture = None;
if device
.CreateTexture2D(&desc, None, Some(&mut texture))
.is_err()
{
return None;
}
let texture = texture?;
let handle = texture
.cast::<IDXGIResource>()
.and_then(|resource| resource.GetSharedHandle())
.ok()?;
Some((texture, handle.0 as usize as u64))
}
impl GlInteropState {
unsafe fn create(width: u32, height: u32) -> Option<Self> {
let procs = InteropProcs::load()?;
let (device, context, swap_chain, dummy_hwnd) = create_interop_d3d11_device()?;
let (texture, shared_handle) = match create_shared_texture(&device, width, height) {
Some(texture) => texture,
None => {
let _ = DestroyWindow(dummy_hwnd);
return None;
}
};
if shared_handle == 0 {
let _ = DestroyWindow(dummy_hwnd);
return None;
}
let dx_device = (procs.open_device)(device.as_raw());
if dx_device.is_null() {
verbose_log("opengl interop: wglDXOpenDeviceNV returned NULL");
let _ = DestroyWindow(dummy_hwnd);
return None;
}
verbose_log("opengl interop: wglDXOpenDeviceNV opened private D3D11 device");
let mut gl_texture = 0u32;
glGenTextures(1, &mut gl_texture);
if gl_texture == 0 {
(procs.close_device)(dx_device);
let _ = DestroyWindow(dummy_hwnd);
return None;
}
let dx_object = (procs.register_object)(
dx_device,
texture.as_raw(),
gl_texture,
GL_TEXTURE_2D,
WGL_ACCESS_WRITE_DISCARD_NV,
);
if dx_object.is_null() {
verbose_log("opengl interop: wglDXRegisterObjectNV returned NULL");
glDeleteTextures(1, &gl_texture);
(procs.close_device)(dx_device);
let _ = DestroyWindow(dummy_hwnd);
return None;
}
verbose_log(&format!(
"opengl interop: registered D3D11 texture <-> GL texture {gl_texture} ({width}x{height} BGRA)"
));
let mut draw_fbo = 0u32;
(procs.gen_framebuffers)(1, &mut draw_fbo);
if draw_fbo == 0 {
(procs.unregister_object)(dx_device, dx_object);
glDeleteTextures(1, &gl_texture);
(procs.close_device)(dx_device);
let _ = DestroyWindow(dummy_hwnd);
return None;
}
Some(Self {
procs,
_device: device,
_context: context,
swap_chain,
_texture: texture,
dummy_hwnd,
shared_handle,
dx_device,
dx_object,
gl_texture,
draw_fbo,
width,
height,
})
}
fn matches(&self, width: u32, height: u32) -> bool {
self.width == width && self.height == height
}
unsafe fn blit_default_framebuffer(&self) -> bool {
let objects = [self.dx_object];
let mut prev_read_fbo = 0i32;
let mut prev_draw_fbo = 0i32;
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &mut prev_read_fbo);
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &mut prev_draw_fbo);
let mut prev_tex = 0i32;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &mut prev_tex);
if (self.procs.lock_objects)(self.dx_device, 1, objects.as_ptr()) == 0 {
verbose_log("opengl interop: wglDXLockObjectsNV FAILED");
return false;
}
(self.procs.bind_framebuffer)(GL_DRAW_FRAMEBUFFER, self.draw_fbo);
(self.procs.framebuffer_texture_2d)(
GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
self.gl_texture,
0,
);
let status = (self.procs.check_framebuffer_status)(GL_DRAW_FRAMEBUFFER);
if status != GL_FRAMEBUFFER_COMPLETE {
verbose_log(&format!(
"opengl interop: draw FBO incomplete (status 0x{status:04X}); unlocking and falling back"
));
(self.procs.framebuffer_texture_2d)(
GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
0,
0,
);
(self.procs.bind_framebuffer)(GL_DRAW_FRAMEBUFFER, prev_draw_fbo as u32);
(self.procs.bind_framebuffer)(GL_READ_FRAMEBUFFER, prev_read_fbo as u32);
let _ = (self.procs.unlock_objects)(self.dx_device, 1, objects.as_ptr());
return false;
}
(self.procs.bind_framebuffer)(GL_READ_FRAMEBUFFER, 0);
let w = self.width as i32;
let h = self.height as i32;
(self.procs.blit_framebuffer)(
0,
0,
w,
h,
0,
h,
w,
0,
GL_COLOR_BUFFER_BIT,
if w == self.width as i32 && h == self.height as i32 {
GL_NEAREST
} else {
GL_LINEAR
},
);
let blit_err = glGetError();
(self.procs.framebuffer_texture_2d)(
GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
0,
0,
);
(self.procs.bind_framebuffer)(GL_DRAW_FRAMEBUFFER, prev_draw_fbo as u32);
(self.procs.bind_framebuffer)(GL_READ_FRAMEBUFFER, prev_read_fbo as u32);
glBindTexture(GL_TEXTURE_2D, prev_tex as u32);
glFinish();
if (self.procs.unlock_objects)(self.dx_device, 1, objects.as_ptr()) == 0 {
verbose_log("opengl interop: wglDXUnlockObjectsNV FAILED");
return false;
}
self._context.Flush();
let present_result = {
let _guard = DummyPresentGuard::enter();
self.swap_chain.Present(0, DXGI_PRESENT(0))
};
if present_result.is_err() {
verbose_log(&format!(
"opengl interop: dummy D3D11 Present flush failed hr={:#010x}",
present_result.0 as u32
));
return false;
}
if blit_err != GL_NO_ERROR {
verbose_log(&format!(
"opengl interop: glBlitFramebuffer raised GL error 0x{blit_err:04X}"
));
return false;
}
true
}
}
impl Drop for GlInteropState {
fn drop(&mut self) {
unsafe {
if !self.dx_object.is_null() {
let _ = (self.procs.unregister_object)(self.dx_device, self.dx_object);
}
if self.draw_fbo != 0 {
(self.procs.delete_framebuffers)(1, &self.draw_fbo);
}
if self.gl_texture != 0 {
glDeleteTextures(1, &self.gl_texture);
}
if !self.dx_device.is_null() {
let _ = (self.procs.close_device)(self.dx_device);
}
if !self.dummy_hwnd.is_null() {
let _ = DestroyWindow(self.dummy_hwnd);
}
}
}
}
unsafe fn interop_state_for_frame(
state: &mut HookState,
width: u32,
height: u32,
) -> Option<&mut GlInteropState> {
let recreate = state
.gl_interop
.as_ref()
.map(|interop| !interop.matches(width, height))
.unwrap_or(true);
if recreate {
state.gl_interop = None;
match GlInteropState::create(width, height) {
Some(interop) => state.gl_interop = Some(interop),
None => return None,
}
}
state.gl_interop.as_mut()
}
pub(crate) unsafe fn capture_opengl_frame_gpu(
state: &mut HookState,
hwnd: SysHwnd,
width: u32,
height: u32,
) -> bool {
if gpu_path_disabled() {
return false;
}
let Some(interop) = interop_state_for_frame(state, width, height) else {
if !GL_GPU_UNAVAILABLE_LOGGED.swap(true, Ordering::AcqRel) {
verbose_log(
"opengl interop: WGL_NV_DX_interop2 unavailable or pipeline creation failed",
);
}
latch_disable("interop pipeline creation failed");
set_fallback_reason(state, GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED);
return false;
};
let shared_handle = interop.shared_handle;
let blitted = interop.blit_default_framebuffer();
if !blitted {
latch_disable("lock/blit failed after successful registration");
set_fallback_reason(state, GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED);
return false;
}
mark_present(state, GAME_CAPTURE_API_OPENGL);
set_capture_flags(state, 0);
set_fallback_reason(state, GAME_CAPTURE_FALLBACK_NONE);
let published = publish_shared_texture_frame(
state,
hwnd,
width,
height,
DXGI_FORMAT(DXGI_FORMAT_B8G8R8A8_UNORM.0),
shared_handle,
);
if published {
verbose_log(&format!(
"opengl interop: published shared-texture frame {width}x{height} (handle 0x{shared_handle:X})"
));
}
published
}
@@ -0,0 +1,262 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
#[cfg(not(target_arch = "aarch64"))]
use retour::Function;
#[cfg(target_arch = "aarch64")]
pub(crate) use aarch64_function::Function;
#[cfg(target_arch = "aarch64")]
mod aarch64_function {
pub(crate) trait Function: Copy + Sync + 'static {
unsafe fn from_ptr(ptr: *const ()) -> Self;
fn to_ptr(&self) -> *const ();
}
macro_rules! impl_function {
($($arg:ident),*) => {
impl<Ret: 'static, $($arg: 'static),*> Function
for unsafe extern "system" fn($($arg),*) -> Ret
{
unsafe fn from_ptr(ptr: *const ()) -> Self {
core::mem::transmute(ptr)
}
fn to_ptr(&self) -> *const () {
*self as *const ()
}
}
};
}
impl_function!();
impl_function!(A);
impl_function!(A, B);
impl_function!(A, B, C);
impl_function!(A, B, C, D);
impl_function!(A, B, C, D, E);
impl_function!(A, B, C, D, E, F);
}
pub(crate) struct Detour<T: Function> {
inner: Inner<T>,
}
#[cfg(not(target_arch = "aarch64"))]
enum Inner<T: Function> {
Retour(retour::GenericDetour<T>),
}
#[cfg(target_arch = "aarch64")]
enum Inner<T: Function> {
Aarch64(aarch64::Aarch64Detour<T>),
}
impl<T: Function> Detour<T> {
pub(crate) unsafe fn new(target: T, detour: T) -> Result<Self, ()> {
#[cfg(not(target_arch = "aarch64"))]
{
match retour::GenericDetour::<T>::new(target, detour) {
Ok(detour) => Ok(Self {
inner: Inner::Retour(detour),
}),
Err(_) => Err(()),
}
}
#[cfg(target_arch = "aarch64")]
{
aarch64::Aarch64Detour::<T>::new(target, detour).map(|detour| Self {
inner: Inner::Aarch64(detour),
})
}
}
pub(crate) unsafe fn enable(&self) -> Result<(), ()> {
match &self.inner {
#[cfg(not(target_arch = "aarch64"))]
Inner::Retour(detour) => detour.enable().map_err(|_| ()),
#[cfg(target_arch = "aarch64")]
Inner::Aarch64(detour) => detour.enable(),
}
}
pub(crate) fn trampoline_fn(&self) -> T {
match &self.inner {
#[cfg(not(target_arch = "aarch64"))]
Inner::Retour(detour) => unsafe {
T::from_ptr(detour.trampoline() as *const () as *const ())
},
#[cfg(target_arch = "aarch64")]
Inner::Aarch64(detour) => detour.trampoline_fn(),
}
}
}
#[cfg(target_arch = "aarch64")]
mod aarch64 {
use super::Function;
use crate::arm64_reloc::{
NOP, STOLEN_BYTES, append_abs_branch, assemble_trampoline, import_thunk_target,
};
use core::marker::PhantomData;
use std::ptr;
use windows_sys::Win32::System::{
Diagnostics::Debug::FlushInstructionCache,
Memory::{
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE,
PAGE_PROTECTION_FLAGS, VirtualAlloc, VirtualFree, VirtualProtect,
},
Threading::GetCurrentProcess,
};
const TRAMPOLINE_CAP: usize = 256;
pub(super) struct Aarch64Detour<T: Function> {
target: *mut u8,
detour: *const u8,
trampoline: *mut u8,
original_prologue: [u8; STOLEN_BYTES],
enabled: std::cell::Cell<bool>,
_marker: PhantomData<T>,
}
unsafe impl<T: Function> Send for Aarch64Detour<T> {}
unsafe impl<T: Function> Sync for Aarch64Detour<T> {}
impl<T: Function> Aarch64Detour<T> {
pub(super) unsafe fn new(target: T, detour: T) -> Result<Self, ()> {
let target_ptr = target.to_ptr() as *mut u8;
let detour_ptr = detour.to_ptr() as *const u8;
if target_ptr.is_null() || detour_ptr.is_null() {
return Err(());
}
let mut original = [0u8; STOLEN_BYTES];
ptr::copy_nonoverlapping(target_ptr, original.as_mut_ptr(), STOLEN_BYTES);
let trampoline = VirtualAlloc(
ptr::null(),
TRAMPOLINE_CAP,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE,
) as *mut u8;
if trampoline.is_null() {
return Err(());
}
let trampoline_addr = trampoline as u64;
let resume = target_ptr as u64 + STOLEN_BYTES as u64;
let body =
match assemble_trampoline(&original, target_ptr as u64, trampoline_addr, resume) {
Some(body) => body,
None => match import_thunk_target(&original, target_ptr as u64) {
Some(target) => {
let mut body = Vec::new();
append_abs_branch(&mut body, target, false);
body
}
None => {
VirtualFree(trampoline.cast(), 0, MEM_RELEASE);
return Err(());
}
},
};
if body.len() > TRAMPOLINE_CAP {
VirtualFree(trampoline.cast(), 0, MEM_RELEASE);
return Err(());
}
ptr::copy_nonoverlapping(body.as_ptr(), trampoline, body.len());
let mut old = 0 as PAGE_PROTECTION_FLAGS;
VirtualProtect(
trampoline.cast(),
TRAMPOLINE_CAP,
PAGE_EXECUTE_READ,
&mut old,
);
FlushInstructionCache(GetCurrentProcess(), trampoline.cast(), TRAMPOLINE_CAP);
Ok(Self {
target: target_ptr,
detour: detour_ptr,
trampoline,
original_prologue: original,
enabled: std::cell::Cell::new(false),
_marker: PhantomData,
})
}
pub(super) unsafe fn enable(&self) -> Result<(), ()> {
if self.enabled.get() {
return Ok(());
}
let mut patch = Vec::new();
append_abs_branch(&mut patch, self.detour as u64, false);
if patch.len() > STOLEN_BYTES {
return Err(());
}
while patch.len() < STOLEN_BYTES {
patch.extend_from_slice(&NOP.to_le_bytes());
}
let mut old = 0 as PAGE_PROTECTION_FLAGS;
if VirtualProtect(
self.target.cast(),
STOLEN_BYTES,
PAGE_EXECUTE_READWRITE,
&mut old,
) == 0
{
return Err(());
}
ptr::copy_nonoverlapping(patch.as_ptr(), self.target, STOLEN_BYTES);
let mut restore = 0 as PAGE_PROTECTION_FLAGS;
VirtualProtect(self.target.cast(), STOLEN_BYTES, old, &mut restore);
FlushInstructionCache(GetCurrentProcess(), self.target.cast(), STOLEN_BYTES);
self.enabled.set(true);
Ok(())
}
unsafe fn disable(&self) {
if !self.enabled.get() {
return;
}
let mut old = 0 as PAGE_PROTECTION_FLAGS;
if VirtualProtect(
self.target.cast(),
STOLEN_BYTES,
PAGE_EXECUTE_READWRITE,
&mut old,
) != 0
{
ptr::copy_nonoverlapping(
self.original_prologue.as_ptr(),
self.target,
STOLEN_BYTES,
);
let mut restore = 0 as PAGE_PROTECTION_FLAGS;
VirtualProtect(self.target.cast(), STOLEN_BYTES, old, &mut restore);
FlushInstructionCache(GetCurrentProcess(), self.target.cast(), STOLEN_BYTES);
}
self.enabled.set(false);
}
pub(super) fn trampoline_fn(&self) -> T {
unsafe { T::from_ptr(self.trampoline as *const ()) }
}
}
impl<T: Function> Drop for Aarch64Detour<T> {
fn drop(&mut self) {
unsafe {
self.disable();
if !self.trampoline.is_null() {
VirtualFree(self.trampoline.cast(), 0, MEM_RELEASE);
self.trampoline = ptr::null_mut();
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,412 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[path = "../src/arm64_reloc.rs"]
mod arm64_reloc;
use arm64_reloc::*;
fn adr(rd: u32, imm21: i32) -> u32 {
let raw = (imm21 as u32) & 0x1F_FFFF;
let immlo = (raw & 0x3) << 29;
let immhi = ((raw >> 2) & 0x7FFFF) << 5;
0x1000_0000 | immlo | immhi | (rd & 0x1F)
}
fn adrp(rd: u32, imm21: i32) -> u32 {
let raw = (imm21 as u32) & 0x1F_FFFF;
let immlo = (raw & 0x3) << 29;
let immhi = ((raw >> 2) & 0x7FFFF) << 5;
0x9000_0000 | immlo | immhi | (rd & 0x1F)
}
fn b(off_words: i32) -> u32 {
0x1400_0000 | ((off_words as u32) & 0x03FF_FFFF)
}
fn bl(off_words: i32) -> u32 {
0x9400_0000 | ((off_words as u32) & 0x03FF_FFFF)
}
fn bcond(cond: u32, off_words: i32) -> u32 {
0x5400_0000 | (((off_words as u32) & 0x7FFFF) << 5) | (cond & 0xF)
}
fn cbz(rt: u32, off_words: i32) -> u32 {
0xB400_0000 | (((off_words as u32) & 0x7FFFF) << 5) | (rt & 0x1F)
}
fn tbz(rt: u32, bit: u32, off_words: i32) -> u32 {
let b5 = (bit & 0x20) << (31 - 5);
let b40 = (bit & 0x1F) << 19;
0x3600_0000 | b5 | b40 | (((off_words as u32) & 0x3FFF) << 5) | (rt & 0x1F)
}
fn ldr_lit(rt: u32, off_words: i32) -> u32 {
0x5800_0000 | (((off_words as u32) & 0x7FFFF) << 5) | (rt & 0x1F)
}
fn adr_target(insn: u32, pc: u64, page: bool) -> u64 {
let immlo = ((insn >> 29) & 0x3) as i64;
let immhi = ((insn >> 5) & 0x7FFFF) as i64;
let raw = (immhi << 2) | immlo;
let imm21 = (raw << 43) >> 43;
if page {
((pc & !0xFFF) as i64 + imm21 * 4096) as u64
} else {
(pc as i64 + imm21) as u64
}
}
fn imm19_target(insn: u32, pc: u64) -> u64 {
let imm19 = ((insn >> 5) & 0x7FFFF) as i64;
let off = ((imm19 << 45) >> 45) * 4;
(pc as i64 + off) as u64
}
fn imm14_target(insn: u32, pc: u64) -> u64 {
let imm14 = ((insn >> 5) & 0x3FFF) as i64;
let off = ((imm14 << 50) >> 50) * 4;
(pc as i64 + off) as u64
}
#[test]
fn non_pc_relative_copied_verbatim() {
let stp = 0xA9BF_7BFD;
assert_eq!(relocate_instruction(stp, 0x1000, 0x9000), Some(stp));
let mov = 0xAA01_03E0;
assert_eq!(relocate_instruction(mov, 0x1000, 0x9000), Some(mov));
let sub = 0xD100_83FF;
assert_eq!(relocate_instruction(sub, 0x1000, 0x9000), Some(sub));
let mov_fp_sp = 0x9100_03FD;
assert_eq!(
relocate_instruction(mov_fp_sp, 0x1000, 0x9000),
Some(mov_fp_sp)
);
}
#[test]
fn adr_relocates_to_same_target() {
let src_pc = 0x140_0010_0000u64;
let dst_pc = 0x140_0010_8000u64;
let insn = adr(0, 0x4000);
let original = adr_target(insn, src_pc, false);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(adr_target(reloc, dst_pc, false), original);
assert_eq!(reloc & 0x1F, 0);
}
#[test]
fn adr_negative_offset() {
let src_pc = 0x140_0010_0000u64;
let dst_pc = 0x140_0010_0010u64;
let insn = adr(5, -0x100);
let original = adr_target(insn, src_pc, false);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(adr_target(reloc, dst_pc, false), original);
assert_eq!(reloc & 0x1F, 5);
}
#[test]
fn adr_out_of_range_refused() {
let src_pc = 0x0000_0000_0000u64;
let dst_pc = 0x0000_0080_0000u64;
let insn = adr(0, 0x1000);
assert_eq!(relocate_instruction(insn, src_pc, dst_pc), None);
}
#[test]
fn adrp_relocates_to_same_page() {
let src_pc = 0x140_0010_0000u64;
let dst_pc = 0x140_0030_0000u64;
let insn = adrp(9, 0x10);
let original = adr_target(insn, src_pc, true);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(adr_target(reloc, dst_pc, true), original);
assert_eq!(reloc & 0x1F, 9);
assert_eq!(reloc & 0x9F00_0000, 0x9000_0000);
}
#[test]
fn adrp_negative() {
let src_pc = 0x140_0090_0000u64;
let dst_pc = 0x140_0050_0000u64;
let insn = adrp(1, -0x20);
let original = adr_target(insn, src_pc, true);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(adr_target(reloc, dst_pc, true), original);
}
#[test]
fn bcond_relocates() {
let src_pc = 0x10_0000u64;
let dst_pc = 0x12_0000u64;
let insn = bcond(0x0, 0x40);
let original = imm19_target(insn, src_pc);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(imm19_target(reloc, dst_pc), original);
assert_eq!(reloc & 0xF, 0x0);
}
#[test]
fn bcond_out_of_range_refused() {
let src_pc = 0x0u64;
let dst_pc = 0x20_0000u64;
let insn = bcond(0x1, 0x10);
assert_eq!(relocate_instruction(insn, src_pc, dst_pc), None);
}
#[test]
fn cbz_relocates() {
let src_pc = 0x10_0000u64;
let dst_pc = 0x10_8000u64;
let insn = cbz(3, -0x20);
let original = imm19_target(insn, src_pc);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(imm19_target(reloc, dst_pc), original);
assert_eq!(reloc & 0x1F, 3);
assert_eq!(reloc & 0x8000_0000, 0x8000_0000);
}
#[test]
fn tbz_relocates() {
let src_pc = 0x10_0000u64;
let dst_pc = 0x10_1000u64;
let insn = tbz(7, 5, 0x10);
let original = imm14_target(insn, src_pc);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(imm14_target(reloc, dst_pc), original);
assert_eq!(reloc & 0x1F, 7);
}
#[test]
fn tbz_out_of_range_refused() {
let src_pc = 0x0u64;
let dst_pc = 0x1_0000u64;
let insn = tbz(0, 1, 0x8);
assert_eq!(relocate_instruction(insn, src_pc, dst_pc), None);
}
#[test]
fn ldr_literal_relocates() {
let src_pc = 0x20_0000u64;
let dst_pc = 0x20_4000u64;
let insn = ldr_lit(2, 0x100);
let original = imm19_target(insn, src_pc);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(imm19_target(reloc, dst_pc), original);
assert_eq!(reloc & 0x1F, 2);
}
#[test]
fn direct_b_relocation_in_range() {
let src_pc = 0x10_0000u64;
let dst_pc = 0x14_0000u64;
let insn = b(0x100);
let target = branch_target(insn, src_pc).unwrap();
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(branch_target(reloc, dst_pc).unwrap(), target);
}
#[test]
fn branch_target_decode() {
let pc = 0x10_0000u64;
assert_eq!(branch_target(b(4), pc), Some(pc + 16));
assert_eq!(branch_target(b(-4), pc), Some(pc - 16));
assert_eq!(branch_target(bl(1), pc), Some(pc + 4));
}
#[test]
fn abs_branch_encoding() {
let mut bytes = Vec::new();
append_abs_branch(&mut bytes, 0x1234_5678_9ABC_DEF0, false);
assert_eq!(bytes.len(), 16);
assert_eq!(
u32::from_le_bytes(bytes[0..4].try_into().unwrap()),
LDR_X16_PC8
);
assert_eq!(u32::from_le_bytes(bytes[4..8].try_into().unwrap()), BR_X16);
assert_eq!(
u64::from_le_bytes(bytes[8..16].try_into().unwrap()),
0x1234_5678_9ABC_DEF0
);
let mut linked = Vec::new();
append_abs_branch(&mut linked, 0xDEAD_BEEF, true);
assert_eq!(
u32::from_le_bytes(linked[4..8].try_into().unwrap()),
BLR_X16
);
}
#[test]
fn classify_branches() {
assert!(is_b(b(1)));
assert!(!is_bl(b(1)));
assert!(is_bl(bl(1)));
assert!(!is_b(bl(1)));
assert!(needs_absolute_island(b(1)));
assert!(needs_absolute_island(bl(1)));
assert!(!needs_absolute_island(adr(0, 1)));
assert!(!needs_absolute_island(NOP));
}
#[test]
fn assemble_trampoline_relocates_prologue() {
let stp = 0xA9BF_7BFDu32;
let mov = 0x9100_03FDu32;
let adrp_insn = adrp(8, 0x20);
let mut prologue = Vec::new();
for insn in [stp, mov, adrp_insn, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x140_0010_0000u64;
let dst_base = 0x140_0030_0000u64;
let resume = src_base + STOLEN_BYTES as u64;
let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("relocatable");
assert_eq!(body.len(), 32);
assert_eq!(u32::from_le_bytes(body[0..4].try_into().unwrap()), stp);
assert_eq!(u32::from_le_bytes(body[4..8].try_into().unwrap()), mov);
assert_eq!(u32::from_le_bytes(body[12..16].try_into().unwrap()), NOP);
let orig_target = adr_target(adrp_insn, src_base + 8, true);
let reloc_adrp = u32::from_le_bytes(body[8..12].try_into().unwrap());
assert_eq!(adr_target(reloc_adrp, dst_base + 8, true), orig_target);
assert_eq!(
u32::from_le_bytes(body[16..20].try_into().unwrap()),
LDR_X16_PC8
);
assert_eq!(u32::from_le_bytes(body[20..24].try_into().unwrap()), BR_X16);
assert_eq!(u64::from_le_bytes(body[24..32].try_into().unwrap()), resume);
}
#[test]
fn assemble_trampoline_promotes_leading_branch() {
let lead_b = b(0x4000);
let mut prologue = Vec::new();
for insn in [lead_b, NOP, NOP, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x140_0010_0000u64;
let dst_base = 0x0000_7000_0000u64;
let resume = src_base + STOLEN_BYTES as u64;
let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("island path");
assert_eq!(body.len(), 48);
let first = u32::from_le_bytes(body[0..4].try_into().unwrap());
assert!(is_b(first));
let island_addr = branch_target(first, dst_base).unwrap();
assert_eq!(island_addr, dst_base + 16 + 16);
let original_b_target = branch_target(lead_b, src_base).unwrap();
assert_eq!(
u32::from_le_bytes(body[32..36].try_into().unwrap()),
LDR_X16_PC8
);
assert_eq!(u32::from_le_bytes(body[36..40].try_into().unwrap()), BR_X16);
assert_eq!(
u64::from_le_bytes(body[40..48].try_into().unwrap()),
original_b_target
);
}
#[test]
fn assemble_trampoline_promotes_leading_bl_with_blr() {
let lead_bl = bl(0x100);
let mut prologue = Vec::new();
for insn in [lead_bl, NOP, NOP, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x140_0010_0000u64;
let dst_base = 0x0000_7000_0000u64;
let resume = src_base + STOLEN_BYTES as u64;
let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("island path");
assert_eq!(body.len(), 48);
let first = u32::from_le_bytes(body[0..4].try_into().unwrap());
assert!(is_bl(first));
let original = branch_target(lead_bl, src_base).unwrap();
assert_eq!(
u32::from_le_bytes(body[36..40].try_into().unwrap()),
BLR_X16
);
assert_eq!(
u64::from_le_bytes(body[40..48].try_into().unwrap()),
original
);
}
#[test]
fn assemble_trampoline_two_islands() {
let b0 = b(0x10);
let bl1 = bl(0x20);
let mut prologue = Vec::new();
for insn in [b0, NOP, bl1, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x140_0010_0000u64;
let dst_base = 0x0000_7000_0000u64;
let resume = src_base + STOLEN_BYTES as u64;
let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("islands");
assert_eq!(body.len(), 64);
let first = u32::from_le_bytes(body[0..4].try_into().unwrap());
let third = u32::from_le_bytes(body[8..12].try_into().unwrap());
let island0 = branch_target(first, dst_base).unwrap();
let island1 = branch_target(third, dst_base + 8).unwrap();
assert_eq!(island0, dst_base + 32);
assert_eq!(island1, dst_base + 48);
assert_eq!(
u64::from_le_bytes(body[40..48].try_into().unwrap()),
branch_target(b0, src_base).unwrap()
);
assert_eq!(
u64::from_le_bytes(body[56..64].try_into().unwrap()),
branch_target(bl1, src_base + 8).unwrap()
);
}
#[test]
fn assemble_trampoline_refuses_unrelocatable_narrow_branch() {
let cond = bcond(0x2, 0x10);
let mut prologue = Vec::new();
for insn in [NOP, cond, NOP, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x0u64;
let dst_base = 0x0000_0080_0000u64;
let resume = src_base + STOLEN_BYTES as u64;
assert_eq!(
assemble_trampoline(&prologue, src_base, dst_base, resume),
None
);
}
#[test]
fn relocated_prologue_matches_assemble_prefix() {
let stp = 0xA9BF_7BFDu32;
let mut prologue = Vec::new();
for insn in [stp, NOP, NOP, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x140_0010_0000u64;
let dst_base = 0x140_0030_0000u64;
let pro = relocated_prologue(&prologue, src_base, dst_base).expect("ok");
assert_eq!(pro.len(), 16);
assert_eq!(u32::from_le_bytes(pro[0..4].try_into().unwrap()), stp);
}
#[test]
fn island_holds_absolute_branch_to_original_target() {
let lead_bl = bl(0x100);
let src_base = 0x140_0010_0000u64;
let original_target = branch_target(lead_bl, src_base).unwrap();
let island = island_for_branch(lead_bl, src_base).expect("island");
assert_eq!(island.len(), 16);
assert_eq!(
u32::from_le_bytes(island[4..8].try_into().unwrap()),
BLR_X16
);
assert_eq!(
u64::from_le_bytes(island[8..16].try_into().unwrap()),
original_target
);
}
#[test]
fn stolen_bytes_is_four_instructions() {
assert_eq!(STOLEN_BYTES, 16);
assert_eq!(STOLEN_BYTES % 4, 0);
}
#[test]
fn encode_imm26_rejects_unaligned_and_overflow() {
assert_eq!(encode_imm26(3), None);
assert_eq!(encode_imm26(4), Some(1));
assert_eq!(encode_imm26(-4), Some((-1i32 as u32) & 0x03FF_FFFF));
assert_eq!(encode_imm26(1 << 27), None);
}
+169
View File
@@ -0,0 +1,169 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {EventEmitter} from 'node:events';
export type GameCaptureInjectionMethod = 'auto' | 'remote-thread' | 'set-windows-hook';
export type CaptureStrategyName = 'game-hook' | 'wgc' | 'dxgi-duplication' | 'window-gdi';
export interface ScreenCaptureRect {
x: number;
y: number;
width: number;
height: number;
}
export interface ScreenCaptureOptions {
sourceId: string;
sourceKind: 'screen' | 'window' | 'game';
width?: number;
height?: number;
frameRate?: number;
hookDllPath?: string;
hookDllPathX86?: string;
injectionMethod?: GameCaptureInjectionMethod;
captureId?: string;
colorRange?: 'full' | 'limited';
colorSpace?: 'rec709' | 'srgb';
showCursorClicks?: boolean;
captureRect?: ScreenCaptureRect;
frameSinkHandle?: unknown;
nativeFrameSinkRequired?: boolean;
}
export interface ScreenCaptureStartResult {
width: number;
height: number;
frameRate: number;
pixelFormat: 'nv12' | 'bgra';
}
export interface ScreenCaptureSourceDescriptor {
kind: 'screen' | 'window' | 'game';
id: string;
name: string;
width: number;
height: number;
targetPid?: number;
}
export interface AvailabilityInfo {
available: boolean;
backend: string;
reason?: string;
}
export interface CaptureDiagnostics {
state: number;
apiType: number;
transport: number;
fallbackReason: number;
captureFlags: number;
width: number;
height: number;
dxgiFormat: number;
frameCounter: number;
droppedFrameCounter: number;
lastPresentTimestampUs: number;
lastError: number;
requestedInjectionMethod: GameCaptureInjectionMethod;
injectionMethod: 'remote-thread' | 'set-windows-hook';
activeStrategy: CaptureStrategyName;
lastFallbackReason: string;
startOptions: ScreenCaptureStartOptionsDiagnostics;
frameSinkAccepted: number;
frameSinkCoalesced: number;
frameSinkRejected: number;
mediaFramesDroppedWithoutSink: number;
cpuFallbackFramesDropped: number;
}
export interface ScreenCaptureStartOptionsDiagnostics {
colorRange?: 'full' | 'limited';
colorSpace?: 'rec709' | 'srgb';
showCursorClicks?: boolean;
captureRect?: ScreenCaptureRect;
unsupportedOptions: Array<'showCursorClicks' | 'captureRect' | 'colorRange' | 'colorSpace'>;
}
export interface SharedTextureHandleInfo {
handle: bigint;
width: number;
height: number;
dxgiFormat: number;
timestampUs: number;
}
export interface EncoderAttachDiagnostics {
attached: boolean;
width: number;
height: number;
capacity: number;
framesSubmitted: number;
framesDropped: number;
ringFullEvents: number;
failedBlits: number;
}
export interface FrameSinkDiagnostics {
accepted: number;
coalesced: number;
rejected: number;
mediaFramesDroppedWithoutSink: number;
cpuFallbackFramesDropped: number;
}
export interface VulkanLayerRegistrationState {
registered: boolean;
manifestExists: boolean;
dllExists: boolean;
manifestPath: string | null;
}
export declare interface ScreenCapture {
on(event: 'error', listener: (err: Error) => void): this;
on(event: 'closed', listener: () => void): this;
on(event: 'stalled', listener: (message?: string) => void): this;
on(event: 'diagnostic', listener: (message?: string) => void): this;
on(event: string | symbol, listener: (...args: Array<unknown>) => void): this;
off(event: 'error', listener: (err: Error) => void): this;
off(event: 'closed', listener: () => void): this;
off(event: 'stalled', listener: (message?: string) => void): this;
off(event: 'diagnostic', listener: (message?: string) => void): this;
off(event: string | symbol, listener: (...args: Array<unknown>) => void): this;
emit(event: 'error', err: Error): boolean;
emit(event: 'closed'): boolean;
emit(event: 'stalled', message?: string): boolean;
emit(event: 'diagnostic', message?: string): boolean;
}
export declare class ScreenCapture extends EventEmitter {
constructor(options?: ScreenCaptureOptions);
start(): Promise<ScreenCaptureStartResult | undefined>;
stop(): Promise<void>;
getDiagnostics(): CaptureDiagnostics | null;
getSharedTextureHandle(): SharedTextureHandleInfo | null;
attachEncoder(width: number, height: number, frameRate?: number): void;
detachEncoder(): void;
isEncoderAttached(): boolean;
encoderRingFullCount(): number;
getEncoderAttachDiagnostics(): EncoderAttachDiagnostics | null;
getFrameSinkDiagnostics(): FrameSinkDiagnostics;
}
export declare function isSupported(): boolean;
export declare function getAvailability(): AvailabilityInfo;
export declare function listSources(): Promise<Array<ScreenCaptureSourceDescriptor>>;
export declare function resolveGameHookPath(): string | null;
export declare function resolveGameHookPathX86(): string | null;
export declare function resolveVulkanLayerManifestPath(): string | null;
export declare function registerVulkanLayerManifest(): boolean;
export declare function unregisterVulkanLayerManifest(): boolean;
export declare function getVulkanLayerRegistrationState(): VulkanLayerRegistrationState;
export declare function parseFallbackRecommendation(message: string | undefined): CaptureStrategyName | 'none' | null;
export declare function elevateGpuSchedulingPriority(processId?: number, priorityClass?: 'high' | 'realtime'): boolean;
export declare function restoreGpuSchedulingPriority(processId?: number): boolean;
export declare function __setBindingForTests(binding: unknown): void;
export declare const loadError: Error | null;
@@ -0,0 +1,469 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const {EventEmitter} = require('node:events');
const {existsSync} = require('node:fs');
const {join, sep} = require('node:path');
const {createNativeLoadError, loadNativeBinding} = require('./loader-diagnostics.cjs');
const MODULE_NAME = '@fluxer/win-game-capture';
const WINDOWS_GAME_CAPTURE_MODULE_ENV = 'FLUXER_WINDOWS_GAME_CAPTURE_MODULE_ENABLED';
function resolveNativeRoot() {
const asarSegment = `${sep}app.asar${sep}`;
if (!__dirname.includes(asarSegment)) return __dirname;
const unpackedDir = __dirname.replace(asarSegment, `${sep}app.asar.unpacked${sep}`);
return existsSync(unpackedDir) ? unpackedDir : __dirname;
}
function nativeFileName(arch) {
switch (arch) {
case 'x64':
return 'win-game-capture.win32-x64-msvc.node';
case 'arm64':
return 'win-game-capture.win32-arm64-msvc.node';
default:
return null;
}
}
let binding = null;
let loadError = null;
const nativeRoot = resolveNativeRoot();
if (process.platform === 'win32' && process.env[WINDOWS_GAME_CAPTURE_MODULE_ENV] !== 'true') {
loadError = createNativeLoadError({
moduleName: MODULE_NAME,
nativeRoot,
packageDir: __dirname,
reason: 'Windows game capture is disabled in this build',
});
} else if (process.platform === 'win32') {
const fileName = nativeFileName(process.arch);
if (!fileName) {
loadError = createNativeLoadError({
moduleName: MODULE_NAME,
nativeRoot,
packageDir: __dirname,
reason: `unsupported Windows architecture: ${process.arch}`,
});
} else {
const nativePath = join(nativeRoot, fileName);
const loaded = loadNativeBinding({
moduleName: MODULE_NAME,
nativePath,
nativeRoot,
packageDir: __dirname,
probe: false,
});
binding = loaded.binding;
loadError = loaded.loadError;
}
} else {
loadError = createNativeLoadError({
moduleName: MODULE_NAME,
nativeRoot,
packageDir: __dirname,
reason: `not supported on platform ${process.platform}`,
});
}
function gameHookFileName(arch) {
switch (arch) {
case 'x64':
return 'fluxer-game-hook.win32-x64-msvc.dll';
case 'ia32':
return 'fluxer-game-hook.win32-ia32-msvc.dll';
case 'arm64':
return 'fluxer-game-hook.win32-arm64-msvc.dll';
default:
return null;
}
}
function resolveGameHookPathForArch(arch, root = nativeRoot) {
if (process.platform !== 'win32') return null;
const fileName = gameHookFileName(arch);
if (!fileName) return null;
const hookPath = join(root, fileName);
return existsSync(hookPath) ? hookPath : null;
}
function resolveGameHookPath(root = nativeRoot) {
return resolveGameHookPathForArch(process.arch, root);
}
function resolveGameHookPathX86(root = nativeRoot) {
return resolveGameHookPathForArch('ia32', root);
}
function vulkanLayerManifestFileName(arch) {
switch (arch) {
case 'x64':
return 'fluxer-vulkan-layer.win32-x64-msvc.json';
case 'ia32':
return 'fluxer-vulkan-layer.win32-ia32-msvc.json';
case 'arm64':
return 'fluxer-vulkan-layer.win32-arm64-msvc.json';
default:
return null;
}
}
function resolveVulkanLayerManifestPath(root = nativeRoot) {
if (process.platform !== 'win32') return null;
const fileName = vulkanLayerManifestFileName(process.arch);
if (!fileName) return null;
const manifestPath = join(root, fileName);
return existsSync(manifestPath) ? manifestPath : null;
}
function registerVulkanLayerManifest(root = nativeRoot) {
if (!binding?.registerVulkanLayerManifest) return false;
const manifestPath = resolveVulkanLayerManifestPath(root);
if (!manifestPath) return false;
binding.registerVulkanLayerManifest(manifestPath);
return true;
}
function unregisterVulkanLayerManifest(root = nativeRoot) {
if (!binding?.unregisterVulkanLayerManifest) return false;
const manifestPath = resolveVulkanLayerManifestPath(root);
if (!manifestPath) return false;
try {
binding.unregisterVulkanLayerManifest(manifestPath);
return true;
} catch (error) {
console.warn('[win-game-capture] unregisterVulkanLayerManifest failed:', error?.message || error);
return false;
}
}
function getVulkanLayerRegistrationState(root = nativeRoot) {
const manifestPath = resolveVulkanLayerManifestPath(root);
if (!binding?.getVulkanLayerRegistrationState) {
return {registered: false, manifestExists: Boolean(manifestPath), dllExists: false, manifestPath};
}
try {
return binding.getVulkanLayerRegistrationState(manifestPath ?? '');
} catch (error) {
console.warn('[win-game-capture] getVulkanLayerRegistrationState failed:', error?.message || error);
return {registered: false, manifestExists: Boolean(manifestPath), dllExists: false, manifestPath};
}
}
if (process.platform === 'win32' && binding) {
try {
registerVulkanLayerManifest();
} catch (error) {
console.warn('[win-game-capture] registerVulkanLayerManifest failed:', error?.message || error);
}
}
class ScreenCapture extends EventEmitter {
constructor(options = {}) {
super();
if (!binding) {
throw loadError || new Error(`${MODULE_NAME} binding unavailable`);
}
this.sourceId = options.sourceId;
this.sourceKind = options.sourceKind ?? 'window';
this.width = options.width ?? 0;
this.height = options.height ?? 0;
this.frameRate = options.frameRate ?? 30;
this.hookDllPath = options.hookDllPath ?? resolveGameHookPath();
this.hookDllPathX86 = options.hookDllPathX86 ?? resolveGameHookPathX86();
this.injectionMethod = options.injectionMethod ?? undefined;
this.captureId = typeof options.captureId === 'string' ? options.captureId : undefined;
this.colorRange = options.colorRange;
this.colorSpace = options.colorSpace;
this.showCursorClicks = options.showCursorClicks === true;
this.captureRect = options.captureRect;
this.frameSinkHandle = options.frameSinkHandle;
this.nativeFrameSinkRequired = options.nativeFrameSinkRequired === true;
this.started = false;
this.stopped = false;
this.closedEmitted = false;
this.native = new binding.ScreenCapture();
this.native.setLifecycleCallback((...lifecycleArgs) => {
const [type, message] =
lifecycleArgs.length === 1 && Array.isArray(lifecycleArgs[0]) ? lifecycleArgs[0] : lifecycleArgs;
if (type === 'stalled') {
if (this.stopped) return;
this.emit('stalled', message);
return;
}
if (type === 'diagnostic') {
if (this.stopped) return;
this.emit('diagnostic', message);
return;
}
if (type === 'error') {
this.emit('error', new Error(message || 'DXGI capture error'));
return;
}
if (type === 'closed') {
if (this.stopped) {
this._emitClosedOnce();
return;
}
this.stopped = true;
try {
this.native.stop();
} catch {}
this._emitClosedOnce();
}
});
}
async start() {
if (this.started || this.stopped) return undefined;
if (this.sourceKind === 'game' && !this.hookDllPath) {
throw new Error(`Game capture hook unavailable for ${process.platform}-${process.arch}`);
}
this.started = true;
try {
if (this.frameSinkHandle != null) {
if (typeof this.native.setFrameSinkHandle !== 'function') {
throw new Error(`${MODULE_NAME} native binding does not support native frame sink handles`);
}
this.native.setFrameSinkHandle(this.frameSinkHandle);
} else if (this.nativeFrameSinkRequired) {
throw new Error('Native frame sink handle is required for Windows screen capture');
}
const result = this.native.start(
this.sourceId,
this.sourceKind,
this.width || undefined,
this.height || undefined,
this.frameRate || undefined,
this.sourceKind === 'game' ? this.hookDllPath : undefined,
this.sourceKind === 'game' ? (this.hookDllPathX86 ?? undefined) : undefined,
this.sourceKind === 'game' ? (this.injectionMethod ?? undefined) : undefined,
this.captureId,
{
colorRange: this.colorRange,
colorSpace: this.colorSpace,
showCursorClicks: this.showCursorClicks,
captureRect: this.captureRect,
},
);
return result
? {
width: result.width,
height: result.height,
frameRate: result.frameRate,
pixelFormat: result.pixelFormat,
}
: undefined;
} catch (error) {
this.stopped = true;
this.emit('error', error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async stop() {
if (this.stopped) return;
this.stopped = true;
try {
this.native.stop();
} finally {
this._emitClosedOnce();
}
}
getDiagnostics() {
if (!this.native || typeof this.native.getDiagnostics !== 'function') return null;
try {
return this.native.getDiagnostics() ?? null;
} catch (error) {
console.warn('[win-game-capture] getDiagnostics failed:', error?.message || error);
return null;
}
}
getSharedTextureHandle() {
if (!this.native || typeof this.native.getSharedTextureHandle !== 'function') return null;
try {
return this.native.getSharedTextureHandle() ?? null;
} catch (error) {
console.warn('[win-game-capture] getSharedTextureHandle failed:', error?.message || error);
return null;
}
}
attachEncoder(width, height) {
if (!this.native || typeof this.native.attachEncoder !== 'function') {
throw new Error(`${MODULE_NAME} native binding does not support encoder attachment`);
}
this.native.attachEncoder(width, height);
}
detachEncoder() {
if (!this.native || typeof this.native.detachEncoder !== 'function') return;
this.native.detachEncoder();
}
isEncoderAttached() {
if (!this.native || typeof this.native.isEncoderAttached !== 'function') return false;
return Boolean(this.native.isEncoderAttached());
}
encoderRingFullCount() {
if (!this.native || typeof this.native.encoderRingFullCount !== 'function') return 0;
const count = this.native.encoderRingFullCount();
return Number.isFinite(count) && count > 0 ? Math.floor(count) : 0;
}
getEncoderAttachDiagnostics() {
if (!this.native || typeof this.native.getEncoderAttachDiagnostics !== 'function') return null;
try {
return this.native.getEncoderAttachDiagnostics() ?? null;
} catch (error) {
console.warn('[win-game-capture] getEncoderAttachDiagnostics failed:', error?.message || error);
return null;
}
}
getFrameSinkDiagnostics() {
if (!this.native || typeof this.native.getFrameSinkDiagnostics !== 'function') {
return {
accepted: 0,
coalesced: 0,
rejected: 0,
mediaFramesDroppedWithoutSink: 0,
cpuFallbackFramesDropped: 0,
};
}
try {
return this.native.getFrameSinkDiagnostics();
} catch (error) {
console.warn('[win-game-capture] getFrameSinkDiagnostics failed:', error?.message || error);
return {
accepted: 0,
coalesced: 0,
rejected: 0,
mediaFramesDroppedWithoutSink: 0,
cpuFallbackFramesDropped: 0,
};
}
}
_emitClosedOnce() {
if (this.closedEmitted) return;
this.closedEmitted = true;
queueMicrotask(() => this.emit('closed'));
}
}
const FALLBACK_STRATEGY_NAMES = new Set(['game-hook', 'wgc', 'dxgi-duplication', 'window-gdi', 'none']);
function parseFallbackRecommendation(message) {
if (typeof message !== 'string') return null;
const match = message.match(/\[next-strategy=([a-z-]+)\]/);
if (!match) return null;
const name = match[1];
return FALLBACK_STRATEGY_NAMES.has(name) ? name : null;
}
async function listSources() {
if (!binding || typeof binding.listSources !== 'function') return [];
try {
const sources = await binding.listSources();
if (!Array.isArray(sources)) return [];
return sources
.filter((source) => {
return (
source &&
(source.kind === 'screen' || source.kind === 'window' || source.kind === 'game') &&
typeof source.id === 'string' &&
source.id.length > 0
);
})
.map((source) => ({
kind: source.kind,
id: source.id,
name: typeof source.name === 'string' && source.name.length > 0 ? source.name : source.id,
width: Number.isFinite(source.width) ? Math.max(0, Math.floor(source.width)) : 0,
height: Number.isFinite(source.height) ? Math.max(0, Math.floor(source.height)) : 0,
targetPid: Number.isFinite(source.targetPid) && source.targetPid > 0 ? Math.floor(source.targetPid) : undefined,
}));
} catch (error) {
console.warn('[win-game-capture] listSources failed:', error?.message || error);
return [];
}
}
function isSupported() {
if (!binding) return false;
return binding.isSupported();
}
function getAvailability() {
if (!binding) {
return {available: false, backend: 'windows-game-capture', reason: 'load-failed'};
}
return binding.getAvailability();
}
function normalizeProcessId(processId) {
if (processId === undefined || processId === null) return undefined;
if (Number.isInteger(processId) && processId > 0) return processId;
throw new TypeError(`Invalid process id: ${processId}`);
}
function normalizePriorityClass(priorityClass) {
if (priorityClass === undefined || priorityClass === null) return undefined;
const normalized = String(priorityClass).trim().toLowerCase();
if (normalized === 'real-time') return 'realtime';
if (normalized === 'high' || normalized === 'realtime') return normalized;
throw new TypeError(`Invalid GPU scheduling priority class: ${priorityClass}`);
}
function elevateGpuSchedulingPriority(processId, priorityClass) {
if (!binding) return false;
try {
binding.elevateGpuSchedulingPriority(normalizeProcessId(processId), normalizePriorityClass(priorityClass));
return true;
} catch (error) {
console.warn('[win-game-capture] elevateGpuSchedulingPriority failed:', error?.message || error);
return false;
}
}
function restoreGpuSchedulingPriority(processId) {
if (!binding) return false;
try {
binding.restoreGpuSchedulingPriority(normalizeProcessId(processId));
return true;
} catch (error) {
console.warn('[win-game-capture] restoreGpuSchedulingPriority failed:', error?.message || error);
return false;
}
}
function __setBindingForTests(nextBinding) {
binding = nextBinding;
loadError = null;
}
module.exports = {
isSupported,
getAvailability,
resolveGameHookPath,
resolveGameHookPathX86,
resolveVulkanLayerManifestPath,
registerVulkanLayerManifest,
unregisterVulkanLayerManifest,
getVulkanLayerRegistrationState,
listSources,
ScreenCapture,
parseFallbackRecommendation,
elevateGpuSchedulingPriority,
restoreGpuSchedulingPriority,
__setBindingForTests,
get loadError() {
return loadError;
},
};
@@ -0,0 +1,839 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import {existsSync} from 'node:fs';
import {createRequire} from 'node:module';
import {afterEach, describe, test} from 'node:test';
const require = createRequire(import.meta.url);
const winGameCapture = require('./index.js');
const startupSupported = winGameCapture.isSupported();
const startupAvailability = winGameCapture.getAvailability();
const realBindingSkip = startupSupported
? false
: 'no native binding loaded at startup (binding-less platform / unbuilt addon)';
const hasBindingHook = typeof winGameCapture.__setBindingForTests === 'function';
const injectionSkip = hasBindingHook ? false : 'no __setBindingForTests hook exported';
if (!hasBindingHook) {
console.warn(
'[index.test.mjs] __setBindingForTests not present on index.js; ' +
'skipping binding-injection tests and running only the no-binding tests. ' +
'This should only happen when testing an older or stripped loader.',
);
}
function makeFakeBinding() {
const calls = [];
const frameSinkHandleCalls = [];
const priorityCalls = [];
const natives = [];
const diagnostics = {
state: 1,
apiType: 5,
transport: 0,
fallbackReason: 0,
captureFlags: 0,
width: 1920,
height: 1080,
dxgiFormat: 87,
frameCounter: 42,
droppedFrameCounter: 0,
lastPresentTimestampUs: 123456,
lastError: 0,
requestedInjectionMethod: 'auto',
injectionMethod: 'remote-thread',
activeStrategy: 'game-hook',
lastFallbackReason: '',
startOptions: {
colorRange: 'full',
colorSpace: 'rec709',
showCursorClicks: true,
captureRect: {x: 10, y: 20, width: 300, height: 200},
unsupportedOptions: ['showCursorClicks', 'captureRect', 'colorRange', 'colorSpace'],
},
frameSinkAccepted: 0,
frameSinkCoalesced: 0,
frameSinkRejected: 0,
mediaFramesDroppedWithoutSink: 0,
cpuFallbackFramesDropped: 0,
};
const encoderDiagnostics = {
attached: false,
width: 0,
height: 0,
capacity: 0,
framesSubmitted: 0,
framesDropped: 0,
ringFullEvents: 0,
failedBlits: 0,
};
const frameSinkDiagnostics = {
accepted: 0,
coalesced: 0,
rejected: 0,
mediaFramesDroppedWithoutSink: 0,
cpuFallbackFramesDropped: 0,
};
class FakeNative {
constructor() {
this.lifecycleCallback = undefined;
this.stopCount = 0;
natives.push(this);
}
setLifecycleCallback(callback) {
this.lifecycleCallback = callback;
}
setFrameSinkHandle(handle) {
frameSinkHandleCalls.push(handle);
}
start(
sourceId,
sourceKind,
width,
height,
frameRate,
hookDllPath,
hookDllPathX86,
injectionMethod,
captureId,
captureOptions,
) {
calls.push({
sourceId,
sourceKind,
width,
height,
frameRate,
hookDllPath,
hookDllPathX86,
injectionMethod,
captureId,
captureOptions,
});
return {
width: width || 1920,
height: height || 1080,
frameRate: frameRate || 30,
pixelFormat: 'bgra',
};
}
stop() {
this.stopCount += 1;
}
getDiagnostics() {
return diagnostics;
}
attachEncoder(width, height) {
encoderDiagnostics.attached = true;
encoderDiagnostics.width = width;
encoderDiagnostics.height = height;
encoderDiagnostics.capacity = 8;
}
detachEncoder() {
encoderDiagnostics.attached = false;
}
isEncoderAttached() {
return encoderDiagnostics.attached;
}
encoderRingFullCount() {
return encoderDiagnostics.ringFullEvents;
}
getEncoderAttachDiagnostics() {
if (!encoderDiagnostics.attached) return null;
return {...encoderDiagnostics};
}
getFrameSinkDiagnostics() {
return {...frameSinkDiagnostics};
}
}
return {
binding: {
ScreenCapture: FakeNative,
isSupported: () => true,
getAvailability: () => ({available: true, backend: 'windows-game-capture'}),
listSources: () => [
{
kind: 'screen',
id: 'screen:0:0',
name: 'Display 1',
width: 2560,
height: 1440,
},
{
kind: 'window',
id: 'window:5050:0',
name: 'Fixture',
width: 1280,
height: 720,
targetPid: 4242,
},
{
kind: 'browser',
id: 'screen:1:0',
name: 'unsupported shape',
},
],
elevateGpuSchedulingPriority: (processId, priorityClass) => {
priorityCalls.push({type: 'elevate', processId, priorityClass});
},
restoreGpuSchedulingPriority: (processId) => {
priorityCalls.push({type: 'restore', processId});
},
},
calls,
frameSinkHandleCalls,
priorityCalls,
natives,
diagnostics,
encoderDiagnostics,
frameSinkDiagnostics,
};
}
afterEach(() => {
if (hasBindingHook) {
winGameCapture.__setBindingForTests(null);
}
});
describe('win-game-capture loader wrapper -- binding-absent fallback path', () => {
function forceNoBinding() {
if (hasBindingHook) winGameCapture.__setBindingForTests(null);
}
test(
'isSupported() is false when no binding is loaded',
{skip: hasBindingHook ? false : 'no __setBindingForTests hook'},
() => {
forceNoBinding();
assert.equal(winGameCapture.isSupported(), false);
},
);
test(
'getAvailability() returns the {available:false, ...} shape when no binding is loaded',
{skip: hasBindingHook ? false : 'no __setBindingForTests hook'},
() => {
forceNoBinding();
const availability = winGameCapture.getAvailability();
assert.equal(availability.available, false);
assert.equal(availability.backend, 'windows-game-capture');
assert.equal(typeof availability.reason, 'string');
},
);
test(
'constructing ScreenCapture without a binding throws',
{skip: hasBindingHook ? false : 'no __setBindingForTests hook'},
() => {
forceNoBinding();
assert.throws(() => new winGameCapture.ScreenCapture({sourceId: '1'}));
try {
new winGameCapture.ScreenCapture({sourceId: '1'});
assert.fail('expected ScreenCapture constructor to throw without a binding');
} catch (error) {
assert.ok(error instanceof Error);
}
},
);
});
describe('win-game-capture loader wrapper -- arch path resolvers (platform-portable)', () => {
test('resolveGameHookPath() is null or the host-arch hook path', () => {
const r = winGameCapture.resolveGameHookPath();
assert.ok(
r === null || (typeof r === 'string' && /fluxer-game-hook\.win32-(x64|ia32|arm64)-msvc\.dll$/.test(r)),
`unexpected resolveGameHookPath(): ${r}`,
);
});
test('resolveGameHookPathX86() is null or the ia32 hook path', () => {
const r = winGameCapture.resolveGameHookPathX86();
assert.ok(
r === null || (typeof r === 'string' && r.endsWith('fluxer-game-hook.win32-ia32-msvc.dll')),
`unexpected resolveGameHookPathX86(): ${r}`,
);
});
test('resolveVulkanLayerManifestPath() is null or the host-arch layer manifest path', () => {
const r = winGameCapture.resolveVulkanLayerManifestPath();
assert.ok(
r === null || (typeof r === 'string' && /fluxer-vulkan-layer\.win32-(x64|ia32|arm64)-msvc\.json$/.test(r)),
`unexpected resolveVulkanLayerManifestPath(): ${r}`,
);
});
});
describe('win-game-capture loader wrapper -- real native binding (built Windows box)', () => {
test('getAvailability() reports available with the windows-game-capture backend', {skip: realBindingSkip}, () => {
assert.equal(startupAvailability.available, true);
assert.equal(startupAvailability.backend, 'windows-game-capture');
});
test('resolveGameHookPath() points at an existing host-arch hook DLL', {skip: realBindingSkip}, () => {
const r = winGameCapture.resolveGameHookPath();
assert.equal(typeof r, 'string', 'expected a hook path on a built Windows box');
assert.ok(existsSync(r), `hook DLL should exist on disk: ${r}`);
});
test('resolveVulkanLayerManifestPath() points at an existing layer manifest', {skip: realBindingSkip}, () => {
const r = winGameCapture.resolveVulkanLayerManifestPath();
assert.equal(typeof r, 'string', 'expected a Vulkan layer manifest path on a built Windows box');
assert.ok(existsSync(r), `Vulkan layer manifest should exist on disk: ${r}`);
});
});
describe('win-game-capture loader wrapper -- injected fake binding', () => {
test(
'listSources() forwards sanitized screen/window sources from the native binding',
{skip: injectionSkip},
async () => {
const {binding} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const sources = await winGameCapture.listSources();
assert.deepEqual(sources, [
{
kind: 'screen',
id: 'screen:0:0',
name: 'Display 1',
width: 2560,
height: 1440,
targetPid: undefined,
},
{
kind: 'window',
id: 'window:5050:0',
name: 'Fixture',
width: 1280,
height: 720,
targetPid: 4242,
},
]);
},
);
test(
'start() forwards sourceId/kind/dims/frameRate and BOTH hook paths (6th + 7th args)',
{skip: injectionSkip},
async () => {
const {binding, calls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
sourceId: '987654',
sourceKind: 'game',
width: 1600,
height: 900,
frameRate: 60,
hookDllPath: 'C:/hooks/fluxer-game-hook.win32-x64-msvc.dll',
hookDllPathX86: 'C:/hooks/fluxer-game-hook.win32-ia32-msvc.dll',
captureId: 'capture-1',
colorRange: 'full',
colorSpace: 'rec709',
showCursorClicks: true,
captureRect: {x: 10, y: 20, width: 300, height: 200},
});
capture.on('error', () => {});
const result = await capture.start();
assert.equal(calls.length, 1);
assert.deepEqual(calls[0], {
sourceId: '987654',
sourceKind: 'game',
width: 1600,
height: 900,
frameRate: 60,
hookDllPath: 'C:/hooks/fluxer-game-hook.win32-x64-msvc.dll',
hookDllPathX86: 'C:/hooks/fluxer-game-hook.win32-ia32-msvc.dll',
injectionMethod: undefined,
captureId: 'capture-1',
captureOptions: {
colorRange: 'full',
colorSpace: 'rec709',
showCursorClicks: true,
captureRect: {x: 10, y: 20, width: 300, height: 200},
},
});
assert.equal(result.pixelFormat, 'bgra');
assert.equal(result.width, 1600);
assert.equal(result.height, 900);
assert.equal(result.frameRate, 60);
},
);
test(
'start() forwards the injectionMethod option as the 8th arg for game capture',
{skip: injectionSkip},
async () => {
const {binding, calls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
sourceId: '555',
sourceKind: 'game',
hookDllPath: 'C:/hooks/fluxer-game-hook.win32-x64-msvc.dll',
hookDllPathX86: 'C:/hooks/fluxer-game-hook.win32-ia32-msvc.dll',
injectionMethod: 'set-windows-hook',
});
capture.on('error', () => {});
await capture.start();
assert.equal(calls.length, 1);
assert.equal(calls[0].injectionMethod, 'set-windows-hook');
},
);
test('window sourceKind does not forward the injectionMethod', {skip: injectionSkip}, async () => {
const {binding, calls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
sourceId: '42',
sourceKind: 'window',
injectionMethod: 'set-windows-hook',
});
capture.on('error', () => {});
await capture.start();
assert.equal(calls[0].injectionMethod, undefined);
});
test('window sourceKind does not forward hook paths', {skip: injectionSkip}, async () => {
const {binding, calls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
sourceId: '42',
sourceKind: 'window',
hookDllPath: 'C:/hooks/primary.dll',
hookDllPathX86: 'C:/hooks/x86.dll',
});
capture.on('error', () => {});
await capture.start();
assert.equal(calls[0].sourceKind, 'window');
assert.equal(calls[0].hookDllPath, undefined);
assert.equal(calls[0].hookDllPathX86, undefined);
});
test('screen sourceKind does not forward hook paths or the injectionMethod', {skip: injectionSkip}, async () => {
const {binding, calls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
sourceId: 'screen:0:0',
sourceKind: 'screen',
hookDllPath: 'C:/hooks/primary.dll',
hookDllPathX86: 'C:/hooks/x86.dll',
injectionMethod: 'set-windows-hook',
});
capture.on('error', () => {});
await capture.start();
assert.equal(calls[0].sourceKind, 'screen');
assert.equal(calls[0].hookDllPath, undefined);
assert.equal(calls[0].hookDllPathX86, undefined);
assert.equal(calls[0].injectionMethod, undefined);
});
test('getDiagnostics() exposes native start option state', {skip: injectionSkip}, () => {
const {binding} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '42', sourceKind: 'window'});
const diagnostics = capture.getDiagnostics();
assert.deepEqual(diagnostics.startOptions, {
colorRange: 'full',
colorSpace: 'rec709',
showCursorClicks: true,
captureRect: {x: 10, y: 20, width: 300, height: 200},
unsupportedOptions: ['showCursorClicks', 'captureRect', 'colorRange', 'colorSpace'],
});
});
test('encoder attachment diagnostics are forwarded by the wrapper', {skip: injectionSkip}, () => {
const {binding, encoderDiagnostics} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '42', sourceKind: 'window'});
assert.equal(capture.isEncoderAttached(), false);
assert.equal(capture.encoderRingFullCount(), 0);
assert.equal(capture.getEncoderAttachDiagnostics(), null);
capture.attachEncoder(1280, 720);
encoderDiagnostics.framesSubmitted = 3;
encoderDiagnostics.framesDropped = 1;
encoderDiagnostics.ringFullEvents = 1;
encoderDiagnostics.failedBlits = 0;
assert.equal(capture.isEncoderAttached(), true);
assert.equal(capture.encoderRingFullCount(), 1);
assert.deepEqual(capture.getEncoderAttachDiagnostics(), {
attached: true,
width: 1280,
height: 720,
capacity: 8,
framesSubmitted: 3,
framesDropped: 1,
ringFullEvents: 1,
failedBlits: 0,
});
capture.detachEncoder();
assert.equal(capture.isEncoderAttached(), false);
});
test('frame sink diagnostics are forwarded by the wrapper', {skip: injectionSkip}, () => {
const {binding, frameSinkDiagnostics} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '42', sourceKind: 'window'});
frameSinkDiagnostics.accepted = 7;
frameSinkDiagnostics.coalesced = 2;
frameSinkDiagnostics.rejected = 1;
frameSinkDiagnostics.mediaFramesDroppedWithoutSink = 3;
frameSinkDiagnostics.cpuFallbackFramesDropped = 4;
assert.deepEqual(capture.getFrameSinkDiagnostics(), {
accepted: 7,
coalesced: 2,
rejected: 1,
mediaFramesDroppedWithoutSink: 3,
cpuFallbackFramesDropped: 4,
});
});
test('installs a native frame sink handle once before start', {skip: injectionSkip}, async () => {
const {binding, calls, frameSinkHandleCalls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const frameSinkHandle = {native: true};
const capture = new winGameCapture.ScreenCapture({
sourceId: '42',
sourceKind: 'window',
frameSinkHandle,
nativeFrameSinkRequired: true,
});
capture.on('error', () => {});
await capture.start();
assert.deepEqual(frameSinkHandleCalls, [frameSinkHandle]);
assert.equal(calls.length, 1);
});
test(
'fails before native start when a native frame sink is required but missing',
{skip: injectionSkip},
async () => {
const {binding, calls, frameSinkHandleCalls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
sourceId: '42',
sourceKind: 'window',
nativeFrameSinkRequired: true,
});
capture.on('error', () => {});
await assert.rejects(() => capture.start(), /native frame sink handle is required/i);
assert.deepEqual(frameSinkHandleCalls, []);
assert.deepEqual(calls, []);
},
);
test(
'game sourceKind without a hook path fails closed without WGC/browser fallback',
{skip: injectionSkip},
async () => {
const {binding, calls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
sourceId: '7',
sourceKind: 'game',
hookDllPath: '',
hookDllPathX86: '',
});
capture.on('error', () => {});
await assert.rejects(() => capture.start(), /Game capture hook unavailable/);
assert.equal(calls.length, 0, 'native start must not be called when the game hook is unavailable');
},
);
test('lifecycle "error" emits an Error event', {skip: injectionSkip}, () => {
const {binding, natives} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const errors = [];
capture.on('error', (err) => errors.push(err));
natives[0].lifecycleCallback('error', 'DXGI device removed');
assert.equal(errors.length, 1);
assert.ok(errors[0] instanceof Error);
assert.equal(errors[0].message, 'DXGI device removed');
});
test('lifecycle "closed" stops the native capture and emits closed once', {skip: injectionSkip}, async () => {
const {binding, natives} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
let closed = 0;
capture.on('closed', () => {
closed += 1;
});
natives[0].lifecycleCallback('closed', '');
await Promise.resolve();
await capture.stop();
assert.equal(closed, 1);
assert.equal(natives[0].stopCount, 1);
});
test('non-fatal "stalled" lifecycle re-emits as a stalled event WITHOUT stopping', {skip: injectionSkip}, () => {
const {binding, natives} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const stalls = [];
let closed = 0;
capture.on('stalled', (message) => stalls.push(message));
capture.on('closed', () => {
closed += 1;
});
natives[0].lifecycleCallback('stalled', 'frame counter frozen while focused');
assert.deepEqual(stalls, ['frame counter frozen while focused']);
assert.equal(natives[0].stopCount, 0);
assert.equal(closed, 0);
});
test(
'non-fatal "diagnostic" lifecycle re-emits as a diagnostic event WITHOUT stopping',
{skip: injectionSkip},
() => {
const {binding, natives} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const diags = [];
let closed = 0;
capture.on('diagnostic', (message) => diags.push(message));
capture.on('closed', () => {
closed += 1;
});
natives[0].lifecycleCallback('diagnostic', 'fell back to CPU readback');
assert.deepEqual(diags, ['fell back to CPU readback']);
assert.equal(natives[0].stopCount, 0);
assert.equal(closed, 0);
},
);
test(
'lifecycle "error" in the real napi [type, message] array shape emits an Error event',
{skip: injectionSkip},
() => {
const {binding, natives} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const errors = [];
capture.on('error', (err) => errors.push(err));
natives[0].lifecycleCallback(['error', 'DXGI device removed']);
assert.equal(errors.length, 1);
assert.ok(errors[0] instanceof Error);
assert.equal(errors[0].message, 'DXGI device removed');
},
);
test(
'lifecycle "closed" in the napi array shape stops the native capture and emits closed once',
{skip: injectionSkip},
async () => {
const {binding, natives} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
let closed = 0;
capture.on('closed', () => {
closed += 1;
});
natives[0].lifecycleCallback(['closed', '']);
await Promise.resolve();
await capture.stop();
assert.equal(closed, 1);
assert.equal(natives[0].stopCount, 1);
},
);
test(
'lifecycle "stalled" in the napi array shape re-emits as a stalled event WITHOUT stopping',
{skip: injectionSkip},
() => {
const {binding, natives} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const stalls = [];
let closed = 0;
capture.on('stalled', (message) => stalls.push(message));
capture.on('closed', () => {
closed += 1;
});
natives[0].lifecycleCallback(['stalled', 'frame counter frozen while focused']);
assert.deepEqual(stalls, ['frame counter frozen while focused']);
assert.equal(natives[0].stopCount, 0);
assert.equal(closed, 0);
},
);
test(
'lifecycle "diagnostic" (injected via <method>) in the napi array shape re-emits WITHOUT stopping',
{skip: injectionSkip},
() => {
const {binding, natives} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const diags = [];
let closed = 0;
capture.on('diagnostic', (message) => diags.push(message));
capture.on('closed', () => {
closed += 1;
});
natives[0].lifecycleCallback(['diagnostic', 'game capture injected via set-windows-hook']);
assert.deepEqual(diags, ['game capture injected via set-windows-hook']);
assert.equal(natives[0].stopCount, 0);
assert.equal(closed, 0);
},
);
test('getDiagnostics() passes the native snapshot through', {skip: injectionSkip}, () => {
const {binding, diagnostics} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const snapshot = capture.getDiagnostics();
assert.deepEqual(snapshot, diagnostics);
});
test('getDiagnostics() surfaces the activeStrategy + lastFallbackReason fields', {skip: injectionSkip}, () => {
const {binding, diagnostics} = makeFakeBinding();
diagnostics.activeStrategy = 'dxgi-duplication';
diagnostics.lastFallbackReason =
'game-hook capture could not inject its capture hook; switching to dxgi-duplication capture';
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const snapshot = capture.getDiagnostics();
assert.equal(snapshot.activeStrategy, 'dxgi-duplication');
assert.match(snapshot.lastFallbackReason, /switching to dxgi-duplication/);
});
test('elevateGpuSchedulingPriority forwards the optional realtime priority class', {skip: injectionSkip}, () => {
const {binding, priorityCalls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
assert.equal(winGameCapture.elevateGpuSchedulingPriority(1234, 'realtime'), true);
assert.deepEqual(priorityCalls, [{type: 'elevate', processId: 1234, priorityClass: 'realtime'}]);
});
test('elevateGpuSchedulingPriority canonicalizes the real-time priority alias', {skip: injectionSkip}, () => {
const {binding, priorityCalls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
assert.equal(winGameCapture.elevateGpuSchedulingPriority(1234, 'real-time'), true);
assert.deepEqual(priorityCalls, [{type: 'elevate', processId: 1234, priorityClass: 'realtime'}]);
});
test('elevateGpuSchedulingPriority preserves the existing default priority class', {skip: injectionSkip}, () => {
const {binding, priorityCalls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
assert.equal(winGameCapture.elevateGpuSchedulingPriority(1234), true);
assert.deepEqual(priorityCalls, [{type: 'elevate', processId: 1234, priorityClass: undefined}]);
});
test(
'elevateGpuSchedulingPriority rejects unsupported priority classes before binding calls',
{skip: injectionSkip},
() => {
const {binding, priorityCalls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
assert.equal(winGameCapture.elevateGpuSchedulingPriority(1234, 'normal'), false);
assert.deepEqual(priorityCalls, []);
},
);
test('lifecycle fallback "error" (next-strategy=...) re-emits as a fatal error event', {skip: injectionSkip}, () => {
const {binding, natives} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const errors = [];
capture.on('error', (err) => errors.push(err));
natives[0].lifecycleCallback([
'error',
'fallback: game-hook -> dxgi-duplication (game-hook capture could not inject its capture hook) [next-strategy=dxgi-duplication]',
]);
assert.equal(errors.length, 1);
assert.ok(errors[0] instanceof Error);
assert.equal(winGameCapture.parseFallbackRecommendation(errors[0].message), 'dxgi-duplication');
});
test(
'lifecycle fallback "diagnostic" (upgrade, next-strategy=...) re-emits WITHOUT stopping',
{skip: injectionSkip},
() => {
const {binding, natives} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const diags = [];
let closed = 0;
capture.on('diagnostic', (message) => diags.push(message));
capture.on('closed', () => {
closed += 1;
});
natives[0].lifecycleCallback([
'diagnostic',
'upgrade: window-gdi -> dxgi-duplication (window-gdi has been stable) [next-strategy=dxgi-duplication]',
]);
assert.equal(diags.length, 1);
assert.equal(winGameCapture.parseFallbackRecommendation(diags[0]), 'dxgi-duplication');
assert.equal(natives[0].stopCount, 0);
assert.equal(closed, 0);
},
);
});
describe('win-game-capture loader wrapper -- parseFallbackRecommendation', () => {
test('extracts the recommended strategy from a transition error message', () => {
assert.equal(
winGameCapture.parseFallbackRecommendation(
'fallback: game-hook -> window-gdi (reason) [next-strategy=window-gdi]',
),
'window-gdi',
);
});
test('returns "none" for an exhausted give-up message', () => {
assert.equal(
winGameCapture.parseFallbackRecommendation(
'fallback exhausted: window-gdi was the last resort [next-strategy=none]',
),
'none',
);
});
test('returns null for an ordinary error with no recommendation', () => {
assert.equal(winGameCapture.parseFallbackRecommendation('DXGI device removed'), null);
assert.equal(winGameCapture.parseFallbackRecommendation(undefined), null);
assert.equal(winGameCapture.parseFallbackRecommendation(''), null);
});
test('returns null for an unrecognised strategy token', () => {
assert.equal(winGameCapture.parseFallbackRecommendation('[next-strategy=teleporter]'), null);
});
test('accepts the WGC fallback strategy token', () => {
assert.equal(winGameCapture.parseFallbackRecommendation('[next-strategy=wgc]'), 'wgc');
assert.equal(
winGameCapture.parseFallbackRecommendation(
'fallback: game-hook -> wgc (game-hook capture could not inject its capture hook) [next-strategy=wgc]',
),
'wgc',
);
});
test('rejects browser fallback strategy tokens', () => {
assert.equal(winGameCapture.parseFallbackRecommendation('[next-strategy=browser-display]'), null);
assert.equal(winGameCapture.parseFallbackRecommendation('[next-strategy=desktop-capturer]'), null);
});
});
@@ -0,0 +1,25 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "fluxer_inject_helper"
version = "0.0.0"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
@@ -0,0 +1,32 @@
[package]
name = "fluxer_inject_helper"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
[workspace]
resolver = "2"
[[bin]]
name = "fluxer-inject-helper"
path = "src/main.rs"
[profile.release]
# Keep the helper tiny: it is a one-shot injector exe shipped alongside the
# hook DLLs, so optimise hard for size and strip symbols.
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = true
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = {version = "0.61.2", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_Diagnostics_Debug",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Threading",
]}
@@ -0,0 +1,242 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![deny(clippy::all)]
#[cfg(not(target_os = "windows"))]
fn main() {
eprintln!("fluxer-inject-helper is only supported on Windows");
std::process::exit(Stage::Unsupported as i32);
}
#[repr(i32)]
#[derive(Clone, Copy)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
enum Stage {
Success = 0,
BadArgs = 2,
HookMissing = 3,
OpenProcess = 4,
Alloc = 5,
Write = 6,
Kernel32 = 7,
LoadLibraryAddr = 8,
CreateThread = 9,
WaitTimeout = 10,
LoadLibraryFailed = 11,
#[cfg_attr(target_os = "windows", allow(dead_code))]
Unsupported = 64,
}
#[cfg(target_os = "windows")]
fn main() {
let code = win::run();
std::process::exit(code as i32);
}
#[cfg(target_os = "windows")]
mod win {
use super::Stage;
use core::ffi::c_void;
use std::ptr::{null, null_mut};
use windows_sys::Win32::Foundation::{
CloseHandle, GetLastError, HANDLE, INVALID_HANDLE_VALUE, WAIT_ABANDONED, WAIT_OBJECT_0,
};
use windows_sys::Win32::System::Diagnostics::Debug::{OutputDebugStringW, WriteProcessMemory};
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
use windows_sys::Win32::System::Memory::{
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, VirtualAllocEx, VirtualFreeEx,
};
use windows_sys::Win32::System::Threading::{
CreateRemoteThread, GetExitCodeThread, INFINITE, OpenProcess, PROCESS_CREATE_THREAD,
PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE,
WaitForSingleObject,
};
const DEFAULT_TIMEOUT_MS: u32 = 10_000;
struct OwnedHandle(HANDLE);
impl OwnedHandle {
fn raw(&self) -> HANDLE {
self.0
}
}
impl Drop for OwnedHandle {
fn drop(&mut self) {
if !self.0.is_null() && self.0 != INVALID_HANDLE_VALUE {
unsafe {
CloseHandle(self.0);
}
}
}
}
fn to_wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
fn log(message: &str) {
let text = format!("[fluxer-inject-helper] {message}");
let wide = to_wide(&text);
unsafe {
OutputDebugStringW(wide.as_ptr());
}
eprintln!("{text}");
}
fn fail(stage: Stage, context: &str) -> Stage {
let err = unsafe { GetLastError() };
log(&format!(
"FAILED stage={} ({context}); GetLastError={err}",
stage as i32
));
stage
}
pub(super) fn run() -> Stage {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.len() < 2 || args.len() > 3 {
log(&format!(
"bad args: expected <pid> <hook-dll-path> [timeout-ms], got {} arg(s)",
args.len()
));
return Stage::BadArgs;
}
let Ok(target_pid) = args[0].parse::<u32>() else {
log(&format!("bad args: unparseable pid {:?}", args[0]));
return Stage::BadArgs;
};
if target_pid == 0 {
log("bad args: pid must be non-zero");
return Stage::BadArgs;
}
let hook_path = args[1].as_str();
let timeout_ms = match args.get(2) {
None => DEFAULT_TIMEOUT_MS,
Some(raw) => match raw.parse::<u32>() {
Ok(0) => INFINITE,
Ok(value) => value,
Err(_) => {
log(&format!("bad args: unparseable timeout {raw:?}"));
return Stage::BadArgs;
}
},
};
if !std::path::Path::new(hook_path).exists() {
log(&format!("hook DLL missing: {hook_path}"));
return Stage::HookMissing;
}
log(&format!(
"injecting (pid={target_pid}, hook={hook_path}, timeout_ms={timeout_ms}, \
helper_bits={})",
usize::BITS
));
inject(target_pid, hook_path, timeout_ms)
}
fn inject(target_pid: u32, hook_path: &str, timeout_ms: u32) -> Stage {
let wide_path = to_wide(hook_path);
let path_bytes = wide_path.len() * std::mem::size_of::<u16>();
unsafe {
let process = OpenProcess(
PROCESS_CREATE_THREAD
| PROCESS_VM_OPERATION
| PROCESS_VM_WRITE
| PROCESS_VM_READ
| PROCESS_QUERY_INFORMATION,
0,
target_pid,
);
if process.is_null() {
return fail(Stage::OpenProcess, "OpenProcess returned null");
}
let process = OwnedHandle(process);
let remote_path = VirtualAllocEx(
process.raw(),
null(),
path_bytes,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
);
if remote_path.is_null() {
return fail(Stage::Alloc, "VirtualAllocEx returned null");
}
let mut written: usize = 0;
let write_ok = WriteProcessMemory(
process.raw(),
remote_path,
wide_path.as_ptr().cast(),
path_bytes,
&mut written,
) != 0;
if !write_ok || written != path_bytes {
let stage = fail(Stage::Write, "WriteProcessMemory failed/short");
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
return stage;
}
let kernel32_name = to_wide("kernel32.dll");
let kernel32 = GetModuleHandleW(kernel32_name.as_ptr());
if kernel32.is_null() {
let stage = fail(Stage::Kernel32, "GetModuleHandleW(kernel32.dll)");
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
return stage;
}
let load_library = GetProcAddress(kernel32, c"LoadLibraryW".as_ptr().cast());
let Some(load_library) = load_library else {
let stage = fail(Stage::LoadLibraryAddr, "GetProcAddress(LoadLibraryW)");
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
return stage;
};
let start_routine: unsafe extern "system" fn(*mut c_void) -> u32 =
std::mem::transmute(load_library);
let thread = CreateRemoteThread(
process.raw(),
null(),
0,
Some(start_routine),
remote_path,
0,
null_mut(),
);
if thread.is_null() {
let stage = fail(Stage::CreateThread, "CreateRemoteThread returned null");
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
return stage;
}
let thread = OwnedHandle(thread);
let wait = WaitForSingleObject(thread.raw(), timeout_ms);
if wait != WAIT_OBJECT_0 && wait != WAIT_ABANDONED {
let stage = fail(Stage::WaitTimeout, "WaitForSingleObject did not signal");
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
return stage;
}
let mut exit_code: u32 = 0;
let got_exit = GetExitCodeThread(thread.raw(), &mut exit_code) != 0;
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
if !got_exit {
return fail(Stage::LoadLibraryFailed, "GetExitCodeThread failed");
}
if exit_code == 0 {
log("remote LoadLibraryW returned NULL -- DLL failed to load in target");
return Stage::LoadLibraryFailed;
}
log(&format!(
"injection succeeded (remote LoadLibraryW HMODULE low bits={exit_code:#010x})"
));
Stage::Success
}
}
}
@@ -0,0 +1,524 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const {existsSync, readdirSync, readFileSync, statSync} = require('node:fs');
const os = require('node:os');
const {basename} = require('node:path');
const {spawnSync} = require('node:child_process');
const NATIVE_LOAD_ERROR_MARKER = Symbol.for('fluxer.nativeLoadError');
const MAX_TEXT_LENGTH = 6000;
const MAX_DIRECTORY_ENTRIES = 80;
function trimText(value, limit = MAX_TEXT_LENGTH) {
const text = Buffer.isBuffer(value) ? value.toString('utf8') : String(value ?? '');
const trimmed = text.trim();
if (!trimmed) return null;
return trimmed.length > limit ? `${trimmed.slice(0, limit)}\n...<truncated>` : trimmed;
}
function errorDiagnostic(error) {
if (!error) return null;
if (error instanceof Error) {
return {
name: error.name || 'Error',
message: error.message,
code: error.code || null,
stack: trimText(error.stack || error.message),
};
}
return {
name: typeof error,
message: trimText(String(error)),
code: null,
stack: null,
};
}
function formatErrorDiagnostic(diagnostic) {
if (!diagnostic) return null;
const lines = [];
if (diagnostic.code) lines.push(`code=${diagnostic.code}`);
if (diagnostic.stack) lines.push(diagnostic.stack);
else if (diagnostic.message) lines.push(diagnostic.message);
return trimText(lines.join('\n'));
}
function fileDiagnostic(filePath) {
if (!filePath) return {path: null, exists: false, error: 'not resolved'};
try {
const stat = statSync(filePath);
return {
path: filePath,
exists: true,
size: stat.size,
mode: `0${(stat.mode & 0o777).toString(8)}`,
mtime: stat.mtime.toISOString(),
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
};
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
return {path: filePath, exists: false, error: reason};
}
}
function formatFileDiagnostic(diagnostic) {
if (!diagnostic) return 'not resolved';
if (!diagnostic.exists) return `exists=false, statError=${diagnostic.error || '<unknown>'}`;
return [
`exists=true`,
`size=${diagnostic.size}`,
`mode=${diagnostic.mode}`,
`mtime=${diagnostic.mtime}`,
`isFile=${diagnostic.isFile}`,
].join(', ');
}
function directoryDiagnostic(dirPath) {
if (!dirPath) return {path: null, ok: false, error: 'not resolved', entries: [], total: 0, omitted: 0};
try {
const entries = readdirSync(dirPath, {withFileTypes: true}).map((entry) => ({
name: entry.name,
type: entry.isDirectory() ? 'directory' : entry.isFile() ? 'file' : 'other',
}));
entries.sort((a, b) => a.name.localeCompare(b.name));
const visible = entries.slice(0, MAX_DIRECTORY_ENTRIES);
return {
path: dirPath,
ok: true,
entries: visible,
total: entries.length,
omitted: Math.max(0, entries.length - visible.length),
};
} catch (error) {
return {
path: dirPath,
ok: false,
error: error instanceof Error ? error.message : String(error),
entries: [],
total: 0,
omitted: 0,
};
}
}
function formatDirectoryDiagnostic(diagnostic) {
if (!diagnostic) return '<unavailable>';
if (!diagnostic.ok) return `directory listing failed: ${diagnostic.error || '<unknown>'}`;
const entries = diagnostic.entries.map((entry) => `${entry.name}${entry.type === 'directory' ? '/' : ''}`);
const suffix = diagnostic.omitted > 0 ? [`...<${diagnostic.omitted} more entries>`] : [];
return [...entries, ...suffix].join('\n') || '<empty>';
}
function selectedEnvironmentNames(skipNativeProbeEnv) {
const names = [
'ELECTRON_RUN_AS_NODE',
'FLUXER_NATIVE_MODULE_PREFLIGHT_CHILD',
'LD_LIBRARY_PATH',
'DYLD_LIBRARY_PATH',
'DISPLAY',
'WAYLAND_DISPLAY',
'XDG_CURRENT_DESKTOP',
'XDG_SESSION_TYPE',
'DBUS_SESSION_BUS_ADDRESS',
'PULSE_SERVER',
'PIPEWIRE_REMOTE',
'PATH',
];
if (skipNativeProbeEnv) names.push(skipNativeProbeEnv);
return names;
}
function environmentDiagnostics(skipNativeProbeEnv) {
return Object.fromEntries(
selectedEnvironmentNames(skipNativeProbeEnv).map((name) => [name, process.env[name] ?? null]),
);
}
function formatEnvironment(diagnostic) {
return Object.entries(diagnostic)
.map(([name, value]) => `${name}=${value ?? '<unset>'}`)
.join('\n');
}
function runtimeDiagnostics() {
const versions = process.versions || {};
let reportHeader = null;
if (process.report && typeof process.report.getReport === 'function') {
try {
reportHeader = process.report.getReport().header || null;
} catch {
reportHeader = null;
}
}
const glibcRuntime = versions.glibcVersionRuntime || reportHeader?.glibcVersionRuntime || '<unknown>';
const glibcCompiler = versions.glibcVersionCompiler || reportHeader?.glibcVersionCompiler || '<unknown>';
return {
node: versions.node || null,
electron: versions.electron || null,
modules: versions.modules || null,
napi: versions.napi || null,
v8: versions.v8 || null,
uv: versions.uv || null,
openssl: versions.openssl || null,
glibcRuntime,
glibcCompiler,
platform: process.platform,
arch: process.arch,
osType: os.type(),
osRelease: os.release(),
osVersion: typeof os.version === 'function' ? os.version() : null,
execPath: process.execPath,
resourcesPath: process.resourcesPath || null,
cwd: process.cwd(),
};
}
function formatRuntimeDiagnostics(diagnostic) {
return [
`node=${diagnostic.node || '<unknown>'}`,
`electron=${diagnostic.electron || '<none>'}`,
`modules=${diagnostic.modules || '<unknown>'}`,
`napi=${diagnostic.napi || '<unknown>'}`,
`v8=${diagnostic.v8 || '<unknown>'}`,
`uv=${diagnostic.uv || '<unknown>'}`,
`openssl=${diagnostic.openssl || '<unknown>'}`,
`glibcRuntime=${diagnostic.glibcRuntime || '<unknown>'}`,
`glibcCompiler=${diagnostic.glibcCompiler || '<unknown>'}`,
`process=${diagnostic.platform}/${diagnostic.arch}`,
`os=${diagnostic.osType} ${diagnostic.osRelease} ${diagnostic.osVersion || '<unknown>'}`,
`execPath=${diagnostic.execPath}`,
`resourcesPath=${diagnostic.resourcesPath || '<unknown>'}`,
`cwd=${diagnostic.cwd}`,
].join('\n');
}
const REDISTRIBUTABLE_RUNTIME_PATTERNS = [
/^vcruntime\d+(?:_\d+)?\.dll$/i,
/^msvcp\d+(?:_\d+)?\.dll$/i,
/^msvcr\d+(?:_\d+)?\.dll$/i,
/^concrt\d+\.dll$/i,
/^vcamp\d+\.dll$/i,
/^vcomp\d+\.dll$/i,
];
function readPeImports(filePath) {
let buffer;
try {
buffer = readFileSync(filePath);
} catch {
return null;
}
if (buffer.length < 0x40) return null;
const peOffset = buffer.readUInt32LE(0x3c);
if (peOffset <= 0 || peOffset + 24 >= buffer.length) return null;
if (buffer.readUInt32LE(peOffset) !== 0x4550) return null;
const coffOffset = peOffset + 4;
const numberOfSections = buffer.readUInt16LE(coffOffset + 2);
const sizeOfOptionalHeader = buffer.readUInt16LE(coffOffset + 16);
const optionalHeaderOffset = coffOffset + 20;
if (optionalHeaderOffset + sizeOfOptionalHeader > buffer.length) return null;
const magic = buffer.readUInt16LE(optionalHeaderOffset);
if (magic !== 0x10b && magic !== 0x20b) return null;
const dataDirectoriesOffset = optionalHeaderOffset + (magic === 0x20b ? 112 : 96);
const importEntryOffset = dataDirectoriesOffset + 8;
if (importEntryOffset + 8 > buffer.length) return null;
const importRva = buffer.readUInt32LE(importEntryOffset);
if (importRva === 0) return [];
const sections = [];
const sectionTableOffset = optionalHeaderOffset + sizeOfOptionalHeader;
for (let i = 0; i < numberOfSections; i++) {
const base = sectionTableOffset + i * 40;
if (base + 40 > buffer.length) return null;
sections.push({
virtualSize: buffer.readUInt32LE(base + 8),
virtualAddress: buffer.readUInt32LE(base + 12),
rawSize: buffer.readUInt32LE(base + 16),
rawPointer: buffer.readUInt32LE(base + 20),
});
}
const rvaToOffset = (rva) => {
for (const s of sections) {
const span = Math.max(s.virtualSize, s.rawSize);
if (rva >= s.virtualAddress && rva < s.virtualAddress + span) {
return rva - s.virtualAddress + s.rawPointer;
}
}
return -1;
};
const readCString = (offset) => {
let end = offset;
while (end < buffer.length && buffer[end] !== 0) end++;
return buffer.toString('ascii', offset, end);
};
const importTableOffset = rvaToOffset(importRva);
if (importTableOffset < 0) return [];
const imports = new Set();
for (let i = 0; i < 1024; i++) {
const base = importTableOffset + i * 20;
if (base + 20 > buffer.length) break;
const lookupRva = buffer.readUInt32LE(base);
const nameRva = buffer.readUInt32LE(base + 12);
const iatRva = buffer.readUInt32LE(base + 16);
if (lookupRva === 0 && nameRva === 0 && iatRva === 0) break;
const nameOffset = rvaToOffset(nameRva);
if (nameOffset < 0) continue;
const name = readCString(nameOffset);
if (name) imports.add(name);
}
return Array.from(imports);
}
function windowsImportProbe(nativePath) {
const imports = readPeImports(nativePath);
if (imports === null) return null;
const sortedImports = [...imports].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
const redistributable = sortedImports.filter((dll) =>
REDISTRIBUTABLE_RUNTIME_PATTERNS.some((pattern) => pattern.test(dll)),
);
return {
command: ['pe-imports', nativePath],
status: 0,
signal: null,
error: null,
stdout: sortedImports.join('\n') || null,
stderr: null,
missing: [],
redistributable,
};
}
function dependencyProbe(nativePath) {
if (!nativePath || !existsSync(nativePath)) return null;
if (process.platform === 'win32') return windowsImportProbe(nativePath);
const command =
process.platform === 'linux'
? ['ldd', nativePath]
: process.platform === 'darwin'
? ['otool', '-L', nativePath]
: null;
if (!command) return null;
const [bin, ...args] = command;
const result = spawnSync(bin, args, {
encoding: 'utf8',
timeout: 4000,
stdio: ['ignore', 'pipe', 'pipe'],
});
const stdout = trimText(result.stdout);
const stderr = trimText(result.stderr);
const missing =
process.platform === 'linux' && stdout
? stdout
.split('\n')
.map((line) => line.trim())
.filter((line) => line.includes('not found'))
: [];
return {
command,
status: result.status,
signal: result.signal || null,
error: result.error ? result.error.message : null,
stdout,
stderr,
missing,
redistributable: [],
};
}
function formatDependencyProbe(diagnostic) {
if (!diagnostic) return null;
const status = diagnostic.error
? `error=${diagnostic.error}`
: diagnostic.signal
? `signal=${diagnostic.signal}`
: `status=${diagnostic.status}`;
return [
`$ ${diagnostic.command.join(' ')}`,
status,
diagnostic.missing?.length ? `missing:\n${diagnostic.missing.join('\n')}` : null,
diagnostic.redistributable?.length
? `redistributableRuntimeImports (require VC++ redist on host):\n${diagnostic.redistributable.join('\n')}`
: null,
diagnostic.stdout ? `stdout:\n${diagnostic.stdout}` : null,
diagnostic.stderr ? `stderr:\n${diagnostic.stderr}` : null,
]
.filter(Boolean)
.join('\n');
}
function formatExtraDiagnostic(diagnostic) {
if (!diagnostic) return null;
if (typeof diagnostic === 'string') return diagnostic;
if (typeof diagnostic === 'object' && diagnostic.name && diagnostic.text) {
return `${diagnostic.name}:\n${diagnostic.text}`;
}
return `extra:\n${trimText(JSON.stringify(diagnostic, null, 2))}`;
}
function collectNativeDiagnostics({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason,
cause,
skipNativeProbeEnv,
extraDiagnostics = [],
}) {
return {
schemaVersion: 1,
moduleName,
reason,
target: {
platform: process.platform,
arch: process.arch,
},
packageDir: packageDir || null,
nativeRoot: nativeRoot || null,
nativePath: nativePath || null,
nativeFile: nativePath ? basename(nativePath) : null,
nativeFileStat: fileDiagnostic(nativePath),
runtime: runtimeDiagnostics(),
environment: environmentDiagnostics(skipNativeProbeEnv),
nativeRootEntries: directoryDiagnostic(nativeRoot),
dependencyProbe: dependencyProbe(nativePath),
extraDiagnostics: extraDiagnostics.filter(Boolean),
cause: errorDiagnostic(cause),
};
}
function formatNativeDiagnostics(diagnostics) {
const sections = [
`module=${diagnostics.moduleName}`,
`reason=${diagnostics.reason}`,
`target=${diagnostics.target.platform}/${diagnostics.target.arch}`,
`packageDir=${diagnostics.packageDir || '<unknown>'}`,
`nativeRoot=${diagnostics.nativeRoot || '<unknown>'}`,
`nativePath=${diagnostics.nativePath || '<unknown>'}`,
`nativeFile=${diagnostics.nativeFile || '<unknown>'}`,
`nativeFileStat=${formatFileDiagnostic(diagnostics.nativeFileStat)}`,
`runtime:\n${formatRuntimeDiagnostics(diagnostics.runtime)}`,
`environment:\n${formatEnvironment(diagnostics.environment)}`,
`nativeRootEntries:\n${formatDirectoryDiagnostic(diagnostics.nativeRootEntries)}`,
...diagnostics.extraDiagnostics.map(formatExtraDiagnostic).filter(Boolean),
];
const dependencyOutput = formatDependencyProbe(diagnostics.dependencyProbe);
if (dependencyOutput) sections.push(`dependencyProbe:\n${dependencyOutput}`);
const causeText = formatErrorDiagnostic(diagnostics.cause);
if (causeText) sections.push(`cause:\n${causeText}`);
return sections.join('\n');
}
function isNativeLoadError(error) {
return Boolean(error?.[NATIVE_LOAD_ERROR_MARKER]);
}
function createNativeLoadError({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason,
cause,
skipNativeProbeEnv,
extraDiagnostics = [],
}) {
if (isNativeLoadError(cause)) return cause;
const diagnostics = collectNativeDiagnostics({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason,
cause,
skipNativeProbeEnv,
extraDiagnostics,
});
const error = new Error(`${moduleName} native module failed to load.\n${formatNativeDiagnostics(diagnostics)}`);
error.name = 'NativeModuleLoadError';
error[NATIVE_LOAD_ERROR_MARKER] = true;
error.nativeDiagnostics = diagnostics;
error.toJSON = () => ({
name: error.name,
message: error.message,
nativeDiagnostics: diagnostics,
});
if (cause) error.cause = cause;
return error;
}
function probeNativeBinary({moduleName, nativePath, nativeRoot, packageDir, skipNativeProbeEnv, timeoutMs = 5000}) {
if (!skipNativeProbeEnv || process.env[skipNativeProbeEnv] === '1') {
return null;
}
const result = spawnSync(process.execPath, ['-e', 'require(process.argv[1])', nativePath], {
env: {...process.env, ELECTRON_RUN_AS_NODE: '1', [skipNativeProbeEnv]: '1'},
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: timeoutMs,
});
if (result.status === 0) return null;
const reason = result.error
? result.error.message
: result.signal
? `safety probe terminated by signal ${result.signal}`
: `safety probe exited with code ${result.status}`;
return createNativeLoadError({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason,
skipNativeProbeEnv,
extraDiagnostics: [
result.stdout ? {name: 'probeStdout', text: trimText(result.stdout)} : null,
result.stderr ? {name: 'probeStderr', text: trimText(result.stderr)} : null,
],
});
}
function loadNativeBinding({moduleName, nativePath, nativeRoot, packageDir, skipNativeProbeEnv, probe = true}) {
if (!existsSync(nativePath)) {
return {
binding: null,
loadError: createNativeLoadError({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason: 'native binary not found',
skipNativeProbeEnv,
}),
};
}
const nativeProbeError = probe
? probeNativeBinary({moduleName, nativePath, nativeRoot, packageDir, skipNativeProbeEnv})
: null;
if (nativeProbeError) {
return {binding: null, loadError: nativeProbeError};
}
try {
return {binding: require(nativePath), loadError: null};
} catch (error) {
return {
binding: null,
loadError: createNativeLoadError({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason: 'require(nativePath) threw',
cause: error,
skipNativeProbeEnv,
}),
};
}
}
module.exports = {
collectNativeDiagnostics,
createNativeLoadError,
formatNativeDiagnostics,
isNativeLoadError,
loadNativeBinding,
probeNativeBinary,
};
@@ -0,0 +1,41 @@
{
"name": "@fluxer/win-game-capture",
"version": "0.0.0",
"description": "Native Windows game capture fallback for fullscreen games that display capture cannot capture",
"private": true,
"license": "AGPL-3.0-or-later",
"os": [
"win32"
],
"cpu": [
"x64",
"arm64"
],
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.js",
"index.d.ts",
"loader-diagnostics.cjs",
"win-game-capture.win32-x64-msvc.node",
"win-game-capture.win32-arm64-msvc.node",
"fluxer-game-hook.win32-x64-msvc.dll",
"fluxer-game-hook.win32-ia32-msvc.dll",
"fluxer-game-hook.win32-arm64-msvc.dll",
"fluxer-inject-helper.win32-x64-msvc.exe",
"fluxer-inject-helper.win32-ia32-msvc.exe",
"fluxer-inject-helper.win32-arm64-msvc.exe",
"fluxer-vulkan-layer.win32-x64-msvc.dll",
"fluxer-vulkan-layer.win32-x64-msvc.json",
"fluxer-vulkan-layer.win32-ia32-msvc.dll",
"fluxer-vulkan-layer.win32-ia32-msvc.json",
"fluxer-vulkan-layer.win32-arm64-msvc.dll",
"fluxer-vulkan-layer.win32-arm64-msvc.json"
],
"scripts": {
"build": "cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon",
"test": "cargo test --manifest-path Cargo.toml && node --test index.test.mjs",
"test:fixtures": "node scripts/fixture-smoke.mjs",
"test:loader": "node --test index.test.mjs"
}
}
@@ -0,0 +1,377 @@
#!/usr/bin/env node
// SPDX-License-Identifier: AGPL-3.0-or-later
import {spawn} from 'node:child_process';
import {existsSync} from 'node:fs';
import {createRequire} from 'node:module';
import {dirname, join} from 'node:path';
import {setTimeout as delay} from 'node:timers/promises';
import {fileURLToPath} from 'node:url';
const require = createRequire(import.meta.url);
const capture = require('../index.js');
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
const DEFAULT_FIXTURES = ['d3d11-present-fixture'];
const ALL_FIXTURES = [
'd3d9-present-fixture',
'd3d10-present-fixture',
'd3d11-present-fixture',
'd3d12-present-fixture',
'opengl-swapbuffers-fixture',
'vulkan-present-fixture',
];
const WIDTH = Number.parseInt(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_WIDTH ?? '640', 10);
const HEIGHT = Number.parseInt(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_HEIGHT ?? '360', 10);
const FRAME_RATE = Number.parseInt(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_FPS ?? '30', 10);
const START_TIMEOUT_MS = Number.parseInt(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_START_TIMEOUT_MS ?? '15000', 10);
const FRAME_TIMEOUT_MS = Number.parseInt(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_FRAME_TIMEOUT_MS ?? '15000', 10);
const TRANSPORT_MEMORY = 0;
const TRANSPORT_SHARED_TEXTURE = 1;
const API_OPENGL = 1;
const API_D3D9 = 3;
const API_D3D10 = 4;
const API_D3D11 = 5;
const API_D3D12 = 6;
const API_VULKAN = 7;
const FALLBACK_NONE = 0;
const FALLBACK_SHARED_TEXTURE_UNSUPPORTED = 1;
const EXPECTED_DIAGNOSTICS = {
'd3d9-present-fixture': {
apiType: API_D3D9,
transport: TRANSPORT_SHARED_TEXTURE,
fallbackReason: FALLBACK_NONE,
requiresDxgiFormat: true,
},
'd3d10-present-fixture': {
apiType: API_D3D10,
transport: TRANSPORT_SHARED_TEXTURE,
fallbackReason: FALLBACK_NONE,
requiresDxgiFormat: true,
},
'd3d11-present-fixture': {
apiType: API_D3D11,
transport: TRANSPORT_SHARED_TEXTURE,
fallbackReason: FALLBACK_NONE,
requiresDxgiFormat: true,
},
'd3d12-present-fixture': {
apiType: API_D3D12,
transport: TRANSPORT_SHARED_TEXTURE,
fallbackReason: FALLBACK_NONE,
requiresDxgiFormat: true,
},
'opengl-swapbuffers-fixture': {
apiType: API_OPENGL,
transportOneOf: [TRANSPORT_SHARED_TEXTURE, TRANSPORT_MEMORY],
fallbackReasonOneOf: [FALLBACK_NONE, FALLBACK_SHARED_TEXTURE_UNSUPPORTED],
requiresDxgiFormatWhenShared: true,
},
'vulkan-present-fixture': {
apiType: API_VULKAN,
transport: TRANSPORT_SHARED_TEXTURE,
fallbackReason: FALLBACK_NONE,
requiresDxgiFormat: true,
},
'i686-present-fixture': {
apiType: API_D3D11,
transport: TRANSPORT_SHARED_TEXTURE,
fallbackReason: FALLBACK_NONE,
requiresDxgiFormat: true,
},
};
function envFlag(name) {
return /^(1|true|yes|on)$/i.test(process.env[name] ?? '');
}
function selectedFixtures() {
const raw = process.argv.slice(2).join(',') || process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURES || '';
if (!raw.trim()) return DEFAULT_FIXTURES;
const names = raw
.split(',')
.map((name) => name.trim())
.filter(Boolean);
return names.flatMap((name) => (name === 'all' ? ALL_FIXTURES : [name]));
}
function fixtureTarget(fixture) {
return fixture === 'i686-present-fixture' ? 'i686-pc-windows-msvc' : null;
}
function fixtureExePath(fixture) {
const target = fixtureTarget(fixture);
const targetDir = target
? join(ROOT, 'test-apps', fixture, 'target', target, 'release')
: join(ROOT, 'test-apps', fixture, 'target', 'release');
return join(targetDir, `${fixture}.exe`);
}
function envKeyForFixture(fixture) {
return `FLUXER_WIN_GAME_CAPTURE_FIXTURE_ARGS_${fixture.toUpperCase().replaceAll(/[^A-Z0-9]+/g, '_')}`;
}
function fixtureEnvValue(baseName, fixture) {
const fixtureKey = `${baseName}_${fixture.toUpperCase().replaceAll(/[^A-Z0-9]+/g, '_')}`;
return process.env[fixtureKey] ?? process.env[baseName];
}
function splitExtraArgs(raw) {
return (raw ?? '')
.split(/\s+/)
.map((arg) => arg.trim())
.filter(Boolean);
}
function extraFixtureArgs(fixture) {
return [
...splitExtraArgs(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_ARGS),
...splitExtraArgs(process.env[envKeyForFixture(fixture)]),
];
}
function diagnosticOverride(baseName, fixture) {
const raw = fixtureEnvValue(baseName, fixture);
if (raw === undefined || raw === '') return undefined;
const value = Number.parseInt(raw, 10);
if (!Number.isFinite(value)) throw new Error(`Invalid ${baseName} override: ${raw}`);
return value;
}
function runCommand(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: options.cwd ?? ROOT,
env: process.env,
stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'],
windowsHide: false,
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', (chunk) => {
stdout += chunk.toString();
if (options.echo) process.stdout.write(chunk);
});
child.stderr?.on('data', (chunk) => {
stderr += chunk.toString();
if (options.echo) process.stderr.write(chunk);
});
child.once('error', reject);
child.once('exit', (code, signal) => {
if (code === 0) {
resolve({stdout, stderr});
return;
}
reject(new Error(`${command} ${args.join(' ')} failed with ${signal ?? code}\n${stderr || stdout}`));
});
});
}
async function buildFixture(fixture) {
const manifest = join(ROOT, 'test-apps', fixture, 'Cargo.toml');
if (!existsSync(manifest)) throw new Error(`unknown fixture: ${fixture}`);
const exe = fixtureExePath(fixture);
if (envFlag('FLUXER_WIN_GAME_CAPTURE_FIXTURE_SKIP_BUILD') && existsSync(exe)) {
console.log(`[fixture-smoke] using existing ${fixture}`);
return exe;
}
const args = ['build', '--release', '--manifest-path', manifest];
const target = fixtureTarget(fixture);
if (target) args.push('--target', target);
console.log(`[fixture-smoke] building ${fixture}`);
await runCommand('cargo', args, {echo: envFlag('FLUXER_WIN_GAME_CAPTURE_FIXTURE_VERBOSE')});
if (!existsSync(exe)) throw new Error(`fixture build did not produce ${exe}`);
return exe;
}
function waitForHwnd(child, fixture) {
return new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
const timeout = setTimeout(() => {
reject(new Error(`${fixture} did not print HWND within ${START_TIMEOUT_MS}ms\n${stderr || stdout}`));
}, START_TIMEOUT_MS);
const finish = (hwnd) => {
clearTimeout(timeout);
resolve(hwnd);
};
child.stdout.on('data', (chunk) => {
const text = chunk.toString();
stdout += text;
const match = stdout.match(/HWND=(\d+)/);
if (match) finish(match[1]);
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
if (envFlag('FLUXER_WIN_GAME_CAPTURE_FIXTURE_VERBOSE')) process.stderr.write(chunk);
});
child.once('exit', (code, signal) => {
clearTimeout(timeout);
reject(new Error(`${fixture} exited before capture started (${signal ?? code})\n${stderr || stdout}`));
});
});
}
function frameSignature(frame) {
const bytes = frame.data;
const width = Math.max(1, frame.width);
const height = Math.max(1, frame.height);
const stride = Math.max(1, frame.strideY || (frame.format === 'bgra' ? width * 4 : width));
const rows = frame.format === 'bgra' ? Math.min(height, 12) : Math.min(height, 32);
let hash = 2166136261;
for (let y = 0; y < rows; y += 1) {
const row = y * stride;
const rowBytes = frame.format === 'bgra' ? Math.min(stride, width * 4, 256) : Math.min(stride, width, 256);
for (let x = 0; x < rowBytes; x += 4) {
hash ^= bytes[row + x] ?? 0;
hash = Math.imul(hash, 16777619) >>> 0;
}
}
return hash >>> 0;
}
async function waitForAdvancingFrames(screenCapture, fixture) {
const signatures = new Set();
let frameCount = 0;
let lastDiagnostics = null;
const onFrame = (frame) => {
frameCount += 1;
signatures.add(frameSignature(frame));
lastDiagnostics = screenCapture.getDiagnostics?.() ?? lastDiagnostics;
};
screenCapture.on('frame', onFrame);
const start = Date.now();
try {
while (Date.now() - start < FRAME_TIMEOUT_MS) {
lastDiagnostics = screenCapture.getDiagnostics?.() ?? lastDiagnostics;
const nativeFrames = Number(lastDiagnostics?.frameCounter ?? 0);
if (frameCount >= 3 && (signatures.size >= 2 || nativeFrames >= 3)) {
return {frameCount, signatures: signatures.size, diagnostics: lastDiagnostics};
}
await delay(100);
}
throw new Error(
`${fixture} capture did not deliver advancing frames within ${FRAME_TIMEOUT_MS}ms (frames=${frameCount}, signatures=${signatures.size}, diagnostics=${JSON.stringify(lastDiagnostics)})`,
);
} finally {
screenCapture.off('frame', onFrame);
}
}
function assertEqualDiagnostic(fixture, diagnostics, key, expected) {
if (diagnostics?.[key] !== expected) {
throw new Error(`${fixture} expected diagnostics.${key}=${expected}, got ${JSON.stringify(diagnostics)}`);
}
}
function assertOneOfDiagnostic(fixture, diagnostics, key, expected) {
if (!expected.includes(diagnostics?.[key])) {
throw new Error(
`${fixture} expected diagnostics.${key} in [${expected.join(', ')}], got ${JSON.stringify(diagnostics)}`,
);
}
}
function assertFixtureDiagnostics(fixture, diagnostics) {
const expectedBase = EXPECTED_DIAGNOSTICS[fixture];
if (!expectedBase) return;
const expected = {...expectedBase};
const apiOverride = diagnosticOverride('FLUXER_WIN_GAME_CAPTURE_EXPECT_API_TYPE', fixture);
const transportOverride = diagnosticOverride('FLUXER_WIN_GAME_CAPTURE_EXPECT_TRANSPORT', fixture);
const fallbackOverride = diagnosticOverride('FLUXER_WIN_GAME_CAPTURE_EXPECT_FALLBACK_REASON', fixture);
if (apiOverride !== undefined) expected.apiType = apiOverride;
if (transportOverride !== undefined) {
expected.transport = transportOverride;
delete expected.transportOneOf;
expected.requiresDxgiFormat = expected.transport === TRANSPORT_SHARED_TEXTURE;
}
if (fallbackOverride !== undefined) {
expected.fallbackReason = fallbackOverride;
delete expected.fallbackReasonOneOf;
}
assertEqualDiagnostic(fixture, diagnostics, 'apiType', expected.apiType);
if (expected.transportOneOf) {
assertOneOfDiagnostic(fixture, diagnostics, 'transport', expected.transportOneOf);
} else {
assertEqualDiagnostic(fixture, diagnostics, 'transport', expected.transport);
}
if (expected.fallbackReason !== undefined) {
assertEqualDiagnostic(fixture, diagnostics, 'fallbackReason', expected.fallbackReason);
} else if (expected.fallbackReasonOneOf) {
assertOneOfDiagnostic(fixture, diagnostics, 'fallbackReason', expected.fallbackReasonOneOf);
}
if (diagnostics?.activeStrategy !== 'game-hook') {
throw new Error(`${fixture} expected activeStrategy=game-hook, got ${JSON.stringify(diagnostics)}`);
}
if (
(expected.requiresDxgiFormat ||
(expected.requiresDxgiFormatWhenShared && diagnostics?.transport === TRANSPORT_SHARED_TEXTURE)) &&
Number(diagnostics?.dxgiFormat ?? 0) === 0
) {
throw new Error(`${fixture} expected a non-zero shared texture DXGI format, got ${JSON.stringify(diagnostics)}`);
}
}
async function runFixture(fixture) {
const exe = await buildFixture(fixture);
const args = ['--frames', '900', '--width', String(WIDTH), '--height', String(HEIGHT), '--windowed'];
if (fixture === 'vulkan-present-fixture') args.push('--resize-at', '120');
args.push(...extraFixtureArgs(fixture));
const child = spawn(exe, args, {
cwd: dirname(exe),
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: false,
});
let started = false;
try {
const hwnd = await waitForHwnd(child, fixture);
const screenCapture = new capture.ScreenCapture({
sourceId: `window:${hwnd}:0`,
sourceKind: 'game',
width: WIDTH,
height: HEIGHT,
frameRate: FRAME_RATE,
injectionMethod: process.env.FLUXER_WIN_GAME_CAPTURE_INJECTION_METHOD || 'auto',
});
let result;
try {
result = await screenCapture.start();
started = true;
const observed = await waitForAdvancingFrames(screenCapture, fixture);
assertFixtureDiagnostics(fixture, observed.diagnostics);
console.log(
`[fixture-smoke] PASS ${fixture}: start=${JSON.stringify(result)} frames=${observed.frameCount} signatures=${observed.signatures} diagnostics=${JSON.stringify(observed.diagnostics)}`,
);
} finally {
if (started) await screenCapture.stop().catch(() => {});
}
} finally {
if (!child.killed) child.kill();
}
}
async function main() {
if (process.platform !== 'win32') {
console.log('[fixture-smoke] SKIP: Windows game-capture fixtures only run on Windows');
return 0;
}
if (!capture.isSupported()) {
throw new Error(`win-game-capture binding unavailable: ${capture.loadError?.message ?? 'unknown error'}`);
}
for (const fixture of selectedFixtures()) {
await runFixture(fixture);
}
return 0;
}
try {
process.exitCode = await main();
} catch (error) {
console.error(`[fixture-smoke] FAIL: ${error.stack ?? error.message}`);
process.exitCode = 1;
}
@@ -0,0 +1,610 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
#[cfg(target_os = "windows")]
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InjectionPolicy {
Allow,
ForceCpuReadback,
Deny(String),
}
const HARD_DENY_PROCESS_NAMES: &[(&str, &str)] = &[
("easyanticheat.exe", "EasyAntiCheat"),
("easyanticheat_eos.exe", "EasyAntiCheat"),
("easyanticheat_launcher.exe", "EasyAntiCheat"),
("eac.exe", "EasyAntiCheat"),
("eac_launcher.exe", "EasyAntiCheat"),
("beservice.exe", "BattlEye"),
("beservice_x64.exe", "BattlEye"),
("bedaisy.exe", "BattlEye"),
("be_service.exe", "BattlEye"),
("vgc.exe", "Riot Vanguard"),
("vgk.exe", "Riot Vanguard"),
("vgtray.exe", "Riot Vanguard"),
("vanguard.exe", "Riot Vanguard"),
("destiny2.exe", "Destiny 2 anti-cheat policy"),
("equ8.exe", "EQU8 anti-cheat"),
("equ8_service.exe", "EQU8 anti-cheat"),
("gameguard.des", "nProtect GameGuard"),
("gamemon.des", "nProtect GameGuard"),
("gamemon64.des", "nProtect GameGuard"),
("npggnt.des", "nProtect GameGuard"),
("xigncode.exe", "XIGNCODE"),
("xigncode3.exe", "XIGNCODE3"),
("mhyprot.exe", "mhyprot anti-cheat"),
("mhyprot2.exe", "mhyprot anti-cheat"),
("anticheatexpert.exe", "Anti-Cheat Expert"),
("ace-base.exe", "Anti-Cheat Expert"),
("faceit.exe", "FACEIT Anti-cheat"),
("faceitclient.exe", "FACEIT Anti-cheat"),
("faceitservice.exe", "FACEIT Anti-cheat"),
("esea.exe", "ESEA Anti-cheat"),
("eseaclient.exe", "ESEA Anti-cheat"),
("eseaservice.exe", "ESEA Anti-cheat"),
("punkbuster.exe", "PunkBuster"),
("pnkbstra.exe", "PunkBuster"),
("pnkbstrb.exe", "PunkBuster"),
("system", "Windows kernel process"),
("csrss.exe", "Windows system process"),
("smss.exe", "Windows system process"),
("wininit.exe", "Windows system process"),
("winlogon.exe", "Windows system process"),
("services.exe", "Windows system process"),
("svchost.exe", "Windows service host"),
("dwm.exe", "Windows compositor"),
("fontdrvhost.exe", "Windows font driver host"),
("logonui.exe", "Windows secure desktop"),
("consent.exe", "Windows secure desktop"),
("secureuxhost.exe", "Windows secure desktop"),
("lsass.exe", "Windows security process"),
("lsaiso.exe", "Windows security process"),
("msmpeng.exe", "Microsoft Defender"),
("securityhealthservice.exe", "Windows Security"),
("securityhealthsystray.exe", "Windows Security"),
("audiodg.exe", "Windows protected audio graph"),
("wudfhost.exe", "Windows driver host"),
("taskhostw.exe", "Windows task host"),
("dllhost.exe", "Windows COM surrogate"),
("runtimebroker.exe", "Windows runtime broker"),
("applicationframehost.exe", "Windows application frame host"),
("lockapp.exe", "Windows lock screen"),
("sihost.exe", "Windows shell infrastructure"),
("startmenuexperiencehost.exe", "Windows shell"),
("searchhost.exe", "Windows shell"),
("searchapp.exe", "Windows shell"),
("textinputhost.exe", "Windows shell"),
("explorer.exe", "Windows shell"),
("taskmgr.exe", "Windows administrative tool"),
("regedit.exe", "Windows administrative tool"),
("mmc.exe", "Windows administrative tool"),
("obs32.exe", "capture application"),
("obs64.exe", "capture application"),
("fluxer.exe", "Fluxer application"),
("fluxer-desktop.exe", "Fluxer application"),
("fluxer_desktop.exe", "Fluxer application"),
];
const COMPATIBILITY_DENY_PROCESS_NAMES: &[(&str, &str)] = &[
("gta-sa.exe", "legacy D3D8/RenderWare compatibility"),
("samp.exe", "legacy D3D8/RenderWare compatibility"),
("leagueclientux.exe", "League of Legends launcher"),
("steamwebhelper.exe", "Chromium-based launcher"),
("epicgameslauncher.exe", "Chromium-based launcher"),
("riotclientux.exe", "Riot client"),
("riotclientservices.exe", "Riot client"),
("battle.net.exe", "Chromium-based launcher"),
("gamingservices.exe", "Xbox Gaming Services"),
("gamingservicesnet.exe", "Xbox Gaming Services"),
];
const COMPATIBILITY_DENY_WINDOW_CLASSES: &[(&str, &str)] = &[
("chrome_widgetwin_0", "Chromium-based game window"),
("chrome_widgetwin_1", "Chromium-based game window"),
(
"gamingservicesui_hosting_window_class",
"Xbox Gaming Services",
),
];
const FORCE_CPU_PROCESS_NAMES: &[&str] = &["terraria.exe"];
const OVERRIDE_FILE_NAME: &str = "compatibility.json";
pub fn injection_policy(target_pid: u32) -> InjectionPolicy {
let exe_name = match target_process_exe_name(target_pid) {
Some(name) => name,
None => {
return InjectionPolicy::Allow;
}
};
evaluate_policy(&exe_name, None, load_override())
}
#[cfg(target_os = "windows")]
pub fn injection_policy_for_window(
target_pid: u32,
hwnd: windows_sys::Win32::Foundation::HWND,
) -> InjectionPolicy {
let exe_name = match target_process_exe_name(target_pid) {
Some(name) => name,
None => {
return InjectionPolicy::Allow;
}
};
evaluate_policy(
&exe_name,
target_window_class_name(hwnd).as_deref(),
load_override(),
)
}
fn evaluate_policy(
exe_name: &str,
window_class: Option<&str>,
override_lists: Option<OverrideLists>,
) -> InjectionPolicy {
let exe_name_lower = file_name_lower(exe_name);
let exe_name_lower = exe_name_lower.as_str();
let window_class_lower = window_class.map(|name| name.trim().to_ascii_lowercase());
let window_class_lower = window_class_lower.as_deref();
if let Some(lists) = override_lists.as_ref() {
if let Some(reason) = embedded_hard_deny_reason(exe_name_lower) {
return reason;
}
if lists.deny.iter().any(|name| name == exe_name_lower) {
return InjectionPolicy::Deny(format!(
"{exe_name_lower} is on the local compatibility deny list; Fluxer will not inject \
its game-capture hook"
));
}
let allowed_by_override = lists.allow.iter().any(|name| name == exe_name_lower);
if !allowed_by_override
&& let Some(reason) = embedded_compatibility_deny_reason(exe_name_lower)
{
return reason;
}
if !allowed_by_override
&& let Some(reason) = embedded_window_class_deny_reason(window_class_lower)
{
return reason;
}
if lists.force_cpu.iter().any(|name| name == exe_name_lower) {
return InjectionPolicy::ForceCpuReadback;
}
if allowed_by_override {
return InjectionPolicy::Allow;
}
} else {
if let Some(reason) = embedded_hard_deny_reason(exe_name_lower) {
return reason;
}
if let Some(reason) = embedded_compatibility_deny_reason(exe_name_lower) {
return reason;
}
if let Some(reason) = embedded_window_class_deny_reason(window_class_lower) {
return reason;
}
}
if FORCE_CPU_PROCESS_NAMES.contains(&exe_name_lower) {
return InjectionPolicy::ForceCpuReadback;
}
InjectionPolicy::Allow
}
fn embedded_hard_deny_reason(exe_name_lower: &str) -> Option<InjectionPolicy> {
HARD_DENY_PROCESS_NAMES
.iter()
.find(|(name, _)| *name == exe_name_lower)
.map(|(_, label)| {
InjectionPolicy::Deny(format!(
"{exe_name_lower} is protected by {label}; Fluxer will not inject its game-capture \
hook into anti-cheat or security-sensitive processes"
))
})
}
fn embedded_compatibility_deny_reason(exe_name_lower: &str) -> Option<InjectionPolicy> {
COMPATIBILITY_DENY_PROCESS_NAMES
.iter()
.find(|(name, _)| *name == exe_name_lower)
.map(|(_, label)| {
InjectionPolicy::Deny(format!(
"{exe_name_lower} has known game-capture compatibility issues ({label}); Fluxer \
will not inject its game-capture hook by default"
))
})
}
fn embedded_window_class_deny_reason(window_class_lower: Option<&str>) -> Option<InjectionPolicy> {
let window_class_lower = window_class_lower?;
COMPATIBILITY_DENY_WINDOW_CLASSES
.iter()
.find(|(name, _)| *name == window_class_lower)
.map(|(_, label)| {
InjectionPolicy::Deny(format!(
"window class {window_class_lower} has known OBS game-capture compatibility issues \
({label}); Fluxer will not inject its game-capture hook by default"
))
})
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct OverrideLists {
deny: Vec<String>,
allow: Vec<String>,
force_cpu: Vec<String>,
}
impl OverrideLists {
fn is_empty(&self) -> bool {
self.deny.is_empty() && self.allow.is_empty() && self.force_cpu.is_empty()
}
}
fn load_override() -> Option<OverrideLists> {
let path = override_file_path()?;
let contents = std::fs::read_to_string(&path).ok()?;
let lists = parse_override_json(&contents);
if lists.is_empty() { None } else { Some(lists) }
}
fn override_file_path() -> Option<PathBuf> {
if let Some(dir) = addon_directory() {
let candidate = dir.join(OVERRIDE_FILE_NAME);
if candidate.is_file() {
return Some(candidate);
}
}
let exe_dir = std::env::current_exe().ok()?.parent()?.to_path_buf();
let candidate = exe_dir.join(OVERRIDE_FILE_NAME);
if candidate.is_file() {
Some(candidate)
} else {
None
}
}
#[cfg(target_os = "windows")]
fn addon_directory() -> Option<PathBuf> {
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::Foundation::HMODULE;
use windows_sys::Win32::System::LibraryLoader::{
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
GetModuleFileNameW, GetModuleHandleExW,
};
let mut module: HMODULE = std::ptr::null_mut();
let ok = unsafe {
GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
addon_directory as *const u16,
&mut module,
)
};
if ok == 0 || module.is_null() {
return None;
}
let mut buffer = vec![0u16; 1024];
let len = unsafe { GetModuleFileNameW(module, buffer.as_mut_ptr(), buffer.len() as u32) };
if len == 0 || len as usize >= buffer.len() {
return None;
}
buffer.truncate(len as usize);
let module_path = PathBuf::from(std::ffi::OsString::from_wide(&buffer));
module_path.parent().map(Path::to_path_buf)
}
#[cfg(not(target_os = "windows"))]
fn addon_directory() -> Option<PathBuf> {
None
}
#[cfg(target_os = "windows")]
fn target_process_exe_name(pid: u32) -> Option<String> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
QueryFullProcessImageNameW,
};
if pid == 0 {
return None;
}
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
return None;
}
let mut buffer = vec![0u16; 1024];
let mut size = buffer.len() as u32;
let ok = unsafe {
QueryFullProcessImageNameW(handle, PROCESS_NAME_WIN32, buffer.as_mut_ptr(), &mut size)
};
unsafe {
CloseHandle(handle);
}
if ok == 0 || size == 0 || size as usize > buffer.len() {
return None;
}
let full_path: String = String::from_utf16_lossy(&buffer[..size as usize]);
Some(file_name_lower(&full_path))
}
#[cfg(target_os = "windows")]
fn target_window_class_name(hwnd: windows_sys::Win32::Foundation::HWND) -> Option<String> {
use windows_sys::Win32::UI::WindowsAndMessaging::GetClassNameW;
if hwnd.is_null() {
return None;
}
let mut buffer = vec![0u16; 256];
let len = unsafe { GetClassNameW(hwnd, buffer.as_mut_ptr(), buffer.len() as i32) };
if len <= 0 {
return None;
}
buffer.truncate(len as usize);
Some(
String::from_utf16_lossy(&buffer)
.trim()
.to_ascii_lowercase(),
)
}
#[cfg(not(target_os = "windows"))]
fn target_process_exe_name(_pid: u32) -> Option<String> {
None
}
fn file_name_lower(path: &str) -> String {
path.rsplit(['\\', '/'])
.next()
.unwrap_or(path)
.trim()
.to_ascii_lowercase()
}
fn parse_override_json(text: &str) -> OverrideLists {
let mut force_cpu = extract_string_array(text, "forceCpu");
force_cpu.extend(extract_string_array(text, "force_cpu"));
OverrideLists {
deny: extract_string_array(text, "deny"),
allow: extract_string_array(text, "allow"),
force_cpu,
}
}
fn extract_string_array(text: &str, key: &str) -> Vec<String> {
let needle = format!("\"{key}\"");
let mut search_from = 0usize;
while let Some(rel) = text[search_from..].find(&needle) {
let key_pos = search_from + rel;
let after_key = key_pos + needle.len();
let rest = text[after_key..].trim_start();
if let Some(rest) = rest.strip_prefix(':') {
let rest = rest.trim_start();
if let Some(array_body) = rest.strip_prefix('[')
&& let Some(end) = array_body.find(']')
{
return parse_json_string_list(&array_body[..end]);
}
}
search_from = after_key;
}
Vec::new()
}
fn parse_json_string_list(body: &str) -> Vec<String> {
let mut out = Vec::new();
let mut chars = body.char_indices().peekable();
while let Some((_, ch)) = chars.next() {
if ch != '"' {
continue;
}
let mut value = String::new();
let mut closed = false;
while let Some((_, c)) = chars.next() {
match c {
'\\' => {
if let Some((_, escaped)) = chars.next() {
match escaped {
'n' => value.push('\n'),
't' => value.push('\t'),
'r' => value.push('\r'),
other => value.push(other),
}
}
}
'"' => {
closed = true;
break;
}
other => value.push(other),
}
}
if closed {
let normalised = file_name_lower(&value);
if !normalised.is_empty() {
out.push(normalised);
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_deny_matches_anticheat() {
match evaluate_policy("easyanticheat.exe", None, None) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("EasyAntiCheat")),
other => panic!("expected Deny, got {other:?}"),
}
match evaluate_policy("beservice.exe", None, None) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("BattlEye")),
other => panic!("expected Deny, got {other:?}"),
}
match evaluate_policy("vgc.exe", None, None) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("Vanguard")),
other => panic!("expected Deny, got {other:?}"),
}
match evaluate_policy("destiny2.exe", None, None) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("Destiny 2")),
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn embedded_deny_matches_security_sensitive_windows_processes() {
for name in [
"lsass.exe",
"dwm.exe",
"explorer.exe",
"applicationframehost.exe",
"obs64.exe",
"fluxer-desktop.exe",
] {
match evaluate_policy(name, None, None) {
InjectionPolicy::Deny(reason) => {
assert!(reason.contains("security-sensitive") || reason.contains("protected"))
}
other => panic!("expected Deny for {name}, got {other:?}"),
}
}
}
#[test]
fn unknown_process_is_allowed() {
assert_eq!(
evaluate_policy("mygame.exe", None, None),
InjectionPolicy::Allow
);
}
#[test]
fn override_deny_wins() {
let lists = parse_override_json(r#"{ "deny": ["MyGame.exe"] }"#);
match evaluate_policy("mygame.exe", None, Some(lists)) {
InjectionPolicy::Deny(_) => {}
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn override_allow_cannot_unblock_hard_deny() {
let lists = parse_override_json(r#"{ "allow": ["easyanticheat.exe"] }"#);
match evaluate_policy("easyanticheat.exe", None, Some(lists)) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("EasyAntiCheat")),
other => panic!("expected hard Deny, got {other:?}"),
}
}
#[test]
fn override_allow_unblocks_soft_compatibility_deny() {
let lists = parse_override_json(r#"{ "allow": ["LeagueClientUx.exe"] }"#);
assert_eq!(
evaluate_policy("LeagueClientUx.exe", None, Some(lists)),
InjectionPolicy::Allow
);
}
#[test]
fn override_allow_plus_force_cpu_unblocks_soft_deny_with_cpu_readback() {
let lists = parse_override_json(
r#"{ "allow": ["LeagueClientUx.exe"], "forceCpu": ["LeagueClientUx.exe"] }"#,
);
assert_eq!(
evaluate_policy("LeagueClientUx.exe", None, Some(lists)),
InjectionPolicy::ForceCpuReadback
);
}
#[test]
fn override_force_cpu_applies() {
let lists = parse_override_json(r#"{ "forceCpu": ["weird.exe"] }"#);
assert_eq!(
evaluate_policy("weird.exe", None, Some(lists)),
InjectionPolicy::ForceCpuReadback
);
}
#[test]
fn embedded_force_cpu_applies_for_known_cross_adapter_case() {
assert_eq!(
evaluate_policy("Terraria.exe", None, None),
InjectionPolicy::ForceCpuReadback
);
}
#[test]
fn local_force_cpu_cannot_override_hard_deny() {
let lists = parse_override_json(r#"{ "forceCpu": ["lsass.exe"] }"#);
match evaluate_policy("lsass.exe", None, Some(lists)) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("security-sensitive")),
other => panic!("expected hard Deny, got {other:?}"),
}
}
#[test]
fn force_cpu_snake_case_alias_parses() {
let lists = parse_override_json(r#"{ "force_cpu": ["weird.exe"] }"#);
assert!(lists.force_cpu.contains(&"weird.exe".to_string()));
}
#[test]
fn embedded_window_class_deny_matches_obs_chromium_game_windows() {
match evaluate_policy("game.exe", Some("Chrome_WidgetWin_1"), None) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("Chromium")),
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn embedded_window_class_deny_matches_xbox_gaming_services() {
match evaluate_policy(
"game.exe",
Some("GamingServicesUI_Hosting_Window_Class"),
None,
) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("Xbox Gaming Services")),
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn override_allow_unblocks_soft_window_class_deny() {
let lists = parse_override_json(r#"{ "allow": ["game.exe"] }"#);
assert_eq!(
evaluate_policy("game.exe", Some("Chrome_WidgetWin_0"), Some(lists)),
InjectionPolicy::Allow
);
}
#[test]
fn parser_normalises_paths_and_ignores_garbage() {
let lists = parse_override_json(
r#"{ "deny": ["C:\\Games\\Foo\\Foo.exe", "/opt/bar/Bar.EXE", 123, null] }"#,
);
assert_eq!(lists.deny, vec!["foo.exe", "bar.exe"]);
}
#[test]
fn malformed_json_yields_empty() {
let lists = parse_override_json("not json at all");
assert!(lists.is_empty());
}
#[test]
fn file_name_lower_handles_both_separators() {
assert_eq!(file_name_lower("C:\\A\\B\\Game.EXE"), "game.exe");
assert_eq!(file_name_lower("/a/b/Game.EXE"), "game.exe");
assert_eq!(file_name_lower("bare.exe"), "bare.exe");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,523 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
use parking_lot::{Condvar, Mutex};
use fluxer_encoder_ring::d3d11::D3D11SharedHandle;
use fluxer_encoder_ring::{
D3D11KeyedMutexBackend, EncoderFrameRate, EncoderInputRing, EncoderReady, RING_SIZE, RingError,
TextureFormat,
};
#[cfg(target_os = "windows")]
use fluxer_encoder_ring::{EncoderDims, FillReservation, NvencD3D11Handoff};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Direct3D11::{ID3D11DeviceContext, ID3D11Texture2D};
pub const READY_WAIT_TIMEOUT_CAP: Duration = Duration::from_millis(17);
const EVICTIONS_PER_RESERVE_MAX: u64 = 2;
#[cfg(target_os = "windows")]
const NVENC_BITRATE_BPS_DEFAULT: u32 = 8_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncoderAttachError {
AlreadyAttached,
InvalidDimensions { width: u32, height: u32 },
RingInitFailed,
NotAttached,
BlitFailed,
SlotUnavailable,
DeviceUnavailable,
}
impl std::fmt::Display for EncoderAttachError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AlreadyAttached => write!(f, "encoder ring already attached"),
Self::InvalidDimensions { width, height } => {
write!(f, "encoder ring invalid dimensions {width}x{height}")
}
Self::RingInitFailed => write!(f, "encoder ring initialise failed"),
Self::NotAttached => write!(f, "encoder ring not attached"),
Self::BlitFailed => write!(f, "blit into ring slot failed"),
Self::SlotUnavailable => write!(f, "no ring slot texture available"),
Self::DeviceUnavailable => write!(f, "ring backend device unavailable"),
}
}
}
impl std::error::Error for EncoderAttachError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EncoderAttachStats {
pub frames_submitted: u64,
pub frames_dropped: u64,
pub ring_full_events: u64,
pub failed_blits: u64,
}
pub struct EncoderAttachment {
ring: Mutex<EncoderInputRing<D3D11KeyedMutexBackend>>,
ready_condvar: Condvar,
attached: AtomicBool,
frames_submitted: AtomicU64,
frames_dropped: AtomicU64,
ring_full_events: AtomicU64,
failed_blits: AtomicU64,
width: u32,
height: u32,
frame_rate: EncoderFrameRate,
#[cfg(target_os = "windows")]
context: ID3D11DeviceContext,
#[cfg(target_os = "windows")]
nvenc: Mutex<Option<NvencD3D11Handoff>>,
}
impl EncoderAttachment {
pub fn try_new(width: u32, height: u32) -> Result<Arc<Self>, EncoderAttachError> {
Self::try_new_with_frame_rate(width, height, EncoderFrameRate::default())
}
pub fn try_new_with_frame_rate(
width: u32,
height: u32,
frame_rate: EncoderFrameRate,
) -> Result<Arc<Self>, EncoderAttachError> {
if width == 0 || height == 0 {
return Err(EncoderAttachError::InvalidDimensions { width, height });
}
assert!(frame_rate.numerator > 0, "frame rate numerator positive");
assert!(
frame_rate.denominator > 0,
"frame rate denominator positive"
);
let backend =
D3D11KeyedMutexBackend::new().map_err(|_| EncoderAttachError::RingInitFailed)?;
let mut ring = EncoderInputRing::new(backend);
ring.initialise(width, height, TextureFormat::Nv12)
.map_err(|_| EncoderAttachError::RingInitFailed)?;
#[cfg(target_os = "windows")]
let context = ring
.backend_mut()
.context()
.ok_or(EncoderAttachError::DeviceUnavailable)?;
#[cfg(target_os = "windows")]
let nvenc = init_nvenc_pre_registered(&mut ring, width, height, frame_rate);
let attachment = Self {
ring: Mutex::new(ring),
ready_condvar: Condvar::new(),
attached: AtomicBool::new(true),
frames_submitted: AtomicU64::new(0),
frames_dropped: AtomicU64::new(0),
ring_full_events: AtomicU64::new(0),
failed_blits: AtomicU64::new(0),
width,
height,
frame_rate,
#[cfg(target_os = "windows")]
context,
#[cfg(target_os = "windows")]
nvenc: Mutex::new(nvenc),
};
assert!(
attachment.attached.load(Ordering::Acquire),
"attachment is attached"
);
assert!(attachment.width > 0, "attachment width positive");
assert!(
attachment.frame_rate.numerator > 0,
"attachment fps positive"
);
Ok(Arc::new(attachment))
}
pub fn width(&self) -> u32 {
let w = self.width;
assert!(w > 0, "attachment width positive");
assert!(self.height > 0, "attachment height positive");
w
}
pub fn height(&self) -> u32 {
let h = self.height;
assert!(h > 0, "attachment height positive");
assert!(self.width > 0, "attachment width positive");
h
}
pub fn frame_rate(&self) -> EncoderFrameRate {
let rate = self.frame_rate;
assert!(rate.numerator > 0, "attachment fps numerator positive");
assert!(rate.denominator > 0, "attachment fps denominator positive");
rate
}
pub fn is_attached(&self) -> bool {
let a = self.attached.load(Ordering::Acquire);
assert!(self.width > 0, "width intact while reading attached");
assert!(self.height > 0, "height intact while reading attached");
a
}
pub fn detach(&self) {
self.attached.store(false, Ordering::Release);
self.ready_condvar.notify_all();
assert!(
!self.attached.load(Ordering::Acquire),
"detach flag cleared"
);
assert!(self.width > 0, "attachment intact across detach");
}
pub fn capacity(&self) -> usize {
let cap = RING_SIZE;
assert!(cap > 0, "ring capacity positive");
assert_eq!(cap, 8, "ring capacity matches RING_SIZE");
cap
}
pub fn note_ring_full(&self) {
self.ring_full_events.fetch_add(1, Ordering::Relaxed);
}
#[cfg(target_os = "windows")]
pub fn nvenc_pre_registered(&self) -> bool {
let registered = self.nvenc.lock().is_some();
assert!(self.width > 0, "width intact while reading nvenc state");
assert!(self.height > 0, "height intact while reading nvenc state");
registered
}
pub fn stats(&self) -> EncoderAttachStats {
let stats = EncoderAttachStats {
frames_submitted: self.frames_submitted.load(Ordering::Relaxed),
frames_dropped: self.frames_dropped.load(Ordering::Relaxed),
ring_full_events: self.ring_full_events.load(Ordering::Relaxed),
failed_blits: self.failed_blits.load(Ordering::Relaxed),
};
assert!(
stats.frames_submitted <= u64::MAX / 2,
"submitted plausible"
);
assert!(stats.frames_dropped <= u64::MAX / 2, "dropped plausible");
stats
}
fn account_evictions(&self, evicted_delta: u64) {
assert!(
evicted_delta <= EVICTIONS_PER_RESERVE_MAX,
"evictions bounded per reserve"
);
assert!(self.width > 0, "attachment intact while accounting");
if evicted_delta == 0 {
return;
}
self.frames_dropped
.fetch_add(evicted_delta, Ordering::Relaxed);
self.ring_full_events
.fetch_add(evicted_delta, Ordering::Relaxed);
}
pub fn submit_notify(&self) -> Result<(), EncoderAttachError> {
if !self.attached.load(Ordering::Acquire) {
return Err(EncoderAttachError::NotAttached);
}
let mut ring = self.ring.lock();
let pre_dropped = ring.dropped_count();
let result: Result<(), RingError> = ring.submit_skip_oldest(|_handle| {});
let evicted_delta = ring.dropped_count().saturating_sub(pre_dropped);
drop(ring);
self.account_evictions(evicted_delta);
match result {
Ok(()) => {
self.frames_submitted.fetch_add(1, Ordering::Relaxed);
self.ready_condvar.notify_one();
Ok(())
}
Err(RingError::FullDropped { .. }) => Ok(()),
Err(_) => Err(EncoderAttachError::RingInitFailed),
}
}
pub fn wait_next_ready(&self, timeout: Duration) -> Option<EncoderReady<D3D11SharedHandle>> {
assert!(self.width > 0, "attachment width positive");
assert!(self.height > 0, "attachment height positive");
if !self.attached.load(Ordering::Acquire) {
return None;
}
let capped_timeout = timeout.min(READY_WAIT_TIMEOUT_CAP);
let mut ring = self.ring.lock();
if let Some(ready) = ring.poll_next_ready() {
return Some(ready);
}
let _ = self.ready_condvar.wait_for(&mut ring, capped_timeout);
if !self.attached.load(Ordering::Acquire) {
return None;
}
ring.poll_next_ready()
}
#[cfg(target_os = "windows")]
fn cancel_reservation(&self, reservation: FillReservation<D3D11SharedHandle>) {
assert!(
(reservation.slot_index() as usize) < RING_SIZE,
"cancelled slot within ring"
);
assert!(self.width > 0, "attachment intact while cancelling");
let mut ring = self.ring.lock();
let _ = ring.cancel(reservation);
}
#[cfg(target_os = "windows")]
pub fn submit_capture_frame_with_blit(
&self,
capture_texture: &ID3D11Texture2D,
capture_width: u32,
capture_height: u32,
) -> Result<(), EncoderAttachError> {
assert!(capture_width > 0, "capture width positive");
assert!(capture_height > 0, "capture height positive");
assert!(self.width > 0, "attachment width positive");
assert!(self.height > 0, "attachment height positive");
if !self.attached.load(Ordering::Acquire) {
return Err(EncoderAttachError::NotAttached);
}
let mut ring = self.ring.lock();
let pre_dropped = ring.dropped_count();
let reserved = ring.reserve_skip_oldest();
let evicted_delta = ring.dropped_count().saturating_sub(pre_dropped);
let dest_texture = match &reserved {
Ok(reservation) => ring
.backend_mut()
.texture_for_slot(reservation.slot_index()),
Err(_) => None,
};
drop(ring);
self.account_evictions(evicted_delta);
let reservation = match reserved {
Ok(reservation) => reservation,
Err(RingError::FullDropped { .. }) => return Ok(()),
Err(_) => return Err(EncoderAttachError::RingInitFailed),
};
let Some(dest_texture) = dest_texture else {
self.cancel_reservation(reservation);
self.failed_blits.fetch_add(1, Ordering::Relaxed);
return Err(EncoderAttachError::BlitFailed);
};
unsafe {
self.context.CopyResource(&dest_texture, capture_texture);
}
let mut ring = self.ring.lock();
match ring.commit(reservation) {
Ok(sequence) => {
drop(ring);
assert!(sequence > 0, "committed sequence positive");
self.frames_submitted.fetch_add(1, Ordering::Relaxed);
self.ready_condvar.notify_one();
Ok(())
}
Err(_) => Err(EncoderAttachError::RingInitFailed),
}
}
}
#[cfg(target_os = "windows")]
fn collect_ring_slot_handles(
ring: &mut EncoderInputRing<D3D11KeyedMutexBackend>,
) -> Option<Vec<D3D11SharedHandle>> {
assert_eq!(
ring.free_count(),
RING_SIZE,
"collect requires a fresh ring"
);
let mut reservations: Vec<FillReservation<D3D11SharedHandle>> = Vec::with_capacity(RING_SIZE);
let mut handles: Vec<D3D11SharedHandle> = Vec::with_capacity(RING_SIZE);
for _ in 0..RING_SIZE {
let Ok(reservation) = ring.reserve() else {
break;
};
handles.push(reservation.handle.clone());
reservations.push(reservation);
}
let mut all_cancelled = true;
for reservation in reservations.drain(..) {
if ring.cancel(reservation).is_err() {
all_cancelled = false;
}
}
if !all_cancelled {
return None;
}
assert_eq!(ring.free_count(), RING_SIZE, "all reservations returned");
assert!(handles.len() <= RING_SIZE, "collected handles bounded");
if handles.len() == RING_SIZE {
Some(handles)
} else {
None
}
}
#[cfg(target_os = "windows")]
fn init_nvenc_pre_registered(
ring: &mut EncoderInputRing<D3D11KeyedMutexBackend>,
width: u32,
height: u32,
frame_rate: EncoderFrameRate,
) -> Option<NvencD3D11Handoff> {
assert!(width > 0, "nvenc init width positive");
assert!(height > 0, "nvenc init height positive");
assert!(frame_rate.numerator > 0, "nvenc frame rate positive");
let device = ring.backend_mut().device()?;
let handles = collect_ring_slot_handles(ring)?;
assert_eq!(handles.len(), RING_SIZE, "pre-register covers whole ring");
let dims = EncoderDims::new(width, height);
let mut handoff =
NvencD3D11Handoff::new_with_frame_rate(device, dims, NVENC_BITRATE_BPS_DEFAULT, frame_rate)
.ok()?;
handoff.pre_register_slots(&handles, dims).ok()?;
Some(handoff)
}
impl Drop for EncoderAttachment {
fn drop(&mut self) {
self.attached.store(false, Ordering::Release);
self.ready_condvar.notify_all();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_zero_dimensions() {
let err = EncoderAttachment::try_new(0, 720).err();
assert!(matches!(
err,
Some(EncoderAttachError::InvalidDimensions { .. })
));
let err2 = EncoderAttachment::try_new(640, 0).err();
assert!(matches!(
err2,
Some(EncoderAttachError::InvalidDimensions { .. })
));
}
#[test]
fn ready_wait_timeout_cap_is_one_frame_interval() {
assert!(READY_WAIT_TIMEOUT_CAP >= Duration::from_millis(1));
assert!(READY_WAIT_TIMEOUT_CAP <= Duration::from_millis(33));
}
#[cfg(not(target_os = "windows"))]
#[test]
fn off_windows_init_fails() {
let err = EncoderAttachment::try_new(640, 480).err();
assert!(matches!(err, Some(EncoderAttachError::RingInitFailed)));
}
#[cfg(target_os = "windows")]
#[test]
fn windows_attach_then_detach_smoke() {
let attach = match EncoderAttachment::try_new(640, 480) {
Ok(a) => a,
Err(_) => return,
};
assert!(attach.is_attached());
assert_eq!(attach.width(), 640);
assert_eq!(attach.height(), 480);
assert_eq!(attach.capacity(), 8);
let stats = attach.stats();
assert_eq!(stats.frames_submitted, 0, "fresh attach has 0 submissions");
assert_eq!(stats.failed_blits, 0, "fresh attach has 0 failed blits");
attach.detach();
assert!(!attach.is_attached());
}
#[cfg(target_os = "windows")]
#[test]
fn windows_submit_notify_records_stats() {
let attach = match EncoderAttachment::try_new(64, 64) {
Ok(a) => a,
Err(_) => return,
};
for _ in 0..3 {
attach.submit_notify().expect("notify ok");
}
let stats = attach.stats();
assert_eq!(stats.frames_submitted, 3);
assert_eq!(stats.frames_dropped, 0);
assert_eq!(stats.ring_full_events, 0);
}
#[cfg(target_os = "windows")]
#[test]
fn windows_submit_rejected_when_detached() {
let attach = match EncoderAttachment::try_new(64, 64) {
Ok(a) => a,
Err(_) => return,
};
attach.detach();
let err = attach.submit_notify().err();
assert!(matches!(err, Some(EncoderAttachError::NotAttached)));
}
#[cfg(target_os = "windows")]
#[test]
fn windows_wait_next_ready_returns_committed_frame() {
let attach = match EncoderAttachment::try_new(64, 64) {
Ok(a) => a,
Err(_) => return,
};
attach.submit_notify().expect("notify ok");
let ready = attach.wait_next_ready(Duration::from_millis(5));
let ready = ready.expect("committed frame becomes ready");
assert_eq!(ready.sequence, 1);
assert_eq!(ready.duplicate_count, 0);
}
#[cfg(target_os = "windows")]
#[test]
fn windows_wait_next_ready_empty_ring_times_out() {
let attach = match EncoderAttachment::try_new(64, 64) {
Ok(a) => a,
Err(_) => return,
};
let ready = attach.wait_next_ready(Duration::from_millis(2));
assert!(ready.is_none());
assert!(attach.is_attached());
}
#[cfg(target_os = "windows")]
#[test]
fn windows_wait_next_ready_after_detach_returns_none() {
let attach = match EncoderAttachment::try_new(64, 64) {
Ok(a) => a,
Err(_) => return,
};
attach.detach();
let ready = attach.wait_next_ready(Duration::from_millis(2));
assert!(ready.is_none());
assert!(!attach.is_attached());
}
#[cfg(target_os = "windows")]
#[test]
fn windows_ring_usable_after_nvenc_pre_register_attempt() {
let attach = match EncoderAttachment::try_new(64, 64) {
Ok(a) => a,
Err(_) => return,
};
let _ = attach.nvenc_pre_registered();
attach.submit_notify().expect("notify ok");
let stats = attach.stats();
assert_eq!(stats.frames_submitted, 1);
assert_eq!(stats.frames_dropped, 0);
}
}
@@ -0,0 +1,710 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
use crate::game_capture_abi::{
GAME_CAPTURE_FALLBACK_DEVICE_LOST, GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED,
GAME_CAPTURE_FALLBACK_FORCED_CPU, GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED,
GAME_CAPTURE_FALLBACK_MULTISAMPLED, GAME_CAPTURE_FALLBACK_NONE,
GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CaptureStrategy {
GameHook,
Wgc,
DxgiDuplication,
WindowGdi,
}
impl CaptureStrategy {
pub const RANKED: [CaptureStrategy; 4] = [
CaptureStrategy::GameHook,
CaptureStrategy::Wgc,
CaptureStrategy::DxgiDuplication,
CaptureStrategy::WindowGdi,
];
pub fn rank(self) -> u8 {
match self {
CaptureStrategy::GameHook => 0,
CaptureStrategy::Wgc => 1,
CaptureStrategy::DxgiDuplication => 2,
CaptureStrategy::WindowGdi => 3,
}
}
pub fn next_worse(self) -> Option<CaptureStrategy> {
match self {
CaptureStrategy::GameHook => Some(CaptureStrategy::Wgc),
CaptureStrategy::Wgc => Some(CaptureStrategy::DxgiDuplication),
CaptureStrategy::DxgiDuplication => Some(CaptureStrategy::WindowGdi),
CaptureStrategy::WindowGdi => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
CaptureStrategy::GameHook => "game-hook",
CaptureStrategy::Wgc => "wgc",
CaptureStrategy::DxgiDuplication => "dxgi-duplication",
CaptureStrategy::WindowGdi => "window-gdi",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FailureSignature {
InjectionFailed,
AntiCheatDenied,
NoFramesWithinTimeout,
DeviceLost,
UnsupportedTransport,
UnsupportedFormat,
WindowGone,
FramesRecovered,
}
impl FailureSignature {
pub fn from_fallback_reason(reason: u32) -> Option<FailureSignature> {
match reason {
GAME_CAPTURE_FALLBACK_NONE => None,
GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED
| GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED => {
Some(FailureSignature::UnsupportedTransport)
}
GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED | GAME_CAPTURE_FALLBACK_MULTISAMPLED => {
Some(FailureSignature::UnsupportedFormat)
}
GAME_CAPTURE_FALLBACK_DEVICE_LOST => Some(FailureSignature::DeviceLost),
GAME_CAPTURE_FALLBACK_FORCED_CPU => None,
_ => None,
}
}
fn is_terminal_for_all(self) -> bool {
matches!(self, FailureSignature::WindowGone)
}
fn permanently_disqualifies_current(self) -> bool {
matches!(
self,
FailureSignature::AntiCheatDenied
| FailureSignature::InjectionFailed
| FailureSignature::UnsupportedTransport
| FailureSignature::UnsupportedFormat
)
}
}
pub const MAX_DEVICE_LOST_RETRIES: u32 = 3;
pub const STABLE_FRAMES_BEFORE_UPGRADE: u32 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FallbackDecision {
Stay {
retry_in_place: bool,
},
Transition {
from: CaptureStrategy,
to: CaptureStrategy,
reason: String,
},
Upgrade {
from: CaptureStrategy,
to: CaptureStrategy,
reason: String,
},
GiveUp {
reason: String,
},
}
#[derive(Debug, Clone)]
pub struct FallbackRanker {
active: CaptureStrategy,
disqualified: Vec<CaptureStrategy>,
device_lost_streak: u32,
stable_frame_streak: u32,
last_fallback_reason: Option<String>,
}
impl FallbackRanker {
pub fn new(initial: CaptureStrategy) -> Self {
Self {
active: initial,
disqualified: Vec::new(),
device_lost_streak: 0,
stable_frame_streak: 0,
last_fallback_reason: None,
}
}
pub fn active(&self) -> CaptureStrategy {
self.active
}
pub fn last_fallback_reason(&self) -> Option<&str> {
self.last_fallback_reason.as_deref()
}
pub fn is_disqualified(&self, strategy: CaptureStrategy) -> bool {
self.disqualified.contains(&strategy)
}
fn best_available_above(&self) -> Option<CaptureStrategy> {
CaptureStrategy::RANKED.iter().copied().find(|candidate| {
candidate.rank() < self.active.rank() && !self.is_disqualified(*candidate)
})
}
fn next_available_below(&self) -> Option<CaptureStrategy> {
let mut candidate = self.active.next_worse();
while let Some(strategy) = candidate {
if !self.is_disqualified(strategy) {
return Some(strategy);
}
candidate = strategy.next_worse();
}
None
}
fn disqualify(&mut self, strategy: CaptureStrategy) {
if !self.disqualified.contains(&strategy) {
self.disqualified.push(strategy);
}
}
pub fn observe(&mut self, signature: FailureSignature) -> FallbackDecision {
if signature.is_terminal_for_all() {
return FallbackDecision::GiveUp {
reason: format!(
"the capture target is gone; no capture strategy can continue (was using \
{})",
self.active.as_str()
),
};
}
if signature == FailureSignature::FramesRecovered {
self.device_lost_streak = 0;
self.stable_frame_streak = self.stable_frame_streak.saturating_add(1);
if self.stable_frame_streak >= STABLE_FRAMES_BEFORE_UPGRADE
&& let Some(target) = self.best_available_above()
{
let from = self.active;
let reason = format!(
"{} has been stable for {} frames; attempting to upgrade back to the \
preferred {} strategy",
from.as_str(),
self.stable_frame_streak,
target.as_str()
);
self.active = target;
self.stable_frame_streak = 0;
self.last_fallback_reason = Some(reason.clone());
return FallbackDecision::Upgrade {
from,
to: target,
reason,
};
}
return FallbackDecision::Stay {
retry_in_place: false,
};
}
self.stable_frame_streak = 0;
if signature == FailureSignature::DeviceLost {
self.device_lost_streak = self.device_lost_streak.saturating_add(1);
if self.device_lost_streak <= MAX_DEVICE_LOST_RETRIES {
return FallbackDecision::Stay {
retry_in_place: true,
};
}
return self.fall_back(
signature,
&format!(
"{} kept losing its capture device ({} consecutive recoveries failed); falling \
back",
self.active.as_str(),
self.device_lost_streak
),
);
}
self.device_lost_streak = 0;
if signature.permanently_disqualifies_current() {
self.disqualify(self.active);
}
let reason = describe_failure(self.active, signature);
self.fall_back(signature, &reason)
}
fn fall_back(&mut self, signature: FailureSignature, reason: &str) -> FallbackDecision {
self.device_lost_streak = 0;
match self.next_available_below() {
Some(target) => {
let from = self.active;
let full = format!("{reason}; switching to {} capture", target.as_str());
self.active = target;
self.last_fallback_reason = Some(full.clone());
FallbackDecision::Transition {
from,
to: target,
reason: full,
}
}
None => {
let give_up = format!(
"{reason}, and no lower-ranked capture strategy is available ({} was the last \
resort, signature={:?})",
self.active.as_str(),
signature
);
self.last_fallback_reason = Some(give_up.clone());
FallbackDecision::GiveUp { reason: give_up }
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FallbackSnapshot {
pub active_strategy: String,
pub last_fallback_reason: String,
}
#[derive(Debug)]
pub struct FallbackTracker {
ranker: FallbackRanker,
}
impl FallbackTracker {
pub fn new(initial: CaptureStrategy) -> Self {
Self {
ranker: FallbackRanker::new(initial),
}
}
pub fn observe(&mut self, signature: FailureSignature) -> FallbackDecision {
self.ranker.observe(signature)
}
pub fn active(&self) -> CaptureStrategy {
self.ranker.active()
}
pub fn snapshot(&self) -> FallbackSnapshot {
FallbackSnapshot {
active_strategy: self.ranker.active().as_str().to_string(),
last_fallback_reason: self.ranker.last_fallback_reason().unwrap_or("").to_string(),
}
}
}
pub fn decision_lifecycle(decision: &FallbackDecision) -> (&'static str, String) {
match decision {
FallbackDecision::Stay { retry_in_place } => (
"diagnostic",
if *retry_in_place {
"capture hit a transient device loss; retrying the current strategy in place"
.to_string()
} else {
"capture is healthy on the current strategy".to_string()
},
),
FallbackDecision::Transition { from, to, reason } => (
"error",
format!(
"fallback: {} -> {} ({reason}) [next-strategy={}]",
from.as_str(),
to.as_str(),
to.as_str()
),
),
FallbackDecision::Upgrade { from, to, reason } => (
"diagnostic",
format!(
"upgrade: {} -> {} ({reason}) [next-strategy={}]",
from.as_str(),
to.as_str(),
to.as_str()
),
),
FallbackDecision::GiveUp { reason } => (
"error",
format!("fallback exhausted: {reason} [next-strategy=none]"),
),
}
}
fn describe_failure(strategy: CaptureStrategy, signature: FailureSignature) -> String {
let what = match signature {
FailureSignature::InjectionFailed => "could not inject its capture hook",
FailureSignature::AntiCheatDenied => {
"is protected by anti-cheat, so the capture hook must not be injected"
}
FailureSignature::NoFramesWithinTimeout => "produced no frames within the start timeout",
FailureSignature::DeviceLost => "lost its capture device",
FailureSignature::UnsupportedTransport => "used a frame transport this build cannot read",
FailureSignature::UnsupportedFormat => "used a swapchain format the fast path cannot read",
FailureSignature::WindowGone => "lost its capture target",
FailureSignature::FramesRecovered => "recovered",
};
format!("{} capture {what}", strategy.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game_capture_abi::{
GAME_CAPTURE_FALLBACK_DEVICE_LOST, GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED,
GAME_CAPTURE_FALLBACK_FORCED_CPU, GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED,
GAME_CAPTURE_FALLBACK_MULTISAMPLED, GAME_CAPTURE_FALLBACK_NONE,
GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED,
};
#[test]
fn ranking_order_is_best_to_worst() {
assert!(CaptureStrategy::GameHook.rank() < CaptureStrategy::Wgc.rank());
assert!(CaptureStrategy::Wgc.rank() < CaptureStrategy::DxgiDuplication.rank());
assert!(CaptureStrategy::DxgiDuplication.rank() < CaptureStrategy::WindowGdi.rank());
assert_eq!(
CaptureStrategy::GameHook.next_worse(),
Some(CaptureStrategy::Wgc)
);
assert_eq!(
CaptureStrategy::Wgc.next_worse(),
Some(CaptureStrategy::DxgiDuplication)
);
assert_eq!(
CaptureStrategy::DxgiDuplication.next_worse(),
Some(CaptureStrategy::WindowGdi)
);
assert_eq!(CaptureStrategy::WindowGdi.next_worse(), None);
}
#[test]
fn injection_failure_falls_back_to_wgc() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
match ranker.observe(FailureSignature::InjectionFailed) {
FallbackDecision::Transition { from, to, .. } => {
assert_eq!(from, CaptureStrategy::GameHook);
assert_eq!(to, CaptureStrategy::Wgc);
}
other => panic!("expected Transition, got {other:?}"),
}
assert_eq!(ranker.active(), CaptureStrategy::Wgc);
assert!(ranker.is_disqualified(CaptureStrategy::GameHook));
assert!(ranker.last_fallback_reason().is_some());
}
#[test]
fn no_frames_timeout_falls_back_but_does_not_disqualify() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
match ranker.observe(FailureSignature::NoFramesWithinTimeout) {
FallbackDecision::Transition { to, .. } => {
assert_eq!(to, CaptureStrategy::Wgc)
}
other => panic!("expected Transition, got {other:?}"),
}
assert!(!ranker.is_disqualified(CaptureStrategy::GameHook));
}
#[test]
fn wgc_failure_falls_back_to_dxgi() {
let mut ranker = FallbackRanker::new(CaptureStrategy::Wgc);
match ranker.observe(FailureSignature::NoFramesWithinTimeout) {
FallbackDecision::Transition { from, to, .. } => {
assert_eq!(from, CaptureStrategy::Wgc);
assert_eq!(to, CaptureStrategy::DxgiDuplication);
}
other => panic!("expected Transition, got {other:?}"),
}
assert!(!ranker.is_disqualified(CaptureStrategy::Wgc));
}
#[test]
fn anti_cheat_denied_disqualifies_game_hook() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
let decision = ranker.observe(FailureSignature::AntiCheatDenied);
assert!(matches!(
decision,
FallbackDecision::Transition {
to: CaptureStrategy::Wgc,
..
}
));
assert!(ranker.is_disqualified(CaptureStrategy::GameHook));
}
#[test]
fn device_lost_retries_in_place_then_falls_back() {
let mut ranker = FallbackRanker::new(CaptureStrategy::DxgiDuplication);
for _ in 0..MAX_DEVICE_LOST_RETRIES {
assert_eq!(
ranker.observe(FailureSignature::DeviceLost),
FallbackDecision::Stay {
retry_in_place: true
}
);
assert_eq!(ranker.active(), CaptureStrategy::DxgiDuplication);
}
match ranker.observe(FailureSignature::DeviceLost) {
FallbackDecision::Transition { to, .. } => assert_eq!(to, CaptureStrategy::WindowGdi),
other => panic!("expected Transition after retry budget, got {other:?}"),
}
assert!(!ranker.is_disqualified(CaptureStrategy::DxgiDuplication));
}
#[test]
fn frames_recovered_resets_device_lost_streak() {
let mut ranker = FallbackRanker::new(CaptureStrategy::DxgiDuplication);
ranker.observe(FailureSignature::DeviceLost);
ranker.observe(FailureSignature::DeviceLost);
assert_eq!(
ranker.observe(FailureSignature::FramesRecovered),
FallbackDecision::Stay {
retry_in_place: false
}
);
for _ in 0..MAX_DEVICE_LOST_RETRIES {
assert_eq!(
ranker.observe(FailureSignature::DeviceLost),
FallbackDecision::Stay {
retry_in_place: true
}
);
}
}
#[test]
fn window_gone_gives_up_from_any_strategy() {
for start in CaptureStrategy::RANKED {
let mut ranker = FallbackRanker::new(start);
match ranker.observe(FailureSignature::WindowGone) {
FallbackDecision::GiveUp { .. } => {}
other => panic!("expected GiveUp from {start:?}, got {other:?}"),
}
}
}
#[test]
fn last_resort_failure_gives_up() {
let mut ranker = FallbackRanker::new(CaptureStrategy::WindowGdi);
match ranker.observe(FailureSignature::NoFramesWithinTimeout) {
FallbackDecision::GiveUp { reason } => assert!(reason.contains("window-gdi")),
other => panic!("expected GiveUp at last resort, got {other:?}"),
}
}
#[test]
fn full_descent_game_to_wgc_to_dxgi_to_window_then_give_up() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
assert!(matches!(
ranker.observe(FailureSignature::InjectionFailed),
FallbackDecision::Transition {
to: CaptureStrategy::Wgc,
..
}
));
assert!(matches!(
ranker.observe(FailureSignature::UnsupportedTransport),
FallbackDecision::Transition {
to: CaptureStrategy::DxgiDuplication,
..
}
));
assert!(matches!(
ranker.observe(FailureSignature::UnsupportedTransport),
FallbackDecision::Transition {
to: CaptureStrategy::WindowGdi,
..
}
));
assert!(matches!(
ranker.observe(FailureSignature::UnsupportedFormat),
FallbackDecision::GiveUp { .. }
));
}
#[test]
fn stable_recovery_climbs_back_up_skipping_disqualified() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
ranker.observe(FailureSignature::InjectionFailed);
for _ in 0..=MAX_DEVICE_LOST_RETRIES {
ranker.observe(FailureSignature::DeviceLost);
}
assert_eq!(ranker.active(), CaptureStrategy::DxgiDuplication);
assert_eq!(
ranker.observe(FailureSignature::FramesRecovered),
FallbackDecision::Stay {
retry_in_place: false
}
);
match ranker.observe(FailureSignature::FramesRecovered) {
FallbackDecision::Upgrade { from, to, .. } => {
assert_eq!(from, CaptureStrategy::DxgiDuplication);
assert_eq!(to, CaptureStrategy::Wgc);
}
other => panic!("expected Upgrade, got {other:?}"),
}
assert_eq!(ranker.active(), CaptureStrategy::Wgc);
}
#[test]
fn no_upgrade_when_already_best() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
for _ in 0..(STABLE_FRAMES_BEFORE_UPGRADE + 2) {
assert_eq!(
ranker.observe(FailureSignature::FramesRecovered),
FallbackDecision::Stay {
retry_in_place: false
}
);
}
assert_eq!(ranker.active(), CaptureStrategy::GameHook);
}
#[test]
fn upgrade_does_not_revive_disqualified_better_strategy() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
ranker.observe(FailureSignature::AntiCheatDenied);
assert_eq!(ranker.active(), CaptureStrategy::Wgc);
for _ in 0..(STABLE_FRAMES_BEFORE_UPGRADE + 2) {
assert_eq!(
ranker.observe(FailureSignature::FramesRecovered),
FallbackDecision::Stay {
retry_in_place: false
}
);
}
assert_eq!(ranker.active(), CaptureStrategy::Wgc);
}
#[test]
fn fallback_reason_maps_from_abi_reason() {
assert_eq!(
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_NONE),
None
);
assert_eq!(
FailureSignature::from_fallback_reason(
GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED
),
Some(FailureSignature::UnsupportedTransport)
);
assert_eq!(
FailureSignature::from_fallback_reason(
GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED
),
Some(FailureSignature::UnsupportedTransport)
);
assert_eq!(
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED),
Some(FailureSignature::UnsupportedFormat)
);
assert_eq!(
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_MULTISAMPLED),
Some(FailureSignature::UnsupportedFormat)
);
assert_eq!(
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_DEVICE_LOST),
Some(FailureSignature::DeviceLost)
);
assert_eq!(
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_FORCED_CPU),
None
);
assert_eq!(FailureSignature::from_fallback_reason(u32::MAX), None);
}
#[test]
fn unsupported_transport_disqualifies_current_strategy() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
match ranker.observe(FailureSignature::UnsupportedTransport) {
FallbackDecision::Transition { from, to, reason } => {
assert_eq!(from, CaptureStrategy::GameHook);
assert_eq!(to, CaptureStrategy::Wgc);
assert!(reason.contains("transport"));
}
other => panic!("expected Transition, got {other:?}"),
}
assert!(ranker.is_disqualified(CaptureStrategy::GameHook));
}
#[test]
fn forced_cpu_reason_is_not_a_failure_signature() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
if let Some(signature) =
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_FORCED_CPU)
{
ranker.observe(signature);
}
assert_eq!(ranker.active(), CaptureStrategy::GameHook);
assert!(!ranker.is_disqualified(CaptureStrategy::GameHook));
}
#[test]
fn strategy_names_are_stable_for_the_js_seam() {
assert_eq!(CaptureStrategy::GameHook.as_str(), "game-hook");
assert_eq!(CaptureStrategy::Wgc.as_str(), "wgc");
assert_eq!(
CaptureStrategy::DxgiDuplication.as_str(),
"dxgi-duplication"
);
assert_eq!(CaptureStrategy::WindowGdi.as_str(), "window-gdi");
}
#[test]
fn tracker_snapshot_tracks_active_and_reason() {
let mut tracker = FallbackTracker::new(CaptureStrategy::GameHook);
let snap = tracker.snapshot();
assert_eq!(snap.active_strategy, "game-hook");
assert_eq!(snap.last_fallback_reason, "", "no reason before a fallback");
tracker.observe(FailureSignature::InjectionFailed);
let snap = tracker.snapshot();
assert_eq!(snap.active_strategy, "wgc");
assert!(snap.last_fallback_reason.contains("wgc"));
}
#[test]
fn decision_lifecycle_maps_to_event_pairs() {
let transition = FallbackDecision::Transition {
from: CaptureStrategy::GameHook,
to: CaptureStrategy::DxgiDuplication,
reason: "game-hook capture could not inject its capture hook".to_string(),
};
let (kind, msg) = decision_lifecycle(&transition);
assert_eq!(kind, "error");
assert!(msg.contains("next-strategy=dxgi-duplication"));
let upgrade = FallbackDecision::Upgrade {
from: CaptureStrategy::WindowGdi,
to: CaptureStrategy::DxgiDuplication,
reason: "stable".to_string(),
};
let (kind, msg) = decision_lifecycle(&upgrade);
assert_eq!(kind, "diagnostic");
assert!(msg.contains("next-strategy=dxgi-duplication"));
let give_up = FallbackDecision::GiveUp {
reason: "window-gdi was the last resort".to_string(),
};
let (kind, msg) = decision_lifecycle(&give_up);
assert_eq!(kind, "error");
assert!(msg.contains("next-strategy=none"));
let (kind, _) = decision_lifecycle(&FallbackDecision::Stay {
retry_in_place: true,
});
assert_eq!(kind, "diagnostic");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,307 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
pub const GAME_CAPTURE_MAGIC: u32 = 0x4658_4743;
pub const GAME_CAPTURE_ABI_VERSION: u32 = 4;
pub const GAME_CAPTURE_ABI_VERSION_PRESENT_CLOCK: u32 = 4;
pub const GAME_CAPTURE_PRESENT_CLOCK_WALL: u32 = 0;
pub const GAME_CAPTURE_PRESENT_CLOCK_QPC: u32 = 1;
pub const GAME_CAPTURE_STATE_INIT: u32 = 0;
pub const GAME_CAPTURE_STATE_ACTIVE: u32 = 1;
pub const GAME_CAPTURE_STATE_STOPPED: u32 = 2;
pub const GAME_CAPTURE_STATE_ERROR: u32 = 3;
pub const GAME_CAPTURE_STATE_RESIZE_REQUIRED: u32 = 4;
pub const GAME_CAPTURE_BUFFER_COUNT: usize = 2;
pub const GAME_CAPTURE_BYTES_PER_PIXEL: u32 = 4;
pub const GAME_CAPTURE_TRANSPORT_MEMORY: u32 = 0;
pub const GAME_CAPTURE_TRANSPORT_SHARED_TEXTURE: u32 = 1;
pub const GAME_CAPTURE_API_UNKNOWN: u32 = 0;
pub const GAME_CAPTURE_API_OPENGL: u32 = 1;
pub const GAME_CAPTURE_API_D3D8: u32 = 2;
pub const GAME_CAPTURE_API_D3D9: u32 = 3;
pub const GAME_CAPTURE_API_D3D10: u32 = 4;
pub const GAME_CAPTURE_API_D3D11: u32 = 5;
pub const GAME_CAPTURE_API_D3D12: u32 = 6;
pub const GAME_CAPTURE_API_VULKAN: u32 = 7;
pub const GAME_CAPTURE_FALLBACK_NONE: u32 = 0;
pub const GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED: u32 = 1;
pub const GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED: u32 = 2;
pub const GAME_CAPTURE_FALLBACK_FORCED_CPU: u32 = 3;
pub const GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED: u32 = 4;
pub const GAME_CAPTURE_FALLBACK_MULTISAMPLED: u32 = 5;
pub const GAME_CAPTURE_FALLBACK_DEVICE_LOST: u32 = 6;
pub const GAME_CAPTURE_FLAG_HDR: u32 = 1 << 0;
pub const GAME_CAPTURE_FLAG_MULTISAMPLED: u32 = 1 << 1;
pub const GAME_CAPTURE_FLAG_FLIP_VERTICAL: u32 = 1 << 2;
pub const GAME_CAPTURE_FLAG_PROTECTED: u32 = 1 << 3;
pub const GAME_CAPTURE_FLAG_TEN_BIT: u32 = 1 << 4;
pub const ENV_DISABLE_HOOK: &str = "FLUXER_GAME_CAPTURE_DISABLE_HOOK";
pub const ENV_DISABLE_D3D12: &str = "FLUXER_GAME_CAPTURE_DISABLE_D3D12";
pub const ENV_DISABLE_VULKAN: &str = "DISABLE_FLUXER_VULKAN_CAPTURE";
pub const ENV_FORCE_CPU: &str = "FLUXER_GAME_CAPTURE_FORCE_CPU";
pub const ENV_FORCE_SHARED_TEXTURE: &str = "FLUXER_GAME_CAPTURE_FORCE_SHARED_TEXTURE";
pub const ENV_ENABLE_OPENGL_SHARED_TEXTURE: &str =
"FLUXER_GAME_CAPTURE_ENABLE_OPENGL_SHARED_TEXTURE";
pub const ENV_DISABLE_OPENGL_SHARED_TEXTURE: &str =
"FLUXER_GAME_CAPTURE_DISABLE_OPENGL_SHARED_TEXTURE";
pub const ENV_VERBOSE: &str = "FLUXER_GAME_CAPTURE_VERBOSE";
pub const ENV_INJECT_METHOD: &str = "FLUXER_GAME_CAPTURE_INJECT_METHOD";
pub const GAME_CAPTURE_CONTROL_DISABLE_SHARED_TEXTURE: u32 = 1 << 0;
pub fn env_flag_enabled(name: &str) -> bool {
std::env::var_os(name)
.map(|value| !value.is_empty())
.unwrap_or(false)
}
pub const GAME_CAPTURE_INFO_PREFIX: &str = "FluxerGameCapture_Info_";
pub const GAME_CAPTURE_FRAME_PREFIX: &str = "FluxerGameCapture_Frame_";
pub const GAME_CAPTURE_READY_PREFIX: &str = "FluxerGameCapture_Ready_";
pub const GAME_CAPTURE_STOP_PREFIX: &str = "FluxerGameCapture_Stop_";
pub const GAME_CAPTURE_KEEPALIVE_PREFIX: &str = "FluxerGameCapture_KeepAlive_";
pub const GAME_CAPTURE_MUTEX_PREFIX: &str = "FluxerGameCapture_FrameMutex_";
#[repr(C)]
#[derive(Clone, Copy)]
pub struct GameCaptureSharedInfo {
pub magic: u32,
pub version: u32,
pub state: u32,
pub last_error: u32,
pub hwnd: u64,
pub max_width: u32,
pub max_height: u32,
pub width: u32,
pub height: u32,
pub pitch: u32,
pub frame_index: u32,
pub frame_counter: u64,
pub timestamp_us: i64,
pub target_frame_interval_ns: u64,
pub transport: u32,
pub dxgi_format: u32,
pub texture_handle: u64,
pub api_type: u32,
pub fallback_reason: u32,
pub capture_flags: u32,
pub dropped_frame_counter: u64,
pub last_present_timestamp_us: i64,
pub control: u32,
pub present_clock: u32,
pub reserved: [u32; 23],
}
const _: () = assert!(std::mem::size_of::<GameCaptureSharedInfo>() == 224);
impl GameCaptureSharedInfo {
pub fn new(hwnd: u64, max_width: u32, max_height: u32, frame_rate: u32) -> Self {
Self {
magic: GAME_CAPTURE_MAGIC,
version: GAME_CAPTURE_ABI_VERSION,
state: GAME_CAPTURE_STATE_INIT,
last_error: 0,
hwnd,
max_width,
max_height,
width: 0,
height: 0,
pitch: max_width.saturating_mul(GAME_CAPTURE_BYTES_PER_PIXEL),
frame_index: 0,
frame_counter: 0,
timestamp_us: 0,
target_frame_interval_ns: frame_interval_ns(frame_rate),
transport: GAME_CAPTURE_TRANSPORT_MEMORY,
dxgi_format: 0,
texture_handle: 0,
api_type: GAME_CAPTURE_API_UNKNOWN,
fallback_reason: GAME_CAPTURE_FALLBACK_NONE,
capture_flags: 0,
dropped_frame_counter: 0,
last_present_timestamp_us: 0,
control: 0,
present_clock: GAME_CAPTURE_PRESENT_CLOCK_WALL,
reserved: [0; 23],
}
}
}
pub fn host_supports_present_clock(version: u32) -> bool {
version >= GAME_CAPTURE_ABI_VERSION_PRESENT_CLOCK
}
pub fn qpc_ticks_to_us(ticks: i64, frequency: i64) -> i64 {
if frequency <= 0 {
return 0;
}
if ticks <= 0 {
return 0;
}
let whole_second_us = (ticks / frequency).saturating_mul(1_000_000);
let fractional_us = (ticks % frequency).saturating_mul(1_000_000) / frequency;
whole_second_us.saturating_add(fractional_us)
}
pub fn presented_recently(
present_clock: u32,
last_present_us: i64,
wall_now_us: i64,
qpc_now_us: i64,
window_us: i64,
) -> bool {
assert!(window_us > 0);
if last_present_us <= 0 {
return false;
}
let now_us = if present_clock == GAME_CAPTURE_PRESENT_CLOCK_QPC {
qpc_now_us
} else {
wall_now_us
};
now_us.saturating_sub(last_present_us) < window_us
}
#[cfg(target_os = "windows")]
pub fn qpc_now_us() -> i64 {
use windows_sys::Win32::System::Performance::{
QueryPerformanceCounter, QueryPerformanceFrequency,
};
let mut ticks = 0i64;
let mut frequency = 0i64;
let counter_ok = unsafe { QueryPerformanceCounter(&mut ticks) };
let frequency_ok = unsafe { QueryPerformanceFrequency(&mut frequency) };
if counter_ok == 0 || frequency_ok == 0 {
return 0;
}
qpc_ticks_to_us(ticks, frequency)
}
pub fn frame_interval_ns(frame_rate: u32) -> u64 {
let rate = frame_rate.clamp(1, 144) as u64;
1_000_000_000 / rate
}
pub fn frame_buffer_stride(width: u32) -> u32 {
width.saturating_mul(GAME_CAPTURE_BYTES_PER_PIXEL)
}
pub fn frame_buffer_size(width: u32, height: u32) -> Option<usize> {
frame_buffer_stride(width)
.checked_mul(height)?
.try_into()
.ok()
}
pub fn shared_frame_mapping_size(width: u32, height: u32) -> Option<usize> {
frame_buffer_size(width, height)?.checked_mul(GAME_CAPTURE_BUFFER_COUNT)
}
pub fn object_name(prefix: &str, pid: u32) -> String {
format!("{prefix}{pid}")
}
pub fn mutex_name(pid: u32, index: usize) -> String {
format!("{GAME_CAPTURE_MUTEX_PREFIX}{index}_{pid}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn qpc_ticks_to_us_converts_whole_and_fractional_seconds() {
assert_eq!(qpc_ticks_to_us(10_000_000, 10_000_000), 1_000_000);
assert_eq!(qpc_ticks_to_us(15_000_000, 10_000_000), 1_500_000);
assert_eq!(qpc_ticks_to_us(1, 10_000_000), 0);
assert_eq!(qpc_ticks_to_us(10, 10_000_000), 1);
}
#[test]
fn qpc_ticks_to_us_does_not_overflow_for_long_uptimes() {
let frequency = 3_800_000_000i64;
let uptime_seconds = 400 * 24 * 60 * 60i64;
let ticks = frequency * uptime_seconds;
assert_eq!(
qpc_ticks_to_us(ticks, frequency),
uptime_seconds * 1_000_000
);
}
#[test]
fn qpc_ticks_to_us_rejects_invalid_inputs() {
assert_eq!(qpc_ticks_to_us(123, 0), 0);
assert_eq!(qpc_ticks_to_us(123, -1), 0);
assert_eq!(qpc_ticks_to_us(-123, 10_000_000), 0);
assert_eq!(qpc_ticks_to_us(0, 10_000_000), 0);
}
#[test]
fn host_supports_present_clock_gates_on_abi_version() {
assert!(!host_supports_present_clock(0));
assert!(!host_supports_present_clock(3));
assert!(host_supports_present_clock(4));
assert!(host_supports_present_clock(5));
}
#[test]
fn presented_recently_uses_wall_clock_for_legacy_hooks() {
assert!(presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_WALL,
1_000_000,
1_500_000,
i64::MAX,
1_000_000
));
assert!(!presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_WALL,
1_000_000,
2_000_000,
0,
1_000_000
));
}
#[test]
fn presented_recently_uses_qpc_for_present_clock_hooks() {
assert!(presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_QPC,
1_000_000,
i64::MAX,
1_500_000,
1_000_000
));
assert!(!presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_QPC,
1_000_000,
0,
2_000_000,
1_000_000
));
}
#[test]
fn presented_recently_requires_an_observed_present() {
assert!(!presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_WALL,
0,
1,
1,
1_000_000
));
assert!(!presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_QPC,
-1,
1,
1,
1_000_000
));
}
}
@@ -0,0 +1,174 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(target_os = "windows")]
use std::{
collections::HashMap,
sync::{LazyLock, Mutex},
};
#[cfg(target_os = "windows")]
use windows::{
Win32::{
Foundation::{CloseHandle, HANDLE},
System::{
LibraryLoader::{GetProcAddress, LoadLibraryW},
Threading::{
GetCurrentProcess, OpenProcess, PROCESS_QUERY_INFORMATION,
PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SET_INFORMATION,
},
},
},
core::{s, w},
};
#[cfg(target_os = "windows")]
const D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL: u32 = 2;
#[cfg(target_os = "windows")]
const D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH: u32 = 4;
#[cfg(target_os = "windows")]
const D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME: u32 = 5;
#[cfg(target_os = "windows")]
static SAVED_PRIORITIES: LazyLock<Mutex<HashMap<u32, u32>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
#[cfg(target_os = "windows")]
type SetSchedulingPriorityClassFn = unsafe extern "system" fn(HANDLE, u32) -> i32;
#[cfg(target_os = "windows")]
type GetSchedulingPriorityClassFn = unsafe extern "system" fn(HANDLE, *mut u32) -> i32;
#[cfg(target_os = "windows")]
struct D3dkmtFns {
set: SetSchedulingPriorityClassFn,
get: GetSchedulingPriorityClassFn,
}
#[cfg(target_os = "windows")]
fn load_d3dkmt_fns() -> Result<D3dkmtFns, String> {
unsafe {
let gdi32 =
LoadLibraryW(w!("gdi32.dll")).map_err(|e| format!("LoadLibraryW(gdi32.dll): {e}"))?;
let set_ptr = GetProcAddress(gdi32, s!("D3DKMTSetProcessSchedulingPriorityClass"))
.ok_or("D3DKMTSetProcessSchedulingPriorityClass not exported by gdi32.dll")?;
let get_ptr = GetProcAddress(gdi32, s!("D3DKMTGetProcessSchedulingPriorityClass"))
.ok_or("D3DKMTGetProcessSchedulingPriorityClass not exported by gdi32.dll")?;
Ok(D3dkmtFns {
set: std::mem::transmute::<
unsafe extern "system" fn() -> isize,
SetSchedulingPriorityClassFn,
>(set_ptr),
get: std::mem::transmute::<
unsafe extern "system" fn() -> isize,
GetSchedulingPriorityClassFn,
>(get_ptr),
})
}
}
#[cfg(target_os = "windows")]
fn process_id_or_current(process_id: Option<u32>) -> Result<u32, String> {
let pid = process_id.unwrap_or_else(std::process::id);
if pid == 0 {
return Err("Invalid process id 0".into());
}
Ok(pid)
}
#[cfg(target_os = "windows")]
fn with_process_handle<T>(
process_id: u32,
operation: impl FnOnce(HANDLE) -> Result<T, String>,
) -> Result<T, String> {
if process_id == std::process::id() {
return operation(unsafe { GetCurrentProcess() });
}
let access =
PROCESS_SET_INFORMATION | PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION;
let handle = unsafe { OpenProcess(access, false, process_id) }
.map_err(|e| format!("OpenProcess({process_id}): {e}"))?;
let result = operation(handle);
let _ = unsafe { CloseHandle(handle) };
result
}
#[cfg(target_os = "windows")]
fn scheduling_priority_class(
priority_class: Option<String>,
) -> Result<(u32, &'static str), String> {
match priority_class
.as_deref()
.unwrap_or("high")
.trim()
.to_ascii_lowercase()
.as_str()
{
"" | "high" => Ok((D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH, "HIGH")),
"realtime" | "real-time" => Ok((D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME, "REALTIME")),
other => Err(format!(
"Unsupported GPU scheduling priority class '{other}'; expected 'high' or 'realtime'"
)),
}
}
#[cfg(target_os = "windows")]
pub fn elevate(process_id: Option<u32>, priority_class: Option<String>) -> Result<(), String> {
let fns = load_d3dkmt_fns()?;
let process_id = process_id_or_current(process_id)?;
let (priority_class, priority_label) = scheduling_priority_class(priority_class)?;
with_process_handle(process_id, |process| {
let mut current: u32 = D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL;
let status = unsafe { (fns.get)(process, &mut current) };
if status == 0 {
let mut saved = SAVED_PRIORITIES
.lock()
.map_err(|_| "GPU priority saved-state lock poisoned".to_string())?;
saved.entry(process_id).or_insert(current);
}
let status = unsafe { (fns.set)(process, priority_class) };
if status != 0 {
return Err(format!(
"D3DKMTSetProcessSchedulingPriorityClass(pid={process_id}, {priority_label}) returned NTSTATUS 0x{:08X}",
status as u32
));
}
Ok(())
})
}
#[cfg(target_os = "windows")]
pub fn restore(process_id: Option<u32>) -> Result<(), String> {
let fns = load_d3dkmt_fns()?;
let process_id = process_id_or_current(process_id)?;
let target = {
let mut saved = SAVED_PRIORITIES
.lock()
.map_err(|_| "GPU priority saved-state lock poisoned".to_string())?;
saved
.remove(&process_id)
.unwrap_or(D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL)
};
with_process_handle(process_id, |process| {
let status = unsafe { (fns.set)(process, target) };
if status != 0 {
return Err(format!(
"D3DKMTSetProcessSchedulingPriorityClass(pid={process_id}, restore={target}) returned NTSTATUS 0x{:08X}",
status as u32
));
}
Ok(())
})
}
#[cfg(not(target_os = "windows"))]
pub fn elevate(_process_id: Option<u32>, _priority_class: Option<String>) -> Result<(), String> {
Ok(())
}
#[cfg(not(target_os = "windows"))]
pub fn restore(_process_id: Option<u32>) -> Result<(), String> {
Ok(())
}
@@ -0,0 +1,465 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
pub const DXGI_FORMAT_R16G16B16A16_FLOAT: u32 = 10;
pub const DXGI_FORMAT_R16G16B16A16_UNORM: u32 = 11;
pub const DXGI_FORMAT_R10G10B10A2_UNORM: u32 = 24;
pub const DXGI_FORMAT_R8G8B8A8_UNORM: u32 = 28;
pub const DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: u32 = 29;
pub const DXGI_FORMAT_B8G8R8A8_UNORM: u32 = 87;
pub const DXGI_FORMAT_B8G8R8X8_UNORM: u32 = 88;
pub const DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM: u32 = 89;
pub const DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: u32 = 91;
pub const DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: u32 = 93;
pub const DXGI_FORMAT_P010: u32 = 104;
pub const DXGI_FORMAT_P016: u32 = 105;
pub const DXGI_FORMAT_420_OPAQUE: u32 = 106;
pub const DXGI_FORMAT_YUY2: u32 = 107;
pub const DXGI_FORMAT_R32G32B32A32_FLOAT: u32 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceFormat {
Bgra8,
Rgba8,
R10G10B10A2 { hdr: bool },
Rgba16Float { hdr: bool },
}
impl SourceFormat {
pub fn classify(dxgi_format: u32, capture_flags: u32) -> Option<SourceFormat> {
let hdr = capture_flags & crate::game_capture_abi::GAME_CAPTURE_FLAG_HDR != 0;
match dxgi_format {
DXGI_FORMAT_B8G8R8A8_UNORM
| DXGI_FORMAT_B8G8R8A8_UNORM_SRGB
| DXGI_FORMAT_B8G8R8X8_UNORM
| DXGI_FORMAT_B8G8R8X8_UNORM_SRGB => Some(SourceFormat::Bgra8),
DXGI_FORMAT_R8G8B8A8_UNORM | DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => {
Some(SourceFormat::Rgba8)
}
DXGI_FORMAT_R10G10B10A2_UNORM => Some(SourceFormat::R10G10B10A2 { hdr }),
DXGI_FORMAT_R16G16B16A16_FLOAT => Some(SourceFormat::Rgba16Float { hdr }),
_ => None,
}
}
pub fn is_8bit(self) -> bool {
matches!(self, SourceFormat::Bgra8 | SourceFormat::Rgba8)
}
pub fn is_hdr(self) -> bool {
matches!(
self,
SourceFormat::R10G10B10A2 { hdr: true } | SourceFormat::Rgba16Float { hdr: true }
)
}
pub fn bytes_per_pixel(self) -> usize {
match self {
SourceFormat::Bgra8 | SourceFormat::Rgba8 | SourceFormat::R10G10B10A2 { .. } => 4,
SourceFormat::Rgba16Float { .. } => 8,
}
}
}
const SDR_WHITE_NITS: f32 = 80.0;
pub fn r10g10b10a2_row_to_bgra(src_row: &[u8], width: usize, dst_row: &mut [u8], hdr: bool) {
for x in 0..width {
let so = x * 4;
let dofs = x * 4;
if so + 4 > src_row.len() || dofs + 4 > dst_row.len() {
break;
}
let packed = u32::from_le_bytes([
src_row[so],
src_row[so + 1],
src_row[so + 2],
src_row[so + 3],
]);
let r10 = (packed & 0x3FF) as u16;
let g10 = ((packed >> 10) & 0x3FF) as u16;
let b10 = ((packed >> 20) & 0x3FF) as u16;
let a2 = ((packed >> 30) & 0x3) as u8;
let (r, g, b) = if hdr {
tonemap_rec2020_pq_to_srgb8(r10, g10, b10)
} else {
(scale10_to_8(r10), scale10_to_8(g10), scale10_to_8(b10))
};
dst_row[dofs] = b;
dst_row[dofs + 1] = g;
dst_row[dofs + 2] = r;
dst_row[dofs + 3] = (a2 as u16 * 255 / 3) as u8;
}
}
pub fn rgba16f_row_to_bgra(src_row: &[u8], width: usize, dst_row: &mut [u8], hdr: bool) {
for x in 0..width {
let so = x * 8;
let dofs = x * 4;
if so + 8 > src_row.len() || dofs + 4 > dst_row.len() {
break;
}
let r = f16_to_f32(u16::from_le_bytes([src_row[so], src_row[so + 1]]));
let g = f16_to_f32(u16::from_le_bytes([src_row[so + 2], src_row[so + 3]]));
let b = f16_to_f32(u16::from_le_bytes([src_row[so + 4], src_row[so + 5]]));
let a = f16_to_f32(u16::from_le_bytes([src_row[so + 6], src_row[so + 7]]));
let (lr, lg, lb) = if hdr {
(
reinhard(r.max(0.0)),
reinhard(g.max(0.0)),
reinhard(b.max(0.0)),
)
} else {
(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
};
dst_row[dofs] = linear_to_srgb8(lb);
dst_row[dofs + 1] = linear_to_srgb8(lg);
dst_row[dofs + 2] = linear_to_srgb8(lr);
dst_row[dofs + 3] = (a.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
}
}
fn scale10_to_8(v10: u16) -> u8 {
((v10 as u32 * 255 + 511) / 1023) as u8
}
fn reinhard(linear: f32) -> f32 {
let v = linear.max(0.0);
(v / (1.0 + v)).clamp(0.0, 1.0)
}
fn tonemap_rec2020_pq_to_srgb8(r10: u16, g10: u16, b10: u16) -> (u8, u8, u8) {
let lr = pq_eotf(r10 as f32 / 1023.0);
let lg = pq_eotf(g10 as f32 / 1023.0);
let lb = pq_eotf(b10 as f32 / 1023.0);
let scale = 10000.0 / SDR_WHITE_NITS;
let map = |v: f32| reinhard((v * scale).max(0.0));
(
linear_to_srgb8(map(lr)),
linear_to_srgb8(map(lg)),
linear_to_srgb8(map(lb)),
)
}
fn pq_eotf(e: f32) -> f32 {
const M1: f64 = 0.1593017578125;
const M2: f64 = 78.84375;
const C1: f64 = 0.8359375;
const C2: f64 = 18.8515625;
const C3: f64 = 18.6875;
let e = (e.clamp(0.0, 1.0)) as f64;
let ep = e.powf(1.0 / M2);
let num = (ep - C1).max(0.0);
let den = C2 - C3 * ep;
if den <= 0.0 {
return 0.0;
}
(num / den).powf(1.0 / M1) as f32
}
fn linear_to_srgb8(linear: f32) -> u8 {
let l = linear.clamp(0.0, 1.0);
let srgb = if l <= 0.0031308 {
l * 12.92
} else {
1.055 * l.powf(1.0 / 2.4) - 0.055
};
(srgb.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
}
pub fn f16_to_f32(h: u16) -> f32 {
let sign = (h >> 15) & 0x1;
let exp = (h >> 10) & 0x1F;
let mant = h & 0x3FF;
let sign_f = if sign == 1 { -1.0f32 } else { 1.0f32 };
if exp == 0 {
sign_f * (mant as f32) * 2f32.powi(-24)
} else if exp == 0x1F {
if mant == 0 { sign_f * 65504.0 } else { 0.0 }
} else {
sign_f * (1.0 + (mant as f32) / 1024.0) * 2f32.powi(exp as i32 - 15)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game_capture_abi::{GAME_CAPTURE_FLAG_HDR, GAME_CAPTURE_FLAG_TEN_BIT};
#[test]
fn classifies_8bit_formats() {
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_B8G8R8A8_UNORM, 0),
Some(SourceFormat::Bgra8)
);
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R8G8B8A8_UNORM, 0),
Some(SourceFormat::Rgba8)
);
assert!(
SourceFormat::classify(DXGI_FORMAT_B8G8R8A8_UNORM, 0)
.unwrap()
.is_8bit()
);
}
#[test]
fn classifies_obs_8bit_alias_set() {
for format in [
DXGI_FORMAT_B8G8R8A8_UNORM,
DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
DXGI_FORMAT_B8G8R8X8_UNORM,
DXGI_FORMAT_B8G8R8X8_UNORM_SRGB,
] {
assert_eq!(SourceFormat::classify(format, 0), Some(SourceFormat::Bgra8));
assert_eq!(
SourceFormat::classify(format, GAME_CAPTURE_FLAG_HDR),
Some(SourceFormat::Bgra8),
"8-bit format {format} must not become HDR just because the flag is set"
);
}
for format in [DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB] {
assert_eq!(SourceFormat::classify(format, 0), Some(SourceFormat::Rgba8));
assert_eq!(
SourceFormat::classify(format, GAME_CAPTURE_FLAG_HDR),
Some(SourceFormat::Rgba8),
"8-bit format {format} must not become HDR just because the flag is set"
);
}
}
#[test]
fn classifies_10bit_and_hdr_flag() {
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R10G10B10A2_UNORM, GAME_CAPTURE_FLAG_TEN_BIT),
Some(SourceFormat::R10G10B10A2 { hdr: false })
);
assert_eq!(
SourceFormat::classify(
DXGI_FORMAT_R10G10B10A2_UNORM,
GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR
),
Some(SourceFormat::R10G10B10A2 { hdr: true })
);
assert!(
SourceFormat::classify(DXGI_FORMAT_R10G10B10A2_UNORM, GAME_CAPTURE_FLAG_HDR)
.unwrap()
.is_hdr()
);
}
#[test]
fn high_precision_formats_have_expected_byte_widths() {
assert_eq!(SourceFormat::Bgra8.bytes_per_pixel(), 4);
assert_eq!(SourceFormat::Rgba8.bytes_per_pixel(), 4);
assert_eq!(
SourceFormat::R10G10B10A2 { hdr: false }.bytes_per_pixel(),
4
);
assert_eq!(SourceFormat::R10G10B10A2 { hdr: true }.bytes_per_pixel(), 4);
assert_eq!(
SourceFormat::Rgba16Float { hdr: false }.bytes_per_pixel(),
8
);
assert_eq!(SourceFormat::Rgba16Float { hdr: true }.bytes_per_pixel(), 8);
}
#[test]
fn classifies_fp16_scrgb() {
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R16G16B16A16_FLOAT, GAME_CAPTURE_FLAG_HDR),
Some(SourceFormat::Rgba16Float { hdr: true })
);
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R16G16B16A16_FLOAT, 0),
Some(SourceFormat::Rgba16Float { hdr: false })
);
assert_eq!(
SourceFormat::Rgba16Float { hdr: false }.bytes_per_pixel(),
8
);
}
#[test]
fn unsupported_format_is_none() {
assert_eq!(SourceFormat::classify(104, 0), None);
assert_eq!(SourceFormat::classify(0, 0), None);
}
#[test]
fn video_and_opaque_formats_stay_unsupported_even_with_hdr_flags() {
for format in [
DXGI_FORMAT_P010,
DXGI_FORMAT_P016,
DXGI_FORMAT_420_OPAQUE,
DXGI_FORMAT_YUY2,
] {
assert_eq!(
SourceFormat::classify(format, GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR),
None,
"DXGI video/opaque format {format} is not a directly readable game backbuffer"
);
}
}
#[test]
fn obs_high_precision_formats_without_reader_conversion_stay_unsupported() {
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R16G16B16A16_UNORM, GAME_CAPTURE_FLAG_HDR),
None,
"OBS maps RGBA16 UNORM, but Fluxer needs a row converter before claiming support"
);
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R32G32B32A32_FLOAT, GAME_CAPTURE_FLAG_HDR),
None,
"OBS maps RGBA32F, but Fluxer has no reader-side conversion for it yet"
);
assert_eq!(
SourceFormat::classify(
DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM,
GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR
),
None,
"XR-bias 10A2 is not equivalent to regular R10G10B10A2 UNORM"
);
}
#[test]
fn f16_round_trips_known_values() {
assert_eq!(f16_to_f32(0x0000), 0.0);
assert_eq!(f16_to_f32(0x3C00), 1.0);
assert_eq!(f16_to_f32(0x4000), 2.0);
assert_eq!(f16_to_f32(0x3800), 0.5);
assert!((f16_to_f32(0xC000) + 2.0).abs() < 1e-6);
}
#[test]
fn f16_special_values_are_bounded_for_sdr_fallback() {
assert_eq!(f16_to_f32(0x7C00), 65504.0);
assert_eq!(f16_to_f32(0xFC00), -65504.0);
assert_eq!(f16_to_f32(0x7E00), 0.0);
}
#[test]
fn r10_sdr_unpack_scales_extremes() {
let packed: u32 = 0x3FF | (0x3FF << 10) | (0x3FF << 20) | (0x3 << 30);
let src = packed.to_le_bytes();
let mut dst = [0u8; 4];
r10g10b10a2_row_to_bgra(&src, 1, &mut dst, false);
assert_eq!(dst, [255, 255, 255, 255]);
let src0 = 0u32.to_le_bytes();
let mut dst0 = [0u8; 4];
r10g10b10a2_row_to_bgra(&src0, 1, &mut dst0, false);
assert_eq!(dst0, [0, 0, 0, 0]);
}
#[test]
fn r10_sdr_unpack_scales_midpoints_and_alpha() {
let packed: u32 = 0x200 | (0x100 << 10) | (0x080 << 20) | (0x2 << 30);
let src = packed.to_le_bytes();
let mut dst = [0u8; 4];
r10g10b10a2_row_to_bgra(&src, 1, &mut dst, false);
assert!((31..=33).contains(&dst[0]), "B was {}", dst[0]);
assert!((63..=65).contains(&dst[1]), "G was {}", dst[1]);
assert!((127..=129).contains(&dst[2]), "R was {}", dst[2]);
assert_eq!(dst[3], 170);
}
#[test]
fn r10_unpack_respects_short_rows() {
let src = [0xFF, 0xFF, 0xFF];
let mut dst = [9u8; 8];
r10g10b10a2_row_to_bgra(&src, 2, &mut dst, false);
assert_eq!(dst, [9u8; 8]);
}
#[test]
fn r10_unpack_channel_order_is_bgra() {
let packed: u32 = 0x3FF;
let src = packed.to_le_bytes();
let mut dst = [0u8; 4];
r10g10b10a2_row_to_bgra(&src, 1, &mut dst, false);
assert_eq!(dst[0], 0, "B should be 0");
assert_eq!(dst[1], 0, "G should be 0");
assert_eq!(dst[2], 255, "R should be 255");
}
#[test]
fn r10_hdr_unpack_is_bounded_and_nonzero() {
let packed: u32 = 0x3FF | (0x200 << 10) | (0x100 << 20) | (0x3 << 30);
let src = packed.to_le_bytes();
let mut dst = [0u8; 4];
r10g10b10a2_row_to_bgra(&src, 1, &mut dst, true);
assert!(dst[2] > 0, "bright red PQ should map to a visible value");
}
#[test]
fn r10_hdr_unpack_is_monotonic_after_tonemap() {
let pack = |v: u32| -> [u8; 4] { (v | (v << 10) | (v << 20) | (0x3 << 30)).to_le_bytes() };
let mut low = [0u8; 4];
let mut mid = [0u8; 4];
let mut high = [0u8; 4];
r10g10b10a2_row_to_bgra(&pack(0x100), 1, &mut low, true);
r10g10b10a2_row_to_bgra(&pack(0x200), 1, &mut mid, true);
r10g10b10a2_row_to_bgra(&pack(0x3FF), 1, &mut high, true);
assert!(low[2] <= mid[2] && mid[2] <= high[2]);
assert_eq!(high[3], 255);
}
#[test]
fn fp16_sdr_clamps_and_encodes_srgb() {
let mut src = [0u8; 8];
src[0..2].copy_from_slice(&0x3C00u16.to_le_bytes());
src[6..8].copy_from_slice(&0x3C00u16.to_le_bytes());
let mut dst = [0u8; 4];
rgba16f_row_to_bgra(&src, 1, &mut dst, false);
assert_eq!(dst[0], 0, "B");
assert_eq!(dst[1], 0, "G");
assert_eq!(dst[2], 255, "R linear 1.0 -> sRGB 255");
assert_eq!(dst[3], 255, "A");
}
#[test]
fn fp16_sdr_clamps_negative_and_above_one_channels() {
let mut src = [0u8; 8];
src[0..2].copy_from_slice(&0xBC00u16.to_le_bytes());
src[2..4].copy_from_slice(&0x4000u16.to_le_bytes());
src[4..6].copy_from_slice(&0x3800u16.to_le_bytes());
src[6..8].copy_from_slice(&0x4000u16.to_le_bytes());
let mut dst = [0u8; 4];
rgba16f_row_to_bgra(&src, 1, &mut dst, false);
assert!((180..=196).contains(&dst[0]), "B linear 0.5 was {}", dst[0]);
assert_eq!(dst[1], 255, "G linear 2.0 clamps to white");
assert_eq!(dst[2], 0, "R negative clamps to black");
assert_eq!(dst[3], 255, "A above 1.0 clamps opaque");
}
#[test]
fn fp16_unpack_respects_short_rows() {
let src = [0x00, 0x3C, 0x00, 0x3C];
let mut dst = [7u8; 8];
rgba16f_row_to_bgra(&src, 2, &mut dst, false);
assert_eq!(dst, [7u8; 8]);
}
#[test]
fn fp16_hdr_above_one_is_tonemapped_not_clipped_to_garbage() {
let mut src = [0u8; 8];
src[0..2].copy_from_slice(&0x4400u16.to_le_bytes());
src[6..8].copy_from_slice(&0x3C00u16.to_le_bytes());
let mut dst = [0u8; 4];
rgba16f_row_to_bgra(&src, 1, &mut dst, true);
assert!(dst[2] > 100, "HDR highlight should be bright after tonemap");
}
#[test]
fn srgb_encode_endpoints() {
assert_eq!(linear_to_srgb8(0.0), 0);
assert_eq!(linear_to_srgb8(1.0), 255);
let mid = linear_to_srgb8(0.5);
assert!((180..=196).contains(&mid), "sRGB(0.5) was {mid}");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,365 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use windows::Win32::Graphics::Direct3D11::{
D3D11_BIND_RENDER_TARGET, D3D11_RESOURCE_MISC_SHARED, D3D11_TEXTURE2D_DESC,
D3D11_USAGE_DEFAULT, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, D3D11_VIDEO_PROCESSOR_CONTENT_DESC,
D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0,
D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0,
D3D11_VIDEO_PROCESSOR_STREAM, D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
D3D11_VPIV_DIMENSION_TEXTURE2D, D3D11_VPOV_DIMENSION_TEXTURE2D, ID3D11Device,
ID3D11DeviceContext, ID3D11Texture2D, ID3D11VideoContext, ID3D11VideoContext1,
ID3D11VideoDevice, ID3D11VideoProcessor, ID3D11VideoProcessorEnumerator,
ID3D11VideoProcessorInputView, ID3D11VideoProcessorOutputView,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709, DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709,
DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020, DXGI_COLOR_SPACE_TYPE,
DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, DXGI_FORMAT_NV12, DXGI_RATIONAL, DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::IDXGIResource;
use windows::core::Interface;
use crate::hdr;
fn vlog(msg: &str) {
if crate::game_capture_abi::env_flag_enabled(crate::game_capture_abi::ENV_VERBOSE) {
use std::io::Write;
let _ = writeln!(std::io::stderr(), "[fluxer-nv12] {msg}");
}
}
pub const NV12_OUTPUT_SLOT_COUNT: usize = 3;
struct Nv12OutputSlot {
_texture: ID3D11Texture2D,
view: ID3D11VideoProcessorOutputView,
handle: u64,
}
pub struct Nv12GpuConverter {
_video_device: ID3D11VideoDevice,
video_context: ID3D11VideoContext,
processor: ID3D11VideoProcessor,
_enumerator: ID3D11VideoProcessorEnumerator,
input_view: ID3D11VideoProcessorInputView,
output_slots: [Nv12OutputSlot; NV12_OUTPUT_SLOT_COUNT],
slot_cursor: usize,
context: ID3D11DeviceContext,
out_width: u32,
out_height: u32,
}
pub struct Nv12SharedTextureFrame {
pub handle: u64,
pub width: u32,
pub height: u32,
pub dxgi_format: u32,
}
impl Nv12GpuConverter {
#[allow(clippy::too_many_arguments)]
pub fn new(
device: &ID3D11Device,
context: &ID3D11DeviceContext,
input: &ID3D11Texture2D,
in_width: u32,
in_height: u32,
out_width: u32,
out_height: u32,
source_format: hdr::SourceFormat,
) -> Option<Self> {
let out_width = (out_width & !1).max(2);
let out_height = (out_height & !1).max(2);
let video_device = device
.cast::<ID3D11VideoDevice>()
.inspect_err(|e| vlog(&format!("cast ID3D11VideoDevice failed: {e:?}")))
.ok()?;
let video_context = context
.cast::<ID3D11VideoContext>()
.inspect_err(|e| vlog(&format!("cast ID3D11VideoContext failed: {e:?}")))
.ok()?;
let content_desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
InputFrameRate: DXGI_RATIONAL {
Numerator: 60,
Denominator: 1,
},
InputWidth: in_width,
InputHeight: in_height,
OutputFrameRate: DXGI_RATIONAL {
Numerator: 60,
Denominator: 1,
},
OutputWidth: out_width,
OutputHeight: out_height,
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
};
let enumerator = unsafe { video_device.CreateVideoProcessorEnumerator(&content_desc) }
.inspect_err(|e| vlog(&format!("CreateVideoProcessorEnumerator: {e:?}")))
.ok()?;
let processor = unsafe { video_device.CreateVideoProcessor(&enumerator, 0) }
.inspect_err(|e| vlog(&format!("CreateVideoProcessor: {e:?}")))
.ok()?;
if let Ok(vctx1) = video_context.cast::<ID3D11VideoContext1>() {
let input_cs = input_colour_space(source_format);
unsafe {
vctx1.VideoProcessorSetStreamColorSpace1(&processor, 0, input_cs);
vctx1.VideoProcessorSetOutputColorSpace1(
&processor,
DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709,
);
}
vlog(&format!(
"video processor colour space set: input={} -> output=YCbCr studio Rec.709",
input_cs.0
));
} else {
vlog("ID3D11VideoContext1 unavailable; using default SDR Rec.709 colour space");
}
let output_desc = D3D11_TEXTURE2D_DESC {
Width: out_width,
Height: out_height,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_NV12,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32,
CPUAccessFlags: 0,
MiscFlags: D3D11_RESOURCE_MISC_SHARED.0 as u32,
};
let mut output_slots = Vec::with_capacity(NV12_OUTPUT_SLOT_COUNT);
for _ in 0..NV12_OUTPUT_SLOT_COUNT {
output_slots.push(create_output_slot(
device,
&video_device,
&enumerator,
&output_desc,
)?);
}
assert_eq!(
output_slots.len(),
NV12_OUTPUT_SLOT_COUNT,
"all NV12 output slots created"
);
let Ok(output_slots) = <[Nv12OutputSlot; NV12_OUTPUT_SLOT_COUNT]>::try_from(output_slots)
else {
vlog("NV12 output slot count mismatch");
return None;
};
let input_view_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
FourCC: 0,
ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
Anonymous: D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0 {
Texture2D: windows::Win32::Graphics::Direct3D11::D3D11_TEX2D_VPIV {
MipSlice: 0,
ArraySlice: 0,
},
},
};
let mut input_view = None;
unsafe {
video_device.CreateVideoProcessorInputView(
input,
&enumerator,
&input_view_desc,
Some(&mut input_view),
)
}
.inspect_err(|e| vlog(&format!("CreateVideoProcessorInputView: {e:?}")))
.ok()?;
let input_view = input_view?;
vlog(&format!(
"NV12 converter built OK ({in_width}x{in_height} -> {out_width}x{out_height})"
));
Some(Self {
_video_device: video_device,
video_context,
processor,
_enumerator: enumerator,
input_view,
output_slots,
slot_cursor: 0,
context: context.clone(),
out_width,
out_height,
})
}
pub fn dxgi_format(&self) -> u32 {
DXGI_FORMAT_NV12.0 as u32
}
pub fn convert_shared_texture(&mut self) -> Result<Nv12SharedTextureFrame, String> {
assert!(
self.slot_cursor < NV12_OUTPUT_SLOT_COUNT,
"slot cursor in range"
);
assert!(self.out_width >= 2, "output width at least 2");
let slot_index = self.slot_cursor;
self.slot_cursor = (slot_index + 1) % NV12_OUTPUT_SLOT_COUNT;
self.run_video_processor(slot_index)?;
unsafe {
self.context.Flush();
}
Ok(Nv12SharedTextureFrame {
handle: self.output_slots[slot_index].handle,
width: self.out_width,
height: self.out_height,
dxgi_format: self.dxgi_format(),
})
}
fn run_video_processor(&self, slot_index: usize) -> Result<(), String> {
assert!(slot_index < NV12_OUTPUT_SLOT_COUNT, "slot index in range");
let mut stream = D3D11_VIDEO_PROCESSOR_STREAM {
Enable: windows::core::BOOL(1),
OutputIndex: 0,
InputFrameOrField: 0,
PastFrames: 0,
FutureFrames: 0,
ppPastSurfaces: std::ptr::null_mut(),
pInputSurface: std::mem::ManuallyDrop::new(Some(self.input_view.clone())),
ppFutureSurfaces: std::ptr::null_mut(),
ppPastSurfacesRight: std::ptr::null_mut(),
pInputSurfaceRight: std::mem::ManuallyDrop::new(None),
ppFutureSurfacesRight: std::ptr::null_mut(),
};
let blt = unsafe {
self.video_context.VideoProcessorBlt(
&self.processor,
&self.output_slots[slot_index].view,
0,
std::slice::from_ref(&stream),
)
};
unsafe {
std::mem::ManuallyDrop::drop(&mut stream.pInputSurface);
}
blt.inspect_err(|e| vlog(&format!("VideoProcessorBlt RGB->NV12: {e:?}")))
.map_err(|e| format!("VideoProcessorBlt RGB->NV12: {e}"))
}
}
fn create_output_slot(
device: &ID3D11Device,
video_device: &ID3D11VideoDevice,
enumerator: &ID3D11VideoProcessorEnumerator,
output_desc: &D3D11_TEXTURE2D_DESC,
) -> Option<Nv12OutputSlot> {
assert!(output_desc.Width >= 2, "output width at least 2");
assert!(output_desc.Height >= 2, "output height at least 2");
let mut output_texture = None;
unsafe { device.CreateTexture2D(output_desc, None, Some(&mut output_texture)) }
.inspect_err(|e| vlog(&format!("CreateTexture2D NV12 output: {e:?}")))
.ok()?;
let output_texture = output_texture?;
let resource: IDXGIResource = output_texture
.cast()
.inspect_err(|e| {
vlog(&format!(
"QueryInterface IDXGIResource for NV12 output: {e:?}"
))
})
.ok()?;
let shared_handle = unsafe { resource.GetSharedHandle() }
.inspect_err(|e| vlog(&format!("GetSharedHandle NV12 output: {e:?}")))
.ok()?;
if shared_handle.is_invalid() {
vlog("GetSharedHandle NV12 output returned an invalid handle");
return None;
}
let shared_handle = shared_handle.0 as usize as u64;
let output_view_desc = D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC {
ViewDimension: D3D11_VPOV_DIMENSION_TEXTURE2D,
Anonymous: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0 {
Texture2D: windows::Win32::Graphics::Direct3D11::D3D11_TEX2D_VPOV { MipSlice: 0 },
},
};
let mut output_view = None;
unsafe {
video_device.CreateVideoProcessorOutputView(
&output_texture,
enumerator,
&output_view_desc,
Some(&mut output_view),
)
}
.inspect_err(|e| vlog(&format!("CreateVideoProcessorOutputView: {e:?}")))
.ok()?;
let output_view = output_view?;
Some(Nv12OutputSlot {
_texture: output_texture,
view: output_view,
handle: shared_handle,
})
}
fn input_colour_space(source_format: hdr::SourceFormat) -> DXGI_COLOR_SPACE_TYPE {
match source_format {
hdr::SourceFormat::R10G10B10A2 { hdr: true } => DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020,
hdr::SourceFormat::Rgba16Float { hdr: true } => DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709,
_ => DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709,
}
}
unsafe impl Send for Nv12GpuConverter {}
#[cfg(test)]
mod tests {
use super::*;
fn cs_value(source_format: hdr::SourceFormat) -> i32 {
input_colour_space(source_format).0
}
#[test]
fn eight_bit_sources_use_sdr_rec709_colour_space() {
assert_eq!(
cs_value(hdr::SourceFormat::Bgra8),
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
);
assert_eq!(
cs_value(hdr::SourceFormat::Rgba8),
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
);
}
#[test]
fn unflagged_high_precision_sources_stay_sdr_rec709() {
assert_eq!(
cs_value(hdr::SourceFormat::R10G10B10A2 { hdr: false }),
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
);
assert_eq!(
cs_value(hdr::SourceFormat::Rgba16Float { hdr: false }),
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
);
}
#[test]
fn ten_bit_hdr_uses_pq_rec2020_input_space() {
assert_eq!(
cs_value(hdr::SourceFormat::R10G10B10A2 { hdr: true }),
DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020.0
);
}
#[test]
fn fp16_hdr_uses_linear_extended_rec709_input_space() {
assert_eq!(
cs_value(hdr::SourceFormat::Rgba16Float { hdr: true }),
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709.0
);
}
}
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::ScreenCaptureSourceDescriptor;
#[cfg(target_os = "windows")]
use std::ptr::null_mut;
#[cfg(target_os = "windows")]
use windows_sys::Win32::{
Foundation::{HWND, LPARAM, RECT},
Graphics::Gdi::{EnumDisplayMonitors, HMONITOR},
System::Threading::GetCurrentProcessId,
UI::WindowsAndMessaging::{
EnumWindows, GW_OWNER, GWL_STYLE, GetWindow, GetWindowLongPtrW, GetWindowRect,
GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible, WS_CHILD,
},
};
#[cfg(target_os = "windows")]
const MONITOR_SOURCE_LIMIT: usize = 16;
pub fn list_sources() -> Vec<ScreenCaptureSourceDescriptor> {
#[cfg(target_os = "windows")]
{
let mut sources = enumerate_monitor_sources();
sources.extend(enumerate_window_sources());
sources
}
#[cfg(not(target_os = "windows"))]
{
Vec::new()
}
}
#[cfg(target_os = "windows")]
fn enumerate_monitor_sources() -> Vec<ScreenCaptureSourceDescriptor> {
unsafe extern "system" fn enum_monitor(
_monitor: HMONITOR,
_hdc: windows_sys::Win32::Graphics::Gdi::HDC,
rect: *mut RECT,
param: LPARAM,
) -> i32 {
let monitors = &mut *(param as *mut Vec<RECT>);
if monitors.len() >= MONITOR_SOURCE_LIMIT {
return 0;
}
if !rect.is_null() {
monitors.push(*rect);
}
1
}
let mut monitors: Vec<RECT> = Vec::new();
unsafe {
EnumDisplayMonitors(
null_mut(),
null_mut(),
Some(enum_monitor),
&mut monitors as *mut _ as LPARAM,
);
}
assert!(
monitors.len() <= MONITOR_SOURCE_LIMIT,
"monitor source enumeration bounded"
);
monitors
.into_iter()
.enumerate()
.map(|(index, rect)| {
let width = (rect.right - rect.left).max(0) as u32;
let height = (rect.bottom - rect.top).max(0) as u32;
ScreenCaptureSourceDescriptor {
kind: "screen".to_string(),
id: format!("screen:{index}:0"),
name: format!("Display {}", index + 1),
width,
height,
target_pid: None,
}
})
.collect()
}
#[cfg(target_os = "windows")]
fn enumerate_window_sources() -> Vec<ScreenCaptureSourceDescriptor> {
struct EnumState {
own_pid: u32,
sources: Vec<ScreenCaptureSourceDescriptor>,
}
unsafe extern "system" fn enum_window(hwnd: HWND, param: LPARAM) -> i32 {
let state = &mut *(param as *mut EnumState);
if let Some(source) = describe_window_source(hwnd, state.own_pid) {
state.sources.push(source);
}
1
}
let mut state = EnumState {
own_pid: unsafe { GetCurrentProcessId() },
sources: Vec::new(),
};
unsafe {
EnumWindows(Some(enum_window), &mut state as *mut _ as LPARAM);
}
state.sources
}
#[cfg(target_os = "windows")]
fn describe_window_source(hwnd: HWND, own_pid: u32) -> Option<ScreenCaptureSourceDescriptor> {
if hwnd.is_null() || unsafe { IsWindowVisible(hwnd) } == 0 {
return None;
}
if !unsafe { GetWindow(hwnd, GW_OWNER) }.is_null() {
return None;
}
let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) } as u32;
if style & WS_CHILD != 0 {
return None;
}
let mut pid = 0u32;
unsafe {
GetWindowThreadProcessId(hwnd, &mut pid);
}
if pid == 0 || pid == own_pid {
return None;
}
let mut rect = RECT::default();
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
return None;
}
let width = (rect.right - rect.left).max(0) as u32;
let height = (rect.bottom - rect.top).max(0) as u32;
if width == 0 || height == 0 {
return None;
}
let title = window_title(hwnd).unwrap_or_else(|| format!("Window {pid}"));
Some(ScreenCaptureSourceDescriptor {
kind: "window".to_string(),
id: format!("window:{}:0", hwnd as usize),
name: title,
width,
height,
target_pid: Some(pid),
})
}
#[cfg(target_os = "windows")]
fn window_title(hwnd: HWND) -> Option<String> {
let len = unsafe { GetWindowTextLengthW(hwnd) };
if len <= 0 {
return None;
}
let mut buf = vec![0u16; (len + 1) as usize];
let copied = unsafe { GetWindowTextW(hwnd, buf.as_mut_ptr(), buf.len() as i32) };
if copied <= 0 {
return None;
}
let title = String::from_utf16_lossy(&buf[..copied as usize])
.trim()
.to_string();
if title.is_empty() { None } else { Some(title) }
}
@@ -0,0 +1,205 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::path::{Path, PathBuf};
use std::ptr::{null, null_mut};
use windows_sys::Win32::{
Foundation::{ERROR_FILE_NOT_FOUND, ERROR_SUCCESS},
System::Registry::{
HKEY, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE, KEY_SET_VALUE, REG_DWORD,
REG_OPTION_NON_VOLATILE, REG_VALUE_TYPE, RegCloseKey, RegCreateKeyExW, RegDeleteValueW,
RegOpenKeyExW, RegQueryValueExW, RegSetValueExW,
},
};
const VULKAN_IMPLICIT_LAYERS_KEY: &str = "Software\\Khronos\\Vulkan\\ImplicitLayers";
const HKCU: HKEY = HKEY_CURRENT_USER;
const HKLM: HKEY = HKEY_LOCAL_MACHINE;
pub struct RegistrationState {
pub registered: bool,
pub manifest_exists: bool,
pub dll_exists: bool,
pub manifest_path: String,
}
fn wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
fn manifest_dll_path(manifest_path: &Path) -> Option<PathBuf> {
let dir = manifest_path.parent()?;
if let Ok(contents) = std::fs::read_to_string(manifest_path)
&& let Some(library) = extract_library_path(&contents)
{
let candidate = Path::new(&library);
if candidate.is_absolute() {
return Some(candidate.to_path_buf());
}
return Some(dir.join(candidate));
}
None
}
fn extract_library_path(contents: &str) -> Option<String> {
let key = "\"library_path\"";
let key_pos = contents.find(key)?;
let rest = contents[key_pos + key.len()..].trim_start();
let rest = rest.strip_prefix(':')?.trim_start();
let rest = rest.strip_prefix('"')?;
let end = rest.find('"')?;
let raw = &rest[..end];
Some(raw.replace("\\\\", "\\"))
}
pub fn register_manifest(manifest_path: &str) -> Result<(), String> {
if manifest_path.trim().is_empty() {
return Err("Vulkan layer manifest path is empty".into());
}
let manifest = Path::new(manifest_path);
if !manifest.is_file() {
return Err(format!(
"Vulkan layer manifest does not exist: {}",
manifest.display()
));
}
if let Some(dll) = manifest_dll_path(manifest)
&& !dll.is_file()
{
return Err(format!(
"Vulkan layer DLL referenced by manifest does not exist: {}",
dll.display()
));
}
set_value_under(HKCU, manifest_path)?;
let _ = set_value_under(HKLM, manifest_path);
Ok(())
}
fn set_value_under(root: HKEY, manifest_path: &str) -> Result<(), String> {
let subkey = wide(VULKAN_IMPLICIT_LAYERS_KEY);
let value_name = wide(manifest_path);
let enabled: u32 = 0;
let mut key: HKEY = null_mut();
let create_status = unsafe {
RegCreateKeyExW(
root,
subkey.as_ptr(),
0,
null(),
REG_OPTION_NON_VOLATILE,
KEY_SET_VALUE,
null(),
&mut key,
null_mut(),
)
};
if create_status != ERROR_SUCCESS {
return Err(format!(
"RegCreateKeyExW Vulkan implicit layers failed: {create_status}"
));
}
let set_status = unsafe {
RegSetValueExW(
key,
value_name.as_ptr(),
0,
REG_DWORD,
(&enabled as *const u32).cast(),
std::mem::size_of::<u32>() as u32,
)
};
unsafe {
RegCloseKey(key);
}
if set_status != ERROR_SUCCESS {
return Err(format!(
"RegSetValueExW Vulkan implicit layer manifest failed: {set_status}"
));
}
Ok(())
}
pub fn unregister_manifest(manifest_path: &str) -> Result<(), String> {
if manifest_path.trim().is_empty() {
return Err("Vulkan layer manifest path is empty".into());
}
let hkcu = delete_value_under(HKCU, manifest_path);
let _ = delete_value_under(HKLM, manifest_path);
hkcu
}
fn delete_value_under(root: HKEY, manifest_path: &str) -> Result<(), String> {
let subkey = wide(VULKAN_IMPLICIT_LAYERS_KEY);
let value_name = wide(manifest_path);
let mut key: HKEY = null_mut();
let open_status = unsafe { RegOpenKeyExW(root, subkey.as_ptr(), 0, KEY_SET_VALUE, &mut key) };
if open_status == ERROR_FILE_NOT_FOUND {
return Ok(());
}
if open_status != ERROR_SUCCESS {
return Err(format!(
"RegOpenKeyExW Vulkan implicit layers failed: {open_status}"
));
}
let delete_status = unsafe { RegDeleteValueW(key, value_name.as_ptr()) };
unsafe {
RegCloseKey(key);
}
if delete_status == ERROR_SUCCESS || delete_status == ERROR_FILE_NOT_FOUND {
Ok(())
} else {
Err(format!(
"RegDeleteValueW Vulkan implicit layer manifest failed: {delete_status}"
))
}
}
pub fn registration_state(manifest_path: &str) -> RegistrationState {
let manifest = Path::new(manifest_path);
let manifest_exists = manifest.is_file();
let dll_exists = manifest_dll_path(manifest)
.map(|dll| dll.is_file())
.unwrap_or(false);
let registered = registry_value_present(manifest_path);
RegistrationState {
registered,
manifest_exists,
dll_exists,
manifest_path: manifest_path.to_string(),
}
}
fn registry_value_present(manifest_path: &str) -> bool {
if manifest_path.trim().is_empty() {
return false;
}
registry_value_present_under(HKCU, manifest_path)
|| registry_value_present_under(HKLM, manifest_path)
}
fn registry_value_present_under(root: HKEY, manifest_path: &str) -> bool {
let subkey = wide(VULKAN_IMPLICIT_LAYERS_KEY);
let value_name = wide(manifest_path);
let mut key: HKEY = null_mut();
let open_status = unsafe { RegOpenKeyExW(root, subkey.as_ptr(), 0, KEY_QUERY_VALUE, &mut key) };
if open_status != ERROR_SUCCESS {
return false;
}
let mut value_type: REG_VALUE_TYPE = 0;
let query_status = unsafe {
RegQueryValueExW(
key,
value_name.as_ptr(),
null(),
&mut value_type,
null_mut(),
null_mut(),
)
};
unsafe {
RegCloseKey(key);
}
query_status == ERROR_SUCCESS
}
@@ -0,0 +1,885 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::sync::{Arc, atomic::Ordering};
use windows::Graphics::Capture::{
Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem, GraphicsCaptureSession,
};
use windows::Graphics::DirectX::Direct3D11::IDirect3DDevice;
use windows::Graphics::DirectX::DirectXPixelFormat;
use windows::Graphics::SizeInt32;
use windows::Win32::Foundation::{HWND, LPARAM, RECT, RPC_E_CHANGED_MODE};
use windows::Win32::Graphics::Direct3D11::{
D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, ID3D11Device,
ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::IDXGIDevice;
use windows::Win32::Graphics::Gdi::{EnumDisplayMonitors, HDC, HMONITOR};
use windows::Win32::System::WinRT::Direct3D11::{
CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess,
};
use windows::Win32::System::WinRT::Graphics::Capture::IGraphicsCaptureItemInterop;
use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize};
use windows::Win32::UI::WindowsAndMessaging::IsWindow;
use windows::core::{BOOL, Interface};
use crate::dxgi_capture::{
SharedTextureOutput, capture_timestamp_us, create_shared_output_texture,
pacing_sleep_and_next_deadline, resolve_output_size,
};
use crate::nv12_gpu::Nv12GpuConverter;
use crate::{
CaptureInner, emit_lifecycle, emit_shared_texture_frame, note_media_frame_without_sink,
resolve_frame_sink,
};
const WGC_FRAME_POOL_BUFFERS: i32 = 2;
const WGC_FRAME_DRAIN_LIMIT: u32 = 4;
const WGC_MONITOR_ENUM_LIMIT: usize = 16;
fn ensure_winrt_initialized() {
let result = unsafe { RoInitialize(RO_INIT_MULTITHREADED) };
if let Err(error) = result {
assert!(
error.code() == RPC_E_CHANGED_MODE,
"RoInitialize failed unexpectedly: {error:?}"
);
}
}
pub fn wgc_capture_supported() -> bool {
ensure_winrt_initialized();
GraphicsCaptureSession::IsSupported().unwrap_or(false)
}
#[derive(Clone, Copy)]
pub enum WgcCaptureTarget {
Window(HWND),
Monitor(HMONITOR),
}
impl WgcCaptureTarget {
fn create_item(
self,
interop: &IGraphicsCaptureItemInterop,
) -> Result<GraphicsCaptureItem, String> {
match self {
Self::Window(hwnd) => unsafe { interop.CreateForWindow(hwnd) }
.map_err(|e| format!("IGraphicsCaptureItemInterop::CreateForWindow: {e}")),
Self::Monitor(monitor) => unsafe { interop.CreateForMonitor(monitor) }
.map_err(|e| format!("IGraphicsCaptureItemInterop::CreateForMonitor: {e}")),
}
}
fn is_alive(self) -> bool {
match self {
Self::Window(hwnd) => unsafe { IsWindow(Some(hwnd)) }.as_bool(),
Self::Monitor(monitor) => !monitor.is_invalid(),
}
}
fn closed_message(self) -> &'static str {
match self {
Self::Window(_) => "window closed",
Self::Monitor(_) => "monitor capture target unavailable",
}
}
}
pub fn parse_monitor_source_id(source_id: &str, source_kind: &str) -> Option<HMONITOR> {
if source_kind != "screen" {
return None;
}
let mut parts = source_id.split(':');
if parts.next()? != "screen" {
return None;
}
let index = parts.next()?.parse::<usize>().ok()?;
let sub_id = parts.next()?;
if sub_id != "0" && sub_id != "1" {
return None;
}
if parts.next().is_some() || index >= WGC_MONITOR_ENUM_LIMIT {
return None;
}
enumerate_monitors().get(index).copied()
}
fn enumerate_monitors() -> Vec<HMONITOR> {
struct MonitorEnumState {
monitors: Vec<HMONITOR>,
}
unsafe extern "system" fn enum_monitor(
monitor: HMONITOR,
_hdc: HDC,
_rect: *mut RECT,
param: LPARAM,
) -> BOOL {
let state = unsafe { &mut *(param.0 as *mut MonitorEnumState) };
if state.monitors.len() >= WGC_MONITOR_ENUM_LIMIT {
return BOOL(0);
}
if !monitor.is_invalid() {
state.monitors.push(monitor);
}
BOOL(1)
}
let mut state = MonitorEnumState {
monitors: Vec::with_capacity(WGC_MONITOR_ENUM_LIMIT),
};
unsafe {
let _ = EnumDisplayMonitors(
None,
None,
Some(enum_monitor),
LPARAM((&mut state as *mut MonitorEnumState) as isize),
);
}
assert!(
state.monitors.len() <= WGC_MONITOR_ENUM_LIMIT,
"monitor enumeration bounded"
);
state.monitors
}
pub struct WgcCaptureSession {
device: ID3D11Device,
context: ID3D11DeviceContext,
d3d_device: IDirect3DDevice,
item: GraphicsCaptureItem,
target: WgcCaptureTarget,
output_width: u32,
output_height: u32,
requested_width: Option<u32>,
requested_height: Option<u32>,
}
unsafe impl Send for WgcCaptureSession {}
impl WgcCaptureSession {
pub fn new(
hwnd: HWND,
requested_width: Option<u32>,
requested_height: Option<u32>,
) -> Result<Self, String> {
Self::new_for_target(
WgcCaptureTarget::Window(hwnd),
requested_width,
requested_height,
)
}
pub fn new_monitor(
monitor: HMONITOR,
requested_width: Option<u32>,
requested_height: Option<u32>,
) -> Result<Self, String> {
Self::new_for_target(
WgcCaptureTarget::Monitor(monitor),
requested_width,
requested_height,
)
}
fn new_for_target(
target: WgcCaptureTarget,
requested_width: Option<u32>,
requested_height: Option<u32>,
) -> Result<Self, String> {
ensure_winrt_initialized();
let (device, context) = crate::game_capture::create_shared_texture_device(None)?;
let dxgi_device: IDXGIDevice = device
.cast()
.map_err(|e| format!("IDXGIDevice cast: {e}"))?;
let inspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device) }
.map_err(|e| format!("CreateDirect3D11DeviceFromDXGIDevice: {e}"))?;
let d3d_device: IDirect3DDevice = inspectable
.cast()
.map_err(|e| format!("IDirect3DDevice cast: {e}"))?;
let interop = windows::core::factory::<GraphicsCaptureItem, IGraphicsCaptureItemInterop>()
.map_err(|e| format!("GraphicsCaptureItem interop factory: {e}"))?;
let item = target.create_item(&interop)?;
let size = item
.Size()
.map_err(|e| format!("GraphicsCaptureItem.Size: {e}"))?;
let content_width = size.Width.max(1) as u32;
let content_height = size.Height.max(1) as u32;
let (output_width, output_height) = resolve_output_size(
content_width,
content_height,
requested_width,
requested_height,
);
assert!(output_width > 0, "WGC output width positive");
assert!(output_height > 0, "WGC output height positive");
Ok(Self {
device,
context,
d3d_device,
item,
target,
output_width,
output_height,
requested_width,
requested_height,
})
}
pub fn capture_width(&self) -> u32 {
assert!(self.output_width > 0, "WGC output width positive");
assert!(self.output_height > 0, "WGC output height positive");
self.output_width
}
pub fn capture_height(&self) -> u32 {
assert!(self.output_height > 0, "WGC output height positive");
assert!(self.output_width > 0, "WGC output width positive");
self.output_height
}
}
struct WgcState {
frame_pool: Direct3D11CaptureFramePool,
session: GraphicsCaptureSession,
output_pipeline: Option<WgcOutputPipeline>,
pixel_format: DirectXPixelFormat,
content_width: u32,
content_height: u32,
out_w: u32,
out_h: u32,
}
impl WgcState {
fn close(self) {
let _ = self.session.Close();
let _ = self.frame_pool.Close();
}
}
struct WgcNv12Pipeline {
input_resource: ID3D11Resource,
converter: Nv12GpuConverter,
}
enum WgcOutputPipeline {
Bgra(SharedTextureOutput),
Nv12(WgcNv12Pipeline),
}
enum WgcFrameResult {
Ok,
NoFrame,
Resized { width: u32, height: u32 },
Error(String),
}
enum LoopStep {
Paced,
Restart,
}
struct WgcLoopContext {
device: ID3D11Device,
context: ID3D11DeviceContext,
d3d_device: IDirect3DDevice,
item: GraphicsCaptureItem,
target: WgcCaptureTarget,
requested_width: Option<u32>,
requested_height: Option<u32>,
}
fn wgc_loop_context(inner: &Arc<CaptureInner>) -> Option<WgcLoopContext> {
let guard = inner.wgc_session.lock();
let session = guard.as_ref()?;
Some(WgcLoopContext {
device: session.device.clone(),
context: session.context.clone(),
d3d_device: session.d3d_device.clone(),
item: session.item.clone(),
target: session.target,
requested_width: session.requested_width,
requested_height: session.requested_height,
})
}
fn teardown_wgc_state(wgc_state: &mut Option<WgcState>) {
if let Some(state) = wgc_state.take() {
state.close();
}
}
fn sleep_with_backoff(backoff: &mut std::time::Duration) {
assert!(
*backoff >= std::time::Duration::from_millis(100),
"backoff at least base interval"
);
assert!(
*backoff <= std::time::Duration::from_secs(2),
"backoff bounded"
);
std::thread::sleep(*backoff);
*backoff = (*backoff * 2).min(std::time::Duration::from_secs(2));
}
pub fn capture_loop(inner: &Arc<CaptureInner>, frame_interval: std::time::Duration) {
ensure_winrt_initialized();
let Some(ctx) = wgc_loop_context(inner) else {
emit_lifecycle(inner, "closed-clean", "no WGC session");
return;
};
let capture_id = inner.capture_id.lock().clone();
let mut wgc_state: Option<WgcState> = None;
let mut recreate_backoff = std::time::Duration::from_millis(100);
let capture_start = std::time::Instant::now();
let mut next_frame_deadline = capture_start + frame_interval;
let mut frames_dropped_coalesced: u64 = 0;
while inner.running.load(Ordering::Acquire) {
if !ctx.target.is_alive() {
emit_lifecycle(inner, "closed", ctx.target.closed_message());
break;
}
if wgc_state.is_none() {
match setup_wgc_state(&ctx) {
Ok(state) => {
wgc_state = Some(state);
}
Err(e) => {
if !inner.running.load(Ordering::Acquire) {
break;
}
emit_lifecycle(
inner,
"error",
&format!("Failed to create WGC capture session: {e}"),
);
sleep_with_backoff(&mut recreate_backoff);
continue;
}
}
}
let Some(state) = wgc_state.as_mut() else {
continue;
};
let result = poll_and_emit_frame(
inner,
&ctx.context,
state,
capture_id.as_deref(),
capture_start,
&mut frames_dropped_coalesced,
);
match handle_frame_result(inner, &ctx, &mut wgc_state, result, &mut recreate_backoff) {
LoopStep::Paced => {}
LoopStep::Restart => continue,
}
let now = std::time::Instant::now();
let (sleep_duration, deadline) =
pacing_sleep_and_next_deadline(now, next_frame_deadline, frame_interval);
next_frame_deadline = deadline;
if sleep_duration > std::time::Duration::ZERO {
std::thread::sleep(sleep_duration);
}
}
teardown_wgc_state(&mut wgc_state);
inner.running.store(false, Ordering::Release);
emit_lifecycle(inner, "closed-clean", "capture stopped");
}
fn handle_frame_result(
inner: &Arc<CaptureInner>,
ctx: &WgcLoopContext,
wgc_state: &mut Option<WgcState>,
result: WgcFrameResult,
recreate_backoff: &mut std::time::Duration,
) -> LoopStep {
match result {
WgcFrameResult::Ok => {
*recreate_backoff = std::time::Duration::from_millis(100);
LoopStep::Paced
}
WgcFrameResult::NoFrame => LoopStep::Paced,
WgcFrameResult::Resized { width, height } => {
let Some(state) = wgc_state.as_mut() else {
return LoopStep::Restart;
};
match resize_wgc_state(ctx, state, width, height) {
Ok(()) => LoopStep::Paced,
Err(e) => {
emit_lifecycle(
inner,
"error",
&format!("WGC frame pool resize failed: {e}"),
);
teardown_wgc_state(wgc_state);
sleep_with_backoff(recreate_backoff);
LoopStep::Restart
}
}
}
WgcFrameResult::Error(e) => {
emit_lifecycle(inner, "error", &e);
{
let mut guard = inner.fallback.lock();
if let Some(tracker) = guard.as_mut() {
let _ = tracker.observe(crate::fallback::FailureSignature::DeviceLost);
}
}
teardown_wgc_state(wgc_state);
sleep_with_backoff(recreate_backoff);
LoopStep::Restart
}
}
}
fn setup_wgc_state(ctx: &WgcLoopContext) -> Result<WgcState, String> {
let size = ctx
.item
.Size()
.map_err(|e| format!("GraphicsCaptureItem.Size: {e}"))?;
let content_width = size.Width.max(1) as u32;
let content_height = size.Height.max(1) as u32;
let (out_w, out_h) = resolve_output_size(
content_width,
content_height,
ctx.requested_width,
ctx.requested_height,
);
create_wgc_state_for_format(
ctx,
DirectXPixelFormat::R16G16B16A16Float,
content_width,
content_height,
out_w,
out_h,
)
.or_else(|_| {
create_wgc_state_for_format(
ctx,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
content_width,
content_height,
out_w,
out_h,
)
})
}
fn create_wgc_state_for_format(
ctx: &WgcLoopContext,
pixel_format: DirectXPixelFormat,
content_width: u32,
content_height: u32,
out_w: u32,
out_h: u32,
) -> Result<WgcState, String> {
let output_pipeline = create_wgc_output_pipeline(
ctx,
pixel_format,
content_width,
content_height,
out_w,
out_h,
)?;
let frame_pool = Direct3D11CaptureFramePool::CreateFreeThreaded(
&ctx.d3d_device,
pixel_format,
WGC_FRAME_POOL_BUFFERS,
SizeInt32 {
Width: content_width as i32,
Height: content_height as i32,
},
)
.map_err(|e| format!("Direct3D11CaptureFramePool::CreateFreeThreaded: {e}"))?;
let session = frame_pool
.CreateCaptureSession(&ctx.item)
.map_err(|e| format!("Direct3D11CaptureFramePool.CreateCaptureSession: {e}"))?;
session
.StartCapture()
.map_err(|e| format!("GraphicsCaptureSession.StartCapture: {e}"))?;
Ok(WgcState {
frame_pool,
session,
output_pipeline,
pixel_format,
content_width,
content_height,
out_w,
out_h,
})
}
fn resize_wgc_state(
ctx: &WgcLoopContext,
state: &mut WgcState,
width: u32,
height: u32,
) -> Result<(), String> {
assert!(width > 0, "WGC resize width positive");
assert!(height > 0, "WGC resize height positive");
let (out_w, out_h) =
resolve_output_size(width, height, ctx.requested_width, ctx.requested_height);
match resize_wgc_state_for_format(ctx, state, state.pixel_format, width, height, out_w, out_h) {
Ok(()) => Ok(()),
Err(first_error) if state.pixel_format == DirectXPixelFormat::R16G16B16A16Float => {
resize_wgc_state_for_format(
ctx,
state,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
width,
height,
out_w,
out_h,
)
.map_err(|fallback_error| {
format!(
"HDR resize failed ({first_error}); BGRA fallback failed ({fallback_error})"
)
})
}
Err(first_error) => Err(first_error),
}
}
fn resize_wgc_state_for_format(
ctx: &WgcLoopContext,
state: &mut WgcState,
pixel_format: DirectXPixelFormat,
width: u32,
height: u32,
out_w: u32,
out_h: u32,
) -> Result<(), String> {
let output_pipeline =
create_wgc_output_pipeline(ctx, pixel_format, width, height, out_w, out_h)?;
state
.frame_pool
.Recreate(
&ctx.d3d_device,
pixel_format,
WGC_FRAME_POOL_BUFFERS,
SizeInt32 {
Width: width as i32,
Height: height as i32,
},
)
.map_err(|e| format!("Direct3D11CaptureFramePool.Recreate: {e}"))?;
state.content_width = width;
state.content_height = height;
state.out_w = out_w;
state.out_h = out_h;
state.pixel_format = pixel_format;
state.output_pipeline = output_pipeline;
Ok(())
}
fn create_wgc_output_pipeline(
ctx: &WgcLoopContext,
pixel_format: DirectXPixelFormat,
content_width: u32,
content_height: u32,
out_w: u32,
out_h: u32,
) -> Result<Option<WgcOutputPipeline>, String> {
if pixel_format == DirectXPixelFormat::R16G16B16A16Float {
return create_wgc_nv12_pipeline(
ctx,
DXGI_FORMAT_R16G16B16A16_FLOAT,
content_width,
content_height,
out_w,
out_h,
crate::hdr::SourceFormat::Rgba16Float { hdr: true },
)
.map(WgcOutputPipeline::Nv12)
.map(Some);
}
if out_w != content_width || out_h != content_height {
return create_wgc_nv12_pipeline(
ctx,
DXGI_FORMAT_B8G8R8A8_UNORM,
content_width,
content_height,
out_w,
out_h,
crate::hdr::SourceFormat::Bgra8,
)
.map(WgcOutputPipeline::Nv12)
.map(Some);
}
Ok(
create_shared_output_texture(&ctx.device, content_width, content_height)
.ok()
.map(WgcOutputPipeline::Bgra),
)
}
fn create_wgc_nv12_pipeline(
ctx: &WgcLoopContext,
input_format: DXGI_FORMAT,
content_width: u32,
content_height: u32,
out_w: u32,
out_h: u32,
source_format: crate::hdr::SourceFormat,
) -> Result<WgcNv12Pipeline, String> {
assert!(content_width > 0, "WGC NV12 input width positive");
assert!(content_height > 0, "WGC NV12 input height positive");
let input_desc = D3D11_TEXTURE2D_DESC {
Width: content_width,
Height: content_height,
MipLevels: 1,
ArraySize: 1,
Format: input_format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
CPUAccessFlags: 0,
MiscFlags: 0,
};
let mut input_texture = None;
unsafe {
ctx.device
.CreateTexture2D(&input_desc, None, Some(&mut input_texture))
}
.map_err(|e| format!("CreateTexture2D WGC NV12 input: {e}"))?;
let input_texture =
input_texture.ok_or_else(|| "CreateTexture2D WGC NV12 input returned null".to_string())?;
let input_resource: ID3D11Resource = input_texture
.cast()
.map_err(|e| format!("ID3D11Resource WGC NV12 input cast: {e}"))?;
let converter = Nv12GpuConverter::new(
&ctx.device,
&ctx.context,
&input_texture,
content_width,
content_height,
out_w,
out_h,
source_format,
)
.ok_or_else(|| "WGC NV12 converter unavailable".to_string())?;
Ok(WgcNv12Pipeline {
input_resource,
converter,
})
}
fn poll_and_emit_frame(
inner: &Arc<CaptureInner>,
context: &ID3D11DeviceContext,
state: &mut WgcState,
capture_id: Option<&str>,
capture_start: std::time::Instant,
frames_dropped_coalesced: &mut u64,
) -> WgcFrameResult {
let mut newest: Option<Direct3D11CaptureFrame> = None;
let mut drained: u32 = 0;
while drained < WGC_FRAME_DRAIN_LIMIT {
let Ok(frame) = state.frame_pool.TryGetNextFrame() else {
break;
};
if let Some(previous) = newest.replace(frame) {
*frames_dropped_coalesced += 1;
let _ = previous.Close();
}
drained += 1;
}
assert!(drained <= WGC_FRAME_DRAIN_LIMIT, "frame drain bounded");
let Some(frame) = newest else {
return WgcFrameResult::NoFrame;
};
let result = emit_wgc_frame(inner, context, state, capture_id, &frame, capture_start);
let _ = frame.Close();
result
}
fn wgc_frame_source_resource(frame: &Direct3D11CaptureFrame) -> Result<ID3D11Resource, String> {
let surface = frame
.Surface()
.map_err(|e| format!("Direct3D11CaptureFrame.Surface: {e}"))?;
let access: IDirect3DDxgiInterfaceAccess = surface
.cast()
.map_err(|e| format!("IDirect3DDxgiInterfaceAccess cast: {e}"))?;
let source_texture: ID3D11Texture2D =
unsafe { access.GetInterface() }.map_err(|e| format!("WGC surface GetInterface: {e}"))?;
source_texture
.cast()
.map_err(|e| format!("ID3D11Resource WGC surface cast: {e}"))
}
fn emit_wgc_frame(
inner: &Arc<CaptureInner>,
context: &ID3D11DeviceContext,
state: &mut WgcState,
capture_id: Option<&str>,
frame: &Direct3D11CaptureFrame,
capture_start: std::time::Instant,
) -> WgcFrameResult {
let content = match frame.ContentSize() {
Ok(size) => size,
Err(e) => {
return WgcFrameResult::Error(format!("Direct3D11CaptureFrame.ContentSize: {e}"));
}
};
let content_width = content.Width.max(1) as u32;
let content_height = content.Height.max(1) as u32;
if content_width != state.content_width || content_height != state.content_height {
return WgcFrameResult::Resized {
width: content_width,
height: content_height,
};
}
let Some(frame_sink) = resolve_frame_sink(inner, capture_id) else {
note_media_frame_without_sink(
inner,
"WGC frame dropped because no native frame sink is registered",
);
return WgcFrameResult::Ok;
};
let source_resource = match wgc_frame_source_resource(frame) {
Ok(resource) => resource,
Err(e) => return WgcFrameResult::Error(e),
};
let timestamp_us = capture_timestamp_us(capture_start);
let Some(output_pipeline) = state.output_pipeline.as_mut() else {
return WgcFrameResult::Error("WGC shared texture output unavailable".into());
};
match output_pipeline {
WgcOutputPipeline::Bgra(shared_output) => emit_wgc_bgra_frame(
inner,
context,
&frame_sink,
shared_output,
&source_resource,
content_width,
content_height,
timestamp_us,
),
WgcOutputPipeline::Nv12(pipeline) => emit_wgc_nv12_frame(
inner,
context,
&frame_sink,
pipeline,
&source_resource,
content_width,
content_height,
timestamp_us,
),
}
}
#[allow(clippy::too_many_arguments)]
fn emit_wgc_bgra_frame(
inner: &Arc<CaptureInner>,
context: &ID3D11DeviceContext,
frame_sink: &crate::FrameSinkRef,
shared_output: &mut SharedTextureOutput,
source_resource: &ID3D11Resource,
content_width: u32,
content_height: u32,
timestamp_us: i64,
) -> WgcFrameResult {
if shared_output.width != content_width || shared_output.height != content_height {
return WgcFrameResult::Error(
"WGC BGRA shared output size does not match content size".into(),
);
}
let slot_index = shared_output.next_slot_index();
let slot = &shared_output.slots[slot_index];
let output_resource: ID3D11Resource = match slot.texture.cast() {
Ok(resource) => resource,
Err(e) => return WgcFrameResult::Error(format!("ID3D11Resource shared output cast: {e}")),
};
let src_box = D3D11_BOX {
left: 0,
top: 0,
front: 0,
right: content_width,
bottom: content_height,
back: 1,
};
unsafe {
context.CopySubresourceRegion(
&output_resource,
0,
0,
0,
0,
source_resource,
0,
Some(&src_box),
);
context.Flush();
}
let _ = emit_shared_texture_frame(
inner,
frame_sink,
slot.handle,
shared_output.width,
shared_output.height,
shared_output.dxgi_format,
timestamp_us,
);
WgcFrameResult::Ok
}
#[allow(clippy::too_many_arguments)]
fn emit_wgc_nv12_frame(
inner: &Arc<CaptureInner>,
context: &ID3D11DeviceContext,
frame_sink: &crate::FrameSinkRef,
pipeline: &mut WgcNv12Pipeline,
source_resource: &ID3D11Resource,
content_width: u32,
content_height: u32,
timestamp_us: i64,
) -> WgcFrameResult {
let src_box = D3D11_BOX {
left: 0,
top: 0,
front: 0,
right: content_width,
bottom: content_height,
back: 1,
};
unsafe {
context.CopySubresourceRegion(
&pipeline.input_resource,
0,
0,
0,
0,
source_resource,
0,
Some(&src_box),
);
}
let frame = match pipeline.converter.convert_shared_texture() {
Ok(frame) => frame,
Err(error) => return WgcFrameResult::Error(error),
};
let _ = emit_shared_texture_frame(
inner,
frame_sink,
frame.handle,
frame.width,
frame.height,
frame.dxgi_format,
timestamp_us,
);
WgcFrameResult::Ok
}
@@ -0,0 +1,156 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "d3d10-present-fixture"
version = "0.0.0"
dependencies = [
"windows",
"windows-core",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
@@ -0,0 +1,29 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
name = "d3d10-present-fixture"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
# matching the sibling crates (hook/, vulkan-layer/) in this package.
[workspace]
resolver = "2"
[[bin]]
name = "d3d10-present-fixture"
path = "src/main.rs"
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D10",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_UI_WindowsAndMessaging",
]}
windows-core = "0.62.2"
@@ -0,0 +1,389 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(not(windows))]
fn main() {
eprintln!("d3d10-present-fixture is only functional on Windows");
}
#[cfg(windows)]
fn main() -> windows_core::Result<()> {
windows_impl::run()
}
#[cfg_attr(not(windows), allow(dead_code))]
mod pattern {
pub const BARS: [(u8, u8, u8); 8] = [
(255, 255, 255),
(255, 255, 0),
(0, 255, 255),
(0, 255, 0),
(255, 0, 255),
(255, 0, 0),
(0, 0, 255),
(0, 0, 0),
];
pub const COUNTER_BLOCK: u32 = 16;
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
let width = width.max(1);
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
BARS[index.min(BARS.len() - 1)]
}
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
let low = (frame_index & 0x00FF_FFFF) as u32;
(
(low & 0xFF) as u8,
((low >> 8) & 0xFF) as u8,
((low >> 16) & 0xFF) as u8,
)
}
}
#[cfg(windows)]
mod cli {
pub struct Options {
pub frames: Option<u64>,
pub width: u32,
pub height: u32,
pub borderless: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
frames: None,
width: 1280,
height: 720,
borderless: false,
}
}
}
impl Options {
pub fn from_env() -> Self {
let mut options = Options::default();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--frames" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.frames = Some(value);
}
}
"--width" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.width = value;
}
}
"--height" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.height = value;
}
}
"--borderless" => options.borderless = true,
"--windowed" => options.borderless = false,
_ => {}
}
}
options
}
}
}
#[cfg(windows)]
mod windows_impl {
use super::{cli::Options, pattern};
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Graphics::Direct3D10::{
D3D10_DRIVER_TYPE_HARDWARE, D3D10_MAPPED_TEXTURE2D, D3D10_SDK_VERSION, D3D10CreateDevice,
ID3D10Device, ID3D10RenderTargetView, ID3D10Resource, ID3D10Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_MODE_DESC,
DXGI_RATIONAL, DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::{
DXGI_PRESENT, DXGI_SWAP_CHAIN_DESC, DXGI_SWAP_EFFECT_DISCARD,
DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIDevice, IDXGIFactory, IDXGISwapChain,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::{
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW,
ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
WS_POPUP, WS_VISIBLE,
};
use windows_core::{BOOL, Interface, Result, w};
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
match msg {
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
unsafe {
let instance = GetModuleHandleW(None)?;
let class_name = w!("FluxerD3D10Fixture");
let wc = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wnd_proc),
hInstance: instance.into(),
lpszClassName: class_name,
..Default::default()
};
RegisterClassExW(&wc);
let style = if borderless {
WS_POPUP | WS_VISIBLE
} else {
WS_OVERLAPPEDWINDOW | WS_VISIBLE
};
let hwnd = CreateWindowExW(
Default::default(),
class_name,
w!("Fluxer D3D10 Present Fixture"),
style,
CW_USEDEFAULT,
CW_USEDEFAULT,
width as i32,
height as i32,
None,
None,
Some(instance.into()),
None,
)?;
let _ = ShowWindow(hwnd, SW_SHOW);
Ok(hwnd)
}
}
struct Renderer {
device: ID3D10Device,
swap_chain: IDXGISwapChain,
rtv: ID3D10RenderTargetView,
width: u32,
height: u32,
staging: ID3D10Texture2D,
format: DXGI_FORMAT,
}
impl Renderer {
unsafe fn new(hwnd: HWND, width: u32, height: u32) -> Result<Self> {
unsafe {
let mut device: Option<ID3D10Device> = None;
D3D10CreateDevice(
None,
D3D10_DRIVER_TYPE_HARDWARE,
HMODULE::default(),
0,
D3D10_SDK_VERSION,
Some(&mut device),
)?;
let device = device.expect("D3D10CreateDeviceAndSwapChain yielded no device");
let dxgi_device: IDXGIDevice = device.cast()?;
let adapter = dxgi_device.GetAdapter()?;
let factory: IDXGIFactory = adapter.GetParent()?;
let (swap_chain, format) =
Self::make_swap_chain(&factory, &device, hwnd, width, height)?;
let rtv = Self::make_rtv(&device, &swap_chain)?;
let staging = Self::make_staging(&device, width, height, format)?;
Ok(Self {
device,
swap_chain,
rtv,
width,
height,
staging,
format,
})
}
}
unsafe fn make_swap_chain(
factory: &IDXGIFactory,
device: &ID3D10Device,
hwnd: HWND,
width: u32,
height: u32,
) -> Result<(IDXGISwapChain, DXGI_FORMAT)> {
unsafe {
let mut last = windows_core::HRESULT(0);
for format in [DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM] {
let desc = DXGI_SWAP_CHAIN_DESC {
BufferDesc: DXGI_MODE_DESC {
Width: width,
Height: height,
RefreshRate: DXGI_RATIONAL {
Numerator: 0,
Denominator: 0,
},
Format: format,
..Default::default()
},
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
BufferCount: 1,
OutputWindow: hwnd,
Windowed: BOOL(1),
SwapEffect: DXGI_SWAP_EFFECT_DISCARD,
Flags: 0,
};
let mut swap_chain: Option<IDXGISwapChain> = None;
let hr = factory.CreateSwapChain(device, &desc, &mut swap_chain);
if hr.is_ok() {
return Ok((
swap_chain.expect("CreateSwapChain yielded no swapchain"),
format,
));
}
last = hr;
}
last.ok()?;
unreachable!()
}
}
unsafe fn make_rtv(
device: &ID3D10Device,
swap_chain: &IDXGISwapChain,
) -> Result<ID3D10RenderTargetView> {
unsafe {
let back_buffer: ID3D10Texture2D = swap_chain.GetBuffer(0)?;
let resource: ID3D10Resource = back_buffer.cast()?;
let mut rtv: Option<ID3D10RenderTargetView> = None;
device.CreateRenderTargetView(&resource, None, Some(&mut rtv))?;
Ok(rtv.expect("CreateRenderTargetView yielded no view"))
}
}
unsafe fn make_staging(
device: &ID3D10Device,
width: u32,
height: u32,
format: DXGI_FORMAT,
) -> Result<ID3D10Texture2D> {
use windows::Win32::Graphics::Direct3D10::{
D3D10_CPU_ACCESS_WRITE, D3D10_TEXTURE2D_DESC, D3D10_USAGE_STAGING,
};
unsafe {
let desc = D3D10_TEXTURE2D_DESC {
Width: width,
Height: height,
MipLevels: 1,
ArraySize: 1,
Format: format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D10_USAGE_STAGING,
BindFlags: 0,
CPUAccessFlags: D3D10_CPU_ACCESS_WRITE.0 as u32,
MiscFlags: 0,
};
device.CreateTexture2D(&desc, None)
}
}
unsafe fn render_frame(&self, frame_index: u64) -> Result<()> {
use windows::Win32::Graphics::Direct3D10::D3D10_MAP_WRITE;
unsafe {
let (cr, cg, cb) =
pattern::BARS[(frame_index % pattern::BARS.len() as u64) as usize];
let clear = [cr as f32 / 255.0, cg as f32 / 255.0, cb as f32 / 255.0, 1.0];
self.device.ClearRenderTargetView(&self.rtv, &clear);
let mapped: D3D10_MAPPED_TEXTURE2D = self.staging.Map(0, D3D10_MAP_WRITE, 0)?;
let row_pitch = mapped.RowPitch as usize;
let base = mapped.pData as *mut u8;
let counter = pattern::counter_colour(frame_index);
let luma = (frame_index & 0xFF) as u8;
for y in 0..self.height {
let row = base.add(y as usize * row_pitch);
for x in 0..self.width {
let (r, g, b) = if x < pattern::COUNTER_BLOCK && y < pattern::COUNTER_BLOCK
{
counter
} else {
pattern::bar_colour(x, self.width)
};
let px = row.add(x as usize * 4);
if self.format == DXGI_FORMAT_R8G8B8A8_UNORM {
*px = r;
*px.add(1) = g;
*px.add(2) = b.saturating_add(luma / 4);
} else {
*px = b.saturating_add(luma / 4);
*px.add(1) = g;
*px.add(2) = r;
}
*px.add(3) = 255;
}
}
self.staging.Unmap(0);
let back_buffer: ID3D10Texture2D = self.swap_chain.GetBuffer(0)?;
let dst: ID3D10Resource = back_buffer.cast()?;
let src: ID3D10Resource = self.staging.cast()?;
self.device.CopyResource(&dst, &src);
self.swap_chain.Present(1, DXGI_PRESENT(0)).ok()?;
let _ = &self.device;
Ok(())
}
}
}
pub fn run() -> Result<()> {
let options = Options::from_env();
unsafe {
let hwnd = create_window(options.width, options.height, options.borderless)?;
let mut rect = Default::default();
let _ = GetClientRect(hwnd, &mut rect);
let width = ((rect.right - rect.left).max(1)) as u32;
let height = ((rect.bottom - rect.top).max(1)) as u32;
let renderer = Renderer::new(hwnd, width, height)?;
println!("HWND={}", hwnd.0 as isize);
use std::io::Write;
let _ = std::io::stdout().flush();
let mut frame_index: u64 = 0;
let mut msg = MSG::default();
loop {
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
return Ok(());
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
renderer.render_frame(frame_index)?;
frame_index += 1;
if let Some(limit) = options.frames {
if frame_index >= limit {
return Ok(());
}
}
}
}
}
}
@@ -0,0 +1,156 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "d3d11-present-fixture"
version = "0.0.0"
dependencies = [
"windows",
"windows-core",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
@@ -0,0 +1,29 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
name = "d3d11-present-fixture"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
# matching the sibling crates (hook/, vulkan-layer/) in this package.
[workspace]
resolver = "2"
[[bin]]
name = "d3d11-present-fixture"
path = "src/main.rs"
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_UI_WindowsAndMessaging",
]}
windows-core = "0.62.2"
@@ -0,0 +1,496 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(not(windows))]
fn main() {
eprintln!("d3d11-present-fixture is only functional on Windows");
}
#[cfg(windows)]
fn main() -> windows_core::Result<()> {
windows_impl::run()
}
#[cfg_attr(not(windows), allow(dead_code))]
mod pattern {
pub const BARS: [(u8, u8, u8); 8] = [
(255, 255, 255),
(255, 255, 0),
(0, 255, 255),
(0, 255, 0),
(255, 0, 255),
(255, 0, 0),
(0, 0, 255),
(0, 0, 0),
];
pub const COUNTER_BLOCK: u32 = 16;
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
let width = width.max(1);
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
BARS[index.min(BARS.len() - 1)]
}
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
let low = (frame_index & 0x00FF_FFFF) as u32;
(
(low & 0xFF) as u8,
((low >> 8) & 0xFF) as u8,
((low >> 16) & 0xFF) as u8,
)
}
}
#[cfg_attr(not(windows), allow(dead_code))]
mod cli {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PresentFormat {
Bgra8,
Rgba8,
R10G10B10A2,
Rgba16Float,
}
impl PresentFormat {
fn parse(value: &str) -> Option<Self> {
match value {
"bgra8" | "b8g8r8a8" => Some(Self::Bgra8),
"rgba8" | "r8g8b8a8" => Some(Self::Rgba8),
"r10g10b10a2" | "rgb10a2" | "10bit" => Some(Self::R10G10B10A2),
"rgba16f" | "r16g16b16a16f" | "fp16" => Some(Self::Rgba16Float),
_ => None,
}
}
}
pub struct Options {
pub frames: Option<u64>,
pub width: u32,
pub height: u32,
pub borderless: bool,
pub format: PresentFormat,
}
impl Default for Options {
fn default() -> Self {
Self {
frames: None,
width: 1280,
height: 720,
borderless: false,
format: PresentFormat::Bgra8,
}
}
}
impl Options {
pub fn from_env() -> Self {
let mut options = Options::default();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--frames" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.frames = Some(value);
}
}
"--width" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.width = value;
}
}
"--height" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.height = value;
}
}
"--borderless" => options.borderless = true,
"--windowed" => options.borderless = false,
"--format" => {
if let Some(format) = args.next().and_then(|v| PresentFormat::parse(&v)) {
options.format = format;
}
}
_ => {}
}
}
options
}
}
#[cfg(test)]
mod tests {
use super::PresentFormat;
#[test]
fn parses_explicit_format_modes() {
assert_eq!(PresentFormat::parse("bgra8"), Some(PresentFormat::Bgra8));
assert_eq!(PresentFormat::parse("rgba8"), Some(PresentFormat::Rgba8));
assert_eq!(
PresentFormat::parse("r10g10b10a2"),
Some(PresentFormat::R10G10B10A2)
);
assert_eq!(
PresentFormat::parse("fp16"),
Some(PresentFormat::Rgba16Float)
);
assert_eq!(PresentFormat::parse("unknown"), None);
}
}
}
#[cfg(windows)]
mod windows_impl {
use super::{
cli::{Options, PresentFormat},
pattern,
};
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
use windows::Win32::Graphics::Direct3D11::{
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION,
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11RenderTargetView,
ID3D11Resource, ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM,
DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT,
DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::{
DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1, DXGI_SWAP_EFFECT_FLIP_DISCARD,
DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIDevice, IDXGIFactory2, IDXGISwapChain1,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::{
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW,
ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
WS_POPUP, WS_VISIBLE,
};
use windows_core::{Interface, Result, w};
impl PresentFormat {
fn dxgi(self) -> DXGI_FORMAT {
match self {
PresentFormat::Bgra8 => DXGI_FORMAT_B8G8R8A8_UNORM,
PresentFormat::Rgba8 => DXGI_FORMAT_R8G8B8A8_UNORM,
PresentFormat::R10G10B10A2 => DXGI_FORMAT_R10G10B10A2_UNORM,
PresentFormat::Rgba16Float => DXGI_FORMAT_R16G16B16A16_FLOAT,
}
}
fn bytes_per_pixel(self) -> usize {
match self {
PresentFormat::Bgra8 | PresentFormat::Rgba8 | PresentFormat::R10G10B10A2 => 4,
PresentFormat::Rgba16Float => 8,
}
}
}
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
match msg {
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
unsafe {
let instance = GetModuleHandleW(None)?;
let class_name = w!("FluxerD3D11Fixture");
let wc = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wnd_proc),
hInstance: instance.into(),
lpszClassName: class_name,
..Default::default()
};
RegisterClassExW(&wc);
let style = if borderless {
WS_POPUP | WS_VISIBLE
} else {
WS_OVERLAPPEDWINDOW | WS_VISIBLE
};
let hwnd = CreateWindowExW(
Default::default(),
class_name,
w!("Fluxer D3D11 Present Fixture"),
style,
CW_USEDEFAULT,
CW_USEDEFAULT,
width as i32,
height as i32,
None,
None,
Some(instance.into()),
None,
)?;
let _ = ShowWindow(hwnd, SW_SHOW);
Ok(hwnd)
}
}
struct Renderer {
device: ID3D11Device,
context: ID3D11DeviceContext,
swap_chain: IDXGISwapChain1,
rtv: ID3D11RenderTargetView,
width: u32,
height: u32,
staging: ID3D11Texture2D,
format: PresentFormat,
}
impl Renderer {
unsafe fn new(hwnd: HWND, width: u32, height: u32, format: PresentFormat) -> Result<Self> {
unsafe {
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)?;
let device = device.expect("D3D11CreateDevice yielded no device");
let context = context.expect("D3D11CreateDevice yielded no context");
let dxgi_device: IDXGIDevice = device.cast()?;
let adapter = dxgi_device.GetAdapter()?;
let factory: IDXGIFactory2 = adapter.GetParent()?;
let desc = DXGI_SWAP_CHAIN_DESC1 {
Width: width,
Height: height,
Format: format.dxgi(),
Stereo: false.into(),
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
BufferCount: 2,
Scaling: DXGI_SCALING_STRETCH,
SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD,
AlphaMode: DXGI_ALPHA_MODE_IGNORE,
Flags: 0,
};
let swap_chain =
factory.CreateSwapChainForHwnd(&device, hwnd, &desc, None, None)?;
let rtv = Self::make_rtv(&device, &swap_chain)?;
let staging = Self::make_staging(&device, width, height, format)?;
Ok(Self {
device,
context,
swap_chain,
rtv,
width,
height,
staging,
format,
})
}
}
unsafe fn make_rtv(
device: &ID3D11Device,
swap_chain: &IDXGISwapChain1,
) -> Result<ID3D11RenderTargetView> {
unsafe {
let back_buffer: ID3D11Texture2D = swap_chain.GetBuffer(0)?;
let resource: ID3D11Resource = back_buffer.cast()?;
let mut rtv: Option<ID3D11RenderTargetView> = None;
device.CreateRenderTargetView(&resource, None, Some(&mut rtv))?;
Ok(rtv.expect("CreateRenderTargetView yielded no view"))
}
}
unsafe fn make_staging(
device: &ID3D11Device,
width: u32,
height: u32,
format: PresentFormat,
) -> Result<ID3D11Texture2D> {
use windows::Win32::Graphics::Direct3D11::{
D3D11_CPU_ACCESS_WRITE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING,
};
unsafe {
let desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: height,
MipLevels: 1,
ArraySize: 1,
Format: format.dxgi(),
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_STAGING,
BindFlags: 0,
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
MiscFlags: 0,
};
let mut staging: Option<ID3D11Texture2D> = None;
device.CreateTexture2D(&desc, None, Some(&mut staging))?;
Ok(staging.expect("CreateTexture2D yielded no staging texture"))
}
}
unsafe fn render_frame(&self, frame_index: u64) -> Result<()> {
use windows::Win32::Graphics::Direct3D11::D3D11_MAP_WRITE;
unsafe {
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
self.context
.Map(&self.staging, 0, D3D11_MAP_WRITE, 0, Some(&mut mapped))?;
let row_pitch = mapped.RowPitch as usize;
let base = mapped.pData as *mut u8;
let counter = pattern::counter_colour(frame_index);
let luma = (frame_index & 0xFF) as u8;
let bpp = self.format.bytes_per_pixel();
for y in 0..self.height {
let row = base.add(y as usize * row_pitch);
for x in 0..self.width {
let (r, g, b) = if x < pattern::COUNTER_BLOCK && y < pattern::COUNTER_BLOCK
{
counter
} else {
pattern::bar_colour(x, self.width)
};
let px = row.add(x as usize * bpp);
write_pixel(self.format, px, r, g, b.saturating_add(luma / 4));
}
}
self.context.Unmap(&self.staging, 0);
let back_buffer: ID3D11Texture2D = self.swap_chain.GetBuffer(0)?;
let dst: ID3D11Resource = back_buffer.cast()?;
let src: ID3D11Resource = self.staging.cast()?;
self.context.CopyResource(&dst, &src);
let _ = &self.rtv;
self.swap_chain.Present(1, Default::default()).ok()?;
let _ = &self.device;
Ok(())
}
}
}
unsafe fn write_pixel(format: PresentFormat, px: *mut u8, r: u8, g: u8, b: u8) {
unsafe {
match format {
PresentFormat::Bgra8 => {
*px = b;
*px.add(1) = g;
*px.add(2) = r;
*px.add(3) = 255;
}
PresentFormat::Rgba8 => {
*px = r;
*px.add(1) = g;
*px.add(2) = b;
*px.add(3) = 255;
}
PresentFormat::R10G10B10A2 => {
let packed = scale8_to_10(r)
| (scale8_to_10(g) << 10)
| (scale8_to_10(b) << 20)
| (0x3 << 30);
std::ptr::copy_nonoverlapping(packed.to_le_bytes().as_ptr(), px, 4);
}
PresentFormat::Rgba16Float => {
let highlight = if r == 255 && g == 255 && b > 240 {
1.25
} else {
1.0
};
let values = [
f32_to_f16((r as f32 / 255.0) * highlight),
f32_to_f16((g as f32 / 255.0) * highlight),
f32_to_f16((b as f32 / 255.0) * highlight),
f32_to_f16(1.0),
];
for (index, value) in values.iter().enumerate() {
std::ptr::copy_nonoverlapping(
value.to_le_bytes().as_ptr(),
px.add(index * 2),
2,
);
}
}
}
}
}
fn scale8_to_10(value: u8) -> u32 {
(value as u32 * 1023 + 127) / 255
}
fn f32_to_f16(value: f32) -> u16 {
let value = value.clamp(0.0, 65504.0);
if value == 0.0 {
return 0;
}
let bits = value.to_bits();
let exp = ((bits >> 23) & 0xFF) as i32 - 127 + 15;
let mant = bits & 0x7F_FFFF;
if exp <= 0 {
return 0;
}
if exp >= 31 {
return 0x7BFF;
}
((exp as u16) << 10) | (((mant + 0x1000) >> 13) as u16)
}
pub fn run() -> Result<()> {
let options = Options::from_env();
unsafe {
let hwnd = create_window(options.width, options.height, options.borderless)?;
let mut rect = Default::default();
let _ = GetClientRect(hwnd, &mut rect);
let width = ((rect.right - rect.left).max(1)) as u32;
let height = ((rect.bottom - rect.top).max(1)) as u32;
let renderer = Renderer::new(hwnd, width, height, options.format)?;
println!("HWND={}", hwnd.0 as isize);
use std::io::Write;
let _ = std::io::stdout().flush();
let mut frame_index: u64 = 0;
let mut msg = MSG::default();
loop {
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
return Ok(());
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
renderer.render_frame(frame_index)?;
frame_index += 1;
if let Some(limit) = options.frames {
if frame_index >= limit {
return Ok(());
}
}
}
}
}
}
@@ -0,0 +1,156 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "d3d12-present-fixture"
version = "0.0.0"
dependencies = [
"windows",
"windows-core",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
@@ -0,0 +1,31 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
name = "d3d12-present-fixture"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
# matching the sibling crates (hook/, vulkan-layer/) in this package.
[workspace]
resolver = "2"
[[bin]]
name = "d3d12-present-fixture"
path = "src/main.rs"
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D12",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Security",
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
]}
windows-core = "0.62.2"
@@ -0,0 +1,582 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(not(windows))]
fn main() {
eprintln!("d3d12-present-fixture is only functional on Windows");
}
#[cfg(windows)]
fn main() -> windows_core::Result<()> {
windows_impl::run()
}
#[cfg_attr(not(windows), allow(dead_code))]
mod pattern {
pub const BARS: [(u8, u8, u8); 8] = [
(255, 255, 255),
(255, 255, 0),
(0, 255, 255),
(0, 255, 0),
(255, 0, 255),
(255, 0, 0),
(0, 0, 255),
(0, 0, 0),
];
pub const COUNTER_BLOCK: u32 = 16;
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
let width = width.max(1);
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
BARS[index.min(BARS.len() - 1)]
}
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
let low = (frame_index & 0x00FF_FFFF) as u32;
(
(low & 0xFF) as u8,
((low >> 8) & 0xFF) as u8,
((low >> 16) & 0xFF) as u8,
)
}
}
#[cfg_attr(not(windows), allow(dead_code))]
mod cli {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PresentFormat {
Bgra8,
Rgba8,
R10G10B10A2,
Rgba16Float,
}
impl PresentFormat {
fn parse(value: &str) -> Option<Self> {
match value {
"bgra8" | "b8g8r8a8" => Some(Self::Bgra8),
"rgba8" | "r8g8b8a8" => Some(Self::Rgba8),
"r10g10b10a2" | "rgb10a2" | "10bit" => Some(Self::R10G10B10A2),
"rgba16f" | "r16g16b16a16f" | "fp16" => Some(Self::Rgba16Float),
_ => None,
}
}
}
pub struct Options {
pub frames: Option<u64>,
pub width: u32,
pub height: u32,
pub borderless: bool,
pub backbuffers: u32,
pub format: PresentFormat,
}
impl Default for Options {
fn default() -> Self {
Self {
frames: None,
width: 1280,
height: 720,
borderless: false,
backbuffers: 2,
format: PresentFormat::Bgra8,
}
}
}
impl Options {
pub fn from_env() -> Self {
let mut options = Options::default();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--frames" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.frames = Some(value);
}
}
"--width" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.width = value;
}
}
"--height" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.height = value;
}
}
"--backbuffers" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.backbuffers = value;
}
}
"--borderless" => options.borderless = true,
"--windowed" => options.borderless = false,
"--format" => {
if let Some(format) = args.next().and_then(|v| PresentFormat::parse(&v)) {
options.format = format;
}
}
_ => {}
}
}
options.backbuffers = options.backbuffers.max(2);
options
}
}
#[cfg(test)]
mod tests {
use super::PresentFormat;
#[test]
fn parses_explicit_format_modes() {
assert_eq!(PresentFormat::parse("bgra8"), Some(PresentFormat::Bgra8));
assert_eq!(PresentFormat::parse("rgba8"), Some(PresentFormat::Rgba8));
assert_eq!(
PresentFormat::parse("r10g10b10a2"),
Some(PresentFormat::R10G10B10A2)
);
assert_eq!(
PresentFormat::parse("fp16"),
Some(PresentFormat::Rgba16Float)
);
assert_eq!(PresentFormat::parse("unknown"), None);
}
}
}
#[cfg(windows)]
mod windows_impl {
use super::cli::{Options, PresentFormat};
use super::pattern;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0};
use windows::Win32::Graphics::Direct3D12::{
D3D12_COMMAND_LIST_TYPE_DIRECT, D3D12_COMMAND_QUEUE_DESC, D3D12_COMMAND_QUEUE_FLAG_NONE,
D3D12_CPU_DESCRIPTOR_HANDLE, D3D12_DESCRIPTOR_HEAP_DESC, D3D12_DESCRIPTOR_HEAP_FLAG_NONE,
D3D12_DESCRIPTOR_HEAP_TYPE_RTV, D3D12_FENCE_FLAG_NONE, D3D12_RESOURCE_BARRIER,
D3D12_RESOURCE_BARRIER_0, D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
D3D12_RESOURCE_BARRIER_FLAG_NONE, D3D12_RESOURCE_BARRIER_TYPE_TRANSITION,
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_TRANSITION_BARRIER, D3D12CreateDevice, ID3D12CommandAllocator,
ID3D12CommandQueue, ID3D12DescriptorHeap, ID3D12Device, ID3D12Fence,
ID3D12GraphicsCommandList, ID3D12PipelineState, ID3D12Resource,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM,
DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT,
DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::{
CreateDXGIFactory2, DXGI_ADAPTER_FLAG, DXGI_ADAPTER_FLAG_SOFTWARE,
DXGI_CREATE_FACTORY_FLAGS, DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1,
DXGI_SWAP_EFFECT_FLIP_DISCARD, DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIAdapter1,
IDXGIFactory4, IDXGISwapChain3,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::{CreateEventW, INFINITE, WaitForSingleObject};
use windows::Win32::UI::WindowsAndMessaging::{
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW,
ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
WS_POPUP, WS_VISIBLE,
};
use windows_core::{Interface, Result, w};
impl PresentFormat {
fn dxgi(self) -> DXGI_FORMAT {
match self {
PresentFormat::Bgra8 => DXGI_FORMAT_B8G8R8A8_UNORM,
PresentFormat::Rgba8 => DXGI_FORMAT_R8G8B8A8_UNORM,
PresentFormat::R10G10B10A2 => DXGI_FORMAT_R10G10B10A2_UNORM,
PresentFormat::Rgba16Float => DXGI_FORMAT_R16G16B16A16_FLOAT,
}
}
}
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
match msg {
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
unsafe {
let instance = GetModuleHandleW(None)?;
let class_name = w!("FluxerD3D12Fixture");
let wc = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wnd_proc),
hInstance: instance.into(),
lpszClassName: class_name,
..Default::default()
};
RegisterClassExW(&wc);
let style = if borderless {
WS_POPUP | WS_VISIBLE
} else {
WS_OVERLAPPEDWINDOW | WS_VISIBLE
};
let hwnd = CreateWindowExW(
Default::default(),
class_name,
w!("Fluxer D3D12 Present Fixture"),
style,
CW_USEDEFAULT,
CW_USEDEFAULT,
width as i32,
height as i32,
None,
None,
Some(instance.into()),
None,
)?;
let _ = ShowWindow(hwnd, SW_SHOW);
Ok(hwnd)
}
}
unsafe fn create_device(factory: &IDXGIFactory4) -> Result<ID3D12Device> {
unsafe {
let mut index = 0u32;
while let Ok(adapter) = factory.EnumAdapters1(index) {
let adapter: IDXGIAdapter1 = adapter;
let desc = adapter.GetDesc1()?;
let is_software =
(DXGI_ADAPTER_FLAG(desc.Flags as i32).0 & DXGI_ADAPTER_FLAG_SOFTWARE.0) != 0;
if !is_software {
let mut device: Option<ID3D12Device> = None;
if D3D12CreateDevice(&adapter, D3D_FEATURE_LEVEL_11_0, &mut device).is_ok() {
if let Some(device) = device {
return Ok(device);
}
}
}
index += 1;
}
let mut device: Option<ID3D12Device> = None;
let _ = D3D_DRIVER_TYPE_HARDWARE;
D3D12CreateDevice(None, D3D_FEATURE_LEVEL_11_0, &mut device)?;
device.ok_or_else(windows_core::Error::from_thread)
}
}
struct Renderer {
device: ID3D12Device,
queue: ID3D12CommandQueue,
swap_chain: IDXGISwapChain3,
rtv_heap: ID3D12DescriptorHeap,
rtv_descriptor_size: usize,
render_targets: Vec<ID3D12Resource>,
allocators: Vec<ID3D12CommandAllocator>,
list: ID3D12GraphicsCommandList,
fence: ID3D12Fence,
fence_event: HANDLE,
fence_value: u64,
frame_fence_values: Vec<u64>,
width: u32,
height: u32,
format: PresentFormat,
}
impl Renderer {
unsafe fn new(
hwnd: HWND,
width: u32,
height: u32,
backbuffers: u32,
format: PresentFormat,
) -> Result<Self> {
unsafe {
let factory: IDXGIFactory4 = CreateDXGIFactory2(DXGI_CREATE_FACTORY_FLAGS(0))?;
let device = create_device(&factory)?;
let queue_desc = D3D12_COMMAND_QUEUE_DESC {
Type: D3D12_COMMAND_LIST_TYPE_DIRECT,
Priority: 0,
Flags: D3D12_COMMAND_QUEUE_FLAG_NONE,
NodeMask: 0,
};
let queue: ID3D12CommandQueue = device.CreateCommandQueue(&queue_desc)?;
let desc = DXGI_SWAP_CHAIN_DESC1 {
Width: width,
Height: height,
Format: format.dxgi(),
Stereo: false.into(),
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
BufferCount: backbuffers,
Scaling: DXGI_SCALING_STRETCH,
SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD,
AlphaMode: DXGI_ALPHA_MODE_IGNORE,
Flags: 0,
};
let swap_chain1 =
factory.CreateSwapChainForHwnd(&queue, hwnd, &desc, None, None)?;
let swap_chain: IDXGISwapChain3 = swap_chain1.cast()?;
let rtv_heap_desc = D3D12_DESCRIPTOR_HEAP_DESC {
Type: D3D12_DESCRIPTOR_HEAP_TYPE_RTV,
NumDescriptors: backbuffers,
Flags: D3D12_DESCRIPTOR_HEAP_FLAG_NONE,
NodeMask: 0,
};
let rtv_heap: ID3D12DescriptorHeap = device.CreateDescriptorHeap(&rtv_heap_desc)?;
let rtv_descriptor_size = device
.GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV)
as usize;
let heap_start = rtv_heap.GetCPUDescriptorHandleForHeapStart();
let mut render_targets = Vec::with_capacity(backbuffers as usize);
let mut allocators = Vec::with_capacity(backbuffers as usize);
for i in 0..backbuffers {
let back_buffer: ID3D12Resource = swap_chain.GetBuffer(i)?;
let handle = D3D12_CPU_DESCRIPTOR_HANDLE {
ptr: heap_start.ptr + i as usize * rtv_descriptor_size,
};
device.CreateRenderTargetView(&back_buffer, None, handle);
render_targets.push(back_buffer);
let allocator: ID3D12CommandAllocator =
device.CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT)?;
allocators.push(allocator);
}
let list: ID3D12GraphicsCommandList = device.CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
&allocators[0],
None::<&ID3D12PipelineState>,
)?;
list.Close()?;
let fence: ID3D12Fence = device.CreateFence(0, D3D12_FENCE_FLAG_NONE)?;
let fence_event = CreateEventW(None, false, false, None)?;
Ok(Self {
device,
queue,
swap_chain,
rtv_heap,
rtv_descriptor_size,
render_targets,
frame_fence_values: vec![0; backbuffers as usize],
allocators,
list,
fence,
fence_event,
fence_value: 0,
width,
height,
format,
})
}
}
unsafe fn transition(
resource: &ID3D12Resource,
before: windows::Win32::Graphics::Direct3D12::D3D12_RESOURCE_STATES,
after: windows::Win32::Graphics::Direct3D12::D3D12_RESOURCE_STATES,
) -> D3D12_RESOURCE_BARRIER {
D3D12_RESOURCE_BARRIER {
Type: D3D12_RESOURCE_BARRIER_TYPE_TRANSITION,
Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE,
Anonymous: D3D12_RESOURCE_BARRIER_0 {
Transition: std::mem::ManuallyDrop::new(D3D12_RESOURCE_TRANSITION_BARRIER {
pResource: std::mem::ManuallyDrop::new(Some(resource.clone())),
Subresource: D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
StateBefore: before,
StateAfter: after,
}),
},
}
}
unsafe fn render_frame(&mut self, frame_index: u64) -> Result<()> {
unsafe {
let buffer_index = self.swap_chain.GetCurrentBackBufferIndex() as usize;
self.wait_for_fence_value(self.frame_fence_values[buffer_index])?;
let allocator = &self.allocators[buffer_index];
let back_buffer = &self.render_targets[buffer_index];
allocator.Reset()?;
self.list.Reset(allocator, None::<&ID3D12PipelineState>)?;
let to_rt = Self::transition(
back_buffer,
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET,
);
self.list.ResourceBarrier(&[to_rt]);
let rtv = D3D12_CPU_DESCRIPTOR_HANDLE {
ptr: self.rtv_heap.GetCPUDescriptorHandleForHeapStart().ptr
+ buffer_index * self.rtv_descriptor_size,
};
self.list.OMSetRenderTargets(1, Some(&rtv), false, None);
self.paint_pattern(rtv, frame_index);
let to_present = Self::transition(
back_buffer,
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT,
);
self.list.ResourceBarrier(&[to_present]);
self.list.Close()?;
let command_list: windows::Win32::Graphics::Direct3D12::ID3D12CommandList =
self.list.cast()?;
self.queue.ExecuteCommandLists(&[Some(command_list)]);
self.swap_chain.Present(1, Default::default()).ok()?;
self.fence_value += 1;
let signal = self.fence_value;
self.queue.Signal(&self.fence, signal)?;
self.frame_fence_values[buffer_index] = signal;
Ok(())
}
}
unsafe fn paint_pattern(&self, rtv: D3D12_CPU_DESCRIPTOR_HANDLE, frame_index: u64) {
unsafe {
let width = self.width.max(1);
let height = self.height as i32;
let luma = (frame_index & 0xFF) as u8;
for bar in 0..pattern::BARS.len() as u32 {
let x0 = ((bar as u64 * width as u64) / pattern::BARS.len() as u64) as i32;
let x1 =
(((bar + 1) as u64 * width as u64) / pattern::BARS.len() as u64) as i32;
if x1 <= x0 {
continue;
}
let (r, g, b) = pattern::bar_colour(x0 as u32, width);
let colour = rgba(self.format, r, g, b.saturating_add(luma / 4));
let rect = RECT {
left: x0,
top: 0,
right: x1,
bottom: height,
};
self.list.ClearRenderTargetView(rtv, &colour, Some(&[rect]));
}
let (cr, cg, cb) = pattern::counter_colour(frame_index);
let block = pattern::COUNTER_BLOCK as i32;
let counter_rect = RECT {
left: 0,
top: 0,
right: block.min(self.width as i32),
bottom: block.min(height),
};
let counter_colour = rgba(self.format, cr, cg, cb);
self.list
.ClearRenderTargetView(rtv, &counter_colour, Some(&[counter_rect]));
}
}
unsafe fn wait_for_fence_value(&self, value: u64) -> Result<()> {
unsafe {
if value == 0 || self.fence.GetCompletedValue() >= value {
return Ok(());
}
self.fence.SetEventOnCompletion(value, self.fence_event)?;
WaitForSingleObject(self.fence_event, INFINITE);
Ok(())
}
}
unsafe fn flush(&mut self) -> Result<()> {
unsafe {
self.fence_value += 1;
let signal = self.fence_value;
self.queue.Signal(&self.fence, signal)?;
self.wait_for_fence_value(signal)
}
}
}
impl Drop for Renderer {
fn drop(&mut self) {
unsafe {
let _ = self.flush();
let _ = CloseHandle(self.fence_event);
let _ = &self.device;
}
}
}
fn rgba(format: PresentFormat, r: u8, g: u8, b: u8) -> [f32; 4] {
let highlight =
if matches!(format, PresentFormat::Rgba16Float) && r == 255 && g == 255 && b > 240 {
1.25
} else {
1.0
};
[
(r as f32 / 255.0) * highlight,
(g as f32 / 255.0) * highlight,
(b as f32 / 255.0) * highlight,
1.0,
]
}
pub fn run() -> Result<()> {
let options = Options::from_env();
unsafe {
let hwnd = create_window(options.width, options.height, options.borderless)?;
let mut rect = Default::default();
let _ = GetClientRect(hwnd, &mut rect);
let width = ((rect.right - rect.left).max(1)) as u32;
let height = ((rect.bottom - rect.top).max(1)) as u32;
let mut renderer =
Renderer::new(hwnd, width, height, options.backbuffers, options.format)?;
println!("HWND={}", hwnd.0 as isize);
use std::io::Write;
let _ = std::io::stdout().flush();
let mut frame_index: u64 = 0;
let mut msg = MSG::default();
loop {
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
return Ok(());
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
renderer.render_frame(frame_index)?;
frame_index += 1;
if let Some(limit) = options.frames {
if frame_index >= limit {
return Ok(());
}
}
}
}
}
}
@@ -0,0 +1,156 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "d3d9-present-fixture"
version = "0.0.0"
dependencies = [
"windows",
"windows-core",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
@@ -0,0 +1,26 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
name = "d3d9-present-fixture"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
# matching the sibling crates (hook/, vulkan-layer/) in this package.
[workspace]
resolver = "2"
[[bin]]
name = "d3d9-present-fixture"
path = "src/main.rs"
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D9",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_UI_WindowsAndMessaging",
]}
windows-core = "0.62.2"
@@ -0,0 +1,417 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(not(windows))]
fn main() {
eprintln!("d3d9-present-fixture is only functional on Windows");
}
#[cfg(windows)]
fn main() -> windows_core::Result<()> {
windows_impl::run()
}
#[cfg_attr(not(windows), allow(dead_code))]
mod pattern {
pub const BARS: [(u8, u8, u8); 8] = [
(255, 255, 255),
(255, 255, 0),
(0, 255, 255),
(0, 255, 0),
(255, 0, 255),
(255, 0, 0),
(0, 0, 255),
(0, 0, 0),
];
pub const COUNTER_BLOCK: u32 = 16;
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
let width = width.max(1);
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
BARS[index.min(BARS.len() - 1)]
}
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
let low = (frame_index & 0x00FF_FFFF) as u32;
(
(low & 0xFF) as u8,
((low >> 8) & 0xFF) as u8,
((low >> 16) & 0xFF) as u8,
)
}
}
#[cfg(windows)]
mod cli {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PresentMode {
Device,
SwapChain,
}
pub struct Options {
pub frames: Option<u64>,
pub width: u32,
pub height: u32,
pub borderless: bool,
pub multisample: bool,
pub present_mode: PresentMode,
}
impl Default for Options {
fn default() -> Self {
Self {
frames: None,
width: 1280,
height: 720,
borderless: false,
multisample: false,
present_mode: PresentMode::Device,
}
}
}
impl Options {
pub fn from_env() -> Self {
let mut options = Options::default();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--frames" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.frames = Some(value);
}
}
"--width" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.width = value;
}
}
"--height" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.height = value;
}
}
"--borderless" => options.borderless = true,
"--windowed" => options.borderless = false,
"--multisample" => options.multisample = true,
"--no-multisample" => options.multisample = false,
"--swapchain-present" => options.present_mode = PresentMode::SwapChain,
"--device-present" => options.present_mode = PresentMode::Device,
_ => {}
}
}
options
}
}
}
#[cfg(windows)]
mod windows_impl {
use super::{
cli::{Options, PresentMode},
pattern,
};
use std::ptr::null;
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Graphics::Direct3D9::{
D3D_SDK_VERSION, D3DADAPTER_DEFAULT, D3DCLEAR_TARGET, D3DCREATE_HARDWARE_VERTEXPROCESSING,
D3DCREATE_SOFTWARE_VERTEXPROCESSING, D3DDEVTYPE_HAL, D3DFMT_X8R8G8B8,
D3DMULTISAMPLE_2_SAMPLES, D3DMULTISAMPLE_NONE, D3DPRESENT_PARAMETERS, D3DRECT,
D3DSWAPEFFECT_DISCARD, Direct3DCreate9, IDirect3D9, IDirect3DDevice9,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::{
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW,
ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
WS_POPUP, WS_VISIBLE,
};
use windows_core::{BOOL, Result, w};
const D3DERR_DEVICELOST: i32 = 0x8876_0868u32 as i32;
fn d3dcolor_xrgb(r: u8, g: u8, b: u8) -> u32 {
0xFF00_0000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
}
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
match msg {
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
unsafe {
let instance = GetModuleHandleW(None)?;
let class_name = w!("FluxerD3D9Fixture");
let wc = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wnd_proc),
hInstance: instance.into(),
lpszClassName: class_name,
..Default::default()
};
RegisterClassExW(&wc);
let style = if borderless {
WS_POPUP | WS_VISIBLE
} else {
WS_OVERLAPPEDWINDOW | WS_VISIBLE
};
let hwnd = CreateWindowExW(
Default::default(),
class_name,
w!("Fluxer D3D9 Present Fixture"),
style,
CW_USEDEFAULT,
CW_USEDEFAULT,
width as i32,
height as i32,
None,
None,
Some(instance.into()),
None,
)?;
let _ = ShowWindow(hwnd, SW_SHOW);
Ok(hwnd)
}
}
fn present_params(
hwnd: HWND,
width: u32,
height: u32,
multisample: bool,
) -> D3DPRESENT_PARAMETERS {
D3DPRESENT_PARAMETERS {
BackBufferWidth: width,
BackBufferHeight: height,
BackBufferFormat: D3DFMT_X8R8G8B8,
BackBufferCount: 1,
MultiSampleType: if multisample {
D3DMULTISAMPLE_2_SAMPLES
} else {
D3DMULTISAMPLE_NONE
},
MultiSampleQuality: 0,
SwapEffect: D3DSWAPEFFECT_DISCARD,
hDeviceWindow: hwnd,
Windowed: BOOL(1),
EnableAutoDepthStencil: BOOL(0),
..Default::default()
}
}
struct Renderer {
_d3d9: IDirect3D9,
device: IDirect3DDevice9,
hwnd: HWND,
width: u32,
height: u32,
device_lost: bool,
multisample: bool,
present_mode: PresentMode,
}
impl Renderer {
unsafe fn new(
hwnd: HWND,
width: u32,
height: u32,
multisample: bool,
present_mode: PresentMode,
) -> Result<Self> {
unsafe {
let d3d9 = Direct3DCreate9(D3D_SDK_VERSION)
.ok_or_else(|| windows_core::Error::from_thread())?;
let mut params = present_params(hwnd, width, height, multisample);
let mut device: Option<IDirect3DDevice9> = None;
let mut created = d3d9
.CreateDevice(
D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hwnd,
D3DCREATE_HARDWARE_VERTEXPROCESSING as u32,
&mut params,
&mut device,
)
.is_ok();
if !created {
created = d3d9
.CreateDevice(
D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hwnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING as u32,
&mut params,
&mut device,
)
.is_ok();
}
let device = if created {
device.ok_or_else(|| windows_core::Error::from_thread())?
} else {
return Err(windows_core::Error::from_thread());
};
Ok(Self {
_d3d9: d3d9,
device,
hwnd,
width,
height,
device_lost: false,
multisample,
present_mode,
})
}
}
unsafe fn try_reset(&mut self) {
unsafe {
let mut params =
present_params(self.hwnd, self.width, self.height, self.multisample);
if self.device.Reset(&mut params).is_ok() {
self.device_lost = false;
}
}
}
unsafe fn render_frame(&mut self, frame_index: u64) -> Result<()> {
unsafe {
if self.device_lost {
self.try_reset();
if self.device_lost {
return self.present();
}
}
let width = self.width.max(1);
let height = self.height as i32;
let bars = pattern::BARS.len() as u32;
for bar in 0..bars {
let x1 = ((bar as u64 * width as u64) / bars as u64) as i32;
let x2 = (((bar + 1) as u64 * width as u64) / bars as u64) as i32;
if x2 <= x1 {
continue;
}
let mid = ((x1 + x2) / 2).max(0) as u32;
let (r, g, b) = pattern::bar_colour(mid, width);
let rect = D3DRECT {
x1,
y1: 0,
x2,
y2: height,
};
self.device.Clear(
1,
&rect,
D3DCLEAR_TARGET as u32,
d3dcolor_xrgb(r, g, b),
1.0,
0,
)?;
}
let block = pattern::COUNTER_BLOCK as i32;
let block_x2 = block.min(width as i32);
let block_y2 = block.min(height);
if block_x2 > 0 && block_y2 > 0 {
let (r, g, b) = pattern::counter_colour(frame_index);
let rect = D3DRECT {
x1: 0,
y1: 0,
x2: block_x2,
y2: block_y2,
};
self.device.Clear(
1,
&rect,
D3DCLEAR_TARGET as u32,
d3dcolor_xrgb(r, g, b),
1.0,
0,
)?;
}
self.present()
}
}
unsafe fn present(&mut self) -> Result<()> {
unsafe {
let result = match self.present_mode {
PresentMode::Device => {
self.device.Present(null(), null(), HWND::default(), null())
}
PresentMode::SwapChain => self.device.GetSwapChain(0).and_then(|swap_chain| {
swap_chain.Present(null(), null(), HWND::default(), null(), 0)
}),
};
match result {
Ok(()) => Ok(()),
Err(err) if err.code().0 == D3DERR_DEVICELOST => {
self.device_lost = true;
Ok(())
}
Err(err) => Err(err),
}
}
}
}
pub fn run() -> Result<()> {
let options = Options::from_env();
unsafe {
let hwnd = create_window(options.width, options.height, options.borderless)?;
let mut rect = Default::default();
let _ = GetClientRect(hwnd, &mut rect);
let width = ((rect.right - rect.left).max(1)) as u32;
let height = ((rect.bottom - rect.top).max(1)) as u32;
let mut renderer = Renderer::new(
hwnd,
width,
height,
options.multisample,
options.present_mode,
)?;
println!("HWND={}", hwnd.0 as isize);
use std::io::Write;
let _ = std::io::stdout().flush();
let mut frame_index: u64 = 0;
let mut msg = MSG::default();
loop {
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
return Ok(());
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
renderer.render_frame(frame_index)?;
frame_index += 1;
if let Some(limit) = options.frames {
if frame_index >= limit {
return Ok(());
}
}
}
}
}
}
@@ -0,0 +1,156 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "i686-present-fixture"
version = "0.0.0"
dependencies = [
"windows",
"windows-core",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
name = "i686-present-fixture"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
# matching the sibling crates (hook/, vulkan-layer/) in this package.
[workspace]
resolver = "2"
[[bin]]
name = "i686-present-fixture"
path = "src/main.rs"
# This fixture is a deterministic Direct3D 11 present fixture whose ONLY purpose
# is to be built for the 32-bit (i686 / WOW64) target so the harness can drive
# the cross-bitness injection paths (an x64 host injecting the 32-bit hook into a
# 32-bit game). The source is target-arch-agnostic -- what makes it "i686" is
# building it for `i686-pc-windows-msvc`:
#
# cargo build --release --manifest-path test-apps/i686-present-fixture/Cargo.toml \
# --target i686-pc-windows-msvc
#
# It also compile-checks for x86_64-pc-windows-msvc so the standard cargo-check
# matrix stays uniform across fixtures.
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_UI_WindowsAndMessaging",
]}
windows-core = "0.62.2"
@@ -0,0 +1,358 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(not(windows))]
fn main() {
eprintln!("i686-present-fixture is only functional on Windows");
}
#[cfg(windows)]
fn main() -> windows_core::Result<()> {
windows_impl::run()
}
#[cfg_attr(not(windows), allow(dead_code))]
mod pattern {
pub const BARS: [(u8, u8, u8); 8] = [
(255, 255, 255),
(255, 255, 0),
(0, 255, 255),
(0, 255, 0),
(255, 0, 255),
(255, 0, 0),
(0, 0, 255),
(0, 0, 0),
];
pub const COUNTER_BLOCK: u32 = 16;
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
let width = width.max(1);
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
BARS[index.min(BARS.len() - 1)]
}
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
let low = (frame_index & 0x00FF_FFFF) as u32;
(
(low & 0xFF) as u8,
((low >> 8) & 0xFF) as u8,
((low >> 16) & 0xFF) as u8,
)
}
}
#[cfg(windows)]
mod cli {
pub struct Options {
pub frames: Option<u64>,
pub width: u32,
pub height: u32,
pub borderless: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
frames: None,
width: 1280,
height: 720,
borderless: false,
}
}
}
impl Options {
pub fn from_env() -> Self {
let mut options = Options::default();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--frames" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.frames = Some(value);
}
}
"--width" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.width = value;
}
}
"--height" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.height = value;
}
}
"--borderless" => options.borderless = true,
"--windowed" => options.borderless = false,
_ => {}
}
}
options
}
}
}
#[cfg(windows)]
mod windows_impl {
use super::{cli::Options, pattern};
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
use windows::Win32::Graphics::Direct3D11::{
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION,
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11RenderTargetView,
ID3D11Resource, ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::{
DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1, DXGI_SWAP_EFFECT_FLIP_DISCARD,
DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIDevice, IDXGIFactory2, IDXGISwapChain1,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::{
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW,
ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
WS_POPUP, WS_VISIBLE,
};
use windows_core::{Interface, Result, w};
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
match msg {
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
unsafe {
let instance = GetModuleHandleW(None)?;
let class_name = w!("FluxerI686Fixture");
let wc = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wnd_proc),
hInstance: instance.into(),
lpszClassName: class_name,
..Default::default()
};
RegisterClassExW(&wc);
let style = if borderless {
WS_POPUP | WS_VISIBLE
} else {
WS_OVERLAPPEDWINDOW | WS_VISIBLE
};
let hwnd = CreateWindowExW(
Default::default(),
class_name,
w!("Fluxer i686 (WOW64) Present Fixture"),
style,
CW_USEDEFAULT,
CW_USEDEFAULT,
width as i32,
height as i32,
None,
None,
Some(instance.into()),
None,
)?;
let _ = ShowWindow(hwnd, SW_SHOW);
Ok(hwnd)
}
}
struct Renderer {
device: ID3D11Device,
context: ID3D11DeviceContext,
swap_chain: IDXGISwapChain1,
rtv: ID3D11RenderTargetView,
width: u32,
height: u32,
staging: ID3D11Texture2D,
}
impl Renderer {
unsafe fn new(hwnd: HWND, width: u32, height: u32) -> Result<Self> {
unsafe {
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)?;
let device = device.expect("D3D11CreateDevice yielded no device");
let context = context.expect("D3D11CreateDevice yielded no context");
let dxgi_device: IDXGIDevice = device.cast()?;
let adapter = dxgi_device.GetAdapter()?;
let factory: IDXGIFactory2 = adapter.GetParent()?;
let desc = DXGI_SWAP_CHAIN_DESC1 {
Width: width,
Height: height,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
Stereo: false.into(),
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
BufferCount: 2,
Scaling: DXGI_SCALING_STRETCH,
SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD,
AlphaMode: DXGI_ALPHA_MODE_IGNORE,
Flags: 0,
};
let swap_chain =
factory.CreateSwapChainForHwnd(&device, hwnd, &desc, None, None)?;
let rtv = Self::make_rtv(&device, &swap_chain)?;
let staging = Self::make_staging(&device, width, height)?;
Ok(Self {
device,
context,
swap_chain,
rtv,
width,
height,
staging,
})
}
}
unsafe fn make_rtv(
device: &ID3D11Device,
swap_chain: &IDXGISwapChain1,
) -> Result<ID3D11RenderTargetView> {
unsafe {
let back_buffer: ID3D11Texture2D = swap_chain.GetBuffer(0)?;
let resource: ID3D11Resource = back_buffer.cast()?;
let mut rtv: Option<ID3D11RenderTargetView> = None;
device.CreateRenderTargetView(&resource, None, Some(&mut rtv))?;
Ok(rtv.expect("CreateRenderTargetView yielded no view"))
}
}
unsafe fn make_staging(
device: &ID3D11Device,
width: u32,
height: u32,
) -> Result<ID3D11Texture2D> {
use windows::Win32::Graphics::Direct3D11::{
D3D11_CPU_ACCESS_WRITE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING,
};
unsafe {
let desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: height,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_STAGING,
BindFlags: 0,
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
MiscFlags: 0,
};
let mut staging: Option<ID3D11Texture2D> = None;
device.CreateTexture2D(&desc, None, Some(&mut staging))?;
Ok(staging.expect("CreateTexture2D yielded no staging texture"))
}
}
unsafe fn render_frame(&self, frame_index: u64) -> Result<()> {
use windows::Win32::Graphics::Direct3D11::D3D11_MAP_WRITE;
unsafe {
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
self.context
.Map(&self.staging, 0, D3D11_MAP_WRITE, 0, Some(&mut mapped))?;
let row_pitch = mapped.RowPitch as usize;
let base = mapped.pData as *mut u8;
let counter = pattern::counter_colour(frame_index);
let luma = (frame_index & 0xFF) as u8;
for y in 0..self.height {
let row = base.add(y as usize * row_pitch);
for x in 0..self.width {
let (r, g, b) = if x < pattern::COUNTER_BLOCK && y < pattern::COUNTER_BLOCK
{
counter
} else {
pattern::bar_colour(x, self.width)
};
let px = row.add(x as usize * 4);
*px = b.saturating_add(luma / 4);
*px.add(1) = g;
*px.add(2) = r;
*px.add(3) = 255;
}
}
self.context.Unmap(&self.staging, 0);
let back_buffer: ID3D11Texture2D = self.swap_chain.GetBuffer(0)?;
let dst: ID3D11Resource = back_buffer.cast()?;
let src: ID3D11Resource = self.staging.cast()?;
self.context.CopyResource(&dst, &src);
let _ = &self.rtv;
self.swap_chain.Present(1, Default::default()).ok()?;
let _ = &self.device;
Ok(())
}
}
}
pub fn run() -> Result<()> {
let options = Options::from_env();
unsafe {
let hwnd = create_window(options.width, options.height, options.borderless)?;
let mut rect = Default::default();
let _ = GetClientRect(hwnd, &mut rect);
let width = ((rect.right - rect.left).max(1)) as u32;
let height = ((rect.bottom - rect.top).max(1)) as u32;
let renderer = Renderer::new(hwnd, width, height)?;
println!("HWND={}", hwnd.0 as isize);
use std::io::Write;
let _ = std::io::stdout().flush();
let mut frame_index: u64 = 0;
let mut msg = MSG::default();
loop {
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
return Ok(());
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
renderer.render_frame(frame_index)?;
frame_index += 1;
if let Some(limit) = options.frames {
if frame_index >= limit {
return Ok(());
}
}
}
}
}
}
@@ -0,0 +1,156 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "opengl-swapbuffers-fixture"
version = "0.0.0"
dependencies = [
"windows",
"windows-core",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
@@ -0,0 +1,26 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
name = "opengl-swapbuffers-fixture"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
# matching the sibling crates (hook/, vulkan-layer/) in this package.
[workspace]
resolver = "2"
[[bin]]
name = "opengl-swapbuffers-fixture"
path = "src/main.rs"
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_Graphics_OpenGL",
"Win32_System_LibraryLoader",
"Win32_UI_WindowsAndMessaging",
]}
windows-core = "0.62.2"
@@ -0,0 +1,484 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(not(windows))]
fn main() {
eprintln!("opengl-swapbuffers-fixture is only functional on Windows");
}
#[cfg(windows)]
fn main() -> windows_core::Result<()> {
windows_impl::run()
}
#[allow(dead_code)]
mod pattern {
pub const BARS: [(u8, u8, u8); 8] = [
(255, 255, 255),
(255, 255, 0),
(0, 255, 255),
(0, 255, 0),
(255, 0, 255),
(255, 0, 0),
(0, 0, 255),
(0, 0, 0),
];
pub const COUNTER_BLOCK: u32 = 16;
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
let width = width.max(1);
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
BARS[index.min(BARS.len() - 1)]
}
pub fn bar_x_range(index: usize, width: u32) -> (u32, u32) {
let width = width.max(1) as u64;
let len = BARS.len() as u64;
let start = (index as u64 * width) / len;
let end = ((index as u64 + 1) * width) / len;
(start as u32, end.min(width) as u32)
}
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
let low = (frame_index & 0x00FF_FFFF) as u32;
(
(low & 0xFF) as u8,
((low >> 8) & 0xFF) as u8,
((low >> 16) & 0xFF) as u8,
)
}
pub fn clear_colour(frame_index: u64) -> [f32; 3] {
let (r, g, b) = BARS[(frame_index % BARS.len() as u64) as usize];
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]
}
}
#[cfg(windows)]
mod cli {
pub struct Options {
pub frames: Option<u64>,
pub width: u32,
pub height: u32,
pub borderless: bool,
pub layer: bool,
pub stress_pack_state: bool,
pub resize_at: Option<u64>,
pub frame_delay_ms: u64,
}
impl Default for Options {
fn default() -> Self {
Self {
frames: None,
width: 1280,
height: 720,
borderless: false,
layer: false,
stress_pack_state: false,
resize_at: None,
frame_delay_ms: 0,
}
}
}
impl Options {
pub fn from_env() -> Self {
let mut options = Options::default();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--frames" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.frames = Some(value);
}
}
"--width" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.width = value;
}
}
"--height" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.height = value;
}
}
"--borderless" => options.borderless = true,
"--windowed" => options.borderless = false,
"--layer" => options.layer = true,
"--stress-pack-state" => options.stress_pack_state = true,
"--resize-at" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.resize_at = Some(value);
}
}
"--frame-delay-ms" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.frame_delay_ms = value;
}
}
_ => {}
}
}
options
}
}
}
#[cfg(windows)]
mod windows_impl {
use super::cli::Options;
use super::pattern;
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Graphics::Gdi::{GetDC, HDC, ReleaseDC, WGL_SWAP_MAIN_PLANE};
use windows::Win32::Graphics::OpenGL::{
ChoosePixelFormat, GL_COLOR_BUFFER_BIT, GL_FRONT, GL_PACK_ALIGNMENT, GL_PACK_LSB_FIRST,
GL_PACK_ROW_LENGTH, GL_PACK_SKIP_PIXELS, GL_PACK_SKIP_ROWS, GL_PACK_SWAP_BYTES,
GL_READ_BUFFER, GL_SCISSOR_TEST, PFD_DOUBLEBUFFER, PFD_DRAW_TO_WINDOW, PFD_MAIN_PLANE,
PFD_SUPPORT_OPENGL, PFD_TYPE_RGBA, PIXELFORMATDESCRIPTOR, SetPixelFormat, SwapBuffers,
glClear, glClearColor, glDisable, glEnable, glGetIntegerv, glPixelStorei, glReadBuffer,
glScissor, glViewport, wglCreateContext, wglDeleteContext, wglGetProcAddress,
wglMakeCurrent, wglSwapLayerBuffers,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::{
CS_HREDRAW, CS_OWNDC, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW,
DispatchMessageW, GetClientRect, MSG, MoveWindow, PM_REMOVE, PeekMessageW, PostQuitMessage,
RegisterClassExW, SW_SHOW, ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW,
WS_OVERLAPPEDWINDOW, WS_POPUP, WS_VISIBLE,
};
use windows_core::{PCSTR, Result, w};
const GL_PIXEL_PACK_BUFFER: u32 = 0x88EB;
const GL_PIXEL_PACK_BUFFER_BINDING: u32 = 0x88ED;
const GL_STREAM_READ: u32 = 0x88E1;
type GlGenBuffersFn = unsafe extern "system" fn(i32, *mut u32);
type GlBindBufferFn = unsafe extern "system" fn(u32, u32);
type GlBufferDataFn = unsafe extern "system" fn(u32, isize, *const core::ffi::c_void, u32);
type GlDeleteBuffersFn = unsafe extern "system" fn(i32, *const u32);
#[derive(Clone, Copy)]
struct BufferProcs {
gen_buffers: GlGenBuffersFn,
bind_buffer: GlBindBufferFn,
buffer_data: GlBufferDataFn,
delete_buffers: GlDeleteBuffersFn,
}
struct PackStateStress {
procs: Option<BufferProcs>,
pbo: u32,
}
impl PackStateStress {
unsafe fn new() -> Self {
let Some(procs) = (unsafe { BufferProcs::load() }) else {
return Self {
procs: None,
pbo: 0,
};
};
let mut pbo = 0u32;
unsafe {
(procs.gen_buffers)(1, &mut pbo);
}
if pbo != 0 {
let storage = [0u8; 16];
unsafe {
(procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, pbo);
(procs.buffer_data)(
GL_PIXEL_PACK_BUFFER,
storage.len() as isize,
storage.as_ptr().cast(),
GL_STREAM_READ,
);
(procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, 0);
}
}
Self {
procs: Some(procs),
pbo,
}
}
fn pbo(&self) -> Option<u32> {
(self.pbo != 0).then_some(self.pbo)
}
}
impl Drop for PackStateStress {
fn drop(&mut self) {
unsafe {
if let Some(procs) = self.procs {
if self.pbo != 0 {
(procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, 0);
(procs.delete_buffers)(1, &self.pbo);
}
}
}
}
}
impl BufferProcs {
unsafe fn load() -> Option<Self> {
Some(Self {
gen_buffers: unsafe { load_gl_proc(b"glGenBuffers\0")? },
bind_buffer: unsafe { load_gl_proc(b"glBindBuffer\0")? },
buffer_data: unsafe { load_gl_proc(b"glBufferData\0")? },
delete_buffers: unsafe { load_gl_proc(b"glDeleteBuffers\0")? },
})
}
}
unsafe fn load_gl_proc<T>(name: &'static [u8]) -> Option<T> {
let proc = unsafe { wglGetProcAddress(PCSTR(name.as_ptr()))? };
let address = proc as usize;
if address <= 3 || address == usize::MAX {
return None;
}
Some(unsafe { std::mem::transmute_copy::<_, T>(&proc) })
}
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
match msg {
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
unsafe {
let instance = GetModuleHandleW(None)?;
let class_name = w!("FluxerOpenGLFixture");
let wc = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
lpfnWndProc: Some(wnd_proc),
hInstance: instance.into(),
lpszClassName: class_name,
..Default::default()
};
RegisterClassExW(&wc);
let style = if borderless {
WS_POPUP | WS_VISIBLE
} else {
WS_OVERLAPPEDWINDOW | WS_VISIBLE
};
let hwnd = CreateWindowExW(
Default::default(),
class_name,
w!("Fluxer OpenGL SwapBuffers Fixture"),
style,
CW_USEDEFAULT,
CW_USEDEFAULT,
width as i32,
height as i32,
None,
None,
Some(instance.into()),
None,
)?;
let _ = ShowWindow(hwnd, SW_SHOW);
Ok(hwnd)
}
}
unsafe fn set_pixel_format(hdc: HDC) -> Result<()> {
unsafe {
let mut pfd = PIXELFORMATDESCRIPTOR {
nSize: size_of::<PIXELFORMATDESCRIPTOR>() as u16,
nVersion: 1,
dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
iPixelType: PFD_TYPE_RGBA,
cColorBits: 32,
cDepthBits: 24,
cStencilBits: 8,
iLayerType: PFD_MAIN_PLANE.0 as u8,
..Default::default()
};
let format = ChoosePixelFormat(hdc, &pfd);
if format == 0 {
return Err(windows_core::Error::from_thread());
}
SetPixelFormat(hdc, format, &mut pfd)?;
Ok(())
}
}
unsafe fn clear_rect(colour: (u8, u8, u8)) {
unsafe {
let (r, g, b) = colour;
glClearColor(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
}
}
unsafe fn render_pattern(frame_index: u64, width: u32, height: u32) {
unsafe {
glViewport(0, 0, width as i32, height as i32);
glEnable(GL_SCISSOR_TEST);
for index in 0..pattern::BARS.len() {
let (start, end) = pattern::bar_x_range(index, width);
if end <= start {
continue;
}
glScissor(start as i32, 0, (end - start) as i32, height as i32);
clear_rect(pattern::BARS[index]);
}
let block = pattern::COUNTER_BLOCK.min(width).min(height);
if block > 0 {
let y = height - block;
glScissor(0, y as i32, block as i32, block as i32);
clear_rect(pattern::counter_colour(frame_index));
}
glDisable(GL_SCISSOR_TEST);
}
}
unsafe fn client_size(hwnd: HWND) -> (u32, u32) {
unsafe {
let mut rect = Default::default();
let _ = GetClientRect(hwnd, &mut rect);
(
((rect.right - rect.left).max(1)) as u32,
((rect.bottom - rect.top).max(1)) as u32,
)
}
}
unsafe fn stress_pack_state(stress: &PackStateStress) {
unsafe {
glReadBuffer(GL_FRONT);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
glPixelStorei(GL_PACK_SKIP_ROWS, 0);
glPixelStorei(GL_PACK_SWAP_BYTES, 1);
glPixelStorei(GL_PACK_LSB_FIRST, 1);
if let (Some(procs), Some(pbo)) = (stress.procs, stress.pbo()) {
(procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, pbo);
}
}
}
unsafe fn assert_stress_pack_state(stress: &PackStateStress) {
unsafe {
let mut read_buffer = 0i32;
let mut pack_alignment = 0i32;
let mut pack_row_length = 0i32;
let mut pack_skip_pixels = 0i32;
let mut pack_skip_rows = 0i32;
let mut pack_swap_bytes = 0i32;
let mut pack_lsb_first = 0i32;
let mut pack_buffer = 0i32;
glGetIntegerv(GL_READ_BUFFER, &mut read_buffer);
glGetIntegerv(GL_PACK_ALIGNMENT, &mut pack_alignment);
glGetIntegerv(GL_PACK_ROW_LENGTH, &mut pack_row_length);
glGetIntegerv(GL_PACK_SKIP_PIXELS, &mut pack_skip_pixels);
glGetIntegerv(GL_PACK_SKIP_ROWS, &mut pack_skip_rows);
glGetIntegerv(GL_PACK_SWAP_BYTES, &mut pack_swap_bytes);
glGetIntegerv(GL_PACK_LSB_FIRST, &mut pack_lsb_first);
glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &mut pack_buffer);
assert_eq!(
read_buffer as u32, GL_FRONT,
"GL_READ_BUFFER was not restored"
);
assert_eq!(pack_alignment, 1, "GL_PACK_ALIGNMENT was not restored");
assert_eq!(pack_row_length, 0, "GL_PACK_ROW_LENGTH was not restored");
assert_eq!(pack_skip_pixels, 0, "GL_PACK_SKIP_PIXELS was not restored");
assert_eq!(pack_skip_rows, 0, "GL_PACK_SKIP_ROWS was not restored");
assert_eq!(pack_swap_bytes, 1, "GL_PACK_SWAP_BYTES was not restored");
assert_eq!(pack_lsb_first, 1, "GL_PACK_LSB_FIRST was not restored");
if let Some(pbo) = stress.pbo() {
assert_eq!(
pack_buffer as u32, pbo,
"GL_PIXEL_PACK_BUFFER_BINDING was not restored"
);
}
}
}
pub fn run() -> Result<()> {
let options = Options::from_env();
unsafe {
let hwnd = create_window(options.width, options.height, options.borderless)?;
println!("HWND={}", hwnd.0 as isize);
use std::io::Write;
let _ = std::io::stdout().flush();
let hdc = GetDC(Some(hwnd));
set_pixel_format(hdc)?;
let context = wglCreateContext(hdc)?;
wglMakeCurrent(hdc, context)?;
let pack_state_stress = PackStateStress::new();
let mut frame_index: u64 = 0;
let mut msg = MSG::default();
let mut resized = false;
'render: loop {
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
break 'render;
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
if let Some(resize_at) = options.resize_at {
if !resized && frame_index >= resize_at {
let width = options.width.saturating_add(160).max(1) as i32;
let height = options.height.saturating_add(96).max(1) as i32;
let _ = MoveWindow(hwnd, 0, 0, width, height, true);
resized = true;
}
}
let (width, height) = client_size(hwnd);
render_pattern(frame_index, width, height);
if options.stress_pack_state {
stress_pack_state(&pack_state_stress);
}
if options.layer {
wglSwapLayerBuffers(hdc, WGL_SWAP_MAIN_PLANE)?;
} else {
SwapBuffers(hdc)?;
}
if options.stress_pack_state {
assert_stress_pack_state(&pack_state_stress);
}
frame_index += 1;
if options.frame_delay_ms > 0 {
std::thread::sleep(std::time::Duration::from_millis(options.frame_delay_ms));
}
if let Some(limit) = options.frames {
if frame_index >= limit {
break 'render;
}
}
}
let _ = wglMakeCurrent(
HDC::default(),
windows::Win32::Graphics::OpenGL::HGLRC::default(),
);
let _ = wglDeleteContext(context);
ReleaseDC(Some(hwnd), hdc);
Ok(())
}
}
}
@@ -0,0 +1,182 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ash"
version = "0.38.0+1.3.281"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
dependencies = [
"libloading",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "vulkan-present-fixture"
version = "0.0.0"
dependencies = [
"ash",
"windows",
"windows-core",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
@@ -0,0 +1,26 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
name = "vulkan-present-fixture"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
# matching the sibling crates (hook/, vulkan-layer/) in this package.
[workspace]
resolver = "2"
[[bin]]
name = "vulkan-present-fixture"
path = "src/main.rs"
[target.'cfg(target_os = "windows")'.dependencies]
ash = {version = "0.38.0", default-features = false, features = ["std", "loaded"]}
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_UI_WindowsAndMessaging",
]}
windows-core = "0.62.2"
@@ -0,0 +1,681 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(not(windows))]
fn main() {
eprintln!("vulkan-present-fixture is only functional on Windows");
}
#[cfg(windows)]
fn main() {
if let Err(error) = windows_impl::run() {
eprintln!("vulkan-present-fixture failed: {error}");
std::process::exit(1);
}
}
#[cfg_attr(not(windows), allow(dead_code))]
mod pattern {
pub const BARS: [(u8, u8, u8); 8] = [
(255, 255, 255),
(255, 255, 0),
(0, 255, 255),
(0, 255, 0),
(255, 0, 255),
(255, 0, 0),
(0, 0, 255),
(0, 0, 0),
];
pub fn clear_colour(frame_index: u64) -> [f32; 4] {
let (r, g, b) = BARS[(frame_index % BARS.len() as u64) as usize];
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
}
}
#[cfg(windows)]
mod cli {
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum FormatChoice {
Bgra,
Rgba,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PresentModeChoice {
Fifo,
Mailbox,
Immediate,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PresentWaitChoice {
Semaphore,
None,
}
pub struct Options {
pub frames: Option<u64>,
pub width: u32,
pub height: u32,
pub borderless: bool,
pub format: FormatChoice,
pub present_mode: PresentModeChoice,
pub present_wait: PresentWaitChoice,
pub resize_at: Option<u64>,
pub resize_width: Option<u32>,
pub resize_height: Option<u32>,
}
impl Default for Options {
fn default() -> Self {
Self {
frames: None,
width: 1280,
height: 720,
borderless: false,
format: FormatChoice::Bgra,
present_mode: PresentModeChoice::Fifo,
present_wait: PresentWaitChoice::Semaphore,
resize_at: None,
resize_width: None,
resize_height: None,
}
}
}
impl Options {
pub fn from_env() -> Self {
let mut options = Options::default();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--frames" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.frames = Some(value);
}
}
"--width" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.width = value;
}
}
"--height" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.height = value;
}
}
"--borderless" => options.borderless = true,
"--windowed" => options.borderless = false,
"--format" => {
if let Some(value) = args.next() {
options.format = match value.to_ascii_lowercase().as_str() {
"rgba" => FormatChoice::Rgba,
_ => FormatChoice::Bgra,
};
}
}
"--present-mode" => {
if let Some(value) = args.next() {
options.present_mode = match value.to_ascii_lowercase().as_str() {
"mailbox" => PresentModeChoice::Mailbox,
"immediate" => PresentModeChoice::Immediate,
_ => PresentModeChoice::Fifo,
};
}
}
"--present-wait" => {
if let Some(value) = args.next() {
options.present_wait = match value.to_ascii_lowercase().as_str() {
"none" => PresentWaitChoice::None,
_ => PresentWaitChoice::Semaphore,
};
}
}
"--resize-at" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.resize_at = Some(value);
}
}
"--resize-width" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.resize_width = Some(value);
}
}
"--resize-height" => {
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
options.resize_height = Some(value);
}
}
_ => {}
}
}
options
}
}
}
#[cfg(windows)]
mod windows_impl {
use super::cli::{FormatChoice, Options, PresentModeChoice, PresentWaitChoice};
use super::pattern;
use ash::{khr, vk};
use std::error::Error;
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::{
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
GetClientRect, MSG, MoveWindow, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW,
SW_SHOW, ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW,
WS_OVERLAPPEDWINDOW, WS_POPUP, WS_VISIBLE,
};
use windows::core::w;
type DynError = Box<dyn Error>;
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
match msg {
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
unsafe fn create_window(
width: u32,
height: u32,
borderless: bool,
) -> Result<(HWND, isize), DynError> {
unsafe {
let instance = GetModuleHandleW(None)?;
let class_name = w!("FluxerVulkanFixture");
let wc = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wnd_proc),
hInstance: instance.into(),
lpszClassName: class_name,
..Default::default()
};
RegisterClassExW(&wc);
let style = if borderless {
WS_POPUP | WS_VISIBLE
} else {
WS_OVERLAPPEDWINDOW | WS_VISIBLE
};
let hwnd = CreateWindowExW(
Default::default(),
class_name,
w!("Fluxer Vulkan Present Fixture"),
style,
CW_USEDEFAULT,
CW_USEDEFAULT,
width as i32,
height as i32,
None,
None,
Some(instance.into()),
None,
)?;
let _ = ShowWindow(hwnd, SW_SHOW);
Ok((hwnd, instance.0 as isize))
}
}
unsafe fn client_extent(hwnd: HWND) -> vk::Extent2D {
unsafe {
let mut rect = RECT::default();
let _ = GetClientRect(hwnd, &mut rect);
vk::Extent2D {
width: (rect.right - rect.left).max(1) as u32,
height: (rect.bottom - rect.top).max(1) as u32,
}
}
}
unsafe fn resize_window(hwnd: HWND, width: u32, height: u32) {
unsafe {
let _ = MoveWindow(hwnd, 0, 0, width as i32, height as i32, true);
}
}
fn pick_surface_format(
formats: &[vk::SurfaceFormatKHR],
choice: FormatChoice,
) -> Result<vk::SurfaceFormatKHR, DynError> {
let preferred: [vk::Format; 2] = match choice {
FormatChoice::Bgra => [vk::Format::B8G8R8A8_UNORM, vk::Format::B8G8R8A8_SRGB],
FormatChoice::Rgba => [vk::Format::R8G8B8A8_UNORM, vk::Format::R8G8B8A8_SRGB],
};
let any_8bit = [
vk::Format::B8G8R8A8_UNORM,
vk::Format::R8G8B8A8_UNORM,
vk::Format::B8G8R8A8_SRGB,
vk::Format::R8G8B8A8_SRGB,
];
formats
.iter()
.copied()
.find(|f| preferred.contains(&f.format))
.or_else(|| {
formats
.iter()
.copied()
.find(|f| any_8bit.contains(&f.format))
})
.or_else(|| formats.first().copied())
.ok_or_else(|| "surface advertised no formats".into())
}
fn pick_present_mode(
available: &[vk::PresentModeKHR],
choice: PresentModeChoice,
) -> vk::PresentModeKHR {
let wanted = match choice {
PresentModeChoice::Fifo => vk::PresentModeKHR::FIFO,
PresentModeChoice::Mailbox => vk::PresentModeKHR::MAILBOX,
PresentModeChoice::Immediate => vk::PresentModeKHR::IMMEDIATE,
};
if available.contains(&wanted) {
wanted
} else {
vk::PresentModeKHR::FIFO
}
}
struct Swapchain {
handle: vk::SwapchainKHR,
images: Vec<vk::Image>,
command_buffers: Vec<vk::CommandBuffer>,
extent: vk::Extent2D,
format: vk::Format,
}
impl Swapchain {
#[allow(clippy::too_many_arguments)]
unsafe fn create(
device: &ash::Device,
swapchain_device: &khr::swapchain::Device,
surface_instance: &khr::surface::Instance,
physical_device: vk::PhysicalDevice,
surface: vk::SurfaceKHR,
command_pool: vk::CommandPool,
surface_format: vk::SurfaceFormatKHR,
present_mode: vk::PresentModeKHR,
requested_extent: vk::Extent2D,
old: vk::SwapchainKHR,
) -> Result<Self, DynError> {
unsafe {
let surface_caps = surface_instance
.get_physical_device_surface_capabilities(physical_device, surface)?;
let extent = if surface_caps.current_extent.width != u32::MAX {
surface_caps.current_extent
} else {
vk::Extent2D {
width: requested_extent.width.clamp(
surface_caps.min_image_extent.width,
surface_caps.max_image_extent.width,
),
height: requested_extent.height.clamp(
surface_caps.min_image_extent.height,
surface_caps.max_image_extent.height,
),
}
};
let image_count =
surface_caps
.min_image_count
.max(2)
.min(if surface_caps.max_image_count == 0 {
u32::MAX
} else {
surface_caps.max_image_count
});
let swapchain_info = vk::SwapchainCreateInfoKHR::default()
.surface(surface)
.min_image_count(image_count)
.image_format(surface_format.format)
.image_color_space(surface_format.color_space)
.image_extent(extent)
.image_array_layers(1)
.image_usage(
vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_DST,
)
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
.pre_transform(surface_caps.current_transform)
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
.present_mode(present_mode)
.clipped(true)
.old_swapchain(old);
let handle = swapchain_device.create_swapchain(&swapchain_info, None)?;
let images = swapchain_device.get_swapchain_images(handle)?;
let alloc_info = vk::CommandBufferAllocateInfo::default()
.command_pool(command_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(images.len() as u32);
let command_buffers = device.allocate_command_buffers(&alloc_info)?;
Ok(Self {
handle,
images,
command_buffers,
extent,
format: surface_format.format,
})
}
}
unsafe fn destroy(
self,
device: &ash::Device,
swapchain_device: &khr::swapchain::Device,
command_pool: vk::CommandPool,
) {
unsafe {
if !self.command_buffers.is_empty() {
device.free_command_buffers(command_pool, &self.command_buffers);
}
swapchain_device.destroy_swapchain(self.handle, None);
}
}
}
pub fn run() -> Result<(), DynError> {
let options = Options::from_env();
unsafe {
let (hwnd, hinstance) =
create_window(options.width, options.height, options.borderless)?;
println!("HWND={}", hwnd.0 as isize);
use std::io::Write;
let _ = std::io::stdout().flush();
let entry = ash::Entry::load()?;
let app_name = c"fluxer-vulkan-present-fixture";
let app_info = vk::ApplicationInfo::default()
.application_name(app_name)
.api_version(vk::API_VERSION_1_1);
let instance_extensions = [
khr::surface::NAME.as_ptr(),
khr::win32_surface::NAME.as_ptr(),
];
let instance_info = vk::InstanceCreateInfo::default()
.application_info(&app_info)
.enabled_extension_names(&instance_extensions);
let instance = entry.create_instance(&instance_info, None)?;
let surface_instance = khr::surface::Instance::new(&entry, &instance);
let win32_surface = khr::win32_surface::Instance::new(&entry, &instance);
let surface_info = vk::Win32SurfaceCreateInfoKHR::default()
.hinstance(hinstance)
.hwnd(hwnd.0 as isize);
let surface = win32_surface.create_win32_surface(&surface_info, None)?;
let physical_devices = instance.enumerate_physical_devices()?;
let (physical_device, queue_family_index) = physical_devices
.iter()
.find_map(|&physical_device| {
let families =
instance.get_physical_device_queue_family_properties(physical_device);
families.iter().enumerate().find_map(|(index, family)| {
let index = index as u32;
let graphics = family.queue_flags.contains(vk::QueueFlags::GRAPHICS);
let present = surface_instance
.get_physical_device_surface_support(physical_device, index, surface)
.unwrap_or(false);
(graphics && present).then_some((physical_device, index))
})
})
.ok_or("no Vulkan device with a graphics+present queue family")?;
let queue_priorities = [1.0_f32];
let queue_info = vk::DeviceQueueCreateInfo::default()
.queue_family_index(queue_family_index)
.queue_priorities(&queue_priorities);
let device_extensions = [khr::swapchain::NAME.as_ptr()];
let queue_infos = [queue_info];
let device_info = vk::DeviceCreateInfo::default()
.queue_create_infos(&queue_infos)
.enabled_extension_names(&device_extensions);
let device = instance.create_device(physical_device, &device_info, None)?;
let queue = device.get_device_queue(queue_family_index, 0);
let swapchain_device = khr::swapchain::Device::new(&instance, &device);
let formats =
surface_instance.get_physical_device_surface_formats(physical_device, surface)?;
let surface_format = pick_surface_format(&formats, options.format)?;
let available_present_modes = surface_instance
.get_physical_device_surface_present_modes(physical_device, surface)?;
let present_mode = pick_present_mode(&available_present_modes, options.present_mode);
eprintln!(
"vulkan-present-fixture: vkFormat={} vkColorSpace={} vkPresentMode={}",
surface_format.format.as_raw(),
surface_format.color_space.as_raw(),
present_mode.as_raw()
);
let pool_info = vk::CommandPoolCreateInfo::default()
.queue_family_index(queue_family_index)
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
let command_pool = device.create_command_pool(&pool_info, None)?;
let mut swapchain = Swapchain::create(
&device,
&swapchain_device,
&surface_instance,
physical_device,
surface,
command_pool,
surface_format,
present_mode,
client_extent(hwnd),
vk::SwapchainKHR::null(),
)?;
let image_available =
device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?;
let render_finished =
device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?;
let in_flight = device.create_fence(
&vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED),
None,
)?;
let subresource_range = vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
};
let recreate_swapchain = |swapchain: &mut Swapchain| -> Result<(), DynError> {
device.device_wait_idle()?;
let new = Swapchain::create(
&device,
&swapchain_device,
&surface_instance,
physical_device,
surface,
command_pool,
surface_format,
present_mode,
client_extent(hwnd),
swapchain.handle,
)?;
eprintln!(
"vulkan-present-fixture: swapchain recreated extent={}x{} vkFormat={}",
new.extent.width,
new.extent.height,
new.format.as_raw()
);
let old = std::mem::replace(swapchain, new);
old.destroy(&device, &swapchain_device, command_pool);
Ok(())
};
let mut frame_index: u64 = 0;
let mut resized = false;
let mut msg = MSG::default();
'render: loop {
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
break 'render;
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
if let Some(at) = options.resize_at {
if !resized && frame_index >= at {
if let (Some(width), Some(height)) =
(options.resize_width, options.resize_height)
{
resize_window(hwnd, width, height);
}
recreate_swapchain(&mut swapchain)?;
resized = true;
}
}
device.wait_for_fences(&[in_flight], true, u64::MAX)?;
let (image_index, suboptimal) = match swapchain_device.acquire_next_image(
swapchain.handle,
u64::MAX,
image_available,
vk::Fence::null(),
) {
Ok(value) => value,
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
recreate_swapchain(&mut swapchain)?;
continue;
}
Err(error) => return Err(error.into()),
};
device.reset_fences(&[in_flight])?;
let command_buffer = swapchain.command_buffers[image_index as usize];
let image = swapchain.images[image_index as usize];
device
.reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty())?;
device.begin_command_buffer(
command_buffer,
&vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
)?;
let to_transfer = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(image)
.subresource_range(subresource_range);
device.cmd_pipeline_barrier(
command_buffer,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[to_transfer],
);
let clear = pattern::clear_colour(frame_index);
let clear_value = vk::ClearColorValue { float32: clear };
device.cmd_clear_color_image(
command_buffer,
image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&clear_value,
&[subresource_range],
);
let to_present = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.dst_access_mask(vk::AccessFlags::empty())
.old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(image)
.subresource_range(subresource_range);
device.cmd_pipeline_barrier(
command_buffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
vk::DependencyFlags::empty(),
&[],
&[],
&[to_present],
);
device.end_command_buffer(command_buffer)?;
let wait_semaphores = [image_available];
let wait_stages = [vk::PipelineStageFlags::TRANSFER];
let command_buffers = [command_buffer];
let mut submit = vk::SubmitInfo::default()
.wait_semaphores(&wait_semaphores)
.wait_dst_stage_mask(&wait_stages)
.command_buffers(&command_buffers);
let signal_semaphores = [render_finished];
if options.present_wait == PresentWaitChoice::Semaphore {
submit = submit.signal_semaphores(&signal_semaphores);
}
device.queue_submit(queue, &[submit], in_flight)?;
let swapchains = [swapchain.handle];
let image_indices = [image_index];
if options.present_wait == PresentWaitChoice::None {
device.wait_for_fences(&[in_flight], true, u64::MAX)?;
}
let mut present_info = vk::PresentInfoKHR::default()
.swapchains(&swapchains)
.image_indices(&image_indices);
if options.present_wait == PresentWaitChoice::Semaphore {
present_info = present_info.wait_semaphores(&signal_semaphores);
}
let present_result = swapchain_device.queue_present(queue, &present_info);
let needs_recreate = match present_result {
Ok(present_suboptimal) => present_suboptimal || suboptimal,
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => true,
Err(error) => return Err(error.into()),
};
if needs_recreate {
recreate_swapchain(&mut swapchain)?;
}
frame_index += 1;
if let Some(limit) = options.frames {
if frame_index >= limit {
break 'render;
}
}
}
let _ = device.device_wait_idle();
device.destroy_fence(in_flight, None);
device.destroy_semaphore(render_finished, None);
device.destroy_semaphore(image_available, None);
swapchain.destroy(&device, &swapchain_device, command_pool);
device.destroy_command_pool(command_pool, None);
surface_instance.destroy_surface(surface, None);
device.destroy_device(None);
instance.destroy_instance(None);
Ok(())
}
}
}
@@ -0,0 +1,180 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![cfg(target_os = "windows")]
use fluxer_win_game_capture::{EncoderAttachError, EncoderAttachment};
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0};
use windows::Win32::Graphics::Direct3D11::{
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_CREATE_DEVICE_BGRA_SUPPORT,
D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_RESOURCE_MISC_SHARED, D3D11_SDK_VERSION,
D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11CreateDevice, ID3D11Device,
ID3D11DeviceContext, ID3D11Multithread, ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC};
use windows::core::Interface;
fn try_create_device() -> Option<(ID3D11Device, ID3D11DeviceContext)> {
let feature_levels = [D3D_FEATURE_LEVEL_11_0];
let flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
let result = unsafe {
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
Default::default(),
flags,
Some(&feature_levels),
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)
};
if result.is_err() {
return None;
}
let dev = device?;
let ctx = context?;
if let Ok(mt) = dev.cast::<ID3D11Multithread>() {
let _ = unsafe { mt.SetMultithreadProtected(true) };
}
Some((dev, ctx))
}
fn create_bgra_capture_texture(
device: &ID3D11Device,
width: u32,
height: u32,
) -> Option<ID3D11Texture2D> {
let desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: height,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
CPUAccessFlags: 0,
MiscFlags: D3D11_RESOURCE_MISC_SHARED.0 as u32,
};
let mut tex = None;
unsafe { device.CreateTexture2D(&desc, None, Some(&mut tex)) }.ok()?;
tex
}
#[test]
fn windows_attach_construct_and_detach() {
let _device = match try_create_device() {
Some(d) => d,
None => {
eprintln!("skip: no D3D11 device available");
return;
}
};
let attach = match EncoderAttachment::try_new(640, 480) {
Ok(a) => a,
Err(e) => {
eprintln!("skip: EncoderAttachment init failed: {e:?}");
return;
}
};
assert!(attach.is_attached(), "attached after construction");
assert_eq!(attach.width(), 640, "width preserved");
assert_eq!(attach.height(), 480, "height preserved");
assert_eq!(attach.capacity(), 8, "capacity matches RING_SIZE");
let stats = attach.stats();
assert_eq!(stats.frames_submitted, 0, "fresh attach: no submissions");
assert_eq!(stats.failed_blits, 0, "fresh attach: no failed blits");
attach.detach();
assert!(!attach.is_attached(), "post-detach not attached");
}
#[test]
fn submit_capture_frame_with_blit_records_stats() {
let (device, _context) = match try_create_device() {
Some(d) => d,
None => {
eprintln!("skip: no D3D11 device available");
return;
}
};
let attach = match EncoderAttachment::try_new(640, 480) {
Ok(a) => a,
Err(e) => {
eprintln!("skip: attach init failed: {e:?}");
return;
}
};
let capture = match create_bgra_capture_texture(&device, 640, 480) {
Some(t) => t,
None => {
eprintln!("skip: capture texture create failed");
return;
}
};
let result = attach.submit_capture_frame_with_blit(&capture, 640, 480);
let stats = attach.stats();
match result {
Ok(()) => {
assert!(
stats.frames_submitted == 1,
"expected 1 submission, got {}",
stats.frames_submitted
);
assert!(
stats.failed_blits == 0,
"expected 0 failed blits, got {}",
stats.failed_blits
);
}
Err(EncoderAttachError::BlitFailed) => {
assert!(
stats.failed_blits >= 1,
"expected >=1 failed blits, got {}",
stats.failed_blits
);
}
Err(EncoderAttachError::DeviceUnavailable) => {
eprintln!("device unavailable from ring backend (driver constraint)");
}
Err(other) => {
eprintln!("submit_capture_frame_with_blit unexpected error: {other:?}");
}
}
}
#[test]
fn submit_notify_back_pressure_increments_drops_after_capacity() {
let _device = match try_create_device() {
Some(d) => d,
None => {
eprintln!("skip: no D3D11 device available");
return;
}
};
let attach = match EncoderAttachment::try_new(64, 64) {
Ok(a) => a,
Err(e) => {
eprintln!("skip: attach init failed: {e:?}");
return;
}
};
let total = (attach.capacity() as u64) * 2;
for _ in 0..total {
attach.submit_notify().expect("notify ok");
}
let stats = attach.stats();
assert!(
stats.frames_submitted >= attach.capacity() as u64,
"many submissions accepted"
);
assert!(stats.frames_dropped > 0, "back pressure drops oldest");
assert_eq!(
stats.ring_full_events, stats.frames_dropped,
"drops==ring_full_events"
);
}
@@ -0,0 +1,277 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ash"
version = "0.38.0+1.3.281"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
[[package]]
name = "ash-layer"
version = "0.0.2+v0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedca11308198ffa300ad1a981c10ee176e44c8ff6ee7c6883a062608b9779a5"
dependencies = [
"ash",
]
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "dashmap"
version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "fluxer_vulkan_layer"
version = "0.0.0"
dependencies = [
"ash",
"ash-layer",
"dashmap",
"once_cell",
"windows",
"windows-sys",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
@@ -0,0 +1,31 @@
[package]
name = "fluxer_vulkan_layer"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
[workspace]
resolver = "2"
[lib]
crate-type = ["cdylib"]
[target.'cfg(target_os = "windows")'.dependencies]
ash = {version = "0.38.0", default-features = false, features = ["std", "debug"]}
ash-layer = "0.0.2"
dashmap = "6.2.1"
once_cell = "1.21.4"
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
]}
windows-sys = {version = "0.61.2", features = [
"Win32_Foundation",
"Win32_System_Memory",
"Win32_System_Performance",
"Win32_System_Threading",
]}
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use ash::vk;
use windows::Win32::Graphics::{
Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN, D3D_DRIVER_TYPE_WARP},
Direct3D11::{
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_CREATE_DEVICE_BGRA_SUPPORT,
D3D11_RESOURCE_MISC_SHARED, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT,
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
},
Dxgi::{
Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_SAMPLE_DESC,
},
IDXGIResource,
},
};
use windows::core::Interface;
use crate::game_capture_abi::{GAME_CAPTURE_FLAG_HDR, GAME_CAPTURE_FLAG_TEN_BIT};
#[derive(Clone, Copy)]
pub struct InteropFormat {
pub vk_format: vk::Format,
pub dxgi_format: DXGI_FORMAT,
pub capture_flags: u32,
}
pub fn interop_format(format: vk::Format) -> Option<InteropFormat> {
let (dxgi_format, capture_flags) = match format {
vk::Format::B8G8R8A8_UNORM => (DXGI_FORMAT_B8G8R8A8_UNORM, 0),
vk::Format::B8G8R8A8_SRGB => (DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, 0),
vk::Format::R8G8B8A8_UNORM => (DXGI_FORMAT_R8G8B8A8_UNORM, 0),
vk::Format::R8G8B8A8_SRGB => (DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, 0),
vk::Format::A2B10G10R10_UNORM_PACK32 => (
DXGI_FORMAT_R10G10B10A2_UNORM,
GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR,
),
vk::Format::R16G16B16A16_SFLOAT => (DXGI_FORMAT_R16G16B16A16_FLOAT, GAME_CAPTURE_FLAG_HDR),
_ => return None,
};
Some(InteropFormat {
vk_format: format,
dxgi_format,
capture_flags,
})
}
pub struct D3d11Device {
device: ID3D11Device,
_context: ID3D11DeviceContext,
}
unsafe impl Send for D3d11Device {}
unsafe impl Sync for D3d11Device {}
pub struct SharedTexture {
_texture: ID3D11Texture2D,
pub handle: u64,
}
unsafe impl Send for SharedTexture {}
unsafe impl Sync for SharedTexture {}
impl D3d11Device {
pub fn create() -> Option<Self> {
for driver in [D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP] {
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
let result = unsafe {
D3D11CreateDevice(
None,
driver,
Default::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)
};
if result.is_ok()
&& let (Some(device), Some(context)) = (device, context)
{
return Some(Self {
device,
_context: context,
});
}
}
let _ = D3D_DRIVER_TYPE_UNKNOWN;
None
}
pub fn create_shared_texture(
&self,
width: u32,
height: u32,
format: InteropFormat,
) -> Option<SharedTexture> {
if width == 0 || height == 0 {
return None;
}
let desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: height,
MipLevels: 1,
ArraySize: 1,
Format: format.dxgi_format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: (D3D11_BIND_SHADER_RESOURCE.0 | D3D11_BIND_RENDER_TARGET.0) as u32,
CPUAccessFlags: 0,
MiscFlags: D3D11_RESOURCE_MISC_SHARED.0 as u32,
};
let mut texture: Option<ID3D11Texture2D> = None;
let result = unsafe { self.device.CreateTexture2D(&desc, None, Some(&mut texture)) };
if result.is_err() {
return None;
}
let texture = texture?;
let resource: IDXGIResource = texture.cast().ok()?;
let handle = unsafe { resource.GetSharedHandle() }.ok()?;
if handle.is_invalid() {
return None;
}
Some(SharedTexture {
_texture: texture,
handle: handle.0 as usize as u64,
})
}
}
File diff suppressed because it is too large Load Diff