Apple - Metal Programming Guide

本文翻译整理自:Metal Programming Guide(更新日期:2016-12-12
https://developer.apple.com/library/archive/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40014221


文章目录

  • 一、关于 Metal 和本指南
    • 1、概览
    • 2、依赖条件
    • 3、也可以看看
  • 二、Metal 基本概念
  • 三、指挥组织与执行模型
    • 1、设备对象代表 GPU
    • 2、Metal 中的瞬态和非瞬态物体
    • 3、命令队列
    • 4、命令缓冲区
      • 创建命令缓冲区
      • 执行命令
      • 注册用于命令缓冲区执行的处理程序块
      • 监控命令缓冲区执行状态
    • 命令编码器
      • 创建命令编码器对象
      • 渲染命令编码器
      • 计算命令编码器
      • Blit 命令编码器
      • 多线程、命令缓冲区和命令编码器
  • 四、资源对象:缓冲区和纹理
    • 1、缓冲区是无类型的内存分配
      • 创建缓冲区对象
      • 缓冲方法
    • 2、纹理是格式化的图像数据
      • 创建纹理对象
        • 使用纹理描述符创建纹理对象
        • 使用纹理切片
        • 使用便捷方法创建纹理描述符
        • 将图像数据复制到纹理或从纹理中复制图像数据
      • 纹理的像素格式
    • 3、为纹理查找创建采样器状态对象
    • 4、保持 CPU 和 GPU 内存之间的一致性
  • 五、函数和库
    • 1、MTLFunction 表示着色器或计算函数
    • 2、库是函数的存储库
      • 从编译代码创建库
      • 从源代码创建库
      • 从库中获取函数
    • 3、在运行时确定函数详细信息
  • 六、图形渲染:渲染命令编码器
    • 1、创建并使用渲染命令编码器
      • 创建渲染过程描述符
        • 加载和存储操作
        • 指定清除加载操作
        • 示例:使用加载和存储操作创建渲染过程描述符
        • 示例:为多重采样渲染创建渲染过程描述符
      • 使用渲染过程描述符创建渲染命令编码器
    • 2、使用 Core Animation 显示渲染内容
    • 3、创建渲染管道状态
      • 创建和配置渲染管道描述符
      • 从描述符创建渲染管道状态
      • 在渲染管道附件描述符中配置混合
        • 了解混合因子和操作
        • 使用自定义混合配置
    • 4、为渲染命令编码器指定资源
      • 数据组织的顶点描述符
    • 5、执行固定功能渲染命令编码器操作
      • 使用视口和像素坐标系
      • 执行深度和模板操作
    • 6、绘制几何基元
    • 7、结束渲染过程
    • 8、代码示例:绘制三角形
    • 9、使用多线程编码单个渲染过程
  • 七、数据并行计算处理:计算命令编码器
    • 1、创建计算管道状态
    • 2、为计算命令编码器 指定计算状态和资源
    • 3、执行计算命令
    • 4、代码示例:执行数据并行函数
  • 八、缓冲区和纹理操作:Blit 命令编码器
    • 1、在资源对象之间复制 GPU 内存中的数据
      • 在两个缓冲区之间复制数据
      • 将数据从缓冲区复制到纹理
      • 在两个纹理之间复制数据
      • 将数据从纹理复制到缓冲区
    • 2、生成 Mipmap
    • 3、填充缓冲区的内容
    • 4、结束 Blit 命令编码器的编码
  • 九、Metal 工具
    • 1、在应用程序构建过程中创建库
      • 使用 Xcode 构建库
      • 使用命令行实用程序构建库
    • 2、Xcode Scheme 设置和性能
    • 3、调试
      • Metal 着色语言源文件的文件扩展名
      • 使用 Xcode 执行帧捕获
      • Label 属性
      • Metal System Trace
  • 十、Metal 功能集表
  • 十一、iOS 9 和 OS X 10.11 的新功能
    • 1、功能集
    • 2、设备选择
    • 3、资源存储模式和设备内存模型
      • 共享
      • 私人的
      • 管理
      • 选择资源存储模式
      • 设置和查询资源存储模式
    • 4、纹理
      • 压缩纹理
      • PVRTC Blit 操作
      • 深度/模板渲染目标
      • 立方体阵列纹理
      • 纹理使用
      • 详细纹理视图
      • IOSurface 支持
    • 5、渲染添加
      • 渲染命令编码器
      • 分层渲染
    • 6、计算附加项
    • 7、支持框架
      • Metal 套件
      • Metal 性能着色器
  • 十二、iOS 10、tvOS 10 和 macOS 10.12 中的新功能
    • 1、新的 Metal 功能集
    • 2、Tessellation
    • 3、资源堆
    • 4、无记忆渲染目标
      • 用例
      • 规则和限制
    • 5、函数专业化
      • 声明函数常量
      • 设置常量值
      • 编译专用函数
      • 获取反射数据
    • 6、函数资源读写
      • 函数缓冲区读写
        • 原子函数
      • 函数纹理读写
        • 读写纹理
          • Access
          • 同步
          • 像素格式
          • 1 级像素格式
          • 第 2 层像素格式
      • 规则和限制
        • 内存屏障
        • 片段函数
    • 7、纹理数组
    • 8、模板纹理视图
    • 9、深度 16 像素格式
    • 10、扩展范围像素格式
    • 11、组合 MSAA 存储和解析操作
    • 12、延迟存储操作
    • 13、双源混合
      • 产生第二种源颜色
      • 引用第二个源颜色
      • 规则和限制
    • 14、MSAA Blits
    • 15、sRGB 写入
    • 16、附加着色语言功能
      • 整数函数
      • 纹理函数
      • 计算函数
      • 采样器限定符
      • 缓冲区、纹理和采样器的结构
      • 便利常量
    • 17、行为变化
  • 十三、Tessellation
    • 1、Metal 镶嵌管道
      • 计算内核
      • Tessellator
        • 曲面细分器基元生成
      • 细分后顶点函数
    • 2、每个面片的曲面细分因子
      • 理解四边形补丁
        • 解释 MTLQuadTessellationFactorsHalf 结构
      • 理解三角形面片
        • 解释 MTLTriangleTessellationFactorsHalf 结构
      • 补丁丢弃规则
      • 指定每个面片曲面细分因子缓冲区
    • 3、补丁功能
      • 创建计算内核
      • 创建后细分顶点函数
        • 细分后顶点函数输入
        • 细分后顶点函数输出
    • 4、曲面细分管道状态
      • 构建计算管道
      • 构建渲染管道
    • 5、补丁绘制调用
      • 绘制镶嵌面片
    • 6、示例代码
    • 7、将 DirectX 11 样式曲面细分着色器移植到 Metal
  • 十四、资源堆
    • 1、堆
      • 创建堆
      • 二次分配堆资源
    • 2、Fences
      • 创建栅栏
      • 跨 Blit 和计算命令编码器跟踪栅栏
      • 跨渲染命令编码器跟踪围栏
      • 栅栏示例
    • 3、最佳实践
      • 根据渲染目标类型使用单独的堆
      • 为可别名和不可别名资源分配单独的堆
      • 分离堆以减少碎片
      • 最小化围栏
      • 考虑跟踪非堆资源
    • 4、示例代码


一、关于 Metal 和本指南

Metal 框架 支持 GPU加速的高级 3D 图形渲染 和数据并行计算工作负载。
Metal 提供了 现代且精简的 API,用于对图形和计算命令的组织、处理和提交进行细粒度的低级控制,以及对这些命令的相关数据和资源进行管理。
Metal 的主要目标是 最大限度地减少执行 GPU 工作负载所产生的 CPU 开销。


1、概览

本文档介绍了 Metal 的基本概念:命令提交模型、内存管理模型以及图形着色器和数据并行计算功能的独立编译代码的使用。
然后,该文档详细介绍了如何使用 Metal API 编写应用程序。

您可以在以下章节中找到更多详细信息:

  • 基本 Metal 概念简要描述了 Metal 的主要特性。
  • 命令组织和执行模型解释了如何创建并提交命令给 GPU 执行。
  • 资源对象:缓冲区和纹理讨论设备内存的管理,包括代表 GPU 内存分配的缓冲区和纹理对象。
  • 函数和库描述了 Metal 着色语言代码如何在 Metal 应用程序中表示,以及如何将 Metal 着色语言代码加载到 GPU 上并由 GPU 执行。
  • 图形渲染:渲染命令编码器描述如何渲染 3D 图形,包括如何在多个线程之间分配图形操作。
  • 数据并行计算处理:计算命令编码器解释如何执行数据并行处理。
  • 缓冲区和纹理操作:Blit 命令编码器描述如何在纹理和缓冲区之间复制数据。
  • Metal Tools列出了可用的工具来帮助您定制和改进您的开发工作流程。
  • Metal 功能集表列出了每个 Metal 功能集的功能可用性、实现限制和像素格式功能。
  • iOS 9 和 OS X 10.11 中的新功能功能总结了 iOS 9 和 OS X 10.11 中引入的新功能。
  • iOS 10、tvOS 10 和 OS X 10.12 中的新功能新功能总结了 iOS 10、tvOS 10 和 OS X 10.12 中引入的新功能。
  • 曲面细分描述了用于细分面片的 Metal 曲面细分管道,包括使用计算内核、曲面细分器和后曲面细分顶点函数。
  • 资源堆描述了如何从堆中子分配资源、在它们之间使用别名以及如何用围栏跟踪它们。

2、依赖条件

您应该熟悉 Objective-C 语言,并具有使用 OpenGL、OpenCL 或类似 API 进行编程的经验。


3、也可以看看

Metal*框架参考*是描述 Metal 框架中接口的文档集合。

Metal着色语言规范是一份指定 Metal 着色语言的文档,用于编写图形着色器或 Metal 应用程序使用的计算函数。

此外,Apple 开发者库中还提供了一些使用 Metal 的示例代码项目。


二、Metal 基本概念

Metal 为图形和数据并行计算工作负载提供了统一的编程接口和语言。
Metal 让您能够更高效地集成图形和计算任务,而无需使用单独的 API 和着色器语言。

Metal 框架提供以下内容:

  • 低开销接口。
    Metal 旨在消除“隐藏的”性能瓶颈,例如隐式状态验证。
    您可以控制 GPU 的异步行为,以实现用于并行创建和提交命令缓冲区的高效多线程。
    有关 Metal 命令提交的详细信息,请参阅 命令组织和执行模型。
  • 内存和资源管理。
    Metal 框架描述了表示 GPU 内存分配的缓冲区和纹理对象。
    纹理对象具有特定的像素格式,可用于纹理图像或附件。
    有关 Metal 内存对象的详细信息,请参阅 资源对象:缓冲区和纹理。
  • 集成对图形和计算操作的支持。
    Metal对图形和计算操作使用相同的数据结构和资源(例如缓冲区、纹理和命令队列)。
    此外,Metal 着色语言支持图形和计算功能。
    Metal 框架支持在运行时接口、图形着色器和计算功能之间共享资源。
    有关编写使用 Metal 进行图形渲染或数据并行计算操作的应用程序的详细信息,请参阅 图形渲染:渲染命令编码器 或 数据并行计算处理:计算命令编码器。
  • 预编译着色器。
    Metal着色器可以在构建时与应用代码一起编译,然后在运行时加载。
    此工作流程提供更好的代码生成以及更轻松的着色器代码调试。
    (Metal 还支持着色器代码的运行时编译。)
    有关从 Metal 框架代码使用 Metal 着色器的详细信息,请参阅 函数和库。
    有关 Metal 着色语言本身的详细信息,请参阅Metal 着色语言指南

Metal 应用程序无法在后台执行 Metal 命令,尝试这样做的 Metal 应用程序将被终止。


三、指挥组织与执行模型

在 Metal 架构中,MTLDevice协议定义了代表单个 GPU 的接口。
MTLDevice协议支持查询设备属性、创建其他设备特定对象(如缓冲区和纹理)以及编码和排队要提交给 GPU 执行的渲染和计算命令的方法。

命令队列由**命令缓冲区队列组成,命令队列组织这些命令缓冲区的执行顺序。
命令缓冲区包含用于在特定设备上执行的编码命令。
命令编码将渲染、计算和位块传送命令附加到命令缓冲区上,这些命令缓冲区最终被提交到设备上执行。

MTLCommandQueue协议定义了命令队列的接口,主要支持创建命令缓冲区对象的方法。
MTLCommandBuffer协议定义了命令缓冲区的接口,并提供了创建命令编码器、将命令缓冲区排队以进行执行、检查状态和其他操作的方法。
MTLCommandBuffer协议支持以下命令编码器类型,它们是用于将不同类型的 GPU 工作负载编码到命令缓冲区的接口:

  • MTLRenderCommandEncoder协议对单次渲染过程的图形(3D)渲染命令进行编码。
  • MTLComputeCommandEncoder协议对数据并行计算工作负载进行编码。
  • MTLBlitCommandEncoder协议对缓冲区和纹理之间的简单复制操作以及 mipmap 生成等实用操作进行编码。

在任何时间点,只能有一个命令编码器 处于活动状态,并将命令附加到命令缓冲区中。
必须先结束每个命令编码器,然后才能创建另一个命令编码器以用于同一命令缓冲区。
“每个命令缓冲区一个活动命令编码器”规则的一个例外是MTLParallelRenderCommandEncoder协议,详见如使用多个线程编码单个渲染通道。

完成所有编码后,即可提交MTLCommandBuffer对象本身,这将标记命令缓冲区已准备好由 GPU 执行。
MTLCommandQueue协议控制 提交的MTLCommandBuffer对象中的命令何时执行(相对于命令队列中已有的其他MTLCommandBuffer对象)。

图 2-1显示了命令队列、命令缓冲区和命令编码器对象之间的密切关系。
图表顶部的每一列组件(缓冲区、纹理、采样器、深度和模板状态、管道状态)代表特定于特定命令编码器的资源和状态。


图 2-1 Metal 对象关系
在这里插入图片描述


1、设备对象代表 GPU

MTLDevice对象 表示可以执行命令的 GPU。
MTLDevice协议 具有创建新命令队列、从内存分配缓冲区、创建纹理以及查询设备功能的方法。
要获取系统上的首选系统设备,请调用MTLCreateSystemDefaultDevice函数。


2、Metal 中的瞬态和非瞬态物体

Metal 中的某些对象被设计为瞬时且极其轻量级的,而其他对象则更昂贵且可以持续很长时间,或许是应用程序的整个生命周期。

命令缓冲区和命令编码器对象是临时的,设计为一次性使用。
它们的分配和释放成本非常低,因此它们的创建方法会返回自动释放的对象。

以下对象不是瞬态的。
请在性能敏感的代码中重用这些对象,并避免重复创建它们。

  • 命令队列
  • 数据缓冲区
  • 纹理
  • 采样器状态
  • 图书馆
  • 计算状态
  • 渲染管道状态
  • 深度/模板状态

3、命令队列

命令队列接受 GPU 将执行的命令缓冲区的有序列表。
发送到单个队列的所有命令缓冲区都保证按照命令缓冲区入队的顺序执行。
一般来说,命令队列是线程安全的,并允许同时编码多个活动命令缓冲区。

要创建命令队列,请调用newCommandQueue方法或 MTLDevice对象的 newCommandQueueWithMaxCommandBufferCount: 方法。
一般来说,命令队列应该长期存在,因此不应重复创建和销毁它们。


4、命令缓冲区

命令缓冲区会存储已编码的命令,直到缓冲区被提交给 GPU 执行。
单个命令缓冲区可以包含许多不同类型的已编码命令,具体取决于用于构建它的编码器的数量和类型。
在典型的应用中,整个渲染帧都会被编码到单个命令缓冲区中,即使渲染该帧涉及多个渲染过程、计算处理函数或位块传输操作。

命令缓冲区是临时一次性对象,不支持重复使用。
一旦命令缓冲区提交执行,唯一有效的操作就是等待命令缓冲区被调度或完成(通过同步调用或处理程序块(在注册命令缓冲区执行的处理程序块中讨论))并检查命令缓冲区执行的状态。

命令缓冲区还代表应用程序唯一可独立跟踪的工作单元,它们定义了由 Metal 内存模型建立的一致性边界,如资源对象:缓冲区和纹理中所述。


创建命令缓冲区

要创建MTLCommandBuffer对象,请调用 MTLCommandQueuecommandBuffer 方法。
MTLCommandBuffer对象只能提交给 创建它的MTLCommandQueue对象。

通过 commandBuffer 方法创建的命令缓冲区 会保留执行所需的数据。
在某些情况下,您会在对象执行期间 将这些MTLCommandBuffer对象保留在其他地方,此时您可以通过调用 MTLCommandQueuecommandBufferWithUnretainedReferences 方法来创建命令缓冲区。
仅对性能极为关键的应用,使用commandBufferWithUnretainedReferences方法,这些应用可以保证关键对象在命令缓冲区执行完成之前在应用中的其他地方具有引用。
否则,不再具有其他引用的对象可能会被过早释放,并且命令缓冲区执行的结果未定义。


执行命令

MTLCommandBuffer 协议 使用以下方法建立命令队列中命令缓冲区的执行顺序。
命令缓冲区在提交之前不会开始执行。
提交后,命令缓冲区将按照其入队的顺序执行。

  • enqueue方法在命令队列中为命令缓冲区预留一个位置,但不会提交命令缓冲区以供执行。
    当最终提交此命令缓冲区时,它将在相关命令队列中任何先前排队的命令缓冲区之后执行。
  • commit方法会尽快执行命令缓冲区,但在同一命令队列中之前已入队的命令缓冲区提交之后执行。
    如果命令缓冲区之前未入队,则commit进行隐式enqueue调用。

有关使用多个线程 使用enqueue的示例,请参阅 多线程、命令缓冲区和命令编码器。


注册用于命令缓冲区执行的处理程序块

下面列出的MTLCommandBuffer方法用于监控命令的执行。
计划处理程序和已完成处理程序在未定义的线程上按执行顺序调用。
您在这些处理程序中执行的任何代码都应快速完成;如果需要完成昂贵或阻塞的工作,请将该工作推迟到另一个线程。

  • addScheduledHandler:方法 注册一个代码块,在命令缓冲区被调度时调用。
    当系统中其他对象或其他 API 提交的工作之间的任何依赖关系得到满足时,命令缓冲区即被视为 已调度MTLCommandBuffer
    您可以为一个命令缓冲区注册多个已调度的处理程序。
  • waitUntilScheduled方法 在命令缓冲区被调度,并且addScheduledHandler:方法注册的所有处理程序完成后同步等待并返回。
  • addCompletedHandler:方法 注册一个代码块,在设备完成命令缓冲区的执行后立即调用。
    您可以为一个命令缓冲区注册多个已完成的处理程序。
  • waitUntilCompleted方法 同步等待,并在设备完成命令缓冲区的执行并且addCompletedHandler:方法 注册的所有处理程序都返回后返回。

presentDrawable: 方法是一种已完成处理程序的特殊情况。
此便捷方法在调度命令缓冲区时显示可显示资源(CAMetalDrawable对象)的内容。
有关presentDrawable:方法的详细信息,请参阅 与 Core Animation 集成:CAMetalLayer。


监控命令缓冲区执行状态

该只读status属性 包含一个列在 Command Buffer Status Codes 中的 MTLCommandBufferStatus枚举值,该值反映了此命令缓冲区 生命周期中 的当前调度阶段。

如果执行成功完成,则只读error属性的值为nil
如果执行失败,则status设置为MTLCommandBufferStatusError,并且error属性可能包含 Command Buffer Error Codes 中列出的值,该值指示失败的原因。


命令编码器

命令编码器是一个临时对象,您可以使用它一次将命令和状态以 GPU 可以执行的格式写入单个命令缓冲区。
许多命令编码器对象方法将命令附加到命令缓冲区。
当命令编码器处于活动状态时,它拥有为其命令缓冲区附加命令的专有权。
完成命令编码后,调用endEncoding方法。
要写入更多命令,请创建一个新的命令编码器。


创建命令编码器对象

由于命令编码器会将命令附加到特定的命令缓冲区中,因此您可以通过向要使用命令编码器的MTLCommandBuffer对象请求命令编码器来创建命令编码器。
使用以下MTLCommandBuffer方法创建每种类型的命令编码器:

  • renderCommandEncoderWithDescriptor:方法创建一个MTLRenderCommandEncoder对象,用于将图形渲染到 MTLRenderPassDescriptor 中的附件。
  • computeCommandEncoder 方法创建一个MTLComputeCommandEncoder用于数据并行计算的对象。
  • blitCommandEncoder方法创建一个用于内存操作的MTLBlitCommandEncoder对象。
  • parallelRenderCommandEncoderWithDescriptor:方法创建一个MTLParallelRenderCommandEncoder对象,使几个MTLRenderCommandEncoder对象能够在不同的线程上运行,同时仍呈现到共享中指定的MTLRenderPassDescriptor附件。

渲染命令编码器

图形渲染可以用 渲染过程来描述。
一个 MTLRenderCommandEncoder对象表示 与单个渲染过程相关的渲染状态和绘制命令。
MTLRenderCommandEncoder需要一个关联的MTLRenderPassDescriptor(在创建渲染过程描述符中描述),其中包括颜色、深度和模板附件,作为渲染命令的目标。
MTLRenderCommandEncoder 具有以下方法:

  • 指定包含顶点、片段或纹理图像数据的图形资源,例如缓冲区和纹理对象
  • 指定包含已编译渲染状态的MTLRenderPipelineState对象,包括顶点和片段着色器
  • 指定固定功能状态,包括视口、三角形填充模式、剪刀矩形、深度和模板测试以及其他值
  • 绘制 3D 图元

有关MTLRenderCommandEncoder协议的详细信息,请参阅 图形渲染:渲染命令编码器。


计算命令编码器

对于数据并行计算,MTLComputeCommandEncoder协议提供了在命令缓冲区中编码命令的方法,这些命令可以指定计算函数及其参数(例如,纹理、缓冲区和采样器状态)并调度计算函数进行执行。
要创建计算命令编码器对象,请使用MTLCommandBuffercomputeCommandEncoder方法。
有关MTLComputeCommandEncoder方法和属性的详细信息,请参阅 数据并行计算处理:计算命令编码器。


Blit 命令编码器

MTLBlitCommandEncoder协议具有在缓冲区 (MTLBuffer) 和纹理 (MTLTexture)之间附加内存复制操作命令的方法。
MTLBlitCommandEncoder协议还提供了用纯色填充纹理和生成 mipmap 的方法。
要创建 blit 命令编码器对象,请使用MTLCommandBufferblitCommandEncoder方法。
有关MTLBlitCommandEncoder的方法和属性的详细信息,请参阅 缓冲区和纹理操作:Blit 命令编码器。


多线程、命令缓冲区和命令编码器

大多数应用使用单个线程在单个命令缓冲区中对单个帧的渲染命令进行编码。
在每一帧结束时,您都会提交命令缓冲区,该缓冲区会安排并开始执行命令。

如果您想要并行化命令缓冲区编码,那么您可以同时创建多个命令缓冲区,并使用单独的线程对每个命令缓冲区进行编码。
如果您提前知道命令缓冲区应按什么顺序执行,那么MTLCommandBufferenqueue方法可以在命令队列中声明执行顺序,而无需等待命令被编码和提交。
否则,当命令缓冲区被提交时,它会在命令队列中被分配一个位置,排在任何先前排队的命令缓冲区之后。

每次只能有一个 CPU 线程访问命令缓冲区。
多线程应用可以对每个命令缓冲区使用一个线程来并行创建多个命令缓冲区。

图 2-2显示了具有三个线程的示例。
每个线程都有自己的命令缓冲区。
对于每个线程,每次只有一个命令编码器可以访问其关联的命令缓冲区。
图 2-2还显示了每个命令缓冲区从不同的命令编码器接收命令。
完成编码后,调用endEncoding命令编码器的方法,然后新的命令编码器对象就可以开始将命令编码到命令缓冲区。


图 2-2 具有多个线程的 Metal 命令缓冲区

在这里插入图片描述


MTLParallelRenderCommandEncoder 对象 允许将单个渲染过程拆分为多个命令编码器并分配给单独的线程。
有关 MTLParallelRenderCommandEncoder 的更多信息,请参阅 使用多个线程对单个渲染过程进行编码。


四、资源对象:缓冲区和纹理

本章介绍用于存储未格式化内存和格式化图像数据的 Metal 资源对象( MTLResource)。
MTLResource对象有两种类型:

  • MTLBuffer表示可包含任意类型数据的未格式化内存分配。
    缓冲区通常用于顶点、着色器和计算状态数据。
  • MTLTexture表示具有指定纹理类型和像素格式的格式化图像数据的分配。
    纹理对象用作顶点、片段或计算函数的源纹理,以及存储图形渲染输出(即作为附件)。

本章还讨论了MTLSamplerState对象。
虽然采样器本身不是资源,但它们在使用纹理对象执行查找计算时会用到。


1、缓冲区是无类型的内存分配

MTLBuffer对象表示 可以包含任何类型数据的内存分配。


创建缓冲区对象

以下MTLDevice方法 创建并返回一个MTLBuffer对象:

  • newBufferWithLength:options:方法创建一个具有新的存储分配的MTLBuffer对象。
  • newBufferWithBytes:length:options:方法通过将数据从现有存储(位于 CPU 地址pointer)复制到新的存储分配 来创建MTLBuffer对象。
  • newBufferWithBytesNoCopy:length:options:deallocator:方法 使用现有的存储分配创建一个MTLBuffer对象,并且不为该对象分配任何新存储。

所有缓冲区创建方法都有输入值length来指示存储分配的大小(以字节为单位)。
所有方法还接受 MTLResourceOptions 对象 作为可以修改创建的缓冲区行为的options
如果值为options值为0,则资源选项将使用默认值。


缓冲方法

MTLBuffer协议有以下方法:

  • contents方法返回缓冲区存储分配的 CPU 地址。
  • newTextureWithDescriptor:offset:bytesPerRow:方法创建一种引用缓冲区数据的特殊纹理对象。
    该方法的详细说明请参阅 创建纹理对象。

2、纹理是格式化的图像数据

MTLTexture对象表示格式化图像数据的分配,可用作顶点着色器、片段着色器或计算函数的资源,或用作渲染目标的附件。
MTLTexture 对象可以具有以下结构之一:

  • 1D、2D 或 3D 图像
  • 1D 或 2D 图像数组
  • 由六幅 2D 图像组成的立方体

MTLPixelFormat : 指定 MTLTexture对象中各个像素的组织。
像素格式将在纹理的像素格式 中进一步讨论。


创建纹理对象

以下方法创建并返回一个MTLTexture对象:

  • newTextureWithDescriptor:方法MTLDevice创建一个MTLTexture对象,为纹理图像数据分配新的存储,并使用MTLTextureDescriptor对象来描述纹理的属性。
  • MTLTexturenewTextureViewWithPixelFormat: 方法创建一个 与调用对象共享相同存储分配的MTLTexture对象。
    由于它们共享相同的存储,因此对新纹理对象像素的任何更改都会反映在调用纹理对象中,反之亦然。
    对于新创建的纹理,newTextureViewWithPixelFormat:方法会重新解释 调用MTLTexture对象存储分配的现有纹理图像数据,就好像数据是以指定的像素格式存储的一样。
    新纹理对象的 MTLPixelFormat 必须与原始纹理对象的 MTLPixelFormat 兼容。
    (有关普通、打包和压缩像素格式的详细信息,请参阅 纹理的像素格式。)
  • MTLBuffernewTextureWithDescriptor:offset:bytesPerRow:方法创建一个MTLTexture对象,该对象将 调用MTLBuffer对象 的存储分配 与其纹理图像数据共享。
    由于它们共享相同的存储空间,因此对新纹理对象的像素的任何更改都会反映在调用纹理对象中,反之亦然。
    在纹理和缓冲区之间共享存储空间可能会阻止使用某些纹理优化,例如像素混合或平铺。

使用纹理描述符创建纹理对象

MTLTextureDescriptor定义用于创建MTLTexture对象的属性,包括其图像大小(宽度、高度和深度)、像素格式、排列(数组或立方体类型)和 mipmap 数量。
这些MTLTextureDescriptor属性仅在创建MTLTexture对象期间使用。
创建MTLTexture对象后,MTLTextureDescriptor对象中的属性更改 不再对该纹理产生任何影响。

要从描述符创建一个或多个纹理:

  1. 创建一个MTLTextureDescriptor包含描述纹理数据的纹理属性的自定义对象:
    • textureType 属性指定纹理的维数和排列(例如,数组或立方体)。
    • widthheightdepth属性指定基础级别纹理 mipmap 各个维度的像素大小。
    • pixelFormat 属性指定像素如何存储在纹理中。
    • arrayLength 属性指定MTLTextureType1DArrayMTLTextureType2DArray类型纹理对象 的数组元素的数量。
    • mipmapLevelCount属性指定 mipmap 级别的数量。
    • sampleCount属性指定每个像素中的样本数。
    • resourceOptions属性指定其内存分配的行为。
  2. 通过调用 MTLDevice对象的newTextureWithDescriptor:方法 从MTLTextureDescriptor对象创建纹理。
    创建纹理后,调用replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage: 方法加载纹理图像数据,详情请参阅 将图像数据复制到纹理和从纹理复制图像数据。
  3. 要创建更多MTLTexture对象,您可以重用同一个MTLTextureDescriptor对象,并根据需要修改描述符的属性值。

例 3-1展示了创建纹理描述符txDesc,并设置其 3D、64x64x64 纹理属性的代码。


例 3-1 使用自定义纹理描述符创建纹理对象

MTLTextureDescriptor* txDesc = [[MTLTextureDescriptor alloc] init];
txDesc.textureType = MTLTextureType3D;
txDesc.height = 64;
txDesc.width = 64;
txDesc.depth = 64;
txDesc.pixelFormat = MTLPixelFormatBGRA8Unorm;
txDesc.arrayLength = 1;
txDesc.mipmapLevelCount = 1;
id <MTLTexture> aTexture = [device newTextureWithDescriptor:txDesc];

使用纹理切片

切片是单个 1D、2D 或 3D 纹理图像 及 其所有关联的 mipmap。
对于每个切片:

  • 基础级别 mipmap 的大小,由MTLTextureDescriptor对象的widthheightdepth属性指定。
  • mipmap 级别i 的缩放大小由 m a x ( 1 , f l o o r ( ‘ w i d t h ‘ / 2 i ) ) x m a x ( 1 , f l o o r ( ‘ h e i g h t ‘ / 2 i ) ) x m a x ( 1 , f l o o r ( ‘ d e p t h ‘ / 2 i ) ) max(1, floor( `width`/ 2^i )) x max(1, floor( `height`/ 2^i )) x max(1, floor( `depth`/ 2^i )) max(1,floor(width‘/2i))xmax(1,floor(height‘/2i))xmax(1,floor(depth‘/2i)) 指定。
    最大 mipmap 级别是第一个达到 1 x 1 x 1 大小的 mipmap 级别。
  • 一个切片中的 mipmap 级别数可以通过 f l o o r ( l o g 2 ( m a x ( ‘ w i d t h ‘ , ‘ h e i g h t ‘ , ‘ d e p t h ‘ ) ) ) + 1 floor(log_2 (max( `width`, `height`, `depth`)))+1 floor(log2(max(width,height,depth)))+1 确定。

所有纹理对象至少有一个切片;立方体和数组纹理类型可能有多个切片。
在将图像数据复制到纹理和从纹理中复制图像数据中讨论的写入和读取纹理图像数据的方法中,slice是一个从零开始的输入值。
对于 1D、2D 或 3D 纹理,只有一个切片,因此 slice 的值必须为 0。
立方体纹理共有六个 2D 切片,地址从 0 到 5。
对于 1DArray 和 2DArray 纹理类型,每个数组元素代表一个切片。
例如,对于arrayLength= 10 的 2DArray 纹理类型,共有 10 个切片,地址从 0 到 9。
要从整体纹理结构中选择单个 1D、2D 或 3D 图像,请先选择一个切片,然后选择该切片内的 mipmap 级别。


使用便捷方法创建纹理描述符

对于常见的 2D 和立方体纹理,使用以下便捷方法创建一个MTLTextureDescriptor对象并自动设置其几个属性值:

  • texture2DDescriptorWithPixelFormat:width:height:mipmapped:方法为 2D 纹理创建一个MTLTextureDescriptor对象。
    widthheight值定义 2D 纹理的尺寸。
    type属性自动设置为MTLTextureType2D,并且deptharrayLength设置为 1。
  • textureCubeDescriptorWithPixelFormat:size:mipmapped:方法为立方体纹理创建一个MTLTextureDescriptor对象,其中type属性设置为MTLTextureTypeCubewidthheight设置为尺寸,并且deptharrayLength设置为1。

这两种MTLTextureDescriptor便捷方法都接受输入值,pixelFormat,定义了纹理的像素格式。
这两种方法还接受输入值mipmapped,它决定纹理图像是否经过 mipmapped。(如果mipmappedYES,则纹理经过 mipmapped。)

例 3-2 使用 texture2DDescriptorWithPixelFormat:width:height:mipmapped:方法为 未经 mipmapped 的 64x64 2D 纹理创建描述符对象。


例 3-2 使用便捷纹理描述符创建纹理对象

MTLTextureDescriptor *texDesc = [MTLTextureDescriptor 
         texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm 
         width:64 height:64 mipmapped:NO];
id <MTLTexture> myTexture = [device newTextureWithDescriptor:texDesc];

将图像数据复制到纹理或从纹理中复制图像数据

要将图像数据同步复制到MTLTexture对象的存储分配中,或从对象的存储分配中复制数据,请使用以下方法:

  • replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:将调用者的指针中的像素数据区域复制到指定纹理切片的存储分配部分中。
    replaceRegion:mipmapLevel:withBytes:bytesPerRow:是一种类似的便捷方法,它将像素数据区域复制到默认切片中,假设切片相关参数的默认值(即slice= 0 和bytesPerImage= 0)。
  • getBytes:bytesPerRow:bytesPerImage:fromRegion:mipmapLevel:slice:从指定的纹理切片中检索像素数据区域。
    getBytes:bytesPerRow:fromRegion:mipmapLevel:是一种类似的便捷方法,它从默认切片中检索像素数据区域,假设切片相关参数的默认值(slice= 0 和bytesPerImage= 0)。

例 3-3显示了如何调用replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage: 来从系统内存中的源数据中指定纹理图像,textureData,在切片0 和 mipmap 级别0


例 3-3 将图像数据复制到纹理中

//  pixelSize is the size of one pixel, in bytes
//  width, height - number of pixels in each dimension
NSUInteger myRowBytes = width * pixelSize;
NSUInteger myImageBytes = rowBytes * height;
[tex replaceRegion:MTLRegionMake2D(0,0,width,height)
    mipmapLevel:0 slice:0 withBytes:textureData
    bytesPerRow:myRowBytes bytesPerImage:myImageBytes];

纹理的像素格式

MTLPixelFormat : 指定MTLTexture对象各个像素中颜色、深度和模板数据存储的组织。
像素格式有三种:普通、打包和压缩。

  • 普通格式只有常规的 8 位、16 位或 32 位颜色分量。
    每个分量按递增的内存地址排列,第一个列出的分量位于最低地址。
    例如,MTLPixelFormatRGBA8Unorm是 32 位格式,每个颜色分量有 8 位;最低地址包含红色,下一个地址包含绿色,依此类推。
    相反,对于MTLPixelFormatBGRA8Unorm,最低地址包含蓝色,下一个地址包含绿色,依此类推。
  • 打包格式将多个组件组合成一个 16 位或 32 位值,其中组件按从最低有效位到最高有效位 (LSB 到 MSB) 的顺序存储。
    例如,MTLPixelFormatRGB10A2Uint是一个 32 位打包格式,包含三个 10 位通道(用于 R、G 和 B)和两个用于 alpha 的位。
  • 压缩格式以像素块的形式排列,每个块的布局特定于该像素格式。
    压缩像素格式只能用于 2D、2D 数组或立方体纹理类型。
    压缩格式不能用于创建 1D、2DMultisample 或 3D 纹理。

MTLPixelFormatGBGR422MTLPixelFormatBGRG422是特殊的像素格式,用于在 YUV 颜色空间中存储像素。
这些格式仅支持 2D 纹理(但不支持 2D 数组或立方体类型),不支持 mipmap,甚至不支持width

有几种像素格式使用 sRGB 颜色空间值存储颜色分量(例如MTLPixelFormatRGBA8Unorm_sRGBMTLPixelFormatETC2_RGB8_sRGB)。
当采样操作引用具有 sRGB 像素格式的纹理时,Metal 实现会在采样操作发生之前将 sRGB 颜色空间分量转换为线性颜色空间。
从 sRGB 分量 S 到线性分量 L 的转换如下:

  • 如果 S <= 0.04045,则 L = S/12.92
  • 如果 S > 0.04045,则 L = ( ( S + 0.055 ) / 1.055 ) 2.4 L = ((S+0.055)/1.055)^{2.4} L=((S+0.055)/1.055)2.4

相反,当渲染到使用具有 sRGB 像素格式的纹理的可颜色渲染附件时,实现会将线性颜色值转换为 sRGB,如下所示:

  • 如果 L <= 0.0031308,则 S = L * 12.92
  • 如果 L > 0.0031308,则 S = (1.055 * L^{0.41667} ) - 0.055

有关渲染像素格式的更多信息,请参阅 创建渲染过程描述符。


3、为纹理查找创建采样器状态对象

MTLSamplerState 对象定义 图形或计算函数对MTLTexture对象执行纹理采样操作时使用的寻址、过滤和其他属性。
采样器描述符定义采样器状态对象的属性。
要创建采样器状态对象:

  1. 调用MTLDevice对象的newSamplerStateWithDescriptor:方法来创建 MTLSamplerDescriptor 对象。
  2. MTLSamplerDescriptor对象中设置所需的值,包括过滤选项、寻址模式、最大各向异性和细节级别参数。
  3. 通过调用创建描述符的MTLDevice对象的newSamplerStateWithDescriptor:方法,从采样器描述符创建一个MTLSamplerState对象。

您可以重复使用采样器描述符对象来创建更多MTLSamplerState对象,并根据需要修改描述符的属性值。
描述符的属性仅在对象创建期间使用。
创建采样器状态后,更改其描述符中的属性不再对该采样器状态产生影响。

例 3-4是一个代码示例,它创建MTLSamplerDescriptor并配置它,以创建MTLSamplerState
为描述符对象的过滤器和地址模式属性设置了非默认值。
然后newSamplerStateWithDescriptor:方法使用采样器描述符创建采样器状态对象。


例 3-4 创建采样器状态对象

// create MTLSamplerDescriptor
MTLSamplerDescriptor *desc = [[MTLSamplerDescriptor alloc] init];
desc.minFilter = MTLSamplerMinMagFilterLinear;
desc.magFilter = MTLSamplerMinMagFilterLinear;
desc.sAddressMode = MTLSamplerAddressModeRepeat;
desc.tAddressMode = MTLSamplerAddressModeRepeat;

//  all properties below have default values
desc.mipFilter        = MTLSamplerMipFilterNotMipmapped;
desc.maxAnisotropy    = 1U;
desc.normalizedCoords = YES;
desc.lodMinClamp      = 0.0f;
desc.lodMaxClamp      = FLT_MAX;

// create MTLSamplerState
id <MTLSamplerState> sampler = [device newSamplerStateWithDescriptor:desc];

4、保持 CPU 和 GPU 内存之间的一致性

CPU 和 GPU 都可以访问 MTLResource对象的底层存储空间。
不过,GPU 与主机 CPU 的运行方式是异步的,因此在使用主机 CPU 访问这些资源的存储空间时,请注意以下几点。

当执行 MTLCommandBuffer 对象时,仅当(且仅当)主机CPU在提交MTLCommandBuffer对象之前做出这些更改时,MTLDevice对象才能保证 观察到主机CPU对该MTLCommandBuffer 对象引用的任何 MTLResource 对象的存储分配所做的任何更改。也就是说,在提交相应的 MTLCommandBuffer 对象之后,MTLDevice 对象可能不会观察到主机CPU对资源所做的更改(即,MTLCommandBuffer对象的status 属性为MTLCommand缓冲器StatusCommitted)。

类似地,在MTLDevice对象执行MTLCommandBuffer对象之后,只有在命令缓冲区已完成执行的情况下(即,MTLCommandBuffer对象的status属性为MTLCommandCacheStatusCompleted),主机CPU才能保证观察到MTLDevice对象对该命令缓冲区引用的任何资源的存储分配所做的任何更改。


五、函数和库

本章介绍如何创建一个MTLFunction对象作为对 Metal 着色器或计算函数的引用,以及如何使用MTLLibrary对象组织和访问函数。


1、MTLFunction 表示着色器或计算函数

MTLFunction对象 表示用 Metal 着色语言编写的单个函数,该函数作为图形或计算管道的一部分在 GPU 上执行。
有关 Metal 着色语言的详细信息,请参阅Metal 着色语言指南

要在 Metal 运行时和使用 Metal 着色语言编写的图形或计算函数之间传递数据或状态,您需要为纹理、缓冲区和采样器分配参数索引。
参数索引标识 Metal 运行时和 Metal 着色代码引用了哪个纹理、缓冲区或采样器。

对于渲染过程,您可以指定一个MTLFunction对象作为MTLRenderPipelineDescriptor对象中的顶点或片段着色器,如创建渲染管道状态中所述。
对于计算过程,您可以在 为目标设备创建MTLComputePipelineState对象时 指定一个MTLFunction对象,如指定计算命令编码器的计算状态和资源 中所述。


2、库是函数的存储库

MTLLibrary 对象表示一个或多个MTLFunction对象的存储库。
单个MTLFunction对象表示使用着色语言编写的一个 Metal 函数。
在 Metal 着色语言源代码中,任何使用 Metal 函数限定符(vertexfragmentkernel)的函数都可以由MTLFunction库中的对象表示。
没有这些函数限定符之一的 Metal 函数不能直接由对象表示MTLFunction,尽管它可以由着色器中的另一个函数调用。

库中的MTLFunction对象可以从以下任一来源创建:

  • 在应用程序构建过程中编译成二进制library格式的 Metal 着色语言代码。
  • 包含应用程序在运行时 编译的 Metal 着色语言源代码的文本字符串。

从编译代码创建库

为了获得最佳性能,请在 Xcode 中构建应用的过程中将 Metal 着色语言源代码编译为库文件,这样可以避免在应用运行时编译函数源的成本。
要从库二进制文件创建MTLLibrary对象,请调用MTLDevice的以下方法之一:

  • newDefaultLibrary:检索为应用程序 Xcode 项目中,包含所有着色器和计算函数的主包构建的库。
  • newLibraryWithFile:error::获取库文件的路径,并返回MTLLibrary包含存储在该库文件中的所有函数的对象。
  • newLibraryWithData:error::获取包含库中函数代码的二进制 blob 并返回一个MTLLibrary对象。

有关在构建过程中编译 Metal 着色语言源代码的更多信息,请参阅 在应用程序构建过程中创建库。


从源代码创建库

要从可能包含多个函数的 Metal 着色语言源代码字符串 创建MTLLibrary,请调用MTLDevice的以下方法之一。这些方法在创建库时编译源代码。
要指定要使用的编译器选项,请设置MTLCompileOptions对象中的属性。

  • newLibraryWithSource:options:error::从输入字符串同步编译源代码 以创建MTLFunction对象,然后返回包含它们的MTLLibrary对象。
  • newLibraryWithSource:options:completionHandler::从输入字符串异步编译源代码以创建MTLFunction对象,然后返回包含它们的MTLLibrary对象。
    completionHandler 是对象创建完成时调用的代码块。

从库中获取函数

MTLLibrarynewFunctionWithName:方法返回具有请求名称的MTLFunction对象。
如果在库中未找到使用 Metal 着色语言函数限定符的函数名称,则newFunctionWithName:返回nil

例 4-1使用 MTLDevicenewLibraryWithFile:error:方法 通过其完整路径名定位库文件,并使用其内容创建一个具有一个或多个MTLFunction对象的MTLLibrary对象。
加载文件时出现的任何错误都会返回到 error 中。
然后 MTLLibrarynewFunctionWithName:方法创建一个表示源代码中my_func调用的函数的MTLFunction对象。
返回的函数对象myFunc现在可以在应用程序中使用。


例 4-1 从库中访问函数

NSError *errors;
id <MTLLibrary> library = [device newLibraryWithFile:@"myarchive.metallib"
                          error:&errors];

id <MTLFunction> myFunc = [library newFunctionWithName:@"my_func"];

3、在运行时确定函数详细信息

由于MTLFunction对象的实际内容 由图形着色器或计算函数定义,而这些函数可能在 MTLFunction对象创建之前就已编译,因此其源代码可能无法直接供应用使用。
您可以在运行时查询以下MTLFunction属性:

  • name,包含函数名称的字符串。
  • functionType,指示该函数是否声明为顶点、片段还是计算函数。
  • vertexAttributes,一组MTLVertexAttribute对象,用于描述顶点属性数据在内存中的组织方式以及将其映射到顶点函数参数的方式。
    有关更多详细信息,请参阅 数据组织的顶点描述符。

MTLFunction不提供对函数参数的访问。
在创建管道状态期间,可以获取反射对象(MTLRenderPipelineReflectionMTLComputePipelineReflection,取决于命令编码器的类型),该对象揭示了着色器或计算函数参数的详细信息。
有关创建管道状态和反射对象的详细信息,请参阅 创建渲染管道状态 或 创建计算管道状态。
如果不使用反射数据,请避免获取它。

反射对象包含命令编码器支持的每种函数类型的MTLArgument对象数组。
对于MTLComputeCommandEncoder,在参数属性中 MTLComputePipelineReflection有一个对象数组,对应于其计算函数的参数。
对于MTLRenderCommandEncoderMTLRenderPipelineReflection 有两个属性vertexArgumentsfragmentArguments,它们是分别对应于顶点函数参数和片段函数参数的数组。

函数的参数并非全部都存在于反射对象中。
反射对象仅包含具有关联资源的参数,但不包含使用[[ stage_in ]]限定符,或内置限定符声明的 [[ vertex_id ]][[ attribute_id ]]参数。

例 4-2显示了如何获取反射对象(在此示例中为MTLComputePipelineReflection),然后遍历 arguments属性中的MTLArgument对象。


例 4-2 通过函数参数进行迭代

MTLComputePipelineReflection* reflection;
id <MTLComputePipelineState> computePS = [device
              newComputePipelineStateWithFunction:func
              options:MTLPipelineOptionArgumentInfo
              reflection:&reflection error:&error];
for (MTLArgument *arg in reflection.arguments) {
    //  process each MTLArgument
}

这些MTLArgument属性揭示了着色语言函数参数的细节。

  • name属性只是参数的名称。
  • active是一个布尔值,指示是否可以忽略该参数。
  • index是其对应参数表中从零开始的位置。
    例如,对于[[ buffer(2) ]]index为 2。
  • access描述任何访问限制,例如读或写访问限定符。
  • type由阴影语言限定符表示,例如[[ buffer(n) ]][[ texture(n) ]][[ sampler(n) ]][[ threadgroup(n) ]]

type确定哪些其他MTLArgument属性是相关的。

  • 如果typeMTLArgumentTypeTexture,则textureType属性表示整体纹理类型(例如着色语言中的texture1d_arraytexturecubetextureDataType 类型),属性texture2d_ms表示组件数据类型(例如halffloatintuint)。
  • 如果typeMTLArgumentTypeThreadgroupMemory,则threadgroupMemoryAlignmentthreadgroupMemoryDataSize属性相关。
  • 如果typeMTLArgumentTypeBuffer,则bufferAlignmentbufferDataSizebufferDataTypebufferStructType性质是相关的。

如果缓冲区参数是结构体(即bufferDataTypeMTLDataTypeStruct),则bufferStructType属性包含MTLStructType来表示该结构 ,而却bufferDataSize包含该结构体的大小(以字节为单位)。
如果缓冲区参数是数组(或指向数组的指针),则bufferDataType指示元素的数据类型,并bufferDataSize包含一个数组元素的大小(以字节为单位)。

例 4-3深入研究一个MTLStructType对象以检查结构成员的详细信息,每个成员都由一个MTLStructMember对象表示。
结构成员可以是简单类型、数组或嵌套结构。
如果成员是嵌套结构,则调用MTLStructMemberstructType方法,获取表示该结构的MTLStructType对象,然后递归深入分析它。
如果成员是数组,则使用MTLStructMemberarrayType方法获取代表它的MTLArrayType
然后检查MTLArrayTypeelementType属性。
如果elementTypeMTLDataTypeStruct,则调用elementStructType方法获取结构并继续深入研究其成员。
如果elementTypeMTLDataTypeArray,则调用elementArrayType方法获取子数组并进一步分析它。


例 4-3 处理结构体参数

MTLStructType *structObj = [arg.bufferStructType];
for (MTLStructMember *member in structObj.members) {
    //  process each MTLStructMember
    if (member.dataType == MTLDataTypeStruct) {
       MTLStructType *nestedStruct = member.structType;
       // recursively drill down into the nested struct
    }
    else if (member.dataType == MTLDataTypeArray) {
       MTLStructType *memberArray = member.arrayType;
       // examine the elementType and drill down, if necessary
    }
    else {
       // member is neither struct nor array
       // analyze it; no need to drill down further
    }
}

六、图形渲染:渲染命令编码器

本章介绍如何创建和使用MTLRenderCommandEncoderMTLParallelRenderCommandEncoder对象,它们用于将图形渲染命令编码到命令缓冲区中。
MTLRenderCommandEncoder命令描述图形渲染管道,如图5-1所示。


图 5-1 Metal 图形渲染管道

在这里插入图片描述


MTLRenderCommandEncoder 对象代表单个渲染命令编码器。
MTLParallelRenderCommandEncoder对象可以将单个渲染过程分解为多个单独的MTLRenderCommandEncoder对象,每个对象可以分配给不同的线程。
然后,来自不同渲染命令编码器的命令将链接在一起,并以一致、可预测的顺序执行,如渲染过程的多个线程中所述。


1、创建并使用渲染命令编码器

要创建、初始化和使用单个渲染命令编码器:

  1. 创建一个MTLRenderPassDescriptor对象来定义一组附件,这些附件用作该渲染过程的命令缓冲区中图形命令的渲染目标。
    通常,您创建一次 MTLRenderPassDescriptor对象,并在应用每次渲染一帧时重复使用它。
    请参阅 创建渲染过程描述符。
  2. 使用指定的渲染过程描述符,调用MTLCommandBufferrenderCommandEncoderWithDescriptor:方法来创建MTLRenderCommandEncoder对象。
    请参阅 使用渲染过程描述符创建渲染命令编码器。
  3. 创建一个MTLRenderPipelineState对象来定义一个或多个绘制调用的图形渲染管道(包括着色器、混合、多重采样和可见性测试)的状态。
    要使用此渲染管道状态绘制图元,请调用MTLRenderCommandEncodersetRenderPipelineState:方法。
    有关详情,请参阅 创建渲染管道状态。
  4. 设置渲染命令编码器要使用的纹理、缓冲区和采样器,如为渲染命令编码器指定资源中所述。
  5. 调用MTLRenderCommandEncoder方法指定其他固定功能状态,包括深度和模板状态,如固定功能状态操作中所述。
  6. 最后,调用MTLRenderCommandEncoder方法绘制图形基元,如绘制几何基元中所述。

创建渲染过程描述符

MTLRenderPassDescriptor对象 表示编码渲染命令的目标,它是附件的集合。
渲染过程描述符的属性可能包括最多四个用于颜色像素数据的附件、一个用于深度像素数据的附件和一个用于模板像素数据的附件。
renderPassDescriptor便捷方法创建一个具有颜色、深度和模板附件属性的MTLRenderPassDescriptor对象,并带有默认附件状态。
visibilityResultBuffer属性指定一个缓冲区,设备可以更新该缓冲区以指示是否有任何样本通过深度和模板测试 - 有关详细信息,请参阅 固定功能状态操作。

每个单独的附件(包括将要写入的纹理)都由附件描述符表示。
对于附件描述符,必须适当选择相关纹理的像素格式来存储颜色、深度或模板数据。
对于颜色附件描述符,MTLRenderPassColorAttachmentDescriptor,使用颜色可渲染像素格式。
对于深度附件描述符,MTLRenderPassDepthAttachmentDescriptor,使用深度可渲染像素格式,例如MTLPixelFormatDepth32Float
对于模板附件描述符,MTLRenderPassStencilAttachmentDescriptor,使用模板可渲染像素格式,例如MTLPixelFormatStencil8

纹理在设备上每个像素实际使用的内存量并不总是与 Metal 框架代码中纹理像素格式的大小相匹配,因为设备会添加填充以进行对齐或其他目的。
请参阅 Metal功能集表章节,了解每个像素格式实际使用的内存量,以及附件的大小和数量限制。


加载和存储操作

附件描述符的loadActionstoreAction属性,指定在渲染过程开始或结束时执行的操作。
(对于MTLParallelRenderCommandEncoder,加载和存储操作发生在整个命令的边界,而不是其每个MTLRenderCommandEncoder对象的边界。
有关详细信息,请参阅 渲染过程的多个线程。)

可能的loadAction值包括:

  • MTLLoadActionClear,它将相同的值写入指定附件描述符中的每个像素。
    有关此操作的更多详细信息,请参阅 指定清除加载操作。
  • MTLLoadActionLoad,它保留了纹理的现有内容。
  • MTLLoadActionDontCare,它允许附件中的每个像素在渲染过程开始时采用任意值。

如果您的应用程序将为给定帧渲染附件的所有像素,请使用默认加载操作MTLLoadActionDontCare
MTLLoadActionDontCare操作允许 GPU 避免加载纹理的现有内容,从而确保最佳性能。
否则,您可以使用操作MTLLoadActionClear清除附件的先前内容,或使用MTLLoadActionLoad操作保留它们。
MTLLoadActionClear操作还可以避免加载现有纹理内容,但需要用纯色填充目标。


可能的storeAction值包括:

  • MTLStoreActionStore,将渲染过程的最终结果保存到附件中。
  • MTLStoreActionMultisampleResolve,它将渲染目标中的多重采样数据解析为单个采样值,将它们存储在附件resolveTexture属性指定的纹理中,并使附件的内容保持未定义状态。
    有关详细信息,请参阅 示例:为多重采样渲染创建渲染过程描述符。
  • MTLStoreActionDontCare,这会使附件在渲染过程完成后处于未定义状态。
    这可以提高性能,因为它使实现能够避免保存渲染结果所需的任何工作。

对于颜色附件,MTLStoreActionStore操作是默认的存储操作,因为应用程序几乎总是在渲染过程结束时在附件中保留最终颜色值。
对于深度和模板附件,MTLStoreActionDontCare是默认的存储操作,因为这些附件在渲染过程完成后通常不需要保留。


指定清除加载操作

如果附件描述符的loadAction属性设置为MTLLoadActionClear,则在渲染过程开始时,会将清除值写入指定附件描述符中的每个像素。
清除值属性取决于附件的类型。

  • 对于MTLRenderPassColorAttachmentDescriptorclearColor包含一个MTLClearColor值,由四个双精度浮点 RGBA 分量组成的值,用于清除颜色附件。
    MTLClearColorMake函数 从红色、绿色、蓝色和 alpha 分量中创建一个清除颜色值。
    默认清除颜色为 (0.0, 0.0, 0.0, 1.0),即不透明黑色。
  • 对于MTLRenderPassDepthAttachmentDescriptorclearDepth包含一个双精度浮点清除值,范围在 [0.0, 1.0] 内,用于清除深度附件。默认值为 1.0。
  • 对于MTLRenderPassStencilAttachmentDescriptorclearStencil包含一个 32 位无符号整数,用于清除模板附件。默认值为 0。

示例:使用加载和存储操作创建渲染过程描述符

例 5-1创建了一个带有颜色和深度附件的简单渲染过程描述符。
首先,创建两个纹理对象,一个具有可渲染颜色的像素格式,另一个具有深度像素格式。
接下来,MTLRenderPassDescriptorrenderPassDescriptor便捷方法 创建一个默认的渲染过程描述符。
然后通过MTLRenderPassDescriptor的属性访问颜色和深度附件。
纹理和操作在中设置colorAttachments[0],它代表第一个颜色附件(在数组中的索引 0 处)和深度附件。


例 5-1 创建带有颜色和深度附件的渲染过程描述符

MTLTextureDescriptor *colorTexDesc = [MTLTextureDescriptor
           texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
           width:IMAGE_WIDTH height:IMAGE_HEIGHT mipmapped:NO];
id <MTLTexture> colorTex = [device newTextureWithDescriptor:colorTexDesc];
 
MTLTextureDescriptor *depthTexDesc = [MTLTextureDescriptor
           texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float
           width:IMAGE_WIDTH height:IMAGE_HEIGHT mipmapped:NO];
id <MTLTexture> depthTex = [device newTextureWithDescriptor:depthTexDesc];
 
MTLRenderPassDescriptor *renderPassDesc = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDesc.colorAttachments[0].texture = colorTex;
renderPassDesc.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDesc.colorAttachments[0].storeAction = MTLStoreActionStore;
renderPassDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.0,1.0,0.0,1.0);
 
