Arm Mali GPU最佳实践(Arm Mali GPU Best Practices)

Posted ZJU_fish1996

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Arm Mali GPU最佳实践(Arm Mali GPU Best Practices)相关的知识,希望对你有一定的参考价值。

(部分收录)

简介

        本文设计为快速查询指南,所以假设读者熟悉了底层API的使用;我们将在其它文章中更详细地去讨论特定的主题,并花更多时间向仍在学习API的开发人员解释相关的概念。

注意:这些建议是为Mali GPU提供最佳实践,但实际中应用非常复杂,这些一般性的建议总会有例外。我们强烈建议对优化进行实测,来验证它们是否在目标设备上按预期执行。

Article structure

        图形处理的过程可以被描述为一个包含了应用、图形驱动层以及GPU本身内部的各种硬件阶段的流水线。

        大多数阶段遵循严格的架构管道,一个阶段的输出会作为下一个阶段的输入。其中计算着色器是一个例外,因为它们只是把结果写入到存储在系统内存的资源中,因此它们的输出可以在任何可以消耗这些资源的阶段中使用。

 

        对于每个最佳实践主题,我们都提供了详细解释的建议,以及在开发过程中应该考虑的做和不做的技术要点。

        如果可能的话,我们还记录了不遵循建议可能产生的影响,以及可应用的调试技术。

        为了简洁起见,我们在本文档提供了相对直白的建议,例如,“不要在片元着色器中使用discard“。但有些情况下,由于算法需要,会不可避免地使用到我们不建议的功能。

        不要被我们的建议约束而永远不去使用某个功能。但至少要在开始算法设计的时候能够意识到该算法可能存在的潜在性能影响。

Application logic

        在每一帧的开始,应用程序都会在CPU上准备调用指令来驱动图形栈。因此,性能问题的首要可能来源就是CPU上运行的软件,它可能位于应用程序代码内部,也可能位于图形驱动程序内部。

        本节着重给出可将图形驱动程序内部产生的CPU负载降至最低的建议。

Draw call Batching

        对于驱动程序的CPU开销而言,将绘制调用提交到指令流是一项昂贵的操作,尤其是对OpenGL,其每次drawcall的运行时成本高于Vulkan。

        将drawcall的渲染状态加载到硬件的成本是固定的,该成本会分摊到其执行的GPU线程。包含少量顶点和片元的绘制调用无法有效地分摊此成本,因此包含了大量小的绘制调用的应用程序可能无法充分利用硬件性能。

        这些问题都可以通过drawcall合批来改善;将多个共享相同渲染状态的物件合并渲染,减少每帧需要的drawcall数量,从而降低CPU的负载和功耗。

        Do

        ▪ 物件合批减少drawcall

        ▪ 绘制多个相同mesh使用Instancing,使用更灵活的实例化将静态批处理由更积极的示例剔除来实现,确保不可见的实例被剔除

        ▪ 即使未达到CPU上限也进行合批,以降低系统功耗

        ▪ 在OpenGL平台上一帧不应超过500drawcall

        ▪ 在Vulkan平台上一帧不应超过2000drawcall

        需要注意的是,这些drawcall的建议是一个比较粗略的经验法则,CPU的性能由硬件集的区别有较大的差异。

        Don't

        ▪ 批处理过大导致影响了剔除效率和渲染顺序

        ▪ 在不进行合批的情况下渲染大量小的drawcall,比如单个点或四边形

        Impact

        ▪ 更高的CPU应用负载

        ▪ CPU达到瓶颈后降低性能

        Debugging

        ▪ 分析应用的CPU负载率

        ▪ 跟踪每帧的API使用和call数量

Draw call culling

        应用程序最快能够处理的drawcall是那些在API调用前就能够丢弃的drawcall,因为它们确保是不可见的。记住一旦drawcall调用了API,那么在几何体被剔除前,它至少需要执行顶点着色器以获得裁剪空间的坐标。

        Do

        ▪ 剔除在视锥体外的物体; eg:包围盒视锥体检查

        ▪ 剔除被遮挡的物体;eg. 预剔除

        ▪ 在batching和culling之间达到平衡

        Don't

        ▪ 无视世界坐标而发起所有物件的drawcall

        Impact

        ▪ 更高的CPU应用负载

        ▪ 更高的顶点着色加载和内存带宽

        Debugging

        ▪ 分析应用的CPU负载率

        ▪ 跟踪每帧的API使用和call数量

        ▪ 使用GPU性能计数器来验证tile阶段的几何体剔除率。我们通常期望~50%的三角形剔除率,因为它们通常处在视锥体内但位于背面,更高的剔除率意味着应用逻辑的剔除逻辑可能存在问题。

Draw call render order

        GPU可以通过使用Early-Z测试最高效地剔除片元。为了达到Early-Z单元地最高剔除率,应该首先从前到后地渲染那些不透明的物件,然后再从后往前地在不透明物件上渲染那些透明物件,来确保alpha混合正确地工作。

        自Mali-T620开始,Mali GPU提供了叫做Forward Pixel Kill的优化,这有利于降低被遮挡但未被Early-Z剔除的片元的耗时。但是,不应该仅依赖于此,Early-Z通常更加高效和一致,并且可以在不支持FPK的旧Mali GPUs设备上工作。

        Do

        ▪ 从前往后渲染不透明物件

        ▪ 渲染不透明物件的时候关闭混合

        Don't

        ▪ 在片元着色器中使用discard;这将强制Late-Z

        ▪ 使用alpha-to-coverage;这将强制Late-Z

        ▪ 在片元着色器中写入片元深度;这将强制Late-Z

        Impact

        更高的片元着色器加载

        Debugging

        ▪ 渲染不带透明物件的场景,并使用GPU性能计数器来检查每个输出像素中片元的渲染次数。如果该值高于1,这意味着场景中包含了不透明的片元overdraw导致Early-Z失效。

        ▪ 使用GPU性能计数器来检查需要Late-Z测试的片元数量,以及被Late-Z测试剔除的片元数量。

Avoid depth prepasses

        在PC和主机游戏上的一种通用技术是Depth prepass。在这个算法中,不透明物件将被绘制两次,第一次仅绘制深度,然后使用EQUALS类型的深度比较来绘制颜色。这个技术是为了最小化重复片元执行的数量,并以双倍的drawcall数量为代价。

        对于Tile-based的GPU,例如Mali GPU,已经提供了类似FPK的优化来自动地减少重复的片元执行,这种重复drawcall,顶点执行以及内存带宽消耗已经抵消了这种好处。这属于可能会降低性能的“优化”。

        Do

        ▪ 使用合适的drawcall渲染顺序来最大化利用Early-Z

        Don't

        ▪ 使用深度prepass算法来避免片元的overdraw。

        Impact

        ▪ 由于重复的drawcall带来的更高CPU负载

        ▪ 由于重复的几何体带来更高的顶点带宽消耗

Vulkan GPU pipelining

        Mali GPU支持运行vertex/compute工作的同时,运行来自另一个Render Pass的fragment程序。性能良好的应用程序应该始终保证管线中不会产生大的空隙(Buddle)。

        Vulkan中可能存在的管道间隙可能是因为:

        ▪ 没有及时地提交命令缓冲区,导致GPU利用率不足,或者限制了可能存在的调度机会。Tile-based渲染对此非常敏感,因为将来自一个渲染pass的vertex/compute工作和更早的渲染pass的fragment工作重叠起来是非常重要的

        ▪ 管线阶段中存在依赖关系,来自渲染pass N的结果将被输出到之后的渲染pass M,N和M之间没有足够的其它工作来隐藏延迟。

        Do

        ▪ 相对频繁地提交渲染缓冲区;eg.对于帧中的每个主要渲染通道

        ▪ 如果较晚的管道阶段等待来自较早的管道阶段的结果,请确保通过在两个渲染通道之间插入独立的工作负载来隐藏延迟

        ▪ 考虑是否可以在管道中更早地生成依赖数据;compute是为顶点处理阶段输入数据的重要阶段

        ▪ 考虑是否可以将依赖于数据的处理移到管道靠后的位置;eg.依赖于片元着色的片元着色往往比依赖于片元的计算着色更高效

        ▪ 使用fence来异步回读数据到CPU,而不是同步地阻塞导致管道停滞

        Don't

        ▪ 在CPU或GPU上不必要地等待GPU的数据

        ▪ 直到一帧的最后才去一次性的提交所有渲染pass

        ▪ 在没有足够的独立工作来隐藏延迟的情况下,在管线中创建后向的数据依赖

        ▪ 使用vkQueueWaitIdle()或vkDeviceWaitIdle()

        Impact

        ▪ 影响可能会很小,也可能会非常显著,这取决于队列中工作负载的相对大小和顺序。

        Debugging

        ▪ DS-5 Streamline系统分析器可以可视化两个GPU队列上的Arm CPU和GPU工作,并且可以快速显示调度中的空隙,无论是本地的GPU阶段(表示阶段依赖性问题)还是全局的跨CPU和GPU(表示正在使用阻塞CPU的调用)

Vulkan pipeline synchronization

        Mali GPU公开了两个硬件处理槽,每个槽都实现了渲染管线阶段中的一个子集,并且能够和另一个槽并行。为了获得最佳性能,提交给GPU的工作负载允许跨这两个硬件槽进行最大化的并行执行,这一点至关重要。Vulkan阶段到Mali GPU执行槽的映射如下:

        Vertex/compute hardward slot

        ▪ VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT

        ▪ VK_PIPELINE_STAGE_VERTEX_*_BIT

        ▪ VK_PIPELINE_STAGE_TESSELLATION_*_BIT

        ▪ VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT

        ▪ VK_PIPELINE_STAGE_TRANSFER_BIT

        Fragment hardward slot

        ▪ VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT

        ▪ VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT

        ▪ VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT

        ▪ VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT

        ▪ VK_PIPELINE_STAGE_TRANSFER_BIT

        Vulkan让应用程序来控制命令之间的依赖关系;应用程序必须确保一个命令的管线阶段在之后依赖于它的管线阶段之前完成。API有多个可用的原语可用于命令同步:

        ▪ subpass依赖,管线屏障(barrier),以及用于在同一队列中进行同步的事件(event)

        ▪ 用于跨队列同步的信号量(Semaphore)

        这些不同细粒度的依赖工具允许应用指定作用域的同步,其中srcStage表明需要等待完成的管线阶段,而dstStage表明需要在执行前进行等待同步的管线阶段。正确地设置同步的最小作用域,在管线中尽可能早地设置srcStage,并且尽可能迟地设置dstStage,对于两个Mali硬件槽实现最大并行度有着很大帮助。信号量使用pWaitDestStages来允许控制依赖的命令何时执行,但是假设源阶段是最差的case也就是BOTTOM_OF_PIPE_BIT,所以仅在没有其它可用方式的时候启用信号量同步。

        在低级别中Mali GPU有两种类型的同步:一个是在单个硬件槽中的同步,另外一个是跨硬件槽的同步。

        ▪ 硬件槽内的同步是轻量的,因为不同渲染命令之间的依赖可以由执行顺序来约束

        ▪ 在srcStage运行在vertex/compute槽,而dstStage运行在fargment槽的同步是免费的,因为在整体渲染管线中片元着色总是在vertex/compute之后处理,所以这仅仅是一个执行顺序的约束

        ▪ 在srcStage运行在fragment槽,而dstStage运行在vertex/compute槽的同步可能会非常昂贵,因为它可能会产生管道空隙,除非在srcStage后有足够的隐藏延迟的工作。

        请注意,在Vulkan管线中TRANSFER阶段是一个有些重叠的术语,因为传输过程可能在任一硬件执行槽中实现,因此前向或后向的依赖方向并不那么明显。使用那个硬件槽取决于传输类型和传输资源的当前配置。从缓冲区到缓冲区的传输总是在vertex/compute槽中实现,而其它传输可能在任一槽上实现,这取决于当前正在写入的数据资源的状态。使用不同的处理槽可能会对应用程序渲染工作负载的流水线产生较大影响,请注意这一点,并检查传输操作的执行情况。

        Do

        ▪ 在管线中将srcStageMask设置的尽可能早

        ▪ 在管线中将dstStageMask设置的尽可能迟

        ▪ 检查你的依赖项是前向依赖(vertex/compute->fragment)还是后向依赖(fragment->vertex/compute),并尽可能减少后向依赖,除非可以在资源生成之间添加足够的工作延迟来隐藏引入的调度空隙。

        ▪ 在同步渲染pass的时候使用srcStageMask=ALL_GRAPHICS_BIT和dstStageMask=FRAGMENT_SHADING_BIT

        ▪ 最小化TRANSFER拷贝操作的使用 - 如果可以的话使用更高效的零拷贝算法-并且始终检查它们对硬件流水线的影响。

        ▪ 仅在需要时使用队列内的屏障,并在屏障之间放置尽可能多的工作

        Don't

        ▪ 不必要的让硬件饿死,旨在重叠vertex/compute和fragment进程

        ▪ 使用如下srcStageMask->dstStageMask同步配对,它们常能带来管线的耗时:

                ▪ BOTTOM_OF_PIPE_BIT -> TOP_OF_PIPE_BIT

                ▪ ALL_GRAPHICS_BIT -> ALL_GRAPHICS_BIT

                ▪ ALL_COMMANDS_BIT -> ALL_COMMANDS_BIT

        ▪ 需要发起信号并等待事件的时候使用VkEvent(应该使用vkCmdPipelineBarrier)

        ▪ 在单一队列中使用VKSemaphore来管理依赖

        Impact

        错误的使用管道评到可能会导致GPU工作不足(同步太多)或渲染效果错误(同步太少),正确地实现这一部分是任何Vulkan应用程序的关键部分。

        Warning

        请注意,为不同类型的工作负载安排两个不同的独立硬件插槽是tile-based GPU(如Mali)和桌面即时模式渲染的一个不同的地方。当从桌面GPU移植到tile-based GPU时,要调整管线内容使其更好地适配。

        Debugging

        ▪ DS-5 Streamline系统分析器可以可视化各个GPU硬件槽上Arm CPU和GPU的进程并快速显示调度中产生的空隙,这可能是GPU硬件内部的(意味着阶段依赖问题)或者全局的CPU和GPU之间的(意味着一个阻塞的CPU调用)

