DirectX 12 學習之路(四)

一、幀資源(frame resource) 一個由每幀都需CPU來修改的資源所構成的環形數組。
這種方法令CPU無需等待GPU結束當前的任務,即可繼續處理下一幀的相關工作;對此,CPU只需要處理下一幀可用的(即GPU沒有使用中的)幀資源。
如果CPU處理幀的速度總是快於GPU,則CPU必須在某個時刻等待GPU追趕上來,但此情景又是我們所期盼的:不僅GPU的處理能力將得到充分地發揮,
同時,多出來的CPU資源又總是可被遊戲的其他部分,如AI,物理模擬與遊戲邏輯所利用。

二、我們可以利用ID3D12DescriptorHeap::GetCPUDescriptorHandleForHeapStart方法來獲取堆中第一個描述符的句柄,
通過ID3D12Device::GetDescriptorHandleIncrementSize方法得到描述符的大小(依賴於硬件與描述符的類型)。一旦知道了描述符增量的大小,
我們就能用兩種CD3DX12_CPU_DESCRIPTOR_HANDLE::offset方法之一偏移至第n個描述符的句柄處:

// 指定要偏移到的描述符的編號,再將它乘以描述符的增量大小
D3D12_CPU_DESCRIPTOR_HANDLE handle = mCbvHeap->GetCPUDescriptorHandleForHeapStart();
handle.offset(n * mCbvSrvDescriptorSize);

// 或者用另一種等價實現,先指定要偏移到的描述符編號,再設置描述符的增量大小
D3D12_CPU_DESCRIPTOR_HANDLE handle = mCbvHeap->GetCPUDescriptorHandleForHeapStart();
handle.offset(n, mCbvSrvDescriptorSize);

D3D12_GPU_DESCRIPTOR_HANDLE 類型有着同樣的偏移方法。

三、根簽名定義了繪製調用開始之前,需要與渲染流水線相綁定的資源,以及這些資源將被映射到的具體着色器輸入寄存器。
綁定到流水線的具體資源要根據着色器程序來確定。在創建PSO後,根簽名與着色器程序的組合就開始生效了。
根簽名由一系列根參數所構成。根參數可以是描述符表、根描述符或根常量。
描述符表在堆中指定了一塊描述符的連續範圍。
根描述符用於直接綁定根簽名中的描述符(此過程無需涉及描述符堆)。
根常量則用於直接綁定根簽名中的常量數據。
出於性能的原因,1個根簽名中所能容納的數據大小被限制爲最多64DWORD。
一個描述符表佔1DWORD,每個根描述符用2DWORD,而每個32位的根常量佔用1DWORD。
硬件會爲每次繪製調用而自動保存根實參的快照。這樣一來,我們就能在每次繪製調用的過程中安全地修改根實參了。
但儘量縮小根簽名的規模,以此降低內存間數據的複製量。

四、我們把單次繪製調用過程中,需要向渲染流水線提交的數據集稱爲渲染項(render item)。

五、不要在着色器內使用過多的常量緩衝區,出於性能考慮,常量緩衝區的數量以少於5個爲宜。

六、 爲了安全控制,也就是防止因多線程渲染帶來的不必要衝突,命令列表的狀態被分爲:錄製狀態和可Execute狀態(也叫關閉狀態),命令列表對象通常處在兩個狀態之一。通常一個命令列表在被創建時是默認處於錄製狀態的,此狀態下是不能被執行的。錄製完成後我們調用命令列表對象的Close方法關閉它,它就變成了可執行狀態,就可以提交給Command Queue(命令隊列)的ExecuteCommandList方法去執行,待執行完畢後我們又調用命令列表的Reset方法使它恢復到記錄狀態即可。當然Reset之後,命令列表之前記錄的命令也就丟失了,嚴格來說是這些命令被交給命令隊列去執行了,而命令列表不在記錄原來的命令了。

