Debug Logging

Declared in include/metagl/Debug.hpp. meta-gl has two separate debug mechanisms: the compile-time METAGLDEBUG per-call logger and the runtime OpenGL ES 3.2 debug callback API.

Overview

The METAGLDEBUG logger records every GL call with its function name, typed argument values (using generated enum names), and the return value. Records accumulate in a ring buffer and are flushed to stderr every 5 seconds.

When METAGLDEBUG is not defined, all logging macros expand to do {} while(0) — the optimizer removes them entirely.

Enabling METAGLDEBUG

Two ways to enable the logger:

Option A — Uncomment in Debug.hpp

// In include/metagl/Debug.hpp, change:
// #define METAGLDEBUG
// to:
#define METAGLDEBUG

Option B — CMake compiler flag

cmake -S . -B build -DCMAKE_CXX_FLAGS="-DMETAGLDEBUG"
cmake --build build

Warning: METAGLDEBUG has significant runtime overhead — every GL call performs string formatting and buffer management. Use only in debug builds. Never ship with METAGLDEBUG enabled.

Sample Output

With METAGLDEBUG enabled, you will see output like this on stderr:

[metagl debug flush — 5 calls]
  #1  2026-06-13T10:00:01.123  glEnable(Blend)  → void
  #2  2026-06-13T10:00:01.124  glBlendFunc(SrcAlpha, OneMinusSrcAlpha)  → void
  #3  2026-06-13T10:00:01.125  glBindBuffer(Array, 3)  → void
  #4  2026-06-13T10:00:01.126  glBufferData(Array, 72, 0x7ffd12345678, StaticDraw)  → void
  #5  2026-06-13T10:00:01.127  glCreateShader(Vertex)  → 1

Note that enum values are printed as their named enumerator (e.g., SrcAlpha, Array, StaticDraw), not as raw integers. This makes the log human-readable without a GL header lookup.

How It Works

The debug system has three layers:

  1. Value formattermetagl::debug::to_str(T): a template that formats any value. For enum classes it calls metagl::to_string() if available (from EnumNames.hpp), otherwise falls back to the raw integer. Pointers print as hex. Null pointers print as "null".
  2. Argument formattermetagl::debug::fmt_args(args...): a fold-expression that joins all arguments with ", ".
  3. Recordermetagl::debug::record(func, retval, params): stores a CallRecord into a ring buffer. Flushes to stderr when 5 seconds have passed since the last flush or the buffer is full.
// Simplified internal layout of Debug.hpp:

namespace metagl::debug {

template<typename T>
std::string to_str(T val) {
    if constexpr (std::is_enum_v<T>) {
        if constexpr (requires { metagl::to_string(val); }) {
            auto sv = metagl::to_string(val);
            if (sv != "?") return std::string(sv);
        }
        return std::to_string(static_cast<std::underlying_type_t<T>>(val));
    } else if constexpr (std::is_pointer_v<T>) {
        if (!val) return "null";
        // ... hex format
    } else {
        return std::to_string(val);
    }
}

template<typename... Args>
std::string fmt_args(Args&&... args) {
    // fold expression joining all args with ", "
}

void record(std::string_view func, std::string_view retval, std::string params);

} // namespace metagl::debug

// Macros used in Functions.cpp:
#define METAGL_DEBUG_LOG_VOID(name, ...) \
    metagl::debug::record_void(name __VA_OPT__(,) __VA_ARGS__)
#define METAGL_DEBUG_LOG(name, _r, ...) \
    metagl::debug::record_ret(name, _r __VA_OPT__(,) __VA_ARGS__)

Zero-Cost When Disabled

When METAGLDEBUG is not defined:

#else // METAGLDEBUG not defined

#define METAGL_DEBUG_LOG_VOID(name, ...) do {} while(0)
#define METAGL_DEBUG_LOG(name, _r, ...)  do {} while(0)

#endif

The macros become empty statements. At -O2 or higher, the compiler eliminates them completely. No string allocation, no function call, no cycle cost.

EnumNames Integration

The to_str() function uses a C++20 requires expression to detect whether metagl::to_string() is available for a given type:

if constexpr (requires { metagl::to_string(val); }) {
    // to_string() available — use the human-readable name
}

This means that even if you add a new enum class to Enums.hpp but forget to add it to EnumNames.hpp, the debug logger will still work — it just prints the raw integer value instead of the name.

CallRecord Format

Each logged call has:

GL Debug Callbacks (ES 3.2)

meta-gl also exposes the OpenGL ES 3.2 debug callback API through metagl::glDebugMessageCallback(). This is separate from METAGLDEBUG and works at the driver level — the driver calls your callback when it detects an error or performance warning.

// Requires ES 3.2 or the GL_KHR_debug extension.

metagl::glEnable(metagl::Capability::DebugOutput);
metagl::glEnable(metagl::Capability::DebugOutputSynchronous);

metagl::glDebugMessageCallback(
    [](GLenum source, GLenum type, GLuint id, GLenum severity,
       GLsizei length, const GLchar* message, const void* userParam)
    {
        std::cerr << "GL:" << message << "\n";
    },
    nullptr
);

// Filter by severity:
metagl::glDebugMessageControl(
    metagl::DebugSource::DontCare,
    metagl::DebugType::DontCare,
    metagl::DebugSeverity::Notification,  // suppress notifications
    0, nullptr, GL_FALSE
);

Tip: Combining DebugOutputSynchronous with the driver callback gives you a stack trace in the debugger at the exact call that triggered the error. Without Synchronous, the callback may fire at any time.