Pipelined resource updates

        对于应用开发者来说,OpenES提供了一种同步渲染模型,尽管底层执行是异步的。渲染必须反映提交drawcall时数据资源的状态,这意味着,如果一个待提交的drawcall引用的资源被应用立即修改了,驱动必须采取规避操作来保证正确性。在Mali GPU中,我们会尽量避免阻塞和等待资源引用计数归0,这可能会耗尽管线并影响性能。相反,我们创建一个新版本的资源来反映新状态,并且保留老版本的资源-作为一个ghost-它将一直存在,直到drawcall完成且引用计数归0。

        这一操作是非常昂贵的,它至少需要对新资源的内存分配,和不再需要时对旧资源的清理,并且可能需要将旧资源缓冲区拷贝到新资源中,如果更新并不是替换形式的。

        Do

        ▪ 对动态更新的资源使用N-buffer资源,避免修改drawcall队列中正在引用的资源

        ▪ 使用GL_MAP_UNSYNCHRONIZED来允许使用glMapBufferRange来修改被drawcall引用的缓冲区中的未引用区域。

        Don't

        ▪ 修改被drawcall引用的资源
        ▪ 使用GL_MAP_INVALIDATE_RANGE或GL_MAP_INVALIDATE_BUFFER标记的glMapBufferRange(),由于历史规范的存在歧义,这些标记现在仍然会导致ghost资源的创建

        Impact

        ▪ ghosing资源会增加CPU负载,因为额外的内存分配和可能存在的创建新资源的拷贝

        ▪ 尽管表面上看起来它通常不会增加内存占用;常用的替代方法是应用程序手动对资源使用N-buffer,这将分配和ghost资源一样多的内存,在内存跟踪中你看到的是ghost资源不断分配和销毁导致的内存波动

        Debugging

        ▪ DS-5 Streamline系统分析器可以可视化Arm CPU和GPU进程。如果资源更新管线出错可能会显示为CPU进程消耗的上升或者CPU忙碌时GPU中的空隙。

CPU overheads

Pipeline creation

        在shader编译和链接的阶段,Vulkan和OpenGL有相似的性能影响,但在Vulkan中,应用开发者还需要提供编译管线的持久缓存。

        Do

        ▪ 在应用启动或者游戏关卡加载的时候创建Pipeline

        ▪ 使用Piepline缓存来加速pipeline创建

        ▪ 将pipeline缓存序列化到硬盘,并在下次应用使用的时候重新加载它,让用户加载速度变快获得更好的用户体验

        Don't

        ▪ 使用CREATE_DERIVATIVE_BIT来创建derivative pipeline,这对当前Mal GPU驱动没有任何帮助

        Impact

        ▪ 存在高CPU负载和跳帧,如果在应用交互期间动态创建pipeline

        ▪ 无法序列化和重载pipeline缓存意味着应用无法从后续应用程序运行时减少的加载时间中受益

Allocating memory

        vkAllocateMemory()分配器并不是给频繁的直接内存分配设计的;我们认为vkAllocateMemory()的所有分配都是一个比较重的分页内核调用。

        Do

        ▪ 使用自己的分配器来对分配进行子管理

        Don't

        ▪ 使用vkAllocateMemory() 作为通用的内存分配器

        Impact

        ▪ 增加应用的CPU负载

        Debugging

        ▪ 在运行时追踪vkAllocateMemory() 调用的频率和分配大小

Vulkan CPU memory mapping

        Vulkan提供了对复杂缓冲区映射的更多支持,允许应用对使用的内存类型有更多的控制。

        Mali GPU驱动暴露了Midgard架构GPU的三种内存类型:

        ▪ DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT

        ▪ DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_CACHED_BIT

        ▪ DEVICE_LOCAL_BIT | LAZILY_ALLOCATION_BIT

        以及四种Bifrost架构GPU上可能的内存类型:

        ▪ DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT

        ▪ DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_CACHED_BIT

        ▪ DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT | HOST_CACHED_BIT

        ▪ DEVICE_LOCAL_BIT | LAZILY_ALLOCATED_BIT

        每一个都可以有着不同的用处。

        Not Cached, coherent

        HOST_VISIBLE | HOST_COHERENT内存标记是用于支持和匹配OpenGL ES描述的CPU上未缓存的数据。对于CPU上只写资源是最佳类型,因为它避免CPU缓存CPU永远不会使用的数据,并且使用CPU写入缓冲区将小的写入合并到更大更高效的外部存储设备。

        Cached, incoherent

        HOST_VISIBLE | HOST_CACHED内存类型设置了CPU端使用CPU缓存的映射内存。这适用于由应用软件可读的映射资源;由于能将数据预读取到CPU缓存中,使用memcpy()读取缓存回读的吞吐量观测的速度提升了10倍。

        但是,由于其内存与GPU内存视图不一致,当CPU完成写入,要将数据发送到GPU时需要调用vkFlushMappedRanges(),并且还需要调用vkInvalidateMappedRanges()来确保安全回读GPU已经写入的数据。这两者都需要驱动插入手动CPU缓存维护操作,以确保CPU缓存和主内存内容之间的一致性,这是相对昂贵的。谨慎使用在CPU上回读的资源。

        Cached, coherent

        HOST_VISIBLE | HOST_COHERENT | HOST_CACHED内存类型仅在Birfrost GPU上支持,并且仅当芯片组支持CPU和GPU之间的硬件一致性协议时可用。如果平台不支持,驱动将使用HOST_VISIBLE | HOST_CACHED来代替。

        CPU端缓存内存的时候提高了高效率的回读,硬件一致性的使用意味着避免了手动控制缓存的开销。所以当可行的时候,更推荐使用HOST_VISIBLE | HOST_COHERENT内存类型。

        硬件一致性确实有一些比较小的功耗成本,并且对于大多数CPU上只写的资源是不需要的;对于这一类资源,我们仍然建议使用HOST_VISIBLE | HOST_COHERENT内存类型来绕过CPU缓存。

        Lazily allocated

        LAZILY_ALLOCATED内存类型是一种特殊的内存类型,它被设计为仅支持GPU上的虚拟地址空间而不存在物理地址,这是由于内存应该是临时的并且不会由GPU内部之外访问。

        它适用于仅存在GPU tiled缓冲区内存的资源,并且不会在创建它们的render pass之外使用 - 比如用于简单渲染的深度/模板缓冲区以及G-buffer附件。如果需要,驱动程序将自动为这些资源分配物理内存,但这可能会导致停顿。

        Do

        ▪ 对不变的资源使用HOST_VISIBLE | HOST_COHERENT类型

        ▪ 对CPU只写的资源使用HOST_VISIBLE | HOST_COHERENT类型

        ▪ 当向HOST_VISIBLE | HOST_COHERENT写入更新时使用memcpy()或确保写入是连续的,以便从CPU写入连续单元时获得最佳效率

        ▪ 对于只在一个renderpass中存在的临时帧缓冲attachment,使用LAZILY_ALLOCATED

        ▪ 持久映射那些经常访问的缓冲区,例如Uniform Buffer或Dynamic Vertex Buffer数据,因为映射和取消映射缓冲区都需要CPU成本

        Don't

        ▪ 在CPU中从未缓存的数据回读数据

        ▪ 实现一个子分配器,并将其CPU侧管理的元数据存储在未缓存的缓冲区上

        ▪ 对TRANSITENT_ATTACHMENT帧缓冲区以外的内容使用LAZILY_ALLOCATED内存

        Impact

        ▪ 增加的CPU进程消耗,尤其是从未缓存内存的回读,这可能比缓存读取慢一个数量级

        Debugging

        ▪ 检查所有CPU上读取的缓冲区是否被缓存

        ▪ 为缓冲区设计接口来支持按需进行隐式刷新/无效设置,如果这种基础的解耦,调试由于缺少维护操作导致的一致性错误是非常困难和耗时的。

