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

前言

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

DirectX11 With Windows SDK完整目錄html

Github項目源碼git

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。github

概述

這裏所使用的計算着色器其實是屬於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];
}

上面的代碼有以下須要注意的:

  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的值會影響你在調度線程組時填充的參數

注意:

  1. cs_4_x下,一個線程組的最大線程數爲768,且Z的最大值爲1.
  2. 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_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(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的紋理,那應該怎麼作呢?

  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,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索