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:
- Value formatter —
metagl::debug::to_str(T): a template that formats any value. For enum classes it callsmetagl::to_string()if available (from EnumNames.hpp), otherwise falls back to the raw integer. Pointers print as hex. Null pointers print as"null". - Argument formatter —
metagl::debug::fmt_args(args...): a fold-expression that joins all arguments with", ". - Recorder —
metagl::debug::record(func, retval, params): stores aCallRecordinto 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:
- Sequential call number since last flush
- ISO timestamp (millisecond precision)
- Function name
- Formatted arguments (enum names or hex/decimal values)
- Return value (
voidfor void functions)
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.