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

前言

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

下面的教程基於Visual Studio 2017 Community進行.

同時推薦大家瞭解一下我的DirectX 11教程,講述瞭如何脫離DirectX SDK及Effects11,使用HLSL編譯器/D3DCompiler和Windows SDK來開發DirectX 11應用程序:

DirectX11 With Windows SDK完整目錄

Github項目源碼

準備工作

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

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

然後觀察着色器的編譯選項,如果使用的是HLSL編譯器,則要重點關注Debug模式下所有着色器是否都禁用了優化,並啓用了調試信息。

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

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

如果使用的是D3DCompiler,在代碼層(運行時)編譯着色器,則需要在Debug模式下給D3DComplieFromFile函數添加D3DCOMPILE_DEBUGD3DCOMPILE_SKIP_OPTIMIZATION的Flag以開啓着色器調試並關閉優化:

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

    // 尋找是否有已經編譯好的頂點着色器
    if (objFileNameInOut && filesystem::exists(objFileNameInOut))
    {
        HR(D3DReadFileToBlob(objFileNameInOut, ppBlobOut));
    }
    else
    {
        DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
#ifdef _DEBUG
        // 設置 D3DCOMPILE_DEBUG 標誌用於獲取着色器調試信息。該標誌可以提升調試體驗,
        // 但仍然允許着色器進行優化操作
        dwShaderFlags |= D3DCOMPILE_DEBUG;

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

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

    return hr;
}

截取一幀畫面

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

運行圖形調試之前請先確保沒有能夠導致觸發斷點異常的問題,如果有的話請先通過普通的調試器解決問題。畢竟圖形調試器是要解決圖形顯示異常,普通調試無法查出來的問題,而要對GPU進行調試。除此之外,還需要撤掉之前在圖形繪製階段的所有斷點。

有兩種方式打開圖形調試器,第一種是快捷鍵Alt+F5啓動,如果沒有反應,則可以通過第二種方式啓動並確認快捷鍵。

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

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

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

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

圖形調試器預覽

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

事件列表

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

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

查看傳入的緩衝區數據

我們可以在圖形調試器查看頂點緩衝區,索引緩衝區和常量緩衝區。

在上面的事件列表中,我們可以看到很多藍色字體的對象:XX,這些都可以點進去觀察。這裏我們以某個繪製事件綁定的頂點緩衝區爲例

我們可以觀察到緩衝區的字節數、使用情況、綁定標籤、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 CBChangesEveryDrawing : register(b0)
{
    row_major matrix gWorld;
    row_major matrix gWorldInvTranspose;
    row_major matrix gTexTransform;
    Material gMaterial;
}

我們使用float4格式就可以觀察信息。其中每個矩陣佔了4行,Material也佔用了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右邊的時間標誌,就可以在右邊看到資源的讀取和寫入情況:

然後點擊查看就可以看到該資源當時的具體情況了。

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

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

管道階段

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

對於3D模型,你可以點擊輸入裝配器進入預覽網格界面來觀察加載出來的網格。至於對模型的操作,這裏暫且省略。要對場景進行操作,必須要選擇上行的其中一個工具才能對場景操作。而若要對物體進行操作,則必須要選擇左邊列的其中一個工具來對其操作。

而對於可編程的頂點着色器階段來說,我們可以看到視圖:輸入/輸出欄有 輸入/輸出的每個頂點的值和對應語義。其中SV_POSITION的值需要將(x, y, z, w)處理成(x/w, y/w, z/w, 1)來觀察它是否位於NDC座標系(齊次裁剪座標系)內,若不在則該頂點不會傳遞給下一階段。並且每個頂點都可以單獨進行着色器調試。

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

切換到像素着色器有可能是看不到任何的輸入和輸出的,但可以通過另一種方式,通過指定像素來觀察該像素經歷的像素着色器階段。這裏先不說。

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

查看深度緩衝區資源

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

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

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

另一種方式就是更改查看方式。如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常量緩衝區打包規則

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

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

着色器反彙編

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

對着色器代碼右鍵,選擇 轉到反彙編,就可以看到反彙編指令,然後一路往上滾,滾到開頭就可以看到上述所說的內容:

總結

調試技巧需要通過經常的使用才能夠熟練,相比普通調試來說,圖形調試會更加複雜,因爲它需要先確認在繪製之前,綁定到渲染管線的各種資源是否正常,然後纔是對着色器代碼進行調試,所以前期準備工作的出錯一般佔很大的一部分,而着色器代碼引發的錯誤可能只是佔較小的一部分。有時候圖形調試器解決不了的問題,還需要仔細觀察普通調試下的輸出窗口是否有渲染管線繪製事件執行時輸出的報錯信息。

當然裏面還有很多強大的功能沒有挖掘出來,或者現在還不是比較常用而沒列出來。有興趣的讀者可以查看微軟的官方中文文檔瞭解一下:

Visual Studio 圖形診斷概述

這篇博客在後續還會有所變動,因爲後續個人的學習會引發新的調試需求而變動。

DirectX11 With Windows SDK完整目錄

Github項目源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章