M2.2/M2.3: CAVLC macroblock decoding, intra and inter prediction, DPB

- CAVLC residual parsing (coeff_token / total_zeros / run_before tables taken
  from the spec text, level decoding with escape codes), macroblock layer for
  I/P/B slices (mb_type, sub_mb_type, mb_pred, sub_mb_pred, me(v) cbp,
  mb_qp_delta), I_PCM
- intra prediction: Intra4x4 (9 modes), Intra8x8 with reference filtering,
  Intra16x16, chroma; constrained_intra_pred
- dequantisation with flat/custom scaling matrices; 4x4, 8x8, luma DC
  Hadamard and 2x2 chroma DC inverse transforms
- inter prediction: median / directional mv prediction, P_Skip, B spatial and
  temporal direct (direct_8x8_inference), B_Skip, 6-tap luma and bilinear
  chroma interpolation, explicit and implicit weighted prediction
- reference lists (init + modification), DPB with sliding window, all MMCOs,
  frame_num gaps, bumping / max_num_reorder_frames output order, IDR and
  no_output_of_prior_pics
- public Decoder/Picture API (Annex-B and avcC input, pts passes through
  reordering)

Deblocking (M2.5) follows in the next commit, so the bit-exact fixtures in
this commit are the no-deblock ones. CABAC slices return Unsupported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-09-30 06:14:18 -04:00
parent a2ac32916b
commit 9c8de5468b
14 changed files with 4137 additions and 2 deletions

View File

@ -0,0 +1,35 @@
//! Decode an Annex-B H.264 file to raw yuv420p: `h264dec in.h264 [out.yuv]`.
//! Prints timing information to stderr.
use filmcraft_h264::Decoder;
use std::io::Write;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("usage: h264dec in.h264 [out.yuv]");
std::process::exit(2);
}
let data = std::fs::read(&args[1]).expect("read input");
let mut out = args.get(2).map(|p| std::io::BufWriter::new(std::fs::File::create(p).expect("create output")));
let mut dec = Decoder::new();
let t0 = std::time::Instant::now();
let mut n = 0usize;
let mut write = |pics: Vec<filmcraft_h264::Picture>| {
for p in pics {
n += 1;
if let Some(o) = out.as_mut() {
o.write_all(&p.y).unwrap();
o.write_all(&p.u).unwrap();
o.write_all(&p.v).unwrap();
}
}
};
match dec.decode(&data, 0) {
Ok(p) => write(p),
Err(e) => eprintln!("decode error: {e}"),
}
write(dec.flush());
let dt = t0.elapsed().as_secs_f64();
eprintln!("{n} frames in {dt:.3}s ({:.1} fps)", n as f64 / dt);
}

281
crates/h264/src/cavlc.rs Normal file
View File

@ -0,0 +1,281 @@
//! CAVLC residual block parsing (9.2).
use crate::cavlc_tables::*;
use crate::error::{Result, ensure};
use filmcraft_bitstream::BitReader;
use std::sync::OnceLock;
/// Prefix-code lookup keyed by (number of leading zeros, next 5 bits after the first one bit).
pub(crate) struct VlcTable {
/// entry = (len << 8) | value, 0 = invalid.
entries: Box<[u16; 17 * 32]>,
}
impl VlcTable {
pub fn build(codes: &[(&str, u8)]) -> Self {
let mut entries = Box::new([0u16; 17 * 32]);
for &(code, value) in codes {
if code.is_empty() {
continue;
}
let len = code.len();
let lz = code.bytes().take_while(|&b| b == b'0').count();
let val = ((len as u16) << 8) | value as u16;
if lz == len {
// all-zero code: any continuation with at least `len` zeros
for l in lz..=16 {
for s in 0..32 {
entries[l * 32 + s] = val;
}
}
continue;
}
let suffix = &code[lz + 1..];
assert!(suffix.len() <= 5, "suffix too long in {code}");
let sbits = u32::from_str_radix(if suffix.is_empty() { "0" } else { suffix }, 2).unwrap();
let free = 5 - suffix.len();
for ext in 0..(1u32 << free) {
let s = (sbits << free) | ext;
entries[lz * 32 + s as usize] = val;
}
}
Self { entries }
}
#[inline]
pub fn decode(&self, r: &mut BitReader) -> Result<u8> {
let bits = r.peek(32);
let lz = bits.leading_zeros().min(16);
let s = if lz >= 16 { 0 } else { ((bits << lz) << 1) >> 27 };
let e = self.entries[(lz * 32 + s) as usize];
ensure!(e != 0, "invalid VLC code");
r.skip((e >> 8) as usize)?;
Ok(e as u8)
}
}
struct Tables {
/// coeff_token per nC class; value = (TrailingOnes << 5) | TotalCoeff
coeff_token: [VlcTable; 6],
total_zeros_4x4: [VlcTable; 15],
total_zeros_2x2: [VlcTable; 3],
total_zeros_2x4: [VlcTable; 7],
run_before: [VlcTable; 7],
}
fn tables() -> &'static Tables {
static T: OnceLock<Tables> = OnceLock::new();
T.get_or_init(|| {
let coeff_token = std::array::from_fn(|col| {
let codes: Vec<(&str, u8)> = COEFF_TOKEN.iter().map(|&(t1, tc, ref c)| (c[col], (t1 << 5) | tc)).collect();
VlcTable::build(&codes)
});
fn simple<const N: usize>(row: &[&'static str; N]) -> VlcTable {
let codes: Vec<(&str, u8)> = row.iter().enumerate().map(|(i, &c)| (c, i as u8)).collect();
VlcTable::build(&codes)
}
Tables {
coeff_token,
total_zeros_4x4: std::array::from_fn(|i| simple(&TOTAL_ZEROS_4X4[i])),
total_zeros_2x2: std::array::from_fn(|i| simple(&TOTAL_ZEROS_2X2[i])),
total_zeros_2x4: std::array::from_fn(|i| simple(&TOTAL_ZEROS_2X4[i])),
run_before: std::array::from_fn(|i| simple(&RUN_BEFORE[i])),
}
})
}
/// Force table construction (so the first slice does not pay for it).
pub(crate) fn init_tables() {
let _ = tables();
}
/// Parse residual_block_cavlc(). Coefficients are written to `coeff[start_idx..=end_idx]` in scan order
/// (entries outside the coded range are left untouched; the caller zeroes them).
/// `nc` is the nC value (-1 for 4:2:0 chroma DC, -2 for 4:2:2 chroma DC). Returns TotalCoeff.
pub(crate) fn residual_block(r: &mut BitReader, coeff: &mut [i32], start_idx: usize, end_idx: usize, max_num_coeff: usize, nc: i32) -> Result<u8> {
let t = tables();
let col = match nc {
-1 => 4,
-2 => 5,
0..=1 => 0,
2..=3 => 1,
4..=7 => 2,
_ => 3,
};
let tok = t.coeff_token[col].decode(r)?;
let total_coeff = (tok & 31) as usize;
let trailing_ones = (tok >> 5) as usize;
if total_coeff == 0 {
return Ok(0);
}
ensure!(total_coeff <= max_num_coeff, "TotalCoeff exceeds maxNumCoeff");
let mut level = [0i32; 16];
let mut suffix_length: u32 = if total_coeff > 10 && trailing_ones < 3 { 1 } else { 0 };
for (i, lv) in level.iter_mut().enumerate().take(total_coeff) {
if i < trailing_ones {
*lv = if r.read_bit()? { -1 } else { 1 };
continue;
}
// level_prefix
let bits = r.peek(32);
let level_prefix = bits.leading_zeros();
ensure!(level_prefix <= 25, "level_prefix too large");
r.skip(level_prefix as usize + 1)?;
let level_suffix_size = if level_prefix == 14 && suffix_length == 0 {
4
} else if level_prefix >= 15 {
level_prefix - 3
} else {
suffix_length
};
let level_suffix = if level_suffix_size > 0 { r.read_bits(level_suffix_size)? as i32 } else { 0 };
let mut level_code = ((level_prefix.min(15) as i32) << suffix_length) + level_suffix;
if level_prefix >= 15 && suffix_length == 0 {
level_code += 15;
}
if level_prefix >= 16 {
level_code += (1 << (level_prefix - 3)) - 4096;
}
if i == trailing_ones && trailing_ones < 3 {
level_code += 2;
}
*lv = if level_code % 2 == 0 { (level_code + 2) >> 1 } else { (-level_code - 1) >> 1 };
if suffix_length == 0 {
suffix_length = 1;
}
if lv.abs() > (3 << (suffix_length - 1)) && suffix_length < 6 {
suffix_length += 1;
}
}
let mut zeros_left = if total_coeff < end_idx - start_idx + 1 {
let tz_idx = total_coeff - 1;
let tz = match max_num_coeff {
4 => t.total_zeros_2x2[tz_idx].decode(r)?,
8 => t.total_zeros_2x4[tz_idx].decode(r)?,
_ => t.total_zeros_4x4[tz_idx].decode(r)?,
} as usize;
ensure!(tz + total_coeff <= end_idx - start_idx + 1, "total_zeros out of range");
tz
} else {
0
};
// runs and placement: highest-frequency coefficient first
let mut pos = start_idx + total_coeff + zeros_left - 1;
for (i, &lv) in level.iter().enumerate().take(total_coeff) {
coeff[pos] = lv;
if i + 1 == total_coeff {
break;
}
let run = if zeros_left > 0 {
let run = t.run_before[zeros_left.min(7) - 1].decode(r)? as usize;
ensure!(run <= zeros_left, "run_before out of range");
zeros_left -= run;
run
} else {
0
};
pos -= 1 + run;
}
Ok(total_coeff as u8)
}
#[cfg(test)]
mod tests {
use super::*;
use filmcraft_bitstream::BitWriter;
fn kraft(codes: &[&str]) -> f64 {
codes.iter().filter(|c| !c.is_empty()).map(|c| 0.5f64.powi(c.len() as i32)).sum()
}
fn prefix_free(codes: &[&str]) -> bool {
let c: Vec<&&str> = codes.iter().filter(|c| !c.is_empty()).collect();
for (i, a) in c.iter().enumerate() {
for (j, b) in c.iter().enumerate() {
if i != j && b.starts_with(**a) {
return false;
}
}
}
true
}
#[test]
fn coeff_token_tables_are_prefix_codes() {
for col in 0..6 {
let codes: Vec<&str> = COEFF_TOKEN.iter().map(|e| e.2[col]).collect();
assert!(prefix_free(&codes), "column {col}");
let k = kraft(&codes);
assert!(k <= 1.0 + 1e-12, "column {col} kraft {k}");
}
// nC>=8 is a complete 6-bit fixed code except the unused 000011 pattern
let codes: Vec<&str> = COEFF_TOKEN.iter().map(|e| e.2[3]).collect();
assert!(codes.iter().all(|c| c.len() == 6));
}
#[test]
fn total_zeros_and_run_before_are_complete_prefix_codes() {
for (i, row) in TOTAL_ZEROS_4X4.iter().enumerate() {
assert!(prefix_free(row));
// tzVlcIndex 1 leaves the all-zero 9-bit code unused; all others are complete
let k = kraft(row);
if i == 0 {
assert!((k - (1.0 - 1.0 / 512.0)).abs() < 1e-12, "{row:?}");
} else {
assert!((k - 1.0).abs() < 1e-12, "{row:?}");
}
}
for row in TOTAL_ZEROS_2X2.iter() {
assert!(prefix_free(row));
assert!((kraft(row) - 1.0).abs() < 1e-12);
}
for row in TOTAL_ZEROS_2X4.iter() {
assert!(prefix_free(row));
assert!((kraft(row) - 1.0).abs() < 1e-12);
}
for row in RUN_BEFORE.iter() {
assert!(prefix_free(row));
}
}
fn write_code(w: &mut BitWriter, code: &str) {
for b in code.bytes() {
w.write_bit(b == b'1');
}
}
#[test]
fn decodes_spec_style_example() {
// Block: 0 3 -1 0 | 0 -1 1 0 | 1 0 0 0 | 0 0 0 0 (zig-zag order), nC = 0.
// TotalCoeff 5, TrailingOnes 3: coeff_token 0000 100.
// trailing ones (reverse order): +1, -1, -1 -> sign bits 0 1 1
// level -1? No: remaining levels (reverse): 1 (idx 4?) ... use the classic example:
// coefficients in scan order: [0, 3, -1, 0, 0, -1, 1, 0, 1, 0, ...]
// reverse order non-zero: 1 (pos 8), 1 (pos 6), -1 (pos 5), -1 (pos 2), 3 (pos 1)
// TotalCoeff 5, TrailingOnes 3 (1, 1, -1).
let mut w = BitWriter::new();
write_code(&mut w, "0000100"); // coeff_token TC=5 T1=3, nC 0..2
write_code(&mut w, "0"); // +1
write_code(&mut w, "0"); // +1
write_code(&mut w, "1"); // -1
// level -1 with suffixLength 0: levelCode = 1 (T1 == 3 so no +2) -> prefix 1 => "01"
write_code(&mut w, "01");
// level 3 with suffixLength 1: levelCode = 4 -> prefix 2, suffix 0 => "001" "0"
write_code(&mut w, "0010");
// total_zeros = 4 (positions 0, 3, 4, 7) with tzVlcIndex 5 -> "110"
write_code(&mut w, "110");
// run_before: zerosLeft 4 at pos 8: run 1 -> "10"; zerosLeft 3 at pos 6: run 0 -> "11";
// zerosLeft 3 at pos 5: run 2 -> "01"; zerosLeft 1 at pos 2: run 0 -> "1"
write_code(&mut w, "10");
write_code(&mut w, "11");
write_code(&mut w, "01");
write_code(&mut w, "1");
w.rbsp_trailing();
let b = w.finish();
let mut r = BitReader::new(&b);
let mut c = [0i32; 16];
let tc = residual_block(&mut r, &mut c, 0, 15, 16, 0).unwrap();
assert_eq!(tc, 5);
assert_eq!(&c[..10], &[0, 3, -1, 0, 0, -1, 1, 0, 1, 0]);
}
}

View File

