feat(render): Renderer with LOD selection, 3.3 and 4.3+ upload paths, headless smoke test

This commit is contained in:
Martino Ferrari committed 2026-09-27 23:03:08 +02:00
1 parent e0cdd95a3b
commit 3555ab63d7
20 files changed
+1544 -25

No files matched your search

+95 -9
View File
@@ -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