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