feat(portal): async encode pipeline - decouple capture from encoding

Split synchronous encode pipeline so sws_scale + libx264 runs on a
dedicated thread, leaving only VAAPI import + GPU scale + GPU→CPU
transfer on the main capture thread.

Problem: encode_p95 occasionally hit 74ms, blocking the entire capture
pipeline and causing capture_gap_max=356ms stutter.

Solution:
- avhw.rs: Split SwEncState into SwEncImport (main thread: VAAPI import,
  filter_graph scale, GPU→CPU transfer) and SwEncEncode (encode thread:
  sws_scale NV12→YUV420P, libx264 encode). New CpuNv12Frame struct
  carries owned pixel data across threads via crossbeam channel.
  SwEncState wraps both for backward compat (MP4/sync path untouched).
- state_portal.rs: WebRTC portal path spawns 'wl-webrtc-encode' thread
  with bounded(2) input channel (drop-newest backpressure) and separate
  timing channel. Graceful shutdown: drop webrtc_rx → drop input_tx →
  join encode thread → flush sync encoder.
- stats.rs: Add record_import() + record_encode_thread() for async timing.

Results: encode_p95 stable at 2.9-4.2ms (was 11-74ms), capture_fps
stable 59-60fps, cap_gap_p95 17-19ms. Remaining capture stalls traced
to PipeWire compositor frame delivery (external, not our code).
This commit is contained in:
dailz
2026-06-07 16:55:28 +08:00
parent aae030f309
commit 826f544569
8 changed files with 561 additions and 236 deletions

View File

@@ -15,6 +15,7 @@ mod backend_detect; // 截屏后端自动检测wlroots vs Portal/PipeWire
mod cap_portal; // XDG Portal 屏幕捕获
mod cap_wlr_screencopy; // wlroots wlr-screencopy 截屏协议
mod fps_limit; // 帧率限制器
mod stats; // 管道性能统计(卡顿诊断)
mod state; // wlr-screencopy 后端的主状态机
mod state_portal; // Portal/PipeWire 后端的主状态机
mod transform; // 图像变换(旋转/翻转)
@@ -43,18 +44,23 @@ fn main() -> Result<()> {
// 解析命令行参数
let args = Args::parse();
// 根据是否启用 verbose 模式设置日志级别
// 根据 verbose 模式或 RUST_LOG 环境变量设置日志级别
// 支持 RUST_LOG 粒度控制(如 RUST_LOG=wl_webrtc::webrtc=trace
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| {
if args.verbose {
tracing_subscriber::EnvFilter::new("debug")
} else {
tracing_subscriber::EnvFilter::new("info")
}
});
tracing_subscriber::fmt()
.with_max_level(if args.verbose {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
})
.with_env_filter(env_filter)
.with_writer(std::io::stderr)
.init();
tracing::info!("wl-webrtc starting");
tracing::debug!("Args: {:?}", args);
tracing::debug!("Args: output={:?} fps={} codec={} port={} verbose={}", args.output, args.fps, args.codec, args.port, args.verbose);
// MVP 阶段仅支持 H.264 编码,不支持 HEVC
if args.codec != "h264" {
@@ -250,7 +256,7 @@ fn run_wlr_screencopy(args: Args) -> Result<()> {
// 状态机遇到致命错误时退出
if state.errored {
tracing::error!("Fatal error in state machine, exiting");
tracing::error!("Fatal error in state machine (check preceding error logs), exiting");
running = false;
}
@@ -346,7 +352,7 @@ fn run_portal_pipewire(args: Args) -> Result<()> {
// Portal 状态机遇到致命错误时退出
if state.is_errored() {
tracing::error!("Fatal error in portal state machine, exiting");
tracing::error!("Fatal error in portal state machine (check preceding error logs), exiting");
running = false;
}
}