Skip to content
Open
44 changes: 22 additions & 22 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

215 changes: 215 additions & 0 deletions examples/encode_bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
//! Example: Encoding Benchmarks
//!
//! Demonstrates returning of encoding statistics for benchmarking or other needs.
//! Loads raw YUV420 frames from `testdata/test_frames_1080p.yuv` in 1920 x 1080 resolution at 60 FPS.
//! Tests all supported codecs and tuning modes.

use pixelforge::{
Codec, EncodeBitDepth, EncodeConfig, Encoder, EncoderTuningMode, InputImage, PixelFormat,
RateControlMode, VideoContextBuilder,
};
use std::fs::File;
use std::io::Read;
use std::path::Path;
use tracing_subscriber::{Layer, layer::SubscriberExt, util::SubscriberInitExt};

use std::time::Duration;

const TEST_FRAMES_PATH: &str = "testdata/test_frames_1080p.yuv";
Comment thread
hgaiser marked this conversation as resolved.
const WIDTH: u32 = 1920;
const HEIGHT: u32 = 1080;

fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing.
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer().with_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
),
)
.init();

println!("PixelForge Encode Bench Example\n");

// Load test frames.
let test_path = Path::new(TEST_FRAMES_PATH);
if !test_path.exists() {
eprintln!("Test frames not found at '{TEST_FRAMES_PATH}'");
eprintln!(
"Generate with: ffmpeg -f lavfi -i testsrc=duration=5:size=1920x1080:rate=60 -pix_fmt yuv420p -f rawvideo testdata/test_frames_1080p.yuv"
);
return Ok(());
}

let mut yuv_data = Vec::new();
File::open(test_path)?.read_to_end(&mut yuv_data)?;

let frame_size = (WIDTH * HEIGHT * 3 / 2) as usize;
let num_frames = yuv_data.len() / frame_size;
println!(
"Input: {num_frames} frames, {WIDTH}x{HEIGHT} YUV420, {} bytes\n",
yuv_data.len()
);

// Create video context.
let context = VideoContextBuilder::new()
.app_name("Encode Bench Example")
.enable_validation(cfg!(debug_assertions))
.build()?;

// Define codecs and tuning modes to test.
let codecs = [Codec::H264, Codec::H265, Codec::AV1];
let tuning_modes = [
EncoderTuningMode::Default,
EncoderTuningMode::HighQuality,
EncoderTuningMode::LowLatency,
EncoderTuningMode::UltraLowLatency,
EncoderTuningMode::Lossless,
];

// Test each codec and tuning mode combination.
for &codec in &codecs {
if !context.supports_encode(codec) {
println!("=== {:?} not supported, skipping ===\n", codec);
continue;
}

for &tuning_mode in &tuning_modes {
println!("=== Testing {:?} with {:?} tuning ===", codec, tuning_mode);

match run_encode_test(
&context,
&yuv_data,
frame_size,
num_frames,
codec,
tuning_mode,
) {
Ok(()) => println!(),
Err(e) => eprintln!("Error: {}\n", e),
}
}
}

Ok(())
}

