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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

DirectX12_入门之三角形

發布時間:2023/12/14 编程问答 69 豆豆
生活随笔 收集整理的這篇文章主要介紹了 DirectX12_入门之三角形 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

為了更加深刻的理解圖形API之間的區別,從此文讓我們正式開始DirectX12的學習之旅。之前了解過OpenGL、DX11與Vulkan,我們也簡單的知道了這些圖形API之間的區別和架構上的差異,我們現在來看一下DX12,從使用中了解它與Vulkan的異步架構之間的異同。

具體代碼參照DX12龍書github。

一、準備工作

首先需要先了解:DirectX12_基礎知識;

為了實現DX12這個目標的大致步驟也跟調用歷史版本的D3D11、OpenGL、Vulkan中一樣,就是先創建Windows的窗口,接著創建設備對象、準備各種資源,再設置渲染管線的狀態,最終在消息循環中不斷的調用OnUpdate和OnRender(我的例子中甚至沒有封裝這兩個函數,這里只是讓大家先有個框架概念的認識,聰明的你應該一點就通了)。當然這個過程和我們以前學用其他的D3D接口甚至與學用OpenGL接口的過程都是完全一致的。看到這些共同點,我們應該慶幸,同時也應該信心滿滿的認為,至少這世界還盡在我們掌握之中!

而進一步我們真正要好好注意和學習的就是那些不同點和足以致命的細節了,因為在D3D12中加入了“顯存管理”、“多線程渲染”、“異步Draw Call”等的高級概念,所以在具體使用上就有其獨特的風格和復雜性了。

1.1 頭文件與庫

要調用D3D12,第一步就是包含其頭文件并鏈接其lib,那么就在你的代碼開頭這樣來寫:

#include <SDKDDKVer.h> #define WIN32_LEAN_AND_MEAN // 從 Windows 頭中排除極少使用的資料 #include <windows.h> #include <tchar.h> #include <wrl.h> //添加WTL支持 方便使用COM #include <strsafe.h> #include <dxgi1_6.h> #include <DirectXMath.h> #include <d3d12.h> #include <d3d12shader.h> #include <d3dcompiler.h> #if defined(_DEBUG) #include <dxgidebug.h> #endifusing namespace Microsoft; using namespace Microsoft::WRL; using namespace DirectX;//linker #pragma comment(lib, "dxguid.lib") #pragma comment(lib, "dxgi.lib") #pragma comment(lib, "d3d12.lib") #pragma comment(lib, "d3dcompiler.lib")#define GRS_WND_CLASS_NAME _T("GRS Game Window Class") #define GRS_WND_TITLE _T("DirectX12 triangle")#define GRS_THROW_IF_FAILED(hr) {HRESULT _hr = (hr);if (FAILED(_hr)){ throw CGRSCOMException(_hr); }}class CGRSCOMException { public:CGRSCOMException(HRESULT hr) : m_hrError(hr){}HRESULT Error() const{return m_hrError;} private:const HRESULT m_hrError; };

首先,代碼中使用了WRL,它給我們帶了基礎安全的便捷性(就是讓我們忘記那些Release調用,也不會帶來內存泄漏等問題)。

其次,我們包含了<DirectXMath.h>這個頭文件,這是一個非常常用的數學庫(因為它通過匯編語言幾乎高效利用了所有現代CPU上的SIMD擴展指令,并且是內聯函數形式,是榨干CPU的重要擴展庫),可以在GitHub上下載到它的最新版本,當然Windows SDK中自帶的也是較新的版本了。

再次,用#pragma comment(lib, “xxxxxx.xxx”)來引用lib庫,對了VS使用的是2017。

又次,項目中包含了"d3dx12.h",這個文件是將D3D12中的結構都派生(說擴展更合適)為簡單的類,以便于使用,它的“封裝”我認為應該算作是D3D12 sdk的一部分,可以直接從最新的微軟D3D12示例中找到。

最后,GRS_THROW_IF_FAILED這個宏其實它就是為了在調用COM接口時簡化出錯時處理時使用的一個宏,就是為了出錯時拋出一個異常,因為只要是有異常機制的語言,程序員們都會使用拋異常來簡化。

接下來我們開始看DX12的初始化主要流程階段:

  • 使用 D3D12CreateDevice 函數創建 ID3D12Device

  • 創建 ID3D12Fence object 并確認 descriptor 大小

  • 檢查 4X MSAA quality level 支持

  • 創建 command queue, command list allocator, and main command list

  • 定義并創建 swap chain

  • 創建應用需要的 descriptor heaps

  • 重置 back buffer 大小,并為 back buffer 創建 render target view

  • 創建 depth/stencil buffer 和 associated depth/stencil view

  • 設置 viewport 和 裁剪框

1.2 變量定義

接下來,我們需要進行DX12變量定義:

const UINT nFrameBackBufCount = 3u;int iWidth = 1024; int iHeight = 768; UINT nFrameIndex = 0; UINT nFrame = 0;UINT nDXGIFactoryFlags = 0U; UINT nRTVDescriptorSize = 0U;HWND hWnd = nullptr; MSG msg = {};float fAspectRatio = 3.0f;D3D12_VERTEX_BUFFER_VIEW stVertexBufferView = {};UINT64 n64FenceValue = 0ui64; HANDLE hFenceEvent = nullptr;CD3DX12_VIEWPORT stViewPort(0.0f, 0.0f, static_cast<float>(iWidth), static_cast<float>(iHeight)); CD3DX12_RECT stScissorRect(0, 0, static_cast<LONG>(iWidth), static_cast<LONG>(iHeight));ComPtr<IDXGIFactory5> pIDXGIFactory5; ComPtr<IDXGIAdapter1> pIAdapter; ComPtr<ID3D12Device4> pID3DDevice; ComPtr<ID3D12CommandQueue> pICommandQueue; ComPtr<IDXGISwapChain1> pISwapChain1; ComPtr<IDXGISwapChain3> pISwapChain3; ComPtr<ID3D12DescriptorHeap> pIRTVHeap; ComPtr<ID3D12Resource> pIARenderTargets[nFrameBackBufCount]; ComPtr<ID3D12CommandAllocator> pICommandAllocator; ComPtr<ID3D12RootSignature> pIRootSignature; ComPtr<ID3D12PipelineState> pIPipelineState; ComPtr<ID3D12GraphicsCommandList> pICommandList; ComPtr<ID3D12Resource> pIVertexBuffer; ComPtr<ID3D12Fence> pIFence;struct GRS_VERTEX { XMFLOAT3 position; XMFLOAT4 color; };

