1. 程式人生 > >DirectX12(D3D12)基礎教程(三)——使用獨立堆以“定位方式”建立資源、建立動態取樣器、初步理解採取器型別

DirectX12(D3D12)基礎教程(三)——使用獨立堆以“定位方式”建立資源、建立動態取樣器、初步理解採取器型別

目錄

1、前言

1、前言

經過了第二部分教程的“折騰”之後,後面的教程我覺得應該順暢多了。至少我現在可以一天時間就把教程示例程式碼調通,並且可以按照自己的想法自由的去發揮了。我很喜歡這種感覺,就像在打遊戲中虐那些無腦的機器AI角色一樣。

經過前面兩章的學習,我相信大家對D3D12的程式設計複雜性應該有所認識了。那麼這一章讓我們放慢腳步,不再過多的學習知識點。開始緩慢爬坡,因為後面還有更吸(fu)引(za)人的內容等著我們一起去學習探討。所以喘口氣還是必要的。

通過前兩章的例子,我發現雖然刻意沒有使用什麼封裝,甚至連定義函式都沒有的方式線性的灌輸概念,但是由於D3D12本身在程式設計上的複雜和繁瑣,因此程式碼的可讀性實際上是不高的,這非常不利於學習,所以本章起,我使用中括號將每部分相關程式碼都放在一起,並且加上步驟序號和說明,方便大家線性的閱讀和理解(使用VS的大綱摺疊顯示功能)。這也是為以後的例子程式碼做一個準備。因為說實話我在組織準備每篇教程的例子程式碼時,有時也是想吐的,雖然只是畫一個簡單的紋理,但程式碼量隨便就超過了1000行,再往後我發現居然有點hold不住了。所以就使用了這個方法稍微組織一下。

言歸正傳,本篇教程中,我將重點講解一下顯示卡的架構、獨立堆、定位方式的資源,以及動態取樣器的用法,這些是D3D12中新加入的比較新的概念性的東西,我認為這些才是真正的D3D12中比較核心的一些概念,因此掌握它們才是用好用活D3D12必須要學習的。

2、顯示卡架構和儲存管理

在一開始的教程中,我已經提到過D3D12是一個很“低階”的介面,在我的系列文章《D3D11和D3D12多執行緒渲染框架的比較》中我甚至提到D3D12就是一個“顯示卡作業系統”。其實我這樣的說的另一重目的是說,要徹底學懂D3D12,那就需要像我們學習組合語言或C語言一樣,對計算機底層的架構甚至較詳細的硬體架構有所瞭解。否則學習這些內容就將停留在表面,很難深入的理解和掌握。

因此學習D3D12,就需要我們對現代的顯示卡子系統以及它與系統互動連線的方式有所瞭解。

首先我們在之前的教程中已經說過現代的GPU上是有很多可以並行執行命令的引擎的,如下圖所示:

這個圖來自MSDN,它很形象的說明了一個GPU上至少有三大類引擎,一個是複製引擎(Copy engine)、一個是計算引擎(Compute engine)、另一個是3D引擎(3D engine),實質上如果以最新的Nvidia的20xx系顯示卡GPU核來說,其上還有AI引擎、以及獨立的專門做實時光追運算的核心,當然還有跟我們渲染沒太大關係的視訊處理引擎等,未來不排除D3D中會不會加入對這些獨立的引擎以及對應型別的命令佇列的支援。所以現在瞭解下GPU的架構及程式設計理念,至少不至於在未來因為不能理解程式設計框架而被淘汰掉。

同時這幅圖上實際更進一步的示意說明的了CPU執行緒和GPU引擎之間如何互動命令,以及並行執行的原理。核心還是使用命令列表記錄命令,再將命令列表在命令佇列中排隊,然後各引擎從命令佇列中不斷獲取命令並執行的基本模式(回憶一下我們之前教程中提到的飯館模型),至此我想關於這個CPU、GPU並行執行的概念大家應該是深刻理解並消化了。需要再次強調的就是雖然命令列表是分開錄製的,但是它們被排隊至命令佇列之後,巨集觀上各個引擎在執行它們時仍然是序列順序的,而在微觀上針對一個個的原子資料,比如:紋理的畫素、網格的頂點之類的則是並行的(SIMD)。

當然上圖還是從執行緒角度或者說動態執行的角度來看現代CPU+GPU體系結構的檢視。而另一個方面就需要我們進一步去了解CPU+GPU是如何管理和使用記憶體的,以便於我們深入掌握D3D12中的記憶體管理方法,從而為真正提高效能做好準備。我想提高效能對於一個引擎或者遊戲來說意味著什麼,就不需要我過分強調了。

從型別上來說現代的PC、筆記本甚至手機中CPU和GPU本質上都是獨立的處理器,它們之間使用的是被稱為SMP架構模型進行互聯並相互協作的,SMP即共享儲存型多處理機(Shared Memory mulptiProcessors),)也稱為對稱型多處理機(Symmetry MultiProcessors)。

或者直白的說CPU和GPU間的互動就是通過共享記憶體這種方式來進行的,但他們各自又有各自的記憶體控制器和管理器,甚至各自還有自己的片上快取記憶體,因此最終要共享記憶體就需要一些額外的通訊控制方式來進行,這也是我們使用D3D12進行儲存管理程式設計的複雜性的根源。這裡要注意的是從第一篇教程起我就特別說明是儲存管理,而不是隻說記憶體管理(CPU側)、視訊記憶體管理(GPU側)或者共享記憶體(SMP中CPU和GPU共享的記憶體),這裡大家要特別注意這個概念上的區別。因為在D3D12中我們需要管理的不僅僅是GPU上的視訊記憶體,根據SMP的描述,我們還需要額外管理二者之間共享的記憶體。當然我想CPU的記憶體如何管理各位C/C++程式設計師應該已經輕車熟路了,就不需要我多囉嗦了。

更進一步的SMP架構又被細分為:均勻儲存器存取(Uniform-Memory-Access,簡稱UMA)模型、非均勻儲存器存取(Nonuniform-Memory-Access,簡稱NUMA)模型和快取記憶體相關的儲存器結構(cache-coherent Memory Architecture,簡稱CC-UMA)模型,這些模型的區別在於儲存器和外圍資源如何共享或分佈。

UMA架構模型示意圖如下:

從圖中可以看出UMA架構中物理儲存器被所有處理機均勻共享。所有處理機對所有儲存器具有相同的存取時間,這就是為什麼稱它為均勻儲存器存取的原因。每個處理器(CPU或GPU)可以有私有快取記憶體,外圍裝置也以一定形式共享(GPU因為沒有訪問外圍其他裝置的能力,實質就不共享外圍裝置了,這裡主要指多個CPU的系統共享外圍裝置)。實質上UMA方式是目前已經很少見的主機板整合顯示卡的方式之一。需要注意的是這裡只是一個簡化的示意圖,裡面只示意了一個CPU和GPU的情況,實質上它是可以擴充套件到任意多個CPU或GPU互通互聯的情況的。

NUMA架構的示意圖如下:

從NUMA的示意圖中可以看出,其儲存器物理上是分佈在所有處理器的本地儲存器上。本地儲存器的一般具有各自獨立的地址空間,因此一般不能直接互訪問各自的本地儲存。而處理器(CPU或GPU)訪問本地儲存器是比較快的,但要訪問屬於另一個處理器的遠端儲存器則比較慢,並且需要額外的方式和手段,因此其效能也是有額外的犧牲的。其實這也就是我們現在常見的“獨顯”的架構。當然一般來說現代GPU訪問視訊記憶體的速度是非常高的,甚至遠高於CPU訪問記憶體的速度。所以在程式設計中經常要考慮為了效能,而將盡可能多的純GPU計算需要的資料放在視訊記憶體中,從而提高GPU運算的效率和速度。

CC-UMA架構的示意圖如下:

如圖所示,CC-UMA是一種只用快取記憶體互聯互通的多處理器系統。CC-UMA模型是NUMA機的一種特例,只是將後者中分佈主儲存器換成了快取記憶體, 在每個處理器上沒有儲存器層次結構,全部高速緩衝儲存器組成了全域性地址空間。通常這是現代CPU中集顯最容易採取的架構方式。當然快取記憶體共享或直連的方式擁有最高的互訪效能。但其缺點就是快取記憶體因為高昂的價格,所以往往空間很小,目前的集顯上還只有幾兆,最多到幾十兆高速緩衝的樣子,所以對於現代的渲染來說這點儲存量實在是少的可憐了。另外因為快取記憶體是在不同的處理器(CPU或GPU)之間直接共享或互聯的,因此還有一個額外的問題就是儲存一致性的問題,就是說高速緩衝的內容跟實質記憶體中的內容是否一致,比如CPU實質是將資料先載入進記憶體中然後再載入進高速緩衝的,而GPU在CPU還沒有完成從記憶體到高速緩衝的載入時,就直接訪問高速緩衝中的資料就會引起錯誤了,反之亦然。因此就需要額外的機制來保證儲存一致性,當然這就導致一些額外的效能開銷。具體的關於儲存一致性的內容,我就不多講了,我們主要還是要靠獨顯來幹活。進一步的知識大家有興趣的可以百度一下相關資料。

具體來說,比如我的膝上型電腦上就有一個“集顯”也有一個“獨顯”,集顯跟CPU形成了CC-UMA架構,並且它獨佔了128M的記憶體當做視訊記憶體,而獨顯則與CPU形成了NUMA架構,獨顯上有2G的獨立視訊記憶體,兩個GPU都與CPU共享了8149M的記憶體,作為統一的共享記憶體。其實這也可以看出實際的系統中往往是上述架構混用的形式。

通過執行Dxdiag程式就可以看到這些資訊:

上圖是集顯情況,下面則顯示了獨顯的情況:

因為我的系統總共有16G的記憶體,所以DX子系統乾脆就共享了8149M的記憶體作為三個處理器(CPU+2個GPU,你應該已經習慣我用處理器來指代CPU或GPU了)之間的公共記憶體。這樣做的意義在哪裡呢?除了首先想到的可以擁有巨大的可用的視訊記憶體之外,其實它更重要的深層意義就是可以讓這兩個GPU因共享相同的公共記憶體,實現資料的互通互聯,從而可以並行的工作,這也是D3D12重要的高階特性之一——支援多顯示卡渲染尤其是異構多顯示卡渲染。也為支援DXR中的多顯示卡實時光追渲染提供了基礎支撐能力。

綜上,實質上這些架構之間的主要區別是在各處理器訪問儲存的速度上,簡言之就是說使用快取記憶體具有最高的訪問速度。其次就是訪問各自獨佔的儲存,而最慢的就是訪問共享記憶體了,當然對於CPU來說訪問共享記憶體與自己獨佔的記憶體在效能是基本沒有差異的。這裡的效能差異主要是從GPU的角度來說的。因此我們肯定願意將一些CPU或GPU專有的資料首先考慮放在各自的獨佔儲存中,其次需要多方來訪問的資料就放在共享記憶體中。這也就是我們上一講提到的D3D12中不同種類的儲存堆的本質含義。

另外需要提醒的是,現代的CPU+GPU以及系統的架構都是在不斷進化和變化的,目標就是更高的效率和效能,因此這裡說的架構僅僅還只是概念上的模型,跟實際的系統架構可能還有出入,如果想進一步瞭解這類資訊就請各位關注硬體類網站最新的一些CPU或GPU顯示卡測試類的文章,其中往往會提及一些最新的架構方面的知識。瞭解的目的就是為了更好的從根本上去了解軟體框架為什麼是這個樣子,從而提高學習的效率和效果。這也是我這麼多年學習的一個經驗總結之一。

最後我們可以通過下面的簡單程式來檢測我們的GPU與系統是以什麼架構互聯的,同時我們可以準確的知道他們各自獨佔儲存和共享儲存的情況。程式碼如下(可獨立執行):

#include <SDKDDKVer.h>
#define WIN32_LEAN_AND_MEAN // 從 Windows 頭中排除極少使用的資料
#include <windows.h>
#include <tchar.h>
#include <strsafe.h>		//for StringCchxxxx function
//新增WTL支援 方便使用COM
#include <wrl.h>
using namespace Microsoft;
using namespace Microsoft::WRL;

#include <dxgi1_6.h>
#include <d3d12.h> //for d3d12
//linker
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3d12.lib")

#define GRS_THROW_IF_FAILED(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;
};

#define GRS_USEPRINTF() TCHAR pBuf[1024] = {};DWORD dwRead = 0;
#define GRS_PRINTF(...) \
    StringCchPrintf(pBuf,1024,__VA_ARGS__);\
    WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),pBuf,lstrlen(pBuf),NULL,NULL);

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR    lpCmdLine, int nCmdShow)
{

	UINT nDXGIFactoryFlags = 0U;
	ComPtr<IDXGIFactory5>				pIDXGIFactory5;
	ComPtr<IDXGIAdapter1>				pIAdapter;
	ComPtr<ID3D12Device4>				pID3DDevice;
	GRS_USEPRINTF();
	try
	{
		AllocConsole(); //開啟視窗程式中的命令列視窗支援

#if defined(_DEBUG)
		{//開啟顯示子系統的除錯支援
			ComPtr<ID3D12Debug> debugController;
			if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
			{
				debugController->EnableDebugLayer();
				// 開啟附加的除錯支援
				nDXGIFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
			}
		}
#endif
		//1、建立DXGI Factory物件
		GRS_THROW_IF_FAILED(CreateDXGIFactory2(nDXGIFactoryFlags, IID_PPV_ARGS(&pIDXGIFactory5)));
		//2、列舉介面卡,並檢測其架構及儲存情況
		DXGI_ADAPTER_DESC1 desc = {};
		D3D12_FEATURE_DATA_ARCHITECTURE stArchitecture = {};
		for (UINT adapterIndex = 0; DXGI_ERROR_NOT_FOUND != pIDXGIFactory5->EnumAdapters1(adapterIndex, &pIAdapter); ++adapterIndex)
		{
			pIAdapter->GetDesc1(&desc);

			if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
			{//跳過軟體虛擬介面卡裝置
				continue;
			}
		
			GRS_PRINTF(_T("顯示卡[%d]-\"%s\":獨佔視訊記憶體[%dMB]、獨佔記憶體[%dMB]、共享記憶體[%dMB] ")
				, adapterIndex
				, desc.Description
				, desc.DedicatedVideoMemory / (1024*1024)
				, desc.DedicatedSystemMemory / (1024*1024)
				, desc.SharedSystemMemory / (1024 * 1024)
			);

			//建立裝置,並檢測架構
			//檢測顯示卡架構型別
			//D3D12_FEATURE_DATA_ARCHITECTURE.UMA = true一般為集顯
			//此時若D3D12_FEATURE_DATA_ARCHITECTURE.CacheCoherentUMA = true 則表示是CC-UMA架構 GPU和CPU通過快取記憶體來交換資料
			//若UMA = FALSE 一般表示為獨顯,此時CacheCoherentUMA 無意義

			GRS_THROW_IF_FAILED(D3D12CreateDevice(pIAdapter.Get(), D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&pID3DDevice)));
			GRS_THROW_IF_FAILED(pID3DDevice->CheckFeatureSupport(D3D12_FEATURE_ARCHITECTURE
				, &stArchitecture, sizeof(D3D12_FEATURE_DATA_ARCHITECTURE)));

			if (stArchitecture.UMA)
			{//UMA
				if ( stArchitecture.CacheCoherentUMA )
				{
					GRS_PRINTF(_T("架構為(CC-UMA)"));
				}
				else
				{
					GRS_PRINTF(_T("架構為(UMA)"));
				}
			}
			else
			{//NUMA
				GRS_PRINTF(_T("架構為(NUMA)"));
			}
			
			if ( stArchitecture.TileBasedRenderer )
			{
				GRS_PRINTF(_T(" 支援Tile-based方式渲染"));
			}

			pID3DDevice.Reset();
			GRS_PRINTF(_T("\n"));
		}
		::system("PAUSE");

		//釋放命令列環境,做個乾淨的程式設計師
		FreeConsole();
	}
	catch (CGRSCOMException& e)
	{//發生了COM異常
		e;
	}
	return 0;
}

獨立建立專案並執行上述程式後,在我的膝上型電腦上執行結果如下:

這實際與執行Dxdiag工具顯示的結果是相同的。當然這種在程式中使用程式碼檢測的方式就要比使用獨立的工具的方式更靈活方便,所以建議大家一定要掌握這種程式碼方式,主要是為了將來在封裝引擎或開發遊戲中可以靈活使用。

當然你也可以使用上述程式碼中的檢測手段更準確的判定多顯示卡系統中究竟哪個介面卡是獨顯,哪個介面卡是集顯。這也是後續異構多顯示卡渲染教程中將要使用的方法。建議你明白了這段程式碼之後,可以在所有的例子中加入判定使用獨顯的判斷程式碼,並使用獨顯來執行本系列教程中的例子。

3、建立預設堆並在其上以“定位方式”建立2D紋理

紋理(Txture)作為3D渲染中最重要的資源之一,操作紋理也是任何3D渲染程式都不可或缺的核心功能。因此我們很有必要牢牢掌握操作紋理的能力。

當然紋理一般在整個場景渲染過程中一般是不會變化的,所以我們優先考慮將它放置到視訊記憶體中,在D3D12中也就是將紋理放在預設堆上。並且之前的教程中我們已經學習瞭如何使用最簡單的“隱式堆”的方式來建立預設堆上的紋理資源,並且我們也說過了這種方式因為我們無法控制隱式堆的生命期,所以沒法通過重用緩衝的方式來提高系統性能。

說到這裡我想你應該有點明白我為什麼在本篇教程一開始囉嗦了一大堆顯示卡系統架構的原因了。其實在這裡我們可以將D3D12中的預設堆理解為在GPU視訊記憶體中的一塊緩衝,它對GPU來說就是“預設”的儲存位置,故名預設堆。因此放置其上的紋理以及資源對於GPU來說是擁有最高的訪問速度,當然付出的代價就是CPU是無法直接訪問GPU上的視訊記憶體的(注意前述架構中視訊記憶體只能由GPU訪問就明白了),所以我們就需要使用一個額外的上傳堆並利用複製引擎來載入紋理或其它資料。也正如你所想的上傳堆就不折不扣的在共享記憶體中了,這樣就使得CPU也能訪問它,GPU也能訪問它,當然最終為了從共享記憶體中把資料傳到視訊記憶體中,那麼就需要GPU上的複製引擎來幹活了。只是付出的代價就是我們需要協調CPU和GPU同時通知它們某段共享記憶體被用來當做上傳堆了,這甚至比CPU單獨管理記憶體需要額外付出一些效能代價。對於資料量不大,並且不會反覆申請-釋放的程式來說這沒什麼,有時候甚至感覺不到這種因為分配和釋放儲存而導致效能低下的問題。但是對於一些大型的遊戲程式來說,尤其有不同關卡場景,不同物體的複雜應用來說,不可避免的就需要大量反覆的申請-載入-釋放資源的呼叫。在D3D12之前的介面上,我們要想優化它幾乎是無能為力的。當然之前的教程中我們已經講過,聰明的遊戲程式設計師是通過“化零為整”的方法來減少分配-釋放的次數從而提高效能的。當然如果你能夠在自己的引擎或遊戲中綜合利用這兩種能力,那麼最終帶來的效能提升一定是相當可觀的。

為了能夠控制這些被分配的資料緩衝(包括紋理)的生命週期,同時能夠通過反覆重用,減少分配和釋放儲存的次數從而提高效能,D3D12中就提供了獨立的堆管理的介面。比如建立一個預設堆就可以像下面這樣寫程式碼:

D3D12_HEAP_DESC stTextureHeapDesc = {};
//為堆指定紋理圖片至少2倍大小的空間,這裡沒有詳細去計算了,只是指定了一個足夠大的空間,夠放紋理就行
//實際應用中也是要綜合考慮分配堆的大小,以便可以重用堆
stTextureHeapDesc.SizeInBytes = GRS_UPPER(2 * nPicRowPitch * nTextureH, D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT);
//指定堆的對齊方式,這裡使用了預設的64K邊界對齊,因為我們暫時不需要MSAA支援
stTextureHeapDesc.Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT;
stTextureHeapDesc.Properties.Type = D3D12_HEAP_TYPE_DEFAULT;		//預設堆型別
stTextureHeapDesc.Properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
stTextureHeapDesc.Properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
//拒絕渲染目標紋理、拒絕深度蠟板紋理,實際就只是用來擺放普通紋理
stTextureHeapDesc.Flags = D3D12_HEAP_FLAG_DENY_RT_DS_TEXTURES | D3D12_HEAP_FLAG_DENY_BUFFERS;

GRS_THROW_IF_FAILED(pID3DDevice->CreateHeap(&stTextureHeapDesc, IID_PPV_ARGS(&pITextureHeap)));

程式碼中pITextureHeap的型別為ID3D12Heap*。這裡再次強調一下雖然它的名字中帶有Heap,但它實際和我們使用的C/C++儲存管理中的堆疊是不同的。D3D12中的堆還是靜態大小的,即我們分配多大它就始終是多大,還沒法完全做到像C/C++中那樣利用realloc這樣的方法來動態改變大小。這主要是因為,如剛才所說這個預設堆的儲存雖然是在GPU的視訊記憶體中,但管理它其實是靠CPU的能力來與GPU通訊完成的,它具有一定的複雜性和額外的開銷,所以還無法做到類似realloc這樣的功能。當然我想聰明的你一定不會被這個問題難道,事實上我們可以利用複製引擎來封裝一個具有realloc功能的動態堆出來,比如,我們需要將一塊預設堆增大的時候,就可以先重新建立一個更大的ID3D12Heap堆,再利用複製引擎將原來堆上的資料複製到新的更大的堆上,最後釋放原來的堆即可。其實這樣也還是要分配和釋放記憶體,所以功能實現了,但是效能實質上沒什麼改進。最終我們現在可以採取的策略就是像上面示例程式碼中這樣事先分配一塊儘可能大的儲存,然後想辦法重用它,而不是反覆的分配和釋放浪費時間。

除了大小問題,其實上面程式碼中還要求我們的儲存區域是邊界對齊的,目前對於一般的緩衝和紋理來說,D3D12中都要求64K邊界對齊,對應的巨集就是D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT,而對於需要MSAA(抗鋸齒)取樣支援的紋理資料來說,就需要4M邊界對齊了,對應的巨集是D3D12_DEFAULT_MSAA_RESOURCE_PLACEMENT_ALIGNMENT。如果你指定0,其預設含義一樣是64k邊界對齊。因此,我們對於緩衝區的大小這個引數使用了一個上對其計算,我將它定義成了巨集GRS_UPPER,其定義如下:

#define GRS_UPPER(A,B) ((UINT)(((A)+((B)-1))&~(B - 1)))

這個演算法與之前教程講到的(A+B-1)/B的上取整演算法類似,只是它不用去做除法了,而是直接通過與非運算最大的餘數,從而使數字上對齊,這個演算法也希望各位能夠記住,因為對於任何與儲存管理相關的內容來說,這兩個演算法都是最常用的。

當然你可以把它用作你的面試題,從而難住那些沒有看過我這些文章的應聘者,只是不要說是我說的就行。

這個巨集的引數中,A表示我們需要的堆大小,B表示需要邊界對齊的大小,最終結果就是A上對齊到B邊界的大小。比如我們需要分配一塊100K大小的儲存,64k上對齊之後實質需要分配的是128K的記憶體大小。最後之所以需要儲存邊界對齊,是因為現代GPU是一個大的SIMD處理器也是RISC架構的,訪問邊界對齊的儲存具有巨大的效能優勢,甚至它們都不支援任意位置記憶體訪問,必須以邊界對齊的格式化的方式訪問儲存。而現代的x86架構的CPU則基本沒有這個限制,當然代價就是其指令的複雜性和一定的效能損失,而優勢就是在軟體編寫上尤其是變數儲存分配上基本沒有什麼限制。因此也可以這樣認為,如果我們在軟體編寫上有什麼奇怪的限制時,往往其實是因為底層硬體設計使然。這也就是需要學習一些底層硬體架構知識的原因。

結構體D3D12_HEAP_DESC中的Properties引數,則是用來說明堆的型別的,當Type欄位是D3D12_HEAP_TYPE_DEFAULT(預設堆),  D3D12_HEAP_TYPE_UPLOAD(上傳堆),D3D12_HEAP_TYPE_READBACK(回讀堆),後面兩個引數CPUPageProperty,MemoryPoolPreference就像這裡這樣賦值即可。或者理解為這些堆型別已經是系統預製好的,後面的引數在這些情況下實質上是沒有意義的。

只有當Type是D3D12_HEAP_TYPE_CUSTOM時我們才能指定後面兩個引數的值。通常這時我們需要指定CPUPageProperty就是CPU對這個堆記憶體的訪問特性,還有就是通過MemoryPoolPreference指定在那個儲存中——共享記憶體還是視訊記憶體中。對於自定義堆將放在後續的教程中酌情進行講解,現在我們暫時只關注前三種堆的型別。

最後我們需要指定的就是D3D12_HEAP_DESC 結構體的Flags引數,它的含義就是明確描述清楚這個堆上將用來儲存什麼資源。在這裡我們指定的是拒絕屬性,即拒絕放置渲染目標紋理和深度蠟板緩衝紋理以及普通緩衝,其含義就是說我們這個預設堆只是用來放置普通紋理的(因為普通紋理沒有被拒絕)。當然這個標誌值有點怪異,必須要反過來思考問題,即我們把不讓放置的資源型別都拒絕掉,那麼允許放置的就只有沒被拒絕的型別了。當然這裡還有個更奇怪的約定就是程式碼中使用的兩個拒絕(Deny)型別必須要一起設定,單獨設定一個是不被允許的。我想關於這個Flags欄位的這些奇怪約定和值,應該是GPU特殊要求的,才會是這樣一個嚴重反人類的樣子。

其實上述程式碼中真正需要我們關注的正是這個Flags標誌,後續通過它我們還可以指定獨立堆應該是多個GPU共享的形式,具體的一些用法我們將逐步的講解,就不在這裡一下子全部告訴大家,防止因為概念太多而不好理解和記憶。它實在是設計的太反人類了!

最後一步,我們就是使用ID3D12Device::CreateHeap方法來建立獨立的預設堆。

預設堆建立好之後,我們就使用CreatePlacedResource方法來建立具體的紋理資源了,程式碼如下:

stTextureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
stTextureDesc.MipLevels = 1;
stTextureDesc.Format = stTextureFormat; //DXGI_FORMAT_R8G8B8A8_UNORM;
stTextureDesc.Width = nTextureW;
stTextureDesc.Height = nTextureH;
stTextureDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
stTextureDesc.DepthOrArraySize = 1;
stTextureDesc.SampleDesc.Count = 1;
stTextureDesc.SampleDesc.Quality = 0;

//-----------------------------------------------------------------------------------------------------------
////建立預設堆上的資源,型別是Texture2D,GPU對預設堆資源的訪問速度是最快的
////因為紋理資源一般是不易變的資源,所以我們通常使用上傳堆複製到預設堆中
////在傳統的D3D11及以前的D3D介面中,這些過程都被封裝了,我們只能指定建立時的型別為預設堆 
//GRS_THROW_IF_FAILED(pID3DDevice->CreateCommittedResource(
//	&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT)
//	, D3D12_HEAP_FLAG_NONE
//	, &stTextureDesc //可以使用CD3DX12_RESOURCE_DESC::Tex2D來簡化結構體的初始化
//	, D3D12_RESOURCE_STATE_COPY_DEST
//	, nullptr
//	, IID_PPV_ARGS(&pITexture)));
//-----------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------------
//使用“定位方式”來建立紋理,注意下面這個呼叫內部實際已經沒有儲存分配和釋放的實際操作了,所以效能很高
//同時可以在這個堆上反覆呼叫CreatePlacedResource來建立不同的紋理,當然前提是它們不在被使用的時候,才考慮重用堆
GRS_THROW_IF_FAILED(pID3DDevice->CreatePlacedResource(
	pITextureHeap.Get()
	, 0
	, &stTextureDesc				//可以使用CD3DX12_RESOURCE_DESC::Tex2D來簡化結構體的初始化
	, D3D12_RESOURCE_STATE_COPY_DEST
	, nullptr
	, IID_PPV_ARGS(&pITexture)));
//-----------------------------------------------------------------------------------------------------------

//獲取上傳堆資源緩衝的大小,這個尺寸通常大於實際圖片的尺寸
n64UploadBufferSize = GetRequiredIntermediateSize(pITexture.Get(), 0, 1);

程式碼中的註釋已經說明了這段程式碼的一些要點。重要的就是要注意第二個引數,不再是Flags了,而是指定從堆開始處算起的偏移量,單位是位元組。主要用於在一個堆上不斷的Placed(放置)不同的資料。當然它也必須要邊界對齊,必須是64K或者4M的整數倍。

預設堆上的2D紋理建立完了,我們就需要建立獨立的上傳堆了,與建立預設堆類似,其程式碼如下:

D3D12_HEAP_DESC stUploadHeapDesc = {  };
//尺寸依然是實際紋理資料大小的2倍並64K邊界對齊大小
stUploadHeapDesc.SizeInBytes = GRS_UPPER(2 * n64UploadBufferSize, D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT);
//注意上傳堆肯定是Buffer型別,可以不指定對齊方式,其預設是64k邊界對齊
stUploadHeapDesc.Alignment = 0;
stUploadHeapDesc.Properties.Type = D3D12_HEAP_TYPE_UPLOAD;		//上傳堆型別
stUploadHeapDesc.Properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
stUploadHeapDesc.Properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
//上傳堆就是緩衝,可以擺放任意資料
stUploadHeapDesc.Flags = D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS;

GRS_THROW_IF_FAILED(pID3DDevice->CreateHeap(&stUploadHeapDesc, IID_PPV_ARGS(&pIUploadHeap)));

這段程式碼與之前的類似,需要注意的就是Flags標誌,這次我們又正面的指定只允許緩衝的型別D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS,堆大小我們特意設定為兩倍紋理圖片資料的大小,並且上邊界對齊。再一次領教它的反人類設計!

接著我們就可以像下面這樣首先來建立上傳堆上的紋理資源緩衝了:

GRS_THROW_IF_FAILED(pID3DDevice->CreatePlacedResource(
        pIUploadHeap.Get()
	, 0
	, &CD3DX12_RESOURCE_DESC::Buffer(n64UploadBufferSize)
	, D3D12_RESOURCE_STATE_GENERIC_READ
	, nullptr
	, IID_PPV_ARGS(&pITextureUpload)));

從上面程式碼我們可以看到其實只使用了一般不到的區域用來做我們的紋理資源資料緩衝了,那麼剩下的部分如果不用的話就浪費了,所以我們可以接著象下面這樣,把頂點緩衝也放在上面:

GRS_THROW_IF_FAILED(pID3DDevice->CreatePlacedResource(
	pIUploadHeap.Get()
	, GRS_UPPER(n64UploadBufferSize, D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT)
	, &CD3DX12_RESOURCE_DESC::Buffer(nVertexBufferSize)
	, D3D12_RESOURCE_STATE_GENERIC_READ
	, nullptr
	, IID_PPV_ARGS(&pIVertexBuffer)));

那麼請注意第二個引數,我們設定了偏移量為紋理資源大小上邊界對齊的位置,這裡的對齊要求是必須的。從記憶體檢視上看的話,其實就是我們將頂點緩衝區放在了需要上傳的紋理資源圖片資料的後面。這樣我們就省去了一次頂點緩衝儲存的實際的申請和釋放操作,提高了效率和效能。

4、動態取樣器

在第二篇教程中,我們已經演示和學習瞭如何使用根簽名的靜態引數化的方式來指定一個取樣器,如當時我們所說,靜態的取樣器是不能隨便改變的,如果要改變的話我們就需要重新建立一個根簽名物件,這對於取樣器的管理以及渲染管線狀態的管理來說都有些太囉嗦了。所以我們這次就使用純動態的方式來建立取樣器。同時為了對取樣器的型別建立一些初步的概念,所以我們就一次性建立了五個取樣器。

要使用動態取樣器,那麼首先我們需要的就是建立一個取樣器的描述符堆,程式碼如下:

D3D12_DESCRIPTOR_HEAP_DESC stSamplerHeapDesc = {};
stSamplerHeapDesc.NumDescriptors = nSampleMaxCnt;
stSamplerHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
stSamplerHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
			
GRS_THROW_IF_FAILED(pID3DDevice->CreateDescriptorHeap(&stSamplerHeapDesc,IID_PPV_ARGS(&pISamplerDescriptorHeap)));

nSamplerDescriptorSize = pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER);

CD3DX12_CPU_DESCRIPTOR_HANDLE hSamplerHeap(pISamplerDescriptorHeap->GetCPUDescriptorHandleForHeapStart());

D3D12_SAMPLER_DESC stSamplerDesc = {};
stSamplerDesc.Filter	 = D3D12_FILTER_MIN_MAG_MIP_LINEAR;

stSamplerDesc.MinLOD	= 0;
stSamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;
stSamplerDesc.MipLODBias = 0.0f;
stSamplerDesc.MaxAnisotropy = 1;
stSamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS;

// Sampler 1
stSamplerDesc.BorderColor[0] = 1.0f;
stSamplerDesc.BorderColor[1] = 0.0f;
stSamplerDesc.BorderColor[2] = 1.0f;
stSamplerDesc.BorderColor[3] = 1.0f;
stSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
stSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
stSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
pID3DDevice->CreateSampler(&stSamplerDesc, hSamplerHeap);

hSamplerHeap.Offset(nSamplerDescriptorSize);

// Sampler 2
stSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
stSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
stSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
pID3DDevice->CreateSampler(&stSamplerDesc, hSamplerHeap);

hSamplerHeap.Offset(nSamplerDescriptorSize);

// Sampler 3
stSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
stSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
stSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
pID3DDevice->CreateSampler(&stSamplerDesc, hSamplerHeap);

hSamplerHeap.Offset(nSamplerDescriptorSize);

// Sampler 4
stSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_MIRROR;
stSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_MIRROR;
stSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_MIRROR;
pID3DDevice->CreateSampler(&stSamplerDesc, hSamplerHeap);

hSamplerHeap.Offset(nSamplerDescriptorSize);

// Sampler 5
stSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_MIRROR_ONCE;
stSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_MIRROR_ONCE;
stSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_MIRROR_ONCE;
pID3DDevice->CreateSampler(&stSamplerDesc, hSamplerHeap);

與靜態取樣器類似我們都需要填充取樣器描述資訊的結構體,這裡主要是區別下每個紋理座標上具體的溢位後的取樣方式(本例中我們刻意將最大紋理座標擴大到了3.0f)。

這些型別我就不再具體囉嗦了,如果你不知道可以百度一下其他教程,或者你可以直接執行和除錯本章的例子程式加深理解。如果不太懂紋理,那麼先建立一個感性認識也是好的。

接著我們的要做的就是在建立根簽名時明確指出我們需要的是動態的取樣器,程式碼如下:

D3D12_FEATURE_DATA_ROOT_SIGNATURE stFeatureData = {};
// 檢測是否支援V1.1版本的根簽名
stFeatureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1;
if (FAILED(pID3DDevice->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &stFeatureData, sizeof(stFeatureData))))
{
	stFeatureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0;
}
// 在GPU上執行SetGraphicsRootDescriptorTable後,我們不修改命令列表中的SRV,因此我們可以使用預設Rang行為:
// D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTE
CD3DX12_DESCRIPTOR_RANGE1 stDSPRanges[2];
stDSPRanges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTE);
stDSPRanges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0);

CD3DX12_ROOT_PARAMETER1 stRootParameters[2];
stRootParameters[0].InitAsDescriptorTable(1, &stDSPRanges[0], D3D12_SHADER_VISIBILITY_PIXEL);
stRootParameters[1].InitAsDescriptorTable(1, &stDSPRanges[1], D3D12_SHADER_VISIBILITY_PIXEL);

//---------------------------------------------------------------------------------------------
//靜態的取樣器不用了,我們使用動態的在堆上建立的取樣器
//D3D12_STATIC_SAMPLER_DESC stSamplerDesc = {};
//stSamplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_POINT;
//stSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
//stSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
//stSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
//stSamplerDesc.MipLODBias = 0;
//stSamplerDesc.MaxAnisotropy = 0;
//stSamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;
//stSamplerDesc.BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
//stSamplerDesc.MinLOD = 0.0f;
//stSamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;
//stSamplerDesc.ShaderRegister = 0;
//stSamplerDesc.RegisterSpace = 0;
//stSamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
//---------------------------------------------------------------------------------------------

CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC stRootSignatureDesc;
//stRootSignatureDesc.Init_1_1(_countof(stRootParameters), stRootParameters
//	, 1, &stSamplerDesc
//	, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

stRootSignatureDesc.Init_1_1(_countof(stRootParameters), stRootParameters
	, 0, nullptr
	, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

ComPtr<ID3DBlob> pISignatureBlob;
ComPtr<ID3DBlob> pIErrorBlob;
GRS_THROW_IF_FAILED(D3DX12SerializeVersionedRootSignature(&stRootSignatureDesc
	, stFeatureData.HighestVersion
	, &pISignatureBlob
	, &pIErrorBlob));

GRS_THROW_IF_FAILED(pID3DDevice->CreateRootSignature(0
	, pISignatureBlob->GetBufferPointer()
	, pISignatureBlob->GetBufferSize()
	, IID_PPV_ARGS(&pIRootSignature)));

我們可以看到,根簽名中多了兩個根引數,分別指向一個SRV和一個Sample的描述符堆,這樣我們在具體渲染時就需要指定具體的Sample堆了,程式碼如下:

ID3D12DescriptorHeap* ppHeaps[] = { pISRVHeap.Get(),pISamplerDescriptorHeap.Get()};
pICommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
pICommandList->SetGraphicsRootDescriptorTable(0, pISRVHeap->GetGPUDescriptorHandleForHeapStart());

CD3DX12_GPU_DESCRIPTOR_HANDLE hGPUSampler(pISamplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart()
	, nCurrentSamplerNO
	, nSamplerDescriptorSize);
pICommandList->SetGraphicsRootDescriptorTable(1, hGPUSampler);

首先與之前的例子一樣,我們先把所有的描述符堆的物件指標編成一個數組整體的設定到命令列表中,實質也就是指定到渲染管線上去,然後我們在按照根引數中描述的對應序號挨個設定每個引數對應的描述符堆是哪個。這裡實質上設定的是GPU地址空間中的首地址,從其結構名字即可理解這一點,因為最終渲染是GPU來完成的,所以它當然需要知道的是自己的地址空間中的指標值。

另外為了動態切換每種取樣器,我們實現了一個簡單的功能就是按空格鍵來切換當前取樣器的序號,然後偏移到指定序號的取樣器首地址處,再將它設定到管線上,當然這裡就是發出一個命令,最終執行是需要在命令佇列中排隊等待圖形引擎順序執行的。

這裡再次強調一下,取樣器描述符堆跟其他的描述符堆一樣,本質上實際是個陣列,不要被Heap這個名字給迷惑了。整個過程簡單的理解,就是我們建立了一個有5個元素的陣列,然後按陣列索引號設定不同的陣列元素的首地址作為當前使用的描述符堆的首地址而已。只是因為程式碼的複雜性掩蓋了陣列操作的本質而已。

最後關於使用空格鍵改變序號的功能和其他的一些細節就請大家檢視最後的完整程式碼了。還有不清楚的地方請及時留意垂詢。

5、完整程式碼

#include <SDKDDKVer.h>
#define WIN32_LEAN_AND_MEAN // 從 Windows 頭中排除極少使用的資料
#include <windows.h>
#include <tchar.h>
//新增WTL支援 方便使用COM
#include <wrl.h>
using namespace Microsoft;
using namespace Microsoft::WRL;

#include <dxgi1_6.h>
#include <DirectXMath.h>
using namespace DirectX;
//for d3d12
#include <d3d12.h>
#include <d3dcompiler.h>

//linker
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "d3dcompiler.lib")

#if defined(_DEBUG)
#include <dxgidebug.h>
#endif

//for WIC
#include <wincodec.h>

#include "..\WindowsCommons\d3dx12.h"

#define GRS_WND_CLASS_NAME _T("Game Window Class")
#define GRS_WND_TITLE	_T("DirectX12 Texture Sample")

#define GRS_THROW_IF_FAILED(hr) if (FAILED(hr)){ throw CGRSCOMException(hr); }

//新定義的巨集用於上取整除法
#define GRS_UPPER_DIV(A,B) ((UINT)(((A)+((B)-1))/(B)))

//更簡潔的向上邊界對齊演算法 記憶體管理中常用 請記住
#define GRS_UPPER(A,B) ((UINT)(((A)+((B)-1))&~(B - 1)))

class CGRSCOMException
{
public:
	CGRSCOMException(HRESULT hr) : m_hrError(hr)
	{
	}
	HRESULT Error() const
	{
		return m_hrError;
	}
private:
	const HRESULT m_hrError;
};

struct WICTranslate
{
	GUID wic;
	DXGI_FORMAT format;
};