Command pools

        如果command pool创建的时候未添加RESET_COMMAND_BUFFER_BIT的标记,那么command pool不会自动回收已删除的command buffer的内存。未设置该标记的pool不会回收这些内存而是一直保留这些内存引用,直到应用程序重置了pool。

        Do

        ▪ 使用RESET_COMMAND_BUFFER_BIT来创建command pool,或者定期调用vkResetCommandPool() 来释放内存。注意RESET_COMMAND_BUFFER_BIT将会强制pool中每个command buffer使用单独的内部分配器,相比起单个poold的重置会增加CPU的负载。

        Impact

        在command pool reset调用前,会有持续增长的内存使用

Command buffers

        Command buffer的使用标记会影响性能。为了获得最佳性能,应该设置ONE_TIME_SUBMIT_BIT标记。如果使用了SIMULTANEOUS_USE_BIT标记可能会导致性能的下降。

        Do

        ▪ 默认使用ONE_TIME_SUBMIT_BIT

        ▪ 考虑构造每帧的command buffer,考虑连续使用的command buffer的复用

        ▪ 如果应用程序逻辑每帧重播完全相同的command序列,使用SIMULTANEOUS_USE_BIT;相比起应用重播,驱动能更高效地处理这种情况,但是不如One-time submit buffer高效。

        Don't

        ▪ 非必要时使用SIMULTANEOUS_USE_BIT

        ▪ 使用包含RESET_COMMAND_BUFFER_BIT标记的command pool。这将禁止驱动对pool中的所有command buffer使用单一的大分配器,这增加了内存管理的消耗。

        Impact

        ▪ 如果标记使用的不恰当,会带来CPU负载的增长

        Debugging

        ▪ 评估除了ONE_TIME_SUBMIT_BIT之外的任何命令的缓冲区标志的使用,并检查它是否是必要的。

        ▪ 评估每个vkResetCommndBuffer()的使用,并且测试它们是否能够被vkResetCommandPool()替代。

        当前vkResetCommandBuffer的实现会比预期中更昂贵,因为它等价于释放并且重新分配command buffer。

        Do

        ▪ 避免频繁的调用vkResetCommandBuffer

        Impact

        ▪ 如果频繁的调用command buffer reset会带来CPU负载的增长

Secondary command buffers

        当前Mali硬件层没有原生支持在secondary command buffer中发起命令,所以当使用secondary command buffer的时候会产生额外开销。应用程序通常会在需要构造多线程渲染的command buffer的时候会用到secondaryd command buffer,但是建议最小化secondardy command buffer的调用。对于primary command buffer而言,我们建议避免创建使用SIMULTANEOUS_USE_BIT的command buffer因为这会带来更高的负载。

        Do

        ▪ 在需要使用多线程渲染的时候使用secondary command buffer

        ▪ 最小化每帧调用secondary command buffer的次数

        Don't

        ▪ 对secondaryd command buffer使用SIMULTANEOUS_USE_BIT

        Impact

        ▪ 增加CPU负载

Descriptor sets and layouts

        Mali GPU在API层支持四个同时绑定的descriptor set,但在内部每个drawcall都需要一个物理的描述符表。

        对于任一drawcall而言,如果API层中四个descriptor set中的一个或多个descriptor set发生了变化,驱动需要重建内部的表。descriptor变化后的第一个drawcall相比其后面重用相同descriptor set的drawcall有更高的CPU负载,并且descriptor sets越大,重建的代价也就越高。

        此外,当前驱动中descriptor set pool分配并不是池化的,因此不建议在性能敏感的代码中调用vkAllocateDescriptorSets()

        Do

        ▪ 尽可能地pack descriptor set绑定空间

        ▪ 更新已经分配但不再引用的descriptor set,而不是重置descriptor pool并且重新分配新的descriptor set

        ▪ 倾向于重用已经分配的descriptor set,而不是每次都使用相同的信息更新它们

        ▪ 如果你计划绑定相同的UBO/SSBO但仅使用不同的偏移,倾向于使用UNIFORM_BUFFER_DYNAMIC或STORAGE_BUFFER_DYNAMIC的描述符类型,备选方案是使用更多的descriptor set

        Don't

        ▪ 使用稀疏的descriptor set

        ▪ 留下未使用的条目 - 需要付出拷贝和合并的代价

        ▪ 在性能敏感的代码段中从descriptor pool分配descriptor set

        ▪ 在不需要修改绑定偏移的时候使用DYNAMIC_OFFSET的UBO/SSBO,使用动态偏移的额外开销很小

        Impact

        ▪ drawcall带来增加的CPU负载

        Debugging

        ▪ 跟踪pipeline layout中未使用的条目

        ▪ 检查vkAllocateDescriptorSets中是否存在竞争,如果发生了会产生性能问题

Vertex shading

