feat(gl): dependency-free GL loader, RAII handles, caps probe, headless EGL harness

This commit is contained in:
Martino Ferrari committed 2026-09-27 21:07:36 +02:00
1 parent ed71719df7
commit 258caa38a7
11 files changed
+1458 -1

No files matched your search

+340
View File
@@ -0,0 +1,340 @@
#include <plot/gl/handle.hpp>
#include <cstdint>
#include <cstdio>
namespace plot::gl {
namespace {
constexpr const char* kNoLoader = "plot::gl::load() has not been called";
/// `Status::message` is a non-owning `const char*`, so the text of a failure
/// lives here: valid until the next failure reported on the same thread.
thread_local char g_message[1024];
bool fail(Status* st, ErrorCode code, const char* text) noexcept {
if (st != nullptr) {
std::snprintf(g_message, sizeof g_message, "%s", text);
st->code = code;
st->message = g_message;
}
return false;
}
/// As `fail()`, but appends driver text (a shader or program info log).
bool fail_log(Status* st, const char* what, const char* log) noexcept {
if (st != nullptr) {
std::snprintf(g_message, sizeof g_message, "%s: %s", what, log != nullptr ? log : "");
st->code = ErrorCode::GLFailure;
st->message = g_message;
}
return false;
}
/// Compiles one stage and returns its shader name, or 0 on failure (the reason
/// is already in `st`). The shader is deleted if it fails to compile.
GLuint compile_shader(GLenum stage, const char* source, const char* what, Status* st) noexcept {
const GLuint shader = glCreateShader(stage);
if (shader == 0) {
fail(st, ErrorCode::GLFailure, "glCreateShader returned 0");
return 0;
}
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == 0) {
char log[1024] = {};
GLsizei written = 0;
glGetShaderInfoLog(shader, static_cast<GLsizei>(sizeof log - 1), &written, log);
glDeleteShader(shader);
fail_log(st, what, log);
return 0;
}
return shader;
}
/// Allocates 2D storage with the sampling state every texture here shares:
/// linear filtering, clamped edges. Returns the name, or 0 if the driver
/// handed out none.
GLuint alloc_texture(GLenum internal_format, GLenum format, u32 width, u32 height) noexcept {
GLuint name = 0;
glGenTextures(1, &name);
if (name == 0) {
return 0;
}
glBindTexture(GL_TEXTURE_2D, name);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internal_format), static_cast<GLsizei>(width),
static_cast<GLsizei>(height), 0, format, GL_UNSIGNED_BYTE, nullptr);
return name;
}
} // namespace
// ------------------------------------------------------------------- Buffer
bool Buffer::create(Status* st) noexcept {
if (glGenBuffers == nullptr) {
return fail(st, ErrorCode::Unsupported, kNoLoader);
}
reset();
GLuint name = 0;
glGenBuffers(1, &name);
if (name == 0) {
return fail(st, ErrorCode::GLFailure, "glGenBuffers returned 0");
}
id_ = name;
return true;
}
void Buffer::bind(GLenum target) const noexcept { glBindBuffer(target, id_); }
void Buffer::upload(const void* data, usize bytes, GLenum usage) const noexcept {
bind();
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(bytes), data, usage);
}
void Buffer::upload_sub(const void* data, usize bytes, usize offset) const noexcept {
bind();
glBufferSubData(GL_ARRAY_BUFFER, static_cast<GLintptr>(offset), static_cast<GLsizeiptr>(bytes),
data);
}
void Buffer::destroy(GLuint id) noexcept { glDeleteBuffers(1, &id); }
// -------------------------------------------------------------- VertexArray
bool VertexArray::create(Status* st) noexcept {
if (glGenVertexArrays == nullptr) {
return fail(st, ErrorCode::Unsupported, kNoLoader);
}
reset();
GLuint name = 0;
glGenVertexArrays(1, &name);
if (name == 0) {
return fail(st, ErrorCode::GLFailure, "glGenVertexArrays returned 0");
}
id_ = name;
return true;
}
void VertexArray::bind() const noexcept { glBindVertexArray(id_); }
void VertexArray::attrib(u32 index, i32 components, usize stride, usize offset) const noexcept {
const GLuint attrib = static_cast<GLuint>(index);
glEnableVertexAttribArray(attrib);
glVertexAttribPointer(attrib, static_cast<GLint>(components), GL_FLOAT, GL_FALSE,
static_cast<GLsizei>(stride),
reinterpret_cast<const void*>(static_cast<std::uintptr_t>(offset)));
}
void VertexArray::destroy(GLuint id) noexcept { glDeleteVertexArrays(1, &id); }
// ------------------------------------------------------------------ Program
Program Program::from_source(const char* vertex_src, const char* fragment_src,
Status* st) noexcept {
Program program;
if (glCreateShader == nullptr || glCreateProgram == nullptr) {
fail(st, ErrorCode::Unsupported, kNoLoader);
return program;
}
if (vertex_src == nullptr || fragment_src == nullptr) {
fail(st, ErrorCode::InvalidArgument, "shader source must not be null");
return program;
}
const GLuint vertex = compile_shader(GL_VERTEX_SHADER, vertex_src, "vertex shader failed", st);
if (vertex == 0) {
return program;
}
const GLuint fragment =
compile_shader(GL_FRAGMENT_SHADER, fragment_src, "fragment shader failed", st);
if (fragment == 0) {
glDeleteShader(vertex);
return program;
}
const GLuint name = glCreateProgram();
if (name == 0) {
glDeleteShader(vertex);
glDeleteShader(fragment);
fail(st, ErrorCode::GLFailure, "glCreateProgram returned 0");
return program;
}
glAttachShader(name, vertex);
glAttachShader(name, fragment);
glLinkProgram(name);
// The program owns the linked code from here on, so the shader objects can
// be detached and deleted.
glDetachShader(name, vertex);
glDetachShader(name, fragment);
glDeleteShader(vertex);
glDeleteShader(fragment);
GLint linked = 0;
glGetProgramiv(name, GL_LINK_STATUS, &linked);
if (linked == 0) {
char log[1024] = {};
GLsizei written = 0;
glGetProgramInfoLog(name, static_cast<GLsizei>(sizeof log - 1), &written, log);
glDeleteProgram(name);
fail_log(st, "program link failed", log);
return program;
}
program.id_ = name;
return program;
}
void Program::use() const noexcept { glUseProgram(id_); }
void Program::set_uniform(const char* name, f32 value) const noexcept {
if (id_ == 0 || name == nullptr) {
return;
}
const GLint location = glGetUniformLocation(id_, name);
if (location >= 0) {
glUniform1f(location, value);
}
}
void Program::set_uniform(const char* name, Vec2 value) const noexcept {
if (id_ == 0 || name == nullptr) {
return;
}
const GLint location = glGetUniformLocation(id_, name);
if (location >= 0) {
glUniform2f(location, value.x, value.y);
}
}
void Program::destroy(GLuint id) noexcept { glDeleteProgram(id); }
// ------------------------------------------------------------------ Texture
bool Texture::create_rgba(u32 width, u32 height, Status* st) noexcept {
if (glGenTextures == nullptr) {
return fail(st, ErrorCode::Unsupported, kNoLoader);
}
if (width == 0 || height == 0) {
return fail(st, ErrorCode::InvalidArgument, "texture size must be non-zero");
}
reset();
const GLuint name = alloc_texture(GL_RGBA8, GL_RGBA, width, height);
if (name == 0) {
return fail(st, ErrorCode::GLFailure, "glGenTextures returned 0");
}
id_ = name;
return true;
}
bool Texture::create_r8(u32 width, u32 height, Status* st) noexcept {
if (glGenTextures == nullptr) {
return fail(st, ErrorCode::Unsupported, kNoLoader);
}
if (width == 0 || height == 0) {
return fail(st, ErrorCode::InvalidArgument, "texture size must be non-zero");
}
reset();
const GLuint name = alloc_texture(GL_R8, GL_RED, width, height);
if (name == 0) {
return fail(st, ErrorCode::GLFailure, "glGenTextures returned 0");
}
id_ = name;
return true;
}
void Texture::upload_rgba(u32 width, u32 height, const void* pixels) const noexcept {
glBindTexture(GL_TEXTURE_2D, id_);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(GL_RGBA8), static_cast<GLsizei>(width),
static_cast<GLsizei>(height), 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
}
void Texture::upload_r8(u32 width, u32 height, const void* pixels) const noexcept {
glBindTexture(GL_TEXTURE_2D, id_);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // single-channel rows have no padding
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(GL_R8), static_cast<GLsizei>(width),
static_cast<GLsizei>(height), 0, GL_RED, GL_UNSIGNED_BYTE, pixels);
}
void Texture::bind(GLenum unit) const noexcept {
glActiveTexture(unit);
glBindTexture(GL_TEXTURE_2D, id_);
}
void Texture::set_filter(GLenum min, GLenum mag) const noexcept {
glBindTexture(GL_TEXTURE_2D, id_);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, static_cast<GLint>(min));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, static_cast<GLint>(mag));
}
void Texture::destroy(GLuint id) noexcept { glDeleteTextures(1, &id); }
// -------------------------------------------------------------- Framebuffer
bool Framebuffer::create(Status* st) noexcept {
if (glGenFramebuffers == nullptr) {
return fail(st, ErrorCode::Unsupported, kNoLoader);
}
reset();
GLuint name = 0;
glGenFramebuffers(1, &name);
if (name == 0) {
return fail(st, ErrorCode::GLFailure, "glGenFramebuffers returned 0");
}
id_ = name;
return true;
}
void Framebuffer::bind(GLenum target) const noexcept { glBindFramebuffer(target, id_); }
void Framebuffer::attach_color(const Texture& texture) const noexcept {
bind();
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.id(), 0);
}
bool Framebuffer::complete() const noexcept {
bind();
return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
}
void Framebuffer::destroy(GLuint id) noexcept { glDeleteFramebuffers(1, &id); }
// -------------------------------------------------------------------- Fence
bool Fence::create(Status* st) noexcept {
if (glFenceSync == nullptr) {
return fail(st, ErrorCode::Unsupported, kNoLoader);
}
reset();
const GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (sync == nullptr) {
return fail(st, ErrorCode::GLFailure, "glFenceSync returned no sync object");
}
id_ = sync;
return true;
}
bool Fence::wait(u64 timeout_ns) const noexcept {
if (id_ == nullptr) {
return true; // nothing in flight
}
const GLenum result = glClientWaitSync(id_, GL_SYNC_FLUSH_COMMANDS_BIT, timeout_ns);
return result == GL_ALREADY_SIGNALED || result == GL_CONDITION_SATISFIED;
}
void Fence::reset() noexcept {
if (id_ != nullptr) {
glDeleteSync(id_);
id_ = nullptr;
}
}
} // namespace plot::gl