实践项目:https://github.com/JackieLong/OpenGL/tree/main/project_debug_test

一、C++代码调试

技巧一:查询error flag

先看例子,了解glGetError的特性:

  1. while(......) // 渲染循环
  2. {
  3. ......
  4. // **********************************
  5. // 情形一
  6. // **********************************
  7. glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, // 产生一个GL_INVALID_OPERATION错误
  8. ScreenWidth, ScreenHeight,
  9. 0, GL_RGBA,
  10. GL_UNSIGNED_BYTE_3_3_2,
  11. NULL );
  12. errorCode = glGetError(); // GL_INVALID_OPERATION
  13. glBindBuffer( GL_VERTEX_ARRAY, -1 ); // 产生一个GL_INVALID_ENUM错误
  14. errorCode = glGetError(); // GL_INVALID_ENUM
  15. errorCode = glGetError(); // GL_NO_ERROR
  16. // **********************************
  17. // 情形二
  18. // **********************************
  19. glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, // 产生一个GL_INVALID_OPERATION错误
  20. ScreenWidth, ScreenHeight,
  21. 0, GL_RGBA,
  22. GL_UNSIGNED_BYTE_3_3_2,
  23. NULL );
  24. glBindBuffer( GL_VERTEX_ARRAY, -1 ); // 产生一个GL_INVALID_ENUM错误
  25. errorCode = glGetError(); // GL_INVALID_OPERATION
  26. errorCode = glGetError(); // GL_NO_ERROR
  27. ......
  28. }

当不正确使用OpenGL函数时,OpenGL将会在幕后生成一个或多个错误标记(error flag),当调用一次glGetError时,将返回其中一个错误标记并且会清除其他标记(分布式系统除外)。如果在每帧结尾调用glGetError,它会返回一个错误,但这并不一定是唯一错误,并且这个错误可能来自任意的地方。
如果OpenGL是分布式运行的时候,如果产生了多个错误,调用一次glGetError只会重置其中一个错误标记,所以我们通常会在循环中调用glGetError。

glGetError

  1. GLenum errorCode = glGetError( void );
  2. // glGetError returns the value of the error flag. Each detectable error is assigned
  3. // a numeric code and symbolic name. When an error occurs, the error flag is set to the
  4. // appropriate error code value. No other errors are recorded until glGetError is called,
  5. // the error code is returned, and the flag is reset to GL_NO_ERROR. If a call to glGetError
  6. // returns GL_NO_ERROR, there has been no detectable error since the last call to glGetError,
  7. // or since the GL was initialized.
  8. // errorCode: 返回一个错误标记的值,3.0版本的可能值如下:(更新版本会有增加)
  9. // GL_NO_ERROR: 表示没有错误,值一定是0
  10. // GL_INVALID_ENUM: GLenum类型的参数不合法,有问题的调用将被忽略不会产生任何影响
  11. // GL_INVALID_VALUE: 数值类型的参数越界,有问题的调用将被忽略不会产生任何影响
  12. // GL_INVALID_OPERATION: 在当前state下,指定操作不被允许,有问题的调用将被忽略不会产生任何影响。
  13. // GL_INVALID_FRAMEBUFFER_OPERATION: 帧缓冲对象不完整,有问题的调用将被忽略不会产生任何影响。
  14. // GL_OUT_OF_MEMORY: 内存不足,有问题调用将会导致GL state未定义
  15. // 只有在GL_OUT_OF_MEMORY错误下,有问题的OpenGL调用结果将未定义,其他情况对GL State和帧缓冲内容不会有任何影响。

封装函数

直接使用glGetError函数,返回的errorcode是一个数值,鬼知道是啥意思。我们可以封装一下:

  1. // *****************************************
  2. // 封装函数使用方法如下:
  3. // *****************************************
  4. glBindBuffer( GL_VERTEX_ARRAY, -1 ); // 产生GL_INVALID_ENUM错误
  5. // 返回errorcode,同时打印:invalid_enum|test.cpp(40),
  6. // test.cpp: 是glCheckError所在文件路径
  7. // 40: 是glCheckError调用所在行号。
  8. GLenum errorCode = glCheckError();
  9. // *****************************************
  10. // 封装函数定义如下
  11. // *****************************************
  12. //__FILE__、__LINE__皆为预处理指令,分别表示调用所在文件路径和调用所在行号。
  13. #define glCheckError() _glCheckError(__FILE__, __LINE__)
  14. GLenum _glCheckError( const string &file, int line )
  15. {
  16. GLenum errorCode;
  17. while( ( errorCode = glGetError() ) != GL_NO_ERROR )
  18. {
  19. string error;
  20. switch( errorCode )
  21. {
  22. case GL_INVALID_ENUM:
  23. error = "invalid_enum";
  24. break;
  25. case GL_INVALID_VALUE:
  26. error = "invalid_value";
  27. break;
  28. case GL_INVALID_OPERATION:
  29. error = "invalid_operation";
  30. break;
  31. case GL_STACK_OVERFLOW:
  32. error = "stack_overflow";
  33. break;
  34. case GL_STACK_UNDERFLOW:
  35. error = "stack_underflow";
  36. default:
  37. break;
  38. case GL_OUT_OF_MEMORY:
  39. error = "out_of_memory";
  40. break;
  41. case GL_INVALID_FRAMEBUFFER_OPERATION:
  42. error = "invalid_framebuffer_operation";
  43. break;
  44. }
  45. cout << error << "|" << file.substr( file.find_last_of( "\\" ) + 1 ) << "(" << line << ")" << endl;
  46. }
  47. return errorCode;
  48. }

技巧二:调试输出

简单理解就是设置一个全局回调函数,当错误产生或者输出debugOutput时,触发这个回调函数。使用方法如下:

  1. // ************************************************
  2. // 第一步:请求调试输出的上下文(在初始化GLFW时候调用)
  3. // ************************************************
  4. // 向OpenGL请求一个调试输出上下文,用于输出调试信息
  5. // 在调试上下文中使用OpenGL会明显更缓慢一点,所以当你在优化或者发布程序之前请将这一GLFW调试请求给注释掉。
  6. glfwWindowHint( GLFW_OPENGL_DEBUG_CONTEXT, true );
  7. ......
  8. // ************************************************
  9. // 第二步:初始化调试输出
  10. // ************************************************
  11. initDebugOutput();
  12. // ************************************************
  13. // 第三步:触发回调
  14. // ************************************************
  15. while(......) // 渲染循环中
  16. {
  17. ......
  18. glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, // 产生一个GL_INVALID_OPERATION错误,触发debugOutputCallback
  19. ScreenWidth, ScreenHeight,
  20. 0, GL_RGBA,
  21. GL_UNSIGNED_BYTE_3_3_2,
  22. NULL );
  23. glBindBuffer( GL_VERTEX_ARRAY, -1 ); // 产生一个GL_INVALID_ENUM错误,触发debugOutputCallback
  24. glDebugMessageInsert( // 手动输出一条自定义调试输出,触发debugOutputCallback
  25. GL_DEBUG_SOURCE_APPLICATION,
  26. GL_DEBUG_TYPE_ERROR,
  27. 0,
  28. GL_DEBUG_SEVERITY_MEDIUM,
  29. -1,
  30. "error message here" );
  31. ......
  32. }
  33. void initDebugOutput() // 初始化调试输出
  34. {
  35. int flags;
  36. glGetIntegerv( GL_CONTEXT_FLAGS, &flags );
  37. if( flags & GL_CONTEXT_FLAG_DEBUG_BIT ) // 检查是否成功请求到了调试上下文
  38. {
  39. glEnable( GL_DEBUG_OUTPUT );
  40. glEnable( GL_DEBUG_OUTPUT_SYNCHRONOUS ); // makes sure errors are displayed synchronously
  41. glDebugMessageCallback( // 设置调试输出回调函数,到产生错误标记时,会触发该函数
  42. debugOutputCallback, // 回调函数地址
  43. nullptr ); // 我们自定义的参数,对应回调函数中的userParam
  44. glDebugMessageControl( // 过滤调试输出,可以选择过滤出需要的错误类型
  45. GL_DONT_CARE,
  46. GL_DONT_CARE,
  47. GL_DONT_CARE,
  48. 0,
  49. nullptr,
  50. GL_TRUE );
  51. }
  52. }
  53. void APIENTRY debugOutputCallback( // 调试输出回调函数
  54. GLenum source,
  55. GLenum type,
  56. unsigned int id,
  57. GLenum severity,
  58. GLsizei length,
  59. const char *message,
  60. const void *userParam )
  61. {
  62. if( id == 131169 || id == 131185 || id == 131218 || id == 131204 )
  63. {
  64. // 这些是不重要的错误,可以忽略
  65. return;
  66. }
  67. std::cout << "---------------" << std::endl;
  68. std::cout << "Debug message (" << id << "): " << message << std::endl;
  69. switch( source )
  70. {
  71. case GL_DEBUG_SOURCE_API:
  72. std::cout << "Source: API";
  73. break;
  74. case GL_DEBUG_SOURCE_WINDOW_SYSTEM:
  75. std::cout << "Source: Window System";
  76. break;
  77. case GL_DEBUG_SOURCE_SHADER_COMPILER:
  78. std::cout << "Source: Shader Compiler";
  79. break;
  80. case GL_DEBUG_SOURCE_THIRD_PARTY:
  81. std::cout << "Source: Third Party";
  82. break;
  83. case GL_DEBUG_SOURCE_APPLICATION:
  84. std::cout << "Source: Application";
  85. break;
  86. case GL_DEBUG_SOURCE_OTHER:
  87. std::cout << "Source: Other";
  88. break;
  89. }
  90. std::cout << std::endl;
  91. switch( type )
  92. {
  93. case GL_DEBUG_TYPE_ERROR:
  94. std::cout << "Type: Error";
  95. break;
  96. case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
  97. std::cout << "Type: Deprecated Behaviour";
  98. break;
  99. case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
  100. std::cout << "Type: Undefined Behaviour";
  101. break;
  102. case GL_DEBUG_TYPE_PORTABILITY:
  103. std::cout << "Type: Portability";
  104. break;
  105. case GL_DEBUG_TYPE_PERFORMANCE:
  106. std::cout << "Type: Performance";
  107. break;
  108. case GL_DEBUG_TYPE_MARKER:
  109. std::cout << "Type: Marker";
  110. break;
  111. case GL_DEBUG_TYPE_PUSH_GROUP:
  112. std::cout << "Type: Push Group";
  113. break;
  114. case GL_DEBUG_TYPE_POP_GROUP:
  115. std::cout << "Type: Pop Group";
  116. break;
  117. case GL_DEBUG_TYPE_OTHER:
  118. std::cout << "Type: Other";
  119. break;
  120. }
  121. std::cout << std::endl;
  122. switch( severity )
  123. {
  124. case GL_DEBUG_SEVERITY_HIGH:
  125. std::cout << "Severity: high";
  126. break;
  127. case GL_DEBUG_SEVERITY_MEDIUM:
  128. std::cout << "Severity: medium";
  129. break;
  130. case GL_DEBUG_SEVERITY_LOW:
  131. std::cout << "Severity: low";
  132. break;
  133. case GL_DEBUG_SEVERITY_NOTIFICATION:
  134. std::cout << "Severity: notification";
  135. break;
  136. }
  137. std::cout << std::endl;
  138. std::cout << std::endl;
  139. }

技巧三:帧缓冲输出

通过实时渲染帧缓冲(纹理附件),从视觉上快速检查错误,比如检查某个非默认帧缓冲的纹理附件数据是否正常,比如将法向量输出到帧缓冲纹理附件来检查法向量数据是否正常。
具体做法,我们需要一个最简单的顶点着色器和片段着色器,再加上一个助手函数,输入纹理ID,然后在屏幕右上角绘制一个小窗口用于渲染该纹理。代码如下:

  1. // **************************************************
  2. // ********* 助手函数使用例子
  3. // **************************************************
  4. while(......)
  5. {
  6. ......
  7. GLuint texxtureID; // 可以是普通2D纹理,可以是帧缓冲纹理附件
  8. ......
  9. debugOutputTexture(textureID); // 输出纹理到指定窗口
  10. ......
  11. }
  12. // **************************************************
  13. // ********* 以下是助手函数代码
  14. // **************************************************
  15. const GLenum textureCellID = GL_TEXTURE0; // 调试输出使用的纹理单元ID
  16. const GLfloat winWidth = 0.5f; // 窗口宽
  17. const GLfloat winHeight = 0.5f; // 窗口高
  18. const glm::vec2 winPos = glm::vec2( 1.0f - winWidth, 1.0f - winHeight ); // 窗口左下角位置
  19. const GLfloat vertices_debugOutputTexture[] =
  20. {
  21. // 顶点坐标(NDC坐标) // 纹理坐标
  22. winPos.x, winPos.y, 0.0f, 0.0f, // 左下角
  23. winPos.x + winWidth, winPos.y, 1.0f, 0.0f, // 右下角
  24. winPos.x + winWidth, winPos.y + winHeight, 1.0f, 1.0f, // 右上角
  25. winPos.x, winPos.y, 0.0f, 0.0f, // 左下角
  26. winPos.x + winWidth, winPos.y + winHeight, 1.0f, 1.0f, // 右上角
  27. winPos.x, winPos.y + winHeight, 0.0f, 1.0f, // 左上角
  28. };
  29. const string vertexShaderSrc_debugOutputTexture = // 顶点着色器
  30. "#version 330 core \n\
  31. \n\
  32. layout( location = 0 ) in vec2 aPos; // 直接是NDC坐标 \n\
  33. layout( location = 1 ) in vec2 aTexCoords; // 纹理坐标 \n\
  34. out vec2 texCoords; \n\
  35. \n\
  36. void main() { \n\
  37. gl_Position = vec4( aPos, 0.0, 1.0 ); \n\
  38. texCoords = aTexCoords; \n\
  39. } \
  40. ";
  41. const string fragmentShaderSrc_debugOutputTexture = // 片段着色器
  42. "#version 330 core \n\
  43. \n\
  44. in vec2 texCoords; \n\
  45. uniform sampler2D textureSampler; \n\
  46. out vec4 FragColor; \n\
  47. \n\
  48. void main() { \n\
  49. FragColor = texture( textureSampler, texCoords ); \n\
  50. } \
  51. ";
  52. GLuint VAO_debugOutputTexture = -1;
  53. GLuint VBO_debugOutputTexture = -1;
  54. Shader shader_debugOutputTexture;
  55. void debugOutputTexture( GLuint textureID )
  56. {
  57. if( VAO_debugOutputTexture == -1 )
  58. {
  59. createVertexBuffer( vertices_debugOutputTexture,
  60. sizeof( vertices_debugOutputTexture ),
  61. "22",
  62. &VAO_debugOutputTexture,
  63. &VBO_debugOutputTexture );
  64. shader_debugOutputTexture.initWithSrc( vertexShaderSrc_debugOutputTexture,
  65. fragmentShaderSrc_debugOutputTexture );
  66. }
  67. glBindVertexArray( VAO_debugOutputTexture );
  68. glActiveTexture( textureCellID );
  69. glBindTexture( GL_TEXTURE_2D, textureID );
  70. shader_debugOutputTexture.use();
  71. shader_debugOutputTexture.setInt( "textureSampler", textureCellID - GL_TEXTURE0 );
  72. glDrawArrays( GL_TRIANGLES, 0, 6 );
  73. glBindVertexArray( 0 );
  74. glUseProgram( 0 );
  75. }

技巧四:第三方调试工具

这些工具会注入到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. // 如何通过视觉获取有用信息
  2. // (1.0f, 0.0f, 0.0f),红色,法向量朝正右方,模型上的朝向右方的面显示红色,如腿的右侧
  3. // (0.0f, 1.0f, 0.0f),绿色,法向量朝正上方,模型上朝上的面显示绿色,如头顶、肩膀
  4. // (0.0f, 0.0f, 1.0f),蓝色,法向量朝向“我们”,模型朝前方的面显示蓝色,如胸脯。

}

  1. 正确的法向量结果输出如下:<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)
  2. <a name="vmd3a"></a>
  3. ## GLSL参考编译器
  4. 不同GPU厂商实现的OpenGL之间有细微差别,如NVIDIA会更宽容一点,忽略一些限制和规范,ATI/AMD则严格执行OpenGL规范,所以同一份着色器代码不能保证在不同的图形设备上都能正常运行。为了能尽量达到这个目的,我们可以使用官方提供的OpenGL参考编译器(reference compiler)来检查着色器代码,它能确保着色器代码是否完全符合OpenGL标准规范,但要记住,这也不能完全保证着色器完全没有BUG
  5. - [参考编译器源码](https://github.com/KhronosGroup/glslang)
  6. - [参考编译器可执行文件](https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/)
  7. 使用方法如下:

// 命令行,检查顶点着色器文件shader的语法

glsllangvalidator shader.vert

不同类型着色器的扩展名 .vert 顶点着色器(Vertex Shader) .frag 片段着色器(Fragment Shader) .geom 几何着色器(Geometry Shader) .tesc 细分控制着色器(Tessellation Control Shader) .tese 细分计算着色器(Tessellation Evaluation Sahder) .comp 计算着色器(Compute Shader)

``` 对于一个不正确的顶点着色器,参考编译器的输出结果如下:
debugging_glsl_reference_compiler.png