Index draw calls

        索引绘制往往会比非索引绘制更高效,因为它们重用了更多的顶点,如在相邻的三角形strips之间。这里有一些可以提升索引绘制的建议。

        Do

        ▪ 在可能进行顶点复用的时候使用索引绘制

        ▪ 对于post-transform缓存优化索引的局部性

        ▪ 确保索引缓冲区引用的每个值在最大和最小使用的索引值之间,来最小化顶点捕获的消耗

        ▪ 避免修改索引缓冲区中的内容,或者对volatile资源使用glDrawRangeElements(),否则驱动程序必须扫描索引缓冲区来决定有效的索引区间。

        Don't

        ▪ 使用客户端侧的索引缓冲区

        ▪ 使用稀疏访问顶点缓冲区中顶点的索引

        ▪ 对于非常简单的模型比如一个四边形或者点的列表使用索引绘制,只有较少或没有顶点重用

        ▪ 通过从完整精度模型稀疏的采样顶点来实现几何LOD,应为每个层级创建连续的顶点。

        Impact

        ▪ 使用客户端侧的索引缓冲区会带来CPU负载的增加,因为需要分配服务器侧的缓冲区存储,拷贝数据,并且遍历内容来确定有效的索引区间

        ▪ 使用频繁修改的索引缓冲区会带来CPU负载的增加,因为需要重新便利缓冲区来决定有效的索引区间

        ▪ 由于索引的稀疏性或者较低的局部性产生的低效模型索引通常会带来GPU顶点处理额外的开销和带宽,这还取决于模型的复杂性和内存中索引缓冲区的布局。

        Debugging

        ▪ 遍历你提交的索引缓冲区并且确定你的索引缓冲区是否对顶点缓冲区存在稀疏访问,标记哪些包含未使用下标的缓冲区。

Index buffer encoding

        索引缓冲区数据是Mali GPU几何体装配和tiling阶段的主要数据资源之一。通过最小化packing索引缓冲区能够降低tiling的消耗。

        Do

        ▪ 使用最低精度的索引数据类型来尽可能降低索引列表的大小

        ▪ 倾向于使用strip格式而不是list格式来减少索引列表的大小

        ▪ 使用Primitive Restart来减少索引列表的大小

        ▪ 使用post-transform缓存来优化索引局部性

        Don't

        ▪ 对所有数据都使用32-bit下标值

        ▪ 使用空间一致性较差的索引缓冲区,因为这会导致缓存命中下降

        Impact

        在实际中很少会带来比较大的问题,只要drawcall足够大,能确保顶点着色和tiling管道的清晰,但是这些小改进可以累积成比较大的优化。

Attribute precision

        对于很多顶点属性数据来说,并不需要FP32 highp的精度,比如颜色。一个好的资产管线使用最小的精度去记录数据,并且能够确保最终的输出可用,节省带宽并提升性能。

        OpenGL和Vulkan都能表达不同格式的属性来适配相应所需的精度,比如8-bit,16-bit还有packed格式如RGB10_A2。

        Do

        ▪ 使用FP32来计算顶点的位置,额外的精度通常是为了确保稳定的几何位置输出的

        ▪ 对于其它属性使用最小精度的属性;Mali GPU硬件可以在数据加载的时候免费转换FP16/FP32,因此使用尽可能小的格式并打包数据格式会降低带宽

        Don't

        ▪ 总是对任何属性使用FP32,因为这样非常简单并且桌面GPU也是这么做的;这会带来很多性能和功耗问题

        ▪ 使用FP32数据上传到缓冲区,并且将其作为mediump属性来访问,这是一种对内存存储和带宽的浪费因为额外的精度被丢弃了

        Impact

        ▪ 更高的内存带宽和占用,并且降低了顶点着色的性能

Attibute layout

        从Birfrost架构开始,顶点数据可以使用索引驱动的顶点着色(IDVS)来绘制,首先绘制位置,然后对剔除后的图元顶点进行着色。良好的缓冲区布局可以最大限度的发挥这种几何管道的优势。

        Do

        ▪ 对位置信息使用单独的顶点缓冲区

        ▪ 在单个缓冲区中交错传递给顶点着色器未修改的属性

        ▪ 在单个缓冲区中交错由顶点着色器修改的每个非位置属性

        ▪ 保证顶点缓冲区属性步长紧凑

        ▪ 考虑移除特定用途未使用属性的专门优化网格版本:例如,为阴影贴图生成一个仅包含位置属性的缓冲区

        Don't

        ▪ 对每个属性使用一个缓冲区;每个缓冲区占用了缓冲区描述符缓存中的一个槽,并且对于剔除的顶点更多属性带宽会在fetch的时候被浪费,因为它们和可见顶点共享cache line。

        ▪ 将交错的顶点缓冲区补偿对齐到2的幂来“帮助”硬件;这只会增加内存带宽

Varying precision

        顶点着色器的输出-通常被称为可变输出(varying output)-会写回Mali GPU显存,因为在片元着色开始工作前所有的几何处理必须完成。最小化可变输出的精度可以带来双倍的提升,一次是顶点着色器输出的时候,另一次是片元着色访问的时候,因此正确设置非常重要的。

        如下可变数据通常使用mediump精度:

        ▪ 法线

        ▪ 顶点色

        ▪ 视图空间的位置和方向 

        ▪ 对于特定大小未tiled纹理的纹理坐标(大概在512x512)

        如下可变数据通常使用highp精度:

        ▪ 世界空间的位置

        ▪ 对于大纹理的坐标,或者级别warpping的纹理坐标

        Do

        ▪ 如果精度可接受的话对可变输出使用mediump

        Don't

        ▪ 对可变输出使用超过所需的分量和精度

        ▪ 顶点着色输出一些片元不需要的输出

        Impact

        ▪ 增加GPU内存带宽

        ▪ 降低顶点和片元着色器性能

Triangle density

        顶点的带宽和执行时间通常会比片元的消耗要大。确保你能够让每个片元的计算价值能够匹配的上几何体渲染的代价,将顶点处理的消耗分摊到输出的多个像素。

        Do

        ▪ 确保模型中每个几何体至少会生成10-20个片元

        ▪ 使用LOD动态模型,当物体离相机更远时使用更简单的模型

        ▪ 使用法线贴图技术将逐像素光照计算所需的复杂几何信息烘焙到补充纹理,从而使用更简单的实时模型。ASTC压缩格式包括了对压缩法线贴图的专门设计的压缩优化,因而可以优化带宽

        ▪ 更倾向于提升光照表现和纹理来提升最终的图像质量,而不是暴力增加几何数量。

        Don't

        ▪ 生成细节三角形

        Impact

        ▪ 对于tiled-based渲染器高精度的几何体会带来许多问题,包括着色器计算,内存带宽以及由于内存流量带来的系统功耗

        Debugging

        ▪ 跟踪总几何体数量并且检查像素数量,对于1080p渲染通道,实际上不需要超过250k的三角形(平均每个正面三角形16个片元)

        ▪ Bifrost GPU包含了一个硬件性能计数器,用于检查由于生成覆盖范围不足被杀死的微三角形;如果这个数量超过了特定比例,请检查网格对象,并考虑在运行时使用更积极的LOD选择。

