通过挖掘顶点和像素片断之间的独立性,硬件管道实现了很高的处理吞吐量。大多数顶点和像素着色器都是以并行的状态来处理相互独立的顶点和像素片断。典型的硬件实现中pixel shader的数量要比vertex shader多很多,反映出典型的渲染过程中,像素处理的工作量要比顶点多很多。与vertex shader相比,这种特性将影响pixel shader的性能,因为pixel shader被过多的复制了。>>
可编程管道直接使用了较低的抽象层,比如OpenGL或Direct3D。这些抽象层隐藏了不同硬件管道实现之间的差别,提供了一个方便的接口。对特定的平台来说,比如游戏机,它的硬件管道与PC平台相比是不同的,底层细节也是由这些抽象层来暴露。>>
我们把这些抽象层称为运行时(runtime),并且通过它所提供的API对他进行控制。运行时为设备提供了独立的资源管理(分配内存,控制生存期,初始化,虚拟技术,等等),所有纹理贴图,顶点缓冲,状态改变以及和硬件加速器的通信都通过特定设备的驱动程序来完成。对可变成管道来说,运行时还加入了对照色程序的抽象和管理任务。>>
由于早期可编程处理器对指令储存空间的限制,为了在有限资源内,最大化对硬件的控制,不得不使用类似于汇编的语言来编写照色程序。但是,随着硬件功能的增加,需要一种高级的编程抽象来提高程序员的生产力。一种与C类似,并且添加了对潜在渲染管道进行定制的编程语言满足了这种需求。此外,还发展出了一些其他的语言,利用浮点处理器和GPU内存带宽来完成渲染以外的一些计算,但是,我们不打算在这篇文章里对这些应用进行讨论。>>
虽然新的编程语言有必要与CPU编程语言(特别是C)类似,但我们还是进行了一些重要的修饰。举例来说,硬件结构和编译模型更像是一台虚拟机,汇编着色语言扮演了独立于硬件的中间语言(IL),而不是特定的机器语言。在离线环境下,Microsoft HLSL之类的高级语言被编译为IL,在程序运行时,通过驱动程序内建的翻译器,实时转换为目标硬件的指令。需要注意的是,OpenGL shading language使用了一种不同的方法来完成运行时的编译过程。>>
另一个重要的区别是,着色程序并不是孤立的程序,通过一个运行在CPU上的程序来协调渲染管道,它们通常是共同(in concert)执行的。 此外CPU程序还以纹理或填充寄存器常量的方式,为着色程序提供参数。
虽然本文没有具体描述特定硬件新图形管道的构架,但图形管道的设计很大程度上都是根据硬件实用性以及多硬件并行处理来设计的。此外,当前硬件实现的结构体系也将延续或者影响我们的设计。
3.图形管道
Direct3D保留了通用硬件加速器的3D管道结构。我们添加了两个新的阶段,并对其他的阶段进行了简化或者进一步归纳(generalized)。基本的图形管道如图1所示。为了保证文章的连续性,我们将讨论图形管道的每一个阶段,而不是只讨论新增的两个阶段。为了和以前的术语一致,使用了通用的术语,比如 顶点(vertex),纹理(texture)以及像素(pixel),但需要注意,这些术语只表示普通用法中的某些特定含义。
Input Assembler(IA)从附加到顶点缓冲上的8个输入流中接收1D的顶点数据,并且把数据项转换为规范的格式(比如,float32)。可以为每个流指定独立的顶点结构,每种结构最多包含16个域(也称为元素,element)。每种元素可以由1到4个基本数据项组成(比如,float32s)。通过读取当前的有效流来装配(assembled)顶点。一般来说,将会连续的从顶点缓冲中读取顶点数据,但是,如果指定了索引缓冲,那么每个流将使用共享的索引来计算每个顶点缓冲中顶点数据的偏移值。指定索引可以带来额外的性能优化,顶点处理器将根据索引值计算结果,通过用索引值作索引的结果缓存,可以避免使用相同索引重新计结果(译注:这里的结果因该是指顶点数据序列)。
此外,IA还提供了一种机制,允许IA高效的复制对象n次。这种机制是实现instancing的解决方案,把包含k个顶点的数据块(block)复制n次。同时,将使用当前的实体(instance),图元(primitive),以及顶点id对图元数据进行“标记(tagged)”,在可编程阶段,可以访问这些id,以便计算变换,材质等参数。
Vertex Shader(VS) 通常用来把顶点从模型空间变换到裁剪空间。VS读取一个顶点,输出一个顶点。VS与其它可编程阶段一样,有一些共同的特性,包括支持扩展的浮点、整数、控制类型,可以访问128块内存缓冲(纹理)以及16个参数(常量)冲。我们会在第四节详细讨论这些通用核心(common core)。
Geometry Shader(GS) 把同一图元的所有顶点作为输入,产生新的顶点或者图元。输入和输出图元的类型不一定要匹配,但对着色程序程序来说是固定的。以发射额外图元对象的方式,GS程序可以增加输入图元的数量,每一次调用(per-invocation)最多产生1024个32-bit的顶点数据。三角形和线段被输出为连续的顶点带。在一次调用中,GS程序可以输出1个以上的图形带,或者删除输入的图元,不进行任何输出。GS程序同样可以在不产生新几何体的情况下,把额外的属性附加到图元上,比如为每个图元计算额外的属性。由于可以访问当前图元的所有顶点,因此,计算三角形平面方程(triangle’s plane equation)之类的几何属性将会容易。
除了输入图元之外,还可以处理三角形和线段的邻接顶点。每个三角形包含三个顶点,以及三个邻接顶点,而每条线段则包含2个顶点以及2个邻接顶点。对于三角形和线段来说,邻接顶点将作为顶点缓冲格式的一部份,当指定(渲染)了带邻接的图元拓扑之后,IA会对提取邻接顶点。
Stream Output(SO)将把GS输出的顶点信息复制为4个连续的输出缓冲子集。理想情况下,SO的输出能力(不带索引)应该和IA(8 streams * 16 elements)的输入能力相匹配,但硬件代价是不合理的。SO只能输出一个1~16元素的流,或者4个单一元素的流。此外, IA可以读取8或者16bit的数据类型,并把他们转换为float32,而SO只能写入原始的32-bit的数据类型。但是,数据类型转换和打包可以在GS程序中轻易实现,减少了固定功能的支持。
Set-up and Rasterization Stage(RS) 是一个功能固定的阶段,用来处理剪切(clipping),剔除(culling),透视划分,观察点变化,图元设置,裁剪(scissoring),深度偏移,以及片断(fragment)生成。现代GPU设计总是包含某种形势的早期深度处理(z-cull,hierarchical-z)。我们将明确讨论这种优化,因为对应用程序开发者来说,它变的不太透明了。RS的输入是单一图元的顶点以及属性,输出一系列像素片断。
Pixel shader程序指定了通过顶点属性插值产生片断属性的方式(no interpolation, non-perspective-corrected interpolation, or perspective-corrected interpolation)。现代GPU通常支持多采样抗锯齿。当一个片断不包含像素的中心时,需要很小心的指定属性赋值行为,因为中心点的值有可能超出范围。通过片断边界,可以使用赋值限定器(质心centroid)来指定所需的值。
Pixel Shader(PS)读取单一像素片断的属性,输出包含1~8个属性(颜色)以及任意深度值的单一片断。每个属性值(元素)要么被分别写入单独的颜色缓冲中(称为渲染目标),要么完全丢弃(不输出片断)。一般情况下,深度和stencil值来自于RS。PS可以可以替些深度值,但无法改变stencil值。无论是丢弃像素还是重写深度值,都有可能影响RS中的深度处理优化(depth-processing optimizations),因为这样做会改变片断的可见性。
Output Merger(OM)接收来自于PS的片断,执行普通的 stencil和深度测试操作,以及渲染目标混合。OM为一个统一的depth / stencil缓冲以及8个其他的渲染目标(属性缓冲)指定了约束点(bind point)。Pixel shader必须分别为每个渲染目标输出单独的值(不支持多点传送)。此外,所有的渲染目标都共享一种混合功能,但是,可以对每个渲染目标独立的激活或者屏蔽混合。 |