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,187 @@
|
||||
#pragma once
|
||||
#include <plot/core/types.hpp>
|
||||
|
||||
// The GL surface of this library is hand-written on purpose: the core target
|
||||
// links and includes nothing third-party, so there is no glad/GLEW/GLM and no
|
||||
// Khronos header. Everything below is either a typedef fixed by the OpenGL
|
||||
// specification or an entry point resolved at runtime by `plot::gl::load()`
|
||||
// (see loader.hpp). Only OpenGL 3.3 core is assumed; anything newer is guarded
|
||||
// by the capability probe in caps.hpp.
|
||||
|
||||
namespace plot::gl {
|
||||
|
||||
using GLenum = unsigned int;
|
||||
using GLuint = unsigned int;
|
||||
using GLint = int;
|
||||
using GLsizei = int;
|
||||
using GLboolean = unsigned char;
|
||||
using GLbitfield = unsigned int;
|
||||
using GLubyte = unsigned char;
|
||||
using GLchar = char;
|
||||
using GLintptr = std::intptr_t;
|
||||
using GLsizeiptr = std::ptrdiff_t;
|
||||
using GLuint64 = u64;
|
||||
using GLsync = void*; // opaque sync object (`struct __GLsync*`)
|
||||
|
||||
inline constexpr GLboolean GL_FALSE = 0;
|
||||
inline constexpr GLboolean GL_TRUE = 1;
|
||||
|
||||
// Enumerant values are fixed by the OpenGL specification; comment each group
|
||||
// with the version or extension that introduced it.
|
||||
enum : GLenum {
|
||||
// Errors and primitives.
|
||||
GL_NO_ERROR = 0x0000,
|
||||
GL_LINE_STRIP = 0x0003,
|
||||
GL_TRIANGLES = 0x0004,
|
||||
GL_TRIANGLE_STRIP = 0x0005,
|
||||
|
||||
// Clear state.
|
||||
GL_COLOR_BUFFER_BIT = 0x00004000,
|
||||
|
||||
// Data types (unsigned short is the index/16-bit data type).
|
||||
GL_UNSIGNED_BYTE = 0x1401,
|
||||
GL_UNSIGNED_SHORT = 0x1403,
|
||||
GL_FLOAT = 0x1406,
|
||||
|
||||
// Strings and context queries.
|
||||
GL_VENDOR = 0x1F00,
|
||||
GL_RENDERER = 0x1F01,
|
||||
GL_VERSION = 0x1F02,
|
||||
GL_EXTENSIONS = 0x1F03,
|
||||
GL_NUM_EXTENSIONS = 0x821D /* 3.0 */,
|
||||
GL_CONTEXT_PROFILE_MASK = 0x9126 /* 3.2 */,
|
||||
GL_CONTEXT_CORE_PROFILE_BIT = 0x00000001,
|
||||
|
||||
// Blending.
|
||||
GL_BLEND = 0x0BE2,
|
||||
GL_SRC_ALPHA = 0x0302,
|
||||
GL_ONE_MINUS_SRC_ALPHA = 0x0303,
|
||||
|
||||
// Pixel store state.
|
||||
GL_UNPACK_ALIGNMENT = 0x0CF5,
|
||||
|
||||
// Shaders and programs.
|
||||
GL_FRAGMENT_SHADER = 0x8B30,
|
||||
GL_VERTEX_SHADER = 0x8B31,
|
||||
GL_COMPILE_STATUS = 0x8B81,
|
||||
GL_LINK_STATUS = 0x8B82,
|
||||
|
||||
// Buffers (3.3 baseline plus the 4.4 storage flags used by the caps-driven
|
||||
// persistent-mapping path).
|
||||
GL_ARRAY_BUFFER = 0x8892,
|
||||
GL_DYNAMIC_DRAW = 0x88E8,
|
||||
GL_STATIC_DRAW = 0x88E4,
|
||||
GL_STREAM_DRAW = 0x88E0,
|
||||
GL_MAP_WRITE_BIT = 0x0002 /* 4.4 */,
|
||||
GL_MAP_PERSISTENT_BIT = 0x0040 /* 4.4 */,
|
||||
GL_MAP_COHERENT_BIT = 0x0080 /* 4.4 */,
|
||||
|
||||
// Textures.
|
||||
GL_TEXTURE_2D = 0x0DE1,
|
||||
GL_TEXTURE0 = 0x84C0,
|
||||
GL_TEXTURE_MAG_FILTER = 0x2800,
|
||||
GL_TEXTURE_MIN_FILTER = 0x2801,
|
||||
GL_TEXTURE_WRAP_S = 0x2802,
|
||||
GL_TEXTURE_WRAP_T = 0x2803,
|
||||
GL_NEAREST = 0x2600,
|
||||
GL_LINEAR = 0x2601,
|
||||
GL_CLAMP_TO_EDGE = 0x812F,
|
||||
GL_RED = 0x1903,
|
||||
GL_RGBA = 0x1908,
|
||||
GL_R8 = 0x8229,
|
||||
GL_RGBA8 = 0x8058,
|
||||
|
||||
// Framebuffer objects (renderbuffers back depth attachments).
|
||||
GL_FRAMEBUFFER = 0x8D40,
|
||||
GL_COLOR_ATTACHMENT0 = 0x8CE0,
|
||||
GL_FRAMEBUFFER_COMPLETE = 0x8CD5,
|
||||
GL_RENDERBUFFER = 0x8D41,
|
||||
|
||||
// Sync objects (3.2).
|
||||
GL_SYNC_GPU_COMMANDS_COMPLETE = 0x9117,
|
||||
GL_SYNC_FLUSH_COMMANDS_BIT = 0x00000001,
|
||||
GL_ALREADY_SIGNALED = 0x911A,
|
||||
GL_CONDITION_SATISFIED = 0x911C,
|
||||
};
|
||||
|
||||
// Single source of truth for the runtime entry points: every entry is declared
|
||||
// as a function pointer here and resolved by `plot::gl::load()`. Adding an
|
||||
// entry makes it required, so only add what the library actually calls.
|
||||
#define PLOT_GL_FUNCS(X) \
|
||||
/* context and state */ \
|
||||
X(const GLubyte*, glGetString, (GLenum)) \
|
||||
X(const GLubyte*, glGetStringi, (GLenum, GLuint)) \
|
||||
X(void, glGetIntegerv, (GLenum, GLint*)) \
|
||||
X(GLenum, glGetError, (void)) \
|
||||
X(void, glFinish, (void)) \
|
||||
X(void, glViewport, (GLint, GLint, GLsizei, GLsizei)) \
|
||||
X(void, glClearColor, (float, float, float, float)) \
|
||||
X(void, glClear, (GLbitfield)) \
|
||||
X(void, glEnable, (GLenum)) \
|
||||
X(void, glBlendFunc, (GLenum, GLenum)) \
|
||||
X(void, glPixelStorei, (GLenum, GLint)) \
|
||||
/* shaders and programs */ \
|
||||
X(GLuint, glCreateShader, (GLenum)) \
|
||||
X(void, glShaderSource, (GLuint, GLsizei, const GLchar* const*, const GLint*)) \
|
||||
X(void, glCompileShader, (GLuint)) \
|
||||
X(void, glGetShaderiv, (GLuint, GLenum, GLint*)) \
|
||||
X(void, glGetShaderInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*)) \
|
||||
X(void, glDeleteShader, (GLuint)) \
|
||||
X(GLuint, glCreateProgram, (void)) \
|
||||
X(void, glAttachShader, (GLuint, GLuint)) \
|
||||
X(void, glDetachShader, (GLuint, GLuint)) \
|
||||
X(void, glLinkProgram, (GLuint)) \
|
||||
X(void, glGetProgramiv, (GLuint, GLenum, GLint*)) \
|
||||
X(void, glGetProgramInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*)) \
|
||||
X(void, glDeleteProgram, (GLuint)) \
|
||||
X(void, glUseProgram, (GLuint)) \
|
||||
X(GLint, glGetUniformLocation, (GLuint, const GLchar*)) \
|
||||
X(void, glUniform1i, (GLint, GLint)) \
|
||||
X(void, glUniform1f, (GLint, float)) \
|
||||
X(void, glUniform2f, (GLint, float, float)) \
|
||||
X(void, glUniform1fv, (GLint, GLsizei, const float*)) \
|
||||
X(void, glUniform2fv, (GLint, GLsizei, const float*)) \
|
||||
/* buffers */ \
|
||||
X(void, glGenBuffers, (GLsizei, GLuint*)) \
|
||||
X(void, glDeleteBuffers, (GLsizei, const GLuint*)) \
|
||||
X(void, glBindBuffer, (GLenum, GLuint)) \
|
||||
X(void, glBufferData, (GLenum, GLsizeiptr, const void*, GLenum)) \
|
||||
X(void, glBufferSubData, (GLenum, GLintptr, GLsizeiptr, const void*)) \
|
||||
/* vertex arrays */ \
|
||||
X(void, glGenVertexArrays, (GLsizei, GLuint*)) \
|
||||
X(void, glDeleteVertexArrays, (GLsizei, const GLuint*)) \
|
||||
X(void, glBindVertexArray, (GLuint)) \
|
||||
X(void, glEnableVertexAttribArray, (GLuint)) \
|
||||
X(void, glVertexAttribPointer, (GLuint, GLint, GLenum, GLboolean, GLsizei, const void*)) \
|
||||
/* drawing */ \
|
||||
X(void, glDrawArrays, (GLenum, GLint, GLsizei)) \
|
||||
X(void, glDrawArraysInstanced, (GLenum, GLint, GLsizei, GLsizei)) \
|
||||
/* textures */ \
|
||||
X(void, glGenTextures, (GLsizei, GLuint*)) \
|
||||
X(void, glDeleteTextures, (GLsizei, const GLuint*)) \
|
||||
X(void, glActiveTexture, (GLenum)) \
|
||||
X(void, glBindTexture, (GLenum, GLuint)) \
|
||||
X(void, glTexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*)) \
|
||||
X(void, glTexParameteri, (GLenum, GLenum, GLint)) \
|
||||
/* framebuffers */ \
|
||||
X(void, glGenFramebuffers, (GLsizei, GLuint*)) \
|
||||
X(void, glDeleteFramebuffers, (GLsizei, const GLuint*)) \
|
||||
X(void, glBindFramebuffer, (GLenum, GLuint)) \
|
||||
X(void, glFramebufferTexture2D, (GLenum, GLenum, GLenum, GLuint, GLint)) \
|
||||
X(GLenum, glCheckFramebufferStatus, (GLenum)) \
|
||||
X(void, glReadPixels, (GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*)) \
|
||||
X(void, glGenRenderbuffers, (GLsizei, GLuint*)) \
|
||||
X(void, glDeleteRenderbuffers, (GLsizei, const GLuint*)) \
|
||||
X(void, glBindRenderbuffer, (GLenum, GLuint)) \
|
||||
X(void, glRenderbufferStorage, (GLenum, GLenum, GLsizei, GLsizei)) \
|
||||
X(void, glFramebufferRenderbuffer, (GLenum, GLenum, GLenum, GLuint)) \
|
||||
/* sync objects */ \
|
||||
X(GLsync, glFenceSync, (GLenum, GLbitfield)) \
|
||||
X(GLenum, glClientWaitSync, (GLsync, GLbitfield, GLuint64)) \
|
||||
X(void, glDeleteSync, (GLsync))
|
||||
|
||||
#define PLOT_GL_DECL(ret, name, args) extern ret (*name) args;
|
||||
PLOT_GL_FUNCS(PLOT_GL_DECL)
|
||||
#undef PLOT_GL_DECL
|
||||
|
||||
} // namespace plot::gl
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
#include <plot/gl/api.hpp>
|
||||
|
||||
namespace plot::gl {
|
||||
|
||||
/// What the current context can do. Only OpenGL 3.3 core is required; every
|
||||
/// field above that baseline is false when the driver does not offer it.
|
||||
struct Caps {
|
||||
int major = 0;
|
||||
int minor = 0;
|
||||
/// GL_VERSION / GL_RENDERER / GL_VENDOR strings. They are owned by the
|
||||
/// context: valid until it is destroyed, and empty ("") when nothing has
|
||||
/// been probed yet.
|
||||
const char* renderer = "";
|
||||
const char* vendor = "";
|
||||
bool compute = false; ///< GL 4.3+ (compute shaders)
|
||||
bool buffer_storage = false; ///< GL 4.4+ or ARB_buffer_storage
|
||||
bool persistent_map = false; ///< GL 4.4+ or ARB_buffer_storage
|
||||
bool core_profile = false; ///< GL_CONTEXT_CORE_PROFILE_BIT
|
||||
};
|
||||
|
||||
/// Capabilities of the context that is current on the calling thread, probed
|
||||
/// once on the first successful query and cached afterwards. Safe to call
|
||||
/// before load(): with no usable context (or no resolved entry points) every
|
||||
/// field stays zero. `load()` invalidates the cache. The cache is
|
||||
/// process-global and unsynchronised: probe from the thread that owns the
|
||||
/// current context.
|
||||
const Caps& caps() noexcept;
|
||||
|
||||
} // namespace plot::gl
|
||||
@@ -0,0 +1,192 @@
|
||||
#pragma once
|
||||
#include <plot/core/status.hpp>
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/gl/api.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
|
||||
namespace plot::gl {
|
||||
|
||||
/// Move-only ownership of one GL object name (`GLuint`).
|
||||
///
|
||||
/// `Derived` supplies `static void destroy(GLuint) noexcept`. Copying is
|
||||
/// deleted and moving transfers the name, leaving the source empty.
|
||||
/// Destruction and `reset()` are no-ops when `id() == 0`, so an empty handle is
|
||||
/// safe to destroy at any point — including after its context is gone. A
|
||||
/// non-empty handle must not outlive the context that owns the name; call
|
||||
/// `reset()` first when the context is about to be destroyed.
|
||||
template <class Derived>
|
||||
class Handle {
|
||||
public:
|
||||
Handle() noexcept = default;
|
||||
Handle(const Handle&) = delete;
|
||||
Handle& operator=(const Handle&) = delete;
|
||||
~Handle() { reset(); }
|
||||
|
||||
/// GL object name, or 0 when the handle is empty.
|
||||
GLuint id() const noexcept { return id_; }
|
||||
explicit operator bool() const noexcept { return id_ != 0; }
|
||||
|
||||
/// Deletes the GL object, if any, and empties the handle.
|
||||
void reset() noexcept {
|
||||
if (id_ != 0) {
|
||||
Derived::destroy(id_);
|
||||
id_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
Handle(Handle&& other) noexcept : id_(other.id_) { other.id_ = 0; }
|
||||
Handle& operator=(Handle&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
id_ = other.id_;
|
||||
other.id_ = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
GLuint id_ = 0;
|
||||
};
|
||||
|
||||
/// Vertex buffer object. All fallible operations report through `Status`;
|
||||
/// the void ones assume that `plot::gl::load()` ran and a context is current.
|
||||
class Buffer final : public Handle<Buffer> {
|
||||
public:
|
||||
/// Allocates a buffer name. Fails with `Unsupported` when the entry points
|
||||
/// are not resolved yet and `GLFailure` when the driver returns name 0.
|
||||
bool create(Status* st = nullptr) noexcept;
|
||||
|
||||
void bind(GLenum target = GL_ARRAY_BUFFER) const noexcept;
|
||||
|
||||
/// Replaces the whole store with `bytes` bytes of `data` (`data` may be null
|
||||
/// to allocate without initialising).
|
||||
void upload(const void* data, usize bytes, GLenum usage) const noexcept;
|
||||
|
||||
/// Overwrites `bytes` bytes starting at `offset`; the store must already be
|
||||
/// at least `offset + bytes` large.
|
||||
void upload_sub(const void* data, usize bytes, usize offset) const noexcept;
|
||||
|
||||
/// Exposed for the CRTP base. Deleting an unknown name is a GL no-op.
|
||||
static void destroy(GLuint id) noexcept;
|
||||
};
|
||||
|
||||
/// Vertex array object holding the attribute layout of the bound buffer.
|
||||
class VertexArray final : public Handle<VertexArray> {
|
||||
public:
|
||||
bool create(Status* st = nullptr) noexcept;
|
||||
|
||||
void bind() const noexcept;
|
||||
|
||||
/// Enables attribute `index` as `components` floats read from the currently
|
||||
/// bound `GL_ARRAY_BUFFER`, advancing `stride` bytes per vertex and starting
|
||||
/// `offset` bytes into it. Requires this vertex array to be bound.
|
||||
void attrib(u32 index, i32 components, usize stride, usize offset) const noexcept;
|
||||
|
||||
/// Exposed for the CRTP base.
|
||||
static void destroy(GLuint id) noexcept;
|
||||
};
|
||||
|
||||
/// Linked shader program. Constructed only through `from_source()`.
|
||||
class Program final : public Handle<Program> {
|
||||
public:
|
||||
/// Compiles and links a vertex/fragment source pair. On failure the returned
|
||||
/// program is empty and `st` (when non-null) carries `GLFailure` plus the
|
||||
/// driver's info log, or `Unsupported` when `load()` was never called.
|
||||
static Program from_source(const char* vertex_src, const char* fragment_src,
|
||||
Status* st = nullptr) noexcept;
|
||||
|
||||
void use() const noexcept;
|
||||
|
||||
/// Sets a float uniform; a name that is not an active uniform of this program
|
||||
/// is a silent no-op.
|
||||
void set_uniform(const char* name, f32 value) const noexcept;
|
||||
|
||||
/// Sets a two-component float uniform.
|
||||
void set_uniform(const char* name, Vec2 value) const noexcept;
|
||||
|
||||
/// Exposed for the CRTP base.
|
||||
static void destroy(GLuint id) noexcept;
|
||||
};
|
||||
|
||||
/// 2D texture. Creation only allocates storage; `upload_*` replaces the whole
|
||||
/// image (`glTexImage2D`), which is what the plot pipelines need for atlases
|
||||
/// and one-shot uploads.
|
||||
class Texture final : public Handle<Texture> {
|
||||
public:
|
||||
/// Allocates `width` x `height` 8-bit RGBA storage (`GL_RGBA8`), linear
|
||||
/// filtering, clamped edges.
|
||||
bool create_rgba(u32 width, u32 height, Status* st = nullptr) noexcept;
|
||||
|
||||
/// Allocates `width` x `height` single-channel 8-bit storage (`GL_R8`) for
|
||||
/// coverage masks and narrow series data.
|
||||
bool create_r8(u32 width, u32 height, Status* st = nullptr) noexcept;
|
||||
|
||||
/// Replaces the whole image with `width * height * 4` bytes of RGBA data.
|
||||
void upload_rgba(u32 width, u32 height, const void* pixels) const noexcept;
|
||||
|
||||
/// Replaces the whole image with `width * height` bytes of single-channel
|
||||
/// data (row padding is disabled, so any width is valid).
|
||||
void upload_r8(u32 width, u32 height, const void* pixels) const noexcept;
|
||||
|
||||
/// Binds to `unit` (e.g. `GL_TEXTURE0`).
|
||||
void bind(GLenum unit = GL_TEXTURE0) const noexcept;
|
||||
|
||||
void set_filter(GLenum min, GLenum mag) const noexcept;
|
||||
|
||||
/// Exposed for the CRTP base.
|
||||
static void destroy(GLuint id) noexcept;
|
||||
};
|
||||
|
||||
/// Framebuffer object, the render target of the off-screen paths.
|
||||
class Framebuffer final : public Handle<Framebuffer> {
|
||||
public:
|
||||
bool create(Status* st = nullptr) noexcept;
|
||||
|
||||
void bind(GLenum target = GL_FRAMEBUFFER) const noexcept;
|
||||
|
||||
/// Attaches `texture`'s level 0 to `GL_COLOR_ATTACHMENT0`.
|
||||
void attach_color(const Texture& texture) const noexcept;
|
||||
|
||||
/// True when the attachment set is `GL_FRAMEBUFFER_COMPLETE`.
|
||||
bool complete() const noexcept;
|
||||
|
||||
/// Exposed for the CRTP base.
|
||||
static void destroy(GLuint id) noexcept;
|
||||
};
|
||||
|
||||
/// Move-only GL sync object (`GLsync`, GL 3.2) used to guard reuse of buffers
|
||||
/// that the GPU may still be reading.
|
||||
class Fence {
|
||||
public:
|
||||
Fence() noexcept = default;
|
||||
Fence(const Fence&) = delete;
|
||||
Fence& operator=(const Fence&) = delete;
|
||||
Fence(Fence&& other) noexcept : id_(other.id_) { other.id_ = nullptr; }
|
||||
Fence& operator=(Fence&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
id_ = other.id_;
|
||||
other.id_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
/// Deleting no fence is a no-op, so an empty fence is always safe.
|
||||
~Fence() { reset(); }
|
||||
|
||||
/// Inserts a fence in the GL command stream.
|
||||
bool create(Status* st = nullptr) noexcept;
|
||||
|
||||
/// Waits up to `timeout_ns` nanoseconds for the fence to be signalled,
|
||||
/// flushing the command stream first. True when it signalled in time.
|
||||
bool wait(u64 timeout_ns) const noexcept;
|
||||
|
||||
GLsync id() const noexcept { return id_; }
|
||||
explicit operator bool() const noexcept { return id_ != nullptr; }
|
||||
|
||||
void reset() noexcept;
|
||||
|
||||
private:
|
||||
GLsync id_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace plot::gl
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <plot/gl/api.hpp>
|
||||
|
||||
namespace plot::gl {
|
||||
|
||||
/// Entry point resolver supplied by the caller, normally the *GetProcAddress
|
||||
/// of the API that owns the current context: `eglGetProcAddress`,
|
||||
/// `glXGetProcAddressARB` or `wglGetProcAddress`.
|
||||
using ProcLoader = void* (*)(const char*);
|
||||
|
||||
/// Resolves every entry point of `PLOT_GL_FUNCS` (see api.hpp) into the global
|
||||
/// table declared there, so the rest of the library can call `gl*` directly.
|
||||
///
|
||||
/// Returns false when any required name cannot be found; entries that did
|
||||
/// resolve keep their pointers, so a failed load() never breaks a table that
|
||||
/// was already usable. `loader` is consulted first; when it is null, or returns
|
||||
/// null for a name, the fallback is `dlsym(RTLD_DEFAULT, name)` and then
|
||||
/// `dlopen("libGL.so.1")` + `dlsym`. The fallback makes the library usable
|
||||
/// without a caller-supplied resolver, but only a context-aware resolver is
|
||||
/// guaranteed to return pointers valid for the *current* context — pass one
|
||||
/// whenever a context is current.
|
||||
///
|
||||
/// A load() attempt invalidates the caps() cache. The table is process-global
|
||||
/// and is not synchronised, so call from the thread that owns the current
|
||||
/// context.
|
||||
bool load(ProcLoader loader = nullptr) noexcept;
|
||||
|
||||
} // namespace plot::gl
|
||||
Reference in new issue
Block a user