mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-24 10:10:05 -06:00
599 lines
23 KiB
Rust
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
|
|
}
|
|
}
|