Appearance
拖拽 0 帧延迟优化
拖拽跟手度差不是「渲染太慢」,而是两个一帧级的时间差叠加:手势在 Update 里处理导致画面晚一帧动,帧率切换要等下一个 vsync 才生效。两处都抹掉,才有真正的 0 帧延迟。
拖拽延迟1 帧 → 0 帧
延迟量级~16ms @60fps
验证方式高帧率摄像机
背景
车机 SR(小鹏一张图)支持手势拖拽地图。这套渲染的帧率不是一个值,而是三层:Unity 应用帧(只有 30/60 两档)、全局唯一的逻辑帧、每路 Surface 各自的渲染帧。逻辑帧和渲染帧都是在 Unity 主帧里做的定时计算——应用帧 30、逻辑帧 15,就是主帧每跑两帧逻辑帧才跑一次,所以逻辑/渲染帧永远不会高于应用帧。
拖拽跟手度是这套交互的基本盘:手指动、画面必须同帧动。
问题
台架测试时用高帧率摄像机逐帧对比,能清楚拍到:手指先动,地图后动,差约 16ms(@60fps)。拆开来是两个独立的一帧延迟:
- 手势处理位置错了。手势在
Update()里处理,但当帧的渲染此时已经开始,地图位置要等下一帧才被画出来。 - 帧率切换生效晚了。拖拽发生时帧率策略会把手势场景拉到高帧率档,但
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 |
| P4 | SR 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 的差异人眼一般感知不到,但拖拽地图时延迟会逐帧累积,「不跟手」的感觉是真实的——高帧率摄像机下的逐帧对比是唯一能服众的验证方式。
复盘
- 跟手度问题要拆成「事件处理位置」和「配置生效时机」两件事分别看,只修一处仍会残留一帧级的延迟。
- 一帧级的优化不能靠体感验收,台架 + 高帧率摄像机逐帧对比是这类问题的标准验证手段。
- 帧率决策链(采集 → 纯逻辑决策 → 应用)拆干净之后,排查「帧率为什么没变」只需要看日志:先搜「帧率变化」,不正常就往上追可见区域信号和档位信号。
- 排查链路本身也值得固化:先看帧率变化是否符合预期,不符合就先查上游可见区域信号(最常见的错误来源),再查更细的档位信号——链路简单到可以直接交给工具按日志自动定位。