Skip to content

录制渲染卡顿攻克 ​

录制卡顿的根因不是 GPU 也不是内存,而是编码被放在 Fetch 线程里同步执行,堵住了下一帧的读取。修法是经典的生产者-消费者解耦:编码独立成线程,中间加一个有界队列,满了就丢最老的帧。

队列上限3 帧
单帧大小7-10MB → ~1MB
背压策略丢最老帧

背景 ​

高德车道级导航的录制回放功能,录的是 3D 渲染画面(SR)。录制对帧率要求高,H.264 编码的计算压力也大——一边要维持渲染帧率,一边要把每帧编码落盘。功能上线后录制时画面明显卡顿。

问题 ​

SR 卡顿在排查纪律上要先分成三类症状,修法完全不同:

症状类别特征典型根因
严重掉帧渲染帧率被钉在 1fpsSurface 前后台门控(可见区域信号下发 foregroundState=0),是设计行为不是渲染 bug
轻微掉帧逻辑/渲染帧率低于 25云配下发的帧率档位本身偏低,或命中了 P4/P5 低帧率档
偶发卡顿帧率决策正常,但单帧超时单帧内某个任务耗时过长(帧超时异常)

按这个分类走排查链路:先搜「帧率变化」日志,渲染帧没有掉到 1、决策档位也正常——排除前两类,落到「偶发卡顿」。同时做了两个量化排除:

  1. 怀疑 GPU 瓶颈 → 量化 GPU 占比,录制时未超 80%,排除。
  2. 怀疑内存 → Heap 分析,录制时内存增长正常,排除。

最后定位到编码:H.264 编码在 Fetch 线程里同步执行,编码耗时长,直接阻塞了下一帧的读取——渲染管线被录制拖住,表现为卡顿。

佐证来自单帧耗时的监控:CartonProfiler 会对超时帧打「帧超时异常」日志,每条带帧号和耗时(cost_time)。卡顿时刻附近这类日志成簇出现,且耗时集中在编码调用上——帧率决策正常、单帧超时,正是「偶发卡顿」这一类。

方案 ​

mermaid
flowchart LR
    subgraph Before["重构前"]
        A1["Fetch 线程"] --> B1["读渲染帧"] --> C1["编码 H.264<br>耗时阻塞"] --> D1["写文件"]
    end
    subgraph After["重构后"]
        A2["Fetch 线程"] --> B2["读渲染帧"] --> C2["入队即返回"]
        C2 --> Q["有界队列<br>上限 3 帧"]
        Q --> E2["独立编码线程"] --> F2["写文件"]
        Q -.->|"满"| G2["丢弃最老帧<br>丢帧背压"]
    end

核心是把「读帧」和「编码」解耦:Fetch 线程只负责把渲染帧搬进队列,立刻返回去读下一帧;编码在自己的线程里慢慢跑。队列必须有界——无界队列只是把卡顿换成内存无限增长。

关键实现 ​

有界录制队列 ​

  • 队列大小上限 3 帧。
  • 队列满时丢弃最老的帧(丢帧背压):编码跟不上时,宁可录制文件跳帧,也不让延迟逐帧累积,更不让内存无界增长。
  • 丢最老而不是丢最新:新帧代表最新画面,跳帧发生在旧帧上,观看体感更连贯。

帧率策略侧的配合 ​

录制回放要求渲染画面流畅,手势/运镜场景由帧率策略的 P3 档(Gesture,默认 60/60)保证高帧率;决策层 FrameRatePolicyResolver 是纯逻辑、不依赖 MonoBehaviour,优先级判断可以 EditMode 单测。云配下发的帧率配置走原子校验:任一字段非法(负数/非数字)整包拒绝,防止半个配置生效导致帧率状态不一致。

排查时还有一个容易误判的点:Surface 退到后台时渲染帧率被门控到 1fps(不可见时几乎不渲染,最大化省 GPU),这是设计行为。如果录制时看到渲染帧是 1,先查「可见区域」信号,别先去怀疑编码。

帧率配置链路本身可以用 ADB 注入做等效验证:广播一条云配同格式的帧率字符串,走的是同一条解析-应用链路——注入成功说明 Unity 侧 apply 链路正常,失败则一定伴随 [CloudConfigFpsApplier] 的错误日志(原子校验拒绝整包),不会出现「一半生效」的中间态。

为什么队列上限是 3 帧 ​

3 帧是在「编码偶发抖动」和「录制时效性」之间取的值:小于 3 帧,编码线程一次正常的抖动就会触发丢帧;大于 3 帧,队列里积的帧越来越旧,录制文件和实时画面之间会出现可感知的延迟,内存占用也随之上去了。

后续:P 帧压缩 ​

录制文件体积的进一步优化:

高德录制 P 帧压缩:细化颗粒度 + mask 压缩,预估单帧 7-10MB → 约 1MB。

指标 ​

指标优化前优化后
录制时帧率明显卡顿稳定
录制内存风险编码积压可无限增长有界,最多 3 帧
编码 CPU 占用阻塞 Fetch/渲染管线独立线程,不影响主线程
单帧大小(P 帧压缩后)7-10MB约 1MB
录制文件质量—与实时渲染一致

复盘 ​

  • 卡顿先分类再排查:严重掉帧查前后台门控,轻微掉帧查云配档位,偶发卡顿查单帧超时。三类混在一起看,只会互相误导。
  • 排除法要有量化依据:「GPU 未超 80%」「Heap 增长正常」这种结论才能支撑继续往下走,拍脑袋排除会把排查带进沟里。
  • 生产者-消费者之间只要有耗时不对称,队列就必须有界;背压策略(丢哪帧、什么时候丢)是设计的一部分,不是事后补丁。
  • 编码这类确定性耗时的任务,永远不要和帧读取这类时效性任务放在同一个线程里。
  • 可观测性是分类排查的前提:没有 CartonProfiler 的帧超时日志,「偶发卡顿」很难和帧率决策类问题区分开,只能继续靠猜。

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

Lv.1