DirectX11 With Windows SDK--26 計算着色器:入門

前言

現在開始迎來所謂的高級篇了,目前計劃是計算着色器部分的內容視項目情況,大概會分3-5章來講述。

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裏彙報。

概述

這裏所使用的計算着色器實際上是屬於DirectCompute的一部分,DirectCompute是一種應用程序編程接口(API),最初與DirectX 11 API 一起發佈,但如果你的顯卡只支持到特性等級10.x,那麼你只能使用到計算着色器的有限功能,這裏不討論。

GPU通常被設計爲從一個位置或連續的位置讀取並處理大量的內存數據(即流操作),而CPU則被設計爲專門處理隨機內存的訪問。

由於頂點數據和像素數據可以分開處理,GPU架構使得它能夠高度並行,在處理圖像上效率非常高。但是一些非圖像應用程序也能夠利用GPU強大的並行計算能力以獲得效益。GPU用在非圖像用途的應用程序可以稱之爲:通用GPU(GPGPU)編程。

GPU需要數據並行的算法才能從GPU的並行架構中獲得優勢,並不是所有的算法都適合用GPU來實現。對於大量的數據,我們需要保證它們都進行相似的操作以確保並行處理。比如頂點着色器都是對大量的頂點數據進行處理,而像素着色器也是對大量的像素片元進行處理。

對於GPGPU編程,用戶通常需要從顯存中獲取運算結果,將其傳回CPU。這需要從顯存將結果複製到內存中,這樣雖然速度會慢一些,但起碼還是比直接在CPU運算會快很多。如果是用於圖形編程的話倒是可以省掉數據傳回CPU的時間,比如說我們要對渲染好的場景再通過計算着色器來進行一次模糊處理。

在Direct3D中,計算着色器也是一個可編程着色器,它並不屬於渲染管線的一個直接過程。我們可以通過它對GPU資源進行讀寫操作,運行的結果通常會保存在Direct3D的資源中,我們可以將它作爲結果顯示到屏幕,可以給別的地方作爲輸入使用,甚至也可以將它保存到本地。

線程和線程組

在GPU編程中,我們編寫的着色器程序會同時給大量的線程運行,可以將這些線程按網格來劃分成線程組。一個線程組由一個多處理器來執行,如果你的CPU有16個多處理器,你會想要把問題分解成至少16個線程組以保證每個多處理器都工作。爲了獲取更好的性能,你看你會想要每個多處理器來處理至少2個線程組,這樣當一個線程組在等待別的資源時就可以先去考慮完成另一個線程組的工作。

每個線程組都會獲得共享內存,這樣每個線程都可以訪問它。但是不同的線程組不能相互訪問對方獲得的共享內存。

線程同步操作可以在線程組中的線程之間進行,但處於不同線程組的兩個線程無法被同步。事實上,我們沒有辦法控制不同線程組的處理順序,畢竟線程組可以在不同的多處理器上執行。

一個線程組由N個線程組成。硬件實際上會將這些線程劃分成一系列warps(一個warp包含32個線程),並且一個warp由SIMD32中的多處理器進行處理(32個線程同時執行相同的指令)。在Direct3D中,你可以指定一個線程組不同維度下的大小使得它不是32的倍數,但是出於性能考慮,最好還是把線程組的維度大小設爲warp的倍數。

將線程組的大小設爲256看起來是個比較好的選擇,它適用於大量的硬件情況。修改線程組的大小意味着你還需要修改需要調度的線程組數目。

注意:NVIDIA硬件中,每個warp包含32個線程。而ATI則是每個wavefront包含64個線程。warp或者wavefront的大小可能隨後續硬件的升級有所修改。

ID3D11DeviceContext::Dispatch方法–調度線程組執行計算着色器程序

方法如下:

void ID3D11DeviceContext::Dispatch(
	UINT ThreadGroupCountX,		// [In]X維度下線程組數目
	UINT ThreadGroupCountY,		// [In]Y維度下線程組數目
	UINT ThreadGroupCountZ);	// [In]Z維度下線程組數目

可以看到上面列出了X, Y, Z三個維度,說明線程組本身是可以3維的。當前例子的一個線程組包含了8x8x1個線程,而線程組數目爲3x2x1,即我們進行了這樣的調用:

md3dDeviceContext->Dispatch(3, 2, 1);

第一份計算着色器程序

現在我們有這兩張圖片,我想要將它混合並將結果輸出到一張圖片:

下面的這個着色器負責對兩個紋理的像素顏色進行分量乘法運算。

Texture2D gTexA : register(t0);
Texture2D gTexB : register(t1);

RWTexture2D<float4> gOutput : register(u0);

// 一個線程組中的線程數目。線程可以1維展開,也可以
// 2維或3維排布
[numthreads(16, 16, 1)]
void CS( uint3 DTid : SV_DispatchThreadID )
{
    gOutput[DTid.xy] = gTexA[DTid.xy] * gTexB[DTid.xy];
}

上面的代碼有如下需要注意的:

  1. Texture2D僅能作爲輸入,但RWTexture2D<T>類型支持讀寫,在本樣例中主要是用於輸出
  2. RWTexture2D<T>使用時也需要指定寄存器,u說明使用的是無序訪問視圖寄存器
  3. [numthreads(X, Y, Z)]修飾符指定了一個線程組包含的線程數目,以及在3D網格中的佈局
  4. 每個線程都會執行一遍該函數
  5. SV_DispatchThreadID是當前線程在3D網格中所處的位置,每個線程都有獨立的SV_DispatchThreadID
  6. Texture2D除了使用Sample方法來獲取像素外,還支持通過索引的方式來指定像素

如果使用1D紋理,線程修飾符通常爲[numthreads(X, 1, 1)][numthreads(1, Y, 1)]

如果使用2D紋理,線程修飾符通常爲[numthreads(X, Y, 1)],即第三維度爲1

2D紋理X和Y的值會影響你在調度線程組時填充的參數

紋理輸出與無序訪問視圖

留意上面着色器代碼中的類型RWTexture2D<T>,你可以對他進行像素寫入,也可以從中讀取像素。不過模板參數類型填寫就比較講究了。我們需要保證紋理的數據格式和RWTexture2D<T>的模板參數類型一致,這裏使用下表來描述比較常見的紋理數據類型和HLSL類型的對應關係:

DXGI_FORMAT HLSL類型
DXGI_FORMAT_R32_FLOAT float
DXGI_FORMAT_R32G32_FLOAT float2
DXGI_FORMAT_R32G32B32A32_FLOAT float4
DXGI_FORMAT_R32_UINT uint
DXGI_FORMAT_R32G32_UINT uint2
DXGI_FORMAT_R32G32B32A32_UINT uint4
DXGI_FORMAT_R32_SINT int
DXGI_FORMAT_R32G32_SINT int2
DXGI_FORMAT_R32G32B32A32_SINT int4
DXGI_FORMAT_R16G16B16A16_FLOAT float4
DXGI_FORMAT_R8G8B8A8_UNORM unorm float4
DXGI_FORMAT_R8G8B8A8_SNORM snorm float4

此外,UAV不支持DXGI_FORMAT_B8G8R8A8_UNORM

其中unorm float表示的是一個32位無符號的,規格化的浮點數,可以表示範圍0到1
而與之對應的snorm float表示的是32位有符號的,規格化的浮點數,可以表示範圍-1到1

從上表可以得知DXGI_FORMAT枚舉值的後綴要和HLSL的類型對應(浮點型對應浮點型,整型對應整型,規格化浮點型對應規格化浮點型),否則可能會引發下面的錯誤(這裏舉DXGI_FORMATunormHLSL類型爲float的例子):

