美文网首页
Modern Drawcall API

Modern Drawcall API

作者: 离原春草 | 来源:发表于2022-12-05 00:15 被阅读0次

    突然发现,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<int>(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

          本文链接:https://www.haomeiwen.com/subject/ycarfdtx.html