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

作者: 张芳涛 | 来源:发表于2018-08-03 16:31 被阅读10次

    写在前面:

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

    正文

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

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

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

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

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

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

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

    资源管理

    对象持久化

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

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

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

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

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

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

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

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

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

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

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

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

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

    预先分配资源存储

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

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

    资源选项

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

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

    熟悉设备内存模型

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

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

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

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

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

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

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

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

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

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

    缓冲存储模式(macOS)

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

    • 如果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之后)
    纹理存储模式(macOS)

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

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

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

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

    • 如果CPUGPU频繁访问纹理,请选择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
    

    缓冲区绑定

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

    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,请执行以下两个步骤:

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

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

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

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

    图6-1 drawable的生命周期

    使用MetalKit视图与Drawables交互

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

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

    • 如果创建由对象支持的自己的子类UIViewNSView子类,则CAMetalLayer必须显式获取drawable并使用其纹理来配置渲染过程描述符。您也可以为自己的MTKView对象执行此操作,但简单地使用currentRenderPassDescriptor便利属性要容易得多。有关如何从a UIViewNSView子类获取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类提供限定天然尺寸和物理屏幕的比例因子两个属性:nativeBoundsnativeScale。查询nativeBounds属性以确定屏幕的本机边界矩形(以像素为单位)。查询nativeScale属性以确定用于将点转换为像素的本机比例因子。

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

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

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

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

    帧率(iOS和tvOS)

    最佳实践:以一致且稳定的帧速率显示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帧速率)。

    调整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中还总结了这些指南。

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

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

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

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

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

    选择适当的Store Action

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

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

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

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

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

    评估渲染过程之间的操作

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

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

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

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

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

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

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

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

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

    合并命令编码器中的渲染目标数量不得超过“ 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基于其共享的渲染目标:

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

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

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

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

    命令缓冲区

    最佳实践:每帧提交尽可能少的命令缓冲区,而不会低估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设置期间调用该方法一次。

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

    • 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 */
    

    相关文章

      网友评论

        本文标题:Metal入门资料017-Metal最佳实践指南

        本文链接:https://www.haomeiwen.com/subject/lsujvftx.html