Overview
In every lab so far, OpenGL errors have gone completely unreported. The OpenGL API does not throw exceptions or write to the console when something goes wrong. It stores error codes in an internal queue. If you never call glGetError, a misconfigured buffer or invalid enum will cause corrupted rendering with no indication of where the problem is.
This lab creates the error-checking foundation that every subsequent class will build on.
You will write the ASSERT and GlCall macros, implement the glClearError and glLogCall utility functions, and begin src/Renderer.h -
the shared header that every buffer and resource class in the project will include.
Learning Objectives
By the end of this lab you will be able to:
- Explain why OpenGL stores errors in a queue and why that queue must be drained before each call
- Write the GlCall macro that wraps any OpenGL call with a clear-before and check-after sequence
- Use __debugbreak to pause the debugger at the exact source line where an OpenGL error occurred
- Implement glLogCall to print the GL error code, function name, file path, and line number
- Identify which header every subsequent buffer class will include to access GlCall
Concept
OpenGL is a state machine that communicates errors through a queue of error codes. When a function receives an invalid argument or is called in the wrong state, it pushes a GL error code onto that queue and returns without doing anything. The queue can hold multiple errors at once. Because OpenGL never crashes and never prints to the console on its own, these errors are completely invisible unless your code explicitly calls glGetError to inspect them. In practice this means a typo in a buffer size or an invalid texture format can silently corrupt every frame with no hint of where the problem is.
The standard solution is to wrap every OpenGL call in a two-step pattern: drain the error queue before the call so it is empty, then check the queue after the call to see whether that specific call produced any error. The GlCall macro encodes this pattern. It expands to three lines - a call to glClearError, the original OpenGL call unchanged, and an ASSERT that invokes glLogCall. The preprocessor operators #x, __FILE__, and __LINE__ capture the call site as strings at compile time, so glLogCall can print exactly which function failed, in which file, on which line. The ASSERT macro calls __debugbreak() - a compiler intrinsic that pauses the running process and hands control to the attached debugger, freezing execution at the exact instruction that triggered the failure.
glClearError drains the queue by calling glGetError in a loop until it returns GL_NO_ERROR. glLogCall reads the queue with the same loop and prints each error code alongside the function name, file path, and line number passed in from the macro, then returns false to trigger the ASSERT. Both functions live in src/Renderer.cpp, and their declarations live in src/Renderer.h alongside the macros. Every other class in the project will include Renderer.h in its .cpp file so GlCall is available everywhere without repeating any boilerplate. This header also contains a stub Renderer class with Clear() methods. The Draw() methods that reference VertexArray, IndexBuffer, and Shader are added in Lab 14 once those classes exist.
Tasks
Task 1 - Create the Error-Checking Header
Create src/Renderer.h. This header is the foundation - every subsequent class includes it for access to GlCall.
- Create a new file
src/Renderer.hand add a#pragma onceguard. Include<GL/glew.h>(needed for theGLenumtype used byglGetError),<iostream>, and<string>. - Write the
ASSERTmacro. It takes a conditionxand calls__debugbreak()if that condition is false:#define ASSERT(x) if (!(x)) __debugbreak(); - Write the
GlCallmacro. It wraps a single OpenGL callxand expands to three lines. The backslash at the end of each line is a line-continuation character:
The#define GlCall(x) glClearError(); x; ASSERT(glLogCall(#x, __FILE__, __LINE__))#xtoken stringifies the call so its name can be printed byglLogCall. - Declare the two utility functions that the macro calls:
void glClearError(); bool glLogCall(const char* function, const char* file, int line); - Add the
Rendererclass stub. It exposesClear()methods now. TheDraw()methods that reference buffer classes will be added in Lab 14. - The reference block below shows the complete
Renderer.hat this stage.
#pragma once
#include <GL/glew.h>
#include <iostream>
#include <string>
// Halt execution if condition is false; use this to catch impossible states
#define ASSERT(x) if (!(x)) __debugbreak();
// Wrap any OpenGL call: clear the error queue before, check it after.
// If an error is found, prints the function name, file, and line, then breaks.
#define GlCall(x) glClearError();\
x;\
ASSERT(glLogCall(#x, __FILE__, __LINE__))
// Drain the OpenGL error queue so it is empty before the next call
void glClearError();
// Check the error queue after a call; print details and return false if any error found
bool glLogCall(const char* function, const char* file, int line);
// Renderer stub - Clear() is available now. Draw() methods are added in Lab 14
// once VertexArray, IndexBuffer, and Shader classes exist.
class Renderer {
public:
void Clear() const;
void Clear(std::string colour) const;
};
Task 2 - Implement glClearError and glLogCall
Create src/Renderer.cpp and implement the two error-checking utilities plus the Clear methods.
- Create
src/Renderer.cppand include"Renderer.h"and<iostream>. - Implement
glClearError. CallglGetErrorin a loop until it returnsGL_NO_ERROR:void glClearError() { while (glGetError() != GL_NO_ERROR); } - Implement
glLogCall. CallglGetErrorin a loop. For each non-zero code, print the error value, function name, file, and line, then returnfalsesoASSERTfires:bool glLogCall(const char* function, const char* file, int line) { while (GLenum error = glGetError()) { std::cout << "[OpenGL Error]( " << error << " ) " << function << " File: " << file << " Line:" << line << std::endl; return false; } return true; } - Implement
Renderer::Clear()to clear both the colour and depth buffers:void Renderer::Clear() const { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } - Add
src/Renderer.cppto your Visual Studio project if it is not already listed. - Build the project. No new visual output is expected.
To confirm
GlCallworks, temporarily add a bad call inside your render loop (for exampleGlCall(glBindBuffer(GL_ARRAY_BUFFER, 999999))), build and run, and verify the debugger breaks with an error in the console. Remove the test call afterwards.
#include "Renderer.h"
#include <iostream>
#include <algorithm>
// Drain every pending error from the OpenGL error queue
void glClearError()
{
while (glGetError() != GL_NO_ERROR);
}
// Check the error queue after a call; print function/file/line per error and return false
bool glLogCall(const char* function, const char* file, int line)
{
while (GLenum error = glGetError())
{
std::cout << "[OpenGL Error]( " << error << " ) "
<< function << " File: " << file << " Line:" << line << std::endl;
return false;
}
return true;
}
void Renderer::Clear() const
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void Renderer::Clear(std::string colour) const
{
std::transform(colour.begin(), colour.end(), colour.begin(), ::towlower);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if (colour == "black") GlCall(glClearColor(0, 0, 0, 1));
if (colour == "white") GlCall(glClearColor(1, 1, 1, 1));
if (colour == "red") GlCall(glClearColor(1, 0, 0, 1));
if (colour == "green") GlCall(glClearColor(0, 1, 0, 1));
if (colour == "blue") GlCall(glClearColor(0, 0, 1, 1));
if (colour == "yellow") GlCall(glClearColor(1, 1, 0, 1));
if (colour == "cyan") GlCall(glClearColor(0, 1, 1, 1));
if (colour == "magenta") GlCall(glClearColor(1, 0, 1, 1));
}
Solution
Try the tasks above before revealing the solution.