Merge pull request #1067 from vangel76/perf-hevc-u8

#1039: 8-bit HEVC pictures as u8, in-place motion compensation (−14…23 % per frame), big-picture analysis
This commit is contained in:
Brandon Thomas
2026-10-11 22:36:31 -04:00
8 changed files with 862 additions and 315 deletions
+4 -2
View File
@@ -64,8 +64,10 @@ POC / RPS / DPB bookkeeping; the CTU layer of each picture runs as a job (rayon
reconstructs into private planes, deblocks CTB row r once row r+1 is decoded, applies SAO to row r
once row r+1 is deblocked, and publishes final rows (samples + 16x16 compressed motion for TMVP)
into the shared `Frame` (`OnceLock` per CTB row). Motion compensation of later pictures blocks per
row on exactly the reference rows it reads. Samples are stored as `u16` internally for all bit
depths.
row on exactly the reference rows it reads. Samples are stored as `u8` for 8-bit pictures (half
the memory traffic in prediction reads, in-loop filters and output) and as `u16` above 8 bits: the
slice decoder and the filters are generic over the sample type (`picture::Sample`), monomorphised
once per type.
Draft mode (`Decoder::set_draft(true)`; FilmCraft enables it with Settings ▸ Playback ▸ Draft
decoding while the Program monitor plays at 1/2 or 1/4): sub-layer non-reference pictures
+64 -28
View File
@@ -8,10 +8,10 @@
use crate::dpb::{Dpb, Output, OutputMeta, PocState, RefPicSet, build_ref_lists};
use crate::error::{Error, Result, ensure, invalid};
use crate::params::{Layout, Pps, Sps, Vps};
use crate::picture::{Frame, FrameRef};
use crate::picture::{Frame, FrameRef, Sample, stores_u8};
use crate::slice::{NalHeader, SliceHeader, nal_type};
use crate::slicedec::{F_INTRA, F_SKIP, PicState, SliceDecoder, SliceJob};
use crate::{ColorInfo, Picture, Plane};
use crate::slicedec::{Blk, F_INTRA, F_SKIP, PicState, SliceDecoder, SliceJob};
use crate::{ColorInfo, Picture};
use filmcraft_bitstream::{annexb_nals, length_prefixed_nals, unescape_rbsp};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
@@ -507,7 +507,7 @@ impl Decoder {
}
}
/// Decode all slices of one picture and publish its rows.
/// Decode all slices of one picture and publish its rows, in the picture's sample type.
#[allow(clippy::too_many_arguments)]
fn run_job(
frame: FrameRef,
@@ -519,7 +519,25 @@ fn run_job(
draft: bool,
shared: &Shared,
) {
let mut pic = PicState::new(frame.clone(), sps, pps, layout, scaling);
if stores_u8(frame.bit_depth, frame.bit_depth_c) {
run_job_as::<u8>(frame, sps, pps, layout, scaling, slices, draft, shared)
} else {
run_job_as::<u16>(frame, sps, pps, layout, scaling, slices, draft, shared)
}
}
#[allow(clippy::too_many_arguments)]
fn run_job_as<S: Sample>(
frame: FrameRef,
sps: Arc<Sps>,
pps: Arc<Pps>,
layout: Arc<Layout>,
scaling: Option<Arc<Vec<Vec<u8>>>>,
slices: Vec<SliceJob>,
draft: bool,
shared: &Shared,
) {
let mut pic = PicState::<S>::new(frame.clone(), sps, pps, layout, scaling);
pic.draft = draft;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut first_err = None;
@@ -535,33 +553,34 @@ fn run_job(
Ok(e) => e,
Err(_) => {
// make sure readers never block on this frame
let gray = Frame::gray(0, frame.poc, &frame);
for r in 0..frame.num_rows() {
if !frame.is_published(r) {
let row = gray.row(r);
frame.publish(r, crate::picture::FrameRow { y: row.y.clone(), cb: row.cb.clone(), cr: row.cr.clone(), col: row.col.clone() });
}
}
frame.publish_gray();
Some(Error::Invalid("internal error while decoding a picture".into()))
}
};
if let Ok(mut s) = shared.stats.lock() {
s.pictures += 1;
for b in &pic.blk {
if b.flags & F_INTRA != 0 {
s.intra_4x4_blocks += 1;
} else if b.flags & F_SKIP != 0 {
s.skip_4x4_blocks += 1;
} else if b.flags != 0 {
s.inter_4x4_blocks += 1;
}
}
let [intra, skip, inter] = block_counts(&pic.blk);
s.intra_4x4_blocks += intra;
s.skip_4x4_blocks += skip;
s.inter_4x4_blocks += inter;
}
if let (Some(e), Ok(mut slot)) = (err, shared.error.lock()) {
slot.get_or_insert(e);
}
}
/// Intra, skipped and other inter 4x4 blocks of a picture, branch-free (one pass over every
/// block): intra blocks have F_INTRA, skipped ones F_SKIP without it, inter ones any other flags.
fn block_counts(blk: &[Blk]) -> [u64; 3] {
let (mut intra, mut skip, mut coded) = (0u64, 0u64, 0u64);
for b in blk {
intra += (b.flags & F_INTRA != 0) as u64;
skip += (b.flags & (F_INTRA | F_SKIP) == F_SKIP) as u64;
coded += (b.flags != 0) as u64;
}
[intra, skip, coded.saturating_sub(intra + skip)]
}
fn output_meta(sps: &Sps, pts: i64, key: bool, draft: bool) -> OutputMeta {
let vui = sps.vui.clone().unwrap_or_default();
OutputMeta {
@@ -583,13 +602,7 @@ fn make_picture(o: &Output<FrameRef>) -> Picture {
let (cx, cy, cw, ch) = (cx as usize, cy as usize, cw as usize, ch as usize);
let (ccw, cch) = (cw.div_ceil(2), ch.div_ceil(2));
let bd = o.meta.bit_depth;
let (y, u, v) = if bd <= 8 {
let (y, u, v) = o.frame.copy_cropped_u8((cx, cy, cw, ch));
(Plane::U8(y), Plane::U8(u), Plane::U8(v))
} else {
let (y, u, v) = o.frame.copy_cropped((cx, cy, cw, ch));
(Plane::U16(y), Plane::U16(u), Plane::U16(v))
};
let (y, u, v) = o.frame.cropped_planes((cx, cy, cw, ch), bd <= 8);
Picture {
width: cw as u32,
height: ch as u32,
@@ -614,3 +627,26 @@ fn make_picture(o: &Output<FrameRef>) -> Picture {
draft: o.meta.draft,
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The branch-free block counts classify every flag combination as the if / else-if chain did.
#[test]
fn block_counts_match_the_classification_chain() {
let blk: Vec<Blk> = (0..=255u8).flat_map(|f| [Blk { flags: f, ..Blk::default() }; 3]).collect();
let mut want = [0u64; 3];
for b in &blk {
if b.flags & F_INTRA != 0 {
want[0] += 1;
} else if b.flags & F_SKIP != 0 {
want[1] += 1;
} else if b.flags != 0 {
want[2] += 1;
}
}
assert_eq!(block_counts(&blk), want);
assert_eq!(block_counts(&[]), [0; 3]);
}
}
+45 -45
View File
@@ -4,13 +4,13 @@
//! Row r is deblocked once rows 0..=r+1 are decoded (row r+1 still needs the unfiltered bottom samples
//! of row r for intra prediction); SAO of row r runs once row r+1 is deblocked; then row r is final.
use crate::picture::{ColMv, FrameRow};
use crate::picture::{ColMv, FrameRow, Sample};
use crate::slicedec::{E_PU_H, E_PU_V, E_TU_H, E_TU_V, F_CBF, F_CODED, F_INTRA, F_NOFILTER, MvField, PicState};
use crate::spec_tables::{BETA_TABLE, TC_TABLE};
use crate::tables::qpc_420;
/// Called after each decoded CTB of CTB row `ry`.
pub fn ctb_decoded(pic: &mut PicState, ry: usize) {
pub fn ctb_decoded<S: Sample>(pic: &mut PicState<S>, ry: usize) {
pic.row_done[ry] += 1;
while pic.rows_complete < pic.hctb && pic.row_done[pic.rows_complete] as usize == pic.wctb {
pic.rows_complete += 1;
@@ -19,7 +19,7 @@ pub fn ctb_decoded(pic: &mut PicState, ry: usize) {
}
/// Run the filters / publication as far as the decoded rows allow (`all`: end of picture).
pub fn advance(pic: &mut PicState, all: bool) {
pub fn advance<S: Sample>(pic: &mut PicState<S>, all: bool) {
let h = pic.hctb;
loop {
let r = pic.rows_deblocked;
@@ -39,7 +39,7 @@ pub fn advance(pic: &mut PicState, all: bool) {
}
/// End of picture: filter and publish everything.
pub fn finish(pic: &mut PicState) {
pub fn finish<S: Sample>(pic: &mut PicState<S>) {
pic.rows_complete = pic.hctb;
advance(pic, true);
}
@@ -53,7 +53,7 @@ fn tc_beta(q: i32, bs: i32, tc_off: i32, beta_off: i32, bd: u32) -> (i32, i32) {
/// Boundary strength (8.7.2.4) between 4x4 blocks p and q, in slices `ps` and `qs` (indices into
/// `pic.slices`, from [`edge_params`]).
fn boundary_strength(pic: &PicState, pi: usize, qi: usize, tu_edge: bool, ps: u32, qs: u32) -> i32 {
fn boundary_strength<S: Sample>(pic: &PicState<S>, pi: usize, qi: usize, tu_edge: bool, ps: u32, qs: u32) -> i32 {
let (p, q) = (&pic.blk[pi], &pic.blk[qi]);
if (p.flags | q.flags) & F_INTRA != 0 {
return 2;
@@ -70,14 +70,14 @@ fn boundary_strength(pic: &PicState, pi: usize, qi: usize, tu_edge: bool, ps: u3
}
/// The picture referenced by list `l` of motion `f` in slice `s`.
fn ref_id(pic: &PicState, s: u32, f: &MvField, l: usize) -> u32 {
fn ref_id<S: Sample>(pic: &PicState<S>, s: u32, f: &MvField, l: usize) -> u32 {
if s == u32::MAX {
return u32::MAX;
}
pic.slices.get(s as usize).and_then(|sl| sl.ref_ids[l].get(f.ref_idx[l] as usize)).copied().unwrap_or(u32::MAX - 1)
}
fn motion_bs(pic: &PicState, ps: u32, p: &MvField, qs: u32, q: &MvField) -> i32 {
fn motion_bs<S: Sample>(pic: &PicState<S>, ps: u32, p: &MvField, qs: u32, q: &MvField) -> i32 {
let np = p.pred(0) as u32 + p.pred(1) as u32;
let nq = q.pred(0) as u32 + q.pred(1) as u32;
if np != nq {
@@ -106,7 +106,7 @@ fn motion_bs(pic: &PicState, ps: u32, p: &MvField, qs: u32, q: &MvField) -> i32
/// Whether the edge between blocks p and q (q = current block, owning the edge) is filtered, and the
/// slice-level parameters (tc offset, beta offset) of q's slice, then the slices of p and q.
fn edge_params(pic: &PicState, px: usize, py: usize, qx: usize, qy: usize) -> Option<(i32, i32, u32, u32)> {
fn edge_params<S: Sample>(pic: &PicState<S>, px: usize, py: usize, qx: usize, qy: usize) -> Option<(i32, i32, u32, u32)> {
let s = pic.log2_ctb;
let pc = (py >> s) * pic.wctb + (px >> s);
let qc = (qy >> s) * pic.wctb + (qx >> s);
@@ -131,7 +131,7 @@ fn edge_params(pic: &PicState, px: usize, py: usize, qx: usize, qy: usize) -> Op
}
/// Deblock CTB row `r`: vertical edges of the row, then horizontal edges whose q side is in the row.
fn deblock_row(pic: &mut PicState, r: usize) {
fn deblock_row<S: Sample>(pic: &mut PicState<S>, r: usize) {
if !pic.deblocking_enabled_anywhere || pic.draft {
return;
}
@@ -236,7 +236,7 @@ fn deblock_row(pic: &mut PicState, r: usize) {
/// the 4 lines at a time for horizontal ones (`along` = 1): no per-sample index arithmetic or
/// bounds checks in the filter itself.
#[allow(clippy::too_many_arguments)]
fn filter_luma(s: &mut [u16], o: usize, step: usize, along: usize, tc: i32, beta: i32, nfp: bool, nfq: bool, bd: u32) {
fn filter_luma<S: Sample>(s: &mut [S], o: usize, step: usize, along: usize, tc: i32, beta: i32, nfp: bool, nfq: bool, bd: u32) {
// l[k][i]: line k, i = 0..4 -> p3, p2, p1, p0 and i = 4..8 -> q0, q1, q2, q3
let mut l = [[0i32; 8]; 4];
let Some(start) = o.checked_sub(4 * step) else { return };
@@ -244,14 +244,14 @@ fn filter_luma(s: &mut [u16], o: usize, step: usize, along: usize, tc: i32, beta
for (k, line) in l.iter_mut().enumerate() {
let Some(src) = s.get(start + k * along..).and_then(|v| v.first_chunk::<8>()) else { return };
for (d, &v) in line.iter_mut().zip(src) {
*d = v as i32;
*d = v.to_i32();
}
}
} else {
for i in 0..8 {
let Some(src) = s.get(start + i * step..).and_then(|v| v.first_chunk::<4>()) else { return };
for (k, &v) in src.iter().enumerate() {
l[k][i] = v as i32;
l[k][i] = v.to_i32();
}
}
}
@@ -262,14 +262,14 @@ fn filter_luma(s: &mut [u16], o: usize, step: usize, along: usize, tc: i32, beta
for (k, line) in l.iter().enumerate() {
let Some(dst) = s.get_mut(start + k * along..).and_then(|v| v.first_chunk_mut::<8>()) else { return };
for (d, &v) in dst.iter_mut().zip(line) {
*d = v as u16;
*d = S::from_i32(v);
}
}
} else {
for i in 1..7 {
let Some(dst) = s.get_mut(start + i * step..).and_then(|v| v.first_chunk_mut::<4>()) else { return };
for (k, d) in dst.iter_mut().enumerate() {
*d = l[k][i] as u16;
*d = S::from_i32(l[k][i]);
}
}
}
@@ -340,26 +340,26 @@ fn filter_luma_lines(l: &mut [[i32; 8]; 4], tc: i32, beta: i32, nfp: bool, nfq:
/// Filter one 4-sample chroma edge segment.
#[allow(clippy::too_many_arguments)]
fn filter_chroma(s: &mut [u16], o: usize, step: usize, along: usize, tc: i32, nfp: bool, nfq: bool, bd: u32) {
fn filter_chroma<S: Sample>(s: &mut [S], o: usize, step: usize, along: usize, tc: i32, nfp: bool, nfq: bool, bd: u32) {
let max = (1i32 << bd) - 1;
for k in 0..4 {
let base = o + k * along;
let (p0, p1) = (s[base - step] as i32, s[base - 2 * step] as i32);
let (q0, q1) = (s[base] as i32, s[base + step] as i32);
let (p0, p1) = (s[base - step].to_i32(), s[base - 2 * step].to_i32());
let (q0, q1) = (s[base].to_i32(), s[base + step].to_i32());
let delta = ((((q0 - p0) << 2) + p1 - q1 + 4) >> 3).clamp(-tc, tc);
if !nfp {
s[base - step] = (p0 + delta).clamp(0, max) as u16;
s[base - step] = S::from_i32((p0 + delta).clamp(0, max));
}
if !nfq {
s[base] = (q0 - delta).clamp(0, max) as u16;
s[base] = S::from_i32((q0 - delta).clamp(0, max));
}
}
}
/// SAO of CTB row `r` from the deblocked planes into new row buffers (luma, Cb, Cr).
fn sao_row(pic: &PicState, r: usize) -> [Vec<u16>; 3] {
fn sao_row<S: Sample>(pic: &PicState<S>, r: usize) -> [Vec<S>; 3] {
let ctb = 1usize << pic.log2_ctb;
let mut out: [Vec<u16>; 3] = [Vec::new(), Vec::new(), Vec::new()];
let mut out: [Vec<S>; 3] = [Vec::new(), Vec::new(), Vec::new()];
for c in 0..3 {
let (w, h, cs) = if c == 0 { (pic.width, pic.height, ctb) } else { (pic.cwidth, pic.cheight, ctb / 2) };
let y0 = r * cs;
@@ -388,8 +388,8 @@ fn sao_row(pic: &PicState, r: usize) -> [Vec<u16>; 3] {
}
#[allow(clippy::too_many_arguments)]
fn sao_ctb(
pic: &PicState,
fn sao_ctb<S: Sample>(
pic: &PicState<S>,
c: usize,
rs: usize,
x0: usize,
@@ -399,14 +399,14 @@ fn sao_ctb(
w: usize,
h: usize,
p: &crate::slicedec::SaoParams,
dst: &mut [u16],
dst: &mut [S],
) {
let bd = if c == 0 { pic.bd_y } else { pic.bd_c };
let max = (1i32 << bd) - 1;
let sc = if c == 0 { 0 } else { 1 };
let offs = p.offsets[c].map(|v| v as i32);
// samples of CUs that must not be modified
let nofilter = |pic: &PicState, x: usize, y: usize| pic.blk[((y << sc) >> 2) * pic.w4 + ((x << sc) >> 2)].flags & F_NOFILTER != 0;
let nofilter = |pic: &PicState<S>, x: usize, y: usize| pic.blk[((y << sc) >> 2) * pic.w4 + ((x << sc) >> 2)].flags & F_NOFILTER != 0;
let any_nofilter = {
let mut a = false;
for by in ((y0 << sc) >> 2)..(((y1 << sc) + 3) >> 2).min(pic.h4) {
@@ -432,8 +432,8 @@ fn sao_ctb(
if nofilter(pic, x, y) {
continue;
}
let v = pic.planes[c][y * w + x] as i32;
dst[(y - y0) * w + x] = (v + table[(v >> shift) as usize]).clamp(0, max) as u16;
let v = pic.planes[c][y * w + x].to_i32();
dst[(y - y0) * w + x] = S::from_i32((v + table[(v >> shift) as usize]).clamp(0, max));
}
}
return;
@@ -450,7 +450,7 @@ fn sao_ctb(
let cur_slice = pic.ctb_slice[rs];
let lf_cur = pic.slices[cur_slice as usize].lf_across;
let cur_ts = pic.layout.rs_to_ts[rs];
let cross_ok = |pic: &PicState, xn: usize, yn: usize| -> bool {
let cross_ok = |pic: &PicState<S>, xn: usize, yn: usize| -> bool {
let nrs = (yn >> s) * pic.wctb + (xn >> s);
if nrs == rs {
return true;
@@ -478,12 +478,12 @@ fn sao_ctb(
// Offset per (2 + sum of neighbour signs); edgeIdx 0 adds nothing.
let by_sum = edge_map.map(|e| if e == 0 { 0 } else { offs[e - 1] });
// Any sample: neighbours outside the picture or in a CTB that may not be used leave it as is.
let general = |x: usize, y: usize| -> Option<u16> {
let general = |x: usize, y: usize| -> Option<S> {
if any_nofilter && nofilter(pic, x, y) {
return None;
}
let mut sum = 0i32;
let v = pic.planes[c][y * w + x] as i32;
let v = pic.planes[c][y * w + x].to_i32();
for k in 0..2 {
let xn = x as i32 + hp[k];
let yn = y as i32 + vp[k];
@@ -494,9 +494,9 @@ fn sao_ctb(
if (xn < x0 || xn >= x1 || yn < y0 || yn >= y1) && !cross_ok(pic, xn, yn) {
return None;
}
sum += (v - pic.planes[c][yn * w + xn] as i32).signum();
sum += (v - pic.planes[c][yn * w + xn].to_i32()).signum();
}
Some((v + by_sum[(2 + sum) as usize]).clamp(0, max) as u16)
Some(S::from_i32((v + by_sum[(2 + sum) as usize]).clamp(0, max)))
};
// Samples whose two neighbours lie inside this CTB need none of those checks: a tight loop
// (same arithmetic) for them, the general path for the CTB border.
@@ -535,14 +535,14 @@ const SAO_LANES: usize = 16;
/// the sum of the two neighbour signs selects the offset (no table lookup), so the loop
/// vectorises; bit-exact with the per-sample form.
#[inline(always)]
fn sao_edge_run(cur: &[u16], na: &[u16], nb: &[u16], out: &mut [u16], by_sum: &[i32; 5], max: i32) {
fn sao_edge_run<S: Sample>(cur: &[S], na: &[S], nb: &[S], out: &mut [S], by_sum: &[i32; 5], max: i32) {
let o = by_sum.map(|v| v as i16);
let max = max as i16;
let lane = |v: i16, a: i16, b: i16| -> u16 {
let lane = |v: i16, a: i16, b: i16| -> S {
let s = ((v > a) as i16 - (v < a) as i16) + ((v > b) as i16 - (v < b) as i16);
let m = |k: i16| -((s == k) as i16);
let off = (o[0] & m(-2)) | (o[1] & m(-1)) | (o[3] & m(1)) | (o[4] & m(2));
(v + off).max(0).min(max) as u16
S::from_i32((v + off).max(0).min(max) as i32)
};
let n = cur.len();
let full = n - n % SAO_LANES;
@@ -551,36 +551,36 @@ fn sao_edge_run(cur: &[u16], na: &[u16], nb: &[u16], out: &mut [u16], by_sum: &[
return;
};
for l in 0..SAO_LANES {
d[l] = lane(c[l] as i16, a[l] as i16, b[l] as i16);
d[l] = lane(c[l].to_i16(), a[l].to_i16(), b[l].to_i16());
}
}
for i in full..n {
out[i] = lane(cur[i] as i16, na[i] as i16, nb[i] as i16);
out[i] = lane(cur[i].to_i16(), na[i].to_i16(), nb[i].to_i16());
}
}
/// `SAO_LANES` samples from `i`, if all present.
#[inline(always)]
fn lanes(s: &[u16], i: usize) -> Option<&[u16; SAO_LANES]> {
fn lanes<S>(s: &[S], i: usize) -> Option<&[S; SAO_LANES]> {
s.get(i..)?.first_chunk()
}
#[inline(always)]
fn lanes_mut(s: &mut [u16], i: usize) -> Option<&mut [u16; SAO_LANES]> {
fn lanes_mut<S>(s: &mut [S], i: usize) -> Option<&mut [S; SAO_LANES]> {
s.get_mut(i..)?.first_chunk_mut()
}
/// Band offset of a run of samples: the band index relative to `band_pos` selects one of the
/// four offsets per lane.
#[inline(always)]
fn sao_band_run(src: &[u16], out: &mut [u16], shift: u32, (band_pos, offs): (i32, [i32; 4]), max: i32) {
fn sao_band_run<S: Sample>(src: &[S], out: &mut [S], shift: u32, (band_pos, offs): (i32, [i32; 4]), max: i32) {
let o = offs.map(|v| v as i16);
let (bp, max) = (band_pos as i16, max as i16);
let lane = |v: i16| -> u16 {
let lane = |v: i16| -> S {
let k = ((v >> shift) - bp) & 31;
let m = |j: i16| -((k == j) as i16);
let off = (o[0] & m(0)) | (o[1] & m(1)) | (o[2] & m(2)) | (o[3] & m(3));
(v + off).max(0).min(max) as u16
S::from_i32((v + off).max(0).min(max) as i32)
};
let n = src.len();
let full = n - n % SAO_LANES;
@@ -589,16 +589,16 @@ fn sao_band_run(src: &[u16], out: &mut [u16], shift: u32, (band_pos, offs): (i32
return;
};
for l in 0..SAO_LANES {
d[l] = lane(c[l] as i16);
d[l] = lane(c[l].to_i16());
}
}
for i in full..n {
out[i] = lane(src[i] as i16);
out[i] = lane(src[i].to_i16());
}
}
/// Publish CTB row `r` (final samples + compressed motion) into the frame.
fn publish_row(pic: &mut PicState, r: usize) {
fn publish_row<S: Sample>(pic: &mut PicState<S>, r: usize) {
let ctb = 1usize << pic.log2_ctb;
let (y0, y1) = (r * ctb, ((r + 1) * ctb).min(pic.height));
let (c0, c1) = (y0 / 2, y1.div_ceil(2));
+286 -88
View File
@@ -1,6 +1,6 @@
//! Fractional sample interpolation (8.5.3.3.3) and weighted sample prediction (8.5.3.3.4).
use crate::picture::Frame;
use crate::picture::{Frame, Sample};
use crate::spec_tables::{CHROMA_FILTER, LUMA_FILTER};
/// Max prediction block size + filter margin.
@@ -18,18 +18,49 @@ impl Default for McScratch {
}
}
/// `out[j] = (sum_k f[k] * src[j + k * step]) >> shift` for `W` outputs: taps outer, outputs
/// inner, so each tap is one vector multiply-add across the row (exact integer sums).
/// A sample the interpolation filters read: reference samples (`u8` / `u16`) or 16-bit window
/// and first-pass values.
trait Px: Copy {
fn w32(self) -> i32;
fn w16(self) -> i16;
}
impl Px for i16 {
#[inline(always)]
fn w32(self) -> i32 {
self as i32
}
#[inline(always)]
fn w16(self) -> i16 {
self
}
}
impl<S: Sample> Px for S {
#[inline(always)]
fn w32(self) -> i32 {
self.to_i32()
}
#[inline(always)]
fn w16(self) -> i16 {
self.to_i16()
}
}
/// `out[j] = (sum_k f[k] * tap(k)[j]) >> shift` for `W` outputs: taps outer, outputs inner, so
/// each tap is one vector multiply-add across the row (exact integer sums). `tap(k)` starts at
/// the samples under filter tap k: the row shifted by k (horizontal) or the k-th row (vertical);
/// a function rather than an array of slices so nothing goes through the stack.
#[inline(always)]
fn filt_w<const T: usize, const W: usize>(src: &[i16], step: usize, f: &[i8; T], shift: u32, out: &mut [i16]) {
fn filt_w<'a, P: Px + 'a, const T: usize, const W: usize>(tap: impl Fn(usize) -> Option<&'a [P]>, f: &[i8; T], shift: u32, out: &mut [i16]) {
let mut acc = [0i32; W];
for k in 0..T {
let c = f[k] as i32;
let Some(p) = src.get(k * step..).and_then(|s| s.first_chunk::<W>()) else {
let Some(p) = tap(k).and_then(|s| s.first_chunk::<W>()) else {
return;
};
for j in 0..W {
acc[j] += c * p[j] as i32;
acc[j] += c * p[j].w32();
}
}
let Some(o) = out.first_chunk_mut::<W>() else {
@@ -45,15 +76,15 @@ fn filt_w<const T: usize, const W: usize>(src: &[i16], step: usize, f: &[i8; T],
/// luma or chroma filter, 8.5.3.3.3.1 / 8.5.3.3.3.2), so it fits in an `i16`, and wrapping sums
/// are exact whenever the final value fits. Twice the lanes of [`filt_w`] per vector.
#[inline(always)]
fn filt16_w<const T: usize, const W: usize>(src: &[i16], step: usize, f: &[i8; T], out: &mut [i16]) {
fn filt16_w<'a, P: Px + 'a, const T: usize, const W: usize>(tap: impl Fn(usize) -> Option<&'a [P]>, f: &[i8; T], out: &mut [i16]) {
let mut acc = [0i16; W];
for k in 0..T {
let c = f[k] as i16;
let Some(p) = src.get(k * step..).and_then(|s| s.first_chunk::<W>()) else {
let Some(p) = tap(k).and_then(|s| s.first_chunk::<W>()) else {
return;
};
for j in 0..W {
acc[j] = acc[j].wrapping_add(c.wrapping_mul(p[j]));
acc[j] = acc[j].wrapping_add(c.wrapping_mul(p[j].w16()));
}
}
if let Some(o) = out.first_chunk_mut::<W>() {
@@ -64,45 +95,59 @@ fn filt16_w<const T: usize, const W: usize>(src: &[i16], step: usize, f: &[i8; T
/// [`filt_w`] for a run of `w` outputs (prediction block widths are fixed sizes); in 16-bit lanes
/// ([`filt16_w`]) when `narrow` (a first pass over 8-bit samples).
#[inline(always)]
fn filt<const T: usize>(src: &[i16], step: usize, f: &[i8; T], shift: u32, out: &mut [i16], w: usize, narrow: bool) {
fn filt<'a, P: Px + 'a, const T: usize>(tap: impl Fn(usize) -> Option<&'a [P]> + Copy, f: &[i8; T], shift: u32, out: &mut [i16], w: usize, narrow: bool) {
if narrow {
match w {
4 => return filt16_w::<T, 4>(src, step, f, out),
8 => return filt16_w::<T, 8>(src, step, f, out),
12 => return filt16_w::<T, 12>(src, step, f, out),
16 => return filt16_w::<T, 16>(src, step, f, out),
24 => return filt16_w::<T, 24>(src, step, f, out),
32 => return filt16_w::<T, 32>(src, step, f, out),
48 => return filt16_w::<T, 48>(src, step, f, out),
64 => return filt16_w::<T, 64>(src, step, f, out),
4 => return filt16_w::<P, T, 4>(tap, f, out),
8 => return filt16_w::<P, T, 8>(tap, f, out),
12 => return filt16_w::<P, T, 12>(tap, f, out),
16 => return filt16_w::<P, T, 16>(tap, f, out),
24 => return filt16_w::<P, T, 24>(tap, f, out),
32 => return filt16_w::<P, T, 32>(tap, f, out),
48 => return filt16_w::<P, T, 48>(tap, f, out),
64 => return filt16_w::<P, T, 64>(tap, f, out),
_ => {}
}
}
match w {
2 => filt_w::<T, 2>(src, step, f, shift, out),
4 => filt_w::<T, 4>(src, step, f, shift, out),
6 => filt_w::<T, 6>(src, step, f, shift, out),
8 => filt_w::<T, 8>(src, step, f, shift, out),
12 => filt_w::<T, 12>(src, step, f, shift, out),
16 => filt_w::<T, 16>(src, step, f, shift, out),
24 => filt_w::<T, 24>(src, step, f, shift, out),
32 => filt_w::<T, 32>(src, step, f, shift, out),
48 => filt_w::<T, 48>(src, step, f, shift, out),
64 => filt_w::<T, 64>(src, step, f, shift, out),
2 => filt_w::<P, T, 2>(tap, f, shift, out),
4 => filt_w::<P, T, 4>(tap, f, shift, out),
6 => filt_w::<P, T, 6>(tap, f, shift, out),
8 => filt_w::<P, T, 8>(tap, f, shift, out),
12 => filt_w::<P, T, 12>(tap, f, shift, out),
16 => filt_w::<P, T, 16>(tap, f, shift, out),
24 => filt_w::<P, T, 24>(tap, f, shift, out),
32 => filt_w::<P, T, 32>(tap, f, shift, out),
48 => filt_w::<P, T, 48>(tap, f, shift, out),
64 => filt_w::<P, T, 64>(tap, f, shift, out),
_ => {
for (j, o) in out[..w].iter_mut().enumerate() {
let s: i32 = (0..T).map(|k| f[k] as i32 * src[j + k * step] as i32).sum();
for (j, o) in out.iter_mut().take(w).enumerate() {
let s: i32 = (0..T).map(|k| f[k] as i32 * tap(k).and_then(|s| s.get(j)).map_or(0, |p| p.w32())).sum();
*o = (s >> shift) as i16;
}
}
}
}
/// Separable sub-sample interpolation of a w x h block from `win` (stride WIN, the block's
/// top-left sample at (m, m) for a T-tap filter with m = T / 2 - 1).
/// The taps of a horizontal filter along `row`: the row shifted by k samples.
#[inline(always)]
fn h_taps<'a, P>(row: &'a [P]) -> impl Fn(usize) -> Option<&'a [P]> + Copy {
move |k| row.get(k..)
}
/// The taps of a vertical filter: row `r + k` from sample `x` on.
#[inline(always)]
fn v_taps<'a, P>(rows: &'a [&'a [P]], r: usize, x: usize) -> impl Fn(usize) -> Option<&'a [P]> + Copy {
move |k| rows.get(r + k).and_then(|row| row.get(x..))
}
/// Separable sub-sample interpolation of a w x h block from the window `rows` (h + T - 1 rows of
/// at least w + T - 1 samples; the block's top-left sample at (m, m) for a T-tap filter with
/// m = T / 2 - 1). The window rows are the reference picture's own lines when the window lies
/// inside it horizontally, or lines of an edge-replicated copy.
#[allow(clippy::too_many_arguments)]
fn interpolate<const T: usize>(
win: &[i16],
fn interpolate<P: Px, const T: usize>(
rows: &[&[P]],
tmp: &mut [i16],
fx: usize,
fy: usize,
@@ -117,81 +162,122 @@ fn interpolate<const T: usize>(
let m = T / 2 - 1;
// 8-bit samples: first passes in 16-bit lanes (see filt16_w)
let narrow = shift1 == 0 && shift3 == 6;
let out_rows = out.chunks_mut(w).take(h);
if fx == 0 && fy == 0 {
for r in 0..h {
for (o, &s) in out[r * w..r * w + w].iter_mut().zip(&win[(r + m) * WIN + m..]) {
*o = s << shift3;
for (r, o) in out_rows.enumerate() {
let Some(src) = rows.get(r + m).and_then(|row| row.get(m..)) else { return };
for (o, &s) in o.iter_mut().zip(src) {
*o = s.w16() << shift3;
}
}
} else if fy == 0 {
for r in 0..h {
filt(&win[(r + m) * WIN..], 1, hf, shift1, &mut out[r * w..], w, narrow);
for (r, o) in out_rows.enumerate() {
let Some(row) = rows.get(r + m) else { return };
filt(h_taps(row), hf, shift1, o, w, narrow);
}
} else if fx == 0 {
for r in 0..h {
filt(&win[r * WIN + m..], WIN, vf, shift1, &mut out[r * w..], w, narrow);
for (r, o) in out_rows.enumerate() {
filt(v_taps(rows, r, m), vf, shift1, o, w, narrow);
}
} else {
for r in 0..h + T - 1 {
filt(&win[r * WIN..], 1, hf, shift1, &mut tmp[r * WIN..], w, narrow);
for (row, t) in rows.iter().take(h + T - 1).zip(tmp.chunks_mut(WIN)) {
filt(h_taps(row), hf, shift1, t, w, narrow);
}
for r in 0..h {
filt(&tmp[r * WIN..], WIN, vf, 6, &mut out[r * w..], w, false);
let mut trows: [&[i16]; WIN] = [&[]; WIN];
for (t, row) in trows.iter_mut().zip(tmp.chunks(WIN)).take(h + T - 1) {
*t = row;
}
for (r, o) in out_rows.enumerate() {
filt(v_taps(&trows, r, 0), vf, 6, o, w, false);
}
}
}
/// Prediction samples of a w x h block of plane `c` whose filter window (`T`-tap filters) starts
/// at (x0, y0): read straight from the reference lines when the window is inside the picture
/// horizontally, else from an edge-replicated copy.
#[allow(clippy::too_many_arguments)]
fn predict_block<S: Sample, const T: usize>(
f: &Frame,
c: usize,
(x0, y0): (i32, i32),
(fx, fy): (usize, usize),
filters: (&[i8; T], &[i8; T]),
(w, h): (usize, usize),
(shift1, shift3): (u32, u32),
out: &mut [i16],
sc: &mut McScratch,
) {
let (ww, wh) = (w + T - 1, h + T - 1);
let McScratch { win, tmp } = sc;
let mut rows: [&[S]; WIN] = [&[]; WIN];
if let Some(rows) = rows.get_mut(..wh)
&& f.window_rows(c, x0, y0, ww, rows)
{
interpolate::<S, T>(rows, tmp, fx, fy, filters.0, filters.1, w, h, shift1, shift3, out);
return;
}
if c == 0 {
f.luma_window::<S>(x0, y0, ww, wh, win, WIN);
} else {
f.chroma_window::<S>(c - 1, x0, y0, ww, wh, win, WIN);
}
let mut wrows: [&[i16]; WIN] = [&[]; WIN];
for (r, row) in wrows.iter_mut().zip(win.chunks(WIN)).take(wh) {
*r = row;
}
interpolate::<i16, T>(&wrows[..wh.min(WIN)], tmp, fx, fy, filters.0, filters.1, w, h, shift1, shift3, out);
}
/// Luma prediction samples (14-bit intermediate precision) of a w x h block at (x, y) displaced by the
/// quarter-sample motion vector `mv`, into `out` (stride w).
#[allow(clippy::too_many_arguments)]
pub fn mc_luma(f: &Frame, x: i32, y: i32, mv: [i16; 2], w: usize, h: usize, out: &mut [i16], sc: &mut McScratch) {
pub fn mc_luma<S: Sample>(f: &Frame, x: i32, y: i32, mv: [i16; 2], w: usize, h: usize, out: &mut [i16], sc: &mut McScratch) {
let bd = f.bit_depth;
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;
f.luma_window(xi - 3, yi - 3, w + 7, h + 7, &mut sc.win, WIN);
interpolate::<8>(&sc.win, &mut sc.tmp, fx, fy, &LUMA_FILTER[fx], &LUMA_FILTER[fy], w, h, shift1, shift3, out);
predict_block::<S, 8>(f, 0, (xi - 3, yi - 3), (fx, fy), (&LUMA_FILTER[fx], &LUMA_FILTER[fy]), (w, h), (shift1, shift3), out, sc);
}
/// Chroma prediction samples of a w x h chroma block at chroma position (x, y) with a 1/8-sample
/// vector (4:2:0).
#[allow(clippy::too_many_arguments)]
pub fn mc_chroma(f: &Frame, c: usize, x: i32, y: i32, mv: [i16; 2], w: usize, h: usize, out: &mut [i16], sc: &mut McScratch) {
pub fn mc_chroma<S: Sample>(f: &Frame, c: usize, x: i32, y: i32, mv: [i16; 2], w: usize, h: usize, out: &mut [i16], sc: &mut McScratch) {
let bd = f.bit_depth_c;
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;
f.chroma_window(c, xi - 1, yi - 1, w + 3, h + 3, &mut sc.win, WIN);
interpolate::<4>(&sc.win, &mut sc.tmp, fx, fy, &CHROMA_FILTER[fx], &CHROMA_FILTER[fy], w, h, shift1, shift3, out);
predict_block::<S, 4>(f, c + 1, (xi - 1, yi - 1), (fx, fy), (&CHROMA_FILTER[fx], &CHROMA_FILTER[fy]), (w, h), (shift1, shift3), out, sc);
}
/// Calls `$f::<W>(args)` with `W` the prediction block width `$w` as a constant (block widths are
/// Calls `$f::<_, W>(args)` with `W` the prediction block width `$w` as a constant (block widths are
/// fixed sizes; a constant width lets the row loops vectorise even for 4- and 8-sample rows), or
/// `$f::<0>` for any other width.
macro_rules! by_width {
($w:expr, $f:ident($($a:expr),*)) => {
match $w {
2 => $f::<2>($($a),*),
4 => $f::<4>($($a),*),
6 => $f::<6>($($a),*),
8 => $f::<8>($($a),*),
12 => $f::<12>($($a),*),
16 => $f::<16>($($a),*),
24 => $f::<24>($($a),*),
32 => $f::<32>($($a),*),
48 => $f::<48>($($a),*),
64 => $f::<64>($($a),*),
_ => $f::<0>($($a),*),
2 => $f::<_, 2>($($a),*),
4 => $f::<_, 4>($($a),*),
6 => $f::<_, 6>($($a),*),
8 => $f::<_, 8>($($a),*),
12 => $f::<_, 12>($($a),*),
16 => $f::<_, 16>($($a),*),
24 => $f::<_, 24>($($a),*),
32 => $f::<_, 32>($($a),*),
48 => $f::<_, 48>($($a),*),
64 => $f::<_, 64>($($a),*),
_ => $f::<_, 0>($($a),*),
}
};
}
/// Default weighted prediction, one list (8-262).
pub fn put_uni(p: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u16], ds: usize) {
pub fn put_uni<S: Sample>(p: &[i16], w: usize, h: usize, bd: u32, dst: &mut [S], ds: usize) {
by_width!(w, put_uni_w(p, w, h, bd, dst, ds))
}
@@ -202,7 +288,7 @@ pub fn put_uni(p: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u16], ds: usiz
/// in an `i16` and the sums are exact. SSE2 clips 16-bit lanes in one instruction each, 32-bit lanes
/// in four.
#[inline(always)]
fn put_uni_w<const W: usize>(p: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u16], ds: usize) {
fn put_uni_w<S: Sample, const W: usize>(p: &[i16], w: usize, h: usize, bd: u32, dst: &mut [S], ds: usize) {
let w = if W > 0 { W } else { w };
let shift = 14 - bd;
let off: i16 = if shift > 0 { 1 << (shift - 1) } else { 0 };
@@ -210,13 +296,13 @@ fn put_uni_w<const W: usize>(p: &[i16], w: usize, h: usize, bd: u32, dst: &mut [
// Rows as slices and min / max clipping (`clamp` asserts its bounds) so the loop vectorises.
for (d, s) in dst.chunks_mut(ds).zip(p[..w * h].chunks(w)) {
for (d, &s) in d[..w].iter_mut().zip(s) {
*d = (s.wrapping_add(off) >> shift).max(0).min(max) as u16;
*d = S::from_i32((s.wrapping_add(off) >> shift).max(0).min(max) as i32);
}
}
}
/// Default weighted prediction, bi-prediction (8-263).
pub fn put_bi(p0: &[i16], p1: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u16], ds: usize) {
pub fn put_bi<S: Sample>(p0: &[i16], p1: &[i16], w: usize, h: usize, bd: u32, dst: &mut [S], ds: usize) {
by_width!(w, put_bi_w(p0, p1, w, h, bd, dst, ds))
}
@@ -226,7 +312,7 @@ pub fn put_bi(p0: &[i16], p1: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u1
/// `((a >> 1) + (b >> 1) + (a & b & 1) + off / 2) >> (shift - 1)` (the two dropped low bits only
/// carry when both are set), and `(a >> 1) + (b >> 1)` fits in an `i16` (see [`put_uni_w`]).
#[inline(always)]
fn put_bi_w<const W: usize>(p0: &[i16], p1: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u16], ds: usize) {
fn put_bi_w<S: Sample, const W: usize>(p0: &[i16], p1: &[i16], w: usize, h: usize, bd: u32, dst: &mut [S], ds: usize) {
let w = if W > 0 { W } else { w };
let shift = 15 - bd; // >= 3 (bd <= 12)
let half_off: i16 = 1 << (shift - 2);
@@ -234,31 +320,44 @@ fn put_bi_w<const W: usize>(p0: &[i16], p1: &[i16], w: usize, h: usize, bd: u32,
for ((d, a), b) in dst.chunks_mut(ds).zip(p0[..w * h].chunks(w)).zip(p1[..w * h].chunks(w)) {
for ((d, &a), &b) in d[..w].iter_mut().zip(a).zip(b) {
let v = (a >> 1).wrapping_add(b >> 1).wrapping_add(a & b & 1).wrapping_add(half_off) >> (shift - 1);
*d = v.max(0).min(max) as u16;
*d = S::from_i32(v.max(0).min(max) as i32);
}
}
}
/// Explicit weighted prediction, one list (8-265). `o` is the offset already scaled to the bit depth.
pub fn put_weighted_uni(p: &[i16], w: usize, h: usize, bd: u32, log2wd: u32, wt: i32, o: i32, dst: &mut [u16], ds: usize) {
pub fn put_weighted_uni<S: Sample>(p: &[i16], w: usize, h: usize, bd: u32, log2wd: u32, wt: i32, o: i32, dst: &mut [S], ds: usize) {
let max = (1i32 << bd) - 1;
for r in 0..h {
for c in 0..w {
let v = p[r * w + c] as i32 * wt;
let v = if log2wd >= 1 { ((v + (1 << (log2wd - 1))) >> log2wd) + o } else { v + o };
dst[r * ds + c] = v.max(0).min(max) as u16;
dst[r * ds + c] = S::from_i32(v.max(0).min(max));
}
}
}
/// Explicit weighted bi-prediction (8-266).
pub fn put_weighted_bi(p0: &[i16], p1: &[i16], w: usize, h: usize, bd: u32, log2wd: u32, w0: i32, w1: i32, o0: i32, o1: i32, dst: &mut [u16], ds: usize) {
pub fn put_weighted_bi<S: Sample>(
p0: &[i16],
p1: &[i16],
w: usize,
h: usize,
bd: u32,
log2wd: u32,
w0: i32,
w1: i32,
o0: i32,
o1: i32,
dst: &mut [S],
ds: usize,
) {
let max = (1i32 << bd) - 1;
for r in 0..h {
for c in 0..w {
let i = r * w + c;
let v = (p0[i] as i32 * w0 + p1[i] as i32 * w1 + ((o0 + o1 + 1) << log2wd)) >> (log2wd + 1);
dst[r * ds + c] = v.max(0).min(max) as u16;
dst[r * ds + c] = S::from_i32(v.max(0).min(max));
}
}
}
@@ -319,6 +418,50 @@ mod tests {
}
}
/// Explicit weighted prediction gives exactly 8-265 / 8-266 for every block width, bit depth,
/// denominator and the extreme weights and offsets, and with the default weights (2^denom,
/// offset 0) exactly the default weighted prediction (8-262 / 8-263) that replaces it then.
#[test]
fn explicit_weighted_prediction_matches_the_formulas() {
let mut rng = Rng(0x7f4a_7c15_9e37_79b9);
for bd in [8u32, 10, 12] {
let max = (1i32 << bd) - 1;
for &w in &WIDTHS {
for _ in 0..40 {
let h = 2;
let p0: Vec<i16> = (0..w * h).map(|_| rng.range(-10_240, 26_624) as i16).collect();
let p1: Vec<i16> = (0..w * h).map(|_| rng.range(-10_240, 26_624) as i16).collect();
let log2wd = rng.range(0, 7) as u32 + 14 - bd;
let wt = |rng: &mut Rng| [-128, 255, rng.range(-128, 255)][rng.range(0, 2) as usize];
let (w0, w1) = (wt(&mut rng), wt(&mut rng));
let off = |rng: &mut Rng| [-128, 127, rng.range(-128, 127)][rng.range(0, 2) as usize] << (bd - 8);
let (o0, o1) = (off(&mut rng), off(&mut rng));
let ds = w + 3;
let (mut uni, mut bi) = (vec![0u16; ds * h], vec![0u16; ds * h]);
put_weighted_uni(&p0, w, h, bd, log2wd, w0, o0, &mut uni, ds);
put_weighted_bi(&p0, &p1, w, h, bd, log2wd, w0, w1, o0, o1, &mut bi, ds);
let denom = log2wd - (14 - bd);
let (mut duni, mut dbi) = (vec![0u16; ds * h], vec![0u16; ds * h]);
put_weighted_uni(&p0, w, h, bd, log2wd, 1 << denom, 0, &mut duni, ds);
put_weighted_bi(&p0, &p1, w, h, bd, log2wd, 1 << denom, 1 << denom, 0, 0, &mut dbi, ds);
let (mut uni0, mut bi0) = (vec![0u16; ds * h], vec![0u16; ds * h]);
put_uni(&p0, w, h, bd, &mut uni0, ds);
put_bi(&p0, &p1, w, h, bd, &mut bi0, ds);
assert_eq!((duni, dbi), (uni0, bi0), "default weights bd {bd} w {w} denom {denom}");
for r in 0..h {
for c in 0..w {
let (a, b) = (p0[r * w + c] as i32, p1[r * w + c] as i32);
let v = if log2wd >= 1 { ((a * w0 + (1 << (log2wd - 1))) >> log2wd) + o0 } else { a * w0 + o0 };
assert_eq!(uni[r * ds + c] as i32, v.clamp(0, max), "uni bd {bd} w {w}");
let v = (a * w0 + b * w1 + ((o0 + o1 + 1) << log2wd)) >> (log2wd + 1);
assert_eq!(bi[r * ds + c] as i32, v.clamp(0, max), "bi bd {bd} w {w}");
}
}
}
}
}
}
/// First passes over 8-bit samples in 16-bit lanes equal the 32-bit sums, including the
/// extreme sample patterns of every filter.
#[test]
@@ -326,8 +469,9 @@ mod tests {
let mut rng = Rng(0x2545_f491_4f6c_dd1d);
fn check<const T: usize>(f: &[i8; T], src: &[i16], step: usize, w: usize) {
let (mut a, mut b) = ([0i16; 64], [0i16; 64]);
filt(src, step, f, 0, &mut a, w, true);
filt(src, step, f, 0, &mut b, w, false);
let taps = |k: usize| src.get(k * step..);
filt(taps, f, 0, &mut a, w, true);
filt(taps, f, 0, &mut b, w, false);
assert_eq!(a[..w], b[..w], "filter {f:?} step {step} w {w}");
}
for &w in &WIDTHS {
@@ -369,36 +513,90 @@ mod tests {
Frame::from_planes(1, 0, &like, &y, &c, &c2)
}
/// Interpolation from the reference lines in place gives the same prediction samples as from
/// an edge-replicated copy of the window, for every fractional position, block size and bit
/// depth, inside the picture and next to its edges (where in-place reads are used for windows
/// that cross only the top or bottom edge).
#[test]
fn in_place_window_matches_the_copied_window() {
fn check<S: Sample>(rng: &mut Rng, bd: u32) {
let mut sc = McScratch::default();
let f = frame(rng, 96, 64, bd);
let (mut in_place, mut copied) = (0, 0);
for _ in 0..2000 {
let c = rng.range(0, 2) as usize;
let (w, h) = ([4usize, 8, 12, 16, 24, 32][rng.range(0, 5) as usize], [4usize, 8, 16, 32][rng.range(0, 3) as usize]);
let (w, h) = if c == 0 { (w, h) } else { (w / 2, h / 2) };
let (x, y) = (rng.range(-12, 100), rng.range(-12, 70));
let mv = [rng.range(-64, 64) as i16, rng.range(-64, 64) as i16];
let mut a = vec![0i16; w * h];
let mut b = vec![0i16; w * h];
let (t, frac, unit) = if c == 0 { (8usize, 3, 2) } else { (4, 7, 3) };
let (fx, fy) = ((mv[0] & frac) as usize, (mv[1] & frac) as usize);
let (x0, y0) = (x + (mv[0] as i32 >> unit) - (t as i32 / 2 - 1), y + (mv[1] as i32 >> unit) - (t as i32 / 2 - 1));
let (shift1, shift3) = (bd - 8, 14 - bd);
let mut rows: Vec<&[S]> = vec![&[]; h + t - 1];
if f.window_rows(c, x0, y0, w + t - 1, &mut rows) {
in_place += 1;
} else {
copied += 1;
}
// reference: the copied window
let mut win = vec![0i16; WIN * WIN];
let mut tmp = vec![0i16; WIN * WIN];
if c == 0 {
f.luma_window::<S>(x0, y0, w + t - 1, h + t - 1, &mut win, WIN);
} else {
f.chroma_window::<S>(c - 1, x0, y0, w + t - 1, h + t - 1, &mut win, WIN);
}
let wrows: Vec<&[i16]> = win.chunks(WIN).take(h + t - 1).collect();
if c == 0 {
mc_luma::<S>(&f, x, y, mv, w, h, &mut a, &mut sc);
interpolate::<i16, 8>(&wrows, &mut tmp, fx, fy, &LUMA_FILTER[fx], &LUMA_FILTER[fy], w, h, shift1, shift3, &mut b);
} else {
mc_chroma::<S>(&f, c - 1, x, y, mv, w, h, &mut a, &mut sc);
interpolate::<i16, 4>(&wrows, &mut tmp, fx, fy, &CHROMA_FILTER[fx], &CHROMA_FILTER[fy], w, h, shift1, shift3, &mut b);
}
assert_eq!(a, b, "plane {c} bd {bd} at ({x}, {y}) mv {mv:?} {w}x{h}");
}
assert!(in_place > 500 && copied > 100, "{in_place} in place, {copied} copied");
}
let mut rng = Rng(0x5851_f42d_4c95_7f2d);
check::<u8>(&mut rng, 8);
check::<u16>(&mut rng, 10);
}
/// The full-sample uni-prediction shortcut (`Frame::copy_block`) gives what interpolation and
/// 8-262 give, inside the picture and across every edge.
/// 8-262 give, inside the picture and across every edge (8-bit pictures stored as `u8`, 10-bit
/// ones as `u16`).
#[test]
fn full_sample_copy_matches_interpolation() {
let mut rng = Rng(0x0123_4567_89ab_cdef);
let mut sc = McScratch::default();
for bd in [8u32, 10] {
let f = frame(&mut rng, 48, 40, bd);
fn check<S: Sample>(rng: &mut Rng, bd: u32) {
let mut sc = McScratch::default();
let f = frame(rng, 48, 40, bd);
for _ in 0..300 {
let (w, h) = ([4usize, 8, 12, 16, 24, 32][rng.range(0, 5) as usize], [4usize, 8, 16][rng.range(0, 2) as usize]);
let (x, y) = (rng.range(-40, 80), rng.range(-40, 70));
let mut pred = vec![0i16; w * h];
let (mut a, mut b) = (vec![0u16; w * h], vec![0u16; w * h]);
inter_luma(&f, x, y, w, h, &mut pred, &mut sc);
let (mut a, mut b) = (vec![S::default(); w * h], vec![S::default(); w * h]);
mc_luma::<S>(&f, 0, 0, [(x * 4) as i16, (y * 4) as i16], w, h, &mut pred, &mut sc);
put_uni(&pred, w, h, bd, &mut a, w);
f.copy_block(0, x, y, w, h, &mut b, w);
assert_eq!(a, b, "luma bd {bd} at ({x}, {y}) {w}x{h}");
// (the window really was read: not all samples equal the default)
assert!(a.iter().any(|&v| v != S::default()) || w * h < 8);
for c in 1..3 {
let (cw, ch) = (w / 2, h / 2);
mc_chroma(&f, c - 1, 0, 0, [(x * 8) as i16, (y * 8) as i16], cw, ch, &mut pred, &mut sc);
let (mut a, mut b) = (vec![0u16; cw * ch], vec![0u16; cw * ch]);
mc_chroma::<S>(&f, c - 1, 0, 0, [(x * 8) as i16, (y * 8) as i16], cw, ch, &mut pred, &mut sc);
let (mut a, mut b) = (vec![S::default(); cw * ch], vec![S::default(); cw * ch]);
put_uni(&pred, cw, ch, bd, &mut a, cw);
f.copy_block(c, x, y, cw, ch, &mut b, cw);
assert_eq!(a, b, "chroma {c} bd {bd} at ({x}, {y}) {cw}x{ch}");
}
}
}
}
fn inter_luma(f: &Frame, x: i32, y: i32, w: usize, h: usize, out: &mut [i16], sc: &mut McScratch) {
mc_luma(f, 0, 0, [(x * 4) as i16, (y * 4) as i16], w, h, out, sc);
let mut rng = Rng(0x0123_4567_89ab_cdef);
check::<u8>(&mut rng, 8);
check::<u16>(&mut rng, 10);
}
}
+12 -10
View File
@@ -1,6 +1,7 @@
//! Intra sample prediction (8.4.4.2): reference substitution, filtering (incl. strong intra
//! smoothing), planar, DC and the 33 angular modes.
use crate::picture::Sample;
use crate::spec_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],
@@ -100,7 +101,7 @@ pub fn filter(r: &mut Refs, mode: u32, n: usize, strong_enabled: bool, is_luma:
/// 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) {
pub fn predict<S: Sample>(r: &Refs, mode: u32, n: usize, edge_filters: bool, bit_depth: u32, out: &mut [S], os: usize) {
let max = (1i32 << bit_depth) - 1;
match mode {
0 => {
@@ -113,7 +114,7 @@ pub fn predict(r: &Refs, mode: u32, n: usize, edge_filters: bool, bit_depth: u32
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;
out[y * os + x] = S::from_i32(v);
}
}
}
@@ -121,17 +122,18 @@ pub fn predict(r: &Refs, mode: u32, n: usize, edge_filters: bool, bit_depth: u32
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;
let dcs = S::from_u16(dc);
for y in 0..n {
out[y * os..y * os + n].fill(dc);
out[y * os..y * os + n].fill(dcs);
}
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;
out[0] = S::from_i32(((r.left[1] as u32 + 2 * d + r.top[1] as u32 + 2) >> 2) as i32);
for x in 1..n {
out[x] = ((r.top[1 + x] as u32 + 3 * d + 2) >> 2) as u16;
out[x] = S::from_i32(((r.top[1 + x] as u32 + 3 * d + 2) >> 2) as i32);
}
for y in 1..n {
out[y * os] = ((r.left[1 + y] as u32 + 3 * d + 2) >> 2) as u16;
out[y * os] = S::from_i32(((r.left[1 + y] as u32 + 3 * d + 2) >> 2) as i32);
}
}
}
@@ -167,9 +169,9 @@ pub fn predict(r: &Refs, mode: u32, n: usize, edge_filters: bool, bit_depth: u32
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;
out[i * os + j] = S::from_i32(v);
} else {
out[j * os + i] = v as u16;
out[j * os + i] = S::from_i32(v);
}
}
}
@@ -177,12 +179,12 @@ pub fn predict(r: &Refs, mode: u32, n: usize, edge_filters: bool, bit_depth: u32
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;
out[y * os] = S::from_i32(v.clamp(0, max));
}
} 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;
out[x] = S::from_i32(v.clamp(0, max));
}
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ fn same_motion(a: &MvField, b: &MvField) -> bool {
true
}
impl<'a> SliceDecoder<'a> {
impl<'a, S: crate::picture::Sample> SliceDecoder<'a, S> {
/// Prediction block availability (6.4.2).
fn pb_avail(&self, xc: i32, yc: i32, ncb: i32, xp: i32, yp: i32, w: i32, h: i32, part_idx: usize, xn: i32, yn: i32) -> bool {
let same_cb = xc <= xn && yc <= yn && xc + ncb > xn && yc + ncb > yn;
+411 -120
View File
@@ -2,6 +2,7 @@
//! deblocking and SAO) together with the compressed motion field used for temporal motion vector
//! prediction.
use crate::Plane;
use std::sync::{Arc, OnceLock};
/// Motion data of one 16x16 block of a decoded picture (8.5.3.2.9 uses the motion of the top-left
@@ -15,16 +16,109 @@ pub struct ColMv {
pub flags: u8,
}
/// Storage type of decoded samples: `u8` for 8-bit pictures (half the memory and cache traffic
/// of 16-bit storage in prediction reads, filters and output), `u16` for higher bit depths.
/// Values are always within `0..1 << bit_depth`.
pub trait Sample: Copy + Default + PartialEq + std::fmt::Debug + Send + Sync + 'static {
fn to_i32(self) -> i32;
fn to_i16(self) -> i16;
fn to_u16(self) -> u16;
/// `v` (a sample value, already clipped to the bit depth) in the storage type.
fn from_i32(v: i32) -> Self;
/// `v` (a sample value, within the bit depth) in the storage type.
fn from_u16(v: u16) -> Self;
/// The rows of `f` if they are stored with this type.
fn rows(f: &Frame) -> Option<&[OnceLock<FrameRow<Self>>]>;
fn wrap_rows(rows: Box<[OnceLock<FrameRow<Self>>]>) -> Rows;
}
impl Sample for u8 {
#[inline(always)]
fn to_i32(self) -> i32 {
self as i32
}
#[inline(always)]
fn to_i16(self) -> i16 {
self as i16
}
#[inline(always)]
fn to_u16(self) -> u16 {
self as u16
}
#[inline(always)]
fn from_i32(v: i32) -> Self {
v as u8
}
#[inline(always)]
fn from_u16(v: u16) -> Self {
v as u8
}
#[inline(always)]
fn rows(f: &Frame) -> Option<&[OnceLock<FrameRow<u8>>]> {
match &f.rows {
Rows::U8(r) => Some(r),
Rows::U16(_) => None,
}
}
fn wrap_rows(rows: Box<[OnceLock<FrameRow<u8>>]>) -> Rows {
Rows::U8(rows)
}
}
impl Sample for u16 {
#[inline(always)]
fn to_i32(self) -> i32 {
self as i32
}
#[inline(always)]
fn to_i16(self) -> i16 {
self as i16
}
#[inline(always)]
fn to_u16(self) -> u16 {
self
}
#[inline(always)]
fn from_i32(v: i32) -> Self {
v as u16
}
#[inline(always)]
fn from_u16(v: u16) -> Self {
v
}
#[inline(always)]
fn rows(f: &Frame) -> Option<&[OnceLock<FrameRow<u16>>]> {
match &f.rows {
Rows::U16(r) => Some(r),
Rows::U8(_) => None,
}
}
fn wrap_rows(rows: Box<[OnceLock<FrameRow<u16>>]>) -> Rows {
Rows::U16(rows)
}
}
/// Whether pictures with these bit depths are stored as `u8` (both 8-bit), else as `u16`.
pub fn stores_u8(bit_depth: u32, bit_depth_c: u32) -> bool {
bit_depth <= 8 && bit_depth_c <= 8
}
/// One published CTB row.
pub struct FrameRow {
pub struct FrameRow<S> {
/// Luma lines of the row (stride = picture width).
pub y: Box<[u16]>,
pub cb: Box<[u16]>,
pub cr: Box<[u16]>,
pub y: Box<[S]>,
pub cb: Box<[S]>,
pub cr: Box<[S]>,
/// Motion per 16x16 block: (width16 * rows16) entries, raster within the row.
pub col: Box<[ColMv]>,
}
/// The CTB rows of a frame, in its storage type.
pub enum Rows {
U8(Box<[OnceLock<FrameRow<u8>>]>),
U16(Box<[OnceLock<FrameRow<u16>>]>),
}
/// A decoded (or in-progress) picture as stored in the DPB and referenced by later pictures.
pub struct Frame {
pub id: u32,
@@ -36,69 +130,87 @@ pub struct Frame {
pub log2_ctb: u32,
pub bit_depth: u32,
pub bit_depth_c: u32,
rows: Box<[OnceLock<FrameRow>]>,
rows: Rows,
}
/// The value of a published row (waiting for it with the `threads` feature).
#[inline(always)]
fn wait<T>(cell: &OnceLock<T>) -> Option<&T> {
#[cfg(feature = "threads")]
{
Some(cell.wait())
}
#[cfg(not(feature = "threads"))]
{
cell.get()
}
}
impl Frame {
pub fn new(id: u32, poc: i32, width: usize, height: usize, log2_ctb: u32, bit_depth: u32, bit_depth_c: u32) -> Self {
let n = height.div_ceil(1 << log2_ctb);
Frame {
id,
poc,
width,
height,
cwidth: width / 2,
cheight: height / 2,
log2_ctb,
bit_depth,
bit_depth_c,
rows: (0..n).map(|_| OnceLock::new()).collect(),
fn cells<S: Sample>(n: usize) -> Rows {
S::wrap_rows((0..n).map(|_| OnceLock::new()).collect())
}
let rows = if stores_u8(bit_depth, bit_depth_c) { cells::<u8>(n) } else { cells::<u16>(n) };
Frame { id, poc, width, height, cwidth: width / 2, cheight: height / 2, log2_ctb, bit_depth, bit_depth_c, rows }
}
pub fn num_rows(&self) -> usize {
self.rows.len()
match &self.rows {
Rows::U8(r) => r.len(),
Rows::U16(r) => r.len(),
}
}
/// Number of luma lines in CTB row `r`.
pub fn row_lines(&self, r: usize) -> usize {
let ctb = 1usize << self.log2_ctb;
ctb.min(self.height - r * ctb)
ctb.min(self.height.saturating_sub(r * ctb))
}
pub fn width16(&self) -> usize {
self.width.div_ceil(16)
}
/// Publish CTB row `r` (ignored if already published).
pub fn publish(&self, r: usize, row: FrameRow) {
let _ = self.rows[r].set(row);
/// Publish CTB row `r` (ignored if already published, or not of the frame's sample type).
pub fn publish<S: Sample>(&self, r: usize, row: FrameRow<S>) {
if let Some(cell) = S::rows(self).and_then(|rows| rows.get(r)) {
let _ = cell.set(row);
}
}
pub fn is_published(&self, r: usize) -> bool {
self.rows[r].get().is_some()
match &self.rows {
Rows::U8(rows) => rows.get(r).is_some_and(|c| c.get().is_some()),
Rows::U16(rows) => rows.get(r).is_some_and(|c| c.get().is_some()),
}
}
pub fn is_complete(&self) -> bool {
self.rows.last().is_none_or(|r| r.get().is_some())
let n = self.num_rows();
n == 0 || self.is_published(n - 1)
}
/// CTB row `r`, waiting until it has been published.
/// CTB row `r` in sample type `S`, waiting until it has been published (None: no such row,
/// or the frame stores another sample type).
#[inline]
pub fn row(&self, r: usize) -> &FrameRow {
#[cfg(feature = "threads")]
{
self.rows[r].wait()
}
#[cfg(not(feature = "threads"))]
{
self.rows[r].get().expect("reference row decoded before use")
pub fn row<S: Sample>(&self, r: usize) -> Option<&FrameRow<S>> {
wait(S::rows(self)?.get(r)?)
}
/// The motion of CTB row `r`, waiting until it has been published.
#[inline]
fn col_row(&self, r: usize) -> Option<&[ColMv]> {
match &self.rows {
Rows::U8(rows) => wait(rows.get(r)?).map(|row| &*row.col),
Rows::U16(rows) => wait(rows.get(r)?).map(|row| &*row.col),
}
}
pub fn wait_complete(&self) {
for r in 0..self.rows.len() {
self.row(r);
for r in 0..self.num_rows() {
let _ = self.col_row(r);
}
}
@@ -106,47 +218,78 @@ impl Frame {
#[inline]
pub fn col_mv(&self, x: usize, y: usize) -> ColMv {
let r = y >> self.log2_ctb;
let row = self.row(r);
let y16 = (y & ((1 << self.log2_ctb) - 1)) >> 4;
row.col[y16 * self.width16() + (x >> 4)]
self.col_row(r).and_then(|col| col.get(y16 * self.width16() + (x >> 4))).copied().unwrap_or_default()
}
/// Copy a w x h luma window at (x0, y0) with edge replication into `out` (stride `os`).
pub fn luma_window(&self, x0: i32, y0: i32, w: usize, h: usize, out: &mut [i16], os: usize) {
pub fn luma_window<S: Sample>(&self, x0: i32, y0: i32, w: usize, h: usize, out: &mut [i16], os: usize) {
let (width, height) = (self.width as i32, self.height as i32);
let sh = self.log2_ctb;
let mask = (1i32 << sh) - 1;
for r in 0..h {
let yy = (y0 + r as i32).clamp(0, height - 1);
let row = self.row((yy >> sh) as usize);
let Some(row) = self.row::<S>((yy >> sh) as usize) else { return };
let off = ((yy & mask) * width) as usize;
copy_line(&row.y[off..off + width as usize], x0, &mut out[r * os..r * os + w]);
}
}
/// Copy a w x h window of chroma component `c` (0 = Cb, 1 = Cr) with edge replication.
pub fn chroma_window(&self, c: usize, x0: i32, y0: i32, w: usize, h: usize, out: &mut [i16], os: usize) {
pub fn chroma_window<S: Sample>(&self, c: usize, x0: i32, y0: i32, w: usize, h: usize, out: &mut [i16], os: usize) {
let (cw, ch) = (self.cwidth as i32, self.cheight as i32);
let sh = self.log2_ctb - 1;
let mask = (1i32 << sh) - 1;
for r in 0..h {
let yy = (y0 + r as i32).clamp(0, ch - 1);
let row = self.row((yy >> sh) as usize);
let Some(row) = self.row::<S>((yy >> sh) as usize) else { return };
let plane = if c == 0 { &row.cb } else { &row.cr };
let off = ((yy & mask) * cw) as usize;
copy_line(&plane[off..off + cw as usize], x0, &mut out[r * os..r * os + w]);
}
}
/// The w-sample runs at x0 of the h lines from y0 of plane `c` (0 = luma, 1 = Cb, 2 = Cr) into
/// `rows` (h = `rows.len()`), lines outside the picture replicated from its top / bottom edge
/// (8-228, 8-229): the reference window of a prediction block without copying it. False (and
/// `rows` partly filled) when the runs are not inside the picture horizontally.
pub fn window_rows<'f, S: Sample>(&'f self, c: usize, x0: i32, y0: i32, w: usize, rows: &mut [&'f [S]]) -> bool {
let (pw, ph, sh) = if c == 0 { (self.width, self.height, self.log2_ctb) } else { (self.cwidth, self.cheight, self.log2_ctb - 1) };
let Some(x) = usize::try_from(x0).ok().filter(|&x| x.saturating_add(w) <= pw) else { return false };
let mask = (1usize << sh) - 1;
let max_y = ph as i32 - 1;
let mut cur: Option<(usize, &'f [S])> = None;
for (r, slot) in rows.iter_mut().enumerate() {
let yy = y0.saturating_add(r as i32).min(max_y).max(0) as usize;
let plane = match cur {
Some((i, p)) if i == yy >> sh => p,
_ => {
let Some(row) = self.row::<S>(yy >> sh) else { return false };
let p: &'f [S] = match c {
0 => &row.y,
1 => &row.cb,
_ => &row.cr,
};
cur = Some((yy >> sh, p));
p
}
};
let off = (yy & mask) * pw + x;
let Some(run) = plane.get(off..off + w) else { return false };
*slot = run;
}
true
}
/// Copy the w x h block at (x0, y0) of plane `c` (0 = luma, 1 = Cb, 2 = Cr) into `dst`
/// (stride `ds`), samples outside the picture replicated from its edges (8-228, 8-229).
#[allow(clippy::too_many_arguments)]
pub fn copy_block(&self, c: usize, x0: i32, y0: i32, w: usize, h: usize, dst: &mut [u16], ds: usize) {
pub fn copy_block<S: Sample>(&self, c: usize, x0: i32, y0: i32, w: usize, h: usize, dst: &mut [S], ds: usize) {
let (pw, ph, sh) = if c == 0 { (self.width, self.height, self.log2_ctb) } else { (self.cwidth, self.cheight, self.log2_ctb - 1) };
let inside = usize::try_from(x0).ok().filter(|&x| x.saturating_add(w) <= pw);
for (r, d) in dst.chunks_mut(ds).take(h).enumerate() {
let yy = y0.saturating_add(r as i32).min(ph as i32 - 1).max(0) as usize;
let row = self.row(yy >> sh);
let Some(row) = self.row::<S>(yy >> sh) else { return };
let plane = match c {
0 => &row.y,
1 => &row.cb,
@@ -160,98 +303,147 @@ impl Frame {
None => {
let max = (pw as i32 - 1).max(0);
for (i, d) in d.iter_mut().enumerate() {
*d = line.get(x0.saturating_add(i as i32).clamp(0, max) as usize).copied().unwrap_or(0);
*d = line.get(x0.saturating_add(i as i32).clamp(0, max) as usize).copied().unwrap_or_default();
}
}
}
}
}
/// Crop and copy the frame into planar buffers (waits for completion).
pub fn copy_cropped(&self, crop: (usize, usize, usize, usize)) -> (Vec<u16>, Vec<u16>, Vec<u16>) {
let mut out = (Vec::new(), Vec::new(), Vec::new());
self.copy_cropped_with(crop, &mut out, |o, s| o.extend_from_slice(s));
out
/// Crop the frame into planar buffers (waits for completion): 8-bit planes when `eight_bit`,
/// else 16-bit ones. Rows stored in the output type are copied as they are.
pub fn cropped_planes(&self, crop: (usize, usize, usize, usize), eight_bit: bool) -> (Plane, Plane, Plane) {
fn planes<S: Sample, T>(f: &Frame, crop: (usize, usize, usize, usize), conv: impl Fn(S) -> T, wrap: impl Fn(Vec<T>) -> Plane) -> (Plane, Plane, Plane) {
let (y, u, v) = f.copy_cropped::<S, T>(crop, conv);
(wrap(y), wrap(u), wrap(v))
}
match (&self.rows, eight_bit) {
(Rows::U8(_), true) => planes::<u8, u8>(self, crop, |v| v, Plane::U8),
(Rows::U8(_), false) => planes::<u8, u16>(self, crop, u16::from, Plane::U16),
// (8-bit luma with deeper chroma: the 8-bit output keeps the low bits, as before)
(Rows::U16(_), true) => planes::<u16, u8>(self, crop, |v| v as u8, Plane::U8),
(Rows::U16(_), false) => planes::<u16, u16>(self, crop, |v| v, Plane::U16),
}
}
/// [`copy_cropped`](Self::copy_cropped) into 8-bit planes (samples of an 8-bit picture),
/// without an intermediate 16-bit copy.
pub fn copy_cropped_u8(&self, crop: (usize, usize, usize, usize)) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let mut out = (Vec::new(), Vec::new(), Vec::new());
self.copy_cropped_with(crop, &mut out, |o, s| o.extend(s.iter().map(|&v| v as u8)));
out
}
fn copy_cropped_with<T>(&self, crop: (usize, usize, usize, usize), out: &mut (Vec<T>, Vec<T>, Vec<T>), put: impl Fn(&mut Vec<T>, &[u16])) {
/// The cropped planes of the frame converted sample by sample with `conv` (a plain copy for
/// the identity).
fn copy_cropped<S: Sample, T>(&self, crop: (usize, usize, usize, usize), conv: impl Fn(S) -> T) -> (Vec<T>, Vec<T>, Vec<T>) {
let (cx, cy, cw, ch) = crop;
let ctb = 1usize << self.log2_ctb;
let (y, u, v) = out;
y.reserve_exact(cw * ch);
for r in cy..cy + ch {
let row = self.row(r / ctb);
let o = (r % ctb) * self.width + cx;
put(y, &row.y[o..o + cw]);
let (mut y, mut u, mut v) = (Vec::new(), Vec::new(), Vec::new());
let put = |o: &mut Vec<T>, plane: &[S], off: usize, n: usize| {
if let Some(s) = plane.get(off..off.saturating_add(n)) {
o.extend(s.iter().map(|&v| conv(v)));
}
};
y.reserve_exact(cw.saturating_mul(ch));
for r in cy..cy.saturating_add(ch) {
let Some(row) = self.row::<S>(r / ctb) else { break };
put(&mut y, &row.y, (r % ctb) * self.width + cx, cw);
}
let (ccx, ccy, ccw, cch) = (cx / 2, cy / 2, cw.div_ceil(2), ch.div_ceil(2));
let cctb = ctb / 2;
u.reserve_exact(ccw * cch);
v.reserve_exact(ccw * cch);
for r in ccy..ccy + cch {
let row = self.row(r / cctb);
u.reserve_exact(ccw.saturating_mul(cch));
v.reserve_exact(ccw.saturating_mul(cch));
for r in ccy..ccy.saturating_add(cch) {
let Some(row) = self.row::<S>(r / cctb) else { break };
let o = (r % cctb) * self.cwidth + ccx;
put(u, &row.cb[o..o + ccw]);
put(v, &row.cr[o..o + ccw]);
put(&mut u, &row.cb, o, ccw);
put(&mut v, &row.cr, o, ccw);
}
(y, u, v)
}
/// A frame with every row published from full-picture planes and no motion (used to replace missing
/// reference pictures).
pub fn from_planes(id: u32, poc: i32, like: &Frame, y: &[u16], cb: &[u16], cr: &[u16]) -> Frame {
let f = Frame::new(id, poc, like.width, like.height, like.log2_ctb, like.bit_depth, like.bit_depth_c);
let ctb = 1usize << f.log2_ctb;
let w16 = f.width16();
for r in 0..f.num_rows() {
let lines = f.row_lines(r);
let (y0, y1) = (r * ctb * f.width, (r * ctb + lines) * f.width);
let clines = lines.div_ceil(2);
let (c0, c1) = (r * ctb / 2 * f.cwidth, (r * ctb / 2 + clines) * f.cwidth);
f.publish(
r,
FrameRow { y: y[y0..y1].into(), cb: cb[c0..c1].into(), cr: cr[c0..c1].into(), col: vec![ColMv::default(); w16 * lines.div_ceil(16)].into() },
);
/// Publish every row not published yet as mid-grey without motion (a missing reference picture,
/// or a picture whose decoding failed: readers never block on it).
pub fn publish_gray(&self) {
fn fill<S: Sample>(f: &Frame) {
let (gy, gc) = (S::from_u16(1 << (f.bit_depth.clamp(1, 16) - 1)), S::from_u16(1 << (f.bit_depth_c.clamp(1, 16) - 1)));
let w16 = f.width16();
for r in 0..f.num_rows() {
if f.is_published(r) {
continue;
}
let lines = f.row_lines(r);
let clines = lines.div_ceil(2);
f.publish(
r,
FrameRow::<S> {
y: vec![gy; lines * f.width].into(),
cb: vec![gc; clines * f.cwidth].into(),
cr: vec![gc; clines * f.cwidth].into(),
col: vec![ColMv::default(); w16 * lines.div_ceil(16)].into(),
},
);
}
}
match &self.rows {
Rows::U8(_) => fill::<u8>(self),
Rows::U16(_) => fill::<u16>(self),
}
f
}
/// A mid-grey frame with the geometry of `like`.
pub fn gray(id: u32, poc: i32, like: &Frame) -> Frame {
let y = vec![1u16 << (like.bit_depth - 1); like.width * like.height];
let c = vec![1u16 << (like.bit_depth_c - 1); like.cwidth * like.cheight];
Frame::from_planes(id, poc, like, &y, &c, &c)
let f = Frame::new(id, poc, like.width, like.height, like.log2_ctb, like.bit_depth, like.bit_depth_c);
f.publish_gray();
f
}
/// A frame with every row published from full-picture planes and no motion.
#[cfg(test)]
pub fn from_planes(id: u32, poc: i32, like: &Frame, y: &[u16], cb: &[u16], cr: &[u16]) -> Frame {
fn fill<S: Sample>(f: &Frame, y: &[u16], cb: &[u16], cr: &[u16]) {
let ctb = 1usize << f.log2_ctb;
let w16 = f.width16();
let conv = |s: &[u16]| -> Box<[S]> { s.iter().map(|&v| S::from_u16(v)).collect() };
for r in 0..f.num_rows() {
let lines = f.row_lines(r);
let (y0, y1) = (r * ctb * f.width, (r * ctb + lines) * f.width);
let clines = lines.div_ceil(2);
let (c0, c1) = (r * ctb / 2 * f.cwidth, (r * ctb / 2 + clines) * f.cwidth);
f.publish(
r,
FrameRow::<S> {
y: conv(&y[y0..y1]),
cb: conv(&cb[c0..c1]),
cr: conv(&cr[c0..c1]),
col: vec![ColMv::default(); w16 * lines.div_ceil(16)].into(),
},
);
}
}
let f = Frame::new(id, poc, like.width, like.height, like.log2_ctb, like.bit_depth, like.bit_depth_c);
match &f.rows {
Rows::U8(_) => fill::<u8>(&f, y, cb, cr),
Rows::U16(_) => fill::<u16>(&f, y, cb, cr),
}
f
}
}
/// `dst.copy_from_slice(src)` for the run lengths of prediction blocks as fixed-size copies: a
/// libc `memcpy` call per short block line costs more than the copy.
#[inline(always)]
fn copy_run(src: &[u16], dst: &mut [u16]) {
fn copy_run<S: Sample>(src: &[S], dst: &mut [S]) {
#[inline(always)]
fn fixed<const N: usize>(src: &[u16], dst: &mut [u16]) {
fn fixed<S: Sample, const N: usize>(src: &[S], dst: &mut [S]) {
if let (Some(s), Some(d)) = (src.first_chunk::<N>(), dst.first_chunk_mut::<N>()) {
*d = *s;
}
}
match dst.len() {
2 => fixed::<2>(src, dst),
4 => fixed::<4>(src, dst),
6 => fixed::<6>(src, dst),
8 => fixed::<8>(src, dst),
12 => fixed::<12>(src, dst),
16 => fixed::<16>(src, dst),
24 => fixed::<24>(src, dst),
32 => fixed::<32>(src, dst),
48 => fixed::<48>(src, dst),
64 => fixed::<64>(src, dst),
2 => fixed::<S, 2>(src, dst),
4 => fixed::<S, 4>(src, dst),
6 => fixed::<S, 6>(src, dst),
8 => fixed::<S, 8>(src, dst),
12 => fixed::<S, 12>(src, dst),
16 => fixed::<S, 16>(src, dst),
24 => fixed::<S, 24>(src, dst),
32 => fixed::<S, 32>(src, dst),
48 => fixed::<S, 48>(src, dst),
64 => fixed::<S, 64>(src, dst),
n => {
if let Some(s) = src.get(..n) {
dst.copy_from_slice(s);
@@ -262,44 +454,58 @@ fn copy_run(src: &[u16], dst: &mut [u16]) {
/// Copy `out.len()` samples of `line` starting at x0, replicating edge samples outside the line.
#[inline(always)]
fn copy_line(line: &[u16], x0: i32, out: &mut [i16]) {
fn copy_line<S: Sample>(line: &[S], x0: i32, out: &mut [i16]) {
let w = out.len();
if x0 >= 0 && x0 as usize + w <= line.len() {
let src = &line[x0 as usize..x0 as usize + w];
// fixed-size copies for the window widths (block width + 7 luma, + 3 chroma): see copy_run
#[inline(always)]
fn fixed<const N: usize>(src: &[u16], out: &mut [i16]) {
fn fixed<S: Sample, const N: usize>(src: &[S], out: &mut [i16]) {
if let (Some(s), Some(o)) = (src.first_chunk::<N>(), out.first_chunk_mut::<N>()) {
for j in 0..N {
o[j] = s[j] as i16;
o[j] = s[j].to_i16();
}
}
}
match w {
5 => fixed::<5>(src, out),
7 => fixed::<7>(src, out),
9 => fixed::<9>(src, out),
11 => fixed::<11>(src, out),
15 => fixed::<15>(src, out),
19 => fixed::<19>(src, out),
23 => fixed::<23>(src, out),
27 => fixed::<27>(src, out),
31 => fixed::<31>(src, out),
35 => fixed::<35>(src, out),
39 => fixed::<39>(src, out),
55 => fixed::<55>(src, out),
71 => fixed::<71>(src, out),
5 => fixed::<S, 5>(src, out),
7 => fixed::<S, 7>(src, out),
9 => fixed::<S, 9>(src, out),
11 => fixed::<S, 11>(src, out),
15 => fixed::<S, 15>(src, out),
19 => fixed::<S, 19>(src, out),
23 => fixed::<S, 23>(src, out),
27 => fixed::<S, 27>(src, out),
31 => fixed::<S, 31>(src, out),
35 => fixed::<S, 35>(src, out),
39 => fixed::<S, 39>(src, out),
55 => fixed::<S, 55>(src, out),
71 => fixed::<S, 71>(src, out),
_ => {
for (o, &s) in out.iter_mut().zip(src) {
*o = s as i16;
*o = s.to_i16();
}
}
}
} else {
let max = line.len() as i32 - 1;
for (c, o) in out.iter_mut().enumerate() {
*o = line[(x0 + c as i32).clamp(0, max) as usize] as i16;
// the window crosses the left and / or right picture edge: the edge samples repeated
// outside, the samples inside copied as a run
let (Some(&first), Some(&last)) = (line.first(), line.last()) else { return };
let n = line.len() as i64;
let (x0, w) = (x0 as i64, w as i64);
// out[i] is inside the line for i in lo..hi
let lo = (-x0).clamp(0, w) as usize;
let hi = (n - x0).clamp(0, w) as usize;
let (left, rest) = out.split_at_mut(lo);
left.fill(first.to_i16());
let (mid, right) = rest.split_at_mut(hi.saturating_sub(lo));
let start = (x0 + lo as i64).max(0) as usize;
if let Some(src) = line.get(start..start + mid.len()) {
for (o, &s) in mid.iter_mut().zip(src) {
*o = s.to_i16();
}
}
right.fill(last.to_i16());
}
}
@@ -307,3 +513,88 @@ pub type FrameRef = Arc<Frame>;
/// An entry of RefPicList0/1 of the current slice.
pub type RefPic = crate::dpb::Ref<FrameRef>;
#[cfg(test)]
mod tests {
use super::*;
fn planes(w: usize, h: usize, bd: u32, seed: u64) -> (Vec<u16>, Vec<u16>, Vec<u16>) {
let mut x = seed;
let mut next = move || {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
(x % (1 << bd)) as u16
};
let y = (0..w * h).map(|_| next()).collect();
let cb = (0..w * h / 4).map(|_| next()).collect();
let cr = (0..w * h / 4).map(|_| next()).collect();
(y, cb, cr)
}
/// 8-bit pictures are stored as `u8`, deeper ones as `u16`, and both crop back to exactly the
/// samples they were given.
#[test]
fn storage_type_follows_the_bit_depth_and_crops_exactly() {
let (w, h) = (40usize, 36usize);
let crop = (2usize, 4usize, 33usize, 27usize);
for bd in [8u32, 10] {
let (y, cb, cr) = planes(w, h, bd, 0x9e37_79b9_7f4a_7c15 + bd as u64);
let like = Frame::new(0, 0, w, h, 4, bd, bd);
let f = Frame::from_planes(1, 0, &like, &y, &cb, &cr);
assert_eq!(matches!(f.rows, Rows::U8(_)), bd == 8);
assert!(f.is_complete());
let (py, pu, pv) = f.cropped_planes(crop, bd == 8);
let (cx, cy, cw, ch) = crop;
let want_y: Vec<u16> = (cy..cy + ch).flat_map(|r| y[r * w + cx..r * w + cx + cw].to_vec()).collect();
let (ccw, cch) = (cw.div_ceil(2), ch.div_ceil(2));
let want_c = |p: &[u16]| -> Vec<u16> { (cy / 2..cy / 2 + cch).flat_map(|r| p[r * w / 2 + cx / 2..r * w / 2 + cx / 2 + ccw].to_vec()).collect() };
let got = |p: &Plane| -> Vec<u16> { (0..p.len()).map(|i| p.get(i)).collect() };
assert_eq!(got(&py), want_y, "luma bd {bd}");
assert_eq!(got(&pu), want_c(&cb), "cb bd {bd}");
assert_eq!(got(&pv), want_c(&cr), "cr bd {bd}");
assert_eq!(py.as_u8().is_some(), bd == 8);
}
}
/// Window lines crossing the left and / or right picture edge repeat the edge samples, as the
/// per-sample clamp of 8-228 does.
#[test]
fn edge_replicated_lines_match_the_clamped_reads() {
let line: Vec<u8> = (0..23).map(|i| (i * 11 % 256) as u8).collect();
for n in [1usize, 2, 23] {
let line = &line[..n];
for w in [1usize, 5, 7, 11, 15, 19, 23, 39, 71] {
for x0 in -80i32..80 {
let mut out = vec![-1i16; w];
copy_line(line, x0, &mut out);
let want: Vec<i16> = (0..w as i32).map(|i| line[(x0 + i).clamp(0, n as i32 - 1) as usize] as i16).collect();
assert_eq!(out, want, "n {n} w {w} x0 {x0}");
}
}
}
}
/// Reading or publishing a frame with the other sample type (never done by the decoder) gives
/// nothing instead of a panic, and a grey frame of either type is complete.
#[test]
fn wrong_sample_type_is_harmless() {
let f = Frame::new(0, 0, 32, 32, 4, 8, 8);
f.publish(0, FrameRow::<u16> { y: vec![1; 16 * 32].into(), cb: vec![1; 8 * 16].into(), cr: vec![1; 8 * 16].into(), col: Box::new([]) });
assert!(!f.is_published(0));
f.publish_gray();
assert!(f.is_complete());
assert!(f.row::<u16>(0).is_none());
assert_eq!(f.row::<u8>(1).map(|r| r.y[0]), Some(128));
let mut win = [7i16; 64];
f.luma_window::<u16>(-3, -3, 8, 8, &mut win, 8);
assert!(win.iter().all(|&v| v == 7));
let mut dst = [5u16; 16];
f.copy_block::<u16>(0, 0, 0, 4, 4, &mut dst, 4);
assert!(dst.iter().all(|&v| v == 5));
let g = Frame::gray(1, 0, &Frame::new(0, 0, 16, 8, 4, 10, 10));
assert!(g.is_complete());
assert_eq!(g.row::<u16>(0).map(|r| r.cb[0]), Some(512));
assert_eq!(g.col_mv(15, 7), ColMv::default());
}
}
+39 -21
View File
@@ -8,7 +8,7 @@ use crate::error::{Result, ensure, invalid};
use crate::inter;
use crate::intra::{self, Refs};
use crate::params::{Layout, Pps, Sps};
use crate::picture::{FrameRef, RefPic};
use crate::picture::{FrameRef, RefPic, Sample};
use crate::slice::{SliceHeader, SliceType};
use crate::spec_tables::*;
use crate::tables::{CTX_IDX_MAP, LEVEL_SCALE, SCAN_4, qpc_420, scan_order};
@@ -95,7 +95,7 @@ pub struct SliceJob {
}
/// Per-picture decoding state.
pub struct PicState {
pub struct PicState<S> {
pub frame: FrameRef,
#[allow(dead_code)]
pub sps: Arc<Sps>,
@@ -114,7 +114,7 @@ pub struct PicState {
pub bd_y: u32,
pub bd_c: u32,
/// Reconstructed (pre-SAO) planes.
pub planes: [Vec<u16>; 3],
pub planes: [Vec<S>; 3],
pub blk: Vec<Blk>,
pub mvf: Vec<MvField>,
/// Index into `slices` per CTB (u32::MAX = not decoded).
@@ -136,7 +136,7 @@ pub struct PicState {
pub draft: bool,
}
impl PicState {
impl<S: Sample> PicState<S> {
pub fn new(frame: FrameRef, sps: Arc<Sps>, pps: Arc<Pps>, layout: Arc<Layout>, scaling: Option<Arc<Vec<Vec<u8>>>>) -> Self {
let (width, height) = (sps.width as usize, sps.height as usize);
let (cwidth, cheight) = (width / 2, height / 2);
@@ -144,8 +144,8 @@ impl PicState {
let log2_ctb = sps.log2_ctb;
let (wctb, hctb) = (sps.pic_width_in_ctbs() as usize, sps.pic_height_in_ctbs() as usize);
let (bd_y, bd_c) = (sps.bit_depth_luma, sps.bit_depth_chroma);
let gy = 1u16 << (bd_y - 1);
let gc = 1u16 << (bd_c - 1);
let gy = S::from_u16(1u16 << (bd_y - 1));
let gc = S::from_u16(1u16 << (bd_c - 1));
PicState {
poc: frame.poc,
frame,
@@ -231,8 +231,8 @@ pub enum PartMode {
}
/// Decoder of one slice segment.
pub struct SliceDecoder<'a> {
pub pic: &'a mut PicState,
pub struct SliceDecoder<'a, S> {
pub pic: &'a mut PicState<S>,
pub sh: &'a SliceHeader,
pub sps: &'a Sps,
pub pps: &'a Pps,
@@ -272,7 +272,7 @@ pub struct SliceDecoder<'a> {
pub ctb_y: i32,
}
impl<'a> SliceDecoder<'a> {
impl<'a, S: Sample> SliceDecoder<'a, S> {
#[inline(always)]
fn dd(&mut self, ctx: usize) -> u32 {
self.c.decode_decision(ctx)
@@ -294,7 +294,7 @@ impl<'a> SliceDecoder<'a> {
}
/// Decode the slice segment `job` into `pic`.
pub fn decode(pic: &'a mut PicState, job: &'a SliceJob) -> Result<()> {
pub fn decode(pic: &'a mut PicState<S>, job: &'a SliceJob) -> Result<()> {
let sh = &job.sh;
let (sps, pps) = (&*job.sps, &*job.pps);
ensure!(sh.data_offset < job.rbsp.len(), "empty slice segment data");
@@ -802,7 +802,7 @@ impl<'a> SliceDecoder<'a> {
for y in 0..n {
for x in 0..n {
let v = r.read_bits(bl)? as u16;
self.pic.planes[0][(y0 as usize + y) * w + x0 as usize + x] = v << sy;
self.pic.planes[0][(y0 as usize + y) * w + x0 as usize + x] = S::from_u16(v << sy);
}
}
let cw = self.pic.cwidth;
@@ -810,7 +810,7 @@ impl<'a> SliceDecoder<'a> {
for y in 0..n / 2 {
for x in 0..n / 2 {
let v = r.read_bits(bc)? as u16;
self.pic.planes[c][(y0 as usize / 2 + y) * cw + x0 as usize / 2 + x] = v << sc;
self.pic.planes[c][(y0 as usize / 2 + y) * cw + x0 as usize / 2 + x] = S::from_u16(v << sc);
}
}
}
@@ -997,14 +997,14 @@ impl<'a> SliceDecoder<'a> {
let mut at = [false; 129];
let addr = self.slice_addr;
let avail =
|pic: &PicState, xn: i32, yn: i32| -> bool { pic.zavail(xl, yl, xn, yn, addr) && (!constrained || pic.blk_at(xn, yn).flags & F_INTRA != 0) };
|pic: &PicState<S>, xn: i32, yn: i32| -> bool { pic.zavail(xl, yl, xn, yn, addr) && (!constrained || pic.blk_at(xn, yn).flags & F_INTRA != 0) };
let plane = &self.pic.planes[c];
let r = &mut self.refs_buf;
// corner
if avail(self.pic, xl - 1, yl - 1) {
al[0] = true;
at[0] = true;
let v = plane[(y0 - 1) as usize * stride + (x0 - 1) as usize];
let v = plane[(y0 - 1) as usize * stride + (x0 - 1) as usize].to_u16();
r.left[0] = v;
r.top[0] = v;
}
@@ -1014,7 +1014,7 @@ impl<'a> SliceDecoder<'a> {
if avail(self.pic, xl - 1, yn * sc) {
for i in 0..unit {
al[1 + k + i] = true;
r.left[1 + k + i] = plane[(yn as usize + i) * stride + (x0 - 1) as usize];
r.left[1 + k + i] = plane[(yn as usize + i) * stride + (x0 - 1) as usize].to_u16();
}
}
let xn = x0 + k as i32;
@@ -1022,7 +1022,7 @@ impl<'a> SliceDecoder<'a> {
let o = (y0 - 1) as usize * stride + xn as usize;
for i in 0..unit {
at[1 + k + i] = true;
r.top[1 + k + i] = plane[o + i];
r.top[1 + k + i] = plane[o + i].to_u16();
}
}
k += unit;
@@ -1318,7 +1318,7 @@ impl<'a> SliceDecoder<'a> {
for y in 0..n {
let row = &mut plane[(y0 as usize + y) * stride + x0 as usize..][..n];
for (x, s) in row.iter_mut().enumerate() {
*s = (*s as i32 + coeffs[y * n + x]).clamp(0, max) as u16;
*s = S::from_i32((s.to_i32() + coeffs[y * n + x]).clamp(0, max));
}
}
Ok(())
@@ -1446,11 +1446,17 @@ impl<'a> SliceDecoder<'a> {
SliceType::I => false,
};
let bi = f.pred(0) && f.pred(1);
let pwt = self.sh.pwt.as_ref().filter(|_| weighted);
for c in 0..3 {
let (x, y, bw, bh, bd) = if c == 0 { (xp, yp, w, h, self.pic.bd_y) } else { (xp / 2, yp / 2, w / 2, h / 2, self.pic.bd_c) };
// Explicit weights equal to the default ones (weight 2^denom and offset 0 for every
// list used, as signalled for most slices of a weighted-prediction stream) give exactly
// the default prediction: with log2WD = denom + shift1 the weighted sums of 8-265 /
// 8-266 are those of 8-262 / 8-263 multiplied by 2^denom.
let pwt = pwt.filter(|pwt| !default_weights(pwt, f, c));
// Uni-prediction at a full-sample position without weighting: the prediction
// ((s << shift3) + off) >> shift3 (8-228, 8-262) is the reference sample itself.
if !bi && !weighted {
if !bi && pwt.is_none() {
let l = if f.pred(0) { 0 } else { 1 };
let frac = if c == 0 { 3 } else { 7 };
let mv = f.mv[l];
@@ -1472,14 +1478,14 @@ impl<'a> SliceDecoder<'a> {
let frame = &self.refs[l][f.ref_idx[l] as usize].frame;
let buf = if l == 0 || !bi { &mut self.pred0 } else { &mut self.pred1 };
if c == 0 {
inter::mc_luma(frame, x, y, f.mv[l], bw, bh, buf, &mut self.mc);
inter::mc_luma::<S>(frame, x, y, f.mv[l], bw, bh, buf, &mut self.mc);
} else {
inter::mc_chroma(frame, c - 1, x, y, f.mv[l], bw, bh, buf, &mut self.mc);
inter::mc_chroma::<S>(frame, c - 1, x, y, f.mv[l], bw, bh, buf, &mut self.mc);
}
}
let stride = if c == 0 { self.pic.width } else { self.pic.cwidth };
let dst = &mut self.pic.planes[c][y as usize * stride + x as usize..];
let Some(pwt) = self.sh.pwt.as_ref().filter(|_| weighted) else {
let Some(pwt) = pwt else {
if bi {
inter::put_bi(&self.pred0, &self.pred1, bw, bh, bd, dst, stride);
} else {
@@ -1506,3 +1512,15 @@ impl<'a> SliceDecoder<'a> {
}
}
}
/// Whether the explicit weights of the lists `f` predicts from are the default ones for component
/// `c` (weight 2^denom, offset 0).
fn default_weights(pwt: &crate::slice::PredWeightTable, f: &MvField, c: usize) -> bool {
let denom = if c == 0 { pwt.luma_log2_denom } else { pwt.chroma_log2_denom };
(0..2).filter(|&l| f.pred(l)).all(|l| {
pwt.l[l].get(f.ref_idx[l] as usize).is_some_and(|e| {
let (wt, o) = if c == 0 { e.luma } else { e.chroma[c - 1] };
wt == 1 << denom && o == 0
})
})
}