大多数浏览器和
Developer App 均支持流媒体播放。
-
将 OpenGL App 迁移到 Metal
Metal 是 Apple 各平台上 GPU 加速图形和计算的现代基础,正在逐步取代 OpenGL、OpenGL ES 和 OpenCL。认识 Metal 的架构和功能集,并了解将基于 OpenGL 的 app 迁移至 Metal API 的分步方法。
资源
相关视频
WWDC21
WWDC19
-
下载
大家好 欢迎参加今天的 Metal 会议 我是 Lionel 我是 Apple 的 GPU 软件性能团队中的一员 我将与我的朋友 Max 和 Sarah 一起 指导你如何将 OpenGL App 带到 Metal 中 去年 我们宣布 OpenGL OpenGL ES 和 OpenCL 都是不推荐的
iOS 13 和 macOS Catalina 将继续支持它们 但现在是时候向前一步了
新项目应该从一开始 就以 Metal 为目标 但如果你有一个 想要移植到 Metal 的 OpenGL App 那你就来对地方了 我们在 2014 年 首次引入 Metal 作为我们全新的 低成本 高效率 高性能的 GPU 编程 API 在过去的五年里 Apple 的核心框架 一直采用 Metal 并且取得了非常好的效果 如果你的 App 构建在 SpriteKit SceneKit RealityKit Core Image Core Animation 等层之上 那么你已经在使用 Metal 了
我们还与 Unity Unreal Engine 4 和 Lumberyard 等 引擎厂商紧密合作 以充分利用 Metal 的优势 如果你正在使用这些引擎之一 那么你已经赶上了速度
如果你已经建立了 自己的渲染器 那么 Metal 会给你带来很多优势 Metal 将 OpenGL 的图形 和 OpenCL 的计算结合成 一个统一的 API
它允许你在 App 中使用 多线程渲染
每当有繁复的 CPU 操作 需要执行时 我们都会确保这些操作 尽可能不频繁地发生 以减少 App 在执行期间的消耗
Metal 的着色语言 是基于 C++ 的 App 中使用的所有着色器 都可以预编译 例如 你可以更容易地拥有 各种材料的着色器 最后同样重要的是 我们在 Xcode 中 构建了一整套调试和优化工具
因此 一旦你将其移植到 Metal 你就拥有了使你的 App 更好运行的全部支持
让我们开始吧 在本会议中 我们将看看从 GL 迁移到 Metal 的步骤 并将典型的 GL App 与 Metal App 进行比较 作为概述 让我们快速浏览一下 GL App 的步骤 首先 你需要设立一个窗口 用于渲染 然后创建缓冲区 纹理和采样器等资源 你可以实现所有 用 GLSL 编写的着色器
在你可以在 GL 中 渲染任何东西之前 你需要创建特定的对象状态 比如 GL 程序 GL 帧缓冲对象 顶点数组对象
一旦你初始化了 渲染循环就开始了 你的资源就会开始绘制你的帧 对于每个帧 你要首先更新资源 绑定特定的帧缓冲区 设置图形状态并进行绘制调用
对每个帧缓冲区 都重复此过程 你可能有阴影贴图 光照通道 一些后期处理 可能有相当多的渲染通道 最后 呈现最终渲染出的图像十分简单
正如你所看到的 Metal 流程看起来非常相似 我们更新了一些原始的概念 并引入了一些新的东西 但总的来说 流程基本相同 它不是对引擎的完全重写 它以同样的方式工作
我们将重新引入新的概念 同时在 GL 和 Metal 之间 画出平行线 比较和对比这两个 API 以帮助你成功地进行转换 当你浏览 任何的图形教程时 首先要学习的是 如何创建和绘制窗口 让我们从窗口子系统开始 GL 和 Metal 都有这个概念 但是实现的方式略有不同 需要对 App 设置 并渲染一个绘图面 视图和视图委托 管理 API 和底层窗口系统 之间的接口 你可能正在使用这些框架 来管理 GL 视图 我们在 Metal 中 也有类似的框架
NSOpenGLView 和 GLKView 映射到 MTKView 如果你在你的 App 中 通过 EAGLLayer 使用 Core Animation 那么有一个等价的 CAMetalLayer 例如 假设你正在使用 GLKView
它有一个单一的入口点与提取率 你需要它来检查 目标的分辨率 自上一帧以来是否发生变化 根据需要 从渲染循环中 更新渲染目标的大小
在 MetalKit 中有一点更新 当 drawable 需要更改时 例如当你旋转屏幕 或调整窗口大小时 有一个单独的函数进行处理 所以你不需要检查 你的资源是否需要 在你的 draw 函数中重新分配 它致力于渲染代码
如果你需要额外的灵活性 我们提供 CAMetalLayer 你可以将其 用作视图的支持层
CAEAGLLayer 定义了 可绘制的属性 比如颜色格式 CAMetalLayer 允许设置 drawable 尺寸 像素格式 颜色空间等等 重要的是 CAMetalLayer 保有一个纹理库 你可以调用下一个 drawable 从而获取 drawable 来渲染你的帧 这是一个很重要的概念 我们会在需要的时候 再回顾一下 现在我们有一个窗口 接下来我们要介绍一些 Metal 中的新概念 命令队列 命令缓冲区 命令编码器 这些对象在 Metal 中协同工作 并向 GPU 提交工作 它们是新的 因为底层的 GL Context 为你管理着提交 GL 是一个隐式 API 这意味着没有代码告诉 GL 何时调度工作 作为开发者 你几乎无法控制 图形工作何时真正发生 比如何时编译着色器 何时分配资源存储 何时进行验证 或者何时将工作 真正地提交给 GPU GL Context 是一个 大型的状态机器 典型的工作流应该是这样的 你的 App 创建一个 GL Context 在线程上设置它 然后调用任意的 GL 注释 注释由底层的 上下文记录 并将在某个时间点执行 让我们仔细看看 到底发生了什么
假设你的 App 只是发送 GL 这些调用 一些状态变化 一些 draw 调用 在一个完美的场景中 上下文会将其 转换为 GPU 注释 来填充内部缓冲区 然后当它满了的时候 它会把它发送到 GPU 如果你插入一个 glFlush 来强制执行 你肯定知道它们就会在那个时候启动 但实际上 GPU 可以在任何时候提前开始执行
好的 例如 如果我们改变一个 引入每个依赖项的绘制调用 突然执行就会在那个时候启动 你可能会遭受大量的停滞
我们不得不再一次发问 应该什么时候提交工作呢 视情况而定 这就是 OpenGL 的缺点之一 性能不稳定 任何一个小小的改变都可能 迫使你偏离正轨 另一方面 Metal 是一个显式的 API 这意味着 App 可以决定 什么时候 向 GPU 发送什么工作 Metal 将 GL Context 的概念 分解为内部工作对象的集合 App 创建的第一个对象 是一个 metalDevice 对象 它是 GPU 的 抽象表示 然后 它创建一个名为 metalCommandQueue 的键对象 metalCommandQueue 通过分配要填充的命令缓冲区 来维护发送给 GPU 的 命令的顺序 而命令缓冲区只是一个 GPU 命令列表 你的 App 将填补发送给 GPU 执行 在我们刚刚学习的 GL 例子中 我们同样在 GL 中看到了 这个命令缓冲区的概念
让我们从现在开始 处理这个命令缓冲区 App 不会直接将命令 写入命令缓冲区 相反 它会创建一个 Metal 命令编码器 让我们看看主要的 三种编码器
我们使用的第一个编码器 将充满用于复制周围资源的 blit 命令 这个命令编码器将 API 代码 转换成 GPU 指令 然后将它们写入命令缓冲区 在一系列的命令被编码之后 例如 一系列的 blit 命令用于复制资源 你的 App 将结束编码 这将释放 encoder 对象
此外 Metal 还支持 一个计算编码器 用于进行通常在 OpenCL 中 已经完成的并行工作
你需要将一些内核放入队列 这些内核会被写入命令缓冲区 然后你运行编码器 并释放它
最后 让我们使用渲染编码器 执行你熟悉的渲染命令
你可以对状态更改和绘制调用进行排队 并结束编码器
这里我们有一个充满 不同工作负载的命令缓冲区 但是 GPU 还没有做任何工作 Metal 在 CPU 中 创建了对象并编码了命令 只有在 App 完成注释编码 并显式提交命令缓冲区之后 GPU 才开始工作 并执行这些命令
现在 我们已经编码了命令 让我们比较 GL 和 Metal 的命令提交
在 GL 中 没有对工作 何时提交给 GPU 的直接控制 你依赖于像 glFlush 和 glFinish 这样的字锤 来确保代码执行 glFlush 提交命令 并提交 CPU 线程 直到它们被调度 glFinish 提交 CPU 线程 直到 GPU 完全完成
在这些命令发生之前 仍然可以在任何时候提交工作 从而导致 潜在的停滞和减速 Metal 也有类似的功能 你仍然可以 显式地提交 并等待命令缓冲区的 调度或完成 但除非绝对需要 否则不建议 使用这些等待命令 相反 我们建议你 只需提交你的命令缓冲区 然后添加一个回调函数 以便稍后当命令缓冲区 在 GPU 上完成时 你的 App 可以得到通知 这将释放你的 CPU 来继续其他工作 现在 我们已经回顾了 命令队列 命令缓冲区 命令编码器 接下来我们讨论资源创建
任何图形 App 都可能使用三种 主要类型的资源 缓冲区 纹理 采样器 我们先来看看缓冲区
在 GL 中 你有一个缓冲区对象 和与其关联的内存 你使用的 API 代码 可以同时或分别修改对象状态内存 例如 这里可以使用 glBufferData 修改内存和对象的状态 稍后可以通过 调用 glBufferData 再次修改缓冲区维度 在这种情况下 旧对象及其内容将被 OpenGL 在内部丢弃 在 Metal 中 创建和填充缓冲区的 API 看起来非常相似 但主要的区别在于 生成的主题是不可变的 如果你在任何时候 需要调整缓冲区的大小 只需创建一个新的缓冲区 并丢弃旧的缓冲区
OpenGL 和 Metal 都有方法 来指示你 如何使用对象 然而在 GL 中 enum 只是一个 关于如何访问缓冲区对象中的 数据的使用提示 驱动程序使用该提示 来决定缓冲区的 定位内存的基础位置 但是没有对存储的直接控制 OpenGL 最终决定 在哪里存储对象
在 Metal 中 API 允许你指定映射到 特定内存分配行为的 存储模式
Metal 给予你控制 因为你最知道 你的对象将如何被使用 这在对象创建中 是一个很重要的概念 所以我们会在看完 纹理 API 之后马上回到这个内容
在 GL 中 每个纹理都有一个内部采样器对象 App 采样模式时 一个通常应用的采样器设置 但是你也可以选择 在纹理之外 创建一个单独的采样器对象
这里有一个 创建和绑定纹理 设置采样器 最后填充数据的例子 值得一提的是 GL 有很多 API 调用 来创建初始化的数据化纹理
它还具有 相同 API 的被命名资源版本
在管理采样器方面 还有更多的 API 这样的例子不胜枚举 Metal 的设计目标之一是 提供一个更简单的 API 能够保有所有的灵活性 在 Metal 中 纹理和采样器对象在创建后 总是分离和不可变的 为了创建纹理 我们创建一个描述符 设置各种属性来定义纹理尺寸 比如 pixelFormat 和尺寸等
同样 我们说过的一个重要属性是 storageMode 用于指定在内存中存储纹理的位置
最后 我们使用 这个描述符 来创建一个不可变的对象 以类似的方式 从 samplerDescriptor 开始 设置其属性 并创建不可变的 sampler 对象
非常简单
为了填充纹理的图像数据 我们计算每一行的字节数 就像我们在 OpenGL 中做的一样 我们指定要加载的区域 然后我们调用 texture replaceRegion 方法 该方法将数据从我们指定的指针 复制到纹理中
如果你加载了第一个纹理 你可能会发现 它是颠倒的
这是因为在 Metal 中 纹理坐标相对于 GL 在 Y 轴上是翻转的
值得一提的是 Metal API 不在底层 执行任何像素格式转换 所以你需要 使用你想要使用的格式 上传你的纹理
现在让我们回到存储模式 如前所述 在 GL 中 驱动程序必须 对你希望如何使用资源做出最佳猜测 作为开发者 你可以在某些情况下提供提示 比如何时创建缓冲区 或者通过为帧缓冲区附件 创建渲染缓冲区对象 但在所有这些例子中 这些仍然是提示 实现细节对你仍然是隐藏的 几分钟前 我们简要介绍了 Metal 附加的存储模式属性 你可以在纹理描述符 和创建缓冲区时设置它 让我们来看看这些的主要用例
最简单的选择是 使用共享存储模式 CPU 和 GPU 都将可以访问资源 对于缓冲区而言 这意味着你必须在这里 指向对象的内存支持 对于 iOS 上的纹理 这意味着你可以调用一些 简单易用的函数来设置和检索图像数据 你也可以使用一个 私有存储模式 它让 GPU 独占访问数据 它允许 Metal 应用一些优化 通常情况下 如果 CPU 能够访问数据 它是无法使用这些优化的 只有 GPU 可以 直接填充数据的内容 因此 你可以使用 来自使用共享存储的 第二个中间资源的 blitEncoder 来间接地填充来自 CPU 的数据 在具有专用视频内存的语音上 将资源设置为 使用私有存储 仅将其分配到视频内存中 单个副本 在 macOS 上 有一个托管存储模式 允许 CPU 和 GPU 访问对象的数据
在具有专用视频内存的系统上 Metal 可能需要 创建第二个镜像内存支持 以便两个进程都能有效地访问 因此 显式代码是必要的 以确保你的数据 被 CPU 和 GPU 同步访问 例如使用 didModifyRange
为了总结 我们回顾一下每种模式的一些典型用法
在 macOS 上 你将为静态素材和渲染目标 使用私有存储模式
你的小型动态缓冲区 可以使用共享存储模式 带有较小更新的较大缓冲区 将使用托管存储模式 在 iOS 上 静态数据和渲染目标 可以使用私有存储模式 由于我们的设备使用统一的内存 任何大小的动态数据 都可以使用共享存储模式 并且仍然可以获得很好的性能
接下来 让我们讨论一下 为图形 App 开发着色器 以及使用什么 API 来处理着色器 在 GL 中 进行着色器编译时 你必须创建一个 shader 对象 替换对象中的 ShaderSource 及时进行编译 并验证编译成功 虽然这种工作流有它的优点 但是你的 App 必须为每次编译所有着色器 付出性能的代价 Metal 实现其效率的 一个关键方法是 更早 更少地工作 在构建时 Xcode 将编译所有 Metal ShaderSource 文件 到一个默认的 Metal 库文件中 并将其放在 App 捆绑包中 以便在运行时检索 这消除了在运行时 编译大量代码的需要 并将 App 运行时的 编译时间缩短了一半 你所需要做的就是 从与 App 绑定的文件中 创建一个 Metal 库 并从中获取着色器函数
在 GL 中你使用 GLSL 它基于 C 编程语言
Metal 的着色语言 或 MSL 是基于 C++ 的 因此 对于大多数 GL 开发人员来说 它应该是相当熟悉的
它的 C++ 基础意味着 你可以创建类 模板和扩展 你可以定义枚举和名称空间
和 GLSL 一样 也有内置的向量 和矩阵类型 许多内置的函数 和操作用于图形 还有一些类 用于操作指定采样器状态的纹理
与 Metal 一样 MSL 也统一用于图形和计算
最后 由于着色器是预编译的 Xcode 能够提供错误 警告和指导 帮助你在构建时进行调试
让我们看一下 MSL 的实际代码 并将其与 GLSL 进行比较
我们将通过一个 简单的顶点着色器 顶部是 GLSL 底部是 MSL 让我们开始定义着色器 这些是原型 在 GLSL 中 void main() 着色器中没有指定 着色器阶段的内容 它完全由 传递到 glCreateShader 调用的 着色器类型决定
在 MSL 中 着色器阶段 在着色器代码中显式指定 这里的 vertex 限定符表示 它将对生成 完美示例的 每个顶点执行
在 GLSL 中 每个着色器入口点都必须被调用 main 并接受和返回 void 在 MSL 中 每个入口点 都有一个不同的名称 当你使用 Xcode 构建着色器时 编译器可以 在预处理阶段 解析 include 语句 就像解析普通 C++ 代码一样 在运行时 你可以根据预先编译的 Metal 库的不同名称查询函数 然后我们来谈谈输入
因为 GLSL 中的每个入口点 都是一个没有参数的主函数 所以所有输入 都作为全局参数传递 这适用于 vertex 属性 和统一变量
在 Metal 中 所有到阴影阶段的输入 都是入口函数的参数 双括号声明 C++ 属性 我们待会再看
这里的一个输入 是一个模型视图投影矩阵 在 OpenGL 中 为了将数据绑定到这些变量上 App 必须知道 C++ 代码中的 GLSL 名称 这使得着色器开发容易出错 在 MSL 中 统一绑定索引 由开发者 在着色器中显式控制 因此 App 可以直接绑定到特定的槽 在这个例子中 位置 1
这里的关键字常量 表示模型视图投影的意图 对所有顶点 都是一致的
着色器的另一个输入 是一组顶点属性 在 GLSL 中 通常使用单独的属性输入 这里的主要区别是 MSL 使用你自己设计的结构 阶段关键字表明 着色器的每次调用 都将收到自己的参数
在设置好着色器的 所有输入之后 你就可以执行所有的计算
至于输出 在 GLSL 中 输出被分割为 不同的属性 比如 glTexCoord 和预定义的变量 在本例中是 gl_Position 在 MSL 中顶点着色器输出 被合并到你自己的结构中
我们使用了 vertex VertexOutput 结构 让我们向上滚动 MSL 代码 看看它们到底是什么样子的
如前所述 GLSL 分别定义 输入顶点属性 Metal 允许你在结构中定义它们 在 MSL 中有一些 特殊的关键字顶点着色器输入 我们用一个 attribute 关键字 标记每个结构成分 并为其分配一个属性索引 与 GLSL 类似 这些索引在 Metal API 中 用于为顶点属性 分配顶点缓冲流
GLSL 预先定义了 一些特殊的关键字 比如 gl_Position 表示哪个变量包含了 用模型视图投影矩阵 转换过的顶点坐标
类似地 对于顶点输出 MSL 中的结构 特殊的关键字 position 表示顶点着色器输出位置 存储在该结构成分中
与 GLSL 向量类型类似 MSL 通过 simd.h 标头 定义了许多 simd 类型 可以在 CPU 和 GPU 代码之间共享
但是有几件事 你需要记住
缓冲区中的向量和矩阵类型 被对齐到 16 字节 或者为了获得一半的精度 对齐到 8 字节 因此它们不一定是压缩的 例如 float3 的大小 为 12 字节 但对齐为 16 字节 这是为了确保数据 被对齐到最佳的 CPU 和 GPU 访问
如果需要 你可以使用特定的支持格式 但是在使用它们之前 你需要在着色器中解压它们 我们刚刚回顾了 GLSL 和 MSL 之间的主要区别 为了使这个过渡 平稳且容易 我的同事 Max 将向你展示一个 非常酷的工具来帮助你轻松地完成它 谢谢大家
晚上好
Metal 不仅仅是一个 API 和一种着色语言 它还是一个功能强大的工具集合
我是 Max 我会尽量减少 你在移植到 Metal 的过程中所遇到的麻烦 让我们来看看这个场景 这是第一次 从一个老的 OpenGL 演示中调用绘制 我们在 Apple 这里将把它移植到 Metal 它绘制了一座寺庙 和一棵树的模型 两者都被四处的光源照亮 让我们一起移植片段着色器
我做的第一件事是 直接复制粘贴我所有的 旧 OpenGL 代码 到我的 Metal 着色器文件中
基于这点 我已经创建了输入结构 和函数原型
让我们开始吧
我们要做的就是 直接复制粘贴 主函数的内容 到 Metal 函数中
这里我们看到了 Metal 的第一个强大之处
因为着色器是预编译的 我们会立即得到错误 让我们仔细看看 当然 构建向量类型 现在有不同的名称 vec2 变成了 float2 vec3 变成了 float3 vec4 变成了 float4 我们很快就解决了这个问题
下一个错误是 我们所有的 输入结构 所有的全局变量 都来自于输入结构 因为我用了 类似的命名方案 这也很简单
当然 我们也要用同样的方法 修改 统一变量
下一个错误有点复杂 Metal 的取样是不同的 让我们来看一下
我们要从头开始 我们可以直接调用 colorMap 上的一个 sample 函数 这里我们可以看到 全自动完成是多么强大
这个函数要求我们 放入一个采样器和一个纹理坐标 我们已经有了纹理坐标
我们可以将采样器 作为参数传递给函数 或者在 Metal 中 我们可以像这样 在代码中声明一个采样器 我们需要对我们的 normalMap 做同样的事情
我们看到的 最后一个错误是 我们写进了很多 OpenGL 变量中的一个 我们将返回 最终计算得到的颜色
我们还可以看到 所有其他的函数 比如 normalize dot 点积 以及我最喜欢的函数 max 都是一样的
现在着色器编译成功 让我们试着运行一下
发生了一些错误 在 OpenGL 中 当你的着色器出现错误时 你通常会做的是 查看源代码 查看输出 然后认真思考 我们只需要使用 着色器调试器
轻点调试区域中的 小相机图标 将会捕获 GPU 跟踪 这是我们对每个 Metal API 调用的记录 现在我们可以导航到 draw 调用 我们在这绘制这棵树
我们在这绘制寺庙 让我长按下 寺庙的楼梯 打开像素检查器 它允许我们启动着色器调试器
我们现在看到的是 我们移植到一起的代码 和我们刚刚选择的 像素的每行值 让我们先看看我们的 colorMap 我们可以看到这看起来像一个合理的纹理 我们还可以看到楼梯 在这个纹理的上半部分 然而 如果我们看一下纹理坐标 我们就会发现 我们是从下半部分采样的 让我快速验证一下是不是这样
我们要做的是 反转纹理的 Y 坐标 现在我们可以更新着色器 看起来很合理 我们可以继续执行 你看 现在好多了 在从 OpenGL 移植到 Metal 时 这是一个非常常见的错误
当然 真正的修复是 你进入你的纹理加载代码 并确保你的纹理在正确的原点加载 这样你就不必在每个着色器中都做这个修复
然而 功能丰富的编辑器 和强大的调试工具的组合 最终都将帮助你 将游戏移植到 Metal
非常感谢 我的同事 Sarah 现在将引导你完成之后的幻灯片内容
谢谢你 Max 大家好 我是 Sarah Clawson 我将与你讨论 关于从 GL 移植到 Metal 的其他内容 目前为止 在一个图形 App 的生命中 我们已经进行了很多的设置 我们有一个用于渲染的窗口 一个将命令传达给 GPU 的方式 以及一组完备的 资源和着色器 接下来 我们将讨论 为渲染循环设置状态
OpenGL 在状态管理方面 有几个关键概念 顶点数组对象定义了 顶点属性布局 和顶点缓冲区 这个程序是顶点着色器 和片段着色器的链接组合 Framebuffer 是 App 打算渲染的 一组颜色 和深度模板附件
这些状态对象 是在初始化过程中创建的 并在整个帧中使用 让我们通过一个示例 来展示 OpenGL 如何管理状态
这里我们有一个示例渲染循环 其中 OpenGL App 绑定一个 Framebuffer 设置一个程序 然后进行其他状态修改 比如启用深度 人脸剔除 或在调用 draw 之前 更改颜色映射 如果你从 OpenGL 的角度 查看相同的 API 跟踪 它必须在每次 API 调用时 跟踪所有这些更改 当一个 draw 调用发生时 它必须停止并验证 以确保以前 对原始程序集 深度状态 光栅化器和可编程阶段的更改 都彼此兼容 这种验证可能非常繁重 虽然 OpenGL 确实试图 将其负面影响最小化 但这样做的成效是有限的
值得注意的是 OpenGL 状态对象 在首次引入时就走在了前面
Framebuffer 对象将 附加的渲染目标 程序链接片段 和顶点着色器组合在一起 顶点数组对象 则是更大的对象 结合了一些顶点属性 API 和顶点缓冲区设置 但是即使有了所有这些变化 尽管它们产生了积极的结果 OpenGL 仍然需要在 draw 调用中 验证许多东西 比如 glColorMask 能否帮助优化片段着色器 片段着色器输出是否 与附加的帧缓冲区兼容 顶点布局是否 与绑定程序兼容
或者附加的渲染目标 是可混合的吗 当我们重新设计 Metal 的 图形状态管理时 我们将程序着色器 与 VertexArray 对象的 顶点输入布局相结合 并添加关于 pixelFormat 附件 和混合状态的信息 然后将它们组合到一个 名为 PipelineDescriptor 的对象中 这个结构描述了 图形管道中的所有相关状态
为了设置描述符 你首先要初始化它 然后设置所有 我们刚才讲过的状态 比如顶点和片段着色器 顶点信息 像素格式 以及混合状态 然后使用描述符 创建管道状态对象 也就是 PSO 这个不可变对象 完全描述了渲染状态 它的伟大之处在于 你只需创建一次 验证它的正确性 然后在整个程序中使用它 用类似的方式 我们将所有深度和与模板相关的设置 合并到深度/模板状态描述符中 同样地 它是所有深度/模板状态的集合 使用这个描述符 你就可以创建一个 深度/模板状态对象
这个对象也是不可变的 并可在整个程序中使用
我们在 OpenGL 中 看到的渲染循环 现在在 Metal 中看起来是这样的 对于所有 预验证的状态对象 不再需要任何状态验证或跟踪
让我们来比较一下 在 Metal 中 渲染编码器是渲染通道的开始 类似于绑定帧缓冲区 你的深度状态已经 预置到对象中 只需在 renderEncoder 上设置它 pipelineState 对象 表示程序着色器 VertexArray 属性 和 pixelFormat 的组合 它也可在 renderEncoder 上设置 现在 renderEncoder 直接管理你的光栅化状态 这里需要注意的是 管道中 仍然具有灵活性 因为并不是所有内容 都被预先放入了 pipelineState 对象中 这是我们刚刚 讨论过的状态列表 你可以把它预置到你的 PSO 中 比如顶点 片段函数 像素格式 等等
另一方面 这是你在绘图时 仍然设置的所有状态
比如原始剔除模式和方向 填充模式 剪刀和视图区域 仍然像 OpenGL 一样设置 重要的是绘制调用保持不变 这里的主要区别是 你无需启用新状态 这可能导致 隐藏的验证成本 你只需要替换掉一个新的 pipelineState 对象 该对象在描述符中启用了混合
我还想讨论 另一种可能的优化 你可能在 OpenGL 中使用过它 来隐藏某些繁复的操作
作为 OpenGL 开发者 你可能已经看到 在进行了一系列状态更改之后的 第一次绘制调用时 渲染循环出现了意外的小问题 如果你遇到了这种情况 你可以使用一个 名为着色器预热的优化来隐藏它
在着色器预热中 App 为最常见的 GL 程序 使用虚拟绘制调用 以便 OpenGL 提前创建 所有必要的状态
如果你已经在 你的引擎中这样做了 那么你将会很容易 用 PSO 创建来替换它 现在 Metal 中的着色器预热 是通过在不同的启用状态 创建单独的 PSO 对象而完成的 首先 你需要创建描述符 然后设置直到第一个 绘制调用之前的所有状态 并创建第一个 pipelineState 对象 然后 你可以使用 相同的描述符 更改它的一些状态 就像这里我们启用了混合 然后创建第二个 PipelineState 对象
这两个都是预验证的 因此在绘制期间 你可以在绘制调用之间交换它们
如果你正在从 OpenGL 移植到 Metal 希望这是一个简单了当的更改 现在我们将结束 App 的设置阶段 我想提出一点将 App 从 OpenGL 移植到 Metal 的主要好处 那就是 这将开始减少 高执行成本操作的频率 在 OpenGL 中 你的 App 必须等到绘制时间 才能执行编译 链接着色器 或验证状态等操作 这意味着这些繁复的操作 在每一帧中都会发生很多次 一旦你将 App 移植到 Metal App 就会将这些操作 移动到其生命周期的不同阶段
使用预编译的着色器 着色器编译已经从初始化阶段 移到了构建阶段 因此它只需只执行一次
然后使用 PSO 将状态定义移动到内容加载 这样你就有足够的绘制时间 来调用 draw 函数 现在我们已经完成了 App 的设置阶段 让我们讨论一下如何使用所有这些 资源 着色器和对象来渲染帧
为了绘制一个单独的帧 你的 App 需要首先 更新纹理和缓冲区 然后建立一个 要渲染的渲染目标 然后在最终呈现你的工作之前 进行多次渲染
让我们讨论一下更新资源
通常来说 在整个渲染循环中 至少一部分资源需要不断更新
这样的例子有 着色器常量顶 顶点和索引缓冲区 以及纹理 这些修改可以通过 GPU 和 CPU 之间的同步 在帧之间完成 一个典型的 GL 资源更新 可以是 以下调用的任意组合 一个缓冲区可以由 CPU 更新 或者你也可以通过 GPU 凭借缓冲到缓冲的拷贝 来更新一个缓冲区
类似地 纹理可以由 CPU 更新 也可以通过 GPU 上的 纹理到纹理拷贝来更新 乍一看 Metal 提供了类似的功能 但是正如 Lionel 之前提到的 用于存储缓冲区和纹理的容器 是不可变的 并且是在初始化过程中创建的 但是 它们的内容 可以通过以下任意组合进行修改 具有共享或托管存储模式的缓冲区 可以通过其在 CPU 上的 contents 属性进行更新 在 GPU 上 blitEncoder 负责所有的数据复制 所以你可以通过 blitEncoder 上的 copyFromBuffer 方法 从 GPU 更新一个缓冲区
类似地 你可以通过 replaceRegion 方法 在 CPU 上更新具有共享 或托管存储模式的纹理
或者在 GPU 上 你可以通过 blitEncoder 上的 copyFromTexture 方法来更新纹理 值得注意的是 当涉及到这些更新时 存储模式很重要 因为只有具有共享或托管存储模式的 缓冲区和纹理可以由 CPU 更新
OpenGL 为你管理 GPU 和 CPU 之间的同步 尽管有时 在你的 App 等待一个 或另一个完成时 你的 App 会付出过高的代价
在 Metal 中 因为你可以控制内存的存储方式 所以还可以控制数据同步的方式和时间 这对于缓冲区和纹理都是适用的 如果你将 GL App 移植到 Metal 并且仅为资源更新 使用一个缓冲区 那么流程将会是这个样子 首先 CPU 将在 设置渲染通道期间更新资源 然后在完成之后 缓冲区 将被 GPU 在执行渲染通道期间消耗 然而 当 GPU 从这个缓冲区读取数据时 CPU 可能会开始设置 接下来的渲染通道 并需要更新相同的缓冲区 这是一个明显的竞争情况 我们来看一个解决这个问题的方法 一个简单的解决方案是 将这个资源提交给 GPU 并在它所使用的 commandBuffer 上 调用 waitUntilCompleted
正如我们前面讨论的 这类似于 glFinish 它在所有 CPU 工作上放置一个信号量 直到 GPU 完成执行 使用该缓冲区的渲染通道 执行完成后 从 GPU 接收回一个调用 通过这种方式 你可以确保 你的单个缓冲区 不会被 CPU 或 GPU 争夺
然而正如你所看到的 CPU 在 GPU 执行时是空闲的 GPU 在等待 CPU 提交工作时处于饥饿状态 因此 虽然在你开始 解决这些竞争情况时 这对你很有帮助 但不建议 使用 waitUntilCompleted 因为它会给程序 带来延迟 同步更新的一种有效方法是 根据 App 的需要 使用两个或多个缓冲区 这样 CPU 就可以 写一个缓冲区 而 GPU 可以从另一个缓冲区读取数据
让我们看一个简单的三重缓冲示例 这里我们从 第一个资源开始 准备让 GPU 使用 但是我们没有使用 waitUntilCompleted 而是添加了一个补全处理程序 这样一旦相应的帧 在 GPU 上完成 它就可以让 CPU 知道它已经完成了 但现在我们不需要等待它完成
当 GPU 执行时 三重缓冲的 CPU 可以跳过两次更新 因为它在不同的缓冲区
这是在 GPU 上 完成的帧 这是补全处理程序 运行的地方 它通知 GPU 的工作已经完成 然后将缓冲区 返回到缓冲池 以便在 GPU 继续执行时 下一帧的 CPU 可以使用它 我认为大多数开发者会发现 他们需要实现 三重缓冲 才能获得最佳性能 至于实现 当然 对于三重缓冲 你需要从一个包含三个缓冲区的队列开始
你还需要初始化 frameBorderySemaphore 初始值为 3 当 GPU 执行完毕时 这个信号量会在 每个帧边界处发出信号 让 CPU 知道 重写缓冲区是安全的 最后 我们需要 初始化缓冲区索引 以指向当前帧的缓冲区 在渲染循环内部 在写入缓冲区之前 我们需要确保 GPU 执行完成了相应的帧
所以在每次渲染通道的开始 我们需要等待 frameBoundarySemaphore 一旦接收到信号 我们就知道获取它的缓冲区 并将其用于新的帧数据 是安全的 现在我们对命令进行编码 并将这个资源绑定到 GPU 以便在下一帧中使用
但在提交之前 我们必须将完成处理程序 添加到 commandBuffer 中 然后提交它 一旦 GPU 完成执行 我们的完成处理程序 就会向帧信号量发出信号 让 CPU 知道 它已经完成了 可以重用缓冲区来进行下一帧的编码 这是一个简单的 三重缓冲区实现 你可以用于任何的动态资源更新 好的 现在我们已经更新了资源 让我们来谈谈渲染目标
在 OpenGL 中 Framebuffer 对象(FBO) 是渲染命令的目标
FBO 在一个伞下 收集许多纹理 和渲染缓冲对象 并在其中进行渲染 Framebuffer 的状态是可变的 通过绑定 Framebuffer 并最终将它们交换显示 渲染通道得以被松散地描述 这是一个典型的 带有 Framebuffer 的 OpenGL 工作流
在 App 的初始化阶段 一个 Framebuffer 被创建 然后通过绑定使它运转 然后你附加像纹理这样的资源 然后检查 Framebuffer 状态 以确保它是有效的
在绘制期间 你可以通过绑定 Framebuffer 来生成一个流 这是渲染通道的是隐式的开始 然后在对它调用绘制之前 你必须清除它 最后 你可以发出信号 表示可以丢弃某些附件 让 OpenGL 知道没有必要 将这些内容存储到内存中 这些丢弃事件可以作为 结束渲染通道的提示 但这不是一个保证
在 Metal 中 渲染命令编码器是渲染命令的目标 渲染命令编码器 是由渲染通道描述符创建的 与 FBO 类似 它为一个渲染通道 收集许多渲染目的地 并促进对它们的渲染
渲染命令编码器 直接负责 为 GPU 生成硬件命令 渲染通道 由编码器的开始和结束 显式地描述 这是一个 Metal 中的渲染通道
首先 你创建 RenderPassDescriptor RenderPassDescriptor 描述了所有附加的资源 还指定了 在渲染通道开始和结束时 发生的操作 这些操作称为 loadAction 和 storeAction 与 GL 相反 在 Metal 中 你不需要直接清除资源 相反地 你可以指定一个 loadAction 来清除它和颜色 这里 它是黑色的 这里的 storeAction 是 DontCare 类似于 GL 示例中的 GL 丢弃 Framebuffer
如果你希望将结果存储到内存中 你可以使用这里的 storeAction 在渲染时 你使用描述符来创建编码器 这样状态就设置好了 你进行所有的绘制调用 然后显式地结束编码 但是在丢弃 Framebuffer 或结束编码之前 让我们先绘制一些内容
一系列渲染命令 通常称为渲染通道
在渲染通道中 你可以设置状态并绘制调用输入 比如纹理和缓冲区 然后发出绘制命令
这是一个典型的 OpenGL 绘制序列
一个表现良好的 OpenGL App 试图提前设置它的所有状态 然后绑定它的目标 和一个 GL 程序来链接着色器 然后它将把 顶点缓冲区 统一变量 和纹理等资源绑定到程序的不同阶段
最后 它会开始绘制
正如我们之前讨论过的 OpenGL 状态更改 可能会导致隐藏的验证检查 如果你已经在 OpenGL 中 将状态更改 组合在一起 以避免这些性能问题 那么你将充分利用 Metal 的预验证状态对象
在 Metal 中 因为验证只在 创建 PipelineState 对象时发生 而且着色器是预编译的 所以渲染循环会变得更小 但是对于开发者来说 不需要做太多的更改
这和我们在 OpenGL 中看到的代码是一样的 但是现在是在 Metal 中
你从渲染命令编码器开始 这相当于设置 GL Framebuffer 然后设置预构建的 PipelineState 对象 它相当于 GL use 程序 在那之后 我们为我们的 Metal 程序分配资源 从 VertexBuffer 和统一变量开始 你可以在这里注意到 你必须为每个着色器阶段设置统一变量 而不是像 GL 中那样 为 GL 程序设置统一变量
在这里 因为我们是 直接从 OpenGL 移植过来的 所以我们发送的是同样的统一变量 但在 Metal 中 你可以按你的意愿发送不同的统一变量 然后设置你的纹理 并发出绘制调用 最后 一旦你完成了 所有的绘制调用 你就可以结束渲染通道了
虽然工作被提交了 却仍然有呈现的问题 当 GPU 渲染这个场景时 它会写出来给 Framebuffer 来显示
在 OpenGL 中为了呈现 渲染过的帧 当你从 drawInRect 返回时 上下文会为你调用 presetRenderBuffer
而 Metal 则直接通过 drawables 的 Core Animations 池来实现这一点 drawables 是用于屏幕显示的纹理 你还可以将渲染通道 编码为 drawables
获取当前 drawable 然后在渲染循环之后 告诉命令缓冲区显示它 还记得我们的代码吗 从刚开始 我们讲窗口子系统的时候 这里 我们将深入研究 glkView 和 drawInMTKView 看看如何呈现所渲染的内容 就是这样 你需要在 glkView 中 绑定 Framebuffer 执行渲染命令 然后当你从 drawInRect 返回时 显示就已经处理好了
在 Metal 中也是一样的 你创建 commandBuffer 通过创建结束编码器 来执行渲染命令 然后你必须执行的 一个额外步骤是 在最终提交 commandBuffer 之前 调用 presentDrawable 如果你的渲染循环非常简单 只有一个编码器 那么这就是全部你要做的 然而 如果你有一个 更复杂的 App 你可能需要查看 我们关于“如何处理你的 drawables 优化 Metal App 和游戏” 的演讲 这就是我们的框架 我们已经展示了如何轻松地 迁移窗口子系统 我们已经讨论了资源创建步骤 我们已经移植了着色器 并使用超棒的工具来快速找到问题 我们创建了渲染命令队列 命令缓冲区和命令编码器 来设置渲染通道 我们创建了预验证状态对象 然后为了渲染每个帧 我们使用三重缓冲来更新资源 我们在命令中 使用了渲染命令编码器 在最终呈现渲染帧之前 我们在渲染通道中 绘制了几何图形 我们已经通览了 图形 App 的生命历程 并展示了 Metal 是如何自然演变的 OpenGL 中的 许多已建立的概念 已经迁移到 Metal 中 并与我们 为解决图形社区中所提出的特定问题 而添加的新概念协同工作
如果你能从这个会议中学到一件事 我们希望你能知道 将你的 App 从 OpenGL 移植到 Metal 其实并不可怕 并且你的 App 将从中受益
但如果你还能够学一件事 那就是 Metal 还提供了 一套很棒的工具
来增强你的开发体验 Max 已经演示了 Xcode 的内置框架 捕获和着色器调试器 以便你更深入地了解 代码中的细微问题 Xcode 也提供了 新的 GPU 内存查看器 以理解和优化如何在你的 App 中使用内存
在 Instruments 中 我们有一个游戏性能模板 其中包括 Metal 系统跟踪 从而可视化地提交问题 这可能会导致帧率下降
今年我们还在模拟器中 添加了对 Metal 的支持
是的 你一定很兴奋 macOS Catalina 上的全新 Xcode 11 中 我们有完整的硬件加速 使你能够利用 Metal 运行 你的 iOS 游戏和 App 以及 tvOS 模拟器
模拟器支持 MTLGPUFamilyApple2 功能集 应该可以满足 在所有可用屏幕分辨率下 运行所有 App 和游戏的大部分需求 要更深入地了解模拟器 以及它是如何 实现硬件加速的 请在明天早上查看关于模拟器的会议 如果你想解决一个 关于 Metal 的特定问题 你可以在网上看到我们很多很多的会议内容
获取更多信息 你可以在我们的网站上查看我们的文档 或者明天上午到 Metal 实验室 访问我们
这就是今天的全部内容 谢谢大家的到来 我希望能在派对上见到你们 [掌声]
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。