From e6d6498a49fa19f9723cb1b3c595ca1152134e32 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 5 Aug 2026 20:23:18 +0200 Subject: [PATCH] =?UTF-8?q?test(pf-vkdecode):=20frame-hash=20parity=20vs?= =?UTF-8?q?=20libavcodec=20=E2=80=94=20bit-exact=20on=20the=20whole=20flee?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WP-D parity A/B. gpu_parity (ignored) decodes the conformance vector, reads every frame back through the presenter's exact contract (wait, layout round-trip, signal-back, release), crops at the copy so pitch can never leak, and compares SHA-256s in display order against goldens from ffmpeg software decode — cross-checked bit-identical between ffmpeg 8.0.1 (linux) and 8.1.1 (macOS), so the reference is the spec, not one build. PF_VKD_TEST_READBACK=1 is the one test-only hook (ORs TRANSFER_SRC into pool usage; production pools stay zero-copy-tight). Fleet verdict: 250/250 frames bit-identical to libavcodec on RADV (Mesa 26.0.3, distinct), AMD proprietary Windows (25.10.30.02, distinct) and NVIDIA Windows (610.88, coincide) — H.264 decode is exactly specified, and the native path meets the spec on every driver and both DPB arrangements. --- Cargo.lock | 1 + crates/pf-vkdecode/Cargo.toml | 5 + crates/pf-vkdecode/src/decoder.rs | 13 +- .../tests/data/test-25fps.nv12.sha256 | 267 +++++++ crates/pf-vkdecode/tests/gpu_parity.rs | 662 ++++++++++++++++++ 5 files changed, 947 insertions(+), 1 deletion(-) create mode 100644 crates/pf-vkdecode/tests/data/test-25fps.nv12.sha256 create mode 100644 crates/pf-vkdecode/tests/gpu_parity.rs diff --git a/Cargo.lock b/Cargo.lock index 726b786d..430220d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3223,6 +3223,7 @@ dependencies = [ "ash", "cros-codecs", "pf-bitstream", + "sha2", "tracing", ] diff --git a/crates/pf-vkdecode/Cargo.toml b/crates/pf-vkdecode/Cargo.toml index e1625166..3303bcb4 100644 --- a/crates/pf-vkdecode/Cargo.toml +++ b/crates/pf-vkdecode/Cargo.toml @@ -18,5 +18,10 @@ cros-codecs = { path = "../pf-bitstream/vendor/cros-codecs" } pf-bitstream = { path = "../pf-bitstream" } tracing = "0.1" +[dev-dependencies] +# The GPU parity test hashes decoded frames against libavcodec goldens (already +# in the workspace lock via other crates). +sha2 = "0.10" + [lints] workspace = true diff --git a/crates/pf-vkdecode/src/decoder.rs b/crates/pf-vkdecode/src/decoder.rs index bb1bc928..9159f2da 100644 --- a/crates/pf-vkdecode/src/decoder.rs +++ b/crates/pf-vkdecode/src/decoder.rs @@ -1126,7 +1126,18 @@ impl VkH264Decoder { max_active_references: (required_slots - 1).min(caps.max_active_references), std_profile_idc: std_profile, }; - let pool_plan = plan_pools(caps, required_slots); + let mut pool_plan = plan_pools(caps, required_slots); + // TEST-ONLY readback hook: the GPU parity test (tests/gpu_parity.rs) + // copies decoded pictures back to the host to hash them against + // libavcodec's output, and `vkCmdCopyImageToBuffer` requires + // TRANSFER_SRC on the source image — a bit the zero-copy production + // pools deliberately do not carry. Opt-in via env so no production path + // ever grows it. (The fleet's drivers — RADV, NVIDIA, AMD Windows — + // advertise TRANSFER_SRC on their decode-output formats; it is the same + // bit FFmpeg's hwdownload path relies on.) + if std::env::var("PF_VKD_TEST_READBACK").is_ok_and(|v| v == "1") { + pool_plan.picture_usage |= vk::ImageUsageFlags::TRANSFER_SRC; + } // SAFETY: live device per the constructor contract, for every create in // this block; each created half is owned by a Drop type the moment it // exists, so a mid-build failure unwinds cleanly. diff --git a/crates/pf-vkdecode/tests/data/test-25fps.nv12.sha256 b/crates/pf-vkdecode/tests/data/test-25fps.nv12.sha256 new file mode 100644 index 00000000..c2607b9f --- /dev/null +++ b/crates/pf-vkdecode/tests/data/test-25fps.nv12.sha256 @@ -0,0 +1,267 @@ +# SHA-256 per decoded frame of test-25fps.h264, DISPLAY order — 250 frames. +# Each frame is the 320x240 conformance-window crop as tightly packed NV12: +# Y plane 320*240 bytes, then interleaved UV 320*120 bytes = 115200 bytes/frame. +# +# Generated ONCE from libavcodec's software decoder (H.264 decoding is exactly +# specified — every conformant decoder is bit-identical), inside the +# pf-lxcheck2 image (docker run --rm --platform linux/amd64, ffmpeg via +# `apt-get install -y ffmpeg`), 2026-08-05: +# +# ffmpeg -i crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264 \ +# -f rawvideo -pix_fmt nv12 -fps_mode passthrough ref.yuv +# # then split ref.yuv into 115200-byte frames and sha256 each +# +# ffmpeg version 8.0.1-3ubuntu2 Copyright (c) 2000-2025 the FFmpeg developers +# (cross-checked bit-identical against ffmpeg 8.1.1 on macOS arm64; ffmpeg's +# two "non-existing SPS 0 referenced in buffering period" warnings are an SEI +# preceding the SPS in the vector — harmless, no frame is affected) +0b90656d0073d0f5ef80c7a8f19e08e55fb7d5d964cec920e010472c2bce2a4f +426edeacc9a3bc7d155ef638b9d83f76c06718494b68c679b86773a1db4b0369 +103384f7b1da90c8a4207195388514955df75b5aa0c6cf4c3c0117bff4bfe65c +b43e92c17d32801461344bb9727b6b508ab3abd937b592522fa13efb254d5582 +64d69ce1f948367761a48ee1bcceeabdcc58b4052ae74f8cc46b2c3064874c3b +ef3c87d5812386cd391e0b41baa887bea57607a5781e8de5b2efef6e27c1f33b +641d0c4b9ca37198fbf1294eee0205b03392fc872f0f0856335a4c797169c0f9 +fb496f343c222e2551e72440af276fb5f6876ef0397a16d59ac8403f484749b6 +6c79338dd34b4316cbbcc27b274b96d60ad7f9697b3ebf883697a5f3b792bf1e +b36896f1f70c8971824ecbf247ca4bae402e041ef87a8f8e3d975a2f183f3e68 +2052d220bf06fa1701217806b5cd9bf33f3eee536731916ce3e6af87f62f839d +2f6d10ec3c7359f60b0b87718a66d935ebe735e03d2f2e6548873d2b0ba26400 +0556a84be0d929432712c1e5b43e0bde63222a7168300a5cf41705e7278188fb +652c512e9156521d7cfd3c235eccb2c3f35138104523cc3142ffb87399e14e5a +0a670d6fd129aaa946f9b3f514d741f12d6a764627e676f9a0ccc8914904f145 +ad04fbb191208944069ce66b8ef7d522d4c9600127296f64a1894f5488faff6c +357fad51f7a789022e20b6f3b34594643b85cfae180a164a8eb80737c441ff4e +244e8f5b209082a7ceb943552fde977673dfa806b1b4cf6e8e748a97eaeddff3 +f7b904b81ecdbc25db60b5952b9610da446467874a8e20e702d559b828929824 +e733ed049d880fbf0d620d6e2add3bbac1ce91c38f22c1c23730ad3a2e3501c8 +bc092845dbcb9e7a21aead8e2567db94a2ae6a6495897bdabc515c6c647bbad0 +45f101681599e69e0adfdd71efd7985711d22d36b3d89292cb9d6eab0939c2cf +3859c9ba68a834279ff0feba9df74f6055b148a7bdc557cdd486429d50dfcf89 +8bfbd3b025bfc984090dba2d70d76d01e3413846bab9fc58f9fd14ab366f54bd +6d5de7d54c939210410cd4a6bdfe2ec5c1f5b45a236030f6f0da708507c8af88 +e91e07d21ab7541513083b65d6515139c77229a4291ddf595f37c1624784cb88 +c9f8af29896ba9c9dc4644058861e35add58b7076306263e6ea702c39ec8211f +43698f8c167d9712a56505df7fbb7e7dbb72dd475b8c350bbd1aa8b7d8bd617e +0e0dee1d1cb9a21f2d3a541a6f2c59dbb1891ee32a07a6484a3b2405415bb0b4 +8e0610370c4c76bfcc7d7265c47b3ed1a362b59ebfe06bd7b6ae156028e28b84 +c41b96d91b8a0381d0fe9d38cf75fd9476ebc391323407425b2faa9cd3e27f1e +ad9e3b8c6f77d7007ba3ec783f1a97bb5b20fae9f38392f36b72d4eddcbf3d13 +ce8da4c829ba77a6126b4b0a5fe844ab9b6af17e295477bb8990d1d1764fc712 +c2750f8725482d14122acd3b11101d574c0962d4069c8b3a6e124ec44ddc49cb +6097344d5b3bec003f118386a5b002e1aac8e2af25a1fee2ef1d5a1f84a82afb +514e8b635a72344834f73929c86d1f3977160f74234e1185d94a6c639983b0e5 +70e247589ec800cae250fcbd20c20d6a89265961e64fce7a6a2b0193d5f54db4 +7fa9e6c2dfb413840525e7f9cb9314156705b9c68d3eea234fd940a2745be81b +7503bbe71d90d54ba169caea94a381cdc33311f4075a4803188531762d68f8d3 +152bf0bfc1139c878dc156ca1cccd7c4deba8776f5daa84281ea158ddb373d9c +d94a8273bd427ec794cdd4314cf89c494c55845e534b49a9607a3d3cca19c23b +ee7e890a13c62f68fbaa22ffaa42e8ee4eccf54ee67c8098c4dc0026d988a40f +f9a23a95b8833a65e75e8ccc84f273e58d0f902a2948f9c8855c90c214186881 +542fde8cb98ccbb13cb42b2fcb3cbb92a4d998142b5a32da1cffa241f444f1eb +54ff4bd22812bb7089fcc4e2df8cd8a3397151cf090dc6e5a7e2185c177d7619 +be1d1daa788a71906e9860faa7803522f9e01f462c05cb9c2aa1fa741df12d17 +fe369395a5ead4741ae5a3e8a470b2c2f1f637bfb1e3bdf2fcd5361aa7f564ca +faa51b4103b04885225ef65ebdf79215c4733c9b7f645ac6bff9eee6646e712f +6f69723828dc2244af8f5443417aece5e4839e56ac0760d0d5f823bc9b125d15 +71989cb65be483ada531009f3b8a16bc842573fc95e3199e9a9e826eaffe24db +da740a8a5dc37df5044c0c573c4ca398175a229a591c3ec6832f9fb274ba1103 +30fb23368a1922fec1626f73fd5c1623f14288b4e678472e2318bdebecddeaf6 +b95204fe9ac70f4664b631a651e95cd963ed76e1df4d32b1891c8d4af6a509db +e162aad9b481bc038cb987a89a0e024f37a389f3331c3bf6dbd0b6462e81eafa +808562bda2cd82d490c3485f9daf815c4a5dc7fd1fcc0c6d411dd226d2f0d9e3 +01fbe98468e7d810caab30fa24da03d03c8cf0d8d9ebb2a474a2d01536b4b1a6 +ef2e100fd95b7a6c562cea6c6055355f96cafc7a99d8fd9a77142fafe318e57d +a4d2f9a3e6e0d131e7fb99475af549790a8f6770926f97e89c7afbdf0e3ac0a2 +9c5a074ce591cd6f043b7284df35c2ae06d4befecb57622c0ab0dbccf32f6dbf +c8e67952dc7f7a1ca1cdc9432ea2ec20c361d5f6dceb4ac5a307908e7da6abe8 +a22c0d543bbbd72d5eecfe5994d9c7c096d358e60b708190408038b96e137736 +383367dd08ed34208e5ff74efdcdbea9b19eb7687751950cb419f08086892008 +70776046dbe080eec944c3170ba37e9a236ae1fe569874efd0ad298307127c3e +8e3a48cb61f5ec8dd5a965ceae8974f0bb65bfa6b5cfe21dba08ef3236d0d199 +452d178a5f88b320e0d351cce506c4b51ededacbc4a9eaf80af22c2b11d8a3c1 +86b41c15ea413828f58580ed11c2a0444d2f8d15ec6a59a5fc183ac4821b1f9e +6b9fb776d125d421221f9949ff6f0b0f240c1f0a85c51a69e060023ead53203f +6544223a981420acf6e34dc357716fc9da6b9a0497cacb6cc8fe0c8aa12a7bc1 +3e081407ebda818323a9346e3a7c061b12976d047b642619cf8625b8d6a65ad9 +716c7d891909fd42aebb6ce1c133b7713e433d276c74874b56cd0efa2c05a762 +1002b6644c41b51cda605d36a74b199dea056efab04473aa9c2df6d895c17977 +fdd01c9cb5bd9d5a0ebdd5b30fd33f15274f38a778f9cd870e1f24154fcb2d51 +2cc968f5af71e3fca3a539a1d98abc2ff5e3e107ca136c8f903abd62f8588ab5 +90f752b9b747af113b6a2eceb85a23fe132e777f63b0a66a6b9b31e815c761c4 +1176f465739905e32e07fd3eaf42721ee6557530df277faf9d2ee65065291261 +70ee1d5922bb80958d0d0e548056ec3ad52553a1ceeb05fd453fb80b8cb52f37 +47500975b2477ad17a32b88c5713939b3bbb72b943f77df64af9d26788c15633 +677bf3b75e5bbc91c7c861234cdd69b78d454bc7d60cc4ef4466ca1aaeb0cda0 +7340dae25917ef0e9455470b71ee9828bb0257522de0b69df0261707601abaae +66366a42398f8e2fc33b5c398c637fc8599f1c9095a591ec5fedb245b65d8259 +6b10f4704a9305ada1a188cb1344810f7f3ca860daa654d99fc089071d9efd07 +2841355a13f0078892bf187a3e9a26bf933b0f5b9e44ddeefdfcd9b5e4eb52d4 +f7eefab9d7850165142fb013e38f06c8ded9343e3de7c99ecf625d7eee49df1c +eeda3ca26958e168696869fd60295b91d36f245e75fa2c1dba96eb015d0c5ff6 +9116997bc75478e08064b06e564d908098b7de0df8c48c1b3f85129099cc2284 +33e7d7e773adbc6c90a1e2fe31064cf40c55de325e605c39ada460ee5030dcf6 +03df8278481af8c050bed6be2a20d40321f7d5a1660875f3bbb13967b77f7dd2 +cb0226f67ec08a8f774efcd98ffe0af370ed08166c6b56faa2e530b0ee476953 +dc00284e63283d9f3f1767f306b0cd1b95cef54c48a669b8566f887fa27b4c07 +343c133465107f0180f7f0a91096d4415bfe35bab6fb973331791ddd359418a6 +24a39ead65655210805a4e24febea47fe193a063925914fde70c72409391f399 +c4a50d150fe1160bc355b535c5d869a6a9f3707939339cfca0fb1073fe367ef6 +7599b2bba47f8a79a44174dd711003f1431ff944e0347c6496ef81030ffe97e6 +e2e824df2e1ee1b45921c44f132b62a4259d2ec904e92216d0872229991fc223 +6b3d6e6e268badc21cb8d890ad545bef9524b9660ef3a42b3c7d122276476930 +12db06cc6fb3d84453e9f15f73d682ba1d0caa5a27784a386abd753b65c27f4e +41740504395cd80144ae1a2530fcc7dd73c5db46a8e47f75478bfc9fb2c47366 +8a64ec8fa1c6a5a0dbadd11f6775b5e0ef4f455a33425db2c7d95481d8b1a599 +9a1b795bd182ea5ed3cdd7d62f4573687893f8955447f90eb23caaa0c4228955 +67f1a041201eab6471c8e3166220ab523ff1ccd67c049278555e47385bbbe333 +d868eb283905efe040a252eebd84079763285b195e4fe045ae700950ec7b3b21 +42edd6516283a6687a1de7e8fb98ce39240af44b0bc9a49b1b5786b5a8f0ca8e +1ff6ca0cebc0bffc1d6748b4a137d75d2f8181855e4ed14dce0f600b21360e26 +e9a5d3c6415771b054396ab56624764cda12ee0c3a2d79b300d72c2a74171e6a +343e81d501c8abc228fa19d8c2069dbac9f2efd576a8cc09ea82d6b18664a79c +883329828ec3a2804d49afee9b701d36a479b3975276767919ce3c691c677f91 +e379590cbc0f04bb49ac1911964e072aebe04352973869e017d65bbb147b5763 +04727f24b045201d199437046ea84d32312af19d9962539465e9574e3398fe52 +5b11ef561f154c89332dfa9781ed58b70dd6445b8ee6cda542fb3c208ad9a1fc +8f37f1006041e928599f4cdb34bb4661c91d0931057de16328fcc4b0dce159b5 +413c97ba67c2487a4266801f1553c18d4d0ae9164d41117a31ce33888e10cee6 +dc09400dc2a5e441319acad4f1056a922ce43eb3977f7cf746452f5fb95ae833 +72eb2c8c57cc5d66a8529965542142bfdb6daed539b2d70d674272358e1aa144 +6dec84a209d299e48de98efbeebaff005d01bdede477bd17b4dab28053ffab22 +e41eb98c56fef112632c21e11fdb5c200ce7d233ef02f5105687912bd7c73489 +0d2b44581f52f17b5bb34f63dc9bb3d8595606dcf22bc92b2fa7a2701f330bd8 +ae805bb2be43dde5a21132391a8ec4db4e5cbfea9bc8a252e1517f5d6880556a +a12d311502b0163241d02199fce8dc178fb3c78fda94bf30b0011f87debaf8b4 +db218d273e3ae407fdbebf9a461b74e41febc233f95ea586a17ed5fc63ad0ea6 +b3ec4e7f77292ee0a222cb4a894542afdf0204a18301a1a92a22d5900d2c5e3a +4b4d5e2e64059efe43f33a50d35bab01bea69b5c59920a903a337078453257c1 +9a3768e9179be20683bf0145e6ed3c831c86cc283034fdba8d836924eab32229 +df593ff95f4ce6400bb0725e4d1a37eb563d513a7e79efde923b7fcfdb418010 +84d26ec9fc8b3d64e298d5f37eb1b2a5d0ec480a7247c00710cf6e8b6a3d3503 +efef3e26f95fbe1ccfb1e1d8a2e6aaa3aaf0e978725ce755a3e26f01e16e645c +2bbc737a1758d223a2faeae1881abf8c21caf3db2e8f99f1b741dadaf79b9420 +a2ece09eaa2c14ceb75d8fe9b73bd9e5ecfb2a3a15a986a4441823e990455523 +01e2a5b57b232b8908f72b0329bfe341498fbf6f4d0e3538259cb69f8cf861af +250a6d33f7ef0de3b4aa7e6c05a337bf5afb0eb09f34cbeaf1a581a5deb15b3f +d2af6504ec674c17ab0fe17a74af59dd7ac80f6f98f07b9d29fe75af9554dfd5 +1685a738835865cb0ad9ee40b1648be021ad542c0c204f6ac17e9f5b30af4b6a +0844e227c0320b3e59082c5a7c1c2f0b7ed1128974f5076f790e6f316425cdd7 +2023bd089470332e859e5bab766408bd8a5faa4abb74a5f454df4a66fbc067ed +276c9f17585c5b84872d26a212f0a25191aee4ce69febffc4e1ea0ef3954858a +60dece81f32a1a7aee0e5a1016c8e8c3eb2fef7e08f04b560015b97bdffc596f +ba9b621cac58968944b23825202b83ed9945de2983768d09d1b69b278c0cbe6f +6e90989c6314749661d53c5f2fa4f02e342a80207a0930be6424879a9086ac6f +905d02b8c2de44a29b68772ce6a3d117b90f13d2379f1159d3e73bc0ba9a25e3 +1668df55d8f04d41aff1996dfbffcdaccb220ae625a04dccbb4515b90ae8f28f +bef8b50baebfa3eea65e3f0edd98ef185321c27816a09e9575169495f4581c7f +824ff1ef4f93aaa6e568b4eb32c724bb69b350171230745ebc47740a75472519 +37e42144279ff5899f646a5de9896bc2fb887a88918e7b5b78bc207a56e52d4d +f64d103958e08551e06a5f72f22b76aee671ce76b4f986c666697e08a3fc4e97 +b9647e2405c3068c3a0743bd41e30d554d4be6db91183805c424b43c0e904801 +c2bb17fea55efa58210ab3ab2636aa6f828bbba1d37d4906c2da57ecb5715c93 +8a71fb38b1fb2074a8a1f68967749201b7661e4e97ab7919a25f6f145929aaf3 +9af8cc8034eada25787b6427e9d24ee7f618d962cf3624dabed91776d79193ef +4679e1a74d207bb95fbc0605a999e939ef4844f1a1038c39f59ff855f35621b6 +8b8859f4cc9e8ed67feafa17a1c7e8b66c760b5eca9c104ad33e6bd8c5d08a37 +c1e427ff9ec73be4e6e64d3fcdd58a452014d69aea16f6b72ffd93027ee7a533 +35f09c7808be14418de04ac833ac846778efdff42c28ceef54e06fabb94ae9e4 +e5ef08f78698d441b519d53880b5aa366f8eae04d6512e267b57bd7c9996fd1e +38a0a28c91eac581647ff745d6e497be18c31fe374f61ed1d8502bf299572693 +6d451d866460c108bbc258c29c2de596813d88b9835b35ef92f495265b64e302 +1a9c67bbcb45c71a5ec97976c7a54a647beb3c445257022a4da71eb2a14667e3 +c43195cc687a785db43e64feee841c512b6a4c8aef2bf6420972700dc8f925c8 +b2d0a6b3f6785c28ff537908f9a7fbc30c3025223d76ba2a3bad622f2313ea29 +a2f6b2f9455449a32ec8df50fc6ad2dd7a0ab04888baf52779f2d84560b9d15a +e2b0e08324465c9489cbcfde87267a046e0d30a409e29dc32d4fe2e62dfac432 +8ce434e02fc4a79fa83993cb19f808f159ebb06d342b0723e3ddb3c2e4c05f62 +89786221bea32a38e6dbc27c04443e47da40173b3474b15d860e49606ced09b2 +258b6d4c01e0e3efc81da1e1a50aa607fa23cc1f2584210ff4317aadfa4babde +4732a7ff59f63199be7661519d8c38c9c401c8bcd04bfe94a437613a1aa10150 +6b174a5a8c75d0c6b30464d6924be0670e89f5dc6cd665849b63520f05d126da +b496050efeb03af2976a90dc8344a4042261b668114171d6b825e1eeace3b7cd +4997d172ef5f06d03dcb13e4d6b0d44efb3612bee00572d02755b5552b7acd2e +7abc1388e80a48b98ab494f5e679c3afd6ed48d93bb83c87d5d1bdc77315cb93 +7bbd1c5daa4cf65ae29065938fc9a89477900875d2f4a3083cfb11d5485289aa +fd2bdd77848ce41da3a1a05bfa34f7d05c2c0c1eddf9f32c3f99f58879c12492 +355f35cb2e8abc77f01db2d9dfe0a87d62005a3ac8cd13d401853e55c2b69b61 +26a6f4a4904d479bcdb09b3a8edd459e618886cfeddcdfcb543cf46c7d10c76d +b5dbb623aa2876bdc1d13cc439da72d9ce7de68ee6873a57c4af18f28e65dbd6 +09fa44af5bf92dd67362dde07a49ca4abd75afe1fd0b08bbdba651264db26aa2 +52c4e4eea32a5ecb6b9d5b958ad78ae732b1d821c82b9c82450543c998b849d5 +ebfed9b549643aac86d0bea6a520ed704e046b5bae859766e768a9f378aedfbd +c80e01ad78922a195593ab0531e6ee11600ed43f9567e548c5a7fbb8b15c3de2 +a4b0a3eaee44d1ed069b6b2a99c6584aa1c004b8c8d3af3519709eb8c6caec65 +f1f7b5be4b662303f43d46fdcf6718f4631b81af67a563637db4727bf4462a83 +8dc72ec654ced2a029b475c90d36e36bff03a1447946c573f8a109b8937f802a +198bf80c83d29db20a7e72589d4b7150cce4753a22048c54cf19aeac62ff8f88 +f8f483b40379e562632a4ff062d9605b2dffa4dd9b4d889cbe169fbaa969bf99 +226c197633ecc8d28d4b8849df8d108480f2e865c98210ca06b12c10ab3ec086 +0997fea45eb7180ebfd766cd9de64e6a1186be152d4e085da6eacd93590c8ed5 +84f4d784723e1160580245d8184fc0be827821f9c37342e9b405bd9cef161e7f +20aede1edf420efb978355c6c436b3d4baf8bdb2caada92b4adfbf1629fc79a0 +e6ec12769e54cc3025dd3c05ddc61d10bba59fa23acea9e513d1d5c8793f107a +e6011532e1fc39801b922983e395c6de403329d2214d202d156911336f8f86a8 +f685605a0bcff5eae9c984f2d01157d9c745adc2d070393c4e49648145fc8287 +438b1ed285dbcb8e1dd5572cb38e48f703ea054cc486f849d55add0f9bb80115 +846ecd0ec2b9d89dc1784f483bf8a151bc5712b4bac5b6e335b0a0c4ff56df83 +cdcf4f1db20116cc11f195d62cb27ec29b50c2967e1ea9745857d19fa2b3e3f3 +7bd94902f85dc9afd07577cfbc4409db0e43b09e9ca5b408a8a8e83e09406c5e +9edf5a32cb1da57fef79718bb37ff22b7a2c23d0c19dce567cab9a8829246c2c +6961a882ea08aa2957d93f8d70ecf80989f6b2045e232362fdd88e1fa6e2a8eb +8f3db233155aad751018b98a55a0d823068b6ef981691b8173ee4dc63add943c +884ed0f0baa64ccf52519e0564ff5fb42205799aa558d9b96ebe5f1d292bf24e +eb25d5728dbb19f2f9c7902a56f70dbe34d8f59c214cbf67316221b6d134472d +d43c2c580fd009720ff3896cd835e1f0de3bed6f765936fc691661e6ef78fce3 +66984354174c7faf0cf6919ff27a745e4d04cbd00a71fe6fb10a9b22c061f692 +2ae17293c2b8e3ab8492f2df93cc5df458ad5f2084a99a25d65498a4c3ac5915 +c4ebaee307f1bac9679eb52b716d6cca1934c51d4213b83520c87f1a4d3dc354 +c6a4f0d671ac4aa25acd87ef1f912a8ad56f40542b3edf55331b5971734db2bd +08c554594139a6f5dda9c9e62279fea24100423c151370952f84ae0278c08e2e +f6a8404804d821a105c048c58afba9baa123cc1430a40461410e6405351554ba +86b5113ac2b5b3541e32db3bd0420e4724d3bb6069ad9071577c65796da2f56b +91fec67eed692e5d1bbdc97bc58ae4541ea21decbd79543feec575cfffe8920f +9bb81e4d42cf106ae74a5670d1e6eb47ab0e90f3fdd22fbbe2a3096919dff8dc +98546509149cccafb0e304fa8394e7d16b4cca97d9481ea0c5604932f6fe4a9f +2f59aa4bd67f83d2f247026af5940758c9b7a8a91903ae6d6d09aeff8699eb72 +d49793a2d3e4dd81680b3356f1b807870dbbf01d145764d0cee2667a105dda0a +da6db2186f97a7595f8c8651c9df2f3002a29e5bc1d4b45a4da47aa3b0a902c0 +b2b198faa26f1928030f0e612d8a447b276e33214e6d461f487bd1668ffcebb1 +6616b7da478711919a1135527f0d3fc3466d4ddfea020966c6ff5c86d76c29a8 +9fdb7f7812019991c0fc2a94e43c9a72ea37ef0f51103aa69b470da581952ad1 +f26e2001c3b3fb417e595934886bf577391ff78e45362df0202cafc059840bc3 +ddb6e82617370b947a01ed35a97f0b222a69cc82205db763e313d31313afcd90 +cca0fb8c2f22f5bc6c72b44c8ceff3a65ef0aebc76b5c9df444e00822d9f95ac +3f1a3829ea9a2cd6aaa16a88482e4bf48462cdf2f09e5882a8f1060836fe6c02 +264f5c0bdfa9bac5cd12204ec220d5825a199c226fb02d129d163b0e5e127075 +b51b71e1a6f14a3cccdc7962b42ac8e66b3513dbd241ea441dda5c50fd907a69 +68bfee7e76375143067494dba0056e091bef2c0d16091f9e64a60b377f074888 +e3c8d0c23a36df5d8cffed0e1348bea745dd3f80aa21bcdb2ba13c36ae63b2e2 +03bfbbaad233af3690fded6cd3d8ccbd1807e6cf5bcaa2f3e7da3488225d953e +507e46d9471718243f2086542b7cfe43c9cbd2cfc1dac516bb85180bbeea8c1c +a86053a40337cbb6199b02546e7ccd1ea9f9823102b7bcd2a847788afd2bf265 +1d5e5ea6fa92890a6ce6566929094b68758f12ef9b405c9e2927ce3cf1b90a76 +3bd7bc3ceb7ffa284adbd644d09c1d3b298b4b28b71fbc254f7140278f18a5f0 +4a5f25c3d9f88fbf86844d9fd29b3687320866ee9968d1115ecbf9d2c397b3da +f7167f09784700563d3b314046c7341efbfca80a48b4c830b8d1d07034d19d59 +9871a9e37d3c2c51844e3a07a6b75b8ed553d7fd4d6e2af1250e91366ebc174b +cba6d7b129a290fc15c2f85c180702725d0f9abb82d0c658f339e9f973512494 +41dc29241aee4b80346e5df268806daf34d6606d831ee8088bba9365c115b7f0 +c8232ee1f0a86dfb9101d968a06361ba2ba8af5b864517b3653100bfb70de3b0 +be3968003b5f580ed732414420882633e22c683811e253689220a21a48987c23 +d952bed0f72c7d84deea9fcaad7d64555004114ea3ea3d9d945440e3ebd6286e +962976cd6403603d59e71a1ecaccd75a8e3f2e1e090f96415b9b51e5ed1e0c08 +ca1160ee7670730d9f413dd199db1d9424ac03ebc3b9504f1ad0e5874fa65c29 +716d40bdc4b850d3d101150863d1d1547dd629648632e53f8de856e5c0c94367 +65510bc206a782477fc102584341525b5e71d33dedc67a61a7c4d031eb003b09 +a1c8dd0345d5adbab643f7b463933e0ae18daec04c5aeaad7bf8d2ab81634316 +e6ca742bc83b756bf25fdaec471bcf75d04d19313d758db7d315fb37fa7c6d4f +54efd66ffd75ede95ba9485a68aada5fdffe46ffac8cda030f6ff0c8af0ff0cd +1567cb38f2df05407c5b2b03ac1b6abdb3805d52aaf2b949406450a9231c762e +0b70603bc497317b0395c3e084f7c49ac5f749a84dbeb38f23bf5b2a7e3e632d +a22b607fed5e95281d1060ac50a9def45233247d99ae72838c7caa9bf977533f +7d920490373e40445ac28d9f1e19821b19d2da2196e5343b7ec3a660a41119e1 +cda6c02cdb38a68346d9303a99c921b9b6253c5ed795c9ee4927530f53fb2ca2 +31c41a44c6802cf7880ab64c7d5774c4c6adb5a80bfa2d9a1066a5f7bc170097 +cf8aa6ff1b51cc5066aff47668d58fe2dcbddc3a2cce07c3050c3a9f3cc00808 +688ed8a607cc26680d24a7f3183dcfab7ed465ef00b5a9eee15d0d4f5aecc3eb diff --git a/crates/pf-vkdecode/tests/gpu_parity.rs b/crates/pf-vkdecode/tests/gpu_parity.rs new file mode 100644 index 00000000..a781a585 --- /dev/null +++ b/crates/pf-vkdecode/tests/gpu_parity.rs @@ -0,0 +1,662 @@ +//! GPU frame-hash parity test (WP-D) — `#[ignore]`d because it needs real +//! Vulkan Video hardware. +//! +//! Run on a Vulkan-Video box with: +//! +//! ```text +//! cargo test -p pf-vkdecode --test gpu_parity -- --ignored --nocapture +//! ``` +//! +//! (RADV boxes additionally need `RADV_PERFTEST=video_decode`; multi-GPU boxes +//! pin the vendor with `PF_VKD_SMOKE_VENDOR=0x1002` / `0x10de`, same knob as +//! the smoke test.) +//! +//! What it proves: H.264 decoding is exactly specified — every conformant +//! decoder must produce bit-identical output — so the vendored 25fps vector is +//! decoded through [`VkH264Decoder`], every output frame's NV12 planes are read +//! back (`vkCmdCopyImageToBuffer` on the graphics queue — GPU→CPU is fine in a +//! test; the pool grows TRANSFER_SRC via the decoder's `PF_VKD_TEST_READBACK` +//! hook), cropped to the display region, SHA-256-hashed in DISPLAY order and +//! compared against goldens from libavcodec's SOFTWARE decoder (the reference +//! implementation — provenance in `data/test-25fps.nv12.sha256`). ALL frames +//! are collected, including the tail [`VkH264Decoder::flush`] delivers, and the +//! frame count must match libavcodec's too. +//! +//! The readback follows the presenter's exact frame contract: wait the frame's +//! timeline `value`, round-trip the layout, signal `value + 1` in the SAME +//! submission, then `release_frame(frame, true)` — and every submission is +//! host-waited before the next decode, so nothing here races the decode queue. +//! +//! Reading a failure: frame 0 is IDR-only — if it already mismatches, suspect +//! the readback geometry (row pitch / crop) or intra decode; mismatches that +//! only appear on later frames point at inter prediction / DPB management. + +#![deny(clippy::undocumented_unsafe_blocks)] + +use std::io::Cursor; + +use ash::vk; +use ash::vk::Handle; +use cros_codecs::codec::h264::parser::Nalu; +use cros_codecs::codec::h264::parser::NaluType; +use pf_vkdecode::DecodeStatus; +use pf_vkdecode::DecodedVkFrame; +use pf_vkdecode::DeviceHandles; +use pf_vkdecode::NoopQueueLock; +use pf_vkdecode::VkH264Decoder; +use sha2::Digest; + +// The same vendored vector the WP-A tests convert, same relative path. +const TEST_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264" +); + +/// Golden SHA-256 per display-order frame, from libavcodec software decode +/// (generation command + ffmpeg version in the file's header). +const GOLDENS: &str = include_str!("data/test-25fps.nv12.sha256"); + +/// The vector's display (conformance-window) size; the goldens hash exactly +/// this region as tightly packed NV12. +const DISPLAY_W: u32 = 320; +const DISPLAY_H: u32 = 240; +const FRAME_BYTES: usize = (DISPLAY_W * DISPLAY_H * 3 / 2) as usize; + +/// Test-only AU splitter, mirroring pf-bitstream's (`#[cfg(test)]`-private there). +fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> { + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let nalu_offset = cursor.position() as usize; + let start = nalu_offset - nalu.offset; + let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr); + let first_mb_zero = is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_mb_zero) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus +} + +/// The golden file's hash lines (comments and blanks skipped). +fn golden_hashes() -> Vec<&'static str> { + GOLDENS + .lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .collect() +} + +fn sha256_hex(data: &[u8]) -> String { + use std::fmt::Write as _; + sha2::Sha256::digest(data) + .iter() + .fold(String::with_capacity(64), |mut out, byte| { + let _ = write!(out, "{byte:02x}"); + out + }) +} + +/// Test-only GPU→CPU readback: one persistently mapped staging buffer plus one +/// command buffer on the GRAPHICS queue. Each read follows the presenter's +/// frame contract — wait the frame's timeline `value`, transition the image out +/// of its video layout, copy, restore the layout, signal `value + 1` in the +/// same submission — and is host-waited (fence) before returning, so the test +/// stays fully serialized against the decode queue. +struct Readback { + device: ash::Device, + queue: vk::Queue, + cmd_pool: vk::CommandPool, + cmd: vk::CommandBuffer, + fence: vk::Fence, + buffer: vk::Buffer, + memory: vk::DeviceMemory, + mapped: *const u8, +} + +impl Readback { + /// # Safety + /// + /// `instance`/`pd`/`device` are live; `graphics_qf` names a queue family a + /// queue was created on (index 0) whose family supports TRANSFER (GRAPHICS + /// implies it). + unsafe fn new( + instance: &ash::Instance, + pd: vk::PhysicalDevice, + device: &ash::Device, + graphics_qf: u32, + ) -> Self { + // SAFETY: fn contract — live device, queue 0 of this family exists. + let queue = unsafe { device.get_device_queue(graphics_qf, 0) }; + let pool_ci = vk::CommandPoolCreateInfo::default() + .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER) + .queue_family_index(graphics_qf); + // SAFETY: live device; destroyed in `destroy`. + let cmd_pool = unsafe { device.create_command_pool(&pool_ci, None) } + .expect("create the readback command pool"); + let alloc_ci = vk::CommandBufferAllocateInfo::default() + .command_pool(cmd_pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(1); + // SAFETY: the pool was just created on this device. + let cmd = unsafe { device.allocate_command_buffers(&alloc_ci) } + .expect("allocate the readback command buffer")[0]; + // SAFETY: live device; destroyed in `destroy`. + let fence = unsafe { device.create_fence(&vk::FenceCreateInfo::default(), None) } + .expect("create the readback fence"); + + let buffer_ci = vk::BufferCreateInfo::default() + .size(FRAME_BYTES as u64) + .usage(vk::BufferUsageFlags::TRANSFER_DST) + .sharing_mode(vk::SharingMode::EXCLUSIVE); + // SAFETY: live device; destroyed in `destroy`. + let buffer = + unsafe { device.create_buffer(&buffer_ci, None) }.expect("create the staging buffer"); + // SAFETY: the buffer was just created on this device. + let req = unsafe { device.get_buffer_memory_requirements(buffer) }; + // SAFETY: live instance + physical device (fn contract). + let props = unsafe { instance.get_physical_device_memory_properties(pd) }; + let wanted = vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT; + let type_index = (0..props.memory_type_count) + .find(|&i| { + req.memory_type_bits & (1u32 << i) != 0 + && props.memory_types[i as usize] + .property_flags + .contains(wanted) + }) + .expect("a HOST_VISIBLE|HOST_COHERENT memory type for the staging buffer"); + let alloc = vk::MemoryAllocateInfo::default() + .allocation_size(req.size) + .memory_type_index(type_index); + // SAFETY: live device, size from the requirements just queried; freed in + // `destroy`. + let memory = + unsafe { device.allocate_memory(&alloc, None) }.expect("allocate staging memory"); + // SAFETY: fresh buffer bound to fresh memory of the required size. + unsafe { device.bind_buffer_memory(buffer, memory, 0) }.expect("bind staging memory"); + // SAFETY: the memory is HOST_VISIBLE and not yet mapped; the mapping + // lives until `destroy` frees the memory (implicit unmap). + let mapped = + unsafe { device.map_memory(memory, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty()) } + .expect("map the staging buffer") + .cast_const() + .cast::(); + + Self { + device: device.clone(), + queue, + cmd_pool, + cmd, + fence, + buffer, + memory, + mapped, + } + } + + /// Copy `frame`'s cropped NV12 planes into the staging buffer and return + /// them tightly packed (Y `w*h` bytes, then interleaved UV `w*h/2` bytes) — + /// exactly the layout ffmpeg's `-f rawvideo -pix_fmt nv12` writes, so the + /// hashes compare 1:1 and row pitch/crop padding can never leak in + /// (`bufferRowLength = 0` packs rows at the copy extent). + /// + /// # Safety + /// + /// `frame` was delivered by a decoder on this device and is not yet + /// released; its image carries TRANSFER_SRC usage (the decoder's + /// `PF_VKD_TEST_READBACK` hook); no other work uses the graphics queue or + /// this frame's image concurrently (the test is fully serialized). + unsafe fn read_nv12(&self, frame: &DecodedVkFrame) -> Vec { + assert_eq!( + (frame.crop.width, frame.crop.height), + (DISPLAY_W, DISPLAY_H), + "the vector's display size (goldens hash exactly this region)" + ); + assert_eq!( + (frame.crop.x % 2, frame.crop.y % 2), + (0, 0), + "chroma-aligned crop origin" + ); + + let begin = vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + // SAFETY: the buffer came from a RESET_COMMAND_BUFFER pool (begin + // implicitly resets) and its previous submission was fence-waited. + unsafe { self.device.begin_command_buffer(self.cmd, &begin) } + .expect("begin the readback command buffer"); + + let subresource = vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: frame.layer, + layer_count: 1, + }; + // Into TRANSFER_SRC: execution/memory dependencies against the decode + // are carried by the timeline wait at submit (visibility included), so + // no src access is needed here. + let to_transfer = vk::ImageMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) + .src_access_mask(vk::AccessFlags2::empty()) + .dst_stage_mask(vk::PipelineStageFlags2::COPY) + .dst_access_mask(vk::AccessFlags2::TRANSFER_READ) + .old_layout(frame.layout) + .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(frame.image) + .subresource_range(subresource); + let dep = + vk::DependencyInfo::default().image_memory_barriers(std::slice::from_ref(&to_transfer)); + // SAFETY: recording state; the image is live until release (fn contract). + unsafe { self.device.cmd_pipeline_barrier2(self.cmd, &dep) }; + + // Two plane copies, crop applied at the source (plane-1 offsets/extents + // are in the R8G8 plane's own half-resolution coordinates), rows packed + // into the buffer at the copy extent. + let layers = |aspect| vk::ImageSubresourceLayers { + aspect_mask: aspect, + mip_level: 0, + base_array_layer: frame.layer, + layer_count: 1, + }; + let regions = [ + vk::BufferImageCopy { + buffer_offset: 0, + buffer_row_length: 0, + buffer_image_height: 0, + image_subresource: layers(vk::ImageAspectFlags::PLANE_0), + image_offset: vk::Offset3D { + x: frame.crop.x as i32, + y: frame.crop.y as i32, + z: 0, + }, + image_extent: vk::Extent3D { + width: DISPLAY_W, + height: DISPLAY_H, + depth: 1, + }, + }, + vk::BufferImageCopy { + buffer_offset: u64::from(DISPLAY_W * DISPLAY_H), + buffer_row_length: 0, + buffer_image_height: 0, + image_subresource: layers(vk::ImageAspectFlags::PLANE_1), + image_offset: vk::Offset3D { + x: (frame.crop.x / 2) as i32, + y: (frame.crop.y / 2) as i32, + z: 0, + }, + image_extent: vk::Extent3D { + width: DISPLAY_W / 2, + height: DISPLAY_H / 2, + depth: 1, + }, + }, + ]; + // SAFETY: the image is in TRANSFER_SRC_OPTIMAL via the barrier above and + // carries TRANSFER_SRC usage (fn contract); the buffer's FRAME_BYTES + // exactly spans the two packed regions. + unsafe { + self.device.cmd_copy_image_to_buffer( + self.cmd, + frame.image, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + self.buffer, + ®ions, + ); + } + + // Restore the video layout (the presenter contract: the consumer puts + // the image back exactly as delivered) and make the copy host-readable. + let restore = vk::ImageMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::COPY) + .src_access_mask(vk::AccessFlags2::empty()) + .dst_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) + .dst_access_mask(vk::AccessFlags2::empty()) + .old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL) + .new_layout(frame.layout) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(frame.image) + .subresource_range(subresource); + let host_read = vk::BufferMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::COPY) + .src_access_mask(vk::AccessFlags2::TRANSFER_WRITE) + .dst_stage_mask(vk::PipelineStageFlags2::HOST) + .dst_access_mask(vk::AccessFlags2::HOST_READ) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .buffer(self.buffer) + .offset(0) + .size(vk::WHOLE_SIZE); + let dep = vk::DependencyInfo::default() + .image_memory_barriers(std::slice::from_ref(&restore)) + .buffer_memory_barriers(std::slice::from_ref(&host_read)); + // SAFETY: recording state; own buffer, live image. + unsafe { self.device.cmd_pipeline_barrier2(self.cmd, &dep) }; + // SAFETY: recording above is complete and valid. + unsafe { self.device.end_command_buffer(self.cmd) }.expect("end the readback commands"); + + // Wait `value`, signal `value + 1` — the DecodedVkFrame sync contract. + let wait = vk::SemaphoreSubmitInfo::default() + .semaphore(frame.semaphore) + .value(frame.value) + .stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS); + let signal = vk::SemaphoreSubmitInfo::default() + .semaphore(frame.semaphore) + .value(frame.value + 1) + .stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS); + let cmd_info = vk::CommandBufferSubmitInfo::default().command_buffer(self.cmd); + let submit = vk::SubmitInfo2::default() + .wait_semaphore_infos(std::slice::from_ref(&wait)) + .command_buffer_infos(std::slice::from_ref(&cmd_info)) + .signal_semaphore_infos(std::slice::from_ref(&signal)); + // SAFETY: live queue/fence; the semaphore is the frame's timeline + // semaphore (fn contract), the fence was reset after its last use. + unsafe { + self.device + .queue_submit2(self.queue, std::slice::from_ref(&submit), self.fence) + } + .expect("submit the readback"); + // SAFETY: the fence was just submitted. + unsafe { + self.device + .wait_for_fences(&[self.fence], true, 10_000_000_000) + } + .expect("readback completes within 10s"); + // SAFETY: the fence was observed signalled above. + unsafe { self.device.reset_fences(&[self.fence]) }.expect("reset the readback fence"); + + // SAFETY: `mapped` points at FRAME_BYTES host-coherent bytes; the fence + // wait (plus the HOST_READ barrier) ordered the device writes before + // this host read. + unsafe { std::slice::from_raw_parts(self.mapped, FRAME_BYTES) }.to_vec() + } + + /// # Safety + /// + /// No submission in flight (every `read_nv12` fence-waited before + /// returning) and nothing else references these handles. + unsafe fn destroy(&self) { + // SAFETY: own handles on the live device, idle per the fn contract; + // freeing the memory implicitly unmaps it. + unsafe { + self.device.destroy_buffer(self.buffer, None); + self.device.free_memory(self.memory, None); + self.device.destroy_fence(self.fence, None); + self.device.destroy_command_pool(self.cmd_pool, None); + } + } +} + +/// Wait the frame's decode verdict, read + hash its pixels, release it (with +/// the presenter write-back the readback enqueued). `index` is the display +/// index the hash will land at. +fn consume_frame( + decoder: &mut VkH264Decoder, + readback: &Readback, + frame: &DecodedVkFrame, + index: usize, +) -> String { + assert_eq!( + decoder.wait_status(frame), + DecodeStatus::Ok, + "frame {index}: decode op not COMPLETE\n state: {}", + decoder.debug_snapshot() + ); + // SAFETY: the frame is delivered and unreleased on the readback's device; + // the pool carries TRANSFER_SRC (PF_VKD_TEST_READBACK was set before the + // decoder's first decode); the test is fully serialized, so nothing else + // touches the graphics queue or this image. + let nv12 = unsafe { readback.read_nv12(frame) }; + decoder + .release_frame(frame, true) + .unwrap_or_else(|e| panic!("frame {index}: release failed: {e}")); + sha256_hex(&nv12) +} + +#[test] +#[ignore = "needs a Vulkan Video H.264 decode device (fleet boxes; see module docs)"] +fn every_frame_hashes_bit_identical_to_libavcodec() { + // The decoder reads this at session creation (first decode call): pool + // images grow TRANSFER_SRC so vkCmdCopyImageToBuffer is legal. + std::env::set_var("PF_VKD_TEST_READBACK", "1"); + + let goldens = golden_hashes(); + assert_eq!( + goldens.len(), + 250, + "the golden file carries one hash per libavcodec frame" + ); + + // ---- instance ---- + // SAFETY: loads the system Vulkan loader; no Vulkan objects exist yet. + let entry = unsafe { ash::Entry::load() }.expect("a Vulkan loader on this box"); + let app = vk::ApplicationInfo::default().api_version(vk::make_api_version(0, 1, 3, 0)); + let instance_ci = vk::InstanceCreateInfo::default().application_info(&app); + // SAFETY: valid create info rooted in locals; the instance is destroyed at the + // end of this test after everything created from it. + let instance = + unsafe { entry.create_instance(&instance_ci, None) }.expect("create a Vulkan 1.3 instance"); + + // Optional vendor pin (`PF_VKD_SMOKE_VENDOR`, hex `0x1002` or decimal) — the + // smoke test's knob, same semantics: makes multi-GPU runs attributable. + let vendor_filter: Option = std::env::var("PF_VKD_SMOKE_VENDOR").ok().map(|raw| { + let trimmed = raw.trim(); + trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + .map_or_else(|| trimmed.parse(), |hex| u32::from_str_radix(hex, 16)) + .unwrap_or_else(|_| panic!("PF_VKD_SMOKE_VENDOR is not a PCI vendor id: {raw:?}")) + }); + + // ---- physical device with an H.264 decode queue family ---- + // SAFETY: live instance. + let physical_devices = + unsafe { instance.enumerate_physical_devices() }.expect("enumerate physical devices"); + let mut picked: Option<(vk::PhysicalDevice, u32, u32)> = None; + for pd in physical_devices { + // SAFETY: `pd` was just enumerated from this instance. + let props = unsafe { instance.get_physical_device_properties(pd) }; + if vendor_filter.is_some_and(|vendor| props.vendor_id != vendor) { + continue; + } + // SAFETY: `pd` was just enumerated from this instance. + let ext_props = + unsafe { instance.enumerate_device_extension_properties(pd) }.unwrap_or_default(); + let has = |name: &std::ffi::CStr| { + ext_props.iter().any(|e| { + e.extension_name_as_c_str() + .is_ok_and(|extension| extension == name) + }) + }; + if !(has(ash::khr::video_queue::NAME) + && has(ash::khr::video_decode_queue::NAME) + && has(ash::khr::video_decode_h264::NAME)) + { + continue; + } + // SAFETY: live physical device; the two-call form fills the chained video + // properties for each family. + let family_count = unsafe { instance.get_physical_device_queue_family_properties2_len(pd) }; + let mut video_props = vec![vk::QueueFamilyVideoPropertiesKHR::default(); family_count]; + let mut families: Vec> = video_props + .iter_mut() + .map(|v| vk::QueueFamilyProperties2::default().push_next(v)) + .collect(); + // SAFETY: as above, arrays sized to the reported count. + unsafe { instance.get_physical_device_queue_family_properties2(pd, &mut families) }; + let flags_per_family: Vec = families + .iter() + .map(|f| f.queue_family_properties.queue_flags) + .collect(); + drop(families); // release the &mut borrows so video_props is readable + + let mut decode_qf = None; + let mut graphics_qf = None; + for (index, flags) in flags_per_family.iter().enumerate() { + if flags.contains(vk::QueueFlags::GRAPHICS) && graphics_qf.is_none() { + graphics_qf = Some(index as u32); + } + if flags.contains(vk::QueueFlags::VIDEO_DECODE_KHR) + && video_props[index] + .video_codec_operations + .contains(vk::VideoCodecOperationFlagsKHR::DECODE_H264) + && decode_qf.is_none() + { + decode_qf = Some(index as u32); + } + } + // Unlike the smoke test this one NEEDS a graphics queue (the readback + // runs there), so a device without one is skipped, not defaulted. + if let (Some(decode), Some(graphics)) = (decode_qf, graphics_qf) { + picked = Some((pd, decode, graphics)); + break; + } + } + let (pd, decode_qf, graphics_qf) = picked.expect( + "a physical device with VK_KHR_video_decode_h264, a decode queue and a graphics queue", + ); + + // Attribution header: which device (and driver) this run actually exercised. + { + let mut driver_props = vk::PhysicalDeviceDriverProperties::default(); + let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut driver_props); + // SAFETY: live physical device; the chain fills the Vulkan 1.2 core + // driver-identity struct. + unsafe { instance.get_physical_device_properties2(pd, &mut props2) }; + let props = props2.properties; + eprintln!( + "picked: {:?} vendor=0x{:04x} driver={:?} info={:?}", + props.device_name_as_c_str().unwrap_or(c"?"), + props.vendor_id, + driver_props.driver_name_as_c_str().unwrap_or(c"?"), + driver_props.driver_info_as_c_str().unwrap_or(c"?"), + ); + } + + // ---- logical device: decode (+ graphics) queues, video + sync features ---- + let priorities = [1.0f32]; + let mut queue_infos = vec![vk::DeviceQueueCreateInfo::default() + .queue_family_index(decode_qf) + .queue_priorities(&priorities)]; + if graphics_qf != decode_qf { + queue_infos.push( + vk::DeviceQueueCreateInfo::default() + .queue_family_index(graphics_qf) + .queue_priorities(&priorities), + ); + } + let extensions = [ + ash::khr::video_queue::NAME.as_ptr(), + ash::khr::video_decode_queue::NAME.as_ptr(), + ash::khr::video_decode_h264::NAME.as_ptr(), + ]; + let mut features12 = vk::PhysicalDeviceVulkan12Features::default().timeline_semaphore(true); + let mut features13 = vk::PhysicalDeviceVulkan13Features::default().synchronization2(true); + let device_ci = vk::DeviceCreateInfo::default() + .queue_create_infos(&queue_infos) + .enabled_extension_names(&extensions) + .push_next(&mut features12) + .push_next(&mut features13); + // SAFETY: live physical device, valid create info rooted in locals; destroyed + // at the end of this test after the decoder drops. + let device = + unsafe { instance.create_device(pd, &device_ci, None) }.expect("create the decode device"); + + // ---- decode the WHOLE vector, hash every display-order frame ---- + let handles = DeviceHandles { + get_instance_proc_addr: entry.static_fn().get_instance_proc_addr as usize, + instance: instance.handle().as_raw() as usize, + physical_device: pd.as_raw() as usize, + device: device.handle().as_raw() as usize, + decode_qf, + decode_queue_index: 0, + graphics_qf, + }; + let mut hashes: Vec = Vec::new(); + { + // SAFETY: the handles above are live for this whole block (the decoder + // drops at its end, before the device/instance destroys below), the device + // was created with the decode extensions + timeline/sync2 features, and + // the queue fields name the families/queues created above. + let mut decoder = unsafe { VkH264Decoder::new(&handles, Box::new(NoopQueueLock)) } + .expect("wrap the device"); + // SAFETY: live instance/device; queue 0 of `graphics_qf` was created + // above; destroyed at the end of this block after its last read. + let readback = unsafe { Readback::new(&instance, pd, &device, graphics_qf) }; + + let aus = split_into_aus(TEST_25FPS); + for (au_index, au) in aus.iter().enumerate() { + let mut next = decoder.decode(au).unwrap_or_else(|e| { + panic!( + "AU {au_index}: decode failed: {e}\n state: {}", + decoder.debug_snapshot() + ) + }); + while let Some(frame) = next { + let hash = consume_frame(&mut decoder, &readback, &frame, hashes.len()); + hashes.push(hash); + next = decoder.take_ready(); + } + } + // The decoder emits in bumping (display) order and the stream may hold + // frames — the flush tail belongs in the comparison too. + decoder.flush(); + while let Some(frame) = decoder.take_ready() { + let hash = consume_frame(&mut decoder, &readback, &frame, hashes.len()); + hashes.push(hash); + } + eprintln!("final state: {}", decoder.debug_snapshot()); + // SAFETY: every readback was fence-waited inside `read_nv12`; nothing + // else references its handles. + unsafe { readback.destroy() }; + } + + // ---- teardown (decoder is gone; its Drop drained the queue) ---- + // SAFETY: every object created from the device (the decoder's pools/session, + // the readback's buffer/pool/fence) was destroyed above. + unsafe { + device.destroy_device(None); + instance.destroy_instance(None); + } + + // ---- the verdict ---- + assert_eq!( + hashes.len(), + goldens.len(), + "frame count diverges from libavcodec ({} decoded vs {} golden)", + hashes.len(), + goldens.len() + ); + let mut mismatches = 0usize; + for (index, (got, want)) in hashes.iter().zip(goldens.iter()).enumerate() { + if got.as_str() != *want { + if mismatches < 10 { + eprintln!("frame {index}: MISMATCH\n ours: {got}\n golden: {want}"); + } + mismatches += 1; + } + } + assert_eq!( + mismatches, + 0, + "{mismatches}/{} frames diverge from libavcodec (first 10 printed above; \ + frame 0 is IDR-only — if IT mismatches, suspect readback geometry \ + (pitch/crop) or intra decode; later-only mismatches point at inter \ + prediction / DPB management)", + hashes.len() + ); + eprintln!( + "{} frames bit-identical to libavcodec software decode", + hashes.len() + ); +}