test(pf-vkdecode): frame-hash parity vs libavcodec — bit-exact on the whole fleet
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.
This commit is contained in:
Generated
+1
@@ -3223,6 +3223,7 @@ dependencies = [
|
||||
"ash",
|
||||
"cros-codecs",
|
||||
"pf-bitstream",
|
||||
"sha2",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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::<u8>();
|
||||
|
||||
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<u8> {
|
||||
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<u32> = 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<vk::QueueFamilyProperties2<'_>> = 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<vk::QueueFlags> = 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<String> = 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()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user