[filler] 停滞期间 filler 帧循环两端浪费:3245 filler vs 3209 dedup #18

Closed
opened 2026-06-13 23:03:54 +08:00 by dailz · 1 comment
Owner

现象

wl-webrtc.log 全会话统计:

指标 计数
总帧入编码 5429
filler_frames_sent(末值) 3245(占 60%)
skipping duplicate frame 3209

即:停滞期间,portal 端把上一帧的 NV12 数据复制塞进 encode 通道 → 编码端算 Y 平面 hash → 发现完全相同 → 丢弃。两端都在做无用功

流程图

[state_portal.rs maybe_send_filler_frame]
  clone() y_data/uv_data  ←── 复制上一帧的 NV12 缓冲
  try_send(input_tx)      ←── 跨线程通道
        ↓
[avhw.rs encode_cpu_frame]
  hash_sampled_y_plane()  ←── 算 hash
  current_hash == last_frame_hash? → yes → "skipping duplicate frame" → return Ok

根因

src/state_portal.rs:379-427 的 filler 策略假设"重复帧编码后客户端看到画面继续动",但没考虑:

  1. 编码端有 dedup(src/avhw.rs:1131-1137),相同 hash 的帧根本不会编码产出
  2. 即使产出,画面也仍然是上一帧——对端看到的是冻结画面,filler 没有带来任何视觉信息
  3. 唯一作用是"维持 encoded_fps 数字好看"——这反而掩盖了真实质量(见 #21)

影响

  • portal 线程:每次 filler clone NV12 数据(1920×1080 NV12 ≈ 3.1MB)+ 跨线程 send
  • 编码线程:每次 hash 计算(采样 Y 平面)+ 比较
  • 跨线程 channel 带宽占用
  • 在 60% 停滞率下,这是稳定的 ~30fps 浪费循环

修复方向

方案 A(推荐):filler 入队前自己比较 hash,相同就不发

fn maybe_send_filler_frame(&mut self) {
    // ... existing checks ...
    let current_hash = hash_sampled_y_plane(&cached.y_data, ...);
    if current_hash == self.last_filler_hash { return; }  // 跳过
    self.last_filler_hash = current_hash;
    // ... send ...
}

方案 B:删除整个 filler 机制。让合成器主导帧率(KWin 在画面变化时自然会送帧),WebRTC 层依赖 PLI/FIR 触发真实关键帧(依赖 #16 修复)。

方案 C:filler 退化为 RTP 层的 padding/keep-alive(如果担心 NAT/UDP 超时),不要走完整编码路径。

关联

  • #15 给了 filler 触发条件(compositor stall)
  • #16 修复后可以走方案 B(keyframe 请求 < 1 帧响应,filler 没必要)
  • 修复 #17 后才能定量验证 filler 是否真的节省了"发送"带宽(目前看 output_bps=0 无法判断)
## 现象 `wl-webrtc.log` 全会话统计: | 指标 | 计数 | |---|---| | 总帧入编码 | 5429 | | `filler_frames_sent`(末值) | **3245**(占 60%) | | `skipping duplicate frame` | **3209** | 即:停滞期间,portal 端把上一帧的 NV12 数据**复制**塞进 encode 通道 → 编码端算 Y 平面 hash → 发现完全相同 → 丢弃。**两端都在做无用功**。 ## 流程图 ``` [state_portal.rs maybe_send_filler_frame] clone() y_data/uv_data ←── 复制上一帧的 NV12 缓冲 try_send(input_tx) ←── 跨线程通道 ↓ [avhw.rs encode_cpu_frame] hash_sampled_y_plane() ←── 算 hash current_hash == last_frame_hash? → yes → "skipping duplicate frame" → return Ok ``` ## 根因 `src/state_portal.rs:379-427` 的 filler 策略假设"重复帧编码后客户端看到画面继续动",但没考虑: 1. 编码端有 dedup(`src/avhw.rs:1131-1137`),相同 hash 的帧**根本不会编码产出** 2. 即使产出,画面也仍然是上一帧——对端看到的是冻结画面,filler 没有带来任何视觉信息 3. 唯一作用是"维持 `encoded_fps` 数字好看"——这反而掩盖了真实质量(见 #21) ## 影响 - portal 线程:每次 filler clone NV12 数据(1920×1080 NV12 ≈ 3.1MB)+ 跨线程 send - 编码线程:每次 hash 计算(采样 Y 平面)+ 比较 - 跨线程 channel 带宽占用 - 在 60% 停滞率下,这是稳定的 ~30fps 浪费循环 ## 修复方向 **方案 A(推荐)**:filler 入队前自己比较 hash,相同就不发 ```rust fn maybe_send_filler_frame(&mut self) { // ... existing checks ... let current_hash = hash_sampled_y_plane(&cached.y_data, ...); if current_hash == self.last_filler_hash { return; } // 跳过 self.last_filler_hash = current_hash; // ... send ... } ``` **方案 B**:删除整个 filler 机制。让合成器主导帧率(KWin 在画面变化时自然会送帧),WebRTC 层依赖 PLI/FIR 触发真实关键帧(依赖 #16 修复)。 **方案 C**:filler 退化为 RTP 层的 padding/keep-alive(如果担心 NAT/UDP 超时),不要走完整编码路径。 ## 关联 - #15 给了 filler 触发条件(compositor stall) - #16 修复后可以走方案 B(keyframe 请求 < 1 帧响应,filler 没必要) - 修复 #17 后才能定量验证 filler 是否真的节省了"发送"带宽(目前看 `output_bps=0` 无法判断)
dailz added the priority/higharea/pipelinetype/bug labels 2026-06-13 23:03:54 +08:00
Author
Owner

Dependencies

Blocked by: #17 — 需要 stats 量化 filler 浪费程度(3245 filler vs 3209 dedup)
Blocks: #15 — stall 缓解依赖本 issue 的 filler 策略改进

## Dependencies **Blocked by:** #17 — 需要 stats 量化 filler 浪费程度(3245 filler vs 3209 dedup) **Blocks:** #15 — stall 缓解依赖本 issue 的 filler 策略改进
dailz closed this issue 2026-06-20 20:56:49 +08:00
Sign in to join this conversation.