renderPassDesc.depthAttachment.texture = depthTex;
renderPassDesc.depthAttachment.loadAction = MTLLoadActionClear;
renderPassDesc.depthAttachment.storeAction = MTLStoreActionStore;
renderPassDesc.depthAttachment.clearDepth = 1.0;

示例:为多重采样渲染创建渲染过程描述符

要使用MTLStoreActionMultisampleResolve操作,您必须将texture属性设置为多重采样类型的纹理,并且resolveTexture属性 将包含多重采样解析操作的结果。
(如果texture不支持多重采样,则多重采样解析操作的结果未定义。)
resolveLevelresolveSliceresolveDepthPlane属性也可用于多重采样解析操作,以分别指定多重采样纹理的 mipmap 级别、立方体切片和深度平面。
在大多数情况下, resolveLevelresolveSliceresolveDepthPlane 的值 默认可用。
在例 5-2中,最初创建一个附件,然后将其loadActionstoreActiontextureresolveTexture属性 设置为支持多重采样解析。


例 5-2 使用多重采样解析设置附件的属性

MTLTextureDescriptor *colorTexDesc = [MTLTextureDescriptor
           texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
           width:IMAGE_WIDTH height:IMAGE_HEIGHT mipmapped:NO];
id <MTLTexture> colorTex = [device newTextureWithDescriptor:colorTexDesc];
 
