RenderDoc圖形調試器詳細使用教程(基於DirectX11)

前言

由於最近Visual Studio的圖形調試器老是抽風,不得不尋找一個替代品了。

對於圖形程式開發者來說,學會使用RenderDoc圖形調試器可以幫助你全面了解渲染管線綁定的資源和運行狀態,從而確認問題所在。

RenderDoc官網

DirectX11 With Windows SDK完整目錄

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

運行程式

為了調試我們的程式,需要通過RenderDoc來執行程式。

選擇File – Launch Application後,在Program – Executable Path中選擇要打開的程式。

注意:在你自己編寫的項目需要將exe放到項目(.vcxproj)所在的位置,或者讓VS在生成程式的時候輸出到項目位置!

如果待調試的程式需要載入Assimp的動態庫,我們還需要添加環境變數:

然後就可以點擊Launch運行程式了。

截取一幀畫面

在進入程式後,按下Print Screen(PrtSc)鍵截取一幀有問題的畫面,然後就可以看到程式窗口說已經捕獲了一幀:

捕獲完成後退出程式即可,捕獲的一幀文件類型為*.rdc

你可以在一次調試截取多幀畫面,但基本上目前我們只需要截取一幀畫面就可以退出程式了。

事件瀏覽器(Event Broser)

下面是圖形調試器的主介面:

事件瀏覽器展示了DirectX中關於ID3D11DeviceContext的重要調用,呈現了這一幀繪製涉及到的ClearDrawDispatchPresentResolve等命令。選擇具體某個事件,可以在下面的API Inspector看到在這個事件之前大概15個DeviceContext的調用事件。

事件瀏覽器會將繪製到同一系列渲染目標和深度緩衝區的事件摺疊成一個Pass,我們可以展開觀察裡面的具體繪製過程。

在選中某次繪製後,我們可以觀察的有:

  • Texture Viewer:完成當前繪製後渲染目標的結果、深度緩衝區的結果、像素著色器調試
  • Pipeline State:觀察當前渲染管線有哪些階段是被激活的,以及不同的階段狀態是怎樣的
  • Mesh Viewer:觀察當前正在渲染的模型從頂點輸入是什麼情況,經過頂點著色輸出後又是什麼情況,並且能夠觀察正在渲染的模型
  • Resource Inspector:觀察當前繪製後有哪些資源,狀態如何

接下來會按教程的順序來講可能需要查看的內容

Pipeline State

在管線狀態中我們可以清楚地看到當前有哪些執行的階段,選擇IA(輸入裝配階段)可以看到輸入布局頂點緩衝區輸入圖元類型

如果找不到窗口可以去菜單欄Window找到Pipeline State。

Mesh Viewer

點擊上圖中的Mesh View內的立方體可以跳轉到模型線框觀察頁面,同時可以觀察輸入的頂點數據:

通過Controls可以切換攝像機模式為第一人稱,然後使用WSAD移動

如果螢幕上沒有渲染出想要的東西,首先應當檢查的是輸出的頂點SV_POSITION是否位於NDC空間內,具體為:

\[-1\leq\frac{x}{w}\leq 1\\
-1\leq\frac{y}{w}\leq 1\\
0\leq\frac{z}{w}\leq 1
\]

要調試某個頂點,只需要在VS Input中選擇一個頂點右鍵 – Debug this vertex即可進入著色器調試。但調試環節我們留到後面再講。

Texture Viewer

在Texture Viewer中我們可以觀察綁定到管線上的圖片(Input),以及渲染管線輸出到的渲染目標、深度緩衝區(Output)。在選擇某個Output圖後,我們右鍵選中一個像素,右下角的Pixel Context就會顯示具體的位置:

選擇History可以查看在此之前有哪些繪製事件影響到當前像素,選擇Debug則可以調試當前像素。

觀察深度/模板緩衝區

選中深度/模板緩衝區,一般情況下越遠的物體顯得越白,越近顯得越黑,且深度圖的顏色分布大多在白色上。

而如果使用了反向Z,越遠的物體顯得越黑,越近顯得越白,且分布大多在黑色上,這時候看深度圖就是純黑一片,根本不知道什麼情況:

由於此時深度值大部分在靠近0的位置上,我們需要縮小顯示範圍來提高較遠物體的亮度:

為了觀察模板測試的結果,我們先選中Stencil,如果模板的輸出值為1,可能需要將Range右邊的條拖到最左邊才看得到(白色區域模板值為1,黑色區域模板值為0):

在Overlay中,我們可以觀察當前繪製中影響到的像素區域、深度測試(綠Pass紅Fail)、模板測試、背面剔除等結果。下圖演示了模型的線框在圖中的位置:

Resource Inspector

在這裡可以觀察與當前繪製相關的所有資源:

選中某個資源後,可以看到和它相關的資源、資源在哪些事件中被用到、資源初始化相關的調用。

觀察常量緩衝區

在管道狀態的著色器階段中,我們可以看到綁定的常量緩衝區:

其中Slot的名稱來自著色器聲明cbuffer時的名稱,Buffer的名稱則需要在C++程式碼中設置,具體參考下一節。

選擇某一個常量緩衝區,點擊Go處的箭頭,我們就可以看到裡面的具體內容:

注意:在當前教程中我們會傳入經過DirectXMath轉置後的矩陣,但是在這裡觀察值的時候,依然是以行矩陣的方式顯示才是正常的!即平移分量位於第四行。

若常量緩衝區的值在從C++端傳入到這裡出現問題,你還需要去觀察常量緩衝區的打包是否出現了問題。

關於HLSL的打包規則,可以查看這裡:
深入理解HLSL常量緩衝區打包規則

為圖形調試器的對象添加自定義名稱

看前面的圖片,Buffer在沒有指定名稱的時候默認是以Buffer 142的形式顯示的。等對象一多,我們就難以判別管線所綁定的對象是否正確。因此在某些需要的情況下,我們可以在C++程式碼來為對象指定名稱。

d3dUtil.h中提供了兩個系列的函數,一個用於D3D設備創建出來的對象,一個用於DXGI對象。通過SetPrivateData方法,並使用WKPDID_D3DDebugObjectNameGUID使得我們可以為其設置圖形調試器下的名稱(string_view版本要求C++17,或者可以參照舊d3dUtil.h中的實現):

// ------------------------------
// D3D11SetDebugObjectName函數
// ------------------------------
// 為D3D設備創建出來的對象在圖形調試器中設置對象名
// [In]resource				D3D11設備創建出的對象
// [In]name					對象名
inline void D3D11SetDebugObjectName(_In_ ID3D11DeviceChild* resource, _In_ std::string_view name)
{
#if (defined(DEBUG) || defined(_DEBUG)) && (GRAPHICS_DEBUGGER_OBJECT_NAME)
	resource->SetPrivateData(WKPDID_D3DDebugObjectName, (UINT)name.length(), name.data());
#else
	UNREFERENCED_PARAMETER(resource);
	UNREFERENCED_PARAMETER(name);
#endif
}

// ------------------------------
// D3D11SetDebugObjectName函數
// ------------------------------
// 為D3D設備創建出來的對象在圖形調試器中清空對象名
// [In]resource				D3D11設備創建出的對象
inline void D3D11SetDebugObjectName(_In_ ID3D11DeviceChild* resource, _In_ std::nullptr_t)
{
#if (defined(DEBUG) || defined(_DEBUG)) && (GRAPHICS_DEBUGGER_OBJECT_NAME)
	resource->SetPrivateData(WKPDID_D3DDebugObjectName, 0, nullptr);
#else
	UNREFERENCED_PARAMETER(resource);
#endif
}

// ------------------------------
// DXGISetDebugObjectName函數
// ------------------------------
// 為DXGI對象在圖形調試器中設置對象名
// [In]object				DXGI對象
// [In]name					對象名
inline void DXGISetDebugObjectName(_In_ IDXGIObject* object, _In_ std::string_view name)
{
#if (defined(DEBUG) || defined(_DEBUG)) && (GRAPHICS_DEBUGGER_OBJECT_NAME)
	object->SetPrivateData(WKPDID_D3DDebugObjectName, (UINT)name.length(), name.c_str());
#else
	UNREFERENCED_PARAMETER(object);
	UNREFERENCED_PARAMETER(name);
#endif
}

// ------------------------------
// DXGISetDebugObjectName函數
// ------------------------------
// 為DXGI對象在圖形調試器中清空對象名
// [In]object				DXGI對象
inline void DXGISetDebugObjectName(_In_ IDXGIObject* object, _In_ std::nullptr_t)
{
#if (defined(DEBUG) || defined(_DEBUG)) && (GRAPHICS_DEBUGGER_OBJECT_NAME)
	object->SetPrivateData(WKPDID_D3DDebugObjectName, 0, nullptr);
#else
	UNREFERENCED_PARAMETER(object);
#endif
}

在已經設置過名字的情況下,想要更名需要先調用nullptr_t重載版本,再調用正常版本。

設置好後,在圖形調試的時候一看名字就能知道綁定的情況了。

如果你不希望使用調試器對象具名化,可以在d3dUtil.h的開頭找到這樣的宏:

// 默認開啟圖形調試器具名化
// 如果不需要該項功能,可通過全局文本替換將其值設置為0
#ifndef GRAPHICS_DEBUGGER_OBJECT_NAME
#define GRAPHICS_DEBUGGER_OBJECT_NAME (1)
#endif

