Files
wl-webrtc/analysis.md
dailz 6d49222de8 feat: Phase 1 MVP with audit fixes — Wayland screen capture + VAAPI encoding
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
2026-04-05 23:35:00 +08:00

444 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 modifierVulkan 模式接受任意 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` 路径:捕获仍用 GPUDMA-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 使用模式 4Pin\<Box\>)并被模式 6帧率控制调节。