Files
craft-libs/crates/video/tests/h264_decode/conformance.rs
Dylan M. Taylor 0226f1d03e craft-video: H.264 decoder from FilmCraft, with DeckCraft's hardening (h264-decode) (#834)
* craft-video: H.264 decoder from FilmCraft (`h264-decode` feature)

FilmCraft's crates/h264 at 4c0d2ad7 becomes craft_video::h264::decode. The encoder moves to
h264/encode and is still exported from craft_video::h264. Feature h264 now turns on h264-decode
and h264-encode, so either half can be built without the other.

Without the threads feature a reference row that was never published read through expect();
it now reads as a gray row. 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: H.264 decoder hardening from DeckCraft's copy

- The picture store (DPB, current picture, pictures in flight or queued for output) is checked
  against a memory budget before a picture is allocated; Decoder::set_memory_budget.
- configure_avcc checks every entry before handling any: SPS NAL units in the first list, PPS in
  the second. A picture slice in the record starts no picture.
- The inverse transforms clamp coefficients to the spec's r range instead of overflowing.
- Temporal direct motion vectors are scaled in i32 and clipped to the spec range.
- slice_qp_delta is added with a checked add.
- A picture job sets up its picture state inside the panic guard.
2026-11-11 15:25:33 -05:00

205 lines
6.8 KiB
Rust

