use std::path::PathBuf;
use std::sync::Once;
use std::time::{Duration, Instant};
use criterion::{Criterion, Throughput, black_box, criterion_group, criterion_main};
const CLIPS: &[&str] = &[
"avm-v14.1.0-bus.64x64.l5.obu",
"avm-v14.1.0-bus.64x64.l5.lossless.obu",
"avm-v14.1.0-bus.64x64.l5.opfl0-refinemv0.obu",
"avm-v14.1.0-bus.64x64.l1.sdp0.obu",
"avm-v14.1.0-bus.64x64.l1.sdp1.obu",
"avm-v14.1.0-bus.352x288.l5.seg1.obu",
"avm-v14.1.0-bus.352x288.l10.deltaq1.obu",
"avm-v14.1.0-bus.352x288.l1.partial_lossless.obu",
"avm-v14.1.0-hm.64x64.l5.filmgrain.obu",
];
#[cfg(target_os = "macos")]
const EAGAIN: i32 = -35;
#[cfg(not(target_os = "macos"))]
const EAGAIN: i32 = -11;
fn media(name: &str) -> PathBuf {
PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../../dav2d/media")).join(name)
}
fn ss(layout: i32) -> (i32, i32) {
match layout {
1 => (1, 1), 2 => (1, 0), _ => (0, 0), }
}
fn frame_samples(w: i32, h: i32, layout: i32) -> u64 {
let (ssh, ssv) = ss(layout);
let luma = (w as u64) * (h as u64);
if layout == 0 {
return luma;
}
let cw = ((w + ssh) >> ssh) as u64;
let ch = ((h + ssv) >> ssv) as u64;
luma + 2 * cw * ch
}
fn rav2d_run(bytes: &[u8], filters: rav2d::InloopFilterType) -> (u32, u64) {
use rav2d::{Data, Decoder, Settings};
let mut s = Settings::default();
s.n_threads = 1;
s.apply_grain = false;
s.run_decode = true;
s.inloop_filters = filters;
let mut dec = match Decoder::open(&s) {
Ok(d) => d,
Err(_) => return (0, 0),
};
let mut frames = 0u32;
let mut samples = 0u64;
let mut sent = false;
loop {
if !sent {
match dec.send_data(Some(Data::wrap(bytes.to_vec()))) {
Ok(()) => sent = true,
Err(rav2d::Rav2dError::Again) => {}
Err(_) => break,
}
}
match dec.get_picture() {
Ok(pic) => {
frames += 1;
samples += frame_samples(pic.p.w, pic.p.h, pic.p.layout as i32);
}
Err(rav2d::Rav2dError::Again) => {
if sent {
let _ = dec.send_data(None);
} else {
break;
}
}
Err(_) => break,
}
if frames > 4096 {
break;
}
}
(frames, samples)
}
fn dav2d_run(bytes: &[u8], inloop_filters: u32) -> (u32, u64) {
use rav2d_sys as sys;
let mut frames = 0u32;
let mut samples = 0u64;
unsafe {
let mut settings: sys::Dav2dSettings = std::mem::zeroed();
sys::dav2d_default_settings(&mut settings);
settings.n_threads = 1;
settings.apply_grain = 0;
settings.inloop_filters = inloop_filters;
settings.output_invisible_frames = 1;
let mut ctx: *mut sys::Dav2dContext = std::ptr::null_mut();
if sys::dav2d_open(&mut ctx, &settings) != 0 {
return (0, 0);
}
let mut d: sys::Dav2dData = std::mem::zeroed();
let buf = sys::dav2d_data_create(&mut d, bytes.len());
assert!(!buf.is_null(), "dav2d_data_create failed");
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
let mut drained = false;
loop {
loop {
let mut pic: sys::Dav2dPicture = std::mem::zeroed();
let pr = sys::dav2d_get_picture(ctx, &mut pic);
if pr == EAGAIN || pr < 0 {
break;
}
frames += 1;
samples += frame_samples(pic.p.w, pic.p.h, pic.p.layout as i32);
sys::dav2d_picture_unref(&mut pic);
}
if d.sz > 0 {
let sr = sys::dav2d_send_data(ctx, &mut d);
if sr < 0 && sr != EAGAIN {
break;
}
} else if !drained {
drained = true;
sys::dav2d_send_data(ctx, std::ptr::null_mut());
} else {
let mut pic: sys::Dav2dPicture = std::mem::zeroed();
if sys::dav2d_get_picture(ctx, &mut pic) == 0 {
frames += 1;
samples += frame_samples(pic.p.w, pic.p.h, pic.p.layout as i32);
sys::dav2d_picture_unref(&mut pic);
} else {
break;
}
}
}
sys::dav2d_close(&mut ctx);
}
(frames, samples)
}
fn median_decode(
bytes: &[u8],
iters: usize,
run: impl Fn(&[u8]) -> (u32, u64),
) -> (Duration, u32, u64) {
let (frames, samples) = run(bytes);
let mut times = Vec::with_capacity(iters);
for _ in 0..iters {
let t = Instant::now();
let _ = black_box(run(black_box(bytes)));
times.push(t.elapsed());
}
times.sort();
(times[times.len() / 2], frames, samples)
}
fn print_comparison_table() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
eprintln!();
eprintln!("=== rav2d vs dav2d decode throughput (single-thread, filters off) ===");
eprintln!(
"{:<46} {:>6} {:>10} {:>10} {:>9} {:>9} {:>8}",
"clip", "frames", "rav2d ms", "dav2d ms", "rav MP/s", "dav MP/s", "dav/rav"
);
for name in CLIPS {
let path = media(name);
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(_) => {
eprintln!("{name:<46} (missing)");
continue;
}
};
let iters = 15;
let (r_ms, r_frames, r_samples) = median_decode(&bytes, iters, |b| {
rav2d_run(b, rav2d::InloopFilterType::None)
});
let (d_ms, d_frames, d_samples) = median_decode(&bytes, iters, |b| dav2d_run(b, 0));
let r_s = r_ms.as_secs_f64();
let d_s = d_ms.as_secs_f64();
let r_mps = if r_s > 0.0 {
r_samples as f64 / 1e6 / r_s
} else {
0.0
};
let d_mps = if d_s > 0.0 {
d_samples as f64 / 1e6 / d_s
} else {
0.0
};
let ratio = if r_s > 0.0 { d_s / r_s } else { 0.0 };
let frames = if r_frames == d_frames {
format!("{r_frames}")
} else {
format!("{r_frames}/{d_frames}")
};
eprintln!(
"{:<46} {:>6} {:>10.3} {:>10.3} {:>9.1} {:>9.1} {:>7.2}x",
name,
frames,
r_s * 1e3,
d_s * 1e3,
r_mps,
d_mps,
ratio
);
}
eprintln!(
"(frames shown as rav2d/dav2d when they differ — rav2d still gaining inter coverage)"
);
eprintln!("(dav2d uses SIMD; rav2d uses NEON for MC on aarch64, scalar elsewhere)");
eprintln!();
});
}
fn bench_decode(c: &mut Criterion) {
print_comparison_table();
let mut grp = c.benchmark_group("decode");
for name in CLIPS {
let path = media(name);
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(_) => continue,
};
let (_, samples) = rav2d_run(&bytes, rav2d::InloopFilterType::None);
if samples > 0 {
grp.throughput(Throughput::Elements(samples));
}
grp.bench_with_input(format!("rav2d/{name}"), &bytes, |b, bytes| {
b.iter(|| black_box(rav2d_run(black_box(bytes), rav2d::InloopFilterType::None)));
});
grp.bench_with_input(format!("dav2d/{name}"), &bytes, |b, bytes| {
b.iter(|| black_box(dav2d_run(black_box(bytes), 0)));
});
}
grp.finish();
}
criterion_group!(benches, bench_decode);
criterion_main!(benches);