Metal编程指南
  • [Metal基础概念] 简明的描述Metal的主要特性。
  • [命令组成和执行模型] 解释如何创建和提交命令到GPU执行。
  • [资源对象:缓冲和纹理] 讨论设备内存管理,包括表现GPU内存分配的缓冲和纹理对象。
  • [函数和库] 描述Metal着色语言代码如何被呈现在一个Metal应用中,Metal着色语言代码在GPU上如何被加载和执行的。
  • [图像渲染:渲染命令编码器] 描述如何渲染3D图像,包括如何穿过多个线程来分配图形操作。
  • [数据并行计算处理:计算命令编码器] 阐述如何执行数据并行处理。
  • [缓冲和纹理操作:位命令编码器] 描述如何在纹理和缓冲之间拷贝数据。

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工作量进入一个命令缓冲区的接口。

在任何时候,只有一个命令编码器可以是活跃的,追加命令到一个命令缓冲区。每个命令编码器在创建用于使用相同命令缓冲区的另一个命令编码器之前必须结束。一个例外“每个命令缓冲区的一个活跃的命令编码器”的规则是MTLParallelRenderCommandEncoder协议,在使用使用多线程编码单一渲染通道中讨论。

一旦所有的编码结束,提交MTLCommandBuffer对象本身,它标记那个命令缓冲区准备由GPU执行。MTLCommandQueue协议控制提交到MTLCommandBuffer对象的命令何时被执行,相对于其他MTLCommandBuffer对象已经在命令队列。

图表2-1展示了命令队列,命令缓冲,命令编码器对象是如何紧密联系的。表格顶部的每列组件(缓冲,纹理,采样器,深度以及模板状态,管线状态)表示资源和状态,这些状态一个特殊命令编码器的特性。

图表 2-1 Metal对象关系

img-w600

设备对象表示一个GPU

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

Metal中的瞬态与非瞬态对象

Metal中的一些对象被设计成瞬态和极其轻量级的,而另一些可能对应用程序的生命周期则更昂贵并会持续很长时间。

命令缓冲区和命令编码器对象是瞬态的并用途单一。他们是非常便宜的分配和销毁,所以他们的创建方法返回自动释放对象。

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

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

命令队列

命令队列接受一个GPU将要执行的命令缓冲区的有序列表。所有命令缓冲区发送到一个队列,这保证了入队的命令缓冲区有序执行。通常,命令队列是线程安全的并且允许多个活跃的命令缓冲区被同时编码。

创建命令队列,调用MTLDevice对象的newCommandQueue方法或newCommandQueueWithMaxCommandBufferCount:方法。通常,命令队列希望长时间存活,所以他们不应当被反复创建和销毁。

命令缓冲区

命令缓冲区存储被编码的命令直到缓冲区被提交到GPU执行。一个命令缓冲可以包含许多不同种类的被编码的命令,依赖于编码器的数量和类型,这些编码器被用来构建它。在一个典型的应用程序中,渲染的整个帧被编码进一个单独的命令缓冲区,即使渲染的帧包含多个帧渲染通道,计算处理功能,或者位块传送操作。

命令缓冲区是瞬态的一次性对象并且不支持重用。一旦一个命令缓冲区被提交执行,唯一有效的操作是去等待命令缓冲区被安排或者执行完成—-通过同步调用或者在注册命令缓冲执行处理程序中讨论的处理程序—-检查命令缓冲区执行状态。

命令缓冲区也表示了唯一独立地可跟踪的工作单元,并且他们定义了由Metal内存模型确定的一致性界限,详情见资源对象:缓冲和纹理

创建命令缓冲区

创建MTLCommandBuffer对象,调用MTLCommandQueuecommandBuffer方法。MTLCommandBuffer对象只能被提交到创建的MTLCommandQueue对象中。

commandBuffer方法创建的命令缓冲区持有需要执行的数据。对于特定场景,在MTLCommandBuffer对象执行期间在别处持有保留这些对象,你可以通过调用MTLCommandQueuecommandBufferWithUnretainedReferences方法创建一个命令缓冲区。使用commandBufferWithUnretainedReferences方法仅仅为了性能极其关键的应用程序,这可以确保关键对象在应用的别处有引用直到命令缓冲执行完成。否则,不再有其他引用的一个对象可能过早地被施放,命令缓冲执行结果是不明确的。

执行命令

MTLCommandBuffer协议使用下列方法去确定命令缓冲在命令队列中的执行顺序。命令缓冲并未开始执行直到它被提交。一旦提交,命令缓冲会按照它们入队的顺序去执行。

  • enqueue方法在命令队列缓冲区上保留命令缓冲的一个地方,但不提交到命令缓冲区去执行。当这个命令缓冲区最后被提交,它将在任何以前入队相关队列的命令缓冲区后被执行。
  • commit方法引起了命令缓冲区被尽快执行,但是在任何以前入队相同命令队列的命令缓冲区被提交之后。如果命令缓冲没有以前入队的,commit做一个隐含的enqueue调用。

多线程使用enqueue的例子,参见多线程,命令缓冲区和命令编码器

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

下面列出的MTLCommandBuffer方法监控命令执行。计划和完成处理程序在一个不明确的线程上按执行顺序被调用。执行这些处理程序代码应很快完成;如果昂贵的或阻塞的工作需要被完成,推迟那个工作到另一个线程。

  • addScheduledHandler:方法注册一个当命令缓冲调度时被调用的代码块。在由其他的MTLCommandBuffer对象递交的工作或者系统其他API的任何依赖被满足时命令缓冲被考虑调度。你可以为一个命令缓冲注册多个调度处理程序。
  • waitUntilScheduled方法同步等待并且在命令缓冲被调度后返回,所有由addScheduledHandler:方法注册的调度程序被完成。
  • addCompletedHandler:方法注册一个在设备完成命令缓冲执行后立刻被调用的代码块。你可以为一个命令缓冲注册多个完成处理代码块。
  • waitUntilCompleted方法同步等待,在设备已经完成命令缓冲执行后返回,所有通过addCompletedHandler:方法注册的处理程序返回。
