Files
plotter/src/gl/loader.cpp
T

209 lines
6.9 KiB
C++

#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)
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<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 || (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<GLuint>(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<ret (*) args>(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