1. 程式人生 > >Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第四章:Direct 3D初始化

Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第四章:Direct 3D初始化

學習目標

  1. 對Direct 3D程式設計在3D硬體中扮演的角色有基本瞭解;
  2. 理解COM在Direct 3D中扮演的角色;
  3. 學習基本的圖形學概念,比如儲存2D影象、頁面切換,深度緩衝、多重紋理對映和CPU與GPU如何互動;
  4. 學習如何使用效能計數函式讀取高精度時間;
  5. 學習如何初始化Direct 3D;
  6. 熟悉本書Demo通用的應用框架中的基本結構。


1 前言

在理解Direct3D初始化步驟前,需要我們先熟悉一些圖形學概念和Direct3D型別。


1.1 Direct3D 12 概述

Direct3D是一個底層圖形API用來控制和對GPU程式設計,它可以讓我們使用硬體加速來渲染3D圖形;比如要向GPU提交一個清空渲染目標的命令,我們可以呼叫方法ID3D12CommandList::ClearRenderTargetView。
Direct3D 12添加了一些新的渲染特性,但是主要的提升在於它被重新設計用來減少CPU的開銷和提高多執行緒支援。


1.2 COM

Component Object Model (COM)可以讓DirectX成為獨立的程式語言並且讓它向下相容。我們通常像使用C++類一樣,以介面的形式應用COM物件。值得注意的是,我們通常使用特定的函式或者其他COM介面來獲得COM介面引用的指標,我們不能使用C++中的new關鍵字直接建立COM物件;另外COM對介面是引用計數的,當我們使用完畢後,需要呼叫它的Release方法後(而不是Delete),當其引用計數等於0時,COM物件會釋放其佔用的記憶體。
為了管理COM物件的生命週期,Windows Runtime Library (WRL)提供了Microsoft::WRL::ComPtr類(#include <wrl.h>)可以看做是COM物件的智慧指標,當ComPtr超出範圍時,它會自動呼叫它包含的COM物件的Release方法。本書中主要用到的ComPtr的三個方法如下:

  1. Get:返回其包含的COM介面,這個主要用於在函式中傳遞引數;
  2. GetAddressOf:返回其包含的COM介面的指標指向的地址,這個主要用於在函式引數中傳遞COM指標;
  3. Reset:將ComPtr介面設定為空指標nullptr,並且減少其包含的COM介面的引用計數,相同的,你也可以給ComPtr物件賦值為nullptr;
    當然,COM還有更多方法,但是對高效實用Direct3D來說,不需要。

1.3 貼圖格式

一張2D貼圖是資料元素的矩陣。2D貼圖的其中一個用法是用來儲存2D影象,其每個元素用來儲存畫素的顏色,當然,它的用途不僅於此,比如在法線貼圖中,其每個元素用來儲存3D向量;一張貼圖也不僅限於是儲存資料陣列,它們可以包含紋理對映等級,還可以讓GPU對其進行過濾和多重紋理對映等特殊操作。貼圖中不能儲存任意格式的資料,它只能儲存在DXGI_FORMAT共用體中定義了的幾種型別,其中一些格式型別如下:

  1. DXGI_FORMAT_R32G32B32_FLOAT:每個元素包含3個32位的浮點陣列件;
  2. DXGI_FORMAT_R16G16B16A16_UNORM:每個元素包含4個16位元件,並對映到0到1;
  3. DXGI_FORMAT_R32G32_UINT:每個元素包含2個32位無符號整形元件;
  4. DXGI_FORMAT_R8G8B8A8_UNORM:每個元素包含4個8位元件,並對映到0到1;
  5. DXGI_FORMAT_R8G8B8A8_SNORM:每個元素包含4個8位元件,並對映到-1到1;
  6. DXGI_FORMAT_R8G8B8A8_SINT:每個元素包含4個8位整形元件,並對映到-128到127;
  7. DXGI_FORMAT_R8G8B8A8_UINT:每個元素包含4個8位無符號整形,並對映到0到255;
    還有一些無型別格式,我們只是申請了記憶體,等到使用它的時候在申明它的型別,比如DXGI_FORMAT_R16G16B16A16_TYPELESS

1.4 交換鏈和頁面切換

為了避免動畫中的閃爍問題,使用多個緩衝來交換顯示,只有畫面在離屏緩衝中渲染完畢後,才切換到螢幕中顯示;前和後緩衝形式的交換鏈在Direct3D中使用IDXGISwapChain介面來表示;其提供重置尺寸方法:IDXGISwapChain::ResizeBuffers和呈現方法:IDXGISwapChain::Present(交換2個緩衝前後位置)。
使用兩個快取稱之為雙緩衝,使用三個快取的稱之為三緩衝,大部分情況下雙緩衝就夠用了。


1.5 深度緩衝

深度快取用來儲存每個畫素的深度資訊,其值域為0到1,0代表距離是椎體最近距離,1代表最遠距離,因為其餘畫素是一一對應的,所以它的解析度和back buffer的解析度是一樣的;
深度快取是一張貼圖,所以它必須用特定的格式來建立:

  1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT:使用32位浮點數深度快取,和8位無符號整形預留給模板快取(0~255)和24位未使用資料;
  2. DXGI_FORMAT_D32_FLOAT:使用32位浮點數深度快取;
  3. DXGI_FORMAT_D24_UNORM_S8_UINT:使用24位無符號深度快取,並對映到0到1,和8位無符號整形預留給模板快取;
  4. DXGI_FORMAT_D16_UNORM:使用16位無符號深度快取,並對映到0到1。

應用不需要一定有模板快取,但是如果有的話,它經常附加到深度快取中,比如32位格式:
DXGI_FORMAT_D24_UNORM_S8_UINT

所以深度快取最好稱之為深度/模板快取;


1.6 資源和描述符(Descriptors)

GPU資源並不是直接繫結的,而是通過descriptor物件來引用,之所以這樣做是因為GPU資源本質上是一堆普通的記憶體塊,所以它們可以在渲染管線中不同階段中被使用;更進一步,GPU資源可以建立成無型別的,所以GPU可能不知道資源的型別。所以就需要使用descriptors來描述資源。
(View和descriptor是一樣的,老版本中使用View,DX12中部分地方也沿用View)
Descriptors擁有型別,用來定義它將如何被使用,在本書中使用到的型別有:

  1. CBV/SRV/UAV descriptors用來描述嘗試快取(constant buffers)、著色器資源(shader resources)和unordered access view resources;
  2. Sampler descriptors用來描述紋理對映資源;
  3. RTV descriptors用來描述渲染目標資源;
  4. DSV descriptors用來描述深度/模板資源;

一個descriptor heap是一個descriptors的陣列,它用來儲存所有特定型別的descriptors,不同型別的descriptors需要用不用descriptors heap儲存,你也可以針對同一個型別的descriptors建立多個descriptors heap;同時也可以多個descriptors heap引用同一個資源。

Descriptors應該在初始化的時候建立,因為它需要做一些型別檢查和驗證;


1.7 多重紋理對映理論

因為顯示器上的畫素不是無限小,所以任意線段都不能在顯示器上完美呈現出來;當無法增加顯示器解析度的時候,我們可以使用抗鋸齒技術。
其中一種叫超級紋理對映技術,它使用4倍於螢幕解析度的back buffer 和 深度快取(depth buffer),當顯示到螢幕上時,取4個畫素的平均值;這種計數計算量和記憶體佔用都太大了,Direct3D選用了一種折中的方案稱為多重紋理對映:該計數也使用4倍於螢幕解析度的back buffer 和 深度快取(depth buffer),它並不計算每個字畫素的顏色,而是每個畫素只計算一遍,然後分享給每個可見和未被遮擋的子畫素,如下圖所示:
在這裡插入圖片描述


1.8 Direct3D中的多重紋理對映

在下一個部分中,我們需要填寫一個結構體DXGI_SAMPLE_DESC,這個結構體有2個成員變數如下:

typedef struct DXGI_SAMPLE_DESC
{
	UINT Count;
	UINT Quality;
} DXGI_SAMPLE_DESC;

Count用來指定對每個畫素進行多少次取樣,Quality用來指定品質等級(quality level 指可以相容不同硬體廠商?);高取樣次數和品質等級代表更好的效果也代表更大的運算和記憶體開銷;品質等級的範圍只要基於紋理格式,取樣次數基於每個畫素。

我們可以使用函式ID3D12Device::CheckFeatureSupport檢查品質等級對於當前的紋理格式,和取樣次數是否可用:

typedef struct D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS {
	DXGI_FORMAT Format;
	UINT SampleCount;
	D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG Flags;
	UINT NumQualityLevels;
} D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS;

D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;

ThrowIfFailed(md3dDevice->CheckFeatureSupport(
	D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
	&msQualityLevels,
	sizeof(msQualityLevels)));

第二個引數同時是輸入和輸出引數,對於輸入引數,我們必須指定紋理格式,取樣次數和我們需要確認的多重紋理對映支援flag進行賦值;函式在輸出的時候回對quality level進行賦值。無效的紋理格式的品質等級和取樣次數組合範圍是0到NumQualityLevels–1。

最大采樣次數定義為:

#define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT ( 32 )

取樣次數設定為4或者8,對於效能和記憶體開銷都是合理的;如果你不想使用多重紋理對映,可以把取樣次數設定為1,品質等級設定為0(back buffer 和 depth buffer要設定一樣的取樣設定)。


1.9 特徵級別(Feature Levels)

Direct3D 11中介紹了特徵級別的概念,它直接對應到每個版本的Direct3D:

enum D3D_FEATURE_LEVEL
{
	D3D_FEATURE_LEVEL_9_1 = 0x9100,
	D3D_FEATURE_LEVEL_9_2 = 0x9200,
	D3D_FEATURE_LEVEL_9_3 = 0x9300,
	D3D_FEATURE_LEVEL_10_0 = 0xa000,
	D3D_FEATURE_LEVEL_10_1 = 0xa100,
	D3D_FEATURE_LEVEL_11_0 = 0xb000,
	D3D_FEATURE_LEVEL_11_1 = 0xb100
}D3D_FEATURE_LEVEL;

特徵級別定義了一系列嚴格的功能;比如,如果一個GPU支援11,它必須支援所有Direct 11的功能,除了少量一些功能(比如多重紋理對映還是需要被確認下,因為它可以在不同支援Direct 11的硬體之間變化)。特徵級別讓開發變得容易一些,因為當你知道特徵級別時,你就知道了你處理的Direct3D支援的功能。
如果使用者的硬體不支援當前特徵級別,應用程式會返回到上一個(更老的)特徵級別。


1.10 DirectX 影象基礎設施(DXGI)

DXGI是一套和Direct3D一起使用的API。它的基本思想是:一些圖形任務對於一些圖形API是相同的。比如:為了平滑動畫的交換鏈(swap chain)和頁面切換(page flipping)在2D和3D情況是相同的,所以交換鏈的介面IDXGISwapChain就是DXGI API的一部分。DXGI還有其他功能,比如:全屏切換,遍歷系統資訊比如顯示介面卡(display adapters),顯示器,支援的顯示模式(解析度,重新整理頻率等);它也定義了各種支援的表面格式(surface formats (DXGI_FORMAT))。

在這裡我們簡單介紹一些後面將要用到的DXGI介面的概念。一個主要的介面是IDXGIFactory,它主要用來建立IDXGISwapChain介面和遍歷顯示介面卡。顯示介面卡用來執行圖形功能,通常它是物理硬體上的一部分;但是系統也可以包含一個軟體的顯示介面卡;一個系統可以擁有多個顯示介面卡,每個介面卡可以用一個IDXGIAdapter介面來表示,我們可以使用下列程式碼遍歷系統中所有的顯示介面卡:

void D3DApp::LogAdapters()
{
	UINT i = 0;
	IDXGIAdapter* adapter = nullptr;
	std::vector<IDXGIAdapter*> adapterList;
	while(mdxgiFactory->EnumAdapters(i, &adapter) != DXGI_ERROR_NOT_FOUND)
	{
		DXGI_ADAPTER_DESC desc;
		adapter->GetDesc(&desc);
		std::wstring text = L"***Adapter: ";
		text += desc.Description;
		text += L"\n";
		OutputDebugString(text.c_str());
		adapterList.push_back(adapter);
		++i;
	}
	
	for(size_t i = 0; i < adapterList.size(); ++i)
	{
		LogAdapterOutputs(adapterList[i]);
		ReleaseCom(adapterList[i]);
	}
}

一個系統可以由多個顯示器,一個顯示器輸出可以使用IDXGIOutput介面表示。每個介面卡關聯一個顯示輸出列表;比如,一個系統包含2個顯示卡和3個顯示器,其中一個顯示卡與2個顯示器掛鉤,另一個顯示卡和一個顯示器掛鉤,那麼在這種情況下,一個介面卡關聯2個輸出,另一個介面卡關聯一個輸出。
這些資訊我們可以使用下列程式碼遍歷出來:

void D3DApp::LogAdapterOutputs(IDXGIAdapter* adapter)
{
	UINT i = 0;
	IDXGIOutput* output = nullptr;
	while(adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND)
	{
		DXGI_OUTPUT_DESC desc;
		output->GetDesc(&desc);
		std::wstring text = L"***Output: ";
		text += desc.DeviceName;
		text += L"\n";
		OutputDebugString(text.c_str());
		LogOutputDisplayModes(output, DXGI_FORMAT_B8G8R8A8_UNORM);
		ReleaseCom(output);
		++i;
	}
}

每個顯示器又可以支援一些列顯示模式,一個顯示模式用DXGI_MODE_DESC結構體表示:

typedef struct DXGI_MODE_DESC
{
	UINT Width; // Resolution width
	UINT Height; // Resolution height
	DXGI_RATIONAL RefreshRate;
	DXGI_FORMAT Format; // Display format
	DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; //Progressive vs. interlaced
	DXGI_MODE_SCALING Scaling; // How the image is stretched
	// over the monitor.
} DXGI_MODE_DESC;

typedef struct DXGI_RATIONAL
{
	UINT Numerator;
	UINT Denominator;
} DXGI_RATIONAL;

typedef enum DXGI_MODE_SCANLINE_ORDER
{
	DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED = 0,
	DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE = 1,
	DXGI_MODE_SCANLINE_ORDER_UPPER_FIELD_FIRST = 2,
	DXGI_MODE_SCANLINE_ORDER_LOWER_FIELD_FIRST = 3
} DXGI_MODE_SCANLINE_ORDER;

typedef enum DXGI_MODE_SCALING
{
	DXGI_MODE_SCALING_UNSPECIFIED = 0,
	DXGI_MODE_SCALING_CENTERED = 1,
	DXGI_MODE_SCALING_STRETCHED = 2
} DXGI_MODE_SCALING;

我們可以使用下列程式碼把所有顯示模式都打印出來:

void D3DApp::LogOutputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format)
{
	UINT count = 0;
	UINT flags = 0;
	// Call with nullptr to get list count.
	output->GetDisplayModeList(format, flags, &count, nullptr);
	std::vector<DXGI_MODE_DESC> modeList(count);
	output->GetDisplayModeList(format, flags, &count, &modeList[0]);
	
	for(auto& x : modeList)
	{
		UINT n = x.RefreshRate.Numerator;
		UINT d = x.RefreshRate.Denominator;
		
		std::wstring text = L"Width = " + std::to_wstring(x.Width) + L" " +
		L"Height = " + std::to_wstring(x.Height) + L" " +
		L"Refresh = " + std::to_wstring(n) + L"/" + std::to_wstring(d) + L"\n";
		
		::OutputDebugString(text.c_str());
	}
}

當進入全屏模式的時候,遍歷顯示模式就變得很重要,為了優化全屏模式下的效能,準確匹配顯示模式就很重要,比如重新整理頻率。

如果要更多的瞭解DXGI,我們建議閱讀下面的文件:
DXGI Overview: http://msdn.microsoft.com/enus/library/windows/desktop/bb205075(v=vs.85).aspx
DirectX Graphics Infrastructure: http://msdn.microsoft.com/enus/brary/windows/desktop/ee417025(v=vs.85).aspx
DXGI 1.4 Improvements: https://msdn.microsoft.com/enus/library/windows/desktop/mt427784(v=vs.85).aspx


1.11 檢查特徵等級支援

我們已經使用ID3D12Device::CheckFeatureSupport函式來檢查裝置對多重紋理對映的支援,我們也可以用它來檢查對其他特徵的支援,它的引數如下:

HRESULT ID3D12Device::CheckFeatureSupport(
	D3D12_FEATURE Feature,
	void *pFeatureSupportData,
	UINT FeatureSupportDataSize);
  1. Feature:是D3D12_FEATURE列舉型別,用來定義我們要檢查哪種型別:
    D3D12_FEATURE_D3D12_OPTIONS:檢查各種Direct 12特徵的支援;
    D3D12_FEATURE_ARCHITECTURE:檢查對硬體結構特徵的支援;
    D3D12_FEATURE_FEATURE_LEVELS:檢查對特徵等級的支援;
    D3D12_FEATURE_FORMAT_SUPPORT:檢查特徵對紋理型別的支援;
    D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS:檢查多重紋理對映的支援;
  2. pFeatureSupportData指向特徵支援資料的資料結構的指標,資料的型別基於指定的Feature的值改變:
    a、如果指定D3D12_FEATURE_D3D12_OPTIONS,那麼傳遞D3D12_FEATURE_DATA_D3D12_OPTIONS的例項;
    b、如果指定D3D12_FEATURE_ARCHITECTURE,那麼傳遞D3D12_FEATURE_DATA_ARCHITECTURE的例項;
    c、如果指定D3D12_FEATURE_FEATURE_LEVELS,那麼傳遞D3D12_FEATURE_DATA_FEATURE_LEVELS的例項;
    d、如果指定D3D12_FEATURE_FORMAT_SUPPORT,那麼傳遞D3D12_FEATURE_DATA_FORMAT_SUPPORT的例項;
    e、如果指定D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,那麼傳遞D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS的例項;
  3. FeatureSupportDataSize是傳遞到引數pFeatureSupportData的資料大小。

ID3D12Device::CheckFeatureSupport可以檢查大量各種特徵,很多本書沒有使用到的和高階特徵,可以通過SDK文件來檢視細節,在這裡我們使用特徵等級檢查來舉例:

typedef struct D3D12_FEATURE_DATA_FEATURE_LEVELS
{
	UINT NumFeatureLevels;
	const D3D_FEATURE_LEVEL *pFeatureLevelsRequested;
	D3D_FEATURE_LEVEL MaxSupportedFeatureLevel;
} D3D12_FEATURE_DATA_FEATURE_LEVELS;

D3D_FEATURE_LEVEL featureLevels[3] =
{
	D3D_FEATURE_LEVEL_11_0, // First check D3D 11 support
	D3D_FEATURE_LEVEL_10_0, // Next, check D3D 10 support
	D3D_FEATURE_LEVEL_9_3 // Finally, check D3D 9.3 support
}; 

D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevelsInfo;
featureLevelsInfo.NumFeatureLevels = 3;
featureLevelsInfo.pFeatureLevelsRequested = featureLevels;
md3dDevice->CheckFeatureSupport(
	D3D12_FEATURE_FEATURE_LEVELS,
	&featureLevelsInfo,
	sizeof(featureLevelsInfo));

值得注意的是,第二個引數既是輸入也是輸出引數;輸入的是將要檢查的特徵等級陣列(pFeatureLevelsRequested),然後輸出硬體所支援的最大等級(MaxSupportedFeatureLevel)。


1.12 Residency

在Direct 12中,應用程式使用資源residency來管理資源的申請和釋放GPU記憶體,也可以使用下面函式來手動管理residency:

HRESULT ID3D12Device::MakeResident(
	UINT NumObjects,
	ID3D12Pageable *const *ppObjects);
	
HRESULT ID3D12Device::Evict(
	UINT NumObjects,
	ID3D12Pageable *const *ppObjects);

第二個引數是型別ID3D12Pageable的資源陣列,第一個引數是陣列中元素的個數;在本書中我們不使用residency,如果想要繼續瞭解,可以參考文件:https://msdn.microsoft.com/enus/library/windows/desktop/mt186622(v=vs.85).aspx



2 CPU/GPU的互動

在圖形程式中,有2個處理器在同時執行:CPU和GPU,為了優化效能,我們的目標是讓他們儘可能長時間同時在處理,並且減少同步。如果它們需要同步,就代表著其中一個處理器正在空閒,在等到另一個處理器處理完畢,這種情況就破壞了它們的並行運算,所以要儘可能減少同步操作。


2.1 命令佇列和命令列表(The Command Queue and Command Lists)

GPU有一個命令佇列,CPU呼叫Direct 3D API使用命令列表項GPU提交命令:
在這裡插入圖片描述
如果命令佇列為空,表示GPU會被閒置;如果命令佇列太滿,CPU將要等待GPU處理完成;上述兩種情況都不利於高效能的程式,比如遊戲。所以我們的目標是讓他們同時都高效的執行。

在Direct 12中,使用ID3D12CommandQueue介面來表示命令佇列,我們填寫D3D12_COMMAND_QUEUE_DESC資料結構,然後呼叫函式ID3D12Device::CreateCommandQueue來建立它。

Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;

D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;

ThrowIfFailed(md3dDevice->CreateCommandQueue( &queueDesc, IID_PPV_ARGS(&mCommandQueue)));

其中IID_PPV_ARGS巨集定義如下:

#define IID_PPV_ARGS(ppType) __uuidof(** (ppType)), IID_PPV_ARGS_Helper(ppType)

__uuidof(**(ppType))等同於COM介面的ID,比如在上面程式碼中就是ID3D12CommandQueue;IID_PPV_ARGS_Helper函式本質上是將ppType型別轉換為void* *;許多Direct 12的API的呼叫需要COM介面ID作為引數,所以本書中將大量使用這個巨集。

這個介面中的一個重要的方法是:ExecuteCommandLists,它用來將命令從命令列表提交到命令佇列:

void ID3D12CommandQueue::ExecuteCommandLists(
	// Number of commands lists in the array
	UINT Count,
	// Pointer to the first element in an array of command lists
	ID3D12CommandList *const *ppCommandLists);

命令列表將會從第一個元素開始執行。

命令列表使用ID3D12GraphicsCommandList介面來表示(繼承自ID3D12CommandList介面),ID3D12GraphicsCommandList介面有很多方法可以新增命令到命令列表,比如下面程式碼添加了一個設定視窗的命令,清空渲染目標的命令和一個釋出draw call的命令:

// mCommandList pointer to ID3D12CommandList
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->ClearRenderTargetView(mBackBufferView, Colors::LightSteelBlue, 0, nullptr);
mCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);

從這些新增命令的方法名字看,感覺命令會立刻被執行,但實際上只是將他們新增到了命令列表。當我們新增完畢後,需要呼叫方法ID3D12GraphicsCommandList::Close來表示命令已經新增完畢:

// Done recording commands.
mCommandList->Close();

命令列表需要關閉後才能呼叫ID3D12CommandQueue::ExecuteCommandLists被傳遞到命令佇列。

和命令列表相關聯的是一個記憶體支援類:ID3D12CommandAllocator。當一個命令被記錄到命令列表時,它實際上是被儲存到一個關聯的命令分配器(command allocator),當列表被傳遞到命令佇列是,命令佇列將從命令分配器中引用這些命令。一個命令分配器通過ID3D12Device建立:

HRESULT ID3D12Device::CreateCommandAllocator(
	D3D12_COMMAND_LIST_TYPE type,
	REFIID riid,
	void **ppCommandAllocator);
  1. type:可以被關聯到這個分配器的命令列表型別,本書中將要使用到的2個型別:
    a、D3D12_COMMAND_LIST_TYPE_DIRECT:儲存一個將直接被GPU執行的命令列表;
    b、D3D12_COMMAND_LIST_TYPE_BUNDLE:將命令列表表示為一個包(bundle),這個是Direct 12提供的一個優化方法,將一組命令記錄到一個bundle裡,這組裡的命令會被預處理用以優化。所以bundle需要在初始化時記錄。只有當效能分析出在建立零碎的命令列表時佔用了大量時間,再考慮使用該型別。一般情況下Direct 12的API已經非常高效,所以不需要使用該型別為預設,本書中也不會使用該型別,如果有需求,可以檢視官方文件。
  2. riid:ID3D12CommandAllocator介面的COM ID;
  3. ppCommandAllocator:建立好的分配器的指標;

命令列表也是從ID3D12Device建立:

HRESULT ID3D12Device::CreateCommandList(
	UINT nodeMask,
	D3D12_COMMAND_LIST_TYPE type,
	ID3D12CommandAllocator *pCommandAllocator,
	ID3D12PipelineState *pInitialState,
	REFIID riid,
	void **ppCommandList);
  1. nodeMask:設定0表示單GPU,否則表示指定該命令列表關聯的GPU,本書中使用單GPU;
  2. type:命令列表的型別:D3D12_COMMAND_LIST_TYPE_DIRECT和D3D12_COMMAND_LIST_TYPE_BUNDLE;
  3. pCommandAllocator:關聯的分配器,分配器和命令列表的型別要一致;
  4. pInitialState:指定該命令列表最初的流水線狀態,對於bundles可以指定為null,我們在第六章詳細討論ID3D12PipelineState;
  5. riid:ID3D12CommandList介面的COM ID;
  6. ppCommandList:輸出常見的命令列表的指標;

你可以使用ID3D12Device::GetNodeCount來獲取當前系統中的GPU介面卡的個數。

你可以建立多個命令列表關聯同一個分配器,但是不能同時為它們記錄命令;所以當為一個命令列表記錄命令時,其它關聯同一個分配器的命令列表必須關閉;所以所有從關聯了相同分配器新增的命令是連續的。
(一個命令列表建立時的預設狀態是open,所以如果同時建立2個命令列表關聯到同一個分配器,會報錯:D3D12 ERROR: ID3D12CommandList:: {Create,Reset}CommandList: The command allocator is currently in-use by another command list.)

當我們呼叫ID3D12CommandQueue::ExecuteCommandList©後,繼續呼叫ID3D12CommandList::Reset之後重新使用C內部的記憶體來記錄新的命令是安全的,它的引數和ID3D12Device::CreateCommandList中的引數是一致的:

HRESULT ID3D12CommandList::Reset(
	ID3D12CommandAllocator *pAllocator,
	ID3D12PipelineState *pInitialState);

重置命令列表不會影響到命令佇列中的命令,因為關聯的命令分配器中還儲存這命令佇列中引用的命令。

當完成當前幀的渲染時,我們可以呼叫ID3D12CommandAllocator::Reset方法在下一幀中重新使用命令分配器中的記憶體:

HRESULT ID3D12CommandAllocator::Reset(void);

這個思路類似於std::vector::clear,重置vector的大小為0,但是保持容量和之前相同;因為命令佇列引用命令分配器中的命令,所以在確保GPU已經完成渲染前,不能重置命令分配器,在下一章中討論這個方法。


2.2 CPU和GPU的同步

因為有2個處理器並行運算,就會出現一些同步問題:
假設有一個資源R儲存了一個即將繪製的幾何體的位置資料,CPU設定了一個位置給R,並將命令新增到了命令佇列,然後CPU又設定了一個新位置給R,並將命令新增到了命令佇列,如果這個時候第一個命令還沒有被GPU執行,那麼就會出現錯誤:
在這裡插入圖片描述
其中一個解決方案是,強制CPU等待GPU處理完所有在一個特殊標記(fence point)前的命令,我們管這個方案叫沖洗命令佇列(flushing the command queue)。我們可以使用一個由ID3D12Fence介面代表的fence來同步CPU和GPU。一個fence可以用下面的方法建立:

HRESULT ID3D12Device::CreateFence(
	UINT64 InitialValue,
	D3D12_FENCE_FLAGS Flags,
	REFIID riid,
	void **ppFence);
	
// Example
ThrowIfFailed(md3dDevice->CreateFence(
	0,
	D3D12_FENCE_FLAG_NONE,
	IID_PPV_ARGS(&mFence)));

一個fence物件包含一個UINT64的值用來實時表示fence點,開始的時候設定為0,然後在需要的時候標記一個fence點,然後讓它增長;下面的程式碼表示瞭如何使用它:

UINT64 mCurrentFence = 0;

void D3DApp::FlushCommandQueue()
{
	// Advance the fence value to mark commands up to this fence point.
	mCurrentFence++;
	
	// Add an instruction to the command queue to set a new fence point.
	// Because we are on the GPU timeline, the new fence point won’t be
	// set until the GPU finishes processing all the commands prior to
	// this Signal().
	ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));
	
	// Wait until the GPU has completed commands up to this fence point.
	if(mFence->GetCompletedValue() < mCurrentFence)
	{
		HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
		
		// Fire event when GPU hits current fence.
		ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
		
		// Wait until the GPU hits current fence event is fired.
		WaitForSingleObject(eventHandle, INFINITE);
		CloseHandle(eventHandle);
	}
}

在這裡插入圖片描述
這並不是一個很好的方案,因為CPU需要等待GPU執行完畢,但是在第七章前,它給我們提供了一個很簡單的思路可以同步CPU和GPU;我們可以在任何需要的地方進行同步(不需要太頻繁,每幀一次就可以),比如初始化的時候、我們想重置分配器的時候。


2.3 資源狀態轉變

為了保證共同的渲染效果,有時GPU需要先在資源R中寫入資料,然後在下一步中讀取R的資料;這樣在使用R資源的時候就會有一定的風險,比如讀取R資料的時候,可能還沒有寫入完成或者還沒有開始寫;為了解決這個問題Direct3D給資源關聯了一個狀態來避免這種情況。
一個資源轉換是使用在命令列表中的一個transition resource barriers陣列來表示,它代表了你想轉換資源的資料;在程式碼中,一個資源barrier用D3D12_RESOURCE_BARRIER_DESC結構來表示:

struct CD3DX12_RESOURCE_BARRIER : public D3D12_RESOURCE_BARRIER
{
	// [...] convenience methods
	static inline CD3DX12_RESOURCE_BARRIER Transition(
		_In_ ID3D12Resource* pResource,
		D3D12_RESOURCE_STATES stateBefore,
		D3D12_RESOURCE_STATES stateAfter,
		UINT subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
		D3D12_RESOURCE_BARRIER_FLAGS flags = D3D12_RESOURCE_BARRIER_FLAG_NONE)
	{
		CD3DX12_RESOURCE_BARRIER result;
		
		ZeroMemory(&result, sizeof(result));
		D3D12_RESOURCE_BARRIER &barrier = result;
		result.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
		result.Flags = flags;
		barrier.Transition.pResource = pResource;
		barrier.Transition.StateBefore = stateBefore;
		barrier.Transition.StateAfter = stateAfter;
		barrier.Transition.Subresource = subresource;
		
		return result;
	}
	// [...] more convenience methods
};

CD3DX12_RESOURCE_BARRIER繼承自D3D12_RESOURCE_BARRIER_DESC並且添加了一些方法,大多數Direct 12結構都有擴充套件結構,它們定義在d3dx12.h。這個檔案不是SDK的核心,但是可以從微軟官方上下載,為了方便,本書將它們賦值到了Common目錄下。
其中一個使用的例子如下:

mCommandList->ResourceBarrier(1,
	&CD3DX12_RESOURCE_BARRIER::Transition(
	CurrentBackBuffer(),
	D3D12_RESOURCE_STATE_PRESENT,
	D3D12_RESOURCE_STATE_RENDER_TARGET));

上述程式碼將back buffer 從用來顯示到螢幕的狀態修改為渲染目標。


2.4 多執行緒與命令

Direct 12被設計用來提高多執行緒的效能,命令列表就是其中一種;當我們要渲染一個很大的場景的時候,使用4個執行緒和4個命令列表分別計算25%的資源可以很大的提高效能。
但是有幾點需要注意:

  1. 命令列表不是執行緒自由的,多個執行緒不能共用一個命令列表;
  2. 命令分配器不是執行緒自由的,多個執行緒不能供用一個命令分配器;
  3. 命令佇列是執行緒自由的,多個執行緒可以共用一個命令佇列;
  4. 出於效能考慮,在初始化的時候需要指明最大同時記錄的命令列表的數量。

為了簡化考慮,本書中不使用多執行緒,但是希望讀完本書後參考SDK中的Multithreading12例子來學習多執行緒。


3 Direct3D 的初始化

下面展示初始化我們Demo中使用的Direct3D框架,初始化的流程可以概括如下:

  1. 使用D3D12CreateDevice函式建立ID3D12Device;
  2. 建立ID3D12Fence物件和query descriptor大小;
  3. 檢查4重紋理對映支援;
  4. 建立命令佇列,命令列表分配器和主命令列表;
  5. 描述和建立交換鏈;
  6. 建立應用需要的descriptor heaps;
  7. 重置back buffer大小,並建立back buffer的render target view;
  8. 建立depth/stencil buffer和與它關聯的depth/stencil view;
  9. 設定viewport和scissor;

3.1 建立裝置

Direct 12的裝置代表顯示介面卡,通常情況下它是硬體的一部分(比如顯示卡,有時也可以是軟體),它用來檢查特徵支援和建立其它介面比如資源、views和命令列表;可以使用下面的方法來建立:

HRESULT WINAPI D3D12CreateDevice(
	IUnknown* pAdapter,
	D3D_FEATURE_LEVEL MinimumFeatureLevel,
	REFIID riid, // Expected: ID3D12Device
	void** ppDevice );
  1. pAdapter:表示我們建立的裝置表示的顯示介面卡,設定為null表示使用主顯示介面卡,本書中通常使用主顯示介面卡;
  2. MinimumFeatureLevel:表示我們的應用需要支援的最小特徵等級,如果顯示介面卡不支援最小等級,裝置會建立失敗;在我們的框架中使用D3D_FEATURE_LEVEL_11_0;
  3. riid:ID3D12Device介面COM ID;
  4. ppDevice:返回建立好的裝置的指標;

下面是呼叫上面函式的例子:

#if defined(DEBUG) || defined(_DEBUG)
// Enable the D3D12 debug layer.
{
	ComPtr<ID3D12Debug> debugController;
	ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
	debugController->EnableDebugLayer();
}
#endif

ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));

// Try to create hardware device.
HRESULT hardwareResult = D3D12CreateDevice(
	nullptr, // default adapter
	D3D_FEATURE_LEVEL_11_0,
	IID_PPV_ARGS(&md3dDevice));
	
// Fallback to WARP device.
if(FAILED(hardwareResult))
{
	ComPtr<IDXGIAdapter> pWarpAdapter;
	
	ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
	ThrowIfFailed(D3D12CreateDevice(
		pWarpAdapter.Get(),
		D3D_FEATURE_LEVEL_11_0,
		IID_PPV_ARGS(&md3dDevice)));
}

3.2 建立Fence和Descriptor Sizes

當建立完裝置後,我們可以建立Fence來同步GPU和CPU,另外Descriptor的大小會根據不同GPU來改變,所以我們需要確認Descriptor的大小:

ThrowIfFailed(md3dDevice->CreateFence(
	0, D3D12_FENCE_FLAG_NONE,
	IID_PPV_ARGS(&mFence)));
	
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(
	D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(
	D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(
	D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

3.3 檢查4重紋理對映支援

本書中我們檢查4重紋理對映,選擇它一方面是因為它能帶來不錯的效果並且不佔用太多的效能;另一方面是所有支援Direct 11的裝置都可以支援所有格式的4重紋理對映,所以我們不需要確認它的支援情況;但是我們還是需要確認質量等級,程式碼如下:

D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;

ThrowIfFailed(md3dDevice->CheckFeatureSupport(
	D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
	&msQualityLevels,
	sizeof(msQualityLevels)));
	
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");

3.4 建立命令佇列和命令列表

示例程式碼如下:

ComPtr<ID3D12CommandQueue> mCommandQueue;
ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
ComPtr<ID3D12GraphicsCommandList> mCommandList;

void D3DApp::CreateCommandObjects()
{
	D3D12_COMMAND_QUEUE_DESC queueDesc = {};
	queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
	queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
	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(), // Associated command allocator
		nullptr, // Initial PipelineStateObject
		IID_PPV_ARGS(mCommandList.GetAddressOf())));
		
	// Start off in a closed state. This is because the first time we
	// refer to the command list we will Reset it, and it needs to be
	// closed before calling Reset.
	mCommandList->Close();
}

3.5 描述和建立交換鏈

首先需要填寫一個DXGI_SWAP_CHAIN_DESC結構的例項,它用來描述交換鏈的特徵,它的定義如下:

typedef struct DXGI_SWAP_CHAIN_DESC
{
	DXGI_MODE_DESC BufferDesc;
	DXGI_SAMPLE_DESC SampleDesc;
	DXGI_USAGE BufferUsage;
	UINT BufferCount;
	HWND OutputWindow;
	BOOL Windowed;
	DXGI_SWAP_EFFECT SwapEffect;
	UINT Flags;
} DXGI_SWAP_CHAIN_DESC;

另外一個結構DXGI_MODE_DESC的定義如下:

typedef struct DXGI_MODE_DESC
{
	UINT Width; // Buffer resolution width
	UINT Height; // Buffer resolution height
	DXGI_RATIONAL RefreshRate;
	DXGI_FORMAT Format; // Buffer display format
	DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; //Progressive vs. interlaced
	DXGI_MODE_SCALING Scaling; // How the image is stretched over the monitor.
} DXGI_MODE_DESC;

下面介紹一些重要的引數:

  1. BufferDesc:這個結構體描述了我們需要建立的back buffer;
  2. SampleDesc:多重紋理對映的值和質量等級;
  3. BufferUsage:指定DXGI_USAGE_RENDER_TARGET_OUTPUT;
  4. BufferCount:在交換鏈中使用的buffer數量;
  5. OutputWindow:我們要輸出的視窗的控制代碼;
  6. Windowed:是否為視窗模式;
  7. SwapEffect:指定DXGI_SWAP_EFFECT_FLIP_DISCARD;
  8. Flags:如果指定為DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH,那麼切換到全屏模式後,應用會選擇與當前最匹配的顯示模式,否則會保留當前桌面模式;

當我們描述完交換鏈後,我們可以使用IDXGIFactory::CreateSwapChain函式來建立它:

HRESULT IDXGIFactory::CreateSwapChain(
	IUnknown *pDevice, // Pointer to ID3D12CommandQueue.
	DXGI_SWAP_CHAIN_DESC *pDesc, // Pointer to swap chain description.
	IDXGISwapChain **ppSwapChain);// Returns created swap chain interface.

下面的程式碼展示了在我們的Demo框架中如何建立交換鏈;該函式可以多次呼叫,它在建立新的交換鏈的時候會先銷燬之前建立的,所以我們可以使用不同的設定重新建立交換鏈,也代表我們可以在執行時重新設定多重紋理對映。

DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
void D3DApp::CreateSwapChain()
{
	// Release the previous swapchain we will be recreating.
	mSwapChain.Reset();
	
	DXGI_SWAP_CHAIN_DESC sd;
	sd.BufferDesc.Width = mClientWidth;
	sd.BufferDesc.Height = mClientHeight;
	sd.BufferDesc.RefreshRate.Numerator = 60;
	sd.BufferDesc.RefreshRate.Denominator = 1;
	sd.BufferDesc.Format = mBackBufferFormat;
	sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
	sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
	sd.SampleDesc.Count = m4xMsaaState ? 4 : 1;
	sd.SampleDesc.Quality = m4xMsaaState ? 	(m4xMsaaQuality - 1) : 0;
	sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
	sd.BufferCount = SwapChainBufferCount;
	sd.OutputWindow = mhMainWnd;
	sd.Windowed = true;
	sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
	sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
	
	// Note: Swap chain uses queue to perform flush.
	ThrowIfFailed(mdxgiFactory->CreateSwapChain(
		mCommandQueue.Get(),
		&sd,
		mSwapChain.GetAddressOf()));
}

3.6建立Descriptor Heaps

我們需要建立一個descriptor heaps來儲存descriptors/views;descriptor heap使用ID3D12DescriptorHeap介面來表示,使用ID3D12Device::CreateDescriptorHeap函式來建立;在本章的簡單程式中,我們需要SwapChainBufferCount需要個數個RTV(render target views)來描述buffer;和一個DSV(depth/stencil view):

ComPtr<ID3D12DescriptorHeap> mRtvHeap;
ComPtr<ID3D12DescriptorHeap> mDsvHeap;

void D3DApp::CreateRtvAndDsvDescriptorHeaps()
{
	D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
	rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
	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())));
}

在我們的框架中,我們定義:

static const int SwapChainBufferCount = 2;
int mCurrBackBuffer = 0;

並且我們需要跟蹤當前的back buffer索引mCurrBackBuffer;我們的應用程式通過控制代碼來引用descriptors,可以通過ID3D12DescriptorHeap::GetCPUDescriptorHandleForHeapStar函式來獲取堆中的第一個控制代碼:

D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBufferView()const
{
	// CD3DX12 constructor to offset to the RTV of the current back buffer.
	return CD3DX12_CPU_DESCRIPTOR_HANDLE(
		mRtvHeap->GetCPUDescriptorHandleForHeapStart(),// handle start
		mCurrBackBuffer, // index to offset
		mRtvDescriptorSize); // byte size of descriptor
}

D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const
{
	return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}

現在我們知道獲取descriptor大小的目的了,我們需要根據當前back buffer索引偏移到我們需要的descriptor。


3.7 建立Render Target View

如果我們想繫結back buffer到輸出合併階段(D3D可以渲染到它上),我們需要為back buffer建立一個Render Target View,第一步是獲取在交換鏈中的back buffer:

HRESULT IDXGISwapChain::GetBuffer(
	UINT Buffer,
	REFIID riid,
	void **ppSurface);
  1. Buffer:指定我們想要獲取的back buffer的索引;
  2. riid:ID3D12Resource介面的COM ID;
  3. ppSurface:返回獲取的ID3D12Resource物件指標。

呼叫IDXGISwapChain::GetBuffer會增加COM的引用計數,所以使用完畢後需要釋放它;

為了建立Render Target View,我們使用ID3D12Device::CreateRenderTargetView函式:

void ID3D12Device::CreateRenderTargetView(
	ID3D12Resource *pResource,
	const D3D12_RENDER_TARGET_VIEW_DESC *pDesc,
	D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor);
  1. pResource:指明將要使用的pResource;
  2. pDesc:一個D3D12_RENDER_TARGET_VIEW_DESC型別的指標,它描述了資料的型別;如果resource建立的時候不是typeless,則這裡可以設定為Null,那麼會和resource的型別保持一致;
  3. DestDescriptor:返回包含建立的Render Target View的控制代碼。

根據呼叫上面兩個函式,我們可以為交換鏈中的每個back buffer建立RTV:

ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());

for (UINT i = 0; i < SwapChainBufferCount; i++)
{
	// Get the ith buffer in the swap chain.
	ThrowIfFailed(mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
	
	// Create an RTV to it.
	md3dDevice->CreateRenderTargetView(
		mSwapChainBuffer[i].Get(), nullptr,
		rtvHeapHandle);
		
	// Next entry in heap.
	rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}

3.8 建立Depth/Stencil Buffer 和 View

一個紋理是GPU的一種資源,所以我們填充D3D12_RESOURCE_DESC結構來描述紋理資源,然後呼叫ID3D12Device::CreateCommittedResource函式來建立,D3D12_RESOURCE_DESC結構定義如下:

typedef struct D3D12_RESOURCE_DESC
{
	D3D12_RESOURCE_DIMENSION Dimension;
	UINT64 Alignment;
	UINT64 Width;
	UINT Height;
	UINT16 DepthOrArraySize;
	UINT16 MipLevels;
	DXGI_FORMAT Format;
	DXGI_SAMPLE_DESC SampleDesc;
	D3D12_TEXTURE_LAYOUT Layout;
	D3D12_RESOURCE_MISC_FLAG MiscFlags;
} D3D12_RESOURCE_DESC;
  1. dimension:資源的維數,其列舉如下:
		enum D3D12_RESOURCE_DIMENSION
		{
			D3D12_RESOURCE_DIMENSION_UNKNOWN = 0,
			D3D12_RESOURCE_DIMENSION_BUFFER = 1,
			D3D12_RESOURCE_DIMENSION_TEXTURE1D = 2,
			D3D12_RESOURCE_DIMENSION_TEXTURE2D = 3,
			D3D12_RESOURCE_DIMENSION_TEXTURE3D = 4
		} D3D12_RESOURCE_DIMENSION;
  1. Width:紋理的寬度(對於buffer,它代表位數);
  2. Height:紋理的高度;
  3. DepthOrArraySize:紋理元素的深度,或者紋理矩陣的大小;
  4. MipLevels:紋理對映的等級;
  5. Format:一個DXGI_FORMAT列舉型別,用來指明格式;
  6. SampleDesc:多重紋理對映和質量等級;
  7. Layout:一個D3D12_TEXTURE_LAYOUT列舉型別,用來指明紋理佈局,目前我們可以直接設定為D3D12_TEXTURE_LAYOUT_UNKNOWN;
  8. MiscFlags:對於depth/stencil buffer設定為D3D12_RESOURCE_MISC_DEPTH_STENCIL。

使用ID3D12Device::CreateCommittedResource方法根據我們指明的引數來建立和提交資源到對應的堆中 :

HRESULT ID3D12Device::CreateCommittedResource(
	const D3D12_HEAP_PROPERTIES *pHeapProperties,
	D3D12_HEAP_MISC_FLAG HeapMiscFlags,
	const D3D12_RESOURCE_DESC *pResourceDesc,
	D3D12_RESOURCE_USAGE InitialResourceState,
	const D3D12_CLEAR_VALUE *pOptimizedClearValue,
	REFIID riidResource,
	void **ppvResource);
	
typedef struct D3D12_HEAP_PROPERTIES {
	D3D12_HEAP_TYPE Type;
	D3D12_CPU_PAGE_PROPERTIES CPUPageProperties;
	D3D12_MEMORY_POOL MemoryPoolPreference;
	UINT CreationNodeMask;
	UINT VisibleNodeMask;
} D3D12_HEAP_PROPERTIES;
  1. pHeapProperties:我們要提交資源的堆的屬性,很多屬性都是高階應用,當前我們只考慮D3D12_HEAP_TYPE,它的值可以是列舉D3D12_HEAP_PROPERTIES中的一種
    a、D3D12_HEAP_TYPE_DEFAULT:預設堆,只能被GPU訪問,CPU不能訪問,所以depth/stencil buffer應該放在這裡;
    b、D3D12_HEAP_TYPE_UPLOAD:需要從CPU上傳資源到GPU的堆;
    c、D3D12_HEAP_TYPE_READBACK:需要被CPU讀取資源的堆;
    d、D3D12_HEAP_TYPE_CUSTOM:高階應用的情況,檢視MSDN文件;
  2. HeapMiscFlags:對堆進一步的描述,這裡我們直接設定為D3D12_HEAP_MISC_NONE;
  3. pResourceDesc:D3D12_RESOURCE_DESC例項的指標,描述我們想建立的資源;
  4. InitialResourceState:設定資源的初始化狀態;對於depth/stencil buffer,初始化狀態設定為D3D12_RESOURCE_USAGE_INITIAL,後續轉化成D3D12_RESOURCE_USAGE_DEPTH,這樣它在渲染管線中就可以被繫結為depth/stencil buffer;
  5. pOptimizedClearValue:D3D12_CLEAR_VALUE物件的指標用來描述清空資源的優化值;清空呼叫如果匹配到一個優化過的清空值可能會更高效,當然也可以直接設定為Null:
			struct D3D12_CLEAR_VALUE
			{
				DXGI_FORMAT Format;
				union
				{
					FLOAT Color[ 4 ];
					D3D12_DEPTH_STENCIL_VALUE DepthStencil;
				};
			} D3D12_CLEAR_VALUE;
  1. riidResource:ID3D12Resource介面的COM ID:
  2. ppvResource:返回建立的ID3D12Resource的指標;

出於優化考慮,資源應該放在預設堆中,除非你真的需要upload或者read堆的特性;

在使用depth/stencil buffer前,需要建立一個關聯的depth/stencil view來繫結到渲染管線,這個的做法和render target view類似:

// Create the depth/stencil buffer and view.
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.DepthOrArraySize = 1;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.Format = mDepthStencilFormat;
depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;

D3D12_CLEAR_VALUE optClear;
optClear.Format = mDepthStencilFormat;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0;

ThrowIfFailed(md3dDevice->CreateCommittedResource(
	&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
	D3D12_HEAP_FLAG_NONE,
	&depthStencilDesc,
	D3D12_RESOURCE_STATE_COMMON,
	&optClear,
	IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));
	
// Create descriptor to mip level 0 of entire resource using the
// format of the resource.
md3dDevice->CreateDepthStencilView(
	mDepthStencilBuffer.Get(),
	nullptr,
	DepthStencilView());
	
// Transition the resource from its initial state to be used as a depth buffer.
mCommandList->ResourceBarrier(
	1,
	&CD3DX12_RESOURCE_BARRIER::Transition(
		mDepthStencilBuffer.Get(),
		D3D12_RESOURCE_STATE_COMMON,
		D3D12_RESOURCE_STATE_DEPTH_WRITE));

我們使用了CD3DX12_HEAP_PROPERTIES helper constructor來建立對屬性結構:

explicit CD3DX12_HEAP_PROPERTIES(
	D3D12_HEAP_TYPE type,
	UINT creationNodeMask = 1,
	UINT nodeMask = 1 )
{
	Type = type;
	CPUPageProperty =
	D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
	MemoryPoolPreference =
	D3D12_MEMORY_POOL_UNKNOWN;
	CreationNodeMask = creationNodeMask;
	VisibleNodeMask = nodeMask;
}

CreateDepthStencilView的第二個引數是D3D12_DEPTH_STENCIL_VIEW_DESC的指標,它描述了資源中的資料型別,如果資源建立的時候有型別,那麼這個引數可以為null。


3.9 設定Viewport

正常情況下,我們都是畫滿整個螢幕,但是有些特殊情況下只需要畫在一個小的矩形裡:
在這裡插入圖片描述
這個back buffer的子矩形我們稱之為viewport,它可以用下面的結構體描述:

typedef struct D3D12_VIEWPORT {
	FLOAT TopLeftX;
	FLOAT TopLeftY;
	FLOAT Width;
	FLOAT Height;
	FLOAT MinDepth;
	FLOAT MaxDepth;
	} D3D12_VIEWPORT;

前四個引數用來定義矩形的位置,在Direct 3D中,深度值為0到1,後兩個引數用來轉化深度值範圍MinDepth到MaxDepth;設定深度值可以達到一些特殊效果,比如設定MinDepth=0,MaxDepth=0,那麼所有畫素都會渲染都最前面;一般情況下就設定為0到1。

當填充好D3D12_VIEWPORT結構後,我們使用ID3D12CommandList::RSSetViewports函式來設定VIEWPORT:

D3D12_VIEWPORT vp;
vp.TopLeftX = 0.0f;
vp.TopLeftY = 0.0f;
vp.Width = static_cast<float>(mClientWidth);
vp.Height = static_cast<float>(mClientHeight);
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;

mCommandList->RSSetViewports(1, &vp);

第一個引數是需要繫結的viewports的數字(應用於多個viewports的特殊效果);
你不能指定多個viewports到同一個render target,多個viewports應用於多個render target的高階效果;
每當命令列表被重置時,viewport也要被重置。


3.10 設定裁剪框(scissor rectangle)

關聯在back buffer裁剪框以外的畫素會被裁切,這個可以用來做優化操作;一個裁剪框可以由D3D12_RECT結構來定義:

typedef struct tagRECT
{
	LONG left;
	LONG top;
	LONG right;
	LONG bottom;
} RECT;

我們使用ID3D12CommandList::RSSetScissorRects方法來設定裁剪框;下面的程式碼只保留左上方的畫素:

mScissorRect = { 0, 0, mClientWidth/2, mClientHeight/2 };
mCommandList->RSSetScissorRects(1, &mScissorRect);

和RSSetViewports類似,第一個引數是需要繫結的裁剪框的數字(應用於多個viewports的特殊效果);
你不能指定多個裁剪框到同一個render target,多個裁剪框應用於多個render target的高階效果;
每當命令列表被重置時,裁剪框也要被重置。



4 時間和動畫

為了保證動畫正確,我們需要一個高精度的計時器。


4.1 The Performance Timer

出於對精確度的考慮,我們使用The Performance Timer,使用Win32函式來查詢The Performance Timer,需要#include <windows.h>;
The performance timer使用Counts來測量時間,我們使用下面的函式來獲取Counts:

__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

通過下面的方法來獲取頻率(每秒的Counts數):

__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);

