转载请注明出处为KlayGE游戏引擎,本文的永久链接为http://www.klayge.org/?p=2837

BUILD 2014上有个D3D12相关的讲座,向开发者预览了D3D12的API。比之前的简介详细了许多。这里我谈谈我对这个东西的理解。

Context

这里先对比一下D3D11和12的context,能对它们的API有个大致的印象。

D3D11的context

这张图表达了在D3D11中,context的组成。

D3D11 Context

其中,最左边的双向箭头表示状态可以Get和Set。橙色方块表示各个stage的资源slot。它们会bind上显存里的各种资源。

D3D12的context

D3D12里面,context变成了这样:

D3D12 Context

首先,各种状态被合并成了一个,叫pipeline state object(PSO)。这么一来,只需要调用一次API就能设置所有stage的状态。同时,Get接口被去掉了,就剩下Set。不过反正也从来不会去用Get。另外,slot也没了,变成了descriptor table(context右边绿色箭头)。table中的每一项都可以表示descriptor heap(context右上蓝色方块)中的一个区域。这样就能在不需要bind的情况下在shader中访问大量资源。这样做的好处在简介中有

可以放在PSO里面的状态有:

  • Shaders: VS/HS/TS/DS/GS/PS
  • Blend State
  • Rasterizer State
  • Depth/Stencil State
  • Input Layout
  • IA Primitive Topology Type
    • Triangle/Line/Point/Patch
  • RT/DS Properties
    • Format
    • Sample Counts

而这些状态不在PSO里:

  • Resource Bindings
  • Viewports
  • Scissor Rects
  • Blend Factor
  • Depth Test
  • Stencil Ref
  • IA PrimitiveTopology Bucket
    • List/Strip/ListAdj/StripAdj

Resource Hazard Resolution

在D3D10和D3D11中,每次设置shader resource view的时候,runtime都需要跟踪一遍所有的render target,如果一个资源同时被绑到输入和输出,就会把输入设置为空,同时通知驱动resolve hazard。但现在的程序中很多时候都需要在一个pass里render to texture,下一个pass里使用这个texture,这么一来就会不停地出现hazard。D3D12给了个让程序显式控制资源生命周期地方法,通过建立一系列的barrier,让程序告诉API需要什么时候同步,避免了CPU开销。

1
2
3
4
5
6
7
D3D12_RESOURCE_BARRIER_DESC Desc;
Desc.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
Desc.Transition.pResource   = pRTTexture;
Desc.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
Desc.Transition.StateBefore = D3D12_RESOURCE_USAGE_RENDER_TARGET;
Desc.Transition.StateAfter  = D3D12_RESOURCE_USAGE_PIXEL_SHADER_RESOURCE;
pContext->ResourceBarrier( 1, &Desc );

Bundles

程序里每一帧的渲染经常是发出同样的指令,90-95%是高度相关的。D3D12新增了bundle的功能,可以把一些渲染指令打成一个包,调用的时候只需要一次调用就能执行包中的所有指令。在不用bundle的时候,一段代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Setup
pContext->SetPipelineState(pPSO);
pContext->SetRenderTargetViewTable(0, 1, FALSE, 0);
pContext->SetVertexBufferTable(0, 1);
pContext->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// Draw 1
pContext->SetConstantBufferViewTable(D3D12_SHADER_STAGE_PIXEL, 0, 1);
pContext->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 0, 1);
pContext->DrawInstanced(6, 1, 0, 0);
pContext->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 1, 1);
pContext->DrawInstanced(6, 1, 6, 0);
// Draw 2
pContext->SetConstantBufferViewTable(D3D12_SHADER_STAGE_PIXEL, 1, 1);
pContext->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 0, 1);
pContext->DrawInstanced(6, 1, 0, 0);
pContext->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 1, 1);
pContext->DrawInstanced(6, 1, 6, 0);

有了bundle,就只要在建立阶段:

1
2
3
4
5
6
7
8
9
// Create bundle
pDevice->CreateCommandList(D3D12_COMMAND_LIST_TYPE_BUNDLE, pBundleAllocator, pPSO, pDescriptorHeap, &pBundle);
// Record commands
pBundle->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
pBundle->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 0, 1);
pBundle->DrawInstanced(6, 1, 0, 0);
pBundle->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 1, 1);
pBundle->DrawInstanced(6, 1, 6, 0);
pBundle->Close();

在使用阶段:

1
2
3
4
5
6
7
8
// Setup
pContext->SetRenderTargetViewTable(0, 1, FALSE, 0);
pContext->SetVertexBufferTable(0, 1);
// Draw 1 and 2
pContext->SetConstantBufferViewTable(D3D12_SHADER_STAGE_PIXEL, 0, 1);
pContext->ExecuteBundle(pBundle);
pContext->SetConstantBufferViewTable(D3D12_SHADER_STAGE_PIXEL, 1, 1);
pContext->ExecuteBundle(pBundle);

Bundle的建立可以在其他线程,内容由驱动和硬件管理。所以CPU开销比单独调用每个函数小得多了。前面的bundle还可以进一步组合成command list,并通过command queue提交给硬件执行。

未来

D3D12的API还没有最终完成,在不久的将来还会加入更多功能。其中比较有意思的是保守式光栅化和保顺序的pixel shader UAV。前者可以用于SVO的voxelization一步,以及一些碰撞检测的加速。目前的做法来自GPU Gems 2,通过在VS或GS里面通过拉拉伸和裁剪三角形来完成。如果能有硬件支持,性能和方便性都可以有改善。后者目前只有intel的卡有,可以在PS里面设置手动同步机制,可以做到可编程blending的效果。常用于OIT、deep shadow map、voxelization、手动AA等地方。这两个特性不只是API和驱动的任务,可能还需要新硬件才能完全支持。