@ -0,0 +1,117 @@
// Generated from ITU-T H.264 (06/2026) Tables 9-5, 9-7..9-10 by a table-extraction script.
// Codes are bit strings; "" marks an unused entry.
/// (TrailingOnes, TotalCoeff, [nC 0..2, 2..4, 4..8, 8.., -1, -2])
#[rustfmt::skip]
pub(crate) static COEFF_TOKEN: [(u8, u8, [&str; 6]); 62] = [
(0, 0, ["1", "11", "1111", "000011", "01", "1"]),
(0, 1, ["000101", "001011", "001111", "000000", "000111", "0001111"]),
(1, 1, ["01", "10", "1110", "000001", "1", "01"]),
(0, 2, ["00000111", "000111", "001011", "000100", "000100", "0001110"]),
(1, 2, ["000100", "00111", "01111", "000101", "000110", "0001101"]),
(2, 2, ["001", "011", "1101", "000110", "001", "001"]),
(0, 3, ["000000111", "0000111", "001000", "001000", "000011", "000000111"]),
(1, 3, ["00000110", "001010", "01100", "001001", "0000011", "0001100"]),
(2, 3, ["0000101", "001001", "01110", "001010", "0000010", "0001011"]),
(3, 3, ["00011", "0101", "1100", "001011", "000101", "00001"]),
(0, 4, ["0000000111", "00000111", "0001111", "001100", "000010", "000000110"]),
(1, 4, ["000000110", "000110", "01010", "001101", "00000011", "000000101"]),
(2, 4, ["00000101", "000101", "01011", "001110", "00000010", "0001010"]),
(3, 4, ["000011", "0100", "1011", "001111", "0000000", "000001"]),
(0, 5, ["00000000111", "00000100", "0001011", "010000", "", "0000000111"]),
(1, 5, ["0000000110", "0000110", "01000", "010001", "", "0000000110"]),
(2, 5, ["000000101", "0000101", "01001", "010010", "", "000000100"]),
(3, 5, ["0000100", "00110", "1010", "010011", "", "0001001"]),
(0, 6, ["0000000001111", "000000111", "0001001", "010100", "", "00000000111"]),
(1, 6, ["00000000110", "00000110", "001110", "010101", "", "00000000110"]),
(2, 6, ["0000000101", "00000101", "001101", "010110", "", "0000000101"]),
(3, 6, ["00000100", "001000", "1001", "010111", "", "0001000"]),
(0, 7, ["0000000001011", "00000001111", "0001000", "011000", "", "000000000111"]),
(1, 7, ["0000000001110", "000000110", "001010", "011001", "", "000000000110"]),
(2, 7, ["00000000101", "000000101", "001001", "011010", "", "00000000101"]),
(3, 7, ["000000100", "000100", "1000", "011011", "", "0000000100"]),
(0, 8, ["0000000001000", "00000001011", "00001111", "011100", "", "0000000000111"]),
(1, 8, ["0000000001010", "00000001110", "0001110", "011101", "", "000000000101"]),
(2, 8, ["0000000001101", "00000001101", "0001101", "011110", "", "000000000100"]),
(3, 8, ["0000000100", "0000100", "01101", "011111", "", "00000000100"]),
(0, 9, ["00000000001111", "000000001111", "00001011", "100000", "", ""]),
(1, 9, ["00000000001110", "00000001010", "00001110", "100001", "", ""]),
(2, 9, ["0000000001001", "00000001001", "0001010", "100010", "", ""]),
(3, 9, ["00000000100", "000000100", "001100", "100011", "", ""]),
(0, 10, ["00000000001011", "000000001011", "000001111", "100100", "", ""]),
(1, 10, ["00000000001010", "000000001110", "00001010", "100101", "", ""]),
(2, 10, ["00000000001101", "000000001101", "00001101", "100110", "", ""]),
(3, 10, ["0000000001100", "00000001100", "0001100", "100111", "", ""]),
(0, 11, ["000000000001111", "000000001000", "000001011", "101000", "", ""]),
(1, 11, ["000000000001110", "000000001010", "000001110", "101001", "", ""]),
(2, 11, ["00000000001001", "000000001001", "00001001", "101010", "", ""]),
(3, 11, ["00000000001100", "00000001000", "00001100", "101011", "", ""]),
(0, 12, ["000000000001011", "0000000001111", "000001000", "101100", "", ""]),
(1, 12, ["000000000001010", "0000000001110", "000001010", "101101", "", ""]),
(2, 12, ["000000000001101", "0000000001101", "000001101", "101110", "", ""]),
(3, 12, ["00000000001000", "000000001100", "00001000", "101111", "", ""]),
(0, 13, ["0000000000001111", "0000000001011", "0000001101", "110000", "", ""]),
(1, 13, ["000000000000001", "0000000001010", "000000111", "110001", "", ""]),
(2, 13, ["000000000001001", "0000000001001", "000001001", "110010", "", ""]),
(3, 13, ["000000000001100", "0000000001100", "000001100", "110011", "", ""]),
(0, 14, ["0000000000001011", "0000000000111", "0000001001", "110100", "", ""]),
(1, 14, ["0000000000001110", "00000000001011", "0000001100", "110101", "", ""]),
(2, 14, ["0000000000001101", "0000000000110", "0000001011", "110110", "", ""]),
(3, 14, ["000000000001000", "0000000001000", "0000001010", "110111", "", ""]),
(0, 15, ["0000000000000111", "00000000001001", "0000000101", "111000", "", ""]),
(1, 15, ["0000000000001010", "00000000001000", "0000001000", "111001", "", ""]),
(2, 15, ["0000000000001001", "00000000001010", "0000000111", "111010", "", ""]),
(3, 15, ["0000000000001100", "0000000000001", "0000000110", "111011", "", ""]),
(0, 16, ["0000000000000100", "00000000000111", "0000000001", "111100", "", ""]),
(1, 16, ["0000000000000110", "00000000000110", "0000000100", "111101", "", ""]),
(2, 16, ["0000000000000101", "00000000000101", "0000000011", "111110", "", ""]),
(3, 16, ["0000000000001000", "00000000000100", "0000000010", "111111", "", ""]),
];
/// total_zeros for 4x4 blocks: [tzVlcIndex-1][total_zeros]
#[rustfmt::skip]
pub(crate) static TOTAL_ZEROS_4X4: [[&str; 16]; 15] = [
["1", "011", "010", "0011", "0010", "00011", "00010", "000011", "000010", "0000011", "0000010", "00000011", "00000010", "000000011", "000000010", "000000001"],
["111", "110", "101", "100", "011", "0101", "0100", "0011", "0010", "00011", "00010", "000011", "000010", "000001", "000000", ""],
["0101", "111", "110", "101", "0100", "0011", "100", "011", "0010", "00011", "00010", "000001", "00001", "000000", "", ""],
["00011", "111", "0101", "0100", "110", "101", "100", "0011", "011", "0010", "00010", "00001", "00000", "", "", ""],
["0101", "0100", "0011", "111", "110", "101", "100", "011", "0010", "00001", "0001", "00000", "", "", "", ""],
["000001", "00001", "111", "110", "101", "100", "011", "010", "0001", "001", "000000", "", "", "", "", ""],
["000001", "00001", "101", "100", "011", "11", "010", "0001", "001", "000000", "", "", "", "", "", ""],
["000001", "0001", "00001", "011", "11", "10", "010", "001", "000000", "", "", "", "", "", "", ""],
["000001", "000000", "0001", "11", "10", "001", "01", "00001", "", "", "", "", "", "", "", ""],
["00001", "00000", "001", "11", "10", "01", "0001", "", "", "", "", "", "", "", "", ""],
["0000", "0001", "001", "010", "1", "011", "", "", "", "", "", "", "", "", "", ""],
["0000", "0001", "01", "1", "001", "", "", "", "", "", "", "", "", "", "", ""],
["000", "001", "1", "01", "", "", "", "", "", "", "", "", "", "", "", ""],
["00", "01", "1", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["0", "1", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
];
/// total_zeros for chroma DC 2x2: [tzVlcIndex-1][total_zeros]
#[rustfmt::skip]
pub(crate) static TOTAL_ZEROS_2X2: [[&str; 4]; 3] = [
["1", "01", "001", "000"],
["1", "01", "00", ""],
["1", "0", "", ""],
];
/// total_zeros for chroma DC 2x4: [tzVlcIndex-1][total_zeros]
#[rustfmt::skip]
pub(crate) static TOTAL_ZEROS_2X4: [[&str; 8]; 7] = [
["1", "010", "011", "0010", "0011", "0001", "00001", "00000"],
["000", "01", "001", "100", "101", "110", "111", ""],
["000", "001", "01", "10", "110", "111", "", ""],
["110", "00", "01", "10", "111", "", "", ""],
["00", "01", "10", "11", "", "", "", ""],
["00", "01", "1", "", "", "", "", ""],
["0", "1", "", "", "", "", "", ""],
];
/// run_before: [min(zerosLeft,7)-1][run_before]
#[rustfmt::skip]
pub(crate) static RUN_BEFORE: [[&str; 15]; 7] = [
["1", "0", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["1", "01", "00", "", "", "", "", "", "", "", "", "", "", "", ""],
["11", "10", "01", "00", "", "", "", "", "", "", "", "", "", "", ""],
["11", "10", "01", "001", "000", "", "", "", "", "", "", "", "", "", ""],
["11", "10", "011", "010", "001", "000", "", "", "", "", "", "", "", "", ""],
["11", "000", "001", "011", "010", "101", "100", "", "", "", "", "", "", "", ""],
["111", "110", "101", "100", "011", "010", "001", "0001", "00001", "000001", "0000001", "00000001", "000000001", "0000000001", "00000000001"],
];

412
crates/h264/src/decoder.rs Normal file
View File

@ -0,0 +1,412 @@
//! Top-level decoder: NAL dispatch, picture boundaries, POC, DPB, output.
use crate::dpb::{Dpb, Output, OutputMeta};
use crate::error::{Error, Result, ensure, unsupported};
use crate::params::{Pps, Sps};
use crate::picture::{Frame, MotionField, Planes};
use crate::slice::{NalHeader, Poc, PocState, SliceHeader, nal_type};
use crate::slicedec::{PicState, SliceDecoder};
use crate::transform::LevelScale;
use crate::{ColorInfo, Picture};
use filmcraft_bitstream::{BitReader, annexb_nals, length_prefixed_nals, unescape_rbsp};
use std::sync::Arc;
struct CurPic {
pic: PicState,
first: SliceHeader,
sps: Arc<Sps>,
poc: Poc,
pts: i64,
key: bool,
has_mmco5: bool,
}
/// H.264 decoder.
pub struct Decoder {
spss: Vec<Option<Arc<Sps>>>,
ppss: Vec<Option<Arc<Pps>>>,
nal_length_size: Option<usize>,
dpb: Dpb,
poc_state: PocState,
prev_ref_frame_num: u32,
cur: Option<CurPic>,
next_id: u32,
active_sps: Option<Arc<Sps>>,
ls_cache: Vec<(Arc<Pps>, Arc<LevelScale>)>,
out: Vec<Picture>,
}
impl Default for Decoder {
fn default() -> Self {
Self::new()
}
}
/// Is `sh` the first slice of a new primary coded picture relative to `prev` (7.4.1.2.4)?
fn is_new_picture(prev: &SliceHeader, sh: &SliceHeader, sps: &Sps) -> bool {
if sh.frame_num != prev.frame_num
|| sh.pps_id != prev.pps_id
|| sh.field_pic != prev.field_pic
|| sh.bottom_field != prev.bottom_field
|| (sh.nal_ref_idc == 0) != (prev.nal_ref_idc == 0)
|| sh.idr != prev.idr
|| (sh.idr && prev.idr && sh.idr_pic_id != prev.idr_pic_id)
{
return true;
}
if sps.pic_order_cnt_type == 0 && (sh.pic_order_cnt_lsb != prev.pic_order_cnt_lsb || sh.delta_pic_order_cnt_bottom != prev.delta_pic_order_cnt_bottom) {
return true;
}
if sps.pic_order_cnt_type == 1 && sh.delta_pic_order_cnt != prev.delta_pic_order_cnt {
return true;
}
false
}
impl Decoder {
pub fn new() -> Self {
crate::cavlc::init_tables();
Decoder {
spss: vec![None; 32],
ppss: vec![None; 256],
nal_length_size: None,
dpb: Dpb::new(),
poc_state: PocState::default(),
prev_ref_frame_num: 0,
cur: None,
next_id: 1,
active_sps: None,
ls_cache: Vec::new(),
out: Vec::new(),
}
}
/// Configure from an `avcC` (AVCDecoderConfigurationRecord) box payload.
pub fn from_avcc(avcc: &[u8]) -> Result<Self> {
let mut d = Decoder::new();
ensure!(avcc.len() >= 7, "avcC too short");
ensure!(avcc[0] == 1, "unsupported avcC version {}", avcc[0]);
d.nal_length_size = Some((avcc[4] & 3) as usize + 1);
let mut pos = 5;
let read_sets = |pos: &mut usize, count: usize, d: &mut Decoder| -> Result<()> {
for _ in 0..count {
ensure!(*pos + 2 <= avcc.len(), "avcC truncated");
let len = u16::from_be_bytes([avcc[*pos], avcc[*pos + 1]]) as usize;
*pos += 2;
ensure!(*pos + len <= avcc.len(), "avcC truncated");
d.handle_nal(&avcc[*pos..*pos + len], 0)?;
*pos += len;
}
Ok(())
};
let nsps = (avcc[pos] & 0x1f) as usize;
pos += 1;
read_sets(&mut pos, nsps, &mut d)?;
ensure!(pos < avcc.len(), "avcC truncated");
let npps = avcc[pos] as usize;
pos += 1;
read_sets(&mut pos, npps, &mut d)?;
Ok(d)
}
/// Length of NAL length prefixes (from avcC), or None for Annex-B input.
pub fn nal_length_size(&self) -> Option<usize> {
self.nal_length_size
}
/// Decode one access unit. Returns pictures that became ready for output, in output order.
pub fn decode(&mut self, data: &[u8], pts: i64) -> Result<Vec<Picture>> {
let nals = match self.nal_length_size {
Some(n) => length_prefixed_nals(data, n)?,
None => annexb_nals(data),
};
let mut result = Ok(());
for nal in nals {
if let Err(e) = self.handle_nal(nal, pts) {
result = Err(e);
break;
}
}
// Finish a complete picture at the end of the access unit.
if self.cur.as_ref().is_some_and(|c| c.pic.decoded_mbs >= c.pic.mbs.len()) {
self.finish_picture();
}
result?;
Ok(std::mem::take(&mut self.out))
}
/// Output all remaining pictures.
pub fn flush(&mut self) -> Vec<Picture> {
if self.cur.is_some() {
self.finish_picture();
}
let mut outs = Vec::new();
self.dpb.flush(&mut outs);
self.emit(outs);
std::mem::take(&mut self.out)
}
fn handle_nal(&mut self, nal: &[u8], pts: i64) -> Result<()> {
if nal.is_empty() {
return Ok(());
}
let hdr = NalHeader::parse(nal[0])?;
match hdr.nal_unit_type {
nal_type::SPS => {
let sps = Sps::parse(&unescape_rbsp(&nal[1..]))?;
let id = sps.id as usize;
self.spss[id] = Some(Arc::new(sps));
}
nal_type::PPS => {
let rbsp = unescape_rbsp(&nal[1..]);
let pps = Pps::parse(&rbsp, &self.spss_plain())?;
let id = pps.id as usize;
self.ppss[id] = Some(Arc::new(pps));
}
nal_type::SLICE | nal_type::IDR => self.handle_slice(nal, hdr, pts)?,
nal_type::SLICE_DPA | nal_type::SLICE_DPB | nal_type::SLICE_DPC => {
return unsupported("data partitioning (Extended profile)");
}
nal_type::END_SEQ | nal_type::END_STREAM if self.cur.is_some() => self.finish_picture(),
// SEI, AUD, filler, SPS extension, prefix NAL, subset SPS, auxiliary and extension slices: skipped.
_ => {}
}
Ok(())
}
fn spss_plain(&self) -> Vec<Option<Sps>> {
self.spss.iter().map(|s| s.as_ref().map(|s| (**s).clone())).collect()
}
fn level_scale(&mut self, pps: &Arc<Pps>) -> Arc<LevelScale> {
if let Some((_, ls)) = self.ls_cache.iter().find(|(p, _)| Arc::ptr_eq(p, pps)) {
return ls.clone();
}
let ls: Arc<LevelScale> = Arc::from(LevelScale::new(&pps.scaling));
self.ls_cache.retain(|(p, _)| self.ppss.iter().flatten().any(|q| Arc::ptr_eq(p, q)));
self.ls_cache.push((pps.clone(), ls.clone()));
ls
}
fn handle_slice(&mut self, nal: &[u8], hdr: NalHeader, pts: i64) -> Result<()> {
let rbsp = unescape_rbsp(&nal[1..]);
let ppss = &self.ppss;
let spss = &self.spss;
let mut found: Option<(Arc<Pps>, Arc<Sps>)> = None;
let (sh, _, _) = SliceHeader::parse(&rbsp, hdr, |id| {
let pps = ppss[id as usize].as_ref().ok_or_else(|| Error::MissingParameterSet(format!("PPS {id}")))?;
let sps = spss[pps.sps_id as usize].as_ref().ok_or_else(|| Error::MissingParameterSet(format!("SPS {}", pps.sps_id)))?;
found = Some((pps.clone(), sps.clone()));
Ok((&**pps, &**sps))
})?;
let (pps, sps) = found.expect("lookup succeeded");
sps.check_supported()?;
if pps.num_slice_groups > 1 {
return unsupported("slice groups (FMO)");
}
if sh.field_pic {
return unsupported("field pictures");
}
if matches!(sh.slice_type, crate::slice::SliceType::Sp | crate::slice::SliceType::Si) {
return unsupported("SP/SI slices");
}
if sh.redundant_pic_cnt > 0 {
return Ok(()); // redundant slices are ignored
}
let new_pic = match &self.cur {
None => true,
Some(c) => is_new_picture(&c.first, &sh, &sps) || (sh.first_mb_in_slice == 0 && c.pic.decoded_mbs > 0) || !Arc::ptr_eq(&c.sps, &sps),
};
if new_pic {
if self.cur.is_some() {
self.finish_picture();
}
self.start_picture(&sh, &sps, pts)?;
}
let ls = self.level_scale(&pps);
let cur = self.cur.as_mut().expect("picture started");
if sh.has_mmco5() {
cur.has_mmco5 = true;
}
let refs = self.dpb.build_ref_lists(&sh, cur.poc.frame(), sps.max_frame_num())?;
if !sh.slice_type.is_intra() {
ensure!(!refs[0].is_empty(), "no reference pictures available for inter slice");
if sh.slice_type.is_b() {
ensure!(!refs[1].is_empty(), "empty RefPicList1 in B slice");
}
}
let mut sd = SliceDecoder::new(&sh, &pps, &sps, &mut cur.pic, &refs, &ls)?;
if pps.entropy_coding_mode {
return unsupported("CABAC entropy coding");
} else {
let mut r = BitReader::new(&rbsp);
r.seek_bits(sh.header_bits);
sd.decode_cavlc(&mut r)?;
}
Ok(())
}
fn start_picture(&mut self, sh: &SliceHeader, sps: &Arc<Sps>, pts: i64) -> Result<()> {
// Activate SPS; a resolution change flushes the DPB.
let changed = match &self.active_sps {
None => true,
Some(a) => a.width() != sps.width() || a.height() != sps.height() || a.max_dpb_frames() != sps.max_dpb_frames(),
};
if changed && self.active_sps.is_some() {
let mut outs = Vec::new();
self.dpb.flush(&mut outs);
self.emit(outs);
self.dpb.entries.clear();
}
self.active_sps = Some(sps.clone());
self.dpb.capacity = sps.max_dpb_frames();
self.dpb.max_reorder = sps.max_num_reorder_frames().min(self.dpb.capacity);
let mb_w = sps.pic_width_in_mbs as usize;
let mb_h = sps.frame_height_in_mbs() as usize;
if sh.idr {
let mut outs = Vec::new();
self.dpb.idr(sh.no_output_of_prior_pics, &mut outs);
self.emit(outs);
self.prev_ref_frame_num = 0;
} else {
if self.dpb.entries.iter().all(|e| e.mark == crate::dpb::RefMark::Unused) && !sh.slice_type.is_intra() {
// Stream starts without an IDR: nothing to reference; decoding will conceal.
}
let max = sps.max_frame_num();
if sh.frame_num != self.prev_ref_frame_num && sh.frame_num != (self.prev_ref_frame_num + 1) % max {
// frame_num gap (8.2.5.2)
let (w, h) = (mb_w * 16, mb_h * 16);
let meta = Arc::new(output_meta(sps, pts, false));
let next_id = &mut self.next_id;
let poc_state = &mut self.poc_state;
let prev_last = self.dpb.entries.iter().filter(|e| !e.non_existing).max_by_key(|e| e.frame.id).map(|e| e.frame.clone());
let mut make = |_fnum: u32| {
let id = *next_id;
*next_id += 1;
let planes = match &prev_last {
Some(f) => f.planes.clone(),
None => Planes::gray(w, h),
};
let frame = Arc::new(Frame { id, poc: 0, planes, motion: MotionField::default() });
(frame, meta.clone())
};
let mut upd = |fnum: u32| poc_state.update_gap_frame(fnum, sps);
self.dpb.fill_frame_num_gap(self.prev_ref_frame_num, sh.frame_num, max, sps.max_num_ref_frames as usize, &mut make, &mut upd);
self.prev_ref_frame_num = (sh.frame_num + max - 1) % max;
}
}
let poc = self.poc_state.compute(sh, sps);
let pic = PicState::new(mb_w, mb_h, poc.frame());
self.cur = Some(CurPic { pic, first: sh.clone(), sps: sps.clone(), poc, pts, key: sh.idr, has_mmco5: false });
Ok(())
}
}
fn output_meta(sps: &Sps, pts: i64, key: bool) -> OutputMeta {
{
let vui = sps.vui.clone().unwrap_or_default();
OutputMeta {
pts,
key,
crop: sps.crop_rect(),
full_range: vui.full_range,
colour_primaries: vui.colour_primaries,
transfer_characteristics: vui.transfer_characteristics,
matrix_coefficients: vui.matrix_coefficients,
sar: vui.sar,
}
}
}
impl Decoder {
fn finish_picture(&mut self) {
let Some(cur) = self.cur.take() else { return };
// motion field for co-located use
let n = cur.pic.mbs.len();
let mut motion = MotionField {
mv: [vec![[0; 2]; n * 16], vec![[0; 2]; n * 16]],
ref_idx: [vec![-1; n * 4], vec![-1; n * 4]],
ref_id: [vec![u32::MAX; n * 4], vec![u32::MAX; n * 4]],
intra: vec![true; n],
};
for (i, st) in cur.pic.mbs.iter().enumerate() {
if st.slice_num == u32::MAX {
continue;
}
motion.intra[i] = st.kind.is_intra();
let sl = &cur.pic.slices[st.slice_num as usize];
for l in 0..2 {
motion.mv[l][i * 16..i * 16 + 16].copy_from_slice(&st.mv[l]);
for b in 0..4 {
let r = st.ref_idx[l][b];
motion.ref_idx[l][i * 4 + b] = r;
if r >= 0 {
motion.ref_id[l][i * 4 + b] = sl.ref_ids[l].get(r as usize).copied().unwrap_or(u32::MAX);
}
}
}
}
let mut poc = cur.poc.frame();
let sh = cur.first.clone();
if cur.has_mmco5 {
// tempPicOrderCnt adjustment (8.2.1): the picture's POC becomes relative to itself.
poc -= cur.poc.frame();
}
let id = self.next_id;
self.next_id += 1;
let frame = Arc::new(Frame { id, poc, planes: cur.pic.planes, motion });
let meta = Arc::new(output_meta(&cur.sps, cur.pts, cur.key));
let sh_eff = sh.clone();
self.poc_state.update(&sh_eff, &cur.poc);
if sh.nal_ref_idc != 0 {
self.prev_ref_frame_num = if cur.has_mmco5 { 0 } else { sh.frame_num };
}
let mut outs = Vec::new();
self.dpb.store_picture(&sh_eff, frame, poc, cur.sps.max_frame_num(), cur.sps.max_num_ref_frames as usize, meta, &mut outs);
self.emit(outs);
}
fn emit(&mut self, outs: Vec<Output>) {
for o in outs {
self.out.push(make_picture(&o));
}
}
}
fn make_picture(o: &Output) -> Picture {
let p = &o.frame.planes;
let (cx, cy, cw, ch) = o.meta.crop;
let (cx, cy, cw, ch) = (cx as usize, cy as usize, cw as usize, ch as usize);
let mut y = Vec::with_capacity(cw * ch);
for r in cy..cy + ch {
y.extend_from_slice(&p.y[r * p.width + cx..r * p.width + cx + cw]);
}
let (ccx, ccy, ccw, cch) = (cx / 2, cy / 2, cw.div_ceil(2), ch.div_ceil(2));
let mut u = Vec::with_capacity(ccw * cch);
let mut v = Vec::with_capacity(ccw * cch);
for r in ccy..ccy + cch {
u.extend_from_slice(&p.cb[r * p.cwidth + ccx..r * p.cwidth + ccx + ccw]);
v.extend_from_slice(&p.cr[r * p.cwidth + ccx..r * p.cwidth + ccx + ccw]);
}
Picture {
width: cw as u32,
height: ch as u32,
chroma_width: ccw as u32,
chroma_height: cch as u32,
y,
u,
v,
y_stride: cw,
uv_stride: ccw,
pts: o.meta.pts,
poc: o.poc,
key: o.meta.key,
color: ColorInfo {
full_range: o.meta.full_range,
primaries: o.meta.colour_primaries,
transfer: o.meta.transfer_characteristics,
matrix: o.meta.matrix_coefficients,
},
sar: o.meta.sar,
}
}

409
crates/h264/src/dpb.rs Normal file
View File

@ -0,0 +1,409 @@
//! Decoded picture buffer: reference marking (8.2.5), reference list construction (8.2.4) and output
//! ordering / bumping (C.4).
use crate::error::{Result, ensure};
use crate::picture::{FrameRef, RefPic};
use crate::slice::{Mmco, SliceHeader, SliceType};
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RefMark {
Unused,
Short,
Long,
}
/// Output-related metadata attached to a decoded frame.
#[derive(Clone, Debug)]
pub struct OutputMeta {
pub pts: i64,
pub key: bool,
pub crop: (u32, u32, u32, u32),
pub full_range: bool,
pub colour_primaries: u8,
pub transfer_characteristics: u8,
pub matrix_coefficients: u8,
pub sar: (u16, u16),
}
pub struct DpbEntry {
pub frame: FrameRef,
pub poc: i32,
pub frame_num: u32,
pub long_term_frame_idx: u32,
pub mark: RefMark,
pub needed_for_output: bool,
pub non_existing: bool,
pub meta: Arc<OutputMeta>,
}
pub struct Dpb {
pub entries: Vec<DpbEntry>,
/// None = "no long-term frame indices".
pub max_long_term_frame_idx: Option<u32>,
pub capacity: usize,
pub max_reorder: usize,
}
/// A picture leaving the DPB for output.
pub struct Output {
pub frame: FrameRef,
pub poc: i32,
pub meta: Arc<OutputMeta>,
}
impl Dpb {
pub fn new() -> Self {
Dpb { entries: Vec::new(), max_long_term_frame_idx: None, capacity: 16, max_reorder: 16 }
}
fn frame_num_wrap(e: &DpbEntry, cur_frame_num: u32, max_frame_num: u32) -> i32 {
if e.frame_num > cur_frame_num { e.frame_num as i32 - max_frame_num as i32 } else { e.frame_num as i32 }
}
fn num_refs(&self) -> usize {
self.entries.iter().filter(|e| e.mark != RefMark::Unused).count()
}
/// Remove entries not needed for output and not used for reference.
fn prune(&mut self) {
self.entries.retain(|e| e.needed_for_output || e.mark != RefMark::Unused);
}
/// Output the picture with the smallest POC (C.4.5.3 "bumping"). Returns false if none waits.
fn bump(&mut self, out: &mut Vec<Output>) -> bool {
let Some(i) = self.entries.iter().enumerate().filter(|(_, e)| e.needed_for_output).min_by_key(|(_, e)| e.poc).map(|(i, _)| i) else {
return false;
};
let e = &mut self.entries[i];
e.needed_for_output = false;
out.push(Output { frame: e.frame.clone(), poc: e.poc, meta: e.meta.clone() });
self.prune();
true
}
/// Output everything in POC order and clear non-reference entries.
pub fn flush(&mut self, out: &mut Vec<Output>) {
while self.bump(out) {}
self.prune();
}
/// IDR: mark all references unused; output (or drop) prior pictures.
pub fn idr(&mut self, no_output_of_prior_pics: bool, out: &mut Vec<Output>) {
for e in &mut self.entries {
e.mark = RefMark::Unused;
}
if no_output_of_prior_pics {
self.entries.clear();
} else {
self.flush(out);
}
self.max_long_term_frame_idx = None;
}
/// Sliding window marking (8.2.5.3).
fn sliding_window(&mut self, cur_frame_num: u32, max_frame_num: u32, max_num_ref_frames: usize) {
let limit = max_num_ref_frames.max(1);
while self.num_refs() >= limit {
let victim = self
.entries
.iter()
.enumerate()
.filter(|(_, e)| e.mark == RefMark::Short)
.min_by_key(|(_, e)| Self::frame_num_wrap(e, cur_frame_num, max_frame_num))
.map(|(i, _)| i);
match victim {
Some(i) => self.entries[i].mark = RefMark::Unused,
None => break,
}
}
self.prune();
}
/// Insert "non-existing" frames for a frame_num gap (8.2.5.2).
#[allow(clippy::too_many_arguments)]
pub fn fill_frame_num_gap(
&mut self,
prev_ref_frame_num: u32,
frame_num: u32,
max_frame_num: u32,
max_num_ref_frames: usize,
make_frame: &mut dyn FnMut(u32) -> (FrameRef, Arc<OutputMeta>),
poc_state: &mut dyn FnMut(u32),
) {
let mut unused = (prev_ref_frame_num + 1) % max_frame_num;
let mut guard = 0;
while unused != frame_num && guard < max_frame_num {
self.sliding_window(unused, max_frame_num, max_num_ref_frames);
let (frame, meta) = make_frame(unused);
poc_state(unused);
self.entries.push(DpbEntry {
poc: frame.poc,
frame,
frame_num: unused,
long_term_frame_idx: 0,
mark: RefMark::Short,
needed_for_output: false,
non_existing: true,
meta,
});
unused = (unused + 1) % max_frame_num;
guard += 1;
}
}
/// Initial reference picture lists (8.2.4.2) + modification (8.2.4.3) for a frame slice.
pub fn build_ref_lists(&self, sh: &SliceHeader, cur_poc: i32, max_frame_num: u32) -> Result<[Vec<RefPic>; 2]> {
let mut lists: [Vec<RefPic>; 2] = [Vec::new(), Vec::new()];
if sh.slice_type.is_intra() {
return Ok(lists);
}
let cur_fn = sh.frame_num;
let pic_num = |e: &DpbEntry| Self::frame_num_wrap(e, cur_fn, max_frame_num);
let short: Vec<&DpbEntry> = self.entries.iter().filter(|e| e.mark == RefMark::Short).collect();
let mut long: Vec<&DpbEntry> = self.entries.iter().filter(|e| e.mark == RefMark::Long).collect();
long.sort_by_key(|e| e.long_term_frame_idx);
let to_ref = |e: &&DpbEntry| RefPic { frame: e.frame.clone(), long_term: e.mark == RefMark::Long };
let mut entry_lists: [Vec<&DpbEntry>; 2] = [Vec::new(), Vec::new()];
if sh.slice_type != SliceType::B {
let mut s = short.clone();
s.sort_by_key(|e| std::cmp::Reverse(pic_num(e)));
entry_lists[0] = s.into_iter().chain(long.iter().copied()).collect();
} else {
let mut before: Vec<&DpbEntry> = short.iter().copied().filter(|e| e.poc < cur_poc).collect();
let mut after: Vec<&DpbEntry> = short.iter().copied().filter(|e| e.poc > cur_poc).collect();
before.sort_by_key(|e| std::cmp::Reverse(e.poc));
after.sort_by_key(|e| e.poc);
entry_lists[0] = before.iter().chain(after.iter()).chain(long.iter()).copied().collect();
entry_lists[1] = after.iter().chain(before.iter()).chain(long.iter()).copied().collect();
if entry_lists[1].len() > 1 && entry_lists[0].len() == entry_lists[1].len() {
let same = entry_lists[0].iter().zip(entry_lists[1].iter()).all(|(a, b)| std::ptr::eq(*a, *b));
if same {
entry_lists[1].swap(0, 1);
}
}
}
let nlists = if sh.slice_type == SliceType::B { 2 } else { 1 };
for l in 0..nlists {
let n = sh.num_ref_idx_active[l] as usize;
let mut list: Vec<Option<&DpbEntry>> = entry_lists[l].iter().copied().map(Some).collect();
list.truncate(n);
list.resize(n, None);
if !sh.ref_pic_list_mod[l].is_empty() {
let max_pic_num = max_frame_num as i32;
let curr_pic_num = cur_fn as i32;
let mut pred = curr_pic_num;
let mut ref_idx = 0usize;
for m in &sh.ref_pic_list_mod[l] {
ensure!(ref_idx < n, "too many ref_pic_list_modification entries");
list.push(None); // temporarily one longer
let (target, is_long): (Option<&DpbEntry>, bool) = match m.idc {
0 | 1 => {
let d = m.value as i32 + 1;
let no_wrap = if m.idc == 0 {
if pred - d < 0 { pred - d + max_pic_num } else { pred - d }
} else if pred + d >= max_pic_num {
pred + d - max_pic_num
} else {
pred + d
};
pred = no_wrap;
let pn = if no_wrap > curr_pic_num { no_wrap - max_pic_num } else { no_wrap };
(short.iter().copied().find(|e| pic_num(e) == pn), false)
}
2 => (long.iter().copied().find(|e| e.long_term_frame_idx == m.value), true),
_ => continue,
};
let Some(target) = target else {
// Missing picture: keep the list unchanged (error concealment).
list.pop();
ref_idx += 1;
continue;
};
for c in (ref_idx + 1..=n).rev() {
list[c] = list[c - 1];
}
list[ref_idx] = Some(target);
ref_idx += 1;
let mut nidx = ref_idx;
for c in ref_idx..=n {
let keep = match list[c] {
None => true,
Some(e) => {
if is_long {
!(e.mark == RefMark::Long && e.long_term_frame_idx == target.long_term_frame_idx)
} else {
!(e.mark == RefMark::Short && std::ptr::eq(e, target))
}
}
};
if keep {
list[nidx] = list[c];
nidx += 1;
}
}
list.truncate(n);
}
}
// Fill missing entries (should not happen in conforming streams) with the first valid entry.
let fallback = list.iter().flatten().next().copied().or_else(|| self.entries.last());
let out: Vec<RefPic> = list.iter().filter_map(|e| e.or(fallback)).map(|e| to_ref(&e)).collect();
lists[l] = out;
}
Ok(lists)
}
/// Adaptive memory control (8.2.5.4). Returns true if MMCO 5 was executed.
fn apply_mmcos(&mut self, mmcos: &[Mmco], cur_frame_num: u32, max_frame_num: u32, cur_long: &mut Option<u32>) -> bool {
let mut had5 = false;
let curr_pic_num = cur_frame_num as i32;
for m in mmcos {
match m.op {
1 => {
let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1);
for e in &mut self.entries {
if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn {
e.mark = RefMark::Unused;
}
}
}
2 => {
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_pic_num {
e.mark = RefMark::Unused;
}
}
}
3 => {
let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1);
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx {
e.mark = RefMark::Unused;
}
}
for e in &mut self.entries {
if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn {
e.mark = RefMark::Long;
e.long_term_frame_idx = m.long_term_frame_idx;
}
}
}
4 => {
let max = m.max_long_term_frame_idx_plus1;
for e in &mut self.entries {
if e.mark == RefMark::Long && (max == 0 || e.long_term_frame_idx > max - 1) {
e.mark = RefMark::Unused;
}
}
self.max_long_term_frame_idx = if max == 0 { None } else { Some(max - 1) };
}
5 => {
for e in &mut self.entries {
e.mark = RefMark::Unused;
}
self.max_long_term_frame_idx = None;
had5 = true;
}
6 => {
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx {
e.mark = RefMark::Unused;
}
}
*cur_long = Some(m.long_term_frame_idx);
}
_ => {}
}
}
self.prune();
had5
}
/// Reference marking for the just-decoded picture and insertion into the DPB with output bumping.
/// Returns pictures ready for output.
#[allow(clippy::too_many_arguments)]
pub fn store_picture(
&mut self,
sh: &SliceHeader,
frame: FrameRef,
poc: i32,
max_frame_num: u32,
max_num_ref_frames: usize,
meta: Arc<OutputMeta>,
out: &mut Vec<Output>,
) {
let mut mark = RefMark::Unused;
let mut long_idx = 0;
let mut had5 = false;
if sh.nal_ref_idc != 0 {
if sh.idr {
if sh.long_term_reference {
mark = RefMark::Long;
self.max_long_term_frame_idx = Some(0);
} else {
mark = RefMark::Short;
self.max_long_term_frame_idx = None;
}
} else {
let mut cur_long = None;
if sh.adaptive_ref_pic_marking {
had5 = self.apply_mmcos(&sh.mmcos, sh.frame_num, max_frame_num, &mut cur_long);
} else {
self.sliding_window(sh.frame_num, max_frame_num, max_num_ref_frames);
}
if let Some(i) = cur_long {
mark = RefMark::Long;
long_idx = i;
} else {
mark = RefMark::Short;
// make room if MMCOs left the DPB over-full (non-conforming streams)
if self.num_refs() >= max_num_ref_frames.max(1) && sh.adaptive_ref_pic_marking {
self.sliding_window(sh.frame_num, max_frame_num, max_num_ref_frames);
}
}
}
}
if had5 {
// All earlier pictures are output before the current one (C.4.4).
self.flush(out);
}
let frame_num = if had5 { 0 } else { sh.frame_num };
// Bumping until there is space (C.4.5.1 / C.4.5.2).
if mark == RefMark::Unused {
loop {
let waiting = self.entries.len();
if waiting < self.capacity {
break;
}
// non-reference picture with the smallest POC is output directly
let min_poc = self.entries.iter().filter(|e| e.needed_for_output).map(|e| e.poc).min();
if min_poc.is_none_or(|m| poc < m) {
out.push(Output { frame, poc, meta });
return;
}
if !self.bump(out) {
break;
}
}
} else {
while self.entries.len() >= self.capacity {
if !self.bump(out) {
break;
}
}
}
self.entries.push(DpbEntry { frame, poc, frame_num, long_term_frame_idx: long_idx, mark, needed_for_output: true, non_existing: false, meta });
// Low-delay output: honour max_num_reorder_frames.
while self.entries.iter().filter(|e| e.needed_for_output).count() > self.max_reorder {
if !self.bump(out) {
break;
}
}
}
}
impl Default for Dpb {
fn default() -> Self {
Self::new()
}
}

