Metal入门资料017-Metal最佳实践指南
写在前面:
对Metal技术感兴趣的同学,可以关注我的专题:Metal专辑
也可以关注我个人的简书账号:张芳涛
所有的代码存储的Github地址是:Metal
正文
本文是摘抄的苹果官方文档,苹果的文档里面有Metal Best Practices Guide这个章节。
Metal
提供对GPU
的最低开销访问,使您能够在iOS
,macOS
和tvOS
上最大化应用程序的图形和计算潜力。每毫秒和每一点都是Metal
应用程序和用户体验的组成部分 ,所以 我们有责任通过遵循本指南中描述的最佳实践来确保Metal
应用程序尽可能高效地运行。除非另有说明,否则这些最佳实践适用于支持Metal
的所有平台。
一个高性能的Metal
应用程序需要具备以下特点:
-
低
CPU
开销。Metal
旨在减少或消除许多CPU
端性能瓶颈。只有按照建议使用Metal API
,应用才能从此受益。 -
最佳
GPU
性能。Metal
允许创建并向GPU
提交命令。要优化GPU
性能,您的应用应优化这些命令的配置和组织。 -
处理器持续和并行工作能力。
Metal
旨在最大化CPU
和GPU
并行性。应用应该让这些处理器起作用并同时工作。 -
有效的资源管理。
Metal
为您的资源对象提供简单而强大的接口。应用应该有效地管理这些资源,以减少内存消耗并提高访问速度。
资源管理
对象持久化
最佳实践:尽早创建持久对象并经常重用它们。
Metal
框架提供了在应用程序的整个生命周期内管理持久对象的协议。这些对象的创建成本很高,但通常会初始化一次并经常重复使用。不需要在每个渲染或计算循环的开头创建这些对象。
首先初始化的设备和命令队列
MTLCreateSystemDefaultDevice在应用程序启动时 调用该函数以获取默认系统设备。接下来,调用newCommandQueue或者 newCommandQueueWithMaxCommandBufferCount:方法创建一个命令队列,用于在该设备上执行GPU指令。
所有应用程序应该只MTLDevice为每个GPU 创建一个对象,并将其重用于该GPU上的所有Metal工作。大多数应用程序应该MTLCommandQueue每个GPU 只创建一个对象,但如果每个命令队列代表不同的Metal工作(例如,非实时计算处理和实时图形渲染),您可能需要更多。
- 一些
macOS
设备具有多个GPU
。如果需要使用多个GPU
,请调用该MTLCopyAllDevices函数以获取可用设备的数组。为您使用的每个GPU
创建并保留至少一个命令队列。
在构建时编译函数并构建Library
有关在构建时编译函数和构建库的概述,请参阅Libraries最佳实践。
在运行时,使用MTLLibrary和MTLFunction对象访问图形库和计算函数。避免在运行时构建库或在渲染或计算循环期间获取函数。
如果需要配置多个渲染或计算管道,请MTLFunction尽可能重用对象。您可以释放MTLLibrary和MTLFunction建设的所有对象后,渲染并依赖于它们的计算pipelines
。
建立一次Pipelines并经常重复使用
构建可编程管道涉及对GPU状态的评估工作非常消耗性能。应该只构建MTLRenderPipelineState和MTLComputePipelineState对象一次,然后为创建的每个新渲染或计算命令编码器重用。不要为新的命令编码器构建新的管道。有关异步构建多个pipelines
的概述,请参阅pipelines最佳实践。
- 注意 : 除了渲染和计算管线,则可以选择创建MTLDepthStencilState和MTLSamplerState封装深度,模板,和采样器状态的对象。这些对象较轻便,但也应仅创建一次并经常重复使用。
预先分配资源存储
资源数据可以是静态的或动态的,并可在应用程序的整个生命周期的各个阶段进行访问。 但是,应尽早创建为此数据分配内存的MTLBuffer和MTLTexture对象。创建这些对象后,资源属性和存储分配是不可变的,但数据本身不是; 可以在必要时更新数据。
尽可能 重用MTLBuffer和MTLTexture对象,特别是对于静态数据。避免在渲染或计算循环期间创建新资源,即使对于动态数据也是如此。有关缓冲区和纹理的更多信息,请参阅资源管理和三重缓冲最佳实践。
资源选项
最佳实践:设置适当的资源存储模式和纹理使用选项。
必须正确配置您的Metal
资源,以利用快速内存访问和驱动程序性能优化。资源存储模式允许定义MTLBuffer和MTLTexture对象的存储位置和访问权限。纹理使用选项允许显式声明打算如何使用MTLTexture对象。
熟悉设备内存模型
设备内存型号因操作系统而异。iOS
和tvOS
设备支持统一的内存模型,其中CPU
和GPU
共享系统内存。macOS
设备支持具有CPU
可访问系统内存和GPU
可访问视频内存的独立内存模型。
- 一些
macOS
设备具有集成的GPU
。在这些设备中,驱动程序优化底层架构以支持离散内存模型。macOS Metal
应该始终以离散内存模型为目标。 - 所有
iOS
和tvOS
设备都集成了GPU
。
选择适当的资源存储模式(iOS和tvOS)
在iOS
和tvOS
中,Shared模式定义了CPU
和GPU
Private都可访问的系统内存,而模式定义了只能由GPU
访问的系统内存。
该Shared模式通常是iOS
和tvOS
资源的正确选择。Private仅当CPU
从不访问资源时才选择模式。
- 在
iOS
和tvOS
中,memoryless存储模式用于无记忆纹理。此存储模式只能用于存储在片上磁贴存储器中的临时渲染目标。有关详细信息,请参阅Memoryless Textures
Metal编程指南。
选择适当的资源存储模式(macOS)
在macOS中,Shared模式定义了CPU
和GPU
都可访问的系统内存,而Private模式定义了只能由GPU
访问的视频内存。
此外,macOS
实现了Managed为资源定义同步内存对的模式,其中一个副本位于系统内存中,另一个副本位于视频内存中。管理资源受益于对每个资源副本的快速CPU
和GPU
访问,同步这些副本所需的API
调用最少。
- 在
macOS
中,Shared模式仅适用于缓冲区,而不适用于纹理。缓冲区数据通常是线性的,从而产生简单的GPU
访问模式。纹理更复杂,其数据通常是平铺或调配的,从而导致更复杂的GPU
访问模式。
缓冲存储模式(macOS)
使用以下准则确定特定缓冲区的适当存储模式。
-
如果
GPU
专门访问缓冲区,请选择Private模式。这是GPU
生成的数据的常见情况,例如每个补丁镶嵌的因子。 -
如果
CPU
专门访问缓冲区,请选择Shared模式。这是一种罕见的情况,通常是blit
操作中的中间步骤。 -
如果
CPU
和GPU
都访问缓冲区,就像大多数顶点数据一样,请考虑以下几点并参考表3-1:-
对于频繁更改的小型数据,请选择Shared模式。将数据复制到视频存储器的开销可能比直接访问
GPU
系统存储器的开销更大。 -
对于不经常更改的中型数据,请选择Managed模式。始终在修改托管缓冲区的内容后调用适当的同步方法。
执行CPU
写入后,调用didModifyRange:方法以通知Metal
有关已修改的特定数据范围; 这允许Metal
仅更新视频内存副本中的特定范围。
在编写GPU写入之后,编码包括对synchronizeResource:方法的调用的blit
操作; 这允许Metal
在相关命令缓冲区完成执行后更新系统内存副本。 -
对于永不更改的大型数据,请选择Private模式。使用Shared模式初始化并填充源缓冲区,然后将其数据
blit
到具有Private模式的目标缓冲区。这是一次性成本的最佳操作。
-
-
表3-1为
CPU
和GPU
访问的缓冲区数据选择存储模式
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 之后) |
纹理存储模式(macOS)
在macOS
中,纹理的默认存储模式是Managed。使用以下准则确定特定纹理的适当存储模式。
-
如果
GPU
专门访问纹理,请选择Private模式。这是GPU
生成的数据的常见情况,例如可显示的渲染目标。 -
如果
CPU
专门访问纹理,请选择Managed模式。这是一种罕见的情况,通常是blit
操作中的中间步骤。 -
如果纹理由
CPU
初始化一次并由GPU
频繁访问,则使用Managed模式初始化源纹理,然后将其数据blit到具有Private模式的目标纹理。这是静态纹理的常见情况,例如漫反射贴图。 -
如果
CPU
和GPU
频繁访问纹理,请选择Managed模式。这是动态纹理的常见情况,例如图像滤镜。始终在修改托管纹理的内容后调用适当的同步方法。
对GPU写入进行编码后,对包含对以下任一方法的调用的blit操作进行编码。这允许Metal在关联的命令缓冲区完成执行后更新系统内存副本。
* synchronizeResource:
* synchronizeTexture:slice:level:
设置适当的纹理使用标志
Metal
可以根据其预期用途优化给定纹理的GPU
操作。如果您事先知道它们,请始终声明显式纹理使用选项。不要依赖Unknown选项; 虽然此选项为纹理提供了最大的灵活性,但却会产生显着的性能损失。如果驱动程序不知道您打算如何使用纹理,则无法执行任何优化。有关可用纹理使用选项的说明,请参阅MTLTextureUsage参考。
三重缓冲
最佳实践:实现三重缓冲模型以更新动态缓冲区数据。
动态缓冲区数据是指存储在缓冲区中的频繁更新的数据。为避免每帧创建新缓冲区并最小化帧之间的处理器空闲时间,我们需要实现三重缓冲模型。
防止访问冲突并减少处理器空闲时间
动态缓冲区数据通常由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:),提交命令缓冲区(commit),并将命令缓冲区传输到GPU
。 - 3:
GPU
执行命令缓冲区并从动态数据缓冲区读取。 - 4:GPU完成其执行并调用命令缓冲区完成处理程序(
[MTLCommandBufferHandler](https://developer.apple.com/documentation/metal/mtlcommandbufferhandler)
)。
此序列可以与两个动态数据缓冲区并行化,但是如果任一处理器正在等待繁忙的动态数据缓冲区,则命令缓冲区事务可能导致CPU停止或GPU闲置。
实现三重缓冲模型
在考虑处理器空闲时间,内存开销和帧延迟时,添加第三个动态数据缓冲区是理想的解决方案。图4-1显示了三重缓冲时间线,清单4-1显示了三重缓冲实现。
图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 */
}
- (void)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 */
}
- (void)makeResources
{
// Create a FIFO queue of three dynamic data buffers
// This ensures that the CPU and GPU are never accessing the same buffer simultaneously
MTLResourceOptions 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];
}
- (void)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 buffer
DynamicBufferData *dynamicBufferData = [_dynamicDataBuffers[_currentFrameIndex] contents];
/* Perform updates */
}
- (void)render
{
// Wait until the inflight command buffer has completed its work
dispatch_semaphore_wait(_frameBoundarySemaphore, DISPATCH_TIME_FOREVER);
// Update the per-frame dynamic buffer data
[self update];
// Create a command buffer and render command encoder
id <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
缓冲区绑定
最佳实践:使用适当的方法将缓冲区数据绑定到图形或计算功能。
- 本章使用顶点函数绑定作为示例。Metal为和类中的片段和内核函数提供了等效的API 。MTLRenderCommandEncoderMTLComputeCommandEncoder
setVertexBytes:length:atIndex:方法是将极小量(小于4 KB)的动态缓冲区数据绑定到顶点函数的最佳选项,如清单5-1所示。此方法避免了创建中间MTLBuffer对象的开销。相反,Metal为我们管理瞬态缓冲区。
float _verySmallData = 1.0;
[renderEncoder setVertexBytes:&_verySmallData length:sizeof(float) 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];
}
Drawables
最佳实践:尽量不要长期持有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
Drawable
是由Core Animation
框架创建和维护的相当消耗性能的系统资源。它们存在于有限且可重复使用的资源池中,并且在您的应用程序请求时可能可用,也可能不可用。如果在请求时没有可用的可绘制,则调用线程将被阻塞,直到新的可绘制可用(通常在下一个显示刷新间隔)。
Drawable
是由Core Animation
框架创建和维护的昂贵系统资源。它们存在于有限且可重复使用的资源池中,并且在您的应用程序请求时可能可用,也可能不可用。如果在请求时没有可用的可绘制,则调用线程将被阻塞,直到新的可绘制可用(通常在下一个显示刷新间隔)。
要尽可能简短地持有drawable
,请执行以下两个步骤:
-
1:总是尽可能晚地获得抽签; 优选地,紧接在编码屏幕上渲染通道之前。帧的
CPU
工作可能包括动态数据更新和屏幕外渲染过程,您可以在获取可绘制之前执行这些过程。 -
2:务必尽快释放
drawable
,越早越好。在完成帧的CPU
工作之后立即执行。在自动释放池块中需要包含渲染循环,以避免可能出现多个drawable
的死锁情况。 -
从
iOS 10
和tvOS 10
开始,可以安全地保存drawables
以用于演示后属性查询,例如drawableID和presentedTime。否则,drawables应该在不再需要时释放,这通常是在调用命令缓冲区的presentDrawable:方法之后。
图6-1显示了drawable
相对于其他CPU
工作的生命周期。
使用MetalKit视图与Drawables交互
使用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
。
- (void)render:(MTKView *)view {
// Update your dynamic data
[self update];
// Create a new command buffer
id <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 drawable
MTLRenderPassDescriptor* renderPassDescriptor = view.currentRenderPassDescriptor;
// If there's a valid render pass descriptor, use it to render into the current drawable
if(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];
}
- (void)drawInMTKView:(MTKView *)view {
@autoreleasepool {
[self render:view];
}
}
原生屏幕比例(iOS和tvOS)
最佳实践:以目标显示的精确像素大小渲染绘图。
drawables
的像素大小应始终与目标显示的精确像素大小相匹配。这对于避免渲染到屏幕外像素或产生额外的采样阶段至关重要。
UIScreen类提供限定天然尺寸和物理屏幕的比例因子两个属性:nativeBounds和nativeScale。查询nativeBounds属性以确定屏幕的本机边界矩形(以像素为单位)。查询nativeScale属性以确定用于将点转换为像素的本机比例因子。
- 在
iOS
和tvOS
中,大多数绘图技术都以磅而不是像素来衡量大小。您的金属应用应始终以像素为单位测量大小,并完全避免使用点数。要了解有关这两个单位之间差异的更多信息,请参阅点数与像素数。
使用MetalKit视图支持本机屏幕比例
MTKView级自动支持本机屏幕比例。默认情况下,视图当前drawable
的大小始终保证与视图本身的大小相匹配。
- 如果创建UIView由CAMetalLayer对象支持的子类,则必须先设置视图的contentScaleFactor属性以匹配屏幕的nativeScale属性。接下来,确保在视图大小发生变化时调整渲染目标的大小。最后,调整图层的drawableSize属性以匹配本机屏幕比例。
帧率(iOS和tvOS)
最佳实践:以一致且稳定的帧速率显示drawables
。
大多数应用程序的目标帧速率为60 FPS
,相当于每帧16.67 ms
。但是,在此时间内始终无法完成帧工作的应用应针对较低的帧速率以避免抖动。
- 实时游戏的最低可接受帧速率为
30 FPS
。较低的帧速率被认为是糟糕的用户体验,应该避免这种情的发生。如果应用无法保持30 FPS
的最低可接受帧速率,则应考虑进一步优化或减少工作负载(每帧花费少于33.33
ms)。
查询和调整帧率
可以通过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
帧速率)。
调整Drawable Presentation时间
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 |
选择适当的Store Action
使用以下准则确定特定渲染目标的相应存储操作。
-
如果不需要保留渲染目标的内容,请选择该DontCare操作。没有与此操作相关的成本,纹理数据始终被解释为未定义。这是深度和模板渲染目标的常见情况。
- 如果需要保留渲染目标的内容,请选择Store操作。对于
drawable
和其他可显示的渲染目标,情况总是如此。
- 如果需要保留渲染目标的内容,请选择Store操作。对于
-
如果渲染目标是多重采样纹理,请看下面的表格。
保留多重采样内容 | 解析指定的纹理 | 已解决的内容已保留 | 存储操作 |
---|---|---|---|
是 | 是 | 是 | storeAndMultisampleResolve |
没有 | 是 | 是 | MultisampleResolve |
是 | 没有 | N / A | Store |
没有 | 没有 | N / A | DontCare |
- 如果需要执行存储和解析操作,请始终将storeAndMultisampleResolve操作与单个渲染命令编码器一起使用。某些功能集不支持该storeAndMultisampleResolve操作; 相反,通过使用Store和MultisampleResolve动作,使用两个渲染命令编码器执行存储和解析操作。
在某些情况下,可能不会预先知道特定渲染目标的存储操作。要推迟此决定,请unknown在创建MTLRenderPassAttachmentDescriptor对象时设置临时值。在完成渲染过程的编码之前,必须指定已知的存储操作,否则会发生错误。设置该unknown值可以避免通过Store过早设置存储操作而产生的潜在成本。
评估渲染过程之间的操作
应仔细评估在多个渲染过程中使用的渲染目标,以获得渲染过程之间的存储和加载操作的最佳组合。下面的表格列出了这些组合。
首先渲染传递存储操作 | 第二次渲染传递加载动作 |
---|---|
DontCare | 以下操作之一: DontCare Clear |
以下操作之一: Store MultisampleResolve storeAndMultisampleResolve |
Load |
渲染命令编码器(iOS和tvOS)
最佳实践:尽可能合并渲染命令编码器。
消除不必要的渲染命令编码器可减少内存带宽并提高性能。如果可能,可以通过将渲染命令编码器合并到单个渲染过程中来实现这些目标。要确定两个渲染命令编码器是否兼容兼容,您必须仔细评估其渲染目标,加载和存储操作,关系和依赖关系。两个合并兼容的最简单的标准渲染指令编码器,RCE1
并且RCE2
,如下所示:
-
RCE1
并RCE2
在同一帧中创建。 -
RCE1
并RCE2
从同一个命令缓冲区创建。 -
RCE1
是在之前创建的RCE2
。 -
RCE2
共享相同的渲染目标RCE1
。 -
RCE2
不从任何渲染目标中采样RCE1
。 -
RCE1
渲染目标存储操作是Store或DontCare,并且RCE2
渲染目标加载操作是Load或DontCare。 - 在
RCE1
和之间没有创建其他渲染命令编码器RCE2
。
如果满足这些条件,RCE1
并且RCE2
可以合并到单个渲染命令编码器中,如图下图所示。
此外,如果RCE1
能与之前(创建一个渲染指令编码器合并RCE0
),并RCE2
可以与后(创建一个渲染指令编码器合并RCE3
),然后RCE0
,RCE1
,RCE2
,并且RCE3
都可以合并。
假设满足所有其他条件,以下部分提供了评估渲染命令编码器之间的合并兼容性的指南。
- 渲染通道的详细信息特定于您的应用; 因此,本指南无法提供有关如何合并特定渲染命令编码器集的具体建议。渲染命令编码器手动合并; 没有Metal API可以自动为您执行合并。大多数合并是通过合并绘制调用,顶点或片段函数或渲染目标来完成的。有些合并甚至可以通过可编程混合来完成,如MetalDeferredLighting示例中所示。
合并命令编码器中的渲染目标数量不得超过“ metal特征集”中记录的限制。
评估 Rendering Pass Order
某些应用程序可能会开始编码为渲染命令编码器(RCE1
),如果需要其他动态数据继续,则会过早地结束初始渲染过程。然后,在单独的渲染过程中使用第二渲染命令encoder
(RCE2
)生成动态数据。然后,初始渲染过程继续第三个渲染命令编码器(RCE3
)。下图显示了这种低效的顺序,包括分离的渲染命令编码器。
如果RCE2
不依赖RCE1
,则RCE2
不需要编码RCE1
。编码RCE2
首先允许RCE1
和RCE3
合并,RCEM
因为它们代表相同的渲染过程,并且它们的动态数据依赖性保证在渲染过程开始时可用。下图显示了这种改进的顺序,包括合并的渲染命令编码器。
评估采样依赖性
如果它们之间存在任何采样依赖关系,则无法合并渲染命令编码器。对于共享相同渲染目标的渲染命令编码器,可以通过它们之间的其他渲染命令编码器引入这些依赖关系,如下图所示。
渲染命令编码器之间的采样依赖关系RCE1
和RCE3
共享相同的渲染目标,RT1
,RT2
,和RT3
。此外,之间的行动RCE1
,并RCE3
表示渲染通道的延续。但是,由于引入的采样依赖性,这些渲染命令编码器无法合并RCE2
。RCE2
渲染到单独的渲染目标RT4
,由其进行采样RCE3
。此外,它后面的RCE2
样本RT3
呈现RCE1
。这些采样依赖项定义了严格的渲染传递顺序,可防止合并这些渲染命令编码器。
评估渲染过程之间的操作
渲染命令编码器渲染目标之间的存储和加载操作并不像其他标准那样重要,但有一些值得注意的额外考虑因素。使用以下准则进一步了解渲染命令编码器之间的合并兼容性,RCE1
并RCE2
基于其共享的渲染目标:
-
如果存储操作
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具有表示绘制或调度调用参数的特定数据布局的对象。支持的布局由以下结构定义:
间接缓冲区允许发出依赖于调用时未知的动态参数的调用。这些参数可以在发出调用后动态生成,但是在关联的渲染或计算传递开始执行时,它们必须始终可用。动态参数通常由GPU
生成; 例如,补丁内核可以动态生成用于对曲面细分后顶点函数的补丁绘制调用的参数。
消除不必要的数据传输并减少处理器空闲时间
如果没有间接缓冲区,GPU
会生成调用参数并将其写入常规缓冲区。CPU
必须等到GPU
完成所有工作,然后才能从常规缓冲区读取参数并发出调用。然后GPU
必须等到CPU
完成所有工作才能执行调用。这种低效的序列如下图所示。
使用间接缓冲区,CPU
不需要等待任何值,并且可以立即发出引用间接缓冲区的绘制调用。在CPU
完成所有工作之后,GPU
可以生成参数,在一次传递中将它们写入间接缓冲区,并在另一次传递中执行与它们相关联的调用。这种改进的顺序如下图所示。
间接缓冲区消除了CPU
和GPU
之间不必要的数据传输,从而减少了处理器空闲时间。如果CPU
不需要访问绘制或调度调用的动态参数,请使用间接缓冲区。
汇编
函数和库
最佳实践:在构建时编译函数并构建库。
编译Metal
着色语言源代码是Metal
应用程序生命周期中最消耗资源的阶段之一。Metal
允许在构建时编译图形和计算函数,然后在运行时将它们作为库加载,从而最大限度地降低了这一成本。
在编译的时间里编译你的库
在构建应用程序时,Xcode
会自动编译.metal
源文件并将它们构建到单个默认库中。要获取生成的MTLLibrary对象,请newDefaultLibrary在初始Metal
设置期间调用该方法一次。
- 如果应用程序具有自定义构建
pipeline
,您可能更喜欢使用Metal
的命令行实用程序来构建库。有关详细信息,请参阅 Metal编程指南中的使用命令行实用程序构建库。
在运行时构建库会导致显着的性能成本。仅当图形和计算功能是在运行时动态创建时才这样做。在所有其他情况下,始终在构建时构建库。
-
include用户文件在运行时不支持该指令。
将您的功能分组到单个库中
使用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 complete
dispatch_group_wait(pipelineGroup, DISPATCH_TIME_FOREVER);
/* Use the render pipelines */