diff --git a/crates/pf-vkdecode/tests/common/mod.rs b/crates/pf-vkdecode/tests/common/mod.rs index 12e30f29..3cd04d03 100644 --- a/crates/pf-vkdecode/tests/common/mod.rs +++ b/crates/pf-vkdecode/tests/common/mod.rs @@ -1,18 +1,18 @@ //! Shared Vulkan Video bring-up for the `#[ignore]`d GPU legs. //! -//! `tests/gpu_smoke.rs` and `tests/gpu_parity.rs` each drive TWO codecs, and the +//! `tests/gpu_smoke.rs` and `tests/gpu_parity.rs` each drive THREE codecs, and the //! path from "a Vulkan loader exists" to "a [`DeviceHandles`] a decoder can be //! constructed on" is the same ~150 unsafe lines every time: loader → instance → //! pick a physical device whose queue families carry the codec's decode ops → //! logical device with the decode extensions plus `timelineSemaphore` and -//! `synchronization2`. Four copies of that would be four places for a +//! `synchronization2`. Six copies of that would be six places for a //! fleet-only failure to hide, so it lives here once, parameterised by the one //! thing that genuinely differs between the callers ([`Graphics`]: the parity //! legs read back on a graphics queue and so REQUIRE one, while the smoke legs //! accept a decode-only device and fall back to the decode family — which also //! decides whether pool images end up EXCLUSIVE or CONCURRENT, so it is not //! cosmetic). [`Request::report_families`] exists so a caller CAN suppress the -//! per-family table, but all four legs currently ask for it: it is the first +//! per-family table, but every leg currently asks for it: it is the first //! thing a fleet failure report needs, and it is a physical-device property //! query, never a recorded RESULT_STATUS query, so it cannot trip the RADV VCN //! hang. @@ -64,6 +64,39 @@ pub const TEST_25FPS_H265: &[u8] = include_bytes!( "../../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265" ); +/// The vendored AV1 twin: 320x240 Main 4:2:0 8-bit, no film grain, **250 temporal +/// units carrying 274 coded frames** — 24 units carry two frames each, and those 24 +/// extras are HIDDEN (decoded, referenced, never shown; the vector contains no +/// `show_existing_frame` at all). 250 frames are displayed, which is what the rung +/// delivers and what `data/test-25fps-av1.nv12.sha256` hashes. +/// +/// Note the file name: it is `test-25fps.ivf.av1`, not `test-25fps.av1.ivf` — the +/// directory holds BOTH, byte-identical, and only the former has the `.md5`/`.crc` +/// reference hashes beside it. pf-bitstream's planner tests include this one. +pub const TEST_25FPS_AV1: &[u8] = include_bytes!( + "../../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1" +); + +/// Split the AV1 vector into access units — one IVF packet per TEMPORAL UNIT. +/// +/// Unlike the two Annex-B splitters below there is nothing to re-derive here: AV1 +/// carries no start codes, so an access unit is not something a scan recovers from +/// the elementary stream — it is the container's framing, and the vector's container +/// is IVF. `IvfIterator` is the vendored parser's own reader (the same one +/// pf-bitstream's AV1 planner tests walk), so this is a rename for symmetry with +/// [`split_h264_aus`]/[`split_h265_aus`] rather than a second implementation that +/// could disagree with production. +/// +/// The consequence for the parity legs is worth stating: AV1 has **no prefix-width +/// leg** and needs none. The four-byte-start-code hazard that made HEVC unplayable +/// on every driver (see [`h265_four_byte_start_codes`]) simply has no AV1 +/// counterpart — OBUs are length-delimited, the host hands whole temporal units +/// across, and there is no prefix for a driver to mis-skip. Its absence here is +/// deliberate, not an omission. +pub fn split_av1_aus(stream: &[u8]) -> Vec<&[u8]> { + cros_codecs::bitstream_utils::IvfIterator::new(stream).collect() +} + /// Test-only H.264 AU splitter, mirroring pf-bitstream's /// (`#[cfg(test)]`-private there): a new AU starts at a non-VCL NALU following /// slices, or at a slice whose `first_mb_in_slice` is 0 (the first bit of the byte @@ -190,11 +223,11 @@ pub fn h265_four_byte_start_codes(stream: &[u8]) -> Vec { /// The slice of a decoder's surface the GPU legs drive. /// -/// `VkH264Decoder` and `VkH265Decoder` expose it method-for-method (the crate -/// docs say so deliberately) but share no trait — codec DISPATCH is the client -/// wiring's job, not this crate's. Binding it here lets each GPU leg run ONE body -/// against both codecs, which is the only way "the H.265 leg proves the same thing -/// the H.264 leg does" can be a fact instead of a claim about two hand-copied +/// `VkH264Decoder`, `VkH265Decoder` and `VkAv1Decoder` expose it method-for-method +/// (the crate docs say so deliberately) but share no trait — codec DISPATCH is the +/// client wiring's job, not this crate's. Binding it here lets each GPU leg run ONE +/// body against all three codecs, which is the only way "the AV1 leg proves the same +/// thing the H.264 leg does" can be a fact instead of a claim about three hand-copied /// functions. pub trait TestDecoder { fn decode(&mut self, au: &[u8]) -> Result, VkDecodeError>; @@ -210,7 +243,7 @@ pub trait TestDecoder { fn debug_snapshot(&self) -> String; } -/// Forwarding impl — one macro so the two decoders can never drift into being +/// Forwarding impl — one macro so the three decoders can never drift into being /// driven differently by accident. macro_rules! impl_test_decoder { ($ty:ty) => { @@ -246,6 +279,7 @@ macro_rules! impl_test_decoder { impl_test_decoder!(pf_vkdecode::VkH264Decoder); impl_test_decoder!(pf_vkdecode::VkH265Decoder); +impl_test_decoder!(pf_vkdecode::VkAv1Decoder); /// Serializes the GPU legs within one test binary. Hold it for the whole leg. /// @@ -293,6 +327,18 @@ pub const H265: Codec = Codec { extension: ash::khr::video_decode_h265::NAME, }; +/// AV1 decode (`VkAv1Decoder`). +/// +/// The narrowest of the three on the fleet: `VK_KHR_video_decode_av1` only reached +/// core drivers in 2024, so a box that decodes both H.26x codecs may still report +/// "no physical device with VK_KHR_video_decode_av1", which is a fact about the box. +/// On RADV the extension is additionally behind `RADV_PERFTEST=video_decode`, the +/// same knob the other two need. +pub const AV1: Codec = Codec { + op: vk::VideoCodecOperationFlagsKHR::DECODE_AV1, + extension: ash::khr::video_decode_av1::NAME, +}; + /// What a caller needs from the GRAPHICS queue family — the one behavioural /// difference between the smoke and parity bring-ups, an explicit parameter so /// it cannot drift back into being an accident of two copied loops. diff --git a/crates/pf-vkdecode/tests/data/test-25fps-av1.nv12.sha256 b/crates/pf-vkdecode/tests/data/test-25fps-av1.nv12.sha256 new file mode 100644 index 00000000..a333918d --- /dev/null +++ b/crates/pf-vkdecode/tests/data/test-25fps-av1.nv12.sha256 @@ -0,0 +1,304 @@ +# SHA-256 per DELIVERED frame of test-25fps.ivf.av1, DISPLAY order — 250 frames. +# +# Each frame is the 320x240 render region as tightly packed NV12: +# Y plane 320*240 bytes, then interleaved UV 320*120 bytes = 115200 bytes/frame. +# +# EIGHT-BIT NV12, not P010. The vector's sequence header carries +# `high_bitdepth = 0` / `mono_chrome = 0` / `seq_profile = 0`, i.e. Main 4:2:0 +# 8-bit, so the Vulkan pool is VK_FORMAT_G8_B8R8_2PLANE_420_UNORM and one byte +# per sample is the whole story here. (The ten-bit scar the sibling +# data/test-main10.p010.sha256 header records — P010's ten bits sit in the HIGH +# end of each 16-bit word, NOT yuv420p10le's low end — does not arise for this +# vector, but it is the first thing to check if an AV1 Main-10 golden is ever +# added beside this one.) +# +# NO FILM GRAIN. `film_grain_params_present = 0` in the sequence header, so the +# golden is grain-free and the Vulkan decode profile this is compared against is +# the film-grain-DISABLED one (`Av1ProfileKey::film_grain == false`; grain +# synthesis is part of the Vulkan decode PROFILE, not a per-frame toggle). A +# vector that gained grain would need its own golden AND a device that offers the +# grain-enabled profile — it would not merely change these hashes. +# +# 250, NOT 274. The vector is 250 temporal units carrying 274 coded frames: 24 +# units carry two frames each, and those 24 extras are HIDDEN frames (decoded, +# referenced later, never shown — the vector contains no `show_existing_frame` at +# all, so they are never displayed by any route). The rung delivers DISPLAYED +# frames, one per `dpb.outputs` id, so the golden is 250 entries in display order +# and NOT one per coded frame. pf-bitstream's +# `the_whole_vendored_vector_plans_and_the_frame_count_is_the_parsers` pins all +# four numbers (250 / 274 / 24 / 250) on CPU, and `gpu_parity.rs`'s +# `av1_goldens_and_the_ivf_split_agree_with_the_planner` re-derives the 250 from +# the planner beside this file's line count so the two can never drift apart +# silently. +# +# Generated 2026-08-06 from libavcodec's SOFTWARE decoder (AV1 decoding is exactly +# specified — every conformant decoder is bit-identical), and CROSS-CHECKED between +# two independent builds on two architectures whose 28,800,000-byte raw outputs are +# byte-identical (not merely equal per frame): +# ffmpeg 8.1.1 (Homebrew, macOS arm64, libdav1d) +# ffmpeg 8.0.1-3ubuntu2 (Ubuntu x86_64 in the pf-lxcheck2 image via +# `apt-get install -y ffmpeg`, libdav1d) +# +# ffmpeg -i crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1 \ +# -f rawvideo -pix_fmt nv12 -fps_mode passthrough ref.yuv +# # then split ref.yuv into 115200-byte frames and sha256 each +# +# THIRD, INDEPENDENT CORROBORATION: the vendored vector ships upstream per-frame +# MD5s beside it (test-25fps.ivf.av1.md5, generated by cros-codecs' own gen_crcs.sh +# with `-pix_fmt nv12 -f framehash`), produced on other machines by another +# toolchain at another time. Re-running that command on BOTH builds above +# reproduces all 250 of those MD5s exactly, so this golden is not just two builds +# of one opinion — it agrees with a decode nobody here performed. +# +# Sibling of data/test-25fps.nv12.sha256 (H.264) and data/test-25fps-h265.nv12.sha256 +# (H.265) and consumed the same way, by the AV1 parity leg in tests/gpu_parity.rs. +04a9916b634632172d1e21d1bf1f083305e847225d361784a1d4fb46c49d7b1f +51172eeef06a23063aa881bc6652e43e319c86f954eb21f3cf80d31c3832f365 +9db9cc170509db22fad2436de8afea4bc3601cd645706f21a9a786491ffa4714 +f589376bb225817e337da827ff08d55aec024148691c16589ef135dc74f364d2 +c0ae27f37ccea7405e5508c768c95a96ab44f1e916625f000410f1d82a43912e +a8cc856dd4c2b4d558871d26fb5501385ce900a9eab4f66efbc7be8f8597912b +155a58840b546a6c65ae6bdc16523e723dcf50788b92baf813186026e7b39fb3 +48b6c37599f073ad7f6d61f65756d85d82b5b35a1fecc22cd1491f4c3366bffe +99846bf7c69f3c709621f3c6bbbdf7d385dc5a3736c1702933ea30441c5a2352 +a2f970fc8d340e6978b327f89366660635aec6782b2845c4a5f9319cee79d340 +a3245f047ebff7cacba1baeee09f9eb34977dd578f4a4ef3e4fe40c3601c56be +39ebba702c84e7a342792a85ff922130335d724597fd48e41f32a471fa76333d +8540fe45480763d891673ecf99b0dc4ff23fa1f7b1f8e25c7532805141b6ed85 +88a37ad837a0778ec7465405416e06487e2aeebb1307d550a07e5f66498a0721 +9678d42aca9aaf32d7aad049988ba6ec530babfcd547da545b9d8be4a5e2763f +1f608b7af5d30bdb8e1786af9549b27f216656cb34175f3ab1538c07fe32a91e +29341aeeac9d91760a3e1aa45a0a91b69a8505a125e236d35476e438b2d8b5f3 +18254332b0d8455bff2edff3c6421beabdebe45ef45a1e4da6155190ffdd8c37 +e731deed91bb66157e2c51eb2ca351801205397922fe99936defe6fe53252daa +e52cef7ee6f7cb335350743ef4acb5eb2f1e5a1bf5db030f58afd827499be877 +f46832050dec356423266289871b6e1e48e1a8551ab03e879548f7b05f679f1c +a9505b4fad6f60176131196b37d8d435bb079090e24402b33e96d0ad18676abb +d41c9853a927e9c215c795050a4eb017fdecf8aee3da5e92ec8a09328ebeff4a +73dc659f8c749f92acddf83df17deb23c55b81bdaaefe5efe3444de4eab5d6d0 +51728e28c25190da1812d15bd3652a6ff6f07eb1ebffc80a15819913446d7b5c +861cb38a660b83663dc12da2d806e00a97ea9a17d637285c6f1b66ad66fad16e +8f05f727c2d601fee4c46230ddea131b06d252acdadb6985ec85665e4452b6ec +cbb46e9c3ebb5de26151c90c094bf903c1b0e64827610480e891e0857454be86 +16102b7bb0a6188c5a81ca165d160086a7e1235bda2a318bab0492119aa8c494 +bfc34108b13be73ac0d8ffac1414076f5dd79bd4de686b457a95023bbb14f8ed +6142990f104c14f0f3095f291c9566eeedf650070d722e6da143f594b80931ec +3b7e155c3862dfa85c2326f643617578094fcc2b9dfb2d655e8cc6b014a4da63 +66e2f3d97f3ece05e56ca37159d9586f08c7034a291fc4b003bfd4195c404fef +e1cd61fb1b9b0378027a737ab4affaa590830d71fda2c6d8e93e82af7f0e3d19 +c141b82b6f7b20b4928f715819da5c6c4576d7228202a331a1953676295498bc +28c2b7f4b9fc9653e4ab10cf40b270b14d92e7725370cd3203f92b5ce5531458 +bb4015bd6cf672cd51eb5d30095419b2f4c37de4e299daaec423b5c4c6d3bb92 +bd898e002d41d1539510cdf0820f6757506a781db541e90f793010ca61e633be +7a2e6dafcc16aab285e11f763871f8d06f653aa437d5a874347fa4f3e2902036 +52389449d67fc312137ab62db2ebfd3a6550eab707bd03c04754b2b69ea0c68a +c9e44fd4cc038e9ebaad163effdd7e1fa1fb564a7226680418744266eb7beb13 +cc66d3cc8a0ac53b360695ea9f3851a218f659a2bac5a75569c65fb663a2963a +32bc728bd387325bfc383ff5266e790f59d10cd79f1379da7292e4b5313fac26 +50901e7012c4549cea01c9039568a002b26f0d4861acdda6e1353363fc907b2e +d7d1c8c9f3d8533119164292583ef7f06490ebfc42f39bfa04100eb13f8dc572 +e861a7eec4472574229a22819f4c5312570eb671babf394b5d71250fbebbd963 +ae4f62b2cb9454d16729cc475fc77717d7863dde2b90295983ee83b7c23945dc +cc1f95cba3fdf71afe9f4a17f185aea83059091cfeb217dcd83764a02537c98e +2f01b1a190556fec2760c1bc6b9abd94839e332b6b229178aa8caaad809a76a9 +e7214ac73576798426c38f27936f5222a8f12ff046e4bef4fd7db56765b5cc1b +cf6c88ec0eac4461d28a3544af15271a960ccc19573ef6cb300cfb9800112f88 +559d96b0d160a10581969136b4abc0d653a39ee45b03fbe1dcb04b5a98d245d8 +15631d150c3215a1fabcf3bf0035314d898a546a2b6a4b2255957202c55bca71 +44bfe0041bf7b233e2c9f39023d617b85ffd227b1922d06eee3e3d158db94426 +c520a61755e0f5e0fd78d2791431b220e131a8b7a095b5a29fed2ff66bf3ef4e +c9b176c0d8fb92056f6d29e64a0e6f899b646bc5ea397ca09bffde796314d790 +feb41aaa9c0afa2cd36ae45318b70118aec59ac8ef694bef014d224fd4d9970f +9f00fb66da1f277f88ba31610203e78d9cecdc750bfaf4f773b29a36c3d156b5 +225728f96f584f7a019d2a47b4cc104332d92892ae1f0ff1db2a384f11f8e6e3 +ffaccd50aaed93791506e4db411673303804e3bffd6dce69f1ab0ac9527c26fa +dd889709e85b4f655365111efb007fe05564548898cbdb4c9328c24508a82eb0 +00c2f8613d319adb52e3fcc59014de4204aac015f549e01013c40f6a74b3af9c +84f4f2a6b403a2c255979368d0e5ba4840e1647e3bc20c57f88506701c3adab7 +27bd83a287a03570572f1d70f823841802dccac6e92a718dd29d321b36bf4882 +5829642335c09a3d86f280a674e7d559957f40a9327917e5864f8dba5a30819f +235aa9abe1c41aca79f4a052821a20b3687c6aad26a8921bee251d84645be5bc +83ea45720f61cd77e6998465929e338a5bba0d94b188441570d76d8b5cd7fc42 +612d69a24cd95badafdc4315e1992f08170a212e1a8d20dbe0f54e73142d64aa +5130f8cd836552a00e5f296b03bfe511dc3469ec8484b23fe31f787b445aaad0 +b8e25ead0212886654eb57e4949f3e00c3995360019a8270448031648949f7a2 +b370f67f98a36a98b0080beabdac6b0503646e6971f02c971e1de506d414a4c9 +af95d0cd2bb7191275681e284fb85003831b10884c610e84c55860a2497169d2 +f4a1db148944256991a51531620fb107637f2e6ef298ef100a2dde3694c7c7c2 +a1717c8df1ece7821c7202afafa951c184c097f4c0231105a1380ff9cffd3d72 +695261e9aadc259d2a05926b8591235c35939ef89ec4de0e9dcc858988ff7d95 +cdbbc29d212ce0f66ad991615b2fb01d7d3c9c2caa761a9e11f744ed2a4c0115 +e29fa67c173cbc04bf0a3a6d6fbfb2a6c6208fd71f386b8fca5c6adc0adea614 +481b12556e2a0c019a3dae5cece32af4089e077106cd672ceb79630c76f5190d +848f032dd30b84a12383381a2e689c034377630458651d27f5d3ab946d632389 +7a9a535f138d5a966edb4f27d59faabfbe39d33d92e19c99d1ce58d3b0391d95 +7ccc23f5ee19dbac8be8c6a49145fac7b03d6f3dd19496a4236961098ded7920 +bb4c430d52662df7f2ffaef738c067d489073231834d40b819d58e38c6a44cb4 +0204f817e8e6891657bfcec7b830c244dc9dc028ed753529cc021709a6ac49c7 +4484238b52c1775e3a5436976b00d83f5398e8be0d019c491b383f169e343754 +f78c775cf0da2138af3174b9e36335bdf689af55288002ef64fc5a0cfdce05ea +ad2934c43eb8e088c1f5f51518fa0254e418b941d1c7218f4611246e0981023c +d83c3eefe9437fdebdb111401a3622ca6c33a36490079b7a77dea14e04cb01fa +0b3a39e42415f1181f47f6a9df7e2083a234499a0eecdeb7a073a1421beda8a3 +1ba58406e0e81a9d283b414f3e0cf4c143d2d2b0dbccd6378d1973af864cfedc +2097812bf91b0cad050ddf2a62cae3eb6713f191bed44d52c1ec594706365268 +90bb9b77ad84af847c7915702755b2e28805f1597d65bb04aac805a7135f4468 +c3a7b4d956edce520afaca9510f206982380617f1328f7b37e94aeb3f6827a5c +ccc19ee8a386a1fdadf674a87ce519ed490d98b6dd071810f3286a91cf97fd61 +a0dfdd093e1bebc1c3735bd166b9f63bb52373aae85e3ed7cef0207826ed4dfb +0319dc031150b2f2773743291ca5d9a91f756e0f6224dd6e4da7eb4d88c1bd40 +0c2edd8861e6d3397983c3bdff9dc13d2cae4522197db10401379a8d13898f38 +3393e5f4b352df95d11ad00b70dfd96802c25ea23b9d710623c175a40ad54afc +1c2e35c9db9c862ba64ff1c7b5086b7178e347c5c68e39c916ddad03c55b9418 +95bbab9230052ba329b42fc4ab2bbd4db437e9cd65bb137b0ef604ec196335f4 +b00716a8f666a443345c03b302a90b6f56cbeac378c03c9b70d92c15f038b4f7 +c9f818bf8f49e67945410573eade3b7efb66cbda16eee10d678ff7cb9bce4b0c +28aed1c9e7b11922920b407f5b2cf2c7df363e9f793b3961a0142691cb5a20d1 +8dd178787e662d7aade20307db2b089c09f930788ae1a0bf53ff085060c5e8d2 +48336f5699128d39f91dd9de306ebbdf05ff9f8c0e5ed94692fb75454940b388 +6c67cf9ac541919e8330f736922738f38c63adae4b19f3049f76c724bf5acfbe +56b3569f6c2e7b52b3d87633e1295dd1ea18907e536875643ac07cb262434de0 +316979d5edd9f59ae6d46e75f527a099686ce8e7474009b2962c514479ea8c58 +c24ec25ab6b37e3115ce9e529f6e696270fea37971565543eb93e780da09d905 +9c42b6ef210397982cf52cd2915a687d39e99495cf1276643f270644d2bff2ed +99135c0c59e8e12487b77325af4151be2cc37a77f06b7912ec3d20bc5526ddb3 +82eff222c286981bf376b18ea879cbf48db197d2fc5e5c2143b8cd6c1122c001 +19bf0af707de5933b49d84595e6e9d0c312ef2479f8eda74cf838099328de201 +bbc769d24c42c68fe33d0a3d0cca2ded31091f9430434f66f74d731a8740ecd6 +f7fafac3d6f77b639528bc29222a3ef495ffa2281ff83071edd6978849138e00 +0f3fda4e040e6ea9431eb659cfb31780afe6b59606dd839e6a1ba42e63e90ccd +822d68d0a791832aab0cf32ee5db1681d6061bff00ead8803c1cb3beb38b47e2 +3861804dd6152ebf876be7e24868b7e8f38ae4c4f8a35de9cc4db2caff680ade +fc0827486353b291004ef921f859fed4e158b570dda3c00006f3f654a9c94d46 +88c08db2b7aac161c53c53e10b1a7955c9f34ff7fdb63bdaa018231638d53c80 +e652c08d1ef97bfc4036d24013079e4536b0ab897ce71fee9cdd1aacf873cfe7 +9aa766897bd50812c5ad944f1cbbf85abef921dc177bcbbdb8d5af395729ebbd +d5aa42f129cf03c4404f63875d6500dae74f142b53f90c493f65f4ea6cd52e29 +f046d91a8520f79b82b6c8127eccc10bdfa8dc33c23de610cb88e685a46dcc9d +2e5795b34aaca15982aa068acfbf61d6a91adec487e8e58579d04c03504df010 +1feb946111faaf40be12cec1e8d9e3749241a529978fb16d84a024f8363f14a0 +58a18c5b6c68cd7e966404622e9176c6ba03d7a2b351327dd32114d24eedab6c +a6b592f74134be10420ee0d26a80ce61d6a22b8c858b1f03551f127d469eeb35 +f0aeb9eff65e9a45937d6089ece2d75e2cf08d9723869dd629a85b83f1daf858 +3311498180e377211ebd282c3daea28675d8616c560d233aeb2990f4df3a3157 +bad41313546387ae30b83aad7aedc854fa2761fb18db2f62b8b4f060c84a6bac +444ef76a1b0fc74a410b5bce137d05e073a4461ca10f29406ae47bdcbd6f0b9d +9b45c7c24a19132a9fb2f9833801319ec6fca92dab61c18ca6c8eb6c52b5cbb5 +88f4546ddedf5363d5d267db4a5659986f59c0f559563b11b164fb2e67156467 +a3597d945ee641d1016f42b02db41f93e12a41d49008103119a479fc1a1d64d3 +79c3e3c8aed31550eb0d2fb8762aa0350f7aaeb107fe5c2eac577b39c2cd8d72 +ff483d3e8ba9af42c26233ef67e4d755d64245a68d580c2c1903cbbb9746dfb5 +8572121897bd614f3efa03ef6767f3e21695f2cf48b2476560dfaea16c281762 +fbd251b057a3156b32d9d5565da046dadc8ac51892d9c77b74851b31c7c08053 +f34de916154676ceda653796b53fb2fe9f892094759dd3939279b4c7d512f9a0 +eaccd92e77fcc29f32f202d93e7dae85382397098377e23051f9e0dbba31b34d +fe83002ddfdaedc72986f35f7703cff06a67a4ffa034b7aedddd394b731dccfc +cd23524ffe622557dc33e36b40af70d8726e124bcfe2e26d54d4e2c3b3b19417 +5a858bbe9916be0e0c5e1460ed6e7bb227a6c4a939005dba78a0f6f3a4029215 +e04e68822bc3a196507df77f4a206918438eddef2404e608cfa5c7ee8922c57b +c3c8f51b5b2b2374316c9b2ac39c0d9464f01a220c5d8c5ff6b455aaaf45ccde +146cd43a435e663d6eaf36975e5bb2ea5019891aeaaa762fffada09df61c848b +d2d4b3246c184c3b219abfbd48634107aa0aa426fb190c23aae3f088867371e6 +be86426ad5e44ab73ed6f058625ab920836b532b9634aca82a4a1343342372b2 +34114cecdd6017b74adb6701f9622bc7fba27aa3410c2373479ba5e0540d267d +35017238c99551d827d7dacbef35ed6076630049b165ae83180eb71d81e7823e +058069a3afb141b4158815205f6a74ff1c94318cd4177abbe0c540e5ce70653d +d6b3d6d433ed65df97f9c33a761a31507e5d98db07c632c052518efc1956a099 +e3c8746bbf9ae6f2c620bbcfb07e854d6e75741350df8cf37b57699c000c687c +73d5cf36367ba4e506e2ad34891202da22fe77980ebc4b5d4a4f081f5616726c +abdf3028d25ba750b9295bd1aea2686a15c8f8ba464eb60710b40053279e1bb6 +bc324cc77981f01c72f620c5cc1dd85334f781eafdb8a89b012d8e08216b62a4 +5108ff93f95752c540859a23c31822107398a38f7629f9805cde7d0e826471d6 +0b82dc51c686bd6c062ee38ba028d2abb5d7bd3fe66611294d281b3a2afe581e +efad4f5d6f244eeabaaef316640b123b591297bbc84c3b3fade3fc18d8a35df9 +b034a1c107559c8f444e1791dc5e3c73664d9f05c945b3ed0e237c94fee5cabd +f6423105f012a404af935e7a7385095579fe417a27b932372bc357d01bd0ec5e +ef421cd426de02948ff7cf456e9a69f3665e6be7d1a3f0ea42821e62a3faa44d +8c5f37b28e980057d5f168514f4031283c1bc45e0f5d28ef97640507156e4999 +a40ffe4ad42e22f4e79ab1d4565848957bb4526d8dd68016796c86eb7006c4a4 +7c193d2d343bfdc36dae5e3f49b2f73b1c77d9b41a1493fb88d1df5a8ed2f95c +7b316fb7c9be2138fd04cc55ed2ecd4158e46239062f2890d3262dbb73996f86 +511cc99acfd3c66096d01e1225d3eab1f9ff1a14fcc84a5e8606813da3a4d5bb +2f3799815a546dced31bdf70749462fdc81642142b393f893856afe40f1ca764 +1bb241bcbd388fe6ca5c6d6070d0526dac14abb6569dec2eb036cfe3bba694bd +4267b825cb3a692eda0d1bd75d48f6d1f74de5c256434192c9ae27220d552ab8 +294a8e9277ae256c7e519515deaef45a2c41ce223864c9468db54c8e44233045 +ce0842add29867140313f607508076ae9fd4b7f3ed7117a0c969cdfdcda47aca +15b4e0600d0cef8591e53681c92aedaaec419b202094533935cf52fd3419282d +2a2f74a4d066a09c26677867345d64edf41dc5f419e0fec3b2f4618708164585 +9852a0524833dc3e60d68924558ad424b9d6fd498351b926db7c3b9501b79e97 +6fd26a7b2c5fded3688130e003560a8ddd58d0f9bc2906ec1b679842d1cf16c5 +7bf43d1d54f813057713c66ed1ac69060c26ee9dbfa10de7bee70f1272e4c4cd +ae3dd2cfb8d7ebf28fbee064b9ee95e8ebc555e479c48a6b64e1e453f4951bf7 +93850ae80180f782c311c182d21207991df3db3b17d9f76af910ac655ae5c1fa +e8dc4065580d4e764de7efc30498824cd2e1a3bc32db46211c8f8128182d41d7 +d154299c13772040f936fc48ac1c84001baad3ba97f42c935f34e49eb5f647c1 +daf8fe6bb8f25a1c4e07829fd7bd282ece7ac184c4671465036c102814c03ddf +fc73ca6be0515992d070467b0f099c075f733d85d3d2202e333c20dcde11e5cf +f1d624e05f1d4bbd19595e52d33c6ee9cf655bd7a830078a5d989f88d5e7fcff +9457a3d05a76811600fae446c1391963aa411ac2ce4e53784af7b5a688625fdc +b153bd9cccfd998ca9bfe291fe12f3c234fdfa342e4124c3575d1e35443ea669 +274f46071fda32c474cddbd234b837a0aacb3c57638eb6d5079cf1ffbea1651c +874d47f84440c15fdd0c37d2c20d471e1675de27671208e007c8507ea0d7a313 +77c47246000c7168b87adeca3b5c5d80e8a5e9d723086a73ed99a14d01ac272c +ec77435e3a8469c287bffecfac6c0e08505a467da7f4b6d09ab3248f1c1d47a8 +013c86ff9eed9007d8d44fd15fd4b050958e7737a65add691c126cbba7ad10f7 +b7adacbe6bbb4a33f0346215f67566fe4e6adcd43c221bcdf6cc112b07323396 +d37d05cebb4b4145b1934555a064069659c8a6c91854c5e207ff4e065c4df4ce +6c377b3e07d0b16a73ff58ea7cb50fc50c49aa38cccc7dba6f8c14db16139655 +1988b08348836747be71e0828dd0ef306c437043eda757eb5baa0cf3af860dc0 +f49648f9d652552863ac10227bbfe5c29d43c8c7e2dd436d30cca8adcdcaa40f +c8e47679b7d12b5335e24f86ee4f4a37d9bfae6ff70c99b92cba16960169fe82 +e1c353d3fd564e699359b4d5046b26a5e26323192a7a9b43be685ffbc60959ef +c87c2abb582a2969f68b46a98815f4e0bc8d003fc2fec32a529126fb07e63be0 +d88be32ee96a5b0eb436042b3ad8017c48abf3d1fdf8cda290fb3ba3fcae5618 +cab86bce00ce96e259056298408e2bf51026801b3985be7187b927ba1433e5b1 +93b729c6995d3576c286de166850681592876dfa30bd7d2cd979aa510de00964 +cd610cb405436263464bf116632a2dd71ae379efe1f38d66238308c82941a8e9 +1925666c067fada0dc9dcb9019198829071229aa7ffadd6aca98c1b918a88a3c +c975916319c5b80581908e88483d301c977f2b463a71f2554597bcb0e133924d +2f28cc1210626c92c5720c9013ad4e610383911f5a8f8786dd7ae85e70ae11db +9c826990635030face0b4016d139bab00be992debbf2cc5ae17d55ba6c3c2bc8 +7710fdb6e25f11880b021afe45244f128df67b750f634598e3902edb184dd2f7 +59d739493f8d60822d323b4236dc38073e3c231b5c1ce90ef2b2744afe503c3d +e8b7bcf58d95f3d7ff5b26ea7c5bf82583f4cfe752569ae97904c668dd2880cc +b537e54d9368c2a70ae6bd37c16d7d55476c1aa25247b191db34c03723d7cc89 +9d5b75f00298e2d088f4a47b8422c31f793e3c7be0093ba696c9234e213b1f45 +70cab03ae1fb6d1d8d4f090dab142ed3bb8079b924564513cd65ad2d1ff50ebe +2699bb193449fb1b85afc49e0504cd84cab903b4329a15fd3cb423ec27eca250 +5f23520761f8ac1b09e9da8dd3cc85753f12921320b76e3bb534e8f375d71e17 +746f5bd533e793d2604bebd0e1514bec7865e92c58ea1abe554e30b27abdaf0f +d9bd51b37c524ef653c108be5a4c5e38c4bf16da7328a0e3f88a520298975bf5 +45a7ea96e52ab5344bb5b9b69088123861b4f29d94b918b3fc895280a258022b +29c2b08cae2f068b818792e27b5ec01e2fded550a531ecbc9385db925e9269b6 +7b6169487d371b695f2e3837cc2a2e4d6fe52a6bc6f6949ee9ee36212de897c3 +021b8dd7a86360029f7028571c9e3226057a61909b67b1471610d354eb49e270 +18c55b4d764148e3052f1232d068a542697b30f0b522d5e544ffd6e41fb62314 +f374d94bb22cf9cfdb7ace086f4e9c4e9e30bdcfab544eaefa2d23c1a39995d4 +ddb1802bc2645c54f916ba144fd96141f1705fab084ff645e11ecdabe4788d48 +5cde86b5f8a25c323774e8ba27a5cb4158f1bad5da8b75d9a530f476ddd9cb72 +197389c50e97878feaf583df1d5ef5b0f48154207c40dba89c6a72f9ed367924 +5459967d1c6b9f1dfa95e7b4564907fa1e9f42a31d77856402fb0af27c81e5aa +647006388c6fa8f6a69b432172b31ee56835b53ba9a4d2a8d7646eba8d699424 +6d4e9ef701148328c0ba14ac0a62b41ae96babe384542731ff8b7c5ef46fe63c +b6d3de098262e4d24011095345bf3bac97433401a1d96f0233b79c7e48ba55a7 +f4a81172ff9454e221b669c822ab3b5f76cdd8e62d3fa923e42eaaa58e60e6b2 +053ee6f10d5d37d451656c6e25c33110296f02bd5e2461ea710cd049006b224b +b2ac14a086577e381e80865a66cce66300b72e961a1d7b8a817b9f4f5189d71d +ceebdccc9606c9d6d59f0b8b27b8f7da95107d27864e9e1fef6d4d5379039e7a +431569f73856c9478390635049ef07fc73a7b9ad2f0e58e71b806068e93fd844 +be4474bcf98e5112f903d54799dec8f6e8b738a6e9d901e6cbe2c1e99f886c7a +9be9d34ab68928b3fccb05e1863fcd2a8302400b748f3d4de0411c614fca0673 +de433edf968140c2f32a4ad3cedb5153cc4e65a7d846cee1d621ef7cd5e1ce25 +1fc5889fa4fc6aa5111cb3c816a75feea5fd7159a0090d950a50935045cb2de2 +df8ad0344cc492f244ef63288517116db1404f5bac56f362ee214ff41062f6ba +4c7a7ad1a5ebe5c7fa2c075a95b13e67418ddd3ee95c72fbca66b36150daeb63 +07c6d9e6128d4844d7665e3a12d8d9d67a104cc4bb0a7a14a34c8355aebaa486 +5583161f61a665f7d03ceca5b878621942659303c856245887bca36adab7f834 +59687559c0c38a27cc79c6d72e77d238569427a040d992d28c22a70d34dd918b +a3485384bb20822e4b0e67f95764cd765884912149c2cd3b89d5072023044b08 +21b3d28c3324e7c2e97106e8459eb8bc56946a326fd584049e8ff93007efc368 +207bba7349318b67dc97af31762acb76a8718ef4749dbbbe51240abea6d03c72 +c60a83c60c28e3b173a6a2e4548ce11e4760f99ea740b1900697d946136d86bd +36c83d820c97b467810e84727782fbc64b355a0692acac00621bb8817dd08080 +2917e0ad1745f53b79359df1be6886138d54219e01ddcca28525d6187ef7cf40 diff --git a/crates/pf-vkdecode/tests/gpu_parity.rs b/crates/pf-vkdecode/tests/gpu_parity.rs index 6934d0eb..8c02a8a6 100644 --- a/crates/pf-vkdecode/tests/gpu_parity.rs +++ b/crates/pf-vkdecode/tests/gpu_parity.rs @@ -1,4 +1,4 @@ -//! GPU frame-hash parity tests (WP-D) — the two decode legs are `#[ignore]`d +//! GPU frame-hash parity tests (WP-D) — the decode legs are `#[ignore]`d //! because they need real Vulkan Video hardware; the coherence guards at the //! bottom of this file are not, and run in ordinary CI. //! @@ -8,35 +8,54 @@ //! cargo test -p pf-vkdecode --test gpu_parity -- --ignored --nocapture //! ``` //! -//! (RADV boxes additionally need `RADV_PERFTEST=video_decode`; multi-GPU boxes +//! (RADV boxes additionally need `RADV_PERFTEST=video_decode` — for AV1 as much as +//! for the other two, and without it `bring_up` reports "no physical device with +//! VK_KHR_video_decode_av1", which reads like missing silicon; multi-GPU boxes //! pin the vendor with `PF_VKD_SMOKE_VENDOR=0x1002` / `0x10de`, same knob as //! the smoke tests. Device bring-up lives in `tests/common/mod.rs`.) //! -//! What they prove: H.264 and H.265 decoding are both exactly specified — every +//! What they prove: H.264, H.265 and AV1 decoding are all exactly specified — every //! conformant decoder must produce bit-identical output — so the vendored 25fps -//! vector of each codec is decoded through [`VkH264Decoder`] / [`VkH265Decoder`], +//! vector of each codec is decoded through [`VkH264Decoder`] / [`VkH265Decoder`] / +//! [`VkAv1Decoder`], //! 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 //! decoders' `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` and `data/test-25fps-h265.nv12.sha256`). ALL +//! `data/test-25fps.nv12.sha256`, `data/test-25fps-h265.nv12.sha256` and +//! `data/test-25fps-av1.nv12.sha256`). ALL //! frames are collected, including the tail `flush` delivers, and the frame count //! must match libavcodec's too. //! -//! Both legs run ONE body ([`collect_hashes`]) over `common::TestDecoder`, so the -//! H.265 leg cannot quietly test something weaker than the H.264 one. A box that -//! decodes only one codec runs that leg and reports the other as "no physical -//! device with VK_KHR_video_decode_…", which is a fact about the box. +//! Every leg runs ONE body ([`collect_hashes`]) over `common::TestDecoder`, so the +//! H.265 and AV1 legs cannot quietly test something weaker than the H.264 one. A box +//! that decodes only some of the three runs those legs and reports the rest as "no +//! physical device with VK_KHR_video_decode_…", which is a fact about the box — +//! and on today's fleet AV1 is the one most likely to say so. //! -//! Each codec runs that body TWICE: once over the vendored vector as it sits, -//! and once over the same vector rewritten to FOUR-byte start codes, which is +//! The two Annex-B codecs run that body TWICE: once over the vendored vector as it +//! sits, and once over the same vector rewritten to FOUR-byte start codes, which is //! what the real host emits on 100% of access units in both codecs (1514/1514 //! H.264 and 1133/1133 HEVC, measured off the M0 NVENC corpus). Prefix width //! carries no information, so both runs must reproduce the same goldens — //! and submitting the four-byte form to the driver unchanged is precisely the //! defect that made HEVC unplayable on every driver tested. Until these legs -//! existed no parity vector exercised the form that actually ships. +//! existed no parity vector exercised the form that actually ships. **AV1 has no +//! such twin and needs none**: OBUs are length-delimited, so there is no start-code +//! prefix for a driver to mis-skip and no second framing to test (see +//! `common::split_av1_aus`). Its absence is deliberate. +//! +//! # Why the AV1 leg exists at all +//! +//! Because until it did, the AV1 rung had no pixel evidence whatsoever. An +//! adversarial review of the conversion found four defects — per-frame flags left +//! unset on all 274 frames, a units error in `LoopRestorationSize`, per-reference +//! info describing the wrong picture, and zeroed film-grain fields — and every one +//! of them would have shown as a hash mismatch on frame 0 or shortly after, while +//! NONE of them failed clippy or the crate's unit tests. Type-checking a struct +//! conversion cannot tell you the struct describes the right picture; only the +//! pixels can. //! //! 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 @@ -56,6 +75,7 @@ use common::TestDecoder; use pf_vkdecode::DecodeStatus; use pf_vkdecode::DecodedVkFrame; use pf_vkdecode::NoopQueueLock; +use pf_vkdecode::VkAv1Decoder; use pf_vkdecode::VkH264Decoder; use pf_vkdecode::VkH265Decoder; use sha2::Digest; @@ -67,6 +87,12 @@ const GOLDENS_H264: &str = include_str!("data/test-25fps.nv12.sha256"); /// The H.265 twin, cross-checked between two independent FFmpeg builds (header). const GOLDENS_H265: &str = include_str!("data/test-25fps-h265.nv12.sha256"); +/// The AV1 twin: 250 DISPLAYED frames of a 274-coded-frame vector, cross-checked +/// between two independent FFmpeg builds on two architectures AND against the +/// per-frame MD5s cros-codecs vendored beside the vector (full provenance in the +/// file's header — it is the only golden here with a third-party corroboration). +const GOLDENS_AV1: &str = include_str!("data/test-25fps-av1.nv12.sha256"); + /// The ten-bit vector and its goldens. No hardware leg in this file consumes them /// yet — the D3D11VA rung is where the ten-bit parity leg currently runs — but the /// files live here, beside the other goldens, so the guard that keeps them honest @@ -87,15 +113,39 @@ const DISPLAY_H264: (u32, u32) = (320, 240); /// parameter instead of reading one global pair. const DISPLAY_H265: (u32, u32) = (320, 240); -/// Both vectors' picture format: 8-bit 4:2:0. H.264 is NV12 by envelope -/// (`derive_caps` wants nothing else), H.265 Main resolves to it from the SPS — +/// The AV1 vector's display region — its `render_width` x `render_height`, AV1's +/// answer to a conformance window, and what [`DecodedVkFrame::crop`] carries on this +/// rung. Equal to the coded (post-superres) size for this vector, which +/// [`av1_goldens_and_the_ivf_split_agree_with_the_planner`] pins rather than assumes: +/// a re-synced vector whose render region shrank would make the readback crop a +/// region the goldens never hashed. +const DISPLAY_AV1: (u32, u32) = (320, 240); + +/// All three vectors' picture format: 8-bit 4:2:0. H.264 is NV12 by envelope +/// (`derive_caps` wants nothing else), H.265 Main resolves to it from the SPS and +/// AV1 Main (`seq_profile = 0`, `high_bitdepth = 0`) from the sequence header — /// and [`DecodedVkFrame::format`] exists precisely so a pool misconfigured to /// P010 fails loudly instead of hashing differently. const EXPECTED_FORMAT: vk::Format = pf_vkdecode::NV12; -/// Every vendored 25fps vector, in both codecs, is 250 display frames. +/// Every vendored 25fps vector, in all three codecs, is 250 DISPLAY frames. +/// +/// For H.264 and H.265 that is also one per access unit. For AV1 it is emphatically +/// not: its 250 temporal units carry [`AV1_CODED_FRAME_COUNT`] coded frames, 24 of +/// which are HIDDEN — decoded, referenced by later frames, never shown (the vector +/// uses no `show_existing_frame`, so they are displayed by no route at all). The +/// rung delivers one frame per `dpb.outputs` id, so 250 is the number the goldens +/// carry and the number the parity leg must compare. const FRAME_COUNT: usize = 250; +/// The AV1 vector's CODED frame count — 24 more than [`FRAME_COUNT`]. +/// +/// Asserted by the CPU guard so the display/coded distinction stays a measured fact +/// rather than a comment: if a re-sync ever made these two numbers equal, the vector +/// would have lost its hidden-frame coverage (the exact thing that makes AV1's +/// multi-frame temporal units worth testing) while every hash still matched. +const AV1_CODED_FRAME_COUNT: usize = 274; + /// The golden file's hash lines (comments and blanks skipped). fn golden_hashes(file: &'static str) -> Vec<&'static str> { file.lines() @@ -104,6 +154,50 @@ fn golden_hashes(file: &'static str) -> Vec<&'static str> { .collect() } +/// Refuse a golden set that could make a parity verdict vacuous. +/// +/// Three ways a comparison can "pass" while proving nothing, all closed here: +/// +/// - **an empty or short set** — [`assert_bit_identical`] compares `hashes` against +/// `goldens` pairwise and asserts the lengths match, so a file that lost its +/// entries to a bad regeneration would agree with a decoder that delivered +/// nothing. Pinning the count against a constant the CPU guards also re-derive +/// from the planner closes that. +/// - **junk that is not a digest** — a truncated or re-formatted line can never +/// equal a real hash, but a file of blank-looking lines could quietly become a +/// comparison of nothing. +/// - **all entries identical** — the one that matters most on a video codec. If +/// every golden were the same digest, a decoder emitting one frozen frame 250 +/// times would pass, which is precisely the failure mode a broken reference +/// conversion produces. All four golden sets here are fully distinct (250/250 +/// H.264, 250/250 H.265, 250/250 AV1, 50/50 Main 10), so requiring full +/// distinctness is not a weak bound. +fn assert_goldens_are_a_real_set(goldens: &[&str], expected: usize, path: &str) { + assert_eq!( + goldens.len(), + expected, + "{path} must carry one hash per display frame" + ); + assert!( + goldens + .iter() + .all(|line| line.len() == 64 && line.bytes().all(|b| b.is_ascii_hexdigit())), + "{path}: every golden line is a bare lowercase SHA-256 hex digest" + ); + let distinct = goldens + .iter() + .collect::>() + .len(); + assert_eq!( + distinct, + goldens.len(), + "{path}: {distinct} of {} goldens are distinct — a set with repeats (and \ + above all a set that is ALL one digest) would let a decoder that froze on \ + a single frame pass parity", + goldens.len() + ); +} + fn sha256_hex(data: &[u8]) -> String { use std::fmt::Write as _; sha2::Sha256::digest(data) @@ -486,7 +580,16 @@ fn consume_frame( } /// Decode every AU, hash every delivered frame in display order, including the -/// tail `flush` hands back. One body for both codecs. +/// tail `flush` hands back. One body for all three codecs. +/// +/// The flush tail is where the codecs legitimately differ and the body deliberately +/// does not: H.264 and H.265 can hold pictures back for reorder, so their planners' +/// `flush` releases a tail. AV1's planner has no `flush` at all — a shown frame is +/// output by the very temporal unit that decodes it — so `VkAv1Decoder::flush` frees +/// the hidden pictures' images and hands back nothing. Draining afterwards is +/// therefore a no-op for AV1 rather than a special case, and running the identical +/// body means an AV1 rung that ever DID strand a shown frame would be caught by the +/// frame-count assertion instead of hidden by a codec-specific shortcut. fn collect_hashes( decoder: &mut impl TestDecoder, readback: &Readback, @@ -532,21 +635,28 @@ fn assert_bit_identical(hashes: &[String], goldens: &[&str], codec: &str) { goldens.len() ); let mut mismatches = 0usize; + let mut first_divergence: Option = None; 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}"); } + first_divergence.get_or_insert(index); mismatches += 1; } } - assert_eq!( - mismatches, - 0, - "{codec}: {mismatches}/{} frames diverge from libavcodec (first 10 printed \ - above; frame 0 is intra-only — if IT mismatches, suspect readback \ - geometry (pitch/crop) or intra decode; later-only mismatches point at \ - inter prediction / DPB management)", + // The FIRST divergent index is the whole diagnostic: everything after it may be + // downstream of that one frame through prediction and the DPB, so a report that + // only counted mismatches would bury the one number that localises the defect. + assert!( + first_divergence.is_none(), + "{codec}: FIRST DIVERGENT FRAME = {} ({mismatches}/{} frames diverge from \ + libavcodec; up to 10 printed above). Frame 0 is intra-only — if IT is the \ + first, suspect readback geometry (pitch/crop), the picture format, or intra \ + decode / the per-frame parameter conversion; a first divergence LATER points \ + at inter prediction, per-reference info or DPB management, and the frames \ + after it are probably just downstream of it.", + first_divergence.unwrap_or_default(), hashes.len() ); eprintln!( @@ -766,6 +876,115 @@ fn h265_four_byte_start_codes_decode_bit_identically() { ); } +/// The AV1 twin of [`h265_parity_run`]. +/// +/// Concrete where the H.265 one is parameterised, because AV1 has exactly one +/// vendored vector and one shape (Main 4:2:0 8-bit, no film grain, 320x240); the +/// facts it hard-codes are re-derived from the planner, without a GPU, by +/// [`av1_goldens_and_the_ivf_split_agree_with_the_planner`], so a re-synced vector +/// of another shape fails in ordinary CI with the reason rather than on the fleet as +/// a confusing probe refusal. A second AV1 vector (Main 10, or one that uses +/// `show_existing_frame`) is the point at which this should grow the same parameters +/// the H.265 body carries — not before. +fn av1_parity_run(aus: &[&[u8]], label: &str) { + // As the other legs: one codec at a time on the device, and the `set_var` below + // happens only under this lock (see `common::gpu_lock`). + let _gpu = common::gpu_lock(); + + std::env::set_var("PF_VKD_TEST_READBACK", "1"); + + let goldens = golden_hashes(GOLDENS_AV1); + // Non-vacuity, before any hardware is touched: the right number of entries, all + // real digests, all distinct (see the helper's docs — a frozen-frame decoder + // must not be able to pass this leg). + assert_goldens_are_a_real_set(&goldens, FRAME_COUNT, "data/test-25fps-av1.nv12.sha256"); + // …and the leg must actually be fed something. An IVF whose packets failed to + // parse would hand `collect_hashes` an empty AU list, which delivers no frames + // and would then fail as a frame-count mismatch that reads like a decoder defect. + assert_eq!( + aus.len(), + FRAME_COUNT, + "{label}: the vector must split into {FRAME_COUNT} temporal units" + ); + + let setup = common::bring_up(&common::Request { + codec: common::AV1, + // As the other parity legs: the readback records on the graphics queue, so a + // device without a graphics family is skipped rather than defaulted. + graphics: common::Graphics::Required, + report_families: true, + }); + let handles = setup.handles(); + + let hashes = { + // SAFETY: as the H.264/H.265 legs — `setup` outlives this block (destroyed + // below, after the decoder and readback drop at the block's end), it was + // created with the AV1 decode extension + timeline/sync2 features, and its + // queue fields name the families/queues it created. + let mut decoder = unsafe { VkAv1Decoder::new(&handles, Box::new(NoopQueueLock)) } + .expect("wrap the device"); + // The construction-time shape gate, on the vector's own facts: 4:2:0, 8-bit, + // and NO film grain. The third argument is the load-bearing one — grain + // synthesis is part of the Vulkan decode PROFILE, so a box that offers only + // the grain-enabled profile (or only the disabled one) refuses HERE with a + // caps reason instead of failing at the first temporal unit. + decoder + .probe_stream_support(1, 8, false) + .unwrap_or_else(|e| { + panic!("{label}: the box must host AV1 Main 4:2:0 8-bit, no film grain — {e:?}"); + }); + // SAFETY: as the other legs — live instance/device, queue 0 of `graphics_qf` + // was created by the bring-up; destroyed at the end of this block. + let readback = unsafe { + Readback::new( + &setup.instance, + setup.pd, + &setup.device, + setup.graphics_qf, + DISPLAY_AV1, + EXPECTED_FORMAT, + ) + }; + let hashes = collect_hashes(&mut decoder, &readback, aus); + // SAFETY: every readback was fence-waited inside `read_nv12`; nothing else + // references its handles. + unsafe { readback.destroy() }; + hashes + }; + + // SAFETY: as the other legs — the decoder is gone (its Drop drained the queue and + // destroyed its session/pools) and the readback's handles are destroyed. + unsafe { setup.destroy() }; + + assert_bit_identical(&hashes, &goldens, label); +} + +/// The AV1 rung's first pixel evidence. +/// +/// 250 temporal units in, 250 DISPLAYED frames out (the 24 hidden frames the vector +/// also codes are decoded, referenced and never shown — module docs), each read back +/// as tightly packed NV12 over its `render_width` x `render_height` region and +/// compared against libavcodec's software decode. +/// +/// What a failure looks like, and where to point it: +/// - **frame 0** — the sequence header or the per-frame parameter conversion: +/// `StdVideoAV1SequenceHeader`, the eight per-frame sub-blocks (tile info, +/// quantisation, segmentation, loop filter, CDEF, loop restoration, global motion, +/// film grain), the tile-group ranges, or the readback geometry. AV1 puts in the +/// frame header what H.26x puts in parameter sets, so a single wrong field here +/// damages every frame. +/// - **frame 1** — the first frame with a reference. Per-reference info, the +/// reference-NAME → DPB-slot table, or `ref_frame_idx` ordering. +/// - **later, then everything after** — DPB slot management, `refresh_frame_flags`, +/// or the hidden frames: a run that is clean until roughly the first multi-frame +/// temporal unit and wrong thereafter is the signature of the hidden ALTREF being +/// stored wrong or not at all. +#[test] +#[ignore = "needs a Vulkan Video AV1 decode device (fleet boxes; see module docs)"] +fn av1_every_frame_hashes_bit_identical_to_libavcodec() { + av1_parity_run(&common::split_av1_aus(common::TEST_25FPS_AV1), "AV1"); +} + // --------------------------------------------------------------------------- // CPU coherence guards — NOT `#[ignore]`d. // @@ -776,7 +995,10 @@ fn h265_four_byte_start_codes_decode_bit_identically() { // hardware round trip to disprove. // // Each guard pins the whole chain the parity verdict rests on: the AU split, the -// planner's output count, and the golden line count — with NO GPU involved. +// planner's output count, the vector's shape and the golden set — with NO GPU +// involved. And they are what make the verdicts non-vacuous: a comparison of zero +// frames, or of 250 copies of one digest, would otherwise "pass" on any hardware +// (see [`assert_goldens_are_a_real_set`]). // --------------------------------------------------------------------------- #[test] @@ -784,17 +1006,7 @@ fn h265_goldens_and_au_split_agree_with_the_planner() { use pf_bitstream::h265::H265Planner; let goldens = golden_hashes(GOLDENS_H265); - assert_eq!( - goldens.len(), - FRAME_COUNT, - "data/test-25fps-h265.nv12.sha256 must carry one hash per display frame" - ); - assert!( - goldens - .iter() - .all(|line| line.len() == 64 && line.bytes().all(|b| b.is_ascii_hexdigit())), - "every golden line is a bare lowercase SHA-256 hex digest" - ); + assert_goldens_are_a_real_set(&goldens, FRAME_COUNT, "data/test-25fps-h265.nv12.sha256"); // The AU split the parity leg feeds the decoder. `common::split_h265_aus` is // the copy of pf-bitstream's private splitter, and it keys on HEVC's 2-byte @@ -886,17 +1098,7 @@ fn h264_goldens_and_au_split_agree_with_the_planner() { use pf_bitstream::h264::H264Planner; let goldens = golden_hashes(GOLDENS_H264); - assert_eq!( - goldens.len(), - FRAME_COUNT, - "data/test-25fps.nv12.sha256 must carry one hash per display frame" - ); - assert!( - goldens - .iter() - .all(|line| line.len() == 64 && line.bytes().all(|b| b.is_ascii_hexdigit())), - "every golden line is a bare lowercase SHA-256 hex digest" - ); + assert_goldens_are_a_real_set(&goldens, FRAME_COUNT, "data/test-25fps.nv12.sha256"); let aus = common::split_h264_aus(common::TEST_25FPS_H264); assert_eq!( @@ -927,17 +1129,7 @@ fn the_main10_vector_is_ten_bit_and_agrees_with_its_goldens() { use pf_bitstream::h265::H265Planner; let goldens = golden_hashes(GOLDENS_MAIN10); - assert_eq!( - goldens.len(), - MAIN10_FRAME_COUNT, - "data/test-main10.p010.sha256 must carry one hash per display frame" - ); - assert!( - goldens - .iter() - .all(|line| line.len() == 64 && line.bytes().all(|b| b.is_ascii_hexdigit())), - "every golden line is a bare lowercase SHA-256 hex digest" - ); + assert_goldens_are_a_real_set(&goldens, MAIN10_FRAME_COUNT, "data/test-main10.p010.sha256"); let aus = common::split_h265_aus(TEST_MAIN10_H265); assert_eq!( @@ -984,6 +1176,145 @@ fn the_main10_vector_is_ten_bit_and_agrees_with_its_goldens() { ); } +/// The AV1 leg's whole chain, with no GPU: the golden set, the IVF split, the shape +/// the leg hard-codes, and — the one that matters — that **250 goldens is the +/// DISPLAY count of a 274-frame vector**, re-derived from the planner rather than +/// asserted from a comment. +/// +/// Every number here is a way the fleet run could otherwise fail for a reason that +/// is not the decoder: +/// +/// - a golden file regenerated per CODED frame would carry 274 hashes and the leg +/// would report a frame-count mismatch that reads exactly like dropped frames; +/// - an IVF reader that lost packets would feed a short AU list and the leg would +/// report the same thing; +/// - a re-synced vector at another bit depth, sampling, or with film grain would +/// make `probe_stream_support(1, 8, false)` probe the WRONG Vulkan profile and the +/// readback expect the wrong format, which on hardware surfaces as a caps refusal +/// or half a hashed picture; +/// - and if the 24 hidden frames ever disappeared, the leg would still pass while +/// having quietly stopped exercising multi-frame temporal units at all — the one +/// thing AV1 has that neither H.26x vector does. +#[test] +fn av1_goldens_and_the_ivf_split_agree_with_the_planner() { + use pf_bitstream::av1::Av1Planner; + + let goldens = golden_hashes(GOLDENS_AV1); + assert_goldens_are_a_real_set(&goldens, FRAME_COUNT, "data/test-25fps-av1.nv12.sha256"); + + // The AU split the parity leg feeds the decoder: one IVF packet per temporal + // unit. AV1 carries no start codes, so this is the container's framing rather + // than something a scan could get subtly wrong — but a truncated or re-muxed + // vector would still shorten it silently. + let aus = common::split_av1_aus(common::TEST_25FPS_AV1); + assert_eq!( + aus.len(), + FRAME_COUNT, + "the vendored AV1 vector is {FRAME_COUNT} temporal units" + ); + assert!( + aus.iter().all(|au| !au.is_empty()), + "no temporal unit is empty — an IVF reader that returned empty packets would \ + make the parity leg decode nothing and blame the decoder" + ); + + // Walk the CPU planner over the same temporal units. It is the authority on how + // many frames the GPU leg can possibly deliver: the decoder builds exactly one + // delivered frame per `dpb.outputs` id, and AV1's planner has no `flush` tail. + let mut planner = Av1Planner::new(); + let mut outputs = 0usize; + let mut coded_frames = 0usize; + let mut multi_frame_units = 0usize; + let mut show_existing = 0usize; + let mut warnings = 0usize; + for (index, au) in aus.iter().enumerate() { + let plans = planner.plan_au(au).unwrap_or_else(|e| { + panic!("temporal unit {index}: the clean vector must plan without errors, got {e:?}") + }); + if plans.len() > 1 { + multi_frame_units += 1; + } + for plan in &plans { + coded_frames += 1; + outputs += plan.dpb.outputs.len(); + warnings += plan.warnings.len(); + // A `show_existing_frame` decodes nothing and stores nothing. + if plan.dpb.stored.is_none() { + show_existing += 1; + } + // Pin the picture shape both AV1 legs hard-code. They call + // `probe_stream_support(1, 8, false)` and assert an NV12 output format; + // a re-synced Main-10, 4:4:4 or film-grain vector would make both + // silently probe and expect the WRONG Vulkan decode profile on the + // fleet — grain synthesis is part of the PROFILE, not a per-frame + // toggle, so a grain-bearing vector is a different device requirement, + // not merely different pixels. + assert_eq!( + ( + plan.picture.chroma_format_idc, + plan.picture.bit_depth, + plan.sequence.film_grain_params_present, + ), + (1, 8, false), + "frame {coded_frames} (temporal unit {index}): the vendored AV1 vector \ + must stay Main 4:2:0 8-bit with no film grain" + ); + if coded_frames == 1 { + assert!(plan.picture.is_key, "the vector opens on a key frame"); + // What `Readback` crops to, and what the goldens hash. + assert_eq!( + (plan.picture.render_width, plan.picture.render_height), + DISPLAY_AV1, + "the display (render) region the readback asserts against" + ); + // …and what the pool allocates. Equal to the render region here, so + // the vector needs no AV1 conformance-window equivalent — the golden + // header's claim. + assert_eq!( + (plan.picture.upscaled_width, plan.picture.frame_height), + DISPLAY_AV1, + "the decoded (post-superres) picture IS the display region for \ + this vector — coded size and render size coincide" + ); + } + } + } + + assert_eq!( + outputs, + goldens.len(), + "the planner outputs {outputs} pictures but the goldens carry {} hashes — the \ + parity leg's frame-count assertion would fail on hardware for a reason that \ + has nothing to do with the GPU", + goldens.len() + ); + assert_eq!( + coded_frames, + AV1_CODED_FRAME_COUNT, + "the vendored AV1 vector codes {AV1_CODED_FRAME_COUNT} frames; {} of them are \ + hidden, which is why the goldens are {FRAME_COUNT} and not {coded_frames}", + AV1_CODED_FRAME_COUNT - FRAME_COUNT + ); + assert_eq!( + multi_frame_units, 24, + "24 temporal units carry two frames each — the hidden ALTREFs, and the only \ + reason AV1's `plan_au` returns a vector at all. If this reaches 0 the parity \ + leg has stopped exercising multi-frame temporal units while still passing" + ); + assert_eq!( + show_existing, 0, + "this vector uses no `show_existing_frame`; if that ever changes, frames start \ + being displayed by a route the decoder handles differently and the display \ + order the goldens assume needs rederiving" + ); + assert_eq!( + warnings, 0, + "a clean conformance vector must plan without concealment — any warning here \ + means the parity leg would be hashing concealed pixels against a clean \ + reference" + ); +} + /// Count Annex-B start codes in `stream` as `(total, three_byte)`. /// /// Emulation prevention guarantees `00 00 01` cannot occur inside a NAL payload, diff --git a/crates/pf-vkdecode/tests/gpu_smoke.rs b/crates/pf-vkdecode/tests/gpu_smoke.rs index 2f95d07f..b7c4bba3 100644 --- a/crates/pf-vkdecode/tests/gpu_smoke.rs +++ b/crates/pf-vkdecode/tests/gpu_smoke.rs @@ -11,15 +11,18 @@ //! - a Vulkan 1.3 loader on the library path (`libvulkan.so.1` / `vulkan-1.dll`); //! - a physical device advertising `VK_KHR_video_queue`, //! `VK_KHR_video_decode_queue` and the leg's codec extension -//! (`VK_KHR_video_decode_h264` / `VK_KHR_video_decode_h265`), with a queue +//! (`VK_KHR_video_decode_h264` / `VK_KHR_video_decode_h265` / +//! `VK_KHR_video_decode_av1`), with a queue //! family carrying `VIDEO_DECODE_KHR` ops for that codec; -//! - `timelineSemaphore` + `synchronization2` feature support (Vulkan 1.3 core). +//! - `timelineSemaphore` + `synchronization2` feature support (Vulkan 1.3 core); +//! - on RADV, `RADV_PERFTEST=video_decode` in the environment — for AV1 exactly as +//! for the other two, and without it the AV1 leg reports missing silicon it has. //! //! One leg per codec, running the SAME body ([`smoke`]) over the vendored 25fps -//! vector of that codec — a box that decodes only one of the two runs that leg and -//! reports the other as "no physical device with VK_KHR_video_decode_…", which is -//! a fact about the box rather than a failure. Device bring-up lives in -//! `tests/common/mod.rs`. +//! vector of that codec — a box that decodes only some of the three runs those legs +//! and reports the rest as "no physical device with VK_KHR_video_decode_…", which is +//! a fact about the box rather than a failure (AV1 is the one most likely to say so +//! on today's fleet). Device bring-up lives in `tests/common/mod.rs`. //! //! What they prove: device wrap → caps query/derivation on REAL caps → session + //! parameters creation → the decoupled picture pool → 48 AUs of the vendored @@ -45,11 +48,13 @@ use common::TestDecoder; use pf_vkdecode::DecodeStatus; use pf_vkdecode::DecodedVkFrame; use pf_vkdecode::NoopQueueLock; +use pf_vkdecode::VkAv1Decoder; use pf_vkdecode::VkH264Decoder; use pf_vkdecode::VkH265Decoder; -/// AUs fed: far past either vector's DPB depth (`max_dpb_frames = 7` for the -/// H.264 clip), so DPB slots re-activate onto fresh pool images repeatedly. +/// AUs fed: far past every vector's DPB depth (`max_dpb_frames = 7` for the +/// H.264 clip, eight reference slots for AV1), so DPB slots re-activate onto fresh +/// pool images repeatedly. const AUS: usize = 48; /// The REAL client's consumption shape: the consumer holds four delivered frames /// and releases only the oldest beyond that (its channels + preroll + in-flight @@ -77,8 +82,8 @@ struct Geometry { /// Decode [`AUS`] access units while holding [`CLIENT_HOLD`] frames, asserting the /// decode verdict of every frame before its release. /// -/// One body for both codecs (over `common::TestDecoder`) so "the H.265 leg proves -/// what the H.264 leg proves" is structural rather than a claim about two copies. +/// One body for all three codecs (over `common::TestDecoder`) so "the AV1 leg proves +/// what the H.264 leg proves" is structural rather than a claim about three copies. fn smoke(decoder: &mut impl TestDecoder, aus: &[&[u8]], geometry: &Geometry) { // The smoke legs exist to prove the PRODUCTION pool arrangement survives 48 // AUs at the client's hold depth. `PF_VKD_TEST_READBACK` adds TRANSFER_SRC to @@ -131,7 +136,7 @@ fn smoke(decoder: &mut impl TestDecoder, aus: &[&[u8]], geometry: &Geometry) { } // A pool built for the wrong picture format decodes and then // renders with the wrong maths (`DecodedVkFrame::format` docs); - // both vectors are 8-bit 4:2:0, so both must land on NV12. + // all three vectors are 8-bit 4:2:0, so all three must land on NV12. assert_eq!( frame.format, pf_vkdecode::NV12, @@ -267,13 +272,67 @@ fn h265_decodes_48_aus_holding_four_frames_like_the_real_client() { unsafe { setup.destroy() }; } +/// The AV1 leg — the rung's first hardware evidence of ANY kind. +/// +/// The same 48 access units at the same client hold depth, but AV1 loads the pool +/// harder than either H.26x leg does and that is the point of running it: the first +/// 48 temporal units carry 53 coded frames to show 48 (the number +/// [`the_delivery_floor_is_under_what_the_planners_emit_from_the_first_48_aus`] +/// prints), each hidden frame keeps a pool image resident as a reference while +/// nothing displays it, and eight reference slots re-activate against that. A pool +/// sized as if one access unit meant one picture starves exactly here — which is this +/// leg's whole job, since `gpu_parity`'s AV1 leg would report the same starvation as a +/// decode failure with a less obvious cause. +#[test] +#[ignore = "needs a Vulkan Video AV1 decode device (fleet boxes; see module docs)"] +fn av1_decodes_48_aus_holding_four_frames_like_the_real_client() { + // One codec at a time on the device (see `common::gpu_lock`). + let _gpu = common::gpu_lock(); + + let setup = common::bring_up(&common::Request { + codec: common::AV1, + graphics: common::Graphics::DecodeFamilyIsFine, + report_families: true, + }); + let handles = setup.handles(); + { + // SAFETY: as the H.264 leg — `setup` outlives this block and was created + // with the AV1 decode extension + timeline/sync2 features. + let mut decoder = unsafe { VkAv1Decoder::new(&handles, Box::new(NoopQueueLock)) } + .expect("wrap the device"); + // The construction-time shape gate on the vector's own facts (Main, 4:2:0, + // 8-bit, NO film grain → NV12). The film-grain argument is the one that has + // no H.26x counterpart: grain synthesis is part of the Vulkan decode PROFILE, + // so a box offering only the grain-enabled profile refuses HERE with a caps + // reason rather than at the first temporal unit. + decoder.probe_stream_support(1, 8, false).expect( + "the box must host AV1 Main 4:2:0 8-bit without film grain (the vector's shape)", + ); + smoke( + &mut decoder, + &common::split_av1_aus(common::TEST_25FPS_AV1), + &Geometry { + display: (320, 240), + // As HEVC: no hardware evidence for AV1's `pictureAccessGranularity` + // on any fleet box yet, so the leg prints what it allocates rather + // than asserting a number nobody has observed. AV1's decode extent is + // the POST-superres width, which is another reason not to guess. + exact_coded: None, + }, + ); + } + // SAFETY: as the H.264 leg — the decoder is gone and nothing else references + // the setup's handles. + unsafe { setup.destroy() }; +} + // --------------------------------------------------------------------------- -// CPU coherence guard — NOT `#[ignore]`d. +// CPU coherence guards — NOT `#[ignore]`d. // // The legs above only run on the fleet, so [`MIN_DELIVERED`] would otherwise be a -// number copied from the H.264 leg and never checked against the H.265 vector's -// own reorder depth. It is the CPU planner that decides how many of the first -// [`AUS`] pictures can possibly be delivered — the decoder builds exactly one +// number copied from the H.264 leg and never checked against the H.265 or AV1 +// vector's own reorder depth. It is the CPU planner that decides how many of the +// first [`AUS`] pictures can possibly be delivered — the decoder builds exactly one // frame per `dpb.outputs` id — so the floor is checkable here, without a GPU, and // a re-synced vector that reorders more deeply fails HERE instead of looking like // a pool-starvation bug on hardware. @@ -313,7 +372,30 @@ fn the_delivery_floor_is_under_what_the_planners_emit_from_the_first_48_aus() { }) .sum::() }; - eprintln!("outputs from the first {AUS} AUs: h264={h264} h265={h265}"); + // AV1 needs the extra fold: one temporal unit can plan SEVERAL frames, so the + // outputs of a unit are the outputs of all of its plans — and counting one plan + // per unit is exactly how a reader would under-count here. + let (av1, av1_frames) = { + let mut planner = pf_bitstream::av1::Av1Planner::new(); + let mut outputs = 0usize; + let mut frames = 0usize; + for (index, au) in common::split_av1_aus(common::TEST_25FPS_AV1) + .iter() + .take(AUS) + .enumerate() + { + let plans = planner + .plan_au(au) + .unwrap_or_else(|e| panic!("AV1 temporal unit {index} must plan, got {e:?}")); + frames += plans.len(); + outputs += plans.iter().map(|p| p.dpb.outputs.len()).sum::(); + } + (outputs, frames) + }; + eprintln!( + "outputs from the first {AUS} AUs: h264={h264} h265={h265} av1={av1} \ + (av1 decoded {av1_frames} frames to show {av1} — the hidden ones)" + ); // No `flush` here on purpose: the smoke legs do not flush either, so the // planner's un-flushed output count is exactly the frame budget they have. assert!( @@ -326,4 +408,19 @@ fn the_delivery_floor_is_under_what_the_planners_emit_from_the_first_48_aus() { "the H.265 leg asserts >= {MIN_DELIVERED} delivered but the planner only \ outputs {h265} pictures from the first {AUS} AUs" ); + assert!( + av1 >= MIN_DELIVERED, + "the AV1 leg asserts >= {MIN_DELIVERED} delivered but the planner only \ + outputs {av1} pictures from the first {AUS} temporal units" + ); + // The AV1 leg's load is not the same as the other two's, and the assertion above + // cannot see the difference: the pool must hold the hidden frames as well as the + // shown ones. If these ever became equal, the leg would have stopped exercising + // multi-frame temporal units — the one pool pressure AV1 has that H.26x has not — + // while still passing everything above. + assert!( + av1_frames > av1, + "the first {AUS} AV1 temporal units must decode MORE frames ({av1_frames}) \ + than they show ({av1}); equal counts mean the hidden-frame coverage is gone" + ); }