mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-24 10:10:05 -06:00
M4.10: VP9 frame threading (band-published references, per-frame post stage) and draft mode
Frames are published one superblock row at a time once loop filtered; a post stage per frame (own thread) assembles bands from the tile strips, filters them in raster order and builds the output picture, while the next frame's tile columns decode and wait per band for the reference rows they read. Bit-exact with 1, 3 and all threads. Draft mode skips the loop filter of frames that refresh no reference slot. av1dec / hevcdec bench knobs and VP9 / AV1 4K playback scenarios in the bench. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a814ff632a
commit
842801de41
@@ -0,0 +1,52 @@
|
||||
//! Decode an IVF AV1 file: `cargo run --release -p filmcraft-av1 --example av1dec -- in.ivf [out.yuv]`.
|
||||
//! Writes raw planar 16-bit little-endian output. `AV1_THREADS=n` sets the thread count,
|
||||
//! `AV1_LOOPS=k` decodes the file k times (benchmarking), `AV1_STATS=1` prints stage statistics.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() < 2 {
|
||||
eprintln!("usage: av1dec in.ivf [out.yuv]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let data = std::fs::read(&args[1]).expect("read input");
|
||||
let mut out = args.get(2).map(|p| std::io::BufWriter::new(std::fs::File::create(p).expect("create output")));
|
||||
let threads = std::env::var("AV1_THREADS").ok().and_then(|s| s.parse().ok()).unwrap_or(0usize);
|
||||
let loops = std::env::var("AV1_LOOPS").ok().and_then(|s| s.parse().ok()).unwrap_or(1usize);
|
||||
let hl = u16::from_le_bytes([data[6], data[7]]) as usize;
|
||||
let t0 = std::time::Instant::now();
|
||||
let mut n = 0;
|
||||
let mut dec = if threads > 0 { filmcraft_av1::Decoder::with_threads(threads) } else { filmcraft_av1::Decoder::new() };
|
||||
let mut write = |pics: Vec<filmcraft_av1::Picture>| {
|
||||
for pic in pics {
|
||||
n += 1;
|
||||
if let Some(o) = out.as_mut() {
|
||||
for pl in &pic.planes {
|
||||
let bytes: Vec<u8> = pl.iter().flat_map(|s| s.to_le_bytes()).collect();
|
||||
o.write_all(&bytes).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
for _ in 0..loops {
|
||||
let mut p = hl;
|
||||
let mut i = 0i64;
|
||||
while p + 12 <= data.len() {
|
||||
let sz = u32::from_le_bytes(data[p..p + 4].try_into().unwrap()) as usize;
|
||||
p += 12;
|
||||
let Some(tu) = data.get(p..p + sz) else { break };
|
||||
p += sz;
|
||||
match dec.decode_pts(tu, i) {
|
||||
Ok(pics) => write(pics),
|
||||
Err(e) => eprintln!("temporal unit {i}: {e}"),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
write(dec.flush());
|
||||
}
|
||||
eprintln!("{n} pictures in {:.3}s", t0.elapsed().as_secs_f64());
|
||||
if std::env::var_os("AV1_STATS").is_some() {
|
||||
eprintln!("{:#?}", dec.stats());
|
||||
}
|
||||
}
|
||||
@@ -398,7 +398,7 @@ impl Vp9Decoder {
|
||||
if p.color.full_range {
|
||||
color.range = Range::Full;
|
||||
}
|
||||
let pts = p.pts;
|
||||
let (pts, draft) = (p.pts, p.draft);
|
||||
if p.color.color_space == 7 {
|
||||
// RGB (profiles 1 / 3, 4:4:4): the planes carry G, B, R.
|
||||
let shift = p.bit_depth - 8;
|
||||
@@ -408,7 +408,7 @@ impl Vp9Decoder {
|
||||
}
|
||||
let mut frame = VideoFrame::rgba8(p.width, p.height, rgba);
|
||||
frame.color = filmcraft_color::ColorInfo { range: Range::Full, transfer: filmcraft_color::Transfer::Srgb, ..color };
|
||||
return DecodedFrame { pts, frame, draft: false };
|
||||
return DecodedFrame { pts, frame, draft };
|
||||
}
|
||||
// 4:4:0 has no frame format of its own: chroma rows are repeated to 4:4:4.
|
||||
let (chroma, rows_440) = match (p.subsampling_x, p.subsampling_y) {
|
||||
@@ -445,7 +445,7 @@ impl Vp9Decoder {
|
||||
// render_size (the intended display size) is not applied: the container's display
|
||||
// dimensions / pixel aspect describe the presentation.
|
||||
let par = (1, 1);
|
||||
DecodedFrame { pts, frame: VideoFrame { width: p.width, height: p.height, data, color, par, pts: filmcraft_time::Tick::ZERO }, draft: false }
|
||||
DecodedFrame { pts, frame: VideoFrame { width: p.width, height: p.height, data, color, par, pts: filmcraft_time::Tick::ZERO }, draft }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,6 +463,9 @@ impl VideoDecoder for Vp9Decoder {
|
||||
fn name(&self) -> &str {
|
||||
"FilmCraft VP9"
|
||||
}
|
||||
fn set_draft(&mut self, on: bool) {
|
||||
self.dec.set_draft(on);
|
||||
}
|
||||
fn is_random_access(&self, sample: &[u8]) -> Option<bool> {
|
||||
Some(filmcraft_vp9::is_keyframe(sample))
|
||||
}
|
||||
|
||||
@@ -11,9 +11,15 @@ fn main() {
|
||||
}
|
||||
let data = std::fs::read(&args[1]).expect("read input");
|
||||
let t0 = std::time::Instant::now();
|
||||
let mut dec = Decoder::new();
|
||||
let mut pics = dec.decode(&data, 0).expect("decode");
|
||||
pics.extend(dec.flush());
|
||||
// HEVC_THREADS=n sets the thread count, HEVC_LOOPS=k decodes the file k times (benchmarking).
|
||||
let threads = std::env::var("HEVC_THREADS").ok().and_then(|s| s.parse().ok()).unwrap_or(0usize);
|
||||
let loops = std::env::var("HEVC_LOOPS").ok().and_then(|s| s.parse().ok()).unwrap_or(1usize);
|
||||
let mut pics = Vec::new();
|
||||
for _ in 0..loops {
|
||||
let mut dec = if threads > 0 { Decoder::with_threads(threads) } else { Decoder::new() };
|
||||
pics = dec.decode(&data, 0).expect("decode");
|
||||
pics.extend(dec.flush());
|
||||
}
|
||||
let secs = t0.elapsed().as_secs_f64();
|
||||
eprintln!("{} pictures in {:.3}s ({:.1} fps)", pics.len(), secs, pics.len() as f64 / secs);
|
||||
if let Some(out) = args.get(2) {
|
||||
|
||||
@@ -41,6 +41,8 @@ const SPECS: &[Spec] = &[
|
||||
Spec { name: "c1080.mp4", src: "smptehdbars=s=1920x1080:r=24000/1001:d=20,noise=alls=10:allf=t+u", secs: 20, audio: false, codec: X264 },
|
||||
Spec { name: "a2160.mp4", src: "testsrc2=s=3840x2160:r=24000/1001:d=10,noise=alls=6:allf=t", secs: 10, audio: false, codec: X264 },
|
||||
Spec { name: "hevc2160.mp4", src: "testsrc2=s=3840x2160:r=24000/1001:d=10,noise=alls=6:allf=t", secs: 10, audio: false, codec: X265 },
|
||||
Spec { name: "vp92160.webm", src: "testsrc2=s=3840x2160:r=24000/1001:d=10,noise=alls=6:allf=t", secs: 10, audio: false, codec: VP9 },
|
||||
Spec { name: "av12160.mp4", src: "testsrc2=s=3840x2160:r=24000/1001:d=10,noise=alls=6:allf=t", secs: 10, audio: false, codec: AV1 },
|
||||
// Decode-speed matrix: 1080p (5 s) and 2160p (3 s) per codec, same picture content (ProRes
|
||||
// 2 s / 1 s: intra-only, every frame costs the same, and the files are large).
|
||||
Spec { name: "dec_h264_1080.mp4", src: "testsrc2=s=1920x1080:r=24000/1001:d=5,noise=alls=6:allf=t", secs: 5, audio: false, codec: X264 },
|
||||
|
||||
@@ -626,7 +626,7 @@ fn parse_args() -> Args {
|
||||
"--json" => a.json = it.next(),
|
||||
"--help" | "-h" => {
|
||||
println!(
|
||||
"bench_playback [--scenario h264-1080,stack3,h264-2160,hevc-2160,demo,after-preview,seek-storm] [--res full,half] [--cpu|--gpu] [--seconds 8] [--refresh 60] [--workers N] [--repeat N] [--json out.json]"
|
||||
"bench_playback [--scenario h264-1080,stack3,h264-2160,hevc-2160,vp9-2160,av1-2160,demo,after-preview,seek-storm] [--res full,half] [--cpu|--gpu] [--seconds 8] [--refresh 60] [--workers N] [--repeat N] [--json out.json]"
|
||||
);
|
||||
std::process::exit(0);
|
||||
}
|
||||
@@ -667,6 +667,14 @@ pub fn scenario_session(name: &str) -> Option<(Session, ItemId)> {
|
||||
let a = import(&mut s, &fixture("hevc2160.mp4")?);
|
||||
build_sequence(&mut s, 3840, 2160, &[(a, 100.0, None, 0.0, 100.0)], &[], 10.0)
|
||||
}
|
||||
"vp9-2160" => {
|
||||
let a = import(&mut s, &fixture("vp92160.webm")?);
|
||||
build_sequence(&mut s, 3840, 2160, &[(a, 100.0, None, 0.0, 100.0)], &[], 10.0)
|
||||
}
|
||||
"av1-2160" => {
|
||||
let a = import(&mut s, &fixture("av12160.mp4")?);
|
||||
build_sequence(&mut s, 3840, 2160, &[(a, 100.0, None, 0.0, 100.0)], &[], 10.0)
|
||||
}
|
||||
"after-preview" => {
|
||||
let a = import(&mut s, &fixture("a1080.mp4")?);
|
||||
let seq = build_sequence(&mut s, 1920, 1080, &[(a, 100.0, None, 0.0, 100.0)], &["lumetri", "sharpen", "levels", "tint"], 20.0);
|
||||
|
||||
@@ -167,6 +167,13 @@ pub fn playback(o: &Opts) -> Vec<Value> {
|
||||
("h264-2160", "quarter draft", 0.25, true),
|
||||
("hevc-2160", "full", 1.0, false),
|
||||
("hevc-2160", "half", 0.5, false),
|
||||
("hevc-2160", "half draft", 0.5, true),
|
||||
("vp9-2160", "full", 1.0, false),
|
||||
("vp9-2160", "half", 0.5, false),
|
||||
("vp9-2160", "half draft", 0.5, true),
|
||||
("av1-2160", "full", 1.0, false),
|
||||
("av1-2160", "half", 0.5, false),
|
||||
("av1-2160", "half draft", 0.5, true),
|
||||
];
|
||||
let mut rows = Vec::new();
|
||||
for &(scenario, res, scale, draft) in cases.iter().filter(|c| o.wants(&format!("{} {}", c.0, c.1))) {
|
||||
|
||||
@@ -9,6 +9,8 @@ fn main() {
|
||||
let mut out = args.get(2).map(|p| std::io::BufWriter::new(std::fs::File::create(p).expect("create output")));
|
||||
let threads = std::env::var("VP9_THREADS").ok().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
let mut dec = if threads > 0 { filmcraft_vp9::Decoder::with_threads(threads) } else { filmcraft_vp9::Decoder::new() };
|
||||
// VP9_DRAFT=1: draft mode (non-reference frames skip the loop filter).
|
||||
dec.set_draft(std::env::var_os("VP9_DRAFT").is_some());
|
||||
// VP9_LOOPS=n decodes the file n times (for profiling; the stream starts with a key frame).
|
||||
let loops = std::env::var("VP9_LOOPS").ok().and_then(|s| s.parse().ok()).unwrap_or(1usize);
|
||||
let hl = u16::from_le_bytes([data[6], data[7]]) as usize;
|
||||
@@ -24,7 +26,7 @@ fn main() {
|
||||
p += sz;
|
||||
match dec.decode(frame, i) {
|
||||
Ok(pics) => {
|
||||
for pic in pics {
|
||||
for pic in pics.into_iter().chain(if p >= data.len() { dec.flush() } else { Vec::new() }) {
|
||||
n += 1;
|
||||
if let Some(o) = out.as_mut() {
|
||||
for pl in [&pic.y, &pic.u, &pic.v] {
|
||||
|
||||
+383
-303
@@ -1,15 +1,17 @@
|
||||
//! Top-level decoder: superframes, frame header handling, tile scheduling (tile columns in
|
||||
//! parallel), frame assembly, loop filter (superblock wavefront in parallel), probability
|
||||
//! adaptation and reference frame management.
|
||||
//! parallel), probability adaptation, reference frame management, and the per-frame "post" stage
|
||||
//! (band assembly, loop filter, publication, output picture) that runs behind the next frame
|
||||
//! (frame threading).
|
||||
|
||||
use crate::error::{Error, Result, ensure};
|
||||
use crate::frame::{Frame, MiGrid, MiInfo, Plane as FPlane, Pools, plane_geometry};
|
||||
use crate::frame::{BAND_PAD, Band, Frame, FrameInfo, MiGrid, MiInfo, Pools};
|
||||
use crate::header::{FrameHeader, HeaderState, KEY_FRAME, RefInfo, parse_compressed, parse_uncompressed, split_superframe};
|
||||
use crate::loopfilter::{LfFrame, PlaneView, filter_superblock};
|
||||
use crate::probs::{Counts, adapt_coef_probs, adapt_noncoef_probs};
|
||||
use crate::tables::*;
|
||||
use crate::tile::{FrameShared, RefUse, Strip, TileDecoder};
|
||||
use crate::{ColorInfo, Picture, Plane};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Counters describing what the decoder has seen (useful to check test coverage).
|
||||
@@ -40,6 +42,18 @@ pub struct DecodeStats {
|
||||
pub compound_frames: u64,
|
||||
pub bit_depths: [u64; 3],
|
||||
pub profiles: [u64; 4],
|
||||
/// Frames decoded in draft mode without the loop filter ([`Decoder::set_draft`]).
|
||||
pub draft_frames: u64,
|
||||
}
|
||||
|
||||
/// A picture on its way out, in output order.
|
||||
enum Pending {
|
||||
Ready(Picture),
|
||||
/// Produced by a post job running on another thread.
|
||||
#[cfg(all(feature = "threads", not(target_arch = "wasm32")))]
|
||||
Job(std::sync::mpsc::Receiver<Picture>),
|
||||
/// show_existing_frame: converted once the frame is complete.
|
||||
Existing(Arc<Frame>, i64, bool),
|
||||
}
|
||||
|
||||
/// VP9 decoder.
|
||||
@@ -52,7 +66,11 @@ pub struct Decoder {
|
||||
last_show_frame: bool,
|
||||
stats: DecodeStats,
|
||||
threads: usize,
|
||||
bufs: Pools,
|
||||
bufs: Arc<Pools>,
|
||||
draft: bool,
|
||||
pending: VecDeque<Pending>,
|
||||
#[cfg(all(feature = "threads", not(target_arch = "wasm32")))]
|
||||
jobs: VecDeque<std::thread::JoinHandle<()>>,
|
||||
#[cfg(feature = "threads")]
|
||||
pool: Option<rayon::ThreadPool>,
|
||||
}
|
||||
@@ -86,7 +104,8 @@ impl Decoder {
|
||||
Self::with_threads(default_threads())
|
||||
}
|
||||
|
||||
/// A decoder using up to `threads` worker threads (1 = decode on the calling thread).
|
||||
/// A decoder using up to `threads` worker threads (1 = decode on the calling thread, and
|
||||
/// every picture is returned by the `decode` call that completes it).
|
||||
pub fn with_threads(threads: usize) -> Self {
|
||||
#[cfg(feature = "threads")]
|
||||
let pool = if threads > 1 && cfg!(not(target_arch = "wasm32")) {
|
||||
@@ -103,7 +122,11 @@ impl Decoder {
|
||||
last_show_frame: false,
|
||||
stats: DecodeStats::default(),
|
||||
threads: threads.max(1),
|
||||
bufs: Pools::default(),
|
||||
bufs: Arc::default(),
|
||||
draft: false,
|
||||
pending: VecDeque::new(),
|
||||
#[cfg(all(feature = "threads", not(target_arch = "wasm32")))]
|
||||
jobs: VecDeque::new(),
|
||||
#[cfg(feature = "threads")]
|
||||
pool,
|
||||
}
|
||||
@@ -114,32 +137,49 @@ impl Decoder {
|
||||
self.stats.clone()
|
||||
}
|
||||
|
||||
/// Worker threads in use (1: everything runs on the calling thread).
|
||||
pub fn threads(&self) -> usize {
|
||||
if self.frame_threads() { self.threads } else { 1 }
|
||||
}
|
||||
|
||||
/// Draft mode for reduced-resolution playback (off by default): frames that no other frame
|
||||
/// can reference (`refresh_frame_flags` 0) skip the loop filter and come out flagged
|
||||
/// [`Picture::draft`]. Every other picture is unchanged: the loop filter changes neither the
|
||||
/// mode info, segmentation map nor probabilities that later frames read.
|
||||
pub fn set_draft(&mut self, on: bool) {
|
||||
self.draft = on;
|
||||
}
|
||||
|
||||
/// Decode one chunk (a frame or a superframe, as stored in one IVF / WebM / MP4 sample).
|
||||
/// Returns the frames to show, in order; each carries `pts`.
|
||||
/// Returns the pictures finished so far, in order; each carries the `pts` of the chunk that
|
||||
/// showed it. With frame threads a picture may come out of a later call or [`Self::flush`].
|
||||
pub fn decode(&mut self, data: &[u8], pts: i64) -> Result<Vec<Picture>> {
|
||||
let frames = split_superframe(data);
|
||||
if frames.len() > 1 {
|
||||
self.stats.superframes += 1;
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for f in frames {
|
||||
if f.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(frame) = self.decode_frame(f)? {
|
||||
out.push(make_picture(&frame, pts, self.par()));
|
||||
}
|
||||
self.decode_frame(f, pts)?;
|
||||
}
|
||||
Ok(out)
|
||||
Ok(self.drain(false))
|
||||
}
|
||||
|
||||
/// End of stream. VP9 has no reordering delay, so this always returns no pictures.
|
||||
/// End of stream: every picture still in flight.
|
||||
pub fn flush(&mut self) -> Vec<Picture> {
|
||||
Vec::new()
|
||||
let out = self.drain(true);
|
||||
#[cfg(all(feature = "threads", not(target_arch = "wasm32")))]
|
||||
for j in self.jobs.drain(..) {
|
||||
let _ = j.join();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Forget all decoding state (references, probability contexts, segmentation), e.g. before
|
||||
/// decoding from another key frame after a seek. The thread pool and statistics are kept.
|
||||
/// Forget all decoding state (references, probability contexts, segmentation) and pictures
|
||||
/// in flight, e.g. before decoding from another key frame after a seek. The thread pool and
|
||||
/// statistics are kept.
|
||||
pub fn reset(&mut self) {
|
||||
self.st = HeaderState::default();
|
||||
self.slots = Default::default();
|
||||
@@ -147,10 +187,63 @@ impl Decoder {
|
||||
self.prev_seg_ids = Vec::new();
|
||||
self.last_size = None;
|
||||
self.last_show_frame = false;
|
||||
self.pending.clear();
|
||||
}
|
||||
|
||||
fn decode_frame(&mut self, data: &[u8]) -> Result<Option<Arc<Frame>>> {
|
||||
let refs: [Option<RefInfo>; 8] = std::array::from_fn(|i| self.slots[i].as_ref().map(|f| RefInfo { width: f.width, height: f.height }));
|
||||
fn frame_threads(&self) -> bool {
|
||||
#[cfg(all(feature = "threads", not(target_arch = "wasm32")))]
|
||||
{
|
||||
self.pool.is_some()
|
||||
}
|
||||
#[cfg(not(all(feature = "threads", not(target_arch = "wasm32"))))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Pictures at the front of the queue that are finished (`all`: wait for every one). The
|
||||
/// queue is kept short: beyond a few pictures in flight the oldest is waited for.
|
||||
fn drain(&mut self, all: bool) -> Vec<Picture> {
|
||||
let max_lag = (self.threads / 2).clamp(2, 6);
|
||||
let mut out = Vec::new();
|
||||
loop {
|
||||
let must = all || self.pending.len() > max_lag;
|
||||
let Some(front) = self.pending.front_mut() else { break };
|
||||
let pic = match front {
|
||||
Pending::Ready(_) => match self.pending.pop_front() {
|
||||
Some(Pending::Ready(p)) => Some(p),
|
||||
_ => None,
|
||||
},
|
||||
#[cfg(all(feature = "threads", not(target_arch = "wasm32")))]
|
||||
Pending::Job(rx) => {
|
||||
let p = if must {
|
||||
rx.recv().ok()
|
||||
} else {
|
||||
match rx.try_recv() {
|
||||
Ok(p) => Some(p),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => break,
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => None,
|
||||
}
|
||||
};
|
||||
self.pending.pop_front();
|
||||
p
|
||||
}
|
||||
Pending::Existing(f, pts, draft) => {
|
||||
if !must && !f.is_complete() {
|
||||
break;
|
||||
}
|
||||
let p = picture_of(f, *pts, *draft);
|
||||
self.pending.pop_front();
|
||||
Some(p)
|
||||
}
|
||||
};
|
||||
out.extend(pic);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn decode_frame(&mut self, data: &[u8], pts: i64) -> Result<()> {
|
||||
let refs: [Option<RefInfo>; 8] = std::array::from_fn(|i| self.slots[i].as_ref().map(|f| RefInfo { width: f.info.width, height: f.info.height }));
|
||||
let mut h = parse_uncompressed(data, &mut self.st, &refs)?;
|
||||
if h.show_existing_frame {
|
||||
self.stats.show_existing += 1;
|
||||
@@ -158,7 +251,8 @@ impl Decoder {
|
||||
.clone()
|
||||
.ok_or_else(|| Error::MissingReference(format!("show_existing_frame of empty slot {}", h.frame_to_show_map_idx)))?;
|
||||
self.stats.shown += 1;
|
||||
return Ok(Some(f));
|
||||
self.pending.push_back(Pending::Existing(f, pts, false));
|
||||
return Ok(());
|
||||
}
|
||||
ensure!(h.width <= 16384 && h.height <= 16384, "frame size {}x{} too large", h.width, h.height);
|
||||
let (mi_rows, mi_cols) = (h.mi_rows as usize, h.mi_cols as usize);
|
||||
@@ -196,11 +290,12 @@ impl Decoder {
|
||||
let Some(rf) = &ref_frames[i] else {
|
||||
return Err(Error::MissingReference(format!("reference slot {} is empty", h.ref_frame_idx[i])));
|
||||
};
|
||||
let ri = &rf.info;
|
||||
ensure!(
|
||||
rf.bit_depth == h.color.bit_depth && rf.ss_x == h.color.subsampling_x && rf.ss_y == h.color.subsampling_y,
|
||||
ri.bit_depth == h.color.bit_depth && ri.ss_x == h.color.subsampling_x && ri.ss_y == h.color.subsampling_y,
|
||||
"reference frame format differs from the current frame"
|
||||
);
|
||||
let (rw, rh) = (rf.width as i64, rf.height as i64);
|
||||
let (rw, rh) = (ri.width as i64, ri.height as i64);
|
||||
let (w, hh) = (h.width as i64, h.height as i64);
|
||||
if 2 * w >= rw && 2 * hh >= rh && w <= 16 * rw && hh <= 16 * rh {
|
||||
let x_scale = ((rw << 14) / w) as i32;
|
||||
@@ -272,9 +367,10 @@ impl Decoder {
|
||||
let results: Vec<(Strip, Option<Error>)> = self.run_parallel(tile_cols, &decode_col);
|
||||
let mut counts = Counts::default();
|
||||
let mut strips = Vec::with_capacity(results.len());
|
||||
let mut error = None;
|
||||
for (s, e) in results {
|
||||
if let Some(e) = e {
|
||||
return Err(e);
|
||||
error.get_or_insert(e);
|
||||
}
|
||||
if counting {
|
||||
counts.add(&s.counts);
|
||||
@@ -285,32 +381,39 @@ impl Decoder {
|
||||
self.stats.inter_blocks += s.inter_blocks;
|
||||
strips.push(s);
|
||||
}
|
||||
if let Some(e) = error {
|
||||
recycle_strips(strips, &self.bufs);
|
||||
return Err(e);
|
||||
}
|
||||
// Release the references before the slots are refreshed, so that evicted frames can be
|
||||
// recycled.
|
||||
drop(ref_frames);
|
||||
drop(prev_mi);
|
||||
// Assemble the frame and mode info.
|
||||
let geo = plane_geometry(h.width, h.height, h.color.subsampling_x, h.color.subsampling_y);
|
||||
let (planes, mi, seg_ids) = assemble(strips, &geo, mi_rows, mi_cols, &self.bufs, self.par());
|
||||
let mut frame = Frame {
|
||||
planes,
|
||||
width: h.width,
|
||||
height: h.height,
|
||||
ss_x: h.color.subsampling_x,
|
||||
ss_y: h.color.subsampling_y,
|
||||
bit_depth: h.color.bit_depth,
|
||||
color_space: h.color.color_space,
|
||||
color_range: h.color.color_range,
|
||||
render_width: h.render_width,
|
||||
render_height: h.render_height,
|
||||
key: h.frame_type == KEY_FRAME,
|
||||
intra_only: h.intra_only,
|
||||
};
|
||||
// Loop filter (8.8).
|
||||
if self.st.lf.level > 0 {
|
||||
let lf = LfFrame::new(&self.st.lf, &seg, mi_rows, mi_cols, h.color.subsampling_x, h.color.subsampling_y, h.color.bit_depth);
|
||||
self.loop_filter(&mut frame, &mi, &lf);
|
||||
}
|
||||
let (mi, seg_ids) = assemble_mi(&mut strips, mi_rows, mi_cols, &self.bufs);
|
||||
let mi = Arc::new(mi);
|
||||
let frame = Arc::new(Frame::new(
|
||||
FrameInfo {
|
||||
width: h.width,
|
||||
height: h.height,
|
||||
ss_x: h.color.subsampling_x,
|
||||
ss_y: h.color.subsampling_y,
|
||||
bit_depth: h.color.bit_depth,
|
||||
color_space: h.color.color_space,
|
||||
color_range: h.color.color_range,
|
||||
render_width: h.render_width,
|
||||
render_height: h.render_height,
|
||||
key: h.frame_type == KEY_FRAME,
|
||||
intra_only: h.intra_only,
|
||||
},
|
||||
self.bufs.clone(),
|
||||
));
|
||||
// Loop filter (8.8), skipped in draft mode for frames nothing can reference.
|
||||
let draft = self.draft && h.refresh_frame_flags == 0 && h.show_frame && self.st.lf.level > 0;
|
||||
self.stats.draft_frames += draft as u64;
|
||||
let lf = (self.st.lf.level > 0 && !draft)
|
||||
.then(|| LfFrame::new(&self.st.lf, &seg, mi_rows, mi_cols, h.color.subsampling_x, h.color.subsampling_y, h.color.bit_depth));
|
||||
let job = PostJob { frame: frame.clone(), strips, mi: mi.clone(), lf, out: h.show_frame.then_some((pts, draft)), pools: self.bufs.clone() };
|
||||
self.run_post(job);
|
||||
// refresh_probs (6.1.2).
|
||||
if counting {
|
||||
let saved = &self.st.contexts[h.frame_context_idx as usize];
|
||||
@@ -331,32 +434,61 @@ impl Decoder {
|
||||
if seg.enabled && seg.update_map {
|
||||
self.prev_seg_ids = seg_ids;
|
||||
}
|
||||
let frame = Arc::new(frame);
|
||||
let mut evicted = Vec::new();
|
||||
for i in 0..8 {
|
||||
if (h.refresh_frame_flags >> i) & 1 == 1 {
|
||||
evicted.extend(self.slots[i].replace(frame.clone()));
|
||||
self.slots[i] = Some(frame.clone());
|
||||
}
|
||||
}
|
||||
// Recycle the planes of frames no longer referenced anywhere.
|
||||
for f in evicted {
|
||||
if let Ok(f) = Arc::try_unwrap(f) {
|
||||
for p in f.planes {
|
||||
self.bufs.samples.put(p.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(old) = self.prev_mi.replace(Arc::new(mi))
|
||||
if let Some(old) = self.prev_mi.replace(mi)
|
||||
&& let Ok(old) = Arc::try_unwrap(old)
|
||||
{
|
||||
self.bufs.mi.put(old.mi);
|
||||
}
|
||||
if h.show_frame {
|
||||
self.stats.shown += 1;
|
||||
Ok(Some(frame))
|
||||
} else {
|
||||
self.stats.hidden += 1;
|
||||
Ok(None)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the post stage of a frame: on its own thread with frame threads (later frames wait
|
||||
/// per band for the samples they read), else right here.
|
||||
fn run_post(&mut self, job: PostJob) {
|
||||
#[cfg(all(feature = "threads", not(target_arch = "wasm32")))]
|
||||
if self.frame_threads() {
|
||||
// Bound the post stages in flight (each is one thread).
|
||||
while self.jobs.front().is_some_and(|j| j.is_finished()) {
|
||||
let _ = self.jobs.pop_front().map(|j| j.join());
|
||||
}
|
||||
let max_jobs = (self.threads / 3).clamp(2, 6);
|
||||
while self.jobs.len() >= max_jobs {
|
||||
let _ = self.jobs.pop_front().map(|j| j.join());
|
||||
}
|
||||
let shown = job.out.is_some();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let spawned = std::thread::Builder::new().name("vp9-post".into()).spawn(move || {
|
||||
if let Some(p) = job.run() {
|
||||
let _ = tx.send(p);
|
||||
}
|
||||
});
|
||||
match spawned {
|
||||
Ok(handle) => {
|
||||
self.jobs.push_back(handle);
|
||||
if shown {
|
||||
self.pending.push_back(Pending::Job(rx));
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
// No thread available: the job (moved into the failed closure) is gone,
|
||||
// which cannot happen in practice; report as a decoding failure.
|
||||
panic!("cannot spawn VP9 post-processing thread: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(p) = job.run() {
|
||||
self.pending.push_back(Pending::Ready(p));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,17 +516,6 @@ impl Decoder {
|
||||
s.profiles[h.profile as usize] += 1;
|
||||
}
|
||||
|
||||
fn par(&self) -> Par<'_> {
|
||||
#[cfg(feature = "threads")]
|
||||
{
|
||||
Par(self.pool.as_ref())
|
||||
}
|
||||
#[cfg(not(feature = "threads"))]
|
||||
{
|
||||
Par(std::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `f(0..n)`, in parallel when a pool is available.
|
||||
fn run_parallel<T: Send>(&self, n: usize, f: &(dyn Fn(usize) -> T + Sync)) -> Vec<T> {
|
||||
#[cfg(feature = "threads")]
|
||||
@@ -406,253 +527,212 @@ impl Decoder {
|
||||
}
|
||||
(0..n).map(f).collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn loop_filter(&self, frame: &mut Frame, mi: &MiGrid, lf: &LfFrame) {
|
||||
let sb_rows = lf.mi_rows.div_ceil(8);
|
||||
let sb_cols = lf.mi_cols.div_ceil(8);
|
||||
#[cfg(feature = "threads")]
|
||||
if let Some(pool) = &self.pool
|
||||
&& sb_rows > 1
|
||||
{
|
||||
lf_parallel(pool, self.threads, frame, mi, lf, sb_rows, sb_cols);
|
||||
return;
|
||||
}
|
||||
let _ = self.threads;
|
||||
let [p0, p1, p2] = &mut frame.planes;
|
||||
let mut views = [
|
||||
PlaneView { stride: p0.stride, data: &mut p0.data, ox: 0, oy: 0 },
|
||||
PlaneView { stride: p1.stride, data: &mut p1.data, ox: 0, oy: 0 },
|
||||
PlaneView { stride: p2.stride, data: &mut p2.data, ox: 0, oy: 0 },
|
||||
];
|
||||
for r in 0..sb_rows {
|
||||
for c in 0..sb_cols {
|
||||
filter_superblock(&mut views, mi, lf, r * 8, c * 8);
|
||||
}
|
||||
impl Drop for Decoder {
|
||||
fn drop(&mut self) {
|
||||
// Post jobs own everything they use; let them finish rather than leaving threads behind.
|
||||
#[cfg(all(feature = "threads", not(target_arch = "wasm32")))]
|
||||
for j in self.jobs.drain(..) {
|
||||
let _ = j.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loop filter with a superblock wavefront: superblock (r, c) runs once (r - 1, c + 1) is done.
|
||||
/// The planes are split into superblock-row bands, each behind its own lock. Each superblock
|
||||
/// copies the samples it may touch (its area plus 8 samples above / left, i.e. the bottom rows
|
||||
/// of band r - 1 and its own band) out, filters them locally and writes them back; the locks are
|
||||
/// only held while copying, and only neighbouring rows ever contend.
|
||||
#[cfg(feature = "threads")]
|
||||
fn lf_parallel(pool: &rayon::ThreadPool, threads: usize, frame: &mut Frame, mi: &MiGrid, lf: &LfFrame, sb_rows: usize, sb_cols: usize) {
|
||||
use rayon::prelude::*;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Condvar, Mutex};
|
||||
let strides = [frame.planes[0].stride, frame.planes[1].stride, frame.planes[2].stride];
|
||||
let sub = [(0usize, 0usize), (lf.ss_x, lf.ss_y), (lf.ss_x, lf.ss_y)];
|
||||
let band_h = [64usize, 64 >> lf.ss_y, 64 >> lf.ss_y];
|
||||
// bands[r][p]: rows [r * band_h[p], (r + 1) * band_h[p]) of plane p.
|
||||
let bands: Vec<Mutex<[&mut [u16]; 3]>> = {
|
||||
let [a, b, c] = &mut frame.planes;
|
||||
let mut ia = a.data.chunks_mut(band_h[0] * strides[0]);
|
||||
let mut ib = b.data.chunks_mut(band_h[1] * strides[1]);
|
||||
let mut ic = c.data.chunks_mut(band_h[2] * strides[2]);
|
||||
(0..sb_rows).map(|_| Mutex::new([ia.next().unwrap_or_default(), ib.next().unwrap_or_default(), ic.next().unwrap_or_default()])).collect()
|
||||
};
|
||||
let progress = Mutex::new(vec![0usize; sb_rows]);
|
||||
let cv = Condvar::new();
|
||||
let next = AtomicUsize::new(0);
|
||||
// Copy rows [ry, ry + rh) x [rx, rx + rw) of plane p between the bands and `buf` (row-major,
|
||||
// stride rw); `to_buf` selects the direction.
|
||||
let xfer = |r: usize, p: usize, rx: usize, ry: usize, rw: usize, rh: usize, buf: &mut [u16], to_buf: bool| {
|
||||
let bh = band_h[p];
|
||||
let mut yy = 0;
|
||||
while yy < rh {
|
||||
let y = ry + yy;
|
||||
let br = y / bh;
|
||||
let n = ((br + 1) * bh - y).min(rh - yy);
|
||||
debug_assert!(br + 1 >= r && br <= r);
|
||||
let mut g = bands[br].lock().expect("lf band");
|
||||
let band = &mut g[p];
|
||||
for k in 0..n {
|
||||
let s = (y + k - br * bh) * strides[p] + rx;
|
||||
let b = (yy + k) * rw;
|
||||
if to_buf {
|
||||
buf[b..b + rw].copy_from_slice(&band[s..s + rw]);
|
||||
} else {
|
||||
band[s..s + rw].copy_from_slice(&buf[b..b + rw]);
|
||||
}
|
||||
}
|
||||
yy += n;
|
||||
}
|
||||
};
|
||||
pool.install(|| {
|
||||
(0..threads.min(sb_rows)).into_par_iter().for_each(|_| {
|
||||
let mut bufs: [Vec<u16>; 3] = [vec![0; 72 * 72], vec![0; 72 * 72], vec![0; 72 * 72]];
|
||||
loop {
|
||||
let r = next.fetch_add(1, Ordering::Relaxed);
|
||||
if r >= sb_rows {
|
||||
break;
|
||||
}
|
||||
for c in 0..sb_cols {
|
||||
if r > 0 {
|
||||
let need = (c + 2).min(sb_cols);
|
||||
let mut p = progress.lock().expect("lf progress");
|
||||
while p[r - 1] < need {
|
||||
p = cv.wait(p).expect("lf progress");
|
||||
}
|
||||
}
|
||||
// Region of each plane: [x0 - 8, x0 + size) x [y0 - 8, y0 + size).
|
||||
let mut regions = [(0usize, 0usize, 0usize, 0usize); 3];
|
||||
for p in 0..3 {
|
||||
let (sx, sy) = sub[p];
|
||||
let (x0, y0) = ((c * 64) >> sx, (r * 64) >> sy);
|
||||
let (rx, ry) = (x0.saturating_sub(8), y0.saturating_sub(8));
|
||||
let rw = (x0 + (64 >> sx)).min(strides[p]) - rx;
|
||||
let rh = (y0 + band_h[p]) - ry;
|
||||
regions[p] = (rx, ry, rw, rh);
|
||||
xfer(r, p, rx, ry, rw, rh, &mut bufs[p], true);
|
||||
}
|
||||
{
|
||||
let [b0, b1, b2] = &mut bufs;
|
||||
let mut views = [
|
||||
PlaneView { data: b0, stride: regions[0].2, ox: regions[0].0, oy: regions[0].1 },
|
||||
PlaneView { data: b1, stride: regions[1].2, ox: regions[1].0, oy: regions[1].1 },
|
||||
PlaneView { data: b2, stride: regions[2].2, ox: regions[2].0, oy: regions[2].1 },
|
||||
];
|
||||
filter_superblock(&mut views, mi, lf, r * 8, c * 8);
|
||||
}
|
||||
for p in 0..3 {
|
||||
let (rx, ry, rw, rh) = regions[p];
|
||||
xfer(r, p, rx, ry, rw, rh, &mut bufs[p], false);
|
||||
}
|
||||
let mut p = progress.lock().expect("lf progress");
|
||||
p[r] = c + 1;
|
||||
cv.notify_all();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Optional thread pool for data-parallel copies.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Par<'a>(#[cfg(feature = "threads")] Option<&'a rayon::ThreadPool>, #[cfg(not(feature = "threads"))] std::marker::PhantomData<&'a ()>);
|
||||
|
||||
impl Par<'_> {
|
||||
/// `f(i, chunk)` for the consecutive `chunk_len`-element chunks of `data`.
|
||||
fn chunks<T: Send>(self, data: &mut [T], chunk_len: usize, f: impl Fn(usize, &mut [T]) + Sync) {
|
||||
let chunk_len = chunk_len.max(1);
|
||||
#[cfg(feature = "threads")]
|
||||
if let Some(pool) = self.0
|
||||
&& data.len() > chunk_len
|
||||
{
|
||||
use rayon::prelude::*;
|
||||
pool.install(|| data.par_chunks_mut(chunk_len).enumerate().for_each(|(i, c)| f(i, c)));
|
||||
return;
|
||||
}
|
||||
for (i, c) in data.chunks_mut(chunk_len).enumerate() {
|
||||
f(i, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rows per chunk of the parallel frame copies.
|
||||
const COPY_ROWS: usize = 64;
|
||||
|
||||
/// Build full-frame planes, mode info and segment ids from the tile column strips.
|
||||
fn assemble(
|
||||
mut strips: Vec<Strip>,
|
||||
geo: &[(usize, usize, usize, usize); 3],
|
||||
mi_rows: usize,
|
||||
mi_cols: usize,
|
||||
pools: &Pools,
|
||||
par: Par,
|
||||
) -> ([FPlane; 3], MiGrid, Vec<u8>) {
|
||||
if strips.len() == 1 {
|
||||
let s = strips.pop().expect("one strip");
|
||||
let [a, b, c] = s.planes;
|
||||
let mk = |d: Vec<u16>, g: (usize, usize, usize, usize)| FPlane { data: d, stride: g.0, width: g.2, height: g.3 };
|
||||
return ([mk(a, geo[0]), mk(b, geo[1]), mk(c, geo[2])], MiGrid { cols: mi_cols, rows: mi_rows, mi: s.mi }, s.seg_ids);
|
||||
}
|
||||
let mk = |g: (usize, usize, usize, usize)| FPlane { data: pools.samples.take(g.0 * g.1, 0), stride: g.0, width: g.2, height: g.3 };
|
||||
let mut planes = [mk(geo[0]), mk(geo[1]), mk(geo[2])];
|
||||
let mut mi = pools.mi.take(mi_rows * mi_cols, MiInfo::default());
|
||||
let mut seg = vec![0u8; mi_rows * mi_cols];
|
||||
for (p, plane) in planes.iter_mut().enumerate() {
|
||||
let fs = plane.stride;
|
||||
// Each chunk of destination rows gathers its part of every strip.
|
||||
par.chunks(&mut plane.data, COPY_ROWS * fs, |ci, dst| {
|
||||
let y0 = ci * COPY_ROWS;
|
||||
for s in &strips {
|
||||
let ss = s.strides[p];
|
||||
let x0 = s.x_off[p];
|
||||
let w = ss.min(fs - x0);
|
||||
for (k, row) in dst.chunks_mut(fs).enumerate() {
|
||||
let y = y0 + k;
|
||||
if let Some(src) = s.planes[p].get(y * ss..y * ss + w) {
|
||||
row[x0..x0 + w].copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
for s in &strips {
|
||||
let mw = s.mi_col_end - s.mi_col_start;
|
||||
for r in 0..mi_rows {
|
||||
mi[r * mi_cols + s.mi_col_start..r * mi_cols + s.mi_col_end].copy_from_slice(&s.mi[r * mw..r * mw + mw]);
|
||||
seg[r * mi_cols + s.mi_col_start..r * mi_cols + s.mi_col_end].copy_from_slice(&s.seg_ids[r * mw..r * mw + mw]);
|
||||
}
|
||||
}
|
||||
fn recycle_strips(strips: Vec<Strip>, pools: &Pools) {
|
||||
for s in strips {
|
||||
for p in s.planes {
|
||||
pools.samples.put(p);
|
||||
}
|
||||
pools.mi.put(s.mi);
|
||||
}
|
||||
(planes, MiGrid { cols: mi_cols, rows: mi_rows, mi }, seg)
|
||||
}
|
||||
|
||||
fn make_picture(f: &Frame, pts: i64, par: Par) -> Picture {
|
||||
// Crop (and narrow 8-bit samples), in parallel chunks of rows.
|
||||
let conv = |p: &FPlane| -> Plane {
|
||||
let w = p.width;
|
||||
if w == 0 || p.height == 0 {
|
||||
return if f.bit_depth == 8 { Plane::U8(Vec::new()) } else { Plane::U16(Vec::new()) };
|
||||
/// The post stage of one frame: build each band from the tile column strips, loop filter it
|
||||
/// (superblocks in raster order, so the result is the specification's), publish it once the
|
||||
/// next band's filtering can no longer change it, and copy it into the output picture.
|
||||
struct PostJob {
|
||||
frame: Arc<Frame>,
|
||||
strips: Vec<Strip>,
|
||||
mi: Arc<MiGrid>,
|
||||
lf: Option<LfFrame>,
|
||||
/// (pts, draft) when the frame is shown.
|
||||
out: Option<(i64, bool)>,
|
||||
pools: Arc<Pools>,
|
||||
}
|
||||
|
||||
/// Publishes placeholder bands if a post job dies, so that waiting frames cannot hang.
|
||||
struct PublishGuard<'a>(&'a Frame);
|
||||
|
||||
impl Drop for PublishGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
for r in 0..self.0.sb_rows() {
|
||||
if !self.0.is_published(r) {
|
||||
self.0.publish(r, self.0.new_band());
|
||||
}
|
||||
}
|
||||
if f.bit_depth == 8 {
|
||||
let mut out = vec![0u8; w * p.height];
|
||||
par.chunks(&mut out, COPY_ROWS * w, |ci, dst| {
|
||||
for (k, row) in dst.chunks_mut(w).enumerate() {
|
||||
for (d, &s) in row.iter_mut().zip(&p.row(ci * COPY_ROWS + k)[..w]) {
|
||||
*d = s as u8;
|
||||
}
|
||||
}
|
||||
});
|
||||
Plane::U8(out)
|
||||
} else {
|
||||
let mut out = vec![0u16; w * p.height];
|
||||
par.chunks(&mut out, COPY_ROWS * w, |ci, dst| {
|
||||
for (k, row) in dst.chunks_mut(w).enumerate() {
|
||||
row.copy_from_slice(&p.row(ci * COPY_ROWS + k)[..w]);
|
||||
}
|
||||
});
|
||||
Plane::U16(out)
|
||||
}
|
||||
};
|
||||
let [y, u, v] = &f.planes;
|
||||
Picture {
|
||||
width: f.width,
|
||||
height: f.height,
|
||||
chroma_width: u.width as u32,
|
||||
chroma_height: u.height as u32,
|
||||
bit_depth: f.bit_depth as u32,
|
||||
subsampling_x: f.ss_x,
|
||||
subsampling_y: f.ss_y,
|
||||
y: conv(y),
|
||||
u: conv(u),
|
||||
v: conv(v),
|
||||
y_stride: y.width,
|
||||
uv_stride: u.width,
|
||||
pts,
|
||||
key: f.key,
|
||||
intra_only: f.intra_only,
|
||||
color: ColorInfo { color_space: f.color_space, full_range: f.color_range },
|
||||
render_width: f.render_width,
|
||||
render_height: f.render_height,
|
||||
}
|
||||
}
|
||||
|
||||
impl PostJob {
|
||||
fn run(self) -> Option<Picture> {
|
||||
let f = &*self.frame;
|
||||
let _guard = PublishGuard(f);
|
||||
let mut pic = self.out.map(|(pts, draft)| PictureBuilder::new(f, pts, draft));
|
||||
let sb_rows = f.sb_rows();
|
||||
let mut held: Option<Band> = None;
|
||||
for r in 0..sb_rows {
|
||||
let mut band = f.new_band();
|
||||
for p in 0..3 {
|
||||
let (fs, bh) = (f.strides[p], f.band_h[p]);
|
||||
let dst = &mut band.planes[p];
|
||||
for s in &self.strips {
|
||||
let ss = s.strides[p];
|
||||
let x0 = s.x_off[p];
|
||||
let w = ss.min(fs - x0);
|
||||
for k in 0..bh {
|
||||
let y = r * bh + k;
|
||||
let d = (BAND_PAD + k) * fs + x0;
|
||||
dst[d..d + w].copy_from_slice(&s.planes[p][y * ss..y * ss + w]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(lf) = &self.lf {
|
||||
if let Some(prev) = &held {
|
||||
// The bottom rows of the previous band, which this row's filter may change.
|
||||
for p in 0..3 {
|
||||
let (fs, bh) = (f.strides[p], f.band_h[p]);
|
||||
band.planes[p][..BAND_PAD * fs].copy_from_slice(&prev.planes[p][bh * fs..(bh + BAND_PAD) * fs]);
|
||||
}
|
||||
}
|
||||
let [a, b, c] = &mut band.planes;
|
||||
let mut views = [band_view(f, a, 0, r), band_view(f, b, 1, r), band_view(f, c, 2, r)];
|
||||
for c in 0..lf.mi_cols.div_ceil(8) {
|
||||
filter_superblock(&mut views, &self.mi, lf, r * 8, c * 8);
|
||||
}
|
||||
if let Some(prev) = &mut held {
|
||||
for p in 0..3 {
|
||||
let (fs, bh) = (f.strides[p], f.band_h[p]);
|
||||
prev.planes[p][bh * fs..(bh + BAND_PAD) * fs].copy_from_slice(&band.planes[p][..BAND_PAD * fs]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(prev) = held.take() {
|
||||
if let Some(pic) = &mut pic {
|
||||
pic.add(f, r - 1, &prev);
|
||||
}
|
||||
f.publish(r - 1, prev);
|
||||
}
|
||||
held = Some(band);
|
||||
}
|
||||
if let Some(last) = held {
|
||||
if let Some(pic) = &mut pic {
|
||||
pic.add(f, sb_rows - 1, &last);
|
||||
}
|
||||
f.publish(sb_rows - 1, last);
|
||||
}
|
||||
for s in self.strips {
|
||||
for p in s.planes {
|
||||
self.pools.samples.put(p);
|
||||
}
|
||||
}
|
||||
pic.map(PictureBuilder::finish)
|
||||
}
|
||||
}
|
||||
|
||||
/// The loop filter's view of band `r` of plane `p`: the band's rows plus (below row 0) the
|
||||
/// scratch rows holding the bottom of band r - 1.
|
||||
fn band_view<'a>(f: &Frame, d: &'a mut [u16], p: usize, r: usize) -> PlaneView<'a> {
|
||||
let (fs, bh) = (f.strides[p], f.band_h[p]);
|
||||
if r == 0 { PlaneView { data: &mut d[BAND_PAD * fs..], stride: fs, ox: 0, oy: 0 } } else { PlaneView { data: d, stride: fs, ox: 0, oy: r * bh - BAND_PAD } }
|
||||
}
|
||||
|
||||
/// Mode info and segment ids of the whole frame from the tile column strips (whose mode info
|
||||
/// buffers go back to the pool).
|
||||
fn assemble_mi(strips: &mut [Strip], mi_rows: usize, mi_cols: usize, pools: &Pools) -> (MiGrid, Vec<u8>) {
|
||||
if strips.len() == 1 {
|
||||
let s = &mut strips[0];
|
||||
return (MiGrid { cols: mi_cols, rows: mi_rows, mi: std::mem::take(&mut s.mi) }, std::mem::take(&mut s.seg_ids));
|
||||
}
|
||||
let mut mi = pools.mi.take(mi_rows * mi_cols, MiInfo::default());
|
||||
let mut seg = vec![0u8; mi_rows * mi_cols];
|
||||
for s in strips.iter_mut() {
|
||||
let mw = s.mi_col_end - s.mi_col_start;
|
||||
for r in 0..mi_rows {
|
||||
mi[r * mi_cols + s.mi_col_start..r * mi_cols + s.mi_col_end].copy_from_slice(&s.mi[r * mw..r * mw + mw]);
|
||||
seg[r * mi_cols + s.mi_col_start..r * mi_cols + s.mi_col_end].copy_from_slice(&s.seg_ids[r * mw..r * mw + mw]);
|
||||
}
|
||||
pools.mi.put(std::mem::take(&mut s.mi));
|
||||
}
|
||||
(MiGrid { cols: mi_cols, rows: mi_rows, mi }, seg)
|
||||
}
|
||||
|
||||
/// Builds the cropped output picture band by band (narrowing 8-bit samples).
|
||||
struct PictureBuilder {
|
||||
planes: [Plane; 3],
|
||||
info: FrameInfo,
|
||||
vis: [(usize, usize); 3],
|
||||
pts: i64,
|
||||
draft: bool,
|
||||
}
|
||||
|
||||
impl PictureBuilder {
|
||||
fn new(f: &Frame, pts: i64, draft: bool) -> Self {
|
||||
let planes = f.vis.map(|(w, h)| if f.info.bit_depth == 8 { Plane::U8(vec![0; w * h]) } else { Plane::U16(vec![0; w * h]) });
|
||||
PictureBuilder { planes, info: f.info, vis: f.vis, pts, draft }
|
||||
}
|
||||
|
||||
fn add(&mut self, f: &Frame, r: usize, band: &Band) {
|
||||
for p in 0..3 {
|
||||
let (w, h) = f.vis[p];
|
||||
let (fs, bh) = (f.strides[p], f.band_h[p]);
|
||||
let y0 = r * bh;
|
||||
for y in y0..(y0 + bh).min(h) {
|
||||
let src = &band.planes[p][(y - y0 + BAND_PAD) * fs..][..w];
|
||||
match &mut self.planes[p] {
|
||||
Plane::U8(d) => {
|
||||
for (d, &s) in d[y * w..y * w + w].iter_mut().zip(src) {
|
||||
*d = s as u8;
|
||||
}
|
||||
}
|
||||
Plane::U16(d) => d[y * w..y * w + w].copy_from_slice(src),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Picture {
|
||||
let i = self.info;
|
||||
let [y, u, v] = self.planes;
|
||||
Picture {
|
||||
width: i.width,
|
||||
height: i.height,
|
||||
chroma_width: self.vis[1].0 as u32,
|
||||
chroma_height: self.vis[1].1 as u32,
|
||||
bit_depth: i.bit_depth as u32,
|
||||
subsampling_x: i.ss_x,
|
||||
subsampling_y: i.ss_y,
|
||||
y,
|
||||
u,
|
||||
v,
|
||||
y_stride: self.vis[0].0,
|
||||
uv_stride: self.vis[1].0,
|
||||
pts: self.pts,
|
||||
key: i.key,
|
||||
intra_only: i.intra_only,
|
||||
color: ColorInfo { color_space: i.color_space, full_range: i.color_range },
|
||||
render_width: i.render_width,
|
||||
render_height: i.render_height,
|
||||
draft: self.draft,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The output picture of a complete (or completing) frame (show_existing_frame).
|
||||
fn picture_of(f: &Frame, pts: i64, draft: bool) -> Picture {
|
||||
let mut b = PictureBuilder::new(f, pts, draft);
|
||||
for r in 0..f.sb_rows() {
|
||||
b.add(f, r, f.band(r));
|
||||
}
|
||||
b.finish()
|
||||
}
|
||||
|
||||
+167
-25
@@ -63,27 +63,20 @@ impl MiGrid {
|
||||
}
|
||||
}
|
||||
|
||||
/// One plane of samples. The allocation covers whole 64x64 superblocks; `width` x `height` is the
|
||||
/// visible (cropped) area.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Plane {
|
||||
pub data: Vec<u16>,
|
||||
pub stride: usize,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
/// Scratch rows above the samples of every band: the loop filter of superblock row r reads 8
|
||||
/// and modifies up to 7 rows of row r - 1 (the 16-wide filter of the horizontal edges on top of
|
||||
/// row r), so those rows are copied there while row r is filtered (see `decoder::post`).
|
||||
pub const BAND_PAD: usize = 8;
|
||||
|
||||
/// One superblock row (64 luma rows) of a frame: per plane `BAND_PAD` scratch rows followed by
|
||||
/// the band's rows, all `Frame::strides[p]` samples wide.
|
||||
pub struct Band {
|
||||
pub planes: [Vec<u16>; 3],
|
||||
}
|
||||
|
||||
impl Plane {
|
||||
#[inline]
|
||||
pub fn row(&self, y: usize) -> &[u16] {
|
||||
&self.data[y * self.stride..(y + 1) * self.stride]
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoded frame (also used as reference).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Frame {
|
||||
pub planes: [Plane; 3],
|
||||
/// Frame metadata from the frame header.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct FrameInfo {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub ss_x: bool,
|
||||
@@ -97,6 +90,126 @@ pub struct Frame {
|
||||
pub intra_only: bool,
|
||||
}
|
||||
|
||||
/// A decoded frame (also used as reference). Its samples are published one superblock row
|
||||
/// ("band") at a time, once loop filtered, so that later frames can predict from the finished
|
||||
/// top of a frame while its bottom is still being filtered (frame threading).
|
||||
pub struct Frame {
|
||||
pub info: FrameInfo,
|
||||
/// Allocated plane widths (whole superblocks) = row strides.
|
||||
pub strides: [usize; 3],
|
||||
/// Rows per band of each plane.
|
||||
pub band_h: [usize; 3],
|
||||
/// Visible (cropped) plane sizes.
|
||||
pub vis: [(usize, usize); 3],
|
||||
bands: Box<[std::sync::OnceLock<Band>]>,
|
||||
pools: std::sync::Arc<Pools>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn new(info: FrameInfo, pools: std::sync::Arc<Pools>) -> Frame {
|
||||
let geo = plane_geometry(info.width, info.height, info.ss_x, info.ss_y);
|
||||
let sb_rows = (info.height as usize).div_ceil(64);
|
||||
Frame {
|
||||
info,
|
||||
strides: geo.map(|g| g.0),
|
||||
band_h: [64, 64 >> info.ss_y as usize, 64 >> info.ss_y as usize],
|
||||
vis: geo.map(|g| (g.2, g.3)),
|
||||
bands: (0..sb_rows).map(|_| std::sync::OnceLock::new()).collect(),
|
||||
pools,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sb_rows(&self) -> usize {
|
||||
self.bands.len()
|
||||
}
|
||||
|
||||
/// A band buffer for this frame's geometry (recycled memory, contents unspecified).
|
||||
pub fn new_band(&self) -> Band {
|
||||
Band { planes: std::array::from_fn(|p| self.pools.bands.take_any((self.band_h[p] + BAND_PAD) * self.strides[p], 0)) }
|
||||
}
|
||||
|
||||
/// Publish band `r` (ignored if already published).
|
||||
pub fn publish(&self, r: usize, band: Band) {
|
||||
if let Err(b) = self.bands[r].set(band) {
|
||||
for p in b.planes {
|
||||
self.pools.bands.put(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_published(&self, r: usize) -> bool {
|
||||
self.bands[r].get().is_some()
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.bands.iter().all(|b| b.get().is_some())
|
||||
}
|
||||
|
||||
/// Band `r`, waiting until it has been published.
|
||||
#[inline]
|
||||
pub fn band(&self, r: usize) -> &Band {
|
||||
#[cfg(feature = "threads")]
|
||||
{
|
||||
self.bands[r].wait()
|
||||
}
|
||||
#[cfg(not(feature = "threads"))]
|
||||
{
|
||||
self.bands[r].get().expect("reference band decoded before use")
|
||||
}
|
||||
}
|
||||
|
||||
/// Row `y` of plane `p` (all `strides[p]` samples), waiting until it is final.
|
||||
#[inline]
|
||||
pub fn row(&self, p: usize, y: usize) -> &[u16] {
|
||||
let bh = self.band_h[p];
|
||||
let s = self.strides[p];
|
||||
let o = (y % bh + BAND_PAD) * s;
|
||||
&self.band(y / bh).planes[p][o..o + s]
|
||||
}
|
||||
|
||||
/// Rows `y0..y1` of plane `p` as one slice with stride `strides[p]` when they lie in one band:
|
||||
/// (band samples, offset of row y0).
|
||||
#[inline]
|
||||
pub fn span(&self, p: usize, y0: usize, y1: usize) -> Option<(&[u16], usize)> {
|
||||
let bh = self.band_h[p];
|
||||
let r = y0 / bh;
|
||||
if (y1 - 1) / bh != r {
|
||||
return None;
|
||||
}
|
||||
Some((&self.band(r).planes[p], (y0 % bh + BAND_PAD) * self.strides[p]))
|
||||
}
|
||||
|
||||
/// A complete frame from full planes (tests).
|
||||
#[cfg(test)]
|
||||
pub fn from_planes(info: FrameInfo, planes: [&[u16]; 3]) -> Frame {
|
||||
let f = Frame::new(info, Default::default());
|
||||
for r in 0..f.sb_rows() {
|
||||
let mut b = f.new_band();
|
||||
for p in 0..3 {
|
||||
let (s, bh) = (f.strides[p], f.band_h[p]);
|
||||
for k in 0..bh {
|
||||
let y = r * bh + k;
|
||||
if let Some(src) = planes[p].get(y * s..y * s + s) {
|
||||
b.planes[p][(k + BAND_PAD) * s..(k + BAND_PAD + 1) * s].copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
}
|
||||
f.publish(r, b);
|
||||
}
|
||||
f
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Frame {
|
||||
fn drop(&mut self) {
|
||||
for b in std::mem::take(&mut self.bands).into_vec().into_iter().filter_map(|b| b.into_inner()) {
|
||||
for p in b.planes {
|
||||
self.pools.bands.put(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Geometry of the frame planes: allocation sizes for (MiCols, MiRows) rounded to superblocks.
|
||||
pub fn plane_geometry(width: u32, height: u32, ss_x: bool, ss_y: bool) -> [(usize, usize, usize, usize); 3] {
|
||||
let sb_cols = (width as usize).div_ceil(64);
|
||||
@@ -112,16 +225,38 @@ pub fn plane_geometry(width: u32, height: u32, ss_x: bool, ss_y: bool) -> [(usiz
|
||||
/// allocate (and page-fault) megabytes per frame.
|
||||
pub struct Pool<T> {
|
||||
bufs: std::sync::Mutex<Vec<Vec<T>>>,
|
||||
max: usize,
|
||||
}
|
||||
|
||||
impl<T> Default for Pool<T> {
|
||||
fn default() -> Self {
|
||||
Pool { bufs: Default::default() }
|
||||
Pool::with_max(32)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Pool<T> {
|
||||
/// A pool keeping at most `max` buffers.
|
||||
pub fn with_max(max: usize) -> Self {
|
||||
Pool { bufs: Default::default(), max }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Pool<T> {
|
||||
const MAX: usize = 32;
|
||||
/// A buffer of `len` elements with unspecified contents (recycled memory when available;
|
||||
/// `fill` only initialises fresh memory).
|
||||
pub fn take_any(&self, len: usize, fill: T) -> Vec<T> {
|
||||
let found = {
|
||||
let mut b = self.bufs.lock().expect("buffer pool");
|
||||
b.iter().position(|v| v.len() >= len).map(|i| b.swap_remove(i))
|
||||
};
|
||||
match found {
|
||||
Some(mut v) => {
|
||||
v.truncate(len);
|
||||
v
|
||||
}
|
||||
None => vec![fill; len],
|
||||
}
|
||||
}
|
||||
|
||||
/// A buffer of `len` copies of `fill` (recycled memory when available).
|
||||
pub fn take(&self, len: usize, fill: T) -> Vec<T> {
|
||||
@@ -131,6 +266,7 @@ impl<T: Clone> Pool<T> {
|
||||
};
|
||||
match found {
|
||||
Some(mut v) => {
|
||||
v.clear();
|
||||
v.resize(len, fill);
|
||||
v
|
||||
}
|
||||
@@ -138,21 +274,27 @@ impl<T: Clone> Pool<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put(&self, mut v: Vec<T>) {
|
||||
pub fn put(&self, v: Vec<T>) {
|
||||
if v.capacity() == 0 {
|
||||
return;
|
||||
}
|
||||
v.clear();
|
||||
let mut b = self.bufs.lock().expect("buffer pool");
|
||||
if b.len() < Self::MAX {
|
||||
if b.len() < self.max {
|
||||
b.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The decoder's buffer pools.
|
||||
#[derive(Default)]
|
||||
pub struct Pools {
|
||||
pub samples: Pool<u16>,
|
||||
/// Frame bands (many small buffers per frame).
|
||||
pub bands: Pool<u16>,
|
||||
pub mi: Pool<MiInfo>,
|
||||
}
|
||||
|
||||
impl Default for Pools {
|
||||
fn default() -> Self {
|
||||
Pools { samples: Pool::default(), bands: Pool::with_max(1024), mi: Pool::default() }
|
||||
}
|
||||
}
|
||||
|
||||
+44
-12
@@ -1,6 +1,7 @@
|
||||
//! Block inter prediction process (8.5.2.4): separable 8-tap sub-sample interpolation with edge
|
||||
//! clamping, for unscaled (step 16) and scaled references.
|
||||
|
||||
use crate::frame::Frame;
|
||||
use crate::tables::SUBPEL_FILTERS;
|
||||
|
||||
/// Filter taps of `filter` (0..3) at sub-sample position `frac` (0..15).
|
||||
@@ -10,14 +11,22 @@ fn taps(filter: u8, frac: usize) -> &'static [i16] {
|
||||
&SUBPEL_FILTERS[o..o + 8]
|
||||
}
|
||||
|
||||
/// A reference plane: samples addressed `data[y * stride + x]`, valid for x <= last_x, y <= last_y.
|
||||
/// Plane `plane` of a reference frame, valid for x <= last_x, y <= last_y. Rows are read through
|
||||
/// [`Frame::row`] / [`Frame::span`], which wait until the reference has published them.
|
||||
pub struct RefPlane<'a> {
|
||||
pub data: &'a [u16],
|
||||
pub stride: usize,
|
||||
pub frame: &'a Frame,
|
||||
pub plane: usize,
|
||||
pub last_x: i32,
|
||||
pub last_y: i32,
|
||||
}
|
||||
|
||||
impl RefPlane<'_> {
|
||||
#[inline]
|
||||
fn row(&self, y: usize) -> &[u16] {
|
||||
self.frame.row(self.plane, y)
|
||||
}
|
||||
}
|
||||
|
||||
/// Predict a `w` x `h` block whose top-left sample is at (`x`, `y`) in 1/16 sample units of the
|
||||
/// reference, stepping `step_x` / `step_y` (16 = unscaled). Output samples go to `out` (stride
|
||||
/// `w`). `tmp` must hold at least `(h * step_y / 16 + 8) * w` entries, `win` at least
|
||||
@@ -49,7 +58,7 @@ fn fetch_window(r: &RefPlane, x0: i32, y0: i32, w: usize, h: usize, win: &mut [u
|
||||
let ws = w + 7;
|
||||
for row in 0..h + 7 {
|
||||
let yy = (y0 - 3 + row as i32).clamp(0, r.last_y) as usize;
|
||||
let src = &r.data[yy * r.stride..];
|
||||
let src = r.row(yy);
|
||||
let dst = &mut win[row * ws..row * ws + ws];
|
||||
let xs = x0 - 3;
|
||||
if xs >= 0 && xs + ws as i32 - 1 <= r.last_x {
|
||||
@@ -69,12 +78,15 @@ fn predict_unscaled(r: &RefPlane, x: i32, y: i32, w: usize, h: usize, filter: u8
|
||||
let (fx, fy) = ((x & 15) as usize, (y & 15) as usize);
|
||||
let max = (1i32 << bit_depth) - 1;
|
||||
let inside = x0 - 3 >= 0 && y0 - 3 >= 0 && x0 + w as i32 + 4 <= r.last_x && y0 + h as i32 + 4 <= r.last_y;
|
||||
// Source view with origin at (x0 - 3, y0 - 3).
|
||||
let (src, ss, so): (&[u16], usize, usize) = if inside {
|
||||
(r.data, r.stride, (y0 - 3) as usize * r.stride + (x0 - 3) as usize)
|
||||
} else {
|
||||
let ws = fetch_window(r, x0, y0, w, h, win);
|
||||
(&win[..], ws, 0)
|
||||
// Source view with origin at (x0 - 3, y0 - 3): the reference band itself when the window
|
||||
// lies inside the picture and inside one band.
|
||||
let span = if inside { r.frame.span(r.plane, (y0 - 3) as usize, (y0 + h as i32 + 5) as usize) } else { None };
|
||||
let (src, ss, so): (&[u16], usize, usize) = match span {
|
||||
Some((data, o)) => (data, r.frame.strides[r.plane], o + (x0 - 3) as usize),
|
||||
None => {
|
||||
let ws = fetch_window(r, x0, y0, w, h, win);
|
||||
(&win[..], ws, 0)
|
||||
}
|
||||
};
|
||||
match (fx, fy) {
|
||||
(0, 0) => {
|
||||
@@ -184,7 +196,7 @@ fn predict_scaled(r: &RefPlane, x: i32, y: i32, step_x: i32, step_y: i32, w: usi
|
||||
let ih = ((((h as i32 - 1) * step_y + 15) >> 4) + 8) as usize;
|
||||
for row in 0..ih {
|
||||
let yy = ((y >> 4) + row as i32 - 3).clamp(0, r.last_y) as usize;
|
||||
let src = &r.data[yy * r.stride..];
|
||||
let src = r.row(yy);
|
||||
for c in 0..w {
|
||||
let p = x + step_x * c as i32;
|
||||
let f = taps(filter, (p & 15) as usize);
|
||||
@@ -217,7 +229,27 @@ mod tests {
|
||||
fn scaled_path_with_unit_step_matches_unscaled() {
|
||||
let (w, h) = (40usize, 30usize);
|
||||
let data: Vec<u16> = (0..w * h).map(|i| ((i * 37 + (i / w) * 11) % 256) as u16).collect();
|
||||
let r = RefPlane { data: &data, stride: w, last_x: w as i32 - 1, last_y: h as i32 - 1 };
|
||||
let info = crate::frame::FrameInfo {
|
||||
width: w as u32,
|
||||
height: h as u32,
|
||||
ss_x: true,
|
||||
ss_y: true,
|
||||
bit_depth: 8,
|
||||
color_space: 0,
|
||||
color_range: false,
|
||||
render_width: w as u32,
|
||||
render_height: h as u32,
|
||||
key: true,
|
||||
intra_only: false,
|
||||
};
|
||||
// Planes are allocated in whole superblocks: copy the test picture into a 64x64 plane.
|
||||
let mut full = vec![0u16; 64 * 64];
|
||||
for y in 0..h {
|
||||
full[y * 64..y * 64 + w].copy_from_slice(&data[y * w..y * w + w]);
|
||||
}
|
||||
let chroma = vec![0u16; 32 * 32];
|
||||
let frame = Frame::from_planes(info, [&full, &chroma, &chroma]);
|
||||
let r = RefPlane { frame: &frame, plane: 0, last_x: w as i32 - 1, last_y: h as i32 - 1 };
|
||||
let mut tmp = vec![0u16; 80 * 80];
|
||||
for filter in 0..4u8 {
|
||||
for &(x, y) in &[(0, 0), (5, 3), (16 * 7 + 5, 16 * 9 + 11), (-40, -3), (16 * 36 + 1, 16 * 25 + 15), (16 * 10, 16 * 10 + 8)] {
|
||||
|
||||
@@ -163,4 +163,7 @@ pub struct Picture {
|
||||
/// Intended display size (render_size); has no effect on decoding.
|
||||
pub render_width: u32,
|
||||
pub render_height: u32,
|
||||
/// Decoded in draft mode without the loop filter ([`Decoder::set_draft`]): approximate, for
|
||||
/// reduced-resolution playback only. Never set without draft mode.
|
||||
pub draft: bool,
|
||||
}
|
||||
|
||||
@@ -309,13 +309,8 @@ impl<'a> TileDecoder<'a> {
|
||||
(((base_x << 4) + dx) as i32, ((base_y << 4) + dy) as i32, rf.x_step, rf.y_step)
|
||||
};
|
||||
let f = rf.frame;
|
||||
let p = &f.planes[plane];
|
||||
let rp = RefPlane {
|
||||
data: &p.data,
|
||||
stride: p.stride,
|
||||
last_x: ((f.width as i32 + sx as i32) >> sx) - 1,
|
||||
last_y: ((f.height as i32 + sy as i32) >> sy) - 1,
|
||||
};
|
||||
let rp =
|
||||
RefPlane { frame: f, plane, last_x: ((f.info.width as i32 + sx as i32) >> sx) - 1, last_y: ((f.info.height as i32 + sy as i32) >> sy) - 1 };
|
||||
let out = if rl == 0 { &mut self.pred } else { &mut self.pred2 };
|
||||
inter::predict(&rp, start_x, start_y, step_x, step_y, w, h, b.interp_filter, self.bit_depth, out, &mut self.mc_tmp, &mut self.mc_win);
|
||||
}
|
||||
|
||||
@@ -112,6 +112,26 @@ pub const FIXTURES: &[Fixture] = &[
|
||||
fx!("sharpness", "testsrc2", 352, 288, 8, NOISE, P420, ["-b:v", "400k", "-sharpness", "5", "-frame-parallel", "0"]),
|
||||
fx!("fade_intra_heavy", "testsrc2", 352, 288, 20, "fade=in:0:15", P420, ["-b:v", "400k", "-g", "8", "-frame-parallel", "0"]),
|
||||
fx!("hd_1080p", "testsrc2", 1920, 1080, 3, NOISE, P420, ["-b:v", "4M", "-speed", "5", "-deadline", "realtime"]),
|
||||
// Two temporal layers: every other frame refreshes no reference slot (draft mode test).
|
||||
fx!(
|
||||
"temporal_layers",
|
||||
"testsrc2",
|
||||
352,
|
||||
288,
|
||||
20,
|
||||
NOISE,
|
||||
P420,
|
||||
[
|
||||
"-b:v",
|
||||
"800k",
|
||||
"-speed",
|
||||
"8",
|
||||
"-deadline",
|
||||
"realtime",
|
||||
"-ts-parameters",
|
||||
"ts_number_layers=2:ts_target_bitrate=400,800:ts_rate_decimator=2,1:ts_periodicity=2:ts_layer_id=0,1:ts_layering_mode=2"
|
||||
]
|
||||
),
|
||||
fx!(
|
||||
"bench_1080p",
|
||||
"testsrc2",
|
||||
@@ -383,7 +403,8 @@ pub fn check_fixture(name: &str) -> Result<bool, String> {
|
||||
}
|
||||
|
||||
pub fn check_files(name: &str, ivf: &Path, reference: &[u8], pix_fmt: &str) -> Result<(), String> {
|
||||
for threads in [1, 0] {
|
||||
// Single-threaded, frame threads with a small pool, and every core.
|
||||
for threads in [1, 3, 0] {
|
||||
let pics = match decode_file_threads(ivf, threads) {
|
||||
Ok(p) => p,
|
||||
Err((fr, e, partial)) => {
|
||||
@@ -392,6 +413,9 @@ pub fn check_files(name: &str, ivf: &Path, reference: &[u8], pix_fmt: &str) -> R
|
||||
}
|
||||
};
|
||||
compare(&pics, reference, pix_fmt).map_err(|e| format!("{name} (threads {threads}): {e}"))?;
|
||||
if pics.iter().any(|p| p.draft) {
|
||||
return Err(format!("{name} (threads {threads}): picture flagged draft without draft mode"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -61,8 +61,46 @@ fixture_tests!(
|
||||
sharpness,
|
||||
fade_intra_heavy,
|
||||
hd_1080p,
|
||||
temporal_layers,
|
||||
);
|
||||
|
||||
/// Draft mode: frames no other frame references skip the loop filter and are flagged; every
|
||||
/// other picture stays bit-exact, single-threaded and with frame threads.
|
||||
#[test]
|
||||
fn draft_mode_changes_only_flagged_non_reference_frames() {
|
||||
let f = common::fixture("temporal_layers");
|
||||
let Some((ivf, yuv)) = common::ensure(f) else { return };
|
||||
let reference = std::fs::read(&yuv).unwrap();
|
||||
let data = std::fs::read(&ivf).unwrap();
|
||||
let fsize = common::raw_size(f.width as usize, f.height as usize, f.pix_fmt);
|
||||
for threads in [1, 3] {
|
||||
let mut dec = filmcraft_vp9::Decoder::with_threads(threads);
|
||||
dec.set_draft(true);
|
||||
let mut pics = Vec::new();
|
||||
for (i, c) in common::ivf_frames(&data).into_iter().enumerate() {
|
||||
pics.extend(dec.decode(c, i as i64).unwrap());
|
||||
}
|
||||
pics.extend(dec.flush());
|
||||
assert_eq!(pics.len(), f.frames as usize, "threads {threads}");
|
||||
let (mut flagged, mut changed) = (0, 0);
|
||||
for (i, p) in pics.iter().enumerate() {
|
||||
let mut bytes = Vec::new();
|
||||
for pl in [&p.y, &p.u, &p.v] {
|
||||
bytes.extend(pl.to_le_bytes());
|
||||
}
|
||||
let same = bytes == reference[i * fsize..(i + 1) * fsize];
|
||||
if p.draft {
|
||||
flagged += 1;
|
||||
changed += !same as usize;
|
||||
} else {
|
||||
assert!(same, "threads {threads}: unflagged frame {i} differs");
|
||||
}
|
||||
}
|
||||
assert!(flagged >= f.frames as usize / 3, "threads {threads}: only {flagged} draft frames");
|
||||
assert!(changed > 0, "threads {threads}: draft frames identical to the filtered ones");
|
||||
}
|
||||
}
|
||||
|
||||
/// Two streams of different sizes concatenated into one IVF (key frame at the size change).
|
||||
#[test]
|
||||
fn resize_concatenated() {
|
||||
|
||||
@@ -16,6 +16,7 @@ fn bench(frames: &[&[u8]], threads: usize, iters: usize) -> (usize, f64) {
|
||||
for f in frames {
|
||||
n += dec.decode(f, 0).expect("decode").len();
|
||||
}
|
||||
n += dec.flush().len();
|
||||
best = best.min(t0.elapsed().as_secs_f64());
|
||||
}
|
||||
(n, best)
|
||||
|
||||
Reference in New Issue
Block a user