Phase 1 MVP implementation of wl-webrtc: Wayland screen capture tool with hardware-accelerated VAAPI H.264 encoding and WebTransport output. Includes all 9 runtime bug fixes from code audit (fix-audit-issues plan): CRITICAL: - C2: h264_metadata BSF with repeat_sps/repeat_pps in encode pipeline - C4: FpsLimit wired as timing gate in on_copy_complete HIGH: - C3+A2: DRM device discovery via dmabuf feedback MainDevice event, unified resolve_drm_path() helper (CLI > compositor > auto > fallback) - H2: Separate physical_size (mm) from mode_size (pixels) in wl_output - H1+A3: Multi-output warning + named-output-not-found error MEDIUM: - M5: tv_sec u32->u64 to avoid Y2106 timestamp truncation - M4: Guard against SHM Buffer event (DMA-BUF only) Key components: - src/avhw.rs: FFmpeg VAAPI encoder + filter graph + BSF pipeline - src/state.rs: Wayland event loop + output negotiation + screencopy - src/cap_wlr_screencopy.rs: wlr-screencopy capture source - src/fps_limit.rs: Frame rate limiting with configurable target - src/transform.rs: Frame format conversion utilities
444 lines
26 KiB
Markdown
444 lines
26 KiB
Markdown
# wl-screenrec 源码分析
|
||
|
||
## 1. 项目概述
|
||
|
||
wl-screenrec 是一个高性能 Wayland 屏幕录制器,核心特性:原始视频数据不经过 CPU,全程在 GPU 上完成捕获、格式转换和编码。
|
||
|
||
**技术栈**:Rust + FFmpeg(硬件加速)+ Wayland 协议(wayland-client crate)+ mio 事件循环
|
||
|
||
**9 个源文件**:
|
||
|
||
| 文件 | 行数 | 职责 |
|
||
|------|------|------|
|
||
| `main.rs` | ~2430 | 状态机、事件循环、编码管线编排 |
|
||
| `avhw.rs` | ~444 | FFmpeg 硬件设备/帧上下文(VAAPI/Vulkan) |
|
||
| `audio.rs` | ~405 | 音频捕获→解码→重采样→编码(独立线程) |
|
||
| `cap_ext_image_copy.rs` | ~237 | ext-image-copy-capture 协议后端 |
|
||
| `filter.rs` | ~194 | FFmpeg 视频滤镜图(crop+scale+transpose) |
|
||
| `transform.rs` | ~215 | 坐标系变换(处理旋转/翻转) |
|
||
| `cap_wlr_screencopy.rs` | ~165 | wlr-screencopy 协议后端 |
|
||
| `fps_limit.rs` | ~130 | 帧率限制器(VRR 感知) |
|
||
| `fifo.rs` | ~60 | FFmpeg AVAudioFifo 安全封装 |
|
||
|
||
---
|
||
|
||
## 2. 架构概览
|
||
|
||
### 2.1 模块依赖
|
||
|
||
```
|
||
┌──────────────────────────────┐
|
||
│ main.rs │
|
||
└──┬──┬──┬──┬──┬──┬──┬─────────┘
|
||
│ │ │ │ │ │ │
|
||
┌───────────┘ │ │ │ │ │ └──────────┐
|
||
▼ ▼ │ ▼ │ ▼ ▼
|
||
┌──────────┐ ┌────────┐│┌───────┐ ┌────────┐ ┌──────────┐
|
||
│ avhw.rs │ │filter.rs│││audio.rs│ │fps_limit│ │transform │
|
||
└──────────┘ └───┬────┘│└───┬───┘ └────────┘ └──────────┘
|
||
│ │ │
|
||
┌───────┤ │ ▼
|
||
▼ ▼ │ ┌──────┐
|
||
┌────────────┐┌─────┴┐│fifo.rs│
|
||
│cap_wlr_ ││cap_ │└──────┘
|
||
│screencopy ││ext_* │
|
||
└────────────┘└──────┘
|
||
```
|
||
|
||
**依赖层次**:`fifo/fps_limit/transform`(叶节点)→ `avhw/audio` → `filter` → `cap_*` → `main.rs`(核心)。main.rs 与 cap_*.rs 之间存在**双向依赖**:main 定义 `CaptureSource` trait 和 `State<S>`,cap_* 为 `State<Cap*>` 实现 `Dispatch`。
|
||
|
||
### 2.2 State\<S\> 全局状态
|
||
|
||
核心状态结构体持有以下关键字段:
|
||
|
||
- **in_flight_surface**:帧在途状态,跟踪当前帧的捕获生命周期
|
||
- **dma**:DMA-BUF 协议对象,用于 GPU 缓冲区共享
|
||
- **enc**:编码器构造状态机(`EncConstructionStage`),管理从探测到就绪的全过程
|
||
- **starting_timestamp**:首帧时间戳(纳秒),用于音视频同步
|
||
- **args**:命令行参数
|
||
- **errored**:致命错误标志
|
||
- **gm**:Wayland 全局对象列表
|
||
- **xdg_output_manager**:输出几何信息管理器
|
||
|
||
泛型 `S: CaptureSource` 使同一个 State 支持两种截屏后端,无需运行时动态分发。
|
||
|
||
### 2.3 CaptureSource Trait
|
||
|
||
定义了屏幕捕获后端的统一接口契约:
|
||
|
||
- **关联类型 Frame**:每种后端有自己的帧类型
|
||
- **new()**:从 Wayland 全局对象和输出创建后端实例
|
||
- **alloc_frame()**:分配捕获帧,返回 `Option<Frame>` 统一同步/异步两种模式
|
||
- **queue_copy()**:提交 DMA-BUF 缓冲区给合成器,请求捕获
|
||
- **on_done_with_frame()**:帧使用完毕后的回收回调
|
||
|
||
| 实现者 | 协议 | 文件 |
|
||
|--------|------|------|
|
||
| `CapWlrScreencopy` | zwlr-screencopy-unstable-v1 | `cap_wlr_screencopy.rs` |
|
||
| `CapExtImageCopy` | ext-image-copy-capture-v1 | `cap_ext_image_copy.rs` |
|
||
|
||
关键差异:wlr-screencopy 的 `alloc_frame()` 返回 `None`(异步),ext-image-copy 直接返回 `Some(frame)`(同步)。
|
||
|
||
### 2.4 事件循环
|
||
|
||
采用 mio polling + Wayland 事件队列 + Unix 信号三合一架构:
|
||
|
||
- **Token(0)** = 信号(SIGINT/SIGTERM/SIGHUP 退出,SIGUSR1 触发 history flush)
|
||
- **Token(1)** = Wayland 连接 fd → `queue.dispatch_pending(&mut state)`
|
||
- 超时由 FPS 报告周期驱动
|
||
- 退出时仅 `Complete` 状态才 flush 编码器
|
||
|
||
后端自动选择:探测全局列表,优先 ext-image-copy-capture(跨桌面标准),否则回退 wlr-screencopy。
|
||
|
||
---
|
||
|
||
## 3. Wayland 协议交互层
|
||
|
||
### 3.1 两个后端的事件流对比
|
||
|
||
**wlr-screencopy**:
|
||
|
||
```
|
||
capture_output() [异步]
|
||
→ LinuxDmabuf { format, w, h } × N → 收集格式(仅 LINEAR)
|
||
→ BufferDone → negotiate_format() + on_frame_allocd()
|
||
queue_copy(WlBuffer)
|
||
→ Ready { timestamp } → on_copy_complete()
|
||
```
|
||
|
||
**ext-image-copy-capture**:
|
||
|
||
```
|
||
[会话初始化]
|
||
→ BufferSize / DmabufDevice / DmabufFormat × N → 收集约束
|
||
→ Done → negotiate_format()
|
||
[每帧]
|
||
create_frame() [同步,直接返回 Some]
|
||
queue_copy(WlBuffer)
|
||
→ PresentationTime { timestamp } → 暂存
|
||
→ Ready → on_copy_complete()
|
||
```
|
||
|
||
核心差异:wlr 每帧触发格式协商(帧级别),ext 在会话建立时完成(会话级别)。ext 提供真实 modifier 列表,wlr 硬编码 `DrmModifier::LINEAR`。
|
||
|
||
### 3.2 DMA-BUF 缓冲区创建
|
||
|
||
零拷贝路径:
|
||
|
||
```
|
||
AV HW Surface → av_hwframe_map → DRM PRIME 描述符 (DMA-BUF fd)
|
||
→ zwp_linux_dmabuf.create_params → add(planes) → create_immed → WlBuffer
|
||
→ cap.queue_copy(WlBuffer)
|
||
→ 合成器直接写入 GPU Surface(零拷贝)
|
||
```
|
||
|
||
### 3.3 格式协商
|
||
|
||
格式优先级:`XRGB8888` > `XBGR8888` > `XRGB2101010`。VAAPI 模式仅接受 LINEAR modifier,Vulkan 模式接受任意 modifier。
|
||
|
||
DRM 设备发现:两条路径(wlr 的 `MainDevice` / ext 的 `DmabufDevice`),核心逻辑相同:`dev_t` → `DrmNode` → Render 节点路径。回退 `/dev/dri/renderD128`。
|
||
|
||
### 3.4 Dispatch 泛型分发模式
|
||
|
||
三种模式:
|
||
|
||
- **A. 完全泛型**:`Dispatch<ZwpLinuxDmabufV1, ()> for State<S>` — 共享协议,通常空实现
|
||
- **B. 带状态回调的泛型**:`Dispatch<WlOutput, ()> for State<S>` — 需要 `'static`,含实质逻辑
|
||
- **C. 后端专用**:`Dispatch<ZwlrScreencopyFrameV1, ()> for State<CapWlrScreencopy>` — 非泛型,含后端特有逻辑
|
||
|
||
输出探测通过 `WlOutput` + `ZxdgOutputV1` 协作完成,`PartialOutputInfo` 增量收集直到所有字段填充。每个输出收到两次 `Done` 事件,忽略第一次。
|
||
|
||
---
|
||
|
||
## 4. GPU 编码管道
|
||
|
||
### 4.1 零拷贝数据流
|
||
|
||
```
|
||
GPU 帧池 ─alloc()→ HW Surface
|
||
↓
|
||
av_hwframe_map → DMA-BUF fd
|
||
↓
|
||
zwp_linux_dmabuf → WlBuffer (fd 共享)
|
||
↓
|
||
合成器直接写入 GPU Surface
|
||
↓
|
||
buffersrc → GPU 滤镜 (crop/scale/transpose)
|
||
↓
|
||
buffersink → 编码器 (send_frame)
|
||
↓
|
||
receive_packet → Muxer → 文件
|
||
```
|
||
|
||
**整条链路中原始帧数据始终在 GPU 内存,不经过 CPU。**
|
||
|
||
### 4.2 硬件设备上下文
|
||
|
||
两种硬件加速路径:
|
||
|
||
- **VAAPI**:一步创建,直接从 DRM 设备创建 VAAPI 硬件设备上下文
|
||
- **Vulkan**:两步创建,先创建 DRM 上下文,再派生 Vulkan 上下文,中间 DRM 上下文立即释放
|
||
|
||
帧上下文两种用途:
|
||
- **Capture**:Vulkan flags = `SAMPLED | TRANSFER_DST`,tiling = `Drm(modifiers)`
|
||
- **Enc**:Vulkan flags = `VIDEO_ENCODE_SRC_KHR | TRANSFER_DST`,tiling = `Optimal`
|
||
|
||
### 4.3 Vulkan 自引用 Pin 模式
|
||
|
||
`AvHwDevCtxVulkanBuffers` 包含自引用 C 指针链(drm_info → image_fmt_list_info → 内部数组),通过 `Pin<Box<Self>>` 解决。`PhantomPinned` 标记 `!Unpin`,`'static` 是对 FFmpeg C API 的"善意谎言"。
|
||
|
||
### 4.4 FFmpeg 滤镜图
|
||
|
||
```
|
||
buffersrc (HW) → crop → scale → [transpose] → [hwdownload] → buffersink
|
||
```
|
||
|
||
- `hw_frames_ctx` 绑定是零拷贝的关键
|
||
- crop 使用 `exact=1` workaround
|
||
- scale/transpose 按硬件类型选择:`scale_vaapi`/`scale_vulkan`,`transpose_vaapi`/`transpose_vulkan`
|
||
- `hwdownload` 仅在软件编码路径添加
|
||
|
||
### 4.5 编码器选择
|
||
|
||
| Codec | VAAPI | Vulkan |
|
||
|-------|-------|--------|
|
||
| H.264 | `h264_vaapi` | `h264_vulkan` |
|
||
| HEVC | `hevc_vaapi` | `hevc_vulkan` |
|
||
| VP8/VP9 | `vp8/vp9_vaapi` | 不支持 |
|
||
| AV1 | `av1_vaapi` | `av1_vulkan` |
|
||
|
||
选择优先级:`--ffmpeg-encoder` 显式指定 > 硬件编码器(尝试 `low_power=1` 后回退)> 通用编码器。
|
||
|
||
### 4.6 EncodePixelFormat 三路派发
|
||
|
||
`Vaapi(Pixel) / Vulkan(Pixel) / Sw(Pixel)` 在编码器格式设置、硬件上下文绑定、滤镜图构建、帧上下文创建四处做三路匹配。
|
||
|
||
`--no-hw` 路径:捕获仍用 GPU(DMA-BUF),编码前 `hwdownload` 到 CPU,软件编码器(x264 自动 `ultrafast`)。
|
||
|
||
---
|
||
|
||
## 5. 状态机与帧生命周期
|
||
|
||
### 5.1 EncConstructionStage 状态机
|
||
|
||
```
|
||
┌──────────────────┐
|
||
应用启动 │ ProbingOutputs │
|
||
│ └────────┬─────────┘
|
||
▼ │ 所有输出探测完毕
|
||
┌───────────────┐ ▼
|
||
│ ProbingOutputs├──→ ┌──────────────────┐
|
||
└───────────────┘ │EverythingButFmt │
|
||
└────────┬─────────┘
|
||
│ negotiate_format()
|
||
▼
|
||
┌───────────┐ 输出断开
|
||
┌─────→│ Complete │──────────┐
|
||
│ └─────┬─────┘ │
|
||
│ │ ▼
|
||
│ 格式变化 │ ┌──────────────┐
|
||
│ on_new_ │ │OutputWentAway│
|
||
│ capture_ │ └──────┬───────┘
|
||
│ format() │ │ 同名输出重连
|
||
└────────────┘ │
|
||
←───────────────────────┘
|
||
|
||
Intermediate 瞬态存在于所有转换箭头处(mem::replace)
|
||
```
|
||
|
||
关键转换点:
|
||
- **ProbingOutputs → EverythingButFormat**:所有输出探测完毕
|
||
- **EverythingButFormat → Complete**:`negotiate_format()` 创建 EncState
|
||
- **Complete → OutputWentAway**:`on_copy_fail()` 检测到输出断开,**保留 enc 丢弃 cap**
|
||
- **OutputWentAway → Complete**:同名输出重新出现时创建新 cap 复用旧 enc
|
||
|
||
`Intermediate` 瞬态通过 `mem::replace` + `take_enc()` 实现安全所有权转移。`take_enc()` 只允许从 `Complete`/`OutputWentAway` 提取编码器。
|
||
|
||
### 5.2 InFlightSurface 帧生命周期
|
||
|
||
```
|
||
┌──────┐ queue_alloc_frame() ┌─────────────┐
|
||
│ None │ ───────────────────→ │ AllocQueued │
|
||
└──────┘ └──────┬───────┘
|
||
↑ │ on_frame_allocd()
|
||
│ ▼
|
||
│ ┌───────────┐
|
||
│ │ Allocd │
|
||
│ └─────┬─────┘
|
||
│ │ queue_frame_capture()
|
||
│ ▼
|
||
│ ┌──────────────┐
|
||
└────── on_copy_complete ─│ CopyQueued │
|
||
/ on_copy_fail └──────────────┘
|
||
```
|
||
|
||
帧级串行化:同一时间只有一帧在途,通过 `assert!` 强制执行。`CopyQueued` 持有 GPU surface、DRM 映射、Wayland 帧和 buffer 四个资源的所有权,拷贝完成后全部释放并启动下一帧。
|
||
|
||
### 5.3 HistoryState 双模式
|
||
|
||
- **RecordingHistory(Duration, VecDeque\<Packet\>)**:环形缓冲,以关键帧为边界裁剪,确保回放可解码
|
||
- **Recording(i64)**:正常写入,PTS 减去偏移量保证起始对齐
|
||
|
||
SIGUSR1 触发 `RecordingHistory → Recording` 转换:先转换状态,再将历史包通过正常录制路径写出。
|
||
|
||
### 5.4 错误恢复
|
||
|
||
`on_copy_fail()` 三个分支按优先级判断:
|
||
|
||
1. `output_went_away == true` → 保留编码器,进入 `OutputWentAway` 等待重连
|
||
2. `format_change == true` → 预期失败,重置标志后重新分配帧
|
||
3. 其他 → 未知错误,记录日志后重试
|
||
|
||
### 5.5 动态格式切换
|
||
|
||
捕获格式变化时重建 `frames_rgb`、`video_filter`、`enc_video`、`frames_yuv`,但保留 `octx`、`hw_device_ctx`、`audio`、`history_state`。
|
||
|
||
---
|
||
|
||
## 6. 音频管道与辅助模块
|
||
|
||
### 6.1 音频管道
|
||
|
||
独立线程运行,三阶段构造:
|
||
|
||
1. **IncompleteAudioState**:完成编码器选择、设备打开、解码/编码器创建
|
||
2. **AudioHandle**:主线程句柄,含 `Receiver<Packet>` + `AtomicBool` 控制标志
|
||
3. **AudioState**:音频线程内部状态,move 到独立线程
|
||
|
||
数据流:`音频设备 → 解码 → audio_filter(aformat) → AudioFifo(可选) → 编码 → mpsc → 主线程`
|
||
|
||
同步机制:`started` 原子标志在视频首帧时间戳获得后才置 true,确保音视频起点对齐。
|
||
|
||
AudioFifo:解决变长帧编码器需要固定 `frame_size` 的问题。条件判断:编码器不支持 `VARIABLE_FRAME_SIZE` 时创建。
|
||
|
||
### 6.2 帧率限制器
|
||
|
||
VRR 感知设计:引入一帧缓冲延迟判定,避免在 VRR 场景下丢弃"更长时间显示"的帧。
|
||
|
||
```
|
||
on_new_frame(frame, ts):
|
||
第1帧 → 直接通过
|
||
第2帧 → 存入 on_deck 缓冲
|
||
第N帧 → 比较缓冲帧与新帧时间戳:
|
||
新帧太近 → 丢弃缓冲帧
|
||
间隔足够 → 输出缓冲帧,新帧存入 on_deck
|
||
```
|
||
|
||
### 6.3 坐标变换
|
||
|
||
处理 Wayland 输出变换(旋转/翻转)对坐标系的影响:
|
||
|
||
- **transform_basis()**:8 种变换的基矩阵映射
|
||
- **screen_to_frame()**:矩形从屏幕空间到帧空间
|
||
- **transpose_if_transform_transposed()**:90° 旋转时交换宽高
|
||
- **fit_inside_bounds()**:ROI 越界时安全裁剪
|
||
|
||
### 6.4 主线程事件循环集成
|
||
|
||
主循环中音频包在视频帧处理间隙通过 `try_recv` 非阻塞收取,无需额外事件源。
|
||
|
||
退出时 `EncState::flush()` 依次:刷出 FPS 限制器缓冲帧 → flush 音频线程 → 刷视频过滤器 → 发送编码器 EOF → 写容器 trailer。
|
||
|
||
---
|
||
|
||
## 7. 可移植设计模式
|
||
|
||
从代码库中提取的 10 个可复用设计模式,按复杂度从低到高排列。
|
||
|
||
### 7.1 策略 Trait + 泛型状态(CaptureSource)
|
||
**问题**:多后端系统如何在避免运行时动态分发(`dyn Trait`)开销的同时保持类型安全和可扩展性?
|
||
**方案**:定义 `CaptureSource` trait 带关联类型 `Frame`,将整个状态 `State<S: CaptureSource>` 泛型参数化。`State<CapWlrScreencopy>` 和 `State<CapExtImageCopy>` 编译为两个独立单态化类型,后端选择在启动时确定。`alloc_frame()` 返回 `Option<Self::Frame>` 统一了同步和异步两种帧分配模式。
|
||
**移植要点**:
|
||
- 适用后端数量有限(2-5 个)且进程生命周期内不变的场景;需运行时热切换则改用 trait object
|
||
- `State<S>` 中 `Sized` 约束必须,因为 `S` 作为字段存储;编译膨胀需注意大型 State 的泛型实例化
|
||
- main.rs 定义 trait 而 cap_*.rs 实现它,形成双向依赖,大型项目应将 trait 提取到独立模块
|
||
|
||
### 7.2 多态枚举状态机(EncConstructionStage)
|
||
**问题**:Rust 中如何以零开销实现状态机,同时保证状态转换的类型安全?
|
||
**方案**:`EncConstructionStage<S>` 有 5 个枚举变体(`ProbingOutputs`、`EverythingButFormat`、`Complete`、`OutputWentAway`、`Intermediate`),每个携带该状态所需的数据载荷。`Intermediate` 瞬态 + `mem::replace` 组合解决了部分借用限制:match 解构 `&mut self.enc` 同时给 `self.enc` 赋新值。`take_enc()` 通过消费 `self` 确保只有含编码器的状态才能被提取。
|
||
**移植要点**:
|
||
- 3-7 个状态是 enum 状态机甜蜜点;优势是编译期穷尽检查,添加新状态时所有 match 报编译错误
|
||
- `Intermediate` 瞬态必须存在,否则 `mem::replace` 无法满足类型系统要求
|
||
- `Complete` 和 `OutputWentAway` 都持有 `EncState` 但后者丢弃 `cap`,体现"保留昂贵资源、丢弃可重建资源"
|
||
|
||
### 7.3 类型安全帧生命周期(InFlightSurface)
|
||
**问题**:异步 DMA-BUF 传输涉及多个阶段,如何防止在错误阶段执行操作?
|
||
**方案**:`InFlightSurface<S>` 是 4 状态枚举 `None → AllocQueued → Allocd(S::Frame) → CopyQueued{...} → None`。每个状态携带该阶段特有的资源(`CopyQueued` 持有 GPU surface、DRM 映射、Wayland 帧和 buffer)。状态转换通过 `assert!(matches!(...))` 运行时守护,`take()` 方法(`mem::replace`)提供安全取出并自动重置为 `None`。同一时间只有一帧在途。
|
||
**移植要点**:
|
||
- 适用任何"请求→资源就绪→提交操作→操作完成"的异步 I/O 管道
|
||
- 运行时 assert 而非编译期 typestate 是合理权衡:回调驱动的异步场景中编译期类型状态过于复杂
|
||
- RAII 确保 `CopyQueued → None` 路径释放所有资源(DRM 映射、Wayland buffer、帧对象)
|
||
|
||
### 7.4 Pin\<Box\> 自引用结构(Vulkan Buffers)
|
||
**问题**:C 库中的链式结构体(Vulkan pNext 链)内部指针指向同结构其他字段,Rust 中移动会使指针失效,如何安全构建?
|
||
**方案**:`AvHwDevCtxVulkanBuffers` 通过 `PhantomPinned` 标记 `!Unpin`,`Box::pin` 在堆上分配并固定,`get_unchecked_mut` 设置自引用指针。Vulkan 结构体的生命周期标记为 `'static` 作为对 C API 的"善意谎言",实际受 `Pin<Box<>>` 控制。unsafe 代码集中在 `new()` 中,使用方完全安全。`chain_ptr()` 根据有无 DRM modifier 返回不同链头。
|
||
**移植要点**:
|
||
- 通用模式,适用于 Vulkan、FFmpeg 硬件加速、内核 IOCTL 等涉及自引用 C 结构的场景
|
||
- `'static` 不是真正静态生命周期,而是向 C API 表达"指针在使用期间有效";确保持有者比 C API 使用时间更长
|
||
- 优于 `ouroboros` crate:手写 `Pin<Box<>>` 逻辑清晰可控,生成的代码可调试
|
||
|
||
### 7.5 独立线程管道 + 原子标志(音频线程)
|
||
**问题**:音频需要持续低延迟处理,视频帧率不固定且受 VRR 影响,如何设计无锁跨线程协作?
|
||
**方案**:音频处理完全隔离在独立线程。`mpsc::channel` 传递已编码 `Packet`,主线程在视频帧处理间隙通过 `try_recv()` 非阻塞收取。`Arc<AtomicBool>` + `SeqCst` 实现两个控制信号:`started`(视频首帧时间戳获得后置 true,音视频起点对齐)、`flush_flag`(退出通知)。音频线程主循环为 pull 模型,生命周期由输入设备驱动。
|
||
**移植要点**:
|
||
- `AtomicBool` 适用于简单布尔信号,比 `Mutex<bool>` 高效且不死锁;不适用于需要等待/通知的场景
|
||
- 主循环间隙调用 `try_recv` 是经典的"顺便收取"模式,避免为音频注册额外事件源
|
||
- 适用任何生产者-消费者跨线程场景:传感器采集、网络 I/O 卸载、日志异步写入
|
||
|
||
### 7.6 VRR 感知帧率控制(FpsLimit)
|
||
**问题**:VRR 显示器上帧时间戳极不规则,简单"距上帧太近就丢弃"会产生错误决策,如何在不确定的时间戳流中做出正确帧选择?
|
||
**方案**:`FpsLimit<T>` 引入一帧延迟:第一帧直接通过,第二帧存入 `on_deck` 缓冲,从第三帧起用新帧时间戳判断旧帧是否保留。新帧太近则丢弃缓冲帧,间隔足够则输出缓冲帧。目标时间计算中使用 `max` 防止回退,正确处理帧跳跃后恢复。零项目内依赖,可直接复制使用。
|
||
**移植要点**:
|
||
- 泛型 `T` 无约束,只做保留/丢弃决策,调用者完全控制帧生命周期
|
||
- 结束时必须调用 `flush()` 取出缓冲中的最后一帧,否则丢帧
|
||
- 一帧延迟对录屏/编码场景可接受,实时交互场景(如游戏输入)需评估
|
||
|
||
### 7.7 泛型 Dispatch 三层分发(Wayland 协议)
|
||
**问题**:多后端 Wayland 客户端如何组织 Dispatch 实现,使共享协议代码只写一次、后端专用代码各自独立?
|
||
**方案**:三层分发模式。**A. 完全泛型**:`impl<S: CaptureSource> Dispatch<ZwpLinuxDmabufV1, ()> for State<S>`,共享协议,通常空实现。**B. 带状态回调泛型**:`impl<S: CaptureSource + 'static> Dispatch<WlOutput, ()> for State<S>`,需 `'static` 约束,含实质状态更新逻辑。**C. 后端专用**:`impl Dispatch<ZwlrScreencopyFrameV1, ()> for State<CapWlrScreencopy>`,非泛型,在各自后端文件中。Rust trait 系统根据代理类型 × 状态泛型参数 × UserData 自动路由。
|
||
**移植要点**:
|
||
- 适用于所有 wayland-client 项目;共享 Dispatch 放 main.rs,专用 Dispatch 放各自后端文件
|
||
- 需关联信息时(如 xdg-output 关联 WlOutput)用 `TypedObjectId<T>` 作为 UserData
|
||
- `'static` 约束源自事件循环要求,状态类型必须满足因为回调可能在任意时刻触发
|
||
|
||
### 7.8 三阶段安全构造(IncompleteAudioState)
|
||
**问题**:对象需分多阶段初始化且后续阶段依赖前阶段资源,如何在类型系统中安全表达?
|
||
**方案**:三个不同类型表示三个阶段。`IncompleteAudioState` 持有输入设备、解码器、编码器(完成 FFmpeg 流创建)。`finish(self)` 消费不完整状态,创建过滤器、FIFO、通道,组装 `AudioState` 并启动线程,返回 `AudioHandle`(主线程句柄,含 `Receiver<Packet>` + `AtomicBool`)。`AudioState` 通过 move 语义进入线程。`finish(self)` 而非 `finish(&mut self)` 保证不完整状态被消费后不再存在。
|
||
**移植要点**:
|
||
- typestate pattern 变体,用不同类型(非泛型参数)编码状态,优势是不需要泛型
|
||
- 每阶段恰好分配该阶段所需资源;第一阶段打开设备(可能失败)不浪费线程资源
|
||
- 适用 FFmpeg 管线构建、数据库连接池、GPU 资源分配等"先收集信息、再一次性创建"的场景
|
||
|
||
### 7.9 显示器热插拔自动恢复(OutputWentAway)
|
||
**问题**:长时间录屏中显示器断连/重连,如何保持编码上下文不丢失并自动恢复录制?
|
||
**方案**:`OutputWentAway` 状态机变体实现完整断连恢复。`wl_registry` 的 `GlobalRemove` 设置 `output_went_away` 标志(延迟到 `on_copy_fail` 时再切换,避免事件处理中途转状态)。转换时通过 `Intermediate` 取出 `enc`(保留编码器),丢弃 `cap`(协议对象已失效),记录等待的输出名称并重新探测。重连时按名称匹配创建新 `CaptureSource`,复用旧编码器继续录制。
|
||
**移植要点**:
|
||
- 核心策略:保留昂贵资源(编码器、文件句柄)、丢弃可重建资源(协议对象、设备句柄)
|
||
- 名称匹配(如 "DP-1")而非序号或指针,因为重连后 Wayland 对象 ID 会变化;稳定标识符是热插拔场景关键
|
||
- 适用 USB 摄像头、音频设备、网络连接等可热插拔设备的应用
|
||
|
||
### 7.10 零拷贝 GPU 管道(DMA-BUF → HW Frame → Filter → Encoder)
|
||
**问题**:传统录屏将 GPU 帧下载到 CPU 再上传回 GPU 编码,如何实现全程不离开 GPU 内存的零拷贝管线?
|
||
**方案**:GPU 帧池分配硬件表面,`av_hwframe_map` 映射为 DRM PRIME 描述符获取 DMA-BUF fd,注册为 `WlBuffer` 后合成器直接写入 GPU 表面。滤镜图 `buffersrc → crop → scale → [transpose] → buffersink` 全部在 GPU 执行,`hw_frames_ctx` 绑定确保 FFmpeg 识别 GPU 帧。编码器(VAAPI/Vulkan)直接消费 GPU 帧。`EncodePixelFormat` 三路枚举在编码器选择、滤镜构建、帧上下文创建处统一派发,仅 `Sw` 路径添加 `hwdownload`。
|
||
**移植要点**:
|
||
- DMA-BUF 桥接是 Linux 特有的;Windows/macOS 需用 D3D11 共享句柄或 IOSurface
|
||
- 捕获帧上下文用 `Drm(modifiers)` 匹配合成器,编码帧上下文用 `Optimal` 获最佳性能,滤镜图做格式转换
|
||
- 零拷贝路径失败应降级到 CPU 路径或重试,而非直接崩溃
|
||
|
||
### 模式总结与关联
|
||
|
||
| # | 模式 | 核心机制 | 复杂度 |
|
||
|--|------|---------|--------|
|
||
| 1 | 策略 Trait + 泛型状态 | `trait + State<S>` 单态化 | 中 |
|
||
| 2 | 多态枚举状态机 | `enum + mem::replace + Intermediate` | 中高 |
|
||
| 3 | 类型安全帧生命周期 | 4 状态 enum + assert 守护 | 低中 |
|
||
| 4 | Pin\<Box\> 自引用结构 | `PhantomPinned + Box::pin + unsafe` | 高 |
|
||
| 5 | 独立线程管道 + 原子标志 | `mpsc::channel + AtomicBool` | 低 |
|
||
| 6 | VRR 感知帧率控制 | 一帧缓冲延迟决策 | 低 |
|
||
| 7 | 泛型 Dispatch 三层分发 | `impl<S: Trait> Dispatch for State<S>` | 中 |
|
||
| 8 | 三阶段安全构造 | 不同类型 × 消费 self | 低中 |
|
||
| 9 | 显示器热插拔恢复 | 标志延迟 + 资源分类 + 名称匹配 | 中 |
|
||
| 10 | 零拷贝 GPU 管道 | DMA-BUF + HW Frame + GPU Filter | 高 |
|
||
|
||
模式围绕"GPU 加速屏幕录制"协同工作:模式 1(策略 Trait)是架构骨架,模式 2(状态机)是运行时驱动核心,模式 10(零拷贝管道)是性能关键路径。模式 1 被模式 2/3/7/9 使用,模式 5(音频线程)使用模式 8(三阶段构造),模式 10 使用模式 4(Pin\<Box\>)并被模式 6(帧率控制)调节。
|