blob: 5b279121fae120a8701862a2d588d3664eb13420 [file] [log] [blame]
// 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.
#[cfg(test)]
mod dummy;
#[cfg(feature = "vaapi")]
mod vaapi;
use std::cell::RefCell;
use std::io::Cursor;
use std::os::fd::AsFd;
use std::os::fd::BorrowedFd;
use std::rc::Rc;
use anyhow::anyhow;
use anyhow::Context;
use crate::codec::h265::dpb::Dpb;
use crate::codec::h265::dpb::DpbEntry;
use crate::codec::h265::parser::Nalu;
use crate::codec::h265::parser::NaluType;
use crate::codec::h265::parser::Parser;
use crate::codec::h265::parser::Pps;
use crate::codec::h265::parser::ShortTermRefPicSet;
use crate::codec::h265::parser::Slice;
use crate::codec::h265::parser::SliceHeader;
use crate::codec::h265::parser::Sps;
use crate::codec::h265::picture::PictureData;
use crate::codec::h265::picture::Reference;
use crate::decoder::stateless::DecodeError;
use crate::decoder::stateless::DecodingState;
use crate::decoder::stateless::NewPictureResult;
use crate::decoder::stateless::PoolLayer;
use crate::decoder::stateless::StatelessBackendResult;
use crate::decoder::stateless::StatelessCodec;
use crate::decoder::stateless::StatelessDecoder;
use crate::decoder::stateless::StatelessDecoderBackend;
use crate::decoder::stateless::StatelessDecoderBackendPicture;
use crate::decoder::stateless::StatelessVideoDecoder;
use crate::decoder::stateless::TryFormat;
use crate::decoder::BlockingMode;
use crate::decoder::DecodedHandle;
use crate::decoder::DecoderEvent;
use crate::decoder::StreamInfo;
use crate::Resolution;
const MAX_DPB_SIZE: usize = 16;
// Equation 5-8
pub(crate) fn clip3(x: i32, y: i32, z: i32) -> i32 {
if z < x {
x
} else if z > y {
y
} else {
z
}
}
// See 6.5.3
const fn up_right_diagonal<const N: usize, const ROWS: usize>() -> [usize; N] {
// Generics can't be used in const operations for now, so [0; ROWS * ROWS]
// is rejected by the compiler
assert!(ROWS * ROWS == N);
let mut i = 0;
let mut x = 0i32;
let mut y = 0i32;
let mut ret = [0; N];
loop {
while y >= 0 {
if x < (ROWS as i32) && y < (ROWS as i32) {
ret[i] = (x + ROWS as i32 * y) as usize;
i += 1;
}
y -= 1;
x += 1;
}
y = x;
x = 0;
if i >= N {
break;
}
}
ret
}
const UP_RIGHT_DIAGONAL_4X4: [usize; 16] = up_right_diagonal::<16, 4>();
const UP_RIGHT_DIAGONAL_8X8: [usize; 64] = up_right_diagonal::<64, 8>();
fn get_raster_from_up_right_diagonal_8x8(src: [u8; 64], dst: &mut [u8; 64]) {
for i in 0..64 {
dst[UP_RIGHT_DIAGONAL_8X8[i]] = src[i];
}
}
fn get_raster_from_up_right_diagonal_4x4(src: [u8; 16], dst: &mut [u8; 16]) {
for i in 0..16 {
dst[UP_RIGHT_DIAGONAL_4X4[i]] = src[i];
}
}
/// Stateless backend methods specific to H.265.
pub trait StatelessH265DecoderBackend:
StatelessDecoderBackend + StatelessDecoderBackendPicture<H265>
{
/// Called when a new SPS is parsed.
fn new_sequence(&mut self, sps: &Sps) -> StatelessBackendResult<()>;
/// Called when the decoder determines that a frame or field was found.
fn new_picture(
&mut self,
coded_resolution: Resolution,
timestamp: u64,
) -> NewPictureResult<Self::Picture>;
/// Called by the decoder for every frame or field found.
#[allow(clippy::too_many_arguments)]
fn begin_picture(
&mut self,
picture: &mut Self::Picture,
picture_data: &PictureData,
sps: &Sps,
pps: &Pps,
dpb: &Dpb<Self::Handle>,
rps: &RefPicSet<Self::Handle>,
slice: &Slice,
) -> StatelessBackendResult<()>;
/// Called to dispatch a decode operation to the backend.
#[allow(clippy::too_many_arguments)]
fn decode_slice(
&mut self,
picture: &mut Self::Picture,
slice: &Slice,
sps: &Sps,
pps: &Pps,
ref_pic_list0: &[Option<RefPicListEntry<Self::Handle>>; 16],
ref_pic_list1: &[Option<RefPicListEntry<Self::Handle>>; 16],
) -> StatelessBackendResult<()>;
/// Called when the decoder wants the backend to finish the decoding
/// operations for `picture`. At this point, `decode_slice` has been called
/// for all slices.
fn submit_picture(&mut self, picture: Self::Picture) -> StatelessBackendResult<Self::Handle>;
}
/// An entry in the Reference Picture Lists. Unlike H.264, H.265 can use the
/// current picture itself as a reference.
#[derive(Clone)]
pub enum RefPicListEntry<T> {
CurrentPicture(PictureData),
DpbEntry(DpbEntry<T>),
}
enum BumpingType {
BeforeDecoding,
AfterDecoding,
}
enum RenegotiationType<'a> {
CurrentSps,
NewSps(&'a Sps),
}
/// Keeps track of the last values seen for negotiation purposes.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct NegotiationInfo {
/// The current coded resolution
coded_resolution: Resolution,
general_profile_idc: u8,
bit_depth_luma_minus8: u8,
bit_depth_chroma_minus8: u8,
chroma_format_idc: u8,
}
impl From<&Sps> for NegotiationInfo {
fn from(sps: &Sps) -> Self {
NegotiationInfo {
coded_resolution: Resolution {
width: sps.width().into(),
height: sps.height().into(),
},
general_profile_idc: sps.profile_tier_level.general_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,
}
}
}
/// The RefPicSet data, derived once per picture.
#[derive(Clone, Debug)]
pub struct RefPicSet<T> {
curr_delta_poc_msb_present_flag: [bool; MAX_DPB_SIZE],
foll_delta_poc_msb_present_flag: [bool; MAX_DPB_SIZE],
num_poc_st_curr_before: usize,
num_poc_st_curr_after: usize,
num_poc_st_foll: usize,
num_poc_lt_curr: usize,
num_poc_lt_foll: usize,
poc_st_curr_before: [i32; MAX_DPB_SIZE],
poc_st_curr_after: [i32; MAX_DPB_SIZE],
poc_st_foll: [i32; MAX_DPB_SIZE],
poc_lt_curr: [i32; MAX_DPB_SIZE],
poc_lt_foll: [i32; MAX_DPB_SIZE],
ref_pic_set_lt_curr: [Option<DpbEntry<T>>; MAX_DPB_SIZE],
ref_pic_set_st_curr_after: [Option<DpbEntry<T>>; MAX_DPB_SIZE],
ref_pic_set_st_curr_before: [Option<DpbEntry<T>>; MAX_DPB_SIZE],
ref_pic_set_st_foll: [Option<DpbEntry<T>>; MAX_DPB_SIZE],
ref_pic_set_lt_foll: [Option<DpbEntry<T>>; MAX_DPB_SIZE],
}
impl<T> RefPicSet<T>
where
T: Clone,
{
// See 8.3.4.
// Builds the reference picture list for `hdr` for P and B slices.
fn build_ref_pic_lists(
&self,
hdr: &SliceHeader,
pps: &Pps,
cur_pic: &PictureData,
) -> anyhow::Result<ReferencePicLists<T>> {
let mut ref_pic_lists = ReferencePicLists::default();
// I slices do not use inter prediction.
if !hdr.type_.is_p() && !hdr.type_.is_b() {
return Ok(ref_pic_lists);
}
if self.num_poc_st_curr_before == 0
&& self.num_poc_st_curr_after == 0
&& self.num_poc_lt_curr == 0
&& pps.scc_extension_flag
&& !pps.scc_extension.curr_pic_ref_enabled_flag
{
// Let's try and keep going, if it is a broken stream then maybe it
// will sort itself out as we go. In any case, we must not loop
// infinitely here.
log::error!("Bug or broken stream: out of pictures and can't build ref pic lists.");
return Ok(ref_pic_lists);
}
let rplm = &hdr.ref_pic_list_modification;
let num_rps_curr_temp_list0 = std::cmp::max(
u32::from(hdr.num_ref_idx_l0_active_minus1) + 1,
hdr.num_pic_total_curr,
);
// This could be simplified using a Vec, but lets not change the
// algorithm from the spec too much.
let mut ref_pic_list_temp0: [Option<RefPicListEntry<T>>; MAX_DPB_SIZE] = Default::default();
// Equation 8-8
let mut r_idx = 0;
assert!(num_rps_curr_temp_list0 as usize <= MAX_DPB_SIZE);
while r_idx < num_rps_curr_temp_list0 {
let mut i = 0;
while i < self.num_poc_st_curr_before && r_idx < num_rps_curr_temp_list0 {
ref_pic_list_temp0[r_idx as usize] = self.ref_pic_set_st_curr_before[i]
.clone()
.map(RefPicListEntry::DpbEntry);
i += 1;
r_idx += 1;
}
let mut i = 0;
while i < self.num_poc_st_curr_after && r_idx < num_rps_curr_temp_list0 {
ref_pic_list_temp0[r_idx as usize] = self.ref_pic_set_st_curr_after[i]
.clone()
.map(RefPicListEntry::DpbEntry);
i += 1;
r_idx += 1;
}
let mut i = 0;
while i < self.num_poc_lt_curr && r_idx < num_rps_curr_temp_list0 {
ref_pic_list_temp0[r_idx as usize] = self.ref_pic_set_lt_curr[i]
.clone()
.map(RefPicListEntry::DpbEntry);
i += 1;
r_idx += 1;
}
if pps.scc_extension.curr_pic_ref_enabled_flag {
ref_pic_list_temp0[r_idx as usize] =
Some(RefPicListEntry::CurrentPicture(cur_pic.clone()));
r_idx += 1;
}
}
// Equation 8-9
for r_idx in 0..=usize::from(hdr.num_ref_idx_l0_active_minus1) {
let entry = if rplm.ref_pic_list_modification_flag_l0 {
let idx = rplm.list_entry_l0[r_idx];
ref_pic_list_temp0[idx as usize].clone()
} else {
ref_pic_list_temp0[r_idx].clone()
};
ref_pic_lists.ref_pic_list0[r_idx] = entry;
}
if pps.scc_extension.curr_pic_ref_enabled_flag
&& !rplm.ref_pic_list_modification_flag_l0
&& num_rps_curr_temp_list0 > (u32::from(hdr.num_ref_idx_l0_active_minus1) + 1)
{
ref_pic_lists.ref_pic_list0[r_idx as usize] =
Some(RefPicListEntry::CurrentPicture(cur_pic.clone()));
}
if hdr.type_.is_b() {
let mut ref_pic_list_temp1: [Option<RefPicListEntry<T>>; MAX_DPB_SIZE] =
Default::default();
let num_rps_curr_temp_list1 = std::cmp::max(
u32::from(hdr.num_ref_idx_l1_active_minus1) + 1,
hdr.num_pic_total_curr,
);
// Equation 8-10
let mut r_idx = 0;
assert!(num_rps_curr_temp_list1 as usize <= MAX_DPB_SIZE);
while r_idx < num_rps_curr_temp_list1 {
let mut i = 0;
while i < self.num_poc_st_curr_after && r_idx < num_rps_curr_temp_list1 {
ref_pic_list_temp1[r_idx as usize] = self.ref_pic_set_st_curr_after[i]
.clone()
.map(RefPicListEntry::DpbEntry);
i += 1;
r_idx += 1;
}
let mut i = 0;
while i < self.num_poc_st_curr_before && r_idx < num_rps_curr_temp_list1 {
ref_pic_list_temp1[r_idx as usize] = self.ref_pic_set_st_curr_before[i]
.clone()
.map(RefPicListEntry::DpbEntry);
i += 1;
r_idx += 1;
}
let mut i = 0;
while i < self.num_poc_lt_curr && r_idx < num_rps_curr_temp_list1 {
ref_pic_list_temp1[r_idx as usize] = self.ref_pic_set_lt_curr[i]
.clone()
.map(RefPicListEntry::DpbEntry);
i += 1;
r_idx += 1;
}
if pps.scc_extension.curr_pic_ref_enabled_flag {
ref_pic_list_temp1[r_idx as usize] =
Some(RefPicListEntry::CurrentPicture(cur_pic.clone()));
r_idx += 1;
}
}
// Equation 8-11
for r_idx in 0..=usize::from(hdr.num_ref_idx_l1_active_minus1) {
let entry = if rplm.ref_pic_list_modification_flag_l1 {
let idx = rplm.list_entry_l1[r_idx];
ref_pic_list_temp1[idx as usize].clone()
} else {
ref_pic_list_temp1[r_idx].clone()
};
ref_pic_lists.ref_pic_list1[r_idx] = entry;
}
}
Ok(ref_pic_lists)
}
}
impl<T: Clone> Default for RefPicSet<T> {
fn default() -> Self {
Self {
curr_delta_poc_msb_present_flag: Default::default(),
foll_delta_poc_msb_present_flag: Default::default(),
num_poc_st_curr_before: Default::default(),
num_poc_st_curr_after: Default::default(),
num_poc_st_foll: Default::default(),
num_poc_lt_curr: Default::default(),
num_poc_lt_foll: Default::default(),
poc_st_curr_before: Default::default(),
poc_st_curr_after: Default::default(),
poc_st_foll: Default::default(),
poc_lt_curr: Default::default(),
poc_lt_foll: Default::default(),
ref_pic_set_lt_curr: Default::default(),
ref_pic_set_st_curr_after: Default::default(),
ref_pic_set_st_curr_before: Default::default(),
ref_pic_set_st_foll: Default::default(),
ref_pic_set_lt_foll: Default::default(),
}
}
}
/// State of the picture being currently decoded.
///
/// Stored between calls to [`StatelessDecoder::handle_slice`] that belong to the same picture.
struct CurrentPicState<H: DecodedHandle, P> {
/// Data for the current picture as extracted from the stream.
pic: PictureData,
/// Backend-specific data for that picture.
backend_pic: P,
/// List of reference pictures, used once per slice.
ref_pic_lists: ReferencePicLists<H>,
}
/// All the reference picture lists used to decode a stream.
struct ReferencePicLists<T> {
/// Reference picture list 0 for P and B slices. Retains the same meaning as
/// in the specification. Points into the pictures stored in the DPB.
/// Derived once per slice.
ref_pic_list0: [Option<RefPicListEntry<T>>; MAX_DPB_SIZE],
/// Reference picture list 1 for B slices. Retains the same meaning as in
/// the specification. Points into the pictures stored in the DPB. Derived
/// once per slice.
ref_pic_list1: [Option<RefPicListEntry<T>>; MAX_DPB_SIZE],
}
impl<T> Default for ReferencePicLists<T> {
fn default() -> Self {
Self {
ref_pic_list0: Default::default(),
ref_pic_list1: Default::default(),
}
}
}
pub struct H265DecoderState<H: DecodedHandle, P> {
/// A parser to extract bitstream metadata
parser: Parser,
/// Keeps track of the last values seen for negotiation purposes.
negotiation_info: NegotiationInfo,
/// The set of reference pictures.
rps: RefPicSet<H>,
/// The decoded picture buffer
dpb: Dpb<H>,
/// The current active SPS id.
cur_sps_id: u8,
/// Used to identify first picture in decoding order or first picture that
/// follows an EOS NALU.
first_picture_after_eos: bool,
/// Whether this is the first picture in the bitstream in decoding order.
first_picture_in_bitstream: bool,
// Same as PrevTid0Pic in the specification.
prev_tid_0_pic: Option<PictureData>,
/// A H.265 syntax element.
max_pic_order_cnt_lsb: i32,
/// The value of NoRaslOutputFlag for the last IRAP picture.
irap_no_rasl_output_flag: bool,
/// We keep track of the last independent header so we can copy that into
/// dependent slices.
last_independent_slice_header: Option<SliceHeader>,
/// The picture currently being decoded. We need to preserve it between
/// calls to `decode` because multiple slices will be processed in different
/// calls to `decode`.
current_pic: Option<CurrentPicState<H, P>>,
pending_pps: Vec<Nalu<'static>>,
}
impl<H, P> Default for H265DecoderState<H, P>
where
H: DecodedHandle + Clone,
{
fn default() -> Self {
H265DecoderState {
parser: Default::default(),
negotiation_info: Default::default(),
rps: Default::default(),
dpb: Default::default(),
cur_sps_id: Default::default(),
first_picture_after_eos: true,
first_picture_in_bitstream: true,
prev_tid_0_pic: Default::default(),
max_pic_order_cnt_lsb: Default::default(),
irap_no_rasl_output_flag: Default::default(),
last_independent_slice_header: Default::default(),
current_pic: Default::default(),
pending_pps: Default::default(),
}
}
}
/// [`StatelessCodec`] structure to use in order to create a H.265 stateless decoder.
///
/// # Accepted input
///
/// A decoder using this codec processes exactly one NAL unit of input per call to
/// [`StatelessDecoder::decode`], and returns the number of bytes until the end of this NAL unit.
/// This makes it possible to call [`Decode`](StatelessDecoder::decode) repeatedly on some unsplit
/// Annex B stream and shrinking it by the number of bytes processed after each call, until the
/// stream ends up being empty.
pub struct H265;
impl StatelessCodec for H265 {
type FormatInfo = Sps;
type DecoderState<H: DecodedHandle, P> = H265DecoderState<H, P>;
}
impl<B> StatelessDecoder<H265, B>
where
B: StatelessH265DecoderBackend + TryFormat<H265>,
B::Handle: Clone,
{
/// Whether the stream parameters have changed, indicating that a negotiation window has opened.
fn negotiation_possible(
sps: &Sps,
dpb: &Dpb<B::Handle>,
old_negotiation_info: &NegotiationInfo,
) -> bool {
let negotiation_info = NegotiationInfo::from(sps);
let max_dpb_size = std::cmp::min(sps.max_dpb_size(), 16);
let prev_max_dpb_size = dpb.max_num_pics();
*old_negotiation_info != negotiation_info || prev_max_dpb_size != max_dpb_size
}
/// Apply the parameters of `sps` to the decoder.
fn apply_sps(&mut self, sps: &Sps) -> anyhow::Result<()> {
self.drain()?;
self.codec.negotiation_info = NegotiationInfo::from(sps);
let max_dpb_size = std::cmp::min(sps.max_dpb_size(), 16);
self.codec.dpb.set_max_num_pics(max_dpb_size);
self.coded_resolution = Resolution {
width: u32::from(sps.width()),
height: u32::from(sps.height()),
};
Ok(())
}
// See 8.3.2, Note 2.
fn st_ref_pic_set<'a>(hdr: &'a SliceHeader, sps: &'a Sps) -> &'a ShortTermRefPicSet {
if hdr.curr_rps_idx == sps.num_short_term_ref_pic_sets {
&hdr.short_term_ref_pic_set
} else {
&sps.short_term_ref_pic_set[usize::from(hdr.curr_rps_idx)]
}
}
// See 8.3.2.
fn decode_rps(
&mut self,
slice: &Slice,
sps: &Sps,
cur_pic: &PictureData,
) -> anyhow::Result<()> {
let hdr = &slice.header;
if cur_pic.nalu_type.is_irap() && cur_pic.no_rasl_output_flag {
self.codec.dpb.mark_all_as_unused_for_ref();
}
if slice.nalu.header.type_.is_idr() {
self.codec.rps.poc_st_curr_before = Default::default();
self.codec.rps.poc_st_curr_after = Default::default();
self.codec.rps.poc_st_foll = Default::default();
self.codec.rps.poc_lt_curr = Default::default();
self.codec.rps.poc_lt_foll = Default::default();
self.codec.rps.num_poc_st_curr_before = 0;
self.codec.rps.num_poc_st_curr_after = 0;
self.codec.rps.num_poc_st_foll = 0;
self.codec.rps.num_poc_lt_curr = 0;
self.codec.rps.num_poc_lt_foll = 0;
} else {
let curr_st_rps = Self::st_ref_pic_set(hdr, sps);
let mut j = 0;
let mut k = 0;
for i in 0..usize::from(curr_st_rps.num_negative_pics) {
let poc = cur_pic.pic_order_cnt_val + curr_st_rps.delta_poc_s0[i];
if curr_st_rps.used_by_curr_pic_s0[i] {
self.codec.rps.poc_st_curr_before[j] = poc;
j += 1;
} else {
self.codec.rps.poc_st_foll[k] = poc;
k += 1;
}
}
self.codec.rps.num_poc_st_curr_before = j as _;
let mut j = 0;
for i in 0..usize::from(curr_st_rps.num_positive_pics) {
let poc = cur_pic.pic_order_cnt_val + curr_st_rps.delta_poc_s1[i];
if curr_st_rps.used_by_curr_pic_s1[i] {
self.codec.rps.poc_st_curr_after[j] = poc;
j += 1;
} else {
self.codec.rps.poc_st_foll[k] = poc;
k += 1;
}
}
self.codec.rps.num_poc_st_curr_after = j as _;
self.codec.rps.num_poc_st_foll = k as _;
let mut j = 0;
let mut k = 0;
for i in 0..usize::from(hdr.num_long_term_sps + hdr.num_long_term_pics) {
let mut poc_lt = hdr.poc_lsb_lt[i] as i32;
if hdr.delta_poc_msb_present_flag[i] {
poc_lt += cur_pic.pic_order_cnt_val;
let delta_poc =
hdr.delta_poc_msb_cycle_lt[i] as i32 * self.codec.max_pic_order_cnt_lsb;
poc_lt -= delta_poc;
poc_lt -= cur_pic.pic_order_cnt_val & (self.codec.max_pic_order_cnt_lsb - 1);
}
if hdr.used_by_curr_pic_lt[i] {
self.codec.rps.poc_lt_curr[j] = poc_lt;
self.codec.rps.curr_delta_poc_msb_present_flag[j] =
hdr.delta_poc_msb_present_flag[i];
j += 1;
} else {
self.codec.rps.poc_lt_foll[k] = poc_lt;
self.codec.rps.foll_delta_poc_msb_present_flag[k] =
hdr.delta_poc_msb_present_flag[i];
k += 1;
}
}
self.codec.rps.num_poc_lt_curr = j as _;
self.codec.rps.num_poc_lt_foll = k as _;
}
self.derive_and_mark_rps()?;
Ok(())
}
// See the derivation process in the second half of 8.3.2.
fn derive_and_mark_rps(&mut self) -> anyhow::Result<()> {
let max_pic_order_cnt_lsb = self.codec.max_pic_order_cnt_lsb;
// Equation 8-6
for i in 0..self.codec.rps.num_poc_lt_curr {
if !self.codec.rps.curr_delta_poc_msb_present_flag[i] {
let poc = self.codec.rps.poc_lt_curr[i];
let mask = max_pic_order_cnt_lsb - 1;
let reference = self.codec.dpb.find_ref_by_poc_masked(poc, mask);
if reference.is_none() {
log::warn!("No reference found for poc {} and mask {}", poc, mask);
}
self.codec.rps.ref_pic_set_lt_curr[i] = reference;
} else {
let poc = self.codec.rps.poc_lt_curr[i];
let reference = self.codec.dpb.find_ref_by_poc(poc);
if reference.is_none() {
log::warn!("No reference found for poc {}", poc);
}
self.codec.rps.ref_pic_set_lt_curr[i] = reference;
}
}
for i in 0..self.codec.rps.num_poc_lt_foll {
if !self.codec.rps.foll_delta_poc_msb_present_flag[i] {
let poc = self.codec.rps.poc_lt_foll[i];
let mask = max_pic_order_cnt_lsb - 1;
let reference = self.codec.dpb.find_ref_by_poc_masked(poc, mask);
if reference.is_none() {
log::warn!("No reference found for poc {} and mask {}", poc, mask);
}
self.codec.rps.ref_pic_set_lt_foll[i] = reference;
} else {
let poc = self.codec.rps.poc_lt_foll[i];
let reference = self.codec.dpb.find_ref_by_poc(poc);
if reference.is_none() {
log::warn!("No reference found for poc {}", poc);
}
self.codec.rps.ref_pic_set_lt_foll[i] = reference;
}
}
for pic in self.codec.rps.ref_pic_set_lt_curr.iter().flatten() {
pic.0.borrow_mut().set_reference(Reference::LongTerm);
}
for pic in self.codec.rps.ref_pic_set_lt_foll.iter().flatten() {
pic.0.borrow_mut().set_reference(Reference::LongTerm);
}
// Equation 8-7
for i in 0..self.codec.rps.num_poc_st_curr_before {
let poc = self.codec.rps.poc_st_curr_before[i];
let reference = self.codec.dpb.find_short_term_ref_by_poc(poc);
if reference.is_none() {
log::warn!("No reference found for poc {}", poc);
}
self.codec.rps.ref_pic_set_st_curr_before[i] = reference;
}
for i in 0..self.codec.rps.num_poc_st_curr_after {
let poc = self.codec.rps.poc_st_curr_after[i];
let reference = self.codec.dpb.find_short_term_ref_by_poc(poc);
if reference.is_none() {
log::warn!("No reference found for poc {}", poc);
}
self.codec.rps.ref_pic_set_st_curr_after[i] = reference;
}
for i in 0..self.codec.rps.num_poc_st_foll {
let poc = self.codec.rps.poc_st_foll[i];
let reference = self.codec.dpb.find_short_term_ref_by_poc(poc);
if reference.is_none() {
log::warn!("No reference found for poc {}", poc);
}
self.codec.rps.ref_pic_set_st_foll[i] = reference;
}
// 4. All reference pictures in the DPB that are not included in
// RefPicSetLtCurr, RefPicSetLtFoll, RefPicSetStCurrBefore,
// RefPicSetStCurrAfter, or RefPicSetStFoll and have nuh_layer_id equal
// to currPicLayerId are marked as "unused for reference"
for dpb_pic in self.codec.dpb.entries() {
let find_predicate = |p: &Option<DpbEntry<B::Handle>>| match p {
Some(p) => p.0.borrow().pic_order_cnt_val == dpb_pic.0.borrow().pic_order_cnt_val,
None => false,
};
if !self.codec.rps.ref_pic_set_lt_curr[0..self.codec.rps.num_poc_lt_curr]
.iter()
.any(find_predicate)
&& !self.codec.rps.ref_pic_set_lt_foll[0..self.codec.rps.num_poc_lt_foll]
.iter()
.any(find_predicate)
&& !self.codec.rps.ref_pic_set_st_curr_after
[0..self.codec.rps.num_poc_st_curr_after]
.iter()
.any(find_predicate)
&& !self.codec.rps.ref_pic_set_st_curr_before
[0..self.codec.rps.num_poc_st_curr_before]
.iter()
.any(find_predicate)
&& !self.codec.rps.ref_pic_set_st_foll[0..self.codec.rps.num_poc_st_foll]
.iter()
.any(find_predicate)
{
dpb_pic.0.borrow_mut().set_reference(Reference::None);
}
}
let total_rps_len = self.codec.rps.ref_pic_set_lt_curr[0..self.codec.rps.num_poc_lt_curr]
.len()
+ self.codec.rps.ref_pic_set_lt_foll[0..self.codec.rps.num_poc_lt_foll].len()
+ self.codec.rps.ref_pic_set_st_curr_after[0..self.codec.rps.num_poc_st_curr_after]
.len()
+ self.codec.rps.ref_pic_set_st_curr_before[0..self.codec.rps.num_poc_st_curr_before]
.len()
+ self.codec.rps.ref_pic_set_st_foll[0..self.codec.rps.num_poc_st_foll].len();
let dpb_len = self.codec.dpb.entries().len();
if dpb_len != total_rps_len {
log::warn!(
"The total RPS length {} is not the same as the DPB length {}",
total_rps_len,
dpb_len
);
log::warn!("A reference pic may be in more than one RPS list. This is against the specification. See 8.3.2. NOTE 5")
}
// According to Chromium, unavailable reference pictures are handled by
// the accelerators internally.
Ok(())
}
/// Drain the decoder, processing all pending frames.
fn drain(&mut self) -> anyhow::Result<()> {
log::debug!("Draining the decoder");
// Finish the current picture if there is one pending.
if let Some(cur_pic) = self.codec.current_pic.take() {
self.finish_picture(cur_pic)?;
}
let pics = self.codec.dpb.drain();
log::debug!(
"Adding POCs {:?} to the ready queue while draining",
pics.iter()
.map(|p| p.0.borrow().pic_order_cnt_val)
.collect::<Vec<_>>()
);
log::trace!(
"{:#?}",
pics.iter().map(|p| p.0.borrow()).collect::<Vec<_>>()
);
self.ready_queue.extend(pics.into_iter().map(|h| h.1));
self.codec.dpb.clear();
Ok(())
}
fn clear_ref_lists(&mut self) {
if let Some(pic) = self.codec.current_pic.as_mut() {
pic.ref_pic_lists = Default::default();
}
self.codec.rps.ref_pic_set_lt_curr = Default::default();
self.codec.rps.ref_pic_set_st_curr_after = Default::default();
self.codec.rps.ref_pic_set_st_curr_before = Default::default();
self.codec.rps.num_poc_lt_curr = Default::default();
self.codec.rps.num_poc_lt_foll = Default::default();
self.codec.rps.num_poc_st_curr_after = Default::default();
self.codec.rps.num_poc_st_curr_before = Default::default();
self.codec.rps.num_poc_st_foll = Default::default();
}
/// Bumps the DPB if needed.
fn bump_as_needed(
&mut self,
bumping_type: BumpingType,
) -> anyhow::Result<Vec<DpbEntry<B::Handle>>> {
let mut pics = vec![];
let needs_bumping = match bumping_type {
BumpingType::BeforeDecoding => Dpb::<B::Handle>::needs_bumping,
BumpingType::AfterDecoding => Dpb::<B::Handle>::needs_additional_bumping,
};
let sps = self
.codec
.parser
.get_sps(self.codec.cur_sps_id)
.context("Invalid SPS id")?;
while needs_bumping(&mut self.codec.dpb, sps) {
match self.codec.dpb.bump(false) {
Some(pic) => pics.push(pic),
None => return Ok(pics),
}
}
Ok(pics)
}
// See C.5.2.2
fn update_dpb_before_decoding(&mut self, cur_pic: &PictureData) -> anyhow::Result<()> {
if cur_pic.nalu_type.is_irap()
&& cur_pic.no_rasl_output_flag
&& !self.codec.first_picture_after_eos
{
if cur_pic.no_output_of_prior_pics_flag {
self.codec.dpb.clear();
} else {
self.drain()?;
}
} else {
self.codec.dpb.remove_unused();
let bumped = self.bump_as_needed(BumpingType::BeforeDecoding)?;
log::debug!(
"Adding POCs {:?} to the ready queue before decoding",
bumped
.iter()
.map(|p| p.0.borrow().pic_order_cnt_val)
.collect::<Vec<_>>()
);
log::trace!(
"{:#?}",
bumped.iter().map(|p| p.0.borrow()).collect::<Vec<_>>()
);
let bumped = bumped.into_iter().map(|p| p.1).collect::<Vec<_>>();
self.ready_queue.extend(bumped);
}
Ok(())
}
/// Called once per picture to start it.
#[allow(clippy::type_complexity)]
fn begin_picture(
&mut self,
timestamp: u64,
slice: &Slice,
) -> Result<Option<CurrentPicState<B::Handle, B::Picture>>, DecodeError> {
self.update_current_set_ids(slice.header.pic_parameter_set_id)?;
self.renegotiate_if_needed(RenegotiationType::CurrentSps)?;
// We renegotiated and must return the NALU and wait.
if matches!(self.decoding_state, DecodingState::AwaitingFormat(_)) {
return Err(DecodeError::CheckEvents);
}
let mut backend_pic = self.backend.new_picture(self.coded_resolution, timestamp)?;
let pic = PictureData::new_from_slice(
slice,
self.codec.first_picture_in_bitstream,
self.codec.first_picture_after_eos,
self.codec.prev_tid_0_pic.as_ref(),
self.codec.max_pic_order_cnt_lsb,
);
self.codec.first_picture_after_eos = false;
self.codec.first_picture_in_bitstream = false;
if pic.nalu_type.is_irap() {
self.codec.irap_no_rasl_output_flag = pic.no_rasl_output_flag;
} else if pic.nalu_type.is_rasl() && self.codec.irap_no_rasl_output_flag {
// NOTE – All RASL pictures are leading pictures of an associated
// BLA or CRA picture. When the associated IRAP picture has
// NoRaslOutputFlag equal to 1, the RASL picture is not output and
// may not be correctly decodable, as the RASL picture may contain
// references to pictures that are not present in the bitstream.
// RASL pictures are not used as reference pictures for the decoding
// process of non-RASL pictures.
log::debug!(
"Dropping POC {}, as it may not be decodable according to the specification",
pic.pic_order_cnt_val
);
return Ok(None);
}
log::debug!("Decode picture POC {}", pic.pic_order_cnt_val);
let pps = self
.codec
.parser
.get_pps(slice.header.pic_parameter_set_id)
.context("invalid PPS ID")?
.clone();
self.decode_rps(slice, &pps.sps, &pic)?;
self.update_dpb_before_decoding(&pic)?;
self.backend.begin_picture(
&mut backend_pic,
&pic,
pps.sps.as_ref(),
&pps,
&self.codec.dpb,
&self.codec.rps,
slice,
)?;
Ok(Some(CurrentPicState {
pic,
backend_pic,
ref_pic_lists: Default::default(),
}))
}
fn update_current_set_ids(&mut self, pps_id: u8) -> anyhow::Result<()> {
let pps = self.codec.parser.get_pps(pps_id).context("Invalid PPS")?;
self.codec.cur_sps_id = pps.seq_parameter_set_id;
Ok(())
}
/// Handle a slice. Called once per slice NALU.
fn handle_slice(
&mut self,
pic: &mut CurrentPicState<B::Handle, B::Picture>,
slice: &Slice,
) -> anyhow::Result<()> {
// A dependent slice may refer to a previous SPS which
// is not the one currently in use.
self.update_current_set_ids(slice.header.pic_parameter_set_id)?;
let pps = self
.codec
.parser
.get_pps(slice.header.pic_parameter_set_id)
.context("invalid PPS ID")?;
let sps = pps.sps.as_ref();
pic.ref_pic_lists = self
.codec
.rps
.build_ref_pic_lists(&slice.header, pps, &pic.pic)?;
// Make sure that no negotiation is possible mid-picture. How could it?
// We'd lose the context with the previous slices on it.
if Self::negotiation_possible(sps, &self.codec.dpb, &self.codec.negotiation_info) {
anyhow::bail!("invalid stream: mid-frame format negotiation requested");
}
self.backend.decode_slice(
&mut pic.backend_pic,
slice,
sps,
pps,
&pic.ref_pic_lists.ref_pic_list0,
&pic.ref_pic_lists.ref_pic_list1,
)?;
Ok(())
}
fn finish_picture(
&mut self,
pic: CurrentPicState<B::Handle, B::Picture>,
) -> anyhow::Result<()> {
log::debug!("Finishing picture POC {:?}", pic.pic.pic_order_cnt_val);
// Submit the picture to the backend.
let handle = self.submit_picture(pic.backend_pic)?;
let pic = pic.pic;
// 8.3.1
if pic.valid_for_prev_tid0_pic {
self.codec.prev_tid_0_pic = Some(pic.clone());
}
self.clear_ref_lists();
// First store the current picture in the DPB, only then we should
// decide whether to bump.
self.codec
.dpb
.store_picture(Rc::new(RefCell::new(pic)), handle)
.map_err(|err| anyhow!(err))?;
let bumped = self.bump_as_needed(BumpingType::AfterDecoding)?;
log::debug!(
"Adding POCs {:?} to the ready queue after decoding",
bumped
.iter()
.map(|p| p.0.borrow().pic_order_cnt_val)
.collect::<Vec<_>>()
);
log::trace!(
"{:#?}",
bumped.iter().map(|p| p.0.borrow()).collect::<Vec<_>>()
);
let bumped = bumped.into_iter().map(|p| p.1).collect::<Vec<_>>();
self.ready_queue.extend(bumped);
Ok(())
}
fn renegotiate_if_needed(
&mut self,
renegotiation_type: RenegotiationType,
) -> anyhow::Result<()> {
let sps = match renegotiation_type {
RenegotiationType::CurrentSps => self
.codec
.parser
.get_sps(self.codec.cur_sps_id)
.context("Invalid SPS")?,
RenegotiationType::NewSps(sps) => sps,
};
if Self::negotiation_possible(sps, &self.codec.dpb, &self.codec.negotiation_info) {
// Make sure all the frames we decoded so far are in the ready queue.
self.drain()?;
let sps = match renegotiation_type {
RenegotiationType::CurrentSps => self
.codec
.parser
.get_sps(self.codec.cur_sps_id)
.context("Invalid SPS")?,
RenegotiationType::NewSps(sps) => sps,
};
self.backend.new_sequence(sps)?;
self.await_format_change(sps.clone());
}
Ok(())
}
fn process_nalu(&mut self, timestamp: u64, nalu: Nalu) -> Result<(), DecodeError> {
log::debug!(
"Processing NALU {:?}, length is {}",
nalu.header.type_,
nalu.size
);
match nalu.header.type_ {
NaluType::VpsNut => {
self.codec
.parser
.parse_vps(&nalu)
.map_err(|err| DecodeError::ParseFrameError(err))?;
}
NaluType::SpsNut => {
let sps = self
.codec
.parser
.parse_sps(&nalu)
.map_err(|err| DecodeError::ParseFrameError(err))?;
self.codec.max_pic_order_cnt_lsb = 1 << (sps.log2_max_pic_order_cnt_lsb_minus4 + 4);
let pending_pps = std::mem::take(&mut self.codec.pending_pps);
// Try parsing the PPS again.
for pps in pending_pps.into_iter() {
if self.codec.parser.parse_pps(&pps).is_err() {
// Failed to parse PPS add it again to pending
self.codec.pending_pps.push(pps);
}
}
}
NaluType::PpsNut => {
if self.codec.parser.parse_pps(&nalu).is_err() {
self.codec.pending_pps.push(nalu.into_owned())
}
}
NaluType::BlaWLp
| NaluType::BlaWRadl
| NaluType::BlaNLp
| NaluType::IdrWRadl
| NaluType::IdrNLp
| NaluType::TrailN
| NaluType::TrailR
| NaluType::TsaN
| NaluType::TsaR
| NaluType::StsaN
| NaluType::StsaR
| NaluType::RadlN
| NaluType::RadlR
| NaluType::RaslN
| NaluType::RaslR
| NaluType::CraNut => {
let mut slice = self
.codec
.parser
.parse_slice_header(nalu)
.map_err(|err| DecodeError::ParseFrameError(err))?;
let first_slice_segment_in_pic_flag = slice.header.first_slice_segment_in_pic_flag;
if slice.header.dependent_slice_segment_flag {
let previous_independent_header = self.codec.last_independent_slice_header.as_ref().ok_or(anyhow!("Cannot process an dependent slice without first processing and independent one"))?.clone();
slice
.replace_header(previous_independent_header)
.map_err(|err| DecodeError::ParseFrameError(err))?;
} else {
self.codec.last_independent_slice_header = Some(slice.header.clone());
}
let cur_pic = match self.codec.current_pic.take() {
// No current picture, start a new one.
None => self.begin_picture(timestamp, &slice)?,
Some(cur_pic) if first_slice_segment_in_pic_flag => {
self.finish_picture(cur_pic)?;
self.begin_picture(timestamp, &slice)?
}
Some(cur_pic) => Some(cur_pic),
};
// Picture may have been dropped during begin_picture()
if let Some(mut cur_pic) = cur_pic {
self.handle_slice(&mut cur_pic, &slice)?;
self.codec.current_pic = Some(cur_pic);
}
}
NaluType::EosNut => {
self.codec.first_picture_after_eos = true;
}
NaluType::EobNut => {
self.codec.first_picture_in_bitstream = true;
}
other => {
log::debug!("Unsupported NAL unit type {:?}", other,);
}
}
Ok(())
}
/// Submits the picture to the accelerator.
fn submit_picture(&mut self, backend_pic: B::Picture) -> Result<B::Handle, DecodeError> {
let handle = self.backend.submit_picture(backend_pic)?;
if self.blocking_mode == BlockingMode::Blocking {
handle.sync()?;
}
Ok(handle)
}
}
impl<B> StatelessVideoDecoder for StatelessDecoder<H265, B>
where
B: StatelessH265DecoderBackend + TryFormat<H265>,
B::Handle: Clone + 'static,
{
type Handle = B::Handle;
type FramePool = B::FramePool;
fn decode(&mut self, timestamp: u64, bitstream: &[u8]) -> Result<usize, DecodeError> {
let mut cursor = Cursor::new(bitstream);
let nalu = Nalu::next(&mut cursor).map_err(|err| DecodeError::ParseFrameError(err))?;
if nalu.header.type_ == NaluType::SpsNut {
let sps = self
.codec
.parser
.parse_sps(&nalu)
.map_err(|err| DecodeError::ParseFrameError(err))?
.clone();
if matches!(self.decoding_state, DecodingState::AwaitingStreamInfo) {
// If more SPS come along we will renegotiate in begin_picture().
self.renegotiate_if_needed(RenegotiationType::NewSps(&sps))?;
} else if matches!(self.decoding_state, DecodingState::Reset) {
// We can resume decoding since the decoding parameters have not changed.
self.decoding_state = DecodingState::Decoding;
}
} else if matches!(self.decoding_state, DecodingState::Reset) {
let mut cursor = Cursor::new(bitstream);
while let Ok(nalu) = Nalu::next(&mut cursor) {
// In the Reset state we can resume decoding from any key frame.
if nalu.header.type_.is_idr() {
self.decoding_state = DecodingState::Decoding;
break;
}
}
}
let nalu_len = nalu.offset + nalu.size;
match &mut self.decoding_state {
// Process parameter sets, but skip input until we get information
// from the stream.
DecodingState::AwaitingStreamInfo | DecodingState::Reset => {
if matches!(
nalu.header.type_,
NaluType::VpsNut | NaluType::SpsNut | NaluType::PpsNut
) {
self.process_nalu(timestamp, nalu)?;
}
}
// Ask the client to confirm the format before we can process this.
DecodingState::AwaitingFormat(_) => return Err(DecodeError::CheckEvents),
DecodingState::Decoding => {
self.process_nalu(timestamp, nalu)?;
}
}
Ok(nalu_len)
}
fn flush(&mut self) -> Result<(), DecodeError> {
self.drain()?;
self.decoding_state = DecodingState::Reset;
Ok(())
}
fn next_event(&mut self) -> Option<DecoderEvent<B::Handle>> {
self.query_next_event(|decoder, sps| {
// Apply the SPS settings to the decoder so we don't enter the AwaitingFormat state
// on the next decode() call.
// TODO: unwrap this for now, but ideally change this closure to return Result
decoder.apply_sps(sps).unwrap();
})
}
fn frame_pool(&mut self, layer: PoolLayer) -> Vec<&mut B::FramePool> {
self.backend.frame_pool(layer)
}
fn stream_info(&self) -> Option<&StreamInfo> {
self.backend.stream_info()
}
fn poll_fd(&self) -> BorrowedFd {
self.epoll_fd.0.as_fd()
}
}
#[cfg(test)]
pub mod tests {
use crate::bitstream_utils::NalIterator;
use crate::codec::h265::parser::Nalu;
use crate::decoder::stateless::h265::H265;
use crate::decoder::stateless::tests::test_decode_stream;
use crate::decoder::stateless::tests::TestStream;
use crate::decoder::stateless::StatelessDecoder;
use crate::decoder::BlockingMode;
use crate::utils::simple_playback_loop;
use crate::utils::simple_playback_loop_owned_frames;
use crate::DecodedFormat;
/// Run `test` using the dummy decoder, in both blocking and non-blocking modes.
fn test_decoder_dummy(test: &TestStream, blocking_mode: BlockingMode) {
let decoder = StatelessDecoder::<H265, _>::new_dummy(blocking_mode).unwrap();
test_decode_stream(
|d, s, f| {
simple_playback_loop(
d,
NalIterator::<Nalu>::new(s),
f,
&mut simple_playback_loop_owned_frames,
DecodedFormat::NV12,
blocking_mode,
)
},
decoder,
test,
false,
false,
);
}
/// A 64x64 progressive byte-stream encoded I-frame to make it easier to
/// spot errors on the libva trace.
/// Encoded with the following GStreamer pipeline:
///
/// gst-launch-1.0 videotestsrc num-buffers=1 ! video/x-raw,format=I420,width=64,height=64 ! x265enc ! video/x-h265,profile=main ! filesink location="64x64-I.h265"
pub const DECODE_64X64_PROGRESSIVE_I: TestStream = TestStream {
stream: include_bytes!("../../codec/h265/test_data/64x64-I.h265"),
crcs: include_str!("../../codec/h265/test_data/64x64-I.h265.crc"),
};
#[test]
fn test_64x64_progressive_i_block() {
test_decoder_dummy(&DECODE_64X64_PROGRESSIVE_I, BlockingMode::Blocking);
}
#[test]
fn test_64x64_progressive_i_nonblock() {
test_decoder_dummy(&DECODE_64X64_PROGRESSIVE_I, BlockingMode::NonBlocking);
}
/// A 64x64 progressive byte-stream encoded I-frame and P-frame to make
/// it easier to spot errors on the libva trace.
/// Encoded with the following GStreamer pipeline:
/// gst-launch-1.0 videotestsrc num-buffers=2 ! video/x-raw,format=I420,width=64,height=64 ! x265enc option-string="b-adapt=0" ! video/x-h265,profile=main ! filesink location="64x64-I-P.h265"
pub const DECODE_64X64_PROGRESSIVE_I_P: TestStream = TestStream {
stream: include_bytes!("../../codec/h265/test_data/64x64-I-P.h265"),
crcs: include_str!("../../codec/h265/test_data/64x64-I-P.h265.crc"),
};
#[test]
fn test_64x64_progressive_i_p_block() {
test_decoder_dummy(&DECODE_64X64_PROGRESSIVE_I_P, BlockingMode::Blocking);
}
#[test]
fn test_64x64_progressive_i_p_nonblock() {
test_decoder_dummy(&DECODE_64X64_PROGRESSIVE_I_P, BlockingMode::NonBlocking);
}
/// A 64x64 progressive byte-stream encoded I-P-B-P sequence to make it
/// easier to it easier to spot errors on the libva trace.
/// Encoded with the following GStreamer pipeline:
/// gst-launch-1.0 videotestsrc num-buffers=3 ! video/x-raw,format=I420,width=64,height=64 ! x265enc option-string="b-adapt=0:bframes=1" ! video/x-h265,profile=main ! filesink location="64x64-I-P-B-P.h265"
pub const DECODE_64X64_PROGRESSIVE_I_P_B_P: TestStream = TestStream {
stream: include_bytes!("../../codec/h265/test_data/64x64-I-P-B-P.h265"),
crcs: include_str!("../../codec/h265/test_data/64x64-I-P-B-P.h265.crc"),
};
#[test]
fn test_64x64_progressive_i_p_b_p_block() {
test_decoder_dummy(&DECODE_64X64_PROGRESSIVE_I_P_B_P, BlockingMode::Blocking);
}
#[test]
fn test_64x64_progressive_i_p_b_p_nonblock() {
test_decoder_dummy(&DECODE_64X64_PROGRESSIVE_I_P_B_P, BlockingMode::NonBlocking);
}
/// Same as Chromium's test-25fps.h265
pub const DECODE_TEST_25FPS: TestStream = TestStream {
stream: include_bytes!("../../codec/h265/test_data/test-25fps.h265"),
crcs: include_str!("../../codec/h265/test_data/test-25fps.h265.crc"),
};
#[test]
fn test_25fps_block() {
test_decoder_dummy(&DECODE_TEST_25FPS, BlockingMode::Blocking);
}
#[test]
fn test_25fps_nonblock() {
test_decoder_dummy(&DECODE_TEST_25FPS, BlockingMode::NonBlocking);
}
/// Same as Chromium's bear.h265
pub const DECODE_BEAR: TestStream = TestStream {
stream: include_bytes!("../../codec/h265/test_data/bear.h265"),
crcs: include_str!("../../codec/h265/test_data/bear.h265.crc"),
};
#[test]
fn test_bear_block() {
test_decoder_dummy(&DECODE_BEAR, BlockingMode::Blocking);
}
#[test]
fn test_bear_nonblock() {
test_decoder_dummy(&DECODE_BEAR, BlockingMode::NonBlocking);
}
/// Same as Chromium's bbb.h265
pub const DECODE_BBB: TestStream = TestStream {
stream: include_bytes!("../../codec/h265/test_data/bbb.h265"),
crcs: include_str!("../../codec/h265/test_data/bbb.h265.crc"),
};
#[test]
fn test_bbb_block() {
test_decoder_dummy(&DECODE_BBB, BlockingMode::Blocking);
}
#[test]
fn test_bbb_nonblock() {
test_decoder_dummy(&DECODE_BBB, BlockingMode::NonBlocking);
}
}