强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

OpenGL / OpenCL 编程指南 / 第 17 章:常见问题与调试

第 17 章:常见问题与调试

GPU 编程的一大痛点是"黑盒"——渲染结果不对时很难定位原因。本章汇总最常见的错误模式,并介绍强大的调试工具。


17.1 OpenGL 常见问题

17.1.1 黑屏(什么都不显示)

可能原因 排查方法
着色器编译失败 检查 glGetShaderiv(GL_COMPILE_STATUS)
链接失败 检查 glGetProgramiv(GL_LINK_STATUS)
MVP 矩阵错误 gl_Position 设为固定值测试
深度测试导致全部被丢弃 关闭深度测试看是否有内容
视口设置错误 确认 glViewport 参数正确
颜色全黑 将片段着色器输出设为红色测试
相机在物体内部 确认相机位置在物体外面
背面剔除错误 尝试关闭 glDisable(GL_CULL_FACE)
// 快速诊断:强制输出红色
// 片段着色器
void main() {
    FragColor = vec4(1.0, 0.0, 0.0, 1.0);  // 强制红色
}

17.1.2 纹理显示为黑色

可能原因 排查方法
未生成 Mipmap 调用 glGenerateMipmap
纹理坐标错误 使用 gl_FragCoord.xy / screenSize 作为临时 UV
stb_image 未翻转 设置 stbi_set_flip_vertically_on_load(true)
纹理单元未激活 确认 glActiveTexture + glUniform1i
图片加载失败 检查文件路径和 stbi_load 返回值
纹理未绑定 确认 glBindTexture 在正确位置

17.1.3 Z-fighting(深度冲突)

症状:两个重叠表面交替闪烁

原因:深度精度不足,两个面的深度值非常接近

解决方案:
1. 增大近裁剪面距离 (near = 0.1 → 1.0)
2. 使用多边形偏移
   glEnable(GL_POLYGON_OFFSET_FILL);
   glPolygonOffset(1.0f, 1.0f);
3. 使用更高精度的深度缓冲 (GL_DEPTH_COMPONENT32F)
4. 使用对数深度缓冲

17.1.4 性能问题排查清单

□ 是否每帧调用 glGen* / glDelete*?
□ 是否每帧查询 uniform 位置(未缓存)?
□ 是否有过多的绘制调用(>1000/帧)?
□ 纹理尺寸是否过大(>4K)?
□ 是否启用了不必要的状态(深度测试、模板测试、混合)?
□ 是否使用了过大的 Mipmap 偏移?
□ 着色器中是否有严重分支发散?
□ 是否使用了实例化渲染?

17.2 OpenCL 常见问题

17.2.1 内核编译失败

// 获取详细的编译错误信息
err = clBuildProgram(program, 1, &device, NULL, NULL, NULL);
if (err != CL_SUCCESS) {
    size_t log_size;
    clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size);
    char *log = malloc(log_size);
    clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, log, NULL);
    printf("Build Log:\n%s\n", log);
    free(log);
}

17.2.2 内核执行结果错误

可能原因 排查方法
索引越界 添加边界检查 if (gid < N)
内存未传输 确认 clEnqueueWriteBuffer 使用 CL_TRUE
全局大小不匹配 检查 glDispatchCompute 参数
浮点精度问题 对比 CPU 参考结果
竞态条件 检查 barrier 使用
参数设置错误 确认 clSetKernelArg 的索引和大小

17.2.3 性能瓶颈诊断

瓶颈类型 症状 优化方向
内存带宽 内核执行时间与数据量线性相关 合并访问、局部内存
计算瓶颈 增加数据量不影响时间 减少计算复杂度
启动开销 内核本身很快但总时间长 合并小内核、批量处理
传输瓶颈 大量时间花在数据传输 零拷贝、映射、异步传输

17.3 调试工具:OpenGL 调试回调

17.3.1 启用调试输出

// OpenGL 4.3+ 调试回调
void APIENTRY debugCallback(GLenum source, GLenum type, GLuint id,
                            GLenum severity, GLsizei length,
                            const GLchar* message, const void* userParam) {
    // 过滤通知级别
    if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) return;

    const char* severityStr;
    switch (severity) {
        case GL_DEBUG_SEVERITY_HIGH:         severityStr = "HIGH"; break;
        case GL_DEBUG_SEVERITY_MEDIUM:       severityStr = "MEDIUM"; break;
        case GL_DEBUG_SEVERITY_LOW:          severityStr = "LOW"; break;
        default:                             severityStr = "NOTIFICATION"; break;
    }

    const char* typeStr;
    switch (type) {
        case GL_DEBUG_TYPE_ERROR:               typeStr = "ERROR"; break;
        case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: typeStr = "DEPRECATED"; break;
        case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:  typeStr = "UNDEFINED"; break;
        case GL_DEBUG_TYPE_PORTABILITY:         typeStr = "PORTABILITY"; break;
        case GL_DEBUG_TYPE_PERFORMANCE:         typeStr = "PERFORMANCE"; break;
        default:                                typeStr = "OTHER"; break;
    }

    fprintf(stderr, "[GL %s/%s] ID:%u: %s\n", severityStr, typeStr, id, message);
}

// 初始化时启用
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);  // 同步回调(方便调试)
glDebugMessageCallback(debugCallback, nullptr);

17.4 RenderDoc

17.4.1 简介

RenderDoc 是最流行的开源 GPU 调试工具,支持 OpenGL、Vulkan、D3D、OpenCL。

17.4.2 安装

# Ubuntu
sudo apt install renderdoc

# 或从官网下载
# https://renderdoc.org/builds

17.4.3 使用流程

1. 启动 RenderDoc
2. File → Launch Application
   - 设置可执行文件路径
   - 设置工作目录
   - 配置命令行参数
3. 按 F12(或 Print Screen)捕获一帧
4. 在 RenderDoc 中分析捕获的帧

分析内容:
├── 事件浏览器:查看所有 GL 调用
├── 纹理查看器:查看每阶段的纹理/帧缓冲
├── 网格查看器:查看顶点数据
├── 着色器查看器:查看编译后的着色器
├── 管线状态:查看当前绑定的资源
└── 像素历史:追踪特定像素的渲染过程

17.4.4 关键功能

功能 用途
帧捕获 捕获单帧的所有 GPU 操作
纹理查看 查看任意纹理/帧缓冲的内容
像素历史 追踪某个像素经过了哪些绘制调用
着色器调试 单步执行着色器、查看变量值
网格覆盖 在 3D 视图中查看顶点数据
性能计数器 GPU 硬件性能计数器

17.5 APITrace

17.5.1 简介

APITrace 记录所有 OpenGL/Vulkan API 调用,可以回放和分析。

17.5.2 使用方法

# 安装
sudo apt install apitrace

# 追踪 OpenGL 调用
apitrace trace --api=gl --output=trace.trace ./my_gl_app

# 查看追踪结果
qapitrace trace.trace

# 命令行分析
apitrace dump trace.trace | head -100

# 回放追踪
glretrace trace.trace

# 性能统计
apitrace replay trace.trace --benchmark

17.5.3 APITrace 输出示例

glCreateShader(GL_VERTEX_SHADER) = 3
glShaderSource(3, 1, 0x7ffd4a2b3c00, NULL)
glCompileShader(3)
glGetShaderiv(3, GL_COMPILE_STATUS, 0x7ffd4a2b3bfc) = 1
glCreateShader(GL_FRAGMENT_SHADER) = 4
glShaderSource(4, 1, 0x7ffd4a2b3c08, NULL)
glCompileShader(4)
glGetShaderiv(4, GL_COMPILE_STATUS, 0x7ffd4a2b3bfc) = 1
glCreateProgram() = 5
glAttachShader(5, 3)
glAttachShader(5, 4)
glLinkProgram(5)
glGetProgramiv(5, GL_LINK_STATUS, 0x7ffd4a2b3bfc) = 1

17.6 NVIDIA NSight

17.6.1 NSight Graphics

功能:
- 帧调试(类似 RenderDoc)
- GPU 性能分析
- 着色器调试与优化
- 硬件性能计数器
- 光线追踪调试

安装:从 NVIDIA 开发者网站下载
支持:Windows + Linux,NVIDIA GPU

17.6.2 NSight Systems

# 系统级性能分析
nsys profile --trace=cuda,opengl,vulkan ./my_app

# 生成报告
nsys stats report1.nsys-rep

17.7 错误排查流程图

渲染结果异常
    │
    ├─ 完全黑屏?
    │   ├─ 着色器编译成功? → 检查 gl_Position
    │   ├─ 深度测试? → 关闭试试
    │   └─ 面剔除? → glDisable(GL_CULL_FACE)
    │
    ├─ 纹理异常?
    │   ├─ 全黑? → 检查绑定、激活、翻转
    │   ├─ 花屏? → 检查 UV 坐标、数据格式
    │   └─ 颜色错? → 检查格式 (RGB vs BGR)
    │
    ├─ 闪烁?
    │   ├─ Z-fighting → 增大 near 值
    │   └─ 未清除缓冲 → 每帧 glClear
    │
    ├─ 性能差?
    │   ├─ RenderDoc 分析绘制调用
    │   ├─ NSight 分析 GPU 瓶颈
    │   └─ 检查是否使用了实例化
    │
    └─ 只在特定 GPU 上出错?
        ├─ 驱动版本太旧?
        ├─ 着色器版本不支持?
        └─ 扩展不可用?

17.8 日志与断言

17.8.1 GL 错误检查宏

// 宏:每行 GL 调用后自动检查
#ifdef DEBUG
#define GL_CHECK(call) do { \
    call; \
    GLenum err = glGetError(); \
    if (err != GL_NO_ERROR) { \
        fprintf(stderr, "GL Error 0x%04X at %s:%d (%s)\n", \
                err, __FILE__, __LINE__, #call); \
    } \
} while(0)
#else
#define GL_CHECK(call) call
#endif

// 使用
GL_CHECK(glDrawArrays(GL_TRIANGLES, 0, 36));
GL_CHECK(glBindTexture(GL_TEXTURE_2D, textureId));

17.8.2 着色器编译检查

GLuint compileShader(GLenum type, const char* source) {
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, &source, NULL);
    glCompileShader(shader);

    GLint success;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success) {
        char infoLog[2048];
        glGetShaderInfoLog(shader, sizeof(infoLog), NULL, infoLog);
        fprintf(stderr, "Shader compilation failed:\n%s\n", infoLog);
        fprintf(stderr, "Source:\n%s\n", source);
        glDeleteShader(shader);
        return 0;
    }
    return shader;
}

17.9 跨平台调试技巧

17.9.1 Mesa 软件渲染调试

# 强制使用软件渲染(排除 GPU 驱动问题)
LIBGL_ALWAYS_SOFTWARE=1 ./my_app

# 使用特定的 Mesa 驱动
GALLIUM_DRIVER=llvmpipe ./my_app   # LLVM 软件渲染
GALLIUM_DRIVER=softpipe ./my_app   # 参考软件渲染(最慢但最准确)

17.9.2 调试构建

# CMake 调试构建
cmake .. -DCMAKE_BUILD_TYPE=Debug -DENABLE_GL_DEBUG=ON

# 启用 Address Sanitizer
cmake .. -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON

17.10 注意事项

⚠️ 调试回调的性能影响GL_DEBUG_OUTPUT_SYNCHRONOUS 会强制同步回调,影响性能。只在调试时启用,发布时关闭。

⚠️ RenderDoc 会改变渲染行为:RenderDoc 拦截所有 GL 调用,可能掩盖时序相关的问题。某些 bug 在 RenderDoc 中不出现。

⚠️ APITrace 文件可能很大:长时间运行的程序会产生 GB 级的追踪文件。限制追踪时间或使用过滤。


17.11 业务场景

场景 1:新 GPU 型号兼容性测试

使用 RenderDoc 分析在新 GPU 上出现的渲染异常,对比正常帧和异常帧的差异。

场景 2:性能优化

使用 NSight 的性能计数器定位瓶颈:是 ALU 限制还是内存带宽限制。

场景 3:自动化回归测试

CI 中使用 APITrace 追踪渲染输出,与基准图像对比检测回归。


17.12 扩展阅读

资源 说明
RenderDoc 文档 官方使用指南
APITrace Wiki 工具文档
NVIDIA NSight NVIDIA 调试工具
Mesa 调试 Mesa 驱动调试信息
Khronos Debugging OpenGL 调试输出文档

本章小结

  • 黑屏排查顺序:着色器 → 矩阵 → 深度测试 → 面剔除
  • 纹理异常排查:绑定 → 激活 → 坐标 → 格式 → 翻转
  • 调试回调(GL_DEBUG_OUTPUT)是最轻量的诊断工具
  • RenderDoc 是帧级调试的标准工具,支持像素历史追踪
  • APITrace 记录所有 API 调用,适合回归测试
  • NSight 提供硬件级性能分析
  • Mesa 软件渲染可以排除 GPU 驱动问题

上一章第 16 章:Docker 中的 GPU 下一章第 18 章:最佳实践