MTLTextureDescriptor *msaaTexDesc = [MTLTextureDescriptor
           texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
           width:IMAGE_WIDTH height:IMAGE_HEIGHT mipmapped:NO];
msaaTexDesc.textureType = MTLTextureType2DMultisample;
msaaTexDesc.sampleCount = sampleCount;  //  must be > 1
id <MTLTexture> msaaTex = [device newTextureWithDescriptor:msaaTexDesc];
 
MTLRenderPassDescriptor *renderPassDesc = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDesc.colorAttachments[0].texture = msaaTex;
renderPassDesc.colorAttachments[0].resolveTexture = colorTex;
renderPassDesc.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDesc.colorAttachments[0].storeAction = MTLStoreActionMultisampleResolve;
renderPassDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.0,1.0,0.0,1.0);

使用渲染过程描述符创建渲染命令编码器

创建渲染过程描述符并指定其属性后,使用MTLCommandBuffer对象的 renderCommandEncoderWithDescriptor:方法 创建渲染命令编码器,如例 5-3所示。


例 5-3 使用渲染过程描述符创建渲染命令编码器

id <MTLRenderCommandEncoder> renderCE = [commandBuffer
                    renderCommandEncoderWithDescriptor:renderPassDesc];

2、使用 Core Animation 显示渲染内容

Core Animation 定义了一个CAMetalLayer类,该类专为使用 Metal 渲染内容的层支持视图的专门行为而设计。
CAMetalLayer对象表示有关内容的几何形状(位置和大小)、其视觉属性(背景颜色、边框和阴影)以及 Metal 用于在颜色附件中呈现内容的资源的信息。
它还封装了内容呈现的时间,以便内容可以在可用时或在指定时间显示。
有关 Core Animation 的更多信息,请参阅 Core Animation 编程指南

Core Animation 还为可显示资源的对象定义了CAMetalDrawable协议。
CAMetalDrawable协议扩展MTLDrawable,并提供符合该MTLTexture协议的对象,因此可以将其用作渲染命令的目标。
要渲染到CAMetalLayer对象中,您应为每个渲染过程获取一个新CAMetalDrawable对象,获取它提供的MTLTexture对象,然后使用该纹理创建颜色附件。
与颜色附件不同,创建和销毁深度或模板附件的成本很高。
如果您需要深度或模板附件,请创建一次,然后在每次渲染帧时重复使用它们。

通常,您可以使用layerClass方法指定CAMetalLayer您自己的自定义 UIView 子类的底层类型,如例 5-4所示。
否则,您可以创建一个带有init方法的CAMetalLayer的视图,并将该层包含在现有视图中。


例 5-4 使用 CAMetalLayer 作为 UIView 子类的背景层

+ (id) layerClass {
    return [CAMetalLayer class];
}

要在图层中显示 Metal 渲染的内容,您必须从CAMetalLayer对象获取可显示资源(CAMetalDrawable对象),然后通过将其附加到MTLRenderPassDescriptor对象来渲染到此资源中的纹理。
为此,您首先设置CAMetalLayer对象的属性,来描述它提供的可绘制资源,然后在每次开始渲染新帧时调用其nextDrawable方法。
如果CAMetalLayer未设置属性,则nextDrawable方法调用失败。
以下CAMetalLayer属性描述了可绘制对象:

  • device属性声明了创建资源的MTLDevice对象。
  • pixelFormat属性声明纹理的像素格式。
    支持的值包括MTLPixelFormatBGRA8Unorm(默认值)和MTLPixelFormatBGRA8Unorm_sRGB
  • drawableSize属性 声明纹理的尺寸(以设备像素为单位)。
    为确保您的应用能够以显示屏的精确尺寸呈现内容(某些设备上无需额外的采样阶段),请在计算图层的所需尺寸时考虑目标屏幕的nativeScalenativeBounds属性。
  • framebufferOnly属性声明纹理是否只能用作附件(YES)或 是否还可用于纹理采样和像素读/写操作(NO)。
    如果YES,则图层对象可以优化纹理以进行显示。对于大多数应用,建议值为YES
  • presentsWithTransaction属性声明对图层渲染资源的更改是否使用标准核心动画事务机制进行更新(YES),还是与正常图层更新异步更新(NO,默认值)。

如果nextDrawable方法成功,它将返回具有以下只读属性的CAMetalDrawable对象:

  • 该属性保存纹理对象。
    在创建渲染管道(MTLRenderPipelineColorAttachmentDescriptor对象)texture时,可以使用它作为附件。
  • layer属性指向CAMetalLayer,负责显示可绘制对象的对象。

重要提示: 可绘制资源的数量很少,因此较长的帧渲染时间可能会暂时耗尽这些资源,并导致nextDrawable方法调用阻塞其 CPU 线程,直到该方法完成为止。
为避免昂贵的 CPU 停顿,请在调用CAMetalLayer对象的nextDrawable方法之前执行所有不需要可绘制资源的每帧操作。


要在渲染完成后显示可绘制对象的内容,您必须通过调用可绘制对象的present方法将其提交给 Core Animation。
要将可绘制对象的呈现与负责其渲染的命令缓冲区的完成同步,您可以调用MTLCommandBuffer对象上的presentDrawable:presentDrawable:atTime:便捷方法。
这些方法使用 预定的处理程序(请参阅 注册用于命令缓冲区执行的处理程序块)来调用可绘制对象的present方法,该方法涵盖了大多数场景。
presentDrawable:atTime:方法可以进一步控制可绘制对象的呈现时间。


3、创建渲染管道状态

要使用MTLRenderCommandEncoder对象 对渲染命令进行编码,您必须先指定一个MTLRenderPipelineState对象 来定义任何绘制调用的图形状态。
渲染管道状态对象是一个长期存在的持久对象,可以在渲染命令编码器之外创建、提前缓存并在多个渲染命令编码器之间重复使用。
在描述同一组图形状态时,重复使用先前创建的渲染管道状态对象可以避免重新评估并将指定状态转换为 GPU 命令的昂贵操作。

渲染管道状态是不可变的对象。
要创建渲染管道状态,首先要创建并配置一个描述渲染管道状态属性的可变MTLRenderPipelineDescriptor对象。
然后,使用描述符创建一个MTLRenderPipelineState对象。


创建和配置渲染管道描述符

要创建渲染管线状态,首先要创建一个MTLRenderPipelineDescriptor对象,该对象具有描述要在渲染过程中使用的图形渲染管线状态的属性,如图5-2所示。
MTLRenderPipelineDescriptor对象的colorAttachments属性包含一个MTLRenderPipelineColorAttachmentDescriptor对象数组,每个描述符代表一个颜色附件状态,该状态指定该附件的混合操作和因子,如在渲染管线附件描述符中配置混合中所述。
附件描述符还指定附件的像素格式,该格式必须与具有相应附件索引的渲染管线描述符的纹理的像素格式匹配,否则会发生错误。


图 5-2 从描述符创建渲染管道状态

在这里插入图片描述


除了配置颜色附件之外,还要设置MTLRenderPipelineDescriptor对象的以下属性:

  • 设置depthAttachmentPixelFormat属性来匹配MTLRenderPassDescriptor中的depthAttachment纹理的像素格式。
  • 设置stencilAttachmentPixelFormat属性来匹配MTLRenderPassDescriptor中的stencilAttachment纹理的像素格式。
  • 要在渲染管道状态中指定顶点或片段着色器,请分别设置vertexFunctionfragmentFunction属性。
    设置fragmentFunctionnil,将禁用将像素光栅化到指定的颜色附件中,这通常用于仅深度渲染或将数据从顶点着色器输出到缓冲区对象中。
  • 如果顶点着色器具有带有每个顶点输入属性的参数,则设置vertexDescriptor属性来描述该参数中顶点数据的组织,如数据组织的顶点描述符中所述。
  • rasterizationEnabled属性的默认值YES,足以满足大多数典型渲染任务的需求。
    若要仅使用图形管道的顶点阶段(例如,收集顶点着色器中转换的数据),请将此属性设置为NO
  • 如果附件支持多重采样(即附件是MTLTextureType2DMultisample纹理类型),则可以为每个像素创建多个样本。
    要确定片段如何组合以提供像素覆盖,请使用以下MTLRenderPipelineDescriptor属性。
    • sampleCount属性决定每个像素的采样数。
      创建MTLRenderCommandEncoder时,所有附件的纹理的sampleCount 都必须与sampleCount属性匹配。
      如果附件不支持多重采样,则sampleCount为1,这也是默认值。
    • 如果alphaToCoverageEnabled设置为YES,则读取colorAttachments[0]的alpha通道片段输出并用于确定覆盖掩码。
    • 如果alphaToOneEnabled设置为YES,则 的 alpha 通道片段值colorAttachments[0]将被强制为 1.0,这是可表示的最大值。(其他附件不受影响。)

从描述符创建渲染管道状态

创建渲染管道描述符并指定其属性后,使用它来创建MTLRenderPipelineState对象。
由于创建渲染管道状态可能需要对图形状态进行昂贵的评估,并且可能需要编译指定的图形着色器,因此您可以使用阻塞或异步方法以最适合您应用设计的方式安排此类工作。

  • 要同步创建渲染管道状态对象,请调用 MTLDevice 对象的newRenderPipelineStateWithDescriptor:error:newRenderPipelineStateWithDescriptor:options:reflection:error: 方法。
    这些方法会阻止当前线程,同时 Metal 会评估描述符的图形状态信息并编译着色器代码以创建管道状态对象。
  • 要异步创建渲染管道状态对象,请调用MTLDevice对象的newRenderPipelineStateWithDescriptor:completionHandler:newRenderPipelineStateWithDescriptor:options:completionHandler: 方法。
    这些方法会立即返回 - Metal 会异步评估描述符的图形状态信息并编译着色器代码以创建管道状态对象,然后调用完成处理程序来提供新MTLRenderPipelineState对象。

创建MTLRenderPipelineState对象时,您还可以选择创建反射数据,以揭示管道着色器函数及其参数的详细信息。
newRenderPipelineStateWithDescriptor:options:reflection:error:newRenderPipelineStateWithDescriptor:options:completionHandler: 方法提供此数据。
如果不使用反射数据,请避免获取它。
有关如何分析反射数据的更多信息,请参阅 在运行时确定函数详细信息。

创建MTLRenderPipelineState对象后,调用MTLRenderCommandEncodersetRenderPipelineState:方法 将渲染管道状态与命令编码器关联起来以用于渲染。

例 5-5演示了如何创建一个名为 pipeline 的渲染管道状态对象。


例 5-5 创建一个简单的管道状态

MTLRenderPipelineDescriptor *renderPipelineDesc =
                             [[MTLRenderPipelineDescriptor alloc] init];
renderPipelineDesc.vertexFunction = vertFunc;
renderPipelineDesc.fragmentFunction = fragFunc;
renderPipelineDesc.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm;
 
// Create MTLRenderPipelineState from MTLRenderPipelineDescriptor
NSError *errors = nil;
id <MTLRenderPipelineState> pipeline = [device
         newRenderPipelineStateWithDescriptor:renderPipelineDesc error:&errors];
assert(pipeline && !errors);
 
// Set the pipeline state for MTLRenderCommandEncoder
[renderCE setRenderPipelineState:pipeline];

变量vertFuncfragFunc是着色器函数,它们被名为renderPipelineDesc 的渲染管道状态描述符的属性 指定。
调用MTLDevice对象的newRenderPipelineStateWithDescriptor:error:方法会同步使用管道状态描述符来创建渲染管道状态对象。
调用MTLRenderCommandEncodersetRenderPipelineState:方法 指定要与渲染命令编码器一起使用的MTLRenderPipelineState对象。

注意: 由于MTLRenderPipelineState创建对象的开销很大,因此每当您想要使用相同的图形状态时,都应该重复使用它。


在渲染管道附件描述符中配置混合

混合使用高度可配置的混合操作将片段函数(源)返回的输出与附件(目标)中的像素值混合。
混合操作确定源值和目标值如何与混合因子组合。

要配置颜色附件的混合,请设置以下MTLRenderPipelineColorAttachmentDescriptor属性:

  • 要启用混合,请设置blendingEnabledYES。默认情况下,混合是禁用的。
  • writeMask标识混合哪些颜色通道。默认值MTLColorWriteMaskAll允许混合所有颜色通道。
  • rgbBlendOperationalphaBlendOperation分别为 RGB 和 Alpha 片段数据的混合操作指定一个MTLBlendOperation值。
    这两个属性的默认值都是MTLBlendOperationAdd
  • sourceRGBBlendFactorsourceAlphaBlendFactordestinationRGBBlendFactordestinationAlphaBlendFactor 分配源和目标混合因子。

了解混合因子和操作

四个混合因子指的是一个恒定的混合颜色值:MTLBlendFactorBlendColorMTLBlendFactorOneMinusBlendColorMTLBlendFactorBlendAlphaMTLBlendFactorOneMinusBlendAlpha
调用 MTLRenderCommandEncodersetBlendColorRed:green:blue:alpha:方法 指定这些混合因子使用的恒定颜色和 alpha 值,如 固定功能状态操作中所述。

一些混合操作通过将源值乘以源MTLBlendFactor值(缩写为 SBF)、将目标值乘以目标混合因子(DBF),然后使用MTLBlendOperation值 指示的算术组合结果来组合片段值。
(如果混合操作是 MTLBlendOperationMinMTLBlendOperationMax,则忽略 SBF 和 DBF 混合因子。)
例如,MTLBlendOperationAdd对于rgbBlendOperationalphaBlendOperation属性,为 RGB 和 Alpha 值定义以下加法混合操作:

  • RGB = (Source.rgb * sourceRGBBlendFactor) + (Dest.rgb * destinationRGBBlendFactor)
  • Alpha = (Source.a * sourceAlphaBlendFactor) + (Dest.a * destinationAlphaBlendFactor)

在默认的混合行为中,源完全覆盖目标。
此行为相当于将sourceRGBBlendFactor和设置sourceAlphaBlendFactorMTLBlendFactorOne,将destinationRGBBlendFactordestinationAlphaBlendFactor设置为MTLBlendFactorZero
此行为在数学上表示为:

  • RGB = (源.rgb * 1.0) + (目标.rgb * 0.0)
  • A = (源.a * 1.0) + (目标.a * 0.0)

另一种常用的混合操作,其中源 alpha 定义目标颜色剩余的数量,可以用数学方式表示为:

  • RGB = (源.rgb * 1.0) + (目标.rgb * (1 - 源.a))
  • A = (源.a * 1.0) + (目标.a * (1 - 源.a))

使用自定义混合配置

例 5-6展示了自定义混合配置的代码,使用混合操作MTLBlendOperationAdd、源混合因子MTLBlendFactorOne和目标混合因子 MTLBlendFactorOneMinusSourceAlpha
colorAttachments[0] 是一个具有指定混合配置的属性的MTLRenderPipelineColorAttachmentDescriptor 对象。


例 5-6 指定自定义混合配置

MTLRenderPipelineDescriptor *renderPipelineDesc = 
                             [[MTLRenderPipelineDescriptor alloc] init];
renderPipelineDesc.colorAttachments[0].blendingEnabled = YES; 
renderPipelineDesc.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
renderPipelineDesc.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
renderPipelineDesc.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne;
renderPipelineDesc.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
renderPipelineDesc.colorAttachments[0].destinationRGBBlendFactor = 
       MTLBlendFactorOneMinusSourceAlpha;
renderPipelineDesc.colorAttachments[0].destinationAlphaBlendFactor = 
       MTLBlendFactorOneMinusSourceAlpha;

NSError *errors = nil;
id <MTLRenderPipelineState> pipeline = [device 
         newRenderPipelineStateWithDescriptor:renderPipelineDesc error:&errors];

4、为渲染命令编码器指定资源

本节讨论的MTLRenderCommandEncoder方法 指定用作顶点和片段着色器函数参数的资源,这些资源由 MTLRenderPipelineState 对象中的vertexFunctionfragmentFunction属性指定。
这些方法将着色器资源(缓冲区、纹理和采样器)分配给渲染命令编码器中相应的参数表索引(atIndex ),如图5-3所示。


图 5-3 渲染命令编码器的参数表

在这里插入图片描述


以下setVertex*方法将一个或多个资源分配给顶点着色器函数的相应参数。

  • setVertexBuffer:offset:atIndex:
  • setVertexBuffers:offsets:withRange:
  • setVertexTexture:atIndex:
  • setVertexTextures:withRange:
  • setVertexSamplerState:atIndex:
  • setVertexSamplerState:lodMinClamp:lodMaxClamp:atIndex:
  • setVertexSamplerStates:withRange:
  • setVertexSamplerStates:lodMinClamps:lodMaxClamps:withRange:

这些setFragment*方法类似地将一个或多个资源分配给片段着色器函数的相应参数。

  • setFragmentBuffer:offset:atIndex:
  • setFragmentBuffers:offsets:withRange:
  • setFragmentTexture:atIndex:
  • setFragmentTextures:withRange:
  • setFragmentSamplerState:atIndex:
  • setFragmentSamplerState:lodMinClamp:lodMaxClamp:atIndex:
  • setFragmentSamplerStates:withRange:
  • setFragmentSamplerStates:lodMinClamps:lodMaxClamps:withRange:

缓冲区参数表最多有 31 个条目,纹理参数表最多有 31 个条目,采样器状态参数表最多有 16 个条目。

Metal 着色语言源代码中指定资源位置的属性限定符必须与 Metal 框架方法中的参数表索引相匹配。
在例 5-7中,为顶点着色器定义了两个缓冲区(posBuftexCoordBuf),其索引分别为 0 和 1。


例 5-7 Metal Framework:为顶点函数指定资源

[renderEnc setVertexBuffer:posBuf offset:0 atIndex:0];
[renderEnc setVertexBuffer:texCoordBuf offset:0 atIndex:1];

在例 5-8中,函数签名具有与属性限定符buffer(0) 和 相对应的参数buffer(1)


例 5-8 Metal 着色语言:顶点函数参数与框架参数表索引匹配

vertex VertexOutput metal_vert(float4 *posData [[ buffer(0) ]],
                               float2 *texCoordData [[ buffer(1) ]])

类似地,在例 5-9中,为片段着色器定义了一个缓冲区、一个纹理和一个采样器(分别为fragmentColorBufshadeTexsampler),它们的索引均为 0。


例 5-9 Metal Framework:为片段函数指定资源

[renderEnc setFragmentBuffer:fragmentColorBuf offset:0 atIndex:0];
[renderEnc setFragmentTexture:shadeTex atIndex:0];
[renderEnc setFragmentSamplerState:sampler atIndex:0]; 

在例 5-10中,函数签名具有与属性限定符分别对应的参数buffer(0)texture(0)sampler(0)


例 5-10 Metal 着色语言:片段函数参数与框架参数表索引匹配

fragment float4 metal_frag(VertexOutput in [[stage_in]],
                           float4 *fragColorData [[ buffer(0) ]],
                           texture2d<float> shadeTexValues [[ texture(0) ]],
                           sampler samplerValues [[ sampler(0) ]] )


数据组织的顶点描述符

在 Metal 框架代码中,每个管道状态可以有一个MTLVertexDescriptor,用于描述输入到顶点着色器函数的数据的组织,并在着色语言和框架代码之间共享资源位置信息。

在Metal着色语言代码中,每个顶点的输入(如标量或整数或浮点值的矢量)可以组织在一个结构中,该结构可以在一个用 [[stage_in]] 属性限定符声明的参数中传递,如清单5-11中示例顶点函数 vertex MathVertexInput 结构所示。逐顶点输入结构的每个字段都有 [[attribute(index)]] 限定符,该限定符指定顶点属性参数表中的索引。


例 5-11 Metal 着色语言:带有属性索引的顶点函数输入

struct VertexInput {
    float2    position [[ attribute(0) ]];
    float4    color    [[ attribute(1) ]];
    float2    uv1      [[ attribute(2) ]];
    float2    uv2      [[ attribute(3) ]];
};

struct VertexOutput {
    float4 pos [[ position ]];
    float4 color;
};

vertex VertexOutput vertexMath(VertexInput in [[ stage_in ]])
{
  VertexOutput out;
  out.pos = float4(in.position.x, in.position.y, 0.0, 1.0);

  float sum1 = in.uv1.x + in.uv2.x;
  float sum2 = in.uv1.y + in.uv2.y;
  out.color = in.color + float4(sum1, sum2, 0.0f, 0.0f);
  return out;
}

要使用[[ stage_in ]]限定符引用着色器函数输入,描述一个MTLVertexDescriptor对象,然后将其设置为 vertexDescriptor的属性MTLRenderPipelineState
MTLVertexDescriptor有两个属性:attributeslayouts

attributes的属性 MTLVertexDescriptor是一个MTLVertexAttributeDescriptorArray对象,它定义每个顶点属性在映射到顶点函数参数的缓冲区中的组织方式。
attributes属性可以支持对同一缓冲区内交错的多个属性(如顶点坐标、表面法线和纹理坐标)的访问。
着色语言代码中的成员顺序不必在框架代码中的缓冲区中保留。
数组中的每个顶点属性描述符都具有以下属性,它们为顶点着色器函数提供信息以定位和加载参数数据:

  • bufferIndex,它是缓冲区参数表的索引,指定要访问哪个MTLBuffer参数。
    缓冲区参数表在为渲染命令编码器指定资源中讨论。
  • format,它指定了数据在框架代码中的解释方式。
    如果数据类型不是精确匹配的类型,则可能会进行转换或扩展。
    例如,如果着色语言类型为half4 ,而框架formatMTLVertexFormatFloat2,则当数据用作顶点函数的参数时,可能会从浮点数转换为半值,并从两个元素扩展为四个元素(最后两个元素为 0.0、1.0)。
  • offset,指定从顶点的起点到终点可以找到数据的位置。

图 5-4说明了Metal 框架代码中 MTLVertexAttributeDescriptorArray 实现的交叉缓冲区,该缓冲区对应于例 5-11中的着色语言代码中的 顶点函数的输入。
vertexMath


图 5-4 带有顶点属性描述符的缓冲区组织

在这里插入图片描述


例 5-12展示了与图 5-4所示的交叉缓冲区相对应的 Metal 框架代码。


例 5-12 Metal 框架:使用顶点描述符访问交叉数据

id <MTLFunction> vertexFunc = [library newFunctionWithName:@"vertexMath"];            
MTLRenderPipelineDescriptor* pipelineDesc =      
                             [[MTLRenderPipelineDescriptor alloc] init];
MTLVertexDescriptor* vertexDesc = [[MTLVertexDescriptor alloc] init];