Instanced vertex buffers

        OpenGL和Vulkan都支持了实例化绘制,使用属性实例数来决定如何划分缓冲区以定位每个实例的数据。有一些硬件限制和实例化顶点缓冲区的正确使用相关。

        Do

        ▪ 对所有实例数据使用单一交错的缓冲区

        ▪ 使用实例缓冲区来解决Uniform buffer需要小于16kb的限制

        ▪ 如果可行的话,使用二的幂次方,即每个实例的顶点实例数据应该是2的幂次

        ▪ 如果无法遵循此处的建议,则首选使用gl_InstanceID对Uniform buffer或shader存储缓冲区进行索引查找

        ▪ 如果实例数据可以用较小的数据类型表示,则首选实例化属性,因为Uniform buffer和shader存储缓冲区无法利用这些更密集的数据类型

        Don't

        ▪ 使用超过一个类型为Instance的顶点缓冲区

        Impact

        ▪ 受影响的实例drawcal会降低性能

Tiling

Effective triangulation

        Tiling和光栅化都在比单个像素更大的片元patches上工作;eg.对于Mali GPUs而言,tiling会使用至少16x16像素大小的bins,片元光栅化需要使用2x2个像素小大用于片元着色。一个最佳实践是使用最少的三角形尽可能覆盖到所需的像素,尤其是提高三角形面积和边长的比率。

        Do

        ▪ 尽可能使用接近等边的三角形;这提升了面积和变长的比率,减少了占用率不足的片元四边形生成的数量。

        Don't

        ▪ 由于三角形在2x2像素的片元四边形中采样不足导致了上升的片元着色消耗

        Debugging

        ▪ Mali图形调试器包含了模型可视化工具,允许可视化被提交的物件在物体空间的轮廓。

Fragment shading

Efficient render passes

        Tile-based的渲染作用在渲染pass上;每个渲染pass都有明确的开始和结束,并且仅在pass结束的时候在内存中产生输出。pass开始的时候对应于GPU中tiled内存的初始化,pass结束对应于将输出写回主内存。所有中间帧的帧缓冲区工作状态都完全存在于图块内存中,并在主内存中不可见。

        Vulkan渲染pss是API中显式概念,并且定义了loadOp操作 - 它定义了Mali GPUs在pass开始的时候如何初始化tile内存 - 以及storeOp操作 - 它定义了在pass的结束什么将被写回到主存。此外,Vulkan引入了延迟分配内存(lazily allocated memory)的概念,这意味着临时的attachment仅在单个渲染pass期间存在,并不会实际分配物理存储。

        Do

        ▪ 在一个渲染pass开始的时候使用loadOp = LOAD_OP_CLEAR或loadOp = LOAD_OP_DONT_CARE来清除或使attachment失效

        ▪ 对那些只在一个渲染pass内生效的attachment使用TRANSIENT_ATTACHMENT标记,使用LAZILY_ALLOCATED内存,并确保在渲染pass结束的时候使用storeOp = STORE_OP_DONT_CARE使内容失效。

        Don't

        ▪ 在renderpass内部使用的图像使用vkCmdClearColorImage()或vkCmdClearDepthStencilImage();将清除转移到loadOp设置上

        ▪ 在渲染pass内部使用vkCmdClearAttachments()来清除attachment;这并不是免费的,不想清除或失效的加载操作

        ▪ 在shader代码中通过写入一个常量的颜色来清除一个渲染pass

        ▪ 使用loadOp=LOAD_OP_LOAD,除非你的算法确实需要framebuffer的初始状态

        ▪ 为attachment设置渲染pass实际上不需要的loadOp和storeOp; 你将会为attachment生成一些不必要的tile内存的交互

        ▪ 在需要直接渲染UI/HUD的时候,设置loadOp=LOAD_OP_LOAD,并使用vkCmdBlitImage来实现低分辨率到原始分辨率的缩放,这会带来不必要的内存交互

        Impact

        ▪ 正确的处理渲染pass是非常关键的;如果不遵循这个建议的话可能会带来显著的片元着色性能下降,以及由于在渲染开始的时候需要读取未清除的attchment到tile内存,并且在渲染结束的时候写出未失效的attachment,带来的内存带宽的增加。

        Debugging

        ▪ 浏览渲染pass创建的API使用,以及任意vkCmdClearColorImage(), vkCmdClearDepthStencilImage()和vkCmdClearAttachments()的使用

Multisampling

        多重采样的大多数用处,是将额外采样的数据保存在GPU内部的tile内存中,并且将该值解析为单个像素颜色,作为图块最终写入的一部分。这意味着这些额外采样带来的额外带宽永远不会影响到外部存储器,这使得它非常高效。

        多重采样可以完全集成到Vulkan的渲染pass中,允许在subpass结束的时候明确指明使用多重采样解析。

        Do

        ▪ 尽可能的使用4x MSAA;它并不昂贵并能提供较好的图像质量提升

        ▪ 对多采样图像使用loadOp = LOAD_OP_CLEAR或loadOp = LOAD_OP_DONT_CARE

        ▪ 在subpass中使用pResolveAttachments来自动从多采样颜色解析到单采样颜色缓冲区

        ▪ 对多采样图像使用storeOp = STORE_OP_DONT_CARE

        ▪ 对分配的多采样图像使用LAZILY_ALLOCATED内u才能,它们不需要持久保存到主存中,因此不需要物理分配

        Don't

        ▪ 使用vkCmdResolveImage();这对带宽和性能有显著的影响

        ▪ 对多采样图像attachment使用storeOp=STORE_OP_STORE

        ▪ 对多采样图像attachment使用storeOp=LOAD_OP_STORE

        ▪ 使用超过4x的MSAA而没有考量过性能,由于它不是完整的吞吐量

        Impact

        ▪ 未获得内敛解析可能会导致内存带宽的显著提升并降低性能;以60fps手动写入和解析4xMSAA1080p表面需要3.9GB/s的内存带宽,而使用内联解析时仅为500MB/s

