Skip to content

feat(render): VSync 呈现时钟升级为去抖 PI 滤波,消除周期性 hitch#56

Merged
qiin2333 merged 6 commits into
masterfrom
feat/vrr-clock-filter
Jul 6, 2026
Merged

feat(render): VSync 呈现时钟升级为去抖 PI 滤波,消除周期性 hitch#56
qiin2333 merged 6 commits into
masterfrom
feat/vrr-clock-filter

Conversation

@qiin2333

@qiin2333 qiin2333 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

背景

主机(Sunshine)把捕获时刻(QPC 精度)写进 RTP 90kHz 时间戳,common-c 在 RtpVideoQueue.c 映射为 presentationTimeUs(以首帧为原点的呈现时间)。鸿蒙端 native_render 一直用它做 host→本地时钟的呈现映射,但旧滤波器是"首帧锚点 + 漂移时硬重置",会周期性产生可见 hitch。

改动

CalculatePresentTime 升级为 alpha-beta(PI)时钟恢复 + 自适应 cushion

  • offset 比例项 (Kp=1/64):跟踪 (本地单调钟 − host PTS) 偏移均值,保持呈现网格近似刚性,不追逐逐帧网络抖动
  • skew 积分项 (Ki=1/2048):跟踪主机/客户端时钟频差,消除斜坡滞后(旧算法漂移到边界就硬重置)
  • 自适应 cushion:在线抖动估计(平均绝对偏差)驱动,净 LAN 极小延迟、抖动网络自动加深缓冲
  • 迟到帧不拽网格:网络尖峰导致目标已过去时,按原网格时刻直接送解码器(过去时间戳=立即呈现),网格保持连续 —— 这是消除旧硬重置周期性 hitch 的关键

受影响的帧节奏模式

模式 是否受影响 说明
VSync 模式config.enableVsync = true ✅ 是 RenderOutputBufferAtTime,本 PR 改的就是它的呈现时刻计算
低延迟模式enableVsync = false ❌ 否 OH_VideoDecoder_RenderOutputBuffer 直渲染,完全绕过 CalculatePresentTime

验证

因 OHOS NDK 全量构建需 DevEco 环境,采用离线仿真(g++,抽取函数原文逐字编译,-Wall -Wextra 零警告)对比新旧算法,4 场景(净 LAN / 典型 WiFi / 抖动远端 / 120fps):

指标(典型 WiFi,120s@60fps,150ppm skew,3ms 抖动+1% 尖峰) 旧(硬重置) 新(PI)
硬重置次数 4(约每 20s 一次) 0
可见卡顿(间隔偏差 > 半帧) 4 0
最大间隔跳变 31 ms 3.8 ms
额外延迟 0 +8 ms(自适应,净 LAN 仅 +1.2ms)

注:常数(Kp/Ki/cushion 系数)建议上真机后按体感微调。

风险

  • 仅 VSync 模式受影响,低延迟/LAN 竞速路径零变化
  • cushion 带来自适应延迟(净 LAN ≈1ms,远端 ≈16ms),可调
  • 无接口/ABI 变化,不改 common-c 子模块

Summary by CodeRabbit

  • Bug Fixes
    • 优化 VSync 时间同步:基于单调时钟进行偏移/频差(offset/skew)与抖动(jitter)在线估计,提升呈现稳定性与同步精度。
    • 针对 PTS 不连续、回退等异常自动重新锚定,并重置抖动累计,避免累计误差引发的漂移。
    • 对迟到帧采用更稳健的策略(不再强制改写目标时间),减少卡顿与不同步;并新增周期性同步状态统计日志。

将 CalculatePresentTime 从"朴素首帧锚点 + 漂移硬重置"改为
alpha-beta(PI)时钟恢复 + 自适应 cushion + 迟到帧不拽网格:
- offset 小比例项(Kp=1/64)跟踪偏移均值,保持呈现网格近似刚性
- skew 积分项(Ki=1/2048)消除主机/客户端时钟频差的斜坡滞后
- 在线抖动估计驱动自适应 cushion(净 LAN 低延迟、抖动网络更深缓冲)
- 迟到帧按原网格时刻立即呈现,网格保持连续,消除旧硬重置的周期性 hitch

仅影响 VSync 模式(config.enableVsync=true);低延迟模式走
OH_VideoDecoder_RenderOutputBuffer 直渲染,完全不受影响。

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@qiin2333, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0e9fb2ca-4622-4a86-852c-187892f0475e

📥 Commits

Reviewing files that changed from the base of the PR and between 609be07 and 8faf8fd.

📒 Files selected for processing (1)
  • nativelib/src/main/cpp/CMakeLists.txt
📝 Walkthrough

Walkthrough

本次修改重构了 NativeRender 的 VSync 呈现时间计算逻辑,改为基于单调时钟的偏移、频差和抖动在线估计,并同步更新了相关状态字段;另外还更新了一个子模块指针。

Changes

呈现时间估计模型重构

Layer / File(s) Summary
状态字段定义与核心计算逻辑
nativelib/src/main/cpp/native_render.h, nativelib/src/main/cpp/native_render.cpp
移除旧的 baseSystemTimeNs_/basePtsUs_ 锚点字段,新增 estimatedOffsetNs_skewNs_jitterEstNs_lastPtsUs_ 等状态字段及统计计数器;CalculatePresentTime 改为检测 PTS 不连续并重锚,连续情况下用残差限幅更新偏移与频差、EMA 估计抖动,计算自适应 cushion 得出目标呈现时间,迟到帧仅计数不再重写目标时间,并周期性输出统计日志。

子模块指针更新

