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
@@ -7,6 +7,8 @@ include(plot_options)
|
|||||||
add_library(plot_core
|
add_library(plot_core
|
||||||
src/core/status.cpp
|
src/core/status.cpp
|
||||||
src/math/transform.cpp
|
src/math/transform.cpp
|
||||||
|
src/gl/loader.cpp
|
||||||
|
src/gl/handle.cpp
|
||||||
)
|
)
|
||||||
add_library(plot::core ALIAS plot_core)
|
add_library(plot::core ALIAS plot_core)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
#include <plot/gl/handle.hpp>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
namespace plot::gl {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char* kNoLoader = "plot::gl::load() has not been called";
|
||||||
|
|
||||||
|
/// `Status::message` is a non-owning `const char*`, so the text of a failure
|
||||||
|
/// lives here: valid until the next failure reported on the same thread.
|
||||||
|
thread_local char g_message[1024];
|
||||||
|
|
||||||
|
bool fail(Status* st, ErrorCode code, const char* text) noexcept {
|
||||||
|
if (st != nullptr) {
|
||||||
|
std::snprintf(g_message, sizeof g_message, "%s", text);
|
||||||
|
st->code = code;
|
||||||
|
st->message = g_message;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// As `fail()`, but appends driver text (a shader or program info log).
|
||||||
|
bool fail_log(Status* st, const char* what, const char* log) noexcept {
|
||||||
|
if (st != nullptr) {
|
||||||
|
std::snprintf(g_message, sizeof g_message, "%s: %s", what, log != nullptr ? log : "");
|
||||||
|
st->code = ErrorCode::GLFailure;
|
||||||
|
st->message = g_message;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compiles one stage and returns its shader name, or 0 on failure (the reason
|
||||||
|
/// is already in `st`). The shader is deleted if it fails to compile.
|
||||||
|
GLuint compile_shader(GLenum stage, const char* source, const char* what, Status* st) noexcept {
|
||||||
|
const GLuint shader = glCreateShader(stage);
|
||||||
|
if (shader == 0) {
|
||||||
|
fail(st, ErrorCode::GLFailure, "glCreateShader returned 0");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == 0) {
|
||||||
|
char log[1024] = {};
|
||||||
|
GLsizei written = 0;
|
||||||
|
glGetShaderInfoLog(shader, static_cast<GLsizei>(sizeof log - 1), &written, log);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
fail_log(st, what, log);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocates 2D storage with the sampling state every texture here shares:
|
||||||
|
/// linear filtering, clamped edges. Returns the name, or 0 if the driver
|
||||||
|
/// handed out none.
|
||||||
|
GLuint alloc_texture(GLenum internal_format, GLenum format, u32 width, u32 height) noexcept {
|
||||||
|
GLuint name = 0;
|
||||||
|
glGenTextures(1, &name);
|
||||||
|
if (name == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
glBindTexture(GL_TEXTURE_2D, name);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internal_format), static_cast<GLsizei>(width),
|
||||||
|
static_cast<GLsizei>(height), 0, format, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------- Buffer
|
||||||
|
|
||||||
|
bool Buffer::create(Status* st) noexcept {
|
||||||
|
if (glGenBuffers == nullptr) {
|
||||||
|
return fail(st, ErrorCode::Unsupported, kNoLoader);
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
GLuint name = 0;
|
||||||
|
glGenBuffers(1, &name);
|
||||||
|
if (name == 0) {
|
||||||
|
return fail(st, ErrorCode::GLFailure, "glGenBuffers returned 0");
|
||||||
|
}
|
||||||
|
id_ = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Buffer::bind(GLenum target) const noexcept { glBindBuffer(target, id_); }
|
||||||
|
|
||||||
|
void Buffer::upload(const void* data, usize bytes, GLenum usage) const noexcept {
|
||||||
|
bind();
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(bytes), data, usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Buffer::upload_sub(const void* data, usize bytes, usize offset) const noexcept {
|
||||||
|
bind();
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, static_cast<GLintptr>(offset), static_cast<GLsizeiptr>(bytes),
|
||||||
|
data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Buffer::destroy(GLuint id) noexcept { glDeleteBuffers(1, &id); }
|
||||||
|
|
||||||
|
// -------------------------------------------------------------- VertexArray
|
||||||
|
|
||||||
|
bool VertexArray::create(Status* st) noexcept {
|
||||||
|
if (glGenVertexArrays == nullptr) {
|
||||||
|
return fail(st, ErrorCode::Unsupported, kNoLoader);
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
GLuint name = 0;
|
||||||
|
glGenVertexArrays(1, &name);
|
||||||
|
if (name == 0) {
|
||||||
|
return fail(st, ErrorCode::GLFailure, "glGenVertexArrays returned 0");
|
||||||
|
}
|
||||||
|
id_ = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VertexArray::bind() const noexcept { glBindVertexArray(id_); }
|
||||||
|
|
||||||
|
void VertexArray::attrib(u32 index, i32 components, usize stride, usize offset) const noexcept {
|
||||||
|
const GLuint attrib = static_cast<GLuint>(index);
|
||||||
|
glEnableVertexAttribArray(attrib);
|
||||||
|
glVertexAttribPointer(attrib, static_cast<GLint>(components), GL_FLOAT, GL_FALSE,
|
||||||
|
static_cast<GLsizei>(stride),
|
||||||
|
reinterpret_cast<const void*>(static_cast<std::uintptr_t>(offset)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void VertexArray::destroy(GLuint id) noexcept { glDeleteVertexArrays(1, &id); }
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ Program
|
||||||
|
|
||||||
|
Program Program::from_source(const char* vertex_src, const char* fragment_src,
|
||||||
|
Status* st) noexcept {
|
||||||
|
Program program;
|
||||||
|
if (glCreateShader == nullptr || glCreateProgram == nullptr) {
|
||||||
|
fail(st, ErrorCode::Unsupported, kNoLoader);
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
if (vertex_src == nullptr || fragment_src == nullptr) {
|
||||||
|
fail(st, ErrorCode::InvalidArgument, "shader source must not be null");
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint vertex = compile_shader(GL_VERTEX_SHADER, vertex_src, "vertex shader failed", st);
|
||||||
|
if (vertex == 0) {
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
const GLuint fragment =
|
||||||
|
compile_shader(GL_FRAGMENT_SHADER, fragment_src, "fragment shader failed", st);
|
||||||
|
if (fragment == 0) {
|
||||||
|
glDeleteShader(vertex);
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint name = glCreateProgram();
|
||||||
|
if (name == 0) {
|
||||||
|
glDeleteShader(vertex);
|
||||||
|
glDeleteShader(fragment);
|
||||||
|
fail(st, ErrorCode::GLFailure, "glCreateProgram returned 0");
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
glAttachShader(name, vertex);
|
||||||
|
glAttachShader(name, fragment);
|
||||||
|
glLinkProgram(name);
|
||||||
|
// The program owns the linked code from here on, so the shader objects can
|
||||||
|
// be detached and deleted.
|
||||||
|
glDetachShader(name, vertex);
|
||||||
|
glDetachShader(name, fragment);
|
||||||
|
glDeleteShader(vertex);
|
||||||
|
glDeleteShader(fragment);
|
||||||
|
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(name, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == 0) {
|
||||||
|
char log[1024] = {};
|
||||||
|
GLsizei written = 0;
|
||||||
|
glGetProgramInfoLog(name, static_cast<GLsizei>(sizeof log - 1), &written, log);
|
||||||
|
glDeleteProgram(name);
|
||||||
|
fail_log(st, "program link failed", log);
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
program.id_ = name;
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Program::use() const noexcept { glUseProgram(id_); }
|
||||||
|
|
||||||
|
void Program::set_uniform(const char* name, f32 value) const noexcept {
|
||||||
|
if (id_ == 0 || name == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const GLint location = glGetUniformLocation(id_, name);
|
||||||
|
if (location >= 0) {
|
||||||
|
glUniform1f(location, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Program::set_uniform(const char* name, Vec2 value) const noexcept {
|
||||||
|
if (id_ == 0 || name == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const GLint location = glGetUniformLocation(id_, name);
|
||||||
|
if (location >= 0) {
|
||||||
|
glUniform2f(location, value.x, value.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Program::destroy(GLuint id) noexcept { glDeleteProgram(id); }
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ Texture
|
||||||
|
|
||||||
|
bool Texture::create_rgba(u32 width, u32 height, Status* st) noexcept {
|
||||||
|
if (glGenTextures == nullptr) {
|
||||||
|
return fail(st, ErrorCode::Unsupported, kNoLoader);
|
||||||
|
}
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
return fail(st, ErrorCode::InvalidArgument, "texture size must be non-zero");
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
const GLuint name = alloc_texture(GL_RGBA8, GL_RGBA, width, height);
|
||||||
|
if (name == 0) {
|
||||||
|
return fail(st, ErrorCode::GLFailure, "glGenTextures returned 0");
|
||||||
|
}
|
||||||
|
id_ = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Texture::create_r8(u32 width, u32 height, Status* st) noexcept {
|
||||||
|
if (glGenTextures == nullptr) {
|
||||||
|
return fail(st, ErrorCode::Unsupported, kNoLoader);
|
||||||
|
}
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
return fail(st, ErrorCode::InvalidArgument, "texture size must be non-zero");
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
const GLuint name = alloc_texture(GL_R8, GL_RED, width, height);
|
||||||
|
if (name == 0) {
|
||||||
|
return fail(st, ErrorCode::GLFailure, "glGenTextures returned 0");
|
||||||
|
}
|
||||||
|
id_ = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::upload_rgba(u32 width, u32 height, const void* pixels) const noexcept {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, id_);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(GL_RGBA8), static_cast<GLsizei>(width),
|
||||||
|
static_cast<GLsizei>(height), 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::upload_r8(u32 width, u32 height, const void* pixels) const noexcept {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, id_);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // single-channel rows have no padding
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(GL_R8), static_cast<GLsizei>(width),
|
||||||
|
static_cast<GLsizei>(height), 0, GL_RED, GL_UNSIGNED_BYTE, pixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::bind(GLenum unit) const noexcept {
|
||||||
|
glActiveTexture(unit);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::set_filter(GLenum min, GLenum mag) const noexcept {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, id_);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, static_cast<GLint>(min));
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, static_cast<GLint>(mag));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::destroy(GLuint id) noexcept { glDeleteTextures(1, &id); }
|
||||||
|
|
||||||
|
// -------------------------------------------------------------- Framebuffer
|
||||||
|
|
||||||
|
bool Framebuffer::create(Status* st) noexcept {
|
||||||
|
if (glGenFramebuffers == nullptr) {
|
||||||
|
return fail(st, ErrorCode::Unsupported, kNoLoader);
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
GLuint name = 0;
|
||||||
|
glGenFramebuffers(1, &name);
|
||||||
|
if (name == 0) {
|
||||||
|
return fail(st, ErrorCode::GLFailure, "glGenFramebuffers returned 0");
|
||||||
|
}
|
||||||
|
id_ = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Framebuffer::bind(GLenum target) const noexcept { glBindFramebuffer(target, id_); }
|
||||||
|
|
||||||
|
void Framebuffer::attach_color(const Texture& texture) const noexcept {
|
||||||
|
bind();
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.id(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Framebuffer::complete() const noexcept {
|
||||||
|
bind();
|
||||||
|
return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Framebuffer::destroy(GLuint id) noexcept { glDeleteFramebuffers(1, &id); }
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------- Fence
|
||||||
|
|
||||||
|
bool Fence::create(Status* st) noexcept {
|
||||||
|
if (glFenceSync == nullptr) {
|
||||||
|
return fail(st, ErrorCode::Unsupported, kNoLoader);
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
const GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
if (sync == nullptr) {
|
||||||
|
return fail(st, ErrorCode::GLFailure, "glFenceSync returned no sync object");
|
||||||
|
}
|
||||||
|
id_ = sync;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Fence::wait(u64 timeout_ns) const noexcept {
|
||||||
|
if (id_ == nullptr) {
|
||||||
|
return true; // nothing in flight
|
||||||
|
}
|
||||||
|
const GLenum result = glClientWaitSync(id_, GL_SYNC_FLUSH_COMMANDS_BIT, timeout_ns);
|
||||||
|
return result == GL_ALREADY_SIGNALED || result == GL_CONDITION_SATISFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Fence::reset() noexcept {
|
||||||
|
if (id_ != nullptr) {
|
||||||
|
glDeleteSync(id_);
|
||||||
|
id_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace plot::gl
|
||||||
@@ -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
|
||||||
+27
-1
@@ -1,8 +1,34 @@
|
|||||||
|
# EGL powers the surfaceless headless context the GL tests assert pixels
|
||||||
|
# against. It is a *test-only* dependency: plot_core never links or includes it,
|
||||||
|
# so a machine without EGL can still build the library (-DPLOT_BUILD_TESTS=OFF).
|
||||||
|
# Prefer CMake's own module, fall back to pkg-config, and fail loudly if neither
|
||||||
|
# can supply EGL rather than silently dropping the GL tests.
|
||||||
|
find_package(EGL QUIET)
|
||||||
|
if(NOT EGL_FOUND)
|
||||||
|
find_package(PkgConfig QUIET)
|
||||||
|
if(PkgConfig_FOUND)
|
||||||
|
pkg_check_modules(EGL_PC QUIET IMPORTED_TARGET egl)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET EGL::EGL)
|
||||||
|
set(plot_egl_library EGL::EGL)
|
||||||
|
elseif(TARGET PkgConfig::EGL_PC)
|
||||||
|
set(plot_egl_library PkgConfig::EGL_PC)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"plot: PLOT_BUILD_TESTS=ON requires EGL (EGL/egl.h plus libEGL) for the headless "
|
||||||
|
"OpenGL context used by the GL tests. Install it (e.g. libegl-dev, libegl1-mesa-dev "
|
||||||
|
"or mesa-libEGL-devel) or configure with -DPLOT_BUILD_TESTS=OFF.")
|
||||||
|
endif()
|
||||||
|
|
||||||
add_executable(plot_tests
|
add_executable(plot_tests
|
||||||
main.cpp
|
main.cpp
|
||||||
test_transform.cpp
|
test_transform.cpp
|
||||||
|
test_gl_context.cpp
|
||||||
|
egl_headless.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(plot_tests PRIVATE plot::core)
|
target_link_libraries(plot_tests PRIVATE plot::core ${plot_egl_library})
|
||||||
plot_apply_warnings(plot_tests)
|
plot_apply_warnings(plot_tests)
|
||||||
|
|
||||||
add_test(NAME plot_tests COMMAND plot_tests)
|
add_test(NAME plot_tests COMMAND plot_tests)
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
#include "egl_headless.hpp"
|
||||||
|
|
||||||
|
#include <plot/gl/api.hpp>
|
||||||
|
#include <plot/gl/loader.hpp>
|
||||||
|
|
||||||
|
#include <EGL/egl.h>
|
||||||
|
#include <EGL/eglext.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace plottest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// `eglGetPlatformDisplayEXT`, declared locally so the harness does not depend
|
||||||
|
/// on the exact spelling of the EGL_EXT_platform_base typedef.
|
||||||
|
using GetPlatformDisplayEXT = EGLDisplay (*)(EGLenum platform, void* native_display,
|
||||||
|
const EGLint* attrib_list);
|
||||||
|
|
||||||
|
/// Keeps the text of the last EGL failure alive for the returned Status.
|
||||||
|
thread_local char g_message[512];
|
||||||
|
|
||||||
|
/// Records the pending EGL error in `g_message`, echoes it to stderr so a
|
||||||
|
/// failing CTest run shows the driver's own error code, and returns false.
|
||||||
|
bool fail(plot::Status* st, const char* what) noexcept {
|
||||||
|
const EGLint error = eglGetError();
|
||||||
|
std::snprintf(g_message, sizeof g_message, "%s (EGL error 0x%04X)", what,
|
||||||
|
static_cast<unsigned>(error));
|
||||||
|
std::fprintf(stderr, "plot_tests: headless EGL: %s\n", g_message);
|
||||||
|
if (st != nullptr) {
|
||||||
|
st->code = plot::ErrorCode::GLFailure;
|
||||||
|
st->message = g_message;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
HeadlessContext::HeadlessContext(u32 w, u32 h) noexcept : width(w), height(h) {}
|
||||||
|
|
||||||
|
HeadlessContext::~HeadlessContext() { destroy(); }
|
||||||
|
|
||||||
|
bool HeadlessContext::create(plot::Status* st) noexcept {
|
||||||
|
if (framebuffer_.id() != 0) {
|
||||||
|
return true; // already created
|
||||||
|
}
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
if (st != nullptr) {
|
||||||
|
st->code = plot::ErrorCode::InvalidArgument;
|
||||||
|
st->message = "headless context size must be non-zero";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto abort = [&](const char* what) {
|
||||||
|
const bool reported = fail(st, what);
|
||||||
|
destroy();
|
||||||
|
return reported;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Prefer the surfaceless platform: no window system at all. Fall back to
|
||||||
|
// the default display (still surfaceless when it can be), and only then
|
||||||
|
// accept a pbuffer surface.
|
||||||
|
const GetPlatformDisplayEXT get_platform_display = reinterpret_cast<GetPlatformDisplayEXT>(
|
||||||
|
eglGetProcAddress("eglGetPlatformDisplayEXT"));
|
||||||
|
EGLDisplay display = EGL_NO_DISPLAY;
|
||||||
|
if (get_platform_display != nullptr) {
|
||||||
|
display = get_platform_display(EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY, nullptr);
|
||||||
|
}
|
||||||
|
if (display == EGL_NO_DISPLAY) {
|
||||||
|
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||||
|
}
|
||||||
|
if (display == EGL_NO_DISPLAY) {
|
||||||
|
return abort("eglGetPlatformDisplayEXT(EGL_PLATFORM_SURFACELESS_MESA) and eglGetDisplay failed");
|
||||||
|
}
|
||||||
|
display_ = display;
|
||||||
|
|
||||||
|
// 2. Initialise the display for the desktop GL API.
|
||||||
|
EGLint egl_major = 0;
|
||||||
|
EGLint egl_minor = 0;
|
||||||
|
if (eglInitialize(display, &egl_major, &egl_minor) != EGL_TRUE) {
|
||||||
|
return abort("eglInitialize failed");
|
||||||
|
}
|
||||||
|
if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) {
|
||||||
|
return abort("eglBindAPI(EGL_OPENGL_API) failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. An RGBA8 config that can back an OpenGL context.
|
||||||
|
const EGLint config_attribs[] = {
|
||||||
|
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
||||||
|
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8,
|
||||||
|
EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
|
||||||
|
EGL_DEPTH_SIZE, 0, EGL_NONE,
|
||||||
|
};
|
||||||
|
EGLConfig config = nullptr;
|
||||||
|
EGLint config_count = 0;
|
||||||
|
if (eglChooseConfig(display, config_attribs, &config, 1, &config_count) != EGL_TRUE ||
|
||||||
|
config_count == 0) {
|
||||||
|
return abort("no EGL config offers 8-bit RGBA with EGL_OPENGL_BIT");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. OpenGL 3.3 core, the baseline the library targets.
|
||||||
|
const EGLint context_attribs[] = {
|
||||||
|
EGL_CONTEXT_MAJOR_VERSION,
|
||||||
|
3,
|
||||||
|
EGL_CONTEXT_MINOR_VERSION,
|
||||||
|
3,
|
||||||
|
EGL_CONTEXT_OPENGL_PROFILE_MASK,
|
||||||
|
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR,
|
||||||
|
EGL_NONE,
|
||||||
|
};
|
||||||
|
const EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attribs);
|
||||||
|
if (context == EGL_NO_CONTEXT) {
|
||||||
|
return abort("eglCreateContext(OpenGL 3.3 core) failed");
|
||||||
|
}
|
||||||
|
context_ = context;
|
||||||
|
|
||||||
|
// 5. Surfaceless current context first; a 1x1 pbuffer only if the driver
|
||||||
|
// lacks EGL_KHR_surfaceless_context. PLOT_HEADLESS_FORCE_PBUFFER=1 skips
|
||||||
|
// straight to the fallback, so that path can be verified somewhere the
|
||||||
|
// surfaceless path would otherwise always win.
|
||||||
|
const bool force_pbuffer = std::getenv("PLOT_HEADLESS_FORCE_PBUFFER") != nullptr;
|
||||||
|
if (!force_pbuffer &&
|
||||||
|
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context) == EGL_TRUE) {
|
||||||
|
egl_path = "egl surfaceless";
|
||||||
|
} else {
|
||||||
|
const EGLint pbuffer_attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};
|
||||||
|
const EGLSurface surface = eglCreatePbufferSurface(display, config, pbuffer_attribs);
|
||||||
|
if (surface == EGL_NO_SURFACE ||
|
||||||
|
eglMakeCurrent(display, surface, surface, context) != EGL_TRUE) {
|
||||||
|
return abort("neither a surfaceless context nor a 1x1 pbuffer surface could be made current");
|
||||||
|
}
|
||||||
|
surface_ = surface;
|
||||||
|
egl_path = "egl pbuffer";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Resolve the entry points against the context that is now current.
|
||||||
|
if (!plot::gl::load(reinterpret_cast<plot::gl::ProcLoader>(eglGetProcAddress))) {
|
||||||
|
if (st != nullptr) {
|
||||||
|
st->code = plot::ErrorCode::GLFailure;
|
||||||
|
st->message = "plot::gl::load(eglGetProcAddress) could not resolve every entry point";
|
||||||
|
}
|
||||||
|
std::fprintf(stderr, "plot_tests: headless EGL: %s\n", st != nullptr ? st->message : "");
|
||||||
|
destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. The off-screen target: an RGBA8 texture on colour attachment 0.
|
||||||
|
plot::Status gl_status;
|
||||||
|
if (!color_.create_rgba(width, height, &gl_status) ||
|
||||||
|
!framebuffer_.create(&gl_status)) {
|
||||||
|
if (st != nullptr) {
|
||||||
|
*st = gl_status;
|
||||||
|
}
|
||||||
|
std::fprintf(stderr, "plot_tests: headless EGL: %s\n",
|
||||||
|
gl_status.message != nullptr ? gl_status.message : "framebuffer setup failed");
|
||||||
|
destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
framebuffer_.attach_color(color_);
|
||||||
|
if (!framebuffer_.complete()) {
|
||||||
|
std::fprintf(stderr, "plot_tests: headless EGL: framebuffer is not complete\n");
|
||||||
|
if (st != nullptr) {
|
||||||
|
st->code = plot::ErrorCode::GLFailure;
|
||||||
|
st->message = "framebuffer is not complete";
|
||||||
|
}
|
||||||
|
destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadlessContext::destroy() noexcept {
|
||||||
|
// GL names belong to the context that created them, so drop them while it is
|
||||||
|
// still current.
|
||||||
|
framebuffer_.reset();
|
||||||
|
color_.reset();
|
||||||
|
|
||||||
|
if (display_ != nullptr) {
|
||||||
|
const EGLDisplay display = static_cast<EGLDisplay>(display_);
|
||||||
|
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||||
|
if (context_ != nullptr) {
|
||||||
|
eglDestroyContext(display, static_cast<EGLContext>(context_));
|
||||||
|
}
|
||||||
|
if (surface_ != nullptr) {
|
||||||
|
eglDestroySurface(display, static_cast<EGLSurface>(surface_));
|
||||||
|
}
|
||||||
|
eglTerminate(display);
|
||||||
|
}
|
||||||
|
display_ = nullptr;
|
||||||
|
context_ = nullptr;
|
||||||
|
surface_ = nullptr;
|
||||||
|
egl_path = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HeadlessContext::read_pixel(u32 x, u32 y, std::uint8_t rgba[4]) const noexcept {
|
||||||
|
if (framebuffer_.id() == 0 || x >= width || y >= height) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
framebuffer_.bind();
|
||||||
|
plot::gl::glReadPixels(static_cast<plot::gl::GLint>(x), static_cast<plot::gl::GLint>(y), 1, 1,
|
||||||
|
plot::gl::GL_RGBA, plot::gl::GL_UNSIGNED_BYTE, rgba);
|
||||||
|
return plot::gl::glGetError() == plot::gl::GL_NO_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace plottest
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <plot/core/status.hpp>
|
||||||
|
#include <plot/core/types.hpp>
|
||||||
|
#include <plot/gl/handle.hpp>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace plottest {
|
||||||
|
|
||||||
|
using plot::u32;
|
||||||
|
|
||||||
|
/// A surfaceless EGL context plus an off-screen RGBA framebuffer, so renderer
|
||||||
|
/// output can be asserted pixel by pixel without a window or a display server.
|
||||||
|
///
|
||||||
|
/// Test-only: `plot_core` neither links nor includes EGL.
|
||||||
|
class HeadlessContext {
|
||||||
|
public:
|
||||||
|
explicit HeadlessContext(u32 w = 256, u32 h = 256) noexcept;
|
||||||
|
~HeadlessContext();
|
||||||
|
|
||||||
|
HeadlessContext(const HeadlessContext&) = delete;
|
||||||
|
HeadlessContext& operator=(const HeadlessContext&) = delete;
|
||||||
|
|
||||||
|
/// Makes an OpenGL 3.3 core context current on this thread, resolves the GL
|
||||||
|
/// entry points against it (`plot::gl::load(eglGetProcAddress)`) and
|
||||||
|
/// allocates the colour attachment. Idempotent. On failure `st` (when
|
||||||
|
/// non-null) carries the reason, every resource already created is released,
|
||||||
|
/// and the error is also printed to stderr so a failing CTest run is
|
||||||
|
/// diagnosable.
|
||||||
|
bool create(plot::Status* st = nullptr) noexcept;
|
||||||
|
|
||||||
|
/// Releases the framebuffer, the GL context and the EGL display. Safe to call
|
||||||
|
/// more than once, and to call after a failed create().
|
||||||
|
void destroy() noexcept;
|
||||||
|
|
||||||
|
/// Binds the off-screen target and reads one RGBA8 pixel, in GL's
|
||||||
|
/// origin-at-the-bottom-left convention. False when it cannot be read.
|
||||||
|
bool read_pixel(u32 x, u32 y, std::uint8_t rgba[4]) const noexcept;
|
||||||
|
|
||||||
|
u32 width = 0;
|
||||||
|
u32 height = 0;
|
||||||
|
/// The headless path create() selected ("egl surfaceless" or "egl pbuffer");
|
||||||
|
/// empty before a successful create() and after destroy(). Setting
|
||||||
|
/// PLOT_HEADLESS_FORCE_PBUFFER in the environment skips the surfaceless
|
||||||
|
/// attempt, so the pbuffer fallback can be exercised on a driver that does
|
||||||
|
/// support surfaceless contexts.
|
||||||
|
const char* egl_path = "";
|
||||||
|
|
||||||
|
private:
|
||||||
|
plot::gl::Texture color_;
|
||||||
|
plot::gl::Framebuffer framebuffer_;
|
||||||
|
void* display_ = nullptr; // EGLDisplay
|
||||||
|
void* context_ = nullptr; // EGLContext
|
||||||
|
void* surface_ = nullptr; // EGLSurface, null on the surfaceless path
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace plottest
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
#include "harness.hpp"
|
||||||
|
#include "egl_headless.hpp"
|
||||||
|
|
||||||
|
#include <EGL/egl.h> // eglGetProcAddress: the context-aware entry point resolver
|
||||||
|
|
||||||
|
#include <plot/gl/caps.hpp>
|
||||||
|
#include <plot/gl/handle.hpp>
|
||||||
|
#include <plot/gl/loader.hpp>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// `eglGetProcAddress` is the context-aware resolver of the EGL context: in a
|
||||||
|
/// surfaceless context there is no GLX/WGL dispatch to fall back on, so this is
|
||||||
|
/// the path that must work.
|
||||||
|
plot::gl::ProcLoader egl_loader() noexcept {
|
||||||
|
return reinterpret_cast<plot::gl::ProcLoader>(eglGetProcAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr const char* kVertexSrc = R"(#version 330 core
|
||||||
|
layout(location = 0) in vec2 a_pos;
|
||||||
|
uniform vec2 u_offset;
|
||||||
|
void main() { gl_Position = vec4(a_pos + u_offset, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFragmentSrc = R"(#version 330 core
|
||||||
|
uniform float u_alpha;
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(1.0, 0.0, 0.0, u_alpha); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
/// Deliberately unparseable GLSL, to exercise the failure path of
|
||||||
|
/// `Program::from_source`.
|
||||||
|
constexpr const char* kBrokenFragmentSrc = R"(#version 330 core
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = this is not glsl; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Must stay the first GL case in this translation unit: `caps()` caches after
|
||||||
|
// the first successful query, so "before any load()" is observable only once.
|
||||||
|
PLOT_TEST(caps_before_load_is_zeroed) {
|
||||||
|
const plot::gl::Caps& caps = plot::gl::caps();
|
||||||
|
PLOT_CHECK(caps.major == 0 && caps.minor == 0);
|
||||||
|
PLOT_CHECK(!caps.compute && !caps.buffer_storage && !caps.persistent_map);
|
||||||
|
PLOT_CHECK(!caps.core_profile);
|
||||||
|
PLOT_CHECK(caps.renderer != nullptr && caps.renderer[0] == '\0');
|
||||||
|
PLOT_CHECK(caps.vendor != nullptr && caps.vendor[0] == '\0');
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(headless_context_provides_gl33) {
|
||||||
|
plottest::HeadlessContext ctx;
|
||||||
|
plot::Status st;
|
||||||
|
PLOT_CHECK(ctx.create(&st)); // fails loudly if surfaceless EGL is unavailable
|
||||||
|
|
||||||
|
// Primary path: resolve through the current context.
|
||||||
|
PLOT_CHECK(plot::gl::load(egl_loader()));
|
||||||
|
|
||||||
|
const plot::gl::Caps& caps = plot::gl::caps();
|
||||||
|
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
|
||||||
|
const char* version = reinterpret_cast<const char*>(plot::gl::glGetString(plot::gl::GL_VERSION));
|
||||||
|
PLOT_CHECK(version != nullptr);
|
||||||
|
const int major = caps.major;
|
||||||
|
const int minor = caps.minor;
|
||||||
|
std::printf("[info] GL %d.%d, renderer \"%s\", vendor \"%s\", %s profile, EGL path \"%s\"\n",
|
||||||
|
major, minor, caps.renderer, caps.vendor, caps.core_profile ? "core" : "compat",
|
||||||
|
ctx.egl_path);
|
||||||
|
std::printf("[info] caps: compute=%d buffer_storage=%d persistent_map=%d core_profile=%d\n",
|
||||||
|
caps.compute ? 1 : 0, caps.buffer_storage ? 1 : 0, caps.persistent_map ? 1 : 0,
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
PLOT_CHECK(plot::gl::load(egl_loader()));
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(headless_framebuffer_reads_back_clear_color) {
|
||||||
|
plottest::HeadlessContext ctx;
|
||||||
|
plot::Status st;
|
||||||
|
PLOT_CHECK(ctx.create(&st));
|
||||||
|
PLOT_CHECK(plot::gl::load(egl_loader()));
|
||||||
|
|
||||||
|
plot::gl::glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
|
||||||
|
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
std::uint8_t px[4] = {0, 0, 0, 0};
|
||||||
|
PLOT_CHECK(ctx.read_pixel(0, 0, px));
|
||||||
|
PLOT_CHECK_NEAR(px[0], 64, 2); // round(0.25 * 255)
|
||||||
|
PLOT_CHECK_NEAR(px[1], 128, 2);
|
||||||
|
PLOT_CHECK_NEAR(px[2], 191, 2);
|
||||||
|
PLOT_CHECK_NEAR(px[3], 255, 2);
|
||||||
|
|
||||||
|
// A second corner proves the whole attachment is readable.
|
||||||
|
PLOT_CHECK(ctx.read_pixel(ctx.width - 1, ctx.height - 1, px));
|
||||||
|
PLOT_CHECK_NEAR(px[2], 191, 2);
|
||||||
|
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(raii_handles_round_trip) {
|
||||||
|
plottest::HeadlessContext ctx;
|
||||||
|
plot::Status st;
|
||||||
|
PLOT_CHECK(ctx.create(&st));
|
||||||
|
PLOT_CHECK(plot::gl::load(egl_loader()));
|
||||||
|
|
||||||
|
const float verts[6] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f};
|
||||||
|
plot::gl::Buffer vbo;
|
||||||
|
PLOT_CHECK(vbo.create(&st));
|
||||||
|
PLOT_CHECK(vbo.id() != 0);
|
||||||
|
vbo.upload(verts, sizeof verts, plot::gl::GL_STATIC_DRAW);
|
||||||
|
vbo.upload_sub(verts, sizeof verts, 0);
|
||||||
|
|
||||||
|
plot::gl::VertexArray vao;
|
||||||
|
PLOT_CHECK(vao.create(&st));
|
||||||
|
vao.bind();
|
||||||
|
vbo.bind();
|
||||||
|
vao.attrib(0, 2, 2 * sizeof(float), 0);
|
||||||
|
|
||||||
|
const std::uint8_t texels[8 * 8 * 4] = {0};
|
||||||
|
plot::gl::Texture color;
|
||||||
|
PLOT_CHECK(color.create_rgba(8, 8, &st));
|
||||||
|
color.upload_rgba(8, 8, texels);
|
||||||
|
|
||||||
|
const std::uint8_t coverage[8 * 8] = {0};
|
||||||
|
plot::gl::Texture mask;
|
||||||
|
PLOT_CHECK(mask.create_r8(8, 8, &st));
|
||||||
|
mask.upload_r8(8, 8, coverage);
|
||||||
|
mask.set_filter(plot::gl::GL_NEAREST, plot::gl::GL_NEAREST);
|
||||||
|
mask.bind();
|
||||||
|
|
||||||
|
plot::gl::Framebuffer fbo;
|
||||||
|
PLOT_CHECK(fbo.create(&st));
|
||||||
|
fbo.attach_color(color);
|
||||||
|
PLOT_CHECK(fbo.complete());
|
||||||
|
|
||||||
|
plot::gl::Fence fence;
|
||||||
|
PLOT_CHECK(fence.create(&st));
|
||||||
|
plot::gl::glFinish();
|
||||||
|
PLOT_CHECK(fence.wait(0));
|
||||||
|
|
||||||
|
// Move-only: the source is emptied and the GL name survives.
|
||||||
|
plot::gl::Buffer moved = std::move(vbo);
|
||||||
|
PLOT_CHECK(moved.id() != 0);
|
||||||
|
PLOT_CHECK(vbo.id() == 0);
|
||||||
|
moved.reset();
|
||||||
|
PLOT_CHECK(moved.id() == 0);
|
||||||
|
|
||||||
|
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(program_compiles_trivial_shaders) {
|
||||||
|
plottest::HeadlessContext ctx;
|
||||||
|
plot::Status st;
|
||||||
|
PLOT_CHECK(ctx.create(&st));
|
||||||
|
PLOT_CHECK(plot::gl::load(egl_loader()));
|
||||||
|
|
||||||
|
plot::Status ok;
|
||||||
|
plot::gl::Program program = plot::gl::Program::from_source(kVertexSrc, kFragmentSrc, &ok);
|
||||||
|
PLOT_CHECK(bool(ok));
|
||||||
|
PLOT_CHECK(program.id() != 0);
|
||||||
|
|
||||||
|
program.use();
|
||||||
|
program.set_uniform("u_offset", plot::Vec2{1.0f, 2.0f});
|
||||||
|
program.set_uniform("u_alpha", 0.5f);
|
||||||
|
program.set_uniform("u_missing", 0.25f); // unknown name: silent no-op
|
||||||
|
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||||
|
|
||||||
|
plot::Status bad;
|
||||||
|
plot::gl::Program broken = plot::gl::Program::from_source(kVertexSrc, kBrokenFragmentSrc, &bad);
|
||||||
|
PLOT_CHECK(!bad);
|
||||||
|
PLOT_CHECK(bad.code == plot::ErrorCode::GLFailure);
|
||||||
|
PLOT_CHECK(bad.message != nullptr && bad.message[0] != '\0');
|
||||||
|
PLOT_CHECK(broken.id() == 0);
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user