Skip to content

拖拽 0 帧延迟优化 ​

拖拽跟手度差不是「渲染太慢」,而是两个一帧级的时间差叠加:手势在 Update 里处理导致画面晚一帧动,帧率切换要等下一个 vsync 才生效。两处都抹掉,才有真正的 0 帧延迟。

拖拽延迟1 帧 → 0 帧
延迟量级~16ms @60fps
验证方式高帧率摄像机

背景 ​

车机 SR(小鹏一张图)支持手势拖拽地图。这套渲染的帧率不是一个值,而是三层:Unity 应用帧(只有 30/60 两档)、全局唯一的逻辑帧、每路 Surface 各自的渲染帧。逻辑帧和渲染帧都是在 Unity 主帧里做的定时计算——应用帧 30、逻辑帧 15,就是主帧每跑两帧逻辑帧才跑一次,所以逻辑/渲染帧永远不会高于应用帧。

拖拽跟手度是这套交互的基本盘:手指动、画面必须同帧动。

问题 ​

台架测试时用高帧率摄像机逐帧对比,能清楚拍到:手指先动,地图后动,差约 16ms(@60fps)。拆开来是两个独立的一帧延迟:

  1. 手势处理位置错了。手势在 Update() 里处理,但当帧的渲染此时已经开始,地图位置要等下一帧才被画出来。
  2. 帧率切换生效晚了。拖拽发生时帧率策略会把手势场景拉到高帧率档,但 targetFrameRate 设置后要等下一个 vsync 才真正生效,交互刚开始的那几帧仍按旧帧率跑,第一时间的响应被拖慢。

方案 ​

mermaid
flowchart LR
    A["输入事件到达"] --> B["Update<br>相机等组件完成当帧计算"]
    B --> C["LateUpdate<br>处理手势,更新地图位置"]
    C --> D{"帧率提升?"}
    D -->|"是"| E["forceCameraRender<br>当帧强制渲染一帧"]
    D -->|"否"| F["等待自然帧循环"]
    E --> G["当帧上屏"]
    F --> G

思路很直接:手势处理挪到当帧渲染开始前的最后一个机会(LateUpdate),帧率提升时不再等 vsync,直接强制渲染一帧让它立刻生效。

这套链路里每一次决策变化都有日志可查:每次切换都会打一条「帧率变化」记录(逻辑帧、渲染帧的旧值 → 新值,以及命中的优先级来源),台架验证时可以逐条和摄像机的画面比对。

关键实现 ​

手势处理从 Update 迁到 LateUpdate ​

csharp
// 原来:Update 中处理
void Update() {
    HandleGesture(); // 地图在这一帧不会动
}

// 修复:LateUpdate 中处理
void LateUpdate() {
    HandleGesture(); // 地图在当前帧就会动
}

LD 地图移动统一收到 OnLateUpdate 里对齐手势。之所以不直接在 Input 回调里更新,是因为相机等其他组件需要先在 Update 里完成当帧计算,手势改地图位置必须排在它们之后、渲染之前。

帧率策略侧的配合 ​

拖拽发生时,帧率决策链每帧在 LateUpdate 执行:HUCameraStateNode 采集手势/运镜/迎宾/回车位/档位等输入,交给 FrameRatePolicyResolver 按优先级决策。优先级表(P1/P2 已永久移除):

优先级触发条件帧率档说明
P0迎宾动画中或迎宾混合中Gesture(默认 60/60)最高优先,保证迎宾流畅
P3手势/运镜/回车位 且 长期 P 档Gesture交互时高帧率,仅长期 P
P4SR 1/3 屏(非 LensShift 过渡)HalfCoverage(默认 15/20)小窗降帧省功耗
P5默认空闲Idle(默认 25/25)兜底,省电

几个和延迟直接相关的设计决策:

  • 帧率提升时强制渲染一帧:targetRenderFps > current 时调用 forceCameraRender,避免等下一个 vsync 才生效,减少感知延迟。
  • 只在帧率真正变化时设置:logicFps != last OR cameraFps != current 才写入,避免每帧重复调用 Application.targetFrameRate 产生不必要开销。
  • 应用帧归一化:max(logic, camera) <= 30 → 30,否则 60,Unity 只支持固定刷新率档位。
  • 策略层保持纯逻辑:FrameRatePolicyResolver 不读单例、不碰 Camera,优先级判断可以 EditMode 单测——后续改策略先改测试,比上车机肉眼看帧率靠谱。
  • 云配原子校验:云配下发的帧率配置任一字段非法(负数/非数字)就整包拒绝,XSystemSetings 不变更,防止部分字段生效导致帧率配置不一致。
  • P1/P2 已永久移除:历史遗留的中间档位不再需要,排查时遇到旧文档或旧日志里的 P1/P2 分支直接忽略。
  • LensShift 非长期 P 不拉高帧率:过渡动画用默认帧率是可接受的,GPU 预算和功耗优先——这是设计决策,不是掉帧 bug。

指标 ​

指标优化前优化后
拖拽延迟1 帧(~16ms @60fps)0 帧
跟手度有拖影感即时响应
高帧率档生效时机下一个 vsync当帧强制渲染

16ms 的差异人眼一般感知不到,但拖拽地图时延迟会逐帧累积,「不跟手」的感觉是真实的——高帧率摄像机下的逐帧对比是唯一能服众的验证方式。

复盘 ​

  • 跟手度问题要拆成「事件处理位置」和「配置生效时机」两件事分别看,只修一处仍会残留一帧级的延迟。
  • 一帧级的优化不能靠体感验收,台架 + 高帧率摄像机逐帧对比是这类问题的标准验证手段。
  • 帧率决策链(采集 → 纯逻辑决策 → 应用)拆干净之后,排查「帧率为什么没变」只需要看日志:先搜「帧率变化」,不正常就往上追可见区域信号和档位信号。
  • 排查链路本身也值得固化:先看帧率变化是否符合预期,不符合就先查上游可见区域信号(最常见的错误来源),再查更细的档位信号——链路简单到可以直接交给工具按日志自动定位。

罗子初 · AI 座舱专家 · 8年端侧研发

Lv.1