[presentDrawable:](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443029-presentdrawable)方法是一个完成处理程序的特例。这个方便的方法当命令缓冲被调度时显示一个可显示资源(一个[CAMetalDrawable](https://developer.apple.com/documentation/quartzcore/cametaldrawable)对象)的内容。关于[presentDrawable:](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443029-presentdrawable)方法的详情,参见[集成核心动画:CAMetalLayer]()。 #### 监控命令缓冲区执行状态 只读[status](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443048-status)属性包含一个列举在[Command Buffer Status Codes](https://developer.apple.com/documentation/metal/mtlcommandbufferstatus)中的[MTLCommandBufferStatus](https://developer.apple.com/documentation/metal/mtlcommandbufferstatus)的枚举值,这些反应了在这个命令缓冲声明周期中的当前调度阶段。 如果执行成功完成,只读[error](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443040-error)属性值是nil。如果执行失败,那么status被设置为MTLCommandBufferStatusErrorerror属性可能包含一个被列举在[Command Buffer Error Codes](https://developer.apple.com/documentation/metal/mtlcommandbuffererror.code)中的值,它表明了失败的原因。 ### 命令编码器 命令编码器是一个瞬态对象,你用一次写命令和状态到一个以一种GPU可执行格式的命令缓冲区。许多命令编码器对象方法追加命令到命令缓冲区。当一个缓冲编码器是活跃的,它有在其命令缓冲区追加命令的专属权。一旦你完成了编码命令,调用[endEncoding](https://developer.apple.com/documentation/metal/mtlcommandencoder/1458038-endencoding)方法。继续写命令,创建一个新的命令编码器。 #### 创建一个命令编码器对象 由于一个命令编码器追加命令到一个指定的命令缓冲区,通过从你想使用的[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)对象请求创建一个命令编码器。使用下面的[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)方法来创建各类型的命令编码器: * [renderCommandEncoderWithDescriptor:](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1442999-rendercommandencoderwithdescript)方法创建一个[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)]对象图像渲染在[MTLRenderPassDescriptor](https://developer.apple.com/documentation/metal/mtlrenderpassdescriptor)中的一个附件。 * computeCommandEncoder方法创建一个[MTLComputeCommandEncoder](https://developer.apple.com/documentation/metal/mtlcomputecommandencoder)对象对于数据并行计算。 * blitCommandEncoder方法创建一个[MTLBlitCommandEncoder](https://developer.apple.com/documentation/metal/mtlblitcommandencoder)对象对于内存操作。 * parallelRenderCommandEncoderWithDescriptor:方法创建一个[MTLParallelRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlparallelrendercommandencoder)对象,它使得一些[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)对象运行在不同的线程上,当静止渲染一个附件时,它被指定到一个共享的MTLRenderPassDescriptor中。 #### 渲染命令编码器 图像渲染可以被描述依据一个渲染管道。一个[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)对象表示渲染状态和关联单一渲染通道的绘制指令。[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)需要一个相关联的[MTLRenderPassDescriptor](https://developer.apple.com/documentation/metal/mtlrenderpassdescriptor)(描述在创建一个渲染管道描述符),它包含颜色,深度和模板附件,它们作为渲染命令的终点。[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)有下列方法: * 指定图像资源,例如缓冲区和纹理对象,包含顶点,片元,或者纹理图像数据 * 指定[MTLRenderPipelineState](https://developer.apple.com/documentation/metal/mtlrenderpipelinestate)对象,包含已编译渲染状态,包括顶点和片元着色器 * 指定固定功能状态,包括视口,三角形填充模式,剪刀矩形,深度和模板测试以及其他值 * 绘制3D基元 关于[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)协议的详细信息,请看[图像渲染:渲染命令编码器]()。 #### 计算命令编码器 对于数据并行计算,[MTLComputeCommandEncoder](https://developer.apple.com/documentation/metal/mtlcomputecommandencoder)协议提供了在命令缓冲区编码命令的方法,可以指定计算功能和它的参数(例如,纹理,缓冲,和采样器状态),调度执行计算功能。创建一个计算命令编码器对象,使用[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)的[computeCommandEncoder](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443044-makecomputecommandencoder)方法。关于[MTLComputeCommandEncoder](https://developer.apple.com/documentation/metal/mtlcomputecommandencoder)的方法和属性的详细信息,请看[数据并行计算处理:计算命令编码器]()。 #### 位块传送命令编码器 [MTLBlitCommandEncoder](https://developer.apple.com/documentation/metal/mtlblitcommandencoder)协议有方法在缓冲([MTLBuffer](https://developer.apple.com/documentation/metal/mtlbuffer))和纹理([MTLTexture](https://developer.apple.com/documentation/metal/mtltexture))之间为内存拷贝操作追加命令。[MTLBlitCommandEncoder](https://developer.apple.com/documentation/metal/mtlblitcommandencoder)协议也提供了方法用纯色去填充纹理和产生纹理映射。创建一个位块传送命令编码器对象,使用[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)的[blitCommandEncoder](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443001-makeblitcommandencoder)方法。关于[MTLBlitCommandEncoder](https://developer.apple.com/documentation/metal/mtlblitcommandencoder)方法和属性的详细信息,请看[缓冲区和纹理操作:位块传送命令编码器]()。 #### 多线程,命令缓冲,和命令编码器 大多数应用使用单线程来编码在单一指令缓冲区中一帧的渲染命令。在每帧的末尾,提交编码缓冲区,计划和开始命令执行。 如果你想去并行编码命令缓冲区,那么你可以同时创建多个命令缓冲区,用一个分开的线程编码每一个。如果你知道提前以什么顺序应该执行一个命令缓冲区,那么[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)的[enqueue](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443019-enqueue)方法可以声明在命令队列中的执行顺序,不需要等待命令被编码和提交。否则,当一个命令缓冲区被提交,在任何以前入队的命令缓冲以后它会被分配到命令队列的一个地方。 有时仅仅一个CPU线程可以访问一个命令缓冲。多线程应用每个命令缓冲可以使用一个线程来创建多个并行的命令缓冲。 图表2-2显示了一个三线程的例子。每个线程有它自己的命令缓冲区。对于每个线程,一个命令编码器在一次访问它相关的命令缓冲。图表2-2也显示了每个指令缓冲接收来自不同命令编码器的命令。当你结束编码时,调用命令编码器的[endEncoding](https://developer.apple.com/documentation/metal/mtlcommandencoder/1458038-endencoding)方法,一个新的命令编码器对象可以开始编码指令到命令缓冲区。 图表2-2 多线程Metal命令缓冲区 ![img-w600](https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Art/Cmd-Model-threads_2x.png) [MTLParallelRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlparallelrendercommandencoder)对象允许将一个渲染通过拆分到多个命令编码器和分配到单独的线程。关于[MTLParallelRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlparallelrendercommandencoder)更多信息,请看使用[多线程编码单一渲染管道]()。 ## 资源对象:缓冲和纹理 这章描述Metal资源对象(MTLResource)存储未格式化的内存和格式化的图像数据。MTLResource对象有两个类型: * MTLBuffer表示未格式化内存的分配,它可以包含任意类型数据。缓冲区通常被用在顶点,着色器,和计算状态数据。 * MTLTexture表示指定纹理类型和像素格式的格式化图像数据的分配。纹理对象被用作顶点,片元的源纹理,或者计算方法,和存储图像渲染输出一样(换言之,作为一个附件)。 MTLSamplerState对象也在这章中被讨论。尽管采样器不是资源本身,他们当执行用纹理对象检查计算时被使用。 ### 缓冲是无类型的内存分配 MTLBuffer对象表示一个可以包含任意类型数据的内存分配。 #### 创建一个缓冲对象 [MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)的下列方法创建和返回一个MTLBuffer对象: * newBufferWithLength:options:方法用一个新的存储分配创建MTLBuffer对象。 * newBufferWithBytes:length:options:方法通过从已存在的存储(位于CPU地址指针)拷贝进入一个新的存储分配来创建一个MTLBuffer对象。 * newBufferWithBytesNoCopy:length:options:deallocator:方法用一个已存在的存储分配来创建一个MTLBuffer对象,对于这个对象不能分配任何新的存储。 所有缓冲创建方法有输入值length来标明存储分配的大小,以字节来表示。对于可以修改创建缓冲区行为的options,所有方法也接受一个MTLResourceOptions对象。如果options的值时0,默认值被用作资源选项。 #### 缓冲方法 MTLBuffer协议有下列方法: * contents方法返回缓冲区的存储分配的CPU地址。 * newTextureWithDescriptor:offset:bytesPerRow:方法创建一种特殊的纹理对象,该纹理对象引用缓冲区的数据。这个方法被详细讲解在创建一个纹理对象章节中。 ### 纹理是格式化的图像数据 一个MTLTexture对象代笔爱哦一个格式化图像数据的分配,它可以被用作一个顶点着色器的资源,片元着色器,或者计算方法,或者作为一个附件被用作一个渲染的终点。MTLTexture对象可以有下列结构中的一个: * 一个1D,2D,或者3D的图像 * 一个1D或者2D图像的数组 * 六个2D图像的立方体 MTLPixelFormat指定在一个MTLTexture对象个体像素构成。像素格式被更进一步讨论在纹理像素格式。 #### 创建一个纹理对象 下列方法创建并返回一个MTLTexture对象: * [MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)的newTextureWithDescriptor:方法用一个纹理图像数据的新存储分配来创建一个MTLTexture对象,使用MTLTextureDescriptor对象来描述纹理的属性。 * MTLTexture的newTextureViewWithPixelFormat:方法来创建一个MTLTexture对象,这个对象共享相同的存储分配来调用MTLTexture对象。因为他们共享相同的存储,新纹理对象的像素的任何改变被反映在调用纹理对象上,反之亦然。对于最新创建的纹理,newTextureViewWithPixelFormat:方法重新解释了已存在的调用MTLTexture对象的存储分配的纹理图像数据,好像数据被被存储到一个指定的像素格式。新纹理对象MTLPixelFormat必须和原始纹理对象``的MTLPixelFormat是兼容的。(请看像素格式关于普通的纹理细节,包装,和压缩像素格式。) * MTLBuffer的newTextureWithDescriptor:offset:bytesPerRow:方法创建一个MTLTexture对象,调用MTLBuffer对象作为它的纹理图像数据来共享存储分配。当它们共享相同的存储,新纹理对象像素的任何改变都会被反映在调用纹理对象上,反之亦然。在纹理和缓冲之间共享存储可以防止指定纹理优化的使用,例如像素混合或平铺。 #### 使用纹理描述符创建一个纹理对象 MTLTextureDescriptor定义属性,这些属性被用来创建一个MTLTexture对象,包括它图像大小(宽,高,和深),像素格式,排列(数组或者立方体类型)和纹理映射数量。MTLTextureDescriptor属性仅仅被使用在MTLTexture对象创建期间。在创建MTLTexture对象后,属性改变在它的MTLTextureDescriptor对象中而纹理上不再有任何效果。 从描述符创建一个或者多个纹理: 1. 创建一个自定义MTLTextureDescriptor对象,它包含描述纹理数据的纹理属性: * textureType属性指定纹理维度和排列(例如,数组或立方体)。 * width,height和depth属性指定基准面纹理映射每个尺寸的像素大小。 * pixelFormat属性指定一个像素如何在纹理中存储的。 * arrayLength属性指定MTLTextureType1DArray或MTLTextureType2DArray类型纹理对象的数组元素的个数。 * mipmapLevelCount属性指定了每个像素采样的个数。 * resourceOptions属性指定了它内存分配的行为。 2. 从MTLTextureDescriptor对象通过调用一个[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)对象的newTextureWithDescriptor:方法来创建一个纹理。纹理创建后,调用replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:方法加载纹理图像数据,详情见复制图像数据和纹理。 3. 创建更多MTLTexture对象,你可以重用相同的MTLTextureDescriptor对象,修改需要的描述符的属性值。 清单3-1 显示了创建一个纹理描述符txDesc和设置它对于一个3D,64×64×64的纹理属性的代码 清单3-1 用一个自定义的纹理描述符创建一个纹理对象
1
2
3
4
5
6
7
8
9
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纹理图像和所有关联的纹理映射。对于每个切片: * 基准面纹理映射大小被指定MTLTextureDescriptor对象的width,height,和depth属性。 * 纹理映射级别缩放大小 *i* 被指定,max(1, floor(width/2i))×max(1,floor(height/2i))×max(1,floor(depth/2i))。最大纹理映射级别是第一个纹理映射获得的级别大小为1×1×1。 * 纹理映射级别数量在一个切片中可以被决定floor(log2(max(width, height, depth)))+1。 所有纹理对象有至少一个切片;立方体和数组纹理类型可能有数个切片。在复制图像数据和纹理章节中讨论读写纹理图像数据的规范方法,切片是一个零点输入值。对于一个1D,2D,或3D纹理,只有一个切片,所以切片的值必须是0.一个立方体纹理有6个完全的2D切片,地址从0到5.对于1DArray和2DArray纹理类型,每个数组元素表示一个切片。例如,对于一个2DArray纹理类型有arrayLength = 10,有10个完整的切片,地址从0到9.选择一个单一的1D,2D,或3D图像整体的纹理结构,首选选择一个切片,然后选择一个切片内的纹理映射级别。 #### 用便利的方法创建一个纹理描述符 对于普通2D和立方体纹理,使用下列便利的方法来创建一个MTLTextureDescriptor对象,几个属性值自动设置: * texture2DDescriptorWithPixelFormat:width:height:mipmapped:方法创建一个MTLTextureDescriptor对象对于2D纹理。width和height值定义了2D纹理的尺寸。type属性被自动设置为MTLTextureType2D, depth和arrayLength被设置成1. * textureCubeDescriptorWithPixelFormat:size:mipmapped: 方法创建一个立方体纹理的MTLTextureDescriptor对象,type属性被设置为MTLTextureTypeCube,width和height被设置为size,depth和arrayLength被设置为1. 两个MTLTextureDescriptor便利方法接收一个输入值,pixelFormat,它定义了纹理的像素格式。这两种方法也接受输入值产生,这决定了是否产生纹理图像。(如果mipmapped是YES,纹理被贴图细化。) 清单3-2 使用texture2DDescriptorWithPixelFormat:width:height:mipmapped:方法来创建一个64×64的2D纹理描述符对象,它不是纹理细化的。 清单3-2 用便利的纹理描述符创建一个纹理对象
1
2
3
4
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和纹理映射级别0。 清单3-3 复制图像数据和纹理
1
2
3
4
5
6
7
//  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对象的颜色,深度,和模板数据存储的组成。有三种像素格式:ordinary,packed和compressed。 * Ordinary格式只有常规8,16或32位颜色组件。每个组件都是安排在增加内存地址,第一列组件在最低的地址。例如,MTLPixelFormatRGBA8Unorm是一个每个颜色组件8位的32位的格式;最低位地址包含红色,下一个地址包含绿色,等等。与此相反,MTLPixelFormatBGRA8Unorm,最低位地址包含蓝色,下一个地址包含绿色,等等。 * Packed格式结合多个组件组合为一个16位或32位值,组件存储从最小最有效位(LSB MSB)。例如,MTLPixelFormatRGB0A2Unit是一个32位packed格式,它由3个10位通道和两位Alpha组成。 * Compressed格式排列的像素块,每一块的布局是特定于该像素格式。压缩像素格式仅仅可以被用于2D,2D数组,或者立方体纹理类型。压缩格式不能被用于创建1D,2D多采样或3D纹理。 MTLPixelFormatGBGR422和MTLPixelFormatBGRG422是特殊像素格式,这个格式是用来存储YUV色彩空间的像素。这些格式只支持2D纹理(但是既不是2D数组,也不是立方体类型),没有纹理映射,偶数宽度。 一些像素格式存储sRGB色彩空间的颜色组件(例如,MTLPixelFormatRGBA8Unorm_sRGB或MTLPixelFormatETC2_RGB8_sRGB)。当一个采样操作引用拥有sRGB像素格式的纹理,在采样操作发生之前,Metal实现转换sRGB色彩空间组件到一个线性颜色空间。sRGB转换,S,一个线性组件,L,如下: * If S <= 0.04045,="" l="S/12.92" *="" if="" s=""> 0.04045, L = ((S+0.055)/1.055)2.4 相反地,当渲染一个色彩渲染附件,它使用sRGB像素格式的纹理,实现转换成想线性颜色值到sRGB,如下: * If L <= 0.0031308,="" s="L" *="" 12.92="" if="" l=""> 0.0031308, S = (1.055 * L0.41667) - 0.055 关于渲染像素格式的更多信息,请看创建一个渲染通道描述符。 #### 创建一个纹理查询的采样器状态对象 MTLSamplerState对象定义了寻址,过滤和其他属性,当一个图像或者计算函数对MTLTexture对象执行纹理采样操作时这些属性被使用。采样器描述符定义了采样器状态对象的属性。创建一个采样器对象: 1. 调用[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)的newSamplerStateWithDescriptor:方法来创建一个MTLSamplerDescriptor对象。 2. 在MTLSamplerDescriptor对象中设置期望的值,包括过滤选项,寻址方式,最大各向异性,和细节层次参数。 3. 通过调用[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)对象的newSamplerStateWithDescriptor:方法创建描述符,从采样器描述符创建MTLSamplerState对象。 你可以重用采样器描述符对象来创建更多MTLSamplerState对象,修改所需的描述符的属性值。描述符属性仅在对象创建期间被使用。采样器状态已经被创建后,在采样器状态上改变其描述符的属性不再有效果。 清单3-4 是一个代码例子创建一个MTLSamplerDescriptor和配置它为了创建一个MTLSamplerState。对于描述符对象的过滤和寻址方式属性设置为非默认值。然后newSamplerStateWithDescriptor:方法。 清单3-4 创建一个采样器状态对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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];
#### 保持CPU和GPU内存之间的一致性 MTLResource对象可以访问在CPU和GPU两者之间的底层存储。然而,GPU从主机CPU异步操作,所以记住以下当使用主机CPU访问这些资源的存储。 当执行一个[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)对象,[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)对象只保证观察任何更改由[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)对象引用的任何MTLResource对象分配到主机CPU的存储,如果(且仅当)这些变化是由主机CPU在[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)对象被提交之前产生。就是说,[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)对象可能不观察在相应的[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)对象被提交后主机CPU资源产生的改变(例如,[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)对象的status属性是MTLCommandBufferStatusCommitted)。 相似地,在[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)对象执行一个[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)对象之后,如果命令缓冲已经被执行完成,那么主机CPU只保证观察[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)对象产生的命令缓冲引用的任何资源存储分配的任何改变(也就是说,[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)对象的status属性是[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)StatusCompleted)。 ## 函数和库 这章描述如何创建一个MTLFunction对象作为一个Metal着色器的参考或计算函数,如何组织和访问MTLLibrary对象的方法。 ### MTLFunction表示着色器或计算函数 MTLFunction对象表示单一的函数,这个函数用Metal着色语言编写,作为图形或者计算管线的一部分在GPU上执行。Metal着色语言详情,参见Metal着色语言指导。 在Metal运行时和图像或者用Metal着色语言编写的计算函数之间传递数据或状态,分配纹理,缓冲和采样器的索引参数。参数索引确定哪个纹理,缓冲或采样器正在被引用,通过Metal运行时和Metal着色代码。 渲染通道,指定一个MTLFunction对象作为一个MTLRenderPipelineDescriptor对象的顶点或者片元着色器,详见创建一个渲染管线状态。计算管道,当在目标设备上创建一个MTLComputePipelineState对象时指定一个MTLFunction对象,详见指定命令编码器的计算状态和资源。 ### 库是函数的一个仓库 MTLLibrary对象表示了一个或更多MTLFunction对象的一个仓库。一个MTLFunction对象表示了一个Metal函数,这个函数用着色语言编写。在Metal着色语言中源码中,任何函数使用Metal标识符(vertex,fragment或kernel)可以通过在库中的一个MTLFunction对象来显示。Metal函数没有这些函数标识符中的一个不能直接用一个MTLFunction对象来显示。 库中的MTLFunction对象可以从这些资源中创建: * Metal着色语言代码在app编译过程中被编译成一个二进库格式。 * 包含在Metal着色语言源码中的一个文本字符串在app运行时被编译。 #### 从编译的代码创建一个库 为了最佳性能,在你的应用程序在Xcode编译过程中Metal着色语言源码将被编译成为一个库文件,这避免了在应用程序运行时编译函数源码的消耗。创建一个MTLLibrary对象从一个二进制库,调用[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)的下列方法之一: * newDefaultLibrary获取为main bundle构建的一个库,它包含所有共享和计算函数在应用程序的xCode项目中。 * newLibraryWithFile:error:获取库文件的路径并返回一个MTLLibrary对象,这个对象包含了所有存储在库文件中的所有函数。 * newLIbraryWithData:error:获取二进制大对象包含在库中的函数源码并返回一个MTLLibrary对象。 更多关于在构建过程中编译Metal着色语言源码的信息,参见创建应用程序过程中创建库。 #### 从源码创建一个库 调用下列[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)方法中的一个,从一个包含数个函数的Metal着色语言源码的字符串来创建一个MTLLibrary。当库被创建时这些函数编译源码。指定使用的编译器选项,设置MTLCompileOptions对象的属性。 * newLibraryWithSource:options:error:从输入字符串同步编译源码创建MTLFunction对象,返回包含他们的一个MTLLibrary对象。 * newLibraryWithSource:options:completionHandler:从输入字符串异步编译源码创建MTLFunction对象,随后返回包含他们的一个MTLLibrary对象。completionHandler是一个当对象创建完成时被调用的代码块。 #### 从库中获取函数 MTLLibrary的newFunctionWithName:方法返回一个带有请求名字的MTLFunction对象。如果函数名在库中没有发现使用Metal着色语言函数标识符,那么newFunctionWithName:返回nil。 清单4-1 使用[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)的newLibraryWithFile:error:方法通过它的全名来查找一个库文件,使用它的内容来创建一个带有一个或者多个MTLFunction对象的MTLLibrary对象。加载文件中的任何错误都被返回在error中。接下来MTLLibrary的newFunctionWithName:方法创建一个MTLFunction对象,这个对象表示了在源码中名叫my_func的函数。返回的函数对象myFunc可以在应用中被使用。 清单4-1 从库中获取函数
1
2
3
4
NSError *errors;
id <MTLLibrary> library = [device newLibraryWithFile:@"myarchive.metallib"
error:&errors];
id <MTLFunction> myFunc = [library newFunctionWithName:@"my_func"];
### 在运行时决定函数细节 由于MTLFunction对象的实际内容通过一个图像着色器或者计算函数定义,这些可能在MTLFunction对象创建前被编译,对于应用其源码可能不是直接可用的。在运行时可以查询下列MTLFunction属性: * name,函数名称字符串 * functionType,表明函数是被声明为顶点,片源还是计算方法。 * vertexAttributes,MTLVertexAttribute对象的一个数组,其描述顶点属性数据在内存中是如何被组织的,它是如何被映射到顶点函数参数的。对于更多细节,参见数据组织顶点描述符。 MTLFunction不提供函数参数的访问。一个映射对象(或者MTLRenderPipelineReflection或者MTLComputePipelineReflection,依赖于命令编码器的类型)揭示了在管线状态创建时的着色器细节或计算方法参数获取。创建管线状态和映射对象的细节,参见创建一个渲染管线状态或创建一个计算管线状态。避免获取映射数据如果它没有被使用。 映射对象包含一组命令编码器支持的各种类型函数的MTLArgument对象。对于[MTLComputeCommandEncoder](https://developer.apple.com/documentation/metal/mtlcomputecommandencoder),在MTLComputePipelineReflection的arguments属性为一组MTLArgument对象,对应其计算函数的参数。对于[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder),MTLRenderPipelineReflection有两个属性,vertexArguments和fragmentArguments,它们各自对应顶点函数参数和片元函数参数的数组。 并不是函数的所有参数都表示一个映射对象。映射对象只包含有一个关联资源的参数,但不是用[[ stage_in ]]标识符或者内建的[[ vertex_id ]]或[[ attribute_id ]]标识符声明的参数。 清单4-2 展示了如何获取一个映射对象(在这个例子中,MTLComputePipelineReflection)和接下来通过MTLArgument在其arguments属性进行迭代。 清单4-2 通过函数参数迭代
1
2
3
4
5
6
7
8
MTLComputePipelineReflection* reflection;
id <MTLComputePipelineState> computePS = [device
newComputePipelineStateWithFunction:func
options:MTLPipelineOptionArgumentInfo
reflection:&reflection error:&error];
for (MTLArgument *arg in reflection.arguments) {
// 处理每一个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, texture2d_msh以及texturecube类型)和textureDataType属性表明组件数据类型(例如half,float,int或uint)。 * 如果type是MTLArgumentTypeThreadGroupMemory,threadgrouopMemoryAlignment和threadgroupMemoryDataSize属性是相关的。 * 如果type是MTLArgumentTypeBuffer,bufferAlignmentbufferDataSizebufferDataType以及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 处理结构体参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)和MTLParalledlRenderCommandEncoder对象,他们被用来编码图像渲染指令到命令缓冲。[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)命令描述图像渲染管线,像图表5-1看的。 图表 5-1 Metal图像渲染管线 ![img-w600](https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Art/gfx-pipeline_2x.png) [MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)表示了一个单独的渲染命令编码器。[MTLParallelRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlparallelrendercommandencoder)对象使单一渲染通道能够分解成大量离散的[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)对象,它们每个可能分配在不同的线程上。来自不同渲染命令编码器的命令链接在一起并一起一致的执行,可预见的顺序,正如在多线程渲染通道章节中描述的。 ### 创建并使用渲染命令编码器 创建,初始化和使用一个渲染命令编码器: 1. 创建一个MTLRenderPassDescriptor对象来定义附件集合,对于渲染通道这些附件担任命令缓冲区中图形命令渲染终点的角色。表示性地,一旦你创建一个MTLRenderPassDescriptor对象并你的应用每次重用它渲染帧。 2. 使用指定渲染管线通过调用[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)的renderCommandEncoderWithDescriptor方法来创建一个[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)对象。参见使用渲染管道描述符来创建一个渲染命令编码器。 3. 创建一个[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)对象来定义对于一次或多次绘制调用图像渲染管线的状态(包括着色器,混合,多采样和可视化测试)。为了使用这个绘制基元渲染管线状态,调用[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)的setRenderPipelineState:方法。相关详细信息,参见创建渲染管线状态。 4. 通过渲染命令编码器设置纹理,缓冲和被使用的采样器,如指定渲染着色器资源中描述的。 5. 调用[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)方法来指定额外的固定功能状态,包括深度和模板状态,在固定功能状态操作中解释。 6. 最终,调用[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)方法来绘制图像基元,如绘制几何基元中描述的。 #### 创建渲染管道描述符 MTLResourcePassDescriptor对象表示编码的渲染命令终点,它是一个附件的集合。渲染描述符的属性可能包含色彩像素数据四个附件的一个数组,深度像素数据的一个附件,模板像素数据的一个附件。renderPassDescriptor便利的方法用默认附件状态的颜色,深度和模板附件创建一个MTLRenderPassDescriptor对象。visibilityResultBuffer属性指定了一个缓冲,这个缓冲指定一个缓冲区,设备可以更新指示是否任何采样通过了深度和模板测试---相关详细信息,参见固定功能状态操作。 每个独立的附件,包含纹理,这个纹理将通过一个附件描述符被编写和表现。对于一个附件描述符,必须选择合适关联的纹理像素格式来存储颜色,深度或者模板数据。对于颜色附件描述符,MTLRenderPassColorAttachmentDescriptor,使用一个颜色可渲染的像素格式。对于深度附件描述符,MTLRenderPassDepthAttachmentDescriptor,使用一个深度可渲染的像素格式,例如MTLPixelFormatDepth32Float。对于一个模板缓冲描述符,使用一个模板可渲染的像素格式,例如MTLPixelFormatStencil8。 在设备上每个像素实际使用纹理内存的数量不总是匹配Metal框架代码的纹理像素格式的大小。因为设备添加了对齐的填料或者其他的目的。参见Metal特性表集章节对于每种像素格式实际使用了多少内存,同样大小的限制和附件的数量。 ##### 加载和存储动作 附件描述符的loadAction和storeAction属性指定了一个动作,这个动作执行在渲染通道的开始或结束。([MTLParallelRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlparallelrendercommandencoder),加载和存储动作出现在整个命令边界,而不是每个[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)对象。相关详细信息,参见多线程渲染通道)。 可能的loadAction值包括: * MTLLoadActionClear,写相同的值到指定附件描述符的每个像素。关于这个动作的详细信息,参见指定清除加载动作。 * MTLLoadActionLoad,保留现有的纹理内容。 * MTLLoadActionDontCare,允许附件的每个像素获得在渲染通道初始任意值。 如果你的应用程序将渲染给定帧的附件的所有像素,使用默认加载动作MTLLoadActionDontCare。MTLLoadActionDontCare动作允许GPU避免加载已存在的纹理内容,确保最佳的新能。否则,你可以使用MTLLoadActionClear动作来清除附件以前的内容,或者MTLLoadActionLoad动作来保留他们。MTLLoadActionClear动作也避免了加载已存在的纹理内容,但是它引发了用纯色填充终点的消耗。 可能的storeAction值包括: * MTLStoreActionStore,保存渲染通道的最终结果到附件。 * MTLStoreActionMultisampleResovle,解决来自渲染目标多采样数据到单一采样值,通过附件属性resolveTexture来指定他们存储的纹理,保留未定义附件内容。相关详细信息,参见例子:创建一个多采样渲染的渲染管道描述符。 * MTLStoreActionDontCare,在渲染通道完成时以未定义状态保留附件。这可能提高性能,它使避免任何保存渲染结果的必要工作的实现成为可能。 对于颜色附件,MTLStoreActionStore动作是默认存储动作,由于应用程序几乎总是保存渲染管道末尾附件的最终颜色值。对于深度和模板附件,MTLStoreActionDontCare是默认存储动作,因为那些附件表示性地不需要被保存在渲染通道完成时。 ##### 指定清除加载动作 如果附件描述符的加载动作属性设置为MTLLoadActionClear,那么清除值被写入渲染通道起始位置指定附件描述符的每个像素。清除值依赖于附件类型。 * MTLRenderPassColorAttachmentDescriptor,clearColor包含一个MTLClearColor值,该值包含四个双精度浮点数RGBA组件并用来清除颜色附件。MTLClearColorMake函数创建了一个来自红,绿,蓝和透明组件的清除颜色值。默认清除颜色值为(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 用颜色和深度附件创建一个渲染管道描述符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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属性为一个多采样类型的纹理,resovleTexture属性将包含多采样解决操作的结果。(如果纹理不支持多采样,那么一个多采样解决动作是未定义的结果。)resolveLevel,resolveSlice和resolveDepthPlane属性可能也被用于多采样解决操作去分别指定纹理映射级别,立方体切片,多采样纹理的深度表面。大多数情况,resolveLevel默认值,resovleSlice和resolveDepthPlain是可用的。在清单5-2,附件被初始化创建并它的loadAction,storeAction,texture和resovleTexture属性被设置支持多采样解决。 清单5-2 设置多采样解决附件属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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](https://developer.apple.com/documentation/metal/mtlcommandbuffer)对象的renderCommandEncoderWithDescriptor:方法来创建一个渲染命令编码器,如清单5-3所示: 清单5-3 用渲染管道描述符创建渲染命令编码器
1
2
id <MTLRenderCommandEncoder> renderCE = [commandBuffer
renderCommandEncoderWithDescriptor:renderPassDesc];
### 用核心动画显示渲染内容 核心动画定义了CAMetalLayer类,它为一个使用Metal渲染的层支持视图的专业行为而设计。CAMetalLayer对象表示关于内容(位置和大小)的几何结构信息,它的可视化属性(背景色,边界和阴影),并且资源使用Metal来表现颜色附件内容。 它也封装了内容呈现的时机以便在可用或者在指定时间尽可能快的显示。核心动画的更多信息,参见核心动画编程指南。 核心动画也定义了显示资源对象的CAMetalDrawable协议。扩展MTLDrawable的CAMetalDrawable协议提供了遵循MTLTexture协议的对象,所以它可以被用作渲染命令的终点。渲染CAMetalLayer对象,你应当获取一个新的CAMetalDrawable对象对于每个渲染通道,获取它提供的MTLTexture对象,并使用哪个纹理来创建颜色附件。不像颜色附件,深度或者模板附件的创建和销毁是代价很高的。如果你需要深度或模板附件,创建他们一次并在帧渲染时重用他们。 表示性地,你使用layerClass方法来指定CAMetalLayer作为你自己自定义的UIView子类的支持层,如清单5-4所示。否则,你可以用它的init方法来创建一个CAMetalLayer,已存在的视图中包含这个层。 清单5-4 使用CAMetalLayer作为UIView子类的支持层
1
2
3
+ (id) layerClass {
return [CAMetalLayer class];
}
为了用Metal渲染层的内容,你必须获取一个来自CAMetalLayer对象可显示的资源(一个CAMetalDrawable对象),然后通过附加它到一个MTLRenderPassDescriptor对象源渲染纹理的这个资源。为此,你首先设置CAMetalLayer对象的属性,它们描述了其提供的可绘制资源,然后每次在你开始渲染一个新帧时调用它的nextDrawable方法。如果CAMetalLayer属性不设置,nextDrawable方法会调用失败。CAMetalLayer的下列属性描述可绘制对象: * device属性声明了[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)对象创建资源。 * pixelFormat属性声明纹理像素格式。支持的值时MTLPixelFormatBGRA8Unorm(默认)和MTLPixelFormatBGRA8Unorm_sRGB。 * drawableSize属性声明了设备像素的纹理尺寸。确保你应用以精准的尺寸显示渲染内容(在相同的设备上不需要额外采样阶段),当计算你的层期望的尺寸时考虑目标屏幕的nativeScale或nativeBounds属性。 * framebufferOnly属性声明是否纹理可以被仅仅用作附件(YES)或者是否它可以被用作纹理采样和像素读/写操作(NO)。如果YES,层对象可以优化纹理显示。对于大多数应用,推荐值为YES。 * presentsWithTransaction属性声明了是否用核心动画事务机制(YES)改变层的渲染资源更新或者被异步更新到普通层(NO,默认值)。 如果nextDrawable方法成功,它返回一个带有下列只读属性的CAMetalDrawable对象: * texture属性持有纹理对象。当创建你的渲染管线(MTLRenderPipelineColorAttachmentDescriptor)时使用这个作为一个附件。 * 指向CAMetalLayer对象的layer属性负责显示几何体。 > 重要说明:只有一套小的几何体资源,所以一个长的帧渲染时间可以临时耗尽那些资源并引起nextDrawable方法调用它的CPU线程直到这个方法完成。避免昂贵的CPU停转,在调用CAMetalLayer对象的nextDrawable方法之前执行不需要可绘制资源的所有每帧操作。 在渲染完成后显示几何体的内容,你必须通过调用可绘制对象的present方法提交它到核心动画。随着负责它渲染的命令缓冲的完成来同步显示几何体,你可以调用[MTLCommandBuffer](https://developer.apple.com/documentation/metal/mtlcommandbuffer)的[presentDrawable:](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443029-presentdrawable)presentDrawable:atTime:便利方法。这些方法使用预定的处理程序(参见注册命令缓冲执行的处理程序块)来调用几何体的present方法,它覆盖了大多数场景。presentDrawable:atTime:方法提供了当几何体被显示时更进一步的控制。 ### 创建一个渲染管线状态 使用一个[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)对象来编码渲染命令,你必须首先指定一个MTLRenderPipelineState对象来定义每次绘制调用的图形状态。渲染管线状态对象是一个可以在渲染命令编码器外部被创建的长时间持久化对象,提前缓存和穿过多个渲染命令编码器被重用。当描述同一套图形状态时,重用一个以前创建的渲染管线状态对象可能避免再求值和转化CPU命令指定状态的高耗费操作。 渲染管线状态是一个不可变对象。创建一个渲染管线状态,首先创建和配置一个可变的描述渲染管线状态属性的MTLRenderPipelineDescriptor对象。然后,使用描述符来创建一个MTLRenderPipelineState对象。 #### 创建和配置一个渲染管线描述符 创建渲染管线状态,首先创建一个MTLRenderPipelineDescriptor对象,其有属性来描述在渲染过程中你想使用的图像渲染管线状态,如图表5-2所示。新的MTLRenderPipelineDescriptorcolorAttachment属性包含一组MTLRenderPipelineColorAttachmentDescript对象,每个描述符表示一个颜色附件状态,其指定混合操作和那个附件因子,详见配置渲染管线附件描述符的混合。附件描述符也指定了附件的像素格式,它必须匹配渲染管线描述符的像素格式,使用相应的附件索引,或者一个错误的出现。 图表 5-2 从描述符创建一个渲染描述状态 ![img-w360](https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Art/PipelineState_2x.png) 除了配置颜色附件之外,设置MTLRenderPipelineDescriptor对象的这些属性 * 设置depthAttachmentPixelFormat属性用MTLRenderPassDescriptor来匹配depthAttachment纹理像素格式。 * 设置stencilAttachmentPixelFormat属性用MTLRenderPassDescriptor来匹配stencilAttachment纹理像素格式。 * 指定在渲染管线状态的顶点或片元着色器,分别设置vertexFunctionfragmentFunction属性,设置fragmentFunction为nil禁用像素光栅化到指定颜色附件,其表示性地使用仅仅深度渲染或对于输出数据从顶点着色器进入一个缓冲对象。 * 如果顶点着色器有一个带有每个顶点输入属性的参数,设置vertexDescriptor属性来设置描述顶点数据的组成在那个参数,如数据组成顶点描述符所述。 * reasteriazationEnabled属性的默认值YES对于大多数典型渲染任务是足够的。只使用图形管线的顶点阶段(例如,收集顶点数据转换),设置这个属性为NO。 * 如果附件支持多采样(就是说,附件是一个MTLTextureTypeDMultisample类型纹理),那么每个像素多采样可以被创建。决定片元如何结合提供的像素覆盖,使用MTLRenderPipelineDescriptor的下列属性。 * sampleCount属性决定每个像素采样数量。当[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)被创建,对于所有附件纹理的sampleCount必须匹配这个sampleCount属性。如果附件不支持多采样,那么sampleCount是1,这也是默认值。 * 如果alphaToCoverageEnabled被设置为YES,那么colorAttachments[0]透明通道片元输出被读并用来决定覆盖面。 * 如果alphaToOneEnabled被设置为YES,那么colorAttachment[0]透明通道片元被强制设置为1,这是最大的可表示的值。(其他附件不受影响。) #### 从描述符创建一个渲染管线状态 在创建一个渲染管线描述符和指定其属性后,用它来创建MTLRenderPipelineState对象。因为创建一个渲染管线状态可能需要图形状态的一个昂贵的估价和指定图形着色器的可能的编译,你可以使用一个代码块或一个异步方法以一种最适合你应用的方式来安排这样的工作。 * 同步创建渲染管线状态对象,调用[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)对象的newRenderPipelineStateWithDescriptor:error:或newRenderPipelineStateWithDescriptor:options:reflections:error:方法。这些方法阻塞了当前线程,当Metal评估描述符图像状态信息和编译着色器代码来创建管线状态对象。 * 异步创建着色器管线状态对象,调用[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)对象的newRenderPipelineStateWithDescriptor:completionHandler:或newRenderPipelineStateWithDescriptor:options:completionHandler:方法。这些方法立即返回---Metal异步评估描述符的图像状态信息和编译着色器代码来创建管线状态对象,然后调用完成回调提供新的MTLRenderPipelineState对象。 当你创建一个MTLRenderPipelineState对像时,你也可以选择去创建揭示管线着色器函数及其参数的反射数据。newRenderPipelineStateWithDescriptor:options:reflection:error:和newRenderPipelineStateWithDescriptor:options:completionHandler:方法提供了这个数据。避免获取反射数据如果它将不被使用。更多关于如何分析反射数据的细节,参见运行时决定功能细节。 在你创建一个MTLRenderPipelineState对象后,调用[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)的setRenderPipelineState:方法来关联的渲染管线状态和渲染使用的命令编码器。 清单5-5 演示渲染管线状态被称为pipeline的对象的创建。 清单5-5 创建一个简单的管线状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
MTLRenderPipelineDescriptor *renderPipelineDesc =
[[MTLRenderPipelineDescriptor alloc] init];
renderPipelineDesc.vertexFunction = vertFunc;
renderPipelineDesc.fragmentFunction = fragFunc;
renderPipelineDesc.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm;

// Create MTLRenderPipelineState from <font color=Fuchsia>MTLRenderPipelineDescriptor</font>
NSError *errors = nil;
id <MTLRenderPipelineState> pipeline = [device
newRenderPipelineStateWithDescriptor:renderPipelineDesc error:&errors];
assert(pipeline && !errors);

// Set the pipeline state for [<font color=Fuchsia>MTLRenderCommandEncoder</font>](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)
[renderCE setRenderPipelineState:pipeline];
变量vertFunc和fragFunc是着色器函数,他们被指定作为渲染管线状态描述符的属性被称作renderPipelineDesc。调用[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)对象的newRenderPipelineStateWithDescriptor:error:方法同步地使用管线状态描述符来创建管线状态对象。调用[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)的setRenderPipelineState:方法指定MTLRenderPipelineState对象和渲染命令编码器一起使用。 > 注意:因为创建一个MTLRenderPipelineState对象是昂贵的,无论何时你打算使用相同图像状态应该重用它。 #### 配置渲染管线附件描述符混合 使用一个高可配置的缓和操作的混合来混合通过带有附件(终点)像素值的片元函数(来源)返回的输出。混合操作决定来源和终点的值是如何与混合因子结合起来的。 配置颜色附件的混合,设置MTLRenderPipelineColorAttachmentDescriptor的下列属性: * 开启混合,设置blendingEnabled为YES。混合默认是关闭的。 * writeMask识别哪个颜色通道被混合。默认值MTLColorWriteMaskAll允许所有颜色通道被混合。 * rgbBlendOperation和alphaBlendOperation分别用一个MTLBlendOperation值分配RGB和透明片元数据的混合操作。两个属性默认值是MTLBlendOperationAdd。 * sourceRGBBlendFactor,sourceAlphaBlendFactor,destinationRGBBlendFactor和destinationAlphaBlendFactor分配来源和终点混合因子。 #### 理解混合因子和操作 四个混合因子指向一个常量混合颜色值:MTLBlendFactorBlendColor,MTLBlendFactorOneMinusBlendColor,MTLBlendFactorBlendAlpha和MTLBlendFactorOneMinusBlendAlpha。调用[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)的setBlendColorRed:green:blue:alpha:方法用这些混合因子来指定常量颜色和透明值,如固定功能状态操作所述。 一些混合操作结合片元值通过源值乘以MTLBlendFactor值(缩写SBF),通过终点混合因子(DBF)乘以终点值并结合使用算术MTLBlendOperation表示的值的结果。(如果混合操作是MTLBlendOperationMin或MTLBlendOperationMax,SBF和DBF混合因子被忽略。)例如,MTLBlendOperationAdd对于如果被BlendOperation和alphaBlendOperation属性定义在下列RGB和透明值的添加剂混合操作: * RGB = (Source.rgb * sourceRGBBlendFactor) + (Dest.rgb * destinationRGBBlendFactor) * Alpha = (Source.a * sourceAlphaBlendFactor) + (Dest.a * destinationAlphaBlendFactor) 在默认的混合行为中,来源完全重写终点。这种行为等价于设置sourceRGBBlendFactor和sourceAlphaBlendFactor为MTLBlendFactorOne,destinationRGBBlendFactor和destinationAlphaBlendFactor为MTLBlendFactorZero。用数学表达这种行为: * RGB = (Source.rgb * 1.0) + (Dest.rgb * 0.0) * A = (Source.a * 1.0) + (Dest.a * 0.0) 另一个正常的使用混合操作,源透明定义了保留多少终点颜色,用数学表达为: * RGB = (Source.rgb * 1.0) + (Dest.rgb * (1 - Source.a)) * A = (Source.a * 1.0) + (Dest.a * (1 - Source.a)) #### 使用自定义混合配置 清单5-6显示了自定义混合配置的代码,使用混合操作MTLBlendOperationAdd,源混合因子MTLBlendFactorOne,终点混合因子MTLBlenderOneMinusSourceAlpha。colorAttachments[0]是一个带有指定混合配置属性的MTLRenderPipelineColorAttachmentDescriptor对象。 清单5-6 指定一个自定义混合配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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];
### 指定渲染命令编码器资源 在这部分讨论的[MTLRenderCommandEncoder](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)方法指定被用做顶点和片元着色器函数参数的资源,它们被指定通过在MTLRenderPipelineState对象的vertexFunction和fragmentFunction属性。这些方法分配了一个着色器资源(缓冲,纹理和采样器)到相应的参数表索引(atIndex)在渲染命令编码器中,如图表5-3所示。 图表 5-3 渲染指令编码器的参数表 ![img-w600](https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Art/ArgTable-render_2x.png) 下列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中,各自被定义在顶点着色器中有索引0和1的两个缓冲区(posBuf和texCoordBuf)

清单5-7 Metal框架:指定顶点函数资源

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

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

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

相似地,在清单5-9中,缓冲,纹理和采样器(分别的fragmentColorBuf,shadeTex和sampler),所有索引为0的,被定义在片元着色器。

清单5-9 Metal框架:指定片元函数资源

1
2
3
[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着色语言:片元函数参数匹配框架参数表索引

1
2
3
4
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中顶点函数vertexMath例子中看到的VertexInput结构体。每个顶点的输入结构体的各个域有[[ attribute(index) ]]限定符,它指定了顶点属性参数表的索引。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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对象,设置它作为MTLRenderPipelineState的vertexDescriptor属性。MTLVertexDescriptor有两个属性:attributes和layouts。

MTLVertexDescriptor的attributes属性是一个MTLVertexAttributeDescriptorArray对象,它定义了每个顶点属性在被纹理映射到顶点函数参数的缓冲中是如何组织的。attributes属性可以支持存取多个属性(例如顶点坐标,表面法线和纹理顶点)在相同缓冲区内是交叉的。在着色语言代码中成员的顺序没有必要被保存在框架代码的缓冲区中。数组中的每个顶点属性描述符有一下属性,提供一个顶点着色函数信息去定位和加载参数数据:

  • bufferIndex,是一个缓冲参数表的索引,指定哪个MTLBuffer被访问。缓冲参数表在渲染命令编码器的指定资源章节中被讨论。
  • format,指定在框架代码中的数据应当如何被解释。如果数据类型不是一个精确的类型匹配,它可能被转换或扩展。例如,如果着色语言类型是half4,框架format是MTLVertexFormatFloat2,那么当数据被用作一个顶点函数的参数时,它可能被从浮点数转换到half并且扩展从2到4个元素(用0.0, 1.0在最后两个元素)。
  • offset,指定数据从顶点的起点开始被发现的位置。

图表 5-4 在Metal框架代码中阐明MTLVertexAttributeDescriptorArray,实现一个交叉缓冲区,和在清单5-11中的着色器语言代码中的顶点函数vertexMath的输入一致。

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

img-w600

清单5-12 和图表 5-4中显示的交叉缓冲区相对应的Metal框架代码。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id <<font color=Fuchsia>MTLFunction</font>> vertexFunc = [library newFunctionWithName:@"vertexMath"];            
<font color=Fuchsia>MTLRenderPipelineDescriptor</font>* pipelineDesc =
[[<font color=Fuchsia>MTLRenderPipelineDescriptor</font> 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 描述MTLVertexBufferLayoutDescriptor对应的diamante在清单5-12中。layout[0]指定如何从缓冲区参数表对应索引0的位置获取顶点数据。layouts[0].stride指定在两个顶点数据之间的40个字节的一个距离。layout[0].stepFunction的值MTLVertexStepFunctionPerVertex,指定绘制时每个顶点获取到的属性数据。如果stepFunction的值时MTLVertexStepFunctionPerInstance,stepRate属性决定了属性数据多久获取一次。例如,如果stepRate是1,数据被获取对于每个实例;如果stepRate是2,对于每两个实例,等等。

图表 5-5 缓冲区组织用顶点缓冲布局描述符

img-w600

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

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

  • setViewport:指定在屏幕坐标中的区域,它是虚拟3D世界投影的终点。视口是3D,所以它包含深度值;详情请见与视口和像素坐标系统一起工作。
  • setTriangleFillMode:决定是否光栅化三角形和有皱纹的三角形带基元(MTLTriangleFillModeLines)或作为填充三角形(MTLLTriangleFillModeFill)。默认值是MTLTriangleFillModeFill。
  • setCullMode:和setFrontFacingWinding:被一起使用去决定是否和如何挑选被应用。你可以使用挑选在一些几何模型上除去隐藏表面,例如一个用填充三角形渲染的可定向的球体。(表面是可定向的如果它的基元被一致地绘制按照顺时针或逆时针顺序)

    • setFrontFacingWinding的值:表明是否一个正面基元有它的顶点按照顺时针(MTLWindingClockWise)被绘制或逆时针(MTLWindingCounterClockwise)顺序。默认值是MTLWindingClockWise。
    • setCullMode的值:表明是否去执行选择(MTLCullModeNone,如果选择被禁用)或去选择哪个基元类型(MTLCullModeFront或MTLCullModeBack)。

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

  • setScissorRect:指定一个2D剪刀矩形。位于指定剪刀矩形外面的片元被丢弃。
  • setDepthStencilState:设置深度和模板测试状态如深度和模板状态中描述。
  • setStencilReferenceValue:指定模板引用值。
  • setDepthBias:slopeScale:clamp:指定一个对比阴影映射和来自片元着色器的深度值输出的调整。
  • setVisibilityResultMode:offset:决定是否去监控如果任何采样器通过深度和模板测试。如果设置MTLVisibilityResultModeBoolean,那么如果任何采样器通过深度和模板测试,一个非零值被写入一个由MTLRenderPassDescriptor的visibilityResultBuffer属性指定的缓冲区,如创建一个渲染通道描述符所述。

    你可以使用这个模式去执行遮挡测试。如果你绘制一个边界盒子并且没有样本通过,那么你可以得出结论,在该边界盒子里面的任何对象被遮挡,因此不需要渲染。

使用视口和像素坐标系统

Metal定义了其规格化设备坐标(NDC)系统作为中心在(0,0,0.5)的2×2×1立方体。左侧和底部的x和y分别被NDC系统指定为-1。右侧和顶部的x和y分别被NDC系统设置为+1。

视口指定来自NDC到窗口系统的转换。Metal视口是一个由MTLRenderCommandEncoder得setViewport:方法指定的3D转换。窗口原点坐标是在左上角。

在Metal中,像素中心偏移(0.5,0.5)。例如,原点像素有它的中心在(0.5,0.5);其右边相邻像素的中心是(1.5,0.5)。这也是真实的纹理。

执行深度和模板操作

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

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

如果深度测试被启用,渲染管线状态必须包含一个深度附件来支持写深度值。执行模板测试,渲染管线状态必须包含一个模板附件。配置附件,参见创建和配置一个渲染管线描述符。

如果你经常改变深度/模板状态,那么你可能想重用状态描述符对象,修改其属性值需要创建更多的状态对象。

注意: 从着色器函数中的深度格式纹理采样,着色器中实现采样操作不使用MTLSamplerState。

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

  • 对深度附件开启写深度值,设置depthWriteEnabled为YES。
  • depthCompareFunction指定深度测试如何被执行。如果一个片元的深度值在深度测试中失败,该片元被丢弃。例如,通常被用到的MTLComapreFunctionLess函数引起比(以前写入的)像素深度距离观察者更远的片元值在深度测试中失败;换言之,该片段被认为是由早期的深度值闭塞。
  • frontFaceStencil和backFaceStencil属性分别指定一个各自的MTLStencilDescriptor对象对于正面和背面基元。对于正面和背面基元使用相同模板状态,你可以分配同一个MTLStencilDescriptor到frontFaceStencil和backFaceStencil属性。显式地禁用模板测试对于一个或两个表面,设置相应的属性为nil,默认值。

显式地禁用模板状态是没有必要的。Metal基于模板描述符是否被配置到一个有效模板操作来决定是否去开启一个模板测试。

清单5-13 显示了一个创建的例子和对于MTLDepthStencilState对象的创建使用MTLDepthStencilDescriptor对象,然后使用渲染命令编码器。在这个例子中,正面基元的模板状态从深度/模板状态描述符的frontFaceStencil属性被存取。对于背面基元模板测试被显式地禁用。

清单5-13 创建和使用一个深度/模板描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)。

绘制几何基元

在你已经确定管线状态和固定功能状态后,你可以调用下列MTLRenderCommandEncoder方法来绘制几何基元。这些绘制方法引用资源(例如包含顶点坐标,纹理坐标,表面法线及其他数据的缓冲区)与着色器函数和以前用MTLRenderCommandEncoder确定的其他状态一起执行管线。

  • drawPrimitives:vertexStart:vertexCount:instanceCount:渲染大量(instanceCount)使用连续数组元素的顶点数据基元的实例,开始于数组元素的索引vertexStart并结束于数组元素索引vertexStart + vertexCount - 1。
  • drawPrimitives:vertexStart:vertexCount:和以前的有一个instanceCount为1的方法相同。
  • drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:渲染大量(instanceCount)使用一个在MTLBuffer对象indexBuffer指定的索引列表基元实例。indexCount决定的数量指标。索引表开始于indexBuffer里的数据内indexBufferOffset字节偏移的索引。indexBufferOffset必须是索引大小的倍数,它由indexType决定。
  • drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:和以前的有一个instanceCount为1的方法类似。

对于上述的每个基元渲染方法,第一个输入值用MTLPrimitiveType值中的一个来决定基元类型。其他输入值决定顶点被用来聚集基元。对于所有这些输入方法,instanceStart输入值决定绘制的第一个实例,instanceCount输入值决定绘制多少个实例。

像先前讨论的,setTriangleFillMode:决定是否三角形被渲染为填满或线框,setCullMode:和setFrontFacingWinding:设置决定是否GPU在渲染期间剔除三角形。对于更多信息,参见固定功能状态操作。

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

当扁平渲染一个三角形基元时,第一个顶点(又称为引发顶点)的属性被整个三角形使用。顶点函数的着色语言代码必须提供[[ flat ]]的插值限定符。

关于所有Metal着色语言属性和限定符更多细节,参见Metal着色语言指南。

结束渲染过程

结束渲染过程,调用在渲染命令编码上的endEncoding函数。在结束以前的命令编码器后,你可以创建一个任何类型新的命令编码器来编码额外的命令进入到命令缓冲区。

代码例子:绘制一个三角形

清单5-14阐述的下列步骤描述了一个渲染三角形基本的过程。

  1. 创建一个MTLCommandQueue并用它创建一个MTLCommandBuffer
  2. 创建一个MTLRenderPassDescriptor,它指定了一个附件集合,这些附件作为命令缓冲区中编码的渲染命令的终点。

    在这个例子中,仅仅第一个颜色附件被建立和使用。(变量currentTexture假定包含一个MTLTexture,它被用作一个颜色附件。)然后MTLRenderPassDescriptor被用来创建一个新的MTLRenderCommandEncoder

  3. 创建两个MTLBuffer对象,posBuf和colBuf,调用newBufferWithBytes:length:options:来拷贝顶点坐标和顶点颜色数据,posData和colData,分别进入缓冲区存储。

  4. 调用MTLRenderCommandEncodersetVertexBuffer:offset:atIndex:方法两次去指定坐标和颜色。

    setVertexBuffer:offset:atIndex:方法的输入值atIndex对应顶点函数源码中的属性buffer(atIndex)。

  5. 创建一个MTLRenderPipelineDescriptor并确定在管线描述符中的顶点和片元函数:

    • 用来自progSrc的源码创建一个MTLLibrary,它被假定为一个字符串,该字符串包含Metal着色器源码。
    • 然后调用MTLLibrary的newFunctionWithName:方法来创建MTLFunction verFunc表示hello_vertex的函数,创建MTLFunction fragFunc表示hello_fragment的函数。
    • 最终,用这些MTLFunction对象设置MTLRenderPipelineDescriptor的vertexFunction和fragmentFunction属性。
  6. 通过调用newRenderPipelineStateWithDescriptor:error:或MTLDevice的一个相似方法从MTLRenderPipelineDescriptor创建一个MTLRenderPipelineState。然后使用MTLRenderCommandEncoder的setRenderPipelineState:方法创建渲染管线状态。

  7. 调用MTLRenderCommandEncoder的drawPrimitives:vertexStart:vertexCount:方法追加命令去执行一个填充三角形(MTLPrimitvieTypeTriangle类型)的渲染。
  8. 调用endEncoding方法来结束这个渲染过程的编码。调用MTLCommandBuffercommit方法来执行设备上的命令。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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:colData
length:sizeof(colData) options:nil];
[renderEncoder setVertexBuffer:posBuf offset:0 atIndex:0];
[renderEncoder setVertexBuffer:colBuf offset:0 atIndex:1];

NSError *errors;
id <MTLLibrary> library = [device newLibraryWithSource:progSrc options:nil
error:&errors];
id <MTLFunction> vertFunc = [library newFunctionWithName:@"hello_vertex"];
id <MTLFunction> fragFunc = [library newFunctionWithName:@"hello_fragment"];
MTLRenderPipelineDescriptor *renderPipelineDesc
= [[MTLRenderPipelineDescriptor alloc] init];
renderPipelineDesc.vertexFunction = vertFunc;
renderPipelineDesc.fragmentFunction = fragFunc;
renderPipelineDesc.colorAttachments[0].pixelFormat = currentTexture.pixelFormat;
id <MTLRenderPipelineState> pipeline = [device
newRenderPipelineStateWithDescriptor:renderPipelineDesc error:&errors];
[renderEncoder setRenderPipelineState:pipeline];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0 vertexCount:3];
[renderEncoder endEncoding];
[commandBuffer commit];

在清单5-14中,一个MTLFunction对象表示叫做hello_vertex的着色器函数。MTLRenderCommandEncodersetVertexBuffer:offset:atIndex:的方法被用来指定顶点资源(在这个实例中,两个buffer对象)作为参数被传递进hello_vertex中。setVertexBuffer:offset:atIndex:方法的输入值atIndex对应顶点函数源码中的属性buffer(atIndex),如清单5-15所示。

清单5-15 对应的着色器函数声明

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

使用多线程编码单一渲染通道

在一些情况下,你应用的性能可能被单一渲染通道编码命令的单CPU工作量限制。然而,尝试避免这个瓶颈通过分离的工作量进入到多个CPU线程进行编码的多个渲染通道也可能会影响性能,因为每个渲染通道需要其本身中间附件存储和保存渲染目标内容的加载动作。

相反地,使用一个MTLParallelRenderCommandEncoder对象,它管理多个附属的MTLRenderCommandEncoder对象,这些对象共享相同命令缓冲和渲染通道描述符。并行渲染命令编码器保证附件加载和存储动作只发生在整个渲染通道的开始和结尾,而不是每个附属渲染命令编码器的指令集合的开头和结尾。在这种体系结构下,你可以并行、安全以及高效地方式分配每个MTLRenderCommandEncoder对象到它自己的线程。

使用MTLCommandBuffer对象的paralleleRenderCommandEncoderWithDescriptor:方法创建一个并行渲染命令编码器。从你想去执行命令编码的每个CPU线程调用一次MTLParallelRenderCommandEncoder对象的renderCommandEncoder方法创建附属命令编码器。所有来自相同并行渲染命令编码器创建的附属命令编码器进行编码相同命令缓冲的命令。按顺序被编码到命令缓冲区的命令,在该命令中,渲染命令编码器被创建。调用MTLRenderCommandEncoderendEncoding方法来结束指定渲染命令编码。在所有由并行渲染命令编码器创建的渲染命令编码器结束编码以后,调用MTLParallelRenderCommandEncoderendEncoding方法去结束渲染过程。

清单5-16 展示MTLParallelRenderCommandEncoder创建三个MTLRenderCommandEncoder对象:rCE1,rCE2和rCE3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 [<font color=Fuchsia>MTLRenderCommandEncoder</font>](https://developer.apple.com/documentation/metal/mtlrendercommandencoder)s must end before [<font color=Fuchsia>MTLParallelRenderCommandEncoder</font>](https://developer.apple.com/documentation/metal/mtlparallelrendercommandencoder)
[parallelRCE endEncoding];

命令编码器的调用endEncoding的顺序与命令被编码和追加进MTLCommandBuffer的顺序无关。MTLParallelRenderCommandEncoderMTLCommandBuffer总是按附属渲染命令编码器被创建的顺序包含命令,如图表5-6所示。

图表 5-6 并行渲染通道中渲染命令编码器的排序

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

这章解释如何创建和使用一个MTLComputeCommandEncoder对象去编码数据并行计算处理状态,命令以及提交他们到设备执行。

执行一个数据并行计算,主要有下面这些步骤:

  1. 使用MTLDevice方法来创建一个计算状态(MTLComputePipelineState),其包含来自MTLFunction对象编译的代码,如创建计算状态中讨论的。MTLFunction对象表示Metal着色语言中的编写的一个计算函数,如函数和库中描述的。
  2. 通过计算命令编码器来使用指定的MTLComputePipelineState对象,正如在指定计算状态和计算命令编码器资源中讨论的。
  3. 指定资源和相关对象(MTLBuffer,MTLTexture和可能的MTLSamplerState)可能包含被处理的数据和通过计算状态返回的数据,正如在指定计算状态和计算命令编码器资源中讨论的。也设置他们的参数表指数,以便Metal框架代码可以定位一个在着色器代码中相应的资源。在任何给定时刻,MTLComputeCommandEncoder可以被关联到许多资源对象。
  4. 调度指定次数的计算函数,正如执行计算命令中解释的。

创建计算管线状态

MTLFunction对象表示数据并行代码可以通过MTLComputePipelineState对象被执行。[MTLComputeCommandEncoder](https://developer.apple.com/documentation/metal/mtlcomputecommandencoder)对象编码命令,这些命令设置参数和执行计算功能。因为创建一个计算管线状态可能需要一个Metal着色语言代码的昂贵的编译,你可以使用一种最适合你应用设计的块或者异步方法来安排这样的工作。 * 同步创建计算管线状态对象,调用[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)的newComputePipelineStateWithFunction:error:newComputePipelineStateWithFunction:options:reflection:error:方法。当Metal编译着色器代码来创建管线状态对象时这些方法阻塞了当前线程。 * 异步创建计算管线状态对象,调用[MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)的newComputePipelineStateWithFunction:completionHandler:newComputePipelineStateWithFunction:options:completionHandler:方法。这些方法立即返回---Metal异步编译着色器代码来创建管线状态对象,然后调用完成回调去提供新的MTLComputePipelineState对象。 当你创建一个MTLComputePipelineState对象时,你也可以选择去创建反射数据,这些数据揭示了计算函数的细节及其参数。newComputePipelineStateWithFunction:options:reflection:error:newComputePipelineStateWithFunction:options:completionHandler:方法提供了这个数据。避免获取反射数据如果它不被使用。更多关于如何分析反射数据的信息,参见决定运行时函数细节。 ### 指定计算状态和计算命令编码器的资源 [MTLComputeCommandEncoder](https://developer.apple.com/documentation/metal/mtlcomputecommandencoder)对象的setComputePipelineState:方法指定其状态,包括一个编译的计算渲染函数,使用数据并行计算通道。在任何给定时刻,计算命令编码器可以被关联到仅有的一个计算函数。 下列[MTLComputeCommandEncoder](https://developer.apple.com/documentation/metal/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 计算命令编码器的参数表

img-w400

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

整个线程组内存分配的最大限制也被列在实现限制表中。

执行计算命令

编码一个执行计算函数的命令,调用MTLComputeCommandEncoder的dispatchThreadgroups:threadsPerThreadgroup:方法并指定线程组尺寸和线程组的数量。你可以查询MTLComputePipelineState的threadExecutionWidth和maxTotalThreadPerThreadgroup属性来优化设备计算函数的执行。

在线程组中的线程总数是threadsPerThreadgroup: threadsPerThreadgroup.width threadsPerThreadgroup.height threadsPerThreadgroup.depth组件的乘积。maxTotalThreadsPerThreadgroup属性指定线程最大数量,它可能是一个单一的线程组去在设备上执行这个计算函数。

调用MTLComputeCommandEncoderendEncoding方法结束计算命令编码器的编码命令。结束以前命令编码器以后,你可以创建一个任意类型的新的命令编码器来编码额外的命令进入到命令缓冲区。

代码样例:执行数据并行函数

清单6-1显示了一个样例,这个样例创建和使用MTLComputeCommandEncoder对象来执行一个指定数据图像变换的并行计算。(这个例子未展示设备,库,命令队列和资源对象被创建和初始化。)样例创建一个命令缓冲区,然后使用它来创建MTLComputeCommandEncoder对象。接下来MTLFunction对象被创建,其表示从MTLLibrary对象加载的入口点filter_main,如清单6-2所示。函数对象被用来创建一个被叫做filterState的MTLComputePipelineState对象。

计算函数在图像inputImage上执行一个图像变换和过滤操作,使用outputImage返回结果。首先setTexture:atIndex:setBuffer:offset:atIndex:方法分配纹理和缓冲对象到指定参数表的指数。paramsBuffer指定值用来执行图像变换,inputTableData指定滤镜权重。计算函数被执行作为一个各个尺寸的16×16像素大小的2D线程组。dispatchThreadgroups:threadsPerThreadgroup:方法排队命令来调度线程执行计算函数,endEncoding方法终止MTLComputeCommandEncoder。最终,MTLCommandBuffercommit方法引起了尽快地去执行命令。

清单6-1 在计算状态指定和运行一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 着色语言计算函数声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}

缓冲和纹理操作:位块传送命令编码器

MTLBlitCommandEncoder提供了在资源(缓冲和纹理)之间拷贝数据方法。数据拷贝操作对于图像处理和纹理效果可能是必要的,例如迷糊或反射。他们可能被用来访问被离屏渲染的图像数据。

执行数据拷贝操作,首先通过调用MTLCommandBuffer对象的blitCommandEncoder方法创建一个MTLBlitCommandEncoder对象。然后调用下面描述的编码指定到命令缓冲区的MTLBlitCommandEncoder的方法。

在资源对象之间的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:方法拷贝两个纹理图像数据范围:从单一立方体切片和源纹理映射级别到纹理终点toTexture。

纹理到缓冲的数据拷贝

copyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toBuffer:destinationOffset:destinationBytesPerRow:destinationBytesPerImage:方法拷贝图像数据范围从单一立方体切片和源纹理映射级别到缓冲终点toBuffer。

产生纹理映射

MTLBlitCommandEncoder的generateMipmapsForTexture:方法对于给定纹理自动产生纹理映射,从基本层次纹理图像开始。generateMipmapsForTexture:创建所有纹理映射级别到达最高级别的缩放图像。

填充缓冲内容

MTLBlitCommandEncoderfillBuffer:range:value:方法在给定缓冲指定的range上每个字节存储8位常量。

结束位块传送命令编码器

调用endEncoding来结束位块传送命令编码器的命令编码。在结束前一个命令编码器后,你可以创建一个任意类型的新的命令编码器来编码额外命令进入命令缓冲区。

Metal工具

这章列出了可用的工具来帮助你自定义和改善你开发的工作流。

在应用构建过程期间创建库

在应用构建过程期间编译着色语言源文件和构建库(.metallib文件)比运行时编译着色器源码能实现更好的应用性能。你可以用Xcode构建一个库或者通过使用命令行工具。

使用Xcode构建一个库

任何在你工程中的着色器源文件自动被使用去产生默认的库,你可以用MTLDevicenewDefaultLibrary方法从Metal框架代码存取。

使用命令行工具去构建一个库

图表8-1展示了产生Metal着色器源码编译器工具链的命令行工具。当你工程中包含.metal文件时,Xcode调用这些工具来构建一个库文件,这个文件你可以在应用运行时存取。

不使用Xcode编译着色器源码进入一个库:

  1. 使用metal工具来编译每个.metal文件进入单独的.air文件,它存储了着色语言代码的中间表示(IR)。
  2. 可选择地,使用metal-ar工具来把若干个.air文件归档到一起成为一个.metalar文件。(metal-ar和Unix ar类似。)
  3. 使用metallib工具来构建一个Metal的.metallib库文件从IR .air文件或从归档的.metalar文件。

图表 8-1 使用命令行工具构建一个库文件

img-w600

清单8-1 展示了编译和构建一个.metal文件成为一个.metallib文件需要的最少命令行。

清单8-1 用命令行工具构建一个库文件

1
2
xcrun -sdk macosx metal MyLibrary.metal -o MyLibrary.air
xcrun -sdk macosx metallib MyLibrary.air -o MyLibrary.metallib

在框架代码中使用结果库,调用newLibraryWithFile:error:方法,如清单8-2所示。

清单8-2 在你的应用中使用一个库文件

1
2
3
4
5
6
NSError *libraryError = NULL;
NSString *libraryFile = [[NSBundle mainBundle] pathForResource:@"MyLibrary" ofType:@"metallib"];
id <<font color=Fuchsia>MTLLibrary</font>> myLibrary = [_device newLibraryWithFile:libraryFile error:&libraryError];
if (!myLibrary) {
NSLog(@"Library error: %@", libraryError);
}
文章作者: Boyka·Yuri
文章链接: https://zhaolilong.com/2018/06/10/Metal编程指南/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 博伊卡の楼閣
支付宝打赏
微信打赏