NVIDIA OptiX 光线追踪引擎作为一个应用框架,旨在最大化 GPU 上的光线追踪性能。然而,使用 OptiX 开发的应用有时会遭遇一些棘手问题:API 参数传递错误、画面渲染异常黑屏,或者 GPU 端潜藏的缺陷隐藏在成千上万的并发线程中,让人难以定位根源。
值得庆幸的是,NVIDIA OptiX Toolkit(OTK)中的调试工具能够有效应对这些挑战。OTK 是一个托管在 GitHub 上的仓库,其中包含一系列实用工具,专门针对 GPU 光线追踪应用的常见工作流进行了优化。此外,它采用 BSD 3-Clause 许可证,允许你自由复制、修改其中的代码,灵活性极高。
本文将重点介绍两个 OTK 调试利器:一是针对 OptiX 与 CUDA API 错误码的一致性校验机制,二是面向设备端的定向调试打印功能。OTK 还附带了一个名为 DemandPbrtScene 的示例程序,该程序实际演示了设备端调试打印的具体应用方法。
OptiX 日志与验证机制如何运作?
在深入了解 OTK 的具体帮助之前,先掌握 OptiX 自身的日志机制会更有助于理解。创建 OptiX 设备上下文时,你需要提供一个选项结构,其中可以配置日志回调函数与验证模式。如果将验证模式设置为 OPTIX_DEVICE_CONTEXT_VALIDATION_MODE_ALL,OptiX 便会自动检查 API 函数输入参数的合法性。
OptiX 会将验证错误的提示信息写入日志,这些信息以人类可读形式呈现。因此,当遇到 OPTIX_ERROR_INVALID_VALUE 这类 API 错误时,应首先检查日志输出内容。当然,开启验证模式会带来一定的额外 API 开销。建议在调试与测试版本中启用全部验证功能,而在发布版本中将其关闭。更多详细信息可参考 OptiX SDK 的示例代码。
如何一致性地检查返回码错误
最佳做法是在错误发生的早期阶段就将其发现——在后续失败掩盖原始问题之前,及时把错误排查出来。下面将详细说明。
API 机制
OptiX 中大部分函数会返回一个 OptixResult 错误码,非零值即表示出错。CUDA 运行时 API 与 CUDA 驱动 API 也采用类似机制。这三个 API 都支持以下特性:
- 一个专用的枚举类型来表示错误码,例如
OptixResult - 一个函数,能够将错误码转换为符号名称字符串,例如
OPTIX_ERROR_INVALID_VALUE - 一个函数,能够返回错误码对应的人类可读错误信息,例如
Invalid value
这三个 API 的函数签名略有差异,但底层机制是一致的。
错误检查策略
在每个 API 调用点手动检查错误码既繁琐又容易遗漏。更优的方案是借助宏或函数调用,强制实施统一的错误处理策略。OTK 提供了两个宏,分别对应两种策略:
OTK_ERROR_CHECK( expr ):抛出异常OTK_ERROR_CHECK_NOTHROW( expr ):将消息打印到std::cerr,然后继续执行
如果你希望实现其他策略,只需复用现有基础设施,再编写一个新宏即可,过程非常简单。
宏机制的最小化使用
该错误检查机制仅使用了少量宏,实际工作都委托给了内联函数。你可以在内联函数定义处设置断点,这样调试器检测到错误时便会自动暂停。
宏存在的意义在于提供出错代码的上下文信息:
expr:宏参数的字符串形式,即计算出错误码的那个表达式__FILE__:宏被调用的源文件名__LINE__:宏被调用时在源文件中的行号
这些从宏调用点获取的信息,会被传递给真正执行错误检查的内联函数。
跨 API 的统一错误检查
内联模板函数 checkError 负责检查状态码,一旦检测到失败,便会生成一条诊断消息。对于这三个 API 而言,将错误码直接强制转换为 bool 即可判断是否出错——它们均以 0 表示成功,非零表示失败。
错误消息的格式如下:
file(line): expr failed with error nnn (name): message
其中 expr 是被求值的表达式,nnn 是状态码强制转换为 int 的结果,name 是状态码的符号名称,message 是人类可读的错误信息。如果名称或消息为空,函数会省略对应的部分。
内联模板函数 makeErrorString 负责构建这条消息,它内部会调用 getErrorName 和 getErrorMessage 来拼接出完整的消息内容。
每个 API 都有自己的状态码类型,因此你可以针对不同的 API 特化模板函数,以调用正确的 API 来获取扩展错误信息。
使用方式
OTK 为每个 API 提供了一个头文件,其中包含了上述模板函数所需的特化版本(表 1)。
| 头文件 | API |
| CUDA 驱动 API |
| CUDA 运行时 API |
| OptiX API |
使用时,只需包含你所用 API 对应的头文件,然后在所有调用点统一使用 OTK_ERROR_CHECK 宏即可。下面这个示例同时使用了三个 API:
OTK_ERROR_CHECK( cudaSetDevice( m_deviceIndex ) ); OTK_ERROR_CHECK( cuCtxGetCurrent( &m_cudaContext ) ); OTK_ERROR_CHECK( cuStreamCreate( &m_stream, CU_STREAM_DEFAULT ) ); OTK_ERROR_CHECK( optixInit() );
如何执行定向的设备端调试打印
图形应用的问题在于——导致黑屏的原因实在太多,令人防不胜防。
要调试 OptiX 设备代码中的问题,有几种常见思路可供参考:
- 对设备代码进行调试构建,配合 CUDA 调试器使用
- 在发布构建的设备代码中利用
printf获取信息
许多应用在调试模式下编译后运行速度会大幅下降,导致交互式调试器几乎无法正常使用。
而 printf 式调试的主要麻烦在于:GPU 上同时运行的线程数量过多,输出信息会像开闸放水般大量涌出,让人难以逐一查看。此外,问题可能只有在应用与用户交互一段时间后才显现——在问题尚未暴露之前的调试输出,全是干扰信息,只会妨碍你找到真正关键的内容。
DebugLocation 详解
头文件 提供了一个可复用的调试输出机制。结构体 DebugLocation 控制着整体行为:
struct DebugLocation
{
bool enabled;
bool dumpSuppressed;
bool debugIndexSet;
uint3 debugIndex;
};
enabled 成员用于开启或关闭整个机制。dumpSuppressed 成员用于在机制启用时临时禁用调试输出。debugIndexSet 表示一个有效的发射索引已经存储到了 debugIndex 中。
当以下条件全部满足时,该机制才会输出调试信息:
enabled为真dumpSuppressed为假debugIndexSet为真,且- 当前发射索引与
debugIndex匹配
在 OptiX 管线的发射参数中包含一个 DebugLocation 结构体实例,即可在运行时以交互方式控制调试输出。
debugInfoDump 函数详解
模板函数 debugInfoDump 提供了发射调试信息的接口:
templatestatic __forceinline__ __device__ bool debugInfoDump( const DebugLocation& debug, const Callback &callback )
Callback 模板参数应为一个结构体或类,满足以下接口:
struct Callback
{
void setColor( float red, float green, float blue );
void dump( const uint3& index );
};
setColor 方法用于在调试位置周围绘制一个可视化的方框,方便在屏幕上快速定位信息被输出的点。典型用法是,将当前发射索引对应的输出像素设为指定颜色。如果不需要可视化指示,让这个方法保持为空即可。
dump 方法则用于打印应用认为相关的任何信息,它会收到当前发射索引作为参数。
显示调试位置
启用调试位置显示时,回调结构体的 setColor 方法会在屏幕上绘制一个方框来指示当前调试位置,即使 dumpSuppressed 为真也会绘制。当禁用时,方框会被隐藏。
具体的视觉方案是:调试位置的那个像素本身显示为红色,外面包裹一圈 1 像素宽的黑色边框,再外面包裹一圈 1 像素宽的白色边框。这样就能提供一个高对比度的指示器,告诉你 dump 消息是从哪个位置产生的。如果输出缓冲区不是传统的颜色缓冲区,你可以自由地将红、绿、蓝值映射成某种可视化时能够区分的数值。
单次模式
为了避免被调试输出淹没,一种非常实用的做法是采用“单次模式”——仅在用户控制下输出一次。你可以按照以下步骤来安排:
- 启用
DebugLocation机制 - 正常发射
- 当用户交互式地选定了调试位置后,将
dumpSuppressed设为true,debugIndexSet设为true,并把debugIndex设为所选位置 - 后续的发射会显示调试位置,但机制不会输出 dump
- 用户与应用交互,将应用操纵到合适的状态(过程中可以移动调试位置)
- 当用户想获取当前位置的调试信息时,将
dumpSuppressed设为false - 发射,获得调试输出
- 发射后将
dumpSuppressed重新设为true
DemandPbrtScene 示例
OTK 中的 DemandPbrtScene 示例演示了 pbrt 版本 3 场景的按需加载几何体。它使用了 DebugLocation 机制,包括单次行为、交互式切换调试输出和交互式选择调试位置。UI 框架采用的是 ImGui。
要运行 OTK 中的这个示例,你需要准备一个 pbrt-v3 的场景文件。
开始使用 NVIDIA OptiX Toolkit 进行调试
OptiX Toolkit 为常见的 OptiX 开发问题提供了可复用的调试与测试工具:一致性 API 错误检查,以及定向的设备端调试输出。代码托管在 NVIDIA/optix-toolkit GitHub 仓库中。
准备好了吗?从 GitHub 下载 OptiX Toolkit,然后开始操作:在调试构建中启用 OptiX 验证,用 OTK_ERROR_CHECK 包裹你的 CUDA 和 OptiX 调用,再使用 DebugLocation 在开发早期就隔离 GPU 端的问题。OTK 采用宽松的 BSD 3-Clause 许可证,因此你可以直接复制、改编并将这些工具集成到自己的 OptiX 应用中。
