#include #include #include #include #include namespace plot::render { namespace detail { bool InstanceStore::create(UploadMode mode, Status* st) noexcept { // plot::gl::Buffer::create() already reports Unsupported while the entry // points are unresolved and GLFailure when the driver hands out no name, so // the wrapper only has to propagate its answer. if (!buffer_.create(st)) { return false; } capacity_ = 0; slot_bytes_ = 0; slot_count_ = kInitialSlots; slot_ = 0; frame_uploads_ = 0; // The mapped path additionally needs its entry points, which are resolved // best-effort (see api.hpp): without them the store would never be able to // write anything, so it stays on the baseline path instead. mode_ = mode == UploadMode::PersistentMapped && gl::glBufferStorage != nullptr && gl::glMapBufferRange != nullptr ? UploadMode::PersistentMapped : UploadMode::Orphan; return true; } void InstanceStore::release_storage() noexcept { if (mapped_ != nullptr) { // A persistent mapping outlives the frame that created it, so it has to be // dropped before the buffer it belongs to is deleted. buffer_.unmap(); mapped_ = nullptr; } frame_fence_.reset(); buffer_.reset(); slot_bytes_ = 0; } void InstanceStore::drop_to_orphan() noexcept { // The hazard the ring exists to prevent -- writing storage the GPU may still be // reading -- can no longer be guarded, and a frame that draws through the // baseline path beats one that draws nothing. The old buffer is deleted while // the GPU may still read it, which is safe: GL keeps its storage alive until the // last command that references it retires. release_storage(); mode_ = UploadMode::Orphan; capacity_ = 0; frame_uploads_ = 0; Status retry; (void)buffer_.create(&retry); // a failed retry leaves nothing to draw with } bool InstanceStore::grow_persistent(usize bytes, u32 slots) noexcept { release_storage(); Status st; if (!buffer_.create(&st)) { mode_ = UploadMode::Orphan; // nothing left to draw with at all capacity_ = 0; return false; } const gl::GLbitfield flags = gl::GL_MAP_WRITE_BIT | gl::GL_MAP_PERSISTENT_BIT | gl::GL_MAP_COHERENT_BIT; const usize total = usize(slots) * bytes; buffer_.storage(total, flags); mapped_ = static_cast(buffer_.map_range(0, total, flags)); if (mapped_ == nullptr) { // The driver refused the storage or the mapping. A frame that draws through // the baseline path beats one that draws nothing, so re-create the empty // buffer the orphan path needs and let the next upload use it. The vertex // array's attribute pointers went out with the buffer they captured, and the // epoch reports it (`gl::Buffer::generation()`), which is what re-points them // at this buffer before the draw. drop_to_orphan(); return false; } slot_bytes_ = bytes; slot_count_ = slots; slot_ = 0; // Fresh storage: every one of its slots is unwritten, so the frame's slot // cursor restarts at 0. The draws that read the old buffer are already in the // command stream and keep it alive until they retire. frame_uploads_ = 0; #ifdef PLOT_RING_STATS note_ring_slots(slot_count_); #endif return true; } bool InstanceStore::begin_frame() noexcept { const u32 written = frame_uploads_; frame_uploads_ = 0; if (mode_ != UploadMode::PersistentMapped || mapped_ == nullptr) { return true; // nothing the GPU reads, so no slot has to be waited for } if (written == 0) { return true; // the frame before this one never read the ring } if (!frame_fence_) { // Slots were written and no fence was ever recorded for them: the caller // skipped mark_drawn(), so those slots are unguarded and writing any of them // again could race a read still in flight. The ring is abandoned rather than // reused, exactly as a refused fence's is. drop_to_orphan(); return false; } #ifdef PLOT_RING_STATS ring_waits().fetch_add(1, std::memory_order_relaxed); #endif // The slots this frame is about to write were all last written a frame ago, and // this one fence sits behind every draw that read them -- unless the driver has // not retired it, in which case writing would race the GPU and the ring is // abandoned for the baseline path. The non-blocking check is the common case. const bool retired = frame_fence_.wait(0) || frame_fence_.wait(kFenceWaitNs); frame_fence_.reset(); if (retired) { return true; } drop_to_orphan(); return false; } void InstanceStore::upload(const void* data, u32 count, usize instance_bytes) noexcept { if (buffer_.id() == 0 || data == nullptr || count == 0 || instance_bytes == 0) { return; } const usize bytes = usize(count) * instance_bytes; if (mode_ == UploadMode::PersistentMapped && (bytes > slot_bytes_ || frame_uploads_ >= slot_count_)) { // The frame has written every slot the ring has, or this instance payload is // wider than a slot: grow rather than wrap. Covering the frame's upload count // is what lets a single fence, recorded after its last draw, guard all of its // slots -- and doubling makes the growth rare for a workload whose upload // count has settled. const usize want_bytes = std::max(bytes, slot_bytes_); const u32 want_slots = frame_uploads_ >= slot_count_ ? std::max(frame_uploads_ + 1u, slot_count_ * 2u) : slot_count_; (void)grow_persistent(want_bytes, want_slots); // a failure leaves the orphan path } if (mode_ == UploadMode::Orphan) { if (count > capacity_) { capacity_ = count; // grow only when the workload outgrows the store } buffer_.upload(nullptr, usize(capacity_) * instance_bytes, gl::GL_STREAM_DRAW); buffer_.upload_sub(data, bytes, 0); return; } // The slot this frame has not written yet: the frame's write cursor is the ring // index, so a frame writes 0, 1, 2, ... and never reaches a slot it already // filled. That is the whole reason one fence at the end of the frame is enough. slot_ = frame_uploads_; #ifdef PLOT_RING_STATS ring_uploads().fetch_add(1, std::memory_order_relaxed); #endif // MAP_COHERENT storage needs no explicit flush, so this is the whole transfer. std::memcpy(mapped_ + usize(slot_) * slot_bytes_, data, bytes); ++frame_uploads_; ++slot_epoch_; } bool InstanceStore::mark_drawn() noexcept { if (mode_ != UploadMode::PersistentMapped || mapped_ == nullptr) { return true; // nothing reads the store after the draw } if (frame_uploads_ == 0) { return true; // no draw read the ring this frame, so it needs no guard } Status st; frame_fence_.reset(); if (frame_fence_.create(&st)) { return true; } // A frame whose fence the driver refused leaves the slots it wrote // unsynchronised, and such a slot can never be written again safely: the next // upload would memcpy into storage the GPU may still be reading, which is the // tearing the ring exists to prevent. So the ring is abandoned for the orphan // path, which is what the map failure's treatment does one step earlier. drop_to_orphan(); return false; } } // namespace detail } // namespace plot::render