Architecture
meta-gl is designed around a single principle: replace unsafe OpenGL integer parameters with strongly-typed C++ types, with zero extra cost.
Design Goals
- Thin: every meta-gl function maps 1-to-1 to one GL call. No batching, no caching.
- Type-safe: separate
enum classfor every OpenGL parameter domain. - Zero-overhead: enum conversions are
static_cast, functions are inlined by the compiler. - Procedural: no OOP resource classes. Ownership belongs to easy-gl, not meta-gl.
- Predictable: names closely mirror the original GL names for easy lookup.
Stack Overview
┌─────────────────────────────────────────┐
│ Host Application │
│ (creates window + GL context) │
└──────────────────┬──────────────────────┘
│ GetProcAddress
┌──────────────────▼──────────────────────┐
│ easy-gl (easygl::) │
│ OOP/RAII: Texture, Buffer, Program │
│ owns and manages GL object lifetimes │
└──────────────────┬──────────────────────┘
│ calls metagl::gl*
┌──────────────────▼──────────────────────┐
│ meta-gl (metagl::) │
│ ● enum class wrappers │
│ ● function pointer table │
│ ● context / capability detection │
│ ● optional debug logging │
└──────────────────┬──────────────────────┘
│ real GL calls
┌──────────────────▼──────────────────────┐
│ OpenGL ES driver / WebGL runtime │
└─────────────────────────────────────────┘
Namespace Layout
| Symbol | Location | Purpose |
|---|---|---|
metagl::gl* | Functions.hpp | Wrapper functions, one per GL call |
metagl::BufferTarget, etc. | Enums.hpp | Type-safe GL enum domains |
metagl::GLuint, etc. | Types.hpp | Re-exported GL primitive types |
metagl::Initialize() | Loader.hpp | Function pointer table init |
metagl::GetContextInfo() | Context.hpp | Context status and version |
metagl::GetCapabilities() | Capabilities.hpp | Runtime ES/WebGL capabilities |
metagl::debug::* | Debug.hpp | Call logging (opt-in compile flag) |
metagl::to_string() | EnumNames.hpp | Human-readable enum names for logging |
Enum Strategy
Each OpenGL parameter domain maps to its own enum class backed by GLenum.
This means the compiler refuses to accept a BufferTarget where a TextureTarget
is expected, and vice versa — even though both are uint32_t at runtime.
// Raw OpenGL — both compile, second is a bug: glBindBuffer(GL_ARRAY_BUFFER, vbo); // correct glBindBuffer(GL_TEXTURE_2D, vbo); // silent bug! // meta-gl — second is a compile error: metagl::glBindBuffer(metagl::BufferTarget::Array, vbo); // correct metagl::glBindBuffer(metagl::TextureTarget::Texture2D, vbo); // ERROR
For bitfield parameters (like glClear), meta-gl defines operator| on the enum class:
metagl::glClear(metagl::ClearBufferBit::Color | metagl::ClearBufferBit::Depth); // ↑ returns ClearBufferBit, not a raw integer
Lightweight Handle Types
Raw GLuint is used for everything in OpenGL — textures, buffers, shaders, programs.
meta-gl wraps them in minimal named structs so they cannot be accidentally swapped:
struct TextureId { GLuint value{}; }; struct BufferId { GLuint value{}; }; struct ProgramId { GLuint value{}; }; struct ShaderId { GLuint value{}; };
These structs own nothing. They are plain data. RAII lifetimes belong in easy-gl.
Function Pointer Loading
meta-gl does not link directly against any GL library. Instead, it stores a table of
void* function pointers loaded at runtime via the host-provided
GetProcAddress callback. This allows the same binary to run on OpenGL ES 2.0
through 3.2 and WebGL, gracefully skipping unavailable functions.
// Internal (simplified) — actual table is in Functions.cpp: static PFNGLBINDBUFFERPROC _glBindBuffer = nullptr; static PFNGLCREATESHADERPPROC _glCreateShader = nullptr; // ... ~400 pointers total bool metagl::Initialize(GlGetProcAddressFn loader) { _glBindBuffer = (PFNGLBINDBUFFERPROC) loader("glBindBuffer"); _glCreateShader = (PFNGLCREATESHADERPPROC) loader("glCreateShader"); // ... return _glBindBuffer != nullptr; // core set check }
C++20 Features Used
| Feature | Where used | Purpose |
|---|---|---|
enum class | Enums.hpp | Type-safe GL parameter categories |
std::span | Functions.hpp, examples | Buffer data views without raw pointer+size |
constexpr if | Debug.hpp | Zero-cost branch on template type |
Concepts (requires) | Debug.hpp | Optional to_string() detection |
| Fold expressions | Debug.hpp fmt_args | Variadic argument formatting |
[[nodiscard]] | Context.hpp, Capabilities.hpp, Loader.hpp | Prevent ignoring important return values |
noexcept | Context.hpp | Context state queries are non-throwing |
std::string_view | Loader.hpp, Debug.hpp | Non-owning string parameters |
Not used: C++ modules, std::expected, std::ranges pipelines,
heavy SFINAE, consteval, C++26 features. The design deliberately avoids anything
that would hurt portability or compiler error readability.
What meta-gl Is NOT
- Not a scene graph or renderer
- Not an RAII resource manager (that is easy-gl's job)
- Not a windowing library
- Not a context creator
- Not a shader compiler or SPIR-V pipeline
- Not a math library
- Not an error-reporting framework (only debug assertions and optional logging)
Refactoring Rules
When contributing to meta-gl, follow these constraints:
- Read
CLAUDE.mdbefore making changes. - Keep patches small and reviewable — one concept or enum group at a time.
- Do not redesign the whole project in one patch.
- Do not introduce RAII or OOP resource ownership into meta-gl.
- Do not add modules or experimental C++26 features.
- Build after every non-trivial change.
- Show the diff before continuing with more refactoring.