【D3D12學習手記】The Command Queue and Command Lists


GPU有一個命令隊列,CPU通過Direct3D API將命令提交到隊列里來使用命令列表(command lists),如下圖。當一套命令(a set of commands)已經被提交到命令隊列,他們不會被GPU立刻執行,理解這一點非常重要。由於GPU很可能忙着處理之前插入的命令,所以它們會待在隊列里直到GPU准備好處理它們。

如果命令隊列空了,沒有任何工作可做,GPU就會處於空閑狀態;另一方面,如果命令隊列太滿,CPU在某個時刻必須停下來等着GPU追上來。這兩種情況都不是我們希望看到的;對於高性能要求的應用,比如游戲,目標是同時保持CPU和GPU的處於繁忙狀態以使得能夠充分利用硬件資源的優勢。

在Direct3D12中,命令隊列由接口ID3D12CommandQueue來表示。它是通過填充D3D12_COMMAND_QUEUE_DESC結構來描述隊列,然后調用ID3D12Device::CreateCommandQueue來創建的。在本書中,我們通過以下方式來創建我們的命令隊列:

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這個幫助宏(helper macro)的定義如下

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

其中__uuidof(**(ppType))求值為(**(ppType))的COM接口ID,在上面的代碼中是ID3D12CommandQueue。IID_PPV_ARGS_Helper函數實質上將ppType強制轉換為void **。 我們在本書中使用了這個宏,這是因為許多Direct3D 12 API調用都要求有一個參數,即我們正在創建的接口的COM ID,並使用void **類型。

 

這個接口中的一個主要函數是ExecuteCommandLists方法,它將命令列表(command lists)中的命令(commands)添加到到命令隊列(command queue):

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

命令列表(command lists)將會從ppCommandLists的第一個數組元素開始順序執行

正如上面的方法聲明所暗示的,圖形的命令列表(a command list for graphics)由ID3D12GraphicsCommandList接口表示,該接口繼承自ID3D12CommandList接口。 ID3D12GraphicsCommandList接口有許多方法可以將命令添加到命令列表中。 例如,以下代碼添加了設置視口(set the viewport),清除渲染目標視圖(clear the render target view)和發出繪制調用(issue a draw call)的命令:

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

這些方法的名稱暗示命令是立即執行的,但實際並不是。上面的代碼只是將命令添加到命令列表中。 ExecuteCommandLists方法將命令添加到命令隊列,GPU處理來自隊列的命令。 在我們閱讀本書的過程中,我們將了解ID3D12GraphicsCommandList支持的各種命令。 當我們完成向命令列表添加命令時,我們必須通過調用ID3D12GraphicsCommandList :: Close方法來表明我們已完成命令錄制(finished recording commands)。

// Done recording commands.
mCommandList->Close();

命令列表在被傳遞給ID3D12CommandQueue :: ExecuteCommandLists之前,必須先被關閉。

與命令列表相關聯的是一個名為ID3D12CommandAllocator的內存支持類。 當命令被記錄到命令列表中時,它們實際上將存儲在相關的命令分配器(command allocator)中。 當通過ID3D12CommandQueue :: ExecuteCommandLists執行命令列表時,命令隊列將引用分配器中的命令(commands)。 從ID3D12Device創建命令分配器的代碼如下:

HRESULT ID3D12Device::CreateCommandAllocator( 
  D3D12_COMMAND_LIST_TYPE type,
  REFIID riid,
  void **ppCommandAllocator);

參數解釋如下

1.type:可與此分配器關聯的命令列表類型。我們在本書中使用的兩種常見類型是:
  第一種:D3D12_COMMAND_LIST_TYPE_DIRECT:存儲會被GPU直接執行的命令列表(到目前為止我們已經描述的命令列表的類型)。
  第二種:D3D12_COMMAND_LIST_TYPE_BUNDLE:指定命令列表的捆綁包。構建(building)命令列表時會產生一些CPU開銷,因此Direct3D 12提供了一種優化,允許我們將一系列命令記錄到所謂的bundle中。記錄捆綁后,驅動程序將預處理命令以優化其在渲染過程中的執行。因此,應在初始化時記錄捆綁。如果分析顯示構建特定命令列表需要花費大量時間,則應將bundle的使用視為必要的優化。 Direct3D 12繪圖API已經非常高效,因此您不需要經常使用捆綁包,只有在您可以通過它們取得立竿見影的性能時才應該使用它們;也就是說,默認情況下不要使用它們。我們在本書中不使用bundle;有關更多詳細信息,請參閱DirectX 12文檔。

2.riid:我們要創建的ID3D12CommandAllocator接口的COM ID。

 

3.ppCommandAllocator:輸出的指向被創建的命令分配器的指針。

 

命令列表也是用ID3D12Device中的方法來創建的

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

參數解釋如下

1.nodeMask:單GPU系統設置為0。 否則,nodeMask 標識與該命令列表相關聯的物理GPU。 在本書中,我們假設單GPU系統。

2.命令列表的類型:_COMMAND_LIST_TYPE_DIRECT或D3D12_COMMAND_LIST_TYPE_BUNDLE。

3.pCommandAllocator:與創建的命令列表關聯的分配器。 命令分配器類型必須與命令列表類型匹配。

4.pInitialState:指定命令列表的初始管道狀態(pipeline state)。 對於bundle而言,這可以為null,並且在特殊情況下,執行命令列表以進行初始化並且不包含任何繪制命令。 我們將在第6章討論ID3D12PipelineState。

5.riid:我們想要創建的ID3D12CommandList接口的COM ID。

6.ppCommandList:輸出指向被創建的命令列表的指針。

你可以創建多個命令列表,並關聯到同一個分配器上,但不能同時為這些命令列表錄制命令。 也就是說,除了我們將要錄制命令的列表之外,其他命令列表必須被關閉。 因此,來自給定命令列表的所有命令將連續地添加到分配器。 請注意,創建或重置命令列表時,它處於“打開”(open)狀態。 因此,如果我們嘗試使用相同的分配器在一行(a row)中創建兩個命令列表,我們將收到錯誤:

D3D12 ERROR: ID3D12CommandList::{Create,Reset}CommandList: The command allocator is currently in-use by another command list.

在我們調用了ID3D12CommandQueue :: ExecuteCommandList(C)之后,通過調用ID3D12CommandList :: Reset方法重用C的內部存儲器來記錄一組新命令是安全的。 此方法的參數與ID3D12Device :: CreateCommandList中的相應參數相同:

HRESULT ID3D12CommandList::Reset( 
  ID3D12CommandAllocator *pAllocator,
  ID3D12PipelineState *pInitialState);

此方法將命令列表設置為和剛剛創建時相同的狀態,但允許我們重用內部內存並避免取消分配舊命令列表並分配新命令列表。 請注意,重置命令列表不會影響命令隊列中的命令,因為關聯的命令分配器仍然具有命令隊列引用的內存中的命令。

在我們將完整幀的渲染命令提交給GPU之后,我們希望重用命令分配器中的內存來錄制下一幀的渲染命令。 ID3D12CommandAllocator :: Reset方法可用於此目的:

HRESULT ID3D12CommandAllocator::Reset(void);

這個想法類似於調用std :: vector :: clear,它將向量的大小調整為零,但保持當前容量相同。 但是,因為命令隊列可能在分配器中引用數據,所以在我們確定GPU已完成執行分配器中的所有命令之前,不得重置命令分配器。 如何執行此操作將在下一節中介紹。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM