perf(portal): achieve 58-60fps PipeWire screen capture
- Force PipeWire quantum=512 via NODE_FORCE_QUANTUM (48000/512=93Hz scheduling) - Switch to libx264 ultrafast/zerolatency with 6 threads - Use two-phase poll_and_encode: blocking recv_timeout for first frame, non-blocking try_recv drain for subsequent frames - Remove fps_limit from portal path (PW already rate-limits via quantum/KWin; fps_limit's min_interval was silently dropping ~10% of valid frames) - Remove diagnostic instrumentation (TIMING/PIPEWIRE logs, timing fields, pw_stats counters) - Add lightweight production stats: per-10s fps log + shutdown summary - Prefer libx264 over libopenh264 (better quality at same speed)
This commit is contained in:
+22
-24
@@ -13,6 +13,7 @@
|
||||
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -80,13 +81,10 @@ pub struct CapPortal {
|
||||
struct PwThreadCtx {
|
||||
frame_tx: Sender<PwDmaBufFrame>,
|
||||
event_tx: Sender<PwCtrlEvent>,
|
||||
dropped: AtomicU64,
|
||||
dropped: Arc<AtomicU64>,
|
||||
shutdown_read: OwnedFd,
|
||||
/// Portal 返回的 PipeWire 远程连接文件描述符
|
||||
pw_fd: OwnedFd,
|
||||
/// Portal 返回的 PipeWire 节点 ID,标识要捕获的屏幕流
|
||||
node_id: u32,
|
||||
/// 目标帧率(当前保留,未直接用于 PipeWire 协商)
|
||||
fps: u32,
|
||||
}
|
||||
|
||||
@@ -96,7 +94,7 @@ impl CapPortal {
|
||||
/// 执行流程:
|
||||
/// 1. 创建 Tokio 运行时(用于异步 Portal 调用)
|
||||
/// 2. 通过 XDG Desktop Portal 请求屏幕录制权限,获取 PipeWire fd 和 node_id
|
||||
/// 3. 创建有界通道(容量 3)用于帧传递
|
||||
/// 3. 创建有界通道(容量 16)用于帧传递
|
||||
/// 4. 创建 eventfd 对,用于线程安全的关闭信号传递
|
||||
/// 5. 启动 PipeWire 捕获线程
|
||||
pub fn new(args: &Args) -> Result<Self> {
|
||||
@@ -107,7 +105,7 @@ impl CapPortal {
|
||||
// block_on 在此处同步等待异步 Portal 调用完成
|
||||
let (pw_fd, node_id) = rt.block_on(async { Self::setup_portal().await })?;
|
||||
|
||||
let (frame_tx, frame_rx) = bounded(3);
|
||||
let (frame_tx, frame_rx) = bounded(16);
|
||||
let (event_tx, event_rx) = bounded(8);
|
||||
|
||||
let efd = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) };
|
||||
@@ -124,10 +122,12 @@ impl CapPortal {
|
||||
return Err(anyhow::anyhow!("dup eventfd failed: {err}"));
|
||||
}
|
||||
|
||||
let pw_dropped = Arc::new(AtomicU64::new(0));
|
||||
|
||||
let ctx = PwThreadCtx {
|
||||
frame_tx,
|
||||
event_tx,
|
||||
dropped: AtomicU64::new(0),
|
||||
dropped: pw_dropped.clone(),
|
||||
shutdown_read: unsafe { OwnedFd::from_raw_fd(efd) },
|
||||
pw_fd,
|
||||
node_id,
|
||||
@@ -305,7 +305,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
|
||||
shutdown_read,
|
||||
pw_fd,
|
||||
node_id,
|
||||
fps: _fps,
|
||||
fps,
|
||||
} = ctx;
|
||||
|
||||
let mainloop = match pw::main_loop::MainLoopBox::new(None) {
|
||||
@@ -344,6 +344,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
|
||||
*pw::keys::MEDIA_TYPE => "Video",
|
||||
*pw::keys::MEDIA_CATEGORY => "Capture",
|
||||
*pw::keys::MEDIA_ROLE => "Screen",
|
||||
*pw::keys::NODE_FORCE_QUANTUM => "512",
|
||||
},
|
||||
) {
|
||||
Ok(s) => s,
|
||||
@@ -359,9 +360,13 @@ fn pipewire_thread(ctx: PwThreadCtx) {
|
||||
let _listener = stream
|
||||
.add_local_listener::<()>()
|
||||
.state_changed(move |_, _, old, new| {
|
||||
tracing::debug!("PipeWire stream state: {old:?} -> {new:?}");
|
||||
tracing::info!("PipeWire stream state: {old:?} -> {new:?}");
|
||||
match new {
|
||||
pw::stream::StreamState::Error(_) | pw::stream::StreamState::Unconnected => {
|
||||
pw::stream::StreamState::Error(e) => {
|
||||
tracing::error!("PipeWire stream error: {e}");
|
||||
let _ = event_tx_state.try_send(PwCtrlEvent::StreamEnded);
|
||||
}
|
||||
pw::stream::StreamState::Unconnected => {
|
||||
let _ = event_tx_state.try_send(PwCtrlEvent::StreamEnded);
|
||||
}
|
||||
_ => {}
|
||||
@@ -390,11 +395,16 @@ fn pipewire_thread(ctx: PwThreadCtx) {
|
||||
let drm_format = spa_to_drm_fourcc(info.format());
|
||||
// 获取 DRM 修饰符,描述 GPU buffer 的内存布局(如 tiling 模式)
|
||||
let modifier = info.modifier();
|
||||
let framerate = info.framerate();
|
||||
let max_framerate = info.max_framerate();
|
||||
// 保存协商后的格式信息,供 process 回调读取
|
||||
format_info.set(Some((width, height, drm_format, modifier)));
|
||||
tracing::info!(
|
||||
"PipeWire format negotiated: {width}x{height}, \
|
||||
drm_format={drm_format:#010x}, modifier={modifier:#x}"
|
||||
drm_format={drm_format:#010x}, modifier={modifier:#x}, \
|
||||
framerate={}/{}, max_framerate={}/{}",
|
||||
framerate.num, framerate.denom,
|
||||
max_framerate.num, max_framerate.denom,
|
||||
);
|
||||
}
|
||||
})
|
||||
@@ -405,15 +415,8 @@ fn pipewire_thread(ctx: PwThreadCtx) {
|
||||
let format_info = format_info.clone();
|
||||
let frame_tx = frame_tx.clone();
|
||||
let dropped = dropped;
|
||||
let process_count = Rc::new(Cell::new(0u64));
|
||||
let process_count_clone = process_count.clone();
|
||||
move |stream, _| {
|
||||
let count = process_count_clone.get() + 1;
|
||||
process_count_clone.set(count);
|
||||
if count <= 5 || count % 60 == 0 {
|
||||
tracing::info!("PipeWire process callback #{count}");
|
||||
}
|
||||
// 从流中出队原始 buffer(包含帧数据的元信息)
|
||||
|
||||
let raw_buf = unsafe { stream.dequeue_raw_buffer() };
|
||||
if raw_buf.is_null() {
|
||||
return;
|
||||
@@ -519,13 +522,8 @@ fn pipewire_thread(ctx: PwThreadCtx) {
|
||||
})
|
||||
.register();
|
||||
|
||||
// 空的参数数组,不主动请求特定格式(由 PipeWire 和源端协商决定)
|
||||
let mut params: [&pw::spa::pod::Pod; 0] = [];
|
||||
|
||||
// 连接到指定的 PipeWire 节点
|
||||
// Direction::Input: 作为消费者(输入方向接收数据)
|
||||
// AUTOCONNECT: 允许 PipeWire 自动连接源和消费者
|
||||
// MAP_BUFFERS: 映射 buffer 到用户空间(DMA-BUF 模式下必须设置)
|
||||
if let Err(e) = stream.connect(
|
||||
pw::spa::utils::Direction::Input,
|
||||
Some(node_id),
|
||||
|
||||
Reference in New Issue
Block a user