The Direct3D 10 System
注:SIGGRAPH 2006即将在波士顿开幕,微软也将发布DirectX10的相关资料。为此特地翻译了源自微软DirectX开发社区的一篇PDF文档,“The Direct3D 10 System”,原文地址为http://download.microsoft.com/download/f/2/d/f2d5ee2c-b7ba-4cd0-9686-b6508b5479a1/Direct3D10_web.pdf。原文的脚注有“ACM, (2006). This is the author’s version of the work. It is posted here by permission of ACM for your personal use. Not for redistribution.”原文版权归其作者以及相关组织所有,遵守ACM关于仅限个人使用,不得发布的条令。中文版由[email protected]翻译,仅限个人学习之用。有删节。
摘要:我们将向您展示第四代GPGPU的系统架构。整套架构新的特征将包括诸如,直接向内存存取流式的Primitive数据,统一顶点数据以及图像数据资源的管理,以及新的存储格式。我们也将向您说明一些在新的管线(Pipeline)设计中,所作的对API、Runtime、Shading Language的一些改动。
一、正文:
OpenGL 以及Direct3D的架构已经发展了好几代。在过去五年中,技术的逐渐进步推动着处理模式由传统的固定管线(Fixed Function)向可编程管线过渡(Progammable Pipeline)。在进步过程中,实际上体现了设计者对通用性、性能、成本的综合考虑。到目前为止我们已经编写了许多图形加速的应用程序,比如演示系统,CAD,多媒体处理系统等等,以及游戏。这些程序无一例外的都需要管理数以G字节计算的数据,比如几何体顶点数据、纹理数据、动画数据,以及处理图形的程序,占用大量的系统资源,以人们可以接受的速率渲染逼真的图形。在许多的系统设计中,我们都可以看到人们如何有效的解决问题。如同旧版本的Direct3D一样,Direct3D 10实际上是三方面工作的合作成果,硬件设计者,API/Runtime架构者,以及应用程序开发人员。参考了应用程序开发者们反映的意见,我们把一些常见的问题进行了整理:
-
状态机切换代价太高(High State-change overhead)。改变任何的状态,比如顶点格式、纹理格式、Shader参数、混合模式等,代价太高。为了尽量减少状态的改变操作,需要我们对处理顺序进行排序,使用基于Shader的技术,优化整个程序的性能。比如,需要多重纹理贴图时,可以将几个纹理合并到一个纹理上,然后通过对定点坐标的处理,进行多次的贴图操作。
-
厂商之间定义的硬件加速功能过于混杂。有些时候应用程序使用了一些专有厂商自己定义的特殊功能,导致无法在大部分的硬件上有效的运行。
-
CPU 与GPU的同步问题。传统结构在管线中提取出新的数据是有次数限制的。比如渲染场景到纹理(Render To Texture)技术,把场景拷贝出来作为纹理贴图使用,CPU使用率并不高。尽管这样,产生新的顶点数据或者创建立方体纹理依旧需要更多的坐标数据,以及GPU与CPU之间的同步,降低了效率。
-
指令的数目以及数据类型的限制。比如Vertex Shader需要处理数据的精度,以及实现传统的流程控制(Flow Control)结构。Pixel Shader也一样。但是无论是Vertex Shader还是Pixel Shader,处理整数的能力要远远强于处理浮点数据能力。应用程序要么舍弃了这些代价太高的处理方法,要么以比如基于查招表的方式运行。
-
资源限制。太多了,比如纹理单元(Texture Unit)数目的限制,Shader指令数目的限制等等。所以现在很多程序都不得不尽量将算法简化,或者使用多Shader的方法进行处理。
二、背景
整个可编程GPU系统的发展可以分为四个阶段,如下表:
-
Feature
1.1 2001
2.0 2002
3.0 2004
4.0 2006
Instruction slot
128
≥512
≥64K
4+8
32+64
≥512
Constant registers
≥96
≥256
≥256
16x4K
8
32
224
Tmp registers
12
12
32
4K
2
12
32
Input registers
16
16
16
16
4+2
8+2
10
32
Render targets
1
4
4
8
Samplers
8
16
16
16
textures
4
128
8
16
16
2D texture size
2Kx2K
8Kx8K
Integer ops
√
Load op
√
Sample offsets
√
Transcendental ops
√
√
√
√
√
√
Derivative op
√
√
Flow control
Static
Static/dynamic
dynamic
硬件实现实现了数据从Vertex Shader到Pixel Shader的高速处理。多Vertex Shaders和Pixel Shaders可以用来并行的处理独立的顶点以及像素。一般来说GPU的像素处理单元要远多于顶点处理单元,因为在实际情况中需要处理的像素总是要比顶点多得多。
在OpenGL与Direct3D中,可编程处理管线使用低等级的抽象层与硬件交互。这个抽象层对于程序来说是完全透明的,隐藏了各种底层的实现。而其它平台,比如工作站,和PC平台就完全不同,直接由硬件实现一切,硬件所有的特性都是向外暴露的。
早期由于硬件的限制,为了精简指令数,不得不采用汇编语言进行编码。如今随着硬件的发展,处理程序的编写完全可以使用高等级语言实现,比如C-like的语言进行编写——而且,熟悉C语言的开发人员非常多。可是,当GPU编程语言等级提高了之后,又产生了一些问题,比如,硬件的编译模型更像虚拟机,Shader汇编语言比起机器语言跨平台的特性更好。HLSL语言可以被预编译,等到运行时再被GPU驱动翻译成机器语言执行。GLSL语言则不同,它是在运行的时候从源代码直接翻译成机器语言执行。
三、管线
Direct3D 10 依旧保留了传统的硬件加速管线,不过增加了2个新的流程。如图所示:
Input Assembler (IA)将从输入流(Input Stream)中将所有的1D顶点数据转换为浮点格式(float32)。每一个流对象指定了一个顶点结构,最多包含16个顶点缓冲结构。一般来说GPU按照顶点的输入顺序处理,必要时应该使用索引缓冲(Index Buffer)提高性能。IA也支持对物体进行快速复制,类似于实例绘制(Instancing)技术,不过更有效率。
Vertex Shader (VS)一般用来做空间变换。VS读取一个顶点,输出一个顶点。VS可以和其他的Shader分享数据,允许访问多达128个纹理,以及16个内部缓冲。更多的内容请看第4节。
Geometry Shader (GS)读取Primitive中的顶点,输出定点或者Primitive。可输入输出的Primitive可是是不同类型的,但它们对于Shader来说都是一样的。GS程序可以在输入的Primitive中插入新的顶点,产生新的Primitive,进而产生新的几何结构,也可以剔除输入的Primitive。
Stream Output (SO)的作用是将GS输出的数据拷贝到输出缓冲。可是由于硬件的限制,SO只可以实现1个最多包含16个Elements的Multi-Element的输出流,或者是4个single-element输出流。IA支持8bit以及16bit数据的读取,转换成float32格式,SO只能写入32bit格式的数据。在GS可以方便的实现数据格式的转换以及Packing。
Set-up and Rasterization Stage (RS)执行诸如Clipping、Perspective divide、viewport transform、primitive set-up、Scissoring、depth offset、fragment generation(不好意思没有翻译)。GPU的设计中一般都有Early Depth处理。RS负责处理顶点,Primitive的Attributes,输出光栅化处理后的像素。
Pixel Shader (PS)读取Pixel Fragment的attribute,输出最多包含8个attribute的Fragment。Attribute值被写入不同的色彩缓冲,如果不符合输出条件则丢弃(discard)这个像素。一般Depth以及Stencil值在RS中已经被计算好。虽然说在PS中依旧可以进行Depth的计算以及重新写入,不过为了优化请尽量在RS中进行这些运算。Stencil值不可以被改写。
Out Merger (OM)读取PS处理过的像素,进行传统的Stencil测试和Depth测试,以及Alpha混合。OM绑定了一个Depth/Stencil缓冲,以及最多8个Attribute缓冲。PS必须向每个渲染目标都输出值。当在所有渲染目标之间使用同一个混合函数的时候,您可以分别设置各个渲染目标是否启用或者禁用混合。
内存结构与数据Flow
GPU 需要处理大量储存在诸如顶点缓冲、索引缓冲、纹理单元里的数据。GPU一般将数据储存在自己的高速显存中。这些数据包括1/2/3D的纹理贴图,1D的索引以及顶点数据等等。在Direct3D 10中把所有的这些都统统称为资源(resources),目的是为了提高处理效率。因为提高效率无非有两个方法,在单通道里尽量做最多的事情,或者单通道里创建了Resources后在多通道里复用。
几种Resources可以提高单通道渲染的效率。Arrayed Resources。纹理贴图,以及渲染目标可以被当作线性数组(Linear Arrays)绑定到一个渲染通道中。Shader里用来取得纹理索引的指令被扩展为Computed Array Index。现在有不少的开发人员为了效率将一些纹理放到一张大的纹理上,渲染时通过纹理坐标分割纹理。这种改变可能会给他们带来一些压力。当然,Texture Arrays不需要考虑这种特殊情况,因为它是为了处理大量尺寸相同的纹理而设计的。
当一个渲染目标数组(Array of Render Targets)绑定到OM时,GS处理时可以对Primitive进行排序或者复制。比如,当在单通道中渲染场景生成立方体贴图时,可以把它当作一个包括6个2D渲染目标的数组。(Array of 6 2D Textures)。当对物体进行立方体贴图时,GS将决定使用哪一个面的像素进行贴图。需要了解的是,GS处理渲染目标数组与PS的多渲染目标输出之间是独立的进行的。
为了使用渲染场景到纹理(Render to cube map)功能以及为了更加方便的使用数组,添加了一个新的概念——View。编码时,不需要制定资源,不需要额外的辅助参数,Direct3D 10允许创建资源时,不需要指定类型,比如究竟是float16还是snorm16,所以可能会发生非常有限的类型隐式转换,只有相同大小的类型可以转换,其余的不可以,比如2个float16数字绝对不可以当作1个float32数字。资源也无法直接用于管线处理,它们必须通过View绑定。
流输出对象特别适合处理连续的1D数据比如顶点坐标等,优点包括,可以支持输出非常多的数据类型。流对象支持不支持对输出数据进行随机访问。如果有需要在VS里可以做足够的处理。
Shader 里操作的数据是32bit的(无论是浮点数还是整数)。此时就需要提供更加丰富的数据类型,减少内存占用以及带宽。数据会被自动对齐为整数的大小(32bit)。几乎所有的数据类型都可以作为顶点数据、纹理数据,作为流输出,以及渲染目标。如下表:
-
Name
Widths
Range
UnormN,snormN
8,16
[0,1] and [-1,1]
FloatN
32,16,11,10
S32e8,s10e5,6e5,5e5
UintN,sintN
8,16,32
[0,2^n-1] and [-2^n-1,-2^n-1]
RGBE
32
999e5
SRGB
8
[0,1] no-linear
unorm 、snorm以及半浮点数是用的最多的格式。比如在需要HDR的程序中,半浮点数可以提供最好的精度以及最合理的带宽占用,而且硬件也支持丰富的过滤操作。DirectX10里面的32bit数字有两种格式,第一种是,RG通道各占11bit,B占10bit,剩下1bit存储共享的指数;第二种,RGB各占9bit,5bit储存指数。这两种格式都能够达到float16格式的精度。其中,低精度的11-11-10格式适合用来储存HDR色彩数据。当然Direct3D 10依旧支持低动态范围的压缩纹理格式。
设计考虑
对比以前的架构,Direct3D 10要求所有的Features都由硬件来实现。目前问题有两个,硬件需要支持32bit纹理过滤以及渲染目标的多重采样以及反锯齿。所有的可编程结构可以模拟的固定管线处理功能,都将从核心以及API中去处,包括光照,矩阵变换,Alpha混合。固定处理管线可以很容易的用软件模拟。不过一些较重要的传统处理管线依旧保留。我们设计曾经考虑过把IA设计成完全可编程的,但是无法证明一切会变得更加的简单还是复杂,而在VS里可以计算。事实上,VS可以不需要进行任何的内存读取操作,直接从IA中读取数据计算。话虽如此,为了性能我们依旧保留了一些IA的复杂特征,这样可以完全发挥可编程的优势,在处理顶点时硬件可以达到最高的效率。
Gs 的设计更为复杂。GS可以进行并行处理,保存数据(类似于Primitive)的输入顺序,这样有多个GS单元处理数据时不会发生顺序的错误。这种并行的设计要求必须有一个输出缓冲,而且数据的顺序必须同输入的数据相同。让GS尽可能的多的代替RS的工作,比如Viewport变换等。GS可以做一部分Clipping,比如计算顶点到Clip-Plane的距离,把Clip-Space空间中的坐标传递到RS。RS采用的依旧是传统处理管线,对比GS来说精度不高,所以不可能让GS模拟RS生成Image-Space坐标。Om也是经常被讨论的部分。OM是整个管线中唯一可以进行内存读写的部分,也经常被要求设计成可编程单元。不过这样一来管线的复杂度就大大的提高,而且内存系统的效率也会打折扣。曾经提过是否可以把OM结合到PS中,可是如果这样,多重采样以及混合操作就会变得非常的复杂(因为PS不可以获得Fragment内存地址),而且Early Depth/Stencil已经可以显著的提高性能。
Shader Model 4
在以前版本的Direct3D中,可编程管线单元(VS PS)是用一个特殊的虚拟机实现的。每个虚拟机使用冯诺伊曼架构,类似于汇编语言的指令集,拥有输入输出寄存器,堆栈,以及可以获取内存资源的Binding Points。Direct3D 10为每个可编程管线单元定义了一个“Common Core”,新增加了以下内容:
-
32bit 整数指令,算术运算,Bitwise计算,Conversion计算。
-
可寻址的(Indexable)通用寄存器,大小为4096x4
-
分开执行Unfiltered指令与filtered内存读取指令(读取以及采样指令)
-
支持Shadow Map采样
-
16 个Constant(parameter)缓冲 4096x4
-
减少Texture Bind Points(128),和Sampler State(16)
这个统一模型(Unified Model)代替CPU执行所有的算术运算、逻辑运算、以及Flow Control。资源(Resource)比如寄存器、Texture Bind Points,以及指令存储器等等这些不应该是软件开发人员头痛的问题,接下来的几年,硬件厂商会自己对产品进行性能与成本的折中。Texture Bind Point数目的提高显然不会对Texture Filtering Combinations有太多的要求。加大Constant存储空间与更新Constant的效率之间的冲突不可避免。目前的系统一个明显的问题就是用一般的更新操作更新Constant值效率不高,而如果程序使用了多个Shader那么效率会更加低。在实际情况中Group Of Constant更新的频率并不相同,有时是每帧,或者每个物体,或者每个实例。由此我们把Constant分开存储到不同的缓冲中,分开进行操作。而Constants访问的频率远远高于纹理,Constants一般使用uniform索引,而纹理一般使用纹理坐标。这也要求硬件设计者把Constant与纹理进行分离。同时我们也提高了浮点数的精确度,对比IEEE754的0.5ulp,我们达到了1.0ulp。在进行平方开放运行时,精度可以达到2.0ulp。
核心API与Runtime
核心API与Runtime作为一个瘦抽象层(Thin Abstract Layer)工作于硬件上,对上层的操作进行解释。API与Runtime负责处理内存资源,建立View、Shaders,绑定到管线上,以及初始化渲染,提供硬件查询等等。我们希望提高命令执行的效率,把命令分为两类。一类是处理资源的命令,一类是操作管线的命令。Runtime先初始化一个缓冲,API命令对缓冲执行各种操作,等到缓冲已满或者需要同步的时候把缓冲上传到GPU中,比如当回读渲染目标的时候。Runtime模型在PC系统上发展了十几年,我们的目标是提高命令的效率,当命令执行时不需要额外的处理。过去这是不可能实现的需求,不过我们改进了一些设计,让这个目标离我们更加近。其中有一些关键原因:
-
API 与硬件之间不兼容
-
处理延迟
-
应用程序的真正需要
第一个问题已经被轻松的解决,这样在应用程序开发者,Runtime,驱动程序,以及硬件提供者之间,不敢说从此100%的兼容,最起码能够满足不是那么苛刻的要求(垄断加大棒)。第二个问题经常碰到,尤其是需要做许多处理的时候。不过这样的好处是,各个处理过程之间互不干扰是正交的。可是,这给CPU带来了许多负担,比如当纹理绑定发生更改时需要重新编译Shader以符合新的纹理格式。我们希望尽可能多的消除硬件中内在关联的状态操作,同时在运行期把这些操作归入Optional Layer进行单独的操作。状态机机制也发生了改变。在OpenGL与以前的Direct3D软件的颗粒度非常令人满意,比如改变混合方式的操作,样本过滤操作等。为了提高效率可以使用OpenGL的Display List或者是Direct3D的State Block。我们当然希望更加的优化,提高效率。于是减少了不少固定处理管线。我们的研究发现现有的状态机结构模型无法再进行效率的提高,所以我们把一些相关联Functions组成一个固定集合,称为State Object。Direct3D 10定义了5个State Object:InputLayer(Vertex Buffer Layout),Sampler,Rasterizer,DepthStencil,以及Blend。当构造这些对象的事后,驱动程序先在寄存器中初始化硬件操作需要的数值。当对象绑定到管线的时候,所有的数值被拷贝到命令缓冲里(Command Buffer),在硬件本地提供命令以及操作。还有一些难题还无法避免,比如上面说过的Constant问题,还有比如切换资源的读写状态。这需要应用程序自己进行优化。
一些API的设计为了执行期的效率没有考虑容错,或者只为频繁使用的API提供容错。对比运行期的错误处理机制,我们更青睐在程序开发时就进行错误处理。首先我们把出现的错误分为两类,致命的(Critical)和非致命(Non Critical)。运行期时一定会报告致命的错误,而非致命的错误会被一个特殊的拦截层拦截。开发调试时主要使用Validation Layer。即使如此,也不能全指望API自己的容错机制,驱动程序也应该阻止一些可能发生硬件损坏的操作。我们的目的是在渲染期间尽量不发生错误,也就是说,错误检测不应该在渲染期间执行。致命的错误包括Depth Buffer与渲染目标之间尺寸不符合,Shader编译错误等等。
CPU 与GPU之间共享资源也是讨论已久的问题。比如,OpenGL与Direct3D都允许先在程序自己的内存地址里创建一个顶点缓冲,然后全部上传到显卡中。虽然说性能有了很大的提高,但是带宽的差距依旧非常巨大,显卡与高速显存只见的带宽能够达到50GB/s,而PCI-E总线最多只能在GPU与系统内存之间实现2.8G/s的带宽。即使如此,由于CPU与GPU处理的数据格式并不相同,而且数据储存的格式也并不相同,所以只能期望应用程序自己进行性能的优化。传统意义里,GPU一般是资源的Writer,而CPU一般是Reader。为了优化性能,需要明确的知道,那些读取哪些资源,写入哪些资源。幸运的是开发人员一般都知道这一点。比如,渲染目标以及纹理一般是GPU的读取资源,限制写入操作。可对于顶点缓冲的就有了难题,因为GPU读取的是静态的网格,而一般由CPU进行动画的计算,生成动态的网格,然后在传入GPU。这样一来,在CPU与 GPU之间的读写操作就非常频繁,占用可怜的带宽。Direct3D 10把资源(Resource)4类:Default,Immutable,Dynamic,Staging。Default资源包括简单的纹理,渲染目标,静态的顶点缓冲,一般通过Copy操作生成。而Immutable资源不允许事先通过Copy生成,但是提供了其他的生成方式。Default与Immutable资源不能被CPU访问。Dynamic资源可以被绑定到管线,与CPU写操作进行映射。比如让CPU进行动画的计算,视频解码等等,直接写入Dynamic资源。Staging资源允许CPU映射,但是可以互相进行拷贝操作。
我们也对HLSL语言进行了改进,把底层实现封装,开发人员无须了解底层虚拟机细节就可以进行开发。特性如下:
-
无须显式进行任何资源的赋值操作。
-
Bind-by-Position 代替了Bind-by-name
-
尽量让开发人员使用高级语言
开发人员一般使用在多个Shader里传递数值的方式进行通信。当有了多Multiple Constant Buffers后,我们相信编译器会更加对单缓冲操作进行更好的优化,但是开发人员依旧需要制定要写入的Constant Buffer。第二个变化是非常巨大的,与以前的发展发展方向完全不同。这主要是为了性能和未来的发展考虑。Shader里对输入输出变量的声明更类似于C语言的原型(这一点GLSL已经体现出来)。兼容性,在这里的意义是输入与输入的数据类型要完全符合。Bind-by-position也用在了顶点缓冲对IA与SO的邦定上。尽管这样在这些处理阶段我们还是定义了特殊的Objects,以便在运行期执行昂贵的Matching操作。第三项也是和开发密切相关的,这要求我们在新的实现中不再提供Intermediate语言的支持。我们发现即使非常优化的代码,编译器为IL产生的机器码效率低于其他单元的机器码。而且当与驱动程序结合起来的时候,我们将不可能只对一个IL单元进行额外的优化。系统可以通过编译器把IL处理过的数据作为调试信息输出,不过不允许程序开发者在运行期进行对编译器的输出做任何更改。谁都希望编译产生的代码是最最优化的,可是这也带来的许多问题。最重要的是驱动程序究竟能为IL产生多么优化的机器码。Shader复杂度提高要求开发人员自己对算法、执行顺序进行优化。而且,编译关键部分的代码,结果一定要相同,这样当使用多通道算法的时候才能产生相同的中间值(Intermediate),在各个Shaders之间进行复制操作。我们考虑了一些实现方式,比如当它们需要内联的时候,子程序被编译为相同的码。同时也不能忽视驱动程序的编译器,那也是性能优化的关键。需要注意的是,我们提倡的使用模型是预先编译好HLSL代码,等到执行的时候由驱动程序编译为机器码,系统当然也支持运行期的动态编译与执行。
我们同时也注意到,可编程管线的价值不仅仅在于Shader Program,也应该动态支持的特效实现比如CgFX。由此,产生了HLSL-FX 10。FX系统要考虑问题是,开发人员如何使用,以及如何提高效率。应用了FX系统后,如何有效地进行状态处理又成为了讨论的主题。为了最好的性能最好的办法依旧是只使用一个效果,渲染好所有的物体。Effect,其实是就是一系列操作的集合。为了性能效率,程序最好一次性渲染好使用同一个效果的物体。在真实的场景中,Effect只是一个环节,其余还有诸如View Position处理,纹理贴图等等其他操作,而且,不可能总是根据Effect渲染场景,还需要考虑诸如距离,透明状态等等。
系统体验
整个工程从2003年开始API的设计,以及硬件的设计,2006年公布。2004年开始测试软件,公开于2005年。测试数据如下,不过都是旧的,因为真正合格的测试环境还没有。
Operation |
Direct3D 9 |
Direct3D 10 (reference) |
---|---|---|
Draw |
1470 |
154 |
Bind VS Shader |
6636 |
416 |
Set Constant |
3297 |
916 |
Set Blend Function |
787 |
530 |
在开发过程中,我们写了许多关于GS,SO等的DEMO程序。包括Soft shadow volume,Procedural content generation,High Order normal interpolation,motion blur等。开发人员也在进行程序的移植,从DirectX9过渡到DirectX10,同时我们正在与外部的开发人员使用新的API从头开始编写程序,估计过几年将会出现完全使用DirectX10的程序。
总结
我们正在总结开发设计经验,等到Direct3D10真正发布的时候对API进行一些改动升级。软件一直在发展,可是硬件的发展却很缓慢。这些都是在升级时需要考虑的。长远的,我们依旧在探讨管线结构与编程模型。系统的瓶颈依然在如何有效的进行处理,硬件的发展是关键。比如传统的过程纹理以及表面的处理,已经即将被位移映射等新技术代替。我们相信,随着以后的硬件软件的发展将会在整体上超越目前的系统。