The repository had no licence at all, so every source was all-rights-reserved by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino Ferrari) and a two-line SPDX header to each of the 113 first-party sources -- 48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the grant travels with each file rather than relying on the root document alone. Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely vendored code is untouched: third_party/stb_truetype.h carries no header of ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py emit() now writes the two lines into its header comment and the file was regenerated -- gen_atlas.py --check passes and the regeneration adds only those comment lines to the previously committed file. README's provenance section now states MIT and links LICENSE. No code, test or public API change.
247 lines
9.4 KiB
C++
247 lines
9.4 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#pragma once
|
|
#include <plot/core/color.hpp>
|
|
#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; }
|
|
|
|
/// Identity of the object this handle names, as a counter that only ever
|
|
/// advances: every `reset()` that deleted a name, every allocating `create()`
|
|
/// and every move advances it. A driver is free to hand out a name it has
|
|
/// already used, so state that captured the old object -- the attribute
|
|
/// pointers of a vertex array above all -- must compare this counter rather
|
|
/// than the name (see `render::detail::InstanceStore::epoch()`).
|
|
u32 generation() const noexcept { return generation_; }
|
|
|
|
/// Deletes the GL object, if any, and empties the handle.
|
|
void reset() noexcept {
|
|
if (id_ != 0) {
|
|
Derived::destroy(id_);
|
|
id_ = 0;
|
|
++generation_;
|
|
}
|
|
}
|
|
|
|
protected:
|
|
Handle(Handle&& other) noexcept : id_(other.id_), generation_(other.generation_) {
|
|
other.id_ = 0;
|
|
++generation_; // the object is this handle's now
|
|
++other.generation_; // and no longer the other's
|
|
}
|
|
Handle& operator=(Handle&& other) noexcept {
|
|
if (this != &other) {
|
|
reset(); // advances the generation when it deleted a name
|
|
id_ = other.id_;
|
|
other.id_ = 0;
|
|
++generation_;
|
|
++other.generation_;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
/// Advances the generation for a name the derived class has just allocated and
|
|
/// stored: every allocating `create()` calls it, so the counter is a complete
|
|
/// record of the handle's identity. A failed allocation needs no call -- it has
|
|
/// already gone through `reset()`, which advanced it.
|
|
void note_allocated() noexcept { ++generation_; }
|
|
|
|
GLuint id_ = 0;
|
|
|
|
private:
|
|
u32 generation_ = 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;
|
|
/// Replaces the store with `bytes` bytes of immutable storage carrying `flags`
|
|
/// (`GL_MAP_WRITE_BIT` and friends). GL 4.4 / ARB_buffer_storage: only legal on
|
|
/// a context whose caps() report `buffer_storage`, and the entry point may be
|
|
/// null even on such a context (it is resolved best-effort, see api.hpp), so a
|
|
/// caller checks `glBufferStorage != nullptr` first. The contents start
|
|
/// undefined.
|
|
void storage(usize bytes, GLbitfield flags) const noexcept;
|
|
|
|
/// Maps `bytes` bytes from `offset` with `access`, returning the pointer or
|
|
/// null. GL 3.0 core, but a `GL_MAP_PERSISTENT_BIT` map needs the same
|
|
/// capability as `storage()` above.
|
|
void* map_range(usize offset, usize bytes, GLbitfield access) const noexcept;
|
|
|
|
/// Unmaps the store's data; the pointer from map_range() is invalid after this.
|
|
bool unmap() 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;
|
|
/// Sets a four-component float uniform, e.g. the colour of a pipeline.
|
|
void set_uniform(const char* name, Color value) const noexcept;
|
|
|
|
/// Sets an integer uniform, e.g. a sampler's texture unit. Deliberately not
|
|
/// an overload of the float setter: an untyped literal such as `0` must not
|
|
/// turn a float uniform into a `glUniform1i` call.
|
|
void set_uniform_int(const char* name, i32 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). It sets
|
|
/// `GL_UNPACK_ALIGNMENT` to 1 to do so and leaves it there -- a caller that
|
|
/// shares the context with other uploaders saves and restores it, as
|
|
/// `render::TextAtlas` does around every glyph-atlas upload, whenever in a
|
|
/// frame that upload happens to run.
|
|
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
|