From f352718e4aef6186cf12609d3cb932002eb66796 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 17 Oct 2026 01:24:33 +0200 Subject: [PATCH] Stream model data through persistently mapped buffers --- CMakeLists.txt | 1 + docs/opengl46-renderer.md | 16 ++ gl/streambuffer.cpp | 221 ++++++++++++++++++++++++ gl/streambuffer.h | 142 +++++++++++++++ rendering/opengl46/opengl46exposure.cpp | 2 +- rendering/opengl46/opengl46exposure.h | 3 +- rendering/opengl46/opengl46renderer.cpp | 72 ++++---- rendering/opengl46/opengl46renderer.h | 14 +- utilities/Globals.cpp | 6 + utilities/Globals.h | 3 + 10 files changed, 441 insertions(+), 39 deletions(-) create mode 100644 gl/streambuffer.cpp create mode 100644 gl/streambuffer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d323f12..3e80aca9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -262,6 +262,7 @@ set(SOURCES "gl/pbo.cpp" "gl/query.cpp" "gl/ssbo.cpp" +"gl/streambuffer.cpp" "gl/cluster_grid.cpp" "gl/readback.cpp" diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index bc9e7e07..9d6619d8 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -246,6 +246,18 @@ drawn (its colour is not worked out with the depth prepass on). Models whose mat water and the like) are left out of the depth prepass and drawn as before. All three can be changed while the simulator runs, in the **Instance culling** debug window. +### Model data of the draws + +Before every draw the renderer hands the graphics card the data of the drawn part of the model (its position in the view, its +colours and parameters), and before batches of models the positions of the copies in the batch. With `gfx.bufferstreaming` +on, these go to buffers the processor writes to directly, a new place for every draw, one buffer for each of the last three +frames; before, every draw replaced the data of the one block with a separate copy through the driver. The picture is the +same either way. + +| Entry | Default | Meaning | +|---|---|---| +| `gfx.bufferstreaming yes/no` | `yes` | `yes`: the data of the draws is written to persistently mapped buffers. `no`: as before (`glBufferSubData`), for comparison or when a driver misbehaves. Read at the start | + ### Changed option `dynamiclights` (0..7) used to limit the number of headlight sets that light the surroundings. It is still @@ -305,6 +317,10 @@ editor goes through the first `dynamiclights` headlights, the reflections throug of detail, the draw calls with those of the depth prepass, and the processor time of the whole step) and the timings `instance cull:`, `occlusion depth:` (the depth pyramid of the test) and `depth prepass:`. With `compare` it shows how many models both counted and how many levels of detail differ. +- Shift+F12 shows the `instance buffer:` row (positions of copies written in the frame and the size of the buffer) and the + `model blocks:` row: how many blocks of model data the frame wrote, with `gfx.bufferstreaming yes` also how much of the + buffer of the frame they took, how many times a frame had to wait for the graphics card to finish with a buffer before + writing to it (`waits`, should stay at 0) and how many times a buffer had to grow (`grown`, a few times after the start). - `gfx.gldebug yes` turns on the OpenGL debug output (`KHR_debug`); messages go to the log. ## Building diff --git a/gl/streambuffer.cpp b/gl/streambuffer.cpp new file mode 100644 index 00000000..039ce6c7 --- /dev/null +++ b/gl/streambuffer.cpp @@ -0,0 +1,221 @@ +#include "stdafx.h" +#include "streambuffer.h" + +#include +#include +#include + +#include "utilities/Logs.h" + +namespace +{ +// a single frame isn't given more than this; past it the frame waits for the gpu to finish everything and starts the buffer over +constexpr GLsizeiptr STREAM_SIZE_MAX = 256 * 1024 * 1024; +constexpr GLbitfield STREAM_MAP_FLAGS = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT; + +GLsizeiptr align_up(GLsizeiptr const Value, GLsizeiptr const Alignment) +{ + return (Value + Alignment - 1) / Alignment * Alignment; +} +} // namespace + +gl::frame_stream::frame_stream(GLsizeiptr Size, GLsizeiptr Alignment, bool Mapped, void const *Header, GLsizeiptr Headersize) + : m_alignment(std::max(Alignment, 1)), m_mapped(Mapped) +{ + if (Header != nullptr && Headersize > 0) + { + auto const *bytes{static_cast(Header)}; + m_header.assign(bytes, bytes + Headersize); + } + m_start = align_up(static_cast(m_header.size()), m_alignment); + m_fill = m_start; + Size = std::max(Size, m_start + m_alignment); + for (auto &slot : m_slots) + { + create(slot, Size); + } +} + +gl::frame_stream::~frame_stream() +{ + for (auto &slot : m_slots) + { + release(slot); + } +} + +void gl::frame_stream::create(slot &Slot, GLsizeiptr Size) +{ + ++m_generation; + glCreateBuffers(1, &Slot.buffer); + Slot.size = Size; + Slot.data = nullptr; + if (m_mapped) + { + glNamedBufferStorage(Slot.buffer, Size, nullptr, STREAM_MAP_FLAGS); + Slot.data = static_cast(glMapNamedBufferRange(Slot.buffer, 0, Size, STREAM_MAP_FLAGS)); + if (Slot.data == nullptr) + { + // the driver wouldn't map it: this buffer and the ones made later are written the plain way + ErrorLog("Bad buffer: persistent mapping failed, buffer streaming falls back to glNamedBufferSubData"); + m_mapped = false; + glDeleteBuffers(1, &Slot.buffer); + glCreateBuffers(1, &Slot.buffer); + } + } + if (!m_mapped) + { + glNamedBufferStorage(Slot.buffer, Size, nullptr, GL_DYNAMIC_STORAGE_BIT); + } + if (!m_header.empty()) + { + if (Slot.data != nullptr) + std::memcpy(Slot.data, m_header.data(), m_header.size()); + else + glNamedBufferSubData(Slot.buffer, 0, static_cast(m_header.size()), m_header.data()); + } +} + +void gl::frame_stream::release(slot &Slot) +{ + if (Slot.fence != nullptr) + { + glDeleteSync(Slot.fence); + Slot.fence = nullptr; + } + if (Slot.buffer != 0) + { + // a mapped buffer is unmapped by its deletion. draws issued before which read it keep its storage until they are done + glDeleteBuffers(1, &Slot.buffer); + Slot.buffer = 0; + } + Slot.data = nullptr; + Slot.size = 0; +} + +void gl::frame_stream::wait(slot &Slot) +{ + if (Slot.fence == nullptr) + return; + + auto result{glClientWaitSync(Slot.fence, 0, 0)}; + if (result == GL_TIMEOUT_EXPIRED) + { + ++m_waits; + do + { + result = glClientWaitSync(Slot.fence, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000); + } while (result == GL_TIMEOUT_EXPIRED); + } + glDeleteSync(Slot.fence); + Slot.fence = nullptr; +} + +void gl::frame_stream::next_frame() +{ + auto ¤t{m_slots[m_slot]}; + if (current.data != nullptr) + { + // the cpu writes to a mapped buffer behind the back of the driver, the frames take turns by the fences + if (current.fence != nullptr) + glDeleteSync(current.fence); + current.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + // the fence may be waited for in another context, whose flush wouldn't send it + glFlush(); + } + m_slot = (m_slot + 1) % FRAMES; + wait(m_slots[m_slot]); + m_fill = m_start; +} + +GLintptr gl::frame_stream::push(void const *Data, GLsizeiptr Size) +{ + auto *current{&m_slots[m_slot]}; + auto offset{align_up(m_fill, m_alignment)}; + if (offset + Size > current->size) + { + auto const needed{m_start + align_up(Size, m_alignment)}; + auto const size{needed > STREAM_SIZE_MAX ? needed : std::min(std::max(needed, 2 * current->size), STREAM_SIZE_MAX)}; + if (size > current->size) + { + // a bigger buffer for the rest of the frame, starting after its header again. the fence of the slot (if any) was + // waited for when the frame took the slot over, the old buffer is only read by the draws of this frame + release(*current); + create(*current, size); + ++m_grows; + } + else + { + // the frame wrote as much as a buffer may hold: start it over once the gpu read all of it + ErrorLog("Bad buffer: a frame streamed more than " + std::to_string(STREAM_SIZE_MAX / (1024 * 1024)) + " MiB, waiting for the gpu"); + glFinish(); + ++m_waits; + } + current = &m_slots[m_slot]; + offset = m_start; + } + if (current->data != nullptr) + std::memcpy(current->data + offset, Data, static_cast(Size)); + else + glNamedBufferSubData(current->buffer, offset, Size, Data); + m_fill = offset + Size; + return offset; +} + +gl::stream_ubo::stream_ubo(GLsizeiptr Blocksize, GLuint Index, bool Streamed) : m_index(Index), m_blocksize(Blocksize) +{ + if (!Streamed) + { + m_plain = std::make_unique(static_cast(Blocksize), static_cast(Index), GL_STREAM_DRAW); + return; + } + GLint alignment{256}; + glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &alignment); + // a frame draws some thousands of times; the buffers grow when it draws more + m_stream = std::make_unique(align_up(Blocksize, std::max(alignment, 1)) * 8192, std::max(alignment, 1), true); + m_last.resize(static_cast(Blocksize)); +} + +void gl::stream_ubo::update(void const *Data, GLsizeiptr Size) +{ + ++m_updates; + if (m_plain) + { + m_plain->update(static_cast(Data), 0, Size); + return; + } + m_boundoffset = m_stream->push(Data, Size); + m_boundbuffer = m_stream->buffer(); + m_lastsize = std::min(Size, m_blocksize); + if (Data != m_last.data()) + std::memcpy(m_last.data(), Data, static_cast(m_lastsize)); + // the plural form leaves the general GL_UNIFORM_BUFFER binding alone, which the binding tracker of gl::buffer relies on + GLsizeiptr const size{Size}; + glBindBuffersRange(GL_UNIFORM_BUFFER, m_index, 1, &m_boundbuffer, &m_boundoffset, &size); +} + +void gl::stream_ubo::bind_uniform() +{ + if (m_plain) + { + m_plain->bind_uniform(); + return; + } + if (m_boundbuffer == 0) + return; + GLsizeiptr const size{m_lastsize}; + glBindBuffersRange(GL_UNIFORM_BUFFER, m_index, 1, &m_boundbuffer, &m_boundoffset, &size); +} + +void gl::stream_ubo::next_frame() +{ + m_updates = 0; + if (m_plain) + return; + m_stream->next_frame(); + if (m_lastsize > 0) + { + update(m_last.data(), m_lastsize); + m_updates = 0; + } +} diff --git a/gl/streambuffer.h b/gl/streambuffer.h new file mode 100644 index 00000000..4fc9d26d --- /dev/null +++ b/gl/streambuffer.h @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "object.h" +#include "ubo.h" + +namespace gl +{ + // data the cpu writes for the draws of one frame (the uniform block of every draw, matrices of instances), appended to the + // buffer of the frame. the frames use FRAMES buffers in turn. mapped: the buffers are persistently mapped and written with + // memcpy, a fence marks the end of each frame, and a frame waits for the fence of the buffer it takes over (written + // FRAMES - 1 frames before; normally the gpu is long done with it). not mapped: written with glNamedBufferSubData, the driver + // keeps it in step with the gpu, as with any other buffer + class frame_stream + { + public: + static constexpr std::size_t FRAMES = 3; + + // Size: initial size of the buffer of a frame, it grows when a frame needs more. Alignment: of the offsets push() + // returns. Header: bytes placed at the start of every buffer, never overwritten (nullptr: none) + frame_stream(GLsizeiptr Size, GLsizeiptr Alignment, bool Mapped, void const *Header = nullptr, GLsizeiptr Headersize = 0); + ~frame_stream(); + frame_stream(frame_stream const &) = delete; + frame_stream &operator=(frame_stream const &) = delete; + + // ends the frame: what was written so far goes with its fence, the next frame writes to the next buffer + void next_frame(); + // appends Size bytes to the buffer of the frame and returns their offset. may replace the buffer of the frame with a + // bigger one (generation() changes, buffer() tells which to bind); the draws issued before keep the old one until the + // gpu is done with them + GLintptr push(void const *Data, GLsizeiptr Size); + + GLuint buffer() const + { + return m_slots[m_slot].buffer; + } + // changes whenever buffer() is replaced by a new buffer object. the name alone can't tell: gl hands the name of a + // deleted buffer to the next one made + std::uint64_t generation() const + { + return m_generation; + } + // size of the buffer of the frame, and bytes written to it in this frame (after the header) + GLsizeiptr size() const + { + return m_slots[m_slot].size; + } + GLsizeiptr used() const + { + return m_fill - m_start; + } + // the buffer of the frame is persistently mapped + bool mapped() const + { + return m_slots[m_slot].data != nullptr; + } + // frames which had to wait for the gpu before writing, buffers grown, since the start + std::uint64_t waits() const + { + return m_waits; + } + std::uint64_t grows() const + { + return m_grows; + } + + private: + struct slot + { + GLuint buffer = 0; + GLsizeiptr size = 0; + std::uint8_t *data = nullptr; // mapped + GLsync fence = nullptr; // mapped: end of the last frame which wrote to the buffer + }; + + void create(slot &Slot, GLsizeiptr Size); + void release(slot &Slot); + void wait(slot &Slot); + + std::array m_slots; + std::size_t m_slot = 0; + GLsizeiptr m_alignment; + bool m_mapped; // buffers made from now on are mapped + std::vector m_header; + GLsizeiptr m_start; // the header, aligned + GLsizeiptr m_fill; + std::uint64_t m_waits = 0; + std::uint64_t m_grows = 0; + std::uint64_t m_generation = 0; + }; + + // uniform block written anew for (nearly) every draw, the model block of the renderer. streamed: every update() goes to + // its own place in a frame_stream and is bound there with glBindBuffersRange. not streamed: one plain ubo, updated with + // glBufferSubData, as before + class stream_ubo + { + public: + stream_ubo(GLsizeiptr Blocksize, GLuint Index, bool Streamed); + + void update(void const *Data, GLsizeiptr Size); + template void update(T const &Data) + { + static_assert(std::is_trivially_copyable_v, "ubo data must be trivially copyable"); + update(&Data, static_cast(sizeof(Data))); + } + // binds the block written last (to a new context) + void bind_uniform(); + // call at the start of a frame: the last block is copied to the buffer of the new frame and bound, so nothing drawn in + // it reads a buffer the frame doesn't fence + void next_frame(); + + bool streamed() const + { + return m_stream != nullptr; + } + frame_stream const *stream() const + { + return m_stream.get(); + } + // blocks written in this frame + std::uint64_t updates() const + { + return m_updates; + } + + private: + GLuint m_index; + GLsizeiptr m_blocksize; + std::unique_ptr m_plain; + std::unique_ptr m_stream; + std::vector m_last; // streamed: copy of the last block + GLsizeiptr m_lastsize = 0; + GLuint m_boundbuffer = 0; + GLintptr m_boundoffset = 0; + std::uint64_t m_updates = 0; + }; +} // namespace gl diff --git a/rendering/opengl46/opengl46exposure.cpp b/rendering/opengl46/opengl46exposure.cpp index ea9a1ba1..470cdc32 100644 --- a/rendering/opengl46/opengl46exposure.cpp +++ b/rendering/opengl46/opengl46exposure.cpp @@ -116,7 +116,7 @@ void opengl46_exposure::measure(opengl_texture &Image, double const Deltatime) opengl_texture::reset_unit_cache(); } -opengl_texture *opengl46_exposure::bloom(bloom_chain &Chain, opengl_texture &Image, gl::ubo &Modelubo, gl::model_ubs &Model) +opengl_texture *opengl46_exposure::bloom(bloom_chain &Chain, opengl_texture &Image, gl::stream_ubo &Modelubo, gl::model_ubs &Model) { m_bloom_timer.begin(); // down, the first step from the image itself diff --git a/rendering/opengl46/opengl46exposure.h b/rendering/opengl46/opengl46exposure.h index d634b6c7..68613cab 100644 --- a/rendering/opengl46/opengl46exposure.h +++ b/rendering/opengl46/opengl46exposure.h @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "gl/ubo.h" #include "gl/ssbo.h" +#include "gl/streambuffer.h" #include "gl/shader.h" #include "gl/postfx.h" #include "gl/framebuffer.h" @@ -56,7 +57,7 @@ class opengl46_exposure void measure(opengl_texture &Image, double const Deltatime); // takes the image down and up the chain; returns the result, which the tonemapping blends in. leaves the viewport // and the framebuffer binding changed. Model is the model_ubs the post processing shaders read the parameters from - opengl_texture *bloom(bloom_chain &Chain, opengl_texture &Image, gl::ubo &Modelubo, gl::model_ubs &Model); + opengl_texture *bloom(bloom_chain &Chain, opengl_texture &Image, gl::stream_ubo &Modelubo, gl::model_ubs &Model); // parameters of the tonemapping (postfx_tonemapping.frag), with bloom from a chain of Bloomlevels levels (0: none) glm::vec4 tonemap_params(std::size_t const Bloomlevels) const; // lines for the gpu times of the debug panel diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index e5ded79a..a0275944 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -178,14 +178,19 @@ bool opengl46_renderer::Init(GLFWwindow *Window) } scene_ubo = std::make_unique(sizeof(gl::scene_ubs), 0); - model_ubo = std::make_unique(sizeof(gl::model_ubs), 1, GL_STREAM_DRAW); + model_ubo = std::make_unique(sizeof(gl::model_ubs), 1, Global.gfx_bufferstreaming); light_ssbo = std::make_unique(sizeof(gl::light_data), gl::LIGHT_SSBO_BINDING); light_ssbo->bind_base(); - for (auto &buffer : m_instancebuffers) { - // grows when a frame needs more - buffer = std::make_unique(static_cast(16384 * sizeof(glm::mat4)), gl::INSTANCE_SSBO_BINDING); + // element 0 of the instance buffers is identity. This is the matrix sampled by + // non-instanced draws, so existing rendering paths + // continue to multiply effective_modelview = identity * modelview = modelview. + // the buffers grow when a frame needs more + glm::mat4 const identity(1.0f); + m_instancestream = std::make_unique( + static_cast(16384 * sizeof(glm::mat4)), static_cast(sizeof(glm::mat4)), Global.gfx_bufferstreaming, &identity, static_cast(sizeof(identity))); } + WriteLog(std::string("Model data streaming: ") + (model_ubo->streamed() ? "persistently mapped buffers" : "glBufferSubData")); try { @@ -213,15 +218,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) model_ubo->update(model_ubs); scene_ubo->update(scene_ubs); - // element 0 of the instance buffers is identity. This is the matrix sampled by - // non-instanced draws, so existing rendering paths - // continue to multiply effective_modelview = identity * modelview = modelview. - for (auto &buffer : m_instancebuffers) - { - glm::mat4 const identity(1.0f); - buffer->update(identity); - } - m_instancebuffers[m_instancebufferindex]->bind_base(); + Bind_Instance_Stream(); int samples = 1 << Global.iMultisampling; if (samples > 1) @@ -659,10 +656,11 @@ bool opengl46_renderer::Render() // scenery lamps which get shadows in this frame. their maps hold the scenery as of the version of the instance records if (simulation::is_ready && (false == Global.gfx_skiprendering)) m_localshadows.update_lamps(m_locallights.lights(), Global.pCamera.Pos, m_instancerecordsversion, m_framestamp); - // instanced draws of the frame place their matrices in the next of the instance buffers, the one used the longest ago - m_instancebufferindex = ( m_instancebufferindex + 1 ) % m_instancebuffers.size(); - m_instancebufferfill = 1; - m_instancebuffers[ m_instancebufferindex ]->bind_base(); + // instanced draws of the frame place their matrices in the next of the instance buffers, the one used the longest ago; + // the same for the model blocks of the draws + m_instancestream->next_frame(); + Bind_Instance_Stream(); + model_ubo->next_frame(); m_renderpass.draw_mode = rendermode::none; // force setup anew m_renderpass.draw_stats = debug_stats(); @@ -676,6 +674,8 @@ bool opengl46_renderer::Render() } glfwMakeContextCurrent(m_window); + model_ubo->bind_uniform(); + Bind_Instance_Stream(); gl::framebuffer::unbind(); @@ -766,8 +766,14 @@ void opengl46_renderer::SwapBuffers() + " instanced:" + to_string( m_colorpass.draw_stats.instances, 7 ) + " +" + to_string( shadowstats.instances, 7 ) + " =" + to_string( m_colorpass.draw_stats.instances + shadowstats.instances, 7 ) + " (" + std::to_string( m_colorpass.draw_stats.instanced_drawcalls + shadowstats.instanced_drawcalls ) + " batches)\n" - + " instance buffer: " + std::to_string( m_instancebufferfill - 1 ) + " matrices, " - + std::to_string( m_instancebuffers[ m_instancebufferindex ]->size() / 1024 ) + " KiB\n" + + " instance buffer: " + std::to_string( m_instancestream->used() / static_cast( sizeof( glm::mat4 ) ) ) + " matrices, " + + std::to_string( m_instancestream->size() / 1024 ) + " KiB\n" + + " model blocks: " + std::to_string( model_ubo->updates() ) + + ( model_ubo->streamed() ? + ", " + std::to_string( model_ubo->stream()->used() / 1024 ) + "/" + std::to_string( model_ubo->stream()->size() / 1024 ) + " KiB, streamed (waits " + + std::to_string( model_ubo->stream()->waits() + m_instancestream->waits() ) + ", grown " + + std::to_string( model_ubo->stream()->grows() + m_instancestream->grows() ) + ")\n" : + ", glBufferSubData\n" ) + " inst-pool:" + std::to_string( TAnimModel::s_instanceable_total ) + "/" + std::to_string( TAnimModel::s_classified_total ) + " rej(noModel/lights/anim/animSubM): " @@ -900,6 +906,9 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) { glfwMakeContextCurrent(vp.window); m_current_viewport = &vp; + // the streamed model block and the instance buffer of the frame move with every update, bindings are per context + model_ubo->bind_uniform(); + Bind_Instance_Stream(); } if ((!simulation::is_ready) || (Global.gfx_skiprendering)) @@ -3432,20 +3441,19 @@ void opengl46_renderer::Update_Instance_Records( TModel3d const *Model, scene::b // places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them std::uint32_t opengl46_renderer::Upload_Instance_Modelviews( glm::mat4 const *Modelviews, std::size_t const Count ) { - auto &buffer { m_instancebuffers[ m_instancebufferindex ] }; - auto const needed { static_cast( ( m_instancebufferfill + Count ) * sizeof( glm::mat4 ) ) }; - if( needed > buffer->size() ) { + auto const generation { m_instancestream->generation() }; + auto const offset { m_instancestream->push( Modelviews, static_cast( Count * sizeof( glm::mat4 ) ) ) }; + if( m_instancestream->generation() != generation ) { // the draws issued with the replaced buffer keep it until the gpu is done with them. the new one starts with identity - buffer->resize( std::max( needed, 2 * buffer->size() ) ); - glm::mat4 const identity( 1.0f ); - buffer->update( identity ); - buffer->bind_base(); - m_instancebufferfill = 1; + Bind_Instance_Stream(); } - auto const base { static_cast( m_instancebufferfill ) }; - buffer->update( Modelviews, static_cast( base * sizeof( glm::mat4 ) ), static_cast( Count * sizeof( glm::mat4 ) ) ); - m_instancebufferfill += Count; - return base; + return static_cast( offset / static_cast( sizeof( glm::mat4 ) ) ); +} + +void opengl46_renderer::Bind_Instance_Stream() const +{ + GLuint const buffer { m_instancestream->buffer() }; + ::glBindBuffersBase( GL_SHADER_STORAGE_BUFFER, gl::INSTANCE_SSBO_BINDING, 1, &buffer ); } // True GPU-instanced render path for a group of TAnimModel instances sharing the same @@ -3792,7 +3800,7 @@ void opengl46_renderer::Render_Instances_Indirect() model_ubs.emission = 0.0f; ::glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0 ); - m_instancebuffers[ m_instancebufferindex ]->bind_base(); + Bind_Instance_Stream(); } m_renderpass.draw_stats.instanced_drawcalls += static_cast( m_indirectdraws.size() ); diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index ebc833d3..98d4a1eb 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "vr/vr_interface.h" #include "gl/ubo.h" #include "gl/ssbo.h" +#include "gl/streambuffer.h" #include "gl/cluster_grid.h" #include "gl/framebuffer.h" #include "gl/renderbuffer.h" @@ -343,6 +344,8 @@ class opengl46_renderer : public gfx_renderer { void Update_Instance_Records( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket, glm::dvec3 const &Origin ); // places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them std::uint32_t Upload_Instance_Modelviews( glm::mat4 const *Modelviews, std::size_t const Count ); + // binds the instance buffer of the frame for the vertex shaders (INSTANCE_SSBO_BINDING) + void Bind_Instance_Stream() const; // draws the instances queued in the pass from the result of the gpu culling (Global.gfx_gpucull 1): makes the depth // pyramid of the occlusion test, runs the culling, walks the submodel trees of the groups once per lod band to record // the draws (m_indirectdraws, m_indirectcommands), has their instances filled in, then draws them with indirect draws, @@ -587,16 +590,17 @@ class opengl46_renderer : public gfx_renderer { std::unique_ptr m_vertex_shader; std::unique_ptr scene_ubo; - std::unique_ptr model_ubo; + // the model block of every draw, streamed (Global.gfx_bufferstreaming): each update to its own place in the buffer of + // the frame instead of glBufferSubData of the one block + std::unique_ptr model_ubo; std::unique_ptr light_ssbo; // per-instance modelview matrices of GPU-instanced draws, one buffer for each of the last frames, used in turn so // a frame doesn't write to the buffer the gpu may still read for an earlier one. element 0 of each is identity // and never overwritten: draws which aren't instanced (gl_BaseInstance == gl_InstanceID == 0) compute // identity*modelview in the vertex shader. Render_Instanced and Render_Sleepers append the camera-space root - // matrices of their instances past the ones placed earlier in the frame, and draw them from that base instance - std::array, 3> m_instancebuffers; - std::size_t m_instancebufferindex { 0 }; // the buffer of the current frame - std::size_t m_instancebufferfill { 1 }; // matrices placed in it so far, identity included + // matrices of their instances past the ones placed earlier in the frame, and draw them from that base instance. + // persistently mapped with Global.gfx_bufferstreaming + std::unique_ptr m_instancestream; // when > 0, the renderer is inside a Render_Instanced() call. The draw(handle) // method routes to glDrawElementsInstanced* with this many instances from // m_current_instance_base onwards rather than a single regular draw. The vertex shader reads diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index e7c2df1c..73ff7a24 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1444,6 +1444,11 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser.getTokens(1); Parser >> gfx_depthprepass; } + else if (Token == "gfx.bufferstreaming") + { + Parser.getTokens(1); + Parser >> gfx_bufferstreaming; + } else if (Token == "gfx.lights.clusterheatmap") { Parser.getTokens(1); @@ -1913,6 +1918,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.gpucull", std::string{ gfx_gpucull == 1 ? "yes" : gfx_gpucull == 2 ? "compare" : "no" } ); export_as_text( Output, "gfx.gpucull.occlusion", gfx_gpucull_occlusion ); export_as_text( Output, "gfx.depthprepass", gfx_depthprepass ); + export_as_text( Output, "gfx.bufferstreaming", gfx_bufferstreaming ); export_as_text( Output, "gfx.postfx.motionblur.enabled", gfx_postfx_motionblur_enabled ); export_as_text( Output, "gfx.postfx.motionblur.shutter", gfx_postfx_motionblur_shutter ); // TODO: export gfx_postfx_motionblur_format diff --git a/utilities/Globals.h b/utilities/Globals.h index 2d369b8d..24b73715 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -290,6 +290,9 @@ struct global_settings { int gfx_gpucull = 0; bool gfx_gpucull_occlusion = true; // with gfx_gpucull 1: instances hidden behind the depth drawn before them are left out bool gfx_depthprepass = true; // with gfx_gpucull 1: the depth of the instances is drawn before their colour + // (opengl 4.6) the model block of every draw and the matrices of instances are written to persistently mapped buffers, + // one for each of the last frames, instead of with glBufferSubData. read at the start + bool gfx_bufferstreaming = true; bool gfx_postfx_motionblur_enabled = true; float gfx_postfx_motionblur_shutter = 0.01f; GLenum gfx_postfx_motionblur_format = GL_RG16F;