Files
plotter/include/plot/render/buffer.hpp
T
Martino Ferrari 8f45108559 fix(render): bind identity into the epoch, titles into reserved strips, one layout per frame
Four review findings on the Task 7 renderer:

- InstanceStore::epoch() is derived from gl::Buffer::generation() -- advanced by
  every allocation, deletion and move -- plus the slot the instances were written
  to, so the orphan fallback of grow_persistent() can no longer leave the vertex
  array's attribute pointers bound to a deleted buffer: the epoch reports the new
  identity and upload() re-points them before the draw.
- PlotLayout reserves y_title_area/x_title_area outside axes (sized from the text
  metrics) and draw_titles() draws in them, so a title can no longer land on the
  data, whatever the envelope does.
- draw() computes plot.layout() once and hands it to the helpers, instead of six
  times per frame, each of which rebuilt the ticks and the grid.
- A refused fence is no longer dropped: mark_drawn() falls back to the orphan path
  and reports the failure rather than rotating through an unsynchronised slot.

New smoke cases force both refusals through the GL entry points (glMapBufferRange,
glFenceSync) and assert the pixels plus the live transfer mode; the title case
asserts the title colour is absent from the bands inside axes and present in both
strips. All three fail against the pre-fix sources.
2026-09-27 23:19:31 +02:00

165 lines
7.2 KiB
C++

#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: the glyph quad's top-left corner
/// in Rect pixels plus the glyph's atlas texel origin.
struct TextInstance {
f32 x;
f32 y;
f32 u;
f32 v;
};
static_assert(sizeof(LineInstance) == 4 * sizeof(f32), "line instance must stay 4 floats");
static_assert(sizeof(TextInstance) == 4 * sizeof(f32), "text instance must stay 4 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. Three slots of immutable
/// storage are mapped once and written directly, and the fence recorded after
/// each slot's draw guards its reuse, so the render thread never waits on the
/// GPU. 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 rotates through. Three is the smallest ring that
/// lets a frame's draw finish while the CPU fills two more.
static constexpr u32 kSlots = 3;
/// Upper bound on the wait for a slot's draw 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;
/// Uploads `count` instances of `instance_bytes`. `Orphan` mode orphans and
/// refills; `PersistentMapped` mode writes the next slot of the ring, waiting
/// out any draw still reading it.
void upload(const void* data, u32 count, usize instance_bytes) noexcept;
/// Records that the draw the caller just issued read the current slot, so the
/// fence guarding that slot's reuse ends up in the command stream behind the
/// draw. True when the slot is guarded, and always in `Orphan` mode, where
/// nothing reads the store after the orphan.
///
/// False when the driver refused the fence: the slot 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`).
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:
/// Grows the mapped ring to `bytes` per slot. 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) noexcept;
/// Deletes every fence, unmaps and releases the GL buffer. `slot_bytes_` drops
/// to 0 so the next upload regrows.
void release_storage() 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_ = kSlots - 1u; ///< last slot written; an upload advances it first
/// 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
gl::Fence fences_[kSlots];
};
} // 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);
}
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);
}
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