来源请注明:http://www.cnblogs.com/vertexshader/articles/3563883.html
一、导言
在OpenGL应用程序开发者心中,最需要的一个功能莫过于是帮助应用程序开发者调试程序的功能模块。过去OpenGL只简单地提供了glGetError这个命令来简单地获取OpenGL产生的错误信息;而glGetError所提供的调试消息及其有限,只是简单地返回了一个GLenum枚举类型,开发者并不知道哪个GL命令出现了错误,而且必须对照着参考手册去判断出现错误的原因。OpenGL 4.3则提供了GL_ARB_debug_output扩展,引入了新的反馈机制来提供调试输出功能,帮助应用程序开发者更加容易地获取OpenGL调试消息,进而方便地去调试OpenGL程序,提升OpenGL程序的性能。
Debug Output | |
列入核心的版本 | 4.3 |
核心ARB扩展 | |
ARB扩展 | |
供应商扩展 |
表格1: 调试输出功能的核心版本和相关扩展。
二、描述
扩展GL_ARB_debug_output最初是由AMD引入的,目的是为了提供一个新的反馈机制,来帮助应用程序开发者获取更多的调试消息,称之为调试输出(Debug Output)。这个扩展允许应用程序开发者提供一个可以把调试消息(Debug Message)返回到应用程序的调试回调函数(Debug Callback Function),来避免在检查错误期间,在每个GL命令后面添加glGetError命令代码的繁琐过程——在GL命令执行的时候会生成调试消息,在这个之后GL会自己调用应用程序开发者所定义的回调函数。不过该功能也并非一定要定义一个调试回调函数,在没有定义调试回调函数的时候,GL会将消息存储在内部默认的调试消息日志(Debug Message Log)中,应用程序可以查询这个调试消息日志来获取调试消息。该扩展所提供的功能非常的详细,例如错误的使用API、性能警告、未定义的行为、着色器编译错误和链接错误,或者其他有用的信息。调试消息包含着消息源(Source)、类型(Type)和严重性级别(Severity)这些信息,调试输出控制(Debug Output Control)可以把外部的应用程序的事件生成的消息添加到调试消息流中;调试输出控制也可以在应用程序不需要调试消息的时候关闭调试消息。
1. 调试环境
不同级别的调试消息的产生,取决于渲染环境(Render Context)的创建。如果当前的渲染环境不是调试环境(Debug Context),也就是在创建环境时GL_CONTEXT_FLAGS没有设置GL_CONTEXT_FLAG_DEBUG_BIT,那么GL不会产生任何的调试消息,不过使用调试消息所提供的命令不会产生任何的错误。应用程序可以通过参数为符号常量的GL_DEBUG_OUTPUT的glEnable和glDisable命令来开启或者关闭调试输出的功能。如果当前的渲染环境是调试环境(也就是在创建环境时GL_CONTEXT_FLAGS设置了GL_CONTEXT_FLAG_DEBUG_BIT),那么GL_DEBUG_OUTPUT的初始值是GL_TRUE;反之,其初始值为GL_FALSE。调试环境是使用窗口系统相关的API,比如WGL_ARB_create_context或者GLX_ARB_created_context中的wglCreateContextARB或者glxCreateContextARB,在创建渲染环境时指定的:
GLuint attrib[] = { #ifdef WIN32 WGL_CONTEXT_MAJOR_VERSION_ARB, 4, WGL_CONTEXT_MINOR_VERSION_ARB, 4, #ifdef DEBUG WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_DEBUG_BIT_ARB, #endif // DEBUG #endif // WIN32 #ifdef __linux__ GLX_CONTEXT_MAJOR_VERSION_ARB, 4, GLX_CONTEXT_MINOR_VERSION_ARB, 4, #ifdef DEBUG GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_DEBUG_BIT_ARB, #endif // DEBUG #endif // __linux__ 0 }
在调试环境中,当GL_DEBUG_OUTPUT关闭时,GL则不会产生任何的调试消息到调试回调函数或者调试消息日志中;当GL_DEBUG_OUTPUT开启时则会启用完全的调试输出功能。在非调试环境中,如果GL_DEBUG_OUTPUT被关闭,那么调试输出日志的级别则是取决于GL的实现,在一些情况下可能没有任何的调试输出。完全的调试输出只在调试环境中被完整地支持。
2. 调试消息
一个调试会被其生成源、和源有关的类型,以及一个和这个类型有关的无符号整数ID独一无二地标记。表格2展示了消息生成源的类型,而表格3则展示了消息的类型。每个信息源与其类型的对,有其消息自己的命名空间,在其中每个消息都会与一个ID相关联,而这个命名空间下消息的ID的赋值则是依赖于实现的。在不同的源于类型对所拥有的命名空间中,ID的值可能会重复。所以想要唯一地识别一个消息,要通过源、类型和ID的组合来完成。
每个消息也分配了一个严重性级别(Severity Level),其大致描述了消息的重要程度。严重性级别的相关符号常量展示在表格4中。应用程序可以选择关闭不同等级的严重性,来达到控制调试输出量的目的。每条消息中有一条以null结尾的用于描述消息的字符串,而消息的内容也是依赖于实现。
每个消息字符串的长度,包括null结尾符,必须小于或者等于依赖于实现的常量GL_MAX_DEBUG_MESSAGE_LENGTH的值。每个消息都可以被关闭或者启用,所有的消息缺省是开启的并且其严重性级别是GL_DEBUG_SEVERITY_LOW。而消息的开启或者关闭状态则可以通过glDebugMessageControl来改变。
调试消息源 | 生成消息的对象 |
GL_DEBUG_SOURCE_API | GL |
GL_DEBUG_SOURCE_SHADER_COMPILER | GLSL编译器或者其他着色语言的编译器 |
GL_DEBUG_SOURCE_WINDOW_SYSTEM | 窗口系统,比如WGL或者GLX |
GL_DEBUG_SOURCE_THIRD_PARTY | 外部调试器或者第三方中间库 |
GL_DEBUG_SOURCE_APPLICATION | 应用程序 |
GL_DEBUG_SOURCE_OTHER | 与前面列出都不相符的源 |
表格2: 调试消息源和其生成对象,每个消息都必须是由其中的一个源产生的。
调试输出消息类型 | 产生消息的情况 |
GL_DEBUG_TYPE_ERROR | 生成一个错误的事件 |
GL_DEBUG_TYPE_PEPRECATED_BEHAVIOR | 被标记为弃用的行为 |
GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR | 在规范中未定义的行为 |
GL_DEBUG_TYPE_PERFORMANCE | 依赖于实现的性能警告 |
GL_DEBUG_TYPE_PORTABILITY | 使用特定供应商提供的扩展或者着色器 |
GL_DEBUG_TYPE_MARKER | 命令流的注解(Annotation) |
GL_DEBUG_TYPE_PUSH_GROUP | 进入一个调试组(Debug Group) |
GL_DEBUG_TYPE_POP_GROUP | 离开一个调试组 |
GL_DEBUG_TYPE_OTHER | 与前面列出都不相符的类型 |
表格3: 调试输出消息的类型和产生这些消息的情况。
严重性级别 | 显示消息的例子 |
GL_DEBUG_SEVERITY_HIGH | 任何GL错误;危险的未定义行为;着色器编译错误和链接错误 |
GL_DEBUG_SEVERITY_MEDIUM | 严重的性能警告;着色器编译链接警告;使用弃用的行为 |
GL_DEBUG_SEVERITY_LOW | 冗余状态改变所产生的性能警告;微不足道的弃用行为 |
GL_DEBUG_SEVERITY_NOTIFICATION | 没有任何错误或者性能警告 |
表格4: 消息的严重性级别和产生这个级别的例子。
3. 调试消息回调
这个扩展也定义了一个回调函数的机制,来获取调试消息而不必访问默认的调试消息日志,应用程序可以通过向glDebugMessageCallback命令提供一个调试回调函数来接收所产生的调试信息,并且通过自定义的回调函数来处理这些消息:
void glDebugMessageCallback(DEBUGPROC callback, void *userParam);
参数<callback>则是回调函数的指针,其形式必须是:
void callback(GLenum source, GLenum type, GLuint id, GLenum serverity, GLsizei length, const GLchar *message, const GLvoid *userParam);
此外也必须声明回调函数的类型为相关平台所定义的DEBUGPROC类型,否则会导致未定义行为。当前的调试环境中只会存在唯一的一个调试回调函数,重复的设置调试回调函数指针会重写之前的回调函数指针;而将<callback>设置为null,则会清楚当前环境中所设定的回调函数指针,并且不再通过回调函数指针输出消息,而是存放在默认的调试消息日志中。在不同的需求下,应用程序开发者可以定义不同的回调函数,变更渲染环境中的回调函数指针,来达到处理不同消息的目的。应用程序也可以通过<userParam>来放入自定义的数据,渲染环境会将<userParam>作为消息回调函数的参数添加进去。
当应用程序定义了一个用于接收调试消息输出的回调函数之后,OpenGL实现会在处于开启状态的消息生成的时候调用这个回调函数,并且将消息的源、类型、ID、严重性级别、消息字符串的长度、消息字符串,以及用户定义的数据分别填入这个消息回调函数的<source>、<type>、<id>、<serverity>、<length>、<message>和<userParam>中。值得注意的是,<message>的内存是属于GL并且是由GL管理的,所以其指针仅限于回调函数内部使用。在回调函数中调用任何GL或者窗口系统的函数,也是未定义的行为,可能会导致应用程序的终止。如果应用程序关闭了GL_DEBUG_OUTPUT,那么GL根本不会调用这个callback函数。对于调试回调函数最简单的实现如下:
void MyProc(GLenum source, GLenum type, GLuint id, GLenum serverity, GLsizei length, const GLchar *message, const GLvoid *userParam) { std::cout << “source: ” << ValueToString(source) << “; ” << “type: ” << ValueToString(type) << “; ” << “id: ” << id << “; ” << “severity: ” << ValueToString(serverity) << “; ” << “message: ” << message << std::endl; }
4. 调试消息日志
如果GL_DEBUG_CALLBACK_FUNCTION的值是null,那么调试消息则会存储到GL内部的消息日志中去,而最大的消息数量则由GL_MAX_DEBUG_LOGGED_MESSAGES的值定义。而每个渲染环境只会存储自己的调试消息日志,并且只会存储在这个环境中执行命令所产的调试消息。如果消息日志已满,那么之后生成的消息都会被丢弃,直到消息日志被清除。应用程序可以通过查询GL_DEBUG_LOGGED_MESSAGES来获取当前存储在消息日志中的消息数量。可以通过查询GL_DEBUG_NEXT_LOGGED_MESSAGE_LENGTH得到最早的消息的长度,并且可以通过glGetDebugMessageLog来获取日志中的消息。如果GL_DEBUG_CALLBACK_FUNCTION的值不是null,那么任何生成的消息都不会存储在消息日志中,而是通过调试回调函数来处理。如果GL_DEBUG_OUTPUT被关闭了,那么任何消息都不会添加到消息日志中。
5. 控制调试消息
应用程序可以通过glDebugMessageControl在激活的调试组中控制调试输出量:
void glDebugMessageControl(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint *ids, GLboolean enabled);
参数<enabled>是消息的开启状态,如果<enable>的值是GL_TRUE,那么所引用的消息则会被启用;如果<enable>的值是GL_FALSE,那么所引用的消息则会被关闭。这个命令也可以处理全部的消息集,并且通过控制的开启状态达到消息过滤的目的:
- 如果<source>、<type>或者<severity>是GL_DONT_CARE,那么所有的源、所有的类型或者所有的严重性级别都将被引用;
- 如果<source>、<type>或者<severity>不是GL_DONT_CARE,那么和值匹配的源、类型或者严重性级别将被引用;
- 如果<count>大于0,那么<ids>是有<count>个特定的<source>和<type>组合的消息的数组。在这种情况下,<source>或者<type>不可以是GL_DONT_CARE,但是<serverity>必须是GL_DONT_CARE;<id>中不能识别的消息ID将被忽略。如果<count>是0,那么<ids>的值则会被忽略。
虽然消息通过其源和类型被分组到一个隐式的层次结构中,但是对于每个源或者类型或者严重性级别没有显式的启用状态;相反的,启用状态则是存储在每个消息之中的。通过命令一次性关闭所有的消息,或者通过<source>、<type>和<id>来关闭所有的消息是没有差异的。如果GL_DEBUG_OUTPUT被关闭,那么每个<source>、<type>或者<severity>所对应的消息也会被关闭。消息过滤的范例如下:
// Disabling events related to deprecated behaviour. glDebugMessageControlGL_DONT_CARE , GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR, GL_DONT_CARE , 0, NULL , GL_FALSE ); // Enabling only two particular combinations of source , type and id. // Note that first we had to disable all events . GLuint id [2] = {1280 , 1282}; glDebugMessageControl(GL_DONT_CARE , GL_DONT_CARE , GL_DONT_CARE , 0, 0, FALSE); glDebugMessageControl(GL_DEBUG_SOURCE_API_, GL_DEBUG_TYPE_ERROR, GL_DONT_CARE , 2, id, GL_TRUE);
6. 外部生成的消息
为了支持应用程序或者第三方库生成他们自己的消息,比如特定渲染系统事件的时间标记信息,可以使用如下函数插入调试消息:
void glDebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLint length, const GLchar *buf);
其用法和之前的几个函数一样,其中字符串<buf>则代表了消息的内容,<length>则是字符串的长度,如果<length>的值是负数,则暗示<buf>是有null结尾符的。
// Insert a debug message to GL. glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 10000 , GL_DEBUG_SEVERITY_HIGH, -1, "Error: My Application Generated an Error!");
7. 调试组
有应用程序插入的或者由GL实现生成的消息都会先写入激活的调试组中。在一个大型的渲染系统中有许多的子模块,在每个模块中对于调试消息的级别要求不尽然相同,应用程序开发者可能需要在某个模块中设定一个级别,然后在这个模块执行完毕之后把级别恢复到过去的状态。相应的OpenGL提供了将调试组入栈,在处理消息之后,通过出栈恢复默认的调试组的功能。调试组可以嵌套在别的调试组内,但是不可以重叠。如果应用程序没有设置入栈的调试组,那么当前所启用的调试组是缺省调试组。
void glPushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar *message);
上面的命令用来将一个调试组入栈,参数的使用方法和其他的类似,<source>和<id>则标识了一个消息,其类型是GL_DEBUG_TYPE_PUSH_GROUP,其严重性级别则是GL_DEBUG_SEVERITY_NOTIFICATION。GL会把该调试组放入栈顶,用于继承控制之前处于栈顶部的调试输出量控制。调试组是有严格的层次结构的,所以任何额外的调试输出量的控制只会应用于当前激活的调试组或者栈顶部激活的调试组中。
void glPopDebugGroup(void);
上面的命令则是将一个调试组出栈,在出栈调试组后GL也会生成一个调试消息,而消息的源和ID则是通过glPopDebugGroup所设置的<source>和<id>,其类型是GL_DEBUG_TYPE_POP_GROUP,该符号常量与GL_DEBUG_TYPE_PUSH_GROUP共享相同的命名空间,严重性等级则是GL_DEBUG_SEVERITY_NOTIFICATION。在调试组出栈之后,调试输出控制则又重新回归到父级调试组中。
三、测试
本文提供了一个范例代码来应用调试输出的功能,该程序创建了调试渲染环境,并且设置了显示所有的调试输出消息,并且通过调试回调函数输出到控制台:
glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); glDebugMessageCallback(&callback, NULL); glDebugMessageControlARB(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, GL_TRUE);
应用程序故意做了两个生成错误的方法,一个是错误地使用了OpenGL的API,一个是编译的GLSL的代码存在着错误:
// Error code 1: Has not create buffer yet. glBindBuffer(GL_ARRAY_BUFFER, 1); // Error code 2: Compiled Shader Failed. std::string code = "#version 330\n" \ "void main(void)\n" \ "{\n" \ "gl_Position = ftransform()\n" \ "}"; const GLchar *ptr = code.c_str(); GLuint shader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(shader, 1, &ptr, 0); glCompileShader(shader); GLuint prog = glCreateProgram(); glAttachShader(prog, shader); glLinkProgram(prog);
本次测试的平台属性是Intel(R) Core(TM) i3-4130 CPU @ 3.40GHz 3.40 GHz,Nvidia GeForce GTX 650(Driver 334.89),DDR3 1600 4G, Microsoft Windows 7 SP1 x64,程序运行了之后产生如下调试消息:
source: DEBUG_SOURCE_API
type: DEBUG_TYPE_ERROR
id: 1282
severity: DEBUG_SEVERITY_HIGH
message:
GL_INVALID_OPERATION error generated. Buffer name does not refer to an buffer object generated by OpenGL.
source: DEBUG_SOURCE_API
type: DEBUG_TYPE_OTHER
id: 131216
severity: DEBUG_SEVERITY_LOW
message:
Program/shader state info: GLSL program 2 failed to link.
source: DEBUG_SOURCE_API
type: DEBUG_TYPE_OTHER
id: 131184
severity: DEBUG_SEVERITY_LOW
message:
Buffer Info:
Total VBO memory usage in the system:
memtype: SYSHEAP, 0 bytes Allocated, numAllocations: 0.
memtype: VID, 0 bytes Allocated, numAllocations: 0.
memtype: DMA_CACHED, 0 bytes Allocated, numAllocations: 0.
memtype: MALLOC, 0 bytes Allocated, numAllocations: 0.
memtype: PAGE_AND_MAPPED, 0 bytes Allocated, numAllocations: 0.
memtype: PAGED, 0 bytes Allocated, numAllocations: 0.
测试结果正确地显示了当前的错误消息,以及更加明显的详细的错误提示,这将大大帮助应用程序开发者直接发现错误的原因,而不必要在每个GL命令后面添加glGetError命令并且查询手册寻找出错的原因。
四、总结
1. 应用总结
GL_ARB_debug_output提供了一个新的机制来获得调试信息,避免了过去繁琐地设置glGetError并且查询手册的过程,在开发和调试OpenGL应用程序上有很大的帮助。在Direct3D11中也有相关的功能实现——ID3DInfoQueue接口,功能上如出一辙,可以在上层渲染系统接口抽象中抽象这部分的功能。应用程序可以通过预编译宏来处理相关的调试输出代码,这样在应用程序发布时能直接去掉调试输出功能。