
移动游戏出现卡顿时,我们通常先查看 GPU 使用率。
奇怪的是,有些设备的 GPU 占用并不高,画面却仍然间歇性停顿;降低分辨率和纹理质量后,帧率也没有明显改善。
这往往说明问题并不发生在 GPU 执行阶段,而是卡在了更靠前的渲染线程。
一、一帧画面是怎样产生的
一帧并不是由 GPU 独立完成的。简化后的流水线如下:
游戏逻辑
↓
动画与物理计算
↓
场景更新
↓
生成渲染指令
↓
RenderThread 提交命令
↓
GPU 执行
↓
屏幕显示
在 60Hz 屏幕上,每一帧只有大约:
1000 ÷ 60 ≈ 16.67ms
如果前面的逻辑线程消耗 5ms,RenderThread 消耗 14ms,即使 GPU 只工作了 4ms,整帧仍然超过预算。
因此:
GPU 不忙 ≠ 渲染没有瓶颈
二、RenderThread 到底负责什么
不同引擎和系统的具体实现有所区别,但渲染线程通常承担以下任务:
- 整理当前帧需要绘制的对象;
- 计算或更新渲染状态;
- 生成绘制命令;
- 切换材质、纹理与管线;
- 上传部分资源;
- 管理渲染目标;
- 向图形驱动提交命令;
- 等待必要的线程或硬件同步。
它更像 GPU 的“施工调度员”。
如果调度员迟迟没有准备好任务,GPU 只能等待。监控工具看到的结果便是:GPU 占用不高,但画面仍然掉帧。
三、先判断卡顿属于哪一种
平均帧率很容易掩盖问题。
例如下面两组数据的平均值可能接近,但体验完全不同:
场景 A:16ms、17ms、16ms、17ms、16ms
场景 B:10ms、11ms、48ms、9ms、10ms
场景 B 的平均性能看起来不错,却存在明显的瞬时卡顿。
排查时应该重点关注:
- 单帧总耗时;
- RenderThread 单帧耗时;
- 超过帧预算的次数;
- 帧间隔是否稳定;
- 卡顿是否固定周期出现;
- 卡顿时 GPU 是否处于等待状态。
四、最常见的五类原因
1. 绘制调用过多
场景中的对象越零散,渲染线程需要生成和提交的命令就越多。
假设一个场景包含:
- 400 个独立装饰物;
- 200 个粒子发射器;
- 100 个不同材质对象;
- 80 个动态 UI 元素。
即使每个对象的模型非常简单,大量状态切换与命令提交仍然会压垮渲染线程。
可尝试:
- 合并静态网格;
- 使用实例化绘制;
- 减少材质种类;
- 合并相同纹理;
- 对不可见物体提前裁剪;
- 降低远处对象的更新频率。
2. 每帧创建或销毁资源
下面这种逻辑非常危险:
void RenderFrame()
{
Texture texture = LoadTexture("effect.png");
Draw(texture);
}
纹理、缓冲区和管线对象不应该在高频循环中反复创建。
更合理的方式是提前加载并复用:
class EffectRenderer {
public:
void Init()
{
texture_ = LoadTexture("effect.png");
}
void Render()
{
Draw(texture_);
}
private:
Texture texture_;
};
资源创建不仅消耗 CPU 时间,还可能触发驱动内部锁和内存分配。
3. 着色器或管线临时编译
第一次进入新场景、释放新技能或更换天气时突然卡一下,常见原因是运行期首次创建渲染管线。
典型表现是:
平时稳定
↓
第一次出现某个特效
↓
单帧耗时突然升高
↓
之后再次播放恢复正常
优化方向包括:
- 在加载阶段预热常用着色器;
- 控制着色器变体数量;
- 避免产生大量材质组合;
- 缓存已经创建的管线对象;
- 将首次使用成本移出战斗阶段。
4. CPU 与 GPU 发生同步等待
如果代码在渲染过程中立即读取 GPU 结果,CPU 可能被迫等待前面所有任务完成。
例如:
RenderScene();
ReadPixelsImmediately();
ProcessScreenshot();
同步读回会破坏流水线并行。
更好的策略是:
- 使用异步读回;
- 推迟一至数帧处理结果;
- 使用双缓冲或环形缓冲;
- 避免每帧查询 GPU 状态;
- 将截图、统计等功能放到低频任务中。
5. UI 更新过于频繁
游戏主场景并不复杂,但打开背包、排行榜或聊天面板后突然掉帧,问题可能来自 UI。
高风险操作包括:
- 每帧修改大量文本;
- 列表节点全部参与布局;
- 透明层反复叠加;
- 频繁创建和销毁组件;
- 不变的 UI 仍然持续重绘;
- 动画同时触发测量、布局和绘制。
列表应优先采用复用机制,只更新屏幕可见区域。
五、为什么降低画质没有效果
降低画质主要减少 GPU 的工作,例如:
- 减少像素着色计算;
- 降低纹理采样量;
- 减少后处理效果;
- 降低渲染分辨率。
但如果瓶颈在 RenderThread,真正耗时的是命令准备、状态切换或线程同步。
此时可能出现:
高画质:RenderThread 19ms,GPU 8ms
低画质:RenderThread 18ms,GPU 4ms
GPU 时间减少了一半,但一帧仍然超过 16.67ms,所以用户几乎感受不到改善。
六、建立可重复的测试场景
不要只凭“感觉有点卡”进行优化。
可以设计固定测试路线:
- 从同一个存档进入游戏;
- 使用固定角色和装备;
- 沿相同路径移动;
- 在固定地点释放技能;
- 保持测试时间一致;
- 分别记录逻辑线程、渲染线程和 GPU 数据。
建议保存以下指标:
平均帧耗时
P95 帧耗时
P99 帧耗时
最长单帧耗时
RenderThread P95
GPU P95
卡顿帧数量
P99 表示 99% 的帧都不超过这个数值,它比平均帧率更能反映偶发卡顿。
七、一次典型的排查过程
某游戏在战斗中平均保持 58 FPS,但释放群体技能时会明显停顿。
最初怀疑粒子效果导致 GPU 过载,于是降低特效分辨率,结果没有明显变化。
进一步分析得到:
游戏逻辑线程:6ms
RenderThread:31ms
GPU:7ms
继续拆分后发现,该技能会瞬间创建 120 个特效对象,每个对象都拥有独立材质实例。
优化方式:
- 使用特效对象池;
- 共享只读材质;
- 合并纹理;
- 对相同粒子进行批处理;
- 在进入战斗前预热相关管线。
优化后的数据变为:
游戏逻辑线程:6ms
RenderThread:10ms
GPU:8ms
卡顿消失,而 GPU 时间几乎没有变化。这正说明原始瓶颈不在 GPU。
八、推荐的排查顺序
遇到类似问题,可以按以下顺序处理:
- 确认卡顿发生时的单帧时间;
- 比较主线程、RenderThread 与 GPU 耗时;
- 检查绘制调用和状态切换数量;
- 检查是否临时加载或创建资源;
- 检查着色器首次编译;
- 检查同步读回与线程等待;
- 检查 UI 布局和组件创建;
- 最后再调整画质与分辨率。
总结
移动游戏的渲染性能是一条流水线。任何一段超过帧预算,最终都会表现为画面卡顿。
真正有效的诊断方式不是只看 GPU 使用率,而是回答三个问题:
谁没有按时完成?
它在等待什么?
这一帧为什么与其他帧不同?
把 RenderThread 纳入分析后,许多“GPU 很闲却依然掉帧”的问题便不再神秘。

417

被折叠的 条评论
为什么被折叠?



