GPU 架构初探
一、绘制光栅图像的基本原理
1.1 物体形状的表示与处理
核心观点: 计算机通过将三维物体拆解成简单的、可处理的基本图元(主要是三角形),并利用齐次坐标与矩阵变换来确定它们在屏幕上的最终位置。
- 
图元 (Primitive): 计算机图形学中用于构建复杂形状的最基本几何元素。 - 核心图元是三角形: GPU 选择三角形作为渲染的基本单位,因为三点确定一个平面。这保证了图元的平坦性(Planarity),极大地简化了后续的光栅化计算和硬件设计。任何复杂多边形都可以被拆解为三角形。
 
- 
网格曲面 (Mesh Surface): 通过大量的三角形拼接,来逼近和表示复杂三维物体的表面。这些模型由一系列顶点构成,每个顶点包含位置、法线、颜色、纹理坐标等属性。 
- 
坐标变换 (Coordinate Transformations): 一个顶点在最终显示到屏幕上前,需要经历一系列坐标空间的变换。这个过程本质上是向量与矩阵的乘法。 - 变换流水线: 局部坐标系 (Model Space) → 世界坐标系 (World Space) → 观察坐标系 (View Space) → 投影坐标系 (Projection Space) → 屏幕坐标系 (Screen Space)。
 
- 
齐次坐标 (Homogeneous Coordinates): - 核心思想: 使用 维向量来表示 维空间中的点或向量。例如,用四维向量 表示三维空间。
- 关键特性:
- 当 时,表示一个点 。
- 当 时,表示一个方向向量 。
 
- 核心优势: 统一了平移、旋转、缩放等所有几何变换,使其都可以通过单一的矩阵乘法来完成,简化了数学表达和硬件实现。
 
- 
裁剪 (Clipping) 与 剔除 (Culling): - 裁剪 (Clipping): 在投影变换后,由硬件自动执行,用于切掉部分超出视锥体的图元。
- 剔除 (Culling): 通常指在流水线早期完整移除某些图元,例如背面剔除 (Back-face Culling) 会移除那些背对摄像机的三角形,以提升效率。
 
1.2 光栅化与像素生成
核心观点: 光栅化是将矢量化的几何图元(三角形)转换为像素集合(片元)的过程,随后经过一系列测试和计算,最终确定每个像素的颜色。
- 
光栅图像 (Raster Image): 屏幕是由一个二维的像素矩阵构成的,光栅图像(或称位图)就是为这个矩阵中的每一个像素精确定义颜色值。 
- 
光栅化 (Rasterization): - 核心任务: 判断一个三角形覆盖了屏幕上的哪些像素中心点。
- 生成片元 (Fragment): 对于每个被三角形覆盖的像素位置,生成一个片元。片元可以理解为一个“潜在的像素”,它携带了从三角形顶点插值而来的各种属性(如颜色、深度值、纹理坐标等)。
 
- 
片元处理流水线 (Fragment Processing): 生成的片元需要经过一系列操作才能最终写入屏幕。 - 着色 (Shading): 计算片元的最终颜色。这是通过片元着色器完成的,它会利用片元的属性(如法线、纹理坐标)和光照模型(如 Phong 模型)来进行计算。
- 深度测试 (Depth Test): 使用 Z-Buffer 算法,比较当前片元的深度值与 framebuffer 中已存储的深度值。如果当前片元更远(被遮挡),则被丢弃。这是实现三维场景遮挡关系的关键。
- 模板测试 (Stencil Test): 类似深度测试,但依据模板缓冲区的值来决定是否丢弃片元,常用于实现复杂的遮罩效果(如镜面、轮廓光)。
- 颜色混合 (Color Blending): 将通过测试的片元颜色与 framebuffer 中已有的颜色按照特定规则进行混合,用于实现半透明效果。
 
