文章目录

前言

一、Drawcall优化

二、移动平台硬件架构

三、GPU逻辑管道

总结

参考

前言

这篇文章主要介绍drawcall相关内容以及移动平台drawcall优化。还需要再补充内容。

一、Drawcall优化

DrawCall是CPU调用底层图形接口的操作。比如有上千个物体,每一个的渲染都需要去调用一次底层接口,而每一次的调用CPU都需要做很多工作,那么CPU必然不堪重负。但是对于GPU来说,图形处理的工作量是一样的。

我们先来看看Draw Call对CPU的消耗大概是一个什么级别的量:

NVIDIA 在 GDC 曾提出,25K batchs/sec 会吃满 1GHz 的 CPU,100%的使用率。

公式:DrawCall_Num = 25K * CPU_Frame * CPU_Percentage / FPS

DrawCall_Num : DrawCall数量(最大支持)

CPU_Frame : CPU 工作频率(GHZ单位)

CPU_Percentage:CPU 分配在drawcall这件事情上的时间率 (百分比)

FPS:希望的游戏帧率

比如说我们使用一个高通820,工作频率在2GHz上,分配10%的CPU时间给drawcall上,并且我们要求60帧,那么一帧最多能有83个drawcall(25000210%/60 = 83.33), 如果分配是20%的CPU时间,那就是大概167。所以对drawcall的优化,主要就是为了尽量解放CPU在调用图形接口上的开销。所以针对drawcall我们主要的思路就是每个物体尽量减少渲染次数,多个物体最好一起渲染。

drawcall次数的优化有哪些方案呢?

目前的主要目的就是减少drawcall次数,做法有静态合并,动态合并,GPU Instancing,unity还有SRP Batcher(本质上drawcall不会减少,减少的是drawcall数据拷贝)。具体的做法很多文章都有介绍,笔者就不多写了。

汤姆猫X:SRP Batcher & GPU Instancing

zhuanlan.zhihu.com/p/265153591

关于drawcall相关还需要注意的问题?

1、如果静态批处理前有一些物体共享了相同的网格,那么每一个物体都会有一个该网格的复制品(本来unity只会保留一份,但是静态批处理会生成新的一个大网格,所以会保留所有物体的网格,最后合并),即一个网格会变成多个网格被发送给GPU,这回导致一次性提交过大的数据,虽然引擎是有限制的,一般是65535个顶点,但制作也是要注意的。

2、对于那些shader相同,纹理不同导致的不同材质无法进行批处理的物体(比如项目中的场景环境,地面,如果都使用了比较复杂的 shader)通过纹理合并的方法来使得它们可以被静态批处理。但这会引发带宽问题,详细原因关系到Bus总线带宽的问题。

3.还有一种情况需要注意的,就是一下子提交大量的drawcall导致的卡顿问题,这个可以阅读下多线程渲染相关的。主要是可能是因为,提交渲染(eglSwapBuffers)会导致驱动层中缓存的渲染指令立即执行,此时CPU被阻塞。如果在提交渲染时驱动层缓存了大量的指令,CPU就会被阻塞很长时间;也可能是因为提交大量drawcall的时候存在纹理的Upload。

飞飞:多线程渲染

493 赞同 · 24 评论文章

BUS总线带宽

CPU完成一次drawcall,除了需要发一个drwacall的命令之外,还需要把内存中顶点数据、纹理贴图、shader参数通过bus总线拷贝到内存分配给GPU的显存之中,注意这是拷贝,不是指针传递,速度不快。如果一次drawcall传递的数据过大,带宽成为了瓶颈,那就会大大影响效率(其它的drawcall无法出发,GPU又处于闲置)。这种情况最有可能出现在为了减少drawcall,疯狂的合并纹理上。在项目中,UI的drawcall调用占了很大一部分,也会最难优化的,为了减少drawcall , 我们把U合并成大的贴图,drawcall下降了,但是帧率却也下降了,内存使用也增加了,原因就是这个。在项目中,不会同时出现的元素不要打包到一起,保证单张合并纹理不大于1024一般就不会有问题了,最大尽量不要超过2048。

drawcall的优化大概就是这些,优化的目标其实是往一个目标上靠,cpu的drawcall命令刚刚好能被GPU处理,不要让CPU等待(带宽限制)。

drawcall超过限制数量就是很容易成为渲染CPU侧的瓶颈,有一种做法是可以提升这个限制数量的,就是参考类似堡垒之夜的做法,添加RHI线程来缓解主线程和渲染线程的压力。不过这块的技术难度也是不小的。

fangcun:堡垒之夜的移动端优化实践(Vulkan与OpenGL ES)

234 赞同 · 10 评论文章

二、移动平台硬件架构

提供这个是为了方便理解上面说的drawcall,先看下图:

Android 还是 IOS 用的都是统一内存架构,GPU和CPU共享一个物理内存,通常我们有“显存”和“内存”两种叫法,可以认为是这块物理内存的所有者不同,当这段映射到cpu,就是通常意义上的内存;当映射到gpu,就是通常意义上的显存。并且同一段物理内存同一时刻只会映射到一个device。

即使是在同一物理内存上 ,openGL ES规范中CPU和GPU之间的内存是不能共享的,vertex和texture的buffer是需要拷贝的。vulkan 与IOS的metal 可以共享内存。

三、GPU逻辑管道

介绍这个是为了更理解drawcall如何来的。

1)先看看下图GPU架构图,采纳了主流的Turing架构:

上图是采纳了Turing架构的TU102 GPU,它的特点如下:

6 GPC(图形处理簇)

36 TPC(纹理处理簇)

72 SM(流多处理器)

每个GPC有6个TPC,每个TPC有2个SM

4,608 CUDA核

72 RT核

576 Tensor核

288 纹理单元

12x32位 GDDR6内存控制器 (共384位)

单个SM的结构图如下:

由于Fermi,NVIDIA具有类似的原理架构。有一个Giga线程引擎可以管理所有正在进行的工作。GPU分为多个GPC(图形处理集群),每个GPC具有多个SM(流式多处理器)和一个Raster Engine。在此过程中有很多互连,最明显的是Crossbar,它允许跨GPC或其他功能单元(如ROP(渲染输出单元)子系统)进行工作迁移。

程序员认为的工作(着色程序执行)在SM上完成。它包含许多内核,这些内核为线程执行数学运算。例如,一个线程可以是顶点或像素着色器调用。这些核心和其他单元由Warp Scheduler驱动,Warp Scheduler管理一组32个线程作为warp,并将要执行的指令移交给Dispatch Units。代码逻辑由调度程序处理,而不是在内核本身内部处理,内核只看到类似“求和寄存器4234与寄存器4235并存储在4230中”的内容。来自调度员。与内核相当智能的CPU相比,内核本身是相当愚蠢的。GPU将智能性提升到更高的层次,它可以进行整个整体的工作(如果需要的话,可以进行多个工作)。

这些单元中实际上有多少个在GPU上(每个GPC多少个SM,多少GPC ..)取决于芯片配置本身。从上面可以看到,GM204具有4个GPC,每4个SM,但Tegra X1例如具有1个GPC和2个SM,均采用Maxwell设计。SM设计本身(内核,指令单元,调度程序的数量)也随着时间的推移而一代又一代地发生了变化(请参见第一张图片),并有助于使芯片如此高效,从而可以从高端台式机扩展到笔记本电脑,再扩展到笔记本电脑。移动的。

2)逻辑管道

我们来看看CPU到GPU的逻辑管道流程:

1、程序通过图形API(DX、GL、WEBGL)发出drawcall指令,指令会被推送到驱动程序,驱动会检查指令的合法性,然后会把指令放到GPU可以读取的Pushbuffer中。

2、经过一段时间或者显式调用flush指令后,驱动程序把Pushbuffer的内容发送给GPU,GPU通过主机接口(Host Interface)接受这些命令,并通过前端(Front End)处理这些命令。

3、在图元分配器(Primitive Distributor)中开始工作分配,处理indexbuffer中的顶点产生三角形分成批次(batches),然后发送给多个PGCs。这一步的理解就是提交上来n个三角形,分配给这几个PGC同时处理。

4、在GPC中,其中一个SM的Poly Morph Engine负责从三角形索引(Vertex Fetch)中获取顶点数据。

5、在获取数据之后,在SM中以32个线程为一组的线程束(Warp)来调度,来开始处理顶点数据。Warp是典型的单指令多线程(SIMT,SIMD单指令多数据的升级)的实现,也就是32个线程同时执行的指令是一模一样的,只是线程数据不一样,这样的好处就是一个warp只需要一个套逻辑对指令进行解码和执行就可以了,芯片可以做的更小更快,之所以可以这么做是由于GPU需要处理的任务是天然并行的。

6、SM的warp调度器会按照顺序分发指令给整个warp,单个warp中的线程会锁步(lock-step)执行各自的指令,如果线程碰到不激活执行的情况也会被遮掩(be masked out)。被遮掩的原因有很多,例如当前的指令是if(true)的分支,但是当前线程的数据的条件是false,或者循环的次数不一样(比如for循环次数n不是常量,或被break提前终止了但是别的还在走),因此在shader中的分支会显著增加时间消耗,在一个warp中的分支除非32个线程都走到if或者else里面,否则相当于所有的分支都走了一遍,线程不能独立执行指令而是以warp为单位,而这些warp之间才是独立的。

7、warp中的指令可以被一次完成,也可能经过多次调度,例如通常SM中的LD/ST(加载存取)单元数量明显少于基础数学操作单元

8、由于某些指令比其他指令(尤其是内存负载)需要更长的时间才能完成,因此warp调度程序可能会简单地切换到另一个不等待内存的warp。这是GPU如何克服内存读取延迟的关键概念,它们只需切换出活动线程组即可。为了使切换非常快,由调度程序管理的所有线程在寄存器文件中都有自己的寄存器。着色器程序需要的寄存器越多,线程/线程的空间就越少。我们之间可以切换的次数越少,等待指令完成(最多获取内存)时我们可以做的功用就越少。

9、Wrap Scheduler完成顶点着色器的所有指令后,其结果将由Viewport Transform处理。三角形被裁剪空间体积裁剪,可以进行栅格化了。GPU会使用L1和L2缓存来进行vertex-shader和pixel-shader的数据通信。

10、接下来这些三角形将被分割,再分配给多个GPC,三角形的范围决定着它将被分配到哪个光栅引擎(raster engines),每个raster engines覆盖了多个屏幕上的tile,这等于把三角形的渲染分配到多个tile上面。也就是像素阶段就把按三角形划分变成了按显示的像素划分了。

11、SM上的Attribute Setup保证了从vertex-shader来的数据经过插值后是片元着色器是可读的。

12、GPC上的光栅引擎(raster engines)在它接收到的三角形上工作,来负责这些这些三角形的像素信息的生成(同时会处理裁剪Clipping、背面剔除和Early-Z剔除)。

13、同样,我们批处理了32个像素线程,或者更好的说是8倍2x2像素四边形,这是我们在像素着色器中将始终使用的最小单位。这个2x2的四边形允许我们计算诸如纹理mip贴图过滤之类的导数(四边形内纹理坐标的较大变化会导致较高的mip)。2x2正方形内的那些线程(其样本位置实际上并未覆盖该三角形)被屏蔽(gl_HelperInvocation)。本地SM的Wrap Scheduler程序之一将管理像素着色任务。

14、接下来的阶段就和顶点着色器中的逻辑步骤完全一样,但是变成了在片元着色器线程中执行。 由于不耗费任何性能可以获取一个像素内的值,导致锁步执行非常便利,所有的线程可以保证所有的指令可以在同一点。(NV_shader_thread_group)。

15、最后一步,现在像素着色器已经完成了颜色的计算还有深度值的计算,在这个点上,我们必须考虑三角形的原始api顺序,然后才将数据移交给ROP(render output unit,渲染输入单元),一个ROP内部有很多ROP单元,在ROP单元中处理深度测试,和framebuffer的混合,深度和颜色的设置必须是原子操作,否则两个不同的三角形在同一个像素点就会有冲突和错误。

参考

移动平台Unity3D 应用性能优化 - 腾讯WeTest

wetest.qq.com/lab/view/315.html?from=content_SegmentFault

Life of a triangle - NVIDIA’s logical pipeline

developer.nvidia.com/content/life-triangle-nvidias-logical-pipeline

fangcun:堡垒之夜的移动端优化实践(Vulkan与OpenGL ES)

234 赞同 · 10 评论文章

深入GPU硬件架构及运行机制 - 0向往0 - 博客园

www.cnblogs.com/timlly/p/11471507.html

https://blog.csdn.net/qq_35312463/article/details/108561115

blog.csdn.net/qq_35312463/article/details/108561115

飞飞:多线程渲染

493 赞同 · 24 评论文章