Files
filmcraft/crates/apv/src/encode.rs
T

599 lines
23 KiB
Rust

//! RFC 9924 APV intra frame encoder.
use filmcraft_bitstream::BitWriter;
use crate::dct::{fdct8x8, quantize8x8};
use crate::header::{FrameHeader, MAX_DIMENSION, PBU_PRIMARY_FRAME, write_frame_header, write_metadata_pbu};
use crate::tables::{DEFAULT_Q_MATRIX_VAL, ZIGZAG_8X8, select_level_and_band};
use crate::{ChromaFormat, ColorInfo, ContentLightLevel, Error, Frame, MasteringDisplay, Profile, Result};
/// Generate RFC 9924 §7.2.4 variable-length codeword (`h(v)`).
#[inline]
pub(crate) fn write_vlc(w: &mut BitWriter, symbol_val: u32, k_param: u32) {
const PREFIX_VLC: [[u32; 2]; 3] = [[1, 0], [0, 0], [0, 1]];
let mut symbol_value = symbol_val;
let val_prefix = ((symbol_val >> k_param) as usize).min(2);
let mut bit_count = 0usize;
let mut k = k_param;
while symbol_value >= (1u32 << k) {
symbol_value -= 1u32 << k;
if bit_count < 2 {
w.write_bits(PREFIX_VLC[val_prefix][bit_count], 1);
} else {
w.write_bits(0, 1);
}
if bit_count >= 2 {
k += 1;
}
bit_count += 1;
}
if bit_count < 2 {
w.write_bits(PREFIX_VLC[val_prefix][bit_count], 1);
} else {
w.write_bits(1, 1);
}
if k > 0 {
w.write_bits(symbol_value, k);
}
}
/// Exact bit length of a VLC codeword generated by [`write_vlc`].
#[inline]
pub(crate) fn vlc_bit_len(symbol_val: u32, k_param: u32) -> u32 {
if symbol_val < (1u32 << k_param) {
1 + k_param
} else if symbol_val < (2u32 << k_param) {
2 + k_param
} else {
let rem = (symbol_val >> k_param) - 1;
let zeros = 31 - rem.leading_zeros();
3 + k_param + 2 * zeros
}
}
/// APV encoder configuration.
#[derive(Debug, Clone)]
pub struct EncoderConfig {
pub profile: Profile,
pub width: u32,
pub height: u32,
pub chroma: ChromaFormat,
pub bit_depth: u8,
pub color: ColorInfo,
pub color_description_present: bool,
/// Fixed tile quantization parameter (`0..=51 + QpBdOffset`). When `None`, rate control
/// chooses a uniform QP per frame to fit [`Self::target_frame_bytes`].
pub qp: Option<u8>,
/// Target encoded frame size in bytes when `qp` is `None`.
pub target_frame_bytes: u32,
/// Tile width in 16x16 macroblocks (RFC 9924 §9.4.1 requires `>= 16`).
pub tile_width_in_mbs: u32,
/// Tile height in 16x16 macroblocks (RFC 9924 §9.4.1 requires `>= 8`).
pub tile_height_in_mbs: u32,
pub level_idc: u8,
pub band_idc: u8,
/// Optional custom per-component 8x8 quantization matrices (`y * 8 + x`).
pub q_matrix: Option<[[u8; 64]; 4]>,
/// Write `tile_size_in_fh` array in `frame_header()` (`tile_size_present_in_fh_flag = 1`).
pub write_tile_sizes_in_fh: bool,
pub mastering_display: Option<MasteringDisplay>,
pub content_light: Option<ContentLightLevel>,
}
impl EncoderConfig {
pub fn new(profile: Profile, width: u32, height: u32) -> Self {
let chroma = profile.default_chroma();
let bit_depth = profile.default_bit_depth();
let w_mbs = width.div_ceil(16).max(1);
let h_mbs = height.div_ceil(16).max(1);
// Choose conforming tile dimensions (>= 16x8 MBs, <= 20x20 tiles) with ~4-8 tiles on HD/4K.
let tile_width_in_mbs = w_mbs.div_ceil(4).max(16).max(w_mbs.div_ceil(20));
let tile_height_in_mbs = h_mbs.div_ceil(4).max(8).max(h_mbs.div_ceil(20));
let target_frame_bytes = profile.nominal_frame_bytes(width, height);
let kbps = ((target_frame_bytes as u64) * 8 * 30 / 1000).max(1000) as u32;
let (level_idc, band_idc) = select_level_and_band((width as u64) * (height as u64) * 30, kbps);
Self {
profile,
width,
height,
chroma,
bit_depth,
color: ColorInfo::BT709,
color_description_present: true,
qp: None,
target_frame_bytes,
tile_width_in_mbs,
tile_height_in_mbs,
level_idc,
band_idc,
q_matrix: None,
write_tile_sizes_in_fh: false,
mastering_display: None,
content_light: None,
}
}
}
/// APV intra encoder.
pub struct Encoder {
cfg: EncoderConfig,
hdr: FrameHeader,
}
struct PreparedTileComp {
/// Forward-transformed 8x8 blocks in MB-then-subblock scan order.
blocks: Vec<[i32; 64]>,
}
struct PreparedTile {
tile_idx: usize,
comps: Vec<PreparedTileComp>,
}
impl Encoder {
pub fn new(profile: Profile, width: u32, height: u32) -> Result<Self> {
Self::with_config(EncoderConfig::new(profile, width, height))
}
pub fn with_config(cfg: EncoderConfig) -> Result<Self> {
if cfg.width == 0 || cfg.height == 0 || cfg.width > MAX_DIMENSION || cfg.height > MAX_DIMENSION {
return Err(Error::Input(format!("unsupported dimensions {}x{}", cfg.width, cfg.height)));
}
if cfg.chroma == ChromaFormat::Yuv422 && !cfg.width.is_multiple_of(2) {
return Err(Error::Input("4:2:2 requires even frame width".into()));
}
if !(10..=16).contains(&cfg.bit_depth) {
return Err(Error::Input(format!("unsupported coded bit depth {}", cfg.bit_depth)));
}
if !cfg.profile.supports(cfg.chroma, cfg.bit_depth) {
return Err(Error::Input(format!("profile {} does not support {:?} {}-bit", cfg.profile.name(), cfg.chroma, cfg.bit_depth)));
}
if cfg.tile_width_in_mbs == 0 || cfg.tile_height_in_mbs == 0 {
return Err(Error::Input("tile dimensions in MBs must be non-zero".into()));
}
let w_mbs = cfg.width.div_ceil(16);
let h_mbs = cfg.height.div_ceil(16);
let tile_cols = w_mbs.div_ceil(cfg.tile_width_in_mbs);
let tile_rows = h_mbs.div_ceil(cfg.tile_height_in_mbs);
if tile_cols > 20 || tile_rows > 20 {
return Err(Error::Input(format!("tile grid {tile_cols}x{tile_rows} exceeds RFC 9924 limit of 20x20")));
}
let mut col_starts = Vec::with_capacity(tile_cols as usize + 1);
let mut s = 0u32;
while s < w_mbs {
col_starts.push(s * 16);
s += cfg.tile_width_in_mbs;
}
col_starts.push(w_mbs * 16);
let mut row_starts = Vec::with_capacity(tile_rows as usize + 1);
let mut s = 0u32;
while s < h_mbs {
row_starts.push(s * 16);
s += cfg.tile_height_in_mbs;
}
row_starts.push(h_mbs * 16);
if let Some(qm) = &cfg.q_matrix {
for c in 0..cfg.chroma.num_comps() {
if qm[c].contains(&0) {
return Err(Error::Input("q_matrix entries must be non-zero (1..=255)".into()));
}
}
}
let hdr = FrameHeader {
profile_idc: cfg.profile.idc(),
level_idc: cfg.level_idc,
band_idc: cfg.band_idc.min(3),
width: cfg.width,
height: cfg.height,
chroma: cfg.chroma,
bit_depth: cfg.bit_depth,
capture_time_distance: 0,
color_description_present: cfg.color_description_present,
color: cfg.color,
use_q_matrix: cfg.q_matrix.is_some(),
q_matrix: cfg.q_matrix.unwrap_or([[DEFAULT_Q_MATRIX_VAL; 64]; 4]),
tile_width_in_mbs: cfg.tile_width_in_mbs,
tile_height_in_mbs: cfg.tile_height_in_mbs,
tile_cols,
tile_rows,
col_starts,
row_starts,
tile_sizes_in_fh: None,
header_bytes: 0,
};
Ok(Self { cfg, hdr })
}
pub fn config(&self) -> &EncoderConfig {
&self.cfg
}
pub fn target_frame_bytes(&self) -> u32 {
self.cfg.target_frame_bytes
}
/// Encode one frame into an APV Access Unit (`b"aPv1"...`), suitable for MP4/MOV (`apv1`) or MKV (`V_APV`).
pub fn encode(&mut self, frame: &Frame) -> Result<Vec<u8>> {
if frame.width != self.cfg.width || frame.height != self.cfg.height {
return Err(Error::Input(format!("frame is {}x{}, encoder is {}x{}", frame.width, frame.height, self.cfg.width, self.cfg.height)));
}
if frame.chroma != self.cfg.chroma {
return Err(Error::Input(format!("frame chroma {:?} does not match encoder {:?}", frame.chroma, self.cfg.chroma)));
}
if !(8..=16).contains(&frame.bit_depth) {
return Err(Error::Input(format!("unsupported input bit depth {}", frame.bit_depth)));
}
let n_luma = (frame.width as usize) * (frame.height as usize);
let n_chroma = (frame.chroma_width() as usize) * (frame.chroma_height() as usize);
if frame.y.len() != n_luma || frame.cb.len() != n_chroma || frame.cr.len() != n_chroma {
return Err(Error::Input("plane buffer lengths do not match frame geometry".into()));
}
if self.cfg.chroma == ChromaFormat::Yuv4444 && frame.alpha.as_ref().is_none_or(|a| a.len() != n_luma) {
return Err(Error::Input("4:4:4:4 profile requires an alpha plane of width * height samples".into()));
}
let prepared = self.prepare_tiles(frame);
let max_qp = (51 + self.hdr.qp_bd_offset()) as u8;
let qp = match self.cfg.qp {
Some(q) => q.min(max_qp),
None => self.select_qp(&prepared, max_qp),
};
let encoded_tiles: Vec<Vec<u8>> = {
#[cfg(feature = "threads")]
{
use rayon::prelude::*;
prepared.par_iter().map(|t| self.encode_tile(t, qp)).collect()
}
#[cfg(not(feature = "threads"))]
{
prepared.iter().map(|t| self.encode_tile(t, qp)).collect()
}
};
let mut hdr = self.hdr.clone();
hdr.color = if !self.cfg.color_description_present {
ColorInfo::default()
} else if frame.color != ColorInfo::BT709 {
frame.color
} else {
self.cfg.color
};
if self.cfg.write_tile_sizes_in_fh {
hdr.tile_sizes_in_fh = Some(encoded_tiles.iter().map(|t| t.len() as u32).collect());
}
let fh_bytes = write_frame_header(&hdr);
let tiles_total_bytes: usize = encoded_tiles.iter().map(|t| 4 + t.len()).sum();
let pbu_size = 4 + fh_bytes.len() + tiles_total_bytes;
let mdcv = frame.mastering_display.as_ref().or(self.cfg.mastering_display.as_ref());
let cll = frame.content_light.as_ref().or(self.cfg.content_light.as_ref());
let meta_pbu = write_metadata_pbu(1, mdcv, cll);
let meta_total = meta_pbu.as_ref().map_or(0, |m| 4 + m.len());
let mut au = Vec::with_capacity(4 + 4 + pbu_size + meta_total);
au.extend_from_slice(b"aPv1");
// Primary frame PBU (pbu_type = 1, group_id = 1)
au.extend_from_slice(&(pbu_size as u32).to_be_bytes());
au.push(PBU_PRIMARY_FRAME);
au.extend_from_slice(&1u16.to_be_bytes());
au.push(0); // reserved_zero_8bits
au.extend_from_slice(&fh_bytes);
for t in &encoded_tiles {
au.extend_from_slice(&(t.len() as u32).to_be_bytes());
au.extend_from_slice(t);
}
if let Some(m) = meta_pbu {
au.extend_from_slice(&(m.len() as u32).to_be_bytes());
au.extend_from_slice(&m);
}
Ok(au)
}
/// Encode one frame as a `raw_bitstream_access_unit()` (prefixed by `au_size: u32 BE`, RFC 9924 Appendix A).
pub fn encode_raw_au(&mut self, frame: &Frame) -> Result<Vec<u8>> {
let au = self.encode(frame)?;
let mut raw = Vec::with_capacity(4 + au.len());
raw.extend_from_slice(&(au.len() as u32).to_be_bytes());
raw.extend_from_slice(&au);
Ok(raw)
}
/// Build the `APVDecoderConfigurationRecord` payload (for ISOBMFF `apvC` or Matroska CodecPrivate).
pub fn decoder_config_record(&self) -> Vec<u8> {
let mut v = Vec::with_capacity(24);
v.push(1); // configurationVersion = 1
v.push(1); // number_of_configuration_entry = 1
v.push(PBU_PRIMARY_FRAME); // pbu_type[0] = 1
v.push(1); // number_of_frame_info[0] = 1
let flags = if self.cfg.color_description_present { 0x02 } else { 0x00 } | 0x01; // capture_time_distance_ignored = 1
v.push(flags);
v.push(self.hdr.profile_idc);
v.push(self.hdr.level_idc);
v.push(self.hdr.band_idc);
v.extend_from_slice(&self.hdr.width.to_be_bytes());
v.extend_from_slice(&self.hdr.height.to_be_bytes());
v.push((self.hdr.chroma.idc() << 4) | (self.hdr.bit_depth.saturating_sub(8) & 0x0F));
v.push(0); // capture_time_distance = 0
if self.cfg.color_description_present {
v.push(self.cfg.color.primaries);
v.push(self.cfg.color.transfer);
v.push(self.cfg.color.matrix);
v.push(if self.cfg.color.full_range { 0x80 } else { 0x00 });
}
v
}
fn prepare_tiles(&self, frame: &Frame) -> Vec<PreparedTile> {
let num_tiles = self.hdr.num_tiles();
let prepare_one = |tile_idx: usize| -> PreparedTile {
let tx = tile_idx % (self.hdr.tile_cols as usize);
let ty = tile_idx / (self.hdr.tile_cols as usize);
let x0 = self.hdr.col_starts[tx];
let x1 = self.hdr.col_starts[tx + 1];
let y0 = self.hdr.row_starts[ty];
let y1 = self.hdr.row_starts[ty + 1];
let num_mb_cols = (x1 - x0) / 16;
let num_mb_rows = (y1 - y0) / 16;
let num_comps = self.hdr.chroma.num_comps();
let mut comps = Vec::with_capacity(num_comps);
for c in 0..num_comps {
let (plane, pw, ph, sub_w, sub_h) = match c {
0 => (frame.y.as_slice(), frame.width as usize, frame.height as usize, 1u32, 1u32),
1 => (
frame.cb.as_slice(),
frame.chroma_width() as usize,
frame.chroma_height() as usize,
self.hdr.chroma.sub_width_c(),
self.hdr.chroma.sub_height_c(),
),
2 => (
frame.cr.as_slice(),
frame.chroma_width() as usize,
frame.chroma_height() as usize,
self.hdr.chroma.sub_width_c(),
self.hdr.chroma.sub_height_c(),
),
_ => (frame.alpha.as_deref().unwrap_or(&[]), frame.width as usize, frame.height as usize, 1u32, 1u32),
};
let blk_w = (16 / sub_w) as usize;
let blk_h = (16 / sub_h) as usize;
let blocks_x = blk_w / 8;
let blocks_y = blk_h / 8;
let total_blocks = (num_mb_cols as usize) * (num_mb_rows as usize) * blocks_x * blocks_y;
let mut blocks = Vec::with_capacity(total_blocks);
let coded_depth = self.hdr.bit_depth;
let in_depth = frame.bit_depth;
for mb_y in 0..num_mb_rows {
for mb_x in 0..num_mb_cols {
let x_mb_c = ((x0 + mb_x * 16) / sub_w) as usize;
let y_mb_c = ((y0 + mb_y * 16) / sub_h) as usize;
for by in 0..blocks_y {
for bx in 0..blocks_x {
let bx0 = x_mb_c + bx * 8;
let by0 = y_mb_c + by * 8;
let mut samples = [0u16; 64];
for ry in 0..8 {
let sy = (by0 + ry).min(ph.saturating_sub(1));
for rx in 0..8 {
let sx = (bx0 + rx).min(pw.saturating_sub(1));
let v = plane[sy * pw + sx];
samples[ry * 8 + rx] = scale_sample(v, in_depth, coded_depth);
}
}
blocks.push(fdct8x8(&samples, coded_depth));
}
}
}
}
comps.push(PreparedTileComp { blocks });
}
PreparedTile { tile_idx, comps }
};
#[cfg(feature = "threads")]
{
use rayon::prelude::*;
(0..num_tiles).into_par_iter().map(prepare_one).collect()
}
#[cfg(not(feature = "threads"))]
{
(0..num_tiles).map(prepare_one).collect()
}
}
fn select_qp(&self, tiles: &[PreparedTile], max_qp: u8) -> u8 {
let target_bytes = (self.cfg.target_frame_bytes as usize).max(64);
let frame_bytes_at = |qp: u8| -> usize {
let tile_bytes = |t: &PreparedTile| -> usize {
let num_comps = t.comps.len();
let mut sz = 4 + (5 + 5 * num_comps);
for (c, comp) in t.comps.iter().enumerate() {
let bits = self.estimate_comp_bits(comp, c, qp);
sz += (bits.div_ceil(8) as usize).max(1);
}
sz
};
#[cfg(feature = "threads")]
let sum: usize = {
use rayon::prelude::*;
tiles.par_iter().map(tile_bytes).sum()
};
#[cfg(not(feature = "threads"))]
let sum: usize = tiles.iter().map(tile_bytes).sum();
24 + sum
};
let mut lo = 0u8;
let mut hi = max_qp;
if frame_bytes_at(0) <= target_bytes {
return 0;
}
while lo < hi {
let mid = lo + (hi - lo) / 2;
if frame_bytes_at(mid) <= target_bytes {
hi = mid;
} else {
lo = mid + 1;
}
}
lo
}
fn estimate_comp_bits(&self, comp: &PreparedTileComp, c: usize, qp: u8) -> u64 {
let qm = &self.hdr.q_matrix[c];
let depth = self.hdr.bit_depth;
let mut prev_dc: i32 = 0;
let mut prev_dc_diff: u32 = 20;
let mut prev_1st_ac_level: u32 = 0;
let mut bits: u64 = 0;
for f_blk in &comp.blocks {
let coeff = quantize8x8(f_blk, qm, qp, depth);
let dc = coeff[0] as i32;
let diff = dc - prev_dc;
let abs_dc_diff = diff.unsigned_abs();
let k_dc = (prev_dc_diff >> 1).min(5);
bits += vlc_bit_len(abs_dc_diff, k_dc) as u64;
if abs_dc_diff > 0 {
bits += 1;
}
prev_dc = dc;
prev_dc_diff = abs_dc_diff;
let mut scan_pos = 1usize;
let mut first_ac = true;
let mut prev_level = prev_1st_ac_level;
let mut prev_run = 0u32;
while scan_pos < 64 {
let mut run = 0u32;
while scan_pos + (run as usize) < 64 && coeff[ZIGZAG_8X8[scan_pos + (run as usize)] as usize] == 0 {
run += 1;
}
let k_run = (prev_run >> 2).min(2);
bits += vlc_bit_len(run, k_run) as u64;
scan_pos += run as usize;
prev_run = run;
if scan_pos < 64 {
let level = coeff[ZIGZAG_8X8[scan_pos] as usize];
let abs_level = level.unsigned_abs() as u32;
let k_lvl = (prev_level >> 2).min(4);
bits += vlc_bit_len(abs_level - 1, k_lvl) as u64 + 1;
scan_pos += 1;
prev_level = abs_level;
if first_ac {
first_ac = false;
prev_1st_ac_level = prev_level;
}
}
}
}
bits
}
fn encode_tile(&self, tile: &PreparedTile, qp: u8) -> Vec<u8> {
let num_comps = tile.comps.len();
let mut comp_payloads: Vec<Vec<u8>> = Vec::with_capacity(num_comps);
let depth = self.hdr.bit_depth;
for (c, comp) in tile.comps.iter().enumerate() {
let qm = &self.hdr.q_matrix[c];
let mut w = BitWriter::new();
let mut prev_dc: i32 = 0;
let mut prev_dc_diff: u32 = 20;
let mut prev_1st_ac_level: u32 = 0;
for f_blk in &comp.blocks {
let coeff = quantize8x8(f_blk, qm, qp, depth);
let dc = coeff[0] as i32;
let diff = dc - prev_dc;
let abs_dc_diff = diff.unsigned_abs();
let k_dc = (prev_dc_diff >> 1).min(5);
write_vlc(&mut w, abs_dc_diff, k_dc);
if abs_dc_diff > 0 {
w.write_bit(diff < 0);
}
prev_dc = dc;
prev_dc_diff = abs_dc_diff;
let mut scan_pos = 1usize;
let mut first_ac = true;
let mut prev_level = prev_1st_ac_level;
let mut prev_run = 0u32;
while scan_pos < 64 {
let mut run = 0u32;
while scan_pos + (run as usize) < 64 && coeff[ZIGZAG_8X8[scan_pos + (run as usize)] as usize] == 0 {
run += 1;
}
let k_run = (prev_run >> 2).min(2);
write_vlc(&mut w, run, k_run);
scan_pos += run as usize;
prev_run = run;
if scan_pos < 64 {
let level = coeff[ZIGZAG_8X8[scan_pos] as usize];
let abs_level = level.unsigned_abs() as u32;
let k_lvl = (prev_level >> 2).min(4);
write_vlc(&mut w, abs_level - 1, k_lvl);
w.write_bit(level < 0);
scan_pos += 1;
prev_level = abs_level;
if first_ac {
first_ac = false;
prev_1st_ac_level = prev_level;
}
}
}
}
comp_payloads.push(w.finish());
}
// Write tile_header(tileIdx) (RFC 9924 §5.3.13)
let tile_header_size = (5 + 5 * num_comps) as u16;
let total_size = tile_header_size as usize + comp_payloads.iter().map(Vec::len).sum::<usize>();
let mut out = Vec::with_capacity(total_size);
out.extend_from_slice(&tile_header_size.to_be_bytes());
out.extend_from_slice(&(tile.tile_idx as u16).to_be_bytes());
for cp in &comp_payloads {
out.extend_from_slice(&(cp.len() as u32).to_be_bytes());
}
for _ in 0..num_comps {
out.push(qp);
}
out.push(0); // reserved_zero_8bits
for cp in comp_payloads {
out.extend_from_slice(&cp);
}
out
}
}
#[inline]
fn scale_sample(v: u16, from_depth: u8, to_depth: u8) -> u16 {
let max_in = (1u32 << from_depth) - 1;
let v = (v as u32).min(max_in);
if to_depth == from_depth {
v as u16
} else if to_depth > from_depth {
let shift = (to_depth - from_depth) as u32;
((v << shift) | (v >> ((from_depth as u32) - shift))) as u16
} else {
let shift = (from_depth - to_depth) as u32;
let round = 1u32 << (shift - 1);
let max_out = (1u32 << to_depth) - 1;
(((v + round) >> shift).min(max_out)) as u16
}
}