303
crates/h264/src/inter.rs Normal file
View File

@ -0,0 +1,303 @@
//! Inter prediction sample interpolation (8.4.2.2) and weighted sample prediction (8.4.2.3).
/// A reference plane.
#[derive(Clone, Copy)]
pub struct PlaneRef<'a> {
pub data: &'a [u8],
pub width: usize,
pub height: usize,
pub stride: usize,
}
const WIN: usize = 16 + 5;
/// Copy the (bw+5)x(bh+5) window whose top-left is (x-2, y-2) into `win` (stride WIN), clamping
/// coordinates to the picture.
#[inline]
fn fetch_window(p: PlaneRef, x: i32, y: i32, bw: usize, bh: usize, win: &mut [u8; WIN * WIN]) {
let x0 = x - 2;
let y0 = y - 2;
let ww = bw + 5;
let wh = bh + 5;
let maxx = p.width as i32 - 1;
let maxy = p.height as i32 - 1;
if x0 >= 0 && y0 >= 0 && x0 + ww as i32 - 1 <= maxx && y0 + wh as i32 - 1 <= maxy {
for r in 0..wh {
let src = (y0 as usize + r) * p.stride + x0 as usize;
win[r * WIN..r * WIN + ww].copy_from_slice(&p.data[src..src + ww]);
}
} else {
for r in 0..wh {
let yy = (y0 + r as i32).clamp(0, maxy) as usize;
let row = &p.data[yy * p.stride..yy * p.stride + p.width];
for c in 0..ww {
let xx = (x0 + c as i32).clamp(0, maxx) as usize;
win[r * WIN + c] = row[xx];
}
}
}
}
#[inline(always)]
fn tap6(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32) -> i32 {
a - 5 * b + 20 * c + 20 * d - 5 * e + f
}
#[inline(always)]
fn clip1(v: i32) -> u8 {
v.clamp(0, 255) as u8
}
/// Luma sample interpolation for a bw x bh block. (x, y) is the integer sample position
/// (block position + (mv >> 2)); (fx, fy) the quarter-sample fraction. Output stride is `os`.
#[allow(clippy::too_many_arguments)]
pub fn mc_luma(p: PlaneRef, x: i32, y: i32, fx: u32, fy: u32, bw: usize, bh: usize, out: &mut [u8], os: usize) {
let mut win = [0u8; WIN * WIN];
fetch_window(p, x, y, bw, bh, &mut win);
// sample G at (r, c) in block coordinates is win[(r + 2) * WIN + c + 2]
let g = |r: usize, c: usize| win[(r + 2) * WIN + c + 2] as i32;
// unclipped horizontal half-sample b1 at block row r (-2..bh+3 via offset) and column c
let b1 = |r: isize, c: usize| {
let base = ((r + 2) as usize) * WIN + c;
tap6(win[base] as i32, win[base + 1] as i32, win[base + 2] as i32, win[base + 3] as i32, win[base + 4] as i32, win[base + 5] as i32)
};
let h1 = |r: usize, c: isize| {
let col = (c + 2) as usize;
tap6(
win[r * WIN + col] as i32,
win[(r + 1) * WIN + col] as i32,
win[(r + 2) * WIN + col] as i32,
win[(r + 3) * WIN + col] as i32,
win[(r + 4) * WIN + col] as i32,
win[(r + 5) * WIN + col] as i32,
)
};
match (fx, fy) {
(0, 0) => {
for r in 0..bh {
out[r * os..r * os + bw].copy_from_slice(&win[(r + 2) * WIN + 2..(r + 2) * WIN + 2 + bw]);
}
}
(_, 0) => {
// a, b, c
for r in 0..bh {
for c in 0..bw {
let b = clip1((b1(r as isize, c) + 16) >> 5) as i32;
out[r * os + c] = match fx {
1 => ((g(r, c) + b + 1) >> 1) as u8,
2 => b as u8,
_ => ((g(r, c + 1) + b + 1) >> 1) as u8,
};
}
}
}
(0, _) => {
// d, h, n
for r in 0..bh {
for c in 0..bw {
let h = clip1((h1(r, c as isize) + 16) >> 5) as i32;
out[r * os + c] = match fy {
1 => ((g(r, c) + h + 1) >> 1) as u8,
2 => h as u8,
_ => ((g(r + 1, c) + h + 1) >> 1) as u8,
};
}
}
}
(2, _) | (_, 2) => {
// j-based: j needs b1 for rows -2..bh+3
let mut bcol = [0i32; WIN * WIN];
for r in 0..bh + 5 {
for c in 0..bw {
bcol[r * WIN + c] = b1(r as isize - 2, c);
}
}
for r in 0..bh {
for c in 0..bw {
let j1 = tap6(
bcol[r * WIN + c],
bcol[(r + 1) * WIN + c],
bcol[(r + 2) * WIN + c],
bcol[(r + 3) * WIN + c],
bcol[(r + 4) * WIN + c],
bcol[(r + 5) * WIN + c],
);
let j = clip1((j1 + 512) >> 10) as i32;
let v = match (fx, fy) {
(2, 2) => j,
(2, 1) => (clip1((bcol[(r + 2) * WIN + c] + 16) >> 5) as i32 + j + 1) >> 1, // f
(2, 3) => (clip1((bcol[(r + 3) * WIN + c] + 16) >> 5) as i32 + j + 1) >> 1, // q
(1, 2) => (clip1((h1(r, c as isize) + 16) >> 5) as i32 + j + 1) >> 1, // i
_ => (clip1((h1(r, c as isize + 1) + 16) >> 5) as i32 + j + 1) >> 1, // k
};
out[r * os + c] = v as u8;
}
}
}
_ => {
// e, g, p, r: average of a horizontal half-sample (b or s) and a vertical one (h or m)
for r in 0..bh {
for c in 0..bw {
let hr = if fy == 1 { r as isize } else { r as isize + 1 };
let vc = if fx == 1 { c as isize } else { c as isize + 1 };
let bh_ = clip1((b1(hr, c) + 16) >> 5) as i32;
let vv = clip1((h1(r, vc) + 16) >> 5) as i32;
out[r * os + c] = ((bh_ + vv + 1) >> 1) as u8;
}
}
}
}
}
/// Chroma sample interpolation (8.4.2.2.2) for a bw x bh block at integer position (x, y) with
/// eighth-sample fraction (fx, fy).
#[allow(clippy::too_many_arguments)]
pub fn mc_chroma(p: PlaneRef, x: i32, y: i32, fx: u32, fy: u32, bw: usize, bh: usize, out: &mut [u8], os: usize) {
let maxx = p.width as i32 - 1;
let maxy = p.height as i32 - 1;
let fx = fx as i32;
let fy = fy as i32;
let w00 = (8 - fx) * (8 - fy);
let w10 = fx * (8 - fy);
let w01 = (8 - fx) * fy;
let w11 = fx * fy;
let inside = x >= 0 && y >= 0 && x + bw as i32 <= maxx && y + bh as i32 <= maxy;
if inside {
let (x, y) = (x as usize, y as usize);
for r in 0..bh {
let r0 = &p.data[(y + r) * p.stride + x..];
let r1 = &p.data[(y + r + 1) * p.stride + x..];
for c in 0..bw {
let v = w00 * r0[c] as i32 + w10 * r0[c + 1] as i32 + w01 * r1[c] as i32 + w11 * r1[c + 1] as i32;
out[r * os + c] = ((v + 32) >> 6) as u8;
}
}
} else {
let at = |xx: i32, yy: i32| p.data[yy.clamp(0, maxy) as usize * p.stride + xx.clamp(0, maxx) as usize] as i32;
for r in 0..bh as i32 {
for c in 0..bw as i32 {
let v = w00 * at(x + c, y + r) + w10 * at(x + c + 1, y + r) + w01 * at(x + c, y + r + 1) + w11 * at(x + c + 1, y + r + 1);
out[(r as usize) * os + c as usize] = ((v + 32) >> 6) as u8;
}
}
}
}
/// Weights for one prediction direction or pair.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Weight {
/// Default: copy (single list) or rounded average (bi).
Default,
/// Explicit/implicit weighting: log2 denominator, weights and offsets for L0 and L1.
Weighted { log_wd: i32, w0: i32, w1: i32, o0: i32, o1: i32 },
}
/// Combine prediction(s) into `dst` (stride `ds`). `p0`/`p1` have stride `ps`.
#[allow(clippy::too_many_arguments)]
pub fn weighted_store(dst: &mut [u8], ds: usize, p0: Option<&[u8]>, p1: Option<&[u8]>, ps: usize, bw: usize, bh: usize, w: Weight) {
match (p0, p1, w) {
(Some(a), Some(b), Weight::Default) => {
for r in 0..bh {
let d = &mut dst[r * ds..r * ds + bw];
let ra = &a[r * ps..r * ps + bw];
let rb = &b[r * ps..r * ps + bw];
for ((d, &x), &y) in d.iter_mut().zip(ra).zip(rb) {
*d = ((x as u32 + y as u32 + 1) >> 1) as u8;
}
}
}
(Some(a), Some(b), Weight::Weighted { log_wd, w0, w1, o0, o1 }) => {
let round = 1 << log_wd;
let off = (o0 + o1 + 1) >> 1;
for r in 0..bh {
for c in 0..bw {
let v = ((a[r * ps + c] as i32 * w0 + b[r * ps + c] as i32 * w1 + round) >> (log_wd + 1)) + off;
dst[r * ds + c] = clip1(v);
}
}
}
(Some(a), None, w) | (None, Some(a), w) => {
let (wt, o) = match (w, p0.is_some()) {
(Weight::Default, _) => {
for r in 0..bh {
dst[r * ds..r * ds + bw].copy_from_slice(&a[r * ps..r * ps + bw]);
}
return;
}
(Weight::Weighted { w0, o0, .. }, true) => (w0, o0),
(Weight::Weighted { w1, o1, .. }, false) => (w1, o1),
};
let log_wd = match w {
Weight::Weighted { log_wd, .. } => log_wd,
Weight::Default => 0,
};
for r in 0..bh {
for c in 0..bw {
let s = a[r * ps + c] as i32;
let v = if log_wd >= 1 { ((s * wt + (1 << (log_wd - 1))) >> log_wd) + o } else { s * wt + o };
dst[r * ds + c] = clip1(v);
}
}
}
(None, None, _) => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn plane(w: usize, h: usize, f: impl Fn(usize, usize) -> u8) -> Vec<u8> {
(0..w * h).map(|i| f(i % w, i / w)).collect()
}
#[test]
fn full_sample_copy_and_clamp() {
let d = plane(16, 16, |x, y| (x + 16 * y) as u8);
let p = PlaneRef { data: &d, width: 16, height: 16, stride: 16 };
let mut out = [0u8; 16];
mc_luma(p, 2, 3, 0, 0, 4, 4, &mut out, 4);
assert_eq!(&out[..4], &[50, 51, 52, 53]);
// clamped: far outside to the top-left gives sample (0,0)
mc_luma(p, -40, -40, 0, 0, 4, 4, &mut out, 4);
assert!(out.iter().all(|&v| v == 0));
}
#[test]
fn half_sample_constant_is_constant() {
let d = vec![100u8; 32 * 32];
let p = PlaneRef { data: &d, width: 32, height: 32, stride: 32 };
for fx in 0..4 {
for fy in 0..4 {
let mut out = [0u8; 64];
mc_luma(p, 8, 8, fx, fy, 8, 8, &mut out, 8);
assert!(out.iter().all(|&v| v == 100), "{fx},{fy}");
mc_chroma(p, 8, 8, fx * 2, fy * 2, 8, 8, &mut out, 8);
assert!(out.iter().all(|&v| v == 100));
}
}
}
#[test]
fn half_sample_horizontal_ramp() {
// linear ramp: 6-tap filter of a linear function is exact: b = (G + H) / 2 (rounded)
let d = plane(32, 32, |x, _| (x * 4) as u8);
let p = PlaneRef { data: &d, width: 32, height: 32, stride: 32 };
let mut out = [0u8; 16];
mc_luma(p, 8, 8, 2, 0, 4, 4, &mut out, 4);
assert_eq!(&out[..4], &[34, 38, 42, 46]);
mc_luma(p, 8, 8, 1, 0, 4, 4, &mut out, 4);
assert_eq!(&out[..4], &[33, 37, 41, 45]);
}
#[test]
fn weighting() {
let a = [100u8; 4];
let b = [50u8; 4];
let mut d = [0u8; 4];
weighted_store(&mut d, 4, Some(&a), Some(&b), 4, 4, 1, Weight::Default);
assert_eq!(d, [75; 4]);
weighted_store(&mut d, 4, Some(&a), None, 4, 4, 1, Weight::Weighted { log_wd: 5, w0: 16, w1: 0, o0: 3, o1: 0 });
assert_eq!(d, [53; 4]);
}
}

462
crates/h264/src/intra.rs Normal file
View File

@ -0,0 +1,462 @@
//! Intra prediction (8.3): Intra_4x4, Intra_8x8 (with reference filtering), Intra_16x16 and chroma.
/// Neighbouring sample availability for one block.
#[derive(Clone, Copy, Debug, Default)]
pub struct Avail {
pub left: bool,
pub top: bool,
pub top_left: bool,
pub top_right: bool,
}
/// Neighbouring samples of an NxN block: p[x,-1] for x = 0..2N-1, p[-1,y] for y = 0..N-1, p[-1,-1].
#[derive(Clone, Copy)]
struct Edge {
top: [i32; 16],
left: [i32; 8],
tl: i32,
}
impl Edge {
/// p[x, -1] for x >= -1
#[inline(always)]
fn t(&self, x: i32) -> i32 {
if x < 0 { self.tl } else { self.top[x as usize] }
}
/// p[-1, y] for y >= -1
#[inline(always)]
fn l(&self, y: i32) -> i32 {
if y < 0 { self.tl } else { self.left[y as usize] }
}
}
fn load_edge(plane: &[u8], stride: usize, x: usize, y: usize, n: usize, a: Avail) -> Edge {
let mut e = Edge { top: [128; 16], left: [128; 8], tl: 128 };
if a.top {
let row = &plane[(y - 1) * stride + x..];
for i in 0..n {
e.top[i] = row[i] as i32;
}
if a.top_right {
for i in n..2 * n {
e.top[i] = row[i] as i32;
}
} else {
let v = e.top[n - 1];
for t in &mut e.top[n..2 * n] {
*t = v;
}
}
}
if a.left {
for i in 0..n {
e.left[i] = plane[(y + i) * stride + x - 1] as i32;
}
}
if a.top_left {
e.tl = plane[(y - 1) * stride + x - 1] as i32;
}
e
}
/// Directional / DC prediction shared by Intra_4x4 and Intra_8x8 (8.3.1.2.x, 8.3.2.2.x).
fn pred_nxn(mode: u8, e: &Edge, n: i32, a: Avail, out: &mut [u8], stride: usize) {
let mut put = |x: i32, y: i32, v: i32| out[y as usize * stride + x as usize] = v as u8;
match mode {
0 => {
for y in 0..n {
for x in 0..n {
put(x, y, e.t(x));
}
}
}
1 => {
for y in 0..n {
for x in 0..n {
put(x, y, e.l(y));
}
}
}
2 => {
let shift = if n == 4 { 2 } else { 3 };
let st: i32 = (0..n).map(|x| e.t(x)).sum();
let sl: i32 = (0..n).map(|y| e.l(y)).sum();
let dc = match (a.top, a.left) {
(true, true) => (st + sl + n) >> (shift + 1),
(false, true) => (sl + (n >> 1)) >> shift,
(true, false) => (st + (n >> 1)) >> shift,
_ => 128,
};
for y in 0..n {
for x in 0..n {
put(x, y, dc);
}
}
}
3 => {
for y in 0..n {
for x in 0..n {
let v = if x == n - 1 && y == n - 1 {
(e.t(2 * n - 2) + 3 * e.t(2 * n - 1) + 2) >> 2
} else {
(e.t(x + y) + 2 * e.t(x + y + 1) + e.t(x + y + 2) + 2) >> 2
};
put(x, y, v);
}
}
}
4 => {
for y in 0..n {
for x in 0..n {
let v = if x > y {
(e.t(x - y - 2) + 2 * e.t(x - y - 1) + e.t(x - y) + 2) >> 2
} else if x < y {
(e.l(y - x - 2) + 2 * e.l(y - x - 1) + e.l(y - x) + 2) >> 2
} else {
(e.t(0) + 2 * e.tl + e.l(0) + 2) >> 2
};
put(x, y, v);
}
}
}
5 => {
for y in 0..n {
for x in 0..n {
let z = 2 * x - y;
let v = if z >= 0 && z % 2 == 0 {
(e.t(x - (y >> 1) - 1) + e.t(x - (y >> 1)) + 1) >> 1
} else if z > 0 {
(e.t(x - (y >> 1) - 2) + 2 * e.t(x - (y >> 1) - 1) + e.t(x - (y >> 1)) + 2) >> 2
} else if z == -1 {
(e.l(0) + 2 * e.tl + e.t(0) + 2) >> 2
} else {
(e.l(y - 2 * x - 1) + 2 * e.l(y - 2 * x - 2) + e.l(y - 2 * x - 3) + 2) >> 2
};
put(x, y, v);
}
}
}
6 => {
for y in 0..n {
for x in 0..n {
let z = 2 * y - x;
let v = if z >= 0 && z % 2 == 0 {
(e.l(y - (x >> 1) - 1) + e.l(y - (x >> 1)) + 1) >> 1
} else if z > 0 {
(e.l(y - (x >> 1) - 2) + 2 * e.l(y - (x >> 1) - 1) + e.l(y - (x >> 1)) + 2) >> 2
} else if z == -1 {
(e.l(0) + 2 * e.tl + e.t(0) + 2) >> 2
} else {
(e.t(x - 2 * y - 1) + 2 * e.t(x - 2 * y - 2) + e.t(x - 2 * y - 3) + 2) >> 2
};
put(x, y, v);
}
}
}
7 => {
for y in 0..n {
for x in 0..n {
let i = x + (y >> 1);
let v = if y % 2 == 0 { (e.t(i) + e.t(i + 1) + 1) >> 1 } else { (e.t(i) + 2 * e.t(i + 1) + e.t(i + 2) + 2) >> 2 };
put(x, y, v);
}
}
}
_ => {
let zmax = 2 * n - 3;
for y in 0..n {
for x in 0..n {
let z = x + 2 * y;
let i = y + (x >> 1);
let v = if z < zmax && z % 2 == 0 {
(e.l(i) + e.l(i + 1) + 1) >> 1
} else if z < zmax {
(e.l(i) + 2 * e.l(i + 1) + e.l(i + 2) + 2) >> 2
} else if z == zmax {
(e.l(n - 2) + 3 * e.l(n - 1) + 2) >> 2
} else {
e.l(n - 1)
};
put(x, y, v);
}
}
}
}
}
/// Intra_4x4 prediction written into `plane` at (x, y).
pub fn pred4x4(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8, a: Avail) {
let e = load_edge(plane, stride, x, y, 4, a);
pred_nxn(mode, &e, 4, a, &mut plane[y * stride + x..], stride);
}
/// Intra_8x8 prediction (with reference sample filtering) written into `plane` at (x, y).
pub fn pred8x8(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8, a: Avail) {
let p = load_edge(plane, stride, x, y, 8, a);
let mut f = p;
if a.top {
f.top[0] = if a.top_left { (p.tl + 2 * p.top[0] + p.top[1] + 2) >> 2 } else { (3 * p.top[0] + p.top[1] + 2) >> 2 };
for i in 1..15 {
f.top[i] = (p.top[i - 1] + 2 * p.top[i] + p.top[i + 1] + 2) >> 2;
}
f.top[15] = (p.top[14] + 3 * p.top[15] + 2) >> 2;
}
if a.top_left {
f.tl = match (a.top, a.left) {
(true, true) => (p.top[0] + 2 * p.tl + p.left[0] + 2) >> 2,
(true, false) => (3 * p.tl + p.top[0] + 2) >> 2,
(false, true) => (3 * p.tl + p.left[0] + 2) >> 2,
_ => p.tl,
};
}
if a.left {
f.left[0] = if a.top_left { (p.tl + 2 * p.left[0] + p.left[1] + 2) >> 2 } else { (3 * p.left[0] + p.left[1] + 2) >> 2 };
for i in 1..7 {
f.left[i] = (p.left[i - 1] + 2 * p.left[i] + p.left[i + 1] + 2) >> 2;
}
f.left[7] = (p.left[6] + 3 * p.left[7] + 2) >> 2;
}
pred_nxn(mode, &f, 8, a, &mut plane[y * stride + x..], stride);
}
#[inline(always)]
fn clip(v: i32) -> u8 {
v.clamp(0, 255) as u8
}
/// Intra_16x16 prediction (8.3.3) for the macroblock at luma (x, y).
pub fn pred16x16(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8, a: Avail) {
let mut top = [0i32; 16];
let mut left = [0i32; 16];
if a.top {
for i in 0..16 {
top[i] = plane[(y - 1) * stride + x + i] as i32;
}
}
if a.left {
for i in 0..16 {
left[i] = plane[(y + i) * stride + x - 1] as i32;
}
}
let base = y * stride + x;
match mode {
0 => {
for r in 0..16 {
for c in 0..16 {
plane[base + r * stride + c] = top[c] as u8;
}
}
}
1 => {
for r in 0..16 {
plane[base + r * stride..base + r * stride + 16].fill(left[r] as u8);
}
}
2 => {
let st: i32 = top.iter().sum();
let sl: i32 = left.iter().sum();
let dc = match (a.top, a.left) {
(true, true) => (st + sl + 16) >> 5,
(false, true) => (sl + 8) >> 4,
(true, false) => (st + 8) >> 4,
_ => 128,
} as u8;
for r in 0..16 {
plane[base + r * stride..base + r * stride + 16].fill(dc);
}
}
_ => {
let tl = if a.top_left { plane[(y - 1) * stride + x - 1] as i32 } else { 128 };
let t = |i: i32| if i < 0 { tl } else { top[i as usize] };
let l = |i: i32| if i < 0 { tl } else { left[i as usize] };
let mut h = 0;
let mut v = 0;
for k in 0..8 {
h += (k + 1) * (t(8 + k) - t(6 - k));
v += (k + 1) * (l(8 + k) - l(6 - k));
}
let aa = 16 * (left[15] + top[15]);
let b = (5 * h + 32) >> 6;
let c = (5 * v + 32) >> 6;
for r in 0..16 {
for cc in 0..16 {
plane[base + r * stride + cc] = clip((aa + b * (cc as i32 - 7) + c * (r as i32 - 7) + 16) >> 5);
}
}
}
}
}
/// Chroma intra prediction for a 4:2:0 macroblock (8x8 block at chroma (x, y)), 8.3.4.
/// Modes: 0 DC, 1 horizontal, 2 vertical, 3 plane.
pub fn pred_chroma(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8, a: Avail) {
const W: usize = 8;
const H: usize = 8;
let mut top = [0i32; W];
let mut left = [0i32; H];
if a.top {
for i in 0..W {
top[i] = plane[(y - 1) * stride + x + i] as i32;
}
}
if a.left {
for i in 0..H {
left[i] = plane[(y + i) * stride + x - 1] as i32;
}
}
let base = y * stride + x;
match mode {
0 => {
for by in 0..H / 4 {
for bx in 0..W / 4 {
let st: i32 = top[bx * 4..bx * 4 + 4].iter().sum();
let sl: i32 = left[by * 4..by * 4 + 4].iter().sum();
let dc = if (bx == 0 && by == 0) || (bx > 0 && by > 0) {
match (a.top, a.left) {
(true, true) => (st + sl + 4) >> 3,
(false, true) => (sl + 2) >> 2,
(true, false) => (st + 2) >> 2,
_ => 128,
}
} else if bx > 0 {
if a.top {
(st + 2) >> 2
} else if a.left {
(sl + 2) >> 2
} else {
128
}
} else if a.left {
(sl + 2) >> 2
} else if a.top {
(st + 2) >> 2
} else {
128
};
for r in 0..4 {
let o = base + (by * 4 + r) * stride + bx * 4;
plane[o..o + 4].fill(dc as u8);
}
}
}
}
1 => {
for r in 0..H {
plane[base + r * stride..base + r * stride + W].fill(left[r] as u8);
}
}
2 => {
for r in 0..H {
for c in 0..W {
plane[base + r * stride + c] = top[c] as u8;
}
}
}
_ => {
let tl = if a.top_left { plane[(y - 1) * stride + x - 1] as i32 } else { 128 };
let t = |i: i32| if i < 0 { tl } else { top[i as usize] };
let l = |i: i32| if i < 0 { tl } else { left[i as usize] };
let mut h = 0;
let mut v = 0;
for k in 0..4 {
h += (k + 1) * (t(4 + k) - t(2 - k));
v += (k + 1) * (l(4 + k) - l(2 - k));
}
let aa = 16 * (left[H - 1] + top[W - 1]);
let b = (34 * h + 32) >> 6;
let c = (34 * v + 32) >> 6;
for r in 0..H {
for cc in 0..W {
plane[base + r * stride + cc] = clip((aa + b * (cc as i32 - 3) + c * (r as i32 - 3) + 16) >> 5);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn setup() -> (Vec<u8>, usize) {
// 12x12 plane; block at (4,4); top row y=3 = 10,20,..; left column x=3 = 1,2,3,...
let stride = 16;
let mut p = vec![0u8; stride * 16];
for i in 0..12 {
p[3 * stride + 4 + i] = (10 * (i + 1)) as u8;
}
for i in 0..8 {
p[(4 + i) * stride + 3] = (i + 1) as u8;
}
p[3 * stride + 3] = 5;
(p, stride)
}
const ALL: Avail = Avail { left: true, top: true, top_left: true, top_right: true };
#[test]
fn i4_vertical_horizontal_dc() {
let (mut p, s) = setup();
pred4x4(&mut p, s, 4, 4, 0, ALL);
assert_eq!(&p[4 * s + 4..4 * s + 8], &[10, 20, 30, 40]);
assert_eq!(&p[7 * s + 4..7 * s + 8], &[10, 20, 30, 40]);
pred4x4(&mut p, s, 4, 4, 1, ALL);
assert_eq!(&p[5 * s + 4..5 * s + 8], &[2, 2, 2, 2]);
pred4x4(&mut p, s, 4, 4, 2, ALL);
// (10+20+30+40 + 1+2+3+4 + 4) >> 3 = 114 >> 3 = 14
assert_eq!(p[4 * s + 4], 14);
}
#[test]
fn i4_diag_down_left_hand_computed() {
let (mut p, s) = setup();
pred4x4(&mut p, s, 4, 4, 3, ALL);
// pred[0,0] = (t0 + 2 t1 + t2 + 2) >> 2 = (10 + 40 + 30 + 2) >> 2 = 20
assert_eq!(p[4 * s + 4], 20);
// pred[3,3] = (t6 + 3 t7 + 2) >> 2 = (70 + 240 + 2) >> 2 = 78
assert_eq!(p[7 * s + 7], 78);
}
#[test]
fn i4_diag_down_right_hand_computed() {
let (mut p, s) = setup();
pred4x4(&mut p, s, 4, 4, 4, ALL);
// x==y: (t0 + 2 tl + l0 + 2) >> 2 = (10 + 10 + 1 + 2) >> 2 = 5
assert_eq!(p[4 * s + 4], 5);
// x=1,y=0: (tl + 2 t0 + t1 + 2)>>2 = (5 + 20 + 20 + 2) >> 2 = 11
assert_eq!(p[4 * s + 5], 11);
// x=0,y=1: (tl + 2 l0 + l1 + 2) >> 2 = (5 + 2 + 2 + 2) >> 2 = 2
assert_eq!(p[5 * s + 4], 2);
}
#[test]
fn i4_top_right_substitution() {
let (mut p, s) = setup();
let a = Avail { top_right: false, ..ALL };
pred4x4(&mut p, s, 4, 4, 3, a);
// top-right replaced by t3 = 40: pred[3,3] = (40 + 120 + 2) >> 2 = 40
assert_eq!(p[7 * s + 7], 40);
}
#[test]
fn i16_plane_flat_is_flat() {
let s = 32;
let mut p = vec![77u8; s * 32];
pred16x16(&mut p, s, 8, 8, 3, ALL);
assert!(p[8 * s + 8..8 * s + 24].iter().all(|&v| v == 77));
}
#[test]
fn chroma_dc_rules() {
let s = 16;
let mut p = vec![0u8; s * 16];
for i in 0..8 {
p[3 * s + 4 + i] = if i < 4 { 40 } else { 80 };
p[(4 + i) * s + 3] = if i < 4 { 8 } else { 16 };
}
pred_chroma(&mut p, s, 4, 4, 0, ALL);
assert_eq!(p[4 * s + 4], (160 + 32 + 4) >> 3); // top-left block: both
assert_eq!(p[4 * s + 8], 80); // top-right block: top only
assert_eq!(p[8 * s + 4], 16); // bottom-left: left only
assert_eq!(p[8 * s + 8], ((320 + 64 + 4) >> 3) as u8); // bottom-right: both
}
}

View File

@ -1,13 +1,75 @@
//! Clean-room pure-Rust H.264 / AVC decoder, implemented from ITU-T Rec. H.264 (ISO/IEC 14496-10).
//!
//! M2.1: NAL unit, parameter set, slice header and picture order count parsing.
//! Supported: progressive (frame) coding, 8-bit 4:2:0, Baseline / Main / High profiles — CAVLC and CABAC,
//! I/P/B slices, 8x8 transform, custom scaling matrices, weighted prediction, spatial/temporal direct,
//! multiple slices, long-term references and all MMCOs, deblocking.
//!
//! ```no_run
//! let mut dec = filmcraft_h264::Decoder::new();
//! let stream = std::fs::read("video.h264").unwrap();
//! for pic in dec.decode(&stream, 0).unwrap() {
//! println!("{}x{} poc {}", pic.width, pic.height, pic.poc);
//! }
//! for pic in dec.flush() {
//! let _ = pic;
//! }
//! ```
// Constant tables are shared with the macroblock decoding stages that follow.
// Index loops over fixed-size blocks read more clearly than iterator chains in codec code.
#![allow(clippy::needless_range_loop, clippy::too_many_arguments)]
// Deblocking tables and per-slice filter parameters are consumed by the M2.5 loop filter.
#![allow(dead_code)]
mod cavlc;
#[rustfmt::skip]
mod cavlc_tables;
mod decoder;
mod dpb;
mod error;
mod inter;
mod intra;
mod mbtypes;
pub mod params;
mod picture;
pub mod slice;
mod slicedec;
mod tables;
mod transform;
pub use decoder::Decoder;
pub use error::{Error, Result};
/// Colour description from the VUI (ITU-T H.273 code points).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ColorInfo {
/// `video_full_range_flag`.
pub full_range: bool,
pub primaries: u8,
pub transfer: u8,
pub matrix: u8,
}
/// A decoded, cropped picture in 8-bit planar 4:2:0.
#[derive(Clone, Debug)]
pub struct Picture {
/// Cropped luma width.
pub width: u32,
/// Cropped luma height.
pub height: u32,
pub chroma_width: u32,
pub chroma_height: u32,
pub y: Vec<u8>,
pub u: Vec<u8>,
pub v: Vec<u8>,
pub y_stride: usize,
pub uv_stride: usize,
/// Presentation timestamp passed to [`Decoder::decode`] with the access unit of this picture.
pub pts: i64,
/// Picture order count.
pub poc: i32,
/// IDR picture.
pub key: bool,
pub color: ColorInfo,
/// Sample aspect ratio (0, 0 when unspecified).
pub sar: (u16, u16),
}

235
crates/h264/src/mbtypes.rs Normal file
View File

@ -0,0 +1,235 @@
//! Macroblock / sub-macroblock type tables (7.4.5, Tables 7-11 .. 7-18).
use crate::picture::MbKind;
pub const PRED_L0: u8 = 1;
pub const PRED_L1: u8 = 2;
pub const PRED_BI: u8 = 3;
/// Macroblock partitioning for inter macroblocks.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Part {
#[default]
P16x16,
P16x8,
P8x16,
P8x8,
}
impl Part {
pub fn num_parts(self) -> usize {
match self {
Part::P16x16 => 1,
Part::P16x8 | Part::P8x16 => 2,
Part::P8x8 => 4,
}
}
/// (x, y, w, h) of partition `i` in 4x4-block units.
pub fn rect(self, i: usize) -> (usize, usize, usize, usize) {
match self {
Part::P16x16 => (0, 0, 4, 4),
Part::P16x8 => (0, 2 * i, 4, 2),
Part::P8x16 => (2 * i, 0, 2, 4),
Part::P8x8 => ((i & 1) * 2, (i >> 1) * 2, 2, 2),
}
}
}
/// Sub-macroblock partitioning.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum SubPart {
#[default]
S8x8,
S8x4,
S4x8,
S4x4,
}
impl SubPart {
pub fn num_parts(self) -> usize {
match self {
SubPart::S8x8 => 1,
SubPart::S8x4 | SubPart::S4x8 => 2,
SubPart::S4x4 => 4,
}
}
/// (x, y, w, h) of sub-partition `j` relative to the 8x8 block, in 4x4 units.
pub fn rect(self, j: usize) -> (usize, usize, usize, usize) {
match self {
SubPart::S8x8 => (0, 0, 2, 2),
SubPart::S8x4 => (0, j, 2, 1),
SubPart::S4x8 => (j, 0, 1, 2),
SubPart::S4x4 => (j & 1, j >> 1, 1, 1),
}
}
}
/// Decoded meaning of mb_type.
#[derive(Clone, Copy, Debug, Default)]
pub struct MbTypeInfo {
pub kind: MbKind,
/// Intra16x16PredMode.
pub i16_mode: u8,
/// coded_block_pattern implied by an Intra_16x16 mb_type.
pub i16_cbp: u8,
pub part: Part,
/// Prediction list flags per macroblock partition.
pub pred: [u8; 2],
/// P_8x8ref0.
pub ref0: bool,
}
/// Intra mb_type (I-slice numbering 0..=25).
pub fn intra_mb_type(t: u32) -> Option<MbTypeInfo> {
let mut m = MbTypeInfo::default();
match t {
0 => m.kind = MbKind::I4x4, // or I8x8, decided by transform_size_8x8_flag
1..=24 => {
m.kind = MbKind::I16x16;
m.i16_mode = ((t - 1) % 4) as u8;
let chroma = ((t - 1) / 4) % 3;
let luma = if t >= 13 { 15 } else { 0 };
m.i16_cbp = (luma | (chroma << 4)) as u8;
}
25 => m.kind = MbKind::IPcm,
_ => return None,
}
Some(m)
}
/// P-slice mb_type 0..=4 (5.. are intra, handled by the caller).
pub fn p_mb_type(t: u32) -> Option<MbTypeInfo> {
let mut m = MbTypeInfo { kind: MbKind::Inter, pred: [PRED_L0; 2], ..Default::default() };
m.part = match t {
0 => Part::P16x16,
1 => Part::P16x8,
2 => Part::P8x16,
3 => Part::P8x8,
4 => {
m.ref0 = true;
Part::P8x8
}
_ => return None,
};
Some(m)
}
/// B-slice mb_type 0..=22 (23.. are intra).
pub fn b_mb_type(t: u32) -> Option<MbTypeInfo> {
let mut m = MbTypeInfo { kind: MbKind::Inter, ..Default::default() };
match t {
0 => {
m.kind = MbKind::BDirect16x16;
m.pred = [PRED_BI; 2];
}
1..=3 => {
m.part = Part::P16x16;
m.pred = [t as u8; 2];
}
4..=21 => {
const PAIRS: [(u8, u8); 9] = [
(PRED_L0, PRED_L0),
(PRED_L1, PRED_L1),
(PRED_L0, PRED_L1),
(PRED_L1, PRED_L0),
(PRED_L0, PRED_BI),
(PRED_L1, PRED_BI),
(PRED_BI, PRED_L0),
(PRED_BI, PRED_L1),
(PRED_BI, PRED_BI),
];
let (a, b) = PAIRS[((t - 4) / 2) as usize];
m.pred = [a, b];
m.part = if t.is_multiple_of(2) { Part::P16x8 } else { Part::P8x16 };
}
22 => m.part = Part::P8x8,
_ => return None,
}
Some(m)
}
/// Sub-macroblock type: (shape, prediction flags, direct).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SubMbInfo {
pub shape: SubPart,
pub pred: u8,
pub direct: bool,
}
pub fn p_sub_mb_type(t: u32) -> Option<SubMbInfo> {
let shape = match t {
0 => SubPart::S8x8,
1 => SubPart::S8x4,
2 => SubPart::S4x8,
3 => SubPart::S4x4,
_ => return None,
};
Some(SubMbInfo { shape, pred: PRED_L0, direct: false })
}
pub fn b_sub_mb_type(t: u32) -> Option<SubMbInfo> {
let (shape, pred, direct) = match t {
0 => (SubPart::S8x8, PRED_BI, true),
1 => (SubPart::S8x8, PRED_L0, false),
2 => (SubPart::S8x8, PRED_L1, false),
3 => (SubPart::S8x8, PRED_BI, false),
4 => (SubPart::S8x4, PRED_L0, false),
5 => (SubPart::S4x8, PRED_L0, false),
6 => (SubPart::S8x4, PRED_L1, false),
7 => (SubPart::S4x8, PRED_L1, false),
8 => (SubPart::S8x4, PRED_BI, false),
9 => (SubPart::S4x8, PRED_BI, false),
10 => (SubPart::S4x4, PRED_L0, false),
11 => (SubPart::S4x4, PRED_L1, false),
12 => (SubPart::S4x4, PRED_BI, false),
_ => return None,
};
Some(SubMbInfo { shape, pred, direct })
}
/// Table 9-4 (a): codeNum -> coded_block_pattern for (Intra_4x4/Intra_8x8, Inter), ChromaArrayType 1 or 2.
#[rustfmt::skip]
pub const CBP_ME: [(u8, u8); 48] = [
(47, 0), (31, 16), (15, 1), (0, 2), (23, 4), (27, 8), (29, 32), (30, 3),
(7, 5), (11, 10), (13, 12), (14, 15), (39, 47), (43, 7), (45, 11), (46, 13),
(16, 14), (3, 6), (5, 9), (10, 31), (12, 35), (19, 37), (21, 42), (26, 44),
(28, 33), (35, 34), (37, 36), (42, 40), (44, 39), (1, 43), (2, 45), (4, 46),
(8, 17), (17, 18), (18, 20), (20, 24), (24, 19), (6, 21), (9, 26), (22, 28),
(25, 23), (32, 27), (33, 29), (34, 30), (36, 22), (40, 25), (38, 38), (41, 41),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cbp_table_is_permutation() {
let mut a = [false; 48];
let mut b = [false; 48];
for &(i, p) in &CBP_ME {
a[i as usize] = true;
b[p as usize] = true;
}
assert!(a.iter().all(|&x| x) && b.iter().all(|&x| x));
}
#[test]
fn i16_types() {
let m = intra_mb_type(1).unwrap();
assert_eq!((m.kind, m.i16_mode, m.i16_cbp), (MbKind::I16x16, 0, 0));
let m = intra_mb_type(24).unwrap();
assert_eq!((m.i16_mode, m.i16_cbp), (3, 15 | (2 << 4)));
let m = intra_mb_type(13).unwrap();
assert_eq!((m.i16_mode, m.i16_cbp), (0, 15));
}
#[test]
fn b_types() {
let m = b_mb_type(12).unwrap();
assert_eq!((m.part, m.pred), (Part::P16x8, [PRED_L0, PRED_BI]));
let m = b_mb_type(21).unwrap();
assert_eq!((m.part, m.pred), (Part::P8x16, [PRED_BI, PRED_BI]));
let m = b_mb_type(11).unwrap();
assert_eq!((m.part, m.pred), (Part::P8x16, [PRED_L1, PRED_L0]));
}
}

144
crates/h264/src/picture.rs Normal file
View File

@ -0,0 +1,144 @@
//! Picture buffers and per-macroblock state.
use std::sync::Arc;
/// Planar 4:2:0 8-bit sample buffers (MB-aligned dimensions, stride == width).
#[derive(Clone)]
pub struct Planes {
pub y: Vec<u8>,
pub cb: Vec<u8>,
pub cr: Vec<u8>,
pub width: usize,
pub height: usize,
pub cwidth: usize,
pub cheight: usize,
}
impl Planes {
pub fn new(width: usize, height: usize) -> Self {
let (cw, ch) = (width / 2, height / 2);
Planes { y: vec![0; width * height], cb: vec![128; cw * ch], cr: vec![128; cw * ch], width, height, cwidth: cw, cheight: ch }
}
pub fn gray(width: usize, height: usize) -> Self {
let mut p = Self::new(width, height);
p.y.fill(128);
p
}
}
/// Macroblock coding category.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum MbKind {
#[default]
None,
I4x4,
I8x8,
I16x16,
IPcm,
PSkip,
BSkip,
BDirect16x16,
/// Any other P or B inter macroblock.
Inter,
}
impl MbKind {
#[inline]
pub fn is_intra(self) -> bool {
matches!(self, MbKind::I4x4 | MbKind::I8x8 | MbKind::I16x16 | MbKind::IPcm)
}
}
/// Per-macroblock state kept for the whole picture (neighbour derivations, deblocking, co-located data).
#[derive(Clone, Copy)]
pub struct MbState {
/// Slice number within the picture; `u32::MAX` = not (yet) decoded.
pub slice_num: u32,
pub kind: MbKind,
pub transform_8x8: bool,
/// coded_block_pattern: bits 0..3 luma, bits 4..5 chroma.
pub cbp: u8,
pub qp: u8,
/// QPc for Cb / Cr (used by deblocking).
pub qpc: [u8; 2],
pub intra_chroma_mode: u8,
/// Intra4x4PredMode / Intra8x8PredMode per 4x4 block (raster order).
pub intra_modes: [u8; 16],
/// CAVLC: TotalCoeff per luma 4x4 block (raster). CABAC: coded_block_flag.
pub nnz: [u8; 16],
/// Same for chroma AC blocks [Cb, Cr][raster 2x2].
pub nnz_c: [[u8; 4]; 2],
/// CABAC coded_block_flag of DC blocks: bit0 luma (I16x16), bit1 Cb, bit2 Cr.
pub cbf_dc: u8,
/// Luma 4x4 blocks (raster bit) with non-zero coefficients, for deblocking bS = 2.
pub nz_mask: u16,
pub ref_idx: [[i8; 4]; 2],
/// Motion vectors per 4x4 block (raster order).
pub mv: [[[i16; 2]; 16]; 2],
/// |mvd| per 4x4 block (raster), saturated, for CABAC context selection.
#[allow(dead_code)] // used by CABAC context selection
pub mvd: [[[u8; 2]; 16]; 2],
/// Bit per 8x8 block predicted in direct mode.
pub direct8x8: u8,
}
impl Default for MbState {
fn default() -> Self {
MbState {
slice_num: u32::MAX,
kind: MbKind::None,
transform_8x8: false,
cbp: 0,
qp: 0,
qpc: [0; 2],
intra_chroma_mode: 0,
intra_modes: [2; 16],
nnz: [0; 16],
nnz_c: [[0; 4]; 2],
cbf_dc: 0,
nz_mask: 0,
ref_idx: [[-1; 4]; 2],
mv: [[[0; 2]; 16]; 2],
mvd: [[[0; 2]; 16]; 2],
direct8x8: 0,
}
}
}
/// Motion data of a decoded picture, used as co-located data for direct prediction.
#[derive(Clone, Default)]
pub struct MotionField {
/// Per 4x4 block: index mb * 16 + raster.
pub mv: [Vec<[i16; 2]>; 2],
/// Per 8x8 block: index mb * 4 + b8.
pub ref_idx: [Vec<i8>; 2],
/// Unique id of the referenced picture per 8x8 block (u32::MAX = none).
pub ref_id: [Vec<u32>; 2],
pub intra: Vec<bool>,
}
/// A decoded frame as stored in the DPB and referenced by later pictures.
pub struct Frame {
pub id: u32,
pub poc: i32,
pub planes: Planes,
pub motion: MotionField,
}
pub type FrameRef = Arc<Frame>;
/// An entry of RefPicList0/1 for the current slice.
#[derive(Clone)]
pub struct RefPic {
pub frame: FrameRef,
pub long_term: bool,
}
impl RefPic {
pub fn poc(&self) -> i32 {
self.frame.poc
}
pub fn id(&self) -> u32 {
self.frame.id
}
}

