fix(gl): gate fallible constructors on a complete loader table

- loader: add available(), true only after load() has resolved every
  required entry point (false at the start of load()). Program::from_source
  and the other fallible constructors return ErrorCode::Unsupported while it
  is false, so a failed load() with a partially populated table can no longer
  fault on an unresolved entry point.
- caps: GL_CONTEXT_PROFILE_MASK only exists from 3.2, so query it only on
  3.2+ contexts. A 3.0/3.1 query latches GL_INVALID_ENUM in the context, which
  the next unrelated glGetError() (e.g. read_pixel) then reports as failure.
- tests: gate the dlsym/libGL.so.1 fallback assertions on libGL being
  resolvable, so the GL suite also passes on a GLES-only host.
This commit is contained in:
Martino Ferrari committed 2026-09-27 21:15:57 +02:00
1 parent 258caa38a7
commit 6866457155
4 files changed
+59 -12

No files matched your search

+24
View File
@@ -1,5 +1,7 @@
#include <plot/gl/handle.hpp>
#include <plot/gl/loader.hpp>
#include <cstdint>
#include <cstdio>
@@ -78,6 +80,9 @@ GLuint alloc_texture(GLenum internal_format, GLenum format, u32 width, u32 heigh
// ------------------------------------------------------------------- Buffer
bool Buffer::create(Status* st) noexcept {
if (!available()) {
return fail(st, ErrorCode::Unsupported, kNoLoader);
}
if (glGenBuffers == nullptr) {
return fail(st, ErrorCode::Unsupported, kNoLoader);
}
@@ -109,6 +114,9 @@ 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);
}
@@ -139,6 +147,10 @@ void VertexArray::destroy(GLuint id) noexcept { glDeleteVertexArrays(1, &id); }
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;
@@ -219,6 +231,9 @@ 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);
}
@@ -235,6 +250,9 @@ bool Texture::create_rgba(u32 width, u32 height, Status* st) noexcept {
}
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);
}
@@ -280,6 +298,9 @@ 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);
}
@@ -310,6 +331,9 @@ 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);
}
+11 -1
View File
@@ -30,6 +30,10 @@ PLOT_GL_FUNCS(PLOT_GL_DEF)
namespace {
/// False until a `load()` has resolved every required entry point of the table
/// above, which may stay partially populated until then. Read via available().
bool g_table_ready = false;
/// 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.
@@ -138,7 +142,7 @@ bool query_caps(Caps& out) noexcept {
out.persistent_map = storage;
GLint profile = 0;
if (major >= 3) {
if (major > 3 || (major == 3 && minor >= 2)) {
// 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);
@@ -150,6 +154,9 @@ bool query_caps(Caps& out) noexcept {
} // namespace
bool load(ProcLoader loader) noexcept {
// A partially resolved table must not be used: the fallible constructors
// consult available() and report Unsupported until the whole table is good.
g_table_ready = false;
const char* missing = nullptr;
#define PLOT_GL_RESOLVE(ret, name, args) \
@@ -173,9 +180,12 @@ bool load(ProcLoader loader) noexcept {
PLOT_ASSERT(false, missing);
return false;
}
g_table_ready = true;
return true;
}
bool available() noexcept { return g_table_ready; }
const Caps& caps() noexcept {
if (!g_caps_valid) {
if (query_caps(g_caps)) {