1. 程式人生 > >D3D12渲染技術之編譯Shader

D3D12渲染技術之編譯Shader

很多人只知道寫Shader,但是並不瞭解DX或者OpenGL是如何編譯Shader的,我們寫的Shader是一種文字檔案,它可以被DX或者OpenGL讀取,說明它們提供了介面編譯Shader。3D引擎都與DX或者OpenGL相關的,本篇部落格就給讀者介紹如何編譯Shader的。 在Direct3D中,必須首先將著色器程式編譯為可移植位元組碼, 然後,圖形驅動程式將採用此位元組碼並將其再次編譯為系統GPU 的最佳本機指令。 在執行時,我們可以使用以下函式編譯著色器:

HRESULT D3DCompileFromFile(
  LPCWSTR pFileName,
  const D3D_SHADER_MACRO *pDefines
, ID3DInclude *pInclude, LPCSTR pEntrypoint, LPCSTR pTarget, UINT Flags1, UINT Flags2, ID3DBlob **ppCode, ID3DBlob **ppErrorMsgs);

介紹一下,上面介面的引數含義: pFileName:包含我們要編譯的HLSL原始碼的.hlsl檔案的名稱。 pDefines:我們不使用的高階選項; 請參閱SDK文件。 我們總是在案例中指定null; pInclude:我們不使用的高階選項; 請參閱SDK文件。 我們總是在案例中指定null。 pEntrypoint:著色器入口點的函式名稱, .hlsl可以包含多個著色器程式(例如,一個頂點著色器和一個畫素著色器),因此我們需要指定我們想要編譯的特定著色器的入口點。 pTarget:一個字串,指定我們正在使用的著色器程式型別和版本。 在本部落格中,我們以5.0和5.1為目標。 a)vs_5_0和vs_5_1:分別為頂點著色器5.0和5.1。 b)hs_5_0和hs_5_1:分別為Hull著色器5.0和5.1。 c)ds_5_0和ds_5_1:域著色器5.0和5.1。 d)gs_5_0和gs_5_1:分別為幾何著色器5.0和5.1。 e)ps_5_0和ps_5_1:分別為畫素著色器5.0和5.1。 f)cs_5_0和cs_5_1:分別計算著色器5.0和5.1。 Flags1:標誌,用於指定著色器程式碼的編譯方式, SDK文件中列出了相當多的這些標誌,但我們在本部落格中使用的只有兩個: a)D3DCOMPILE_DEBUG:在除錯模式下編譯著色器。 b)D3DCOMPILE_SKIP_OPTIMIZATION:指示編譯器跳過優化(對除錯很有用)。 Flags2:我們不使用的高階效果編譯選項; 請參閱SDK文件。 ppCode:返回指向儲存已編譯著色器物件位元組碼的ID3DBlob資料結構的指標。 ppErrorMsgs:返回指向ID3DBlob資料結構的指標,該資料結構儲存包含編譯錯誤的字串(如果有)。 型別ID3DBlob只是一個通用的記憶體塊,有兩種方法: LPVOID GetBufferPointer:返回資料的void *,因此在使用之前必須將其轉換為適當的型別(請參閱下面的示例)。 SIZE_T GetBufferSize:返回緩衝區的位元組大小。 為了支援錯誤輸出,我們實現了以下幫助函式,以便在執行時在d3dUtil.h / .cpp中編譯著色器:

ComPtr<ID3DBlob> d3dUtil::CompileShader(
   const std::wstring& filename,
   const D3D_SHADER_MACRO* defines,
   const std::string& entrypoint,
   const std::string& target)
{
 // Use debug flags in debug mode.
  UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG) 
  compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
HRESULT hr = S_OK; ComPtr<ID3DBlob> byteCode = nullptr; ComPtr<ID3DBlob> errors; hr = D3DCompileFromFile(filename.c_str(), defines, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint.c_str(), target.c_str(), compileFlags, 0, &byteCode, &errors); // Output errors to debug window. if(errors != nullptr) OutputDebugStringA((char*)errors->GetBufferPointer()); ThrowIfFailed(hr); return byteCode; } Here is an example of calling this function: ComPtr<ID3DBlob> mvsByteCode = nullptr; ComPtr<ID3DBlob> mpsByteCode = nullptr; mvsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "VS", "vs_5_0"); mpsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "PS", "ps_5_0");

