The repository had no licence at all, so every source was all-rights-reserved by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino Ferrari) and a two-line SPDX header to each of the 113 first-party sources -- 48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the grant travels with each file rather than relying on the root document alone. Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely vendored code is untouched: third_party/stb_truetype.h carries no header of ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py emit() now writes the two lines into its header comment and the file was regenerated -- gen_atlas.py --check passes and the regeneration adds only those comment lines to the previously committed file. README's provenance section now states MIT and links LICENSE. No code, test or public API change.
413 lines
13 KiB
C++
413 lines
13 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#include <plot/gl/handle.hpp>
|
|
|
|
#include <plot/gl/loader.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 (!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<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::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<GLsizeiptr>(bytes), nullptr, flags);
|
|
}
|
|
|
|
void* Buffer::map_range(usize offset, usize bytes, GLbitfield access) const noexcept {
|
|
bind();
|
|
return glMapBufferRange(GL_ARRAY_BUFFER, static_cast<GLintptr>(offset),
|
|
static_cast<GLsizeiptr>(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<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 (!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<GLsizei>(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<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 (!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
|