diff --git a/crates/pf-dxvadec/src/descriptors.rs b/crates/pf-dxvadec/src/descriptors.rs new file mode 100644 index 00000000..ad2f1b58 --- /dev/null +++ b/crates/pf-dxvadec/src/descriptors.rs @@ -0,0 +1,444 @@ +//! The buffer DESCRIPTORS one `ID3D11VideoContext::SubmitDecoderBuffers` call +//! carries: which buffers are in the set at all, and the four +//! `D3D11_VIDEO_DECODER_BUFFER_DESC` fields whose values are a DECISION rather +//! than a pointer the driver handed back. +//! +//! # Why this is a module of its own +//! +//! Review 13 found four defects in this backend. **Two of the three structural +//! ones lived here rather than in the picture parameters**: an HEVC +//! quantization-matrix buffer submitted unconditionally (so a driver was handed a +//! matrix of zeros on every stream that disables scaling lists), and a +//! `NumMBsInBuffer` left at 0 where libavcodec's H.264 path writes +//! `mb_width * mb_height` — on the exact call (`SubmitDecoderBuffers`) that this +//! codebase has already seen an Intel driver reject a hand-built variant on. +//! +//! Neither is visible in the picture parameters, neither is visible in a smoke +//! test, and — before this module — neither was visible to any gate this program +//! runs, because the descriptors were built inside `cfg(windows)` code that no CI +//! leg compiles. That is the whole reason the values live here: a descriptor set +//! is a pure function of the conversion's output plus the packer's output, so it +//! can be asserted on any host, on every leg, over every AU of the vendored +//! vectors. +//! +//! # ⚠ The Windows layer still builds its own — rewire it +//! +//! `pf-client-core`'s `video_d3d11_native.rs` (`fill_and_submit` + its private +//! `buffer_desc`) constructs the same four descriptors itself. This module was +//! written to be the single source of truth for them, and that file should be +//! rewired to call [`descriptors_h264`] / [`descriptors_h265`] and translate the +//! result into `D3D11_VIDEO_DECODER_BUFFER_DESC` field for field. Until it is, +//! the two must be read together: this module is the SPEC and the tests are its +//! proof, and a divergence between them is a defect in the Windows file. The +//! ordering, the values and the presence rule below are exactly what that file +//! does today, transcribed — not a new invention. +//! +//! # The values, and where each comes from +//! +//! `CompressedBufferType` (D3D11's `BufferType`) code points, from windows-rs at +//! the workspace's pinned rev (`acb5a1a`, +//! `crates/libs/windows/src/Windows/Win32/d3d11/mod.rs`) — the same numbers +//! DXVA2's `DXVA2_*BufferType` enumeration uses: +//! +//! | buffer | code point | +//! |---|---| +//! | picture parameters | 0 | +//! | inverse quantization matrix | 4 | +//! | slice control | 5 | +//! | bitstream | 6 | +//! +//! **Order**: picture parameters, quantization matrices, bitstream, slice +//! control. libavcodec's `ff_dxva2_common_end_frame` fills its four-entry +//! descriptor array in exactly that order and submits the array as filled; a +//! driver is entitled to care, and matching the path every Windows player +//! exercises costs nothing. +//! +//! **`DataOffset`** is 0 on every buffer, for both sides: each buffer is written +//! from its own mapping's byte 0. (libavcodec `memset`s the descriptor and never +//! writes the field.) +//! +//! **`DataSize`** is the number of bytes actually written: the whole +//! hand-declared struct for the picture parameters and the quantization matrices, +//! the packer's PADDED size for the bitstream ([`crate::pack::Packed::data_size`], +//! a multiple of [`crate::dxva::BITSTREAM_ALIGN`]), and `slices * +//! size_of::()` for the slice control — **ten** bytes per +//! record, not twelve. That number is a measured fact rather than a derivation +//! (`dxva.rs`'s alignment section carries the measurement), and the slice-control +//! `DataSize` is where it is observable from outside: 20 bytes for a two-slice +//! H.264 picture, 10 for a one-segment HEVC one. +//! +//! **`NumMBsInBuffer` is codec-ASYMMETRIC, and that is not an accident to be +//! tidied up:** +//! +//! * H.264 — `mb_width * mb_height` on the BITSTREAM and SLICE_CONTROL +//! descriptors ([`crate::pic::DecodePlanDxva::mb_count`]); +//! * HEVC — 0 on the same two. HEVC has no macroblocks and the field has no CTB +//! spelling; +//! * picture parameters and quantization matrices — 0 in both codecs. +//! +//! That asymmetry is libavcodec's, read out of an **FFmpeg n8.1** tree: +//! `dxva2_h264.c:307` computes `const unsigned mb_count = h->mb_width * +//! h->mb_height` and writes it on the bitstream descriptor (`:412` D3D11, `:425` +//! DXVA2) and passes it for the slice-control commit (`:440-442`); +//! `dxva2_hevc.c` writes a literal 0 in the same three places (`:338`, `:349`, +//! `:359-361`); and `dxva2.c` passes a literal 0 for the two parameter buffers. +//! Setting a CTB count on the HEVC path would be a fresh divergence in the other +//! direction, which is why it is spelled out here rather than left to symmetry. +//! +//! # Presence: the quantization matrix is codec-asymmetric too +//! +//! * **H.264: always submitted.** `dxva2_h264.c:513-516` passes `&ctx_pic->qm` +//! with `sizeof(qm)` unconditionally, and the PPS's lists are always meaningful +//! (the vendored parser has already applied Table 7-2's fallback rules, so a PPS +//! that codes no matrix carries the SPS's or the flat default). +//! * **HEVC: submitted only when the sequence enables scaling lists.** +//! `dxva2_hevc.c:417` takes `int scale = ctx_pic->pp.dwCodingParamToolFlags & 1` +//! — bit 0 is `scaling_list_enabled_flag` — and `:423-426` passes `NULL`/0 when +//! it is clear; the generic layer then submits an IQ-matrix buffer only `if +//! (qm_size > 0)` (`dxva2.c` ~962), with `NumMBsInBuffer` 0. +//! [`crate::pic_h265::DecodePlanDxvaH265::qmatrix`] is `None` in exactly that +//! case, so presence here is `qmatrix.is_some()` and nothing else. Handing a +//! driver a matrix the picture parameters just told it to ignore is a bet on the +//! driver ignoring it too — and with the vendored parser leaving an uncoded list +//! all-zero, the losing side of that bet is every residual dequantizing to +//! nothing. +//! +//! ⚠ The flag test is NECESSARY but not SUFFICIENT. HEVC 7.4.5 says that with +//! `scaling_list_enabled_flag` set and NO scaling-list data in either parameter +//! set, the Table 7-5/7-6 DEFAULT lists apply. FFmpeg's parser seeds those +//! defaults; the vendored cros-codecs parser leaves an uncoded SPS's lists ALL +//! ZERO. So "submit iff the flag" is only half the rule, and the other half lives +//! in [`crate::pic_h265`]'s `quantization_matrices`, which reads the PPS's copy +//! (which that parser DOES default-fill) unless the SPS is the only side that +//! coded any. All three cases are named CPU tests — two in `pic_h265.rs` for the +//! contents, three in `tests/libav_picparams_parity.rs` for the submission fact. +//! +//! # Provenance +//! +//! The libavcodec file:line references above were read out of an FFmpeg n8.1 tree +//! by this work package's coordinator, not out of this repository — there is no +//! FFmpeg source in the worktree, so nothing here can verify them, and a capture is +//! the authority. The buffer ORDER is the one claim with no line reference: it is +//! what `video_d3d11_native.rs` already submits and what +//! `ff_dxva2_common_end_frame` fills its array in, and the harness's descriptor +//! comparison is what will confirm it. + +use std::mem::size_of; + +use crate::dxva::PicParamsH264; +use crate::dxva::PicParamsHevc; +use crate::dxva::QmatrixH264; +use crate::dxva::QmatrixHevc; +use crate::dxva::SliceH264Short; +use crate::dxva::SliceHevcShort; +use crate::pack::Packed; +use crate::pic::DecodePlanDxva; +use crate::pic_h265::DecodePlanDxvaH265; + +/// `D3D11_VIDEO_DECODER_BUFFER_PICTURE_PARAMETERS`. +pub const BUFFER_PICTURE_PARAMETERS: u32 = 0; +/// `D3D11_VIDEO_DECODER_BUFFER_INVERSE_QUANTIZATION_MATRIX`. +pub const BUFFER_INVERSE_QUANTIZATION_MATRIX: u32 = 4; +/// `D3D11_VIDEO_DECODER_BUFFER_SLICE_CONTROL`. +pub const BUFFER_SLICE_CONTROL: u32 = 5; +/// `D3D11_VIDEO_DECODER_BUFFER_BITSTREAM`. +pub const BUFFER_BITSTREAM: u32 = 6; + +/// One buffer of a submission, reduced to the fields a caller DECIDES. +/// +/// Deliberately not a `D3D11_VIDEO_DECODER_BUFFER_DESC`: that structure has +/// fourteen members, of which ten are either for a mode this backend does not use +/// (`BufferIndex`, `FirstMBaddress`, `Width`/`Height`/`Stride` — motion-compensation +/// buffers), or for protected content (`pIV`, `IVSize`, `PartialEncryption`, +/// `EncryptedBlockInfo`), or reserved. All ten are zero on every buffer this +/// backend submits, which the Windows layer expresses as `..Default::default()`; +/// the four here are the ones that carry a decision, and therefore the ones a +/// comparison against libavcodec is about. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BufferDescriptor { + /// `BufferType` — one of this module's `BUFFER_*` code points. (The DXVA + /// specs and libavcodec's DXVA2 path call the same field + /// `CompressedBufferType`.) + pub buffer_type: u32, + /// `DataOffset` — 0 on every buffer of every submission (module docs). + pub data_offset: u32, + /// `DataSize` — bytes written into the driver's mapping. + pub data_size: u32, + /// `NumMBsInBuffer` — codec-asymmetric; see the module docs. + pub num_mbs_in_buffer: u32, +} + +impl BufferDescriptor { + /// A descriptor with `DataOffset` 0, which is the only value this backend + /// ever submits. + const fn new(buffer_type: u32, data_size: u32, num_mbs_in_buffer: u32) -> BufferDescriptor { + BufferDescriptor { + buffer_type, + data_offset: 0, + data_size, + num_mbs_in_buffer, + } + } +} + +/// The slice-control buffer's `DataSize`: `n` short-format records back to back, +/// exactly as [`crate::dxva::slice_bytes`] lays them out. +/// +/// Saturating rather than panicking on the (unreachable) overflow: a `u32` holds +/// 429 million ten-byte records, and an AU that produced more has already been +/// refused by the packer. +fn slice_control_size(record_size: usize, records: usize) -> u32 { + u32::try_from(record_size.saturating_mul(records)).unwrap_or(u32::MAX) +} + +/// The descriptor set of one H.264 submission, in libavcodec's order. +/// +/// Four buffers, always: the quantization matrices travel on every H.264 picture +/// (module docs). +pub fn descriptors_h264(plan: &DecodePlanDxva, packed: &Packed) -> Vec { + let mb_count = plan.mb_count; + vec![ + BufferDescriptor::new( + BUFFER_PICTURE_PARAMETERS, + size_of::() as u32, + 0, + ), + BufferDescriptor::new( + BUFFER_INVERSE_QUANTIZATION_MATRIX, + size_of::() as u32, + 0, + ), + BufferDescriptor::new(BUFFER_BITSTREAM, packed.data_size, mb_count), + BufferDescriptor::new( + BUFFER_SLICE_CONTROL, + slice_control_size(size_of::(), packed.records.len()), + mb_count, + ), + ] +} + +/// The descriptor set of one HEVC submission, in libavcodec's order. +/// +/// THREE buffers when the sequence disables scaling lists (which is every +/// punktfunk HEVC stream and the vendored vector with it), four when it enables +/// them — and `NumMBsInBuffer` is 0 on all of them (module docs). +pub fn descriptors_h265(plan: &DecodePlanDxvaH265, packed: &Packed) -> Vec { + let mut out = Vec::with_capacity(4); + out.push(BufferDescriptor::new( + BUFFER_PICTURE_PARAMETERS, + size_of::() as u32, + 0, + )); + if plan.qmatrix.is_some() { + out.push(BufferDescriptor::new( + BUFFER_INVERSE_QUANTIZATION_MATRIX, + size_of::() as u32, + 0, + )); + } + out.push(BufferDescriptor::new(BUFFER_BITSTREAM, packed.data_size, 0)); + out.push(BufferDescriptor::new( + BUFFER_SLICE_CONTROL, + slice_control_size(size_of::(), packed.records.len()), + 0, + )); + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dxva::PicEntry; + use crate::pack::SliceRecord; + use crate::pic::DxvaRef; + + /// A conversion result with nothing in it but the two fields the descriptors + /// read. Built by hand rather than planned from a vector: this module's job is + /// the descriptor SET, and the whole-stream evidence (250 H.264 + 250 HEVC AUs + /// through the real planners) is in `tests/libav_picparams_parity.rs`. + fn h264_plan(mb_count: u32) -> DecodePlanDxva { + DecodePlanDxva { + pic_params: PicParamsH264::zeroed(), + qmatrix: QmatrixH264::zeroed(), + slice_ranges: Vec::new(), + setup_slot: 0, + setup_id: 1, + setup_is_reference: true, + refs: Vec::::new(), + mb_count, + } + } + + fn h265_plan(qmatrix: Option) -> DecodePlanDxvaH265 { + DecodePlanDxvaH265 { + pic_params: PicParamsHevc::zeroed(), + qmatrix, + slice_ranges: Vec::new(), + setup_slot: 0, + setup_id: 1, + setup_is_reference: true, + refs: Vec::new(), + } + } + + /// `n` slices packed into `data_size` bytes; the record contents do not matter + /// here, only how many there are. + fn packed(slices: usize, data_size: u32) -> Packed { + Packed { + records: (0..slices) + .map(|i| SliceRecord { + location: i as u32 * 64, + bytes: 64, + }) + .collect(), + data_size, + } + } + + #[test] + fn the_buffer_type_code_points_are_the_ones_windows_rs_declares() { + // From the workspace's pinned windows-rs rev (`acb5a1a`), + // `crates/libs/windows/src/Windows/Win32/d3d11/mod.rs`: + // D3D11_VIDEO_DECODER_BUFFER_PICTURE_PARAMETERS = 0, + // …_INVERSE_QUANTIZATION_MATRIX = 4, …_SLICE_CONTROL = 5, …_BITSTREAM = 6. + // Nothing else in this crate can catch a transposed pair, and a + // transposition would hand the driver a bitstream where it expects slice + // control. + assert_eq!(BUFFER_PICTURE_PARAMETERS, 0); + assert_eq!(BUFFER_INVERSE_QUANTIZATION_MATRIX, 4); + assert_eq!(BUFFER_SLICE_CONTROL, 5); + assert_eq!(BUFFER_BITSTREAM, 6); + } + + #[test] + fn an_h264_submission_carries_four_buffers_in_libavcodecs_order() { + let descs = descriptors_h264(&h264_plan(300), &packed(2, 512)); + assert_eq!( + descs.iter().map(|d| d.buffer_type).collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_INVERSE_QUANTIZATION_MATRIX, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ] + ); + assert_eq!(descs[0].data_size, 1040); + assert_eq!(descs[1].data_size, 224); + assert_eq!(descs[2].data_size, 512); + assert_eq!(descs[3].data_size, 2 * 10, "two ten-byte short records"); + } + + #[test] + fn only_the_h264_bitstream_and_slice_control_buffers_carry_a_macroblock_count() { + // Review 13's defect, in the smallest form that can express it: the field + // is 0 on the two parameter buffers and mb_width*mb_height on the two the + // hardware parses. + let descs = descriptors_h264(&h264_plan(300), &packed(1, 256)); + assert_eq!(descs[0].num_mbs_in_buffer, 0, "picture parameters"); + assert_eq!(descs[1].num_mbs_in_buffer, 0, "quantization matrices"); + assert_eq!(descs[2].num_mbs_in_buffer, 300, "bitstream"); + assert_eq!(descs[3].num_mbs_in_buffer, 300, "slice control"); + } + + #[test] + fn an_hevc_submission_omits_the_quantization_matrix_buffer_when_there_is_none() { + let descs = descriptors_h265(&h265_plan(None), &packed(1, 384)); + assert_eq!( + descs.iter().map(|d| d.buffer_type).collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ], + "a submission with no matrix must not carry an empty matrix buffer" + ); + assert!(descs + .iter() + .all(|d| d.buffer_type != BUFFER_INVERSE_QUANTIZATION_MATRIX)); + } + + #[test] + fn an_hevc_submission_carries_the_quantization_matrix_buffer_when_there_is_one() { + let descs = descriptors_h265(&h265_plan(Some(QmatrixHevc::zeroed())), &packed(3, 640)); + assert_eq!( + descs.iter().map(|d| d.buffer_type).collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_INVERSE_QUANTIZATION_MATRIX, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ] + ); + assert_eq!(descs[0].data_size, 232); + assert_eq!(descs[1].data_size, 1000); + assert_eq!(descs[2].data_size, 640); + assert_eq!(descs[3].data_size, 3 * 10, "three ten-byte short records"); + } + + #[test] + fn the_hevc_descriptors_carry_no_macroblock_count_at_all() { + // The asymmetry, asserted rather than assumed: libavcodec's HEVC path + // writes 0 where its H.264 path writes mb_width*mb_height, and a CTB count + // here would be a divergence in the other direction. + for descs in [ + descriptors_h265(&h265_plan(None), &packed(1, 256)), + descriptors_h265(&h265_plan(Some(QmatrixHevc::zeroed())), &packed(4, 1024)), + ] { + for desc in descs { + assert_eq!( + desc.num_mbs_in_buffer, 0, + "buffer type {} carries a macroblock count", + desc.buffer_type + ); + } + } + } + + #[test] + fn every_descriptor_starts_at_byte_zero_of_its_own_buffer() { + let h264 = descriptors_h264(&h264_plan(1), &packed(2, 256)); + let h265 = descriptors_h265(&h265_plan(Some(QmatrixHevc::zeroed())), &packed(2, 256)); + for desc in h264.into_iter().chain(h265) { + assert_eq!(desc.data_offset, 0); + } + } + + #[test] + fn the_slice_control_size_is_one_short_format_record_per_slice() { + // TEN bytes per record is the SHORT format, packed — measured against + // libavcodec on hardware, not derived from the field types (a `#[repr(C)]` + // `{u32, u32, u16}` would be twelve). The long format's record is an order of + // magnitude larger, so this size is also the check that the records match the + // `ConfigBitstreamRaw` this backend asks for. + assert_eq!(size_of::(), 10); + assert_eq!(size_of::(), 10); + for slices in [1usize, 2, 5, 68] { + let h264 = descriptors_h264(&h264_plan(1), &packed(slices, 4096)); + assert_eq!(h264[3].data_size, 10 * slices as u32); + let h265 = descriptors_h265(&h265_plan(None), &packed(slices, 4096)); + assert_eq!(h265[2].data_size, 10 * slices as u32); + } + } + + #[test] + fn a_reference_entry_in_the_plan_does_not_reach_the_descriptors() { + // A guard on the shape of this module rather than on a value: descriptors + // are a function of SIZES and the macroblock count, so nothing about the + // reference list may leak into them. (Also keeps `DxvaRef` in the test's + // vocabulary, so the plan built above stays a realistic one.) + let mut plan = h264_plan(300); + plan.refs.push(DxvaRef { + slot: 2, + id: 7, + is_long_term: true, + top_field_order_cnt: 4, + bottom_field_order_cnt: 4, + frame_num_or_lt_idx: 1, + }); + plan.pic_params.RefFrameList[0] = PicEntry::new(2, true); + assert_eq!( + descriptors_h264(&plan, &packed(1, 256)), + descriptors_h264(&h264_plan(300), &packed(1, 256)) + ); + } +} diff --git a/crates/pf-dxvadec/src/dxva.rs b/crates/pf-dxvadec/src/dxva.rs index 4de7789c..82b5ba61 100644 --- a/crates/pf-dxvadec/src/dxva.rs +++ b/crates/pf-dxvadec/src/dxva.rs @@ -41,12 +41,37 @@ //! The one unsafe in the crate is [`as_bytes`], and it is fenced behind a //! sealed trait that only these `#[repr(C)]` PODs implement. //! -//! # Alignment +//! # Alignment — and the one place natural alignment is WRONG //! -//! Every member is 1/2/4 bytes, so the natural alignment of all of these is 4 and -//! no member ever needs the x86-64 8-byte rules. `#[repr(C)]` reproduces MSVC's -//! default (`/Zp8`) packing exactly for that shape; the offset assertions are what -//! proves it rather than assumes it. +//! `dxva.h` declares these as wire-format structures under **1-byte packing**, not +//! under MSVC's default. For five of the six that is indistinguishable from natural +//! alignment, because every member happens to sit at a naturally-aligned offset and +//! every total is already a multiple of 4: `DXVA_PicParams_H264` is 1040, +//! `DXVA_PicParams_HEVC` 232, the two quantization matrices 224 and 1000 — all +//! confirmed against libavcodec's runtime `sizeof` in the n8.1 capture described in +//! `tests/libav_picparams_parity.rs`. +//! +//! The slice-control records are the exception and the reason this section exists. +//! `{UINT, UINT, USHORT}` is **10 bytes packed and 12 under natural alignment**, and +//! an earlier revision of this file declared them plain `#[repr(C)]` — asserting 12 +//! with "2 bytes tail padding" in the comment, which was a guess dressed as a proof. +//! Measured on hardware (RTX 4090, patched FFmpeg n8.1, both vendored vectors, 250 +//! AUs each): the H.264 slice-control buffer's `DataSize` is 20 on a stream with two +//! slices per picture, and the HEVC one's is 10 on a stream with one slice segment +//! per picture. Two codecs, two slice counts, one answer — 10. +//! +//! What the mistake costs, so it is never re-introduced: record 0's fields land at +//! 0/4/8 either way, so a SINGLE-slice stream decodes correctly and the two extra +//! bytes are trailing slop nobody reads. From the second record on, every field is +//! displaced by two bytes per preceding record, so the driver reads a slice offset +//! built from half of one field and half of the next. punktfunk hosts do emit +//! multi-slice streams. +//! +//! Hence: **the packed structs carry `#[repr(C, packed)]`** and the proofs below +//! pin `align_of` as well as `size_of`, plus — for every struct — that its size is +//! exactly its last member's offset plus that member's size, which is the assertion +//! that would have caught this one. Interior padding was already impossible (the +//! per-field offset asserts see it); it was TAIL padding that got in. //! //! Sources: the DXVA specifications "DirectX Video Acceleration Specification for //! H.264/AVC Decoding" (§4.2 `DXVA_PicParams_H264`, §4.4 `DXVA_Qmatrix_H264`, §4.6 @@ -62,6 +87,7 @@ // every generated Win32 struct. #![allow(non_snake_case)] +use std::mem::align_of; use std::mem::offset_of; use std::mem::size_of; @@ -388,16 +414,21 @@ impl QmatrixH264 { /// UINT BSNALunitDataLocation; /* 0 */ /// UINT SliceBytesInBuffer; /* 4 */ /// USHORT wBadSliceChopping; /* 8 */ -/// } DXVA_Slice_H264_Short; /* 12 bytes (2 bytes tail padding) */ +/// } DXVA_Slice_H264_Short; /* 10 bytes — PACKED, no tail padding */ /// ``` /// +/// **Ten bytes, not twelve** — `#[repr(C, packed)]`, and the single most important +/// number in this file after the picture-parameters offsets. See the module docs' +/// alignment section for the hardware measurement it comes from and for what +/// getting it wrong does to every record after the first. +/// /// Short format only. The long format (`DXVA_Slice_H264_Long`) additionally /// carries the derived reference lists and the prediction weight tables, which /// this backend does not build — a device offering only long-format configs is /// refused at decoder creation and the ladder answers with the FFmpeg rung, which /// does implement both. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -#[repr(C)] +#[repr(C, packed)] pub struct SliceH264Short { /// Byte offset of the slice's **start code** within the bitstream buffer. pub BSNALunitDataLocation: u32, @@ -787,10 +818,14 @@ impl QmatrixHevc { /// UINT BSNALunitDataLocation; /* 0 */ /// UINT SliceBytesInBuffer; /* 4 */ /// USHORT wBadSliceChopping; /* 8 */ -/// } DXVA_Slice_HEVC_Short; /* 12 bytes */ +/// } DXVA_Slice_HEVC_Short; /* 10 bytes — PACKED, no tail padding */ /// ``` +/// +/// Ten bytes for the same reason as [`SliceH264Short`], and measured independently: +/// the HEVC capture's slice-control `DataSize` is 10 on a vector with exactly one +/// slice segment per picture. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -#[repr(C)] +#[repr(C, packed)] pub struct SliceHevcShort { pub BSNALunitDataLocation: u32, pub SliceBytesInBuffer: u32, @@ -805,6 +840,18 @@ pub struct SliceHevcShort { // reproduced above. This is the whole defence against a silently mis-declared // buffer: nothing else in the pipeline can tell a wrong offset from a right one, // because the driver accepts either and only the picture differs. +// +// Three kinds of assertion, and the third is new because the first two missed a +// real defect (the slice records' 12-vs-10; see the module docs): +// +// 1. `size_of` per struct, against the C total. +// 2. `offset_of` per FIELD, which is what makes interior padding or a reordering +// impossible. +// 3. **size == last field's offset + last field's own size**, per struct — the +// assertion that catches TAIL padding, which is exactly what (1) and (2) cannot +// see: a struct whose declared total is itself wrong satisfies both. Every one of +// these buffers is a wire format with no padding anywhere, and this is where that +// is stated as a proof rather than a comment. const _: () = { assert!(size_of::() == 1); @@ -852,7 +899,8 @@ const _: () = { assert!(offset_of!(QmatrixH264, bScalingLists4x4) == 0); assert!(offset_of!(QmatrixH264, bScalingLists8x8) == 96); - assert!(size_of::() == 12); + assert!(size_of::() == 10); + assert!(align_of::() == 1); assert!(offset_of!(SliceH264Short, BSNALunitDataLocation) == 0); assert!(offset_of!(SliceH264Short, SliceBytesInBuffer) == 4); assert!(offset_of!(SliceH264Short, wBadSliceChopping) == 8); @@ -908,10 +956,25 @@ const _: () = { assert!(offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID2) == 992); assert!(offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID3) == 998); - assert!(size_of::() == 12); + assert!(size_of::() == 10); + assert!(align_of::() == 1); assert!(offset_of!(SliceHevcShort, BSNALunitDataLocation) == 0); assert!(offset_of!(SliceHevcShort, SliceBytesInBuffer) == 4); assert!(offset_of!(SliceHevcShort, wBadSliceChopping) == 8); + + // NO TAIL PADDING, per struct: the size is the last member's offset plus the + // last member's own size, nothing more. The right-hand sizes are the C + // declarations' (`UCHAR SliceGroupMap[810]`, `UINT StatusReportFeedbackNumber`, + // …), so each line is an independent statement of the total rather than a + // restatement of `size_of`. + assert!(size_of::() == offset_of!(PicParamsH264, SliceGroupMap) + 810); + assert!(size_of::() == offset_of!(QmatrixH264, bScalingLists8x8) + 2 * 64); + assert!(size_of::() == offset_of!(SliceH264Short, wBadSliceChopping) + 2); + assert!( + size_of::() == offset_of!(PicParamsHevc, StatusReportFeedbackNumber) + 4 + ); + assert!(size_of::() == offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID3) + 2); + assert!(size_of::() == offset_of!(SliceHevcShort, wBadSliceChopping) + 2); }; // --------------------------------------------------------------------------- @@ -1164,11 +1227,18 @@ mod tests { // reader who distrusts a `const _` finds the same claim executable. assert_eq!(size_of::(), 1040); assert_eq!(size_of::(), 224); - assert_eq!(size_of::(), 12); assert_eq!(size_of::(), 232); assert_eq!(size_of::(), 1000); - assert_eq!(size_of::(), 12); assert_eq!(size_of::(), 1); + // TEN, not twelve. Measured against libavcodec on hardware: the H.264 + // slice-control buffer is 20 bytes for a two-slice picture and the HEVC one + // 10 bytes for a one-slice-segment picture (module docs). A `#[repr(C)]` + // `{u32, u32, u16}` is 12, and every record after the first would then be + // displaced by two bytes per preceding record. + assert_eq!(size_of::(), 10); + assert_eq!(size_of::(), 10); + assert_eq!(align_of::(), 1); + assert_eq!(align_of::(), 1); } #[test] @@ -1207,10 +1277,16 @@ mod tests { }, ]; let bytes = slice_bytes(&records); - assert_eq!(bytes.len(), 24); + // TEN bytes per record, so the second record starts at byte 10 — this is the + // test that fails if the packing is ever relaxed back to natural alignment, + // and it fails on the SECOND record, which is exactly where the driver + // would have started misreading. + assert_eq!(bytes.len(), 20); assert_eq!(&bytes[0..4], &0u32.to_le_bytes()); assert_eq!(&bytes[4..8], &100u32.to_le_bytes()); - assert_eq!(&bytes[12..16], &100u32.to_le_bytes()); - assert_eq!(&bytes[16..20], &250u32.to_le_bytes()); + assert_eq!(&bytes[8..10], &0u16.to_le_bytes()); + assert_eq!(&bytes[10..14], &100u32.to_le_bytes()); + assert_eq!(&bytes[14..18], &250u32.to_le_bytes()); + assert_eq!(&bytes[18..20], &0u16.to_le_bytes()); } } diff --git a/crates/pf-dxvadec/src/lib.rs b/crates/pf-dxvadec/src/lib.rs index 20e7a4f1..9fafeaca 100644 --- a/crates/pf-dxvadec/src/lib.rs +++ b/crates/pf-dxvadec/src/lib.rs @@ -22,6 +22,10 @@ //! - [`pic`] / [`pic_h265`]: one [`pf_bitstream`] `AuPlan` into //! `DXVA_PicParams_*`, `DXVA_Qmatrix_*` and the slice-control records, with the //! reference lists resolved through a DPB slot map. +//! - [`descriptors`]: which buffers one `SubmitDecoderBuffers` call carries and +//! the four `D3D11_VIDEO_DECODER_BUFFER_DESC` fields that are a decision — +//! where two of review 13's three structural defects lived, and the reason +//! they are now a CPU test rather than a Windows-only code path. //! //! # Why the slot map comes from pf-vkdecode //! @@ -46,6 +50,7 @@ #![deny(clippy::undocumented_unsafe_blocks)] pub mod config; +pub mod descriptors; pub mod dxva; pub mod pack; pub mod pic; @@ -95,6 +100,13 @@ pub use config::DXGI_FORMAT_P010; pub use config::H264_VLD_NOFGT; pub use config::HEVC_VLD_MAIN; pub use config::HEVC_VLD_MAIN10; +pub use descriptors::descriptors_h264; +pub use descriptors::descriptors_h265; +pub use descriptors::BufferDescriptor; +pub use descriptors::BUFFER_BITSTREAM; +pub use descriptors::BUFFER_INVERSE_QUANTIZATION_MATRIX; +pub use descriptors::BUFFER_PICTURE_PARAMETERS; +pub use descriptors::BUFFER_SLICE_CONTROL; pub use dxva::as_bytes; pub use dxva::slice_bytes; pub use dxva::PicParamsH264; diff --git a/crates/pf-dxvadec/src/pic.rs b/crates/pf-dxvadec/src/pic.rs index a2c641c2..6546c0cd 100644 --- a/crates/pf-dxvadec/src/pic.rs +++ b/crates/pf-dxvadec/src/pic.rs @@ -1084,11 +1084,21 @@ mod tests { ]; let control = slice_control(&records); assert_eq!(control.len(), 2); - assert_eq!(control[0].BSNALunitDataLocation, 0); - assert_eq!(control[0].SliceBytesInBuffer, 40); - assert_eq!(control[1].BSNALunitDataLocation, 40); - assert_eq!(control[1].SliceBytesInBuffer, 216); - assert!(control.iter().all(|c| c.wBadSliceChopping == 0)); + // Read by VALUE, in braces: `DXVA_Slice_H264_Short` is `#[repr(C, packed)]` + // (ten bytes, see `dxva.rs`'s alignment section), so a reference to one of + // its `u32` members would be unaligned and is a compile error — `assert_eq!` + // takes references to its operands. + assert_eq!({ control[0].BSNALunitDataLocation }, 0); + assert_eq!({ control[0].SliceBytesInBuffer }, 40); + assert_eq!({ control[1].BSNALunitDataLocation }, 40); + assert_eq!({ control[1].SliceBytesInBuffer }, 216); + assert!(control.iter().all(|c| { c.wBadSliceChopping } == 0)); + // …and the records reach the driver ten bytes apart, which is the fact the + // whole submission depends on: the second record's location is at byte 10, + // not 12. + let bytes = crate::dxva::slice_bytes(&control); + assert_eq!(bytes.len(), 20); + assert_eq!(&bytes[10..14], &40u32.to_le_bytes()); } #[test] diff --git a/crates/pf-dxvadec/src/pic_h265.rs b/crates/pf-dxvadec/src/pic_h265.rs index 16889be3..dbb32293 100644 --- a/crates/pf-dxvadec/src/pic_h265.rs +++ b/crates/pf-dxvadec/src/pic_h265.rs @@ -1298,14 +1298,31 @@ mod tests { #[test] fn slice_control_records_carry_the_packers_locations_verbatim() { - let records = [crate::pack::SliceRecord { - location: 0, - bytes: 128, - }]; + let records = [ + crate::pack::SliceRecord { + location: 0, + bytes: 128, + }, + crate::pack::SliceRecord { + location: 128, + bytes: 256, + }, + ]; let control = slice_control_h265(&records); - assert_eq!(control[0].BSNALunitDataLocation, 0); - assert_eq!(control[0].SliceBytesInBuffer, 128); - assert_eq!(control[0].wBadSliceChopping, 0); + // Read by VALUE, in braces: the record is `#[repr(C, packed)]` (ten bytes), + // so a reference to a `u32` member would be unaligned — and `assert_eq!` + // takes references. See `dxva.rs`'s alignment section. + assert_eq!({ control[0].BSNALunitDataLocation }, 0); + assert_eq!({ control[0].SliceBytesInBuffer }, 128); + assert_eq!({ control[0].wBadSliceChopping }, 0); + // A SECOND record, because the ten-vs-twelve byte defect is invisible on a + // single-record buffer — the vendored HEVC vector is one slice segment per + // picture, which is precisely the shape that hid it. + assert_eq!({ control[1].BSNALunitDataLocation }, 128); + let bytes = crate::dxva::slice_bytes(&control); + assert_eq!(bytes.len(), 20); + assert_eq!(&bytes[10..14], &128u32.to_le_bytes()); + assert_eq!(&bytes[14..18], &256u32.to_le_bytes()); } #[test] diff --git a/crates/pf-dxvadec/tests/libav_picparams_parity.rs b/crates/pf-dxvadec/tests/libav_picparams_parity.rs index 4aa382a5..f5b770e9 100644 --- a/crates/pf-dxvadec/tests/libav_picparams_parity.rs +++ b/crates/pf-dxvadec/tests/libav_picparams_parity.rs @@ -1,5 +1,11 @@ -//! The byte-diff harness: pf-dxvadec's `DXVA_PicParams_H264` against libavcodec's, for the -//! same access units of the same elementary stream. +//! 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 //! @@ -9,106 +15,377 @@ //! 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 -//! `memcmp` against the path every Windows player exercises. +//! comparison against the path every Windows player exercises. //! -//! This harness is the cheap version of that `memcmp`. It is `#[ignore]`d because it needs a -//! capture the repository cannot carry: libavcodec's own picture parameters, produced by a -//! patched FFmpeg on a Windows box with a D3D11VA-capable GPU. +//! 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 //! -//! On the Windows box (192.168.1.173 — see the box notes for the ssh identity), with an -//! FFmpeg source tree: +//! 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. Patch `libavcodec/dxva2_h264.c`. At the very END of `fill_picture_parameters` — after -//! the `RefFrameList` loop and the `UsedForReferenceFlags` writes, so every field is -//! final — add: +//! **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 au_index; -//! const uint8_t *raw = (const uint8_t *)pp; -//! char line[2 * sizeof(*pp) + 32]; -//! unsigned i; -//! for (i = 0; i < sizeof(*pp); i++) -//! snprintf(line + 2 * i, 3, "%02x", raw[i]); -//! av_log(avctx, AV_LOG_INFO, "PFPP h264 %u %s\n", au_index++, line); -//! } -//! ``` +//! ```c +//! static unsigned pf_au_index; +//! ``` //! -//! The identical block goes at the end of `dxva2_hevc.c`'s `fill_picture_parameters`, with -//! `h264` replaced by `hevc`, once this harness grows the HEVC half. +//! 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: //! -//! 2. Build FFmpeg (`--enable-d3d11va` is on by default on Windows) and decode the SAME -//! elementary stream this test plans — the vendored vector, which is in the repository at -//! `crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264`: +//! ```c +//! const unsigned pf_au = pf_au_index++; /* first line of the function body */ +//! ``` //! -//! ```text -//! ffmpeg -hwaccel d3d11va -hwaccel_output_format d3d11 \ -//! -i test-25fps.h264 -f null - 2> capture.log -//! ``` +//! (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`. //! -//! A software fallback produces no lines at all, which is the check that the hwaccel -//! actually engaged: 250 `PFPP h264` lines or the capture is void. +//! **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: //! -//! 3. `grep '^PFPP h264 ' capture.log > libav-h264.picparams`, copy it back, and run: +//! ```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); +//! ``` //! -//! ```text -//! PF_LIBAV_PICPARAMS=libav-h264.picparams cargo test -p pf-dxvadec --test \ -//! libav_picparams_parity -- --ignored --nocapture -//! ``` +//! ⚠ **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: //! -//! `PF_DXVA_DUMP=` writes THIS side in the same format without needing a capture, so the -//! two files can also be diffed by hand. +//! ```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. +//! +//! **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 \ +//! cargo test -p pf-dxvadec --test libav_picparams_parity -- --ignored --nocapture +//! ``` +//! +//! `PF_DXVA_DUMP=` 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` will differ in three places by design. The harness reports offsets rather -//! than a verdict for exactly this reason — read the offsets against this list: +//! 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 `RefFrameList` entry carry a decode-surface -//! index. libavcodec's comes from its own frame pool's allocation order; ours comes from +//! * **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. A real comparison must build the mapping from the first AU that names a picture -//! and then check it holds; a differing index alone proves nothing. -//! * **`RefFrameList` ORDER.** 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). Both are correct — DXVA imposes no order — so the array -//! must be compared as a SET of (marking, `FrameNumList`, `FieldOrderCntList`, used-flags) -//! tuples, with `UsedForReferenceFlags` re-indexed to the compared order. -//! * **Padding.** `DXVA_PicParams_H264` has no interior padding by construction (dxva.rs -//! proves the offsets at compile time), but the `Reserved*` fields are only defined where -//! this crate writes them deliberately; libavcodec zeroes the struct once at the top of the -//! frame and writes a subset. Any difference in a reserved field is a REAL finding for this -//! crate, not an expected divergence — that is what `Reserved16Bits = 3` is about. +//! 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`. //! -//! Everything else — every parameter-set field, every flag word, `frame_num`, -//! `CurrFieldOrderCnt`, `ContinuationFlag`, `StatusReportFeedbackNumber` (both count from 1 -//! per picture) — must match byte for byte, and a difference there is the finding this harness -//! exists to produce. +//! 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. //! -//! # What is not covered yet +//! 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. //! -//! The HEVC half (`DXVA_PicParams_HEVC` + `DXVA_Qmatrix_HEVC`), and the buffer DESCRIPTORS — -//! `NumMBsInBuffer`, `DataSize`, and whether the quantization-matrix buffer is submitted at -//! all. The descriptors are where two of review 13's three structural defects lived, and they -//! are not in `pp`: capturing them needs the same treatment applied to -//! `commit_bitstream_and_slice_buffer` and `ff_dxva2_commit_buffer`. +//! # 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 cros_codecs::codec::h264::parser::Nalu; -use cros_codecs::codec::h264::parser::NaluType; -use pf_dxvadec::{plan_to_dxva, AuPlan, H264Planner, SlotMap}; +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::AuPlan; +use pf_dxvadec::BufferDescriptor; +use pf_dxvadec::Codec; +use pf_dxvadec::H264Planner; +use pf_dxvadec::H265Planner; +use pf_dxvadec::SliceRecord; +use pf_dxvadec::SlotMap; -const TEST_25FPS: &[u8] = include_bytes!( +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 same AU splitter every 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. +/// 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; @@ -131,125 +408,2773 @@ fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> { aus } -/// This crate's picture parameters for the vendored vector, one entry per planned AU, as the -/// bytes a `SubmitDecoderBuffers` call would carry. -fn our_picparams() -> Vec<(usize, Vec)> { +/// 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, + /// 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>, + /// The descriptor set, in submission order. + descriptors: Vec, + /// The packer's slice records, for the internal-consistency checks. + records: Vec, + /// 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 { let mut planner = H264Planner::new(); let mut slots: Option = None; + let mut mapping = vec![0u8; MAPPING_BYTES]; let mut out = Vec::new(); - for (i, au) in split_into_aus(TEST_25FPS).into_iter().enumerate() { - let plan: AuPlan = match planner.plan_au(au) { - Ok(plan) => plan, - Err(_) => continue, - }; + 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); } - let dxva = plan_to_dxva(&plan, map, out.len() as u32 + 1).expect("conversion"); - out.push((i, pf_dxvadec::as_bytes(&dxva.pic_params).to_vec())); + // `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}")); + 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, + 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 { + let mut planner = H265Planner::new(); + let mut slots: Option = 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, + unpadded, + mb_count: 0, + }); + } + assert_eq!(out.len(), VENDORED_AUS); + 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. +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, +); + +/// 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)> { + 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, + documented: BTreeMap, +} + +impl Findings { + fn note(&mut self, field: impl Into, au: usize, detail: impl Into) { + 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, au: usize, reason: impl Into) { + 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, +} + +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() +} + +/// 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, +} + +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 = BTreeMap::new(); + let mut theirs_seen: BTreeMap = 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>, + /// `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>>, + descriptors: BTreeMap>, + config_bitstream_raw: BTreeMap, + /// 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, +} + +/// `hex` → bytes, or `None` when it is not an even-length hex string. +fn from_hex(hex: &str) -> Option> { + 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 } -/// `PFPP h264 ` lines → index → bytes. -fn parse_capture(text: &str) -> BTreeMap> { - let mut out = BTreeMap::new(); - for line in text.lines() { - let rest = match line.trim().strip_prefix("PFPP h264 ") { - Some(rest) => rest, - None => continue, - }; - let mut parts = rest.split_whitespace(); - let (Some(index), Some(hex)) = (parts.next(), parts.next()) else { +/// 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 index: usize = index.parse().expect("a decimal AU index"); - assert!(hex.len() % 2 == 0, "AU {index}: odd hex length"); - let bytes = (0..hex.len() / 2) - .map(|i| u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).expect("hex")) - .collect(); - out.insert(index, bytes); + 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 ` …`, 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::() 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::(), + size.parse::(), + mbs.parse::(), + offset.parse::(), + ) { + (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::() { + Ok(raw) => { + out.config_bitstream_raw.insert(au, raw); + true + } + Err(_) => false, + }, + _ => false, + }; + if !ok { + out.unreadable.push(line.to_string()); + } } out } -/// Emit this crate's side in the capture's own format — usable without a capture at all, so -/// the two files can be diffed by any tool. +/// Read the capture named by `var`, or `None` when it is unset. +fn capture_from_env(var: &str, codec: &str) -> Option { + 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) { + 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 = (0..ours).collect(); + let seen: BTreeSet = 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. + let want = pf_dxvadec::short_slice_config(match codec { + "h264" => Codec::H264, + _ => Codec::H265, + }); + 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)], + 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!(""), + 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 = ours.iter().map(|&(_, e)| e).collect(); + let mut theirs_sorted: Vec = 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 = 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::()); + 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( + "", + au, + "the capture holds no PFPP line for this AU", + ); + continue; + }; + if theirs.len() != sub.pic_params.len() { + findings.note( + "", + 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> { + 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 HEVC picture-parameter comparison. +fn compare_hevc_picparams(ours: &[OurSubmission], capture: &Capture) -> Findings { + let ranges = field_ranges(HEVC_FIELDS, size_of::()); + 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( + "", + au, + "the capture holds no PFPP line for this AU", + ); + continue; + }; + if theirs.len() != sub.pic_params.len() { + findings.note( + "", + 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( + "", + 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( + "", + 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( + "", + 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( + "", + 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", + _ => "", + } +} + +/// 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( + "", + au, + "the capture holds no PFBD lines for this AU", + ); + continue; + }; + let our_types: Vec = sub.descriptors.iter().map(|d| d.buffer_type).collect(); + let their_types: Vec = 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::>(), + their_types + .iter() + .map(|&t| buffer_name(t)) + .collect::>() + ) + }; + findings.note("", 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::()); + 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::()), + ("PicParamsHevc", HEVC_FIELDS, size_of::()), + ("QmatrixH264", H264_QMATRIX_FIELDS, size_of::()), + ("QmatrixHevc", HEVC_QMATRIX_FIELDS, size_of::()), + ( + "SliceH264Short", + H264_SLICE_FIELDS, + size_of::(), + ), + ( + "SliceHevcShort", + HEVC_SLICE_FIELDS, + size_of::(), + ), + ] { + 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![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_INVERSE_QUANTIZATION_MATRIX, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ], + "AU {au}" + ); + } +} + +#[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::() 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![ + 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::(), 10); + assert_eq!(size_of::(), 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::() - 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::()), + ("hevc", our_hevc_submissions(), size_of::()), + ] { + 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, pps_coded: Option) -> 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, + 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::()); + 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::()); + 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![ + 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::()); + 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::(), + ), + ] { + 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::(), + ), + ] { + 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)); +} + +/// 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 = (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::()], + qmatrix: None, + 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 ` 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)) -> 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 = 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(), + 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!(""); + 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> = 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::()]); + subs[3].descriptors = vec![ + BufferDescriptor { + buffer_type: BUFFER_PICTURE_PARAMETERS, + data_offset: 0, + data_size: size_of::() as u32, + num_mbs_in_buffer: 0, + }, + BufferDescriptor { + buffer_type: BUFFER_INVERSE_QUANTIZATION_MATRIX, + data_offset: 0, + data_size: size_of::() 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::(), + ); + assert_eq!(findings.fields(), vec![""]); + assert_eq!(findings.by_field[""].first_au, 3); + let findings = compare_descriptors(&subs, &capture); + assert_eq!(findings.fields(), vec![""]); +} + +#[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![""]); + assert!( + findings.by_field[""] + .detail + .contains("commit_bitstream_and_slice_buffer"), + "the report must name the patch site, got {:?}", + findings.by_field[""].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 { + 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 = (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 { + 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 = "writes a dump: PF_DXVA_DUMP="] -fn dump_our_h264_picture_parameters() { +fn dump_our_submission_in_the_captures_own_format() { let path = std::env::var("PF_DXVA_DUMP").expect("PF_DXVA_DUMP= names the output file"); - let mut text = String::new(); - for (index, (_, bytes)) in our_picparams().into_iter().enumerate() { - text.push_str(&format!("PFPP h264 {index} ")); - for b in bytes { - text.push_str(&format!("{b:02x}")); - } - text.push('\n'); - } + let mut text = dump("h264", &our_h264_submissions()); + text.push_str(&dump("hevc", &our_hevc_submissions())); std::fs::write(&path, text).expect("write the dump"); println!("wrote {path}"); } -/// Diff this crate's picture parameters against a libavcodec capture, reporting the offsets -/// that differ rather than a pass/fail — the module docs list the three divergences that are -/// expected, and an offset outside them is the finding. #[test] -#[ignore = "needs a libavcodec capture: PF_LIBAV_PICPARAMS= (see the module docs)"] +#[ignore = "needs a libavcodec capture: PF_LIBAV_CAPTURE_H264= (see the module docs)"] fn our_h264_picture_parameters_match_libavcodecs() { - let path = std::env::var("PF_LIBAV_PICPARAMS") - .expect("PF_LIBAV_PICPARAMS= names a capture (see the module docs)"); - let capture = parse_capture(&std::fs::read_to_string(&path).expect("read the capture")); - let ours = our_picparams(); - assert!(!capture.is_empty(), "{path} holds no PFPP h264 lines"); - assert_eq!( - capture.len(), + let capture = capture_from_env("PF_LIBAV_CAPTURE_H264", "h264") + .expect("PF_LIBAV_CAPTURE_H264= names a capture (see the module docs)"); + let ours = our_h264_submissions(); + preflight( + &capture, ours.len(), - "the capture covers {} AUs and this crate plans {} — the two sides must decode the \ - same elementary stream, split the same way", - capture.len(), - ours.len() - ); - - // Offsets that differ, and on how many AUs: a field that differs on every picture is a - // systematic divergence (read it against the module docs' list), while one that differs - // on a handful is a case-specific defect. - let mut differing: BTreeMap = BTreeMap::new(); - let mut first_example: BTreeMap = BTreeMap::new(); - for ((index, ours), (_, theirs)) in ours.iter().zip(capture.values().enumerate()) { - assert_eq!( - ours.len(), - theirs.len(), - "AU {index}: 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(), - ours.len() - ); - for (offset, (a, b)) in ours.iter().zip(theirs).enumerate() { - if a != b { - *differing.entry(offset).or_default() += 1; - first_example.entry(offset).or_insert((*index, *a, *b)); - } - } - } - - if differing.is_empty() { - println!("{} AUs, byte-identical to libavcodec", ours.len()); - return; - } - println!( - "{} differing byte offsets over {} AUs:", - differing.len(), - ours.len() - ); - for (offset, count) in &differing { - let (au, ours, theirs) = first_example[offset]; - println!(" offset {offset:#06x}: {count} AUs, first at AU {au} (ours {ours:#04x}, libav {theirs:#04x})"); - } - panic!( - "picture parameters diverge at {} offsets — check each against the module docs' list \ - of expected divergences before treating it as a defect", - differing.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= (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= 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= and/or PF_LIBAV_CAPTURE_HEVC= 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= and/or PF_LIBAV_CAPTURE_HEVC= 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::(), + ) + .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::(), + ) + .verdict("HEVC quantization matrices", ours.len()); + } }