分类: LINUX
2012-12-21 10:01:51
http://www.cnblogs.com/clayman/archive/2011/10/09/2203177.html
The Beauty of DirectX 11 (1) --- Device & Resource
作者:clayman
仅供个人学习使用,请勿转载,勿用于任何商业用途。
这是一系列关于DirectX 11的文章中的第一篇,部分内容来自于一书(我没有电子版,不要找我要电子版@_@!!如果你感兴趣,可以找到此书的源码),但不是翻译。文章不会介绍常见的基本概念,并且假定读者已经有DirectX9/10或者xna使用经验,并对图形学有一定了解。此外,也不会有太多示例代码,我将会着重介绍DirectX 11中API的新特性和用法,整个pipeline流程以及HLSL 5.0, 文章将不定期更新,但不保证最后一定能写完:)
当使用DirectX 11时,首先要清楚的就是device拆分为了2个类:device和device context。ID3D11Device负责资源创建,查询硬件功能一些和debug相关的操作。ID3D11Device还引入了一个称为feature levels的概念(和dx9中的Caps类似),允许DirectX11运行在早期硬件上,比如DX9/10级别的GPU。这意味着开发者可以使用一组统一的接口来开发运行在不同代硬件上的程序。因此,如果你正考虑更新引擎的话,可以直接跳过DirectX10,升级到DirectX11。
所有使用资源,管理渲染流水线的功能,则属于device context的职责,包括绑定资源,shader,设置render state,执行渲染命令等等。Device context的功能通过ID3D11DeviceContext接口实现, 为了支持多线程,又分为immediate context和deferred context两种。前者和之前的DX device类似,每个程序只允许有一个immediate context,它所发出的所有命令都直接由runtime提交到到驱动执行。Dferred context所发出的命令则先缓存在一块command list缓冲中,提交整块缓冲时,才会依次执行其中的指令。这部分会在讲多线程的时候详细讨论。可以通过D3D11CreateDevice和D3D11CreateDeviceAndSwapChain创建设备,SDK对这两个函数,以及所有参数的用法有详细文档,这里不再重复。
接下来,我们将详细讨论DX11中的资源(Resource),因为它们是所有操作的最基本元素。DX11中的资源分为两类:buffer和texture,每种又有各自的子类型,以及不同的配置参数,基本类关系如下:
ID3D11Resource
|
|---- ID3DBuffer
|---- ID3D11Texture1D
|---- ID3D11Texture2D
|---- ID3D11Texture3D
如前所述,所有资源都必须通过ID3D11Device创建,所创建的资源可以直接或者间接通过resource view绑定到pipleline的某个阶段。虽然每种创建每种资源的方法不一样,但模式是相同的:所有创建函数都接受3个参数 1. 包含了对所创建资源各种选项描述的结构(resource description), 2. 指向D3D11_SUBRESOURCE_DATA结构,用来初始化资源数据的指针, e.g 用来初始化vertex buffer的顶点数据。如果希望稍后再填充数据,可以使用null;3,所创建的资源对象。每种资源的resource description都不一样,接下来,我介绍一些常用参数和选项:
Resource Usage Flags(D3D11_USAGE)
资源可以存放在显存和系统内存,可以作为复制操作的数据源或者目标。DX根据这个枚举来选择放置资源的位置以及内部如何操作,e.g 把只有GPU能操作的资源放到显存中,这个参数类似于之前的memory pool,区别在于memory pool是直接指定了资源位置。D3D11_USAGE有四个值,每种的访问级别也不一样
GPU-Read GPU-Write CPU-Read CPU-Write
Default yes yes
Dynamic yes yes
Immutable yes
Staging yes yes yes yes
Immutable 最简单的类型,一旦创建之后数据就不可改变。适合储存静态数据,比如静态常量,顶点索引缓冲等。
Default 对于只需要被GPU操作(读写)的数据来说,这是最优的选项,适合于render target, stream ouput vertex buffer等。这种类型资源通常存在于显存中,效率非常高。
Dynamic 这是一种CPU可以访问的资源,允许CPU写入数据,之后由GPU读取,注意CPU是不能访问已写入输入的,适合于shader constant buffer.
Staging 这种类型的资源提供了一种特别的访问模式,允许GPU写入数据,之后由CPU读出。它存在的主要作用是提供其他3种资源之间的交换,对于GPGPU和stream output 特别有用
CPU Access Flag(D3D11_CPU_ACCESS_FLAG)
这是一个位标记,在指定usage flage之后,进一步说明是否允许cpu访问某块资源(其实个人觉得这个枚举稍微有些多余)。这个标记必须配合usage使用,比如只有dynamic和stagin资源能设置为D3D11_CPU_ACCESS_WRITE。如果一块资源不需要被CPU访问,那么必须把值设置为0.
Bind Flags
指定资源可以绑定到pipeline的哪些位置。这同样是一个位标记,因此可以组合起来,允许资源绑定到流水线的多个位置。
D3D11_BIND_FLAG {
D3D11_BIND_VERTEX_BUFFER,
D3D11_BIND_INDEX_BUFFER,
D3D11_BIND_CONSTANT_BUFFER,
D3D11_BIND_SHADER_RESOURCE,
D3D11_BIND_STREAM_OUTPUT,
D3D11_BIND_RENDER_TARGET,
D3D11_BIND_DEPTH_STENCIL,
D3D11_BIND_UNORDERED_ACCESS
}
整个管线只有8种类型的位置可以绑定资源,分别对应D3D11_BIND_FLAG中的一个值。Vertex/index buffer表示资源可以作为input assembler阶段的输入数据;render target和depth stencil表示资源可以接收output merger阶段的数据;stream out表示可以接收来自于pipeline生成的几何数据。这几个枚举分别对应着管线中的一个可绑定点。Constant buffer, shader resource和unorderer access则都可以作为可编程阶段的输入数据。后面会讨论三者之间的区别。
此外Miscellaneous Flags中的枚举值,用来指定一些特别的用途,大部分值都不经常用到,不再仔细介绍,详见文档。唯一值得一提的是D3D11_RESOURCE_MISC_GDI_COMPATIBLE,这个值允许创建与GDI兼容的资源,对于需要用GDI创建文字的程序可能会用到。
Resource View
有两种方法把资源绑定到pipeline,如前所述的8种绑定点中,有一半可以直接绑定资源,分别是vertex/index/const/stream buffer;其他4种类型则必须通过Resource View(RV)绑定。Resource View是在DirectX10时代引入的概念,它实际上是连接resouce和pipeline绑定点之间的一种适配器对象,作用类似翻译。同一种资源可以对应多个不同的RV,允许它被绑定到不同的pipline阶段。
resource type
有四种不同类型的RV,分别对应着pipeline中的4个可绑定位置:
Render target view (ID3D11RenderTargetView)
Depth stencil view (ID3D11DepthStencilView)
Shader resource view (ID3D11ShaderResourceView)
Unordered access view (ID3D11UnorderedAccessView)
第一种就是常见的render target,大家应该很熟悉,没有太多可说的。DSV和RTV的区别在于为了优化性能,可以选择DSV是否可写。不可写的DSV资源既可以绑定为depth stencil buffer做depth stencil test,又能*同时*绑定为SRV使用。SRV其实和之前DX中纹理的作用几乎一样。UVA则稍微有些特别,和SRV类似,UVA也可以为某个shader阶段提供数据,不仅如此,它还允许在同一个shader阶段,同时读取和写入数据,并且能把数据写入到资源的任意位置,灵活性非常高,但只能在pixel shader和compute shader中使用。
Resource View Creation
创建RV的模式和创建资源的模式类似,也通过ID3DDevice的成员函数完成,每个Create函数接收3个参数,第一个是RV所依赖的资源;第三个是所创建的RV对象。第二个参数则是每个类型都不同的一个属性结构,SDK中对这些结构有详细描述,这里不再重复。
Resource in Detail
Buffer Resource
BR是一维的线性内存块,虽然有多种不同的buffer配置选项,但内部数据都是基本的线性布局。Buffer以byte为单位,大小等于每个元素大小乘元素个数,与普通数组类似。接下来将详细介绍每一种buffer。
Vertex Buffer
VB最基本的用途就是保存几何体顶点数据,是一系列格式相同的顶点的集合。当然,也可以把顶点的不同元素分离到不同VB中,比如位置信息储存在vb1,纹理和法线信息储存在vb2,根据渲染时的需要选择使用合适的buffer,也就是常说的multi-stream。此外,VB也可以用来实现instancing rendering(注:后文将把instance翻译为”副本”,而不是常见的”实体”,这样更容易理解),比如用一块或几块vb保存per-vertex data,也就是普通的顶点数据,再用额外的vb来提供per-instance data,比如每个模型副本的world matrix,color等等,这样便可一次渲染大量图形,减少CPU负载。后面还会详细介绍与instancing rendering相关的内容。
VB主要用于Input Assembler阶段给整个流水线提供顶点数据,也可以用于stream out阶段接收生成的顶点,以便后续渲染使用。
创建VB时,必须使用D3D11_BIND_VERTEX_BUFFER以及可选的D3D11_BIND_STREAM_OUTPUT枚举。再根据程序预期的用途,选择合适的usage。e.g对于静态资源(静态模型,地形数据),使用D3D11_USAGE_IMMUTABLE枚举,并且使用D3D11_SUBRESOURCE_DATA参数初始化资源;对需要频繁修改的资源使用D3D11_USAGE_DYNAMIC以及D3D11_CPU_ACCESS_WRITE枚举;对于需要作为stream output输出目标的资源,使用D3D11_USAGE_DEFAULT。VB不需要RV就可以绑定到管线。
Index buffer
Index buffer的基本操作都与vertex buuffer基本类似,详细请参考sdk文档。
Constant Buffer
Constant buffer(cb)是DX10中引入的概念,它取代了DX9时代GPU常量寄存器的概念,允许通过一块大小可变的buffer向shader提供常量数据,而不是之前数量非常受限的n个寄存器,这也是我们遇到的第一种可在shader着色阶段由HLSL访问的资源。 管线的每个可编程阶段都能同时访问一个或者几个cb,对于shader代码来说,cb中的数据都是全局常量,作为cb而创建的资源不能绑定到其他*类型*的管线位置,但同一个cb可以同时绑定到管线的多个不同阶段。
虽然cb与DX中shader constant的用法几乎一样,但要特别注意,cb中的数据总是作为一个整体被提交给GPU,这意味着即使cb中只有一个变量改变了,也必须重新提交整个cb。因此,所有关于DX10/11的文章都会强调不要把所有变量都放到一个cb中,而是按照变量改变的频率来组织变量,以尽量减少带宽消耗。比如,把viewMatrix,viewProjMatrix,eyePosition,sunVector等per-frame数据放到cb0中,只要每帧渲染前更新一次即可;把worldMatrix,localLight,objectColor等作为per-object参数放到另外一块cb中,每次渲染物体时更新。游戏总是以每秒30fps以上的速度运行,并且每帧需要渲染大量物体,因此,合理组织cb非常重要!
创建cb的代码和上一次创建vertex buffer的代码类似,大部分参数的用途也一致,选择合适的枚举,以获得最好性能。有三点需要注意的地方,1. cb中的数据结构应该直接对应着HLSL中的数据结构,这样更新cb时,只需要直接复制数据即可;2. cb的大小必须是16byte的n倍(n个float4大小);3. cb的bind flag只能是D3D11_BIND_CONSTANT_BUFFER,不能与其他位标记组合使用。使用cb时,不需要resource view,可以直接绑定到管线。
因为cb是shader直接可访问的资源,在HLSL中,用以下语法声明cb:
用cbuffer关键字声明cb,cb的名称一般对HLSL没太多用处,而是让C++应用程序端来识别特定cb。可以通过shader reflection API获得每个cb中的数据结构信息,在介绍HLSL的部分会详细讨论。
Buffer/Structured Buffer Resource
接下来的一种资源,根据所保存的数据类型不同,又分为standard(buffered) buffer resouce(扭曲的名字)和structured buffer resource。两者都是类似数组的结构,区别在于BBR的元素是普通内置类型,比如float4,而SBR的元素则是自定义的结构。之所以要做区分,原因是底层在处理数据映射到HLSL时的方式稍有不同。这种类型的buffer特别适合于shader需要访问大量数据的情况。也是目前为止第一种需要通过resource view绑定到管线的资源。所有可编程shader阶段都可访问b/s buffer,在pixel shader和computer shader中还允许写入操作。通过不同的resource view来控制b/s buffer的可访问性,比如shader resource view, unordered access view或者两者一起使用,任何shade都可以使用shader resource view标志,而unordered access view则只能对ps和cs使用。当b/s buffer为只读时,可以同时绑定到管线的多个阶段;允许写入时(使用了uav标志)则只能绑定到一个位置。
B/S buffer有三种典型的用途,第一种,用来保存静态数据,比如预计算的辐射传播(PRT)数据;第二种,运行时,由CPU计算出的数据,比如很多涉及到GPGPU的算法;最后一种则是由GPU计算填充的数据,比如由GPU计算的物理模拟结果。创建b/s buffer时,通常根据这三种用途,选择合适的访问参数。对于structured buffer来说,除了指定buffer大小之外,还需要提供每个结构的大小。以下是创建structured buffer的代码:
B/S buffer需要通过RV才能绑定到管线上,并且只能是shader resource view或者unordered access view。需要在创建RV时提供buffer的数据格式,BBR的格式必须是DXGI中定义的格式之一,SBR的格式总是DXGI_FORMAT_UNKNOWN。创建RV时,可以指定开始元素(ElementOffset)的位置和元素个数(ElementWidth),可以选择把整个或者一部分buffer数据包含在view中。对于HLSL来说,绑定到管线的buffer则总是一个下表标从在[0,ElementWidth]之间的数组。
使用ElementOffset和ElementWidth参数,可以把同一个B/S buffer分成多个子buffer,每个子buffer都可以有独立的RV。创建UAV的方法和SRV类似,但需要额外加一个属性:
desc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_COUNTER;
为了在shader中访问B/S buffer数据, HLSL中也要用类似一种模板或泛型的语法声明相应的变量,下面是SBR的声明:
可以通过两种方法声明结构化buffer:RWStructuredBuffer<>和StructuredBuffer< >。区别在于前者对应着可读可写的buffer(UAV),后者则是只读的数据(SRV),注意,不同buffer对应的寄存器标识也不一样。声明了HLSL变量以后,就能用普通数组的语法,通过索引访问buffer中的数据了。HLSL中提供GetDimensions()函数,可以返回buffer大小,或者是SBR中的数组元素个数。对于使用UAV绑定的SRB来说,由于指定了D3D11_BUFFER_UAV_FLAG_COUNTER标记,还可以使用IncrementCounter()和DecrementCounter函数管理数据。
Append/Consume Buffers
Append和comsume buffer都是SBV的变体。本质上,他们都是需要UAV绑定的资源,但他们在HLSL中实现了一种类似堆栈的访问行为:使用append()函数把元素push到buffer中,或者用consume()函数拉出元素。对于这两种buffer来说,添加元素的顺序并不重要,但所添加元素的数量很重要。UAV内部会记录添向buffer中添加或者删除的元素数量,因此当不同的GPU线程同时操作这样的buffer时,并不需要同步,大大提高了效率。
我们通过一个简单的例子来介绍这种buffer的用途,一个基于GPU的粒子系统:系统使用2块buffer保存粒子信息,一块保存粒子当前的状态,另一块保存更新过的粒子。运行时,computer shader中的每个一个GPU线程都使用consume()方法读取当前粒子信息,进行更新计算,然后,用append()方法把结果写到另一块buffer中。由于每个粒子的状态都是独立的,所以buffer中粒子的顺序无关紧要,只要保证数量正确即可。
创建A/C类型的buffer约束比较严格,因为buffer需要可同时读写,需要使用D3D11_USAGE_DEFAULT标志,此外绑定标识必须包含3D311_BIND_UNORDERED_ACCESS标识,一般还需要加上D3D11_BIND_SHADER_RESOURCE。创建相应的RV时,数据格式总是DXGI_FORMAT_R32_UNKNOWN,同时无论是append还是comsume buffer都必须用D3D11_BUFFER_UAV_FLAG_APPEND参数。上一部分介绍B/S buffer时曾经说个可以把B/S buffer划分为多个subresource,分别使用不同的RV,但是,对于允许读写操作UAV来说却不行,这样的资源只能作为一个整体。
下面是在HLSL中声明A/C buffer的代码
struc Particle
{
float3 position;
float3 velocity;
float time;
}
AppendStructuredBuffer
ConsumeStrucuredBuffer
Byte Address Buffers
BAB为HLSL提供了一种相对低级的方式来访问显存块。与通过索引访问元素的方法不同,BAB使用字节地址访问其中的元素: 字节地址n代表的值为从资源开头偏移n个字节后的4个32位无符号整数的值,注意,n必须为4的倍数,指向的4个32位无符号整数也可以通过转型转义为其他类型的值。这种类型的buffer为HLSL提供了强大的数据管理操作能力,几乎可以用它来实现任意类型的数据结构。
举例来说,一个储存32位颜色值的链表,每个链表节点由一个颜色值和指向下一个元素的标记组成。当添加第一个元素时,颜色值被写到偏移值为0的位置,指向下一个元素的索引为-1。 添加第二个元素时,程序首先读取偏移值为0处的2个32位值,然后通过索引,依次找到链表末端,最后完成添加。使用BAB,以前很多只能在CPU上计算的传统算法,都可以能GPU上实现了! 当然,由于GPU天生的平行性,实际情况要稍微复杂一些,以后会仔细介绍。
BAB的创建方式与前面介绍的几种资源基本一致,唯一的区别是需要把MiscFlags指定为D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS。只读的BAB可以通过shader resource view绑定到任意shader阶段;作为UAV使用的可读写BAB则只能绑定到pixel shader和compute shader阶段。两种情况下,RV都必须是DXGI_FORMAT_R32_TYPELESS格式,UAV还必须加上D3D11_BUFFER_UAV_FLAG_RAW标识。
在HLSL中,用以下代码声明BAB资源:
ByteAddressBuffer rawBuffer0;
RWByteAddressBuffer rawBUffer1;
Indirect Argument Buffers
前面介绍的几种buffer主要用于把数据从CPU端传递到GPU端,而IAB的设计理念是允许GPU自己填充数据,并且在后面的计算中使用。IAB主要在以下三个函数中使用:
DrawInstancedIndirect(ID3D11Buffer* pBufferForArgs, UINT AlignedByteOffsetForArgs);
DrawIndexedInstancedIndirect(ID3D11Buffer *pBufferForArgs,UINT AlignedByteOffsetForArgs);
void DispatchIndirect(ID3D11Buffer *pBufferForArgs,UINT AlignedOffsetForArgs);
三个函数都以indirect后缀结尾,与没有indirect后缀的方法相比,虽然参数类型不同,本质却相同,比如DrawInstanced()接受4个参数:vertexCountPerInstance,instanceCount,startVertexLocation和startInstanceLocation。Indirect版本同样需要这4个参数,只不过把他们放到了第一个参数pBufferForArgs中,并且通过第二个参数定位他们在buffer中的位置。因此,必须保证pBufferForArgs在指定偏移位置有4个有效的uint值,并且偏移值必须以4byte对齐的,pBufferForArgs可以在不同位置,包含多组不同的参数值,通过offset选择希望使用的参数。这样的好处在于 buffer数据可由GPU填充,所以CPU完全不需要知道要绘制多少图元!图形管线输出的数据和CPU计算的结果都可以用来更新IAB数据。 比如,把compute shader的计算结果保存到AppendStructuredBuffer中,使用ID3D11DeviceContext:CopyStructureCount把AppendStructuredBuffer中的数据复制到IAB中,最后使用DrawInstancedIndirect()把CS计算的数据直接渲染出来,整个过程只需要很少的CPU参与,CPU完全不用知道渲染了多少图形。
创建IAB的方法和前面几种buffer没有太多区别,一般来说,如果希望用GPU填充数据,那么一定要使用default usage,此外,MiscFlags必须是D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS,最后,byteWidth必须是4byte的n倍。
当由GPU填充IAB数据时,比如通过stream out,render target或者UAV方式,必须和配合相应的resource view一起使用,当由CPU填充数据时,则主要通过UpdateSubresource方法和CopyStructureCount。
至此,我们介绍了DirectX 11中的所有buffer resource,下一部分将讨论texture resource。
------------------------------to be continue----------------------------------------------
再次提醒,已经发了的四篇文章对DX11的介绍都比较抽象,如果觉得看不太懂,那么应该先找一些有demo的DX11的入门教程来看。这系列的文章偏重对DX11 API原理的介绍和梳理,适合作为参考资料,而不是第一本入门教程。未来的内容在介绍完texture resource之后,还主要有以下几个部分:
Rendering Pipeline 介绍流水线的每一个阶段
Tessellation Pipeline
Computation Pipeline 着重介绍DX11中两个最重要的功能
HLSL 5.0
Multithreaded Rendering