mirror of
https://github.com/storytold/filmcraft.git
synced 2026-11-08 01:20:18 +00:00
* Tests: one integration-test binary per crate Each crate with several tests/*.rs files keeps them as modules of tests/it/, so cargo links one test executable per crate instead of one per file. Files that change process-wide state or time a workload stay binaries of their own. * Profiles: line tables for workspace crates, no debug info for dependencies * Windows ARM64: nightly and on release pushes instead of every push to main * xtask ci: clippy and tests in the dev profile The release profile's thin LTO and single codegen unit made linking the test binaries take most of the gate. The codec and audio crates stay at opt-level 3 in the dev profile, so their tests run at release speed. * xtask ci: nextest, doctests, no examples, one wasm check Tests run with cargo nextest when it is installed (cargo test otherwise) and build with --lib --bins --tests, so examples aren't linked; doctests run separately. The wasm check keeps going past the first failing crate. VP8 fixture generation locks across processes, since nextest runs each test in its own. * VP9 synthetic fixtures: write aside and rename A test in another process could read a fixture ffmpeg was still writing. * Fuzz tests: a third of each sweep by default, all of it with FILMCRAFT_FUZZ_ITERS Every seed, mutation kind and thread count still runs. Header bytes each get one overwrite value and two bit flips in rotation, so every value and bit index still occurs across the region. FILMCRAFT_FUZZ_ITERS=N runs the whole sweep plus N random rounds per seed, as before. * Platform fallback tests: one failure point per hybrid path The failure-anywhere test injected a failure at four points around every restart point; the hybrid's paths differ only in the first GOP, around the second and last restart points, at the end and never. The replay test needs the first two GOPs. * Export tests: shorter ranges with the same checks The bitrate test measures 0.5–1.5 s, where each preset reaches its own target (on 0–3 s the simple demo left two presets at the same rate). The every-preset test exports three frames per preset. * Audio tests: effects in parallel, a table-driven FFT for the Opus quality metric The extreme-settings test keeps both settings and the 40 s tail for every effect, one thread each. The Opus quality metric's FFT builds its twiddles once and allocates nothing per transform (same algorithm and summation order). * CI: dev-profile gate with nextest xtask ci no longer builds with --release, so the release-profile overrides go; nextest is installed for the test step. * CI: apt packages through the cached install-apt-packages action * Review fixes: fuzz tests with a panic hook in their own binaries, loud fixture lock The fuzz tests of av1, h264, hevc, mpeg2v, vp8 and vp9 count panics through a process-wide hook, so a panic in another test of the same process would fail them; they are binaries of their own again. The VP8 fixture lock fails the test instead of skipping it silently, and the full-sweep doc comments say how many truncation points the full run samples. * Fuzz tests: rotate the default subset per run (FILMCRAFT_FUZZ_SEED replays one) * platform tests: decode-switch tests in their own binaries, one NVENC lock
494 lines
22 KiB
Rust
494 lines
22 KiB
Rust
//! VA-API decoding against our software decoders (Linux): every picture of H.264 and HEVC fixtures
|
|
//! covering the stream features the hardware path takes (H.264: CAVLC and CABAC, B-pyramids,
|
|
//! several references, weighted prediction, temporal direct, custom scaling matrices, several
|
|
//! slices per picture; HEVC Main and Main 10: open GOP with RASL pictures, scaling lists, several
|
|
//! slices with wavefront parallel processing, weighted prediction, transform skip and AMP; 1080p /
|
|
//! 2160p) must be identical (bit-exact planes, colour, pixel aspect, pts and presentation
|
|
//! order), also after `reset` + reseek and a mid-stream `flush`; a forced mid-stream failure must
|
|
//! continue in software with the software decoder's exact output; what the hardware path does not
|
|
//! take (interlaced and 10-bit H.264, 4:2:2 HEVC) is declined; damaged samples and parameter sets give
|
|
//! errors or fall back, never crash or hang. Skips without ffmpeg (fixture generator) or without a
|
|
//! VA-API driver. Its own test binary: one test turns the process-wide Hardware decoding switch off
|
|
//! and on.
|
|
#![cfg(target_os = "linux")]
|
|
|
|
#[path = "it/common/mod.rs"]
|
|
mod common;
|
|
|
|
use std::time::Duration;
|
|
|
|
use common::*;
|
|
use filmcraft_codecs::VideoDecoder;
|
|
use filmcraft_codecs::hw::NalStreamInfo;
|
|
use filmcraft_platform::HybridDecoder;
|
|
use filmcraft_platform::vaapi::VaDecoder;
|
|
|
|
/// The hardware decoder for a stream, or `None` (skip) when this machine has none for it.
|
|
fn hardware(s: &Stream) -> Option<Box<dyn VideoDecoder>> {
|
|
match filmcraft_platform::vaapi_factory(&s.entry) {
|
|
Some(Ok(d)) => {
|
|
assert!(d.name().starts_with("VA-API"), "{}", d.name());
|
|
Some(d)
|
|
}
|
|
Some(Err(e)) => panic!("factory error: {e}"),
|
|
None => {
|
|
eprintln!("SKIPPED: no VA-API hardware decoder for {}", s.entry.codec.name());
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn software(s: &Stream) -> Box<dyn VideoDecoder> {
|
|
filmcraft_codecs::software_video_decoder(&s.entry).unwrap()
|
|
}
|
|
|
|
/// H.264 fixtures for the stream features the hardware path takes: (file, x264 profile, x264
|
|
/// parameters). All 640x360 (coded 368: cropping), three seconds at 24 fps, keyframes every second.
|
|
const H264: &[(&str, &str, &str)] = &[
|
|
("va_h264_cbp_slices.mp4", "baseline", "keyint=24:min-keyint=24:scenecut=0:slices=3:ref=3"),
|
|
("va_h264_main_weighted.mp4", "main", "bframes=3:b-pyramid=normal:keyint=24:min-keyint=24:scenecut=0:ref=4:weightp=2:weightb=1"),
|
|
("va_h264_temporal_direct.mp4", "high", "bframes=2:b-pyramid=strict:direct=temporal:keyint=24:min-keyint=24:scenecut=0:ref=3:weightb=0"),
|
|
("va_h264_cqm_jvt.mp4", "high", "bframes=3:keyint=24:min-keyint=24:scenecut=0:cqm=jvt:8x8dct=1:slices=2"),
|
|
("va_h264_open_gop.mp4", "high", "bframes=3:b-pyramid=normal:keyint=24:min-keyint=24:scenecut=0:open-gop=1"),
|
|
];
|
|
|
|
fn h264_fixture(ff: &std::path::Path, name: &str, profile: &str, params: &str) -> Option<std::path::PathBuf> {
|
|
let src = "testsrc2=s=640x360:r=24:d=3,noise=alls=12:allf=t";
|
|
fixture(ff, name, &["-f", "lavfi", "-i", src, "-c:v", "libx264", "-profile:v", profile, "-preset", "fast", "-x264-params", params, "-pix_fmt", "yuv420p"])
|
|
}
|
|
|
|
/// HEVC fixtures for the stream features the hardware path takes: (file, pixel format, x265
|
|
/// parameters). 640x360, three seconds at 24 fps, keyframes every second.
|
|
const HEVC: &[(&str, &str, &str)] = &[
|
|
("va_hevc_scaling_lists.mp4", "yuv420p", "log-level=error:bframes=3:keyint=24:min-keyint=24:scenecut=0:scaling-list=default"),
|
|
("va_hevc_slices_wpp.mp4", "yuv420p10le", "log-level=error:bframes=3:keyint=24:min-keyint=24:scenecut=0:slices=3:wpp=1"),
|
|
("va_hevc_weighted.mp4", "yuv420p", "log-level=error:bframes=3:keyint=24:min-keyint=24:scenecut=0:weightp=1:weightb=1:ref=4"),
|
|
("va_hevc_tskip_amp.mp4", "yuv420p10le", "log-level=error:bframes=4:b-pyramid=1:keyint=24:min-keyint=24:scenecut=0:tskip=1:amp=1:rect=1:open-gop=1"),
|
|
];
|
|
|
|
fn hevc_fixture(ff: &std::path::Path, name: &str, pix_fmt: &str, params: &str) -> Option<std::path::PathBuf> {
|
|
let src = "testsrc2=s=640x360:r=24:d=3,noise=alls=12:allf=t";
|
|
let profile = if pix_fmt.contains("10") { "main10" } else { "main" };
|
|
fixture(
|
|
ff,
|
|
name,
|
|
&["-f", "lavfi", "-i", src, "-c:v", "libx265", "-preset", "fast", "-profile:v", profile, "-tag:v", "hvc1", "-x265-params", params, "-pix_fmt", pix_fmt],
|
|
)
|
|
}
|
|
|
|
/// The small fixtures: the shared H.264 High, HEVC Main (open GOP) and Main 10 ones and the
|
|
/// feature fixtures above.
|
|
fn small_streams(ff: &std::path::Path) -> Vec<(String, Stream)> {
|
|
let mut out = Vec::new();
|
|
for name in ["h264_high.mp4", "hevc_main.mp4", "hevc_main10.mp4"] {
|
|
if let Some(path) = named(ff, name) {
|
|
out.push((name.to_string(), read_stream(&path)));
|
|
}
|
|
}
|
|
for (name, profile, params) in H264 {
|
|
if let Some(path) = h264_fixture(ff, name, profile, params) {
|
|
out.push((name.to_string(), read_stream(&path)));
|
|
}
|
|
}
|
|
for (name, pix_fmt, params) in HEVC {
|
|
if let Some(path) = hevc_fixture(ff, name, pix_fmt, params) {
|
|
out.push((name.to_string(), read_stream(&path)));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Every picture of `s` from the hardware decoder equals the software decoder's: whole stream,
|
|
/// after `reset` + reseek to each later sync sample, after a mid-stream `flush`, and a full pass
|
|
/// after all the resets.
|
|
fn parity(name: &str, s: &Stream) {
|
|
let Some(mut hw) = hardware(s) else { return };
|
|
let mut sw = software(s);
|
|
let a = decode_all(hw.as_mut(), &s.samples);
|
|
let b = decode_all(sw.as_mut(), &s.samples);
|
|
assert!(a.len() >= 40, "{name}: {} pictures", a.len());
|
|
assert_same(name, &a, &b);
|
|
assert!(hw.name().starts_with("VA-API"), "{name}: stayed in hardware ({})", hw.name());
|
|
|
|
// reset + reseek to each later sync sample, decode a stretch. After a seek to a non-IDR I picture
|
|
// (open GOP), the pictures shown before it are predicted from frames that were never decoded:
|
|
// both decoders conceal them (the software decoder from mid-gray stand-ins, the hardware also
|
|
// from motion data those stand-ins do not have), so only the pictures from the seek point on
|
|
// must match, as they must.
|
|
let info = NalStreamInfo::from_entry(&s.entry).unwrap().unwrap();
|
|
let syncs: Vec<usize> = (1..s.samples.len()).filter(|&i| s.sync[i]).collect();
|
|
assert!(!syncs.is_empty(), "{name}: more than one GOP");
|
|
for &k in syncs.iter().rev() {
|
|
let end = (k + 17).min(s.samples.len());
|
|
for d in [&mut hw, &mut sw] {
|
|
d.reset();
|
|
}
|
|
let mut a = decode_all(hw.as_mut(), &s.samples[k..end]);
|
|
let mut b = decode_all(sw.as_mut(), &s.samples[k..end]);
|
|
assert_eq!(a.len(), b.len(), "{name} from sample {k}: pictures");
|
|
if info.codec == filmcraft_codecs::hw::NalCodec::H264 && !info.nal_types(&s.samples[k].0).contains(&5) {
|
|
let start = s.samples[k].1;
|
|
a.retain(|f| f.pts >= start);
|
|
b.retain(|f| f.pts >= start);
|
|
}
|
|
assert!(!a.is_empty(), "{name}: pictures after seeking to {k}");
|
|
assert_same(&format!("{name} from sample {k}"), &a, &b);
|
|
}
|
|
// a flush in the middle of a GOP, then carrying on: H.264 only, where references survive a
|
|
// flush (an HEVC flush ends the stream; the GOP cache always seeks after one)
|
|
if info.codec == filmcraft_codecs::hw::NalCodec::H264 {
|
|
for d in [&mut hw, &mut sw] {
|
|
d.reset();
|
|
}
|
|
let mid = s.samples.len() / 2;
|
|
let mut a = Vec::new();
|
|
let mut b = Vec::new();
|
|
for (d, out) in [(&mut hw, &mut a), (&mut sw, &mut b)] {
|
|
for (smp, pts) in &s.samples[..mid] {
|
|
out.extend(d.decode(smp, *pts).unwrap());
|
|
}
|
|
out.extend(d.flush());
|
|
out.extend(decode_all(d.as_mut(), &s.samples[mid..]));
|
|
}
|
|
assert_same(&format!("{name} with a flush at {mid}"), &a, &b);
|
|
}
|
|
// back to the start after seeking around: the whole stream again
|
|
hw.reset();
|
|
sw.reset();
|
|
assert_same(&format!("{name} after resets"), &decode_all(hw.as_mut(), &s.samples), &decode_all(sw.as_mut(), &s.samples));
|
|
assert!(hw.name().starts_with("VA-API"), "{name}: stayed in hardware ({})", hw.name());
|
|
// random-access / disposable answers match the software decoder's
|
|
for (smp, _) in &s.samples {
|
|
assert_eq!(hw.is_random_access(smp), sw.is_random_access(smp), "{name}");
|
|
assert_eq!(hw.is_disposable(smp), sw.is_disposable(smp), "{name}");
|
|
}
|
|
}
|
|
|
|
/// [`parity`] for large streams, with little memory: the two decoders run side by side and each
|
|
/// picture is compared and dropped as soon as both have output it (a 4K 10-bit stream held whole
|
|
/// takes gigabytes). The whole stream, then from the last sync sample after a reset.
|
|
fn parity_streaming(name: &str, s: &Stream) {
|
|
let Some(mut hw) = hardware(s) else { return };
|
|
let mut sw = software(s);
|
|
let last_sync = (1..s.samples.len()).rev().find(|&i| s.sync[i]).unwrap_or(0);
|
|
for start in [0, last_sync] {
|
|
hw.reset();
|
|
sw.reset();
|
|
let (mut a, mut b) = (std::collections::VecDeque::new(), std::collections::VecDeque::new());
|
|
let mut compared = 0usize;
|
|
let mut compare = |a: &mut std::collections::VecDeque<filmcraft_codecs::DecodedFrame>,
|
|
b: &mut std::collections::VecDeque<filmcraft_codecs::DecodedFrame>| {
|
|
while !a.is_empty() && !b.is_empty() {
|
|
let (Some(x), Some(y)) = (a.pop_front(), b.pop_front()) else { break };
|
|
assert_same(&format!("{name} from sample {start}"), &[x], &[y]);
|
|
compared += 1;
|
|
}
|
|
};
|
|
for (smp, pts) in &s.samples[start..] {
|
|
a.extend(hw.decode(smp, *pts).unwrap());
|
|
b.extend(sw.decode(smp, *pts).unwrap());
|
|
compare(&mut a, &mut b);
|
|
}
|
|
a.extend(hw.flush());
|
|
b.extend(sw.flush());
|
|
compare(&mut a, &mut b);
|
|
assert!(a.is_empty() && b.is_empty(), "{name} from sample {start}: picture counts differ");
|
|
assert!(compared >= s.samples.len().saturating_sub(start) / 2, "{name} from sample {start}: {compared} pictures");
|
|
assert!(hw.name().starts_with("VA-API"), "{name}: stayed in hardware ({})", hw.name());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bit_exact_with_the_software_decoder() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let streams = small_streams(&ff);
|
|
assert!(streams.len() > H264.len(), "fixtures");
|
|
for (name, s) in streams {
|
|
parity(&name, &s);
|
|
}
|
|
}
|
|
|
|
fn large(size: &str) {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let (_, h) = size.split_once('x').unwrap();
|
|
let src = format!("testsrc2=s={size}:r=24:d=2,noise=alls=12:allf=t");
|
|
let args = [
|
|
"-f",
|
|
"lavfi",
|
|
"-i",
|
|
&src,
|
|
"-c:v",
|
|
"libx264",
|
|
"-profile:v",
|
|
"high",
|
|
"-preset",
|
|
"veryfast",
|
|
"-x264-params",
|
|
"bframes=3:b-pyramid=normal:keyint=24:min-keyint=24:scenecut=0",
|
|
"-pix_fmt",
|
|
"yuv420p",
|
|
];
|
|
let Some(path) = fixture(&ff, &format!("h264_{h}.mp4"), &args) else { return };
|
|
parity_streaming(&format!("h264 {size}"), &read_stream(&path));
|
|
}
|
|
|
|
#[test]
|
|
fn h264_1080p_and_2160p_are_bit_exact() {
|
|
large("1920x1080");
|
|
large("3840x2160");
|
|
}
|
|
|
|
fn large_hevc(size: &str, pix_fmt: &str) {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let (_, h) = size.split_once('x').unwrap();
|
|
let src = format!("testsrc2=s={size}:r=24:d=2,noise=alls=12:allf=t");
|
|
let ten = pix_fmt.contains("10");
|
|
let args = [
|
|
"-f",
|
|
"lavfi",
|
|
"-i",
|
|
&src,
|
|
"-c:v",
|
|
"libx265",
|
|
"-preset",
|
|
"ultrafast",
|
|
"-profile:v",
|
|
if ten { "main10" } else { "main" },
|
|
"-tag:v",
|
|
"hvc1",
|
|
"-x265-params",
|
|
"log-level=error:bframes=4:keyint=24:min-keyint=24:scenecut=0:open-gop=1",
|
|
"-pix_fmt",
|
|
pix_fmt,
|
|
];
|
|
let name = format!("va_hevc_{}_{h}.mp4", if ten { "main10" } else { "main" });
|
|
let Some(path) = fixture(&ff, &name, &args) else { return };
|
|
parity_streaming(&name, &read_stream(&path));
|
|
}
|
|
|
|
#[test]
|
|
fn hevc_main_and_main10_1080p_and_2160p_are_bit_exact() {
|
|
for size in ["1920x1080", "3840x2160"] {
|
|
large_hevc(size, "yuv420p");
|
|
large_hevc(size, "yuv420p10le");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mid_stream_failure_continues_in_software() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
for (name, s) in small_streams(&ff) {
|
|
let name = name.as_str();
|
|
let info = NalStreamInfo::from_entry(&s.entry).unwrap().unwrap();
|
|
let Ok(_) = VaDecoder::new(info.clone()) else {
|
|
eprintln!("SKIPPED: no VA-API hardware decoder for {name}");
|
|
continue;
|
|
};
|
|
let reference = decode_all(software(&s).as_mut(), &s.samples);
|
|
assert!(!reference.is_empty(), "{name}");
|
|
// fail at the first sample, inside the first GOP, at a sync sample and just after one
|
|
let k = (1..s.samples.len()).find(|&i| s.sync[i]).unwrap();
|
|
for fail_at in [0, 5, k, k + 1, k + 9] {
|
|
let mut va = VaDecoder::new(info.clone()).unwrap();
|
|
va.fail_after(fail_at as u64);
|
|
let mut d = HybridDecoder::new(Box::new(va), s.entry.clone(), info.clone());
|
|
let before = filmcraft_codecs::hw::hw_stats().fallbacks;
|
|
let out = decode_all(&mut d, &s.samples);
|
|
assert!(!d.is_hardware(), "{name}: switched to software");
|
|
assert!(filmcraft_codecs::hw::hw_stats().fallbacks > before, "{name}: fallback counted");
|
|
assert_same(&format!("{name} failing at sample {fail_at}"), &out, &reference);
|
|
d.reset();
|
|
assert_same(&format!("{name} after the fallback and a reset"), &decode_all(&mut d, &s.samples), &reference);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What the hardware path does not take is declined (the software decoders keep it): interlaced
|
|
/// and 10-bit H.264, and HEVC (not decoded through VA-API yet).
|
|
#[test]
|
|
fn declines_what_the_hardware_path_does_not_take() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let src = "testsrc2=s=640x360:r=24:d=1";
|
|
let cases: [(&str, Vec<&str>); 2] = [
|
|
("va_h264_interlaced.mp4", vec!["-f", "lavfi", "-i", src, "-c:v", "libx264", "-x264-params", "interlaced=1:keyint=12", "-pix_fmt", "yuv420p"]),
|
|
("va_h264_10bit.mp4", vec!["-f", "lavfi", "-i", src, "-c:v", "libx264", "-profile:v", "high10", "-pix_fmt", "yuv420p10le"]),
|
|
];
|
|
for (name, args) in cases {
|
|
// an ffmpeg whose x264 lacks 10-bit leaves the fixture out
|
|
let Some(path) = fixture(&ff, name, &args) else { continue };
|
|
let s = read_stream(&path);
|
|
assert!(filmcraft_platform::vaapi_factory(&s.entry).is_none(), "{name}: declined");
|
|
}
|
|
let args = ["-f", "lavfi", "-i", src, "-c:v", "libx265", "-tag:v", "hvc1", "-x265-params", "log-level=error:keyint=12", "-pix_fmt", "yuv422p10le"];
|
|
if let Some(path) = fixture(&ff, "va_hevc_422.mp4", &args) {
|
|
let s = read_stream(&path);
|
|
assert!(filmcraft_platform::vaapi_factory(&s.entry).is_none(), "HEVC 4:2:2: declined");
|
|
}
|
|
// Settings ▸ Playback ▸ Hardware decoding Off
|
|
let Some(path) = named(&ff, "h264_high.mp4") else { return };
|
|
let s = read_stream(&path);
|
|
filmcraft_codecs::hw::set_hardware_decoding(false);
|
|
let off = filmcraft_platform::vaapi_factory(&s.entry).is_none();
|
|
filmcraft_codecs::hw::set_hardware_decoding(true);
|
|
assert!(off, "declined while hardware decoding is Off");
|
|
}
|
|
|
|
/// Run `f` on a thread; fail if it panics or takes longer than `limit`.
|
|
fn bounded(what: &str, limit: Duration, f: impl FnOnce() + Send + 'static) {
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
std::thread::spawn(move || {
|
|
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
|
|
let _ = tx.send(r.is_ok());
|
|
});
|
|
match rx.recv_timeout(limit) {
|
|
Ok(true) => {}
|
|
Ok(false) => panic!("{what}: panicked"),
|
|
Err(_) => panic!("{what}: hung"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn damaged_samples_and_parameter_sets_never_crash_or_hang() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
for (name, s) in small_streams(&ff) {
|
|
let name = name.as_str();
|
|
if hardware(&s).is_none() {
|
|
continue;
|
|
}
|
|
// damaged samples (bit flips, truncation, corrupt length prefixes): through the hybrid, and
|
|
// straight into the hardware decoder (no software to fall back to)
|
|
for seed in 1..=8u64 {
|
|
let samples = s.samples.clone();
|
|
let entry = s.entry.clone();
|
|
bounded(&format!("{name} seed {seed}"), Duration::from_secs(60), move || {
|
|
let mut seed = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
|
|
let mut d = filmcraft_platform::vaapi_factory(&entry).unwrap().unwrap();
|
|
let info = NalStreamInfo::from_entry(&entry).unwrap().unwrap();
|
|
let mut raw = VaDecoder::new(info).unwrap();
|
|
for (i, (smp, pts)) in samples.iter().enumerate().take(40) {
|
|
let mut m = smp.clone();
|
|
match xorshift(&mut seed) % 4 {
|
|
0 => {
|
|
for _ in 0..1 + xorshift(&mut seed) % 8 {
|
|
let at = (xorshift(&mut seed) as usize) % m.len().max(1);
|
|
if let Some(b) = m.get_mut(at) {
|
|
*b ^= 1 << (xorshift(&mut seed) % 8);
|
|
}
|
|
}
|
|
}
|
|
1 => m.truncate((xorshift(&mut seed) as usize) % m.len().max(1)),
|
|
2 if m.len() > 4 => m[..4].copy_from_slice(&(xorshift(&mut seed) as u32).to_be_bytes()),
|
|
_ => {}
|
|
}
|
|
let _ = d.decode(&m, *pts);
|
|
let _ = raw.decode(&m, *pts);
|
|
if i % 13 == 12 {
|
|
d.reset();
|
|
raw.reset();
|
|
}
|
|
}
|
|
let _ = d.flush();
|
|
let _ = raw.flush();
|
|
});
|
|
}
|
|
// damaged parameter sets: the decoder declines, errors or decodes; never crashes
|
|
let filmcraft_isobmff::CodecConfig::Avc(a) = &s.entry.codec else { continue };
|
|
let rec = a.to_bytes();
|
|
let mut seed = 0x1234_5678u64;
|
|
for round in 0..24 {
|
|
let mut r = rec.clone();
|
|
if round % 3 == 2 {
|
|
r.truncate(r.len() * (round + 1) / 30);
|
|
} else {
|
|
for _ in 0..1 + round % 4 {
|
|
let at = 6 + (xorshift(&mut seed) as usize) % r.len().saturating_sub(6).max(1);
|
|
if let Some(b) = r.get_mut(at) {
|
|
*b ^= 1 << (xorshift(&mut seed) % 8);
|
|
}
|
|
}
|
|
}
|
|
let samples: Vec<_> = s.samples.iter().take(12).cloned().collect();
|
|
bounded(&format!("{name} record round {round}"), Duration::from_secs(60), move || {
|
|
let Ok(info) = NalStreamInfo::from_avcc(&r) else { return };
|
|
let Ok(mut va) = VaDecoder::new(info) else { return };
|
|
for (smp, pts) in &samples {
|
|
if va.decode(smp, *pts).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
let _ = va.flush();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Creating and dropping decoders again and again (every clip on a timeline makes one) neither
|
|
/// fails nor leaks VA-API objects into the next one.
|
|
#[test]
|
|
fn repeated_initialisation() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let Some(path) = named(&ff, "h264_high.mp4") else { return };
|
|
let s = read_stream(&path);
|
|
let reference = decode_all(software(&s).as_mut(), &s.samples[..13]);
|
|
for round in 0..40 {
|
|
let Some(mut hw) = hardware(&s) else { return };
|
|
if round % 8 == 0 {
|
|
assert_same(&format!("round {round}"), &decode_all(hw.as_mut(), &s.samples[..13]), &reference);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Several decoders at once on different threads (the frame workers) give the software decoder's
|
|
/// pictures.
|
|
#[test]
|
|
fn concurrent_decoders() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let mut paths: Vec<_> = named(&ff, "h264_high.mp4").into_iter().collect();
|
|
for (name, profile, params) in H264.iter().take(3) {
|
|
paths.extend(h264_fixture(&ff, name, profile, params));
|
|
}
|
|
let jobs: Vec<_> = paths
|
|
.into_iter()
|
|
.chain(named(&ff, "h264_high.mp4"))
|
|
.map(|path| {
|
|
std::thread::spawn(move || {
|
|
let s = read_stream(&path);
|
|
let Some(mut hw) = hardware(&s) else { return };
|
|
let n = s.samples.len().min(40);
|
|
let a = decode_all(hw.as_mut(), &s.samples[..n]);
|
|
hw.reset();
|
|
let b = decode_all(hw.as_mut(), &s.samples[..n]);
|
|
let c = decode_all(software(&s).as_mut(), &s.samples[..n]);
|
|
assert_same("concurrent", &a, &c);
|
|
assert_same("concurrent, after a reset", &b, &c);
|
|
})
|
|
})
|
|
.collect();
|
|
for j in jobs {
|
|
j.join().unwrap();
|
|
}
|
|
}
|
|
|
|
/// A decoder used and moved between threads (decoders migrate between frame workers).
|
|
#[test]
|
|
fn decoder_moves_between_threads() {
|
|
let ff = filmcraft_testkit::require_ffmpeg!();
|
|
let Some(path) = named(&ff, "h264_high.mp4") else { return };
|
|
let s = read_stream(&path);
|
|
let Some(mut hw) = hardware(&s) else { return };
|
|
let reference = decode_all(software(&s).as_mut(), &s.samples[..30]);
|
|
let samples = s.samples.clone();
|
|
let (hw, out) = std::thread::spawn(move || {
|
|
let out = decode_all(hw.as_mut(), &samples[..30]);
|
|
(hw, out)
|
|
})
|
|
.join()
|
|
.unwrap();
|
|
assert_same("first thread", &out, &reference);
|
|
let mut hw = hw;
|
|
hw.reset();
|
|
let samples = s.samples.clone();
|
|
let out = std::thread::spawn(move || decode_all(hw.as_mut(), &samples[..30])).join().unwrap();
|
|
assert_same("second thread", &out, &reference);
|
|
}
|