vertexDesc.attributes[0].format = MTLVertexFormatFloat2;
vertexDesc.attributes[0].bufferIndex = 0;
vertexDesc.attributes[0].offset = 0;
vertexDesc.attributes[1].format = MTLVertexFormatFloat4;
vertexDesc.attributes[1].bufferIndex = 0;
vertexDesc.attributes[1].offset = 2 * sizeof(float);  // 8 bytes
vertexDesc.attributes[2].format = MTLVertexFormatFloat2;
vertexDesc.attributes[2].bufferIndex = 0;
vertexDesc.attributes[2].offset = 8 * sizeof(float);  // 32 bytes
vertexDesc.attributes[3].format = MTLVertexFormatFloat2;
vertexDesc.attributes[3].bufferIndex = 0;
vertexDesc.attributes[3].offset = 6 * sizeof(float);  // 24 bytes
vertexDesc.layouts[0].stride = 10 * sizeof(float);    // 40 bytes
vertexDesc.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;

pipelineDesc.vertexDescriptor = vertexDesc;
pipelineDesc.vertexFunction = vertFunc;

MTLVertexDescriptor对象attributes数组中的 每个MTLVertexAttributeDescriptor对象,都与着色器函数中VertexInput的索引结构成员相对应。
attributes[1].bufferIndex = 0 指定参数表中索引 0 处的缓冲区的用途。
(在此示例中,每个MTLVertexAttributeDescriptor都有相同的bufferIndex,因此每个都引用参数表中索引 0 处的相同顶点缓冲区。)
这些 offset值指定顶点内数据的位置,因此 attributes[1].offset = 2 * sizeof(float) 将相应数据的开头定位在缓冲区开头 8 个字节处。
这些 format值的选择 与着色器函数中的数据类型相匹配,因此 attributes[1].format = MTLVertexFormatFloat4 指定使用四个浮点值。

MTLVertexDescriptorlayouts属性是 一个MTLVertexBufferLayoutDescriptorArray
对于每个layouts中的MTLVertexBufferLayoutDescriptor,属性指定当 Metal 绘制图元时,如何从参数表中的相应MTLBuffer位置获取顶点和属性数据。
(有关绘制图元的更多信息,请参阅 绘制几何图元。)
MTLVertexBufferLayoutDescriptorstepFunction 属性确定是否为每个顶点、为一定数量的实例或仅一次获取属性数据。
如果stepFunction设置为获取一定数量实例的属性数据,则MTLVertexBufferLayoutDescriptorstepRate属性确定多少个实例。
stride属性指定两个顶点数据之间的距离(以字节为单位)。

图 5-5描述了与例 5-12中的代码相对应的MTLVertexBufferLayoutDescriptor
layouts[0] 指定如何从缓冲区参数表中相应的索引 0 处获取顶点数据。
layouts[0].stride 指定两个顶点数据之间的距离为 40 字节。
layouts[0].stepFunction 的值,MTLVertexStepFunctionPerVertex,指定在绘制时为每个顶点获取属性数据。
如果 stepRate的值为 MTLVertexStepFunctionPerInstance,则该属性决定获取属性数据的频率。
例如,如果stepRate为 1,则为每个实例获取数据;如果stepRate为 2,则每两个实例获取一次数据,依此类推。


图 5-5 带有顶点缓冲区布局描述符的缓冲区组织

在这里插入图片描述


5、执行固定功能渲染命令编码器操作

使用这些MTLRenderCommandEncoder方法设置固定功能图形状态值:

  • setViewport: 指定屏幕坐标中的区域,该区域是虚拟 3D 世界投影的目标。
    视口是 3D 的,因此它包含深度值;有关详细信息,请参阅 使用视口和像素坐标系。
  • setTriangleFillMode: 确定是否将三角形和三角形带图元光栅化为线条 ( MTLTriangleFillModeLines) 或填充三角形 ( MTLTriangleFillModeFill)。
    默认值为MTLTriangleFillModeFill
  • setCullMode:setFrontFacingWinding:一起使用来确定是否以及如何应用剔除。
    您可以使用剔除来移除某些几何模型上的隐藏表面,例如用填充三角形渲染的可定向球体。
    (如果表面的图元始终按顺时针或逆时针顺序绘制,则该表面是可定向的。)
    • setFrontFacingWinding:的值表示正面图元的顶点是按顺时针(MTLWindingClockwise),还是逆时针(MTLWindingCounterClockwise)绘制。默认值为MTLWindingClockwise
  • setCullMode:的值 决定是否执行剔除(如果禁用剔除则为 MTLCullModeNone)或剔除哪种类型的图元(MTLCullModeFrontMTLCullModeBack)。

使用以下MTLRenderCommandEncoder方法对固定功能状态 改变命令进行编码:

  • setScissorRect: : 指定一个 2D 剪切矩形。
    位于指定剪切矩形之外的片段将被丢弃。
  • setDepthStencilState:设置深度和模板测试状态,如深度和模板状态中所述。
  • setStencilReferenceValue: : 指定模板参考值。
  • setDepthBias:slopeScale:clamp: : 指定用于比较阴影图和片段着色器输出的深度值的调整。
  • setVisibilityResultMode:offset:确定是否监视是否有任何样本通过深度和模板测试。
    如果设置为MTLVisibilityResultModeBoolean,则如果有任何样本通过深度和模板测试,则会将非零值写入MTLRenderPassDescriptorvisibilityResultBuffer 属性指定的缓冲区,如创建渲染过程描述符中所述。
    您可以使用此模式执行遮挡测试。
    如果您绘制一个边界框并且没有样本通过,那么您可以得出结论,该边界框内的任何对象都被遮挡,因此不需要渲染。
  • setBlendColorRed:green:blue:alpha: : 指定常量混合颜色和 alpha 值,详情请参阅在渲染管道附件描述符中配置混合。

使用视口和像素坐标系

Metal 将其标准化设备坐标 (NDC) 系统定义为一个 2x2x1 立方体,其中心位于 (0, 0, 0.5)。
NDC 系统的 x 和 y 的左侧和底部分别指定为 -1。
NDC 系统的 x 和 y 的右侧和顶部分别指定为 +1。

视口指定从NDC到窗口坐标的变换。
Metal视口是一个通过MTLRenderCommandEncodersetViewport:方法指定的3D变换。
窗口坐标的原点在左上角。

在 Metal 中,像素中心偏移 (0.5, 0.5)。
例如,原点处的像素中心位于 (0.5, 0.5);其右侧相邻像素的中心为 (1.5, 0.5)。纹理也是如此。


执行深度和模板操作

深度和模板操作是您指定的片段操作,如下所示:

  1. 指定MTLDepthStencilDescriptor包含深度/模板状态设置的自定义对象。
    创建自定义MTLDepthStencilDescriptor对象 可能需要创建一个或两个适用于正面图元和背面图元的MTLStencilDescriptor对象。
  2. 通过调用具有深度/模板状态描述符 MTLDepthStencilStatenewDepthStencilStateWithDescriptor:方法,来创建一个MTLDepthStencilState对象。
  3. 要设置深度/模板状态,请调用MTLRenderCommandEncodersetDepthStencilState:的方法。
  4. 如果正在使用模板测试,请调用setStencilReferenceValue:以指定模板参考值。

如果启用了深度测试,渲染管道状态必须包含深度附件以支持写入深度值。
要执行模板测试,渲染管道状态必须包含模板附件。
要配置附件,请参阅 创建和配置渲染管道描述符。

如果您要定期更改深度/模板状态,那么您可能需要重用状态描述符对象,根据需要修改其属性值以创建更多状态对象。

注意: 要在着色器函数中从深度格式纹理中进行采样,请在着色器中实现采样操作,而无需使用MTLSamplerState

使用MTLDepthStencilDescriptor对象的属性如下,来设置深度和模板状态:

  • 要将深度值写入深度附件,请设置depthWriteEnabledYES
  • depthCompareFunction 指定如何执行深度测试。
    如果片段的深度值未通过深度测试,则丢弃该片段。
    例如,常用MTLCompareFunctionLess函数会导致距离观察者比(先前写入的)像素深度值更远的片段值未通过深度测试;也就是说,该片段被视为被较早的深度值遮挡。
  • frontFaceStencilbackFaceStencil属性 分别指定 面向正面和面向背面的图元的单独MTLStencilDescriptor对象。
    要对面向正面和面向背面的图元使用相同的模板状态,您可以将相同的状态分配MTLStencilDescriptorfrontFaceStencilbackFaceStencil属性。
    要明确禁用一面或两面的模板测试,请将相应属性设置为nil默认值。

无需显式禁用模板状态。
Metal 根据模板描述符是否配置为有效的模板操作来确定是否启用模板测试。

例 5-13 展示了创建和使用 MTLDepthStencilDescriptor 对象的示例,该对象随后与渲染命令编码器一起使用。
在此示例中,从深度/模板状态描述符的属性访问正面图元的模板状态frontFaceStencil
对于背面图元,模板测试被明确禁用。


例 5-13 创建和使用深度/模板描述符

MTLDepthStencilDescriptor *dsDesc = [[MTLDepthStencilDescriptor alloc] init];
if (dsDesc == nil)
     exit(1);   //  if the descriptor could not be allocated
dsDesc.depthCompareFunction = MTLCompareFunctionLess;
dsDesc.depthWriteEnabled = YES;
 
dsDesc.frontFaceStencil.stencilCompareFunction = MTLCompareFunctionEqual;
dsDesc.frontFaceStencil.stencilFailureOperation = MTLStencilOperationKeep;
dsDesc.frontFaceStencil.depthFailureOperation = MTLStencilOperationIncrementClamp;
dsDesc.frontFaceStencil.depthStencilPassOperation =
                          MTLStencilOperationIncrementClamp;
dsDesc.frontFaceStencil.readMask = 0x1;
dsDesc.frontFaceStencil.writeMask = 0x1;
dsDesc.backFaceStencil = nil;
id <MTLDepthStencilState> dsState = [device
                          newDepthStencilStateWithDescriptor:dsDesc];
 
[renderEnc setDepthStencilState:dsState];
[renderEnc setStencilReferenceValue:0xFF];

MTLStencilDescriptor 的以下属性定义了模板测试:

  • readMask是一个位掩码;GPU 计算此掩码与模板参考值和存储的模板值的按位与。
    模板测试是对结果掩码参考值和掩码存储值进行比较。
  • writeMask是一个位掩码,它限制哪些模板值可以通过模板操作写入模板附件。
  • stencilCompareFunction 指定如何对片段执行模板测试。
    在例 5-13中,模板比较函数为MTLCompareFunctionEqual,因此如果屏蔽参考值等于已存储在片段位置的屏蔽模板值,则模板测试通过。
  • stencilFailureOperationdepthFailureOperationdepthStencilPassOperation 分别指定针对三种不同的测试结果对模板附件中存储的模板值执行的操作:模板测试失败、模板测试通过但深度测试失败,或模板测试和深度测试均成功。
    在上例中,如果模板测试失败,模板值保持不变 ( MTLStencilOperationKeep),但如果模板测试通过,模板值会增加,除非模板值已经是最大可能值 ( MTLStencilOperationIncrementClamp)。

6、绘制几何基元

建立管道状态和固定功能状态后,您可以调用以下的MTLRenderCommandEncoder方法来绘制几何图元。
这些绘制方法引用资源(例如包含顶点坐标、纹理坐标、表面法线和其他数据的缓冲区),以使用您之前用 MTLRenderCommandEncoder建立的着色器函数和其他状态来执行管道。

  • drawPrimitives:vertexStart:vertexCount:instanceCount: 使用连续数组元素中的顶点数据渲染多个基元实例(instanceCount),从索引处的数组元素处的第一个顶点开始vertexStart,到 vertexStart + vertexCount - 1 索引处的数组元素处结束 。
  • drawPrimitives:vertexStart:vertexCount: 与前一种方法相同,但instanceCount为 1。
  • drawIndexedPrimities:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount: 使用MTLBuffer对象indexBuffer中指定的索引列表呈现多个基元实例(instanceCountindexCount 确定索引的数量。
    索引列表从indexBuffer中数据的indexBufferOffset字节偏移量的索引开始indexBufferOffset必须是由indexType确定的索引大小的倍数。
  • drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:与之前的方法类似,但instanceCount值为 1。

对于上面列出的每种图元渲染方法,第一个输入值使用其中一个MTLPrimitiveType值确定图元类型。
其他输入值确定使用哪些顶点来组装图元。
对于所有这些方法,输入instanceStart值 确定要绘制的第一个实例,instanceCount输入值确定要绘制的实例数。

如前所述,setTriangleFillMode: 确定三角形是渲染为填充还是线框,以及setCullMode:设置setFrontFacingWinding:确定 GPU 是否在渲染期间剔除三角形。
有关更多信息,请参阅 固定功能状态操作)。

当渲染点图元时,顶点函数的着色器语言代码必须提供属性[[ point_size ]],否则点大小未定义。

使用平面着色渲染三角形图元时,第一个顶点(也称为激发顶点)的属性用于整个三角形。
顶点函数的着色器语言代码必须提供插值限定符[[ flat ]]

有关所有 Metal 着色语言属性和限定符的详细信息,请参阅Metal 着色语言指南


7、结束渲染过程

要终止渲染过程,请在渲染命令编码器调用endEncoding
结束上一个命令编码器后,您可以创建任何类型的新命令编码器,以将其他命令编码到命令缓冲区中。


8、代码示例:绘制三角形

以下步骤(如例 5-14所示)描述了渲染三角形的基本过程。

  1. 创建一个MTLCommandQueue并使用它来创建一个MTLCommandBuffer
  2. 创建一个MTLRenderPassDescriptor : 指定附件的集合,作为命令缓冲区中编码渲染命令的目的地。
    在此示例中,仅设置并使用了第一个颜色附件。
    currentTexture变量 假设包含用于颜色附件的MTLTexture。)
    然后,MTLRenderPassDescriptor用于创建新的MTLRenderCommandEncoder
  3. 创建两个MTLBuffer对象,posBufcolBuf,并调用newBufferWithBytes:length:options: 将 顶点坐标和顶点颜色数据 posDatacolData 复制到缓冲存储中。
  4. 调用两次MTLRenderCommandEncodersetVertexBuffer:offset:atIndex:方法,来指定坐标和颜色。
    setVertexBuffer:offset:atIndex:方法的输入值atIndex 与顶点函数源代码中的buffer(atIndex)属性相对应。
  5. 在管线描述符中创建MTLRenderPipelineDescriptor并建立顶点和片段函数:
    • 创建一个带有progSrc源代码的MTLLibrary,假定它是一个包含 Metal 着色器源代码的字符串。
    • 然后调用MTLLibrarynewFunctionWithName:方法来创建MTLFunction vertFunc,代表所调用的hello_vertex函数,并创建MTLFunction fragFunc代表所调用的hello_fragment函数。
    • 最后,使用这些MTLFunction对象设置MTLRenderPipelineDescriptorvertexFunctionfragmentFunction属性。
  6. 通过调用 newRenderPipelineStateWithDescriptor:error: 或类似的MTLDevice方法 从MTLRenderPipelineDescriptor中创建MTLRenderPipelineState
    然后MTLRenderCommandEncodersetRenderPipelineState:方法 使用创建的管道状态 进行渲染。
  7. 调用MTLRenderCommandEncoderdrawPrimitives:vertexStart:vertexCount:方法 来附加命令来执行填充三角形(MTLPrimitiveTypeTriangle类型)的渲染。
  8. 调用endEncoding方法结束本次渲染过程的编码。
    并调用MTLCommandBuffercommit方法,在设备上执行命令。

例 5-14 绘制三角形的 Metal 代码

id <MTLDevice> device = MTLCreateSystemDefaultDevice();
 
id <MTLCommandQueue> commandQueue = [device newCommandQueue];
id <MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
 
MTLRenderPassDescriptor *renderPassDesc
                               = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDesc.colorAttachments[0].texture = currentTexture;
renderPassDesc.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.0,1.0,1.0,1.0);
id <MTLRenderCommandEncoder> renderEncoder =
           [commandBuffer renderCommandEncoderWithDescriptor:renderPassDesc];
 
static const float posData[] = {
        0.0f, 0.33f, 0.0f, 1.f,
        -0.33f, -0.33f, 0.0f, 1.f,
        0.33f, -0.33f, 0.0f, 1.f,
};
static const float colData[] = {
        1.f, 0.f, 0.f, 1.f,
        0.f, 1.f, 0.f, 1.f,
        0.f, 0.f, 1.f, 1.f,
};
id <MTLBuffer> posBuf = [device newBufferWithBytes:posData
        length:sizeof(posData) options:nil];
id <MTLBuffer> colBuf = [device newBufferWithBytes:colorData
        length:sizeof(colData) options:nil];
[renderEncoder setVertexBuffer:posBuf offset:0 atIndex:0];
[renderEncoder setVertexBuffer:colBuf offset:0 atIndex:1];
 
NSError *errors;
id <MTLLibrary> library = [device newLibraryWithSource:progSrc options:nil
                           error:&errors];
id <MTLFunction> vertFunc = [library newFunctionWithName:@"hello_vertex"];
id <MTLFunction> fragFunc = [library newFunctionWithName:@"hello_fragment"];
MTLRenderPipelineDescriptor *renderPipelineDesc
                                   = [[MTLRenderPipelineDescriptor alloc] init];
renderPipelineDesc.vertexFunction = vertFunc;
renderPipelineDesc.fragmentFunction = fragFunc;
renderPipelineDesc.colorAttachments[0].pixelFormat = currentTexture.pixelFormat;
id <MTLRenderPipelineState> pipeline = [device
             newRenderPipelineStateWithDescriptor:renderPipelineDesc error:&errors];
[renderEncoder setRenderPipelineState:pipeline];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
               vertexStart:0 vertexCount:3];
[renderEncoder endEncoding];
[commandBuffer commit];

在例 5-14中,MTLFunction对象表示名为 hello_vertex 的着色器函数。
MTLRenderCommandEncodersetVertexBuffer:offset:atIndex:方法 用于指定作为参数传递给 hello_vertex 的顶点资源(在本例中为两个缓冲区对象)。
setVertexBuffer:offset:atIndex: 方法的输入值 atIndex对应于顶点函数源代码中的buffer(atIndex)属性,如例 5-15 所示。


例 5-15 相应的着色器函数声明

vertex VertexOutput hello_vertex(
                    const global float4 *pos_data [[ buffer(0) ]],
                    const global float4 *color_data [[ buffer(1) ]])
{
    ...
}

9、使用多线程编码单个渲染过程

在某些情况下,应用的性能可能会受到单次渲染过程编码命令的单 CPU 工作负载的限制。
但是,尝试通过将工作负载分成在多个 CPU 线程上编码的多个渲染过程来规避此瓶颈也会对性能产生不利影响,因为每个渲染过程都需要自己的中间附件存储和加载操作来保留渲染目标内容。

相反,使用一个MTLParallelRenderCommandEncoder对象来管理共享相同命令缓冲区和渲染过程描述符的多个下级MTLRenderCommandEncoder对象。
并行渲染命令编码器可确保附件加载和存储操作仅发生在整个渲染过程的开始和结束时,而不是在每个下级渲染命令编码器的命令集的开始和结束时。
借助此架构,您可以以安全且高性能的方式将每个MTLRenderCommandEncoder对象并行分配给自己的线程。

要创建并行渲染命令编码器,请使用MTLCommandBuffer对象的 parallelRenderCommandEncoderWithDescriptor:方法。
要创建从属渲染命令编码器,请为要从中执行命令编码的每个 CPU 线程调用一次MTLParallelRenderCommandEncoder 对象的 renderCommandEncoder方法。
从同一并行渲染命令编码器创建的所有从属命令编码器都会将命令编码到同一命令缓冲区。
命令按照渲染命令编码器的创建顺序编码到命令缓冲区。
要结束特定渲染命令编码器的编码,请调用MTLRenderCommandEncoderendEncoding方法。
在结束并行渲染命令编码器创建的所有渲染命令编码器的编码后,调用MTLParallelRenderCommandEncoderendEncoding方法 结束渲染过程。

例 5-16显示了MTLParallelRenderCommandEncoder创建三个MTLRenderCommandEncoder对象:rCE1rCE2rCE3


例 5-16 具有三个渲染命令编码器的并行渲染编码器

MTLRenderPassDescriptor *renderPassDesc 
                     = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDesc.colorAttachments[0].texture = currentTexture;
renderPassDesc.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.0,0.0,0.0,1.0);

id <MTLParallelRenderCommandEncoder> parallelRCE = [commandBuffer 
                     parallelRenderCommandEncoderWithDescriptor:renderPassDesc];
id <MTLRenderCommandEncoder> rCE1 = [parallelRCE renderCommandEncoder];
id <MTLRenderCommandEncoder> rCE2 = [parallelRCE renderCommandEncoder];
id <MTLRenderCommandEncoder> rCE3 = [parallelRCE renderCommandEncoder];

//  not shown: rCE1, rCE2, and rCE3 call methods to encode graphics commands
//
//  rCE1 commands are processed first, because it was created first
//  even though rCE2 and rCE3 end earlier than rCE1
[rCE2 endEncoding];
[rCE3 endEncoding];
[rCE1 endEncoding];

//  all MTLRenderCommandEncoders must end before MTLParallelRenderCommandEncoder
[parallelRCE endEncoding];

命令编码器调用endEncoding的顺序 与命令编码 和附加到MTLCommandBuffer的顺序无关。
对于MTLParallelRenderCommandEncoderMTLCommandBuffer 始终按照下级渲染命令编码器的创建顺序包含命令,如图5-6 所示。


图 5-6 并行渲染过程中渲染命令编码器的排序

在这里插入图片描述


七、数据并行计算处理:计算命令编码器

本章介绍如何创建和使用MTLComputeCommandEncoder对象来编码数据并行计算处理状态和命令并提交它们以在设备上执行。

要执行数据并行计算,请遵循以下主要步骤:

  1. 使用MTLDevice方法创建包含对象编译代码的计算状态 (MTLComputePipelineState) MTLFunction,如创建计算状态中所述。
    MTLFunction对象表示使用 Metal 着色语言编写的计算函数,如函数和库中所述。
  2. 指定计算命令编码器要使用的MTLComputePipelineState对象,如指定计算命令编码器的计算状态和资源中所述。
  3. 指定资源和相关对象(MTLBufferMTLTexture和可能的MTLSamplerState),它们可能包含要由计算状态处理和返回的数据,如指定计算命令编码器的计算状态和资源中所述。
    还设置它们的参数表索引,以便 Metal 框架代码可以在着色器代码中找到相应的资源。
    在任何给定时刻, MTLComputeCommandEncoder都可以与多个资源对象相关联。
  4. 按照执行计算命令 中的说明,分派计算函数指定的次数。

1、创建计算管道状态

MTLFunction对象表示 可由MTLComputePipelineState对象执行的数据并行代码。
MTLComputeCommandEncoder对象对设置参数和执行计算函数的命令进行编码。
由于创建计算管道状态可能需要编译昂贵的 Metal 着色语言代码,因此您可以使用阻塞或异步方法以最适合您应用设计的方式安排此类工作。

  • 要同步创建计算管道状态对象,请调用MTLDevicenewComputePipelineStateWithFunction:error:newComputePipelineStateWithFunction:options:reflection:error:方法。
    当 Metal 编译着色器代码以创建管道状态对象时,这些方法会阻止当前线程。
  • 要异步创建计算管道状态对象,请调用 MTLDevicenewComputePipelineStateWithFunction:completionHandler:newComputePipelineStateWithFunction:options:completionHandler:方法。
    这些方法会立即返回 — Metal 异步编译着色器代码以创建管道状态对象,然后调用完成处理程序来提供新MTLComputePipelineState对象。

创建MTLComputePipelineState对象时,您还可以选择创建反射数据,以揭示计算函数及其参数的详细信息。
newComputePipelineStateWithFunction:options:reflection:error:newComputePipelineStateWithFunction:options:completionHandler:方法提供此数据。
如果不使用反射数据,请避免获取它。
有关如何分析反射数据的更多信息,请参阅 在运行时确定函数详细信息。


2、为计算命令编码器 指定计算状态和资源

MTLComputeCommandEncoder 对象的setComputePipelineState:方法指定 用于数据并行计算过程的状态(包括已编译的计算着色器函数)。
在任何给定时刻,计算命令编码器只能与一个计算函数相关联。

以下MTLComputeCommandEncoder方法指定一个资源(即缓冲区、纹理、采样器状态或线程组内存),该资源用作 MTLComputePipelineState 对象所表示的计算函数的参数。

  • setBuffer:offset:atIndex:
  • setBuffers:offsets:withRange:
  • setTexture:atIndex:
  • setTextures:withRange:
  • setSamplerState:atIndex:
  • setSamplerState:lodMinClamp:lodMaxClamp:atIndex:
  • setSamplerStates:withRange:
  • setSamplerStates:lodMinClamps:lodMaxClamps:withRange:
  • setThreadgroupMemoryLength:atIndex:

每种方法将一个或多个资源分配给相应的参数,如图6-1所示。


图 6-1 计算命令编码器的参数表

在这里插入图片描述


缓冲区、纹理或采样器状态参数表中的最大条目数限制在实施限制表中列出。

实施限制表中还列出了最大总线程组内存分配的限制。


3、执行计算命令

要对命令进行编码以执行计算函数,请调用 MTLComputeCommandEncoderdispatchThreadgroups:threadsPerThreadgroup:方法 并指定线程组维度和线程组数量。
您可以查询 MTLComputePipelineStatethreadExecutionWidthmaxTotalThreadsPerThreadgroup属性以优化此设备上计算函数的执行。

线程组中的线程总数是以下组件的乘积threadsPerThreadgroupthreadsPerThreadgroup.width * threadsPerThreadgroup.height * threadsPerThreadgroup.depth
maxTotalThreadsPerThreadgroup 属性指定单个线程组中可在设备上执行此计算功能的最大线程数。

计算命令按照它们被编码到命令缓冲区的顺序执行。
当与命令相关的所有线程组都完成执行并且所有结果都写入内存时,计算命令就完成执行。
由于这种排序,命令缓冲区中在它之后编码的任何命令都可以使用计算命令的结果。

要结束计算命令编码器的编码命令,请调用MTLComputeCommandEncoderendEncoding方法。
结束前一个命令编码器后,您可以创建任何类型的新命令编码器,以将其他命令编码到命令缓冲区中。


4、代码示例:执行数据并行函数

