Final whole-branch review fixes. 1 (Important). Plot::measurement() cached on (series pointer, sample count) only, so a saturated StreamSeries -- whose size() stays pinned while x0() advances every acquire() -- froze its readout on the first saturated frame until a cursor moved. The cache key now carries the series' x0() (new MeasureSlot::last_x0), so a sliding window recomputes and a static series still does not: asserted with a new macro-gated counter hook (plot/detail/measure_count.hpp, PLOT_COUNT_MEASURE_RECOMPUTES, wired like the pyramid and ring counters, absent from the release library). New case saturated_stream_measurement_tracks_the_sliding_window: saturated ring, stationary cursors, ramp in absolute x -- zero recomputes for repeated reads with nothing moving, one per acquire while it slides. 2-3, 9. Spec: 4.3 described a GL_LINE_STRIP path that does not exist (there is one rasteriser -- instanced anti-aliased quads -- and dense_threshold_spp selects the gather level, not a primitive); 8 named a PLOT_GL_CHECK() macro that exists nowhere, and stated no exception policy. The table, the field's comment and 8 now describe the real gather-level threshold, the real glGetError() drain plus the stores' drop-to-orphan fallbacks, and the real policy: library failures return Status/bool, allocating entry points propagate std::bad_alloc. 4. The frame handed back neither GL_UNPACK_ALIGNMENT (init() sets 1 for the atlas) nor the clear colour (queried once with a new glGetFloatv entry). end_frame() restores both and renderer.hpp lists them in the residue contract. 5. draw() kept a host TextBackend for layout while drawing with its own font. The renderer now installs its font unconditionally -- strips and labels measure and draw with one backend -- with a debug-build diagnostic if a different one was set, and Plot::set_text_backend() is documented as the no-renderer path. New case renderer_measures_the_labels_it_draws asserts layout, measuring and the drawn label's ink agree. 6-10. draw()'s style overwrite stated at the call site; the load-sensitive wall-clock bound in test_measure.cpp replaced by a counter assertion (same interval, same fold count over a 512x length ratio); dump_frame_ppm drops noexcept and says why it may allocate; error.txt ignored (not this project's, not generated here) rather than deleted. Verified: debug and release build clean (-Wall -Wextra -Wpedantic -Wconversion -Wshadow), 4/4 ctest entries pass, 130 cases pass (was 127), umbrella and header self-containment checks pass, and no counter symbol in the release archive.
243 lines
9.2 KiB
C++
243 lines
9.2 KiB
C++
#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
|
|
/// `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<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
|