想象你花了半天时间编写了图形程序,一切都是那样的完美,直到…你按下 F5 打算调试一下,于是噩梦就降临了。黑漆漆一块大屏乎在脸上,啥都木有。

你开始怀疑某个 shader 出了错,可人家正经过了编译。或者是某处 API 没用对?验证层又告诉你没问题。那总不能是 Feature 支持没跟上吧?于是你又写了一堆 vkGetPhysicalDeviceFeatures(),最后发现自己还是小瞧这张破卡了。

以上都是图形开发的日常惹…先不要着急拍烂大腿。就好像 C/C++ 可以随时在调试时打个断点,看看堆栈里究竟填了什么,图形开发自然也会有类似的调试操作。

使用 Nvidia NSight 截帧

截帧指的是捕获 GPU 渲染的整个指令序列和资源状态,用于离线分析。截帧工具会在程序运行时抓取某一帧的所有 GPU 状态和调用,然后你可以逐步查看每个渲染通道、每个 Shader 的结果。这里使用 NSight 以作示范,相较于 Intel GPA,RDC 等,NSight 是我所使用过最为直观的截帧工具:绑定图形程序所在的进程之后,按下 F11 便可以直接开始分析。

虽然 NSight 有直接依附进 IDE,比如 VisualStudio 的能力,不过还是建议直接绑定目标图形程序(项目 Debug 目录下的 .exe 文件),在欢迎页选择 Start Activity,选择图形程序的位置后就可以启动调试器。

案例1:错误调试

正如在引文部分的,大多数不能被编译器和验证层捕获到的错误,几乎都是对图形 API 调用的逻辑上的一时疏忽造成的。比如资源没有传递到对应 shader,又或者是忘记清空上一帧的深度缓冲,这类逻辑错误实在是难以排查,需要截帧分析具体是哪趟 PASS  在掉链子。

比如接下来这个图形程序不显示任何内容,我们按下 F11 抓取一帧,分析究竟是谁在捣蛋。

所有 API 调用都被按照执行次序依次排列在事件栏,我们具体关注 Warning 和 Error。可以看到在第13次调用时,NSight 提示本次 Drawcall 什么都没画出来,所有片元都提前死在了深度测试上。。。于是便破案了:深度缓冲没清屏,所有的像素值都是1,代表近裁切面,于是后续所有渲染尝试都挂在这里了。(大腿拍烂ing)

案例2:性能分析

所有图形程序员都曾脱口而出地感慨:“卡爆了。。。”

但更具体的问题是,帧生成时间的预算究竟花去哪里了?哪几趟 PASS 最耗时间?分析管线性能对后续选择优化方向十分重要,此处我以 Vulkan 图形程序做示范。

可以看到帧时间来到了24ms,其中的一趟 PASS 竟然占据了79%的时间预算。此 Pass 直接指向65号 API 调用,实为一次 Drawcall。

我们首先看到此次 vkCmdDraw 调用的参数中,顶点数量仅有66个。大致是一个窗口矩形的顶点数量,很有可能是延迟渲染中的屏幕绘制阶段。通过查看 Fragment Shader 中的资源绑定可以发现大量的模型纹理,立即推断出此为延迟渲染的光照 Pass。

此时 Tag 提醒本次调用包含了 RayQuery,且相关管线为经典的 Vertex-Fragment 结构,并未包含任何光线追踪着色器。于是得出结论,性能损失大概率由片元着色器中的光线查询函数引发,之后的排查应集中在着色器端进行。