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

前言

對於DirectX程序開發者來講,學會使用Visual Studio Graphics Debugger(圖形調試器)能夠幫助你全面瞭解渲染管線綁定的資源和運行狀態,從而確認問題所在。如今就以我所掌握的圖形調試經驗來進行展開描述。html

下面的教程基於Visual Studio 2017/2019 Community進行.由於最近換了VS2019,而且添加了調試對象具名化的功能,裏面的圖片來不及作完整更換,但仍是能看的。編程

這一篇須要消耗比較多的流量,沒鏈接WIFI或者網線的慎入。windows

同時推薦你們瞭解一下個人DirectX 11教程,講述瞭如何脫離DirectX SDK及Effects11,使用HLSL編譯器/D3DCompiler和Windows SDK來開發DirectX 11應用程序:數組

DirectX11 With Windows SDK完整目錄函數

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

準備工做

首先肯定是否安裝了DirectX圖形調試器,須要在Visual Studio Installer中肯定是否已經勾選了該項內容。學習

安裝好並進入項目,在調試以前須要將項目配置成Debug模式測試

而後觀察着色器的編譯選項,若是使用的是HLSL編譯器,則要重點關注Debug模式下全部着色器是否都禁用了優化,並啓用了調試信息。字體

首先對其中的一個着色器右鍵-屬性
優化

而後在Debug配置下,選擇HLSL編譯器-全部選項,禁用優化並啓用調試信息

若是使用的是D3DCompiler,在代碼層(運行時)編譯着色器,則須要在Debug模式下給D3DComplieFromFile函數添加D3DCOMPILE_DEBUGD3DCOMPILE_SKIP_OPTIMIZATION的Flag以開啓着色器調試並關閉優化,不然在調試着色器的時候只能看到彙編代碼

HRESULT CreateShaderFromFile(const WCHAR * csoFileNameInOut, const WCHAR * hlslFileName,
    LPCSTR entryPoint, LPCSTR shaderModel, ID3DBlob ** ppBlobOut)
{
    HRESULT hr = S_OK;

    // 尋找是否有已經編譯好的頂點着色器
    if (csoFileNameInOut && D3DReadFileToBlob(csoFileNameInOut, ppBlobOut) == S_OK)
    {
        return hr;
    }
    else
    {
        DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
#ifdef _DEBUG
        // 設置 D3DCOMPILE_DEBUG 標誌用於獲取着色器調試信息。該標誌能夠提高調試體驗,
        // 但仍然容許着色器進行優化操做
        dwShaderFlags |= D3DCOMPILE_DEBUG;

        // 在Debug環境下禁用優化以免出現一些不合理的狀況
        dwShaderFlags |= D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
        ID3DBlob* errorBlob = nullptr;
        hr = D3DCompileFromFile(hlslFileName, nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entryPoint, shaderModel,
            dwShaderFlags, 0, ppBlobOut, &errorBlob);
        if (FAILED(hr))
        {
            if (errorBlob != nullptr)
            {
                OutputDebugStringA(reinterpret_cast<const char*>(errorBlob->GetBufferPointer()));
            }
            SAFE_RELEASE(errorBlob);
            return hr;
        }

        // 若指定了輸出文件名,則將着色器二進制信息輸出
        if (csoFileNameInOut)
        {
            return D3DWriteBlobToFile(*ppBlobOut, csoFileNameInOut, FALSE);
        }
    }

    return hr;
}

截取一幀畫面

圖形調試器的調試一般是針對某一幀的畫面進行的。完成了上面的配置後,第一步咱們須要打開圖形調試器去截取一幀認爲有問題的畫面來進行調試。

運行圖形調試以前請先確保沒有可以致使觸發斷點異常的問題,若是有的話請先經過普通的調試器解決問題。畢竟圖形調試器是要解決圖形顯示異常,普通調試沒法查出來的問題,而要對GPU進行調試。除此以外,還須要撤掉以前在圖形繪製階段的全部斷點。

有兩種方式打開圖形調試器,第一種是快捷鍵Alt+F5啓動,若是沒有反應,則能夠經過第二種方式啓動並確認快捷鍵。

第二種是VS界面選擇調試-圖形-啓動圖形調試。

在進入程序後,按下Print Screen(PrtSc)鍵截取一幀有問題的畫面,而後就能夠看到紅色方框區域就是你剛截下的一幀畫面

實際上生成的是一個圖形日誌文檔(.vsglog),咱們須要經過他來進行圖形調試。

你能夠在一次調試截取多幀畫面,但基本上目前咱們只須要截取一幀畫面就能夠退出程序了。關閉程序後,咱們能夠點擊藍色部分的字:幀XXXX 或者雙擊畫面來打開Visual Studio圖形分析器。

