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

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

前言

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

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

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

準備工作

首先確定是否安裝了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++端傳入到這裡出現問題,你還需要去觀察常量緩衝區的打包是否出現了問題。

若出現區域性變數有未使用的說明,有可能在這個偵錯程式的確根本不會用到這個值,又或者你忘記將該常量緩衝區繫結到該著色器階段了。

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

著色器反彙編

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

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

總結

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

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

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