作者:MahomeAlex, iOS 开发工程师
Sessions: https://developer.apple.com/videos/play/wwdc2020/10602/
前言
本文内容基于 WWDC 2020 Session 602 Harness Apple GPUs with Metal[1] 整理。
本 Session 主要讲的是 Metal 如何和 Apple 图形处理器特性相结合,为我们呈现完美的高性能 App 和游戏体验。因为讲的是 GPU (硬件),所以我们只谈渲染管线,不涉及代码。
当然希望您有基本的 Metal 和图形渲染知识,以便更好的理解本 Session 的内容。
概况
本 Session 主要讲两个部分。
TBDR GPUs
贴图延迟渲染 TBDR ( Tile Based Deferred Rendering ) 是一种更*的渲染架构,负责基本的管线渲染。移动端 GPU 渲染模式发展是:
-
IMR 模式
-
TBR 模式
-
TBDR 模式
有兴趣的可以另行了解,这里不做展开。
最新 GPUs
对渲染管线的一些加强。
GPU 的重要性在于:
-
它是一系列处理器的核心之一。
-
使用量巨大,全世界有 10 亿以上的设备使用了苹果的 GPU 。
苹果处理器的基本特点:
-
苹果处理器非常高效
-
他们共享统一的内存架构,也就是说 CPU 和 GPU 共享系统内存。而且 GPU 有自己独立的内存,叫贴图内存(显卡内存)
-
GPU 没有自己的视频内存,所以当视频内容没有被调整的话,带宽就会是一个问题
-
GPU 特有的 TBDR 架构解决视频内存问题
接下来会讲一下渲染管线还有它的一些特性。接下来会讲几个方面:
-
顶点和片元阶段重叠
-
隐面消除
-
可编程混合
-
低内存渲染目标
-
MSAA 实现
正文
首先我们看一下图形管线:
我们的 GPU 是基于 TBDR 框架的,TBDR 有两个主要阶段:
-
处理几何计算(顶点)的贴图阶段
-
处理像素的渲染阶段
贴图阶段
在对整个渲染通道中,GPU 主要做这几个步骤:
-
把视图分割成一系列的图块
-
然后着色所有的顶点
-
并把转换过的图元放到这些图块中
GPU 并没有很大的专有内存池,所以这些转换后的数据就会放到贴图的顶点缓冲区中:Tiled Vertex Buffer。顶点缓冲区缓存了贴图阶段的输出内容,包括转化后的顶点数据,还有其他的内部数据,他们对我们都是不透明的。但是当数据满了的话会引发一个本地渲染,意思是当 GPU 分割渲染通道从而刷新缓冲区的内容。不过我们只需要知道顶点缓冲区的存在,并不用担心什么。
渲染阶段
GPU 把整个视图切成一块块的图块,接下来就是单独渲染这块图块。
渲染阶段,GPU 在渲染通道里会对每个图块执行如下操作:
-
加载
-
光栅化图元和计算他们的可见度
-
着色所有可见的像素
-
执行存储操作
这些对应用都是不透明的,而且执行的很快。每个图块都会被执行加载和存储操作。
加载和存储操作
对加载和存储操作来说,在通道的开始阶段,我们会告诉 GPU 如何初始化图块内存,结束时又会告诉 GPU 如何处理最终渲染,推荐只加载我们需要用到的数据,如果什么都不需要,那就不要加载并且做清除操作,这样可以省下用来上传颜色附件,深度信息和模板缓冲的内存。存储操作也是如此,尽量节约内存开销对渲染非常有用。
隐面消除- HSR
渲染阶段的另一个重要点就是 HSR(隐面消除),这是在渲染之前发生的,归功于芯片的深度缓存信息,也是 TBDR 的一个关键点之一。HSR 通过持续追踪图层每个像素点的可见区域来让 GPU 实现最小绘制区域。HSR 是像素最优和命令提交隔离的。所以像素处理分两个阶段,隐面消除和片元处理。比如,即使你想画两个前后顺序的三角形,HSR 也会确保没有多余绘制。
假设我们要从后往前绘制三个三角形。依次是蓝色,橙色,和一个半透明的紫色三角形。
HSR 会持续追踪可视化信息,而且每个像素都会保留最前面图元的深度信息和图元 ID,这种情况下,以为是背景颜色也会用到,因为我们还没光栅化任何事情。
首先,我们光栅化第一个图元(蓝色那个)。
这个图元会把深度信息记录到覆盖到像素点上,这时候并没有走着色流程,然后光栅化第二个三角形。
因为这个在前面,所以需要更新它所覆盖到的像素的图元信息。GPU 会计算每个像素的可视化情况,但是不会着色。接着第三个三角形。
HSR 只保留每个像素最前面图元的 ID,但是现在有一个图元需要着色。这个时候, HSR 块就会刷新半透明图元覆盖到的所以像素点,GPU 就没办法再延迟渲染了,不过只会刷新被覆盖到的点,然后得到正确的图元信息。因为是最后的图元,所以可见缓冲区内的其他点也会被着色。从而完整着色所有点。这里我们可以发现,有的点只是被着色一次。尽管存在多个片元重叠,但是这些像素点并没有多次重绘。在这种情况下,重绘的只是每个点所涉及到的片元着色器的数量,尽量控制数量。优先处理不透明的材质,然后做 Alpha 测试,丢弃或者深度反馈,最终是那些半透明的材质,避免交叉不透明和半透明材质,或者是有颜色附件的掩图等,最优化 HSR 效率可以降低重绘率。
所以在有半透明材质的场景下,重绘依赖于 HSR 的效率, 所以想尽办法提高效率,根据材质特性调整顺序:
-
优先不透明材质
-
做 Alpha 测试,丢弃或者深度反馈
-
半透明材质
Alpha 混合
回到刚才的例子,这次把半透明三角形放到第二的位置。
如果我们从后往前提交,就会遇到不同的可视化状态,这种情况下,交叉不透明和半透明图元是不高效的,很多像素刚着色完又会被下一个图元遮罩,然后重新着色。为了优化效率,我们先渲染所有不透明几何体,通过先后提交顺序去掉排序环节,然后到片元处理阶段,因为显卡自带帧缓冲,这个过程非常快。
Alpha 混合也是在显卡内存中实现,而且没有混合单元,当我们执行加载和存储行为读写渲染目标的任务就完成了。不过有时候传统的 Alpha 混合并不够,比如像全局迷雾或者延迟光照这种需要自定义混合的全局效果。
大多数的混合都会把附件存储起来,以便下一个通道用到。所以 GPU 开发了可编程混合模式
允许片元着色器直接操作贴图内存,这样就可以把多个通道融合成一个从而大幅降低带宽。在这里例子中,我们不需要加载或者存储任何中间渲染目标, 不过还是有些浪费的地方。比如有些渲染目标并没有加载和存储,只是在贴图内存中使用了。而且他们是非常大的纹理内存。针对这点,TDBR 提出了低内存渲染目标概念,意思是极低内存占用的纹理。
低内存渲染目标
这里先讲一下锯齿问题。
我们经常在游戏等场景中见到那种锯齿类型或性质的纹理样式,GPU 会采样中间像素,并且只着色交叉的相同点的图元。
抗锯齿就是一种非常常见用于光栅化多采样每个像素点的技术,以更优的像素处理解决方法,从而获得更好的 UI 效果。主要体现在:
-
着色器会对渲染三角形覆盖到的每个像素,然后混合其他的采样点。从而让图元的边角变得平滑。
-
检测图元的边缘,非边缘像素是整个像素混合,边缘的做采样着色。
-
多采样的数据还会存储到芯片的内存中,并且在图元被刷新的时候删除,使得存储行为非常高效。
这种低内存模式还可以省下足迹内存开销,比如像这样的纹理,在通道最后,我们只存储了多采样的数据,从而节省了大量的存储多采样纹理的带宽,因为他们存储在贴图内存中。这样的纹理就非常短暂,根部不需要加载和存储,从而实现真正的低内存开销。TBDR 还有一个特性是图块和渲染在渲染通道中是不同阶段的,这样就允许贴图可以混合上一个通道的渲染结果。
在优秀的 TBDR 架构下,苹果设计的 Metal 可以快速的调用框架,两者相得益彰。Metal 开发了一个特有的图形和计算机框架和一些 TBDR 功能,比如可编程渲染,低内存渲染目标,明确的提交模型和多线程,这些深度的软硬件优化使得算法优化非常容易,比如:
延迟渲染
延迟渲染是多通道算法,可以在实际的呈现过程中解耦渲染的场景特性。一般情况下,我们可以认为每一个算法阶段都是一个渲染通道,这样需要着色出结果然后采样很多数据从而最终渲染,非常耗带宽,而且会有很多临时内存去存储纹理和缓存数据。所以在前面几个优化特性下,我们可以让算法非常高效运行。
Modern Apple GPUs
苹果自 A11 开始就重新设计和平衡了 GPU,从而支持更现代化的渲染算法,管理复杂的数据结果和更高的数据精度,并且做了几个非常稀有的优化。下图是 TBDR 管线。
大部分的改动都在这里。苹果新增了一个芯片图块和可编程图块计算。
图块
图块是 2D 数据结构,有宽高和深度信息,存在贴图内存中,可以被片元函数和内存函数访问,明确使用这种数据结构可以获得很高的收益。
在图块之前,我们可能得把纹理一点点的移到线程池的内存中,但是 CPU 不知道你在操作图片数据,所以只能把数据存储起来。有了图块之后,我们可以一次性加载和存储。
图块着色
图块着色是系统内核函数,我们可以进行分发,从而在渲染通道中去改变图块信息,分发行为是和绘制操作交错的。而且他们是跟前后绘制操作隔离的,所以不需要关心同步问题。贴图着色和图块都会应用 MSAA 技术,而且可以灵活开关。
为了更好地扩展图块和图块着色能力,图块采样覆盖控制可以控制到每个像素采样覆盖的数据,甚至于多采样通道,从而可以深度的发挥 MSAA 效果。在一些有很多不透明或者半透明几何体的复杂场景,比如粒子效果下,大量的小半透明三角形需要每个像素点都有很多采样,这种情况就需要在渲染前用图块把用贴图管线把采样数据处理下,而这个能力就可以通过图块管线处理采样数据,通过渲染这些不透明材质,确保所有的像素点都有单一的颜色,而且这也是可编程的。支持对每中类型的渲染目标定制通道,比如 HDR 颜色或者线性深度。总之,贴图着色和图块给 GPU 提供了很多灵活性,帮助我们去优化 TBDR 架构。
Metal
Metal 使得优化 GPU 的能力成为可能,主要体现在精确地控制贴图内存和 GPU 驱动型渲染。Metal 允许通过贴图着色器,图块,和持有线程池内存直接使用贴图内存,从而优化一些复杂算法的效率,比如图块延迟渲染。我们可以认为这是一个更高级版本的延迟渲染,在大型的着色场景下效果更明显。
这个算法增加了一个额外的步骤,把光线切成小图块,这些图块通过后面的计算通道用来渲染场景。所以,如果要用延迟渲染,就需要添加额外的计算步骤去切割光线。我们也可以用估算来做光线卷积计算,但是会阻止 GPU 整合管线,因为可编程渲染只允许一次处理单一像素,导致光线切割成本太高昂。好在图块着色允许整块处理,这样就能交叉渲染计算,从而把三个渲染通道整合成一个,效率可想而知。
Metal 还通过 GPU 驱动渲染模式更大的发挥能力,因为 Metal 独有的自变量缓冲和间接命令缓冲,当然整个渲染流程是 CPU 驱动的。因为数据集和传送数据过程是很复杂的,需要几个结构来定义整个场景。
在这个例子中包含你了一些网格,材质和模型,每个模型包含标记父网格和对镜材质。主要问题是当我们要开始渲染时,我们不能每帧都全部渲染。
这时 CPU 会基于上一帧和视景的遮罩决定哪个图形需要渲染,还需要选择适当的细节。这个就需要一些同步点,CPU 需要读取 GPU 写入的遮罩数据,从而发出绘制命令。所以这里就有两个问题需要解决,遍历场景描述和编码绘制命令。
Metal 提供了几点帮助:
-
自变量缓冲,使得场景数据在 GPU 上可用, 而且可以定义复杂数据结构
-
间接命令缓冲,允许 GPU 编码绘图指令。
所以这种场景就可以通过一个自变量缓冲数据来解决,而且 GPU 可以很方便地传输。
所以 GPU 管理渲染回路的步骤是:
-
遍历场景渲染遮罩。
-
再遍历场景剔除不可见像素和细节选择。
-
最终渲染场景。
这些都不需要做 CPU 和 GPU 同步流程。
通过 Modern Rendering with Metal[2] 的代码可以学习如那个何实现这些算法,包括了一个完全的 GPU 驱动渲染循环。这些是用新的结构,比如自定义缓存或间接命令缓存写的,还利用了 GPU 的那些能力,包括图块着色,图块,低内存纹理和可变的光栅化率。这些只是概要,具体需要深入去学习。今天的 Session 就到此。
来源 中华文学