Multipass rendering

        Vulkan的一个主要特性就是多pass渲染,它允许应用程序使用标准API来充分利用Tile-based架构的全部功能。Mali GPU能够从一个subpass中获取颜色和深度attachment,并将它们用作后续subpass的输入attachment,而无需通过主内存。这使得强大的算法如延迟着色或可编程混合能够广泛高效的使用,但需要正确设置一些东西。

        Per-pixel storage requirements

        大多数Mali Gpus设计为使用tile缓冲区颜色存储每个像素128-bit来渲染16x16的像素tiles,此外最近的一些GPUs比如Mali-G72将其增长为每个像素256-bits。G-buffer需要比这更多的颜色存储,这可以以片元着色器期间使用更小的tile作为代价来使用,可以降低整体的吞吐量并增加读取列表的带宽。

        例如,适用于128位带宽预算的G-Buffer布局可为:

        ▪ Light:B10G11R11_UFLOAT

        ▪ Albedo:RGBA8_UNORM

        ▪ Normal:RGB10A2_UNORM

        ▪ PBR material parameters/misc:RGBA8_UNORM

        Image layouts

        多pass渲染是图像布局能够产生影响的例子,因为它对允许驱动程序启用的优化有重大影响。这是一个包含所有良好路径的示例多pass布局:

        Initial layouts:

        ▪ Light : UNDEFINED

        ▪ Albedo:UNDEFINED

        ▪ Normal:UNDEFINED

        ▪ PBR:UNDEFINED

        ▪ Depth:UNDEFINED

        G-buffer pass(subpass#0) output attachments:

        ▪ Light : COLOR_ATTACHMENT_OPTIMAL

        ▪ Albedo:COLOR_ATTACHMENT_OPTIMAL

        ▪ Normal:COLOR_ATTACHMENT_OPTIMAL

        ▪ PBR:COLOR_ATTACHMENT_OPTIMAL

        ▪ Depth:DEPTH_STENCIL_ATTACHMENT_OPTIMAL

        注意light attachment,它会作为我们最终的输出,因此应该作为VkRenderPass的attachment#0,这样它会占据渲染目标的第一个slot,硬件获取的性能更好。我们将light作为G-buffer pass的输出是因为渲染器可能需要输出一些不透明物件的发光参数。由于我们会合并subpasses因为不会有写出到渲染目标的额外带宽,因此-不像桌面端-我们不需要开发一些特别的方案来通过其它G-Buffer缓冲区转发自发光的贡献。

        Lighting pass(subpass#1)input attachments:

        ▪ Albedo:SHADER_READ_ONLY_OPTIMAL

        ▪ Normal:SHADER_READ_ONLY_OPTIMAL

        ▪ PBR:SHADER_READ_ONLY_OPTIMAL

        ▪ Depth:DEPTH_STENCIL_READ_ONLY

        这里重要的一点是,一旦任一pass开始从tile缓冲区读取,任何depth/stencil attachment之后将被标记为read-only。这允许了大幅提升多pass性能的优化;DEPTH_STENCIL_READ_ONLY为如下用例设计,只读的depth/stencil测试,同时也将其用作着色器的输入attachment,用程序方式访问深度值。

        Lighting pass(subpass #1)output attachments:

        ▪ Light:COLOR_ATTACHMENT_OPTIMAL
        在subpass#1期间的光照计算的混合应该在subpass#0期间提供的计算后的发光数据之上进行。如果需要的话,应用程序还可以在所有光照计算完成后将透明对象混合。

        Subpass denpendencies

        在pass之间的subpass依赖必须使用设置了DEPENDENCY_BY_REGION_BIT标记的VkSubpassDependency,这告诉驱动每个subpass仅在该像素坐标处依赖于前一个subpass,因此保证不会从先前的subpass读取出tile外的数据。

        根据我们描述的例子,subpass依赖设置可能为:

VkSubpassDependency subpassDependency = ;

subpassDependency.srcSubpass=0;

subpassDependency.dstSubpass=1;

subpassDependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |

VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |

VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT

subpassDependency.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |

VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT|

VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |

VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;

subpassDependency.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT|

VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT

subpassDependency.dstAccessMask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT|

VK_ACCESS_COLOR_ATTACHMENT_READ_BIT|

VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT|

VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT|

VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;

subpassDependency.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;

        Subpass merge considerations

        驱动会合并subpasses,如果它们满足以下条件:

        ▪ 颜色缓冲区数据格式可以合并

        ▪ 合并可以节约写出/回读,两个不相关的不共享任何数据的subpasses不会从多pass受益,因此不会合并

        ▪ 如果在所有subpass中用于输入和颜色attachments的VkAttachments数量<=8,注意depth/stencil不受该限制

        ▪ depth/stencil attachment在subpasses之间未发生改变

        ▪ 多采样数量对每个attachments都是相同的

        Do

        ▪ 使用多pass

        ▪ 对颜色使用128-bit G-Buffer预算

        ▪ 在subpasses之间使用区域依赖

        ▪ 对深度使用DEPTH_STENCIL_READ_ONLY的图像布局,在G-Buffer pass完成后

        ▪ 对使用LAZILY_ALLOCATED内存为每个attachment备份图像,除了lighting buffer,它是唯一写入内存的纹理

        ▪ 遵循以下渲染pass最佳实践:对加载的attachments使用LOAD_OP_CLEAR或LOAD_OP_DONT_CARE,对短暂的存储使用STORE_OP_DONT_CARE

        Don't

        ▪ 将G-buffer存储到内存

        Impact

        ▪ 未正确的使用多pass会导致驱动使用多个物理passes,将pass之间的及时图像数据传输到内存中。这就丢失了多pass渲染特性的所有优势。

        Debugging

        ▪ GPU性能计数器提供了渲染的物理tiles的数量信息,这可用于确定哪些pass在被合并

        ▪ GPU性能计数器提供了使用late-z测试的片元线程数量,如果值比较高说明未正确地设置DEPTH_STENCIL_READ_ONLY。

HDR rendering

        对于移动端内容,渲染HDR图像相关的格式包括:RGB10_A2(unform),B10R11G11和RGBA16F(float)

        Do

        ▪ 如果需要让动态范围得到小的提升,更倾向于使用RGB10_A2 UNORM的格式

        ▪ 如果仅有32bpp,更倾向于使用B10G10R11,相比起f16 float,这可以节省大量带宽,并且可以维持G-Buffer带宽在128bpp内

        Don't

      ▪ 使用RGBA16,除非B10G11R11表现得不够好,或者实际上需要framebuffer中的alpha值

        Impact

      ▪ 增加带宽并减低性能

      ▪ 对于多pass渲染适配到128bpp比较困难

Stencil updates

        许多模板的掩码算法在创建使用模板时,会使用比较小的值来修改模板值(比如0或1)。当设计使用多个drawcall完成的模板掩码算法时,应该尽可能最小化模板更新的次数。

        Do

        ▪ 当已经值是相等的时候,使用KEEP而不是REPLACE

        ▪ 在执行成对绘制的算法中 - 一个用于创建模板掩码,另一个用于对未掩码的片元着色 - 使用第二次绘制重置模板值,为下一次绘制做好准备,避免需要单独做中途清除操作。

        Don't

        ▪ 写入一个不需要的新模板值

        Impact

        ▪ 写入模板值的片元不能被快速丢弃,这会引入额外的片元处理成本

Blending

        在Mali GPUs中混合通常都是非常高效的,因为我们可以在on-chip tile缓冲区上直接访问到dstColor,但是对于特定格式来说可能会更加高效。

        Do

        ▪ 更倾向于使用无符号归一化的格式(unorm)

        ▪ 物体是不透明的时候总是禁用混合

        ▪ 关注逐像素混合的层数;高层数会带来需要计算的片元数量的增加,哪怕着色器非常简单

        ▪ 考虑将大的包含透明和半透明区域的UI元素拆分为透明和不透明两个部分,来确保early-zs/FPK能够移除不透明部分的过度绘制(overdraw)

        Don't

        ▪ 对浮点格式的帧缓冲区执行混合

        ▪ 对多重采样浮点格式的帧缓冲区执行混合

        ▪ 通用的用户渲染代码接口,总是开启混合,并通过将alpha设为1.0来"禁用"混合

        Impact

        ▪ 混合导致很多移除片元过度绘制的优化失效了,比如early-z测试和FPK,所以它对性能有显著的影响。对于用户界面渲染和2D游戏尤其如此,它们通常会有多层sprites。

        Debugging

        ▪ 使用Mali图形调试器来单独调试每帧的组成,观察哪些drawcall包含了混合并且带来了多少overdraw

Transaction elimination

        Transaction elimination是Mali的一项技术,当tile中的渲染颜色和内存中的数据一致时,避免产生写入到帧缓冲区的带宽,仅当tile与内存中的签名对比失败后才从tile中写出。该技术对于包含大量静态不透明覆盖层的用户界面以及游戏特别有帮助。

        Transcation elimination可作用于如下图像:

        ▪ 采样数量为1

        ▪ mipmap层级为1

        ▪ 图像属性包括COLOR_ATTACHMENT_BIT

        ▪ 图像属性不包括TRANSIENT_ATTACHMENT_BITT

        ▪ 使用单个颜色附件(除了Mali-G51)

        ▪ 最高效的tile大小,由像素数据存储要求决定,为16x16像素

        Transaction elimination是为数不多的驱动会关注图像布局的例子之一。当图像从”安全“转换到”不安全”布局时,Transaction elimination标记的缓冲区必须失效。“安全"的图像布局被认为是哪些只读或者只能在片元着色中被写入的图像布局,这些布局为:

        ▪ VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL

        ▪ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL

        ▪ VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL

        ▪ VK_IMAGE_LAYOUT_PRESENT_SRC_KHR

        所有其它的布局都被认为是”不安全“的,因为它们可以被写入到tile写入路径之外的图像,这意味着签名元数据和实际颜色数据可能不同步。如果颜色attachment的布局和最终布局不同,签名缓冲区失效可能作为vkImageMemoryBarrier, vkCmdPipelineBarrier(),vkCmdWaitEvents()的一部分或作为vkRenderPass的一部分发生。

        从UNDEFINED布局转换保留了图像的内存内容,因此从UNDEFINED布局转换本身不会让签名缓冲区失效,除非其它的触发效果需要它。

        Do

        ▪ 对颜色attachment使用COLOR_ATTACHMENT_OPTIMAL图像布局

        ▪ 尝试对颜色attachment使用”安全“图像布局来避免签名失效

        Don't

        ▪ 将颜色attachment从”安全“转换到”不安全“,除非算法需要这么做

        Impact

        ▪ Transaction elimination失效将增加跨帧的静态区域场景的外部存储器带宽。这可能会降低内存带宽受限的系统的性能。

        Debugging

        ▪  GPU性能计数器可以统计由于transaction elimination剔除的tile数量,因此你可以确定它是否被触发

Buffers

Robust buffer access

        Vulkan设备支持robustBufferAccess的特性。当开启这一特性的时候,会向GPU缓冲区内存访问添加边界检查,确保不会出现越界访问。Mali GPU的边界检查不总是免费的;开启这一特性可能会带来性能的损失,尤其是对uniform缓冲区和着色器存储缓冲区的访问。

        Do

        ▪ 使用robustBufferAccess作为开发期间的调试工具

        ▪ 当发行编译版本中禁用robustBufferAccess,除非应用用例中由于使用了用户提供的绘制参数,而确实需要额外级别的可靠性保证。

        ▪ 如果需要robustBufferAccess,为了Uniform buffer的性能使用push constants的方式。这样降低了每帧访问缓冲区的次数,这样需要的边界检查次数会变少。

        Don't

        ▪ 开启robustBufferAccess,除非确实需要

        ▪ 开启robustBufferAccess,而没有关注性能影响

        Impact

        ▪ 使用robustBufferAccess可能带来能够可衡量的性能损失

        Debugging

        ▪ 通过对比两次跑测来验证性能影响 - 一个开启robustBufferAccess另一个关闭。

        ▪ robustBufferAccess特性是开发期非常有用的调试工具。如果你的应用存在崩溃或返回了DEVICE_LOST的错误,开启robust accesst特性,并观察问题是否结束了。如果不再有问题的话,说明有些drawcall或compute dispatch发生了越界访问。

Textures

Sampling performance

        Mali GPU纹理单元可以花费可变的周期对纹理进行采样,具体取决于纹理格式和过滤模式。纹理单元的设计旨在为nearest和bilinear过滤(LINEAR_MIPMAP_NEAREST)提供全速的性能。

        忽略数据缓存效果,需要额外周期的例子如下:

        ▪ 三线性(LINEAR_MIAPMAP_LINEAR)过滤:2x 消耗

        ▪ 3D格式:2x 消耗

        ▪ FP32格式:2x 消耗

        ▪ 深度格式:2x 消耗(Utgard/Midgard), 1x 消耗(Bifrost)

        ▪ 立方体贴图格式:每个面的访问 1x 消耗

        ▪ YUV格式:Nx 消耗,其中N表示旧的Mali GPU的纹理平面数量。第二代Bifrost架构内核(Mali-G51后)YUV的消耗是1x,与平面数量无关

        举例来说,一个三线性过滤的RGBA8 3D纹理访问时间是线性过滤的2D RGBA纹理的4倍。

        Do以上是关于Arm Mali GPU最佳实践(Arm Mali GPU Best Practices)的主要内容,如果未能解决你的问题,请参考以下文章

安卓平台下ARM Mali OpenCL编程-GPU信息检测(转)

嵌入式图形解决方案升级!RT-Thread Smart成功支持ARM Mali GPU

嵌入式图形解决方案升级!RT-Thread Smart成功支持ARM Mali GPU

现在GPU是否有4核这个说法?-GPU Quad Mali 400 .是GPU4核的意思吧?

mali开发板

Android GPU Inspector