GPU架构和渲染技术
GPU主导的渲染演进
随着显卡硬件的升级,渲染技术不断发展。从 GeForce 256 支持 T&L,到 RTX 支持光追,GPU 已成为渲染的核心推动力。
从 GPU 架构的角度来看,现代 GPU 拥有庞大的流处理器阵列,专为高效处理图形渲染任务而设计。这些流处理器并行工作,执行顶点处理、光栅化和像素着色等渲染阶段。
这一架构优化了图形处理的吞吐量,从而实现更逼真和身临其境的视觉体验。它还为复杂的光照模型和后处理效果提供了支持,进一步提升了图像质量。
GPU 架构由显存(类似于 DRAM)和计算单元(SM)组成。
显存:容量大,速度慢,可同时被 CPU 和 GPU 访问。
计算单元:执行计算,拥有自己的控制模块、寄存器和缓存等部件。
计算单元NVIDIA GPU 架构演变:Maxwell vs. Turing
与之前的架构类似,Maxwell 和 Turing 架构都采用以下结构:
* GPC(图形处理簇):包括多个 SM(流多处理器)
* SM(流多处理器):处理任务和线程
Maxwell 架构
* 每个 GPC 包含 4 个 SM
* GPU 有 4 个 GPC
Turing 架构
* 每个 GPC 包含 6 个 TPC(纹理/处理器簇)
* 每个 TPC 包含 2 个 SM
* GPU 有 6 个 GPC
这种架构改进提高了 GPU 的处理效率和性能,使其能够处理更多计算密集型任务。
GPC(图形处理集群)由 SM(流式多处理器)和光栅化引擎组成,通过 Crossbar 连接。Crossbar 负责在各个 GPC 之间分配处理任务,例如当一个 GPC 完成计算后,Crossbar 会将数据分配给需要处理的另一个 GPC。
SM(Stream Multiprocessor,流多处理器)不同GPU厂商的架构中,SM的叫法不尽相同。
高通 Streaming Processor / Shader Processor,为您的设备提供卓越的图形处理性能,带来流畅的游戏体验和令人惊叹的视觉效果。• Mali称作Shader Core。Unified Shading Cluster (USC),又名 PowerVR Shading Cluster,是专门为提升图形处理性能而设计的创新技术。• ATI/OpenCL称作Compute Unit,通常简称为CU。下图展示了一个SM的内部结构。
以Fermi架构的单个SM来说,其包含以下部件。
• Vertex Fetch模块:顶点处理前期的通过三角形索引取出三角形数据。• Tesselator模块:对应着DX11引入的新特性曲面细分。Stream Output:DX10引入的创新功能,允许GPU输出顶点数据至缓冲区,支持高效处理复杂几何体。优化后的文字:每个 Streaming Multiprocessor (SM) 集成了 32 个运算核心,称为流处理器(SP)。SP 负责执行由 Warp Scheduler 调度的指令,在 Dispatch Units 的指导下进行运算。
优化后的内容:Warp 调度器管理 32 个线程组(Warps),每个组对应 32 个运算核心。通过调度单元,Warp 调度器的指令发送至核心执行。
指令缓存存放即将执行的指令,通过分配单元填充到每个计算核心进行处理。提升了核心对指令的快速访问,优化了指令处理效率。SFU(特殊函数单元)与 GPU 中的 EFU(初等函数单元)类似,执行特殊数学运算。SFU 数量有限,导致在使用高级数学函数时出现瓶颈。SFU 执行的特殊函数包括:* 三角函数(正弦、余弦)
* 指数函数
* 对数函数
* 反双曲函数
• 幂函数:pow(x, a)、sqrt(x)。• 对数函数:log(x)、log2(x)。• 三角函数:sin(x)、cos(x)、tan(x)。• 反三角函数:asin(x)、acos(x)、atan(x)。LD/ST:加载/存储模块协助线程组从共享内存或显存加载或存储数据。此模块极大地提高了数据访问效率,从而优化了 GPU 性能。
• Register File:寄存器堆。存放将要处理的数据。L1 缓存:不同 GPU 架构的 L1 缓存配置有所不同:
* 某些架构将 L1 缓存与共享内存共享。
* 其他架构将 L1 缓存与纹理缓存共享。
• Uniform Cache:全局统一内存缓存。优化后文本:Fermi 架构拥有 4 个纹理读取单元,每个单元可在单个运算周期获取 4 个采样值,为 16 个线程的线程束提供数据。每个单元配备 16K 纹理缓存,并由 L2 缓存提供支持。
• Interconnect Network:内部链接网络。GPU内存架构GPU内存层次结构
与CPU类似,GPU拥有自己的内存,包括寄存器、缓存(L1、L2)和显存(Global Memory)。
内存类型:
* 寄存器:最快
* L1 缓存:次之
* L2 缓存:跨越 GPU 芯片
* 显存(Global Memory):速度最慢
速度差异:
从寄存器到显存,速度依次变慢。
探索 GPU 架构下的渲染管线,获得不同寻常的视角。了解 GPU 硬件如何处理渲染过程,揭秘其内部运作机制。
应用阶段CPU 与 GPU 异步数据交互
在这个阶段,CPU 主要负责准备数据,包括图元数据、渲染状态等,然后将数据传递给 GPU 进行处理。数据传输是一个异步过程,类似于服务器和客户端之间的交互。
CPU 和 GPU 采用生产者/消费者异步处理模型。CPU 产生 "命令",GPU 消费 "命令"。通过这种机制,CPU 可以将数据和行为传输给 GPU,由 GPU 执行相应的动作。
CPU 端通过渲染 API(例如 DX 或 GL)将操作封装为命令并存储在命令队列(FIFO Push Buffer)中。
当内存写满或提交命令队列时,CPU 将队列提交给应用驱动,并在队列末尾添加一条修改 Fence 值的命令。
在系统驱动的调度下,轮到此应用传输数据时,数据将被写入 RingBuffer。RingBuffer 就像一个轮转的水车,将命令逐一 "搬运" 到 GPU 前端。当 RingBuffer 已满,则 CPU 发生拥塞。
当命令队列中的最后一条命令(即修改 Fence 值的命令)被前端接收后,CPU 接收到 Fence 修改信号,拥塞解除,CPU 可继续生成后续命令。
图元装配器迅速处理渲染任务,根据图元类型、顶点索引和命令,将其分配给多个 GPC(图形处理集群),提升渲染效率。
Vertex Fetch模块通过三角形索引从显存中获取三角形数据,将其放入SM寄存器中。Shader在SM上处理数据,并在不同的寄存器上执行操作。这些寄存器不仅包括语义寄存器,还包括各种类型,如输入、常量和临时寄存器。
SM 工作机制
数据进入流式多处理器 (SM) 后,线程调度器创建并分配线程。每个线程分配给一个运算核心,并在寄存器堆中分配指定数量的寄存器。
同时,指令调度单元从指令缓存中取出指令,分配给运算核心执行。
这种处理机制适用于所有着色器,包括顶点着色器 (VS)、像素着色器 (PS) 和几何着色器 (GS)。
关键概念
* 统一着色器架构:SM 中的所有运算核心都可以执行任何类型的着色器指令。
* SIMT(单指令多线程):每个运算核心同时执行多个线程的同一指令。
* 线程束:一组并行执行的线程。
统一着色器架构Shader Model最初提供了分离的顶点着色器和像素着色器,然而在实践中,这种分离带来了以下问题:
* 阻碍了着色器代码的重用
* 限制了流水线优化
细碎三角形场景会导致顶点着色器过载,而像素着色器闲置,形成资源分配不均。解决办法是减少三角形数量或使用更高级的场景优化技术来平衡工作负载。复杂场景中的像素着色器负载过高,而顶点着色器则闲置。这是因为大型三角形占据了大部分像素,导致像素着色器处理量激增。统一着色器架构优化图形性能
长期以来,NVIDIA 和 ATI 意识到,要优化性能和能效,必须采用统一着色器架构。此架构废除了顶点着色器和像素着色器概念,取而代之的是运算核心。
运算核心是一个全面的图形处理系统,可以执行顶点和像素指令。关键在于,它可以根据需要无缝切换调用,显着提高游戏性能。这种灵活性和效率使统一着色器架构成为图形处理的未来,确保最佳体验和节能。
SIMT什么是指令?
指令是计算机中一条条的操作命令,指导运算核心执行操作。例如,"将寄存器 25 的值加上寄存器 26 的值,结果存入寄存器 27"就是一个指令。
指令调度
调度单元为每个运算核心分配相同的指令。这确保所有运算核心在同一时间执行相同的操作。然而,每个运算核心处理的数据不同。
例如,对于指令"将寄存器 25 的值加上寄存器 26 的值,结果存入寄存器 27":
* 运算核心 A:寄存器 25 和 26 中的值可能是 [A, B]。
* 运算核心 B:寄存器 25 和 26 中的值可能是 [X, Y]。
因此,尽管运算核心执行相同的指令,但它们处理的数据不同。
运算核心A运算中,tmp25和tmp26寄存器分别存储数字"2"和"3"。运算后,结果"5"存储在tmp27寄存器中。SIMT(单指令多线程)
SIMT 指的是单指令多线程处理,其中指令相同,但线程不同。
运算核心以“lock-step”方式执行指令,即所有核心同时执行相同的指令。只有所有核心完成当前指令后,调度单元才会分配下一条指令。
线程束SM 寄存器管理
每个流多处理器 (SM) 拥有大量寄存器。Shader 核心函数 (VS/GS/PS) 作为线程执行,所需的寄存器数量在编译后确定。
当线程执行时,它会从寄存器堆中分配所需数量的寄存器。例如,如一个 SM 拥有 32768 个寄存器,而线程需要 256 个寄存器,则该 SM 最多可执行 128 个线程 (32768/256 = 128)。
SM 运算核心
SM 上每个运算核心在一个时钟周期内执行一个线程。然而,一个运算核心对应着多个线程。
Shader 中所需的寄存器数量决定了 SM 上可执行的线程数量。每个 SM 拥有 32 个运算核心。如果超过 32 个线程需要执行,线程调度器就会将线程分组为线程束 (Warp),每个 Warp 包含 32 个线程。
例如,一个 SM 拥有 128 个线程,则它将分配 4 个线程束 (128/32 = 4)。
运算核心间的切换
一个运算核心一次只能处理一个线程,32 个运算核心一次只能处理一个线程束。某些指令的处理时间较长,特别是内存加载。
当一个线程束遇到耗时的操作时,它将被阻塞 (Stall)。为了减少延迟,GPU 线程调度器会切换到另一组线程。
通过在多个线程束间切换执行,运算核心最大化利用资源。
线程调度器通过操纵线程束来管理线程。每个 SM 的寄存器数量限制了可执行的线程数量。运算核心在多个线程束间切换执行,以优化资源利用。
受限于寄存器数量,每个线程获取的寄存器越多,线程束数量越少。线程束减少意味着调度资源减少,当遇到耗时指令时,线程无其他线程束可调度,只能等待,导致资源浪费和执行效率低下。
裁剪空间
随着 Warp 处理完指令运算,PolyMorph Engine 的视口变换模块介入,将三角形数据存储在 L1 和 L2 缓存中。它们被转换为裁剪空间,为像素处理阶段优化顶点坐标,确保高效的图形渲染。
像素处理:GPC 和 PolyMorph Engine 的协作
GPU通过工作分配交叉开关(WDC)平衡光栅化工作负载,将屏幕划分为区域块并分配给图形处理集群(GPC)。每个GPC包含一个光栅化引擎,负责基于三角形像素生成光栅化数据。光栅化数据随后传输到PolyMorph Engine,由属性设置模块插值并填充Pixel Shader寄存器。
像素着色:SM 和线程分配
在逻辑上,每个线程执行Pixel Shader的内核函数,处理单个像素。GPU将屏幕细分为2×2像素块,每个Warp包含32个线程,负责处理8个像素块。WDC分配区域块的策略影响SM分配。
实验观察:SM 分配和像素划分
研究表明,SM分配不是按编号顺序划分的。同一色块内的像素可能分配给不同的SM,这取决于三角形的细碎程度。每个色块包含16×16个像素,对应SM内运行256个线程的分布。
利用 NVIDIA Warp ID,计算像素亮度:
光亮度 = Warp ID / SM 中的 Warp 数
将光亮度作为片段颜色,渲染出渐变效果。
渲染画面如下图所示。由于一个色块是由4×8个像素组成,也就证明了一个Warp包含了32个线程。
高效渲染流程:
数据流经 PS 渲染计算后,SM 将其分配给 Crossbar,再传至 ROP。ROP 进行深度测试和帧缓冲混合,并将最终像素写入后备帧缓冲。最终,后备帧缓冲中的数据被写入显存,完成渲染过程。
多平台渲染架构关于IMR、TBR、TBDR介绍的文章有很多,下面简单归纳一下。
IMR优化后的文字:每次 Drawcall 以流水线形式连续执行,涵盖顶点着色、像素着色和输出到帧缓冲区的颜色和深度缓冲区,避免中间中断。
FrameBuffer的多重访问特性允许每个Drawcall的像素渲染直接写入FrameBuffer,省去了中间渲染目标,提高渲染效率。• 每个像素频繁访问显存上的FrameBuffer,带宽消耗大。IMR模式的GPU执行的伪代码如下。遍历渲染过程:
1. 处理顶点着色器,为每个顶点执行。
2. 执行裁剪测试,剔除不可见几何体。
3. 遍历图元中的片段,执行片段着色器。
问题:
• 发热量大。主要是带宽消耗大导致的,这个在PC上没有太大问题。• 耗电量大。主要是带宽消耗大导致的,这个在PC上没有太大问题。TBRTBR(Tile-Based Rendering)采用分块渲染架构,将场景分割为小块分别渲染,提升渲染效率。
• 发热、费电,移动设备接受不了。• 芯片太大,移动设备接受不了。创新芯片设计解决了移动设备的显示问题:
* 分块渲染:将屏幕分割成小块,存储在片上内存(On Chip Memory)中。
* 延迟写入:只在需要时才将分块数据写入显存,减少带宽消耗。
* 空间优化:片上内存作为高速缓存,减少芯片尺寸。
这些改进显著降低了发热、耗电量和芯片面积。
Drawcall 执行过程经过分块和顶点计算,生成 FrameData。在特定时机执行 Early-Z、着色和测试等优化,最终将数据一次性写入帧缓冲中的颜色和深度缓冲区。• FrameBuffer访问次数很少,FrameData会被频繁访问。TBDRTBDR(Tile-Base-Deffered Rendering)基于分块延迟渲染技术,可显著减少延迟渲染中几何和光照计算瓶颈,从而大幅提升图形处理效率。
Early-Z 优化了 Overdraw,而 TBR 则受制于绘制顺序。对于相互遮挡的物体,Early-Z 可有效降低 Overdraw。PowerVR 的图像信号处理器 (ISP) 采用创新技术 HSR(隐藏表面移除),通过像素级排序优化图形呈现。与 Adreno 的早期 Z 拒绝和 Mali 的 FPK(前向像素剔除) 等技术类似,HSR 可有效减少绘制过绘制,以获得更好的图形性能。
相关渲染优化寄存器充分使用过度使用寄存器会减少 Warp 数量,影响 GPU 利用率。为充分利用 GPU,应节约寄存器使用。
GPU 的 ALU 使用 SIMD 指令同时处理四维数据。每条指令可处理四维数据,类似于 SSE 指令集中的 SIMD 指令。
float4 c = a + b;
如果没有SIMD处理单元,汇编伪代码如下,四个数据需要四个指令周期才能完成。
利用加法操作,将三个四维向量 a、b、c 相加,得到新的四维向量 c。此操作广泛应用于计算机图形学、机器学习和物理模拟等领域。
而使用SIMD处理后就变为一条指令处理四个数据了,大大提高了处理效率。
SIMD_ADD c, a, b
优化寄存器使用是 SIMD 编程的关键。Unity 的 TRANSFORM_TEX 宏巧妙地将缩放和偏移 UV 坐标所用的两个二维向量合并到一个四维向量中,节省了一个寄存器。充分利用寄存器向量的所有分量,将显著提升性能。
使用 TRANSFORM_TEX 宏,通过 scale 和 bias 属性转换 2D UV 坐标,实现如下的计算:
tex.xy * name_ST.xy + name_ST.zw
GPU并行处理优化:Co-Issue技术
为了充分利用GPU的SIMD计算单元,引入了Co-Issue技术,可以优化代码利用率。当处理不同数量的向量时,ALU利用率会下降;例如,从浮点向量数量为4时的100%,依次降低到3时的75%、2时的50%和1时的25%。
Co-Issue通过合并不同维度的指令来提高低维向量处理效率。例如,原本需要两条指令执行的操作,Co-Issue可以将其合并为一条指令,在单个时钟周期内完成。
然而,如果变量既作为操作数又作为存储数,则无法使用Co-Issue优化。为了解决这一问题,开发了标量指令着色器(Scalar Instruction Shader),它可以有效地组合任何向量,充分利用Co-Issue技术和SIMD优势。
CPU与GPU架构差异:分支控制
由于不同的设计目标,CPU和GPU的架构存在显著差异。CPU拥有大量存储和控制单元,而计算单元相对较少,因此更适合逻辑控制,而不是大规模并行计算。
相反,GPU擅长大规模并行计算,但不擅长逻辑控制。因此,建议避免在着色器中使用逻辑控制语句,如if-else和for循环。以下列出两种芯片在分支控制上的差异:
CPU - 分支预测有序数组和无序数组取值性能比较:
有序数组:16ms
无序数组:6,432ms
有序数组明显更快,速度超无序数组400倍。
通过并行化算法,该代码高效地汇总了一个百万元素数组中所有大于等于 128 的值的总和,实现以下优化:
- 使用多线程处理,大幅提升执行速度。
- 优化数据访问模式,提高内存效率。
- 采用高效的数据结构,优化数据存储和检索。
最终,该算法可快速准确地计算数组中满足条件的值的总和,为数据处理应用提供了高效的解决方案。
以上这段代码,按实验步骤来做。
采用预排序数组优化算法,耗时仅为 5.69566 秒,计算结果 sum 为 312426300000。实验表明,相同的数据和循环次数下,两次运行时间相差巨大。究其原因,在于CPU的分支预测。CPU指令执行分为取指、译码、执行和访存四个步骤。
1. fetch(获取指令)2. decode(解码指令)3. execute(执行指令)4. write-back(写回数据)并行指令执行提升效率。在传统方式下,指令必须顺序执行。然而,CPU可以优化这一过程。
当一条指令开始解码(第二步)时,下一条指令可以开始获取(第一步)。同时,当一条指令开始执行(第三步)时,下一条指令可以解码。
但对于条件判断(如 if 语句),CPU需要预测要执行的语句块。如果预测正确,性能得到提升。若预测错误,CPU 重新执行,并不会损失性能。这种预测机制称为“分支预测”。
GPU - 遮掩GPU条件分支处理
GPU擅长大规模并行计算,缺乏强大的逻辑控制能力,因此没有分支预测。它采用遮罩(mask out)机制处理条件分支。
在锁步执行模式下,GPU每个内核必须完成当前指令的所有步骤才能执行下一条指令。因此,GPU不进行分支预测。
当多个线程同时执行条件分支指令时,满足条件的线程执行相关代码,不满足条件的线程则被遮罩,等待满足条件的线程执行完毕后才能继续执行。
频繁的if-else语句会浪费GPU时间周期。同样,在for循环中使用不固定的循环次数会造成线程等待,导致资源浪费。
在编写Shader时,应尽量避免使用不固定的数值作为逻辑控制条件,以提升GPU效率。
减少调用费时指令因SFU单元稀少,GPU架构中特殊数学函数运算效率较低,导致从缓存或内存读取数据的操作耗时较高。
移动渲染架构的优化及时clear或者discard在 TB(D)R 架构中,数据会累积在 FrameData 中。定期调用 clear 指令可清空 FrameData,释放内存。不再使用的 RenderTexture 应及时 Discard,以提升性能。
例如,渲染前调用 clear 可清空上一次 FrameData。不再使用 RenderTexture 时,应立即 Discard(),释放资源。
不要频繁切换RenderTexture频繁切换 RenderTexture 会加大性能消耗,因为它需要将 Tile 数据反复复制到帧缓冲区。
Early-ZAlpha Test、Clip 和 Discard 渲染模式依赖于像素深度。只有在执行像素着色器后,才能确定像素深度是否写入。• 手动修改GPU插值得到的深度。• 开启透明混合(AlphaBlend)。• 关闭深度测试。Early-Z 优化
早期 Z 剔除 (Early-Z) 易受透明对象和挖孔对象的影响。
当透明对象或挖孔对象通过深度测试并遮挡不透明对象时,背景会出现错误渲染,呈现为 clear 颜色。
因此,无论使用图像内存重映射 (IMR) 还是深度/颜色缓冲区渲染 (T(D)BR) 方法,Early-Z 都会受到 Alpha 测试的影响。
因此要做到以下几点。
渲染流程优化:采用 "Opaque → AlphaTest → AlphaBlend" 顺序,以提高渲染效率。
“Opaque”渲染优化:按“其他不透明物体→地形”顺序渲染,可最大化Early-Z优化覆盖区域,实现高效Overdraw消除。
优化后的文字:避免过度使用 AlphaTest 技术,无论使用 PowerVR、Mali 还是 Adreno 芯片,因为它会影响性能。
为不支持Early-Z的硬件,可采用PreDepthPass多渲染一帧来提升Overdraw优化,但需注意对顶点绘制带来的额外负担。避免大量drawcall和顶点数GPU 架构优化技巧
FrameData 优化:
* FrameData 存储当前帧的顶点数据,占用大量内存。
* 过大的 FrameData 会影响读写速度,尤其是在移动设备上渲染百万顶点或数百 Drawcall 时。
性能优化技巧:
* 减少 FrameData 大小:通过优化几何体或使用实例化技术。
* 优化 Drawcall 数量:批处理渲染调用以降低 FrameData 增长。
* 使用 GPU 内存分页:将 FrameData 分配到不同的内存区域,避免性能下降。
-对此,您有什么看法见解?-
-欢迎在评论区留言探讨和分享。-