mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-15 06:52:23 +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>
115 lines
4.1 KiB
Rust
115 lines
4.1 KiB
Rust
//! Decode/encode throughput. Run with
|
||
//! `cargo test --release -p craft-video --features prores --test prores perf:: -- --ignored --nocapture`.
|
||
|
||
use crate::common;
|
||
use craft_video::prores::*;
|
||
use std::time::Instant;
|
||
|
||
/// A 1080p frame whose HQ encoding uses the full nominal rate (detailed, noisy content).
|
||
fn busy_1080p(chroma: ChromaFormat) -> Frame {
|
||
let mut f = Frame::new(1920, 1080, chroma, 10, false).unwrap();
|
||
let mut s = 0x1234_5678u32;
|
||
let mut rnd = move || {
|
||
s ^= s << 13;
|
||
s ^= s >> 17;
|
||
s ^= s << 5;
|
||
s
|
||
};
|
||
let cw = f.chroma_width() as usize;
|
||
for y in 0..1080usize {
|
||
for x in 0..1920usize {
|
||
let base = 200 + ((x * 3 + y * 2) % 600) as u32;
|
||
f.y[y * 1920 + x] = (base + rnd() % 48) as u16;
|
||
}
|
||
for x in 0..cw {
|
||
f.cb[y * cw + x] = (480 + (x + y) as u32 % 64 + rnd() % 16) as u16;
|
||
f.cr[y * cw + x] = (500 + (x * 7) as u32 % 50 + rnd() % 16) as u16;
|
||
}
|
||
}
|
||
f
|
||
}
|
||
|
||
fn fps(frame: &[u8], threads: bool, secs: f64) -> f64 {
|
||
let opts = DecodeOptions { bit_depth: None, threads };
|
||
// Reuse the output planes across frames, as a player would.
|
||
let mut out = decode_frame_with(frame, &opts).unwrap();
|
||
let t = Instant::now();
|
||
let mut n = 0;
|
||
while t.elapsed().as_secs_f64() < secs {
|
||
decode_frame_into(frame, &opts, &mut out).unwrap();
|
||
std::hint::black_box(&out);
|
||
n += 1;
|
||
}
|
||
n as f64 / t.elapsed().as_secs_f64()
|
||
}
|
||
|
||
#[test]
|
||
#[ignore]
|
||
fn decode_1080p_fps() {
|
||
let mut streams: Vec<(String, Vec<u8>)> = Vec::new();
|
||
for p in [Profile::Hq, Profile::P4444] {
|
||
let src = busy_1080p(p.chroma());
|
||
let mut enc = Encoder::new(p, 1920, 1080);
|
||
let t = Instant::now();
|
||
let coded = enc.encode(&src).unwrap();
|
||
println!("encode {p:?} 1080p: {:.1} ms, {} bytes", t.elapsed().as_secs_f64() * 1e3, coded.len());
|
||
streams.push((format!("ours {p:?} (nominal rate)"), coded));
|
||
}
|
||
if let Some(ff) = common::ffmpeg() {
|
||
let spec = common::Spec::new(
|
||
"perf_ks_hq_mandel_1080",
|
||
"mandelbrot=s=1920x1080:r=25,format=yuv422p10le",
|
||
1,
|
||
&["-c:v", "prores_ks", "-profile:v", "3"],
|
||
);
|
||
let mov = common::make(&ff, &spec);
|
||
streams.push(("prores_ks HQ mandelbrot".into(), common::packets(&ff, &mov).remove(0)));
|
||
}
|
||
for (name, s) in &streams {
|
||
let mt = fps(s, true, 2.0);
|
||
let st = fps(s, false, 2.0);
|
||
println!("{name:<32} {:>8} bytes: {mt:7.1} fps threaded, {st:6.1} fps single-threaded", s.len());
|
||
}
|
||
}
|
||
|
||
/// A run of similar 3840×2160 frames (busy content drifting by a pixel per frame), as a preview
|
||
/// render or export of footage hands them to one encoder in order.
|
||
fn busy_2160p_run(frames: usize) -> Vec<Frame> {
|
||
let one = busy_1080p(ChromaFormat::Yuv422);
|
||
(0..frames)
|
||
.map(|k| {
|
||
let mut f = Frame::new(3840, 2160, ChromaFormat::Yuv422, 10, false).unwrap();
|
||
let (cw, scw) = (f.chroma_width() as usize, one.chroma_width() as usize);
|
||
for y in 0..2160usize {
|
||
for x in 0..3840usize {
|
||
f.y[y * 3840 + x] = one.y[(y % 1080) * 1920 + (x + k) % 1920];
|
||
}
|
||
for x in 0..cw {
|
||
f.cb[y * cw + x] = one.cb[(y % 1080) * scw + (x + k) % scw];
|
||
f.cr[y * cw + x] = one.cr[(y % 1080) * scw + (x + k) % scw];
|
||
}
|
||
}
|
||
f
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
#[test]
|
||
#[ignore]
|
||
fn encode_2160p_run_fps() {
|
||
let run = busy_2160p_run(12);
|
||
for p in [Profile::Standard, Profile::Hq] {
|
||
let mut enc = Encoder::new(p, 3840, 2160);
|
||
let t = Instant::now();
|
||
let bytes: usize = run.iter().map(|f| enc.encode(f).unwrap().len()).sum();
|
||
let s = t.elapsed().as_secs_f64();
|
||
println!(
|
||
"encode {p:?} 2160p run of {}: {:.1} ms/frame ({:.1} fps), {:.1}% of the target",
|
||
run.len(),
|
||
s * 1e3 / run.len() as f64,
|
||
run.len() as f64 / s,
|
||
bytes as f64 * 100.0 / (enc.target_frame_bytes() * run.len()) as f64
|
||
);
|
||
}
|
||
}
|