use std::rc::Rc;
use crate::cdf::QuantizerParams;
use crate::encoder::film_grain_elect::grain_seed_for;
use crate::encoder::frame_obu::write_frame_header_obu;
use crate::encoder::inter_frame::{
elect_film_grain, encode_inter_frame_generic, narrow_gop_8bit, EncodedGop, EncodedGopYuv,
GopFrameReconYuv, InterFrameConfig, RefSlotCarry, SavedMotionField, GOP_MAX_FRAMES,
};
use crate::encoder::ivf::{IvfWriter, FOURCC_AV01};
use crate::encoder::key_frame::lambda_for;
use crate::encoder::key_frame::KeyExtras;
use crate::encoder::obu::{build_temporal_unit, ObuFrame};
use crate::encoder::rate_twin::RateModel;
use crate::encoder::yuv_frame::YuvFrame;
use crate::encoder::yuv_frame::{build_intra_only_yuv420_8bit_fh_with_q, Yuv420Frame};
use crate::frame_header::{FrameHeader, FrameType, PRIMARY_REF_NONE};
use crate::obu::ObuType;
use crate::sequence_header::SequenceHeader;
use crate::uncompressed_header_tail::FilmGrainParams;
use crate::Error;
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct PyramidTuning {
pub model: RateModel,
pub max_mini_gop: usize,
pub layer_q_offsets: bool,
pub primary_ref: bool,
pub high_precision_mv: bool,
pub delta_q: bool,
pub qm: bool,
pub cdef: bool,
pub cdef_units: bool,
pub lr: bool,
pub tiles: (u32, u32),
pub tile_groups: u32,
pub ctx_update_elect: bool,
pub superres: bool,
pub film_grain: bool,
pub short_ref_signaling: bool,
}
impl Default for PyramidTuning {
fn default() -> Self {
PyramidTuning {
model: RateModel::Twin,
max_mini_gop: 16,
layer_q_offsets: true,
primary_ref: true,
high_precision_mv: true,
delta_q: true,
qm: true,
cdef: true,
cdef_units: true,
lr: true,
tiles: (0, 0),
tile_groups: 1,
ctx_update_elect: true,
superres: true,
film_grain: true,
short_ref_signaling: true,
}
}
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct TunedPyramidGop {
pub film_grain_elected: bool,
pub gop: EncodedGop,
pub chunk_lengths: Vec<usize>,
pub primary_elections: Vec<(u32, u8)>,
pub ctx_donor_elections: Vec<(u32, Option<u32>)>,
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct TunedPyramidGopYuv {
pub film_grain_elected: bool,
pub gop: EncodedGopYuv,
pub chunk_lengths: Vec<usize>,
pub primary_elections: Vec<(u32, u8)>,
pub ctx_donor_elections: Vec<(u32, Option<u32>)>,
}
struct Role {
display: usize,
show: bool,
refresh: Option<usize>,
last_slot: usize,
bwd_slot: Option<usize>,
alt2_slot: Option<usize>,
alt_ref_slot: Option<usize>,
q_off: i32,
singles: Vec<i8>,
pairs: Vec<[i8; 2]>,
}
enum Step {
Code(Role),
Show(usize),
}
struct BackwardRoles {
bwd: Option<usize>,
alt2: Option<usize>,
alt_ref: Option<usize>,
singles: Vec<i8>,
pairs: Vec<[i8; 2]>,
}
fn backward_roles(chain: &[usize]) -> BackwardRoles {
let bwd = chain.first().copied();
let alt_ref = if chain.len() >= 2 {
chain.last().copied()
} else {
None
};
let alt2 = if chain.len() >= 3 {
Some(chain[1])
} else {
None
};
let mut singles: Vec<i8> = vec![1];
let mut pairs: Vec<[i8; 2]> = Vec::new();
if bwd.is_some() {
singles.push(5);
pairs.push([1, 5]);
}
if alt2.is_some() {
singles.push(6);
pairs.push([1, 6]);
}
if alt_ref.is_some() {
singles.push(7);
pairs.push([1, 7]);
}
BackwardRoles {
bwd,
alt2,
alt_ref,
singles,
pairs,
}
}
fn mid_q_off(level: u32) -> i32 {
match level {
1 => 2,
2 => 4,
_ => 6,
}
}
fn plan_interval(
lo: usize,
hi: usize,
lo_slot: usize,
chain: &[usize],
level: u32,
free: &mut Vec<usize>,
steps: &mut Vec<Step>,
) {
let gap = hi - lo;
if gap <= 1 {
return;
}
if gap == 2 {
let br = backward_roles(chain);
steps.push(Step::Code(Role {
display: lo + 1,
show: true,
refresh: None,
last_slot: lo_slot,
bwd_slot: br.bwd,
alt2_slot: br.alt2,
alt_ref_slot: br.alt_ref,
q_off: 8,
singles: br.singles,
pairs: br.pairs,
}));
return;
}
let mid = (lo + hi).div_ceil(2);
let mid_slot = free.pop().expect("slot free list exhausted (depth > 6)");
let br = backward_roles(chain);
steps.push(Step::Code(Role {
display: mid,
show: false,
refresh: Some(mid_slot),
last_slot: lo_slot,
bwd_slot: br.bwd,
alt2_slot: br.alt2,
alt_ref_slot: br.alt_ref,
q_off: mid_q_off(level),
singles: br.singles,
pairs: br.pairs,
}));
let mut inner_chain = Vec::with_capacity(chain.len() + 1);
inner_chain.push(mid_slot);
inner_chain.extend_from_slice(chain);
plan_interval(lo, mid, lo_slot, &inner_chain, level + 1, free, steps);
steps.push(Step::Show(mid_slot));
plan_interval(mid, hi, mid_slot, chain, level + 1, free, steps);
free.push(mid_slot);
}
fn plan_mini_gop(pos: usize, l: usize, anchor_slot: usize) -> (Vec<Step>, usize) {
let mut free: Vec<usize> = (0..8usize).rev().filter(|&s| s != anchor_slot).collect();
let alt_slot = free.pop().expect("eight slots, one anchor");
let mut steps = Vec::new();
if l == 1 {
steps.push(Step::Code(Role {
display: pos,
show: true,
refresh: Some(alt_slot),
last_slot: anchor_slot,
bwd_slot: None,
alt2_slot: None,
alt_ref_slot: None,
q_off: 0,
singles: vec![1],
pairs: vec![],
}));
return (steps, alt_slot);
}
let hi = pos + l - 1;
steps.push(Step::Code(Role {
display: hi,
show: false,
refresh: Some(alt_slot),
last_slot: anchor_slot,
bwd_slot: None,
alt2_slot: None,
alt_ref_slot: None,
q_off: -3,
singles: vec![1],
pairs: vec![],
}));
plan_interval(
pos - 1,
hi,
anchor_slot,
&[alt_slot],
1,
&mut free,
&mut steps,
);
steps.push(Step::Show(alt_slot));
(steps, alt_slot)
}
#[derive(Clone)]
struct PyramidSession {
film_grain: Option<FilmGrainParams>,
seq: SequenceHeader,
base_q: u8,
tuning: PyramidTuning,
mf_store: [SavedMotionField; 8],
carry_store: [Rc<RefSlotCarry>; 8],
slot_hints: [u32; 8],
slot_display: [usize; 8],
anchor_slot: usize,
recons: Vec<Option<GopFrameReconYuv>>,
temporal_units: Vec<Vec<u8>>,
chunk_lengths: Vec<usize>,
primary_elections: Vec<(u32, u8)>,
slot_wire: [Option<(usize, usize)>; 8],
ctx_donor_elections: Vec<(u32, Option<u32>)>,
}
impl PyramidSession {
fn donor_armed(tuning: &PyramidTuning) -> bool {
tuning.ctx_update_elect && tuning.primary_ref && tuning.tiles != (0, 0)
}
fn new(
frames: &[YuvFrame],
base_q: u8,
tuning: PyramidTuning,
film_grain: Option<&FilmGrainParams>,
) -> Result<Self, Error> {
let n = frames.len();
let (width, height) = (frames[0].width, frames[0].height);
let key_fg: Option<FilmGrainParams> = film_grain.map(|base| {
let mut f = base.clone();
f.grain_seed = grain_seed_for(0);
f
});
let (key, key_carry) = crate::encoder::key_frame::encode_key_frame_yuv_full(
&frames[0],
base_q,
tuning.model,
&[],
None,
tuning.cdef,
tuning.cdef_units,
tuning.lr,
&KeyExtras {
tiles: tuning.tiles,
tile_groups: tuning.tile_groups,
delta_q: true,
qm: tuning.qm,
superres_elect: tuning.superres,
film_grain: key_fg.as_ref(),
collect_donor_cdfs: Self::donor_armed(&tuning),
..KeyExtras::default()
},
)?;
let seq = key.seq.clone();
let mut recons: Vec<Option<GopFrameReconYuv>> = (0..n).map(|_| None).collect();
recons[0] = Some(GopFrameReconYuv {
y: key.recon_y,
u: key.recon_u,
v: key.recon_v,
});
let key_mi = {
let fh0 = build_intra_only_yuv420_8bit_fh_with_q(&seq, width, height, base_q);
let fs0 = fh0.frame_size.expect("KEY builder always sizes");
(fs0.mi_rows, fs0.mi_cols)
};
let key_carry = Rc::new(key_carry);
Ok(PyramidSession {
film_grain: film_grain.cloned(),
seq,
base_q,
tuning,
mf_store: core::array::from_fn(|_| SavedMotionField::intra(key_mi.0, key_mi.1)),
carry_store: core::array::from_fn(|_| key_carry.clone()),
slot_hints: [0u32; 8],
slot_display: [0usize; 8],
anchor_slot: 0,
recons,
temporal_units: vec![key.temporal_unit_bytes],
chunk_lengths: Vec::new(),
primary_elections: Vec::new(),
slot_wire: [Some((0, 0)); 8],
ctx_donor_elections: Vec::new(),
})
}
#[allow(clippy::too_many_arguments)]
fn adopt_short_refs(
rfi: &mut [u8; 7],
singles: &mut Vec<i8>,
pairs: &mut Vec<[i8; 2]>,
bwd_ordinal: &mut u8,
order_hint: u32,
order_hint_bits: u8,
slot_hints: &[u32; 8],
) -> bool {
let derived = crate::frame_header::set_frame_refs(
rfi[0],
rfi[3],
order_hint,
order_hint_bits,
true,
slot_hints,
);
let remap = |o: i8| -> Option<i8> {
let slot = rfi[(o - 1) as usize];
if derived[(o - 1) as usize] == slot {
Some(o)
} else {
derived.iter().position(|&d| d == slot).map(|i| i as i8 + 1)
}
};
let mut new_singles = Vec::with_capacity(singles.len());
for &o in singles.iter() {
match remap(o) {
Some(n) => new_singles.push(n),
None => return false,
}
}
let mut new_pairs = Vec::with_capacity(pairs.len());
for &[a, b] in pairs.iter() {
match (remap(a), remap(b)) {
(Some(x), Some(y)) if x != y => new_pairs.push([x, y]),
_ => return false,
}
}
let Some(new_bwd) = remap(*bwd_ordinal as i8 + 1) else {
return false;
};
*rfi = derived;
*singles = new_singles;
*pairs = new_pairs;
*bwd_ordinal = (new_bwd - 1) as u8;
true
}
fn role_q(&self, role: &Role) -> u8 {
if self.base_q == 0 || !self.tuning.layer_q_offsets {
return self.base_q;
}
(i32::from(self.base_q) + role.q_off).clamp(1, 255) as u8
}
fn encode_chunk(&mut self, frames: &[YuvFrame], pos: usize, l: usize) -> Result<(), Error> {
let donor_armed = Self::donor_armed(&self.tuning);
let (steps, alt_slot) = plan_mini_gop(pos, l, self.anchor_slot);
let mut pending: Vec<ObuFrame> = Vec::new();
let mut pending_slot_idx: [Option<usize>; 8] = [None; 8];
for step in steps {
match step {
Step::Code(role) => {
let mut rfi = [role.last_slot as u8; 7];
if let Some(b) = role.bwd_slot {
rfi[4] = b as u8;
rfi[5] = b as u8;
}
if let Some(a2) = role.alt2_slot {
rfi[5] = a2 as u8;
}
if let Some(a) = role.alt_ref_slot.or(role.bwd_slot) {
rfi[6] = a as u8;
}
let mut singles = role.singles.clone();
let mut pairs = role.pairs.clone();
let mut bwd_ordinal: u8 = 4;
let short_ref_signaling = self.tuning.short_ref_signaling
&& self.seq.enable_order_hint
&& Self::adopt_short_refs(
&mut rfi,
&mut singles,
&mut pairs,
&mut bwd_ordinal,
role.display as u32,
self.seq.order_hint_bits,
&self.slot_hints,
);
let mut displays: Vec<usize> = Vec::new();
let mut slot_to_plane = [0usize; 8];
for (plane, &d) in slot_to_plane.iter_mut().zip(&self.slot_display) {
*plane = displays.iter().position(|&x| x == d).unwrap_or_else(|| {
displays.push(d);
displays.len() - 1
});
}
let refs: Vec<&GopFrameReconYuv> = displays
.iter()
.map(|&d| self.recons[d].as_ref().expect("slot holds a coded frame"))
.collect();
let last_carry;
let bwd_carry;
let (primary_ref_frame, primary_carry, alt_primaries) =
if self.tuning.primary_ref {
last_carry = self.carry_store[role.last_slot].clone();
let mut alts: Vec<(u8, Option<&RefSlotCarry>)> =
vec![(PRIMARY_REF_NONE, None)];
match role.bwd_slot {
Some(b) if b != role.last_slot => {
bwd_carry = self.carry_store[b].clone();
alts.push((bwd_ordinal, Some(&*bwd_carry)));
}
_ => {}
}
(0u8, Some(&*last_carry), alts)
} else {
(PRIMARY_REF_NONE, None, Vec::new())
};
let role_fg: Option<FilmGrainParams> = self.film_grain.as_ref().map(|b| {
let mut f = b.clone();
f.grain_seed = grain_seed_for(role.display as u32);
f
});
let cfg = InterFrameConfig {
order_hint: role.display as u32,
show_frame: role.show,
refresh_frame_flags: role.refresh.map_or(0, |s| 1u8 << s),
ref_frame_idx: rfi,
short_ref_signaling,
slot_hints: self.slot_hints,
single_refs: singles,
compound_pairs: pairs,
refs,
slot_to_plane,
primary_ref_frame,
primary_carry,
allow_temporal_seg: false,
alt_primaries,
exact_mask: None,
auto_lossless: false,
seg_extras: None,
high_precision_mv: self.tuning.high_precision_mv,
delta_q: self.tuning.delta_q,
cdef: self.tuning.cdef,
cdef_units: self.tuning.cdef_units,
lr: self.tuning.lr,
freeze_cdfs: false,
tiles: self.tuning.tiles,
tile_groups: self.tuning.tile_groups,
collect_donor_cdfs: donor_armed && role.refresh.is_some(),
elect_donor: donor_armed,
explicit_tiles: None,
qm: self.tuning.qm,
film_grain: role_fg.as_ref(),
s_frame: false,
error_resilient: false,
tile_spans: false,
superres: None,
superres_source: None,
ref_dims: Vec::new(),
};
let q = self.role_q(&role);
let (obus, rc, saved, carry, aux) = encode_inter_frame_generic(
&frames[role.display],
&self.seq,
q,
&cfg,
&[],
&self.mf_store,
self.tuning.model,
)?;
self.primary_elections
.push((role.display as u32, aux.primary_ref));
let consumed_slot: Option<usize> = if self.tuning.primary_ref {
match aux.primary_ref {
PRIMARY_REF_NONE => None,
o => Some(rfi[o as usize] as usize),
}
} else {
None
};
if let Some(slot) = consumed_slot {
if self.carry_store[slot].donor_cdfs.len() > 1 {
let consumed = self.carry_store[slot].clone();
let fixed_cdfs = match aux.donor_elected {
Some(t) => {
let span = consumed
.ctx_update_span
.expect("donor election only fires on multi-tile frames");
match pending_slot_idx[slot] {
Some(idx) => {
crate::encoder::inter_frame::patch_bits_at(
&mut pending[idx].body,
0,
span,
t,
);
}
None => {
let (tu, ord) = self.slot_wire[slot]
.expect("flushed frame located on the wire");
crate::encoder::inter_frame::patch_ctx_update_in_tu_frame(
&mut self.temporal_units[tu],
ord,
span,
t,
);
}
}
consumed.donor_cdfs[t as usize].clone()
}
None => consumed.cdfs.clone(),
};
let fixed = Rc::new(RefSlotCarry {
cdfs: fixed_cdfs,
segment_ids: consumed.segment_ids.clone(),
mi_rows: consumed.mi_rows,
mi_cols: consumed.mi_cols,
gm_params: consumed.gm_params,
donor_cdfs: Vec::new(),
ctx_update_span: None,
});
for s in self.carry_store.iter_mut() {
if Rc::ptr_eq(s, &consumed) {
*s = fixed.clone();
}
}
}
}
if donor_armed {
self.ctx_donor_elections
.push((role.display as u32, aux.donor_elected));
}
let header_idx = pending.len()
+ obus
.iter()
.position(|o| {
matches!(o.header.obu_type, ObuType::Frame | ObuType::FrameHeader)
})
.expect("coded frame carries a header OBU");
pending.extend(obus);
self.recons[role.display] = Some(rc);
if let Some(s) = role.refresh {
self.mf_store[s] = saved;
self.carry_store[s] = Rc::new(carry);
self.slot_hints[s] = role.display as u32;
self.slot_display[s] = role.display;
pending_slot_idx[s] = Some(header_idx);
self.slot_wire[s] = None;
}
if role.show {
self.flush_pending(&mut pending, &mut pending_slot_idx);
}
}
Step::Show(slot) => {
pending.push(show_existing_obu(&self.seq, slot as u8));
self.flush_pending(&mut pending, &mut pending_slot_idx);
}
}
}
debug_assert!(pending.is_empty(), "every mini-GOP plan ends shown");
self.anchor_slot = alt_slot;
self.chunk_lengths.push(l);
Ok(())
}
fn flush_pending(
&mut self,
pending: &mut Vec<ObuFrame>,
pending_slot_idx: &mut [Option<usize>; 8],
) {
let tu_idx = self.temporal_units.len();
for (s, slot) in pending_slot_idx.iter_mut().enumerate() {
if let Some(idx) = slot.take() {
let ord = pending[..idx]
.iter()
.filter(|o| matches!(o.header.obu_type, ObuType::Frame | ObuType::FrameHeader))
.count();
self.slot_wire[s] = Some((tu_idx, ord));
}
}
self.temporal_units.push(build_temporal_unit(None, pending));
pending.clear();
}
fn total_bytes(&self) -> usize {
self.temporal_units.iter().map(Vec::len).sum()
}
fn distortion_over(&self, frames: &[YuvFrame], range: core::ops::Range<usize>) -> u64 {
let mut d = 0u64;
for pos in range {
let rc = self.recons[pos].as_ref().expect("range coded");
let f = &frames[pos];
for (a, b) in
rc.y.iter()
.zip(&f.y)
.chain(rc.u.iter().zip(&f.u))
.chain(rc.v.iter().zip(&f.v))
{
let diff = i64::from(*a) - i64::from(*b);
d += (diff * diff) as u64;
}
}
d
}
fn finish(self, width: u32, height: u32) -> Result<TunedPyramidGopYuv, Error> {
let mut ivf_bytes: Vec<u8> = Vec::new();
{
let cursor = std::io::Cursor::new(&mut ivf_bytes);
let mut iw = IvfWriter::new(cursor, FOURCC_AV01, width as u16, height as u16, 25, 1)
.map_err(|_| Error::PartitionWalkOutOfRange)?;
for (idx, tu) in self.temporal_units.iter().enumerate() {
iw.write_frame(tu, idx as u64)
.map_err(|_| Error::PartitionWalkOutOfRange)?;
}
iw.patch_frame_count()
.map_err(|_| Error::PartitionWalkOutOfRange)?;
}
Ok(TunedPyramidGopYuv {
film_grain_elected: false,
gop: EncodedGopYuv {
ivf_bytes,
temporal_units: self.temporal_units,
recon: self
.recons
.into_iter()
.map(|r| r.expect("every display position coded"))
.collect(),
seq: self.seq,
},
chunk_lengths: self.chunk_lengths,
primary_elections: self.primary_elections,
ctx_donor_elections: self.ctx_donor_elections,
})
}
}
pub(crate) fn validate_gop_input(frames: &[YuvFrame]) -> Result<(u32, u32), Error> {
if frames.is_empty() || frames.len() > GOP_MAX_FRAMES {
return Err(Error::PartitionWalkOutOfRange);
}
let (width, height) = (frames[0].width, frames[0].height);
let (bit_depth, format) = (frames[0].bit_depth, frames[0].format);
if frames.iter().any(|f| {
f.width != width || f.height != height || f.bit_depth != bit_depth || f.format != format
}) {
return Err(Error::PartitionWalkOutOfRange);
}
Ok((width, height))
}
pub fn encode_pyramid_gop_yuv420(frames: &[Yuv420Frame]) -> Result<EncodedGop, Error> {
encode_pyramid_gop_yuv420_with_q(frames, 0)
}
pub fn encode_pyramid_gop_yuv420_with_q(
frames: &[Yuv420Frame],
base_q_idx: u8,
) -> Result<EncodedGop, Error> {
encode_pyramid_gop_yuv420_with_q_tuned(frames, base_q_idx, PyramidTuning::default())
.map(|t| t.gop)
}
#[doc(hidden)]
pub fn encode_pyramid_gop_yuv420_with_q_rate_model(
frames: &[Yuv420Frame],
base_q_idx: u8,
model: RateModel,
) -> Result<EncodedGop, Error> {
encode_pyramid_gop_yuv420_with_q_tuned(
frames,
base_q_idx,
PyramidTuning {
model,
..PyramidTuning::default()
},
)
.map(|t| t.gop)
}
#[doc(hidden)]
pub fn encode_pyramid_gop_yuv420_with_q_tuned(
frames: &[Yuv420Frame],
base_q_idx: u8,
tuning: PyramidTuning,
) -> Result<TunedPyramidGop, Error> {
let wide: Vec<YuvFrame> = frames.iter().map(YuvFrame::from_yuv420_8bit).collect();
let t = encode_pyramid_gop_yuv_with_q_tuned(&wide, base_q_idx, tuning)?;
Ok(TunedPyramidGop {
film_grain_elected: t.film_grain_elected,
gop: narrow_gop_8bit(t.gop),
chunk_lengths: t.chunk_lengths,
primary_elections: t.primary_elections,
ctx_donor_elections: t.ctx_donor_elections,
})
}
pub fn encode_pyramid_gop_yuv_with_q(
frames: &[YuvFrame],
base_q_idx: u8,
) -> Result<EncodedGopYuv, Error> {
encode_pyramid_gop_yuv_with_q_tuned(frames, base_q_idx, PyramidTuning::default()).map(|t| t.gop)
}
#[doc(hidden)]
pub fn encode_pyramid_gop_yuv_with_q_tuned(
frames: &[YuvFrame],
base_q_idx: u8,
tuning: PyramidTuning,
) -> Result<TunedPyramidGopYuv, Error> {
let plain = encode_pyramid_core(frames, base_q_idx, tuning, None)?;
film_grain_settle(frames, base_q_idx, tuning, plain, |denoised, fg| {
encode_pyramid_core(denoised, base_q_idx, tuning, Some(fg))
})
}
fn film_grain_settle<F>(
frames: &[YuvFrame],
base_q_idx: u8,
tuning: PyramidTuning,
plain: TunedPyramidGopYuv,
encode_arm: F,
) -> Result<TunedPyramidGopYuv, Error>
where
F: FnMut(&[YuvFrame], &FilmGrainParams) -> Result<TunedPyramidGopYuv, Error>,
{
if !(tuning.film_grain
&& base_q_idx > 0
&& tuning.model == RateModel::Twin
&& frames.len() >= 2)
{
return Ok(plain);
}
let elected = elect_film_grain(
frames,
base_q_idx,
plain.gop.seq.color_config.matrix_coefficients,
plain.gop.ivf_bytes.len(),
&plain.gop.recon,
encode_arm,
|c: &TunedPyramidGopYuv| (c.gop.ivf_bytes.len(), c.gop.recon.as_slice()),
)?;
match elected {
Some((cand, grained)) => {
let mut out = cand;
out.gop.recon = grained;
out.film_grain_elected = true;
Ok(out)
}
None => Ok(plain),
}
}
fn encode_pyramid_core(
frames: &[YuvFrame],
base_q_idx: u8,
tuning: PyramidTuning,
film_grain: Option<&FilmGrainParams>,
) -> Result<TunedPyramidGopYuv, Error> {
let (width, height) = validate_gop_input(frames)?;
if tuning.max_mini_gop == 0 || tuning.max_mini_gop > 32 {
return Err(Error::PartitionWalkOutOfRange);
}
let n = frames.len();
let mut session = PyramidSession::new(frames, base_q_idx, tuning, film_grain)?;
let mut pos = 1usize;
while pos < n {
let l = (n - pos).min(tuning.max_mini_gop);
session.encode_chunk(frames, pos, l)?;
pos += l;
}
session.finish(width, height)
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct AdaptiveTuning {
pub pyramid: PyramidTuning,
pub elect: bool,
}
impl Default for AdaptiveTuning {
fn default() -> Self {
AdaptiveTuning {
pyramid: PyramidTuning::default(),
elect: true,
}
}
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct TunedAdaptiveGopYuv {
pub film_grain_elected: bool,
pub gop: EncodedGopYuv,
pub chunk_lengths: Vec<usize>,
pub mc_mads: Vec<f64>,
pub cuts: Vec<bool>,
pub elections: Vec<(usize, usize, usize, usize)>,
pub primary_elections: Vec<(u32, u8)>,
pub ctx_donor_elections: Vec<(u32, Option<u32>)>,
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct TunedAdaptiveGop {
pub film_grain_elected: bool,
pub gop: EncodedGop,
pub chunk_lengths: Vec<usize>,
pub mc_mads: Vec<f64>,
pub cuts: Vec<bool>,
pub elections: Vec<(usize, usize, usize, usize)>,
pub primary_elections: Vec<(u32, u8)>,
pub ctx_donor_elections: Vec<(u32, Option<u32>)>,
}
fn half_res_luma(f: &YuvFrame) -> (Vec<u16>, usize, usize) {
let (w, h) = (f.width as usize, f.height as usize);
let (hw, hh) = (w / 2, h / 2);
let mut out = vec![0u16; hw * hh];
for i in 0..hh {
for j in 0..hw {
let s = u32::from(f.y[(2 * i) * w + 2 * j])
+ u32::from(f.y[(2 * i) * w + 2 * j + 1])
+ u32::from(f.y[(2 * i + 1) * w + 2 * j])
+ u32::from(f.y[(2 * i + 1) * w + 2 * j + 1]);
out[i * hw + j] = ((s + 2) / 4) as u16;
}
}
(out, hw, hh)
}
fn mc_mad(prev: &[u16], cur: &[u16], w: usize, h: usize) -> f64 {
let mut best = f64::INFINITY;
for dy in -3i32..=3 {
for dx in -3i32..=3 {
let y0 = dy.max(0) as usize;
let y1 = (h as i32 + dy.min(0)) as usize;
let x0 = dx.max(0) as usize;
let x1 = (w as i32 + dx.min(0)) as usize;
if y1 <= y0 || x1 <= x0 {
continue;
}
let mut sum = 0u64;
for i in y0..y1 {
let pi = (i as i32 - dy) as usize;
for j in x0..x1 {
let pj = (j as i32 - dx) as usize;
sum += u64::from(cur[i * w + j].abs_diff(prev[pi * w + pj]));
}
}
let mad = sum as f64 / ((y1 - y0) * (x1 - x0)) as f64;
if mad < best {
best = mad;
}
}
}
best
}
const CUT_MC_MAD: f64 = 20.0;
fn class_len(mean_mc_mad: f64, cap: usize) -> usize {
let target = if mean_mc_mad < 1.5 {
16
} else if mean_mc_mad < 6.0 {
8
} else if mean_mc_mad < 12.0 {
4
} else if mean_mc_mad < CUT_MC_MAD {
2
} else {
1
};
target.min(cap).max(1)
}
pub fn encode_adaptive_gop_yuv420_with_q(
frames: &[Yuv420Frame],
base_q_idx: u8,
) -> Result<EncodedGop, Error> {
encode_adaptive_gop_yuv420_with_q_tuned(frames, base_q_idx, AdaptiveTuning::default())
.map(|t| t.gop)
}
#[doc(hidden)]
pub fn encode_adaptive_gop_yuv420_with_q_tuned(
frames: &[Yuv420Frame],
base_q_idx: u8,
tuning: AdaptiveTuning,
) -> Result<TunedAdaptiveGop, Error> {
let wide: Vec<YuvFrame> = frames.iter().map(YuvFrame::from_yuv420_8bit).collect();
let t = encode_adaptive_gop_yuv_with_q_tuned(&wide, base_q_idx, tuning)?;
Ok(TunedAdaptiveGop {
film_grain_elected: t.film_grain_elected,
gop: narrow_gop_8bit(t.gop),
chunk_lengths: t.chunk_lengths,
mc_mads: t.mc_mads,
cuts: t.cuts,
elections: t.elections,
primary_elections: t.primary_elections,
ctx_donor_elections: t.ctx_donor_elections,
})
}
pub fn encode_adaptive_gop_yuv_with_q(
frames: &[YuvFrame],
base_q_idx: u8,
) -> Result<EncodedGopYuv, Error> {
encode_adaptive_gop_yuv_with_q_tuned(frames, base_q_idx, AdaptiveTuning::default())
.map(|t| t.gop)
}
#[doc(hidden)]
pub fn encode_adaptive_gop_yuv_with_q_tuned(
frames: &[YuvFrame],
base_q_idx: u8,
tuning: AdaptiveTuning,
) -> Result<TunedAdaptiveGopYuv, Error> {
let plain = encode_adaptive_core(frames, base_q_idx, tuning, None)?;
let plain_pyr = TunedPyramidGopYuv {
film_grain_elected: false,
gop: plain.gop,
chunk_lengths: plain.chunk_lengths,
primary_elections: plain.primary_elections,
ctx_donor_elections: plain.ctx_donor_elections,
};
let (mc_mads, cuts, elections) = (plain.mc_mads, plain.cuts, plain.elections);
let mut arm_trace: Option<AdaptiveTrace> = None;
let settled = film_grain_settle(frames, base_q_idx, tuning.pyramid, plain_pyr, |den, fg| {
let t = encode_adaptive_core(den, base_q_idx, tuning, Some(fg))?;
arm_trace = Some((t.mc_mads, t.cuts, t.elections));
Ok(TunedPyramidGopYuv {
film_grain_elected: false,
gop: t.gop,
chunk_lengths: t.chunk_lengths,
primary_elections: t.primary_elections,
ctx_donor_elections: t.ctx_donor_elections,
})
})?;
let (mc_mads, cuts, elections) = if settled.film_grain_elected {
arm_trace.expect("an elected arm ran the adaptive driver")
} else {
(mc_mads, cuts, elections)
};
Ok(TunedAdaptiveGopYuv {
film_grain_elected: settled.film_grain_elected,
gop: settled.gop,
chunk_lengths: settled.chunk_lengths,
mc_mads,
cuts,
elections,
primary_elections: settled.primary_elections,
ctx_donor_elections: settled.ctx_donor_elections,
})
}
type AdaptiveTrace = (Vec<f64>, Vec<bool>, Vec<(usize, usize, usize, usize)>);
fn encode_adaptive_core(
frames: &[YuvFrame],
base_q_idx: u8,
tuning: AdaptiveTuning,
film_grain: Option<&FilmGrainParams>,
) -> Result<TunedAdaptiveGopYuv, Error> {
let (width, height) = validate_gop_input(frames)?;
if tuning.pyramid.max_mini_gop == 0 || tuning.pyramid.max_mini_gop > 32 {
return Err(Error::PartitionWalkOutOfRange);
}
let n = frames.len();
let halves: Vec<(Vec<u16>, usize, usize)> = frames.iter().map(half_res_luma).collect();
let depth_scale = f64::from(1u32 << (frames[0].bit_depth - 8));
let mc_mads: Vec<f64> = (0..n.saturating_sub(1))
.map(|k| {
let (p, w, h) = &halves[k];
let (c, _, _) = &halves[k + 1];
mc_mad(p, c, *w, *h) / depth_scale
})
.collect();
let cuts: Vec<bool> = mc_mads.iter().map(|&m| m > CUT_MC_MAD).collect();
let lambda = lambda_for(&QuantizerParams::neutral(base_q_idx, frames[0].bit_depth));
let mut session = PyramidSession::new(frames, base_q_idx, tuning.pyramid, film_grain)?;
let mut elections: Vec<(usize, usize, usize, usize)> = Vec::new();
let mut pos = 1usize;
while pos < n {
let remaining = n - pos;
let mut w = 1usize;
if !cuts[pos - 1] {
while w < remaining.min(tuning.pyramid.max_mini_gop) && !cuts[pos + w - 1] {
w += 1;
}
}
let l_main = if cuts[pos - 1] {
1
} else {
let mean = mc_mads[pos - 1..pos + w - 1].iter().sum::<f64>() / w as f64;
class_len(mean, w)
};
let l_alt = l_main / 2;
if !tuning.elect || l_alt == 0 || l_alt == l_main {
session.encode_chunk(frames, pos, l_main)?;
pos += l_main;
continue;
}
let base_bytes = session.total_bytes();
let mut deep = session.clone();
deep.encode_chunk(frames, pos, l_main)?;
let mut split = session.clone();
let mut covered = 0usize;
while covered < l_main {
let step = l_alt.min(l_main - covered);
split.encode_chunk(frames, pos + covered, step)?;
covered += step;
}
let deep_j = deep.distortion_over(frames, pos..pos + l_main) * 256
+ lambda * ((deep.total_bytes() - base_bytes) as u64 * 8 * 256);
let split_j = split.distortion_over(frames, pos..pos + l_main) * 256
+ lambda * ((split.total_bytes() - base_bytes) as u64 * 8 * 256);
let chosen = if deep_j <= split_j { l_main } else { l_alt };
elections.push((pos, l_main, l_alt, chosen));
session = if deep_j <= split_j { deep } else { split };
pos += l_main;
}
let tuned = session.finish(width, height)?;
Ok(TunedAdaptiveGopYuv {
film_grain_elected: false,
gop: tuned.gop,
chunk_lengths: tuned.chunk_lengths,
mc_mads,
cuts,
elections,
primary_elections: tuned.primary_elections,
ctx_donor_elections: tuned.ctx_donor_elections,
})
}
fn show_existing_obu(seq: &SequenceHeader, map_idx: u8) -> ObuFrame {
let fh = FrameHeader {
show_existing_frame: true,
frame_to_show_map_idx: Some(map_idx),
display_frame_id: None,
frame_type: FrameType::Inter,
frame_is_intra: false,
show_frame: true,
showable_frame: false,
error_resilient_mode: false,
disable_cdf_update: false,
allow_screen_content_tools: false,
force_integer_mv: false,
current_frame_id: 0,
frame_size_override_flag: false,
order_hint: 0,
primary_ref_frame: PRIMARY_REF_NONE,
refresh_frame_flags: 0,
ref_order_hints: None,
frame_size: None,
allow_intrabc: false,
disable_frame_end_update_cdf: false,
tile_info: None,
quantization_params: None,
segmentation_params: None,
delta_q_params: None,
delta_lf_params: None,
loop_filter_params: None,
cdef_params: None,
lr_params: None,
tx_mode: None,
reference_select: None,
skip_mode_present: None,
skip_mode_frame: None,
allow_warped_motion: None,
reduced_tx_set: None,
global_motion_params: None,
film_grain_params: None,
inter_refs: None,
bits_consumed: 0,
};
let body = write_frame_header_obu(&fh, seq);
ObuFrame::new(ObuType::FrameHeader, body)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encoder::yuv_frame::ChromaFormat;
fn plane8(p: &[u16]) -> Vec<u8> {
p.iter().map(|&s| s as u8).collect()
}
fn moving_gradient(w: u32, h: u32, shift_y: usize, shift_x: usize, seed: u32) -> YuvFrame {
let (wu, hu) = (w as usize, h as usize);
let s = seed as usize;
let mut f = YuvFrame::filled(w, h, 8, ChromaFormat::Yuv420, 0);
for i in 0..hu {
for j in 0..wu {
let (si, sj) = (i + shift_y, j + shift_x);
f.y[i * wu + j] = ((si * 5 + sj * 3 + (si / 16) * (sj / 16) + s) % 256) as u16;
}
}
let (cw, ch) = (wu / 2, hu / 2);
for i in 0..ch {
for j in 0..cw {
let (si, sj) = (i + shift_y / 2, j + shift_x / 2);
f.u[i * cw + j] = ((128 + si * 2 + sj + s) % 256) as u16;
f.v[i * cw + j] = ((64 + si + sj * 2 + s) % 256) as u16;
}
}
f
}
fn assert_pyramid_round_trip(frames: &[YuvFrame], q: u8) -> EncodedGopYuv {
let (w, h) = (frames[0].width, frames[0].height);
let enc = encode_pyramid_gop_yuv_with_q(frames, q)
.unwrap_or_else(|e| panic!("{w}x{h} q={q}: pyramid encode failed: {e:?}"));
assert_eq!(enc.recon.len(), frames.len());
let decoded = crate::decoder::decode_av1_spec(&enc.ivf_bytes)
.unwrap_or_else(|e| panic!("{w}x{h} q={q}: spec driver rejected pyramid GOP: {e:?}"));
assert_eq!(
decoded.len(),
frames.len(),
"{w}x{h} q={q}: one shown frame per input (display order)"
);
for (idx, f) in decoded.iter().enumerate() {
assert_eq!((f.width, f.height), (w, h));
let rc = &enc.recon[idx];
assert_eq!(
f.planes[0],
plane8(&rc.y),
"{w}x{h} q={q} display {idx}: luma"
);
assert_eq!(f.planes[1], plane8(&rc.u), "{w}x{h} q={q} display {idx}: U");
assert_eq!(f.planes[2], plane8(&rc.v), "{w}x{h} q={q} display {idx}: V");
if q == 0 {
assert_eq!(
f.planes[0],
plane8(&frames[idx].y),
"lossless {idx}: luma != input"
);
assert_eq!(
f.planes[1],
plane8(&frames[idx].u),
"lossless {idx}: U != input"
);
assert_eq!(
f.planes[2],
plane8(&frames[idx].v),
"lossless {idx}: V != input"
);
}
}
enc
}
#[test]
fn r415_pyramid_len5_lossy_round_trips() {
let frames: Vec<YuvFrame> = (0..5)
.map(|k| moving_gradient(64, 64, 2 * k, 3 * k, 11))
.collect();
let enc = assert_pyramid_round_trip(&frames, 60);
assert_eq!(enc.temporal_units.len(), 5);
}
#[test]
fn r415_pyramid_len3_lossless_round_trips() {
let frames: Vec<YuvFrame> = (0..3)
.map(|k| moving_gradient(64, 64, 3 * k, 5 * k, 9))
.collect();
let enc = assert_pyramid_round_trip(&frames, 0);
assert_eq!(enc.temporal_units.len(), 3);
}
#[test]
fn r424_pyramid_len9_three_levels_round_trips() {
let frames: Vec<YuvFrame> = (0..9)
.map(|k| moving_gradient(64, 64, 2 * k, 3 * k, 23))
.collect();
let enc = assert_pyramid_round_trip(&frames, 60);
assert_eq!(enc.temporal_units.len(), 9);
}
#[test]
fn r424_planner_covers_every_position_once() {
for max in [1usize, 2, 3, 4, 8, 16] {
for n in 1..=17usize {
let mut covered = vec![false; n + 1];
let mut pos = 1usize;
let mut anchor = 0usize;
while pos <= n {
let l = (n + 1 - pos).min(max);
let (steps, alt) = plan_mini_gop(pos, l, anchor);
let mut shows = 0usize;
for s in &steps {
match s {
Step::Code(role) => {
assert!(
!covered[role.display],
"max={max} n={n}: display {} coded twice",
role.display
);
covered[role.display] = true;
if let Some(r) = role.refresh {
assert_ne!(r, anchor, "must not clobber the live anchor");
}
if role.show {
shows += 1;
}
}
Step::Show(_) => shows += 1,
}
}
assert_eq!(shows, l, "max={max} n={n}: one display per chunk frame");
pos += l;
anchor = alt;
}
assert!(
covered[1..=n].iter().all(|&c| c),
"max={max} n={n}: every position coded"
);
}
}
}
#[test]
fn r424_adaptive_scene_cut_round_trips() {
let n = 9usize;
let frames: Vec<YuvFrame> = (0..n)
.map(|k| {
if k >= n / 2 {
moving_gradient(64, 64, 40 + 2 * k, 70 + 3 * k, 901)
} else {
moving_gradient(64, 64, 2 * k, 3 * k, 5)
}
})
.collect();
let tuned = encode_adaptive_gop_yuv_with_q_tuned(&frames, 60, AdaptiveTuning::default())
.expect("adaptive encode");
assert!(
tuned.cuts.iter().any(|&c| c),
"the constructed cut must trip the probe: {:?}",
tuned.mc_mads
);
let decoded = crate::decoder::decode_av1_spec(&tuned.gop.ivf_bytes)
.expect("spec driver accepts adaptive stream");
assert_eq!(decoded.len(), n);
for (idx, f) in decoded.iter().enumerate() {
assert_eq!(
f.planes[0],
plane8(&tuned.gop.recon[idx].y),
"display {idx} luma"
);
}
}
}