static WICTranslate g_WICFormats[] =
{//WIC格式與DXGI畫素格式的對應表,該表中的格式為被支援的格式
	{ GUID_WICPixelFormat128bppRGBAFloat,       DXGI_FORMAT_R32G32B32A32_FLOAT },

	{ GUID_WICPixelFormat64bppRGBAHalf,         DXGI_FORMAT_R16G16B16A16_FLOAT },
	{ GUID_WICPixelFormat64bppRGBA,             DXGI_FORMAT_R16G16B16A16_UNORM },

	{ GUID_WICPixelFormat32bppRGBA,             DXGI_FORMAT_R8G8B8A8_UNORM },
	{ GUID_WICPixelFormat32bppBGRA,             DXGI_FORMAT_B8G8R8A8_UNORM }, // DXGI 1.1
	{ GUID_WICPixelFormat32bppBGR,              DXGI_FORMAT_B8G8R8X8_UNORM }, // DXGI 1.1

	{ GUID_WICPixelFormat32bppRGBA1010102XR,    DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM }, // DXGI 1.1
	{ GUID_WICPixelFormat32bppRGBA1010102,      DXGI_FORMAT_R10G10B10A2_UNORM },

	{ GUID_WICPixelFormat16bppBGRA5551,         DXGI_FORMAT_B5G5R5A1_UNORM },
	{ GUID_WICPixelFormat16bppBGR565,           DXGI_FORMAT_B5G6R5_UNORM },

	{ GUID_WICPixelFormat32bppGrayFloat,        DXGI_FORMAT_R32_FLOAT },
	{ GUID_WICPixelFormat16bppGrayHalf,         DXGI_FORMAT_R16_FLOAT },
	{ GUID_WICPixelFormat16bppGray,             DXGI_FORMAT_R16_UNORM },
	{ GUID_WICPixelFormat8bppGray,              DXGI_FORMAT_R8_UNORM },

	{ GUID_WICPixelFormat8bppAlpha,             DXGI_FORMAT_A8_UNORM },
};

// WIC 畫素格式轉換表.
struct WICConvert
{
	GUID source;
	GUID target;
};

static WICConvert g_WICConvert[] =
{
	// 目標格式一定是最接近的被支援的格式
	{ GUID_WICPixelFormatBlackWhite,            GUID_WICPixelFormat8bppGray }, // DXGI_FORMAT_R8_UNORM

	{ GUID_WICPixelFormat1bppIndexed,           GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
	{ GUID_WICPixelFormat2bppIndexed,           GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
	{ GUID_WICPixelFormat4bppIndexed,           GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
	{ GUID_WICPixelFormat8bppIndexed,           GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM

	{ GUID_WICPixelFormat2bppGray,              GUID_WICPixelFormat8bppGray }, // DXGI_FORMAT_R8_UNORM
	{ GUID_WICPixelFormat4bppGray,              GUID_WICPixelFormat8bppGray }, // DXGI_FORMAT_R8_UNORM

	{ GUID_WICPixelFormat16bppGrayFixedPoint,   GUID_WICPixelFormat16bppGrayHalf }, // DXGI_FORMAT_R16_FLOAT
	{ GUID_WICPixelFormat32bppGrayFixedPoint,   GUID_WICPixelFormat32bppGrayFloat }, // DXGI_FORMAT_R32_FLOAT

	{ GUID_WICPixelFormat16bppBGR555,           GUID_WICPixelFormat16bppBGRA5551 }, // DXGI_FORMAT_B5G5R5A1_UNORM

	{ GUID_WICPixelFormat32bppBGR101010,        GUID_WICPixelFormat32bppRGBA1010102 }, // DXGI_FORMAT_R10G10B10A2_UNORM

	{ GUID_WICPixelFormat24bppBGR,              GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
	{ GUID_WICPixelFormat24bppRGB,              GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
	{ GUID_WICPixelFormat32bppPBGRA,            GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
	{ GUID_WICPixelFormat32bppPRGBA,            GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM

	{ GUID_WICPixelFormat48bppRGB,              GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
	{ GUID_WICPixelFormat48bppBGR,              GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
	{ GUID_WICPixelFormat64bppBGRA,             GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
	{ GUID_WICPixelFormat64bppPRGBA,            GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
	{ GUID_WICPixelFormat64bppPBGRA,            GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM

	{ GUID_WICPixelFormat48bppRGBFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
	{ GUID_WICPixelFormat48bppBGRFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
	{ GUID_WICPixelFormat64bppRGBAFixedPoint,   GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
	{ GUID_WICPixelFormat64bppBGRAFixedPoint,   GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
	{ GUID_WICPixelFormat64bppRGBFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
	{ GUID_WICPixelFormat48bppRGBHalf,          GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
	{ GUID_WICPixelFormat64bppRGBHalf,          GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT

	{ GUID_WICPixelFormat128bppPRGBAFloat,      GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
	{ GUID_WICPixelFormat128bppRGBFloat,        GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
	{ GUID_WICPixelFormat128bppRGBAFixedPoint,  GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
	{ GUID_WICPixelFormat128bppRGBFixedPoint,   GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
	{ GUID_WICPixelFormat32bppRGBE,             GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT

	{ GUID_WICPixelFormat32bppCMYK,             GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
	{ GUID_WICPixelFormat64bppCMYK,             GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
	{ GUID_WICPixelFormat40bppCMYKAlpha,        GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
	{ GUID_WICPixelFormat80bppCMYKAlpha,        GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM

	{ GUID_WICPixelFormat32bppRGB,              GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
	{ GUID_WICPixelFormat64bppRGB,              GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
	{ GUID_WICPixelFormat64bppPRGBAHalf,        GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
};

bool GetTargetPixelFormat(const GUID* pSourceFormat, GUID* pTargetFormat)
{//查表確定相容的最接近格式是哪個
	*pTargetFormat = *pSourceFormat;
	for (size_t i = 0; i < _countof(g_WICConvert); ++i)
	{
		if (InlineIsEqualGUID(g_WICConvert[i].source, *pSourceFormat))
		{
			*pTargetFormat = g_WICConvert[i].target;
			return true;
		}
	}
	return false;
}

DXGI_FORMAT GetDXGIFormatFromPixelFormat(const GUID* pPixelFormat)
{//查表確定最終對應的DXGI格式是哪一個
	for (size_t i = 0; i < _countof(g_WICFormats); ++i)
	{
		if (InlineIsEqualGUID(g_WICFormats[i].wic, *pPixelFormat))
		{
			return g_WICFormats[i].format;
		}
	}
	return DXGI_FORMAT_UNKNOWN;
}

struct GRS_VERTEX
{
	XMFLOAT3 m_vPos;		//Position
	XMFLOAT2 m_vTxc;		//Texcoord
};

UINT nCurrentSamplerNO = 0; //當前使用的取樣器索引
UINT nSampleMaxCnt = 5;		//建立五個典型的取樣器

LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR    lpCmdLine, int nCmdShow)
{
	::CoInitialize(nullptr);  //for WIC & COM

	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;

	UINT nTextureW = 0u;
	UINT nTextureH = 0u;
	UINT nBPP = 0u;
	UINT nPicRowPitch = 0;
	UINT64 n64UploadBufferSize = 0;
	DXGI_FORMAT stTextureFormat = DXGI_FORMAT_UNKNOWN;
	D3D12_PLACED_SUBRESOURCE_FOOTPRINT stTxtLayouts = {};
	D3D12_RESOURCE_DESC stTextureDesc = {};
	D3D12_RESOURCE_DESC stDestDesc = {};

	
	UINT nSamplerDescriptorSize = 0; //取樣器大小
	
	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<ID3D12CommandAllocator>		pICommandAllocator;
	ComPtr<ID3D12GraphicsCommandList>	pICommandList;

	ComPtr<IDXGISwapChain1>				pISwapChain1;
	ComPtr<IDXGISwapChain3>				pISwapChain3;
	ComPtr<ID3D12Resource>				pIARenderTargets[nFrameBackBufCount];
	ComPtr<ID3D12DescriptorHeap>		pIRTVHeap;

	ComPtr<ID3D12Heap>					pITextureHeap;
	ComPtr<ID3D12Heap>					pIUploadHeap;
	ComPtr<ID3D12Resource>				pITexture;
	ComPtr<ID3D12Resource>				pITextureUpload;
	ComPtr<ID3D12Resource>				pIVertexBuffer;
	ComPtr<ID3D12DescriptorHeap>		pISRVHeap;
	ComPtr<ID3D12DescriptorHeap>		pISamplerDescriptorHeap;
	
	ComPtr<ID3D12Fence>					pIFence;
	ComPtr<ID3DBlob>					pIBlobVertexShader;
	ComPtr<ID3DBlob>					pIBlobPixelShader;
	ComPtr<ID3D12RootSignature>			pIRootSignature;
	ComPtr<ID3D12PipelineState>			pIPipelineState;

	ComPtr<IWICImagingFactory>			pIWICFactory;
	ComPtr<IWICBitmapDecoder>			pIWICDecoder;
	ComPtr<IWICBitmapFrameDecode>		pIWICFrame;
	ComPtr<IWICBitmapSource>			pIBMP;

	try
	{
		//1、建立視窗
		{
			//---------------------------------------------------------------------------------------------
			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);

			hWnd = CreateWindowW(GRS_WND_CLASS_NAME
				, GRS_WND_TITLE
				, dwWndStyle
				, CW_USEDEFAULT
				, 0
				, rtWnd.right - rtWnd.left
				, rtWnd.bottom - rtWnd.top
				, nullptr
				, nullptr
				, hInstance
				, nullptr);

			if (!hWnd)
			{
				return FALSE;
			}

			ShowWindow(hWnd, nCmdShow);
			UpdateWindow(hWnd);
		}

		//2、使用WIC建立並載入一個圖片,並轉換為DXGI相容的格式
		{
			//---------------------------------------------------------------------------------------------
			//使用純COM方式建立WIC類廠物件,也是呼叫WIC第一步要做的事情
			GRS_THROW_IF_FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pIWICFactory)));

			//使用WIC類廠物件介面載入紋理圖片,並得到一個WIC解碼器物件介面,圖片資訊就在這個介面代表的物件中了
			WCHAR* pszTexcuteFileName = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\2-D3D12WICTexture\\Texture\\bear.jpg");
			GRS_THROW_IF_FAILED(pIWICFactory->CreateDecoderFromFilename(
				pszTexcuteFileName,              // 檔名
				NULL,                            // 不指定解碼器,使用預設
				GENERIC_READ,                    // 訪問許可權
				WICDecodeMetadataCacheOnDemand,  // 若需要就緩衝資料 
				&pIWICDecoder                    // 解碼器物件
			));

			// 獲取第一幀圖片(因為GIF等格式檔案可能會有多幀圖片,其他的格式一般只有一幀圖片)
			// 實際解析出來的往往是點陣圖格式資料
			GRS_THROW_IF_FAILED(pIWICDecoder->GetFrame(0, &pIWICFrame));

			WICPixelFormatGUID wpf = {};
			//獲取WIC圖片格式
			GRS_THROW_IF_FAILED(pIWICFrame->GetPixelFormat(&wpf));
			GUID tgFormat = {};

			//通過第一道轉換之後獲取DXGI的等價格式
			if (GetTargetPixelFormat(&wpf, &tgFormat))
			{
				stTextureFormat = GetDXGIFormatFromPixelFormat(&tgFormat);
			}

			if (DXGI_FORMAT_UNKNOWN == stTextureFormat)
			{// 不支援的圖片格式 目前退出了事 
			 // 一般 在實際的引擎當中都會提供紋理格式轉換工具,
			 // 圖片都需要提前轉換好,所以不會出現不支援的現象
				throw CGRSCOMException(S_FALSE);
			}

			if (!InlineIsEqualGUID(wpf, tgFormat))
			{// 這個判斷很重要,如果原WIC格式不是直接能轉換為DXGI格式的圖片時
			 // 我們需要做的就是轉換圖片格式為能夠直接對應DXGI格式的形式
				//建立圖片格式轉換器
				ComPtr<IWICFormatConverter> pIConverter;
				GRS_THROW_IF_FAILED(pIWICFactory->CreateFormatConverter(&pIConverter));

				//初始化一個圖片轉換器,實際也就是將圖片資料進行了格式轉換
				GRS_THROW_IF_FAILED(pIConverter->Initialize(
					pIWICFrame.Get(),                // 輸入原圖片資料
					tgFormat,						 // 指定待轉換的目標格式
					WICBitmapDitherTypeNone,         // 指定點陣圖是否有調色盤,現代都是真彩點陣圖,不用調色盤,所以為None
					NULL,                            // 指定調色盤指標
					0.f,                             // 指定Alpha閥值
					WICBitmapPaletteTypeCustom       // 調色盤型別,實際沒有使用,所以指定為Custom
				));
				// 呼叫QueryInterface方法獲得物件的點陣圖資料來源介面
				GRS_THROW_IF_FAILED(pIConverter.As(&pIBMP));
			}
			else
			{
				//圖片資料格式不需要轉換,直接獲取其點陣圖資料來源介面
				GRS_THROW_IF_FAILED(pIWICFrame.As(&pIBMP));
			}
			//獲得圖片大小(單位:畫素)
			GRS_THROW_IF_FAILED(pIBMP->GetSize(&nTextureW, &nTextureH));

			//獲取圖片畫素的位大小的BPP(Bits Per Pixel)資訊,用以計算圖片行資料的真實大小(單位:位元組)
			ComPtr<IWICComponentInfo> pIWICmntinfo;
			GRS_THROW_IF_FAILED(pIWICFactory->CreateComponentInfo(tgFormat, pIWICmntinfo.GetAddressOf()));

			WICComponentType type;
			GRS_THROW_IF_FAILED(pIWICmntinfo->GetComponentType(&type));

			if (type != WICPixelFormat)
			{
				throw CGRSCOMException(S_FALSE);
			}

			ComPtr<IWICPixelFormatInfo> pIWICPixelinfo;
			GRS_THROW_IF_FAILED(pIWICmntinfo.As(&pIWICPixelinfo));

			// 到這裡終於可以得到BPP了,這也是我看的比較吐血的地方,為了BPP居然饒了這麼多環節
			GRS_THROW_IF_FAILED(pIWICPixelinfo->GetBitsPerPixel(&nBPP));

			// 計算圖片實際的行大小(單位:位元組),這裡使用了一個上取整除法即(A+B-1)/B ,
			// 這曾經被傳說是微軟的面試題,希望你已經對它瞭如指掌
			nPicRowPitch = GRS_UPPER_DIV(uint64_t(nTextureW) * uint64_t(nBPP), 8);
		}

		//3、開啟顯示子系統的除錯支援
		{
#if defined(_DEBUG)
			ComPtr<ID3D12Debug> debugController;
			if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
			{
				debugController->EnableDebugLayer();
				// 開啟附加的除錯支援
				nDXGIFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
			}
#endif
		}

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

		//5、列舉介面卡,並選擇合適的介面卡來建立3D裝置物件
		{
			for (UINT adapterIndex = 1; 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)));
		}

		//6、建立