easy-gl › Getting Started

Getting Started

Prerequisites

CMake integration

Add easy-gl and meta-gl as subdirectories or use FetchContent:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
project(MyApp)

include(FetchContent)
FetchContent_Declare(easy-gl
    GIT_REPOSITORY https://github.com/openeggbert/easy-gl.git
    GIT_TAG        main)
FetchContent_MakeAvailable(easy-gl)

add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE easy-gl)

easy-gl's CMake target is easy-gl. It automatically pulls in meta-gl and sets the include paths.

Including the library

A single umbrella header includes everything:

#include <easygl/easygl.hpp>

Or include individual class headers for faster compilation:

#include <easygl/Device.hpp>
#include <easygl/Buffer.hpp>
#include <easygl/Program.hpp>
#include <easygl/VertexArray.hpp>

Step 1 — Create an OpenGL context

easy-gl does not create a window or context itself. Use SDL3, GLFW, or any other library for that. The key requirement is that you have an active OpenGL context before calling device.initialize().

// SDL3 example — create window + context
SDL_Init(SDL_INIT_VIDEO);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);

SDL_Window* window = SDL_CreateWindow("My App", 800, 600, SDL_WINDOW_OPENGL);
SDL_GLContext glCtx = SDL_GL_CreateContext(window);
SDL_GL_MakeCurrent(window, glCtx);

Step 2 — Initialize the Device

Device is the central object. Initialise it by passing a function pointer loader that resolves OpenGL symbols at runtime:

easygl::Device device;
try {
    device.initialize(
        reinterpret_cast<easygl::GLGetProcAddressFn>(SDL_GL_GetProcAddress));
} catch (const easygl::Exception& e) {
    std::cerr << "easy-gl init failed: " << e.what() << '\n';
    return 1;
}
After initialization you can inspect device.capabilities() to query the vendor, renderer, version and supported features.

Step 3 — Create GPU resources

All resource classes follow the same pattern: construct, then call create() to allocate the GPU handle.

// A buffer
easygl::Buffer vbo;
vbo.create();

// A vertex array object
easygl::VertexArray vao;
vao.create();

// A compiled+linked program (one-shot constructor)
easygl::Program prog(vertexSrc, fragmentSrc);
Resources must be created while a valid OpenGL context is current. Never construct them before calling device.initialize().

Step 4 — Upload data

constexpr float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

vao.bind();
vbo.bind(easygl::BufferTarget::Array);
vbo.set_data(easygl::BufferTarget::Array, vertices, sizeof(vertices));

vao.set_attribute(easygl::VertexAttribute{
    .index            = 0,
    .components       = 3,
    .type             = easygl::DataType::Float,
    .normalized       = false,
    .stride_in_bytes  = 3 * sizeof(float),
    .offset_in_bytes  = 0,
    .enabled          = true
});

Step 5 — Render loop

while (running)
{
    device.set_clear_color(0.1f, 0.1f, 0.1f, 1.0f);
    device.clear(easygl::ClearFlags::Color);

    prog.use();
    vao.bind();
    device.draw_arrays(easygl::PrimitiveType::Triangles, 0, 3);

    SDL_GL_SwapWindow(window);
}

Step 6 — Cleanup

Resources are RAII. They are destroyed automatically when they go out of scope. The only rule: ensure all easy-gl objects are destroyed before the OpenGL context is destroyed. The simplest way is a scoped block:

// all easy-gl objects inside this scope
{
    easygl::Device device;
    device.initialize(...);

    easygl::Buffer vbo;
    vbo.create();

    // ... render loop ...

} // vbo and device destroyed here, while context is still alive

SDL_GL_DestroyContext(glCtx); // safe to destroy context now
If you destroy the GL context before easy-gl objects, the destructors will call OpenGL with an invalid context. Use scoped blocks or explicit destroy() calls to avoid this.

Next steps