//! Bit-exactness against ffmpeg's decoder on libx264-generated fixtures (skipped when ffmpeg is absent).
use crate::common;
macro_rules! fixture_tests {
($($name:ident),* $(,)?) => {
$(
#[test]
fn $name() {
match common::check_fixture(stringify!($name)) {
Ok(true) => {}
Ok(false) => eprintln!("skipped {}", stringify!($name)),
Err(e) => panic!("{e}"),
}
}
)*
};
}
fixture_tests!(
intra_cavlc,
intra_cavlc_noise,
intra_cavlc_8x8,
intra_cavlc_cqm,
p_cavlc_nodeblock,
baseline_qcif,
baseline_cif_noise,
cavlc_b,
cavlc_b_temporal,
cavlc_weightp,
slices4_cavlc,
qp1_cavlc,
main_cabac_b,
high_720p,
high_1080p,
crop_1918x1078,
bpyramid,
weightp2,
weightb,
direct_temporal,
direct_spatial,
ref4,
no_deblock,
deblock_m2_2,
cqm_jvt,
slices4,
keyint10,
open_gop,
constrained_intra,
no8x8dct,
qp50,
qp1,
vt_high,
vt_main,
vt_baseline,
vt_high_1080p,
);
/// Draft mode (reduced-resolution playback) skips deblocking of non-reference pictures only:
/// every other picture stays bit-exact with ffmpeg, single- and frame-threaded, the skipped ones
/// are flagged, and they are the only ones that differ.
#[test]
fn draft_mode_changes_only_flagged_non_reference_pictures() {
let mut checked = 0;
for name in ["main_cabac_b", "bpyramid", "cavlc_b", "direct_temporal", "weightb"] {
let f = common::fixture(name);
let Some((h264, yuv)) = common::ensure(f) else {
eprintln!("skipped {name}");
continue;
};
let reference = std::fs::read(&yuv).unwrap();
let (w, h) = (f.width as usize, f.height as usize);
let fsize = w * h + 2 * w.div_ceil(2) * h.div_ceil(2);
for threads in [1, 0] {
let pics = common::decode_file_opts(&h264, threads, true).unwrap_or_else(|(au, e, _)| panic!("{name}: error at {au}: {e}"));
common::check_pts(&pics).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(pics.len() * fsize, reference.len(), "{name}: picture count");
let (mut draft, mut changed) = (0, 0);
for (i, p) in pics.iter().enumerate() {
let r = &reference[i * fsize..(i + 1) * fsize];
let same = p.y == r[..w * h] && [&p.u[..], &p.v[..]].concat() == r[w * h..];
if p.draft {
draft += 1;
changed += !same as usize;
} else {
assert!(same, "{name} (threads {threads}): picture {i} (not draft) differs from the reference");
}
}
assert!(draft > 0, "{name}: has non-reference pictures");
if name != "no_deblock" {
assert!(changed > 0, "{name}: skipping deblocking changes some draft picture");
}
checked += 1;
}
}
if checked == 0 {
eprintln!("skipped: no fixtures");
}
}
/// Print which coding tools each fixture exercises (run with `-- --ignored --nocapture`).
#[test]
#[ignore]
fn coverage_report() {
for f in common::FIXTURES {
let Some((h264, _)) = common::ensure(f) else { return };
let data = std::fs::read(h264).unwrap();
let mut dec = craft_video::h264::decode::Decoder::new();
for au in common::split_access_units(&data) {
dec.decode(au, 0).unwrap();
}
dec.flush();
let s = dec.stats();
println!(
"{:<20} pics {:>3} cavlc/cabac {:>3}/{:<3} I/P/B {:>3}/{:>3}/{:>3} i4 {:>6} i8 {:>6} i16 {:>6} pcm {:>4} pskip {:>6} bskip {:>6} bdirect {:>5} inter {:>6} t8 {:>6} mmco {:>3} lt {} gaps {} wp {} tdirect {}",
f.name,
s.pictures,
s.slices_cavlc,
s.slices_cabac,
s.slices_i,
s.slices_p,
s.slices_b,
s.mb_i4x4,
s.mb_i8x8,
s.mb_i16x16,
s.mb_pcm,
s.mb_p_skip,
s.mb_b_skip,
s.mb_b_direct16x16,
s.mb_inter,
s.mb_inter_8x8_transform,
s.mmco_ops,
s.long_term_marks,
s.frame_num_gaps,
s.weighted_slices,
s.temporal_direct_slices
);
}
}
/// Pre-generate every fixture and its reference decode .
#[test]
#[ignore]
fn generate_fixtures() {
let dir = common::fixtures_dir();
for f in common::FIXTURES {
let outs = [dir.join(format!("{}.h264", f.name)), dir.join(format!("{}.yuv", f.name))];
craft_media_testkit::generate_and_report(&format!("h264/{}", f.name), &outs, || common::ensure(f));
}
}
/// Concurrent consumers must publish one complete fixture without replacing an open file.
#[test]
fn concurrent_fixture_publication_is_safe() {
if craft_media_testkit::ffmpeg_or_skip("concurrent_fixture_publication").is_none() {
return;
}
let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let name = Box::leak(format!("concurrent_fixture_{}_{stamp}", std::process::id()).into_boxed_str());
let fixture = common::Fixture {
name,
source: "testsrc2",
width: 64,
height: 48,
frames: 2,
filter: "",
args: &["-preset", "ultrafast", "-x264-params", "keyint=1"],
};
let open_published = |path: &std::path::Path| {
let mut options = std::fs::OpenOptions::new();
options.read(true);
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
// Like the external decoder, allow reads but deny replacement while open.
options.share_mode(1);
}
options.open(path).unwrap()
};
let start = std::sync::Barrier::new(4);
let results = std::thread::scope(|scope| {
let jobs: Vec<_> = (0..4)
.map(|_| {
scope.spawn(|| {
start.wait();
let paths = common::ensure(&fixture).expect("software fixture must generate");
// Keep both published files open until every producer has finished.
let files = (open_published(&paths.0), open_published(&paths.1));
(paths, files)
})
})
.collect();
jobs.into_iter().map(|j| j.join().expect("fixture producer panicked")).collect::<Vec<_>>()
});
let paths = results[0].0.clone();
for (got, _) in &results {
assert_eq!(got, &paths);
}
assert!(!std::fs::read(&paths.0).unwrap().is_empty());
assert_eq!(std::fs::metadata(&paths.1).unwrap().len(), 2 * (64 * 48 + 2 * 32 * 24));
drop(results);
std::fs::remove_file(paths.0).unwrap();
std::fs::remove_file(paths.1).unwrap();
}