fn run_encode_test(
context: &pixelforge::VideoContext,
yuv_data: &[u8],
frame_size: usize,
num_frames: usize,
codec: Codec,
tuning_mode: EncoderTuningMode,
) -> Result<(), Box<dyn std::error::Error>> {
// Configure encoder.
let config = match codec {
Codec::H264 => EncodeConfig::h264(WIDTH, HEIGHT),
Codec::H265 => EncodeConfig::h265(WIDTH, HEIGHT),
Codec::AV1 => EncodeConfig::av1(WIDTH, HEIGHT),
}
.with_rate_control(if matches!(tuning_mode, EncoderTuningMode::Lossless) {
RateControlMode::Cqp
} else {
RateControlMode::Cbr
})
.with_target_bitrate(8_000_000)
.with_max_bitrate(12_000_000)
.with_frame_rate(60, 1)
.with_gop_size(120)
.with_b_frames(0)
.with_encode_content_hint(pixelforge::EncodeContentHint::Rendered)
.with_encode_usage_hint(pixelforge::EncodeUsageHint::Streaming)
.with_encoder_tuning_mode(tuning_mode);

println!(
"Config: {:?}, bitrate={}, GOP={}, B-frames={}",
config.rate_control_mode, config.target_bitrate, config.gop_size, config.b_frame_count
);

// Create input image for uploading frames.
let mut input_image = InputImage::new(
context.clone(),
codec,
WIDTH,
HEIGHT,
EncodeBitDepth::Eight,
PixelFormat::Yuv420,
)?;
let mut encoder = Encoder::new(context.clone(), config)?;
let mut total_bytes = 0;

let mut gpu_durations: Vec<Duration> = Vec::new();
let mut cpu_durations: Vec<Duration> = Vec::new();
let mut wall_latency_durations: Vec<Duration> = Vec::new();

let mut pending: std::collections::VecDeque<pixelforge::EncodeFuture> =
std::collections::VecDeque::new();

let mut write_packet = |packet: pixelforge::EncodedPacket| {
total_bytes += packet.data.len();
if let Some(stats) = packet.stats {
gpu_durations.push(Duration::from_nanos(stats.gpu_time_ns));
cpu_durations.push(Duration::from_nanos(stats.cpu_time_ns));
wall_latency_durations.push(Duration::from_nanos(stats.wall_latency_ns));
}
Ok::<(), Box<dyn std::error::Error>>(())
};

// Encode frames
for i in 0..num_frames {
let frame = &yuv_data[i * frame_size..(i + 1) * frame_size];

// Upload YUV420 data to the input image
input_image.upload_yuv420(frame)?;

pending.push_back(encoder.encode(input_image.image())?);
// drain before submitting when at capacity
while pending.len() >= 2 {
let packet = pollster::block_on(pending.pop_front().unwrap())?;
write_packet(packet)?;
}
}

// Flush remaining frames without using them for statistics
encoder.flush()?;
while let Some(_future) = pending.pop_front() {}

let ratio = (num_frames * frame_size) as f64 / total_bytes as f64;
println!("Encoded {num_frames} frames, {total_bytes} bytes, {ratio:.1}:1 compression");

// Print GPU timings.
if !gpu_durations.is_empty() {
gpu_durations.sort_unstable();
print_timing_stats("GPU", &gpu_durations);
}

// Print CPU timings.
if !cpu_durations.is_empty() {
cpu_durations.sort_unstable();
print_timing_stats("CPU", &cpu_durations);
}

// Print wall latency timings.
if !wall_latency_durations.is_empty() {
wall_latency_durations.sort_unstable();
print_timing_stats("Wall Latency", &wall_latency_durations);
}

Ok(())
}

fn print_timing_stats(label: &str, durations: &[Duration]) {
let min = durations[0];
let max = *durations.last().unwrap();
let sum: Duration = durations.iter().sum();
let avg = sum / durations.len() as u32;
let p99_index = (durations.len() as f64 * 0.99) as usize;
let p99 = durations[p99_index.min(durations.len() - 1)];

println!(
"{} Encode timings: Min: {:?}, Max: {:?}, Avg: {:?}, P99: {:?}",
label, min, max, avg, p99
);
}
13 changes: 12 additions & 1 deletion src/encoder/av1/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,24 @@ impl Av1 {
let chroma_subsampling: vk::VideoChromaSubsamplingFlagsKHR = config.pixel_format.into();
let bit_depth: vk::VideoComponentBitDepthFlagsKHR = config.bit_depth.into();

// Get encoder tuning mode, usage and content hints.
let encode_usage_hint: vk::VideoEncodeUsageFlagsKHR = config.encode_usage_hint.into();
let encode_content_hint: vk::VideoEncodeContentFlagsKHR = config.encode_content_hint.into();
let encoder_tuning_mode: vk::VideoEncodeTuningModeKHR = config.encoder_tuning_mode.into();

let mut video_encode_usage_info = vk::VideoEncodeUsageInfoKHR::default()
.video_usage_hints(encode_usage_hint)
.video_content_hints(encode_content_hint)
.tuning_mode(encoder_tuning_mode);

let mut av1_profile_info = vk::VideoEncodeAV1ProfileInfoKHR::default().std_profile(profile);
let profile_info = vk::VideoProfileInfoKHR::default()
.video_codec_operation(vk::VideoCodecOperationFlagsKHR::ENCODE_AV1)
.chroma_subsampling(chroma_subsampling)
.luma_bit_depth(bit_depth)
.chroma_bit_depth(bit_depth)
.push(&mut av1_profile_info);
.push(&mut av1_profile_info)
.push(&mut video_encode_usage_info);

let bitstream_buffer_size = MIN_BITSTREAM_BUFFER_SIZE
.max(config.dimensions.width as usize * config.dimensions.height as usize);
Expand Down
Loading
Loading