指令组织和执行模式
在 Metal框架中, MTLDevice
协议定义的接口描述一个GPU。 MTLDevice
协议提供了一系列方法,可以查询设备属性、创建设备相关的对象比如缓存(buffers)和纹理(texture),并且可以编码( encoding)、排队渲染(queueing render) 以及提交计算指令(compute commands)到 GPU上执行。
一个 command queue 包含一系列的 command buffers,并且 command queue 管理着这些 command buffers 的执行顺序。 一个 command buffer包含多个在特定设备上执行的被编码指令(encoded commands)。 一个 command encoder 可以将绘制(rendering), 计算(computing), 位图传输( blotting)指令推入一个 command buffer , 最后这些 command buffers 将会被提交到设备上执行。
MTLCommandQueue
协议为 command queues 定义了接口, 主要提供创建 command buffer 对象的方法。 MTLCommandBuffer
协议为 command buffers 定义了接口, 并且提供了创建 command encoders, command buffers 入队执行,状态校验等方法。 MTLCommandBuffer
协议支持以下几种Encoder类型,它们被用于为 command buffer 编码不同的 GPU 任务:
MTLRenderCommandEncoder
,该类型的 Encoder 为一个 渲染 pass 编码3D图形渲染指令。MTLComputeCommandEncoder
,该类型的 Encoder 编码并行数据计算任务。MTLBlitCommandEncoder
,该类型的 Encoder 支持在 buffer 和 texture 之间进行简单的拷贝操作,以及类似 mipmap 生成操作。
在任意时刻, 只能有一个 command encoder 处于激活状态,它可以向一个 command buffer 提交指令。 前一个 command encoder 结束后,后一个 command encoder 才可以被创建并用于同一个 command buffer。 MTLParallelRenderCommandEncoder
协议是一个例外,它不遵守 “只有 command buffer 中只有一个 command encoder 处于激活状态” 这个规则。 详见 Encoding a Single Rendering Pass Using Multiple Threads.
一旦所有的编码工作结束,你可以提交(commit操作) MTLCommandBuffer
对象本身,这样标记该 command buffer 已经准备好被 GPU 执行。 MTLCommandQueue
协议会控制什么时候执行已经提交到 MTLCommandBuffer
对象上的指令,协调其他已经在 command queue中准备好的 MTLCommandBuffer
对象。
图 2-1 展示了 command queue, command buffer, 和 command encoder 这几种对象的紧密联系。每一列处于图顶部的元素 ( 缓存(buffer), 纹理(texture), 采样器(sampler), 深度/模板状态(depth and stencil state), 绘制管线状态(pipeline state) ) 表示和特定 command encoder 相关的资源和状态。
Figure 2-1 Metal Object Relationships
设备(MTLDevice)对象表示 GPU
一个 MTLDevice
对象代表一个可以执行指令的 GPU。 MTLDevice
协议包含创建新的 command queues 、从内存中申请缓存、创建纹理、查询设备功能等方法。调用 MTLCreateSystemDefaultDevice
方法,可以获取系统首选的设备对象。
Metal 中暂态和非暂态对象
在 Metal 中有些对象被设计成暂态,使用它们非常轻量。另外一些则要昂贵很多,它们拥有很长的生命周期,可能是整个 App 的生命周期。
Command buffer 和 command encoder 对象是暂态的,被设计来一次性使用。它们的创建和销毁成本都非常廉价,所以它们的创建方法都返回 autoreleased 对象。
相反,下面的这些对象是非暂态的,在性能敏感的代码里应该尽量重用它们,避免反复创建。
- Command queues
- Data buffers
- Textures
- Sampler states
- Libraries
- Compute states
- Render pipeline states
- Depth/stencil states
指令队列(Command Queue)
一个 command queue 管理着一个将要在 GPU上有序执行的 command buffers 队列。所有被塞进同一个队列的 command buffer 都会按照它们进入队列的次序执行。 通常, command queues 是线程安全的,并且允多个 command buffer 同时编码。
可以通过 MTLDevice
对象的 newCommandQueue
方法或者 newCommandQueueWithMaxCommandBufferCount:
来创建一个 command queue。通常情况下,command queue 对象应该具有长的生命周期,不要反复创建和销毁。
指令缓存(Command Buffer)
一个 command buffer 在被 GPU 执行之前,会存储多个被编码的指令。一个 command buffer 可以包含多种类型的编码指令,这依赖于被用来创建它的 Encoder 的种类和数量。在一个典型的 App 中,一帧画面的渲染操作可以被编码到一个 command buffer 中,就算这帧画面需要多个渲染 pass(rendering passes) ,多个计算处理着色程序( compute processing functions),或者多个位图操作( blit operations)才能完成。
Command buffers 被设计为暂态的一次性使用的对象,它不支持重用。一旦一个 command buff 被提交执行,接下去唯一有效的操作就是等待它被排入队列或是结束(同步调用和基于block的调用会在 Registering Handler Blocks for Command Buffer Execution 这一章节中详述)然后检查它被执行的状态。
Command buffers 还代表了 App 中独立可被追踪的任务单元,它还定义了 Metal的内存模型确立的一致性边界,详见 Resource Objects: Buffers and Textures.
创建 Command Buffer
调用 MTLCommandQueue
的 commandBuffer
方法创建一个 MTLCommandBuffer
对象。一个MTLCommandBuffer
只能提交给创建它的那个 MTLCommandQueue
对象。
由 commandBuffer
方法创建的 command buffer 对象将持有在它被执行时需要用到的数据。 某些情况下,如果不需要强引用这些相关数据,可以调用 MTLCommandQueue
的 commandBufferWithUnretainedReferences
方法。在保证和 command buffer 相关数据在其被执行时都有引用计数的情况下,又极端需要提示性能,才使用该方法。否则,一个 command buffer 相关的资源对象可能会因为没有引用计数而被释放,command buffer 的执行结果就不可预料了。
执行指令(Executing Commands)
MTLCommandBuffer
协议有如下的方法来设定 command buffers 在 command queue 中的执行顺序。一个 command buffer 只有在它被提交后, 才有可能被执行。一旦提交,它按照入队的顺序被执行。
enqueue
方法为一个 command buffer 在 command queue 中预定一个位置,但是不会提交这个 command buffer。当这个 command buffer 最后被提交,command queue 把它安排在之前做 enqueue 操作的 command buffer 之后执行。commit
方法使得 command buffer 尽可能快得被执行,但是还是得等到所有在 command queue 中早入队的 command buffer 被执行完后,才能执行。如果 command queue 中没有排在前面的,commit
方法隐式执行enqueue
操作。
关于在多线程中使用 enqueue
的例子,详见 Multiple Threads, Command Buffers, and Command Encoders.
为 command buffer 的执行注册处理程序块(Handler Blocks)
下列的 MTLCommandBuffer
方法可以监视指令的执行。使用了这些方法注册 handlers,那么在某个线程中,这些 handlers 会按照执行顺序被调用。这些 handlers 应该是可被迅速执行完成的,如果有开销大、造成阻塞的任务需要执行,那么应该将它们安排到其他线程执行。
addScheduledHandler:
该方法注册的程序块(a block of code)将在 command buffer 被排定(scheduled)好时调用。所谓排定好,是指当所有MTLCommandBuffer
对象或系统 API 提交的任务之间的依赖被满足,一个 command buffer 才被认为是排定好。一个 command buffer 对象可以为“排定好”注册多个 handlers 。waitUntilScheduled
该方法调用后开始等待,并在 command buffer 排定好且所有通过addScheduledHandler:
方法注册的 handlers 都执行完毕后返回。addCompletedHandler:
该方法注册的 handlers 将在 command buffer 被执行完毕后立即调用,一个 command buffer 对象可以为 “执行完” 注册多个 handlers。waitUntilCompleted
该方法调用后开始等等,一直到 command buffer 被执行完毕且所以通过addCompletedHandler:
方法注册的 handlers 都执行完毕后才返回。presentDrawable:
该方法是一个便捷方法,它用于当 command buffer 处于排定好时呈现一个可显示资源(一个CAMetalDrawable
对象) 的内容。更多关于presentDrawable:
方法,详见 Integration with Core Animation: CAMetalLayer.
监视 Command Buffer 执行状态
status
是 command buffer 的一个只读属性,一个在Command Buffer Status Codes
中列举的 MTLCommandBufferStatus
类型的枚举值,它反映了 command buffer 在其生命周期中处于哪个阶段。
如果 command buffer 成功执行,只读属性 error
的值为 nil
。如果执行失败, status
属性将会被设置为 MTLCommandBufferStatusError
,同时 error
属性将包含一个列举在 Command Buffer Error Codes
中的值,这个值指示了错误原因。
指令编码(Command Encoder)
command encoder 是一个一次性使用的暂态对象,它用来编码计算指令和绘制状态,然后它被推入到一个 command buffer 中,并最终在 GPU 上执行。 有很多 command encoder 对象方法可以把指令推入 command buffer。当一个 command encoder 处于激活状态,它有给它的 command buffer 附加指令的专有权。你可以调用 endEncoding
方法来停止编码。如果要写入更多的指令,就创建一个新的 encoder。
创建一个 Command Encoder 对象
因为一个 command encoder 推送指令到一个特定的 command buffer 中,所以你需要通过对应的 MTLCommandBuffer
对象,来创建 command encoder 对象。使用下面的 MTLCommandBuffer
方法来创建各种 Encoder 对象:
renderCommandEncoderWithDescriptor:
该方法创建一个MTLRenderCommandEncoder
类型的对象来实现图形渲染,图形渲染用到的 attachment 由MTLRenderPassDescriptor
类型的入参对象指定。computeCommandEncoder
该方法创建一个MTLComputeCommandEncoder
类型的对象来实现并行数据计算。blitCommandEncoder
该方法创建一个MTLBlitCommandEncoder
类型的对象来实现内存操作。parallelRenderCommandEncoderWithDescriptor:
该方法创建一个MTLParallelRenderCommandEncoder
类型的对象,它用于支持多个MTLRenderCommandEncoder
类型的对象同时在不同的线程中运行,但是依然把渲染结果写入由共享的MTLRenderPassDescriptor
对象指定的 attachment 中。
Render Command Encoder
图形渲染可以被描述为一系列的渲染 pass,一个 MTLRenderCommandEncoder
对象表示和一个渲染pass相关联的渲染状态和渲染指令。 一个 MTLRenderCommandEncoder
对象需要一个相关联的 MTLRenderPassDescriptor
对象(详见 Creating a Render Pass Descriptor) ,在这个 descriptor 对象中包含了颜色、深度、模板 attachments,这些 attachment 将被当做渲染命令的目标结果。 MTLRenderCommandEncoder
对象有如下方法:
- 设定图形资源,比如 buffer 和 texture 对象,这些对象包含顶点(vertex),片段(fragment)和纹理数据(texture image data)。
- 设定
MTLRenderPipelineState
对象,包含编译后的渲染状态,包括顶点和片段着色器。 - 设置固定功能状态,包括视口,三角形填充模式,裁剪矩形,深度和模板测试等等。
- 绘制 3D 图元。
更多关于 MTLRenderCommandEncoder
协议的知识,详见 Graphics Rendering: Render Command Encoder.
Compute Command Encoder
对于并行数据计算, MTLComputeCommandEncoder
协议提供了方法来编码计算指令到 command buffer,设定计算程序和参数(比如纹理,缓存,取样器状态)和调度计算程序的执行。 使用 MTLCommandBuffer
的 computeCommandEncoder
方法可以创建一个并行计算用的 Encoder。更多关于 MTLComputeCommandEncoder
的方法和属性,详见 Data-Parallel Compute Processing: Compute Command Encoder.
Blit Command Encoder
MTLBlitCommandEncoder
协议提供了方法来实现缓存 (MTLBuffer
) 和纹理 (MTLTexture
)之间进行拷贝。 MTLBlitCommandEncoder
协议还提供了用一种颜色填充纹理的方法,以及创建 mipmaps 的方法。使用 MTLCommandBuffer
的 blitCommandEncoder
方法,来创建一个 bait command encoder 对象。更多关于MTLBlitCommandEncoder
的方法和属性,详见 Buffer and Texture Operations: Blit Command Encoder.
多线程, Command Buffers 和 Command Encoders
很多的 App 只用一个线程来编码渲染指令到一个 command buffer 来绘制一帧画面。在每帧的末尾,提交 command buffer,如此可以安排和开始指令的执行。
如果你希望并行对 command buffer 执行指令编码,那么可以同时创建多个 command buffer,在单独的线程中为每个 command buffer 执行指令编码(使用多线程, 每个线程对应一个)。如果你事先知道 command buffer 应该以什么样的顺序执行,那么 MTLCommandBuffer
的 enqueue
方法可以在 command buffer 中声明执行顺序,而不必等待执行编码和提交操作。否则,只有等到 command buffer 被提交后,这时在 command queue 中它就被指定了一个位置,位于所有先提交的 command buffer 后面。
任一时刻只有一个 GPU 线程可以访问 command buffer。多线程的应用可以为每个 command buffer 准备一个线程,如此实现并行创建多个 command buffer。
图 2-2 展示了一个三线程例子。每个线程都操作各自的一个 command buffer。每个线程中,任一时刻只有一个 Encoder 在访问它对应的 command buffer。图 2-2 也展示了每个 command buffer 可以接受来自不同 command encoders 的指令。当你完成编码,调用 command encoder的 endEncoding
方法,然后一个新的 Encoder 才可以开始为这个 command buffer 编码指令。
图 2-2 多线程 Metal Command Buffers
一个 MTLParallelRenderCommandEncoder
对象可以允许一个渲染 pass 被打散成多个子 Encoder 并分配到多个线程进行操作。更多关于MTLParallelRenderCommandEncoder
的,详见 Encoding a Single Rendering Pass Using Multiple Threads.