From 68664571553dad20e7885e681b205034d88d6c36 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Sun, 27 Sep 2026 21:15:57 +0200 Subject: [PATCH] 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. --- include/plot/gl/loader.hpp | 7 +++++++ src/gl/handle.cpp | 24 ++++++++++++++++++++++++ src/gl/loader.cpp | 12 +++++++++++- tests/test_gl_context.cpp | 28 +++++++++++++++++----------- 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/include/plot/gl/loader.hpp b/include/plot/gl/loader.hpp index 2660994..b1b005d 100644 --- a/include/plot/gl/loader.hpp +++ b/include/plot/gl/loader.hpp @@ -25,4 +25,11 @@ using ProcLoader = void* (*)(const char*); /// context. bool load(ProcLoader loader = nullptr) noexcept; +/// True once a `load()` has resolved every required entry point of the global +/// table. This is the gate superseding per-pointer null checks: while it is +/// false the table may be only partially populated (see load()), so no `gl*` +/// entry point may be called. The fallible handle constructors report +/// `ErrorCode::Unsupported` until it becomes true. +bool available() noexcept; + } // namespace plot::gl diff --git a/src/gl/handle.cpp b/src/gl/handle.cpp index cb063f5..5f5fdc4 100644 --- a/src/gl/handle.cpp +++ b/src/gl/handle.cpp @@ -1,5 +1,7 @@ #include +#include + #include #include @@ -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); } diff --git a/src/gl/loader.cpp b/src/gl/loader.cpp index 059ebb6..c981553 100644 --- a/src/gl/loader.cpp +++ b/src/gl/loader.cpp @@ -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)) { diff --git a/tests/test_gl_context.cpp b/tests/test_gl_context.cpp index f8560e1..aea067c 100644 --- a/tests/test_gl_context.cpp +++ b/tests/test_gl_context.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace { @@ -65,7 +66,8 @@ PLOT_TEST(headless_context_provides_gl33) { PLOT_CHECK(caps.major > 3 || (caps.major == 3 && caps.minor >= 3)); PLOT_CHECK(caps.renderer != nullptr && caps.renderer[0] != '\0'); PLOT_CHECK(caps.vendor != nullptr && caps.vendor[0] != '\0'); - PLOT_CHECK(caps.core_profile); // the context was created as 3.3 core + // EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR is requested, so this checks our profile-mask parsing. + PLOT_CHECK(caps.core_profile); const char* version = reinterpret_cast(plot::gl::glGetString(plot::gl::GL_VERSION)); PLOT_CHECK(version != nullptr); const int major = caps.major; @@ -78,16 +80,20 @@ PLOT_TEST(headless_context_provides_gl33) { caps.core_profile ? 1 : 0); // The dlsym(RTLD_DEFAULT) / libGL.so.1 fallback must resolve the whole table - // too, and its pointers must still dispatch to the current EGL context. - PLOT_CHECK(plot::gl::load(nullptr)); - PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR); - const char* fallback_version = - reinterpret_cast(plot::gl::glGetString(plot::gl::GL_VERSION)); - PLOT_CHECK(fallback_version != nullptr); - PLOT_CHECK(std::strcmp(fallback_version, version) == 0); // same context, same driver - const plot::gl::Caps& reprobed = plot::gl::caps(); // load() invalidated the cache - PLOT_CHECK(reprobed.major == major && reprobed.minor == minor); - PLOT_CHECK(reprobed.renderer != nullptr && reprobed.renderer[0] != '\0'); + // too, and its pointers must still dispatch to the current EGL context. Only + // a host that ships desktop libGL can exercise it; on a GLES-only host there + // is nothing to assert. + if (dlopen("libGL.so.1", RTLD_LAZY) != nullptr) { + PLOT_CHECK(plot::gl::load(nullptr)); + PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR); + const char* fallback_version = + reinterpret_cast(plot::gl::glGetString(plot::gl::GL_VERSION)); + PLOT_CHECK(fallback_version != nullptr); + PLOT_CHECK(std::strcmp(fallback_version, version) == 0); // same context, same driver + const plot::gl::Caps& reprobed = plot::gl::caps(); // load() invalidated the cache + PLOT_CHECK(reprobed.major == major && reprobed.minor == minor); + PLOT_CHECK(reprobed.renderer != nullptr && reprobed.renderer[0] != '\0'); + } // Hand the table back to the context-aware resolver so teardown (deleting the // framebuffer) runs through the entry points of the context it belongs to.