资讯专栏INFORMATION COLUMN

剖析虚幻渲染体系(12)- 移动端专题Part 3(渲染优化)

defcon / 1087人阅读

摘要:管线优化管线优化曲面细分期间消除子像素。然而,高级别的曲面细分可以产生子像素三角形,这导致光栅化利用率降低。另外,如果合并或批处理之后的物体包围盒过大,反而会造成性能下降,因为无法有效使用遮挡剔除等技术进行剔除。

 

目录
  • 12.6 移动端渲染优化
    • 12.6.1 渲染管线优化
      • 12.6.1.1 使用新特性
      • 12.6.1.2 管线优化
      • 12.6.1.3 带宽优化
    • 12.6.2 资源优化
      • 12.6.2.1 纹理优化
      • 12.6.2.2 顶点优化
      • 12.6.2.3 网格优化
    • 12.6.3 Shader优化
      • 12.6.3.1 语句优化
      • 12.6.3.2 状态优化
      • 12.6.3.3 汇编级优化
    • 12.6.4 综合优化
      • 12.6.4.1 光影优化
      • 12.6.4.2 后处理优化
      • 12.6.4.3 精灵渲染优化
      • 12.6.4.4 均衡GPU工作负载
      • 12.6.4.5 Compute Shader优化
      • 12.6.4.6 多核并行
      • 12.6.4.7 其它综合优化
    • 12.6.5 XR优化
      • 12.6.5.1 注视点渲染(Foveated Rendering)
      • 12.6.5.2 多视图(Multiview)
      • 12.6.5.3 立体渲染(Stereo Rendering)
      • 12.6.5.4 隐藏延时
      • 12.6.5.5 制定技术规格
      • 12.6.5.6 其它XR优化
    • 12.6.6 调试工具
  • 12.7 本篇总结
    • 12.7.1 本篇思考
  • 团队招员
  • 特别说明
  • 参考文献

 

 

 

12.6 移动端渲染优化

前面几章详尽地剖析了移动端GPU架构的特性和机理,那么就可以指导我们抽象出一些准则,从而获得高性能的渲染代码和应用程序。

为了获得流畅、高效、良好体验,每个应用程序都必须重视性能优化,并贯穿始终。应用程序的性能优化分为以下三角循环:

第一步,分析应用程序的整体性能。

第二步,利用工具定位出性能瓶颈。

第三步,修改应用程序。回到第一步递归分析。

这个三角循环什么时候停止呢?那就是应用程序的性能已经达到了项目之初指定的标准(如高中低画质不低于多少帧,DC、三角面数小于多少等),并且已经知道应用程序已经达到了效率极限,再往下便到了投入产出比很小的牛角尖。

本篇会涉及以下概念:

名称 别名 描述
USC (Unified Shading Cluster) Shading Cluster, Shading Unit, Execution Unit 图形核心的半自主部分,通常可以执行整个工作组。其他大型部件如纹理单位(Texture Unit)可以在USC之间共享。
Core Processor, Graphics Core 图形核心的一个几乎完全自主的部分。通常情况下,是USC的集合以及可能支持的硬件,如纹理单元。
Task Thread Group, Warp, Wavefront USC执行的线程的原生分组,PowerVR Rogue内核由32个线程组成。
shared Shared variables 存储于Shared memory的变量。
const / uniform const / uniform变量, uniform块, uniform缓冲区 存储于Constant memory的变量、块、缓冲区。

补充一下PowerVR Rogue硬件架构和数据流交互图,如下所示:

PowerVR%20Rogue的Unified%20Shading%20Cluster(USC)如下所示:

另外,补充一下本章大量涉及的片元(fragment)的概念:

片元(fragment)是GPU内部的几何体光栅化后形成的最小表示单元,它经过一系列片元操作(alpha测试,深度测试,模板测试等)后,才可能最终写入渲染纹理成为像素(pixel)。所以,片元不是像素,但有概率成为像素。

不过在D3D或UE内部,没有片元的概念,像素包含了片元。

12.6.1%20渲染管线优化 12.6.1.1%20使用新特性
  • Variable%20Rate%20Shading

Variable%20Rate%20Shading(VRS,可变率着色)允许像素着色器一次着色一个或多个像素,这样一个着色计算可以代表一个像素或一组像素。VRS是反锯齿技术的逆解。抗锯齿技术通过平滑高变化的内容,更频繁地采样每个像素,以避免走样(aliasing)和锯齿(jagged)边缘。然而,如果要渲染的表面没有高的颜色变化或将在随后的通道上被模糊(例如,运动模糊),在每个像素都一个着色计算的操作通常是低效的。

VRS允许开发者指定着色率,其中只对一个像素执行一个着色器计算,结果操作应用于指定的像素组配置。如果使用得当,应该不会导致视觉质量下降,同时显著减轻GPU渲染的负担,从而节省功耗并提高性能。

VRS示意图。画面根据颜色变化频率采用不同的着色率,变化高的采用高着色率(如汽车),反之用低着色率(如左下和右下路面)。

VRS支持的常见着色率和运行机制。其中黄点是着色坐标,绿点是直接复用黄点的着色结果。

VRS在渲染管线的工作机制。VRS在光栅化阶段采用指定着色率执行光栅化,进入PS之后再放大。

UE可以给每个材质设定1个着色率,在材质属性模板中:

VRS优化的核心思想在于减少计算次数并复用周边计算点的结果,从而达到提升渲染效率的目的。适合使用VRS的情形:

  • 颜色变化率低的物体。
  • 处于运动模糊区域的物体。
  • 景深范围之外的物体。

使用移动端的VRS需要依赖不同图形API的扩展:

// ------ OpenGLES ------// Qualcomm QCOM_shading_rateGL_SHADING_RATE_1X1_PIXELS_QCOMGL_SHADING_RATE_1X2_PIXELS_QCOM......// Arm / Imagination Tech(不支持)// ------ Vulkan ------VK_KHR_fragment_shading_rate
  • 使用Vulkan代替OpenGL。

相比OpenGL等传统API,Vulkan支持多线程,轻量化驱动层,可以精确地管控GPU内存、同步等资源,避免运行时校验,基于命令队列的机制,没有全局状态等等(下图)。

得益于Vulkan的先进设计理念,使得它的渲染性能更高,通常在CPU、GPU、带宽、能耗等指标都优于OpenGL。但如果是应用程序本身的CPU或者GPU负载高,则使用Vulkan的收益可能没有那么明显:

  • 使用遮挡剔除。

遮挡剔除可以提前剔除掉被遮挡的物体或者远处占屏幕很小的物体,避免进入GPU管线,占用带宽和计算资源。UE在移动端的遮挡剔除延迟了两帧(因为BasePass结束之后才有深度缓冲,需要再增加一帧延迟确保结果可用):

然后是在RHI线程等待遮挡查询的结果,遮挡查询的结果是在渲染线程使用,由于延迟了两帧,所以渲染线程在计算可见性时不需要等待:

使用遮挡剔除时,需要遵循以下建议:

1、只在需要时返回查询结果,不要等待它,因为同步等待是非常低效的。

