图形编程想要调试并不是一件容易的事,有的时候渲染出全黑的结果基本上只能凭经验来查错,特别是对于着色器,断点日志都是无效的,因此想办法掌握一些调试方法还是有必要的,不然找错误的源头可能真的会非常困难
一、glGetError()
可以在程序的任意地方调用glGetError方法,它会返回之前所有的错误标记,常见的错误如下:
标记 | 代码 | 描述 |
---|---|---|
GL_NO_ERROR | 0 | 自上次调用glGetError以来没有错误 |
GL_INVALID_ENUM | 1280 | 枚举参数不合法 |
GL_INVALID_VALUE | 1281 | 值参数不合法 |
GL_INVALID_OPERATION | 1282 | 一个指令的状态对指令的参数不合法 |
GL_STACK_OVERFLOW | 1283 | 压栈操作造成栈上溢(Overflow) |
GL_STACK_UNDERFLOW | 1284 | 弹栈操作时栈在最低点(译注:即栈下溢(Underflow)) |
GL_OUT_OF_MEMORY | 1285 | 内存调用操作无法调用(足够的)内存 |
GL_INVALID_FRAMEBUFFER_OPERATION | 1286 | 读取或写入一个不完整的帧缓冲 |
不过需要注意一下几点:
- 每次调用glGetError方法,都会输出并清空当前所有的错误标记
- 输出的错误可能是在任意地方产生的,你并不会知道产生这个错误的具体方法和行号
- 调用glewInit()会自动设置一个GL_INVALID_ENUM的错误标记,所以需要在调用glewInit之后立即调用glGetError以消除这个标记,这应该是一个glew自带的bug
当然,glGetError()方法返回的并不是字符串,而是一个错误码(数字)也就是上表中的代码那一列,因此每次拿到这些错误码都需要去查一次错误码对应的实际错误描述,这个比较麻烦
一个一劳永逸的方法就是这样:
void glCheckError_(const char* file, int line)
{
GLenum errorCode;
while ((errorCode = glGetError()) != GL_NO_ERROR)
{
string error;
switch (errorCode)
{
case GL_INVALID_ENUM: error = "INVALID_ENUM"; break;
case GL_INVALID_VALUE: error = "INVALID_VALUE"; break;
case GL_INVALID_OPERATION: error = "INVALID_OPERATION"; break;
case GL_STACK_OVERFLOW: error = "STACK_OVERFLOW"; break;
case GL_STACK_UNDERFLOW: error = "STACK_UNDERFLOW"; break;
case GL_OUT_OF_MEMORY: error = "OUT_OF_MEMORY"; break;
case GL_INVALID_FRAMEBUFFER_OPERATION: error = "INVALID_FRAMEBUFFER_OPERATION"; break;
}
cout << error << " | " << file << " (" << line << ")" << endl;
}
}
也就是自己写一个枚举来自动转化,并且同时输出当前glGetError()的行号
二、调试输出
还有个比glGetError更好的办法,那就是使用openGL自带的调试输出扩展工具,可惜的是只有4.3以上的openGL版本支持,它可以给出更详细的错误报告,并且你可以通过断点来得到产生错误的堆栈
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
GLint flags; glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
{
cout << "成功启用调试功能" << endl;
}
常规操作,使用glfwWindowHint开启,并且检查是否成功地初始化了调试上下文的功能,注意一下,若是发布正式的版本,记得不要开启调试,它必然会影响程序效率
其次就是开启调试输出,设置输出回调和日志筛选:
- glDebugMessageCallback:提供一个调试回调函数来接收所产生的调试信息,非常重要
- glDebugMessageControl:过滤输出的调试信息/日志,例如你可以选择无视一些无关紧要的报告。前三个参数分别表示消息来源、消息类型、和消息等级,如果设置为GL_DONT_CARE则默认全部接收,第4个参数表示消息标志符的数目,第5个参数为消息标志符的数组,这两个不用管,传0和空就好,最后一个参数为是否启用,为GL_FALSE时表示丢弃对应消息
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
{
cout << "DEBUG_CONTEXT_RUNING_SUCCESS" << endl;
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(glDebugOutput, NULL);
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, 0, GL_TRUE);
}
回调函数直接复制粘贴下面的代码吧,看了就明白了,不需要多解释
需要注意的是:这个接口的参数是不可变更的,必须要对的上,不然就会出现严重错误
void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, void* userParam)
{
if (id == 131169 || id == 131185 || id == 131218 || id == 131204)
return;
cout << "---------------" << endl;
cout << "Debug message (" << id << "): " << message << endl;
//生成消息的对象
switch (source)
{
case GL_DEBUG_SOURCE_API: cout << "Source: API"; break; //GL
case GL_DEBUG_SOURCE_WINDOW_SYSTEM: cout << "Source: Window System"; break; //GLSL编译器或者其他着色语言的编译器
case GL_DEBUG_SOURCE_SHADER_COMPILER: cout << "Source: Shader Compiler"; break; //窗口系统,比如WGL或者GLX
case GL_DEBUG_SOURCE_THIRD_PARTY: cout << "Source: Third Party"; break; //外部调试器或者第三方中间库
case GL_DEBUG_SOURCE_APPLICATION: cout << "Source: Application"; break; //应用程序
case GL_DEBUG_SOURCE_OTHER: cout << "Source: Other"; break; //与前面列出都不相符的源
}
cout << "\n";
//产生消息的情况
switch (type)
{
case GL_DEBUG_TYPE_ERROR: cout << "Type: Error"; break; //生成一个错误的事件
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: cout << "Type: Deprecated Behaviour"; break; //被标记为弃用的行为
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: cout << "Type: Undefined Behaviour"; break; //在规范中未定义的行为
case GL_DEBUG_TYPE_PORTABILITY: cout << "Type: Portability"; break; //依赖于实现的性能警告
case GL_DEBUG_TYPE_PERFORMANCE: cout << "Type: Performance"; break; //使用特定供应商提供的扩展或者着色器
case GL_DEBUG_TYPE_MARKER: cout << "Type: Marker"; break; //命令流的注解
case GL_DEBUG_TYPE_PUSH_GROUP: cout << "Type: Push Group"; break; //进入一个调试组
case GL_DEBUG_TYPE_POP_GROUP: cout << "Type: Pop Group"; break; //离开一个调试组
case GL_DEBUG_TYPE_OTHER: cout << "Type: Other"; break; //与前面列出都不相符的类型
}
cout << "\n";
//显示消息的例子
switch (severity)
{
case GL_DEBUG_SEVERITY_HIGH: cout << "Severity: high"; break; //任何GL错误;危险的未定义行为;着色器编译错误和链接错误
case GL_DEBUG_SEVERITY_MEDIUM: cout << "Severity: medium"; break; //严重的性能警告;着色器编译链接警告;使用弃用的行为
case GL_DEBUG_SEVERITY_LOW: cout << "Severity: low"; break; //冗余状态改变所产生的性能警告;微不足道的弃用行为
case GL_DEBUG_SEVERITY_NOTIFICATION: cout << "Severity: notification"; break; //没有任何错误或者性能警告
}
cout << "\n";
cout << endl;
}
好了,可以利用glDebugMessageInsert方法测试一下:
glDebugMessageInsert:自定义调试消息传送给调试环境,第124个参数分别表示表示消息来源、消息类型、和消息等级,第3个参数为消息标志,第5个参数为消息长度,第6个参数为内容,第3个参数和第5个参数可以暂时不用管
glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 10000, GL_DEBUG_SEVERITY_HIGH, -1, "这是一个测试");
三、着色器调试
直接将对应的属性/变量传入片段着色器中输出是一个不错的选择,或者合理的利用几何着色器
如果得到了一个全黑的结果,最好检查一下所有的uniform变量是否合理的赋值,其次检查观察矩阵投影矩阵等几个决定屏幕位置的重要矩阵值是否正确,很有可能结果是被渲染到了很远的屏幕外面
这篇文章列举了几乎所有可能黑屏的原因

