突然发现,D3D的Render API中蕴藏了很多可以用于做性能优化的参数,而平时自己对这块的了解不多,基础不是非常扎实,因此专门开一篇文章来进行学习与总结。
DX Render API
1. DrawPrimitive
大概的使用逻辑:
// 设置vertex buffer
g_pd3dDevice->SetStreamSource(0,g_pVB,0,sizeof(CUSTEMVERTEX));
// 设置顶点格式,此处使用了自定义顶点格式
g_pd3dDevice->SetFVF(D3DFVF_CUSTEMVERTEX);
// 开始进行绘制
g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLELIST,0,1);
接口细节描述:
HRESULT DrawPrimitive(
[in] D3DPRIMITIVETYPE PrimitiveType,
[in] UINT StartVertex, // 需要绘制的起始顶点在VB中的序号
[in] UINT PrimitiveCount // 需要绘制的图元的数目
);
PrimitiveType描述的是图元的类型:
typedef enum D3DPRIMITIVETYPE {
D3DPT_POINTLIST = 1, // 点集合
D3DPT_LINELIST = 2, // 线集合
D3DPT_LINESTRIP = 3, // 多段线(相邻线段共用一个顶点)
D3DPT_TRIANGLELIST = 4, // 三角形集合
D3DPT_TRIANGLESTRIP = 5, // 三角形阵列(相邻三角形共用两个顶点)
D3DPT_TRIANGLEFAN = 6, // 扇形三角面
D3DPT_FORCE_DWORD = 0x7fffffff
} D3DPRIMITIVETYPE, *LPD3DPRIMITIVETYPE;
值得一提的是,对于D3DPT_TRIANGLESTRIP而言,backface-culling标记会自动在奇数面片上做一次翻转避免被剔除,上面的多种图元对应的形状按照顺序如下图所示:
这个接口在D3D11及往后API中,对应的是Draw:
Draw( UINT VertexCount
UINT StartVertexLocation)
UINT VertexCount: How many vertices to read sequentially from the Vertex Buffer(s)
UINT StartVertexLocation: Which Vertex to start at in each Vertex Buffer.
2. DrawPrimitiveUp
这个方法通常用在顶点数据不能用Vertex Buffer来存储的场景(什么情况下不能存储?单次绘制吗),只支持单个Vertex Stream,即参数中传入的数据会被自动分配到Stream 0上,如果在shader中访问Stream 0之外的Stream,会报错。
这个接口大概使用逻辑:
g_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, 2, (void*)Verticesrhw, sizeof(CUSTOMVERTEX_RHW)); }
可以看到,不需要提前做StreamSource与FVF的设置,直接调用渲染API,将待渲染的数据作为参数传入到API中,接口细节描述:
HRESULT DrawPrimitiveUP(
[in] D3DPRIMITIVETYPE PrimitiveType, // 图元类型,同上
[in] UINT PrimitiveCount, // 图元数目,同上
[in] const void *pVertexStreamZeroData, // 顶点数据在内存中的指针
[in] UINT VertexStreamZeroStride // 顶点数据的尺寸(字节为单位)
);
需要注意的是,pVertexStreamZeroData所指向的数据并不需要一直保持有效,在这个接口调用完成之前,渲染所需要的数据就已经访问完成,也就是说,当这个接口调用之后,这个指针所指向的数据就可以释放了。
3. DrawIndexedPrimitive & DrawIndexedPrimitiveUp
DrawIndexedPrimitive的大致使用逻辑为:
gPD3DDevice->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_GOURAUD);
gPD3DDevice->SetStreamSource(0, gPVertexBuffer, 0, sizeof(CUSTOM_VERTEX));
gPD3DDevice->SetFVF(D3DFVF_CUSTOM_VERTEX);
gPD3DDevice->SetIndices(gPIndexBuffer);
gPD3DDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 17, 0, 16);
可以看到,相对于DrawPrimitive多了一个SetIndices的步骤,再来看下这个接口的调用逻辑:
HRESULT DrawIndexedPrimitive(
[in] D3DPRIMITIVETYPE PrimitiveType, // 图元类型,同上,不支持D3DPT_POINTLIST
[in] INT BaseVertexIndex, // 起始顶点在VB中的偏移
[in] UINT MinVertexIndex, // 所有待绘制顶点相对于BaseVertexIndex的最小偏移
[in] UINT NumVertices, // 从BaseVertexIndex+MinVertexIndex开始,会用到的顶点数目
[in] UINT startIndex, // 当前DrawCall在IB中的起始索引
[in] UINT primCount // 图元数目
);
BaseVertexIndex:
这个参数是用于给IndexBuffer中的index来增加一个全局的offset使用的(即如果IB中取出某个element的数值是3,那么它实际上对应的顶点是BaseVertexIndex+3),目的是用于应对那些将多个VertexBuffer合并成一个,但IndexBuffer不做合并的情况。
更具体一点,假如我们有桌子、椅子两个物件,原本两者各有一个VB一个IB,此时,我们将两个VB合并到一起,桌子在前,椅子在后,桌子的顶点数为100, 椅子的为50,那么我们在绘制两者的时候,可以共用同一个VB,只需要重新设置一个IB,并在绘制桌子的时候,将BaseVertexIndex设置为0,绘制椅子的时候,将BaseVertexIndex设置100来输出正确效果。
//绘制桌子,设置为矩形的索引缓存
g_pd3dDevice->SetIndices(pIB_Desk);
g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,0,100,0,200 );
//绘制椅子,设置为三角形的索引缓存
g_pd3dDevice->SetIndices(pIB_Chair);
g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 100,0,50,0,100 );
也就是说,这种IB的整体偏移参数实际上是为了降低VB设置频率使用的。
MinVertexIndex & NumVertices:
这个参数表示我们当前传入的IB,在顶点buffer中将会访问哪些顶点,这些顶点范围为:[BaseVertexIndex + MinVertexIndex, BaseVertexIndex + MinVertexIndex + NumVertices - 1 ]。
这里之所以需要指定范围,在微软的API介绍页面中没有说明,但是推测是出于硬件访问加速考虑,在指定了偏移量与长度之后,就能够知道哪些顶点是当前drawcall中常用顶点,有助于提高缓存命中率?
不过有一点还不明白,为什么需要MinVertexIndex,看起来这个参数的作用完全可以由BaseVertexIndex来实现?
startIndex & primCount:
这两个参数用于指定IB中的有效数据段,startIndex指定从IB中的哪个位置开始读取,primCount则指定读取多少数据量。
这两个参数可以用于实现实例化渲染,举个例子,类似于上面的VB合并,我们这里还可以进一步将IB也合并了,这样在渲染桌子跟椅子的时候,可以省去IB的设置,直接调用两个DrawCall:
// 设置整合后的IB
g_pd3dDevice->SetIndices(pIB_DeskChair);
//绘制桌子
g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,0,100,0,200 );
//绘制椅子
g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 100,0,50,600,100 );
甚至更进一步,我们还可以将材质相同(比如通过TextureArray或者Bindless将多个模型材质整合到一起)的Mesh的VB & IB都合并到一起,之后设置好Instance的StreamSource,从而通过一个DrawCall完成若干个不同模型的渲染。
DrawIndexedPrimitiveUp的使用方法与DrawPrimitiveUp类似,这里不需要提前指定VB/IB,而是在参数中传入对应的数据指针:
HRESULT DrawIndexedPrimitiveUP(
[in] D3DPRIMITIVETYPE PrimitiveType, // 图元类型,同上
[in] UINT MinVertexIndex, // 索引全局偏移量,同上
[in] UINT NumVertices, // VB中从MinVertexIndex开始会用到的顶点数目,同上
[in] UINT PrimitiveCount, // 图元数目,同上
[in] const void *pIndexData, // 索引数据流
[in] D3DFORMAT IndexDataFormat, // 索引数据格式,只支持D3DFMT_INDEX16与D3DFMT_INDEX32
[in] const void *pVertexStreamZeroData, // 顶点数据流,只支持单一Stream
[in] UINT VertexStreamZeroStride // 顶点数据结构尺寸
);
相对于DrawIndexedPrimitive,DrawIndexedPrimitiveUp移除了对索引起始偏移startIndex与MinVertexIndex参数,后者前面说过,用处不大,而移除前者就限定了,我们一个IB只能用于一个物件。
这两个接口在D3D11及以后的API中,对应的是DrawIndexed:
DrawIndexed( UINT IndexCount,
UINT StartIndexLocation,
INT BaseVertexLocation)
UINT IndexCount: How many indices to read sequentially from the Index Buffer.
UINT StartIndexLocation: Which Index to start at in the Index Buffer.
INT BaseVertexLocation: Which Vertex in each buffer marked as Vertex Data to consider as Index "0".
Note that this value is signed. A negative BaseVertexLocation allows, for example,
the first vertex to be referenced by an index value > 0.
4. Up不Up?
前面给出的四个接口,其实是两两成对的,差别就在于是否有Up,没有Up的接口,需要通过SetStreamSource与SetFVF指定VertexBuffer,而有Up的接口就将这两个数据直接放入到参数中的pVertexStreamZeroData(对于带Index的,IB也是同样处理)与VertexStreamZeroStride中了。
这里一个疑问点是,两者的区别仅仅在于调用方式的不同吗?[1]中对这个有一些相对清楚的说明,我这里偷懒直接引用:
DrawPrimitiveUP调用时,其内部相当于维护了一个dynamic vertex buffer(动态顶点缓存,单帧用完即弃,与之相对的是static vertex buffer,是属于生命周期相对较长的数据缓存,可以横跨多帧存在),这跟我们通过SetStreamSource自己指定一个dynamic vertex buffer没区别。
为什么不推荐用Up接口?
1. 显存容量提升:
DX8发布前,显存容量低,静态顶点缓存不够用,数据塞进去很快就要搞出来,所以大家普遍使用动态顶点缓存;
在DX8发布后,显存容量有了很大提高,静态顶点缓冲可以缓存在显存或AGP内存里,从而节省带宽占用,因此使用静态顶点缓冲可以比DrawPrimitiveUP和自行设置动态顶点缓冲都快很多。
2. 额外复制消耗:
相对动态顶点缓冲而言,DrawPrimitiveUP还需要将用户内存里的顶点数据复制到内部动态顶点缓冲,即多了一次复制,如果顶点数量较大,复制开销也会加大。
3. 应用场景有限:
一帧内DrawCall数量会影响CPU的占用率,1G处理器30FPS下每帧700Batch左右就会占用100%CPU。
每个设置设备状态到发出绘制命令的转换都将产生一个DrawCall。
动态顶点缓冲通常用于DrawCall可合并的情况,即将原本需要多个DrawCall来绘制的数据塞到一起,之后一次性提交,以减少DrawCall数。但是能够合并到一起的DrawCall并不多,或者说要想找出能够合并的Drawcall不是一件简单的事情,因此在大部分应用下,这个优势并没有被发挥出来。
5. DrawInstanced & DrawIndexedInstanced
DrawInstanced 跟 DrawIndexedInstanced接口都是D3D11即开放出来的接口,下面逐个介绍一下二者的用法。
DrawInstanced的大概用法:
void DrawInstanced(
[in] UINT VertexCountPerInstance, // 每个Instance对应的顶点数目
[in] UINT InstanceCount, // Instance数目
[in] UINT StartVertexLocation, // 顶点buffer中,起始顶点的Index
[in] UINT StartInstanceLocation // Instance Buffer中,起始Instance的Index
);
void Draw(PrimitiveTopology pt, int numVertices, int start, int instanceCount)
{
ID3D12GraphicsCommandList* list = deviceMan.GetCommandList();
list->IASetPrimitiveTopology(pt); // PrimitiveTopology跟前面的PrimitiveType是同义词
list->DrawInstanced(numVertices, instanceCount, start, 0);
}
从上面的代码可以看到,这里是将某个模型(instance)绘制多边所使用的接口,这里每个模型的数据用vertex buffer存储,没有用到索引buffer,而DrawIndexedInstanced从名字上推测与DrawInstanced的区别就在于添加了Index Buffer:
void DrawIndexedInstanced(
[in] UINT IndexCountPerInstance, // 每个Instance所包含的Index数目
[in] UINT InstanceCount, // Instance数目
[in] UINT StartIndexLocation,// Index buffer中,起始Index的序号
[in] INT BaseVertexLocation, // 顶点buffer中,起始顶点的Index,IB以此Index对应的顶点为0点Vertex,从而可以实现多个不同模型合并到一起后的实例化
[in] UINT StartInstanceLocation // Instance Buffer中,起始Instance的Index
);
void IASetVertexBuffers(
[in] UINT StartSlot,
[in] UINT NumBuffers,
[in, optional] ID3D11Buffer * const *ppVertexBuffers,
[in, optional] const UINT *pStrides,
[in, optional] const UINT *pOffsets
);
typedef struct D3D11_INPUT_ELEMENT_DESC {
LPCSTR SemanticName; // Shader中访问的变量名字,如POSITION/NORMAL等
UINT SemanticIndex; // 同变量名会有多个数据,如TEXCOORD
DXGI_FORMAT Format; // 变量数据格式
UINT InputSlot; // Buffer索引,使用的是哪个buffer(StreamSource)的数据
UINT AlignedByteOffset; //当前变量在对应Buffer中存储的数据结构中的偏移
D3D11_INPUT_CLASSIFICATION InputSlotClass; // 这个数据的组合频次,顶点还是实例
UINT InstanceDataStepRate; // 见下面的详细解说
} D3D11_INPUT_ELEMENT_DESC;
typedef enum D3D11_INPUT_CLASSIFICATION {
D3D11_INPUT_PER_VERTEX_DATA = 0,
D3D11_INPUT_PER_INSTANCE_DATA = 1
} ;
InstanceDataStepRate:
- 某个Instance数据被使用的频次
- D3D11_INPUT_PER_VERTEX_DATA 变量,这个数值需要是0
- D3D11_INPUT_PER_INSTANCE_DATA变量,这个数值不受限制
- 0表示这个数据不随instance而变化,即所有instance都使用同一个数据,不需要在instane buffer中step forward
- 1表示每个instance对应于buffer中的一个数据,这是最常用的模式
- n > 1,表示有n个instance会共享同一份数据,比如n个绿色苹果,n个红色苹果等
使用参考:
// Create instance Data
instanceBufferDesc.Usage = D3D11_USAGE_DEFAULT;
instanceBufferDesc.ByteWidth = sizeof(treePositions);
instanceBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
instanceBufferDesc.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA instanceData;
ZeroMemory(&instanceData, sizeof(D3D11_SUBRESOURCE_DATA));
instanceData.pSysMem = treePositions;
device->CreateBuffer(&instanceBufferDesc, &instanceData, &instanceBuffer);
// 设置顶点数据,在这里包含了MeshBuffer跟InstanceBuffer
// 实例数据使用D3D11_INPUT_PER_INSTANCE_DATA标识
D3D11_INPUT_ELEMENT_DESC inputElementDescLOD0[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 1, DXGI_FORMAT_R32G32B32_FLOAT, 1, 0, D3D11_INPUT_PER_INSTANCE_DATA, 1}
};
ID3D11Buffer* buffers[2] = { trunkMesh.GetVB11(0, 0), instanceBuffer };
unsigned strides[2] = { trunkMesh.GetVertexStride(0,0), sizeof(Vector3) };
unsigned offsets[2] = {0};
deviceContext->IASetInputLayout(inputLayout);
deviceContext->IASetVertexBuffers(0, 2, buffers, strides, offsets);
deviceContext->IASetIndexBuffer(trunkMesh.GetIB11(0), trunkMesh.GetIBFormat11(0), 0);
...
for(unsigned i = 0; i < trunkMesh.GetNumSubsets(0); ++i)
{
SDKMESH_SUBSET* subset = trunkMesh.GetSubset(0, i);
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST);
SDKMESH_MATERIAL* material = trunkMesh.GetMaterial(subset->MaterialID);
if(material)
evTrunkTexture->SetResource(material->pDiffuseRV11);
pass->Apply(0, deviceContext);
deviceContext->DrawIndexedInstanced((unsigned)subset->IndexCount, drawInstanceCount,
(unsigned)subset->IndexStart, (int)subset->VertexStart,0);
}
可以看到,调用Instance接口,在设置VertexBuffer的时候需要传入两个buffer,一个是普通的mesh数据,一个是instance数据。
6. Dispatch
跟其他接口用Graphics管线(VS+PS)进行渲染不同,Dispatch是用来唤起Compute Shader的接口,大概的使用方式给出如下:
// Run the particle simulation using the compute shader.
void D3D12nBodyGravity::Simulate(UINT threadIndex)
{
ID3D12GraphicsCommandList* pCommandList = m_computeCommandList[threadIndex].Get();
...
pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pUavResource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS));
pCommandList->SetPipelineState(m_computeState.Get());
pCommandList->SetComputeRootSignature(m_computeRootSignature.Get());
ID3D12DescriptorHeap* ppHeaps[] = { m_srvUavHeap.Get() };
pCommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
CD3DX12_GPU_DESCRIPTOR_HANDLE srvHandle(m_srvUavHeap->GetGPUDescriptorHandleForHeapStart(), srvIndex + threadIndex, m_srvUavDescriptorSize);
CD3DX12_GPU_DESCRIPTOR_HANDLE uavHandle(m_srvUavHeap->GetGPUDescriptorHandleForHeapStart(), uavIndex + threadIndex, m_srvUavDescriptorSize);
pCommandList->SetComputeRootConstantBufferView(RootParameterCB, m_constantBufferCS->GetGPUVirtualAddress());
pCommandList->SetComputeRootDescriptorTable(RootParameterSRV, srvHandle);
pCommandList->SetComputeRootDescriptorTable(RootParameterUAV, uavHandle);
pCommandList->Dispatch(static_cast(ceil(ParticleCount / 128.0f)), 1, 1);
pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pUavResource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE));
}
这个接口的定义给出如下:
void Dispatch(
[in] UINT ThreadGroupCountX,
[in] UINT ThreadGroupCountY,
[in] UINT ThreadGroupCountZ
);
CS中计算是通过多线程完成的,这里指定了三个维度的Thread Group长度,注意,这里是Thread Group的数目,每个Group还可以包含多个Thread,每个Group中Thread的数目不是在这里指定的,而是在shader中指定,这样可以让编译器根据寄存器数目来做一些性能平衡。
每个维度上的Group数目不能超过D3D11_CS_DISPATCH_MAX_THREAD_GROUPS_PER_DIMENSION ,这个值在D3D11/D3D12中是65535,此外,可以指定Group数目为0(任意维度),这种设定下,不会做任何计算。
7. Indirect Draw
Indirect Draw可以将一些场景遍历以及剔除的工作从CPU转移到GPU以提升整体性能,绘制所需要的Buffer数据既可以在CPU中生成,也可以在GPU中生成。
D3D12中提供了一个叫做ID3D12CommandSignature的概念,基于这个对象,在应用层可以完成三个参数的设置:
- indirect argument buffer的格式(通过D3D12_INDIRECT_ARGUMENT_DESC指定)
- indirect Draw Call类型,这里一共有三种DrawInstanced, DrawIndexedInstanced, Dispatch
- 指定对应的Resource Bindings(资源绑定关系),包含了每个Call Command特有的资源绑定,以及所有Call Command所共享的资源绑定
具体使用上,可以按照如下步骤进行:
1. 应用启动的时候,创建少量的Command Signature对象
Command Signature的作用是定义一组可以重复执行的命令,这个对象是在CPU中创建的,且GPU不可修改。
这个对象可以通过CreateCommandSignature创建:
HRESULT CreateCommandSignature(
// Command Signature的说明参数(或者说属性)
[in] const D3D12_COMMAND_SIGNATURE_DESC *pDesc,
// 当前Command Signature需要关联的Root Signature
// 如果当前Command Signature只有纯粹的Draw/Dispatch Call,那么这个值可以为NULL
// 如果Command Signature需要修改管线上的resource bindings,就需要关联一个Root Signature供update
[in, optional] ID3D12RootSignature *pRootSignature,
REFIID riid,
// 接口调用成功时,指向的Command Signature
[out, optional] void **ppvCommandSignature
);
如果Command Signature需要对Root Arguments做修改,就需要指定Root Signature。
typedef struct D3D12_COMMAND_SIGNATURE_DESC
{
UINT ByteStride; // drawing buffer中每个Command的字节长度
UINT NumArgumentDescs; // command signature中Argument数目
// arguments细节,指定每个Argument的的类型
// 不同类型的Argument具有不同的参数解释
// 参考下面的D3D12_INDIRECT_ARGUMENT_DESC
const D3D12_INDIRECT_ARGUMENT_DESC *pArgumentDescs;
// 多GPU模式下,指定需要应用此Signature的Node的Mask(每个Node代表一个GPU?)
UINT NodeMask;
} D3D12_COMMAND_SIGNATURE_DESC;
// Indirect Argument的属性描述
typedef struct D3D12_INDIRECT_ARGUMENT_DESC
{
D3D12_INDIRECT_ARGUMENT_TYPE Type;
union
{
struct
{
UINT Slot;
} VertexBuffer;
struct
{
UINT RootParameterIndex;
UINT DestOffsetIn32BitValues;
UINT Num32BitValuesToSet;
} Constant;
struct
{
UINT RootParameterIndex;
} ConstantBufferView;
struct
{
UINT RootParameterIndex;
} ShaderResourceView;
struct
{
UINT RootParameterIndex;
} UnorderedAccessView;
};
} D3D12_INDIRECT_ARGUMENT_DESC;
虽然Indirect Argument的Type有很多种,但是Command Signature只有两种,Graphics或者Compute,如果是后者,Command中必然有一个Dispatch指令。不同的Command Signature只会影响对应的Root Arguments,比如Graphics Command Signature只影响Graphics Root Arguments。
// Indirect Argument的类型
typedef enum D3D12_INDIRECT_ARGUMENT_TYPE {
D3D12_INDIRECT_ARGUMENT_TYPE_DRAW = 0,
D3D12_INDIRECT_ARGUMENT_TYPE_DRAW_INDEXED,
D3D12_INDIRECT_ARGUMENT_TYPE_DISPATCH,
D3D12_INDIRECT_ARGUMENT_TYPE_VERTEX_BUFFER_VIEW,
D3D12_INDIRECT_ARGUMENT_TYPE_INDEX_BUFFER_VIEW,
D3D12_INDIRECT_ARGUMENT_TYPE_CONSTANT,
D3D12_INDIRECT_ARGUMENT_TYPE_CONSTANT_BUFFER_VIEW,
D3D12_INDIRECT_ARGUMENT_TYPE_SHADER_RESOURCE_VIEW,
D3D12_INDIRECT_ARGUMENT_TYPE_UNORDERED_ACCESS_VIEW,
D3D12_INDIRECT_ARGUMENT_TYPE_DISPATCH_RAYS,
D3D12_INDIRECT_ARGUMENT_TYPE_DISPATCH_MESH
} ;
上面代码中,pArgumentDescs用来指定Command Signature中Commands所对应的Indirect Arugment的属性说明,需要注意的是pArgumentDescs中元素的数目与顺序需要与Indirect Argument Buffer中元素的数目顺序保持一致。
每个Draw/Dispatch指令所对应的Indirect Arguments在Indirect Argument Buffer中是紧密相连(tightly packed)的,不过对于不同的Draw/Dispatch Call,其在Indirect Argument Buffer中的byte stride是可以随意指定的。
针对不同的Indirect Argument,我们有不同的参数Layout:
typedef struct D3D12_DRAW_ARGUMENTS
{
UINT VertexCountPerInstance;
UINT InstanceCount;
UINT StartVertexLocation;
UINT StartInstanceLocation;
} D3D12_DRAW_ARGUMENTS;
typedef struct D3D12_DRAW_INDEXED_ARGUMENTS
{
UINT IndexCountPerInstance;
UINT InstanceCount;
UINT StartIndexLocation;
INT BaseVertexLocation;
UINT StartInstanceLocation;
} D3D12_DRAW_INDEXED_ARGUMENTS;
typedef struct D3D12_DISPATCH_ARGUMENTS
{
UINT ThreadGroupCountX;
UINT ThreadGroupCountY;
UINT ThreadGroupCountZ;
} D3D12_DISPATCH_ARGUMENTS;
typedef struct D3D12_VERTEX_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
UINT StrideInBytes;
} D3D12_VERTEX_BUFFER_VIEW;
typedef struct D3D12_INDEX_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
DXGI_FORMAT Format;
} D3D12_INDEX_BUFFER_VIEW;
typedef struct D3D12_CONSTANT_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
UINT Padding;
} D3D12_CONSTANT_BUFFER_VIEW;
2. 运行时,用Commands来对Command Buffer进行填充,填充方式方法不限
填充后的Command Buffer大概效果可以参考下图:
可以看到,每个Command包含两部分,分别是Command调用所需要的参数,如VertexCount、InstanceCount、StartVertexLocation & StartInstanceLocation,以及Command执行时所需要用的一些资源绑定关系,在图中通过Root Constant来表示。
为了有一个更为直观的理解,这里我们给几个例子:
2.1 简单数据
D3D12_INDIRECT_ARGUMENT_DESC Args[1];
Args[0].Type = D3D12_INDIRECT_PARAMETER_DRAW_INDEXED_INSTANCED;
D3D12_COMMAND_SIGNATURE_DESC ProgramDesc;
ProgramDesc.ByteStride = 36;
ProgramDesc.ArgumentCount = 1;
ProgramDesc.pArguments = Args;
这里准备调用的是DrawIndexedInstanced指令,对应的Indirect Argument Buffer中的Indirect Argument的长度是36个字节,数据在内存中的布局为:
2.2 Root Constants + Vertex Buffers
假设我们希望某个Command会修改两个Root Constants,同时还要更改Vertex Buffer的绑定,并且采用无Index的DrawCall,那么可以参考如下逻辑:
// 对于每个操作,都需要增加一个Argument来描述
D3D12_INDIRECT_ARGUMENT_DESC Args[4];
Args[0].Type = D3D12_INDIRECT_PARAMETER_CONSTANT;
Args[0].Constant.RootParameterIndex = 2;
Args[1].Type = D3D12_INDIRECT_PARAMETER_CONSTANT;
Args[1].Constant.RootParameterIndex = 6;
Args[2].Type = D3D12_INDIRECT_PARAMETER_VERTEX_BUFFER;
Args[2].VertexBuffer.VBSlot = 3;
Args[3].Type = D3D12_INDIRECT_PARAMETER_DRAW_INSTANCED;
D3D12_COMMAND_SIGNATURE ProgramDesc;
ProgramDesc.ByteStride = 40;
// 这里需要指定当前Command在Indirect Argument Buffer中的Length
ProgramDesc.ArgumentCount = 4;
ProgramDesc.pArguments = Args;
对应的Indirect Argument Buffer中Command对应的Argument的数据布局为:
3. 使用CommandList的接口完成State的设置(如RenderTarget绑定,PSO等)
4. 使用Command List的某个API触发GPU对Command Buffer中数据的翻译,这个翻译是在前面创建的Command Signature的指导下完成的
最终,我们通过Indirect接口完成对应的绘制或计算操作:
void ID3D12CommandList::ExecuteIndirect(
// 前面定义的Command Signature
ID3D12CommandSignature* pCommandSignature,
// 待执行的Command的数量
UINT MaxCommandCount,
// Command对应的Indirect Buffer
ID3D12Resource* pArgumentBuffer,
// 在Indirect Buffer中的偏移(即从哪个字节开始对应于第一个Command)
UINT64 ArgumentBufferOffset,
// Command实际执行次数
ID3D12Resource* pCountBuffer,
// Count偏移
UINT64 CountBufferOffset
);
如果pCountBuffer 非空,那么MaxCommandCount将用于指定待执行的操作的最大(重复?)次数,而实际上执行的次数由pCountBuffer中包含的32-bit无符号整数给出 (当然,需要考虑CountBufferOffset的偏移);
如果pCountBuffer为空,那么实际执行次数就由MaxCommandCount指定。
为方便理解这个接口的执行逻辑,下面用伪代码做一个大致说明:
// 先计算出当前Draw Call的执行次数
UINT CommandCount = pCountBuffer->ReadUINT32(CountBufferOffset);
CommandCount = min(CommandCount, MaxCommandCount)
// 再获取Command对应的Indirect Argument起始地址
BYTE* Arguments = pArgumentBuffer->GetBase() + ArgumentBufferOffset;
// 对Argument Buffer中的Command进行解释(包含了对应的执行操作)
for(UINT CommandIndex = 0; CommandIndex < CommandCount; CommandIndex++)
{
// Interpret the data contained in *Arguments
// according to the command signature
pCommandSignature->Interpret(Arguments);
Arguments += pCommandSignature ->GetByteStride();
}
NULL pCountBuffer:
// Get pointer to first Commanding argument
BYTE* Arguments = pArgumentBuffer->GetBase() + ArgumentBufferOffset;
for(UINT CommandIndex = 0; CommandIndex < MaxCommandCount;CommandIndex++)
{
// Interpret the data contained in *Arguments
// according to the command signature
pCommandSignature->Interpret(Arguments);
Arguments += pCommandSignature ->GetByteStride();
}
参考
[1]. DrawPrimitiveUP And DrawIndexedPrimitiveUP
[2]. Indirect Drawing
[3]. Indirect drawing and GPU culling
[4]. DirectX advanced learning video tutorials : Execute Indirect and Async GPU culling
[5]. 天刀手游中的GPU Driven流程和Draw Instanced Indirect
[6]. IDirect3DDevice9::DrawPrimitive method
[7]. IDirect3DDevice9::DrawPrimitiveUP method (d3d9.h)
[8]. IDirect3DDevice9::DrawIndexedPrimitive method (d3d9.h)
[9]. IDirect3DDevice9::DrawIndexedPrimitiveUP method (d3d9.h)
[10]. Direct3D 11.3 Functional Specification
[11]. DX11 InstanceDataStepRate
[12]. ID3D12GraphicsCommandList::Dispatch method
[13]. D3D12 Indirect Drawing