Metal:对 iOS 中 GPU 编程的高度优化的框架

1188 查看

Metal 框架支持 GPU 加速高级 3D 图像渲染,以及数据并行计算工作。Metal 提供了先进合理的 API,它不仅为图形的组织、处理和呈现,也为计算命令以及为这些命令相关的数据和资源的管理,提供了细粒度和底层的控制。Metal 的主要目的是最小化 GPU 工作时 CPU 所要的消耗。– Metal Programming Guide

Metal 是针对 iPhone 和 iPad 中 GPU 编程的高度优化的框架。其名字来源是因为 Metal 是 iOS 平台中最底层的图形框架 (意指 “最接近硬件”)。

该框架被设计用来实现两个目标: 3D 图形渲染和并行计算。这两者有很多共同点。它们都在数量庞大的数据上并行运行特殊的代码,并可以在 GPU. 上执行。

什么人应该使用 Metal?

在谈论 API 和语言本身之前,我们应该讨论一下什么样的开发者能从 Metal 中受益。正如上面提过的,Metal 提供两个功能: 图形渲染和并行计算。

对于寻找游戏引擎的开发者来说,Metal 不是最佳选择。苹果官方的的 Scene Kit (3D) 和 Sprite Kit (2D) 是更好的选择。这些 API 提供了包括物理模拟在内的更高级别的游戏引擎。另外还有功能更全面的 3D 引擎,例如 Epic 的 Unreal Engine 或 Unity,二者都是跨平台的。使用这些引擎,你无需直接使用 Metal 的 API,就可以从 Metal 中获益。

编写基于底层图形 API 的渲染引擎时,除了 Metal 以外的其他选择还有 OpenGL 和 OpenGL ES。OpenGL 不仅支持包括 OSX,Windows,Linux 和 Android 在内的几乎所有平台,还有大量的教程,书籍和最佳实践指南等资料。目前,Metal 的资源非常有限,并且仅限于搭载了 64 位处理器的 iPhone 和 iPad。但另外一方面,因为 OpenGL 的限制,其性能与 Metal 相比并不占优势,毕竟后者是专门用来解决这些问题的。

如果想要一个 iOS 上高性能的并行计算库,答案非常简单。Metal 是唯一的选择。OpenCL 在 iOS 上是私有框架,而 Core Image (使用了 OpenCL) 对这样的任务来说既不够强大又不够灵活。

使用 Metal 的好处

Metal 的最大好处就是与 OpenGL ES 相比显著降低了消耗。在 OpenGL 中无论创建缓冲区还是纹理,OpenGL 都会复制一份以防止 GPU 在使用它们的时候被意外访问。出于安全的原因复制类似纹理和缓冲区这样的大的资源是非常耗时的操作。而 Metal 并不复制资源。开发者需要负责在 CPU 和 GPU 之间同步访问。幸运的是,苹果提供了另一个很棒的 API 使资源同步访问更加容易,那就是Grand Central Dispatch。虽然使用 Metal 时仍然有些这方面的问题需要注意,但是一个在渲染时加载和卸载资源的先进的引擎,在避免额外的复制后能够获得更多的好处。

Metal 的另外一个好处是其预估 GPU 状态来避免多余的验证和编译。通常在 OpenGL 中,你需要依次设置 GPU 的状态,在每个绘制指令 (draw call) 之前需要验证新的状态。最坏的情况是 OpenGL 需要再次重新编译着色器 (shader) 以反映新的状态。当然,这种评估是必要的,但 Metal 选择了另一种方法。在渲染引擎初始化过程中,一组状态被烘焙 (bake) 至预估渲染的 路径 (pass) 中。多个不同资源可以共同使用该渲染路径对象,但其它的状态是恒定的。Metal 中一个渲染路径无需更进一步的验证,使 API 的消耗降到最低,从而大大增加每帧的绘制指令的数量。

Metal API

