实践项目:https://github.com/JackieLong/OpenGL/tree/main/project_debug_test
一、C++代码调试
技巧一:查询error flag
先看例子,了解glGetError的特性:
while(......) // 渲染循环{......// **********************************// 情形一// **********************************glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, // 产生一个GL_INVALID_OPERATION错误ScreenWidth, ScreenHeight,0, GL_RGBA,GL_UNSIGNED_BYTE_3_3_2,NULL );errorCode = glGetError(); // GL_INVALID_OPERATIONglBindBuffer( GL_VERTEX_ARRAY, -1 ); // 产生一个GL_INVALID_ENUM错误errorCode = glGetError(); // GL_INVALID_ENUMerrorCode = glGetError(); // GL_NO_ERROR// **********************************// 情形二// **********************************glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, // 产生一个GL_INVALID_OPERATION错误ScreenWidth, ScreenHeight,0, GL_RGBA,GL_UNSIGNED_BYTE_3_3_2,NULL );glBindBuffer( GL_VERTEX_ARRAY, -1 ); // 产生一个GL_INVALID_ENUM错误errorCode = glGetError(); // GL_INVALID_OPERATIONerrorCode = glGetError(); // GL_NO_ERROR......}
当不正确使用OpenGL函数时,OpenGL将会在幕后生成一个或多个错误标记(error flag),当调用一次glGetError时,将返回其中一个错误标记并且会清除其他标记(分布式系统除外)。如果在每帧结尾调用glGetError,它会返回一个错误,但这并不一定是唯一错误,并且这个错误可能来自任意的地方。
如果OpenGL是分布式运行的时候,如果产生了多个错误,调用一次glGetError只会重置其中一个错误标记,所以我们通常会在循环中调用glGetError。
glGetError
GLenum errorCode = glGetError( void );// glGetError returns the value of the error flag. Each detectable error is assigned// a numeric code and symbolic name. When an error occurs, the error flag is set to the// appropriate error code value. No other errors are recorded until glGetError is called,// the error code is returned, and the flag is reset to GL_NO_ERROR. If a call to glGetError// returns GL_NO_ERROR, there has been no detectable error since the last call to glGetError,// or since the GL was initialized.// errorCode: 返回一个错误标记的值,3.0版本的可能值如下:(更新版本会有增加)// GL_NO_ERROR: 表示没有错误,值一定是0// GL_INVALID_ENUM: GLenum类型的参数不合法,有问题的调用将被忽略不会产生任何影响// GL_INVALID_VALUE: 数值类型的参数越界,有问题的调用将被忽略不会产生任何影响// GL_INVALID_OPERATION: 在当前state下,指定操作不被允许,有问题的调用将被忽略不会产生任何影响。// GL_INVALID_FRAMEBUFFER_OPERATION: 帧缓冲对象不完整,有问题的调用将被忽略不会产生任何影响。// GL_OUT_OF_MEMORY: 内存不足,有问题调用将会导致GL state未定义// 只有在GL_OUT_OF_MEMORY错误下,有问题的OpenGL调用结果将未定义,其他情况对GL State和帧缓冲内容不会有任何影响。
封装函数
直接使用glGetError函数,返回的errorcode是一个数值,鬼知道是啥意思。我们可以封装一下:
// *****************************************// 封装函数使用方法如下:// *****************************************glBindBuffer( GL_VERTEX_ARRAY, -1 ); // 产生GL_INVALID_ENUM错误// 返回errorcode,同时打印:invalid_enum|test.cpp(40),// test.cpp: 是glCheckError所在文件路径// 40: 是glCheckError调用所在行号。GLenum errorCode = glCheckError();// *****************************************// 封装函数定义如下// *****************************************//__FILE__、__LINE__皆为预处理指令,分别表示调用所在文件路径和调用所在行号。#define glCheckError() _glCheckError(__FILE__, __LINE__)GLenum _glCheckError( const string &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";default:break;case GL_OUT_OF_MEMORY:error = "out_of_memory";break;case GL_INVALID_FRAMEBUFFER_OPERATION:error = "invalid_framebuffer_operation";break;}cout << error << "|" << file.substr( file.find_last_of( "\\" ) + 1 ) << "(" << line << ")" << endl;}return errorCode;}
技巧二:调试输出
简单理解就是设置一个全局回调函数,当错误产生或者输出debugOutput时,触发这个回调函数。使用方法如下:
// ************************************************// 第一步:请求调试输出的上下文(在初始化GLFW时候调用)// ************************************************// 向OpenGL请求一个调试输出上下文,用于输出调试信息// 在调试上下文中使用OpenGL会明显更缓慢一点,所以当你在优化或者发布程序之前请将这一GLFW调试请求给注释掉。glfwWindowHint( GLFW_OPENGL_DEBUG_CONTEXT, true );......// ************************************************// 第二步:初始化调试输出// ************************************************initDebugOutput();// ************************************************// 第三步:触发回调// ************************************************while(......) // 渲染循环中{......glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, // 产生一个GL_INVALID_OPERATION错误,触发debugOutputCallbackScreenWidth, ScreenHeight,0, GL_RGBA,GL_UNSIGNED_BYTE_3_3_2,NULL );glBindBuffer( GL_VERTEX_ARRAY, -1 ); // 产生一个GL_INVALID_ENUM错误,触发debugOutputCallbackglDebugMessageInsert( // 手动输出一条自定义调试输出,触发debugOutputCallbackGL_DEBUG_SOURCE_APPLICATION,GL_DEBUG_TYPE_ERROR,0,GL_DEBUG_SEVERITY_MEDIUM,-1,"error message here" );......}void initDebugOutput() // 初始化调试输出{int flags;glGetIntegerv( GL_CONTEXT_FLAGS, &flags );if( flags & GL_CONTEXT_FLAG_DEBUG_BIT ) // 检查是否成功请求到了调试上下文{glEnable( GL_DEBUG_OUTPUT );glEnable( GL_DEBUG_OUTPUT_SYNCHRONOUS ); // makes sure errors are displayed synchronouslyglDebugMessageCallback( // 设置调试输出回调函数,到产生错误标记时,会触发该函数debugOutputCallback, // 回调函数地址nullptr ); // 我们自定义的参数,对应回调函数中的userParamglDebugMessageControl( // 过滤调试输出,可以选择过滤出需要的错误类型GL_DONT_CARE,GL_DONT_CARE,GL_DONT_CARE,0,nullptr,GL_TRUE );}}void APIENTRY debugOutputCallback( // 调试输出回调函数GLenum source,GLenum type,unsigned int id,GLenum severity,GLsizei length,const char *message,const void *userParam ){if( id == 131169 || id == 131185 || id == 131218 || id == 131204 ){// 这些是不重要的错误,可以忽略return;}std::cout << "---------------" << std::endl;std::cout << "Debug message (" << id << "): " << message << std::endl;switch( source ){case GL_DEBUG_SOURCE_API:std::cout << "Source: API";break;case GL_DEBUG_SOURCE_WINDOW_SYSTEM:std::cout << "Source: Window System";break;case GL_DEBUG_SOURCE_SHADER_COMPILER:std::cout << "Source: Shader Compiler";break;case GL_DEBUG_SOURCE_THIRD_PARTY:std::cout << "Source: Third Party";break;case GL_DEBUG_SOURCE_APPLICATION:std::cout << "Source: Application";break;case GL_DEBUG_SOURCE_OTHER:std::cout << "Source: Other";break;}std::cout << std::endl;switch( type ){case GL_DEBUG_TYPE_ERROR:std::cout << "Type: Error";break;case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:std::cout << "Type: Deprecated Behaviour";break;case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:std::cout << "Type: Undefined Behaviour";break;case GL_DEBUG_TYPE_PORTABILITY:std::cout << "Type: Portability";break;case GL_DEBUG_TYPE_PERFORMANCE:std::cout << "Type: Performance";break;case GL_DEBUG_TYPE_MARKER:std::cout << "Type: Marker";break;case GL_DEBUG_TYPE_PUSH_GROUP:std::cout << "Type: Push Group";break;case GL_DEBUG_TYPE_POP_GROUP:std::cout << "Type: Pop Group";break;case GL_DEBUG_TYPE_OTHER:std::cout << "Type: Other";break;}std::cout << std::endl;switch( severity ){case GL_DEBUG_SEVERITY_HIGH:std::cout << "Severity: high";break;case GL_DEBUG_SEVERITY_MEDIUM:std::cout << "Severity: medium";break;case GL_DEBUG_SEVERITY_LOW:std::cout << "Severity: low";break;case GL_DEBUG_SEVERITY_NOTIFICATION:std::cout << "Severity: notification";break;}std::cout << std::endl;std::cout << std::endl;}
技巧三:帧缓冲输出
通过实时渲染帧缓冲(纹理附件),从视觉上快速检查错误,比如检查某个非默认帧缓冲的纹理附件数据是否正常,比如将法向量输出到帧缓冲纹理附件来检查法向量数据是否正常。
具体做法,我们需要一个最简单的顶点着色器和片段着色器,再加上一个助手函数,输入纹理ID,然后在屏幕右上角绘制一个小窗口用于渲染该纹理。代码如下:
// **************************************************// ********* 助手函数使用例子// **************************************************while(......){......GLuint texxtureID; // 可以是普通2D纹理,可以是帧缓冲纹理附件......debugOutputTexture(textureID); // 输出纹理到指定窗口......}// **************************************************// ********* 以下是助手函数代码// **************************************************const GLenum textureCellID = GL_TEXTURE0; // 调试输出使用的纹理单元IDconst GLfloat winWidth = 0.5f; // 窗口宽const GLfloat winHeight = 0.5f; // 窗口高const glm::vec2 winPos = glm::vec2( 1.0f - winWidth, 1.0f - winHeight ); // 窗口左下角位置const GLfloat vertices_debugOutputTexture[] ={// 顶点坐标(NDC坐标) // 纹理坐标winPos.x, winPos.y, 0.0f, 0.0f, // 左下角winPos.x + winWidth, winPos.y, 1.0f, 0.0f, // 右下角winPos.x + winWidth, winPos.y + winHeight, 1.0f, 1.0f, // 右上角winPos.x, winPos.y, 0.0f, 0.0f, // 左下角winPos.x + winWidth, winPos.y + winHeight, 1.0f, 1.0f, // 右上角winPos.x, winPos.y + winHeight, 0.0f, 1.0f, // 左上角};const string vertexShaderSrc_debugOutputTexture = // 顶点着色器"#version 330 core \n\\n\layout( location = 0 ) in vec2 aPos; // 直接是NDC坐标 \n\layout( location = 1 ) in vec2 aTexCoords; // 纹理坐标 \n\out vec2 texCoords; \n\\n\void main() { \n\gl_Position = vec4( aPos, 0.0, 1.0 ); \n\texCoords = aTexCoords; \n\} \";const string fragmentShaderSrc_debugOutputTexture = // 片段着色器"#version 330 core \n\\n\in vec2 texCoords; \n\uniform sampler2D textureSampler; \n\out vec4 FragColor; \n\\n\void main() { \n\FragColor = texture( textureSampler, texCoords ); \n\} \";GLuint VAO_debugOutputTexture = -1;GLuint VBO_debugOutputTexture = -1;Shader shader_debugOutputTexture;void debugOutputTexture( GLuint textureID ){if( VAO_debugOutputTexture == -1 ){createVertexBuffer( vertices_debugOutputTexture,sizeof( vertices_debugOutputTexture ),"22",&VAO_debugOutputTexture,&VBO_debugOutputTexture );shader_debugOutputTexture.initWithSrc( vertexShaderSrc_debugOutputTexture,fragmentShaderSrc_debugOutputTexture );}glBindVertexArray( VAO_debugOutputTexture );glActiveTexture( textureCellID );glBindTexture( GL_TEXTURE_2D, textureID );shader_debugOutputTexture.use();shader_debugOutputTexture.setInt( "textureSampler", textureCellID - GL_TEXTURE0 );glDrawArrays( GL_TRIANGLES, 0, 6 );glBindVertexArray( 0 );glUseProgram( 0 );}
技巧四:第三方调试工具
这些工具会注入到OpenGL驱动中,会拦截OpenGL的各种调用。可以帮助我们进行性能测试、瓶颈检测、缓冲内存检测、显示纹理和帧缓冲附件。这里列举一些常见工具:
- gDebugger:独立调试工具。
- RenderDoc:独立调试工具,地址。
- CodeXL:有独立版、VS插件版,由AMD开发,支持NVIDIA、Intel显卡,不支持OpenCL调试。
- NVIDIA Nsight:VS插件或者Eclipse插件,非常易用,只支持NVIDIA显卡,非常适合VS开发+NVIDIA显卡。
二、GLSL调试输出
输出到颜色通道
将变量输出到片段着色器颜色通道,通过视觉观察获取调试输出,如调试一个模型的法向量,可以如下做法: ```cpp
version 330 core
…… out vec4 FragColor; in vec3 normal; ……
void main() { …… FragColor = vec4(normal, 1.0f);
// 如何通过视觉获取有用信息// (1.0f, 0.0f, 0.0f),红色,法向量朝正右方,模型上的朝向右方的面显示红色,如腿的右侧// (0.0f, 1.0f, 0.0f),绿色,法向量朝正上方,模型上朝上的面显示绿色,如头顶、肩膀// (0.0f, 0.0f, 1.0f),蓝色,法向量朝向“我们”,模型朝前方的面显示蓝色,如胸脯。
}
正确的法向量结果输出如下:<br /><a name="vmd3a"></a>## GLSL参考编译器不同GPU厂商实现的OpenGL之间有细微差别,如NVIDIA会更宽容一点,忽略一些限制和规范,ATI/AMD则严格执行OpenGL规范,所以同一份着色器代码不能保证在不同的图形设备上都能正常运行。为了能尽量达到这个目的,我们可以使用官方提供的OpenGL参考编译器(reference compiler)来检查着色器代码,它能确保着色器代码是否完全符合OpenGL标准规范,但要记住,这也不能完全保证着色器完全没有BUG。- [参考编译器源码](https://github.com/KhronosGroup/glslang)- [参考编译器可执行文件](https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/)使用方法如下:
// 命令行,检查顶点着色器文件shader的语法
glsllangvalidator shader.vert
不同类型着色器的扩展名 .vert 顶点着色器(Vertex Shader) .frag 片段着色器(Fragment Shader) .geom 几何着色器(Geometry Shader) .tesc 细分控制着色器(Tessellation Control Shader) .tese 细分计算着色器(Tessellation Evaluation Sahder) .comp 计算着色器(Compute Shader)
```
对于一个不正确的顶点着色器,参考编译器的输出结果如下:
