From 55ba7a9f41e463dcb94f2adaf6859d8563df316c Mon Sep 17 00:00:00 2001 From: pawelw Date: Sat, 7 Nov 2026 12:35:55 +0100 Subject: [PATCH] Instancing: skip the buckets of a cell lying wholly beyond the range of their instances (#225) * perf(instancing): skip a cell's buckets lying wholly beyond the range of their instances * perf(instancing): go only through the instances drawn one by one in the per-node pass * perf(instancing): leave out of the color pass the cells holding only instances too far to be drawn * refactor(instancing): keep the caches of the instances of a cell without mutable fields, set by the renderer when the cell is queued --- model/AnimModel.cpp | 3 ++ model/AnimModel.h | 3 ++ rendering/opengl33renderer.cpp | 93 +++++++++++++++++++++++++++++++++- rendering/opengl33renderer.h | 6 +++ scene/scene.cpp | 3 ++ scene/scene.h | 13 +++++ 6 files changed, 119 insertions(+), 2 deletions(-) diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index 770790c1e..cafb3a93e 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -514,6 +514,7 @@ bool submodel_tree_blocks_instancing( TSubModel const *Sub ) { } // anonymous namespace int TAnimModel::s_instanceable_total = 0; +unsigned int TAnimModel::s_instanceable_revoked = 0; int TAnimModel::s_classified_total = 0; int TAnimModel::s_rejected_no_pmodel = 0; int TAnimModel::s_rejected_lights = 0; @@ -551,6 +552,7 @@ std::shared_ptr TAnimModel::AddContainer(std::string const &Name m_animlist.push_back(tmp); // containers are added by events after the instance was loaded and bucketed; // the instanced path skips RaAnimate(), so this instance has to be drawn on its own + if( m_instanceable ) { ++s_instanceable_revoked; } m_instanceable = false; return tmp; } @@ -780,6 +782,7 @@ void TAnimModel::SkinSet( int const Index, material_handle const Material ) { m_materialdata.replacable_skins[ std::clamp( Index, 1, 4 ) ] = Material; // the instance stays in the bucket of its old skin set, where it would get the textures of the batch + if( m_instanceable ) { ++s_instanceable_revoked; } m_instanceable = false; } diff --git a/model/AnimModel.h b/model/AnimModel.h index 0087418df..2cf1003bb 100644 --- a/model/AnimModel.h +++ b/model/AnimModel.h @@ -237,6 +237,9 @@ public: // diagnostic counters (process-wide). Updated inside update_instanceable_flag() // so the renderer can surface load-time classification stats in the debug overlay. static int s_instanceable_total; // count of TAnimModel*'s classified instanceable + // bumped whenever an instance loses its instanceable status after it was put in a scene cell (an event adds + // an animation container, or changes a skin); the cells rebuild their lists of instances drawn one by one then + static unsigned int s_instanceable_revoked; static int s_classified_total; // count of TAnimModel*'s that ran through the classifier static int s_rejected_no_pmodel; static int s_rejected_lights; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 5c64988b4..33346fad2 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2666,6 +2666,12 @@ void opengl33_renderer::Render(section_sequence::iterator First, section_sequenc { if ((true == cell.m_active) && (m_renderpass.pass_camera.visible(cell.m_area))) { + // a cell with nothing but instances too far to be drawn is left out of the color pass altogether + if( ( true == Prepare_Instances( cell ) ) + && ( m_renderpass.draw_mode == rendermode::color ) + && ( true == Cell_Out_Of_Range( cell ) ) ) { + continue; + } // store visible cells with content as well as their current distance, for sorting later m_cellqueue.emplace_back(glm::length2(m_renderpass.pass_camera.position() - cell.m_area.center), &cell); } @@ -2680,6 +2686,7 @@ void opengl33_renderer::Render(section_sequence::iterator First, section_sequenc { if ((true == cell.m_active) && (m_renderpass.pass_camera.visible(cell.m_area))) { + Prepare_Instances( cell ); // store visible cells with content as well as their current distance, for sorting later m_cellqueue.emplace_back(glm::length2(m_renderpass.pass_camera.position() - cell.m_area.center), &cell); } @@ -2871,8 +2878,10 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite Queue_Instances( cell ); } } - // remaining (non-instanceable) opaque instance nodes go through the per-node path - for (auto *instance : cell->m_instancesopaque) + // remaining (non-instanceable) opaque instance nodes go through the per-node path. the list of them kept for + // the cell spares going through every instance of it only to skip the instanceable ones - with many of these + // (vegetation) that took more time than drawing everything else + for (auto *instance : ( Instance_Cache_Valid( *cell ) ? cell->m_instancecache.single : cell->m_instancesopaque )) { if( instance->m_instanceable ) { continue; } // already handled via bucket Render(instance); @@ -3178,6 +3187,11 @@ void opengl33_renderer::Queue_Instances( scene::basic_cell const *Cell ) auto const distancelimit { slack * ( reflections ? sq( static_cast( Global.reflectiontune.range_instances ) ) : sq( static_cast( m_renderpass.draw_range ) + 250.0 ) ) }; // up close the rounding of the copies is too large a part of the distance for the slack to cover it auto const distancetested { sq( 64.0 ) }; + // the nearest point of the bounding sphere of the cell, which holds the locations of all its instances. no instance + // of the cell is closer, so where this distance fails the range test below for the largest range in a bucket, + // it fails it for each instance of that bucket, and the bucket can be skipped without going through them + auto const cellnearestdistance { std::max( 0.0, glm::length( Cell->m_area.center - cameraposition ) - Cell->m_area.radius ) }; + auto const cellnearestdistancesquared { cellnearestdistance * cellnearestdistance }; for( auto const &bucket : Cell->m_instancebuckets_opaque ) { auto const &instances { bucket.second.instances }; @@ -3190,6 +3204,11 @@ void opengl33_renderer::Queue_Instances( scene::basic_cell const *Cell ) || ( bucket.second.bounds.size() != instances.size() ) ) { Update_Instance_Bounds( bucket.first.pModel, bucket.second ); } + else if( ( bucket.second.rangeversion == m_instanceboundsversion ) + && ( cellnearestdistancesquared > distancetested ) + && ( cellnearestdistancesquared * distancescale + distanceoffset >= slack * bucket.second.rangesquaredmax ) ) { + continue; + } auto const &bounds { bucket.second.bounds }; std::vector *dest { nullptr }; // looked up when there's something to put in it for( std::size_t idx = 0; idx < bounds.size(); ++idx ) { @@ -3210,6 +3229,76 @@ void opengl33_renderer::Queue_Instances( scene::basic_cell const *Cell ) } } +// brings up to date what the renderer keeps of the instances of the cell: the copies of the bounds of the instances +// and the largest range in each bucket, the largest range in the cell, and the list of its instances drawn one by one. +// rebuilt after the content of the cell changed, or any instance lost its instanceable status (an event added an +// animation container, or changed a skin). returns: true if the cache can be used; it can't in the editor, which +// moves and resizes the instances in place, as with the copies of their bounds +bool opengl33_renderer::Prepare_Instances( scene::basic_cell &Cell ) +{ + if( EditorModeFlag ) { return false; } + if( Instance_Cache_Valid( Cell ) ) { return true; } + + auto &cache { Cell.m_instancecache }; + cache.rangesquaredmax = 0.f; + for( auto &[ key, bucket ] : Cell.m_instancebuckets_opaque ) { + if( ( bucket.boundsversion != m_instanceboundsversion ) + || ( bucket.bounds.size() != bucket.instances.size() ) ) { + Update_Instance_Bounds( key.pModel, bucket ); + } + auto rangesquaredmax { 0.f }; + for( auto const &instancebounds : bucket.bounds ) { + rangesquaredmax = std::max( rangesquaredmax, instancebounds.rangesquaredmax ); + } + bucket.rangesquaredmax = rangesquaredmax; + bucket.rangeversion = m_instanceboundsversion; + cache.rangesquaredmax = std::max( cache.rangesquaredmax, rangesquaredmax ); + } + cache.single.clear(); + for( auto *instance : Cell.m_instancesopaque ) { + if( false == instance->m_instanceable ) { + cache.single.emplace_back( instance ); + } + } + cache.instancedonly = + ( false == Cell.m_instancebuckets_opaque.empty() ) + && Cell.m_paths.empty() + && Cell.m_shapesopaque.empty() + && Cell.m_shapestranslucent.empty() + && Cell.m_lines.empty() + && Cell.m_traction.empty() + && Cell.m_instancetranslucent.empty() + && cache.single.empty(); + cache.boundsversion = m_instanceboundsversion; + cache.revoked = TAnimModel::s_instanceable_revoked; + return true; +} + +bool opengl33_renderer::Instance_Cache_Valid( scene::basic_cell const &Cell ) const +{ + return ( false == EditorModeFlag ) + && ( Cell.m_instancecache.boundsversion == m_instanceboundsversion ) + && ( Cell.m_instancecache.revoked == TAnimModel::s_instanceable_revoked ); +} + +// true if nothing of the cell would be drawn by the color pass: it holds only instances drawn with the instanced path, +// and the nearest point of the cell is past the largest range of them. the test is the one made for each bucket in +// Queue_Instances(), done before the cell is put in the cell queue: a scenery with many small instances of a short +// range has them in nearly every cell up to the draw range, and queueing, sorting and visiting these cells only +// to skip all their buckets took a good part of the frame +bool opengl33_renderer::Cell_Out_Of_Range( scene::basic_cell const &Cell ) const +{ + if( ( false == Instance_Cache_Valid( Cell ) ) || ( false == Cell.m_instancecache.instancedonly ) ) { return false; } + // same measures as in Queue_Instances() for the color pass + auto const distancescale { 1.0 / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) * static_cast( Global.fDistanceFactor ) ) }; + auto const slack { 1.001 }; + auto const distancetested { sq( 64.0 ) }; + auto const cellnearestdistance { std::max( 0.0, glm::length( Cell.m_area.center - m_renderpass.pass_camera.position() ) - Cell.m_area.radius ) }; + auto const cellnearestdistancesquared { cellnearestdistance * cellnearestdistance }; + return ( cellnearestdistancesquared > distancetested ) + && ( cellnearestdistancesquared * distancescale >= slack * Cell.m_instancecache.rangesquaredmax ); +} + // makes the copies of the bounds of the instances held by a bucket of a scene cell void opengl33_renderer::Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ) { diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index d48055f9f..b472849bc 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -302,6 +302,12 @@ class opengl33_renderer : public gfx_renderer { // adds the bucketed instances of a scene cell to the frame-level buckets drawn by Render_Instanced(), // less the ones the copies of their bounds show to be out of sight in the current render pass void Queue_Instances( scene::basic_cell const *Cell ); + // brings up to date what the renderer keeps of the instances of the cell; returns: true if the cache can be used + bool Prepare_Instances( scene::basic_cell &Cell ); + // true if the cache of the cell is up to date + bool Instance_Cache_Valid( scene::basic_cell const &Cell ) const; + // true if nothing of the cell would be drawn by the color pass from the camera of the pass + bool Cell_Out_Of_Range( scene::basic_cell const &Cell ) const; // makes the copies of the bounds of the instances held by a bucket of a scene cell void Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ); bool Render(TDynamicObject *Dynamic); diff --git a/scene/scene.cpp b/scene/scene.cpp index 7b0e29940..312765c4d 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -352,6 +352,7 @@ void basic_cell::insert( TAnimModel *Instance ) { m_active = true; + m_instancecache.boundsversion = 0; // the renderer builds it anew auto const flags = Instance->Flags(); auto alpha = @@ -454,6 +455,8 @@ basic_cell::unregister_end( TTrack *Path ) { void basic_cell::erase( TAnimModel *Instance ) { + m_instancecache.boundsversion = 0; // the renderer builds it anew + auto const flags = Instance->Flags(); auto alpha = Instance->Material() != nullptr ? Instance->Material()->textures_alpha : 0x30300030; diff --git a/scene/scene.h b/scene/scene.h index 086e2e830..0f52fcdbd 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -252,6 +252,18 @@ public: // caches owned by the renderer; emptied whenever the content of the bucket changes mutable std::vector bounds; // copies for the instances, in the same order mutable unsigned int boundsversion { 0 }; + // the largest rangesquaredmax of the copies - past it no instance of the bucket is drawn - and the version + // of the bounds it was taken with. set by the renderer before the cell is drawn, see Prepare_Instances() + float rangesquaredmax { 0.f }; + unsigned int rangeversion { 0 }; + }; + // what the renderer keeps of the instances of the cell, set by it before the cell is drawn + struct instance_cache { + instance_sequence single; // the opaque instances drawn one by one, as they aren't instanceable + float rangesquaredmax { 0.f }; // the largest rangesquaredmax of the instance buckets + bool instancedonly { false }; // the cell holds nothing to draw but instances drawn with the instanced path + unsigned int boundsversion { 0 }; // version of the bounds of the renderer it was built with, 0: not built + unsigned int revoked { 0 }; // TAnimModel::s_instanceable_revoked at the time of the build }; using instance_bucketdata_map = std::unordered_map< instance_bucket_key, instance_bucket, instance_bucket_key_hash >; using sound_sequence = std::vector; @@ -275,6 +287,7 @@ public: // m_instancesopaque for nodes whose TAnimModel::m_instanceable == true. // The renderer uses these to amortise per-model state setup across many instances. instance_bucketdata_map m_instancebuckets_opaque; + instance_cache m_instancecache; traction_sequence m_traction; sound_sequence m_sounds; eventlauncher_sequence m_eventlaunchers;