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,以及有什么问题也能够在这里汇报。

相关文章
相关标签/搜索