test(vkdecode): the AV1 rung finally has pixels to answer to

A parity and smoke harness for AV1, mirroring the H.264 and H.265 legs that
proved those rungs bit-identical to libavcodec on four drivers before either
ran on glass. This was the milestone's largest test gap: the adversarial
review found four blocking defects in the AV1 conversion — flags unset on
274 frames of 274, a units error in LoopRestorationSize, per-reference info
describing the wrong picture, film-grain fields left zero — and every one of
them would have shown on frame 1 of a parity run, while clippy and 164 green
unit tests said nothing at all.

The golden is 250 per-frame SHA-256s in DISPLAY order, not 274. The vector
carries 274 coded frames in 250 temporal units; the 24 extras are hidden
ALTREFs, decoded and referenced but never shown, and the rung delivers what
dpb.outputs names. The count is re-derived from the planner rather than
assumed.

Cross-checked between ffmpeg 8.1.1 on macOS arm64 and 8.0.1 on Linux x86_64,
whose raw outputs are byte-identical — and then against a third party neither
build knows about: the vendored vector ships upstream's own per-frame MD5s,
and re-running those reproduces all 250. The golden agrees with a decode
nobody in this program performed. I reproduced both independently before
committing.

8-bit NV12, traced from the sequence header rather than presumed
(seq_profile 0, high_bitdepth 0, mono_chrome 0), so the P010 scar does not
apply here — and the header says which check to make if a Main 10 golden is
ever added. film_grain_params_present is 0, which is load-bearing: grain
synthesis is part of the Vulkan decode profile, so this golden is only
comparable against a grain-less profile key.

Anti-vacuity is the point of the exercise, so it is structural. The golden
guard asserts the exact count, that every line is a bare digest, and that all
entries are DISTINCT — 250 copies of one digest would let a decoder frozen on
a single frame pass parity. The parity body asserts the golden set and the
access-unit count before it touches hardware, so an IVF reader returning
nothing cannot become "0 frames compared, pass". The agent verified the
guards fire by mutating the golden three ways.

assert_bit_identical now names the FIRST divergent frame, which is what
localises a defect; that improves all six legs, not just AV1.

AV1 has no four-byte-start-code twin, deliberately: OBUs are
length-delimited, so there is no prefix for a driver to mis-skip. Documented
where a reader would otherwise see an omission.

Nothing here has run on a GPU. The harness exists precisely so the four
review defects can be answered by measurement instead of argument.
This commit is contained in:
2026-08-06 22:38:04 +02:00
parent a404830456
commit 96fc3eca10
4 changed files with 860 additions and 82 deletions
+55 -9
View File
@@ -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<u8> {
/// 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<Option<DecodedVkFrame>, 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.
@@ -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
+388 -57
View File
@@ -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::<std::collections::HashSet<_>>()
.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<usize> = 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,
+113 -16
View File
@@ -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::<usize>()
};
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::<usize>();
}
(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"
);
}