Visual Studio图形调试器详细使用教程(基于DirectX11)

Posted x-jun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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项目源码

以上是关于Visual Studio图形调试器详细使用教程(基于DirectX11)的主要内容,如果未能解决你的问题,请参考以下文章

Visual Studio 2017 安装使用教程(详细版)

解决windows配置visual studio code调试golang环境问题

在Visual Studio中调试时,如何检查有关进程令牌的详细信息?

Visual Studio2015使用tinyfox2.x作为Owin Host调试教程

vs2019安装教程 Visual Studio2019安装详细步骤

JAVA优化师入门教程——如何使用visual studio 对mysql进行源码级调试和优化