#pragma once #include #include #include 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