其中我們目標是要畫一個三角形,所以要先定義我們的3D頂點數據結構GRS_VERTEX,當然其中的XMFLOAT3和XMFLOAT4來自DirectXMath庫,等價于float[3]和float[4]。當然如果你之前有了解過關于shader優化的一些專題的話,那么在正式的項目中你應該至少保持4*sizeof(float)邊界對齊,這樣可以提高GPU訪問這些數據的效率。

1.3 創建窗口

// 創建窗口{WNDCLASSEX wcex = {};wcex.cbSize = sizeof(WNDCLASSEX);wcex.style = CS_GLOBALCLASS;wcex.lpfnWndProc = WndProc;wcex.cbClsExtra = 0;wcex.cbWndExtra = 0;wcex.hInstance = hInstance;wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);wcex.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH); //防止無聊的背景重繪wcex.lpszClassName = GRS_WND_CLASS_NAME;RegisterClassEx(&wcex);DWORD dwWndStyle = WS_OVERLAPPED | WS_SYSMENU;RECT rtWnd = { 0, 0, iWidth, iHeight };AdjustWindowRect(&rtWnd, dwWndStyle, FALSE);// 計算窗口居中的屏幕坐標INT posX = (GetSystemMetrics(SM_CXSCREEN) - rtWnd.right - rtWnd.left) / 2;INT posY = (GetSystemMetrics(SM_CYSCREEN) - rtWnd.bottom - rtWnd.top) / 2;hWnd = CreateWindowW(GRS_WND_CLASS_NAME, GRS_WND_TITLE, dwWndStyle, posX, posY, rtWnd.right - rtWnd.left, rtWnd.bottom - rtWnd.top, nullptr, nullptr, hInstance, nullptr);if (!hWnd){return FALSE;}ShowWindow(hWnd, nCmdShow);UpdateWindow(hWnd);}

注意代碼中指定的窗口風格WS_OVERLAPPEDWINDOW,這里因為我們不處理OnSize事件(眾所周知,窗口重繪需要動態修改,以后再詳細處理吧)。

二、創建DXGI

這些基礎的工作做完了,我們就要開始正式調用D3D12接口了。根據前面的簡要描述這里就該創建設備對象接口了,在D3D12中,一個重要的概念是將設備對象概念進行了擴展。將原來在D3D9中揉在一起的圖形子系統(從硬件子系統角度抽象),顯示器,適配器,3D設備等對象進行了分離,而分離的標志就是使用IDXGIFactory來代表整個圖形子系統,它主要的功用之一就是讓我們創建適配器、3D設備等對象接口用的,因此它的名字就多了個Factory,這估計也是暗指Factory設計模式之故。這個對象接口就是我們要創建的第一個接口:

CreateDXGIFactory2(nDXGIFactoryFlags, IID_PPV_ARGS(&pIDXGIFactory5)); // 關閉ALT+ENTER鍵切換全屏的功能,因為我們沒有實現OnSize處理,所以先關閉 GRS_THROW_IF_FAILED(pIDXGIFactory5->MakeWindowAssociation(hWnd, DXGI_MWA_NO_ALT_ENTER));

在D3D中,如果你了解COM的話,你就會知道所有D3D12對象接口的初始化創建不能再使用COM規范的CoCreateInstance函數了,這是你必須忘記的第一個招式。這里你要記住的就是D3D12僅僅利用了COM的接口概念而已,其它的都忽略了。這樣我們在使用這些接口時就可以簡單的理解為是系統提供的只有公有函數的類的對象指針即可

三、創建設備對象接口

有了IDXGIFactory的接口,我們就需要枚舉并選擇一個合適的顯示適配器(顯卡)來創建D3D設備接口。這里要說明的是為什么我們要選擇枚舉這種啰嗦的方式來創建我們的設備接口呢?因為對于現代的很多PC系統來說CPU中往往集成了顯卡,同時系統中還會有一個獨立的顯卡。另外大多數筆記本系統中,為節能之目的,往往會把集顯作為默認的顯示適配器,而由于集顯功能性能限制的問題,所以在有些示例中可能會引起一些問題,尤其是將來準備使用DXR渲染的時候。所以基于這樣的原因,這里就使用比較繁瑣的枚舉顯卡適配器的方式來創建3D設備對象。另外這也是為將來使用多顯卡渲染示例的需要做準備的。代碼如下: for (UINT adapterIndex = 0; DXGI_ERROR_NOT_FOUND != pIDXGIFactory5->EnumAdapters1(adapterIndex, &pIAdapter); ++adapterIndex) { DXGI_ADAPTER_DESC1 desc = {}; pIAdapter->GetDesc1(&desc); if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) {//軟件虛擬適配器,跳過continue; } //檢查適配器對D3D支持的兼容級別,這里直接要求支持12.1的能力,注意返回接口的那個參數被置為了nullptr,這樣 //就不會實際創建一個設備了,也不用我們啰嗦的再調用release來釋放接口。這也是一個重要的技巧,請記住! if (SUCCEEDED(D3D12CreateDevice(pIAdapter.Get(), D3D_FEATURE_LEVEL_12_1, _uuidof(ID3D12Device), nullptr))) {break; } } //創建D3D12.1的設備 GRS_THROW_IF_FAILED(D3D12CreateDevice( pIAdapter.Get() , D3D_FEATURE_LEVEL_12_1 ,IID_PPV_ARGS( &pID3DDevice )));

特別需要提醒的是,你在代碼的創建循環中的if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)語句上設置斷點,然后仔細查看desc中的內容,確認你用于創建設備對象的適配器是你系統中最強的一塊顯卡。一般系統中默認序號0的設備是集顯,如果不是獨顯,那就請你修改adapterIndex這個循環初值,比如改為1或2過更高的值試試,當然前提是你的系統中確定有那么多適配器(也就是顯卡),直到使用了性能最強的一個適配器來創建設備。這樣做的目的不是為了跑性能,而是目前我發現集顯在運行一些高級功能時會出現一些問題,很多高級功能是不支持的,用功能比較強的獨顯是不錯的一個方法。