D3D11 ERROR: ID3D11DeviceContext::Dispatch: The resource return type for component 0 declared in the shader code (FLOAT) is not compatible with the resource type bound to Unordered Access View slot 0 of the Compute Shader unit (UNORM). This mismatch is invalid if the shader actually uses the view (e.g. it is not skipped due to shader code branching). [ EXECUTION ERROR #2097372: DEVICE_UNORDEREDACCESSVIEW_RETURN_TYPE_MISMATCH]

由於DXGI_FORMAT的部分格式比較緊湊,HLSL中能表示的最小類型通常又比較大。比如DXGI_FORMAT_R16G16B16A16_FLOATfloat4,個人猜測HLSL的類型爲了能傳遞給DXGI_FORMAT,允許做丟失精度的同類型轉換。

現在我們回到C++代碼,現在需要創建一個2D紋理,然後在此基礎上再創建無序訪問視圖作爲着色器輸出。

bool GameApp::InitResource()
{

	HR(CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\\flare.dds",
		nullptr, mTextureInputA.GetAddressOf()));
	HR(CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\\flarealpha.dds",
		nullptr, mTextureInputB.GetAddressOf()));

	// 創建用於UAV的紋理,必須是非壓縮格式
	D3D11_TEXTURE2D_DESC texDesc;
	texDesc.Width = 512;
	texDesc.Height = 512;
	texDesc.MipLevels = 1;
	texDesc.ArraySize = 1;
	texDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
	texDesc.SampleDesc.Count = 1;
	texDesc.SampleDesc.Quality = 0;
	texDesc.Usage = D3D11_USAGE_DEFAULT;
	texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE |
		D3D11_BIND_UNORDERED_ACCESS;
	texDesc.CPUAccessFlags = 0;
	texDesc.MiscFlags = 0;

	HR(md3dDevice->CreateTexture2D(&texDesc, nullptr, mTextureOutputA.GetAddressOf()));

	// 創建無序訪問視圖
	D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
	uavDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
	uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D;
	uavDesc.Texture2D.MipSlice = 0;
	HR(md3dDevice->CreateUnorderedAccessView(mTextureOutputA.Get(), &uavDesc,
		mTextureOutputA_UAV.GetAddressOf()));
	
	// 創建計算着色器
	ComPtr<ID3DBlob> blob;
	HR(CreateShaderFromFile(L"HLSL\\TextureMul_R32G32B32A32_CS.cso",
		L"HLSL\\TextureMul_R32G32B32A32_CS.hlsl", "CS", "cs_5_0", blob.GetAddressOf()));
	HR(md3dDevice->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mTextureMul_R32G32B32A32_CS.GetAddressOf()));
}	

觀察上面的代碼,如果我們想要讓紋理綁定到無序訪問視圖,就需要提供D3D11_BIND_UNORDERED_ACCESS綁定標籤。

注意:如果你還爲紋理創建了着色器資源視圖,那麼UAV和SRV不能同時綁定到渲染管線上。

ID3D11DeviceContext::CSSetUnorderedAccessViews–計算着色階段設置無序訪問視圖

void ID3D11DeviceContext::CSSetUnorderedAccessViews(
	UINT                      StartSlot,						// [In]起始槽,值與寄存器對應
	UINT                      NumUAVs,							// [In]UAV數目
	ID3D11UnorderedAccessView * const *ppUnorderedAccessViews,	// [In]UAV數組
	const UINT                *pUAVInitialCounts				// [In]忽略
);

調度過程實現如下:

void GameApp::DrawScene()
{
	assert(md3dImmediateContext);
	assert(mSwapChain);

	md3dImmediateContext->CSSetShaderResources(0, 1, mTextureInputA.GetAddressOf());
	md3dImmediateContext->CSSetShaderResources(1, 1, mTextureInputB.GetAddressOf());

	// DXGI Format: DXGI_FORMAT_R32G32B32A32_FLOAT
	// Pixel Format: A32B32G32R32
	md3dImmediateContext->CSSetShader(mTextureMul_R32G32B32A32_CS.Get(), nullptr, 0);
	md3dImmediateContext->CSSetUnorderedAccessViews(0, 1, mTextureOutputA_UAV.GetAddressOf(), nullptr);
	md3dImmediateContext->Dispatch(32, 32, 1);

	HR(SaveDDSTextureToFile(md3dImmediateContext.Get(), mTextureOutputA.Get(), L"Texture\\flareoutputA.dds"));
	
	SendMessage(MainWnd(), WM_DESTROY, 0, 0);
}

