iOS音视频开发音视频专辑视频播放器和音视频基础知识

Metal入门资料017-Metal最佳实践指南

2018-08-03  本文已影响10人  张芳涛

写在前面:

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

正文

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

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

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

资源管理

对象持久化

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

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

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

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

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

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

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

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

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

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

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

预先分配资源存储

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

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

资源选项

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

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

熟悉设备内存模型

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

选择适当的资源存储模式(iOS和tvOS)

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

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

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

选择适当的资源存储模式(macOS)

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

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

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

缓冲存储模式(macOS)

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

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写入进行编码后,对包含对以下任一方法的调用的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它已完成该命令缓冲区的执行。对于单个帧,此序列详述如下:

此序列可以与两个动态数据缓冲区并行化,但是如果任一处理器正在等待繁忙的动态数据缓冲区,则命令缓冲区事务可能导致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

缓冲区绑定

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

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

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

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

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

图6-1 drawable的生命周期

使用MetalKit视图与Drawables交互

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

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

下面的代码显示了如何使用带有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类提供限定天然尺寸和物理屏幕的比例因子两个属性:nativeBoundsnativeScale。查询nativeBounds属性以确定屏幕的本机边界矩形(以像素为单位)。查询nativeScale属性以确定用于将点转换为像素的本机比例因子。

使用MetalKit视图支持本机屏幕比例

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

帧率(iOS和tvOS)

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

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

查询和调整帧率

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

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

调整Drawable Presentation时间

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

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

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

命令行相关介绍

加载和存储操作

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

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

选择适当的加载操作

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

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

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

选择适当的Store Action

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

保留多重采样内容 解析指定的纹理 已解决的内容已保留 存储操作
storeAndMultisampleResolve
没有 MultisampleResolve
没有 N / A Store
没有 没有 N / A DontCare

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

评估渲染过程之间的操作

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

首先渲染传递存储操作 第二次渲染传递加载动作
DontCare 以下操作之一:
DontCare
Clear
以下操作之一:
Store
MultisampleResolve
storeAndMultisampleResolve
Load

渲染命令编码器(iOS和tvOS)

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

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

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

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

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

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

评估 Rendering Pass Order

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

渲染过程的低效顺序

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

渲染过程的改进顺序

评估采样依赖性

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

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

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

评估渲染过程之间的操作

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

命令缓冲区

最佳实践:每帧提交尽可能少的命令缓冲区,而不会低估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可以生成参数,在一次传递中将它们写入间接缓冲区,并在另一次传递中执行与它们相关联的调用。这种改进的顺序如下图所示。

使用间接缓冲区发出调用

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

汇编

函数和库

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

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

在编译的时间里编译你的库

在构建应用程序时,Xcode会自动编译.metal源文件并将它们构建到单个默认库中。要获取生成的MTLLibrary对象,请newDefaultLibrary在初始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 complete
dispatch_group_wait(pipelineGroup, DISPATCH_TIME_FOREVER);

/* Use the render pipelines */
上一篇下一篇

猜你喜欢

热点阅读