四、創建命令隊列接口

再接下去如果你熟悉D3D11的話,我們就需要創建DeviceContext對象及接口了,而在D3D9中有了設備接口就相當于有了一切,直接就可以加載資源,設置管線狀態,然后開始渲染。

其實我一直覺得在D3D11中這個接口對象及名字DeviceContext不是那么直觀。在D3D12中就直接改叫CommandQueue了。這是為什么呢?其實現代的顯卡上或者說GPU中,已經包含多個可以同時并行執行命令的引擎了,不是游戲引擎,可以理解為執行某類指令的專用微核。也請注意這里的概念,一定要理解并行執行的引擎這個概念,因為將來的重要示例如多線程渲染,多顯卡渲染示例等中還會用到這個概念。

這里再舉個例子來加深理解這個概念,比如支持D3D12的GPU中至少就有執行3D命令的引擎,執行復制命令的引擎(就是從CPU內存中復制內容到顯存中或反之或GPU內部以及引擎之間),執行通用計算命令的引擎(執行Computer Shader的引擎)以及可以進行視頻編碼解碼的視頻引擎等。而在D3D12中針對這些不同的引擎,就需要創建不同的命令隊列接口來代表不同的引擎對象了。這相較于傳統的D3D9或者D3D11設備接口來說,不但接口被拆分了,而且在對象概念層級上都進行了拆分。

因為我們的目標是繪制一個三角形,因此我們第一個要創建的引擎(命令隊列)就是3D圖形命令隊列(暫時我們也只需要這個)。創建的代碼如下:

D3D12_COMMAND_QUEUE_DESC queueDesc = {}; queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&pICommandQueue)));

這段代碼中較特別的地方之一就是我們需要一個結構來傳遞參數給創建函數,這是一種函數設計風格,之所以這里要重點強調這個,是因為在D3D12中這種風格被大量使用。比之用參數列表來調用函數的方式,這種方式在可寫性修改性上有很大的改觀,對于不用的參數,不賦值即可。這也是與Vulkan相似的地方。

另一個需要注意的細節就是我們創建3D圖形命令隊列(引擎)的標志是D3D12_COMMAND_LIST_TYPE_DIRECT從其名字幾乎看不出是什么意思,其實這個標志的真正含義是說,我們創建的不只是能夠執行3D圖形命令的隊列那么簡單,而是說它是圖形設備的“直接”代表物,本質上還可以執行幾乎所有的命令,包括圖形命令、復制命令、計算命令甚至視頻編解碼命令,還可以執行捆綁包(這個也是以后介紹),因此它是3D圖形命令隊列(引擎)的超集,基本就是代表了整個GPU的執行能力,固名直接。

五、創建交換鏈

有了命令隊列對象,接下去我們就可以創建交換鏈了。與之前的D3D版本不同,尤其是與D3D9等古老接口不同,D3D12中交換鏈更加的獨立了。為了概念上更加清晰,我建議你將交換鏈理解為一個畫板上有很多頁畫紙,而渲染管線就是畫筆顏料等等,雖然他們要組合在一起才能繪畫,但本質上是獨立的東西,因為畫紙我們還可以使用完全不同的別的筆來寫字或繪畫,比如交換鏈還可以用于D2D、DirectWrite繪制等,只是在這里我們是用來作為3D渲染的目標。

另外在D3D12中具體創建交換鏈時就需要指定一個命令隊列,這也是最終呈現畫面前,交換鏈需要確定繪制操作是否完全完成了,也就是需要這個命令隊列最終Flush(刷新)一下。創建交換鏈的代碼如下:

DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {}; swapChainDesc.BufferCount = nFrameBackBufCount; swapChainDesc.Width = iWidth; swapChainDesc.Height = iHeight; swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; swapChainDesc.SampleDesc.Count = 1;GRS_THROW_IF_FAILED(pIDXGIFactory5->CreateSwapChainForHwnd(pICommandQueue.Get(), hWnd,&swapChainDesc,nullptr,nullptr,&pISwapChain1));

上面的代碼中沒什么特別的,風格上依舊是結構體做函數的主要參數,要注意的就是SwapEffect參數,目前賦值DXGI_SWAP_EFFECT_FLIP_DISCARD即可,在后面的文章中再細聊這個參數的作用,對于一般的應用來說,這樣就已經足夠了。

有了交換鏈,那么我們就需要知道當前被繪制的后緩沖序號是哪一個(注意這個序號是從0開始的,并且每個后緩沖序號在新的D3D12中是不變的),調用下面的代碼就可以得到當前繪制的后緩沖的序號:

GRS_THROW_IF_FAILED(pISwapChain1.As(&pISwapChain3)); //6、得到當前后緩沖區的序號,也就是下一個將要呈送顯示的緩沖區的序號 //注意此處使用了高版本的SwapChain接口的函數 nFrameIndex = pISwapChain3->GetCurrentBackBufferIndex();

