Files
filmcraft/crates/dnx/tests/oracle_decode.rs
Brandon ThomasandClaude Opus 5.5 226c139741 M9.6: DNxHD/DNxHR (SMPTE ST 2019-1 VC-3) decoder and DNxHR encoder
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>
2026-10-01 10:53:02 -04:00

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}");
}