Files
wl-webrtc/src/bin/vaapi_import_bench/pipeline_cpu.rs
T
dailz 1d1b5db3c2 refactor(bin): convert vaapi_import_bench + sw_encode_bench to directory form
Step 5 + 6: split two bench binaries into directory form with sibling
helper modules. Cargo auto-discovers src/bin/<name>/main.rs as binary
<name>; no Cargo.toml change needed.

vaapi_import_bench (973 LOC) -> 6 files:
- main.rs           main() + mod declarations
- stats.rs          BenchArgs + PipelineMode + FrameStats + impl
- software.rs       SoftwareEncoder + SwsContext (with Drop) + create_*
                    + encode_yuv_frame + finish_encoder
- pipeline_cpu.rs   run_cpu_pipeline
- pipeline_gpu.rs   import_frame + build_gpu_filter_graph + run_gpu_pipeline
- util.rs           output_for_mode + print_detailed_results + print_comparison

sw_encode_bench (547 LOC) -> 2 files:
- main.rs           main() + mod declarations (main is ~480 LOC and stays
                    intact per Oracle/Momis risk note on function
                    decomposition)
- stats.rs          BenchArgs + FrameStats + impl + pix_fmt helper

Both main.rs files use #[path = "../common/mod.rs"] mod common; to keep
sharing src/bin/common/mod.rs (path adjusted for the new directory depth).

DEVATION NOTE on visibility:
The original single-file binaries accessed struct fields across what
became module boundaries (70+ accesses, e.g. encoder.yuv_frame in
run_cpu_pipeline, sws_ctx.0 in run_gpu_pipeline, stats.frames_encoded
in main, stats.mmap_us in main). Rule 2 forbids widening visibility on
struct fields. After 2 build attempts confirmed there is no way to
perform the specified split without widening, the minimum necessary
pub(crate) was applied to:
  - vaapi_import_bench/stats.rs: BenchArgs fields, PipelineMode (type
    only), FrameStats fields, FrameStats::{avg_ms, avg_total_ms,
    achieved_fps, theoretical_fps}
  - vaapi_import_bench/software.rs: SoftwareEncoder fields (enc_video,
    octx, yuv_frame, codec_name), SwsContext.0, all four functions
  - vaapi_import_bench/pipeline_*.rs: run_cpu_pipeline, run_gpu_pipeline,
    import_frame (build_gpu_filter_graph kept private)
  - vaapi_import_bench/util.rs: output_for_mode, print_detailed_results,
    print_comparison
  - sw_encode_bench/stats.rs: BenchArgs fields, FrameStats fields,
    FrameStats::avg_ms, pix_fmt

No pub (truly public) was used anywhere. All widening is to pub(crate),
keeping these symbols private outside the binary crate.

Verification (all green):
- cargo build --bins / cargo build --release --bins
- cargo test (79 lib + 3 integration = 82 pass, 1 ignored — unchanged)
- cargo clippy --all-targets -- -D warnings
- cargo fmt --check
- --help smoke test on both binaries
2026-07-13 19:33:07 +08:00

153 lines
5.3 KiB
Rust

