feat(gl): dependency-free GL loader, RAII handles, caps probe, headless EGL harness
This commit is contained in:
1 parent
ed71719df7
commit
258caa38a7
11 files changed
+1458
-1
No files matched your search
@@ -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
|
||||
@@ -0,0 +1,192 @@
|
||||
#include <plot/gl/loader.hpp>
|
||||
|
||||
#include <plot/gl/caps.hpp>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
|
||||
// Debug-only diagnostic for the first entry point the loader cannot resolve;
|
||||
// the failure itself is reported through load()'s return value, so this never
|
||||
// aborts a release build or a test run.
|
||||
#ifndef NDEBUG
|
||||
#define PLOT_ASSERT(cond, message) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
std::fprintf(stderr, "plot::gl::load: cannot resolve %s (%s:%d)\n", (message), \
|
||||
__FILE__, __LINE__); \
|
||||
} \
|
||||
} while (false)
|
||||
#else
|
||||
#define PLOT_ASSERT(cond, message) ((void)0)
|
||||
#endif
|
||||
|
||||
namespace plot::gl {
|
||||
|
||||
// The one definition of the entry point table declared in api.hpp.
|
||||
#define PLOT_GL_DEF(ret, name, args) ret (*name) args = nullptr;
|
||||
PLOT_GL_FUNCS(PLOT_GL_DEF)
|
||||
#undef PLOT_GL_DEF
|
||||
|
||||
namespace {
|
||||
|
||||
/// The system GL library, opened on the first name that neither the caller's
|
||||
/// resolver nor the global namespace can supply. Never closed: the entry
|
||||
/// points handed out must stay valid for the whole process.
|
||||
void* system_gl() noexcept {
|
||||
static void* lib = nullptr;
|
||||
static bool opened = false;
|
||||
if (!opened) {
|
||||
opened = true;
|
||||
lib = dlopen("libGL.so.1", RTLD_LAZY | RTLD_LOCAL);
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
|
||||
void* resolve(ProcLoader loader, const char* name) noexcept {
|
||||
if (loader != nullptr) {
|
||||
if (void* symbol = loader(name)) {
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
if (void* symbol = dlsym(RTLD_DEFAULT, name)) {
|
||||
return symbol;
|
||||
}
|
||||
if (void* lib = system_gl()) {
|
||||
return dlsym(lib, name);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- caps probe
|
||||
|
||||
Caps g_caps;
|
||||
bool g_caps_valid = false;
|
||||
|
||||
const char* gl_string(GLenum name) noexcept {
|
||||
const char* text = reinterpret_cast<const char*>(glGetString(name));
|
||||
return text != nullptr ? text : "";
|
||||
}
|
||||
|
||||
/// True when `all` (the legacy space-separated GL_EXTENSIONS string) contains
|
||||
/// `name` as a whole token.
|
||||
bool has_token(const char* all, const char* name) noexcept {
|
||||
if (all == nullptr || name == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const usize len = std::strlen(name);
|
||||
for (const char* hit = std::strstr(all, name); hit != nullptr; hit = std::strstr(hit + len, name)) {
|
||||
const bool at_start = hit == all || hit[-1] == ' ';
|
||||
const bool at_end = hit[len] == '\0' || hit[len] == ' ';
|
||||
if (at_start && at_end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Tests for an extension in the form the current context exposes it: the
|
||||
/// indexed list on 3.0+, the legacy string before that (where
|
||||
/// `glGetString(GL_EXTENSIONS)` is still legal).
|
||||
bool has_extension(int major, const char* name) noexcept {
|
||||
if (major >= 3) {
|
||||
if (glGetStringi == nullptr) {
|
||||
return false; // indexed list unavailable: the legacy string is illegal on 3.0+ core
|
||||
}
|
||||
GLint count = 0;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
|
||||
for (GLint i = 0; i < count; ++i) {
|
||||
const char* ext =
|
||||
reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
|
||||
if (ext != nullptr && std::strcmp(ext, name) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Before 3.0 the legacy space-separated string is the only enumeration.
|
||||
return has_token(reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS)), name);
|
||||
}
|
||||
|
||||
/// Fills `out` from the context that is current on this thread. Returns false
|
||||
/// when there is no usable context, leaving `out` untouched.
|
||||
bool query_caps(Caps& out) noexcept {
|
||||
if (glGetString == nullptr || glGetIntegerv == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const char* version = reinterpret_cast<const char*>(glGetString(GL_VERSION));
|
||||
if (version == nullptr) {
|
||||
return false; // no current context
|
||||
}
|
||||
int major = 0;
|
||||
int minor = 0;
|
||||
// Drivers report e.g. "4.6 (Compatibility Profile) Mesa 25.2.0"; only the
|
||||
// leading "major.minor" is part of the specification.
|
||||
if (std::sscanf(version, "%d.%d", &major, &minor) != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out.major = major;
|
||||
out.minor = minor;
|
||||
out.renderer = gl_string(GL_RENDERER);
|
||||
out.vendor = gl_string(GL_VENDOR);
|
||||
|
||||
const bool has_44 = major > 4 || (major == 4 && minor >= 4);
|
||||
out.compute = major > 4 || (major == 4 && minor >= 3);
|
||||
const bool storage = has_44 || has_extension(major, "GL_ARB_buffer_storage");
|
||||
out.buffer_storage = storage;
|
||||
out.persistent_map = storage;
|
||||
|
||||
GLint profile = 0;
|
||||
if (major >= 3) {
|
||||
// GL_CONTEXT_PROFILE_MASK only exists from 3.2 on; older contexts would
|
||||
// raise GL_INVALID_ENUM and leave the value at 0.
|
||||
glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &profile);
|
||||
}
|
||||
out.core_profile = (static_cast<GLuint>(profile) & GL_CONTEXT_CORE_PROFILE_BIT) != 0u;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool load(ProcLoader loader) noexcept {
|
||||
const char* missing = nullptr;
|
||||
|
||||
#define PLOT_GL_RESOLVE(ret, name, args) \
|
||||
do { \
|
||||
void* symbol = resolve(loader, #name); \
|
||||
if (symbol != nullptr) { \
|
||||
name = reinterpret_cast<ret (*) args>(symbol); \
|
||||
} else if (missing == nullptr) { \
|
||||
missing = #name; \
|
||||
} \
|
||||
} while (false);
|
||||
|
||||
PLOT_GL_FUNCS(PLOT_GL_RESOLVE)
|
||||
|
||||
#undef PLOT_GL_RESOLVE
|
||||
|
||||
// The table may have changed, so whatever caps() cached for the old one is
|
||||
// stale — including when this load() fails.
|
||||
g_caps_valid = false;
|
||||
if (missing != nullptr) {
|
||||
PLOT_ASSERT(false, missing);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const Caps& caps() noexcept {
|
||||
if (!g_caps_valid) {
|
||||
if (query_caps(g_caps)) {
|
||||
g_caps_valid = true;
|
||||
} else {
|
||||
// No context (or no resolved entry points): report an empty probe rather
|
||||
// than stale or uninitialised values.
|
||||
g_caps = Caps{};
|
||||
}
|
||||
}
|
||||
return g_caps;
|
||||
}
|
||||
|
||||
} // namespace plot::gl
|
||||
Reference in new issue
Block a user