docs(vaapi_import_bench): [2/2] 中文注释 CPU/GPU pipeline 与报告
This commit is contained in:
@@ -645,6 +645,16 @@ fn build_gpu_filter_graph(
|
||||
Ok(graph)
|
||||
}
|
||||
|
||||
// CPU 流水线主函数:跑 `frames` 帧测量每阶段耗时(import → transfer → scale → encode)。
|
||||
// 与 GPU 路径的核心差异:CPU 路径**不经过 scale_vaapi 滤镜**,而是用 `av_hwframe_transfer_data`
|
||||
// 把硬件帧"下载"到 CPU 内存(4K BGRA),再用 `sws_scale` 在 CPU 上做下采样到 2K YUV420P;
|
||||
// 因此 CPU 路径的"transfer"和"scale"耗时都明显高于 GPU 路径。
|
||||
//
|
||||
// 类比 Go benchmark:类似 `func benchCPU(b *testing.B) { for n := 0; n < b.N; n++ {...} }`,
|
||||
// 但 Rust 用 `while stats.frames_encoded < frames` 显式循环(无 testing.B 框架)。
|
||||
//
|
||||
// 参数:8 个参数(含 src/enc 尺寸 4 个)—— clippy 默认会嫌太多,故上方 `#[allow]` 抑制。
|
||||
// 返回 `Result<FrameStats>`:任何 FFmpeg/Portal 失败立即 `?` 传播到 main。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run_cpu_pipeline(
|
||||
cap: &CapPortal,
|
||||
@@ -656,7 +666,10 @@ fn run_cpu_pipeline(
|
||||
enc_width: u32,
|
||||
enc_height: u32,
|
||||
) -> Result<FrameStats> {
|
||||
// 构造软件编码器(libx264 或 libopenh264,取决于 create_software_encoder 内部 fallback)。
|
||||
// `?` 自动把 anyhow::Error 上浮到调用者;类比 Go `if err != nil { return err }`。
|
||||
let mut encoder = create_software_encoder(Path::new(output), enc_width, enc_height)?;
|
||||
// 构造 sws_scale 上下文:源 = 4K BGRA,目标 = 2K YUV420P;sws_scale 内部完成下采样+色彩空间转换。
|
||||
let sws_ctx = create_sws_context(
|
||||
src_width,
|
||||
src_height,
|
||||
@@ -665,6 +678,8 @@ fn run_cpu_pipeline(
|
||||
enc_height,
|
||||
)?;
|
||||
|
||||
// println! 是宏(不是函数),类比 Go fmt.Println;第一参数是 format! 模板字符串。
|
||||
// `{output}` 是内联格式化语法(Rust 1.58+),等价于 `format!("{}", output)`。
|
||||
println!(
|
||||
" Encoder: {}, {}x{} YUV420P",
|
||||
encoder.codec_name, enc_width, enc_height
|
||||
@@ -672,26 +687,40 @@ fn run_cpu_pipeline(
|
||||
println!(" Output: {output}");
|
||||
println!(" CPU Pipeline: DMA-BUF 4K BGRA -> av_hwframe_map -> av_hwframe_transfer_data -> sws_scale -> YUV420P 2K -> encode\n");
|
||||
|
||||
// 构造 FrameStats,用 struct update 语法 `..FrameStats::default()` 让其余字段取 Default 值。
|
||||
// 类比 Go `&FrameStats{Codec: codec, Output: out}` 仅设置 2 字段其余清零。
|
||||
let mut stats = FrameStats {
|
||||
codec_name: encoder.codec_name.clone(),
|
||||
output_path: output.to_string(),
|
||||
..FrameStats::default()
|
||||
};
|
||||
// 整条流水线总耗时起点;elapsed() 返回 Duration,后续 as_secs_f64() 取秒(float)。
|
||||
let total_start = Instant::now();
|
||||
// PTS(Presentation Time Stamp,单位 = 编码器 time_base.den 的倒数)—— 单调递增的演示时钟。
|
||||
// `let mut` 表示可变绑定(默认不可变,Rust 与 Go 的关键差异之一)。
|
||||
let mut pts: i64 = 0;
|
||||
|
||||
// 主循环:直到编码完 `frames` 帧;类比 Go `for stats.FramesEncoded < frames {`。
|
||||
while stats.frames_encoded < frames {
|
||||
// try_recv 非阻塞从 PipeWire 控制通道取事件;Ok 表示有事件,Err(TryRecvError::Empty) 跳过。
|
||||
// 类比 Go `select { case ev := <-ctrlCh: ... default: }`。
|
||||
if let Ok(ctrl) = cap.event_receiver().try_recv() {
|
||||
// match 是穷尽性模式匹配(每个 enum variant 必须覆盖或用 `_` 兜底)。
|
||||
match ctrl {
|
||||
// 流正常结束(用户停止共享 / Portal 关闭):跳出主循环。
|
||||
PwCtrlEvent::StreamEnded => break,
|
||||
// PipeWire 报错:把帧号 + 错误信息 bail! 到调用者(bail! = return Err(anyhow!(...)))。
|
||||
PwCtrlEvent::Error(e) => bail!(
|
||||
"PipeWire error after {} CPU frames: {e}",
|
||||
stats.frames_encoded
|
||||
),
|
||||
// 格式变化(分辨率/像素格式):本基准忽略,等下一帧自然到达。
|
||||
PwCtrlEvent::FormatChanged { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
// recv_timeout 阻塞最多 5 秒取下一帧;类比 Go `select { case f := <-frCh: ... case <-time.After(5*time.Second): }`。
|
||||
// match 直接对 Result 解构:Ok(f) 拿到帧,Err(_)(超时或断开)直接 break 结束。
|
||||
let frame = match cap
|
||||
.frame_receiver()
|
||||
.recv_timeout(std::time::Duration::from_secs(5))
|
||||
@@ -700,31 +729,42 @@ fn run_cpu_pipeline(
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
// 单帧起点:用于统计 total_us(包含所有子阶段)。
|
||||
let frame_start = Instant::now();
|
||||
// import 阶段起点:把 DMA-BUF 帧封装为 AV_PIX_FMT_VAAPI 硬件帧(av_hwframe_map 路径)。
|
||||
let t_import = Instant::now();
|
||||
// match 表达式对 Result 解构并支持多分支(含 guard 与错误处理)。
|
||||
let vaapi_frame = match import_frame(frames_ctx, &frame) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
// 失败计数器自增;前 3 次打印到 stderr,避免日志淹没。
|
||||
stats.import_failures += 1;
|
||||
if stats.import_failures <= 3 {
|
||||
eprintln!("CPU frame {}: import failed: {e}", stats.frames_encoded);
|
||||
}
|
||||
// continue 跳过本帧后续步骤(不是错误退出)。
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// elapsed() 返回 Duration;as_micros() → u128;`as u64` 截断到 u64(帧耗时不会超 2^64 微秒)。
|
||||
let import_us = t_import.elapsed().as_micros() as u64;
|
||||
|
||||
// transfer 阶段:用 av_hwframe_transfer_data 把硬件帧拷贝到 CPU 内存(4K BGRA)。
|
||||
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() {
|
||||
// av_frame_alloc 返回 NULL 表示 OOM; bail! 把错误抛到 main(不是 panic)。
|
||||
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.
|
||||
// av_hwframe_transfer_data:FFmpeg 提供的硬件→软件帧拷贝 API;src=VAAPI,dst=CPU 内存帧。
|
||||
// 第 3 参数 flags 通常传 0;返回 0 表示成功,负数表示 FFmpeg 错误码。
|
||||
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.
|
||||
// 错误路径必须 free,否则内存泄漏;FFmpeg C API 无 RAII。
|
||||
unsafe { ffi::av_frame_free(&mut sw_frame) };
|
||||
bail!(
|
||||
"CPU frame {}: av_hwframe_transfer_data failed: {} ({})",
|
||||
@@ -735,11 +775,15 @@ fn run_cpu_pipeline(
|
||||
}
|
||||
let transfer_us = t_transfer.elapsed().as_micros() as u64;
|
||||
|
||||
// scale 阶段:在 CPU 上把 4K BGRA 下采样到 2K YUV420P(CPU 路径的瓶颈所在)。
|
||||
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 {
|
||||
// av_frame_make_writable:确保 yuv_frame 内部 buffer 可写(FFmpeg 引用计数可能共享)。
|
||||
ffi::av_frame_make_writable(encoder.yuv_frame);
|
||||
// sws_scale:libswscale 主接口;参数 = (ctx, src_slices[], src_stride[], src_y_start, src_h, dst_slices[], dst_stride[])。
|
||||
// `(*sw_frame).data.as_ptr() as *const *const u8` 把 C 数组首地址转裸指针(FFmpeg AVFrame.data 是 [u8*; 8])。
|
||||
ffi::sws_scale(
|
||||
sws_ctx.0,
|
||||
(*sw_frame).data.as_ptr() as *const *const u8,
|
||||
@@ -752,11 +796,15 @@ fn run_cpu_pipeline(
|
||||
}
|
||||
let scale_us = t_scale.elapsed().as_micros() as u64;
|
||||
// SAFETY: sw_frame was allocated above and is no longer needed after scaling.
|
||||
// 缩放完成后立即释放中间 BGRA 帧(约 4K*2160*4 = 33MB),避免峰值内存。
|
||||
unsafe { ffi::av_frame_free(&mut sw_frame) };
|
||||
|
||||
// encode 阶段:把 YUV420P 帧送入 libx264/openh264 编码器;返回编码单帧耗时(微秒)。
|
||||
// `?` 把 anyhow::Error 传播到调用者;`&mut encoder` & `&mut pts` 都是可变借用。
|
||||
let encode_us = encode_yuv_frame(&mut encoder, &mut pts)?;
|
||||
let total_us = frame_start.elapsed().as_micros() as u64;
|
||||
|
||||
// 把本帧的 5 个阶段耗时 push 到 Vec<u64>;后续 print_detailed_results 计算 avg_ms/p95。
|
||||
stats.import_us.push(import_us);
|
||||
stats.transfer_us.push(transfer_us);
|
||||
stats.scale_us.push(scale_us);
|
||||
@@ -764,6 +812,7 @@ fn run_cpu_pipeline(
|
||||
stats.total_us.push(total_us);
|
||||
stats.frames_encoded += 1;
|
||||
|
||||
// 节流打印:前 3 帧详打 + 之后每 30 帧打一次,避免日志淹没;`{:>4}` 右对齐 4 列宽。
|
||||
if stats.frames_encoded <= 3 || stats.frames_encoded % 30 == 0 {
|
||||
println!(
|
||||
" CPU frame {:>4}/{frames}: import={:.2}ms transfer={:.2}ms scale={:.2}ms encode={:.2}ms total={:.2}ms",
|
||||
@@ -777,11 +826,24 @@ fn run_cpu_pipeline(
|
||||
}
|
||||
}
|
||||
|
||||
// flush 编码器(送 NULL frame 触发 EOS)+ write_trailer + 关闭输出文件。
|
||||
// 任何失败经 `?` 传播到 main。
|
||||
finish_encoder(encoder)?;
|
||||
// as_secs_f64 把 Duration 转为秒(f64),用于后续 FPS 计算。
|
||||
stats.elapsed_secs = total_start.elapsed().as_secs_f64();
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
// GPU 流水线主函数:跑 `frames` 帧测量 GPU 路径每阶段耗时(import → filter → transfer → format → encode)。
|
||||
// 与 CPU 路径的核心差异:GPU 路径用 `scale_vaapi` 滤镜**在硬件内**把 4K BGRA 下采样到 2K NV12,
|
||||
// 再用 `av_hwframe_transfer_data` 把**小**NV12 帧拷贝到 CPU 内存(数据量 = 4K BGRA 的 1/6),
|
||||
// 最后用 `sws_scale` 做 NV12→YUV420P 的**纯格式转换**(无尺寸变化,比 CPU 路径快得多)。
|
||||
//
|
||||
// 性能对比的关键:
|
||||
// - CPU 路径 transfer ~33MB + scale 33MB→2MB;GPU 路径 transfer ~3MB + format 仅 NV12→YUV420P。
|
||||
// - 因此 GPU 路径的 transfer/format 总耗时远低于 CPU 路径的 transfer+scale。
|
||||
//
|
||||
// 参数:9 个(比 CPU 多一个 hw_dev 用于 filter graph);同样用 `#[allow]` 抑制 clippy。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run_gpu_pipeline(
|
||||
cap: &CapPortal,
|
||||
@@ -794,7 +856,11 @@ fn run_gpu_pipeline(
|
||||
enc_width: u32,
|
||||
enc_height: u32,
|
||||
) -> Result<FrameStats> {
|
||||
// 同 CPU 路径:构造软件编码器(最终编码阶段仍是 CPU 上的 libx264/openh264)。
|
||||
// 注意:本基准目标是测 import/scale 性能,**不**测 VAAPI 硬件编码;所以两条路径都用软件编码器。
|
||||
let mut encoder = create_software_encoder(Path::new(output), enc_width, enc_height)?;
|
||||
// 这里命名为 format_ctx 而非 sws_ctx —— 因为它只做 NV12→YUV420P 的**色度重排**(无下采样)。
|
||||
// 源/目标尺寸相同(enc_width × enc_height),sws_scale 内部走 fast path(无缩放,仅 deinterleave)。
|
||||
let format_ctx = create_sws_context(
|
||||
enc_width,
|
||||
enc_height,
|
||||
@@ -802,6 +868,8 @@ fn run_gpu_pipeline(
|
||||
enc_width,
|
||||
enc_height,
|
||||
)?;
|
||||
// 构造 GPU 滤镜图:bufferin (hw) → scale_vaapi → bufferout (hw),详见 build_gpu_filter_graph。
|
||||
// 滤镜图在 GPU 显存里完成下采样,输出仍是 VAAPI 硬件帧。
|
||||
let mut graph = build_gpu_filter_graph(
|
||||
hw_dev, frames_ctx, src_width, src_height, enc_width, enc_height,
|
||||
)?;
|
||||
@@ -822,6 +890,7 @@ fn run_gpu_pipeline(
|
||||
let mut pts: i64 = 0;
|
||||
|
||||
while stats.frames_encoded < frames {
|
||||
// 同 CPU 路径(详见 run_cpu_pipeline 的同位置注释)。
|
||||
if let Ok(ctrl) = cap.event_receiver().try_recv() {
|
||||
match ctrl {
|
||||
PwCtrlEvent::StreamEnded => break,
|
||||
@@ -855,23 +924,33 @@ fn run_gpu_pipeline(
|
||||
};
|
||||
let import_us = t_import.elapsed().as_micros() as u64;
|
||||
|
||||
// filter 阶段:把 VAAPI 4K 帧送入 scale_vaapi 滤镜图,取出 2K NV12 VAAPI 帧。
|
||||
// 这是 GPU 路径相对 CPU 路径最大的性能优势所在。
|
||||
let t_filter = Instant::now();
|
||||
// graph.get("in").unwrap():按 name 取滤镜图的输入 pad;unwrap 在此是安全的(图刚构造必有 "in")。
|
||||
let mut filter_src_ctx = graph.get("in").unwrap();
|
||||
// source():从 pad 上下文获取发送端;后续 .add(&frame) 把帧送入图。
|
||||
let mut filter_src = filter_src_ctx.source();
|
||||
let mut filter_sink_ctx = graph.get("out").unwrap();
|
||||
let mut filter_sink = filter_sink_ctx.sink();
|
||||
// map_err 把 ffmpeg_next::Error 转换为 anyhow::Error(保持错误链可读)。
|
||||
// anyhow::anyhow! 是宏,构造 ad-hoc 错误(类比 Go fmt.Errorf)。
|
||||
filter_src
|
||||
.add(&vaapi_frame)
|
||||
.map_err(|e| anyhow::anyhow!("GPU filter source add failed: {e}"))?;
|
||||
|
||||
// ff::frame::Video::empty():构造一个空视频帧(无 buffer),后续 filter_sink.frame() 填充。
|
||||
let mut filtered = ff::frame::Video::empty();
|
||||
// 三路 match:成功 / EAGAIN(图未就绪,需要更多输入帧)/ 真错误。
|
||||
match filter_sink.frame(&mut filtered) {
|
||||
Ok(()) => {}
|
||||
// EAGAIN 表示滤镜图内部缓冲不足,跳过本帧不报错(next iteration 继续喂下一帧)。
|
||||
Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => continue,
|
||||
Err(e) => bail!("GPU filter sink get frame failed: {e}"),
|
||||
}
|
||||
let filter_us = t_filter.elapsed().as_micros() as u64;
|
||||
|
||||
// transfer 阶段:把 2K NV12 硬件帧拷贝到 CPU 内存(数据量 = 2K NV12 ≈ 3MB,远小于 CPU 路径 33MB)。
|
||||
let t_transfer = Instant::now();
|
||||
// SAFETY: sw_nv12 is allocated by FFmpeg and freed after format conversion.
|
||||
let mut sw_nv12 = unsafe { ffi::av_frame_alloc() };
|
||||
@@ -892,6 +971,8 @@ fn run_gpu_pipeline(
|
||||
}
|
||||
let transfer_us = t_transfer.elapsed().as_micros() as u64;
|
||||
|
||||
// format 阶段:NV12 → YUV420P 纯格式转换(同尺寸无缩放)。
|
||||
// NV12 与 YUV420P 的 Y plane 完全相同,只是 UV plane 排列不同(NV12 = interleaved,YUV420P = planar)。
|
||||
let t_format = Instant::now();
|
||||
// SAFETY: sw_nv12 contains CPU-side NV12 at enc dimensions; encoder.yuv_frame is writable
|
||||
// YUV420P at the same dimensions, so sws_scale performs only chroma deinterleave/format conversion.
|
||||
@@ -914,6 +995,8 @@ fn run_gpu_pipeline(
|
||||
let encode_us = encode_yuv_frame(&mut encoder, &mut pts)?;
|
||||
let total_us = frame_start.elapsed().as_micros() as u64;
|
||||
|
||||
// GPU 路径的 stats 包含 6 个阶段(filter + format),CPU 路径只有 5 个(scale);
|
||||
// FrameStats 的字段用 Option/空 Vec 区分。
|
||||
stats.import_us.push(import_us);
|
||||
stats.filter_us.push(filter_us);
|
||||
stats.transfer_us.push(transfer_us);
|
||||
@@ -941,6 +1024,9 @@ fn run_gpu_pipeline(
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
// 打印单条流水线的详细统计报告(捕获/编码分辨率、总时长、各阶段平均毫秒、FPS)。
|
||||
// 纯展示函数:无 Result 返回值,无副作用(除 stdout),无错误路径。
|
||||
// 类比 Go `func printResults(label string, stats *FrameStats, ...) { fmt.Println(...) }`。
|
||||
fn print_detailed_results(
|
||||
label: &str,
|
||||
stats: &FrameStats,
|
||||
@@ -949,20 +1035,25 @@ fn print_detailed_results(
|
||||
enc_width: u32,
|
||||
enc_height: u32,
|
||||
) {
|
||||
// println!() 无参数版本等价于 Go fmt.Println() —— 打印空行做视觉分隔。
|
||||
println!();
|
||||
println!("=== {label} Pipeline Results ===");
|
||||
println!("Capture resolution: {}x{}", src_width, src_height);
|
||||
println!("Encode resolution: {}x{}", enc_width, enc_height);
|
||||
println!("Frames encoded: {}", stats.frames_encoded);
|
||||
// {:.2} 保留 2 位小数;类比 Go fmt.Printf("%.2fs", v)。
|
||||
println!("Total time: {:.2}s", stats.elapsed_secs);
|
||||
println!("Output: {}", stats.output_path);
|
||||
if stats.import_failures > 0 {
|
||||
println!("Import failures: {}", stats.import_failures);
|
||||
}
|
||||
// FrameStats::avg_ms 是关联函数(不是 method),签名 `fn avg_ms(v: &[u64]) -> f64`。
|
||||
// 类比 Go 顶层函数 `func avgMs(v []uint64) float64`,Rust 关联函数等价于 Go 的 package-level。
|
||||
println!(
|
||||
"import avg: {:.2} ms/frame",
|
||||
FrameStats::avg_ms(&stats.import_us)
|
||||
);
|
||||
// is_empty() 判断 Vec 是否为空;GPU 路径才有 filter_us,CPU 路径此 Vec 永远空。
|
||||
if !stats.filter_us.is_empty() {
|
||||
println!(
|
||||
"filter avg: {:.2} ms/frame",
|
||||
@@ -973,12 +1064,14 @@ fn print_detailed_results(
|
||||
"transfer avg: {:.2} ms/frame",
|
||||
FrameStats::avg_ms(&stats.transfer_us)
|
||||
);
|
||||
// CPU 路径才有 scale_us,GPU 路径此 Vec 永远空。
|
||||
if !stats.scale_us.is_empty() {
|
||||
println!(
|
||||
"scale avg: {:.2} ms/frame",
|
||||
FrameStats::avg_ms(&stats.scale_us)
|
||||
);
|
||||
}
|
||||
// GPU 路径才有 format_us,CPU 路径此 Vec 永远空。
|
||||
if !stats.format_us.is_empty() {
|
||||
println!(
|
||||
"format avg: {:.2} ms/frame",
|
||||
@@ -990,14 +1083,19 @@ fn print_detailed_results(
|
||||
stats.codec_name,
|
||||
FrameStats::avg_ms(&stats.encode_us)
|
||||
);
|
||||
// avg_total_ms / achieved_fps / theoretical_fps 都是 method(&self 形式),调用语法 `stats.method()`。
|
||||
println!("total avg: {:.2} ms/frame", stats.avg_total_ms());
|
||||
println!("achieved FPS: {:.1}", stats.achieved_fps());
|
||||
println!("max theoretical: {:.1} FPS", stats.theoretical_fps());
|
||||
}
|
||||
|
||||
// 打印 CPU 与 GPU 流水线的对比摘要(一行 = 一条流水线),便于横向对比。
|
||||
// 接收 Option<&FrameStats>:当基准只跑 CPU 或只跑 GPU 时,另一边为 None。
|
||||
// 类比 Go `func printComparison(cpu, gpu *FrameStats)`,Go 用 nil 表示缺失;Rust 用 Option<T> 强制处理。
|
||||
fn print_comparison(cpu: Option<&FrameStats>, gpu: Option<&FrameStats>) {
|
||||
println!();
|
||||
println!("=== Pipeline Comparison ===");
|
||||
// if let Some(s) = cpu:模式匹配 Option;只在 Some 时打印,None 静默跳过(不需要 else)。
|
||||
if let Some(s) = cpu {
|
||||
println!(
|
||||
"CPU: import={:.2}ms transfer={:.2}ms scale={:.2}ms encode={:.2}ms total={:.2}ms ({:.1} FPS)",
|
||||
@@ -1023,7 +1121,16 @@ fn print_comparison(cpu: Option<&FrameStats>, gpu: Option<&FrameStats>) {
|
||||
}
|
||||
}
|
||||
|
||||
// 二进制入口点。Rust 标准签名 `fn main() -> Result<()>`:返回 Result 时失败会用 exit code 1 + Debug 打印错误。
|
||||
// 类比 Go `func main() { err := run(); if err != nil { log.Fatal(err) } }` —— Rust 用 `?` 传播更简洁。
|
||||
//
|
||||
// 流程概览(3 个阶段,对应 println 中的 [1/3]/[2/3]/[3/3] 标号):
|
||||
// 1. 通过 XDG Portal 请求屏幕捕获权限 → 拿到 PipeWire fd → 构造 CapPortal
|
||||
// 2. 等待首帧 → 测试 av_hwframe_map 导入(若失败,回退 mmap 测试后退出)
|
||||
// 3. 根据 --mode 跑 CPU/GPU/Both 流水线,输出详细统计 + 对比报告
|
||||
fn main() -> Result<()> {
|
||||
// clap::Parser::parse() 解析 std::env::args,匹配失败的会自动 exit code 1 + 打印 help。
|
||||
// 类比 Go `flag.Parse()` + cobra.Struct,但 clap 用 Derive 宏更声明式。
|
||||
let bench_args = BenchArgs::parse();
|
||||
|
||||
println!("=== VAAPI Import Benchmark ===");
|
||||
@@ -1036,11 +1143,15 @@ fn main() -> Result<()> {
|
||||
println!("DRM device: {}", bench_args.drm_device);
|
||||
println!();
|
||||
|
||||
// ff::init():FFmpeg 全局初始化(注册所有编解码器/滤镜/格式)。必须在所有 FFmpeg 调用前执行一次。
|
||||
// 类比 Go 的 `import _ "image/jpeg"` 副作用导入;FFmpeg 5+ 改为运行时自动注册,但 init 仍推荐。
|
||||
ff::init()?;
|
||||
|
||||
println!("[1/3] Requesting screen capture via XDG Portal...");
|
||||
println!(" (Select a screen to share in the portal dialog)");
|
||||
|
||||
// 构造 Args(生产 CLI 类型)——本基准复用 wl-webrtc 主程序的 Args 结构以驱动 CapPortal。
|
||||
// 大部分字段写死;只有 output 从 BenchArgs 透传。类比 Go `args := &wlwebrtc.Args{...}`。
|
||||
let portal_args = Args {
|
||||
output: Some(bench_args.output.clone()),
|
||||
output_name: None,
|
||||
@@ -1058,16 +1169,22 @@ fn main() -> Result<()> {
|
||||
stats: false,
|
||||
};
|
||||
|
||||
// CapPortal::new 启动 Portal 异步协商 + PipeWire 流;阻塞至用户在对话框点"允许"。
|
||||
// 内部会启动 pipewire_thread 后台线程推帧到 frame_receiver 通道。
|
||||
let cap = CapPortal::new(&portal_args)?;
|
||||
println!("[1/3] Portal connected, PipeWire stream active\n");
|
||||
|
||||
println!("[2/3] Waiting for first frame from PipeWire...");
|
||||
// 阻塞等首帧(带 30s 超时,详见 receive_first_frame 实现)。
|
||||
let first_frame = receive_first_frame(&cap)?;
|
||||
|
||||
// 把 first_frame 的字段拷贝到局部变量;后续两条流水线都要用 src_width/src_height 做下采样。
|
||||
// 注意:first_frame 必须 drop 之前不能让 import_dma_buf_to_vaapi 持有 fd 引用(所有权检查)。
|
||||
let src_width = first_frame.width;
|
||||
let src_height = first_frame.height;
|
||||
let src_format = first_frame.format;
|
||||
|
||||
// 0x{:08X}:8 位 16 进制(大写)前补 0 —— 用于打印 DRM 四字符码(ARGB8888 = 0x34325241)。
|
||||
println!(
|
||||
"[2/3] First frame: {}x{}, format=0x{:08X}, stride={}, modifier=0x{:X}",
|
||||
src_width, src_height, src_format, first_frame.stride, first_frame.modifier
|
||||
@@ -1079,14 +1196,19 @@ fn main() -> Result<()> {
|
||||
src_format
|
||||
);
|
||||
|
||||
// 打开 DRM render node(默认 /dev/dri/renderD128),构造 VAAPI 硬件设备上下文。
|
||||
// AvHwDevCtx 内部封装 AVBufferRef(FFmpeg 引用计数),Drop 时自动释放。
|
||||
let drm_device = Path::new(&bench_args.drm_device);
|
||||
let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?;
|
||||
println!(" VAAPI device context created OK");
|
||||
|
||||
// 构造硬件帧上下文:绑定设备 + sw_format=BGRA + 源尺寸;scale_vaapi 滤镜需要此 ctx。
|
||||
let frames_ctx =
|
||||
AvHwFrameCtx::for_capture(&hw_dev, src_width, src_height, ff::format::Pixel::BGRA)?;
|
||||
println!(" VAAPI frames context created OK (sw_format=BGRA)");
|
||||
|
||||
// 首帧导入测试:unsafe 块因为 import_dma_buf_to_vaapi 是 raw FFI(av_hwframe_map + AVDRMFrameDescriptor)。
|
||||
// 此处 unsafe 块**未写** // SAFETY: 注释,因为 import_dma_buf_to_vaapi 自身在 src/avhw.rs 内部已有详尽 SAFETY 注释。
|
||||
let vaapi_frame = unsafe {
|
||||
import_dma_buf_to_vaapi(
|
||||
frames_ctx.as_ptr(),
|
||||
@@ -1100,11 +1222,13 @@ fn main() -> Result<()> {
|
||||
)
|
||||
};
|
||||
|
||||
// 用 match 处理 Result;分支内提前 return Ok(()) 表示"基准结束但不报错"(非失败路径)。
|
||||
match &vaapi_frame {
|
||||
Ok(_) => {
|
||||
println!(" Result: SUCCESS — av_hwframe_map imported DMA-BUF to VAAPI surface!");
|
||||
}
|
||||
Err(e) => {
|
||||
// 失败路径:诊断 + mmap 对照测试 + 友好退出(不返回 Err)。
|
||||
println!(" Result: FAILED");
|
||||
println!(" Error: {e}");
|
||||
println!();
|
||||
@@ -1115,8 +1239,10 @@ fn main() -> Result<()> {
|
||||
println!();
|
||||
println!(" Falling back to mmap readback test for comparison...");
|
||||
|
||||
// mmap 对照:如果 av_hwframe_map 失败,看 mmap 是否也失败(区分根因:driver vs 配置)。
|
||||
let mmap_size = (first_frame.stride as usize) * (first_frame.height as usize);
|
||||
let mmap_start = Instant::now();
|
||||
// unsafe:libc::mmap 是 POSIX FFI,返回 void*;MAP_FAILED (== -1) 表示失败。
|
||||
let mmap_ptr = unsafe {
|
||||
libc::mmap(
|
||||
ptr::null_mut(),
|
||||
@@ -1130,6 +1256,7 @@ fn main() -> Result<()> {
|
||||
let mmap_elapsed = mmap_start.elapsed();
|
||||
|
||||
if mmap_ptr == libc::MAP_FAILED {
|
||||
// last_os_error():取 errno;类比 Go syscall.Errno。
|
||||
let errno = std::io::Error::last_os_error();
|
||||
println!(" mmap also FAILED: {errno}");
|
||||
} else {
|
||||
@@ -1138,6 +1265,7 @@ fn main() -> Result<()> {
|
||||
mmap_size as f64 / 1024.0 / 1024.0,
|
||||
mmap_elapsed.as_secs_f64() * 1000.0
|
||||
);
|
||||
// 必须配对 munmap,否则内核 VMA 泄漏。
|
||||
unsafe {
|
||||
libc::munmap(mmap_ptr, mmap_size);
|
||||
}
|
||||
@@ -1146,10 +1274,13 @@ fn main() -> Result<()> {
|
||||
println!();
|
||||
println!("=== Benchmark ended: av_hwframe_map import FAILED ===");
|
||||
println!("Fix the import issue before proceeding to GPU downscale tests.");
|
||||
// 主动 Ok(()):基准本身没崩,只是诊断后退出;让 CI 不报红。
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// 导入成功后释放首帧资源(vaapi_frame 持有硬件帧引用,first_frame 持有 fd);
|
||||
// 后续主循环每帧重新 import,避免长持有造成硬件帧饥饿。
|
||||
drop(vaapi_frame);
|
||||
drop(first_frame);
|
||||
|
||||
@@ -1157,12 +1288,19 @@ fn main() -> Result<()> {
|
||||
|
||||
let enc_width = bench_args.enc_width;
|
||||
let enc_height = bench_args.enc_height;
|
||||
// PipelineMode::Both 时输出文件名加 cpu/gpu 后缀(详见 output_for_mode)。
|
||||
let split_outputs = bench_args.mode == PipelineMode::Both;
|
||||
// 用 Option 包裹:mode 只跑 CPU 时 gpu_stats 永远 None;print_detailed_results/print_comparison 接 Option。
|
||||
let mut cpu_stats = None;
|
||||
let mut gpu_stats = None;
|
||||
|
||||
// matches! 宏:等价于 `match bench_args.mode { PipelineMode::Cpu | PipelineMode::Both => true, _ => false }`,
|
||||
// 但语法更紧凑(无臂返回值);类比 Go `switch mode { case Cpu, Both: ... }`。
|
||||
if matches!(bench_args.mode, PipelineMode::Cpu | PipelineMode::Both) {
|
||||
let output = output_for_mode(&bench_args.output, PipelineMode::Cpu, split_outputs);
|
||||
// Some(...) 把 Result<FrameStats> 包成 Option<Result<FrameStats>>,再 ? 解开 Result;最终 cpu_stats = Option<FrameStats>。
|
||||
// 注意 `?` 在 Option 上下文也工作(需要 main 返回 Option,但这里 main 返回 Result,所以 ? 只对 Result 起作用,
|
||||
// Some(...) 是显式包装,里面的 run_cpu_pipeline()? 把 Err 传到 main)。
|
||||
cpu_stats = Some(run_cpu_pipeline(
|
||||
&cap,
|
||||
&frames_ctx,
|
||||
@@ -1190,6 +1328,7 @@ fn main() -> Result<()> {
|
||||
)?);
|
||||
}
|
||||
|
||||
// as_ref():把 &Option<T> 借用(避免消耗 T);print_detailed_results 接收 &FrameStats。
|
||||
if let Some(stats) = cpu_stats.as_ref() {
|
||||
print_detailed_results("CPU", stats, src_width, src_height, enc_width, enc_height);
|
||||
}
|
||||
@@ -1198,6 +1337,9 @@ fn main() -> Result<()> {
|
||||
}
|
||||
print_comparison(cpu_stats.as_ref(), gpu_stats.as_ref());
|
||||
|
||||
// 迭代器链:把两个 Option 串成统一迭代器,.any() 短路检查是否有任何一条流水线低于 30 FPS。
|
||||
// Option::into_iter():把 Option<T> 转为 0/1 元素迭代器;chain 把两段接起来。
|
||||
// 类比 Go:`var all []*FrameStats; if cpu != nil { all = append(all, cpu) }; for _, s := range all { if s.FPS < 30 {...} }`。
|
||||
if cpu_stats
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
|
||||
Reference in New Issue
Block a user