filmcraft/crates/prores/tests/oracle_decode.rs

154 lines
7.5 KiB
Rust

//! Decoder accuracy against ffmpeg's ProRes decoder (oracle only; skipped without ffmpeg).
mod common;
use common::*;
use filmcraft_prores::{AlphaType, ChromaFormat, DecodeOptions, Interlace, decode_frame, decode_frame_with, probe};
fn specs() -> Vec<Spec> {
let p422 = "format=yuv422p10le";
let p444 = "format=yuv444p10le";
let alpha = "format=yuva444p10le,geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='mod(X*7+Y*3,1024)'";
let ks = |p: &'static str| -> Vec<&'static str> { vec!["-c:v", "prores_ks", "-profile:v", p] };
let aw = |p: &'static str| -> Vec<&'static str> { vec!["-c:v", "prores_aw", "-profile:v", p] };
let mut v = Vec::new();
for (i, p) in ["0", "1", "2", "3"].into_iter().enumerate() {
let names = [
["ks_proxy_testsrc2_1080", "ks_lt_testsrc2_1080", "ks_std_testsrc2_1080", "ks_hq_testsrc2_1080"],
["ks_proxy_mandel_1918", "ks_lt_mandel_1918", "ks_std_mandel_1918", "ks_hq_mandel_1918"],
["ks_proxy_bars_720", "ks_lt_bars_720", "ks_std_bars_720", "ks_hq_bars_720"],
["aw_proxy_testsrc2_640", "aw_lt_testsrc2_640", "aw_std_testsrc2_640", "aw_hq_testsrc2_640"],
];
v.push(Spec::new(names[0][i], format!("testsrc2=s=1920x1080:r=25,{p422}"), 2, &ks(p)));
v.push(Spec::new(names[1][i], format!("mandelbrot=s=1918x1080:r=25,{p422}"), 2, &ks(p)));
v.push(Spec::new(names[2][i], format!("smptehdbars=s=1280x720:r=25,{p422}"), 1, &ks(p)));
v.push(Spec::new(names[3][i], format!("testsrc2=s=640x360:r=25,{p422}"), 2, &aw(p)));
}
v.push(Spec::new("ks_hq_odd_70x38", format!("testsrc2=s=70x38:r=25,{p422}"), 2, &ks("3")));
v.push(Spec::new("ks_hq_small_slices", format!("mandelbrot=s=352x288:r=25,{p422}"), 1, &[&ks("3")[..], &["-mbs_per_slice", "2"]].concat()));
let il = ["-flags", "+ildct+ilme"];
v.push(Spec::new("ks_hq_ntsc_tff", format!("testsrc2=s=720x486:r=30000/1001,setfield=tff,{p422}"), 2, &[&ks("3")[..], &il].concat()));
v.push(Spec::new("ks_std_ntsc_bff", format!("mandelbrot=s=720x486:r=30000/1001,setfield=bff,{p422}"), 2, &[&ks("2")[..], &il].concat()));
v.push(Spec::new("aw_hq_1080i", format!("testsrc2=s=1920x1080:r=30000/1001,setfield=tff,{p422}"), 1, &[&aw("3")[..], &il].concat()));
v.push(Spec::new("ks_4444_testsrc2_1080", format!("testsrc2=s=1920x1080:r=25,{p444}"), 2, &ks("4")));
v.push(Spec::new("ks_4444xq_mandel_1918", format!("mandelbrot=s=1918x1080:r=25,{p444}"), 1, &ks("5")));
v.push(Spec::new("ks_4444_alpha16_640", format!("testsrc2=s=640x360:r=25,{alpha}"), 2, &[&ks("4")[..], &["-alpha_bits", "16"]].concat()));
v.push(Spec::new("ks_4444_alpha8_640", format!("testsrc2=s=640x360:r=25,{alpha}"), 1, &[&ks("4")[..], &["-alpha_bits", "8"]].concat()));
v.push(Spec::new("ks_4444xq_alpha_odd", format!("mandelbrot=s=333x201:r=25,{alpha}"), 1, &ks("5")));
v.push(Spec::new("aw_4444_640", format!("testsrc2=s=640x360:r=25,{p444}"), 1, &aw("4")));
v.push(Spec::new("aw_4444_alpha_640", format!("testsrc2=s=640x360:r=25,{alpha}"), 1, &aw("4")));
v.push(Spec::new("ks_4444_ntsc_tff", format!("testsrc2=s=720x486:r=30000/1001,setfield=tff,{p444}"), 1, &[&ks("4")[..], &il].concat()));
v
}
fn native_pix_fmt(chroma: ChromaFormat, alpha: bool) -> &'static str {
match (chroma, alpha) {
(ChromaFormat::Yuv422, _) => "yuv422p10le",
(ChromaFormat::Yuv444, false) => "yuv444p12le",
(ChromaFormat::Yuv444, true) => "yuva444p12le",
}
}
#[test]
fn decode_matches_ffmpeg() {
let Some(ff) = ffmpeg() else { return };
let filter = std::env::var("PRORES_FIXTURE").ok();
let mut report = vec![format!("{:<26} {:>9} {:>5} {:>8} {:>8} {:>8} {:>8}", "fixture", "format", "maxY", "meanY", "maxC/A", "meanC/A", "exact%")];
let mut worst = 0;
for spec in specs() {
if let Some(f) = &filter
&& !spec.name.contains(f.as_str())
{
continue;
}
let mov = make(&ff, &spec);
let pkts = packets(&ff, &mov);
assert_eq!(pkts.len(), spec.frames as usize, "{}", spec.name);
let hdr = probe(&pkts[0]).unwrap();
let has_alpha = hdr.alpha != AlphaType::None;
let fmt = native_pix_fmt(hdr.chroma, has_alpha);
let raw = reference(&ff, &mov, fmt);
let per = frame_samples(hdr.width as usize, hdr.height as usize, hdr.chroma, has_alpha);
assert_eq!(raw.len(), per * pkts.len());
let (mut sl, mut sc) = (Stats::default(), Stats::default());
for (i, p) in pkts.iter().enumerate() {
let f = decode_frame(p).unwrap_or_else(|e| panic!("{}: frame {i}: {e}", spec.name));
if spec.enc.contains(&"+ildct+ilme") {
assert_ne!(f.interlace, Interlace::Progressive);
}
let (l, c) = compare(&f, &raw[i * per..(i + 1) * per]);
sl.merge(&l);
sc.merge(&c);
}
let mut all = sl;
all.merge(&sc);
worst = worst.max(all.max);
report.push(format!(
"{:<26} {:>9} {:>5} {:>8.5} {:>8} {:>8.5} {:>8.3}",
spec.name,
&fmt[..fmt.len() - 2],
sl.max,
sl.mean(),
sc.max,
sc.mean(),
all.exact_pct()
));
}
let text = report.join("\n");
println!("{text}");
std::fs::write(fixture_dir().join("decode_report.txt"), &text).unwrap();
assert!(worst <= 1, "decoder deviates from reference by up to {worst} LSB\n{text}");
}
/// 10-bit output of 4444 streams stays within ±1 of ffmpeg's 12-bit output rounded to 10 bits.
#[test]
fn decode_4444_at_10_bits() {
let Some(ff) = ffmpeg() else { return };
let spec = Spec::new("ks_4444_alpha16_640", "", 2, &[]);
let spec = specs().into_iter().find(|s| s.name == spec.name).unwrap();
let mov = make(&ff, &spec);
let pkts = packets(&ff, &mov);
let raw12 = reference(&ff, &mov, "yuva444p12le");
let per = frame_samples(640, 360, ChromaFormat::Yuv444, true);
let f = decode_frame_with(&pkts[0], &DecodeOptions { bit_depth: Some(10), threads: true }).unwrap();
assert_eq!(f.bit_depth, 10);
let r10: Vec<u16> = raw12[..per].iter().map(|&v| ((v as u32 + 2) >> 2).min(1023) as u16).collect();
let (l, c) = compare(&f, &r10);
assert!(l.max <= 1 && c.max <= 1, "{l:?} {c:?}");
}
#[test]
fn single_threaded_matches_threaded() {
let Some(ff) = ffmpeg() else { return };
let spec = specs().into_iter().find(|s| s.name == "ks_hq_ntsc_tff").unwrap();
let mov = make(&ff, &spec);
let pkts = packets(&ff, &mov);
let a = decode_frame_with(&pkts[0], &DecodeOptions { bit_depth: None, threads: false }).unwrap();
let b = decode_frame(&pkts[0]).unwrap();
assert_eq!(a, b);
}
#[test]
fn color_metadata_is_reported() {
let Some(ff) = ffmpeg() else { return };
let spec = Spec::new(
"ks_hq_bt2020_pq",
"testsrc2=s=128x64:r=25,format=yuv422p10le,setparams=color_primaries=bt2020:color_trc=smpte2084:colorspace=bt2020nc",
1,
&["-c:v", "prores_ks", "-profile:v", "3"],
);
let mov = make(&ff, &spec);
let f = decode_frame(&packets(&ff, &mov)[0]).unwrap();
assert_eq!((f.color.primaries, f.color.transfer, f.color.matrix), (9, 16, 9));
}
/// Pre-generate every decode fixture (`cargo xtask fixtures`).
#[test]
#[ignore]
fn generate_fixtures() {
let Some(ff) = ffmpeg() else { return };
for spec in specs() {
let out = [fixture_dir().join(format!("{}.mov", spec.name))];
filmcraft_testkit::fixtures::generate_and_report(&format!("prores/{}", spec.name), &out, || Some(make(&ff, &spec)));
}
}