幸运的是,只要能渲染出结果,那么调试就会变得容易不少
GLSL参考编译器
每一个驱动对于着色器的规范都不太一样,NVIDIA相对于更加宽容一些,但是AMD却会严格执行OpenGL规范,这样就会导致同样的着色器在一台机器上能正常工作,另一台机器就不可以了
想要保证着色器代码在所有的机器上都能运行,可以直接对着官方的标准使用OpenGL的GLSL参考编译器(Reference Compiler)来检查。从这里下载所谓的GLSL语言校验器(GLSL Lang Validator)的可执行版本,或者从这里找到完整的源码
有了GLSL语言校验器,就可以很方便的检查你的着色器代码,只需要把着色器文件作为程序的第一格参数即可:
- .vert:顶点着色器(Vertex Shader)
- .frag:片段着色器(Fragment Shader)
- .geom:几何着色器(Geometry Shader)
- .tesc:细分控制着色器(Tessellation Control Shader)
- .tese:细分评估着色器(Tessellation Evaluation Shader)
- .comp:计算着色器(Compute Shader)
运行GLSL参考编译器非常简单,如果没有检测到错误的话就没有输出
glsllangvalidator shaderFile.vert
这本质上是一个规范检查工具,因此并不是万能的,你的着色器仍然有可能有BUG
四、专业调试软件
在专业调试软件下,上面的测试方法就显得不太需要了
这些第三方应用的运作原理是:注入到OpenGL驱动中拦截各种OpenGL调用,给你大量有用的数据,能提供的帮助包括但不限于:对OpenGL函数使用进行性能测试,寻找瓶颈,检查缓冲内存,显示纹理和帧缓冲附件等,若要写(大规模)生产代码,这类的工具在开发过程中是非常有用的
当然具体用哪一个应用,就去专门了解对应的就好了
gDebugger:http://www.gremedy.com/
RenderDoc:https://github.com/baldurk/renderdoc
CodeXL:https://developer.amd.com/tools-and-sdks/opencl-zone/codexl/
参考: