图形处理单元
引言
第一个包含硬件顶点处理的消费级芯片(NVIDIA GeForce256)于 1999 年发布。
NVIDIA 创造了图形处理单元 (graphics processing unit,GPU)这个术语,用来区别 GeForce256 和过去所使用的光栅化芯片,并将这个术语沿用了下来
各种可编程的着色器(shader)是控制 GPU 的主要手段。为了获得更高的效率,渲染管线中的有些部分仍然只是可配置的,而并非是可编程的,但是 GPU 的整体发展趋势是可编程性和灵活性。
我们需要知道一点,由于在存储中访问数据需要花费一定时间,因此延迟是所有处理器都会面临的一个问题
数据并行结构
CPU
不同的处理器架构使用了不同的策略来避免停滞。CPU 经过优化,可以处理大量的数据结构和大型代码段,CPU 一般都具有多个处理器,每个处理器都以串行的方式来执行代码。为了最小化延迟所带来的影响,CPU芯片中的大部分面积都是高速的本地缓存,这些缓存中存满了接下来可能会用到的数据。CPU也还会使用一些智能技能来避免停滞,比如 分支预测 等…
GPU
GPU 采用了不同的策略,GPU 芯片中的很大一片面积都是大量的处理器,也叫做着色器核心,GPU芯片中通常会有数千个着色器核心。
GPU 是一个流处理器,他会依次处理有序的相似数据。由于数据的相似性(例如一组顶点或者像素),因此 GPU 可以通过大规模并行的方式来处理这些数据
GPU 专门对吞吐量(throughput)进行了优化,吞吐量指的是数据能够被处理的最大速度。但是这种快速处理是有代价的,由于用于缓存和控制逻辑的芯片面积较少, 因此每个着色器核心的延迟,通常都会比 CPU 处理器所遇到的延迟要大
SIMD
当遇到令着色处理器停滞的指令时(例如:对于一个给定的表面位置,程序需要知道纹理上对应位置的像素颜色,而这个纹理是一个独立的资源,并不是像素着色程序本地内存中的一部分, 因此可能会涉及到访问纹理的操作),我们通过切换并执行其他片元程序的方式,来让 GPU 时刻保持忙碌,从而避免延迟。
更进一步,GPU 可以将指令执行的逻辑与数据分离开来,这种设计叫做单指令、多数据(single instruction, multiple data,SIMD)
这种设计会在固定数量的着色器程序上,以一个固定的步长来执行完全相同的指令
wrap / wavefronts
每个片元的像素着色器调用都可以被称为一个线程,但是这里所说的线程不同于 CPU 的线程,他还包括用于存储着色其输入数据的存储空间,以及用于着色器执行的任何寄存器空间。这些用相同着色器程序会被打包成组
NVDIA将其称为一个 wrap,AMD是称为一个 wavefront
一个 warp / wavefronts 负责调度一定数量的 GPU 处理核心, 可能是 8 到 64 个,并且都会使用 SIMD 处理。每个线程都会被映射到一个 SIMD 通 道(SIMD lane)。
影响 GPU 运行效率的点
-
如果线程的数量很少,那么就只能创建很少的 warp,这可能就没法有效隐藏延迟
-
影响执行效率的另一个重要特征是着色器程序的结构,其中最重要的一个因素就是每个线程所使用的寄存器数量。
-
另一个影响整体运行效率的因素是由
if语句和循环语句导致的动态分支(dynamic branching)。假设现在我们的着色器程序中遇到了一个
if语句,如果所有线程都进入了相同的分支,那么这个 warp 可以不用管其他的分支,继续执行进入的那个分支即可。但是,如果其中有几个线程,甚至是只有一个线程进入了其他的分支,那么 这个 warp 就必须把两个分支都执行一遍,然后再根据每个线程的具体情况,丢弃不需要的结果。这个问题叫做线程发散(thread divergence),它意味着有 一些线程需要去执行一个循环操作,或者是进入了所在 warp 中其他线程都没有进入 的“if”分支,这会导致其他的线程空转。
GPU 管线概述