虽然这个平台上许多 API 都暴露为具体的类,但 Metal 提供的大多是协议。因为 Metal 对象的具体类型取决于 Metal 运行在哪个设备上。这更鼓励了面向接口而不是面向实现编程。然而,这同时也意味着,如果不使用 Objective-C 运行时的广泛而危险的操作,就不能子类化 Metal 的类或者为其增加扩展,

Metal 为了速度而在安全性上做了必要的妥协。对于错误,苹果的其它框架显得更加安全和健壮,而 Metal 则完全相反。在某些时候,你会收到指向内部缓冲区的裸指针,你必须小心的同步访问它。OpenGL 中发生错误时,结果通常是黑屏;然而在 Metal 中,结果可能是完全随机的效果,例如闪屏和偶尔的崩溃。之所以有这些陷阱,是因为 Metal 框架是对 GPU 的非常轻量级抽象。

一个有趣的方面是苹果并没有为 Metal 实现可以在 iOS 模拟器上使用的软件渲染。使用 Metal 框架的时候应用必须运行在真实设备上。

基础 Metal 程序

在这部分中,我们会介绍写出第一个 Metal 程序所必要的部分。这个简单的程序绘制了一个正方形的旋转。你可以在 GitHub 中下载这篇文章的示例代码

虽然不能涵盖每一个细节,但我们尽量涉及至少所有的移动部分。你可以阅读源代码和参阅线上资源来深入理解。

使用 UIKit 创建设备和界面

在 Metal 中,设备是 GPU 的抽象。它被用来创建很多其它类型的对象,例如缓冲区,纹理和函数库。使用MTLCreateSystemDefaultDevice 函数来获取默认设备:

注意 device 并不是一个详细具体的类,正如前面提到的,它是遵循 MTLDevice 协议的类。

下面的代码展示了如何创建一个 Metal layer 并将它作为 sublayer 添加到一个 UIView 的 layer:

CAMetalLayer 是 CALayer 的子类,它可以展示 Metal 帧缓冲区的内容。我们必须告诉 layer 该使用哪个 Metal 设备 (我们刚创建的那个),并通知它所预期的像素格式。我们选择 8-bit-per-channel BGRA 格式,即每个像素由蓝,绿,红和透明组成,值从 0-255。

库和函数

你的 Metal 程序的很多功能会被用顶点和片段函数的方式书写,也就是我们所说的着色器。Metal 着色器用 Metal 着色器语言编写,我们将在下面详细讨论。Metal 的优点之一就是着色器函数在你的应用构建到中间语言时进行编译,这可以节省很多应用启动时所需的时间。

一个 Metal 库是一组函数的集合。你的所有写在工程内的着色器函数都将被编译到默认库中,这个库可以通过设备获得:

接下来构建渲染管道状态的时候将使用这个库。

命令队列

命令通过与 Metal 设备相关联的命令队列提交给 Metal 设备。命令队列以线程安全的方式接收命令并顺序执行。创建一个命令队列:

构建管道

当我们在 Metal 编程中提到管道,指的是顶点数据在渲染时经历的变化。顶点着色器和片段着色器是管道中两个可编程的节点,但还有其它一定会发生的事件 (剪切,栅格化和视图变化) 不在我们的直接控制之下。管道特性中的后者的类组成了固定功能管道。

在 Metal 中创建一个管道,我们需要指定对于每个顶点和每个像素分别想要执行哪个顶点和片段函数 (译者注: 片段着色器又被称为像素着色器)。我们还需要将帧缓冲区的像素格式告诉管道。在本例中,该格式必须与 Metal layer 的格式匹配,因为我们想在屏幕上绘制。

从库中通过名字来获取函数:

接下来创建一个设置了函数和像素格式的管道描述器:

最后,我们从描述器中创建管道状态。这会根据程序运行的硬件环境,从中间代码中编译着色器函数为优化后的代码。

读取数据到缓冲区

现在已经有了一个构建好的管道,我们需要用数据填充它。在示例工程中,我们绘制了一个简单的几何图形: 一个旋转的正方形。正方形由两个共享一条边的直角三角形组成: