feat(gl): dependency-free GL loader, RAII handles, caps probe, headless EGL harness
This commit is contained in:
1 parent
ed71719df7
commit
258caa38a7
11 files changed
+1458
-1
No files matched your search
@@ -0,0 +1,192 @@
|
||||
#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)
|
||||
#undef PLOT_GL_DEF
|
||||
|
||||
namespace {
|
||||
|
||||
/// 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) {
|
||||
// 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 {
|
||||
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)
|
||||
|
||||
#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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
Reference in new issue
Block a user