1. 程式人生 > >Visual Studio圖形調試器詳細使用教程(基於DirectX11)

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

fail jpg 訪問權限 mic 9.png 很多 斷點 str result

前言

對於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_DEBUG

D3DCOMPILE_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項目源碼

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