HLSL錯誤和警告將通過ppErrorMsgs引數返回, 例如,如果我們錯誤拼寫了mul函式,那麼我們將以下錯誤輸出到除錯視窗: Shaders\color.hlsl(29,14-55): error X3004: undeclared identifier ‘mu’ 編譯著色器不會將其繫結到渲染管道以供使用,我們將在後面部落格中看到如何做到這一點。 我們可以在單獨的步驟(例如,構建步驟或作為資產內容管道流程的一部分)離線編譯它們,而不是在執行時編譯著色器。 有幾個原因可以做到這一點:

對於複雜的著色器,編譯可能需要很長時間, 因此,離線編譯將使載入時間更快。

在構建過程的早期而不是在執行時,很容易看到著色器編譯錯誤。 對編譯的著色器使用.cso(已編譯的著色器物件)副檔名是常見做法。

要離線編譯著色器,我們使用DirectX附帶的FXC工具。 這是一個命令列工具。 要分別使用入口點VS和PS編譯儲存在color.hlsl中的頂點和畫素著色器,我們將編寫:

fxc "color.hlsl" /Od /Zi /T vs_5_0 /E "VS" /Fo "color_vs.cso" /Fc "color_vs.asm"
fxc "color.hlsl" /Od /Zi /T ps_5_0 /E "PS" /Fo "color_ps.cso" /Fc "color_ps.asm"

要分別使用儲存在color.hlsl中的頂點和畫素著色器,我們將編寫:

fxc "color.hlsl" /T vs_5_0 /E "VS" /Fo "color_vs.cso" /Fc "color_vs.asm"
fxc "color.hlsl" /T ps_5_0 /E "PS" /Fo "color_ps.cso" /Fc "color_ps.asm"

這裡寫圖片描述 如果嘗試使用語法錯誤編譯著色器,FXC會將錯誤/警告輸出到命令視窗。 例如,如果我們錯誤地命名color.hlsl效果檔案中的變數:

// Should be gWorldViewProj, not worldViewProj!
vout.PosH = mul(float4(vin.Pos, 1.0f), worldViewProj);

然後我們從首次亮相輸出視窗中列出的這一個錯誤(頂部錯誤是要修復的關鍵錯誤)中得到了很多錯誤:

color.hlsl(29,42-54): error X3004: undeclared identifier ‘worldViewProj’
color.hlsl(29,14-55): error X3013: ‘mul’: no matching 2 parameter intrinsic function
color.hlsl(29,14-55): error X3013: Possible intrinsic functions are:
color.hlsl(29,14-55): error X3013:   mul(float|half…

在編譯時獲取錯誤訊息比執行時更方便。 我們已經展示瞭如何將頂點和畫素著色器離線編譯為.cso檔案。 因此,我們不再需要在執行時執行它(即,我們不需要呼叫D3DCompileFromFile)。 但是,我們仍然需要將.cso檔案中已編譯的著色器物件位元組碼載入到我們的應用程式中。 這可以使用標準C ++檔案輸入機制完成,如下所示:

ComPtr<ID3DBlob> d3dUtil::LoadBinary(const std::wstring& filename)
{
  std::ifstream fin(filename, std::ios::binary);

  fin.seekg(0, std::ios_base::end);
  std::ifstream::pos_type size = (int)fin.tellg();
  fin.seekg(0, std::ios_base::beg);

  ComPtr<ID3DBlob> blob;
  ThrowIfFailed(D3DCreateBlob(size, blob.GetAddressOf()));

  fin.read((char*)blob->GetBufferPointer(), size);
  fin.close();

  return blob;
}
…
ComPtr<ID3DBlob> mvsByteCode = d3dUtil::LoadBinary(L"Shaders\\color_vs.cso");
ComPtr<ID3DBlob> mpsByteCode = d3dUtil::LoadBinary(L"Shaders\\color_ps.cso");

FXC的/ Fc可選引數生成彙編程式碼, 不時地檢視著色器的程式集對於檢查著色器指令計數以及檢視正在生成的程式碼型別非常有用 - 有時它可能與期望的不同。 例如,如果HLSL程式碼中有條件語句,那麼我們可能希望彙編程式碼中存在分支指令。 在可程式設計GPU的早期階段,著色器中的分支過去很昂貴,因此有時編譯器會通過評估兩個分支來扁平化條件語句,然後在兩者之間進行插值以選擇正確的答案。 也就是說,以下程式碼將給出相同的答案: 這裡寫圖片描述 因此,扁平化方法在沒有任何分支的情況下給出了相同的結果,但是如果不檢視彙編程式碼,我們就不知道是否發生了扁平化,或者是否生成了真正的分支指令。 關鍵是有時候你想要檢視程式集以檢視實際情況,以下是color.hlsl中為頂點著色器生成的裝配示例:

// Generated by Microsoft (R) HLSL Shader Compiler 6.4.9844.0
//
//
// Buffer Definitions: 
//
// cbuffer cbPerObject
// {
//
//  float4x4 gWorldViewProj;      // Offset:  0 Size:  64
//
// }
//
//
// Resource Bindings:
//
// Name                 Type Format     Dim Slot Elements
// ------------------------------ ---------- ------- ----------- ---- ---------
// cbPerObject            cbuffer   NA     NA  0     1
//
//
//
// Input signature:
//
// Name         Index  Mask Register SysValue Format  Used
// -------------------- ----- ------ -------- -------- ------- ------
// POSITION         0  xyz     0   NONE  float  xyz 
// COLOR          0  xyzw    1   NONE  float  xyzw
//
//
// Output signature:
//
// Name         Index  Mask Register SysValue Format  Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_POSITION       0  xyzw    0   POS  float  xyzw
// COLOR          0  xyzw    1   NONE  float  xyzw
//
vs_5_0
dcl_globalFlags refactoringAllowed | skipOptimization
dcl_constantbuffer cb0[4], immediateIndexed
dcl_input v0.xyz
dcl_input v1.xyzw
dcl_output_siv o0.xyzw, position
dcl_output o1.xyzw
dcl_temps 2
//
// Initial variable locations:
//  v0.x <- vin.PosL.x; v0.y <- vin.PosL.y; v0.z <- vin.PosL.z; 
//  v1.x <- vin.Color.x; v1.y <- vin.Color.y; v1.z <- vin.Color.z; v1.w <- vin.Color.w; 
//  o1.x <- <VS return value>.Color.x; 
//  o1.y <- <VS return value>.Color.y; 
//  o1.z <- <VS return value>.Color.z; 
//  o1.w <- <VS return value>.Color.w; 
//  o0.x <- <VS return value>.PosH.x; 
//  o0.y <- <VS return value>.PosH.y; 
//  o0.z <- <VS return value>.PosH.z; 
//  o0.w <- <VS return value>.PosH.w
//
#line 29 "color.hlsl"
mov r0.xyz, v0.xyzx
mov r0.w, l(1.000000)
dp4 r1.x, r0.xyzw, cb0[0].xyzw // r1.x <- vout.PosH.x
dp4 r1.y, r0.xyzw, cb0[1].xyzw // r1.y <- vout.PosH.y
dp4 r1.z, r0.xyzw, cb0[2].xyzw // r1.z <- vout.PosH.z
dp4 r1.w, r0.xyzw, cb0[3].xyzw // r1.w <- vout.PosH.w

#line 32
mov r0.xyzw, v1.xyzw // r0.x <- vout.Color.x; r0.y <- vout.Color.y;
           // r0.z <- vout.Color.z; r0.w <- vout.Color.w
mov o0.xyzw, r1.xyzw
mov o1.xyzw, r0.xyzw
ret 
// Approximately 10 instruction slots used

Visual Studio 2013具有一些用於編譯著色器程式的整合支援, 可以將.hlsl檔案新增到專案中,Visual Studio(VS)將識別它們並提供編譯選項。 這些選項為FXC引數提供UI, 將HLSL檔案新增到VS專案時,它將成為構建過程的一部分,並且著色器將使用FXC進行編譯。 這裡寫圖片描述 使用VS整合HLSL支援的一個缺點是它每個檔案只支援一個著色器程式,因此,我們不能將頂點和畫素著色器都儲存在一個檔案中。 此外,有時我們希望使用不同的前處理器指令編譯相同的著色器程式,以獲得著色器的不同變體。 同樣,使用整合的VS支援是不可能的,因為它是每個.hlsl輸入的一個.cso輸出。