游戏引擎架构#4 低阶渲染器(3)
背景:
作为游戏开发从业者,从业务到语言到框架到引擎,积累了一些知识和经验,特别是在看了几遍《游戏引擎架构》后对引擎架构的理解又深入了些。
近段时间有对引擎剖析的想法,正好借这书本对游戏引擎架构做一个完整分析。
此书用简明、清楚的方式覆盖了游戏引擎架构的庞大领域,巧妙地平衡了广度与深度,并且提供了足够的细节。
借助《游戏引擎架构》这本书、结合引擎源码和自己的经验,深入分析游戏引擎的历史、架构、模块,最后通过实践简单引擎开发来完成对引擎知识的掌握。
游戏引擎知识面深而广,所以对这系列的文章书编写范围做个保护,即不对细节进行过多的阐述,重点剖析的是架构、流程以及模块的运作原理。
同时《游戏引擎架构》中部分知识太过陈旧的部分,会重新深挖后总结出自己的观点。
概述:
本系列文章对引擎中的重要的模块和库进行详细的分析,我挑选了十五个库和模块来分析:
- 时间库
- 自定义容器库
- 字符串散列库
- 内存管理框架
- RTTI与反射模块
- 图形计算库
- 资产管理模块
- 低阶渲染器
- 剔除与合批模块
- 动画模块
- 物理模块
- UI底层框架
- 性能剖析器的核心部分
- 脚本系统
- 视觉效果模块
本篇内容为列表中的第8个部分的第1节。
正文:
简单回顾下前文
前文我们聊了下显卡在计算机硬件主板中的位置与结构,知道了CPU、GPU的通信介质,并简单介绍了手机上的主板结构。本篇开头对上一篇做一些内容补充,PC和手机的不同硬件组织,以及CPU与其他芯片的通信过程。
下面我们开始这篇内容
本次内容会围绕GPU来写,从硬件架构到软件驱动再到引擎架构,目标是帮大家理解GPU硬件的运作原理,理解图形接口的架构,理解引擎低阶渲染器的架构。
目录:
- 主板结构中的显卡
- GPU功能发展史
- GPU与CPU的差异
- GPU硬件特点
- 图形驱动程序架构
- 引擎低阶渲染架构
内容结构
- CPU硬件结构
- GPU硬件结构
- GPU手机管线与PC管线的差异
简单回顾下前文,前文我们主要讲了显卡的发展历史,知道了显卡功能和管线是如何一步步转变为现在这样子的。
CPU结构与工作原理
我们知道,CPU运行时有三类元器件构成,取指器、译码器、运算器(逻辑算术运算器、浮点数运算器、单指令多数据运算器等)。
这三类元器件代表三个阶段取指阶段(Fetch)、指令译码阶段(Decode)、执行阶段(Execute),它们在CPU内执行的步骤如下图:
(图-取指-译指-执行三步骤)
取指阶段为从内存或缓存中取得指令并存放到寄存器中的过程。
接着,译码器会将寄存器中的指令翻译成操作指令,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。在组合逻辑控制的计算机中,指令译码器对不同的指令操作码产生不同的控制电位,以形成不同的微操作序列;在微程序控制的计算机中,指令译码器用指令操作码来找到执行该指令的微程序的入口,并从此入口开始执行。
运算阶段,则根据指令执行不同的运算单元,完成指令所规定的各种操作,具体实现指令的功能。为此,CPU 的不同部分被连接起来,以执行所需的操作。
(图-控制单元-运算单元-存储单元)
因此,通常我们将取指器、译码器统称为控制单元,计算器称为算术逻辑单元(ALU),寄存器和高速缓存称为存储单元。
除了这三个基本单元,当下这样复杂的CPU中还有包括分支预测器、乱序控制器、内存预加载器等等。
这里简单介绍下CPU指令流水线、分支预测、乱序执行的原理。
指令流水线
起初CPU指令执行是线性的,只靠取指、译码、运算顺序执行三个模块,这导致元器件的工作顺序是线性的,当一个元器件执行时,其他元器件是空等待状态,CPU执行效率比较低。
为了提高效率,提高空等待的元器件的利用率,对指令执行流水线进行了拆分,并同时增加多个流水线不断减少元器件的空等待装填。如下图:
(拆分多级流水线)
将原本线性的三个指令执行顺序,拆分成一个个小模块,让这些独立的小模块可以自顾自的循环工作,减少前后的等待时间,从而提高了指令执行效率。
用这种方式把一个指令拆分成“取指令 - 指令译码 - 执行指令”这样三个部分,这就是一个三级的流水线。进一步把“执行指令”拆分成“ALU 计算(指令执行)- 内存访问 - 数据写回”,就会变成一个五级的流水线。
继续拆分,将一个长时间的操作步骤,拆分成更多的步骤,让所有步骤需要执行的时间尽量都差不多长。这样,也就可以解决我们在单指令周期处理器中遇到的复杂指令性能问题。(同时多级流水线会出现许多问题,例如模块间寄存器的写入次数太多,多模块读写同一个资源相互冲突等,这里不细说,CPU最终都有解决方案去解决)现代的 ARM 或 Intel 的 CPU,流水线级数都已经到了 14 级。
乱序执行
我们在写程序时,常常会发现函数内几个执行操作顺序并不互相依赖,哪个在前哪个在后都没有太大的关系。此时不仅编译器会对这些不相互依赖的计算操作进行重新顺序排序用于优化CPU执行效率(这也是导致线程不同步的其中一个原因),CPU也会将不相互依赖的指令放在不相同的指令流水线上以加快执行速度。
(指令在不同CPU流水线上乱序执行:图来源网络)
我们从图中可以看到,在流水线里,当后面的指令不依赖前面的指令时,就不用等待前面的指令执行完毕后再执行,可以另起一个流水线执行,否则就需要用NOP隔周期等待的方式将执行单元延后计算。因此我们所写的代码的执行顺序其实并不是我们所想象的那样,在CPU中大部分时候其实是乱序执行的,这样元器件的利用率更高,执行效率也更高,而依赖而停顿的次数也更少。
(乱序执行的流程图)
乱序执行实际的过程比我们想象的要复杂一些,总体上它会先拆分指令,再分发给执行单元,结束后将结果重新排序,最后提交缓冲。
分支预测
程序中有很多true或false的判断来跳转下文要执行的指令,这种跳转会使得执行流水线发生停顿,因为要依赖前面代码计算的结果再决定要执行哪段程序,因此流水线不中断并等待结果,这会使CPU执行效率降低。
在CPU中有分支预测器,它是一种数字电路,在分支指令执行前,猜测哪一个分支会被执行,这样能显著提高pipeline的性能。
可以理解为,分支预测器会主动猜测分支是true还是false。
如果猜错了,处理器要flush掉pipeline, 回滚到之前的分支,然后重新热启动,选择另一条路径。 如果猜对了,处理器不需要暂停,继续往下执行。
也就是说,如果CPU每次都猜错,处理器将耗费大量时间在停止-回滚-热启动这一周期性过程里。反之,如果侥幸每次都猜对了,那么处理器将从不停止、无需等待的执行后面的指令。
(分支预测图)
CPU执行指令遇到条件时不知道该读取哪些指令,需要等待判断条件中的计算结果,这样就中断了后面指令执行流水线使得执行效率下降。于是CPU增加了分支预测器,猜if条件中是True还是False,如果猜对了效率就会提高,如果猜错了,则重新计算。
分支预测的关键是,预测算法能猜对多少。
分支预测分为动态分支预测和静态分支预测。动态预测在执行过程中统计了通过率,根据通过率去调整预测方向,静态则始终以一个值作为判断标准。动态预测有好几种,最常见的是双模预测,通过四个状态位来动态调整预测结果。其它常见分支预测器如两级自适应预测器,局部/全局分支预测器,融合分支预测器,Agree预测期,神经分支预测器等。
CPU原理小结
(CPU抽象元件图)
现在我们知道了CPU指令周期的工作方式,分为三个步骤,取指、译码、运算。运算后需要寄存器和高速缓存来作为存储器,CPU会从内存中获取指令并最终将数据写入内存。
我们把CPU中的元件抽象成,取指和译码元件、逻辑运算元件、数据缓存,就有了上面这幅简单抽象的CPU结构图。
下面我们来看看硬件上的元器件是如何分布的:
(CPU硬件结构图:来源网络)
我们看到CPU除了基本的控制器、运算器、寄存器、高速缓存外,还额外放置了乱序执行器、分支预测器、内存预装载器等用于提高CPU效率。这些元器件全部加起来,整个就是一个CPU Core。
(多核架构图:来源网络)
实际的设备中通常由多个CPU Core组成多核的架构,每个CPU Core都有自己的高速缓存L1,不同CPU Core之间也有共享的高速缓存L2,通常每级缓存的存取速度有10倍的差距,而内存的存取速度比高速缓存差的更多,对于CPU Core来说可以认为它是一个外部存储设备,通过桥接芯片连接。
GPU硬件结构与原理
前面介绍了CPU的内部结构,现代无论是手机还是PC机基本都是多核的,每个核就是1个CPU Core,每个CPU Core里都有取指器和译码器,还有逻辑运算器,以及寄存器和高速缓存。除了上述基本元件外还有其他元器件用于优化CPU执行效率,包括乱序执行器、分支预测器、内存预装载器等。
GPU图形管线的变迁
我们从GPU历史里知道,原本显卡只是一个数据传输和画面转换接口,在不断的变革下成了主板上一个独立的芯片,之后就有了GPU的概念。CPU将数据传输到显存再通知GPU处理这些数据,GPU则拥有图形图像的处理流水线,专门处理图像。
起初图形的顶点、片元都在CPU上计算,到了Voodoo FX显卡时已经将图元生成后的步骤拆分到了GPU上,最后再将顶点处理部分的计算合入到GPU上,此时GPU才真正形成了自己的图形管线。如下图:
(图形计算管线变迁1-1982年前的纯2D时代)
1982年前,CPU承担大部分的工作,当时还没有GPU的概念,还只能以显示适配器的名称称呼。
(图形计算管线变迁2-1996年3dfx Voodoo)
到1996年,GPU已经可以分担CPU的部分功能,只留下顶点处理部分部分给CPU。
(图形计算管线变迁3-1998年GeForce)
到1998年,所有顶点处理和片元处理都由GPU来完成了,但没有可编程部分,管线是固定的,传入顶点后无法控制顶点和片元的变化。
(图形计算管线变迁4-2002年GeForce FX)
到2002年,正式加入了可编程着色器,让顶点和片元的计算和展示有了更多变化。
(图形计算管线变迁5-2006年GeForce 8800)
到2006年,GPU管线中又增加了细分着色器,pre-Z等节点。
这部分历史我们也可以通过OpenGL的功能变化来看这段历史的发展过程。
(来源 wiki)
1.1 1997 年 3 月,纹理对象,顶点数组
1.2 1998 年 3 月,3D 纹理、BGRA 和打包像素格式
1.2.1 1998年10月,ARB 扩展概念
1.3 2001 年 8 月,多重纹理、多重采样、纹理压缩
1.4 2002 年 7 月,深度图,GLSlang
1.5 2003 年 7 月,顶点缓冲对象 (VBO),遮挡查询
2.0 2004 年 9 月, GLSL 1.1,MRT,两个纹理的非幂,点精灵,双面模板
2.1 2006 年 7 月 ,GLSL 1.2,像素缓冲对象 (PBO),sRGB 纹理
3.0 2008 年 8 月 ,GLSL 1.3,纹理数组,条件渲染,帧缓冲对象 (FBO)
3.1 2009 年 3 月, GLSL 1.4,Instancing,纹理缓存对象,统一缓存对象,图元重启
3.2 2009 年 8 月, GLSL 1.5,几何着色器,多重采样纹理
3.3 2010 年 3 月, GLSL 3.30,从 OpenGL 4.0 规范向后移植尽可能多的功能
4.0 2010 年 3 月, GLSL 4.00,GPU 上的曲面细分,具有 64 位精度的着色器
4.1 2010 年 7 月, GLSL 4.10,开发人员友好的调试输出,与 OpenGL ES 2.0 的兼容性
4.2 2011 年 8 月, GLSL 4.20,带原子计数器的着色器,绘制传输给Feed back实例,着色器打包,性能改进
4.3 2012 年 8 月, GLSL 4.30,利用 GPU 并行性的计算着色器、着色器存储缓冲区对象、高质量 ETC2/EAC 纹理压缩、增强的内存安全性、多应用程序稳健性扩展、与 OpenGL ES 3.0 的兼容性
4.4 2013 年 7 月, GLSL 4.40,缓冲区放置控制,高效异步查询,着色器变量布局,高效多对象绑定,Direct3D 应用程序的流线型移植,无绑定纹理扩展,稀疏纹理扩展
4.5 2014 年 8 月, GLSL 4.50,直接状态访问 (DSA),刷新控制,鲁棒性,OpenGL ES 3.1 API 和着色器兼容性,DX11 仿真功能
4.6 2017 年 7 月, GLSL 4.60,更高效的几何处理和着色器执行,更多信息,无错误上下文,多边形偏移钳位,SPIR-V,各向异性过滤
经过显卡历史、GPU管线的变化历史、OpenGL的功能变迁史,让我们把GPU看的更清楚。
GPU Core结构
我们知道现代的 CPU 里除了基本的元器件外,还有许多围绕提高执行效率的元器件,以及增加诸多功能的其他元器件。这些元器件在 GPU 里有点多余了,GPU 的整个处理过程是一个流式处理过程,没有那么多分支条件,以及复杂的依赖关系。
因此我们可以把 GPU 里这些对应的元器件去掉,只留下取指令、指令译码、ALU 以及执行这些计算需要的寄存器和缓存。如图:
(GPU元器件瘦身图)
这样看来GPU core比CPU Core的构造简单的多了,由于传输GPU的数据并不相互依赖的,因此我们可以用很多个GPU Core来并行计算这些数据。
于是就有了,多GPU Core的结构,如下图:
(多个Core并行工作图)
多个Core并行工作时它们使用了相同的取指器并且有相同的代码,为什么不把它们并起来呢。
前面我们说过SIMD,它把4个数据一起提交并用一个指令执行它完成计算。在GPU中借鉴了SIMD,用了一种跟它很像的处理技术叫做SIMT(Single Instruction Multiple Threads),如下图:
( 单指令多数据流管线)
在SIMT中,向GPU Core输入的是8个图元或片元,同时输出8个结果,每次输入多个数据到GPU Core中,并获得多个结果。SIMT 比 SIMD 更加灵活。
SIMT可以把多条数据,交给不同的线程去处理。各个线程里面执行的指令流程是一样的,但是可能根据数据的不同,走到不同的条件分支。这样,相同的代码和相同的流程,可能执行不同的具体的指令。这个线程走到的是 if 的条件分支,另外一个线程走到的就是 else 的条件分支了。
GPU的分支处理
我们CPU有对分支做预测,让流水线停顿更少,GPU Core也会对分支做优化处理。
(GPU的分支处理)
常用的GPU分支处理SIMD里,为每个指令都分配一个ALU做并行处理,用多个周期分别计算分支的两种结果。
这样做就不会让流水线停滞,但是这样做有效率问题,在一个指令周期里,很多ALU是闲置的。
因此在SIMD之后,SIMT(Single Instruction,Multiple Threads)技术可以变相的做分支的顺序执行,如下图:
(SIMT 分支预测并行计算)
在SIMT中,各个线程里面执行的指令流程是一样的,只是走的不同的分支。相同的代码和相同的流程,执行不同的分支。 可能一些线程走到的是 if 的条件分支,而另外一些线程走到的就是 else 的条件分支,这种并行计算使得计算本身无需依赖上文,也让ALU不再空闲停滞。
这里简单说下解决SIMIT流水线中的卡顿问题
拆分存储缓存,让上下文依赖的计算在不同时段同时计算,以提高ALU的利用率。
(卡顿时启动另一条管线)
(拆分整个缓存为独立缓存)
GPU为了不等待分支条件而导致的停顿流水线,就要对每个分支做都做计算。分支内的数据仍然会有依赖关系,依赖关系就会造成卡顿,需要等待计算或等待获取资源。
因此将原来的一整个缓存,拆分为多个缓存,使得流水线在阻塞时能更好的使用闲置ALU计算下一条数据。这样就能更好的利用ALU计算做优化了。
现实GPU硬件中的物理架构
前面我们说的都是抽象的GPU Core结构,下面我们来看下实际中的GPU物理架构。
看到这些GPU架构可以发现它们虽然彼此有差异,但很多概念相同,下面我们俩理清一下这些架构中组建的概念:
GPC(Graphics Processing Cluster) : 图形处理集群,GPU划分多个GPC,每个GPC里有多个TPC,每个TPC里包含了多个SM和1个Rester Engine
TPC(Texture Processing Cluster) : 图像处理集群,是由若干个SM、1个纹理单元(Texture Unit)和一些逻辑控制和ALU组成。
RT Core(Ray Trace Core) : RT Core是SM里面加了一条专用的流水线(ASIC)来计算射线和三角形求交(可以访问BVH,用于光线追踪)。由于是ASIC专用电路逻辑,与shader code做求交计算相比,性能有数量级的提升。
Rester Engine : 光栅引擎,处理它接收到的三角形,并为它负责的那些部分生成像素信息(也处理背面剔除和 Z 剔除)。
PolyMorp Engine:曲面引擎,是一个带有顶点提取器、视口变换的累积集群,它处理属性设置和流输出,这些都合并到了这个处理器中,极大地扩展了曲面细分和(当发送到光栅引擎时)光栅化性能。
Thread Engine:线程引擎,调度线程到核的引擎
SM(Stream Multiprocessor)、SMX、SMM :SM包含GPU Core内核,指令单位,调度程序。
Warp Scheduler、Dispatch Unit:负责线程束调度,将软件线程按一捆一捆(不是一个一个)的方式分配到计算核上。一个Warp由32个线程组成,Warp Scheduler的指令通过Dispatch Units派送到Core核上执行。
SP(Streaming Processors)、Core :SP有时也叫CUDA core,一个 SP 包括多个 ALU 和 FPU。SP是作用于顶点或像素数据的真正处理单元。
ALU(Arithmetic Logic Unit)、FPU(Float Point Unit):ALU 是算术和逻辑单元,FPU 是浮点单元。
INT32,FP32 :在GPU里支持单精度运算的Single Precision ALU称之为FP32 core或简称core,而把双精度运算的Double Precision ALU称之为DP unit或者FP64 core。第三代的Kepler架构里,FP64单元和FP32单元的比例是高端机1:3或者低端机1:24,到了第五代比例为1:2,低端型号里仍然保持为1:32。
SFU(Special Function Unit):执行特殊数学运算(sin、cos、log等)
TENSO CORE : 精度混合计算单元,转换不同精度之间的运算结果,用于执行矩阵乘法的计算单元,精度混合分为整数精度和浮点数精度。
ROP(Render Output Unit) :渲染输出单元 ,一个ROP内部有很多ROP单元,在ROP单元中有深度测试和Framebuffer混合,深度和颜色的设置必须是原子操作,否则两个不同的三角形在同一个像素点就会有冲突和错误。
LD/ST(Load/Store Unit):加载和存储数据
Share Memory、L1 Data Cache、L1 Cache、L2 Cache :共享内存,以及多级的高速缓存
RF(Register File):寄存器堆,多个寄存器组成的阵列
Instruction Cache :指令缓存
未完待续…
参考资料:
《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
《针对移动端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
版权申明
本文为博主原创文章,未经允许不得转载:
Copyright attention
Please don't reprint without authorize.
微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解
QQ交流群: 777859752 (高级程序书友会)