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:
+979
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user