mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
craft-video: FilmCraft's later H.264 encoder work (High 10, threads, sub-pel diamond) and its H.264 and ProRes tests * craft-video: H.264 High 10 and High 10 Intra in the encoder The encoder is generic over the sample type (u8 at 8 bits, u16 at 9-10 bits): QP' = QP + QpBdOffset for quantisation and lambdas, mb_qp_delta range and wrap, chroma QP mapping, deblocking thresholds scaled to the bit depth, clipping in prediction, interpolation and reconstruction. SPS bit depths, profile_idc 110, constraint_set3 for High 10 Intra, avcC high-profile fields, the High 10 cpbBrVclFactor for level choice. New YuvFrame16 / Encoder::encode16, EncoderConfig::bit_depth / intra_only. 8-bit output is unchanged. Merged onto craft-video's copy: its input stride checks, B-frame timestamp check, configuration validation, level 6.2 refusal, motion search start candidates and codec_params are kept. Ported from storytold/filmcraft#1580 (commit d24ce15e). * craft-video: the H.264 encoder honours threads: 1 and reuses MbInfo buffers; raw-YUV bench - A pool is built for every thread count, including 1: rayon calls outside install run on the global pool (all cores), so threads: 1 used to run on every core. A pool that cannot be built is an InvalidConfig error. - Up to eight macroblock-info buffers are recycled between pictures and cleared on reuse, instead of allocating one per picture. - examples/h264_h264enc_bench.rs encodes a raw YUV file and reports speed; its argument checks run as tests. Ported from storytold/filmcraft#3205 (merge 08f8075a). * craft-video: H.264 encoder sub-pel refinement on a 4-point diamond; 16x8 and 8x16 only after 8x8 beat 16x16 Sub-pel refinement tries the four axis neighbours instead of all eight, and the 16x8 / 8x16 partitions are searched only when 8x8 beat 16x16: fewer SATD evaluations for about the same decisions. craft-video's own motion search start candidates are kept. Ported from storytold/filmcraft#3207 (merge 8db5334c). * craft-video: FilmCraft's H.264 encoder tests; EncoderConfig::writer names the program in the SEI The encoder had no integration tests here. FilmCraft's come along as the h264_encode test target: ffmpeg/ffprobe oracle decodes (8-bit and High 10, bit-exact against the reconstruction), pinned decisions, CRF level choice and the settings SEI, thread counts and VBR overshoot. EncoderConfig::writer is the program and version the identification SEI names (craft-h264enc and this crate's version by default), so an application's files keep naming it. Two expectations differ from FilmCraft's copy: the pinned slice hashes (this encoder also starts the 16x16 search from the skip, zero and co-located vectors; with those left out and FilmCraft's writer name, it gives FilmCraft's four hashes exactly) and a CRF that is not a number, which this encoder refuses. * craft-video: FilmCraft's ProRes tests (oracle decode and encode, round trip, fuzz, perf) craft-video's ProRes copy came with only its hostile-input test. FilmCraft's ffmpeg oracle decode and encode tests, round trips, mutation fuzz and the ignored throughput tests come along; craft-video's own fuzz_hostile stays. Frame::new returns a Result here, so the tests unwrap it. --------- Co-authored-by: JordyUSA <75485565+JordyUSA@users.noreply.github.com> Co-authored-by: vangel76 <vangel1976@googlemail.com>
126 lines
4.2 KiB
Rust
126 lines
4.2 KiB
Rust
//! Robustness: mutated and random inputs must never panic (errors are fine).
|
|
|
|
use crate::common;
|
|
use craft_video::prores::*;
|
|
|
|
struct Rng(u64);
|
|
impl Rng {
|
|
fn next(&mut self) -> u64 {
|
|
self.0 ^= self.0 << 13;
|
|
self.0 ^= self.0 >> 7;
|
|
self.0 ^= self.0 << 17;
|
|
self.0
|
|
}
|
|
fn below(&mut self, n: usize) -> usize {
|
|
(self.next() % n.max(1) as u64) as usize
|
|
}
|
|
}
|
|
|
|
fn seeds() -> Vec<Vec<u8>> {
|
|
let mut v = Vec::new();
|
|
for (p, w, h) in [(Profile::Proxy, 96u32, 48u32), (Profile::Hq, 70, 38), (Profile::P4444, 64, 32), (Profile::P4444Xq, 48, 20)] {
|
|
let chroma = p.chroma();
|
|
let mut f = Frame::new(w, h, chroma, 10, chroma == ChromaFormat::Yuv444).unwrap();
|
|
for (i, s) in f.y.iter_mut().enumerate() {
|
|
*s = (64 + (i * 37) % 800) as u16;
|
|
}
|
|
if let Some(a) = f.alpha.as_mut() {
|
|
for (i, s) in a.iter_mut().enumerate() {
|
|
*s = if i % 5 == 0 { 1023 } else { (i % 1024) as u16 };
|
|
}
|
|
}
|
|
let mut cfg = EncoderConfig::new(p, w, h);
|
|
cfg.log2_slice_mbs = (w % 4) as u8;
|
|
v.push(Encoder::with_config(cfg).encode(&f).unwrap());
|
|
}
|
|
// An interlaced ffmpeg stream, when fixtures from the oracle tests are present.
|
|
if let Some(ff) = common::ffmpeg() {
|
|
let spec = common::Spec::new(
|
|
"fuzz_seed_interlaced",
|
|
"testsrc2=s=64x48:r=25,setfield=tff,format=yuv422p10le",
|
|
1,
|
|
&["-c:v", "prores_ks", "-profile:v", "2", "-flags", "+ildct+ilme"],
|
|
);
|
|
let mov = common::make(&ff, &spec);
|
|
v.extend(common::packets(&ff, &mov));
|
|
}
|
|
v
|
|
}
|
|
|
|
#[test]
|
|
fn mutated_streams_never_panic() {
|
|
let seeds = seeds();
|
|
for s in &seeds {
|
|
decode_frame(s).expect("seed decodes");
|
|
}
|
|
let mut rng = Rng(0x9e3779b97f4a7c15);
|
|
let mut decoded_ok = 0;
|
|
let iters = 3000;
|
|
for i in 0..iters {
|
|
let mut d = seeds[i % seeds.len()].clone();
|
|
match rng.below(6) {
|
|
0 => {
|
|
for _ in 0..1 + rng.below(8) {
|
|
let p = rng.below(d.len());
|
|
d[p] ^= 1 << rng.below(8);
|
|
}
|
|
}
|
|
1 => {
|
|
for _ in 0..1 + rng.below(4) {
|
|
let p = rng.below(d.len());
|
|
d[p] = rng.next() as u8;
|
|
}
|
|
}
|
|
2 => {
|
|
let n = rng.below(d.len());
|
|
d.truncate(n);
|
|
if d.len() >= 4 && rng.below(2) == 0 {
|
|
// keep the frame size field consistent with the truncation
|
|
let n = d.len() as u32;
|
|
d[..4].copy_from_slice(&n.to_be_bytes());
|
|
}
|
|
}
|
|
3 => {
|
|
// header fields
|
|
let p = 8 + rng.below(20.min(d.len() - 8));
|
|
d[p] = rng.next() as u8;
|
|
}
|
|
4 => {
|
|
// slice region garbage
|
|
let start = 150.min(d.len() - 1) + rng.below(d.len().saturating_sub(150).max(1));
|
|
for b in d.iter_mut().skip(start).take(1 + rng.below(64)) {
|
|
*b = rng.next() as u8;
|
|
}
|
|
}
|
|
_ => {
|
|
let p = rng.below(d.len());
|
|
let extra: Vec<u8> = (0..1 + rng.below(16)).map(|_| rng.next() as u8).collect();
|
|
d.splice(p..p, extra);
|
|
}
|
|
}
|
|
for depth in [None, Some(8), Some(16)] {
|
|
let opts = DecodeOptions { bit_depth: depth, threads: i % 2 == 0 };
|
|
if let Ok(f) = decode_frame_with(&d, &opts) {
|
|
decoded_ok += 1;
|
|
assert_eq!(f.y.len(), f.width as usize * f.height as usize);
|
|
}
|
|
}
|
|
let _ = probe(&d);
|
|
}
|
|
println!("{decoded_ok} of {} mutated decodes succeeded", iters * 3);
|
|
}
|
|
|
|
#[test]
|
|
fn random_bytes_never_panic() {
|
|
let mut rng = Rng(12345);
|
|
for _ in 0..2000 {
|
|
let n = rng.below(400);
|
|
let mut d: Vec<u8> = (0..n).map(|_| rng.next() as u8).collect();
|
|
if d.len() >= 8 && rng.below(2) == 0 {
|
|
d[..4].copy_from_slice(&(n as u32).to_be_bytes());
|
|
d[4..8].copy_from_slice(b"icpf");
|
|
}
|
|
let _ = decode_frame(&d);
|
|
}
|
|
}
|