Files
plotter/include/plot/render/buffer.hpp
T
Martino Ferrari ed3a8ce645 licence: MIT LICENSE and SPDX identifiers in every first-party file
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.
2026-09-28 22:13:40 +02:00

218 lines
10 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <plot/core/status.hpp>
#include <plot/core/types.hpp>
#include <plot/gl/handle.hpp>
namespace plot::render {
/// Per-instance payload of the line pipeline: one segment in data space. It
/// mirrors the interleaved floats `LineBatch::data()` produces, which is why the
/// attribute offsets in vertex_array.cpp come from `offsetof` rather than from
/// hand-written numbers.
struct LineInstance {
f32 x0;
f32 y0;
f32 x1;
f32 y1;
};
/// Per-instance payload of the text pipeline: one glyph cell.
///
/// - `(x, y)` is the cell's top-left corner in Rect pixels, top-left origin like
/// `plot::Rect`. `TextBatch` already applied the glyph's bearing along the
/// basis, so this is the corner the shader grows the cell from.
/// - `(u, v)` is the glyph rect's **normalised top-left** in the atlas, with `v`
/// increasing *downward* -- the convention `Glyph` states, so `(u, v)` is the
/// rect's top-left texel and the cell spans `(u, v)..(u + w, v + h)`, where
/// `(w, h)` is the glyph cell's normalised size (a per-draw uniform: every
/// glyph a batch stages comes from one atlas and occupies a cell of one size).
/// - `(ax, ay)` is the glyph's local +x axis -- the direction the run advances
/// in -- as a **unit vector in pixels**. It is what rotates the cell: the
/// shader builds it from `(ax, ay)` and its perpendicular, and maps the glyph's
/// rect onto the two the same way round, so a cell is never mirrored. `(1, 0)`
/// is unrotated text, and `(0, -1)` is a run advancing up the screen, i.e. a
/// 90 degree counter-clockwise label such as a y-axis title.
///
/// Six floats, so the whole text pipeline still fits one instanced draw call per
/// frame with rotation free: a matrix per glyph would be three times the bytes
/// for the same pixels.
struct TextInstance {
f32 x;
f32 y;
f32 u;
f32 v;
f32 ax;
f32 ay;
};
static_assert(sizeof(LineInstance) == 4 * sizeof(f32), "line instance must stay 4 floats");
static_assert(sizeof(TextInstance) == 6 * sizeof(f32), "text instance must stay 6 floats");
/// How a store's staged instances reach the GPU. Semantics are identical either
/// way -- only the transfer differs, and `Renderer::init()` picks one from
/// `plot::gl::caps()`:
///
/// - `Orphan`: the GL 3.3 baseline. Every upload re-specifies the store with a
/// null `glBufferData` (orphaning it, so the driver never has to wait for the
/// draw that is still reading the previous one) and then refills it.
/// - `PersistentMapped`: GL 4.4 / ARB_buffer_storage. Immutable storage is mapped
/// once and written directly, one slot per upload. A slot is never written
/// twice within a frame, one fence is recorded after the frame's last draw, and
/// the only wait is at the start of the next frame -- so the render thread
/// never synchronises with GPU work it has just issued. The vertex array's
/// attribute pointers move with the slot.
enum class UploadMode { Orphan, PersistentMapped };
namespace detail {
/// The growth half of an instance buffer, shared by both pipelines so neither
/// duplicates the GL ownership or the transfer strategy. This is the one place
/// vertex data is handed to the GPU: `LineBuffer::upload` and
/// `TextBuffer::upload` both land here, and no caller of either can tell which
/// `UploadMode` is live.
class InstanceStore {
public:
/// Slots the persistent ring starts with. The floor, not the ring's size: it
/// grows to cover the uploads a frame actually performs (see `upload()`), and
/// never shrinks, so a steady workload grows it at most a logarithmic number of
/// times over the life of the store.
static constexpr u32 kInitialSlots = 3;
/// Upper bound on the wait for the previous frame's draws to retire: a driver
/// that has stopped making progress must not stall the render thread forever.
static constexpr u64 kFenceWaitNs = 1'000'000'000ull;
/// Creates the vertex buffer in `mode`. Reports `ErrorCode::Unsupported` while
/// the GL entry points are unresolved and `GLFailure` if the driver hands out
/// no name. A caller asking for `PersistentMapped` must already have checked
/// that the context and the entry points can do it (see Renderer::init): a
/// store created that way on a machine without them would never draw.
bool create(UploadMode mode, Status* st = nullptr) noexcept;
/// Starts a frame: waits, once, for the frame before it to retire the draws
/// that read the ring, so no slot about to be written can still be being read.
/// False when that wait timed out -- or when a slot was written without a fence
/// ever being recorded for it -- in which case the store has dropped to the
/// baseline path, which has no such hazard, and the caller reports it (see
/// `Renderer::begin_frame`).
///
/// One wait covers every slot because no slot is written twice within a frame
/// (`upload()` grows the ring rather than wrapping it), so each slot this frame
/// touches was last written in the previous frame or earlier -- and the
/// previous frame's fence sits behind every draw that read one.
bool begin_frame() noexcept;
/// Uploads `count` instances of `instance_bytes`. `Orphan` mode orphans and
/// refills; `PersistentMapped` mode writes the next slot of the ring, growing
/// the ring first when the frame has already written all of its slots.
void upload(const void* data, u32 count, usize instance_bytes) noexcept;
/// Records the frame's single fence, behind every draw that read the ring, so
/// the next `begin_frame()` can wait for them. Called once per frame, after the
/// last draw that consumes the store. True in `Orphan` mode, where nothing
/// reads the store after the orphan, and true when the frame drew nothing from
/// the ring.
///
/// False when the driver refused the fence: the slots written this frame would
/// stay unsynchronised, so the store drops the mapped ring for the orphan path
/// instead of rotating through it (the caller reports the failure -- see
/// `Renderer::end_frame`).
bool mark_drawn() noexcept;
/// Byte offset the current slot's instances start at: 0 in `Orphan` mode,
/// `slot * slot_bytes` in `PersistentMapped` mode. The vertex array re-points
/// its attribute pointers whenever `epoch()` changes.
usize slot_offset() const noexcept {
return mode_ == UploadMode::PersistentMapped ? usize(slot_) * slot_bytes_ : 0u;
}
/// Changes whenever the staged instances moved (a new slot) or the GL buffer
/// was recreated, i.e. whenever attribute pointers into this store went stale.
///
/// Derived from the two things that can make them stale rather than bumped by
/// hand at every site that replaces the buffer object: `gl::Buffer::generation()`
/// advances on every allocation and deletion (so a fallback that re-creates the
/// buffer cannot leave the vertex array bound to a deleted one), and `slot_epoch_`
/// advances on every upload that lands in a new slot. Both counters only ever
/// increase, so their sum changes whenever either does, and never repeats.
u32 epoch() const noexcept { return buffer_.generation() + slot_epoch_; }
void bind() const noexcept { buffer_.bind(); }
bool persistent() const noexcept { return mode_ == UploadMode::PersistentMapped; }
private:
/// (Re)allocates the mapped ring with `slots` slots of `bytes` each. False when
/// the driver refuses the storage or the map, in which case the store has fallen
/// back to a fresh `Orphan` buffer and the frame still draws.
bool grow_persistent(usize bytes, u32 slots) noexcept;
/// Deletes the frame fence, unmaps and releases the GL buffer. `slot_bytes_`
/// drops to 0 so the next upload regrows.
void release_storage() noexcept;
/// Abandons the mapped ring for the baseline path and re-creates the empty
/// buffer that path needs. Used wherever the ring can no longer be guarded.
void drop_to_orphan() noexcept;
gl::Buffer buffer_;
UploadMode mode_ = UploadMode::Orphan;
u32 capacity_ = 0; ///< instances the orphan store can hold
usize slot_bytes_ = 0; ///< bytes per slot of the persistent ring
u32 slot_count_ = kInitialSlots; ///< slots the ring has; grows, never shrinks
u32 slot_ = 0; ///< slot the last upload wrote
/// Slots written since this frame began, and the index the next upload takes.
/// It is the ring index itself, which is what keeps a frame's slots distinct.
u32 frame_uploads_ = 0;
/// Times an upload landed in a new slot. Kept apart from the buffer's own
/// identity (see `epoch()`) so neither has to know about the other.
u32 slot_epoch_ = 0;
u8* mapped_ = nullptr; ///< base of the mapped ring, null in `Orphan` mode
/// Guards every slot the frame just ended wrote. Recorded once, after its last
/// draw; waited on once, at the start of the next frame.
gl::Fence frame_fence_;
};
} // namespace detail
/// Instance buffer of the line pipeline: one `LineInstance` per segment.
class LineBuffer {
public:
bool create(Status* st = nullptr, UploadMode mode = UploadMode::Orphan) noexcept {
return store_.create(mode, st);
}
bool begin_frame() noexcept { return store_.begin_frame(); }
void upload(const f32* interleaved, u32 count) noexcept {
store_.upload(interleaved, count, sizeof(LineInstance));
}
bool mark_drawn() noexcept { return store_.mark_drawn(); }
usize slot_offset() const noexcept { return store_.slot_offset(); }
u32 epoch() const noexcept { return store_.epoch(); }
bool persistent() const noexcept { return store_.persistent(); }
void bind() const noexcept { store_.bind(); }
private:
detail::InstanceStore store_;
};
/// Instance buffer of the text pipeline: one `TextInstance` per glyph.
class TextBuffer {
public:
bool create(Status* st = nullptr, UploadMode mode = UploadMode::Orphan) noexcept {
return store_.create(mode, st);
}
bool begin_frame() noexcept { return store_.begin_frame(); }
void upload(const f32* interleaved, u32 count) noexcept {
store_.upload(interleaved, count, sizeof(TextInstance));
}
bool mark_drawn() noexcept { return store_.mark_drawn(); }
usize slot_offset() const noexcept { return store_.slot_offset(); }
u32 epoch() const noexcept { return store_.epoch(); }
bool persistent() const noexcept { return store_.persistent(); }
void bind() const noexcept { store_.bind(); }
private:
detail::InstanceStore store_;
};
} // namespace plot::render