Debugging OpenGL ES applications can be challenging for many reasons. One of the reasons is that the error reporting in the API is not great. It is not always clear when an error is being raised and what the reason for it is. This is a limitation of the API since, in most cases, the driver will know exactly what the problem is, but there is no standard mechanism for giving this information back to the application.
Khronos has developed an extension called GL_KHR_debug that provides a mechanism for the graphics driver to notify applications about interesting events. This includes, but is not limited to, API errors. The extension is available for both desktop OpenGL and OpenGL ES. It is already widely adopted for OpenGL, and we expect it will be for OpenGL ES too.
We recently added support for this extension to the OpenGL ES drivers for the Midgard architecture GPUs. In this post I will write about how this extension can be used to improve error reporting and make it easier to debug OpenGL ES applications.
OpenGL ES maintains considerable state, and the current error is part of this state. This has two consequences: errors can occur due to combinations of state, and only one error can be recorded.
If errors could only occur due to invalid parameters in an API call, their cause would be relatively obvious. But since errors may be generated by combinations of state, combining two legal sequences of commands may result in an error whose cause is often less obvious.
For example:
// This does not generate any errors. glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL); GLenum error = glGetError(); // NO_ERROR // This does not generate any errors either. GLuint fbo; glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); GLenum error = glGetError(); // GL_NO_ERROR // But repeating the first sequence now, does generate an error. glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL); GLenum error = glGetError(); // GL_INVALID_FRAMEBUFFER_OPERATION
The OpenGL ES state only stores a single error, specified to be the first error that is generated. Subsequent errors are detected, but have no effect on the error state and are not reported. No information is available about when the error was generated. Combined, these limitations make it difficult to determine the cause of an error, and thus it takes longer to find and fix problems.
To make matters worse, the errors returned by the OpenGL ES API are very broad and they do not always point to an obvious cause. In OpenGL ES 3.0, the available error codes are:
Each API call can potentially trigger one or more of these errors. It is also possible that a single API call triggers the same error for multiple reasons.
The most extreme example of this is the GL_OUT_OF_MEMORY error, which can be generated by any API call, and, while its name implies an out of memory condition, it is sometimes used by drivers to report other error conditions, such as GPU timeouts.
The OpenGL ES specification does not document which errors can be caused by which API call. Instead, it leaves most errors implicit for all API calls, and only documents exceptions and additions to these implicit errors. This makes it quite difficult to determine why an error was set by a particular API call.
The OpenGL ES 3.0 main pages do document errors per API call, but these pages are not a definitive reference. The recent restructuring of the OpenGL 4.3 specification has made this a lot clearer, but this restructuring has not yet been applied to the OpenGL ES specifications.
Partial solutions to the problem described above generally boil down to checking for errors every so often - at least in debug builds. This could be done as rarely as once per frame, as often as per API call, or somewhere in between.
As an example, error checking could be used as pre- and post-conditions for a function:
#ifdef _DEBUG #define CHECK_GL_ERROR() check_gl_error_and_print() #else #define CHECK_GL_ERROR() #endif void check_gl_error_and_print(void) { GLenum error = glGetError(); if (error != GL_NO_ERROR) { printf("GL error detected: 0x%04x\n", error); } } void load_texture(GLsizei width, GLsizei height, GLvoid *data) { CHECK_GL_ERROR(); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); CHECK_GL_ERROR(); }
Another alternative is to do error checking for every API call by wrapping them in a macro:
#ifdef _DEBUG #define CALL_GL(a) a; check_gl_error_and_print() #else #define CALL_GL(a) a; #endif CALL_GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, image->width, image->height, 0, GL_RGB, GL_UNSIGNED_BYTE, image->data)); CALL_GL(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); CALL_GL(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); CALL_GL(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)); CALL_GL(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));
Information about which line and file generated the error can be easily added to these macros. Tools for graphics debugging may also have options to insert these checks automatically.
But neither of these approaches is perfect.
For one thing, there is a trade-off between checking errors rarely or often. If errors are checked rarely, more work is required to track down the cause and location for it. If errors are checked often, the code may become less readable as debug code is sprinkled everywhere. More importantly, these approaches cannot solve the underlying problem of lack of detailed information. At best, they can provide information about when an error was generated, but they cannot provide any additional information about why it was generated. For that we need to extend the error handling capabilities of the API, which is exactly what the GL_KHR_debug extension aims to do.
The GL_KHR_debug extension addresses the problems above by allowing all detected errors to be reported with a verbose error message. By default, the debug notifications are stored in a log inside the driver that applications can query. But applications can also register a callback function that the driver will call whenever it has a new notification. This is the easiest way to use this extension, so we will focus on that for now.
Consider the following example:
glGetIntegerv(GL_CURRENT_PROGRAM, &prog); pos = glGetAttribLocation(prog, "position"); glEnableVertexAttribArray(pos); glVertexAttribPointer(pos, 3, GL_FLOAT, GL_FALSE, 0, triangle_corners); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, triangle_indices); error = glGetError(); // GL_INVALID_OPERATION From this listing, it is not clear why and when this GL_INVALID_OPERATION error was generated. WithGL_KHR_debug, the driver gives us the following information: Error:glGetAttribLocation::<program> could not be made part of current state. <program> is not linked Error:glEnableVertexAttribArray::<index> is greater than or equal to GL_MAX_VERTEX_ATTRIBS Error:glVertexAttribPointer::<index> is greater than or equal to GL_MAX_VERTEX_ATTRIBS
Much better!
So how do we enable this functionality? As we'll see, it's rather easy. It just requires a few changes to our EGL and OpenGL ES initialization code.
Debug information is only guaranteed to be available in so called debug contexts, so we need to create one of those. We can do that using the EGL_KHR_create_context extension:
EGLint ctx_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL_NONE}; EGLContext *ctx = eglCreateContext(dpy, config, EGL_NO_CONTEXT, ctx_attribs);
The only change to the normal context creation is the addition of the EGL_CONTEXT_FLAGS_KHRattribute and its associated parameter.
Next, we need to define a callback function for OpenGL ES to call:
static void on_gl_error(enum source, enum type, uint id, enum severity, sizei length, const char* message, void *userParam) { printf("%s\n", message); }
We need to be a little bit careful about what we do in a callback function since it is called from within the OpenGL ES driver. In particular, we cannot call any OpenGL ES functions as this could cause significant problems – including application crashes.
Finally, we need to tell OpenGL ES about our callback function:
static void enable_debug_callbacks(void)
{ glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_KHR, GL_TRUE); glDebugMessageCallbackKHR(on_gl_error, NULL); }1
That's it. The driver will now call our callback function whenever it generates a debug message. The driver is required to generate a debug message for every API error it detects. We no longer have to check for errors anywhere else in our code! Also, by setting a breakpoint in the callback function, it is easy to find out where the incorrect command was called from.
Note: Of course, as with any OpenGL ES extension, you should only do this after having checked that the extension is supported and having retrieved the extension function pointers via eglGetProcAddress. One thing to be aware of specifically for the GL_KHR_debug extension is a late change in the OpenGL ES version of the specification that added “KHR” suffixes to all new function names. For example, “glDebugMessageCallback” got changed to “glDebugMessageCallbackKHR”. Since this was a very late change, some implementations may still use the non-suffixed version. To avoid portability issues, applications should query for both the suffixed and non-suffixed function pointers to determine which one is available on any given implementation.
You may wonder about the additional call to glEnable in the last example. This is required because some OpenGL ES implementations may be internally multi-threaded, and thus may detect and report multiple errors at once. By enabling synchronous output, we guarantee that the callback function will only be called from one thread at the time. Moreover, we also guarantee that the callback will be called before the API function returns. If we did not enable this behavior, we would have to ensure that our callback function was thread-safe.
In this post, I have concentrated on improved error messages, but that is not the only feature enabled by the GL_KHR_debug extension.
Notably, it also enables the driver to generate debug warnings related to performance, deprecated and undefined behavior, and portability. These warnings are reported by the same mechanism as the error messages. No further application work is required to enable them.
All implementations of this extension are required to generate debug messages when errors occur, but there are no specific requirements for warnings. It is entirely implementation-defined when warnings are generated. Currently, the Midgard OpenGL ES driver does not report any warnings, but we expect to add these in the future.
More advanced features of the extension include filtering messages, retrieving messages manually, and labeling objects.
I hope this write-up has piqued your interest in this extension and that it can make your application development efforts just a tiny bit easier. Like it? Please tell us.
>glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_KHR, GL_TRUE);
The function glEnable does not take 2 arguments.
But, glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_KHR); produced GL_INVALID_OPERATION.