RenderDoc圖形偵錯程式詳細使用教程(基於DirectX11)

X_Jun發表於2022-03-30

前言

由於最近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即可。

以程式設計方式捕獲圖形資訊

因為目前暫時還沒有使用的需要,具體資訊檢視下面文件:

https://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,以及有什麼問題也可以在這裡彙報。

相關文章