【DX12龍書】第四五六章筆記

COM

我們可以把Component Object Model (COM)當作一個接口,或是當作一個C++類使用。COM是會記錄被引用的數量的,當我們不再需要一個COM物體的時候,我們需要釋放它,因爲所有COM接口都繼承自IUnknown COM,而後者提供了釋放函數。我們需要釋放它而不是刪除它,COM物體將會再它們的引用數量到0的時候自動釋放。

爲了幫助管理COM物體的生命週期, the Windows Runtime Library (WRL) provides the Microsoft::WRL::ComPtr class (#include <wrl.h>),可以被認爲是COM物體的智能指針。當ComPtr實例離開了域的時候,將會自動爲COM物體調用Release方法。這本書中將使用三個主要的ComPtr方法:

1.Get:返回COM接口的指針,一般是要把COM接口當成參數傳遞給函數時用到

ComPtr<ID3D12RootSignature> mRootSignature;
…
// SetGraphicsRootSignature expects
ID3D12RootSignature* argument.
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());


2. GetAddressOf:返回COM接口的指針的地址。通常用於讓一個函數返回COM接口指針。

 

ComPtr<ID3D12CommandAllocator>
mDirectCmdListAlloc;
…ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
mDirectCmdListAlloc.GetAddressOf()));


3. Reset: 把ComPtr實例設置爲空指針,然後給對應的COM接口的引用數量減少1.

COM接口通常用前綴大寫I修飾,比如一個指令集叫做ID3D12GraphicsCommandList

Descriptor

在渲染的過程中,GPU會從資源中讀取(比如儲存了幾何體位置信息的緩存),或是寫入(比如back , depth , stencil 這三種buffer。但在我們發佈繪製命令之前,我們需要將資源綁定至渲染管線,然後它們才能被Draw call引用。有些資源每次draw call都會改變,所以我們每個draw call都應該更新綁定。然而不能直接綁定。因此使用一個descriptor 物體來引用這些資源,它可以被認爲是一個像GPU描述資源的輕量級結構。我們在draw call中指定需要引用的descriptor,也就相當於把資源綁定到了渲染管線。

爲什麼要用這麼間接的方法呢?因爲GPU資源本質上一個內存塊。資源保持不變所以它們可以被用於渲染管線的不同階段,比如一個render target,然後用作shader resource(先被採樣作後作爲着色器的紋理)。但是有時候我們只想這個資源的一部分,以及有時候資源創建時並未指定類型,這樣我們直接使用資源就很困難了。

因此就有了descriptor了。除了定義資源數據之外,Descriptor還向GPU描述了這個資源,告訴D3D這個資源將會被如果使用,也就是綁定到渲染管線的哪個階段,這樣我們就能只只使用這個資源的一部分了。如果資源創建時並未指定類型,那麼我們創建descriptor時必須指定類型。

view和descriptor是同義詞,比如常數緩存view 和常數緩存descriptor是同一個東西。Descritor也有自己的類型,包括:
1. CBV/SRV/UAV descriptors describe constant buffers, shader resources and
unordered access view resources.
2. Sampler descriptors describe sampler resources (used in texturing).
3. RTV descriptors describe render target resources.
4. DSV descriptors describe depth/stencil resources.

而Descriptor heap是一組單一類型的descriptors。我們也可以讓多個descriptor只引用同樣的資源。例如讓它們引用資源的不同子區域。之前提到,資源可以被綁定到渲染管線的不同區域,這樣每個區域也都需要一個descritpor。descriptor應該在初始化時間就創建了,因爲要運行類型檢查。

指令集和指令隊列

GPU有一個指令隊列。CPU使用指令集把指令通過D3D API提交到指令隊列上。要注意一組指令被提交到指令隊列上後不會被立即執行,直到GPU準備好。當指令隊列爲空,則GPU除了等待什麼也幹不了。如果指令隊列太滿,CPU也只能慢慢等待。這是不好的情況。


我們需要先填充D3D12_COMMAND_QUEUE_DESC結構體來描述指令隊列,然後用 ID3D12Device::CreateCommandQueue來創建指令隊列。
In Direct3D 12, the command queue is represented by the ID3D12CommandQueue
interface. It is created by filling out a D3D12_COMMAND_QUEUE_DESC structure
describing the queue and then calling ID3D12Device::CreateCommandQueue.
The way we create our command queue in this book is as follows:

Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));

IID_PPV_ARGS宏定義爲

#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)),
IID_PPV_ARGS_Helper(ppType)

__uuidof(**(ppType))計算了 (**(ppType))類型的COM接口的ID。而在上面的代碼中,IID_PPV_ARGS_Helper函數把ppType映射爲void**。我們這樣做的原因是DX12的很多API都需要一個接口的COM ID。

另一個 ExecuteCommandLists用於給將要進入指令隊列的指令集添加指令。指令集將按順序從第一個開始執行。我們這裏使用ID3D12GraphicsCommandList來表示一個指令集,它繼承自ID3D12CommandList 接口。

void ID3D12CommandQueue::ExecuteCommandLists(
// Number of commands lists in the arrayUINT Count,
// Pointer to the first element in an array ofcommand lists
ID3D12CommandList *const *ppCommandLists);

The ID3D12GraphicsCommandList有很多添加命令的方法。比如下面這些代碼將設置view port,清除render target view,並且發佈draw call。

// mCommandList pointer to ID3D12CommandList
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->ClearRenderTargetView(mBackBufferView,
Colors::LightSteelBlue, 0, nullptr);
mCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);
// Done recording commands.
mCommandList->Close();

當我們放完了指令後,需要調用 ID3D12GraphicsCommandList::Close方法來告訴程序我們已經完成了。與指令集有關的是一個memory backing 類叫做ID3D12CommandAllocator,當指令被指令集記錄後,這些指令將被存儲在相關聯的指令allocator裏。A command allocator is created from the ID3D12Device:

HRESULT ID3D12Device::CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE type,
REFIID riid,
void **ppCommandAllocator);
  1. type:可以與這個指令allocator相關聯的指令集。這本書裏將包括兩個,一個是 D3D12_COMMAND_LIST_TYPE_DIRECT,包含了可以被GPU直接執行的指令。D3D12_COMMAND_LIST_TYPE_BUNDLE,將指令集指定爲bundle。Specifies the command list
    represents a bundle. There is some CPU overhead in building a command list, so Direct3D 12 provides an optimization that allows us to record a sequence of commands into a so-called bundle. After a bundle has been recorded, the driver will preprocess the commands to optimize their execution during rendering. Therefore, bundles should be recorded at initialization time. The use of bundles should be thought of as an optimization to use if profiling shows building particular command lists are taking significant time. The Direct3D 12 drawing API is already very efficient, so you should not need to use bundles often, and you should only use them if you can demonstrate a performance gain by them; that is to say, do not use them by default. We do not use bundles in this book; see the DirectX 12 documentation for further details.
  2. riid:ID3D12CommandAllocator 想要創建的COM ID。
  3. ppCommandAllocator:將要創建的指令allocator的指針。

Command lists are also created from the ID3D12Device:
 

HRESULT ID3D12Device::CreateCommandList(UINT nodeMask,
D3D12_COMMAND_LIST_TYPE type,
ID3D12CommandAllocator *pCommandAllocator,
ID3D12PipelineState *pInitialState,
REFIID riid,
void **ppCommandList);

1. nodeMask: 單個GPU則設置爲0。多個GPU則需要設置爲與這個指令集相關聯的物理GPU。
2. type: 指令集的類型
3. pCommandAllocator: 之前創建的指令allocator,類型必須匹配。
4. pInitialState: 指令集的初始管線狀態。
5. riid: ID3D12CommandList 接口想要創建的COM ID.
6. ppCommandList: 將要創建的指令集的指針.

渲染管線概述

Input Assemebler:讀取幾何體的頂點和索引數據,並把它們裝配成geometric primitives (e.g., triangles, lines)。

Tessellation :將三角形劃分爲更多的三角形。優點包括我們可以很方便地設置LOD,近處的多細分一點,遠處的少細分一點。以及我們只需要在low poly上計算動畫和物理動畫,但實際渲染用的是細分後的。這些操作將在GPU中完成。

光柵化階段

使用D3D12_RASTERIZER_DESC來表達一個光柵化狀態組,用於配置渲染管線的光柵化階段。

typedef struct D3D12_RASTERIZER_DESC {
D3D12_FILL_MODE FillMode; // Default:
D3D12_FILL_SOLID
D3D12_CULL_MODE CullMode; // Default:
D3D12_CULL_BACK
BOOL FrontCounterClockwise; // Default: false
INT DepthBias; // Default: 0
FLOAT DepthBiasClamp; // Default: 0.0fFLOAT SlopeScaledDepthBias; // Default: 0.0f
BOOL DepthClipEnable; // Default: true
BOOL ScissorEnable; // Default: false
BOOL MultisampleEnable; // Default: false
BOOL AntialiasedLineEnable; // Default: false
UINT ForcedSampleCount; // Default: 0
// Default:
D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF
D3D12_CONSERVATIVE_RASTERIZATION_MODE
ConservativeRaster;
} D3D12_RASTERIZER_DESC;
  1. FillMode:D3D12_FILL_WIREFRAME用於線框,D3D12_FILL_SOLID用於固體
  2. CullMode:D3D12_CULL_NONE來禁用culling。D3D12_CULL_BACK 來cull掉後向三角形。D3D12_CULL_FRONT類似。
  3. FrontCounterClockwise:前向三角形的索引順序是否應該爲true?
  4. ScissorEnable

CD3DX12_RASTERIZER_DESC則是D3D12_RASTERIZER_DESC的升級版。In particular, it has a constructor that takes an object of type CD3D12_DEFAULT, which is just a dummy type used for overloading to indicate the rasterizer state members should be initialized to the default values. CD3D12_DEFAULT and D3D12_DEFAULT are defined like so:

struct CD3D12_DEFAULT {};
extern const DECLSPEC_SELECTANY CD3D12_DEFAULT
D3D12_DEFAULT;

D3D12_DEFAULT is used in several of the Direct3D convenience classes.

管線狀態物體

我們已經知道了如何創建像素和頂點着色器,如何創建input layout,但要它們弄到一起,我們還需要一個pipeline state object (PSO)
如下:

typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC
{ ID3D12RootSignature *pRootSignature;
D3D12_SHADER_BYTECODE VS;
D3D12_SHADER_BYTECODE PS;
D3D12_SHADER_BYTECODE DS;
D3D12_SHADER_BYTECODE HS;
D3D12_SHADER_BYTECODE GS;
D3D12_STREAM_OUTPUT_DESC StreamOutput;
D3D12_BLEND_DESC BlendState;
UINT SampleMask;
D3D12_RASTERIZER_DESC RasterizerState;
D3D12_DEPTH_STENCIL_DESC DepthStencilState;
D3D12_INPUT_LAYOUT_DESC InputLayout;
D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType;
UINT NumRenderTargets;
DXGI_FORMAT RTVFormats[8];
DXGI_FORMAT DSVFormat;
DXGI_SAMPLE_DESC SampleDesc;
} D3D12_GRAPHICS_PIPELINE_STATE_DESC;
  1. pRootSignature:綁定至PSO的根簽名的指針。必須與着色器兼容
  2. VS:要綁定的頂點着色器。使用D3D12_SHADER_BYTECODE來描述.typedef struct D3D12_SHADER_BYTECODE {
    const BYTE *pShaderBytecode;
    SIZE_T BytecodeLength;
    } D3D12_SHADER_BYTECODE;
  3. PS: The pixel shader to bind.
  4. DS: The domain shader to bind (we will discuss this type of shader in a later chapter).
  5. HS: The hull shader to bind (we will discuss this type of shader in a later chapter).
  6. GS: The geometry shader to bind (we will discuss this type of shader in a later
  7. StreamOutput: Used for an advanced technique called stream-out. We just zeroout this field for now.
  8. BlendState: Specifies the blend state which configures blending. We will discuss this state group in a later chapter; for now, specify the default CD3DX12_BLEND_DESC(D3D12_DEFAULT)
  9. SampleMask:多采樣最多能使用32個採樣,而Mask類似於開關控制將會使用那種採樣。通常使用0xffffffff也就是什麼都不用。
  10. DepthStencilState: Specifies the depth/stencil state which configures the depth/stencil test. We will discuss this state group in a later chapter; for now, specify the default CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT).
  11. InputLayout: An input layout description which is simply an array of D3D12_INPUT_ELEMENT_DESC elements, and the number of elements in the array.
    typedef struct D3D12_INPUT_LAYOUT_DESC
    {
    const D3D12_INPUT_ELEMENT_DESC
    *pInputElementDescs;
    UINT NumElements;
    } D3D12_INPUT_LAYOUT_DESC;
  12. PrimitiveTopologyType: Specifies the primitive topology type.
    typedef enum D3D12_PRIMITIVE_TOPOLOGY_TYPE {
    D3D12_PRIMITIVE_TOPOLOGY_TYPE_UNDEFINED = 0,
    D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT = 1,
    D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE = 2,
    D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE = 3,
    D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH = 4
    } D3D12_PRIMITIVE_TOPOLOGY_TYPE;
  13. NumRenderTargets: The number of render targets we are using simultaneously.
  14. RTVFormats: The render target formats. This is an array to support writing to multiple render targets simultaneously. This should match the settings of the render target we are using the PSO with.
  15. DSVFormat: The format of the depth/stencil buffer. This should match the settings of the depth/stencil buffer we are using the PSO with.
  16. SampleDesc: Describes the multisample count and quality level. This should match the settings of the render target we are using.

當我們填完D3D12_GRAPHICS_PIPELINE_STATE_DESC實例後,就可以使用ID3D12Device::CreateGraphicsPipelineState來創建渲染管線了。

ComPtr<ID3D12RootSignature> mRootSignature;
std::vector<D3D12_INPUT_ELEMENT_DESC> mInputLayout;
ComPtr<ID3DBlob> mvsByteCode;
ComPtr<ID3DBlob> mpsByteCode;
…
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
ZeroMemory(&psoDesc,sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
psoDesc.InputLayout = { mInputLayout.data(),(UINT)mInputLayout.size() };
psoDesc.pRootSignature = mRootSignature.Get();
psoDesc.VS =
{
reinterpret_cast<BYTE*>(mvsByteCode-
>GetBufferPointer()),
mvsByteCode->GetBufferSize()
};
psoDesc.PS ={
reinterpret_cast<BYTE*>(mpsByteCode- >GetBufferPointer()),mpsByteCode->GetBufferSize()
};
psoDesc.RasterizerState = CD3D12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.BlendState = CD3D12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3D12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = mBackBufferFormat;
psoDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
psoDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
psoDesc.DSVFormat = mDepthStencilFormat;
ComPtr<ID3D12PipelineState> mPSO;
md3dDevice->CreateGraphicsPipelineState(&psoDesc,IID_PPV_ARGS(&mPSO)));

Because PSO validation and creation can be time consuming, PSOs should be generated at initialization time. One exception to this might be to create a PSO at runtime on demand the first time it is referenced; then store it in a collection such as a hash table so it can quickly be fetched for future use.

PSO中的狀態相互關聯。爲了防止頻繁改變狀態所帶來的性能下降,DX12採用了延遲編程。直到發佈draw call命令時才能直到PSO最終長什麼樣。

並不是所有的渲染狀態都在PSO中,比如Viewport和scissor retangle就與PSO互相獨立。

D3D本質上是個狀態機,如果不改變的話那麼就會一直停留在某個狀態。如果你需要用一個PSO繪製一些物體,而用另一個PSO繪製另一個物體,那麼代碼就看起來像下面這樣。

// Reset specifies initial PSO.
mCommandList->Reset(mDirectCmdListAlloc.Get(),
mPSO1.Get())
/* …draw objects using PSO 1… */
// Change PSO
mCommandList->SetPipelineState(mPSO2.Get());
/* …draw objects using PSO 2… */
// Change PSO
mCommandList->SetPipelineState(mPSO3.Get());
/* …draw objects using PSO 3… */

當PSO綁定至指令集後,只有重寫PSO或重設指令集後才能改變PSO。但儘量不要改變PSO,在同一時間繪製使用用一個PSO的物體。而且不要每次Draw caldl都改變一些PSO

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章