#include #include #include #include #include // 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) PLOT_GL_OPTIONAL_FUNCS(PLOT_GL_DEF) #undef 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. 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(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(glGetStringi(GL_EXTENSIONS, static_cast(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(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(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 || (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); } out.core_profile = (static_cast(profile) & GL_CONTEXT_CORE_PROFILE_BIT) != 0u; return true; } } // 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) \ do { \ void* symbol = resolve(loader, #name); \ if (symbol != nullptr) { \ name = reinterpret_cast(symbol); \ } else if (missing == nullptr) { \ missing = #name; \ } \ } while (false); PLOT_GL_FUNCS(PLOT_GL_RESOLVE) // Above-baseline entries: resolved the same way, but a name the driver does not // have leaves its pointer null instead of failing the load, so a 3.3-only // context keeps working. Callers gate them on caps() (see api.hpp). PLOT_GL_OPTIONAL_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; } 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)) { 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