將其修改後只會剩下默認的DDSTextureLoaderWICTextureLoader的對象具名化。

注意:在你的Release版本應用程式應該避免出現對調試對象名稱的設置。你可以將相關程式碼移出項目。

查看著色器資源視圖中的紋理資源

以下影像素著色器階段的為例:

我們可以很清楚地看到資源的綁定情況,紅色表示當前Slot沒有資源綁定上去,如果對沒有綁定紋理的對象進行取樣,會在程式調試運行時的調試輸出窗口看到DX Error。當然本示例紅的也並不影響,因為會在著色器檢查Dimension是否為0從而避開取樣。

綠色的資源姑且認為是一個有UNKNOWN含義的DXGI格式,在通過SRV具體化。點擊Go的箭頭我們可以觀察傳入的著色器資源。

查看管線狀態、取樣器

基本上光柵化狀態、深度/模板狀態和混合狀態都是所見即所得

取樣器則在像素著色器階段選中取樣器可以查看

雖然這些狀態你也可以在C++看

著色器調試

接下來就開始進入到重點部分了,使用圖形調試器的核心目的還是要觀察著色器運行的時候遇到了哪些問題。當然有時候甚至會遇到該有的著色器卻被跳過不執行的情況,這時候就先要去前面排查該綁定的資源、狀態、著色器、輸入是否都OK了,然後才是對上一個正常運行的著色器進行調試。

對於頂點著色器,在Mesh Viewer中選擇要調試的頂點右鍵 – Debug this vertex即可

對於像素著色器,在Texture Viewer中的Output選擇RT後,右鍵選取某一像素,在Pixel Context處點Debug即可

而調試計算著色器,需要在Pipeline State選擇CS,按下圖選擇Debug,然後填寫要調試的執行緒組編號和組內執行緒編號(或者全局執行緒ID):

然後就進入到了著色器調試介面:

因為滑鼠操作麻煩,我們需要記住幾個快捷鍵:F10單步跳過,F11單步進入,ctrl+F11單步跳出

左側Constants & Resources可以查看頂點輸入、使用的常量、資源等,右側Watch可以添加變數觀察

滑鼠懸停在程式碼的變數可以觀察變數值

右鍵程式碼Go to disassembly可以轉彙編查看

左側file list可以查看用到的hlsl文件,以及編譯shader時候的預定義宏

此時首先你需要優先關注局部變數中各個會被用到的常量、輸入值是否都是正常的,如果出現常量緩衝區中的值全0或者亂值的情況,說明常量緩衝區可能沒有被更新。

修改著色器再運行

這是VS的圖形調試器所沒有的功能,在修改了某次繪製用到的著色器程式碼並編譯後,就可以影響到當前及之後的所有繪製。

下面是一個例子,這裡嘗試修改某個繪製的像素著色器程式碼:

然後嘗試修改下面g_VisualizePerSampleShaingtrue,使得當前繪製的像素顏色強製為紅色:

完成後選擇Apply changes,返回Texture Viewer觀察渲染目標的輸出變化:

可以看到,那些執行PS的像素都被染成了紅色,觀看後續的幀也可以發現的確產生了影響:

如果要退回變化,則回到像素著色器的Edit處,選擇Remove changes即可。

以編程方式捕獲圖形資訊

因為目前暫時還沒有使用的需要,具體資訊查看下面文檔:

//renderdoc.org/docs/in_application_api.html

如果某些DrawCall、Dispatch不是每幀都會產生的話,編程捕獲的方式還是有必要的。

總結

調試技巧需要經常使用才能夠熟練掌握,相比普通調試來說,圖形調試會更加複雜。目前RenderDoc的調試體驗比VS的圖形調試器會好一些,並且最近VS的圖形調試器有些問題,調試不了shader。在初學DX的階段容易在資源管理上出問題,因此重點是要先確認在繪製之前,綁定到渲染管線的各種資源是否正常,然後才是對著色器程式碼進行調試。所以前期準備工作的出錯一般占很大的一部分,而著色器程式碼引發的錯誤可能只是占較小的一部分。等到了渲染管線的資源綁定管理體系逐漸穩定以後,使用圖形調試的重心才會逐漸轉移到以著色器程式碼的調試為主。有時候圖形調試器解決不了的問題,還需要仔細觀察普通調試下的輸出窗口是否有渲染管線繪製事件執行時輸出的報錯資訊。

當然裡面還有很多強大的功能沒有挖掘出來,或者現在還不是比較常用而沒列出來。有興趣的讀者可以查看renderdoc的文檔:

Introduction — RenderDoc documentation

這篇部落格在後續還會有所變動,因為後續個人的學習會引發新的調試需求而變動。

DirectX11 With Windows SDK完整目錄

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