那麼每秒的Counts就是:mSecondsPerCount = 1.0 / (double)countsPerSec
所以時間就是:valueInSecs = valueInCounts * mSecondsPerCount

我們使用兩次QueryPerformanceCounter的值計算時間差值:

__int64 A = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&A);
/* Do work */
__int64 B = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&B);

MSDN提示QueryPerformanceCounter在多執行緒下,不管任何處理器呼叫都會獲得不同結果的BUG(BIOS或者HAL),你可以使用SetThreadAffinityMask函式,那麼主執行緒將不會切換到其它執行緒。


4.2 遊戲計時器類

下面兩節我們將實現GameTimer類:

class GameTimer
{
public:
	GameTimer();
	float GameTime()const; // in seconds
	float DeltaTime()const; // in seconds
	void Start(); // Call when unpaused.
	void Stop(); // Call when paused.
	void Tick(); // Call every frame.
private:
	double mSecondsPerCount;
	double mDeltaTime;
	__int64 mBaseTime;
	__int64 mPausedTime;
	__int64 mStopTime;
	__int64 mPrevTime;
	__int64 mCurrTime;
	bool mStopped;
};

建構函式中確認了counter的頻率:

GameTimer::GameTimer()
	: mSecondsPerCount(0.0), mDeltaTime(-1.0),
	mBaseTime(0),
	mPausedTime(0), mPrevTime(0), mCurrTime(0),
	mStopped(false)
{
	__int64 countsPerSec;
	QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
	mSecondsPerCount = 1.0 / (double)countsPerSec;
}

4.3 每幀消耗的時間

使用每幀之間的時間差值來計算消耗的時間:

void GameTimer::Tick()
{
	if( mStopped )
	{
		mDeltaTime = 0.0;
		return;
	}
	// Get the time this frame.
	__int64 currTime;
	QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
	mCurrTime = currTime;
	
	// Time difference between this frame and the previous.
	mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;
	
	// Prepare for next frame.
	mPrevTime = mCurrTime;
	
	// Force nonnegative. The DXSDK’s CDXUTTimer mentions that if the
	// processor goes into a power save mode or we get shuffled to
	// another processor, then mDeltaTime can be negative.
	if(mDeltaTime < 0.0)
	{
		mDeltaTime = 0.0;
	}
}

float GameTimer::DeltaTime()const
{
	return (float)mDeltaTime;
}

Tick函式每幀都會呼叫:

int D3DApp::Run()
{
	MSG msg = {0};
	mTimer.Reset();
	while(msg.message != WM_QUIT)
	{
		// If there are Window messages then process them.
		if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
		{
			TranslateMessage( &msg );
			DispatchMessage( &msg );
		}
		// Otherwise, do animation/game stuff.
		else
		{
			mTimer.Tick();
			if( !mAppPaused )
			{
				CalculateFrameStats();
				Update(mTimer);
				Draw(mTimer);
			}
			else
			{
				Sleep(100);
			}
		}
	}
	return (int)msg.wParam;
}

重置函式Reset的實現如下:

void GameTimer::Reset()
{
	__int64 currTime;
	QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
	mBaseTime = currTime;
	mPrevTime = currTime;
	mStopTime = 0;
	mStopped = false;
}

4.4 總時間

詳見Demo中的程式碼



5 Demo應用框架

詳見程式碼工程,可以從本書官網下載程式碼,也可以下載我整理添加註釋後的程式碼:
https://github.com/jiabaodan/Direct12BookReadingNotes



6 Direct 3D 應用的除錯

許多Direct3D函式會返回HRESULT錯誤程式碼,所以我們使用下面的丟擲異常來檢查程式碼:

class DxException
{ 
public:
	DxException() = default;
	DxException(HRESULT hr, const std::wstring&
	functionName,
	const std::wstring& filename, int
	lineNumber);
	std::wstring ToString()const;
	HRESULT ErrorCode = S_OK;
	std::wstring FunctionName;
	std::wstring Filename;
	int LineNumber = -1;
};

#ifndef ThrowIfFailed
#define ThrowIfFailed(x) \
{ \
	HRESULT hr__ = (x); \
	std::wstring wfn = AnsiToWString(__FILE__); \
	if(FAILED(hr__)) { throw DxException(hr__, L#x,
	wfn, __LINE__); } \
} #
endif

ThrowIfFailed必須是一個巨集並且不是函式,另外__FILE__和__LINE__表示異常出現的檔案和行數,L#x可以將引數轉化為Unicode字串,這樣我們就可以用message box來顯示異常:

ThrowIfFailed(md3dDevice->CreateCommittedResource(
	&CD3D12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
	D3D12_HEAP_MISC_NONE,
	&depthStencilDesc,
	D3D12_RESOURCE_USAGE_INITIAL,
	IID_PPV_ARGS(&mDepthStencilBuffer)));

我們整個應用都在這個try/catch中:

try
{
	InitDirect3DApp theApp(hInstance);
	
	if(!theApp.Initialize())
		return 0;
		
	return theApp.Run();
}
catch(DxException& e)
{
	MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);
	return 0;
}

在這裡插入圖片描述



7 總結

  1. Direct 3D是程式設計師和圖形硬體之間的中介;
  2. Component Object Model (COM)可以是DirectX擁有基於語言開發和向下相容;程式設計師不需要了解COM的原理和細節,只需要知道如何獲取和釋放COM介面就可以了;
  3. 紋理是一個數據的陣列,它需要一個DXGI_FORMAT列舉來描述資料格式,它不僅可以包含影象資料,也可以包含其他資料,比如深度值;GPU可以在它上做特殊操作,比如:過濾和多重紋理對映;
  4. 為了避免動畫衝突,使用交換鏈對多個back buffer進行交替渲染和顯示;
  5. 深度快取是用來某點在場景中最接近相機的值,利用這個技術,我們不同擔心場景中物體的排序;
  6. 資源不能直接繫結到渲染管線,它需要繫結到descriptors;
  7. ID3D12Device可以類似於顯示卡硬體的軟體控制器;
  8. GPU有命令佇列,CPU通過命令列表向GPU提交命令;
  9. CPU和GPU是並行運算的2個處理器,它們有時需要進行同步處理;
  10. The performance counter是一個高精度計時器,我們使用它通過時間差來計算時間;
  11. 通過每幀消耗的時間來計算遊戲當前的FPS;