例 6-1示出了创建并使用MTLComputeCommandEncoder对象 对指定数据执行图像变换的并行计算的示例。(此示例未显示如何创建和初始化设备、库、命令队列和资源对象。)
此示例创建一个命令缓冲区,然后使用它创建MTLComputeCommandEncoder对象。接下来,将创建一个MTLFunction对象,该对象表示从MTLLibrary对象加载的入口点filter_main,如[清单6-2]所示(https://developer.apple.com/library/archive/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Compute-Ctx/Compute-Ctx.html#//apple_ref/doc/uid/TP40014221-CH6-SW5)。
然后,函数对象用于创建名为filterStateMTLComputePipelineState对象。

计算函数对图像inputImage执行图像转换和过滤操作,结果返回outputImage
首先,setTexture:atIndex:setBuffer:offset:atIndex:方法将纹理和缓冲区对象分配给指定参数表中的索引paramsBuffer指定用于执行图像转换的值,inputTableData指定过滤器权重。计算函数作为二维线程组执行,每个维度的大小为16 x 16像素。dispatchThreadgroups:threadsPerThreadgroup:方法将命令排入队列以调度执行计算函数的线程,endEncoding方法终止MTLComputeCommandEncoder
最后,MTLCommandBuffercommit方法会使命令尽快执行。


例 6-1 在计算状态下指定并运行函数

id <MTLDevice> device;
id <MTLLibrary> library;
id <MTLCommandQueue> commandQueue;
 
id <MTLTexture> inputImage;
id <MTLTexture> outputImage;
id <MTLTexture> inputTableData;
id <MTLBuffer> paramsBuffer;
 
// ... Create and initialize device, library, queue, resources
 
// Obtain a new command buffer
id <MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
 
// Create a compute command encoder
id <MTLComputeCommandEncoder> computeCE = [commandBuffer computeCommandEncoder];
 
NSError *errors;
id <MTLFunction> func = [library newFunctionWithName:@"filter_main"];
id <MTLComputePipelineState> filterState
              = [device newComputePipelineStateWithFunction:func error:&errors];
[computeCE setComputePipelineState:filterState];
[computeCE setTexture:inputImage atIndex:0];
[computeCE setTexture:outputImage atIndex:1];
[computeCE setTexture:inputTableData atIndex:2];
[computeCE setBuffer:paramsBuffer offset:0 atIndex:0];
 
MTLSize threadsPerGroup = {16, 16, 1};
MTLSize numThreadgroups = {inputImage.width/threadsPerGroup.width,
                           inputImage.height/threadsPerGroup.height, 1};
 
[computeCE dispatchThreadgroups:numThreadgroups
                                threadsPerThreadgroup:threadsPerGroup];
[computeCE endEncoding];
 
// Commit the command buffer
[commandBuffer commit]; 

例 6-2展示了上述示例对应的着色器代码。
(函数read_and_transformfilter_table是用户定义代码的占位符)。


例 6-2 着色语言计算函数声明

kernel void filter_main(
  texture2d<float,access::read>   inputImage   [[ texture(0) ]],
  texture2d<float,access::write>  outputImage  [[ texture(1) ]],
  uint2 gid                                    [[ thread_position_in_grid ]],
  texture2d<float,access::sample> table        [[ texture(2) ]],
  constant Parameters* params                  [[ buffer(0) ]]
  )
{
  float2 p0          = static_cast<float2>(gid);
  float3x3 transform = params->transform;
  float4   dims      = params->dims;
  
  float4 v0 = read_and_transform(inputImage, p0, transform);
  float4 v1 = filter_table(v0,table, dims);
  
  outputImage.write(v1,gid);
}

八、缓冲区和纹理操作:Blit 命令编码器

MTLBlitCommandEncoder提供在资源(缓冲区和纹理)之间复制数据的方法。
数据复制操作对于图像处理和纹理效果(例如模糊或反射)可能是必要的。
它们可用于访问屏幕外渲染的图像数据。

要执行数据复制操作,首先通过调用MTLCommandBufferblitCommandEncoder方法创建一个MTLBlitCommandEncoder对象。
然后调用下面描述的MTLBlitCommandEncoder方法将命令编码到命令缓冲区上。


1、在资源对象之间复制 GPU 内存中的数据

以下MTLBlitCommandEncoder方法在资源对象之间 复制图像数据:两个缓冲区对象之间、两个纹理对象之间以及缓冲区和纹理之间。


在两个缓冲区之间复制数据

copyFromBuffer:sourceOffset:toBuffer:destinationOffset:size: 方法在两个缓冲区之间复制数据:从源缓冲区复制到toBuffer目标缓冲区。
如果源和目标是同一个缓冲区,并且复制的范围重叠,则结果不确定。


将数据从缓冲区复制到纹理

copyFromBuffer:sourceOffset:sourceBytesPerRow:sourceBytesPerImage:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin: 方法将图像数据从源缓冲区复制到toTexture目标纹理。


在两个纹理之间复制数据

copyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin: 方法 在两个纹理之间复制图像数据区域:从源纹理的单个立方体切片 和 mipmap 级别到目标纹理toTexture


将数据从纹理复制到缓冲区

copyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toBuffer:destinationOffset:destinationBytesPerRow:destinationBytesPerImage:方法 将来自源纹理的单个立方体切片和 mipmap 级别的图像数据区域复制到目标缓冲区toBuffer


2、生成 Mipmap

从基础级别纹理图像开始,MTLBlitCommandEncodergenerateMipmapsForTexture:方法 为给定纹理自动生成 mipmap 。
generateMipmapsForTexture: 为所有 mipmap 级别创建缩放图像,直至最高级别。

有关如何确定 mipmap 数量以及每个 mipmap 大小的详细信息,请参阅 切片。


3、填充缓冲区的内容

MTLBlitCommandEncoderfillBuffer:range:value:方法,将8位常数值存储在 给定缓冲器的 指定范围内的 每个字节中。


4、结束 Blit 命令编码器的编码

要结束 blit 命令编码器的编码命令,请调用endEncoding
结束前一个命令编码器后,您可以创建任何类型的新命令编码器,以将其他命令编码到命令缓冲区中。


九、Metal 工具

本章列出了可帮助您定制和改进开发工作流程的工具。


1、在应用程序构建过程中创建库

在应用构建过程中编译着色器语言源文件并构建库(.metallib文件)比在运行时编译着色器源代码可实现更好的应用性能。
您可以在 Xcode 中或使用命令行实用程序构建库。


使用 Xcode 构建库

您的项目中的任何着色器源文件都会自动用于生成默认库,您可以使用 MTLDevice库的newDefaultLibrary方法 从 Metal 框架代码访问。


使用命令行实用程序构建库

图 8-1显示了构成 Metal 着色器源代码的编译器工具链的命令行实用程序。
当您在项目中包含.metal文件时,Xcode 会调用这些工具来构建一个库文件,您可以在运行时在应用程序中访问该文件。

要在不使用 Xcode 的情况下将着色器源代码编译成库:

  1. 使用metal工具将每个.metal文件 编译为单个.air文件,该文件存储着色器语言代码的中间表示 (IR)。
  2. 或者,使用metal-ar工具将多个.air文件存档为一个.metalar文件。(metal-ar类似于 Unix ar。)
  3. 使用metallib 工具从 IR文件 .air 或档案文件构建Metal 库.metallib文件。

图 8-1 使用命令行实用程序构建库文件

在这里插入图片描述


[例 8-1] 显示了编译.metal和构建一个.metallib文件所需的最少命令数。


例 8-1 使用命令行实用程序构建库文件

crun -sdk macosx metal MyLibrary.metal -o MyLibrary.air
xcrun -sdk macosx metallib MyLibrary.air -o MyLibrary.metallib

要在框架代码中访问生成的库,请调用 newLibraryWithFile:error:方法,如[例8-2]所示。


例 8-2 在应用程序中访问库文件

NSError *libraryError = NULL;
NSString *libraryFile = [[NSBundle mainBundle] pathForResource:@"MyLibrary" ofType:@"metallib"];

id <MTLLibrary> myLibrary = [_device newLibraryWithFile:libraryFile error:&libraryError];
if (!myLibrary) {
    NSLog(@"Library error: %@", libraryError);
}

2、Xcode Scheme 设置和性能

当 Metal 应用从 Xcode 运行时,默认方案设置会降低性能。
Xcode 会检测源代码中是否使用了 Metal API,并自动启用 GPU 帧捕获和 Metal API 验证设置,如图8-2所示。
启用 GPU 帧捕获后,将激活调试层。
启用 Metal API 验证后,将验证每个调用,这会进一步影响性能。
对于这两种设置,CPU 性能比 GPU 性能受到的影响更大。
除非您禁用这些设置,否则当应用在 Xcode 之外运行时,应用性能可能会明显提高。


图 8-2 Metal 应用的 Xcode Scheme 编辑器设置

在这里插入图片描述


3、调试

在调试和分析 Metal 应用时,使用以下部分中的提示可获取更多有用的诊断信息。

*注意: 仅当您的 Xcode 项目的 部署目标设置为最新的 SDK 时,才会启用调试。


Metal 着色语言源文件的文件扩展名

对于 Metal 着色语言源代码文件名,必须使用.metal文件扩展名以确保开发工具(Xcode 和 GPU 帧调试器)在调试或分析时识别源文件。


使用 Xcode 执行帧捕获

要在 Xcode 中执行帧捕获,请启用调试并调用 MTLCommandQueueinsertDebugCaptureBoundary 方法来通知 Xcode。
MTLCommandBufferpresentDrawable:presentDrawable:atTime:方法 同样会通知 Xcode 有关帧捕获的信息,因此仅在这些方法不存在时才调用insertDebugCaptureBoundary
有关更多信息,请参阅 调试 Metal 和 OpenGL ES。


Label 属性

许多 Metal 框架对象(例如命令缓冲区、管道状态和资源)都支持label属性。
您可以使用此属性为每个对象分配一个在应用程序设计上下文中有意义的名称。
这些标签显示在 Xcode Frame Capture 调试界面中,让您更轻松地识别对象。

类似地,insertDebugSignpost:pushDebugGroup:popDebugGroup方法 允许您将调试字符串 插入命令缓冲区,并推送或弹出用于识别编码命令组的字符串标签。


Metal System Trace

要在 Instruments 中分析应用,请运行 Metal System Trace 工具。
有关更多信息,请参阅 Metal System Trace 分析模板。


十、Metal 功能集表

Metal 功能集描述了特定 Metal 设备的功能可用性、实现限制和像素格式功能。
MTLFeatureSet每个功能集对应于特定的 GPU 和操作系统,如MTLFeatureSet参考中所述。
如需更多信息,请参阅下表:

  • 功能可用性
  • 实施限制
  • 像素格式功能

十一、iOS 9 和 OS X 10.11 的新功能

本章总结了 iOS 9 和 OS X 10.11 中引入的新功能。


1、功能集

所有支持 Metal 的设备 都符合例 10-1中描述的功能集值。


例 10-1 Metal 功能集

typedef NS_ENUM(NSUInteger, MTLFeatureSet)
{
    MTLFeatureSet_iOS_GPUFamily1_v1 = 0,
    MTLFeatureSet_iOS_GPUFamily2_v1 = 1,
    MTLFeatureSet_iOS_GPUFamily1_v2 = 2,
    MTLFeatureSet_iOS_GPUFamily2_v2 = 3,
    MTLFeatureSet_iOS_GPUFamily3_v1 = 4,
    MTLFeatureSet_OSX_GPUFamily1_v1 = 10000
};

所有支持 Metal 的 OS X 设备都支持OSX_GPUFamily1_v1功能集。

支持 Metal 的 iOS 设备支持的功能集由其 GPU 和操作系统版本决定。
有关更多信息,请参阅MTLFeatureSet参考 和 iOS 设备兼容性参考

要了解设备支持哪些功能集,请查询MTLDevice对象的supportsFeatureSet:方法。


2、设备选择

使用MTLCreateSystemDefaultDevice函数 获取应用的首选 GPU。
OSX_GPUFamily1_v1功能集设备可能有多个 GPU,您可以通过调用MTLCopyAllDevices函数获取。
要进一步获取多 GPU 系统中单个 GPU 的特性,请查询headless属性以查明 GPU 是否连接到显示器,并查询lowPower属性以在自动图形切换系统中查找功率较低的 GPU。

您可以 添加的新的MTLDevice协议中的supportsTextureSampleCount:方法 和maxThreadsPerThreadgroup属性 查询特定的渲染 和 计算特性。


3、资源存储模式和设备内存模型

OSX_GPUFamily1_v1功能集 包括对包含离散内存的 GPU 上的资源管理的支持。
资源内存分配通过从列表 10-2枚举(对于纹理)和列表 10-3枚举(对于缓冲区)中选择适当的存储模式值来明确处理。


例 10-2 纹理存储模式

typedef NS_ENUM(NSUInteger, MTLStorageMode)
{
    MTLStorageModeShared  = 0,
    MTLStorageModeManaged = 1,
    MTLStorageModePrivate = 2,
};

例 10-3 缓冲区存储模式

#define MTLResourceStorageModeShift  4
 
typedef NS_ENUM(NSUInteger, MTLResourceOptions)
{
    MTLResourceStorageModeShared  = MTLStorageModeShared  << MTLResourceStorageModeShift,
    MTLResourceStorageModeManaged = MTLStorageModeManaged << MTLResourceStorageModeShift,
    MTLResourceStorageModePrivate = MTLStorageModePrivate << MTLResourceStorageModeShift,
};

iOS 功能集仅支持共享和私有存储模式。
以下小节总结了三种新的资源存储模式的描述。


共享

使用共享存储模式分配的资源存储在 CPU 和 GPU 均可访问的内存中。
在具有独立内存的设备上,这些资源直接从 CPU 本地内存访问,而不是复制到 GPU 内存。

在 iOS 功能集中,这是纹理的默认存储模式。
OSX_GPUFamily1_v1功能集中,纹理不能使用共享存储模式进行分配。


私人的

使用私有存储模式分配的资源存储在仅 GPU 可访问的内存中。

分配有私有存储模式的缓冲区和纹理只能由 GPU 访问。
contents缓冲区的属性返回NULL,并且MTLTexture下面列出的方法是非法调用的:

  • replaceRegion:mipmapLevel:withBytes:bytesPerRow:
  • replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:
  • getBytes:bytesPerRow:fromRegion:mipmapLevel:
  • getBytes:bytesPerRow:bytesPerImage:fromRegion:mipmapLevel:slice:

要访问私有资源,你的应用可能会执行以下一项或多项操作:

  • 将数据块传送到私有资源或从私有资源传送数据块。
  • 从任何着色器函数中读取私有资源。
  • 从片段着色器渲染到私有资源。
  • 从计算函数写入私有资源。

管理

在没有独立内存的 GPU 上,托管资源只有一个可供 CPU 和 GPU 访问的内存分配。
在具有独立内存的 GPU 上,托管资源在内部分配可供 CPU 访问的内存和可供 GPU 访问的内存。

托管纹理在 iOS 功能集中不可用;请改用 MTLStorageModeShared

任何时候您的应用使用 CPU 直接修改缓冲区的内容,您都必须调用didModifyRange:方法通知 Metal 您已更改特定范围指示的资源的内容。

如果您使用 GPU 修改托管资源的内容,并且希望使用 CPU 访问结果,那么您必须首先使用 synchronizeResource: 方法 或 synchronizeTexture:slice:level:方法同步资源。


选择资源存储模式

一般来说,选择存储模式主要考虑四种场景:

  • 选择MTLResourceStorageModePrivate仅由 GPU 读取和/或写入的资源。
    例如,渲染目标纹理(一种特别常见且重要的情况)。
  • 对于一次初始化并且将来多次使用的资源,请选择MTLResourceStorageModeManaged或。
    MTLResourceStorageModePrivate
  • 选择MTLResourceStorageModeManaged由 CPU 每帧填充然后由 GPU 读取的资源。
    例如着色器常量缓冲区或动态顶点数据。
  • 选择MTLResourceStorageModeShared由 CPU 写入然后位块传输到 GPU 内存的资源(或者,CPU 从 GPU 内存读取)。
    例如 — 暂存缓冲区。

设置和查询资源存储模式

对于纹理,使用MTLTextureDescriptor对象的 storageMode属性 设置所需的存储模式。

在 iOS 功能集中,纹理的默认存储模式是MTLStorageModeShared。在OSX_GPUFamily1_v1功能集中是MTLStorageModeManaged

对于缓冲区,通过在MTLDevicenewBufferWithLength:options:newBufferWithBytes:length:options:newBufferWithBytesNoCopy:length:options:deallocator: 方法中的任意一个中 传递相应的MTLResourceOptions值来设置所需的存储模式。
缓冲区的默认存储模式为MTLStorageModeShared,但OSX_GPUFamily_v1功能集应用程序可以通过使用托管或专用存储模式显式管理其缓冲区,从而提高性能。

可以使用MTLResource对象的storageMode属性 来查询资源的存储模式(纹理或缓冲区)。


4、纹理

硬件对不同纹理类型和像素格式的支持是功能集之间的主要功能差异。
本节列出了框架的主要纹理添加;有关更详细的讨论,请参阅 MTLPixelFormat 参考和 Metal 功能集表 章节中的代码示例和比较表。


压缩纹理

iOS_GPUFamily2_v1iOS_GPUFamily2_v2iOS_GPUFamily3_v1功能集增加了对 ASTC 纹理的支持。
例 10-4 列出了这些像素格式。


例 10-4 ASTC 像素格式

typedef NS_ENUM(NSUInteger, MTLPixelFormat)
{
    // ASTC
    MTLPixelFormatASTC_4x4_sRGB      = 186,
    MTLPixelFormatASTC_5x4_sRGB      = 187,
    MTLPixelFormatASTC_5x5_sRGB      = 188,
    MTLPixelFormatASTC_6x5_sRGB      = 189,
    MTLPixelFormatASTC_6x6_sRGB      = 190,
    MTLPixelFormatASTC_8x5_sRGB      = 192,
    MTLPixelFormatASTC_8x6_sRGB      = 193,
    MTLPixelFormatASTC_8x8_sRGB      = 194,
    MTLPixelFormatASTC_10x5_sRGB     = 195,
    MTLPixelFormatASTC_10x6_sRGB     = 196,
    MTLPixelFormatASTC_10x8_sRGB     = 197,
    MTLPixelFormatASTC_10x10_sRGB    = 198,
    MTLPixelFormatASTC_12x10_sRGB    = 199,
    MTLPixelFormatASTC_12x12_sRGB    = 200,
    MTLPixelFormatASTC_4x4_LDR       = 204,
    MTLPixelFormatASTC_5x4_LDR       = 205,
    MTLPixelFormatASTC_5x5_LDR       = 206,
    MTLPixelFormatASTC_6x5_LDR       = 207,
    MTLPixelFormatASTC_6x6_LDR       = 208,
    MTLPixelFormatASTC_8x5_LDR       = 210,
    MTLPixelFormatASTC_8x6_LDR       = 211,
    MTLPixelFormatASTC_8x8_LDR       = 212,
    MTLPixelFormatASTC_10x5_LDR      = 213,
    MTLPixelFormatASTC_10x6_LDR      = 214,
    MTLPixelFormatASTC_10x8_LDR      = 215,
    MTLPixelFormatASTC_10x10_LDR     = 216,
    MTLPixelFormatASTC_12x10_LDR     = 217,
    MTLPixelFormatASTC_12x12_LDR     = 218,
}; 

OSX_GPUFamily1_v1功能集改为支持 BC 纹理。新的像素格式列于例 10-5中。


例 10-5 BC 像素格式

typedef NS_ENUM(NSUInteger, MTLPixelFormat)
{
    // BC1, BC2, BC3 (aka S3TC/DXT)
    MTLPixelFormatBC1_RGBA           = 130,
    MTLPixelFormatBC1_RGBA_sRGB      = 131,
    MTLPixelFormatBC2_RGBA           = 132,
    MTLPixelFormatBC2_RGBA_sRGB      = 133,
    MTLPixelFormatBC3_RGBA           = 134,
    MTLPixelFormatBC3_RGBA_sRGB      = 135,
 
    // BC4, BC5 (aka RGTC)
    MTLPixelFormatBC4_RUnorm         = 140,
    MTLPixelFormatBC4_RSnorm         = 141,
    MTLPixelFormatBC5_RGUnorm        = 142,
    MTLPixelFormatBC5_RGSnorm        = 143,
 
    // BC6H, BC7 (aka BPTC)
    MTLPixelFormatBC6H_RGBFloat      = 150,
    MTLPixelFormatBC6H_RGBUfloat     = 151,
    MTLPixelFormatBC7_RGBAUnorm      = 152,
    MTLPixelFormatBC7_RGBAUnorm_sRGB = 153,
};

PVRTC Blit 操作

MTLBlitCommandEncoder协议包含一个新MTLBlitOption值,可用于复制 PVRTC 像素格式的纹理或从纹理复制。
两种新方法通过将MTLBlitOptionRowLinearPVRTC值传递到其options参数来支持这些操作:

  • 要将 PVRTC 数据从缓冲区复制到纹理,请使用copyFromBuffer:sourceOffset:sourceBytesPerRow:sourceBytesPerImage:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin:options:
  • 要将 PVRTC 数据从纹理复制到缓冲区,请使用copyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toBuffer:destinationOffset:destinationBytesPerRow:destinationBytesPerImage:options:

PVRTC 块按行主序线性排列在内存中,与所有其他压缩纹理格式类似。
PVRTC 像素格式仅在 iOS 功能集中可用。


深度/模板渲染目标

OSX_GPUFamily1_v1功能集 不支持单独的深度和模板渲染目标。
如果需要这些渲染目标,请使用新引入的深度/模板像素格式之一将相同的纹理设置为深度和模板渲染目标。
组合的深度/模板像素格式列于例 10-6中。


例 10-6 深度/模板像素格式

typedef NS_ENUM(NSUInteger, MTLPixelFormat)
{
    // Depth/Stencil
    MTLPixelFormatDepth24Unorm_Stencil8 = 255,
    MTLPixelFormatDepth32Float_Stencil8 = 260,
};

所有功能集都支持MTLPixelFormatDepth32Float_Stencil8像素格式。
只有支持OSX_GPUFamily1_v1功能集的某些设备,也支持MTLPixelFormatDepth24Unorm_Stencil8像素格式。
查询MTLDevice 对象的 depth24Stencil8PixelFormatSupported属性 以确定是否支持像素格式。

具有深度、模板或深度/模板像素格式的纹理只能使用私有存储模式进行分配。
要加载或保存这些纹理的内容,必须执行位块传输操作。
MTLBlitCommandEncoder协议包含新MTLBlitOption值,这些值定义了具有深度、模板或深度/模板像素格式的纹理的位块传输操作的行为:

  • 使用MTLBlitOptionNone值 以仅深度或仅模板像素格式来块传输纹理的内容。
  • 使用MTLBlitOptionDepthFromDepthStencil值 以组合的深度/模板像素格式来块传输纹理的深度部分。
  • 使用MTLBlitOptionStencilFromDepthStencil值 以组合的深度/模板像素格式对纹理的模板部分进行块传输。

两种新方法通过其options参数 支持这些操作:

  • 要将深度或模板数据从缓冲区复制到纹理,请使用copyFromBuffer:sourceOffset:sourceBytesPerRow:sourceBytesPerImage:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin:options:
  • 要将深度或模板数据从纹理复制到缓冲区,请使用copyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toBuffer:destinationOffset:destinationBytesPerRow:destinationBytesPerImage:options:

注意: 要以仅深度或仅模板像素格式传输纹理内容,您可以放弃采用MTLBlitOption值的方法,而改用copyFromBuffer:sourceOffset:sourceBytesPerRow:sourceBytesPerImage:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin:copyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toBuffer:destinationOffset:destinationBytesPerRow:destinationBytesPerImage:方法。


立方体阵列纹理

OSX_GPUFamily1_v1功能集添加了对立方体数组纹理的支持,其纹理类型值为MTLTextureTypeCubeArray
同样,Metal 着色语言也添加了对此纹理类型的支持,其类型为texturecube_array
立方体数组纹理的最大长度为341(2048 除以 6 个立方体面)。


纹理使用

MTLTextureDescriptor 类包含新usage属性,允许您声明纹理在应用中的使用方式。
如果纹理在其生命周期内将有多种用途,则MTLTextureUsage可以使用 按位或 (|) 组合多个值。
MTLTexture 对象只能以其usage值指定的方式使用(否则将发生错误)。
MTLTextureUsage 选项及其预期用途描述如下:

  • MTLTextureUsageShaderRead允许在任何着色器阶段从纹理加载或采样。
  • MTLTextureUsageShaderWrite允许从计算着色器写入纹理。
  • MTLTextureUsageRenderTarget允许将此纹理用作渲染过程描述符中的颜色、深度或模板渲染目标。
  • MTLTextureUsagePixelFormatView 表示将使用newTextureViewWithPixelFormat:newTextureViewWithPixelFormat:textureType:levels:slices: 方法使用该纹理来创建新纹理。

指定并遵守适当的纹理用法可让 Metal 针对给定纹理优化 GPU 操作。
例如,如果您打算将生成的纹理用作渲染目标,请将描述符的usage值设置为MTLTextureUsageRenderTarget
这可能会显著提高应用的性能(在某些硬件上)。

如果你不知道纹理的用途,请将描述符的usage值设置为MTLTextureUsageUnknown
此值允许你新创建的纹理在任何地方使用,但 Metal 将无法优化其在你的应用中的使用。

重要提示: OSX_GPUFamily1_v1功能集中 的默认使用值为MTLTextureUsageShaderRead
在 iOS 功能集中,默认值为MTLTextureUsageUnknown,但您应始终确定并设置特定的纹理使用情况。
不要依赖 MTLTextureUsageUnknown 来获得最佳性能。

例 10-7展示了如何创建具有多种用途的纹理。


例 10-7 指定纹理的用途

MTLTextureDescriptor* textureDescriptor = [[MTLTextureDescriptor alloc] init];
textureDescriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
// set additional properties
 
id <MTLTexture> texture = [self.device newTextureWithDescriptor:textureDescriptor];
// use the texture in a color render target
// sample from the texture in a fragment shader

详细纹理视图

MTLTexture 协议添加了扩展newTextureViewWithPixelFormat:textureType:levels:slices: 方法,允许您为新的纹理视图指定新的纹理类型、基本级别范围和基本切片范围(除了newTextureViewWithPixelFormat:方法已经支持的像素格式参数之外)。
使用这些纹理视图方法 创建的纹理 现在可以使用parentTextureparentRelativeLevelparentRelativeSlice 属性 查询其父纹理的属性(除了查询pixelFormattextureType属性已经支持的其他属性之外)。
有关纹理视图创建限制的详细信息,例如有效的转换目标,请参阅 MTLTexture 协议参考

注意: 与查询父纹理属性类似,使用newTextureWithDescriptor:offset:bytesPerRow:方法从缓冲区创建的新纹理现在可以使用bufferbufferOffsetbufferBytesPerRow 属性查询其源缓冲区属性。


IOSurface 支持

OSX_GPUFamily1_v1功能集 增加了对 IOSurfaces 的支持。
使用newTextureWithDescriptor:iosurface:plane:方法 可以从现有的 IOSurface 创建新的纹理。


5、渲染添加


渲染命令编码器

MTLRenderCommandEncoder 类新增了几个图形 API 。
主要新功能总结如下:

  • setStencilFrontReferenceValue:backReferenceValue:方法 允许正面和背面图元使用不同的模板测试参考值。
  • MTLDepthClipMode枚举 和setDepthClipMode:方法支持深度剪辑。
  • OSX_GPUFamily1_v1iOS_GPUFamily3_v1 特征集中支持计数遮挡查询,并且可以将 MTLVisibilityResultModeCounting 新值传递到setVisibilityResultMode:offset:方法中。
  • 通过在同纹理写入和读取操作之间调用textureBarrier方法,OSX_GPUFamily1_v1功能集中支持纹理障碍。
  • OSX_GPUFamily1_v1iOS_GPUFamily3_v1功能集使用drawPrimitives:vertexStart:vertexCount:instanceCount:baseInstance:drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:baseVertex:baseInstance:方法支持基本顶点和基本实例值。
    同样,通过提供新的[[ base_vertex ]][[ base_instance ]]顶点着色器输入,Metal 着色语言也添加了对这些绘图输入的支持。
  • OSX_GPUFamily1_v1iOS_GPUFamily3_v1功能集支持间接绘制,其参数结构如例 10-8所示。
    分别将这些结构与drawPrimitives:indirectBuffer:indirectBufferOffset:drawIndexedPrimitives:indexType:indexBuffer:indexBufferOffset:indirectBuffer:indirectBufferOffset:方法一起使用。

例 10-8 间接绘制参数结构

// Without an index list
typedef struct {
    uint32_t vertexCount;
    uint32_t instanceCount;
    uint32_t vertexStart;
    uint32_t baseInstance;
} MTLDrawPrimitivesIndirectArguments;
 
// With an index list
typedef struct {
    uint32_t indexCount;
    uint32_t instanceCount;
    uint32_t indexStart;
    int32_t  baseVertex;
    uint32_t baseInstance;
} MTLDrawIndexedPrimitivesIndirectArguments;

  • 现在,可以通过设置一次顶点缓冲区,然后在绘制循环内简单地更新其偏移量来更有效地执行着色器常量更新,如例 10-9所示。
    如果您的应用程序只有非常少量的常量数据(几十个字节),您可以改用setVertexBytes:length:atIndex:方法,以便 Metal 框架可以为您管理常量缓冲区。

例 10-9 着色器常量更新

id <MTLBuffer> constant_buffer = // initialize buffer
MyConstants* constant_ptr = constant_buffer.contents;
[renderpass setVertexBuffer:constant_buffer offset:0 atIndex:0];
 
for (i=0; i<draw_count; i++)
{
    constant_ptr[i] = // write constants directly into the buffer
    [renderpass setVertexBufferOffset:i*sizeof(MyConstants) atIndex:0];
    // draw
}

分层渲染

OSX_GPUFamily1_v1 功能集通过MTLRenderPassDescriptorMTLRenderPipelineDescriptor 类中的新 API 增加了对分层渲染的支持。
分层渲染使顶点着色器能够将每个图元渲染到纹理数组、立方体纹理或 3D 纹理的一层,由其第一个顶点的顶点着色器指定。
对于 2D 纹理数组或立方体纹理,每个切片都是一层;对于 3D 纹理,每个像素深度平面都是一层。
加载和存储操作适用于渲染目标的每一层。


要启用分层渲染,您必须适当地配置渲染过程和渲染管道描述符:

  1. 设置renderTargetArrayLength属性的值 以指定所有渲染目标中可用的最小层数。
    例如,如果您有一个 2D 纹理数组和一个立方体纹理,每个纹理至少有 6 个层,则将此值设置为6
  2. 设置inputPrimitiveTopology属性的值 以指定要渲染的图元类型。
    例如,将此值设置为MTLPrimitiveTopologyClassTriangle,基于立方体的阴影映射。
    包含所有图元类型值的完整枚举声明列于例 10-10
  3. 此外,sampleCount 的值必须为 1(这是默认值)。

例 10-10 原始拓扑值

typedef NS_ENUM(NSUInteger, MTLPrimitiveTopologyClass)
{
    MTLPrimitiveTopologyClassUnspecified = 0,
    MTLPrimitiveTopologyClassPoint = 1,
    MTLPrimitiveTopologyClassLine = 2,
    MTLPrimitiveTopologyClassTriangle = 3
};

6、计算附加项

现有类和新类中还添加了一些计算 API,总结如下:

  • OSX_GPUFamily1_v1iOS_GPUFamily3_v1 特性集支持间接处理,其参数结构如 例 10-11所示。
    请将此结构与dispatchThreadgroupsWithIndirectBuffer:indirectBufferOffset:threadsPerThreadgroup:方法一起使用。

例 10-11 间接处理参数结构

typedef struct {
    uint32_t threadgroupsPerGrid[3];
} MTLDispatchThreadgroupsIndirectArguments;

  • 新的MTLComputePipelineDescriptor类 指定计算操作过程中使用的计算配置状态。
    此描述符对象用于创建MTLComputePipelineState对象。
    此新类是 先前引入的MTLRenderPipelineDescriptor类的计算对应项。

7、支持框架

Metal 提供了两个新框架,帮助您以更简单、更强大的方式构建 Metal 应用程序。


Metal 套件

MetalKit 框架提供了一组实用函数和类,可减少创建 Metal 应用所需的工作量。
MetalKit 为三个关键领域提供开发支持:

  • 纹理加载可帮助您的应用轻松地从各种来源异步加载纹理。
    支持 PNG 和 JPEG 等常见文件格式以及 KTX 和 PVR 等特定于纹理的格式。
  • 模型处理提供了 Metal 专属功能,可轻松与 Model I/O 资产进行交互。
    使用这些高度优化的函数和对象可在 Model I/O 网格和 Metal 缓冲区之间高效传输数据。
  • 视图管理提供了 Metal 视图的标准实现,大大减少了创建图形渲染应用程序所需的代码量。

MetalKit 框架适用于所有 Metal 功能集。
要了解有关 MetalKit API 的更多信息,请参阅 MetalKit 框架参考


Metal 性能着色器

Metal Performance Shaders 框架提供了高度优化的计算和图形着色器,旨在轻松高效地集成到您的 Metal 应用程序中。

使用 Metal Performance Shader 类可实现所有受支持设备的最佳性能,而无需针对特定 GPU 系列定位或更新着色器代码。
Metal Performance Shader 对象可无缝融入您的 Metal 应用,并可与缓冲区和纹理等资源对象一起使用。

Metal Performance Shader 框架提供的常见着色器包括:

  • 高斯模糊。
  • 图像直方图。
  • Sobel 边缘检测。

Metal Performance Shaders 框架可在iOS_GPUFamily2_v1iOS_GPUFamily2_v2iOS_GPUFamily3_v1功能集中使用。
要了解有关 Metal Performance Shaders API 的更多信息,请参阅 Metal Performance Shaders 框架参考


十二、iOS 10、tvOS 10 和 macOS 10.12 中的新功能

本章总结了 iOS 10、tvOS 10 和 macOS 10.12 中引入的新功能。


1、新的 Metal 功能集

新的 Metal 功能集列表如下:

  • MTLFeatureSet_iOS_GPUFamily1_v3
  • MTLFeatureSet_iOS_GPUFamily2_v3
  • MTLFeatureSet_iOS_GPUFamily3_v2
  • MTLFeatureSet_tvOS_GPUFamily1_v2
  • MTLFeatureSet_OSX_GPUFamily1_v2
  • MTLFeatureSet_OSX_ReadWriteTextureTier2

要确定设备是否支持某个功能集,请查询MTLDevice对象的现有supportsFeatureSet:方法。

本章中介绍的每个新功能都标注了其功能集可用性。
有关所有功能集的功能可用性、实现限制和像素格式功能的更多信息,请参阅 Metal 功能集表页面。

所有新的 Metal 着色语言功能均在 1.2 版中提供(MTLLanguageVersion1_2)。
有关更多信息,请参阅Metal 着色语言指南


2、Tessellation

有关 Metal 中曲面细分的完整概述,请参阅 曲面细分章节。


3、资源堆

有关 Metal 中资源堆的完整概述,请参阅 资源堆章节。


4、无记忆渲染目标

可用iOS_GPUFamily1_v3iOS_GPUFamily2_v3iOS_GPUFamily3_v2tvOS_GPUFamily1_v2

无内存渲染目标是仅暂时存在于 GPU 上的图块内存中的渲染目标,无需任何其他 CPU 或 GPU 内存支持。
无内存渲染目标可满足高分辨率显示器和 MSAA 数据增加的内存需求,让您能够:

  • 防止浪费仅用于临时渲染目标(颜色、深度和模板)的内存。
  • 在相同的内存预算中使用更高的 MSAA 级别来提高图像质量。

要创建无记忆渲染目标,请将MTLTextureDescriptor对象的 storageMode属性设置为MTLStorageModeMemoryless
然后,使用此描述符创建一个MTLTexture对象。

只有MTLTexture对象 才能使用存储模式创建MTLStorageModeMemoryless
无记忆渲染目标只能由临时渲染目标使用;为此,请将其设置为MTLRenderPassAttachmentDescriptor对象的 texture的属性。


用例

以下用例只是可用作无记忆渲染目标的临时渲染目标的几个示例。
对于每个渲染目标,渲染操作完成后永远不会使用渲染目标的内容:

  • 传统的深度测试,使用带有MTLStoreActionDontCare存储动作的深度渲染目标。
  • 传统的模板缓冲区操作,使用带有MTLStoreActionDontCare存储操作的模板渲染目标。
  • MSAA 渲染,使用具有MTLStoreActionMultisampleResolve存储操作的 MSAA 颜色渲染目标。
  • 延迟渲染,在单个渲染命令编码器中使用两个逻辑渲染过程:
  1. 第一遍使用反照率、法线和其他数据填充临时 G 缓冲区渲染目标。
  2. 第二遍读取临时 G 缓冲区渲染目标,然后计算并累积照明数据以将最终颜色输出到持久渲染目标。
    每个临时渲染目标都是无记忆渲染目标,并具有MTLStoreActionDontCare存储动作。
  • 延迟照明,在单个渲染命令编码器中使用三个逻辑渲染过程:
  1. 第一遍用几何数据填充临时深度和法线渲染目标。
  2. 第二遍读取先前的临时渲染目标,然后计算并累积照明数据以填充临时的漫反射和镜面渲染目标。
  3. 第三遍读取所有先前的临时渲染目标,然后计算照明方程以将最终颜色输出到持久渲染目标。
    每个临时渲染目标都是无记忆渲染目标,并具有MTLStoreActionDontCare存储动作。

规则和限制

无记忆渲染目标必须遵守以下规则和限制。无记忆渲染目标…

  • 可以是堆的一部分,但不能使用别名。
    有关更多信息,请参阅 资源堆章节。
  • 必须具有可渲染的颜色、深度或模板像素格式。
  • 仅能通过渲染过程填充。
  • 必须具有MTLTextureType2DMTLTextureType2DMultisample纹理类型。
  • 只能用于MTLRenderPassAttachmentDescriptor对象的texture属性。
  • 不能用于MTLRenderPassAttachmentDescriptor对象的resolveTexture属性。
  • 必须由具有MTLLoadActionDontCareMTLLoadActionClear加载动作的MTLRenderPassAttachmentDescriptor对象使用。
  • 必须由具有MTLStoreActionDontCareMTLStoreActionMultisampleResolve存储操作的 MTLRenderPassAttachmentDescriptor 对象使用。
    MTLRenderPassAttachmentDescriptor对象 也可以具有初始MTLStoreActionUnknown存储操作,但必须在其关联的渲染命令编码器结束编码之前更改此操作。
    有关更多信息,请参阅 延迟存储操作部分。
  • 无法通过MTLTexture协议中的任何方法 读取或写入。
  • 不能用作父纹理来创建纹理视图。
  • 无法被MTLBlitCommandEncoder对象使用。不允许进行任何 blit 操作。
  • 无法被MTLComputeCommandEncoder对象使用。不允许执行任何计算操作。
  • 可以使用帧缓冲区提取由片段函数读取。
    有关更多信息,请参阅 Metal 着色语言指南 的可编程混合部分。

使用无记忆渲染目标的应用应谨慎控制在单个渲染过程中传递以进行处理的数据总量。
无记忆渲染目标使用 GPU 上的图块内存作为其临时存储;只要可以缓存与为渲染过程发出的绘制调用相关的所有数据,无记忆渲染目标就会一次处理一个图块。
为确保成功使用无记忆渲染目标,请务必遵循以下附加规则:

  • 渲染过程中引用的所有资源消耗的物理内存都不应超过可用内存量。
  • 您不应提交超过 64K 个唯一视口、剪刀和深度偏差值。

命令缓冲区将通过MTLCommandBufferErrorMemoryless错误代码报告任何内存错误。


5、函数专业化

可用iOS_GPUFamily1_v3iOS_GPUFamily2_v3iOS_GPUFamily3_v2tvOS_GPUFamily1_v2OSX_GPUFamily1_v2

函数特化使用函数常量来创建图形和计算函数的特化版本。
函数常量是 Metal 着色语言源中声明的编译时常量;函数常量值在编译特化函数之前在 Metal 应用中分配。

注意: 函数常量是预处理器宏的改进替代品,如《Metal 着色语言指南》的程序范围函数常量部分所述。


声明函数常量

Metal着色语言提供了 [[ function_constant(index) ]] 属性 来声明函数常量,如例11-1 所示。


例 11-1 声明函数常量

constant bool a [[ function_constant(0) ]];

函数常量可用于:

  • 控制编译哪些函数代码路径
  • 指定函数的可选参数
  • 指定使用[[ stage_in ]]限定符 声明的结构的可选元素

设置常量值

Metal 框架提供了MTLFunctionConstantValues用于为特定函数设置常量值的类。
常量值可以通过索引、索引范围或名称来设置。
例 11-2显示了如何通过索引设置常量值。


例 11-2 通过索引设置常量值

const bool a = true;
MTLFunctionConstantValues* constantValues = [MTLFunctionConstantValues new];
[constantValues setConstantValue:&a type:MTLDataTypeBool atIndex:0];

单个MTLFunctionConstantValues对象 可以应用于多个MTLFunction对象(例如,顶点函数和片段函数)。
创建专用函数后,对其常量值的任何更改都不会对其产生进一步影响。
但是,您可以重置、添加或修改 MTLFunctionConstantValues对象中的任何常量值,并重用它以创建另一个MTLFunction对象。


编译专用函数

专用函数是通过调用以下方法之一从 MTLLibrary 对象创建MTLFunction的对象:

  • newFunctionWithName:constantValues:completionHandler:
  • newFunctionWithName:constantValues:error:

这些方法调用 Metal 编译器来评估您的函数常量及其常量值。
编译器通过省略函数常量代码路径、参数和提供的常量值未启用的元素来专门化命名函数。
首先按其索引查找函数常量值,然后按其名称查找。
任何与命名函数中的函数常量不对应的值都将被忽略(不会生成错误或警告)。

注意: newFunctionWithName:constantValues:completionHandler:方法异步编译专用函数。
如果您的应用需要创建多个专用函数,请使用此方法最大限度地提高性能和并行性。


获取反射数据

MTLFunctionConstant 对象为函数常量提供反射数据。
仅当您需要反射数据来为专用函数设置常量值时才应获取此对象(例如,在设置函数常量值之前确定函数常量是可选的还是必需的)。
要获取反射数据,请通过调用newFunctionWithName:方法并查询functionConstants属性来获取MTLFunction对象。
使用此反射数据来设置常量值并编译专用函数。


6、函数资源读写


函数缓冲区读写

可用: iOS_GPUFamily3_v2 ,OSX_GPUFamily1_v2

片段函数现在可以写入缓冲区。
可写缓冲区必须在device地址空间中声明,并且不是const
使用动态索引写入缓冲区。


原子函数

顶点和片段函数现在支持缓冲区(在device地址空间中)的原子函数。
有关更多信息,请参阅 Metal Shading Language Guide 的原子函数部分。


函数纹理读写

可用: OSX_GPUFamily1_v2

顶点和片段函数现在都可以写入纹理。
可写入纹理必须使用access::writeaccess::read_write限定符声明。
使用write()函数的适当变体来写入纹理(其中lod始终为常数且等于零)。


读写纹理

读写纹理是可以由同一个顶点、片段或内核函数读取和写入的纹理。


Access

在 Metal 着色语言中,读写纹理被声明为带有access::read_write限定符的纹理。
例 11-3展示了在同一个函数中对读写纹理进行的简单读写操作。


例 11-3 使用读写纹理

kernel void my_kernel(texture2d<float, access::read_write> texA [[ texture(0) ]],
                      ushort2 gid [[ thread_position_in_grid ]])
{
    float4 color = texA.read(gid);
    color = processColor(color);
    texA.write(color, gid);
}

注意: 读写纹理不能调用sample()sample_compare()gather()gather_compare()函数。

不能使用texture2d_msdepth2ddepth2d_arraydepthcubedepthcube_arraydepth2d_ms纹理类型声明读写纹理。

要在图形或计算函数参数表中设置读写纹理,请使用 Metal 框架 API 中的现有方法之一:

  • setVertexTexture:atIndex:
  • setFragmentTexture:atIndex:
  • setTexture:atIndex:

例 11-4 设置读写纹理(Metal Shading Language)

// kernel function signature
kernel void my_kernel(texture2d<float, access::read_write> texA [[ texture(0) ]], ...)


例 11-5 设置读写纹理(Metal Framework)

// kernel function argument table
[computeCommandEncoder setTexture:texA atIndex:0];

注意: 在函数签名中声明两个单独的纹理参数(一个读取,一个写入)然后为两者设置相同的纹理 是**无效的。
**


例 11-6 无效的读写纹理配置(Metal Shading Language)

// kernel function signature
kernel void my_kernel(texture2d<float, access::read> texARead [[ texture(0) ]],
                      texture2d<float, access::write> texAWrite [[ texture(1) ]],
                      ...)

例 11-7 无效的读写纹理配置(Metal Framework)

// kernel function argument table
[computeCommandEncoder setTexture:texA atIndex:0]; // Read
[computeCommandEncoder setTexture:texA atIndex:1]; // Write

同步

fence()函数允许您控制线程内纹理写入和读取操作的顺序,如例 11-8所示。
fence()函数确保线程对纹理的写入对于同一线程对该纹理的后续读取可见。


例 11-8 使用带有栅栏的读写纹理

kernel void my_kernel(texture2d<float, access::read_write> texA [[texture(0)]],
                      ushort2 gid [[ thread_position_in_grid ]])
{
    float4 color = generateColor();
    texA.write(color, gid);
 
    // add a fence to ensure the correct ordering of write and read operations within the thread
    texA.fence();
 
    float4 readColor = texA.read(gid);
}

像素格式

读写纹理支持的像素格式集分为两层,每层由一组特定的特性定义。


1 级像素格式

可用: OSX_GPUFamily1_v2

  • R32Float
  • R32Uint
  • R32Sint

第 2 层像素格式

可用: OSX_ReadWriteTextureTier2

  • RGBA32Float
  • RGBA32Uint
  • RGBA32Sint
  • RGBA16Float
  • RGBA16Uint
  • RGBA16Sint
  • RGBA8Unorm
  • RGBA8Uint
  • RGBA8Sint
  • R16Float
  • R16Uint
  • R16Sint
  • R8Unorm
  • R8Uint
  • R8Sint

规则和限制


内存屏障

命令编码器之间

在给定命令编码器中执行的所有资源写入在下一个命令编码器中都是可见的。
对于渲染和计算命令编码器都是如此。

在渲染命令编码器中

对于缓冲区,原子写入对于跨多个线程的后续原子读取是可见的。

对于纹理,textureBarrier方法可确保在给定绘制调用中执行的写入对于下一个绘制调用中的后续读取可见。

在计算命令编码器内

在给定内核函数中执行的所有资源写入在下一个内核函数中可见。


片段函数

丢弃

如果调用该discard_fragment()函数,则调用之前发生的所有资源写入实际上都会提交到内存中。
调用之后,片段函数的执行要么完全停止,要么继续,但未来的资源写入将被完全忽略。


剪刀测试

剪刀测试总是在执行片段函数之前执行。
如果剪刀测试失败,则不会执行片段函数,也不会发生任何资源写入。


早期和晚期片段测试

可以将[[early_fragment_tests]]限定符添加到片段函数中,以请求在执行片段函数之前执行深度、模板和遮挡查询测试。
如果任何测试失败,则不会执行片段函数,也不会发生任何资源写入。

如果不添加 [[early_fragment_tests]]限定符,则无论测试是否通过,所有片段函数资源写入都将提交到内存中。


MSAA

资源写入操作的执行次数取决于片段函数的执行次数。
默认情况下,片段函数在 1 到N次之间执行,其中N是片段覆盖的样本数。
但是,如果片段函数使用带有colorsample_idsample_perspectivesample_no_perspective 限定符的输入,则它始终以采样率执行。

如果由于设置了0覆盖掩码 或在alphaToCoverageEnabled属性设置为YES,返回了低 alpha 值而丢弃了片段函数结果,则所有片段函数资源写入仍会提交到内存中。


辅助线程

辅助线程是针对不产生输出的原始边缘附近的像素的片段函数调用,通常用于计算导数。
辅助线程执行的资源写入将被忽略;原子不会更新内存,原子返回的值将未定义。

欲了解更多信息,请参阅《*Metal 着色语言指南》*的“片段函数”部分。


7、纹理数组

可用: iOS_GPUFamily3_v2

纹理数组是一种用于存储同类纹理的数据结构,可让您轻松动态索引纹理。
纹理数组在 Metal 着色语言中声明为:

  • array<typename T, size_t N>, 或者
  • const array<typename T, size_t N>

T``access::read是用或access::sample限定符声明的纹理类型,N是数组中的纹理数量。

纹理数组可以作为参数传递给图形、计算或用户函数,也可以在函数内部声明为局部变量。
例 11-9显示了如何将 10 个纹理的数组作为参数传递给内核函数。


例 11-9 将纹理数组作为参数传递给函数

kernel void my_kernel(
    const array<texture2d<float>, 10> src [[ texture(0) ]],
    texture2d<float, access::write> dst [[ texture(10) ]]
)

没有新的 Metal 框架 API 来设置纹理数组;使用MTLRenderCommandEncoder协议或MTLComputeCommandEncoder协议中的现有方法。

Metal 着色语言还增加了对不可变纹理数组的引用支持,声明为array_ref<T>,其中T的纹理类型,并且 size()提供数组中的纹理数量。

纹理数组的存储不属于array_ref<T>对象。
具有连续迭代器的类型(如metal::array)提供了隐式转换操作。
array_ref<T>类型只能作为参数传递给用户函数。
array_ref<T>类型的常见用途是 将纹理数组作为参数传递给您希望能够接受各种数组类型的函数,如例 11-10所示。

示例 11-10 将类型作为参数 传递array_ref<T>给函数

float4 foo(array_ref<texture2d<float>> src)
{
    float4 clr(0.0f);
    for(int i=0; i<src.size(); i++)
    {
        clr += process_texture(src[i]);
    }
    return clr;
}
 
kernel void my_kernel_A(
    const array<texture2d<float>, 10> srcA [[ texture(0) ]],
    texture2d<float, access::write> dstB [[ texture(10) ]])
{
    float4 clrA = foo(srcA);
    /* ... */
}
 
kernel void my_kernel_B(
    const array<texture2d<float>, 20> srcB [[ texture(0) ]],
    texture2d<float, access::write> dstB [[ texture(20) ]])
{
    float4 clrB = foo(srcB);
    /* ... */
}

8、模板纹理视图

可用iOS_GPUFamily1_v3iOS_GPUFamily2_v3iOS_GPUFamily3_v2tvOS_GPUFamily1_v2OSX_GPUFamily1_v2

MTLPixelFormatX32_Stencil8MTLPixelFormatX24_Stencil8 是新的模板像素格式,允许您使用模板纹理视图 轻松访问图形 或计算函数中的 模板纹理数据。

模板纹理视图 允许您从组合的深度和模板纹理 创建仅模板纹理。
然后可以将这两个纹理设置为纹理参数,并在图形或计算函数中单独采样。
仅模板纹理为每个像素返回 8 位无符号整数值。
深度和模板父纹理的格式决定了模板纹理视图的格式,如表 11-1所示。

深度和模板父纹理格式模板纹理视图格式
MTLPixelFormatDepth32Float_Stencil8MTLPixelFormatX32_Stencil8
MTLPixelFormatDepth24Unorm_Stencil8*MTLPixelFormatX24_Stencil8*

*这些像素格式仅在某些设备中受支持;查询MTLDevice对象 的 depth24Stencil8PixelFormatSupported属性 以检查是否支持。

可以使用MTLTexture协议中现有的 Metal 框架 API 来创建模板纹理视图,如例 11-11 所示。


例 11-11 创建模板纹理视图

// Create a combined depth and stencil texture
MTLTextureDescriptor *depthStencilDescriptor = [MTLTextureDescriptor new];
depthStencilDescriptor.textureType = MTLTextureType2D;
depthStencilDescriptor.pixelFormat = MTLPixelFormatDepth32Float_Stencil8;
depthStencilDescriptor.width = 32;
depthStencilDescriptor.height = 32;
depthStencilDescriptor.usage = (MTLTextureUsageShaderRead | MTLTextureUsagePixelFormatView);
 
id<MTLTexture> depthStencilTexture = [device newTextureWithDescriptor:depthStencilDescriptor];
/* Initialize the combined depth and stencil texture data */
 
// Create a stencil texture view
id<MTLTexture> stencilTextureView = [depthStencilTexture newTextureViewWithPixelFormat:MTLPixelFormatX32_Stencil8];

注意: 模板纹理视图是一种改进的替代方案,用于将模板数据从组合深度和模板纹理传输到缓冲区,然后从该缓冲区传输到仅模板纹理。


9、深度 16 像素格式

可用: OSX_GPUFamily1_v2

MTLPixelFormatDepth16Unorm是一种新的 16 位深度像素格式,具有一个规范化的无符号整数分量。


10、扩展范围像素格式

可用: iOS_GPUFamily3_v2

表 11-2列出了新的扩展范围像素格式。
这些像素格式旨在用作具有宽色域显示的设备可显示的渲染目标。

像素格式每像素位数 (bpp)每组件位数 (bpc)编码范围
MTLPixelFormatBGRA10_XR6410线性 (Linear)[-0.752941,1.25098]*
MTLPixelFormatBGRA10_XR_sRGB6410伽玛[-0.527100, 1.66894]*(伽马扩展之前)
MTLPixelFormatBGR10_XR三十二10线性 (Linear)[-0.752941,1.25098]
MTLPixelFormatBGR10_XR_sRGB三十二10伽玛[-0.527100,1.66894](伽马扩展之前)

MTLPixelFormatBGRA10_XRMTLPixelFormatBGRA10_XR_sRGB 像素格式的 alpha 分量在采样、渲染和写入时始终被限制在[0.0, 1.0]范围内(尽管支持超出此范围的值)。

所有扩展范围格式都是可颜色渲染的,可以在CAMetalLayer对象的pixelFormat属性或MTKView对象的colorPixelFormat 属性中设置。
只有具有宽色域显示器的设备才会显示[0.0,1.0]范围之外的值;所有其它设备将把值箝位到[0.0,1.0]范围。

注意: 32bpp MTLPixelFormatBGR10_XRMTLPixelFormatBGR10_XR_sRGB 扩展范围像素格式具有与 32bpp MTLPixelFormatRGBA8UnormMTLPixelFormatRGBA8Unorm_sRGB普通像素格式相同的速度和内存特性。

64bppMTLPixelFormatBGRA10_XRMTLPixelFormatBGRA10_XR_sRGB 扩展范围像素格式需要更多内存带宽,性能也不佳。
虽然它们可用于源纹理或中间渲染目标,但建议仅将这些格式用于CAMetalDrawable对象的纹理。
如果要表示宽色域值并需要 alpha 分量,则应将 32bppMTLPixelFormatBGR10_XRMTLPixelFormatBGR10_XR_sRGB 48bpp 像素格式的颜色纹理与 8bpp MTLPixelFormatA8Unorm 像素格式的单独 alpha 纹理结合使用。


11、组合 MSAA 存储和解析操作

可用: iOS_GPUFamily3_v2 ,OSX_GPUFamily1_v2

MTLStoreActionStoreAndMultisampleResolve存储操作 允许您使用单个渲染命令编码器 存储和解析 MSAA 数据。
未解析的 MSAA 数据存储在texture属性指定的纹理中,已解析的 MSAA 数据存储在resolveTexture属性指定的纹理中,如例 11-12所示


例 11-12 使用单个渲染命令编码器执行组合 MSAA 存储和解析操作

// Create MSAA texture
// This texture is used to store the unresolved MSAA data
MTLTextureDescriptor *msaaTextureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:_width height:_height mipmapped:NO];
msaaTextureDescriptor.textureType = MTLTextureType2DMultisample;
msaaTextureDescriptor.sampleCount = _sampleCount; // must be > 1
 
id <MTLTexture> msaaTexture = [_device newTextureWithDescriptor:msaaTextureDescriptor];
 
// Create resolve texture
// This texture is used to store the resolved MSAA data
MTLTextureDescriptor *colorTextureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:_width height:_height mipmapped:NO];
 
id <MTLTexture> colorTexture = [_device newTextureWithDescriptor:colorTextureDescriptor];
 
// Create descriptor for store and resolve rendering pass
MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDescriptor.colorAttachments[0].texture = msaaTexture;
renderPassDescriptor.colorAttachments[0].resolveTexture = colorTexture;
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionDontCare;
renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStoreAndMultisampleResolve;
 
// Create command buffer and render command encoder
id <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
id <MTLRenderCommandEncoder> renderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
/* Encode rendering pass commands */
 
// End encoding and commit command buffer
[renderCommandEncoder endEncoding];
[commandBuffer commit];

注意: 组合的 MSAA 存储和解析操作 是在一个渲染命令编码器中执行存储操作,然后在另一个渲染命令编码器中执行解析操作的改进替代方案。


12、延迟存储操作

可用iOS_GPUFamily1_v3iOS_GPUFamily2_v3iOS_GPUFamily3_v2tvOS_GPUFamily1_v2OSX_GPUFamily1_v2

MTLStoreActionUnknown存储操作允许您在配置对象时推迟指定存储操作MTLRenderPassAttachmentDescriptor
必须在创建渲染命令编码器之后但在调用endEncoding方法之前指定存储操作,如例 11-13所示。
MTLRenderCommandEncoder对象上调用以下方法之一,来指定除MTLStoreActionUnknown 之外的存储操作:

  • setColorStoreAction:atIndex:
  • setDepthStoreAction:
  • setStencilStoreAction:

MTLParallelRenderCommandEncoder对象提供了等效方法,您只能在父编码器上调用,而不能在任何子编码器上调用。


例 11-13 推迟指定存储操作的决定

// Create descriptor with an unknown store action
MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDescriptor.colorAttachments[0].texture = colorTexture;
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionDontCare;
renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionUnknown;
 
// Create command buffer and render command encoder
id <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
id <MTLRenderCommandEncoder> renderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
/* Encode rendering pass commands */
 
// Specify a known store action
[renderCommandEncoder setColorStoreAction:MTLStoreActionStore atIndex:0];
 
// End encoding and commit command buffer
[renderCommandEncoder endEncoding];
[commandBuffer commit];

使用MTLStoreActionUnknown存储操作 创建的渲染命令编码器 可以在调用endEncoding方法之前根据需要多次更改其存储操作。
使用非MTLStoreActionUnknown存储操作 创建的渲染命令编码器在创建后不能更改其存储操作。

注意: MTLStoreActionUnknown 存储操作可以帮助您 避免因过早 选择MTLStoreActionStore操作 而产生的潜在带宽成本。


13、双源混合

可用: OSX_GPUFamily1_v2

双源混合允许片段函数,将两种源颜色Source0Source1输出到 GPU 的混合单元用于单个渲染目标。


产生第二种源颜色

为了产生两种输出颜色Source0Source1,Metal 着色语言color(m)使用属性限定符扩展了index(i)属性限定符,其中:

  • m是颜色附着指数。
  • i是颜色输出索引。

例 11-14 在片段函数中启用双源混合

struct DualSourceOutput
{
    half4 Source0 [[ color(0), index(0) ]];
    half4 Source1 [[ color(0), index(1) ]];
};
 
fragment DualSourceOutput dual_source_fragment(constant float4* color0 [[ buffer(0) ]],
                                               constant float4* color1 [[ buffer(1) ]])
{
    DualSourceOutput dso;
    dso.Source0 = half4(color0[0]);
    dso.Source1 = half4(color1[0]);
    return dso;
}

注意: 编译时必须知道mi 的值。
如果没有指定index(i),则假定 索引为0


引用第二个源颜色

第二种输出颜色,Source1在下列固定函数混合方程中被引用为源或目标混合因子:

Output.rgb = (Source0.rgb * SBF) {BO} (Destination.rgb * DBF)

Output.a = (Source0.a * SBF) {BO} (Destination.a * DBF)

其中BOMTLBlendOperation运算符,SBF是源混合因子,DBF是目标混合因子,Source1可以作为以下MTLBlendFactor枚举之一引用:


Table 11-3 Source1 blend factors

MTLBlendFactorRGB 混合因子值Alpha 混合因子值
Source1ColorR、G、B 来自Source1来自Source1
OneMinusSource1Color1-R,1-G,1-B 来自Source11-A 来自Source1
Source1AlphaA、A、A 来自Source1来自Source1
OneMinusSource1Alpha1-A,1-A,1-A 来自Source11-A 来自Source1

例如,例 11-15展示了产生以下双源混合方程的渲染管道配置:

Output.rgb = (Source0.rgb * 1) + (Destination.rgb * Source1.rgb)

例 11-15 在渲染管道描述符中 配置双源混合

MTLRenderPipelineDescriptor *rpd = [[MTLRenderPipelineDescriptor alloc] init];
rpd.label = @"DualSourcePipeline";
rpd.fragmentFunction = [_defaultLibrary newFunctionWithName:@"dual_source_fragment"];

rpd.colorAttachments[0].blendingEnabled = YES;
rpd.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
rpd.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne;
rpd.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorSource1Color;

规则和限制

双源混合配置必须遵守以下规则和限制:

  • 双源混合与多个渲染目标不兼容。
  • 片段函数只能输出到color(0)
  • index(0)总是关于Source0,并且index(1)总是关于 Source1
  • Source1混合因子仅可在colorAttachments[0]上设置。

14、MSAA Blits

可用iOS_GPUFamily1_v3iOS_GPUFamily2_v3iOS_GPUFamily3_v2tvOS_GPUFamily1_v2OSX_GPUFamily1_v1OSX_GPUFamily1_v2

新的 iOS 和 tvOS 功能集现在支持从纹理到缓冲区或从缓冲区到纹理的 MSAA 位块传输。
位块传输目标必须具有足够的大小才能存储 MSAA 数据。

(MSAA blits 已在OSX_GPUFamily1_v1功能集中得到支持,并且将继续在OSX_GPUFamily1_v2功能集中得到支持。)


15、sRGB 写入

可用iOS_GPUFamily2_v3iOS_GPUFamily3_v1iOS_GPUFamily3_v2tvOS_GPUFamily1_v2

现在,其他 iOS 和 tvOS 功能集支持写入 sRGB 纹理。

iOS_GPUFamily3_v1 功能集已经支持写入 sRGB 纹理。)


16、附加着色语言功能

可用: iOS_GPUFamily1_v3(除非另有说明),iOS_GPUFamily2_v3iOS_GPUFamily3_v2tvOS_GPUFamily1_v2OSX_GPUFamily1_v2

本节总结了 Metal 着色语言 1.2 版中引入的附加功能。
有关更多信息,请参阅Metal 着色语言指南


整数函数

新的整数函数用于提取、插入和反转位,如整数函数中所述。


纹理函数

现在可以将纹理读写函数与 16 位无符号整数坐标(ushort类型)一起使用,如纹理函数中所述。


计算函数

可用iOS_GPUFamily1_v3iOS_GPUFamily2_v3iOS_GPUFamily3_v2tvOS_GPUFamily1_v2

针对 SIMD 组线程的新同步函数,如线程组同步函数中所述。


采样器限定符

可用iOS_GPUFamily1_v3iOS_GPUFamily2_v3iOS_GPUFamily3_v2tvOS_GPUFamily1_v2

新的采样器限定符用于指定最大各向异性和 LOD 钳位范围,如采样器中所述。


缓冲区、纹理和采样器的结构

现在可以通过值将资源结构作为参数传递给图形或计算函数,如函数参数和变量中所述。


便利常量

floathalf类型 新的便捷常量,如数学函数中所述。


17、行为变化

  • 对于所有MTLRenderCommandEncoder对象,MTLStoreActionStore都需要执行 存储操作 来存储渲染目标的内容,以供后续渲染命令编码器或显示器使用。
    在 iOS 10 和 tvOS 10 之前,如果所有颜色渲染目标都有MTLStoreActionDontCare存储操作,则驱动程序会选择将渲染的内容存储到第一个启用的渲染目标。
    在 iOS 10 和 tvOS 10 中,驱动程序不再执行此不必要的操作。
  • 在 iOS 10 和 tvOS 10 中,MTLBlitCommandEncoder 协议中所有方法的缓冲区对齐都已放宽。

十三、Tessellation

可用: iOS_GPUFamily3_v2 ,OSX_GPUFamily1_v2

曲面细分用于从由控制点组成的四边形或三角形面片构造的初始表面计算出更详细的表面。
为了近似高阶表面,GPU 使用每个面片的曲面细分因子将每个面片细分为三角形


1、Metal 镶嵌管道

图 12-1显示了 Metal 曲面细分管道,它使用计算内核、曲面细分器和后曲面细分顶点函数。


图 12-1 Metal 曲面细分管道

在这里插入图片描述


曲面细分在面片上进行,每个面片代表由一组控制点定义的任意几何排列。
每个面片的曲面细分因子、每个面片的用户数据和面片控制点数据分别存储在单独的MTLBuffer对象中。


计算内核

计算内核是一个执行以下操作的内核函数:

  • 计算每个面片的镶嵌因子。
  • 可选地,计算每个补丁的用户数据。
  • 可选地,计算或修改补丁控制点数据。

注意: 计算内核不需要每帧都执行一次来计算每个面片的曲面细分因子、每个面片的用户数据或面片控制点数据。
您可以每n帧、离线或通过任何其他运行时方式计算这些数据,只要您在需要时向曲面细分器和曲面细分后顶点函数提供所需的数据即可。


Tessellator

曲面细分器是一个固定功能的管道阶段,它创建面片表面的采样模式并生成连接这些样本的图形基元。
曲面细分器在规范化坐标系中平铺规范域,范围从0.01.0

曲面细分器配置为渲染管道的一部分,使用MTLRenderPipelineDescriptor对象 构建MTLRenderPipelineState对象。
曲面细分器的输入是每个面片的曲面细分因子。


曲面细分器基元生成

曲面细分器对每个补丁运行一次,使用输入补丁并生成一组新的三角形。
这些三角形是通过根据提供的每个补丁曲面细分因子细分补丁而生成的。
曲面细分器生成的每个三角形顶点在归一化参数空间中都有一个关联的 (u, v) 或 (u, v, w) 位置,每个参数值的范围从0.01.0。(请注意,细分是以依赖于实现的方式执行的。)


细分后顶点函数

后镶嵌顶点函数是一个顶点函数,用于计算镶嵌器生成的每个面片表面样本的顶点数据。
后镶嵌顶点函数的输入包括:

  • 面片上的规范化顶点坐标(由曲面细分器输出)。
  • 每个补丁的用户数据(可选择由计算内核输出)。
  • 补丁控制点数据(可选择由计算内核输出)。
  • 任何其他顶点函数输入,例如纹理和缓冲区。

后镶嵌顶点函数为镶嵌三角形生成最终顶点数据。
后镶嵌顶点函数执行完毕后,镶嵌图元将被光栅化,渲染管道的其余阶段将照常执行。


2、每个面片的曲面细分因子

每个面片细分因子指定细分器细分每个面片的程度。每个面片的镶嵌因子由四边形面片的 MTLQuadTessectionFactorsHalf 结构或三角形面片的 MILTriangleTestectionFactors Half 构造描述。

注意: 尽管结构成员属于uint16_t类型,但是输入到曲面细分器的每个面片曲面细分因子必须属于half类型。


理解四边形补丁

对于四边形面片,面片中的位置是 (u, v) 笛卡尔坐标,表示顶点相对于四边形面片边界的水平和垂直位置,如图12-2所示。
(u, v) 值的范围从0.01.0


图 12-2 归一化参数空间中的四边形面片坐标

在这里插入图片描述


解释 MTLQuadTessellationFactorsHalf 结构

MTLQuadTessellationFactorsHalf结构定义如下:

typedef struct {
    uint16_t edgeTessellationFactor[4];
    uint16_t insideTessellationFactor[2];
} MTLQuadTessellationFactorsHalf;

结构中的每个值都提供了一个特定的镶嵌因子:

  • edgeTessellationFactor[0]提供面片边缘的镶嵌因子,其中u=0(边缘 0)。
  • edgeTessellationFactor[1]提供面片边缘的镶嵌因子,其中v=0(边缘 1)。
  • edgeTessellationFactor[2]提供面片边缘的镶嵌因子,其中u=1(边缘 2)。
  • edgeTessellationFactor[3]提供面片边缘的镶嵌因子,其中v=1(边缘 3)。
  • insideTessellationFactor[0]为 的所有内部值提供水平镶嵌因子v
  • insideTessellationFactor[1]为 的所有内部值提供垂直镶嵌因子u

理解三角形面片

对于三角形面片,面片中的位置是一个(u,v,w)重心坐标,表示三角形三个顶点对顶点位置的相对影响,如图12-3所示。
(u,v,w)值的范围分别为0.01.0,其中u+v+w=1.0


图 12-3 归一化参数空间中的三角形面片坐标

在这里插入图片描述


解释 MTLTriangleTessellationFactorsHalf 结构

MTLTriangleTessellationFactorsHalf结构定义如下:

typedef struct {
    uint16_t edgeTessellationFactor[3];
    uint16_t insideTessellationFactor;
} MTLTriangleTessellationFactorsHalf;

结构中的每个值都提供了一个特定的镶嵌因子:

  • edgeTessellationFactor[0]提供面片边缘的镶嵌因子,其中u=0(边缘 0)。
  • edgeTessellationFactor[1]提供面片边缘的镶嵌因子,其中v=0(边缘 1)。
  • edgeTessellationFactor[2]提供面片边缘的镶嵌因子,其中w=1(边缘 2)。
  • insideTessellationFactor提供内部镶嵌因子。

补丁丢弃规则

如果边缘镶嵌因子的值为负数、零或对应于浮点数NaN,则镶嵌器将丢弃该补丁。
如果内部镶嵌因子的值为负数,则镶嵌因子将被限制在tessellationPartitionMode属性定义的范围内,并且镶嵌器不会丢弃该补丁。

如果未丢弃补丁并将tessellationFactorScaleEnabled属性设置为YES,则曲面细分器会将边缘和内部曲面细分因子乘以setTessellationFactorScale:方法中指定的比例因子。

当一个补丁被丢弃时,不会生成新的图元,后镶嵌顶点函数不会执行,并且不会为该补丁产生任何可见的输出。


指定每个面片曲面细分因子缓冲区

每个面片的曲面细分因子都会写入MTLBuffer对象中,并通过调用setTessellationFactorBuffer:offset:instanceStride:方法作为输入传递给曲面细分器。
您必须在向同一MTLRenderCommandEncoder对象发出面片绘制调用之前调用此方法。


3、补丁功能

本节总结了 Metal 着色语言为支持曲面细分而做出的主要更改。
有关更多信息,请参阅《Metal 着色语言指南》中的函数、变量和限定符章节。


创建计算内核

计算内核是使用现有kernel函数限定符标识的内核函数。
例 12-1是计算内核函数签名的示例。


例 12-1 计算核函数签名

kernel void my_compute_kernel(...) {...}

计算内核完全受 Metal 着色语言现有功能的支持。
计算内核函数的输入和输出与常规内核函数相同。


创建后细分顶点函数

后镶嵌顶点函数是使用现有vertex函数限定符标识的顶点函数。
此外,新[[patch(patch-type), N]]属性 用于指定面片类型(patch-type)和面片中的控制点数量(N)。
例 12-2是后镶嵌顶点函数签名的示例。


例 12-2 细分后顶点函数签名

[[patch(quad, 16)]]
vertex float4 my_post_tessellation_vertex_function(...) {...}

注意: 在 OS X 中,您必须始终指定 补丁中的控制点数量。
在 iOS 和 tvOS 中,指定此值是可选的。
如果指定了此值,则它必须与numberOfPatchControlPoints补丁绘制调用的参数值匹配。


细分后顶点函数输入

后细分顶点函数的所有输入都作为以下一个或多个参数传递:

  • device缓冲区(在或constant地址空间中声明)、纹理或采样器等资源。
  • 每个面片的数据和面片控制点数据。
    这些数据要么直接从缓冲区读取,要么作为使用限定符声明的输入传递到后细分顶点函数[[stage_in]]
  • 内置变量,如表12-1所列。

Table 12-1 Attribute qualifiers for post-tessellation vertex function input arguments

属性限定符对应数据类型描述
[[patch_id]]ushort或者uint补丁标识符。
[[instance_id]]ushort或者uint每个实例的标识符,包括基本实例值(如果指定)。
[[base_instance]]ushort或者uint在读取每个实例数据之前,添加到每个实例标识符的基本实例值。
[[position_in_patch]]float2或者float3定义要评估的面片上的位置。
对于四边形面片,必须是float2
对于三角形面片,必须是float3

细分后顶点函数输出

后曲面细分顶点函数的输出与常规顶点函数相同。
如果后曲面细分顶点函数写入缓冲区,则其返回类型必须是void


4、曲面细分管道状态

本节总结了 Metal 框架 API 为支持曲面细分而进行的主要更改,涉及曲面细分管道状态。


构建计算管道

构建MTLComputePipelineState对象时,计算内核被指定为计算管道的一部分,如例 12-3 所示。
为了获得最佳性能,计算内核应在帧中尽早执行。
(现有的计算管道 API 无需更改即可支持计算内核或曲面细分。)


例 12-3 使用计算内核构建计算管道

// Fetch the compute kernel from the library
id <MTLFunction> computeKernel = [_library newFunctionWithName:@"my_compute_kernel"];
 
// Build the compute pipeline
NSError *pipelineError = NULL;
_computePipelineState = [_device newComputePipelineStateWithFunction:computeKernel error:&pipelineError];
if (!_computePipelineState) {
    NSLog(@"Failed to create compute pipeline state, error: %@", pipelineError);
}

构建渲染管道

曲面细分器配置为渲染管道的一部分,使用MTLRenderPipelineDescriptor对象构建MTLRenderPipelineState对象。
使用vertexFunction属性 指定后曲面细分顶点函数。
例 12-4演示了如何使用曲面细分器和后曲面细分顶点函数配置和构建渲染管道。
有关更多信息,请参阅MTLRenderPipelineDescriptor类参考的指定曲面细分状态和MTLTessellationFactorStepFunction部分。


例 12-4 使用曲面细分器和后曲面细分顶点函数构建渲染管道

// Fetch the post-tessellation vertex function from the library
id <MTLFunction> postTessellationVertexFunction = [_library newFunctionWithName:@"my_post_tessellation_vertex_function"];
 
// Fetch the fragment function from the library
id <MTLFunction> fragmentFunction = [_library newFunctionWithName:@"my_fragment_function"];
 
// Configure the render pipeline, using the default tessellation values
MTLRenderPipelineDescriptor *renderPipelineDescriptor = [MTLRenderPipelineDescriptor new];
renderPipelineDescriptor.colorAttachments[0].pixelFormat = _view.colorPixelFormat;
renderPipelineDescriptor.fragmentFunction = fragmentFunction;
renderPipelineDescriptor.vertexFunction = postTessellationVertexFunction;
renderPipelineDescriptor.maxTessellationFactor = 16;
renderPipelineDescriptor.tessellationFactorScaleEnabled = NO;
renderPipelineDescriptor.tessellationFactorFormat = MTLTessellationFactorFormatHalf;
renderPipelineDescriptor.tessellationControlPointIndexType = MTLTessellationControlPointIndexTypeNone;
renderPipelineDescriptor.tessellationFactorStepFunction = MTLTessellationFactorStepFunctionConstant;
renderPipelineDescriptor.tessellationOutputWindingOrder = MTLWindingClockwise;
renderPipelineDescriptor.tessellationPartitionMode = MTLTessellationPartitionModePow2;
 
// Build the render pipeline
NSError *pipelineError = NULL;
_renderPipelineState = [_device newRenderPipelineStateWithDescriptor:renderPipelineDescriptor error:&pipelineError];
if (!_renderPipelineState) {
    NSLog(@"Failed to create render pipeline state, error %@", pipelineError);
}

5、补丁绘制调用

本节总结了对 Metal 框架 API 的主要更改,以支持与补丁绘制调用有关的曲面细分。


绘制镶嵌面片

要渲染多个细分面片的实例,请调用以下MTLRenderCommandEncoder方法之一:

  • drawPatches:patchStart:patchCount:patchIndexBuffer:patchIndexBufferOffset:instanceCount:baseInstance:
  • drawPatches:patchIndexBuffer:patchIndexBufferOffset:indirectBuffer:indirectBufferOffset:
  • drawIndexedPatches:patchStart:patchCount:patchIndexBuffer:patchIndexBufferOffset:controlPointIndexBuffer:controlPointIndexBufferOffset:instanceCount:baseInstance:
  • drawIndexedPatches:patchIndexBuffer:patchIndexBufferOffset:controlPointIndexBuffer:controlPointIndexBufferOffset:indirectBuffer:indirectBufferOffset:

注意: 仅当vertexFunction属性设置为后镶嵌顶点函数 时,才可以调用这些补丁绘制调用;调用非补丁绘制调用会导致验证层报告错误。

补丁绘制调用不支持原始重启功能。

对于所有补丁绘制调用,每个补丁数据和补丁控制点数组 都会从baseInstance参数中指定的值 开始组织成连续数组元素进行渲染。
有关每个参数的更多信息,请参阅MTLRenderCommandEncoder协议参考中的绘制细分补丁部分。

为了渲染补丁数据,补丁绘制调用会获取每个补丁的数据和补丁控制点数据。
补丁数据通常一起存储在一个或多个缓冲区中,用于一个或多个网格的所有补丁。
执行计算内核以生成场景相关的每个补丁细分因子;计算内核可能决定只为未被丢弃的补丁生成因子,在这种情况下补丁不连续。
因此,使用补丁索引缓冲区来识别要绘制的补丁的补丁 ID。

[patchStart, patchStart+patchCount-1] 范围内的缓冲区索引 (drawPatchIndex)用于引用数据。
如果用于获取每个补丁数据和补丁控制点数据的补丁索引不连续,drawPatchIndex则可以引用patchIndexBuffer,如图12-4 所示。


图 12-4 使用patchIndexBuffer获取每个补丁数据和补丁控制点数据

在这里插入图片描述


patchIndexBuffer 的每个元素都包含一个 32 位patchIndex值,该值引用每个补丁的数据和补丁控制点数据。
patchIndexBuffer 提取patchIndex的位置为:(drawPatchIndex * 4) + patchIndexBufferOffset

补丁的控制点索引通过以下方式计算:

patchIndex * numberOfPatchControlPoints * ((patchIndex + 1) * numberOfPatchControlPoints) - 1

patchIndexBuffer还使用patchIndex来读取每个面片数据 和面片控制点数据的索引,与用于读取每个面片曲面细分因子的索引不同。
对于曲面细分器,drawPatchIndex直接用作获取每个面片曲面细分因子的索引。

如果patchIndexBufferNULL,则drawPatchIndexpatchIndex为相同值,如图12-5所示。


图 12-5 获取每个补丁数据和补丁控制点数据(如果patchIndexBufferNULL

在这里插入图片描述


如果控制点在多个补丁之间共享,或者补丁控制点数据不连续,请使用drawIndexedPatches方法。
patchIndex引用指定的controlPointIndexBuffer,其中包含补丁的控制点索引,如图12-6所示。
tessellationControlPointIndexType描述中的控制点索引的大小,并且必须是MTLTessellationControlPointIndexTypeUInt16MTLTessellationControlPointIndexTypeUInt32。)


图 12-6 使用controlPointIndexBuffer获取补丁控制点数据

在这里插入图片描述


controlPointIndexBuffer 中第一个控制点索引的实际位置计算如下:

controlPointIndexBufferOffset + (patchIndex * numberOfPatchControlPoints * controlPointIndexType == UInt16 ? 2 : 4) 

必须从第一个控制点索引的位置开始,在controlPointIndexBuffer连续地存储几个(numberOfPatchControlPoints)控制点索引。


6、示例代码

有关如何设置基本曲面细分管道的示例,请参阅MetalBasicTessellation示例。


7、将 DirectX 11 样式曲面细分着色器移植到 Metal

本节介绍如何将 DirectX 11 风格的曲面细分顶点和外壳着色器移植到 Metal 计算内核。

注意: Metal 曲面细分器执行与 DirectX 11 曲面细分器等效的计算。
Metal 后曲面细分顶点函数执行与 DirectX 11 域着色器等效的计算。

在 DirectX 11 中,HLSL 顶点着色器针对补丁的每个控制点执行。
HLSL 外壳着色器由两个函数指定:一个针对补丁的每个控制点执行的函数,另一个针对每个补丁执行的函数。
顶点着色器的输出是构成外壳着色器的这两个函数的输入。

例 12-5展示了一个简单的 HLSL 顶点和外壳着色器。


例 12-5 简单的 HLSL 顶点和外壳着色器

struct VertexIn
{
    float3 PosL;
    float3 NormalL;
    float3 TangentL;
    float2 Tex;
};
 
struct VertexOut
{
    float3 PosW       : POSITION;
    float3 NormalW    : NORMAL;
    float3 TangentW   : TANGENT;
    float2 Tex        : TEXCOORD;
    float  TessFactor : TESS;
};
 
VertexOut VS(VertexIn vin)
{
    VertexOut vout;
 
    // Transform to world space space.
    vout.PosW     = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
    vout.NormalW  = mul(vin.NormalL, (float3x3)gWorldInvTranspose);
    vout.TangentW = mul(vin.TangentL, (float3x3)gWorld);
 
    // Output vertex attributes for interpolation across triangle.
    vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
 
    float d = distance(vout.PosW, gEyePosW);
 
    // Normalized tessellation factor.
    // The tessellation is
    //   0 if d >= gMinTessDistance and
    //   1 if d <= gMaxTessDistance.
    float tess = saturate( (gMinTessDistance - d) /
                   (gMinTessDistance - gMaxTessDistance) );
 
    // Rescale [0,1] --> [gMinTessFactor, gMaxTessFactor].
    vout.TessFactor = gMinTessFactor + tess*(gMaxTessFactor-gMinTessFactor);
 
    return vout;
}
 
