实践项目: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_OPERATION
glBindBuffer( GL_VERTEX_ARRAY, -1 ); // 产生一个GL_INVALID_ENUM错误
errorCode = glGetError(); // GL_INVALID_ENUM
errorCode = 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_OPERATION
errorCode = 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错误,触发debugOutputCallback
ScreenWidth, ScreenHeight,
0, GL_RGBA,
GL_UNSIGNED_BYTE_3_3_2,
NULL );
glBindBuffer( GL_VERTEX_ARRAY, -1 ); // 产生一个GL_INVALID_ENUM错误,触发debugOutputCallback
glDebugMessageInsert( // 手动输出一条自定义调试输出,触发debugOutputCallback
GL_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 synchronously
glDebugMessageCallback( // 设置调试输出回调函数,到产生错误标记时,会触发该函数
debugOutputCallback, // 回调函数地址
nullptr ); // 我们自定义的参数,对应回调函数中的userParam
glDebugMessageControl( // 过滤调试输出,可以选择过滤出需要的错误类型
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; // 调试输出使用的纹理单元ID
const 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 />![debugging_glsl_output.png](https://cdn.nlark.com/yuque/0/2021/png/461452/1611374057793-4eca704b-fc12-4733-b117-960ca4aec951.png#align=left&display=inline&height=475&margin=%5Bobject%20Object%5D&name=debugging_glsl_output.png&originHeight=475&originWidth=600&size=74595&status=done&style=none&width=600)
<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)
```
对于一个不正确的顶点着色器,参考编译器的输出结果如下: