mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
* craft-video: AV1 decoder from FilmCraft (`av1-decode` feature) FilmCraft's crates/av1 at 4c0d2ad7 becomes craft_video::av1::decode. The encoder moves to av1/encode and is still exported from craft_video::av1. Feature av1 now turns on av1-decode and av1-encode, so either half can be built without the other. The ignored speed tests no longer read the thread CPU clock (FilmCraft used rustix for it). craft_video::budget (the picture memory budget from DeckCraft's bitstream crate) is added for the next commit. craft-video is built at opt-level 3 in the dev profile, as FilmCraft built its codecs. * craft-video: AV1 decoder hardening from DeckCraft's copy - The picture store (eight reference slots, the new frame, frames in flight) is checked against a memory budget before a frame is allocated; Decoder::set_memory_budget. - Non-uniform tile spacing is refused past MAX_TILE_COLS columns or MAX_TILE_ROWS rows. * craft-video: AV1 fuzz seeds are 64x64 so SVT-AV1 2.x can encode them FilmCraft's seeds were 64x48; the SVT-AV1 2.3 on the Ubuntu runners refuses a source under 64 lines, and an encoder that exists but fails is a test failure, not a skip.
123 lines
5.2 KiB
Rust
123 lines
5.2 KiB
Rust
//! Decode throughput and per-stage breakdown (ignored; run with
|
|
//! `cargo test --release -p craft-video --features av1-decode,threads --test av1_decode perf:: -- --ignored --nocapture`).
|
|
//!
|
|
//! The machine may be busy (other builds), so single-threaded work is also measured with the
|
|
//! thread CPU clock, which is robust to load; wall-clock figures are printed next to the load
|
|
//! average.
|
|
|
|
use crate::common;
|
|
use common::*;
|
|
use craft_video::av1::decode::{DecodeStats, Decoder, Stage};
|
|
use std::time::Instant;
|
|
|
|
fn thread_cpu_secs() -> Option<f64> {
|
|
// shortcut: no thread CPU clock (FilmCraft read it through rustix); wall-clock figures only,
|
|
// add a rustix dev-dependency if these ignored speed tests need the CPU clock back.
|
|
None
|
|
}
|
|
|
|
fn load_average() -> String {
|
|
let out = std::process::Command::new("sysctl").args(["-n", "vm.loadavg"]).output().ok();
|
|
if let Some(o) = out.filter(|o| o.status.success()) {
|
|
return String::from_utf8_lossy(&o.stdout).trim().trim_matches(|c| c == '{' || c == '}').trim().to_string();
|
|
}
|
|
std::fs::read_to_string("/proc/loadavg").map(|s| s.split_whitespace().take(3).collect::<Vec<_>>().join(" ")).unwrap_or_else(|_| "?".into())
|
|
}
|
|
|
|
struct Run {
|
|
wall: f64,
|
|
cpu: Option<f64>,
|
|
shown: usize,
|
|
stats: DecodeStats,
|
|
}
|
|
|
|
fn run(frames: &[Vec<u8>], threads: usize) -> Run {
|
|
let mut dec = Decoder::with_threads(threads);
|
|
let t = Instant::now();
|
|
let c0 = thread_cpu_secs();
|
|
let mut shown = 0;
|
|
for f in frames {
|
|
shown += dec.decode(f).unwrap().len();
|
|
}
|
|
shown += dec.flush().len();
|
|
let cpu = c0.and_then(|c0| thread_cpu_secs().map(|c1| c1 - c0));
|
|
Run { wall: t.elapsed().as_secs_f64(), cpu, shown, stats: dec.stats() }
|
|
}
|
|
|
|
fn bench(spec: &Spec) {
|
|
// AV1_PERF_ONLY=<substring>[,<substring>...] limits the run to matching fixtures.
|
|
if std::env::var("AV1_PERF_ONLY").is_ok_and(|f| !f.split(',').any(|f| spec.name.contains(f))) {
|
|
return;
|
|
}
|
|
let Some(ff) = ffmpeg() else { return };
|
|
let path = make(&ff, spec);
|
|
let frames = ivf_frames(&path);
|
|
// Single-threaded: best of 3 by thread CPU time (falls back to wall time).
|
|
let mut best: Option<Run> = None;
|
|
for _ in 0..3 {
|
|
let r = run(&frames, 1);
|
|
let key = |r: &Run| r.cpu.unwrap_or(r.wall);
|
|
if best.as_ref().is_none_or(|b| key(&r) < key(b)) {
|
|
best = Some(r);
|
|
}
|
|
}
|
|
let b = best.unwrap();
|
|
let cpu_fps = b.cpu.map(|c| format!("{:6.1}", b.shown as f64 / c)).unwrap_or_else(|| " ?".into());
|
|
println!(
|
|
"{:<26} {:>3} shown {:>3} decoded {:>3} tiles/frame | 1 thread: {} fps (thread CPU) {:6.1} fps (wall)",
|
|
spec.name,
|
|
b.shown,
|
|
b.stats.frames,
|
|
b.stats.tiles / b.stats.frames.max(1),
|
|
cpu_fps,
|
|
b.shown as f64 / b.wall
|
|
);
|
|
println!("{}", stage_line(" ms/frame:", &b.stats));
|
|
// All cores: best of 3 by wall time.
|
|
let threads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
|
let mut best: Option<Run> = None;
|
|
for _ in 0..3 {
|
|
let r = run(&frames, threads);
|
|
assert_eq!(r.shown, b.shown);
|
|
if best.as_ref().is_none_or(|b| r.wall < b.wall) {
|
|
best = Some(r);
|
|
}
|
|
}
|
|
let m = best.unwrap();
|
|
println!(" {threads} threads: {:6.1} fps (wall, load average {})", b.shown as f64 / m.wall, load_average());
|
|
// Per-stage wall time on the frame workers (stages of different frames overlap).
|
|
println!("{}", stage_line(&format!(" {threads} threads, ms/frame:"), &m.stats));
|
|
}
|
|
|
|
/// Per-stage milliseconds per frame and share of the total.
|
|
fn stage_line(label: &str, stats: &DecodeStats) -> String {
|
|
let n = stats.frames.max(1) as f64;
|
|
let total = stats.total_secs();
|
|
let mut line = String::from(label);
|
|
for s in Stage::ALL {
|
|
let v = stats.secs(s);
|
|
if v > 0.0005 {
|
|
line += &format!(" {} {:.2} ({:.0}%)", s.name(), 1e3 * v / n, 100.0 * v / total);
|
|
}
|
|
}
|
|
line
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn decode_throughput() {
|
|
let p = |crf: &'static str, params: &'static str| -> Vec<&'static str> { vec!["-preset", "8", "-crf", crf, "-svtav1-params", params] };
|
|
let src = "testsrc2=s=1920x1080:r=25,noise=alls=6:allf=t";
|
|
bench(&Spec::new("perf_1080p_intra", src, 10, "yuv420p", &p("30", "keyint=1")));
|
|
bench(&Spec::new("perf_1080p_gop", src, 60, "yuv420p", &p("30", "keyint=60")));
|
|
bench(&Spec::new("perf_1080p_gop_10bit", src, 60, "yuv420p10le", &p("30", "keyint=60")));
|
|
// Higher quality (more coefficients, finer partitions) and a multi-tile stream (4x2 tiles).
|
|
bench(&Spec::new("perf_1080p_gop_hq", src, 60, "yuv420p", &p("18", "keyint=60")));
|
|
bench(&Spec::new("perf_1080p_gop_tiles", src, 60, "yuv420p", &p("30", "keyint=60:tile-columns=2:tile-rows=1")));
|
|
// Low delay (each frame references the previous one, as screen and camera recorders encode):
|
|
// frames cannot overlap, so a frame's own parallelism decides the speed.
|
|
bench(&Spec::new("perf_1080p_lowdelay", src, 60, "yuv420p", &p("30", "keyint=60:pred-struct=1")));
|
|
// One tile at 2160p: the post-filters' row bands are most of the parallelism there.
|
|
bench(&Spec::new("perf_2160p_gop", "testsrc2=s=3840x2160:r=25,noise=alls=6:allf=t", 30, "yuv420p", &p("30", "keyint=30")));
|
|
}
|