圖形調試器預覽

下面是圖形調試器的主界面

事件列表

事件列表展現了DirectX的一些接口類對象的重要調用。當前查看的是GPU工做,能夠觀察到D3D設備上下文關於繪製和內部綁定的GPU數據更新的全部操做。若更改成時間線,則能夠觀察更多有關D3D設備上下文的詳細調用操做,能夠看到各個階段都有哪些資源被綁定,哪些狀態被改變,以及調用了繪製。

其中帶筆刷的調用說明這是一個繪製調用,能夠點擊它觀察直到這個方法被調用後的繪製狀態。

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

看到上面的幾張圖片,雖然咱們能夠推測出來對象: 2就是m_pd3dImmediateContext,可是也僅限少數的幾個固定對象名咱們能直接推測出是什麼對象。等對象一多,咱們就難以判別管線所綁定的對象是否正確。所以咱們能夠在C++代碼來爲對象指定名稱。

d3dUtil.h中提供了兩個函數,一個用於D3D設備建立出來的對象,一個用於DXGI對象。經過SetPrivateData方法,並使用WKPDID_D3DDebugObjectNameGUID使得咱們能夠爲其設置圖形調試器下的名稱:

// ------------------------------
// D3D11SetDebugObjectName函數
// ------------------------------
// 爲D3D設備建立出來的對象在圖形調試器中設置對象名
// [In]ID3D11DeviceChild    D3D11設備建立出的對象
// [In]name                 對象名
template<UINT TNameLength>
inline void D3D11SetDebugObjectName(_In_ ID3D11DeviceChild* resource, _In_ const char(&name)[TNameLength])
{
#if (defined(DEBUG) || defined(_DEBUG)) && (GRAPHICS_DEBUGGER_OBJECT_NAME)
    resource->SetPrivateData(WKPDID_D3DDebugObjectName, TNameLength - 1, name);
#else
    UNREFERENCED_PARAMETER(resource);
    UNREFERENCED_PARAMETER(name);
#endif
}

// ------------------------------
// DXGISetDebugObjectName函數
// ------------------------------
// 爲DXGI對象在圖形調試器中設置對象名
// [In]IDXGIObject          DXGI對象
// [In]name                 對象名
template<UINT TNameLength>
inline void DXGISetDebugObjectName(_In_ IDXGIObject* resource, _In_ const char(&name)[TNameLength])
{
#if (defined(DEBUG) || defined(_DEBUG)) && (GRAPHICS_DEBUGGER_OBJECT_NAME)
    resource->SetPrivateData(WKPDID_D3DDebugObjectName, TNameLength - 1, name);
#else
    UNREFERENCED_PARAMETER(resource);
    UNREFERENCED_PARAMETER(name);
#endif
}

此外,GameObject類、Model類、TextureRender類、SkyRender類和DynamicSkyRender類都添加了SetDebugObjectName方法來爲對象設置調試自定義名稱。

如今打開圖形調試器查看,相似效果以下:

對象具名化後能夠十分方便地確認本身有沒有正確綁定所需資源。

若是你不但願使用調試器對象具名化,能夠在d3dUtil.h的開頭找到這樣的宏:

// 默認開啓圖形調試器具名化
// 若是不須要該項功能,可經過全局文本替換將其值設置爲0
#ifndef GRAPHICS_DEBUGGER_OBJECT_NAME
#define GRAPHICS_DEBUGGER_OBJECT_NAME (1)
#endif

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

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

查看傳入的緩衝區數據

咱們能夠在圖形調試器查看頂點緩衝區,索引緩衝區和常量緩衝區。

在上面的事件列表中,咱們能夠看到不少藍色字體的對象,這些均可以點進去觀察。這裏咱們以某個繪製事件綁定的頂點緩衝區爲例

咱們能夠觀察到緩衝區的字節數、使用狀況、綁定標籤、CPU訪問權限等。其中觀察到的數據取決於咱們設置的格式。

圖形調試器支持觀察的基本類型以下:

大類 基本類型
有符號字節類型 byte(sbyte) 2byte 4byte 8byte
無符號字節類型 ubyte u2byte u4byte u8byte
十六進制字節類型 xbyte x2byte x4byte x8byte
有符號整型 short int int64(long)
無符號整型 ushort uint uint64(ulong)
十六進制整型 xshort xint xint64(xlong)
半精度浮點型 half half2 half3 half4
單精度浮點型 float float2 float3 float4
雙精度浮點型 double

除此以外,格式欄容許咱們輸入以支持不一樣基本類型的組合。好比說如今傳入的頂點包含位置、法向量和紋理座標,那咱們能夠在格式欄輸入float3 float3 float2來將輸入的數據從新解釋成咱們傳入的頂點信息:

一樣,對於索引緩衝區,咱們能夠在格式欄輸入short short shortint int int來觀察三個索引組裝一個圖元的索引數組:

而對於常量緩衝區來講,一個着色器階段可能會綁定多個常量緩衝區,傳入的數據取決於你調用的ID3D11DeviceContext::*SSetConstantBuffers方法綁定的常量緩衝區以及最近一次ID3D11DeviceContext::UpdateSubresource方法更新的數據,而使用的緩衝區取決於你在着色器寫的代碼。好比有下面這個常量緩衝區塊:

// 物體表面材質
struct Material
{
    float4 Ambient;
    float4 Diffuse;
    float4 Specular; // w = SpecPower
    float4 Reflect;
};

cbuffer CBChangesOnResize : register(b3)
{
    matrix g_Proj;
}

咱們使用float4格式就能夠觀察信息。其中每一個矩陣佔了4行:

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

由於着色器資源視圖中能夠綁定一張紋理,也能夠綁定一個紋理數組。這裏我以另外一個程序的圖形調試做爲實例,演示如何觀察綁定到渲染管線上的紋理資源。

點擊PS着色器資源的藍字部分(Grass.dds),能夠查看着色器資源的狀態

如今咱們要查看着色器資源綁定的內容,點擊資源對應的藍字(DDSTextureLoader)就能夠查看綁定的紋理資源。

這裏咱們能夠觀察到加載的紋理格式。在通過DDSTextureLoaderWICTextureLoader加載的紋理會自動生成MipMap鏈,如今加載的是一張512x512的紋理,它有10張子資源,選擇Mip切片能夠查看其他子資源紋理。隨着Mip切片等級增大,寬度和高度逐漸是原來上一級的1/2.

而在通道直方圖中,默認觀察的是紋理RGB通道顏色的組合,你能夠取消勾選來關閉某一通道的顏色,或者修改範圍來選擇顏色的可視範圍。若選擇Alpha通道,則只會單獨觀察該通道的顏色。下面是原來用的籬笆盒Alpha通道的狀況(白色爲Alpha值1, 黑色爲Alpha值0):

接下來是紋理數組的觀察,其實和以前的操做差很少,但有時候咱們在繪製過程可能找不到以前綁定上的紋理,咱們能夠經過下面的對象表來尋找。對象表已經包含了由D3D設備建立出來的絕大多數資源或對象。

這裏用的是公告板的例子,好比我如今要尋找紋理資源,在搜索欄輸入Texture來根據類型進行查找:

紋理數組加載了4張紋理,它的字節大小也應該是最大的,雙擊它就能夠看到樹的紋理了:

咱們經過更改數組切片來觀察別的樹的紋理:

固然,若是給對象具名化,在這裏面找對象會更加容易一些:

查看資源歷史記錄

細心的話能夠發現有些資源是有個時間標誌的,點擊它能夠查看該資源的歷史變動狀況,即有哪些方法對該資源進行了變動。

好比說我點擊了PS着色器資源:Grass.dds右邊的時間標誌(VS2015不支持)

就能夠在右邊看到資源的讀取和寫入狀況:

而後點擊查看就能夠看到該資源當時的具體狀況了。

跟蹤渲染管線各個階段的狀態

選擇一個繪製事件,而後在下面的狀態欄就能夠看到跟上一繪製事件相比,有哪些階段發生了變化。變化的部分會有紅色高亮顯示。在該狀態能夠查看當前繪製已經綁定的全部資源、着色器和狀態,相比對象表查找起來會更清晰一些。

管道階段

一樣是要先選擇一個繪製事件,而後在下面的狀態欄選擇管道階段,就能夠看到當前運行的各個着色階段,以及是否存在從某個階段開始就沒有輸入/輸出或者沒有執行的問題。

對象查看

對於3D模型,你能夠點擊輸入裝配器進入預覽網格界面來觀察加載出來的網格。

要對場景進行操做,必需要選擇上行的其中一個工具才能對場景操做。

若要對物體進行操做,則必需要選擇左邊列的其中一個工具來對其操做。

此外,你能夠觀察物體的法向量或面向量

你也能夠經過上圖右邊的屬性欄修改物體的基本屬性。至於其他功能你能夠自行探索。

頂點位置

