Direct3D 12入门教程之 ---- Direct3D 12初始化流程
注:以下內(nèi)容參考自
書籍:《DirectX 12 3D》游戲開發(fā)實戰(zhàn),
微軟官方的 DirectX樣例程序;DirectX-Graphics-Samples, 參見github鏈接:https://github.com/Microsoft/DirectX-Graphics-Samples
這是我實踐時寫的代碼的GitHub鏈接:https://github.com/blowingBreeze/D3D12Guide,持續(xù)更新
1. 創(chuàng)建Direct3D設(shè)備,ICreateD3D12Device
-
Direct3D是我們操控顯卡的一個抽象層,學(xué)習(xí)過面向?qū)ο蟮耐瑢W(xué)應(yīng)該很熟悉,在將一個現(xiàn)實中的對象(也就是這里的顯卡),往往會將該對象分解為代碼中的多個對象,由這些對象對外部系統(tǒng)提供接口;
-
D3D12Device就是Direct3D中用于提供顯卡控制接口的對象,它代表著當(dāng)前系統(tǒng)中的顯示適配器,一般來說,它是一個3D圖形硬件(如顯卡), 但是,操作系統(tǒng)在沒有顯卡的時候也能正常的顯示圖像,這時候使用的就是軟件顯示適配器,如(WARP適配器),
可以在不急著使用電腦的時候折騰一下,將操作系統(tǒng)的顯卡設(shè)備全部卸載,觀察一下電腦的情況
通過這個函數(shù)即可創(chuàng)建一個D3D12的設(shè)備對象
HRESULT D3D12CreateDevice(IUnknown *pAdapter, //想為哪個顯示適配器創(chuàng)建一個設(shè)備對象,傳遞nullptr則使用系統(tǒng)中的默認(rèn)適配器D3D_FEATURE_LEVEL MinimumFeatureLevel, //指定支持的最低版本的Direct3D版本REFIID riid, //GUIDvoid **ppDevice //用于接收設(shè)備對象所在的內(nèi)存的指針 );為了簡單起見,這里使用系統(tǒng)默認(rèn)的顯示適配器
hResult = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&mD3DDevice));其中IID_PPV_ARGS是Direct3D為我們提供的一個工具宏,IID_PPV_macro,它為我們生成了后面接口中的后兩個參數(shù)
1.1 獲取顯示適配器
顯示適配器是真正實現(xiàn)了圖形處理能力的對象,上面的D3D12Device是對顯示適配器的進一步封裝.
一個系統(tǒng)中可能會有多個顯示適配器,比如我就有兩個顯示適配器;
那么在程序中我們怎么才能知道使用的是哪個適配器呢,畢竟游戲的使用性能較高的適配較好。
下面簡單提一下DXGI的概念,現(xiàn)在僅知道有這么個東西就行了,以后慢慢就理解了
DXGI是一種與Direct3D配合使用的API,設(shè)計DXGI的基本理念是使得多種圖形API中的底層任務(wù)能夠使用通用的API,比如3D和2D的圖形API在底層都可以使用相同的,比如Direct3D和Direct2D內(nèi)部實現(xiàn)交換鏈時可以使用同一套接口
我們在獲取系統(tǒng)的可用顯示適配器時,會使用到 IDXGIFactory,主要用于創(chuàng)建SwapChain以及枚舉顯示適配器
我們可以使用下面的代碼來枚舉系統(tǒng)中的顯示適配器
系統(tǒng)不單單可以有多個顯示適配器,每個顯示適配器也可以連接多個顯示輸出(顯示屏),我們可以通過獲取到的adapter對象進一步獲取更詳細的顯示信息,這里就不進行介紹了
2.創(chuàng)建命令隊列和命令列表
- 在《DirectX 12 3D游戲開發(fā)實戰(zhàn)》中,第二步是創(chuàng)建 ID3D12Fence對象,并查詢描述符大小
- 我這里不這么做,是因為我覺得,Fence是一個用來同步CPU,GPU的,但是目前為止還沒有提 到CPU與GPU的交互,在第二步創(chuàng)建會顯得很奇怪;當(dāng)然,對新手來說(比如我)是這樣的,熟悉以后可以依據(jù)實際情況調(diào)換初始化順序;
- 這里你也可以直接先跳到創(chuàng)建 ID3D12Fence對象 的部分進行閱讀
2.1 命令隊列和命令列表
- 進行圖形編程的時候,是有兩種處理器在進行工作的,CPU和GPU,他們之間沒有絕對的從屬關(guān)系,并行工作,但GPU需要CPU告訴它,該畫什么東西;
- CPU和GPU的執(zhí)行命令的速度是不一樣的,如果使用同步的方式執(zhí)行,那么CPU勢必需要等待 GPU執(zhí)行完命令才能給GPU下達下一個繪制指令,而GPU做完繪制工作后在CPU沒有下達指令前也必須 等待 CPU下達指令,這樣就會導(dǎo)致處理器有一定的空轉(zhuǎn)狀態(tài),不利于最大程度的發(fā)揮出處理器的性能;
- 那么我們可以參考異步事件和緩沖池的方式進行處理,每個CPU命令看作一個一條指令,放入指令池中,而GPU不停的從這個指令池中讀取CPU下達的指令,進行繪制工作;這樣就能將兩個處理器進行分離,互不相干(當(dāng)然,不管怎么樣,這兩個處理器都是需要做一些同步操作的,這個會在講Fence的時候說明),GPU可以最大限度的執(zhí)行繪制任務(wù)直到?jīng)]有指令需要執(zhí)行,而CPU也不需要等待GPU繪制完成就可以繼續(xù)下發(fā)任務(wù)
這里面有一點很重要,指令的執(zhí)行是異步的,CPU下發(fā)的指令不會立即執(zhí)行,直到GPU執(zhí)行到了指令池中的對應(yīng)指令
- 在《DirectX 12 3D游戲開發(fā)實戰(zhàn)》中有提到,指令池滿了或者空了之后,CPU和GPU必然有一個處于空閑狀態(tài),但是我并未在書中看到相應(yīng)的解決方案,
- 我的一個想法是,指令池滿了或者空了之后,可以將一部分GPU或CPU中的任務(wù)移交到CPU或GPU中,當(dāng)然,這個在具體實現(xiàn)時難度是很大的
- 在Direct3D 中,使用的是命令隊列和命令列表的方式對CPU和GPU的交互進行緩沖
《DirectX 12 3D游戲開發(fā)實戰(zhàn)》 4.2.1節(jié)中:
- 每個GPU都至少維護著一個命令隊列(command queue, 本質(zhì)上是環(huán)形緩沖區(qū),即ring buffer)
- 借助Direct3D API,CPU可以利用命令列表(command list)將命令提交到這個隊列中去
- 在Direct3D 11中,有立即渲染(immediate rendering)和延遲渲染(deferred rendering),前者是將緩沖區(qū)的命令之間借驅(qū)動層發(fā)往GPU執(zhí)行,后者則與Direct3D 12中的命令列表模型類似,而在Direct 3D 12中則完全采取了 "命令列表->命令隊列的方式"是多個命令列表同時記錄命令,借此充分發(fā)揮多核心處理器的性能
2.2 命令隊列和命令列表代碼示例
在Direct3D 12中,命令隊列使用 ID3D12CommandQueue接口進行表示,通過ID3D12Device::CreateCommandQueue方法創(chuàng)建隊列(還記得1.1中的D3D12Device嗎?)
創(chuàng)建命令隊列時,需要通過填寫D3D12_COMMAND_QUEUE_DESC queueDesc結(jié)構(gòu)體來描述隊列
MSDN上的 ID3D12Device::CreateCommandQueue method
這里我們提到的三個函數(shù)
- CreateCommandQueue:這個用于創(chuàng)建命令隊列,很好理解
- CreateCommandAllocator:用于創(chuàng)建命令分配器(command allocator),這個用于記錄在命令列表中的命令,在執(zhí)行命令列表時,命令隊列會引用命令分配器中的命令; 我目前對這個對象的理解是,用于保存命令隊列中指令的內(nèi)存地址的,方便命令隊列在執(zhí)行命令列表時進行引用
- CreateCommandList:用于創(chuàng)建命令隊列,這個很好理解了,真實的管理命令的添加刪除的對象
CommandList有一系列的方法用于向隊列中添加命令,MSDN上的 ID3D12GraphicsCommandList interface
在添加完命令后一定要調(diào)用 ID3D12GraphicsCommandList::Close方法結(jié)束命令的記錄,命令列表添加完成后,需要使用ID3D12CommandQueu::ExecuteCommandLists方法將命令列表送入命令隊列中,還記得之前提到的命令緩沖嗎,這里的執(zhí)行其實對于CPU來說是以及執(zhí)行了,但實際上GPU并不一定馬上執(zhí)行指令
- 我們可以創(chuàng)建多個關(guān)聯(lián)與同一個命令分配器的命令列表,但是不能同時用他們記錄命令,即必須保證其中一個命令列表在記錄命令時,必須關(guān)閉同一個命令分配器的其他命令列表,
- 換句話說,必須保證命令列表中的所有命令都會按順序地添加到命令分配器中
- 當(dāng)創(chuàng)建或重置一個命令列表的時候,它會處于一種“打開“的狀態(tài),所以當(dāng)嘗試為同一個命令分配器連續(xù)創(chuàng)建兩個命令列表時會報錯
- 在調(diào)用ID3D12CommandQueue::ExcuteCommandList方法后,就可以通過ID3D12GraphicsCommandList::Reset方法,安全地服用命令列表占用的底層內(nèi)存來記錄新的命令集,Reset命令列表并不會英雄命令隊列中的命令,因為相關(guān)的命令分配器依然維護者其內(nèi)存中被命令隊列引用的系列命令
- 在向GPU提交了一幀的渲染命令后,我們可能需要為了繪制下一幀而復(fù)用命令分配器中的內(nèi)存,可以使用ID3D12CommandAllocator::Reset方法,這種方法的功能類似與std::vector::clear方法,使得命命令分配器種的命令清空,但保存內(nèi)存不釋放,**注意,在不確定GPU執(zhí)行完命令分配器中所有的命令之前,不要Reset命令分配器,因為命令隊列可能還引用著命令分配器中的數(shù)據(jù)**
3.創(chuàng)建Fence(圍欄)
前面有提到,CPU和GPU的指令執(zhí)行是異步的,并且他們可能會同時訪問同一塊內(nèi)存(指令分配器),也就有可能發(fā)生訪問沖突,考慮以下情況,
- CPU向GPU發(fā)送了A,B,C三條指令,其中B引用了dataB對象,而在CPU中,發(fā)送ABC指令的同時也在執(zhí)行D指令,D指令可能會修改dataB對象;
這種情況下,GPU在執(zhí)行B指令時,獲取的dataB有可能不是CPU發(fā)送B指令時的dataB,可能導(dǎo)致很奇怪的程序異常,這種由于訪問沖突導(dǎo)致的異常很難進行排查;
這時候我們需要做的,就是讓CPU在執(zhí)行B指令前,不執(zhí)行D指令,也就是CPU和GPU需要進行狀態(tài)同步;
- 在進程和線程的同步方式中,可以選擇鎖,信號量,互斥量等方式進行同步,在這里,也可以參考這種方式進行實現(xiàn),
Drect3D 12中,提供了一種 Fence對象,可以在命令隊列中,設(shè)置一條圍欄指令,當(dāng)GPU執(zhí)行到圍欄指令時,觸發(fā)某個事件,而在GPU中則等待事件的發(fā)生,這樣就達到了同步的目的,這種方法也稱作刷新命令隊列(flushing the command queue)
ThrowIfFailed(mD3DDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mD3DFence)));const UINT64 fence = mFenceValue; //向命令隊列中添加一條用于設(shè)置新的圍欄的命令 ThrowIfFailed(mCommandQueue->Signal(mD3DFence.Get(), fence)); mFenceValue++; // Wait until the previous frame is finished. if (mD3DFence->GetCompletedValue() < fence) { ThrowIfFailed(mD3DFence->SetEventOnCompletion(fence, mFenceEvent)); WaitForSingleObject(mFenceEvent, INFINITE); }4.創(chuàng)建交換鏈
4.1 什么是交換鏈?
- 最終展現(xiàn)在屏幕上的圖像數(shù)據(jù),必定是要保存在某塊內(nèi)存中的,也就是緩沖區(qū)中。
- 想象一下,若我們只創(chuàng)建一個緩沖區(qū),那么每次畫面的更新和屏幕圖像的更新便是混在一起的,幀率不高(也就是繪制速度不夠)時,能看出畫面的撕裂(舊的圖像和新繪制的圖像混在了一起),
- 為了解決這個問題,Direct3D中采用了雙緩沖區(qū)的做法:前臺緩沖區(qū)和后臺緩沖區(qū),前臺緩沖區(qū)存儲屏幕上展示的圖像數(shù)據(jù),而后臺緩沖區(qū)存儲繪制中的數(shù)據(jù),用于下一次展示,當(dāng)后臺緩沖區(qū)的圖像繪制完成時,前后臺緩沖區(qū)角色互換,這種互換操作稱為呈現(xiàn)(presenting),前后臺緩沖區(qū)構(gòu)成的交換鏈(swap chain),他們每幀都需要進行互換;
- 使用兩個緩沖時稱為雙緩沖,使用三個緩沖時稱為三重緩沖,一般使用雙緩沖就夠了,什么時候需要使用三緩沖呢?https://www.intel.cn/content/www/cn/zh/support/articles/000006930/graphics-drivers.html雖然還不是很明白,但是大致理解是為了解決垂直同步的問題
4.2 創(chuàng)建
mSwapChain.Reset(); mSwapChainDesc.BufferDesc.Width = 1366; mSwapChainDesc.BufferDesc.Height = 768; mSwapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; mSwapChainDesc.BufferDesc.RefreshRate.Numerator = 60; mSwapChainDesc.BufferDesc.RefreshRate.Denominator = 1; mSwapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER::DXGI_MODE_SCANLINE_ORDER_LOWER_FIELD_FIRST; mSwapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING::DXGI_MODE_SCALING_CENTERED; mSwapChainDesc.Windowed = true; mSwapChainDesc.OutputWindow = mhMainWind; mSwapChainDesc.BufferCount = BUFFER_COUNT; mSwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; mSwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT::DXGI_SWAP_EFFECT_FLIP_DISCARD; mSwapChainDesc.SampleDesc.Count = 1; //這里要填0,不然會報錯,原因是不支持該功能,具體的還不清楚 mSwapChainDesc.SampleDesc.Quality = 0;ComPtr<IDXGISwapChain> swapChain; ThrowIfFailed(mD3DFactory->CreateSwapChain(mCommandQueue.Get(), // Swap chain needs the queue so that it can force a flush on it.&mSwapChainDesc,swapChain.GetAddressOf() )); ThrowIfFailed(swapChain.As(&mSwapChain));和以前一樣,你需要先填寫一個描述交換鏈的結(jié)構(gòu)體,然后進行創(chuàng)建,具體可以參考:MSDN , IDXGIFactory::CreateSwapChain method
5. 創(chuàng)建描述符堆
5.1 什么是描述符?
- 在渲染的過程中,GPU需要對資源進行讀寫操作,我們需要將與本次繪制調(diào)用(draw call)相關(guān)的綁定(bind,或稱鏈接,link)到流水線上,而部分資源可能在每次繪制調(diào)用時都有所變化,因此我們需要每次按需更新綁定資源到渲染流水線中。
- 但是GPU資源并非直接和渲染流水線綁定的,而是需要通過一種名為描述符(descriptor)的對象來對它進行間接引用,可以把描述符看作時一種對GPU資源的內(nèi)容聲明,告訴GPU,這個資源是什么東西,什么格式,什么類型;
- 每個描述符都有一種具體的類型,這個類型指定了資源的具體作用,常見的有:
- CBV:常量緩沖區(qū)視圖(constant buffer view),
- SRV:著色資源視圖(shader resource view)
- UAV:無序訪問視圖(unordered access view),
- sampler:采樣器資源
- RTV:渲染目標(biāo)視圖(render targe view),
- DSV:深度/模板視圖(depth/stencil view)
這里面每種視圖對應(yīng)的都是一種資源;
5.2 什么是描述符堆?
- 描述符堆(descriptor heap)中存有一系列描述符(可以看作是描述符數(shù)組),本質(zhì)上是存放某種特定類型描述符的一塊內(nèi)存,我們需要為每一種類型的描述符都創(chuàng)建出單獨的描述符堆,也可以為同一種描述符類型創(chuàng)建多個描述符堆;
5.3創(chuàng)建
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc; rtvHeapDesc.NumDescriptors = mSwapChainDesc.BufferCount; rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; rtvHeapDesc.NodeMask = 0; ThrowIfFailed(mD3DDevice->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc; dsvHeapDesc.NumDescriptors = 1; dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; dsvHeapDesc.NodeMask = 0; ThrowIfFailed(mD3DDevice->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));可參考:MSDN,ID3D12Device::CreateDescriptorHeap method
6.創(chuàng)建渲染目標(biāo)視圖(Render Target View,RTV)
前面我們已經(jīng)創(chuàng)建好了描述符堆,接下來應(yīng)該為后臺緩沖區(qū)創(chuàng)建一個渲染目標(biāo)視圖,這樣才能將緩沖區(qū)綁定到渲染流水線中,使得Direct3D向緩沖區(qū)中渲染圖像,可以理解為,本來內(nèi)存中有一塊緩沖區(qū),但是GPU看不到它,我們創(chuàng)建一個視圖,綁定到渲染流水線中,這樣GPU就能看到這個緩沖區(qū)并往里面寫東西了。
//獲取描述符堆的首地址(句柄) D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart()); // Create a RTV for each frame. for (UINT n = 0; n < BUFFER_COUNT; n++) { ThrowIfFailed(mSwapChain->GetBuffer(n, IID_PPV_ARGS(&mRenderTargets[n]))); mD3DDevice->CreateRenderTargetView(mRenderTargets[n].Get(), nullptr, rtvHandle); rtvHandle.ptr += mRtvDescriptorSize; //每次偏移每個描述符的大小,}7.設(shè)置視口
這個比較簡單,
D3D12_VIEWPORT mViewport; //視口信息描述 mViewport.TopLeftX = 0; mViewport.TopLeftY = 0; mViewport.Width = 1366; mViewport.Height = 768; mViewport.MinDepth = D3D12_MIN_DEPTH; mViewport.MaxDepth = D3D12_MAX_DEPTH;mCommandList->RSSetViewports(1, &mViewport); //向命令列表添加命令8.尾聲
到這里整個Direct3D 12的初始化基本就完成了,當(dāng)然,這里只是簡單的介紹了初始化過程中的一些關(guān)鍵步驟,如果希望完整的學(xué)習(xí)整個流程,可以去我的GitHub上看完整的代碼:https://github.com/blowingBreeze/D3D12Guide接下來我會嘗試將整個流程進行封裝,以免除每次都得寫一串冗長的初始化代碼,并開始學(xué)習(xí)渲染流水線部分;
,
總結(jié)
以上是生活随笔為你收集整理的Direct3D 12入门教程之 ---- Direct3D 12初始化流程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。