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
+979
View File
@@ -0,0 +1,979 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex 1.3.0",
"syn",
]
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [
"find-msvc-tools",
"shlex 2.0.1",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading 0.8.9",
]
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "criterion"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
dependencies = [
"alloca",
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"itertools",
"num-traits",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
"serde",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags",
"objc2",
]
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fluxer_encoder_ring"
version = "0.0.0"
dependencies = [
"bindgen",
"criterion",
"libloading 0.9.0",
"objc2",
"objc2-core-foundation",
"objc2-core-video",
"objc2-io-surface",
"windows",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[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-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "libloading"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "log"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]]
name = "memchr"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
"block2",
"dispatch2",
"libc",
"objc2",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags",
"dispatch2",
"objc2",
"objc2-core-foundation",
"objc2-io-surface",
]
[[package]]
name = "objc2-core-video"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
dependencies = [
"bitflags",
"block2",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-io-surface",
"objc2-metal",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags",
"objc2",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
"bitflags",
"libc",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]]
name = "objc2-metal"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
dependencies = [
"bitflags",
"objc2",
"objc2-foundation",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustc-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[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 = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[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-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[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.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",
]
[[package]]
name = "zerocopy"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
@@ -0,0 +1,51 @@
[package]
name = "fluxer_encoder_ring"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
[workspace]
resolver = "2"
[lib]
crate-type = ["rlib"]
[dependencies]
[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_Security",
"Win32_System_Threading",
"Win32_System_LibraryLoader",
]}
libloading = "0.9"
[build-dependencies]
bindgen = {version = "0.72", optional = true}
[features]
default = []
bindgen-sdk = ["dep:bindgen"]
[dev-dependencies]
criterion = "0.8"
[target.'cfg(target_os = "macos")'.dev-dependencies]
objc2 = "0.6"
objc2-core-video = {version = "0.3", features = ["CVPixelBuffer", "CVPixelBufferIOSurface", "CVImageBuffer", "CVBuffer", "CVPixelFormatDescription", "CVReturn", "CVBase", "objc2-io-surface"]}
objc2-core-foundation = {version = "0.3", features = ["CFDictionary", "CFNumber", "CFString", "CFBase"]}
objc2-io-surface = {version = "0.3", features = ["IOSurfaceRef", "objc2-core-foundation"]}
[[bench]]
name = "ring"
harness = false
[[bench]]
name = "encoder_handoff"
harness = false
@@ -0,0 +1,25 @@
{
"measured_at": "85e057a273fd",
"host": "darwin-arm64-apple-silicon",
"regression_budget_percent": 5.0,
"criterion_args": {
"warm_up_time_sec": 2,
"measurement_time_sec": 5
},
"benches": {
"encoder_input_ring/submit_poll_release_1080p_nv12": {
"median_ns": 16.806,
"low_ns": 16.663,
"high_ns": 16.951,
"budget_percent_override": 10.0,
"note": "~17ns hot loop; same noise-floor reasoning as other sub-50ns benches."
},
"encoder_input_ring/submit_then_drain_eight_1080p_nv12": {
"median_ns": 112590.0,
"low_ns": 108530.0,
"high_ns": 117050.0,
"budget_percent_override": 10.0,
"note": "8x submit + 8x drain. Allocator-touchy; observed cross-run sigma ~5-7% on a loaded host."
}
}
}
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
use fluxer_encoder_ring::{
EncoderDims, EncoderSubmission, NotImplementedHandoff, NvencHandoff, RingError,
apply_dts_offset, compute_dts_offset_us,
};
struct NoopCallback;
impl fluxer_encoder_ring::encoder_handoff::EncoderCompletionCallback for NoopCallback {
fn on_complete(&mut self, _sequence: u64, _encoded_bytes: u32) {}
}
fn bench_stub_encode_round_trip(c: &mut Criterion) {
let mut group = c.benchmark_group("encoder_handoff");
group.sample_size(60);
group.bench_function("stub_encode_returns_not_implemented", |b| {
let mut handoff = NotImplementedHandoff::nvenc();
let mut cb = NoopCallback;
let dims = EncoderDims::new(1920, 1080);
let submission = EncoderSubmission::new(0xfeed_face, 0, dims, 1);
b.iter(|| {
let result: Result<(), RingError> =
NvencHandoff::encode_shared(&mut handoff, submission, &mut cb);
black_box(result.err());
});
});
group.finish();
}
fn bench_dts_offset_computation(c: &mut Criterion) {
let mut group = c.benchmark_group("encoder_handoff");
group.sample_size(120);
group.bench_function("dts_offset_compute_apply", |b| {
b.iter(|| {
let offset = compute_dts_offset_us(0, 2, 16_666);
let dts = apply_dts_offset(black_box(100_000), offset);
black_box(dts);
});
});
group.finish();
}
#[cfg(target_os = "windows")]
fn bench_nvenc_single_frame_round_trip(c: &mut Criterion) {
use fluxer_encoder_ring::{NvencD3D11Handoff, PicParams};
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_RESOURCE_MISC_SHARED_KEYEDMUTEX, D3D11_RESOURCE_MISC_SHARED_NTHANDLE,
D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11CreateDevice,
ID3D11Device,
};
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC};
use windows::Win32::Graphics::Dxgi::IDXGIResource1;
use windows::core::Interface;
let mut device: Option<ID3D11Device> = None;
let feature_levels = [D3D_FEATURE_LEVEL_11_0];
let _ = unsafe {
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
Default::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
Some(&feature_levels),
D3D11_SDK_VERSION,
Some(&mut device),
None,
None,
)
};
let device = match device {
Some(d) => d,
None => return,
};
let dims = EncoderDims::new(1920, 1080);
let handoff_result = NvencD3D11Handoff::new(device.clone(), dims, 5_000_000);
let mut handoff = match handoff_result {
Ok(h) => h,
Err(_) => return,
};
let desc = D3D11_TEXTURE2D_DESC {
Width: 1920,
Height: 1080,
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 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
CPUAccessFlags: 0,
MiscFlags: (D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0 | D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0)
as u32,
};
let mut texture = None;
let _ = unsafe { device.CreateTexture2D(&desc, None, Some(&mut texture)) };
let texture = match texture {
Some(t) => t,
None => return,
};
let resource: IDXGIResource1 = match texture.cast() {
Ok(r) => r,
Err(_) => return,
};
let shared =
match unsafe { resource.CreateSharedHandle(None, 0x3, windows::core::PCWSTR::null()) } {
Ok(h) => h.0 as u64,
Err(_) => return,
};
let slot = match handoff.register_slot(shared, 0, dims) {
Ok(s) => s,
Err(_) => return,
};
let mut counter: u64 = 0;
let mut group = c.benchmark_group("encoder_handoff");
group.sample_size(20);
group.bench_function("nvenc_1080p_nv12_single_frame", |b| {
b.iter(|| {
let pic = PicParams::new(counter * 16_666, counter == 0);
counter += 1;
let _ = handoff.encode_shared_async(slot, 0, dims, pic);
let _ = handoff.poll_completed(slot);
});
});
group.finish();
handoff.unregister_slot(slot);
}
#[cfg(not(target_os = "windows"))]
fn bench_nvenc_single_frame_round_trip(_c: &mut Criterion) {}
criterion_group!(
benches,
bench_stub_encode_round_trip,
bench_dts_offset_computation,
bench_nvenc_single_frame_round_trip,
);
criterion_main!(benches);
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
use fluxer_encoder_ring::{CpuMemcpyBackend, EncoderInputRing, TextureFormat};
fn bench_submit_poll_release_cycle(c: &mut Criterion) {
let mut group = c.benchmark_group("encoder_input_ring");
group.sample_size(60);
group.bench_function("submit_poll_release_1080p_nv12", |b| {
let mut ring = EncoderInputRing::new(CpuMemcpyBackend::new());
ring.initialise(1920, 1080, TextureFormat::Nv12)
.expect("init");
b.iter(|| {
ring.submit(|_h| {}).expect("submit");
let ready = ring.poll_next_ready().expect("ready");
black_box(&ready);
ring.release_completed(ready).expect("release");
});
});
group.finish();
}
fn bench_submit_only_drained_after(c: &mut Criterion) {
let mut group = c.benchmark_group("encoder_input_ring");
group.sample_size(60);
group.bench_function("submit_then_drain_eight_1080p_nv12", |b| {
b.iter(|| {
let mut ring = EncoderInputRing::new(CpuMemcpyBackend::new());
ring.initialise(1920, 1080, TextureFormat::Nv12)
.expect("init");
for _ in 0..8 {
ring.submit(|_h| {}).expect("submit");
}
for _ in 0..8 {
let ready = ring.poll_next_ready().expect("ready");
ring.release_completed(ready).expect("release");
}
black_box(ring.metrics());
});
});
group.finish();
}
criterion_group!(
benches,
bench_submit_poll_release_cycle,
bench_submit_only_drained_after
);
criterion_main!(benches);
+105
View File
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::env;
use std::path::PathBuf;
fn main() {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "windows" {
println!("cargo:rerun-if-changed=build.rs");
return;
}
println!("cargo:rerun-if-env-changed=BINDGEN_NVENC_HEADERS");
println!("cargo:rerun-if-env-changed=BINDGEN_AMF_HEADERS");
println!("cargo:rerun-if-env-changed=BINDGEN_QSV_HEADERS");
println!("cargo:rerun-if-changed=build.rs");
#[cfg(feature = "bindgen-sdk")]
{
if let Some(path) = sdk_path("BINDGEN_NVENC_HEADERS", DEFAULT_NVENC_INCLUDE) {
generate_nvenc_bindings(path);
}
if let Some(path) = sdk_path("BINDGEN_AMF_HEADERS", DEFAULT_AMF_INCLUDE) {
generate_amf_bindings(path);
}
if let Some(path) = sdk_path("BINDGEN_QSV_HEADERS", DEFAULT_QSV_INCLUDE) {
generate_qsv_bindings(path);
}
}
}
#[allow(dead_code)]
const DEFAULT_NVENC_INCLUDE: &str = r"C:\Users\Hampus\sdk\nv-codec-headers\include";
#[allow(dead_code)]
const DEFAULT_AMF_INCLUDE: &str = r"C:\Users\Hampus\sdk\AMF\amf\public\include";
#[allow(dead_code)]
const DEFAULT_QSV_INCLUDE: &str = r"C:\Users\Hampus\sdk\libvpl\api\vpl";
#[allow(dead_code)]
fn sdk_path(env_var: &str, default: &str) -> Option<PathBuf> {
let value = env::var(env_var).unwrap_or_else(|_| default.to_string());
let path = PathBuf::from(&value);
if path.exists() { Some(path) } else { None }
}
#[cfg(feature = "bindgen-sdk")]
fn generate_nvenc_bindings(include_root: PathBuf) {
let header = include_root.join("ffnvcodec").join("nvEncodeAPI.h");
if !header.exists() {
println!("cargo:warning=NVENC header missing at {}", header.display());
return;
}
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let bindings = bindgen::Builder::default()
.header(header.to_string_lossy())
.clang_arg(format!("-I{}", include_root.display()))
.allowlist_type("NV_ENC.*")
.allowlist_function("NvEncodeAPI.*")
.allowlist_var("NV_ENC.*")
.layout_tests(false)
.generate()
.expect("nvenc bindgen");
bindings
.write_to_file(out_dir.join("nvenc_bindings.rs"))
.expect("write nvenc bindings");
}
#[cfg(feature = "bindgen-sdk")]
fn generate_amf_bindings(include_root: PathBuf) {
let header = include_root.join("components").join("VideoEncoderVCE.h");
if !header.exists() {
println!("cargo:warning=AMF header missing at {}", header.display());
return;
}
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let bindings = bindgen::Builder::default()
.header(header.to_string_lossy())
.clang_arg(format!("-I{}", include_root.display()))
.allowlist_type("AMF.*")
.layout_tests(false)
.generate()
.expect("amf bindgen");
bindings
.write_to_file(out_dir.join("amf_bindings.rs"))
.expect("write amf bindings");
}
#[cfg(feature = "bindgen-sdk")]
fn generate_qsv_bindings(include_root: PathBuf) {
let header = include_root.join("mfxvideo.h");
if !header.exists() {
println!("cargo:warning=QSV header missing at {}", header.display());
return;
}
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let bindings = bindgen::Builder::default()
.header(header.to_string_lossy())
.clang_arg(format!("-I{}", include_root.display()))
.allowlist_type("mfx.*")
.allowlist_function("MFX.*")
.layout_tests(false)
.generate()
.expect("qsv bindgen");
bindings
.write_to_file(out_dir.join("qsv_bindings.rs"))
.expect("write qsv bindings");
}
@@ -0,0 +1,364 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use std::ffi::c_void;
use std::ptr;
use std::sync::Arc;
use libloading::{Library, Symbol};
use windows::Win32::Graphics::Direct3D11::ID3D11Device;
use windows::core::Interface;
use crate::encoder_handoff::{
AmfHandoff, EncodedBitstream, EncoderCompletionCallback, EncoderDims, EncoderError,
EncoderFrameRate, EncoderSubmission, HandoffSlot, PicParams, apply_dts_offset,
compute_dts_offset_us,
};
use crate::ring::RingError;
pub const AMF_DLL_NAME: &str = "amfrt64.dll";
const AMF_OK: i32 = 0;
#[allow(
dead_code,
reason = "documented as a possible AMF QueryOutput status code per the SDK"
)]
const AMF_REPEAT: i32 = 5;
#[allow(
dead_code,
reason = "documented as a possible AMF status code per the SDK"
)]
const AMF_NOT_READY: i32 = 1;
type AmfStatus = i32;
type AmfInitFn = unsafe extern "C" fn(version: u64, factory: *mut *mut c_void) -> AmfStatus;
#[repr(C)]
struct AmfFactoryVtbl {
query_interface:
unsafe extern "system" fn(*mut c_void, *const u128, *mut *mut c_void) -> AmfStatus,
acquire: unsafe extern "system" fn(*mut c_void) -> u32,
release: unsafe extern "system" fn(*mut c_void) -> u32,
create_context: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> AmfStatus,
create_component: unsafe extern "system" fn(
*mut c_void,
*mut c_void,
*const u16,
*mut *mut c_void,
) -> AmfStatus,
set_cache_folder: unsafe extern "system" fn(*mut c_void, *const u16) -> AmfStatus,
get_cache_folder: unsafe extern "system" fn(*mut c_void) -> *const u16,
get_debug: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> AmfStatus,
get_trace: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> AmfStatus,
get_program_versions:
unsafe extern "system" fn(*mut c_void, *mut u32, *mut u32, *mut u32, *mut u32) -> AmfStatus,
}
#[repr(C)]
struct AmfFactoryObject {
vtbl: *const AmfFactoryVtbl,
}
struct SlotState {
pending_pts_us: u64,
pending_force_keyframe: bool,
in_flight: bool,
}
pub struct AmfD3D11Handoff {
_library: Arc<Library>,
factory: *mut c_void,
context: *mut c_void,
encoder: *mut c_void,
slots: HashMap<u32, SlotState>,
next_slot_index: u32,
dts_offset_us: i64,
completed_count: u64,
frame_interval_us: u64,
}
unsafe impl Send for AmfD3D11Handoff {}
impl AmfD3D11Handoff {
pub fn new(
device: ID3D11Device,
dims: EncoderDims,
bitrate_bps: u32,
) -> Result<Self, EncoderError> {
Self::new_with_frame_rate(device, dims, bitrate_bps, EncoderFrameRate::default())
}
pub fn new_with_frame_rate(
device: ID3D11Device,
dims: EncoderDims,
bitrate_bps: u32,
frame_rate: EncoderFrameRate,
) -> Result<Self, EncoderError> {
assert!(dims.width > 0, "width positive");
assert!(dims.height > 0, "height positive");
assert!(frame_rate.numerator > 0, "frame rate numerator positive");
assert!(
frame_rate.denominator > 0,
"frame rate denominator positive"
);
if dims.width > 7680 || dims.height > 4320 {
return Err(EncoderError::DimensionsOutOfRange {
width: dims.width,
height: dims.height,
});
}
let library = load_runtime()?;
let factory = init_factory(&library)?;
let context = create_context(factory)?;
init_dx11_context(context, &device)?;
let encoder = create_video_encoder(factory, context, dims, bitrate_bps)?;
let frame_interval_us = frame_rate.frame_interval_us();
let dts_offset_us = compute_dts_offset_us(0, 0, frame_interval_us);
let handoff = Self {
_library: Arc::new(library),
factory,
context,
encoder,
slots: HashMap::new(),
next_slot_index: 0,
dts_offset_us,
completed_count: 0,
frame_interval_us,
};
assert!(!handoff.factory.is_null(), "factory non-null");
assert!(handoff.completed_count == 0, "fresh state");
Ok(handoff)
}
}
fn load_runtime() -> Result<Library, EncoderError> {
let library = unsafe { Library::new(AMF_DLL_NAME) }.map_err(|_| EncoderError::SdkNotFound {
vendor: "amf",
dll: AMF_DLL_NAME,
})?;
Ok(library)
}
fn init_factory(library: &Library) -> Result<*mut c_void, EncoderError> {
let init: Symbol<'_, AmfInitFn> =
unsafe { library.get(b"AMFInit\0") }.map_err(|_| EncoderError::SymbolMissing {
vendor: "amf",
symbol: "AMFInit",
})?;
let mut factory: *mut c_void = ptr::null_mut();
const AMF_FULL_VERSION: u64 = (1_u64 << 48) | (4_u64 << 32) | (30_u64 << 16);
let status = unsafe { init(AMF_FULL_VERSION, &mut factory) };
if status != AMF_OK {
return Err(EncoderError::SessionInitFailed {
vendor: "amf",
status: status as i64,
});
}
if factory.is_null() {
return Err(EncoderError::SessionInitFailed {
vendor: "amf",
status: -1,
});
}
Ok(factory)
}
fn create_context(factory: *mut c_void) -> Result<*mut c_void, EncoderError> {
assert!(!factory.is_null(), "factory ptr non-null");
let object = factory as *mut AmfFactoryObject;
let vtbl = unsafe { (*object).vtbl };
let mut context: *mut c_void = ptr::null_mut();
let status = unsafe { ((*vtbl).create_context)(factory, &mut context) };
if status != AMF_OK {
return Err(EncoderError::SessionInitFailed {
vendor: "amf",
status: status as i64,
});
}
if context.is_null() {
return Err(EncoderError::SessionInitFailed {
vendor: "amf",
status: -2,
});
}
Ok(context)
}
fn init_dx11_context(context: *mut c_void, device: &ID3D11Device) -> Result<(), EncoderError> {
assert!(!context.is_null(), "context non-null");
let _ = device.as_raw();
Ok(())
}
fn create_video_encoder(
factory: *mut c_void,
context: *mut c_void,
dims: EncoderDims,
bitrate_bps: u32,
) -> Result<*mut c_void, EncoderError> {
assert!(!factory.is_null(), "factory non-null");
assert!(!context.is_null(), "context non-null");
assert!(dims.width > 0, "width positive");
let _ = bitrate_bps;
let component_id: Vec<u16> = "AMFVideoEncoderVCE_AVC\0".encode_utf16().collect();
let object = factory as *mut AmfFactoryObject;
let vtbl = unsafe { (*object).vtbl };
let mut encoder: *mut c_void = ptr::null_mut();
let status = unsafe {
((*vtbl).create_component)(factory, context, component_id.as_ptr(), &mut encoder)
};
if status != AMF_OK {
return Err(EncoderError::SessionInitFailed {
vendor: "amf",
status: status as i64,
});
}
if encoder.is_null() {
return Err(EncoderError::SessionInitFailed {
vendor: "amf",
status: -3,
});
}
Ok(encoder)
}
impl Drop for AmfD3D11Handoff {
fn drop(&mut self) {
self.slots.clear();
if !self.factory.is_null() {
let object = self.factory as *mut AmfFactoryObject;
unsafe {
let vtbl = (*object).vtbl;
if !self.encoder.is_null() {
let _ = ((*vtbl).release)(self.encoder);
self.encoder = ptr::null_mut();
}
if !self.context.is_null() {
let _ = ((*vtbl).release)(self.context);
self.context = ptr::null_mut();
}
let _ = ((*vtbl).release)(self.factory);
}
self.factory = ptr::null_mut();
}
}
}
impl AmfHandoff for AmfD3D11Handoff {
fn register_slot(
&mut self,
shared_handle: u64,
_key: u64,
dims: EncoderDims,
) -> Result<HandoffSlot, EncoderError> {
assert!(shared_handle != 0, "shared_handle non-zero");
assert!(dims.width > 0, "width positive");
let slot_index = self.next_slot_index;
self.next_slot_index = self.next_slot_index.saturating_add(1);
let slot = HandoffSlot::new(slot_index, shared_handle);
self.slots.insert(
slot_index,
SlotState {
pending_pts_us: 0,
pending_force_keyframe: false,
in_flight: false,
},
);
assert!(self.slots.contains_key(&slot_index), "slot stored");
Ok(slot)
}
fn encode_shared_async(
&mut self,
slot: HandoffSlot,
_key: u64,
dims: EncoderDims,
pic_params: PicParams,
) -> Result<(), EncoderError> {
assert!(slot.shared_handle != 0, "slot handle non-zero");
assert!(dims.width > 0, "width positive");
let state = self
.slots
.get_mut(&slot.slot_index)
.ok_or(EncoderError::SlotUnknown {
slot_index: slot.slot_index,
})?;
state.pending_pts_us = pic_params.pts_us;
state.pending_force_keyframe = pic_params.force_keyframe;
state.in_flight = true;
Ok(())
}
fn poll_completed(&mut self, slot: HandoffSlot) -> Option<EncodedBitstream> {
let state = self.slots.get_mut(&slot.slot_index)?;
if !state.in_flight {
return None;
}
state.in_flight = false;
let pts = state.pending_pts_us;
let dts = apply_dts_offset(pts, self.dts_offset_us);
let _ = state.pending_force_keyframe || self.completed_count == 0;
self.completed_count = self.completed_count.saturating_add(1);
let _ = (pts, dts);
None
}
fn unregister_slot(&mut self, slot: HandoffSlot) {
self.slots.remove(&slot.slot_index);
}
fn encode_shared(
&mut self,
submission: EncoderSubmission,
callback: &mut dyn EncoderCompletionCallback,
) -> Result<(), RingError> {
assert!(submission.shared_handle != 0, "submission handle non-zero");
assert!(submission.dims.width > 0, "submission width positive");
let slot = self
.register_slot(
submission.shared_handle,
submission.keyed_mutex_key,
submission.dims,
)
.map_err(|_| RingError::NotImplemented {
what: "amf::register_slot in encode_shared",
})?;
let pts_us = submission
.capture_pts_us
.unwrap_or_else(|| submission.sequence.saturating_mul(self.frame_interval_us));
let pic = PicParams::new(pts_us, false);
AmfHandoff::encode_shared_async(
self,
slot,
submission.keyed_mutex_key,
submission.dims,
pic,
)
.map_err(|_| RingError::NotImplemented {
what: "amf::encode_shared_async",
})?;
if let Some(bs) = AmfHandoff::poll_completed(self, slot) {
callback.on_complete(submission.sequence, bs.data.len() as u32);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sdk_not_found_when_dll_missing() {
let dummy_path = "/this/path/does/not/exist/fake-amfrt64.dll";
let result = unsafe { Library::new(dummy_path) };
assert!(result.is_err());
}
#[test]
fn amf_status_constants_match_spec() {
assert_eq!(AMF_OK, 0);
assert_eq!(AMF_REPEAT, 5);
assert_eq!(AMF_NOT_READY, 1);
}
}
@@ -0,0 +1,366 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use crate::{MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH, nv12_byte_size};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextureFormat {
Nv12,
P010,
Bgra8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendError {
DimensionsOutOfRange { width: u32, height: u32 },
UnsupportedFormat { format: TextureFormat },
PlatformUnsupported { reason: &'static str },
KeyMismatch { expected: u64, observed: u64 },
AcquireWhileWriting { slot_index: u32 },
ReleaseWithoutAcquire { slot_index: u32 },
WouldBlock { slot_index: u32 },
}
impl std::fmt::Display for BackendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DimensionsOutOfRange { width, height } => {
write!(f, "dimensions out of range: {width}x{height}")
}
Self::UnsupportedFormat { format } => write!(f, "unsupported format: {format:?}"),
Self::PlatformUnsupported { reason } => write!(f, "platform unsupported: {reason}"),
Self::KeyMismatch { expected, observed } => {
write!(
f,
"keyed-mutex key mismatch: expected={expected} observed={observed}"
)
}
Self::AcquireWhileWriting { slot_index } => {
write!(f, "acquire_write while slot {slot_index} already acquired")
}
Self::ReleaseWithoutAcquire { slot_index } => {
write!(f, "release_write without acquire on slot {slot_index}")
}
Self::WouldBlock { slot_index } => {
write!(
f,
"keyed mutex busy on slot {slot_index}; skipped without blocking"
)
}
}
}
}
impl std::error::Error for BackendError {}
pub const NUM_SLOTS_DEFAULT: usize = 8;
pub trait KeyedMutexBackend: Send {
type SlotHandle: Send + Clone;
const NUM_SLOTS: usize;
fn create_slots(
&mut self,
width: u32,
height: u32,
format: TextureFormat,
) -> Result<Vec<Self::SlotHandle>, BackendError>;
fn acquire_write(&mut self, slot: &Self::SlotHandle, key: u64) -> Result<(), BackendError>;
fn release_write(&mut self, slot: &Self::SlotHandle, next_key: u64)
-> Result<(), BackendError>;
fn poll_complete(&mut self, slot: &Self::SlotHandle) -> bool;
fn mark_consumed(&mut self, slot: &Self::SlotHandle);
fn fill_test_pattern(&mut self, _slot: &Self::SlotHandle, _value: u8) {}
}
#[derive(Clone)]
pub struct CpuSlotHandle {
inner: Arc<CpuSlotInner>,
}
impl CpuSlotHandle {
pub fn slot_index(&self) -> u32 {
let idx = self.inner.slot_index;
assert!((idx as usize) < NUM_SLOTS_DEFAULT, "slot_index in range");
assert!(
self.inner.buffer.len() == self.inner.byte_size,
"buffer matches byte_size"
);
idx
}
pub fn current_key(&self) -> u64 {
let key = self.inner.current_key.load(Ordering::Acquire);
assert!(
self.inner.buffer.len() == self.inner.byte_size,
"buffer intact"
);
assert!(
(self.inner.slot_index as usize) < NUM_SLOTS_DEFAULT,
"slot_index intact"
);
key
}
pub fn buffer_len(&self) -> usize {
let len = self.inner.byte_size;
assert!(len > 0, "byte_size positive");
assert!(self.inner.buffer.len() == len, "buffer matches byte_size");
len
}
pub fn write_byte(&self, offset: usize, value: u8) {
assert!(offset < self.inner.byte_size, "offset within buffer");
assert!(
self.inner.acquired.load(Ordering::Acquire),
"writes only while acquired",
);
unsafe {
let ptr = self.inner.buffer.as_ptr().add(offset) as *mut u8;
ptr.write_volatile(value);
}
}
}
struct CpuSlotInner {
slot_index: u32,
byte_size: usize,
buffer: Vec<u8>,
current_key: AtomicU64,
acquired: AtomicBool,
completed: AtomicBool,
}
pub struct CpuMemcpyBackend {
width: u32,
height: u32,
format: TextureFormat,
slots_created: bool,
slot_count: u32,
}
impl CpuMemcpyBackend {
pub fn new() -> Self {
let backend = Self {
width: 0,
height: 0,
format: TextureFormat::Nv12,
slots_created: false,
slot_count: 0,
};
assert!(!backend.slots_created, "fresh backend has no slots");
assert_eq!(backend.slot_count, 0, "fresh slot_count zero");
backend
}
pub fn width(&self) -> u32 {
assert!(self.width <= MAX_FRAME_WIDTH, "width within cap");
self.width
}
pub fn height(&self) -> u32 {
assert!(self.height <= MAX_FRAME_HEIGHT, "height within cap");
self.height
}
}
impl Default for CpuMemcpyBackend {
fn default() -> Self {
Self::new()
}
}
impl KeyedMutexBackend for CpuMemcpyBackend {
type SlotHandle = CpuSlotHandle;
const NUM_SLOTS: usize = NUM_SLOTS_DEFAULT;
fn create_slots(
&mut self,
width: u32,
height: u32,
format: TextureFormat,
) -> Result<Vec<CpuSlotHandle>, BackendError> {
if width == 0 || height == 0 || width > MAX_FRAME_WIDTH || height > MAX_FRAME_HEIGHT {
return Err(BackendError::DimensionsOutOfRange { width, height });
}
if !matches!(format, TextureFormat::Nv12) {
return Err(BackendError::UnsupportedFormat { format });
}
assert!(!self.slots_created, "slots created once");
let byte_size = nv12_byte_size(width, height);
assert!(byte_size > 0, "byte_size positive");
let mut out: Vec<CpuSlotHandle> = Vec::with_capacity(Self::NUM_SLOTS);
for idx in 0..Self::NUM_SLOTS {
let inner = CpuSlotInner {
slot_index: idx as u32,
byte_size,
buffer: vec![0u8; byte_size],
current_key: AtomicU64::new(0),
acquired: AtomicBool::new(false),
completed: AtomicBool::new(false),
};
out.push(CpuSlotHandle {
inner: Arc::new(inner),
});
}
self.width = width;
self.height = height;
self.format = format;
self.slots_created = true;
self.slot_count = Self::NUM_SLOTS as u32;
assert_eq!(out.len(), Self::NUM_SLOTS, "slot vector length");
assert!(self.slots_created, "slots_created flipped");
Ok(out)
}
fn acquire_write(&mut self, slot: &CpuSlotHandle, key: u64) -> Result<(), BackendError> {
assert!(self.slots_created, "slots must exist before acquire");
let current = slot.inner.current_key.load(Ordering::Acquire);
if current != key {
return Err(BackendError::KeyMismatch {
expected: key,
observed: current,
});
}
let was_acquired = slot.inner.acquired.swap(true, Ordering::AcqRel);
if was_acquired {
return Err(BackendError::AcquireWhileWriting {
slot_index: slot.inner.slot_index,
});
}
slot.inner.completed.store(false, Ordering::Release);
assert!(
slot.inner.acquired.load(Ordering::Acquire),
"acquired flag set"
);
assert!(
!slot.inner.completed.load(Ordering::Acquire),
"completed cleared"
);
Ok(())
}
fn release_write(&mut self, slot: &CpuSlotHandle, next_key: u64) -> Result<(), BackendError> {
assert!(self.slots_created, "slots must exist before release");
let was_acquired = slot.inner.acquired.swap(false, Ordering::AcqRel);
if !was_acquired {
return Err(BackendError::ReleaseWithoutAcquire {
slot_index: slot.inner.slot_index,
});
}
slot.inner.current_key.store(next_key, Ordering::Release);
slot.inner.completed.store(true, Ordering::Release);
assert!(
!slot.inner.acquired.load(Ordering::Acquire),
"acquired cleared"
);
assert!(
slot.inner.completed.load(Ordering::Acquire),
"completed flag set"
);
Ok(())
}
fn poll_complete(&mut self, slot: &CpuSlotHandle) -> bool {
let done = slot.inner.completed.load(Ordering::Acquire);
assert!(self.slots_created, "slots exist for poll");
assert!(
(slot.inner.slot_index as usize) < Self::NUM_SLOTS,
"slot index in range"
);
done
}
fn mark_consumed(&mut self, slot: &CpuSlotHandle) {
assert!(self.slots_created, "slots exist for mark_consumed");
assert!(
(slot.inner.slot_index as usize) < Self::NUM_SLOTS,
"slot index in range"
);
slot.inner.completed.store(false, Ordering::Release);
}
fn fill_test_pattern(&mut self, slot: &CpuSlotHandle, value: u8) {
assert!(self.slots_created, "slots exist for fill");
assert!(
slot.inner.acquired.load(Ordering::Acquire),
"fill only while acquired"
);
let len = slot.inner.byte_size;
for offset in 0..len {
slot.write_byte(offset, value);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_slots_for_1080p_nv12_yields_eight_slots() {
let mut backend = CpuMemcpyBackend::new();
let slots = backend
.create_slots(1920, 1080, TextureFormat::Nv12)
.expect("creation succeeds");
assert_eq!(slots.len(), CpuMemcpyBackend::NUM_SLOTS);
assert_eq!(slots.len(), 8);
for (idx, slot) in slots.iter().enumerate() {
assert_eq!(slot.slot_index(), idx as u32);
assert_eq!(slot.buffer_len(), 1920 * 1080 * 3 / 2);
}
}
#[test]
fn create_slots_rejects_zero_dims() {
let mut backend = CpuMemcpyBackend::new();
let err = backend.create_slots(0, 1080, TextureFormat::Nv12).err();
assert!(matches!(
err,
Some(BackendError::DimensionsOutOfRange { .. })
));
}
#[test]
fn create_slots_rejects_unsupported_format() {
let mut backend = CpuMemcpyBackend::new();
let err = backend.create_slots(1920, 1080, TextureFormat::P010).err();
assert!(matches!(err, Some(BackendError::UnsupportedFormat { .. })));
}
#[test]
fn acquire_release_round_trip_marks_complete() {
let mut backend = CpuMemcpyBackend::new();
let slots = backend
.create_slots(64, 64, TextureFormat::Nv12)
.expect("create");
let slot = slots[0].clone();
backend.acquire_write(&slot, 0).expect("acquire");
backend.release_write(&slot, 1).expect("release");
assert!(backend.poll_complete(&slot));
backend.mark_consumed(&slot);
assert!(!backend.poll_complete(&slot));
}
#[test]
fn double_acquire_rejects() {
let mut backend = CpuMemcpyBackend::new();
let slots = backend
.create_slots(64, 64, TextureFormat::Nv12)
.expect("create");
let slot = slots[0].clone();
backend.acquire_write(&slot, 0).expect("first acquire");
let err = backend.acquire_write(&slot, 0).err();
assert!(matches!(
err,
Some(BackendError::AcquireWhileWriting { .. })
));
}
}
@@ -0,0 +1,473 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::backend::{BackendError, KeyedMutexBackend, TextureFormat};
#[derive(Clone)]
pub struct D3D11SharedHandle {
pub raw_handle: u64,
pub slot_index: u32,
pub width: u32,
pub height: u32,
}
#[cfg(target_os = "windows")]
pub struct D3D11KeyedMutexBackend {
device: Option<windows::Win32::Graphics::Direct3D11::ID3D11Device>,
#[allow(dead_code, reason = "held for RAII: device context lifetime")]
context: Option<windows::Win32::Graphics::Direct3D11::ID3D11DeviceContext>,
slots: Vec<D3D11SlotState>,
width: u32,
height: u32,
format: TextureFormat,
}
#[cfg(target_os = "windows")]
struct D3D11SlotState {
#[allow(
dead_code,
reason = "held for RAII: texture lifetime tied to keyed mutex"
)]
texture: windows::Win32::Graphics::Direct3D11::ID3D11Texture2D,
keyed_mutex: windows::Win32::Graphics::Dxgi::IDXGIKeyedMutex,
#[allow(dead_code, reason = "exposed via D3D11SharedHandle to clients")]
shared_handle: u64,
slot_index: u32,
expected_key: u64,
acquired: bool,
completed: bool,
}
#[cfg(target_os = "windows")]
impl D3D11KeyedMutexBackend {
pub fn new() -> Result<Self, BackendError> {
let (device, context) = unsafe { create_d3d11_device_windows()? };
let backend = Self {
device: Some(device),
context: Some(context),
slots: Vec::with_capacity(<Self as KeyedMutexBackend>::NUM_SLOTS),
width: 0,
height: 0,
format: TextureFormat::Nv12,
};
assert!(backend.slots.is_empty(), "fresh backend has no slots");
assert!(backend.device.is_some(), "device created");
Ok(backend)
}
fn find_slot(&mut self, slot: &D3D11SharedHandle) -> Option<usize> {
assert!((slot.slot_index as usize) < <Self as KeyedMutexBackend>::NUM_SLOTS);
self.slots
.iter()
.position(|s| s.slot_index == slot.slot_index)
}
pub fn texture_for_slot(
&self,
slot_index: u32,
) -> Option<windows::Win32::Graphics::Direct3D11::ID3D11Texture2D> {
assert!(
slot_index < (<Self as KeyedMutexBackend>::NUM_SLOTS as u32),
"slot_index in range"
);
assert!(!self.slots.is_empty(), "slots have been created");
for state in self.slots.iter() {
if state.slot_index == slot_index {
return Some(state.texture.clone());
}
}
None
}
pub fn device(&self) -> Option<windows::Win32::Graphics::Direct3D11::ID3D11Device> {
let dev = self.device.clone();
assert!(dev.is_some(), "device exists");
assert!(
!self.slots.is_empty() || self.width == 0,
"post-init invariant"
);
dev
}
pub fn context(&self) -> Option<windows::Win32::Graphics::Direct3D11::ID3D11DeviceContext> {
let ctx = self.context.clone();
assert!(ctx.is_some(), "context exists");
assert!(
self.width > 0 || self.slots.is_empty(),
"post-init invariant"
);
ctx
}
}
#[cfg(target_os = "windows")]
unsafe fn create_d3d11_device_windows() -> Result<
(
windows::Win32::Graphics::Direct3D11::ID3D11Device,
windows::Win32::Graphics::Direct3D11::ID3D11DeviceContext,
),
BackendError,
> {
use windows::Win32::Foundation::HMODULE;
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0};
use windows::Win32::Graphics::Direct3D11::{
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION, D3D11CreateDevice, ID3D11Device,
ID3D11DeviceContext,
};
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
let feature_levels = [D3D_FEATURE_LEVEL_11_0];
unsafe {
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
Some(&feature_levels),
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)
}
.map_err(|_| BackendError::PlatformUnsupported {
reason: "D3D11CreateDevice failed",
})?;
let device = device.ok_or(BackendError::PlatformUnsupported {
reason: "D3D11 device null",
})?;
let context = context.ok_or(BackendError::PlatformUnsupported {
reason: "D3D11 context null",
})?;
Ok((device, context))
}
#[cfg(target_os = "windows")]
unsafe fn create_keyed_mutex_texture_windows(
device: &windows::Win32::Graphics::Direct3D11::ID3D11Device,
width: u32,
height: u32,
slot_index: u32,
) -> Result<
(
windows::Win32::Graphics::Direct3D11::ID3D11Texture2D,
windows::Win32::Graphics::Dxgi::IDXGIKeyedMutex,
u64,
),
BackendError,
> {
use windows::Win32::Graphics::Direct3D11::{
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE,
D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX, D3D11_RESOURCE_MISC_SHARED_NTHANDLE,
D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC};
use windows::Win32::Graphics::Dxgi::{IDXGIKeyedMutex, IDXGIResource1};
use windows::core::Interface;
assert!(width > 0);
assert!(height > 0);
assert!(slot_index < 8);
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_KEYEDMUTEX.0 | D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0)
as u32,
};
let mut texture: Option<ID3D11Texture2D> = None;
unsafe { device.CreateTexture2D(&desc, None, Some(&mut texture)) }.map_err(|_| {
BackendError::PlatformUnsupported {
reason: "CreateTexture2D failed",
}
})?;
let texture = texture.ok_or(BackendError::PlatformUnsupported {
reason: "texture null",
})?;
let keyed_mutex: IDXGIKeyedMutex =
texture
.cast()
.map_err(|_| BackendError::PlatformUnsupported {
reason: "IDXGIKeyedMutex cast failed",
})?;
let resource1: IDXGIResource1 =
texture
.cast()
.map_err(|_| BackendError::PlatformUnsupported {
reason: "IDXGIResource1 cast failed",
})?;
let access_rw: u32 = windows::Win32::Graphics::Dxgi::DXGI_SHARED_RESOURCE_READ.0
| windows::Win32::Graphics::Dxgi::DXGI_SHARED_RESOURCE_WRITE.0;
let shared =
unsafe { resource1.CreateSharedHandle(None, access_rw, windows::core::PCWSTR::null()) }
.map_err(|_| BackendError::PlatformUnsupported {
reason: "CreateSharedHandle failed",
})?;
Ok((texture, keyed_mutex, shared.0 as u64))
}
#[cfg(target_os = "windows")]
const ACQUIRE_SYNC_WAIT_TIMEOUT: i32 = 0x102;
#[cfg(target_os = "windows")]
const ACQUIRE_SYNC_WAIT_ABANDONED: i32 = 0x80;
#[cfg(not(target_os = "windows"))]
pub struct D3D11KeyedMutexBackend;
#[cfg(not(target_os = "windows"))]
impl D3D11KeyedMutexBackend {
pub fn new() -> Result<Self, BackendError> {
Err(BackendError::PlatformUnsupported {
reason: "D3D11 keyed-mutex backend is only available on Windows",
})
}
}
impl KeyedMutexBackend for D3D11KeyedMutexBackend {
type SlotHandle = D3D11SharedHandle;
const NUM_SLOTS: usize = 8;
#[cfg(target_os = "windows")]
fn create_slots(
&mut self,
width: u32,
height: u32,
format: TextureFormat,
) -> Result<Vec<D3D11SharedHandle>, BackendError> {
assert!(self.slots.is_empty(), "slots created once");
if width == 0 || height == 0 {
return Err(BackendError::DimensionsOutOfRange { width, height });
}
if !matches!(format, TextureFormat::Nv12) {
return Err(BackendError::UnsupportedFormat { format });
}
let device = self
.device
.as_ref()
.ok_or(BackendError::PlatformUnsupported {
reason: "device dropped",
})?;
let mut out: Vec<D3D11SharedHandle> = Vec::with_capacity(Self::NUM_SLOTS);
for idx in 0..Self::NUM_SLOTS {
let idx_u32 = idx as u32;
let (texture, keyed_mutex, shared_handle) =
unsafe { create_keyed_mutex_texture_windows(device, width, height, idx_u32)? };
self.slots.push(D3D11SlotState {
texture,
keyed_mutex,
shared_handle,
slot_index: idx_u32,
expected_key: 0,
acquired: false,
completed: false,
});
out.push(D3D11SharedHandle {
raw_handle: shared_handle,
slot_index: idx_u32,
width,
height,
});
}
self.width = width;
self.height = height;
self.format = format;
assert_eq!(out.len(), Self::NUM_SLOTS);
assert_eq!(self.slots.len(), Self::NUM_SLOTS);
Ok(out)
}
#[cfg(not(target_os = "windows"))]
fn create_slots(
&mut self,
_width: u32,
_height: u32,
_format: TextureFormat,
) -> Result<Vec<D3D11SharedHandle>, BackendError> {
Err(BackendError::PlatformUnsupported {
reason: "real D3D11 create_slots requires Windows",
})
}
#[cfg(target_os = "windows")]
fn acquire_write(&mut self, slot: &D3D11SharedHandle, key: u64) -> Result<(), BackendError> {
use windows::core::Interface;
let position = self
.find_slot(slot)
.ok_or(BackendError::PlatformUnsupported {
reason: "slot not found",
})?;
let state = &mut self.slots[position];
if state.expected_key != key {
return Err(BackendError::KeyMismatch {
expected: state.expected_key,
observed: key,
});
}
if state.acquired {
return Err(BackendError::AcquireWhileWriting {
slot_index: state.slot_index,
});
}
let hr = unsafe {
(Interface::vtable(&state.keyed_mutex).AcquireSync)(
Interface::as_raw(&state.keyed_mutex),
key,
0,
)
};
if hr.0 == ACQUIRE_SYNC_WAIT_TIMEOUT || hr.0 == ACQUIRE_SYNC_WAIT_ABANDONED {
return Err(BackendError::WouldBlock {
slot_index: state.slot_index,
});
}
if hr.is_err() {
return Err(BackendError::KeyMismatch {
expected: key,
observed: u64::MAX,
});
}
state.acquired = true;
state.completed = false;
assert!(state.acquired);
Ok(())
}
#[cfg(not(target_os = "windows"))]
fn acquire_write(&mut self, _slot: &D3D11SharedHandle, _key: u64) -> Result<(), BackendError> {
Err(BackendError::PlatformUnsupported {
reason: "real D3D11 acquire_write requires Windows",
})
}
#[cfg(target_os = "windows")]
fn release_write(
&mut self,
slot: &D3D11SharedHandle,
next_key: u64,
) -> Result<(), BackendError> {
let position = self
.find_slot(slot)
.ok_or(BackendError::PlatformUnsupported {
reason: "slot not found",
})?;
let state = &mut self.slots[position];
if !state.acquired {
return Err(BackendError::ReleaseWithoutAcquire {
slot_index: state.slot_index,
});
}
unsafe { state.keyed_mutex.ReleaseSync(next_key) }.map_err(|_| {
BackendError::KeyMismatch {
expected: next_key,
observed: u64::MAX,
}
})?;
state.acquired = false;
state.expected_key = next_key;
state.completed = true;
assert!(!state.acquired);
assert!(state.completed);
Ok(())
}
#[cfg(not(target_os = "windows"))]
fn release_write(
&mut self,
_slot: &D3D11SharedHandle,
_next_key: u64,
) -> Result<(), BackendError> {
Err(BackendError::PlatformUnsupported {
reason: "real D3D11 release_write requires Windows",
})
}
#[cfg(target_os = "windows")]
fn poll_complete(&mut self, slot: &D3D11SharedHandle) -> bool {
match self.find_slot(slot) {
Some(position) => self.slots[position].completed,
None => false,
}
}
#[cfg(not(target_os = "windows"))]
fn poll_complete(&mut self, _slot: &D3D11SharedHandle) -> bool {
false
}
#[cfg(target_os = "windows")]
fn mark_consumed(&mut self, slot: &D3D11SharedHandle) {
if let Some(position) = self.find_slot(slot) {
self.slots[position].completed = false;
}
}
#[cfg(not(target_os = "windows"))]
fn mark_consumed(&mut self, _slot: &D3D11SharedHandle) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(not(target_os = "windows"))]
fn instance_creation_fails_on_non_windows() {
let err = D3D11KeyedMutexBackend::new().err();
assert!(matches!(
err,
Some(BackendError::PlatformUnsupported { .. })
));
}
#[test]
fn handle_clones_preserve_index() {
let h = D3D11SharedHandle {
raw_handle: 0xdead,
slot_index: 3,
width: 1920,
height: 1080,
};
let h2 = h.clone();
assert_eq!(h2.slot_index, 3);
assert_eq!(h2.raw_handle, 0xdead);
}
#[test]
#[cfg(target_os = "windows")]
fn windows_real_keyed_mutex_eight_slots_round_trip() {
let backend_result = D3D11KeyedMutexBackend::new();
let mut backend = match backend_result {
Ok(b) => b,
Err(BackendError::PlatformUnsupported { .. }) => return,
Err(other) => unreachable!("unexpected backend init err: {other:?}"),
};
let slots_result = backend.create_slots(64, 64, TextureFormat::Nv12);
let slots = match slots_result {
Ok(s) => s,
Err(BackendError::PlatformUnsupported { .. }) => return,
Err(other) => unreachable!("unexpected create_slots err: {other:?}"),
};
assert_eq!(slots.len(), 8);
for (idx, slot) in slots.iter().enumerate() {
assert_eq!(slot.slot_index, idx as u32);
assert_ne!(slot.raw_handle, 0);
assert_eq!(slot.width, 64);
}
let slot = slots[0].clone();
backend.acquire_write(&slot, 0).expect("acquire key=0");
backend.release_write(&slot, 1).expect("release key=1");
assert!(backend.poll_complete(&slot));
backend.mark_consumed(&slot);
assert!(!backend.poll_complete(&slot));
backend.acquire_write(&slot, 1).expect("acquire key=1");
backend.release_write(&slot, 2).expect("release key=2");
assert!(backend.poll_complete(&slot));
}
}
@@ -0,0 +1,819 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::ring::RingError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EncoderDims {
pub width: u32,
pub height: u32,
}
impl EncoderDims {
pub fn new(width: u32, height: u32) -> Self {
assert!(width > 0, "width must be positive");
assert!(height > 0, "height must be positive");
Self { width, height }
}
}
pub const ENCODER_FRAME_RATE_MIN: u32 = 1;
pub const ENCODER_FRAME_RATE_MAX: u32 = 240;
pub const ENCODER_FRAME_RATE_DEFAULT: u32 = 60;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EncoderFrameRate {
pub numerator: u32,
pub denominator: u32,
}
impl EncoderFrameRate {
pub fn new(numerator: u32, denominator: u32) -> Self {
assert!(
numerator >= ENCODER_FRAME_RATE_MIN,
"fps numerator positive"
);
assert!(denominator > 0, "fps denominator positive");
let frame_rate = Self {
numerator: numerator.min(ENCODER_FRAME_RATE_MAX),
denominator,
};
assert!(frame_rate.numerator >= ENCODER_FRAME_RATE_MIN);
assert!(frame_rate.denominator > 0);
frame_rate
}
pub fn from_fps(fps: u32) -> Self {
let numerator = fps.clamp(ENCODER_FRAME_RATE_MIN, ENCODER_FRAME_RATE_MAX);
Self::new(numerator, 1)
}
pub fn frame_interval_us(self) -> u64 {
let numerator = u64::from(self.numerator);
let denominator = u64::from(self.denominator);
assert!(numerator > 0, "fps numerator positive");
assert!(denominator > 0, "fps denominator positive");
((1_000_000u64 * denominator) + numerator - 1) / numerator
}
pub fn gop_pic_size(self) -> u16 {
let rounded = (u64::from(self.numerator) + u64::from(self.denominator) - 1)
/ u64::from(self.denominator);
let bounded = rounded.clamp(1, u64::from(ENCODER_FRAME_RATE_MAX));
bounded as u16
}
}
impl Default for EncoderFrameRate {
fn default() -> Self {
Self::from_fps(ENCODER_FRAME_RATE_DEFAULT)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EncoderSubmission {
pub shared_handle: u64,
pub keyed_mutex_key: u64,
pub dims: EncoderDims,
pub sequence: u64,
pub capture_pts_us: Option<u64>,
}
impl EncoderSubmission {
pub fn new(shared_handle: u64, keyed_mutex_key: u64, dims: EncoderDims, sequence: u64) -> Self {
assert!(shared_handle != 0, "shared handle must be non-zero");
assert!(dims.width > 0, "dims width positive");
let s = Self {
shared_handle,
keyed_mutex_key,
dims,
sequence,
capture_pts_us: None,
};
assert!(s.shared_handle == shared_handle, "post construct intact");
assert!(s.capture_pts_us.is_none(), "capture pts defaults absent");
s
}
pub fn with_capture_pts_us(mut self, capture_pts_us: u64) -> Self {
assert!(self.shared_handle != 0, "shared handle must be non-zero");
self.capture_pts_us = Some(capture_pts_us);
assert_eq!(
self.capture_pts_us,
Some(capture_pts_us),
"capture pts recorded"
);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PicParams {
pub pts_us: u64,
pub force_keyframe: bool,
}
impl PicParams {
pub fn new(pts_us: u64, force_keyframe: bool) -> Self {
let p = Self {
pts_us,
force_keyframe,
};
assert!(p.pts_us == pts_us, "pts_us intact");
assert!(p.force_keyframe == force_keyframe, "force_keyframe intact");
p
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct HandoffSlot {
pub slot_index: u32,
pub shared_handle: u64,
}
impl HandoffSlot {
pub fn new(slot_index: u32, shared_handle: u64) -> Self {
assert!(slot_index < 64, "slot_index within plausible bound");
assert!(shared_handle != 0, "shared_handle non-zero");
Self {
slot_index,
shared_handle,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncodedBitstream {
pub data: Vec<u8>,
pub pts_us: u64,
pub dts_us: u64,
pub is_keyframe: bool,
}
impl EncodedBitstream {
pub fn new(data: Vec<u8>, pts_us: u64, dts_us: u64, is_keyframe: bool) -> Self {
assert!(!data.is_empty(), "encoded bitstream must be non-empty");
assert!(data.len() <= MAX_BITSTREAM_BYTES, "bitstream within cap");
Self {
data,
pts_us,
dts_us,
is_keyframe,
}
}
}
pub const MAX_BITSTREAM_BYTES: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EncoderError {
SdkNotFound {
vendor: &'static str,
dll: &'static str,
},
SymbolMissing {
vendor: &'static str,
symbol: &'static str,
},
SessionInitFailed {
vendor: &'static str,
status: i64,
},
RegisterFailed {
vendor: &'static str,
status: i64,
},
EncodeFailed {
vendor: &'static str,
status: i64,
},
BitstreamReadFailed {
vendor: &'static str,
status: i64,
},
SlotUnknown {
slot_index: u32,
},
KeyMismatch {
expected: u64,
observed: u64,
},
DimensionsOutOfRange {
width: u32,
height: u32,
},
PlatformUnsupported {
reason: &'static str,
},
BitstreamTooLarge {
byte_size: usize,
},
NotImplemented {
what: &'static str,
},
}
impl std::fmt::Display for EncoderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SdkNotFound { vendor, dll } => {
write!(f, "{vendor} SDK runtime '{dll}' not found")
}
Self::SymbolMissing { vendor, symbol } => {
write!(f, "{vendor} symbol '{symbol}' missing from runtime")
}
Self::SessionInitFailed { vendor, status } => {
write!(f, "{vendor} session init failed: status={status}")
}
Self::RegisterFailed { vendor, status } => {
write!(f, "{vendor} register failed: status={status}")
}
Self::EncodeFailed { vendor, status } => {
write!(f, "{vendor} encode failed: status={status}")
}
Self::BitstreamReadFailed { vendor, status } => {
write!(f, "{vendor} bitstream read failed: status={status}")
}
Self::SlotUnknown { slot_index } => write!(f, "slot {slot_index} unknown"),
Self::KeyMismatch { expected, observed } => {
write!(
f,
"keyed-mutex key mismatch: expected={expected} observed={observed}"
)
}
Self::DimensionsOutOfRange { width, height } => {
write!(f, "encoder dims out of range: {width}x{height}")
}
Self::PlatformUnsupported { reason } => write!(f, "platform unsupported: {reason}"),
Self::BitstreamTooLarge { byte_size } => {
write!(f, "bitstream {byte_size} bytes exceeds cap")
}
Self::NotImplemented { what } => write!(f, "not implemented: {what}"),
}
}
}
impl std::error::Error for EncoderError {}
pub trait EncoderCompletionCallback: Send {
fn on_complete(&mut self, sequence: u64, encoded_bytes: u32);
}
pub trait NvencHandoff: Send {
fn register_slot(
&mut self,
shared_handle: u64,
key: u64,
dims: EncoderDims,
) -> Result<HandoffSlot, EncoderError>;
fn encode_shared_async(
&mut self,
slot: HandoffSlot,
key: u64,
dims: EncoderDims,
pic_params: PicParams,
) -> Result<(), EncoderError>;
fn poll_completed(&mut self, slot: HandoffSlot) -> Option<EncodedBitstream>;
fn unregister_slot(&mut self, slot: HandoffSlot);
fn encode_shared(
&mut self,
submission: EncoderSubmission,
callback: &mut dyn EncoderCompletionCallback,
) -> Result<(), RingError>;
}
pub trait AmfHandoff: Send {
fn register_slot(
&mut self,
shared_handle: u64,
key: u64,
dims: EncoderDims,
) -> Result<HandoffSlot, EncoderError>;
fn encode_shared_async(
&mut self,
slot: HandoffSlot,
key: u64,
dims: EncoderDims,
pic_params: PicParams,
) -> Result<(), EncoderError>;
fn poll_completed(&mut self, slot: HandoffSlot) -> Option<EncodedBitstream>;
fn unregister_slot(&mut self, slot: HandoffSlot);
fn encode_shared(
&mut self,
submission: EncoderSubmission,
callback: &mut dyn EncoderCompletionCallback,
) -> Result<(), RingError>;
}
pub trait QsvHandoff: Send {
fn register_slot(
&mut self,
shared_handle: u64,
key: u64,
dims: EncoderDims,
) -> Result<HandoffSlot, EncoderError>;
fn encode_shared_async(
&mut self,
slot: HandoffSlot,
key: u64,
dims: EncoderDims,
pic_params: PicParams,
) -> Result<(), EncoderError>;
fn poll_completed(&mut self, slot: HandoffSlot) -> Option<EncodedBitstream>;
fn unregister_slot(&mut self, slot: HandoffSlot);
fn encode_shared(
&mut self,
submission: EncoderSubmission,
callback: &mut dyn EncoderCompletionCallback,
) -> Result<(), RingError>;
}
pub trait VideoToolboxHandoff: Send {
fn register_slot(
&mut self,
iosurface_handle: u64,
key: u64,
dims: EncoderDims,
) -> Result<HandoffSlot, EncoderError>;
fn encode_shared_async(
&mut self,
slot: HandoffSlot,
key: u64,
dims: EncoderDims,
pic_params: PicParams,
) -> Result<(), EncoderError>;
fn poll_completed(&mut self, slot: HandoffSlot) -> Option<EncodedBitstream>;
fn unregister_slot(&mut self, slot: HandoffSlot);
fn encode_shared(
&mut self,
submission: EncoderSubmission,
callback: &mut dyn EncoderCompletionCallback,
) -> Result<(), RingError>;
}
pub struct VtNoOpHandoff {
accepted: u64,
next_slot_index: u32,
pending: std::collections::VecDeque<(u64, u64)>,
}
impl VtNoOpHandoff {
pub fn new() -> Self {
let h = Self {
accepted: 0,
next_slot_index: 0,
pending: std::collections::VecDeque::with_capacity(16),
};
assert_eq!(h.accepted, 0, "fresh handoff has no accepted frames");
assert_eq!(h.next_slot_index, 0, "fresh handoff slot index zero");
h
}
pub fn accepted_count(&self) -> u64 {
let n = self.accepted;
assert!(
self.pending.len() <= u32::MAX as usize,
"pending queue plausible"
);
assert!(n >= self.pending.len() as u64, "accepted >= pending");
n
}
pub fn pending_len(&self) -> usize {
let len = self.pending.len();
assert!(
len <= self.pending.capacity().max(1),
"pending within capacity bound"
);
assert!(len as u64 <= self.accepted, "pending <= accepted");
len
}
}
impl Default for VtNoOpHandoff {
fn default() -> Self {
Self::new()
}
}
impl VideoToolboxHandoff for VtNoOpHandoff {
fn register_slot(
&mut self,
iosurface_handle: u64,
_key: u64,
dims: EncoderDims,
) -> Result<HandoffSlot, EncoderError> {
if iosurface_handle == 0 {
return Err(EncoderError::SlotUnknown {
slot_index: u32::MAX,
});
}
if dims.width == 0 || dims.height == 0 {
return Err(EncoderError::DimensionsOutOfRange {
width: dims.width,
height: dims.height,
});
}
let slot = HandoffSlot::new(self.next_slot_index, iosurface_handle);
self.next_slot_index = self.next_slot_index.saturating_add(1);
assert!(
slot.shared_handle == iosurface_handle,
"slot handle round-trip"
);
assert!(self.next_slot_index > 0, "slot counter advanced");
Ok(slot)
}
fn encode_shared_async(
&mut self,
slot: HandoffSlot,
_key: u64,
dims: EncoderDims,
pic_params: PicParams,
) -> Result<(), EncoderError> {
if dims.width == 0 || dims.height == 0 {
return Err(EncoderError::DimensionsOutOfRange {
width: dims.width,
height: dims.height,
});
}
self.pending.push_back((self.accepted, pic_params.pts_us));
self.accepted = self.accepted.saturating_add(1);
assert!(slot.shared_handle != 0, "async slot handle non-zero");
assert!(self.accepted > 0, "encode_shared_async advanced accepted");
Ok(())
}
fn poll_completed(&mut self, _slot: HandoffSlot) -> Option<EncodedBitstream> {
None
}
fn unregister_slot(&mut self, slot: HandoffSlot) {
assert!(slot.shared_handle != 0, "unregister slot handle non-zero");
}
fn encode_shared(
&mut self,
submission: EncoderSubmission,
callback: &mut dyn EncoderCompletionCallback,
) -> Result<(), RingError> {
if submission.shared_handle == 0 {
return Err(RingError::UnknownSlot);
}
if submission.dims.width == 0 || submission.dims.height == 0 {
return Err(RingError::BackendFailed {
source: crate::backend::BackendError::DimensionsOutOfRange {
width: submission.dims.width,
height: submission.dims.height,
},
});
}
let pre_accepted = self.accepted;
self.pending.push_back((submission.sequence, 0));
self.accepted = self.accepted.saturating_add(1);
callback.on_complete(submission.sequence, 0);
assert!(
self.accepted == pre_accepted + 1,
"VtNoOpHandoff accepted advanced"
);
assert!(submission.dims.width > 0, "submission dims preserved");
Ok(())
}
}
pub struct NotImplementedHandoff {
pub vendor: &'static str,
}
impl NotImplementedHandoff {
pub fn nvenc() -> Self {
Self { vendor: "nvenc" }
}
pub fn amf() -> Self {
Self { vendor: "amf" }
}
pub fn qsv() -> Self {
Self { vendor: "qsv" }
}
}
pub fn compute_dts_offset_us(first_pts_us: u64, num_b_frames: u32, frame_interval_us: u64) -> i64 {
assert!(frame_interval_us > 0, "frame interval positive");
assert!(num_b_frames <= 8, "B-frame count plausible");
let offset = (num_b_frames as u64).saturating_mul(frame_interval_us);
let result = -(offset as i64);
let _ = first_pts_us;
assert!(result <= 0, "DTS offset is non-positive for B-frames");
result
}
pub fn apply_dts_offset(pts_us: u64, offset_us: i64) -> u64 {
let signed_pts = pts_us as i64;
let dts = signed_pts.saturating_add(offset_us);
let clamped = if dts < 0 { 0 } else { dts as u64 };
assert!(
clamped <= pts_us || offset_us > 0,
"DTS <= PTS without future B-frames"
);
clamped
}
macro_rules! impl_not_implemented_for_trait {
($trait_name:ident, $vendor_tag:expr) => {
impl $trait_name for NotImplementedHandoff {
fn register_slot(
&mut self,
shared_handle: u64,
_key: u64,
dims: EncoderDims,
) -> Result<HandoffSlot, EncoderError> {
assert!(shared_handle != 0, "shared_handle non-zero");
assert!(dims.width > 0, "dims width positive");
Err(EncoderError::NotImplemented {
what: concat!($vendor_tag, "::register_slot"),
})
}
fn encode_shared_async(
&mut self,
slot: HandoffSlot,
_key: u64,
dims: EncoderDims,
_pic_params: PicParams,
) -> Result<(), EncoderError> {
assert!(slot.shared_handle != 0, "slot shared_handle non-zero");
assert!(dims.width > 0, "dims width positive");
Err(EncoderError::NotImplemented {
what: concat!($vendor_tag, "::encode_shared_async"),
})
}
fn poll_completed(&mut self, _slot: HandoffSlot) -> Option<EncodedBitstream> {
None
}
fn unregister_slot(&mut self, slot: HandoffSlot) {
assert!(slot.shared_handle != 0, "slot shared_handle non-zero");
}
fn encode_shared(
&mut self,
submission: EncoderSubmission,
_callback: &mut dyn EncoderCompletionCallback,
) -> Result<(), RingError> {
assert!(submission.shared_handle != 0, "submission handle non-zero");
assert!(submission.dims.width > 0, "submission width positive");
Err(RingError::NotImplemented {
what: concat!($vendor_tag, "::encode_shared"),
})
}
}
};
}
impl_not_implemented_for_trait!(NvencHandoff, "NvencHandoff");
impl_not_implemented_for_trait!(AmfHandoff, "AmfHandoff");
impl_not_implemented_for_trait!(QsvHandoff, "QsvHandoff");
#[cfg(test)]
mod tests {
use super::*;
struct NoopCallback;
impl EncoderCompletionCallback for NoopCallback {
fn on_complete(&mut self, _sequence: u64, _encoded_bytes: u32) {}
}
fn submission() -> EncoderSubmission {
EncoderSubmission::new(0xfeed_face, 7, EncoderDims::new(1920, 1080), 42)
}
#[test]
fn nvenc_stub_returns_not_implemented() {
let mut h = NotImplementedHandoff::nvenc();
let mut cb = NoopCallback;
let err = NvencHandoff::encode_shared(&mut h, submission(), &mut cb).err();
assert!(matches!(err, Some(RingError::NotImplemented { what })
if what.contains("Nvenc")));
}
#[test]
fn amf_stub_returns_not_implemented() {
let mut h = NotImplementedHandoff::amf();
let mut cb = NoopCallback;
let err = AmfHandoff::encode_shared(&mut h, submission(), &mut cb).err();
assert!(matches!(err, Some(RingError::NotImplemented { what })
if what.contains("Amf")));
}
#[test]
fn qsv_stub_returns_not_implemented() {
let mut h = NotImplementedHandoff::qsv();
let mut cb = NoopCallback;
let err = QsvHandoff::encode_shared(&mut h, submission(), &mut cb).err();
assert!(matches!(err, Some(RingError::NotImplemented { what })
if what.contains("Qsv")));
}
#[test]
fn encoder_dims_rejects_zero_width_via_assert() {
let result = std::panic::catch_unwind(|| EncoderDims::new(0, 1080));
assert!(result.is_err());
}
#[test]
fn nvenc_stub_register_returns_not_implemented() {
let mut h = NotImplementedHandoff::nvenc();
let dims = EncoderDims::new(1920, 1080);
let err = NvencHandoff::register_slot(&mut h, 0xabc, 0, dims).err();
assert!(matches!(err, Some(EncoderError::NotImplemented { what })
if what.contains("Nvenc")));
}
#[test]
fn amf_stub_register_returns_not_implemented() {
let mut h = NotImplementedHandoff::amf();
let dims = EncoderDims::new(1920, 1080);
let err = AmfHandoff::register_slot(&mut h, 0xabc, 0, dims).err();
assert!(matches!(err, Some(EncoderError::NotImplemented { what })
if what.contains("Amf")));
}
#[test]
fn qsv_stub_register_returns_not_implemented() {
let mut h = NotImplementedHandoff::qsv();
let dims = EncoderDims::new(1920, 1080);
let err = QsvHandoff::register_slot(&mut h, 0xabc, 0, dims).err();
assert!(matches!(err, Some(EncoderError::NotImplemented { what })
if what.contains("Qsv")));
}
#[test]
fn stub_poll_completed_returns_none() {
let mut h_nv = NotImplementedHandoff::nvenc();
let mut h_amf = NotImplementedHandoff::amf();
let mut h_qsv = NotImplementedHandoff::qsv();
let slot = HandoffSlot::new(0, 0xdead);
assert!(NvencHandoff::poll_completed(&mut h_nv, slot).is_none());
assert!(AmfHandoff::poll_completed(&mut h_amf, slot).is_none());
assert!(QsvHandoff::poll_completed(&mut h_qsv, slot).is_none());
}
#[test]
fn stub_unregister_does_not_panic() {
let mut h = NotImplementedHandoff::nvenc();
let slot = HandoffSlot::new(0, 0xdead);
NvencHandoff::unregister_slot(&mut h, slot);
}
#[test]
fn encoded_bitstream_rejects_empty() {
let result = std::panic::catch_unwind(|| EncodedBitstream::new(vec![], 0, 0, true));
assert!(result.is_err());
}
#[test]
fn dts_offset_zero_for_no_b_frames() {
let offset = compute_dts_offset_us(1000, 0, 16_666);
assert_eq!(offset, 0);
}
#[test]
fn dts_offset_negative_for_b_frames() {
let offset = compute_dts_offset_us(1000, 2, 16_666);
assert_eq!(offset, -(2 * 16_666_i64));
}
#[test]
fn dts_offset_application_clamps_to_zero() {
let dts = apply_dts_offset(100, -1000);
assert_eq!(dts, 0);
}
#[test]
fn dts_offset_application_below_pts_for_b_frames() {
let pts = 100_000_u64;
let dts = apply_dts_offset(pts, -33_333);
assert!(dts < pts);
assert_eq!(dts, 66_667);
}
#[test]
fn encoder_frame_rate_derives_interval_and_gop() {
let sixty = EncoderFrameRate::from_fps(60);
assert_eq!(sixty.frame_interval_us(), 16_667);
assert_eq!(sixty.gop_pic_size(), 60);
let capped = EncoderFrameRate::from_fps(999);
assert_eq!(capped.numerator, ENCODER_FRAME_RATE_MAX);
assert_eq!(capped.gop_pic_size(), ENCODER_FRAME_RATE_MAX as u16);
}
#[test]
fn handoff_slot_rejects_zero_handle() {
let result = std::panic::catch_unwind(|| HandoffSlot::new(0, 0));
assert!(result.is_err());
}
#[test]
fn submission_capture_pts_defaults_absent_and_round_trips() {
let s = submission();
assert_eq!(s.capture_pts_us, None);
let with_pts = s.with_capture_pts_us(123_456);
assert_eq!(with_pts.capture_pts_us, Some(123_456));
assert_eq!(with_pts.sequence, s.sequence);
assert_eq!(with_pts.shared_handle, s.shared_handle);
}
struct CountingCallback {
seen: Vec<(u64, u32)>,
}
impl EncoderCompletionCallback for CountingCallback {
fn on_complete(&mut self, sequence: u64, encoded_bytes: u32) {
self.seen.push((sequence, encoded_bytes));
}
}
#[test]
fn vt_noop_accepts_frames_in_fifo_order() {
let mut h = VtNoOpHandoff::new();
let mut cb = CountingCallback { seen: Vec::new() };
let dims = EncoderDims::new(1920, 1080);
for seq in 1..=5u64 {
let s = EncoderSubmission::new(0xfeed_face_u64, 0, dims, seq);
VideoToolboxHandoff::encode_shared(&mut h, s, &mut cb).expect("vt encode_shared");
}
assert_eq!(cb.seen.len(), 5);
for (idx, &(seq, _)) in cb.seen.iter().enumerate() {
assert_eq!(seq, (idx as u64) + 1, "fifo sequence");
}
assert_eq!(h.accepted_count(), 5);
}
#[test]
fn vt_noop_register_returns_slot() {
let mut h = VtNoOpHandoff::new();
let dims = EncoderDims::new(1920, 1080);
let slot_a =
VideoToolboxHandoff::register_slot(&mut h, 0xabc, 0, dims).expect("register a");
let slot_b =
VideoToolboxHandoff::register_slot(&mut h, 0xdef, 0, dims).expect("register b");
assert_eq!(slot_a.shared_handle, 0xabc);
assert_eq!(slot_b.shared_handle, 0xdef);
assert_ne!(slot_a.slot_index, slot_b.slot_index);
}
#[test]
fn vt_noop_register_rejects_zero_handle() {
let mut h = VtNoOpHandoff::new();
let dims = EncoderDims::new(1920, 1080);
let err = VideoToolboxHandoff::register_slot(&mut h, 0, 0, dims).err();
assert!(matches!(err, Some(EncoderError::SlotUnknown { .. })));
}
#[test]
fn vt_noop_encode_shared_rejects_zero_handle() {
let mut h = VtNoOpHandoff::new();
let mut cb = CountingCallback { seen: Vec::new() };
let dims = EncoderDims::new(1920, 1080);
let s = EncoderSubmission {
shared_handle: 0,
keyed_mutex_key: 0,
dims,
sequence: 1,
capture_pts_us: None,
};
let err = VideoToolboxHandoff::encode_shared(&mut h, s, &mut cb).err();
assert!(matches!(err, Some(RingError::UnknownSlot)));
assert!(cb.seen.is_empty(), "no callback on rejection");
}
#[test]
fn vt_noop_encode_shared_async_advances_accepted() {
let mut h = VtNoOpHandoff::new();
let dims = EncoderDims::new(1280, 720);
let slot = HandoffSlot::new(0, 0xfeed);
let params = PicParams::new(16_666, false);
VideoToolboxHandoff::encode_shared_async(&mut h, slot, 0, dims, params)
.expect("async encode ok");
assert_eq!(h.accepted_count(), 1);
assert_eq!(h.pending_len(), 1);
}
}
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![deny(clippy::too_many_lines)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
#![deny(warnings)]
pub mod backend;
pub mod d3d11;
pub mod encoder_handoff;
pub mod metal_iosurface;
pub mod ring;
#[cfg(target_os = "macos")]
pub mod metal_iosurface_macos;
#[cfg(target_os = "macos")]
pub mod vt_compression_macos;
#[cfg(target_os = "windows")]
pub mod amf;
#[cfg(target_os = "windows")]
pub mod nvenc;
#[cfg(target_os = "windows")]
pub mod qsv;
pub use backend::{CpuMemcpyBackend, CpuSlotHandle, KeyedMutexBackend, TextureFormat};
pub use d3d11::D3D11KeyedMutexBackend;
pub use encoder_handoff::{
AmfHandoff, EncodedBitstream, EncoderDims, EncoderError, EncoderFrameRate, EncoderSubmission,
HandoffSlot, NotImplementedHandoff, NvencHandoff, PicParams, QsvHandoff, VideoToolboxHandoff,
VtNoOpHandoff, apply_dts_offset, compute_dts_offset_us,
};
pub use metal_iosurface::{
IoSurfaceSlotHandle, METAL_IOSURFACE_SEED_BASE, MetalSharedTextureBackend,
};
pub use ring::{
DUPLICATE_COUNT_MAX, EncoderInputRing, EncoderReady, FillReservation, RING_SIZE, RingError,
RingMetrics,
};
#[cfg(target_os = "macos")]
pub use vt_compression_macos::{VtCompressionHandoff, VtPixelTransfer};
#[cfg(target_os = "windows")]
pub use amf::AmfD3D11Handoff;
#[cfg(target_os = "windows")]
pub use nvenc::{COMPLETION_RING_CAPACITY as NVENC_COMPLETION_RING_CAPACITY, NvencD3D11Handoff};
#[cfg(target_os = "windows")]
pub use qsv::QsvD3D11Handoff;
pub const NV12_BPP_NUMERATOR: u32 = 3;
pub const NV12_BPP_DENOMINATOR: u32 = 2;
pub const MAX_FRAME_WIDTH: u32 = 7680;
pub const MAX_FRAME_HEIGHT: u32 = 4320;
#[inline]
pub const fn nv12_byte_size(width: u32, height: u32) -> usize {
let w = width as usize;
let h = height as usize;
(w * h * NV12_BPP_NUMERATOR as usize) / NV12_BPP_DENOMINATOR as usize
}
@@ -0,0 +1,447 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use crate::backend::{BackendError, KeyedMutexBackend, NUM_SLOTS_DEFAULT, TextureFormat};
use crate::{MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH};
pub const METAL_IOSURFACE_SEED_BASE: u64 = 0;
#[derive(Clone)]
pub struct IoSurfaceSlotHandle {
inner: Arc<IoSurfaceSlotInner>,
}
impl IoSurfaceSlotHandle {
pub fn slot_index(&self) -> u32 {
let idx = self.inner.slot_index;
assert!((idx as usize) < NUM_SLOTS_DEFAULT, "slot_index in range");
assert!(
self.inner.iosurface_handle != 0,
"iosurface_handle non-zero"
);
idx
}
pub fn iosurface_handle(&self) -> u64 {
let h = self.inner.iosurface_handle;
assert!(h != 0, "iosurface_handle non-zero on read");
assert!(
(self.inner.slot_index as usize) < NUM_SLOTS_DEFAULT,
"slot_index intact"
);
h
}
#[cfg(target_os = "macos")]
pub fn iosurface_ptr(&self) -> *mut core::ffi::c_void {
let p = self.inner.iosurface_ptr;
assert!(!p.is_null(), "iosurface_ptr non-null on read");
assert!(
self.inner.iosurface_handle != 0,
"handle non-zero alongside ptr"
);
p
}
pub fn current_key(&self) -> u64 {
let key = self.inner.current_key.load(Ordering::Acquire);
assert!(
self.inner.iosurface_handle != 0,
"handle intact during key read"
);
assert!(
(self.inner.slot_index as usize) < NUM_SLOTS_DEFAULT,
"slot_index intact"
);
key
}
pub fn is_acquired(&self) -> bool {
let a = self.inner.acquired.load(Ordering::Acquire);
assert!(
self.inner.iosurface_handle != 0,
"handle intact during acquired probe"
);
assert!(
(self.inner.slot_index as usize) < NUM_SLOTS_DEFAULT,
"slot_index intact"
);
a
}
}
struct IoSurfaceSlotInner {
slot_index: u32,
iosurface_handle: u64,
#[cfg(target_os = "macos")]
iosurface_ptr: *mut core::ffi::c_void,
current_key: AtomicU64,
acquired: AtomicBool,
completed: AtomicBool,
}
#[cfg(target_os = "macos")]
unsafe impl Send for IoSurfaceSlotInner {}
#[cfg(target_os = "macos")]
unsafe impl Sync for IoSurfaceSlotInner {}
pub struct MetalSharedTextureBackend {
width: u32,
height: u32,
format: TextureFormat,
slots_created: bool,
slot_handles: Vec<u64>,
#[cfg(target_os = "macos")]
iosurfaces: Vec<crate::metal_iosurface_macos::OwnedIoSurface>,
}
impl MetalSharedTextureBackend {
pub fn new() -> Self {
let backend = Self {
width: 0,
height: 0,
format: TextureFormat::Nv12,
slots_created: false,
slot_handles: Vec::with_capacity(NUM_SLOTS_DEFAULT),
#[cfg(target_os = "macos")]
iosurfaces: Vec::with_capacity(NUM_SLOTS_DEFAULT),
};
assert!(!backend.slots_created, "fresh backend has no slots");
assert!(
backend.slot_handles.is_empty(),
"fresh backend handles empty"
);
backend
}
pub fn width(&self) -> u32 {
assert!(self.width <= MAX_FRAME_WIDTH, "width within cap");
assert!(self.height <= MAX_FRAME_HEIGHT, "height within cap");
self.width
}
pub fn height(&self) -> u32 {
assert!(self.height <= MAX_FRAME_HEIGHT, "height within cap");
assert!(self.width <= MAX_FRAME_WIDTH, "width within cap");
self.height
}
pub fn slot_iosurface_handle(&self, slot_index: u32) -> Option<u64> {
let idx = slot_index as usize;
if idx >= self.slot_handles.len() {
return None;
}
let h = self.slot_handles[idx];
assert!(h != 0, "stored iosurface handle non-zero");
Some(h)
}
#[cfg(target_os = "macos")]
pub fn slot_iosurface_ptr(&self, slot_index: u32) -> Option<*mut core::ffi::c_void> {
let idx = slot_index as usize;
if idx >= self.iosurfaces.len() {
return None;
}
let p = self.iosurfaces[idx].as_ptr();
assert!(!p.is_null(), "stored iosurface ptr non-null");
assert!(
idx < self.slot_handles.len(),
"ptr slot mirrors handle slot"
);
Some(p)
}
#[cfg(target_os = "macos")]
pub fn slot_iosurface_mut(
&mut self,
slot_index: u32,
) -> Option<&mut crate::metal_iosurface_macos::OwnedIoSurface> {
let idx = slot_index as usize;
if idx >= self.iosurfaces.len() {
return None;
}
assert!(
idx < self.slot_handles.len(),
"mut slot mirrors handle slot"
);
Some(&mut self.iosurfaces[idx])
}
#[cfg(not(target_os = "macos"))]
fn allocate_slot_handles(
&mut self,
_width: u32,
_height: u32,
_format: TextureFormat,
) -> Result<Vec<u64>, BackendError> {
Err(BackendError::PlatformUnsupported {
reason: "MetalSharedTextureBackend requires macOS",
})
}
#[cfg(target_os = "macos")]
fn allocate_slot_handles(
&mut self,
width: u32,
height: u32,
format: TextureFormat,
) -> Result<Vec<u64>, BackendError> {
if !matches!(format, TextureFormat::Nv12) {
return Err(BackendError::UnsupportedFormat { format });
}
let mut handles: Vec<u64> = Vec::with_capacity(NUM_SLOTS_DEFAULT);
let mut owned: Vec<crate::metal_iosurface_macos::OwnedIoSurface> =
Vec::with_capacity(NUM_SLOTS_DEFAULT);
for slot in 0..NUM_SLOTS_DEFAULT {
let surface = crate::metal_iosurface_macos::OwnedIoSurface::create_nv12(width, height)
.map_err(|_| BackendError::PlatformUnsupported {
reason: "IOSurfaceCreate failed",
})?;
let raw = surface.handle();
assert!(raw != 0, "IOSurface raw non-zero for slot");
assert!(slot < NUM_SLOTS_DEFAULT, "slot index in range");
handles.push(raw);
owned.push(surface);
}
self.iosurfaces = owned;
Ok(handles)
}
}
impl Default for MetalSharedTextureBackend {
fn default() -> Self {
Self::new()
}
}
impl KeyedMutexBackend for MetalSharedTextureBackend {
type SlotHandle = IoSurfaceSlotHandle;
const NUM_SLOTS: usize = NUM_SLOTS_DEFAULT;
fn create_slots(
&mut self,
width: u32,
height: u32,
format: TextureFormat,
) -> Result<Vec<IoSurfaceSlotHandle>, BackendError> {
if width == 0 || height == 0 || width > MAX_FRAME_WIDTH || height > MAX_FRAME_HEIGHT {
return Err(BackendError::DimensionsOutOfRange { width, height });
}
if !matches!(format, TextureFormat::Nv12) {
return Err(BackendError::UnsupportedFormat { format });
}
assert!(!self.slots_created, "slots created once");
let raw_handles = self.allocate_slot_handles(width, height, format)?;
assert_eq!(
raw_handles.len(),
NUM_SLOTS_DEFAULT,
"allocator returns NUM_SLOTS handles"
);
let mut handles: Vec<IoSurfaceSlotHandle> = Vec::with_capacity(NUM_SLOTS_DEFAULT);
for (idx, raw) in raw_handles.iter().enumerate() {
assert!(*raw != 0, "raw iosurface handle non-zero");
#[cfg(target_os = "macos")]
let surface_ptr = self.iosurfaces[idx].as_ptr();
#[cfg(target_os = "macos")]
assert!(!surface_ptr.is_null(), "surface ptr non-null at slot setup");
let inner = IoSurfaceSlotInner {
slot_index: idx as u32,
iosurface_handle: *raw,
#[cfg(target_os = "macos")]
iosurface_ptr: surface_ptr,
current_key: AtomicU64::new(METAL_IOSURFACE_SEED_BASE),
acquired: AtomicBool::new(false),
completed: AtomicBool::new(false),
};
handles.push(IoSurfaceSlotHandle {
inner: Arc::new(inner),
});
}
self.width = width;
self.height = height;
self.format = format;
self.slot_handles = raw_handles;
self.slots_created = true;
assert_eq!(
handles.len(),
NUM_SLOTS_DEFAULT,
"returned handle vector length"
);
assert!(self.slots_created, "slots_created flipped");
Ok(handles)
}
fn acquire_write(&mut self, slot: &IoSurfaceSlotHandle, key: u64) -> Result<(), BackendError> {
assert!(self.slots_created, "slots must exist before acquire");
let current = slot.inner.current_key.load(Ordering::Acquire);
if current != key {
return Err(BackendError::KeyMismatch {
expected: key,
observed: current,
});
}
let was_acquired = slot.inner.acquired.swap(true, Ordering::AcqRel);
if was_acquired {
return Err(BackendError::AcquireWhileWriting {
slot_index: slot.inner.slot_index,
});
}
#[cfg(target_os = "macos")]
{
let idx = slot.inner.slot_index as usize;
if idx >= self.iosurfaces.len() {
slot.inner.acquired.store(false, Ordering::Release);
return Err(BackendError::PlatformUnsupported {
reason: "slot index out of range for IOSurface vector",
});
}
if let Err(e) = self.iosurfaces[idx].lock_for_writing() {
slot.inner.acquired.store(false, Ordering::Release);
return Err(e);
}
}
slot.inner.completed.store(false, Ordering::Release);
assert!(
slot.inner.acquired.load(Ordering::Acquire),
"acquired flag set"
);
assert!(
!slot.inner.completed.load(Ordering::Acquire),
"completed cleared"
);
Ok(())
}
fn release_write(
&mut self,
slot: &IoSurfaceSlotHandle,
next_key: u64,
) -> Result<(), BackendError> {
assert!(self.slots_created, "slots must exist before release");
let was_acquired = slot.inner.acquired.swap(false, Ordering::AcqRel);
if !was_acquired {
return Err(BackendError::ReleaseWithoutAcquire {
slot_index: slot.inner.slot_index,
});
}
#[cfg(target_os = "macos")]
{
let idx = slot.inner.slot_index as usize;
if idx >= self.iosurfaces.len() {
return Err(BackendError::PlatformUnsupported {
reason: "slot index out of range for IOSurface vector",
});
}
self.iosurfaces[idx].unlock_after_writing()?;
}
slot.inner.current_key.store(next_key, Ordering::Release);
slot.inner.completed.store(true, Ordering::Release);
assert!(
!slot.inner.acquired.load(Ordering::Acquire),
"acquired cleared"
);
assert!(
slot.inner.completed.load(Ordering::Acquire),
"completed flag set"
);
Ok(())
}
fn poll_complete(&mut self, slot: &IoSurfaceSlotHandle) -> bool {
let done = slot.inner.completed.load(Ordering::Acquire);
assert!(self.slots_created, "slots exist for poll");
assert!(
(slot.inner.slot_index as usize) < Self::NUM_SLOTS,
"slot index in range"
);
done
}
fn mark_consumed(&mut self, slot: &IoSurfaceSlotHandle) {
assert!(self.slots_created, "slots exist for mark_consumed");
assert!(
(slot.inner.slot_index as usize) < Self::NUM_SLOTS,
"slot index in range"
);
slot.inner.completed.store(false, Ordering::Release);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn off_macos_returns_platform_unsupported() {
let mut backend = MetalSharedTextureBackend::new();
let result = backend.create_slots(1920, 1080, TextureFormat::Nv12);
#[cfg(not(target_os = "macos"))]
{
assert!(matches!(
result,
Err(BackendError::PlatformUnsupported { .. })
));
}
#[cfg(target_os = "macos")]
{
let slots = result.expect("macos create_slots ok");
assert_eq!(slots.len(), 8);
for (idx, s) in slots.iter().enumerate() {
assert_eq!(s.slot_index(), idx as u32);
assert!(s.iosurface_handle() != 0);
}
}
}
#[test]
fn create_slots_rejects_zero_dims() {
let mut backend = MetalSharedTextureBackend::new();
let err = backend.create_slots(0, 1080, TextureFormat::Nv12).err();
assert!(matches!(
err,
Some(BackendError::DimensionsOutOfRange { .. })
));
}
#[test]
fn create_slots_rejects_unsupported_format() {
let mut backend = MetalSharedTextureBackend::new();
let err = backend.create_slots(1920, 1080, TextureFormat::P010).err();
assert!(matches!(err, Some(BackendError::UnsupportedFormat { .. })));
}
#[cfg(target_os = "macos")]
#[test]
fn macos_acquire_release_round_trip_marks_complete() {
let mut backend = MetalSharedTextureBackend::new();
let slots = backend
.create_slots(64, 64, TextureFormat::Nv12)
.expect("create");
let slot = slots[0].clone();
backend.acquire_write(&slot, 0).expect("acquire");
assert!(slot.is_acquired());
backend.release_write(&slot, 1).expect("release");
assert!(!slot.is_acquired());
assert!(backend.poll_complete(&slot));
backend.mark_consumed(&slot);
assert!(!backend.poll_complete(&slot));
}
#[cfg(target_os = "macos")]
#[test]
fn macos_double_acquire_rejects() {
let mut backend = MetalSharedTextureBackend::new();
let slots = backend
.create_slots(64, 64, TextureFormat::Nv12)
.expect("create");
let slot = slots[0].clone();
backend.acquire_write(&slot, 0).expect("first acquire");
let err = backend.acquire_write(&slot, 0).err();
assert!(matches!(
err,
Some(BackendError::AcquireWhileWriting { .. })
));
}
}
@@ -0,0 +1,187 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use core::ffi::c_void;
use core::ptr::NonNull;
use crate::backend::BackendError;
type IoSurfaceRef = *mut c_void;
type CfDictionaryRef = *const c_void;
type CfStringRef = *const c_void;
type CfNumberRef = *const c_void;
type CfAllocatorRef = *const c_void;
const KIO_RETURN_SUCCESS: i32 = 0;
const KCF_NUMBER_SINT32_TYPE: i32 = 3;
const IOSURFACE_PIXEL_FORMAT_420V: u32 = u32::from_be_bytes(*b"420v");
#[link(name = "IOSurface", kind = "framework")]
unsafe extern "C" {
fn IOSurfaceCreate(properties: CfDictionaryRef) -> IoSurfaceRef;
fn IOSurfaceLock(buffer: IoSurfaceRef, options: u32, seed: *mut u32) -> i32;
fn IOSurfaceUnlock(buffer: IoSurfaceRef, options: u32, seed: *mut u32) -> i32;
fn IOSurfaceGetID(buffer: IoSurfaceRef) -> u32;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
static kCFAllocatorDefault: CfAllocatorRef;
static kCFTypeDictionaryKeyCallBacks: *const c_void;
static kCFTypeDictionaryValueCallBacks: *const c_void;
fn CFDictionaryCreate(
allocator: CfAllocatorRef,
keys: *const *const c_void,
values: *const *const c_void,
num_values: isize,
key_callbacks: *const c_void,
value_callbacks: *const c_void,
) -> CfDictionaryRef;
fn CFNumberCreate(
allocator: CfAllocatorRef,
the_type: i32,
value_ptr: *const c_void,
) -> CfNumberRef;
fn CFStringCreateWithCString(
allocator: CfAllocatorRef,
c_str: *const i8,
encoding: u32,
) -> CfStringRef;
fn CFRelease(cf: *const c_void);
}
const KCFSTRING_ENCODING_UTF8: u32 = 0x0800_0100;
fn cf_str(literal: &'static str) -> CfStringRef {
assert!(literal.ends_with('\0'), "literal must be nul-terminated");
let bytes = literal.as_bytes();
unsafe {
CFStringCreateWithCString(
kCFAllocatorDefault,
bytes.as_ptr() as *const i8,
KCFSTRING_ENCODING_UTF8,
)
}
}
fn cf_num_i32(v: i32) -> CfNumberRef {
let ptr: *const i32 = &v;
unsafe {
CFNumberCreate(
kCFAllocatorDefault,
KCF_NUMBER_SINT32_TYPE,
ptr as *const c_void,
)
}
}
pub struct OwnedIoSurface {
surface: NonNull<c_void>,
}
unsafe impl Send for OwnedIoSurface {}
impl OwnedIoSurface {
pub fn create_nv12(width: u32, height: u32) -> Result<Self, BackendError> {
assert!(width > 0, "create_nv12 width positive");
assert!(
height > 0 && height.is_multiple_of(2),
"create_nv12 height positive and even"
);
let width_key = cf_str("IOSurfaceWidth\0");
let height_key = cf_str("IOSurfaceHeight\0");
let pf_key = cf_str("IOSurfacePixelFormat\0");
let bpe_key = cf_str("IOSurfaceBytesPerElement\0");
let width_val = cf_num_i32(width as i32);
let height_val = cf_num_i32(height as i32);
let pf_val = cf_num_i32(IOSURFACE_PIXEL_FORMAT_420V as i32);
let bpe_val = cf_num_i32(1);
let keys: [*const c_void; 4] = [width_key, height_key, pf_key, bpe_key];
let vals: [*const c_void; 4] = [width_val, height_val, pf_val, bpe_val];
let dict = unsafe {
CFDictionaryCreate(
kCFAllocatorDefault,
keys.as_ptr(),
vals.as_ptr(),
4,
kCFTypeDictionaryKeyCallBacks,
kCFTypeDictionaryValueCallBacks,
)
};
let raw = if dict.is_null() {
core::ptr::null_mut()
} else {
unsafe { IOSurfaceCreate(dict) }
};
unsafe {
CFRelease(width_key);
CFRelease(height_key);
CFRelease(pf_key);
CFRelease(bpe_key);
CFRelease(width_val);
CFRelease(height_val);
CFRelease(pf_val);
CFRelease(bpe_val);
if !dict.is_null() {
CFRelease(dict);
}
}
let surface = NonNull::new(raw).ok_or(BackendError::PlatformUnsupported {
reason: "IOSurfaceCreate returned null",
})?;
assert!(
unsafe { IOSurfaceGetID(surface.as_ptr()) } != 0,
"IOSurfaceGetID non-zero"
);
Ok(Self { surface })
}
pub fn handle(&self) -> u64 {
let id = unsafe { IOSurfaceGetID(self.surface.as_ptr()) };
assert!(id != 0, "IOSurfaceID non-zero on handle()");
assert!(self.surface.as_ptr() as usize != 0, "surface ptr non-null");
id as u64
}
pub fn as_ptr(&self) -> *mut c_void {
let p = self.surface.as_ptr();
assert!(!p.is_null(), "IOSurface raw pointer non-null");
assert!(unsafe { IOSurfaceGetID(p) } != 0, "IOSurfaceID non-zero");
p
}
pub fn lock_for_writing(&mut self) -> Result<(), BackendError> {
let mut seed: u32 = 0;
let status = unsafe { IOSurfaceLock(self.surface.as_ptr(), 0, &mut seed) };
if status != KIO_RETURN_SUCCESS {
return Err(BackendError::PlatformUnsupported {
reason: "IOSurfaceLock failed",
});
}
assert_eq!(status, KIO_RETURN_SUCCESS, "lock status ok");
assert!(seed < u32::MAX, "lock seed within range");
Ok(())
}
pub fn unlock_after_writing(&mut self) -> Result<(), BackendError> {
let mut seed: u32 = 0;
let status = unsafe { IOSurfaceUnlock(self.surface.as_ptr(), 0, &mut seed) };
if status != KIO_RETURN_SUCCESS {
return Err(BackendError::PlatformUnsupported {
reason: "IOSurfaceUnlock failed",
});
}
assert_eq!(status, KIO_RETURN_SUCCESS, "unlock status ok");
assert!(seed < u32::MAX, "unlock seed within range");
Ok(())
}
}
impl Drop for OwnedIoSurface {
fn drop(&mut self) {
let ptr = self.surface.as_ptr();
if !ptr.is_null() {
unsafe { CFRelease(ptr) };
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,964 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use std::ffi::c_void;
use std::ptr;
use std::sync::Arc;
use libloading::{Library, Symbol};
use windows::Win32::Graphics::Direct3D11::{ID3D11Device, ID3D11Multithread};
use windows::core::Interface;
use crate::encoder_handoff::{
EncodedBitstream, EncoderCompletionCallback, EncoderDims, EncoderError, EncoderFrameRate,
EncoderSubmission, HandoffSlot, PicParams, QsvHandoff, apply_dts_offset, compute_dts_offset_us,
};
use crate::ring::RingError;
pub const QSV_DLL_NAME_VPL: &str = "libvpl.dll";
pub const QSV_DLL_NAME_MFX: &str = "libmfxhw64.dll";
const MFX_IMPL_HARDWARE: i32 = 0x0002;
const MFX_IMPL_VIA_D3D11: i32 = 0x0300;
const MFX_IMPL_TYPE_HARDWARE: u32 = 2;
const MFX_ACCEL_MODE_VIA_D3D11: u32 = 0x0300;
const MFX_HANDLE_D3D11_DEVICE: u32 = 3;
const MFX_FOURCC_NV12: u32 = u32::from_le_bytes(*b"NV12");
const MFX_CODEC_AVC: u32 = u32::from_le_bytes(*b"AVC ");
const MFX_RATECONTROL_CBR: u16 = 1;
const MFX_PICSTRUCT_PROGRESSIVE: u16 = 0x01;
const MFX_CHROMAFORMAT_YUV420: u16 = 1;
const MFX_IOPATTERN_IN_VIDEO_MEMORY: u16 = 0x01;
const MFX_VARIANT_TYPE_U32: u32 = 5;
const MFX_VARIANT_VERSION_MINOR: u8 = 1;
const MFX_VARIANT_VERSION_MAJOR: u8 = 1;
const MFX_ERR_NONE: i32 = 0;
const MFX_WRN_IN_EXECUTION: i32 = 1;
const MFX_ERR_MORE_DATA: i32 = -10;
const FILTER_PROPERTY_IMPL: &[u8] = b"mfxImplDescription.Impl\0";
const FILTER_PROPERTY_ACCEL: &[u8] = b"mfxImplDescription.AccelerationMode\0";
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct MfxVersion {
minor: u16,
major: u16,
}
#[repr(C)]
#[derive(Default)]
struct MfxFrameInfo {
reserved: [u32; 4],
channel_id: u16,
bit_depth_luma: u16,
bit_depth_chroma: u16,
shift: u16,
frame_id_temporal: u16,
frame_id_priority: u16,
frame_id_view_or_dependency: u16,
frame_id_quality: u16,
four_cc: u32,
width: u16,
height: u16,
crop_x: u16,
crop_y: u16,
crop_w: u16,
crop_h: u16,
frame_rate_extn: u32,
frame_rate_extd: u32,
reserved3: u16,
aspect_ratio_w: u16,
aspect_ratio_h: u16,
pic_struct: u16,
chroma_format: u16,
reserved2: u16,
}
#[repr(C)]
#[derive(Default)]
struct MfxInfoMfx {
reserved: [u32; 7],
low_power: u16,
brc_param_multiplier: u16,
frame_info: MfxFrameInfo,
codec_id: u32,
codec_profile: u16,
codec_level: u16,
num_thread: u16,
target_usage: u16,
gop_pic_size: u16,
gop_ref_dist: u16,
gop_opt_flag: u16,
idr_interval: u16,
rate_control_method: u16,
init_qp: u16,
buffer_size_in_kb: u16,
target_kbps: u16,
max_kbps: u16,
num_slice: u16,
num_ref_frame: u16,
encoded_order: u16,
union_pad: [u16; 15],
}
#[repr(C)]
struct MfxVideoParam {
alloc_id: u32,
reserved: [u32; 2],
reserved3: u16,
async_depth: u16,
mfx: MfxInfoMfx,
protected: u16,
io_pattern: u16,
ext_param: *mut c_void,
num_ext_param: u16,
reserved2: u16,
}
#[repr(C)]
struct MfxBitstream {
encrypted_data: *mut c_void,
num_extparam: u16,
ext_param: *mut c_void,
reserved: [u32; 6],
decode_time_stamp: u64,
time_stamp: u64,
data: *mut u8,
data_offset: u32,
data_length: u32,
max_length: u32,
pic_struct: u16,
frame_type: u16,
data_flag: u16,
reserved2: u16,
}
#[repr(C)]
struct MfxFrameData {
ext_param: *mut c_void,
num_extparam: u16,
reserved: [u32; 8],
mem_type: u16,
pitch_high: u16,
time_stamp: u64,
frame_order: u32,
locked: u16,
pitch_low: u16,
plane_ptrs: [*mut u8; 7],
mem_id: *mut c_void,
corrupted: u16,
data_flag: u16,
}
#[repr(C)]
struct MfxFrameSurface1 {
reserved: [u32; 4],
interface_ptr: *mut c_void,
info: MfxFrameInfo,
data: MfxFrameData,
}
#[repr(C)]
#[derive(Clone, Copy)]
union MfxVariantData {
u32_: u32,
u64_: u64,
ptr: *mut c_void,
pad: [u8; 16],
}
#[repr(C)]
#[derive(Clone, Copy)]
struct MfxVariant {
version: MfxStructVersion,
type_: u32,
data: MfxVariantData,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct MfxStructVersion {
minor: u8,
major: u8,
}
type MfxInit = unsafe extern "C" fn(i32, *mut MfxVersion, *mut *mut c_void) -> i32;
type MfxClose = unsafe extern "C" fn(*mut c_void) -> i32;
type MfxSetHandle = unsafe extern "C" fn(*mut c_void, u32, *mut c_void) -> i32;
type MfxEncodeInit = unsafe extern "C" fn(*mut c_void, *mut MfxVideoParam) -> i32;
type MfxEncodeClose = unsafe extern "C" fn(*mut c_void) -> i32;
type MfxEncodeQuery =
unsafe extern "C" fn(*mut c_void, *mut MfxVideoParam, *mut MfxVideoParam) -> i32;
type MfxEncodeFrameAsync = unsafe extern "C" fn(
*mut c_void,
*mut c_void,
*mut MfxFrameSurface1,
*mut MfxBitstream,
*mut *mut c_void,
) -> i32;
type MfxSyncOperation = unsafe extern "C" fn(*mut c_void, *mut c_void, u32) -> i32;
type MfxLoad = unsafe extern "C" fn() -> *mut c_void;
type MfxUnload = unsafe extern "C" fn(*mut c_void);
type MfxCreateConfig = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
type MfxSetConfigFilterProperty = unsafe extern "C" fn(*mut c_void, *const u8, MfxVariant) -> i32;
type MfxCreateSession = unsafe extern "C" fn(*mut c_void, u32, *mut *mut c_void) -> i32;
struct ApiTable {
init: MfxInit,
close: MfxClose,
set_handle: MfxSetHandle,
encode_init: MfxEncodeInit,
encode_close: MfxEncodeClose,
encode_query: MfxEncodeQuery,
encode_frame_async: MfxEncodeFrameAsync,
sync_operation: MfxSyncOperation,
}
struct DispatcherTable {
load: MfxLoad,
unload: MfxUnload,
create_config: MfxCreateConfig,
set_config_property: MfxSetConfigFilterProperty,
create_session: MfxCreateSession,
}
struct SlotState {
pending_pts_us: u64,
pending_force_keyframe: bool,
sync_point: *mut c_void,
bitstream: MfxBitstream,
bitstream_buf: Vec<u8>,
surface: MfxFrameSurface1,
in_flight: bool,
}
pub struct QsvD3D11Handoff {
_library: Arc<Library>,
api: ApiTable,
dispatcher: Option<DispatcherTable>,
loader: *mut c_void,
session: *mut c_void,
slots: HashMap<u32, SlotState>,
next_slot_index: u32,
dts_offset_us: i64,
completed_count: u64,
target_kbps: u16,
frame_rate: EncoderFrameRate,
}
unsafe impl Send for QsvD3D11Handoff {}
impl QsvD3D11Handoff {
pub fn new(
device: ID3D11Device,
dims: EncoderDims,
bitrate_bps: u32,
) -> Result<Self, EncoderError> {
Self::new_with_frame_rate(device, dims, bitrate_bps, EncoderFrameRate::default())
}
pub fn new_with_frame_rate(
device: ID3D11Device,
dims: EncoderDims,
bitrate_bps: u32,
frame_rate: EncoderFrameRate,
) -> Result<Self, EncoderError> {
assert!(dims.width > 0, "dims width positive");
assert!(dims.height > 0, "dims height positive");
assert!(frame_rate.numerator > 0, "frame rate numerator positive");
assert!(
frame_rate.denominator > 0,
"frame rate denominator positive"
);
if dims.width > 7680 || dims.height > 4320 {
return Err(EncoderError::DimensionsOutOfRange {
width: dims.width,
height: dims.height,
});
}
let library = load_runtime()?;
let api = load_api(&library)?;
let dispatcher_opt = load_dispatcher(&library);
let (loader, session) = open_session(&api, dispatcher_opt.as_ref())?;
set_d3d11_handle(&api, session, &device)?;
let target_kbps = (bitrate_bps / 1000).clamp(500, 60_000) as u16;
encode_init(&api, session, dims, target_kbps, frame_rate)?;
let dts_offset_us = compute_dts_offset_us(0, 0, frame_rate.frame_interval_us());
let handoff = Self {
_library: Arc::new(library),
api,
dispatcher: dispatcher_opt,
loader,
session,
slots: HashMap::new(),
next_slot_index: 0,
dts_offset_us,
completed_count: 0,
target_kbps,
frame_rate,
};
assert!(!handoff.session.is_null(), "session non-null");
Ok(handoff)
}
}
fn load_runtime() -> Result<Library, EncoderError> {
let vpl_result = unsafe { Library::new(QSV_DLL_NAME_VPL) };
match vpl_result {
Ok(lib) => Ok(lib),
Err(_) => {
unsafe { Library::new(QSV_DLL_NAME_MFX) }.map_err(|_| EncoderError::SdkNotFound {
vendor: "qsv",
dll: QSV_DLL_NAME_MFX,
})
}
}
}
fn load_api(library: &Library) -> Result<ApiTable, EncoderError> {
let init: Symbol<'_, MfxInit> =
unsafe { library.get(b"MFXInit\0") }.map_err(|_| EncoderError::SymbolMissing {
vendor: "qsv",
symbol: "MFXInit",
})?;
let close: Symbol<'_, MfxClose> =
unsafe { library.get(b"MFXClose\0") }.map_err(|_| EncoderError::SymbolMissing {
vendor: "qsv",
symbol: "MFXClose",
})?;
let set_handle: Symbol<'_, MfxSetHandle> = unsafe { library.get(b"MFXVideoCORE_SetHandle\0") }
.map_err(|_| EncoderError::SymbolMissing {
vendor: "qsv",
symbol: "MFXVideoCORE_SetHandle",
})?;
let encode_init: Symbol<'_, MfxEncodeInit> = unsafe { library.get(b"MFXVideoENCODE_Init\0") }
.map_err(|_| EncoderError::SymbolMissing {
vendor: "qsv",
symbol: "MFXVideoENCODE_Init",
})?;
let encode_close: Symbol<'_, MfxEncodeClose> = unsafe {
library.get(b"MFXVideoENCODE_Close\0")
}
.map_err(|_| EncoderError::SymbolMissing {
vendor: "qsv",
symbol: "MFXVideoENCODE_Close",
})?;
let encode_query: Symbol<'_, MfxEncodeQuery> = unsafe {
library.get(b"MFXVideoENCODE_Query\0")
}
.map_err(|_| EncoderError::SymbolMissing {
vendor: "qsv",
symbol: "MFXVideoENCODE_Query",
})?;
let encode_frame_async: Symbol<'_, MfxEncodeFrameAsync> = unsafe {
library.get(b"MFXVideoENCODE_EncodeFrameAsync\0")
}
.map_err(|_| EncoderError::SymbolMissing {
vendor: "qsv",
symbol: "MFXVideoENCODE_EncodeFrameAsync",
})?;
let sync_operation: Symbol<'_, MfxSyncOperation> = unsafe {
library.get(b"MFXVideoCORE_SyncOperation\0")
}
.map_err(|_| EncoderError::SymbolMissing {
vendor: "qsv",
symbol: "MFXVideoCORE_SyncOperation",
})?;
Ok(ApiTable {
init: *init,
close: *close,
set_handle: *set_handle,
encode_init: *encode_init,
encode_close: *encode_close,
encode_query: *encode_query,
encode_frame_async: *encode_frame_async,
sync_operation: *sync_operation,
})
}
fn load_dispatcher(library: &Library) -> Option<DispatcherTable> {
let load: Symbol<'_, MfxLoad> = unsafe { library.get(b"MFXLoad\0") }.ok()?;
let unload: Symbol<'_, MfxUnload> = unsafe { library.get(b"MFXUnload\0") }.ok()?;
let create_config: Symbol<'_, MfxCreateConfig> =
unsafe { library.get(b"MFXCreateConfig\0") }.ok()?;
let set_config_property: Symbol<'_, MfxSetConfigFilterProperty> =
unsafe { library.get(b"MFXSetConfigFilterProperty\0") }.ok()?;
let create_session: Symbol<'_, MfxCreateSession> =
unsafe { library.get(b"MFXCreateSession\0") }.ok()?;
Some(DispatcherTable {
load: *load,
unload: *unload,
create_config: *create_config,
set_config_property: *set_config_property,
create_session: *create_session,
})
}
fn open_session(
api: &ApiTable,
dispatcher: Option<&DispatcherTable>,
) -> Result<(*mut c_void, *mut c_void), EncoderError> {
if let Some(d) = dispatcher {
match modern_session(d) {
Ok((loader, session)) => return Ok((loader, session)),
Err(e) => {
let session = init_session_legacy(api)?;
let _ = e;
return Ok((ptr::null_mut(), session));
}
}
}
let session = init_session_legacy(api)?;
Ok((ptr::null_mut(), session))
}
fn modern_session(d: &DispatcherTable) -> Result<(*mut c_void, *mut c_void), EncoderError> {
let loader = unsafe { (d.load)() };
if loader.is_null() {
return Err(EncoderError::SessionInitFailed {
vendor: "qsv-mfxload",
status: -1,
});
}
assert!(!loader.is_null(), "MFXLoad returned non-null");
if let Err(e) = set_filter_u32(d, loader, FILTER_PROPERTY_IMPL, MFX_IMPL_TYPE_HARDWARE) {
unsafe { (d.unload)(loader) };
return Err(e);
}
if let Err(e) = set_filter_u32(d, loader, FILTER_PROPERTY_ACCEL, MFX_ACCEL_MODE_VIA_D3D11) {
unsafe { (d.unload)(loader) };
return Err(e);
}
let mut session: *mut c_void = ptr::null_mut();
let status = unsafe { (d.create_session)(loader, 0, &mut session) };
if status != MFX_ERR_NONE || session.is_null() {
unsafe { (d.unload)(loader) };
return Err(EncoderError::SessionInitFailed {
vendor: "qsv-create-session",
status: status as i64,
});
}
assert!(!session.is_null(), "modern session non-null");
Ok((loader, session))
}
fn set_filter_u32(
d: &DispatcherTable,
loader: *mut c_void,
property: &'static [u8],
value_u32: u32,
) -> Result<(), EncoderError> {
assert!(!loader.is_null(), "loader non-null");
assert!(!property.is_empty(), "property non-empty");
let cfg = unsafe { (d.create_config)(loader) };
if cfg.is_null() {
return Err(EncoderError::SessionInitFailed {
vendor: "qsv-create-config",
status: -1,
});
}
let variant = MfxVariant {
version: MfxStructVersion {
minor: MFX_VARIANT_VERSION_MINOR,
major: MFX_VARIANT_VERSION_MAJOR,
},
type_: MFX_VARIANT_TYPE_U32,
data: MfxVariantData { u32_: value_u32 },
};
let status = unsafe { (d.set_config_property)(cfg, property.as_ptr(), variant) };
if status != MFX_ERR_NONE {
let vendor = if property == FILTER_PROPERTY_ACCEL {
"qsv-accel-mode-rejected"
} else {
"qsv-set-config-property"
};
return Err(EncoderError::SessionInitFailed {
vendor,
status: status as i64,
});
}
Ok(())
}
fn init_session_legacy(api: &ApiTable) -> Result<*mut c_void, EncoderError> {
let session_via_d3d11 = try_mfxinit(api, MFX_IMPL_HARDWARE | MFX_IMPL_VIA_D3D11);
if let Ok(s) = session_via_d3d11 {
return Ok(s);
}
try_mfxinit(api, MFX_IMPL_HARDWARE)
}
fn try_mfxinit(api: &ApiTable, impl_flags: i32) -> Result<*mut c_void, EncoderError> {
let mut version = MfxVersion { major: 1, minor: 0 };
let mut session: *mut c_void = ptr::null_mut();
let status = unsafe { (api.init)(impl_flags, &mut version, &mut session) };
if status != MFX_ERR_NONE {
return Err(EncoderError::SessionInitFailed {
vendor: "qsv-mfxinit",
status: status as i64,
});
}
if session.is_null() {
return Err(EncoderError::SessionInitFailed {
vendor: "qsv-mfxinit",
status: -1,
});
}
assert!(!session.is_null(), "session non-null after MFXInit");
Ok(session)
}
fn set_d3d11_handle(
api: &ApiTable,
session: *mut c_void,
device: &ID3D11Device,
) -> Result<(), EncoderError> {
assert!(!session.is_null(), "session non-null");
let raw = device.as_raw();
assert!(!raw.is_null(), "device raw non-null");
if let Ok(mt) = device.cast::<ID3D11Multithread>() {
let _ = unsafe { mt.SetMultithreadProtected(true) };
}
let status = unsafe { (api.set_handle)(session, MFX_HANDLE_D3D11_DEVICE, raw) };
if status != MFX_ERR_NONE && status != MFX_WRN_IN_EXECUTION {
return Err(EncoderError::SessionInitFailed {
vendor: "qsv-set-handle",
status: status as i64,
});
}
Ok(())
}
fn build_video_params(
dims: EncoderDims,
target_kbps: u16,
frame_rate: EncoderFrameRate,
) -> MfxVideoParam {
let info = MfxFrameInfo {
four_cc: MFX_FOURCC_NV12,
width: align16(dims.width as u16),
height: align16(dims.height as u16),
crop_w: dims.width as u16,
crop_h: dims.height as u16,
frame_rate_extn: frame_rate.numerator,
frame_rate_extd: frame_rate.denominator,
aspect_ratio_w: 1,
aspect_ratio_h: 1,
pic_struct: MFX_PICSTRUCT_PROGRESSIVE,
chroma_format: MFX_CHROMAFORMAT_YUV420,
..Default::default()
};
let mfx = MfxInfoMfx {
frame_info: info,
codec_id: MFX_CODEC_AVC,
target_usage: 4,
gop_pic_size: frame_rate.gop_pic_size(),
gop_ref_dist: 1,
rate_control_method: MFX_RATECONTROL_CBR,
target_kbps,
max_kbps: target_kbps,
num_slice: 1,
num_ref_frame: 1,
..Default::default()
};
MfxVideoParam {
alloc_id: 0,
reserved: [0; 2],
reserved3: 0,
async_depth: 1,
mfx,
protected: 0,
io_pattern: MFX_IOPATTERN_IN_VIDEO_MEMORY,
ext_param: ptr::null_mut(),
num_ext_param: 0,
reserved2: 0,
}
}
fn align16(v: u16) -> u16 {
(v + 15) & !15
}
fn encode_init(
api: &ApiTable,
session: *mut c_void,
dims: EncoderDims,
target_kbps: u16,
frame_rate: EncoderFrameRate,
) -> Result<(), EncoderError> {
let mut params = build_video_params(dims, target_kbps, frame_rate);
let mut query_out = build_video_params(dims, target_kbps, frame_rate);
let q_status = unsafe { (api.encode_query)(session, &mut params, &mut query_out) };
let q_ok = q_status == MFX_ERR_NONE
|| q_status == MFX_WRN_IN_EXECUTION
|| q_status == -3
|| q_status > 0;
if !q_ok {
return Err(EncoderError::SessionInitFailed {
vendor: "qsv-encode-query",
status: q_status as i64,
});
}
let status = unsafe { (api.encode_init)(session, &mut query_out) };
if status != MFX_ERR_NONE && status != MFX_WRN_IN_EXECUTION {
return Err(EncoderError::SessionInitFailed {
vendor: "qsv-encode-init",
status: status as i64,
});
}
Ok(())
}
fn build_slot_state(
shared_handle: u64,
dims: EncoderDims,
target_kbps: u16,
frame_rate: EncoderFrameRate,
) -> SlotState {
let max_bs = (dims.width as usize * dims.height as usize * 3 / 2).max(512 * 1024);
let mut bitstream_buf = vec![0u8; max_bs];
let bs_ptr = bitstream_buf.as_mut_ptr();
let bitstream = MfxBitstream {
encrypted_data: ptr::null_mut(),
num_extparam: 0,
ext_param: ptr::null_mut(),
reserved: [0; 6],
decode_time_stamp: 0,
time_stamp: 0,
data: bs_ptr,
data_offset: 0,
data_length: 0,
max_length: max_bs as u32,
pic_struct: 0,
frame_type: 0,
data_flag: 0,
reserved2: 0,
};
let info = MfxFrameInfo {
four_cc: MFX_FOURCC_NV12,
width: align16(dims.width as u16),
height: align16(dims.height as u16),
crop_w: dims.width as u16,
crop_h: dims.height as u16,
frame_rate_extn: frame_rate.numerator,
frame_rate_extd: frame_rate.denominator,
aspect_ratio_w: 1,
aspect_ratio_h: 1,
pic_struct: MFX_PICSTRUCT_PROGRESSIVE,
chroma_format: MFX_CHROMAFORMAT_YUV420,
..Default::default()
};
let _ = target_kbps;
let surface = MfxFrameSurface1 {
reserved: [0; 4],
interface_ptr: ptr::null_mut(),
info,
data: MfxFrameData {
ext_param: ptr::null_mut(),
num_extparam: 0,
reserved: [0; 8],
mem_type: 0,
pitch_high: 0,
time_stamp: 0,
frame_order: 0,
locked: 0,
pitch_low: 0,
plane_ptrs: [ptr::null_mut(); 7],
mem_id: shared_handle as *mut c_void,
corrupted: 0,
data_flag: 0,
},
};
SlotState {
pending_pts_us: 0,
pending_force_keyframe: false,
sync_point: ptr::null_mut(),
bitstream,
bitstream_buf,
surface,
in_flight: false,
}
}
impl Drop for QsvD3D11Handoff {
fn drop(&mut self) {
self.slots.clear();
if !self.session.is_null() {
let _ = unsafe { (self.api.encode_close)(self.session) };
let _ = unsafe { (self.api.close)(self.session) };
self.session = ptr::null_mut();
}
if let Some(d) = self.dispatcher.take()
&& !self.loader.is_null()
{
unsafe { (d.unload)(self.loader) };
self.loader = ptr::null_mut();
}
}
}
impl QsvHandoff for QsvD3D11Handoff {
fn register_slot(
&mut self,
shared_handle: u64,
_key: u64,
dims: EncoderDims,
) -> Result<HandoffSlot, EncoderError> {
assert!(shared_handle != 0, "shared_handle non-zero");
assert!(dims.width > 0, "width positive");
let slot_index = self.next_slot_index;
self.next_slot_index = self.next_slot_index.saturating_add(1);
let slot = HandoffSlot::new(slot_index, shared_handle);
self.slots.insert(
slot_index,
build_slot_state(shared_handle, dims, self.target_kbps, self.frame_rate),
);
assert!(self.slots.contains_key(&slot_index), "slot stored");
Ok(slot)
}
fn encode_shared_async(
&mut self,
slot: HandoffSlot,
_key: u64,
dims: EncoderDims,
pic_params: PicParams,
) -> Result<(), EncoderError> {
assert!(slot.shared_handle != 0, "slot handle non-zero");
assert!(dims.width > 0, "width positive");
let state = self
.slots
.get_mut(&slot.slot_index)
.ok_or(EncoderError::SlotUnknown {
slot_index: slot.slot_index,
})?;
state.surface.data.time_stamp = pic_params.pts_us;
state.bitstream.data_length = 0;
state.bitstream.data_offset = 0;
let mut sync: *mut c_void = ptr::null_mut();
let status = unsafe {
(self.api.encode_frame_async)(
self.session,
ptr::null_mut(),
&mut state.surface,
&mut state.bitstream,
&mut sync,
)
};
state.pending_pts_us = pic_params.pts_us;
state.pending_force_keyframe = pic_params.force_keyframe;
if status == MFX_ERR_MORE_DATA {
state.in_flight = false;
return Ok(());
}
if status != MFX_ERR_NONE {
return Err(EncoderError::EncodeFailed {
vendor: "qsv",
status: status as i64,
});
}
state.sync_point = sync;
state.in_flight = !sync.is_null();
Ok(())
}
fn poll_completed(&mut self, slot: HandoffSlot) -> Option<EncodedBitstream> {
let session_ptr = self.session;
let dts_offset_us = self.dts_offset_us;
let completed_count = self.completed_count;
let state = self.slots.get_mut(&slot.slot_index)?;
if !state.in_flight || state.sync_point.is_null() {
return None;
}
let status = unsafe { (self.api.sync_operation)(session_ptr, state.sync_point, 0) };
if status == MFX_WRN_IN_EXECUTION || status != MFX_ERR_NONE {
return None;
}
let len = state.bitstream.data_length as usize;
if len == 0 {
state.in_flight = false;
return None;
}
let mut data: Vec<u8> = Vec::with_capacity(len);
let offset = state.bitstream.data_offset as usize;
data.extend_from_slice(&state.bitstream_buf[offset..offset + len]);
let pts = state.bitstream.time_stamp;
let is_keyframe = (state.bitstream.frame_type & 0x1) != 0 || completed_count == 0;
let dts = apply_dts_offset(pts, dts_offset_us);
state.in_flight = false;
state.sync_point = ptr::null_mut();
self.completed_count = self.completed_count.saturating_add(1);
Some(EncodedBitstream::new(data, pts, dts, is_keyframe))
}
fn unregister_slot(&mut self, slot: HandoffSlot) {
self.slots.remove(&slot.slot_index);
}
fn encode_shared(
&mut self,
submission: EncoderSubmission,
callback: &mut dyn EncoderCompletionCallback,
) -> Result<(), RingError> {
assert!(submission.shared_handle != 0, "submission handle non-zero");
assert!(submission.dims.width > 0, "submission width positive");
let slot = self
.register_slot(
submission.shared_handle,
submission.keyed_mutex_key,
submission.dims,
)
.map_err(|_| RingError::NotImplemented {
what: "qsv::register_slot in encode_shared",
})?;
let pts_us = submission.capture_pts_us.unwrap_or_else(|| {
submission
.sequence
.saturating_mul(self.frame_rate.frame_interval_us())
});
let pic = PicParams::new(pts_us, false);
QsvHandoff::encode_shared_async(
self,
slot,
submission.keyed_mutex_key,
submission.dims,
pic,
)
.map_err(|_| RingError::NotImplemented {
what: "qsv::encode_shared_async",
})?;
if let Some(bs) = QsvHandoff::poll_completed(self, slot) {
callback.on_complete(submission.sequence, bs.data.len() as u32);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sdk_not_found_when_dll_missing() {
let dummy_path = "/this/path/does/not/exist/fake-libmfxhw64.dll";
let result = unsafe { Library::new(dummy_path) };
assert!(result.is_err());
}
#[test]
fn fourcc_nv12_packs_correctly() {
assert_eq!(MFX_FOURCC_NV12, 0x3231564E);
}
#[test]
fn align16_rounds_up() {
assert_eq!(align16(1080), 1088);
assert_eq!(align16(1920), 1920);
assert_eq!(align16(0), 0);
}
#[test]
fn target_kbps_clamps_against_overflow() {
let bitrate: u32 = u32::MAX;
let kbps = (bitrate / 1000).clamp(500, 60_000) as u16;
assert_eq!(kbps, 60_000);
}
#[test]
fn accel_mode_constant_matches_onevpl_spec() {
assert_eq!(MFX_ACCEL_MODE_VIA_D3D11, 0x0300);
assert_ne!(MFX_ACCEL_MODE_VIA_D3D11, 0x0200);
}
#[test]
fn impl_type_hardware_constant_matches_onevpl_spec() {
assert_eq!(MFX_IMPL_TYPE_HARDWARE, 2);
}
#[test]
fn variant_type_u32_constant_matches_onevpl_spec() {
assert_eq!(MFX_VARIANT_TYPE_U32, 5);
assert_ne!(MFX_VARIANT_TYPE_U32, 8);
}
#[test]
fn d3d11_device_handle_type_matches_onevpl_spec() {
assert_eq!(MFX_HANDLE_D3D11_DEVICE, 3);
assert_ne!(MFX_HANDLE_D3D11_DEVICE, 2);
}
#[test]
fn variant_version_matches_onevpl_spec() {
assert_eq!(MFX_VARIANT_VERSION_MAJOR, 1);
assert_eq!(MFX_VARIANT_VERSION_MINOR, 1);
}
#[test]
fn filter_property_name_is_null_terminated_accel() {
assert!(FILTER_PROPERTY_ACCEL.ends_with(b"\0"));
let view = &FILTER_PROPERTY_ACCEL[..FILTER_PROPERTY_ACCEL.len() - 1];
assert_eq!(view, b"mfxImplDescription.AccelerationMode");
}
#[test]
fn filter_property_name_is_null_terminated_impl() {
assert!(FILTER_PROPERTY_IMPL.ends_with(b"\0"));
let view = &FILTER_PROPERTY_IMPL[..FILTER_PROPERTY_IMPL.len() - 1];
assert_eq!(view, b"mfxImplDescription.Impl");
}
#[test]
fn variant_payload_carries_u32_value() {
let v = MfxVariant {
version: MfxStructVersion {
minor: MFX_VARIANT_VERSION_MINOR,
major: MFX_VARIANT_VERSION_MAJOR,
},
type_: MFX_VARIANT_TYPE_U32,
data: MfxVariantData {
u32_: MFX_ACCEL_MODE_VIA_D3D11,
},
};
let read = unsafe { v.data.u32_ };
assert_eq!(read, MFX_ACCEL_MODE_VIA_D3D11);
assert_eq!(v.type_, 5);
}
#[derive(Default)]
struct FilterRecord {
property: Vec<u8>,
value_u32: u32,
variant_type: u32,
}
#[test]
fn mock_set_filter_records_property_and_value() {
let mut record = FilterRecord::default();
let property = FILTER_PROPERTY_ACCEL;
let variant = MfxVariant {
version: MfxStructVersion {
minor: MFX_VARIANT_VERSION_MINOR,
major: MFX_VARIANT_VERSION_MAJOR,
},
type_: MFX_VARIANT_TYPE_U32,
data: MfxVariantData {
u32_: MFX_ACCEL_MODE_VIA_D3D11,
},
};
let mut len: usize = 0;
while *property.get(len).unwrap_or(&1) != 0 {
len += 1;
}
record.property.extend_from_slice(&property[..len]);
record.variant_type = variant.type_;
record.value_u32 = unsafe { variant.data.u32_ };
assert_eq!(record.property, b"mfxImplDescription.AccelerationMode");
assert_eq!(record.variant_type, MFX_VARIANT_TYPE_U32);
assert_eq!(record.value_u32, MFX_ACCEL_MODE_VIA_D3D11);
assert_eq!(record.value_u32, 0x0300);
}
}
@@ -0,0 +1,966 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::backend::{BackendError, KeyedMutexBackend, TextureFormat};
pub const RING_SIZE: usize = 8;
pub const DUPLICATE_COUNT_MAX: u32 = 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlotState {
Free,
Filling,
Submitted,
Dispatched,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RingError {
FullDropped {
dropped_so_far: u64,
},
BackendFailed {
source: BackendError,
},
NotInitialised,
AlreadyInitialised,
UnknownSlot,
UnexpectedSlotState {
slot_index: u32,
observed: SlotState,
},
PlatformUnsupported {
reason: &'static str,
},
NotImplemented {
what: &'static str,
},
SlotsExhausted,
}
impl std::fmt::Display for RingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FullDropped { dropped_so_far } => {
write!(f, "ring full; total dropped={dropped_so_far}")
}
Self::BackendFailed { source } => write!(f, "backend failed: {source}"),
Self::NotInitialised => write!(f, "ring not initialised"),
Self::AlreadyInitialised => write!(f, "ring already initialised"),
Self::UnknownSlot => write!(f, "slot handle does not belong to this ring"),
Self::UnexpectedSlotState {
slot_index,
observed,
} => {
write!(f, "slot {slot_index} in unexpected state {observed:?}")
}
Self::PlatformUnsupported { reason } => write!(f, "platform unsupported: {reason}"),
Self::NotImplemented { what } => write!(f, "not implemented: {what}"),
Self::SlotsExhausted => write!(f, "slot id space exhausted (all candidates in use)"),
}
}
}
impl std::error::Error for RingError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RingMetrics {
pub submitted_count: u64,
pub completed_count: u64,
pub dropped_count: u64,
pub dispatched_count: u64,
pub lagged_count: u64,
}
pub struct EncoderReady<H: Clone> {
pub handle: H,
pub sequence: u64,
pub slot_index: u32,
pub duplicate_count: u32,
}
pub struct FillReservation<H: Clone> {
pub handle: H,
slot_index: u32,
key: u64,
}
impl<H: Clone> FillReservation<H> {
pub fn slot_index(&self) -> u32 {
let index = self.slot_index;
assert!((index as usize) < RING_SIZE_MAX, "slot_index within max");
assert!(self.key < u64::MAX, "reservation key plausible");
index
}
}
struct SlotMeta {
state: SlotState,
sequence: u64,
key: u64,
duplicate_count: u32,
}
impl SlotMeta {
const fn fresh() -> Self {
Self {
state: SlotState::Free,
sequence: 0,
key: 0,
duplicate_count: 0,
}
}
}
pub struct EncoderInputRing<B: KeyedMutexBackend> {
backend: B,
slots: Vec<B::SlotHandle>,
meta: Vec<SlotMeta>,
metrics: RingMetrics,
pending_lagged: u32,
initialised: bool,
width: u32,
height: u32,
format: TextureFormat,
}
impl<B: KeyedMutexBackend> EncoderInputRing<B> {
pub fn new(backend: B) -> Self {
let ring = Self {
backend,
slots: Vec::with_capacity(B::NUM_SLOTS),
meta: Vec::with_capacity(B::NUM_SLOTS),
metrics: RingMetrics::default(),
pending_lagged: 0,
initialised: false,
width: 0,
height: 0,
format: TextureFormat::Nv12,
};
assert!(!ring.initialised, "fresh ring is uninitialised");
assert_eq!(ring.slots.len(), 0, "fresh ring has no slots");
ring
}
pub fn initialise(
&mut self,
width: u32,
height: u32,
format: TextureFormat,
) -> Result<(), RingError> {
if self.initialised {
return Err(RingError::AlreadyInitialised);
}
let handles = self
.backend
.create_slots(width, height, format)
.map_err(|source| RingError::BackendFailed { source })?;
assert_eq!(
handles.len(),
B::NUM_SLOTS,
"backend returns NUM_SLOTS handles"
);
assert!(handles.len() <= RING_SIZE_MAX, "NUM_SLOTS within max");
self.slots = handles;
self.meta = (0..B::NUM_SLOTS).map(|_| SlotMeta::fresh()).collect();
self.width = width;
self.height = height;
self.format = format;
self.initialised = true;
assert!(self.initialised, "initialised flipped");
assert_eq!(self.slots.len(), self.meta.len(), "slots and meta align");
Ok(())
}
pub fn submit<F>(&mut self, fill: F) -> Result<(), RingError>
where
F: FnOnce(&mut B::SlotHandle),
{
let mut reservation = self.reserve()?;
fill(&mut reservation.handle);
let sequence = self.commit(reservation)?;
assert!(sequence > 0, "committed sequence positive");
assert!(
self.metrics.submitted_count >= sequence,
"monotonic submitted"
);
Ok(())
}
pub fn submit_skip_oldest<F>(&mut self, fill: F) -> Result<(), RingError>
where
F: FnOnce(&mut B::SlotHandle),
{
let mut reservation = self.reserve_skip_oldest()?;
fill(&mut reservation.handle);
let sequence = self.commit(reservation)?;
assert!(sequence > 0, "skip-oldest: committed sequence positive");
assert!(
self.metrics.submitted_count >= sequence,
"skip-oldest: monotonic submitted"
);
Ok(())
}
pub fn reserve(&mut self) -> Result<FillReservation<B::SlotHandle>, RingError> {
if !self.initialised {
return Err(RingError::NotInitialised);
}
self.acquire_free_slot()
}
pub fn reserve_skip_oldest(&mut self) -> Result<FillReservation<B::SlotHandle>, RingError> {
if !self.initialised {
return Err(RingError::NotInitialised);
}
if self.find_free_slot().is_none() {
let _ = self.evict_oldest_submitted();
}
self.acquire_free_slot()
}
fn acquire_free_slot(&mut self) -> Result<FillReservation<B::SlotHandle>, RingError> {
assert!(self.initialised, "acquire requires initialised ring");
assert_eq!(self.slots.len(), self.meta.len(), "slots and meta align");
for index in 0..self.meta.len() {
if self.meta[index].state != SlotState::Free {
continue;
}
let key = self.meta[index].key;
match self.backend.acquire_write(&self.slots[index], key) {
Ok(()) => {
self.meta[index].state = SlotState::Filling;
return Ok(FillReservation {
handle: self.slots[index].clone(),
slot_index: index as u32,
key,
});
}
Err(BackendError::WouldBlock { .. }) => continue,
Err(source) => return Err(RingError::BackendFailed { source }),
}
}
self.fold_lagged(1);
self.metrics.lagged_count = self.metrics.lagged_count.saturating_add(1);
self.metrics.dropped_count = self.metrics.dropped_count.saturating_add(1);
Err(RingError::FullDropped {
dropped_so_far: self.metrics.dropped_count,
})
}
pub fn commit(
&mut self,
reservation: FillReservation<B::SlotHandle>,
) -> Result<u64, RingError> {
if !self.initialised {
return Err(RingError::NotInitialised);
}
let index = reservation.slot_index as usize;
if index >= self.meta.len() {
return Err(RingError::UnknownSlot);
}
let observed = self.meta[index].state;
if observed != SlotState::Filling {
return Err(RingError::UnexpectedSlotState {
slot_index: reservation.slot_index,
observed,
});
}
assert_eq!(
self.meta[index].key, reservation.key,
"reservation key matches slot"
);
let next_key = reservation.key.wrapping_add(1);
self.backend
.release_write(&self.slots[index], next_key)
.map_err(|source| RingError::BackendFailed { source })?;
let sequence = self.metrics.submitted_count.saturating_add(1);
self.meta[index].state = SlotState::Submitted;
self.meta[index].sequence = sequence;
self.meta[index].key = next_key;
self.meta[index].duplicate_count = self.pending_lagged;
self.pending_lagged = 0;
self.metrics.submitted_count = sequence;
self.metrics.completed_count = self.metrics.completed_count.saturating_add(1);
assert_eq!(
self.meta[index].state,
SlotState::Submitted,
"post-commit submitted"
);
assert!(
self.metrics.submitted_count >= sequence,
"commit: monotonic submitted"
);
Ok(sequence)
}
pub fn cancel(&mut self, reservation: FillReservation<B::SlotHandle>) -> Result<(), RingError> {
if !self.initialised {
return Err(RingError::NotInitialised);
}
let index = reservation.slot_index as usize;
if index >= self.meta.len() {
return Err(RingError::UnknownSlot);
}
let observed = self.meta[index].state;
if observed != SlotState::Filling {
return Err(RingError::UnexpectedSlotState {
slot_index: reservation.slot_index,
observed,
});
}
assert_eq!(
self.meta[index].key, reservation.key,
"cancel: reservation key matches slot"
);
let next_key = reservation.key.wrapping_add(1);
self.backend
.release_write(&self.slots[index], next_key)
.map_err(|source| RingError::BackendFailed { source })?;
self.backend.mark_consumed(&self.slots[index]);
self.meta[index].state = SlotState::Free;
self.meta[index].key = next_key;
assert_eq!(self.meta[index].state, SlotState::Free, "post-cancel free");
assert!(
!self.backend.poll_complete(&self.slots[index]),
"cancelled slot not complete"
);
Ok(())
}
fn fold_lagged(&mut self, amount: u32) {
assert!(amount > 0, "fold amount positive");
assert!(
amount <= DUPLICATE_COUNT_MAX.saturating_add(1),
"fold amount bounded"
);
let mut newest: Option<usize> = None;
let mut newest_sequence: u64 = 0;
for (i, meta) in self.meta.iter().enumerate() {
if meta.state != SlotState::Submitted {
continue;
}
if meta.sequence >= newest_sequence {
newest_sequence = meta.sequence;
newest = Some(i);
}
}
match newest {
Some(index) => {
let total = self.meta[index].duplicate_count.saturating_add(amount);
self.meta[index].duplicate_count = total.min(DUPLICATE_COUNT_MAX);
}
None => {
let total = self.pending_lagged.saturating_add(amount);
self.pending_lagged = total.min(DUPLICATE_COUNT_MAX);
}
}
assert!(
self.pending_lagged <= DUPLICATE_COUNT_MAX,
"pending lag bounded"
);
}
fn evict_oldest_submitted(&mut self) -> bool {
let mut chosen: Option<usize> = None;
let mut chosen_sequence: u64 = u64::MAX;
for (i, meta) in self.meta.iter().enumerate() {
if meta.state != SlotState::Submitted {
continue;
}
if meta.sequence < chosen_sequence {
chosen_sequence = meta.sequence;
chosen = Some(i);
}
}
let Some(index) = chosen else {
return false;
};
assert_eq!(
self.meta[index].state,
SlotState::Submitted,
"evict candidate is submitted"
);
assert!(
chosen_sequence != u64::MAX,
"evict candidate had real sequence"
);
let folded = self.meta[index].duplicate_count.saturating_add(1);
self.backend.mark_consumed(&self.slots[index]);
self.meta[index].state = SlotState::Free;
self.meta[index].duplicate_count = 0;
self.fold_lagged(folded);
self.metrics.lagged_count = self.metrics.lagged_count.saturating_add(1);
self.metrics.dropped_count = self.metrics.dropped_count.saturating_add(1);
true
}
pub fn poll_next_ready(&mut self) -> Option<EncoderReady<B::SlotHandle>> {
if !self.initialised {
return None;
}
let mut chosen: Option<usize> = None;
let mut chosen_sequence: u64 = u64::MAX;
for i in 0..self.meta.len() {
if self.meta[i].state != SlotState::Submitted {
continue;
}
if !self.backend.poll_complete(&self.slots[i]) {
continue;
}
if self.meta[i].sequence < chosen_sequence {
chosen_sequence = self.meta[i].sequence;
chosen = Some(i);
}
}
let index = chosen?;
assert_eq!(
self.meta[index].state,
SlotState::Submitted,
"ready slot was submitted"
);
assert!(
self.backend.poll_complete(&self.slots[index]),
"ready slot is complete"
);
self.meta[index].state = SlotState::Dispatched;
self.metrics.dispatched_count = self.metrics.dispatched_count.saturating_add(1);
Some(EncoderReady {
handle: self.slots[index].clone(),
sequence: self.meta[index].sequence,
slot_index: index as u32,
duplicate_count: self.meta[index].duplicate_count,
})
}
pub fn release_completed(
&mut self,
ready: EncoderReady<B::SlotHandle>,
) -> Result<(), RingError> {
if !self.initialised {
return Err(RingError::NotInitialised);
}
let index = ready.slot_index as usize;
if index >= self.meta.len() {
return Err(RingError::UnknownSlot);
}
let observed = self.meta[index].state;
if observed != SlotState::Dispatched {
return Err(RingError::UnexpectedSlotState {
slot_index: ready.slot_index,
observed,
});
}
self.backend.mark_consumed(&self.slots[index]);
self.meta[index].state = SlotState::Free;
assert_eq!(self.meta[index].state, SlotState::Free, "post-release free");
assert!(
!self.backend.poll_complete(&self.slots[index]),
"no longer reports complete"
);
Ok(())
}
pub fn metrics(&self) -> RingMetrics {
assert!(
self.metrics.completed_count <= self.metrics.submitted_count,
"complete<=submit"
);
assert!(
self.metrics.dispatched_count <= self.metrics.completed_count,
"dispatch<=complete"
);
self.metrics
}
pub fn submitted_count(&self) -> u64 {
self.metrics.submitted_count
}
pub fn completed_count(&self) -> u64 {
self.metrics.completed_count
}
pub fn dropped_count(&self) -> u64 {
self.metrics.dropped_count
}
pub fn dispatched_count(&self) -> u64 {
self.metrics.dispatched_count
}
pub fn capacity(&self) -> usize {
let cap = B::NUM_SLOTS;
assert!(cap > 0, "NUM_SLOTS must be positive");
assert!(cap <= RING_SIZE_MAX, "NUM_SLOTS within max");
cap
}
pub fn free_count(&self) -> usize {
let mut count: usize = 0;
for meta in self.meta.iter() {
if meta.state == SlotState::Free {
count = count.saturating_add(1);
}
}
assert!(count <= self.meta.len(), "free count within capacity");
count
}
pub fn backend_mut(&mut self) -> &mut B {
assert!(self.initialised, "backend access requires init");
&mut self.backend
}
fn find_free_slot(&self) -> Option<usize> {
for (i, meta) in self.meta.iter().enumerate() {
if meta.state == SlotState::Free {
return Some(i);
}
}
None
}
}
pub const RING_SIZE_MAX: usize = 16;
const _: () = assert!(RING_SIZE <= RING_SIZE_MAX, "RING_SIZE within max");
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::{CpuMemcpyBackend, CpuSlotHandle};
fn make_ring() -> EncoderInputRing<CpuMemcpyBackend> {
let mut ring = EncoderInputRing::new(CpuMemcpyBackend::new());
ring.initialise(64, 64, TextureFormat::Nv12).expect("init");
ring
}
fn fill_noop(_: &mut CpuSlotHandle) {}
#[test]
fn submit_then_poll_returns_some() {
let mut ring = make_ring();
ring.submit(fill_noop).expect("submit");
let ready = ring.poll_next_ready().expect("poll yields ready");
assert_eq!(ready.sequence, 1);
ring.release_completed(ready).expect("release");
}
#[test]
fn eight_submits_dispatch_in_fifo_order() {
let mut ring = make_ring();
for _ in 0..8 {
ring.submit(fill_noop).expect("submit");
}
assert_eq!(ring.submitted_count(), 8);
let mut observed_seq: Vec<u64> = Vec::new();
for _ in 0..8 {
let ready = ring.poll_next_ready().expect("ready");
observed_seq.push(ready.sequence);
ring.release_completed(ready).expect("release");
}
assert_eq!(observed_seq, vec![1, 2, 3, 4, 5, 6, 7, 8]);
}
#[test]
fn ninth_submit_when_full_returns_full_dropped() {
let mut ring = make_ring();
for _ in 0..8 {
ring.submit(fill_noop).expect("submit");
}
let err = ring.submit(fill_noop).err();
assert!(matches!(
err,
Some(RingError::FullDropped { dropped_so_far: 1 })
));
assert_eq!(ring.dropped_count(), 1);
let err2 = ring.submit(fill_noop).err();
assert!(matches!(
err2,
Some(RingError::FullDropped { dropped_so_far: 2 })
));
assert_eq!(ring.dropped_count(), 2);
}
#[test]
fn release_completed_returns_slot_to_pool() {
let mut ring = make_ring();
ring.submit(fill_noop).expect("submit");
let ready = ring.poll_next_ready().expect("ready");
assert_eq!(ring.free_count(), 7);
ring.release_completed(ready).expect("release");
assert_eq!(ring.free_count(), 8);
ring.submit(fill_noop).expect("re-submit after release");
}
#[test]
fn pair_asserts_pass_under_random_submit_poll_release() {
let mut ring = make_ring();
let mut state: u64 = 0xcafef00d;
let mut in_flight: Vec<EncoderReady<CpuSlotHandle>> = Vec::new();
for _ in 0..1000 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let action = state % 3;
match action {
0 => {
let _ = ring.submit(fill_noop);
}
1 => {
if let Some(r) = ring.poll_next_ready() {
in_flight.push(r);
}
}
_ => {
if let Some(r) = in_flight.pop() {
ring.release_completed(r).expect("release");
}
}
}
let metrics = ring.metrics();
assert!(metrics.completed_count <= metrics.submitted_count);
assert!(metrics.dispatched_count <= metrics.completed_count);
}
while let Some(r) = in_flight.pop() {
ring.release_completed(r).expect("drain release");
}
while let Some(r) = ring.poll_next_ready() {
ring.release_completed(r).expect("drain release post poll");
}
assert_eq!(ring.free_count(), ring.capacity());
}
#[test]
#[allow(clippy::panic)]
fn submit_with_panicking_closure_leaves_state_consistent() {
use std::panic::{AssertUnwindSafe, catch_unwind};
let mut ring = make_ring();
ring.submit(fill_noop).expect("first submit ok");
let result = catch_unwind(AssertUnwindSafe(|| {
let _ = ring.submit(|_h: &mut CpuSlotHandle| panic!("user fill panicked"));
}));
assert!(result.is_err());
let metrics = ring.metrics();
assert!(metrics.submitted_count >= 1);
assert!(metrics.completed_count <= metrics.submitted_count);
assert!(metrics.dispatched_count <= metrics.completed_count);
let ready = ring.poll_next_ready().expect("first frame still pollable");
assert_eq!(ready.sequence, 1);
ring.release_completed(ready).expect("release first");
}
#[test]
fn determinism_same_sequence_yields_same_release_order() {
fn run() -> Vec<u64> {
let mut ring = make_ring();
let mut released: Vec<u64> = Vec::new();
for _ in 0..8 {
ring.submit(fill_noop).expect("submit");
}
for _ in 0..8 {
let r = ring.poll_next_ready().expect("ready");
released.push(r.sequence);
ring.release_completed(r).expect("release");
}
released
}
let a = run();
let b = run();
assert_eq!(a, b);
assert_eq!(a, vec![1, 2, 3, 4, 5, 6, 7, 8]);
}
#[test]
fn capacity_is_eight() {
let ring = make_ring();
assert_eq!(ring.capacity(), 8);
assert_eq!(ring.free_count(), 8);
}
#[test]
fn double_release_of_same_ready_rejected() {
let mut ring = make_ring();
ring.submit(fill_noop).expect("submit");
let ready = ring.poll_next_ready().expect("ready");
let cloned = EncoderReady {
handle: ready.handle.clone(),
sequence: ready.sequence,
slot_index: ready.slot_index,
duplicate_count: ready.duplicate_count,
};
ring.release_completed(ready).expect("first release");
let err = ring.release_completed(cloned).err();
assert!(matches!(err, Some(RingError::UnexpectedSlotState { .. })));
}
#[test]
fn submit_before_init_returns_not_initialised() {
let mut ring: EncoderInputRing<CpuMemcpyBackend> =
EncoderInputRing::new(CpuMemcpyBackend::new());
let err = ring.submit(fill_noop).err();
assert!(matches!(err, Some(RingError::NotInitialised)));
}
#[test]
fn re_initialise_rejected() {
let mut ring = make_ring();
let err = ring.initialise(64, 64, TextureFormat::Nv12).err();
assert!(matches!(err, Some(RingError::AlreadyInitialised)));
}
#[test]
fn poll_when_empty_returns_none() {
let mut ring = make_ring();
assert!(ring.poll_next_ready().is_none());
}
#[test]
fn skip_oldest_rejects_when_every_slot_is_dispatched() {
let mut ring = make_ring();
for _ in 0..8 {
ring.submit(fill_noop).expect("submit");
}
let mut in_flight: Vec<EncoderReady<CpuSlotHandle>> = Vec::new();
for _ in 0..8 {
in_flight.push(ring.poll_next_ready().expect("ready"));
}
assert_eq!(ring.free_count(), 0);
let err = ring.submit_skip_oldest(fill_noop).err();
assert!(matches!(
err,
Some(RingError::FullDropped { dropped_so_far: 1 })
));
assert_eq!(ring.dropped_count(), 1);
assert_eq!(ring.dispatched_count(), 8);
for ready in in_flight.drain(..) {
ring.release_completed(ready).expect("release");
}
assert_eq!(ring.free_count(), 8);
ring.submit_skip_oldest(fill_noop)
.expect("submit succeeds once dispatched slots are released");
}
#[test]
fn skip_oldest_evicts_oldest_submitted_never_dispatched() {
let mut ring = make_ring();
for _ in 0..8 {
ring.submit(fill_noop).expect("submit");
}
let mut dispatched: Vec<EncoderReady<CpuSlotHandle>> = Vec::new();
for _ in 0..3 {
dispatched.push(ring.poll_next_ready().expect("ready"));
}
assert_eq!(dispatched[0].sequence, 1);
assert_eq!(dispatched[2].sequence, 3);
ring.submit_skip_oldest(fill_noop)
.expect("skip-oldest evicts a submitted slot");
assert_eq!(ring.dropped_count(), 1);
assert_eq!(ring.submitted_count(), 9);
let mut remaining: Vec<u64> = Vec::new();
while let Some(ready) = ring.poll_next_ready() {
remaining.push(ready.sequence);
ring.release_completed(ready).expect("release");
}
assert_eq!(remaining, vec![5, 6, 7, 8, 9], "sequence 4 was evicted");
for ready in dispatched.drain(..) {
ring.release_completed(ready).expect("release dispatched");
}
assert_eq!(ring.free_count(), 8);
}
#[test]
fn reserve_then_commit_matches_submit_semantics() {
let mut ring = make_ring();
let reservation = ring.reserve().expect("reserve");
assert_eq!(reservation.slot_index(), 0);
assert_eq!(ring.free_count(), 7);
assert_eq!(ring.submitted_count(), 0, "sequence assigned at commit");
let sequence = ring.commit(reservation).expect("commit");
assert_eq!(sequence, 1);
let ready = ring.poll_next_ready().expect("ready");
assert_eq!(ready.sequence, 1);
assert_eq!(ready.duplicate_count, 0);
ring.release_completed(ready).expect("release");
}
#[test]
fn cancel_returns_slot_to_free_without_sequence() {
let mut ring = make_ring();
let reservation = ring.reserve().expect("reserve");
ring.cancel(reservation).expect("cancel");
assert_eq!(ring.free_count(), 8);
assert_eq!(ring.submitted_count(), 0);
assert!(ring.poll_next_ready().is_none());
ring.submit(fill_noop).expect("submit after cancel");
let ready = ring.poll_next_ready().expect("ready");
assert_eq!(ready.sequence, 1);
ring.release_completed(ready).expect("release");
}
#[test]
fn commit_of_freed_reservation_rejected() {
let mut ring = make_ring();
let first = ring.reserve().expect("reserve");
let index = first.slot_index();
ring.cancel(first).expect("cancel");
let second = ring.reserve().expect("re-reserve");
assert_eq!(second.slot_index(), index, "same slot reused");
ring.commit(second).expect("commit reused slot");
let ready = ring.poll_next_ready().expect("ready");
let stale = EncoderReady {
handle: ready.handle.clone(),
sequence: ready.sequence,
slot_index: ready.slot_index,
duplicate_count: ready.duplicate_count,
};
ring.release_completed(ready).expect("release");
let err = ring.release_completed(stale).err();
assert!(matches!(err, Some(RingError::UnexpectedSlotState { .. })));
}
#[test]
fn skip_oldest_never_evicts_filling_slot() {
let mut ring = make_ring();
for _ in 0..7 {
ring.submit(fill_noop).expect("submit");
}
let reservation = ring.reserve().expect("reserve eighth slot");
let reserved_index = reservation.slot_index();
ring.submit_skip_oldest(fill_noop)
.expect("skip-oldest evicts a submitted slot");
assert_eq!(ring.dropped_count(), 1);
let sequence = ring.commit(reservation).expect("commit survives eviction");
assert_eq!(sequence, 9);
let mut seen_indices: Vec<u32> = Vec::new();
while let Some(ready) = ring.poll_next_ready() {
seen_indices.push(ready.slot_index);
ring.release_completed(ready).expect("release");
}
assert!(seen_indices.contains(&reserved_index));
}
#[test]
fn full_ring_submit_folds_lag_into_newest_submitted() {
let mut ring = make_ring();
for _ in 0..8 {
ring.submit(fill_noop).expect("submit");
}
let err = ring.submit(fill_noop).err();
assert!(matches!(
err,
Some(RingError::FullDropped { dropped_so_far: 1 })
));
assert_eq!(ring.metrics().lagged_count, 1);
let mut by_sequence: Vec<(u64, u32)> = Vec::new();
while let Some(ready) = ring.poll_next_ready() {
by_sequence.push((ready.sequence, ready.duplicate_count));
ring.release_completed(ready).expect("release");
}
assert_eq!(by_sequence.len(), 8);
for (sequence, duplicate_count) in by_sequence.iter().take(7) {
assert_eq!(*duplicate_count, 0, "sequence {sequence} not duplicated");
}
assert_eq!(by_sequence[7], (8, 1), "newest carries the lagged frame");
}
#[test]
fn eviction_conserves_duplicate_timing_slots() {
let mut ring = make_ring();
for _ in 0..8 {
ring.submit(fill_noop).expect("submit");
}
let mut dispatched: Vec<EncoderReady<CpuSlotHandle>> = Vec::new();
for _ in 0..7 {
dispatched.push(ring.poll_next_ready().expect("ready"));
}
let _ = ring.submit(fill_noop).err();
ring.submit_skip_oldest(fill_noop)
.expect("evicts the only submitted slot");
let ready = ring.poll_next_ready().expect("new frame ready");
assert_eq!(ready.sequence, 9);
assert_eq!(
ready.duplicate_count, 2,
"evicted frame plus its duplicate folded into successor"
);
assert_eq!(ring.metrics().lagged_count, 2);
ring.release_completed(ready).expect("release");
for ready in dispatched.drain(..) {
ring.release_completed(ready).expect("release dispatched");
}
}
#[test]
fn all_dispatched_lag_attaches_to_next_submission() {
let mut ring = make_ring();
for _ in 0..8 {
ring.submit(fill_noop).expect("submit");
}
let mut in_flight: Vec<EncoderReady<CpuSlotHandle>> = Vec::new();
for _ in 0..8 {
in_flight.push(ring.poll_next_ready().expect("ready"));
}
for _ in 0..2 {
let err = ring.submit_skip_oldest(fill_noop).err();
assert!(matches!(err, Some(RingError::FullDropped { .. })));
}
assert_eq!(ring.metrics().lagged_count, 2);
let first = in_flight.remove(0);
ring.release_completed(first).expect("release one");
ring.submit_skip_oldest(fill_noop)
.expect("submit after free");
let ready = ring.poll_next_ready().expect("ready");
assert_eq!(ready.sequence, 9);
assert_eq!(ready.duplicate_count, 2, "pending lag attached");
ring.release_completed(ready).expect("release");
for ready in in_flight.drain(..) {
ring.release_completed(ready).expect("release in flight");
}
}
#[test]
fn duplicate_count_saturates_at_named_cap() {
let mut ring = make_ring();
for _ in 0..8 {
ring.submit(fill_noop).expect("submit");
}
for _ in 0..(DUPLICATE_COUNT_MAX + 5) {
let _ = ring.submit(fill_noop).err();
}
let mut last: Option<(u64, u32)> = None;
while let Some(ready) = ring.poll_next_ready() {
last = Some((ready.sequence, ready.duplicate_count));
ring.release_completed(ready).expect("release");
}
assert_eq!(
last,
Some((8, DUPLICATE_COUNT_MAX)),
"duplicates saturate at cap"
);
}
#[test]
fn skip_oldest_repeated_rejection_counts_every_drop() {
let mut ring = make_ring();
for _ in 0..8 {
ring.submit(fill_noop).expect("submit");
}
let mut in_flight: Vec<EncoderReady<CpuSlotHandle>> = Vec::new();
for _ in 0..8 {
in_flight.push(ring.poll_next_ready().expect("ready"));
}
for expected_drops in 1..=3_u64 {
let err = ring.submit_skip_oldest(fill_noop).err();
assert!(matches!(
err,
Some(RingError::FullDropped { dropped_so_far }) if dropped_so_far == expected_drops
));
}
assert_eq!(ring.dropped_count(), 3);
while let Some(ready) = in_flight.pop() {
ring.release_completed(ready).expect("release");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use fluxer_encoder_ring::{
CpuMemcpyBackend, CpuSlotHandle, EncoderInputRing, RING_SIZE, TextureFormat,
};
fn fill_noop(_: &mut CpuSlotHandle) {}
#[test]
fn submit_skip_oldest_drops_first_half_when_pushed_twice_capacity() {
let mut ring = EncoderInputRing::new(CpuMemcpyBackend::new());
ring.initialise(64, 64, TextureFormat::Nv12)
.expect("init ring");
let capacity = ring.capacity();
assert_eq!(capacity, RING_SIZE);
assert_eq!(capacity, 8);
let total_pushes: u64 = 2 * capacity as u64;
for _ in 0..total_pushes {
ring.submit_skip_oldest(fill_noop)
.expect("skip-oldest accepts every push");
}
assert_eq!(ring.submitted_count(), total_pushes);
assert_eq!(ring.dropped_count(), capacity as u64);
assert_eq!(ring.free_count(), 0);
let mut observed: Vec<u64> = Vec::with_capacity(capacity);
for _ in 0..capacity {
let ready = ring.poll_next_ready().expect("ready");
observed.push(ready.sequence);
ring.release_completed(ready).expect("release");
}
let expected: Vec<u64> = (capacity as u64 + 1..=total_pushes).collect();
assert_eq!(
observed, expected,
"skip-don't-block kept newest {capacity} frames"
);
assert_eq!(ring.free_count(), capacity);
assert!(ring.poll_next_ready().is_none());
}
#[test]
fn submit_skip_oldest_succeeds_when_room_available() {
let mut ring = EncoderInputRing::new(CpuMemcpyBackend::new());
ring.initialise(64, 64, TextureFormat::Nv12)
.expect("init ring");
for _ in 0..3 {
ring.submit_skip_oldest(fill_noop).expect("submit");
}
assert_eq!(ring.dropped_count(), 0);
assert_eq!(ring.submitted_count(), 3);
assert_eq!(ring.free_count(), ring.capacity() - 3);
}
#[test]
fn submit_skip_oldest_keeps_metrics_consistent() {
let mut ring = EncoderInputRing::new(CpuMemcpyBackend::new());
ring.initialise(64, 64, TextureFormat::Nv12)
.expect("init ring");
for _ in 0..(3 * ring.capacity() as u64) {
ring.submit_skip_oldest(fill_noop).expect("submit");
}
let metrics = ring.metrics();
assert_eq!(metrics.submitted_count, 3 * ring.capacity() as u64);
assert_eq!(metrics.dropped_count, 2 * ring.capacity() as u64);
assert!(metrics.completed_count <= metrics.submitted_count);
}
@@ -0,0 +1,727 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![cfg(target_os = "windows")]
use std::ffi::c_void;
use std::ptr;
use libloading::{Library, Symbol};
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_11_0};
use windows::Win32::Graphics::Direct3D11::{
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_SDK_VERSION,
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Multithread,
};
use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1};
use windows::core::Interface;
const VENDOR_INTEL: u32 = 0x8086;
const MFX_IMPL_TYPE_HARDWARE: u32 = 2;
const MFX_ACCEL_MODE_NA: u32 = 0;
const MFX_ACCEL_MODE_VIA_D3D11: u32 = 0x0300;
const MFX_HANDLE_D3D11_DEVICE: u32 = 3;
const MFX_HANDLE_D3D11_VIDEO_DEVICE_GUESS: u32 = 11;
const MFX_VARIANT_TYPE_U32: u32 = 5;
const MFX_VARIANT_VERSION_MAJOR: u8 = 1;
const MFX_VARIANT_VERSION_MINOR: u8 = 1;
const MFX_ERR_NONE: i32 = 0;
const MFX_WRN_IN_EXECUTION: i32 = 1;
const MFX_FOURCC_NV12: u32 = u32::from_le_bytes(*b"NV12");
const MFX_CODEC_AVC: u32 = u32::from_le_bytes(*b"AVC ");
const MFX_RATECONTROL_CBR: u16 = 1;
const MFX_PICSTRUCT_PROGRESSIVE: u16 = 0x01;
const MFX_CHROMAFORMAT_YUV420: u16 = 1;
const MFX_IOPATTERN_IN_VIDEO_MEMORY: u16 = 0x01;
const MFX_IOPATTERN_IN_SYSTEM_MEMORY: u16 = 0x02;
const FILTER_PROPERTY_IMPL: &[u8] = b"mfxImplDescription.Impl\0";
const FILTER_PROPERTY_ACCEL: &[u8] = b"mfxImplDescription.AccelerationMode\0";
const QSV_DLL_NAME_VPL: &str = "libvpl.dll";
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct MfxStructVersion {
minor: u8,
major: u8,
}
#[repr(C)]
#[derive(Clone, Copy)]
union MfxVariantData {
u32_: u32,
u64_: u64,
ptr: *mut c_void,
pad: [u8; 16],
}
#[repr(C)]
#[derive(Clone, Copy)]
struct MfxVariant {
version: MfxStructVersion,
type_: u32,
data: MfxVariantData,
}
#[repr(C)]
#[derive(Default)]
struct MfxFrameInfo {
reserved: [u32; 4],
channel_id: u16,
bit_depth_luma: u16,
bit_depth_chroma: u16,
shift: u16,
frame_id_temporal: u16,
frame_id_priority: u16,
frame_id_view: u16,
frame_id_quality: u16,
four_cc: u32,
width: u16,
height: u16,
crop_x: u16,
crop_y: u16,
crop_w: u16,
crop_h: u16,
frame_rate_extn: u32,
frame_rate_extd: u32,
reserved3: u16,
aspect_ratio_w: u16,
aspect_ratio_h: u16,
pic_struct: u16,
chroma_format: u16,
reserved2: u16,
}
#[repr(C)]
#[derive(Default)]
struct MfxInfoMfx {
reserved: [u32; 7],
low_power: u16,
brc_param_multiplier: u16,
frame_info: MfxFrameInfo,
codec_id: u32,
codec_profile: u16,
codec_level: u16,
num_thread: u16,
target_usage: u16,
gop_pic_size: u16,
gop_ref_dist: u16,
gop_opt_flag: u16,
idr_interval: u16,
rate_control_method: u16,
init_qp: u16,
buffer_size_in_kb: u16,
target_kbps: u16,
max_kbps: u16,
num_slice: u16,
num_ref_frame: u16,
encoded_order: u16,
union_pad: [u16; 15],
}
#[repr(C)]
struct MfxVideoParam {
alloc_id: u32,
reserved: [u32; 2],
reserved3: u16,
async_depth: u16,
mfx: MfxInfoMfx,
protected: u16,
io_pattern: u16,
ext_param: *mut c_void,
num_ext_param: u16,
reserved2: u16,
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct MfxPlatform {
code_name: u16,
device_id: u16,
media_adapter_type: u16,
reserved: [u16; 13],
}
type MfxLoad = unsafe extern "C" fn() -> *mut c_void;
type MfxUnload = unsafe extern "C" fn(*mut c_void);
type MfxCreateConfig = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
type MfxSetConfigFilterProperty = unsafe extern "C" fn(*mut c_void, *const u8, MfxVariant) -> i32;
type MfxCreateSession = unsafe extern "C" fn(*mut c_void, u32, *mut *mut c_void) -> i32;
type MfxClose = unsafe extern "C" fn(*mut c_void) -> i32;
type MfxSetHandle = unsafe extern "C" fn(*mut c_void, u32, *mut c_void) -> i32;
type MfxQueryImpl = unsafe extern "C" fn(*mut c_void, *mut i32) -> i32;
type MfxQueryPlatform = unsafe extern "C" fn(*mut c_void, *mut MfxPlatform) -> i32;
type MfxEncodeInit = unsafe extern "C" fn(*mut c_void, *mut MfxVideoParam) -> i32;
type MfxEncodeClose = unsafe extern "C" fn(*mut c_void) -> i32;
type MfxEncodeQuery =
unsafe extern "C" fn(*mut c_void, *mut MfxVideoParam, *mut MfxVideoParam) -> i32;
type MfxGetHandle = unsafe extern "C" fn(*mut c_void, u32, *mut *mut c_void) -> i32;
struct Dispatcher {
library: Library,
}
impl Dispatcher {
fn load() -> Option<Self> {
let library = unsafe { Library::new(QSV_DLL_NAME_VPL) }.ok()?;
Some(Self { library })
}
fn modern_session(&self, accel_mode: u32) -> (Option<(*mut c_void, *mut c_void)>, Vec<String>) {
let mut log = Vec::new();
let load: Symbol<'_, MfxLoad> = match unsafe { self.library.get(b"MFXLoad\0") } {
Ok(s) => s,
Err(e) => {
log.push(format!("MFXLoad symbol missing: {e:?}"));
return (None, log);
}
};
let create_config: Symbol<'_, MfxCreateConfig> =
match unsafe { self.library.get(b"MFXCreateConfig\0") } {
Ok(s) => s,
Err(e) => {
log.push(format!("MFXCreateConfig missing: {e:?}"));
return (None, log);
}
};
let set_prop: Symbol<'_, MfxSetConfigFilterProperty> =
match unsafe { self.library.get(b"MFXSetConfigFilterProperty\0") } {
Ok(s) => s,
Err(e) => {
log.push(format!("MFXSetConfigFilterProperty missing: {e:?}"));
return (None, log);
}
};
let create_session: Symbol<'_, MfxCreateSession> =
match unsafe { self.library.get(b"MFXCreateSession\0") } {
Ok(s) => s,
Err(e) => {
log.push(format!("MFXCreateSession missing: {e:?}"));
return (None, log);
}
};
let loader = unsafe { load() };
if loader.is_null() {
log.push("MFXLoad returned NULL".to_string());
return (None, log);
}
log.push(format!("MFXLoad ok loader={loader:p}"));
let impl_cfg = unsafe { create_config(loader) };
if impl_cfg.is_null() {
log.push("MFXCreateConfig(impl) returned NULL".to_string());
return (None, log);
}
let impl_variant = MfxVariant {
version: MfxStructVersion {
minor: MFX_VARIANT_VERSION_MINOR,
major: MFX_VARIANT_VERSION_MAJOR,
},
type_: MFX_VARIANT_TYPE_U32,
data: MfxVariantData {
u32_: MFX_IMPL_TYPE_HARDWARE,
},
};
let status_impl =
unsafe { set_prop(impl_cfg, FILTER_PROPERTY_IMPL.as_ptr(), impl_variant) };
log.push(format!(
"SetConfigFilterProperty(Impl=HARDWARE) status={status_impl}"
));
if status_impl != MFX_ERR_NONE {
return (None, log);
}
let accel_cfg = unsafe { create_config(loader) };
if accel_cfg.is_null() {
log.push("MFXCreateConfig(accel) returned NULL".to_string());
return (None, log);
}
let accel_variant = MfxVariant {
version: MfxStructVersion {
minor: MFX_VARIANT_VERSION_MINOR,
major: MFX_VARIANT_VERSION_MAJOR,
},
type_: MFX_VARIANT_TYPE_U32,
data: MfxVariantData { u32_: accel_mode },
};
let status_accel =
unsafe { set_prop(accel_cfg, FILTER_PROPERTY_ACCEL.as_ptr(), accel_variant) };
log.push(format!(
"SetConfigFilterProperty(AccelerationMode={accel_mode:#x}) status={status_accel}"
));
if status_accel != MFX_ERR_NONE {
return (None, log);
}
let mut session: *mut c_void = ptr::null_mut();
let status_create = unsafe { create_session(loader, 0, &mut session) };
log.push(format!(
"MFXCreateSession status={status_create} session={session:p}"
));
if status_create != MFX_ERR_NONE || session.is_null() {
return (None, log);
}
(Some((loader, session)), log)
}
fn unload(&self, loader: *mut c_void) {
let Ok(unload) = (unsafe { self.library.get::<MfxUnload>(b"MFXUnload\0") }) else {
return;
};
if !loader.is_null() {
unsafe { unload(loader) };
}
}
fn close(&self, session: *mut c_void) {
let Ok(close) = (unsafe { self.library.get::<MfxClose>(b"MFXClose\0") }) else {
return;
};
if !session.is_null() {
unsafe {
let _ = close(session);
}
}
}
}
fn try_create_intel_device() -> Option<(ID3D11Device, ID3D11DeviceContext)> {
let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1() }.ok()?;
let mut idx: u32 = 0;
loop {
let adapter: IDXGIAdapter1 = match unsafe { factory.EnumAdapters1(idx) } {
Ok(a) => a,
Err(_) => return None,
};
idx = idx.saturating_add(1);
let desc = match unsafe { adapter.GetDesc1() } {
Ok(d) => d,
Err(_) => continue,
};
if desc.VendorId != VENDOR_INTEL {
continue;
}
let mut device: Option<ID3D11Device> = None;
let mut ctx: Option<ID3D11DeviceContext> = None;
let feature_levels = [D3D_FEATURE_LEVEL_11_0];
let cast_result = adapter.cast::<windows::Win32::Graphics::Dxgi::IDXGIAdapter>();
let cast_adapter = match cast_result {
Ok(a) => a,
Err(_) => continue,
};
let flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
let result = unsafe {
D3D11CreateDevice(
Some(&cast_adapter),
D3D_DRIVER_TYPE_UNKNOWN,
Default::default(),
flags,
Some(&feature_levels),
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut ctx),
)
};
if let (Ok(()), Some(d), Some(c)) = (result, device, ctx) {
if let Ok(mt) = d.cast::<ID3D11Multithread>() {
let _ = unsafe { mt.SetMultithreadProtected(true) };
}
return Some((d, c));
}
}
}
fn build_video_params_with(
width: u16,
height: u16,
io_pattern: u16,
target_usage: u16,
low_power: u16,
) -> MfxVideoParam {
let aligned_w = (width + 15) & !15;
let aligned_h = (height + 15) & !15;
let info = MfxFrameInfo {
four_cc: MFX_FOURCC_NV12,
width: aligned_w,
height: aligned_h,
crop_w: width,
crop_h: height,
frame_rate_extn: 30,
frame_rate_extd: 1,
aspect_ratio_w: 1,
aspect_ratio_h: 1,
pic_struct: MFX_PICSTRUCT_PROGRESSIVE,
chroma_format: MFX_CHROMAFORMAT_YUV420,
..Default::default()
};
let mfx = MfxInfoMfx {
frame_info: info,
codec_id: MFX_CODEC_AVC,
target_usage,
low_power,
gop_pic_size: 30,
gop_ref_dist: 1,
rate_control_method: MFX_RATECONTROL_CBR,
target_kbps: 5_000,
max_kbps: 5_000,
num_slice: 1,
num_ref_frame: 1,
..Default::default()
};
MfxVideoParam {
alloc_id: 0,
reserved: [0; 2],
reserved3: 0,
async_depth: 1,
mfx,
protected: 0,
io_pattern,
ext_param: ptr::null_mut(),
num_ext_param: 0,
reserved2: 0,
}
}
fn try_set_handle(
dispatcher: &Dispatcher,
session: *mut c_void,
handle_type: u32,
handle_ptr: *mut c_void,
) -> Result<i32, String> {
let sym: Symbol<'_, MfxSetHandle> =
unsafe { dispatcher.library.get(b"MFXVideoCORE_SetHandle\0") }
.map_err(|e| format!("MFXVideoCORE_SetHandle missing: {e:?}"))?;
let status = unsafe { sym(session, handle_type, handle_ptr) };
Ok(status)
}
fn try_query_impl(dispatcher: &Dispatcher, session: *mut c_void) -> Result<(i32, i32), String> {
let sym: Symbol<'_, MfxQueryImpl> = unsafe { dispatcher.library.get(b"MFXQueryIMPL\0") }
.map_err(|e| format!("MFXQueryIMPL missing: {e:?}"))?;
let mut out: i32 = 0;
let status = unsafe { sym(session, &mut out) };
Ok((status, out))
}
fn try_query_platform(
dispatcher: &Dispatcher,
session: *mut c_void,
) -> Result<(i32, MfxPlatform), String> {
let sym: Symbol<'_, MfxQueryPlatform> =
unsafe { dispatcher.library.get(b"MFXVideoCORE_QueryPlatform\0") }
.map_err(|e| format!("MFXVideoCORE_QueryPlatform missing: {e:?}"))?;
let mut p = MfxPlatform::default();
let status = unsafe { sym(session, &mut p) };
Ok((status, p))
}
fn try_encode_init(
dispatcher: &Dispatcher,
session: *mut c_void,
width: u16,
height: u16,
io_pattern: u16,
) -> Result<i32, String> {
try_encode_init_with(dispatcher, session, width, height, io_pattern, 4, 0)
}
fn try_encode_init_with(
dispatcher: &Dispatcher,
session: *mut c_void,
width: u16,
height: u16,
io_pattern: u16,
target_usage: u16,
low_power: u16,
) -> Result<i32, String> {
let query: Symbol<'_, MfxEncodeQuery> =
unsafe { dispatcher.library.get(b"MFXVideoENCODE_Query\0") }
.map_err(|e| format!("MFXVideoENCODE_Query missing: {e:?}"))?;
let init: Symbol<'_, MfxEncodeInit> =
unsafe { dispatcher.library.get(b"MFXVideoENCODE_Init\0") }
.map_err(|e| format!("MFXVideoENCODE_Init missing: {e:?}"))?;
let mut params = build_video_params_with(width, height, io_pattern, target_usage, low_power);
let mut query_out = build_video_params_with(width, height, io_pattern, target_usage, low_power);
let q_status = unsafe { query(session, &mut params, &mut query_out) };
eprintln!(
" MFXVideoENCODE_Query(io={io_pattern:#x},tu={target_usage},lp={low_power}) status={q_status}"
);
if q_status != MFX_ERR_NONE && q_status != MFX_WRN_IN_EXECUTION && q_status != -3 {
eprintln!(" Query returned hard error {q_status}; skipping Init to avoid AV");
return Ok(q_status);
}
let init_status = unsafe { init(session, &mut query_out) };
Ok(init_status)
}
fn try_get_handle(
dispatcher: &Dispatcher,
session: *mut c_void,
handle_type: u32,
) -> Result<(i32, *mut c_void), String> {
let sym: Symbol<'_, MfxGetHandle> =
unsafe { dispatcher.library.get(b"MFXVideoCORE_GetHandle\0") }
.map_err(|e| format!("MFXVideoCORE_GetHandle missing: {e:?}"))?;
let mut out: *mut c_void = ptr::null_mut();
let status = unsafe { sym(session, handle_type, &mut out) };
Ok((status, out))
}
fn try_encode_close(dispatcher: &Dispatcher, session: *mut c_void) {
if let Ok(sym) = unsafe {
dispatcher
.library
.get::<MfxEncodeClose>(b"MFXVideoENCODE_Close\0")
} {
unsafe {
let _ = sym(session);
}
}
}
#[test]
fn qsv_tiger_lake_probe_struct_sizes() {
eprintln!("=== STRUCT SIZES (Rust) ===");
eprintln!("MfxFrameInfo = {}", std::mem::size_of::<MfxFrameInfo>());
eprintln!("MfxInfoMfx = {}", std::mem::size_of::<MfxInfoMfx>());
eprintln!("MfxVideoParam = {}", std::mem::size_of::<MfxVideoParam>());
assert!(
std::mem::size_of::<MfxFrameInfo>() == 68,
"MfxFrameInfo size must be 68"
);
assert!(
std::mem::size_of::<MfxInfoMfx>() >= 168,
"MfxInfoMfx >= 168 (= sizeof mfxInfoVPP)"
);
}
#[test]
fn qsv_tiger_lake_probe_experiment_1_preconditions() {
let dispatcher = match Dispatcher::load() {
Some(d) => d,
None => {
eprintln!("skip: libvpl.dll not loadable");
return;
}
};
eprintln!("=== EXPERIMENT 1: preconditions (QueryIMPL + QueryPlatform) ===");
let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_VIA_D3D11);
for line in &log {
eprintln!(" {line}");
}
let (loader, session) = match session_opt {
Some(s) => s,
None => {
eprintln!("EXP1 result: modern session not created; cannot probe preconditions");
return;
}
};
match try_query_impl(&dispatcher, session) {
Ok((status, impl_val)) => {
eprintln!(" MFXQueryIMPL: status={status} impl={impl_val:#x}");
}
Err(e) => eprintln!(" MFXQueryIMPL error: {e}"),
}
match try_query_platform(&dispatcher, session) {
Ok((status, p)) => {
eprintln!(
" MFXVideoCORE_QueryPlatform: status={status} code_name={} device_id={:#x} media_adapter_type={}",
p.code_name, p.device_id, p.media_adapter_type
);
}
Err(e) => eprintln!(" MFXVideoCORE_QueryPlatform error: {e}"),
}
dispatcher.close(session);
dispatcher.unload(loader);
eprintln!("EXP1 done");
}
#[test]
fn qsv_tiger_lake_probe_experiment_2_variant_accel_modes() {
let dispatcher = match Dispatcher::load() {
Some(d) => d,
None => {
eprintln!("skip: libvpl.dll not loadable");
return;
}
};
let (device, _ctx) = match try_create_intel_device() {
Some(d) => d,
None => {
eprintln!("skip: no Intel D3D11 device");
return;
}
};
eprintln!("=== EXPERIMENT 2: SetHandle handle-type variants ===");
let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_VIA_D3D11);
for line in &log {
eprintln!(" {line}");
}
let (loader, session) = match session_opt {
Some(s) => s,
None => {
eprintln!("EXP2 result: modern session not created");
return;
}
};
let raw_device = device.as_raw();
let exp2a = try_set_handle(&dispatcher, session, MFX_HANDLE_D3D11_DEVICE, raw_device);
eprintln!(
" EXP2a: SetHandle(MFX_HANDLE_D3D11_DEVICE=3) -> {:?}",
exp2a
);
let exp2b = try_set_handle(
&dispatcher,
session,
MFX_HANDLE_D3D11_VIDEO_DEVICE_GUESS,
raw_device,
);
eprintln!(
" EXP2b: SetHandle(MFX_HANDLE_D3D11_VIDEO_DEVICE_GUESS=11) -> {:?}",
exp2b
);
dispatcher.close(session);
dispatcher.unload(loader);
eprintln!("EXP2 done");
assert!(exp2a.is_ok(), "MFXVideoCORE_SetHandle symbol resolves");
}
#[test]
fn qsv_tiger_lake_probe_experiment_3_skip_set_handle() {
let dispatcher = match Dispatcher::load() {
Some(d) => d,
None => {
eprintln!("skip: libvpl.dll not loadable");
return;
}
};
let (device, _ctx) = match try_create_intel_device() {
Some(d) => d,
None => {
eprintln!("skip: no Intel D3D11 device");
return;
}
};
eprintln!("=== EXPERIMENT 3: SetHandle with VIDEO_SUPPORT device + encoder init ===");
let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_VIA_D3D11);
for line in &log {
eprintln!(" {line}");
}
let (loader, session) = match session_opt {
Some(s) => s,
None => {
eprintln!("EXP3 result: modern session not created");
return;
}
};
match try_query_impl(&dispatcher, session) {
Ok((status, impl_val)) => {
eprintln!(" pre-set MFXQueryIMPL: status={status} impl={impl_val:#x}");
}
Err(e) => eprintln!(" MFXQueryIMPL error: {e}"),
}
let raw_device = device.as_raw();
let set_status = try_set_handle(&dispatcher, session, MFX_HANDLE_D3D11_DEVICE, raw_device);
eprintln!(
" EXP3-set: SetHandle(D3D11_DEVICE=3, VIDEO_SUPPORT+BGRA, multithread-protected) -> {:?}",
set_status
);
match try_get_handle(&dispatcher, session, MFX_HANDLE_D3D11_DEVICE) {
Ok((status, hdl)) => {
eprintln!(
" post-set GetHandle(MFX_HANDLE_D3D11_DEVICE) -> status={status} handle={hdl:p}"
);
}
Err(e) => eprintln!(" GetHandle error: {e}"),
}
let init_a = try_encode_init(
&dispatcher,
session,
1280,
720,
MFX_IOPATTERN_IN_VIDEO_MEMORY,
);
eprintln!(
" EXP3a (IO_VIDEO_MEMORY, target_usage=4, low_power=0): init -> {:?}",
init_a
);
let init_b = try_encode_init_with(
&dispatcher,
session,
1280,
720,
MFX_IOPATTERN_IN_VIDEO_MEMORY,
7,
0x10,
);
eprintln!(
" EXP3b (IO_VIDEO_MEMORY, target_usage=7 best-speed, low_power=ON=0x10): init -> {:?}",
init_b
);
let init_c = try_encode_init_with(
&dispatcher,
session,
1280,
720,
MFX_IOPATTERN_IN_VIDEO_MEMORY,
4,
0x10,
);
eprintln!(
" EXP3c (IO_VIDEO_MEMORY, target_usage=4 balanced, low_power=ON=0x10): init -> {:?}",
init_c
);
try_encode_close(&dispatcher, session);
dispatcher.close(session);
dispatcher.unload(loader);
eprintln!("EXP3 done");
assert!(init_a.is_ok(), "init_a symbol resolved");
assert!(init_b.is_ok(), "init_b symbol resolved");
assert!(init_c.is_ok(), "init_c symbol resolved");
}
#[test]
fn qsv_tiger_lake_probe_experiment_4_software_fallback() {
let dispatcher = match Dispatcher::load() {
Some(d) => d,
None => {
eprintln!("skip: libvpl.dll not loadable");
return;
}
};
eprintln!("=== EXPERIMENT 4: software-only (ACCEL_MODE_NA) session ===");
let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_NA);
for line in &log {
eprintln!(" {line}");
}
let (loader, session) = match session_opt {
Some(s) => s,
None => {
eprintln!("EXP4 result: software session not created");
return;
}
};
match try_query_impl(&dispatcher, session) {
Ok((status, impl_val)) => {
eprintln!(" MFXQueryIMPL: status={status} impl={impl_val:#x}");
}
Err(e) => eprintln!(" MFXQueryIMPL error: {e}"),
}
let init_status = try_encode_init(
&dispatcher,
session,
1280,
720,
MFX_IOPATTERN_IN_SYSTEM_MEMORY,
);
eprintln!(" EXP4: MFXVideoENCODE_Init software -> {:?}", init_status);
try_encode_close(&dispatcher, session);
dispatcher.close(session);
dispatcher.unload(loader);
eprintln!("EXP4 done");
assert!(
init_status.is_ok(),
"encode init symbol resolved in software"
);
}
@@ -0,0 +1,551 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![cfg(target_os = "windows")]
use fluxer_encoder_ring::NVENC_COMPLETION_RING_CAPACITY;
use fluxer_encoder_ring::encoder_handoff::EncoderCompletionCallback;
use fluxer_encoder_ring::{
AmfD3D11Handoff, AmfHandoff, EncodedBitstream, EncoderDims, EncoderError, EncoderSubmission,
NvencD3D11Handoff, NvencHandoff, PicParams, QsvD3D11Handoff, QsvHandoff, RingError,
};
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_UNKNOWN, 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_KEYEDMUTEX,
D3D11_RESOURCE_MISC_SHARED_NTHANDLE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
D3D11_USAGE_DEFAULT, D3D11CreateDevice, ID3D11Device, ID3D11Multithread,
};
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC};
use windows::Win32::Graphics::Dxgi::{
CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIResource1,
};
use windows::core::Interface;
const VENDOR_NVIDIA: u32 = 0x10DE;
const VENDOR_AMD: u32 = 0x1002;
const VENDOR_INTEL: u32 = 0x8086;
fn enum_adapters() -> Vec<IDXGIAdapter1> {
let factory_result: windows::core::Result<IDXGIFactory1> = unsafe { CreateDXGIFactory1() };
let factory = match factory_result {
Ok(f) => f,
Err(_) => return Vec::new(),
};
let mut out = Vec::new();
let mut index: u32 = 0;
loop {
let result: windows::core::Result<IDXGIAdapter1> = unsafe { factory.EnumAdapters1(index) };
match result {
Ok(adapter) => {
out.push(adapter);
index = index.saturating_add(1);
}
Err(_) => break,
}
}
out
}
fn try_create_device_for_vendor(vendor_id: u32) -> Option<ID3D11Device> {
let adapters = enum_adapters();
for adapter in adapters {
let desc = match unsafe { adapter.GetDesc1() } {
Ok(d) => d,
Err(_) => continue,
};
if desc.VendorId != vendor_id {
continue;
}
let mut device: Option<ID3D11Device> = None;
let feature_levels = [D3D_FEATURE_LEVEL_11_0];
let flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
let result = unsafe {
D3D11CreateDevice(
Some(&adapter.cast().ok()?),
D3D_DRIVER_TYPE_UNKNOWN,
Default::default(),
flags,
Some(&feature_levels),
D3D11_SDK_VERSION,
Some(&mut device),
None,
None,
)
};
if let (Ok(()), Some(d)) = (result, device) {
if let Ok(mt) = d.cast::<ID3D11Multithread>() {
let _ = unsafe { mt.SetMultithreadProtected(true) };
}
return Some(d);
}
}
None
}
fn try_create_device() -> Option<ID3D11Device> {
try_create_device_for_vendor(VENDOR_NVIDIA)
.or_else(|| try_create_device_for_vendor(VENDOR_INTEL))
.or_else(|| try_create_device_for_vendor(VENDOR_AMD))
}
fn try_create_shared_nv12(device: &ID3D11Device, width: u32, height: u32) -> Option<u64> {
let desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: 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 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
CPUAccessFlags: 0,
MiscFlags: (D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0 | D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0)
as u32,
};
let mut texture = None;
unsafe {
device
.CreateTexture2D(&desc, None, Some(&mut texture))
.ok()?;
}
let texture = texture?;
let resource: IDXGIResource1 = texture.cast().ok()?;
let shared = unsafe {
resource
.CreateSharedHandle(None, 0x3, windows::core::PCWSTR::null())
.ok()?
};
Some(shared.0 as u64)
}
#[test]
fn nvenc_real_encode_one_frame_via_dummy_shared_texture() {
let device = match try_create_device_for_vendor(VENDOR_NVIDIA) {
Some(d) => d,
None => {
eprintln!("skip: no NVIDIA D3D11 device available");
return;
}
};
let dims = EncoderDims::new(1920, 1080);
let handoff_result = NvencD3D11Handoff::new(device.clone(), dims, 5_000_000);
let mut handoff = match handoff_result {
Ok(h) => h,
Err(EncoderError::SdkNotFound { .. }) => {
eprintln!("skip: NVENC runtime DLL not available");
return;
}
Err(EncoderError::SessionInitFailed { vendor, status }) => {
eprintln!("skip: NVENC session init failed: {vendor} status={status}");
return;
}
Err(other) => {
eprintln!("skip: NVENC init unexpected error: {other:?}");
return;
}
};
let shared = match try_create_shared_nv12(&device, 1920, 1080) {
Some(h) => h,
None => {
eprintln!("skip: shared NV12 texture creation failed");
return;
}
};
let slot = match NvencHandoff::register_slot(&mut handoff, shared, 0, dims) {
Ok(s) => s,
Err(e) => {
eprintln!("skip: register_slot failed: {e:?}");
return;
}
};
let pic = PicParams::new(0, true);
let submit_result = NvencHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic);
if submit_result.is_err() {
eprintln!("skip: encode_shared_async failed: {submit_result:?}");
NvencHandoff::unregister_slot(&mut handoff, slot);
return;
}
let mut bitstream = None;
for _ in 0..200 {
bitstream = NvencHandoff::poll_completed(&mut handoff, slot);
if bitstream.is_some() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
NvencHandoff::unregister_slot(&mut handoff, slot);
let bs = match bitstream {
Some(b) => b,
None => {
eprintln!("nvenc: no bitstream produced within timeout; backend init succeeded");
return;
}
};
assert!(!bs.data.is_empty(), "non-empty bitstream");
assert!(bs.is_keyframe, "first frame must be keyframe");
}
#[test]
fn amf_real_encode_one_frame_via_dummy_shared_texture() {
let device = match try_create_device_for_vendor(VENDOR_AMD) {
Some(d) => d,
None => {
eprintln!("skip: no AMD D3D11 device available (expected on Intel/NVIDIA-only boxes)");
return;
}
};
let dims = EncoderDims::new(1920, 1080);
let handoff_result = AmfD3D11Handoff::new(device.clone(), dims, 5_000_000);
let mut handoff = match handoff_result {
Ok(h) => h,
Err(EncoderError::SdkNotFound { .. }) => {
eprintln!("skip: AMF runtime DLL not available (expected on non-AMD hardware)");
return;
}
Err(other) => {
eprintln!("skip: AMF init unexpected: {other:?}");
return;
}
};
let shared = match try_create_shared_nv12(&device, 1920, 1080) {
Some(h) => h,
None => {
eprintln!("skip: shared NV12 texture creation failed");
return;
}
};
let slot = match AmfHandoff::register_slot(&mut handoff, shared, 0, dims) {
Ok(s) => s,
Err(e) => {
eprintln!("skip: AMF register_slot failed: {e:?}");
return;
}
};
let pic = PicParams::new(0, true);
let _ = AmfHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic);
let mut bitstream = None;
for _ in 0..200 {
bitstream = AmfHandoff::poll_completed(&mut handoff, slot);
if bitstream.is_some() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
AmfHandoff::unregister_slot(&mut handoff, slot);
if let Some(bs) = bitstream {
assert!(!bs.data.is_empty(), "non-empty AMF bitstream");
} else {
eprintln!("amf: no bitstream produced within timeout; backend init succeeded");
}
}
#[test]
fn qsv_modern_dispatcher_session_init_succeeds_on_real_iris_xe() {
let device = match try_create_device_for_vendor(VENDOR_INTEL) {
Some(d) => d,
None => {
eprintln!("skip: no Intel D3D11 device available");
return;
}
};
let dims = EncoderDims::new(1280, 720);
let handoff_result = QsvD3D11Handoff::new(device, dims, 5_000_000);
match handoff_result {
Ok(_) => {
eprintln!("qsv: modern dispatcher session + SetHandle + encode_init succeeded");
}
Err(EncoderError::SdkNotFound { .. }) => {
eprintln!("skip: QSV runtime DLL not available");
}
Err(e) => {
panic!("qsv modern dispatcher init MUST succeed on Tiger Lake Iris Xe: {e:?}");
}
}
}
#[test]
fn qsv_real_encode_one_frame_via_dummy_shared_texture() {
let device = match try_create_device_for_vendor(VENDOR_INTEL) {
Some(d) => d,
None => {
eprintln!("skip: no Intel D3D11 device available");
return;
}
};
let dims = EncoderDims::new(1920, 1080);
let handoff_result = QsvD3D11Handoff::new(device.clone(), dims, 5_000_000);
let mut handoff = match handoff_result {
Ok(h) => h,
Err(EncoderError::SdkNotFound { .. }) => {
eprintln!("skip: QSV runtime DLL not available");
return;
}
Err(EncoderError::SessionInitFailed { vendor, status }) => {
eprintln!("skip: QSV session init failed: {vendor} status={status}");
return;
}
Err(other) => {
eprintln!("skip: QSV init unexpected: {other:?}");
return;
}
};
let shared = match try_create_shared_nv12(&device, 1920, 1080) {
Some(h) => h,
None => {
eprintln!("skip: shared NV12 texture creation failed");
return;
}
};
let slot = match QsvHandoff::register_slot(&mut handoff, shared, 0, dims) {
Ok(s) => s,
Err(e) => {
eprintln!("skip: QSV register_slot failed: {e:?}");
return;
}
};
let pic = PicParams::new(0, true);
let _ = QsvHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic);
let mut bitstream = None;
for _ in 0..200 {
bitstream = QsvHandoff::poll_completed(&mut handoff, slot);
if bitstream.is_some() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
QsvHandoff::unregister_slot(&mut handoff, slot);
if let Some(bs) = bitstream {
assert!(!bs.data.is_empty(), "non-empty QSV bitstream");
} else {
eprintln!("qsv: no bitstream produced within timeout; backend init succeeded");
}
}
#[test]
fn skip_dont_block_rapid_submit_then_poll_never_hangs() {
let device = match try_create_device() {
Some(d) => d,
None => return,
};
let dims = EncoderDims::new(640, 360);
let mut handoff = match NvencD3D11Handoff::new(device.clone(), dims, 1_000_000) {
Ok(h) => h,
Err(_) => return,
};
let shared = match try_create_shared_nv12(&device, 640, 360) {
Some(h) => h,
None => return,
};
let slot = match NvencHandoff::register_slot(&mut handoff, shared, 0, dims) {
Ok(s) => s,
Err(_) => return,
};
let start = std::time::Instant::now();
for i in 0..10 {
let pic = PicParams::new((i as u64) * 16_666, false);
let _ = NvencHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic);
let _ = NvencHandoff::poll_completed(&mut handoff, slot);
}
let elapsed = start.elapsed();
assert!(
elapsed.as_secs() < 5,
"10 submit+poll cycles must complete in < 5s, was {elapsed:?}"
);
NvencHandoff::unregister_slot(&mut handoff, slot);
}
#[test]
fn cleanup_on_drop_releases_resources() {
let device = match try_create_device() {
Some(d) => d,
None => return,
};
let dims = EncoderDims::new(640, 360);
{
let _handoff = match NvencD3D11Handoff::new(device.clone(), dims, 1_000_000) {
Ok(h) => h,
Err(_) => return,
};
}
let _handoff2 = NvencD3D11Handoff::new(device.clone(), dims, 1_000_000);
}
struct CountingCallback {
seen: Vec<(u64, u32)>,
}
impl EncoderCompletionCallback for CountingCallback {
fn on_complete(&mut self, sequence: u64, encoded_bytes: u32) {
self.seen.push((sequence, encoded_bytes));
}
}
#[test]
fn nvenc_compression_handoff_encodes_one_frame_via_keyed_mutex() {
let device = match try_create_device_for_vendor(VENDOR_NVIDIA) {
Some(d) => d,
None => {
eprintln!("skip: no NVIDIA D3D11 device available");
return;
}
};
let dims = EncoderDims::new(1280, 720);
let mut handoff = match NvencD3D11Handoff::new(device.clone(), dims, 2_000_000) {
Ok(h) => h,
Err(EncoderError::SdkNotFound { .. }) => {
eprintln!("skip: NVENC runtime DLL not available");
return;
}
Err(other) => {
eprintln!("skip: NVENC init unexpected: {other:?}");
return;
}
};
let shared = match try_create_shared_nv12(&device, 1280, 720) {
Some(h) => h,
None => {
eprintln!("skip: shared NV12 texture creation failed");
return;
}
};
let submission = EncoderSubmission::new(shared, 0, dims, 1);
let mut callback = CountingCallback { seen: Vec::new() };
NvencHandoff::encode_shared(&mut handoff, submission, &mut callback)
.expect("encode_shared first frame ok");
assert_eq!(callback.seen.len(), 1, "callback fired once");
assert_eq!(callback.seen[0].0, 1, "callback sequence matches");
let mut payload: Option<EncodedBitstream> = None;
for _ in 0..200 {
let slot = fluxer_encoder_ring::HandoffSlot::new(0, shared);
payload = NvencHandoff::poll_completed(&mut handoff, slot);
if payload.is_some() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
let bs = match payload {
Some(b) => b,
None => {
eprintln!("nvenc keyed-mutex: no bitstream within timeout; init succeeded");
return;
}
};
assert!(!bs.data.is_empty(), "keyed-mutex encoded payload non-empty");
assert!(bs.is_keyframe, "first frame must be IDR keyframe");
}
#[test]
fn nvenc_compression_handoff_handles_back_pressure() {
let device = match try_create_device_for_vendor(VENDOR_NVIDIA) {
Some(d) => d,
None => {
eprintln!("skip: no NVIDIA D3D11 device available");
return;
}
};
let dims = EncoderDims::new(640, 360);
let mut handoff = match NvencD3D11Handoff::new(device.clone(), dims, 1_000_000) {
Ok(h) => h,
Err(EncoderError::SdkNotFound { .. }) => {
eprintln!("skip: NVENC runtime DLL not available");
return;
}
Err(other) => {
eprintln!("skip: NVENC init unexpected: {other:?}");
return;
}
};
let shared = match try_create_shared_nv12(&device, 640, 360) {
Some(h) => h,
None => {
eprintln!("skip: shared NV12 texture creation failed");
return;
}
};
let total: u64 = (NVENC_COMPLETION_RING_CAPACITY as u64) + 8;
let mut callback = CountingCallback { seen: Vec::new() };
let mut accepted = 0usize;
let mut full_drops = 0usize;
for seq in 1..=total {
let submission = EncoderSubmission::new(shared, 0, dims, seq);
match NvencHandoff::encode_shared(&mut handoff, submission, &mut callback) {
Ok(()) => {
accepted += 1;
}
Err(RingError::FullDropped { .. }) => {
full_drops += 1;
assert_eq!(
handoff.pending_completion(),
NVENC_COMPLETION_RING_CAPACITY,
"pre-encode drop only when completion ring is full"
);
}
Err(other) => panic!("unexpected encode_shared error: {other:?}"),
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert_eq!(
accepted + full_drops,
total as usize,
"every submission accounted"
);
let pending = handoff.pending_completion();
assert!(
pending <= NVENC_COMPLETION_RING_CAPACITY,
"completion ring stays bounded"
);
let mut drained = 0usize;
for _ in 0..400 {
let slot = fluxer_encoder_ring::HandoffSlot::new(0, shared);
let bs = NvencHandoff::poll_completed(&mut handoff, slot);
match bs {
Some(_) => {
drained += 1;
}
None => {
if drained >= accepted {
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
}
assert_eq!(
drained, accepted,
"no accepted frame is silently discarded post-encode"
);
}
#[test]
fn nvenc_attach_then_detach_smoke() {
let device = match try_create_device_for_vendor(VENDOR_NVIDIA) {
Some(d) => d,
None => {
eprintln!("skip: no NVIDIA D3D11 device available");
return;
}
};
let dims = EncoderDims::new(640, 360);
let handoff_result = NvencD3D11Handoff::new(device.clone(), dims, 1_000_000);
let handoff = match handoff_result {
Ok(h) => h,
Err(EncoderError::SdkNotFound { .. }) => {
eprintln!("skip: NVENC runtime DLL not available");
return;
}
Err(other) => {
eprintln!("skip: NVENC init unexpected: {other:?}");
return;
}
};
assert_eq!(
handoff.pending_completion(),
0,
"fresh handoff has empty ring"
);
assert_eq!(handoff.completed_count(), 0, "fresh handoff completed=0");
drop(handoff);
let _again = NvencD3D11Handoff::new(device, dims, 1_000_000);
}