Files
plotter/src/render/buffer.cpp
T

119 lines
4.0 KiB
C++

#include <plot/render/buffer.hpp>
#include <plot/gl/api.hpp>
#include <cstring>
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<u8*>(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