Metal入门资料017,Metal入门资料005

作者:澳门新葡京平台游戏

对Metal技术感兴趣的同学,可以关注我的专题:Metal专辑也可以关注我个人的简书账号:张芳涛所有的代码存储的Github地址是:Metal

原文链接
此文章为了练习我那蹩脚的英文

本系列文章是对 http://metalkit.org 上面MetalKit内容的全面翻译和学习.

MetalKit系统文章目录

对Metal技术感兴趣的同学,可以关注我的专题:Metal专辑也可以关注我个人的简书账号:张芳涛所有的代码存储的Github地址是:Metal

上一篇文章我们查看了Metal Shading Language基础知识。 在我们研究更高级的主题之前,我想现在是重温我们迄今为止学到的东西的一个好时机,特别是关于我承认的graphics pipeline

Note: Metal 程序不能在模拟器中运行; 需要一个 Apple A7 设备.。


本文是摘抄的苹果官方文档,苹果的文档里面有Metal Best Practices Guide这个章节。

让我们更详细地看看graphics pipeline,我将从这里开始一段历史。 这一切都是在十年前开始的,当时引入了shaders,作为程序员能够影响到目前为止存在的fixed pipeline的一种方式。 同时,还推出了针对GPU的floating point支持,促进了GPGPU(图形处理单元上的通用计算)的诞生。 结果,新的programmable pipeline发生了重大变化:

Getting Started

配置Metal需要7个步骤:

  1. 创建 MTLDevice
  2. 创建 CAMetalLayer
  3. 创建顶点缓冲区
  4. 创建顶点着色器
  5. 创建一个片段着色器
  6. 创建一个渲染管道
  7. 创建一个命令队列

上一次我们学习了Metal Shading Language Metal着色语言基础.在我们学习更多高深的课题前,我想现在是时候重新回顾一下我们学到的东西,尤其是关于图形管线的,我也承认这一点我讲得太快了(感谢匿名读者的建议和反馈!)

Metal提供对GPU的最低开销访问,使您能够在iOSmacOStvOS上最大化应用程序的图形和计算潜力。每毫秒和每一点都是Metal应用程序和用户体验的组成部分 ,所以 我们有责任通过遵循本指南中描述的最佳实践来确保Metal应用程序尽可能高效地运行。除非另有说明,否则这些最佳实践适用于支持Metal的所有平台。

图片 1

创建MTLDevice

首先必获取一个MTLDevice的实例。
可以把MTLDevice想象成连接到GPU的桥梁。创建的所有Metal对象都使用这个MTLDevice
在controller中引用头文件:
import Metal

Note: 如果出现编译错误,确定你工程中设置的target指向支持Metal的iOS设备。目前Metal还不支持模拟器。

在ViewController中创建一个成员变量:
var device: MTLDevice!
由于在viewDidLoad()方法中初始化这个变量,所以需要生命为可选类型。为了方便,使用了隐式解包可选类型。

最后在viewDidLoad()中添加初始化方法:
device = MTLCreateSystemDefaultDevice()
这个方法返回一个MTLDevice的实例。

让我们更仔细地了解一下graphics pipeline图形管线,从一小段历史开始.大约十年前,当fixed pipeline固定管线仍然存在时,shaders被引入进来作为一种程序员影响固定管线的工具.在当时,浮点也被引入GPUs中并出现了GPGPU(能用计算图形处理单元).于是,新的programmable pipeline可编程管线就被改变了:

一个高性能的Metal应用程序需要具备以下特点:

正如你所看到的,新的pipeline现在有两个shader阶段,这就是我们可以编写我们自己的定制代码的地方,然后GPU就会运行。 图形程序的第一部分始终在CPU上运行,通常称为host code。 这里是大部分资源分配发生的地方,以及将数据传输到GPU和从中传输。 然而,该程序最重要的部分运行在GPU上。 这两个shaders放入一个带有.metal扩展名的单独文件中(在其他GPGPU框架中,例如OpenCL,它被命名为kernel code[内核代码])。

创建 CAMetalLayer

在iOS中,任何可以看到的东西都基于一个CALayerCALayer的子类都有自己的作用。

如果想使用Metal在屏幕上绘制一些东西,必须使用CAMetalLayer

声明一个metallayer :
var metalLayer: CAMetalLayer!
然后添加到viewDidLoad()方法中:

metalLayer = CAMetalLayer() //1
metalLayer.device = device //2
metalLayer.pixelFormat = .bgra8Unorm //3
metalLayer.framebufferOnly = true //4
metalLayer.frame = view.layer.frame //5
view.layer.addSublayer(metalLayer) //6

1 创建一个CAMetalLayer

  1. 必须指定一个MTLDevice,将之前创建的device赋给它
  2. 指定像素格式为bgra8Unorm,“为Blue, Green, Red和Alpha提供8字节,按照这个顺序 — 标准值在0和1之间”,这是CAMetalLayer仅有的两种格式其中之一。
  3. 由于性能原因,苹果建议将framebufferOnly设置为true,除非你需要在这个layer做纹理采样,或者在可绘制layer上开启计算内核。(Apple encourages you to set framebufferOnly to true for performance reasons unless you need to sample from the textures generated for this layer, or if you need to enable compute kernels on the layer drawable texture)大多数情况,不用这样做。
  4. 相对于父视图设置layer的位置和大小
  5. 最终,将layer添加到视图中

图片 2

  • CPU开销。Metal旨在减少或消除许多CPU端性能瓶颈。只有按照建议使用Metal API,应用才能从此受益。

  • 最佳GPU性能。Metal允许创建并向GPU提交命令。要优化GPU性能,您的应用应优化这些命令的配置和组织。

  • 处理器持续和并行工作能力。Metal旨在最大化CPUGPU并行性。应用应该让这些处理器起作用并同时工作。

  • 有效的资源管理。Metal为您的资源对象提供简单而强大的接口。应用应该有效地管理这些资源,以减少内存消耗并提高访问速度。

pipeline以输入以vertices的形式发送给GPUCPU阶段开始。 他们经历转化和每个顶点的照明。 此时vertex shader可用于在rasterization之前操作顶点。 之后,顶点进行clippingrasterization,产生碎片。 fragment shader然后可以在像素值输出到 framebuffer以供显示之前在每个片段上运行。

创建顶点缓冲区

Metal中的一切都是三角形,即便是再复杂的3D图形,都可以分解成一系列的三角形。

Metal中,默认的坐标系为归一化坐标系,看起来是一个中心为(0,0,0.5)的2x2x1的立方体。

如果假设Z=0,那么(-1,-1,0)为左下角,(0,0,0)为中心,(1,1,0)为右上角。例子中的三角形的三个点:

图片 3

1.png

我们要为这个三角形创建一个缓冲区,接着添加一个属性:

let vertexData: [Float] = [
0.0,1.0,0.0,
-1.0,-1.0,0.0,
1.0,-1.0,0.0]

