学习OpenGL。 第7.1课-调试

图片 图形化编程不仅是乐趣的来源,而且当某些内容未按预期显示或屏幕上什么也不显示时,感到沮丧。 看到我们所做的大多数事情都与像素操作有关,当某些事情无法正常工作时,很难找出错误的原因。 调试这种类型的错误比调试CPU上的错误更加困难。 我们没有可以输出文本的控制台,不能在着色器中放置断点,也不能仅获取并检查GPU上程序的状态。


在本教程中,我们将向您介绍OpenGL程序的一些调试方法和技术。 在OpenGL中调试并不是那么困难,学习一些技巧绝对会有所收获。



glGetError()


当您错误地使用OpenGL时(例如,当您设置一个缓冲区而忘记绑定它时),OpenGL将注意到并在幕后创建一个或多个自定义错误标志。 我们可以通过调用glGetError()函数跟踪这些错误,该函数仅检查设置的错误标志并在发生错误时返回错误值。


 GLenum glGetError(); 

此函数返回错误标志或完全没有错误。 返回值列表:


代号内容描述
GL_NO_ERROR0自上次glGetError调用以来未生成错误
GL_INVALID_ENUM1280枚举参数无效时设置
GL_INVALID_VALUE1281值无效时设置
GL_INVALID_OPERATION1282带有指定参数的命令无效时设置
GL_STACK_OVERFLOW1283在将数据推入堆栈(推入)的操作导致堆栈溢出时建立。
GL_STACK_UNDERFLOW1284当从堆栈的最小点开始从堆栈中弹出数据(pop)的操作时,将建立此标记。
GL_OUT_OF_MEMORY1285当内存分配操作无法分配足够的内存时设置。
GL_INVALID_FRAMEBUFFER_OPERATION1286从未完成的帧缓冲区读取/写入数据时设置

在OpenGL函数的文档中,您可以找到由错误使用的函数生成的错误代码。 例如,如果您查看glBindTexture()函数的文档,则可以在“错误”部分中找到此函数生成的错误代码。
设置错误标志后,将不会生成其他错误标志。 此外,当glGetError时,该函数将擦除所有错误标志(或在分布式系统上仅清除一个错误标志,请参见下文)。 这意味着,如果在每帧之后调用一次glGetError并得到一个错误,这并不意味着这是唯一的错误,并且您仍然不知道该错误发生在哪里。


请注意,当OpenGL以分布式方式工作时(通常在具有X11的系统上),当其他人使用不同的代码时,可能会生成其他错误。 然后,调用glGetError仅刷新错误代码标志之一,而不是全部。 因此,他们建议循环调用此函数。

 glBindTexture(GL_TEXTURE_2D, tex); std::cout << glGetError() << std::endl; //  0 ( ) glTexImage2D(GL_TEXTURE_3D, 0, GL_RGB, 512, 512, 0, GL_RGB, GL_UNSIGNED_BYTE, data); std::cout << glGetError() << std::endl; //  1280 ( ) glGenTextures(-5, textures); std::cout << glGetError() << std::endl; //  1281 (  std::cout << glGetError() << std::endl; //  0 ( ) 

glGetError一个独特功能是,它可以相对容易地确定可能在何处发生错误并验证OpenGL是否正确使用。 假设您什么都没画,也不知道是什么原因:帧缓冲区设置不正确? 忘记设置纹理了吗? 通过在任何地方调用glGetError ,您可以快速找出发生第一个错误的位置。
默认情况下, glGetError仅报告错误号,在记住代码号之前不容易理解。 编写一个小的函数来帮助打印错误字符串以及调用该函数的位置通常是有意义的。


 GLenum glCheckError_(const char *file, int line) { GLenum errorCode; while ((errorCode = glGetError()) != GL_NO_ERROR) { std::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; } std::cout << error << " | " << file << " (" << line << ")" << std::endl; } return errorCode; } #define glCheckError() glCheckError_(__FILE__, __LINE__) 

如果您决定对glCheckError进行更多调用,了解错误发生的位置将很有用。


 glBindBuffer(GL_VERTEX_ARRAY, vbo); glCheckError(); 

结论:



还有一件重要的事情: glewInit()存在一个长期存在的错误: glewInit()始终设置GL_INVALID_ENUM标志。 要解决此问题,只需在glGetError之后调用glewInit以清除该标志:


 glewInit(); glGetError(); 

glGetError并没有多大帮助,因为返回的信息相对简单,但是它通常有助于捕获错别字或捕获发生错误的地方。 这是一个简单但有效的调试工具。


调试输出


该工具鲜为人知,但比glCheckError (OpenGL扩展“调试输出”,包含在OpenGL 4.3核心配置文件中)有用。 使用此扩展,OpenGL将向用户发送错误消息,其中包含错误的详细信息。 此扩展不仅提供更多信息,而且还允许您使用调试器捕获错误发生的地方。


从版本4.3开始,调试输出包含在OpenGL中,这意味着您将在支持OpenGL 4.3及更高版本的任何计算机上找到此功能。 如果此版本不可用,则可以检查扩展名ARB_debug_outputAMD_debug_output 。 还存在未经验证的信息,表明OS X不支持调试输出(原件的作者和翻译者尚未进行测试,如果您发现或反对这一事实,请通过错误纠正机制以私密消息的形式将原件的作者或本人告知我; UPD: Jeka178RUS对此进行了检查实际上:开箱即用,调试输出不起作用,他没有检查扩展名)。

要开始使用调试输出,我们需要在初始化过程中请求OpenGL调试上下文。 在不同的窗口系统上,此过程有所不同,但是这里我们仅讨论GLFW,但是在“其他材料”部分的文章结尾,您可以找到有关其他窗口系统的信息。


在GLFW中调试输出


在GLFW中请求调试上下文非常简单:您需要做的就是给GLFW一个提示,即我们想要一个支持调试输出的上下文。 我们需要在调用glfwCreateWindow之前执行此glfwCreateWindow


 glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE); 

初始化GLFW后,如果我们使用OpenGL 4.3或更高版本,则应该有一个调试上下文,否则我们应该试试运气,希望系统仍然可以创建调试上下文。 万一失败,我们需要通过OpenGL扩展机制请求调试输出。


OpenGL调试上下文的速度可能比正常情况慢,因此在进行优化时或发布之前,应删除或注释掉这一行。

要检查初始化调试上下文的结果,只需执行以下代码即可:


 GLint flags; glGetIntegerv(GL_CONTEXT_FLAGS, &flags); if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) { //  } else { //   } 

调试输出如何工作? 我们将回调函数传递给OpenGL中的消息处理程序(类似于GLFW中的回调),并且在此函数中,我们可以根据需要处理OpenGL数据,在这种情况下,将有用的错误消息发送到控制台。 该函数的原型:


 void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, void *userParam); 

请注意,在某些操作系统上,最后一个参数的类型可能是const void*
给定我们拥有的庞大数据集,我们可以创建一个有用的错误打印工具,如下所示:


 void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, void *userParam) { // ignore non-significant error/warning codes 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; } 

当扩展程序检测到OpenGL错误时,它将调用此函数,我们可以打印大量错误信息。 请注意,我们忽略了一些错误,因为它们是无用的(例如,NVidia驱动程序中的131185表示成功创建了缓冲区)。
现在我们有了所需的回调,是时候初始化调试输出了:


 if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) { glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); glDebugMessageCallback(glDebugOutput, nullptr); glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE); } 

因此,我们告诉OpenGL我们要启用调试输出。 对glEnable(GL_DEBUG_SYNCRHONOUS)的调用告诉OpenGL我们希望在错误发生时收到错误消息。


调试输出过滤


使用glDebugMessageControl函数glDebugMessageControl您可以选择要接收的错误类型。 在我们的情况下,我们会遇到各种错误。 如果我们只想要OpenGL API错误,例如Error和显着性级别High,我们将编写以下代码:


 glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_ERROR, GL_DEBUG_SEVERITY_HIGH, 0, nullptr, GL_TRUE); 

使用此配置和调试上下文,每个不正确的OpenGL命令都会发送很多有用的信息:



通过调用堆栈查找错误源


调试输出的另一个技巧是,您可以相对轻松地在代码中建立错误的确切位置。 通过在DebugOutput函数中为所需的错误类型设置断点(或者在函数的开头,如果要捕获所有错误),调试器将捕获该错误,并且您可以浏览调用堆栈以找出错误发生的位置:



这需要一些手动干预,但是如果您大致了解要查找的内容,则快速确定导致错误的呼叫非常有用。


自己的错误


除了读取错误,我们还可以使用glDebugMessageInsert将它们发送到调试输出系统:


 glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 0, GL_DEBUG_SEVERITY_MEDIUM, -1, "error message here"); 

如果要连接到使用调试上下文的另一个应用程序或OpenGL代码,这将非常有用。 其他开发人员将能够快速找出自定义OpenGL代码中发生的任何报告的错误。
通常,调试输出(如果可用)对于快速发现错误非常有用,并且绝对值得在调优上投入精力,因为它节省了大量的开发时间。 您可以在此处使用glGetError和调试输出找到源代码的副本。 有错误,请尝试解决它们。


着色器调试输出


对于GLSL,我们无法使用glGetError之类的功能,也无法访问调试器中逐步执行代码的功能。 当您遇到黑屏或完全不正确的显示时,如果问题出在着色器上,将很难理解会发生什么。 是的,编译错误报告语法错误,但是捕获语义错误的是那首歌。
找出着色器存在问题的常用方法之一是将着色器程序中的所有相关变量直接发送到片段着色器的输出通道。 通过将着色器变量直接以颜色输出到输出通道,我们可以通过检查输出中的图片来找到有趣的信息。 例如,我们需要找出法线对于该模型是否正确。 我们可以将它们(无论是否经过变换)从顶点发送到片段着色器,从中派生出如下所示的法线:
(请注意:为什么GLSL没有语法高亮显示?)


 #version 330 core out vec4 FragColor; in vec3 Normal; [...] void main() { [...] FragColor.rgb = Normal; FragColor.a = 1.0f; } 

通过将非颜色变量以现在的颜色输出到输出通道,我们可以快速检查该变量的值。 例如,如果结果是黑屏,则很明显法线被错误地转移到了着色器,并且在显示法线时,检查它们的正确性相对容易:



从视觉结果中,我们可以看到法线是正确的,因为西服的右侧主要是红色(这意味着法线在漂洗x轴的方向上大致显示),并且西服的前侧在z轴的正方向(蓝色)上着色。


该方法可以扩展到您要测试的任何变量。 每次卡住并假定错误在于着色器时,请尝试绘制一些变量或中间结果,并找出算法的哪一部分存在错误。


OpenGL GLSL参考编译器


每个视频驱动程序都有自己的怪癖。 例如,NVIDIA驱动程序稍微降低了规范的要求,而AMD驱动程序更好地满足了规范(在我看来,这更好)。 问题在于,由于驱动程序的差异,在一台计算机上运行的着色器可能无法在另一台计算机上赚钱。


凭借多年的经验,您可以了解不同GPU之间的所有差异,但是如果您想确保着色器可以在任何地方使用,则可以使用GLSL参考编译器根据官方规范验证代码。 您可以在此处源代码 )下载所谓的GLSL lang验证程序


使用此程序,可以通过将着色器作为第一个参数传递给程序来测试它们。 请记住,该程序通过扩展名确定着色器的类型:


  • .vert :顶点着色器
  • .frag :片段着色器
  • .geom :几何着色器
  • .tesc :镶嵌控制着色器
  • .tese :镶嵌计算着色器
  • .comp :计算着色器

运行程序很容易:


 glslangValidator shader.vert 

请注意,如果没有错误,程序将不会输出任何内容。 在折断的顶点着色器上,输出如下所示:



该程序不会显示AMD,NVidia或Intel的GLSL编译器之间的差异,甚至无法报告着色器中的所有错误,但至少会检查着色器是否符合标准。


帧缓冲器输出


工具箱的另一种方法是在屏幕的特定部分显示帧缓冲区的内容。 最有可能的是,您经常使用帧缓冲区,并且由于所有魔术都在幕后发生,因此很难确定正在发生的事情。 帧缓冲区内容的输出是验证事情是否正确的有用技巧。


请注意,如此处所述,帧缓冲区的内容适用于纹理,而不适用于图形缓冲区中的对象

使用一个绘制单个纹理的简单着色器,我们可以编写一个小的函数来快速在屏幕的右上角绘制任何纹理:


 // vertex shader #version 330 core layout (location = 0) in vec2 position; layout (location = 1) in vec2 texCoords; out vec2 TexCoords; void main() { gl_Position = vec4(position, 0.0f, 1.0f); TexCoords = texCoords; } 

 //fragment shader #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D fboAttachment; void main() { FragColor = texture(fboAttachment, TexCoords); } 

 //main.cpp void DisplayFramebufferTexture(GLuint textureID) { if(!notInitialized) { // initialize shader and vao w/ NDC vertex coordinates at top-right of the screen [...] } glActiveTexture(GL_TEXTURE0); glUseProgram(shaderDisplayFBOOutput); glBindTexture(GL_TEXTURE_2D, textureID); glBindVertexArray(vaoDebugTexturedRect); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); glUseProgram(0); } int main() { [...] while (!glfwWindowShouldClose(window)) { [...] DisplayFramebufferTexture(fboAttachment0); glfwSwapBuffers(window); } } 

这将在屏幕角落为您提供一个小窗口,用于调试帧缓冲区的输出。 例如,当您尝试检查法线的正确性时,它很有用:



您还可以扩展此功能,以使其渲染多个纹理。 这是从帧缓冲区中的任何内容获取连续反馈的快速方法。


外部调试器程序


当所有其他方法都失败时,还有另外一个技巧:使用第三方程序。 它们内置在OpenGL驱动程序中,可以拦截所有OpenGL调用,从而为您提供有关应用程序的许多有趣数据。 这些应用程序可以剖析OpenGL功能的使用,查找瓶颈,并监视帧缓冲区,纹理和内存。 在处理(大型)代码时,这些工具会变得无价。


我列出了几种流行的工具。 尝试每个,然后选择最适合您的一个。


Renderderoc


RenderDoc是一个很好的(完全开放的 )单独的调试工具。 要开始捕获,请选择可执行文件和工作目录。 您的应用程序照常工作,当您要观看单个框架时,您可以允许RenderDoc捕获应用程序的多个框架。 在捕获的帧中,您可以查看管道的状态,所有OpenGL命令,缓冲区存储和使用的纹理。



法典


CodeXL -GPU调试工具,可作为Visual Studio的独立应用程序和插件。 CodeXL提供了大量信息,非常适合分析图形应用程序。 CodeXL还可以在NVidia和Intel的图形卡上运行,但不提供OpenCL调试支持。



我没有使用CodeXL,因为RenderDoc对我来说似乎更简单,但是我将CodeXL包含在此列表中是因为它看起来像是一种非常可靠的工具,并且主要由GPU的主要制造商之一开发。


NVIDIA Nsight


Nsight是一种流行的NUIDIA GPU调试工具。 它不仅是Visual Studio和Eclipse的插件,还是一个单独的应用程序 。 Nsight插件对于图形开发人员而言非常有用,因为它收集了大量有关GPU使用情况和GPU逐帧状态的实时统计信息。


从使用调试命令或Nsight配置文件通过Visual Studio或Eclipse启动应用程序的那一刻起,它将开始在应用程序内部进行。 Nsight中的一件好事:在正在运行的应用程序之上呈现一个GUI系统(GUI,图形用户界面),您可以使用它来实时或逐帧分析收集有关应用程序的各种信息。



Nsight是一个非常有用的工具,我认为它超越了上述工具,但有一个严重的缺点:它仅适用于NVIDIA图形卡。 如果您使用的是NVIDIA图形卡和Visual Studio,那么Nsight绝对值得一试。


, ( , VOGL APItrace ), , . , , () ( , ).



  • ? — Reto Koradi.
  • — Vallentin Source.

PS:我们有一个电报会议,以协调转账。如果您有强烈的帮助翻译的愿望,欢迎您!

Source: https://habr.com/ru/post/zh-CN462897/


All Articles