Modern OpenGL: Part 4 Error Handling
So hopefully, you have had some initial excitement about having your first triangle on the screen. I’m guessing though that you might have had a typo somewhere or passing the wrong value into a function and something went wrong, and maybe it took way more time than it should have to find the issue. You are not alone.
There are ways to find out if something has gone wrong though.
I’m going to briefly introduce you to the legacy way that OpenGL applications could find out what went wrong (you may have seen this shown in other tutorials as it is a widely propagated method of checking for errors) and then I’m quickly going to move on to show you the modern and better way of doing it. In fact if you go to this page, the following method is listed as ‘the hard way’. Once we have briefly covered it, then we will move on to the easy way.
There exists a function called glGetError which asks the OpenGL runtime if anything went wrong. You get back an error code which you are able to check against some particular values to see what kind of error you have. It is usually wrapped up into some kind of helper function like this one that you can call occasionally after your code to see when these errors occur……
void CheckGLError(std::string str) { GLenum error = glGetError(); if (error != GL_NO_ERROR) { printf("Error! %s %s\n", str.c_str(), openGLErrorString(error)); } }
the openGLErrorString() function might look something like this…..
const char* openGLErrorString(GLenum _errorCode) { if (_errorCode == GL_INVALID_ENUM) { return "GL_INVALID_ENUM"; } else if (_errorCode == GL_INVALID_VALUE) { return "GL_INVALID_VALUE"; } else if (_errorCode == GL_INVALID_OPERATION) { return "GL_INVALID_OPERATION"; } else if (_errorCode == GL_INVALID_FRAMEBUFFER_OPERATION) { return "GL_INVALID_FRAMEBUFFER_OPERATION"; } else if (_errorCode == GL_OUT_OF_MEMORY) { return "GL_OUT_OF_MEMORY"; } else if (_errorCode == GL_NO_ERROR) { return "GL_NO_ERROR"; } else { return "unknown error"; } }
The reason this in not ideal, is that
- You have to call the function manually yourself when you want to find out if there was an error. If you don’t know where the error came from, you may have to copy and paste it all over your code or wrap your gl* function calls into a CHECK() macro to make sure that you know after which call the error came from
- Just because you have an error, doesn’t mean that the place that you get an error back is where the issue is. Because OpenGL is a state machine, with complication rules on what operations are valid and allowed it particular states, it could be that the problem is because you did something like forget to bind some framebuffer, or set the wrong read/write bits on a buffer.
- You just get GL_INVALID_something. No detailed information on what specifically is wrong.
Anyway writing this function is not only the worse way to check for errors, it is also not really necessary because we can simply use our fancy glbindings library to do it for us. If we add a new header and one line of code into our program after the window creation, then all of the above is taken care of for us.
New header…
#include <glbinding-aux/debug.h>
Single line of code to enable basic error checking.
glbinding::aux::enableGetErrorCallback();
Now if you try some thing like commenting out one single line of our program, for example the function that binds the Vertex Array Object that we had to put in our code in the last chapter, then we should get some error message when we run our program.
//glBindVertexArray(vao);
This is the output which is just a stream of errors being printed because glDrawArrays is where the error is occuring.
GL_INVALID_OPERATIONglDrawArrays generated
But glDrawArrays() is not the problem. The problem is because there is no Vertex Array Object bound.
The Modern way
Now we are going to use a better way of debugging. First we have to enable ‘debug output’ by calling glEnable….
… surrounding code (click to expand)#include "error_handling.hpp" #include <array> #include <chrono> // current time #include <cmath> // sin & cos #include <cstdlib> // for std::exit() #include <fmt/core.h> // for fmt::print(). implements c++20 std::format #include <pystring.h> #include <string> #include <unordered_map> // this is really important to make sure that glbindings does not clash with // glfw's opengl includes. otherwise we get ambigous overloads. #define GLFW_INCLUDE_NONE #include <GLFW/glfw3.h> #include <glbinding/gl/gl.h> #include <glbinding/glbinding.h> #include <glbinding-aux/debug.h> using namespace gl; using namespace std::chrono; int main() { auto startTime = system_clock::now(); const auto windowPtr = []() { if (!glfwInit()) { fmt::print("glfw didnt initialize!\n"); std::exit(EXIT_FAILURE); } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); auto windowPtr = glfwCreateWindow(1280, 720, "Chapter 4 - Error Handling", nullptr, nullptr); if (!windowPtr) { fmt::print("window doesn't exist\n"); glfwTerminate(); std::exit(EXIT_FAILURE); } glfwSetWindowPos(windowPtr, 520, 180); glfwMakeContextCurrent(windowPtr); glbinding::initialize(glfwGetProcAddress, false); return windowPtr; }();
glEnable(GL_DEBUG_OUTPUT);
Next we are going to tell OpenGL that we want it to call a function (that we will write) when it detects an error. Isn’t that nice? We do that by setting this so called callback function….
glDebugMessageCallback(errorHandler::MessageCallback, 0); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);… previous code (click to expand)
const char* vertexShaderSource = R"VERTEX( #version 460 core out vec3 colour; const vec4 vertices[] = vec4[]( vec4(-0.5f, -0.7f, 0.0, 1.0), vec4( 0.5f, -0.7f, 0.0, 1.0), vec4( 0.0f, 0.6888f, 0.0, 1.0)); const vec3 colours[] = vec3[]( vec3( 1.0, 0.0, 0.0), vec3( 0.0, 1.0, 0.0), vec3( 0.0, 0.0, 1.0)); void main(){ colour = colours[gl_VertexID]; gl_Position = vertices[gl_VertexID]; } )VERTEX"; const char* fragmentShaderSource = R"FRAGMENT( #version 460 core in vec3 colour; out vec4 finalColor; void main() { finalColor = vec4(colour.x, colour.y, colour.z, 1.0); } )FRAGMENT"; auto vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr); glCompileShader(vertexShader); auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr); glCompileShader(fragmentShader); auto program = glCreateProgram(); glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); glLinkProgram(program); glUseProgram(program); // in core profile, at least 1 vao is needed GLuint vao; glCreateVertexArrays(1, &vao); glBindVertexArray(vao); std::array<GLfloat, 4> clearColour; while (!glfwWindowShouldClose(windowPtr)) { auto currentTime = duration<float>(system_clock::now() - startTime).count(); clearColour = {std::sin(currentTime) * 0.5f + 0.5f, std::cos(currentTime) * 0.5f + 0.5f, 0.2f, 1.0f}; glClearBufferfv(GL_COLOR, 0, clearColour.data()); glDrawArrays(GL_TRIANGLES, 0, 3); glfwSwapBuffers(windowPtr); glfwPollEvents(); } glfwTerminate(); }
We
This errorHandler::MessageCallback function doesn’t exist yet, so we will write that next. It has to adhere to a specific signature so that when opengl calls it, it can pass the right arguments. Enabling GL_DEBUG_OUTPUT_SYNCHRONOUS is also useful as it will allow us to use the debugger in our c++ program if we need to to ensure that we get the right output when we step through our program (because OpenGL can be asynchronous in its operation, debug messages could appear later than we want them too).
Lets create a new file error_handler.hpp. We won’t be using too many extra files in this series, but to keep the main program cpp a bit leaner, I’ve opted for these header only files for ‘utility’ type functionality only. It does increase compile time a little, but its the order of seconds, not minutes, and we are here to learn so who cares.
#pragma once #include <fmt/color.h> #include <fmt/core.h> // for fmt::print(). implements c++20 std::format #include <glbinding/gl/gl.h> #include <iostream> #include <string> #include <unordered_map> using namespace gl; namespace errorHandler { void MessageCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) { std::string src = errorSourceMap.at(source); std::string tp = errorTypeMap.at(type); std::string sv = severityMap.at(severity); fmt::print( stderr, "GL CALLBACK: {0:s} type = {1:s}, severity = {2:s}, message = {3:s}\n", src, tp, sv, message); } } // namespace errorHandler… previous code (click to expand)
#pragma once #include <fmt/color.h> #include <fmt/core.h> // for fmt::print(). implements c++20 std::format #include <glbinding/gl/gl.h> #include <iostream> #include <string> #include <unordered_map> using namespace gl; namespace errorHandler {
static const std::unordered_map<GLenum, std::string> errorSourceMap{ {GL_DEBUG_SOURCE_API, "SOURCE_API"}, {GL_DEBUG_SOURCE_WINDOW_SYSTEM, "WINDOW_SYSTEM"}, {GL_DEBUG_SOURCE_SHADER_COMPILER, "SHADER_COMPILER"}, {GL_DEBUG_SOURCE_THIRD_PARTY, "THIRD_PARTY"}, {GL_DEBUG_SOURCE_APPLICATION, "APPLICATION"}, {GL_DEBUG_SOURCE_OTHER, "OTHER"}}; static const std::unordered_map<GLenum, std::string> errorTypeMap{ {GL_DEBUG_TYPE_ERROR, "ERROR"}, {GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR, "DEPRECATED_BEHAVIOR"}, {GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR, "UNDEFINED_BEHAVIOR"}, {GL_DEBUG_TYPE_PORTABILITY, "PORTABILITY"}, {GL_DEBUG_TYPE_PERFORMANCE, "PERFORMANCE"}, {GL_DEBUG_TYPE_OTHER, "OTHER"}, {GL_DEBUG_TYPE_MARKER, "MARKER"}}; static const std::unordered_map<GLenum, std::string> severityMap{ {GL_DEBUG_SEVERITY_HIGH, "HIGH"}, {GL_DEBUG_SEVERITY_MEDIUM, "MEDIUM"}, {GL_DEBUG_SEVERITY_LOW, "LOW"}, {GL_DEBUG_SEVERITY_NOTIFICATION, "NOTIFICATION"}};… previous code (click to expand)
void MessageCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) { std::string src = errorSourceMap.at(source); std::string tp = errorTypeMap.at(type); std::string sv = severityMap.at(severity); fmt::print( stderr, "GL CALLBACK: {0:s} type = {1:s}, severity = {2:s}, message = {3:s}\n", src, tp, sv, message); } } // namespace errorHandler
GL CALLBACK: SOURCE_API type = ERROR, severity = HIGH, message = GL_INVALID_OPERATION error generated. Array object is not active.
Amazing! We can see that we are getting a message telling us that the Array object is not active. Awesome. Lets re-enable it then!
“GL CALLBACK: SOURCE_API type = PERFORMANCE, severity = MEDIUM, message = Program/shader state performance warning: Fragment shader in program 3 is being recompiled based on GL state.”
Apparently from my internet searching, this is okay so if you see it, feel free to ingore it…
So this is all very good,
Checking for Shader Compiler Errors
This is a nice state to be in, but there is still one thing that the debug output cannot tell us, and that is what errors are occurring in our shaders. If we get something wrong in our shader (for example if we accidently set the out colour attribute in the vertex shader to a vec4 and not the vec3 it should be), we will get these messages printed…
GL CALLBACK: SOURCE_API type = ERROR, severity = HIGH, message = GL_INVALID_OPERATION error generated. <program> has not been linked, or is not a program object. GL CALLBACK: SOURCE_API type = ERROR, severity = HIGH, message = GL_INVALID_OPERATION error generated. <program> object is not successfully linked, or is not a program object.
But we don’t get told what was wrong. That is where we have to step in to do a little bit of extra work.
OpenGL has some functions that allow us to query whether the shader compilation was successful and if not, what messages the compiler spat out. Let write a function that wraps up that functionality so we can check the result of the shader compilation after we call glCompileShader().
bool checkShader(GLuint shaderIn, std::string shaderName) { GLboolean fShaderCompiled = GL_FALSE;
Here we write the signature of out helper function, which takes a shader ID (the id that was returned to us when we called glCreateShader), and also some ‘user’ text that we can pass in to print to the console which will help indication which shader we are printing information for. We also set up a variable ‘fShaderCompiled’ that we will want OpenGL to set for us next….
glGetShaderiv(shaderIn, GL_COMPILE_STATUS, &fShaderCompiled); if (fShaderCompiled != GL_TRUE) {
Then we use the OpenGL function glGetShaderiv to get some information about the shader that we are interested in. We pass in the shader ID inShader, and the piece of information we are after which is the GL_COMPILE_STATUS. We tell it to set the value of the variable we prepared just before based on wether the compilation was successful or not. If was wasn’t, then the if statement will enter it’s block…
fmt::print(stderr, "Unable to compile {0} shader {1}\n", shaderName, shaderIn);
First we print a message to the log telling the user of the program that something was wrong.
GLint log_length; glGetShaderiv(shaderIn, GL_INFO_LOG_LENGTH, &log_length);
Then we will use the same glGetShaderiv function to return to us the length of the compiler log message.
std::vector<char> v(log_length); glGetShaderInfoLog(shaderIn, log_length, nullptr, v.data());
Then based on that log length, we will create a vector of chars big enough for OpenGL to store the message in. Using the glGetShaderInfoLog() function, OpenGL will store the compiler message in that vector for us.
fmt::print(stderr, fmt::fg(fmt::color::light_green), "{}\n", v.data()); return false; } return true; }
Then we simply print that message. Then we return false to let any caller detect wether the compiler failed and to do something about it if they so wish. If there wasn’t any error, we return true;
Now if we use this function right after compiling the shaders, we get a message containing the line number in the shader of where the error occurred and what the error was.
auto vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr); glCompileShader(vertexShader); errorHandler::checkShader(vertexShader, "Vertex"); auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr); glCompileShader(fragmentShader); errorHandler::checkShader(fragmentShader, "Fragment");
That wraps things up for this chapter. Next we will be getting back to some exciting topics and seeing how we can provide vertex data from our c++ program to feed OpenGL to draw multiple triangles at locations that we set.