前言
現在開始迎來所謂的高級篇了,目前計划是計算着色器部分的內容視項目情況,大概會分3-5章來講述。
DirectX11 With Windows SDK完整目錄
歡迎加入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編程中,我們編寫的着色器程序會同時給大量的線程運行,可以將這些線程按網格來划分成線程組。一個線程組由一個多處理器來執行,如果你的GPU有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,即我們進行了這樣的調用:
m_pd3dDeviceContext->Dispatch(3, 2, 1);
第一份計算着色器程序
現在我們有這兩張圖片,我想要將它混合並將結果輸出到一張圖片:
下面的這個着色器負責對兩個紋理的像素顏色進行分量乘法運算。
Texture2D g_TexA : register(t0);
Texture2D g_TexB : register(t1);
RWTexture2D<float4> g_Output : register(u0);
// 一個線程組中的線程數目。線程可以1維展開,也可以
// 2維或3維排布
[numthreads(16, 16, 1)]
void CS( uint3 DTid : SV_DispatchThreadID )
{
g_Output[DTid.xy] = g_TexA[DTid.xy] * g_TexB[DTid.xy];
}
上面的代碼有如下需要注意的:
Texture2D
僅能作為輸入,但RWTexture2D<T>
類型支持讀寫,在本樣例中主要是用於輸出RWTexture2D<T>
使用時也需要指定寄存器,u
說明使用的是無序訪問視圖寄存器[numthreads(X, Y, Z)]
修飾符指定了一個線程組包含的線程數目,以及在3D網格中的布局- 每個線程都會執行一遍該函數
SV_DispatchThreadID
是當前線程在3D網格中所處的位置,每個線程都有獨立的SV_DispatchThreadID
Texture2D
除了使用Sample方法來獲取像素外,還支持通過索引的方式來指定像素
如果使用1D紋理,線程修飾符通常為[numthreads(X, 1, 1)]
或[numthreads(1, Y, 1)]
如果使用2D紋理,線程修飾符通常為[numthreads(X, Y, 1)]
,即第三維度為1
2D紋理X和Y的值會影響你在調度線程組時填充的參數
注意:
- 在
cs_4_x
下,一個線程組的最大線程數為768,且Z的最大值為1.- 在
cs_5_0
下,一個線程組的最大線程數為1024,且Z的最大值為64.
紋理輸出與無序訪問視圖
留意上面着色器代碼中的類型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_FORMAT
為unorm
,HLSL
類型為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_FLOAT
和float4
,個人猜測HLSL的類型為了能傳遞給DXGI_FORMAT,允許做丟失精度的同類型轉換。
現在我們回到C++代碼,現在需要創建一個2D紋理,然后在此基礎上再創建無序訪問視圖作為着色器輸出。
bool GameApp::InitResource()
{
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\flare.dds",
nullptr, m_pTextureInputA.GetAddressOf()));
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\flarealpha.dds",
nullptr, m_pTextureInputB.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(m_pd3dDevice->CreateTexture2D(&texDesc, nullptr, m_pTextureOutputA.GetAddressOf()));
texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
HR(m_pd3dDevice->CreateTexture2D(&texDesc, nullptr, m_pTextureOutputB.GetAddressOf()));
// 創建無序訪問視圖
D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
uavDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D;
uavDesc.Texture2D.MipSlice = 0;
HR(m_pd3dDevice->CreateUnorderedAccessView(m_pTextureOutputA.Get(), &uavDesc,
m_pTextureOutputA_UAV.GetAddressOf()));
uavDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
HR(m_pd3dDevice->CreateUnorderedAccessView(m_pTextureOutputB.Get(), &uavDesc,
m_pTextureOutputB_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(m_pd3dDevice->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pTextureMul_R32G32B32A32_CS.GetAddressOf()));
HR(CreateShaderFromFile(L"HLSL\\TextureMul_R8G8B8A8_CS.cso",
L"HLSL\\TextureMul_R8G8B8A8_CS.hlsl", "CS", "cs_5_0", blob.GetAddressOf()));
HR(m_pd3dDevice->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pTextureMul_R8G8B8A8_CS.GetAddressOf()));
return true;
}
觀察上面的代碼,如果我們想要讓紋理綁定到無序訪問視圖,就需要提供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::Compute()
{
assert(m_pd3dImmediateContext);
m_pd3dImmediateContext->CSSetShaderResources(0, 1, m_pTextureInputA.GetAddressOf());
m_pd3dImmediateContext->CSSetShaderResources(1, 1, m_pTextureInputB.GetAddressOf());
// DXGI Format: DXGI_FORMAT_R32G32B32A32_FLOAT
// Pixel Format: A32B32G32R32
m_pd3dImmediateContext->CSSetShader(m_pTextureMul_R32G32B32A32_CS.Get(), nullptr, 0);
m_pd3dImmediateContext->CSSetUnorderedAccessViews(0, 1, m_pTextureOutputA_UAV.GetAddressOf(), nullptr);
m_pd3dImmediateContext->Dispatch(32, 32, 1);
// DXGI Format: DXGI_FORMAT_R8G8B8A8_SNORM
// Pixel Format: A8B8G8R8
m_pd3dImmediateContext->CSSetShader(m_pTextureMul_R8G8B8A8_CS.Get(), nullptr, 0);
m_pd3dImmediateContext->CSSetUnorderedAccessViews(0, 1, m_pTextureOutputB_UAV.GetAddressOf(), nullptr);
m_pd3dImmediateContext->Dispatch(32, 32, 1);
HR(SaveDDSTextureToFile(m_pd3dImmediateContext.Get(), m_pTextureOutputA.Get(), L"Texture\\flareoutputA.dds"));
HR(SaveDDSTextureToFile(m_pd3dImmediateContext.Get(), m_pTextureOutputB.Get(), L"Texture\\flareoutputB.dds"));
MessageBox(nullptr, L"請打開Texture文件夾觀察輸出文件flareoutputA.dds和flareoutputB.dds", L"運行結束", MB_OK);
}
由於我們的位圖是512x512x1大小,一個線程組的線程布局為16x16x1,線程組的數目自然就是32x32x1了。如果調度的線程組寬度或高度不夠,輸出的位圖也不完全。而如果提供了過寬或過高的線程組並不會影響運行結果,只是提供的線程組資源過多有些浪費而已。
最后通過ScreenGrab庫將紋理保存到文件,就可以結束程序了。
運行結束后,可以打開flareoutputA.dds
查看結果(建議使用DxTex打開):
那么問題來了,如果我想要輸出DXGI_FORMAT_R8G8B8A8_UNORM
的紋理,那應該怎么做呢?
- 將紋理創建時使用的
DXGI_FORMAT
換成DXGI_FORMAT_R8G8B8A8_UNORM
,連同UAV的Format
也要替換 - 計算着色器將
RWTexture2D<float4>
類型替換成RWTexture2D<unorm float4>
類型
修改后的着色器代碼如下:
Texture2D g_TexA : register(t0);
Texture2D g_TexB : register(t1);
RWTexture2D<unorm float4> g_Output : register(u0);
// 一個線程組中的線程數目。線程可以1維展開,也可以
// 2維或3維排布
[numthreads(16, 16, 1)]
void CS( uint3 DTid : SV_DispatchThreadID )
{
g_Output[DTid.xy] = (unorm float4)(g_TexA[DTid.xy] * g_TexB[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視紋理數據類型而定。
用法如下:
g_Output.mip[g_MipSlice][DTid.xy] =
(unorm float4)(g_TexA.mip[gMipSlice][DTid.xy] * g_TexB.mip[g_MipSlice][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 = g_Tex.SampleLevel(g_Sam, pIn.Tex, 0.0f); // 使用第一個mip等級的紋理子資源
還有一種辦法則是使用Load
方法,它需要傳遞int3類型的參數,其中xy分量分別對應x和y方向基於0的索引(而不是0.0-1.0),z分量則為要使用的mipmap等級:
float4 texColor = g_Tex.Load(int3(DTid.xy, g_MipSlice))
練習題
粗體字為自定義題目
- 嘗試修改
ID3D11DeviceContext::Dispatch
的參數,觀察運行結果 - 嘗試利用計算着色器來計算出兩張紋理的顏色差異值,並保存為圖片觀察結果
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。