17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-08 02:58:37 +01:00

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
This commit is contained in:
pawelw
2026-11-07 12:35:55 +01:00
committed by GitHub
parent 3a29a165a9
commit 55ba7a9f41
6 changed files with 119 additions and 2 deletions

View File

@@ -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<TAnimContainer> 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;
}

View File

@@ -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;

View File

@@ -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<double>( Global.reflectiontune.range_instances ) ) : sq( static_cast<double>( 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<TAnimModel *> *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<double>( Global.ZoomFactor ) * static_cast<double>( Global.ZoomFactor ) * static_cast<double>( 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 )
{

View File

@@ -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);

View File

@@ -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;

View File

@@ -252,6 +252,18 @@ public:
// caches owned by the renderer; emptied whenever the content of the bucket changes
mutable std::vector<instance_bounds> 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<sound_source *>;
@@ -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;