这将在CPU中创建一个浮点型数组,接下来要使用MTLBuffer来将其发送到GPU上。
另一个属性:
var vertexBuffer: MTLBuffer!
然后再viewDidLoad()中添加如下方法:

let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0]) // 1
vertexBuffer = device.makeBuffer(bytes:vertexData, length: dataSize, options:[]) //2
  1. 通过数组中第一个元素的大小乘以数组元素的个数来获取vertexData在内存中所占字节数。
  2. 使用makeBuffer(bytes:length:options:)在GPU上创建一个缓冲区。将空座位默认配置。

part4_1.png

最佳实践:尽早创建持久对象并经常重用它们。

Metal框架提供了在应用程序的整个生命周期内管理持久对象的协议。这些对象的创建成本很高,但通常会初始化一次并经常重复使用。不需要在每个渲染或计算循环的开头创建这些对象。

现在让我们看看Metal自己的管道。 我们将回顾第2部分的源代码,我们会提到行号来举例说明我们所触及的概念。 构建Metal应用程序分两个阶段完成。 第一个是Initialization阶段:

创建一个定点着色器

之前创建的顶点将成为接下来要写的vertex shader的一个小程序的输入。顶点着色器就像是在GPU上运行的用Metal Shading Language语言(类似C 语言)编写的小程序。
每个定点都会调用一次定点着色器,其作用则是获取定点的信息,比如位置、颜色、坐标系并且返回一个隐式修改位置和可能的其他数据。
为了简便,顶点着色器将会返回与传入时一样的位置:

图片 4

2.png

接下来创建一个名为Shaders.metal的文件。

Note: 在Metal中可以在一个文件中创建多个顶点着色器。 也可为多个着色器创建不同的文件,因为Metal会将工程中所有的Metail文件都加载一次。

Shaders.metal文件中添加如下代码:

vertex float4 basic_vertex(     //1
    const device packed_float3* vertex_array[[buffer(0)]], //2
    unsigned int vid [ [ vertex_id ] ])  {      //3
    return float4(vertex_array[vid], 1.0);          //4
)
  1. 所有的定点着色器命名必须以vertex开头,函数必须返回最终的顶点位置。然后外部将通过名字来访问着色器。
  2. 第一个参数为指向packed_float3数组的指针—即每个定点的位置。[[ … ]]语法用来声明指定的附加信息,比如资源位置,着色器输入和内置变量。这里使用[[buffer(0)]]来指示这个参数将会由Metal代码发送到定点着色器中第一个数据缓冲区填充。
  3. 定点着色器将持有一个vertex_id属性,这意味着它将会被填充在顶点数组中指定的顶点的索引。
  4. 在这里返回基于顶点id的顶点数组内的位置,还将向量转化为float4,最终之为1.0。

就像看到的那样,新的管线目前拥有两个shader着色器,我们可以在其中编写自定义代码然后在GPU上运行.图形程序的第一部分总是运行在CPU上,被称为host code主机代码.这就是绝大部分资源分配发生的地方,也是和GPU之间传输数据的地方.程序最重要的地方,却是运行在GPU上.两个shaders包含在一个后缀为.metal的单独文件中(在其他GPGPU框架比如OpenCL中它被命名为kernel code内核代码).

首先初始化的设备和命令队列

MTLCreateSystemDefaultDevice在应用程序启动时 调用该函数以获取默认系统设备。接下来,调用newCommandQueue或者 newCommandQueueWithMaxCommandBufferCount:方法创建一个命令队列,用于在该设备上执行GPU指令。

所有应用程序应该只MTLDevice为每个GPU 创建一个对象,并将其重用于该GPU上的所有Metal工作。大多数应用程序应该MTLCommandQueue每个GPU 只创建一个对象,但如果每个命令队列代表不同的Metal工作(例如,非实时计算处理和实时图形渲染),您可能需要更多。

  • 一些macOS设备具有多个GPU。如果需要使用多个GPU,请调用该MTLCopyAllDevices函数以获取可用设备的数组。为您使用的每个GPU创建并保留至少一个命令队列。

图片 5

创建片段着色器

创建顶点着色器之后,为屏幕中的每一个片段(像素)调用另一个着色器:片段着色器。片段着色器通过内插来自顶点着色器的输出值来获取其输入值。比如三角形底部的两个顶点之间的片段:

图片 6

3.png

该片段的输入值将是底部两个顶点的输出值的50/50的混合。
片段着色器的工作是返回每个片段的最终颜色。为了简便,每个片段都将设置为白色。添加如下代码在Shaders.metal文件中:

fragment half4 basic_fragment() { //1
    return half4(1.0)   //2
}
  1. 所有的片段着色器命名必须以fragment开头。方法必须返回片段的最终颜色。返回类型为half4,需要注意half4float4更加内存高效,因为只占用很少的GPU内存。
  2. 这里返回(1,1,1,1)白色

管线开始于CPU站点处,这里的输入以vertices形式被送入到GPU中.它们经过了变换和逐顶点光照计算.此时vertex shader顶点着色器能够在rasterization光栅化之前,对顶点进行操作改变.在这之后,顶点经过clipping裁剪rasterization光栅化并得到fragments片段.接着fragment shader片段着色器会被运行处理每个片段,之后每个像素值被输出到framebuffer帧缓冲器以供显示.

在构建时编译函数并构建Library

有关在构建时编译函数和构建库的概述,请参阅Libraries最佳实践。

在运行时,使用MTLLibrary和MTLFunction对象访问图形库和计算函数。避免在运行时构建库或在渲染或计算循环期间获取函数。

如果需要配置多个渲染或计算管道,请MTLFunction尽可能重用对象。您可以释放MTLLibrary和MTLFunction建设的所有对象后,渲染并依赖于它们的计算pipelines

第一步是获取deviceMetalView.swift中的第19行)。设备是与GPU驱动程序和硬件的直接连接;这是我们需要在Metal中创建所有其他对象的源代码。第二个初始化步骤是创建一个command queue,这是我们向GPU提交工作的渠道。第三个初始化步骤是创建缓冲区,纹理和其他资源。newBufferWithBytes函数将分配一个新的共享内存块,将提供的指针复制到其中,并将句柄返回给该缓冲区。第四个初始化步骤是创建render pipeline,这是一连串的步骤,从一端取顶点数据开始,另一端产生光栅化图像。流水线包含两个元素:保存shader信息和像素格式的描述符,以及从描述符构建并包含编译着色器的state。第五个初始化步骤是创建一个视图。对我们而言,从MTKView继承更容易,而不是创建新的CAMetalLayer并将其添加为子视图。

创建渲染管道