由於我們的位圖是512x512x1大小,一個線程組的線程佈局爲16x16x1,線程組的數目自然就是32x32x1了。如果調度的線程組寬度或高度不夠,輸出的位圖也不完全。而如果提供了過寬或過高的線程組並不會影響運行結果,只是提供的線程組資源過多有些浪費而已。

最後通過ScreenGrab庫將紋理保存到文件,就可以結束程序了。

運行結束後,可以打開flareoutputA.dds查看結果(建議使用DxTex打開):

那麼問題來了,如果我想要輸出DXGI_FORMAT_R8G8B8A8_UNORM的紋理,那應該怎麼做呢?

  1. 將紋理創建時使用的DXGI_FORMAT換成DXGI_FORMAT_R8G8B8A8_UNORM,連同UAV的Format也要替換
  2. 計算着色器將RWTexture2D<float4>類型替換成RWTexture2D<unorm float4>類型

修改後的着色器代碼如下:

Texture2D gTexA : register(t0);
Texture2D gTexB : register(t1);

RWTexture2D<unorm float4> gOutput : register(u0);

// 一個線程組中的線程數目。線程可以1維展開,也可以
// 2維或3維排布
[numthreads(16, 16, 1)]
void CS( uint3 DTid : SV_DispatchThreadID )
{
    gOutput[DTid.xy] = (unorm float4)(gTexA[DTid.xy] * gTexB[DTid.xy]);
}

注意:如果你使用了HLSL Tools For Visual Studio插件,它不認unorm類型,從而引發所謂的語法錯誤提示。你可以直接無視去編譯項目,它還是能成功編譯出着色器的。

運行的圖片顯示結果基本上是一樣的,只是輸出的紋理格式不太一樣:

紋理子資源的採樣和索引

從上面的例子可以看到,我們能夠使用2D索引來指定紋理的某一像素。如果2D索引越界訪問,在計算着色器中是擁有良好定義的:讀取越界資源將返回0,嘗試寫入越界資源的操作將不會執行。

但是這種採樣只針對mip等級爲0的紋理子資源,如果我們想指定其它mip等級的紋理子資源,可以使用mip.operator[][]方法:

R mips.Operator[][](
  in uint mipSlice,		// [In]mip切片值
  in uint2 pos			// [In]2D索引
);

返回值R視紋理數據類型而定。

用法如下:

gOutput.mip[gMipSlice][DTid.xy] = 
	(unorm float4)(gTexA.mip[gMipSlice][DTid.xy] * gTexB.mip[gMipSlice][DTid.xy]);

不過我們的演示程序用到的紋理Mip等級都爲1,這裏就不在代碼端演示了。

紋理的Sample方法通常情況下你是不知道它具體選擇的是哪些Mip等級的紋理子資源來進行採樣,具體的行爲交給採樣器狀態來決定。但是我們可以使用SampleLevel方法來指定要對紋理的哪個mip等級的子資源進行採樣:

R SampleLevel(
	in SamplerState S,		// [In]採樣器狀態
	in float2 Location,		// [In]紋理座標
	in float LOD			// [In]mip等級
);

當LOD爲整數時,指定的就是具體某個mip等級的紋理,但如果LOD爲浮點數,如3.3f,則會對mip等級爲3和4的紋理子資源都進行一次採樣,然後根據小數部分進行線性插值求得最終的插值顏色。

用法如下:

float4 texColor = gTex.SampleLevel(gSam, pIn.Tex, 0.0f);	// 使用第一個mip等級的紋理子資源

練習題

粗體字爲自定義題目

  1. 嘗試修改ID3D11DeviceContext::Dispatch的參數,觀察運行結果
  2. 嘗試利用計算着色器來計算出兩張紋理的顏色差異值,並保存爲圖片觀察結果

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裏彙報。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章