渲染管线入门:CPU与GPU的分工,从Draw Call到像素填充
做动画优化的朋友,迟早要面对一个问题:掉帧到底掉在哪了?
很多人一上来就盯着GPU看,觉得掉帧就是显卡不行。其实我见过太多案例——CPU那边早就堵死了,GPU在那干等着。所以今天咱们把渲染管线从头捋一遍,看看每一帧到底经历了什么。
CPU与GPU:一对不太默契的搭档
先说个最直观的比喻。CPU像个项目经理,啥都能干,但一次只能处理几件事。GPU像个流水线工人,只会干一件事——画像素,但一次能画几百万个。
做动画时,CPU负责“准备画什么”,GPU负责“怎么画出来”。这两者之间有个关键接口,叫Draw Call。
核心概念:Draw Call是CPU发给GPU的一条绘图指令。每次Draw Call都包含“用什么材质、用什么顶点、画成什么样子”这些信息。一帧里Draw Call越多,CPU的负担就越重。
我记得有一次优化一个UI列表动画,帧率死活上不去。查了半天发现一帧发了400多个Draw Call。CPU光忙着发指令了,根本没时间处理动画逻辑。后来合批到80个,帧率直接翻倍。
从Draw Call到像素:完整流程拆解
一帧画面从CPU到屏幕,大致走这么几步。我画了张图,你先看个全貌:
好,咱们一步步拆开看。
第一步:CPU上的应用逻辑
每一帧开始,CPU先跑你的游戏逻辑——动画更新、物理模拟、输入处理。这部分如果太重,帧率直接崩。我见过有人在一帧里做了几百次碰撞检测,CPU直接跑满16ms,GPU连活都接不到。
个人经验:我习惯用Profiler先看CPU耗时。如果CPU单帧超过10ms,那掉帧大概率是CPU瓶颈。先把逻辑优化了再谈GPU的事。
第二步:裁剪与合批
逻辑跑完后,CPU开始决定“哪些东西需要画”。这里有两个关键操作:
- 视锥裁剪:不在相机视野里的物体直接扔掉,不浪费Draw Call。
- 合批:把能用同一材质、同一网格的物体合并成一个Draw Call。
说白了,合批就是让CPU少跑几趟。你想想看,100个同样的箱子,如果分开画就是100个Draw Call。合批之后,1个就够了。
避坑指南:我曾经为了合批把不同材质的物体硬塞到一起,结果GPU那边反而更慢了。合批的前提是材质相同、顶点格式一致。强行合批只会让CPU做更多无用功。
第三步:提交Draw Call
CPU把裁剪和合批后的结果打包成命令,塞进一个叫命令缓冲区的地方。这个缓冲区是CPU和GPU之间的桥梁。
嗯,这里有个坑。命令缓冲区是有大小的。如果一帧的Draw Call太多,缓冲区满了,CPU就得等着。这就是所谓的CPU端瓶颈。
| 指标 | 健康范围 | 警告范围 | 危险范围 |
|---|---|---|---|
| Draw Call数量 | < 100 | 100 - 300 | > 300 |
| CPU单帧耗时 | < 8ms | 8 - 14ms | > 14ms |
| 命令缓冲区等待 | 0ms | 1 - 3ms | > 3ms |
第四步:GPU顶点处理
GPU拿到命令后,先处理顶点。顶点着色器会把每个顶点的位置从模型空间转换到屏幕空间。这里有个关键点——顶点数量。
一个角色模型如果面数太高,顶点数几十万,GPU光算顶点位置就累得够呛。我优化过一个角色展示动画,模型面数从10万降到3万,画质几乎没变,帧率从25飙到55。
第五步:光栅化
顶点处理完后,GPU把三角形转换成像素。这一步叫光栅化。说白了就是决定“这个三角形覆盖了屏幕上的哪些像素点”。
光栅化的效率跟三角形大小有关。很小的三角形(比如远处物体的细节)会导致GPU花大量时间在光栅化上,但实际覆盖的像素很少。这就是所谓的微三角形瓶颈。
第六步:像素处理与输出合并
最后一步,像素着色器给每个像素算颜色。这里最容易出问题的是Overdraw——同一个像素被画了好几次。
举个例子:一个半透明的UI面板叠在角色上面。GPU先画角色,再画UI面板,同一个像素算了两次。如果叠了五六层,那就是五六倍的像素计算量。
关键数据:Overdraw每增加1倍,像素着色器的负载就翻一倍。在移动设备上,Overdraw超过3倍基本就稳不住60帧了。
输出合并阶段,GPU把最终颜色写入帧缓冲。这里还有个带宽的问题。4K分辨率下,一帧要写入约830万个像素。如果帧缓冲还带深度、模板这些附件,带宽消耗更大。
CPU与GPU的并行与等待
理论上CPU和GPU可以并行工作——CPU处理第N+1帧的逻辑,GPU渲染第N帧的画面。但现实往往没那么美好。
我遇到过一种情况:CPU提交Draw Call太快,GPU跟不上,命令缓冲区越堆越多。结果CPU发现缓冲区满了,只能停下来等GPU消费。这就是CPU Stall。
反过来,如果GPU太快,CPU还没准备好下一帧的命令,GPU就空闲了。这叫GPU Stall。
优化思路:我一般用RenderDoc抓帧,看GPU的占用率。如果GPU占用率低于80%,说明CPU在拖后腿。如果GPU占用率长期100%,那就是GPU瓶颈。
一张表总结:掉帧的根因在哪
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| CPU占用高,GPU占用低 | Draw Call过多、逻辑太重 | 合批、减少Update中的计算 |
| CPU和GPU占用都高 | 场景太复杂、分辨率太高 | LOD、降低分辨率、减少Overdraw |
| CPU占用低,GPU占用高 | 像素着色器太重、Overdraw严重 | 简化Shader、减少半透明层叠 |
| 帧率波动大 | 内存带宽不足、纹理采样瓶颈 | 压缩纹理、减少大纹理使用 |
好了,渲染管线的完整流程就是这样。从CPU的逻辑处理,到Draw Call的提交,再到GPU的顶点处理、光栅化、像素填充——每一步都可能成为掉帧的元凶。