#pragma once #include #include #include #include #include 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 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 { 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 { 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 { 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 { 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 /// `Renderer` does around its atlas and its frames. 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 { 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