Files
craft-libs/crates/video/tests/prores/perf.rs
T
c2b7dac5a2 craft-video: FilmCraft's later H.264 encoder work (High 10, threads, sub-pel diamond) and its H.264 and ProRes tests
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>
2026-11-11 22:43:42 -05:00

115 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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
);
}
}