1340
crates/h264/src/slicedec.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,228 @@
//! Scaling (dequantisation) and inverse transforms (8.5.10 - 8.5.13).
use crate::params::ScalingMatrices;
use crate::tables::{norm_adjust4, norm_adjust8};
/// LevelScale4x4 / LevelScale8x8 for all six scaling lists and qP % 6, raster order.
#[derive(Clone)]
pub struct LevelScale {
/// [list 0..6 (IntraY, IntraCb, IntraCr, InterY, InterCb, InterCr)][qp % 6][pos]
pub ls4: [[[i32; 16]; 6]; 6],
/// [list 0..6 (IntraY, InterY, IntraCb, InterCb, IntraCr, InterCr)][qp % 6][pos]
pub ls8: [[[i32; 64]; 6]; 6],
}
impl LevelScale {
pub fn new(m: &ScalingMatrices) -> Box<Self> {
let mut s = Box::new(LevelScale { ls4: [[[0; 16]; 6]; 6], ls8: [[[0; 64]; 6]; 6] });
for l in 0..6 {
for q in 0..6 {
for p in 0..16 {
s.ls4[l][q][p] = m.m4[l][p] as i32 * norm_adjust4(q, p) as i32;
}
for p in 0..64 {
s.ls8[l][q][p] = m.m8[l][p] as i32 * norm_adjust8(q, p) as i32;
}
}
}
s
}
}
/// Scale one 4x4 AC coefficient (8.5.12.1, not the DC of Intra16x16 / chroma blocks).
#[inline(always)]
pub fn scale4(c: i32, ls: i32, qp: i32) -> i32 {
if qp >= 24 { (c * ls) << (qp / 6 - 4) } else { (c * ls + (1 << (3 - qp / 6))) >> (4 - qp / 6) }
}
/// Scale one 8x8 coefficient (8.5.13.1).
#[inline(always)]
pub fn scale8(c: i32, ls: i32, qp: i32) -> i32 {
if qp >= 36 { (c * ls) << (qp / 6 - 6) } else { (c * ls + (1 << (5 - qp / 6))) >> (6 - qp / 6) }
}
/// Intra16x16 luma DC: inverse Hadamard + scaling (8.5.10). `c` is raster order (row*4+col);
/// output replaces `c` with dcY (raster: row i -> 4x4 block row, col j -> block column).
pub fn luma_dc_dequant(c: &mut [i32; 16], qp: i32, ls00: i32) {
let mut t = [0i32; 16];
// rows
for i in 0..4 {
let r = &c[i * 4..i * 4 + 4];
let s01 = r[0] + r[1];
let d01 = r[0] - r[1];
let s23 = r[2] + r[3];
let d23 = r[2] - r[3];
t[i * 4] = s01 + s23;
t[i * 4 + 1] = s01 - s23;
t[i * 4 + 2] = d01 - d23;
t[i * 4 + 3] = d01 + d23;
}
// columns
for j in 0..4 {
let s01 = t[j] + t[4 + j];
let d01 = t[j] - t[4 + j];
let s23 = t[8 + j] + t[12 + j];
let d23 = t[8 + j] - t[12 + j];
let f = [s01 + s23, s01 - s23, d01 - d23, d01 + d23];
for i in 0..4 {
c[i * 4 + j] = if qp >= 36 { (f[i] * ls00) << (qp / 6 - 6) } else { (f[i] * ls00 + (1 << (5 - qp / 6))) >> (6 - qp / 6) };
}
}
}
/// 4:2:0 chroma DC: 2x2 transform + scaling (8.5.11). `c` = [c00, c01, c10, c11].
pub fn chroma_dc_dequant_420(c: &mut [i32; 4], qp: i32, ls00: i32) {
let s0 = c[0] + c[1];
let d0 = c[0] - c[1];
let s1 = c[2] + c[3];
let d1 = c[2] - c[3];
let f = [s0 + s1, d0 + d1, s0 - s1, d0 - d1];
for k in 0..4 {
c[k] = ((f[k] * ls00) << (qp / 6)) >> 5;
}
}
#[inline(always)]
fn clip_u8(v: i32) -> u8 {
v.clamp(0, 255) as u8
}
/// Inverse 4x4 transform of raster-order `d` and add to `dst` (8.5.12.2, 8.5.14).
pub fn idct4_add(d: &[i32; 16], dst: &mut [u8], stride: usize) {
let mut t = [0i32; 16];
for i in 0..4 {
let r = &d[i * 4..i * 4 + 4];
let e = r[0] + r[2];
let f = r[0] - r[2];
let g = (r[1] >> 1) - r[3];
let h = r[1] + (r[3] >> 1);
t[i * 4] = e + h;
t[i * 4 + 1] = f + g;
t[i * 4 + 2] = f - g;
t[i * 4 + 3] = e - h;
}
for j in 0..4 {
let e = t[j] + t[8 + j];
let f = t[j] - t[8 + j];
let g = (t[4 + j] >> 1) - t[12 + j];
let h = t[4 + j] + (t[12 + j] >> 1);
let col = [e + h, f + g, f - g, e - h];
for i in 0..4 {
let p = &mut dst[i * stride + j];
*p = clip_u8(*p as i32 + ((col[i] + 32) >> 6));
}
}
}
/// DC-only shortcut: all AC coefficients zero.
#[allow(dead_code)]
pub fn idct4_dc_add(dc: i32, dst: &mut [u8], stride: usize) {
let v = (dc + 32) >> 6;
for i in 0..4 {
for p in &mut dst[i * stride..i * stride + 4] {
*p = clip_u8(*p as i32 + v);
}
}
}
#[inline(always)]
fn idct8_1d(d: [i32; 8]) -> [i32; 8] {
let a0 = d[0] + d[4];
let a4 = d[0] - d[4];
let a2 = (d[2] >> 1) - d[6];
let a6 = d[2] + (d[6] >> 1);
let b0 = a0 + a6;
let b2 = a4 + a2;
let b4 = a4 - a2;
let b6 = a0 - a6;
let a1 = -d[3] + d[5] - d[7] - (d[7] >> 1);
let a3 = d[1] + d[7] - d[3] - (d[3] >> 1);
let a5 = -d[1] + d[7] + d[5] + (d[5] >> 1);
let a7 = d[3] + d[5] + d[1] + (d[1] >> 1);
let b1 = a1 + (a7 >> 2);
let b7 = a7 - (a1 >> 2);
let b3 = a3 + (a5 >> 2);
let b5 = (a3 >> 2) - a5;
[b0 + b7, b2 + b5, b4 + b3, b6 + b1, b6 - b1, b4 - b3, b2 - b5, b0 - b7]
}
/// Inverse 8x8 transform of raster-order `d` and add to `dst` (8.5.13.2).
pub fn idct8_add(d: &[i32; 64], dst: &mut [u8], stride: usize) {
let mut t = [0i32; 64];
for i in 0..8 {
let row: [i32; 8] = d[i * 8..i * 8 + 8].try_into().unwrap();
t[i * 8..i * 8 + 8].copy_from_slice(&idct8_1d(row));
}
for j in 0..8 {
let col = idct8_1d(std::array::from_fn(|i| t[i * 8 + j]));
for i in 0..8 {
let p = &mut dst[i * stride + j];
*p = clip_u8(*p as i32 + ((col[i] + 32) >> 6));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn idct4_dc_only_matches_shortcut() {
let mut d = [0i32; 16];
d[0] = 640; // -> +10 everywhere
let mut a = [100u8; 16];
let mut b = [100u8; 16];
idct4_add(&d, &mut a, 4);
idct4_dc_add(640, &mut b, 4);
assert_eq!(a, b);
assert!(a.iter().all(|&v| v == 110));
}
#[test]
fn idct4_hand_example() {
// d = [[64, 64, 0, 0], 0...]: row0: e=64,f=64,g=32,h=64 -> [128, 96, 32, 0]; columns: each col only row0 set
// col j: e=f=t0j, g=0, h=0 -> all rows = t0j. Result (t+32)>>6 = [2, 2, 1, 0] for every row.
let mut d = [0i32; 16];
d[0] = 64;
d[1] = 64;
let mut out = [0u8; 16];
idct4_add(&d, &mut out, 4);
for i in 0..4 {
assert_eq!(&out[i * 4..i * 4 + 4], &[2, 2, 1, 0]);
}
}
#[test]
fn idct8_dc() {
let mut d = [0i32; 64];
d[0] = 64 * 5;
let mut out = [10u8; 64];
idct8_add(&d, &mut out, 8);
assert!(out.iter().all(|&v| v == 15));
}
#[test]
fn luma_dc_flat() {
// single DC coefficient spreads equally to all 16 blocks
let mut c = [0i32; 16];
c[0] = 4;
luma_dc_dequant(&mut c, 36, 16 * 16);
assert!(c.iter().all(|&v| v == 4 * 256));
}
#[test]
fn chroma_dc() {
let mut c = [1, 0, 0, 0];
chroma_dc_dequant_420(&mut c, 6, 16 * 16);
// f = [1,1,1,1] -> ((1*256) << 1) >> 5 = 16
assert_eq!(c, [16, 16, 16, 16]);
}
#[test]
fn scale_functions() {
assert_eq!(scale4(2, 160, 24), 2 * 160);
assert_eq!(scale4(2, 160, 12), (2 * 160 + 2) >> 2);
assert_eq!(scale8(1, 320, 36), 320);
assert_eq!(scale8(1, 320, 30), (320 + 1) >> 1);
}
}

View File

@ -1,6 +1,7 @@
//! Test fixture generation (ffmpeg/libx264 as an external oracle) and comparison helpers.
#![allow(dead_code)]
use filmcraft_h264::{Decoder, Picture};
use std::path::{Path, PathBuf};
use std::process::Command;
@ -364,3 +365,89 @@ pub fn split_access_units(data: &[u8]) -> Vec<&[u8]> {
}
aus
}
/// Decode a whole Annex-B file access unit by access unit (pts = AU index).
/// Failed decode: (access unit index, error, pictures output before the error).
pub type DecodeFailure = (usize, filmcraft_h264::Error, Vec<Picture>);
pub fn decode_file(path: &Path) -> Result<Vec<Picture>, DecodeFailure> {
let data = std::fs::read(path).unwrap();
let mut dec = Decoder::new();
let mut out = Vec::new();
for (i, au) in split_access_units(&data).into_iter().enumerate() {
match dec.decode(au, i as i64) {
Ok(p) => out.extend(p),
Err(e) => return Err((i, e, out)),
}
}
out.extend(dec.flush());
Ok(out)
}
/// Compare decoded pictures with a raw yuv420p reference; returns a diagnostic on the first mismatch.
pub fn compare(pics: &[Picture], reference: &[u8], w: usize, h: usize) -> Result<(), String> {
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let fsize = w * h + 2 * cw * ch;
let nref = reference.len() / fsize;
for (i, p) in pics.iter().enumerate() {
if i >= nref {
return Err(format!("decoder produced {} frames, reference has {}", pics.len(), nref));
}
if p.width as usize != w || p.height as usize != h {
return Err(format!("frame {i}: size {}x{} != {}x{}", p.width, p.height, w, h));
}
let f = &reference[i * fsize..(i + 1) * fsize];
let planes =
[("Y", &p.y[..], &f[..w * h], w, h, 16), ("U", &p.u[..], &f[w * h..w * h + cw * ch], cw, ch, 8), ("V", &p.v[..], &f[w * h + cw * ch..], cw, ch, 8)];
for (name, got, exp, pw, ph, mbs) in planes {
if got != exp {
let mut first = None;
let mut count = 0usize;
let mut maxd = 0i32;
for y in 0..ph {
for x in 0..pw {
let (a, b) = (got[y * pw + x], exp[y * pw + x]);
if a != b {
count += 1;
maxd = maxd.max((a as i32 - b as i32).abs());
if first.is_none() {
first = Some((x, y, a, b));
}
}
}
}
let (x, y, a, b) = first.unwrap();
let mb_w = pw.div_ceil(mbs);
return Err(format!(
"frame {i} (poc {}, pts {}) plane {name}: first mismatch at ({x},{y}) MB {} (mb_x {}, mb_y {}): got {a} want {b}; {count} samples differ, max diff {maxd}",
p.poc,
p.pts,
(y / mbs) * mb_w + x / mbs,
x / mbs,
y / mbs
));
}
}
}
if pics.len() != nref {
return Err(format!("decoder produced {} frames, reference has {}", pics.len(), nref));
}
Ok(())
}
/// Full check of one fixture: generate, decode, compare. Returns Ok(false) when skipped.
pub fn check_fixture(name: &str) -> Result<bool, String> {
let f = fixture(name);
let Some((h264, yuv)) = ensure(f) else { return Ok(false) };
let reference = std::fs::read(&yuv).unwrap();
let pics = match decode_file(&h264) {
Ok(p) => p,
Err((au, e, partial)) => {
// still report comparison of what was decoded
let cmp = compare(&partial, &reference, f.width as usize, f.height as usize).err().unwrap_or_default();
return Err(format!("{name}: decode error at access unit {au}: {e} (after {} pictures) {cmp}", partial.len()));
}
};
compare(&pics, &reference, f.width as usize, f.height as usize).map_err(|e| format!("{name}: {e}"))?;
Ok(true)
}

View File

@ -0,0 +1,20 @@
//! Bit-exactness against ffmpeg's decoder on libx264-generated fixtures (skipped when ffmpeg is absent).
mod common;
macro_rules! fixture_tests {
($($name:ident),* $(,)?) => {
$(
#[test]
fn $name() {
match common::check_fixture(stringify!($name)) {
Ok(true) => {}
Ok(false) => eprintln!("skipped {}", stringify!($name)),
Err(e) => panic!("{e}"),
}
}
)*
};
}
fixture_tests!(intra_cavlc, intra_cavlc_noise, intra_cavlc_8x8, intra_cavlc_cqm, p_cavlc_nodeblock,);