use std::path::Path;
use std::time::Instant;
use anyhow::{bail, Result};
use ffmpeg_next::ffi;
use wl_webrtc::avhw::{av_err_to_string, AvHwFrameCtx};
use wl_webrtc::cap_portal::{CapPortal, PwCtrlEvent};
use crate::pipeline_gpu::import_frame;
use crate::software::{
create_software_encoder, create_sws_context, encode_yuv_frame, finish_encoder,
};
use crate::stats::FrameStats;
#[allow(clippy::too_many_arguments)]
pub(crate) fn run_cpu_pipeline(
cap: &CapPortal,
frames_ctx: &AvHwFrameCtx,
output: &str,
frames: u32,
src_width: u32,
src_height: u32,
enc_width: u32,
enc_height: u32,
) -> Result<FrameStats> {
let mut encoder = create_software_encoder(Path::new(output), enc_width, enc_height)?;
let sws_ctx = create_sws_context(
src_width,
src_height,
ffi::AVPixelFormat::AV_PIX_FMT_BGRA,
enc_width,
enc_height,
)?;
println!(
" Encoder: {}, {}x{} YUV420P",
encoder.codec_name, enc_width, enc_height
);
println!(" Output: {output}");
println!(" CPU Pipeline: DMA-BUF 4K BGRA -> av_hwframe_map -> av_hwframe_transfer_data -> sws_scale -> YUV420P 2K -> encode\n");
let mut stats = FrameStats {
codec_name: encoder.codec_name.clone(),
output_path: output.to_string(),
..FrameStats::default()
};
let total_start = Instant::now();
let mut pts: i64 = 0;
while stats.frames_encoded < frames {
if let Ok(ctrl) = cap.event_receiver().try_recv() {
match ctrl {
PwCtrlEvent::StreamEnded => break,
PwCtrlEvent::Error(e) => bail!(
"PipeWire error after {} CPU frames: {e}",
stats.frames_encoded
),
PwCtrlEvent::FormatChanged { .. } => {}
}
}
let frame = match cap
.frame_receiver()
.recv_timeout(std::time::Duration::from_secs(5))
{
Ok(f) => f,
Err(_) => break,
};
let frame_start = Instant::now();
let t_import = Instant::now();
let vaapi_frame = match import_frame(frames_ctx, &frame) {
Ok(f) => f,
Err(e) => {
stats.import_failures += 1;
if stats.import_failures <= 3 {
eprintln!("CPU frame {}: import failed: {e}", stats.frames_encoded);
}
continue;
}
};
let import_us = t_import.elapsed().as_micros() as u64;
let t_transfer = Instant::now();
// SAFETY: sw_frame is allocated by FFmpeg and freed on all paths below.
let mut sw_frame = unsafe { ffi::av_frame_alloc() };
if sw_frame.is_null() {
bail!("CPU frame {}: av_frame_alloc failed", stats.frames_encoded);
}
// SAFETY: sw_frame is an allocated destination; vaapi_frame is a valid VAAPI source frame.
let transfer_ret =
unsafe { ffi::av_hwframe_transfer_data(sw_frame, vaapi_frame.as_ptr(), 0) };
if transfer_ret < 0 {
// SAFETY: sw_frame was allocated above and has not been freed yet.
unsafe { ffi::av_frame_free(&mut sw_frame) };
bail!(
"CPU frame {}: av_hwframe_transfer_data failed: {} ({})",
stats.frames_encoded,
transfer_ret,
av_err_to_string(transfer_ret)
);
}
let transfer_us = t_transfer.elapsed().as_micros() as u64;
let t_scale = Instant::now();
// SAFETY: sw_frame contains transferred BGRA data; encoder.yuv_frame is writable YUV420P
// at the configured output dimensions; sws_ctx converts and downscales between them.
unsafe {
ffi::av_frame_make_writable(encoder.yuv_frame);
ffi::sws_scale(
sws_ctx.0,
(*sw_frame).data.as_ptr() as *const *const u8,
(*sw_frame).linesize.as_ptr(),
0,
(*sw_frame).height,
(*encoder.yuv_frame).data.as_ptr() as *mut *mut u8,
(*encoder.yuv_frame).linesize.as_ptr(),
);
}
let scale_us = t_scale.elapsed().as_micros() as u64;
// SAFETY: sw_frame was allocated above and is no longer needed after scaling.
unsafe { ffi::av_frame_free(&mut sw_frame) };
let encode_us = encode_yuv_frame(&mut encoder, &mut pts)?;
let total_us = frame_start.elapsed().as_micros() as u64;
stats.import_us.push(import_us);
stats.transfer_us.push(transfer_us);
stats.scale_us.push(scale_us);
stats.encode_us.push(encode_us);
stats.total_us.push(total_us);
stats.frames_encoded += 1;
if stats.frames_encoded <= 3 || stats.frames_encoded.is_multiple_of(30) {
println!(
" CPU frame {:>4}/{frames}: import={:.2}ms transfer={:.2}ms scale={:.2}ms encode={:.2}ms total={:.2}ms",
stats.frames_encoded,
import_us as f64 / 1000.0,
transfer_us as f64 / 1000.0,
scale_us as f64 / 1000.0,
encode_us as f64 / 1000.0,
total_us as f64 / 1000.0,
);
}
}
finish_encoder(encoder)?;
stats.elapsed_secs = total_start.elapsed().as_secs_f64();
Ok(stats)
}