二、初探 GPU 架构
2.1 绘制流水线与流式编程模型
核心观点: GPU 专为高吞吐量的并行计算而设计,其架构完美契合流式编程模型,这使其在图形渲染等领域远超 CPU。
- 
GPU vs. CPU 设计哲学: - CPU: 为低延迟 (Low Latency) 设计,擅长复杂的逻辑控制和串行任务。
- GPU: 为高吞吐量 (High Throughput) 设计,拥有大量计算核心,擅长处理大规模、独立的并行数据。
 
- 
流式编程模型 (Stream Programming Model): GPU 计算的核心思想。 - 流 (Stream): 一系列类型相同的数据元素的有序集合(如顶点流、像素流)。
- 核 (Kernel): 一个函数,它会被独立地应用到流中的每一个元素上。
- 核心特性: 流中各元素的计算相互独立。这使得计算可以被大规模并行化,并且硬件可以预先优化数据访存,从而有效隐藏内存延迟。
 
- 
多层次并行机制 (Multi-level Parallelism): - 任务级并行: 流水线的不同阶段(如顶点处理和片元处理)可以同时处理不同的数据。
- 数据级并行: 同一阶段(一个 Kernel)可以同时处理流中的多个元素。
- 指令级并行: 处理单个元素时,没有数据依赖的指令可以并行执行。
 
2.2 从固定功能到可编程流水线
核心观点: GPU 架构从早期功能写死的固定功能流水线,演变成了高度灵活的可编程流水线,将渲染过程中的关键步骤开放给开发者自定义,催生了现代计算机图形学的巨大飞跃。
- 
固定功能流水线 (Fixed-Function Pipeline): - 特点: 早期的 GPU 将渲染流水线的每个阶段(如坐标变换、光照计算)都用专用硬件实现,功能固定,开发者只能通过 图形 API (Graphics API) 调整预设参数。
- 代表 API 版本: 早期的 OpenGL 和 Direct3D (如 Direct3D 7 及以前)。
 
- 
可编程流水线的诞生: - 里程碑: 2001 年,NVIDIA GeForce 3 显卡发布,首次支持可编程着色器。Direct3D 8 和后续的 OpenGL 2.0 正式将此概念引入 API。
- 核心变革: 允许开发者编写自定义程序(Shader),来替代流水线中的某些固定功能模块。
 
- 
核心可编程阶段: - 顶点着色器 (Vertex Shader): 针对每个顶点执行的程序。开发者可以完全控制顶点的位置、颜色等属性,从而实现复杂的模型动画、变形、粒子效果等。
- 片段着色器 (Fragment Shader / Pixel Shader): 针对每个片元执行的程序。开发者可以实现任意复杂的光照模型、材质效果和后处理特效,精细控制最终像素的颜色。
 
