// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include #include #include #include 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(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(internal_format), static_cast(width), static_cast(height), 0, format, GL_UNSIGNED_BYTE, nullptr); return name; } } // namespace // ------------------------------------------------------------------- Buffer bool Buffer::create(Status* st) noexcept { if (!available()) { return fail(st, ErrorCode::Unsupported, kNoLoader); } 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; note_allocated(); 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(bytes), data, usage); } void Buffer::upload_sub(const void* data, usize bytes, usize offset) const noexcept { bind(); glBufferSubData(GL_ARRAY_BUFFER, static_cast(offset), static_cast(bytes), data); } void Buffer::storage(usize bytes, GLbitfield flags) const noexcept { if (glBufferStorage == nullptr) { return; // not resolved on this driver: the caller checked caps() first } bind(); glBufferStorage(GL_ARRAY_BUFFER, static_cast(bytes), nullptr, flags); } void* Buffer::map_range(usize offset, usize bytes, GLbitfield access) const noexcept { bind(); return glMapBufferRange(GL_ARRAY_BUFFER, static_cast(offset), static_cast(bytes), access); } bool Buffer::unmap() const noexcept { bind(); return glUnmapBuffer(GL_ARRAY_BUFFER) == GL_TRUE; } void Buffer::destroy(GLuint id) noexcept { glDeleteBuffers(1, &id); } // -------------------------------------------------------------- VertexArray bool VertexArray::create(Status* st) noexcept { if (!available()) { return fail(st, ErrorCode::Unsupported, kNoLoader); } 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; note_allocated(); 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(index); glEnableVertexAttribArray(attrib); glVertexAttribPointer(attrib, static_cast(components), GL_FLOAT, GL_FALSE, static_cast(stride), reinterpret_cast(static_cast(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 (!available()) { fail(st, ErrorCode::Unsupported, kNoLoader); return 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(sizeof log - 1), &written, log); glDeleteProgram(name); fail_log(st, "program link failed", log); return program; } program.id_ = name; program.note_allocated(); 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::set_uniform(const char* name, Color value) const noexcept { if (id_ == 0 || name == nullptr) { return; } const GLint location = glGetUniformLocation(id_, name); if (location >= 0) { glUniform4f(location, value.r, value.g, value.b, value.a); } } void Program::set_uniform_int(const char* name, i32 value) const noexcept { if (id_ == 0 || name == nullptr) { return; } const GLint location = glGetUniformLocation(id_, name); if (location >= 0) { glUniform1i(location, value); } } void Program::destroy(GLuint id) noexcept { glDeleteProgram(id); } // ------------------------------------------------------------------ Texture bool Texture::create_rgba(u32 width, u32 height, Status* st) noexcept { if (!available()) { return fail(st, ErrorCode::Unsupported, kNoLoader); } 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; note_allocated(); return true; } bool Texture::create_r8(u32 width, u32 height, Status* st) noexcept { if (!available()) { return fail(st, ErrorCode::Unsupported, kNoLoader); } 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; note_allocated(); 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(GL_RGBA8), static_cast(width), static_cast(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(GL_R8), static_cast(width), static_cast(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(min)); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, static_cast(mag)); } void Texture::destroy(GLuint id) noexcept { glDeleteTextures(1, &id); } // -------------------------------------------------------------- Framebuffer bool Framebuffer::create(Status* st) noexcept { if (!available()) { return fail(st, ErrorCode::Unsupported, kNoLoader); } 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; note_allocated(); 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 (!available()) { return fail(st, ErrorCode::Unsupported, kNoLoader); } 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