這段代碼中我們調用了WRL::ComPtr的函數As,其內部就是調用QueryInterface的經典COM方法,沒什么稀奇的。我們是使用低版本的SwapChain接口得到了一個高版本的SwapChain接口。目的是為了調用GetCurrentBackBufferIndex方法,而從其來自高版本接口可以知道,這是后來擴展的方法。主要原因就是現在翻轉繪制緩沖區到顯示緩沖區的方法更高效了,直接就是將對應的后緩沖序號設置為當前顯示的緩沖序號即可,比如原來顯示的是序號為1的緩沖區,那么下一個要顯示的緩沖區就是序號2的緩沖區,如果為2的緩沖區正在顯示,那么下一個將要顯示的序號就又回到了0,當然這里假設緩沖區數量是3,我們的例子中就正好是3個緩沖區,所以緩沖區的序號就正好是緩沖區數量的余數(MOD)。其他情況依此類推。

前版本的D3D中,拿到了交換鏈的后緩沖之后,我們就需要創建一個叫做Render Target View(簡稱RTV,最好背下來)Descriptor渲染目標視圖描述符的對象及接口。類似僵尸片中的各種靈符一樣,描述符也有一些神奇的功用,比如拿RTV描述符貼在一塊紋理上,它立刻就變成了RTV。它的作用是讓GPU能夠正確識別和使用渲染目標資源,其本質就是描述緩沖區占用的顯存,所以從本質上講只要是可以作為一整塊顯存來使用的緩沖都可以作為渲染目標, 比如有一些高級渲染技法中就需要渲染到紋理上,當然我們要做的也很簡單就是給那些紋理貼上RTV符即可。因為GPU內部本質上是一個巨大的SIMD架構的處理器,同時考慮到很多微核(可以理解為就是GPU中的多個ALU單元)并行執行的需要,所以它在存儲器的使用上是非常細化的,在使用某段存儲(內存或顯存)之前,就需要通過類似描述符之類的概念等價物說清楚這段存儲的用途、大小、讀寫屬性、GPU/CPU訪問權限等信息。因為創建交換鏈的主要目的是用它的緩沖區作為3D圖形渲染的目標,所以我們就需要用渲染目標視圖描述符告訴GPU這些緩沖區是渲染目標。

六、創建創建RTV描述符堆和RTV描述符

在D3D12中加入了一個重要的概念——描述符堆,首先看到堆這個詞我們應當聯想到內存管理(如果你想到了數據結構,說明你基本功還可以,這里我們討論的是D3D12,跟數據結構關系不大,所以應當正確聯想到內存管理中的堆棧);其次在D3D12中凡是套用了堆(Heap)這個概念的對象,目前應當將他們理解為固定大小的數組對象,而不是真正意義上可以管理任意大小內存塊并能夠自動伸縮大小的內存堆棧。未來不好說D3D中會不會實現全動態的顯存堆管理。在目前我們就理解為它是數組即可。

這也是D3D12在功能上較之前版本的D3D接口擴展出來的重要概念——顯存管理(或稱之為存儲管理更合適,這里用顯存是為了強調與傳統系統內存管理的區別)的一個重要表現。

渲染目標視圖描述符堆的代碼如下:

D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {}; rtvHeapDesc.NumDescriptors = nFrameBackBufCount; rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;GRS_THROW_IF_FAILED(pID3DDevice->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&pIRTVHeap))); //得到每個描述符元素的大小 nRTVDescriptorSize = pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

上述代碼在風格上依舊是結構體做參數調用函數的老套路。結構體初始化時Type參數賦值為D3D12_DESCRIPTOR_HEAP_TYPE_RTV,表示我們將創建的堆(數組)是用來存儲RTV描述符的堆(數組)。通過NumDescriptors參數我們就指定了堆的大小(實質上是數組元素的個數),Flags參數暫時不介紹,像這里賦值就OK。堆創建完了之后我們就調用D3D設備接口的GetDescriptorHandleIncrementSize方法得到實際上每個RTV描述的大小,也就是數組元素的真實大小,你可以理解為我們相當于調用了一個sizeof運算符,得到了一個不知道里面存了些啥的復雜結構體的大小,當然計量單位是字節。

有了RTV描述符堆(數組),那么我們就可以創建RTV描述符了,代碼如下:

CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(pIRTVHeap->GetCPUDescriptorHandleForHeapStart()); for (UINT i = 0; i < nFrameBackBufCount; i++) {GRS_THROW_IF_FAILED(pISwapChain3->GetBuffer(i, IID_PPV_ARGS(&pIARenderTargets[i])));pID3DDevice->CreateRenderTargetView(pIARenderTargets[i].Get(), nullptr, rtvHandle);rtvHandle.Offset(1, nRTVDescriptorSize); }

代碼中首先我們使用了一個來自D3Dx12.h中擴展的類CD3DX12_CPU_DESCRIPTOR_HANDLE來管理描述堆的當前元素指針位置,概念上你可以將這個對象理解為一個數組元素迭代器,雖然它的名字被定義成了HANDLE但我覺得使用iterator更確切。

接下來就是一個循環,循環上界就是我們創建的交換鏈中的后緩沖個數,在我們的例子中是3個后緩沖,因此這個循環會執行三次,創建三個RTV描述符。這里要特別注意的是,緩沖的序號和對應描述符的數組元素序號要保持一致,當然代碼中已經保證了這一點。循環最后一行Offset則暴漏了這里其實是在操作數組的本質。

七、創建根簽名對象接口

再接下來我們就需要創建一個更重要的對象了,就是根簽名。在這里首先你要為你能堅持看到這里給自己點個贊,因為D3D12中為完成渲染加入了太多的概念和對象,當然這些概念的加入都是為了提高性能而設計的。當然能看到這里的前提是再次提醒你已經看過我之前寫的關于D3D12的相關博客文章了。

