The AV1 review round flagged the H.264 leg as "plausibly the same defect, traced in source, not reproduced" and deliberately did not touch it. It is reproduced now, and it is worse than the AV1 one: it fires on 297 of 300 access units of every stream a punktfunk host emits, at 720p, 1080p and 2160p alike, on BOTH the DXVA rung and the Vulkan one. **Decided on the CPU, no GPU needed.** `H264Planner` snapshots `dpb_refs` in `begin_picture`, BEFORE `finish_picture` runs 8.2.5's marking and C.4.5.3's bump, so a picture the sliding window unmarks and the bump then evicts lands in both `dpb_refs` (which `RefFrameList` is built from) and `dpb.removed`. The conversion released the whole `removed` list and then assigned the decode target a slot; `SlotMap::assign` takes the lowest free slot, which is the one just vacated. `CurrPic = N` and `RefFrameList[k] = N`, in one submission. The two conditions have to coincide in ONE access unit, and low-delay H.264 is exactly what makes them: `max_num_reorder_frames = 0` means the evicted picture has already been output, which is what makes it evictable at all. NVENC seals it by writing `max_num_ref_frames = 3` ALONGSIDE `max_dec_frame_buffering = 3` — a DPB exactly as deep as its reference count — so the window unmarks the oldest reference in the very unit whose bump drops it. The aliased picture is `ref_idx 2` of a three-entry `num_ref_idx_l0_active` list: addressable by any macroblock, not a spare. **Why two hardware-proven codecs and four GPUs never saw it.** `test-25fps.h264` is level 1.3 with no VUI `bitstream_restriction`, so `dpb_limit` falls back to A.3.1's level ceiling and gives a 7-frame DPB against 2 reference frames — the window unmarks two units before the bump can evict — and it REORDERS, which keeps an unmarked picture alive past the unit that unmarked it. Two independent reasons, both properties of that vector rather than of H.264. It measured zero and passed 250/250 throughout. `data/lowdelay-640x480.h264` is vendored to close exactly that: our own host's output, 120 pictures, goldens from libavcodec cross-checked bit-identical across two ffmpeg builds on two architectures. **The fix is the AV1 fix.** `DecodePlanDxva` and `DecodePlanVk` grow `release_after_decode`, the conversions hand the removals back instead of applying them, and the callers release them once the decode op is issued. It costs no slot the map does not have: `SlotMap::new` allocates `max_dpb_frames + 1` and the DPB never exceeds `max_dpb_frames`, so a free slot always exists with the whole `removed` list still held — measured, peak 4 of 4 on the stream that defers on 117 of 120 units. The Vulkan rung breaks on it in both DPB modes and neither loudly: DISTINCT hands the aliased reference the same array layer the setup writes; COINCIDE clears `slot_image[setup]` in the binding sync and the reference then resolves to no bound image, dropping out of `pReferenceSlots` with a `trace!`. Its deferred release runs on the FAILURE paths too — the fallible region's Result is held rather than `?`-ed, because seven exits sat between the conversion and the release and each would have leaked a slot. `a_full_dpb_bump_reuses_the_slot_but_the_pool_model_binds_a_fresh_image` asserted the aliasing as "the planner's normal behaviour": an authored depth-1 stream whose AU1 references the picture it evicts. It now asserts the opposite, which is the defect in two lines. New evidence, all of it runnable: the CPU proof pins BOTH numbers (0 on the vector, 117 of 120 on the low-delay stream) so neither can drift silently; the ledger-pressure test measures the peak; and a low-delay parity leg is added to `pf-vkdecode`'s `gpu_parity` and `pf-client-core`'s `video_d3d11_native::parity` so both rungs are held to what they stream rather than only to what they conform to.
3802 lines
166 KiB
Rust
3802 lines
166 KiB
Rust
//! The parity harness: pf-dxvadec's DXVA submission against libavcodec's, for the same access
|
||
//! units of the same elementary stream — picture parameters, quantization matrices AND the
|
||
//! buffer descriptors, for both codecs.
|
||
//!
|
||
//! (The file is still called `libav_picparams_parity` although it long since outgrew the picture
|
||
//! parameters: the name is what the milestone's plan, the capture recipe below and the gate
|
||
//! `cargo test -p pf-dxvadec --test libav_picparams_parity` all say, and a rename would cost
|
||
//! every one of those a stale reference to buy nothing.)
|
||
//!
|
||
//! # Why this exists
|
||
//!
|
||
//! M3's WP-D closed the native Vulkan rung by comparing DECODED PIXELS against libavcodec —
|
||
//! 250 AUs, bit-exact, three drivers. This milestone has no equivalent. Every claim it makes
|
||
//! about the DXVA structures rests on reading the specification and reading libavcodec, and
|
||
//! reading is exactly the method that produced the defects review 13 found: a quantization
|
||
//! matrix submitted unconditionally, a `NumMBsInBuffer` left at zero, a `RefFrameList` holding
|
||
//! the wrong set. None of those is visible in a smoke test, and all three are visible in one
|
||
//! comparison against the path every Windows player exercises.
|
||
//!
|
||
//! Two halves, and the split matters:
|
||
//!
|
||
//! * **What needs no capture at all** — the descriptor set's internal consistency, that the
|
||
//! quantization-matrix buffer is submitted exactly when the stream has one, that
|
||
//! `NumMBsInBuffer` is `mb_width * mb_height` for H.264 and 0 for HEVC, that the slice
|
||
//! records tile the bitstream buffer exactly. Those are ORDINARY tests here, not `#[ignore]`d,
|
||
//! because they run on any host over every AU of the vendored vectors. Two of review 13's
|
||
//! three structural defects would have failed one of them.
|
||
//! * **What needs libavcodec's own bytes** — the picture parameters, the matrices' contents,
|
||
//! and the descriptor VALUES as libav computes them. Those tests are `#[ignore]`d because they
|
||
//! need a capture the repository cannot carry: a patched FFmpeg on a Windows box with a
|
||
//! D3D11VA-capable GPU.
|
||
//!
|
||
//! # Capturing the libavcodec side
|
||
//!
|
||
//! Verified against **FFmpeg n8.1**, which is the version the Windows CI runs; the names below
|
||
//! are that tree's (they changed — the fill functions are non-static and codec-prefixed now).
|
||
//! On the Windows box (192.168.1.173 — see the box notes for the ssh identity), with an FFmpeg
|
||
//! source tree:
|
||
//!
|
||
//! **1. One AU counter, shared by every line.** In `libavcodec/dxva2.c`, at file scope above
|
||
//! `ff_dxva2_commit_buffer` (dxva2.c:802):
|
||
//!
|
||
//! ```c
|
||
//! static unsigned pf_au_index;
|
||
//! ```
|
||
//!
|
||
//! and bump it exactly once per picture, at the TOP of `ff_dxva2_common_end_frame` — that
|
||
//! function runs once per submitted picture, so `pf_au_index` is the same number for all of one
|
||
//! AU's lines:
|
||
//!
|
||
//! ```c
|
||
//! const unsigned pf_au = pf_au_index++; /* first line of the function body */
|
||
//! ```
|
||
//!
|
||
//! (The lines below that live in other functions read the file-scope `pf_au_index - 1`; the
|
||
//! block for each says which.) Two assumptions, both of which the harness's preflight catches if
|
||
//! they fail: the DXVA hwaccel decodes one picture at a time (`start_frame` → `decode_slice`* →
|
||
//! `end_frame`, no frame threading), and no picture is refused BETWEEN its
|
||
//! `fill_picture_parameters` and its `ff_dxva2_common_end_frame` — a codec-level `end_frame` that
|
||
//! returns early on `slice_count <= 0` would log a `PFPP` line with no matching descriptors, and
|
||
//! the preflight refuses a capture whose AU indices are not exactly `0..250`.
|
||
//!
|
||
//! **2. The buffer descriptors.** `ff_dxva2_commit_buffer` (dxva2.c:802) is the choke point for
|
||
//! three of the four buffers — it writes `dsc11->BufferType/DataSize/NumMBsInBuffer` at
|
||
//! dxva2.c:836-840. Immediately AFTER that write:
|
||
//!
|
||
//! ```c
|
||
//! av_log(NULL, AV_LOG_INFO, "PFBD %s %u %u %u %u %u\n",
|
||
//! avcodec_get_name(avctx->codec_id), pf_au_index - 1,
|
||
//! (unsigned)type, (unsigned)size, (unsigned)mb_count, 0u);
|
||
//! ```
|
||
//!
|
||
//! ⚠ **The BITSTREAM descriptor does NOT pass through that function** — the bitstream buffer is
|
||
//! packed in place, so each codec's `commit_bitstream_and_slice_buffer` fills its descriptor
|
||
//! itself (`dxva2_h264.c:412` D3D11 / `:425` DXVA2, `dxva2_hevc.c:338` / `:349`). Add the same
|
||
//! line after each of those two fills, with the codec spelled literally:
|
||
//!
|
||
//! ```c
|
||
//! av_log(NULL, AV_LOG_INFO, "PFBD h264 %u 6 %u %u 0\n",
|
||
//! pf_au_index - 1, (unsigned)current, mb_count); /* dxva2_h264.c */
|
||
//! av_log(NULL, AV_LOG_INFO, "PFBD hevc %u 6 %u 0 0\n",
|
||
//! pf_au_index - 1, (unsigned)current); /* dxva2_hevc.c */
|
||
//! ```
|
||
//!
|
||
//! A capture whose AUs carry three `PFBD` lines instead of four is this patch site missed, and
|
||
//! the harness says so by name rather than reporting a missing buffer as a defect.
|
||
//!
|
||
//! **3. The picture parameters.** At the very END of `ff_dxva2_h264_fill_picture_parameters`
|
||
//! (`dxva2_h264.c:51`) — after the `RefFrameList` loop and the `UsedForReferenceFlags` writes,
|
||
//! so every field is final:
|
||
//!
|
||
//! ```c
|
||
//! {
|
||
//! const uint8_t *raw = (const uint8_t *)pp;
|
||
//! char line[2 * sizeof(*pp) + 1];
|
||
//! unsigned i;
|
||
//! for (i = 0; i < sizeof(*pp); i++)
|
||
//! snprintf(line + 2 * i, 3, "%02x", raw[i]);
|
||
//! av_log(NULL, AV_LOG_INFO, "PFPP h264 %u %s\n", pf_au_index, line);
|
||
//! }
|
||
//! ```
|
||
//!
|
||
//! `pf_au_index` un-decremented here on purpose: `fill_picture_parameters` runs from
|
||
//! `start_frame`, BEFORE `ff_dxva2_common_end_frame` bumps the counter for the same picture.
|
||
//! The identical block goes at the end of `ff_dxva2_hevc_fill_picture_parameters`
|
||
//! (`dxva2_hevc.c:60`) with `h264` replaced by `hevc`.
|
||
//!
|
||
//! **4. The quantization matrices, including whether they are submitted at all.** In
|
||
//! `ff_dxva2_common_end_frame`, where its `qm`/`qm_size` arguments are in scope (that is where
|
||
//! the codec's decision arrives: `dxva2_h264.c:513-516` passes `&ctx_pic->qm` with `sizeof(qm)`
|
||
//! UNCONDITIONALLY, while `dxva2_hevc.c:417,423-426` passes `NULL`/0 unless
|
||
//! `pp.dwCodingParamToolFlags & 1`):
|
||
//!
|
||
//! ```c
|
||
//! if (qm_size > 0) {
|
||
//! const uint8_t *raw = qm;
|
||
//! char *line = av_malloc(2 * qm_size + 1);
|
||
//! unsigned i;
|
||
//! for (i = 0; i < qm_size; i++)
|
||
//! snprintf(line + 2 * i, 3, "%02x", raw[i]);
|
||
//! av_log(NULL, AV_LOG_INFO, "PFQM %s %u %s\n",
|
||
//! avcodec_get_name(avctx->codec_id), pf_au, line);
|
||
//! av_free(line);
|
||
//! } else {
|
||
//! av_log(NULL, AV_LOG_INFO, "PFQM %s %u absent\n",
|
||
//! avcodec_get_name(avctx->codec_id), pf_au);
|
||
//! }
|
||
//! ```
|
||
//!
|
||
//! The `absent` spelling is required rather than an omitted line: an omitted line is
|
||
//! indistinguishable from a missed patch, and "was the buffer submitted" is the single fact
|
||
//! review 13's HEVC defect turned on.
|
||
//!
|
||
//! **5. The slice-control format.** One line per AU (or one for the whole run — the parser takes
|
||
//! either), from the same place, so an inverted short/long-format number cannot pass unseen:
|
||
//!
|
||
//! ```c
|
||
//! av_log(NULL, AV_LOG_INFO, "PFCFG %s %u %u\n",
|
||
//! avcodec_get_name(avctx->codec_id), pf_au,
|
||
//! (unsigned)DXVA_CONTEXT_CFG_BITSTREAM(avctx, ctx));
|
||
//! ```
|
||
//!
|
||
//! `ConfigBitstreamRaw`'s short-format value is **2 for H.264 and 1 for HEVC** — one number
|
||
//! with two spellings, and an inverted pair swaps which slice-control STRUCT the driver reads
|
||
//! while every other byte still looks right. If the macro is spelled differently in the tree,
|
||
//! any expression yielding the negotiated config's `ConfigBitstreamRaw` will do.
|
||
//!
|
||
//! **5b. AV1.** The identical `PFPP` block goes at the very END of
|
||
//! `ff_dxva2_av1_fill_picture_parameters` (`dxva2_av1.c:60`), with `h264` replaced by `av1` —
|
||
//! after the film-grain block, so every field is final. Three things differ from the two codecs
|
||
//! above and each of them changes what a capture MEANS:
|
||
//!
|
||
//! * **The AU index is a FRAME, not a temporal unit.** `ff_dxva2_common_end_frame` runs once per
|
||
//! submitted picture and an AV1 temporal unit may decode several, so `pf_au_index` walks
|
||
//! decoded frames. This crate's [`our_av1_submissions`] emits one entry per decoded frame for
|
||
//! the same reason, and the vendored vector is **274** frames in 250 units — a capture with
|
||
//! 250 `PFPP av1` lines is a capture of something else. (A `show_existing_frame` unit submits
|
||
//! nothing on either side; this vector has none.)
|
||
//! * **No `PFQM` line and no matrix buffer.** `dxva2_av1_end_frame` passes `NULL, 0` for the qm
|
||
//! pair, so the `qm_size > 0` branch of the block in step 4 logs `absent` on every frame. That
|
||
//! is the expected reading, not a missed patch site.
|
||
//! * **No `PFCFG` check.** [`preflight`]'s `ConfigBitstreamRaw` assertion is about the two short
|
||
//! slice-control formats; AV1's slice-control record is `DXVA_Tile_AV1` and has no short/long
|
||
//! pair, so a captured `PFCFG av1` line is ignored rather than compared.
|
||
//!
|
||
//! The stream is the vendored IVF at
|
||
//! `crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1`:
|
||
//!
|
||
//! ```text
|
||
//! ffmpeg -hwaccel d3d11va -hwaccel_output_format d3d11 -i test-25fps.ivf.av1 -f null - 2> av1.log
|
||
//! grep -oE 'PF(PP|QM|BD|CFG) .*' av1.log > libav-av1.capture
|
||
//! ```
|
||
//!
|
||
//! ⚠ Add `-export_side_data +film_grain` to NOTHING: `apply_grain` and `pp->coding.film_grain`
|
||
//! both turn OFF when film grain is exported as side data, and this crate always applies it in
|
||
//! the decoder. The vendored vector codes no grain either way.
|
||
//!
|
||
//! **No such capture has been taken.** As of 2026-08-07 the AV1 comparison
|
||
//! (`our_av1_picture_parameters_match_libavcodecs`) has never run against libavcodec's bytes,
|
||
//! and the reason is stated rather than left as an absent result: `.221`, the only box in this
|
||
//! fleet with a D3D11VA GPU to spare, has no MSYS2, no gcc and no make, so producing a patched
|
||
//! FFmpeg there is a toolchain bring-up rather than a build. What DID localise the AV1 defect
|
||
//! of 2026-08-07 was `video_d3d11_native`'s frame-hash parity harness plus a CPU invariant
|
||
//! (`no_av1_submission_names_its_decode_surface_in_the_reference_store`, below) — so this file's
|
||
//! AV1 half is currently the no-capture half only, and every claim it makes about libavcodec's
|
||
//! AV1 side is READ out of `dxva2_av1.c` (n8.1) rather than measured. Tier one, in the
|
||
//! provenance section's terms, for all of it.
|
||
//!
|
||
//! **6. Run it.** `--enable-d3d11va` is on by default on Windows. Decode the SAME elementary
|
||
//! streams this test plans — the vendored vectors, in the repository at
|
||
//! `crates/pf-bitstream/vendor/cros-codecs/src/codec/{h264,h265}/test_data/test-25fps.{h264,h265}`:
|
||
//!
|
||
//! ```text
|
||
//! ffmpeg -hwaccel d3d11va -hwaccel_output_format d3d11 -i test-25fps.h264 -f null - 2> h264.log
|
||
//! ffmpeg -hwaccel d3d11va -hwaccel_output_format d3d11 -i test-25fps.h265 -f null - 2> hevc.log
|
||
//! grep -oE 'PF(PP|QM|BD|CFG) .*' h264.log > libav-h264.capture
|
||
//! grep -oE 'PF(PP|QM|BD|CFG) .*' hevc.log > libav-hevc.capture
|
||
//! ```
|
||
//!
|
||
//! `av_log(NULL, …)` rather than `av_log(avctx, …)` throughout, and `grep -o` rather than an
|
||
//! anchored match, for one reason: FFmpeg's logger prefixes a message logged against a context
|
||
//! with `[h264 @ 0x…] `, which no anchored grep would match. The parser finds its marker anywhere
|
||
//! in a line, so a capture made either way is readable — but the flat form is what the recipe
|
||
//! asks for, because a capture that greps cleanly is a capture whose format can be eyeballed.
|
||
//!
|
||
//! A software fallback produces no lines at all, which is the check that the hwaccel actually
|
||
//! engaged: 250 `PFPP` lines per stream or the capture is void. Then:
|
||
//!
|
||
//! ```text
|
||
//! PF_LIBAV_CAPTURE_H264=libav-h264.capture PF_LIBAV_CAPTURE_HEVC=libav-hevc.capture \
|
||
//! PF_LIBAV_CAPTURE_AV1=libav-av1.capture \
|
||
//! cargo test -p pf-dxvadec --test libav_picparams_parity -- --ignored --nocapture
|
||
//! ```
|
||
//!
|
||
//! `PF_DXVA_DUMP=<path>` writes THIS side in the same format, both codecs, without needing a
|
||
//! capture — so the two files can also be diffed by hand.
|
||
//!
|
||
//! # Differences that are EXPECTED, and must not be read as defects
|
||
//!
|
||
//! A raw `memcmp` of the picture parameters differs by design, which is why this harness does
|
||
//! not do one. Each expected divergence is handled STRUCTURALLY instead, so what is left over is
|
||
//! a finding:
|
||
//!
|
||
//! * **Surface indices.** `CurrPic` and every reference entry carry a decode-surface index.
|
||
//! libavcodec's comes from its own frame pool's allocation order; ours from
|
||
//! [`pf_dxvadec::SlotMap`]. The two are a BIJECTION over the same pictures, never equal
|
||
//! numbers. The harness therefore tracks the mapping per PICTURE — identified by the pair-key
|
||
//! DXVA itself resolves references by — and reports only a mapping that CHANGES while the
|
||
//! picture is still in the DPB, or two live pictures collapsing onto one surface. A differing
|
||
//! index alone proves nothing; an index that stops agreeing does.
|
||
//! * **Reference-array ORDER.** For H.264 libavcodec emits `short_ref` then `long_ref`; this
|
||
//! crate emits the AU's own references first and the rest of the marked DPB after (see
|
||
//! `pic.rs`'s module docs for why). For HEVC libavcodec walks its DPB array in slot order
|
||
//! while this crate leads with the three current RPS sets (`pic_h265.rs`'s docs). Both are
|
||
//! correct — DXVA imposes no order, a driver resolves an entry by its keys — so the arrays are
|
||
//! compared as SETS of `(marking, key, POC, use-flags)` tuples, with the per-entry bits of
|
||
//! `UsedForReferenceFlags`/`NonExistingFrameFlags` carried inside each tuple, which is what
|
||
//! "re-indexed to the compared order" amounts to.
|
||
//! * **HEVC's RPS index arrays** (`RefPicSetStCurrBefore`/`StCurrAfter`/`LtCurr`) hold INDICES
|
||
//! into `RefPicList`, so a different array order means different index VALUES for the same
|
||
//! pictures. They are compared by resolving each index through its own side's `RefPicList` and
|
||
//! comparing the PICTURES named, position by position — position order is 8.3.4's and does
|
||
//! matter.
|
||
//! * **The bitstream buffer's `DataSize`** may legitimately differ; see the descriptor section
|
||
//! below, which states exactly how a legitimate difference is told from a defect. (On the
|
||
//! capture described below it does not differ at all, on any of 500 AUs.)
|
||
//! * **libavcodec's POC BASE.** Measured, not predicted: every `CurrFieldOrderCnt` and
|
||
//! `FieldOrderCntList` value in the H.264 capture is the specification's plus exactly **65536**
|
||
//! — FFmpeg seeds `prev_poc_msb = 1 << 16` at each IDR. The progression is identical; only the
|
||
//! base differs, and it is uniform across the current picture and every reference entry, so
|
||
//! every difference a driver computes from these fields (temporal direct, implicit weighted
|
||
//! prediction, co-located selection) is unaffected. This crate keeps 8.2.1's values and the
|
||
//! harness compares POCs RELATIVE to a base it derives from the first AU and then REQUIRES of
|
||
//! every AU after it — so a genuinely wrong POC still reports. libavcodec's HEVC POCs carry no
|
||
//! such offset (measured: base 0 on all 250 AUs). See `PocBase`.
|
||
//! * **HEVC `loop_filter_across_tiles_enabled_flag`** (bit 10 of
|
||
//! `dwCodingSettingPicturePropertyFlags`): ours 1, libavcodec's 0, on all 250 AUs. 7.4.3.3.1
|
||
//! infers 1 when the PPS codes no tiles, which is what the vendored parser reports; libav's
|
||
//! parser evidently leaves it 0. Inert either way — with `tiles_enabled_flag` clear there is no
|
||
//! tile boundary for a loop filter to cross — so it is DOCUMENTED rather than changed: matching
|
||
//! libav would mean overriding a spec inference on the strength of one measurement of another
|
||
//! decoder's parser default. The allowance is exactly bit 10, exactly ours-set-theirs-clear, and
|
||
//! only while both sides agree tiles are off; see `hevc_allowance`.
|
||
//!
|
||
//! The last two are reported on every run as DOCUMENTED divergences with their AU counts, never
|
||
//! silently dropped, and each one's allowance is narrow enough that the next difference in the same
|
||
//! field is still a finding — which two non-ignored tests
|
||
//! (`libavcodecs_constant_poc_base_is_documented_and_anything_else_about_a_poc_is_a_finding`,
|
||
//! `the_hevc_tiles_flag_allowance_is_exactly_bit_ten_with_tiles_disabled_and_nothing_else`) prove
|
||
//! by synthesising the differences an allowance must NOT absorb.
|
||
//!
|
||
//! Everything else — every parameter-set field, every flag word, `frame_num`, `ContinuationFlag`,
|
||
//! `StatusReportFeedbackNumber` (both count from 1 per picture), and every reserved field — must
|
||
//! match byte for byte, and a difference there is the finding this harness exists to produce.
|
||
//!
|
||
//! # What the first real run said
|
||
//!
|
||
//! Run on 2026-08-06 against a patched FFmpeg n8.1 capture from the RTX 4090 box, 250 AUs per
|
||
//! codec, both vendored vectors. **Four comparisons, zero undocumented divergences**: H.264
|
||
//! picture parameters, HEVC picture parameters, the buffer descriptors of both codecs, and the
|
||
//! quantization matrices of both. The two divergences above were the entire delta.
|
||
//!
|
||
//! The measured ground truth, so the next reader needs no capture to know what libavcodec emits:
|
||
//!
|
||
//! | | H.264 | HEVC |
|
||
//! |---|---|---|
|
||
//! | picture parameters | 1040 bytes | 232 bytes |
|
||
//! | `ConfigBitstreamRaw` | 2 | 1 |
|
||
//! | IQ matrix | submitted on all 250 (224 bytes) | `absent` on all 250 |
|
||
//! | descriptors per AU | 4 (types 0, 4, 6, 5) | 3 (types 0, 6, 5) |
|
||
//! | `SLICE_CONTROL.DataSize` | 20 (2 slices × 10) | 10 (1 slice × 10) |
|
||
//! | `BITSTREAM.DataSize` | 256..6272, all ≡ 0 (mod 128) | 128..8320, all ≡ 0 (mod 128) |
|
||
//! | `NumMBsInBuffer` | 300 on BITSTREAM and SLICE_CONTROL, 0 on the other two | 0 on all |
|
||
//! | `DataOffset` | 0 on all | 0 on all |
|
||
//!
|
||
//! Three things that settles beyond this harness: the short slice record is **ten** bytes (20/2 and
|
||
//! 10/1, two codecs and two slice counts agreeing); every `BITSTREAM.DataSize` matches this crate's
|
||
//! packer exactly, so the start-code/rebase/padding rules were right; and the HEVC vector exercises
|
||
//! case 1 of the quantization matrix's three cases (`scaling_list_enabled_flag` clear) — cases 2
|
||
//! and 3 remain CPU-only, which is stated rather than papered over.
|
||
//!
|
||
//! ## The two libavcodec workarounds, and why a capture can be VOID
|
||
//!
|
||
//! `Reserved16Bits = 3` is the notable field: libavcodec writes 3 for every standard profile and
|
||
//! 0 only under one of two workarounds, both of which also change other bytes.
|
||
//!
|
||
//! * `FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO` is set iff the negotiated decoder GUID is the legacy
|
||
//! `ff_DXVADDI_Intel_ModeH264_E` (dxva2.c:302-303), and it changes two H.264 things
|
||
//! (dxva2_h264.c:128 and :257). **Our side cannot select that GUID**: config.rs's table holds
|
||
//! three standard GUIDs — [`pf_dxvadec::H264_VLD_NOFGT`] is `DXVA2_ModeH264_E` — and
|
||
//! `video_d3d11_native.rs` asks the device for nothing else. The exposure is entirely on the
|
||
//! CAPTURE side: on an old Intel part, the FFmpeg producing the capture may negotiate
|
||
//! ClearVideo, and then its bytes are not ours to compare against. ⚠ Worth re-reading at the
|
||
//! Intel bring-up: modern parts negotiate the standard GUID, so this should not fire — but
|
||
//! "should not" is what a preflight is for.
|
||
//! * `FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG` (old ATI/AMD UVD) is never auto-set in the modern
|
||
//! hwaccel path — it is user-set through the legacy context only — so libav takes the
|
||
//! `ff_zigzag_scan`-indexed branch of `ff_dxva2_h264_fill_scaling_lists`, which emits the
|
||
//! matrices in CODED (zig-zag) order. That is the order this crate's matrices are already in
|
||
//! (the vendored parser stores each list as coded), which is what makes the H.264
|
||
//! quantization-matrix comparison a straight byte compare.
|
||
//!
|
||
//! A capture whose `Reserved16Bits` is 0 was therefore made against a workaround path and is VOID
|
||
//! for comparison; the harness refuses it up front rather than reporting 250 findings.
|
||
//!
|
||
//! # The buffer descriptors
|
||
//!
|
||
//! [`pf_dxvadec::descriptors`] models them, and its module docs carry the value table and the
|
||
//! libavcodec citation for every field. What matters for a COMPARISON:
|
||
//!
|
||
//! * `CompressedBufferType` (D3D11's `BufferType`), the buffer SET and its ORDER cannot
|
||
//! legitimately differ. A type present on one side only is a finding — and for HEVC's
|
||
//! quantization matrix that finding IS review 13's defect.
|
||
//! * `DataOffset` is 0 on both sides, always.
|
||
//! * `NumMBsInBuffer` cannot legitimately differ: it is `mb_width * mb_height` on H.264's
|
||
//! bitstream and slice-control buffers, 0 everywhere else and on all of HEVC's.
|
||
//! * `DataSize` for the picture parameters, the matrices and the slice control cannot
|
||
//! legitimately differ either — they are `sizeof` a structure and `slices * 10` (the short
|
||
//! slice record is TEN bytes, packed; see `dxva.rs`'s alignment section for the measurement and
|
||
//! for what twelve would have cost). The slice control's size is therefore also a slice COUNT:
|
||
//! if it differs, the two sides disagree about how many slices the AU has, which voids that
|
||
//! AU's bitstream comparison and is reported as its own finding.
|
||
//! * `DataSize` for the BITSTREAM buffer is the one field with a legitimate divergence class.
|
||
//! Both sides pack slice NALUs only, each behind a normalised three-byte start code, and pad
|
||
//! the total to 128 bytes — this crate because the DXVA specs say so and because non-VCL NALUs
|
||
//! inside the decode range hang AMD's VCN firmware (the same discipline pf-vkdecode's
|
||
//! recording layer follows), libavcodec in `commit_bitstream_and_slice_buffer` for its own
|
||
//! reasons. So the sizes normally match exactly. They may differ by a FEW bytes per slice when
|
||
//! the two NALU splitters delimit a slice differently — trailing `zero_byte`s ahead of the next
|
||
//! start code belong to neither NALU, and a splitter may keep or drop them. That difference is
|
||
//! legitimate, and it is recognisable: the slice COUNT agrees, and the difference is under four
|
||
//! bytes per slice before padding. Anything else — a differing slice count, a size that is not
|
||
//! a multiple of 128 (which means the driver's mapping was too small for the padding), a
|
||
//! difference of hundreds of bytes — is a defect, and the harness classifies the two cases
|
||
//! apart rather than lumping them into one "DataSize differs".
|
||
//!
|
||
//! # Provenance, and what a reviewer must re-check
|
||
//!
|
||
//! There is no FFmpeg source in this worktree, so nothing here can verify a claim about
|
||
//! libavcodec. Two tiers, and the difference matters:
|
||
//!
|
||
//! * **Read out of an FFmpeg n8.1 tree** by this work package's coordinator: the function names
|
||
//! and every `file:line`, the qmatrix predicate's codec-asymmetry, the `NumMBsInBuffer`
|
||
//! asymmetry, and both workaround conditions.
|
||
//! * **Read out of the same tree, then CONFIRMED by the capture**: that
|
||
//! `commit_bitstream_and_slice_buffer` writes a three-byte start code ahead of each slice,
|
||
//! rebases `BSNALunitDataLocation` into the buffer, counts the start code in
|
||
//! `SliceBytesInBuffer`, pads with `FFMIN(128 - ((current - dxva_data) & 127), end - current)`
|
||
//! and charges that padding to the LAST record (`slice->SliceBytesInBuffer += padding`). This was
|
||
//! flagged as an unverified assumption in the first revision of this file, because the
|
||
//! `BITSTREAM.DataSize` classification rests on it; the capture then matched this crate's packed
|
||
//! size on all 500 AUs of both codecs, which is that assumption's proof.
|
||
//!
|
||
//! Everything in either tier is a contract this harness parses and compares against, not a fact
|
||
//! it establishes. The capture is the authority; a disagreement between a capture and a claim
|
||
//! above is a claim to fix.
|
||
|
||
use std::collections::BTreeMap;
|
||
use std::collections::BTreeSet;
|
||
use std::fmt::Write as _;
|
||
use std::io::Cursor;
|
||
use std::mem::offset_of;
|
||
use std::mem::size_of;
|
||
use std::ops::Range;
|
||
|
||
use pf_dxvadec::descriptors::BUFFER_BITSTREAM;
|
||
use pf_dxvadec::descriptors::BUFFER_INVERSE_QUANTIZATION_MATRIX;
|
||
use pf_dxvadec::descriptors::BUFFER_PICTURE_PARAMETERS;
|
||
use pf_dxvadec::descriptors::BUFFER_SLICE_CONTROL;
|
||
use pf_dxvadec::dxva::PicParamsH264;
|
||
use pf_dxvadec::dxva::PicParamsHevc;
|
||
use pf_dxvadec::dxva::QmatrixH264;
|
||
use pf_dxvadec::dxva::QmatrixHevc;
|
||
use pf_dxvadec::dxva::SliceH264Short;
|
||
use pf_dxvadec::dxva::SliceHevcShort;
|
||
use pf_dxvadec::dxva::UNUSED_ENTRY;
|
||
use pf_dxvadec::dxva_av1::PicEntryAv1;
|
||
use pf_dxvadec::dxva_av1::UNUSED_INDEX;
|
||
use pf_dxvadec::AuPlan;
|
||
use pf_dxvadec::Av1Planner;
|
||
use pf_dxvadec::BufferDescriptor;
|
||
use pf_dxvadec::Codec;
|
||
use pf_dxvadec::H264Planner;
|
||
use pf_dxvadec::H265Planner;
|
||
use pf_dxvadec::PicParamsAv1;
|
||
use pf_dxvadec::SliceRecord;
|
||
use pf_dxvadec::SlotMap;
|
||
use pf_dxvadec::TileAv1;
|
||
use pf_dxvadec::NUM_REF_SLOTS;
|
||
|
||
const TEST_25FPS_H264: &[u8] = include_bytes!(
|
||
"../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264"
|
||
);
|
||
const TEST_25FPS_H265: &[u8] = include_bytes!(
|
||
"../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265"
|
||
);
|
||
/// The AV1 vector is an IVF container, and its unit of comparison is the TEMPORAL UNIT rather
|
||
/// than the access unit: one IVF packet may decode several frames, of which at most one shows.
|
||
const TEST_25FPS_AV1: &[u8] = include_bytes!(
|
||
"../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1"
|
||
);
|
||
|
||
/// Both vendored vectors carry exactly this many access units — pf-bitstream's own golden, and
|
||
/// the number of `PFPP` lines a valid capture holds.
|
||
const VENDORED_AUS: usize = 250;
|
||
|
||
/// A generous stand-in for the driver's bitstream mapping. Real mappings are a few MiB; the
|
||
/// vendored vectors are 320x240, so nothing here comes close to the tail-padding clamp (which
|
||
/// [`pf_dxvadec::pack`]'s own unit tests cover).
|
||
const MAPPING_BYTES: usize = 1 << 20;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Access-unit splitting
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// The same AU splitter every H.264 test in this program uses: a new AU starts at a non-slice
|
||
/// NALU following a slice, or at a slice whose `first_mb_in_slice` is 0 following a slice.
|
||
fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> {
|
||
use cros_codecs::codec::h264::parser::Nalu;
|
||
use cros_codecs::codec::h264::parser::NaluType;
|
||
|
||
let mut aus = Vec::new();
|
||
let mut cursor = Cursor::new(stream);
|
||
let mut au_start = 0usize;
|
||
let mut au_has_slice = false;
|
||
|
||
while let Ok(nalu) = Nalu::next(&mut cursor) {
|
||
let nalu_offset = cursor.position() as usize;
|
||
let start = nalu_offset - nalu.offset;
|
||
let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr);
|
||
let first_mb_zero = is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0);
|
||
|
||
if au_has_slice && (!is_slice || first_mb_zero) {
|
||
aus.push(&stream[au_start..start]);
|
||
au_start = start;
|
||
au_has_slice = false;
|
||
}
|
||
au_has_slice |= is_slice;
|
||
}
|
||
aus.push(&stream[au_start..]);
|
||
aus
|
||
}
|
||
|
||
/// The H.265 splitter, which is NOT the H.264 one: the flag that starts a picture is
|
||
/// `first_slice_segment_in_pic_flag`, the first bit after the TWO-byte NAL header, and "slice" is
|
||
/// every NALU type below 32. Copied from the tested implementation in
|
||
/// `crates/pf-bitstream/src/h265.rs` (`fn split_into_aus`, test-private there) rather than
|
||
/// re-derived, because a splitter that disagrees with pf-bitstream's would make every AU index
|
||
/// in a capture point at a different picture.
|
||
fn split_into_aus_h265(stream: &[u8]) -> Vec<&[u8]> {
|
||
use cros_codecs::codec::h265::parser::Nalu;
|
||
|
||
let mut aus = Vec::new();
|
||
let mut cursor = Cursor::new(stream);
|
||
let mut au_start = 0usize;
|
||
let mut au_has_slice = false;
|
||
|
||
while let Ok(nalu) = Nalu::next(&mut cursor) {
|
||
let header_start = cursor.position() as usize;
|
||
let start = header_start - nalu.offset;
|
||
let is_slice = (nalu.header.type_ as u32) < 32;
|
||
let first_slice_flag =
|
||
is_slice && stream.get(header_start + 2).is_some_and(|b| b & 0x80 != 0);
|
||
|
||
if au_has_slice && (!is_slice || first_slice_flag) {
|
||
aus.push(&stream[au_start..start]);
|
||
au_start = start;
|
||
au_has_slice = false;
|
||
}
|
||
au_has_slice |= is_slice;
|
||
}
|
||
aus.push(&stream[au_start..]);
|
||
aus
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// This crate's side
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Everything one AU's `SubmitDecoderBuffers` call would carry, from this crate.
|
||
struct OurSubmission {
|
||
/// The picture-parameters buffer's bytes.
|
||
pic_params: Vec<u8>,
|
||
/// The quantization-matrix buffer's bytes, or `None` when the buffer is not submitted at
|
||
/// all — which for HEVC is the whole of review 13's defect.
|
||
qmatrix: Option<Vec<u8>>,
|
||
/// The descriptor set, in submission order.
|
||
descriptors: Vec<BufferDescriptor>,
|
||
/// The packer's slice records, for the internal-consistency checks. Empty on AV1, whose
|
||
/// slice-control buffer holds [`Self::tiles`] instead.
|
||
records: Vec<SliceRecord>,
|
||
/// AV1's slice-control records — one `DXVA_Tile_AV1` per TILE, not per tile group. Empty
|
||
/// on H.264 and H.265.
|
||
tiles: Vec<TileAv1>,
|
||
/// Bytes the packer wrote BEFORE the tail padding.
|
||
unpadded: u32,
|
||
/// `mb_width * mb_height` (H.264) or 0 (HEVC) — the value the descriptors must carry.
|
||
mb_count: u32,
|
||
}
|
||
|
||
/// Plan and convert the whole vendored H.264 vector, one entry per AU.
|
||
///
|
||
/// Every AU must plan and convert: this vector is pf-bitstream's clean golden, so a skipped AU
|
||
/// is a regression rather than a stream fact. Swallowing an error here — the shape the scaffold
|
||
/// this replaced had — is how a harness reports a clean bill of health while comparing nothing.
|
||
fn our_h264_submissions() -> Vec<OurSubmission> {
|
||
let mut planner = H264Planner::new();
|
||
let mut slots: Option<SlotMap> = None;
|
||
let mut mapping = vec![0u8; MAPPING_BYTES];
|
||
let mut out = Vec::new();
|
||
for (i, au) in split_into_aus(TEST_25FPS_H264).into_iter().enumerate() {
|
||
let plan: AuPlan = planner
|
||
.plan_au(au)
|
||
.unwrap_or_else(|e| panic!("AU {i} of the vendored H.264 vector must plan: {e}"));
|
||
let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames));
|
||
if map.capacity() != plan.picture.max_dpb_frames + 1 {
|
||
*map = SlotMap::new(plan.picture.max_dpb_frames);
|
||
}
|
||
// `StatusReportFeedbackNumber` counts planned pictures from 1, which is exactly what
|
||
// libavcodec's `1 + report_id++` produces for a decoder that saw only this stream.
|
||
let dxva = pf_dxvadec::plan_to_dxva(&plan, map, out.len() as u32 + 1)
|
||
.unwrap_or_else(|e| panic!("AU {i} must convert: {e}"));
|
||
// The conversion's half of the deferral contract
|
||
// ([`pf_dxvadec::DecodePlanDxva::release_after_decode`]): a loop that converts
|
||
// AU after AU without applying it holds a surface per AU and runs the ledger
|
||
// dry. The vendored vector never puts a picture in both `RefFrameList` and
|
||
// `removed`, so this list is always empty HERE — applied anyway, because a
|
||
// harness that mirrors the caller only on the streams where it does not matter
|
||
// is a harness that would not notice the caller being wrong.
|
||
for &id in &dxva.release_after_decode {
|
||
assert!(
|
||
map.release(id),
|
||
"AU {i}: a deferred release named a picture holding no surface"
|
||
);
|
||
}
|
||
let packed = pf_dxvadec::pack(au, &dxva.slice_ranges, &mut mapping)
|
||
.unwrap_or_else(|e| panic!("AU {i} must pack: {e}"));
|
||
let unpadded = pf_dxvadec::packed_size(au, &dxva.slice_ranges).expect("packed size") as u32;
|
||
out.push(OurSubmission {
|
||
pic_params: pf_dxvadec::as_bytes(&dxva.pic_params).to_vec(),
|
||
qmatrix: Some(pf_dxvadec::as_bytes(&dxva.qmatrix).to_vec()),
|
||
descriptors: pf_dxvadec::descriptors_h264(&dxva, &packed),
|
||
records: packed.records,
|
||
tiles: Vec::new(),
|
||
unpadded,
|
||
mb_count: dxva.mb_count,
|
||
});
|
||
}
|
||
assert_eq!(out.len(), VENDORED_AUS);
|
||
out
|
||
}
|
||
|
||
/// Plan and convert the whole vendored HEVC vector, one entry per AU. Same no-skipping contract
|
||
/// as the H.264 side — `RaslSkipped` cannot arise on a vector that starts at an IDR.
|
||
fn our_hevc_submissions() -> Vec<OurSubmission> {
|
||
let mut planner = H265Planner::new();
|
||
let mut slots: Option<SlotMap> = None;
|
||
let mut mapping = vec![0u8; MAPPING_BYTES];
|
||
let mut out = Vec::new();
|
||
for (i, au) in split_into_aus_h265(TEST_25FPS_H265).into_iter().enumerate() {
|
||
let plan = planner
|
||
.plan_au(au)
|
||
.unwrap_or_else(|e| panic!("AU {i} of the vendored HEVC vector must plan: {e}"));
|
||
let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames));
|
||
if map.capacity() != plan.picture.max_dpb_frames + 1 {
|
||
*map = SlotMap::new(plan.picture.max_dpb_frames);
|
||
}
|
||
let dxva = pf_dxvadec::plan_to_dxva_h265(&plan, map, out.len() as u32 + 1)
|
||
.unwrap_or_else(|e| panic!("AU {i} must convert: {e}"));
|
||
let packed = pf_dxvadec::pack(au, &dxva.slice_ranges, &mut mapping)
|
||
.unwrap_or_else(|e| panic!("AU {i} must pack: {e}"));
|
||
let unpadded = pf_dxvadec::packed_size(au, &dxva.slice_ranges).expect("packed size") as u32;
|
||
out.push(OurSubmission {
|
||
pic_params: pf_dxvadec::as_bytes(&dxva.pic_params).to_vec(),
|
||
qmatrix: dxva
|
||
.qmatrix
|
||
.as_ref()
|
||
.map(|qm| pf_dxvadec::as_bytes(qm).to_vec()),
|
||
descriptors: pf_dxvadec::descriptors_h265(&dxva, &packed),
|
||
records: packed.records,
|
||
tiles: Vec::new(),
|
||
unpadded,
|
||
mb_count: 0,
|
||
});
|
||
}
|
||
assert_eq!(out.len(), VENDORED_AUS);
|
||
out
|
||
}
|
||
|
||
/// Every AV1 FRAME the vendored vector decodes — 274, of which 250 are displayed.
|
||
///
|
||
/// The unit of comparison is the frame and not the temporal unit, because that is what
|
||
/// libavcodec's hwaccel counts: `ff_dxva2_common_end_frame` runs once per submitted PICTURE, so
|
||
/// a capture's AU index walks decoded frames. A `show_existing_frame` unit submits nothing and
|
||
/// appears on neither side; this vector has none.
|
||
const VENDORED_AV1_FRAMES: usize = 274;
|
||
|
||
/// Plan, convert and pack the whole vendored AV1 vector, one entry per decoded FRAME.
|
||
///
|
||
/// ⚠ This is the only one of the three that has to speak the conversion's DEFERRED RELEASE
|
||
/// contract ([`pf_dxvadec::DecodePlanDxvaAv1::release_after_decode`]). A loop that converts
|
||
/// without it holds a surface on 268 of these 274 frames and runs the nine-slot ledger dry
|
||
/// inside ten — and, worse for a harness, it would compare a submission built by a caller that
|
||
/// is not the rung.
|
||
fn our_av1_submissions() -> Vec<OurSubmission> {
|
||
let mut planner = Av1Planner::new();
|
||
let mut slots = SlotMap::new(NUM_REF_SLOTS);
|
||
let mut mapping = vec![0u8; MAPPING_BYTES];
|
||
let mut out = Vec::new();
|
||
for (i, unit) in split_ivf(TEST_25FPS_AV1).into_iter().enumerate() {
|
||
let plans = planner
|
||
.plan_au(unit)
|
||
.unwrap_or_else(|e| panic!("unit {i} of the vendored AV1 vector must plan: {e}"));
|
||
for plan in &plans {
|
||
if plan.dpb.stored.is_none() {
|
||
continue; // `show_existing_frame`: no submission at all
|
||
}
|
||
let dxva = pf_dxvadec::plan_to_dxva_av1(unit, plan, &mut slots)
|
||
.unwrap_or_else(|e| panic!("unit {i} must convert: {e}"));
|
||
let packed = pf_dxvadec::pack_av1(unit, &dxva.bitstream, &dxva.tiles, &mut mapping)
|
||
.unwrap_or_else(|e| panic!("unit {i} must pack: {e}"));
|
||
let unpadded = pf_dxvadec::packed_size_av1(&dxva.bitstream) as u32;
|
||
out.push(OurSubmission {
|
||
pic_params: pf_dxvadec::as_bytes(&dxva.pic_params).to_vec(),
|
||
// AV1 transmits no quantization matrix at all: its matrices are SELECTED by
|
||
// index out of tables the decoder already has, and `dxva2_av1_end_frame`
|
||
// passes `NULL, 0` for the pair. `None` here is a fact about the codec, not a
|
||
// condition on the stream the way HEVC's is.
|
||
qmatrix: None,
|
||
descriptors: pf_dxvadec::descriptors_av1(&packed),
|
||
// AV1's slice-control records are `DXVA_Tile_AV1`, a different struct with a
|
||
// different size; the shared `SliceRecord` checks do not apply to them, and
|
||
// the tile records get their own test rather than a coerced one.
|
||
records: Vec::new(),
|
||
tiles: packed.tiles.clone(),
|
||
unpadded,
|
||
mb_count: 0,
|
||
});
|
||
for &id in &dxva.release_after_decode {
|
||
assert!(
|
||
slots.release(id),
|
||
"unit {i}: a deferred release named a picture holding no surface"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
assert_eq!(out.len(), VENDORED_AV1_FRAMES);
|
||
out
|
||
}
|
||
|
||
/// The IVF frame walk — the same one `video_d3d11_native`'s parity module and every AV1 test in
|
||
/// this program use: a 32-byte file header, then a 12-byte header per packet carrying its size.
|
||
fn split_ivf(stream: &[u8]) -> Vec<&[u8]> {
|
||
let mut out = Vec::new();
|
||
let mut at = 32usize;
|
||
while at + 12 <= stream.len() {
|
||
let size = u32::from_le_bytes([stream[at], stream[at + 1], stream[at + 2], stream[at + 3]])
|
||
as usize;
|
||
at += 12;
|
||
if at + size > stream.len() {
|
||
break;
|
||
}
|
||
out.push(&stream[at..at + size]);
|
||
at += size;
|
||
}
|
||
out
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Offset → field name
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// A field table for a hand-declared DXVA struct: `(name, offset)` per field, in declaration
|
||
/// order, built from the field IDENTIFIERS so a name and the offset it reports cannot drift
|
||
/// apart — the whole point of the table is to turn a differing byte into a field name, and a
|
||
/// table with a copy-pasted mismatch would name the wrong one.
|
||
/// Nested paths (`tiles.cols`) are accepted as well as plain identifiers, and are named by
|
||
/// the whole path — AV1's picture parameters are eight nested blocks, and a table that could
|
||
/// only reach the outer members would report "segmentation differs" for a 140-byte struct.
|
||
macro_rules! field_table {
|
||
($ty:ty, $($($field:ident).+),+ $(,)?) => {
|
||
&[$((stringify!($($field).+), offset_of!($ty, $($field).+))),+]
|
||
};
|
||
}
|
||
|
||
/// Every field of `DXVA_PicParams_H264`. Lengths are DERIVED from the next field's offset rather
|
||
/// than written down: the struct has no interior padding (dxva.rs proves every offset at compile
|
||
/// time), so consecutive offsets tile it exactly — and a hand-typed length is one more thing that
|
||
/// can be wrong in a file whose whole job is to catch wrong numbers.
|
||
const H264_FIELDS: &[(&str, usize)] = field_table!(
|
||
PicParamsH264,
|
||
wFrameWidthInMbsMinus1,
|
||
wFrameHeightInMbsMinus1,
|
||
CurrPic,
|
||
num_ref_frames,
|
||
wBitFields,
|
||
bit_depth_luma_minus8,
|
||
bit_depth_chroma_minus8,
|
||
Reserved16Bits,
|
||
StatusReportFeedbackNumber,
|
||
RefFrameList,
|
||
CurrFieldOrderCnt,
|
||
FieldOrderCntList,
|
||
pic_init_qs_minus26,
|
||
chroma_qp_index_offset,
|
||
second_chroma_qp_index_offset,
|
||
ContinuationFlag,
|
||
pic_init_qp_minus26,
|
||
num_ref_idx_l0_active_minus1,
|
||
num_ref_idx_l1_active_minus1,
|
||
Reserved8BitsA,
|
||
FrameNumList,
|
||
UsedForReferenceFlags,
|
||
NonExistingFrameFlags,
|
||
frame_num,
|
||
log2_max_frame_num_minus4,
|
||
pic_order_cnt_type,
|
||
log2_max_pic_order_cnt_lsb_minus4,
|
||
delta_pic_order_always_zero_flag,
|
||
direct_8x8_inference_flag,
|
||
entropy_coding_mode_flag,
|
||
pic_order_present_flag,
|
||
num_slice_groups_minus1,
|
||
slice_group_map_type,
|
||
deblocking_filter_control_present_flag,
|
||
redundant_pic_cnt_present_flag,
|
||
Reserved8BitsB,
|
||
slice_group_change_rate_minus1,
|
||
SliceGroupMap,
|
||
);
|
||
|
||
/// Every field of `DXVA_PicParams_HEVC`, same construction.
|
||
const HEVC_FIELDS: &[(&str, usize)] = field_table!(
|
||
PicParamsHevc,
|
||
PicWidthInMinCbsY,
|
||
PicHeightInMinCbsY,
|
||
wFormatAndSequenceInfoFlags,
|
||
CurrPic,
|
||
sps_max_dec_pic_buffering_minus1,
|
||
log2_min_luma_coding_block_size_minus3,
|
||
log2_diff_max_min_luma_coding_block_size,
|
||
log2_min_transform_block_size_minus2,
|
||
log2_diff_max_min_transform_block_size,
|
||
max_transform_hierarchy_depth_inter,
|
||
max_transform_hierarchy_depth_intra,
|
||
num_short_term_ref_pic_sets,
|
||
num_long_term_ref_pics_sps,
|
||
num_ref_idx_l0_default_active_minus1,
|
||
num_ref_idx_l1_default_active_minus1,
|
||
init_qp_minus26,
|
||
ucNumDeltaPocsOfRefRpsIdx,
|
||
wNumBitsForShortTermRPSInSlice,
|
||
ReservedBits2,
|
||
dwCodingParamToolFlags,
|
||
dwCodingSettingPicturePropertyFlags,
|
||
pps_cb_qp_offset,
|
||
pps_cr_qp_offset,
|
||
num_tile_columns_minus1,
|
||
num_tile_rows_minus1,
|
||
column_width_minus1,
|
||
row_height_minus1,
|
||
diff_cu_qp_delta_depth,
|
||
pps_beta_offset_div2,
|
||
pps_tc_offset_div2,
|
||
log2_parallel_merge_level_minus2,
|
||
CurrPicOrderCntVal,
|
||
RefPicList,
|
||
ReservedBits5,
|
||
PicOrderCntValList,
|
||
RefPicSetStCurrBefore,
|
||
RefPicSetStCurrAfter,
|
||
RefPicSetLtCurr,
|
||
ReservedBits6,
|
||
ReservedBits7,
|
||
StatusReportFeedbackNumber,
|
||
);
|
||
|
||
/// `DXVA_Qmatrix_H264`'s two arrays.
|
||
const H264_QMATRIX_FIELDS: &[(&str, usize)] =
|
||
field_table!(QmatrixH264, bScalingLists4x4, bScalingLists8x8);
|
||
|
||
/// The two short slice-control records, which are the structs the twelve-vs-ten defect was in.
|
||
const H264_SLICE_FIELDS: &[(&str, usize)] = field_table!(
|
||
SliceH264Short,
|
||
BSNALunitDataLocation,
|
||
SliceBytesInBuffer,
|
||
wBadSliceChopping,
|
||
);
|
||
const HEVC_SLICE_FIELDS: &[(&str, usize)] = field_table!(
|
||
SliceHevcShort,
|
||
BSNALunitDataLocation,
|
||
SliceBytesInBuffer,
|
||
wBadSliceChopping,
|
||
);
|
||
|
||
/// `DXVA_Qmatrix_HEVC`'s six.
|
||
const HEVC_QMATRIX_FIELDS: &[(&str, usize)] = field_table!(
|
||
QmatrixHevc,
|
||
ucScalingLists0,
|
||
ucScalingLists1,
|
||
ucScalingLists2,
|
||
ucScalingLists3,
|
||
ucScalingListDCCoefSizeID2,
|
||
ucScalingListDCCoefSizeID3,
|
||
);
|
||
|
||
/// Every field of `DXVA_PicParams_AV1`, same construction — but reaching INTO the eight nested
|
||
/// blocks, because they are where AV1 keeps almost the whole frame header. `tiles` alone is 260
|
||
/// bytes and `segmentation` 140; a table stopping at the outer members would turn every finding
|
||
/// in them into one useless name.
|
||
///
|
||
/// Three members stay whole on purpose. `frame_refs` is seven 36-byte entries which — unlike
|
||
/// the other two codecs' reference arrays — carry NO surface index and so compare byte for
|
||
/// byte: `Index` is `ref_frame_idx[name]`, an AV1 SLOT both sides read out of the same frame
|
||
/// header. `ref_frame_map_texture_index` is the surface array, which cannot be compared by
|
||
/// value at all ([`av1_reference_store`]). And `film_grain` is 158 bytes neither vendored
|
||
/// vector codes.
|
||
const AV1_FIELDS: &[(&str, usize)] = field_table!(
|
||
PicParamsAv1,
|
||
width,
|
||
height,
|
||
max_width,
|
||
max_height,
|
||
curr_pic_texture_index,
|
||
superres_denom,
|
||
bitdepth,
|
||
seq_profile,
|
||
tiles.cols,
|
||
tiles.rows,
|
||
tiles.context_update_id,
|
||
tiles.widths,
|
||
tiles.heights,
|
||
coding,
|
||
format,
|
||
primary_ref_frame,
|
||
order_hint,
|
||
order_hint_bits,
|
||
frame_refs,
|
||
ref_frame_map_texture_index,
|
||
loop_filter.filter_level,
|
||
loop_filter.filter_level_u,
|
||
loop_filter.filter_level_v,
|
||
loop_filter.sharpness_level,
|
||
loop_filter.control_flags,
|
||
loop_filter.ref_deltas,
|
||
loop_filter.mode_deltas,
|
||
loop_filter.delta_lf_res,
|
||
loop_filter.frame_restoration_type,
|
||
loop_filter.log2_restoration_unit_size,
|
||
loop_filter.reserved16,
|
||
quantization.control_flags,
|
||
quantization.base_qindex,
|
||
quantization.y_dc_delta_q,
|
||
quantization.u_dc_delta_q,
|
||
quantization.v_dc_delta_q,
|
||
quantization.u_ac_delta_q,
|
||
quantization.v_ac_delta_q,
|
||
quantization.qm_y,
|
||
quantization.qm_u,
|
||
quantization.qm_v,
|
||
quantization.reserved16,
|
||
cdef.control_flags,
|
||
cdef.y_strengths,
|
||
cdef.uv_strengths,
|
||
interp_filter,
|
||
segmentation.control_flags,
|
||
segmentation.reserved24,
|
||
segmentation.feature_mask,
|
||
segmentation.feature_data,
|
||
film_grain,
|
||
reserved32,
|
||
status_report_feedback_number,
|
||
);
|
||
|
||
/// `DXVA_Tile_AV1` — AV1's slice-control record, and the one this crate had to derive rather
|
||
/// than measure (`dxva.h` declares it; the SIZE is what the descriptor states).
|
||
const AV1_TILE_FIELDS: &[(&str, usize)] = field_table!(
|
||
TileAv1,
|
||
data_offset,
|
||
data_size,
|
||
row,
|
||
column,
|
||
reserved16,
|
||
anchor_frame,
|
||
reserved8,
|
||
);
|
||
|
||
/// Turn a field table into `(name, byte range)`, the last field running to `total`.
|
||
fn field_ranges(
|
||
fields: &[(&'static str, usize)],
|
||
total: usize,
|
||
) -> Vec<(&'static str, Range<usize>)> {
|
||
fields
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, &(name, offset))| {
|
||
let end = fields.get(i + 1).map_or(total, |&(_, next)| next);
|
||
(name, offset..end)
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn u16_at(bytes: &[u8], offset: usize) -> u16 {
|
||
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
|
||
}
|
||
|
||
fn u32_at(bytes: &[u8], offset: usize) -> u32 {
|
||
u32::from_le_bytes([
|
||
bytes[offset],
|
||
bytes[offset + 1],
|
||
bytes[offset + 2],
|
||
bytes[offset + 3],
|
||
])
|
||
}
|
||
|
||
fn i32_at(bytes: &[u8], offset: usize) -> i32 {
|
||
u32_at(bytes, offset) as i32
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Findings
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// One kind of divergence, and how often it happened.
|
||
struct Finding {
|
||
count: usize,
|
||
first_au: usize,
|
||
detail: String,
|
||
}
|
||
|
||
/// Divergences, grouped by the FIELD they belong to rather than by byte offset. A byte offset is
|
||
/// nearly useless on a hand-declared struct; the crate proves its offsets at compile time, so
|
||
/// this maps them back to names and reports those.
|
||
///
|
||
/// Two channels, and the split is the whole reason this type exists rather than a `Vec`:
|
||
/// [`Findings::note`] records a FINDING (the run fails), [`Findings::document`] records a
|
||
/// divergence this program has already decided is not a defect. Documented ones are printed on
|
||
/// every run with their reason and their AU count — never dropped, because a divergence nobody
|
||
/// prints is a divergence nobody re-reads, and each of them is only allowed within a stated
|
||
/// allowance that the comparison itself enforces.
|
||
#[derive(Default)]
|
||
struct Findings {
|
||
by_field: BTreeMap<String, Finding>,
|
||
documented: BTreeMap<String, Finding>,
|
||
}
|
||
|
||
impl Findings {
|
||
fn note(&mut self, field: impl Into<String>, au: usize, detail: impl Into<String>) {
|
||
let entry = self
|
||
.by_field
|
||
.entry(field.into())
|
||
.or_insert_with(|| Finding {
|
||
count: 0,
|
||
first_au: au,
|
||
detail: detail.into(),
|
||
});
|
||
entry.count += 1;
|
||
}
|
||
|
||
/// A divergence the module docs list, with the reason it is not a defect.
|
||
fn document(&mut self, field: impl Into<String>, au: usize, reason: impl Into<String>) {
|
||
let entry = self
|
||
.documented
|
||
.entry(field.into())
|
||
.or_insert_with(|| Finding {
|
||
count: 0,
|
||
first_au: au,
|
||
detail: reason.into(),
|
||
});
|
||
entry.count += 1;
|
||
}
|
||
|
||
fn is_empty(&self) -> bool {
|
||
self.by_field.is_empty()
|
||
}
|
||
|
||
fn fields(&self) -> Vec<&str> {
|
||
self.by_field.keys().map(String::as_str).collect()
|
||
}
|
||
|
||
fn documented_fields(&self) -> Vec<&str> {
|
||
self.documented.keys().map(String::as_str).collect()
|
||
}
|
||
|
||
/// Print the verdict and fail if there is one. Never silently passes: a run that classified
|
||
/// nothing prints the AU count it did compare, so "no findings" cannot be confused with
|
||
/// "nothing was compared".
|
||
fn verdict(&self, what: &str, aus: usize) {
|
||
for (field, documented) in &self.documented {
|
||
println!(
|
||
"{what}: {field} diverges on {} of {aus} AUs (first at AU {}) — DOCUMENTED, not a \
|
||
defect: {}",
|
||
documented.count, documented.first_au, documented.detail
|
||
);
|
||
}
|
||
if self.is_empty() {
|
||
println!("{what}: {aus} AUs compared, no undocumented divergence");
|
||
return;
|
||
}
|
||
println!(
|
||
"{what}: {aus} AUs compared, {} fields diverge:",
|
||
self.by_field.len()
|
||
);
|
||
for (field, finding) in &self.by_field {
|
||
println!(
|
||
" {field}: {} AUs, first at AU {} — {}",
|
||
finding.count, finding.first_au, finding.detail
|
||
);
|
||
}
|
||
panic!(
|
||
"{what}: {} fields diverge ({}) — read each against the module docs' list of \
|
||
expected divergences before treating it as a defect",
|
||
self.by_field.len(),
|
||
self.fields().join(", ")
|
||
);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Reference entries as pictures
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// A picture's identity as the reference arrays express it, and the key the surface mapping is
|
||
/// tracked by: `(long-term, FrameNum or LongTermFrameIdx, TopFieldOrderCnt,
|
||
/// BottomFieldOrderCnt)`. HEVC leaves the second and fourth members at 0 — it identifies a
|
||
/// reference by POC alone.
|
||
///
|
||
/// It is DXVA's own key, deliberately, so both sides express it the same way. The one consequence
|
||
/// worth naming: a picture that is re-marked long-term changes key (`FrameNum` becomes
|
||
/// `LongTermFrameIdx`), so the surface mapping loses the link to its earlier self rather than
|
||
/// reporting a change — a missed check, never a false finding. Both sides re-key identically, so
|
||
/// the SET comparison is unaffected.
|
||
type PictureKey = (bool, u16, i32, i32);
|
||
|
||
/// A reference entry with its surface index REMOVED: the identity DXVA resolves a reference by,
|
||
/// plus the per-entry flag bits that belong to it. Comparing the array as a multiset of these is
|
||
/// what makes the two sides' different orders irrelevant while keeping every fact — the flag
|
||
/// bits travel with their entry, which is "re-indexed to the compared order" in practice.
|
||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||
struct RefEntry {
|
||
long_term: bool,
|
||
/// `FrameNumList[i]` (H.264): `frame_num`, or `LongTermFrameIdx` for a long-term entry.
|
||
/// Always 0 for HEVC, which has no such array.
|
||
frame_num_or_lt_idx: u16,
|
||
/// `FieldOrderCntList[i]` (H.264) or `PicOrderCntValList[i]` (HEVC, in `top`).
|
||
top: i32,
|
||
bottom: i32,
|
||
used_top: bool,
|
||
used_bottom: bool,
|
||
non_existing: bool,
|
||
}
|
||
|
||
impl RefEntry {
|
||
/// The identity half alone — what follows a picture across AUs, and therefore what the
|
||
/// surface mapping is keyed by.
|
||
fn key(self) -> PictureKey {
|
||
(
|
||
self.long_term,
|
||
self.frame_num_or_lt_idx,
|
||
self.top,
|
||
self.bottom,
|
||
)
|
||
}
|
||
}
|
||
|
||
/// FFmpeg's H.264 POC base, and the proof that it is a CONSTANT.
|
||
///
|
||
/// libavcodec's H.264 decoder seeds `prev_poc_msb = 1 << 16` at every IDR, so every
|
||
/// `TopFieldOrderCnt`/`BottomFieldOrderCnt` it hands DXVA is the specification's plus 65536 —
|
||
/// measured, on the RTX 4090 capture: AUs 0..7 carry 65536, 65540, 65538, 65544, … where 8.2.1
|
||
/// (and this crate) derive 0, 4, 2, 8, …. The PROGRESSION is identical; only the base differs.
|
||
///
|
||
/// This crate keeps the spec's values, deliberately: the offset is an artefact of another
|
||
/// decoder's POC bookkeeping, not a DXVA requirement, and importing it would mean writing a magic
|
||
/// 65536 into a derivation that pf-bitstream shares with the Vulkan rung. It is harmless on the
|
||
/// wire because every use a driver makes of these fields is a DIFFERENCE — temporal direct
|
||
/// scaling, implicit weighted prediction, co-located picture selection — and the offset is uniform
|
||
/// across `CurrPic` and every `RefFrameList` entry of a stream, so it cancels. (References are
|
||
/// matched by `FrameNumList`, not by POC.) That it is uniform is exactly what this type checks.
|
||
///
|
||
/// So POCs are compared RELATIVE: the offset is derived from the first AU and then required to
|
||
/// hold for every POC of every later AU. A wrong POC on either side changes the offset and is
|
||
/// reported. Only 0 and 65536 are accepted as the base itself — any other constant is a finding,
|
||
/// because then it is not the quirk documented here.
|
||
#[derive(Default)]
|
||
struct PocBase {
|
||
offset: Option<i64>,
|
||
}
|
||
|
||
impl PocBase {
|
||
/// The offset in force, or 0 before the first AU has established one.
|
||
fn offset(&self) -> i64 {
|
||
self.offset.unwrap_or(0)
|
||
}
|
||
|
||
/// Check one POC pair, establishing the base on the first call.
|
||
fn check(&mut self, au: usize, field: &str, ours: i32, theirs: i32, findings: &mut Findings) {
|
||
let delta = i64::from(theirs) - i64::from(ours);
|
||
match self.offset {
|
||
None => {
|
||
if delta != 0 && delta != 65536 {
|
||
findings.note(
|
||
format!("{field}[POC base]"),
|
||
au,
|
||
format!(
|
||
"libav's first POC is ours {ours} + {delta}, which is neither 0 nor \
|
||
FFmpeg's documented 65536 `prev_poc_msb` seed — an unexplained POC \
|
||
base is a finding, not a quirk to absorb"
|
||
),
|
||
);
|
||
}
|
||
if delta == 65536 {
|
||
findings.document(
|
||
"FieldOrderCnt[POC base]",
|
||
au,
|
||
"libavcodec seeds `prev_poc_msb = 1 << 16` at every IDR, so its POCs are \
|
||
the specification's plus 65536; this crate keeps 8.2.1's values and the \
|
||
harness compares POCs RELATIVE to that constant, which it requires to \
|
||
hold on every AU",
|
||
);
|
||
}
|
||
self.offset = Some(delta);
|
||
}
|
||
Some(offset) if delta != offset => findings.note(
|
||
format!("{field}[POC]"),
|
||
au,
|
||
format!(
|
||
"ours {ours}, libav {theirs}: a difference of {delta} where every earlier POC \
|
||
of this stream differed by {offset} — the POC base is not constant, so this \
|
||
is a real POC divergence rather than libav's base offset"
|
||
),
|
||
),
|
||
Some(_) => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Subtract libav's POC base from a decoded reference array, so the set comparison compares
|
||
/// pictures rather than POC bases (see [`PocBase`]).
|
||
///
|
||
/// `bottom_too` is true for H.264, whose entries carry a real `FieldOrderCntList[i][2]` PAIR, and
|
||
/// false for HEVC, whose `bottom` member is a placeholder this harness leaves at 0 on both sides —
|
||
/// shifting it would invent a difference rather than absorb one.
|
||
fn shift_poc(entries: &mut [(u8, RefEntry)], offset: i64, bottom_too: bool) {
|
||
for (_, entry) in entries.iter_mut() {
|
||
entry.top = (i64::from(entry.top) - offset) as i32;
|
||
if bottom_too {
|
||
entry.bottom = (i64::from(entry.bottom) - offset) as i32;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A per-field allowance: `Some(reason)` when THIS difference in THIS field is a divergence the
|
||
/// module docs have already settled, rather than a finding.
|
||
///
|
||
/// Deliberately per-DIFFERENCE and not per-field: a field with an allowance still reports anything
|
||
/// outside it. An allowlist keyed by field name alone would be the "vacuous green" this program
|
||
/// has been bitten by before — it would hide the next real difference in the same word.
|
||
type Allowance = fn(&str, &[u8], &[u8]) -> Option<&'static str>;
|
||
|
||
/// H.264 has none: every difference in a scalar field is a finding.
|
||
fn no_allowance(_: &str, _: &[u8], _: &[u8]) -> Option<&'static str> {
|
||
None
|
||
}
|
||
|
||
/// HEVC's one documented scalar divergence: bit 10 of `dwCodingSettingPicturePropertyFlags`,
|
||
/// `loop_filter_across_tiles_enabled_flag`, which this crate sets and libavcodec does not.
|
||
///
|
||
/// 7.4.3.3.1 infers the flag to be 1 when the PPS does not code it, and the PPS only codes it
|
||
/// under `tiles_enabled_flag` — so with tiles disabled, 1 is what the specification says and what
|
||
/// the vendored parser reports. libavcodec's capture carries 0 on all 250 AUs of the vendored
|
||
/// vector. Neither can change a decoded picture: with `tiles_enabled_flag` clear there are no tile
|
||
/// boundaries for a loop filter to cross, which is why this is documented rather than fixed —
|
||
/// matching libavcodec here would mean overriding a spec inference on the strength of one
|
||
/// measurement of another decoder's parser default, and that default is not readable from this
|
||
/// worktree.
|
||
///
|
||
/// The allowance is tight: ONLY bit 10, only ours-set-theirs-clear, and only while both sides
|
||
/// agree tiles are disabled. Any other difference in the same word — including bit 10 with tiles
|
||
/// ENABLED, where the flag stops being inert — is a finding.
|
||
fn hevc_allowance(field: &str, ours: &[u8], theirs: &[u8]) -> Option<&'static str> {
|
||
/// `tiles_enabled_flag`.
|
||
const TILES: u32 = 1 << 7;
|
||
/// `loop_filter_across_tiles_enabled_flag`.
|
||
const ACROSS_TILES: u32 = 1 << 10;
|
||
|
||
if field != "dwCodingSettingPicturePropertyFlags" {
|
||
return None;
|
||
}
|
||
let (Ok(ours), Ok(theirs)) = (
|
||
<[u8; 4]>::try_from(ours).map(u32::from_le_bytes),
|
||
<[u8; 4]>::try_from(theirs).map(u32::from_le_bytes),
|
||
) else {
|
||
return None;
|
||
};
|
||
let only_bit_10 = ours ^ theirs == ACROSS_TILES;
|
||
let ours_sets_it = ours & ACROSS_TILES != 0;
|
||
let tiles_off = (ours | theirs) & TILES == 0;
|
||
(only_bit_10 && ours_sets_it && tiles_off).then_some(
|
||
"loop_filter_across_tiles_enabled_flag (bit 10): 7.4.3.3.1 infers 1 when the PPS codes no \
|
||
tiles and the vendored parser reports that; libavcodec emits 0. Inert either way — with \
|
||
tiles_enabled_flag clear there is no tile boundary for a loop filter to cross",
|
||
)
|
||
}
|
||
|
||
/// One side's H.264 reference array, decoded: the in-use entries with their surface indices.
|
||
fn h264_ref_entries(pp: &[u8]) -> Vec<(u8, RefEntry)> {
|
||
let list = offset_of!(PicParamsH264, RefFrameList);
|
||
let poc = offset_of!(PicParamsH264, FieldOrderCntList);
|
||
let nums = offset_of!(PicParamsH264, FrameNumList);
|
||
let used = u32_at(pp, offset_of!(PicParamsH264, UsedForReferenceFlags));
|
||
let missing = u16_at(pp, offset_of!(PicParamsH264, NonExistingFrameFlags));
|
||
(0..16)
|
||
.filter(|i| pp[list + i] != UNUSED_ENTRY)
|
||
.map(|i| {
|
||
(
|
||
pp[list + i] & 0x7F,
|
||
RefEntry {
|
||
long_term: pp[list + i] & 0x80 != 0,
|
||
frame_num_or_lt_idx: u16_at(pp, nums + 2 * i),
|
||
top: i32_at(pp, poc + 8 * i),
|
||
bottom: i32_at(pp, poc + 8 * i + 4),
|
||
used_top: used >> (2 * i) & 1 != 0,
|
||
used_bottom: used >> (2 * i + 1) & 1 != 0,
|
||
non_existing: missing >> i & 1 != 0,
|
||
},
|
||
)
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// One side's HEVC reference array, decoded. HEVC's array carries no `FrameNum` and no use
|
||
/// flags — residency IS the statement — so those members stay at their neutral values.
|
||
fn hevc_ref_entries(pp: &[u8]) -> Vec<(u8, RefEntry)> {
|
||
let list = offset_of!(PicParamsHevc, RefPicList);
|
||
let poc = offset_of!(PicParamsHevc, PicOrderCntValList);
|
||
(0..15)
|
||
.filter(|i| pp[list + i] != UNUSED_ENTRY)
|
||
.map(|i| {
|
||
(
|
||
pp[list + i] & 0x7F,
|
||
RefEntry {
|
||
long_term: pp[list + i] & 0x80 != 0,
|
||
frame_num_or_lt_idx: 0,
|
||
top: i32_at(pp, poc + 4 * i),
|
||
bottom: 0,
|
||
used_top: true,
|
||
used_bottom: true,
|
||
non_existing: false,
|
||
},
|
||
)
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// One side's AV1 reference store, read out of the SUBMITTED BYTES: `(CurrPicTextureIndex,
|
||
/// RefFrameMapTextureIndex[8], frame_refs[name].Index for the seven names)`.
|
||
///
|
||
/// AV1's reference numbering is two arrays that mean different things at once and the split is
|
||
/// exactly what a comparison has to respect. `frame_refs[i].Index` is an AV1 reference SLOT —
|
||
/// `ref_frame_idx[i]`, which both sides read out of the same frame header — so it is a VALUE
|
||
/// that must match libavcodec's exactly, and it is compared as part of the `frame_refs` field.
|
||
/// `RefFrameMapTextureIndex[slot]` and `CurrPicTextureIndex` are SURFACES, which come from each
|
||
/// side's own pool and are only ever a bijection.
|
||
///
|
||
/// So the store is compared as a SHAPE: which slots are occupied, and whether the decode target
|
||
/// collides with any of them.
|
||
fn av1_reference_store(pp: &[u8]) -> (u8, [u8; 8], [u8; 7]) {
|
||
let curr = pp[offset_of!(PicParamsAv1, curr_pic_texture_index)];
|
||
let mut store = [UNUSED_INDEX; 8];
|
||
let base = offset_of!(PicParamsAv1, ref_frame_map_texture_index);
|
||
store.copy_from_slice(&pp[base..base + 8]);
|
||
let mut names = [UNUSED_INDEX; 7];
|
||
// The stride and the member offset come from the TYPE, never from the two numbers
|
||
// `dxva_av1.rs` measured (36 and 33). Those are pinned there as compile-time assertions
|
||
// against the Windows SDK's own header, and re-typing them here would be a second copy that
|
||
// can drift from the first — which for a reader of this array is the difference between a
|
||
// reference slot and a warp coefficient.
|
||
for (name, slot) in names.iter_mut().enumerate() {
|
||
*slot = pp[offset_of!(PicParamsAv1, frame_refs)
|
||
+ name * size_of::<PicEntryAv1>()
|
||
+ offset_of!(PicEntryAv1, index)];
|
||
}
|
||
(curr, store, names)
|
||
}
|
||
|
||
/// The surface mapping between the two sides, tracked per PICTURE.
|
||
///
|
||
/// A global index-to-index bijection over a whole stream is the wrong model: both sides reuse a
|
||
/// surface once its picture leaves the DPB, and they need not reuse it at the same moment. What
|
||
/// must hold is that while a picture is live, its two surface numbers keep agreeing — so the
|
||
/// mapping is keyed by the picture and dropped when a side reassigns the index.
|
||
#[derive(Default)]
|
||
struct SurfaceMapping {
|
||
live: BTreeMap<PictureKey, (u8, u8)>,
|
||
}
|
||
|
||
impl SurfaceMapping {
|
||
/// Record one AU's pairs, reporting a mapping that changed under a live picture and two
|
||
/// pictures collapsing onto one surface.
|
||
fn observe(
|
||
&mut self,
|
||
au: usize,
|
||
field: &str,
|
||
pairs: &[(PictureKey, (u8, u8))],
|
||
findings: &mut Findings,
|
||
) {
|
||
let mut ours_seen: BTreeMap<u8, PictureKey> = BTreeMap::new();
|
||
let mut theirs_seen: BTreeMap<u8, PictureKey> = BTreeMap::new();
|
||
for &(key, (ours, theirs)) in pairs {
|
||
if let Some(&(known_ours, known_theirs)) = self.live.get(&key) {
|
||
if (known_ours, known_theirs) != (ours, theirs) {
|
||
findings.note(
|
||
format!("{field}[surface mapping]"),
|
||
au,
|
||
format!(
|
||
"picture {key:?} was surface {known_ours} (ours) = {known_theirs} \
|
||
(libav) and is now {ours} = {theirs}: the mapping is not a \
|
||
bijection over this picture's lifetime"
|
||
),
|
||
);
|
||
}
|
||
}
|
||
if let Some(other) = ours_seen.insert(ours, key) {
|
||
if other != key {
|
||
findings.note(
|
||
format!("{field}[surface aliasing]"),
|
||
au,
|
||
format!("our surface {ours} carries both {other:?} and {key:?}"),
|
||
);
|
||
}
|
||
}
|
||
if let Some(other) = theirs_seen.insert(theirs, key) {
|
||
if other != key {
|
||
findings.note(
|
||
format!("{field}[surface aliasing]"),
|
||
au,
|
||
format!("libav's surface {theirs} carries both {other:?} and {key:?}"),
|
||
);
|
||
}
|
||
}
|
||
self.live.insert(key, (ours, theirs));
|
||
}
|
||
// A surface either side has just reassigned no longer says anything about the picture
|
||
// that used to hold it, so the stale entries go — that is the difference between
|
||
// "the mapping broke" and "the pool moved on".
|
||
self.live.retain(|key, &mut (ours, theirs)| {
|
||
let ours_now = ours_seen.get(&ours);
|
||
let theirs_now = theirs_seen.get(&theirs);
|
||
ours_now.is_none_or(|k| k == key) && theirs_now.is_none_or(|k| k == key)
|
||
});
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// The capture
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// One captured buffer descriptor.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
struct CapturedDescriptor {
|
||
buffer_type: u32,
|
||
data_size: u32,
|
||
num_mbs_in_buffer: u32,
|
||
data_offset: u32,
|
||
}
|
||
|
||
/// A parsed capture, for ONE codec.
|
||
#[derive(Default)]
|
||
struct Capture {
|
||
pic_params: BTreeMap<usize, Vec<u8>>,
|
||
/// `Some(bytes)` for a submitted matrix, `None` for the explicit `absent` spelling. An AU
|
||
/// missing from this map was never reported either way, which is itself a finding.
|
||
qmatrix: BTreeMap<usize, Option<Vec<u8>>>,
|
||
descriptors: BTreeMap<usize, Vec<CapturedDescriptor>>,
|
||
config_bitstream_raw: BTreeMap<usize, u32>,
|
||
/// Lines that carry one of this harness's prefixes and could not be read. Never dropped
|
||
/// silently: a capture whose format drifted must fail loudly, not compare less.
|
||
unreadable: Vec<String>,
|
||
}
|
||
|
||
/// `hex` → bytes, or `None` when it is not an even-length hex string.
|
||
fn from_hex(hex: &str) -> Option<Vec<u8>> {
|
||
if hex.len() % 2 != 0 || hex.is_empty() {
|
||
return None;
|
||
}
|
||
(0..hex.len() / 2)
|
||
.map(|i| u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).ok())
|
||
.collect()
|
||
}
|
||
|
||
fn to_hex(bytes: &[u8]) -> String {
|
||
let mut out = String::with_capacity(2 * bytes.len());
|
||
for b in bytes {
|
||
let _ = write!(out, "{b:02x}");
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Parse the lines of one codec out of a capture. `codec` is the token FFmpeg's
|
||
/// `avcodec_get_name` produces: `h264` or `hevc`.
|
||
fn parse_capture(text: &str, codec: &str) -> Capture {
|
||
/// The markers, with their trailing space so a bare word cannot match one.
|
||
const MARKERS: [&str; 4] = ["PFPP ", "PFQM ", "PFBD ", "PFCFG "];
|
||
|
||
let mut out = Capture::default();
|
||
for raw in text.lines() {
|
||
// The marker is found ANYWHERE in the line, not required at its start: FFmpeg's logger
|
||
// prefixes a message logged against a codec context with `[h264 @ 0x…] `, and a capture
|
||
// made that way must still be readable (the recipe asks for `av_log(NULL, …)` so it is
|
||
// not, but a capture is expensive and this costs nothing).
|
||
let Some(start) = MARKERS.iter().filter_map(|m| raw.find(m)).min() else {
|
||
continue;
|
||
};
|
||
let line = raw[start..].trim();
|
||
let Some((prefix, rest)) = line.split_once(' ') else {
|
||
continue;
|
||
};
|
||
if !matches!(prefix, "PFPP" | "PFQM" | "PFBD" | "PFCFG") {
|
||
continue;
|
||
}
|
||
let fields: Vec<&str> = rest.split_whitespace().collect();
|
||
// Every line is `<prefix> <codec> <au> …`, so anything shorter is malformed.
|
||
let (Some(line_codec), Some(au)) = (fields.first(), fields.get(1)) else {
|
||
out.unreadable.push(line.to_string());
|
||
continue;
|
||
};
|
||
if *line_codec != codec {
|
||
continue;
|
||
}
|
||
let Ok(au) = au.parse::<usize>() else {
|
||
out.unreadable.push(line.to_string());
|
||
continue;
|
||
};
|
||
let ok = match (prefix, &fields[2..]) {
|
||
("PFPP", [hex]) => match from_hex(hex) {
|
||
Some(bytes) => out.pic_params.insert(au, bytes).is_none(),
|
||
None => false,
|
||
},
|
||
("PFQM", ["absent"]) => out.qmatrix.insert(au, None).is_none(),
|
||
("PFQM", [hex]) => match from_hex(hex) {
|
||
Some(bytes) => out.qmatrix.insert(au, Some(bytes)).is_none(),
|
||
None => false,
|
||
},
|
||
("PFBD", [kind, size, mbs, offset]) => {
|
||
match (
|
||
kind.parse::<u32>(),
|
||
size.parse::<u32>(),
|
||
mbs.parse::<u32>(),
|
||
offset.parse::<u32>(),
|
||
) {
|
||
(Ok(buffer_type), Ok(data_size), Ok(num_mbs_in_buffer), Ok(data_offset)) => {
|
||
out.descriptors
|
||
.entry(au)
|
||
.or_default()
|
||
.push(CapturedDescriptor {
|
||
buffer_type,
|
||
data_size,
|
||
num_mbs_in_buffer,
|
||
data_offset,
|
||
});
|
||
true
|
||
}
|
||
_ => false,
|
||
}
|
||
}
|
||
("PFCFG", [raw]) => match raw.parse::<u32>() {
|
||
Ok(raw) => {
|
||
out.config_bitstream_raw.insert(au, raw);
|
||
true
|
||
}
|
||
Err(_) => false,
|
||
},
|
||
_ => false,
|
||
};
|
||
if !ok {
|
||
out.unreadable.push(line.to_string());
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Read the capture named by `var`, or `None` when it is unset.
|
||
fn capture_from_env(var: &str, codec: &str) -> Option<Capture> {
|
||
let path = std::env::var(var).ok()?;
|
||
let text = std::fs::read_to_string(&path)
|
||
.unwrap_or_else(|e| panic!("{var}={path} could not be read: {e}"));
|
||
Some(parse_capture(&text, codec))
|
||
}
|
||
|
||
/// The checks every comparison needs before it compares anything: the capture is readable, it
|
||
/// covers the same AUs, and it was not made against a workaround path.
|
||
fn preflight(capture: &Capture, ours: usize, codec: &str, reserved16: Option<usize>) {
|
||
assert!(
|
||
capture.unreadable.is_empty(),
|
||
"the capture holds {} unreadable line(s) — the first is:\n {}\nthe recipe in this \
|
||
file's module docs is the format",
|
||
capture.unreadable.len(),
|
||
capture.unreadable[0]
|
||
);
|
||
assert!(
|
||
!capture.pic_params.is_empty(),
|
||
"the capture holds no `PFPP {codec}` lines: either the patch did not apply or the \
|
||
hwaccel never engaged (a software fallback logs nothing)"
|
||
);
|
||
assert_eq!(
|
||
capture.pic_params.len(),
|
||
ours,
|
||
"the capture covers {} AUs and this crate plans {ours} — the two sides must decode the \
|
||
same elementary stream, split the same way",
|
||
capture.pic_params.len()
|
||
);
|
||
let expected: BTreeSet<usize> = (0..ours).collect();
|
||
let seen: BTreeSet<usize> = capture.pic_params.keys().copied().collect();
|
||
assert_eq!(
|
||
seen, expected,
|
||
"the capture's AU indices are not 0..{ours}; pairing by index would compare different \
|
||
pictures"
|
||
);
|
||
if let Some(offset) = reserved16 {
|
||
let zeroed = capture
|
||
.pic_params
|
||
.values()
|
||
.filter(|pp| pp.len() > offset + 1 && u16_at(pp, offset) == 0)
|
||
.count();
|
||
assert_eq!(
|
||
zeroed, 0,
|
||
"{zeroed} of {ours} captured pictures carry Reserved16Bits = 0, which libavcodec \
|
||
writes only under FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO or \
|
||
FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG — this capture is against a workaround \
|
||
path and is VOID for comparison (module docs)"
|
||
);
|
||
}
|
||
// A `PFCFG` line is optional, but a wrong one voids the slice-control comparison: it means
|
||
// the driver read the other slice-control struct entirely.
|
||
//
|
||
// ⚠ AV1 is exempt, and not because the check is inconvenient: `ConfigBitstreamRaw`'s short
|
||
// format is a property of the two SHORT SLICE-CONTROL structs, and AV1's slice-control
|
||
// record is `DXVA_Tile_AV1`, which has no short/long pair for a config to select between.
|
||
// Comparing an AV1 capture's number against HEVC's 1 — which a `_ =>` arm would do — is a
|
||
// check of nothing that fails on anything.
|
||
let want = match codec {
|
||
"h264" => pf_dxvadec::short_slice_config(Codec::H264),
|
||
"hevc" => pf_dxvadec::short_slice_config(Codec::H265),
|
||
_ => return,
|
||
};
|
||
for (au, &raw) in &capture.config_bitstream_raw {
|
||
assert_eq!(
|
||
raw, want,
|
||
"AU {au}: the capture's ConfigBitstreamRaw is {raw}, and short format for {codec} \
|
||
is {want} — the capture used the other slice-control format, so its slice-control \
|
||
sizes describe a different struct"
|
||
);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// The comparisons
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// How two versions of one field differ, in a form a reader can act on: the whole value in hex
|
||
/// when it is small enough to read, and a count plus the first differing byte when it is an array
|
||
/// (a `SliceGroupMap` printed in full is 1620 characters of nothing).
|
||
fn byte_diff_detail(ours: &[u8], theirs: &[u8]) -> String {
|
||
if ours.len() <= 8 {
|
||
return format!("ours {}, libav {}", to_hex(ours), to_hex(theirs));
|
||
}
|
||
let first = ours
|
||
.iter()
|
||
.zip(theirs)
|
||
.position(|(a, b)| a != b)
|
||
.unwrap_or(0);
|
||
let differing = ours.iter().zip(theirs).filter(|(a, b)| a != b).count();
|
||
format!(
|
||
"{differing} of {} bytes differ, first at byte {first} of the field (ours {:#04x}, libav \
|
||
{:#04x})",
|
||
ours.len(),
|
||
ours[first],
|
||
theirs[first]
|
||
)
|
||
}
|
||
|
||
/// Compare the scalar fields — everything that is neither a surface index nor part of a
|
||
/// reference array — byte for byte, reporting by field name.
|
||
fn compare_scalars(
|
||
au: usize,
|
||
ours: &[u8],
|
||
theirs: &[u8],
|
||
ranges: &[(&'static str, Range<usize>)],
|
||
structural: &[&str],
|
||
allowance: Allowance,
|
||
findings: &mut Findings,
|
||
) {
|
||
let mut classified = vec![false; ours.len()];
|
||
for (name, range) in ranges {
|
||
for byte in range.clone() {
|
||
classified[byte] = true;
|
||
}
|
||
if structural.contains(name) {
|
||
continue;
|
||
}
|
||
if ours[range.clone()] != theirs[range.clone()] {
|
||
match allowance(name, &ours[range.clone()], &theirs[range.clone()]) {
|
||
Some(reason) => findings.document(*name, au, reason),
|
||
None => findings.note(
|
||
*name,
|
||
au,
|
||
byte_diff_detail(&ours[range.clone()], &theirs[range.clone()]),
|
||
),
|
||
}
|
||
}
|
||
}
|
||
// The field table must tile the struct; if a byte ever falls outside it, report it as a raw
|
||
// offset rather than pass over it. That is the fallback the module docs promise, and the
|
||
// reason this harness can never silently ignore a difference it cannot name.
|
||
for (offset, covered) in classified.iter().enumerate() {
|
||
if !covered && ours[offset] != theirs[offset] {
|
||
findings.note(
|
||
format!("<unclassified byte {offset:#06x}>"),
|
||
au,
|
||
format!("ours {:#04x}, libav {:#04x}", ours[offset], theirs[offset]),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Compare one AU's reference array as a SET, and feed the surface mapping.
|
||
fn compare_ref_array(
|
||
au: usize,
|
||
field: &str,
|
||
ours: &[(u8, RefEntry)],
|
||
theirs: &[(u8, RefEntry)],
|
||
mapping: &mut SurfaceMapping,
|
||
findings: &mut Findings,
|
||
) {
|
||
let mut ours_sorted: Vec<RefEntry> = ours.iter().map(|&(_, e)| e).collect();
|
||
let mut theirs_sorted: Vec<RefEntry> = theirs.iter().map(|&(_, e)| e).collect();
|
||
ours_sorted.sort_unstable();
|
||
theirs_sorted.sort_unstable();
|
||
if ours_sorted != theirs_sorted {
|
||
let only_ours: Vec<&RefEntry> = ours_sorted
|
||
.iter()
|
||
.filter(|e| !theirs_sorted.contains(e))
|
||
.collect();
|
||
let only_theirs: Vec<&RefEntry> = theirs_sorted
|
||
.iter()
|
||
.filter(|e| !ours_sorted.contains(e))
|
||
.collect();
|
||
findings.note(
|
||
format!("{field}[set]"),
|
||
au,
|
||
format!(
|
||
"{} entries ours vs {} libav; only ours: {only_ours:?}; only libav: {only_theirs:?}",
|
||
ours.len(),
|
||
theirs.len()
|
||
),
|
||
);
|
||
}
|
||
|
||
// The pairs the mapping is built from: a picture present on both sides, identified by its
|
||
// key. An ambiguous key (two entries claiming the same identity) is skipped and reported —
|
||
// it cannot happen off a conformant stream, and guessing which is which would invent a
|
||
// mapping.
|
||
let mut pairs = Vec::new();
|
||
for &(our_slot, entry) in ours {
|
||
let key = entry.key();
|
||
let ours_same = ours.iter().filter(|(_, e)| e.key() == key).count();
|
||
let matches: Vec<u8> = theirs
|
||
.iter()
|
||
.filter(|(_, e)| e.key() == key)
|
||
.map(|&(slot, _)| slot)
|
||
.collect();
|
||
if ours_same > 1 || matches.len() > 1 {
|
||
findings.note(
|
||
format!("{field}[ambiguous key]"),
|
||
au,
|
||
format!(
|
||
"{key:?} appears {ours_same} times ours and {} libav",
|
||
matches.len()
|
||
),
|
||
);
|
||
continue;
|
||
}
|
||
if let Some(&their_slot) = matches.first() {
|
||
pairs.push((key, (our_slot, their_slot)));
|
||
}
|
||
}
|
||
mapping.observe(au, field, &pairs, findings);
|
||
}
|
||
|
||
/// The whole H.264 picture-parameter comparison.
|
||
fn compare_h264_picparams(ours: &[OurSubmission], capture: &Capture) -> Findings {
|
||
let ranges = field_ranges(H264_FIELDS, size_of::<PicParamsH264>());
|
||
let structural = [
|
||
"CurrPic",
|
||
// The POC fields are compared RELATIVE to libavcodec's base offset rather than byte for
|
||
// byte — see `PocBase` for the measurement and for why this crate keeps 8.2.1's values.
|
||
"CurrFieldOrderCnt",
|
||
"RefFrameList",
|
||
"FieldOrderCntList",
|
||
"FrameNumList",
|
||
"UsedForReferenceFlags",
|
||
"NonExistingFrameFlags",
|
||
];
|
||
let mut findings = Findings::default();
|
||
let mut mapping = SurfaceMapping::default();
|
||
let mut poc = PocBase::default();
|
||
for (au, sub) in ours.iter().enumerate() {
|
||
let Some(theirs) = capture.pic_params.get(&au) else {
|
||
findings.note(
|
||
"<no capture>",
|
||
au,
|
||
"the capture holds no PFPP line for this AU",
|
||
);
|
||
continue;
|
||
};
|
||
if theirs.len() != sub.pic_params.len() {
|
||
findings.note(
|
||
"<struct size>",
|
||
au,
|
||
format!(
|
||
"the capture's picture parameters are {} bytes and ours are {} — the \
|
||
hand-declared layout and the header disagree, which is a finding on its own",
|
||
theirs.len(),
|
||
sub.pic_params.len()
|
||
),
|
||
);
|
||
continue;
|
||
}
|
||
compare_scalars(
|
||
au,
|
||
&sub.pic_params,
|
||
theirs,
|
||
&ranges,
|
||
&structural,
|
||
no_allowance,
|
||
&mut findings,
|
||
);
|
||
// The POC fields, compared RELATIVE to libavcodec's base offset (`PocBase`). The base is
|
||
// established from the CURRENT picture's own count, which is the one POC both sides
|
||
// certainly report for the same picture, and then required of every POC after it.
|
||
let poc_at = offset_of!(PicParamsH264, CurrFieldOrderCnt);
|
||
poc.check(
|
||
au,
|
||
"CurrFieldOrderCnt[0]",
|
||
i32_at(&sub.pic_params, poc_at),
|
||
i32_at(theirs, poc_at),
|
||
&mut findings,
|
||
);
|
||
poc.check(
|
||
au,
|
||
"CurrFieldOrderCnt[1]",
|
||
i32_at(&sub.pic_params, poc_at + 4),
|
||
i32_at(theirs, poc_at + 4),
|
||
&mut findings,
|
||
);
|
||
let mut their_entries = h264_ref_entries(theirs);
|
||
shift_poc(&mut their_entries, poc.offset(), true);
|
||
compare_ref_array(
|
||
au,
|
||
"RefFrameList",
|
||
&h264_ref_entries(&sub.pic_params),
|
||
&their_entries,
|
||
&mut mapping,
|
||
&mut findings,
|
||
);
|
||
// `CurrPic` needs no matching: the same AU is the same picture on both sides. It is fed
|
||
// through the mapping under the current picture's own key, so that when this picture
|
||
// shows up as a REFERENCE later, the two surfaces are checked against this pairing.
|
||
let curr = offset_of!(PicParamsH264, CurrPic);
|
||
let frame_num = offset_of!(PicParamsH264, frame_num);
|
||
if sub.pic_params[curr] & 0x80 != theirs[curr] & 0x80 {
|
||
findings.note(
|
||
"CurrPic[AssociatedFlag]",
|
||
au,
|
||
format!(
|
||
"ours {:#04x}, libav {:#04x} — the bottom-field flag, which is 0 for every \
|
||
picture inside this backend's progressive envelope",
|
||
sub.pic_params[curr], theirs[curr]
|
||
),
|
||
);
|
||
}
|
||
let key = (
|
||
false,
|
||
u16_at(&sub.pic_params, frame_num),
|
||
i32_at(&sub.pic_params, poc_at),
|
||
i32_at(&sub.pic_params, poc_at + 4),
|
||
);
|
||
mapping.observe(
|
||
au,
|
||
"CurrPic",
|
||
&[(key, (sub.pic_params[curr] & 0x7F, theirs[curr] & 0x7F))],
|
||
&mut findings,
|
||
);
|
||
}
|
||
findings
|
||
}
|
||
|
||
/// One HEVC RPS index array, resolved through its own side's `RefPicList` into the pictures it
|
||
/// names — which is the only form in which the two sides' arrays are comparable.
|
||
fn hevc_rps_pictures(pp: &[u8], array: usize, entries: &[(u8, RefEntry)]) -> Vec<Option<RefEntry>> {
|
||
let list = offset_of!(PicParamsHevc, RefPicList);
|
||
(0..8)
|
||
.map(|i| {
|
||
let index = pp[array + i];
|
||
// The array holds an index INTO `RefPicList` (15 entries), so anything outside that
|
||
// — the `0xFF` sentinel included — names nothing, and resolving it is how a stale or
|
||
// out-of-range index becomes a reported difference rather than a garbage read.
|
||
if usize::from(index) >= 15 {
|
||
return None;
|
||
}
|
||
let slot = pp[list + usize::from(index)];
|
||
if slot == UNUSED_ENTRY {
|
||
return None;
|
||
}
|
||
entries
|
||
.iter()
|
||
.find(|(s, _)| *s == slot & 0x7F)
|
||
.map(|&(_, entry)| entry)
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// The whole AV1 picture-parameter comparison.
|
||
///
|
||
/// Structurally simpler than the other two and the reason is worth stating: AV1 puts NO surface
|
||
/// index in its reference entries. `frame_refs[i].Index` is `ref_frame_idx[i]`, an AV1 SLOT both
|
||
/// sides read out of the same frame header, so the seven 36-byte entries — sizes, warp
|
||
/// parameters, warp type and slot alike — compare byte for byte with no re-indexing, no set
|
||
/// comparison and no allowance. Only two members carry surfaces, and they are handled as the
|
||
/// SHAPE of the store rather than by value ([`av1_reference_store`]).
|
||
///
|
||
/// There is no POC base to derive either: AV1's `order_hint` is a coded field, not a decoder's
|
||
/// running count, so libavcodec has nothing to seed it with.
|
||
///
|
||
/// ⚠ **`width`/`height` is a divergence waiting to be measured, and this comparison will
|
||
/// report it rather than absorb it.** libavcodec sends `avctx->width`, which
|
||
/// `update_context_with_frame_header` sets from `frame_width_minus_1 + 1` — FrameWidth, the
|
||
/// PRE-superres coded width — and the same for `frame_refs[i].width` off the reference's
|
||
/// `AVFrame`; this crate sends `UpscaledWidth`. With superres off the two are equal by
|
||
/// definition (7.20), which is every frame of the vendored vector and every frame a punktfunk
|
||
/// host emits, so a capture made from this vector cannot tell them apart. Deliberately given no
|
||
/// allowance: if a superres capture ever reaches this harness, the difference must be a finding
|
||
/// somebody reads, not a line somebody already excused. See `pic_av1.rs`'s note at `pp.width`.
|
||
fn compare_av1_picparams(ours: &[OurSubmission], capture: &Capture) -> Findings {
|
||
let ranges = field_ranges(AV1_FIELDS, size_of::<PicParamsAv1>());
|
||
// The two surface arrays, and nothing else: every other byte of this struct is a fact about
|
||
// the bitstream that both sides derive from the same frame header.
|
||
let structural = ["curr_pic_texture_index", "ref_frame_map_texture_index"];
|
||
let mut findings = Findings::default();
|
||
for (au, sub) in ours.iter().enumerate() {
|
||
let Some(theirs) = capture.pic_params.get(&au) else {
|
||
findings.note(
|
||
"<no capture>",
|
||
au,
|
||
"the capture holds no PFPP line for this frame",
|
||
);
|
||
continue;
|
||
};
|
||
if theirs.len() != sub.pic_params.len() {
|
||
findings.note(
|
||
"<struct size>",
|
||
au,
|
||
format!(
|
||
"the capture's picture parameters are {} bytes and ours are {}",
|
||
theirs.len(),
|
||
sub.pic_params.len()
|
||
),
|
||
);
|
||
continue;
|
||
}
|
||
compare_scalars(
|
||
au,
|
||
&sub.pic_params,
|
||
theirs,
|
||
&ranges,
|
||
&structural,
|
||
no_allowance,
|
||
&mut findings,
|
||
);
|
||
|
||
// The store, as a shape. Which SLOTS hold a picture is a fact about the bitstream and
|
||
// must agree; which SURFACE each holds is each side's own pool and never can.
|
||
let (our_curr, our_store, _) = av1_reference_store(&sub.pic_params);
|
||
let (their_curr, their_store, _) = av1_reference_store(theirs);
|
||
for slot in 0..8 {
|
||
let ours_occupied = our_store[slot] != UNUSED_INDEX;
|
||
let theirs_occupied = their_store[slot] != UNUSED_INDEX;
|
||
if ours_occupied != theirs_occupied {
|
||
findings.note(
|
||
format!("ref_frame_map_texture_index[{slot}][occupied]"),
|
||
au,
|
||
format!("ours {ours_occupied}, libav {theirs_occupied}"),
|
||
);
|
||
}
|
||
}
|
||
// The decode target must hold no store entry's surface. libavcodec cannot produce a
|
||
// collision — it fills the store from `h->ref[i]`, which the reference update has not
|
||
// run on yet, and takes `CurrPicTextureIndex` from `h->cur_frame.f` — so a collision on
|
||
// our side is a defect however the surfaces are numbered. This is the check that names
|
||
// the 2026-08-07 defect.
|
||
//
|
||
// ⚠ Note what is deliberately NOT checked: that two slots hold different surfaces. One
|
||
// picture in several reference slots is ordinary AV1 and this very vector does it —
|
||
// the key frame sits in BWDREF and ALTREF2 for the stream's whole length — so a
|
||
// "duplicate surface" check would fire on 273 of 274 frames of a correct conversion.
|
||
for (label, curr, store) in [
|
||
("ours", our_curr, our_store),
|
||
("libav", their_curr, their_store),
|
||
] {
|
||
if store.contains(&curr) {
|
||
findings.note(
|
||
"curr_pic_texture_index[aliases the store]",
|
||
au,
|
||
format!(
|
||
"{label}: surface {curr} is both the decode target and a reference \
|
||
store entry — the frame decodes into a picture it predicts from"
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
findings
|
||
}
|
||
|
||
/// The whole HEVC picture-parameter comparison.
|
||
fn compare_hevc_picparams(ours: &[OurSubmission], capture: &Capture) -> Findings {
|
||
let ranges = field_ranges(HEVC_FIELDS, size_of::<PicParamsHevc>());
|
||
let structural = [
|
||
"CurrPic",
|
||
// Relative, like H.264's — though libavcodec's HEVC POCs carry no base offset (measured:
|
||
// 0 on all 250 AUs of the vendored vector). `PocBase` derives whatever offset exists
|
||
// rather than assuming this one, so a future FFmpeg that grows one produces a documented
|
||
// line instead of 250 findings.
|
||
"CurrPicOrderCntVal",
|
||
"RefPicList",
|
||
"PicOrderCntValList",
|
||
"RefPicSetStCurrBefore",
|
||
"RefPicSetStCurrAfter",
|
||
"RefPicSetLtCurr",
|
||
];
|
||
let mut findings = Findings::default();
|
||
let mut mapping = SurfaceMapping::default();
|
||
let mut poc = PocBase::default();
|
||
for (au, sub) in ours.iter().enumerate() {
|
||
let Some(theirs) = capture.pic_params.get(&au) else {
|
||
findings.note(
|
||
"<no capture>",
|
||
au,
|
||
"the capture holds no PFPP line for this AU",
|
||
);
|
||
continue;
|
||
};
|
||
if theirs.len() != sub.pic_params.len() {
|
||
findings.note(
|
||
"<struct size>",
|
||
au,
|
||
format!(
|
||
"the capture's picture parameters are {} bytes and ours are {}",
|
||
theirs.len(),
|
||
sub.pic_params.len()
|
||
),
|
||
);
|
||
continue;
|
||
}
|
||
compare_scalars(
|
||
au,
|
||
&sub.pic_params,
|
||
theirs,
|
||
&ranges,
|
||
&structural,
|
||
hevc_allowance,
|
||
&mut findings,
|
||
);
|
||
let poc_at = offset_of!(PicParamsHevc, CurrPicOrderCntVal);
|
||
poc.check(
|
||
au,
|
||
"CurrPicOrderCntVal",
|
||
i32_at(&sub.pic_params, poc_at),
|
||
i32_at(theirs, poc_at),
|
||
&mut findings,
|
||
);
|
||
let our_entries = hevc_ref_entries(&sub.pic_params);
|
||
let mut their_entries = hevc_ref_entries(theirs);
|
||
// HEVC's entries carry one POC each, in `top`; `bottom` is a placeholder.
|
||
shift_poc(&mut their_entries, poc.offset(), false);
|
||
compare_ref_array(
|
||
au,
|
||
"RefPicList",
|
||
&our_entries,
|
||
&their_entries,
|
||
&mut mapping,
|
||
&mut findings,
|
||
);
|
||
for (name, offset) in [
|
||
(
|
||
"RefPicSetStCurrBefore",
|
||
offset_of!(PicParamsHevc, RefPicSetStCurrBefore),
|
||
),
|
||
(
|
||
"RefPicSetStCurrAfter",
|
||
offset_of!(PicParamsHevc, RefPicSetStCurrAfter),
|
||
),
|
||
(
|
||
"RefPicSetLtCurr",
|
||
offset_of!(PicParamsHevc, RefPicSetLtCurr),
|
||
),
|
||
] {
|
||
let ours_named = hevc_rps_pictures(&sub.pic_params, offset, &our_entries);
|
||
let theirs_named = hevc_rps_pictures(theirs, offset, &their_entries);
|
||
for (position, (a, b)) in ours_named.iter().zip(&theirs_named).enumerate() {
|
||
if a != b {
|
||
findings.note(
|
||
format!("{name}[{position}]"),
|
||
au,
|
||
format!("ours names {a:?}, libav names {b:?}"),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
let curr = offset_of!(PicParamsHevc, CurrPic);
|
||
let key = (false, 0u16, i32_at(&sub.pic_params, poc_at), 0);
|
||
mapping.observe(
|
||
au,
|
||
"CurrPic",
|
||
&[(key, (sub.pic_params[curr] & 0x7F, theirs[curr] & 0x7F))],
|
||
&mut findings,
|
||
);
|
||
}
|
||
findings
|
||
}
|
||
|
||
/// The quantization-matrix comparison: presence FIRST, then contents by field.
|
||
fn compare_qmatrix(
|
||
ours: &[OurSubmission],
|
||
capture: &Capture,
|
||
fields: &[(&'static str, usize)],
|
||
total: usize,
|
||
) -> Findings {
|
||
let ranges = field_ranges(fields, total);
|
||
let mut findings = Findings::default();
|
||
for (au, sub) in ours.iter().enumerate() {
|
||
let Some(theirs) = capture.qmatrix.get(&au) else {
|
||
findings.note(
|
||
"<no capture>",
|
||
au,
|
||
"the capture reports the matrix neither present nor `absent` for this AU — the \
|
||
PFQM patch (recipe step 4) is missing",
|
||
);
|
||
continue;
|
||
};
|
||
match (&sub.qmatrix, theirs) {
|
||
(None, None) => {}
|
||
(Some(_), None) => findings.note(
|
||
"<submitted>",
|
||
au,
|
||
"we submit an inverse-quantization-matrix buffer where libavcodec submits NONE — \
|
||
for HEVC this is review 13's defect: the picture parameters have told the \
|
||
driver to ignore the matrix, and a driver that honours it anyway dequantizes \
|
||
every residual against it",
|
||
),
|
||
(None, Some(_)) => findings.note(
|
||
"<submitted>",
|
||
au,
|
||
"libavcodec submits an inverse-quantization-matrix buffer and we submit none — \
|
||
the hardware is left to dequantize against whatever it last held",
|
||
),
|
||
(Some(mine), Some(theirs)) => {
|
||
if mine.len() != theirs.len() {
|
||
findings.note(
|
||
"<struct size>",
|
||
au,
|
||
format!("ours {} bytes, libav {}", mine.len(), theirs.len()),
|
||
);
|
||
continue;
|
||
}
|
||
for (name, range) in &ranges {
|
||
if mine[range.clone()] != theirs[range.clone()] {
|
||
findings.note(
|
||
*name,
|
||
au,
|
||
byte_diff_detail(&mine[range.clone()], &theirs[range.clone()]),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
findings
|
||
}
|
||
|
||
/// A descriptor buffer type as a name, for reports.
|
||
fn buffer_name(buffer_type: u32) -> &'static str {
|
||
match buffer_type {
|
||
BUFFER_PICTURE_PARAMETERS => "PICTURE_PARAMETERS",
|
||
BUFFER_INVERSE_QUANTIZATION_MATRIX => "INVERSE_QUANTIZATION_MATRIX",
|
||
BUFFER_SLICE_CONTROL => "SLICE_CONTROL",
|
||
BUFFER_BITSTREAM => "BITSTREAM",
|
||
_ => "<unknown buffer type>",
|
||
}
|
||
}
|
||
|
||
/// The descriptor comparison. Everything but the bitstream buffer's `DataSize` must match
|
||
/// exactly; that one field has a legitimate divergence class, which is classified apart rather
|
||
/// than reported as one undifferentiated difference (module docs).
|
||
fn compare_descriptors(ours: &[OurSubmission], capture: &Capture) -> Findings {
|
||
let mut findings = Findings::default();
|
||
for (au, sub) in ours.iter().enumerate() {
|
||
let Some(theirs) = capture.descriptors.get(&au) else {
|
||
findings.note(
|
||
"<no capture>",
|
||
au,
|
||
"the capture holds no PFBD lines for this AU",
|
||
);
|
||
continue;
|
||
};
|
||
let our_types: Vec<u32> = sub.descriptors.iter().map(|d| d.buffer_type).collect();
|
||
let their_types: Vec<u32> = theirs.iter().map(|d| d.buffer_type).collect();
|
||
if our_types != their_types {
|
||
// A missing BITSTREAM on their side is the one shape that is a MISSED PATCH rather
|
||
// than a divergence — the bitstream descriptor is the one libavcodec fills outside
|
||
// the choke point (recipe step 2) — so it is named as such.
|
||
let detail = if !their_types.contains(&BUFFER_BITSTREAM) {
|
||
"the capture carries no BITSTREAM descriptor at all: recipe step 2's SECOND \
|
||
patch site (the inline fill in commit_bitstream_and_slice_buffer) was missed"
|
||
.to_string()
|
||
} else {
|
||
format!(
|
||
"ours {:?}, libav {:?}",
|
||
our_types
|
||
.iter()
|
||
.map(|&t| buffer_name(t))
|
||
.collect::<Vec<_>>(),
|
||
their_types
|
||
.iter()
|
||
.map(|&t| buffer_name(t))
|
||
.collect::<Vec<_>>()
|
||
)
|
||
};
|
||
findings.note("<buffer set>", au, detail);
|
||
}
|
||
for our_desc in &sub.descriptors {
|
||
let name = buffer_name(our_desc.buffer_type);
|
||
let Some(their_desc) = theirs
|
||
.iter()
|
||
.find(|d| d.buffer_type == our_desc.buffer_type)
|
||
else {
|
||
continue; // already reported by the set comparison
|
||
};
|
||
if our_desc.data_offset != their_desc.data_offset {
|
||
findings.note(
|
||
format!("{name}.DataOffset"),
|
||
au,
|
||
format!(
|
||
"ours {}, libav {}",
|
||
our_desc.data_offset, their_desc.data_offset
|
||
),
|
||
);
|
||
}
|
||
if our_desc.num_mbs_in_buffer != their_desc.num_mbs_in_buffer {
|
||
findings.note(
|
||
format!("{name}.NumMBsInBuffer"),
|
||
au,
|
||
format!(
|
||
"ours {}, libav {} — this field cannot legitimately differ: \
|
||
mb_width*mb_height on H.264's bitstream and slice-control buffers, 0 \
|
||
everywhere else",
|
||
our_desc.num_mbs_in_buffer, their_desc.num_mbs_in_buffer
|
||
),
|
||
);
|
||
}
|
||
if our_desc.data_size == their_desc.data_size {
|
||
continue;
|
||
}
|
||
if our_desc.buffer_type != BUFFER_BITSTREAM {
|
||
findings.note(
|
||
format!("{name}.DataSize"),
|
||
au,
|
||
format!(
|
||
"ours {}, libav {} — a fixed-size buffer ({} is `sizeof` a structure or \
|
||
slices * 10), so this cannot legitimately differ",
|
||
our_desc.data_size, their_desc.data_size, name
|
||
),
|
||
);
|
||
continue;
|
||
}
|
||
// The bitstream buffer. Their slice COUNT is readable from their slice-control
|
||
// size, which is what separates "the two sides split the AU differently" from "the
|
||
// two sides delimit each slice a couple of bytes apart". (Both codecs' short
|
||
// records are TEN bytes — asserted in
|
||
// `the_slice_control_descriptor_is_one_ten_byte_short_format_record_per_slice_for_both_codecs`
|
||
// — so one divisor serves both; a slice-control buffer that is NOT a multiple of it
|
||
// has already been reported as a `SLICE_CONTROL.DataSize` difference above.)
|
||
let their_slices = theirs
|
||
.iter()
|
||
.find(|d| d.buffer_type == BUFFER_SLICE_CONTROL)
|
||
.map(|d| d.data_size as usize / size_of::<SliceH264Short>());
|
||
let our_slices = sub.records.len();
|
||
match their_slices {
|
||
Some(count) if count != our_slices => findings.note(
|
||
"BITSTREAM.DataSize[slice count]",
|
||
au,
|
||
format!(
|
||
"ours {} bytes over {our_slices} slices, libav {} over {count} — the two \
|
||
sides disagree about how many slices this AU has, which voids the size \
|
||
comparison and is the finding itself",
|
||
our_desc.data_size, their_desc.data_size
|
||
),
|
||
),
|
||
_ if their_desc.data_size % 128 != 0 => findings.note(
|
||
"BITSTREAM.DataSize[unpadded]",
|
||
au,
|
||
format!(
|
||
"libav's {} is not a multiple of 128, which means its tail padding was \
|
||
clamped by a mapping too small for the AU",
|
||
their_desc.data_size
|
||
),
|
||
),
|
||
_ => {
|
||
// Classify on the UNPADDED sizes, which is the only way a small difference
|
||
// can be told from a large one: padding rounds up to 128, so an eight-byte
|
||
// delimitation difference shows as a delta of 0 or of a whole 128 depending
|
||
// on which side of a granule the two land. Theirs is not captured directly,
|
||
// but padding is 1..=128 bytes, so it lies in a known window — and the
|
||
// difference is legitimate exactly when that window reaches to within four
|
||
// bytes per slice of our own unpadded size.
|
||
let delta = i64::from(our_desc.data_size) - i64::from(their_desc.data_size);
|
||
let tolerance = 4 * our_slices.max(1) as i64;
|
||
let their_low = i64::from(their_desc.data_size) - 128;
|
||
let their_high = i64::from(their_desc.data_size) - 1;
|
||
let ours_unpadded = i64::from(sub.unpadded);
|
||
let legitimate = their_low <= ours_unpadded + tolerance
|
||
&& ours_unpadded - tolerance <= their_high;
|
||
findings.note(
|
||
if legitimate {
|
||
"BITSTREAM.DataSize[delimitation]"
|
||
} else {
|
||
"BITSTREAM.DataSize"
|
||
},
|
||
au,
|
||
format!(
|
||
"ours {} (unpadded {ours_unpadded}), libav {} (unpadded \
|
||
{their_low}..={their_high}), delta {delta} over {our_slices} \
|
||
slices — {}",
|
||
our_desc.data_size,
|
||
their_desc.data_size,
|
||
if legitimate {
|
||
"within the trailing-zero delimitation class the module docs \
|
||
describe, but read it once rather than assume it"
|
||
} else {
|
||
"OUTSIDE the legitimate delimitation class: too large to be \
|
||
trailing zeros"
|
||
}
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
findings
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// This side, in the capture's own format
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Write our submissions as the capture format, so a dump of ours and a capture of libav's can
|
||
/// be diffed by any tool — and so the parser above is exercised against a writer that shares no
|
||
/// code with it.
|
||
fn dump(codec: &str, ours: &[OurSubmission]) -> String {
|
||
let mut text = String::new();
|
||
let raw = pf_dxvadec::short_slice_config(match codec {
|
||
"h264" => Codec::H264,
|
||
_ => Codec::H265,
|
||
});
|
||
let _ = writeln!(text, "PFCFG {codec} 0 {raw}");
|
||
for (au, sub) in ours.iter().enumerate() {
|
||
let _ = writeln!(text, "PFPP {codec} {au} {}", to_hex(&sub.pic_params));
|
||
match &sub.qmatrix {
|
||
Some(qm) => {
|
||
let _ = writeln!(text, "PFQM {codec} {au} {}", to_hex(qm));
|
||
}
|
||
None => {
|
||
let _ = writeln!(text, "PFQM {codec} {au} absent");
|
||
}
|
||
}
|
||
for desc in &sub.descriptors {
|
||
let _ = writeln!(
|
||
text,
|
||
"PFBD {codec} {au} {} {} {} {}",
|
||
desc.buffer_type, desc.data_size, desc.num_mbs_in_buffer, desc.data_offset
|
||
);
|
||
}
|
||
}
|
||
text
|
||
}
|
||
|
||
// ===========================================================================
|
||
// CPU-provable: no capture, ordinary CI
|
||
// ===========================================================================
|
||
|
||
/// Every hand-declared struct's field table must tile it exactly — no gap anywhere, and nothing
|
||
/// left over at the end.
|
||
///
|
||
/// Two jobs in one. It is what makes the reports above name the right field (a gap would name the
|
||
/// wrong one, or none at all). And it is the AUDIT the twelve-vs-ten slice-record defect asked
|
||
/// for, in executable form: a struct tiled exactly by its members has no padding, interior OR
|
||
/// tail, so its Rust layout is the C declaration under 1-byte packing — which is how `dxva.h`
|
||
/// declares all six. The slice records are in the list precisely because they are the pair that
|
||
/// got it wrong; the other four were confirmed against libavcodec's runtime `sizeof` as well.
|
||
#[test]
|
||
fn every_hand_declared_dxva_struct_is_tiled_exactly_by_its_fields() {
|
||
for (what, fields, total) in [
|
||
("PicParamsH264", H264_FIELDS, size_of::<PicParamsH264>()),
|
||
("PicParamsHevc", HEVC_FIELDS, size_of::<PicParamsHevc>()),
|
||
("QmatrixH264", H264_QMATRIX_FIELDS, size_of::<QmatrixH264>()),
|
||
("QmatrixHevc", HEVC_QMATRIX_FIELDS, size_of::<QmatrixHevc>()),
|
||
(
|
||
"SliceH264Short",
|
||
H264_SLICE_FIELDS,
|
||
size_of::<SliceH264Short>(),
|
||
),
|
||
(
|
||
"SliceHevcShort",
|
||
HEVC_SLICE_FIELDS,
|
||
size_of::<SliceHevcShort>(),
|
||
),
|
||
// AV1's two. `PicParamsAv1` is the one struct in this crate whose offsets were
|
||
// MEASURED rather than mirrored — `layout-probe-av1.c` compiled with MSVC against the
|
||
// Windows SDK's own `dxva.h` — and `dxva_av1.rs` pins every one at compile time. What
|
||
// this adds is the other half: that the TABLE above reaches all 912 bytes, so a
|
||
// capture comparison can name every one of them.
|
||
("PicParamsAv1", AV1_FIELDS, size_of::<PicParamsAv1>()),
|
||
("TileAv1", AV1_TILE_FIELDS, size_of::<TileAv1>()),
|
||
] {
|
||
assert_eq!(fields[0].1, 0, "{what}: the first field must start at 0");
|
||
let ranges = field_ranges(fields, total);
|
||
let mut next = 0usize;
|
||
for (name, range) in &ranges {
|
||
assert_eq!(
|
||
range.start, next,
|
||
"{what}: {name} leaves a gap — the struct has no interior padding, so \
|
||
consecutive offsets must tile it"
|
||
);
|
||
assert!(range.end > range.start, "{what}: {name} is empty");
|
||
next = range.end;
|
||
}
|
||
assert_eq!(next, total, "{what}: the table stops short of the struct");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn every_h264_au_submits_four_buffers_in_libavcodecs_order() {
|
||
for (au, sub) in our_h264_submissions().iter().enumerate() {
|
||
assert_eq!(
|
||
sub.descriptors
|
||
.iter()
|
||
.map(|d| d.buffer_type)
|
||
.collect::<Vec<_>>(),
|
||
vec![
|
||
BUFFER_PICTURE_PARAMETERS,
|
||
BUFFER_INVERSE_QUANTIZATION_MATRIX,
|
||
BUFFER_BITSTREAM,
|
||
BUFFER_SLICE_CONTROL,
|
||
],
|
||
"AU {au}"
|
||
);
|
||
}
|
||
}
|
||
|
||
/// **No AV1 submission names its decode surface anywhere in the reference store**, and every
|
||
/// reference NAME resolves through a slot that holds one.
|
||
///
|
||
/// This is the defect the Windows parity harness caught on 2026-08-07 and the one nothing on
|
||
/// the CPU could see, stated over the SUBMITTED BYTES — which is where a libavcodec capture
|
||
/// would see it too, and the reason it belongs in this file as well as in `pic_av1`'s own
|
||
/// tests. `plan_to_dxva_av1` released the picture this frame's own `refresh_frame_flags`
|
||
/// displaces before assigning the decode target a slot, and `SlotMap::assign` hands back the
|
||
/// slot just vacated — so `CurrPicTextureIndex` and one `RefFrameMapTextureIndex` entry were
|
||
/// the same surface on 268 of these 274 frames: decode into the picture you predict from.
|
||
/// Intel Arc followed the aliased surface and got 245 of 250 delivered frames wrong; NVIDIA
|
||
/// tolerated it for 63 frames and then lost one 16x24 luma block at the `order_hint` wrap.
|
||
///
|
||
/// libavcodec cannot produce this shape and that is the whole argument for calling it a defect
|
||
/// rather than a convention: `ff_dxva2_av1_fill_picture_parameters` fills
|
||
/// `RefFrameMapTextureIndex` from `h->ref[i]`, the pre-refresh store, and takes
|
||
/// `CurrPicTextureIndex` from `h->cur_frame.f`, a frame the reference-frame update has not run
|
||
/// on yet. The two cannot be one surface.
|
||
///
|
||
/// The counts are asserted, not printed. At zero references this test would pass against a
|
||
/// conversion that named nothing at all.
|
||
#[test]
|
||
fn no_av1_submission_names_its_decode_surface_in_the_reference_store() {
|
||
let subs = our_av1_submissions();
|
||
let (mut with_store, mut named_refs) = (0usize, 0usize);
|
||
for (frame, sub) in subs.iter().enumerate() {
|
||
let (curr, store, names) = av1_reference_store(&sub.pic_params);
|
||
assert!(
|
||
store.iter().all(|surface| *surface != curr),
|
||
"frame {frame}: surface {curr} is both CurrPicTextureIndex and a \
|
||
RefFrameMapTextureIndex entry"
|
||
);
|
||
if store.iter().any(|s| *s != UNUSED_INDEX) {
|
||
with_store += 1;
|
||
}
|
||
for (name, slot) in names.iter().enumerate() {
|
||
if *slot == UNUSED_INDEX {
|
||
continue;
|
||
}
|
||
named_refs += 1;
|
||
assert!(
|
||
usize::from(*slot) < store.len(),
|
||
"frame {frame}, reference name {name}: slot {slot} is outside the eight-entry \
|
||
store — `Index` is an AV1 reference SLOT, not a surface"
|
||
);
|
||
assert_ne!(
|
||
store[usize::from(*slot)],
|
||
UNUSED_INDEX,
|
||
"frame {frame}, reference name {name}: slot {slot} holds no surface, so the \
|
||
driver would follow `Index` into an empty entry"
|
||
);
|
||
}
|
||
}
|
||
assert_eq!(
|
||
with_store, 273,
|
||
"every frame but the opening key frame carries a populated reference store"
|
||
);
|
||
assert!(
|
||
named_refs > 0,
|
||
"no frame named a reference, so every check above was skipped"
|
||
);
|
||
}
|
||
|
||
/// AV1 submits THREE buffers and never a quantization matrix, on every frame of the vector.
|
||
///
|
||
/// The codec asymmetry the H.264 and HEVC tests above are about, taken to its third case.
|
||
/// H.264 submits the matrix unconditionally, HEVC only under `scaling_list_enabled_flag`, and
|
||
/// AV1 has no matrix BUFFER at all: its quantiser matrices are SELECTED by index
|
||
/// (`qm_y`/`qm_u`/`qm_v`) out of tables the decoder already holds, and `dxva2_av1_end_frame`
|
||
/// passes `NULL, 0` for the qm pair so the generic layer submits nothing. A fourth descriptor
|
||
/// here would be a buffer the driver has no `DXVA_Qmatrix_AV1` to read it as.
|
||
#[test]
|
||
fn every_av1_frame_submits_three_buffers_and_never_a_quantization_matrix() {
|
||
for (frame, sub) in our_av1_submissions().iter().enumerate() {
|
||
assert_eq!(
|
||
sub.descriptors
|
||
.iter()
|
||
.map(|d| d.buffer_type)
|
||
.collect::<Vec<_>>(),
|
||
vec![
|
||
BUFFER_PICTURE_PARAMETERS,
|
||
BUFFER_BITSTREAM,
|
||
BUFFER_SLICE_CONTROL,
|
||
],
|
||
"frame {frame}"
|
||
);
|
||
assert!(sub.qmatrix.is_none(), "frame {frame}");
|
||
}
|
||
}
|
||
|
||
/// No AV1 descriptor carries a macroblock count. AV1 has no macroblocks and `dxva2_av1.c`
|
||
/// never touches the field — the same statement `no_hevc_descriptor_ever_carries_a_macroblock_count`
|
||
/// makes, and the same defect class review 13 found on the H.264 side in the other direction.
|
||
#[test]
|
||
fn no_av1_descriptor_ever_carries_a_macroblock_count() {
|
||
for (frame, sub) in our_av1_submissions().iter().enumerate() {
|
||
assert_eq!(sub.mb_count, 0, "frame {frame}");
|
||
for desc in &sub.descriptors {
|
||
assert_eq!(
|
||
desc.num_mbs_in_buffer,
|
||
0,
|
||
"frame {frame}, {}",
|
||
buffer_name(desc.buffer_type)
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// AV1's slice-control buffer is `16 * tile count`, its bitstream descriptor is the packer's
|
||
/// PADDED size, and the tile records tile the unpadded window exactly — in order, without gaps
|
||
/// and without overlaps.
|
||
///
|
||
/// The last part is what distinguishes AV1 from the other two codecs here and is the reason
|
||
/// this cannot reuse `the_bitstream_descriptor_is_the_packers_padded_size_and_the_slice_records_tile_it_exactly`:
|
||
/// a `DXVA_Tile_AV1` addresses a TILE PAYLOAD, which is the bytes after that tile's
|
||
/// `tile_size_minus_1` field — so consecutive records are separated by those size fields and do
|
||
/// NOT abut, unlike H.264/HEVC slice records which tile their buffer with no gaps. What must
|
||
/// hold is weaker and still exact: strictly increasing, non-overlapping, inside the unpadded
|
||
/// window, and never starting at a tile-group OBU's first byte (which would hand the driver an
|
||
/// OBU header as entropy-coded tile data).
|
||
///
|
||
/// ⚠ The padding is charged to NO record. H.264 and HEVC add the tail padding to their last
|
||
/// slice record's `SliceBytesInBuffer`; `pack_av1` does not, because a tile's size is the
|
||
/// tile's, and the descriptor is the only place AV1's padding is accounted at all.
|
||
#[test]
|
||
fn the_av1_bitstream_descriptor_is_padded_and_the_tile_records_tile_it_without_overlapping() {
|
||
let mut frames_with_padding = 0usize;
|
||
for (frame, sub) in our_av1_submissions().iter().enumerate() {
|
||
let bitstream = sub
|
||
.descriptors
|
||
.iter()
|
||
.find(|d| d.buffer_type == BUFFER_BITSTREAM)
|
||
.unwrap_or_else(|| panic!("frame {frame} submits no bitstream buffer"));
|
||
assert_eq!(
|
||
bitstream.data_size % 128,
|
||
0,
|
||
"frame {frame}: the bitstream descriptor states the PADDED size"
|
||
);
|
||
// ⚠ `1..=128`, not `0..128`. `pack_av1` writes libavcodec's expression verbatim —
|
||
// `BITSTREAM_ALIGN - (cursor % BITSTREAM_ALIGN)` — so data that is ALREADY on the
|
||
// granule gets a whole 128-byte block rather than none (`pack_av1`'s
|
||
// `data_already_on_the_granule_still_gets_a_full_padding_block`). This vector never
|
||
// lands on the granule, which is exactly why the bound has to come from the rule and
|
||
// not from the measurement.
|
||
let padding = bitstream.data_size - sub.unpadded;
|
||
assert!(
|
||
(1..=128).contains(&padding),
|
||
"frame {frame}: {padding} bytes of padding"
|
||
);
|
||
frames_with_padding += 1;
|
||
|
||
let slice_control = sub
|
||
.descriptors
|
||
.iter()
|
||
.find(|d| d.buffer_type == BUFFER_SLICE_CONTROL)
|
||
.unwrap_or_else(|| panic!("frame {frame} submits no slice-control buffer"));
|
||
assert_eq!(
|
||
slice_control.data_size as usize,
|
||
size_of::<TileAv1>() * sub.tiles.len(),
|
||
"frame {frame}: sixteen bytes per TILE"
|
||
);
|
||
assert!(!sub.tiles.is_empty(), "frame {frame}: a frame has tiles");
|
||
|
||
let mut previous_end = 0u32;
|
||
for (i, tile) in sub.tiles.iter().enumerate() {
|
||
// `#[repr(packed)]` — copy the fields out before using them.
|
||
let (offset, size) = (tile.data_offset, tile.data_size);
|
||
assert!(size > 0, "frame {frame}, tile {i}: an empty tile payload");
|
||
assert!(
|
||
offset >= previous_end,
|
||
"frame {frame}, tile {i}: starts at {offset}, inside the previous tile which \
|
||
ends at {previous_end}"
|
||
);
|
||
assert!(
|
||
offset + size <= sub.unpadded,
|
||
"frame {frame}, tile {i}: runs past the bytes the packer wrote"
|
||
);
|
||
previous_end = offset + size;
|
||
assert_eq!(
|
||
tile.anchor_frame, UNUSED_INDEX,
|
||
"frame {frame}, tile {i}: large-scale-tile anchors are libavcodec's 0xFF"
|
||
);
|
||
}
|
||
}
|
||
assert_eq!(
|
||
frames_with_padding, VENDORED_AV1_FRAMES,
|
||
"every frame is padded — the rule is unconditional"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn every_h264_bitstream_and_slice_control_descriptor_carries_mb_width_times_mb_height() {
|
||
// Review 13's defect, over the whole vector: `NumMBsInBuffer` was 0 where libavcodec's
|
||
// H.264 path writes `h->mb_width * h->mb_height`, on the exact call this codebase has
|
||
// already seen an Intel driver reject a hand-built variant on. The vendored vector is
|
||
// 320x240 — 20x15 macroblocks.
|
||
for (au, sub) in our_h264_submissions().iter().enumerate() {
|
||
assert_eq!(sub.mb_count, 20 * 15, "AU {au}");
|
||
for desc in &sub.descriptors {
|
||
let expected = match desc.buffer_type {
|
||
BUFFER_BITSTREAM | BUFFER_SLICE_CONTROL => sub.mb_count,
|
||
_ => 0,
|
||
};
|
||
assert_eq!(
|
||
desc.num_mbs_in_buffer,
|
||
expected,
|
||
"AU {au}, {}",
|
||
buffer_name(desc.buffer_type)
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn every_h264_au_submits_the_quantization_matrix_buffer() {
|
||
// H.264's predicate is UNCONDITIONAL in libavcodec (it passes `&ctx_pic->qm` with
|
||
// `sizeof(qm)` every time), and the PPS's lists are always meaningful because the vendored
|
||
// parser has applied Table 7-2's fallback rules. This is the codec where omitting the
|
||
// buffer would be the defect — the mirror image of HEVC's.
|
||
for (au, sub) in our_h264_submissions().iter().enumerate() {
|
||
assert!(sub.qmatrix.is_some(), "AU {au}");
|
||
let desc = sub
|
||
.descriptors
|
||
.iter()
|
||
.find(|d| d.buffer_type == BUFFER_INVERSE_QUANTIZATION_MATRIX)
|
||
.unwrap_or_else(|| panic!("AU {au} submits no matrix buffer"));
|
||
assert_eq!(desc.data_size, size_of::<QmatrixH264>() as u32);
|
||
assert_eq!(desc.num_mbs_in_buffer, 0);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_whole_vendored_hevc_vector_omits_the_quantization_matrix_buffer() {
|
||
// Case 1 of the three the qmatrix predicate has: `scaling_list_enabled_flag` clear. The
|
||
// buffer is not submitted AT ALL — three descriptors, not four with an empty one.
|
||
let ours = our_hevc_submissions();
|
||
for (au, sub) in ours.iter().enumerate() {
|
||
assert!(sub.qmatrix.is_none(), "AU {au}");
|
||
assert_eq!(
|
||
sub.descriptors
|
||
.iter()
|
||
.map(|d| d.buffer_type)
|
||
.collect::<Vec<_>>(),
|
||
vec![
|
||
BUFFER_PICTURE_PARAMETERS,
|
||
BUFFER_BITSTREAM,
|
||
BUFFER_SLICE_CONTROL,
|
||
],
|
||
"AU {au}"
|
||
);
|
||
// …and the picture parameters say so, which is the predicate libavcodec reads.
|
||
let flags = u32_at(
|
||
&sub.pic_params,
|
||
offset_of!(PicParamsHevc, dwCodingParamToolFlags),
|
||
);
|
||
assert_eq!(flags & 1, 0, "AU {au}: scaling_list_enabled_flag");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn no_hevc_descriptor_ever_carries_a_macroblock_count() {
|
||
// The asymmetry, over the whole vector: libavcodec's HEVC path writes 0 where its H.264
|
||
// path writes mb_width*mb_height, so a CTB count here would be a fresh divergence in the
|
||
// other direction.
|
||
for (au, sub) in our_hevc_submissions().iter().enumerate() {
|
||
for desc in &sub.descriptors {
|
||
assert_eq!(
|
||
desc.num_mbs_in_buffer,
|
||
0,
|
||
"AU {au}, {}",
|
||
buffer_name(desc.buffer_type)
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn every_descriptor_of_both_codecs_starts_at_offset_zero_and_names_a_distinct_buffer() {
|
||
for (codec, subs) in [
|
||
("h264", our_h264_submissions()),
|
||
("hevc", our_hevc_submissions()),
|
||
] {
|
||
for (au, sub) in subs.iter().enumerate() {
|
||
let mut seen = BTreeSet::new();
|
||
for desc in &sub.descriptors {
|
||
assert_eq!(desc.data_offset, 0, "{codec} AU {au}");
|
||
assert!(
|
||
seen.insert(desc.buffer_type),
|
||
"{codec} AU {au}: buffer type {} submitted twice",
|
||
desc.buffer_type
|
||
);
|
||
assert!(desc.data_size > 0, "{codec} AU {au}: an empty buffer");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_bitstream_descriptor_is_the_packers_padded_size_and_the_slice_records_tile_it_exactly() {
|
||
// The internal consistency a driver reads: every `BSNALunitDataLocation` lands inside the
|
||
// buffer, the records are contiguous from byte 0, and the last one ends EXACTLY at
|
||
// `DataSize` — which is what makes the tail padding charged to it rather than dangling past
|
||
// the end (see pack.rs's module docs).
|
||
for (codec, subs) in [
|
||
("h264", our_h264_submissions()),
|
||
("hevc", our_hevc_submissions()),
|
||
] {
|
||
for (au, sub) in subs.iter().enumerate() {
|
||
let bitstream = sub
|
||
.descriptors
|
||
.iter()
|
||
.find(|d| d.buffer_type == BUFFER_BITSTREAM)
|
||
.unwrap_or_else(|| panic!("{codec} AU {au} submits no bitstream buffer"));
|
||
assert_eq!(bitstream.data_size % 128, 0, "{codec} AU {au}: padded size");
|
||
assert!(
|
||
bitstream.data_size >= sub.unpadded,
|
||
"{codec} AU {au}: {} bytes of slices in a {}-byte buffer",
|
||
sub.unpadded,
|
||
bitstream.data_size
|
||
);
|
||
assert!(
|
||
bitstream.data_size - sub.unpadded <= 128,
|
||
"{codec} AU {au}: {} bytes of padding",
|
||
bitstream.data_size - sub.unpadded
|
||
);
|
||
assert!(!sub.records.is_empty(), "{codec} AU {au}: no slices");
|
||
let mut cursor = 0u32;
|
||
for (i, record) in sub.records.iter().enumerate() {
|
||
assert_eq!(
|
||
record.location, cursor,
|
||
"{codec} AU {au}: slice {i} location"
|
||
);
|
||
assert!(
|
||
record.bytes > 3,
|
||
"{codec} AU {au}: slice {i} is start code only"
|
||
);
|
||
cursor = record.location + record.bytes;
|
||
assert!(
|
||
cursor <= bitstream.data_size,
|
||
"{codec} AU {au}: slice {i} runs past DataSize"
|
||
);
|
||
}
|
||
assert_eq!(
|
||
cursor, bitstream.data_size,
|
||
"{codec} AU {au}: the records must tile the whole buffer, padding included"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_slice_control_descriptor_is_one_ten_byte_short_format_record_per_slice_for_both_codecs() {
|
||
// Two facts in one, both measured against libavcodec on the RTX 4090 box rather than
|
||
// derived:
|
||
//
|
||
// * **The short record is TEN bytes.** The capture's slice-control `DataSize` is 20 on the
|
||
// H.264 vector, which is two slices per picture, and 10 on the HEVC vector, which is one
|
||
// slice segment per picture — two codecs, two slice counts, one record size. `dxva.h`
|
||
// packs these wire structures to a byte; a `#[repr(C)]` `{u32, u32, u16}` would be twelve
|
||
// and would displace every record after the first (see `dxva.rs`'s alignment section).
|
||
// * **The `ConfigBitstreamRaw` hazard**: short format is 2 for H.264 and 1 for HEVC — one
|
||
// number with two spellings — and these are the short records for both. A buffer sized for
|
||
// one format against a config negotiated for the other is a driver reading a different
|
||
// struct at every offset.
|
||
assert_eq!(pf_dxvadec::short_slice_config(Codec::H264), 2);
|
||
assert_eq!(pf_dxvadec::short_slice_config(Codec::H265), 1);
|
||
assert_eq!(size_of::<SliceH264Short>(), 10);
|
||
assert_eq!(size_of::<SliceHevcShort>(), 10);
|
||
for (codec, subs, slices_per_picture, capture_data_size) in [
|
||
("h264", our_h264_submissions(), 2usize, 20u32),
|
||
("hevc", our_hevc_submissions(), 1, 10),
|
||
] {
|
||
for (au, sub) in subs.iter().enumerate() {
|
||
let control = sub
|
||
.descriptors
|
||
.iter()
|
||
.find(|d| d.buffer_type == BUFFER_SLICE_CONTROL)
|
||
.unwrap_or_else(|| panic!("{codec} AU {au} submits no slice-control buffer"));
|
||
assert_eq!(
|
||
control.data_size as usize,
|
||
10 * sub.records.len(),
|
||
"{codec} AU {au}"
|
||
);
|
||
// The vectors' slice counts, so the record size above is anchored to the capture's
|
||
// own number rather than to an arithmetic identity: if our splitter ever produced a
|
||
// different slice count for these streams, the two sides would stop being
|
||
// comparable and 10 would no longer follow from 20 and 10.
|
||
assert_eq!(
|
||
sub.records.len(),
|
||
slices_per_picture,
|
||
"{codec} AU {au}: the vendored vector is {slices_per_picture} slice(s) per picture"
|
||
);
|
||
assert_eq!(
|
||
control.data_size, capture_data_size,
|
||
"{codec} AU {au}: libavcodec's captured slice-control DataSize"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_tail_padding_is_charged_to_the_last_slice_record_and_to_no_other() {
|
||
// libavcodec's `commit_bitstream_and_slice_buffer` zero-fills
|
||
// `FFMIN(128 - ((current - dxva_data) & 127), end - current)` bytes and then does
|
||
// `slice->SliceBytesInBuffer += padding` — on the FINAL loop iteration's record. So the last
|
||
// record's `SliceBytesInBuffer` counts the padding and every earlier record's does not, and
|
||
// a driver reading the records back must find them tiling the buffer exactly.
|
||
// [`pf_dxvadec::pack`] implements the same rule; this is where it is checked on the real
|
||
// vectors, on the H.264 one because that is the one with more than one slice per picture —
|
||
// the shape a single-slice vector cannot distinguish.
|
||
for (codec, subs) in [
|
||
("h264", our_h264_submissions()),
|
||
("hevc", our_hevc_submissions()),
|
||
] {
|
||
for (au, sub) in subs.iter().enumerate() {
|
||
let bitstream = sub
|
||
.descriptors
|
||
.iter()
|
||
.find(|d| d.buffer_type == BUFFER_BITSTREAM)
|
||
.unwrap_or_else(|| panic!("{codec} AU {au} submits no bitstream buffer"));
|
||
let padding = bitstream.data_size - sub.unpadded;
|
||
assert!(
|
||
(1..=128).contains(&padding),
|
||
"{codec} AU {au}: {padding} bytes of padding"
|
||
);
|
||
let (last, earlier) = sub
|
||
.records
|
||
.split_last()
|
||
.unwrap_or_else(|| panic!("{codec} AU {au}: no slices"));
|
||
// Every earlier record stops exactly where the next slice's start code begins, so
|
||
// none of them carries any of the padding.
|
||
for (i, record) in earlier.iter().enumerate() {
|
||
assert_eq!(
|
||
record.location + record.bytes,
|
||
sub.records[i + 1].location,
|
||
"{codec} AU {au}: record {i} does not end where record {} begins",
|
||
i + 1
|
||
);
|
||
}
|
||
// The last one runs to the end of the buffer…
|
||
assert_eq!(
|
||
last.location + last.bytes,
|
||
bitstream.data_size,
|
||
"{codec} AU {au}: the last record must reach DataSize"
|
||
);
|
||
// …and stripping the padding off it leaves exactly the slice bytes the packer wrote,
|
||
// which is the statement that the padding is in THAT record and nowhere else.
|
||
assert_eq!(
|
||
sub.records.iter().map(|r| r.bytes).sum::<u32>() - padding,
|
||
sub.unpadded,
|
||
"{codec} AU {au}: the padding is charged more than once, or not at all"
|
||
);
|
||
assert!(
|
||
last.bytes > padding,
|
||
"{codec} AU {au}: the last record is padding only"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_picture_parameter_buffer_is_the_whole_hand_declared_struct_for_both_codecs() {
|
||
for (codec, subs, size) in [
|
||
("h264", our_h264_submissions(), size_of::<PicParamsH264>()),
|
||
("hevc", our_hevc_submissions(), size_of::<PicParamsHevc>()),
|
||
("av1", our_av1_submissions(), size_of::<PicParamsAv1>()),
|
||
] {
|
||
for (au, sub) in subs.iter().enumerate() {
|
||
assert_eq!(sub.pic_params.len(), size, "{codec} AU {au}");
|
||
assert_eq!(
|
||
sub.descriptors[0].buffer_type, BUFFER_PICTURE_PARAMETERS,
|
||
"{codec} AU {au}"
|
||
);
|
||
assert_eq!(
|
||
sub.descriptors[0].data_size as usize, size,
|
||
"{codec} AU {au}"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The vector's first HEVC plan with its parameter sets rewritten to a chosen scaling-list
|
||
/// shape, converted and packed — the three cases of 7.4.5's activation, at the descriptor level.
|
||
///
|
||
/// Only the scaling-list fields move; everything else is the parser's own output, which is what
|
||
/// makes the "coded nowhere" case meaningful (`pps.scaling_list` then holds the parser's Table
|
||
/// 7-5/7-6 default fill, and `sps.scaling_list` the all-zero `ScalingLists::default()` an
|
||
/// uncoded SPS is left with).
|
||
fn hevc_case(enabled: bool, sps_coded: Option<u8>, pps_coded: Option<u8>) -> OurSubmission {
|
||
use std::rc::Rc;
|
||
|
||
let aus = split_into_aus_h265(TEST_25FPS_H265);
|
||
let mut planner = H265Planner::new();
|
||
let mut plan = planner.plan_au(aus[0]).expect("plan");
|
||
|
||
let mut sps = (*plan.sps).clone();
|
||
sps.scaling_list_enabled_flag = enabled;
|
||
sps.scaling_list_data_present_flag = sps_coded.is_some();
|
||
if let Some(fill) = sps_coded {
|
||
sps.scaling_list.scaling_list_4x4 = [[fill; 16]; 6];
|
||
sps.scaling_list.scaling_list_8x8 = [[fill; 64]; 6];
|
||
sps.scaling_list.scaling_list_16x16 = [[fill; 64]; 6];
|
||
sps.scaling_list.scaling_list_32x32 = [[fill; 64]; 6];
|
||
sps.scaling_list.scaling_list_dc_coef_minus8_16x16 = [i16::from(fill); 6];
|
||
sps.scaling_list.scaling_list_dc_coef_minus8_32x32 = [i16::from(fill); 6];
|
||
}
|
||
let mut pps = (*plan.pps).clone();
|
||
pps.scaling_list_data_present_flag = pps_coded.is_some();
|
||
if let Some(fill) = pps_coded {
|
||
pps.scaling_list.scaling_list_4x4 = [[fill; 16]; 6];
|
||
pps.scaling_list.scaling_list_8x8 = [[fill; 64]; 6];
|
||
pps.scaling_list.scaling_list_16x16 = [[fill; 64]; 6];
|
||
pps.scaling_list.scaling_list_32x32 = [[fill; 64]; 6];
|
||
pps.scaling_list.scaling_list_dc_coef_minus8_16x16 = [i16::from(fill); 6];
|
||
pps.scaling_list.scaling_list_dc_coef_minus8_32x32 = [i16::from(fill); 6];
|
||
}
|
||
plan.sps = Rc::new(sps);
|
||
plan.pps = Rc::new(pps);
|
||
|
||
let mut slots = SlotMap::new(plan.picture.max_dpb_frames);
|
||
let dxva = pf_dxvadec::plan_to_dxva_h265(&plan, &mut slots, 1).expect("convert");
|
||
let mut mapping = vec![0u8; MAPPING_BYTES];
|
||
let packed = pf_dxvadec::pack(aus[0], &dxva.slice_ranges, &mut mapping).expect("pack");
|
||
let unpadded = pf_dxvadec::packed_size(aus[0], &dxva.slice_ranges).expect("size") as u32;
|
||
OurSubmission {
|
||
pic_params: pf_dxvadec::as_bytes(&dxva.pic_params).to_vec(),
|
||
qmatrix: dxva
|
||
.qmatrix
|
||
.as_ref()
|
||
.map(|qm| pf_dxvadec::as_bytes(qm).to_vec()),
|
||
descriptors: pf_dxvadec::descriptors_h265(&dxva, &packed),
|
||
records: packed.records,
|
||
tiles: Vec::new(),
|
||
unpadded,
|
||
mb_count: 0,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn an_hevc_sequence_that_disables_scaling_lists_submits_no_matrix_however_much_is_coded() {
|
||
// Case 1 again, with data coded in BOTH parameter sets: the flag decides, not the data.
|
||
let sub = hevc_case(false, Some(7), Some(9));
|
||
assert!(sub.qmatrix.is_none());
|
||
assert!(!sub
|
||
.descriptors
|
||
.iter()
|
||
.any(|d| d.buffer_type == BUFFER_INVERSE_QUANTIZATION_MATRIX));
|
||
}
|
||
|
||
#[test]
|
||
fn an_hevc_sequence_that_enables_scaling_lists_and_codes_them_submits_the_coded_lists() {
|
||
// Case 2: the buffer travels, sized to the whole struct, carrying the coded data.
|
||
let sub = hevc_case(true, Some(7), Some(9));
|
||
let qm = sub
|
||
.qmatrix
|
||
.as_ref()
|
||
.expect("an enabled sequence submits the matrix");
|
||
assert_eq!(qm.len(), size_of::<QmatrixHevc>());
|
||
let desc = sub
|
||
.descriptors
|
||
.iter()
|
||
.find(|d| d.buffer_type == BUFFER_INVERSE_QUANTIZATION_MATRIX)
|
||
.expect("the matrix buffer is in the set");
|
||
assert_eq!(desc.data_size as usize, size_of::<QmatrixHevc>());
|
||
assert_eq!(desc.num_mbs_in_buffer, 0);
|
||
// The PPS's data wins over the SPS's (7.4.5), which is visible in the bytes themselves.
|
||
assert!(
|
||
qm.iter().all(|&b| b == 9 || b == 17),
|
||
"the PPS's fill of 9 (DC 9 + 8)"
|
||
);
|
||
assert_eq!(
|
||
sub.descriptors
|
||
.iter()
|
||
.map(|d| d.buffer_type)
|
||
.collect::<Vec<_>>(),
|
||
vec![
|
||
BUFFER_PICTURE_PARAMETERS,
|
||
BUFFER_INVERSE_QUANTIZATION_MATRIX,
|
||
BUFFER_BITSTREAM,
|
||
BUFFER_SLICE_CONTROL,
|
||
]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn an_hevc_sequence_that_enables_scaling_lists_but_codes_none_submits_the_defaults_not_zeros() {
|
||
// Case 3, and the one that is a live defect if it regresses: `scaling_list_enabled_flag` set
|
||
// with no scaling-list data in either parameter set is a legal, ordinary shape, and 7.4.5
|
||
// says the Table 7-5/7-6 DEFAULTS apply. FFmpeg's parser seeds those defaults; the vendored
|
||
// cros-codecs parser leaves an uncoded SPS all-ZERO — so a conversion that read the SPS
|
||
// here would submit a matrix of zeros while bit 0 of dwCodingParamToolFlags told the driver
|
||
// it was authoritative, and every residual would dequantize to nothing.
|
||
//
|
||
// pic_h265.rs checks the CONTENTS against the spec tables transcribed by hand; this checks
|
||
// the two facts the descriptor level owns — the buffer is submitted, and what it carries is
|
||
// not zeros.
|
||
let sub = hevc_case(true, None, None);
|
||
let qm = sub
|
||
.qmatrix
|
||
.as_ref()
|
||
.expect("an enabled sequence submits the matrix even with nothing coded");
|
||
let desc = sub
|
||
.descriptors
|
||
.iter()
|
||
.find(|d| d.buffer_type == BUFFER_INVERSE_QUANTIZATION_MATRIX)
|
||
.expect("the matrix buffer is in the set");
|
||
assert_eq!(desc.data_size as usize, size_of::<QmatrixHevc>());
|
||
assert!(
|
||
!qm.iter().all(|&b| b == 0),
|
||
"an all-zero matrix dequantizes every residual to nothing"
|
||
);
|
||
// Table 7-5's 4x4 lists are flat 16, and the inferred DC is 8 + 8 = 16.
|
||
let lists0 = offset_of!(QmatrixHevc, ucScalingLists0);
|
||
assert!(qm[lists0..lists0 + 96].iter().all(|&b| b == 16));
|
||
let dc2 = offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID2);
|
||
assert!(qm[dc2..dc2 + 6].iter().all(|&b| b == 16));
|
||
let dc3 = offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID3);
|
||
assert!(qm[dc3..dc3 + 2].iter().all(|&b| b == 16));
|
||
// …and every 8x8-and-up list carries a real curve rather than zeros.
|
||
let lists1 = offset_of!(QmatrixHevc, ucScalingLists1);
|
||
let lists3_end = offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID2);
|
||
assert!(qm[lists1..lists3_end].iter().all(|&b| b != 0));
|
||
}
|
||
|
||
#[test]
|
||
fn the_dump_and_the_parser_agree_and_the_comparison_finds_nothing_against_ourselves() {
|
||
// The comparators, exercised on real 250-AU data with a known answer. This is not a
|
||
// tautology dressed as a test: the writer and the parser share no code, so a format drift
|
||
// on either side fails here, and — with the next two tests, which mutate the "capture" and
|
||
// require a NAMED finding — it is what keeps this file from reporting a clean bill of
|
||
// health while comparing nothing.
|
||
let ours = our_h264_submissions();
|
||
let capture = parse_capture(&dump("h264", &ours), "h264");
|
||
preflight(
|
||
&capture,
|
||
ours.len(),
|
||
"h264",
|
||
Some(offset_of!(PicParamsH264, Reserved16Bits)),
|
||
);
|
||
assert_eq!(capture.pic_params.len(), VENDORED_AUS);
|
||
assert_eq!(capture.qmatrix.len(), VENDORED_AUS);
|
||
assert_eq!(capture.descriptors.len(), VENDORED_AUS);
|
||
for findings in [
|
||
compare_h264_picparams(&ours, &capture),
|
||
compare_descriptors(&ours, &capture),
|
||
compare_qmatrix(
|
||
&ours,
|
||
&capture,
|
||
H264_QMATRIX_FIELDS,
|
||
size_of::<QmatrixH264>(),
|
||
),
|
||
] {
|
||
assert!(
|
||
findings.is_empty(),
|
||
"comparing our own bytes against themselves must find nothing, got {:?}",
|
||
findings.fields()
|
||
);
|
||
// Nor may anything be DOCUMENTED away: an allowance that fires on identical bytes is an
|
||
// allowance that would hide a real difference.
|
||
assert!(
|
||
findings.documented_fields().is_empty(),
|
||
"identical bytes documented a divergence: {:?}",
|
||
findings.documented_fields()
|
||
);
|
||
}
|
||
|
||
let ours = our_hevc_submissions();
|
||
let capture = parse_capture(&dump("hevc", &ours), "hevc");
|
||
// No Reserved16Bits check for HEVC: the ClearVideo workaround is an H.264-only path.
|
||
preflight(&capture, ours.len(), "hevc", None);
|
||
for findings in [
|
||
compare_hevc_picparams(&ours, &capture),
|
||
compare_descriptors(&ours, &capture),
|
||
compare_qmatrix(
|
||
&ours,
|
||
&capture,
|
||
HEVC_QMATRIX_FIELDS,
|
||
size_of::<QmatrixHevc>(),
|
||
),
|
||
] {
|
||
assert!(
|
||
findings.is_empty(),
|
||
"comparing our own HEVC bytes against themselves must find nothing, got {:?}",
|
||
findings.fields()
|
||
);
|
||
assert!(
|
||
findings.documented_fields().is_empty(),
|
||
"identical bytes documented a divergence: {:?}",
|
||
findings.documented_fields()
|
||
);
|
||
}
|
||
// The HEVC matrices are `absent` on this vector, and the parser must carry that fact rather
|
||
// than losing it — the whole of review 13's defect is the difference between the two.
|
||
assert!(capture.qmatrix.values().all(Option::is_none));
|
||
|
||
// AV1, on all 274 FRAMES. Nothing here has ever been run against libavcodec's own bytes
|
||
// (module docs say why), so this self-comparison is the only thing standing between
|
||
// `compare_av1_picparams` and a first capture: it proves the 912-byte field table reaches
|
||
// every byte, that `av1_reference_store` reads `CurrPicTextureIndex` and the eight-entry
|
||
// store from the offsets it thinks it does — a wrong one would report a false alias on a
|
||
// correct submission — and that the comparison invents nothing on identical input.
|
||
let ours = our_av1_submissions();
|
||
let capture = parse_capture(&dump("av1", &ours), "av1");
|
||
preflight(&capture, ours.len(), "av1", None);
|
||
assert_eq!(capture.pic_params.len(), VENDORED_AV1_FRAMES);
|
||
for findings in [
|
||
compare_av1_picparams(&ours, &capture),
|
||
compare_descriptors(&ours, &capture),
|
||
] {
|
||
assert!(
|
||
findings.is_empty(),
|
||
"comparing our own AV1 bytes against themselves must find nothing, got {:?}",
|
||
findings.fields()
|
||
);
|
||
assert!(
|
||
findings.documented_fields().is_empty(),
|
||
"identical bytes documented a divergence: {:?}",
|
||
findings.documented_fields()
|
||
);
|
||
}
|
||
// AV1 reports `absent` on every frame — it has no matrix BUFFER at all, unlike HEVC where
|
||
// the same spelling is a per-sequence decision.
|
||
assert!(capture.qmatrix.values().all(Option::is_none));
|
||
}
|
||
|
||
/// A submission holding only what [`compare_descriptors`] reads, for the bitstream-size
|
||
/// classifier: `slices` slice records tiling a `padded`-byte buffer whose slice data (before the
|
||
/// tail padding) is `unpadded` bytes.
|
||
fn descriptor_only_submission(unpadded: u32, padded: u32, slices: usize) -> OurSubmission {
|
||
let each = unpadded / slices as u32;
|
||
let mut records: Vec<SliceRecord> = (0..slices)
|
||
.map(|i| SliceRecord {
|
||
location: i as u32 * each,
|
||
bytes: each,
|
||
})
|
||
.collect();
|
||
// The last record carries the remainder and the padding, exactly as the packer charges it.
|
||
let last = records.last_mut().expect("at least one slice");
|
||
last.bytes = padded - last.location;
|
||
OurSubmission {
|
||
pic_params: vec![0u8; size_of::<PicParamsH264>()],
|
||
qmatrix: None,
|
||
tiles: Vec::new(),
|
||
descriptors: vec![
|
||
BufferDescriptor {
|
||
buffer_type: BUFFER_BITSTREAM,
|
||
data_offset: 0,
|
||
data_size: padded,
|
||
num_mbs_in_buffer: 300,
|
||
},
|
||
BufferDescriptor {
|
||
buffer_type: BUFFER_SLICE_CONTROL,
|
||
data_offset: 0,
|
||
data_size: 10 * slices as u32,
|
||
num_mbs_in_buffer: 300,
|
||
},
|
||
],
|
||
records,
|
||
unpadded,
|
||
mb_count: 300,
|
||
}
|
||
}
|
||
|
||
/// Rewrite every `PFPP <codec>` line of a capture through `f`, which receives the AU index and
|
||
/// the picture-parameter bytes. The instrument the two absorbers below are tested with: a
|
||
/// divergence this harness ABSORBS must be reproducible on demand, or its allowance is untested.
|
||
fn map_picparams(text: &str, codec: &str, mut f: impl FnMut(usize, &mut Vec<u8>)) -> String {
|
||
let prefix = format!("PFPP {codec} ");
|
||
let mut out = String::new();
|
||
for line in text.lines() {
|
||
match line.strip_prefix(&prefix) {
|
||
Some(rest) => {
|
||
let (au, hex) = rest.split_once(' ').expect("our own dump is well formed");
|
||
let au: usize = au.parse().expect("a decimal AU index");
|
||
let mut bytes = from_hex(hex).expect("our own dump is hex");
|
||
f(au, &mut bytes);
|
||
let _ = writeln!(out, "{prefix}{au} {}", to_hex(&bytes));
|
||
}
|
||
None => {
|
||
let _ = writeln!(out, "{line}");
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Add `delta` to every POC an H.264 picture-parameters buffer carries: the current picture's pair
|
||
/// and every IN-USE reference entry's (an unused entry's counts are zero on both sides and must
|
||
/// stay that way). This is libavcodec's `prev_poc_msb` offset, synthesised.
|
||
fn shift_h264_capture_poc(pp: &mut [u8], delta: i32) {
|
||
let curr = offset_of!(PicParamsH264, CurrFieldOrderCnt);
|
||
let list = offset_of!(PicParamsH264, RefFrameList);
|
||
let focl = offset_of!(PicParamsH264, FieldOrderCntList);
|
||
for field in [curr, curr + 4] {
|
||
let shifted = i32_at(pp, field).wrapping_add(delta);
|
||
pp[field..field + 4].copy_from_slice(&shifted.to_le_bytes());
|
||
}
|
||
for i in 0..16 {
|
||
if pp[list + i] == UNUSED_ENTRY {
|
||
continue;
|
||
}
|
||
for field in [focl + 8 * i, focl + 8 * i + 4] {
|
||
let shifted = i32_at(pp, field).wrapping_add(delta);
|
||
pp[field..field + 4].copy_from_slice(&shifted.to_le_bytes());
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A one-AU capture of `(buffer type, DataSize, NumMBsInBuffer)` descriptors.
|
||
fn descriptor_capture(descs: &[(u32, u32, u32)]) -> String {
|
||
let mut text = String::new();
|
||
for (buffer_type, data_size, mbs) in descs {
|
||
let _ = writeln!(text, "PFBD h264 0 {buffer_type} {data_size} {mbs} 0");
|
||
}
|
||
text
|
||
}
|
||
|
||
#[test]
|
||
fn libavcodecs_constant_poc_base_is_documented_and_anything_else_about_a_poc_is_a_finding() {
|
||
// The absorber that lets the H.264 comparison pass against the real capture, and the three
|
||
// ways it must NOT absorb. Without this test the POC fields would simply be excluded from the
|
||
// comparison, which is the "green gate that proves nothing" shape this program has been bitten
|
||
// by: an excluded field cannot report a wrong POC either.
|
||
let ours = our_h264_submissions();
|
||
let base = dump("h264", &ours);
|
||
|
||
// 1. Every POC offset by FFmpeg's 65536 — what the RTX 4090 capture actually carries.
|
||
let shifted = map_picparams(&base, "h264", |_, pp| shift_h264_capture_poc(pp, 65536));
|
||
let findings = compare_h264_picparams(&ours, &parse_capture(&shifted, "h264"));
|
||
assert!(
|
||
findings.is_empty(),
|
||
"a constant POC base is not a finding, got {:?}",
|
||
findings.fields()
|
||
);
|
||
assert_eq!(
|
||
findings.documented_fields(),
|
||
vec!["FieldOrderCnt[POC base]"]
|
||
);
|
||
|
||
// 2. A base that is neither 0 nor 65536 is unexplained, and unexplained is a finding.
|
||
let odd = map_picparams(&base, "h264", |_, pp| shift_h264_capture_poc(pp, 7));
|
||
let findings = compare_h264_picparams(&ours, &parse_capture(&odd, "h264"));
|
||
assert_eq!(findings.fields(), vec!["CurrFieldOrderCnt[0][POC base]"]);
|
||
assert!(findings.documented_fields().is_empty());
|
||
|
||
// 3. A base that stops holding is a real POC divergence: 65536 everywhere except one AU,
|
||
// whose current picture is four counts adrift.
|
||
let drifting = map_picparams(&base, "h264", |au, pp| {
|
||
shift_h264_capture_poc(pp, if au == 10 { 65536 - 4 } else { 65536 })
|
||
});
|
||
let findings = compare_h264_picparams(&ours, &parse_capture(&drifting, "h264"));
|
||
assert_eq!(
|
||
findings.fields(),
|
||
vec![
|
||
"CurrFieldOrderCnt[0][POC]",
|
||
"CurrFieldOrderCnt[1][POC]",
|
||
"RefFrameList[set]",
|
||
]
|
||
);
|
||
assert_eq!(findings.by_field["CurrFieldOrderCnt[0][POC]"].first_au, 10);
|
||
}
|
||
|
||
#[test]
|
||
fn the_hevc_tiles_flag_allowance_is_exactly_bit_ten_with_tiles_disabled_and_nothing_else() {
|
||
// The other absorber, and the reason it is written per-DIFFERENCE rather than per-field: the
|
||
// same word carries eighteen other flags, and a difference in any of them — or in bit 10 while
|
||
// tiles are ENABLED, where the flag stops being inert — must still be a finding.
|
||
let ours = our_hevc_submissions();
|
||
let base = dump("hevc", &ours);
|
||
let at = offset_of!(PicParamsHevc, dwCodingSettingPicturePropertyFlags);
|
||
let rewrite = |text: &str, mask_off: u32, mask_on: u32| {
|
||
map_picparams(text, "hevc", |_, pp| {
|
||
let flags = (u32_at(pp, at) & !mask_off) | mask_on;
|
||
pp[at..at + 4].copy_from_slice(&flags.to_le_bytes());
|
||
})
|
||
};
|
||
|
||
// Bit 10 clear on libav's side, tiles disabled on both: the documented divergence, which is
|
||
// what the real capture carries on all 250 AUs.
|
||
let capture = parse_capture(&rewrite(&base, 1 << 10, 0), "hevc");
|
||
let findings = compare_hevc_picparams(&ours, &capture);
|
||
assert!(
|
||
findings.is_empty(),
|
||
"the documented tiles-flag divergence is not a finding, got {:?}",
|
||
findings.fields()
|
||
);
|
||
assert_eq!(
|
||
findings.documented_fields(),
|
||
vec!["dwCodingSettingPicturePropertyFlags"]
|
||
);
|
||
|
||
// A different bit of the same word is a finding: bit 11 is
|
||
// pps_loop_filter_across_slices_enabled_flag, which is not inert at all.
|
||
let capture = parse_capture(&rewrite(&base, 1 << 11, 0), "hevc");
|
||
let findings = compare_hevc_picparams(&ours, &capture);
|
||
assert_eq!(
|
||
findings.fields(),
|
||
vec!["dwCodingSettingPicturePropertyFlags"]
|
||
);
|
||
assert!(findings.documented_fields().is_empty());
|
||
|
||
// Bit 10 differing while BOTH sides say tiles are enabled: the flag now governs a real tile
|
||
// boundary, so the allowance must not apply.
|
||
let ours_with_tiles: Vec<OurSubmission> = ours
|
||
.iter()
|
||
.map(|sub| {
|
||
let mut pp = sub.pic_params.clone();
|
||
let flags = u32_at(&pp, at) | (1 << 7) | (1 << 10);
|
||
pp[at..at + 4].copy_from_slice(&flags.to_le_bytes());
|
||
OurSubmission {
|
||
pic_params: pp,
|
||
qmatrix: sub.qmatrix.clone(),
|
||
descriptors: sub.descriptors.clone(),
|
||
records: sub.records.clone(),
|
||
tiles: sub.tiles.clone(),
|
||
unpadded: sub.unpadded,
|
||
mb_count: sub.mb_count,
|
||
}
|
||
})
|
||
.collect();
|
||
let capture = parse_capture(
|
||
&rewrite(&dump("hevc", &ours_with_tiles), 1 << 10, 1 << 7),
|
||
"hevc",
|
||
);
|
||
let findings = compare_hevc_picparams(&ours_with_tiles, &capture);
|
||
assert_eq!(
|
||
findings.fields(),
|
||
vec!["dwCodingSettingPicturePropertyFlags"]
|
||
);
|
||
assert!(findings.documented_fields().is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn a_bitstream_size_difference_is_classified_by_the_unpadded_window_it_implies() {
|
||
// The one descriptor field with a legitimate divergence class, and the arithmetic that tells
|
||
// the two apart. Ours: 1026 bytes of slice data over two slices, padded to 1152. Their
|
||
// padding is not captured, but it is 1..=128 bytes, so a captured 1024 means their slice data
|
||
// was 896..=1023 — which reaches to within four bytes per slice of our 1026 (1022 is 4 bytes
|
||
// less over two slices), the trailing-zero delimitation shape. A captured 512 cannot: no
|
||
// padding puts their slice data anywhere near ours.
|
||
let ours = vec![descriptor_only_submission(1026, 1152, 2)];
|
||
|
||
let legitimate = descriptor_capture(&[
|
||
(BUFFER_BITSTREAM, 1024, 300),
|
||
(BUFFER_SLICE_CONTROL, 20, 300),
|
||
]);
|
||
let findings = compare_descriptors(&ours, &parse_capture(&legitimate, "h264"));
|
||
assert_eq!(findings.fields(), vec!["BITSTREAM.DataSize[delimitation]"]);
|
||
|
||
let defect = descriptor_capture(&[
|
||
(BUFFER_BITSTREAM, 512, 300),
|
||
(BUFFER_SLICE_CONTROL, 20, 300),
|
||
]);
|
||
let findings = compare_descriptors(&ours, &parse_capture(&defect, "h264"));
|
||
assert_eq!(findings.fields(), vec!["BITSTREAM.DataSize"]);
|
||
|
||
// A differing slice count outranks the size: the two sides split the AU differently, which
|
||
// voids the size comparison rather than needing a verdict of its own. Three ten-byte records
|
||
// where ours has two.
|
||
let split = descriptor_capture(&[
|
||
(BUFFER_BITSTREAM, 1024, 300),
|
||
(BUFFER_SLICE_CONTROL, 30, 300),
|
||
]);
|
||
let findings = compare_descriptors(&ours, &parse_capture(&split, "h264"));
|
||
assert_eq!(
|
||
findings.fields(),
|
||
vec!["BITSTREAM.DataSize[slice count]", "SLICE_CONTROL.DataSize"]
|
||
);
|
||
|
||
// And a NumMBsInBuffer difference is never in the legitimate class.
|
||
let zeroed = descriptor_capture(&[(BUFFER_BITSTREAM, 1152, 0), (BUFFER_SLICE_CONTROL, 20, 0)]);
|
||
let findings = compare_descriptors(&ours, &parse_capture(&zeroed, "h264"));
|
||
assert_eq!(
|
||
findings.fields(),
|
||
vec!["BITSTREAM.NumMBsInBuffer", "SLICE_CONTROL.NumMBsInBuffer"]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn a_changed_scalar_field_is_reported_by_its_name() {
|
||
let ours = our_h264_submissions();
|
||
let mut text = dump("h264", &ours);
|
||
// Flip `pic_init_qp_minus26` (offset 172) on AU 7 of the "capture".
|
||
let offset = offset_of!(PicParamsH264, pic_init_qp_minus26);
|
||
text = mutate_capture_byte(&text, 7, offset, 0x5A);
|
||
let capture = parse_capture(&text, "h264");
|
||
let findings = compare_h264_picparams(&ours, &capture);
|
||
assert_eq!(findings.fields(), vec!["pic_init_qp_minus26"]);
|
||
assert_eq!(findings.by_field["pic_init_qp_minus26"].first_au, 7);
|
||
|
||
// …and a differing byte the field table does NOT cover is still reported, by raw offset.
|
||
// The table tiles both structs (asserted above), so this fallback is unreachable in
|
||
// practice; it exists so that a field added to the struct without being added to the table
|
||
// cannot pass unnoticed, and it is exercised here with a deliberately truncated table rather
|
||
// than left as an unproven claim in the module docs.
|
||
let mut findings = Findings::default();
|
||
let mut theirs = ours[0].pic_params.clone();
|
||
theirs[offset] = 0x5A;
|
||
compare_scalars(
|
||
0,
|
||
&ours[0].pic_params,
|
||
&theirs,
|
||
&field_ranges(&H264_FIELDS[..2], 4),
|
||
&[],
|
||
no_allowance,
|
||
&mut findings,
|
||
);
|
||
let expected = format!("<unclassified byte {offset:#06x}>");
|
||
assert_eq!(findings.fields(), vec![expected.as_str()]);
|
||
}
|
||
|
||
#[test]
|
||
fn a_reordered_reference_list_is_no_finding_but_a_changed_one_is() {
|
||
// The divergence this harness must NOT report, and the one it must. libavcodec emits its
|
||
// reference array in a different order from ours by construction; a set comparison sees
|
||
// through that. Dropping a reference — or renumbering one side's surfaces inconsistently —
|
||
// must still be caught.
|
||
let ours = our_h264_submissions();
|
||
// An AU with at least two references, so a reversal is observable.
|
||
let (au, entries) = ours
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(au, sub)| (au, h264_ref_entries(&sub.pic_params)))
|
||
.find(|(_, entries)| entries.len() >= 2)
|
||
.expect("the vector must reach two references");
|
||
|
||
let reordered = reverse_h264_reference_list(&ours[au].pic_params);
|
||
assert_ne!(
|
||
reordered, ours[au].pic_params,
|
||
"the reversal must change bytes"
|
||
);
|
||
let capture = parse_capture(
|
||
&with_picparams(&dump("h264", &ours), au, &reordered),
|
||
"h264",
|
||
);
|
||
let findings = compare_h264_picparams(&ours, &capture);
|
||
assert!(
|
||
findings.is_empty(),
|
||
"a reordered reference list is not a finding, got {:?}",
|
||
findings.fields()
|
||
);
|
||
|
||
// Now DROP the last reference from that AU: the set differs, and it must be named.
|
||
let mut dropped = ours[au].pic_params.clone();
|
||
let list = offset_of!(PicParamsH264, RefFrameList);
|
||
let last = entries.len() - 1;
|
||
dropped[list + last] = UNUSED_ENTRY;
|
||
let used = offset_of!(PicParamsH264, UsedForReferenceFlags);
|
||
let cleared = u32_at(&dropped, used) & !(0b11 << (2 * last));
|
||
dropped[used..used + 4].copy_from_slice(&cleared.to_le_bytes());
|
||
let capture = parse_capture(&with_picparams(&dump("h264", &ours), au, &dropped), "h264");
|
||
let findings = compare_h264_picparams(&ours, &capture);
|
||
assert!(
|
||
findings.fields().contains(&"RefFrameList[set]"),
|
||
"a dropped reference must be reported, got {:?}",
|
||
findings.fields()
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn a_wholly_renumbered_surface_set_is_no_finding_and_an_inconsistent_one_is() {
|
||
// The bijection, both ways round. Renumbering EVERY surface index on the capture side is
|
||
// exactly what a different frame pool does, and it must pass; renumbering one AU's alone
|
||
// breaks the mapping under live pictures and must not.
|
||
let ours = our_h264_submissions();
|
||
let base = dump("h264", &ours);
|
||
|
||
let renumbered: Vec<Vec<u8>> = ours
|
||
.iter()
|
||
.map(|sub| renumber_h264_surfaces(&sub.pic_params, |slot| slot + 8))
|
||
.collect();
|
||
let mut text = base.clone();
|
||
for (au, pp) in renumbered.iter().enumerate() {
|
||
text = with_picparams(&text, au, pp);
|
||
}
|
||
let capture = parse_capture(&text, "h264");
|
||
let findings = compare_h264_picparams(&ours, &capture);
|
||
assert!(
|
||
findings.is_empty(),
|
||
"a consistently renumbered surface set is a bijection, not a finding, got {:?}",
|
||
findings.fields()
|
||
);
|
||
|
||
// One AU renumbered differently from the rest: the pictures it still holds change surface
|
||
// mid-life, which is precisely what a mis-resolved reference looks like.
|
||
let au = ours
|
||
.iter()
|
||
.position(|sub| h264_ref_entries(&sub.pic_params).len() >= 2)
|
||
.expect("two references");
|
||
let mut text = base;
|
||
for (i, pp) in renumbered.iter().enumerate() {
|
||
if i != au {
|
||
text = with_picparams(&text, i, pp);
|
||
}
|
||
}
|
||
let capture = parse_capture(&text, "h264");
|
||
let findings = compare_h264_picparams(&ours, &capture);
|
||
assert!(
|
||
findings
|
||
.fields()
|
||
.iter()
|
||
.any(|f| f.contains("surface mapping")),
|
||
"an inconsistent surface numbering must be reported, got {:?}",
|
||
findings.fields()
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn an_omitted_hevc_matrix_buffer_is_reported_as_a_presence_difference() {
|
||
// Review 13's HEVC defect, in the form the harness would have caught it: the capture says
|
||
// `absent`, our side submits one. (Built by rewriting the capture rather than the crate,
|
||
// because the crate no longer has the defect — which is the point.)
|
||
let ours = our_hevc_submissions();
|
||
let mut subs = ours;
|
||
subs[3].qmatrix = Some(vec![0u8; size_of::<QmatrixHevc>()]);
|
||
subs[3].descriptors = vec![
|
||
BufferDescriptor {
|
||
buffer_type: BUFFER_PICTURE_PARAMETERS,
|
||
data_offset: 0,
|
||
data_size: size_of::<PicParamsHevc>() as u32,
|
||
num_mbs_in_buffer: 0,
|
||
},
|
||
BufferDescriptor {
|
||
buffer_type: BUFFER_INVERSE_QUANTIZATION_MATRIX,
|
||
data_offset: 0,
|
||
data_size: size_of::<QmatrixHevc>() as u32,
|
||
num_mbs_in_buffer: 0,
|
||
},
|
||
subs[3].descriptors[1],
|
||
subs[3].descriptors[2],
|
||
];
|
||
// The capture is the honest one: no matrix on any AU.
|
||
let honest = our_hevc_submissions();
|
||
let capture = parse_capture(&dump("hevc", &honest), "hevc");
|
||
|
||
let findings = compare_qmatrix(
|
||
&subs,
|
||
&capture,
|
||
HEVC_QMATRIX_FIELDS,
|
||
size_of::<QmatrixHevc>(),
|
||
);
|
||
assert_eq!(findings.fields(), vec!["<submitted>"]);
|
||
assert_eq!(findings.by_field["<submitted>"].first_au, 3);
|
||
let findings = compare_descriptors(&subs, &capture);
|
||
assert_eq!(findings.fields(), vec!["<buffer set>"]);
|
||
}
|
||
|
||
#[test]
|
||
fn a_missing_bitstream_descriptor_is_reported_as_a_missed_patch_site_not_a_defect() {
|
||
// The one capture-side mistake that is likely, because libavcodec fills the bitstream
|
||
// descriptor outside the choke point: three PFBD lines per AU instead of four. The harness
|
||
// must name the patch site rather than accuse our submission of dropping a buffer.
|
||
let ours = our_h264_submissions();
|
||
let text: String = dump("h264", &ours)
|
||
.lines()
|
||
.filter(|line| {
|
||
// Only the BITSTREAM descriptor, which is the FOURTH token: matching on " 6 "
|
||
// anywhere would also delete AU 6's whole descriptor set.
|
||
let fields: Vec<&str> = line.split_whitespace().collect();
|
||
!(fields.first() == Some(&"PFBD") && fields.get(3) == Some(&"6"))
|
||
})
|
||
.map(|line| format!("{line}\n"))
|
||
.collect();
|
||
let capture = parse_capture(&text, "h264");
|
||
let findings = compare_descriptors(&ours, &capture);
|
||
assert_eq!(findings.fields(), vec!["<buffer set>"]);
|
||
assert!(
|
||
findings.by_field["<buffer set>"]
|
||
.detail
|
||
.contains("commit_bitstream_and_slice_buffer"),
|
||
"the report must name the patch site, got {:?}",
|
||
findings.by_field["<buffer set>"].detail
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn an_unreadable_or_short_capture_is_refused_rather_than_partly_compared() {
|
||
let ours = our_h264_submissions();
|
||
let good = dump("h264", &ours);
|
||
|
||
// A line whose format drifted.
|
||
let broken = good.replace("PFPP h264 5 ", "PFPP h264 5 zz");
|
||
let capture = parse_capture(&broken, "h264");
|
||
assert_eq!(capture.unreadable.len(), 1);
|
||
|
||
// A capture of the wrong stream length.
|
||
let short: String = good
|
||
.lines()
|
||
.filter(|line| !line.starts_with("PFPP h264 24 "))
|
||
.map(|line| format!("{line}\n"))
|
||
.collect();
|
||
let capture = parse_capture(&short, "h264");
|
||
assert_eq!(capture.pic_params.len(), VENDORED_AUS - 1);
|
||
assert!(!capture.pic_params.contains_key(&24));
|
||
|
||
// Another codec's lines are not this codec's.
|
||
assert!(parse_capture(&good, "hevc").pic_params.is_empty());
|
||
|
||
// A capture logged against a codec context rather than NULL carries FFmpeg's own
|
||
// `[h264 @ 0x…] ` prefix, and must still read cleanly.
|
||
let prefixed: String = good
|
||
.lines()
|
||
.map(|line| format!("[h264 @ 0x7ff1c380a200] {line}\n"))
|
||
.collect();
|
||
let capture = parse_capture(&prefixed, "h264");
|
||
assert!(capture.unreadable.is_empty());
|
||
assert_eq!(capture.pic_params.len(), VENDORED_AUS);
|
||
assert_eq!(capture.descriptors.len(), VENDORED_AUS);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Capture-side mutators, for the tests above
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Replace AU `au`'s `PFPP` line with `pp`.
|
||
fn with_picparams(text: &str, au: usize, pp: &[u8]) -> String {
|
||
let prefix = format!("PFPP h264 {au} ");
|
||
let hevc = format!("PFPP hevc {au} ");
|
||
text.lines()
|
||
.map(|line| {
|
||
if line.starts_with(&prefix) {
|
||
format!("{prefix}{}\n", to_hex(pp))
|
||
} else if line.starts_with(&hevc) {
|
||
format!("{hevc}{}\n", to_hex(pp))
|
||
} else {
|
||
format!("{line}\n")
|
||
}
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Set one byte of AU `au`'s captured picture parameters.
|
||
fn mutate_capture_byte(text: &str, au: usize, offset: usize, value: u8) -> String {
|
||
let prefix = format!("PFPP h264 {au} ");
|
||
let mut out = String::new();
|
||
for line in text.lines() {
|
||
if let Some(hex) = line.strip_prefix(&prefix) {
|
||
let mut bytes = from_hex(hex).expect("our own dump is hex");
|
||
bytes[offset] = value;
|
||
let _ = writeln!(out, "{prefix}{}", to_hex(&bytes));
|
||
} else {
|
||
let _ = writeln!(out, "{line}");
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Reverse the in-use entries of an H.264 reference array, carrying each entry's keys and flag
|
||
/// bits with it — the reordering libavcodec's own `short_ref`-then-`long_ref` walk produces,
|
||
/// synthesised so the set comparison can be tested without a capture.
|
||
fn reverse_h264_reference_list(pp: &[u8]) -> Vec<u8> {
|
||
let mut out = pp.to_vec();
|
||
let list = offset_of!(PicParamsH264, RefFrameList);
|
||
let poc = offset_of!(PicParamsH264, FieldOrderCntList);
|
||
let nums = offset_of!(PicParamsH264, FrameNumList);
|
||
let used_at = offset_of!(PicParamsH264, UsedForReferenceFlags);
|
||
let missing_at = offset_of!(PicParamsH264, NonExistingFrameFlags);
|
||
let entries = h264_ref_entries(pp);
|
||
let slots: Vec<u8> = (0..entries.len()).map(|i| pp[list + i]).collect();
|
||
let used = u32_at(pp, used_at);
|
||
let missing = u16_at(pp, missing_at);
|
||
let mut new_used = used & !((1u32 << (2 * entries.len())) - 1);
|
||
let mut new_missing = missing & !((1u16 << entries.len()) - 1);
|
||
for (i, source) in (0..entries.len()).rev().enumerate() {
|
||
out[list + i] = slots[source];
|
||
out[nums + 2 * i..nums + 2 * i + 2]
|
||
.copy_from_slice(&pp[nums + 2 * source..nums + 2 * source + 2]);
|
||
out[poc + 8 * i..poc + 8 * i + 8]
|
||
.copy_from_slice(&pp[poc + 8 * source..poc + 8 * source + 8]);
|
||
new_used |= (used >> (2 * source) & 0b11) << (2 * i);
|
||
new_missing |= (missing >> source & 1) << i;
|
||
}
|
||
out[used_at..used_at + 4].copy_from_slice(&new_used.to_le_bytes());
|
||
out[missing_at..missing_at + 2].copy_from_slice(&new_missing.to_le_bytes());
|
||
out
|
||
}
|
||
|
||
/// Rewrite every surface index of an H.264 picture-parameters buffer through `f`.
|
||
fn renumber_h264_surfaces(pp: &[u8], f: impl Fn(u8) -> u8) -> Vec<u8> {
|
||
let mut out = pp.to_vec();
|
||
let curr = offset_of!(PicParamsH264, CurrPic);
|
||
out[curr] = (out[curr] & 0x80) | (f(out[curr] & 0x7F) & 0x7F);
|
||
let list = offset_of!(PicParamsH264, RefFrameList);
|
||
for i in 0..16 {
|
||
if out[list + i] != UNUSED_ENTRY {
|
||
out[list + i] = (out[list + i] & 0x80) | (f(out[list + i] & 0x7F) & 0x7F);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
// ===========================================================================
|
||
// Capture-dependent: #[ignore]d, and saying why
|
||
// ===========================================================================
|
||
|
||
/// Emit this crate's whole submission — both codecs — in the capture's own format, so the two
|
||
/// files can be diffed by any tool without a capture at all.
|
||
#[test]
|
||
#[ignore = "needs a libavcodec capture: PF_LIBAV_CAPTURE_AV1=<file> (see the module docs)"]
|
||
fn our_av1_picture_parameters_match_libavcodecs() {
|
||
let capture = capture_from_env("PF_LIBAV_CAPTURE_AV1", "av1")
|
||
.expect("PF_LIBAV_CAPTURE_AV1=<file> names a capture (see the module docs)");
|
||
let ours = our_av1_submissions();
|
||
// No `Reserved16Bits` preflight: both libavcodec workarounds are H.264-only
|
||
// (`dxva2_h264.c`), and `DXVA_PicParams_AV1` has no such field to test.
|
||
preflight(&capture, ours.len(), "av1", None);
|
||
compare_av1_picparams(&ours, &capture).verdict("AV1 picture parameters", ours.len());
|
||
}
|
||
|
||
#[test]
|
||
#[ignore = "writes a dump: PF_DXVA_DUMP=<path>"]
|
||
fn dump_our_submission_in_the_captures_own_format() {
|
||
let path = std::env::var("PF_DXVA_DUMP").expect("PF_DXVA_DUMP=<path> names the output file");
|
||
let mut text = dump("h264", &our_h264_submissions());
|
||
text.push_str(&dump("hevc", &our_hevc_submissions()));
|
||
// AV1 too, and it is the codec that needs this most: no libavcodec AV1 capture has
|
||
// ever been taken (module docs say why), so for that codec this dump is the only
|
||
// way to read what the driver is being handed at all.
|
||
text.push_str(&dump("av1", &our_av1_submissions()));
|
||
std::fs::write(&path, text).expect("write the dump");
|
||
println!("wrote {path}");
|
||
}
|
||
|
||
#[test]
|
||
#[ignore = "needs a libavcodec capture: PF_LIBAV_CAPTURE_H264=<file> (see the module docs)"]
|
||
fn our_h264_picture_parameters_match_libavcodecs() {
|
||
let capture = capture_from_env("PF_LIBAV_CAPTURE_H264", "h264")
|
||
.expect("PF_LIBAV_CAPTURE_H264=<file> names a capture (see the module docs)");
|
||
let ours = our_h264_submissions();
|
||
preflight(
|
||
&capture,
|
||
ours.len(),
|
||
"h264",
|
||
Some(offset_of!(PicParamsH264, Reserved16Bits)),
|
||
);
|
||
compare_h264_picparams(&ours, &capture).verdict("H.264 picture parameters", ours.len());
|
||
}
|
||
|
||
#[test]
|
||
#[ignore = "needs a libavcodec capture: PF_LIBAV_CAPTURE_HEVC=<file> (see the module docs)"]
|
||
fn our_hevc_picture_parameters_match_libavcodecs() {
|
||
let capture = capture_from_env("PF_LIBAV_CAPTURE_HEVC", "hevc")
|
||
.expect("PF_LIBAV_CAPTURE_HEVC=<file> names a capture (see the module docs)");
|
||
let ours = our_hevc_submissions();
|
||
preflight(&capture, ours.len(), "hevc", None);
|
||
compare_hevc_picparams(&ours, &capture).verdict("HEVC picture parameters", ours.len());
|
||
}
|
||
|
||
#[test]
|
||
#[ignore = "needs a libavcodec capture: PF_LIBAV_CAPTURE_H264/PF_LIBAV_CAPTURE_HEVC (module docs)"]
|
||
fn our_buffer_descriptors_match_libavcodecs() {
|
||
let h264 = capture_from_env("PF_LIBAV_CAPTURE_H264", "h264");
|
||
let hevc = capture_from_env("PF_LIBAV_CAPTURE_HEVC", "hevc");
|
||
assert!(
|
||
h264.is_some() || hevc.is_some(),
|
||
"PF_LIBAV_CAPTURE_H264=<file> and/or PF_LIBAV_CAPTURE_HEVC=<file> name a capture"
|
||
);
|
||
if let Some(capture) = h264 {
|
||
let ours = our_h264_submissions();
|
||
preflight(
|
||
&capture,
|
||
ours.len(),
|
||
"h264",
|
||
Some(offset_of!(PicParamsH264, Reserved16Bits)),
|
||
);
|
||
compare_descriptors(&ours, &capture).verdict("H.264 buffer descriptors", ours.len());
|
||
}
|
||
if let Some(capture) = hevc {
|
||
let ours = our_hevc_submissions();
|
||
preflight(&capture, ours.len(), "hevc", None);
|
||
compare_descriptors(&ours, &capture).verdict("HEVC buffer descriptors", ours.len());
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
#[ignore = "needs a libavcodec capture: PF_LIBAV_CAPTURE_H264/PF_LIBAV_CAPTURE_HEVC (module docs)"]
|
||
fn our_quantization_matrices_match_libavcodecs() {
|
||
let h264 = capture_from_env("PF_LIBAV_CAPTURE_H264", "h264");
|
||
let hevc = capture_from_env("PF_LIBAV_CAPTURE_HEVC", "hevc");
|
||
assert!(
|
||
h264.is_some() || hevc.is_some(),
|
||
"PF_LIBAV_CAPTURE_H264=<file> and/or PF_LIBAV_CAPTURE_HEVC=<file> name a capture"
|
||
);
|
||
if let Some(capture) = h264 {
|
||
let ours = our_h264_submissions();
|
||
preflight(
|
||
&capture,
|
||
ours.len(),
|
||
"h264",
|
||
Some(offset_of!(PicParamsH264, Reserved16Bits)),
|
||
);
|
||
compare_qmatrix(
|
||
&ours,
|
||
&capture,
|
||
H264_QMATRIX_FIELDS,
|
||
size_of::<QmatrixH264>(),
|
||
)
|
||
.verdict("H.264 quantization matrices", ours.len());
|
||
}
|
||
if let Some(capture) = hevc {
|
||
let ours = our_hevc_submissions();
|
||
preflight(&capture, ours.len(), "hevc", None);
|
||
compare_qmatrix(
|
||
&ours,
|
||
&capture,
|
||
HEVC_QMATRIX_FIELDS,
|
||
size_of::<QmatrixHevc>(),
|
||
)
|
||
.verdict("HEVC quantization matrices", ours.len());
|
||
}
|
||
}
|