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

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 NSightNVIDIA 调试工具
Mesa 调试Mesa 驱动调试信息
Khronos DebuggingOpenGL 调试输出文档

本章小结

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

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