图形处理单元

引言:从固定功能到完全可编程

早期的图形硬件是固定功能管线 (Fixed-Function Pipeline),就像一条功能写死的流水线,只能做一些特定的事,比如纹理贴图和深度测试。1999年,NVIDIA GeForce 256的出现标志着GPU (Graphics Processing Unit) 时代的到来,它将顶点处理也集成到了硬件中。

然而,真正的革命是将这条流水线从“写死的”变为“可编程的”。GPU的核心优势在于其高度并行的处理能力,它并非追求单个任务的极致速度,而是追求海量任务的总吞吐量。理解这一点是掌握现代GPU渲染的关键。


3.1 数据并行结构 (Data-Parallel Architectures)

这一节是理解GPU性能哲学的核心。

核心观点:CPU vs. GPU 的不同策略

  • CPU (中央处理器): 为低延迟 (Latency) 优化

    • 策略: 像一位经验丰富的大厨,处理复杂多变的订单(串行任务)。为了不耽误事,它配备了大量高速缓存 (Cache) 来预存食材,并使用分支预测指令重排序等复杂技术来避免任何可能的等待。目标是让单个任务尽快完成
  • GPU (图形处理器): 为高吞-吐量 (Throughput) 优化

    • 策略: 像一个巨大的汉堡工厂,同时处理成千上万个一模一样的订单(并行任务)。它不关心单个汉堡做得有多快,只关心单位时间内能生产多少汉堡。它的核心思想是用海量并行计算来隐藏延迟

关键术语与概念

  • 延迟隐藏 (Latency Hiding): 这是GPU最重要的工作技巧。当一个任务(比如一个像素着色)需要等待耗时操作(如读取纹理)时,处理器不会傻等,而是立刻切换到另一个准备就绪的任务。由于有成千上万个任务在排队,处理器总能找到事情做,从而保持“忙碌”状态,将等待时间完美隐藏。

  • 线程 (Thread): 在GPU语境下,一个线程通常指代一次独立的着色器调用。例如,为一个顶点执行一次顶点着色器,或为一个像素执行一次像素着色器,都构成一个线程。

  • SIMD (Single Instruction, Multiple Data - 单指令,多数据): 这是GPU实现并行化的硬件基础。一组处理器(例如32个)会同时执行完全相同的指令,但每个处理器操作的是它们各自不同的数据(比如各自顶点的坐标或像素的颜色)。

  • Warp / Wavefront: 为了管理和调度,GPU会将一组线程(通常是32个)打包成一个Warp (NVIDIA叫法) 或 Wavefront (AMD叫法)。Warp是GPU调度的基本单位。同一个Warp中的所有线程步调完全一致,执行相同的指令。当一个Warp因为等待内存而停顿时,调度器会换上另一个就绪的Warp,这就是延迟隐藏的具体实现。

  • 占用率 (Occupancy): 指GPU上同时“飞行中 (in flight)”的Warp数量。更高的占用率通常意味着更好的性能,因为它给了调度器更多的选择,从而能更有效地隐藏延迟。如果着色器程序过于复杂,使用了太多寄存器,就会限制同时存在的Warp数量,导致占用率下降。

  • 线程发散 (Thread Divergence): 这是一个重要的性能陷阱。当一个Warp内的线程遇到 if-else 分支,并且走了不同的路径时,就会发生发散。由于硬件的SIMD特性,GPU必须把两个分支的代码都执行一遍,然后根据每个线程的实际情况,丢弃掉不用的那个结果。这会导致部分处理器核心空转,造成性能浪费。


3.2 GPU 管线概述 (GPU Pipeline Overview)

本节描述了从程序员(API)视角看到的逻辑渲染管线。

核心观点:逻辑管线由不同类型的阶段组成

