mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-29 20:47:00 -06:00
Prevent concurrent fixture producers from replacing files held open by external decoders. Add a four-consumer Windows sharing regression. Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
198 lines
6.8 KiB
Rust
198 lines
6.8 KiB
Rust
//! Bit-exactness against ffmpeg's decoder on libx264-generated fixtures (skipped when ffmpeg is absent).
|
|
|
|
mod 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 = filmcraft_h264::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 (`cargo xtask fixtures`).
|
|
#[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))];
|
|
filmcraft_testkit::fixtures::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 filmcraft_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();
|
|
}
|