對於可編程的頂點着色器階段來講,咱們能夠看到視圖:輸入/輸出欄有 輸入/輸出的每一個頂點的值和對應語義。其中SV_POSITION的值是未通過透視除法的,咱們能夠將(x, y, z, w)的每一個份量除以w,變成(x/w, y/w, z/w, 1)來觀察它是否位於NDC座標系(齊次裁剪座標系)內,若不在則該頂點不會傳遞給下一階段。每一個頂點均可以單獨進行着色器調試。

注意:在像素着色器中,SV_POSITIONx份量和y份量都已經通過視口變換成爲最終的屏幕座標,且帶有小數點0.5,這是由於要取到像素的中心位置,即對於800x600的視口區域,實際上的屏幕座標取值範圍爲[0.5, 800.5]x[0.5, 600.5]z份量取值範圍爲[0, 1]。這一點讀者能夠修改像素着色器使得SV_POSITION與像素顏色結果有關聯,而後進入調試以驗證。

綁定的資源

將視圖:輸入/輸出切換成綁定的資源,一樣也能看到在該着色器階段綁定了哪些資源可供使用。

切換到像素着色器有多是看不到任何的輸入和輸出的,但能夠經過另外一種方式,指定像素來觀察該像素經歷的像素着色器階段。這裏在下面會講到。

最後是輸出合併器,切換到綁定的資源,能夠看到輸出合併階段綁定的深度/模板緩衝區和後備緩衝區的狀態。

查看深度/模板緩衝區資源

緊接着剛纔所講的內容,點擊左邊的深度/模板緩衝區,咱們就能夠看到一張以紅色爲背景,黑色表明深度值的紋理。黑色越深,深度值越小。

由於這張圖沒有模板值的變動,我再選擇一張帶有模板和深度值的輸出來演示。

實際上在這裏,包含有模板值的區域應當是綠色,可是連同深度緩衝區的紅色混在一塊兒就變成了黃色,咱們能夠關閉深度部分來觀察只包含模板值的綠色部分。

另外一種方式就是更改查看方式。如DXGI_FORMAT_D24_UNORM_S8_UINT同時包含了模板值和深度值,那DXGI_FORMAT_R24_UNORM_X8_TYPELESS就只包含了深度值,DXGI_FORMAT_X24_TYPELESS_G8_UINT則只包含了模板值。

查看該幀圖片下某一像素的繪製歷史

點擊加載的報告XX-XX.vsglog,而後選擇要觀察的某一個像素,就能夠看到該像素從開始到結束都經歷了哪些繪製步驟,在某一個繪製事件還能夠看到它屬於頂點/幾何着色器的哪個圖元內,以及像素着色器、輸出合併器的經歷。

着色器調試

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

回到管線階段或者在像素的繪製歷史,指定某一個着色器階段,選擇一個元素,點擊一個相似播放的按鈕就能夠開始進入着色器調試。

而後就會在着色器代碼實際可執行的第一行暫停停住。你能夠設置斷點,也能夠單步調試,像以前在VS調試那樣來調試。此時首先你須要優先關注局部變量中各個會被用到的常量、輸入值是否都是正常的,若是出現常量緩衝區中的值全0或者亂值的狀況,說明常量緩衝區可能沒有被更新。若常量緩衝區的值在從C++端傳入到這裏出現問題,你還須要去觀察常量緩衝區的打包是否出現了問題。

關於HLSL的打包規則,能夠查看這裏:
深刻理解HLSL常量緩衝區打包規則

若出現局部變量有未使用的說明,有可能在這個調試器的確根本不會用到這個值,又或者你忘記將該常量緩衝區綁定到該着色器階段了。

而局部變量出現在做用域內的說明,則多是該變量還沒被聲明出來或者沒被賦值,須要繼續執行才能看到。

着色器反彙編

通常來講咱們看着色器的反彙編不主要是爲了看彙編指令,而是它還附帶了一些額外的信息,如該着色器使用了哪些常量緩衝區結構體輸入/輸出簽名如何,這些常量緩衝區通過打包後各個元素所處的字節偏移量如何。

有的同窗在還沒開始進行GPU調試的時候點擊了管道階段的藍字,而後看到編譯器輸出那欄字,覺得反彙編沒有開啓。其實是你的打開方式不對。

進入着色器調試後,對着色器代碼右鍵,選擇 轉到反彙編,就能夠看到反彙編指令,又或者是點擊上方的反彙編窗口切換:

而後一路往上滾,滾到開頭就能夠看到上述所說的內容:

以編程方式捕獲圖形信息

