Direct3D的初始化(上)
學習目標
- 了解Direct3D在3D編程中相對於硬件所扮演的角色
- 理解組件對象模型COM在Direct3D中的作用
- 掌握基礎的圖像學概念,例如2D圖像的存儲方式,頁面翻轉,深度緩沖,多重采樣以及CPU和GPU之間的交互
- 學習使用性能計數器函數,依次讀取高精度計時器的數值
- 了解Direct3D的初始化過程
- 熟悉本書應用程序框架的整體結構,在后續的演示程序中可以經常看到應用程序框架的整體結構
4.1預備知識
要學習Direct3D的初始化流程,我們需要了解一些基本的圖形學概念以及Direct3D中常用數據類型的相關知識,本節將會着重講解這些細節,以防止在后面講解Direct3D的初始化流程中被這些細枝末節影響
4.1.1Direct3D 12概述
通過Direct這種底層圖形應用程序編程接口,可以在應用程序中對圖形處理器進行控制和編程,便可以借此以硬件加速的方式渲染出虛擬的3D場景。
例子:如果要向GPU提交一個清除某渲染目標(例如清屏)的命令,我們可以調用Direct3D中的ID3D12GraphicsCommandList::ClearReanderTargetView方法,然后Direct3D層和硬件驅動會將此Direct3D命令轉換為系統中GPU可以執行的本地機器指令。只要GPu支持當前所用的Direct3D版本,我們就可以不用考慮它的具體規格和硬件控制層面的實現細節
Driect3D 12新特性:12相對於11而言,在性能方面大大減少了CPU開銷的同時,又改進了對多線程的支持。因此,Direct3D 12的API更接近底層,開發人員需要付出更多時間才能完成一個項目的開發,不過這樣帶來的回報就是:性能的提升
4.1.2組件對象模型
組件對象模型(Component Object Model,COM)是一種令DriectX不受編程語言限制,而且可以使他向后兼容的技術。我們一般把COM對象視為一種接口,但考慮到當前編程的目的,我們便將他是做一個c++類來使用。如果要獲取指向某COM接口的指針,需要借助特定函數或另一COM接口的方法,而不是使用C++的關鍵字new去創建一個COM接口,使用完某接口之后,我們應該使用Release方法,而不是使用c++的關鍵字delete。
Windows運行時庫(Windows Runtime Library,WRL)提供了Microsoft::WRL::ComPtr類,它相當於是COM對象的智能指針。當一個ComPtr實例超出作用域范圍時,它會自動調用Release方法自動銷毀實例。本書常用的3個ComPtr方法如下
//1、Get:返回一個指向此底層COM接口的指針,此方法常用於把原始的COM接口指針作為參數傳遞給函數
ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(),nullptr));
//2、GetAddressof:返回指向此底層COM接口指針的地址,此方法即可利用函數參數返回COM接口的指針(函數輸出),例如
D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1,
serizlizedRootSig.GetAddressOf(),
errorBlod.GetAddressOf());
//3、Reset:將Comptr實例設置為nullptr(將指針置為空)
4.1.3紋理格式
2D紋理是一種由數據元素構成的矩陣,它的用途之一是存儲2D圖像數據。在這種情況之下,紋理中的每一個元素存儲的都是一個像素的顏色。但紋理的用處並非作為存儲2D圖像數據的容器。后續會繼續介紹。並不是任意類型的數據元素都可以用於組成紋理,它只能存儲DXGI_FORMAT枚舉類型中描述的特定格式的數據元素。下面是一些相關的格式實例
//1、每個元素由3個32位浮點數分量構成
DXGI_FORMAT_R32G32B32_FLOAT
//2、每個元素由4個16位分量構成,每個分量都被影映射到0-1區間
DXGI_FORMAT_R16G16B16A16_UNORM
//3、每個元素由2個32位無符號整數分量構成
DXGI_FORMAT_R32G32_UINT
//4、每個元素由4個8位無符號分量構成,每個分量都被映射到0-1區間
DXGI_FORMAT_R8G8B8A8_UNORM
//5、每個元素由4個8位有符號分量構成,每個分量都被映射到-1-1區間
DXGI_FORMAT_R8G8B8A8_SNORM
//6、每個元素由4個8位有符號整數分量構成,每個分量都被映射到-128-127區間
DXGI_FORMAT_R8G8B8A8_SINT
//7、每個元素由4個8位無符號整數分量構成,每個分量都被映射到映射到0-255區間
DXGI_FORMAT_R8G8B8A8_UINT
4.1.4交換鏈和頁面翻轉
為了避免動畫中出現畫面閃爍,我們需要利用硬件管理的兩種紋理緩沖區,即前台緩沖區和后台緩沖區。
前台緩沖區:存儲當前顯示在屏幕上的圖像數據
后台緩沖區:存儲動畫的下一幀繪制
當后台緩沖區的動畫繪制完成之后,兩種緩沖區的角色互換,后台緩沖區轉換為前台緩沖區呈現新的畫面,而前台緩沖區則作為展示動畫的下一幀成為后台緩沖區,等待填充數據。前后台緩沖區互換的操作稱為呈現。
前台緩沖區和后台緩沖區構成的交換鏈,在Direct3D中用IDXGISwapChain接口來表示,這個接口不僅存儲了其那台緩沖區和后台緩沖區這兩個紋理,還提供了修改緩沖區的大小(IDXGISwapChain::ResizeBuffers)和呈現緩沖區的內容(IDXGISwapChain::Present)的方法,在一些情況下,我們甚至可以使用三緩沖機制。
4.1.5深度緩沖
深度緩沖區這種紋理資源存儲的不是圖像數據,而是一些特定像素的深度信息。深度值的范圍為0.0-1.0,其中0.0為觀察者在視椎體中看到的離自己最近的物體。深度緩沖區的元素和后台緩沖區的元素是一一對應的,所以如果后台緩沖區有的分辨率為12801024,則深度緩沖區中有12801024個元素。
為了確定不同物體的像素的前后順序,Direct3D采用了深度緩沖的技術。比如當我們要繪制三個物體時,我們首先要清除緩沖區,對像素以及它的深度元素進行初始化,然后依次對三個物體進行深度測試,當找到具有更小深度值的像素的時候,計算機才會對觀察窗口內的像素以及其在深度緩沖區中對應的深度值進行更新。
總結:深度緩沖技術的原理是計算每一個像素的深度值,然后進行深度測試,然后對競爭寫入后台緩沖區的同一像素的多個像素深度值進行比較,然后將最小的像素深度值所對應的像素寫入后台緩沖區中。
附錄:
由於深度緩沖區也是一種紋理資源,所以一定要用對應的數據格式去創建它,下面是一些常用的深度緩沖區數據格式:
DXGI_FORMAT_D32_FLOAT_S8X24_UINT;
DXGI_FORMAT_D32_FLOAT;
DXGI_FORMAT_D24_UNORM_S8_UINT;
DXGI_FORMAT_D16_UNORM;
4.1.6資源與描述符
視圖(view)和描述符(descriptor)是同義詞,視圖是Direct3D先前版本的常用術語,不過它現在只出現在Direct3D12的部分API中,我們只要清楚,視圖和描述符是用一個東西即可
在渲染處理過程中,GPU可能會對資源進行讀寫操作,在發出繪制命令之前,我們需要把與本次繪制調用的相關資源鏈接到渲染流水線上,部分資源可能每次繪制都會進行更新,所以我們要在每次繪制之前都對鏈接的資源進行更新。GPU資源並不是直接和渲染流水線相互鏈接,而是通過一種名為描述符的對象來實現對它的間接引用。我們可以把描述符看成一種對送往GPU的資源進行描述的輕量級結構。
為什么要引用描述符這個中間層呢?
因為GPU資源是指是一些普通的內存塊,由於資源的這種通用性,同一個資源可以在渲染流水線上的不同階段使用,但是也產生了一些問題。
- 不論是作為渲染目標,還是深度緩沖區或者模板緩沖區,僅靠資源本身是無法讓計算機識別出來的
- 我們有時希望將資源中的部分數據鏈接到渲染流水線中,這僅靠資源本身也是無法實現的
- 當一個資源使用的是無類型格式時,GPU甚至無法識別該資源的具體格式
為了解決上述問題,我們便引入了描述符這個概念
每個描述符都有一種具體的類型,這些類型指定了資源的具體作用。以下是一些常用的描述符類型:
- CBV/SRV/UAV:常量緩沖區視圖(constant buffer view)、着色器資源視圖(shader resource view)、無序訪問視圖(unordered access view)
- 采樣器(sampler)描述符描述的是采樣器資源(用於紋理貼圖)
- RTV描述符表示的是渲染目標視圖資源(render target view)
- DSV描述符表示的是深度/模板視圖資源(depth/stencil view)
描述符堆:描述符堆里面有一系列的描述符(可以將其視為描述符數組),本質上存放用戶程序中某種特定類型描述符的一塊內存。我們要為每一種描述符創建出單獨的描述符堆(為每一種類型的描述符創建一個數組),也可以為同一種描述符創建多個描述符堆。
4.1.7多重采樣技術的原理
由於屏幕中顯示的像素不是無窮小的,所以並不是任意一條直線都可以在顯示器中平滑的顯示出來,大部分都會出現“階梯”效果。為了改善這個問題,我們可以將提高顯示器的分辨率,使“階梯”效果不那么容易被用戶發現。
但在大多數情況下,我們不能夠提升顯示器的分辨率,所以我們一般會運用各種反走樣的技術,其中有一種技術叫做超級采樣(SSAA),它是使用4倍於屏幕分辨率大小的后台緩沖區和深度緩沖區,在后台緩沖區將要把數據調往顯示器顯示的時候,會將后台緩沖區中每四個像素為一組進行解析。這種方法是通過軟件的方式提高畫面的分辨率。超級采用是一種需要耗費高額開銷的操作,在Direct3D中,還支持一種效果和開銷都較為折中的反走樣技術,叫做多重采樣(MSAA),現假設使用4X多重采樣,並同樣使用4倍的后台緩沖區和深度緩沖區,這種技術不用對每一個像素進行計算,它計算一次中心像素的顏色,然后基於可視性和覆蓋性,將得到的信息分享給其他子像素(使一次計算,受惠4個像素,從而減少開銷)。
4.1.8利用Direct3D進行多重采樣
略
4.1.9功能級別
從Direct3D 11開始便有了功能級別(D3D_FEATURE_LEVEL)的概念,功能級別為不同級別所支持的功能進行了嚴格的界定,一款支持DX11的GPU,除了一些特定的功能之外,其他功能必須存在。這樣可以方便程序員使用對應的API,否則程序員在使用一些API時還要先對該硬件是否支持該功能進行檢測。為了照顧更多的用戶,每一款程序都應該要從最新版到最舊版逐一進行檢測,即:Direct11到10再到9.3
4.1.10DirectX圖形基礎結構(重點)
略(在這一節會介紹DirectX圖像基礎結構,即DXGI(DirectX Graphics Infrastructure))
4.1.11功能支持的檢測
略
4.1.12資源駐留
復雜的游戲一般都會運用大量的紋理和3D網格,但其中大多數並不是要一直放在顯存中給GPU使用,所以我們應該要在應用程序中通過控制資源在顯存中的去留,主動管理資源的駐留情況,所以我們一般要將短時間內不會再次使用的資源清出顯存
4.2CPU和GPU之間的交互
在進行圖形編程的時候,有兩種處理器在參與處理工作,即CPU和GPU。為了獲得最佳性能,我們一般讓兩者盡量同時工作,少一點同步。
4.2.1命令隊列和命令列表
每個GPU都維護着至少一個命令隊列(本質是環形緩沖區),CPU可以利用命令列表將命令提交到這個隊列中,在一系列命令被提交到隊列中之后,這些命令並不會馬上執行,GPU會處理先前插入命令隊列的命令。后來的新命令必須在前面的命令被執行完畢之后才會執行。
在Direct12中,命令隊列被抽象為ID3D12CommandQueue接口來表示,我們要通過填寫D3D12_COMMAND_QUEUE_DESC結構體來描述隊列,然后再調用ID3D12Device::CreateCommandQueue方法來創建隊列。
ExecuteCommandList是一種常用的ID3D12CommandQueue接口方法,它可以將命令列表中的命令添加到命令隊列中進行等待,供GPU在合適的時機執行。在代碼中,我們一般通過ID3D12GraphicsCommandList接口的方法來向命令列表中添加命令,只有調用ExecuteCommandList方法才會把命令列表中的命令添加到命令隊列中進行等待。同時在所有的命令都被添加到命令列表中之后,我們應該要使用ID3D12GraphicsCommandList::Clsoe方法來結束命令的記錄。
命令分配器(ID3D12CommandAllocator):記錄在命令列表中的命令,實際上是存儲在命令分配器中,當調用ID3D12CommandList::ExecuteCommandList方法之后,命令隊列會引用命令分配器中的命令,而命令分配器則有ID3D12Device接口來創建。我們可以創建出多個關聯於同一命令分配器的命令列表,但不能同時用他們來記錄命令,即我們在使用一個命令列表記錄命令時,必須關閉同一命令分配器所關聯的其他命令列表。這意味着命令列表中的命令必須要一個一個的添加到命令分配器中。
注意:重置命令列表不會影響命令隊列中的命令,因為相關的命令分配器依舊在維護着那些即將被命令隊列引用的命令,反而言之,重置命令分配器會影響到命令隊列中命令的執行,因此,在沒有確定GPU執行完命令分配器中所有的命令之前,千萬不要重置命令分配器
4.2.2CPU和GPU的同步
本節主要介紹圍欄的使用,當我們通過CPU將數據a傳入GPU中,CPU提前於GPU完成了數據b的接收,這時如果CPU提交命令,將會讓數據a被數據b覆蓋。為了避免出現這種嚴重錯誤,我們可以采用圍欄(相當於標識),起初我們將圍欄值設為0,每次提交一次命令列表+1,然后強制CPU等待,當GPU執行完命令隊列中的命令之后,解除CPU的等待,即可避免出現覆蓋的錯誤。但是這種方法並不完美,因為它會使CPU處於空閑狀態,后續將會介紹更好的方法。
4.2.3資源轉換
資源冒險:為了實現常見的渲染效果,我們經常通過GPU對某個資源按順序進行讀寫操作,但是有些時候GPU的寫操作還沒有完成,卻開始讀取資源,這樣便會出現資源冒險這個問題。
為了解決資源冒險,Direct3D針對資源設計了一組相關狀態,資源在創建時會處於默認狀態,該狀態會一直持續到應用程序通過Direct3D將它轉變成另一種狀態為止,這樣GPU便可以根據資源的狀態來確定是否要對它進行讀取操作,從而防止資源冒險的行為出現。
在Direct3D12之前,有關資源狀態的轉換的一系列工作都是交由驅動來管理,因此性能會較差,在DX12中,資源狀態要依靠我們手動進行轉換(如果轉換不當,會使性能比DX11更差),通過命令列表設置轉換資源屏障數據,就可以指定資源的轉換,在代碼中,資源屏障用D3D12_RESOURCE_BARRIER結構體表示,在Direct3D12中,許多結構體都有其所對應的擴展輔助結構變體,這些擴展結構體使用起來更加方便,我們一般都使用這些變體,以CD3DX12作為前綴的變體都定義在d3dx12.h文件中,轉換資源屏障對應的變體為:CD3DX12_RESOURCE_BARRIER。(事實上,我們可以把轉換資源屏障看作是一條用來告知GPU某資源狀態正在進行轉換的命令,在收到這條命令之后,GPU便會采取措施避免產生資源冒險)。
4.2.4命令與多線程
新手暫時跳過