From d3e000768d524aa8f0043605560d3c19a8207dff Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 6 Aug 2026 13:04:07 +0200 Subject: [PATCH] =?UTF-8?q?feat(client):=20M6=20begins=20=E2=80=94=20the?= =?UTF-8?q?=20libva=20layouts,=20measured=20rather=20than=20transcribed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The VAAPI rung's crate, in the shape the other two native rungs established: everything that can be a pure decision or a pure conversion lives in a cross-platform crate the ordinary gates run, and only the parts that genuinely need a device stay behind a platform cfg. This lands the first half of that — the buffer layouts and the decoder-creation decisions — with the conversion to follow. Route: minimal FFI rather than cros-libva. The plan of record permits either ("cros-libva (or minimal FFI)"), and hand-declaring keeps the crate building and testing on macOS and in the Linux container, which is the property that made pf-dxvadec's defects findable on a laptop instead of on a box. The layouts are not eyeballed. A C probe compiled against real libva 2.23.0 headers printed sizeof/alignof/offsetof for every field and set individual bit-fields to read the resulting word back; those numbers are pinned as const assertions, so a transcription slip is a compile error rather than a driver reading the wrong byte. The probe is committed beside them, with the command that runs it, because evidence that cannot be re-run is a claim. What the probe settled that a reader would otherwise get wrong: VAPictureH264 is 36 bytes and is embedded 81 times across the two buffers, so its size is load-bearing for every later offset; the three DEPRECATED FMO fields still occupy bytes 624..628, and dropping them would shift everything after; and C bit-fields allocate from the least significant bit on this ABI — proven, since that is ABI-defined rather than standardised. Groundwork for the conversion, established here so the next work package starts from facts: slice_data_bit_offset needs no new parsing. VAAPI is the only backend that wants a bit position — DXVA takes a byte offset, Vulkan takes none — and the vendored parser already records exactly it as SliceHeader::header_bit_size, computed as (nalu.size - epb) * 8 - bits_left: from and including the NAL header byte, emulation-prevention bytes removed. That is the field's definition verbatim, and it is there because cros-codecs' own production backend is VAAPI. The slice data buffer starts at the NAL header byte, so the start code is skipped — SlicePlan::data is start-code-inclusive and the prefix is three OR four bytes, the host emitting four on 100% of access units. reference_frames is the marked DPB, the same statement DXVA's RefFrameList makes, so it comes from the dpb_refs snapshot; Vulkan's pReferenceSlots is the opposite and takes the access unit's own set. All three conventions now have a written home, which is the distinction that cost M5 a defect. Unlike DXVA short-format, VAAPI wants the per-slice reference lists and the full prediction weight tables inline — hence the 3128-byte slice record. One wrinkle recorded rather than left to be discovered: the vendored PredWeightTable stores luma_offset_l0 as [i8; 32] but luma_offset_l1 as [i16; 32], and libva wants i16 for both. Profile selection resolves H.264 to High for every 8-bit 4:2:0 stream instead of reading profile_idc, because High is a superset for the tools our hosts emit and picking Main for a stream that turns out to use 8x8 transforms is a mid-stream failure where picking High is not. 4:4:4 and 10-bit H.264 are refused rather than narrowed to an 8-bit profile — that class of silent narrowing decodes to garbage instead of failing. 11 tests: the probe's bit patterns, a disjointness check per bit-field word (two probe vectors alone would not catch a shift typo that overlapped two fields), and the envelope refusals. Gates: rustfmt, clippy, cargo doc with no unresolved links, and the Linux container's clippy -D warnings, tests and workspace check. --- Cargo.lock | 8 + Cargo.toml | 1 + crates/pf-vaadec/Cargo.toml | 18 + crates/pf-vaadec/layout-probe.c | 115 +++++++ crates/pf-vaadec/src/config.rs | 174 ++++++++++ crates/pf-vaadec/src/lib.rs | 91 +++++ crates/pf-vaadec/src/va.rs | 575 ++++++++++++++++++++++++++++++++ 7 files changed, 982 insertions(+) create mode 100644 crates/pf-vaadec/Cargo.toml create mode 100644 crates/pf-vaadec/layout-probe.c create mode 100644 crates/pf-vaadec/src/config.rs create mode 100644 crates/pf-vaadec/src/lib.rs create mode 100644 crates/pf-vaadec/src/va.rs diff --git a/Cargo.lock b/Cargo.lock index b3e652ec..355892eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3195,6 +3195,14 @@ dependencies = [ "ureq", ] +[[package]] +name = "pf-vaadec" +version = "0.24.0" +dependencies = [ + "pf-bitstream", + "pf-vkdecode", +] + [[package]] name = "pf-vdisplay" version = "0.24.0" diff --git a/Cargo.toml b/Cargo.toml index 13447180..3a694e83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "crates/pf-vdisplay", "crates/pf-vkdecode", "crates/pf-dxvadec", + "crates/pf-vaadec", "crates/pyrowave-sys", "crates/libvpl-sys", "clients/probe", diff --git a/crates/pf-vaadec/Cargo.toml b/crates/pf-vaadec/Cargo.toml new file mode 100644 index 00000000..487984b2 --- /dev/null +++ b/crates/pf-vaadec/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pf-vaadec" +description = "Native VAAPI H.264/HEVC decode for the Linux clients (M6): the hand-declared libva decode buffer layouts plus the profile/format/surface decisions — the CPU-testable half; the libva plumbing lives in pf-client-core (design/client-native-decode.md §3.4)" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[dependencies] +pf-bitstream = { path = "../pf-bitstream" } +# For `SlotMap`/`SlotError` ONLY — see this crate's lib.rs for why the DPB slot ledger is +# borrowed from the Vulkan crate rather than duplicated or moved. +pf-vkdecode = { path = "../pf-vkdecode" } + +[lints] +workspace = true diff --git a/crates/pf-vaadec/layout-probe.c b/crates/pf-vaadec/layout-probe.c new file mode 100644 index 00000000..83bf2596 --- /dev/null +++ b/crates/pf-vaadec/layout-probe.c @@ -0,0 +1,115 @@ +/* + * Layout probe for the hand-declared libva structures in `src/va.rs`. + * + * `src/va.rs` declares VAAPI's decode buffers as `#[repr(C)]` Rust structs, because + * this crate must build on macOS and in the Linux container where libva headers need + * not exist. This file is how those declarations were CHECKED rather than eyeballed, + * and it is committed so the check is reproducible instead of a claim in a commit + * message. + * + * Run it against real headers (no Linux box required): + * + * docker run --rm --platform linux/amd64 -v "$PWD/crates/pf-vaadec:/w" -w /w \ + * pf-lxcheck2 bash -lc 'apt-get update -qq && apt-get install -y -qq libva-dev \ + * && gcc -w -O0 layout-probe.c -o /tmp/probe && /tmp/probe' + * + * Every number it prints is pinned as a `const` assertion at the bottom of + * `src/va.rs`, so a transcription mistake is a compile error. The bit-field section + * exists because C bit-field allocation order is ABI-defined, not standardised: + * it PROVES least-significant-bit-first on this ABI rather than assuming it. + * + * Last run against libva 2.23.0-1ubuntu1, x86_64-linux-gnu. + */ +#include +#include +#include + +#define S(t) printf("size %-34s %zu align %zu\n", #t, sizeof(t), _Alignof(t)) +#define O(t, f) printf("off %-20s %-28s %zu\n", #t, #f, offsetof(t, f)) + +int main(void) { + S(VAPictureH264); + O(VAPictureH264, picture_id); + O(VAPictureH264, frame_idx); + O(VAPictureH264, flags); + O(VAPictureH264, TopFieldOrderCnt); + O(VAPictureH264, BottomFieldOrderCnt); + O(VAPictureH264, va_reserved); + + S(VAPictureParameterBufferH264); + O(VAPictureParameterBufferH264, CurrPic); + O(VAPictureParameterBufferH264, ReferenceFrames); + O(VAPictureParameterBufferH264, picture_width_in_mbs_minus1); + O(VAPictureParameterBufferH264, picture_height_in_mbs_minus1); + O(VAPictureParameterBufferH264, bit_depth_luma_minus8); + O(VAPictureParameterBufferH264, bit_depth_chroma_minus8); + O(VAPictureParameterBufferH264, num_ref_frames); + O(VAPictureParameterBufferH264, seq_fields); + O(VAPictureParameterBufferH264, num_slice_groups_minus1); + O(VAPictureParameterBufferH264, slice_group_map_type); + O(VAPictureParameterBufferH264, slice_group_change_rate_minus1); + O(VAPictureParameterBufferH264, pic_init_qp_minus26); + O(VAPictureParameterBufferH264, pic_init_qs_minus26); + O(VAPictureParameterBufferH264, chroma_qp_index_offset); + O(VAPictureParameterBufferH264, second_chroma_qp_index_offset); + O(VAPictureParameterBufferH264, pic_fields); + O(VAPictureParameterBufferH264, frame_num); + O(VAPictureParameterBufferH264, va_reserved); + + S(VAIQMatrixBufferH264); + O(VAIQMatrixBufferH264, ScalingList4x4); + O(VAIQMatrixBufferH264, ScalingList8x8); + O(VAIQMatrixBufferH264, va_reserved); + + S(VASliceParameterBufferH264); + O(VASliceParameterBufferH264, slice_data_size); + O(VASliceParameterBufferH264, slice_data_offset); + O(VASliceParameterBufferH264, slice_data_flag); + O(VASliceParameterBufferH264, slice_data_bit_offset); + O(VASliceParameterBufferH264, first_mb_in_slice); + O(VASliceParameterBufferH264, slice_type); + O(VASliceParameterBufferH264, direct_spatial_mv_pred_flag); + O(VASliceParameterBufferH264, num_ref_idx_l0_active_minus1); + O(VASliceParameterBufferH264, num_ref_idx_l1_active_minus1); + O(VASliceParameterBufferH264, cabac_init_idc); + O(VASliceParameterBufferH264, slice_qp_delta); + O(VASliceParameterBufferH264, disable_deblocking_filter_idc); + O(VASliceParameterBufferH264, slice_alpha_c0_offset_div2); + O(VASliceParameterBufferH264, slice_beta_offset_div2); + O(VASliceParameterBufferH264, RefPicList0); + O(VASliceParameterBufferH264, RefPicList1); + O(VASliceParameterBufferH264, luma_log2_weight_denom); + O(VASliceParameterBufferH264, chroma_log2_weight_denom); + O(VASliceParameterBufferH264, luma_weight_l0_flag); + O(VASliceParameterBufferH264, luma_weight_l0); + O(VASliceParameterBufferH264, luma_offset_l0); + O(VASliceParameterBufferH264, chroma_weight_l0_flag); + O(VASliceParameterBufferH264, chroma_weight_l0); + O(VASliceParameterBufferH264, chroma_offset_l0); + O(VASliceParameterBufferH264, luma_weight_l1_flag); + O(VASliceParameterBufferH264, luma_weight_l1); + O(VASliceParameterBufferH264, luma_offset_l1); + O(VASliceParameterBufferH264, chroma_weight_l1_flag); + O(VASliceParameterBufferH264, chroma_weight_l1); + O(VASliceParameterBufferH264, chroma_offset_l1); + O(VASliceParameterBufferH264, va_reserved); + + /* Bit-field allocation order: prove LSB-first rather than assume it. */ + { + VAPictureParameterBufferH264 p; + p.seq_fields.value = 0; + p.seq_fields.bits.chroma_format_idc = 3; + printf("bits seq_fields.chroma_format_idc=3 -> value 0x%08x\n", p.seq_fields.value); + p.seq_fields.value = 0; + p.seq_fields.bits.log2_max_frame_num_minus4 = 0xf; + printf("bits seq_fields.log2_max_frame_num_minus4=0xf -> value 0x%08x\n", p.seq_fields.value); + p.pic_fields.value = 0; + p.pic_fields.bits.reference_pic_flag = 1; + printf("bits pic_fields.reference_pic_flag=1 -> value 0x%08x\n", p.pic_fields.value); + p.pic_fields.value = 0; + p.pic_fields.bits.weighted_bipred_idc = 3; + printf("bits pic_fields.weighted_bipred_idc=3 -> value 0x%08x\n", p.pic_fields.value); + } + printf("VA_PADDING_LOW=%d VA_PADDING_MEDIUM=%d\n", VA_PADDING_LOW, VA_PADDING_MEDIUM); + return 0; +} diff --git a/crates/pf-vaadec/src/config.rs b/crates/pf-vaadec/src/config.rs new file mode 100644 index 00000000..e13eec94 --- /dev/null +++ b/crates/pf-vaadec/src/config.rs @@ -0,0 +1,174 @@ +//! Decoder-creation decisions: which `VAProfile` a stream needs, which render-target +//! format its surfaces must carry, and how many of them to allocate. +//! +//! The same job `pf-dxvadec`'s `config` module does for DXVA, and split out for the same +//! reason: these are pure functions of the stream's shape, so they belong where the +//! ordinary gates run them rather than inside `cfg(target_os = "linux")` FFI that +//! only a box can compile. +//! +//! Constant values are the libva 2.23.0 enumerators. + +/// `VAEntrypointVLD` — full bitstream decode, the only entry point this rung uses. +pub const VA_ENTRYPOINT_VLD: u32 = 1; + +/// `VAProfile` enumerators (`va.h`). +pub const VA_PROFILE_H264_MAIN: i32 = 6; +pub const VA_PROFILE_H264_HIGH: i32 = 7; +pub const VA_PROFILE_H264_CONSTRAINED_BASELINE: i32 = 13; +pub const VA_PROFILE_HEVC_MAIN: i32 = 17; +pub const VA_PROFILE_HEVC_MAIN10: i32 = 18; + +/// `VA_RT_FORMAT_*` — the surface render-target format. +pub const VA_RT_FORMAT_YUV420: u32 = 0x0000_0001; +pub const VA_RT_FORMAT_YUV444: u32 = 0x0000_0004; +pub const VA_RT_FORMAT_YUV420_10: u32 = 0x0000_0100; + +/// Which codec a session decodes. Mirrors `pf-dxvadec`'s `Codec` rather than +/// re-exporting it: this crate must not depend on the Windows-facing one. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Codec { + H264, + H265, +} + +/// A profile choice, with the name the logs print. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaProfile { + pub value: i32, + pub name: &'static str, +} + +/// Why a stream cannot be decoded by this rung at all. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfigError { + /// A (chroma, depth) pair with no profile — 4:4:4, or a depth outside 8/10. + UnsupportedShape { chroma_format_idc: u8, depth: u8 }, +} + +impl std::fmt::Display for ConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConfigError::UnsupportedShape { + chroma_format_idc, + depth, + } => write!( + f, + "no VAAPI decode profile for chroma_format_idc {chroma_format_idc} at {depth} bits" + ), + } + } +} + +impl std::error::Error for ConfigError {} + +/// The profile a stream of this shape decodes under. +/// +/// H.264 resolves to **High** for every 8-bit 4:2:0 stream rather than reading the +/// SPS's `profile_idc`. That is deliberate and is what VAAPI clients do: High is a +/// superset of Main and Constrained Baseline for the tools our hosts emit, every +/// driver advertising H.264 decode advertises High, and picking Main for a stream +/// that turns out to use 8x8 transforms is a mid-stream failure where picking High +/// is not. The narrower enumerators are exported for the capability probe, which +/// reports what the DEVICE offers. +/// +/// 4:4:4 is refused rather than mapped: `VAProfileH264High444` exists in the header +/// but no driver in this fleet advertises it, and the Vulkan rung is where this +/// program's 4:4:4 support actually lives. +pub fn profile_for( + codec: Codec, + chroma_format_idc: u8, + depth: u8, +) -> Result { + match (codec, chroma_format_idc, depth) { + (Codec::H264, 1, 8) => Ok(VaProfile { + value: VA_PROFILE_H264_HIGH, + name: "H.264 High", + }), + (Codec::H265, 1, 8) => Ok(VaProfile { + value: VA_PROFILE_HEVC_MAIN, + name: "HEVC Main", + }), + (Codec::H265, 1, 10) => Ok(VaProfile { + value: VA_PROFILE_HEVC_MAIN10, + name: "HEVC Main 10", + }), + _ => Err(ConfigError::UnsupportedShape { + chroma_format_idc, + depth, + }), + } +} + +/// The surface render-target format for a stream shape. +pub fn rt_format(chroma_format_idc: u8, depth: u8) -> Result { + match (chroma_format_idc, depth) { + (1, 8) => Ok(VA_RT_FORMAT_YUV420), + (1, 10) => Ok(VA_RT_FORMAT_YUV420_10), + (3, 8) => Ok(VA_RT_FORMAT_YUV444), + _ => Err(ConfigError::UnsupportedShape { + chroma_format_idc, + depth, + }), + } +} + +/// Headroom over the DPB for pictures the presenter still holds. +/// +/// A surface handed to the compositor is not free to decode into, and a pool sized +/// exactly to the DPB stalls the decoder behind the display. +pub const PRESENTER_HEADROOM: usize = 4; + +/// How many decode surfaces a session allocates: the DPB, plus the picture being +/// decoded, plus [`PRESENTER_HEADROOM`]. +/// +/// VAAPI reports no driver minimum to honour (DXVA's +/// `ConfigMinRenderTargetBuffCount` has no counterpart), so this is the whole rule. +pub fn surface_count(max_dpb_frames: usize) -> usize { + max_dpb_frames + 1 + PRESENTER_HEADROOM +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn h264_8bit_420_is_high() { + let p = profile_for(Codec::H264, 1, 8).expect("the envelope's only H.264 shape"); + assert_eq!(p.value, VA_PROFILE_H264_HIGH); + } + + #[test] + fn hevc_picks_main_or_main10_by_depth() { + assert_eq!( + profile_for(Codec::H265, 1, 8).unwrap().value, + VA_PROFILE_HEVC_MAIN + ); + assert_eq!( + profile_for(Codec::H265, 1, 10).unwrap().value, + VA_PROFILE_HEVC_MAIN10 + ); + } + + #[test] + fn shapes_outside_the_envelope_are_refused_not_guessed() { + // 10-bit H.264 (High10) and 4:4:4 both have header enumerators; neither is + // in this rung's envelope, and silently narrowing to an 8-bit profile is the + // class of bug that decodes to garbage instead of failing. + assert!(profile_for(Codec::H264, 1, 10).is_err()); + assert!(profile_for(Codec::H264, 3, 8).is_err()); + assert!(profile_for(Codec::H265, 3, 10).is_err()); + assert!(rt_format(1, 12).is_err()); + } + + #[test] + fn rt_format_tracks_depth() { + assert_eq!(rt_format(1, 8).unwrap(), VA_RT_FORMAT_YUV420); + assert_eq!(rt_format(1, 10).unwrap(), VA_RT_FORMAT_YUV420_10); + } + + #[test] + fn the_surface_pool_covers_dpb_plus_current_plus_headroom() { + assert_eq!(surface_count(4), 9); + assert_eq!(surface_count(16), 21); + } +} diff --git a/crates/pf-vaadec/src/lib.rs b/crates/pf-vaadec/src/lib.rs new file mode 100644 index 00000000..b90ca846 --- /dev/null +++ b/crates/pf-vaadec/src/lib.rs @@ -0,0 +1,91 @@ +//! Native VAAPI decode for the Linux clients — M6 of the native-decode program, and +//! the VAAPI counterpart of [`pf_vkdecode`] and `pf-dxvadec`. +//! +//! Like `pf-dxvadec`, this crate is the **CPU-testable half**: everything between +//! pf-bitstream's per-AU plan and the buffers a `vaRenderPicture` call delivers. It +//! links no libva, names no `VA*` handle type, and compiles on macOS and in the Linux +//! container — which is the point. The VAAPI rung itself is +//! `cfg(target_os = "linux")` code that only a box can build, so anything left inside +//! that boundary is verified by a remote `cargo check` and nothing more. +//! +//! - [`va`]: the libva decode buffer layouts, **hand-declared**, with every size and +//! offset measured off the real headers and pinned as compile-time assertions. +//! - [`config`]: profile, render-target format and surface-count decisions. +//! +//! # Status +//! +//! **Layouts and configuration only.** The `AuPlan` → picture/slice/IQ-matrix +//! conversion is the next work package; the groundwork it needs is established and +//! recorded here so it starts from facts rather than from a reading of the spec: +//! +//! * **`slice_data_bit_offset` costs no new parsing.** VAAPI is the only one of the +//! three backends that wants a bit position — DXVA takes a byte offset, Vulkan +//! takes none — and the vendored parser already records exactly it as +//! `SliceHeader::header_bit_size`, because cros-codecs' own production backend is +//! VAAPI. Its definition matches field for field: computed as +//! `(nalu.size - emulation_prevention_bytes) * 8 - bits_left`, it counts from and +//! including the NAL header byte with emulation-prevention bytes removed, which is +//! what `VASliceParameterBufferH264` documents. +//! * **The slice data buffer starts at the NAL header byte**, so the start code must +//! be skipped — `SlicePlan::data` is start-code-inclusive, and the prefix is three +//! OR four bytes (the real host emits four on 100% of access units). The same +//! normalisation the Vulkan ring layer performs, and the reason that layer exists. +//! * **`VAPictureParameterBufferH264::reference_frames` is the MARKED DPB**, not the +//! access unit's own lists — the same statement DXVA's `RefFrameList` makes, so it +//! is filled from pf-bitstream's per-AU `dpb_refs` snapshot. Vulkan's +//! `pReferenceSlots` is the opposite and takes the AU's own set; all three +//! conventions now have a written home. +//! * **Unlike DXVA's short-format slice control, VAAPI wants the per-slice reference +//! lists themselves** (`RefPicList0`/`RefPicList1`, 32 entries each, in 8.2.4.2 +//! order) and the full prediction weight tables. `SlicePlan::ref_list0`/`ref_list1` +//! and `SliceHeader::pred_weight_table` supply both — with one wrinkle to handle +//! rather than discover later: the vendored `PredWeightTable` stores +//! `luma_offset_l0` as `[i8; 32]` but `luma_offset_l1` as `[i16; 32]`, an upstream +//! inconsistency, while libva wants `i16` for both. +//! +//! # Why the slot ledger is borrowed +//! +//! [`SlotMap`] comes from [`pf_vkdecode`] for the reason `pf-dxvadec`'s docs give at +//! length: it is not a Vulkan object but a ledger from +//! [`pf_bitstream::h264::PicId`] to hardware DPB slot indices, and it is as +//! API-agnostic as it is codec-agnostic. VAAPI's own indirection is one step longer — +//! a slot indexes the caller's surface table, because `VAPictureH264::picture_id` is +//! a `VASurfaceID` rather than an index — so the conversion will take that table as +//! a parameter and stay pure. + +pub mod config; +pub mod va; + +/// The DPB slot ledger — borrowed, not redefined (crate docs). +pub use pf_vkdecode::SlotError; +pub use pf_vkdecode::SlotMap; + +/// The planners and plans this crate converts, re-exported so the Linux layer names +/// every type it touches through `pf_vaadec` — the same courtesy `pf-dxvadec` does +/// for the Windows layer. +pub use pf_bitstream::h264::AuPlan; +pub use pf_bitstream::h264::H264Planner; +pub use pf_bitstream::h264::PlanError; +pub use pf_bitstream::h264::PlanWarning; +pub use pf_bitstream::h265::AuPlan as AuPlanH265; +pub use pf_bitstream::h265::H265Planner; +pub use pf_bitstream::h265::PlanError as PlanErrorH265; +pub use pf_bitstream::h265::PlanWarning as PlanWarningH265; +/// Which warnings mean the PICTURE is damaged — pf-vkdecode's one list, so all three +/// native rungs conceal on exactly the same predicate. +pub use pf_vkdecode::is_integrity_warning; +pub use pf_vkdecode::is_integrity_warning_h265; + +pub use config::profile_for; +pub use config::rt_format; +pub use config::surface_count; +pub use config::Codec; +pub use config::ConfigError; +pub use config::VaProfile; +pub use config::VA_ENTRYPOINT_VLD; +pub use va::PicFieldsH264; +pub use va::SeqFieldsH264; +pub use va::VaIqMatrixBufferH264; +pub use va::VaPictureH264; +pub use va::VaPictureParameterBufferH264; +pub use va::VaSliceParameterBufferH264; diff --git a/crates/pf-vaadec/src/va.rs b/crates/pf-vaadec/src/va.rs new file mode 100644 index 00000000..02a65943 --- /dev/null +++ b/crates/pf-vaadec/src/va.rs @@ -0,0 +1,575 @@ +//! The libva decode buffer layouts for H.264, **hand-declared**. +//! +//! There is no libva binding in this workspace and this crate deliberately does not +//! introduce one: it must compile and be tested on macOS and in the Linux container, +//! where `libva` headers need not exist at all. So the structures VAAPI reads are +//! declared here as plain `#[repr(C)]` PODs, exactly as `pf-dxvadec`'s `dxva` module declares +//! DXVA's — same reasoning, same discipline. +//! +//! # These are not eyeballed +//! +//! Every size, every field offset and the bit-field allocation order below were +//! measured against the real headers (libva **2.23.0**, `x86_64-linux-gnu`) by +//! compiling a probe that printed `sizeof`, `_Alignof` and `offsetof` for each field +//! and set individual bit-fields to read the resulting word back. The numbers that +//! probe produced are pinned as `const` assertions at the bottom of this module, so +//! a mistake here is a compile error rather than a driver reading the wrong byte. +//! +//! The measured facts worth stating in prose, because they are the ones a reader +//! would otherwise assume wrongly: +//! +//! * `VAPictureH264` is **36** bytes — five 4-byte fields plus `VA_PADDING_LOW` +//! (4 × `uint32_t`) of reserved tail. It is embedded 1 + 16 times in the picture +//! parameter buffer and 64 times in the slice parameter buffer, so its size being +//! right is load-bearing for every offset after it. +//! * `VAPictureParameterBufferH264` is **672** bytes, `VAIQMatrixBufferH264` **240**, +//! and `VASliceParameterBufferH264` **3128** — the last one because it carries two +//! 32-entry reference lists *and* the full prediction weight tables inline. +//! * The three deprecated FMO fields (`num_slice_groups_minus1`, +//! `slice_group_map_type`, `slice_group_change_rate_minus1`) still occupy bytes +//! 624..628. Deprecated does not mean absent: dropping them would shift every +//! later field. They are declared, and always zero. +//! * C bit-fields on this ABI allocate from the **least significant bit**, proven +//! rather than assumed: setting `log2_max_frame_num_minus4` (the 4 bits declared +//! after eight single-bit flags and a 2-bit field) to `0xf` yields `0x0000_0f00`, +//! and `weighted_bipred_idc = 3` yields `0x0000_000c`. +//! +//! # Surface identity +//! +//! `VAPictureH264::picture_id` is a `VASurfaceID`, not a slot index — unlike DXVA, +//! where the surface index and the DPB slot are the same number by construction. +//! This crate never invents one: the conversion (`plan_to_va`) takes the caller's +//! slot → `VASurfaceID` table and indexes it, so the Linux layer owns surface +//! allocation and this half stays pure. + +/// `VA_INVALID_SURFACE` — what an unused `ReferenceFrames` / `RefPicList` entry +/// carries. Paired with [`VA_PICTURE_H264_INVALID`]; drivers key on the flag, but a +/// stale surface id in an "invalid" entry is the kind of thing that decodes fine on +/// one vendor and not another, so both are always written together. +pub const VA_INVALID_SURFACE: u32 = 0xffff_ffff; + +/// Flags for [`VaPictureH264::flags`]. +pub const VA_PICTURE_H264_INVALID: u32 = 0x0000_0001; +pub const VA_PICTURE_H264_TOP_FIELD: u32 = 0x0000_0002; +pub const VA_PICTURE_H264_BOTTOM_FIELD: u32 = 0x0000_0004; +pub const VA_PICTURE_H264_SHORT_TERM_REFERENCE: u32 = 0x0000_0008; +pub const VA_PICTURE_H264_LONG_TERM_REFERENCE: u32 = 0x0000_0010; + +/// `VA_SLICE_DATA_FLAG_ALL` — this buffer holds the whole slice, which is the only +/// shape we submit (the wire delivers complete access units; nothing here streams a +/// slice in fragments). +pub const VA_SLICE_DATA_FLAG_ALL: u32 = 0x00; + +/// `VAPictureH264` — one DPB entry, or the current picture. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaPictureH264 { + /// `VASurfaceID` of the decode surface holding this picture. + pub picture_id: u32, + /// `frame_num` for a short-term reference, `LongTermFrameIdx` for a long-term + /// one — the same pair DXVA and Vulkan key references by, which is why + /// [`pf_bitstream::h264::RefPic`] already carries exactly this. + pub frame_idx: u32, + pub flags: u32, + pub top_field_order_cnt: i32, + pub bottom_field_order_cnt: i32, + /// `va_reserved[VA_PADDING_LOW]` — "must be zero". + pub va_reserved: [u32; 4], +} + +impl VaPictureH264 { + /// The entry an unused reference slot carries: invalid flag AND invalid surface. + pub const fn invalid() -> Self { + VaPictureH264 { + picture_id: VA_INVALID_SURFACE, + frame_idx: 0, + flags: VA_PICTURE_H264_INVALID, + top_field_order_cnt: 0, + bottom_field_order_cnt: 0, + va_reserved: [0; 4], + } + } +} + +/// `VAPictureParameterBufferH264::seq_fields`, unpacked. +/// +/// Declared as its own type rather than as a bare `u32` so the bit layout lives +/// beside the structure it belongs to and can be unit-tested on its own; [`Self::pack`] +/// is the only place the shifts appear. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SeqFieldsH264 { + pub chroma_format_idc: u8, + /// `residual_colour_transform_flag` in the header's older spelling; the standard + /// renamed it `separate_colour_plane_flag`. + pub separate_colour_plane_flag: bool, + pub gaps_in_frame_num_value_allowed_flag: bool, + pub frame_mbs_only_flag: bool, + pub mb_adaptive_frame_field_flag: bool, + pub direct_8x8_inference_flag: bool, + /// A.3.3.2 — level-derived, not an SPS syntax element. + pub min_luma_bi_pred_size8x8: bool, + pub log2_max_frame_num_minus4: u8, + pub pic_order_cnt_type: u8, + pub log2_max_pic_order_cnt_lsb_minus4: u8, + pub delta_pic_order_always_zero_flag: bool, +} + +impl SeqFieldsH264 { + /// Pack to the `uint32_t` the union aliases. Bit positions are the measured + /// allocation order (module docs), LSB first, in declaration order. + pub const fn pack(self) -> u32 { + (self.chroma_format_idc as u32 & 0x3) + | ((self.separate_colour_plane_flag as u32) << 2) + | ((self.gaps_in_frame_num_value_allowed_flag as u32) << 3) + | ((self.frame_mbs_only_flag as u32) << 4) + | ((self.mb_adaptive_frame_field_flag as u32) << 5) + | ((self.direct_8x8_inference_flag as u32) << 6) + | ((self.min_luma_bi_pred_size8x8 as u32) << 7) + | ((self.log2_max_frame_num_minus4 as u32 & 0xf) << 8) + | ((self.pic_order_cnt_type as u32 & 0x3) << 12) + | ((self.log2_max_pic_order_cnt_lsb_minus4 as u32 & 0xf) << 14) + | ((self.delta_pic_order_always_zero_flag as u32) << 18) + } +} + +/// `VAPictureParameterBufferH264::pic_fields`, unpacked. See [`SeqFieldsH264`]. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct PicFieldsH264 { + pub entropy_coding_mode_flag: bool, + pub weighted_pred_flag: bool, + pub weighted_bipred_idc: u8, + pub transform_8x8_mode_flag: bool, + pub field_pic_flag: bool, + pub constrained_intra_pred_flag: bool, + /// `bottom_field_pic_order_in_frame_present_flag` in current spec spelling. + pub pic_order_present_flag: bool, + pub deblocking_filter_control_present_flag: bool, + pub redundant_pic_cnt_present_flag: bool, + /// `nal_ref_idc != 0` — a statement about THIS picture, not the PPS. + pub reference_pic_flag: bool, +} + +impl PicFieldsH264 { + pub const fn pack(self) -> u32 { + (self.entropy_coding_mode_flag as u32) + | ((self.weighted_pred_flag as u32) << 1) + | ((self.weighted_bipred_idc as u32 & 0x3) << 2) + | ((self.transform_8x8_mode_flag as u32) << 4) + | ((self.field_pic_flag as u32) << 5) + | ((self.constrained_intra_pred_flag as u32) << 6) + | ((self.pic_order_present_flag as u32) << 7) + | ((self.deblocking_filter_control_present_flag as u32) << 8) + | ((self.redundant_pic_cnt_present_flag as u32) << 9) + | ((self.reference_pic_flag as u32) << 10) + } +} + +/// `VAPictureParameterBufferH264` — one per picture, before any slice data. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaPictureParameterBufferH264 { + pub curr_pic: VaPictureH264, + /// The DPB, not this AU's reference lists. VAAPI documents this as "in DPB", + /// which is the same statement DXVA's `RefFrameList` makes and the opposite of + /// Vulkan's `pReferenceSlots` — so it is filled from pf-bitstream's per-AU + /// `dpb_refs` snapshot, the accessor M5 added for exactly this distinction. + pub reference_frames: [VaPictureH264; 16], + pub picture_width_in_mbs_minus1: u16, + pub picture_height_in_mbs_minus1: u16, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub num_ref_frames: u8, + /// Packed [`SeqFieldsH264`]. (One byte of padding precedes it — `num_ref_frames` + /// ends at 619 and the union is 4-aligned at 620.) + pub seq_fields: u32, + /// Deprecated FMO fields. Still occupy bytes 624..628; always zero here, and + /// the conversion (`plan_to_va`) refuses a stream that uses slice groups rather + /// than silently ignoring them. + pub num_slice_groups_minus1: u8, + pub slice_group_map_type: u8, + pub slice_group_change_rate_minus1: u16, + pub pic_init_qp_minus26: i8, + pub pic_init_qs_minus26: i8, + pub chroma_qp_index_offset: i8, + pub second_chroma_qp_index_offset: i8, + /// Packed [`PicFieldsH264`]. + pub pic_fields: u32, + pub frame_num: u16, + /// `va_reserved[VA_PADDING_MEDIUM]`. Two bytes of padding precede it (`frame_num` + /// ends at 638, the array is 4-aligned at 640). + pub va_reserved: [u32; 8], +} + +/// `VAIQMatrixBufferH264` — both scaling list sets, raster scan order. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaIqMatrixBufferH264 { + pub scaling_list4x4: [[u8; 16]; 6], + pub scaling_list8x8: [[u8; 64]; 2], + pub va_reserved: [u32; 4], +} + +/// `VASliceParameterBufferH264` — one per slice NALU. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaSliceParameterBufferH264 { + pub slice_data_size: u32, + pub slice_data_offset: u32, + pub slice_data_flag: u32, + /// Bit offset from the start of the NAL unit byte to the first bit of + /// `slice_data()`, counted **after emulation-prevention bytes are removed** even + /// though the buffer handed to the driver still contains them. + /// + /// Nothing else in this program needs this number: DXVA takes a byte offset to + /// the slice and Vulkan takes none at all. The vendored parser records it as + /// `SliceHeader::header_bit_size` because its own production backend is VAAPI, + /// so it costs no new parsing — see the crate docs. + pub slice_data_bit_offset: u16, + pub first_mb_in_slice: u16, + pub slice_type: u8, + pub direct_spatial_mv_pred_flag: u8, + pub num_ref_idx_l0_active_minus1: u8, + pub num_ref_idx_l1_active_minus1: u8, + pub cabac_init_idc: u8, + pub slice_qp_delta: i8, + pub disable_deblocking_filter_idc: u8, + pub slice_alpha_c0_offset_div2: i8, + pub slice_beta_offset_div2: i8, + /// 8.2.4.2 reference lists — the AU's own, unlike + /// [`VaPictureParameterBufferH264::reference_frames`]. + pub ref_pic_list0: [VaPictureH264; 32], + pub ref_pic_list1: [VaPictureH264; 32], + pub luma_log2_weight_denom: u8, + pub chroma_log2_weight_denom: u8, + pub luma_weight_l0_flag: u8, + pub luma_weight_l0: [i16; 32], + pub luma_offset_l0: [i16; 32], + pub chroma_weight_l0_flag: u8, + pub chroma_weight_l0: [[i16; 2]; 32], + pub chroma_offset_l0: [[i16; 2]; 32], + pub luma_weight_l1_flag: u8, + pub luma_weight_l1: [i16; 32], + pub luma_offset_l1: [i16; 32], + pub chroma_weight_l1_flag: u8, + pub chroma_weight_l1: [[i16; 2]; 32], + pub chroma_offset_l1: [[i16; 2]; 32], + pub va_reserved: [u32; 4], +} + +impl VaSliceParameterBufferH264 { + /// An all-zero record with the reference lists invalidated — the base every + /// slice is built from, so an unwritten entry is never a stale surface id. + pub const fn zeroed() -> Self { + VaSliceParameterBufferH264 { + slice_data_size: 0, + slice_data_offset: 0, + slice_data_flag: VA_SLICE_DATA_FLAG_ALL, + slice_data_bit_offset: 0, + first_mb_in_slice: 0, + slice_type: 0, + direct_spatial_mv_pred_flag: 0, + num_ref_idx_l0_active_minus1: 0, + num_ref_idx_l1_active_minus1: 0, + cabac_init_idc: 0, + slice_qp_delta: 0, + disable_deblocking_filter_idc: 0, + slice_alpha_c0_offset_div2: 0, + slice_beta_offset_div2: 0, + ref_pic_list0: [VaPictureH264::invalid(); 32], + ref_pic_list1: [VaPictureH264::invalid(); 32], + luma_log2_weight_denom: 0, + chroma_log2_weight_denom: 0, + luma_weight_l0_flag: 0, + luma_weight_l0: [0; 32], + luma_offset_l0: [0; 32], + chroma_weight_l0_flag: 0, + chroma_weight_l0: [[0; 2]; 32], + chroma_offset_l0: [[0; 2]; 32], + luma_weight_l1_flag: 0, + luma_weight_l1: [0; 32], + luma_offset_l1: [0; 32], + chroma_weight_l1_flag: 0, + chroma_weight_l1: [[0; 2]; 32], + chroma_offset_l1: [[0; 2]; 32], + va_reserved: [0; 4], + } + } +} + +// --------------------------------------------------------------------------- +// Layout proofs — the probe's output, pinned. +// +// libva 2.23.0, x86_64-linux-gnu. A `#[repr(C)]` Rust struct and a C struct agree +// by definition of repr(C), so these assertions are not testing the compiler: they +// are testing that the FIELDS AND THEIR ORDER above match the header, which is the +// part a human transcribed and can get wrong. +// --------------------------------------------------------------------------- + +const _: () = { + use std::mem::offset_of; + use std::mem::size_of; + + assert!(size_of::() == 36); + assert!(offset_of!(VaPictureH264, picture_id) == 0); + assert!(offset_of!(VaPictureH264, frame_idx) == 4); + assert!(offset_of!(VaPictureH264, flags) == 8); + assert!(offset_of!(VaPictureH264, top_field_order_cnt) == 12); + assert!(offset_of!(VaPictureH264, bottom_field_order_cnt) == 16); + assert!(offset_of!(VaPictureH264, va_reserved) == 20); + + assert!(size_of::() == 672); + assert!(offset_of!(VaPictureParameterBufferH264, curr_pic) == 0); + assert!(offset_of!(VaPictureParameterBufferH264, reference_frames) == 36); + assert!(offset_of!(VaPictureParameterBufferH264, picture_width_in_mbs_minus1) == 612); + assert!(offset_of!(VaPictureParameterBufferH264, picture_height_in_mbs_minus1) == 614); + assert!(offset_of!(VaPictureParameterBufferH264, bit_depth_luma_minus8) == 616); + assert!(offset_of!(VaPictureParameterBufferH264, bit_depth_chroma_minus8) == 617); + assert!(offset_of!(VaPictureParameterBufferH264, num_ref_frames) == 618); + assert!(offset_of!(VaPictureParameterBufferH264, seq_fields) == 620); + assert!(offset_of!(VaPictureParameterBufferH264, num_slice_groups_minus1) == 624); + assert!(offset_of!(VaPictureParameterBufferH264, slice_group_map_type) == 625); + assert!(offset_of!(VaPictureParameterBufferH264, slice_group_change_rate_minus1) == 626); + assert!(offset_of!(VaPictureParameterBufferH264, pic_init_qp_minus26) == 628); + assert!(offset_of!(VaPictureParameterBufferH264, pic_init_qs_minus26) == 629); + assert!(offset_of!(VaPictureParameterBufferH264, chroma_qp_index_offset) == 630); + assert!(offset_of!(VaPictureParameterBufferH264, second_chroma_qp_index_offset) == 631); + assert!(offset_of!(VaPictureParameterBufferH264, pic_fields) == 632); + assert!(offset_of!(VaPictureParameterBufferH264, frame_num) == 636); + assert!(offset_of!(VaPictureParameterBufferH264, va_reserved) == 640); + + assert!(size_of::() == 240); + assert!(offset_of!(VaIqMatrixBufferH264, scaling_list4x4) == 0); + assert!(offset_of!(VaIqMatrixBufferH264, scaling_list8x8) == 96); + assert!(offset_of!(VaIqMatrixBufferH264, va_reserved) == 224); + + assert!(size_of::() == 3128); + assert!(offset_of!(VaSliceParameterBufferH264, slice_data_size) == 0); + assert!(offset_of!(VaSliceParameterBufferH264, slice_data_offset) == 4); + assert!(offset_of!(VaSliceParameterBufferH264, slice_data_flag) == 8); + assert!(offset_of!(VaSliceParameterBufferH264, slice_data_bit_offset) == 12); + assert!(offset_of!(VaSliceParameterBufferH264, first_mb_in_slice) == 14); + assert!(offset_of!(VaSliceParameterBufferH264, slice_type) == 16); + assert!(offset_of!(VaSliceParameterBufferH264, direct_spatial_mv_pred_flag) == 17); + assert!(offset_of!(VaSliceParameterBufferH264, num_ref_idx_l0_active_minus1) == 18); + assert!(offset_of!(VaSliceParameterBufferH264, num_ref_idx_l1_active_minus1) == 19); + assert!(offset_of!(VaSliceParameterBufferH264, cabac_init_idc) == 20); + assert!(offset_of!(VaSliceParameterBufferH264, slice_qp_delta) == 21); + assert!(offset_of!(VaSliceParameterBufferH264, disable_deblocking_filter_idc) == 22); + assert!(offset_of!(VaSliceParameterBufferH264, slice_alpha_c0_offset_div2) == 23); + assert!(offset_of!(VaSliceParameterBufferH264, slice_beta_offset_div2) == 24); + assert!(offset_of!(VaSliceParameterBufferH264, ref_pic_list0) == 28); + assert!(offset_of!(VaSliceParameterBufferH264, ref_pic_list1) == 1180); + assert!(offset_of!(VaSliceParameterBufferH264, luma_log2_weight_denom) == 2332); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_log2_weight_denom) == 2333); + assert!(offset_of!(VaSliceParameterBufferH264, luma_weight_l0_flag) == 2334); + assert!(offset_of!(VaSliceParameterBufferH264, luma_weight_l0) == 2336); + assert!(offset_of!(VaSliceParameterBufferH264, luma_offset_l0) == 2400); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_weight_l0_flag) == 2464); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_weight_l0) == 2466); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_offset_l0) == 2594); + assert!(offset_of!(VaSliceParameterBufferH264, luma_weight_l1_flag) == 2722); + assert!(offset_of!(VaSliceParameterBufferH264, luma_weight_l1) == 2724); + assert!(offset_of!(VaSliceParameterBufferH264, luma_offset_l1) == 2788); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_weight_l1_flag) == 2852); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_weight_l1) == 2854); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_offset_l1) == 2982); + assert!(offset_of!(VaSliceParameterBufferH264, va_reserved) == 3112); +}; + +#[cfg(test)] +mod tests { + use super::*; + + // The three bit patterns the probe read back off real headers. If the shifts + // above are ever "tidied", these fail with the measured value in hand. + #[test] + fn seq_fields_pack_where_the_probe_measured() { + assert_eq!( + SeqFieldsH264 { + chroma_format_idc: 3, + ..Default::default() + } + .pack(), + 0x0000_0003 + ); + assert_eq!( + SeqFieldsH264 { + log2_max_frame_num_minus4: 0xf, + ..Default::default() + } + .pack(), + 0x0000_0f00 + ); + } + + #[test] + fn pic_fields_pack_where_the_probe_measured() { + assert_eq!( + PicFieldsH264 { + reference_pic_flag: true, + ..Default::default() + } + .pack(), + 0x0000_0400 + ); + assert_eq!( + PicFieldsH264 { + weighted_bipred_idc: 3, + ..Default::default() + } + .pack(), + 0x0000_000c + ); + } + + #[test] + fn every_seq_field_owns_a_distinct_bit_range() { + // Each field set alone must light only its own bits, and the OR of all of + // them must equal the packing of all-at-once: a shift typo that overlapped + // two fields would still pass the two probe vectors above. + let each = [ + SeqFieldsH264 { + chroma_format_idc: 3, + ..Default::default() + }, + SeqFieldsH264 { + separate_colour_plane_flag: true, + ..Default::default() + }, + SeqFieldsH264 { + gaps_in_frame_num_value_allowed_flag: true, + ..Default::default() + }, + SeqFieldsH264 { + frame_mbs_only_flag: true, + ..Default::default() + }, + SeqFieldsH264 { + mb_adaptive_frame_field_flag: true, + ..Default::default() + }, + SeqFieldsH264 { + direct_8x8_inference_flag: true, + ..Default::default() + }, + SeqFieldsH264 { + min_luma_bi_pred_size8x8: true, + ..Default::default() + }, + SeqFieldsH264 { + log2_max_frame_num_minus4: 0xf, + ..Default::default() + }, + SeqFieldsH264 { + pic_order_cnt_type: 3, + ..Default::default() + }, + SeqFieldsH264 { + log2_max_pic_order_cnt_lsb_minus4: 0xf, + ..Default::default() + }, + SeqFieldsH264 { + delta_pic_order_always_zero_flag: true, + ..Default::default() + }, + ]; + let mut seen = 0u32; + for f in each { + let bits = f.pack(); + assert_ne!(bits, 0, "a field packed to nothing"); + assert_eq!(seen & bits, 0, "two fields share a bit: {bits:#010x}"); + seen |= bits; + } + let all = SeqFieldsH264 { + chroma_format_idc: 3, + separate_colour_plane_flag: true, + gaps_in_frame_num_value_allowed_flag: true, + frame_mbs_only_flag: true, + mb_adaptive_frame_field_flag: true, + direct_8x8_inference_flag: true, + min_luma_bi_pred_size8x8: true, + log2_max_frame_num_minus4: 0xf, + pic_order_cnt_type: 3, + log2_max_pic_order_cnt_lsb_minus4: 0xf, + delta_pic_order_always_zero_flag: true, + }; + assert_eq!(all.pack(), seen); + // Nothing may reach past bit 18 — the last declared bit. + assert_eq!(seen & !0x0007_ffff, 0); + } + + #[test] + fn every_pic_field_owns_a_distinct_bit_range() { + let each = [ + PicFieldsH264 { + entropy_coding_mode_flag: true, + ..Default::default() + }, + PicFieldsH264 { + weighted_pred_flag: true, + ..Default::default() + }, + PicFieldsH264 { + weighted_bipred_idc: 3, + ..Default::default() + }, + PicFieldsH264 { + transform_8x8_mode_flag: true, + ..Default::default() + }, + PicFieldsH264 { + field_pic_flag: true, + ..Default::default() + }, + PicFieldsH264 { + constrained_intra_pred_flag: true, + ..Default::default() + }, + PicFieldsH264 { + pic_order_present_flag: true, + ..Default::default() + }, + PicFieldsH264 { + deblocking_filter_control_present_flag: true, + ..Default::default() + }, + PicFieldsH264 { + redundant_pic_cnt_present_flag: true, + ..Default::default() + }, + PicFieldsH264 { + reference_pic_flag: true, + ..Default::default() + }, + ]; + let mut seen = 0u32; + for f in each { + let bits = f.pack(); + assert_ne!(bits, 0); + assert_eq!(seen & bits, 0, "two fields share a bit: {bits:#010x}"); + seen |= bits; + } + assert_eq!(seen & !0x0000_07ff, 0); + } + + #[test] + fn an_unused_reference_entry_is_invalid_in_both_ways() { + let e = VaPictureH264::invalid(); + assert_eq!(e.flags, VA_PICTURE_H264_INVALID); + assert_eq!(e.picture_id, VA_INVALID_SURFACE); + } + + #[test] + fn a_zeroed_slice_record_starts_with_invalidated_lists() { + let s = VaSliceParameterBufferH264::zeroed(); + assert!(s + .ref_pic_list0 + .iter() + .all(|e| e.flags == VA_PICTURE_H264_INVALID)); + assert!(s + .ref_pic_list1 + .iter() + .all(|e| e.picture_id == VA_INVALID_SURFACE)); + assert_eq!(s.slice_data_flag, VA_SLICE_DATA_FLAG_ALL); + } +}