现代GPU的渲染管线并非铁板一块,而是由多个阶段串联而成。根据我们对其的控制能力,可以分为三类:

  • 🟩 完全可编程阶段 (Fully Programmable): 我们可以使用着色器 (Shader) 编写自定义代码来完全控制其行为。这是实现各种渲染效果的核心。

    • 顶点着色器 (Vertex Shader)
    • 曲面细分着色器 (Tessellation Shader) - 可选
    • 几何着色器 (Geometry Shader) - 可选
    • 像素着色器 (Pixel Shader / Fragment Shader)
  • 🟨 可配置阶段 (Configurable): 我们不能编写代码,但可以通过API设置不同的状态和参数来调整其行为。

    • 合并阶段 (Output Merger): 负责将像素着色器的输出与颜色缓冲进行混合、执行深度测试、模板测试等。我们可以设置混合模式(如透明混合),但不能改变混合算法本身。
  • 🟦 固定功能阶段 (Fixed Function): 由专门的硬件实现,其功能是固定的,我们几乎无法控制。这些阶段执行的是高度通用且需要极致性能的操作。

    • 输入装配 (Input Assembler)
    • 光栅化 (Rasterization): 包括三角形设置 (Triangle Setup) 和遍历 (Traversal)。
    • 屏幕映射 (Screen Mapping)

重要提示: 这只是一个逻辑模型,GPU内部的物理实现可能完全不同,但这个模型对于理解和优化渲染流程至关重要。


3.3 可编程着色器阶段 (Programmable Shader Stages)

本节探讨了所有可编程阶段的共同特点。

核心观点:统一的编程模型

现代GPU采用统一着色器架构 (Unified Shader Architecture)。这意味着GPU内部不再有“专门的顶点处理单元”和“专门的像素处理单元”,而是拥有一大池通用的着色器核心 (Common-Shader Core)。这些核心可以根据当前负载,动态地被分配去执行任何类型的着色器任务。这极大地提高了硬件利用率。

关键术语与概念

  • 着色器语言 (Shading Language): 我们使用高级的、类似C的语言来编写着色器,最主流的是:

    • HLSL (High-Level Shading Language): 用于 DirectX。
    • GLSL (OpenGL Shading Language): 用于 OpenGL 和 Vulkan。
  • 着色器输入 (Shader Inputs): 着色器处理的数据分为两大类:

    • 统一输入 (Uniform Input): 在一次绘制调用 (Draw Call) 中保持不变的常量数据。例如:世界变换矩阵、光源颜色、当前时间等。这份数据对所有线程都是共享的。
    • 可变输入 (Varying Input): 每个线程都不同的数据。例如:传入顶点着色器的顶点位置、法线;或者从顶点着色器插值后传入像素着色器的纹理坐标。
  • 流程控制 (Flow Control):if 语句和循环。

    • 静态流程控制 (Static Flow Control): 分支条件依赖于一个统一输入 (Uniform)。由于在一次Draw Call中该值不变,所有线程都会走相同的路径,因此没有性能开销
    • 动态流程控制 (Dynamic Flow Control): 分支条件依赖于一个可变输入 (Varying)。这可能导致线程发散,带来性能损失。

3.4 可编程着色及其API的演变

本节回顾了从固定管线到现代低开销API的进化史,这有助于理解为什么现在的API是这个样子。

核心观点:API的演进方向是给予开发者更多的控制权和更低的CPU开销

时间点API / 硬件关键特性意义
~19963dfx Voodoo固定功能管线消费级3D加速卡时代的开端。
2001DirectX 8.0 (NVIDIA GeForce 3)可编程顶点着色器首次允许开发者通过代码控制顶点变换,是可编程革命的起点。
2002DirectX 9.0 (Shader Model 2.0)真正的可编程像素着色器,引入HLSLGLSL图形效果大爆发的时代,开发者终于可以完全控制像素的最终颜色。
2006DirectX 10 (Shader Model 4.0)统一着色器架构,引入几何着色器硬件架构变得更灵活高效,并增加了在GPU上直接创建/销毁图元的能力。
2009DirectX 11 (Shader Model 5.0)引入曲面细分着色器计算着色器 (Compute Shader)极大地增强了模型的几何细节表现力,并正式将GPU用于通用计算。
2013+Mantle, DirectX 12, Vulkan, Metal低开销API (Low-Overhead API)核心思想是减少驱动程序的CPU开销,将更多控制权直接交给开发者,更好地支持CPU多线程,从而更高效地向GPU提交渲染任务。

移动端与Web端的并行发展

  • OpenGL ES: 针对移动和嵌入式设备裁剪的OpenGL版本,功耗和性能更优。
  • WebGL: 将OpenGL ES的能力带到浏览器中,通过JavaScript调用,实现了跨平台的3D网页应用。

