Metal 框架之资源存储模式

992 阅读8分钟

「这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战

概述

Metal 中使用 MTLStorageMode 来指定资源的内存位置和访问权限。

MTLStorageMode 是个枚举类型,定义如下:

public enum MTLStorageMode : UInt {

    case shared = 0

    case managed = 1 //只在 macOS 中可用

    case private = 2

    case memoryless = 3

}

  • case shared

该资源存储在系统内存中,可供 CPU 和 GPU 访问。

shared.png

这是 MTLBuffer 对象的默认存储模式。在 iOS 和 tvOS 中,这也是 MTLTexture 对象的默认存储模式。在 macOS 中,共享存储模式不适用于 MTLTexture 对象。

当 CPU 或 GPU 更改资源的内容时,需要同步访问其他参与者的纹理。

如果使用 CPU 更改资源的内容,则必须在提交访问该资源的命令缓冲区之前完成这些更改。

如果在命令缓冲区中使用编码命令来更改资源的内容,则在 CPU 上运行的代码在命令缓冲区完成执行之前不得读取资源的内容(即 MTLCommandBuffer 对象的 status 属性为 MTLCommandBufferStatus.completed)。

  • case manager

这一存储模式只存在 macOS 中, 这是 MTLTexture 对象的默认存储模式,iOS 和 tvOS 不存在此存储模式。

CPU 和 GPU 可以维护单独的资源副本,并且任何更改都必须显式同步。

manage.png

在统一内存模型中,该模式下的资源驻留在 CPU 和 GPU 均可访问的系统内存中。

在离散内存模型中,该模式下的资源作为一对同步的内存分配存在。一份资源副本驻留在只有 CPU 才能访问的系统内存中;另一份副本驻留在只能由 GPU 访问的显存中。 Metal 通过创建一个 MTLResource 对象来管理两个副本。

在这两种内存模型中,Metal 优化了 CPU 和 GPU 对托管资源的访问。但是,在 CPU 或 GPU 修改其内容后需要显式同步托管资源。

如果使用 CPU 更改资源的内容,则必须使用 MTLBuffer 或 MTLTexture 协议提供的一种或多种方法将更改复制到 GPU。

如果使用 GPU 更改资源的内容,则必须对 blit 通道进行编码以将更改复制到 CPU。具体是请参阅 MTLBlitCommandEncoder 协议。

  • case 'private'

该资源只能由 GPU 访问。CPU 和 GPU 之间的资源一致性不是必需的,因为 CPU 无法访问资源的内容。

在统一内存模型中,此资源驻留在系统内存中。在离散内存模型中,它驻留在显存中。在两种内存模型中,Metal 都优化了 GPU 对私有资源的访问,共享或托管资源上是不允许优化的。

mac_private.png

在离散内存模型中,Metal 总是尝试将私有资源存储在显内存中。但是,在某些内存限制下,Metal 可能会将私有资源存到系统内存中,再次使用这些私有资源时,Metal 会在使用它之前尝试将其复制回显存。

  • case memoryless

资源的内容只能由 GPU 访问,这部分资源被称为 tile memory,并且仅在渲染过程中临时存在。Tile memory 比系统内存具有更高的带宽、更低的延迟和更低的功耗。

memoryless.png

memoryless 存储模式在 Apple 系列 GPU 上可用。

Memoryless 资源只能用作渲染通道中的临时渲染目标(实际上,配置的 MTLTexture 对象与 MTLRenderPassAttachmentDescriptor 对象一起使用)。不能在渲染通道开始时加载纹理的内容 (MTLLoadAction.load),也不能在渲染通道结束时存储其内容 (MTLStoreAction.store)。

当渲染目标的内容仅在渲染过程中被需要时才使用 memoryless 资源。例如,大多数渲染通道不会将深度附件和多重采样附件存储到内存中,将这些附件创建为 memoryless 资源,可以显着减少内存使用量。

在支持 tile 渲染的 Metal 设备上,可以更灵活地使用 imageblocks 来管理短暂的渲染数据。具体请参阅 Metal Shading 语言规范。

设置资源存储模式

对缓冲区或纹理指定合理的存储模式,可以达到快速内存访问和驱动级别的性能优化。

设置缓冲区的存储模式

使用 makeBuffer(length:options:) 方法创建一个新的 MTLBuffer,并在方法的 options 参数中设置其存储模式。

let bufferOptions = MTLResourceOptions.storageModePrivate

let buffer = device.makeBuffer(length: 256,

                               options: bufferOptions)

注释:
MTLResourceOptions 中的存储模式选项等效于 MTLStorageMode 中的存储模式值。
创建新缓冲区时,可以组合多个资源选项,但只能设置一种存储模式。

设置纹理的存储模式

创建一个新的 MTLTextureDescriptor 并在描述符的 storageMode 属性中设置其存储模式。然后使用 makeTexture(descriptor:) 方法创建一个新的 MTLTexture。


let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm,

                                                                 width: 256,

                                                                 height: 256,

                                                                 mipmapped: true)

textureDescriptor.storageMode = .private

let texture = device.makeTexture(descriptor: textureDescriptor)

在 iOS 和 tvOS 中选择资源存储模式

所有 iOS 和 tvOS 设备都有一个统一的内存模型。

为缓冲区或纹理选择一种资源存储模式

选择哪个模式,取决于资源的访问需求:

  • 由 CPU 填充和更新

如果资源需要 CPU 访问,请选择 MTLStorageMode.shared 模式。

  • 仅由 GPU 访问

如果通过计算、渲染或 blit 通道使用 GPU 填充资源,请选择 MTLStorageMode.private 模式。这种情况对于渲染目标、中间资源或纹理流很常见。

  • 由 CPU 填充一次并由 GPU 频繁访问

使用 CPU 以 MTLStorageMode.shared 模式创建资源并填充其内容。然后,使用 GPU 将资源的内容复制到另一个具有 MTLStorageMode.private 模式的资源中。

  • 仅由 GPU 访问,其内容是临时的(仅限纹理)

如果纹理是由 GPU 临时填充和访问的 memoryless 渲染目标,请选择 MTLStorageMode.memoryless 模式。Memoryless 渲染目标是仅存在于 tile memory 中且不受系统内存支持的渲染目标。比如深度或模板纹理,它只在渲染过程中使用,在 GPU 执行之前或之后不需要。

创建 memoryless 渲染目标

要创建 memoryless 渲染目标,请将 MTLTextureDescriptor 的 storageMode 属性设置为 MTLStorageMode.memoryless 并使用此描述符创建新的 MTLTexture。然后将此新纹理设置为 MTLRenderPassAttachmentDescriptor 的 texture 属性。

let memorylessDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r16Float,
                                               width: 256,
                                              height: 256,
                                           mipmapped: true)

memorylessDescriptor.storageMode = .memoryless

let memorylessTexture = device.makeTexture(descriptor: memorylessDescriptor)


let renderPassDescriptor = MTLRenderPassDescriptor()

renderPassDescriptor.depthAttachment.texture = memorylessTexture

注释:
只能使用 MTLStorageMode.memoryless 模式创建纹理,而不能创建缓冲区,
即不能将缓冲区用作 memoryless 渲染目标。

在 macOS 中选择资源存储模式

macOS 设备可以有多个 GPU,每个 GPU 具有统一或离散的内存模型。在统一的内存模型中,CPU 和 GPU 共享系统内存,它们对该内存的访问取决于资源选择的存储模式。在离散内存模型中,系统内存与显存是分开的,CPU 和 GPU 都可以访问系统内存,但只有 GPU 可以访问显存。

尽管统一内存模型和离散内存模型之间存在差异,但无需编写差异性代码,Metal 的资源存储模式 API 对两者都适用。

设置缓冲区的资源存储模式

  • 仅由 GPU 访问

如果通过计算、渲染或 blit 通道使用 GPU 填充缓冲区,请选择 MTLStorageMode.private 模式。这种情况对于通道间的中间缓冲区很常见。

  • 由 CPU 填充一次并由 GPU 频繁访问

选择 MTLStorageMode.managed 模式。首先,用 CPU 填充缓冲区的数据,然后同步缓冲区。最后,使用 GPU 访问缓冲区的数据。

  • 更改频繁,相对较小,并且由 CPU 和 GPU 访问

选择 MTLStorageMode.shared 模式。

  • 变化频繁,比较大,CPU和GPU都可以访问

选择 MTLStorageMode.managed 模式。修改缓冲区内容后,始终与 CPU 或 GPU 同步缓冲区。

有关更多信息,请参阅同步托管资源。

注释:
在 macOS 中,托管缓冲区和私有缓冲区之间的 GPU 性能没有区别。
因此,仅使用托管缓冲区将数据从共享缓冲区传输到私有缓冲区并没有性能优势。

设置纹理的资源存储模式

只能使用 MTLStorageMode.managed 或 MTLStorageMode.private 模式创建纹理,但不能使用 MTLStorageMode.shared。

  • 仅由 GPU 访问

如果通过计算、渲染或 blit 通道使用 GPU 填充纹理,请选择 MTLStorageMode.private 模式。这种情况对于渲染目标和可绘制对象很常见。

  • 由 CPU 填充一次并由 GPU 频繁访问

使用 CPU 以 MTLStorageMode.shared 模式创建缓冲区,将纹理数据填充到缓冲区中。然后,使用 GPU 将缓冲区的内容复制到具有 MTLStorageMode.private 模式的纹理中。

  • CPU 和 GPU 经常访问

选择 MTLStorageMode.managed 模式。在使用 CPU 或 GPU 修改其内容后始终同步纹理。

总结

本文介绍了资源存模式,对四种模式做了详细的解说并做了详细的对比,最后列举了如何在 iOS 及 macOS 下选择相应的存储模式。