Files
punktfunk/crates/pf-bitstream/src/h264.rs
T
enricobuehler dc0766b2f3 fix(client): the native rung now follows the stream's colour and reports true decode latency
The round-4 residuals, closed after the WP-D hardware verdict:

- VUI colour plumbing (the one silent-wrong): the picture's ACTIVE SPS's
  colour signalling (H.273 code points + range, with E.2.1's 'unspecified'
  inference where the VUI is silent — the vendored parser's defaults ARE the
  inferred values, verified) rides PicturePlan -> DecodedVkFrame ->
  NativeVkFrame per frame, never latched: the Windows host switches an HDR
  desktop to PQ/BT.2020 IN-BAND while the Welcome still says SDR. Before
  this, the native path would have painted PQ washed out, silently.
- Native decode-latency stat: the deliberately-deferred NativeVk arm of the
  pump's sampled once-per-stats-window decode measurement now feeds - the
  frame's (semaphore, semaphore_value) is the decode-done signal, resolved
  through the shipped ledger before a bounded, pure-measurement
  vkWaitSemaphores (VkH264Decoder::wait_decoded).
- The renegotiation-teardown window is settled as NO HOLE: rebuild_state now
  documents the full safety argument (graveyarded pools stay intact under
  presenter holds, tokens route strictly by generation, session objects die
  only post-drain with the generation gate INSIDE read_status), and the two
  backend comments that wrongly claimed stale pools were 'gone' are fixed.
- VK_KHR_unified_image_layouts stays deferred (fleet drivers lack it).

Adversarial review round 6: 3 minor findings (2 doc fixes applied; the
SPS-replaced-without-PPS-resend divergence stays a documented envelope
assumption - hosts re-send both at every keyframe, and a hardening
PlanWarning could cost real frames on a false positive).

Gates: fmt clean; clippy -D warnings zero (mac + pf-lxcheck2 container,
incl. pf-client-core/pf-presenter); tests 45+30+53 mac, 30+121+53 container.
2026-08-05 22:22:03 +02:00

2602 lines
104 KiB
Rust

// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file (vendor/cros-codecs/LICENSE).
//
// Adapted from cros-codecs `decoder/stateless/h264.rs` (see
// vendor/cros-codecs/PROVENANCE.md for the snapshot pin). The spec machinery — POC
// computation (8.2.1), frame_num-gap handling (8.2.5.2), reference list initialization
// and modification (8.2.4), sliding-window and adaptive MMCO marking (8.2.5), DPB
// bumping/output (C.4.5.3) — is ported faithfully and keeps upstream's structure and
// spec-section comments so future upstream diffs stay legible. Stripped: the
// StatelessDecoder/backend trait plumbing, fd/event machinery, pooled-buffer handling,
// and the interlaced field-splitting paths (the envelope gate below rejects interlaced
// streams outright).
//! Per-AU H.264 planning: [`H264Planner::plan_au`] turns one access unit exactly as the
//! pump hands it to a decoder (Annex-B, parameter sets + the slices of one picture) into
//! an [`AuPlan`] — everything a stateless hardware decoder needs before submission and
//! nothing it has to re-derive: parsed headers, POC, per-slice reference lists (with
//! long-term/MMCO state, which host RFI recovery leans on) and the DPB delta.
//!
//! Concealment posture: a `frame_num` gap or a reference that is not in the DPB is a
//! [`PlanWarning`], never an error — the session layer sees the warning and requests
//! recovery while planning continues. [`PlanError`] is reserved for AUs that cannot be
//! planned at all.
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::io::Cursor;
use std::mem;
use std::ops::Range;
use std::rc::Rc;
use cros_codecs::codec::h264::dpb::Dpb;
use cros_codecs::codec::h264::dpb::DpbEntry;
use cros_codecs::codec::h264::dpb::DpbPicRefList;
use cros_codecs::codec::h264::dpb::MmcoError;
use cros_codecs::codec::h264::dpb::ReferencePicLists;
use cros_codecs::codec::h264::parser::MaxLongTermFrameIdx;
use cros_codecs::codec::h264::parser::Nalu;
use cros_codecs::codec::h264::parser::NaluType;
use cros_codecs::codec::h264::parser::Parser;
use cros_codecs::codec::h264::parser::Pps;
use cros_codecs::codec::h264::parser::RefPicListModification;
use cros_codecs::codec::h264::parser::Slice;
use cros_codecs::codec::h264::parser::SliceType;
use cros_codecs::codec::h264::parser::Sps;
use cros_codecs::codec::h264::picture::Field;
use cros_codecs::codec::h264::picture::FieldRank;
use cros_codecs::codec::h264::picture::IsIdr;
use cros_codecs::codec::h264::picture::PictureData;
use cros_codecs::codec::h264::picture::RcPictureData;
use cros_codecs::codec::h264::picture::Reference;
use cros_codecs::Resolution;
use tracing::trace;
pub use cros_codecs::codec::h264::parser::Level;
pub use cros_codecs::codec::h264::parser::SliceHeader;
use crate::sei;
pub use crate::sei::RecoveryPoint;
/// Stable identity of a stored picture, monotonically increasing per stored picture.
///
/// This is what backends map to hardware DPB slots. Indices into the live DPB `Vec`
/// shift on bumping and must never be exposed; the `Dpb<PicId>` handle parameter carries
/// this id instead.
pub type PicId = u64;
/// Everything a backend needs to submit one access unit.
#[derive(Debug, Clone)]
pub struct AuPlan {
pub picture: PicturePlan,
pub slices: Vec<SlicePlan>,
pub dpb: DpbUpdate,
pub warnings: Vec<PlanWarning>,
/// The SPS the planner activated for this AU — the one [`Self::picture`]'s
/// parameters derive from (the FIRST slice's PPS's SPS; a later slice may
/// legally reference another PPS, and that drift deliberately does not reach
/// here). Cloned out of the parser's table so backends build their parameter
/// objects from exactly what was activated, never by re-parsing the AU.
pub sps: Rc<Sps>,
/// The PPS the picture was begun with (the first slice's), same contract as
/// [`Self::sps`]. Its `sps` field is the same `Rc` as [`Self::sps`].
pub pps: Rc<Pps>,
}
/// Per-picture parameters, captured after 8.2.1 POC derivation and before end-of-picture
/// marking (the values a hardware picture-parameters struct wants).
#[derive(Debug, Clone)]
pub struct PicturePlan {
pub is_idr: bool,
pub nal_ref_idc: u8,
pub is_reference: bool,
pub frame_num: u16,
pub top_field_order_cnt: i32,
pub bottom_field_order_cnt: i32,
/// The final PicOrderCnt of the picture (min of top/bottom for a frame).
pub pic_order_cnt: i32,
pub coded_width: u32,
pub coded_height: u32,
/// Conformance-window crop (7.4.2.1.1), in luma samples of the coded picture.
pub display_crop: DisplayCrop,
/// Colour signalling from the ACTIVE SPS's VUI (E.2.1 inference where absent).
/// Per picture, like [`Self::display_crop`], never latched at session start:
/// the Windows host switches an HDR desktop to PQ/BT.2020 IN-BAND with a new
/// SPS mid-stream, so a backend that captured the first AU's colour would
/// paint HDR frames washed out.
pub colour: ColourDescription,
pub profile_idc: u8,
pub level_idc: Level,
pub bit_depth_luma_minus8: u8,
pub bit_depth_chroma_minus8: u8,
pub chroma_format_idc: u8,
/// DPB size in frames per A.3.1 — backends size their slot pool from this.
pub max_dpb_frames: usize,
pub recovery_point: Option<RecoveryPoint>,
}
/// The region of the coded picture that is actually displayed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DisplayCrop {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
/// One picture's colour signalling: raw H.273 code points off the active SPS's
/// VUI. When the VUI (or its `video_signal_type`/`colour_description` blocks) is
/// absent these hold E.2.1's INFERRED values — 2/2/2 ("unspecified") with limited
/// range — never a raw struct-zero (0 is a reserved code point no real stream
/// means). That matches the CICP libavcodec reports for such streams, so backends
/// forward these untouched and the consumer's CSC resolves "unspecified" to its
/// SDR default.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ColourDescription {
pub colour_primaries: u8,
pub transfer_characteristics: u8,
pub matrix_coefficients: u8,
/// `video_full_range_flag` (E.2.1 infers limited range when absent).
pub video_full_range: bool,
}
/// One slice NALU of the picture, with its reference lists fully derived.
#[derive(Debug, Clone)]
pub struct SlicePlan {
/// Byte range of the slice NALU in the input AU, start code included — hardware
/// decoders take the raw bitstream, so the plan points instead of copying.
pub data: Range<usize>,
/// The parsed slice header, as the vendored parser produced it.
pub header: SliceHeader,
pub ref_list0: Vec<RefPic>,
pub ref_list1: Vec<RefPic>,
}
/// A reference list entry: the minimum every backend picparams format needs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RefPic {
pub id: PicId,
/// The stored picture's 8.2.1 field order counts. Equal for a progressive frame
/// UNLESS the PPS set `bottom_field_pic_order_in_frame_present_flag` and the
/// slice carried a nonzero `delta_pic_order_cnt_bottom` — backend picparams
/// formats want the pair, and collapsing to one value would fabricate the bottom
/// count. After an MMCO 5 these are the picture's REBASED values (8.2.5.4.5),
/// which is what later AUs reference it by — see [`PlanWarning::Mmco5Rebase`].
pub top_field_order_cnt: i32,
pub bottom_field_order_cnt: i32,
pub is_long_term: bool,
/// `frame_num` for short-term references, `LongTermFrameIdx` for long-term ones —
/// the pair DXVA and Vulkan both key reference pictures by.
pub frame_num_or_lt_idx: u16,
}
/// The DPB delta of one planned AU: what to allocate, what is display-ready, what can
/// be freed.
#[derive(Debug, Clone, Default)]
pub struct DpbUpdate {
/// The id assigned to this AU's picture — allocate a surface for it.
pub stored: Option<PicId>,
/// Display-ready pictures, in output (bumping) order.
pub outputs: Vec<PicId>,
/// Pictures the planner will never reference again; free once displayed.
pub removed: Vec<PicId>,
}
/// Concealment signals: planning continues, the session layer requests recovery.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlanWarning {
/// `frame_num` skipped a value — at least one reference AU was lost upstream.
FrameNumGap { expected: u16, got: u16 },
/// A slice or MMCO named a reference picture the DPB does not hold.
MissingReference {
context: &'static str,
detail: String,
},
/// The AU's NALU walk stopped early — a malformed NALU with real data behind it,
/// or a slice belonging to another picture (mis-split AU). The plan covers only
/// the slices before the cut; `offset` is the byte position of the cut in the AU.
TruncatedAu { offset: usize },
/// The AU carried an MMCO 5 (8.2.5.4.5): the DPB was drained and the CURRENT
/// picture's stored frame_num/POC were rebased to zero AFTER its plan was
/// captured. Spec-legal and fully planned — the [`PicturePlan`] holds the
/// pre-rebase 8.2.1 values a decoder submits with, while later AUs reference the
/// picture by its rebased values ([`RefPic`] carries the stored pair). punktfunk
/// hosts never emit MMCO 5, so this warning is the field signal if that
/// assumption ever breaks.
Mmco5Rebase,
}
/// The AU cannot be planned at all.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlanError {
Parse(String),
/// Legal H.264, but outside what punktfunk hosts emit (clients only decode
/// punktfunk hosts, so this is a stream-integrity failure, not a feature gap).
OutsideEnvelope(&'static str),
NoActiveParamSet {
pps_id: u8,
},
/// [`H264Planner::flush`] discarded the decoding state; planning resumes only at
/// an IDR (the port of upstream's `Reset` gating).
AwaitingIdr,
}
impl std::fmt::Display for PlanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlanError::Parse(msg) => write!(f, "parse error: {msg}"),
PlanError::OutsideEnvelope(what) => {
write!(f, "outside the punktfunk decode envelope: {what}")
}
PlanError::NoActiveParamSet { pps_id } => {
write!(f, "slice references PPS {pps_id}, which has not been seen")
}
PlanError::AwaitingIdr => {
write!(f, "flushed: waiting for an IDR to resume planning")
}
}
}
}
impl std::error::Error for PlanError {}
/// Keeps track of the last values seen for negotiation purposes.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct NegotiationInfo {
coded_resolution: Resolution,
profile_idc: u8,
bit_depth_luma_minus8: u8,
bit_depth_chroma_minus8: u8,
chroma_format_idc: u8,
max_dpb_frames: usize,
interlaced: bool,
}
impl From<&Sps> for NegotiationInfo {
fn from(sps: &Sps) -> Self {
NegotiationInfo {
coded_resolution: Resolution::from((sps.width(), sps.height())),
profile_idc: sps.profile_idc,
bit_depth_luma_minus8: sps.bit_depth_luma_minus8,
bit_depth_chroma_minus8: sps.bit_depth_chroma_minus8,
chroma_format_idc: sps.chroma_format_idc,
max_dpb_frames: sps.max_dpb_frames(),
interlaced: !sps.frame_mbs_only_flag,
}
}
}
#[derive(Copy, Clone, Debug)]
enum RefPicList {
RefPicList0,
RefPicList1,
}
/// Cached variables from the previous reference picture (8.2.1).
struct PrevReferencePicInfo {
frame_num: u32,
has_mmco_5: bool,
top_field_order_cnt: i32,
pic_order_cnt_msb: i32,
pic_order_cnt_lsb: i32,
field: Field,
}
impl Default for PrevReferencePicInfo {
fn default() -> Self {
Self {
frame_num: Default::default(),
has_mmco_5: Default::default(),
top_field_order_cnt: Default::default(),
pic_order_cnt_msb: Default::default(),
pic_order_cnt_lsb: Default::default(),
field: Field::Frame,
}
}
}
impl PrevReferencePicInfo {
fn fill(&mut self, pic: &PictureData) {
self.has_mmco_5 = pic.has_mmco_5;
self.top_field_order_cnt = pic.top_field_order_cnt;
self.pic_order_cnt_msb = pic.pic_order_cnt_msb;
self.pic_order_cnt_lsb = pic.pic_order_cnt_lsb;
self.field = pic.field;
self.frame_num = pic.frame_num;
}
}
/// Cached variables from the previous picture (8.2.1).
#[derive(Default)]
struct PrevPicInfo {
frame_num: u32,
frame_num_offset: u32,
has_mmco_5: bool,
}
impl PrevPicInfo {
fn fill(&mut self, pic: &PictureData) {
self.frame_num = pic.frame_num;
self.has_mmco_5 = pic.has_mmco_5;
self.frame_num_offset = pic.frame_num_offset;
}
}
/// Used to track that `first_mb_in_slice` increases monotonically (7.4.3).
///
/// Upstream tracks this too, but with an inverted comparison that fires on every
/// well-formed slice; corrected here (strictly increasing across a picture's slices),
/// and `None`/vacant marks "no slice seen yet" so the first slice never trips it.
enum CurrentMacroblockTracking {
SeparateColorPlane(BTreeMap<u8, u32>),
NonSeparateColorPlane(Option<u32>),
}
/// State of the picture being planned, spanning the slices of one AU.
struct CurrentPicState {
/// Data for the current picture as extracted from the stream.
pic: PictureData,
/// PPS at the time of the current picture. Follows the slices — a later slice may
/// reference another PPS — and feeds end-of-picture marking, as upstream does.
pps: Rc<Pps>,
/// The PPS the picture was BEGUN with. [`H264Planner::picture_plan`] reads this
/// snapshot, like upstream's `start_picture`, so per-picture parameters cannot
/// drift to a later slice's PPS.
first_slice_pps: Rc<Pps>,
/// The id backends will know this picture by (upstream: the backend picture).
id: PicId,
/// Reference picture lists, derived once per picture, indexed per slice.
ref_pic_lists: ReferencePicLists,
current_macroblock: CurrentMacroblockTracking,
}
/// Plans H.264 access units for stateless hardware decoders.
///
/// Owns the vendored parser and DPB plus the POC/marking state that upstream keeps in
/// `H264DecoderState`. One instance per elementary stream; feed AUs in decode order.
#[derive(Default)]
pub struct H264Planner {
parser: Parser,
negotiation_info: NegotiationInfo,
dpb: Dpb<PicId>,
prev_ref_pic_info: PrevReferencePicInfo,
prev_pic_info: PrevPicInfo,
max_long_term_frame_idx: MaxLongTermFrameIdx,
/// Next [`PicId`] to hand out (upstream: the backend allocates here).
next_pic_id: PicId,
/// Display-ready pictures accumulated while planning (upstream: the decoder's
/// ready queue). Not cleared on a failed AU — the next emitted [`DpbUpdate`]
/// carries them, so an error can never swallow a frame.
pending_outputs: Vec<PicId>,
/// Ids the last emitted [`DpbUpdate`] left alive: the baseline for `removed`.
/// Kept across failed AUs so interim evictions are reported, never dropped.
reported_live: BTreeSet<PicId>,
/// Set by [`Self::flush`]: planning resumes only at an IDR (upstream: `Reset`).
awaiting_idr: bool,
}
impl H264Planner {
pub fn new() -> Self {
Default::default()
}
/// Plan one access unit: Annex-B bytes containing SPS/PPS/SEI/AUD NALUs plus the
/// 1..N slice NALUs of exactly one picture.
///
/// After a [`PlanError`] the planner state is best-effort; the session should
/// request an IDR before feeding more AUs. Outputs and removals queued by a failed
/// AU are retained and emitted with the next successful plan (or [`Self::flush`]) —
/// never discarded.
pub fn plan_au(&mut self, au: &[u8]) -> Result<AuPlan, PlanError> {
let mut warnings = Vec::new();
let mut slices = Vec::new();
let mut recovery_point = None;
let mut current: Option<CurrentPicState> = None;
let mut saw_nalu = false;
let mut cursor = Cursor::new(au);
loop {
let nalu = match Nalu::next(&mut cursor) {
Ok(nalu) => nalu,
Err(_) => {
// End of the AU — or a NALU whose header failed to parse (reserved
// type, truncated byte). A start code past the cursor means real
// data was cut off: degrade to a concealment signal covering the
// slices already planned. Without one this is benign trailing
// padding — a NALU's own payload is emulation-prevented and cannot
// contain a start code.
let pos = (cursor.position() as usize).min(au.len());
if au[pos..].windows(3).any(|w| w == [0x00, 0x00, 0x01]) {
warnings.push(PlanWarning::TruncatedAu { offset: pos });
}
break;
}
};
saw_nalu = true;
// After `Nalu::next` the cursor sits on the NAL header byte; `offset` is the
// start-code length and `size` the NALU payload length, which pins the
// NALU's absolute byte range in the AU without copying.
let nalu_offset = cursor.position() as usize;
let range = (nalu_offset - nalu.offset)..(nalu_offset + nalu.size);
debug_assert_eq!(&au[range.clone()], nalu.data.as_ref());
match nalu.header.type_ {
NaluType::Sps => {
let sps = self.parser.parse_sps(&nalu).map_err(PlanError::Parse)?;
Self::check_envelope(sps)?;
}
NaluType::Pps => {
self.parser.parse_pps(&nalu).map_err(PlanError::Parse)?;
}
NaluType::Sei => {
match sei::parse_recovery_point(nalu.as_ref().get(1..).unwrap_or(&[])) {
Ok(Some(rp)) => recovery_point = Some(rp),
Ok(None) => {}
// A broken SEI must not cost the picture it decorates.
Err(err) => trace!("ignoring unparseable SEI NALU: {err}"),
}
}
NaluType::Slice | NaluType::SliceIdr => {
// Upstream's `Reset` gating: after a flush, only an IDR restarts
// the decoding process.
if current.is_none() && self.awaiting_idr {
if !nalu.header.idr_pic_flag {
return Err(PlanError::AwaitingIdr);
}
self.awaiting_idr = false;
}
let slice = match self.parser.parse_slice_header(nalu) {
Ok(slice) => slice,
Err(err) => return Err(Self::slice_parse_error(err)),
};
match &current {
None => current = Some(self.begin_picture(&slice, &mut warnings)?),
// Upstream would finish the picture and begin another; our
// contract is one picture per AU, so a second first-slice means
// the pump upstream of us is broken.
Some(_) if slice.header.first_mb_in_slice == 0 => {
return Err(PlanError::OutsideEnvelope(
"more than one coded picture in one access unit",
));
}
Some(cur) => {
// Mis-split-AU guard: a continuation slice must belong to
// the picture the first slice began (7.4.3: same frame_num,
// same IDR-ness). A foreign slice and everything after it
// are dropped behind a concealment signal.
if u32::from(slice.header.frame_num) != cur.pic.frame_num
|| slice.nalu.header.idr_pic_flag
!= matches!(cur.pic.is_idr, IsIdr::Yes { .. })
{
warnings.push(PlanWarning::TruncatedAu {
offset: range.start,
});
break;
}
}
}
let cur = current.as_mut().expect("a picture was begun above");
slices.push(self.plan_slice(cur, slice, range, &mut warnings)?);
}
NaluType::SliceDpa | NaluType::SliceDpb | NaluType::SliceDpc => {
return Err(PlanError::OutsideEnvelope("data-partitioned slices"));
}
other => trace!("skipping NAL unit type {other:?}"),
}
}
if !saw_nalu {
return Err(PlanError::Parse("no NAL units in access unit".into()));
}
let cur = current
.ok_or_else(|| PlanError::Parse("access unit contains no coded picture".into()))?;
// Captured before finish_picture: MMCO5 rewrites the stored POC afterwards, but
// backends submit the picture with its 8.2.1 values.
let picture = Self::picture_plan(&cur, recovery_point);
// The activated parameter sets ride out with the plan (AuPlan field docs);
// cloned before finish_picture consumes `cur`.
let pps = Rc::clone(&cur.first_slice_pps);
let sps = Rc::clone(&pps.sps);
let stored = self.finish_picture(cur, &mut warnings)?;
// `removed` is the delta against what the backend last SAW alive, not against
// this call's start — a failed AU in between may have evicted pictures, and
// those removals must still be reported here.
let live_after = self.live_ids();
let mut previously_live = mem::take(&mut self.reported_live);
previously_live.insert(stored);
let removed = previously_live.difference(&live_after).copied().collect();
self.reported_live = live_after;
Ok(AuPlan {
picture,
slices,
dpb: DpbUpdate {
stored: Some(stored),
outputs: mem::take(&mut self.pending_outputs),
removed,
},
warnings,
sps,
pps,
})
}
/// Drain the DPB: every still-buffered picture becomes display-ready and every id is
/// released. The session calls this at teardown or a stream discontinuity.
///
/// The 8.2.1/8.2.5 decoding state is discarded with the pictures; planning resumes
/// only at an IDR ([`PlanError::AwaitingIdr`] until then). Parameter sets survive —
/// per 7.4.1.2 they persist until replaced.
pub fn flush(&mut self) -> DpbUpdate {
let mut removed = mem::take(&mut self.reported_live);
removed.extend(self.live_ids());
self.drain_dpb();
self.prev_ref_pic_info = Default::default();
self.prev_pic_info = Default::default();
self.max_long_term_frame_idx = Default::default();
self.negotiation_info = Default::default();
self.awaiting_idr = true;
DpbUpdate {
stored: None,
outputs: mem::take(&mut self.pending_outputs),
removed: removed.into_iter().collect(),
}
}
/// The envelope gate: punktfunk clients only decode punktfunk hosts, and no host
/// ever emits interlaced video or separate-colour-plane coding.
fn check_envelope(sps: &Sps) -> Result<(), PlanError> {
if !sps.frame_mbs_only_flag {
return Err(PlanError::OutsideEnvelope(
"interlaced stream (frame_mbs_only_flag == 0)",
));
}
if sps.separate_colour_plane_flag {
return Err(PlanError::OutsideEnvelope(
"separate colour plane coding (separate_colour_plane_flag == 1)",
));
}
// A.3.1 caps the DPB at 16 frames; the only route past the cap is the VUI's
// max_dec_frame_buffering, an unbounded ue(v) the vendored parser reads
// uncapped. No hardware decoder implements a deeper DPB — a larger value is a
// corrupt (or hostile) VUI, not a feature request — and backends size real
// slot pools from this number, so it is gated here, at SPS activation.
if sps.max_dpb_frames() > 16 {
return Err(PlanError::OutsideEnvelope(
"DPB deeper than 16 frames (max_dec_frame_buffering)",
));
}
Ok(())
}
/// Map a vendored slice-header parse failure, sniffing the missing-PPS message so it
/// surfaces as [`PlanError::NoActiveParamSet`]. The prefix match is best-effort: if
/// an upstream re-sync rewords it, the error degrades to `Parse`, not silence.
fn slice_parse_error(err: String) -> PlanError {
match err.strip_prefix("Could not get PPS for pic_parameter_set_id ") {
Some(id) => PlanError::NoActiveParamSet {
pps_id: id.trim().parse().unwrap_or(0),
},
None => PlanError::Parse(err),
}
}
/// Ids of every picture the DPB currently holds (non-existing gap placeholders carry
/// no id and are invisible to backends by design).
fn live_ids(&self) -> BTreeSet<PicId> {
self.dpb
.entries()
.iter()
.filter_map(|entry| entry.reference)
.collect()
}
fn compute_pic_order_count(
&mut self,
pic: &mut PictureData,
sps: &Sps,
) -> Result<(), PlanError> {
match pic.pic_order_cnt_type {
// Spec 8.2.1.1
0 => {
let prev_pic_order_cnt_msb;
let prev_pic_order_cnt_lsb;
if matches!(pic.is_idr, IsIdr::Yes { .. }) {
prev_pic_order_cnt_lsb = 0;
prev_pic_order_cnt_msb = 0;
} else if self.prev_ref_pic_info.has_mmco_5 {
if !matches!(self.prev_ref_pic_info.field, Field::Bottom) {
prev_pic_order_cnt_msb = 0;
prev_pic_order_cnt_lsb = self.prev_ref_pic_info.top_field_order_cnt;
} else {
prev_pic_order_cnt_msb = 0;
prev_pic_order_cnt_lsb = 0;
}
} else {
prev_pic_order_cnt_msb = self.prev_ref_pic_info.pic_order_cnt_msb;
prev_pic_order_cnt_lsb = self.prev_ref_pic_info.pic_order_cnt_lsb;
}
let max_pic_order_cnt_lsb = 1 << (sps.log2_max_pic_order_cnt_lsb_minus4 + 4);
// 8.2.1.1 compares against prevPicOrderCntLsb — the DERIVED value,
// which is 0 or the previous TopFieldOrderCnt after an MMCO5 — in BOTH
// wrap branches. Upstream reads the raw stored lsb in the first branch;
// deliberate divergence from upstream here, in favour of spec
// conformance.
pic.pic_order_cnt_msb = if (pic.pic_order_cnt_lsb < prev_pic_order_cnt_lsb)
&& (prev_pic_order_cnt_lsb - pic.pic_order_cnt_lsb >= max_pic_order_cnt_lsb / 2)
{
prev_pic_order_cnt_msb + max_pic_order_cnt_lsb
} else if (pic.pic_order_cnt_lsb > prev_pic_order_cnt_lsb)
&& (pic.pic_order_cnt_lsb - prev_pic_order_cnt_lsb > max_pic_order_cnt_lsb / 2)
{
prev_pic_order_cnt_msb - max_pic_order_cnt_lsb
} else {
prev_pic_order_cnt_msb
};
if !matches!(pic.field, Field::Bottom) {
pic.top_field_order_cnt = pic.pic_order_cnt_msb + pic.pic_order_cnt_lsb;
}
if !matches!(pic.field, Field::Top) {
if matches!(pic.field, Field::Frame) {
pic.bottom_field_order_cnt =
pic.top_field_order_cnt + pic.delta_pic_order_cnt_bottom;
} else {
pic.bottom_field_order_cnt = pic.pic_order_cnt_msb + pic.pic_order_cnt_lsb;
}
}
}
// Spec 8.2.1.2
1 => {
if self.prev_pic_info.has_mmco_5 {
self.prev_pic_info.frame_num_offset = 0;
}
if matches!(pic.is_idr, IsIdr::Yes { .. }) {
pic.frame_num_offset = 0;
} else if self.prev_pic_info.frame_num > pic.frame_num {
pic.frame_num_offset =
self.prev_pic_info.frame_num_offset + sps.max_frame_num();
} else {
pic.frame_num_offset = self.prev_pic_info.frame_num_offset;
}
let mut abs_frame_num = if sps.num_ref_frames_in_pic_order_cnt_cycle != 0 {
pic.frame_num_offset + pic.frame_num
} else {
0
};
if pic.nal_ref_idc == 0 && abs_frame_num > 0 {
abs_frame_num -= 1;
}
let mut expected_pic_order_cnt = 0;
if abs_frame_num > 0 {
if sps.num_ref_frames_in_pic_order_cnt_cycle == 0 {
return Err(PlanError::Parse(
"invalid num_ref_frames_in_pic_order_cnt_cycle".into(),
));
}
let pic_order_cnt_cycle_cnt =
(abs_frame_num - 1) / sps.num_ref_frames_in_pic_order_cnt_cycle as u32;
let frame_num_in_pic_order_cnt_cycle =
(abs_frame_num - 1) % sps.num_ref_frames_in_pic_order_cnt_cycle as u32;
expected_pic_order_cnt =
pic_order_cnt_cycle_cnt as i32 * sps.expected_delta_per_pic_order_cnt_cycle;
assert!(frame_num_in_pic_order_cnt_cycle < 255);
// NOTE: upstream sums the full cycle here where 8.2.1.2 sums
// frame_num_in_pic_order_cnt_cycle + 1 entries; ported as-is —
// punktfunk hosts emit pic_order_cnt_type 0 only.
let cycle = usize::from(sps.num_ref_frames_in_pic_order_cnt_cycle);
for offset in &sps.offset_for_ref_frame[..cycle] {
expected_pic_order_cnt += offset;
}
}
if pic.nal_ref_idc == 0 {
expected_pic_order_cnt += sps.offset_for_non_ref_pic;
}
if matches!(pic.field, Field::Frame) {
pic.top_field_order_cnt = expected_pic_order_cnt + pic.delta_pic_order_cnt0;
pic.bottom_field_order_cnt = pic.top_field_order_cnt
+ sps.offset_for_top_to_bottom_field
+ pic.delta_pic_order_cnt1;
} else if !matches!(pic.field, Field::Bottom) {
pic.top_field_order_cnt = expected_pic_order_cnt + pic.delta_pic_order_cnt0;
} else {
pic.bottom_field_order_cnt = expected_pic_order_cnt
+ sps.offset_for_top_to_bottom_field
+ pic.delta_pic_order_cnt0;
}
}
// Spec 8.2.1.3
2 => {
if self.prev_pic_info.has_mmco_5 {
self.prev_pic_info.frame_num_offset = 0;
}
if matches!(pic.is_idr, IsIdr::Yes { .. }) {
pic.frame_num_offset = 0;
} else if self.prev_pic_info.frame_num > pic.frame_num {
pic.frame_num_offset =
self.prev_pic_info.frame_num_offset + sps.max_frame_num();
} else {
pic.frame_num_offset = self.prev_pic_info.frame_num_offset;
}
let pic_order_cnt = if matches!(pic.is_idr, IsIdr::Yes { .. }) {
0
} else if pic.nal_ref_idc == 0 {
2 * (pic.frame_num_offset + pic.frame_num) as i32 - 1
} else {
2 * (pic.frame_num_offset + pic.frame_num) as i32
};
if matches!(pic.field, Field::Frame | Field::Top) {
pic.top_field_order_cnt = pic_order_cnt;
}
if matches!(pic.field, Field::Frame | Field::Bottom) {
pic.bottom_field_order_cnt = pic_order_cnt;
}
}
_ => {
return Err(PlanError::Parse(format!(
"invalid pic_order_cnt_type: {}",
sps.pic_order_cnt_type
)))
}
}
match pic.field {
Field::Frame => {
pic.pic_order_cnt =
std::cmp::min(pic.top_field_order_cnt, pic.bottom_field_order_cnt);
}
Field::Top => {
pic.pic_order_cnt = pic.top_field_order_cnt;
}
Field::Bottom => {
pic.pic_order_cnt = pic.bottom_field_order_cnt;
}
}
Ok(())
}
/// Queue the frames that the C.4.5.3 bumping process declares ready for output.
fn bump_as_needed(&mut self, current_pic: &PictureData) {
let bumped = self.dpb.bump_as_needed(current_pic);
self.pending_outputs.extend(bumped.into_iter().flatten());
}
/// Queue all frames still present in the DPB for output.
fn drain_dpb(&mut self) {
let pics = self.dpb.drain();
self.pending_outputs.extend(pics.into_iter().flatten());
}
/// Find the first field for the picture started by `hdr`, if any. Always `None`
/// under the envelope gate (the DPB never enters interlaced mode); kept as ported so
/// the upstream diff stays small.
fn find_first_field(
&self,
hdr: &SliceHeader,
) -> Result<Option<(RcPictureData, PicId)>, String> {
let mut prev_field = None;
if self.dpb.interlaced() {
if let Some(last_dpb_entry) = self.dpb.entries().last() {
// Use the last entry in the DPB
let last_pic = last_dpb_entry.pic.borrow();
// If the picture is interlaced but doesn't have its other field set yet,
// then it must be the first field.
if !matches!(last_pic.field, Field::Frame)
&& matches!(last_pic.field_rank(), FieldRank::Single)
{
if let Some(id) = &last_dpb_entry.reference {
// Still waiting for the second field
prev_field = Some((last_dpb_entry.pic.clone(), *id));
}
}
}
}
let prev_field = match prev_field {
None => return Ok(None),
Some(prev_field) => prev_field,
};
let prev_field_pic = prev_field.0.borrow();
if prev_field_pic.frame_num != u32::from(hdr.frame_num) {
return Err(format!(
"the previous field's frame_num value {} differs from the current one's {}",
prev_field_pic.frame_num, hdr.frame_num
));
}
let cur_field = if hdr.bottom_field_flag {
Field::Bottom
} else {
Field::Top
};
if !hdr.field_pic_flag || cur_field == prev_field_pic.field {
let field = prev_field_pic.field;
return Err(format!(
"expected complementary field {:?}, got {:?}",
field.opposite(),
field
));
}
drop(prev_field_pic);
Ok(Some(prev_field))
}
// 8.2.4.3.1 Modification process of reference picture lists for short-term
// reference pictures
#[allow(clippy::too_many_arguments)]
fn short_term_pic_list_modification<'a>(
cur_pic: &PictureData,
dpb: &'a Dpb<PicId>,
ref_pic_list_x: &mut DpbPicRefList<'a, PicId>,
num_ref_idx_lx_active_minus1: u8,
max_pic_num: i32,
rplm: &RefPicListModification,
pic_num_lx_pred: &mut i32,
ref_idx_lx: &mut usize,
) -> Result<(), String> {
let pic_num_lx_no_wrap;
let abs_diff_pic_num = rplm.abs_diff_pic_num_minus1 as i32 + 1;
let modification_of_pic_nums_idc = rplm.modification_of_pic_nums_idc;
if modification_of_pic_nums_idc == 0 {
if *pic_num_lx_pred - abs_diff_pic_num < 0 {
pic_num_lx_no_wrap = *pic_num_lx_pred - abs_diff_pic_num + max_pic_num;
} else {
pic_num_lx_no_wrap = *pic_num_lx_pred - abs_diff_pic_num;
}
} else if modification_of_pic_nums_idc == 1 {
if *pic_num_lx_pred + abs_diff_pic_num >= max_pic_num {
pic_num_lx_no_wrap = *pic_num_lx_pred + abs_diff_pic_num - max_pic_num;
} else {
pic_num_lx_no_wrap = *pic_num_lx_pred + abs_diff_pic_num;
}
} else {
return Err(format!(
"unexpected value for modification_of_pic_nums_idc {modification_of_pic_nums_idc:?}"
));
}
*pic_num_lx_pred = pic_num_lx_no_wrap;
let pic_num_lx = if pic_num_lx_no_wrap > cur_pic.pic_num {
pic_num_lx_no_wrap - max_pic_num
} else {
pic_num_lx_no_wrap
};
let handle = dpb
.find_short_term_with_pic_num(pic_num_lx)
.ok_or_else(|| format!("no ShortTerm reference found with pic_num {pic_num_lx}"))?;
if *ref_idx_lx >= ref_pic_list_x.len() {
return Err("invalid ref_idx_lx index".into());
}
ref_pic_list_x.insert(*ref_idx_lx, handle);
*ref_idx_lx += 1;
let mut nidx = *ref_idx_lx;
for cidx in *ref_idx_lx..=usize::from(num_ref_idx_lx_active_minus1) + 1 {
if cidx == ref_pic_list_x.len() {
break;
}
let target = &ref_pic_list_x[cidx].pic;
if target.borrow().pic_num_f(max_pic_num) != pic_num_lx {
ref_pic_list_x[nidx] = ref_pic_list_x[cidx];
nidx += 1;
}
}
while ref_pic_list_x.len() > (usize::from(num_ref_idx_lx_active_minus1) + 1) {
ref_pic_list_x.pop();
}
Ok(())
}
// 8.2.4.3.2 Modification process of reference picture lists for long-term
// reference pictures
fn long_term_pic_list_modification<'a>(
dpb: &'a Dpb<PicId>,
ref_pic_list_x: &mut DpbPicRefList<'a, PicId>,
num_ref_idx_lx_active_minus1: u8,
max_long_term_frame_idx: MaxLongTermFrameIdx,
rplm: &RefPicListModification,
ref_idx_lx: &mut usize,
) -> Result<(), String> {
let long_term_pic_num = rplm.long_term_pic_num;
let handle = dpb
.find_long_term_with_long_term_pic_num(long_term_pic_num)
.ok_or_else(|| {
format!("no LongTerm reference found with long_term_pic_num {long_term_pic_num}")
})?;
if *ref_idx_lx >= ref_pic_list_x.len() {
return Err("invalid ref_idx_lx index".into());
}
ref_pic_list_x.insert(*ref_idx_lx, handle);
*ref_idx_lx += 1;
let mut nidx = *ref_idx_lx;
for cidx in *ref_idx_lx..=usize::from(num_ref_idx_lx_active_minus1) + 1 {
if cidx == ref_pic_list_x.len() {
break;
}
let target = &ref_pic_list_x[cidx].pic;
if target.borrow().long_term_pic_num_f(max_long_term_frame_idx) != long_term_pic_num {
ref_pic_list_x[nidx] = ref_pic_list_x[cidx];
nidx += 1;
}
}
while ref_pic_list_x.len() > (usize::from(num_ref_idx_lx_active_minus1) + 1) {
ref_pic_list_x.pop();
}
Ok(())
}
fn modify_ref_pic_list(
&self,
cur_pic: &PictureData,
hdr: &SliceHeader,
ref_pic_list_type: RefPicList,
ref_pic_list_indices: &[usize],
) -> Result<DpbPicRefList<'_, PicId>, String> {
let (ref_pic_list_modification_flag_lx, num_ref_idx_lx_active_minus1, rplm) =
match ref_pic_list_type {
RefPicList::RefPicList0 => (
hdr.ref_pic_list_modification_flag_l0,
hdr.num_ref_idx_l0_active_minus1,
&hdr.ref_pic_list_modification_l0,
),
RefPicList::RefPicList1 => (
hdr.ref_pic_list_modification_flag_l1,
hdr.num_ref_idx_l1_active_minus1,
&hdr.ref_pic_list_modification_l1,
),
};
let mut ref_pic_list: Vec<_> = ref_pic_list_indices
.iter()
.map(|&i| &self.dpb.entries()[i])
.take(usize::from(num_ref_idx_lx_active_minus1) + 1)
.collect();
if !ref_pic_list_modification_flag_lx {
return Ok(ref_pic_list);
}
let mut pic_num_lx_pred = cur_pic.pic_num;
let mut ref_idx_lx = 0;
for modification in rplm {
let idc = modification.modification_of_pic_nums_idc;
match idc {
0 | 1 => {
Self::short_term_pic_list_modification(
cur_pic,
&self.dpb,
&mut ref_pic_list,
num_ref_idx_lx_active_minus1,
hdr.max_pic_num as i32,
modification,
&mut pic_num_lx_pred,
&mut ref_idx_lx,
)?;
}
2 => Self::long_term_pic_list_modification(
&self.dpb,
&mut ref_pic_list,
num_ref_idx_lx_active_minus1,
self.max_long_term_frame_idx,
modification,
&mut ref_idx_lx,
)?,
3 => break,
_ => return Err(format!("unexpected modification_of_pic_nums_idc {idc:?}")),
}
}
Ok(ref_pic_list)
}
/// [`Self::modify_ref_pic_list`], degrading a failed modification to a
/// [`PlanWarning::MissingReference`] plus the unmodified 8.2.4.2 initial list —
/// upstream aborts the decode here, but a modification naming a lost picture is
/// punktfunk's cue to conceal and request recovery, not to kill the session.
fn modified_or_initial_list(
&self,
cur_pic: &PictureData,
hdr: &SliceHeader,
ref_pic_list_type: RefPicList,
ref_pic_list_indices: &[usize],
warnings: &mut Vec<PlanWarning>,
) -> DpbPicRefList<'_, PicId> {
match self.modify_ref_pic_list(cur_pic, hdr, ref_pic_list_type, ref_pic_list_indices) {
Ok(list) => list,
Err(detail) => {
warnings.push(PlanWarning::MissingReference {
context: "ref_pic_list_modification",
detail,
});
let num_ref_idx_lx_active_minus1 = match ref_pic_list_type {
RefPicList::RefPicList0 => hdr.num_ref_idx_l0_active_minus1,
RefPicList::RefPicList1 => hdr.num_ref_idx_l1_active_minus1,
};
ref_pic_list_indices
.iter()
.map(|&i| &self.dpb.entries()[i])
.take(usize::from(num_ref_idx_lx_active_minus1) + 1)
.collect()
}
}
}
/// Generate RefPicList0 and RefPicList1 for one slice (8.2.4), already converted to
/// backend-facing [`RefPic`]s.
fn create_ref_pic_lists(
&self,
cur_pic: &PictureData,
hdr: &SliceHeader,
ref_pic_lists: &ReferencePicLists,
warnings: &mut Vec<PlanWarning>,
) -> (Vec<RefPic>, Vec<RefPic>) {
let ref_pic_list0 = match hdr.slice_type {
SliceType::P | SliceType::Sp => self.modified_or_initial_list(
cur_pic,
hdr,
RefPicList::RefPicList0,
&ref_pic_lists.ref_pic_list_p0,
warnings,
),
SliceType::B => self.modified_or_initial_list(
cur_pic,
hdr,
RefPicList::RefPicList0,
&ref_pic_lists.ref_pic_list_b0,
warnings,
),
_ => Vec::new(),
};
let ref_pic_list1 = match hdr.slice_type {
SliceType::B => self.modified_or_initial_list(
cur_pic,
hdr,
RefPicList::RefPicList1,
&ref_pic_lists.ref_pic_list_b1,
warnings,
),
_ => Vec::new(),
};
(
Self::to_ref_pics(&ref_pic_list0, warnings),
Self::to_ref_pics(&ref_pic_list1, warnings),
)
}
/// Convert one reference list to backend-facing [`RefPic`]s, preserving list
/// positions: every ref_idx in the slice syntax indexes the returned Vec 1:1.
///
/// A non-existing picture (8.2.5.2 gap placeholder) has no id a backend could
/// resolve, so it is substituted IN PLACE by the nearest existing reference in
/// list order (the previous existing entry, else the first existing one) —
/// stable-but-wrong concealment, flagged via [`PlanWarning::MissingReference`].
/// Compacting instead would shift every subsequent ref_idx and make the decoder
/// predict from the wrong pictures. Only a list with no existing reference at all
/// collapses to empty (the caller warns on that separately).
fn to_ref_pics(list: &[&DpbEntry<PicId>], warnings: &mut Vec<PlanWarning>) -> Vec<RefPic> {
// Each slot: the resolvable entry plus its picture's frame_num (a long-term
// substitute is re-labelled short-term, so its frame_num is needed).
let mut slots: Vec<Option<(RefPic, u16)>> = Vec::with_capacity(list.len());
for entry in list {
let pic = entry.pic.borrow();
match entry.reference {
Some(id) => {
let is_long_term = matches!(pic.reference(), Reference::LongTerm);
let frame_num_or_lt_idx = if is_long_term {
// long_term_frame_idx is ue(v)-coded; the spec bounds it (<= 15
// via max_long_term_frame_idx) but the parser does not, so
// saturate rather than truncate — unreachable-in-practice
// hardening.
u16::try_from(pic.long_term_frame_idx).unwrap_or(u16::MAX)
} else {
pic.frame_num as u16
};
slots.push(Some((
RefPic {
id,
top_field_order_cnt: pic.top_field_order_cnt,
bottom_field_order_cnt: pic.bottom_field_order_cnt,
is_long_term,
frame_num_or_lt_idx,
},
pic.frame_num as u16,
)));
}
None => {
warnings.push(PlanWarning::MissingReference {
context: "non-existing picture (frame_num gap placeholder) in list",
detail: format!("frame_num {}", pic.frame_num),
});
slots.push(None);
}
}
}
let first_existing = slots.iter().flatten().next().copied();
let mut out = Vec::with_capacity(slots.len());
let mut prev_existing: Option<(RefPic, u16)> = None;
for slot in &slots {
match slot {
Some(real) => {
prev_existing = Some(*real);
out.push(real.0);
}
None => {
// Index mapping preserved; the substituted entry is stable-but-
// wrong concealment. Placeholders are short-term (8.2.5.2), so the
// substitute is labelled short-term with its own frame_num.
if let Some((substitute, frame_num)) = prev_existing.or(first_existing) {
out.push(RefPic {
id: substitute.id,
top_field_order_cnt: substitute.top_field_order_cnt,
bottom_field_order_cnt: substitute.bottom_field_order_cnt,
is_long_term: false,
frame_num_or_lt_idx: frame_num,
});
}
}
}
}
out
}
fn handle_memory_management_ops(&mut self, pic: &mut PictureData) -> Result<(), MmcoError> {
let markings = pic.ref_pic_marking.clone();
for marking in &markings.inner {
match marking.memory_management_control_operation {
0 => break,
1 => self.dpb.mmco_op_1(pic, marking)?,
2 => self.dpb.mmco_op_2(pic, marking)?,
3 => self.dpb.mmco_op_3(pic, marking)?,
4 => self.max_long_term_frame_idx = self.dpb.mmco_op_4(marking),
5 => self.max_long_term_frame_idx = self.dpb.mmco_op_5(pic),
6 => self.dpb.mmco_op_6(pic, marking),
other => return Err(MmcoError::UnknownMmco(other)),
}
}
Ok(())
}
fn reference_pic_marking(&mut self, pic: &mut PictureData, sps: &Sps) -> Result<(), MmcoError> {
/* 8.2.5.1 */
if matches!(pic.is_idr, IsIdr::Yes { .. }) {
self.dpb.mark_all_as_unused_for_ref();
if pic.ref_pic_marking.long_term_reference_flag {
pic.set_reference(Reference::LongTerm, false);
pic.long_term_frame_idx = 0;
self.max_long_term_frame_idx = MaxLongTermFrameIdx::Idx(0);
} else {
pic.set_reference(Reference::ShortTerm, false);
self.max_long_term_frame_idx = MaxLongTermFrameIdx::NoLongTermFrameIndices;
}
return Ok(());
}
if pic.ref_pic_marking.adaptive_ref_pic_marking_mode_flag {
self.handle_memory_management_ops(pic)?;
} else {
self.dpb.sliding_window_marking(pic, sps);
}
Ok(())
}
// Apply the parameters of `sps` to the planning state.
fn apply_sps(&mut self, sps: &Sps) {
self.negotiation_info = NegotiationInfo::from(sps);
let max_dpb_frames = sps.max_dpb_frames();
let interlaced = !sps.frame_mbs_only_flag;
let max_num_order_frames = sps.max_num_order_frames() as usize;
let max_num_reorder_frames = if max_num_order_frames > max_dpb_frames {
0
} else {
max_num_order_frames
};
self.dpb.set_limits(max_dpb_frames, max_num_reorder_frames);
self.dpb.set_interlaced(interlaced);
}
fn negotiation_possible(sps: &Sps, old_negotiation_info: &NegotiationInfo) -> bool {
let negotiation_info = NegotiationInfo::from(sps);
*old_negotiation_info != negotiation_info
}
fn renegotiate_if_needed(&mut self, sps: &Sps) -> Result<(), PlanError> {
if Self::negotiation_possible(sps, &self.negotiation_info) {
Self::check_envelope(sps)?;
// Make sure all the frames planned so far are display-ready before the
// stream parameters change under them.
self.drain_dpb();
self.apply_sps(sps);
}
Ok(())
}
fn handle_frame_num_gap(
&mut self,
sps: &Sps,
frame_num: u32,
warnings: &mut Vec<PlanWarning>,
) -> Result<(), PlanError> {
if self.dpb.is_empty() {
return Ok(());
}
trace!("frame_num gap detected");
// Upstream refuses the gap when gaps_in_frame_num_value_allowed_flag is unset.
// Here the caller has already emitted PlanWarning::FrameNumGap and the 8.2.5.2
// process runs regardless: losing a reference AU on the wire must degrade to
// concealment + recovery, and inserting the non-existing pictures keeps the
// frame_num/pic_num bookkeeping of everything that follows spec-true.
let mut unused_short_term_frame_num =
(self.prev_ref_pic_info.frame_num + 1) % sps.max_frame_num();
while unused_short_term_frame_num != frame_num {
let max_frame_num = sps.max_frame_num();
let mut pic = PictureData::new_non_existing(unused_short_term_frame_num, 0);
self.compute_pic_order_count(&mut pic, sps)?;
self.dpb
.update_pic_nums(unused_short_term_frame_num, max_frame_num, &pic);
self.dpb.sliding_window_marking(&mut pic, sps);
self.bump_as_needed(&pic);
// Interlaced field-splitting dropped: the envelope gate keeps the DPB in
// progressive mode.
if let Err(err) = self.dpb.store_picture(pic.into_rc(), None) {
// A full DPB must not error the AU (warnings-not-errors contract):
// stop inserting placeholders. The pic_num bookkeeping degrades from
// here, which the recovery the warning triggers will heal.
warnings.push(PlanWarning::MissingReference {
context: "frame_num gap placeholder dropped (DPB full)",
detail: err.to_string(),
});
break;
}
unused_short_term_frame_num += 1;
unused_short_term_frame_num %= max_frame_num;
}
Ok(())
}
/// Init the current picture being planned.
fn init_current_pic(
&mut self,
slice: &Slice,
sps: &Sps,
first_field: Option<&RcPictureData>,
) -> Result<PictureData, PlanError> {
let mut pic = PictureData::new_from_slice(slice, sps, 0, first_field);
self.compute_pic_order_count(&mut pic, sps)?;
if matches!(pic.is_idr, IsIdr::Yes { .. }) {
// C.4.5.3 "Bumping process"
// The bumping process is invoked in the following cases:
// Clause 2:
// The current picture is an IDR picture and
// no_output_of_prior_pics_flag is not equal to 1 and is not
// inferred to be equal to 1, as specified in clause C.4.4.
if !pic.ref_pic_marking.no_output_of_prior_pics_flag {
self.drain_dpb();
} else {
// C.4.4 When no_output_of_prior_pics_flag is equal to 1 or is
// inferred to be equal to 1, all frame buffers in the DPB are
// emptied without output of the pictures they contain, and DPB
// fullness is set to 0.
self.dpb.clear();
}
}
self.dpb
.update_pic_nums(u32::from(slice.header.frame_num), sps.max_frame_num(), &pic);
Ok(pic)
}
/// Called once per picture, on its first slice.
fn begin_picture(
&mut self,
slice: &Slice,
warnings: &mut Vec<PlanWarning>,
) -> Result<CurrentPicState, PlanError> {
let hdr = &slice.header;
let pps = Rc::clone(self.parser.get_pps(hdr.pic_parameter_set_id).ok_or(
PlanError::NoActiveParamSet {
pps_id: hdr.pic_parameter_set_id,
},
)?);
// A picture's SPS may require renegotiation.
self.renegotiate_if_needed(&pps.sps)?;
let first_field = self.find_first_field(hdr).map_err(PlanError::Parse)?;
// Upstream secures the backend picture here; the plan's equivalent is the id
// backends will allocate against.
let id = self.next_pic_id;
self.next_pic_id += 1;
if slice.nalu.header.idr_pic_flag {
self.prev_ref_pic_info.frame_num = 0;
}
let frame_num = u32::from(hdr.frame_num);
let current_macroblock = match pps.sps.separate_colour_plane_flag {
true => CurrentMacroblockTracking::SeparateColorPlane(Default::default()),
false => CurrentMacroblockTracking::NonSeparateColorPlane(None),
};
if frame_num != self.prev_ref_pic_info.frame_num
&& frame_num != (self.prev_ref_pic_info.frame_num + 1) % pps.sps.max_frame_num()
{
if !self.dpb.is_empty() {
warnings.push(PlanWarning::FrameNumGap {
expected: ((self.prev_ref_pic_info.frame_num + 1) % pps.sps.max_frame_num())
as u16,
got: hdr.frame_num,
});
}
self.handle_frame_num_gap(&pps.sps, frame_num, warnings)?;
}
let pic = self.init_current_pic(slice, &pps.sps, first_field.as_ref().map(|f| &f.0))?;
let ref_pic_lists = self.dpb.build_ref_pic_lists(&pic);
Ok(CurrentPicState {
pic,
first_slice_pps: Rc::clone(&pps),
pps,
id,
ref_pic_lists,
current_macroblock,
})
}
// Check whether first_mb_in_slice increases monotonically for the current
// picture as required by 7.4.3.
fn check_first_mb_in_slice(current_macroblock: &mut CurrentMacroblockTracking, slice: &Slice) {
let first_mb_in_slice = slice.header.first_mb_in_slice;
match current_macroblock {
CurrentMacroblockTracking::SeparateColorPlane(current_macroblock) => {
match current_macroblock.entry(slice.header.colour_plane_id) {
Entry::Vacant(entry) => {
entry.insert(first_mb_in_slice);
}
Entry::Occupied(mut entry) => {
let current_macroblock = entry.get_mut();
if first_mb_in_slice <= *current_macroblock {
trace!(
"first_mb_in_slice does not increase monotonically, expect \
corrupted output"
);
}
*current_macroblock = first_mb_in_slice;
}
}
}
CurrentMacroblockTracking::NonSeparateColorPlane(current_macroblock) => {
if current_macroblock.is_some_and(|current| first_mb_in_slice <= current) {
trace!(
"first_mb_in_slice does not increase monotonically, expect corrupted \
output"
);
}
*current_macroblock = Some(first_mb_in_slice);
}
}
}
/// Handle one slice of the current picture (upstream: `handle_slice`).
fn plan_slice(
&self,
cur: &mut CurrentPicState,
slice: Slice,
data: Range<usize>,
warnings: &mut Vec<PlanWarning>,
) -> Result<SlicePlan, PlanError> {
Self::check_first_mb_in_slice(&mut cur.current_macroblock, &slice);
// A slice can technically refer to another PPS.
let pps = self
.parser
.get_pps(slice.header.pic_parameter_set_id)
.ok_or(PlanError::NoActiveParamSet {
pps_id: slice.header.pic_parameter_set_id,
})?;
cur.pps = Rc::clone(pps);
// Make sure that no negotiation is possible mid-picture. How could it?
// We'd lose the context of the previous slices.
if Self::negotiation_possible(&cur.pps.sps, &self.negotiation_info) {
return Err(PlanError::Parse(
"invalid stream: inter-picture renegotiation requested".into(),
));
}
let (ref_list0, ref_list1) =
self.create_ref_pic_lists(&cur.pic, &slice.header, &cur.ref_pic_lists, warnings);
// 8.2.4.2.1: an inter slice shall have at least one usable reference. Ending up
// empty (every candidate lost or a gap placeholder) is undecodable-as-intended,
// and it can happen without any per-entry warning when the DPB holds only
// non-existing pictures — flag it so the session requests recovery.
let slice_type = slice.header.slice_type;
if matches!(slice_type, SliceType::P | SliceType::Sp | SliceType::B) && ref_list0.is_empty()
{
warnings.push(PlanWarning::MissingReference {
context: "inter slice with no usable RefPicList0",
detail: format!("slice_type {slice_type:?}"),
});
}
if matches!(slice_type, SliceType::B) && ref_list1.is_empty() {
warnings.push(PlanWarning::MissingReference {
context: "B slice with no usable RefPicList1",
detail: format!("slice_type {slice_type:?}"),
});
}
Ok(SlicePlan {
data,
header: slice.header,
ref_list0,
ref_list1,
})
}
/// Adds the picture to the output queue when it could not be added to the DPB.
fn add_to_ready_queue(&mut self, pic: PictureData, id: PicId) {
if matches!(pic.field, Field::Frame) {
self.pending_outputs.push(id);
} else if let FieldRank::Second(..) = pic.field_rank() {
self.pending_outputs.push(id);
}
}
fn finish_picture(
&mut self,
cur: CurrentPicState,
warnings: &mut Vec<PlanWarning>,
) -> Result<PicId, PlanError> {
let CurrentPicState {
mut pic, pps, id, ..
} = cur;
if matches!(pic.reference(), Reference::ShortTerm | Reference::LongTerm) {
// Upstream aborts on a failed MMCO; an op naming a picture the DPB lost is
// a concealment signal here, and the remaining marking state stays usable.
if let Err(err) = self.reference_pic_marking(&mut pic, &pps.sps) {
warnings.push(PlanWarning::MissingReference {
context: "reference picture marking (MMCO)",
detail: err.to_string(),
});
}
self.prev_ref_pic_info.fill(&pic);
}
self.prev_pic_info.fill(&pic);
if pic.has_mmco_5 {
warnings.push(PlanWarning::Mmco5Rebase);
// C.4.5.3 "Bumping process"
// The bumping process is invoked in the following cases:
// Clause 3:
// The current picture has memory_management_control_operation equal
// to 5, as specified in clause C.4.4.
self.drain_dpb();
}
// Bump the DPB as per C.4.5.3 to cover clauses 1, 4, 5 and 6.
self.bump_as_needed(&pic);
// C.4.5.1, C.4.5.2
// If the current decoded picture is the second field of a complementary
// reference field pair, add to DPB.
// C.4.5.1
// For a reference decoded picture, the "bumping" process is invoked
// repeatedly until there is an empty frame buffer, by which point it is
// added to the DPB. Notice that Dpb::needs_bumping already accounts for
// this.
// C.4.5.2
// For a non-reference decoded picture, if there is empty frame buffer
// after bumping the smaller POC, add to DPB. Otherwise, add it to the
// output queue.
if pic.is_second_field_of_complementary_ref_pair()
|| pic.is_ref()
|| self.dpb.has_empty_frame_buffer()
{
// Upstream splits frames into complementary field pairs when the DPB is in
// interlaced mode; the envelope gate keeps that path unreachable.
self.dpb
.store_picture(pic.into_rc(), Some(id))
.map_err(|err| PlanError::Parse(err.to_string()))?;
} else {
self.add_to_ready_queue(pic, id);
}
Ok(id)
}
fn picture_plan(cur: &CurrentPicState, recovery_point: Option<RecoveryPoint>) -> PicturePlan {
let pic = &cur.pic;
// The first slice's PPS defines the picture's parameters (upstream's
// start_picture semantics); `cur.pps` may have drifted to a later slice's.
let sps = &cur.first_slice_pps.sps;
let rect = sps.visible_rectangle();
PicturePlan {
is_idr: matches!(pic.is_idr, IsIdr::Yes { .. }),
nal_ref_idc: pic.nal_ref_idc,
is_reference: pic.is_ref(),
frame_num: pic.frame_num as u16,
top_field_order_cnt: pic.top_field_order_cnt,
bottom_field_order_cnt: pic.bottom_field_order_cnt,
pic_order_cnt: pic.pic_order_cnt,
coded_width: sps.width(),
coded_height: sps.height(),
// The vendored `visible_rectangle()` returns the crop OFFSET in `min` and
// the visible SIZE in `max` (not an edge coordinate): subtracting would
// double-count the left/top crop and underflow on large offsets.
display_crop: DisplayCrop {
x: rect.min.x,
y: rect.min.y,
width: rect.max.x,
height: rect.max.y,
},
// Read unconditionally: the vendored parser builds every SPS from
// `Default`, whose `VuiParams` already holds E.2.1's inferred values
// (2/2/2, limited range), and parsing only overwrites them under the
// present flags — so this IS the spec inference whether or not the
// stream carried a VUI.
colour: ColourDescription {
colour_primaries: sps.vui_parameters.colour_primaries,
transfer_characteristics: sps.vui_parameters.transfer_characteristics,
matrix_coefficients: sps.vui_parameters.matrix_coefficients,
video_full_range: sps.vui_parameters.video_full_range_flag,
},
profile_idc: sps.profile_idc,
level_idc: sps.level_idc,
bit_depth_luma_minus8: sps.bit_depth_luma_minus8,
bit_depth_chroma_minus8: sps.bit_depth_chroma_minus8,
chroma_format_idc: sps.chroma_format_idc,
max_dpb_frames: sps.max_dpb_frames(),
recovery_point,
}
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use std::rc::Rc;
use cros_codecs::codec::h264::nalu_writer::NaluWriter;
use cros_codecs::codec::h264::parser::Nalu;
use cros_codecs::codec::h264::parser::NaluType;
use cros_codecs::codec::h264::parser::PpsBuilder;
use cros_codecs::codec::h264::parser::Profile;
use cros_codecs::codec::h264::parser::SpsBuilder;
use cros_codecs::codec::h264::parser::VuiParams;
use cros_codecs::codec::h264::synthesizer::Synthesizer;
use super::*;
const TEST_25FPS: &[u8] =
include_bytes!("../vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264");
// The plain 64x64-I-P-B-P.h264 is a constrained-baseline encode: x264 silently
// dropped the requested B frame (its slices parse as I, P, P). The -high variant of
// the same sequence carries the real B slice.
const TEST_64X64_I_P_B_P_HIGH: &[u8] =
include_bytes!("../vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264");
/// Test-only AU splitter: the vendored vectors are raw Annex-B streams, while
/// `plan_au` takes the pre-split AUs punktfunk's pump produces. A new AU starts at a
/// non-slice NALU following slices, or at a slice with first_mb_in_slice == 0
/// (whose ue(v) encoding makes the first RBSP bit 1) when the current AU already
/// has slices.
fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> {
let mut aus = Vec::new();
let mut cursor = Cursor::new(stream);
let mut au_start = 0usize;
let mut au_has_slice = false;
while let Ok(nalu) = Nalu::next(&mut cursor) {
let nalu_offset = cursor.position() as usize;
let start = nalu_offset - nalu.offset;
let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr);
let first_mb_zero =
is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0);
if au_has_slice && (!is_slice || first_mb_zero) {
aus.push(&stream[au_start..start]);
au_start = start;
au_has_slice = false;
}
au_has_slice |= is_slice;
}
aus.push(&stream[au_start..]);
aus
}
#[test]
fn the_full_25fps_vector_plans_every_picture_and_every_pic_id_reaches_output() {
let aus = split_into_aus(TEST_25FPS);
let mut planner = H264Planner::new();
let mut plans = Vec::new();
for au in &aus {
plans.push(
planner
.plan_au(au)
.expect("the clean vector must plan without errors"),
);
}
assert_eq!(plans.len(), 250);
assert_eq!(plans.iter().map(|p| p.slices.len()).sum::<usize>(), 500);
assert!(plans[0].picture.is_idr);
assert!(plans.iter().all(|p| p.warnings.is_empty()));
assert_eq!(
(plans[0].picture.coded_width, plans[0].picture.coded_height),
(320, 240),
"coded size comes from the vector's SPS"
);
for plan in &plans {
if plan.picture.is_idr {
assert_eq!(plan.picture.pic_order_cnt, 0, "POC must reset at an IDR");
}
for slice in &plan.slices {
if slice.header.slice_type.is_p() || slice.header.slice_type.is_b() {
assert!(!slice.ref_list0.is_empty());
}
}
}
let stored: BTreeSet<PicId> = plans.iter().filter_map(|p| p.dpb.stored).collect();
assert_eq!(stored.len(), 250);
let mut emitted: Vec<PicId> = plans
.iter()
.flat_map(|p| p.dpb.outputs.iter().copied())
.collect();
emitted.extend(planner.flush().outputs);
let output: BTreeSet<PicId> = emitted.iter().copied().collect();
assert_eq!(
output, stored,
"bumping plus the final flush must output every picture"
);
// Output ORDER, not just coverage: within each IDR period, ids must emerge in
// ascending POC — the invariant the C.4.5.3 bumping process exists to provide.
// (POC was recorded at plan time; an IDR resets it, hence the period key.)
let mut period = 0usize;
let mut order_key: BTreeMap<PicId, (usize, i32)> = BTreeMap::new();
for plan in &plans {
if plan.picture.is_idr {
period += 1;
}
order_key.insert(
plan.dpb.stored.unwrap(),
(period, plan.picture.pic_order_cnt),
);
}
let mut last: Option<(usize, i32)> = None;
for id in &emitted {
let key = order_key[id];
if let Some(last) = last {
assert!(
key > last,
"outputs must emerge in ascending POC order per IDR period: \
{key:?} emitted after {last:?}"
);
}
last = Some(key);
}
}
#[test]
fn b_slices_get_a_poc_ordered_list1_distinct_from_list0() {
let aus = split_into_aus(TEST_64X64_I_P_B_P_HIGH);
let mut planner = H264Planner::new();
let mut b_slices_seen = 0usize;
for au in &aus {
let plan = planner
.plan_au(au)
.expect("the clean vector must plan without errors");
for slice in &plan.slices {
if !slice.header.slice_type.is_b() {
continue;
}
b_slices_seen += 1;
assert!(!slice.ref_list0.is_empty());
assert!(!slice.ref_list1.is_empty());
let ids0: Vec<PicId> = slice.ref_list0.iter().map(|r| r.id).collect();
let ids1: Vec<PicId> = slice.ref_list1.iter().map(|r| r.id).collect();
assert_ne!(ids0, ids1, "list1 must not be list0's ordering");
// 8.2.4.2.3: list0 leads with the past, list1 with the future
// (a frame's PicOrderCnt is the min of its field order counts).
let poc = |r: &RefPic| r.top_field_order_cnt.min(r.bottom_field_order_cnt);
assert!(poc(&slice.ref_list0[0]) < plan.picture.pic_order_cnt);
assert!(poc(&slice.ref_list1[0]) > plan.picture.pic_order_cnt);
}
}
assert!(b_slices_seen > 0, "the vector must contain B slices");
}
/// Byte-level authoring for the MMCO/LTR test: parameter sets via the vendored
/// builders + synthesizer, slice headers written by hand with the vendored
/// `NaluWriter` (upstream has no slice-header synthesizer — its encoder packs
/// headers in hardware). The planner only reads headers, so no slice data follows
/// the rbsp stop bit.
fn base_sps() -> SpsBuilder {
SpsBuilder::new()
.seq_parameter_set_id(0)
.profile_idc(Profile::Main)
.level_idc(Level::L4)
.frame_mbs_only_flag(true)
.direct_8x8_inference_flag(true)
.max_num_ref_frames(4)
.log2_max_frame_num_minus4(0)
.pic_order_cnt_type(0)
.log2_max_pic_order_cnt_lsb_minus4(0)
}
fn authored_sps_pps() -> (Rc<Sps>, Rc<Pps>) {
let sps = base_sps().resolution(64, 64).build();
let pps = PpsBuilder::new(Rc::clone(&sps))
.pic_parameter_set_id(0)
.pic_init_qp(26)
.build();
(sps, pps)
}
fn param_set_au(sps: &Sps, pps: &Pps) -> Vec<u8> {
let mut au = Vec::new();
Synthesizer::<'_, Sps, _>::synthesize(3, sps, &mut au, true).unwrap();
Synthesizer::<'_, Pps, _>::synthesize(3, pps, &mut au, true).unwrap();
au
}
fn write_idr_slice() -> Vec<u8> {
write_idr_slice_at(0, 0)
}
fn write_idr_slice_at(first_mb: u32, pps_id: u32) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut w = NaluWriter::new(&mut buf, true);
w.write_header(3, NaluType::SliceIdr as u8).unwrap();
w.write_ue(first_mb).unwrap(); // first_mb_in_slice
w.write_ue(2u32).unwrap(); // slice_type: I
w.write_ue(pps_id).unwrap(); // pic_parameter_set_id
w.write_f(4, 0u32).unwrap(); // frame_num, u(4): log2_max_frame_num_minus4 = 0
w.write_ue(0u32).unwrap(); // idr_pic_id
w.write_f(4, 0u32).unwrap(); // pic_order_cnt_lsb, u(4)
w.write_f(1, 0u32).unwrap(); // no_output_of_prior_pics_flag
w.write_f(1, 0u32).unwrap(); // long_term_reference_flag
w.write_se(0i32).unwrap(); // slice_qp_delta
w.write_f(1, 1u32).unwrap(); // rbsp stop bit
while !w.aligned() {
w.write_f(1, 0u32).unwrap();
}
}
buf
}
/// One P slice NALU. `mmco_ops` = `None` for sliding-window marking, `Some(ops)` for
/// adaptive marking with `(operation, single-argument)` pairs (ops 2/4/6 all take
/// exactly one) — the writer appends the terminating op 0.
fn write_p_slice(
frame_num: u32,
poc_lsb: u32,
ref_idc: u8,
num_ref_idx_l0_active: u32,
mmco_ops: Option<&[(u32, u32)]>,
) -> Vec<u8> {
write_p_slice_at(
0,
0,
frame_num,
poc_lsb,
ref_idc,
num_ref_idx_l0_active,
mmco_ops,
)
}
fn write_p_slice_at(
first_mb: u32,
pps_id: u32,
frame_num: u32,
poc_lsb: u32,
ref_idc: u8,
num_ref_idx_l0_active: u32,
mmco_ops: Option<&[(u32, u32)]>,
) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut w = NaluWriter::new(&mut buf, true);
w.write_header(ref_idc, NaluType::Slice as u8).unwrap();
w.write_ue(first_mb).unwrap(); // first_mb_in_slice
w.write_ue(0u32).unwrap(); // slice_type: P
w.write_ue(pps_id).unwrap(); // pic_parameter_set_id
w.write_f(4, frame_num).unwrap(); // frame_num, u(4)
w.write_f(4, poc_lsb).unwrap(); // pic_order_cnt_lsb, u(4)
w.write_f(1, 1u32).unwrap(); // num_ref_idx_active_override_flag
w.write_ue(num_ref_idx_l0_active - 1).unwrap();
w.write_f(1, 0u32).unwrap(); // ref_pic_list_modification_flag_l0
if ref_idc != 0 {
match mmco_ops {
None => w.write_f(1, 0u32).map(|_| ()).unwrap(),
Some(ops) => {
w.write_f(1, 1u32).unwrap(); // adaptive_ref_pic_marking_mode_flag
for (op, arg) in ops {
w.write_ue(*op).unwrap();
w.write_ue(*arg).unwrap();
}
w.write_ue(0u32).unwrap(); // memory_management_control_operation end
}
}
}
w.write_se(0i32).unwrap(); // slice_qp_delta
w.write_f(1, 1u32).unwrap(); // rbsp stop bit
while !w.aligned() {
w.write_f(1, 0u32).unwrap();
}
}
buf
}
#[test]
fn mmco_marks_a_picture_long_term_later_lists_carry_it_and_mmco2_evicts_it() {
let (sps, pps) = authored_sps_pps();
let mut au0 = Vec::new();
Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap();
Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap();
au0.extend(write_idr_slice());
// AU1 marks itself long-term: MMCO 4 admits long-term index 0, MMCO 6 assigns
// it to the current picture.
let au1 = write_p_slice(1, 2, 1, 1, Some(&[(4, 1), (6, 0)]));
let au2 = write_p_slice(2, 4, 1, 2, None);
// AU3 evicts it again: MMCO 2 unmarks long_term_pic_num 0. Its own list is
// 3 deep so the long-term picture's presence (or wrongful absence) is visible.
let au3 = write_p_slice(3, 6, 1, 3, Some(&[(2, 0)]));
let au4 = write_p_slice(4, 8, 0, 3, None);
let mut planner = H264Planner::new();
let p0 = planner.plan_au(&au0).unwrap();
let p1 = planner.plan_au(&au1).unwrap();
let p2 = planner.plan_au(&au2).unwrap();
let p3 = planner.plan_au(&au3).unwrap();
let p4 = planner.plan_au(&au4).unwrap();
for plan in [&p0, &p1, &p2, &p3, &p4] {
assert!(
plan.warnings.is_empty(),
"authored stream must plan clean: {plan:?}"
);
}
assert!(p0.picture.is_idr);
assert_eq!(p2.picture.pic_order_cnt, 4);
let idr_id = p0.dpb.stored.unwrap();
let lt_id = p1.dpb.stored.unwrap();
// After AU1's marking, AU2's list must be [short-term IDR, long-term AU1] —
// 8.2.4.2.1 puts long-term references after the short-term ones.
let list0 = &p2.slices[0].ref_list0;
assert_eq!(list0.len(), 2);
assert!(!list0[0].is_long_term);
assert_eq!(list0[0].id, idr_id);
assert!(list0[1].is_long_term);
assert_eq!(list0[1].id, lt_id);
assert_eq!(
list0[1].frame_num_or_lt_idx, 0,
"LongTermFrameIdx, not frame_num"
);
// AU3 carries the MMCO 2, but marking is an end-of-picture process (8.2.5):
// its OWN list is built before the op applies and must still hold the
// long-term picture, after the short-terms in descending-PicNum order. An
// applied-marking-before-list-build ordering bug surfaces exactly here.
let p2_id = p2.dpb.stored.unwrap();
let p3_id = p3.dpb.stored.unwrap();
assert_eq!(
p3.slices[0]
.ref_list0
.iter()
.map(|r| (r.id, r.is_long_term))
.collect::<Vec<_>>(),
vec![(p2_id, false), (idr_id, false), (lt_id, true)],
"AU3 still sees the long-term ref; its own MMCO 2 applies only at finish"
);
// AU4 must no longer see it: exactly the three short-terms, in descending
// PicNum order (8.2.4.2.1).
assert_eq!(
p4.slices[0]
.ref_list0
.iter()
.map(|r| (r.id, r.is_long_term))
.collect::<Vec<_>>(),
vec![(p3_id, false), (p2_id, false), (idr_id, false)],
"the unmarked picture must have left, short-terms sorted by PicNum"
);
// Unmarked and displayed, the picture leaves the DPB for good.
let flush = planner.flush();
assert!(flush.outputs.contains(&lt_id));
assert!(flush.removed.contains(&lt_id));
}
#[test]
fn a_dropped_reference_au_degrades_to_gap_warnings_and_planning_continues() {
let aus = split_into_aus(TEST_25FPS);
// Pass 1: find a droppable AU — a non-IDR reference picture not followed by an
// IDR (an IDR right after would reset the state and hide the gap).
let mut planner = H264Planner::new();
let mut plans = Vec::new();
for au in &aus {
plans.push(planner.plan_au(au).unwrap());
}
let dropped = plans
.iter()
.enumerate()
.position(|(i, p)| {
p.picture.is_reference
&& !p.picture.is_idr
&& plans.get(i + 1).is_some_and(|next| !next.picture.is_idr)
})
.expect("the vector contains a droppable reference picture");
// Pass 2: the same stream minus that AU must warn, not error — and every ref
// list entry it emits must still resolve to a picture the backend was told to
// store (substitution never leaks a placeholder).
let mut planner = H264Planner::new();
let mut gap_seen = false;
let mut missing_seen = false;
let mut planned = 0usize;
let mut stored_so_far: BTreeSet<PicId> = BTreeSet::new();
for (i, au) in aus.iter().enumerate() {
if i == dropped {
continue;
}
let plan = planner
.plan_au(au)
.expect("a lost reference AU must degrade to warnings, not errors");
planned += 1;
gap_seen |= plan
.warnings
.iter()
.any(|w| matches!(w, PlanWarning::FrameNumGap { .. }));
missing_seen |= plan
.warnings
.iter()
.any(|w| matches!(w, PlanWarning::MissingReference { .. }));
stored_so_far.insert(plan.dpb.stored.unwrap());
for slice in &plan.slices {
for entry in slice.ref_list0.iter().chain(&slice.ref_list1) {
assert!(
stored_so_far.contains(&entry.id),
"every emitted reference must be a real stored PicId"
);
}
}
}
assert_eq!(planned, 249);
assert!(
gap_seen,
"the AU after the drop must report the frame_num gap"
);
// The 8.2.5.2 placeholder is un-resolvable for backends, so planning around
// it must also have flagged it.
assert!(missing_seen);
}
#[test]
fn a_gap_placeholder_inside_a_ref_list_is_substituted_in_place_not_compacted() {
let (sps, pps) = authored_sps_pps();
let mut au0 = Vec::new();
Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap();
Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap();
au0.extend(write_idr_slice());
let au1 = write_p_slice(1, 2, 1, 1, None);
// The reference picture with frame_num 2 is never fed (lost on the wire); the
// next AU's 3-deep list then holds the 8.2.5.2 placeholder at its HEAD.
let au3 = write_p_slice(3, 6, 1, 3, None);
let mut planner = H264Planner::new();
let p0 = planner.plan_au(&au0).unwrap();
let p1 = planner.plan_au(&au1).unwrap();
let p3 = planner.plan_au(&au3).unwrap();
assert!(p3
.warnings
.iter()
.any(|w| matches!(w, PlanWarning::FrameNumGap { .. })));
assert!(p3
.warnings
.iter()
.any(|w| matches!(w, PlanWarning::MissingReference { .. })));
// Initial list by descending PicNum: [placeholder(2), P1(1), IDR(0)]. The
// placeholder heads the list, so its substitute is the first existing entry
// (P1) — and crucially the two real entries keep their ref_idx positions.
let id0 = p0.dpb.stored.unwrap();
let id1 = p1.dpb.stored.unwrap();
let list0 = &p3.slices[0].ref_list0;
assert_eq!(
list0.iter().map(|r| r.id).collect::<Vec<_>>(),
vec![id1, id1, id0],
"substitution must preserve list length and positions"
);
assert!(list0.iter().all(|r| !r.is_long_term));
}
#[test]
fn a_recovery_point_sei_in_the_au_lands_on_the_picture_plan() {
let (sps, pps) = authored_sps_pps();
let mut au0 = Vec::new();
Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap();
Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap();
// SEI NALU: recovery point, recovery_frame_cnt = 5, exact_match = 1.
au0.extend([0x00, 0x00, 0x00, 0x01, 0x06, 0x06, 0x02, 0x34, 0x40, 0x80]);
au0.extend(write_idr_slice());
let mut planner = H264Planner::new();
let plan = planner.plan_au(&au0).unwrap();
assert_eq!(
plan.picture.recovery_point,
Some(RecoveryPoint {
recovery_frame_cnt: 5,
exact_match: true,
broken_link: false
})
);
// The following AU carries no SEI: the field must not stick.
let au1 = write_p_slice(1, 2, 1, 1, None);
let plan = planner.plan_au(&au1).unwrap();
assert_eq!(plan.picture.recovery_point, None);
}
#[test]
fn an_interlaced_sps_is_rejected_as_outside_the_envelope() {
let sps = SpsBuilder::new()
.seq_parameter_set_id(0)
.profile_idc(Profile::Main)
.level_idc(Level::L4)
.resolution(64, 64)
.frame_mbs_only_flag(false)
.mb_adaptive_frame_field_flag(false)
.direct_8x8_inference_flag(true)
.max_num_ref_frames(4)
.log2_max_frame_num_minus4(0)
.pic_order_cnt_type(0)
.log2_max_pic_order_cnt_lsb_minus4(0)
.build();
let mut au = Vec::new();
Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au, true).unwrap();
let mut planner = H264Planner::new();
assert!(matches!(
planner.plan_au(&au),
Err(PlanError::OutsideEnvelope(_))
));
}
#[test]
fn a_dpb_deeper_than_16_frames_is_rejected_as_outside_the_envelope() {
// The one route past the A.3.1 16-frame cap: the VUI bitstream restriction's
// max_dec_frame_buffering, an unbounded ue(v) that overrides the level-derived
// size in `Sps::max_dpb_frames`. The builder has no VUI-restriction setter, so
// the Sps is constructed directly (its fields are public).
let sps = Sps {
profile_idc: Profile::Main as u8,
level_idc: Level::L4,
frame_mbs_only_flag: true,
direct_8x8_inference_flag: true,
max_num_ref_frames: 4,
vui_parameters_present_flag: true,
vui_parameters: VuiParams {
bitstream_restriction_flag: true,
max_dec_frame_buffering: 17,
..Default::default()
},
..Default::default()
};
let mut au = Vec::new();
Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au, true).unwrap();
let err = H264Planner::new().plan_au(&au).unwrap_err();
assert!(
matches!(err, PlanError::OutsideEnvelope(what) if what.contains("DPB")),
"{err:?}"
);
}
/// MMCO 5 writer: op 5 takes NO argument (Table 7-9), so the generic
/// [`write_p_slice`] — whose supported ops all take exactly one — cannot author
/// it.
fn write_p_slice_mmco5(frame_num: u32, poc_lsb: u32) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut w = NaluWriter::new(&mut buf, true);
w.write_header(1, NaluType::Slice as u8).unwrap();
w.write_ue(0u32).unwrap(); // first_mb_in_slice
w.write_ue(0u32).unwrap(); // slice_type: P
w.write_ue(0u32).unwrap(); // pic_parameter_set_id
w.write_f(4, frame_num).unwrap(); // frame_num, u(4)
w.write_f(4, poc_lsb).unwrap(); // pic_order_cnt_lsb, u(4)
w.write_f(1, 1u32).unwrap(); // num_ref_idx_active_override_flag
w.write_ue(0u32).unwrap(); // num_ref_idx_l0_active_minus1
w.write_f(1, 0u32).unwrap(); // ref_pic_list_modification_flag_l0
w.write_f(1, 1u32).unwrap(); // adaptive_ref_pic_marking_mode_flag
w.write_ue(5u32).unwrap(); // memory_management_control_operation 5
w.write_ue(0u32).unwrap(); // memory_management_control_operation end
w.write_se(0i32).unwrap(); // slice_qp_delta
w.write_f(1, 1u32).unwrap(); // rbsp stop bit
while !w.aligned() {
w.write_f(1, 0u32).unwrap();
}
}
buf
}
#[test]
fn an_mmco_5_is_planned_with_a_rebase_warning_not_rejected() {
let (sps, pps) = authored_sps_pps();
let mut au0 = param_set_au(&sps, &pps);
au0.extend(write_idr_slice());
let au1 = write_p_slice_mmco5(1, 2);
let mut planner = H264Planner::new();
let p0 = planner.plan_au(&au0).unwrap();
let p1 = planner.plan_au(&au1).unwrap();
assert!(p1.warnings.contains(&PlanWarning::Mmco5Rebase));
// The plan carries the pre-rebase 8.2.1 values a decoder submits with; the
// zeroed frame_num/POC exist only in the STORED picture later AUs reference.
assert_eq!(p1.picture.frame_num, 1);
assert_eq!(p1.picture.pic_order_cnt, 2);
// And the op's C.4.5.3 clause-3 drain ran: the IDR is display-ready.
assert!(p1.dpb.outputs.contains(&p0.dpb.stored.unwrap()));
}
#[test]
fn a_separate_colour_plane_sps_is_rejected_as_outside_the_envelope() {
// SpsBuilder has no separate_colour_plane setter; construct the Sps directly
// (its fields are public) — the synthesizer writes the flag for High profile
// with chroma_format_idc 3.
let sps = Sps {
profile_idc: Profile::High as u8,
chroma_format_idc: 3,
separate_colour_plane_flag: true,
frame_mbs_only_flag: true,
..Default::default()
};
let mut au = Vec::new();
Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au, true).unwrap();
let err = H264Planner::new().plan_au(&au).unwrap_err();
assert!(
matches!(err, PlanError::OutsideEnvelope(what) if what.contains("separate")),
"{err:?}"
);
}
#[test]
fn display_crop_reports_the_conformance_window_offset_and_size() {
// chroma_format_idc 1 (inferred for Main) puts CropUnitX/Y at 2 with
// frame_mbs_only: offsets top 2 / bottom 2 / left 4 / right 2 are 4/4/8/4 in
// luma samples.
let sps = base_sps()
.resolution(64, 64)
.frame_crop_offsets(2, 2, 4, 2)
.build();
let pps = PpsBuilder::new(Rc::clone(&sps))
.pic_parameter_set_id(0)
.pic_init_qp(26)
.build();
let mut au = param_set_au(&sps, &pps);
au.extend(write_idr_slice());
let plan = H264Planner::new().plan_au(&au).unwrap();
assert_eq!(
plan.picture.display_crop,
DisplayCrop {
x: 8,
y: 4,
width: 52,
height: 56
}
);
// The review's underflow shape: crop_left 100 (200 luma samples) on a
// 320-wide picture passes SPS validation; a max-minus-min derivation
// underflows on it.
let sps = base_sps()
.resolution(320, 240)
.frame_crop_offsets(0, 0, 100, 0)
.build();
let pps = PpsBuilder::new(Rc::clone(&sps))
.pic_parameter_set_id(0)
.pic_init_qp(26)
.build();
let mut au = param_set_au(&sps, &pps);
au.extend(write_idr_slice());
let plan = H264Planner::new().plan_au(&au).unwrap();
assert_eq!(
plan.picture.display_crop,
DisplayCrop {
x: 200,
y: 0,
width: 120,
height: 240
}
);
}
/// A 64x64 SPS with the VUI colour fields set as given. SpsBuilder has no
/// colour setters, so the built Sps is unwrapped and mutated directly (the
/// separate_colour_plane test's idiom); the synthesizer writes the whole
/// `video_signal_type` block from the struct.
fn sps_with_vui_colour(
signal_type: bool,
full_range: bool,
description: Option<(u8, u8, u8)>,
) -> Rc<Sps> {
let mut sps = Rc::try_unwrap(base_sps().resolution(64, 64).build()).expect("freshly built");
sps.vui_parameters_present_flag = true;
sps.vui_parameters.video_signal_type_present_flag = signal_type;
sps.vui_parameters.video_full_range_flag = full_range;
if let Some((primaries, transfer, matrix)) = description {
sps.vui_parameters.colour_description_present_flag = true;
sps.vui_parameters.colour_primaries = primaries;
sps.vui_parameters.transfer_characteristics = transfer;
sps.vui_parameters.matrix_coefficients = matrix;
}
Rc::new(sps)
}
fn plan_one_idr(sps: &Rc<Sps>) -> AuPlan {
let pps = PpsBuilder::new(Rc::clone(sps))
.pic_parameter_set_id(0)
.pic_init_qp(26)
.build();
let mut au = param_set_au(sps, &pps);
au.extend(write_idr_slice());
H264Planner::new().plan_au(&au).unwrap()
}
#[test]
fn an_sps_without_vui_plans_the_e211_unspecified_colour() {
let (sps, pps) = authored_sps_pps();
assert!(
!sps.vui_parameters_present_flag,
"the base SPS carries no VUI"
);
let mut au = param_set_au(&sps, &pps);
au.extend(write_idr_slice());
let plan = H264Planner::new().plan_au(&au).unwrap();
assert_eq!(
plan.picture.colour,
ColourDescription {
colour_primaries: 2,
transfer_characteristics: 2,
matrix_coefficients: 2,
video_full_range: false,
},
"E.2.1 inference: 'unspecified' code points + limited range, never a raw 0"
);
}
#[test]
fn an_explicit_colour_description_rides_the_plan_and_follows_a_new_sps() {
// BT.2020/PQ HDR signalling — the in-band switch the Windows host emits.
let hdr = sps_with_vui_colour(true, false, Some((9, 16, 9)));
let plan = plan_one_idr(&hdr);
assert_eq!(
plan.picture.colour,
ColourDescription {
colour_primaries: 9,
transfer_characteristics: 16,
matrix_coefficients: 9,
video_full_range: false,
}
);
// The colour must track the SPS active for EACH picture, not the
// session's first: an SDR stream renegotiated to HDR mid-stream (same
// SPS id, new content, SPS+PPS in-band at the IDR — the parser's Pps
// snapshots its SPS at PPS-parse time, and hosts re-send both exactly
// so the new content activates) flips at the very next planned picture.
let (sdr_sps, sdr_pps) = authored_sps_pps();
let mut au0 = param_set_au(&sdr_sps, &sdr_pps);
au0.extend(write_idr_slice());
let mut planner = H264Planner::new();
let plan0 = planner.plan_au(&au0).unwrap();
assert_eq!(plan0.picture.colour.matrix_coefficients, 2);
let hdr_pps = PpsBuilder::new(Rc::clone(&hdr))
.pic_parameter_set_id(0)
.pic_init_qp(26)
.build();
let mut au1 = param_set_au(&hdr, &hdr_pps);
au1.extend(write_idr_slice());
let plan1 = planner.plan_au(&au1).unwrap();
assert_eq!(
plan1.picture.colour.matrix_coefficients, 9,
"the replacing SPS's colour lands on its own picture, not latched"
);
}
#[test]
fn a_vui_without_colour_description_keeps_unspecified_but_honours_the_range_flag() {
// video_signal_type present, full-range set, but NO colour description:
// the code points stay E.2.1's "unspecified" while the range flag rides.
let plan = plan_one_idr(&sps_with_vui_colour(true, true, None));
assert_eq!(
plan.picture.colour,
ColourDescription {
colour_primaries: 2,
transfer_characteristics: 2,
matrix_coefficients: 2,
video_full_range: true,
}
);
}
#[test]
fn a_malformed_nalu_mid_au_truncates_with_a_warning_keeping_prior_slices() {
let (sps, pps) = authored_sps_pps();
let mut au = param_set_au(&sps, &pps);
au.extend(write_idr_slice());
// Reserved NAL type 24: the vendored header parser rejects it.
au.extend([0x00, 0x00, 0x00, 0x01, 0x18, 0xAA, 0xBB]);
au.extend(write_idr_slice()); // real data behind the cut, never reached
let plan = H264Planner::new().plan_au(&au).unwrap();
assert_eq!(
plan.slices.len(),
1,
"only the slice before the cut is planned"
);
assert!(plan
.warnings
.iter()
.any(|w| matches!(w, PlanWarning::TruncatedAu { .. })));
}
#[test]
fn a_foreign_slice_in_the_au_is_dropped_with_a_truncated_au_warning() {
let (sps, pps) = authored_sps_pps();
let mut au = param_set_au(&sps, &pps);
au.extend(write_idr_slice());
// A mis-split AU: continuation slices belonging to ANOTHER picture (non-IDR,
// frame_num 1). They and everything after them must be ignored.
au.extend(write_p_slice_at(8, 0, 1, 2, 1, 1, None));
au.extend(write_p_slice_at(9, 0, 1, 2, 1, 1, None));
let plan = H264Planner::new().plan_au(&au).unwrap();
assert!(plan.picture.is_idr);
assert_eq!(plan.slices.len(), 1, "the foreign slices are not planned");
assert!(plan
.warnings
.iter()
.any(|w| matches!(w, PlanWarning::TruncatedAu { .. })));
}
#[test]
fn outputs_queued_during_a_failed_au_surface_in_the_next_successful_plan() {
let (sps, pps) = authored_sps_pps();
let mut au0 = param_set_au(&sps, &pps);
au0.extend(write_idr_slice());
let mut planner = H264Planner::new();
let id0 = planner.plan_au(&au0).unwrap().dpb.stored.unwrap();
// This AU errors AFTER its IDR begin drained the DPB (queueing id0 for
// output): the continuation slice references PPS 1, which was never sent.
let mut bad_au = write_idr_slice();
bad_au.extend(write_p_slice_at(8, 1, 0, 0, 1, 1, None));
assert!(matches!(
planner.plan_au(&bad_au),
Err(PlanError::NoActiveParamSet { pps_id: 1 })
));
// The queued output and the eviction must surface here, not vanish.
let plan = planner.plan_au(&write_idr_slice()).unwrap();
assert!(plan.dpb.outputs.contains(&id0));
assert!(plan.dpb.removed.contains(&id0));
}
#[test]
fn flush_resets_decoding_state_and_refuses_non_idr_until_an_idr_arrives() {
let (sps, pps) = authored_sps_pps();
let mut au0 = param_set_au(&sps, &pps);
au0.extend(write_idr_slice());
let mut planner = H264Planner::new();
let id0 = planner.plan_au(&au0).unwrap().dpb.stored.unwrap();
let id1 = planner
.plan_au(&write_p_slice(1, 2, 1, 1, None))
.unwrap()
.dpb
.stored
.unwrap();
let flushed = planner.flush();
assert!(flushed.outputs.contains(&id0) && flushed.outputs.contains(&id1));
assert_eq!(flushed.removed, vec![id0, id1]);
// A non-IDR AU is refused until the next IDR.
assert!(matches!(
planner.plan_au(&write_p_slice(2, 4, 1, 1, None)),
Err(PlanError::AwaitingIdr)
));
// The IDR restarts planning; parameter sets survived the flush (7.4.1.2).
let plan = planner.plan_au(&write_idr_slice()).unwrap();
assert!(plan.picture.is_idr);
assert_eq!(plan.picture.pic_order_cnt, 0);
assert!(plan.warnings.is_empty());
// And the stream continues cleanly on the reset state.
let plan = planner.plan_au(&write_p_slice(1, 2, 1, 1, None)).unwrap();
assert!(plan.warnings.is_empty());
assert_eq!(plan.slices[0].ref_list0.len(), 1);
}
#[test]
fn picture_plan_parameters_come_from_the_first_slices_pps() {
// Two SPSes with identical negotiation parameters but different conformance
// windows; PPS 1 references the cropped one.
let sps0 = base_sps().resolution(64, 64).build();
let sps1 = base_sps()
.seq_parameter_set_id(1)
.resolution(64, 64)
.frame_crop_offsets(2, 2, 4, 2)
.build();
let pps0 = PpsBuilder::new(Rc::clone(&sps0))
.pic_parameter_set_id(0)
.pic_init_qp(26)
.build();
let pps1 = PpsBuilder::new(Rc::clone(&sps1))
.pic_parameter_set_id(1)
.pic_init_qp(26)
.build();
let mut au = Vec::new();
Synthesizer::<'_, Sps, _>::synthesize(3, &sps0, &mut au, true).unwrap();
Synthesizer::<'_, Sps, _>::synthesize(3, &sps1, &mut au, true).unwrap();
Synthesizer::<'_, Pps, _>::synthesize(3, &pps0, &mut au, true).unwrap();
Synthesizer::<'_, Pps, _>::synthesize(3, &pps1, &mut au, true).unwrap();
au.extend(write_idr_slice_at(0, 0));
// Legal but perverse: a continuation slice may reference another PPS.
au.extend(write_idr_slice_at(8, 1));
let plan = H264Planner::new().plan_au(&au).unwrap();
assert!(plan.warnings.is_empty());
assert_eq!(plan.slices.len(), 2);
// The picture parameters come from the FIRST slice's PPS (the uncropped
// SPS 0); they must not drift to the last slice's.
assert_eq!(
plan.picture.display_crop,
DisplayCrop {
x: 0,
y: 0,
width: 64,
height: 64
}
);
// The accessor pair follows the same first-slice rule: backends build
// their parameter objects from these, so drifting to PPS 1 here would
// desynchronize them from `picture`.
assert_eq!(plan.pps.pic_parameter_set_id, 0);
assert_eq!(plan.sps.seq_parameter_set_id, 0);
assert!(
Rc::ptr_eq(&plan.sps, &plan.pps.sps),
"the SPS accessor is the PPS's own SPS, not a second copy"
);
assert!(
!plan.sps.frame_cropping_flag,
"SPS 0, not the cropped SPS 1"
);
}
#[test]
fn the_plans_parameter_set_accessors_carry_the_activated_content() {
let (sps, pps) = authored_sps_pps();
let mut au0 = param_set_au(&sps, &pps);
au0.extend(write_idr_slice());
let plan = H264Planner::new().plan_au(&au0).unwrap();
// The parser re-parses the in-band parameter sets, so pointer identity
// with the authored `sps`/`pps` is not expected (and whole-struct
// equality would compare parser-side normalizations like the flat
// scaling-list fill); the contract is that the ACTIVATED content rides
// out. Spot-check the fields backends build parameter objects from.
assert_eq!(plan.sps.seq_parameter_set_id, sps.seq_parameter_set_id);
assert_eq!(plan.sps.profile_idc, sps.profile_idc);
assert_eq!(plan.sps.level_idc, sps.level_idc);
assert_eq!(plan.sps.max_num_ref_frames, sps.max_num_ref_frames);
assert_eq!(plan.sps.width(), sps.width());
assert_eq!(plan.sps.height(), sps.height());
assert_eq!(plan.pps.pic_parameter_set_id, pps.pic_parameter_set_id);
assert_eq!(plan.pps.seq_parameter_set_id, pps.seq_parameter_set_id);
assert_eq!(plan.pps.pic_init_qp_minus26, pps.pic_init_qp_minus26);
assert!(Rc::ptr_eq(&plan.sps, &plan.pps.sps));
}
}