3

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

 2 years ago
source link: http://www.luzexi.com/2021/12/12/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B051
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

读书笔记(五十一) 《游戏引擎架构》#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硬件的运作原理,理解图形接口的架构,理解引擎低阶渲染器的架构。

  • 主板结构中的显卡
  • GPU功能发展史
  • GPU与CPU的差异
  • GPU硬件特点
  • 图形驱动程序架构
  • 引擎低阶渲染架构

内容结构:

  • CPU硬件结构
  • GPU硬件结构
  • GPU手机管线与PC管线的差异

接着上篇的内容。前面说了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

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

December 12, 2021 · 读书笔记, 前端技术

感谢您的耐心阅读

Thanks for your reading

版权申明

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

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

Copyright attention

Please don't reprint without authorize.

qrcode_for_gzh.jpg

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK