docs(state_portal): [1/2] 中文注释 Portal 状态机初始化
This commit is contained in:
@@ -1,3 +1,32 @@
|
|||||||
|
//! Portal 后端的主状态机:通过 PipeWire + DMA-BUF 进行屏幕采集并软件编码。
|
||||||
|
//!
|
||||||
|
//! ## 整体角色
|
||||||
|
//!
|
||||||
|
//! `StatePortal` 与 `src/state.rs::State` 是平行的两条采集路径:
|
||||||
|
//! - `state.rs`(wlroots 路径):由外层 `mio` 事件循环驱动(手工版 epoll),
|
||||||
|
//! 通过 `zwlr_screencopy_manager_v1` 协议一帧一帧地拉取。
|
||||||
|
//! - `state_portal.rs`(本文件,XDG Portal / PipeWire 路径):由 `CapPortal`
|
||||||
|
//! 通过 `crossbeam_channel::Receiver<PwDmaBufFrame>` 推帧;本状态机只负责"消费"。
|
||||||
|
//!
|
||||||
|
//! ## 异步模型的真相
|
||||||
|
//!
|
||||||
|
//! 本文件**不**使用 `mio` 或 `tokio`——`CapPortal` 内部在独立线程跑 PipeWire
|
||||||
|
//! asyncio loop,把 DMA-BUF 帧通过 crossbeam channel 投递出来;外层 `main.rs`
|
||||||
|
//! 只需在 `while !is_errored()` 循环里轮询 `poll_and_encode(block)`。编码线程与
|
||||||
|
//! WebRTC 线程通过 `std::thread::spawn`(不是 `tokio::spawn`)启动,再借助
|
||||||
|
//! crossbeam channel 与主线程通信——类比 Go 的 `go func()` + channel。
|
||||||
|
//!
|
||||||
|
//! ## 阶段机
|
||||||
|
//!
|
||||||
|
//! `PortalStage::WaitingForFormat`(等首帧以确定格式)→ `Streaming`(持续编码)。
|
||||||
|
//!
|
||||||
|
//! ## 注意
|
||||||
|
//!
|
||||||
|
//! - T9a(本块)覆盖文件头 + struct 定义 + `impl StatePortal`(至 `fn encode_thread_loop` 之前);
|
||||||
|
//! T9b 覆盖 `encode_thread_loop` / `webrtc_thread_loop` / `resolve_drm_device` 等自由函数。
|
||||||
|
//! - 多处 `unsafe` 调用 FFmpeg/VAAPI FFI;现有英文 `// SAFETY:` 保留不动,
|
||||||
|
//! 本任务在每个 unsafe 块上方加普通 `//` 中文概述(不新增 `// SAFETY:`)。
|
||||||
|
|
||||||
// 采集门户状态模块 —— 通过 PipeWire/DMA-BUF 进行屏幕采集并编码
|
// 采集门户状态模块 —— 通过 PipeWire/DMA-BUF 进行屏幕采集并编码
|
||||||
use std::os::fd::AsRawFd;
|
use std::os::fd::AsRawFd;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -24,12 +53,24 @@ enum PortalStage {
|
|||||||
Streaming,
|
Streaming,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 编码线程单帧计时回执——由 `encode_thread_loop` 通过 `timing_tx` 发回主线程,
|
||||||
|
/// 用于在 `PipelineStats` 中窗口化统计 `sws_us`(libswscale 缩放开销)和
|
||||||
|
/// `encode_us`(H.264 软件编码开销)。类比 Go 的 `type EncodeThreadTiming struct`。
|
||||||
struct EncodeThreadTiming {
|
struct EncodeThreadTiming {
|
||||||
sws_us: u64,
|
sws_us: u64,
|
||||||
encode_us: u64,
|
encode_us: u64,
|
||||||
output_bytes: usize,
|
output_bytes: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 编码工作线程的句柄与通信端点。
|
||||||
|
///
|
||||||
|
/// 由主线程持有,负责把 NV12 帧 (`CpuNv12Frame`) 通过 `input_tx` 投递给
|
||||||
|
/// `encode_thread_loop`;编码完成后通过 `timing_rx` 收回单帧计时;`duplicate_count`
|
||||||
|
/// 是跨线程共享的 `Arc<AtomicU64>`(类比 Go 的 `*uint64` protected by atomic),
|
||||||
|
/// 用于统计被去重跳过的帧数(影响 BWE 与丢弃策略)。
|
||||||
|
///
|
||||||
|
/// 字段全部用 `Option<...>`/`Sender`/`Receiver` 包装,是为了在 `shutdown` 时
|
||||||
|
/// 能用 `Option::take()` 把所有权转移到本地变量、显式 drop `input_tx`、再 `join()`。
|
||||||
struct EncodeThread {
|
struct EncodeThread {
|
||||||
handle: Option<std::thread::JoinHandle<()>>,
|
handle: Option<std::thread::JoinHandle<()>>,
|
||||||
input_tx: crossbeam_channel::Sender<CpuNv12Frame>,
|
input_tx: crossbeam_channel::Sender<CpuNv12Frame>,
|
||||||
@@ -37,6 +78,11 @@ struct EncodeThread {
|
|||||||
duplicate_count: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
duplicate_count: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// WebRTC 工作线程的句柄与单向上行通道。
|
||||||
|
///
|
||||||
|
/// 主线程只能通过 `sent_gap_rx` **被动接收** WebRTC 线程上报的"已发送帧间隔/老化"
|
||||||
|
/// 指标(用于 `PipelineStats::record_send_from_thread`)。下行的码率 / 分辨率 / 暂停
|
||||||
|
/// 控制走另一组 channel(`bitrate_tx` / `resolution_tx` / `webrtc_paused`),不在此处。
|
||||||
struct WebrtcThread {
|
struct WebrtcThread {
|
||||||
handle: Option<std::thread::JoinHandle<()>>,
|
handle: Option<std::thread::JoinHandle<()>>,
|
||||||
sent_gap_rx: crossbeam_channel::Receiver<(f64, Option<f64>)>,
|
sent_gap_rx: crossbeam_channel::Receiver<(f64, Option<f64>)>,
|
||||||
@@ -71,6 +117,14 @@ pub struct StatePortal {
|
|||||||
last_pts_emitted: Option<i64>,
|
last_pts_emitted: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `impl StatePortal` 块集中了门户路径的所有主线程逻辑:
|
||||||
|
// - `new`:构造(DRM 设备探测 + CapPortal 初始化;编码器延后到首帧)。
|
||||||
|
// - `poll_and_encode`:外层 main 循环每轮调用一次,处理 1 个 PipeWire 帧 / 控制事件。
|
||||||
|
// - `shutdown`:幂等清理(编码线程 → WebRTC 线程 → MP4 flush)。
|
||||||
|
// - 私有辅助:`record_capture_timeout` / `record_frame_arrival`(采集空闲日志节流)、
|
||||||
|
// `resolve_drm_device_for_frame`(DMA-BUF 导入兼容性探测)、
|
||||||
|
// `handle_pw_frame`(VAAPI 导入 + 软件编码)、`compute_capture_pts`(90kHz RTP PTS)。
|
||||||
|
// 内部不使用任何锁——所有 `&mut self` 由外层 main 循环单线程串行化保证独占。
|
||||||
impl StatePortal {
|
impl StatePortal {
|
||||||
/// 创建门户状态实例
|
/// 创建门户状态实例
|
||||||
///
|
///
|
||||||
@@ -219,6 +273,10 @@ impl StatePortal {
|
|||||||
if self.webrtc.is_some() {
|
if self.webrtc.is_some() {
|
||||||
let paused = self.webrtc_paused.as_ref()
|
let paused = self.webrtc_paused.as_ref()
|
||||||
.ok_or_else(|| anyhow::anyhow!("internal invariant broken: webrtc_paused missing while WebRTC mode is active"))?;
|
.ok_or_else(|| anyhow::anyhow!("internal invariant broken: webrtc_paused missing while WebRTC mode is active"))?;
|
||||||
|
// WebRTC 模式需要 6 路 crossbeam channel 协调主线程 ↔ 编码线程 ↔ WebRTC 线程。
|
||||||
|
// `crossbeam_channel::bounded::<T>(n)` 类比 Go 的 `make(chan T, n)`——
|
||||||
|
// 容量满时 `send` 阻塞、空时 `recv` 阻塞;返回的 `(Sender, Receiver)` 各占一份
|
||||||
|
//所有权,可 move 到不同线程(前提是元素类型 `T: Send`)。
|
||||||
let (resolution_tx, resolution_rx) =
|
let (resolution_tx, resolution_rx) =
|
||||||
crossbeam_channel::bounded::<BitrateCommand>(4);
|
crossbeam_channel::bounded::<BitrateCommand>(4);
|
||||||
let (encoder_resolution_tx, encoder_resolution_rx) =
|
let (encoder_resolution_tx, encoder_resolution_rx) =
|
||||||
@@ -252,7 +310,19 @@ impl StatePortal {
|
|||||||
let duplicate_count = std::sync::Arc::new(
|
let duplicate_count = std::sync::Arc::new(
|
||||||
std::sync::atomic::AtomicU64::new(0),
|
std::sync::atomic::AtomicU64::new(0),
|
||||||
);
|
);
|
||||||
|
// Arc 引用计数克隆(不是深拷贝)——`duplicate_count` 留在主线程,
|
||||||
|
// `duplicate_count_for_thread` move 进编码线程;两者指向同一原子。
|
||||||
|
// 类比 Go 的 `*uint64` + atomic.Store,但 Rust 用类型系统保证线程安全。
|
||||||
let duplicate_count_for_thread = duplicate_count.clone();
|
let duplicate_count_for_thread = duplicate_count.clone();
|
||||||
|
// `std::thread::Builder::new().name(...).spawn(move || {...})?`:
|
||||||
|
// - 类比 Go 的 `go func() {...}()`,但返回 `JoinHandle<T>` 而非 fire-and-forget——
|
||||||
|
// 主线程可在 shutdown 时 `handle.join()` 等待子线程退出。
|
||||||
|
// - **不**用 `tokio::spawn`:编码是 CPU 密集 + 阻塞 FFmpeg 调用,
|
||||||
|
// 不需要 async/await;标准线程更直接。
|
||||||
|
// - `move ||` 闭包:把 `encode` / `input_rx` / `timing_tx` /
|
||||||
|
// `duplicate_count_for_thread` 的所有权**转移**给子线程(类比 Go 里把变量
|
||||||
|
// 显式传入 goroutine 闭包参数)。
|
||||||
|
// - `?` 传播 `io::Error`——线程创建可能失败(资源限制)。
|
||||||
let handle = std::thread::Builder::new()
|
let handle = std::thread::Builder::new()
|
||||||
.name("wl-webrtc-encode".into())
|
.name("wl-webrtc-encode".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
@@ -283,6 +353,10 @@ impl StatePortal {
|
|||||||
let max_bitrate = self.args.max_bitrate;
|
let max_bitrate = self.args.max_bitrate;
|
||||||
let (sent_gap_tx, sent_gap_rx) =
|
let (sent_gap_tx, sent_gap_rx) =
|
||||||
crossbeam_channel::bounded::<(f64, Option<f64>)>(64);
|
crossbeam_channel::bounded::<(f64, Option<f64>)>(64);
|
||||||
|
// WebRTC 工作线程:同上 `std::thread::spawn(move || ...)` 模式——
|
||||||
|
// 内部跑 str0m 的 asyncio loop(`WebRtcState` 自己驱动),
|
||||||
|
// 通过 `webrtc_rx` 接收 H.264 帧、通过 `bitrate_tx` / `resolution_tx`
|
||||||
|
// 接收码率/分辨率指令、通过 `sent_gap_tx` 上报发送指标。
|
||||||
let webrtc_handle = std::thread::Builder::new()
|
let webrtc_handle = std::thread::Builder::new()
|
||||||
.name("wl-webrtc-webrtc".into())
|
.name("wl-webrtc-webrtc".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
@@ -366,6 +440,11 @@ impl StatePortal {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 记录"采集超时"——本次轮询未取到帧(PipeWire 队列空)。
|
||||||
|
///
|
||||||
|
/// 因为 Wayland 是 damage-driven(只有画面变化才推帧),静态画面下长时间无帧
|
||||||
|
/// 是**正常**行为,不是 compositor 卡死。所以本函数只做"5 秒阈值后的 DEBUG 一次性日志",
|
||||||
|
/// 用 `idle_log_start` 字段保证每次空闲区间只发一条日志(issue #15 / #18)。
|
||||||
fn record_capture_timeout(&mut self) {
|
fn record_capture_timeout(&mut self) {
|
||||||
let Some(last_capture_arrival) = self.last_capture_arrival else {
|
let Some(last_capture_arrival) = self.last_capture_arrival else {
|
||||||
return;
|
return;
|
||||||
@@ -391,6 +470,11 @@ impl StatePortal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 记录"采集到达"——本次轮询成功取到一帧。
|
||||||
|
///
|
||||||
|
/// 与 `record_capture_timeout` 互补:若之前处于空闲区间,则通过 `Option::take()`
|
||||||
|
/// 取出 `idle_log_start` 并发一条 "resumed after idle" DEBUG 日志;然后刷新
|
||||||
|
/// `last_capture_arrival` 时间戳。两者共同实现"一次性空闲日志"语义。
|
||||||
fn record_frame_arrival(&mut self) {
|
fn record_frame_arrival(&mut self) {
|
||||||
if let Some(idle_start) = self.idle_log_start.take() {
|
if let Some(idle_start) = self.idle_log_start.take() {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
@@ -459,6 +543,10 @@ impl StatePortal {
|
|||||||
// processing — DMA-BUF import, VAAPI scale, NV12 clone, channel send, and
|
// processing — DMA-BUF import, VAAPI scale, NV12 clone, channel send, and
|
||||||
// encode thread wakeup. This eliminates ~60fps of pointless work during
|
// encode thread wakeup. This eliminates ~60fps of pointless work during
|
||||||
// the pre-connect idle window. MP4 mode (webrtc_paused == None) is unaffected.
|
// the pre-connect idle window. MP4 mode (webrtc_paused == None) is unaffected.
|
||||||
|
// `Arc<AtomicBool>` 类比 Go 的 `*atomic.Bool`——`Arc` 提供跨线程共享所有权
|
||||||
|
// (引用计数原子递增/递减),`AtomicBool` 提供无锁读/写。
|
||||||
|
// `Ordering::Relaxed`:只保证单变量原子性,不建立与其他变量的 happens-before 关系——
|
||||||
|
// 对"暂停标志"足够(不需要它做屏障同步)。
|
||||||
if let Some(paused) = &self.webrtc_paused {
|
if let Some(paused) = &self.webrtc_paused {
|
||||||
if paused.load(Ordering::Relaxed) {
|
if paused.load(Ordering::Relaxed) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -478,6 +566,10 @@ impl StatePortal {
|
|||||||
|
|
||||||
if let Some(enc) = self.enc.as_mut() {
|
if let Some(enc) = self.enc.as_mut() {
|
||||||
// 将 DMA-BUF 帧零拷贝导入 VAAPI 硬件帧池
|
// 将 DMA-BUF 帧零拷贝导入 VAAPI 硬件帧池
|
||||||
|
// unsafe:FFI 调用 FFmpeg `av_hwframe_ctx_init` / `av_hwframe_map` 系列,
|
||||||
|
// 内部会读取 `enc.frames_rgb()` 指向的 `AVBufferRef`(硬件帧池),
|
||||||
|
// 并把 `frame.fd.as_raw_fd()`(DMA-BUF dmabuf fd)注册到 VAAPI。
|
||||||
|
// 安全性前提:`enc` 在本线程独占(main 串行化保证)、`frame.fd` 未被 close。
|
||||||
let mut vaapi_frame = unsafe {
|
let mut vaapi_frame = unsafe {
|
||||||
avhw::import_dma_buf_to_vaapi(
|
avhw::import_dma_buf_to_vaapi(
|
||||||
enc.frames_rgb().as_ptr(),
|
enc.frames_rgb().as_ptr(),
|
||||||
@@ -515,6 +607,8 @@ impl StatePortal {
|
|||||||
};
|
};
|
||||||
self.stats.record_encode(&timings);
|
self.stats.record_encode(&timings);
|
||||||
} else if let Some(import) = self.enc_import.as_mut() {
|
} else if let Some(import) = self.enc_import.as_mut() {
|
||||||
|
// 同上 unsafe:DMA-BUF → VAAPI 导入;`import.frames_rgb()` 是与编码线程
|
||||||
|
// **不共享**的独立硬件帧池(避免与 `import_and_scale` 的回读路径竞争)。
|
||||||
let mut vaapi_frame = unsafe {
|
let mut vaapi_frame = unsafe {
|
||||||
avhw::import_dma_buf_to_vaapi(
|
avhw::import_dma_buf_to_vaapi(
|
||||||
import.frames_rgb().as_ptr(),
|
import.frames_rgb().as_ptr(),
|
||||||
@@ -540,6 +634,9 @@ impl StatePortal {
|
|||||||
"internal invariant broken: encode thread missing while async import is active"
|
"internal invariant broken: encode thread missing while async import is active"
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
// `try_send` 类比 Go 的 `select { case ch <- v: default: }`——
|
||||||
|
// 非阻塞投递;三种结果分别处理:成功递增、满了丢弃(DEBUG 日志)、
|
||||||
|
// 对端关闭(致命,置 `errored=true` 让外层循环退出)。
|
||||||
match enc_thread.input_tx.try_send(cpu_nv12) {
|
match enc_thread.input_tx.try_send(cpu_nv12) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.frames_encoded += 1;
|
self.frames_encoded += 1;
|
||||||
@@ -608,6 +705,10 @@ impl StatePortal {
|
|||||||
self.shutdown_started = true;
|
self.shutdown_started = true;
|
||||||
|
|
||||||
// 1. Stop encode thread (drops webrtc_tx → signals WebRTC thread to exit)
|
// 1. Stop encode thread (drops webrtc_tx → signals WebRTC thread to exit)
|
||||||
|
// `Option::take()` 把 `EncodeThread` 的所有权从 `self.enc_thread` 转移到本地 `enc_thread`,
|
||||||
|
// 同时 `self.enc_thread` 变成 `None`——这是 Rust 里"消费字段但保留父结构体"的标准习语,
|
||||||
|
// 类比 Go 里把字段设为 nil 但保留外层 struct。接下来显式 `drop(input_tx)` 关闭 channel,
|
||||||
|
// 编码线程的 `input_rx.recv()` 会返回 `Err(Disconnected)` 从而退出循环。
|
||||||
if let Some(mut enc_thread) = self.enc_thread.take() {
|
if let Some(mut enc_thread) = self.enc_thread.take() {
|
||||||
drop(enc_thread.input_tx);
|
drop(enc_thread.input_tx);
|
||||||
if let Some(handle) = enc_thread.handle.take() {
|
if let Some(handle) = enc_thread.handle.take() {
|
||||||
|
|||||||
Reference in New Issue
Block a user