七、渲染:

         //開始記錄命令
         pICommandList->SetGraphicsRootSignature(pIRootSignature.Get());
         pICommandList->RSSetViewports(1, &stViewPort);
         pICommandList->RSSetScissorRects(1, &stScissorRect);
         // 通過資源屏障判定後緩衝已經切換完畢可以開始渲染了
         pICommandList->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(pIARenderTargets[nFrameIndex].Get()
    , D3D12_RESOURCE_STATE_PRESENT
    , D3D12_RESOURCE_STATE_RENDER_TARGET));
         CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(pIRTVHeap->GetCPUDescriptorHandleForHeapStart()
, nFrameIndex
, nRTVDescriptorSize);
         //設置渲染目標
        pICommandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
         // 繼續記錄命令,並真正開始新一幀的渲染
         const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
         pICommandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
         pICommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
         pICommandList->IASetVertexBuffers(0, 1, &stVertexBufferView);
         //Draw Call!!!
         pICommandList->DrawInstanced(3, 1, 0, 0);
         //又一個資源屏障,用於確定渲染已經結束可以提交畫面去顯示了
         pICommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pIARenderTargets[nFrameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
         //關閉命令列表,可以去執行了
         GRS_THROW_IF_FAILED(pICommandList->Close());
         //執行命令列表
         ID3D12CommandList* ppCommandLists[] = { pICommandList.Get() };
         pICommandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
         //提交畫面
         GRS_THROW_IF_FAILED(pISwapChain3->Present(1, 0));
         //開始同步GPU與CPU的執行,先記錄圍欄標記值
         const UINT64 fence = n64FenceValue;
         GRS_THROW_IF_FAILED(pICommandQueue->Signal(pIFence.Get(), fence));
         n64FenceValue++;
         // 看命令有沒有真正執行到圍欄標記的這裏,沒有就利用事件去等待,注意使用的是命令隊列對象的指針
         if (pIFence->GetCompletedValue() < fence)
         {
               GRS_THROW_IF_FAILED(pIFence->SetEventOnCompletion(fence, hFenceEvent));
               WaitForSingleObject(hFenceEvent, INFINITE);
         }
        //到這裏說明一個命令隊列完整的執行完了,在這裏就代表我們的一幀已經渲染完了,接着準備執行下一幀//渲染
        //獲取新的後緩衝序號,因爲Present真正完成時後緩衝的序號就更新了
         nFrameIndex = pISwapChain3->GetCurrentBackBufferIndex();
         //命令分配器先Reset一下
         GRS_THROW_IF_FAILED(pICommandAllocator->Reset());
         //Reset命令列表,並重新指定命令分配器和PSO對象
         GRS_THROW_IF_FAILED(pICommandList->Reset(pICommandAllocator.Get(), pIPipelineState.Get()));
         //GRS_TRACE(_T("第%u幀渲染結束.\n"), nFrame++);

在這段代碼中有兩處用到資源屏障,我們可以看到資源屏障的運用其實也很簡單,它核心的思想就是追蹤資源權限的變化,從而同步GPU上前後執行命令對訪問資源的操作。
代碼中第一處:
pICommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pIARenderTargets[nFrameIndex].Get()
, D3D12_RESOURCE_STATE_PRESENT
, D3D12_RESOURCE_STATE_RENDER_TARGET));
的確切含義就是說我們判定並等待完成渲染目標的資源是否完成了從Present(提交)狀態切換到Render Target(渲染目標)狀態了。ResourceBarrier是一個同步調用,與一般的同步調用不同,首先它在命令列表記錄時也是立即返回的,只是個同步調用記錄;其次它的目的是同步GPU上前後命令函數之間對同一資源的訪問操作的,再次它真正在Execute之中才是同步執行的,而我們在CPU的代碼中是感知不到的;我們唯一能確定的就是在Execute一個命令列表的過程中,如果它被真正執行完了之後,那麼就完全可以確定被轉換狀態的資源已經從其之前命令函數操作要求的狀態轉換成了之後操作要求的狀態了。或者形象的理解這個函數在正在被執行的時候是不能被“跳過”的。那麼這裏可能難以理解的是爲什麼說資源訪問狀態的切換就可以完成一個同步的“等待”操作呢?這就又不得不說GPU構造的特殊性了,因爲如前所述我們已經不止一次講到GPU是一個巨大的SIMD架構的處理器了,因此它上面的所謂命令的執行,往往是由若干個ALU(通常是成千上萬個)並行執行訪問具體的一個資源(其實就是一塊顯存)上不同單元來完成的,而且每種命令對同一塊資源的訪問要求又是完全不同的,比如我們這裏就是Present操作,它是隻讀的要求,而渲染的命令又要求這塊資源是Render Target,也就是可寫的,所以兩個操作直接就需要來回控制這種狀態的切換,而GPU本身知道那個操作已經完成可以執行真正的狀態切換了,而狀態切換成功就說明之前操作已經全部完成,可以進行之後的操作了。這樣一來其實Transition這個函數的含義也就明白了。當然這裏的CD3DX12_RESOURCE_BARRIER類也是來自d3d12.h中,也是其基本結構的擴展,真實的結構體中就是要求我們指明是那塊資源,並且指明之前操作要求的訪問狀態是什麼,以及之後的訪問狀態是什麼,而這個類的封裝就使初始化這個結構體更加的簡便和直觀了。
在這裏插入圖片描述

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