University of Wales Trinity Saint David
ACCA6021 - Graphics and Games Programming

Lab 09 - GlCall Error System

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:

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.

  1. Create a new file src/Renderer.h and add a #pragma once guard. Include <GL/glew.h> (needed for the GLenum type used by glGetError), <iostream>, and <string>.
  2. Write the ASSERT macro. It takes a condition x and calls __debugbreak() if that condition is false:
    #define ASSERT(x) if (!(x)) __debugbreak();
  3. Write the GlCall macro. It wraps a single OpenGL call x and expands to three lines. The backslash at the end of each line is a line-continuation character:
    #define GlCall(x) glClearError();    x;    ASSERT(glLogCall(#x, __FILE__, __LINE__))
    The #x token stringifies the call so its name can be printed by glLogCall.
  4. Declare the two utility functions that the macro calls:
    void glClearError();
    bool glLogCall(const char* function, const char* file, int line);
  5. Add the Renderer class stub. It exposes Clear() methods now. The Draw() methods that reference buffer classes will be added in Lab 14.
  6. The reference block below shows the complete Renderer.h at this stage.
src/Renderer.h
#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.

  1. Create src/Renderer.cpp and include "Renderer.h" and <iostream>.
  2. Implement glClearError. Call glGetError in a loop until it returns GL_NO_ERROR:
    void glClearError()
    {
        while (glGetError() != GL_NO_ERROR);
    }
  3. Implement glLogCall. Call glGetError in a loop. For each non-zero code, print the error value, function name, file, and line, then return false so ASSERT fires:
    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;
    }
  4. Implement Renderer::Clear() to clear both the colour and depth buffers:
    void Renderer::Clear() const
    {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    }
  5. Add src/Renderer.cpp to your Visual Studio project if it is not already listed.
  6. Build the project. No new visual output is expected. To confirm GlCall works, temporarily add a bad call inside your render loop (for example GlCall(glBindBuffer(GL_ARRAY_BUFFER, 999999))), build and run, and verify the debugger breaks with an error in the console. Remove the test call afterwards.
src/Renderer.cpp
#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.