我们继续深入GPU渲染管线的后半部分。这部分内容涉及到了在顶点处理之后、像素着色之前的所有可选阶段,以及最终的像素处理与通用计算,对于引擎开发来说至关重要。

在我们了解了GPU的并行计算哲学后,现在将深入管线的各个具体阶段。你会发现,现代渲染管线不再是一条固定的直线,而是充满了可选的、功能强大的可编程阶段。理解每个阶段的职责、能力和性能特点,是高效利用GPU的关键。


3.5 顶点着色器 (Vertex Shader)

这是我们能直接编程控制的第一个阶段

核心观点:逐顶点处理,负责变换与属性传递

顶点着色器的输入是单个顶点及其属性(位置、法线、UV坐标等),它的主要工作是对这个顶点进行处理,然后将结果传递给下一阶段。

  • 核心职责: 最基本、最重要的任务是进行坐标变换,将顶点位置从模型空间 (Model Space) 变换到齐次裁剪空间 (Homogeneous Clip Space)。这是决定物体最终出现在屏幕上哪个位置的关键一步。
  • 独立性: 每个顶点的处理是完全独立的,它不知道其他任何顶点的存在。这使得GPU可以极致地并行化处理成千上万个顶点。
  • 局限性: 顶点着色器不能创建或销毁顶点。输入N个顶点,就必须输出N个顶点。

关键应用

除了坐标变换,顶点着色器还可以实现各种强大的视觉效果:

  • 动画: 通过骨骼蒙皮 (Skinning)顶点混合 (Vertex Blending) 来实现角色动画。
  • 程序化变形: 模拟旗帜飘动、水面波动、布料模拟等效果。
  • 粒子系统: 通过操作无面积的简并三角形,在GPU上高效地创建和移动大量粒子。
  • 全屏特效: 通过对一个覆盖全屏的网格进行顶点变形,实现透镜畸变、热浪等效果。

注意: 在顶点着色器之前,还有一个叫输入装配器 (Input Assembler) 的固定功能阶段,它负责从内存中读取顶点数据,并将它们“组装”成图元(点、线、三角形),然后才送入顶点着色器。


3.6 曲面细分阶段 (Tessellation Stage)

这是一个可选阶段,位于顶点着色器之后,用于在GPU上动态生成更精细的几何体。

核心观点:按需生成几何细节,实现动态LOD

曲面细分的核心价值在于,我们只需向GPU传入一个由少量控制点组成的**“面片 (Patch)”**,GPU就能根据我们的指令,自动将其细分成成百上千个精细的三角形。

  • 主要优势:
    • 节省带宽和内存: 只需传输粗糙的控制网格,而非高精度模型。
    • 动态细节层次 (LOD - Level of Detail): 可以根据物体离摄像机的距离,动态调整细分程度。近处的物体细分程度高,细节丰富;远处的物体细分程度低,节省性能。

曲面细分的三个子阶段

阶段 (DirectX 叫法)阶段 (OpenGL 叫法)功能类型
壳着色器 (Hull Shader)细分控制着色器决策者:决定这个面片要被“切”成多少块(设置曲面细分因子),并可以调整控制点。🟩 可编程
曲面细分器 (Tessellator)图元生成器执行者:根据壳着色器的指令,在这个面片上生成新的顶点,并定义它们的连接关系。🟦 固定功能
域着色器 (Domain Shader)细分评估着色器计算者:为每个新生成的顶点计算最终的位置、法线、UV等属性。🟩 可编程

3.7 几何着色器 (Geometry Shader)

这同样是一个可选阶段,位于曲面细分之后。它赋予了GPU在管线中凭空创造或销毁图元的能力。

核心观点:逐图元处理,可以创建、销毁和改变图元类型

几何着色器的输入是一个完整的图元(一个点、一条线或一个三角形),以及构成它的所有顶点。

  • 独特能力:
    • 凭空创建: 可以输出0个、1个或多个新的图元。例如,输入一个点,输出一个四边形(用于粒子渲染)。
    • 类型转换: 输入一个三角形,输出三条线段(用于线框渲染)。
    • 图元放大: 输入一个图元,输出多个图元的副本(例如一次性渲染立方体的六个面)。