现在已经创建了片段着色器,需要将他们结合在一起生成一个render pipeline
Metal非常炫酷于着色器都使用了预编译,渲染管道配置在你第一次设置之后就会编译。这使得一切都很高效。
首先在viewController.swift中添加属性:
var pipelineState: MTLRenderPipelineState!
之后在viewDidLoad()中添加如下方法:

// 1
let defaultLibrary = device.newDefaultLibrary()!
let fragmentProgram 
        = defaultLibrary.makeFunction(name: "basic_fragment")
let vertexProgram 
        = defaultLibrary.makeFunction(name: "basic_vertex")

// 2
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertexProgram
pipelineStateDescriptor.fragmentFunction = fragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm

// 3
pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
  1. 可以通过调用device.newDefaultLibrary()!获取MTLLibrary对象来访问项目中任何的预编译着色器。然后可以使用名字来查找着色器
  2. 配置渲染管道。这包含了你想使用的着色器和颜色附件的像素格式—即正在渲染的输出缓冲区,CAMetaLayer本身。
  3. 最后,将管道配置编译成管道状态来高效使用。

现在让我们看看Metal自己的管线.我们将回到 第2部分的代码中part 2 source code 并且将用行号来标明我们的提及的概念.创建Metal应用共需两个阶段.第一个是初始化阶段:

建立一次Pipelines并经常重复使用

构建可编程管道涉及对GPU状态的评估工作非常消耗性能。应该只构建MTLRenderPipelineState和MTLComputePipelineState对象一次,然后为创建的每个新渲染或计算命令编码器重用。不要为新的命令编码器构建新的管道。有关异步构建多个pipelines的概述,请参阅pipelines最佳实践。

  • 注意 : 除了渲染和计算管线,则可以选择创建MTLDepthStencilState和MTLSamplerState封装深度,模板,和采样器状态的对象。这些对象较轻便,但也应仅创建一次并经常重复使用。

接下来,我们来看看构建Metal应用程序的第二阶段,即Drawing阶段:

创建命令队列

最后一步为创建MTLCommandQueue。把这想象成一个命令列表来告诉GPU怎样执行。添加一个新的属性:
var commandQueue: MTLCommandQueue!
接着在viewDidLoad()中添加:
commandQueue = device.makeCommandQueue()
以上,一次性配置代码结束!

图片 7

预先分配资源存储

资源数据可以是静态的或动态的,并可在应用程序的整个生命周期的各个阶段进行访问。 但是,应尽早创建为此数据分配内存的MTLBuffer和MTLTexture对象。创建这些对象后,资源属性和存储分配是不可变的,但数据本身不是; 可以在必要时更新数据。

尽可能 重用MTLBuffer和MTLTexture对象,特别是对于静态数据。避免在渲染或计算循环期间创建新资源,即使对于动态数据也是如此。有关缓冲区和纹理的更多信息,请参阅资源管理和三重缓冲最佳实践。

最佳实践:设置适当的资源存储模式和纹理使用选项。

必须正确配置您的Metal资源,以利用快速内存​​访问和驱动程序性能优化。资源存储模式允许定义MTLBuffer和MTLTexture对象的存储位置和访问权限。纹理使用选项允许显式声明打算如何使用MTLTexture对象。

图片 8

绘制三角形

绘制三角形需要5步:

  1. 创建显示链接
  2. 创建渲染描述符
  3. 创建命令缓冲区
  4. 创建渲染命令编码器
  5. 提交命令缓冲区

part4_2.png

熟悉设备内存模型

设备内存型号因操作系统而异。iOStvOS设备支持统一的内存模型,其中CPUGPU共享系统内存。macOS设备支持具有CPU可访问系统内存和GPU可访问视频内存的独立内存模型。

  • 一些macOS设备具有集成的GPU。在这些设备中,驱动程序优化底层架构以支持离散内存模型。macOS Metal应该始终以离散内存模型为目标。
  • 所有iOStvOS设备都集成了GPU

第一步是获取command buffer。 所有进入GPU的工作将被排入此缓冲区。 我们需要前一阶段的command queue来创建一个command buffer。 第二步是设置render pass。 渲染通道描述符告诉Metal渲染图像时要执行的操作。 配置它需要我们指定我们渲染的颜色纹理(currentDrawable纹理)。 在绘制任何几何体之前,我们还需要选择屏幕将被清除的颜色。 第三步是实际drawing。 我们指定存储顶点的缓冲区,然后指定我们需要绘制的基元。 第四步也是最后一步是commit the command bufferGPU。 调用commit时,command buffer会被编码,发送到命令队列的末尾,并在GPU运行时执行。

创建显示链接

现在需要一个方法,每当屏幕刷新时重新绘制。在iOS中,可以使用CADisplayLink类。添加一个属性:
var timer: CADisplayLink!
viewDidLoad()中初始化:

timer = CADisplayLink(target: self, #selector(ViewController.gameloop))
timer.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)

没当屏幕刷新时,都会调用gameloop()方法。
最后添加以下方法:

func render() {
    // TODO
}

func gameloop() {
    autoreleasepool {
        self.render()
    }
}

第一步是拿到device设备(MetalView.swift中的19行).设备是和GPU驱动及硬件的直接连接;也是我们在Metal中创建其他所有对象的源头.第二个初始化步骤是创建一个command queue命令队列(40行),它是我们提交工作到GPU的通道.第三个初始化步骤是创建缓冲器,纹理和其它资源(20-27行).newBufferWithBytes函数将会分配一块新的共享内存,复制提供的指针到里面,然后返回一个该缓冲器的句柄.第四个初始化步骤是创建render pipeline渲染管线(28-37行),它是一个链式步骤,一端获取数据,另一端产生一个栅格化的图像.管线由两个元素组成:descriptor它持有shader信息和像素格式,state则是从descriptor中创建并包含了编译过了shaders.第五个初始化步骤是创建一个view.对我们来说,创建一个继承于MTKView(11行)的对象,要比创建一个新的CAMetalLayer并将其添加为子视图更为简单.

选择适当的资源存储模式

iOStvOS中,Shared模式定义了CPUGPU Private都可访问的系统内存,而模式定义了只能由GPU访问的系统内存。

该Shared模式通常是iOStvOS资源的正确选择。Private仅当CPU从不访问资源时才选择模式。

  • iOStvOS中,memoryless存储模式用于无记忆纹理。此存储模式只能用于存储在片上磁贴存储器中的临时渲染目标。有关详细信息,请参阅Memoryless TexturesMetal编程指南。

图片 9图3-1 iOS和tvOS中的资源存储模式

创建渲染描述符

第二步则是创建MTLRenderPassDescriptor,这个对象用来配置哪个纹理正在渲染,清晰的颜色是什么,以及一些其他的配置。
render()方法中添加如下方法:

guard let drawable = metalLayer?.nextDrawable() else { return }
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 104.0/255.0, blue: 5.0/255.0, alpha: 1.0)

首先用之前创建的layer来调用nextDrawable()方法,它会返回需要绘制的纹理,使其显示在屏幕上。
接着配置渲染描述符,使用将loadAction设置为清除,这意味着“在绘制之前,将纹理设置为透明色”,然后将清除颜色设置为绿色。

接下来,让我们看看创建Metal应用的第二个阶段,Drawing绘制阶段:

选择适当的资源存储模式

在macOS中,Shared模式定义了CPUGPU都可访问的系统内存,而Private模式定义了只能由GPU访问的视频内存。

此外,macOS实现了Managed为资源定义同步内存对的模式,其中一个副本位于系统内存中,另一个副本位于视频内存中。管理资源受益于对每个资源副本的快速CPUGPU访问,同步这些副本所需的API调用最少。

图片 10图3-2 macOS中的资源存储模式

  • macOS中,Shared模式仅适用于缓冲区,而不适用于纹理。缓冲区数据通常是线性的,从而产生简单的GPU访问模式。纹理更复杂,其数据通常是平铺或调配的,从而导致更复杂的GPU访问模式。

使用以下准则确定特定缓冲区的适当存储模式。

  • 如果GPU专门访问缓冲区,请选择Private模式。这是GPU生成的数据的常见情况,例如每个补丁镶嵌的因子。

  • 如果CPU专门访问缓冲区,请选择Shared模式。这是一种罕见的情况,通常是blit操作中的中间步骤。

  • 如果CPUGPU都访问缓冲区,就像大多数顶点数据一样,请考虑以下几点并参考表3-1:

    • 对于频繁更改的小型数据,请选择Shared模式。将数据复制到视频存储器的开销可能比直接访问GPU系统存储器的开销更大。

    • 对于不经常更改的中型数据,请选择Managed模式。始终在修改托管缓冲区的内容后调用适当的同步方法。 执行CPU写入后,调用didModifyRange:方法以通知Metal有关已修改的特定数据范围; 这允许Metal仅更新视频内存副本中的特定范围。在编写GPU写入之后,编码包括对synchronizeResource:方法的调用的blit操作; 这允许Metal在相关命令缓冲区完成执行后更新系统内存副本。

    • 对于永不更改的大型数据,请选择Private模式。使用Shared模式初始化并填充源缓冲区,然后将其数据blit到具有Private模式的目标缓冲区。这是一次性成本的最佳操作。

  • 表3-1为CPUGPU访问的缓冲区数据选择存储模式

Data size Resource dirtiness Update frequency Storage mode
Small Full Every frame Shared
Medium Partial Every n frames Managed
Large N/A Once Private(来自共享源缓冲区的blit之后)

创建命令缓冲区

下一步则是创建命令缓冲区。把这想象成渲染命令的命令列表。在提交命令缓冲区前,不会执行任何操作。
render()方法中添加如下代码:
let commandBuffer = commandQueue.makeCommandBuffer()
一个命令缓冲区可以包含一个或多个渲染命令。

图片 11

纹理存储模式

macOS中,纹理的默认存储模式是Managed。使用以下准则确定特定纹理的适当存储模式。

  • 如果GPU专门访问纹理,请选择Private模式。这是GPU生成的数据的常见情况,例如可显示的渲染目标。

  • 如果CPU专门访问纹理,请选择Managed模式。这是一种罕见的情况,通常是blit操作中的中间步骤。

  • 如果纹理由CPU初始化一次并由GPU频繁访问,则使用Managed模式初始化源纹理,然后将其数据blit到具有Private模式的目标纹理。这是静态纹理的常见情况,例如漫反射贴图。

  • 如果CPUGPU频繁访问纹理,请选择Managed模式。这是动态纹理的常见情况,例如图像滤镜。始终在修改托管纹理的内容后调用适当的同步方法。

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

对GPU写入进行编码后,对包含对以下任一方法的调用的blit操作进行编码。这允许Metal在关联的命令缓冲区完成执行后更新系统内存副本。* synchronizeResource:* synchronizeTexture:slice:level:

Metal可以根据其预期用途优化给定纹理的GPU操作。如果您事先知道它们,请始终声明显式纹理使用选项。不要依赖Unknown选项; 虽然此选项为纹理提供了最大的灵活性,但却会产生显着的性能损失。如果驱动程序不知道您打算如何使用纹理,则无法执行任何优化。有关可用纹理使用选项的说明,请参阅MTLTextureUsage参考。

创建渲染命令编码器

创建渲染命令,需要使用一个叫做渲染命令编码器的帮助对象。在render()中添加如下代码:

let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, at: 0)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
renderEncoder.endEncoding()

在这里使用之前创建的管道和顶点缓冲区来创建一个命令编码器。
这里最核心的部分为
drawPrimitives(type:vertexStart:vertexCount:instanceCount:)
方法。这里告诉GPU基于顶点缓冲区来绘制一组三角形。每个三角形由三个顶点组成,从顶点缓冲区的第0个开始,这里总共有1个三角形。
最后调用endEncoding()方法

part4_3.png

三重缓冲

最佳实践:实现三重缓冲模型以更新动态缓冲区数据。

动态缓冲区数据是指存储在缓冲区中的频繁更新的数据。为避免每帧创建新缓冲区并最小化帧之间的处理器空闲时间,我们需要实现三重缓冲模型。

动态缓冲区数据通常由CPU写入并由GPU读取。如果这些操作同时发生,则会发生访问冲突; CPU必须在GPU可以读取之前完成数据写入,并且GPU必须在CPU覆盖之前读取该数据。如果动态缓冲区数据存储在单个缓冲区中,则当CPU停止运行或GPU缺乏时,这会导致处理器空闲时间延长。为使处理器并行工作,CPU应至少在GPU前一帧工作。此解决方案需要多个动态缓冲区数据实例,因此CPU可以在帧n 1读取数据时为帧写入数据n

您可以使用可重用缓冲区的FIFO队列管理多个动态缓冲区数据实例。但是,分配太多缓冲区会增加内存开销,并可能限制其他资源的内存分配。此外,如果CPU的工作距离GPU工作太远,分配太多缓冲区会增加帧延迟。

  • 避免每帧创建新的缓冲区。有关预先分配资源存储的概述,请参阅持久对象最佳实践。

