1. IMR——桌面 GPU 的渲染流程
1.1 IMR 渲染特点
DrawCall 中的模型顺序执行 VS 和 PS
每个DrawCall 完成后,PS 将所有像素颜色、深度等写入 FrameBuffer
每个像素可以被多次写入,写入顺序和 DrawCall 执行顺序一致
1.2 作为写入目标的FrameBuffer ,对显存的性能要求
访问速度:极高的访问速度
容量要求:满足响应分辨率和格式(RGBA,HDR等)的容量
功耗:电池供电设备的低功耗要求
在目前的移动设备硬件水平下,这三个目标通常最多只能满足两个。目前的主流选择:牺牲容量,换取更快的访问速度和更低的功耗。
2. 解决方案:TBR(Tile Based Rendering)
2.1 SIMD 核心不直接写到 FrameBuffer,而是写到TileBuffer,TileBuffer 的内容在恰当的时候写到 FrameBuffer
小容量、高速 On chip 的 TileBuffer 作为 SIMD 的写入目标
FrameBuffer 会分块依次渲染
单Tile上所有任务,即每个 Tile 上的 DrawCall 都完成后才一次写入显存
2.2 TBR 中的深度测试
- Early-Z:在 PS 之前执行,测试失败的像素不执行PS
- Late-Z:在 PS 之后执行,测试失败的像素不写入 Color Buffer
- 可见 Early-Z 能显著降低 PS 的性能压力,最好在无法应用 Early-Z 的时候再启用 Late-Z
什么情况下 Early-Z 失效?
因为 Early-Z 在PS 之前执行,所以需要必须在 PS 之前就确定深度,也就是说 执行PS时像素深度不能发生变化
- Alpha Test / Clip:需要执行完 PS 后,才能确定该像素深度是否被写入
- Custom depth,PS中改写深度值,硬件能够感知,Early-Z 阶段无法获取最终的深度值,失效。
2.3 移动端 GPU 的 Early-Z
- TileList 保存当前 Tile 上的所有三角形列表
- 隐藏面剔除HSR(Apple/PowerVR), LRZ(Adreno), FPK(Mali)
- 原理:硬件层面先对 Tile 中的三角面进行 Depth Prepass,相当于先进行了一边深度测试,剔除掉看不到的 fragment,可以显著降低 overdraw 程度,减少 PS 调用次数
- 移动端 Early-Z 失效:打断了硬件的 Depth Prepass
2.4 性能关注点
2.4.1 顶点开销
- TBR 中顶点会存在 TileList 中,过多的顶点会使得 TileList 过大,影响访问性能和内存开销
- 移动端 Early-Z 虽然会降低 PS 开销,但是会增加 VS 开销(额外做一遍 Depth Prepass,有些厂商的GPU实际上是执行了良次 VS,其中一次会优化掉和位置计算无关的部分),因此优化 VS 复杂度(特别是位置计算)尤为重要
2.4.2 Tessellation
- 移动 GPU 中通常没有专门的 Tessellation 单元,效率更低
- Tessellation 产生更多顶点,需要执行更多次 VS,增加开销
2.4.3 充分利用 TileBuffer
TileBuffer 和主显存之间消耗大量带宽,针对这个消耗进行如下的优化
- 每一帧对 TileBuffer 执行Clear ,避免上一帧的 Color Buffer/ Depth Buffer 又被从主存加载一边
- 渲染完成后 Discard 掉不需要的 Render Target 或 Depth Buffer,避免会写到显存,节省大量带宽。
3. TBDR:基于 TileBuffer 的延迟渲染
3.1 延迟渲染传统流程
- 需要提供较多的显存空间,支持多张 RT(MRT),也就是GBuffer,写入材质各种属性
- 独立的一个 LightPass,读入每个像素的GBuffer属性,并进行光照处理,得到最终的 ColorBuffer
- 移动平台上难以实现的原因:延迟渲染因为 MRT 的存在,大量消耗显存读写的带宽
3.2 移动设备上的延迟渲染(TBDR)
- 像素的属性(MRT)不被写入主显存,只存在于 TileBuffer中,最终只复制 Color 信息到主显存中,降低了带宽消耗
- 几乎不会占用额外带宽
4. 移动设备上的 MSAA
4.1 传统 MSAA 流程
- 需要一个4倍RT来获得4倍的fragment,最后再 resolve 会1倍的 RT
4.2 移动设备 MSAA 流程
- 多倍采样的 RT 仅在 TileBuffer 中存在
- Resolve 发生在 TileBuffer 中,逐 Tile 执行
- 仅单倍采样的 RT 被写回显存
5. GPU 和 CPU 协作(Frame Pacing)
5.1 基本的 FramePacing 流程
- CPU 和 GPU 异步运行
- Command Buffer 作为 CPU 和 GPU 的协议包
- CPU 填充 CommandBuffer,在提交后(DrawCall)后 GPU 才开始执行 CommandBuffer
5.2 对 FramePacing 进行优化
目标:最大程度的并行化
- 尽早提交渲染命令,让 GPU 尽早开始工作,但避免将 CommandBuffer 拆分过于细碎,提交本身也有消耗
- 非渲染相关的 CPU 任务不必要等待上一帧渲染完成(不要等 GameUpdate)
- 保存数据单向流动(CPU到GPU),避免单帧内 CPU 依赖 GPU 执行结果
CPU 帧内依赖于 GPU 执行结果的一个例子是 “硬件遮挡查询”机制,它需要 CPU 创建遮挡查询命令提交给 GPU,等待 GPU 的运算结果,再根据结果执行渲染。
5.3 垂直同步
渲染结果只能在固定的时间点显示到屏幕,比如移动设备的屏幕刷新率为 60Hz,意思是手机屏幕每秒最多进行 60 次的缓冲区交换(渲染刷新)
问题:
- 若某帧渲染太快,到了第一个同步点就刷新到屏幕了,而下一阵太慢,第二个同步点才刷新,则帧率不稳定,有卡顿感
- 若刷新率慢而帧渲染率快,未开启垂直同步的情况下,可能画面撕裂,上一个 FrameBuffer 才显示一半,下一帧 FrameBuffer 已经提交,再刷新就变成了下一帧的画面
- 移动设备上始终开启垂直同步,保证最小的帧间隔时间
- 做帧率限制时,保持最大帧率和屏幕刷新率整除的关系,如60Hz设备可限制 30FPS,20FPS,但不要限制 25FPS
- 使用图形层 API 接口来限制