Files
punktfunk/crates/pf-vkdecode/src/params.rs
T
enricobuehler d24f7fc6ac feat(pf-vkdecode): the CPU half of native Vulkan decode — StdVideo conversion + slot map
M2 WP-A (design/client-native-decode.md §3.2). AuPlan -> StdVideo
parameter sets (owned pointer backings), per-AU decode info with slice
start-code offsets, and a PicId->slot map that never evicts on its own.
Deliberate rejections over silent claims: FMO, separate colour planes,
DPBs deeper than 16 frames (unbounded VUI ue(v)) all fail closed.

pf-bitstream API grew what the review proved necessary: RefPic carries
the true top/bottom field order counts (a single poc fabricated
BottomFieldOrderCnt whenever the PPS signals pic-order deltas), the
>16-frame DPB envelope gate, and an MMCO5 rebase warning.

Adversarial review round two: 8 findings fixed pre-commit, including
transactional slot mutation (an error path could permanently desync
the map) and count/pointer coherence on type-1 POC offsets.

Gates: fmt clean, clippy -D warnings zero, 45+26+21 tests green on
macOS and the linux/amd64 container.
2026-08-05 15:03:39 +02:00

718 lines
32 KiB
Rust

//! Parameter-set conversion: the vendored parser's [`Sps`]/[`Pps`] into the
//! `StdVideoH264*ParameterSet` structs a Vulkan Video session-parameters object is
//! created from (WP-B's `vkCreateVideoSessionParametersKHR`).
//!
//! The Std structs embed raw pointers (`pOffsetForRefFrame`, `pScalingLists`,
//! `pSequenceParameterSetVui`), so conversion returns OWNING wrappers instead of bare
//! structs — see [`OwnedStdSps`] for the aliasing/lifetime contract.
//!
//! VUI is deliberately not converted: a DECODE session consumes no VUI (it shapes
//! display, not reconstruction), so `vui_parameters_present_flag` stays 0 and
//! `pSequenceParameterSetVui` stays null. Colour handling rides
//! [`pf_bitstream::h264::PicturePlan`] into the presenter instead, exactly as the
//! FFmpeg-based path did.
use ash::vk::native as hh;
use cros_codecs::codec::h264::parser::Level;
pub use cros_codecs::codec::h264::parser::Pps;
pub use cros_codecs::codec::h264::parser::Sps;
/// A parameter set that cannot be represented as a StdVideo struct. All of these are
/// outside the punktfunk decode envelope (8-bit 4:2:0 streams from encoders we
/// control), so hitting one is a stream-integrity failure, not a feature gap —
/// reject-with-error rather than submit a half-truth to a driver.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParamsError {
/// `profile_idc` has no `StdVideoH264ProfileIdc` code point. Vulkan defines
/// Baseline (66), Main (77), High (100) and High 4:4:4 Predictive (244);
/// Extended/High10/High422 land here.
UnmappableProfileIdc(u8),
/// `chroma_format_idc` past 3 — not legal H.264 to begin with.
InvalidChromaFormatIdc(u8),
/// `pic_order_cnt_type` past 2 — not legal H.264 to begin with.
InvalidPocType(u8),
/// `weighted_bipred_idc` of 3: representable in the two-bit field, invalid per
/// 7.4.2.2, and no `StdVideoH264WeightedBipredIdc` code point exists for it.
InvalidWeightedBipredIdc(u8),
/// FMO (`num_slice_groups_minus1 > 0`): `StdVideoH264PictureParameterSet` has no
/// slice-group fields at all — Vulkan Video cannot express it.
SliceGroups(u32),
}
impl std::fmt::Display for ParamsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParamsError::UnmappableProfileIdc(idc) => {
write!(
f,
"profile_idc {idc} has no StdVideoH264ProfileIdc code point"
)
}
ParamsError::InvalidChromaFormatIdc(idc) => {
write!(f, "invalid chroma_format_idc {idc}")
}
ParamsError::InvalidPocType(t) => write!(f, "invalid pic_order_cnt_type {t}"),
ParamsError::InvalidWeightedBipredIdc(idc) => {
write!(f, "invalid weighted_bipred_idc {idc}")
}
ParamsError::SliceGroups(n) => {
write!(
f,
"FMO ({} slice groups) is not expressible in Vulkan Video",
n + 1
)
}
}
}
}
impl std::error::Error for ParamsError {}
/// The converted SPS plus the heap allocations its embedded pointers target.
///
/// `StdVideoH264SequenceParameterSet` points at data it does not contain: the
/// POC-type-1 offset array (`pOffsetForRefFrame`) and the scaling lists
/// (`pScalingLists`). This wrapper owns that data, and the ownership design is the
/// contract WP-B builds on:
///
/// - The backing is boxed, so the wrapper may be MOVED freely: moving it relocates
/// the `Box` handles (pointer values), never the heap blocks the Std struct's
/// pointers hold the addresses of.
/// - [`Self::std`] hands the struct out by shared reference. The struct is `Copy`; a
/// copy taken out of the wrapper still points INTO the wrapper's backing and must
/// not outlive it. The intended use is passing the reference straight into
/// `vkCreateVideoSessionParametersKHR`, which copies all parameter data before
/// returning — keeping the wrapper alive across that call is the whole obligation.
/// - Nothing exposes mutation of the backing, so for the wrapper's lifetime the
/// pointed-to data is immutable and the `*const` aliasing rules hold trivially.
/// - Deliberately NOT `Clone`: a derived clone would duplicate the pointer VALUES but
/// not the backing, silently tying the clone's validity to the original's lifetime.
/// Re-convert from the `Sps` instead — conversion is cheap and pure.
#[derive(Debug)]
pub struct OwnedStdSps {
std: hh::StdVideoH264SequenceParameterSet,
/// `pOffsetForRefFrame`'s target (POC type 1 only, else `None`/null).
_offset_backing: Option<Box<[i32]>>,
/// `pScalingLists`' target (`seq_scaling_matrix_present_flag` only, else null).
_scaling_backing: Option<Box<hh::StdVideoH264ScalingLists>>,
}
impl OwnedStdSps {
/// The Std struct, valid for as long as `self` lives (see the type-level
/// contract; do not let a `Copy` of it outlive the wrapper).
pub fn std(&self) -> &hh::StdVideoH264SequenceParameterSet {
&self.std
}
}
/// The converted PPS plus the scaling-list allocation its `pScalingLists` targets.
/// Same ownership contract as [`OwnedStdSps`], with the one pointer.
#[derive(Debug)]
pub struct OwnedStdPps {
std: hh::StdVideoH264PictureParameterSet,
_scaling_backing: Option<Box<hh::StdVideoH264ScalingLists>>,
}
impl OwnedStdPps {
/// The Std struct, valid for as long as `self` lives (see [`OwnedStdSps`]).
pub fn std(&self) -> &hh::StdVideoH264PictureParameterSet {
&self.std
}
}
/// H.264 `level_idc` (value-coded: 10 ⇒ 1.0) to Vulkan's index-coded
/// `StdVideoH264LevelIdc`.
const fn level_to_std(level: Level) -> hh::StdVideoH264LevelIdc {
match level {
Level::L1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_1_0,
// Vulkan has no 1b code point. 1b is signalled on the wire as level_idc 11
// plus constraint_set3_flag — the flag is mapped, so 1.1 is the faithful cap.
Level::L1B => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_1_1,
Level::L1_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_1_1,
Level::L1_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_1_2,
Level::L1_3 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_1_3,
Level::L2_0 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_2_0,
Level::L2_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_2_1,
Level::L2_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_2_2,
Level::L3 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_3_0,
Level::L3_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_3_1,
Level::L3_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_3_2,
Level::L4 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_4_0,
Level::L4_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_4_1,
Level::L4_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_4_2,
Level::L5 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_5_0,
Level::L5_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_5_1,
Level::L5_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_5_2,
Level::L6 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_6_0,
Level::L6_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_6_1,
Level::L6_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_6_2,
}
}
/// Pack the parser's scaling-list arrays into the Std layout.
///
/// The vendored parser has already run 7.3.2.1.1.1 plus the Table 7-2 default and
/// fallback rules, so the arrays hold the fully RESOLVED lists. Every resolved list is
/// therefore declared present verbatim (`scaling_list_present_mask` set,
/// `use_default_scaling_matrix_mask` 0) and the driver applies no further inference —
/// simpler and equivalent to re-encoding which lists came from the bitstream.
///
/// `num_8x8` is the count of 8x8 lists the parser actually resolved: 2 for 4:2:0/4:2:2
/// (Y intra/inter), 6 for 4:4:4, and 0 for a PPS without `transform_8x8_mode_flag`
/// (whose 8x8 arrays are untouched zeros and must not be declared present).
fn scaling_lists_to_std(
lists_4x4: &[[u8; 16]; 6],
lists_8x8: &[[u8; 64]; 6],
num_8x8: u16,
) -> hh::StdVideoH264ScalingLists {
// SAFETY: StdVideoH264ScalingLists is a plain-C bindgen struct of two u16 masks
// and two byte arrays; the all-zero bit pattern is a valid value for every field.
let mut std: hh::StdVideoH264ScalingLists = unsafe { std::mem::zeroed() };
std.scaling_list_present_mask = 0x3F | (((1u16 << num_8x8) - 1) << 6);
std.use_default_scaling_matrix_mask = 0;
std.ScalingList4x4 = *lists_4x4;
// All six 8x8 arrays are copied even when only two are declared present; the
// driver ignores entries whose mask bit is clear.
std.ScalingList8x8 = *lists_8x8;
std
}
/// Convert one SPS into the Std struct (owning wrapper), mapping every field the
/// H.264 decode profile consumes. VUI is skipped by design (module docs).
pub fn sps_to_std(sps: &Sps) -> Result<OwnedStdSps, ParamsError> {
// StdVideoH264ProfileIdc code points equal the profile_idc values they name, so
// recognised ones pass through; everything else has no representation.
let profile_idc = match u32::from(sps.profile_idc) {
p @ (66 | 77 | 100 | 244) => p,
_ => return Err(ParamsError::UnmappableProfileIdc(sps.profile_idc)),
};
if sps.chroma_format_idc > 3 {
return Err(ParamsError::InvalidChromaFormatIdc(sps.chroma_format_idc));
}
if sps.pic_order_cnt_type > 2 {
return Err(ParamsError::InvalidPocType(sps.pic_order_cnt_type));
}
// SAFETY: StdVideoH264SequenceParameterSet is a plain-C bindgen struct of
// integers, a bitfield word and const pointers; all-zero is a valid value for
// every field (null for the pointers) and is the "everything absent" baseline the
// field writes below build on. Same idiom as pf-encode's vk_build.rs.
let mut std: hh::StdVideoH264SequenceParameterSet = unsafe { std::mem::zeroed() };
std.flags
.set_constraint_set0_flag(u32::from(sps.constraint_set0_flag));
std.flags
.set_constraint_set1_flag(u32::from(sps.constraint_set1_flag));
std.flags
.set_constraint_set2_flag(u32::from(sps.constraint_set2_flag));
std.flags
.set_constraint_set3_flag(u32::from(sps.constraint_set3_flag));
std.flags
.set_constraint_set4_flag(u32::from(sps.constraint_set4_flag));
std.flags
.set_constraint_set5_flag(u32::from(sps.constraint_set5_flag));
std.flags
.set_direct_8x8_inference_flag(u32::from(sps.direct_8x8_inference_flag));
std.flags
.set_mb_adaptive_frame_field_flag(u32::from(sps.mb_adaptive_frame_field_flag));
// 1 under the punktfunk envelope (pf-bitstream rejects interlaced SPSes), but the
// conversion itself is faithful, not envelope-coupled.
std.flags
.set_frame_mbs_only_flag(u32::from(sps.frame_mbs_only_flag));
std.flags
.set_delta_pic_order_always_zero_flag(u32::from(sps.delta_pic_order_always_zero_flag));
std.flags
.set_separate_colour_plane_flag(u32::from(sps.separate_colour_plane_flag));
std.flags
.set_gaps_in_frame_num_value_allowed_flag(u32::from(
sps.gaps_in_frame_num_value_allowed_flag,
));
std.flags
.set_qpprime_y_zero_transform_bypass_flag(u32::from(
sps.qpprime_y_zero_transform_bypass_flag,
));
std.flags
.set_frame_cropping_flag(u32::from(sps.frame_cropping_flag));
std.flags
.set_seq_scaling_matrix_present_flag(u32::from(sps.seq_scaling_matrix_present_flag));
// vui_parameters_present_flag stays 0: decode sessions consume no VUI (module docs).
std.profile_idc = profile_idc;
std.level_idc = level_to_std(sps.level_idc);
// Chroma format code points equal the chroma_format_idc values (0..3).
std.chroma_format_idc = u32::from(sps.chroma_format_idc);
std.seq_parameter_set_id = sps.seq_parameter_set_id;
std.bit_depth_luma_minus8 = sps.bit_depth_luma_minus8;
std.bit_depth_chroma_minus8 = sps.bit_depth_chroma_minus8;
std.log2_max_frame_num_minus4 = sps.log2_max_frame_num_minus4;
// POC type code points equal the pic_order_cnt_type values (0..2).
std.pic_order_cnt_type = u32::from(sps.pic_order_cnt_type);
std.offset_for_non_ref_pic = sps.offset_for_non_ref_pic;
std.offset_for_top_to_bottom_field = sps.offset_for_top_to_bottom_field;
std.log2_max_pic_order_cnt_lsb_minus4 = sps.log2_max_pic_order_cnt_lsb_minus4;
std.max_num_ref_frames = sps.max_num_ref_frames;
std.pic_width_in_mbs_minus1 = u32::from(sps.pic_width_in_mbs_minus1);
std.pic_height_in_map_units_minus1 = u32::from(sps.pic_height_in_map_units_minus1);
std.frame_crop_left_offset = sps.frame_crop_left_offset;
std.frame_crop_right_offset = sps.frame_crop_right_offset;
std.frame_crop_top_offset = sps.frame_crop_top_offset;
std.frame_crop_bottom_offset = sps.frame_crop_bottom_offset;
// POC type 1's offset array: exactly num_ref_frames_in_pic_order_cnt_cycle
// entries, boxed so the pointer survives moves of the wrapper. The COUNT field
// and the pointer derive from this one condition so they can never disagree — a
// stale cycle count on a type-0/2 SPS must not become a nonzero count over a
// null array (both stay zeroed instead).
let offset_backing =
(sps.pic_order_cnt_type == 1 && sps.num_ref_frames_in_pic_order_cnt_cycle > 0).then(|| {
let cycle = usize::from(sps.num_ref_frames_in_pic_order_cnt_cycle);
Box::<[i32]>::from(&sps.offset_for_ref_frame[..cycle])
});
if let Some(backing) = &offset_backing {
std.num_ref_frames_in_pic_order_cnt_cycle = sps.num_ref_frames_in_pic_order_cnt_cycle;
std.pOffsetForRefFrame = backing.as_ptr();
}
let scaling_backing = sps.seq_scaling_matrix_present_flag.then(|| {
let num_8x8 = if sps.chroma_format_idc == 3 { 6 } else { 2 };
Box::new(scaling_lists_to_std(
&sps.scaling_lists_4x4,
&sps.scaling_lists_8x8,
num_8x8,
))
});
if let Some(backing) = &scaling_backing {
std.pScalingLists = &**backing;
}
Ok(OwnedStdSps {
std,
_offset_backing: offset_backing,
_scaling_backing: scaling_backing,
})
}
/// Convert one PPS into the Std struct (owning wrapper), mapping every field the
/// H.264 decode profile consumes.
///
/// `num_slice_groups_minus1` has no Std field at all; a PPS carrying FMO is rejected
/// rather than converted into a struct that silently claims there is none.
pub fn pps_to_std(pps: &Pps) -> Result<OwnedStdPps, ParamsError> {
if pps.num_slice_groups_minus1 != 0 {
return Err(ParamsError::SliceGroups(pps.num_slice_groups_minus1));
}
if pps.weighted_bipred_idc > 2 {
return Err(ParamsError::InvalidWeightedBipredIdc(
pps.weighted_bipred_idc,
));
}
// SAFETY: StdVideoH264PictureParameterSet is a plain-C bindgen struct of
// integers, a bitfield word and one const pointer; all-zero is a valid value for
// every field (null for the pointer) and is the baseline the writes below fill.
let mut std: hh::StdVideoH264PictureParameterSet = unsafe { std::mem::zeroed() };
std.flags
.set_transform_8x8_mode_flag(u32::from(pps.transform_8x8_mode_flag));
std.flags
.set_redundant_pic_cnt_present_flag(u32::from(pps.redundant_pic_cnt_present_flag));
std.flags
.set_constrained_intra_pred_flag(u32::from(pps.constrained_intra_pred_flag));
std.flags
.set_deblocking_filter_control_present_flag(u32::from(
pps.deblocking_filter_control_present_flag,
));
std.flags
.set_weighted_pred_flag(u32::from(pps.weighted_pred_flag));
std.flags
.set_bottom_field_pic_order_in_frame_present_flag(u32::from(
pps.bottom_field_pic_order_in_frame_present_flag,
));
std.flags
.set_entropy_coding_mode_flag(u32::from(pps.entropy_coding_mode_flag));
std.flags
.set_pic_scaling_matrix_present_flag(u32::from(pps.pic_scaling_matrix_present_flag));
std.seq_parameter_set_id = pps.seq_parameter_set_id;
std.pic_parameter_set_id = pps.pic_parameter_set_id;
std.num_ref_idx_l0_default_active_minus1 = pps.num_ref_idx_l0_default_active_minus1;
std.num_ref_idx_l1_default_active_minus1 = pps.num_ref_idx_l1_default_active_minus1;
// Code points equal the weighted_bipred_idc values (0..2), validated above.
std.weighted_bipred_idc = u32::from(pps.weighted_bipred_idc);
std.pic_init_qp_minus26 = pps.pic_init_qp_minus26;
std.pic_init_qs_minus26 = pps.pic_init_qs_minus26;
std.chroma_qp_index_offset = pps.chroma_qp_index_offset;
std.second_chroma_qp_index_offset = pps.second_chroma_qp_index_offset;
let scaling_backing = pps.pic_scaling_matrix_present_flag.then(|| {
// The parser resolves a PPS's 8x8 lists only under transform_8x8_mode_flag
// (7.3.2.2 reads them only then); without it the arrays are untouched zeros
// and must not be declared present.
let num_8x8 = match (pps.transform_8x8_mode_flag, pps.sps.chroma_format_idc == 3) {
(false, _) => 0,
(true, false) => 2,
(true, true) => 6,
};
Box::new(scaling_lists_to_std(
&pps.scaling_lists_4x4,
&pps.scaling_lists_8x8,
num_8x8,
))
});
if let Some(backing) = &scaling_backing {
std.pScalingLists = &**backing;
}
Ok(OwnedStdPps {
std,
_scaling_backing: scaling_backing,
})
}
#[cfg(test)]
mod tests {
use super::*;
/// An SPS exercising every mapped field with distinct values. The sixteen flags
/// follow the Std bitfield order and strictly ALTERNATE false/true, so a swap of
/// any two adjacent flag mappings fails; `vui_parameters_present_flag` is true at
/// the SOURCE precisely because the conversion must NOT copy it (VUI skip).
fn full_sps() -> Sps {
Sps {
seq_parameter_set_id: 3,
profile_idc: 100,
// Flags, in Std bit order 0..15: F T F T F T F T F T F T F T F (T).
constraint_set1_flag: true,
constraint_set3_flag: true,
constraint_set5_flag: true,
mb_adaptive_frame_field_flag: true,
delta_pic_order_always_zero_flag: true,
gaps_in_frame_num_value_allowed_flag: true,
frame_cropping_flag: true,
vui_parameters_present_flag: true,
// constraint_set0/2/4, direct_8x8_inference, frame_mbs_only,
// separate_colour_plane, qpprime_y_zero_transform_bypass and
// seq_scaling_matrix_present stay false via ..Default.
level_idc: Level::L4_1,
chroma_format_idc: 1,
bit_depth_luma_minus8: 2,
bit_depth_chroma_minus8: 3,
log2_max_frame_num_minus4: 5,
pic_order_cnt_type: 0,
log2_max_pic_order_cnt_lsb_minus4: 6,
offset_for_non_ref_pic: -7,
offset_for_top_to_bottom_field: 3,
max_num_ref_frames: 4,
pic_width_in_mbs_minus1: 119,
pic_height_in_map_units_minus1: 67,
frame_crop_left_offset: 1,
frame_crop_right_offset: 2,
frame_crop_top_offset: 3,
frame_crop_bottom_offset: 4,
..Default::default()
}
}
/// A PPS over `sps` exercising every mapped field with distinct values. The
/// eight flags follow the Std bitfield order and strictly ALTERNATE true/false,
/// so a swap of any two adjacent flag mappings fails.
fn full_pps(sps: Sps) -> Pps {
Pps {
pic_parameter_set_id: 5,
seq_parameter_set_id: 3,
// Flags, in Std bit order 0..7: T F T F T F T F.
transform_8x8_mode_flag: true,
redundant_pic_cnt_present_flag: false,
constrained_intra_pred_flag: true,
deblocking_filter_control_present_flag: false,
weighted_pred_flag: true,
bottom_field_pic_order_in_frame_present_flag: false,
entropy_coding_mode_flag: true,
pic_scaling_matrix_present_flag: false,
num_slice_groups_minus1: 0,
num_ref_idx_l0_default_active_minus1: 2,
num_ref_idx_l1_default_active_minus1: 1,
weighted_bipred_idc: 2,
pic_init_qp_minus26: -3,
pic_init_qs_minus26: 4,
chroma_qp_index_offset: -2,
scaling_lists_4x4: [[0; 16]; 6],
scaling_lists_8x8: [[0; 64]; 6],
second_chroma_qp_index_offset: 6,
sps: std::rc::Rc::new(sps),
}
}
#[test]
fn every_mapped_sps_field_and_flag_round_trips_exactly() {
let sps = full_sps();
let owned = sps_to_std(&sps).unwrap();
let std = owned.std();
// The strictly alternating pattern of the fixture, bit for bit.
assert_eq!(std.flags.constraint_set0_flag(), 0);
assert_eq!(std.flags.constraint_set1_flag(), 1);
assert_eq!(std.flags.constraint_set2_flag(), 0);
assert_eq!(std.flags.constraint_set3_flag(), 1);
assert_eq!(std.flags.constraint_set4_flag(), 0);
assert_eq!(std.flags.constraint_set5_flag(), 1);
assert_eq!(std.flags.direct_8x8_inference_flag(), 0);
assert_eq!(std.flags.mb_adaptive_frame_field_flag(), 1);
assert_eq!(std.flags.frame_mbs_only_flag(), 0);
assert_eq!(std.flags.delta_pic_order_always_zero_flag(), 1);
assert_eq!(std.flags.separate_colour_plane_flag(), 0);
assert_eq!(std.flags.gaps_in_frame_num_value_allowed_flag(), 1);
assert_eq!(std.flags.qpprime_y_zero_transform_bypass_flag(), 0);
assert_eq!(std.flags.frame_cropping_flag(), 1);
assert_eq!(std.flags.seq_scaling_matrix_present_flag(), 0);
assert_eq!(
std.flags.vui_parameters_present_flag(),
0,
"true at the source, skipped by design"
);
assert_eq!(
std.profile_idc,
hh::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH
);
assert_eq!(
std.level_idc,
hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_4_1
);
assert_eq!(
std.chroma_format_idc,
hh::StdVideoH264ChromaFormatIdc_STD_VIDEO_H264_CHROMA_FORMAT_IDC_420
);
assert_eq!(std.seq_parameter_set_id, 3);
assert_eq!(std.bit_depth_luma_minus8, 2);
assert_eq!(std.bit_depth_chroma_minus8, 3, "distinct from luma");
assert_eq!(std.log2_max_frame_num_minus4, 5);
assert_eq!(
std.pic_order_cnt_type,
hh::StdVideoH264PocType_STD_VIDEO_H264_POC_TYPE_0
);
assert_eq!(std.offset_for_non_ref_pic, -7);
assert_eq!(std.offset_for_top_to_bottom_field, 3);
assert_eq!(std.log2_max_pic_order_cnt_lsb_minus4, 6);
assert_eq!(std.num_ref_frames_in_pic_order_cnt_cycle, 0);
assert_eq!(std.max_num_ref_frames, 4);
assert_eq!(std.pic_width_in_mbs_minus1, 119);
assert_eq!(std.pic_height_in_map_units_minus1, 67);
assert_eq!(std.frame_crop_left_offset, 1);
assert_eq!(std.frame_crop_right_offset, 2);
assert_eq!(std.frame_crop_top_offset, 3);
assert_eq!(std.frame_crop_bottom_offset, 4);
assert!(
std.pOffsetForRefFrame.is_null(),
"POC type 0 carries no offset array"
);
assert!(std.pScalingLists.is_null());
assert!(std.pSequenceParameterSetVui.is_null());
}
#[test]
fn poc_type_1_offsets_are_owned_and_survive_moving_the_wrapper() {
let mut sps = full_sps();
sps.pic_order_cnt_type = 1;
sps.num_ref_frames_in_pic_order_cnt_cycle = 3;
sps.offset_for_ref_frame[0] = 2;
sps.offset_for_ref_frame[1] = -1;
sps.offset_for_ref_frame[2] = 4;
// Box the wrapper AFTER conversion: a move that relocates the wrapper itself
// must not invalidate the pointer, because the backing is heap-pinned.
let owned = Box::new(sps_to_std(&sps).unwrap());
let std = owned.std();
assert_eq!(
std.pic_order_cnt_type,
hh::StdVideoH264PocType_STD_VIDEO_H264_POC_TYPE_1
);
assert_eq!(std.num_ref_frames_in_pic_order_cnt_cycle, 3);
assert!(!std.pOffsetForRefFrame.is_null());
// SAFETY: pOffsetForRefFrame points into `owned`'s boxed backing of exactly
// num_ref_frames_in_pic_order_cnt_cycle i32s, alive for this whole scope.
let offsets = unsafe { std::slice::from_raw_parts(std.pOffsetForRefFrame, 3) };
assert_eq!(offsets, [2, -1, 4]);
}
#[test]
fn sps_scaling_lists_convert_when_present_and_stay_absent_when_not() {
let mut sps = full_sps();
assert!(sps_to_std(&sps).unwrap().std().pScalingLists.is_null());
sps.seq_scaling_matrix_present_flag = true;
// Each list gets a DISTINCT fill byte: a permutation of lists, or an
// intra/inter reinterleave, cannot pass.
sps.scaling_lists_4x4 = std::array::from_fn(|i| [10 + i as u8; 16]);
sps.scaling_lists_8x8 = std::array::from_fn(|i| [20 + i as u8; 64]);
let owned = sps_to_std(&sps).unwrap();
let std = owned.std();
assert_eq!(std.flags.seq_scaling_matrix_present_flag(), 1);
assert!(!std.pScalingLists.is_null());
// SAFETY: pScalingLists points at `owned`'s boxed StdVideoH264ScalingLists,
// alive for this whole scope.
let lists = unsafe { &*std.pScalingLists };
// 4:2:0: bits 0-5 (the six 4x4 lists) + bits 6-7 (the two resolved 8x8
// lists), none deferred to driver-side defaults (the parser already
// resolved them).
assert_eq!(lists.scaling_list_present_mask, 0xFF);
assert_eq!(lists.use_default_scaling_matrix_mask, 0);
for i in 0..6 {
assert_eq!(lists.ScalingList4x4[i], [10 + i as u8; 16], "4x4 list {i}");
assert_eq!(lists.ScalingList8x8[i], [20 + i as u8; 64], "8x8 list {i}");
}
// 4:4:4 resolves all six 8x8 lists.
sps.chroma_format_idc = 3;
let owned = sps_to_std(&sps).unwrap();
// SAFETY: as above — the pointer targets `owned`'s boxed backing.
let lists = unsafe { &*owned.std().pScalingLists };
assert_eq!(lists.scaling_list_present_mask, 0xFFF);
}
#[test]
fn every_mapped_pps_field_and_flag_round_trips_exactly() {
let pps = full_pps(full_sps());
let owned = pps_to_std(&pps).unwrap();
let std = owned.std();
// The strictly alternating pattern of the fixture, bit for bit.
assert_eq!(std.flags.transform_8x8_mode_flag(), 1);
assert_eq!(std.flags.redundant_pic_cnt_present_flag(), 0);
assert_eq!(std.flags.constrained_intra_pred_flag(), 1);
assert_eq!(std.flags.deblocking_filter_control_present_flag(), 0);
assert_eq!(std.flags.weighted_pred_flag(), 1);
assert_eq!(std.flags.bottom_field_pic_order_in_frame_present_flag(), 0);
assert_eq!(std.flags.entropy_coding_mode_flag(), 1);
assert_eq!(std.flags.pic_scaling_matrix_present_flag(), 0);
assert_eq!(std.seq_parameter_set_id, 3);
assert_eq!(std.pic_parameter_set_id, 5);
assert_eq!(std.num_ref_idx_l0_default_active_minus1, 2);
assert_eq!(std.num_ref_idx_l1_default_active_minus1, 1);
assert_eq!(
std.weighted_bipred_idc,
hh::StdVideoH264WeightedBipredIdc_STD_VIDEO_H264_WEIGHTED_BIPRED_IDC_IMPLICIT
);
assert_eq!(std.pic_init_qp_minus26, -3);
assert_eq!(std.pic_init_qs_minus26, 4);
assert_eq!(std.chroma_qp_index_offset, -2);
assert_eq!(std.second_chroma_qp_index_offset, 6);
assert!(std.pScalingLists.is_null());
}
#[test]
fn pps_scaling_lists_declare_8x8_present_only_under_transform_8x8_mode() {
let mut pps = full_pps(full_sps());
pps.pic_scaling_matrix_present_flag = true;
// Distinct fill bytes per list, as in the SPS test.
pps.scaling_lists_4x4 = std::array::from_fn(|i| [30 + i as u8; 16]);
pps.scaling_lists_8x8 = std::array::from_fn(|i| [40 + i as u8; 64]);
let owned = pps_to_std(&pps).unwrap();
// SAFETY: pScalingLists points at `owned`'s boxed backing, alive here.
let lists = unsafe { &*owned.std().pScalingLists };
assert_eq!(lists.scaling_list_present_mask, 0xFF);
assert_eq!(lists.use_default_scaling_matrix_mask, 0);
for i in 0..6 {
assert_eq!(lists.ScalingList4x4[i], [30 + i as u8; 16], "4x4 list {i}");
assert_eq!(lists.ScalingList8x8[i], [40 + i as u8; 64], "8x8 list {i}");
}
// Without transform_8x8_mode the parser never resolved the 8x8 arrays: only
// the six 4x4 lists may be declared present.
pps.transform_8x8_mode_flag = false;
let owned = pps_to_std(&pps).unwrap();
// SAFETY: as above — the pointer targets `owned`'s boxed backing.
let lists = unsafe { &*owned.std().pScalingLists };
assert_eq!(lists.scaling_list_present_mask, 0x3F);
assert_eq!(lists.use_default_scaling_matrix_mask, 0);
}
#[test]
fn a_stale_cycle_count_on_a_type_0_sps_converts_to_zero_offsets() {
let mut sps = full_sps();
sps.pic_order_cnt_type = 0;
// A stale/corrupt count with no POC-type-1 semantics behind it: the Std
// struct must not claim a cycle over a null array.
sps.num_ref_frames_in_pic_order_cnt_cycle = 5;
let owned = sps_to_std(&sps).unwrap();
assert_eq!(
owned.std().num_ref_frames_in_pic_order_cnt_cycle,
0,
"count and pointer derive from one condition"
);
assert!(owned.std().pOffsetForRefFrame.is_null());
}
#[test]
fn the_25fps_vectors_own_parameter_sets_convert_cleanly() {
use std::io::Cursor;
use cros_codecs::codec::h264::parser::Nalu;
use cros_codecs::codec::h264::parser::NaluType;
use cros_codecs::codec::h264::parser::Parser;
// The same vendored vector pf-bitstream's tests plan, same relative path.
const TEST_25FPS: &[u8] = include_bytes!(
"../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264"
);
let mut cursor = Cursor::new(TEST_25FPS);
let mut parser = Parser::default();
let (mut sps_seen, mut pps_seen) = (false, false);
while let Ok(nalu) = Nalu::next(&mut cursor) {
match nalu.header.type_ {
NaluType::Sps if !sps_seen => {
let sps = parser.parse_sps(&nalu).expect("the vector's SPS parses");
let owned = sps_to_std(sps).expect("the vector's SPS converts");
let std = owned.std();
// The vector's own goldens: 320x240 progressive Main profile.
assert_eq!(
std.profile_idc,
hh::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_MAIN
);
assert_eq!((std.pic_width_in_mbs_minus1 + 1) * 16, 320);
assert_eq!((std.pic_height_in_map_units_minus1 + 1) * 16, 240);
assert_eq!(std.flags.frame_mbs_only_flag(), 1);
assert_eq!(std.flags.vui_parameters_present_flag(), 0);
sps_seen = true;
}
NaluType::Pps if !pps_seen => {
let pps = parser.parse_pps(&nalu).expect("the vector's PPS parses");
let owned = pps_to_std(pps).expect("the vector's PPS converts");
assert_eq!(owned.std().pic_parameter_set_id, 0);
pps_seen = true;
}
_ => {}
}
if sps_seen && pps_seen {
break;
}
}
assert!(sps_seen && pps_seen, "the vector opens with SPS + PPS");
}
#[test]
fn unrepresentable_parameter_sets_are_rejected_not_approximated() {
let mut sps = full_sps();
sps.profile_idc = 110; // High10: no StdVideoH264ProfileIdc code point.
assert_eq!(
sps_to_std(&sps).unwrap_err(),
ParamsError::UnmappableProfileIdc(110)
);
let mut pps = full_pps(full_sps());
pps.num_slice_groups_minus1 = 1;
assert_eq!(pps_to_std(&pps).unwrap_err(), ParamsError::SliceGroups(1));
let mut pps = full_pps(full_sps());
pps.weighted_bipred_idc = 3;
assert_eq!(
pps_to_std(&pps).unwrap_err(),
ParamsError::InvalidWeightedBipredIdc(3)
);
}
}