Modern Drawcall API

突然发现,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的概念,基于这个对象,在应用层可以完成三个参数的设置:

  1. indirect argument buffer的格式(通过D3D12_INDIRECT_ARGUMENT_DESC指定)
  2. indirect Draw Call类型,这里一共有三种DrawInstanced, DrawIndexedInstanced, Dispatch
  3. 指定对应的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

你可能感兴趣的:(Modern Drawcall API)