读书笔记(五十一) 《游戏引擎架构》#4 低阶渲染器(4)

已发布在微信公众号上,点击跳转

背景:

作为游戏开发从业者,从业务到语言到框架到引擎,积累了一些知识和经验,特别是在看了几遍《游戏引擎架构》后对引擎架构的理解又深入了些。

近段时间有对引擎剖析的想法,正好借这书本对游戏引擎架构做一个完整分析。

此书用简明、清楚的方式覆盖了游戏引擎架构的庞大领域,巧妙地平衡了广度与深度,并且提供了足够的细节。

借助《游戏引擎架构》这本书、结合引擎源码和自己的经验,深入分析游戏引擎的历史、架构、模块,最后通过实践简单引擎开发来完成对引擎知识的掌握。

游戏引擎知识面深而广,所以对这系列的文章书编写范围做个保护,即不对细节进行过多的阐述,重点剖析的是架构、流程以及模块的运作原理。

同时《游戏引擎架构》中部分知识太过陈旧的部分,会重新深挖后总结出自己的观点。

概述:

本系列文章对引擎中的重要的模块和库进行详细的分析,我挑选了十五个库和模块来分析:

1.时间库 2.自定义容器库 3.字符串散列库 4.内存管理框架 5.RTTI与反射模块 6.图形计算库 7.资产管理模块 8.低阶渲染器 9.剔除与合批模块 10.动画模块 11.物理模块 12.UI底层框架 13.性能剖析器的核心部分 14.脚本系统 15.视觉效果模块

本篇内容为列表中的第8个部分的第1节。

正文:

简单回顾下前文

前文我们聊了下显卡在计算机硬件主板中的位置与结构,知道了CPU、GPU的通信介质,并简单介绍了手机上的主板结构。本篇开头对上一篇做一些内容补充,PC和手机的不同硬件组织,以及CPU与其他芯片的通信过程。

下面我们开始这篇内容

本次内容会围绕GPU来写,从硬件架构到软件驱动再到引擎架构,目标是帮大家理解GPU硬件的运作原理,理解图形接口的架构,理解引擎低阶渲染器的架构。

目录:

内容结构:

接着上篇的内容。前面说了CPU、GPU的硬件结构,CPU的构造和GPU的构造,下面我们来聊聊GPU是如何工作的,以及GPU的管线在手机端和PC端的差异。

NVIDIA基于Fermi管线的架构

关于GPU的逻辑管线,这篇Nvidia这篇文章《Life of a triangle - NVIDIA’s logical pipeline》说的很清楚。

(NVIDIA的整体架构图)

下面我以此为标准进行翻译并重新剖析。

为了简单起见,省略了几个细节,假设 drawcall 引用了一些已经充满数据并存在于 GPU DRAM 中的索引和顶点缓冲区,并且仅使用顶点和像素着色器(GL:片段着色器)。

(从图形API调用到图元处理过程图)

1.引擎或业务程序调用图形 API(DX 或 GL)中的绘图函数,接着驱动程序会被调用,驱动程序会进行一些验证以检查参数是否“合法”,再将指令写入到GPU可读写的缓冲队列中。在这个地方 CPU 方面可能会出现很多瓶颈,这也是为什么程序员要好好使用 API 以利用当今 GPU 的强大功能的技术很重要的原因。

(绘制接口调用图)

2.经过一段时间渲染,画面“刷新”被调用,驱动程序在缓冲区中已经缓冲了足够多的工作命令,接着将其发送给 GPU 进行处理(操作系统会参与)。最后 GPU 的主机接口接收命令并交给GPU前端的处理。

(绘制队列与刷新图)

3.接着图元分配器(Primitive Distributor)开始分配工作。为了批量处理索引和三角形,将数据发送给多个图形处理集群(GPC)并行处理。

(SM整体结构图)

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

5.在获取数据后,SM中每32个线程为一捆线程束(Warp),它们被调度去处理这些顶点工作。 线程束(Warp)是典型的单指令多线程(SIMT,SIMD单指令多数据的升级)的实现,也就是32个线程同时执行的指令是一模一样的,只是线程数据不一样,这样的好处就是一个Warp只需要一套逻辑对指令进行解码和执行就可以了,芯片可以做的更小更快。

(线程束与线程束调度器图)

6.SM的线程束(Warp)调度器会按照顺序分发指令给整个线程束(Warp),单个线程束(Warp)中的线程会锁步(lock-step)执行各自的指令。线程束(Warp)会使用SIMT的方式来做分支预测,每个线程执行的分支会不同,当线程遇到到错误判断的执行情况会被遮蔽(be masked out)。

(单个GPU线程与存储设备的关系图)