從總體上來理解D3D12的話,就是在D3D12中加入了存儲管理、所有的調用都是異步并行的方式并且為管理異步調用而加入了同步對象。這里提到的根簽名則是為了整體上統一集中管理之前在D3D11中分散在各個資源創建函數參數中的存儲Slot和對應寄存器序號的對象。也就是說在D3D12中我們不用在創建某個資源時單獨在其參數中指定對應哪個Slot(暫時翻譯為存儲槽)和寄存器及序號了,而是統一在D3D12中用一個根簽名就將這些描述清楚。

熟悉Shader的話,你就知道我們在寫shader的時候有時候就需要指定每種數據,比如常量緩沖、頂點寄存器、紋理等資源是對應哪個存儲槽和寄存器,及序號的。對于存儲槽我們可以理解為一條從內存向顯存傳輸數據的通道,想象成一個流水槽(如果你懂點點PCIe的話,可以將之理解為PCIe的一條通道)。而對于這里的寄存器就不是指CPU上的寄存器了,而是指GPU上的寄存器。根據前面的描述現代的GPU在概念上可以理解為一個巨大的SIMD架構的處理器,由于為高效并行執行指令的需要,它在存儲管理上是非常細分的,甚至它的寄存器也是細化分類的,有常量寄存器、紋理寄存器、采樣器等等,并且每類寄存器都有若干個,以序號來索引使用,所以在我們從CPU加載資源到GPU上時就需要詳細指定那些數據從哪個槽上灌入到哪個序號的寄存器上。

而要達到這個目的就需要在兩個方面明確指定這些參數,一方面是從程序代碼(CPU側)調用D3D12相關接口創建資源時指定傳輸參數(存儲槽序號,寄存器序號),另一方面在Shader代碼中指定接收參數,并指定Shader代碼中具體訪問哪個存儲槽,哪個寄存器中的數據。或者更準確的說一般Shader中就不用管是哪個Slot了,因為數據肯定都已經到了顯存中,Shader中實質關心的只是寄存器和其序號。

或者直接的說根簽名就是描述了整個的用于渲染的資源的存儲布局。在MSDN官方的描述中也是這樣說的:根簽名是一個綁定約定,由應用程序定義,著色器使用它來定位他們需要訪問的資源。

最終在D3D12中之所以要統一管理這些的目的就是為了方便形成一個統一的管線狀態對象(Pipeline States Object PSO),有了管線狀態對象,在渲染時,只要資源加載正確,我們只需要在不同的渲染過程間切換設置不同的渲染管線狀態對象即可,而在傳統的D3D管線編碼中,這些工作需要一個個設置管線狀態,然后自己編寫不同的管線狀態管理代碼來實現,在代碼組織上過于分散和復雜,同時也不利于復雜場景渲染時快速切換不同渲染管線狀態的需要。

而根簽名對象則是總領一條管線狀態對象存儲綁定架構的總綱。在我們這里的例子中,因為我們沒有用到復雜的數據,只是為了畫一個三角形,并且沒有紋理、沒有采樣器等等,所以我們就創建一個都是0索引(序號是1的意思,搞C的你應該明白)的一個根簽名,代碼如下:

CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); ComPtr<ID3DBlob> signature; ComPtr<ID3DBlob> error;GRS_THROW_IF_FAILED(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));GRS_THROW_IF_FAILED(pID3DDevice->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&pIRootSignature)));

這段代碼中我們又用到一個d3dx12.h中擴展的類CD3DX12_ROOT_SIGNATURE_DESC來定義一個根簽名的結構,然后編譯一下,再創建根簽名對象及其接口。這里的根簽名參數已經說清楚了我們需要傳遞網格數據到管線中進行渲染并且要定義對應的Input Layout格式對象。

默認情況下Slot和寄存器序號都使用0。關于根簽名的詳細內容我們放在后續的教程中專門來細講,這里先理解到這樣就可以了。需要補充的就是,根簽名的創建方式主要有兩種,一種是使用腳本方式來編寫一個根簽名(VS中是擴展名為HLSLi的文件),另一種就是我們這里使用的定義一個結構體再編譯生成一個根簽名的方式。我們示例中使用的是第二種方法,我的建議是兩種方法都掌握,這個我們后面的教程都會講到。但是我們必須要清晰的認識到使用結構體然后調用編譯函數在代碼中編譯的方法的巨大優勢,因為這種形式很方便我們定義自己的根簽名腳本,也就是腳本化。比如你可以用XML文件定義一個根簽名結構,然后加載使用,這樣就不會被禁錮于純代碼或純HLSL腳本的方式中。而是可以自己擴展更靈活和易轉換的方式。

八、編譯Shader及創建渲染管線狀態對象接口

至此,經過了這么多對象創建初始化工作后,我們終于可以看到一點曙光了,接下來我們就要創建渲染管線狀態對象了,在D3D12以前,雖然有渲染管線狀態這樣一個概念,但在接口上它的所有狀態設置都是按照渲染階段來分不同的函數直接放在Device對象接口或Context對象接口中。現在渲染管線狀態就被獨立了成了一個對象,并用ID3D12PipelineState接口來代表。

從概念上講,渲染管線狀態就是把原來的Rasterizer State(光柵化狀態)、Depth Stencil State(深度、蠟板狀態)、Blend State(輸出alpha混合狀態)、各階段Shader程序(VS、HS、DS、GS、PS、CS),以及輸入數據模型(Input Layout)等組合在一個對象中,從而形成一個完整的可重用的Pipeline State Object(PSO 渲染管線狀態對象)。這樣我們就從每次不同的渲染需要設置不同的管線狀態參數的過程中解放了出來,在實際使用時,只需要在開始時初始化一堆PSO,然后根據不同的渲染需要在管線上設置不同的PSO即可開始渲染,渲染部分代碼就被大大簡化了,從而使游戲引擎的封裝實現也大大簡化了。

實際上這也是符合現代大多數引擎中關于渲染部分的封裝思路的。因為現代光柵化渲染的很多理論算法都已經很成熟很完備了,完全有條件統一為幾大類不同的主流PSO,然后重用即可。甚至我們現在在使用一些現代化的游戲引擎開發游戲時基本都不用關注渲染部分的組件,引擎自帶的組件已經很強悍了。所以這也是很多會用引擎開發游戲的開發人員往往對渲染部分了解和關注甚少的原因之一。

在D3D12中通過PSO對象,我們也具備了直接封裝實現具有現代化水平的引擎渲染部件的能力,當然這需要你在封裝設計上有一定功力,在這里我依然強調一下,這個教程中不講封裝,只講基礎。所以我們就直接看看最原始的調用代碼是什么樣子的:

ComPtr<ID3DBlob> vertexShader; ComPtr<ID3DBlob> pixelShader; #if defined(_DEBUG)UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #elseUINT compileFlags = 0; #endifTCHAR pszShaderFileName[] = _T("D:\\Projects_2018_08\\D3DPipelineTest\\D3D12Trigger\\Shader\\shaders.hlsl");GRS_THROW_IF_FAILED(D3DCompileFromFile(pszShaderFileName, nullptr, nullptr, "VSMain", "vs_5_0", compileFlags, 0, &vertexShader, nullptr));GRS_THROW_IF_FAILED(D3DCompileFromFile(pszShaderFileName, nullptr, nullptr, "PSMain", "ps_5_0", compileFlags, 0, &pixelShader, nullptr));D3D12_INPUT_ELEMENT_DESC inputElementDescs[] = {{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }};D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) }; psoDesc.pRootSignature = pIRootSignature.Get(); psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get()); psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get()); psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); psoDesc.DepthStencilState.DepthEnable = FALSE; psoDesc.DepthStencilState.StencilEnable = FALSE; psoDesc.SampleMask = UINT_MAX; psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; psoDesc.NumRenderTargets = 1; psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; psoDesc.SampleDesc.Count = 1;GRS_THROW_IF_FAILED(pID3DDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pIPipelineState)));

這段代碼在結構上比較清晰,也很容易理解,在編譯完Shader程序后,就通過初始化一個PSO結構體,然后調用CreateGraphicsPipelineState創建一個PSO對象及其代表接口ID3D12PipelineState。

從PSO結構體的初始化上,你應該看到了D3D12與原來的D3D接口的明顯不同,過去這些參數首先是通過調用一個個函數來設置的,并且按照不同的渲染階段冠以IA、VS、PS、RS、OM等前綴名字,且不說它們看起來有多別扭,光是每次在渲染循環中調用一遍就夠復雜了,如果你少設置了一個狀態,有可能就引起奇奇怪怪的后果,尤其是復雜的場景中,每種物體都可能需要不同的渲染手法,每種渲染手法就需要這堆函數的不同順序的組合調用,而每次渲染完一個,你還需要挨個清理掉,以便不影響后續的渲染調用。如果你熟悉Windows GDI的話,你就會發現之前的D3D接口在渲染狀態編碼的風格上與其十分類似,都是先設置一堆狀態,然后繪制,最后再還原這一堆狀態到之前的樣子。代碼上一樣的令人作嘔,因為一個不留神內存泄漏不說,并且渲染的結果通常也不會正確,所以調試起來像噩夢一樣。

另一方面,過去的那種通過不同函數來設置渲染狀態參數的方法,非常不利于固化(存儲或加載)或腳本化封裝(自定義腳本來靈活封裝渲染管線),顯得很不靈活。而這些對于現代化設計的游戲引擎來說是非常重要的特征。喜大普奔的是,在D3D12中,正如你所見的這些都只是初始化一個巨大的結構體即可。

PSO對象還帶來一個更巨大的好處就是非常有利于多線程渲染,可以在不同的渲染線程之間方便的共享不同的PSO對象,而不用考慮怎樣去靈活的封裝這些渲染狀態參數以便于共享。

其中shader簡單可見如下:

struct PSInput {float4 position : SV_POSITION;float4 color : COLOR; };PSInput VSMain(float4 position : POSITION, float4 color : COLOR) {PSInput result;result.position = position;result.color = color;return result; }float4 PSMain(PSInput input) : SV_TARGET {return input.color; }

九、加載待渲染數據(三角形頂點)

有了PSO我們就可以正式開始加載網格數據并開始渲染了。我想現在你應該猜到在D3D12中渲染也應該沒那么簡單吧?如果你猜到了,那說明通過前面的學習你已經有點適應D3D12為我們帶來的空前的復雜性了,這很好,這說明你基本已經快爬到學習曲線的坡頂了。

開始正式渲染之前的最后一步就是加載資源了。因為我們要繪制一個三角形,所以我們就直接在代碼中準備好這個三角形的數據,代碼如下:

// 定義三角形的3D數據結構,每個頂點使用三原色之一 GRS_VERTEX triangleVertices[] = {{ { 0.0f, 0.25f * fAspectRatio, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },{ { 0.25f * fAspectRatio, -0.25f * fAspectRatio, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },{ { -0.25f * fAspectRatio, -0.25f * fAspectRatio, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } } };const UINT vertexBufferSize = sizeof(triangleVertices);GRS_THROW_IF_FAILED(pID3DDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),D3D12_HEAP_FLAG_NONE,&CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),D3D12_RESOURCE_STATE_GENERIC_READ,nullptr,IID_PPV_ARGS(&pIVertexBuffer)));UINT8* pVertexDataBegin = nullptr; CD3DX12_RANGE readRange(0, 0); GRS_THROW_IF_FAILED(pIVertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin))); memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices)); pIVertexBuffer->Unmap(0, nullptr);stVertexBufferView.BufferLocation = pIVertexBuffer->GetGPUVirtualAddress(); stVertexBufferView.StrideInBytes = sizeof(GRS_VERTEX); stVertexBufferView.SizeInBytes = vertexBufferSize;

上面的代碼中,對于三角形的頂點,我們使用了三角形外接圓半徑的形式參數化定義了它,這樣方便我們調整三角形大小看效果。

接著一個重要的概念就又是與存儲管理有關了,在這里就是資源管理了。基本上在D3D12渲染過程中需要的數據都可以被稱為資源,在這段代碼中我們需要的資源就是三角形的頂點數據,使用的函數是CreateCommittedResource。

首先在一般的場合下,比如我們這里的示例中都可以使用這一個函數來創建資源。其次這個函數是為數不多的幾個內部還同步的D3D12函數之一,也就是當這個函數返回時,實際的資源也就分配好了,與我們在之前版本D3D中用的方法類似。因此這個函數返回后,我們就可以像傳統做法一樣,map然后memcpy數據最后unmap就完成了數據從CPU內存向顯存的傳遞。

幸運的是,CD3DX12_RESOURCE_DESC這個結構體與它在D3D11或D3D9中的前輩很相似,你應該已經很熟悉它了。在后面的系列教程中我們還會詳細講解它的用法。而另一個類CD3DX12_HEAP_PROPERTIES就先暫時不要糾結了,知道在這里它就是為了封裝D3D12_HEAP_TYPE_UPLOAD這個屬性即可。Upload的意思就是從CPU內存上傳到顯存中的意思。

代碼的最后三行,就是通過結構體對象描述清楚了這個資源的視圖,目的是告訴GPU被描述的資源實際是Vertex Buffer。這也與之前的D3D版本中的做法有些區別。在傳統的D3D中,是通過調用Device接口的函數明確的創建一個資源視圖對象的,而在D3D12中只需要在結構體對象中說明即可,主要目的就是為了能夠統一實現一個可固化或腳本化的靈活的資源視圖對象,與之前說的PSO對象結構體對象來描述的目的相類似。

十、異步渲染原理及命令列表詳解

我們做完了渲染管線及渲染管線狀態準備工作,同時也“加載”完了我們需要渲染的三角形的數據,終于可以開始渲染了。當然因為D3D12本質上為了支持多線程渲染而采取了異步設計策略的緣故,渲染也與之前版本的D3D有較大的差別。

首先我們需要接觸的有一個新的概念就是命令列表,它的接口是

ID3D12GraphicsCommandList。其實在D3D11中它的概念等價物就是 Deferred Device Context,而相應的D3D12中的Command Queues就對應D3D11中的Immediate Device Context。

顧名思義,命令列表其實就是為了記錄CPU發給GPU的圖形命令的,因此它里面的方法函數就是一個個圖形命令了,我們逐一調用命令函數,它就按照我們調用的順序記錄了這些圖形命令。

在D3D12中所有的圖形命令函數即ID3D12GraphicsCommandList的接口方法都是異步的,就是說一調用就返回,甚至很多方法連返回值都沒有,調用時不能判定函數調用是否正確,因為調用的時候函數并沒有真正執行,僅是被記錄,這個過程被稱為錄制(Record)。

最終當所有的命令都記錄完畢后,必須發送至Command Queue中ExecuteCommandList方法后,也就是將命令列表作為參數傳給這個函數,一個命令列表才能去執行。

為了安全控制,也就是防止因多線程渲染帶來的不必要沖突,命令列表的狀態被分為:錄制狀態和可Execute狀態(也叫關閉狀態),命令列表對象通常處在兩個狀態之一。

通常一個命令列表在被創建時是默認處于錄制狀態的,此狀態下是不能被執行的。錄制完成后我們調用命令列表對象的Close方法關閉它,它就變成了可執行狀態,就可以提交給Command Queue(命令隊列)的ExecuteCommandList方法去執行,待執行完畢后我們又調用命令列表的Reset方法使它恢復到記錄狀態即可。當然Reset之后,命令列表之前記錄的命令也就丟失了,嚴格來說是這些命令被交給命令隊列去執行了,而命令列表不在記錄原來的命令了。要理解這個概念,讓我們想象命令列表就是我們去飯館吃飯時服務員用于填寫你點的菜的菜單,而命令隊列就是飯館的廚房,當我們點完菜后服務員就將菜單交給了廚房,而他的手里就又是新的一頁空白菜單,準備下一位客戶點菜了。

理解了命令列表,那么我們還需要在剛才那個飯館點菜的模型示例基礎上進一步來思考一下,那就是我們點菜用的菜單紙是要提前準備的,并且它是需要動態分配的,雖然一般的都是固定大小的,但是也會有客人只點幾樣小菜,而整個菜單就有很多空白浪費了,也有很多客人可能因為點很多菜而使用了幾頁菜單紙。在D3D12中,用于最終記錄這些命令的“菜單紙”就是命令分配器對象,其接口是ID3D12CommandAllocator。這也是D3D12中加入的諸多關于存儲管理概念對象中的一個。從本質上講,其實不論什么圖形命令,最終都是需要GPU來執行,那么這個分配器我們可以理解為本質上是用來管理記錄命令用的顯存的。不過幸運的是目前這個接口的細節在D3D12的調用過程中是不用過多關注的,因為它目前的設計只是為了體現了命令分配這個概念,實質上并沒有什么具體直接的方法需要我們去調用,我們要做的只是說需要創建它,并將它和某個命令列表捆在一起即可。

當然命令分配器和命令列表最好是一對一的,用剛才飯館點菜模型示例來說,我們肯定希望每個服務員都單獨拿一份菜單來給你點菜,所以以此來理解的話,你就明白一對一的意義了。我相信誰都不會喜歡自己點的菜和別人點的菜混在一起吧?

從另一方面說,之所以要這么一個有點像純概念式的對象接口來表達分配命令存儲管理的概念的真正意義何在呢?那就是為了多線程渲染。因為我已經不止一次的強調過D3D12中加入并強化的核心概念就是多線程渲染。注意真正引入多線程渲染是在D3D11中,只不過D3D11中仍然保留了同步化的渲染支持。同時D3D11的多線程渲染貌似用的并不多,也沒什么名氣,也或者是我孤陋寡聞了。

