feat: GPU-downscale + software H.264 encode pipeline (WIP)

Add SwEncState in avhw.rs: GPU pipeline using scale_vaapi to downscale
4K BGRA -> 2K NV12 on AMD iGPU, then software encode with libopenh264.

- import_dma_buf_to_vaapi: av_hwframe_map based DMA-BUF import
- SwEncState: GPU filter graph (scale_vaapi) + NV12->YUV420P + libopenh264
- state_portal.rs: integrated SwEncState, auto DRM device detection
- vaapi_import_bench.rs: CPU vs GPU pipeline benchmark
- sw_encode_bench.rs: software encode benchmark

Benchmark results: GPU pipeline ~91 FPS theoretical (10.95ms/frame)
vs CPU pipeline ~33 FPS (30.21ms/frame).

Known issue: only 1 frame encoded in production recording,
diagnostic STATS logging added to debug frame flow.
This commit is contained in:
dailz
2026-05-29 22:04:12 +08:00
parent 55abb5e56d
commit d80b34f44f
9 changed files with 2416 additions and 305 deletions
+61 -43
View File
@@ -16,7 +16,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::thread::{self, JoinHandle};
use anyhow::Result;
use crossbeam_channel::{Receiver, Sender, bounded};
use crossbeam_channel::{bounded, Receiver, Sender};
use tokio::runtime::Runtime;
use crate::args::Args;
@@ -105,9 +105,7 @@ impl CapPortal {
// 通过 Portal 获取 PipeWire 连接 fd 和节点 ID
// block_on 在此处同步等待异步 Portal 调用完成
let (pw_fd, node_id) = rt.block_on(async {
Self::setup_portal().await
})?;
let (pw_fd, node_id) = rt.block_on(async { Self::setup_portal().await })?;
let (frame_tx, frame_rx) = bounded(3);
let (event_tx, event_rx) = bounded(8);
@@ -181,9 +179,9 @@ impl CapPortal {
use ashpd::desktop::PersistMode;
// 创建 Screencast D-Bus 代理,与桌面环境的 Portal 服务通信
let proxy = Screencast::new().await.map_err(|e| {
anyhow::anyhow!("Failed to create Screencast proxy: {e}")
})?;
let proxy = Screencast::new()
.await
.map_err(|e| anyhow::anyhow!("Failed to create Screencast proxy: {e}"))?;
// 创建 ScreenCast 会话(每个会话对应一次屏幕录制请求)
let session = proxy
@@ -287,10 +285,10 @@ impl Drop for CapPortal {
fn pipewire_thread(ctx: PwThreadCtx) {
use pipewire as pw;
use pw::properties::properties;
use pw::spa::param::video::VideoInfoRaw;
use pw::stream::{StreamBox, StreamFlags};
use std::cell::Cell;
use std::rc::Rc;
use pw::spa::param::video::VideoInfoRaw;
// 初始化 PipeWire 进程全局库。
//
@@ -329,9 +327,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
let core = match context.connect_fd(pw_fd, None) {
Ok(c) => c,
Err(e) => {
let _ = event_tx.try_send(PwCtrlEvent::Error(format!(
"connect_fd failed: {e}"
)));
let _ = event_tx.try_send(PwCtrlEvent::Error(format!("connect_fd failed: {e}")));
return;
}
};
@@ -357,8 +353,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
}
};
let format_info: Rc<Cell<Option<(u32, u32, u32, u64)>>> =
Rc::new(Cell::new(None));
let format_info: Rc<Cell<Option<(u32, u32, u32, u64)>>> = Rc::new(Cell::new(None));
let event_tx_state = event_tx.clone();
let _listener = stream
@@ -366,8 +361,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
.state_changed(move |_, _, old, new| {
tracing::debug!("PipeWire stream state: {old:?} -> {new:?}");
match new {
pw::stream::StreamState::Error(_)
| pw::stream::StreamState::Unconnected => {
pw::stream::StreamState::Error(_) | pw::stream::StreamState::Unconnected => {
let _ = event_tx_state.try_send(PwCtrlEvent::StreamEnded);
}
_ => {}
@@ -436,7 +430,8 @@ fn pipewire_thread(ctx: PwThreadCtx) {
// 从第一个数据项中获取 DMA-BUF 文件描述符
// 通过 libspa 的 Data 包装类型安全地访问 SPA 数据结构
let data_ref: &pw::spa::buffer::Data = unsafe { &*(datas_ptr as *const pw::spa::buffer::Data) };
let data_ref: &pw::spa::buffer::Data =
unsafe { &*(datas_ptr as *const pw::spa::buffer::Data) };
let fd = data_ref.fd();
if fd < 0 {
unsafe { stream.queue_raw_buffer(raw_buf) };
@@ -462,7 +457,8 @@ fn pipewire_thread(ctx: PwThreadCtx) {
for i in 0..n_metas {
let meta = &*metas.add(i as usize);
if meta.type_ == libspa::sys::SPA_META_Header
&& meta.size as usize >= std::mem::size_of::<libspa::sys::spa_meta_header>()
&& meta.size as usize
>= std::mem::size_of::<libspa::sys::spa_meta_header>()
&& !meta.data.is_null()
{
let header = &*(meta.data as *const libspa::sys::spa_meta_header);
@@ -505,9 +501,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
pts,
};
if let Err(crossbeam_channel::TrySendError::Full(_)) =
frame_tx.try_send(frame)
{
if let Err(crossbeam_channel::TrySendError::Full(_)) = frame_tx.try_send(frame) {
let prev = dropped.fetch_add(1, Ordering::Relaxed);
if prev > 0 && prev % 30 == 0 {
tracing::warn!("dropped {prev} frames total: encoder backlog");
@@ -593,35 +587,65 @@ const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 {
/// 此函数建立了两者之间的映射关系。
///
/// 支持的格式:
/// - BGRA/BGRx: 蓝绿红(Alpha/X) 32位格式
/// - RGBA/RGBx: 红绿蓝(Alpha/X) 32位格式
/// - ARGB/xRGB: Alpha/X-红绿蓝 32位格式 (映射为 AR24/XR24)
/// - ABGR/xBGR: Alpha/X-蓝绿红 32位格式 (映射为 AB24/XB24)
///
/// 不支持的格式返回 0
/// DRM 格式名描述像素值位布局(大端序),而非内存字节序。
/// 例如 DRM_FORMAT_ARGB8888 在小端 x86 上内存为 [B,G,R,A] = PipeWire BGRA。
fn spa_to_drm_fourcc(format: libspa::param::video::VideoFormat) -> u32 {
use drm_fourcc::DrmFourcc;
use libspa::param::video::VideoFormat;
match format {
VideoFormat::BGRA => fourcc(b'B', b'G', b'R', b'A'),
VideoFormat::BGRx => fourcc(b'B', b'G', b'R', b'X'),
VideoFormat::RGBA => fourcc(b'R', b'G', b'B', b'A'),
VideoFormat::RGBx => fourcc(b'R', b'G', b'B', b'X'),
VideoFormat::ARGB => fourcc(b'A', b'R', b'2', b'4'),
VideoFormat::xRGB => fourcc(b'X', b'R', b'2', b'4'),
VideoFormat::ABGR => fourcc(b'A', b'B', b'2', b'4'),
VideoFormat::xBGR => fourcc(b'X', b'B', b'2', b'4'),
// 不支持的格式返回 0,调用者应检查此值
_ => 0, }
VideoFormat::BGRA => DrmFourcc::Argb8888 as u32,
VideoFormat::BGRx => DrmFourcc::Xrgb8888 as u32,
VideoFormat::RGBA => DrmFourcc::Abgr8888 as u32,
VideoFormat::RGBx => DrmFourcc::Xbgr8888 as u32,
VideoFormat::ARGB => DrmFourcc::Bgra8888 as u32,
VideoFormat::xRGB => DrmFourcc::Bgrx8888 as u32,
VideoFormat::ABGR => DrmFourcc::Rgba8888 as u32,
VideoFormat::xBGR => DrmFourcc::Rgbx8888 as u32,
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use drm_fourcc::DrmFourcc;
#[test]
fn spa_to_drm_fourcc_bgra() {
fn spa_to_drm_fourcc_all_32bit() {
use libspa::param::video::VideoFormat;
assert_eq!(spa_to_drm_fourcc(VideoFormat::BGRA), fourcc(b'B', b'G', b'R', b'A'));
assert_eq!(
spa_to_drm_fourcc(VideoFormat::BGRA),
DrmFourcc::Argb8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::BGRx),
DrmFourcc::Xrgb8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::RGBA),
DrmFourcc::Abgr8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::RGBx),
DrmFourcc::Xbgr8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::ARGB),
DrmFourcc::Bgra8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::xRGB),
DrmFourcc::Bgrx8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::ABGR),
DrmFourcc::Rgba8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::xBGR),
DrmFourcc::Rgbx8888 as u32
);
}
#[test]
@@ -629,10 +653,4 @@ mod tests {
use libspa::param::video::VideoFormat;
assert_eq!(spa_to_drm_fourcc(VideoFormat::NV12), 0);
}
#[test]
fn fourcc_values() {
assert_eq!(fourcc(b'B', b'G', b'R', b'A'), 0x41524742);
assert_eq!(fourcc(b'R', b'G', b'B', b'A'), 0x41424752);
}
}