动态缓冲区数据被编码并绑定到瞬态命令缓冲区。在提交执行后,将此命令缓冲区从CPU传输到GPU需要一定的时间。类似地,GPU需要一定的时间来通知CPU它已完成该命令缓冲区的执行。对于单个帧,此序列详述如下:

  • 1:CPU写入动态数据缓冲区并将命令编码到命令缓冲区中。
  • 2: CPU调度完成处理程序(addCompletedHandler:),提交命令缓冲区,并将命令缓冲区传输到GPU
  • 3: GPU执行命令缓冲区并从动态数据缓冲区读取。
  • 4:GPU完成其执行并调用命令缓冲区完成处理程序([MTLCommandBufferHandler](https://developer.apple.com/documentation/metal/mtlcommandbufferhandler))。

此序列可以与两个动态数据缓冲区并行化,但是如果任一处理器正在等待繁忙的动态数据缓冲区,则命令缓冲区事务可能导致CPU停止或GPU闲置。

在考虑处理器空闲时间,内存开销和帧延迟时,添加第三个动态数据缓冲区是理想的解决方案。图4-1显示了三重缓冲时间线,清单4-1显示了三重缓冲实现。

图片 12图4-1三重缓冲时间线

清单4-1三重缓冲实现:

static const NSUInteger kMaxInflightBuffers = 3;/* Additional constants */@implementation Renderer{dispatch_semaphore_t _frameBoundarySemaphore;NSUInteger _currentFrameIndex;NSArray <id <MTLBuffer>> _dynamicDataBuffers;/* Additional variables */}- configureMetal{// Create a semaphore that gets signaled at each frame boundary.// The GPU signals the semaphore once it completes a frame's work, allowing the CPU to work on a new frame_frameBoundarySemaphore = dispatch_semaphore_create(kMaxInflightBuffers);_currentFrameIndex = 0;/* Additional configuration */}- makeResources{// Create a FIFO queue of three dynamic data buffers// This ensures that the CPU and GPU are never accessing the same buffer simultaneouslyMTLResourceOptions bufferOptions = /* ... */;NSMutableArray *mutableDynamicDataBuffers = [NSMutableArray arrayWithCapacity:kMaxInflightBuffers];for(int i = 0; i < kMaxInflightBuffers; i  ){ // Create a new buffer with enough capacity to store one instance of the dynamic buffer data id <MTLBuffer> dynamicDataBuffer = [_device newBufferWithLength:sizeof(DynamicBufferData) options:bufferOptions]; [mutableDynamicDataBuffers addObject:dynamicDataBuffer];}_dynamicDataBuffers = [mutableDynamicDataBuffers copy];}- update{// Advance the current frame index, which determines the correct dynamic data buffer for the frame_currentFrameIndex = (_currentFrameIndex   1) % kMaxInflightBuffers;// Update the contents of the dynamic data bufferDynamicBufferData *dynamicBufferData = [_dynamicDataBuffers[_currentFrameIndex] contents];/* Perform updates */}- render{// Wait until the inflight command buffer has completed its workdispatch_semaphore_wait(_frameBoundarySemaphore, DISPATCH_TIME_FOREVER);// Update the per-frame dynamic buffer data[self update];// Create a command buffer and render command encoderid <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];id <MTLRenderCommandEncoder> renderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_renderPassDescriptor];// Set the dynamic data buffer for the frame[renderCommandEncoder setVertexBuffer:_dynamicDataBuffers[_currentFrameIndex] offset:0 atIndex:0];/* Additional encoding */[renderCommandEncoder endEncoding];// Schedule a drawable presentation to occur after the GPU completes its work[commandBuffer presentDrawable:view.currentDrawable];__weak dispatch_semaphore_t semaphore = _frameBoundarySemaphore;[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) { // GPU work is complete // Signal the semaphore to start the CPU work dispatch_semaphore_signal(semaphore);}];// CPU work is complete// Commit the command buffer and start the GPU work[commandBuffer commit];}@end

提交命令缓冲区

最后一步则是提交命令缓冲区。在render()方法中添加如下代码:

commandBuffer.present(drawable)
commandBuffer.commit()

第一行代码需要确保在绘制完成后立即显示新纹理。接着提交事务将任务发送到GPU上。
最终的效果:

图片 13

4.png

如果程序崩溃, 确保是在真机上运行,具有 A7 或者更好的芯片 ( iPhone 5S, iPhone 6, iPhone 6 Plus, iPad Air, or iPad mini (2nd generation) )。

第一步是拿到command buffer命令缓冲器(40行).所有进入GPU的工作都会被排列到该缓冲器里面.我们需要前一阶段的command queue命令队列来创建command buffer命令缓冲器.第二步是建立一个rend pass渲染通道(38-39行).一个渲染通道描述符会告诉Metal当渲染一张图片时要采取什么处理.配置它时,我们需要指定我们要渲染什么颜色纹理(currentDrawable纹理).我们还需要指定绘制几何体前的清屏颜色.第三步是真实的drawing绘制(43-44行).我们指定顶点存储的缓冲器和需要绘制的基本实体.第四步也就是最后一步是commit the command buffer提交命令缓冲器(46-47行)到GPU.当调用commit提交时,command buffer命令缓冲器被编码,送入命令队列的末尾,一旦时间到了就在GPU上执行.

缓冲区绑定

最佳实践:使用适当的方法将缓冲区数据绑定到图形或计算功能。

  • 本章使用顶点函数绑定作为示例。Metal为和类中的片段内核函数提供了等效的API 。MTLRenderCommandEncoderMTLComputeCommandEncoder

setVertexBytes:length:atIndex:方法是将极小量的动态缓冲区数据绑定到顶点函数的最佳选项,如清单5-1所示。此方法避免了创建中间MTLBuffer对象的开销。相反,Metal为我们管理瞬态缓冲区。

float _verySmallData = 1.0;[renderEncoder setVertexBytes:&_verySmallData length:sizeof atIndex:0];

如果数据大小大于4 KB,请创建一次MTLBuffer对象并根据需要更新其内容。调用setVertexBuffer:offset:atIndex:方法将缓冲区绑定到顶点函数; 如果缓冲区包含多个绘制调用中使用的数据,则setVertexBufferOffset:atIndex:稍后调用该方法以更新缓冲区偏移量,使其指向相应绘制调用数据的位置,如清单5-2所示。如果仅更新其偏移量,则无需重新绑定当前绑定的缓冲区。

清单5-2更新绑定缓冲区的偏移量

// Bind the vertex buffer once[renderEncoder setVertexBuffer:_vertexBuffer[_frameIndex] offset:0 atIndex:0];for(int i=0; i<_drawCalls; i  ){// Update the vertex buffer offset for each draw call[renderEncoder setVertexBufferOffset:i*_sizeOfVertices atIndex:0];// Draw the vertices[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:_vertexCount];}

最佳实践:尽量不要长期持有Drawable

大多数Metal应用程序实现由CAMetalLayer对象定义的图层支持视图。该层提供符合CAMetalDrawable协议的有效可显示资源,通常称为可绘制的drawable提供了一个MTLTexture对象,该对象通常用作附加到MTLRenderPassDescriptor对象的可显示渲染目标,目标是呈现在屏幕上。

通过presentDrawable:在调用其commit方法之前调用命令缓冲区的方法来注册drawable。但是,只有在命令缓冲区完成执行并且已绘制或写入drawable之后,才会实现drawable本身。

drawable跟踪它是否具有出色的呈现或写入请求,并且在这些请求完成之前不会出现。命令缓冲区仅在计划执行时注册其可绘制请求。在调度命令缓冲区之后注册可绘制的表示可确保在实际呈现drawable之前完成所有命令缓冲区工作。在注册可绘制的演示文稿之前,不要等待命令缓冲区完成其GPU工作; 这将导致相当大的CPU停滞。

  • 为了避免在安排任何工作之前呈现drawable,或者为了避免持续超过必要的drawable,请调用命令缓冲区的presentDrawable:方法而不是drawable的present方法。presentDrawable:是一种方便的方法,present通过命令缓冲区的addScheduledHandler:回调调用给定的drawable 方法。

Drawable是由Core Animation框架创建和维护的相当消耗性能的系统资源。它们存在于有限且可重复使用的资源池中,并且在您的应用程序请求时可能可用,也可能不可用。如果在请求时没有可用的可绘制,则调用线程将被阻塞,直到新的可绘制可用(通常在下一个显示刷新间隔)。

Drawable是由Core Animation框架创建和维护的昂贵系统资源。它们存在于有限且可重复使用的资源池中,并且在您的应用程序请求时可能可用,也可能不可用。如果在请求时没有可用的可绘制,则调用线程将被阻塞,直到新的可绘制可用(通常在下一个显示刷新间隔)。

要尽可能简短地持有drawable,请执行以下两个步骤:

  • 1:总是尽可能晚地获得抽签; 优选地,紧接在编码屏幕上渲染通道之前。帧的CPU工作可能包括动态数据更新和屏幕外渲染过程,您可以在获取可绘制之前执行这些过程。

  • 2:务必尽快释放drawable,越早越好。在完成帧的CPU工作之后立即执行。在自动释放池块中需要包含渲染循环,以避免可能出现多个drawable的死锁情况。

  • iOS 10tvOS 10开始,可以安全地保存drawables以用于演示后属性查询,例如drawableID和presentedTime。否则,drawables应该在不再需要时释放,这通常是在调用命令缓冲区的presentDrawable:方法之后。

图6-1显示了drawable相对于其他CPU工作的生命周期。

图片 14图6-1 drawable的生命周期

使用MTKView对象是与drawable交互的首选方式。一个MTKView目的是通过一个备份CAMetalLayer对象并提供currentDrawable获取用于当前帧中的可拉伸性。当前帧呈现到此drawable中,并且该presentDrawable:方法调度实际呈现以在下一显示刷新间隔发生。该currentDrawable属性在每帧结束时自动更新。

一个MTKView对象还提供了currentRenderPassDescriptor一个引用当前绘制的纹理简便属性; 使用此属性创建渲染命令编码器,该编码器呈现为当前的drawable。对currentRenderPassDescriptor属性的调用隐式获取当前帧的drawable,然后将其存储在currentDrawable属性中。

  • 如果创建由对象支持的自己的子类UIView或NSView子类,则CAMetalLayer必须显式获取drawable并使用其纹理来配置渲染过程描述符。您也可以为自己的MTKView对象执行此操作,但简单地使用currentRenderPassDescriptor便利属性要容易得多。有关如何从a UIView或NSView子类获取drawable 的示例,请参阅MetalBasic3D示例。

下面的代码显示了如何使用带有MetalKit视图的drawable

- render:(MTKView *)view {// Update your dynamic data[self update];// Create a new command bufferid <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];// BEGIN encoding any off-screen render passes/* ... */// END encoding any off-screen render passes// BEGIN encoding your on-screen render pass// Acquire a render pass descriptor generated from the drawable's texture// 'currentRenderPassDescriptor' implicitly acquires the drawableMTLRenderPassDescriptor* renderPassDescriptor = view.currentRenderPassDescriptor;// If there's a valid render pass descriptor, use it to render into the current drawableif(renderPassDescriptor != nil) { id<MTLRenderCommandEncoder> renderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; /* Set render state and resources */ /* Issue draw calls */ [renderCommandEncoder endEncoding]; // END encoding your on-screen render pass // Register the drawable presentation [commandBuffer presentDrawable:view.currentDrawable];}/* Register optional callbacks */// Finalize the CPU work and commit the command buffer to the GPU[commandBuffer commit];}- drawInMTKView:(MTKView *)view {@autoreleasepool { [self render:view];}}

我希望这一部分的教程能有助于理解通用性的概念比如graphics pipeline图形管线Metal pipeline Metal管线.下一章节,我已经迫不及待想要回到编码中去了.
下次见!

原生屏幕比例

最佳实践:以目标显示的精确像素大小渲染绘图。

drawables的像素大小应始终与目标显示的精确像素大小相匹配。这对于避免渲染到屏幕外像素或产生额外的采样阶段至关重要。

UIScreen类提供限定天然尺寸和物理屏幕的比例因子两个属性:nativeBounds和nativeScale。查询nativeBounds属性以确定屏幕的本机边界矩形。查询nativeScale属性以确定用于将点转换为像素的本机比例因子。

  • iOStvOS中,大多数绘图技术都以磅而不是像素来衡量大小。您的金属应用应始终以像素为单位测量大小,并完全避免使用点数。要了解有关这两个单位之间差异的更多信息,请参阅点数与像素数。

MTKView级自动支持本机屏幕比例。默认情况下,视图当前drawable的大小始终保证与视图本身的大小相匹配。

  • 如果创建UIView由CAMetalLayer对象支持的子类,则必须先设置视图的contentScaleFactor属性以匹配屏幕的nativeScale属性。接下来,确保在视图大小发生变化时调整渲染目标的大小。最后,调整图层的drawableSize属性以匹配本机屏幕比例。

帧率

最佳实践:以一致且稳定的帧速率显示drawables

大多数应用程序的目标帧速率为60 FPS,相当于每帧16.67 ms。但是,在此时间内始终无法完成帧工作的应用应针对较低的帧速率以避免抖动。

  • 实时游戏的最低可接受帧速率为30 FPS。较低的帧速率被认为是糟糕的用户体验,应该避免这种情的发生。如果应用无法保持30 FPS的最低可接受帧速率,则应考虑进一步优化或减少工作负载(每帧花费少于33.33ms)。

可以通过maximumFramesPerSecond属性查询iOS和tvOS设备的最大帧速率。对于iOS设备,此值通常为60 FPS; 对于tvOS设备,此值可能会因附加屏幕的硬件功能或Apple TV上用户选择的分辨率而异。

使用MTKView对象是调整应用程序帧速率的推荐方法。默认情况下,视图呈现为60 FPS; 要定位不同的帧速率,需要将视图的preferredFramesPerSecond属性设置为所需的值。

  • 一个MetalKit视图总是舍入的值preferredFramesPerSecond到设备的最接近的因素maximumFramesPerSecond值。如果您的应用无法保持其最大目标帧速率(例如60 FPS),则将此属性设置为较低因子帧速率(例如30 FPS)。将值设置preferredFramesPerSecond为非因子帧速率可能会产生意外结果。
  • 维持目标帧速率要求您的应用程序在允许的渲染间隔时间内完全更新,编码,调度和执行帧的工作(例如,每帧少于16.67ms以保持60 FPS帧速率)。

presentDrawable:方法注册一个drawable presentation,以便尽快发生,通常在绘制或写入drawable之后的下一个显示刷新间隔。如果应用程序可以保持其最大目标帧速率(通过preferredFramesPerSecond属性设置),那么只需调用该presentDrawable:方法即可保持一致且稳定的帧速率。

presentDrawable:afterMinimumDuration:方法允许为每个drawable指定最小显示时间,这意味着只有在前一个drawable在显示器上消耗了足够的时间之后才会出现可绘制的演示文稿。这使我们可以将drawable的演示时间与应用程序的渲染循环同步。下面的代码显示了preferredFramesPerSecond与presentDrawable:afterMinimumDuration:API 之间的关系

view.preferredFramesPerSecond = 30;/* ... */[commandBuffer presentDrawable:view.currentDrawable afterMinimumDuration:1.0/view.preferredFramesPerSecond];

加载和存储操作

最佳实践:为渲染目标设置适当的加载和存储操作。

必须正确配置对Metal渲染目标执行的操作,以避免在渲染过程的开始或结束时进行高能耗且不必要的渲染工作。

选择适当的加载操作

使用以下准则确定特定渲染目标的相应加载操作。表9-1中还总结了这些指南。

  • 如果渲染了所有渲染目标像素,请选择该DontCare操作。没有与此操作相关的成本,纹理数据始终被解释为未定义。

  • 如果不需要保留渲染目标的先前内容并且仅渲染其某些像素,请选择该Clear操作。此操作会产生为每个像素写入清晰值的成本。

  • 如果需要保留渲染目标的先前内容并且仅渲染其某些像素,请选择该Load操作。此操作会产生加载先前内容的成本。

表9-1选择渲染目标加载操作

以前的内容保留 像素渲染到 加载动作
N / A 所有 DontCare
没有 一些 Clear
一些 Load

使用以下准则确定特定渲染目标的相应存储操作。

  • 如果不需要保留渲染目标的内容,请选择该DontCare操作。没有与此操作相关的成本,纹理数据始终被解释为未定义。这是深度和模板渲染目标的常见情况。

    • 如果需要保留渲染目标的内容,请选择Store操作。对于drawable和其他可显示的渲染目标,情况总是如此。
  • 如果渲染目标是多重采样纹理,请看下面的表格。

保留多重采样内容 解析指定的纹理 已解决的内容已保留 存储操作
storeAndMultisampleResolve
没有 MultisampleResolve
没有 N / A Store
没有 没有 N / A DontCare
  • 如果需要执行存储和解析操作,请始终将storeAndMultisampleResolve操作与单个渲染命令编码器一起使用。某些功能集不支持该storeAndMultisampleResolve操作; 相反,通过使用Store和MultisampleResolve动作,使用两个渲染命令编码器执行存储和解析操作。

在某些情况下,可能不会预先知道特定渲染目标的存储操作。要推迟此决定,请unknown在创建MTLRenderPassAttachmentDescriptor对象时设置临时值。在完成渲染过程的编码之前,必须指定已知的存储操作,否则会发生错误。设置该unknown值可以避免通过Store过早设置存储操作而产生的潜在成本。

应仔细评估在多个渲染过程中使用的渲染目标,以获得渲染过程之间的存储和加载操作的最佳组合。下面的表格列出了这些组合。

首先渲染传递存储操作 第二次渲染传递加载动作
DontCare 以下操作之一:DontCareClear
以下操作之一:StoreMultisampleResolvestoreAndMultisampleResolve Load

最佳实践:尽可能合并渲染命令编码器。

消除不必要的渲染命令编码器可减少内存带宽并提高性能。如果可能,可以通过将渲染命令编码器合并到单个渲染过程中来实现这些目标。要确定两个渲染命令编码器是否兼容兼容,您必须仔细评估其渲染目标,加载和存储操作,关系和依赖关系。两个合并兼容的最简单的标准渲染指令编码器,RCE1并且RCE2,如下所示:

  • RCE1RCE2在同一帧中创建。
  • RCE1RCE2从同一个命令缓冲区创建。
  • RCE1是在之前创建的RCE2
  • RCE2共享相同的渲染目标RCE1
  • RCE2不从任何渲染目标中采样RCE1
  • RCE1渲染目标存储操作是Store或DontCare,并且RCE2渲染目标加载操作是Load或DontCare。
  • RCE1和之间没有创建其他渲染命令编码器RCE2。如果满足这些条件,RCE1并且RCE2可以合并到单个渲染命令编码器中,如图下图所示。

图片 15简单的渲染命令编码器合并

此外,如果RCE1能与之前(创建一个渲染指令编码器合并RCE0),并RCE2可以与后(创建一个渲染指令编码器合并RCE3),然后RCE0RCE1RCE2,并且RCE3都可以合并。

假设满足所有其他条件,以下部分提供了评估渲染命令编码器之间的合并兼容性的指南。

  • 渲染通道的详细信息特定于您的应用; 因此,本指南无法提供有关如何合并特定渲染命令编码器集的具体建议。渲染命令编码器手动合并; 没有Metal API可以自动为您执行合并。大多数合并是通过合并绘制调用,顶点或片段函数或渲染目标来完成的。有些合并甚至可以通过可编程混合来完成,如MetalDeferredLighting示例中所示。

合并命令编码器中的渲染目标数量不得超过“ metal特征集”中记录的限制。

某些应用程序可能会开始编码为渲染命令编码器(RCE1),如果需要其他动态数据继续,则会过早地结束初始渲染过程。然后,在单独的渲染过程中使用第二渲染命令encoderRCE2)生成动态数据。然后,初始渲染过程继续第三个渲染命令编码器(RCE3)。下图显示了这种低效的顺序,包括分离的渲染命令编码器。

图片 16渲染过程的低效顺序

如果RCE2不依赖RCE1,则RCE2不需要编码RCE1。编码RCE2首先允许RCE1RCE3合并,RCEM因为它们代表相同的渲染过程,并且它们的动态数据依赖性保证在渲染过程开始时可用。下图显示了这种改进的顺序,包括合并的渲染命令编码器。

图片 17渲染过程的改进顺序

如果它们之间存在任何采样依赖关系,则无法合并渲染命令编码器。对于共享相同渲染目标的渲染命令编码器,可以通过它们之间的其他渲染命令编码器引入这些依赖关系,如下图所示。

图片 18渲染命令编码器之间的采样依赖关系

RCE1RCE3共享相同的渲染目标,RT1RT2,和RT3。此外,之间的行动RCE1,并RCE3表示渲染通道的延续。但是,由于引入的采样依赖性,这些渲染命令编码器无法合并RCE2RCE2渲染到单独的渲染目标RT4,由其进行采样RCE3。此外,它后面的RCE2样本RT3呈现RCE1。这些采样依赖项定义了严格的渲染传递顺序,可防止合并这些渲染命令编码器。

渲染命令编码器渲染目标之间的存储和加载操作并不像其他标准那样重要,但有一些值得注意的额外考虑因素。使用以下准则进一步了解渲染命令编码器之间的合并兼容性,RCE1RCE2基于其共享的渲染目标:

  • 如果存储操作RCE1是Store,并且加载操作RCE2是Load,则渲染目标是合并兼容的,并且通常继续渲染传递。

  • 如果存储操作RCE1是DontCare,并且加载操作RCE2[DontCare](https://developer.apple.com/documentation/metal/mtlloadaction/dontcare),则渲染目标是合并兼容的,并且通常用作中间资源。

  • 如果加载动作RCE2是Clear,则如果可以在合并的渲染命令编码器中执行基元清除操作,则首先将清除值渲染到显示对齐的四边形中,渲染目标是合并兼容的。

  • 有关为特定渲染目标选择适当的加载和存储操作的建议,请参阅加载和存储操作最佳实践。

命令缓冲区

最佳实践:每帧提交尽可能少的命令缓冲区,而不会低估GPU的使用率。

命令缓冲区是Metal中提交的工作单元; 它们由CPU创建并由GPU执行。此关系允许您通过调整每帧提交的命令缓冲区数来平衡CPU和GPU工作。

大多数Metal应用程序通过实现三重缓冲,使其CPU工作比GPU工作提前一到两帧。这意味着通常每个帧仅提交一个或两个命令缓冲区,通常排队的CPU工作足以使GPU保持忙碌状态。但是,如果CPU工作在GPU工作之前不能保持足够远,那么GPU将会处于闲置状态。更频繁的命令缓冲区提交可能会使GPU保持忙碌,但也可能引入CPU-GPU同步导致的CPU停顿。有效地管理这种权衡是提高性能的关键,可以通过仪器中的Metal System Trace分析模板来实现。

  • 有关三重缓冲的完整概述,请参阅三重缓冲最佳实践。

间接缓冲

最佳实践:如果您的绘制或调度调用参数是由GPU动态生成的,请使用间接缓冲区。

间接缓冲区是MTLBuffer具有表示绘制或调度调用参数的特定数据布局的对象。支持的布局由以下结构定义:

  • MTLDrawPrimitivesIndirectArguments

  • MTLDrawIndexedPrimitivesIndirectArguments

  • MTLDrawPatchIndirectArguments

  • MTLDispatchThreadgroupsIndirectArguments

间接缓冲区允许发出依赖于调用时未知的动态参数的调用。这些参数可以在发出调用后动态生成,但是在关联的渲染或计算传递开始执行时,它们必须始终可用。动态参数通常由GPU生成; 例如,补丁内核可以动态生成用于对曲面细分后顶点函数的补丁绘制调用的参数。

如果没有间接缓冲区,GPU会生成调用参数并将其写入常规缓冲区。CPU必须等到GPU完成所有工作,然后才能从常规缓冲区读取参数并发出调用。然后GPU必须等到CPU完成所有工作才能执行调用。这种低效的序列如下图所示。

图片 19在没有间接缓冲区的情况下的调用

使用间接缓冲区,CPU不需要等待任何值,并且可以立即发出引用间接缓冲区的绘制调用。在CPU完成所有工作之后,GPU可以生成参数,在一次传递中将它们写入间接缓冲区,并在另一次传递中执行与它们相关联的调用。这种改进的顺序如下图所示。

图片 20使用间接缓冲区发出调用

间接缓冲区消除了CPUGPU之间不必要的数据传输,从而减少了处理器空闲时间。如果CPU不需要访问绘制或调度调用的动态参数,请使用间接缓冲区。

最佳实践:在构建时编译函数并构建库。

编译Metal着色语言源代码是Metal应用程序生命周期中最消耗资源的阶段之一。Metal允许在构建时编译图形和计算函数,然后在运行时将它们作为库加载,从而最大限度地降低了这一成本。

在构建应用程序时,Xcode会自动编译.metal源文件并将它们构建到单个默认库中。要获取生成的MTLLibrary对象,请newDefaultLibrary在初始Metal设置期间调用该方法一次。

  • 如果应用程序具有自定义构建pipeline,您可能更喜欢使用Metal的命令行实用程序来构建库。有关详细信息,请参阅 Metal编程指南中的使用命令行实用程序构建库

在运行时构建库会导致显着的性能成本。仅当图形和计算功能是在运行时动态创建时才这样做。在所有其他情况下,始终在构建时构建库。

使用Xcode构建单个默认库是最快,最有效的构建选项。如果必须使用Metal的命令行实用程序或运行时方法来构建库,请合并Metal着色语言源代码并将所有函数分组到单个库中。如果可能,请避免创建多个库。

Pipelines

最佳实践:异步构建渲染和计算Pipelines。

拥有多个渲染或计算pipelines允许应用程序针对特定任务使用不同的状态配置。异步构建这些管道可以最大限度地提高性能和并行性。预先构建所有已知的pipelines,避免延迟加载。下面代码显示了如何异步构建多个渲染pipelines

const uint32_t pipelineCount;dispatch_queue_t dispatch_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);// Dispatch the render pipeline build__block NSMutableArray<id<MTLRenderPipelineState>> *pipelineStates = [[NSMutableArray alloc] initWithCapacity:pipelineCount];dispatch_group_t pipelineGroup = dispatch_group_create();for(uint32_t pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex  ){id <MTLFunction> vertexFunction = [_defaultLibrary newFunctionWithName:vertexFunctionNames[pipelineIndex]];id <MTLFunction> fragmentFunction = [_defaultLibrary newFunctionWithName:fragmentFunctionNames[pipelineIndex]];MTLRenderPipelineDescriptor* pipelineDescriptor = [MTLRenderPipelineDescriptor new];pipelineDescriptor.vertexFunction = vertexFunction;pipelineDescriptor.fragmentFunction = fragmentFunction;/* Configure additional descriptor properties */dispatch_group_enter(pipelineGroup);[_device newRenderPipelineStateWithDescriptor:pipelineDescriptor completionHandler: ^(id <MTLRenderPipelineState> newRenderPipeline, NSError *error ) { // Add error handling if newRenderPipeline is nil pipelineStates[pipelineIndex] = newRenderPipeline; dispatch_group_leave(pipelineGroup); }];}/* Do more work */// Wait for build to completedispatch_group_wait(pipelineGroup, DISPATCH_TIME_FOREVER);/* Use the render pipelines */

本文由新葡京8455发布,转载请注明来源

关键词: