4.2 CPU / GPU交互


我們必須明白,在圖形編程中,我們有兩個處理器:CPU和GPU。 它們並行工作,有時需要同步。 爲了獲得最佳性能,目標是儘可能長時間保持忙碌狀態並儘量減少同步。 同步是不可取的,因爲這意味着一個處理單元在等待另一個處理單元完成一些工作時處於空閒狀態; 換句話說,它破壞了並行性。

關於同步與異步本博主其他博客也有描述,地址:https://blog.csdn.net/yaotuzhi/article/details/80144898

4.2.1命令隊列和命令列表
GPU有一個命令隊列。 CPU通過命令列表通過Direct3D API向隊列提交命令(參見圖4.6)。 理解一旦一組命令已經被提交給命令隊列很重要,它們不會立即由GPU執行。 他們坐在隊列中,直到GPU準備好處理它們,因爲GPU很可能忙於處理先前插入的命令。


圖4.6。 命令隊列。

如果命令隊列變空,GPU將閒置,因爲它沒有任何工作要做; 另一方面,如果命令隊列變得太滿,那麼GPU捕捉[Crawfis12],CPU必須閒置。 這兩種情況都是不可取的。 對於像遊戲這樣的高性能應用,目標是保持CPU和GPU的繁忙,充分利用可用的硬件資源。
在Direct3D 12中,命令隊列由ID3D12CommandQueue接口表示。 它是通過填充描述隊列的D3D12_COMMAND_QUEUE_DESC結構,然後調用ID3D12Device :: CreateCommandQueue創建的。 我們在本書中創建命令隊列的方式如下所示:

 Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;//定義ID3D12CommandQueue的地址

D3D12_COMMAND_QUEUE_DESC queueDesc = {};//定義CommandQueue的描述符及其一些參數
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)

其中__uuof(**(ppType))計算爲(**(ppType))的COM接口ID,在上面就是COMJ接口:ID3D12CommandQueue。 IID_PPV_ARGS_Helper函數基本上將ppType轉換爲void **。 在本書中,我們使用這個宏,因爲許多Direct3D 12 API調用都有一個參數,它需要我們創建的接口的COM ID,並取一個void **。這接口的主要方法之一是ExecuteCommandLists方法,它將命令列表中的命令添加到隊列中:

void ID3D12CommandQueue::ExecuteCommandLists(
// 數組中列出的命令數
UINT Count,
//指向命令列表數組中第一個元素的指針

ID3D12CommandList *const *ppCommandLists);

命令列表按照從第一個數組元素開始的順序執行。

正如上面的方法聲明所提示的,圖形的命令列表由從ID3D12CommandList接口繼承的ID3D12GraphicsCommandList接口表示。 ID3D12GraphicsCommandList接口提供了多種向命令列表添加命令的方法。 例如,下面的代碼添加了設置窗口,清除渲染目標視圖和發出繪製調用的命令:

// 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方法來指示我們已完成記錄命令:

mCommandList->Close();//記錄命令

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

與命令列表關聯的是稱爲ID3D12CommandAllocator的內存支持類。 當命令被記錄到命令列表中時,它們實際上將被存儲在關聯的命令分配器中。 當通過ID3D12CommandQueue :: ExecuteCommandLists執行命令列表時,命令隊列將引用分配器中的命令。 命令分配器是從ID3D12Device創建的:

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

1. type:可以與此分配器關聯的命令列表的類型。我們在本書中使用的兩種常見類型是:
1. D3D12_COMMAND_LIST_TYPE_DIRECT:存儲GPU直接執行的命令列表(我們迄今爲止描述的命令列表類型)。

2. D3D12_COMMAND_LIST_TYPE_BUNDLE:指定命令列表表示一個包。在構建命令列表中有一些CPU開銷,所以Direct3D 12提供了一種優化,使我們能夠將一系列命令記錄到所謂的捆綁包中。在記錄包之後,驅動程序將預處理這些命令以在渲染過程中優化它們的執行。因此,應該在初始化時記錄捆綁包。如果分析顯示構建特定命令列表需要花費大量時間,則應該將捆綁的使用視爲優化。 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。 否則,節點掩碼將標識與該命令列表關聯的物理GPU。 在本書中,我們假設單GPU系統。
2.鍵入:命令列表的類型:_COMMAND_LIST_TYPE_DIRECT或D3D12_COMMAND_LIST_TYPE_BUNDLE。
3. pCommandAllocator:與創建的命令列表關聯的分配器。 命令分配器類型必須與命令列表類型匹配。
4. pInitialState:指定命令列表的初始管道狀態。 這對bundle來說可以爲null,在特殊情況下,爲了初始化而執行命令列表並且不包含任何繪圖命令。 我們在第6章討論ID3D12PipelineState。
5. riid:我們要創建的ID3D12CommandList接口的COM ID。

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

您可以使用ID3D12Device :: GetNodeCount方法來查詢系統上GPU適配器節點的數量。

您可以創建與同一分配器關聯的多個命令列表,但不能同時進行記錄。 也就是說,除了我們要記錄的命令之外,所有命令列表都必須關閉。 因此,來自給定命令列表的所有命令將被連續添加到分配器中。 請注意,創建或重置命令列表時,它處於“打開”狀態。 所以如果我們試圖用同一個分配器在一行中創建兩個命令列表,我們會得到一個錯誤:

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,它將vector重新調整爲零,但保持當前容量不變。 但是,由於命令隊列可能引用了分配器中的數據,因此只有在確定GPU已完成執行分配器中的所有命令後,才能重置命令分配器; 下一節將介紹如何做到這一點。

4.2.2 CPU / GPU同步
由於有兩個處理器並行運行,會出現許多同步問題。
假設我們有一些資源R存儲我們想繪製的幾何幾何的位置。 此外,假定CPU更新R的數據以存儲位置p1,然後將用於引用R的繪圖命令C添加到命令隊列中,以便在位置p1繪製幾何圖形。 將命令添加到命令隊列不會阻塞CPU,因此CPU將繼續運行。 在GPU執行繪圖之前,CPU繼續並覆蓋R的數據以存儲新的位置p2是錯誤的

命令C(見圖4.7)。


圖4.7。 這是一個錯誤,因爲C使用p2繪製幾何圖形,或者在R正在更新中間時繪製幾何圖形。 無論如何,這不是預期的行爲。

解決這種情況的一個辦法是強制CPU等待,直到GPU完成處理隊列中所有的命令直到指定的圍欄點。 我們稱之爲刷新命令隊列。 我們可以使用圍欄來做到這一點。 柵欄由ID3D12Fence接口表示,用於同步GPU和CPU。 可以使用以下方法創建柵欄對象:

HRESULT ID3D12Device::CreateFence(
UINT64 InitialValue,
D3D12_FENCE_FLAGS Flags,
REFIID riid,
void **ppFence);
// Example
ThrowIfFailed(md3dDevice->CreateFence(
0,
D3D12_FENCE_FLAG_NONE,

IID_PPV_ARGS(&mFence)));

一個fence對象維護一個UINT64值,這個值只是一個整數,用於標識一個圍欄時間點。 我們從零開始,每次我們需要標記一個新的圍欄點時,我們只是遞增整數。 現在,下面的代碼/註釋顯示了我們如何使用fence來刷新命令隊列。

UINT64 mCurrentFence = 0;
void D3DApp::FlushCommandQueue()
{
// 提高圍欄值以標記直至此圍欄點的命令。
mCurrentFence++;
//向命令隊列添加一條指令來設置一個新的圍欄點。
//因爲我們在GPU時間軸上,所以新的柵欄點不會
//直到GPU完成處理此Signal()前的所有命令爲止。
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(),mCurrentFence));
//等到GPU完成到這個圍欄點的命令。
if(mFence->GetCompletedValue() < mCurrentFence)
{
HANDLE eventHandle = CreateEventEx(nullptr, false,
false, EVENT_ALL_ACCESS);
//當GPU擊中當前柵欄時觸發事件。
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
//等待GPU擊中當前的圍欄事件。
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
}

圖4.8用圖形解釋了這個代碼。


圖4.8。 在這個快照中,GPU已經處理了高達xgpu的命令並且CPU剛纔調用了ID3D12CommandQueue :: Signal(fence,n + 1)方法。 這基本上是在隊列末尾添加一條指令,以將fence值更改爲n + 1.但是,mFence-> GetCompletedValue()將繼續返回n,直到GPU處理隊列中添加的所有命令 信號(fence,n + 1)指令。

所以在前面的例子中,在CPU發出繪圖命令C之後,它將在覆蓋R的數據以存儲新的位置p2之前刷新命令隊列。 這個解決方案並不理想,因爲這意味着CPU在等待GPU完成時處於空閒狀態,但它提供了一個簡單的解決方案,我們將在第7章之前使用它。您可以在幾乎任何點上刷新命令隊列(不一定每次只需刷新一次 幀); 如果您有一些初始化GPU命令,例如,可以在進入主渲染循環之前刷新命令隊列以執行初始化。

  請注意,刷新命令隊列也可以用來解決我們在上一節末尾提到的問題; 也就是說,我們可以刷新命令隊列以確保在重置命令分配器之前所有的GPU命令都已經被執行。

4.2.3資源轉換

爲了實現常見的渲染效果,GPU通常在一步中寫入資源R,然後在後面的步驟中從資源R中讀取。但是,從資源讀取資源會造成資源危險。 GPU尚未完成寫入或未開始寫入。爲了解決這個問題,Direct3D將一個狀態關聯到資源。資源在創建時處於默認狀態,並由應用程序告知Direct3D任何狀態轉換。這使GPU可以做任何需要做的工作來完成轉換並防止資源危害。例如,如果我們正在寫入資源,比如說紋理,我們會將紋理狀態設置爲渲染目標狀態;當我們需要讀取紋理時,我們會將其狀態更改爲着色器資源狀態。通過向Direct3D通知轉換,GPU可以採取措施避免危害,例如,在從資源讀取之前等待所有寫入操作完成。出於性能原因,資源轉移的負擔落在應用程序開發人員身上。應用程序開發人員知道這些轉換何時發生。自動轉換跟蹤系統會帶來額外的開銷。

通過在命令列表上設置轉換資源障礙數組來指定資源轉換; 它是一個數組,以防您想通過一個API調用轉換多個資源。 在代碼中,資源障礙由表示

D3D12_RESOURCE_BARRIER_DESC結構。 以下幫助函數(在d3dx12.h中定義)爲給定資源返回轉換資源障礙描述,並指定之前和之後的狀態:

struct CD3DX12_RESOURCE_BARRIER : public
D3D12_RESOURCE_BARRIER
{
// […] convenience methods
static inline CD3DX12_RESOURCE_BARRIER Transition(
_In_ ID3D12Resource* pResource,
D3D12_RESOURCE_STATES stateBefore,
D3D12_RESOURCE_STATES stateAfter,
UINT subresource =
D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
D3D12_RESOURCE_BARRIER_FLAGS flags =
D3D12_RESOURCE_BARRIER_FLAG_NONE)
{
CD3DX12_RESOURCE_BARRIER result;
ZeroMemory(&result, sizeof(result));
D3D12_RESOURCE_BARRIER &barrier = result;
result.Type =
D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
result.Flags = flags;
barrier.Transition.pResource = pResource;
barrier.Transition.StateBefore = stateBefore;
barrier.Transition.StateAfter = stateAfter;
barrier.Transition.Subresource = subresource;
return result;
}
// […] more convenience methods

};

注意到CD3DX12_RESOURCE_BARRIER擴展了D3D12_RESOURCE_BARRIER_DESC並添加了便利方法。 大多數Direct3D 12結構都有助手的變體,我們更喜歡這些變體以方便使用。 CD3DX12的變體全部在d3dx12.h中定義。 該文件不是核心DirectX 12 SDK的一部分,但可以從Microsoft下載。 爲方便起見,本書源代碼的Common目錄中包含一個副本。

本章示例應用程序中的一個函數示例如下:

mCommandList->ResourceBarrier(1,&CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),

D3D12_RESOURCE_STATE_PRESENT,D3D12_RESOURCE_STATE_RENDER_TARGET));

此代碼將表示我們在屏幕上顯示的圖像的紋理從呈現狀態轉換爲呈現目標狀態。 注意資源障礙已被添加到命令列表中。 您可以將資源障礙轉換看作是指示GPU資源狀態正在轉換的命令,以便在執行後續命令時可以採取必要的步驟來防止資源危險。
除轉換類型外,還有其他類型的資源障礙。 目前,我們只需要轉換類型。 我們將在需要時介紹其他類型。

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