被遮蔽的原因是SIMT执行中错误预测,例如当前的指令是if(true)的分支,但是当前线程的数据的条件是false,或者循环的次数不一样(比如for循环次数n不是常量,或被break提前终止了但是别的还在走),因此在Shader中的分支会显著增加时间消耗,在一个线程束(Warp)中的分支除非32个线程都走到同一个里面,否则相当于所有的分支都走了一遍,线程不能独立执行指令而是以线程束(Warp)为单位,而这些线程束中的线程之间才是相互独立的。

(SIMT线程束做分支预测图)

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

8、由于某些指令比其他指令需要更长的时间才能完成,特别是内存加载,线程束(Warp)调度器可能会简单地切换到另一个没有内存等待的线程束(Warp),这是GPU如何克服内存读取延迟的关键,其操作为简单地切换活动线程组。为了使这种切换更快,调度器管理的所有线程束(Warp)在寄存器列阵(Register File)中都有自己的寄存器。这里就会有个矛盾产生,Shader需要的寄存器越多,给线程束(Warp)留下的空间就越少,于是就会导致能用的线程束(Warp)就越少。此时如果碰到指令在内存获取数据等待就只会等待,而没有其他可以运行的线程束(Warp)可以切换。

(线程与寄存器列阵关系图)

(线程束调度器调度线程图)

9、一旦线程束(Warp)完成了顶点着色器(vertex-shader)的所有指令,运算结果会被Viewport Transform模块处理,三角形会被裁剪然后准备光栅化,此时GPU会使用L1和L2缓存来进行顶点着色起(vertex-shader)和片元着色起(pixel-shader)的数据通信。

(管线中节点、数据、存储器的关系图)

10、接下来这些三角形将被分割,通过 Work Distribution Crossbar 将三角形再分配给多个GPC,三角形的范围决定着它将被分配到哪个光栅引擎(raster engines),每个光栅引擎(raster engines)覆盖了多个屏幕上的图块(tile),这等于把三角形的渲染分配到多个图块(tile)上面。也就是说在像素阶段前就把三角形划分成了方块格式范围,三角形处理分成许多较小的工作。

(三角形被拆分成多个块派发到多个光栅引擎图)

(图块拆分任务派发图)

11、SM上的属性安装器(Attribute Setup)保证了从顶点着色器(vertex-shader)生成的数据,经过插值后,在片元着色器(pixel-shade)上是可读的。

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

13、再次做批量处理,32个像素线程被分成一组(或者说8个2x2的像素块),这是在像素着色器上面的最小工作单元(2x2 四边形允许我们计算诸如纹理 mip 贴图过滤之类的导数–四边形内纹理坐标的大变化会导致更高的 mip)。在这个像素线程内,如果没有被任何三角形覆盖就会被剔除。SM中的线程束(Warp)调度器会管理像素着色器的任务。

(2x2像素组传入到线程束处理像素着色器的图)

14、接下来是同样的线程束调度策略,和顶点着色器(vertex-shader)中的逻辑步骤完全一样,但是变成了在像素着色器线程中执行。 由于不耗费任何性能从2x2四边形中获取一个像素,这使得锁步执行非常便利,于是所有的线程可以保证指令可以在同一点同步执行。

(线程束锁步执行图)

15、最后一步,现在像素着色器已经完成了颜色的计算和深度值的计算。在这个点上,我们必须考虑三角形的调用API顺序,然后才将数据移交给ROP(render output unit,渲染输出单元),一个ROP内部有很多ROP单元,在ROP单元中处理深度测试和帧缓冲(framebuffer)的混合等,深度和颜色的设置必须是原子操作,否则两个不同的三角形在同一个像素点就会有冲突和错误。NVIDIA 通常应用内存压缩,以减少内存带宽要求,从而增加“有效”带宽。

(像素着色器后的像素处理过程图)

以上这些信息有助于我们理解 GPU 中的一些工作和数据流,还可以帮助我们理解CPU与GPU之间的交互。

总结CPU和GPU的交互

GPU是设备,设备都有驱动,CPU可以直接执行二进制指令集,对于GPU设备,图形接口有opengl,directx标准及库封装,计算有cuda和opencl封装。程序代码调用这些图形或计算库,这些库调用驱动,驱动再来对接操作GPU设备,CPU与GPU直接的通信是遵循总线和内存的规则。

原则上CPU、内存外的设备都属于IO设备,通过总线连上来,它们必须遵守IO总线规范,如显卡就走pcie总线,这里还有ionmu,统一内存等,来共享资源,缩短路径,提升效率等。

这里专门说下驱动,计算机有专门的程序接口指定一个计算任务到GPU上,这个接口程序就是驱动程序。CPU给GPU下发任务时通过调用驱动程序,不同GPU厂商实现自己的驱动,并且提供了各种的编程接口。图形计算上实现了OpenGL标准接口规范的图形库,它会调用各厂商的驱动,用户可以通过GLSL编写计算任务进行通用计算。后来的CUDA编程模型专门推出用于编写通用计算任务的接口,于是OpenGL就专门用于图形渲染了。而CUDA则是通过kenel函数来编写计算任务,通过cudaLaunch接口来下发任务。

(从图形库到驱动到GPU指令队列的图)

从硬件角度看:

(从硬件角度看指令和数据处理流程图)

1.GPU设备的配置空间物理地址映射到虚拟地址,可以被程序直接访问;同时建立任务队列缓冲,声明中断等等;

2.CPU在进程内准备数据和缓冲,基于虚拟地址VA、VM将其转换为显存的物理地址IPA。驱动程序获取任务,再将任务信息填充至任务队列内。

3.根据虚拟内存绑定的地址信息,将任务队列的指针更新至GPU设备侧,这个端口称为doorbell寄存器;

4.设备接收到doorbell操作,会触发中断,再读取主存中的任务队列,包括队列内的信息和其指向的任务数据,GPU设备侧读取该数据。

5.完成后,再将数据发送给CPU侧。一般来说,GPU设备侧发送至CPU的读写请求使用的是虚拟地址,由CPU的IOMMU或SMMU转换为物理地址。

GPU手机管线与PC管线的差异

为什么要了解手机与PC管线的差异? PC的能耗和发热比手机端可以更大一些,因此PC与手机在硬件架构上有天然的不同,进而使得它们在GPU管线上也有很大的差异,这使得我们在优化手机端时必须了解这种差异再做针对性的做优化。

TBDR(Tile-Base-Deffered-Rendering)是现代移动端GPU的设计架构,它同传统PC上IR(Immediate-Rendering)架构的GPU在硬件设计上有很大的差别。

为什么呢?因为功耗是Mobile设备设计的第一考虑因素,而带宽是功耗的第一杀手。

我们来看PC的GPU管线,即传统的IR(Immediate-Rendering)模式:

(IMR管线图:源自网络)

IMR(Immediate Mode Rendering)模式中,GPU直接在主存或显存上读写深度缓存(Depth Buffer)和帧缓存(Frame Buffer),这导致带宽消耗很大,如果在手机上耗电和发热都无法承受。

手机使用统一内存架构,CPU和GPU都通过总线来访问主存。GPU需要获取三角形数据(Geometry Data)、贴图数据(Texture Data)以及帧缓存(Frame Buffer),它们都在主存中。如果GPU直接从主存频繁地访问这些数据,就会导致带宽消耗大,成为性能瓶颈。

TBR(Tile-Based-Rendering)管线

基于以上所述原因,手机GPU使用自己的缓存区(SRAM),例如On-Chip深度缓存(On-Chip Depth Buffer)和On-Chip颜色缓存(On-Chip Color Buffer),它们与存取主存相比,速度更快,功耗更低。但它们的存储空间很小。(SRAM不需要充电来保持存储记忆,因此SRAM的读写基本不耗电,缺点是价格昂贵)

如果手机直接读写帧缓存(Frame Buffer)就相当于让一辆火车在你家和公司之间来回奔跑,非常耗电。于是手机端想要拆分绘制内容,每次只绘制一小部分,再把所有绘制完成的部分拼起来。

把帧缓存(Frame Buffer)拆分成很多个小块,使得每个小块可以被GPU附近的SRAM容纳,块的多少取决于GPU硬件的SRAM大小。这样GPU就可以分批的一块块的在SRAM上读写帧缓存(Frame Buffer),一整块都读写完毕后,再整体转移回主存上。

这种模式就叫做TBR(Tile-Based-Rendering),整体管线如下图:

(TBR管线图:源自网络)

屏幕分块后的大小一般为16x16或32x32像素 ,在几何阶段之后再执行分块(Tiling),接着将各个块(Tile)逐个光栅化,最后写入帧缓存中(Frame Buffer)中 。

这里有一些细节要注意,TBR在接受每个指令(CommandBuffer)时并不立即绘制,而是先对这些数据做顶点处理,把顶点处理的结果暂时保存在主存上,等到非得刷新整个帧缓存时,才真正的用这批数据做光栅化。

因此,TBR的管线实际可以认为被切分成两部分,前半部分为顶点数据部分,后半部分为片元数据部分:

(TBR把管线切分为光栅化前和光栅化后)

顶点数据先被处理并存储在Frame Data中,等到必须刷新时(例如帧缓存置换,调用glflush,调用glfinish,调用glreadpixels读取帧缓存像素时,调用glcopytexiamge拷贝贴图时,调用glbitframebuffer获取帧缓存时,调用queryingocclusion,解绑帧缓存时等等)才被集中的拿去处理光栅化。

那么为什么PC不使用TBR呢?

实际上直接对主存或显存(这里也有多级缓存)进行整块数据的读写速度是最快的,而TBR需要一块块的绘制,然后再回拷给主存。可以简单的认为TBR牺牲了执行效率,换来了相对更难解决的带宽功耗。如果哪一天手机上解决了带宽的功耗问题,或者说SRAM足够大了,可能就没有TBR了。

TBDR(Tile-based deferred rendering)管线

TBR会把顶点数据处理完毕后存储在Frame Data中,那么就会有很多厂商针对Frame Data做优化。

TBDR整体的管线图如下:

(TBDR管线图:源自网络)

我们看到相比TBR,TBDR在光栅化(Raster)后多了一个HSR(Hidden Surface Removal)处理,这部分处理主要剔除无需绘制的元素,减少重绘(Overdraw)数量(高通通过优化划分块(Tile)之后执行顶点着色器(Vertex Shader)之前的节点来达到此目的,称为LRZ)。例如提前对不透明像素做深度测试并剔除,剔除被模板裁剪掉的像素等等,总之它们不会进入到像素着色器阶段(Pixel Shader)。

因此在TBDR上,不透明物体的排序没有太大意义,Early-Z这种策略也不存在IOS上。这些GPU硬件巧妙的利用TBR的Frame Data队列实现了一种延迟渲染,尽可能只渲染那些会最终影响帧缓存(Frame Buffer)的像素。

TBDR和软件上的延迟渲染相比有什么区别呢?

软件层面的延迟渲染与TBDR不同。软件层面的延迟渲染是针对一个Drawcall,对于从后到前的不透明物体绘制是每次都要绘制的,而硬件层面的延迟渲染,处理的是一整批Drawcall,剔除这一整批Drawcall中不会绘制的像素最后再渲染。可以说现在大部分的移动端的GPU都使用TBDR架构。

参考资料:

《How Shader Cores Work》 https://engineering.purdue.edu/~smidkiff/KKU/files/GPUIntro.pdf

《CPU体系结构》 https://my.oschina.net/fileoptions/blog/1633021

《深入理解CPU的分支预测(Branch Prediction)模型》 https://zhuanlan.zhihu.com/p/22469702

《分析Unity在移动设备的GPU内存机制(iOS篇)》 https://www.jianshu.com/p/68b41a8d0b37

《PC与Mobile硬件架构对比》 https://www.cnblogs.com/kekec/p/14487050.html

《针对移动端TBDR架构GPU特性的渲染优化》 https://gameinstitute.qq.com/community/detail/123220

《A look at the PowerVR graphics architecture: Tile-based rendering》 https://www.imaginationtech.com/blog/a-look-at-the-powervr-graphics-architecture-tile-based-rendering/

《A look at the PowerVR graphics architecture: Deferred rendering》 https://www.imaginationtech.com/blog/the-dr-in-tbdr-deferred-rendering-in-rogue/

《深入GPU硬件架构及运行机制》 https://www.cnblogs.com/timlly/p/11471507.html

《深入浅出计算机组成原理》 https://time.geekbang.org/column/article/105401?code=7VZ-Md9oM7vSBSE6JyOgcoQhDWTOd-bz5CY8xqGx234%3D

《Nvidia Geforce RTX-series is born》 https://www.fudzilla.com/reviews/47224-nvidia-geforce-rtx-series-is-born?start=2

《渲染管线与GPU(Shading前置知识)》 https://zhuanlan.zhihu.com/p/336999443

《剖析虚幻渲染体系(12)- 移动端专题Part 1(UE移动端渲染分析)》 https://www.cnblogs.com/timlly/p/15511402.html

《tpc-texture-processing-cluster》 https://gputoaster.wordpress.com/2010/12/11/tpc-texture-processing-cluster/

《Life of a triangle - NVIDIA’s logical pipeline》 https://developer.nvidia.com/content/life-triangle-nvidias-logical-pipeline

《Rasterisation wiki》 https://en.wikipedia.org/wiki/Rasterisation

《PolyMorph engine and Data Caches by Hilbert Hagedoorn》 https://www.guru3d.com/articles-pages/nvidia-gf100-(fermi)-technology-preview,3.html

《NVIDIA GPU的一些解析》 https://zhuanlan.zhihu.com/p/258196004

《tensor-core-performance-the-ultimate-guide》 https://developer.download.nvidia.cn/video/gputechconf/gtc/2019/presentation/s9926-tensor-core-performance-the-ultimate-guide.pdf

《Understanding the Understanding the graphics pipeline》 https://www.seas.upenn.edu/~cis565/LECTURES/Lecture2%20New.pdf

已发布在微信公众号上,点击跳转

· 读书笔记, 前端技术

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    读书笔记(五十一) 《游戏引擎架构》#4 低阶渲染器(4)

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解

    QQ交流群: 777859752 (高级程序书友会)