#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_ = kSlots - 1u; // the first upload takes slot 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; } for (gl::Fence& fence : fences_) { fence.reset(); } buffer_.reset(); slot_bytes_ = 0; } bool InstanceStore::grow_persistent(usize bytes) noexcept { // Deleting the old buffer is safe while the GPU may still be reading it: GL // keeps the storage alive until the last command that references it retires. 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; buffer_.storage(kSlots * bytes, flags); mapped_ = static_cast(buffer_.map_range(0, kSlots * bytes, 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. release_storage(); mode_ = UploadMode::Orphan; capacity_ = 0; Status retry; (void)buffer_.create(&retry); // a failed retry leaves nothing to draw with return false; } slot_bytes_ = bytes; slot_ = kSlots - 1u; // fresh storage: the first upload takes slot 0 ++epoch_; return true; } 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_ && !grow_persistent(bytes)) { // The fallback already replaced the store with an orphan buffer. } 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; } // Advance the ring, then make sure the draw that last read this slot has // retired. The non-blocking check is the common case; the bounded wait only // costs anything when three frames' geometry is still in flight. slot_ = (slot_ + 1u) % kSlots; if (fences_[slot_]) { if (!fences_[slot_].wait(0)) { (void)fences_[slot_].wait(kFenceWaitNs); } fences_[slot_].reset(); } // MAP_COHERENT storage needs no explicit flush, so this is the whole transfer. std::memcpy(mapped_ + usize(slot_) * slot_bytes_, data, bytes); ++epoch_; } void InstanceStore::mark_drawn() noexcept { if (mode_ != UploadMode::PersistentMapped || mapped_ == nullptr) { return; } Status st; fences_[slot_].reset(); (void)fences_[slot_].create(&st); } } // namespace detail } // namespace plot::render