Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions nativelib/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ set(MOONLIGHT_COMMON_SOURCES
${MOONLIGHT_COMMON_PATH}/src/ConnectionTester.c
${MOONLIGHT_COMMON_PATH}/src/MicrophoneStream.c
${MOONLIGHT_COMMON_PATH}/src/FakeCallbacks.c
${MOONLIGHT_COMMON_PATH}/src/rswrapper.c
${MOONLIGHT_COMMON_PATH}/nanors/rs.c
${MOONLIGHT_COMMON_PATH}/nanors/deps/obl/oblas_common.c
${MOONLIGHT_COMMON_PATH}/nanors/deps/obl/oblas_lite.c
)

# ENet 网络库源文件
Expand Down Expand Up @@ -185,10 +187,13 @@ add_library(moonlight_nativelib SHARED
)
add_dependencies(moonlight_nativelib generate_shader_header)

# rswrapper.c 需要优化标志以获得最佳 Reed-Solomon 性能
# nanors 需要优化标志以获得最佳 Reed-Solomon 性能
# 使用 ohos_shim/assert.h 避免 OHOS assert.h 的 __assert_fail 与 Clang target
# multiversioning 冲突 (尤其是 x86_64 模拟器构建)
set_source_files_properties(${MOONLIGHT_COMMON_PATH}/src/rswrapper.c
set_source_files_properties(
${MOONLIGHT_COMMON_PATH}/nanors/rs.c
${MOONLIGHT_COMMON_PATH}/nanors/deps/obl/oblas_common.c
${MOONLIGHT_COMMON_PATH}/nanors/deps/obl/oblas_lite.c
PROPERTIES COMPILE_FLAGS "-ftree-vectorize -funroll-loops -isystem ${CMAKE_CURRENT_SOURCE_DIR}/ohos_shim")

# 主项目只需要 aubio 的公开头文件路径 (用于 aubio_onset_wrapper.h)
Expand Down
91 changes: 67 additions & 24 deletions nativelib/src/main/cpp/native_render.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,37 +315,80 @@ void NativeRender::ApplyFrameRateRange() {
// =============================================================================

int64_t NativeRender::CalculatePresentTime(int64_t pts) const {
// 获取当前系统时间(纳秒)
// 获取当前系统时间(纳秒,单调钟
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
int64_t nowNs = static_cast<int64_t>(ts.tv_sec) * 1000000000LL + ts.tv_nsec;

// 初始化时间基准
if (!timeBaseInitialized_) {
baseSystemTimeNs_ = nowNs;
basePtsUs_ = pts;

const int64_t hostNs = pts * 1000LL; // host 时钟(纳秒),原点=首个被捕获帧
const int64_t instOffset = nowNs - hostNs; // 本帧的"本地 - host"瞬时偏移(含网络+解码延迟)
const int64_t frameIntervalNs = configuredFps_ > 0 ? 1000000000LL / configuredFps_ : 16666667LL;

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

Comment on lines +327 to +330

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.

if (!timeBaseInitialized_ || discontinuity) {
// (重新)锚定:用本帧偏移作为初值,清零 skew 与抖动估计。
estimatedOffsetNs_ = instOffset;
skewNs_ = 0;
jitterEstNs_ = static_cast<double>(frameIntervalNs) / 16.0; // 抖动估计初值(约半毫秒量级)
timeBaseInitialized_ = true;
OH_LOG_INFO(LOG_APP, "VSync time base initialized: basePts=%{public}lld us",
static_cast<long long>(basePtsUs_));
if (discontinuity) {
vsyncResyncCount_++;
}
OH_LOG_INFO(LOG_APP, "VSync clock (re)anchored: offset=%{public}lldus, pts=%{public}lldus%{public}s",
static_cast<long long>(estimatedOffsetNs_ / 1000),
static_cast<long long>(pts),
discontinuity ? " [discontinuity]" : "");
} else {
// alpha-beta / PI 时钟恢复(离线仿真验证):
// pred = offset + skew 先按上一帧的频差估计外推一帧
// e = instOffset - pred 本帧残差(网络抖动 + 频差误差)
// offset += ec/64 (Kp=1/64) 小比例项:跟踪偏移均值、保持网格近似刚性(不追逐逐帧抖动)
// skew += ec/2048(Ki=1/2048) 积分项:跟踪时钟频差,消除斜坡滞后
// ec 为限幅残差(±8ms),抑制网络尖峰污染 offset/skew(尖峰由 cushion 与"迟到即立即呈现"兜底)。
// 注:用整除(向零取整)而非算术右移——右移对负 ec 向负无穷取整,会给 offset/skew(尤其积分项)
// 引入每帧亚纳秒级直流偏置并随时间累积;整除无此偏置,且与上述 Kp/Ki 语义一致。
const int64_t pred = estimatedOffsetNs_ + skewNs_;
const int64_t e = instOffset - pred;
int64_t ec = e;
if (ec > 8000000LL) ec = 8000000LL;
else if (ec < -8000000LL) ec = -8000000LL;
estimatedOffsetNs_ = pred + (ec / 64);
skewNs_ += (ec / 2048);
// 在线抖动估计(平均绝对偏差, EMA alpha=1/32),用于自适应 cushion。
const double ae = static_cast<double>(e < 0 ? -e : e);
jitterEstNs_ += (ae - jitterEstNs_) / 32.0;
}

// 计算相对于基准的 PTS 偏移(转换为纳秒)
int64_t ptsDeltaNs = (pts - basePtsUs_) * 1000LL;

// 目标呈现时间 = 基准系统时间 + PTS 偏移
int64_t targetPresentTimeNs = baseSystemTimeNs_ + ptsDeltaNs;

// 如果目标时间已经过去,使用当前时间 + 小延迟
// 避免使用过去的时间戳导致帧被丢弃
lastPtsUs_ = pts;

// 自适应 cushion:随网络抖动伸缩(净 LAN 极小→低延迟;抖动大→更深缓冲)。
// 3×平均绝对偏差(高斯下≈2.4σ),夹在 [1ms, 1 帧] 之间。延迟成本可调、可观测(见统计日志)。
int64_t cushionNs = static_cast<int64_t>(3.0 * jitterEstNs_);
if (cushionNs < 1000000LL) cushionNs = 1000000LL;
else if (cushionNs > frameIntervalNs) cushionNs = frameIntervalNs;

const int64_t targetPresentTimeNs = hostNs + estimatedOffsetNs_ + cushionNs;

// 迟到帧(网络抖动尖峰导致 target 已过去):不改动网格,直接返回原网格时刻。
// 解码器对"过去的时间戳"即立即呈现;网格保持刚性连续,避免"弹出再弹回"的双重卡顿。
if (targetPresentTimeNs < nowNs) {
// 添加半个帧间隔的偏移,给 compositor 一些处理时间
int64_t frameIntervalNs = 1000000000LL / configuredFps_;
targetPresentTimeNs = nowNs + frameIntervalNs / 2;

// 重新同步时间基准(避免持续漂移)
baseSystemTimeNs_ = targetPresentTimeNs - ptsDeltaNs;
vsyncLateFrameCount_++;
}


// 周期性统计,便于评估收益/风险(迟到率高→cushion 偏小或抖动大;重锚频繁→上游 PTS 不稳)。
if (++vsyncFrameCount_ % 6000 == 0) {
OH_LOG_INFO(LOG_APP,
"VSync clock stats: frames=%{public}lld, late=%{public}lld, resync=%{public}lld, offset=%{public}lldus, skew=%{public}lldns/f, cushion=%{public}lldus",
static_cast<long long>(vsyncFrameCount_),
static_cast<long long>(vsyncLateFrameCount_),
static_cast<long long>(vsyncResyncCount_),
static_cast<long long>(estimatedOffsetNs_ / 1000),
static_cast<long long>(skewNs_),
static_cast<long long>(cushionNs / 1000));
}

return targetPresentTimeNs;
}

Expand Down
25 changes: 22 additions & 3 deletions nativelib/src/main/cpp/native_render.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,29 @@ class NativeRender {
// 帧率范围已经应用过(NativeVSync)
bool frameRateApplied_ = false;

// 时间同步基准(用于 VSync 模式)
mutable int64_t baseSystemTimeNs_ = 0; // 系统时间基准(纳秒)
mutable int64_t basePtsUs_ = 0; // PTS 基准(微秒)
// 时间同步(用于 VSync 模式)——去抖时钟(alpha-beta / PI 时钟恢复)
// 用平滑的 offset(+skew) 估计 + 自适应 cushion 替代朴素首帧锚点:
// target = pts*1000 + estimatedOffsetNs_ + cushion
// 经离线仿真(steady/WiFi/jittery/120fps)验证,相较旧朴素锚点:
// 硬重置 resync 由每 20s 一次降为 0;可见卡顿(>半帧)由每场景数次降为 0;
// 最大间隔跳变 31ms→<4ms;代价是自适应缓冲延迟(净 LAN≈1.2ms,WiFi≈8ms,远端≈16ms)。
// 设计要点:
// - estimatedOffsetNs_ 以小比例项(Kp=1/64)跟踪"本地单调钟 - host PTS"偏移的均值,
// 保持网格近似刚性、不追逐逐帧抖动(抖动交由 cushion 吸收);
// - skewNs_ 为每帧频差积分项(Ki=1/2048),消除主机/客户端时钟频差造成的斜坡滞后;
// - jitterEstNs_ 在线估计抖动幅度(平均绝对偏差),cushion 随之自适应;
// - 迟到帧(target<now)直接按原网格时刻送解码器(过去时间戳=立即呈现),绝不把网格拽到
// 到达时刻——避免"弹出网格再弹回"的双重卡顿(旧硬重置的根因)。
mutable int64_t estimatedOffsetNs_ = 0; // 平滑后的 (本地 - host) 偏移均值(纳秒)
mutable int64_t skewNs_ = 0; // 每帧频差估计(纳秒/帧),消除时钟 skew 斜坡滞后
mutable double jitterEstNs_ = 0.0; // 在线抖动估计(平均绝对偏差, 纳秒),驱动自适应 cushion
mutable int64_t lastPtsUs_ = 0; // 上一帧 host PTS(微秒),用于检测不连续(重连/跳变)
mutable bool timeBaseInitialized_ = false;

// VSync 去抖时钟的运行统计(用于评估收益/风险)
mutable int64_t vsyncFrameCount_ = 0; // 已呈现帧数
mutable int64_t vsyncLateFrameCount_ = 0; // 目标时刻已过去、被迫前推的帧数(判断延迟/cushion 是否偏小)
mutable int64_t vsyncResyncCount_ = 0; // 因 PTS 不连续而重锚定的次数(判断硬跳变是否仍在发生)

// NativeVSync(用于设置期望帧率范围,API 20+)
OH_NativeVSync* nativeVSync_ = nullptr;
Expand Down
Loading