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

+7
View File
@@ -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
+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)) {
+17 -11
View File
@@ -10,6 +10,7 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <dlfcn.h>
#include <utility>
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<const char*>(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<const char*>(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<const char*>(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.