mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-25 10:37:34 -06:00
New L0 crate filmcraft-dnx, written from SMPTE ST 2019-1:2016 + Amd 1:2023 (tables extracted from the standard's text by tools/extract_tables.py). Decoder: every compression ID (DNxHD 1235-1260 incl. field-coded and macroblock-adaptive interlace, thin rasters, RGB 4:4:4; DNxHR 1270-1274 LB/SQ/HQ/HQX/444), 8/10/12-bit, 4:2:0/4:2:2/4:4:4, DCT and lossless alpha, macroblock rows in parallel. Matches ffmpeg within IDCT precision (max 2 LSB, <=50 per million beyond 1, 87-99% identical) on 22 ffmpeg fixtures; follows ffmpeg where it departs from the standard's text (reconstruction offset, CID 1271 weights, 4:4:4 signalling) - documented in the README. Encoder: DNxHR LB/SQ/HQ/HQX(10/12-bit)/444, CBR at the eq. 7.1 frame sizes, uniform-qsf binary search + per-macroblock refinement; ffmpeg decodes every profile with -xerror (HQ luma PSNR 58.8-66.3 dB). Wiring: MOV AVdn/AVdh import (codec label from the CID), export Format::DnxHr (MOV AVdh, dnxProfile lb|sq|hq|hqx) in the export dialog and file.exportMedia. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
4.8 KiB
Rust
84 lines
4.8 KiB
Rust
//! Decode ffmpeg-made DNxHD / DNxHR fixtures and compare with ffmpeg's own decode.
|
|
|
|
mod common;
|
|
use common::*;
|
|
use filmcraft_dnx::{DecodeOptions, decode_frame_with};
|
|
|
|
fn specs() -> Vec<Spec> {
|
|
let t = |s: &str, r: u32| format!("testsrc2=s={s}:r={r}");
|
|
let m = |s: &str, r: u32| format!("mandelbrot=s={s}:r={r}");
|
|
let n = |s: &str, r: u32| format!("testsrc2=s={s}:r={r},noise=alls=60:allf=t,lutyuv=y=clip(val\\,24\\,231):u=clip(val\\,24\\,231):v=clip(val\\,24\\,231)");
|
|
vec![
|
|
// DNxHD (HD profile)
|
|
Spec::new("hd1237_testsrc", t("1920x1080", 25), 2, &["-b:v", "120M", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hd1235_mandel", m("1920x1080", 25), 2, &["-b:v", "185M", "-pix_fmt", "yuv422p10le"], "yuv422p10le"),
|
|
Spec::new("hd1238_noise", n("1920x1080", 25), 2, &["-b:v", "185M", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hd1253_testsrc", t("1920x1080", 25), 2, &["-b:v", "36M", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hd1250_noise", n("1280x720", 50), 2, &["-b:v", "90M", "-pix_fmt", "yuv422p10le"], "yuv422p10le"),
|
|
Spec::new("hd1251_testsrc", t("1280x720", 50), 2, &["-b:v", "90M", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hd1252_mandel", m("1280x720", 50), 2, &["-b:v", "60M", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hd1258_testsrc", t("960x720", 50), 2, &["-b:v", "60M", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hd1259_testsrc", t("1440x1080", 25), 2, &["-b:v", "84M", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hd1242_interlaced", t("1920x1080", 25), 2, &["-flags", "+ildct", "-b:v", "120M", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hd1241_interlaced", n("1920x1080", 25), 2, &["-flags", "+ildct", "-b:v", "185M", "-pix_fmt", "yuv422p10le"], "yuv422p10le"),
|
|
Spec::new("hd1243_interlaced", m("1920x1080", 25), 2, &["-flags", "+ildct", "-b:v", "220M", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hd1244_interlaced", t("1440x1080", 25), 2, &["-flags", "+ildct", "-b:v", "120M", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
// DNxHR (RI profile)
|
|
Spec::new("hr_lb_testsrc", t("1920x1080", 25), 2, &["-profile:v", "dnxhr_lb", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hr_sq_mandel", m("1920x1080", 25), 2, &["-profile:v", "dnxhr_sq", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hr_hq_noise", n("1920x1080", 25), 2, &["-profile:v", "dnxhr_hq", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hr_hqx_mandel", m("1920x1080", 25), 2, &["-profile:v", "dnxhr_hqx", "-pix_fmt", "yuv422p10le"], "yuv422p10le"),
|
|
Spec::new("hr_hqx_noise_odd", n("1000x562", 25), 2, &["-profile:v", "dnxhr_hqx", "-pix_fmt", "yuv422p10le"], "yuv422p10le"),
|
|
Spec::new("hr_444_testsrc", t("1280x720", 25), 2, &["-profile:v", "dnxhr_444", "-pix_fmt", "yuv444p10le"], "yuv444p10le"),
|
|
Spec::new("hr_444_rgb_mandel", m("640x360", 25), 2, &["-profile:v", "dnxhr_444", "-pix_fmt", "gbrp10le"], "gbrp10le"),
|
|
Spec::new("hr_hq_small", t("256x120", 25), 2, &["-profile:v", "dnxhr_hq", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
Spec::new("hr_sq_uhd", t("3840x2160", 25), 1, &["-profile:v", "dnxhr_sq", "-pix_fmt", "yuv422p"], "yuv422p"),
|
|
]
|
|
}
|
|
|
|
/// Generate every fixture (used by `cargo xtask fixtures dnx`).
|
|
#[test]
|
|
#[ignore]
|
|
fn generate_fixtures() {
|
|
let Some(ff) = ffmpeg() else { return };
|
|
for s in specs() {
|
|
make(&ff, &s);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decode_matches_ffmpeg() {
|
|
let Some(ff) = ffmpeg() else { return };
|
|
let mut worst = 0;
|
|
for spec in specs() {
|
|
let mov = make(&ff, &spec);
|
|
let pk = packets(&ff, &mov);
|
|
assert_eq!(pk.len(), spec.frames as usize, "{}", spec.name);
|
|
let raw = reference(&ff, &mov, spec.pix_fmt);
|
|
let mut st = Stats::default();
|
|
let mut off = 0;
|
|
for (i, p) in pk.iter().enumerate() {
|
|
let f =
|
|
decode_frame_with(p, &DecodeOptions { threads: i % 2 == 0, ..Default::default() }).unwrap_or_else(|e| panic!("{} frame {i}: {e}", spec.name));
|
|
let ours = planes(&f, spec.pix_fmt);
|
|
let n = frame_samples(&f);
|
|
st.add(&ours, &raw[off..off + n]);
|
|
off += n;
|
|
}
|
|
assert_eq!(off, raw.len(), "{}: frame size mismatch", spec.name);
|
|
println!(
|
|
"{:<22} cid {} max {} mean {:.5} exact {:.3}% beyond ±1: {}",
|
|
spec.name,
|
|
filmcraft_dnx::probe(&pk[0]).unwrap().cid,
|
|
st.max,
|
|
st.mean(),
|
|
st.exact_pct(),
|
|
st.over1
|
|
);
|
|
worst = worst.max(st.max);
|
|
// the remaining differences are ffmpeg's integer IDCT: ±1, at most ~50 samples per million at ±2
|
|
assert!(st.over1 * 10_000 <= st.n, "{}: {} samples beyond ±1", spec.name, st.over1);
|
|
}
|
|
assert!(worst <= 2, "max error {worst}");
|
|
}
|