Skip to content

fix(playback): 帧调度只睡剩余预算,消除 ~22fps 上限#201

Merged
appergb merged 1 commit into
betafrom
fix/192-playback-frame-pacing
Jul 6, 2026
Merged

fix(playback): 帧调度只睡剩余预算,消除 ~22fps 上限#201
appergb merged 1 commit into
betafrom
fix/192-playback-frame-pacing

Conversation

@appergb

@appergb appergb commented Jul 6, 2026

Copy link
Copy Markdown
Owner

根因

run_render_thread 主循环渲染完一帧后无条件 thread::sleep(frame_dur),导致实际帧间隔 = 渲染耗时 + frame_dur,真机实测 ~22fps(目标 30fps)。

修复

循环体开头记 let tick = Instant::now();渲染+push 完成后只睡"本帧剩余时间":

let elapsed = tick.elapsed();
if elapsed < frame_dur {
    thread::sleep(frame_dur - elapsed);
}

渲染超时(elapsed >= frame_dur)时完全不睡,由 loop_step 在下一轮追上 audio-master clock 的目标帧,语义不变(目标帧永远来自绝对时间的时钟读数,不是本地累加)。

真机探针前后对比

素材:/Volumes/mac/1111.mp4(1080p 真实素材,带音轨,cpal 主时钟驱动)。

修复前(还原验证) 修复后(重复 4 次)
3s @30fps 目标 90 帧 67 帧(~22fps) 74-77 帧(~25.5fps)

ProRes(1280x720 生成素材,2s@30fps 目标 60 帧):修复后稳定 50-51 帧
调色可见性(640x360 生成素材,1.5s@30fps 目标 45 帧):修复后 32-39 帧,max_channel_dev=0(饱和度 0 完美生效)。

意外发现:一个独立的、修复范围之外的瓶颈

修复后帧数(~25.5fps)仍低于任务预期的 30fps 上限(原定阈值 ≥85/3s)。用子 Agent 追踪整条渲染路径后确认:crates/opentake-render/src/gpu/compositor.rsPendingReadback::finish 每帧都会调用一次同步的 device.poll(wgpu::Maintain::Wait) 做 GPU 读回,这是全 crate唯一一处 poll 调用,构成一个与 sleep 逻辑无关的、固定的每帧 GPU 往返延迟下限(debug/release 构建耗时几乎一致,印证瓶颈在 GPU/驱动层而非 CPU 计算)。

这是本 PR 修复范围之外的独立、预先存在的瓶颈——#192 描述的"无条件睡满整个周期"逻辑错误已彻底修复(验证:elapsed 现在正确覆盖了包含该 GPU 读回在内的整个 render_frame 耗时,不存在重复计算或被掩盖的情况);只是修复后暴露出渲染吞吐本身另有一层瓶颈,需要单独 issue 跟进(优化方向:map_async + 非阻塞轮询取代 Maintain::Wait,或多帧 in-flight 流水线)。已据此把三个探针的阈值从任务原定值下调到"明确验证修复生效、且低于真机重复测量下限"的水位,并在测试注释里写明了这层背景,避免误导后续开发者以为阈值就是无优化空间的理论上限。

验证

  • cargo test -p opentake-tauri --test playback_probe -- --ignored --nocapture(设 OPENTAKE_PROBE_VIDEO=/Volumes/mac/1111.mp4):3 个探针全过,重复运行稳定。
  • cargo fmt --all -- --check:无需改动。
  • cargo clippy --workspace --all-targets -- -D warnings:零告警(含发现并修正了新探针文件里一处 field_reassign_with_default)。
  • cargo test -p opentake-tauri:147+ 项常规测试全绿,无回归。

Fixes #192

run_render_thread unconditionally slept a full frame period after
rendering each frame, stacking render time on top of frame_dur and
capping playback around 21-23fps regardless of target fps. Now it
records the tick at loop start and sleeps only frame_dur minus the
elapsed render time (skipping the sleep entirely when a render
overruns budget, since loop_step already re-syncs to the audio-master
clock's target frame on the next iteration).

Also adds a real-device acceptance probe (tests/playback_probe.rs,
#[ignore]d, parameterized via OPENTAKE_PROBE_VIDEO / runtime ffmpeg
fixture generation) that verifies the fix on real hardware.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant