From 98eb72e2a2158a2b5d64b1816a27257dc5a1984c Mon Sep 17 00:00:00 2001 From: dailz Date: Mon, 22 Jun 2026 18:17:22 +0800 Subject: [PATCH] =?UTF-8?q?docs(vaapi=5Fimport=5Fbench):=20[2/2]=20?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E6=B3=A8=E9=87=8A=20CPU/GPU=20pipeline=20?= =?UTF-8?q?=E4=B8=8E=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bin/vaapi_import_bench.rs | 142 ++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/src/bin/vaapi_import_bench.rs b/src/bin/vaapi_import_bench.rs index 14ffd33..294a05e 100644 --- a/src/bin/vaapi_import_bench.rs +++ b/src/bin/vaapi_import_bench.rs @@ -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`:任何 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 { + // 构造软件编码器(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;后续 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 { + // 同 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 强制处理。 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 包成 Option>,再 ? 解开 Result;最终 cpu_stats = Option。 + // 注意 `?` 在 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);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 转为 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()