Metal 早在 iOS8 就推出,一直也没太了解过,18 年苹果开始废弃 OpenGL ES,Flutter 1.17 也使用 Metal 优化在 iOS 上的性能 (Announcing Flutter 1.17),不过如果不做图形或游戏引擎相关开发影响不是很大,但也需要开发者对其有些了解。这篇文章主要会介绍 Metal 及基本概念,还有在 pipeline 上与 OpenGL ES 的对比。

如果想学习 Metal 的话其实比较推荐 RW 家的 Metal by Tutorials,如果想了解应用渲染相关的推荐最近掘金上的一篇文章 iOS Rendering 渲染全解析

Metal

Metal 提供对 GPU 的近乎直接的访问,它建立在具有预编译 GPU shader,细粒度资源控制和多线程支持的体系结构上。

metal layer

Metal 在开发中与之前的绘制框架 Core Graphics,OpenGL ES 属于同一层级。我们知道所有的渲染最终都是由 GPU 完成执行的,而 Metal 则提供给我们低层级的 API 使得我们可以直接跟 GPU 打交道,通过软硬件的优化使得 CPU 与 GPU 能够更高效的协同工作。

GPU 和 Shaders

通常在协作时 CPU 会快速,低延迟地处理连续的任务,对视图树进行计算并序列化提交到缓冲区,而 GPU 则可以高吞吐量的处理和计算数据。其主要是因为 CPU 拥有大量的缓存空间和少量的 ALU 算术逻辑单元,而 GPU 则相反,拥有大量的算术逻辑单元,可以将任务切分成很多小的切片来并发执行,执行完成后再进行合并。

gpu

GPU 还具有用于处理几何的特殊电路,通常称为 Shader Core。这些着色器内核负责渲染屏幕上显示的颜色。CPU 会不断的发送 command 来给 GPU 去进行计算,为了避免卡顿,Metal 在 CPU 会将渲染指令排队,这样在 GPU 完成时会去队列中取新的任务而 CPU 也不会等待 GPU 而阻塞自己的工作。

以现代 AMD GPU 架构为例,其内部有:

  • 1 Graphics Command Processor: 用来处理协调指令
  • 4 Shader Engines: 一个 Shader Engine 是一个组织单元,可以为整个 pipeline 服务,每个 Shader Engine 有一个几何处理器,一个光栅化器和计算单元
  • 9 Compute Units: 一个 Shader Engine 有 9 个 CU,一个 CU 是一个着色器核心组
  • 64 Shader Cores: 一个 Shader Core 是一个 GPU 的基本模块,用来完成全部着色工作

所以该架构下 GPU 中总共有 36 个 CU 以及 2304 个 Shader Core

Metal 渲染管线

metal pipeline

Vertex Fetch

要开始渲染内容,首先需要一个由具有顶点网格的模型组成的场景。一般会包括顶点及其位置,纹理坐标,法线和颜色属性等信息。获取到这些信息后会交给 Scheduler 进行下一阶段 Vertex Processing

Vertex Processing

在这个就阶段,顶点会被独立的处理,计算顶点最终在帧缓冲区中的位置。之后 Distributer 将分组的顶点进入下一阶段 Primitvie Assembly

Primitive Assembly

接收到顶点数据后,比较重要的是确保同一组的顶点属于相同的几何图形。基元装配阶段会将点连接起来称为线,三角形或者其他图形。另外在屏幕外的图形也会被剪裁掉。然后会发送给光栅化器进入光栅化阶段。

Rasterization

光栅化器对场景中的每个对象,将光线发送回屏幕然后检查对象覆盖了哪些像素,此时使用 X 和 Y 坐标将二维网格上的顶点连接起来。还会计算斜率等,然后扫描转换屏幕上每一行,最终确定是否可见及其他属性。

Fragment Processing

片元处理阶段跟顶点处理相似都是一个可编程阶段。需要创建一个片元着色器函数,该函数将接收顶点函数输出的照明,纹理坐标,深度和颜色信息。最终会计算出每个像素最终的单一颜色,之后会进行一系列测试比如透明度测试,模板测试等等,然后将数据落到最终用于屏幕展示的帧缓冲区上。

其实整个 pipeline 一些核心概念与后面要讲到的 OpenGL ES 相似,比如顶点着色和片元着色,我们在创建 MTLLibrary 后需要指定 vertex function 和 fragment function

let library = device.makeDefaultLibrary()
let vertexFunction = library?.makeFunction(name: "vertex_main")
let fragmentFunction = library?.makeFunction(name: "fragment_main")

vertex_mainfragment_main 则是 shader program

vertex float4 vertex_main(const VertexIn vertexIn [[ stage_in ]],
                          constant float &timer [[ buffer(1) ]]) {
  float4 position = vertexIn.position;
  position.y += timer;
  return position;
}

fragment float4 fragment_main() {
  return float4(1, 0, 0, 1);
}

一些阶段的处理两边也相近比如都有基元装配,光栅化和片元着色后的 per-fragment operation 等。

OpenGL ES

OpenGL ES 长久来都是一个跨平台的底层绘制框架,兼容性支持很好但苹果的 Metal 会基于自家的设备硬件上进行优化,在支持最新和最出色的功能同时降低性能开销。

两者使用上比较大的区别是,在 Metal 中通常使用两种类型的对象进行操作:

  • Descriptor objects
  • Compiled-state objects

我们可以创建一个描述符对象并对其进行编译,编译状态对象是 GPU 优化的资源。创建和编译都是昂贵的操作,所以我们可以尽可能少地执行它们,然后再使用已编译的状态对象进行操作。

另外我们再来看下 OpenGL ES 的渲染管线(3.0):

gles-pipeline

其实也可以看到大部分的概念比较相近,处理上的细节可能不尽相同。 API 上差别比较大,但这篇文章也更多是关注一些基础概念并做一些简单的讨论。

附上一些 Metal 最近的 Session:

这篇文章部分参考和摘选自:

  1. Metal by Tutorials
  2. Apple Developer - Metal
  3. Moving from OpenGL to Metal
  4. 《OpenGL ES 3.0 编程指南》