而在D3D12中,管你喜歡不喜歡,渲染已經完全是多線程化了。徹底整明白多線程渲染的基本編碼方式就是這篇教程的核心目的了,這也是我們徹底征服D3D12的基礎中的基礎。所以這里我也要多費些篇幅。

了解多線程編程的開發者,應該很清楚一個概念,那就是,從本質上說多線程其實不難。比如在Windows上調用CreateThread(推薦__beginthread)就可以創建線程,而真正的難點在于如何安全的在多線程環境下使用內存。而在D3D12中,實質為了徹底實現強悍的多線程渲染,最終是加入了大量的存儲管理的概念,編程的復雜性也來源于此。在這里實際命令分配器就是典型的存儲管理概念的體現。在D3D12中我們一般是為每一個渲染線程創建一個命令列表和一個命令分配器,這樣就像我們舉得飯館點菜的例子中一樣,大一點的上規模的餐館中,一般每桌都有專門的服務員為你點菜,并且人手一份菜單,而廚房則通常會有多位不同的廚師負責不同的菜品的烹飪。從這里也可以看出為什么D3D12中非要加入多線程渲染的,那就是為了高效率、高品質、高并行,也就是說你不是再像以前一樣開一個也許只有一兩個廚師三四個服務員的小餐館了,而是像現在這樣要開大型的餐廳了,幾乎每個餐桌都有專門的服務員點菜(多線程生成多個命令列表),后臺的廚房中則是n個廚師在并行的為不同桌的客人烹制菜品,甚至廚師的分工都被細化了,有些負責烹制西餐、有些負責粵菜、有些負責涼菜等等(多引擎),想象一下使用了多線程渲染之后,你的引擎也是在異曲同工的繪制復雜大型的場景的情景。而這一切都在高效的D3D12接口的支持下,有條不紊的進行。

十一、創建命令分配器接口、命令列表接口和柵欄對象接口

看過上面那段介紹,在概念上你已經消化吸收了多線程渲染的基本原理。當然在我們現在的例子中,還沒有真正用起多線程渲染,將來我們就會用到。提前說這些的真實目的是因為D3D12本質全是異步的,所以我們還是需要將我們這個單線程例子,按照多線程渲染的套路來編寫,這也是D3D12編程比較復雜的一個體現。首先我們要像下面這樣創建一個命令分配器,然后在創建一個命令列表:

// 12、創建命令列表分配器 GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&pICommandAllocator))); // 13、創建圖形命令列表 GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, pICommandAllocator.Get(), pIPipelineState.Get(), IID_PPV_ARGS(&pICommandList)));

從上面代碼再結合我們之前講過的多引擎知識大家應該看明白我們創建了一個“直接”的命令分配器和命令列表,其“直接”的含義與直接命令隊列的含義對應,就是用來分配和記錄所有種類的命令的。同時在創建命令列表時,我們還要指出它對應的命令分配器對象的指針,這也就是一一對應含義的體現。

接下來因為我們本質上還是在用異步的思路來調用渲染的過程,所以我們就還需要創建控制CPU和GPU同步工作的同步對象——柵欄(Fence),代碼如下:

// 14、創建一個同步對象——圍欄,用于等待渲染完成,因為現在Draw Call是異步的了 GRS_THROW_IF_FAILED(pID3DDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&pIFence))); n64FenceValue = 1; // 15、創建一個Event同步對象,用于等待圍欄事件通知 hFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); if (hFenceEvent == nullptr) {GRS_THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError())); }

上述代碼可以看出創建一個柵欄很簡單,當然為了完成真正的同步控制(這里指CPU與GPU渲染間的同步,以后所說的同步都要結合上下文明白我們指的是什么同步),我們還要準備一個稱為圍欄值的64位無符號整數變量,在示例中就是n64FenceValue,和一個Event(事件)系統內核對象。在這里我們都是先準備好他們,后面我們就會真正用到它。

十二、渲染

終于最后一步就是我們進入消息循環調用渲染了,代碼如下:

//創建定時器對象,以便于創建高效的消息循環 HANDLE phWait = CreateWaitableTimer(NULL, FALSE, NULL); LARGE_INTEGER liDueTime = {};liDueTime.QuadPart = -1i64;//1秒后開始計時 SetWaitableTimer(phWait, &liDueTime, 1, NULL, NULL, 0);//40ms的周期 //開始消息循環,并在其中不斷渲染 DWORD dwRet = 0; BOOL bExit = FALSE; while (!bExit) {dwRet = ::MsgWaitForMultipleObjects(1, &phWait, FALSE, INFINITE, QS_ALLINPUT);switch (dwRet - WAIT_OBJECT_0){case 0:case WAIT_TIMEOUT:{//計時器時間到//開始記錄命令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++);}break;case 1:{//處理消息while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){if (WM_QUIT != msg.message){::TranslateMessage(&msg);::DispatchMessage(&msg);}else{bExit = TRUE;}}}break;default:break;}}

代碼中使用了一個精確定時的消息循環。

這里重點講一下資源屏障的用法和意義。資源屏障的原理在之前Vulkan的文章中已經有過很形象的講解了。在這段代碼中有兩處用到資源屏障,我們可以看到資源屏障的運用其實也很簡單,它核心的思想就是追蹤資源權限的變化,從而同步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中,也是其基本結構的擴展,真實的結構體中就是要求我們指明是那塊資源,并且指明之前操作要求的訪問狀態是什么,以及之后的訪問狀態是什么,而這個類的封裝就使初始化這個結構體更加的簡便和直觀了。

運行即可見如下結果:

總結

以上是生活随笔為你收集整理的DirectX12_入门之三角形的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。