本文翻译整理自: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
对象,请调用 MTLCommandQueue
的 commandBuffer
方法。
MTLCommandBuffer
对象只能提交给 创建它的MTLCommandQueue
对象。
通过 commandBuffer
方法创建的命令缓冲区 会保留执行所需的数据。
在某些情况下,您会在对象执行期间 将这些MTLCommandBuffer
对象保留在其他地方,此时您可以通过调用 MTLCommandQueue
的commandBufferWithUnretainedReferences
方法来创建命令缓冲区。
仅对性能极为关键的应用,使用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
协议提供了在命令缓冲区中编码命令的方法,这些命令可以指定计算函数及其参数(例如,纹理、缓冲区和采样器状态)并调度计算函数进行执行。
要创建计算命令编码器对象,请使用MTLCommandBuffer
的 computeCommandEncoder
方法。
有关MTLComputeCommandEncoder
方法和属性的详细信息,请参阅 数据并行计算处理:计算命令编码器。
Blit 命令编码器
MTLBlitCommandEncoder
协议具有在缓冲区 (MTLBuffer
) 和纹理 (MTLTexture
)之间附加内存复制操作命令的方法。
MTLBlitCommandEncoder
协议还提供了用纯色填充纹理和生成 mipmap 的方法。
要创建 blit 命令编码器对象,请使用MTLCommandBuffer
的 blitCommandEncoder
方法。
有关MTLBlitCommandEncoder
的方法和属性的详细信息,请参阅 缓冲区和纹理操作:Blit 命令编码器。
多线程、命令缓冲区和命令编码器
大多数应用使用单个线程在单个命令缓冲区中对单个帧的渲染命令进行编码。
在每一帧结束时,您都会提交命令缓冲区,该缓冲区会安排并开始执行命令。
如果您想要并行化命令缓冲区编码,那么您可以同时创建多个命令缓冲区,并使用单独的线程对每个命令缓冲区进行编码。
如果您提前知道命令缓冲区应按什么顺序执行,那么MTLCommandBuffer
的 enqueue
方法可以在命令队列中声明执行顺序,而无需等待命令被编码和提交。
否则,当命令缓冲区被提交时,它会在命令队列中被分配一个位置,排在任何先前排队的命令缓冲区之后。
每次只能有一个 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
对象来描述纹理的属性。MTLTexture
的newTextureViewWithPixelFormat:
方法创建一个 与调用对象共享相同存储分配的MTLTexture
对象。
由于它们共享相同的存储,因此对新纹理对象像素的任何更改都会反映在调用纹理对象中,反之亦然。
对于新创建的纹理,newTextureViewWithPixelFormat:
方法会重新解释 调用MTLTexture
对象存储分配的现有纹理图像数据,就好像数据是以指定的像素格式存储的一样。
新纹理对象的MTLPixelFormat
必须与原始纹理对象的MTLPixelFormat
兼容。
(有关普通、打包和压缩像素格式的详细信息,请参阅 纹理的像素格式。)MTLBuffer
的newTextureWithDescriptor:offset:bytesPerRow:
方法创建一个MTLTexture
对象,该对象将 调用MTLBuffer
对象 的存储分配 与其纹理图像数据共享。
由于它们共享相同的存储空间,因此对新纹理对象的像素的任何更改都会反映在调用纹理对象中,反之亦然。
在纹理和缓冲区之间共享存储空间可能会阻止使用某些纹理优化,例如像素混合或平铺。
使用纹理描述符创建纹理对象
MTLTextureDescriptor
定义用于创建MTLTexture
对象的属性,包括其图像大小(宽度、高度和深度)、像素格式、排列(数组或立方体类型)和 mipmap 数量。
这些MTLTextureDescriptor
属性仅在创建MTLTexture
对象期间使用。
创建MTLTexture
对象后,MTLTextureDescriptor
对象中的属性更改 不再对该纹理产生任何影响。
要从描述符创建一个或多个纹理:
- 创建一个
MTLTextureDescriptor
包含描述纹理数据的纹理属性的自定义对象:textureType
属性指定纹理的维数和排列(例如,数组或立方体)。width
、height
和depth
属性指定基础级别纹理 mipmap 各个维度的像素大小。pixelFormat
属性指定像素如何存储在纹理中。arrayLength
属性指定MTLTextureType1DArray
或MTLTextureType2DArray
类型纹理对象 的数组元素的数量。mipmapLevelCount
属性指定 mipmap 级别的数量。sampleCount
属性指定每个像素中的样本数。resourceOptions
属性指定其内存分配的行为。
- 通过调用
MTLDevice
对象的newTextureWithDescriptor:
方法 从MTLTextureDescriptor
对象创建纹理。
创建纹理后,调用replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:
方法加载纹理图像数据,详情请参阅 将图像数据复制到纹理和从纹理复制图像数据。 - 要创建更多
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
对象的width
、height
和depth
属性指定。 - 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
对象。
width
和height
值定义 2D 纹理的尺寸。
type
属性自动设置为MTLTextureType2D
,并且depth
和arrayLength
设置为 1。textureCubeDescriptorWithPixelFormat:size:mipmapped:
方法为立方体纹理创建一个MTLTextureDescriptor
对象,其中type
属性设置为MTLTextureTypeCube
,width
和height
设置为尺寸,并且depth
和arrayLength
设置为1。
这两种MTLTextureDescriptor
便捷方法都接受输入值,pixelFormat
,定义了纹理的像素格式。
这两种方法还接受输入值mipmapped
,它决定纹理图像是否经过 mipmapped。(如果mipmapped
是YES
,则纹理经过 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 纹理。
MTLPixelFormatGBGR422
和MTLPixelFormatBGRG422
是特殊的像素格式,用于在 YUV 颜色空间中存储像素。
这些格式仅支持 2D 纹理(但不支持 2D 数组或立方体类型),不支持 mipmap,甚至不支持width
。
有几种像素格式使用 sRGB 颜色空间值存储颜色分量(例如MTLPixelFormatRGBA8Unorm_sRGB
或MTLPixelFormatETC2_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
对象执行纹理采样操作时使用的寻址、过滤和其他属性。
采样器描述符定义采样器状态对象的属性。
要创建采样器状态对象:
- 调用
MTLDevice
对象的newSamplerStateWithDescriptor:
方法来创建MTLSamplerDescriptor
对象。 - 在
MTLSamplerDescriptor
对象中设置所需的值,包括过滤选项、寻址模式、最大各向异性和细节级别参数。 - 通过调用创建描述符的
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 函数限定符(vertex
、fragment
或kernel
)的函数都可以由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
是对象创建完成时调用的代码块。
从库中获取函数
MTLLibrary
的newFunctionWithName:
方法返回具有请求名称的MTLFunction
对象。
如果在库中未找到使用 Metal 着色语言函数限定符的函数名称,则newFunctionWithName:
返回nil
。
例 4-1使用 MTLDevice
的newLibraryWithFile:error:
方法 通过其完整路径名定位库文件,并使用其内容创建一个具有一个或多个MTLFunction
对象的MTLLibrary
对象。
加载文件时出现的任何错误都会返回到 error
中。
然后 MTLLibrary
的newFunctionWithName:
方法创建一个表示源代码中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
不提供对函数参数的访问。
在创建管道状态期间,可以获取反射对象(MTLRenderPipelineReflection
或 MTLComputePipelineReflection
,取决于命令编码器的类型),该对象揭示了着色器或计算函数参数的详细信息。
有关创建管道状态和反射对象的详细信息,请参阅 创建渲染管道状态 或 创建计算管道状态。
如果不使用反射数据,请避免获取它。
反射对象包含命令编码器支持的每种函数类型的MTLArgument
对象数组。
对于MTLComputeCommandEncoder
,在参数属性中 MTLComputePipelineReflection
有一个对象数组,对应于其计算函数的参数。
对于MTLRenderCommandEncoder
,MTLRenderPipelineReflection
有两个属性vertexArguments
和fragmentArguments
,它们是分别对应于顶点函数参数和片段函数参数的数组。
函数的参数并非全部都存在于反射对象中。
反射对象仅包含具有关联资源的参数,但不包含使用[[ 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
属性是相关的。
- 如果
type
为MTLArgumentTypeTexture
,则textureType
属性表示整体纹理类型(例如着色语言中的texture1d_array
、texturecube
和textureDataType
类型),属性texture2d_ms
表示组件数据类型(例如half
、float
、int
或uint
)。 - 如果
type
是MTLArgumentTypeThreadgroupMemory
,则threadgroupMemoryAlignment
和threadgroupMemoryDataSize
属性相关。 - 如果
type
是MTLArgumentTypeBuffer
,则bufferAlignment
、bufferDataSize
、bufferDataType
和bufferStructType
性质是相关的。
如果缓冲区参数是结构体(即bufferDataType
是MTLDataTypeStruct
),则bufferStructType
属性包含MTLStructType
来表示该结构 ,而却bufferDataSize
包含该结构体的大小(以字节为单位)。
如果缓冲区参数是数组(或指向数组的指针),则bufferDataType
指示元素的数据类型,并bufferDataSize
包含一个数组元素的大小(以字节为单位)。
例 4-3深入研究一个MTLStructType
对象以检查结构成员的详细信息,每个成员都由一个MTLStructMember
对象表示。
结构成员可以是简单类型、数组或嵌套结构。
如果成员是嵌套结构,则调用MTLStructMember
的structType
方法,获取表示该结构的MTLStructType
对象,然后递归深入分析它。
如果成员是数组,则使用MTLStructMember
的arrayType
方法获取代表它的MTLArrayType
。
然后检查MTLArrayType
的 elementType
属性。
如果elementType
是MTLDataTypeStruct
,则调用elementStructType
方法获取结构并继续深入研究其成员。
如果elementType
是MTLDataTypeArray
,则调用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
}
}
六、图形渲染:渲染命令编码器
本章介绍如何创建和使用MTLRenderCommandEncoder
和MTLParallelRenderCommandEncoder
对象,它们用于将图形渲染命令编码到命令缓冲区中。
MTLRenderCommandEncoder
命令描述图形渲染管道,如图5-1所示。
图 5-1 Metal 图形渲染管道
MTLRenderCommandEncoder
对象代表单个渲染命令编码器。
MTLParallelRenderCommandEncoder
对象可以将单个渲染过程分解为多个单独的MTLRenderCommandEncoder
对象,每个对象可以分配给不同的线程。
然后,来自不同渲染命令编码器的命令将链接在一起,并以一致、可预测的顺序执行,如渲染过程的多个线程中所述。
1、创建并使用渲染命令编码器
要创建、初始化和使用单个渲染命令编码器:
- 创建一个
MTLRenderPassDescriptor
对象来定义一组附件,这些附件用作该渲染过程的命令缓冲区中图形命令的渲染目标。
通常,您创建一次MTLRenderPassDescriptor
对象,并在应用每次渲染一帧时重复使用它。
请参阅 创建渲染过程描述符。 - 使用指定的渲染过程描述符,调用
MTLCommandBuffer
的renderCommandEncoderWithDescriptor:
方法来创建MTLRenderCommandEncoder
对象。
请参阅 使用渲染过程描述符创建渲染命令编码器。 - 创建一个
MTLRenderPipelineState
对象来定义一个或多个绘制调用的图形渲染管道(包括着色器、混合、多重采样和可见性测试)的状态。
要使用此渲染管道状态绘制图元,请调用MTLRenderCommandEncoder
的setRenderPipelineState:
方法。
有关详情,请参阅 创建渲染管道状态。 - 设置渲染命令编码器要使用的纹理、缓冲区和采样器,如为渲染命令编码器指定资源中所述。
- 调用
MTLRenderCommandEncoder
方法指定其他固定功能状态,包括深度和模板状态,如固定功能状态操作中所述。 - 最后,调用
MTLRenderCommandEncoder
方法绘制图形基元,如绘制几何基元中所述。
创建渲染过程描述符
MTLRenderPassDescriptor
对象 表示编码渲染命令的目标,它是附件的集合。
渲染过程描述符的属性可能包括最多四个用于颜色像素数据的附件、一个用于深度像素数据的附件和一个用于模板像素数据的附件。
renderPassDescriptor
便捷方法创建一个具有颜色、深度和模板附件属性的MTLRenderPassDescriptor
对象,并带有默认附件状态。
visibilityResultBuffer
属性指定一个缓冲区,设备可以更新该缓冲区以指示是否有任何样本通过深度和模板测试 - 有关详细信息,请参阅 固定功能状态操作。
每个单独的附件(包括将要写入的纹理)都由附件描述符表示。
对于附件描述符,必须适当选择相关纹理的像素格式来存储颜色、深度或模板数据。
对于颜色附件描述符,MTLRenderPassColorAttachmentDescriptor
,使用颜色可渲染像素格式。
对于深度附件描述符,MTLRenderPassDepthAttachmentDescriptor
,使用深度可渲染像素格式,例如MTLPixelFormatDepth32Float
。
对于模板附件描述符,MTLRenderPassStencilAttachmentDescriptor
,使用模板可渲染像素格式,例如MTLPixelFormatStencil8
。
纹理在设备上每个像素实际使用的内存量并不总是与 Metal 框架代码中纹理像素格式的大小相匹配,因为设备会添加填充以进行对齐或其他目的。
请参阅 Metal功能集表章节,了解每个像素格式实际使用的内存量,以及附件的大小和数量限制。
加载和存储操作
附件描述符的loadAction
和storeAction
属性,指定在渲染过程开始或结束时执行的操作。
(对于MTLParallelRenderCommandEncoder
,加载和存储操作发生在整个命令的边界,而不是其每个MTLRenderCommandEncoder
对象的边界。
有关详细信息,请参阅 渲染过程的多个线程。)
可能的loadAction
值包括:
MTLLoadActionClear
,它将相同的值写入指定附件描述符中的每个像素。
有关此操作的更多详细信息,请参阅 指定清除加载操作。MTLLoadActionLoad
,它保留了纹理的现有内容。MTLLoadActionDontCare
,它允许附件中的每个像素在渲染过程开始时采用任意值。
如果您的应用程序将为给定帧渲染附件的所有像素,请使用默认加载操作MTLLoadActionDontCare
。
MTLLoadActionDontCare
操作允许 GPU 避免加载纹理的现有内容,从而确保最佳性能。
否则,您可以使用操作MTLLoadActionClear
清除附件的先前内容,或使用MTLLoadActionLoad
操作保留它们。
MTLLoadActionClear
操作还可以避免加载现有纹理内容,但需要用纯色填充目标。
可能的storeAction
值包括:
MTLStoreActionStore
,将渲染过程的最终结果保存到附件中。MTLStoreActionMultisampleResolve
,它将渲染目标中的多重采样数据解析为单个采样值,将它们存储在附件resolveTexture
属性指定的纹理中,并使附件的内容保持未定义状态。
有关详细信息,请参阅 示例:为多重采样渲染创建渲染过程描述符。MTLStoreActionDontCare
,这会使附件在渲染过程完成后处于未定义状态。
这可以提高性能,因为它使实现能够避免保存渲染结果所需的任何工作。
对于颜色附件,MTLStoreActionStore
操作是默认的存储操作,因为应用程序几乎总是在渲染过程结束时在附件中保留最终颜色值。
对于深度和模板附件,MTLStoreActionDontCare
是默认的存储操作,因为这些附件在渲染过程完成后通常不需要保留。
指定清除加载操作
如果附件描述符的loadAction
属性设置为MTLLoadActionClear
,则在渲染过程开始时,会将清除值写入指定附件描述符中的每个像素。
清除值属性取决于附件的类型。
- 对于
MTLRenderPassColorAttachmentDescriptor
,clearColor
包含一个MTLClearColor
值,由四个双精度浮点 RGBA 分量组成的值,用于清除颜色附件。
MTLClearColorMake
函数 从红色、绿色、蓝色和 alpha 分量中创建一个清除颜色值。
默认清除颜色为 (0.0, 0.0, 0.0, 1.0),即不透明黑色。 - 对于
MTLRenderPassDepthAttachmentDescriptor
,clearDepth
包含一个双精度浮点清除值,范围在 [0.0, 1.0] 内,用于清除深度附件。默认值为 1.0。 - 对于
MTLRenderPassStencilAttachmentDescriptor
,clearStencil
包含一个 32 位无符号整数,用于清除模板附件。默认值为 0。
示例:使用加载和存储操作创建渲染过程描述符
例 5-1创建了一个带有颜色和深度附件的简单渲染过程描述符。
首先,创建两个纹理对象,一个具有可渲染颜色的像素格式,另一个具有深度像素格式。
接下来,MTLRenderPassDescriptor
的 renderPassDescriptor
便捷方法 创建一个默认的渲染过程描述符。
然后通过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
不支持多重采样,则多重采样解析操作的结果未定义。)
resolveLevel
、resolveSlice
和resolveDepthPlane
属性也可用于多重采样解析操作,以分别指定多重采样纹理的 mipmap 级别、立方体切片和深度平面。
在大多数情况下, resolveLevel
、resolveSlice
和 resolveDepthPlane
的值 默认可用。
在例 5-2中,最初创建一个附件,然后将其loadAction
、storeAction
、texture
和resolveTexture
属性 设置为支持多重采样解析。
例 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
属性 声明纹理的尺寸(以设备像素为单位)。
为确保您的应用能够以显示屏的精确尺寸呈现内容(某些设备上无需额外的采样阶段),请在计算图层的所需尺寸时考虑目标屏幕的nativeScale
或nativeBounds
属性。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
纹理的像素格式。 - 要在渲染管道状态中指定顶点或片段着色器,请分别设置
vertexFunction
或fragmentFunction
属性。
设置fragmentFunction
为nil
,将禁用将像素光栅化到指定的颜色附件中,这通常用于仅深度渲染或将数据从顶点着色器输出到缓冲区对象中。 - 如果顶点着色器具有带有每个顶点输入属性的参数,则设置
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
对象后,调用MTLRenderCommandEncoder
的 setRenderPipelineState:
方法 将渲染管道状态与命令编码器关联起来以用于渲染。
例 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];
变量vertFunc
和fragFunc
是着色器函数,它们被名为renderPipelineDesc
的渲染管道状态描述符的属性 指定。
调用MTLDevice
对象的newRenderPipelineStateWithDescriptor:error:
方法会同步使用管道状态描述符来创建渲染管道状态对象。
调用MTLRenderCommandEncoder
的 setRenderPipelineState:
方法 指定要与渲染命令编码器一起使用的MTLRenderPipelineState
对象。
注意: 由于MTLRenderPipelineState
创建对象的开销很大,因此每当您想要使用相同的图形状态时,都应该重复使用它。
在渲染管道附件描述符中配置混合
混合使用高度可配置的混合操作将片段函数(源)返回的输出与附件(目标)中的像素值混合。
混合操作确定源值和目标值如何与混合因子组合。
要配置颜色附件的混合,请设置以下MTLRenderPipelineColorAttachmentDescriptor
属性:
- 要启用混合,请设置
blendingEnabled
为YES
。默认情况下,混合是禁用的。 writeMask
标识混合哪些颜色通道。默认值MTLColorWriteMaskAll
允许混合所有颜色通道。rgbBlendOperation
和alphaBlendOperation
分别为 RGB 和 Alpha 片段数据的混合操作指定一个MTLBlendOperation
值。
这两个属性的默认值都是MTLBlendOperationAdd
。sourceRGBBlendFactor
,sourceAlphaBlendFactor
,destinationRGBBlendFactor
和destinationAlphaBlendFactor
分配源和目标混合因子。
了解混合因子和操作
四个混合因子指的是一个恒定的混合颜色值:MTLBlendFactorBlendColor
、MTLBlendFactorOneMinusBlendColor
、MTLBlendFactorBlendAlpha
和MTLBlendFactorOneMinusBlendAlpha
。
调用 MTLRenderCommandEncoder
的setBlendColorRed:green:blue:alpha:
方法 指定这些混合因子使用的恒定颜色和 alpha 值,如 固定功能状态操作中所述。
一些混合操作通过将源值乘以源MTLBlendFactor
值(缩写为 SBF)、将目标值乘以目标混合因子(DBF),然后使用MTLBlendOperation
值 指示的算术组合结果来组合片段值。
(如果混合操作是 MTLBlendOperationMin
或 MTLBlendOperationMax
,则忽略 SBF 和 DBF 混合因子。)
例如,MTLBlendOperationAdd
对于rgbBlendOperation
和alphaBlendOperation
属性,为 RGB 和 Alpha 值定义以下加法混合操作:
- RGB = (Source.rgb * sourceRGBBlendFactor) + (Dest.rgb * destinationRGBBlendFactor)
- Alpha = (Source.a * sourceAlphaBlendFactor) + (Dest.a * destinationAlphaBlendFactor)
在默认的混合行为中,源完全覆盖目标。
此行为相当于将sourceRGBBlendFactor
和设置sourceAlphaBlendFactor
为MTLBlendFactorOne
,将destinationRGBBlendFactor
和destinationAlphaBlendFactor
设置为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
对象中的vertexFunction
和fragmentFunction
属性指定。
这些方法将着色器资源(缓冲区、纹理和采样器)分配给渲染命令编码器中相应的参数表索引(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中,为顶点着色器定义了两个缓冲区(posBuf
和texCoordBuf
),其索引分别为 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中,为片段着色器定义了一个缓冲区、一个纹理和一个采样器(分别为fragmentColorBuf
、shadeTex
和sampler
),它们的索引均为 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 Math
的 VertexInput
结构所示。逐顶点输入结构的每个字段都有 [[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
有两个属性:attributes
和layouts
。
attributes
的属性 MTLVertexDescriptor
是一个MTLVertexAttributeDescriptorArray
对象,它定义每个顶点属性在映射到顶点函数参数的缓冲区中的组织方式。
attributes
属性可以支持对同一缓冲区内交错的多个属性(如顶点坐标、表面法线和纹理坐标)的访问。
着色语言代码中的成员顺序不必在框架代码中的缓冲区中保留。
数组中的每个顶点属性描述符都具有以下属性,它们为顶点着色器函数提供信息以定位和加载参数数据:
bufferIndex
,它是缓冲区参数表的索引,指定要访问哪个MTLBuffer
参数。
缓冲区参数表在为渲染命令编码器指定资源中讨论。format
,它指定了数据在框架代码中的解释方式。
如果数据类型不是精确匹配的类型,则可能会进行转换或扩展。
例如,如果着色语言类型为half4
,而框架format
为MTLVertexFormatFloat2
,则当数据用作顶点函数的参数时,可能会从浮点数转换为半值,并从两个元素扩展为四个元素(最后两个元素为 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
指定使用四个浮点值。
MTLVertexDescriptor
的layouts
属性是 一个MTLVertexBufferLayoutDescriptorArray
。
对于每个layouts
中的MTLVertexBufferLayoutDescriptor
,属性指定当 Metal 绘制图元时,如何从参数表中的相应MTLBuffer
位置获取顶点和属性数据。
(有关绘制图元的更多信息,请参阅 绘制几何图元。)
MTLVertexBufferLayoutDescriptor
的 stepFunction
属性确定是否为每个顶点、为一定数量的实例或仅一次获取属性数据。
如果stepFunction
设置为获取一定数量实例的属性数据,则MTLVertexBufferLayoutDescriptor
的 stepRate
属性确定多少个实例。
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
)或剔除哪种类型的图元(MTLCullModeFront
或MTLCullModeBack
)。
使用以下MTLRenderCommandEncoder
方法对固定功能状态 改变命令进行编码:
setScissorRect:
: 指定一个 2D 剪切矩形。
位于指定剪切矩形之外的片段将被丢弃。setDepthStencilState:
设置深度和模板测试状态,如深度和模板状态中所述。setStencilReferenceValue:
: 指定模板参考值。setDepthBias:slopeScale:clamp:
: 指定用于比较阴影图和片段着色器输出的深度值的调整。setVisibilityResultMode:offset:
确定是否监视是否有任何样本通过深度和模板测试。
如果设置为MTLVisibilityResultModeBoolean
,则如果有任何样本通过深度和模板测试,则会将非零值写入MTLRenderPassDescriptor
的visibilityResultBuffer
属性指定的缓冲区,如创建渲染过程描述符中所述。
您可以使用此模式执行遮挡测试。
如果您绘制一个边界框并且没有样本通过,那么您可以得出结论,该边界框内的任何对象都被遮挡,因此不需要渲染。setBlendColorRed:green:blue:alpha:
: 指定常量混合颜色和 alpha 值,详情请参阅在渲染管道附件描述符中配置混合。
使用视口和像素坐标系
Metal 将其标准化设备坐标 (NDC) 系统定义为一个 2x2x1 立方体,其中心位于 (0, 0, 0.5)。
NDC 系统的 x 和 y 的左侧和底部分别指定为 -1。
NDC 系统的 x 和 y 的右侧和顶部分别指定为 +1。
视口指定从NDC到窗口坐标的变换。
Metal视口是一个通过MTLRenderCommandEncoder
的 setViewport:
方法指定的3D变换。
窗口坐标的原点在左上角。
在 Metal 中,像素中心偏移 (0.5, 0.5)。
例如,原点处的像素中心位于 (0.5, 0.5);其右侧相邻像素的中心为 (1.5, 0.5)。纹理也是如此。
执行深度和模板操作
深度和模板操作是您指定的片段操作,如下所示:
- 指定
MTLDepthStencilDescriptor
包含深度/模板状态设置的自定义对象。
创建自定义MTLDepthStencilDescriptor
对象 可能需要创建一个或两个适用于正面图元和背面图元的MTLStencilDescriptor
对象。 - 通过调用具有深度/模板状态描述符
MTLDepthStencilState
的newDepthStencilStateWithDescriptor:
方法,来创建一个MTLDepthStencilState
对象。 - 要设置深度/模板状态,请调用
MTLRenderCommandEncoder
的setDepthStencilState:
的方法。 - 如果正在使用模板测试,请调用
setStencilReferenceValue:
以指定模板参考值。
如果启用了深度测试,渲染管道状态必须包含深度附件以支持写入深度值。
要执行模板测试,渲染管道状态必须包含模板附件。
要配置附件,请参阅 创建和配置渲染管道描述符。
如果您要定期更改深度/模板状态,那么您可能需要重用状态描述符对象,根据需要修改其属性值以创建更多状态对象。
注意: 要在着色器函数中从深度格式纹理中进行采样,请在着色器中实现采样操作,而无需使用MTLSamplerState
。
使用MTLDepthStencilDescriptor
对象的属性如下,来设置深度和模板状态:
- 要将深度值写入深度附件,请设置
depthWriteEnabled
为YES
。 depthCompareFunction
指定如何执行深度测试。
如果片段的深度值未通过深度测试,则丢弃该片段。
例如,常用MTLCompareFunctionLess
函数会导致距离观察者比(先前写入的)像素深度值更远的片段值未通过深度测试;也就是说,该片段被视为被较早的深度值遮挡。frontFaceStencil
和backFaceStencil
属性 分别指定 面向正面和面向背面的图元的单独MTLStencilDescriptor
对象。
要对面向正面和面向背面的图元使用相同的模板状态,您可以将相同的状态分配MTLStencilDescriptor
给frontFaceStencil
和backFaceStencil
属性。
要明确禁用一面或两面的模板测试,请将相应属性设置为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
,因此如果屏蔽参考值等于已存储在片段位置的屏蔽模板值,则模板测试通过。stencilFailureOperation
、depthFailureOperation
和depthStencilPassOperation
分别指定针对三种不同的测试结果对模板附件中存储的模板值执行的操作:模板测试失败、模板测试通过但深度测试失败,或模板测试和深度测试均成功。
在上例中,如果模板测试失败,模板值保持不变 (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
中指定的索引列表呈现多个基元实例(instanceCount
)indexCount
确定索引的数量。
索引列表从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所示)描述了渲染三角形的基本过程。
- 创建一个
MTLCommandQueue
并使用它来创建一个MTLCommandBuffer
。 - 创建一个
MTLRenderPassDescriptor
: 指定附件的集合,作为命令缓冲区中编码渲染命令的目的地。
在此示例中,仅设置并使用了第一个颜色附件。
(currentTexture
变量 假设包含用于颜色附件的MTLTexture
。)
然后,MTLRenderPassDescriptor
用于创建新的MTLRenderCommandEncoder
。 - 创建两个
MTLBuffer
对象,posBuf
和colBuf
,并调用newBufferWithBytes:length:options:
将 顶点坐标和顶点颜色数据posData
和colData
复制到缓冲存储中。 - 调用两次
MTLRenderCommandEncoder
的setVertexBuffer:offset:atIndex:
方法,来指定坐标和颜色。
setVertexBuffer:offset:atIndex:
方法的输入值atIndex
与顶点函数源代码中的buffer(atIndex)
属性相对应。 - 在管线描述符中创建
MTLRenderPipelineDescriptor
并建立顶点和片段函数:- 创建一个带有
progSrc
源代码的MTLLibrary
,假定它是一个包含 Metal 着色器源代码的字符串。 - 然后调用
MTLLibrary
的newFunctionWithName:
方法来创建MTLFunction
vertFunc
,代表所调用的hello_vertex
函数,并创建MTLFunction
fragFunc
代表所调用的hello_fragment
函数。 - 最后,使用这些
MTLFunction
对象设置MTLRenderPipelineDescriptor
的vertexFunction
和fragmentFunction
属性。
- 创建一个带有
- 通过调用
newRenderPipelineStateWithDescriptor:error:
或类似的MTLDevice
方法 从MTLRenderPipelineDescriptor
中创建MTLRenderPipelineState
。
然后MTLRenderCommandEncoder
的setRenderPipelineState:
方法 使用创建的管道状态 进行渲染。 - 调用
MTLRenderCommandEncoder
的drawPrimitives:vertexStart:vertexCount:
方法 来附加命令来执行填充三角形(MTLPrimitiveTypeTriangle
类型)的渲染。 - 调用
endEncoding
方法结束本次渲染过程的编码。
并调用MTLCommandBuffer
的commit
方法,在设备上执行命令。
例 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
的着色器函数。
MTLRenderCommandEncoder
的 setVertexBuffer: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
方法。
从同一并行渲染命令编码器创建的所有从属命令编码器都会将命令编码到同一命令缓冲区。
命令按照渲染命令编码器的创建顺序编码到命令缓冲区。
要结束特定渲染命令编码器的编码,请调用MTLRenderCommandEncoder
的endEncoding
方法。
在结束并行渲染命令编码器创建的所有渲染命令编码器的编码后,调用MTLParallelRenderCommandEncoder
的 endEncoding
方法 结束渲染过程。
例 5-16显示了MTLParallelRenderCommandEncoder
创建三个MTLRenderCommandEncoder
对象:rCE1
、rCE2
和rCE3
。
例 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
的顺序无关。
对于MTLParallelRenderCommandEncoder
,MTLCommandBuffer
始终按照下级渲染命令编码器的创建顺序包含命令,如图5-6 所示。
图 5-6 并行渲染过程中渲染命令编码器的排序
七、数据并行计算处理:计算命令编码器
本章介绍如何创建和使用MTLComputeCommandEncoder
对象来编码数据并行计算处理状态和命令并提交它们以在设备上执行。
要执行数据并行计算,请遵循以下主要步骤:
- 使用
MTLDevice
方法创建包含对象编译代码的计算状态 (MTLComputePipelineState
)MTLFunction
,如创建计算状态中所述。
MTLFunction
对象表示使用 Metal 着色语言编写的计算函数,如函数和库中所述。 - 指定计算命令编码器要使用的
MTLComputePipelineState
对象,如指定计算命令编码器的计算状态和资源中所述。 - 指定资源和相关对象(
MTLBuffer
、MTLTexture
和可能的MTLSamplerState
),它们可能包含要由计算状态处理和返回的数据,如指定计算命令编码器的计算状态和资源中所述。
还设置它们的参数表索引,以便 Metal 框架代码可以在着色器代码中找到相应的资源。
在任何给定时刻,MTLComputeCommandEncoder
都可以与多个资源对象相关联。 - 按照执行计算命令 中的说明,分派计算函数指定的次数。
1、创建计算管道状态
MTLFunction
对象表示 可由MTLComputePipelineState
对象执行的数据并行代码。
MTLComputeCommandEncoder
对象对设置参数和执行计算函数的命令进行编码。
由于创建计算管道状态可能需要编译昂贵的 Metal 着色语言代码,因此您可以使用阻塞或异步方法以最适合您应用设计的方式安排此类工作。
- 要同步创建计算管道状态对象,请调用
MTLDevice
的newComputePipelineStateWithFunction:error:
或newComputePipelineStateWithFunction:options:reflection:error:
方法。
当 Metal 编译着色器代码以创建管道状态对象时,这些方法会阻止当前线程。 - 要异步创建计算管道状态对象,请调用
MTLDevice
的newComputePipelineStateWithFunction: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、执行计算命令
要对命令进行编码以执行计算函数,请调用 MTLComputeCommandEncoder
的dispatchThreadgroups:threadsPerThreadgroup:
方法 并指定线程组维度和线程组数量。
您可以查询 MTLComputePipelineState
的threadExecutionWidth
和maxTotalThreadsPerThreadgroup
属性以优化此设备上计算函数的执行。
线程组中的线程总数是以下组件的乘积threadsPerThreadgroup
:threadsPerThreadgroup.width * threadsPerThreadgroup.height * threadsPerThreadgroup.depth
。
maxTotalThreadsPerThreadgroup
属性指定单个线程组中可在设备上执行此计算功能的最大线程数。
计算命令按照它们被编码到命令缓冲区的顺序执行。
当与命令相关的所有线程组都完成执行并且所有结果都写入内存时,计算命令就完成执行。
由于这种排序,命令缓冲区中在它之后编码的任何命令都可以使用计算命令的结果。
要结束计算命令编码器的编码命令,请调用MTLComputeCommandEncoder
的 endEncoding
方法。
结束前一个命令编码器后,您可以创建任何类型的新命令编码器,以将其他命令编码到命令缓冲区中。
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)。
然后,函数对象用于创建名为filterState
的MTLComputePipelineState
对象。
计算函数对图像inputImage
执行图像转换和过滤操作,结果返回outputImage
。
首先,setTexture:atIndex:
和setBuffer:offset:atIndex:
方法将纹理和缓冲区对象分配给指定参数表中的索引paramsBuffer
指定用于执行图像转换的值,inputTableData
指定过滤器权重。计算函数作为二维线程组执行,每个维度的大小为16 x 16像素。dispatchThreadgroups:threadsPerThreadgroup:
方法将命令排入队列以调度执行计算函数的线程,endEncoding
方法终止MTLComputeCommandEncoder
。
最后,MTLCommandBuffer
的commit
方法会使命令尽快执行。
例 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_transform
和filter_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
提供在资源(缓冲区和纹理)之间复制数据的方法。
数据复制操作对于图像处理和纹理效果(例如模糊或反射)可能是必要的。
它们可用于访问屏幕外渲染的图像数据。
要执行数据复制操作,首先通过调用MTLCommandBuffer
的blitCommandEncoder
方法创建一个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
从基础级别纹理图像开始,MTLBlitCommandEncoder
的 generateMipmapsForTexture:
方法 为给定纹理自动生成 mipmap 。
generateMipmapsForTexture:
为所有 mipmap 级别创建缩放图像,直至最高级别。
有关如何确定 mipmap 数量以及每个 mipmap 大小的详细信息,请参阅 切片。
3、填充缓冲区的内容
MTLBlitCommandEncoder
的 fillBuffer:range:value:
方法,将8位常数值存储在 给定缓冲器的 指定范围内的 每个字节中。
4、结束 Blit 命令编码器的编码
要结束 blit 命令编码器的编码命令,请调用endEncoding
。
结束前一个命令编码器后,您可以创建任何类型的新命令编码器,以将其他命令编码到命令缓冲区中。
九、Metal 工具
本章列出了可帮助您定制和改进开发工作流程的工具。
1、在应用程序构建过程中创建库
在应用构建过程中编译着色器语言源文件并构建库(.metallib
文件)比在运行时编译着色器源代码可实现更好的应用性能。
您可以在 Xcode 中或使用命令行实用程序构建库。
使用 Xcode 构建库
您的项目中的任何着色器源文件都会自动用于生成默认库,您可以使用 MTLDevice
库的newDefaultLibrary
方法 从 Metal 框架代码访问。
使用命令行实用程序构建库
图 8-1显示了构成 Metal 着色器源代码的编译器工具链的命令行实用程序。
当您在项目中包含.metal
文件时,Xcode 会调用这些工具来构建一个库文件,您可以在运行时在应用程序中访问该文件。
要在不使用 Xcode 的情况下将着色器源代码编译成库:
- 使用
metal
工具将每个.metal
文件 编译为单个.air
文件,该文件存储着色器语言代码的中间表示 (IR)。 - 或者,使用
metal-ar
工具将多个.air
文件存档为一个.metalar
文件。(metal-ar
类似于 Unixar
。) - 使用
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 中执行帧捕获,请启用调试并调用 MTLCommandQueue
的 insertDebugCaptureBoundary
方法来通知 Xcode。
MTLCommandBuffer
的presentDrawable:
和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
。
对于缓冲区,通过在MTLDevice
的newBufferWithLength:options:
、 newBufferWithBytes:length:options:
或newBufferWithBytesNoCopy:length:options:deallocator:
方法中的任意一个中 传递相应的MTLResourceOptions
值来设置所需的存储模式。
缓冲区的默认存储模式为MTLStorageModeShared
,但OSX_GPUFamily_v1
功能集应用程序可以通过使用托管或专用存储模式显式管理其缓冲区,从而提高性能。
可以使用MTLResource
对象的storageMode
属性 来查询资源的存储模式(纹理或缓冲区)。
4、纹理
硬件对不同纹理类型和像素格式的支持是功能集之间的主要功能差异。
本节列出了框架的主要纹理添加;有关更详细的讨论,请参阅 MTLPixelFormat
参考和 Metal 功能集表 章节中的代码示例和比较表。
压缩纹理
iOS_GPUFamily2_v1
、iOS_GPUFamily2_v2
和iOS_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:
方法已经支持的像素格式参数之外)。
使用这些纹理视图方法 创建的纹理 现在可以使用parentTexture
、parentRelativeLevel
和 parentRelativeSlice
属性 查询其父纹理的属性(除了查询pixelFormat
和textureType
属性已经支持的其他属性之外)。
有关纹理视图创建限制的详细信息,例如有效的转换目标,请参阅 MTLTexture 协议参考。
注意: 与查询父纹理属性类似,使用newTextureWithDescriptor:offset:bytesPerRow:
方法从缓冲区创建的新纹理现在可以使用buffer
、bufferOffset
和 bufferBytesPerRow
属性查询其源缓冲区属性。
IOSurface 支持
OSX_GPUFamily1_v1
功能集 增加了对 IOSurfaces 的支持。
使用newTextureWithDescriptor:iosurface:plane:
方法 可以从现有的 IOSurface 创建新的纹理。
5、渲染添加
渲染命令编码器
MTLRenderCommandEncoder
类新增了几个图形 API 。
主要新功能总结如下:
setStencilFrontReferenceValue:backReferenceValue:
方法 允许正面和背面图元使用不同的模板测试参考值。MTLDepthClipMode
枚举 和setDepthClipMode:
方法支持深度剪辑。OSX_GPUFamily1_v1
和iOS_GPUFamily3_v1
特征集中支持计数遮挡查询,并且可以将MTLVisibilityResultModeCounting
新值传递到setVisibilityResultMode:offset:
方法中。- 通过在同纹理写入和读取操作之间调用
textureBarrier
方法,OSX_GPUFamily1_v1
功能集中支持纹理障碍。 OSX_GPUFamily1_v1
和iOS_GPUFamily3_v1
功能集使用drawPrimitives:vertexStart:vertexCount:instanceCount:baseInstance:
和drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:baseVertex:baseInstance:
方法支持基本顶点和基本实例值。
同样,通过提供新的[[ base_vertex ]]
和[[ base_instance ]]
顶点着色器输入,Metal 着色语言也添加了对这些绘图输入的支持。OSX_GPUFamily1_v1
和iOS_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
功能集通过MTLRenderPassDescriptor
和 MTLRenderPipelineDescriptor
类中的新 API 增加了对分层渲染的支持。
分层渲染使顶点着色器能够将每个图元渲染到纹理数组、立方体纹理或 3D 纹理的一层,由其第一个顶点的顶点着色器指定。
对于 2D 纹理数组或立方体纹理,每个切片都是一层;对于 3D 纹理,每个像素深度平面都是一层。
加载和存储操作适用于渲染目标的每一层。
要启用分层渲染,您必须适当地配置渲染过程和渲染管道描述符:
- 设置
renderTargetArrayLength
属性的值 以指定所有渲染目标中可用的最小层数。
例如,如果您有一个 2D 纹理数组和一个立方体纹理,每个纹理至少有 6 个层,则将此值设置为6
。 - 设置
inputPrimitiveTopology
属性的值 以指定要渲染的图元类型。
例如,将此值设置为MTLPrimitiveTopologyClassTriangle
,基于立方体的阴影映射。
包含所有图元类型值的完整枚举声明列于例 10-10 - 此外,
sampleCount
的值必须为 1(这是默认值)。
例 10-10 原始拓扑值
typedef NS_ENUM(NSUInteger, MTLPrimitiveTopologyClass)
{
MTLPrimitiveTopologyClassUnspecified = 0,
MTLPrimitiveTopologyClassPoint = 1,
MTLPrimitiveTopologyClassLine = 2,
MTLPrimitiveTopologyClassTriangle = 3
};
6、计算附加项
现有类和新类中还添加了一些计算 API,总结如下:
OSX_GPUFamily1_v1
和iOS_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_v1
、iOS_GPUFamily2_v2
和iOS_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_v3
,iOS_GPUFamily2_v3
,iOS_GPUFamily3_v2
,tvOS_GPUFamily1_v2
无内存渲染目标是仅暂时存在于 GPU 上的图块内存中的渲染目标,无需任何其他 CPU 或 GPU 内存支持。
无内存渲染目标可满足高分辨率显示器和 MSAA 数据增加的内存需求,让您能够:
- 防止浪费仅用于临时渲染目标(颜色、深度和模板)的内存。
- 在相同的内存预算中使用更高的 MSAA 级别来提高图像质量。
要创建无记忆渲染目标,请将MTLTextureDescriptor
对象的 storageMode
属性设置为MTLStorageModeMemoryless
。
然后,使用此描述符创建一个MTLTexture
对象。
只有MTLTexture
对象 才能使用存储模式创建MTLStorageModeMemoryless
。
无记忆渲染目标只能由临时渲染目标使用;为此,请将其设置为MTLRenderPassAttachmentDescriptor
对象的 texture
的属性。
用例
以下用例只是可用作无记忆渲染目标的临时渲染目标的几个示例。
对于每个渲染目标,渲染操作完成后永远不会使用渲染目标的内容:
- 传统的深度测试,使用带有
MTLStoreActionDontCare
存储动作的深度渲染目标。 - 传统的模板缓冲区操作,使用带有
MTLStoreActionDontCare
存储操作的模板渲染目标。 - MSAA 渲染,使用具有
MTLStoreActionMultisampleResolve
存储操作的 MSAA 颜色渲染目标。 - 延迟渲染,在单个渲染命令编码器中使用两个逻辑渲染过程:
- 第一遍使用反照率、法线和其他数据填充临时 G 缓冲区渲染目标。
- 第二遍读取临时 G 缓冲区渲染目标,然后计算并累积照明数据以将最终颜色输出到持久渲染目标。
每个临时渲染目标都是无记忆渲染目标,并具有MTLStoreActionDontCare
存储动作。
- 延迟照明,在单个渲染命令编码器中使用三个逻辑渲染过程:
- 第一遍用几何数据填充临时深度和法线渲染目标。
- 第二遍读取先前的临时渲染目标,然后计算并累积照明数据以填充临时的漫反射和镜面渲染目标。
- 第三遍读取所有先前的临时渲染目标,然后计算照明方程以将最终颜色输出到持久渲染目标。
每个临时渲染目标都是无记忆渲染目标,并具有MTLStoreActionDontCare
存储动作。
规则和限制
无记忆渲染目标必须遵守以下规则和限制。无记忆渲染目标…
- 可以是堆的一部分,但不能使用别名。
有关更多信息,请参阅 资源堆章节。 - 必须具有可渲染的颜色、深度或模板像素格式。
- 仅能通过渲染过程填充。
- 必须具有
MTLTextureType2D
或MTLTextureType2DMultisample
纹理类型。 - 只能用于
MTLRenderPassAttachmentDescriptor
对象的texture
属性。 - 不能用于
MTLRenderPassAttachmentDescriptor
对象的resolveTexture
属性。 - 必须由具有
MTLLoadActionDontCare
或MTLLoadActionClear
加载动作的MTLRenderPassAttachmentDescriptor
对象使用。 - 必须由具有
MTLStoreActionDontCare
或MTLStoreActionMultisampleResolve
存储操作的MTLRenderPassAttachmentDescriptor
对象使用。
MTLRenderPassAttachmentDescriptor
对象 也可以具有初始MTLStoreActionUnknown
存储操作,但必须在其关联的渲染命令编码器结束编码之前更改此操作。
有关更多信息,请参阅 延迟存储操作部分。 - 无法通过
MTLTexture
协议中的任何方法 读取或写入。 - 不能用作父纹理来创建纹理视图。
- 无法被
MTLBlitCommandEncoder
对象使用。不允许进行任何 blit 操作。 - 无法被
MTLComputeCommandEncoder
对象使用。不允许执行任何计算操作。 - 可以使用帧缓冲区提取由片段函数读取。
有关更多信息,请参阅 Metal 着色语言指南 的可编程混合部分。
使用无记忆渲染目标的应用应谨慎控制在单个渲染过程中传递以进行处理的数据总量。
无记忆渲染目标使用 GPU 上的图块内存作为其临时存储;只要可以缓存与为渲染过程发出的绘制调用相关的所有数据,无记忆渲染目标就会一次处理一个图块。
为确保成功使用无记忆渲染目标,请务必遵循以下附加规则:
- 渲染过程中引用的所有资源消耗的物理内存都不应超过可用内存量。
- 您不应提交超过 64K 个唯一视口、剪刀和深度偏差值。
命令缓冲区将通过MTLCommandBufferErrorMemoryless
错误代码报告任何内存错误。
5、函数专业化
可用:iOS_GPUFamily1_v3
,iOS_GPUFamily2_v3
,iOS_GPUFamily3_v2
,tvOS_GPUFamily1_v2
,OSX_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::write
或access::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_ms
、depth2d
、depth2d_array
、depthcube
、depthcube_array
或depth2d_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
是片段覆盖的样本数。
但是,如果片段函数使用带有color
、sample_id
、sample_perspective
或sample_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_v3
,iOS_GPUFamily2_v3
,iOS_GPUFamily3_v2
,tvOS_GPUFamily1_v2
,OSX_GPUFamily1_v2
MTLPixelFormatX32_Stencil8
和MTLPixelFormatX24_Stencil8
是新的模板像素格式,允许您使用模板纹理视图 轻松访问图形 或计算函数中的 模板纹理数据。
模板纹理视图 允许您从组合的深度和模板纹理 创建仅模板纹理。
然后可以将这两个纹理设置为纹理参数,并在图形或计算函数中单独采样。
仅模板纹理为每个像素返回 8 位无符号整数值。
深度和模板父纹理的格式决定了模板纹理视图的格式,如表 11-1所示。
深度和模板父纹理格式 | 模板纹理视图格式 |
---|---|
MTLPixelFormatDepth32Float_Stencil8 | MTLPixelFormatX32_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_XR | 64 | 10 | 线性 (Linear) | [-0.752941,1.25098]* |
MTLPixelFormatBGRA10_XR_sRGB | 64 | 10 | 伽玛 | [-0.527100, 1.66894]*(伽马扩展之前) |
MTLPixelFormatBGR10_XR | 三十二 | 10 | 线性 (Linear) | [-0.752941,1.25098] |
MTLPixelFormatBGR10_XR_sRGB | 三十二 | 10 | 伽玛 | [-0.527100,1.66894](伽马扩展之前) |
MTLPixelFormatBGRA10_XR
和MTLPixelFormatBGRA10_XR_sRGB
像素格式的 alpha 分量在采样、渲染和写入时始终被限制在[0.0, 1.0]
范围内(尽管支持超出此范围的值)。
所有扩展范围格式都是可颜色渲染的,可以在CAMetalLayer
对象的pixelFormat
属性或MTKView
对象的colorPixelFormat
属性中设置。
只有具有宽色域显示器的设备才会显示[0.0,1.0]
范围之外的值;所有其它设备将把值箝位到[0.0,1.0]
范围。
注意: 32bpp MTLPixelFormatBGR10_XR
和 MTLPixelFormatBGR10_XR_sRGB
扩展范围像素格式具有与 32bpp MTLPixelFormatRGBA8Unorm
和 MTLPixelFormatRGBA8Unorm_sRGB
普通像素格式相同的速度和内存特性。
64bppMTLPixelFormatBGRA10_XR
和MTLPixelFormatBGRA10_XR_sRGB
扩展范围像素格式需要更多内存带宽,性能也不佳。
虽然它们可用于源纹理或中间渲染目标,但建议仅将这些格式用于CAMetalDrawable
对象的纹理。
如果要表示宽色域值并需要 alpha 分量,则应将 32bppMTLPixelFormatBGR10_XR
或MTLPixelFormatBGR10_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_v3
,iOS_GPUFamily2_v3
,iOS_GPUFamily3_v2
,tvOS_GPUFamily1_v2
,OSX_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
双源混合允许片段函数,将两种源颜色Source0
和Source1
输出到 GPU 的混合单元用于单个渲染目标。
产生第二种源颜色
为了产生两种输出颜色Source0
和Source1
,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;
}
注意: 编译时必须知道m
和i
的值。
如果没有指定index(i)
,则假定 索引为0
。
引用第二个源颜色
第二种输出颜色,Source1
在下列固定函数混合方程中被引用为源或目标混合因子:
Output.rgb = (Source0.rgb * SBF) {BO} (Destination.rgb * DBF)
Output.a = (Source0.a * SBF) {BO} (Destination.a * DBF)
其中BO
是MTLBlendOperation
运算符,SBF
是源混合因子,DBF
是目标混合因子,Source1
可以作为以下MTLBlendFactor
枚举之一引用:
Table 11-3 Source1 blend factors
MTLBlendFactor | RGB 混合因子值 | Alpha 混合因子值 |
---|---|---|
Source1Color | R、G、B 来自Source1 | 来自Source1 |
OneMinusSource1Color | 1-R,1-G,1-B 来自Source1 | 1-A 来自Source1 |
Source1Alpha | A、A、A 来自Source1 | 来自Source1 |
OneMinusSource1Alpha | 1-A,1-A,1-A 来自Source1 | 1-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_v3
,iOS_GPUFamily2_v3
,iOS_GPUFamily3_v2
,tvOS_GPUFamily1_v2
,OSX_GPUFamily1_v1
,OSX_GPUFamily1_v2
新的 iOS 和 tvOS 功能集现在支持从纹理到缓冲区或从缓冲区到纹理的 MSAA 位块传输。
位块传输目标必须具有足够的大小才能存储 MSAA 数据。
(MSAA blits 已在OSX_GPUFamily1_v1
功能集中得到支持,并且将继续在OSX_GPUFamily1_v2
功能集中得到支持。)
15、sRGB 写入
可用:iOS_GPUFamily2_v3
,iOS_GPUFamily3_v1
,iOS_GPUFamily3_v2
,tvOS_GPUFamily1_v2
现在,其他 iOS 和 tvOS 功能集支持写入 sRGB 纹理。
(iOS_GPUFamily3_v1
功能集已经支持写入 sRGB 纹理。)
16、附加着色语言功能
可用: iOS_GPUFamily1_v3
(除非另有说明),iOS_GPUFamily2_v3
,iOS_GPUFamily3_v2
,tvOS_GPUFamily1_v2
,OSX_GPUFamily1_v2
本节总结了 Metal 着色语言 1.2 版中引入的附加功能。
有关更多信息,请参阅Metal 着色语言指南。
整数函数
新的整数函数用于提取、插入和反转位,如整数函数中所述。
纹理函数
现在可以将纹理读写函数与 16 位无符号整数坐标(ushort
类型)一起使用,如纹理函数中所述。
计算函数
可用:iOS_GPUFamily1_v3
,iOS_GPUFamily2_v3
,iOS_GPUFamily3_v2
,tvOS_GPUFamily1_v2
针对 SIMD 组线程的新同步函数,如线程组同步函数中所述。
采样器限定符
可用:iOS_GPUFamily1_v3
,iOS_GPUFamily2_v3
,iOS_GPUFamily3_v2
,tvOS_GPUFamily1_v2
新的采样器限定符用于指定最大各向异性和 LOD 钳位范围,如采样器中所述。
缓冲区、纹理和采样器的结构
现在可以通过值将资源结构作为参数传递给图形或计算函数,如函数参数和变量中所述。
便利常量
float
和half
类型 新的便捷常量,如数学函数中所述。
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.0
到1.0
。
曲面细分器配置为渲染管道的一部分,使用MTLRenderPipelineDescriptor
对象 构建MTLRenderPipelineState
对象。
曲面细分器的输入是每个面片的曲面细分因子。
曲面细分器基元生成
曲面细分器对每个补丁运行一次,使用输入补丁并生成一组新的三角形。
这些三角形是通过根据提供的每个补丁曲面细分因子细分补丁而生成的。
曲面细分器生成的每个三角形顶点在归一化参数空间中都有一个关联的 (u, v) 或 (u, v, w) 位置,每个参数值的范围从0.0
到1.0
。(请注意,细分是以依赖于实现的方式执行的。)
细分后顶点函数
后镶嵌顶点函数是一个顶点函数,用于计算镶嵌器生成的每个面片表面样本的顶点数据。
后镶嵌顶点函数的输入包括:
- 面片上的规范化顶点坐标(由曲面细分器输出)。
- 每个补丁的用户数据(可选择由计算内核输出)。
- 补丁控制点数据(可选择由计算内核输出)。
- 任何其他顶点函数输入,例如纹理和缓冲区。
后镶嵌顶点函数为镶嵌三角形生成最终顶点数据。
后镶嵌顶点函数执行完毕后,镶嵌图元将被光栅化,渲染管道的其余阶段将照常执行。
2、每个面片的曲面细分因子
每个面片细分因子指定细分器细分每个面片的程度。每个面片的镶嵌因子由四边形面片的 MTLQuadTessectionFactorsHalf
结构或三角形面片的 MILTriangleTestectionFactors Half
构造描述。
注意: 尽管结构成员属于uint16_t
类型,但是输入到曲面细分器的每个面片曲面细分因子必须属于half
类型。
理解四边形补丁
对于四边形面片,面片中的位置是 (u, v) 笛卡尔坐标,表示顶点相对于四边形面片边界的水平和垂直位置,如图12-2所示。
(u, v) 值的范围从0.0
到1.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.0
到1.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
直接用作获取每个面片曲面细分因子的索引。
如果patchIndexBuffer
为NULL
,则drawPatchIndex
和patchIndex
为相同值,如图12-5所示。
图 12-5 获取每个补丁数据和补丁控制点数据(如果patchIndexBuffer
是NULL
)
如果控制点在多个补丁之间共享,或者补丁控制点数据不连续,请使用drawIndexedPatches
方法。
patchIndex
引用指定的controlPointIndexBuffer
,其中包含补丁的控制点索引,如图12-6所示。
(tessellationControlPointIndexType
描述中的控制点索引的大小,并且必须是MTLTessellationControlPointIndexTypeUInt16
或MTLTessellationControlPointIndexTypeUInt32
。)
图 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_v3
,iOS_GPUFamily2_v3
,iOS_GPUFamily3_v2
,tvOS_GPUFamily1_v2
资源堆允许 Metal 资源由相同的内存分配支持。
这些资源是从称为*堆(heap)的内存池创建的,并由捕获和管理 GPU 工作依赖关系的**隔离来跟踪。
资源堆可帮助您的应用降低以下成本:
- 资源创建。
资源创建可能涉及分配新内存、将其映射到进程中以及将其填零。
通过从更大的堆或由堆支持的回收资源内存创建资源,可以降低此成本。 - 固定内存预算。
如果某些资源在一段时间内未使用,虚拟内存可能会压缩资源内存以节省空间。
这可能会导致花费额外的时间来使该资源内存再次可供下次使用。
通过使用少量堆,您可以将分配保持在给定的内存预算内,并确保这些资源持续使用(这有助于提供更一致的性能)。 - 瞬态资源。
瞬态资源会为每一帧生成和使用,但并非所有这些资源都会同时使用。
为了减少内存消耗,未一起使用的瞬态资源可以共享由堆支持的同一内存。
1、堆
MTLHeap
对象是一种 Metal 资源,代表抽象内存池。
从此堆创建的资源被定义为可别名或不可别名。
当子分配资源与另一个别名资源共享同一部分堆内存时,它们就是别名资源。
创建堆
通过调用MTLDevice
对象的newHeapWithDescriptor:
方法可以创建MTLHeap
对象。
MTLHeapDescriptor
对象描述了堆的存储模式、CPU 缓存模式和字节大小。
从同一个堆中子分配的所有资源共享相同的存储模式和 CPU 缓存模式。
堆的字节大小必须始终足够大,以便为其资源分配足够的内存。
通过调用setPurgeableState:
方法,可以在创建堆后将其设置为可清除。
堆可清除状态指的是其整个后备内存,并影响堆内的所有资源。
堆是可清除的,但其资源不是;子分配的资源仅反映堆的可清除状态。
可清除性对于仅存储渲染目标的堆可能很有用。
二次分配堆资源
MTLBuffer
和MTLTexture
对象 都可以从堆中进行子分配。
为此,请调用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 和计算命令编码器跟踪栅栏
MTLBlitCommandEncoder
和MTLComputeCommandEncoder
对象都 可以通过栅栏进行跟踪。
要更新栅栏,请分别调用每个命令编码器的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(六)