- 
最终演进: 随着技术成熟,固定功能流水线被完全移除(以 Direct3D 10 和 OpenGL 3.1 为标志),可编程流水线成为现代 GPU 的标准架构。这一变革不仅极大地丰富了图形表现力,也为 GPGPU(通用 GPU 计算) 的发展奠定了基础。 
顶点/像素着色器、统一架构与 SIMT 模型
一、着色器编程入门 (Introduction to Shader Programming)
核心观点: 开发者通过 C++ 等高级语言调用图形 API (Graphics API),将顶点数据、纹理等资源和编译好的着色器程序 (Shader) 提交给 GPU。着色器是运行在 GPU 上的小型程序,用于控制渲染流水线的可编程阶段。
1.1 高级着色器语言:GLSL vs. HLSL
为了取代早期类似汇编的复杂指令,现代图形开发普遍使用高级着色语言。其中最主流的是 GLSL 和 HLSL。
| 特性对比 | GLSL (OpenGL Shading Language) | HLSL (High-Level Shading Language) | 
|---|---|---|
| 主要用途 | OpenGL, Vulkan | Direct3D (Windows, Xbox) | 
| 版本控制 | 文件内声明 ( #version 330 core) | 编译时通过参数指定 (Shader Model) | 
| 数据流定义 | 使用 in,out,uniform等存储限定符 | 使用语义 (Semantics) 附加在变量后 (如 : SV_POSITION) | 
| 入口点函数 | 统一命名为 void main() | 可自定义多个不同名称的入口点函数 | 
| 资源绑定 | 使用 layout(binding = ...)进行显式布局 | 使用 register(b0, t0, ...)显式绑定到虚拟寄存器 | 
| Vulkan 特性 | 需预编译成二进制中间格式 SPIR-V | - | 
1.2 核心概念:资源绑定与描述符
核心观点: 为了让着色器能访问纹理、缓冲区等外部资源,现代图形 API 引入了描述符 (Descriptor) 机制,它是一种标准化的资源“指针”,解耦了着色器代码与具体的资源内存。
- 描述符 (Descriptor): 一块包含访问资源所需全部信息的数据结构。例如,一个纹理的描述符会包含其内存地址、尺寸、格式等信息。
- API 的解决方案:
- Vulkan: 描述符集 (Descriptor Set): 将资源描述符分组管理。开发者通常根据资源的更新频率(例如,每帧更新的、每个物体更新的)来组织描述符集,便于驱动进行优化。
- Direct3D 12: 描述符堆 (Descriptor Heap): 一块连续的内存,用于存放大量的描述符。开发者拥有更高的控制权,可以直接管理这块内存。
 
二、GPU 架构的演进
2.1 从分离到统一:统一着色器架构
核心观点: 现代 GPU 采用统一着色器架构,将计算资源整合为统一的处理器池,可以根据实时需求灵活处理顶点、像素或其他任何着色任务,极大地提高了硬件利用率。
- 
旧的“分离式架构”: - 设计: 拥有两种不同的、专门化的处理单元:一组用于顶点着色,另一组用于像素着色。
- 缺陷: 资源无法动态调配。当场景顶点负载高而像素负载低时,像素处理单元闲置;反之亦然。这导致了严重的硬件资源浪费。
 
- 
现代的“统一着色器架构 (Unified Shader Architecture)”: - 设计: 将所有处理单元设计成同一种通用的流式处理器 (Streaming Processor, SP)。
- 优势: 任何一个 SP 都可以执行任何类型的着色器代码(顶点、像素、几何等)。GPU 的调度器会根据当前任务负载,动态地将任务分配给空闲的 SP,实现了负载均衡和效率最大化。
- 推动力: Direct3D 10 API 的标准要求是推动这一架构变革的重要因素。
 
2.2 并行计算模型:VLIW, SIMD, 与 SIMT
核心观点: GPU 的并行计算模型从依赖编译器静态调度的 VLIW 演进到了由硬件动态调度的 SIMT 模型,从而更高效地处理复杂的、带有分支的现代着色器程序。
- 
SIMD (Single Instruction, Multiple Data): 单指令,多数据。这是所有并行处理器的基础。一条指令可以同时在多个数据上执行。例如, a = b + c这条指令可以同时对 16 组不同的b和c进行求和。
- 
VLIW + SIMD (早期 AMD TeraScale 架构): - VLIW (超长指令字): 编译器负责将多条没有相互依赖的简单指令静态打包成一条“超长指令”,让硬件在一个周期内同时执行。
- 缺点: 严重依赖编译器的优化能力。当着色器逻辑复杂、分支多时,编译器很难找到足够多的独立指令来填满一个 VLIW 包,导致大量硬件计算单元被闲置,执行效率低下。
 
- 
SIMT + SIMD (现代 NVIDIA 和 AMD GCN 之后架构): - SIMT (单指令,多线程): 硬件负责调度。它将大量线程(例如 32 或 64 个)打包成一个执行单元(Warp 或 Wavefront)。硬件每次只发射一条指令,这个指令会被包内的所有活动线程同时执行。
- 优势: 灵活性和效率极高。硬件调度器可以在运行时动态处理分支(通过禁用不满足分支条件的线程)和隐藏内存延迟(当一个 Warp 等待内存时,调度器可以切换到另一个就绪的 Warp 继续执行)。
 
2.3 现代 GPU 并行编程抽象
核心观点: 尽管底层硬件以 Warp/Wavefront 为单位进行调度,但上层编程模型(如 CUDA)提供了更易于管理的抽象层次,让开发者能够组织和控制海量的并行任务。
以下是 NVIDIA (CUDA) 和 AMD 描述相似概念时使用的不同术语:
| 抽象层级 | NVIDIA / CUDA 术语 | AMD 术语 | 说 明 | 
|---|---|---|---|
| 整个计算任务 | 网格 (Grid) | 网格 (Grid) | 对应一个完整的 Kernel 调用,例如处理一张图像的所有像素。 | 
| 线程分组 | 线程块 (Thread Block) | 工作组 (Work Group) | 一组被调度到同一个计算单元(SM/CU)上执行的线程,可以共享该单元的局部内存。 | 
| 硬件调度单元 | Warp | Wavefront | 硬件并行执行的基本单位,通常为 32 或 64 个线程,它们严格按照相同顺序执行相同指令。 | 
| 最小工作单位 | CUDA 线程 (Thread) | 工作项 (Work Item) | 执行计算的最小实体,通常对应一个数据元素(如一个顶点或一个像素)。 | 
描述符绑定硬件实现差异
核心问题
Vulkan 的一大复杂性来源是它需要支持种类极其繁多的硬件,而不同厂商在描述符绑定 (Descriptor Binding) 这一核心功能上的硬件实现方式千差万别。为了理解 Vulkan API 设计背后的考量,我们需要先了解底层硬件的几种主流实现思路。
四种主流硬件绑定方法
硬件上的描述符绑定机制大致可以分为以下四大类,每种都有其独特的优缺点。
1. 直接访问 (Direct Access - D)
- 工作方式: 着色器 (Shader) 直接将完整的描述符信息(可以理解为一个“原始指针”)传递给访存指令。
- 优点: 极其灵活。因为描述符可以存放在内存的任何位置。
- 缺点: 效率较低。每次访问资源都需要在着色器中传递整个描述符,数据传输量大。
2. 描述符缓冲区 (Descriptor Buffers - B)
- 工作方式: 描述符被统一存储在一个或多个缓冲区 (Buffer) 中。着色器只需要知道这个缓冲区的基地址和一个指向目标描述符的偏移量 (Offset) 即可。
- 优点: 比“直接访问”更高效,因为着色器只需传递一个较小的偏移量。更换绑定的缓冲区代价也相对较低。
- 缺点: 硬件需要先从描述符缓冲区中读取描述符,然后再进行真正的资源访问,多了一步间接操作。
3. 描述符堆 (Descriptor Heaps - H)
- 工作方式: 所有同类型的描述符都存放在一个全局的表或堆 (Global Table/Heap) 中。着色器通过一个索引 (Index) 来访问。
- 优点: 着色器传输数据量最小,效率最高,因为只需要一个简单的索引。
- 缺点: 非常不灵活,且更换代价极高。更改全局描述符堆通常会导致整个 GPU 停顿 (GPU stall) 并清空缓存。
4. 固定硬件绑定 (Fixed HW Bindings - F)
- 工作方式: 这是最传统的方式。资源被绑定到数量有限的、固定的硬件槽位 (Slot) 上,通常通过命令流直接设置寄存器来完成。
- 优点: 硬件实现简单。
- 缺点: 灵活性最差,不符合现代“Bindless”的设计趋势。在现代硬件上,这种方式通常只用于渲染目标 (Render Target)、顶点/索引缓冲区等固定功能的部件。
主流硬件厂商方案对比
下表展示了当今主流 GPU 厂商针对不同资源类型所采用的绑定方法:
| 硬件厂商 | 纹理 (Textures) | 图像 (Images) | 采样器 (Samplers) | 边界颜色 | 类型化缓冲区 | UBOs | SSBOs | 
|---|---|---|---|---|---|---|---|
| NVIDIA (Kepler+) | H | H | H | H | D/F | D | |
| AMD | D | D | D | H | D | D | D | 
| Intel (Skylake+) | H | H | H | H | H/D/F | H/D | |
| Intel (pre-Skylake) | F | F | F | F | D/F | F | |
| Arm (Valhall+) | B | B | B | B | B/D/F | B/D | |
| Arm (pre-Valhall) | F | F | F | F | D/F | D | |
| Qualcomm (a5xx+) | B | B | B | B | B | B | |
| Broadcom (vc5) | D | D | D | D | D | D | 
注意: Intel pre-Skylake 架构虽然被标记为固定绑定 (F),但它实际上是一种更灵活的、带有间接层的类堆模型(Binding Table),为后续架构的演进提供了基础。
几何着色器、曲面细分与计算着色器
一、几何着色器 (Geometry Shader, GS)
核心观点: 几何着色器是渲染管线中的一个可选阶段,它弥补了顶点着色器无法处理完整图元的缺陷,允许在 GPU 上动态地创建、销毁或修改几何体。
1.1 核心功能与应用场景
- 
解决的问题: - 顶点着色器 (VS) 的局限: VS 每次只能处理单个顶点,无法访问同一图元中的其它顶点信息,也无法增删顶点。
- GS 的突破: GS 的输入是完整的图元(如一个点、一条线或一个三角形),使其能够进行更复杂的几何操作。
 
- 
核心能力: - 访问图元所有顶点: 可以基于整个图元的形状进行计算(如计算法线、判断朝向)。
- 增删图元: 可以不输出任何图元(实现剔除),也可以输出一个或多个新图元。
- 改变图元类型: 可以输入一个点,输出一个四边形(由两个三角形 strip 构成),常用于公告板 (billboard) 或粒子效果。
 
- 
典型应用:动态曲线细分 - 为了绘制平滑的贝塞尔曲线,可以只向 GPU 传入 4 个控制点。
- GS 接收这 4 个点,根据其到摄像机的距离动态计算一个合适的细分等级。
- 在着色器内部循环,利用贝塞尔参数方程生成一系列细分的顶点,最后输出一条由多个短线段构成的 line_strip。
- 三次贝塞尔曲线参数方程:
 
1.2 严重的设计局限与性能问题
核心观点: 尽管功能强大,但几何着色器因其动态可变的输出数量,在现代 GPU 宽 SIMD 架构上存在严重的性能瓶颈,应谨慎使用或避免使用。
- 并行效率低下: GPU 以 Warp/Wavefront (一组 32 或 64 个线程) 为单位执行。如果组内一个 GS 线程输出 100 个顶点,而其它线程只输出 2 个,那么整个组必须等待最慢的那个线程执行完毕,导致大量计算单元闲置和“线程发散”(Divergence)。
- 内存与带宽压力: GPU 需要为 GS 的输出预留最坏情况下的内存空间,这可能导致缓冲区被放置在较慢的显存中,带来高延迟和带宽瓶頸。
- 破坏顶点缓存: GS 生成的每个顶点都被视为全新的,无法利用顶点后处理缓存 (Post Transform Cache) 来复用相同的顶点,增加了额外的计算负担。
- 适用建议: 仅用于处理整个图元的少量任务,如轮廓线检测,不应用于大规模的几何体生成或曲面细分。
二、曲面细分阶段 (Tessellation Stage)
核心观点: 曲面细分是 Direct3D 11 / OpenGL 4.0 引入的、专用于高效生成平滑表面细节的硬件加速阶段。它通过将低多边形的控制面片 (Patch) 细分为大量微小三角形,实现了高质量的动态细节层次 (LOD)。
2.1 为何需要曲面细分?
几何着色器不适合做大规模、规则的表面细分。曲面细分阶段通过更结构化、更适合并行处理的方式解决了这一问题,避免了 GS 的性能陷阱。
2.2 曲面细分的三个核心阶段
曲面细分由三个协同工作的单元组成:
- 外壳/控制着色器 (Hull/Tessellation Control Shader - HS/TCS):
- 类型: 可编程。
- 任务: 接收输入的控制点组成的面片 (Patch),并决定“如何细分”。它计算并输出细分因子 (Tessellation Factors),告诉下一阶段要生成多少新的顶点。
 
- 镶嵌器 (Tessellator):
- 类型: 固定功能硬件单元。
- 任务: 接收 HS/TCS 输出的细分因子,并据此生成新顶点的参数化坐标 (如 UV 坐标)。它只负责生成细分的**“模式”或“拓扑结构”**,不计算实际的顶点位置。
 
- 域/评估着色器 (Domain/Tessellation Evaluation Shader - DS/TES):
- 类型: 可编程。
- 任务: 接收来自镶嵌器的一个参数化坐标,并访问整个面片的所有原始控制点。它的核心工作是根据参数方程(如贝塞尔、NURBS)计算出新顶点的最终位置和其它属性。
 
2.3 相对优势与局限
- 优势:
- 高度并行友好: 其工作模式(特别是 DS/TES)与顶点着色器类似,非常适合 GPU 的并行架构。
- 硬件加速: 镶嵌器是专用的高速硬件,效率极高。
- 带宽高效: 只需要传输低分辨率的控制点,大大节省了内存带宽。
 
- 局限:
- 灵活性较低: 仅适用于对表面进行规则化的细分,无法像 GS 那样进行任意的几何创造。
 
三、通用计算的演进:从流输出到计算着色器
3.1 流输出 / 变换反馈 (Stream Output / Transform Feedback)
核心观点: 该机制允许将几何处理阶段 (VS 或 GS) 的输出结果直接捕获到 GPU 缓冲区中,而无需经过光栅化,是 GPGPU 的早期雏形。
- 解决的问题: 在此之前,如果想在 GPU 上复用顶点变换后的数据(例如用于物理模拟或多趟渲染),唯一的办法是将其“绘制”到纹理中,流程繁琐且低效。
- 核心功能: 在几何处理和光栅化之间提供一个“旁路”,将顶点数据流直接写入 Buffer。
- 局限性: 仍然受限于图形渲染管线的框架和严格的输出顺序要求,在某些架构上性能开销较大。Vulkan 最初甚至没有包含此功能。
3.2 计算着色器 (Compute Shader, CS)
核心观点: 计算着色器是一个独立于传统图形管线的、为通用并行计算 (GPGPU) 而设计的强大工具。
- 
核心特性: - 独立管线: 它不属于 "VS → GS → PS" 的流程,通过 Dispatch命令而非Draw命令启动。
- 通用内存访问: 可以自由地对 GPU 内存(缓冲区、纹理)进行读写,不受图形管线输入/输出的限制。
- 灵活的线程组织: 开发者可以定义一个三维的计算网格,将任务划分为线程组 (Thread Group) / 工作组 (Work Group),并进一步在组内进行同步和数据共享。
 
- 独立管线: 它不属于 "VS → GS → PS" 的流程,通过 
- 
并行编程抽象(NVIDIA vs. AMD): 
| 抽象层级 | NVIDIA / CUDA 术语 | AMD 术语 | 说 明 | 
|---|---|---|---|
| 整个计算任务 | 网格 (Grid) | 网格 (Grid) | 对应一次 Dispatch调用的全部工作。 | 
| 线程分组 | 线程块 (Thread Block) | 工作组 (Work Group) | 一组被调度到同一个计算单元 (SM/CU) 上的线程。 | 
| 硬件调度单元 | Warp | Wavefront | 硬件并行执行的基本单位,通常为 32 或 64 个线程。 | 
| 最小工作单位 | CUDA 线程 (Thread) | 工作项 (Work Item) | 执行计算的最小逻辑实体。 |