feat(render): Renderer with LOD selection, 3.3 and 4.3+ upload paths, headless smoke test
This commit is contained in:
1 parent
e0cdd95a3b
commit
3555ab63d7
20 files changed
+1544
-25
No files matched your search
+95
-9
@@ -1,9 +1,13 @@
|
||||
#include <plot/render/buffer.hpp>
|
||||
|
||||
#include <plot/gl/api.hpp>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace plot::render {
|
||||
namespace detail {
|
||||
|
||||
bool InstanceStore::create(Status* st) noexcept {
|
||||
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.
|
||||
@@ -11,21 +15,103 @@ bool InstanceStore::create(Status* st) noexcept {
|
||||
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 || count == 0 || instance_bytes == 0) {
|
||||
if (buffer_.id() == 0 || data == nullptr || count == 0 || instance_bytes == 0) {
|
||||
return;
|
||||
}
|
||||
if (count > capacity_) {
|
||||
// Only a workload larger than anything staged so far pays for an
|
||||
// allocation; a frame that repeats the previous one writes into the store
|
||||
// that is already there.
|
||||
buffer_.upload(nullptr, static_cast<usize>(count) * instance_bytes, gl::GL_STREAM_DRAW);
|
||||
capacity_ = count;
|
||||
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.
|
||||
}
|
||||
buffer_.upload_sub(data, static_cast<usize>(count) * instance_bytes, 0);
|
||||
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user