use crate::codec_config::Av1CodecConfig;
use crate::encoder::key_frame::{encode_key_frame_yuv_full, EncodedKeyFrameYuv, KeyExtras};
use crate::encoder::rate_twin::RateModel;
use crate::encoder::yuv_frame::{Yuv420Frame, YuvFrame};
use crate::frame_header::FrameHeader;
use crate::sequence_header::{SequenceHeader, SELECT_INTEGER_MV, SELECT_SCREEN_CONTENT_TOOLS};
use crate::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StillSpeed {
#[default]
Fast,
Balanced,
Thorough,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StillOptions {
pub base_q_idx: u8,
pub tile_cols_log2: u32,
pub tile_rows_log2: u32,
pub speed: StillSpeed,
pub full_range: bool,
pub reduced_header: bool,
pub color_description: Option<(u8, u8, u8)>,
pub threads: usize,
pub auto_tiles: bool,
}
impl StillOptions {
#[must_use]
pub fn new(base_q_idx: u8) -> Self {
Self {
base_q_idx,
tile_cols_log2: 0,
tile_rows_log2: 0,
speed: StillSpeed::Fast,
full_range: false,
reduced_header: true,
color_description: None,
threads: 1,
auto_tiles: false,
}
}
#[must_use]
pub fn threads(mut self, threads: usize) -> Self {
self.threads = threads.max(1);
self.auto_tiles = self.threads > 1;
self
}
#[must_use]
pub fn lossless(mut self) -> Self {
self.base_q_idx = 0;
self
}
#[must_use]
pub fn from_quality(quality: u8) -> Self {
Self::new(quality_to_base_q_idx(quality))
}
}
impl Default for StillOptions {
fn default() -> Self {
Self::new(DEFAULT_STILL_BASE_Q_IDX)
}
}
pub const DEFAULT_STILL_BASE_Q_IDX: u8 = 120;
#[must_use]
pub fn quality_to_base_q_idx(quality: u8) -> u8 {
let q = u32::from(quality.min(100));
(((100 - q) * 255 + 50) / 100) as u8
}
#[derive(Debug, Clone)]
pub struct EncodedStill {
pub temporal_unit_bytes: Vec<u8>,
pub ivf_bytes: Vec<u8>,
pub recon_y: Vec<u16>,
pub recon_u: Vec<u16>,
pub recon_v: Vec<u16>,
pub seq: SequenceHeader,
pub fh: FrameHeader,
pub codec_config: Av1CodecConfig,
}
impl EncodedStill {
fn from_key(k: EncodedKeyFrameYuv) -> Self {
let codec_config = Av1CodecConfig::from_sequence_header(&k.seq);
Self {
temporal_unit_bytes: k.temporal_unit_bytes,
ivf_bytes: k.ivf_bytes,
recon_y: k.recon_y,
recon_u: k.recon_u,
recon_v: k.recon_v,
seq: k.seq,
fh: k.fh,
codec_config,
}
}
}
const LEVEL_PIC_LIMITS: [(u8, u64, u32, u32); 6] = [
(0, 147_456, 2048, 1152), (1, 278_784, 2816, 1584), (4, 665_856, 4352, 2448), (5, 1_065_024, 5504, 3096), (8, 2_359_296, 6144, 3456), (12, 8_912_896, 8192, 4352), ];
const LEVEL_6_0: (u8, u64, u32, u32) = (16, 35_651_584, 16384, 8704);
pub const SEQ_LEVEL_IDX_MAX_PARAMETERS: u8 = 31;
#[must_use]
pub fn elect_seq_level_idx(width: u32, height: u32) -> u8 {
let pic = u64::from(width) * u64::from(height);
LEVEL_PIC_LIMITS
.iter()
.chain(core::iter::once(&LEVEL_6_0))
.find(|&&(_, max_pic, max_h, max_v)| pic <= max_pic && width <= max_h && height <= max_v)
.map_or(SEQ_LEVEL_IDX_MAX_PARAMETERS, |&(idx, _, _, _)| idx)
}
pub fn apply_still_picture_shape(seq: &mut SequenceHeader) {
seq.still_picture = true;
seq.reduced_still_picture_header = true;
seq.timing_info_present_flag = false;
seq.timing_info = None;
seq.decoder_model_info_present_flag = false;
seq.decoder_model_info = None;
seq.initial_display_delay_present_flag = false;
seq.operating_points_cnt_minus_1 = 0;
seq.operating_points.truncate(1);
if let Some(op) = seq.operating_points.first_mut() {
op.operating_point_idc = 0;
op.seq_level_idx = elect_seq_level_idx(
seq.max_frame_width_minus_1 + 1,
seq.max_frame_height_minus_1 + 1,
);
op.seq_tier = 0;
op.decoder_model_present_for_this_op = false;
op.operating_parameters_info = None;
op.initial_display_delay_present_for_this_op = false;
op.initial_display_delay_minus_1 = None;
}
seq.frame_id_numbers_present_flag = false;
seq.delta_frame_id_length_minus_2 = 0;
seq.additional_frame_id_length_minus_1 = 0;
seq.enable_interintra_compound = false;
seq.enable_masked_compound = false;
seq.enable_warped_motion = false;
seq.enable_dual_filter = false;
seq.enable_order_hint = false;
seq.enable_jnt_comp = false;
seq.enable_ref_frame_mvs = false;
seq.seq_force_screen_content_tools = SELECT_SCREEN_CONTENT_TOOLS;
seq.seq_force_integer_mv = SELECT_INTEGER_MV;
seq.order_hint_bits = 0;
}
pub fn encode_still_yuv(input: &YuvFrame, opts: &StillOptions) -> Result<EncodedStill, Error> {
encode_intra_yuv(input, opts, true)
}
pub fn encode_key_frame_yuv_with_options(
input: &YuvFrame,
opts: &StillOptions,
) -> Result<EncodedStill, Error> {
encode_intra_yuv(input, opts, false)
}
fn encode_intra_yuv(
input: &YuvFrame,
opts: &StillOptions,
still_picture: bool,
) -> Result<EncodedStill, Error> {
let elections = opts.speed == StillSpeed::Thorough && opts.base_q_idx > 0;
let tiles = if opts.auto_tiles {
auto_tile_layout(input.width, input.height, opts.threads)
} else {
(opts.tile_cols_log2, opts.tile_rows_log2)
};
let mut search = match opts.speed {
StillSpeed::Fast => crate::encoder::key_frame::SearchLimits::fast(),
StillSpeed::Balanced => crate::encoder::key_frame::SearchLimits::balanced(),
StillSpeed::Thorough => crate::encoder::key_frame::SearchLimits::default(),
};
search.threads = opts.threads.max(1);
let extras = KeyExtras {
tiles,
delta_q: elections,
qm: opts.speed == StillSpeed::Thorough && opts.base_q_idx > 0,
superres_elect: opts.speed == StillSpeed::Thorough && opts.base_q_idx > 0,
still: still_picture && opts.reduced_header,
full_range: opts.full_range,
still_full_header: still_picture && !opts.reduced_header,
color_description: opts.color_description,
search,
..KeyExtras::default()
};
let (k, _carry) = encode_key_frame_yuv_full(
input,
opts.base_q_idx,
RateModel::Twin,
&[],
None,
true,
true,
true,
&extras,
)?;
Ok(EncodedStill::from_key(k))
}
#[must_use]
pub fn auto_tile_layout(width: u32, height: u32, threads: usize) -> (u32, u32) {
let target = threads.max(1).next_power_of_two();
let sb_cols = width.div_ceil(64);
let sb_rows = height.div_ceil(64);
let (mut cols_log2, mut rows_log2) = (0u32, 0u32);
while (1usize << (cols_log2 + 1)) <= target && (sb_cols >> (cols_log2 + 1)) >= 4 {
cols_log2 += 1;
}
while (1usize << (cols_log2 + rows_log2 + 1)) <= target && (sb_rows >> (rows_log2 + 1)) >= 2 {
rows_log2 += 1;
}
(cols_log2, rows_log2)
}
pub fn encode_still_yuv420(
input: &Yuv420Frame,
opts: &StillOptions,
) -> Result<EncodedStill, Error> {
encode_still_yuv(&YuvFrame::from_yuv420_8bit(input), opts)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decoder::decode_av1_spec;
use crate::encoder::yuv_frame::ChromaFormat;
fn textured(width: u32, height: u32, bit_depth: u8, format: ChromaFormat) -> YuvFrame {
let mut f = YuvFrame::filled(width, height, bit_depth, format, 0);
let max = (1u32 << bit_depth) - 1;
for y in 0..height {
for x in 0..width {
let v = (x * 37 + y * 91 + (x * y) % 23) * max / (width * 37 + height * 91 + 23);
f.y[(y * width + x) as usize] = v as u16;
}
}
let (cw, ch) = (f.chroma_width(), f.chroma_height());
for y in 0..ch {
for x in 0..cw {
f.u[(y * cw + x) as usize] = ((x * 53 + y * 7) % max) as u16;
f.v[(y * cw + x) as usize] = ((x * 11 + y * 61) % max) as u16;
}
}
f
}
fn assert_decodes_to_recon(still: &EncodedStill, bit_depth: u8) {
let frames = decode_av1_spec(&still.ivf_bytes).expect("still decodes");
assert_eq!(frames.len(), 1);
let f = &frames[0];
let widen = |p: &[u8]| -> Vec<u16> {
if bit_depth > 8 {
p.chunks(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect()
} else {
p.iter().map(|&b| u16::from(b)).collect()
}
};
assert_eq!(widen(&f.planes[0]), still.recon_y, "luma");
if f.planes.len() > 1 {
assert_eq!(widen(&f.planes[1]), still.recon_u, "u");
assert_eq!(widen(&f.planes[2]), still.recon_v, "v");
} else {
assert!(still.recon_u.is_empty());
}
}
#[test]
fn level_election_follows_annex_a_picture_limits() {
assert_eq!(elect_seq_level_idx(426, 240), 0);
assert_eq!(elect_seq_level_idx(640, 480), 4);
assert_eq!(elect_seq_level_idx(1280, 720), 5);
assert_eq!(elect_seq_level_idx(1920, 1080), 8);
assert_eq!(elect_seq_level_idx(3840, 2160), 12);
assert_eq!(elect_seq_level_idx(4032, 3024), 16);
assert_eq!(elect_seq_level_idx(2048, 1152), 8);
assert_eq!(elect_seq_level_idx(512, 288), 0);
assert_eq!(elect_seq_level_idx(2049, 8), 1);
assert_eq!(elect_seq_level_idx(16385, 8), SEQ_LEVEL_IDX_MAX_PARAMETERS);
assert_eq!(elect_seq_level_idx(8, 8705), SEQ_LEVEL_IDX_MAX_PARAMETERS);
}
#[test]
fn quality_dial_endpoints() {
assert_eq!(quality_to_base_q_idx(100), 0);
assert_eq!(quality_to_base_q_idx(0), 255);
assert_eq!(quality_to_base_q_idx(50), 128);
assert_eq!(quality_to_base_q_idx(200), 0);
}
#[test]
fn reduced_still_round_trips_every_format_pairing() {
for &(bd, fmt) in &[
(8u8, ChromaFormat::Yuv420),
(8, ChromaFormat::Monochrome),
(10, ChromaFormat::Yuv422),
(12, ChromaFormat::Yuv444),
] {
let input = textured(48, 40, bd, fmt);
let still = encode_still_yuv(&input, &StillOptions::new(96)).expect("still encodes");
assert!(still.seq.still_picture && still.seq.reduced_still_picture_header);
assert_eq!(still.seq.operating_points.len(), 1);
assert_eq!(still.seq.operating_points[0].seq_level_idx, 0);
assert!(!still.seq.enable_order_hint && still.seq.order_hint_bits == 0);
assert!(still.fh.disable_frame_end_update_cdf);
assert_eq!(still.codec_config.bit_depth(), bd);
assert_eq!(
still.codec_config.monochrome,
fmt == ChromaFormat::Monochrome
);
assert!(still.codec_config.matches_sequence_header(&still.seq));
assert_decodes_to_recon(&still, bd);
}
}
#[test]
fn lossless_still_reproduces_the_input() {
let input = textured(40, 24, 8, ChromaFormat::Yuv420);
let still =
encode_still_yuv(&input, &StillOptions::new(0)).expect("lossless still encodes");
assert_eq!(still.recon_y, input.y);
assert_eq!(still.recon_u, input.u);
assert_eq!(still.recon_v, input.v);
assert_decodes_to_recon(&still, 8);
}
#[test]
fn full_header_still_and_options_round_trip() {
let input = textured(64, 32, 8, ChromaFormat::Yuv420);
let mut opts = StillOptions::new(140);
opts.reduced_header = false;
opts.full_range = true;
opts.speed = StillSpeed::Fast;
opts.color_description = Some((1, 13, 6));
let still = encode_still_yuv(&input, &opts).expect("full-header still encodes");
assert!(still.seq.still_picture && !still.seq.reduced_still_picture_header);
assert!(still.seq.color_config.color_range);
let cc = &still.seq.color_config;
assert!(cc.color_description_present_flag);
assert_eq!(
(
cc.color_primaries,
cc.transfer_characteristics,
cc.matrix_coefficients
),
(1, 13, 6)
);
let reparsed = crate::sequence_header::parse_sequence_header(
crate::obu::ObuIter::new(&still.temporal_unit_bytes)
.filter_map(Result::ok)
.find(|d| d.obu_type == crate::obu::ObuType::SequenceHeader)
.expect("sequence header")
.payload,
)
.expect("parses");
assert_eq!(reparsed.color_config, still.seq.color_config);
assert_eq!(still.seq.operating_points[0].seq_level_idx, 0);
assert_decodes_to_recon(&still, 8);
let yuv420 = Yuv420Frame::filled(16, 16, 77);
let s2 = encode_still_yuv420(&yuv420, &StillOptions::default()).expect("encodes");
assert_eq!(s2.recon_y.len(), 256);
assert_decodes_to_recon(&s2, 8);
}
#[test]
fn arbitrary_extent_stills_round_trip() {
for &(w, h, bd, fmt, q) in &[
(37u32, 19u32, 8u8, ChromaFormat::Yuv420, 0u8),
(37, 19, 8, ChromaFormat::Yuv420, 110),
(1, 1, 8, ChromaFormat::Yuv420, 90),
(3, 5, 8, ChromaFormat::Monochrome, 0),
(7, 3, 10, ChromaFormat::Yuv444, 60),
(65, 9, 12, ChromaFormat::Yuv422, 140),
(16, 13, 8, ChromaFormat::Yuv420, 200),
(9, 7, 8, ChromaFormat::Yuv420, 0),
(9, 7, 8, ChromaFormat::Yuv420, 120),
] {
let input = textured(w, h, bd, fmt);
assert_eq!(
input.chroma_width(),
if fmt == ChromaFormat::Monochrome {
0
} else {
(w + u32::from(fmt.subsampling().0)) >> fmt.subsampling().0
}
);
let mut opts = StillOptions::new(q);
opts.speed = StillSpeed::Fast;
let still = encode_still_yuv(&input, &opts)
.unwrap_or_else(|e| panic!("{w}x{h} {bd}-bit {fmt:?} q{q}: {e:?}"));
let fs = still.fh.frame_size.as_ref().expect("frame size");
assert_eq!((fs.frame_width, fs.frame_height), (w, h));
assert_eq!(still.seq.max_frame_width_minus_1 + 1, w);
assert_eq!(still.recon_y.len(), (w * h) as usize);
assert_eq!(
still.recon_u.len(),
(input.chroma_width() * input.chroma_height()) as usize
);
if q == 0 {
assert_eq!(still.recon_y, input.y, "{w}x{h}: lossless luma");
assert_eq!(still.recon_u, input.u, "{w}x{h}: lossless u");
}
assert_decodes_to_recon(&still, bd);
}
}
#[test]
fn threaded_tile_search_is_bit_identical() {
let input = textured(320, 192, 8, ChromaFormat::Yuv420);
let mut seq_opts = StillOptions::new(120);
seq_opts.speed = StillSpeed::Fast;
seq_opts.tile_cols_log2 = 1;
seq_opts.tile_rows_log2 = 1;
let mut par_opts = seq_opts;
par_opts.threads = 4;
let a = encode_still_yuv(&input, &seq_opts).expect("sequential");
let b = encode_still_yuv(&input, &par_opts).expect("threaded");
assert_eq!(a.temporal_unit_bytes, b.temporal_unit_bytes);
assert_eq!(a.recon_y, b.recon_y);
assert_eq!(a.recon_u, b.recon_u);
assert_decodes_to_recon(&b, 8);
assert_eq!(auto_tile_layout(4032, 3024, 8), (3, 0));
assert_eq!(auto_tile_layout(4032, 3024, 4), (2, 0));
assert_eq!(auto_tile_layout(640, 480, 8), (1, 2));
assert_eq!(auto_tile_layout(64, 64, 8), (0, 0));
let auto = StillOptions::new(120).threads(4);
assert!(auto.auto_tiles && auto.threads == 4);
}
#[test]
fn oversize_pictures_take_the_minimum_legal_tile_layout() {
assert_eq!(
crate::tile_info::TileInfo::min_uniform_layout(2 * 4032 / 8, 2 * 3024 / 8, false),
Some((0, 1))
);
assert_eq!(
crate::tile_info::TileInfo::min_uniform_layout(2 * 640 / 8, 2 * 480 / 8, false),
Some((0, 0))
);
let input = YuvFrame::filled(4096, 2432, 8, ChromaFormat::Monochrome, 90);
let mut opts = StillOptions::new(200);
opts.speed = StillSpeed::Fast;
let still = encode_still_yuv(&input, &opts).expect("12 MP still encodes");
let ti = still.fh.tile_info.as_ref().expect("tile info");
assert!(
ti.tile_cols * ti.tile_rows >= 2,
"{}x{}",
ti.tile_cols,
ti.tile_rows
);
assert_eq!(still.seq.operating_points[0].seq_level_idx, 16);
}
#[test]
fn tiled_still_round_trips() {
let input = textured(256, 128, 8, ChromaFormat::Yuv420);
let mut opts = StillOptions::new(110);
opts.tile_cols_log2 = 1;
opts.tile_rows_log2 = 1;
opts.speed = StillSpeed::Fast;
let still = encode_still_yuv(&input, &opts).expect("tiled still encodes");
let ti = still.fh.tile_info.as_ref().expect("tile info");
assert_eq!((ti.tile_cols, ti.tile_rows), (2, 2));
assert_decodes_to_recon(&still, 8);
}
}