日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Direct3D 12入门教程之 ---- Direct3D 12初始化流程

發(fā)布時間:2023/12/14 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Direct3D 12入门教程之 ---- Direct3D 12初始化流程 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

注:以下內(nèi)容參考自
書籍:《DirectX 12 3D》游戲開發(fā)實戰(zhàn),
微軟官方的 DirectX樣例程序;DirectX-Graphics-Samples, 參見github鏈接:https://github.com/Microsoft/DirectX-Graphics-Samples

Direct3D 12對于開發(fā)者來說,就是一個SDK, 這篇文章就來講一講這個SDK的初始化流程,以及我在實踐的過程中遇到的一些問題;

這是我實踐時寫的代碼的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)中的顯示適配器

ComPtr<IDXGIFactory4> factory; UINT dxgiFactoryFlags = 0; #if defined(_DEBUG) // Enable the debug layer (requires the Graphics Tools "optional feature"). // NOTE: Enabling the debug layer after device creation will invalidate the active device. { ComPtr<ID3D12Debug> debugController; if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))) { debugController->EnableDebugLayer();// Enable additional debug layers. dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG; } } #endif CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory)); UINT i = 0; ComPtr<IDXGIAdapter> adapter = nullptr; while (factory->EnumAdapters(i, &adapter) != DXGI_ERROR_NOT_FOUND) { DXGI_ADAPTER_DESC desc; adapter->GetDesc(&desc);std::wcout << desc.Description<<std::endl; ++i; }

系統(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

D3D12_COMMAND_QUEUE_DESC queueDesc = {}; queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; ThrowIfFailed(mD3DDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue))); ThrowIfFailed(mD3DDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf()))); ThrowIfFailed(mD3DDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, mDirectCmdListAlloc.Get(), nullptr, IID_PPV_ARGS(mCommandList.GetAddressOf())));//下面是ThrowIfFailed的定義 inline void ThrowIfFailed(HRESULT hr) {if (FAILED(hr)){throw HrException(hr);} }

這里我們提到的三個函數(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)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。