2、对于遮挡,只在必要时使用精确计数选项。OpenGL ES使用GL_ANY_SAMPLES_PASSED,、Vulkan使用VK_QUERY_CONTROL_PRECISE_BIT = false,除非确实需要知道遮挡的数量。

3、不要修改正在绘制调用中引用的资源。

4、不要将GL_MAP_INVALIDATE_BUFFER /GL_MAP_INVALIDATE_RANGE与glMapBufferRange()一起使用,因为这些标志在某些版本的驱动会触发创建一个不必要的资源拷贝。

12.6.1.2 管线优化

  • 曲面细分期间消除子像素。

曲面细分增加细节级别,并可以通过允许其他游戏子系统在低分辨率的网格表示上操作来减少内存带宽和CPU周期。然而,高级别的曲面细分可以产生子像素三角形,这导致光栅化利用率降低。利用距离、屏幕空间大小或其他自适应度量来计算避免子像素三角形的曲面细分因子是很重要的。

  • 曲面细分期间开启背面剔除。

图元的背面剔除可以防止冗余的像素进入像素着色器中,从而提升性能。

  • 删除未使用的render target或shader资源。

操作更多的RT或shader资源,会增加带宽,降低性能。故而尽量删除未引用的资源。

  • 避免GMEM加载。

在每个Pass渲染之前,需要调用图形API明确清理RT。

OpenGL ES: glClear()

Vulkan: LOAD_OP_CLEAR / LOAD_OP_DONT_CARE

  • 使用subpass或PLS。

Vulkan的subpass(或OpenGL ES的PLS)可以让多个pass的数据持续保存在GMEM(Tile缓冲区)中,避免数据反复从GMEM和全局内存之间传输,从而降低带宽和延时。

  • 使用PSO缓存。运行时创建PSO对象比较消耗CPU性能,如果在离线阶段收集、编译材质使用的Shader并保存成二进制文件,以便下次运行时调用时直接读取Cache文件并转成PSO对象,可以降低CPU负载。下图是UE的PSO缓存机制图示:

更多详情请参看UE官方文档:PSO%20Caching。

  • 使用仅深度(Z-only)渲染。

GPU有一种特殊的模式,可以以两倍于正常模式的速率写入Z-only像素,例如应用程序渲染阴影图。

有两种方式可以让GPU进入此模式:

1、图形API明确指示,硬件才能进入这个特殊的渲染模式。

2、应用程序通过特定的渲染状态提示驱动程序。比如:使用一个空的片元着色器和禁用Frame%20Buffer(帧缓冲区)写掩码。

一些渲染程序或引擎(如UE)会使用专用的PrePass来渲染深度,以充分利用Early-Z计算。不过对于移动端GPU需要谨慎对待,应以实际测试为准。

  • 使用间接索引(indirect%20indexed)的绘制接口。

间接绘制调用将开销从CPU转移到GPU,从而减少CPU和GPU的带宽。例如,在加载时缓存绘制调用参数,以便在缓冲对象存储中渲染网格。这些缓存数据可以作为glDrawArraysIndirect%20或glDrawElementsIndirect的输入参数。

需要OpenGL%20ES%203.1才支持。

  • Draw%20Call优化。
    • 合并几何物体,同时合并它们的材质。
    • 使用批处理,即便不是CPU受限,也可以减少能耗。
    • 使用实例化(instance)。
    • 使用非直接索引绘制。
    • 避免多次绘制小量物体。
    • 根据高中低画质设定合理的Draw%20Call数量。

使用批处理时,要注意顶点总数限制,不能超过索引的表达范围(通常最大是65k)。另外,如果合并或批处理之后的物体包围盒过大,反而会造成性能下降,因为无法有效使用Frustum%20Cullinig、遮挡剔除等技术进行剔除。

另外,需要注意提交的几何物体具有相邻性,尽量落在同一个Tile内,以减少覆盖的Tile数量,降低带宽,提升缓存命中率:

上:良好的几何物体提交顺序;下:错误的几何物体提交顺序。

  • 禁用Alpha Test / Discard。

Alpha Test会打乱TBR的正常流程,造成渲染管线Stall,在PowerVR尤为明显(Alpha Test阶段会写回深度到HSR阶段)。

因为TB(D)R在渲染不透明物体时普遍开启了Early-Z技术和特殊的隐藏面消除技术(HSR、FPK),在此阶段会开启深度测试,并写入通过了深度测试的片元深度。但是,如果开启了Alpha Test或Shader中使用了Discard,无法在Early-Z/隐藏面消除技术阶段就确定该片元的深度是否有效,必须等执行完PS、Alpha Test等阶段才行:

这样就无法充分发挥HSR技术的优势,从而降低渲染性能。

可以使用Alpha Blend代替Alpha Test。如果确实需要Alpha Test,则物体的渲染顺序需尊照此顺序:Opaque -> Alpha-tested -> Blended。

  • 尽量减少Alpha Blend。

原因是延迟渲染器,比如PowerVR GPU,在片元着色器处理它之前计算片元的可见性,防止输出图像中的不可见片元被不必要地处理。如果需要透明对象,请尽量减少透明对象的数量。

由于Alpha Blend不能写入深度,不能充分利用HSR/FPK,会引发Overdraw,提升带宽和数据传输量。

如果确实需要,有以下优化建议:

1、优先使用unorm格式,而不是浮点数。(注意:此条来自Arm Mali的建议,其它GPU可能不一样,以实测为主)

2、如果是不透明物体,应禁用Blend和alpht to coverage。

3、不要在携带MSAA数据的浮点frame buffer上使用混合。

4、避免过高的OverDraw。监控每像素基础上生成的混合层数量,即使是简单的着色器,混合层数量高会因为片元数量多而快速消耗时钟周期。

5、考虑将大型UI元素分成不透明和透明部分。然后可以分别绘制不透明部分和透明部分,允许Early-ZS或FPK/HSR删除不透明部分下面的OverDraw。

6、不要仅仅在片元着色器中将alpha设置为1.0来禁用混合。

  • 充分利用Early-Z和FPK/HSR剔除被遮挡的像素。

为了充分利用Early-Z,物体绘制顺序应该如下所示:

1、绘制不透明物体。从前向后绘制。

2、绘制镂空(Masked)物体。从前向后绘制。

3、绘制半透明物体。从后向前绘制。

对于广泛支持TBR架构的移动端GPU,不建议开启Prepass绘制专用的深度,否则反而会增加带宽和Draw Call。

另外,在绘制不透明物体时,尽量做到以下几点:

1、禁用discard语句。

2、禁用Alpha to Coverage。

3、禁止在片元着色器中修改深度。

若是违反以上任意一条,便会使Early-Z失效,强制使用Late-Z,从而降低渲染效率。

  • 充分开启裁剪和测试。

裁剪技术包含遮挡剔除、视锥体裁剪、Scissor、距离裁剪、LOD等等。

测试包含背面测试、深度测试、模板测试等,但禁用透明度测试。

  • 禁用Z-Prepass。

移动端GPU基于TBR结构通常内置了像素级的剔除,无需再专门绘制一次深度。UE在移动端默认禁用了Z-Prepass。

  • 最小化模板缓冲的更新。

1、如果值相同,则使用KEEP而不是REPLACE。

2、有些渲染器(如UE)使用光照绘制Pass对(pair):第一个Pass用于创建模板缓冲,第二个Pass用于给未蒙版的片元着色。可以在第二个Pass重置模板值,以便为下一个光照配对做好准备,这样可以避免多带带的模板清理操作。

UE的移动端场景渲染器在绘制光照时正是使用了此种模板清理优化方式。

  • 正确调用图形API。

    • 除非达到了目标性能,否则不要以导致GPU空闲的方式使用API。

    • 不要过早等待渲染管线中的围栏(fence)和查询(query)对象的查询结果。

    • 调用glMapBufferRange()时使用GL_MAP_UNSYNCHRONIZED标记开启异步,防止渲染管线卡顿。

    • 避免同步方式调用以下接口:

      • glFlush()。但是,某些GPU(如PowerVR)由于使用了双缓冲机制,不会卡调用线程。
      • glFinish()
      • glReadPixels()
      • glWaitSync()
      • glClientWaitSync()
      • eglClientWaitSync()
      • 没有GL_MAP_UNSYNCHRONIZED标记的glMapBufferRange()

      避免不必要地调用以上接口,调用次数越少越好。

    • 避免使用glFlush()来分割渲染通道,因为驱动程序(Mali)会在需要时自动刷新。

    • 尽可能执行Clear。在绘制前或渲染通道开始时,使用glClear/glDiscardFramebufferEXT/glInvalidateFramebuffer执行渲染纹理的清理,防止GPU读取上一帧的数据到Tile缓冲区中,节省带宽。Vulkan则使用loadOp。

    • 尽可能使用glColorMask屏蔽不需要写入的颜色通道。

如果违反以上建议,有可能导致以下结果:

1、如果管道被耗尽,GPU在产生气泡期间部分空闲,导致性能损失。

2、根据与系统动态电压和频率缩放电源管理逻辑的相互作用,可能会有一些性能不稳定。

  • 优化Command Buffer。

1、要获得最佳性能,请设置ONE_TIME_SUBMIT_BIT标志。不要设置SIMULTANEOUS_USE_BIT,除非确实需要。

2、构建每帧命令缓冲区,而不是使用同步命令缓冲区。

3、如果替代方法是每次在应用程序逻辑中重放相同的命令序列,则使用SIMULTANEOUS_USE_BIT。它比应用程序手动重放命令更有效,但比一次性提交缓冲区更低效。

4、不要使用设置了RESET_COMMAND_BUFFER_BIT的命令池,会增加内存管理开销,因为驱动程序无法为池中的所有命令缓冲区使用单个大型分配器。

5、使用secondary command buffer来允许多线程渲染通道的构造。

6、最小化每帧secondary command buffer的调用次数。

  • 优化描述符集和布局(descriptor sets and layouts)。

1、尽可能多地打包描述符集绑定空间。

2、更新已经分配但不再引用的描述符集,而不是重置描述符池和重新分配新的描述符集。

3、重用预分配的描述符集,避免更新相同的信息。

4、使用VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC或VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC绑定相同的UBO或SSBO,但不同的偏移量。 另一种选择是构建更多的描述符集。

5、不要在描述符集中留下空白,会浪费空间,阻断访问连续性。

6、不要留下未使用的条目(entry),因为复制和合并依旧有消耗。

7、不要在性能关键的代码路径上从描述符池(descriptor pool)分配描述符集。

8、如果不打算更改绑定偏移量,就不要使用DYNAMIC_OFFSET UBOs/SSBOs,因为处理动态偏移量会有很小的额外成本。低效的描述符集和布局未优化的Vulkan描述符集和布局的负面影响可能会增加绘制调用的CPU消耗。

  • 避免渲染管线气泡(空闲)。

以下几种情况会产生渲染管线气泡:

1、Command Buffer提交不够频繁。不经常提交命令缓冲区会减少GPU处理队列中的工作量,限制潜在的编排机会。

2、数据依赖。假设有渲染通道M和N,M在稍后的阶段。当N在管道中被M更早地使用时,数据依赖就产生了。数据依赖会导致延迟,在此期间必须做足够的工作来隐藏结果生成中的延迟。

渲染管线气泡示意图。图中显示CPU、VS、PS都存在气泡。

以下建议可以减少管线气泡:

1、频繁地提交Command Buffer。例如,为帧中的每个主要渲染通道之后,但渲染通道期间不宜提交。

2、如果某些情况导致了气泡,尝试填充气泡技术。例如,通过在两个渲染通道之间插入独立的工作负载。

3、考虑在比使用依赖数据的阶段更早的管道阶段生成依赖数据。例如,计算(compute)阶段适合为顶点着色阶段生成输入数据。而片元阶段是不合适的,因为它的执行晚于顶点着色阶段管道,否则会造成卡顿和延时。

4、考虑在管道中的更后阶段处理依赖数据。例如,片元着色使用来自其他片元着色的输出比计算着色使用片元着色更好。

5、使用栅栏异步地将GPU的数据读回CPU。千万不用同步地调用从GPU读取数据到CPU的接口,否则整个渲染管线将可能发生严重停滞。

此外,以下建议可以优化渲染管线:

1、不要在管道的任何地方不必要地等待GPU数据。

2、不要等到帧结束才提交所有的渲染通道。

3、在没有足够的中间工作来隐藏延迟的情况下,不要在管道中创建任何逆向(backwards)的数据依赖。

4、不要使用vkQueueWaitIdle()或vkDeviceWaitIdle()。

  • 正确使用管线同步。

现代图形API(如Vulkan)拥有非常细粒度的管线阶段:

typedef enum VkPipelineStageFlagBits{    VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT = 0x00000001,        // Vertex Stages    VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT = 0x00000002,    VK_PIPELINE_STAGE_VERTEX_INPUT_BIT = 0x00000004,    VK_PIPELINE_STAGE_VERTEX_SHADER_BIT = 0x00000008,    VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT = 0x00000010,    VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT = 0x00000020,    VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT = 0x00000040,    // Fragment Stages    VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT = 0x00000080,    VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT = 0x00000100,    VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT = 0x00000200,    VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT = 0x00000400,    // Compute Stages    VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT = 0x00000800,    VK_PIPELINE_STAGE_TRANSFER_BIT = 0x00001000,    VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT = 0x00002000,        VK_PIPELINE_STAGE_HOST_BIT = 0x00004000,    VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT = 0x00008000,    VK_PIPELINE_STAGE_ALL_COMMANDS_BIT = 0x00010000,        (......)} VkPipelineStageFlagBits;

现代图形API(如Vulkan)也包含了众多同步对象:

1、Subpass依赖、Pipeline Barrier、Event等,用于单个Queue内的精细粒度同步。

2、Semaphore(信号)用于跨Queue的较重度的依赖关系。

管线依赖存在两个变量:srcStagedstStagesrcStage标明必须等待的管线阶段(pipeline stage),dstStage标明在处理开始之前必须等待同步的管线阶段。

为了更好的并行效率和更少的管线气泡,srcStage越早越好,而dstStage越迟越好。如果srcStageVK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT时,将获得最差的性能。

Semaphore可以使用pWaitDstStages指定具体的阶段。

更具体地说,遵循以下准则,可以获得更好的渲染效率:

1、srcStageMask被设置得越早越好。

2、dstStageMask被设置得越晚越好。

3、检查依赖关系是向前的(比如srcStageMask是顶点或计算,dstStageMask是片元)还是向后的(如srcStageMask是片元,dstStageMask是顶点或计算)。 尽量减少使用向后依赖关系。

4、如果确实需要向后依赖,则在生成和消费资源之间添加足够的延迟,以便隐藏向后依赖引起的调度气泡。

5、使用srcStageMask = ALL_GRAPHICS_BIT 和 dstStageMask = FRAGMENT_SHADER_BIT 彼此同步两个渲染通道。

6、零拷贝(Zero-copy)算法是最有效的,因此尽量减少TRANSFER拷贝操作的使用。密切关注TRANSFER副本对硬件流水线的影响。

7、只在需要时使用队列内屏障(intra-queue barrier),并在屏障之间尽可能多地安排工作。

8、不要让硬件处于空闲状态。

9、不要忘记重叠顶点/计算和片元之间的处理。

10、不要使用下面的srcStageMask到dstStageMask同步组合,因为它们会完全耗尽管道:

BOTTOM_OF_PIPE_BIT to TOP_OF_PIPE_BITALL_GRAPHICS_BIT to ALL_GRAPHICS_BITALL_COMMANDS_BIT to ALL_COMMANDS_BIT

11、如果合并管道屏障,请注意不要引入错误的依赖项。确保不打破顶点/片元重叠,并创建一个不必要的气泡。

12、不要使用VkEvent信号并立即等待该事件,用vkCmdPipelineBarrier()。

13、不要在单个Queue中使用VkSemaphore进行依赖管理。

14、不要让渲染管线留有太大的空闲(否则降低性能),也不要让渲染管线留有太小的空闲(否则可能产生错误)。

  • 正确处理管线资源。

OpenGL ES为应用开发人员提供了一个同步呈现模型,即使底层的执行可能是异步的,必须反映数据资源在绘制调用时的状态。如果一个应用程序修改了一个资源,而一个挂起的draw调用仍在引用它,那么驱动程序必须采取规避操作来确保正确性。

驱动程序处理这些资源的同步行为时因GPU厂商而异,例如Mali驱动程序避免了阻塞和等待资源引用计数达到零,因为这样做会耗尽管道并导致性能低下。Mali GPU会创建一个全新版本的资源,资源的旧版本或幽灵(Ghost)版本将一直保留,直到挂起的绘制调用完成,其引用计数降至零。其它一些驱动程序(如PowerVR)会卡住本帧的渲染管线,延迟到下一帧处理,引发性能下降。

这种行为开销大,需要为新资源分配内存,并在完成时清理空资源。如果更新不是完全替换,还需要从旧的资源缓冲区复制到新的资源缓冲区。

为了优化资源,需要遵循以下建议:

1、避免修改已入队的draw call引用的资源,可以使用N-buffered资源,并通过管道进行动态资源更新。

2、使用GL_MAP_UNSYNCHRONIZED标记,以允许使用glMapBufferRange()来补齐缓冲区中仍被动态绘制调用引用的未引用区域。不要将GL_MAP_INVALIDATE_BUFFER /GL_MAP_INVALIDATE_RANGE与glMapBufferRange()一起使用,因为这些标志在某些版本的驱动会触发创建一个不必要的资源拷贝。

  • 高效地上传纹理资源。

上传纹理资源到到图形硬件时,对于非压缩纹理,按线性的扫描线上传,对于压缩的纹理,将会逐块上传。

部分GPU内部(如PowerVR)使用独特的布局来改善内存访问局部性和提高缓存效率。数据的重新格式化是由专用硬件在芯片上完成的,因此非常快。如果能遵循以下步骤更能提升性能:

1、在非性能关键时期上传纹理,如初始化。有助于避免与纹理加载相关的帧率下降。

2、避免上传帧期间(mid-frame)的纹理数据到已经用于该帧的纹理对象。

3、在纹理上传完成后执行一个预热(warm-up)步骤。依然是有助于避免与纹理加载相关的帧率下降。

前面提到的预热(warm-up)步骤可以确保纹理立即完全上传。默认情况下,glTexImage2D不会立即执行上传所需的所有处理,纹理是在第一次使用时完全上传的。可以通过在屏幕上画出一系列三角形或用有问题的纹理对象进行绑定处理来强制上传。

12.6.1.3 带宽优化

  • 注意数据的存放位置。如:RAM、VRAM、Tile Buffer、GPU Cache,减少不必要的数据传输。
  • 关注数据的访问类型。如:是只读还是只写操作,是否需要原子操作,是否需要缓存一致性。
  • 关注缓存数据的可行性,硬件可以缓存数据以供GPU后续操作快速访问。可以通过以下几点提升缓存命中率:
    • 提高传输速度,确保客户端顶点数据缓冲区被用于尽可能少的绘制调用。理想情况下,应用程序永远不应该使用它们。
    • 减少GPU在执行调度或绘制调用时需要访问的数据量。这样可以让尽量多的数据放到缓存行,提升命中率。
  • 使用纹理压缩格式。优先ASTC,其次是ETC、PVRTC、BC等压缩格式。GPU的硬件通常都支持这类压缩格式,可以快速地编解码它们,并且可以一次性读取更多的纹素内容到GPU的缓存行,提升缓存命中率。
  • 使用位数更少的像素格式。如RGB565比RGB888少8位,ASTC_6X6代替ASTC_4x4等。Adreno支持的像素格式参见Spec Sheets。
  • 使用半精度(如FP16)取代高精度(FP32)数据。如模型顶点和索引数据,并且可以使用SOA(Structure of Array)数据布局,而不用AOS。
  • 降分辨率渲染,后期再放大。可以减少带宽、计算量,减少设备热发热量。
  • 尽量减少绘制次数。绘制数量的减少可以减少CPU和GPU之间、GPU内部的带宽和消耗。
  • 确保数据存储在On-Chip内。

利用PLS、Subpass的特性,可以实现移动端的延迟渲染、粒子软混合等。下表是PowerVR GX6250在实现延迟渲染时,使用不同的位数和性能的关系:

配置 时间/帧(ms)
96bit + D32 20
128bit + D32 21
160bit + D32 23
192bit + D32 24
224bit + D32 28
256bit + D32 29
288bit + D32 39

以上可知,当位数大于256,超过GX6250的最大位数,数据无法完全存储在On-Chip内,会外溢到全局内存,导致每帧时间暴增10ms,增幅为34.5%。

因此,对每像素的数据进行精心的组装、优化和压缩,保持数据能够完全容纳于On-Chip内,可有效提升性能,节省带宽。

  • 避免多余的副本。

确保使用相同内存的硬件组件(CPU、图形核心、摄像机接口和视频解码器等)都访问相同的数据,而不需要进行任何中间复制。

  • 使用正确的标记创建Buffer、纹理等内存。部分Mali GPU(如Bifrost)执行以下几个标记组合:

1、DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT

2、DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_CACHED_BIT

3、DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT | HOST_CACHED_BIT

4、DEVICE_LOCAL_BIT | LAZILY_ALLOCATED_BIT

其中HOST_VISIBLE_BIT | HOST_COHERENT_BIT | HOST_CACHED_BIT的内存类型说明如下:

1、提供CPU上的缓存存储,与内存的GPU视图一致,无需手动同步。

2、如果芯片组支持CPU与GPU之间的硬件一致性协议,则该GPU支持此标记组合。

3、由于硬件的一致性,它避免了手动同步操作的开销。当可用时,缓存的、一致的内存优先于缓存的、不一致的内存类型。

4、必须用于CPU上的应用软件映射和读取的资源。

5、硬件一致性的功耗很小,所以不能用于CPU上只写的资源。对于只写资源,通过使用Not Cached,一致内存类型绕过CPU缓存。

关于LAZILY_ALLOCATED内存类型说明:

1、是一种特殊的内存类型,最初只支持GPU虚拟地址空间,而不是物理内存页面。如果访问内存,则根据需要分配物理页。

2、必须与使用VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT创建的瞬态attachment一起使用。瞬态Image的目的是用作帧缓冲attachment,只存在于一个单一的渲染过程中,可以避免使用物理内存。

3、不能将数据写回全局内存。

以下是Vulkan内存标记的使用建议:

1、对于不可变资源,使用HOST_VISIBLE | HOST_COHERENT内存。

2、对于CPU上只写的资源,使用HOST_VISIBLE | HOST_COHERENT内存。

3、使用memcpy()将更新写入HOST_VISIBLE | HOST_COHERENT内存,或者按顺序写入以获得CPU write-combine单元的最佳效率。

4、使用HOST_VISIBLE | HOST_COHERENT | HOST_CACHED内存用于将资源读回CPU,如果此组合不可以,则使用HOST_VISIBLE | HOST_CACHED。

5、使用LAZILY_ALLOCATED内存用于仅在单个渲染过程中存在的临时帧缓冲区附件。

6、只将LAZILY_ALLOCATED内存用于TRANSIENT_ATTACHMENT帧缓冲区附件。

7、映射和取消映射缓冲区消耗CPU性能。因此要持久地映射经常被访问的缓冲区,例如:统一缓冲区、数据缓冲区或动态顶点数据缓冲区。

  • 尽量使用零拷贝(Zero-Copy)路径。

如下图所示,通过使用EglImage实现Camera和OpenCL共享Original Image Data,OpenCL和OpenGL ES共享Final Image Data,从而达到零拷贝:

  • 将内存访问分组。

编译器使用几种启发式方法,可以识别内核中的内存访问模式,这些模式可以组合成读或写操作的突发传输。为了让编译器更好实现这种优化,内存访问应该尽可能紧密地组合在一起。

例如,将读放在内核的开头,写放在内核的结尾,可以获得最佳的效率。对更大的数据类型(如向量)的访问也会尽可能地编译为单个传输,加载1个float4比加载4个多带带的float值更好。

  • 合理使用Shared/Local内存。

可以在Shader初期(如初始化),将常访问的数据先读取到Shared/Local内存,提升访问速度。

  • 以行优先(Row-Major)的顺序访问内存。

GPU通常会预读取行相邻的数据到GPU缓存中,如果着色器算法以行优先的方式访问,可以提升Cache命中率,降低带宽。

  • GPU特定带宽优化。

Mali的Transaction%20elimination只有在以下情形适用:

1、采样数据为1。

2、mimap级别为1。

3、image使用了COLOR_ATTACHMENT_BIT。

4、image没有使用TRANSIENT_ATTACHMENT_BIT。

5、使用单一颜色附件。(Mali-G51%20GPU及之后没有此限制)

6、有效的tile尺寸是16x16像素,像素数据存储决定了有效的tile尺寸。

Mali%20GPU还支持AFBC纹理,可以减少显存和带宽。

12.6.2%20资源优化 12.6.2.1%20纹理优化
  • 使用压缩格式。

ASTC由于出色的压缩率,更接近原图的画质,适应更多平台而成为首选的纹理压缩格式。因此,只要可能,尽量使用ASTC。除非部分古老的设备,无法支持ASTC,才考虑使用ETC、PVRTC等纹理压缩格式。详见12.4.14%20Adaptive%20Scalable%20Texture%20Compression。

  • 尽量使用Mipmaps。

纹理Mipmaps提供提升内存占用来达到降低采样纹理时的数据量,从而降低带宽,提升缓冲命中率,同时还能提升画质效果。鱼和熊掌皆可得,何乐而不为?具体地说表现在以下方面:

1、极大地提高纹理缓存效率来提高图形渲染性能,特别是在强烈缩小的情况下,纹理数据更有可能装在Tile%20Memory。

2、通过减少不使用mipmapping的纹理采样不足而引起的走样来提高图像质量。

但是,使用Mipmaps会提升33%的内存占用。以下情况需要避免使用:

1、过滤不能被合理地应用,例如对于包含非图像数据的纹理(索引或深度纹理)。

2、永远不会缩小的纹理,比如UI元素,其中texel总是一对一地映射到像素。

  • 使用打包的图集。

打包图集之后,有可能合批渲染或实例化渲染,减少CPU和GPU的带宽。

  • 尺寸保持2的N次方。

尽管目前的图形API都已经支持非2N的次方尺寸(NPOT)的纹理,但有充分的理由建议保持纹理尺寸在2的N次方(POT):

1、在大多数情况下,POT纹理应该比NPOT纹理更受青睐,因为这为硬件和驱动程序的优化工作提供了最好的机会。(例如纹理压缩、Mimaps生成、缓存行对齐等)

2、2D应用程序应该不会因为使用NPOT纹理而出现性能损失(除非可能在上传时)。2D应用程序可以是浏览器或其他呈现UI元素的应用程序,其中NPOT纹理以一对一的texel到pixel映射显示。

3、保证长和宽都是32像素倍数的纹理,以便纹理上传可以让硬件优化。

  • 最小化纹理尺寸。
  • 最小化纹理位深。
  • 最小化纹理组件数量。
  • 利用纹理通道打包多张贴图。例如将材质的粗糙度、高光度、金属度、AO等贴图打包到同一张纹理的RGBA通道上。
12.6.2.2%20顶点优化
  • 使用分离位置的交错的顶点布局。原因详见12.4.11%20Index-Driven%20Vertex%20Shading。
  • 使用合适的顶点和索引存储格式。降低数据精度可以降低内存、带宽,提高计算单元运算量。目前主流移动端GPU支持的顶点格式有:
GL_BYTEGL_UNSIGNED_BYTEGL_SHORTGL_UNSIGNED_SHORTGL_FIXEDGL_FLOATGL_HALF_FLOATGL_INT_2_10_10_10_REVGL_UNSIGNED_INT_2_10_10_10_REV
  • 考虑几何物体实例化。现代移动端GPU普遍支持实例化渲染,通过提交少量的几何数据可以绘制多次,来降低带宽。每个实例允许拥有自己的数据,如颜色、变换矩阵、光照等。常用于树、草、建筑物、群兵等物体。

  • 图元类型使用三角形。现代GPU设计便是处理三角形,如果是四边形之类的很可能会降低效率。

  • 减少索引数组大小。如使用条带(strip)格式代替简单列表格式,使用原始的有效索引代替退化三角形。

  • 对于转换后缓存(%20post-transform%20cache),局部地优化索引。

  • 避免使用低空间一致性的索引缓冲区。会降低缓存命中率。

  • 使用实例属性来解决任何统一的缓冲区大小限制。%20例如,16KB的统一缓冲区。

  • 每个实例使用2的N次方个顶点。

  • 优先使用gl_InstanceID到统一缓冲区或着色器存储缓冲区的索引查找,而不是逐实例属性数据。

12.6.2.3%20网格优化
  • 使用LOD。

使用网格的LOD可以提升渲染性能和降低带宽。相反,不使用LOD,会造成性能瓶颈。

同个网格不同LOD的线框模式。

以下是浪费计算和内存资源的例子:

1、使用大量多边形的对象不会覆盖屏幕上的一个小区域,比如一个遥远的背景对象。

2、使用多边形的细节,将永远不会看到由于相机的角度或裁剪(如物体在视野锥之外)。

3、为对象使用大量的图元。实际上可以用更少的图元来绘制,还能保证视觉效果不损失。

  • 简化模型,合并顶点。通过合并相邻很近的顶点,可以有效减少网格顶点数量,利用网格简化技术,可以生成良好的LOD数据。

  • 离线合并靠在一起的小网格。如沙石、植被等。

  • 单个网格的顶点数不能超过65k。主要是移动端的顶点索引精度是16位,最大值是65535。

  • 删除看不见的图元。例如箱子内部的三角形。

  • 使用简单的几何物体,配合法线贴图、凹凸贴图增加细节。

  • 避免小面积的三角形。

Quad的绘制机制,会导致小面积的三角形极大提升OverDraw。在PowerVR硬件上,对于覆盖低于32个像素的三角形,会影响光栅化的效率,导致性能瓶颈。

提交许多小三角形可能会导致硬件在顶点阶段花费大量时间处理它们,此阶段主要影响因素是三角形的数量而不是大小。尤其会导致平铺加速器( tile accelerator,TA)固定功能硬件的瓶颈。数量众多的小三角形将导致对位于系统内存中的参数缓冲区(parameter buffer)的访问次数增加,增加内存带宽占用。

  • 保证网格内每个图元至少能创建10~20个像素。
  • 使用几乎等边的三角形。可以使面积与边长的比例最大化,减少生成的片元Quad的数量。
  • 避免细长的三角形。

和小三角形类似,细长三角形(下图红色所示)也会产生更多无效的像素,占用更高的GPU资源,提高Overdraw。

  • 避免使用扇形或类似的几何布局。三角形扇形的中心点具有较高的三角形密度,以致每个三角形具有非常低的像素覆盖率。可以考虑Tile轴对齐的切割,但会引入更多三角形。(下图)

扇形(图左)进行Tile轴对齐的切割后产生的三角形数量(图右)。

12.6.3 Shader优化

12.6.3.1 语句优化

  • 使用适当的数据类型。

在代码中使用最合适的数据类型可以使编译器和驱动程序优化代码,包括shader指令的配对。使用vec4数据类型而不是float可能会阻止编译器执行优化。

int4 ResultOfA(int4 a) {    return a + 1; // int4和int相加, 只需要1条指令.}int4 ResultOfA(int4 a) {    return a + 1.0; // int4和float相加, 需要3条指令: int4 -> float4 -> 相加 -> int4}
  • 减少类型转换。
uniform sampler2D ColorTexture;in vec2 TexC;vec3 light(in vec3 amb, in vec3 diff){    // 纹理采样返回vec4, 会隐性转换成vec3, 多出1条指令.    vec3 Color = texture(ColorTexture, TexC);     Color *= diff + amb;    return Color;}// 以下代码中, 输入参数/临时变量/返回值都是vec4, 没有隐性类型转换, 比上面代码少1条指令.uniform sampler2D ColorTexture;in vec2 TexC;vec4 light(in vec4 amb, in vec4 diff){    vec4 Color = texture(Color, TexC);    Color *= diff + amb;    return Color;}
  • 打包标量常数。

将标量常数填充到由四个通道组成的向量中,大大提高了硬件获取效率。在GPU骨骼动画系统中,可增加蒙皮的骨骼数量。

float scale, bias;  // 两个float值.vec4 a = Pos * scale + bias; // 需要两条指令.vec2 scaleNbias; // 将两个float值打包成一个vec2vec4 a = Pos * scaleNbias.x + scaleNbias.y; // 一条指令(mad)完成.
  • 使用标量操作。

要小心标量操作向量化,因为相同的向量化输出需要更多的时间周期。例如:

highp vec4 v1, v2;highp float x, y;// Bad!!v2 = (v1 * x) * y; // vector*scalar接着vector*scalar总共8个标量muladd.// Good!!v2 = v1 * (x * y); // scalar*scalar接着vector*scalar总共5个标量muladd.

12.6.3.2 状态优化

  • 尽量使用const。

如果正确使用,const关键字可以提供显著的性能提升。例如,在main()块之外声明一个const数组的着色器比没有的性能要好得多。

另一个例子是使用const值引用数组成员。如果值是const,GPU可以提前知道数字不会改变,并且数据可以在运行着色器之前被预读取,从而降低Stall。

  • 保持着色器指令数量合理

过长的着色器通常比较低效,比如需要在一个着色器中包含相对于纹理获取数量的许多指令槽,可以考虑将算法分成几个部分。

由算法的一部分生成的值可以存储到纹理中,然后通过采样纹理来获取。然而,这种方法在内存带宽方面代价昂贵。以下情形也会降低纹理采样效率:

1、使用三线性、各向异性过滤、宽纹理格式、3D和立方体贴图纹理、纹理投影;

2、使用不同Lod梯度的纹理查找;

3、跨像素Quad的梯度计算。

  • 最小化shader指令数。

现代shader编译器通常会执行特定的指令优化,但它不是自动有效的。很多时候需要人工介入,分析着色器,尽可能减少指令,即使是节省一条指令也值得。

  • 避免使用全能着色器(uber-shader)。

uber-shader使用静态分支组合多个着色器到一个单一的着色器。如果试图减少状态更改和批处理绘制调用,那么是有意义的。然而,通常会增加GPR数量,从而影响性能。

  • 高效地采样纹理。

纹理采样(过滤)的方式很多,性能和效果通常成反比:

纹理的部分过滤类型及对应效果图。

要做到高效地采样纹理,必须遵循以下规则:

1、避免随机访问,保持采样在同一个2x2像素Quad内,命中率高,着色器更有效率。

2、避免使用3D纹理。由于需要执行复杂的过滤来计算结果值,从体积纹理中获取数据通常比较昂贵。

3、限制Shader纹理采样数量。在一个着色器中使用四个采样器是可以接受的,但采样更多的纹理可能会导致性能瓶颈。

4、压缩所有纹理。这允许更好的内存使用,转化为渲染管道中更少的纹理停顿。

5、考虑开启Mipmaps。Mipmaps有助于合并纹理获取,并有助于以增加内存占用为代价的提高性能。同时还能降低带宽,提升缓存命中率。

6、尽量使用简单的纹理过滤。性能从高到低(效果从低到高)的采样方式:最近点(nearest)、双线性(bilinear)、立方(cubic)、三线性(tri-linear)、各向异性(anisotropic)。越复杂的采样方式,会读取越多的数据,从而提升内存访问带宽,降低缓存命中率,造成更大的延迟。需要格外注意这一点。

7、优先使用texelFetch / texture(),通常会比纹理采样效率更高(但需要工具分析验证)。

8、谨慎对待预计算纹理LUT。实时渲染中,很常将复杂计算的结果编码到纹理中,并将其用作查找表(如IBL的辐照度图,皮肤次表面散射预积分图)。这种方式只会在着色器是瓶颈时提升性能。如果函数参数和查找表中的纹理坐标在相邻片元之间相差很大,那么缓存效率就会受到影响。应该执行性能概要分析,以确定此法是否有实际上的提升。

9、使用mediump sampler代替highp sampler,后者的速度是前者的一半。

10、各向异性过滤(Anisotropic Filtering,AF)优化建议:

(1)先使用2x各向异性,评估它是否满足质量要求。较高的样本数量可以提高质量,但也会带来效益递减,并且往往与性能成本不相称。

(2)考虑使用2x双线性各向异性,而非三线性各向同性。在各向异性高的区域,2x双线性算法速度更快,图像质量更好。注意,通过切换到双线性过滤,可以在mipmap级别之间的过度点上看到接缝。

(3)只对受益最大的对象使用各向异性和三线性滤波。注意,8x三线性各向异性的消耗是简单双线性过滤的16倍!

  • 尽量避免依赖纹理读取(Dependent texture read)。

依赖纹理读取是一种特殊的纹理读取,其中纹理坐标依赖于着色器中的一些计算(而不是某种规律变化)。由于这个计算的值不能提前知道,它不可能预取纹理数据,因此在着色器处理降低缓存命中率,引发卡顿。

顶点着色纹理查找总是被视作依赖纹理读取,就像片元着色中基于zw通道变化的纹理读取。在一些驱动程序和平台版本中,如果给定带有无效w的Vec3或Vec4,则Texture2DProj()也可以作为依赖纹理读取。

与依赖纹理读取相关的成本在某种程度上可以通过硬件线程调度来抵消,特别是着色器涉及大量的数学计算。这个过程涉及到线程调度程序暂停当前线程并在另一个线程中交换到USC上的处理。这个交换的线程将尽可能多地处理,一旦纹理获取完成,原始线程将被交换回(下图)。

GPU的Context需要访问缓存或内存,会导致若干个时钟周期的延迟,此时调度器会激活第二组Context以利用ALU。

GPU越多Context可用就越可以提升运算单元的吞吐量,上图的18组Context的架构可以最大化地提升吞吐量。

虽然硬件会尽力隐藏内存延迟,但为了获得良好的性能,应该尽可能避免依赖纹理读取。应用程序尽量在片元着色器执行之前就计算出纹理坐标。

  • 避免使用动态分支

动态分支会延迟shader指令时间,但如果分支的条件是常量,则编译器就会在编译器进行优化。否则如果条件语句和uniform、可变变量相关,则无法优化。其它建议:

1、最小化空间相邻着色线程中的动态分支。

2、使用min(), max(), clamp(), mix(), saturate()等内置函数避免分支语句。

3、检查分支相对于计算的好处。例如,跳过距离相机阈值以上的像素进行光照计算,通常比直接进行计算会更快。

  • 打包shader插值数据。

着色器插值需要GPR(General Purpose Register,通用寄存器)传递数据到像素着色器。GPR的数量有限,若占满,会导致Stall,所以尽量减少它们的使用。

能使用uniform的就不用varying。将值打包在一起,因为所有varying都有四个组件,不管它们是否被使用,比如将两个vec2纹理坐标放入一个vec4。也存在其它更有创意的打包和实时数据压缩。

  • 减少着色器GRP的占用。

占用越多的GPR(General Purpose Register,通用寄存器)意味着计算量大,如果没有足够的可用寄存器时,可能会导致寄存器溢出,从而导致性能欠佳。以下一些措施可以减少GRP的占用:

1、使用更简单的着色器。

2、修改GLSL以减少哪怕是一条指令,有时也能减少一个GPR的占用。

3、不展开循环(unrolling loop)也可以节省GPRs,但取决于着色器编译器。

4、根据目标平台配置着色器,确保最终选择的解决方案是最高效的。

5、展开循环倾向于将纹理获取放到着色器顶部,导致需要更多的GPR来保存多个纹理坐标并同时获取结果。

6、最小化全局变量和局部变量的数量。减少局部变量的作用域。

7、最小化数据维度。比如能用2维的就不要用3维。

8、使用精度更小的数据类型。如FP16代替FP32。

  • 在着色器上避免常量的数学运算

自从着色器出现以来,几乎每一款发行的游戏都在着色器常量上花费了不必要的数学运算指令。需要在着色器中识别这些指令,将这些计算移到CPU上。在编译后的代码中识别着色器常量的数学运算可能更容易。

  • 避免在像素着色器中使用discard等语句。

一些开发者认为,在像素着色器中手动丢弃(也称为杀死)像素可以提高性能。实际上没有那么简单,有以下原因:

1、如果线程中的一些像素被杀死,而同Quad的其他像素没有,着色器仍然执行。

2、依赖于编译器如何生成微代码(Microcode)。

3、某些硬件架构(如PowerVR)会禁用TBDR的优化,造成渲染管线的Stall和数据回写。

  • 避免在像素着色器中修改深度

理由类同上一条。

  • 避免在VS里采样纹理。

虽然目前主流的GPU已经使用了统一着色器架构,VS和PS的执行性能相似。但是,还是得确保在VS对纹理操作是局部的,并且纹理使用压缩格式。

  • 拆分特殊的绘制调用

如果一个着色器瓶颈在于GPR和/或纹理缓存,拆分Draw Call到多个Pass反而可以增加性能。但结果难以预测,应以实际性能测试为准。

  • 尽量使用低精度浮点数

FP16的运算性能通常是FP32的两倍,所以shader中尽可能使用低精度浮点数。

precision mediump float;#ifdef GL_FRAGMENT_PRECISION_HIGH    #define NEED_HIGHP highp#else    #define NEED_HIGHP mediump#endif        varying vec2 vSmallTexCoord;varying NEED_HIGHP vec2 vLargeTexCoord;

UE也对浮点数做了封装,以便在不同平台和画质下自如低切换浮点数的精度。

  • 尽量将PS运算迁移到VS。

通常情况下,顶点数量明显小于像素数量。通过将计算从像素着色器迁移到顶点着色器,可以减少GPU工作负载,有助于消除冗余计算。

例如,拆分光照计算的漫反射和高光反射,将漫反射迁移到VS,而高光反射保留在PS中,这样能获得效果和效率良好平衡的光照结果。

  • 优化Uniform / Uniform Buffer。

1、保持Uniform数据尽可能地小。不超过128字节,以便在多数GPU良好地运行任意给定的着色器。

2、将Uniform改成OpenGL ES的带有#define的编译时常量,Vulkan的专用常量,或者着色源中的静态语法。

3、避免Uniform的向量或矩阵中存在常量,例如总是0或1的元素。

4、优先使用glUniform()设置uniform,而不是从buffer中加载。

5、不要动态地索引uniform数组。

6、不要过度使用实例化。使用gl_InstanceID访问Instanced uniform就是动态索引,无法使用寄存器映射的Uniform。

7、将Uniform的相关计算尽可能地移到CPU的应用层。

8、尽量使用uniform buffer代替着色器存储缓冲区(shader storage buffer)。只要uniform buffer空间充足,就尽量使用之。如果uniform buffer对象在GLSL中静态索引,并且足够小,驱动程序或编译器可以将它们映射到用于默认统一块全局变量的相同硬件常量RAM中。

  • 保持UBO占用尽可能地小。

如果UBO小于8k,则可以放进常量存储器,将获得更高的性能。否则,会存储在全局内存,存取时间周期显著增加。

  • 选择更优的着色算法。

选择更优的有效的算法比低级别(指令级)的优化更重要。因为前者更能显著地提升性能。

  • 选择合适的坐标空间。

顶点着色器的一个常见错误是在模型空间、世界空间、视图空间和剪辑空间之间执行不必要的转换。如果模型世界转换是刚体转换(只包含旋转、平移、镜像、光照或类似),那么可以直接在模型空间中进行计算。

避免将每个顶点的位置转换为世界或视图空间,更好的做法是将uniforms(如光的位置和方向)转换到模型空间,因为它是一个逐网格的操作,计算量更少。在必须使用特定空间的情况下(例如立方体映射反射),最好整个Shader都使用这个空间,避免在同一个shader中使用多个坐标空间。

  • 优化插值(Varying)变量。

减少插值变量数量,减少插值变量的维度,删除无用(片元着色器未使用)的插值变量,紧凑地打包它们,尽可能使用中低精度数据类型。

  • 优化原子(Atomic)。

原子操作在许多计算算法和一些片元算法中比较常见。通过一些微小的修改,原子操作允许许多算法在高度并行的GPU上实现,否则将是串行的。

原子的关键性能问题是争用(contention)。原子操作来自不同的着色器核心。要达到相同的高速缓存行(cache line),需要数据一致性访问L2高速缓存。

通过将原子操作保持在单个着色器核心来避免争用,当着色器核心在L1中控制必要的缓存行时,原子是最高效的。以下是具体的优化建议:

1、考虑在算法设计中使用原子时如何避免争用。

2、考虑将原子间距设置为64个字节,以避免多个原子在同一高速缓存行上竞争。

3、考虑是否可以通过累积到共享内存原子中来分摊争用。然后,让其中的一个线程在工作组的末尾推送全局原子操作。

  • 充分利用指令缓存(Instruction cache)。

着色器核心指令缓存是一个经常被忽略的影响性能的因素。由于并发运行的线程数量众多,因此足够重视指令缓存对性能的重要性。优化建议如下:

1、使用较短的着色器与更多的线程,而不是更长的色器与少量的线程。较短的着色器指令在缓存中更有可能被命中。

2、使用没有动态分支的着色器。动态分支会减少时间局部性,增加缓存压力。

3、不要过于激进地展开循环(unroll loop),尽管一些展开可能有所帮助。

4、不要从相同的源代码生成重复的着色程序或二进制文件。

5、小心同个tile内存在多个可见的片元着色(即Overdraw)。所有未被Early-ZS或FPK/HSR剔除的片元着色器

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/123981.html

相关文章

  • [面试专题]一线互联网大厂面试总结

    摘要:道阻且长啊前端面试总结前端面试笔试面试腾讯一面浏览器工作原理浏览器的主要组件包括用户界面包括地址栏后退前进按钮书签目录浏览器引擎用来查询及操作渲染引擎的接口渲染引擎渲染界面和是基于两种渲染引擎构建的,使用自主研发的渲染引擎,和都使用网络用来 道阻且长啊TAT(前端面试总结) 前端 面试 笔试 面试 腾讯一面 1.浏览器工作原理 浏览器的主要组件包括: 用户界面- 包括地址栏、后退/前...

    lemanli 评论0 收藏0
  • [面试专题]一线互联网大厂面试总结

    摘要:道阻且长啊前端面试总结前端面试笔试面试腾讯一面浏览器工作原理浏览器的主要组件包括用户界面包括地址栏后退前进按钮书签目录浏览器引擎用来查询及操作渲染引擎的接口渲染引擎渲染界面和是基于两种渲染引擎构建的,使用自主研发的渲染引擎,和都使用网络用来 道阻且长啊TAT(前端面试总结) 前端 面试 笔试 面试 腾讯一面 1.浏览器工作原理 浏览器的主要组件包括: 用户界面- 包括地址栏、后退/前...

    xfee 评论0 收藏0
  • [面试专题]一线互联网大厂面试总结

    摘要:道阻且长啊前端面试总结前端面试笔试面试腾讯一面浏览器工作原理浏览器的主要组件包括用户界面包括地址栏后退前进按钮书签目录浏览器引擎用来查询及操作渲染引擎的接口渲染引擎渲染界面和是基于两种渲染引擎构建的,使用自主研发的渲染引擎,和都使用网络用来 道阻且长啊TAT(前端面试总结) 前端 面试 笔试 面试 腾讯一面 1.浏览器工作原理 浏览器的主要组件包括: 用户界面- 包括地址栏、后退/前...

    littleGrow 评论0 收藏0
  • [面试专题]一线互联网大厂面试总结

    摘要:道阻且长啊前端面试总结前端面试笔试面试腾讯一面浏览器工作原理浏览器的主要组件包括用户界面包括地址栏后退前进按钮书签目录浏览器引擎用来查询及操作渲染引擎的接口渲染引擎渲染界面和是基于两种渲染引擎构建的,使用自主研发的渲染引擎,和都使用网络用来 道阻且长啊TAT(前端面试总结) 前端 面试 笔试 面试 腾讯一面 1.浏览器工作原理 浏览器的主要组件包括: 用户界面- 包括地址栏、后退/前...

    leap_frog 评论0 收藏0
  • 微信 - 收藏集 - 掘金

    摘要:基础学习微信小程序视图容器一掘金本文为菜鸟窝编辑吴佳林的连载。如果碰巧遇到关闭情况,可加群全家桶与微信开发前端掘金此项目本身有一个了,为了方便将和微信端数据打通,需要用户微信和用户绑定。 微信扫一扫登录内部实现原理 - Android - 掘金与XMPP相关试题一 与XMPP相关试题二 与性能优化相关试题一 与性能优化相关试题二 与性能优化相... [利用 [微信公众号通知] 给你的网...

    jasperyang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<