use super::bitstream::{EpByteMap, RbspReader};
use super::cavlc::{decode_cavlc_block, EmbeddablePosition};
use super::slice::SliceType;
use super::sps::{Pps, Sps};
use super::H264Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MbType {
I4x4,
I16x16(u8, u8, u8),
IPCM,
PSkip,
P16x16,
P16x8,
P8x16,
P8x8,
P8x8ref0,
}
impl MbType {
pub fn is_intra(self) -> bool {
matches!(self, Self::I4x4 | Self::I16x16(_, _, _) | Self::IPCM)
}
pub fn is_skip(self) -> bool {
matches!(self, Self::PSkip)
}
}
#[derive(Debug, Clone)]
pub struct Macroblock {
pub mb_type: MbType,
pub mb_qp_delta: i32,
pub coded_block_pattern: u32,
pub luma_total_coeffs: [u8; 16],
pub chroma_total_coeffs: [u8; 8],
pub positions: Vec<EmbeddablePosition>,
pub recon: Option<ReconstructionData>,
pub mv_field: Option<super::mv::MvField>,
}
#[derive(Debug, Clone)]
pub struct ReconstructionData {
pub qp_y: i32,
pub qp_cb: i32,
pub qp_cr: i32,
pub intra4x4_modes: [u8; 16],
pub intra16x16_mode: Option<u8>,
pub intra_chroma_pred_mode: u8,
pub luma_blocks: [[i32; 16]; 16],
pub luma_dc_block: Option<[i32; 16]>,
pub chroma_dc_blocks: [Option<[i32; 4]>; 2],
pub chroma_ac_blocks: [[[i32; 16]; 4]; 2],
}
impl ReconstructionData {
pub fn empty(qp_y: i32) -> Self {
Self {
qp_y,
qp_cb: qp_y,
qp_cr: qp_y,
intra4x4_modes: [0; 16],
intra16x16_mode: None,
intra_chroma_pred_mode: 0,
luma_blocks: [[0i32; 16]; 16],
luma_dc_block: None,
chroma_dc_blocks: [None, None],
chroma_ac_blocks: [[[0i32; 16]; 4]; 2],
}
}
}
pub struct NeighborContext {
width_in_mbs: u32,
luma_tc: Vec<u8>,
luma_width: usize, chroma_cb_tc: Vec<u8>,
chroma_cr_tc: Vec<u8>,
chroma_width: usize, luma_dc_tc: Vec<u8>,
intra4x4_modes: Vec<u8>,
}
impl NeighborContext {
pub fn new(width_in_mbs: u32, height_in_mbs: u32) -> Self {
let luma_width = (width_in_mbs * 4) as usize;
let luma_height = (height_in_mbs * 4) as usize;
let chroma_width = (width_in_mbs * 2) as usize;
let chroma_height = (height_in_mbs * 2) as usize;
let mb_count = (width_in_mbs * height_in_mbs) as usize;
Self {
width_in_mbs,
luma_tc: vec![0; luma_width * luma_height],
luma_width,
chroma_cb_tc: vec![0; chroma_width * chroma_height],
chroma_cr_tc: vec![0; chroma_width * chroma_height],
chroma_width,
luma_dc_tc: vec![0xFF; mb_count],
intra4x4_modes: vec![0xFF; luma_width * luma_height],
}
}
pub fn set_intra4x4_mode(&mut self, block_x: usize, block_y: usize, mode: u8) {
let idx = block_y * self.luma_width + block_x;
if idx < self.intra4x4_modes.len() {
self.intra4x4_modes[idx] = mode;
}
}
pub fn intra4x4_mode(&self, block_x: usize, block_y: usize) -> Option<u8> {
let idx = block_y * self.luma_width + block_x;
self.intra4x4_modes
.get(idx)
.copied()
.filter(|&v| v != 0xFF)
}
pub fn set_luma_dc_tc(&mut self, mb_x: u32, mb_y: u32, tc: u8) {
let idx = (mb_y * self.width_in_mbs + mb_x) as usize;
if idx < self.luma_dc_tc.len() {
self.luma_dc_tc[idx] = tc;
}
}
pub fn luma_dc_nc(&self, mb_x: u32, mb_y: u32) -> i8 {
let block_x = (mb_x * 4) as usize;
let block_y = (mb_y * 4) as usize;
self.luma_nc(block_x, block_y)
}
pub fn set_luma_tc(&mut self, block_x: usize, block_y: usize, tc: u8) {
let idx = block_y * self.luma_width + block_x;
if idx < self.luma_tc.len() {
self.luma_tc[idx] = tc;
}
}
pub fn luma_nc(&self, block_x: usize, block_y: usize) -> i8 {
let have_left = block_x > 0;
let have_above = block_y > 0;
match (have_left, have_above) {
(false, false) => 0,
(true, false) => self.luma_tc[block_y * self.luma_width + block_x - 1] as i8,
(false, true) => {
self.luma_tc[(block_y - 1) * self.luma_width + block_x] as i8
}
(true, true) => {
let na = self.luma_tc[block_y * self.luma_width + block_x - 1] as i16;
let nb =
self.luma_tc[(block_y - 1) * self.luma_width + block_x] as i16;
((na + nb + 1) >> 1) as i8
}
}
}
pub fn chroma_nc(&self, block_x: usize, block_y: usize, is_cr: bool) -> i8 {
let tc = if is_cr {
&self.chroma_cr_tc
} else {
&self.chroma_cb_tc
};
let have_left = block_x > 0;
let have_above = block_y > 0;
match (have_left, have_above) {
(false, false) => 0,
(true, false) => tc[block_y * self.chroma_width + block_x - 1] as i8,
(false, true) => tc[(block_y - 1) * self.chroma_width + block_x] as i8,
(true, true) => {
let na = tc[block_y * self.chroma_width + block_x - 1] as i16;
let nb = tc[(block_y - 1) * self.chroma_width + block_x] as i16;
((na + nb + 1) >> 1) as i8
}
}
}
pub fn set_chroma_tc(
&mut self,
block_x: usize,
block_y: usize,
is_cr: bool,
tc: u8,
) {
let grid = if is_cr {
&mut self.chroma_cr_tc
} else {
&mut self.chroma_cb_tc
};
let idx = block_y * self.chroma_width + block_x;
if idx < grid.len() {
grid[idx] = tc;
}
}
}
pub const BLOCK_INDEX_TO_POS: [(u8, u8); 16] = [
(0, 0), (1, 0), (0, 1), (1, 1), (2, 0), (3, 0), (2, 1), (3, 1), (0, 2), (1, 2), (0, 3), (1, 3), (2, 2), (3, 2), (2, 3), (3, 3), ];
fn decode_i16x16_mb_type(mb_type_minus1: u32) -> (u8, u8, u8) {
let pred_mode = (mb_type_minus1 % 4) as u8;
let group = mb_type_minus1 / 4;
let cbp_chroma = (group % 3) as u8;
let cbp_luma = if group < 3 { 0 } else { 15 };
(pred_mode, cbp_luma, cbp_chroma)
}
const CBP_INTER_TABLE: [u32; 48] = [
0, 16, 1, 2, 4, 8, 32, 3, 5, 10, 12, 15, 47, 7, 11, 13,
14, 6, 9, 31, 35, 37, 42, 44, 33, 34, 36, 40, 39, 43, 45, 46,
17, 18, 20, 24, 19, 21, 26, 28, 23, 27, 29, 30, 22, 25, 38, 41,
];
pub const CBP_INTRA_TABLE: [u32; 48] = [
47, 31, 15, 0, 23, 27, 29, 30, 7, 11, 13, 14, 39, 43, 45, 46,
16, 3, 5, 10, 12, 19, 21, 26, 28, 35, 37, 42, 44, 1, 2, 4,
8, 17, 18, 20, 24, 6, 9, 22, 25, 32, 33, 34, 36, 40, 38, 41,
];
pub fn parse_macroblock(
reader: &mut RbspReader<'_>,
slice_type: SliceType,
mb_x: u32,
mb_y: u32,
sps: &Sps,
pps: &Pps,
ctx: &mut NeighborContext,
ep_map: &EpByteMap,
raw_data: &[u8],
current_qp: &mut i32,
num_ref_idx_l0_active: u8,
) -> Result<Macroblock, H264Error> {
parse_macroblock_with_recon(
reader,
slice_type,
mb_x,
mb_y,
sps,
pps,
ctx,
ep_map,
raw_data,
current_qp,
num_ref_idx_l0_active,
false,
None,
)
}
pub fn parse_macroblock_with_recon(
reader: &mut RbspReader<'_>,
slice_type: SliceType,
mb_x: u32,
mb_y: u32,
sps: &Sps,
pps: &Pps,
ctx: &mut NeighborContext,
ep_map: &EpByteMap,
raw_data: &[u8],
current_qp: &mut i32,
num_ref_idx_l0_active: u8,
capture_recon: bool,
mv_ctx: Option<&mut super::mv::MvPredictorContext>,
) -> Result<Macroblock, H264Error> {
let mut positions = Vec::new();
let mut luma_total_coeffs = [0u8; 16];
let mut chroma_total_coeffs = [0u8; 8];
let mut recon: Option<ReconstructionData> = None;
let mb_type = parse_mb_type(reader, slice_type)?;
if mb_type == MbType::PSkip {
if let Some(ctx) = mv_ctx {
let base_x = (mb_x * 4) as usize;
let base_y = (mb_y * 4) as usize;
for by in 0..4 {
for bx in 0..4 {
ctx.set(base_x + bx, base_y + by, super::mv::MotionVector::default(), 0);
}
}
}
return Ok(Macroblock {
mb_type,
mb_qp_delta: 0,
coded_block_pattern: 0,
luma_total_coeffs,
chroma_total_coeffs,
positions,
recon: None,
mv_field: Some(super::mv::MvField::default()),
});
}
if mb_type == MbType::IPCM {
reader.align_to_byte();
let pcm_bytes = 256 + 2 * 64; reader.skip_bits(pcm_bytes * 8)?;
luma_total_coeffs.fill(16);
chroma_total_coeffs.fill(16);
update_neighbor_tc(ctx, mb_x, mb_y, &luma_total_coeffs, &chroma_total_coeffs);
return Ok(Macroblock {
mb_type,
mb_qp_delta: 0,
coded_block_pattern: 0,
luma_total_coeffs,
chroma_total_coeffs,
positions,
recon: None,
mv_field: None,
});
}
if capture_recon && mb_type.is_intra() {
recon = Some(ReconstructionData::empty(*current_qp));
}
let parsed_mv_field = parse_prediction_info(
reader,
mb_type,
slice_type,
sps,
pps,
num_ref_idx_l0_active,
mb_x,
mb_y,
ctx,
recon.as_mut(),
mv_ctx,
ep_map,
raw_data,
&mut positions,
)?;
let coded_block_pattern = if let MbType::I16x16(_, cbp_luma, cbp_chroma) = mb_type {
(cbp_chroma as u32) << 4 | cbp_luma as u32
} else {
let cbp_code = reader.read_ue()?;
if cbp_code >= 48 {
return Err(H264Error::CavlcError(format!(
"cbp code_num {cbp_code} >= 48"
)));
}
if mb_type.is_intra() {
CBP_INTRA_TABLE[cbp_code as usize]
} else {
CBP_INTER_TABLE[cbp_code as usize]
}
};
let cbp_luma = coded_block_pattern & 0x0F;
let cbp_chroma = (coded_block_pattern >> 4) & 0x03;
let mb_qp_delta = if cbp_luma > 0 || cbp_chroma > 0 || matches!(mb_type, MbType::I16x16(_, _, _)) {
let delta = reader.read_se()?;
*current_qp = ((*current_qp + delta).rem_euclid(52) + 52) % 52;
delta
} else {
0
};
if let Some(r) = recon.as_mut() {
r.qp_y = *current_qp;
r.qp_cb = super::transform::derive_chroma_qp(*current_qp, pps.chroma_qp_index_offset);
r.qp_cr = super::transform::derive_chroma_qp(
*current_qp,
pps.second_chroma_qp_index_offset,
);
}
let base_luma_x = (mb_x * 4) as usize;
let base_luma_y = (mb_y * 4) as usize;
if let MbType::I16x16(pred_mode, _, _) = mb_type {
let nc = ctx.luma_dc_nc(mb_x, mb_y);
let (dc_block, mut dc_positions) =
decode_cavlc_block(reader, nc, ep_map, raw_data, 16)?;
for p in &mut dc_positions {
p.block_idx = u32::MAX;
}
positions.extend(dc_positions);
ctx.set_luma_dc_tc(mb_x, mb_y, dc_block.total_coeffs);
if let Some(r) = recon.as_mut() {
r.intra16x16_mode = Some(pred_mode);
r.luma_dc_block = Some(dc_block.coeffs);
}
if cbp_luma != 0 {
for blk_idx in 0..16u8 {
let (bx, by) = BLOCK_INDEX_TO_POS[blk_idx as usize];
let luma_4x4_x = base_luma_x + bx as usize;
let luma_4x4_y = base_luma_y + by as usize;
let nc = ctx.luma_nc(luma_4x4_x, luma_4x4_y);
let (block, mut block_positions) =
decode_cavlc_block(reader, nc, ep_map, raw_data, 15)?;
for p in &mut block_positions {
p.block_idx = blk_idx as u32;
}
luma_total_coeffs[blk_idx as usize] = block.total_coeffs;
ctx.set_luma_tc(luma_4x4_x, luma_4x4_y, block.total_coeffs);
positions.extend(block_positions);
if let Some(r) = recon.as_mut() {
r.luma_blocks[blk_idx as usize] = block.coeffs;
}
}
} else {
for blk_idx in 0..16u8 {
let (bx, by) = BLOCK_INDEX_TO_POS[blk_idx as usize];
ctx.set_luma_tc(
base_luma_x + bx as usize,
base_luma_y + by as usize,
0,
);
}
}
} else {
for blk_idx in 0..16u8 {
let (bx, by) = BLOCK_INDEX_TO_POS[blk_idx as usize];
let luma_4x4_x = base_luma_x + bx as usize;
let luma_4x4_y = base_luma_y + by as usize;
let blk8x8_idx = (by / 2) * 2 + bx / 2;
if cbp_luma & (1 << blk8x8_idx) != 0 {
let nc = ctx.luma_nc(luma_4x4_x, luma_4x4_y);
let (block, mut block_positions) =
decode_cavlc_block(reader, nc, ep_map, raw_data, 16)?;
for p in &mut block_positions {
p.block_idx = blk_idx as u32;
}
luma_total_coeffs[blk_idx as usize] = block.total_coeffs;
ctx.set_luma_tc(luma_4x4_x, luma_4x4_y, block.total_coeffs);
positions.extend(block_positions);
if let Some(r) = recon.as_mut() {
r.luma_blocks[blk_idx as usize] = block.coeffs;
}
} else {
ctx.set_luma_tc(luma_4x4_x, luma_4x4_y, 0);
}
}
}
let base_chroma_x = (mb_x * 2) as usize;
let base_chroma_y = (mb_y * 2) as usize;
if cbp_chroma > 0 {
for is_cr in [false, true] {
let (dc_block, mut dc_positions) =
decode_cavlc_block(reader, -1, ep_map, raw_data, 4)?;
let slot: u32 = if is_cr { 17 } else { 16 };
for p in &mut dc_positions {
p.block_idx = slot;
}
positions.extend(dc_positions);
if let Some(r) = recon.as_mut() {
let mut dc = [0i32; 4];
dc.copy_from_slice(&dc_block.coeffs[..4]);
r.chroma_dc_blocks[if is_cr { 1 } else { 0 }] = Some(dc);
}
}
if cbp_chroma == 2 {
for is_cr in [false, true] {
for blk_idx in 0..4u8 {
let cx = base_chroma_x + (blk_idx % 2) as usize;
let cy = base_chroma_y + (blk_idx / 2) as usize;
let nc = ctx.chroma_nc(cx, cy, is_cr);
let (block, mut block_positions) =
decode_cavlc_block(reader, nc, ep_map, raw_data, 15)?;
let slot: u32 = if is_cr { 22 } else { 18 } + blk_idx as u32;
for p in &mut block_positions {
p.block_idx = slot;
}
let chroma_offset = if is_cr { 4 } else { 0 };
chroma_total_coeffs[chroma_offset + blk_idx as usize] =
block.total_coeffs;
ctx.set_chroma_tc(cx, cy, is_cr, block.total_coeffs);
positions.extend(block_positions);
if let Some(r) = recon.as_mut() {
r.chroma_ac_blocks[if is_cr { 1 } else { 0 }][blk_idx as usize] =
block.coeffs;
}
}
}
} else {
for is_cr in [false, true] {
for blk_idx in 0..4u8 {
let cx = base_chroma_x + (blk_idx % 2) as usize;
let cy = base_chroma_y + (blk_idx / 2) as usize;
ctx.set_chroma_tc(cx, cy, is_cr, 0);
}
}
}
} else {
for is_cr in [false, true] {
for blk_idx in 0..4u8 {
let cx = base_chroma_x + (blk_idx % 2) as usize;
let cy = base_chroma_y + (blk_idx / 2) as usize;
ctx.set_chroma_tc(cx, cy, is_cr, 0);
}
}
}
Ok(Macroblock {
mb_type,
mb_qp_delta,
coded_block_pattern,
luma_total_coeffs,
chroma_total_coeffs,
positions,
recon,
mv_field: parsed_mv_field,
})
}
fn update_neighbor_tc(
ctx: &mut NeighborContext,
mb_x: u32,
mb_y: u32,
luma_tc: &[u8; 16],
chroma_tc: &[u8; 8],
) {
let base_luma_x = (mb_x * 4) as usize;
let base_luma_y = (mb_y * 4) as usize;
for blk_idx in 0..16 {
let (bx, by) = BLOCK_INDEX_TO_POS[blk_idx];
ctx.set_luma_tc(
base_luma_x + bx as usize,
base_luma_y + by as usize,
luma_tc[blk_idx],
);
}
let base_cx = (mb_x * 2) as usize;
let base_cy = (mb_y * 2) as usize;
for i in 0..4 {
ctx.set_chroma_tc(base_cx + i % 2, base_cy + i / 2, false, chroma_tc[i]);
ctx.set_chroma_tc(base_cx + i % 2, base_cy + i / 2, true, chroma_tc[i + 4]);
}
}
fn parse_mb_type(
reader: &mut RbspReader<'_>,
slice_type: SliceType,
) -> Result<MbType, H264Error> {
let raw = reader.read_ue()?;
match slice_type {
SliceType::I | SliceType::SI => {
match raw {
0 => Ok(MbType::I4x4),
1..=24 => {
let (pred, cbp_l, cbp_c) = decode_i16x16_mb_type(raw - 1);
Ok(MbType::I16x16(pred, cbp_l, cbp_c))
}
25 => Ok(MbType::IPCM),
_ => Err(H264Error::CavlcError(format!(
"invalid I-slice mb_type: {raw}"
))),
}
}
SliceType::P | SliceType::SP => {
match raw {
0 => Ok(MbType::P16x16),
1 => Ok(MbType::P16x8),
2 => Ok(MbType::P8x16),
3 => Ok(MbType::P8x8),
4 => Ok(MbType::P8x8ref0),
5 => Ok(MbType::I4x4),
6..=29 => {
let (pred, cbp_l, cbp_c) = decode_i16x16_mb_type(raw - 6);
Ok(MbType::I16x16(pred, cbp_l, cbp_c))
}
30 => Ok(MbType::IPCM),
_ => Err(H264Error::CavlcError(format!(
"invalid P-slice mb_type: {raw}"
))),
}
}
_ => Err(H264Error::Unsupported("B/SI slice mb_type parsing not implemented".to_string())),
}
}
fn parse_prediction_info(
reader: &mut RbspReader<'_>,
mb_type: MbType,
_slice_type: SliceType,
_sps: &Sps,
_pps: &Pps,
num_ref_idx_l0_active: u8,
mb_x: u32,
mb_y: u32,
ctx: &mut NeighborContext,
mut recon: Option<&mut ReconstructionData>,
mv_ctx: Option<&mut super::mv::MvPredictorContext>,
ep_map: &EpByteMap,
raw_data: &[u8],
mvd_positions: &mut Vec<EmbeddablePosition>,
) -> Result<Option<super::mv::MvField>, H264Error> {
let max_ref = num_ref_idx_l0_active.saturating_sub(1) as u32;
match mb_type {
MbType::I4x4 => {
let base_x = (mb_x * 4) as usize;
let base_y = (mb_y * 4) as usize;
for blk_idx in 0..16usize {
let (bx, by) = BLOCK_INDEX_TO_POS[blk_idx];
let block_x = base_x + bx as usize;
let block_y = base_y + by as usize;
let left = if bx > 0 || block_x > 0 {
Some(ctx.intra4x4_mode(block_x - 1, block_y).unwrap_or(2))
} else {
None };
let top = if by > 0 || block_y > 0 {
Some(ctx.intra4x4_mode(block_x, block_y - 1).unwrap_or(2))
} else {
None };
let pred_mode = match (left, top) {
(Some(a), Some(b)) => a.min(b),
_ => 2,
};
let prev_flag = reader.read_bit()?;
let resolved = if prev_flag {
pred_mode
} else {
let rem = reader.read_bits(3)? as u8;
if rem < pred_mode {
rem
} else {
rem + 1
}
};
ctx.set_intra4x4_mode(block_x, block_y, resolved);
if let Some(r) = recon.as_deref_mut() {
r.intra4x4_modes[blk_idx] = resolved;
}
}
let chroma_mode = reader.read_ue()? as u8; if let Some(r) = recon.as_deref_mut() {
r.intra_chroma_pred_mode = chroma_mode;
}
}
MbType::I16x16(_, _, _) => {
let chroma_mode = reader.read_ue()? as u8; if let Some(r) = recon {
r.intra_chroma_pred_mode = chroma_mode;
}
}
MbType::P16x16 | MbType::P16x8 | MbType::P8x16 | MbType::P8x8 | MbType::P8x8ref0 => {
if let Some(mv_ctx) = mv_ctx {
return super::mv::parse_mv_field(
reader,
mb_type,
mb_x,
mb_y,
num_ref_idx_l0_active,
mv_ctx,
ep_map,
raw_data,
mvd_positions,
);
}
discard_p_slice_prediction_bits(reader, mb_type, max_ref)?;
}
MbType::IPCM | MbType::PSkip => {}
}
Ok(None)
}
fn discard_p_slice_prediction_bits(
reader: &mut RbspReader<'_>,
mb_type: MbType,
max_ref: u32,
) -> Result<(), H264Error> {
match mb_type {
MbType::P16x16 => {
if max_ref > 0 {
let _ = reader.read_te(max_ref)?;
}
let _ = reader.read_se()?;
let _ = reader.read_se()?;
}
MbType::P16x8 | MbType::P8x16 => {
for _ in 0..2 {
if max_ref > 0 {
let _ = reader.read_te(max_ref)?;
}
}
for _ in 0..2 {
let _ = reader.read_se()?;
let _ = reader.read_se()?;
}
}
MbType::P8x8 | MbType::P8x8ref0 => {
let mut sub_types = [0u32; 4];
for item in sub_types.iter_mut() {
*item = reader.read_ue()?;
}
if mb_type != MbType::P8x8ref0 && max_ref > 0 {
for _ in 0..4 {
let _ = reader.read_te(max_ref)?;
}
}
for &st in &sub_types {
let num_sub_parts = match st {
0 => 1,
1 | 2 => 2,
3 => 4,
_ => {
return Err(H264Error::CavlcError(format!(
"invalid P-slice sub_mb_type: {st}"
)));
}
};
for _ in 0..num_sub_parts {
let _ = reader.read_se()?;
let _ = reader.read_se()?;
}
}
}
_ => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn neighbor_context_basic() {
let mut ctx = NeighborContext::new(2, 2);
ctx.set_luma_tc(0, 0, 3);
ctx.set_luma_tc(1, 0, 5);
ctx.set_luma_tc(0, 1, 7);
assert_eq!(ctx.luma_nc(0, 0), 0);
assert_eq!(ctx.luma_nc(1, 0), 3);
assert_eq!(ctx.luma_nc(0, 1), 3);
assert_eq!(ctx.luma_nc(1, 1), 6);
}
#[test]
fn block_index_to_pos_coverage() {
let mut seen = std::collections::HashSet::new();
for &(x, y) in &BLOCK_INDEX_TO_POS {
assert!(x < 4 && y < 4);
assert!(seen.insert((x, y)));
}
assert_eq!(seen.len(), 16);
}
#[test]
fn i16x16_mb_type_decode() {
assert_eq!(decode_i16x16_mb_type(0), (0, 0, 0));
assert_eq!(decode_i16x16_mb_type(4), (0, 0, 1));
assert_eq!(decode_i16x16_mb_type(6), (2, 0, 1));
assert_eq!(decode_i16x16_mb_type(8), (0, 0, 2));
assert_eq!(decode_i16x16_mb_type(12), (0, 15, 0));
assert_eq!(decode_i16x16_mb_type(16), (0, 15, 1));
assert_eq!(decode_i16x16_mb_type(20), (0, 15, 2));
assert_eq!(decode_i16x16_mb_type(23), (3, 15, 2));
}
#[test]
fn mb_type_properties() {
assert!(MbType::I4x4.is_intra());
assert!(MbType::I16x16(0, 0, 0).is_intra());
assert!(MbType::IPCM.is_intra());
assert!(!MbType::P16x16.is_intra());
assert!(MbType::PSkip.is_skip());
assert!(!MbType::P16x16.is_skip());
}
}