在某些特殊狀況下,你能夠須要用到編程捕獲的方法:

  1. 圖形應用不使用交換鏈,即只是渲染到一張單獨的紋理
  2. 使用DirectCompute(計算着色器)來執行計算
  3. 經過流輸出階段將數據輸出到緩衝區
  4. 在手動測試的時候難以預測和捕獲問題幀,但能夠經過編程的方式預測出現問題幀的狀況

Windows 8.1及以上的編程捕獲

DirectX 11.2 API須要Windows 8.1及更高版本系統的支持。接下來你須要完成下面的任務:

  1. 準備好所需頭文件
  2. 獲取IDXGraphicsAnalysis接口
  3. 開啓圖形調試,捕獲圖形信息

注意:之前的編程捕獲的實現依賴於Visual Studio遠程工具提供的捕獲功能,但從Windows 8.1起能夠直接經過Direct3D 11.2來支持捕獲功能。所以,你不須要在Windows 8.1上安裝用於編程捕獲的遠程工具。

首先你須要包含下面的這些頭文件:

#include <DXGItype.h>  
#include <dxgi1_2.h>  
#include <dxgi1_3.h>  
#include <DXProgrammableCapture.h>

須要注意的是,這些頭文件沒法與頭文件vsgcapture.h兼容,由於它不能與DirectX 11.2兼容。若是在d3d11_2.h後面包含該頭文件,編譯器將會發出警告;而若是在d3d11_2.h前面包含該頭文件,應用程序將不會啓動。

此外,若是你的電腦安裝了DirectX SDK(June 2010),而且你的項目包含路徑包括了%(DXSDK_DIR)Include\,請將它移到包含路徑的最末端或者去掉。

而後你須要添加下面代碼以獲取DXGI調試接口IDXGraphicsAnalysis

IDXGraphicsAnalysis* pGraphicsAnalysis;  
HRESULT getAnalysis = DXGIGetDebugInterface1(0, __uuidof(pGraphicsAnalysis), reinterpret_cast<void**>(&pGraphicsAnalysis));
if (FAILED(getAnalysis))
{
    // 終止你的應用程序
}

若是你沒有以圖形調試形式啓動程序,DXGIGetDebugInterface1將返回E_NOINTERFACE

如今假定你已經獲取了一個能用的IDXGraphicsAnalysis接口,你可使用BeginCaptureEndCapture方法捕獲圖形信息:

pGraphicsAnalysis->BeginCapture();
// ...這部分管線命令都將被捕獲到
pGraphicsAnalysis->EndCapture();

如今,你應該能夠看到計算着色器的調試了:

早期版本的編程捕獲

若是你的系統不支持DirectX 11.2及以上版本的API,則可使用該舊版圖形捕獲方法。這種方法適用於任意DirectX 11.X版本API中使用。

首先你須要包含頭文件vsgcapture.h,而後建立VsgDbg對象。關於VsgDbg類,目前你只須要了解這些方法:

方法名 描述
構造函數 形參指定爲true時,將默認產生臨時的vsglog文件
BeginCapture 從該語句執行起捕獲全部的GPU事件
EndCapture 結束以BeginCature開始的捕獲事件
CaptureCurrentFrame 捕獲從當前語句到這一幀結束的全部GPU事件

一般狀況下咱們能夠構造函數的形參指定爲true,而後能夠開始捕獲圖形信息:

pVSGraphicsDebugger->BeginCapture();
// ...這部分管線命令都將被捕獲到
pVSGraphicsDebugger->EndCapture();

要想了解更多的信息,能夠查閱MSDN文檔(編程捕獲)

總結

調試技巧須要常用纔可以熟練掌握,相比普通調試來講,圖形調試會更加複雜。在初學DX的階段容易在資源管理上出問題,所以重點是要先確認在繪製以前,綁定到渲染管線的各類資源是否正常,而後纔是對着色器代碼進行調試。因此前期準備工做的出錯通常佔很大的一部分,而着色器代碼引起的錯誤可能只是佔較小的一部分。等到了渲染管線的資源綁定管理體系逐漸穩定之後,使用圖形調試的重心纔會逐漸轉移到以着色器代碼的調試爲主。有時候圖形調試器解決不了的問題,還須要仔細觀察普通調試下的輸出窗口是否有渲染管線繪製事件執行時輸出的報錯信息。

固然裏面還有不少強大的功能沒有挖掘出來,或者如今還不是比較經常使用而沒列出來。有興趣的讀者能夠查看微軟的官方中文文檔瞭解一下:

Visual Studio 圖形診斷概述

這篇博客在後續還會有所變更,由於後續我的的學習會引起新的調試需求而變更。

DirectX11 With Windows SDK完整目錄

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

相關文章
相關標籤/搜索