View in English

  • 打开菜单 关闭菜单
  • Apple Developer
搜索
关闭搜索
  • Apple Developer
  • 新闻
  • 探索
  • 设计
  • 开发
  • 分发
  • 支持
  • 账户
在“”范围内搜索。

快捷链接

5 快捷链接

视频

打开菜单 关闭菜单
  • 专题
  • 相关主题
  • 所有视频
  • 关于

更多视频

  • 简介
  • 概要
  • 转写文稿
  • 代码
  • 深入探索 Metal 4 游戏

    深入了解 Metal 4 的最新改进。我们将介绍全新光线追踪功能,助你将复杂度极高且视觉效果丰富的工作负载成功移植到 Apple 芯片上。了解 MetalFX 如何通过渲染画质提升、帧插值和场景去噪来扩展工作负载。 为了充分从这个讲座中获益,我们建议你先观看“探索 Metal 4”和“探索 Metal 4 游戏”。

    章节

    • 0:00 - 简介
    • 2:13 - 渲染画质提升
    • 7:17 - 帧插值
    • 13:50 - 利用 Metal 4 提升光线追踪效果
    • 19:25 - 同时实现去噪和画质提升
    • 26:08 - 后续步骤

    资源

      • 高清视频
      • 标清视频

    相关视频

    WWDC25

    • 实现 Metal 4 机器学习与图形应用程序的完美融合
    • 探索 Metal 4
    • 探索 Metal 4 游戏
    • 用于打造沉浸式 App 的 Metal 渲染的新功能
    • 让游戏更上一层楼

    WWDC23

    • Metal 光线追踪指南

    WWDC22

    • 利用 MetalFX Upscaling 提升性能
  • 搜索此视频…

    大家好 我叫 Matias Koskela 今天我将介绍一些技术和最佳实践 帮助你进一步提升 Apple 平台上的 高级游戏和专业 App

    在观看这个讲座之前 建议你先观看“探索 Metal 4” 以简要了解 Metal 4 并观看“探索 Metal 4 游戏” 了解如何使用最新版本的 Metal 这个“深入探索”讲座是我们 Metal 4 游戏系列的第二部分 你还可以观看另一个讲座 了解 Metal 4 如何实现机器学习 与图形技术的完美融合

    像《赛博朋克 2077》这样的游戏 通过高质量渲染变得更加逼真 这使得每个像素的处理成本更高 也使实现高分辨率和高帧率 更具有挑战性 借助 Metal 你可以 在 Apple 全平台上渲染高质量帧 无论是 iPhone 还是 Mac 如果你使用光栅化或光线追踪等技术 Metal 为你提供了易于使用的 API

    你可以使用 MetalFX Upscaling 放大工作负载 以获得更高的分辨率和帧率

    如果你想更进一步 现在还可以使用 新的 MetalFX 帧插值

    最新的游戏 如《赛博朋克 2077》 提供非常逼真的实时路径追踪 实时渲染的这些扩展能力 可以通过 Metal 4 中的新功能实现 这包括光线追踪增强功能 和全新的 MetalFX 去噪优化放大器 它通过减少游戏中所需的 光线数量来轻松进行放大

    MetalFX Upscaler 可以帮助你实现 更高的分辨率和更快的帧率 你可以使用新的 MetalFX 帧插值 让游戏运行更加流畅 新的 Metal 4 光线追踪功能 可以进一步提升性能 还可以与新的 MetalFX 去噪优化放大器相结合

    优化放大是一种广泛使用的技术 在大多数场景下都能帮助提升性能 MetalFX 拥有一个 基于机器学习技术的优化放大器 自 2022 年以来一直是 Apple 平台的一部分 而且每年都在迭代优化

    MetalFX Upscaling 现在包含 新的工具和技术 你可以利用它们来提高 游戏的质量和性能 第一步是对游戏正确应用 时序优化放大 这个过程要求 正确设置曝光输入参数 然后 你可以通过动态分辨率 进一步提升性能 还可以在特定场景中使用 响应式提示来提高质量

    想象一个典型的渲染管道 首先对帧进行光栅化或光线追踪

    然后游戏执行后处理效果 例如运动模糊 接着 它应用曝光和色调映射 然后渲染 UI 最后 向玩家显示帧

    添加 MetalFX Upscaling 的理想位置是在抖动渲染之后 以及后处理效果之前 你可以观看“利用 MetalFX Upscaling 提升性能” 详细了解如何集成优化放大器 今年 你将获得更多工具和功能 来提升游戏性能

    在优化放大器上设置正确的曝光值 对于获得高质量的结果至关重要

    如果传入的值严重错误 可能会导致画面闪烁和鬼影

    在渲染管道中 优化放大器的输入和输出颜色 处于线性色彩空间中 优化放大器接受 一个名为 exposure 的参数 这个参数与颜色输入相乘后 产生的亮度应该 与色调映射中使用的曝光 大致匹配

    这可确保优化放大器 在向玩家显示帧时 能够理解帧的可见特征 请注意 这个值只是 优化放大器的一个提示 不会改变输出的亮度 MetalFX 包含一个新工具 可帮助你调整 发送到优化放大器的曝光输入值

    它被称为曝光调试器 要启用它 请设置环境变量 MTLFX_EXPOSURE_TOOL_ENABLED 现在 优化放大器会在帧的上层 显示一个灰色棋盘格 并对它应用曝光值的逆值

    然后 你可以在管线的最后阶段 查看显示屏上的图案效果

    如果传递给优化放大器的曝光值 与色调映射器不匹配 棋盘格会显得过暗或过亮

    不匹配的另一个迹象是 游戏运行时 棋盘格的亮度发生变化

    当曝光值正确时 网格图案呈现不变的中灰色

    由于游戏的复杂程度 在不同场景间可能变化很大 许多游戏采用了动态分辨率渲染

    当帧变复杂时 优化放大器的输入分辨率会降低 当复杂程度更高时 游戏会动态地 进一步降低输入分辨率 MetalFX 时域优化放大器 现在支持动态调整大小的输入 而不需要你每帧都传入 相同大小的输入 为了获得最佳的放大质量 如果不是必要情况 游戏的最大放大比例 不应该设置为 2 倍以上

    MetalFX 时域优化放大器中 还新增了一项可选功能 可向优化放大器提示像素响应性

    当游戏渲染透明效果 或烟花之类的粒子时 不会将它们渲染为运动和深度纹理

    在高放大比和低输入分辨率下 你可能会发现这些粒子 有点融入背景

    或者可能出现鬼影 这是因为在渲染中 它们会显得 就像纹理细节或镜面高光一样

    为了让你能够控制粒子的处理方式 优化放大器现在接受 一个新的可选输入 称为响应式遮罩 这个遮罩允许你 标记这些效果覆盖的区域

    要使用它 在着色器中设置一个响应式遮罩值 例如 基于 G 缓冲区中的材质类型 在主机代码中 在编码之前 将纹理绑定到 时域优化放大器对象

    响应式遮罩的适用场景仅限于 无法提高输入分辨率的情况 另外 也不要使用针对另一个 优化放大器调整的响应式遮罩 因为它可能会遮蔽 在 MetalFX 优化放大器输出中 看起来已经足够好的区域 使用优化放大器可以获得 出色的性能和卓越的质量 但有时你希望达到更高的刷新率 今年 MetalFX 在所有 Apple 平台中 引入了帧插值

    MetalFX 帧插值很容易集成到游戏中 首先要设置一个插值器对象 将 UI 渲染到插值帧中 并正确呈现帧和控制帧节奏

    帧插值可帮助你使用 已经渲染的像素 实现流畅的游戏体验

    这是相同的渲染管道 这次没有 UI 渲染

    在色调映射步骤之后插入帧 注意 要获得更高的分辨率和帧率 可以在同一管道中进行 优化放大和帧插值

    要使用 MetalFX 帧插值器 App 需要提供两个渲染帧、 运动向量和深度 如果你已采用优化放大器 可以使用相同的运动向量和深度 运动纹理中的对象有颜色 因为它们向右移动了 有了这些输入 MetalFX 在这两个渲染帧之间 生成一个帧

    要设置插值器 以获得更高的组合性能 向插值器描述符提供优化放大对象 创建插值器时 定义它的运动缩放系数和深度约定 然后将五个必需的纹理绑定到插值器

    开始获得一些插值帧后 就该考虑 UI 渲染了

    在典型的渲染管道中 游戏通常在每一帧的末尾渲染 UI 也就是应该发生帧插值的位置

    将 Alpha 混合元素渲染到帧里的 UI 可能包含每个帧变化文本 并且不修改运动或深度纹理

    启用帧插值时 可以通过多种方法 实现美观的 UI

    使用帧插值渲染 UI 最常用的方法有三种 包括合成式 UI、 离屏 UI 和逐帧 UI

    在合成式 UI 中 插值器获取带 UI 的 前一帧 N - 1 和当前帧 N 以及不带 UI 的当前帧 N 合成式 UI 方法最容易使用 在这个模式下 帧插值器可以看到 带 UI 和不带 UI 的纹理之间的差异 这样 它就可以尝试移除 UI 并将它放在插值帧中的正确位置 但取消混合已经混合的像素 没有办法做到完美 因此 你可以使用其他两个选项之一 来帮助插值器

    比如离屏 UI UI 被渲染成完全独立的 UI 纹理

    然后 插值器将它叠加到插值帧上 将它输入到插值器可以省去 一次额外的载入和存储 因为插值器可以将 UI 写入输出

    最后 在逐帧 UI 中 UI 由代码处理 这可能需要 你进行最大的代码更改 但采用这种方法时 你也可以更新 插值帧的 UI 从而为玩家带来最流畅的体验

    现在 插值帧上也有了美观的 UI 接下来需要考虑 如何以正确的顺序和间隔显示 插值帧和原生渲染帧

    通常 游戏渲染由渲染线程、 GPU 和呈现线程组成 渲染线程为 GPU 和呈现 设置必要的工作 当渲染某一帧时 插值器可以生成一个时间戳介于 刚渲染的帧和前一帧之间的帧 然后游戏可以呈现这个插值帧 在一个呈现间隔后 游戏可以显示 最新渲染的帧

    要保持这个间隔的长度始终如一 并不是一件容易的事 但要让游戏流畅运行 这是必不可少的

    新的 Metal HUD 是一个很棒的工具 可以帮助你识别混乱的节奏 请观看“让游戏更上一层楼”讲座 详细了解如何启用这个工具 以及它提供的所有出色新功能

    看一下“帧间隔”图 其中横轴是时间 纵轴是帧间隔长度

    如果图表显示不规则的模式 并且指示更长的帧更新间隔的峰值 看起来是随机的 则表明节奏不稳定

    还有一种判断节奏不稳定的方法是 有两个以上的帧间隔直方图桶

    节奏稳定后 如果满足目标显示刷新率 应该会看到一条平坦的线 如果低于目标刷新率 则会看到有规律的重复模式 并且最多有两个直方图桶

    下面是一个使用 方便的 presentHelper 类 来正确实现的示例 在绘制循环期间 所有内容 都渲染为低分辨率纹理 并通过 MetalFX Upscaler 优化放大 在告诉帮助程序 UI 渲染开始后 UI 就会被渲染 最后 presentHelper 类 处理插值器调用 请查看示例代码了解实现的细节

    除了节奏之外 正确设置增量时间 和相机参数也很重要 如果存在错误参数 遮挡区域可能会出现伪影

    而正确的参数 可以使遮挡区域完美对齐

    这是因为插值器 现在可以调整运动向量 以匹配真实模拟运动的长度

    在正确设置所有输入和节奏后 插值帧应该看起来很棒 而且插值输入 应该具有相当高的帧率 尝试在插值之前至少达到每秒 30 帧

    优化放大器和帧插值器 是可以普遍使用的技术 可以放大几乎所有渲染样式 相比之下 光线追踪通常用于 更高端的渲染场景 Metal 4 围绕加速结构构建 和相交函数 增加了许多新的光线追踪功能

    越来越多的游戏在 Apple 平台上 使用 Metal 光线追踪

    在这个演示中 光照非常逼真 可以清晰看到无人机在地板上的倒影 光线追踪技术 和复杂程度因游戏而异

    这需要相交函数管理 具有更高的灵活性 以及加速结构构建有更多选项

    Metal 4 引入了新功能 来帮助简化这两项工作

    要了解 Metal 光线追踪的基础知识 例如构建加速结构和相交函数 请观看“Metal 光线追踪技术指南”

    这个示例游戏对一个青草环绕树木 的简单场景进行光线追踪

    在这个简单的场景中 已经存在多种材质类型 比如经过 Alpha 测试的草和树叶 以及不透明的树干

    因此需要许多不同的 光线追踪相交函数 分别用于主光线和阴影光线

    相交函数缓冲区是一个参数缓冲区 包含场景相交函数的句柄

    例如 草和树叶可能需要类似的功能 来追踪主光线 相交函数缓冲区可让游戏 轻松拥有多个指向 同一相交函数的条目

    设置相交函数缓冲区索引需要 在实例级别设置状态 这个示例场景有两个实例 以及在几何形状级别设置状态 草只有一个几何形状 树有两个几何形状 相交器需要知道 对于照射到树干的阴影光线 应使用哪个相交函数

    在创建实例加速结构时 在每个实例描述符上指定 intersectionFunctionTableOffset

    在构建原始加速结构时 也在几何形状描述符上设置 intersectionFunctionTableOffset

    在着色器中设置相交器时 将 intersection_function_buffer 添加到它的标签中

    接下来在相交器上设置几何形状乘数 乘数是相交函数缓冲区中 光线类型的数量

    在我们的示例中 每个几何形状有两种光线类型 因此这里的正确值是 2 在这两种光线类型中 你需要为要追踪的光线类型 提供基准索引 在这个示例中 追踪主光线的基准索引为 0

    追踪阴影的基准 ID 为 1

    当树干的实例和几何形状贡献、 几何形状乘数和阴影光线类型 的基准 ID 组合起来时 指针最终会落在所需的相交函数中

    最后 将相交函数缓冲区参数 传递给 intersect 方法 以完成代码

    通过指定缓冲区、缓冲区大小和步长 与你在其他 API 中熟悉的方法相比 这为你提供了一些额外的灵活性 如果要从 DirectX 移植 你可以轻松地将着色器绑定表 移植到 Metal 相交函数缓冲区

    在 DirectX 中 创建描述符来调度光线时 你可以在主机上 设置相交函数缓冲区地址和步长 在 Metal 中 可以在着色器中进行设置 SIMD 组中的所有线程 都应设置相同的值 否则行为是未定义的

    光线类型索引和几何形状乘数 在 DirectX 和 Metal 中的 处理方式相同 App 可以在着色器中设置它们 在 DirectX 和 Metal 中 创建实例加速结构时 可以设置每个实例的实例偏移索引 虽然几何形状偏移索引 在 DirectX 中自动生成 但 Metal 允许你灵活地 自行设置这个几何形状偏移

    相交函数缓冲区极大地改善了 光线追踪游戏的 Metal 移植体验 启动并运行后 Metal 4 还使你能够 优化 Metal 构建加速结构的方式

    Metal 已经为你提供了 对加速结构构建的 很大控制权 除了默认行为 你可以针对加速结构更新进行优化 启用更大的场景 或更快地构建加速结构 今年 你可以获得更大的灵活性 并且可以首选快速相交 以减少追踪光线所需的时间

    或者 也可以选择最小化 加速结构的内存使用量

    可以对单个加速结构构建 设置使用标志 并且不必对于所有加速结构都一样

    新的加速结构标志 使渲染管道的光线追踪部分 更贴合你的需求 如果你将它用于随机效果 将需要一个去噪器 现在 MetalFX Upscaler 也可以 包含去噪功能

    实时光线追踪的应用越来越广泛 既有较为简单的混合光线追踪 也有更复杂的路径追踪 在这个示例图片中 光线追踪使一切更加真实

    并显著改善了反射效果 通过使用去噪和更少的光线 可以在光线追踪中实现 最佳的质量和性能平衡

    借助新的 MetalFX API 将优化放大和去噪相结合 只需要添加几个额外的输入 但你可以通过 帮助去噪优化放大器、 添加额外的输入和正确设置细节 来进一步提升质量

    在组合优化放大器和去噪器之前 我们来看看这些步骤 传统上是如何完成的

    典型的实时和交互式 光线追踪渲染管道 分别追踪多个效果 分别对它们进行去噪 并将结果合成 一个无噪点的抖动纹理 继而由 MetalFX 时域优化放大器 进行优化放大 然后进行后处理

    传统的去噪器需要为每个场景 进行单独的艺术参数调整 这里可以看到一些没有艺术调整参数 的去噪器的效果 相比之下 使用 MetalFX 去噪优化放大器 则无需调整参数 它在主渲染之后、后处理之前应用 MetalFX 中基于机器学习的技术 在许多场景中提供了 强大、高性能和高质量的 去噪和优化放大 并且更容易集成 要集成去噪优化放大器 集成优化放大器是一个很好的起点 这里可以看到优化放大器的输入 颜色、运动和深度 新的组合 API 是 Upscaler API 的超集

    对于新的 API 我们需要 添加额外的无噪点辅助缓冲区 如左侧所示 其中大部分可能是你的 App 已有的 接下来我将逐个介绍

    第一个新输入是法线 为了获得最佳结果 这些法线应该在世界空间中

    然后是漫反射反照率 这是材质漫反射辐射的基础色

    接下来是粗糙度 它表示 表面的光滑程度或粗糙程度 是一个线性值 最后一个输入是镜面反照率 这是渲染的镜面辐射亮度的 无噪点近似值 应该包括菲涅尔组件 在代码中添加这些新输入很简单

    创建一个典型的时域优化放大器 只需要大约 10 行代码 要启用去噪版本 你需要更改优化放大器类型 并添加额外纹理的类型

    同样 在对优化放大器进行编码时 这将是优化放大器调用 唯一的区别是 你需要绑定额外的输入纹理

    设置好去噪器的基本用法后 你可以通过使用一些可选输入 和避免一些典型的集成陷阱 来改进它

    有一些可选的输入纹理 可用于提高质量

    首先是镜面反射命中距离 表示从像素主可见性点 到二次反弹点的光线长度 然后是去噪器强度遮罩 可用于标记不需要去噪的区域 最后是透明度叠加层 它基于 Alpha 通道 用于混合仅优化放大而未去噪的颜色

    最典型的集成问题是 输入包含太多噪点 要解决这个问题 你应该使用所有标准的路径追踪 采样改进 比如下一事件估计、重要性采样技术 而在具有许多光源的更大场景中 主要对实际对这个区域有贡献的 光源进行采样

    与光线追踪采样质量相关的 另一个因素是 相关随机数 你不应该使用相关性太强的 随机数生成器 空间和时间相关性都可能导致伪影

    与辅助数据相关的 一个潜在陷阱是 金属材质的漫反射反照率 在这个例子中 棋子是金属的 因此在镜面反照率中具有颜色 这种情况下 棋子的漫反射率反照率 应该更暗

    最后 还有一些 与法线相关的常见陷阱 MetalFX 去噪优化放大器期望法线 在世界空间中 以做出更好的去噪决策 你需要使用具有符号位的 纹理数据类型 否则质量可能不够理想 具体取决于相机的方向

    在正确处理所有这些细节后 你应该会得到漂亮的 去噪和优化放大帧

    我们来看看将所有这些功能 放入单个渲染器时会发生什么

    我的同事们制作了一个演示 使用了我前面提到的渲染管道 演示使用全新的 Metal 4 光线追踪功能 来优化渲染的光线追踪部分 它使用 MetalFX 去噪优化放大器 同时进行去噪和优化放大 经过曝光和色调映射之后 帧由 MetalFX 帧插值器进行插值

    这个演示使用先进的光线追踪光效 例如全局光照、 反射、阴影和环境光遮挡 栩栩如生地呈现了 两个机器人下棋的场景

    在右上角的视图中 你可以看到没有进行任何 MetalFX 处理之前的渲染 其他视图中是其他 MetalFX 输入

    我们采用了 MetalFX 去噪优化放大器 和帧插值器 去噪器还免除了 对最终效果的所有手动调整 从而大大简化了渲染

    如果你已经集成了 MetalFX 优化放大器 现在正是升级到帧插值的好时机 如果你刚开始接触 MetalFX 建议先了解优化放大器 然后确保光线追踪效果 采用了最佳实践 比如今天介绍的相交函数缓冲区 并使用去噪优化放大器 减少游戏的光线预算

    我迫不及待地想看到这些新功能 在你的游戏中发挥作用 以及你利用 Metal 4 创建出的作品 感谢观看!

    • 6:46 - Reactive Mask

      // Create reactive mask setup in shader
      out.reactivity = m_material_id == eRain ? (m_material_id == eSpark ? 1.0f : 0.0f) : 0.8f;
      
      // Set reactive mask before encoding upscaler on host
      temporalUpscaler.reactiveMask = reactiveMaskTexture;
    • 8:35 - MetalFX Frame Interpolator

      // Create and configure the interpolator descriptor
      MTLFXFrameInterpolatorDescriptor* desc = [MTLFXFrameInterpolatorDescriptor new];
      desc.scaler = temporalScaler;
      // ...
      
      // Create the effect and configure your effect
      id<MTLFXFrameInterpolator> interpolator = [desc newFrameInterpolatorWithDevice:device];
      interpolator.motionVectorScaleX = mvecScaleX;
      interpolator.motionVectorScaleY = mvecScaleY;
      interpolator.depthReversed = YES;
      
      // Set input textures
      interpolator.colorTexture = colorTexture;
      interpolator.prevColorTexture = prevColorTexture;
      interpolator.depthTexture = depthTexture;
      interpolator.motionTexture = motionTexture;
      interpolator.outputTexture = outputTexture;
    • 12:45 - Interpolator present helper class

      #include <thread>
      #include <mutex>
      #include <sys/event.h>
      #include <mach/mach_time.h>
      
      
      class PresentThread
      {
          int m_timerQueue;
          std::thread m_encodingThread, m_pacingThread;
          std::mutex m_mutex;
          std::condition_variable m_scheduleCV, m_threadCV, m_pacingCV;
          float m_minDuration;
          
          uint32_t m_width, m_height;
          MTLPixelFormat m_pixelFormat;
          
          const static uint32_t kNumBuffers = 3;
          uint32_t m_bufferIndex, m_inputIndex;
          bool m_renderingUI, m_presentsPending;
          
          CAMetalLayer *m_metalLayer;
          id<MTLCommandQueue> m_presentQueue;
      
          id<MTLEvent> m_event;
          id<MTLSharedEvent> m_paceEvent, m_paceEvent2;
          uint64_t m_eventValue;
          uint32_t m_paceCount;
          
          int32_t m_numQueued, m_framesInFlight;
          
          id<MTLTexture> m_backBuffers[kNumBuffers];
          id<MTLTexture> m_interpolationOutputs[kNumBuffers];
          id<MTLTexture> m_interpolationInputs[2];
          id<MTLRenderPipelineState> m_copyPipeline;
          
          std::function<void(id<MTLRenderCommandEncoder>)> m_uiCallback = nullptr;
          
          void PresentThreadFunction();
          void PacingThreadFunction();
          
          void CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label);
      
      public:
          
          PresentThread(float minDuration, CAMetalLayer *metalLayer);
          ~PresentThread()
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              m_numQueued = -1;
              m_threadCV.notify_one();
              m_encodingThread.join();
          }
          void StartFrame(id<MTLCommandBuffer> commandBuffer)
          {
              [commandBuffer encodeWaitForEvent:m_event value:m_eventValue++];
          }
      
          void StartUI(id<MTLCommandBuffer> commandBuffer)
          {
              assert(m_uiCallback == nullptr);
              if(!m_renderingUI)
              {
                  CopyTexture(commandBuffer, m_interpolationInputs[m_inputIndex], m_backBuffers[m_bufferIndex], @"Copy HUDLESS");
                  m_renderingUI = true;
              }
          }
          
          void Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue);
          
          id<MTLTexture> GetBackBuffer()
          {
              return m_backBuffers[m_bufferIndex];
          }
      
          void Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat);
          
          void DrainPendingPresents()
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              while(m_presentsPending)
                  m_scheduleCV.wait(lock);
          }
          
          bool UICallbackEnabled() const
          {
              return m_uiCallback != nullptr;
          }
          
          void SetUICallback(std::function<void(id<MTLRenderCommandEncoder>)> callback)
          {
              m_uiCallback = callback;
          }
          
      };
      
      PresentThread::PresentThread(float minDuration, CAMetalLayer *metalLayer)
          : m_encodingThread(&PresentThread::PresentThreadFunction, this)
          , m_pacingThread(&PresentThread::PacingThreadFunction, this)
          , m_minDuration(minDuration)
          , m_numQueued(0)
          , m_metalLayer(metalLayer)
          , m_inputIndex(0u)
          , m_bufferIndex(0u)
          , m_renderingUI(false)
          , m_presentsPending(false)
          , m_framesInFlight(0)
          , m_paceCount(0)
          , m_eventValue(0)
      {
          id<MTLDevice> device = metalLayer.device;
          m_presentQueue = [device newCommandQueue];
          m_presentQueue.label = @"presentQ";
          m_timerQueue = kqueue();
          
          metalLayer.maximumDrawableCount = 3;
          
          Resize(metalLayer.drawableSize.width, metalLayer.drawableSize.height, metalLayer.pixelFormat);
          
          m_event = [device newEvent];
          m_paceEvent = [device newSharedEvent];
      	m_paceEvent2 = [device newSharedEvent];
      }
      
      
      void PresentThread::Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue)
      {
          id<MTLCommandBuffer> commandBuffer = [queue commandBuffer];
          
          if(m_renderingUI)
          {
              frameInterpolator.colorTexture = m_interpolationInputs[m_inputIndex];
              frameInterpolator.prevColorTexture = m_interpolationInputs[m_inputIndex^1];
              frameInterpolator.uiTexture = m_backBuffers[m_bufferIndex];
          }
          else
          {
              frameInterpolator.colorTexture = m_backBuffers[m_bufferIndex];
              frameInterpolator.prevColorTexture = m_backBuffers[(m_bufferIndex + kNumBuffers - 1) % kNumBuffers];
              frameInterpolator.uiTexture = nullptr;
          }
          
          frameInterpolator.outputTexture = m_interpolationOutputs[m_bufferIndex];
      
          [frameInterpolator encodeToCommandBuffer:commandBuffer];
          [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) {
              std::unique_lock<std::mutex> lock(m_mutex);
              m_framesInFlight--;
              m_scheduleCV.notify_one();
              m_paceCount++;
              m_pacingCV.notify_one();
          }];
          [commandBuffer encodeSignalEvent:m_event value:m_eventValue++];
          [commandBuffer commit];
      
          std::unique_lock<std::mutex> lock(m_mutex);
          m_framesInFlight++;
          m_numQueued++;
          m_presentsPending = true;
          m_threadCV.notify_one();
          while((m_framesInFlight >= 2) || (m_numQueued >= 2))
              m_scheduleCV.wait(lock);
      
          m_bufferIndex = (m_bufferIndex + 1) % kNumBuffers;
          m_inputIndex = m_inputIndex^1u;
          m_renderingUI = false;
      }
      
      void PresentThread::CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label)
      {
          MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor new];
          desc.colorAttachments[0].texture = dest;
          desc.colorAttachments[0].loadAction = MTLLoadActionDontCare;
          desc.colorAttachments[0].storeAction = MTLStoreActionStore;
          id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:desc];
          [renderEncoder setFragmentTexture:src atIndex:0];
          [renderEncoder setRenderPipelineState:m_copyPipeline];
          [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
          if(m_uiCallback)
              m_uiCallback(renderEncoder);
          renderEncoder.label = label;
          [renderEncoder endEncoding];
      }
      
      
      void PresentThread::PacingThreadFunction()
      {
          NSThread *thread = [NSThread currentThread];
          [thread setName:@"PacingThread"];
          [thread setQualityOfService:NSQualityOfServiceUserInteractive];
          [thread setThreadPriority:1.f];
          
          mach_timebase_info_data_t info;
          mach_timebase_info(&info);
          
          // maximum delta (0.1ms) in machtime units
          const uint64_t maxDeltaInNanoSecs = 100000000;
          const uint64_t maxDelta = maxDeltaInNanoSecs * info.denom / info.numer;
          
          uint64_t time = mach_absolute_time();
          
          uint64_t paceEventValue = 0;
          
          for(;;)
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              while(m_paceCount == 0)
                  m_pacingCV.wait(lock);
              m_paceCount--;
              lock.unlock();
              
              // we get signal...
              const uint64_t prevTime = time;
              time = mach_absolute_time();
      		m_paceEvent.signaledValue = ++paceEventValue;
      
              const uint64_t delta = std::min(time - prevTime, maxDelta);
              const uint64_t timeStamp = time + ((delta*31)>>6);
              
              struct kevent64_s timerEvent, eventOut;
              struct timespec timeout;
              timeout.tv_nsec = maxDeltaInNanoSecs;
              timeout.tv_sec = 0;
              EV_SET64(&timerEvent,
                       0,
                       EVFILT_TIMER,
                       EV_ADD | EV_ONESHOT | EV_ENABLE,
                       NOTE_CRITICAL | NOTE_LEEWAY | NOTE_MACHTIME | NOTE_ABSOLUTE,
                       timeStamp,
                       0,
                       0,
                       0);
              
              kevent64(m_timerQueue, &timerEvent, 1, &eventOut, 1, 0, &timeout);
              
              // main screen turn on...
              m_paceEvent2.signaledValue = ++paceEventValue;
          }
      }
      
      
      void PresentThread::PresentThreadFunction()
      {
          NSThread *thread = [NSThread currentThread];
          [thread setName:@"PresentThread"];
          [thread setQualityOfService:NSQualityOfServiceUserInteractive];
          [thread setThreadPriority:1.f];
          
      
          uint64_t eventValue = 0;
          uint32_t bufferIndex = 0;
      
          uint64_t paceEventValue = 0;
      
          for(;;)
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              
              if(m_numQueued == 0)
              {
                  m_presentsPending = false;
                  m_scheduleCV.notify_one();
              }
              
              while(m_numQueued == 0)
                  m_threadCV.wait(lock);
              
              if(m_numQueued < 0)
                  break;
              lock.unlock();
      
              @autoreleasepool
              {
                  id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable];
      
      			lock.lock();
      			m_numQueued--;
      			m_scheduleCV.notify_one();
      			lock.unlock();
      
                  id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer];
                  [commandBuffer encodeWaitForEvent:m_event value:++eventValue];
                  CopyTexture(commandBuffer, drawable.texture, m_interpolationOutputs[bufferIndex], @"Copy Interpolated");
                  [commandBuffer encodeSignalEvent:m_event value:++eventValue];
      			[commandBuffer encodeWaitForEvent:m_paceEvent value:++paceEventValue];
      
                  if(m_minDuration > 0.f)
                      [commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration];
                  else
                      [commandBuffer presentDrawable:drawable];
                  [commandBuffer commit];
              }
              
              @autoreleasepool
              {
                  id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer];
                  id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable];
                  CopyTexture(commandBuffer, drawable.texture, m_backBuffers[bufferIndex], @"Copy Rendered");
      			[commandBuffer encodeWaitForEvent:m_paceEvent2 value:++paceEventValue];
                  if(m_minDuration > 0.f)
                      [commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration];
                  else
                      [commandBuffer presentDrawable:drawable];
                  [commandBuffer commit];
              }
              
              bufferIndex = (bufferIndex + 1) % kNumBuffers;
          }
      }
      
      void PresentThread::Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat)
      {
          if((m_width != width) || (m_height != height) || (m_pixelFormat != pixelFormat))
          {
              id<MTLDevice> device = m_metalLayer.device;
      
              if(m_pixelFormat != pixelFormat)
              {
                  id<MTLLibrary> lib = [device newDefaultLibrary];
                  MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new];
                  pipelineDesc.vertexFunction = [lib newFunctionWithName:@"FSQ_VS_V4T2"];
                  pipelineDesc.fragmentFunction = [lib newFunctionWithName:@"FSQ_simpleCopy"];
                  pipelineDesc.colorAttachments[0].pixelFormat = pixelFormat;
                  m_copyPipeline = [device newRenderPipelineStateWithDescriptor:pipelineDesc error:nil];
                  m_pixelFormat = pixelFormat;
              }
              
              DrainPendingPresents();
              
              m_width = width;
      		m_height = height;
              
              MTLTextureDescriptor *texDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixelFormat width:width height:height mipmapped:NO];
      		texDesc.storageMode = MTLStorageModePrivate;
              for(uint32_t i = 0; i < kNumBuffers; i++)
              {
                  texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageShaderWrite|MTLTextureUsageRenderTarget;
                  m_backBuffers[i] = [device newTextureWithDescriptor:texDesc];
                  texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget;
                  m_interpolationOutputs[i] = [device newTextureWithDescriptor:texDesc];
              }
              texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget;
              m_interpolationInputs[0] = [device newTextureWithDescriptor:texDesc];
              m_interpolationInputs[1] = [device newTextureWithDescriptor:texDesc];
      
          }
      }
    • 13:00 - Set intersection function table offset

      // Set intersection function table offset on host-side geometry descriptors
      NSMutableArray<MTLAccelerationStructureGeometryDescriptor *> *geomDescs ...;
      for (auto g = 0; g < geomList.size(); ++g)
      {
          MTLAccelerationStructureGeometryDescriptor *descriptor = ...;
          descriptor.intersectionFunctionTableOffset = g;
          ...
          [geomDescs addObject:descriptor];
      }
    • 13:01 - Set up the intersector

      // Set up the intersector
      metal::raytracing::intersector<intersection_function_buffer, instancing, triangle> trace;
      trace.set_geometry_multiplier(2); // Number of ray types, defaults to 1
      trace.set_base_id(1);             // Set ray type index, defaults to 0
    • 13:02 - Ray trace intersection function buffers

      // Ray trace intersection function buffers
      
      // Set up intersection function buffer arguments
      intersection_function_buffer_arguments ifb_arguments;
      ifb_arguments.intersection_function_buffer = raytracingResources.ifbBuffer;
      ifb_arguments.intersection_function_buffer_size = raytracingResources.ifbBufferSize;
      ifb_arguments.intersection_function_stride = raytracingResources.ifbBufferStride;
      
      // Set up the ray and finish intersecting
      metal::raytracing::ray r = { origin, direction };
      auto result = trace.intersect(r, ads, ifb_arguments);
    • 13:02 - Change of temporal scaler setup to denoised temporal scaler setup

      // Change of temporal scaler setup to denoised temporal scaler setup
      
      MTLFXTemporalScalerDescriptor* desc = [MTLFXTemporalScalerDescriptor new];
      desc.colorTextureFormat = MTLPixelFormatBGRA8Unorm_sRGB;
      desc.outputTextureFormat = MTLPixelFormatBGRA8Unorm_sRGB;
      desc.depthTextureFormat = DepthStencilFormat;
      desc.motionTextureFormat = MotionVectorFormat;
      
      desc.diffuseAlbedoTextureFormat = DiffuseAlbedoFormat;
      desc.specularAlbedoTextureFormat = SpecularAlbedoFormat;
      desc.normalTextureFormat = NormalVectorFormat;
      desc.roughnessTextureFormat = RoughnessFormat;
      
      desc.inputWidth = _mainViewWidth;
      desc.inputHeight = _mainViewHeight;
      desc.outputWidth = _screenWidth;
      desc.outputHeight = _screenHeight;
      temporalScaler = [desc newTemporalDenoisedScalerWithDevice:_device];
    • 13:04 - Change temporal scaler encode to denoiser temporal scaler encode

      // Change temporal scaler encode to denoiser temporal scaler encode
      
      temporalScaler.colorTexture = _mainView;
      temporalScaler.motionTexture = _motionTexture;
      
      temporalScaler.diffuseAlbedoTexture = _diffuseAlbedoTexture;
      temporalScaler.specularAlbedoTexture = _specularAlbedoTexture;
      temporalScaler.normalTexture = _normalTexture;
      temporalScaler.roughnessTexture = _roughnessTexture;
      
      temporalScaler.depthTexture = _depthTexture;
      temporalScaler.jitterOffsetX = _pixelJitter.x;
      temporalScaler.jitterOffsetY = -_pixelJitter.y;
      temporalScaler.outputTexture = _upscaledColorTarget;
      temporalScaler.motionVectorScaleX = (float)_motionTexture.width;
      temporalScaler.motionVectorScaleY = (float)_motionTexture.height;
      [temporalScaler encodeToCommandBuffer:commandBuffer];
    • 16:04 - Creating instance descriptors for instance acceleration structure

      // Creating instance descriptors for instance acceleration structure
      MTLAccelerationStructureInstanceDescriptor *grassInstanceDesc, *treeInstanceDesc = . . .;
      grassInstanceDesc.intersectionFunctionTableOffset = 0;
      treeInstanceDesc.intersectionFunctionTableOffset  = 1;
      
      // Create buffer for instance descriptors of as many trees/grass instances the scene holds
      id <MTLBuffer> instanceDescs = . . .;
      for (auto i = 0; i < scene.instances.size(); ++i)
      . . .
    • 0:00 - 简介
    • 了解 Metal 4 游戏系列,包括在 Apple 平台上开发高级游戏和专业 App 的先进技术和最佳实践。Metal 4 的 API 支持跨 Apple 设备扩展工作负载,以获得更高的分辨率和帧率。

    • 2:13 - 渲染画质提升
    • MetalFX 采用基于机器学习的优化放大器,帮助实现更高的分辨率和更快的帧率。今年新增的 MetalFX 时域优化放大器支持动态调整大小的输入,因此在渲染复杂帧时会动态降低输入分辨率。你可选用响应式提示功能,向优化放大器提示透明特效或粒子区域的像素响应性,从而获得更清晰的画面效果。还可以使用新的曝光调试器工具验证传入优化放大器的曝光值是否准确。

    • 7:17 - 帧插值
    • 今年新增的 MetalFX Frame Interpolation 可在两个渲染帧之间生成一个帧。针对 UI 渲染场景,有多种帧插值技术。在同时呈现插值帧和原生渲染帧时,也需要考虑诸多因素。

    • 13:50 - 利用 Metal 4 提升光线追踪效果
    • Metal 4 新增了许多与加速结构构建和相交函数相关的光线追踪功能。如果从其他 API 迁移,则可以轻松将着色器绑定表移植到 Metal 相交函数缓冲区。在启动并运行后,还可以优化 Metal 4 构建加速结构的方式。

    • 19:25 - 同时实现去噪和画质提升
    • 在光线追踪场景时,通过去噪技术配合较少光线投射可实现画质与性能的最佳平衡。新的 MetalFX API 通过将去噪功能直接集成到升频流程中,显著增强了实时光线追踪渲染管道的工作效率,简化了传统方案中追踪、去噪与合成的分步操作。只需指定法线、漫反射反照率、粗糙度与镜面反照率等无噪辅助缓冲区,即可获得稳定、高性能、高质量的渲染结果,无需针对不同场景单独调整参数。

    • 26:08 - 后续步骤
    • 若已集成 MetalFX 优化放大器,现在正是升级到帧插值的好时机。如果刚开始接触 MetalFX,建议先了解优化放大器。然后确保光线追踪效果使用相交函数缓冲区与去噪优化放大器协同工作。

Developer Footer

  • 视频
  • WWDC25
  • 深入探索 Metal 4 游戏
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • App 扩展
    • App Store
    • 音频与视频 (英文)
    • 增强现实
    • 设计
    • 分发
    • 教育
    • 字体 (英文)
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 机器学习与 AI
    • 开源资源 (英文)
    • 安全性
    • Safari 浏览器与网页 (英文)
    打开菜单 关闭菜单
    • 完整文档 (英文)
    • 部分主题文档 (简体中文)
    • 教程
    • 下载 (英文)
    • 论坛 (英文)
    • 视频
    打开菜单 关闭菜单
    • 支持文档
    • 联系我们
    • 错误报告
    • 系统状态 (英文)
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件 (英文)
    • 反馈助理
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program (英文)
    • News Partner Program (英文)
    • Video Partner Program (英文)
    • 安全赏金计划 (英文)
    • Security Research Device Program (英文)
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖 (英文)
    • Apple 设计大奖
    • Apple Developer Academies (英文)
    • WWDC
    获取 Apple Developer App。
    版权所有 © 2025 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则