M13.4: hevcenc I/P encoder (CABAC, RD CU decisions, intra/merge/AMVP), filmcraft + ffmpeg bit-exact round trips

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-03 05:05:28 -04:00
co-authored by Claude Opus 5.5
parent 280f7d509c
commit 7f97c25c3e
11 changed files with 3577 additions and 9 deletions
+135
View File
@@ -0,0 +1,135 @@
//! RBSP bit writer (7.2, 9.2 Exp-Golomb) and NAL unit encapsulation with emulation prevention (7.3.1).
/// MSB-first bit writer.
#[derive(Default)]
pub struct BitWriter {
buf: Vec<u8>,
acc: u64,
nbits: u32,
}
impl BitWriter {
pub fn new() -> Self {
Self::default()
}
/// Write the `n` (<= 32) low bits of `v`.
#[inline]
pub fn put(&mut self, v: u32, n: u32) {
debug_assert!(n <= 32);
if n == 0 {
return;
}
let v = (v as u64) & ((1u64 << n) - 1);
self.acc = (self.acc << n) | v;
self.nbits += n;
while self.nbits >= 8 {
self.nbits -= 8;
self.buf.push((self.acc >> self.nbits) as u8);
}
self.acc &= (1u64 << self.nbits) - 1;
}
#[inline]
pub fn bit(&mut self, b: u32) {
self.put(b & 1, 1);
}
pub fn flag(&mut self, b: bool) {
self.put(b as u32, 1);
}
/// ue(v).
pub fn ue(&mut self, v: u32) {
let x = v as u64 + 1;
let len = 64 - x.leading_zeros(); // bits of x
// len - 1 leading zeros, then x in len bits
self.put(0, len - 1);
if len > 32 {
self.put((x >> 32) as u32, len - 32);
self.put(x as u32, 32);
} else {
self.put(x as u32, len);
}
}
/// se(v).
pub fn se(&mut self, v: i32) {
let k = if v > 0 { 2 * v as u32 - 1 } else { (-(v as i64) as u32) * 2 };
self.ue(k);
}
pub fn is_aligned(&self) -> bool {
self.nbits == 0
}
/// rbsp_trailing_bits(): stop bit and zero alignment.
pub fn trailing(&mut self) {
self.bit(1);
self.align_zero();
}
pub fn align_zero(&mut self) {
if self.nbits > 0 {
self.put(0, 8 - self.nbits);
}
}
pub fn into_bytes(mut self) -> Vec<u8> {
self.align_zero();
self.buf
}
}
/// Build a NAL unit (2-byte header, `nuh_layer_id` 0, `TemporalId` 0) from an RBSP, inserting
/// emulation prevention bytes.
pub fn nal_unit(nal_type: u8, rbsp: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(rbsp.len() + rbsp.len() / 64 + 4);
out.push(nal_type << 1);
out.push(1);
let mut zeros = 0;
for &b in rbsp {
if zeros >= 2 && b <= 3 {
out.push(3);
zeros = 0;
}
out.push(b);
if b == 0 {
zeros += 1;
} else {
zeros = 0;
}
}
// cabac_zero_words / trailing zero bytes never occur: every RBSP ends with the stop bit.
out
}
pub const NAL_TRAIL_R: u8 = 1;
pub const NAL_IDR_N_LP: u8 = 20;
pub const NAL_VPS: u8 = 32;
pub const NAL_SPS: u8 = 33;
pub const NAL_PPS: u8 = 34;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exp_golomb() {
let mut w = BitWriter::new();
w.ue(0); // 1
w.ue(1); // 010
w.ue(2); // 011
w.ue(3); // 00100
w.se(-1); // ue(2) = 011
w.trailing();
// 1 010 011 00100 011 + stop bit: 1010 0110 0100 0111
assert_eq!(w.into_bytes(), vec![0xa6, 0x47]);
}
#[test]
fn emulation_prevention() {
let n = nal_unit(1, &[0, 0, 1, 0, 0, 0, 0, 0, 4]);
assert_eq!(&n[2..], &[0, 0, 3, 1, 0, 0, 3, 0, 0, 3, 0, 4]);
}
}
+220
View File
@@ -0,0 +1,220 @@
//! CABAC: context initialisation (9.3.2.2), the arithmetic encoder (9.3.4.x, informative encoder
//! description: EncodeDecision, EncodeBypass, EncodeTerminate, EncodeFlush) and a fractional-bit
//! estimator with the same context-state evolution, used for rate-distortion decisions.
use crate::bitstream::BitWriter;
use crate::tables::{CABAC_INIT, NEXT_STATE, NUM_CTX, RANGE_TAB_LPS};
use std::sync::OnceLock;
/// Context states, each `pStateIdx << 1 | valMps`.
pub type Contexts = [u8; NUM_CTX];
/// Initialise all context variables for `slice_qp` and `init_type` (9.3.2.2).
pub fn init_contexts(slice_qp: i32, init_type: usize) -> Contexts {
let mut ctx = [0u8; NUM_CTX];
let qp = slice_qp.clamp(0, 51);
for (c, &v) in ctx.iter_mut().zip(CABAC_INIT[init_type].iter()) {
let slope = (v >> 4) as i32;
let offset = (v & 15) as i32;
let m = slope * 5 - 45;
let n = (offset << 3) - 16;
let pre = (((m * qp) >> 4) + n).clamp(1, 126);
*c = if pre <= 63 { ((63 - pre) << 1) as u8 } else { (((pre - 64) << 1) | 1) as u8 };
}
ctx
}
/// Destination of binarised syntax elements: the real arithmetic coder or the bit estimator.
pub trait Sink {
fn decision(&mut self, ctx: usize, bin: u32);
fn bypass(&mut self, bin: u32);
/// `n` bypass bins carrying the low `n` bits of `v`, MSB first.
fn bypass_bits(&mut self, v: u32, n: u32) {
for i in (0..n).rev() {
self.bypass((v >> i) & 1);
}
}
fn terminate(&mut self, bin: u32);
}
/// Arithmetic encoder writing into a bit writer.
pub struct CabacEncoder {
pub ctx: Contexts,
low: u32,
range: u32,
outstanding: u32,
first: bool,
pub w: BitWriter,
}
impl CabacEncoder {
/// Start encoding slice data into `w` (which must be byte aligned).
pub fn new(w: BitWriter, ctx: Contexts) -> Self {
debug_assert!(w.is_aligned());
CabacEncoder { ctx, low: 0, range: 510, outstanding: 0, first: true, w }
}
#[inline]
fn put_bit(&mut self, b: u32) {
if self.first {
self.first = false;
} else {
self.w.bit(b);
}
while self.outstanding > 0 {
self.w.bit(1 - b);
self.outstanding -= 1;
}
}
#[inline]
fn renorm(&mut self) {
while self.range < 256 {
if self.low < 256 {
self.put_bit(0);
} else if self.low >= 512 {
self.low -= 512;
self.put_bit(1);
} else {
self.low -= 256;
self.outstanding += 1;
}
self.range <<= 1;
self.low <<= 1;
}
}
/// EncodeFlush after a terminating bin equal to 1; the final bit written is the
/// rbsp_stop_one_bit. Returns the writer (byte aligned with zero bits).
pub fn finish(mut self) -> BitWriter {
self.range = 2;
self.renorm();
let b = (self.low >> 9) & 1;
self.put_bit(b);
self.w.put(((self.low >> 7) & 3) | 1, 2);
self.w.align_zero();
self.w
}
}
impl Sink for CabacEncoder {
#[inline]
fn decision(&mut self, ctx_idx: usize, bin: u32) {
let s = self.ctx[ctx_idx] as usize;
let mps = (s & 1) as u32;
let q = ((self.range >> 6) & 3) as usize;
let lps = RANGE_TAB_LPS[s >> 1][q] as u32;
self.range -= lps;
if bin != mps {
self.low += self.range;
self.range = lps;
self.ctx[ctx_idx] = NEXT_STATE[s][1];
} else {
self.ctx[ctx_idx] = NEXT_STATE[s][0];
}
self.renorm();
}
#[inline]
fn bypass(&mut self, bin: u32) {
self.low <<= 1;
if bin != 0 {
self.low += self.range;
}
if self.low >= 1024 {
self.put_bit(1);
self.low -= 1024;
} else if self.low < 512 {
self.put_bit(0);
} else {
self.low -= 512;
self.outstanding += 1;
}
}
fn terminate(&mut self, bin: u32) {
self.range -= 2;
if bin != 0 {
self.low += self.range;
// the caller flushes with finish()
} else {
self.renorm();
}
}
}
/// Fractional bit cost (1/32768 bit units) of coding the MPS / LPS in each state.
fn cost_table() -> &'static [[u32; 2]; 64] {
static T: OnceLock<[[u32; 2]; 64]> = OnceLock::new();
T.get_or_init(|| {
let mut t = [[0u32; 2]; 64];
// pLPS(s) = 0.5 * alpha^s with alpha = (0.01875 / 0.5)^(1/63) (9.3.4.3 design)
let alpha = (0.01875f64 / 0.5).powf(1.0 / 63.0);
for (s, e) in t.iter_mut().enumerate() {
let p = 0.5 * alpha.powi(s as i32);
e[0] = (-(1.0 - p).log2() * 32768.0).round() as u32;
e[1] = (-p.log2() * 32768.0).round() as u32;
}
t
})
}
pub const BIT: u64 = 32768;
/// Bit estimator: tracks context states like the encoder and accumulates the ideal code length.
#[derive(Clone)]
pub struct Estimator {
pub ctx: Contexts,
/// Accumulated cost in 1/32768 bits.
pub bits: u64,
table: &'static [[u32; 2]; 64],
}
impl Estimator {
pub fn new(ctx: Contexts) -> Self {
Estimator { ctx, bits: 0, table: cost_table() }
}
}
impl Sink for Estimator {
#[inline]
fn decision(&mut self, ctx_idx: usize, bin: u32) {
let s = self.ctx[ctx_idx] as usize;
let lps = (bin != (s & 1) as u32) as usize;
self.bits += self.table[s >> 1][lps] as u64;
self.ctx[ctx_idx] = NEXT_STATE[s][lps];
}
#[inline]
fn bypass(&mut self, _bin: u32) {
self.bits += BIT;
}
#[inline]
fn bypass_bits(&mut self, _v: u32, n: u32) {
self.bits += BIT * n as u64;
}
fn terminate(&mut self, _bin: u32) {
self.bits += 7; // ~0 bits for a 0 bin
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn context_init_formula() {
let ctx = init_contexts(30, 0);
// initValue 154: m = 0, n = 64 -> pStateIdx 0, valMps 1
assert_eq!(ctx[crate::tables::CU_TRANSQUANT_BYPASS], 1);
// split_cu_flag ctx 0 (initValue 139) at QP 30: preCtxState 62
assert_eq!(ctx[crate::tables::SPLIT_CU], (63 - 62) << 1);
}
#[test]
fn costs_are_sane() {
let t = cost_table();
assert_eq!(t[0][0], 32768);
assert_eq!(t[0][1], 32768);
assert!(t[62][0] < 1000 && t[62][1] > 5 * 32768);
}
}
File diff suppressed because it is too large Load Diff
+156
View File
@@ -0,0 +1,156 @@
//! Fractional sample interpolation (8.5.3.3.3) and default weighted prediction (8-262), mirrored
//! from the filmcraft-hevc decoder. Reference sample fetches clamp to the picture (8-228, 8-229).
use crate::tables::{CHROMA_FILTER, LUMA_FILTER};
/// Max prediction block size + filter margin.
const WIN: usize = 64 + 7;
/// One plane of a reference picture.
#[derive(Clone, Copy)]
pub struct PlaneRef<'a> {
pub data: &'a [u16],
pub w: usize,
pub h: usize,
}
impl PlaneRef<'_> {
/// Copy a w x h window at (x0, y0) with edge replication.
fn window(&self, x0: i32, y0: i32, w: usize, h: usize, out: &mut [i16], os: usize) {
let (pw, ph) = (self.w as i32, self.h as i32);
let inside_x = x0 >= 0 && x0 + w as i32 <= pw;
for r in 0..h {
let yy = (y0 + r as i32).clamp(0, ph - 1) as usize;
let row = &self.data[yy * self.w..yy * self.w + self.w];
let dst = &mut out[r * os..r * os + w];
if inside_x {
for (d, &s) in dst.iter_mut().zip(&row[x0 as usize..x0 as usize + w]) {
*d = s as i16;
}
} else {
for (c, d) in dst.iter_mut().enumerate() {
*d = row[(x0 + c as i32).clamp(0, pw - 1) as usize] as i16;
}
}
}
}
}
/// Luma prediction samples (14-bit intermediate) of a w x h block at (x, y) displaced by the
/// quarter-sample vector `mv`, into `out` (stride w).
pub fn mc_luma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], w: usize, h: usize, out: &mut [i16]) {
let (fx, fy) = ((mv[0] & 3) as usize, (mv[1] & 3) as usize);
let xi = x + (mv[0] as i32 >> 2);
let yi = y + (mv[1] as i32 >> 2);
let shift1 = bd.min(12) - 8;
let shift3 = 14 - bd;
let mut win = [0i16; WIN * WIN];
if fx == 0 && fy == 0 {
f.window(xi, yi, w, h, &mut win, WIN);
for r in 0..h {
for c in 0..w {
out[r * w + c] = win[r * WIN + c] << shift3;
}
}
return;
}
f.window(xi - 3, yi - 3, w + 7, h + 7, &mut win, WIN);
let hf = &LUMA_FILTER[fx];
let vf = &LUMA_FILTER[fy];
if fy == 0 {
for r in 0..h {
let row = &win[(r + 3) * WIN..];
for c in 0..w {
let s: i32 = (0..8).map(|k| hf[k] as i32 * row[c + k] as i32).sum();
out[r * w + c] = (s >> shift1) as i16;
}
}
} else if fx == 0 {
for r in 0..h {
for c in 0..w {
let s: i32 = (0..8).map(|k| vf[k] as i32 * win[(r + k) * WIN + c + 3] as i32).sum();
out[r * w + c] = (s >> shift1) as i16;
}
}
} else {
let mut tmp = [0i16; WIN * WIN];
for r in 0..h + 7 {
let row = &win[r * WIN..];
for c in 0..w {
let s: i32 = (0..8).map(|k| hf[k] as i32 * row[c + k] as i32).sum();
tmp[r * WIN + c] = (s >> shift1) as i16;
}
}
for r in 0..h {
for c in 0..w {
let s: i32 = (0..8).map(|k| vf[k] as i32 * tmp[(r + k) * WIN + c] as i32).sum();
out[r * w + c] = (s >> 6) as i16;
}
}
}
}
/// Chroma prediction samples of a w x h block at chroma position (x, y) with a 1/8-sample vector.
pub fn mc_chroma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], w: usize, h: usize, out: &mut [i16]) {
let (fx, fy) = ((mv[0] & 7) as usize, (mv[1] & 7) as usize);
let xi = x + (mv[0] as i32 >> 3);
let yi = y + (mv[1] as i32 >> 3);
let shift1 = bd.min(12) - 8;
let shift3 = 14 - bd;
let mut win = [0i16; WIN * WIN];
if fx == 0 && fy == 0 {
f.window(xi, yi, w, h, &mut win, WIN);
for r in 0..h {
for c in 0..w {
out[r * w + c] = win[r * WIN + c] << shift3;
}
}
return;
}
f.window(xi - 1, yi - 1, w + 3, h + 3, &mut win, WIN);
let hf = &CHROMA_FILTER[fx];
let vf = &CHROMA_FILTER[fy];
if fy == 0 {
for r in 0..h {
let row = &win[(r + 1) * WIN..];
for c in 0..w {
let s: i32 = (0..4).map(|k| hf[k] as i32 * row[c + k] as i32).sum();
out[r * w + c] = (s >> shift1) as i16;
}
}
} else if fx == 0 {
for r in 0..h {
for c in 0..w {
let s: i32 = (0..4).map(|k| vf[k] as i32 * win[(r + k) * WIN + c + 1] as i32).sum();
out[r * w + c] = (s >> shift1) as i16;
}
}
} else {
let mut tmp = [0i16; WIN * WIN];
for r in 0..h + 3 {
let row = &win[r * WIN..];
for c in 0..w {
let s: i32 = (0..4).map(|k| hf[k] as i32 * row[c + k] as i32).sum();
tmp[r * WIN + c] = (s >> shift1) as i16;
}
}
for r in 0..h {
for c in 0..w {
let s: i32 = (0..4).map(|k| vf[k] as i32 * tmp[(r + k) * WIN + c] as i32).sum();
out[r * w + c] = (s >> 6) as i16;
}
}
}
}
/// Default weighted prediction, one list (8-262): 14-bit samples to `bd`-bit samples.
pub fn put_uni(p: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u16], ds: usize) {
let shift = 14 - bd;
let off = if shift > 0 { 1 << (shift - 1) } else { 0 };
let max = (1i32 << bd) - 1;
for r in 0..h {
for c in 0..w {
dst[r * ds + c] = ((p[r * w + c] as i32 + off) >> shift).clamp(0, max) as u16;
}
}
}
+254
View File
@@ -0,0 +1,254 @@
//! Intra sample prediction (8.4.4.2), mirrored from the filmcraft-hevc decoder so the encoder
//! reconstructs exactly like a decoder: reference substitution, filtering (incl. strong intra
//! smoothing), planar, DC and the 33 angular modes.
use crate::tables::{INTRA_PRED_ANGLE, INV_ANGLE};
/// Reference samples of an nTbS block: `left[0]` = `top[0]` = p[-1][-1], `left[1 + y]` = p[-1][y],
/// `top[1 + x]` = p[x][-1] for x, y = 0..2*nTbS-1.
#[derive(Clone)]
pub struct Refs {
pub left: [u16; 129],
pub top: [u16; 129],
}
impl Default for Refs {
fn default() -> Self {
Refs { left: [0; 129], top: [0; 129] }
}
}
/// Substitution process (8.4.4.2.2). `avail_left[1 + y]` / `avail_top[1 + x]` (index 0 = corner) tell
/// which samples are available; unavailable ones are replaced.
pub fn substitute(r: &mut Refs, avail_left: &[bool; 129], avail_top: &[bool; 129], n: usize, bit_depth: u32) {
let n2 = 2 * n;
// Sequence order: p[-1][2n-1] .. p[-1][-1], p[0][-1] .. p[2n-1][-1]
let any = avail_left[..=n2].iter().any(|&a| a) || avail_top[1..=n2].iter().any(|&a| a);
if !any {
let v = 1u16 << (bit_depth - 1);
r.left[..=n2].fill(v);
r.top[..=n2].fill(v);
return;
}
if !avail_left[n2] {
// search upwards then along the top
let mut found = None;
for y in (0..n2).rev() {
if avail_left[y] {
found = Some(r.left[y]);
break;
}
}
if found.is_none() {
for x in 1..=n2 {
if avail_top[x] {
found = Some(r.top[x]);
break;
}
}
}
r.left[n2] = found.unwrap_or(0);
}
for y in (0..n2).rev() {
if !avail_left[y] {
r.left[y] = r.left[y + 1];
}
}
r.top[0] = r.left[0];
for x in 1..=n2 {
if !avail_top[x] {
r.top[x] = r.top[x - 1];
}
}
}
/// Filtering process of neighbouring samples (8.4.4.2.3) for luma (and 4:4:4 chroma).
pub fn filter(r: &mut Refs, mode: u32, n: usize, strong_enabled: bool, is_luma: bool, bit_depth: u32) {
if mode == 1 || n == 4 {
return;
}
let min_dist = (mode as i32 - 26).abs().min((mode as i32 - 10).abs());
let thres = match n {
8 => 7,
16 => 1,
_ => 0,
};
if min_dist <= thres {
return;
}
let n2 = 2 * n;
let c = r.left[0] as i32;
if strong_enabled && is_luma && n == 32 {
let (bl, tr) = (r.left[n2] as i32, r.top[n2] as i32);
let thr = 1 << (bit_depth - 5);
if (c + tr - 2 * r.top[n] as i32).abs() < thr && (c + bl - 2 * r.left[n] as i32).abs() < thr {
for i in 0..63 {
r.left[1 + i] = (((63 - i as i32) * c + (i as i32 + 1) * bl + 32) >> 6) as u16;
r.top[1 + i] = (((63 - i as i32) * c + (i as i32 + 1) * tr + 32) >> 6) as u16;
}
return;
}
}
let (l, t) = (r.left, r.top);
let corner = ((l[1] as u32 + 2 * c as u32 + t[1] as u32 + 2) >> 2) as u16;
for i in 1..n2 {
r.left[i] = ((l[i - 1] as u32 + 2 * l[i] as u32 + l[i + 1] as u32 + 2) >> 2) as u16;
r.top[i] = ((t[i - 1] as u32 + 2 * t[i] as u32 + t[i + 1] as u32 + 2) >> 2) as u16;
}
r.left[0] = corner;
r.top[0] = corner;
}
/// Predict an n x n block with `mode` into `out` (stride `os`). `edge_filters`: DC / horizontal /
/// vertical boundary smoothing (luma blocks smaller than 32x32).
pub fn predict(r: &Refs, mode: u32, n: usize, edge_filters: bool, bit_depth: u32, out: &mut [u16], os: usize) {
let max = (1i32 << bit_depth) - 1;
match mode {
0 => {
// planar
let log2 = n.trailing_zeros();
let tr = r.top[1 + n] as i32;
let bl = r.left[1 + n] as i32;
for y in 0..n {
let l = r.left[1 + y] as i32;
for x in 0..n {
let t = r.top[1 + x] as i32;
let v = ((n - 1 - x) as i32 * l + (x as i32 + 1) * tr + (n - 1 - y) as i32 * t + (y as i32 + 1) * bl + n as i32) >> (log2 + 1);
out[y * os + x] = v as u16;
}
}
}
1 => {
let log2 = n.trailing_zeros();
let sum: u32 = r.top[1..=n].iter().map(|&v| v as u32).sum::<u32>() + r.left[1..=n].iter().map(|&v| v as u32).sum::<u32>();
let dc = ((sum + n as u32) >> (log2 + 1)) as u16;
for y in 0..n {
out[y * os..y * os + n].fill(dc);
}
if edge_filters && n < 32 {
let d = dc as u32;
out[0] = ((r.left[1] as u32 + 2 * d + r.top[1] as u32 + 2) >> 2) as u16;
for x in 1..n {
out[x] = ((r.top[1 + x] as u32 + 3 * d + 2) >> 2) as u16;
}
for y in 1..n {
out[y * os] = ((r.left[1 + y] as u32 + 3 * d + 2) >> 2) as u16;
}
}
}
_ => {
let angle = INTRA_PRED_ANGLE[mode as usize] as i32;
// reference array ref[-n..=2n] stored at offset n
let mut refa = [0i32; 3 * 64 + 1];
let base = n as i32;
let (main, side) = if mode >= 18 { (&r.top, &r.left) } else { (&r.left, &r.top) };
for x in 0..=n {
refa[(base + x as i32) as usize] = main[x] as i32; // ref[x] = p[-1+x][-1]
}
if angle < 0 {
let lim = (n as i32 * angle) >> 5;
if lim < -1 {
let inv = INV_ANGLE[(mode - 11) as usize] as i32;
for x in lim..=-1 {
let idx = -1 + ((x * inv + 128) >> 8);
refa[(base + x) as usize] = side[(idx + 1) as usize] as i32;
}
}
} else {
for x in n + 1..=2 * n {
refa[(base + x as i32) as usize] = main[x] as i32;
}
}
let horizontal = mode < 18;
for j in 0..n {
let pos = (j as i32 + 1) * angle;
let idx = pos >> 5;
let fact = pos & 31;
for i in 0..n {
let k = (base + i as i32 + idx + 1) as usize;
let v = if fact != 0 { ((32 - fact) * refa[k] + fact * refa[k + 1] + 16) >> 5 } else { refa[k] };
if horizontal {
out[i * os + j] = v as u16;
} else {
out[j * os + i] = v as u16;
}
}
}
if edge_filters && n < 32 {
if mode == 26 {
for y in 0..n {
let v = r.top[1] as i32 + ((r.left[1 + y] as i32 - r.left[0] as i32) >> 1);
out[y * os] = v.clamp(0, max) as u16;
}
} else if mode == 10 {
for x in 0..n {
let v = r.left[1] as i32 + ((r.top[1 + x] as i32 - r.top[0] as i32) >> 1);
out[x] = v.clamp(0, max) as u16;
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn refs(f: impl Fn(i32) -> u16) -> Refs {
let mut r = Refs::default();
r.left[0] = f(0);
r.top[0] = f(0);
for i in 1..129 {
r.left[i] = f(-(i as i32));
r.top[i] = f(i as i32);
}
r
}
#[test]
fn dc_and_planar_flat() {
let r = refs(|_| 100);
let mut out = [0u16; 64];
predict(&r, 1, 8, true, 8, &mut out, 8);
assert!(out.iter().all(|&v| v == 100));
predict(&r, 0, 8, true, 8, &mut out, 8);
assert!(out.iter().all(|&v| v == 100));
}
#[test]
fn pure_vertical_and_horizontal() {
let r = refs(|i| (128 + i) as u16);
let mut out = [0u16; 16];
predict(&r, 26, 4, false, 8, &mut out, 4);
for y in 0..4 {
for x in 0..4 {
assert_eq!(out[y * 4 + x], r.top[1 + x]);
}
}
predict(&r, 10, 4, false, 8, &mut out, 4);
for y in 0..4 {
for x in 0..4 {
assert_eq!(out[y * 4 + x], r.left[1 + y]);
}
}
// diagonal mode 34 reads top[x + y + 2]
predict(&r, 34, 4, false, 8, &mut out, 4);
assert_eq!(out[4 + 1], r.top[1 + 1 + 1 + 1]);
}
#[test]
fn substitution() {
let mut r = Refs::default();
let mut al = [false; 129];
let at = [false; 129];
r.left[3] = 77;
al[3] = true;
substitute(&mut r, &al, &at, 4, 8);
assert!(r.left[..=8].iter().all(|&v| v == 77));
assert!(r.top[..=8].iter().all(|&v| v == 77));
let mut r = Refs::default();
substitute(&mut r, &[false; 129], &[false; 129], 4, 10);
assert_eq!(r.top[5], 512);
}
}
+235 -9
View File
@@ -1,6 +1,36 @@
//! Clean-room pure-Rust HEVC (H.265) encoder — API skeleton (implementation in progress).
//! Clean-room pure-Rust HEVC (H.265) encoder, implemented from ITU-T Rec. H.265 (ISO/IEC 23008-2).
//!
//! The public API below is the contract `crates/export` codes against; keep it stable.
//! Main and Main 10 profiles, 4:2:0, one slice per picture: IDR pictures and single-reference P
//! pictures (no reordering), 64x64 CTUs with rate-distortion coding-unit decisions (8x8 .. 64x64),
//! intra (35 modes, NxN at 8x8), merge / skip and AMVP inter prediction with quarter-sample motion,
//! DCT / DST transforms and CABAC. In-loop filters (deblocking, SAO) are disabled.
//!
//! ```
//! use effectcraft_hevcenc::{Encoder, EncoderConfig, Frame};
//! let mut enc = Encoder::new(EncoderConfig::new(64, 48, 30, 1)).unwrap();
//! let (y, c) = (vec![128u16; 64 * 48], vec![128u16; 32 * 24]);
//! let packet = enc.encode(&Frame { y: &y, u: &c, v: &c, y_stride: 64, uv_stride: 32 });
//! assert!(packet.keyframe && !packet.data.is_empty());
//! let _hvcc = enc.hvcc();
//! ```
// Index loops over fixed-size blocks read more clearly than iterator chains in codec code.
#![allow(clippy::needless_range_loop, clippy::too_many_arguments)]
mod bitstream;
mod cabac;
mod frame;
mod inter;
mod intra;
mod me;
mod params;
#[rustfmt::skip]
mod tables;
mod transform;
use bitstream::{BitWriter, NAL_IDR_N_LP, NAL_TRAIL_R, nal_unit};
use frame::{FrameCoder, SliceKind};
use params::{LOG2_MAX_POC_LSB, LOG2_MIN_CB, MAX_MERGE_CAND, SeqParams};
/// Profile written in the parameter sets.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -19,6 +49,12 @@ impl Profile {
Profile::Main10 => 10,
}
}
fn idc(self) -> u8 {
match self {
Profile::Main => 1,
Profile::Main10 => 2,
}
}
}
/// Rate control.
@@ -33,6 +69,7 @@ pub enum RateControl {
/// Encoder settings.
#[derive(Clone, Debug, PartialEq)]
pub struct EncoderConfig {
/// Picture size; must be even (4:2:0 cropping is in units of two luma samples).
pub width: u32,
pub height: u32,
/// Frame rate numerator / denominator (VUI timing and rate control).
@@ -42,7 +79,7 @@ pub struct EncoderConfig {
/// `general_level_idc` (30 × level, e.g. 93 = level 3.1); `None` picks the lowest level that fits.
pub level_idc: Option<u8>,
pub rate: RateControl,
/// Frames between IDR pictures (1 = all intra).
/// Frames between IDR pictures (1 = all intra, 0 = only the first picture).
pub keyint: u32,
/// BT.709 limited range is signalled in the VUI unless this is set.
pub full_range: bool,
@@ -65,6 +102,7 @@ impl EncoderConfig {
}
/// One input picture, 4:2:0, samples at the profile's bit depth (8-bit values 0–255 for Main).
/// Luma is `width` x `height`, chroma `width / 2` x `height / 2`.
#[derive(Clone, Copy, Debug)]
pub struct Frame<'a> {
pub y: &'a [u16],
@@ -98,9 +136,67 @@ impl std::fmt::Display for Error {
impl std::error::Error for Error {}
/// Frame-level rate control for [`RateControl::Bitrate`]: a bits ≈ X / Qstep model per picture type
/// and a buffer that spreads over- / undershoot over the following pictures.
struct RateCtl {
target: f64,
/// sum(actual) - sum(target), bits.
buffer: f64,
/// Complexity X = bits * Qstep for I and P pictures.
x: [Option<f64>; 2],
last_qp: [i32; 2],
init_qp: i32,
}
fn qstep(qp: i32) -> f64 {
2f64.powf((qp as f64 - 4.0) / 6.0)
}
impl RateCtl {
fn new(kbps: u32, cfg: &EncoderConfig) -> Self {
let fps = cfg.fps_num as f64 / cfg.fps_den.max(1) as f64;
let target = kbps as f64 * 1000.0 / fps.max(0.001);
let bpp = target / (cfg.width as f64 * cfg.height as f64);
let init_qp = (28.0 - 6.0 * (bpp / 0.08).log2()).round().clamp(10.0, 46.0) as i32;
RateCtl { target, buffer: 0.0, x: [None, None], last_qp: [init_qp - 3, init_qp], init_qp }
}
fn qp(&self, idr: bool) -> i32 {
let t = idr as usize ^ 1; // 0 = I, 1 = P
let tp = (self.target - self.buffer / 12.0).clamp(0.3 * self.target, 2.0 * self.target);
let want = if idr { 4.0 * tp } else { tp };
let x = match (self.x[t], self.x[0]) {
(Some(x), _) => x,
(None, Some(xi)) => xi / 5.0,
(None, None) => return if idr { self.init_qp - 3 } else { self.init_qp },
};
let qp = (4.0 + 6.0 * (x / want).log2()).round() as i32;
qp.clamp(self.last_qp[t] - 6, self.last_qp[t] + 6).clamp(1, 51)
}
fn update(&mut self, idr: bool, qp: i32, bits: f64) {
let t = idr as usize ^ 1;
let x = bits * qstep(qp);
self.x[t] = Some(match self.x[t] {
Some(old) => 0.5 * old + 0.5 * x,
None => x,
});
self.last_qp[t] = qp;
self.buffer += bits - self.target;
}
}
/// HEVC encoder. Pictures come out in input order (no reordering: I and P only).
pub struct Encoder {
cfg: EncoderConfig,
seq: SeqParams,
sets: [Vec<u8>; 3],
frames: u64,
/// POC of the previous picture (relative to the last IDR).
poc: u32,
reference: Option<[Vec<u16>; 3]>,
recon: Option<[Vec<u16>; 3]>,
rc: Option<RateCtl>,
}
impl Encoder {
@@ -108,21 +204,151 @@ impl Encoder {
if cfg.width == 0 || cfg.height == 0 {
return Err(Error::InvalidConfig("empty frame".into()));
}
Ok(Encoder { cfg })
if !cfg.width.is_multiple_of(2) || !cfg.height.is_multiple_of(2) {
return Err(Error::InvalidConfig(format!("{}x{}: 4:2:0 HEVC needs even dimensions", cfg.width, cfg.height)));
}
if cfg.width > 16384 || cfg.height > 16384 {
return Err(Error::InvalidConfig("picture larger than 16384 samples".into()));
}
if cfg.fps_num == 0 || cfg.fps_den == 0 {
return Err(Error::InvalidConfig("zero frame rate".into()));
}
match cfg.rate {
RateControl::ConstantQp(q) if q > 51 => return Err(Error::InvalidConfig(format!("QP {q} > 51"))),
RateControl::Bitrate { kbps: 0 } => return Err(Error::InvalidConfig("zero bitrate".into())),
_ => {}
}
let min_cb = 1u32 << LOG2_MIN_CB;
let (coded_width, coded_height) = (cfg.width.div_ceil(min_cb) * min_cb, cfg.height.div_ceil(min_cb) * min_cb);
let kbps = match cfg.rate {
RateControl::Bitrate { kbps } => Some(kbps),
RateControl::ConstantQp(_) => None,
};
let level_idc = cfg.level_idc.unwrap_or_else(|| params::choose_level(coded_width, coded_height, cfg.fps_num, cfg.fps_den, kbps));
let seq = SeqParams {
coded_width,
coded_height,
width: cfg.width,
height: cfg.height,
bit_depth: cfg.profile.bit_depth() as u32,
profile_idc: cfg.profile.idc(),
level_idc,
full_range: cfg.full_range,
fps_num: cfg.fps_num,
fps_den: cfg.fps_den,
};
let sets = [params::vps(&seq), params::sps(&seq), params::pps()];
let rc = kbps.map(|k| RateCtl::new(k, &cfg));
Ok(Encoder { cfg, seq, sets, frames: 0, poc: 0, reference: None, recon: None, rc })
}
pub fn config(&self) -> &EncoderConfig {
&self.cfg
}
/// VPS, SPS, PPS NAL units (without start codes or length prefixes).
/// VPS, SPS, PPS NAL units (2-byte NAL header, emulation prevention, no start codes or length
/// prefixes).
pub fn parameter_sets(&self) -> [Vec<u8>; 3] {
[vec![], vec![], vec![]]
self.sets.clone()
}
/// HEVCDecoderConfigurationRecord (`hvcC` payload) with the parameter sets.
pub fn hvcc(&self) -> Vec<u8> {
vec![]
params::hvcc(&self.seq, &self.sets)
}
/// `general_level_idc` written in the parameter sets.
pub fn level_idc(&self) -> u8 {
self.seq.level_idc
}
/// Reconstruction of the last encoded picture (what a decoder outputs), cropped planes
/// `[Y, Cb, Cr]` with strides `width` and `width / 2`. For tests.
#[doc(hidden)]
pub fn last_reconstruction(&self) -> Option<&[Vec<u16>; 3]> {
self.recon.as_ref()
}
/// Copy the input into coded-size planes, replicating the right / bottom edges.
fn pad_input(&self, f: &Frame) -> [Vec<u16>; 3] {
let max = (1u32 << self.seq.bit_depth) - 1;
let mut out: [Vec<u16>; 3] = Default::default();
for (c, plane) in out.iter_mut().enumerate() {
let (src, stride, w, h, cw, ch) = if c == 0 {
(f.y, f.y_stride, self.seq.width, self.seq.height, self.seq.coded_width, self.seq.coded_height)
} else {
let s = if c == 1 { f.u } else { f.v };
(s, f.uv_stride, self.seq.width / 2, self.seq.height / 2, self.seq.coded_width / 2, self.seq.coded_height / 2)
};
let (w, h, cw, ch) = (w as usize, h as usize, cw as usize, ch as usize);
assert!(stride >= w && src.len() >= (h - 1) * stride + w, "HEVC encoder: plane {c} smaller than {w}x{h} (stride {stride})");
plane.reserve(cw * ch);
for y in 0..ch {
let row = &src[y.min(h - 1) * stride..][..w];
plane.extend(row.iter().map(|&v| (v as u32).min(max) as u16));
let last = *plane.last().unwrap();
plane.extend(std::iter::repeat_n(last, cw - w));
}
}
out
}
/// Encodes one picture.
pub fn encode(&mut self, _frame: &Frame) -> Packet {
Packet { data: vec![], keyframe: true }
pub fn encode(&mut self, frame: &Frame) -> Packet {
let src = self.pad_input(frame);
let keyint = self.cfg.keyint as u64;
let idr = self.reference.is_none() || (keyint > 0 && self.frames.is_multiple_of(keyint));
self.poc = if idr { 0 } else { self.poc + 1 };
let qp = match (&self.rc, self.cfg.rate) {
(Some(rc), _) => rc.qp(idr),
(None, RateControl::ConstantQp(q)) => q as i32,
(None, RateControl::Bitrate { .. }) => 28,
};
let (w, h) = (self.seq.coded_width as usize, self.seq.coded_height as usize);
let bd = self.seq.bit_depth;
let kind = if idr { SliceKind::I } else { SliceKind::P };
let me = if idr { None } else { self.reference.as_ref().map(|r| me::MeRef::new(&r[0], w, h, bd)) };
let refp = if idr { None } else { self.reference.as_ref() };
let mut fc = FrameCoder::new(bd, w, h, kind, qp, &src, refp, me.as_ref());
// slice_segment_header() (7.3.6.1)
let mut bw = BitWriter::new();
bw.put(1, 1); // first_slice_segment_in_pic_flag
if idr {
bw.put(0, 1); // no_output_of_prior_pics_flag
}
bw.ue(0); // slice_pic_parameter_set_id
bw.ue(if idr { 2 } else { 1 }); // slice_type I / P
if !idr {
bw.put(self.poc & ((1 << LOG2_MAX_POC_LSB) - 1), LOG2_MAX_POC_LSB); // slice_pic_order_cnt_lsb
bw.put(1, 1); // short_term_ref_pic_set_sps_flag (the only SPS set, no index coded)
bw.put(0, 1); // num_ref_idx_active_override_flag
bw.ue(5 - MAX_MERGE_CAND as u32); // five_minus_max_num_merge_cand
}
bw.se(qp - 26); // slice_qp_delta (init_qp = 26)
// byte_alignment()
bw.put(1, 1);
bw.align_zero();
let ctx = cabac::init_contexts(qp, if idr { 0 } else { 1 });
let mut enc = cabac::CabacEncoder::new(bw, ctx);
fc.encode(&mut enc);
let rbsp = enc.finish().into_bytes();
let nal = nal_unit(if idr { NAL_IDR_N_LP } else { NAL_TRAIL_R }, &rbsp);
let mut data = Vec::with_capacity(nal.len() + 4);
data.extend_from_slice(&(nal.len() as u32).to_be_bytes());
data.extend_from_slice(&nal);
if let Some(rc) = &mut self.rc {
rc.update(idr, qp, data.len() as f64 * 8.0);
}
// output picture: cropped reconstruction
let rec = std::mem::take(&mut fc.rec);
let (ow, oh) = (self.seq.width as usize, self.seq.height as usize);
let crop = |p: &[u16], stride: usize, w: usize, h: usize| -> Vec<u16> { (0..h).flat_map(|y| p[y * stride..y * stride + w].iter().copied()).collect() };
self.recon = Some([crop(&rec[0], w, ow, oh), crop(&rec[1], w / 2, ow / 2, oh / 2), crop(&rec[2], w / 2, ow / 2, oh / 2)]);
self.reference = Some(rec);
self.frames += 1;
Packet { data, keyframe: idr }
}
}
+144
View File
@@ -0,0 +1,144 @@
//! Motion search reference: the previous reconstructed luma plane padded by edge replication, plus
//! half-sample planes interpolated with the 8-tap half-sample filter. Quarter-sample positions are
//! approximated by averaging neighbouring half-grid samples. Used for estimation only; the coded
//! prediction always uses the normative interpolation in `inter`.
use crate::tables::LUMA_FILTER;
const MARGIN: i32 = 80;
pub struct MeRef {
/// [integer, horizontal half, vertical half, diagonal half] samples, each `stride` x `rows`.
planes: [Vec<u16>; 4],
stride: usize,
rows: usize,
w: i32,
h: i32,
}
impl MeRef {
pub fn new(y: &[u16], w: usize, h: usize, bd: u32) -> MeRef {
let m = MARGIN as usize;
let (stride, rows) = (w + 2 * m, h + 2 * m);
let mut pad = vec![0u16; stride * rows];
for r in 0..rows {
let sy = (r as i32 - MARGIN).clamp(0, h as i32 - 1) as usize;
let src = &y[sy * w..sy * w + w];
let row = &mut pad[r * stride..r * stride + stride];
row[..m].fill(src[0]);
row[m..m + w].copy_from_slice(src);
row[m + w..].fill(src[w - 1]);
}
let max = (1i32 << bd) - 1;
let f = &LUMA_FILTER[2];
let filt_h = |p: &[u16]| -> Vec<u16> {
let mut o = vec![0u16; stride * rows];
for r in 0..rows {
let row = &p[r * stride..r * stride + stride];
for x in 0..stride {
let mut s = 0i32;
for k in 0..8 {
let xx = (x as i32 - 3 + k as i32).clamp(0, stride as i32 - 1) as usize;
s += f[k] as i32 * row[xx] as i32;
}
o[r * stride + x] = ((s + 32) >> 6).clamp(0, max) as u16;
}
}
o
};
let filt_v = |p: &[u16]| -> Vec<u16> {
let mut o = vec![0u16; stride * rows];
let mut acc = vec![0i32; stride];
for r in 0..rows {
acc.fill(0);
for k in 0..8 {
let rr = (r as i32 - 3 + k as i32).clamp(0, rows as i32 - 1) as usize;
let src = &p[rr * stride..rr * stride + stride];
let coef = f[k] as i32;
for (a, &s) in acc.iter_mut().zip(src) {
*a += coef * s as i32;
}
}
for (d, &a) in o[r * stride..r * stride + stride].iter_mut().zip(&acc) {
*d = ((a + 32) >> 6).clamp(0, max) as u16;
}
}
o
};
let hp = filt_h(&pad);
let vp = filt_v(&pad);
let dp = filt_v(&hp);
MeRef { planes: [pad, hp, vp, dp], stride, rows, w: w as i32, h: h as i32 }
}
/// Integer motion vector range keeping an n x n block (plus filter margin) inside the padding.
pub fn int_range(&self, x: i32, y: i32, n: usize) -> (i32, i32, i32, i32) {
let n = n as i32;
let lo_x = -MARGIN + 4 - x;
let hi_x = self.w + MARGIN - n - 4 - x;
let lo_y = -MARGIN + 4 - y;
let hi_y = self.h + MARGIN - n - 4 - y;
(lo_x.max(-256), hi_x.min(256), lo_y.max(-256), hi_y.min(256))
}
/// SAD of the source block against the integer-position reference block at (rx, ry).
pub fn sad(&self, src: &[u16], ss: usize, rx: i32, ry: i32, n: usize) -> u32 {
let p = &self.planes[0];
let base = (ry + MARGIN) as usize * self.stride + (rx + MARGIN) as usize;
let mut s = 0u32;
for r in 0..n {
let a = &src[r * ss..r * ss + n];
let b = &p[base + r * self.stride..base + r * self.stride + n];
s += a.iter().zip(b).map(|(&x, &y)| (x as i32 - y as i32).unsigned_abs()).sum::<u32>();
}
s
}
/// Approximate luma prediction of an n x n block at (x, y) with quarter-sample vector `mv`.
pub fn approx_pred(&self, x: i32, y: i32, n: usize, mv: [i16; 2], out: &mut [u16]) {
// per axis: one or two (half-sample fraction, integer offset) taps
let axis = |v: i32| -> [(usize, i32); 2] {
if v & 1 == 0 {
let hv = v >> 1;
[((hv & 1) as usize, hv >> 1); 2]
} else {
let (a, b) = ((v - 1) >> 1, (v + 1) >> 1);
[((a & 1) as usize, a >> 1), ((b & 1) as usize, b >> 1)]
}
};
let (mx, my) = (mv[0] as i32, mv[1] as i32);
let ax = axis(mx);
let ay = axis(my);
let two = mx & 1 != 0 || my & 1 != 0;
let taps = [(ax[0], ay[0]), (ax[1], ay[1])];
let ntaps = if two { 2 } else { 1 };
let mut first = true;
for &((fx, ox), (fy, oy)) in taps.iter().take(ntaps) {
let plane = &self.planes[fx + 2 * fy];
let (bx, by) = (x + ox + MARGIN, y + oy + MARGIN);
let inside = bx >= 0 && by >= 0 && bx as usize + n <= self.stride && by as usize + n <= self.rows;
for r in 0..n {
let dst = &mut out[r * n..r * n + n];
if inside {
let o = (by as usize + r) * self.stride + bx as usize;
let src = &plane[o..o + n];
if first {
dst.copy_from_slice(src);
} else {
for (d, &s) in dst.iter_mut().zip(src) {
*d = ((*d as u32 + s as u32 + 1) >> 1) as u16;
}
}
} else {
let yy = (by + r as i32).clamp(0, self.rows as i32 - 1) as usize;
for (c, d) in dst.iter_mut().enumerate() {
let xx = (bx + c as i32).clamp(0, self.stride as i32 - 1) as usize;
let s = plane[yy * self.stride + xx];
*d = if first { s } else { ((*d as u32 + s as u32 + 1) >> 1) as u16 };
}
}
}
first = false;
}
}
}
+260
View File
@@ -0,0 +1,260 @@
//! Parameter sets (7.3.2.1 VPS, 7.3.2.2 SPS with E.2.1 VUI, 7.3.2.3 PPS), profile_tier_level (7.3.3),
//! level selection (Annex A, Table A.8) and the `hvcC` record (ISO/IEC 14496-15 §8.3.3.1).
use crate::bitstream::{BitWriter, NAL_PPS, NAL_SPS, NAL_VPS, nal_unit};
/// Fixed coding structure of the encoder.
pub const LOG2_CTB: u32 = 6;
pub const LOG2_MIN_CB: u32 = 3;
pub const LOG2_MIN_TB: u32 = 2;
pub const LOG2_MAX_TB: u32 = 5;
pub const LOG2_MAX_POC_LSB: u32 = 8;
/// MaxNumMergeCand.
pub const MAX_MERGE_CAND: usize = 5;
/// Sequence-level settings shared by the parameter-set writers.
#[derive(Clone, Debug)]
pub struct SeqParams {
/// Coded (padded) size, multiples of the minimum CB size.
pub coded_width: u32,
pub coded_height: u32,
/// Output (cropped) size.
pub width: u32,
pub height: u32,
pub bit_depth: u32,
pub profile_idc: u8,
pub level_idc: u8,
pub full_range: bool,
pub fps_num: u32,
pub fps_den: u32,
}
/// (level_idc, MaxLumaPs, MaxLumaSr, MaxBR main tier in kbit/s) of Table A.8 (and A.9 for luma rates).
const LEVELS: [(u8, u64, u64, u64); 13] = [
(30, 36_864, 552_960, 128),
(60, 122_880, 3_686_400, 1_500),
(63, 245_760, 7_372_800, 3_000),
(90, 552_960, 16_588_800, 6_000),
(93, 983_040, 33_177_600, 10_000),
(120, 2_228_224, 66_846_720, 12_000),
(123, 2_228_224, 133_693_440, 20_000),
(150, 8_912_896, 267_386_880, 25_000),
(153, 8_912_896, 534_773_760, 40_000),
(156, 8_912_896, 1_069_547_520, 60_000),
(180, 35_651_584, 1_069_547_520, 60_000),
(183, 35_651_584, 2_139_095_040, 120_000),
(186, 35_651_584, 4_278_190_080, 240_000),
];
/// Lowest level whose picture size, sample rate and (when known) bitrate limits fit.
pub fn choose_level(width: u32, height: u32, fps_num: u32, fps_den: u32, kbps: Option<u32>) -> u8 {
let ps = width as u64 * height as u64;
let sr = (ps as f64 * fps_num as f64 / fps_den.max(1) as f64).ceil() as u64;
for &(idc, max_ps, max_sr, max_br) in &LEVELS {
let max_dim = ((max_ps * 8) as f64).sqrt() as u64;
if ps <= max_ps && (width as u64) <= max_dim && (height as u64) <= max_dim && sr <= max_sr && kbps.is_none_or(|k| k as u64 <= max_br) {
return idc;
}
}
186
}
fn profile_tier_level(w: &mut BitWriter, p: &SeqParams) {
w.put(0, 2); // general_profile_space
w.put(0, 1); // general_tier_flag (Main tier)
w.put(p.profile_idc as u32, 5);
w.put(compat_flags(p.profile_idc), 32);
w.put(1, 1); // general_progressive_source_flag
w.put(0, 1); // general_interlaced_source_flag
w.put(0, 1); // general_non_packed_constraint_flag
w.put(1, 1); // general_frame_only_constraint_flag
w.put(0, 32); // general_reserved_zero_43bits ...
w.put(0, 11);
w.put(0, 1); // general_inbld_flag / reserved
w.put(p.level_idc as u32, 8);
// sps_max_sub_layers_minus1 == 0: no sub-layer flags
}
/// general_profile_compatibility_flag[j] as a 32-bit word (flag 0 is the MSB). A Main stream is also
/// Main 10 compatible.
fn compat_flags(profile_idc: u8) -> u32 {
let mut f = 1u32 << (31 - profile_idc as u32);
if profile_idc == 1 {
f |= 1 << (31 - 2);
}
f
}
pub fn vps(p: &SeqParams) -> Vec<u8> {
let mut w = BitWriter::new();
w.put(0, 4); // vps_video_parameter_set_id
w.put(1, 1); // vps_base_layer_internal_flag
w.put(1, 1); // vps_base_layer_available_flag
w.put(0, 6); // vps_max_layers_minus1
w.put(0, 3); // vps_max_sub_layers_minus1
w.put(1, 1); // vps_temporal_id_nesting_flag
w.put(0xffff, 16);
profile_tier_level(&mut w, p);
w.put(1, 1); // vps_sub_layer_ordering_info_present_flag
w.ue(1); // vps_max_dec_pic_buffering_minus1
w.ue(0); // vps_max_num_reorder_pics
w.ue(0); // vps_max_latency_increase_plus1
w.put(0, 6); // vps_max_layer_id
w.ue(0); // vps_num_layer_sets_minus1
w.put(0, 1); // vps_timing_info_present_flag
w.put(0, 1); // vps_extension_flag
w.trailing();
nal_unit(NAL_VPS, &w.into_bytes())
}
pub fn sps(p: &SeqParams) -> Vec<u8> {
let mut w = BitWriter::new();
w.put(0, 4); // sps_video_parameter_set_id
w.put(0, 3); // sps_max_sub_layers_minus1
w.put(1, 1); // sps_temporal_id_nesting_flag
profile_tier_level(&mut w, p);
w.ue(0); // sps_seq_parameter_set_id
w.ue(1); // chroma_format_idc 4:2:0
w.ue(p.coded_width);
w.ue(p.coded_height);
let crop = p.coded_width != p.width || p.coded_height != p.height;
w.flag(crop); // conformance_window_flag
if crop {
// offsets in chroma sample units (SubWidthC = SubHeightC = 2)
w.ue(0);
w.ue((p.coded_width - p.width) / 2);
w.ue(0);
w.ue((p.coded_height - p.height) / 2);
}
w.ue(p.bit_depth - 8); // bit_depth_luma_minus8
w.ue(p.bit_depth - 8); // bit_depth_chroma_minus8
w.ue(LOG2_MAX_POC_LSB - 4);
w.put(1, 1); // sps_sub_layer_ordering_info_present_flag
w.ue(1); // sps_max_dec_pic_buffering_minus1
w.ue(0); // sps_max_num_reorder_pics
w.ue(0); // sps_max_latency_increase_plus1
w.ue(LOG2_MIN_CB - 3);
w.ue(LOG2_CTB - LOG2_MIN_CB);
w.ue(LOG2_MIN_TB - 2);
w.ue(LOG2_MAX_TB - LOG2_MIN_TB);
w.ue(0); // max_transform_hierarchy_depth_inter
w.ue(0); // max_transform_hierarchy_depth_intra
w.put(0, 1); // scaling_list_enabled_flag
w.put(0, 1); // amp_enabled_flag
w.put(0, 1); // sample_adaptive_offset_enabled_flag
w.put(0, 1); // pcm_enabled_flag
w.ue(1); // num_short_term_ref_pic_sets
// st_ref_pic_set(0): one negative picture at delta POC -1, used by the current picture
w.ue(1); // num_negative_pics
w.ue(0); // num_positive_pics
w.ue(0); // delta_poc_s0_minus1
w.put(1, 1); // used_by_curr_pic_s0_flag
w.put(0, 1); // long_term_ref_pics_present_flag
w.put(0, 1); // sps_temporal_mvp_enabled_flag
w.put(1, 1); // strong_intra_smoothing_enabled_flag
w.put(1, 1); // vui_parameters_present_flag
vui(&mut w, p);
w.put(0, 1); // sps_extension_present_flag
w.trailing();
nal_unit(NAL_SPS, &w.into_bytes())
}
fn vui(w: &mut BitWriter, p: &SeqParams) {
w.put(0, 1); // aspect_ratio_info_present_flag
w.put(0, 1); // overscan_info_present_flag
w.put(1, 1); // video_signal_type_present_flag
w.put(5, 3); // video_format: unspecified
w.flag(p.full_range);
w.put(1, 1); // colour_description_present_flag
w.put(1, 8); // colour_primaries BT.709
w.put(1, 8); // transfer_characteristics BT.709
w.put(1, 8); // matrix_coeffs BT.709
w.put(0, 1); // chroma_loc_info_present_flag
w.put(0, 1); // neutral_chroma_indication_flag
w.put(0, 1); // field_seq_flag
w.put(0, 1); // frame_field_info_present_flag
w.put(0, 1); // default_display_window_flag
w.put(1, 1); // vui_timing_info_present_flag
w.put(p.fps_den, 32); // vui_num_units_in_tick
w.put(p.fps_num, 32); // vui_time_scale
w.put(0, 1); // vui_poc_proportional_to_timing_flag
w.put(0, 1); // vui_hrd_parameters_present_flag
w.put(0, 1); // bitstream_restriction_flag
}
pub fn pps() -> Vec<u8> {
let mut w = BitWriter::new();
w.ue(0); // pps_pic_parameter_set_id
w.ue(0); // pps_seq_parameter_set_id
w.put(0, 1); // dependent_slice_segments_enabled_flag
w.put(0, 1); // output_flag_present_flag
w.put(0, 3); // num_extra_slice_header_bits
w.put(0, 1); // sign_data_hiding_enabled_flag
w.put(0, 1); // cabac_init_present_flag
w.ue(0); // num_ref_idx_l0_default_active_minus1
w.ue(0); // num_ref_idx_l1_default_active_minus1
w.se(0); // init_qp_minus26
w.put(0, 1); // constrained_intra_pred_flag
w.put(0, 1); // transform_skip_enabled_flag
w.put(0, 1); // cu_qp_delta_enabled_flag
w.se(0); // pps_cb_qp_offset
w.se(0); // pps_cr_qp_offset
w.put(0, 1); // pps_slice_chroma_qp_offsets_present_flag
w.put(0, 1); // weighted_pred_flag
w.put(0, 1); // weighted_bipred_flag
w.put(0, 1); // transquant_bypass_enabled_flag
w.put(0, 1); // tiles_enabled_flag
w.put(0, 1); // entropy_coding_sync_enabled_flag
w.put(0, 1); // pps_loop_filter_across_slices_enabled_flag
w.put(1, 1); // deblocking_filter_control_present_flag
w.put(0, 1); // deblocking_filter_override_enabled_flag
w.put(1, 1); // pps_deblocking_filter_disabled_flag
w.put(0, 1); // pps_scaling_list_data_present_flag
w.put(0, 1); // lists_modification_present_flag
w.ue(0); // log2_parallel_merge_level_minus2
w.put(0, 1); // slice_segment_header_extension_present_flag
w.put(0, 1); // pps_extension_present_flag
w.trailing();
nal_unit(NAL_PPS, &w.into_bytes())
}
/// HEVCDecoderConfigurationRecord with one VPS, SPS and PPS (lengthSizeMinusOne = 3).
pub fn hvcc(p: &SeqParams, sets: &[Vec<u8>; 3]) -> Vec<u8> {
let mut o = vec![1u8]; // configurationVersion
o.push(p.profile_idc); // profile_space 0, tier 0, profile_idc
o.extend_from_slice(&compat_flags(p.profile_idc).to_be_bytes());
// general_constraint_indicator_flags (48 bits): progressive, interlaced, non_packed, frame_only
o.extend_from_slice(&[0x90, 0, 0, 0, 0, 0]);
o.push(p.level_idc);
o.extend_from_slice(&[0xf0, 0x00]); // reserved + min_spatial_segmentation_idc 0
o.push(0xfc); // reserved + parallelismType 0
o.push(0xfc | 1); // reserved + chroma_format_idc 1
o.push(0xf8 | (p.bit_depth - 8) as u8);
o.push(0xf8 | (p.bit_depth - 8) as u8);
o.extend_from_slice(&[0, 0]); // avgFrameRate unspecified
// constantFrameRate 0, numTemporalLayers 1, temporalIdNested 1, lengthSizeMinusOne 3
o.push((1 << 3) | (1 << 2) | 3);
o.push(3); // numOfArrays
for (set, ty) in sets.iter().zip([NAL_VPS, NAL_SPS, NAL_PPS]) {
o.push(0x80 | ty); // array_completeness 1
o.extend_from_slice(&1u16.to_be_bytes());
o.extend_from_slice(&(set.len() as u16).to_be_bytes());
o.extend_from_slice(set);
}
o
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn levels() {
assert_eq!(choose_level(1920, 1080, 30, 1, None), 120);
assert_eq!(choose_level(1920, 1080, 60, 1, None), 123);
assert_eq!(choose_level(3840, 2160, 30, 1, None), 150);
assert_eq!(choose_level(1280, 720, 30, 1, None), 93);
assert_eq!(choose_level(64, 64, 30, 1, None), 30);
assert_eq!(choose_level(1920, 1080, 30, 1, Some(15_000)), 123);
}
}
+232
View File
@@ -0,0 +1,232 @@
//! Constant tables of ITU-T H.265, mirrored from the first-party, spec-derived `filmcraft-hevc`
//! decoder (same organisation, MIT OR Apache-2.0): CABAC context layout and initValues (9.3.2.2),
//! rangeTabLPS / transIdx (Tables 9-52, 9-53), scans (6.5.3 - 6.5.5), transform matrices (8.6.4.2),
//! intra angles (Tables 8-5, 8-6), interpolation filters (8.5.3.3.3) and the chroma QP mapping (Table 8-10).
#![allow(dead_code)]
/// Table 9-52 (same values as H.264) rangeTabLPS[pStateIdx][qCodIRangeIdx].
#[rustfmt::skip]
pub static RANGE_TAB_LPS: [[u8; 4]; 64] = [
[128, 176, 208, 240], [128, 167, 197, 227], [128, 158, 187, 216], [123, 150, 178, 205],
[116, 142, 169, 195], [111, 135, 160, 185], [105, 128, 152, 175], [100, 122, 144, 166],
[95, 116, 137, 158], [90, 110, 130, 150], [85, 104, 123, 142], [81, 99, 117, 135],
[77, 94, 111, 128], [73, 89, 105, 122], [69, 85, 100, 116], [66, 80, 95, 110],
[62, 76, 90, 104], [59, 72, 86, 99], [56, 69, 81, 94], [53, 65, 77, 89],
[51, 62, 73, 85], [48, 59, 69, 80], [46, 56, 66, 76], [43, 53, 63, 72],
[41, 50, 59, 69], [39, 48, 56, 65], [37, 45, 54, 62], [35, 43, 51, 59],
[33, 41, 48, 56], [32, 39, 46, 53], [30, 37, 43, 50], [29, 35, 41, 48],
[27, 33, 39, 45], [26, 31, 37, 43], [24, 30, 35, 41], [23, 28, 33, 39],
[22, 27, 32, 37], [21, 26, 30, 35], [20, 24, 29, 33], [19, 23, 27, 31],
[18, 22, 26, 30], [17, 21, 25, 28], [16, 20, 23, 27], [15, 19, 22, 25],
[14, 18, 21, 24], [14, 17, 20, 23], [13, 16, 19, 22], [12, 15, 18, 21],
[12, 14, 17, 20], [11, 14, 16, 19], [11, 13, 15, 18], [10, 12, 15, 17],
[10, 12, 14, 16], [9, 11, 13, 15], [9, 11, 12, 14], [8, 10, 12, 14],
[8, 9, 11, 13], [7, 9, 11, 12], [7, 9, 10, 12], [7, 8, 10, 11],
[6, 8, 9, 11], [6, 7, 9, 10], [6, 7, 8, 9], [2, 2, 2, 2],
];
/// Table 9-53 transIdxLPS.
#[rustfmt::skip]
static TRANS_IDX_LPS: [u8; 64] = [
0, 0, 1, 2, 2, 4, 4, 5, 6, 7, 8, 9, 9, 11, 11, 12,
13, 13, 15, 15, 16, 16, 18, 18, 19, 19, 21, 21, 22, 22, 23, 24,
24, 25, 26, 26, 27, 27, 28, 29, 29, 30, 30, 30, 31, 32, 32, 33,
33, 33, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 63,
];
/// Table 9-53 transIdxMPS.
#[rustfmt::skip]
static TRANS_IDX_MPS: [u8; 64] = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 62, 63,
];
/// Combined state transition: index = (pStateIdx << 1 | valMPS) * 2 + bin_is_lps.
/// Each context is stored as `pStateIdx << 1 | valMPS`.
pub static NEXT_STATE: [[u8; 2]; 128] = {
let mut t = [[0u8; 2]; 128];
let mut s = 0;
while s < 128 {
let p = s >> 1;
let mps = s & 1;
// MPS path
t[s][0] = (TRANS_IDX_MPS[p] << 1) | mps as u8;
// LPS path
let new_mps = if p == 0 { 1 - mps } else { mps };
t[s][1] = (TRANS_IDX_LPS[p] << 1) | new_mps as u8;
s += 1;
}
t
};
/// Up-right diagonal scan (6.5.3) of a `N x N` block as (x, y) pairs.
const fn diag<const L: usize>(n: usize) -> [(u8, u8); L] {
let mut out = [(0u8, 0u8); L];
let mut i = 0;
let mut x: i32 = 0;
let mut y: i32 = 0;
while i < L {
while y >= 0 {
if (x as usize) < n && (y as usize) < n {
out[i] = (x as u8, y as u8);
i += 1;
}
y -= 1;
x += 1;
}
y = x;
x = 0;
}
out
}
/// Horizontal (6.5.4, `vertical == false`) or vertical (6.5.5) scan.
const fn hv<const L: usize>(n: usize, vertical: bool) -> [(u8, u8); L] {
let mut out = [(0u8, 0u8); L];
let mut i = 0;
while i < L {
let (a, b) = ((i % n) as u8, (i / n) as u8);
out[i] = if vertical { (b, a) } else { (a, b) };
i += 1;
}
out
}
pub static SCAN_1: [[(u8, u8); 1]; 3] = [[(0, 0)], [(0, 0)], [(0, 0)]];
pub static SCAN_2: [[(u8, u8); 4]; 3] = [diag::<4>(2), hv::<4>(2, false), hv::<4>(2, true)];
pub static SCAN_4: [[(u8, u8); 16]; 3] = [diag::<16>(4), hv::<16>(4, false), hv::<16>(4, true)];
pub static SCAN_8: [[(u8, u8); 64]; 3] = [diag::<64>(8), hv::<64>(8, false), hv::<64>(8, true)];
/// ScanOrder[log2BlockSize][scanIdx] (log2BlockSize 0..=3).
#[inline]
pub fn scan_order(log2: u32, scan_idx: usize) -> &'static [(u8, u8)] {
match log2 {
0 => &SCAN_1[scan_idx],
1 => &SCAN_2[scan_idx],
2 => &SCAN_4[scan_idx],
_ => &SCAN_8[scan_idx],
}
}
/// Up-right diagonal scan for log2 size 2 or 3.
pub fn scan_diag(log2: u32) -> &'static [(u8, u8)] {
scan_order(log2, 0)
}
/// Table 8-10: QpC as a function of qPi (ChromaArrayType 1).
#[inline]
pub fn qpc_420(qpi: i32) -> i32 {
if qpi < 30 {
qpi
} else if qpi > 43 {
qpi - 6
} else {
QPC_30_43[(qpi - 30) as usize] as i32
}
}
/// Table 9-50 ctxIdxMap for 4x4 sig_coeff_flag contexts.
pub static CTX_IDX_MAP: [u8; 16] = [0, 1, 4, 5, 2, 3, 4, 5, 6, 6, 8, 8, 7, 7, 8, 8];
/// levelScale (8-309).
pub static LEVEL_SCALE: [i32; 6] = [40, 45, 51, 57, 64, 72];
/// DST-VII 4x4 matrix (8.6.4.2, trType 1).
pub static DST_MATRIX: [[i8; 4]; 4] = [[29, 55, 74, 84], [74, 74, 0, -74], [84, -29, -74, 55], [55, -84, 74, -29]];
// CABAC context layout: offset of each syntax element in the context array (Table 9-4).
pub const SAO_MERGE: usize = 0;
pub const SAO_TYPE: usize = 1;
pub const SPLIT_CU: usize = 2;
pub const CU_TRANSQUANT_BYPASS: usize = 5;
pub const CU_SKIP: usize = 6;
pub const PRED_MODE: usize = 9;
pub const PART_MODE: usize = 10;
pub const PREV_INTRA_LUMA: usize = 14;
pub const INTRA_CHROMA: usize = 15;
pub const RQT_ROOT_CBF: usize = 16;
pub const MERGE_FLAG: usize = 17;
pub const MERGE_IDX: usize = 18;
pub const INTER_PRED_IDC: usize = 19;
pub const REF_IDX: usize = 24;
pub const MVP_FLAG: usize = 26;
pub const SPLIT_TRANSFORM: usize = 27;
pub const CBF_LUMA: usize = 30;
pub const CBF_CHROMA: usize = 32;
pub const ABS_MVD_GT0: usize = 37;
pub const ABS_MVD_GT1: usize = 38;
pub const CU_QP_DELTA_ABS: usize = 39;
pub const TRANSFORM_SKIP: usize = 41;
pub const LAST_X_PREFIX: usize = 43;
pub const LAST_Y_PREFIX: usize = 61;
pub const CODED_SUB_BLOCK: usize = 79;
pub const SIG_COEFF: usize = 83;
pub const GT1: usize = 127;
pub const GT2: usize = 151;
pub const EXPLICIT_RDPCM: usize = 157;
pub const EXPLICIT_RDPCM_DIR: usize = 159;
pub const CU_CHROMA_QP_OFFSET_FLAG: usize = 161;
pub const CU_CHROMA_QP_OFFSET_IDX: usize = 162;
pub const NUM_CTX: usize = 163;
/// initValue per initType (9.3.2.2).
pub static CABAC_INIT: [[u8; NUM_CTX]; 3] = [
[153, 200, 139, 141, 157, 154, 154, 154, 154, 154, 184, 154, 154, 154, 184, 63, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 153, 138, 138, 111, 141, 94, 138, 182, 154, 154, 154, 154, 154, 154, 139, 139, 110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79, 108, 123, 63, 110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79, 108, 123, 63, 91, 171, 134, 141, 111, 111, 125, 110, 110, 94, 124, 108, 124, 107, 125, 141, 179, 153, 125, 107, 125, 141, 179, 153, 125, 107, 125, 141, 179, 153, 125, 140, 139, 182, 182, 152, 136, 152, 136, 153, 136, 139, 111, 136, 139, 111, 141, 111, 140, 92, 137, 138, 140, 152, 138, 139, 153, 74, 149, 92, 139, 107, 122, 152, 140, 179, 166, 182, 140, 227, 122, 197, 138, 153, 136, 167, 152, 152, 154, 154, 154, 154, 154, 154],
[153, 185, 107, 139, 126, 154, 197, 185, 201, 149, 154, 139, 154, 154, 154, 152, 79, 110, 122, 95, 79, 63, 31, 31, 153, 153, 168, 124, 138, 94, 153, 111, 149, 107, 167, 154, 154, 140, 198, 154, 154, 139, 139, 125, 110, 94, 110, 95, 79, 125, 111, 110, 78, 110, 111, 111, 95, 94, 108, 123, 108, 125, 110, 94, 110, 95, 79, 125, 111, 110, 78, 110, 111, 111, 95, 94, 108, 123, 108, 121, 140, 61, 154, 155, 154, 139, 153, 139, 123, 123, 63, 153, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 170, 153, 123, 123, 107, 121, 107, 121, 167, 151, 183, 140, 151, 183, 140, 140, 140, 154, 196, 196, 167, 154, 152, 167, 182, 182, 134, 149, 136, 153, 121, 136, 137, 169, 194, 166, 167, 154, 167, 137, 182, 107, 167, 91, 122, 107, 167, 139, 139, 139, 139, 154, 154],
[153, 160, 107, 139, 126, 154, 197, 185, 201, 134, 154, 139, 154, 154, 183, 152, 79, 154, 137, 95, 79, 63, 31, 31, 153, 153, 168, 224, 167, 122, 153, 111, 149, 92, 167, 154, 154, 169, 198, 154, 154, 139, 139, 125, 110, 124, 110, 95, 94, 125, 111, 111, 79, 125, 126, 111, 111, 79, 108, 123, 93, 125, 110, 124, 110, 95, 94, 125, 111, 111, 79, 125, 126, 111, 111, 79, 108, 123, 93, 121, 140, 61, 154, 170, 154, 139, 153, 139, 123, 123, 63, 124, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 170, 153, 138, 138, 122, 121, 122, 121, 167, 151, 183, 140, 151, 183, 140, 140, 140, 154, 196, 167, 167, 154, 152, 167, 182, 182, 134, 149, 136, 153, 121, 136, 122, 169, 208, 166, 167, 154, 152, 167, 182, 107, 167, 91, 107, 107, 167, 139, 139, 139, 139, 154, 154],
];
/// Table 8-5: intraPredAngle for predModeIntra 0..34 (0 for planar / DC).
pub static INTRA_PRED_ANGLE: [i8; 35] = [0, 0, 32, 26, 21, 17, 13, 9, 5, 2, 0, -2, -5, -9, -13, -17, -21, -26, -32, -26, -21, -17, -13, -9, -5, -2, 0, 2, 5, 9, 13, 17, 21, 26, 32];
/// Table 8-6: invAngle for predModeIntra 11..25 (index = mode - 11).
pub static INV_ANGLE: [i16; 15] = [-4096, -1638, -910, -630, -482, -390, -315, -256, -315, -390, -482, -630, -910, -1638, -4096];
/// Table 8-10: QpC for qPi = 30..43 (qPi < 30: QpC = qPi; qPi > 43: QpC = qPi - 6).
pub static QPC_30_43: [u8; 14] = [29, 30, 31, 32, 33, 33, 34, 34, 35, 35, 36, 36, 37, 37];
/// Luma 8-tap interpolation filter fL[xFrac][k] (8-236..8-238), taps at offsets -3..4.
pub static LUMA_FILTER: [[i8; 8]; 4] = [[0, 0, 0, 64, 0, 0, 0, 0], [-1, 4, -10, 58, 17, -5, 1, 0], [-1, 4, -11, 40, 40, -11, 4, -1], [0, 1, -5, 17, 58, -10, 4, -1]];
/// Chroma 4-tap interpolation filter fC[xFrac][k] (8-255..8-261), taps at offsets -1..2.
pub static CHROMA_FILTER: [[i8; 4]; 8] = [[0, 64, 0, 0], [-2, 58, 10, -2], [-4, 54, 16, -2], [-6, 46, 28, -4], [-4, 36, 36, -4], [-4, 28, 46, -6], [-2, 16, 54, -4], [-2, 10, 58, -2]];
/// DCT matrix (8-319, 8-321): TRANS_MATRIX[n][m] = transMatrix[m][n] (row n = basis function).
pub static TRANS_MATRIX: [[i8; 32]; 32] = [
[64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],
[90, 90, 88, 85, 82, 78, 73, 67, 61, 54, 46, 38, 31, 22, 13, 4, -4, -13, -22, -31, -38, -46, -54, -61, -67, -73, -78, -82, -85, -88, -90, -90],
[90, 87, 80, 70, 57, 43, 25, 9, -9, -25, -43, -57, -70, -80, -87, -90, -90, -87, -80, -70, -57, -43, -25, -9, 9, 25, 43, 57, 70, 80, 87, 90],
[90, 82, 67, 46, 22, -4, -31, -54, -73, -85, -90, -88, -78, -61, -38, -13, 13, 38, 61, 78, 88, 90, 85, 73, 54, 31, 4, -22, -46, -67, -82, -90],
[89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75, 89, 89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75, 89],
[88, 67, 31, -13, -54, -82, -90, -78, -46, -4, 38, 73, 90, 85, 61, 22, -22, -61, -85, -90, -73, -38, 4, 46, 78, 90, 82, 54, 13, -31, -67, -88],
[87, 57, 9, -43, -80, -90, -70, -25, 25, 70, 90, 80, 43, -9, -57, -87, -87, -57, -9, 43, 80, 90, 70, 25, -25, -70, -90, -80, -43, 9, 57, 87],
[85, 46, -13, -67, -90, -73, -22, 38, 82, 88, 54, -4, -61, -90, -78, -31, 31, 78, 90, 61, 4, -54, -88, -82, -38, 22, 73, 90, 67, 13, -46, -85],
[83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83],
[82, 22, -54, -90, -61, 13, 78, 85, 31, -46, -90, -67, 4, 73, 88, 38, -38, -88, -73, -4, 67, 90, 46, -31, -85, -78, -13, 61, 90, 54, -22, -82],
[80, 9, -70, -87, -25, 57, 90, 43, -43, -90, -57, 25, 87, 70, -9, -80, -80, -9, 70, 87, 25, -57, -90, -43, 43, 90, 57, -25, -87, -70, 9, 80],
[78, -4, -82, -73, 13, 85, 67, -22, -88, -61, 31, 90, 54, -38, -90, -46, 46, 90, 38, -54, -90, -31, 61, 88, 22, -67, -85, -13, 73, 82, 4, -78],
[75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18, 75, 75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18, 75],
[73, -31, -90, -22, 78, 67, -38, -90, -13, 82, 61, -46, -88, -4, 85, 54, -54, -85, 4, 88, 46, -61, -82, 13, 90, 38, -67, -78, 22, 90, 31, -73],
[70, -43, -87, 9, 90, 25, -80, -57, 57, 80, -25, -90, -9, 87, 43, -70, -70, 43, 87, -9, -90, -25, 80, 57, -57, -80, 25, 90, 9, -87, -43, 70],
[67, -54, -78, 38, 85, -22, -90, 4, 90, 13, -88, -31, 82, 46, -73, -61, 61, 73, -46, -82, 31, 88, -13, -90, -4, 90, 22, -85, -38, 78, 54, -67],
[64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64],
[61, -73, -46, 82, 31, -88, -13, 90, -4, -90, 22, 85, -38, -78, 54, 67, -67, -54, 78, 38, -85, -22, 90, 4, -90, 13, 88, -31, -82, 46, 73, -61],
[57, -80, -25, 90, -9, -87, 43, 70, -70, -43, 87, 9, -90, 25, 80, -57, -57, 80, 25, -90, 9, 87, -43, -70, 70, 43, -87, -9, 90, -25, -80, 57],
[54, -85, -4, 88, -46, -61, 82, 13, -90, 38, 67, -78, -22, 90, -31, -73, 73, 31, -90, 22, 78, -67, -38, 90, -13, -82, 61, 46, -88, 4, 85, -54],
[50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89, 50, 50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89, 50],
[46, -90, 38, 54, -90, 31, 61, -88, 22, 67, -85, 13, 73, -82, 4, 78, -78, -4, 82, -73, -13, 85, -67, -22, 88, -61, -31, 90, -54, -38, 90, -46],
[43, -90, 57, 25, -87, 70, 9, -80, 80, -9, -70, 87, -25, -57, 90, -43, -43, 90, -57, -25, 87, -70, -9, 80, -80, 9, 70, -87, 25, 57, -90, 43],
[38, -88, 73, -4, -67, 90, -46, -31, 85, -78, 13, 61, -90, 54, 22, -82, 82, -22, -54, 90, -61, -13, 78, -85, 31, 46, -90, 67, 4, -73, 88, -38],
[36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36],
[31, -78, 90, -61, 4, 54, -88, 82, -38, -22, 73, -90, 67, -13, -46, 85, -85, 46, 13, -67, 90, -73, 22, 38, -82, 88, -54, -4, 61, -90, 78, -31],
[25, -70, 90, -80, 43, 9, -57, 87, -87, 57, -9, -43, 80, -90, 70, -25, -25, 70, -90, 80, -43, -9, 57, -87, 87, -57, 9, 43, -80, 90, -70, 25],
[22, -61, 85, -90, 73, -38, -4, 46, -78, 90, -82, 54, -13, -31, 67, -88, 88, -67, 31, 13, -54, 82, -90, 78, -46, 4, 38, -73, 90, -85, 61, -22],
[18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50, 18, 18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50, 18],
[13, -38, 61, -78, 88, -90, 85, -73, 54, -31, 4, 22, -46, 67, -82, 90, -90, 82, -67, 46, -22, -4, 31, -54, 73, -85, 90, -88, 78, -61, 38, -13],
[9, -25, 43, -57, 70, -80, 87, -90, 90, -87, 80, -70, 57, -43, 25, -9, -9, 25, -43, 57, -70, 80, -87, 90, -90, 87, -80, 70, -57, 43, -25, 9],
[4, -13, 22, -31, 38, -46, 54, -61, 67, -73, 78, -82, 85, -88, 90, -90, 90, -90, 88, -85, 82, -78, 73, -67, 61, -54, 46, -38, 31, -22, 13, -4],
];
+278
View File
@@ -0,0 +1,278 @@
//! Forward transforms (encoder side, any integer approximation is allowed), quantisation, and the
//! normative scaling (8.6.3) and inverse transforms (8.6.4.2) the decoder applies. The inverse path
//! is exact integer arithmetic: the even/odd decomposition computes the same sums as the matrix
//! products of the specification.
use crate::tables::{DST_MATRIX, LEVEL_SCALE, TRANS_MATRIX};
#[inline]
fn m(n: usize, k: usize, i: usize) -> i32 {
// k-th basis function of the n-point DCT, sample i
TRANS_MATRIX[k * (32 / n)][i] as i32
}
/// 1-D forward DCT of `x` (length n) into `out`.
fn fdct1d(x: &[i32], out: &mut [i32], n: usize) {
if n <= 4 {
for k in 0..n {
let mut s = 0;
for i in 0..n {
s += m(n, k, i) * x[i];
}
out[k] = s;
}
return;
}
let h = n / 2;
let mut e = [0i32; 16];
let mut o = [0i32; 16];
for i in 0..h {
e[i] = x[i] + x[n - 1 - i];
o[i] = x[i] - x[n - 1 - i];
}
let mut ev = [0i32; 16];
fdct1d(&e[..h], &mut ev[..h], h);
for k in 0..h {
out[2 * k] = ev[k];
let kk = 2 * k + 1;
let mut s = 0;
for i in 0..h {
s += m(n, kk, i) * o[i];
}
out[kk] = s;
}
}
/// 1-D inverse DCT: x[i] = sum_k M[k][i] * c[k].
fn idct1d(c: &[i32], out: &mut [i32], n: usize) {
if n <= 4 {
for i in 0..n {
let mut s = 0;
for k in 0..n {
s += m(n, k, i) * c[k];
}
out[i] = s;
}
return;
}
let h = n / 2;
let mut ce = [0i32; 16];
for k in 0..h {
ce[k] = c[2 * k];
}
let mut e = [0i32; 16];
idct1d(&ce[..h], &mut e[..h], h);
for i in 0..h {
let mut o = 0;
let mut k = 1;
while k < n {
let v = c[k];
if v != 0 {
o += m(n, k, i) * v;
}
k += 2;
}
out[i] = e[i] + o;
out[n - 1 - i] = e[i] - o;
}
}
fn fdst1d(x: &[i32], out: &mut [i32]) {
for k in 0..4 {
out[k] = (0..4).map(|i| DST_MATRIX[k][i] as i32 * x[i]).sum();
}
}
fn idst1d(c: &[i32], out: &mut [i32]) {
for i in 0..4 {
out[i] = (0..4).map(|k| DST_MATRIX[k][i] as i32 * c[k]).sum();
}
}
/// Forward 2-D transform of the n x n residual `r` (row-major) into `c`.
pub fn forward(r: &[i32], c: &mut [i32], n: usize, dst: bool, bit_depth: u32) {
let log2 = n.trailing_zeros();
let shift1 = log2 + bit_depth - 9;
let shift2 = log2 + 6;
let (r1, r2) = (1i32 << (shift1 - 1), 1i32 << (shift2 - 1));
let mut tmp = [0i32; 32 * 32];
let mut out = [0i32; 32];
// rows (horizontal frequencies)
for y in 0..n {
let row = &r[y * n..y * n + n];
if dst {
fdst1d(row, &mut out);
} else {
fdct1d(row, &mut out, n);
}
for k in 0..n {
tmp[k * n + y] = (out[k] + r1) >> shift1; // transposed: tmp[kx][y]
}
}
// columns
let mut col = [0i32; 32];
for kx in 0..n {
col[..n].copy_from_slice(&tmp[kx * n..kx * n + n]);
if dst {
fdst1d(&col, &mut out);
} else {
fdct1d(&col, &mut out, n);
}
for ky in 0..n {
c[ky * n + kx] = (out[ky] + r2) >> shift2;
}
}
}
/// Inverse 2-D transform (8.6.4.2) of scaled coefficients `d` in place, producing residuals.
pub fn inverse(d: &mut [i32], n: usize, dst: bool, bit_depth: u32) {
let bd_shift = 20 - bit_depth;
let mut tmp = [0i32; 32 * 32];
let mut col = [0i32; 32];
let mut out = [0i32; 32];
// max non-zero row, to skip empty work
let mut nz_cols = [false; 32];
for y in 0..n {
for x in 0..n {
if d[y * n + x] != 0 {
nz_cols[x] = true;
}
}
}
// 1. columns
for x in 0..n {
if !nz_cols[x] {
continue; // tmp column stays zero
}
for j in 0..n {
col[j] = d[j * n + x];
}
if dst {
idst1d(&col, &mut out);
} else {
idct1d(&col, &mut out, n);
}
for y in 0..n {
tmp[y * n + x] = ((out[y] + 64) >> 7).clamp(-32768, 32767);
}
}
// 2. rows
let round = 1 << (bd_shift - 1);
for y in 0..n {
let row = &tmp[y * n..y * n + n];
if dst {
idst1d(row, &mut out);
} else {
idct1d(row, &mut out, n);
}
for x in 0..n {
d[y * n + x] = (out[x] + round) >> bd_shift;
}
}
}
const QUANT_SCALE: [i64; 6] = [26214, 23302, 20560, 18396, 16384, 14564];
/// Quantise transform coefficients `c` into levels `l` with a dead-zone rounding offset
/// (`intra`: 1/3, otherwise 1/6). `qp` includes QpBdOffset. Returns the number of non-zero levels.
pub fn quantize(c: &[i32], l: &mut [i32], n: usize, qp: i32, intra: bool, bit_depth: u32) -> usize {
let log2 = n.trailing_zeros() as i32;
let tshift = 15 - bit_depth as i32 - log2;
let qbits = 14 + qp / 6 + tshift;
let scale = QUANT_SCALE[(qp % 6) as usize];
let offset = (if intra { 171i64 } else { 85 }) << (qbits - 9);
let mut nz = 0;
for i in 0..n * n {
let v = c[i] as i64;
let a = ((v.abs() * scale + offset) >> qbits).min(32767);
if a != 0 {
nz += 1;
}
l[i] = if v < 0 { -(a as i32) } else { a as i32 };
}
nz
}
/// Scaling process (8.6.2 / 8.6.3, flat scaling matrix): levels to scaled coefficients.
pub fn dequantize(l: &[i32], d: &mut [i32], n: usize, qp: i32, bit_depth: u32) {
let log2 = n.trailing_zeros();
let bd_shift = bit_depth + log2 - 5;
let round = 1i64 << (bd_shift - 1);
let scale = (LEVEL_SCALE[(qp % 6) as usize] as i64) << (qp / 6);
for i in 0..n * n {
let v = l[i];
d[i] = if v == 0 { 0 } else { ((v as i64 * 16 * scale + round) >> bd_shift).clamp(-32768, 32767) as i32 };
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Direct matrix form of the inverse transform, as written in the specification.
fn inverse_ref(c: &mut [i32], n: usize, dst: bool, bd: u32) {
let mut tmp = vec![0i32; n * n];
let step = 32 / n;
for x in 0..n {
for y in 0..n {
let mut s = 0i32;
for j in 0..n {
let m = if dst { DST_MATRIX[j][y] as i32 } else { TRANS_MATRIX[j * step][y] as i32 };
s += m * c[j * n + x];
}
tmp[y * n + x] = ((s + 64) >> 7).clamp(-32768, 32767);
}
}
let bd_shift = 20 - bd;
for y in 0..n {
for x in 0..n {
let mut s = 0i32;
for j in 0..n {
let m = if dst { DST_MATRIX[j][x] as i32 } else { TRANS_MATRIX[j * step][x] as i32 };
s += m * tmp[y * n + j];
}
c[y * n + x] = (s + (1 << (bd_shift - 1))) >> bd_shift;
}
}
}
#[test]
fn inverse_matches_matrix_form() {
let mut seed = 7u32;
for &(n, dst) in &[(4usize, true), (4, false), (8, false), (16, false), (32, false)] {
for _ in 0..20 {
let mut c = vec![0i32; n * n];
for v in c.iter_mut() {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
if (seed >> 24).is_multiple_of(3) {
*v = ((seed >> 8) % 2001) as i32 - 1000;
}
}
let mut a = c.clone();
let mut b = c.clone();
inverse(&mut a, n, dst, 8);
inverse_ref(&mut b, n, dst, 8);
assert_eq!(a, b, "n {n} dst {dst}");
}
}
}
#[test]
fn round_trip_is_close() {
for &(n, dst) in &[(4usize, true), (8, false), (32, false)] {
for bd in [8u32, 10] {
let r: Vec<i32> = (0..n * n).map(|i| ((i * 37 + i / n * 11) % 61) as i32 - 30).collect();
let mut c = vec![0; n * n];
forward(&r, &mut c, n, dst, bd);
let mut l = vec![0; n * n];
let qp = 4;
quantize(&c, &mut l, n, qp, true, bd);
let mut d = vec![0; n * n];
dequantize(&l, &mut d, n, qp, bd);
inverse(&mut d, n, dst, bd);
let err: i32 = r.iter().zip(&d).map(|(a, b)| (a - b).abs()).max().unwrap();
assert!(err <= 2, "n {n} bd {bd} err {err}");
}
}
}
}
+273
View File
@@ -0,0 +1,273 @@
//! Round trips through the filmcraft-hevc decoder and (when installed) ffmpeg: decoded pictures must
//! equal the encoder's own reconstruction bit-exactly.
use effectcraft_hevcenc::{Encoder, EncoderConfig, Frame, Profile, RateControl};
use std::process::Command;
struct Pic {
w: usize,
y: Vec<u16>,
u: Vec<u16>,
v: Vec<u16>,
}
impl Pic {
fn frame(&self) -> Frame<'_> {
Frame { y: &self.y, u: &self.u, v: &self.v, y_stride: self.w, uv_stride: self.w / 2 }
}
}
/// Smooth gradients with a few hard-edged shapes, translated by (dx, dy) samples.
fn synth(w: usize, h: usize, bd: u32, dx: i32, dy: i32) -> Pic {
let max = ((1u32 << bd) - 1) as f64;
let mut y = vec![0u16; w * h];
let mut u = vec![0u16; w * h / 4];
let mut v = vec![0u16; w * h / 4];
let luma = |x: i32, yy: i32| -> f64 {
let (fx, fy) = ((x - dx) as f64, (yy - dy) as f64);
let mut l = 0.25 + 0.5 * (fx / 97.0).sin().abs() * 0.6 + 0.3 * (fy / 61.0).cos().powi(2);
// a disc and a rectangle
if (fx - 40.0).powi(2) + (fy - 30.0).powi(2) < 18.0f64.powi(2) {
l = 0.85;
}
if (70.0..110.0).contains(&fx) && (50.0..64.0).contains(&fy) {
l = 0.12;
}
// fine texture
l += 0.03 * ((fx * 0.9).sin() * (fy * 0.7).cos());
l.clamp(0.0, 1.0)
};
for yy in 0..h {
for x in 0..w {
y[yy * w + x] = (luma(x as i32, yy as i32) * max).round() as u16;
}
}
for yy in 0..h / 2 {
for x in 0..w / 2 {
let (fx, fy) = ((2 * x) as i32 - dx, (2 * yy) as i32 - dy);
let cu = 0.5 + 0.2 * ((fx as f64) / 53.0).sin();
let cv = 0.5 + 0.2 * ((fy as f64) / 37.0).cos() * if (fx / 16 + fy / 16) % 2 == 0 { 1.0 } else { -0.5 };
u[yy * w / 2 + x] = (cu * max).round() as u16;
v[yy * w / 2 + x] = (cv * max).round() as u16;
}
}
Pic { w, y, u, v }
}
fn psnr(a: &[u16], b: &[u16], bd: u32) -> f64 {
let max = ((1u32 << bd) - 1) as f64;
let mse = a.iter().zip(b).map(|(&x, &y)| (x as f64 - y as f64).powi(2)).sum::<f64>() / a.len() as f64;
if mse == 0.0 { 99.0 } else { 10.0 * (max * max / mse).log10() }
}
struct Run {
packets: Vec<Vec<u8>>,
keys: Vec<bool>,
recons: Vec<[Vec<u16>; 3]>,
hvcc: Vec<u8>,
sets: [Vec<u8>; 3],
psnr_y: Vec<f64>,
}
fn encode(cfg: EncoderConfig, pics: &[Pic]) -> Run {
let bd = cfg.profile.bit_depth() as u32;
let mut enc = Encoder::new(cfg).unwrap();
let mut run = Run { packets: vec![], keys: vec![], recons: vec![], hvcc: enc.hvcc(), sets: enc.parameter_sets(), psnr_y: vec![] };
for p in pics {
let pkt = enc.encode(&p.frame());
let rec = enc.last_reconstruction().unwrap().clone();
run.psnr_y.push(psnr(&p.y, &rec[0], bd));
run.packets.push(pkt.data);
run.keys.push(pkt.keyframe);
run.recons.push(rec);
}
run
}
/// Decode with filmcraft-hevc and compare with the reconstructions.
fn check_filmcraft(run: &Run, w: usize, h: usize) {
let mut dec = filmcraft_hevc::Decoder::from_hvcc(&run.hvcc).unwrap();
let mut pics = Vec::new();
for (i, p) in run.packets.iter().enumerate() {
pics.extend(dec.decode(p, i as i64).unwrap());
}
pics.extend(dec.flush());
assert_eq!(pics.len(), run.packets.len(), "decoded picture count");
for (i, pic) in pics.iter().enumerate() {
assert_eq!((pic.width as usize, pic.height as usize), (w, h));
assert_eq!(pic.pts, i as i64);
let rec = &run.recons[i];
for (c, plane) in [&pic.y, &pic.u, &pic.v].into_iter().enumerate() {
let (pw, ph, stride) = if c == 0 { (w, h, pic.y_stride) } else { (w / 2, h / 2, pic.uv_stride) };
for yy in 0..ph {
for x in 0..pw {
let got = plane.get(yy * stride + x);
let want = rec[c][yy * pw + x];
assert_eq!(got, want, "picture {i} plane {c} at ({x}, {yy})");
}
}
}
}
}
fn annexb(run: &Run) -> Vec<u8> {
let mut out = Vec::new();
for s in &run.sets {
out.extend_from_slice(&[0, 0, 0, 1]);
out.extend_from_slice(s);
}
for p in &run.packets {
let mut i = 0;
while i < p.len() {
let n = u32::from_be_bytes([p[i], p[i + 1], p[i + 2], p[i + 3]]) as usize;
out.extend_from_slice(&[0, 0, 0, 1]);
out.extend_from_slice(&p[i + 4..i + 4 + n]);
i += 4 + n;
}
}
out
}
fn ffmpeg_available() -> bool {
Command::new("ffmpeg").arg("-version").output().map(|o| o.status.success()).unwrap_or(false)
}
/// Decode the Annex-B stream with ffmpeg and require bit-exact equality and an empty stderr.
fn check_ffmpeg(run: &Run, w: usize, h: usize, bd: u32, tag: &str) {
if !ffmpeg_available() {
eprintln!("ffmpeg not found; skipping the ffmpeg oracle");
return;
}
let dir = std::env::temp_dir();
let uniq = format!("{}-{}-{}", tag, std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
let inp = dir.join(format!("hevcenc-{uniq}.hevc"));
let out = dir.join(format!("hevcenc-{uniq}.yuv"));
std::fs::write(&inp, annexb(run)).unwrap();
let pix = if bd == 8 { "yuv420p" } else { "yuv420p10le" };
let o = Command::new("ffmpeg")
.args(["-v", "error", "-xerror", "-y", "-f", "hevc", "-i"])
.arg(&inp)
.args(["-f", "rawvideo", "-pix_fmt", pix])
.arg(&out)
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&o.stderr).to_string();
let data = std::fs::read(&out).unwrap_or_default();
let _ = std::fs::remove_file(&inp);
let _ = std::fs::remove_file(&out);
assert!(o.status.success() && stderr.trim().is_empty(), "ffmpeg failed: {stderr}");
let bps = if bd == 8 { 1 } else { 2 };
let frame_bytes = (w * h + 2 * (w / 2) * (h / 2)) * bps;
assert_eq!(data.len(), frame_bytes * run.packets.len(), "ffmpeg output size");
for (i, rec) in run.recons.iter().enumerate() {
let mut off = i * frame_bytes;
for (c, plane) in rec.iter().enumerate() {
let pw = if c == 0 { w } else { w / 2 };
for (k, &want) in plane.iter().enumerate() {
let got = if bd == 8 { data[off] as u16 } else { u16::from_le_bytes([data[off], data[off + 1]]) };
assert_eq!(got, want, "ffmpeg: picture {i} plane {c} at ({}, {})", k % pw, k / pw);
off += bps;
}
}
}
}
fn cfg(w: u32, h: u32, qp: u8, keyint: u32, profile: Profile) -> EncoderConfig {
let mut c = EncoderConfig::new(w, h, 30, 1);
c.rate = RateControl::ConstantQp(qp);
c.keyint = keyint;
c.profile = profile;
c
}
#[test]
fn intra_8bit_quality_and_decoders() {
let (w, h) = (192, 128);
let pics = vec![synth(w, h, 8, 0, 0)];
let run = encode(cfg(w as u32, h as u32, 22, 1, Profile::Main), &pics);
eprintln!("intra QP22: {} bytes, PSNR-Y {:.2}", run.packets[0].len(), run.psnr_y[0]);
assert!(run.psnr_y[0] > 35.0, "PSNR {}", run.psnr_y[0]);
check_filmcraft(&run, w, h);
check_ffmpeg(&run, w, h, 8, "intra8");
}
#[test]
fn inter_8bit_moving_pattern() {
let (w, h) = (160, 96);
let pics: Vec<Pic> = (0..6).map(|i| synth(w, h, 8, 3 * i, i)).collect();
let run = encode(cfg(w as u32, h as u32, 27, 30, Profile::Main), &pics);
let sizes: Vec<usize> = run.packets.iter().map(|p| p.len()).collect();
eprintln!("inter sizes {sizes:?} psnr {:?}", run.psnr_y);
assert_eq!(run.keys, vec![true, false, false, false, false, false]);
let p_avg = sizes[1..].iter().sum::<usize>() as f64 / 5.0;
assert!(p_avg < sizes[0] as f64 * 0.6, "P frames {p_avg} vs I {}", sizes[0]);
assert!(run.psnr_y.iter().all(|&p| p > 33.0));
check_filmcraft(&run, w, h);
check_ffmpeg(&run, w, h, 8, "inter8");
}
#[test]
fn odd_multiple_sizes_and_cropping() {
for (w, h) in [(34usize, 18usize), (66, 38), (8, 8), (130, 72)] {
let pics: Vec<Pic> = (0..3).map(|i| synth(w, h, 8, i, 2 * i)).collect();
let run = encode(cfg(w as u32, h as u32, 30, 2, Profile::Main), &pics);
check_filmcraft(&run, w, h);
check_ffmpeg(&run, w, h, 8, &format!("size{w}x{h}"));
}
}
#[test]
fn odd_dimensions_are_rejected() {
assert!(Encoder::new(EncoderConfig::new(33, 17, 30, 1)).is_err());
}
#[test]
fn main10_round_trip() {
let (w, h) = (96, 64);
let pics: Vec<Pic> = (0..4).map(|i| synth(w, h, 10, 2 * i, -i)).collect();
let run = encode(cfg(w as u32, h as u32, 24, 3, Profile::Main10), &pics);
eprintln!("main10 psnr {:?}", run.psnr_y);
assert!(run.psnr_y[0] > 35.0);
check_filmcraft(&run, w, h);
check_ffmpeg(&run, w, h, 10, "main10");
}
#[test]
fn qp_extremes() {
let (w, h) = (64, 64);
for qp in [0u8, 51] {
let pics: Vec<Pic> = (0..2).map(|i| synth(w, h, 8, i, i)).collect();
let run = encode(cfg(w as u32, h as u32, qp, 30, Profile::Main), &pics);
check_filmcraft(&run, w, h);
check_ffmpeg(&run, w, h, 8, &format!("qp{qp}"));
}
}
#[test]
fn bitrate_mode_hits_target() {
let (w, h) = (192, 128);
let pics: Vec<Pic> = (0..30).map(|i| synth(w, h, 8, i, i / 2)).collect();
let mut c = EncoderConfig::new(w as u32, h as u32, 30, 1);
c.rate = RateControl::Bitrate { kbps: 400 };
c.keyint = 15;
let run = encode(c, &pics);
let bits: usize = run.packets.iter().map(|p| p.len() * 8).sum();
let kbps = bits as f64 / (pics.len() as f64 / 30.0) / 1000.0;
eprintln!("bitrate mode: {kbps:.1} kbit/s for a 400 kbit/s target");
assert!(kbps > 400.0 / 2.0 && kbps < 400.0 * 2.0, "{kbps}");
check_filmcraft(&run, w, h);
}
#[test]
fn hvcc_layout() {
let enc = Encoder::new(cfg(64, 48, 28, 30, Profile::Main10)).unwrap();
let h = enc.hvcc();
assert_eq!(h[0], 1);
assert_eq!(h[1] & 31, 2); // Main 10
assert_eq!(h[21] & 3, 3); // lengthSizeMinusOne
assert_eq!(h[22], 3); // arrays
let sets = enc.parameter_sets();
assert_eq!(sets[0][0] >> 1, 32);
assert_eq!(sets[1][0] >> 1, 33);
assert_eq!(sets[2][0] >> 1, 34);
}