Skip to main content

oxideav_evc/
lib.rs

1//! Pure-Rust **EVC** — MPEG-5 Essential Video Coding (ISO/IEC 23094-1).
2//!
3//! Round-10 status: a working **Baseline-profile IDR + P + B** decoder
4//! with residual coding (RLE + dequant + IDCT for nTbS up to 64),
5//! deblocking (§8.8.2 luma + chroma path), Main-profile CABAC init
6//! tables (Tables 40-90, §9.3.4.2 ctxInc helpers, eq. 1425/1426),
7//! full **reference-picture-list parsing** (§7.3.7 / §7.4.8) for
8//! non-IDR slices (`sps_rpl_flag = 1`), the **HMVP candidate list**
9//! (§8.5.2.7 / §8.5.2.4.4) wired through the inter pipeline, the
10//! round-9 **multi-ref DPB + POC reordering**, and the round-10
11//! **spatial-neighbour MV grid AMVP** (§8.5.2.4) +
12//! **LTRP RPL → DPB resolution** (§8.3.2 / §8.3.5) +
13//! **`flush()` drain** of the output queue.
14//!
15//! The crate decomposes into:
16//!
17//! * [`bitreader`] — MSB-first bit reader (§9.2 helpers).
18//! * [`nal`] — 2-byte NAL header (§7.3.1.2) + length-prefixed framing.
19//! * [`sps`] / [`pps`] / [`aps`] — parameter-set parsers (§7.3.2.x).
20//! * [`slice_header`] — `slice_header()` parse (§7.3.4).
21//! * [`cabac`] — full CABAC parsing process (§9.3): arithmetic decoding
22//!   engine (regular + bypass + terminate) plus the FL / U / TR / EGk
23//!   binarization helpers. The Baseline `sps_cm_init_flag == 0` path uses
24//!   a single ctxTable=0 / ctxIdx=0 context.
25//! * [`cabac_init`] — Main-profile (`sps_cm_init_flag == 1`) initValue
26//!   tables (Tables 40-90 of §9.3.5) + the §9.3.2.2 init pipeline
27//!   ([`cabac_init::init_main_profile_contexts`]) + the §9.3.4.2 per-
28//!   syntax-element ctxInc helpers (`btt_split_flag`,
29//!   `last_sig_coeff_x/y_prefix`, `sig_coeff_flag`,
30//!   `coeff_abs_level_greaterA/B_flag`, etc.).
31//! * [`slice_data`] — `slice_data()` walker plus the round-3 IDR pixel
32//!   pipeline ([`slice_data::decode_baseline_idr_slice`]) **and** the
33//!   round-4 inter pipeline ([`slice_data::decode_baseline_inter_slice`]).
34//! * [`intra`] — intra prediction (§8.4.4) for the Baseline 5-mode set
35//!   (DC, HOR, VER, UL, UR; `sps_eipd_flag == 0` path).
36//! * [`inter`] — round-4 inter prediction (§8.5): MV resolution + 8-tap
37//!   luma + 4-tap chroma sub-pel interpolation (Tables 25 / 27 — Baseline
38//!   subset only) + AMVP candidate construction + default-weighted bipred.
39//! * [`transform`] — inverse DCT-II for nTbS ∈ {2, 4, 8, 16, 32, 64}
40//!   (eq. 1062-1076). The 64-point matrix is built from the closed-form
41//!   `M[m][n] = round(64·√2·cos(π·m·(2n+1)/128))` (m≥1, M[0][n]=64),
42//!   verified against every printed entry of eq. 1072 / 1074.
43//! * [`dequant`] — scaling + transform + final renorm (§8.7.2 / §8.7.3 /
44//!   §8.7.4) for the `sps_iqt_flag == 0` Baseline path.
45//! * [`picture`] — yuv420p 8-bit picture buffer + per-CU intra
46//!   reconstruct glue (§8.7.5).
47//! * [`decoder`] — registered decoder factory returning a working
48//!   `Decoder` for Baseline IDR + P/B bitstreams (8-bit 4:2:0, no
49//!   residuals, single reference).
50//! * [`rpl`] — round-8 `ref_pic_list_struct()` parser (§7.3.7 /
51//!   §7.4.8). Handles STRP + LTRP entries with the per-entry
52//!   `delta_poc_st` / `strp_entry_sign_flag` / `poc_lsb_lt` shape.
53//! * [`hmvp`] — round-8 history-based MV prediction (§8.5.2.7 LRU
54//!   update + §8.5.2.4.4 derive_default_mv walk). 23-entry list with
55//!   per-CTU-row reset.
56//!
57//! Round-8 deliberate omissions (pending follow-up rounds):
58//!
59//! * 10-bit support,
60//! * advanced deblocking (`sps_addb_flag = 1` — round-6 supports the
61//!   `sps_addb_flag = 0` baseline filter only, now for both luma and
62//!   chroma; addb is a Main-profile feature),
63//! * multi-reference DPB + reference list reordering (round 8 parses
64//!   the RPL and the slice-side `num_ref_idx_active_minus1[]`, but the
65//!   inter pipeline still keys off the previous picture only),
66//! * **Main-profile decode** — round 7 lands the CABAC infrastructure
67//!   (Tables 40-90 + ctxInc helpers); round 8 lands the RPL parse path
68//!   and the HMVP candidate list. The actual Main-profile syntax
69//!   decode (BTT / SUCO / ADMVP / EIPD / IBC / ATS / ADCC / ALF / DRA
70//!   / AMVR / MMVD / affine / DMVR) still bubbles up
71//!   `Error::Unsupported`.
72//!
73//! All section / clause numbers refer to **ISO/IEC 23094-1:2020(E)** at
74//! `docs/video/evc/ISO_IEC_23094-1-EVC-2020.pdf`. Every module is
75//! spec-only — clauses, equations, and table numbers cite the
76//! Recommendation directly.
77
78pub mod alf;
79pub mod alf_tables;
80pub mod aps;
81pub mod bitreader;
82pub mod cabac;
83pub mod cabac_init;
84pub mod deblock;
85pub mod decoder;
86pub mod dequant;
87pub mod dra;
88pub mod hmvp;
89pub mod ibc;
90pub mod inter;
91pub mod intra;
92pub mod nal;
93pub mod neighbour;
94pub mod picture;
95pub mod pps;
96pub mod rpl;
97pub mod scan;
98pub mod slice_data;
99pub mod slice_header;
100pub mod sps;
101pub mod transform;
102
103use oxideav_core::{CodecCapabilities, CodecId, CodecTag};
104use oxideav_core::{CodecInfo, CodecRegistry};
105
106/// Public codec id string. Matches the aggregator feature name `evc`.
107pub const CODEC_ID_STR: &str = "evc";
108
109/// Summary info recoverable from a bare EVC SPS — the public return type
110/// of [`probe`].
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub struct EvcFileInfo {
113    pub width: u32,
114    pub height: u32,
115    pub profile_idc: u8,
116    pub level_idc: u8,
117    pub bit_depth_luma: u32,
118    pub bit_depth_chroma: u32,
119    pub chroma_format_idc: u32,
120}
121
122/// Probe a buffer for an EVC bitstream and return summary info from the
123/// first parseable SPS. Accepts either Annex B raw-bitstream framing
124/// (`u(32)` length prefix, the canonical case per ISO/IEC 23094-1 Annex B)
125/// or the tolerant `0x000001` / `0x00000001` start-code scanner.
126///
127/// Returns `None` when no SPS is found; bubbles up `Some(_)` for the
128/// first SPS that parses cleanly.
129pub fn probe(input: &[u8]) -> Option<EvcFileInfo> {
130    // Try length-prefixed first — that's what Annex B specifies.
131    if let Ok(nals) = nal::iter_length_prefixed(input) {
132        for nal_ref in nals {
133            if let Some(info) = info_from_nal(&nal_ref) {
134                return Some(info);
135            }
136        }
137    }
138    // Fall back to the tolerant Annex-B-style scanner for ad-hoc files.
139    for nal_ref in nal::iter_annex_b(input) {
140        if let Some(info) = info_from_nal(&nal_ref) {
141            return Some(info);
142        }
143    }
144    None
145}
146
147fn info_from_nal(nal_ref: &nal::NalRef<'_>) -> Option<EvcFileInfo> {
148    if nal_ref.header.nal_unit_type != nal::NalUnitType::Sps {
149        return None;
150    }
151    let sps = sps::parse(nal_ref.rbsp()).ok()?;
152    Some(EvcFileInfo {
153        width: sps.pic_width_in_luma_samples,
154        height: sps.pic_height_in_luma_samples,
155        profile_idc: sps.profile_idc,
156        level_idc: sps.level_idc,
157        bit_depth_luma: sps.bit_depth_y(),
158        bit_depth_chroma: sps.bit_depth_c(),
159        chroma_format_idc: sps.chroma_format_idc,
160    })
161}
162
163/// Walk an IDR slice's `slice_data()` end-to-end given the active SPS and
164/// PPS. The slice's RBSP is split into the slice header + the
165/// (byte-aligned) slice-data payload; this helper invokes the slice
166/// header parser then drives [`slice_data::walk_baseline_idr_slice`]
167/// across the rest of the RBSP. Returns the [`slice_data::SliceWalkStats`]
168/// once the engine terminates cleanly.
169///
170/// **Round-2 scope**: Baseline-profile IDR slices only. Errors out on any
171/// SPS toolset combination not yet supported by the walker (see
172/// [`slice_data::walk_baseline_idr_slice`] for the constraint set).
173pub fn walk_idr_slice(
174    sps: &sps::Sps,
175    pps: &pps::Pps,
176    slice_nal_rbsp: &[u8],
177) -> oxideav_core::Result<slice_data::SliceWalkStats> {
178    use oxideav_core::Error;
179    // Build the slice-parse context from SPS + PPS.
180    let ctx = slice_header::SliceParseContext {
181        single_tile_in_pic_flag: pps.single_tile_in_pic_flag,
182        arbitrary_slice_present_flag: pps.arbitrary_slice_present_flag,
183        tile_id_len_minus1: pps.tile_id_len_minus1,
184        num_tile_columns_minus1: pps.num_tile_columns_minus1,
185        num_tile_rows_minus1: pps.num_tile_rows_minus1,
186        sps_pocs_flag: sps.sps_pocs_flag,
187        sps_rpl_flag: sps.sps_rpl_flag,
188        sps_alf_flag: sps.sps_alf_flag,
189        sps_mmvd_flag: sps.sps_mmvd_flag,
190        sps_admvp_flag: sps.sps_admvp_flag,
191        sps_addb_flag: sps.sps_addb_flag,
192        log2_max_pic_order_cnt_lsb_minus4: sps.log2_max_pic_order_cnt_lsb_minus4,
193        chroma_array_type: sps.chroma_array_type(),
194        num_ref_pic_lists_in_sps_l0: sps.num_ref_pic_lists_in_sps_l0,
195        num_ref_pic_lists_in_sps_l1: sps.num_ref_pic_lists_in_sps_l1,
196        rpl1_idx_present_flag: pps.rpl1_idx_present_flag,
197        long_term_ref_pics_flag: sps.long_term_ref_pics_flag,
198        additional_lt_poc_lsb_len: pps.additional_lt_poc_lsb_len,
199    };
200    // Round-2 walker requires the Baseline profile constraint set (Annex
201    // A.3.2). Refuse anything else cleanly.
202    // sps_alf_flag and sps_dra_flag are no longer gated here (round-11
203    // handles them as post-filter passes).
204    if sps.sps_btt_flag
205        || sps.sps_suco_flag
206        || sps.sps_admvp_flag
207        || sps.sps_eipd_flag
208        || sps.sps_addb_flag
209        || sps.sps_dquant_flag
210        || sps.sps_ats_flag
211        || sps.sps_adcc_flag
212        || sps.sps_cm_init_flag
213        || sps.sps_amvr_flag
214        || sps.sps_mmvd_flag
215        || sps.sps_affine_flag
216        || sps.sps_dmvr_flag
217        || sps.sps_hmvp_flag
218    {
219        return Err(Error::unsupported(
220            "evc walk_idr_slice: round-2 only supports Baseline-profile toolset",
221        ));
222    }
223    if !pps.single_tile_in_pic_flag {
224        return Err(Error::unsupported(
225            "evc walk_idr_slice: round-2 requires single_tile_in_pic_flag == 1",
226        ));
227    }
228    // Parse the slice header to find where slice_data() begins. We use a
229    // bit-counting BitReader to determine how many bits the header
230    // consumed.
231    let mut hdr_br = crate::bitreader::BitReader::new(slice_nal_rbsp);
232    parse_slice_header_consume(&mut hdr_br, nal::NalUnitType::Idr, &ctx)?;
233    // Align to the next byte boundary (slice_data() starts byte-aligned).
234    hdr_br.align_to_byte();
235    let consumed_bits = hdr_br.bit_position();
236    if consumed_bits % 8 != 0 {
237        return Err(Error::invalid(
238            "evc walk_idr_slice: slice header not byte-aligned after parse",
239        ));
240    }
241    let consumed_bytes = (consumed_bits / 8) as usize;
242    if consumed_bytes >= slice_nal_rbsp.len() {
243        return Err(Error::invalid(
244            "evc walk_idr_slice: no slice_data bytes after header",
245        ));
246    }
247    let slice_data_bytes = &slice_nal_rbsp[consumed_bytes..];
248    // Build SliceWalkInputs from SPS / PPS. Round 90 surfaces the IBC
249    // SPS gates so the coding_unit() walker can apply §7.4.5
250    // `isIbcAllowed` per-CU.
251    let ctb_log2_size_y = sps.log2_ctu_size_minus5 + 5;
252    let min_cb_log2_size_y = sps.log2_min_cb_size_minus2 + 2;
253    let max_tb_log2_size_y = ctb_log2_size_y.min(6);
254    let log2_max_ibc_cand_size = sps.log2_max_ibc_cand_size().unwrap_or(0);
255    let inputs = slice_data::SliceWalkInputs {
256        pic_width: sps.pic_width_in_luma_samples,
257        pic_height: sps.pic_height_in_luma_samples,
258        ctb_log2_size_y,
259        min_cb_log2_size_y,
260        max_tb_log2_size_y,
261        chroma_format_idc: sps.chroma_format_idc,
262        cu_qp_delta_enabled: pps.cu_qp_delta_enabled_flag,
263        sps_ibc_flag: sps.sps_ibc_flag,
264        log2_max_ibc_cand_size,
265        // This entry point uses a minimal header parse that doesn't yet
266        // surface the §7.3.4 ALF map fields; default them off (no
267        // per-CTU ALF map signalled) so `coding_tree_unit()` reads no
268        // `alf_ctb_*` bins. `decode_non_idr` threads the real values.
269        slice_alf_enabled_flag: false,
270        slice_alf_map_flag: false,
271        slice_chroma_alf_enabled_flag: false,
272        slice_alf_chroma_map_flag: false,
273        slice_chroma2_alf_enabled_flag: false,
274        slice_alf_chroma2_map_flag: false,
275    };
276    slice_data::walk_baseline_idr_slice(slice_data_bytes, inputs)
277}
278
279/// **Round-3** end-to-end decode of a Baseline-profile IDR slice.
280///
281/// Mirrors [`walk_idr_slice`] but invokes the pixel-emission pipeline:
282/// returns a freshly-reconstructed [`picture::YuvPicture`] populated by
283/// per-CU intra prediction and the spec's picture-construction step
284/// (§8.7.5). Round-3 fixtures must use `cbf_luma == cbf_cb == cbf_cr ==
285/// 0` for every CU; non-zero CBFs trigger `Error::Unsupported` (round-4
286/// scope).
287pub fn decode_idr_slice(
288    sps: &sps::Sps,
289    pps: &pps::Pps,
290    slice_nal_rbsp: &[u8],
291) -> oxideav_core::Result<(picture::YuvPicture, slice_data::SliceDecodeStats)> {
292    use oxideav_core::Error;
293    if sps.sps_btt_flag
294        || sps.sps_suco_flag
295        || sps.sps_admvp_flag
296        || sps.sps_eipd_flag
297        || sps.sps_addb_flag
298        || sps.sps_dquant_flag
299        || sps.sps_ats_flag
300        || sps.sps_adcc_flag
301        || sps.sps_cm_init_flag
302    {
303        return Err(Error::unsupported(
304            "evc decode_idr_slice: round-3 only supports Baseline-profile toolset",
305        ));
306    }
307    // sps_alf_flag and sps_dra_flag are handled by the round-11 post-filter
308    // pipeline and no longer gate this function.
309    if !pps.single_tile_in_pic_flag {
310        return Err(Error::unsupported(
311            "evc decode_idr_slice: round-3 requires single_tile_in_pic_flag == 1",
312        ));
313    }
314    let mut hdr_br = crate::bitreader::BitReader::new(slice_nal_rbsp);
315    let slice_qp = parse_slice_header_for_decode(&mut hdr_br, sps, pps)?;
316    hdr_br.align_to_byte();
317    let consumed_bits = hdr_br.bit_position();
318    if consumed_bits % 8 != 0 {
319        return Err(Error::invalid(
320            "evc decode_idr_slice: slice header not byte-aligned after parse",
321        ));
322    }
323    let consumed_bytes = (consumed_bits / 8) as usize;
324    if consumed_bytes >= slice_nal_rbsp.len() {
325        return Err(Error::invalid(
326            "evc decode_idr_slice: no slice_data bytes after header",
327        ));
328    }
329    let slice_data_bytes = &slice_nal_rbsp[consumed_bytes..];
330    let ctb_log2_size_y = sps.log2_ctu_size_minus5 + 5;
331    let min_cb_log2_size_y = sps.log2_min_cb_size_minus2 + 2;
332    let max_tb_log2_size_y = ctb_log2_size_y.min(5); // round-3: cap at 32x32
333    let log2_max_ibc_cand_size = sps.log2_max_ibc_cand_size().unwrap_or(0);
334    let walk = slice_data::SliceWalkInputs {
335        pic_width: sps.pic_width_in_luma_samples,
336        pic_height: sps.pic_height_in_luma_samples,
337        ctb_log2_size_y,
338        min_cb_log2_size_y,
339        max_tb_log2_size_y,
340        chroma_format_idc: sps.chroma_format_idc,
341        cu_qp_delta_enabled: pps.cu_qp_delta_enabled_flag,
342        sps_ibc_flag: sps.sps_ibc_flag,
343        log2_max_ibc_cand_size,
344        // Minimal-header entry point: ALF map fields default off.
345        slice_alf_enabled_flag: false,
346        slice_alf_map_flag: false,
347        slice_chroma_alf_enabled_flag: false,
348        slice_alf_chroma_map_flag: false,
349        slice_chroma2_alf_enabled_flag: false,
350        slice_alf_chroma2_map_flag: false,
351    };
352    let decode = slice_data::SliceDecodeInputs {
353        slice_qp,
354        bit_depth_luma: sps.bit_depth_y(),
355        bit_depth_chroma: sps.bit_depth_c(),
356        enable_deblock: false, // round-3 fixtures keep deblock off
357        slice_cb_qp_offset: 0,
358        slice_cr_qp_offset: 0,
359        sps_ibc_flag: sps.sps_ibc_flag,
360        log2_max_ibc_cand_size,
361    };
362    slice_data::decode_baseline_idr_slice(slice_data_bytes, walk, decode)
363}
364
365/// Helper for [`decode_idr_slice`]: parse the Baseline IDR slice header
366/// and recover `slice_qp` for downstream dequant. Round-3 supports the
367/// minimal set: `slice_pps_id`, `slice_type` (must be 2), trailing
368/// flags, `slice_qp ∈ 0..=51`, `slice_cb_qp_offset`, `slice_cr_qp_offset`.
369fn parse_slice_header_for_decode(
370    br: &mut crate::bitreader::BitReader,
371    _sps: &sps::Sps,
372    _pps: &pps::Pps,
373) -> oxideav_core::Result<i32> {
374    use oxideav_core::Error;
375    let _slice_pps_id = br.ue()?;
376    let slice_type = br.ue()?;
377    if slice_type != 2 {
378        return Err(Error::invalid(format!(
379            "evc decode_idr_slice: IDR slice_type must be 2 (got {slice_type})"
380        )));
381    }
382    let _no_output = br.u1()?;
383    let _slice_deblocking_filter_flag = br.u1()?;
384    let slice_qp = br.u(6)?;
385    if slice_qp > 51 {
386        return Err(Error::invalid(format!(
387            "evc decode_idr_slice: slice_qp {slice_qp} > 51"
388        )));
389    }
390    let _slice_cb_qp_offset = br.se()?;
391    let _slice_cr_qp_offset = br.se()?;
392    Ok(slice_qp as i32)
393}
394
395/// Internal helper that re-runs the slice header parse on a *borrowed*
396/// BitReader so the caller can recover the header bit position. Mirrors
397/// [`slice_header::parse`] but takes a mutable reference. Round-3 may
398/// fold this into the public `slice_header::parse` once the active-PS
399/// tracker lands.
400fn parse_slice_header_consume(
401    br: &mut crate::bitreader::BitReader,
402    _nal_unit_type: nal::NalUnitType,
403    _ctx: &slice_header::SliceParseContext,
404) -> oxideav_core::Result<()> {
405    use oxideav_core::Error;
406    let _slice_pps_id = br.ue()?;
407    // Baseline + IDR + single_tile_in_pic_flag = no tile fields.
408    let slice_type = br.ue()?;
409    if slice_type != 2 {
410        return Err(Error::invalid(format!(
411            "evc walk_idr_slice: IDR slice_type must be 2 (got {slice_type})"
412        )));
413    }
414    let _no_output = br.u1()?;
415    // sps_mmvd_flag, sps_alf_flag = 0 in Baseline → skip.
416    // IDR + sps_pocs_flag is irrelevant (POC LSB not in IDR header).
417    // sps_rpl_flag = 0 → no RPL fields.
418    // not P/B → no ref_idx fields.
419    let _slice_deblocking_filter_flag = br.u1()?;
420    // sps_addb_flag = 0 → no alpha/beta offsets.
421    let slice_qp = br.u(6)?;
422    if slice_qp > 51 {
423        return Err(Error::invalid(format!(
424            "evc walk_idr_slice: slice_qp {slice_qp} > 51"
425        )));
426    }
427    let _slice_cb_qp_offset = br.se()?;
428    let _slice_cr_qp_offset = br.se()?;
429    Ok(())
430}
431
432/// Register the EVC implementation (currently parser-only) with a codec
433/// registry. The registered decoder factory returns an unsupported-error
434/// decoder per the round-1 deliverable.
435pub fn register(reg: &mut CodecRegistry) {
436    let caps = CodecCapabilities::video("evc_sw")
437        .with_lossy(true)
438        .with_intra_only(false)
439        .with_max_size(sps::MAX_DIMENSION, sps::MAX_DIMENSION);
440    // ISOBMFF sample-description FourCCs registered for EVC by
441    // ISO/IEC 14496-15 (clauses 12 / 13):
442    //   `evc1` — track-stored EVC
443    //   `evcC` — EVCDecoderConfigurationRecord box code (atom name, kept
444    //   for completeness so callers carrying a 4-cc atom can locate us).
445    reg.register(
446        CodecInfo::new(CodecId::new(CODEC_ID_STR))
447            .capabilities(caps)
448            .decoder(decoder::make_decoder)
449            .tags([
450                CodecTag::fourcc(b"evc1"),
451                CodecTag::fourcc(b"evcC"),
452                CodecTag::fourcc(b"EVC1"),
453            ]),
454    );
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use crate::sps::tests::BitEmitter;
461
462    /// Build a single-NAL length-prefixed EVC bitstream containing a
463    /// minimal 320x240 4:2:0 8-bit SPS, suitable for exercising `probe`.
464    fn build_minimal_sps_stream() -> Vec<u8> {
465        // SPS body
466        let mut body = BitEmitter::new();
467        body.ue(0); // sps_seq_parameter_set_id
468        body.u(8, 1); // profile_idc
469        body.u(8, 30); // level_idc
470        body.u(32, 0); // toolset_idc_h
471        body.u(32, 0); // toolset_idc_l
472        body.ue(1); // chroma_format_idc
473        body.ue(320); // pic_width_in_luma_samples
474        body.ue(240); // pic_height_in_luma_samples
475        body.ue(0); // bit_depth_luma_minus8
476        body.ue(0); // bit_depth_chroma_minus8
477        for _ in 0..13 {
478            body.u(1, 0);
479        }
480        body.ue(1); // log2_sub_gop_length
481        body.ue(1); // max_num_tid0_ref_pics
482        body.u(1, 0); // picture_cropping_flag
483        body.u(1, 0); // chroma_qp_table_present_flag
484        body.u(1, 0); // vui_parameters_present_flag
485        body.finish_with_trailing_bits();
486        let sps_rbsp = body.into_bytes();
487
488        // 2-byte NAL header for SPS (NUT=24)
489        let nut_plus1: u16 = 24 + 1;
490        let mut hdr_word: u16 = 0;
491        hdr_word |= (nut_plus1 & 0x3F) << 9;
492        // tid 0, reserved 0, ext 0 — leaves the lower bits zero
493        let hdr = [(hdr_word >> 8) as u8, (hdr_word & 0xFF) as u8];
494
495        // Build the length-prefixed envelope: [u32 len BE][NAL]
496        let nal_len = (hdr.len() + sps_rbsp.len()) as u32;
497        let mut out = Vec::new();
498        out.extend_from_slice(&nal_len.to_be_bytes());
499        out.extend_from_slice(&hdr);
500        out.extend_from_slice(&sps_rbsp);
501        out
502    }
503
504    #[test]
505    fn probe_minimal_sps_stream() {
506        let bs = build_minimal_sps_stream();
507        let info = probe(&bs).expect("probe must recover SPS dimensions");
508        assert_eq!(info.width, 320);
509        assert_eq!(info.height, 240);
510        assert_eq!(info.bit_depth_luma, 8);
511        assert_eq!(info.bit_depth_chroma, 8);
512        assert_eq!(info.chroma_format_idc, 1);
513        assert_eq!(info.profile_idc, 1);
514        assert_eq!(info.level_idc, 30);
515    }
516
517    #[test]
518    fn probe_returns_none_on_empty() {
519        assert!(probe(&[]).is_none());
520    }
521
522    #[test]
523    fn probe_returns_none_on_pps_only() {
524        // PPS NAL with empty body — should not satisfy probe.
525        let nut_plus1: u16 = 25 + 1;
526        let mut hdr_word: u16 = 0;
527        hdr_word |= (nut_plus1 & 0x3F) << 9;
528        let hdr = [(hdr_word >> 8) as u8, (hdr_word & 0xFF) as u8];
529        let mut pps_body = BitEmitter::new();
530        pps_body.ue(0); // pps id
531        pps_body.ue(0); // sps id
532        pps_body.ue(0);
533        pps_body.ue(0);
534        pps_body.ue(0);
535        pps_body.u(1, 0);
536        pps_body.u(1, 1); // single_tile_in_pic_flag
537        pps_body.ue(0); // tile_id_len_minus1
538        pps_body.u(1, 0);
539        pps_body.u(1, 0);
540        pps_body.u(1, 0);
541        pps_body.u(1, 0);
542        pps_body.u(1, 0);
543        pps_body.finish_with_trailing_bits();
544        let body = pps_body.into_bytes();
545        let nal_len = (hdr.len() + body.len()) as u32;
546        let mut out = Vec::new();
547        out.extend_from_slice(&nal_len.to_be_bytes());
548        out.extend_from_slice(&hdr);
549        out.extend_from_slice(&body);
550        assert!(probe(&out).is_none());
551    }
552
553    #[test]
554    fn register_creates_factory() {
555        let mut reg = CodecRegistry::default();
556        register(&mut reg);
557        // The registry must know the "evc" codec id and have a decoder
558        // implementation registered.
559        assert!(reg.has_decoder(&CodecId::new(CODEC_ID_STR)));
560    }
561
562    #[test]
563    fn make_decoder_handles_empty_packet() {
564        use oxideav_core::{CodecParameters, Packet, TimeBase};
565        let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
566        let mut dec = decoder::make_decoder(&params).unwrap();
567        let pkt = Packet::new(0, TimeBase::new(1, 90_000), Vec::new());
568        // Empty packet → no NALs → ok, no frame emitted.
569        dec.send_packet(&pkt).unwrap();
570        let err = dec.receive_frame().unwrap_err();
571        // Iterating with no input must surface NeedMore (not Eof).
572        assert!(matches!(err, oxideav_core::Error::NeedMore));
573    }
574
575    /// End-to-end Baseline-IDR walk: build an SPS + PPS in-memory,
576    /// hand-craft a slice header for an IDR slice, append a CABAC-encoded
577    /// `slice_data()` payload, then run [`walk_idr_slice`] across it
578    /// and verify every bin is consumed cleanly through the
579    /// `end_of_tile_one_bit` terminate decision.
580    ///
581    /// This is the round-2 deliverable's "real (small) EVC IDR slice has
582    /// its bins consumed cleanly" milestone. The slice covers a 64×64
583    /// picture (a single 64×64 CTU explicitly split via `split_cu_flag = 1`
584    /// into four 32×32 leaves; each leaf then declines to split via
585    /// `split_cu_flag = 0` since the walker permits more recursion under
586    /// the Baseline default `MinCbLog2SizeY = 2`). Each leaf goes through
587    /// the dual-tree luma + chroma `coding_unit()` pair with no CBFs set.
588    ///
589    /// Bin sequence (21 regular bins + terminate):
590    /// * 1 × `split_cu_flag = 1` (CTB)
591    /// * 4 × `split_cu_flag = 0` (each 32×32 child)
592    /// * 4 × (intra_pred_mode + cbf_luma + cbf_cb + cbf_cr) = 16
593    #[test]
594    fn end_to_end_walk_baseline_idr_slice() {
595        use crate::cabac::CabacEncoder;
596        // Build a Baseline-profile SPS for 64×64 4:2:0 8-bit. Default
597        // toolset (sps_btt = 0) leaves the parser at CtbLog2SizeY = 6
598        // (64×64 CTU) and MinCbLog2SizeY = 2 (4×4 minimum CB).
599        let mut sps_body = BitEmitter::new();
600        sps_body.ue(0); // sps_id
601        sps_body.u(8, 0); // profile_idc = Baseline (0)
602        sps_body.u(8, 30); // level_idc
603        sps_body.u(32, 0); // toolset_idc_h
604        sps_body.u(32, 0); // toolset_idc_l
605        sps_body.ue(1); // chroma_format_idc = 1 (4:2:0)
606        sps_body.ue(64); // pic_width
607        sps_body.ue(64); // pic_height
608        sps_body.ue(0); // bit_depth_luma_minus8
609        sps_body.ue(0); // bit_depth_chroma_minus8
610        for _ in 0..13 {
611            sps_body.u(1, 0);
612        }
613        sps_body.ue(1); // log2_sub_gop_length
614        sps_body.ue(1); // max_num_tid0_ref_pics
615        sps_body.u(1, 0); // picture_cropping_flag
616        sps_body.u(1, 0); // chroma_qp_table_present_flag
617        sps_body.u(1, 0); // vui_parameters_present_flag
618        sps_body.finish_with_trailing_bits();
619        let sps_rbsp = sps_body.into_bytes();
620        let sps = sps::parse(&sps_rbsp).expect("SPS parses");
621
622        // Baseline PPS: cu_qp_delta_enabled = 0 so transform_unit doesn't
623        // emit cu_qp_delta_abs.
624        let mut pps_body = BitEmitter::new();
625        pps_body.ue(0); // pps_id
626        pps_body.ue(0); // sps_id
627        pps_body.ue(0); // num_ref_idx_default_active_minus1[0]
628        pps_body.ue(0); // num_ref_idx_default_active_minus1[1]
629        pps_body.ue(0); // additional_lt_poc_lsb_len
630        pps_body.u(1, 0); // rpl1_idx_present_flag
631        pps_body.u(1, 1); // single_tile_in_pic_flag
632        pps_body.ue(0); // tile_id_len_minus1
633        pps_body.u(1, 0); // explicit_tile_id_flag
634        pps_body.u(1, 0); // pic_dra_enabled_flag
635        pps_body.u(1, 0); // arbitrary_slice_present_flag
636        pps_body.u(1, 0); // constrained_intra_pred_flag
637        pps_body.u(1, 0); // cu_qp_delta_enabled_flag = 0
638        pps_body.finish_with_trailing_bits();
639        let pps_rbsp = pps_body.into_bytes();
640        let pps = pps::parse(&pps_rbsp).expect("PPS parses");
641
642        // Slice header (Baseline IDR with single_tile_in_pic_flag = 1).
643        let mut hdr = BitEmitter::new();
644        hdr.ue(0); // slice_pps_id
645        hdr.ue(2); // slice_type = I
646        hdr.u(1, 0); // no_output_of_prior_pics_flag
647        hdr.u(1, 0); // slice_deblocking_filter_flag
648        hdr.u(6, 22); // slice_qp = 22
649        hdr.ue(0); // slice_cb_qp_offset (se: 0)
650        hdr.ue(0); // slice_cr_qp_offset
651        while hdr.bit_position() % 8 != 0 {
652            hdr.u(1, 0);
653        }
654        let mut slice_rbsp = hdr.into_bytes();
655
656        // CABAC-encoded slice_data:
657        //   1× split_cu_flag = 1 at the CTB
658        //   4× split_cu_flag = 0 at each 32×32 child
659        //   4× (intra_pred_mode + cbf_luma + cbf_cb + cbf_cr)
660        //   then terminate(true).
661        let mut enc = CabacEncoder::new();
662        enc.encode_decision(0, 0, 1); // CTB split = 1
663        for _ in 0..4 {
664            enc.encode_decision(0, 0, 0); // child split = 0
665            enc.encode_decision(0, 0, 0); // intra_pred_mode
666            enc.encode_decision(0, 0, 0); // cbf_luma
667            enc.encode_decision(0, 0, 0); // cbf_cb
668            enc.encode_decision(0, 0, 0); // cbf_cr
669        }
670        enc.encode_terminate(true);
671        let slice_data_bytes = enc.finish();
672        slice_rbsp.extend_from_slice(&slice_data_bytes);
673
674        let stats = walk_idr_slice(&sps, &pps, &slice_rbsp).expect("walk succeeds");
675        assert_eq!(stats.ctus, 1);
676        assert_eq!(stats.split_cu_flag_bins, 5, "1 CTB split + 4 child splits");
677        assert_eq!(stats.coding_units, 8, "4 leaves × (luma + chroma)");
678        assert_eq!(stats.intra_pred_mode_bins, 4);
679        assert_eq!(stats.cbf_luma_bins, 4);
680        assert_eq!(stats.cbf_chroma_bins, 8);
681    }
682
683    /// Confirm the integration helper rejects a non-Baseline SPS up front
684    /// rather than handing the walker a bitstream it cannot parse.
685    #[test]
686    fn walk_idr_slice_rejects_main_profile_sps() {
687        // SPS with sps_btt_flag = 1 — outside Baseline.
688        let mut sps_body = BitEmitter::new();
689        sps_body.ue(0);
690        sps_body.u(8, 1); // profile_idc = 1 (Main)
691        sps_body.u(8, 41);
692        sps_body.u(32, 1); // toolset_idc_h non-zero
693        sps_body.u(32, 1);
694        sps_body.ue(1);
695        sps_body.ue(1920);
696        sps_body.ue(1080);
697        sps_body.ue(2);
698        sps_body.ue(2);
699        sps_body.u(1, 1); // sps_btt_flag = 1
700        sps_body.ue(1);
701        sps_body.ue(0);
702        sps_body.ue(2);
703        sps_body.ue(1);
704        sps_body.ue(0);
705        sps_body.u(1, 0); // sps_suco
706        sps_body.u(1, 0); // sps_admvp
707        sps_body.u(1, 0); // sps_eipd
708        sps_body.u(1, 0); // sps_cm_init
709        sps_body.u(1, 0); // sps_iqt
710        sps_body.u(1, 0); // sps_addb
711        sps_body.u(1, 0); // sps_alf
712        sps_body.u(1, 0); // sps_htdf
713        sps_body.u(1, 0); // sps_rpl
714        sps_body.u(1, 0); // sps_pocs
715        sps_body.u(1, 0); // sps_dquant
716        sps_body.u(1, 0); // sps_dra
717        sps_body.ue(1);
718        sps_body.ue(1);
719        sps_body.u(1, 0);
720        sps_body.u(1, 0);
721        sps_body.u(1, 0);
722        sps_body.finish_with_trailing_bits();
723        let sps_rbsp = sps_body.into_bytes();
724        let sps = sps::parse(&sps_rbsp).expect("SPS parses");
725
726        // Minimal PPS.
727        let mut pps_body = BitEmitter::new();
728        pps_body.ue(0);
729        pps_body.ue(0);
730        pps_body.ue(0);
731        pps_body.ue(0);
732        pps_body.ue(0);
733        pps_body.u(1, 0);
734        pps_body.u(1, 1);
735        pps_body.ue(0);
736        pps_body.u(1, 0);
737        pps_body.u(1, 0);
738        pps_body.u(1, 0);
739        pps_body.u(1, 0);
740        pps_body.u(1, 0);
741        pps_body.finish_with_trailing_bits();
742        let pps_rbsp = pps_body.into_bytes();
743        let pps = pps::parse(&pps_rbsp).unwrap();
744
745        let res = walk_idr_slice(&sps, &pps, &[0u8; 16]);
746        assert!(res.is_err());
747        let err_text = format!("{}", res.unwrap_err());
748        assert!(
749            err_text.contains("Baseline-profile toolset"),
750            "got: {err_text}"
751        );
752    }
753
754    /// Build a minimal Baseline SPS for an `(width × height)` 4:2:0 8-bit
755    /// picture. CTB size defaults to 64×64 (`log2_ctu_size_minus5 = 0` →
756    /// `CtbLog2SizeY = 5` since `sps_btt_flag = 0` keeps the spec's
757    /// default of `1` per §7.4.3.1, putting CTB at 32×32 — round-3
758    /// fixtures keep below 64×64 to dodge the unimplemented `nTbS = 64`
759    /// transform path).
760    fn build_baseline_sps_rbsp(width: u32, height: u32) -> Vec<u8> {
761        let mut sps_body = BitEmitter::new();
762        sps_body.ue(0); // sps_id
763        sps_body.u(8, 0); // profile_idc Baseline
764        sps_body.u(8, 30); // level_idc
765        sps_body.u(32, 0); // toolset_idc_h
766        sps_body.u(32, 0); // toolset_idc_l
767        sps_body.ue(1); // chroma_format_idc 4:2:0
768        sps_body.ue(width);
769        sps_body.ue(height);
770        sps_body.ue(0); // bit_depth_luma_minus8
771        sps_body.ue(0); // bit_depth_chroma_minus8
772        for _ in 0..13 {
773            sps_body.u(1, 0);
774        }
775        sps_body.ue(1); // log2_sub_gop_length
776        sps_body.ue(1); // max_num_tid0_ref_pics
777        sps_body.u(1, 0); // picture_cropping_flag
778        sps_body.u(1, 0); // chroma_qp_table_present_flag
779        sps_body.u(1, 0); // vui_parameters_present_flag
780        sps_body.finish_with_trailing_bits();
781        sps_body.into_bytes()
782    }
783
784    /// Round-8 helper: build a Baseline-shaped SPS that turns on
785    /// `sps_rpl_flag = 1` and `sps_pocs_flag = 1` so non-IDR slice
786    /// headers exercise the RPL parsing path. `num_ref_pic_lists_in_sps`
787    /// is set to 0 for both lists so the slice header MUST carry an
788    /// inline `ref_pic_list_struct()`.
789    fn build_rpl_sps_rbsp(width: u32, height: u32) -> Vec<u8> {
790        let mut sps_body = sps::tests::BitEmitter::new();
791        sps_body.ue(0); // sps_id
792        sps_body.u(8, 0); // profile_idc
793        sps_body.u(8, 30); // level_idc
794        sps_body.u(32, 0); // toolset_idc_h
795        sps_body.u(32, 0); // toolset_idc_l
796        sps_body.ue(1); // chroma_format_idc 4:2:0
797        sps_body.ue(width);
798        sps_body.ue(height);
799        sps_body.ue(0); // bit_depth_luma_minus8
800        sps_body.ue(0); // bit_depth_chroma_minus8
801                        // 13 toolset bit-flags up to (and including) sps_cm_init/sps_iqt
802                        // — all default to 0 except sps_rpl_flag and sps_pocs_flag.
803        sps_body.u(1, 0); // sps_btt
804        sps_body.u(1, 0); // sps_suco
805        sps_body.u(1, 0); // sps_admvp
806        sps_body.u(1, 0); // sps_eipd
807        sps_body.u(1, 0); // sps_cm_init
808        sps_body.u(1, 0); // sps_iqt
809        sps_body.u(1, 0); // sps_addb
810        sps_body.u(1, 0); // sps_alf
811        sps_body.u(1, 0); // sps_htdf
812        sps_body.u(1, 1); // sps_rpl  ← enable
813        sps_body.u(1, 1); // sps_pocs ← enable
814        sps_body.u(1, 0); // sps_dquant
815        sps_body.u(1, 0); // sps_dra
816                          // sps_pocs_flag=1 → log2_max_pic_order_cnt_lsb_minus4
817        sps_body.ue(4); // log2_max_pic_order_cnt_lsb_minus4 = 4 → 8 bits
818                        // sps_rpl_flag=1 path:
819                        //   sps_max_dec_pic_buffering_minus1, long_term_ref_pics_flag,
820                        //   rpl1_same_as_rpl0_flag, num_ref_pic_lists_in_sps[0],
821                        //   (num_ref_pic_lists_in_sps[1] when not same).
822        sps_body.ue(1); // sps_max_dec_pic_buffering_minus1
823        sps_body.u(1, 0); // long_term_ref_pics_flag
824        sps_body.u(1, 0); // rpl1_same_as_rpl0_flag (need both lists explicitly)
825        sps_body.ue(0); // num_ref_pic_lists_in_sps[0] = 0 (force inline RPL)
826        sps_body.ue(0); // num_ref_pic_lists_in_sps[1] = 0
827        sps_body.u(1, 0); // picture_cropping_flag
828        sps_body.u(1, 0); // chroma_qp_table_present_flag
829        sps_body.u(1, 0); // vui_parameters_present_flag
830        sps_body.finish_with_trailing_bits();
831        sps_body.into_bytes()
832    }
833
834    /// Build a minimal Baseline PPS for `cu_qp_delta_enabled_flag = 0`.
835    fn build_baseline_pps_rbsp() -> Vec<u8> {
836        let mut pps_body = BitEmitter::new();
837        pps_body.ue(0); // pps_id
838        pps_body.ue(0); // sps_id
839        pps_body.ue(0);
840        pps_body.ue(0);
841        pps_body.ue(0);
842        pps_body.u(1, 0); // rpl1_idx_present_flag
843        pps_body.u(1, 1); // single_tile_in_pic_flag
844        pps_body.ue(0); // tile_id_len_minus1
845        pps_body.u(1, 0); // explicit_tile_id_flag
846        pps_body.u(1, 0); // pic_dra_enabled_flag
847        pps_body.u(1, 0); // arbitrary_slice_present_flag
848        pps_body.u(1, 0); // constrained_intra_pred_flag
849        pps_body.u(1, 0); // cu_qp_delta_enabled_flag = 0
850        pps_body.finish_with_trailing_bits();
851        pps_body.into_bytes()
852    }
853
854    /// **Round-3 end-to-end pixel decode.** Build a Baseline IDR slice
855    /// covering a 32×32 picture (one 32×32 CTU split into four 16×16
856    /// leaves; each leaf carries `intra_pred_mode = 0` (= INTRA_DC) and
857    /// `cbf_luma = cbf_cb = cbf_cr = 0`). Decode through
858    /// [`decode_idr_slice`] and verify the reconstructed Y plane is
859    /// uniformly 128 (the bit-depth substitution value for "no
860    /// neighbours" is 128, DC-prediction of all-128 references is 128,
861    /// and a zero residual leaves it at 128).
862    #[test]
863    fn round3_end_to_end_decode_grey_idr() {
864        use crate::cabac::CabacEncoder;
865        // 64×64 picture (1 CTU at the default CTB log2 = 6) — keeps the
866        // walker on the simple "CTB == picture" path, avoiding implicit
867        // boundary splits.
868        let sps = sps::parse(&build_baseline_sps_rbsp(64, 64)).unwrap();
869        let pps = pps::parse(&build_baseline_pps_rbsp()).unwrap();
870        // Slice header for an IDR with deblocking off, slice_qp = 22.
871        let mut hdr = BitEmitter::new();
872        hdr.ue(0);
873        hdr.ue(2); // I slice
874        hdr.u(1, 0);
875        hdr.u(1, 0); // slice_deblocking_filter_flag = 0
876        hdr.u(6, 22);
877        hdr.ue(0);
878        hdr.ue(0);
879        while hdr.bit_position() % 8 != 0 {
880            hdr.u(1, 0);
881        }
882        let mut slice_rbsp = hdr.into_bytes();
883        // Encode CABAC: one CTB split (32 → four 16x16 leaves), each
884        // leaf intra_pred_mode = 0 (one "0" bin), cbf_luma = cbf_cb =
885        // cbf_cr = 0 (no residual_coding fires).
886        let mut enc = CabacEncoder::new();
887        enc.encode_decision(0, 0, 1); // CTB split_cu_flag = 1
888        for _ in 0..4 {
889            enc.encode_decision(0, 0, 0); // child split_cu_flag = 0 (leaf)
890                                          // dual-tree luma CU: intra_pred_mode + cbf_luma
891            enc.encode_decision(0, 0, 0); // intra_pred_mode_idx = 0 (one "0" bin)
892            enc.encode_decision(0, 0, 0); // cbf_luma = 0
893                                          // dual-tree chroma CU: cbf_cb + cbf_cr (no intra_pred_mode_idx
894                                          // for chroma in sps_eipd_flag=0 dual-tree path)
895            enc.encode_decision(0, 0, 0); // cbf_cb = 0
896            enc.encode_decision(0, 0, 0); // cbf_cr = 0
897        }
898        enc.encode_terminate(true);
899        slice_rbsp.extend_from_slice(&enc.finish());
900
901        let (pic, stats) = decode_idr_slice(&sps, &pps, &slice_rbsp).unwrap();
902        // Picture geometry checks.
903        assert_eq!(pic.width, 64);
904        assert_eq!(pic.height, 64);
905        assert_eq!(pic.y.len(), 64 * 64);
906        assert_eq!(pic.cb.len(), 32 * 32);
907        assert_eq!(pic.cr.len(), 32 * 32);
908        // Stats: 1 CTU, 5 split_cu_flag bins (1 CTB + 4 children), 8
909        // coding_units (luma+chroma per leaf), 4 intra_pred_mode bins,
910        // 4 cbf_luma bins, 8 cbf_chroma bins.
911        assert_eq!(stats.ctus, 1);
912        assert_eq!(stats.split_cu_flag_bins, 5);
913        assert_eq!(stats.coding_units, 8);
914        assert_eq!(stats.intra_pred_mode_bins, 4);
915        assert_eq!(stats.cbf_luma_bins, 4);
916        assert_eq!(stats.cbf_chroma_bins, 8);
917        // Pixel content: every Y/Cb/Cr sample should be 128 (the IDR-with-
918        // no-neighbours INTRA_DC prediction value at bit-depth 8). Since
919        // the picture buffer was pre-filled with 128 and the prediction
920        // computes 128 from all-128 references with zero residual, the
921        // result is uniform 128.
922        let mismatched_y = pic.y.iter().filter(|&&v| v != 128).count();
923        assert_eq!(
924            mismatched_y, 0,
925            "Y plane should be uniform 128 (got {mismatched_y} non-128 samples)"
926        );
927        let mismatched_cb = pic.cb.iter().filter(|&&v| v != 128).count();
928        let mismatched_cr = pic.cr.iter().filter(|&&v| v != 128).count();
929        assert_eq!(mismatched_cb, 0);
930        assert_eq!(mismatched_cr, 0);
931        // PSNR check vs hand-computed reference (uniform 128): PSNR is
932        // infinite at MSE = 0 — we only assert MSE = 0.
933        let mse: f64 = pic
934            .y
935            .iter()
936            .map(|&v| (v as f64 - 128.0).powi(2))
937            .sum::<f64>()
938            / pic.y.len() as f64;
939        assert_eq!(mse, 0.0);
940    }
941
942    /// End-to-end pixel decode through `make_decoder` (the registered
943    /// codec factory). Wraps the Baseline IDR slice from above into a
944    /// length-prefixed NAL stream containing SPS + PPS + IDR, sends it
945    /// to the registered decoder, and pulls a `Frame::Video` out of
946    /// `receive_frame`. Verifies the pixel plane shape and content.
947    #[test]
948    fn round3_make_decoder_decodes_idr_to_grey_frame() {
949        use crate::cabac::CabacEncoder;
950        use oxideav_core::{CodecParameters, Packet, TimeBase};
951
952        let sps_rbsp = build_baseline_sps_rbsp(64, 64);
953        let pps_rbsp = build_baseline_pps_rbsp();
954
955        // Slice header (IDR, slice_qp = 22, deblocking off).
956        let mut hdr = BitEmitter::new();
957        hdr.ue(0);
958        hdr.ue(2);
959        hdr.u(1, 0);
960        hdr.u(1, 0);
961        hdr.u(6, 22);
962        hdr.ue(0);
963        hdr.ue(0);
964        while hdr.bit_position() % 8 != 0 {
965            hdr.u(1, 0);
966        }
967        let mut idr_rbsp = hdr.into_bytes();
968        let mut enc = CabacEncoder::new();
969        enc.encode_decision(0, 0, 1); // CTB split = 1
970        for _ in 0..4 {
971            enc.encode_decision(0, 0, 0); // child split = 0
972            enc.encode_decision(0, 0, 0); // intra_pred_mode = 0
973            enc.encode_decision(0, 0, 0); // cbf_luma = 0
974            enc.encode_decision(0, 0, 0); // cbf_cb = 0
975            enc.encode_decision(0, 0, 0); // cbf_cr = 0
976        }
977        enc.encode_terminate(true);
978        idr_rbsp.extend_from_slice(&enc.finish());
979
980        // Build NAL headers + length-prefixed envelope.
981        fn nal_envelope(nut: u8, rbsp: &[u8]) -> Vec<u8> {
982            let nut_plus1: u16 = (nut as u16) + 1;
983            let mut hdr_word: u16 = 0;
984            hdr_word |= (nut_plus1 & 0x3F) << 9;
985            let hdr = [(hdr_word >> 8) as u8, (hdr_word & 0xFF) as u8];
986            let nal_len = (hdr.len() + rbsp.len()) as u32;
987            let mut out = Vec::new();
988            out.extend_from_slice(&nal_len.to_be_bytes());
989            out.extend_from_slice(&hdr);
990            out.extend_from_slice(rbsp);
991            out
992        }
993        let mut bs = Vec::new();
994        bs.extend_from_slice(&nal_envelope(24, &sps_rbsp)); // SPS NUT = 24
995        bs.extend_from_slice(&nal_envelope(25, &pps_rbsp)); // PPS NUT = 25
996        bs.extend_from_slice(&nal_envelope(1, &idr_rbsp)); // IDR NUT = 1
997
998        let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
999        let mut dec = decoder::make_decoder(&params).unwrap();
1000        let pkt = Packet::new(0, TimeBase::new(1, 90_000), bs).with_pts(0);
1001        dec.send_packet(&pkt).unwrap();
1002        let frame = dec.receive_frame().unwrap();
1003        let video = match frame {
1004            oxideav_core::Frame::Video(v) => v,
1005            _ => panic!("expected video frame"),
1006        };
1007        assert_eq!(video.planes.len(), 3);
1008        assert_eq!(video.planes[0].stride, 64);
1009        assert_eq!(video.planes[0].data.len(), 64 * 64);
1010        assert!(video.planes[0].data.iter().all(|&v| v == 128));
1011        assert_eq!(video.planes[1].stride, 32);
1012        assert_eq!(video.planes[1].data.len(), 32 * 32);
1013        assert!(video.planes[1].data.iter().all(|&v| v == 128));
1014        assert_eq!(video.planes[2].data.len(), 32 * 32);
1015    }
1016
1017    /// **Round-4 end-to-end fixture through `make_decoder`.** Push an
1018    /// SPS + PPS + IDR + P-slice sequence into the registered EVC
1019    /// decoder. The IDR is a uniform grey 32×32 picture; the P-slice is
1020    /// a single 32×32 leaf with `cu_skip_flag = 1` and `mvp_idx_l0 = 3`
1021    /// (zero MV). Both frames must come out of `receive_frame`, with the
1022    /// P frame being a verbatim copy of the IDR (since zero MV + zero
1023    /// residual ≡ identity).
1024    #[test]
1025    fn round4_make_decoder_decodes_idr_plus_p_to_two_frames() {
1026        use crate::cabac::CabacEncoder;
1027        use oxideav_core::{CodecParameters, Packet, TimeBase};
1028        let sps_rbsp = build_baseline_sps_rbsp(32, 32);
1029        let pps_rbsp = build_baseline_pps_rbsp();
1030
1031        // IDR: single 32×32 leaf at log2 = 5 (no split).
1032        let mut idr_hdr = BitEmitter::new();
1033        idr_hdr.ue(0);
1034        idr_hdr.ue(2); // I slice
1035        idr_hdr.u(1, 0); // no_output
1036        idr_hdr.u(1, 0); // slice_deblocking_filter_flag
1037        idr_hdr.u(6, 22); // slice_qp
1038        idr_hdr.ue(0);
1039        idr_hdr.ue(0);
1040        while idr_hdr.bit_position() % 8 != 0 {
1041            idr_hdr.u(1, 0);
1042        }
1043        let mut idr_rbsp = idr_hdr.into_bytes();
1044        let mut idr_enc = CabacEncoder::new();
1045        // 32x32 single leaf at log2 = 5 (no split needed since
1046        // log2 == ctb_log2_size_y → no split_cu_flag emitted in walker
1047        // path? Actually log2 > min so split is emitted).
1048        // With ctb_log2 = 5 and min_cb_log2 = 4, the CTU is 32×32, and
1049        // we want a single 32x32 leaf → split_cu_flag = 0.
1050        idr_enc.encode_decision(0, 0, 0); // split_cu_flag = 0 at CTB
1051                                          // dual-tree luma CU (32x32):
1052        idr_enc.encode_decision(0, 0, 0); // intra_pred_mode = 0
1053        idr_enc.encode_decision(0, 0, 0); // cbf_luma
1054                                          // dual-tree chroma CU:
1055        idr_enc.encode_decision(0, 0, 0); // cbf_cb
1056        idr_enc.encode_decision(0, 0, 0); // cbf_cr
1057        idr_enc.encode_terminate(true);
1058        idr_rbsp.extend_from_slice(&idr_enc.finish());
1059
1060        // P slice header: slice_type = 1 (P), no override, deblock off.
1061        let mut p_hdr = BitEmitter::new();
1062        p_hdr.ue(0); // slice_pps_id
1063        p_hdr.ue(1); // slice_type = P
1064                     // Not IDR + sps_pocs_flag = 0 → no POC LSB.
1065                     // Not IDR + slice_type=P → ref-idx + admvp branch.
1066        p_hdr.u(1, 0); // num_ref_idx_active_override_flag = 0
1067                       // sps_admvp_flag = 0 → skip temporal_mvp.
1068        p_hdr.u(1, 0); // slice_deblocking_filter_flag
1069        p_hdr.u(6, 22); // slice_qp
1070        p_hdr.ue(0);
1071        p_hdr.ue(0);
1072        while p_hdr.bit_position() % 8 != 0 {
1073            p_hdr.u(1, 0);
1074        }
1075        let mut p_rbsp = p_hdr.into_bytes();
1076        let mut p_enc = CabacEncoder::new();
1077        p_enc.encode_decision(0, 0, 0); // split_cu_flag = 0 at CTB
1078                                        // Single-tree inter CU:
1079        p_enc.encode_decision(0, 0, 1); // cu_skip_flag = 1
1080        for _ in 0..3 {
1081            p_enc.encode_decision(0, 0, 1); // mvp_idx_l0 = 3 prefix (3 ones)
1082        }
1083        p_enc.encode_decision(0, 0, 0); // cbf_luma
1084        p_enc.encode_decision(0, 0, 0); // cbf_cb
1085        p_enc.encode_decision(0, 0, 0); // cbf_cr
1086        p_enc.encode_terminate(true);
1087        p_rbsp.extend_from_slice(&p_enc.finish());
1088
1089        fn nal_envelope(nut: u8, rbsp: &[u8]) -> Vec<u8> {
1090            let nut_plus1: u16 = (nut as u16) + 1;
1091            let mut hdr_word: u16 = 0;
1092            hdr_word |= (nut_plus1 & 0x3F) << 9;
1093            let hdr = [(hdr_word >> 8) as u8, (hdr_word & 0xFF) as u8];
1094            let nal_len = (hdr.len() + rbsp.len()) as u32;
1095            let mut out = Vec::new();
1096            out.extend_from_slice(&nal_len.to_be_bytes());
1097            out.extend_from_slice(&hdr);
1098            out.extend_from_slice(rbsp);
1099            out
1100        }
1101        let mut bs = Vec::new();
1102        bs.extend_from_slice(&nal_envelope(24, &sps_rbsp)); // SPS
1103        bs.extend_from_slice(&nal_envelope(25, &pps_rbsp)); // PPS
1104        bs.extend_from_slice(&nal_envelope(1, &idr_rbsp)); // IDR
1105        bs.extend_from_slice(&nal_envelope(0, &p_rbsp)); // NonIDR
1106
1107        let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
1108        let mut dec = decoder::make_decoder(&params).unwrap();
1109        let pkt = Packet::new(0, TimeBase::new(1, 90_000), bs).with_pts(0);
1110        dec.send_packet(&pkt).unwrap();
1111        // Pull the two frames.
1112        let f0 = dec.receive_frame().unwrap();
1113        let f1 = dec.receive_frame().unwrap();
1114        let v0 = match f0 {
1115            oxideav_core::Frame::Video(v) => v,
1116            _ => panic!("not video"),
1117        };
1118        let v1 = match f1 {
1119            oxideav_core::Frame::Video(v) => v,
1120            _ => panic!("not video"),
1121        };
1122        // Both should be uniform 128 (the IDR is grey, the P slice is a
1123        // zero-MV, zero-residual copy of the IDR → still grey).
1124        assert!(v0.planes[0].data.iter().all(|&v| v == 128));
1125        assert!(v1.planes[0].data.iter().all(|&v| v == 128));
1126        assert_eq!(
1127            v0.planes[0].data, v1.planes[0].data,
1128            "P frame must equal IDR"
1129        );
1130        assert_eq!(v0.planes[1].data, v1.planes[1].data);
1131        assert_eq!(v0.planes[2].data, v1.planes[2].data);
1132        // PSNR Y = ∞ (MSE=0). The acceptance bar is ≥ 30 dB.
1133        let mse: f64 = v0.planes[0]
1134            .data
1135            .iter()
1136            .zip(v1.planes[0].data.iter())
1137            .map(|(&a, &b)| (a as f64 - b as f64).powi(2))
1138            .sum::<f64>()
1139            / v0.planes[0].data.len() as f64;
1140        assert_eq!(mse, 0.0, "PSNR must be infinite for identical frames");
1141    }
1142
1143    /// **Round-8 RPL non-IDR fixture.** Same shape as the round-4 IDR+P
1144    /// fixture but with an SPS that turns on `sps_rpl_flag = 1` and
1145    /// `sps_pocs_flag = 1`, so the P slice header carries:
1146    ///
1147    ///   * `slice_pic_order_cnt_lsb` (8 bits per `log2_max_poc_lsb=8`),
1148    ///   * a per-list inline `ref_pic_list_struct()` (one STRP entry each
1149    ///     because `num_ref_pic_lists_in_sps[i] = 0`).
1150    ///
1151    /// The decoder must walk these fields cleanly via the canonical
1152    /// slice_header parser, then drive the inter pipeline as before.
1153    /// Both the IDR and the P frame come back as uniform-128 — PSNR Y =
1154    /// ∞ (MSE = 0), well above the 30 dB acceptance bar.
1155    #[test]
1156    fn round8_rpl_non_idr_decodes_to_two_frames() {
1157        use crate::cabac::CabacEncoder;
1158        use oxideav_core::{CodecParameters, Packet, TimeBase};
1159        let sps_rbsp = build_rpl_sps_rbsp(32, 32);
1160        let pps_rbsp = build_baseline_pps_rbsp();
1161
1162        // IDR slice (slice_type=I, no RPL fields per §7.3.4 IDR branch).
1163        let mut idr_hdr = BitEmitter::new();
1164        idr_hdr.ue(0);
1165        idr_hdr.ue(2); // I slice
1166        idr_hdr.u(1, 0); // no_output
1167        idr_hdr.u(1, 0); // slice_deblocking_filter_flag
1168        idr_hdr.u(6, 22); // slice_qp
1169        idr_hdr.ue(0);
1170        idr_hdr.ue(0);
1171        while idr_hdr.bit_position() % 8 != 0 {
1172            idr_hdr.u(1, 0);
1173        }
1174        let mut idr_rbsp = idr_hdr.into_bytes();
1175        let mut idr_enc = CabacEncoder::new();
1176        idr_enc.encode_decision(0, 0, 0); // split_cu_flag = 0
1177        idr_enc.encode_decision(0, 0, 0); // intra_pred_mode = 0
1178        idr_enc.encode_decision(0, 0, 0); // cbf_luma
1179        idr_enc.encode_decision(0, 0, 0); // cbf_cb
1180        idr_enc.encode_decision(0, 0, 0); // cbf_cr
1181        idr_enc.encode_terminate(true);
1182        idr_rbsp.extend_from_slice(&idr_enc.finish());
1183
1184        // P slice header with inline RPL on both lists.
1185        let mut p_hdr = BitEmitter::new();
1186        p_hdr.ue(0); // slice_pps_id
1187        p_hdr.ue(1); // slice_type = P
1188                     // sps_pocs_flag = 1 + non-IDR → POC LSB (8 bits).
1189        p_hdr.u(8, 1);
1190        // sps_rpl_flag = 1 path — for both i = 0 and i = 1:
1191        //   ref_pic_list_sps_flag[i] omitted because
1192        //     num_ref_pic_lists_in_sps[i] == 0 → not signalled.
1193        //   The inline `ref_pic_list_struct()` follows directly.
1194        // RPL L0 inline: 1 STRP, delta=1, sign=0 (negative). Round-9
1195        // resolves the ref POC as `slice_poc + signed_delta`, so
1196        // sign=0 → ref POC = 1 + (-1) = 0 → matches the IDR.
1197        p_hdr.ue(1); // num_strp_entries
1198        p_hdr.ue(1); // delta_poc_st = 1
1199        p_hdr.u(1, 0); // sign negative → -1
1200                       // RPL L1 inline: same shape (delta=1, sign=0).
1201        p_hdr.ue(1);
1202        p_hdr.ue(1);
1203        p_hdr.u(1, 0);
1204        // P slice → ref_idx + admvp branch:
1205        p_hdr.u(1, 0); // num_ref_idx_active_override_flag
1206                       // sps_admvp_flag = 0 → no temporal_mvp.
1207        p_hdr.u(1, 0); // slice_deblocking_filter_flag
1208        p_hdr.u(6, 22); // slice_qp
1209        p_hdr.ue(0);
1210        p_hdr.ue(0);
1211        while p_hdr.bit_position() % 8 != 0 {
1212            p_hdr.u(1, 0);
1213        }
1214        let mut p_rbsp = p_hdr.into_bytes();
1215        let mut p_enc = CabacEncoder::new();
1216        p_enc.encode_decision(0, 0, 0); // split_cu_flag = 0
1217                                        // Single-tree inter CU:
1218        p_enc.encode_decision(0, 0, 1); // cu_skip_flag = 1
1219        for _ in 0..3 {
1220            p_enc.encode_decision(0, 0, 1); // mvp_idx_l0 = 3 prefix
1221        }
1222        p_enc.encode_decision(0, 0, 0); // cbf_luma
1223        p_enc.encode_decision(0, 0, 0); // cbf_cb
1224        p_enc.encode_decision(0, 0, 0); // cbf_cr
1225        p_enc.encode_terminate(true);
1226        p_rbsp.extend_from_slice(&p_enc.finish());
1227
1228        fn nal_envelope(nut: u8, rbsp: &[u8]) -> Vec<u8> {
1229            let nut_plus1: u16 = (nut as u16) + 1;
1230            let mut hdr_word: u16 = 0;
1231            hdr_word |= (nut_plus1 & 0x3F) << 9;
1232            let hdr = [(hdr_word >> 8) as u8, (hdr_word & 0xFF) as u8];
1233            let nal_len = (hdr.len() + rbsp.len()) as u32;
1234            let mut out = Vec::new();
1235            out.extend_from_slice(&nal_len.to_be_bytes());
1236            out.extend_from_slice(&hdr);
1237            out.extend_from_slice(rbsp);
1238            out
1239        }
1240        let mut bs = Vec::new();
1241        bs.extend_from_slice(&nal_envelope(24, &sps_rbsp));
1242        bs.extend_from_slice(&nal_envelope(25, &pps_rbsp));
1243        bs.extend_from_slice(&nal_envelope(1, &idr_rbsp));
1244        bs.extend_from_slice(&nal_envelope(0, &p_rbsp));
1245
1246        let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
1247        let mut dec = decoder::make_decoder(&params).unwrap();
1248        let pkt = Packet::new(0, TimeBase::new(1, 90_000), bs).with_pts(0);
1249        dec.send_packet(&pkt).unwrap();
1250        let f0 = dec.receive_frame().unwrap();
1251        let f1 = dec.receive_frame().unwrap();
1252        let v0 = match f0 {
1253            oxideav_core::Frame::Video(v) => v,
1254            _ => panic!("not video"),
1255        };
1256        let v1 = match f1 {
1257            oxideav_core::Frame::Video(v) => v,
1258            _ => panic!("not video"),
1259        };
1260        // Both are uniform 128 (zero-MV inter copy of the all-128 IDR).
1261        assert!(v0.planes[0].data.iter().all(|&v| v == 128));
1262        assert!(v1.planes[0].data.iter().all(|&v| v == 128));
1263        assert_eq!(v0.planes[0].data, v1.planes[0].data);
1264        let mse: f64 = v0.planes[0]
1265            .data
1266            .iter()
1267            .zip(v1.planes[0].data.iter())
1268            .map(|(&a, &b)| (a as f64 - b as f64).powi(2))
1269            .sum::<f64>()
1270            / v0.planes[0].data.len() as f64;
1271        assert_eq!(mse, 0.0, "RPL P-slice must be bit-identical to the IDR");
1272    }
1273
1274    /// **Round-9 multi-frame DPB + POC fixture.** Decode an IDR + two P
1275    /// slices (POC 0, 1, 2) where each P slice references the
1276    /// previously-decoded frame via an inline RPL. The DPB must keep
1277    /// the IDR + the first P alive long enough for the second P's RPL
1278    /// (`delta_poc_st = 1, sign = 0` → `cur - 1`) to resolve. All three
1279    /// frames come back as uniform-128 (the IDR is grey + the P slices
1280    /// are zero-MV identity copies of the previous frame).
1281    #[test]
1282    fn round9_three_frame_idr_p_p_with_dpb() {
1283        use crate::cabac::CabacEncoder;
1284        use oxideav_core::{CodecParameters, Packet, TimeBase};
1285        let sps_rbsp = build_rpl_sps_rbsp(32, 32);
1286        let pps_rbsp = build_baseline_pps_rbsp();
1287
1288        // IDR slice (POC 0).
1289        let mut idr_hdr = BitEmitter::new();
1290        idr_hdr.ue(0);
1291        idr_hdr.ue(2); // I slice
1292        idr_hdr.u(1, 0); // no_output
1293        idr_hdr.u(1, 0); // slice_deblocking_filter_flag
1294        idr_hdr.u(6, 22); // slice_qp
1295        idr_hdr.ue(0);
1296        idr_hdr.ue(0);
1297        while idr_hdr.bit_position() % 8 != 0 {
1298            idr_hdr.u(1, 0);
1299        }
1300        let mut idr_rbsp = idr_hdr.into_bytes();
1301        let mut idr_enc = CabacEncoder::new();
1302        idr_enc.encode_decision(0, 0, 0); // split_cu_flag = 0
1303        idr_enc.encode_decision(0, 0, 0); // intra_pred_mode = 0
1304        idr_enc.encode_decision(0, 0, 0); // cbf_luma
1305        idr_enc.encode_decision(0, 0, 0); // cbf_cb
1306        idr_enc.encode_decision(0, 0, 0); // cbf_cr
1307        idr_enc.encode_terminate(true);
1308        idr_rbsp.extend_from_slice(&idr_enc.finish());
1309
1310        // Helper to build a P slice referencing POC = cur_poc - 1.
1311        fn build_p_slice(cur_poc_lsb: u32) -> Vec<u8> {
1312            use crate::cabac::CabacEncoder;
1313            let mut p_hdr = BitEmitter::new();
1314            p_hdr.ue(0); // slice_pps_id
1315            p_hdr.ue(1); // slice_type = P
1316            p_hdr.u(8, cur_poc_lsb); // POC LSB
1317                                     // RPL L0 inline: 1 STRP, delta=1, sign=0 → ref = cur - 1.
1318            p_hdr.ue(1);
1319            p_hdr.ue(1);
1320            p_hdr.u(1, 0);
1321            // RPL L1 inline: same shape.
1322            p_hdr.ue(1);
1323            p_hdr.ue(1);
1324            p_hdr.u(1, 0);
1325            p_hdr.u(1, 0); // num_ref_idx_active_override_flag
1326            p_hdr.u(1, 0); // slice_deblocking_filter_flag
1327            p_hdr.u(6, 22); // slice_qp
1328            p_hdr.ue(0);
1329            p_hdr.ue(0);
1330            while p_hdr.bit_position() % 8 != 0 {
1331                p_hdr.u(1, 0);
1332            }
1333            let mut p_rbsp = p_hdr.into_bytes();
1334            let mut p_enc = CabacEncoder::new();
1335            p_enc.encode_decision(0, 0, 0); // split_cu_flag = 0
1336            p_enc.encode_decision(0, 0, 1); // cu_skip_flag = 1
1337            for _ in 0..3 {
1338                p_enc.encode_decision(0, 0, 1); // mvp_idx_l0 = 3
1339            }
1340            p_enc.encode_decision(0, 0, 0); // cbf_luma
1341            p_enc.encode_decision(0, 0, 0); // cbf_cb
1342            p_enc.encode_decision(0, 0, 0); // cbf_cr
1343            p_enc.encode_terminate(true);
1344            p_rbsp.extend_from_slice(&p_enc.finish());
1345            p_rbsp
1346        }
1347        let p1_rbsp = build_p_slice(1);
1348        let p2_rbsp = build_p_slice(2);
1349
1350        fn nal_envelope(nut: u8, rbsp: &[u8]) -> Vec<u8> {
1351            let nut_plus1: u16 = (nut as u16) + 1;
1352            let mut hdr_word: u16 = 0;
1353            hdr_word |= (nut_plus1 & 0x3F) << 9;
1354            let hdr = [(hdr_word >> 8) as u8, (hdr_word & 0xFF) as u8];
1355            let nal_len = (hdr.len() + rbsp.len()) as u32;
1356            let mut out = Vec::new();
1357            out.extend_from_slice(&nal_len.to_be_bytes());
1358            out.extend_from_slice(&hdr);
1359            out.extend_from_slice(rbsp);
1360            out
1361        }
1362        let mut bs = Vec::new();
1363        bs.extend_from_slice(&nal_envelope(24, &sps_rbsp));
1364        bs.extend_from_slice(&nal_envelope(25, &pps_rbsp));
1365        bs.extend_from_slice(&nal_envelope(1, &idr_rbsp));
1366        bs.extend_from_slice(&nal_envelope(0, &p1_rbsp));
1367        bs.extend_from_slice(&nal_envelope(0, &p2_rbsp));
1368
1369        let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
1370        let mut dec = decoder::make_decoder(&params).unwrap();
1371        let pkt = Packet::new(0, TimeBase::new(1, 90_000), bs).with_pts(0);
1372        dec.send_packet(&pkt).unwrap();
1373        let f0 = dec.receive_frame().unwrap();
1374        let f1 = dec.receive_frame().unwrap();
1375        let f2 = dec.receive_frame().unwrap();
1376        let v0 = match f0 {
1377            oxideav_core::Frame::Video(v) => v,
1378            _ => panic!("not video"),
1379        };
1380        let v1 = match f1 {
1381            oxideav_core::Frame::Video(v) => v,
1382            _ => panic!("not video"),
1383        };
1384        let v2 = match f2 {
1385            oxideav_core::Frame::Video(v) => v,
1386            _ => panic!("not video"),
1387        };
1388        assert!(v0.planes[0].data.iter().all(|&v| v == 128));
1389        assert!(v1.planes[0].data.iter().all(|&v| v == 128));
1390        assert!(v2.planes[0].data.iter().all(|&v| v == 128));
1391        assert_eq!(v0.planes[0].data, v1.planes[0].data);
1392        assert_eq!(v1.planes[0].data, v2.planes[0].data);
1393    }
1394
1395    /// **Round-10 flush() drain end-to-end.** Decodes a 2-frame
1396    /// IDR + P bitstream and verifies that calling `flush()` after
1397    /// receiving every frame is idempotent — no duplicate frames
1398    /// surface, and `receive_frame` returns `NeedMore` once the queue
1399    /// is drained.
1400    #[test]
1401    fn round10_flush_after_receive_is_idempotent() {
1402        use crate::cabac::CabacEncoder;
1403        use oxideav_core::{CodecParameters, Packet, TimeBase};
1404        let sps_rbsp = build_rpl_sps_rbsp(32, 32);
1405        let pps_rbsp = build_baseline_pps_rbsp();
1406        // IDR slice (POC 0) — uniform 128.
1407        let mut idr_hdr = BitEmitter::new();
1408        idr_hdr.ue(0);
1409        idr_hdr.ue(2);
1410        idr_hdr.u(1, 0);
1411        idr_hdr.u(1, 0);
1412        idr_hdr.u(6, 22);
1413        idr_hdr.ue(0);
1414        idr_hdr.ue(0);
1415        while idr_hdr.bit_position() % 8 != 0 {
1416            idr_hdr.u(1, 0);
1417        }
1418        let mut idr_rbsp = idr_hdr.into_bytes();
1419        let mut idr_enc = CabacEncoder::new();
1420        idr_enc.encode_decision(0, 0, 0);
1421        idr_enc.encode_decision(0, 0, 0);
1422        idr_enc.encode_decision(0, 0, 0);
1423        idr_enc.encode_decision(0, 0, 0);
1424        idr_enc.encode_decision(0, 0, 0);
1425        idr_enc.encode_terminate(true);
1426        idr_rbsp.extend_from_slice(&idr_enc.finish());
1427        // P slice (POC 1).
1428        let mut p_hdr = BitEmitter::new();
1429        p_hdr.ue(0);
1430        p_hdr.ue(1);
1431        p_hdr.u(8, 1);
1432        p_hdr.ue(1);
1433        p_hdr.ue(1);
1434        p_hdr.u(1, 0);
1435        p_hdr.ue(1);
1436        p_hdr.ue(1);
1437        p_hdr.u(1, 0);
1438        p_hdr.u(1, 0);
1439        p_hdr.u(1, 0);
1440        p_hdr.u(6, 22);
1441        p_hdr.ue(0);
1442        p_hdr.ue(0);
1443        while p_hdr.bit_position() % 8 != 0 {
1444            p_hdr.u(1, 0);
1445        }
1446        let mut p_rbsp = p_hdr.into_bytes();
1447        let mut p_enc = CabacEncoder::new();
1448        p_enc.encode_decision(0, 0, 0);
1449        p_enc.encode_decision(0, 0, 1);
1450        for _ in 0..3 {
1451            p_enc.encode_decision(0, 0, 1);
1452        }
1453        p_enc.encode_decision(0, 0, 0);
1454        p_enc.encode_decision(0, 0, 0);
1455        p_enc.encode_decision(0, 0, 0);
1456        p_enc.encode_terminate(true);
1457        p_rbsp.extend_from_slice(&p_enc.finish());
1458
1459        fn nal_envelope(nut: u8, rbsp: &[u8]) -> Vec<u8> {
1460            let nut_plus1: u16 = (nut as u16) + 1;
1461            let mut hdr_word: u16 = 0;
1462            hdr_word |= (nut_plus1 & 0x3F) << 9;
1463            let hdr = [(hdr_word >> 8) as u8, (hdr_word & 0xFF) as u8];
1464            let nal_len = (hdr.len() + rbsp.len()) as u32;
1465            let mut out = Vec::new();
1466            out.extend_from_slice(&nal_len.to_be_bytes());
1467            out.extend_from_slice(&hdr);
1468            out.extend_from_slice(rbsp);
1469            out
1470        }
1471        let mut bs = Vec::new();
1472        bs.extend_from_slice(&nal_envelope(24, &sps_rbsp));
1473        bs.extend_from_slice(&nal_envelope(25, &pps_rbsp));
1474        bs.extend_from_slice(&nal_envelope(1, &idr_rbsp));
1475        bs.extend_from_slice(&nal_envelope(0, &p_rbsp));
1476
1477        let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
1478        let mut dec = decoder::make_decoder(&params).unwrap();
1479        let pkt = Packet::new(0, TimeBase::new(1, 90_000), bs).with_pts(0);
1480        dec.send_packet(&pkt).unwrap();
1481        // Drain both frames.
1482        let _f0 = dec.receive_frame().unwrap();
1483        let _f1 = dec.receive_frame().unwrap();
1484        // Now flush() should be a no-op (every DPB entry has output_emitted = true).
1485        dec.flush().unwrap();
1486        let next = dec.receive_frame();
1487        assert!(matches!(next, Err(oxideav_core::Error::NeedMore)));
1488    }
1489}