第三章 图形处理单元
显示器就是计算机。
--NVIDIA CEO 黄仁勋。
从历史上看,硬件图形加速起始于管道的最后,首先执行三角形扫描线的光栅化。随后,硬件的连续生成物再回到管道处理,此处,一些更高层应用阶段算法被交付给硬件加速器。专用硬件和软件相比,唯一的优势是速度,而速度是关键的。
在过去的十年里,图形硬件已经经历了一个难以置信的转变。第一个包含硬件顶点处理的客户图形芯片(NVIDIA's GeForce256)发送于1999年。NVIDIA创造了术语图形处理单元(GPU)以区分GeForce 256和前面只有光栅化功能的芯片。在接下来的年头里,GPU由可配置复杂固定功能管道实现演变为高度可编程的“白板”(开发者可在该白板上实现自己的算法)。各种可编程着色器是控制GPU的主要方法。顶点着色器执行针对每个顶点的操作(包括转换和变形)。类似地,像素着色器处理各个像素,允许针对每个像素计算复杂的着色方程。几何着色器允许GPU直接创建和销毁几何基元(点、线和三角形)。计算值可被写入到多个高精度缓冲,并用作顶点或纹理数据。为了效率,管道的一些部分仍然保留为可配置但不可编程,不过趋势还是可编程性和灵活性[123]。
图 3.1 渲染管道的GPU实现。根据用户对其操作的控制程度,已将各阶段打上颜色。绿色阶段是完全可编程的。黄色阶段可配置但不可编程,如,剪切阶段可以选择执行剔除或添加用户剪切面。蓝色阶段是完全地固定功能。
3.1 GPU管道概述
GPU实现了第二章叙述的几何和光栅化概念管道阶段。它们被分为几个硬件阶段,这些阶段具有不同程度的可配置性或可编程性。图3.1显示了各个阶段的可配置和可编程性。注意,这些物理阶段的分割和第二章提供的功能阶段稍微有点不同。
顶点着色器是完全可编程阶段,它通常用来实现模型视图转换、顶点着色和投影这几个功能阶段。几何着色器是一个可选的完全可编程阶段,它操作基元的顶点,可以用来执行逐基元的着色操作、销毁基元或创建新基元。剪切、屏幕映射、建立三角形和遍历三角形是固定功能阶段,实现了和它们同名的功能阶段。和顶点着色器与几何着色器类似,像素着色器也是完全可编程的,它实现像素着色功能阶段。最后,合并阶段介于完全可编程和古董功能之间。虽然它不可编程,但它是高度可配置的,能够设置为执行一大堆各种各样的操作。当然,它实现了合并功能阶段,负责修改颜色、z缓冲、混合、模板和其他相关的缓冲。(这段中关于剪切的说明和图对应不上)
GPU管道已随着时间逐渐演变,从原来的硬编码操作向着灵活性和控制性发展。在这个演变中,可编程着色器阶段是最重要的一步。
3.2 可编程着色器阶段
现代的着色器阶段(如那些支持着色器模型4.0的,Vista上的DirectX 10及以上)使用一个通用着色器内核。这意味着顶点、像素和几何着色器共享一个编程模型。我们在本书中区分通用着色器内核和统一着色器;前者针对应用程序员,是API,后者针对GPU架构,是GPU的一个特性,它可以很好地映射到前者(参考节18.4)。早期的GPU在顶点和像素着色器之间缺少共通性,并且没有几何着色器。尽管如此,该模型的大多数设计元素还是被旧的硬件所使用;只不过,做出了一些简化或舍弃,其他并没有太大的不同。现在,我们将关注于着色器模型4.0,并在后面的章节讨论旧GPU的着色器模型。
叙述整个编程模型超出了本书的范围,可以参考已有的一些文档、书籍和网站[261, 338, 647, 1084]。不过,一些说明还是需要的。着色器使用和C类似的着色器语言(如HLSL,Cg和GLSL)进行编程。它们被编译为独立于机器的汇编语言(也叫做中间语言,IL)。前面的着色器模型允许直接在汇编语言中编程,但是对于DirectX10,该语言中的程序仅在调试输出中可见[123]。在一个单独的步骤中,汇编语言被转换为真正的机器语言(通常是在驱动中)。这种安排可以实现不同硬件之间的兼容性。可以把汇编语言当成一个虚拟机,该虚拟机是着色器语言编译器的目标平台。
虚拟机处理各种类型的寄存器和数据源,使用一套指令集编程。许多图形操作基于短向量(最长为4),因此,处理器拥有4路SIMD(单指令多数据)性能。每个寄存器包含四个独立的值。32位单精度浮点数的标量和向量是基本的数据类型;对32位整数的支持在最近也被添加。浮点向量包含的数据通常有位置(xyzw)、法线、矩阵行、颜色(rgba)、纹理坐标(uvwq)。整数大多数用于计数器、索引和位掩码。同时,还支持聚合数据类型(如结构、数组和矩阵)。向量可以和标量、向量和矩阵等混合使用,并且可以复制任何部分。也就是说,向量元素可以被重新排序或按需复制。类似地,还支持使用掩码访问向量的各个元素。
一个绘画调用将使图形API绘画一组基元,执行图形管道。每个可编程着色器阶段有两种类型的输入:uniform和varying;前者在一次绘画调用中保持不变(但可在多个绘画调用间改变),后者随着着色器处理的顶点或像素而变化。纹理是一种特殊的uniform输入,它以前总是一个应用到表面的颜色图像,不过现在可以将其当作是大量数据的数组。需要注意的是,虽然着色器有一大堆各种各样的输入,并且可以用不同的方式定位,但其输出有极大的限制。这是着色器和常用目的处理器程序间的最显著的不同。底层虚拟机为不同类型的输入和输出提供特殊的寄存器。uniform输入使用只读的常量寄存器或常量缓冲进行访问。有效的常量寄存器的数目比有效的varying输入和输出寄存器要多很多。这是因为,我们仅需针对单个的顶点或像素保存其varying输入和输出;而uniform输入仅保存一次,并且要在整个绘画调用过程钟跨顶点或像素进行重用。虚拟机也有常用目的的临时寄存器。所有类型的寄存器都可以使用临时寄存器中的整数值进行数组索引。着色器虚拟机的输入和输出如图3.2所示。
图 3.2 DirectX10中的通用着色器内核虚拟机架构和寄存器结构。最大有效数目已在各个资源边上标明。左斜杠分割的三个数字从左到右分别表示顶点、几何和像素着色器的各个寄存器。
常用的图形计算可在现代的GPU上高效地执行。通常,执行最快的是标量和向量的乘和加,及其组合(如乘加和点积)。其他操作,如倒数、方根、正弦、余弦、指数和对数,就开销稍大,但依然快速。纹理操作(参考第六章)虽然高效,但它们的性能可能受到多种因数(如获取访问结果的等待时间)的限制。着色语言公开了这些常用操作中的绝大部分(如加和乘),它们使用操作符*和+,固有函数atan()、dot()、log()等。固有函数也用于更加复杂的操作,如向量标准化、反射、叉积、矩阵转置和行列式等。
术语流控制指使用指令分支以改变执行代码的流向。这些指令用来实现高级语言的构建,如if和case,以及各种循环。着色器支持两种类型的流控制。静态流控制基于uniform输入的值。这表示,在一次绘画调用过程中,代码的流向完全保持不变。静态流控制的最大好处是允许同一个着色器用于各种不同的情形(如不同的光照数目)。动态流控制基于varying输入的值。它的功能比静态流控制强大,但是开销也大(尤其是着色器的各个调用之间代码流变动无常的情况下)。如节18.4.2所讨论的那样,着色器在一个时间对许多顶点或像素执行计算。如果流控制为某些元素选择了“if”分支,而又为另外一些选择了“else”分支,那么,就会对所有的元素执行这两个分支(各元素未使用的分支则被舍弃)。
着色器程序可以在程序加载之前离线编译,或者在运行期间编译。和其他编译器类似,它也有各种选项,可以用来生成不同的输出文件和控制不同的优化级别。已编译的着色器存储为文本字符串,并通过驱动传递给GPU。