顶点着色器是一个完全可编程的阶段,它用于实现渲染管线中的几何处理阶段。
几何着色器也是一个完全可编程的极端,它可以对图元(点,线,三角形)的顶点进行操作,它也可以用于进行一些逐图元的着色操作,销毁图元或者是创建新图元等
曲面细分阶段和几何着色器都是可选的阶段,但并不是所有的 GPU都支持这两个阶段,尤其是移动设备上的 GPU
裁剪,三角形设置和三角形遍历阶段,都是由固定功能的硬件进行实现。
屏幕映射收到窗口和视口设置的影响,其内部包含了一个简单的缩放和重定位功能。
像素着色器阶段是一个完全可编程的阶段
合并阶段尽管不是可编程的,但是他是高度可配置的,我们可以为其设定各种各样的操作。合并阶段实现了渲染管线中的合并功能,负责修改维护颜色缓冲,Z-Buffer,模板缓冲以及其他任何与输出相关的缓冲区
像素着色器和合并阶段一起,组成了概念化的像素处理阶段
可编程着色器阶段
数据类型
32位精度的浮点标量和浮点向量是最基础的数据类型,现在GPU还会支持 64为浮点数,32位整数。诸如结构体、数组和矩阵等聚合类型,也同样被GPU支持
浮点向量通常用来表示位置(xyzw),向量,矩阵中的某一行,颜色(rgba),或者纹理坐标(uvwq)
整数通常用来表示计数器、索引或者位掩码等
常见操作运算
图形计算中的常见操作和运算都可以在现代 GPU 上高效运行,着色语言通过操作符来暴露最常用的操作。例如:
- 加法乘法操作符是
+&*
其余的操作可以通过使用内置函数(intrinsic function)来提供,这些内置函数针对 GPU 进行了专门优化。例如:
atan()sqrt()log()- …
还有一些函数可以提供更加复杂的操作,例如:
- 向量的标准化
- 反射向量
- 向量的叉乘
- …
流程控制
流程控制(flow control)这个术语,是指使用分支指令来改变代码执行的流程
例如“if”、“case”以及各种类型的循环
着色器支持两种类型的流程控制,包含静态流程控制(static flow control)以及动态流程控制(dynamic flow control)
静态流程控制
其中静态流程控制的分支情况会基于统一输入的值,这意味着在一次 draw call 中,该代码的流程是恒定不变的。
静态流程控制最主要的好处在于,它可以在各种不同的情况下(例如不同数量的光源)使用相同的着色器,并且在这个过程中没有任何的线程发散,因为所有调用都会进入相同的代码路径。
动态流程控制
动态流程控制则基于可变输入的值,它意味着每个片元都可以执行不同的代码,其功能比静态流程控制更加强大,但是也更消耗性能,尤其是当着色器调用之间,代码流程不规则变化时。
可编程着色及其 API 的演变
- 可编程着色框架的想法可以追溯到 1984 年 Cook 所提出的着色树(shade tree)
- 3dfx 交互公司于 1996.10.01,首次引入了消费级的图形硬件
- 2001 年初,NVIDIA 推出了 Geforce3 显卡,这是第一个支持可编程顶点着色器的 GPU ,它通过 DirectX 8.0 来暴露相关接口,并可以扩展到 OpenGL。
- 这时的着色器不允许包含流程控制(分支),如果着色器中包含分支,那么就需要将 两个分支都执行一次,然后在结果中进行选择或者插值来模拟分支。
- DirectX 定义了 着色器模型(Shader Model)的概念来区分具有不同功能的着色器。
- 2002 年微软推出了包含 Shader Model 2.0 的 DirectX 9.0,它支持真正可编程的顶点着色器和像素着色器。同样的功能也可以在 OpenGL 下使用各种扩展来实现。
- Shader Model 3.0 于 2004 年推出,并增加了动态流程控制,这使得着色器更加强大
- 新世代的游戏主机分别于 2005 年底 (Microsoft Xbox 360)和 2006 年底(Sony 计算机娱乐 PS3)推出,他们都配备了支持 Shader Model 3.0 的 GPU。Nintendo 于 2006 年底推出了 Wii 主机,它是最后一个仅支持固定功能 GPU 的著名主机。
- 着色器可编程性的下一次跨域也出现在 2006 年底,DirectX 10.0 推出了 Shader Model 4.0 [175],它引入了几个重要特性,例如几何着色器和流式输出。Shader Model 4.0 包含了一个针对所有着色器(顶点着色器、像素着色器和几何着色器)的 统一编程模型,即我们在前文中描述过的标准着色器设计。并且它进一步扩大了资源 范围,同时支持了整数类型的数据(包括位运算等操作)。在 OpenGL 3.3 中所引入 的 GLSL 3.0,也推出了一个类似的着色器模型。
- 2009 年发布的 DirectX 11 和 Shader Model 5.0 中,增加了曲面细分着色器和计算着色器,计算着色器也被叫做 DirectCompute。这次发布还关注了如何提高 CPU 多线程处理的效率
- 图形 API 的下一个重大变化是由 AMD 于 2013 年提出的 Mantle API,它是 AMD 和 电子游戏开发商 DICE 一起合作开发的,其核心想法是去除用于图形驱动程序的大量开销,将控制权直接交给开发者。除此之外,该技术还进一步支持了 CPU 多线程的高效处理,这一类 API 专注于如何减少 CPU 花费在驱动上的时间,以及如何更加高效的利用 CPU 的多个核心
- 2015 年推出了全新的 DirectX 12.0。这里请注意,DirectX 12 并没有增加更多的 GPU 功能——DirectX 11.3 和 DirectX12 具有完全相同的硬件特性。
- Apple 于 2014 年推出了自家叫做 Metal 的低开销 API,Metal 首先用于移动设备, 例如 iPhone 5s 和 iPad Air。
- AMD 将自身 Mantle 的工作贡献给了 Khronos 组织,后者于 2016 年推出了新一代 的 API,叫做 Vulkan。与 OpenGL 一样,Vulkan 可以用于多个操作系统。Vulkan 使 用了一种被称为 SPIR-V 的全新高级中间语言,它可以同时用于着色器表示和通用 GPU 计算。
- 在移动设备上一般会使用 OpenGL ES,其中“ES”代表的是嵌入式系统(embedded system),因为这个 API 是针对移动设备进行开发的;
顶点着色器
在进入这个阶段之前,就已经存在一些数据计算了。这在 DirectX 中叫做输入汇编器,几个数据流被编织在一起,形成了顶点集合和图元集合,并向下发送给管 线。
顶点着色器是处理这些三角形的第一个阶段。正如顶点着色器的字面意思,它只会对传入的顶点进行处理。
顶点着色器提供了一种用于修改、创建或者忽略三角形顶点数据的方法,这些数据可以是颜色、法线、纹理坐标和位置等。
通常顶点着色器程序会将顶点从模型空间变换到齐次裁剪空间中,在最极端的情况下,顶点着色器也必须要输出顶点的位置
一些顶点着色器能够实现的效果:
- 动画关节的顶点混合以及轮廓渲染(描边)
- 物体生成:仅创建一次模型,并通过顶点着色器对其进行变形。
- 使用蒙皮技术和变形技术来设置角色的身体动画和面部动画。
- 程序化变形:例如旗帜、布料和水面的运动。
- 粒子创建:通过向流水线发送简并(无面积)网格,并根据需要来设定它们的位置,从而来模拟粒子效果
- 透镜畸变、热雾、水波纹、书页卷曲以及其他特效,可以通过将整个帧缓冲的内容作为一个纹理,然后将其应用在一个正在经历变形,并且屏幕对齐的网格上进行实现
- 通过使用顶点纹理来获取并应用地形的高度场
曲面细分阶段
曲面细分阶段允许我们绘制曲面,GPU 的任务就是将每个曲面描述都转换成一组三角形。
曲面细分阶段是一个可选的 GPU 特性,它首次出现在 DirectX 11 中。
使用曲面细分阶段有几个好处:
- 节省内存
- 当场景中存在一些不断变化的角色或者物体时,这个功能还可以防止 CPU 与 GPU 之间的总线带宽成为程序的性能瓶颈。
- 对于一个给定的相机视角,曲面细分可以生成适当的三角形数量,这样的曲面可以被高效渲染。
- 例如:现在有一个距离相机很远的小球,它仅仅需要使用很少的三角形即可;当这个小球距离相机很近的时候,它也可以用几千个三角形来进行表示。从而获得更好的效果。
曲面细分阶段同样包含三个字阶段。在 DirectX 中,它们分别叫做壳着色器(hull shader)、曲面细分器(tessellator)和域着色器(domain shader)。
曲面细分器是流水线中一个固定功能的阶段,并且只用于曲面细分着色器。它的任务是添加新的顶点,并发送给域着色器进行处理。
几何着色器
几何着色器可以将一种图元转换为另一种图元,这是曲面细分着色器所无法实现的。
- 例如:我们可以为每个三角形都创建边界线段,从而将一个三角形网格转换成一个线框模型。
几何着色器的输入是一个独立物体和与其相关联的顶点。这些物体通常由一个条状三角形、一个线段或者仅仅是一个点组成,其他扩展的图元也可以在几何着色器中进行定义和处理。
几何着色器的设计目的是对输入的顶点数据进行修改,或者是创建有限数量的副本。
- 例如:其中一个用途是生成六个变换后的数据副本,从而同时渲染一个立方体的六个面。它也可以用于高效的创建级联阴影贴图(cascaded shadow map,CSM),从而生成高质量的阴影。
几何着色器会保证按照图元的输入顺序来输出图元。这个排序会对执行性能产生影响,因为如果有很多个着色器核心并行执行的话,那么为了保证图元的输出顺序与输入顺序相同,则必须要将所有执行后的结果保存下来并进行排序。这个因素和其他的一些因素一起,不利于几何着色器在一次调用中大量复制或者创建图形
在实践中,几何着色器很少会被使用,因为他和 GPU 并行计算的优势并不相符;在一些移动设备上,几何着色器是使用软件进行实现的,因此在这些设备上也不鼓励使用几何着色器
流式输出
使用 GPU 管线的标准方式是通过顶点着色器向 GPU 发送数据,然后将生成的结果三角形进行光栅化,最后在像素着色器中对这些数据进行处理。
流式输出 (stream output)的想法是在 Shader Model 4.0 中引入的,在顶点被顶点着色器处理完成之后(这里还可以选择曲面细分和几何着色器),这些数据除了被发送到光栅化阶段之外,还可以通过一个流(即一个有序数组)来进行输出。
这些处 过的数据可以通过流式输出从管线中返回,从而允许对其进行迭代处理。这类操作在模拟流动的水体,或者其他粒子特效的时候十分有用。它还可以用于对模型进行蒙皮操作,然后让这些顶点数据可以重复使用。
流式输出只能以浮点数的形式返回数据,因此它可能会占用很多存储空间。流式输出是作用于图元的,而不是直接作用于顶点的。
像素着色器
在顶点着色器、曲面细分和几何着色器完成操作之后,输出的片元将会进行裁剪和设置,从而进行下一步的光栅化。
光栅化是管线中相对固定的处理步骤,它不具备任何可编程性,仅仅是某些地方可以进行自定义配置。
三角形中部分与像素重叠、或者完全与像素重叠的部分被叫做一个片元(fragment)
三角形顶点上的数值,包括 z-buffer 中的值在内,每个被三角形所覆盖的像素都会使用这些数据进行插值。这些插值生成的数据会被发送到像素着色器
在编程中,顶点着色器程序的输出,在经过三角形(或者线段)插值之后,会成为像素着色器程序的输入。
合并阶段
在合并阶段中,我们会将每个独立片元的颜色和深度进行组合,并最终形成帧缓冲。
DirectX 将这个阶段叫做输出合并(output merger);
OpenGL 将其称为逐样本操作(per-sample operation)。
计算着色器
GPU 不仅可以用来实现传统的图形渲染管线,还可以用于很多非图形的领域
- 例如 用于计算股票期权的估计价值,以及用于训练深度学习的神经网络等,这种使用硬件的方式叫做 GPU 计算(GPU computing)。
DirectX 11 引入了计算着色器(compute shader),它是利用 GPU 进行计算的一种方式。计算着色器是一种特殊的着色器,但是它并没有锁定在图形管线中的固定位置。它与渲染的过程密切相关,因为它是通过图形 API 来进行调用的。计算着色器与顶点着色器、像素着色器以及其他着色器可以一起进行使用,它利用了管线中相同的统一着色器处理器池。