⚠️ 性能警告: 几何着色器虽然功能强大,但在实践中很少被使用。因为它破坏了GPU高度并行的计算模型(输出数量不确定),在很多硬件上(尤其是移动端)性能表现不佳。通常有更高效的替代方案(如计算着色器)。

流式输出 (Stream Output)

  • 概念: 这是一个功能,而非一个独立的阶段。它允许我们将顶点着色器、曲面细分或几何着色器的输出结果直接写回到GPU的内存缓冲区中,而不是非得送去光栅化。在OpenGL中它被称为变换反馈 (Transform Feedback)
  • 用途: 对于多趟处理 (Multi-pass) 的算法至关重要。例如,在第一趟中通过流式输出更新粒子的位置,在第二趟中再将这个存有新位置的缓冲区作为输入进行渲染。

3.8 像素着色器 (Pixel Shader)

当几何数据处理完毕后,光栅化器 (Rasterizer) 这个固定功能单元会计算出每个三角形覆盖了屏幕上的哪些像素,并为这些像素生成片元 (Fragment)。然后,像素着色器登场。

核心观点:逐片元处理,计算最终颜色

像素着色器(在OpenGL中称为片元着色器 (Fragment Shader))是决定一个像素最终颜色的地方。

  • 输入: 接收从顶点属性插值而来的数据(如UV坐标、颜色、法线)。
  • 核心职责: 执行光照计算、纹理采样等操作,最终输出一个颜色值 (RGBA)
  • 关键能力:
    • 丢弃片元 (discard): 可以提前终止对某个片元的处理,使其不被写入屏幕。这是实现透明度测试 (Alpha Test) 或自定义裁剪平面的基础。
    • 多重渲染目标 (MRT - Multiple Render Targets): 可以一次性向多个缓冲区输出不同的数据。这是延迟着色 (Deferred Shading) 等高级渲染技术的基础。例如,同时输出颜色、法线、深度到不同的纹理中。
    • 无序访问视图 (UAV / SSBO): 允许像素着色器读写GPU内存中的任意位置,极大地增强了其能力,但也需要通过原子操作 (Atomic Operations) 来避免多线程写入冲突。

3.9 合并阶段 (Merge Stage)

这是图形管线的最后一个阶段,它将像素着色器的输出与屏幕上的帧缓冲 (Framebuffer) 内容进行合并。

核心观点:高度可配置的最终合成

这个阶段虽然不是完全可编程的,但我们可以配置它的行为来执行各种测试和混合操作。

  • 主要操作:

    • 深度测试 (Z-Test): 决定片元是否因为被其他物体遮挡而需要被丢弃。
    • 模板测试 (Stencil-Test): 基于模板缓冲的值进行测试,可以实现很多高级效果(如轮廓光、阴影体积)。
    • 颜色混合 (Blending): 将片元颜色与帧缓冲中已有的颜色进行混合,是实现半透明效果的关键。
  • Early-Z 优化: 这是一个至关重要的性能优化。现代GPU会尝试在执行像素着色器之前就进行深度测试。如果一个片元注定被遮挡,就完全没必要为它运行复杂的像素着色器。如果在像素着色器中修改了片元的深度值,或者使用了discard,就可能会破坏Early-Z优化,导致性能下降。


3.10 计算着色器 (Compute Shader)

计算着色器是一种不属于传统图形渲染管线的特殊着色器,它让我们能更自由地利用GPU进行通用并行计算 (GPGPU)。

核心观点:独立于图形管线的通用并行计算单元

  • 灵活性: 它不接收特定的图元输入,也不需要输出颜色。我们可以定义任意的输入输出数据(缓冲区、纹理),并组织成线程组 (Thread Group) 来执行。
  • 线程间通信: 同一个线程组内的线程可以访问一块共享内存 (Shared Memory),这使得它们可以高效地协同工作,这是其他类型着色器不具备的强大能力。
  • 应用场景: 非常广泛,几乎所有适合大规模并行的任务都可以用它来加速。
    • 后期处理: 如高斯模糊、景深、色调映射等。
    • 物理模拟: 粒子系统、布料、流体模拟。
    • 渲染优化: 视锥剔除、遮挡剔除等。
    • 非图形领域: 深度学习、数据分析等。

计算着色器通常比几何着色器等方式更高效、更灵活,是现代引擎中不可或缺的工具。