From 85b76acca54f6f8aa8773072d0cf23ad1ee6fb4f Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 01:40:34 +0200 Subject: [PATCH] perf(sleepers): the instances of the sleepers and timbers of a track sent to the gpu once, as it's laid, and kept there in a buffer of their own; in each pass the runs of them in sight turned by the modelviews of their cells into the instance buffer of the frame by a compute pass, one dispatch and one barrier a pass, and drawn in one instanced draw for each model, part and level of detail, in place of the cpu turning every instance every frame --- CMakeLists.txt | 1 + gl/glsl_common.cpp | 5 + gl/streambuffer.cpp | 16 +- gl/streambuffer.h | 2 + gl/ubo.h | 18 +++ rendering/opengl46/opengl46renderer.cpp | 204 ++++++++++++++---------- rendering/opengl46/opengl46renderer.h | 21 ++- rendering/opengl46/opengl46timbers.cpp | 152 ++++++++++++++++++ rendering/opengl46/opengl46timbers.h | 66 ++++++++ shaders/timber_instances.comp | 46 ++++++ world/TrackSleepers.cpp | 4 + world/TrackSleepers.h | 1 + 12 files changed, 444 insertions(+), 92 deletions(-) create mode 100644 rendering/opengl46/opengl46timbers.cpp create mode 100644 rendering/opengl46/opengl46timbers.h create mode 100644 shaders/timber_instances.comp diff --git a/CMakeLists.txt b/CMakeLists.txt index e53d4aa5..f856e705 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -393,6 +393,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46localshadows.cpp" "rendering/opengl46/opengl46instancecull.cpp" "rendering/opengl46/opengl46hiz.cpp" + "rendering/opengl46/opengl46timbers.cpp" "rendering/opengl46/opengl46shadowfit.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 16fcb3f3..3fa04174 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -29,6 +29,11 @@ void gl::glsl_common_setup() "#define HIZ_GROUP_SIZE " + std::to_string(HIZ_GROUP_SIZE) + "\n" + "#define SHADOW_FIT_GROUP_SIZE " + std::to_string(SHADOW_FIT_GROUP_SIZE) + "\n" + "#define SHADOW_FIT_SSBO_BINDING " + std::to_string(SHADOW_FIT_SSBO_BINDING) + "\n" + + "#define TIMBER_GROUP_SIZE " + std::to_string(TIMBER_GROUP_SIZE) + "\n" + + "#define TIMBER_LOCAL_SSBO_BINDING " + std::to_string(TIMBER_LOCAL_SSBO_BINDING) + "\n" + + "#define TIMBER_RANGE_SSBO_BINDING " + std::to_string(TIMBER_RANGE_SSBO_BINDING) + "\n" + + "#define TIMBER_OUTPUT_SSBO_BINDING " + std::to_string(TIMBER_OUTPUT_SSBO_BINDING) + "\n" + + "#define TIMBER_ORIGIN_SSBO_BINDING " + std::to_string(TIMBER_ORIGIN_SSBO_BINDING) + "\n" + "#define PARTICLE_GROUP_SIZE " + std::to_string(PARTICLE_GROUP_SIZE) + "\n" + "#define PARTICLE_SSBO_BINDING " + std::to_string(PARTICLE_SSBO_BINDING) + "\n" + "#define PARTICLE_SOURCE_SSBO_BINDING " + std::to_string(PARTICLE_SOURCE_SSBO_BINDING) + "\n" + diff --git a/gl/streambuffer.cpp b/gl/streambuffer.cpp index 38c6dbe2..070723aa 100644 --- a/gl/streambuffer.cpp +++ b/gl/streambuffer.cpp @@ -141,6 +141,17 @@ void gl::frame_stream::next_frame() } GLintptr gl::frame_stream::push(void const *Data, GLsizeiptr Size) +{ + auto const offset{reserve(Size)}; + auto const ¤t{m_slots[m_slot]}; + if (current.data != nullptr) + std::memcpy(current.data + offset, Data, static_cast(Size)); + else + glNamedBufferSubData(current.buffer, offset, Size, Data); + return offset; +} + +GLintptr gl::frame_stream::reserve(GLsizeiptr Size) { auto *current{&m_slots[m_slot]}; auto offset{align_up(m_fill, m_alignment)}; @@ -167,13 +178,8 @@ GLintptr gl::frame_stream::push(void const *Data, GLsizeiptr Size) 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; } diff --git a/gl/streambuffer.h b/gl/streambuffer.h index e8363750..51efefca 100644 --- a/gl/streambuffer.h +++ b/gl/streambuffer.h @@ -34,6 +34,8 @@ namespace gl // bigger one (generation() changes, buffer() tells which to bind); the old one stays until the frame is done with, so // what was written to it can still be bound and drawn from later in the frame GLintptr push(void const *Data, GLsizeiptr Size); + // the room for Size bytes in the buffer of the frame, as push() makes it, left for the gpu to write (a compute pass) + GLintptr reserve(GLsizeiptr Size); GLuint buffer() const { diff --git a/gl/ubo.h b/gl/ubo.h index a070b78f..502665fe 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -506,6 +506,24 @@ namespace gl const GLuint PARTICLE_OUTPUT_SSBO_BINDING = 12; // what the draw reads, a particle_draw per particle drawn const GLuint PARTICLE_COMMAND_SSBO_BINDING = 13; // the indirect draw command, whose instances the passes count + // the instances of the timbers of the tracks (opengl46_timbers, timber_instances.comp): work group of the pass and the binding + // points of its buffers, ones of the instance culling, which binds its buffers before every use; the pass binds its own + const GLuint TIMBER_GROUP_SIZE = 64; + const GLuint TIMBER_LOCAL_SSBO_BINDING = 9; // the transforms of the instances of the tracks, relative to the origins of the tracks + const GLuint TIMBER_RANGE_SSBO_BINDING = 11; // the runs of them drawn, a timber_range each + const GLuint TIMBER_OUTPUT_SSBO_BINDING = 12; // the instance buffer of the frame, their modelviews written there + const GLuint TIMBER_ORIGIN_SSBO_BINDING = 13; // the modelviews of the origins of the cells of the runs + + // layout std430: GLSL timber_range. a run of instances: count of them from source in the transforms, to target in the output, + // turned by the modelview of origin + struct timber_range + { + uint32_t source; + uint32_t count; + uint32_t target; + uint32_t origin; + }; + // layout std430: GLSL smoke_particle_s struct particle_smoke { diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 8bd4c0fb..73c1809e 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -256,6 +256,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) shadowcull.init(&m_instancecull); m_reflectioncull.init(&m_instancecull); m_hiz.init(); + m_timbers.init(); m_shadowfit.init(); m_exposure.init(); m_ao.init(); @@ -739,6 +740,7 @@ bool opengl46_renderer::Render() // the same for the model blocks of the draws m_instancestream->next_frame(); Bind_Instance_Stream(); + m_timbers.next_frame(); model_ubo->next_frame(); if (m_drawblockstream) m_drawblockstream->next_frame(); @@ -3716,6 +3718,8 @@ void opengl46_renderer::Render(cell_sequence::iterator First, cell_sequence::ite ++First; } + // the sleepers and timbers of the tracks of all the cells at once + Flush_Timbers(); // second pass draws elements with their own vbos while (first != Last) { @@ -4413,6 +4417,16 @@ std::uint32_t opengl46_renderer::Upload_Instance_Modelviews( glm::mat4 const *Mo return static_cast( offset / static_cast( sizeof( glm::mat4 ) ) ); } +std::uint32_t opengl46_renderer::Reserve_Instance_Modelviews( std::size_t const Count ) +{ + auto const generation { m_instancestream->generation() }; + auto const offset { m_instancestream->reserve( static_cast( Count * sizeof( glm::mat4 ) ) ) }; + if( m_instancestream->generation() != generation ) { + Bind_Instance_Stream(); + } + return static_cast( offset / static_cast( sizeof( glm::mat4 ) ) ); +} + void opengl46_renderer::Bind_Instance_Stream() const { GLuint const buffer { m_instancestream->buffer() }; @@ -6186,10 +6200,11 @@ void opengl46_renderer::Render_Sleepers( TTrack *Track ) } } -// Renders the sleepers and switch timbers of a run of tracks of a cell. Each beam is made of instances of the parts of a model or -// of a whole model, kept by its track relative to the origin of the track. The runs of beams in range and in sight are gathered -// for the whole run of tracks by the model and its part or its level of detail, and each is drawn in one instanced batch. -// In the shadow pass only those near the camera are drawn, their shadows falling where they can be seen. +// Gathers the sleepers and switch timbers of a run of tracks of a cell. Each beam is made of instances of the parts of a model or +// of a whole model, kept by its track relative to the origin of the track, on the cpu and on the gpu (m_timbers). The runs of beams +// in range and in sight are gathered by the model and its part or its level of detail, for all the cells of the pass, which +// Flush_Timbers() draws each in one instanced batch. In the shadow pass only those near the camera are drawn, their shadows falling +// where they can be seen. void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last ) { if( Global.SleeperDistance <= 0.f ) { return; } @@ -6209,19 +6224,15 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ } // the fastenings laid out to a little past where they're drawn auto const fasteningrange = std::min( static_cast( Global.SleeperDistance ), track_sleepers::fastening_range ) + track_sleepers::fastening_lead; - glm::mat4 const originmodelview = OpenGLMatrices.data( GL_MODELVIEW ); - for( auto &bucket : m_timberbuckets ) { - bucket.instances.clear(); - } - auto const bucket_for = [&]( track_sleepers::beam_model const *Model, int const Part, float const Band ) -> std::vector & { + auto const bucket_for = [&]( track_sleepers::beam_model const *Model, int const Part, float const Band ) -> timber_bucket & { for( auto &bucket : m_timberbuckets ) { if( bucket.model == Model && bucket.part == Part && bucket.band == Band ) { - return bucket.instances; + return bucket; } } - m_timberbuckets.push_back( { Model, Part, Band, {} } ); - return m_timberbuckets.back().instances; }; - // the level of detail of the whole model a beam is seen at, from its distance + m_timberbuckets.push_back( { Model, Part, Band, {}, 0, 0 } ); + return m_timberbuckets.back(); }; + // the level of detail of the whole model a run of beams is seen at, from its distance auto const band_of = []( track_sleepers::beam_model const &Model, double const Distance ) { auto const squaredistance { static_cast( Distance * Distance ) }; auto band { Model.bands.empty() ? 0.f : Model.bands.front() }; @@ -6231,21 +6242,7 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ } } return band; }; - // the beams of a run made of a whole model, each at the level of detail of its own distance; the run is culled as one - auto const put_by_band = [&]( track_sleepers::beam_model const &Model, std::vector const &Transforms, std::uint32_t const From, std::uint32_t const To, glm::dvec3 const &Origin ) { - std::vector *instances { nullptr }; - auto current { 0.f }; - for( auto index = From; index < To; ++index ) { - auto const &local = Transforms[ index ]; - auto const band = band_of( Model, glm::length( Origin + glm::dvec3( glm::vec3( local[ 3 ] ) ) - camera ) ); - if( instances == nullptr || band != current ) { - // looked up again only as the band changes; a bucket added on the way moves the others, so the pointer goes with it - instances = &bucket_for( &Model, -1, band ); - current = band; - } - instances->emplace_back( originmodelview * local ); - } - }; + m_timbertracks.clear(); for( auto first { First }; first != Last; ++first ) { auto *track = *first; if( false == track->m_visible || false == track->m_sleepers3d.active ) { continue; } @@ -6272,10 +6269,33 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ m_timberlaying += std::chrono::duration( std::chrono::steady_clock::now() - start ).count(); } } + if( track->m_sleepers3d.timbers == nullptr || track->m_sleepers3d.chunks.empty() ) { continue; } + m_timbertracks.push_back( track ); + } + if( m_timbertracks.empty() ) { return; } + // the instances of the tracks on the gpu; those of the cells before expanded first when the buffer is to be started over + if( false == m_timbers.place( m_timbertracks, false ) ) { + Flush_Timbers(); + m_timbers.place( m_timbertracks, true ); + } + // the runs of them in range and in sight, turned by the modelview of the cell + auto const origin { static_cast( m_timberorigins.size() ) }; + m_timberorigins.push_back( OpenGLMatrices.data( GL_MODELVIEW ) ); + // a run of instances of a track for the bucket; the reference to it goes with the next bucket added + auto const add_run = [&]( timber_bucket &Bucket, std::uint32_t const Source, std::uint32_t const Count ) { + Bucket.runs.push_back( { Source, Count, 0, origin } ); + Bucket.count += Count; }; + for( auto *track : m_timbertracks ) { auto const &laid = track->m_sleepers3d; - if( laid.timbers == nullptr ) { continue; } - // the transforms of the beams are relative to the origin of the track, which the modelview is translated to already - auto const &trackmodelview = originmodelview; + // where the instances of the parts, of the whole timbers and of the plain sleepers of the track start on the gpu + std::array partfirst; + auto next { m_timbers.first( *track ) }; + for( int part = 0; part < track_sleepers::part_count; ++part ) { + partfirst[ part ] = next; + next += static_cast( laid.instances[ part ].size() ); + } + auto const wholefirst { next }; + auto const plainfirst { wholefirst + static_cast( laid.whole.size() ) }; for( auto const &chunk : laid.chunks ) { auto const centre = track->m_origin + glm::dvec3( chunk.centre ); auto const distance = glm::length( centre - camera ); @@ -6286,66 +6306,86 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ if( chunk.last[ part ] <= chunk.first[ part ] ) { continue; } // the fastenings only near, they're small if( part >= track_sleepers::timber_parts && distance > std::min( range, track_sleepers::fastening_range ) + chunk.radius ) { continue; } - auto &instances = bucket_for( laid.timbers, part, 0.f ); - for( auto index = chunk.first[ part ]; index < chunk.last[ part ]; ++index ) { - instances.emplace_back( trackmodelview * laid.instances[ part ][ index ] ); + add_run( bucket_for( laid.timbers, part, 0.f ), partfirst[ part ] + chunk.first[ part ], chunk.last[ part ] - chunk.first[ part ] ); + } + } + // the beams made of a whole model at the level of detail of the distance of the run + if( chunk.wholelast > chunk.wholefirst ) { + add_run( bucket_for( laid.timbers, -1, band_of( *laid.timbers, distance ) ), wholefirst + chunk.wholefirst, chunk.wholelast - chunk.wholefirst ); + } + if( laid.plains != nullptr && chunk.plainlast > chunk.plainfirst ) { + add_run( bucket_for( laid.plains, -1, band_of( *laid.plains, distance ) ), plainfirst + chunk.plainfirst, chunk.plainlast - chunk.plainfirst ); + } + } + } +} + +// the runs of the sleepers and timbers gathered by Render_Timbers() over the cells of the pass made into modelviews on the gpu at +// once, and each bucket drawn in one instanced batch +void opengl46_renderer::Flush_Timbers() +{ + std::size_t total { 0 }; + for( auto const &bucket : m_timberbuckets ) { + total += bucket.count; + } + if( total > 0 ) { + // in the instance buffer of the frame bucket after bucket, as in Render_Sleepers(), which the recorded walk of the depth + // pass keeps for its replay as well + auto target { Reserve_Instance_Modelviews( total ) }; + m_timberranges.clear(); + for( auto &bucket : m_timberbuckets ) { + bucket.first = target; + for( auto const &run : bucket.runs ) { + m_timberranges.push_back( { run.source, run.count, target, run.origin } ); + target += run.count; + } + } + m_timbers.expand( m_timberorigins, m_timberranges, m_instancestream->buffer() ); + auto const alpha = ( 0x30300030 ^ 0x0F0F000F ); + for( auto &bucket : m_timberbuckets ) { + if( bucket.count == 0 ) { continue; } + auto const &beammodel = *bucket.model; + auto *model = beammodel.model; + if( bucket.part >= 0 ) { + // only the part of the model is visible, its siblings are skipped as the model is walked + for( int part = 0; part < track_sleepers::part_count; ++part ) { + if( beammodel.parts[ part ] != nullptr ) { + beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); } } } - if( chunk.wholelast > chunk.wholefirst ) { - put_by_band( *laid.timbers, laid.whole, chunk.wholefirst, chunk.wholelast, track->m_origin ); + material_data skins {}; + if( beammodel.skin != null_handle ) { + skins.replacable_skins[ 1 ] = beammodel.skin; } - if( laid.plains != nullptr && chunk.plainlast > chunk.plainfirst ) { - put_by_band( *laid.plains, laid.plain, chunk.plainfirst, chunk.plainlast, track->m_origin ); + ::glPushMatrix(); + ::glLoadIdentity(); + m_current_instance_count = bucket.count; + m_current_instance_base = bucket.first; + // inside the range of the level of detail the beams are at + model->Root->fSquareDist = ( bucket.part >= 0 ? 0.f : bucket.band + 1.f ); + model->Root->ReplacableSet( ( beammodel.skin != null_handle ? skins.replacable_skins : nullptr ), alpha ); + model->Root->pRoot = model; + Render( model->Root ); + m_current_instance_count = 0; + m_current_instance_base = 0; + ::glPopMatrix(); + ++m_renderpass.draw_stats.instanced_drawcalls; + if( bucket.part >= 0 ) { + for( int part = 0; part < track_sleepers::part_count; ++part ) { + if( beammodel.parts[ part ] != nullptr ) { + beammodel.parts[ part ]->iVisible = 1; + } + } } } + m_renderpass.draw_stats.instances += static_cast( total ); } - - auto const alpha = ( 0x30300030 ^ 0x0F0F000F ); - std::size_t total { 0 }; for( auto &bucket : m_timberbuckets ) { - auto const &instances = bucket.instances; - if( instances.empty() ) { continue; } - total += instances.size(); - auto const &beammodel = *bucket.model; - auto *model = beammodel.model; - if( bucket.part >= 0 ) { - // only the part of the model is visible, its siblings are skipped as the model is walked - for( int part = 0; part < track_sleepers::part_count; ++part ) { - if( beammodel.parts[ part ] != nullptr ) { - beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); - } - } - } - material_data skins {}; - if( beammodel.skin != null_handle ) { - skins.replacable_skins[ 1 ] = beammodel.skin; - } - // the instances go to the instance stream of the frame, as in Render_Sleepers(), which the recorded walk of the depth - // pass keeps for its replay as well - auto const baseinstance { Upload_Instance_Modelviews( instances.data(), instances.size() ) }; - ::glPushMatrix(); - ::glLoadIdentity(); - m_current_instance_count = instances.size(); - m_current_instance_base = baseinstance; - // inside the range of the level of detail the beams are at - model->Root->fSquareDist = ( bucket.part >= 0 ? 0.f : bucket.band + 1.f ); - model->Root->ReplacableSet( ( beammodel.skin != null_handle ? skins.replacable_skins : nullptr ), alpha ); - model->Root->pRoot = model; - Render( model->Root ); - m_current_instance_count = 0; - m_current_instance_base = 0; - ::glPopMatrix(); - ++m_renderpass.draw_stats.instanced_drawcalls; - if( bucket.part >= 0 ) { - for( int part = 0; part < track_sleepers::part_count; ++part ) { - if( beammodel.parts[ part ] != nullptr ) { - beammodel.parts[ part ]->iVisible = 1; - } - } - } + bucket.runs.clear(); + bucket.count = 0; } - m_renderpass.draw_stats.instances += static_cast( total ); + m_timberorigins.clear(); } bool opengl46_renderer::Render(TDynamicObject *Dynamic) diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 0c269640..26c45d4f 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -44,6 +44,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46localshadows.h" #include "rendering/opengl46/opengl46instancecull.h" #include "rendering/opengl46/opengl46hiz.h" +#include "rendering/opengl46/opengl46timbers.h" #include "rendering/opengl46/opengl46shadowfit.h" // bare-bones render controller, in lack of anything better yet @@ -376,6 +377,8 @@ class opengl46_renderer : public gfx_renderer { static scene::bounding_area Bucket_Area( scene::basic_cell::instance_bucket const &Bucket ); // 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 ); + // the room for Count modelviews in the instance buffer of the frame, for the gpu to write. returns: base instance of the first + std::uint32_t Reserve_Instance_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 @@ -491,9 +494,11 @@ class opengl46_renderer : public gfx_renderer { // queues the sleepers of a track in the gpu culling of the pass (Global.gfx_gpucull_sleepers), with the gates of // Render_Sleepers(), making their records first when they're missing or out of date void Queue_Sleepers( TTrack *Track ); - // renders the sleepers and switch timbers of the tracks in range (TTrack::m_sleepers3d), laying them first where they're out of date. - // the parts of the model are drawn in instanced batches for the whole run of tracks, the runs of beams out of sight left out + // gathers the sleepers and switch timbers of the tracks in range (TTrack::m_sleepers3d), laying them first where they're out of + // date. the parts of the model are drawn in instanced batches for all the cells of the pass, the runs of beams out of sight left out void Render_Timbers( scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last ); + // draws what Render_Timbers() gathered in the pass since the last time + void Flush_Timbers(); // queues a model instance drawn on its own in the gpu culling of the pass (Global.gfx_gpucull_models), as a bucket of // its own, making its record first when it's missing or out of date. Inrange: the cell of the instance lies within the // range of the pass. returns true when the instance isn't drawn by the cpu in the pass: it's drawn from the result of the @@ -709,6 +714,7 @@ class opengl46_renderer : public gfx_renderer { std::unordered_map, scene::basic_cell::instance_bucket_key_hash> m_sleeperbandcounts; // depth pyramid for the occlusion test of the gpu culling opengl46_hiz m_hiz; + opengl46_timbers m_timbers; // the instances of the sleepers and timbers of the tracks on the gpu // the cascades of the sun shadows fitted to the depth of the main view (Global.gfx_shadow_sdsm) opengl46_shadow_fit m_shadowfit; // with a cascade fitted: the corners of the area its map is to cover, for setup_pass() @@ -989,15 +995,20 @@ class opengl46_renderer : public gfx_renderer { // per-instance modelview from instance_ssbo[gl_BaseInstance + gl_InstanceID]. Reset to 0 on exit. std::size_t m_current_instance_count { 0 }; std::uint32_t m_current_instance_base { 0 }; - // camera-space transforms of the sleepers and timbers drawn by Render_Timbers, by the model and its part or its level of - // detail; kept from frame to frame to reuse their allocations + // the runs of the instances of the sleepers and timbers drawn by Render_Timbers, by the model and its part or its level of + // detail, where they lie in m_timbers; kept from frame to frame to reuse their allocations struct timber_bucket { track_sleepers::beam_model const *model { nullptr }; int part { -1 }; // of a model put together from parts; -1: the whole model float band { 0.f }; // the squared distance the level of detail of the whole model is seen from - std::vector instances; + std::vector runs; + std::uint32_t count { 0 }; // instances of the runs + std::uint32_t first { 0 }; // base instance of their modelviews in the instance buffer of the frame }; std::vector m_timberbuckets; + std::vector m_timbertracks; // the tracks of a cell with beams to draw + std::vector m_timberranges; // the runs of all the buckets, as they're expanded + std::vector m_timberorigins; // the modelviews of the cells of the runs // persistent scratch buffer for Render_Instanced(): holds the per-instance // camera-space root modelview matrices for the batch currently being built. // Kept as a member rather than a function-local so its heap allocation is diff --git a/rendering/opengl46/opengl46timbers.cpp b/rendering/opengl46/opengl46timbers.cpp new file mode 100644 index 00000000..e6d122ff --- /dev/null +++ b/rendering/opengl46/opengl46timbers.cpp @@ -0,0 +1,152 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/opengl46/opengl46timbers.h" + +#include "utilities/Globals.h" +#include "world/Track.h" + +#include + +namespace { + +// the buffer to start with, and how soon after being started over it's too small for the tracks in sight +std::uint32_t constexpr initial_capacity{65536}; +unsigned int constexpr restart_frames{600}; + +// the instances of a track, in the order they lie in the buffer +std::uint32_t count_of(track_sleepers::laid const &Laid) +{ + std::size_t count{Laid.whole.size() + Laid.plain.size()}; + for (auto const &instances : Laid.instances) + count += instances.size(); + return static_cast(count); +} + +} // namespace + +opengl46_timbers::~opengl46_timbers() +{ + if (m_buffer != 0) + glDeleteBuffers(1, &m_buffer); +} + +void opengl46_timbers::init() +{ + gl::shader shader("timber_instances.comp"); + m_program = std::make_unique(std::vector>{shader}); + // the modelviews of the origins and the runs, both placed at whole matrices + m_ranges = std::make_unique( + static_cast(4096 * sizeof(gl::timber_range)), static_cast(sizeof(glm::mat4)), Global.gfx_bufferstreaming); + restart(0); +} + +void opengl46_timbers::restart(std::uint32_t const Needed) +{ + auto capacity{std::max(m_capacity, initial_capacity)}; + if ((m_capacity > 0) && (m_frame - m_restartframe < restart_frames)) + capacity *= 2; + while (capacity < 2 * Needed) + capacity *= 2; + // the draws and passes issued with the old buffer keep it until the gpu is done with them + if (m_buffer != 0) + glDeleteBuffers(1, &m_buffer); + glCreateBuffers(1, &m_buffer); + glNamedBufferStorage(m_buffer, static_cast(capacity) * static_cast(sizeof(glm::mat4)), nullptr, GL_DYNAMIC_STORAGE_BIT); + m_capacity = capacity; + m_fill = 0; + m_restartframe = m_frame; + m_tracks.clear(); +} + +bool opengl46_timbers::place(std::vector const &Tracks, bool const Restart) +{ + auto const placed_now = [&](TTrack const &Track) { + auto const found{m_tracks.find(&Track)}; + return (found != m_tracks.end()) && (found->second.made == Track.m_sleepers3d.made); + }; + std::uint32_t needed{0}; + std::uint32_t all{0}; + for (auto const *track : Tracks) + { + auto const count{count_of(track->m_sleepers3d)}; + all += count; + if (false == placed_now(*track)) + needed += count; + } + if (needed == 0) + return true; + if (m_fill + needed > m_capacity) + { + if (false == Restart) + return false; + restart(all); + } + for (auto const *track : Tracks) + { + if (placed_now(*track)) + continue; + auto const &laid{track->m_sleepers3d}; + auto const first{m_fill}; + auto const send = [&](std::vector const &Instances) { + if (false == Instances.empty()) + glNamedBufferSubData(m_buffer, static_cast(m_fill) * static_cast(sizeof(glm::mat4)), + static_cast(Instances.size() * sizeof(glm::mat4)), Instances.data()); + m_fill += static_cast(Instances.size()); + }; + for (auto const &instances : laid.instances) + send(instances); + send(laid.whole); + send(laid.plain); + m_tracks[track] = {laid.made, first}; + } + return true; +} + +std::uint32_t opengl46_timbers::first(TTrack const &Track) const +{ + auto const found{m_tracks.find(&Track)}; + return (found != m_tracks.end() ? found->second.first : 0); +} + +void opengl46_timbers::expand(std::vector const &Origins, std::vector const &Ranges, GLuint const Output) +{ + if (Ranges.empty()) + return; + // sent together, in one buffer of the stream + auto const originbytes{Origins.size() * sizeof(glm::mat4)}; + m_upload.resize(originbytes + Ranges.size() * sizeof(gl::timber_range)); + std::memcpy(m_upload.data(), Origins.data(), originbytes); + std::memcpy(m_upload.data() + originbytes, Ranges.data(), Ranges.size() * sizeof(gl::timber_range)); + auto const origins{m_ranges->push(m_upload.data(), static_cast(m_upload.size()))}; + auto const ranges{origins + static_cast(originbytes)}; + GLuint const buffers[]{m_buffer, m_ranges->buffer()}; + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_LOCAL_SSBO_BINDING, 1, &buffers[0]); + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_RANGE_SSBO_BINDING, 1, &buffers[1]); + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_OUTPUT_SSBO_BINDING, 1, &Output); + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_ORIGIN_SSBO_BINDING, 1, &buffers[1]); + // a dispatch takes at most 65535 work groups + auto const first{static_cast(ranges / static_cast(sizeof(gl::timber_range)))}; + auto const origin{static_cast(origins / static_cast(sizeof(glm::mat4)))}; + for (std::size_t done = 0; done < Ranges.size();) + { + auto const count{std::min(Ranges.size() - done, 65535)}; + ::glProgramUniform4ui(*m_program, 0, first + static_cast(done), origin, 0, 0); + m_program->dispatch(static_cast(count)); + done += count; + } + ::glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); +} + +void opengl46_timbers::next_frame() +{ + ++m_frame; + m_ranges->next_frame(); +} diff --git a/rendering/opengl46/opengl46timbers.h b/rendering/opengl46/opengl46timbers.h new file mode 100644 index 00000000..5790e8f2 --- /dev/null +++ b/rendering/opengl46/opengl46timbers.h @@ -0,0 +1,66 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "gl/shader.h" +#include "gl/streambuffer.h" +#include "gl/ubo.h" + +class TTrack; + +// the instances of the timbers and sleepers of the tracks (track_sleepers::laid) kept on the gpu, those of a track sent once as +// it's laid, and the runs of them drawn made into the modelviews of the instance buffer of the frame by a compute pass +// (timber_instances.comp): for a run the cpu gives only where it lies and where it goes, not its matrices +class opengl46_timbers +{ + public: + opengl46_timbers() = default; + opengl46_timbers(opengl46_timbers const &) = delete; + opengl46_timbers &operator=(opengl46_timbers const &) = delete; + ~opengl46_timbers(); + + // throws gl::shader_exception + void init(); + // the instances of the tracks on the gpu, sent again for a track laid anew since; when those to send don't fit in what's left + // of the buffer, it's started over for all the tracks, a bigger one when it was started over not long before. without + // Restart nothing is sent then, and false returned: the runs of the instances placed before are to be expanded first + bool place(std::vector const &Tracks, bool const Restart); + // where the instances of a track placed lie in the buffer: the first of the parts in turn, then of the whole timbers, then of + // the plain sleepers + std::uint32_t first(TTrack const &Track) const; + // the runs written to the instance buffer Output, each as the modelview of its origin times its transforms, for the draws after + void expand(std::vector const &Origins, std::vector const &Ranges, GLuint const Output); + void next_frame(); + + private: + struct placed + { + std::uint64_t made; // the making of the instances of the track sent + std::uint32_t first; + }; + + // a new buffer, room for Needed instances at least, the tracks to be sent again + void restart(std::uint32_t const Needed); + + std::unique_ptr m_program; + std::unique_ptr m_ranges; + GLuint m_buffer{0}; + std::uint32_t m_capacity{0}; // in instances + std::uint32_t m_fill{0}; + unsigned int m_frame{0}; + unsigned int m_restartframe{0}; + std::unordered_map m_tracks; + std::vector m_upload; // the origins and the runs of an expand(), as they're sent +}; diff --git a/shaders/timber_instances.comp b/shaders/timber_instances.comp new file mode 100644 index 00000000..e1f7d6c9 --- /dev/null +++ b/shaders/timber_instances.comp @@ -0,0 +1,46 @@ +// the modelviews of the instances of the timbers and sleepers of the tracks drawn in a pass (opengl46_timbers): their transforms +// are kept relative to the origins of the tracks, each run of them is turned by the modelview of the origin of its cell and written +// to the instance buffer of the frame, where the instanced draws read it. a work group a run + +#include + +layout(local_size_x = TIMBER_GROUP_SIZE) in; + +struct timber_range +{ + uint source; + uint count; + uint target; + uint origin; +}; + +layout(std430, binding = TIMBER_LOCAL_SSBO_BINDING) readonly buffer timber_local_ssbo +{ + mat4 timber_local[]; +}; + +layout(std430, binding = TIMBER_RANGE_SSBO_BINDING) readonly buffer timber_range_ssbo +{ + timber_range timber_ranges[]; +}; + +layout(std430, binding = TIMBER_OUTPUT_SSBO_BINDING) writeonly buffer timber_output_ssbo +{ + mat4 timber_output[]; +}; + +layout(std430, binding = TIMBER_ORIGIN_SSBO_BINDING) readonly buffer timber_origin_ssbo +{ + mat4 timber_origins[]; +}; + +// x: the first run of the dispatch, y: the first modelview of the origins +layout(location = 0) uniform uvec4 params; + +void main() +{ + timber_range range = timber_ranges[params.x + gl_WorkGroupID.x]; + mat4 origin = timber_origins[params.y + range.origin]; + for (uint index = gl_LocalInvocationID.x; index < range.count; index += TIMBER_GROUP_SIZE) + timber_output[range.target + index] = origin * timber_local[range.source + index]; +} diff --git a/world/TrackSleepers.cpp b/world/TrackSleepers.cpp index f819d8c1..56843763 100644 --- a/world/TrackSleepers.cpp +++ b/world/TrackSleepers.cpp @@ -125,11 +125,15 @@ std::pair shared_zone( TTrack const &Track, int const Path ) { namespace { +// the instances made so far, of all the tracks +std::uint64_t makings { 0 }; + // the beams as instances of the parts of the model of the track: those of the fastenings with them, or left for later and the beams // kept for them meanwhile void instance( TTrack &Track, std::vector &&Beams, bool const Fastened ) { auto &laid { Track.m_sleepers3d }; + laid.made = ++makings; for( auto &instances : laid.instances ) { instances.clear(); } diff --git a/world/TrackSleepers.h b/world/TrackSleepers.h index ba9772e2..f6c3730b 100644 --- a/world/TrackSleepers.h +++ b/world/TrackSleepers.h @@ -103,6 +103,7 @@ struct laid { std::vector whole; // transforms of the timbers of a model without parts, drawn whole std::vector plain; // transforms of the plain sleepers, whole models, relative to the origin of the track std::vector chunks; + std::uint64_t made { 0 }; // which making of the instances they are, none other of any track the same: a copy of them tells by it bool fastened { false }; // the fastenings are laid, else they're laid as the track comes near where they're drawn std::vector beams; // kept for the fastenings till they're laid };