struct HullOut
{
    float3 PosW     : POSITION;
    float3 NormalW  : NORMAL;
    float3 TangentW : TANGENT;
    float2 Tex      : TEXCOORD;
};
 
[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("PatchHS")]
HullOut HS(InputPatch<VertexOut,3> p,
           uint i : SV_OutputControlPointID,
           uint patchId : SV_PrimitiveID)
{
    HullOut hout;
 
    // Pass through shader.
    hout.PosW     = p[i].PosW;
    hout.NormalW  = p[i].NormalW;
    hout.TangentW = p[i].TangentW;
    hout.Tex      = p[i].Tex;
 
    return hout;
}
 
struct PatchTess
{
    float EdgeTess[3] : SV_TessFactor;
    float InsideTess  : SV_InsideTessFactor;
};
 
PatchTess PatchHS(InputPatch<VertexOut,3> patch,
                  uint patchID : SV_PrimitiveID)
{
    PatchTess pt;
 
    // Average tess factors along edges, and pick an edge tess factor for
    // the interior tessellation.  It is important to do the tess factor
    // calculation based on the edge properties so that edges shared by
    // more than one triangle will have the same tessellation factor.
    // Otherwise, gaps can appear.
    pt.EdgeTess[0] = 0.5f*(patch[1].TessFactor + patch[2].TessFactor);
    pt.EdgeTess[1] = 0.5f*(patch[2].TessFactor + patch[0].TessFactor);
    pt.EdgeTess[2] = 0.5f*(patch[0].TessFactor + patch[1].TessFactor);
    pt.InsideTess  = pt.EdgeTess[0];
 
    return pt;
}

这些简单的 HLSL 顶点和外壳着色器可以移植到 Metal 函数,并且可以创建调用这些 Metal 函数的计算内核,将这些函数作为单个内核执行。
移植的顶点和控制点外壳函数在计算内核中按线程调用,然后是线程组屏障,然后按补丁外壳函数由线程组中的线程子集执行。
能够直接调用内核中转换后的顶点和外壳函数,让开发人员能够非常轻松地将他们的顶点和外壳着色器从 DirectX 11 移植到 Metal。

简单的 HLSL 顶点和外壳着色器可以移植到如例 12-6所示的 Metal 函数。


例 12-6 移植到 Metal 函数的简单 HLSL 顶点和外壳着色器

struct VertexIn
{
    float3 PosL  [[ attribute(0) ]];
    float3 NormalL  [[ attribute(1) ]];
    float3 TangentL  [[ attribute(2) ]];
    float2 Tex  [[ attribute(3) ]];
};
 
struct VertexOut
{
    float3 PosW [[ position ]];
    float3 NormalW;
    float3 TangentW;
    float2 Tex;
    float  TessFactor;
};
 
struct ConstantData {;
}
 
// The vertex control point function
VertexOut
VS(VertexIn vin,
   constant ConstantData &c)
{
    VertexOut vout;
 
    // Transform to world space space.
    vout.PosW     = mul(float4(vin.PosL, 1.0f), c.gWorld).xyz;
    vout.NormalW  = mul(vin.NormalL, (float3x3)c.gWorldInvTranspose);
    vout.TangentW = mul(vin.TangentL, (float3x3)c.gWorld);
 
    // Output vertex attributes for interpolation across triangle.
    vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), c.gTexTransform).xy;
 
    float d = distance(vout.PosW, gEyePosW);
 
    // Normalized tessellation factor.
    // The tessellation is
    //   0 if d >= gMinTessDistance and
    //   1 if d <= gMaxTessDistance.
    float tess = saturate( (c.gMinTessDistance - d) /
                   (c.gMinTessDistance - c.gMaxTessDistance) );
 
    // Rescale [0,1] --> [gMinTessFactor, gMaxTessFactor].
    vout.TessFactor = c.gMinTessFactor +
                tess * (c.gMaxTessFactor - c.gMinTessFactor);
 
    return vout;
}
 
struct HullOut
{
    float3 PosW [[ position ]];
    float3 NormalW;
    float3 TangentW;
    float2 Tex;
}
 
// The patch control point function
HullOut
HS(VertexOut p)
{
    HullOut hout;
 
    // Pass through shader.
    hout.PosW     = p.PosW;
    hout.NormalW  = p.NormalW;
    hout.TangentW = p.TangentW;
    hout.Tex      = p.Tex;
 
    return hout;
}
 
struct PatchTess
{
    packed_half3 EdgeTess;
    half  InsideTess;
};
 
// The per-patch function
PatchTess
PatchHS(threadgroup VertexOut *patch)
{
    PatchTess pt;
 
    // Average tess factors along edges, and pick an edge tess factor for
    // the interior tessellation.  It is important to do the tess factor
    // calculation based on the edge properties so that edges shared by
    // more than one triangle will have the same tessellation factor.
    // Otherwise, gaps can appear.
    pt.EdgeTess[0] = 0.5f*(patch[1].TessFactor + patch[2].TessFactor);
    pt.EdgeTess[1] = 0.5f*(patch[2].TessFactor + patch[0].TessFactor);
    pt.EdgeTess[2] = 0.5f*(patch[0].TessFactor + patch[1].TessFactor);
    pt.InsideTess  = pt.EdgeTess[0];
 
    return pt;
}
A compute kernel that calls these vertex and hull functions can be:
struct KernelPatchInfo {
    uint numPatches; // total number of patches to process.
                     // we need this because this value may
                     // not be a multiple of threadgroup size.
    ushort numPatchesInThreadGroup; // number of patches processed by a
                                    // thread-group
    ushort numControlPointsPerPatch;
};  // passed as a constant buffer using setBytes by the runtime
 
kernel void
PatchKernel(VertexIn vIn [[ stage_in ]],
            constant ConstantData &c [[ buffer(1) ]],
            constant KernelPatchInfo &patchInfo [[ buffer(2) ]],
            PatchTess *tessellationFactorBuffer [[ buffer(3) ]],
            device HullOut *hullOutputBuffer [[ buffer(4) ]],
            threadgroup HullOut *hullOutputTGBuffer [[ threadgroup(0) ]],
            uint tID [[ thread_position_in_grid ]],
            ushort lID [[ thread_position_in_threadgroup ]],
            ushort lSize [[ threads_in_threadgroup ]],
            ushort groupID [[ threadgroup_position_in_grid ]])
{
    ushort n = patchInfo.numControlPointsPerPatch;
    uint patchGroupID = groupID * patchInfo.numPatchesInThreadGroup;
 
    // execute the vertex and control-point hull function per-thread
    if ( (lID <= (patchInfo.numPatchesInThreadGroup * n) &&
         (tID <= (patchInfo.numPatches * n)) )
    {
        uint controlPointID = patchGroupID * n + lID;
 
        VertexOut vOut = VS(vIn, c);
      HullOut hOut = HS(vOut);
 
        hullOutputTGBuffer[lID] = hOut;
      hullOutputBuffer[controlPointID] = hOut;
    }
 
    threadgroup_barrier(mem_flags::mem_threadgroup);
 
    // execute the per-patch hull function
    if (lID < patchInfo.numPatchesInThreadGroup)
    {
        uint patchID = patchGroupID + lID;
        tessellationFactorBuffer[patchID] = PatchHS(
                                                  hullOutputTGBuffer[lID*n]);
    }
}

笔记:

  • 线程组大小应设置为 SIMD 大小或 SIMD 大小的倍数。
  • 线程组中的补丁数量由给出threadgroup size / number of control points in a patch
  • HLSL 外壳着色器中描述了每个补丁的控制点数量。
  • 本移植示例中,输入输出控制点数量相同。
    通过对计算内核进行少量修改,也可以支持输入输出控制点数量不相同的情况。

十四、资源堆

可用iOS_GPUFamily1_v3iOS_GPUFamily2_v3iOS_GPUFamily3_v2tvOS_GPUFamily1_v2

资源堆允许 Metal 资源由相同的内存分配支持。
这些资源是从称为*堆(heap)的内存池创建的,并由捕获和管理 GPU 工作依赖关系的**隔离来跟踪。
资源堆可帮助您的应用降低以下成本:

  • 资源创建。
    资源创建可能涉及分配新内存、将其映射到进程中以及将其填零。
    通过从更大的堆或由堆支持的回收资源内存创建资源,可以降低此成本。
  • 固定内存预算。
    如果某些资源在一段时间内未使用,虚拟内存可能会压缩资源内存以节省空间。
    这可能会导致花费额外的时间来使该资源内存再次可供下次使用。
    通过使用少量堆,您可以将分配保持在给定的内存预算内,并确保这些资源持续使用(这有助于提供更一致的性能)。
  • 瞬态资源。
    瞬态资源会为每一帧生成和使用,但并非所有这些资源都会同时使用。
    为了减少内存消耗,未一起使用的瞬态资源可以共享由堆支持的同一内存。

1、堆

MTLHeap 对象是一种 Metal 资源,代表抽象内存池。
从此堆创建的资源被定义为可别名不可别名
当子分配资源与另一个别名资源共享同一部分堆内存时,它们就是别名资源


创建堆

通过调用MTLDevice对象的newHeapWithDescriptor:方法可以创建MTLHeap对象。
MTLHeapDescriptor对象描述了堆的存储模式、CPU 缓存模式和字节大小。
从同一个堆中子分配的所有资源共享相同的存储模式和 CPU 缓存模式。
堆的字节大小必须始终足够大,以便为其资源分配足够的内存。

通过调用setPurgeableState:方法,可以在创建堆后将其设置为可清除。
堆可清除状态指的是其整个后备内存,并影响堆内的所有资源。
堆是可清除的,但其资源不是;子分配的资源仅反映堆的可清除状态。
可清除性对于仅存储渲染目标的堆可能很有用。


二次分配堆资源

MTLBufferMTLTexture对象 都可以从堆中进行子分配。
为此,请调用MTLHeap对象的以下两个方法之一:

  • newBufferWithLength:options:
  • newTextureWithDescriptor:

默认情况下,每个子分配的资源都定义为不可别名,这可防止将来的子分配资源使用其内存。
要使子分配的资源可别名,请调用makeAliasable方法;这允许将来的子分配资源重用其内存。

可别名的子分配资源不会被销毁,命令编码器仍可使用。
这些资源持有对其堆的强引用,该引用仅在资源本身被销毁时释放,而不会在资源变为可别名时释放。
子分配资源仅在引用它们的所有命令缓冲区都已完成执行后才能被销毁。


注意: 堆是线程安全的,但您可能仍需要在应用程序级别同步堆以确保别名设置符合预期。

子分配资源之间的命令依赖关系不是自动的;必须通过 Fences 部分中描述的 MTLFence API明确声明和管理手动跟踪。

例 13-1展示了使用堆进行简单的资源子分配。


例 13-1 简单堆创建和资源子分配

// Calculate the size and alignment of each resource
MTLSizeAndAlign albedoSizeAndAlign = [_device heapTextureSizeAndAlignWithTextureDescriptor:_albedoDescriptor];
MTLSizeAndAlign normalSizeAndAlign = [_device heapTextureSizeAndAlignWithTextureDescriptor:_normalDescriptor];
MTLSizeAndAlign glossSizeAndAlign  = [_device heapTextureSizeAndAlignWithTextureDescriptor:_glossDescriptor];
 
// Calculate a heap size that satisfies the size requirements of all three resources
NSUInteger heapSize = albedoSizeAndAlign.size + normalSizeAndAlign.size + glossSizeAndAlign.size;
 
// Create a heap descriptor
MTLHeapDescriptor* heapDescriptor = [MTLHeapDescriptor new];
heapDescriptor.cpuCacheMode = MTLCPUCacheModeDefaultCache;
heapDescriptor.storageMode = MTLStorageModePrivate;
heapDescriptor.size = heapSize;
 
// Create a heap
id <MTLHeap> heap = [_device newHeapWithDescriptor:heapDescriptor];
 
// Create sub-allocated resources from the heap
id <MTLTexture> albedoTexture = [_heap newTextureWithDescriptor:_albedoDescriptor];
id <MTLTexture> normalTexture = [_heap newTextureWithDescriptor:_normalDescriptor];
id <MTLTexture> glossTexture  = [_heap newTextureWithDescriptor:_glossDescriptor];

2、Fences

MTLFence 对象用于跟踪和管理跨命令编码器的子分配资源依赖关系。
资源依赖关系的产生是因为资源由不同的命令生成和使用,无论这些命令是被编码到同一个队列还是不同的队列。
栅栏会捕获特定时间点之前的 GPU 工作;当 GPU 遇到栅栏时,它必须等到所有捕获的工作完成后才能继续执行。


创建栅栏

通过调用MTLDevice对象的newFence方法创建MTLFence对象。
fence主要用于跟踪目的,仅支持 GPU 内部跟踪,不支持 CPU 和 GPU 之间的跟踪。
MTLFence 协议不提供任何方法或完成处理程序,您只能修改label属性。

注意: 隔离栅可以重复更新,并且硬件管理隔离栅更新以防止死锁。


跨 Blit 和计算命令编码器跟踪栅栏

MTLBlitCommandEncoderMTLComputeCommandEncoder对象都 可以通过栅栏进行跟踪。
要更新栅栏,请分别调用每个命令编码器的updateFence:updateFence:方法。
要等待栅栏,请分别调用每个命令编码器的waitForFence:waitForFence:方法。

当命令缓冲区实际提交给硬件时,会更新或评估围栏。
这可以维护全局顺序并防止死锁。

驱动程序可能会在命令编码器开始时等待栅栏,驱动程序可能会将栅栏更新延迟到命令编码器结束。
因此,您不能先更新,然后等待同一命令编码器中的同一栅栏(但是,您可以先等待,然后更新)。
生产者-消费者关系必须跨不同的命令编码器进行拆分。


跨渲染命令编码器跟踪围栏

MTLRenderCommandEncoder可以使用栅栏 以更精细的粒度跟踪对象。
MTLRenderStages枚举允许您指定更新或等待栅栏的渲染阶段,从而允许顶点和片段命令重叠执行。
调用updateFence:afterStages:方法可更新栅栏,调用waitForFence:beforeStages:方法可等待栅栏。


栅栏示例

例 13-2展示了使用围栏进行简单跟踪。


例 13-2 简单的围栏跟踪

id <MTLFence> fence = [_device newFence];
id <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
 
// Producer
id <MTLRenderCommandEncoder> renderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_descriptor];
/* Draw using resources associated with 'fence' */
[renderCommandEncoder updateFence:fence afterStages:MTLRenderStageFragment];
[renderCommandEncoder endEncoding];
 
// Consumer
id <MTLComputeCommandEncoder> computeCommandEncoder = [commandBuffer computeCommandEncoder];
[computeCommandEncoder waitForFence:fence];
/* Dispatch using resources associated with 'fence' */
[computeCommandEncoder endEncoding];
 
[commandBuffer commit];

如果只有后一个命令编码器更新了栅栏,您就不能假设两个命令编码器会完成。
消费者命令编码器必须明确等待所有与栅栏发生冲突的命令编码器。
(除非遇到栅栏,否则 GPU 可能会开始执行尽可能多的命令。)例 13-3显示了引入竞争条件的栅栏的错误使用。


例 13-3 不正确的围栏跟踪

id <MTLFence> fence = [_device newFence];
id <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
 
// Producer 1
id <MTLRenderCommandEncoder> producerCommandEncoder1 = [commandBuffer renderCommandEncoderWithDescriptor:_descriptor];
/* Draw using resources associated with 'fence' */
[producerCommandEncoder1 endEncoding];
 
// Producer 2
id <MTLComputeCommandEncoder> producerCommandEncoder2 = [commandBuffer computeCommandEncoder];
/* Encode */
[producerCommandEncoder2 updateFence:fence];
[producerCommandEncoder2 endEncoding];
 
// Race condition at consumption!
// producerCommandEncoder2 updated the fence and will have completed its work
// producerCommandEncoder1 did not update the fence and therefore there is no guarantee that it will have completed its work
// Consumer
id <MTLComputeCommandEncoder> computeCommandEncoder = [commandBuffer computeCommandEncoder];
[computeCommandEncoder waitForFence:fence];
/* Dispatch using resources associated with 'fence' */
[computeCommandEncoder endEncoding];
 
[commandBuffer commit];

您仍需负责对命令缓冲区提交队列进行排序,如例 13-4所示。
但是,隔离不允许您控制队列间的命令缓冲区排序。


例 13-4 跨命令缓冲区提交队列的排序隔离

id <MTLFence> fence = [_device newFence];
id <MTLCommandBuffer> commandBuffer0 = [_commandQueue0 commandBuffer];
id <MTLCommandBuffer> commandBuffer1 = [_commandQueue1 commandBuffer];
 
// Producer
id <MTLRenderCommandEncoder> renderCommandEncoder = [commandBuffer0 renderCommandEncoderWithDescriptor:_descriptor];
/* Draw using resources associated with 'fence' */
[renderCommandEncoder updateFence:fence afterStages:MTLRenderStageFragment];
[renderCommandEncoder endEncoding];
 
// Consumer
id <MTLComputeCommandEncoder> computeCommandEncoder = [commandBuffer1 computeCommandEncoder];
[computeCommandEncoder waitForFence:fence];
/* Dispatch using resources associated with 'fence' */
[computeCommandEncoder endEncoding];
 
// Ensure 'commandBuffer0' is scheduled before 'commandBuffer1'
[commandBuffer0 addScheduledHandler:^(id <MTLCommandBuffer>) {
    [commandBuffer1 commit];
}];
[commandBuffer0 commit];

3、最佳实践


根据渲染目标类型使用单独的堆

某些设备无法任意别名子分配的资源;例如,可压缩深度纹理和 MSAA 纹理。
您应该为每种类型的渲染目标创建不同的堆:颜色、深度、模板和 MSAA。


为可别名和不可别名资源分配单独的堆

当将子分配的资源设为可别名时,您必须假设此资源将与所有未来的堆子分配别名。
如果您稍后分配不可别名的资源(例如较长寿命的纹理),那么这些资源可能会与您的临时资源别名,并且很难正确跟踪。

如果保留至少两个资源堆,那么跟踪哪些是别名、哪些不是别名会变得容易得多:一个用于可别名资源(例如,渲染目标),一个用于不可别名资源(例如,资产纹理或顶点缓冲区)。


分离堆以减少碎片

创建或删除许多不同大小的子分配资源可能会造成内存碎片。
碎片整理需要您明确地从碎片堆复制到另一个堆。
或者,您可以创建多个专用于大小相似的子分配资源的堆。

堆也可以用作堆栈。
用作堆栈时,不会发生碎片。


最小化围栏

细粒度的隔离很难管理,而且会降低堆的跟踪优势。
避免对每个子分配资源使用隔离;相反,使用单个隔离来跟踪具有相同同步要求的所有子分配资源。


考虑跟踪非堆资源

手动数据危险跟踪已扩展到直接从MTLDevice对象创建的资源。
创建资源时指定新MTLResourceHazardTrackingModeUntracked资源选项,然后使用围栏跟踪它。
手动跟踪可以减少许多只读资源的自动跟踪开销。


4、示例代码

有关如何使用堆和栅栏的示例,请参阅MetalHeapsAndFences示例。


2024-06-08(六)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/707629.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

next.js开发中页面回退时报Unhandled Runtime ErrorTypeError destroy is not a function

Next.js开发中页面回退时报Unhandled Runtime Error:TypeError: destroy is not a function 问题描述 在Next.js开发中&#xff0c;从A页面跳转到B页面&#xff0c;再使用浏览器回退到A页面时报上述错误&#xff1a; 错误原因 是因为在B页面里&#xff0c;在使用useEffect时…

从“数据孤岛”、Data Fabric(数据编织)谈逻辑数据平台

提到逻辑数据平台&#xff0c;其核心在于“逻辑”&#xff0c;与之相对的便是“物理”。在过去&#xff0c;为了更好地利用和管理数据&#xff0c;我们通常会选择搭建数据仓库和数据湖&#xff0c;将所有数据物理集中起来。但随着数据量、用数需求和用数人员的持续激增&#xf…

掌握midjourney系列:8 个角色设计关键词

Midjourney V6的角色引用功能非常强大&#xff0c;可以让多张图片生成的角色风格保持一致。在实现同一角色的多张场景图片之前&#xff0c;我们需要先设计好基础角色。 以下是我总结的Midjourney中人物设计套路的常用提示&#xff0c;很高兴与大家分享。 1、角色设定表&#…

南京威雅学校:初中转轨国际化教育,她们打开了成长的另一种可能

“上了大学就轻松了。” 又是一年高考季&#xff0c;每每回想起十八岁前那些没日没夜埋头学习的日子&#xff0c;已经为人父母的你是不是也忍不住想要孩子气地吐槽一句&#xff0c;“骗人”——人不会在一场考试后瞬间长大&#xff0c;试卷里也没有人生的全部答案。 三年前&a…

Java-多线程

概念 进程&#xff1a;程序的基本执行实体 线程&#xff1a;操作系统能够进行运算调度的最小单位&#xff0c;被包含在进程之中&#xff0c;是进程的实际运作单位 并发&#xff1a;同一时刻&#xff0c;多个指令在单个CPU上交替执行。 并行&#xff1a;同一时刻&#xff0c;多…

博物馆藏品管理的重要性

博物馆是人们了解历史文化、传承文明的重要场所。而博物馆的藏品管理是博物馆的核心工作之一&#xff0c;对于展现博物馆的魅力、吸引观众的眼球有着至关重要的影响。并且博物馆藏品管理是一项复杂且专业的工作&#xff0c;它涉及到多个方面&#xff0c;包括但不限于藏品的收集…

前沿重器[49] | 聊聊搜索系统2:常见架构

前沿重器 栏目主要给大家分享各种大厂、顶会的论文和分享&#xff0c;从中抽取关键精华的部分和大家分享&#xff0c;和大家一起把握前沿技术。具体介绍&#xff1a;仓颉专项&#xff1a;飞机大炮我都会&#xff0c;利器心法我还有。&#xff08;算起来&#xff0c;专项启动已经…

Unity 踩坑记录 用自定义类 创建的List不显示在 inspector面板

在 自定义类上面添加 【Serializable 】 扩展&#xff1a; 1&#xff1a;Serializable 序列化的是可序列化的类或结构。并且只能序列化非抽象非泛型的自定义的类 2&#xff1a;SerializeField是强制对私有字段序列化

单例模式、工厂模式 c++关键字 static

static 关键字的作用&#xff1a; 主要作用在于 控制变量或函数的作用域、生命周期以及它们如何被不同部分的程序访问&#xff0c;从而帮助程序员管理内存、避免命名冲突&#xff0c;并实现特定的设计模式&#xff08;如单例模式&#xff09;。 1. 静态局部变量&#xff1a;当…

工具推荐-文件捆绑工具

前提 在之前有突发奇想过&#xff0c;有没有那种我发给别人一个pdf文件&#xff0c;别人点击后看到的是pdf文件的内容&#xff0c;我这边也看到了上线的提示。于是就去研究pdf能加入哪些特殊的功能。看了一段时间后发现pdf的一些不一样的功能 像是打开pdf后弹出一个框 或者是…

什么是端口转发?路由器如何正确的设置端口转发和范围转发?(外网访问必备设置)

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 端口转发 📒🚀 端口转发的应用场景💡 路由器如何设置端口转发(示例)💡 端口范围转发(示例)🎯 范围转发的应用场景🛠️ 设置范围转发📝 范围转发实操示例🎈 注意事项 🎈⚓️ 相关链接 ⚓️📖 介绍 📖 …

wmv转换mp4怎么操作?3个格式转换方法分享

wmv转换mp4怎么操作&#xff1f;将WMV转换为MP4格式&#xff0c;可以方便我们在多种设备和平台上流畅播放视频。MP4格式具有广泛的兼容性和优化过的编码&#xff0c;使其在各种媒体播放器、智能手机、平板电脑以及电视上都能得到良好的支持。此外&#xff0c;MP4格式的视频文件…

手机直播不用麦克风可以吗?一文看懂无线麦克风哪个好

市面上对于无线麦克风的需求有增无减&#xff0c;原因是直播、短视频行业火爆&#xff0c;许多人都开始加入这一行业&#xff0c;不过对于麦克风这类产品的疑问也越来越多。例如&#xff1a;无线麦克风怎么选&#xff1f;实不实用&#xff1f;手机直播不用麦克风可以吗&#xf…

安卓启动流程

还是以高通为例子。这次整理并不是很完善&#xff0c;下来会参考一些文档再整理。。。 高通平台启动过程_高通平台启动流程-CSDN博客 https://www.cnblogs.com/schips/p/how_qualcomm_soc_boot.html 1. 初始启动阶段&#xff08;Boot ROM&#xff09; 处理器复位&#xff1a;…

OpenCV图像变换

一 图像的缩放 resize(src,dst,dsize,fx,fy,interpolation) fx&#xff1a;x轴的缩放因子 fy&#xff1a;y轴的缩放因子 interpolation 插值算法 INTER_NEAREST,临近插值&#xff0c;速度快&#xff0c;效果差 INTER_LINEAR,双线性插值&#xff0c;原图中的4个点 INTER_CUBIC…

【原创】springboot+mysql社区住户综合管理系统设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

Java接口实现与类继承

学习初期发现接口实现与类继承很像&#xff0c;随着学习深入发现它们之间的联系与区别&#xff0c;整理如下&#xff1a; 经实验发现&#xff0c;实现接口的类中含有接口中的所有属性和方法&#xff0c;继承父类的子类中也含有父类中所有的属性和方法&#xff0c;可以说接口实…

【SpringBoot + Vue 尚庭公寓实战】公寓杂费接口实现(八)

【SpringBoot Vue 尚庭公寓实战】公寓杂费接口实现&#xff08;八&#xff09; 文章目录 【SpringBoot Vue 尚庭公寓实战】公寓杂费接口实现&#xff08;八&#xff09;1、公寓杂费业务介绍2、公寓杂费逻辑模型介绍3、接口实现3.1、保存或更新杂费值3.2、保存或更新杂费名称3…

Python学习之旅:你的大学计算机专业宝藏路线图

在信息时代的浪潮中&#xff0c;Python以其强大的功能和极简的语法成为了无数程序员心中的白月光。作为大学计算机专业的学生&#xff0c;掌握Python不仅能够为未来的职业生涯铺路&#xff0c;更能让您在学术研究和实际应用中如鱼得水。今天&#xff0c;我将与大家分享一套实用…

南京观海微电子-----PCB设计怎样降低EMI

开关模式电源是AC-DC或DC-DC电源的通用术语&#xff0c;这些电源使用具有快速开关动作的电路进行电压转换/转换(降压或升压)。随着每天开发出更多的设备(潜在的EMI受害者)&#xff0c;克服EMI成为工程师面临的主要挑战&#xff0c;并且实现电磁兼容性(EMC)与使设备正常运行同等…