Layer / File(s) Summary
子模块提交更新
nativelib/src/main/cpp/moonlight-common-c
更新 moonlight-common-c 子项目的提交指针到新的哈希。

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了 VSync 呈现时钟改为去抖 PI/滤波恢复并减少周期性卡顿的核心改动。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vrr-clock-filter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nativelib/src/main/cpp/native_render.cpp`:
- Around line 327-330: The discontinuity check in native_render.cpp only treats
forward PTS jumps greater than 2 seconds as a reset, so smaller reconnect/seek
jumps still flow through the PI path and can skew targetPresentTimeNs. Update
the discontinuity logic in the render timing path around the PTS comparison in
native_render.cpp so that smaller forward jumps within the reconnect/seek range
also trigger a re-anchor based on frame interval or continuity threshold, while
still keeping 2 seconds as the maximum upper bound. Use the existing timing
state such as timeBaseInitialized_ and lastPtsUs_ to detect and reset continuity
before the PI limiter runs.
- Around line 353-357: The PI gain updates in native_render.cpp use bit shifts
on ec, which can bias negative values compared with the intended Kp/Ki behavior.
Update the estimatedOffsetNs_ and skewNs_ calculations in the native render
logic to use division by 64 and 2048 instead of right shifts, keeping the
existing ec clamp and the surrounding PI controller flow intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 84afb973-773b-486f-8517-f1a6e0daae1c

📥 Commits

Reviewing files that changed from the base of the PR and between 364a4dc and 27b3939.

📒 Files selected for processing (2)
  • nativelib/src/main/cpp/native_render.cpp
  • nativelib/src/main/cpp/native_render.h

Comment on lines +327 to +330
// 检测 PTS 不连续(重连 / seek / 编码器重启):PTS 回退,或跳变 > 2s。
const bool discontinuity = timeBaseInitialized_ &&
(pts < lastPtsUs_ || (pts - lastPtsUs_) > 2000000LL);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

补上 2 秒以内的 PTS 正向跳变重锚。

Line 329 只把 >2s 当作不连续;如果 reconnect/seek 让 PTS 正向跳 500ms~2s,会进入 PI 分支并被 ±8ms 限幅,targetPresentTimeNs 仍会偏到未来,可能造成 VSync 冻结。建议按帧间隔设置更近的连续性阈值,保留 2s 作为上限。

🐛 建议修正
-    // 检测 PTS 不连续(重连 / seek / 编码器重启):PTS 回退,或跳变 > 2s。
-    const bool discontinuity = timeBaseInitialized_ &&
-        (pts < lastPtsUs_ || (pts - lastPtsUs_) > 2000000LL);
+    // 检测 PTS 不连续(重连 / seek / 编码器重启):PTS 回退,或明显超过正常帧间隔。
+    const int64_t ptsDeltaUs = pts - lastPtsUs_;
+    int64_t discontinuityThresholdUs = (frameIntervalNs / 1000LL) * 8;
+    if (discontinuityThresholdUs < 250000LL) discontinuityThresholdUs = 250000LL;
+    else if (discontinuityThresholdUs > 2000000LL) discontinuityThresholdUs = 2000000LL;
+    const bool discontinuity = timeBaseInitialized_ &&
+        (ptsDeltaUs < 0 || ptsDeltaUs > discontinuityThresholdUs);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 检测 PTS 不连续(重连 / seek / 编码器重启):PTS 回退,或跳变 > 2s。
const bool discontinuity = timeBaseInitialized_ &&
(pts < lastPtsUs_ || (pts - lastPtsUs_) > 2000000LL);
// 检测 PTS 不连续(重连 / seek / 编码器重启):PTS 回退,或明显超过正常帧间隔。
const int64_t ptsDeltaUs = pts - lastPtsUs_;
int64_t discontinuityThresholdUs = (frameIntervalNs / 1000LL) * 8;
if (discontinuityThresholdUs < 250000LL) discontinuityThresholdUs = 250000LL;
else if (discontinuityThresholdUs > 2000000LL) discontinuityThresholdUs = 2000000LL;
const bool discontinuity = timeBaseInitialized_ &&
(ptsDeltaUs < 0 || ptsDeltaUs > discontinuityThresholdUs);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nativelib/src/main/cpp/native_render.cpp` around lines 327 - 330, The
discontinuity check in native_render.cpp only treats forward PTS jumps greater
than 2 seconds as a reset, so smaller reconnect/seek jumps still flow through
the PI path and can skew targetPresentTimeNs. Update the discontinuity logic in
the render timing path around the PTS comparison in native_render.cpp so that
smaller forward jumps within the reconnect/seek range also trigger a re-anchor
based on frame interval or continuity threshold, while still keeping 2 seconds
as the maximum upper bound. Use the existing timing state such as
timeBaseInitialized_ and lastPtsUs_ to detect and reset continuity before the PI
limiter runs.

Comment thread nativelib/src/main/cpp/native_render.cpp Outdated
qiin2333 and others added 5 commits July 6, 2026 15:42
ec>>6 / ec>>11 为算术右移(向负无穷取整),对负 ec 与预期的 ec/64、
ec/2048(向零取整)差 1 单位。稳态残差常处亚-2048ns 区间,右移每帧
多减 1 并累积进 skew 积分项——离线仿真显示会把 skew 估计带成错误负号。
改为整除后无此直流偏置,且与注释中 Kp=1/64、Ki=1/2048 语义一致。

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@qiin2333 qiin2333 merged commit 7232858 into master Jul 6, 2026
2 checks passed
@qiin2333 qiin2333 deleted the feat/vrr-clock-filter branch July 6, 2026 10:10
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