From 20f536f05e68acb9e3a9fe1140cbd310b29cded2 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 1 Oct 2026 11:51:41 +0200 Subject: [PATCH 1/4] fix(instancing): keep batches correct after load-time classification --- model/AnimModel.cpp | 5 + rendering/opengl33geometrybank.cpp | 35 +++++-- rendering/opengl33renderer.cpp | 156 ++++++++++++++++++++--------- rendering/opengl33renderer.h | 7 +- scene/scene.cpp | 26 ++--- 5 files changed, 158 insertions(+), 71 deletions(-) diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index bc2d6e84..d5d856e0 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -478,6 +478,9 @@ std::shared_ptr TAnimModel::AddContainer(std::string const &Name auto tmp = std::make_shared(); tmp->Init(tsb); 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 + m_instanceable = false; return tmp; } return nullptr; @@ -688,6 +691,8 @@ std::optional> > TAnimModel::L 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 + m_instanceable = false; } void TAnimModel::AnimUpdate(double dt) diff --git a/rendering/opengl33geometrybank.cpp b/rendering/opengl33geometrybank.cpp index 91c9a8dd..506d22f1 100644 --- a/rendering/opengl33geometrybank.cpp +++ b/rendering/opengl33geometrybank.cpp @@ -215,13 +215,34 @@ opengl33_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry, GLsizei const inst_count = static_cast(InstanceCount); if( chunkrecord.index_count > 0 ) { - m_vao->bind(); - ::glDrawElementsInstancedBaseVertex( - chunk.type, - chunkrecord.index_count, GL_UNSIGNED_INT, - reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), - inst_count, - chunkrecord.vertex_offset ); + // GLES 3.0 context doesn't provide the base vertex variant, same fallbacks as in draw_() + if( glDrawElementsInstancedBaseVertex ) { + m_vao->bind(); + ::glDrawElementsInstancedBaseVertex( + chunk.type, + chunkrecord.index_count, GL_UNSIGNED_INT, + reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), + inst_count, + chunkrecord.vertex_offset ); + } + else if( glDrawElementsInstancedBaseVertexOES ) { + m_vao->bind(); + ::glDrawElementsInstancedBaseVertexOES( + chunk.type, + chunkrecord.index_count, GL_UNSIGNED_INT, + reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), + inst_count, + chunkrecord.vertex_offset ); + } + else { + setup_attrib( chunkrecord.vertex_offset ); + m_vao->bind(); + ::glDrawElementsInstanced( + chunk.type, + chunkrecord.index_count, GL_UNSIGNED_INT, + reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), + inst_count ); + } } else { m_vao->bind(); diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index b9937816..352d9632 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2382,8 +2382,11 @@ void opengl33_renderer::Render(scene::basic_region *Region) m_sectionqueue.clear(); m_cellqueue.clear(); - // discard last pass's accumulated instance buckets before this pass starts - m_frame_instance_buckets.clear(); + // discard last pass's accumulated instance buckets before this pass starts. + // the vectors themselves are kept, so their allocations get reused by the following passes + for( auto &bucket : m_frame_instance_buckets ) { + bucket.second.clear(); + } // build a list of region sections to render glm::vec3 const cameraposition{m_renderpass.pass_camera.position()}; auto const camerax = static_cast(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); @@ -2802,10 +2805,11 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // still happens inside Render_Instanced(), so correctness is unchanged -- only // the number of calls and glBufferSubData round-trips drops sharply. // (For pickscenery/pickcontrols modes the map stays empty, so this is a no-op.) - for( auto const &bucket : m_frame_instance_buckets ) { + for( auto &bucket : m_frame_instance_buckets ) { + if( bucket.second.empty() ) { continue; } Render_Instanced( bucket.first.pModel, bucket.second ); + bucket.second.clear(); } - m_frame_instance_buckets.clear(); } void opengl33_renderer::Draw_Geometry(std::vector::iterator begin, std::vector::iterator end) @@ -3024,13 +3028,13 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector::max(); + float farthest_distancesquared = 0.f; material_data const *batch_material { nullptr }; for( auto *Instance : Instances ) { if( Instance == nullptr ) { continue; } + // instances which lost their instanceable status after being bucketed (animation or skin + // assigned by an event) are drawn by the per-node path + if( false == Instance->m_instanceable ) { continue; } if( false == Instance->m_visible ) { continue; } if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { continue; } @@ -3077,6 +3085,7 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorm_framestamp = m_framestamp; closest_distancesquared = std::min( closest_distancesquared, static_cast(distancesquared) ); + farthest_distancesquared = std::max( farthest_distancesquared, static_cast(distancesquared) ); // Build the camera-relative root modelview for this instance: // mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) * scale(m_scale) @@ -3095,55 +3104,106 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector( distancesquared ), mv ); } - if( m_instance_modelviews.empty() ) { return; } + if( m_instance_survivors.empty() ) { return; } + + // LOD: a submodel is drawn only while fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist) + // range, so the distinct range bounds split distance into bands with constant LOD. The bucket + // collects instances from every visible cell, so a single fSquareDist for all of them would draw + // far instances at the LOD of the nearest one. When some bound falls inside the distance span of + // the survivors they're sorted near-to-far and drawn one band at a time, like Render_Sleepers(). + m_instance_lodbounds.clear(); + m_instance_lodpending.clear(); + m_instance_lodpending.push_back( Model->Root ); + while( false == m_instance_lodpending.empty() ) { + auto const *submodel = m_instance_lodpending.back(); + m_instance_lodpending.pop_back(); + if( submodel == nullptr ) { continue; } + if( ( submodel->fSquareMinDist > closest_distancesquared ) && ( submodel->fSquareMinDist <= farthest_distancesquared ) ) { + m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); + } + if( ( submodel->fSquareMaxDist > closest_distancesquared ) && ( submodel->fSquareMaxDist <= farthest_distancesquared ) ) { + m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); + } + m_instance_lodpending.push_back( submodel->Child ); + m_instance_lodpending.push_back( submodel->Next ); + } + if( false == m_instance_lodbounds.empty() ) { + std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ); + m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() ); + std::sort( m_instance_survivors.begin(), m_instance_survivors.end(), + []( std::pair const &Left, std::pair const &Right ) { + return Left.first < Right.first; } ); + } + // contiguous copy of the (sorted) modelview matrices, for the UBO upload + m_instance_modelviews.clear(); + m_instance_modelviews.reserve( m_instance_survivors.size() ); + for( auto const &survivor : m_instance_survivors ) { + m_instance_modelviews.emplace_back( survivor.second ); + } + + auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 ); + alpha ^= 0x0F0F000F; // 2. Walk the submodel tree once per sub-batch. The submodel-local matrix // stack starts at identity; the per-instance camera transform comes from // instance_modelview[gl_InstanceID] in the shader. std::size_t const total = m_instance_modelviews.size(); - std::size_t offset_idx = 0; - while( offset_idx < total ) { - std::size_t const this_batch = std::min( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); - - // 2a. Upload N modelviews to instance_ubo[0..N-1]. - instance_ubo->update( - reinterpret_cast( m_instance_modelviews.data() + offset_idx ), - 0, - static_cast( this_batch * sizeof( glm::mat4 ) ) ); - - // 2b. Push identity onto the matrix stack so submodel transforms - // accumulate from identity, not from camera/instance space. - ::glPushMatrix(); - ::glLoadIdentity(); - - // 2c. Configure the existing TModel3d/TSubModel render path. Setting - // m_current_instance_count routes draw(handle) calls to draw_instanced. - m_current_instance_count = this_batch; - - Model->Root->fSquareDist = closest_distancesquared; // shared global, used by submodel LOD - auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 ); - alpha ^= 0x0F0F000F; - Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); - Model->Root->pRoot = Model; - - Render( Model->Root ); - - m_current_instance_count = 0; - - ::glPopMatrix(); - - // 2d. Restore instance_modelview[0] to identity so subsequent - // non-instanced draws continue to compute identity * modelview. - { - glm::mat4 const identity( 1.0f ); - instance_ubo->update( reinterpret_cast( &identity ), 0, sizeof( identity ) ); + std::size_t band_start = 0; + while( band_start < total ) { + // the band ends at the first instance distance which reaches the next lod bound + // (without bounds inside the distance span the whole set forms a single band) + auto const upperbound = std::upper_bound( m_instance_lodbounds.begin(), m_instance_lodbounds.end(), m_instance_survivors[ band_start ].first ); + float const band_limit = ( upperbound == m_instance_lodbounds.end() ? std::numeric_limits::max() : *upperbound ); + std::size_t band_end = band_start; + while( ( band_end < total ) && ( m_instance_survivors[ band_end ].first < band_limit ) ) { + ++band_end; } + // every distance in the band selects the same lod; the nearest one represents the band + float const band_distancesquared = ( m_instance_lodbounds.empty() ? closest_distancesquared : m_instance_survivors[ band_start ].first ); - offset_idx += this_batch; - ++m_renderpass.draw_stats.instanced_drawcalls; + std::size_t offset_idx = band_start; + while( offset_idx < band_end ) { + std::size_t const this_batch = std::min( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); + + // 2a. Upload N modelviews to instance_ubo[0..N-1]. + instance_ubo->update( + reinterpret_cast( m_instance_modelviews.data() + offset_idx ), + 0, + static_cast( this_batch * sizeof( glm::mat4 ) ) ); + + // 2b. Push identity onto the matrix stack so submodel transforms + // accumulate from identity, not from camera/instance space. + ::glPushMatrix(); + ::glLoadIdentity(); + + // 2c. Configure the existing TModel3d/TSubModel render path. Setting + // m_current_instance_count routes draw(handle) calls to draw_instanced. + m_current_instance_count = this_batch; + + Model->Root->fSquareDist = band_distancesquared; // shared global, used by submodel LOD + Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); + Model->Root->pRoot = Model; + + Render( Model->Root ); + + m_current_instance_count = 0; + + ::glPopMatrix(); + + // 2d. Restore instance_modelview[0] to identity so subsequent + // non-instanced draws continue to compute identity * modelview. + { + glm::mat4 const identity( 1.0f ); + instance_ubo->update( reinterpret_cast( &identity ), 0, sizeof( identity ) ); + } + + offset_idx += this_batch; + ++m_renderpass.draw_stats.instanced_drawcalls; + } + band_start = band_end; } m_renderpass.draw_stats.instances += static_cast( total ); diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 6fda37c3..8e5c0b14 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -376,7 +376,7 @@ class opengl33_renderer : public gfx_renderer { // cell's buckets are merged here keyed by (TModel3d*, skins), so that // Render_Instanced() runs once per unique model across the whole pass instead // of once per cell -- collapsing many tiny instanced draws into a few large - // batches. Reused every pass; cleared at the top of Render(scene::basic_region*). + // batches. Reused every pass; the vectors are emptied (not freed) at the top of Render(scene::basic_region*). scene::basic_cell::instance_bucket_map m_frame_instance_buckets; renderpass_config m_colorpass; // parametrs of most recent color pass std::array m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages @@ -426,6 +426,11 @@ class opengl33_renderer : public gfx_renderer { // reused across calls -- clear() retains capacity, so once it has grown to // the largest batch seen, steady-state frames perform no allocation here. std::vector m_instance_modelviews; + // further persistent scratch buffers for Render_Instanced(): culled instances paired with their + // squared distance, and the lod bounds of the drawn model which split them into lod bands + std::vector> m_instance_survivors; + std::vector m_instance_lodbounds; + std::vector m_instance_lodpending; gl::scene_ubs scene_ubs; gl::model_ubs model_ubs; gl::light_ubs light_ubs; diff --git a/scene/scene.cpp b/scene/scene.cpp index 14825705..8ebff031 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -442,22 +442,18 @@ basic_cell::erase( TAnimModel *Instance ) { [=]( TAnimModel *instance ) { return instance == Instance; } ), std::end( m_instancesopaque ) ); - // also remove from the per-(pModel, skins) instance bucket if present - if( Instance->m_instanceable && Instance->Model() != nullptr ) { - instance_bucket_key key; - key.pModel = Instance->Model(); - auto const *mat = Instance->Material(); - if( mat != nullptr ) { - for( int i = 0; i < 5; ++i ) { key.skins[i] = mat->replacable_skins[i]; } + // also remove from the per-(pModel, skins) instance bucket if present. + // NOTE: searched by pointer rather than by key: the instance may have lost its instanceable + // status or changed its skins since it was inserted, which would leave a dangling pointer behind + for( auto bucket = m_instancebuckets_opaque.begin(); bucket != m_instancebuckets_opaque.end(); ) { + bucket->second.erase( + std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ), + std::end( bucket->second ) ); + if( bucket->second.empty() ) { + bucket = m_instancebuckets_opaque.erase( bucket ); } - auto bucket = m_instancebuckets_opaque.find( key ); - if( bucket != m_instancebuckets_opaque.end() ) { - bucket->second.erase( - std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ), - std::end( bucket->second ) ); - if( bucket->second.empty() ) { - m_instancebuckets_opaque.erase( bucket ); - } + else { + ++bucket; } } } From 4eabf3c3b53dfcc61ef56fc21a667c8b0e98d695 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 1 Oct 2026 12:07:54 +0200 Subject: [PATCH 2/4] feat(instancing): batch models animated only by time, wind or sky --- model/AnimModel.cpp | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index d5d856e0..09f01c8c 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -403,8 +403,8 @@ namespace { // TSubModel tree, which would make batched rendering unsafe. Most animations // loaded from .t3d files are global functions of time (clocks, wind, sky) and // only transform the local modelview matrix — those are safe to share. Camera- -// relative billboards also operate purely on the local matrix using whatever -// modelview the caller pushed, which is exactly per-instance behaviour. +// relative billboards are not: they're computed on the cpu from the modelview of +// the drawn instance, which in a batch is only the submodel-local chain. // The runtime SetRotate/SetTranslate animations (at_Rotate / at_RotateXYZ / // at_Translate) are tied to per-instance iAnimOwner and are unsafe to share. // at_Undefined is the type assigned to .t3d submodels declared with `anim: true` @@ -425,17 +425,40 @@ bool anim_type_unsafe_for_instancing( TAnimType a ) { } } +// returns true if this animation type depends only on global state (simulation +// time, wind, sky) and so gives every instance of the model the same local +// transform. The batch evaluates it once and the per-instance placement comes +// from the instance matrix, the result is the same as with per-instance drawing. +bool anim_type_shared_by_instances( TAnimType a ) { + switch( a ) { + case TAnimType::at_SecondsJump: + case TAnimType::at_MinutesJump: + case TAnimType::at_HoursJump: + case TAnimType::at_Hours24Jump: + case TAnimType::at_Seconds: + case TAnimType::at_Minutes: + case TAnimType::at_Hours: + case TAnimType::at_Hours24: + case TAnimType::at_Wind: + case TAnimType::at_Sky: + return true; + default: + return false; + } +} + // recursively walks a submodel tree and returns true if any submodel declares // an animation type that's unsafe to batch, OR carries the runtime "needs -// animation matrix" flag (iFlags bit 0x4000), which is set whenever the -// submodel was tagged as animatable in the .t3d file or had WillBeAnimated() -// called on it during model load. Either signal means the submodel may receive +// animation matrix" flag (iFlags bit 0x4000) for anything else than an animation +// shared by all instances. The flag is set whenever the submodel was tagged as +// animatable in the .t3d file (any `anim:` other than `false`) or had +// WillBeAnimated() called on it during model load. Such a submodel may receive // per-instance event-driven animation commands at runtime, which the GPU- // instanced path (one shared submodel tree across all instances) cannot serve. bool submodel_tree_blocks_instancing( TSubModel const *Sub ) { if( Sub == nullptr ) { return false; } if( anim_type_unsafe_for_instancing( Sub->b_Anim ) ) { return true; } - if( ( Sub->iFlags & 0x4000 ) != 0 ) { return true; } + if( ( ( Sub->iFlags & 0x4000 ) != 0 ) && ( false == anim_type_shared_by_instances( Sub->b_Anim ) ) ) { return true; } if( submodel_tree_blocks_instancing( Sub->Child ) ) { return true; } if( submodel_tree_blocks_instancing( Sub->Next ) ) { return true; } return false; From 2130d175e941a69b47b4ce47cbda8cb379923efc Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 1 Oct 2026 12:43:07 +0200 Subject: [PATCH 3/4] perf(instancing): cache instance rotation and group lod bands linearly --- model/AnimModel.cpp | 17 +++++ model/AnimModel.h | 8 ++ rendering/opengl33renderer.cpp | 135 ++++++++++++++------------------- rendering/opengl33renderer.h | 11 ++- 4 files changed, 93 insertions(+), 78 deletions(-) diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index 09f01c8c..cc54a2a2 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -664,6 +664,23 @@ void TAnimModel::RaPrepare() } } +glm::mat4 const &TAnimModel::rotation_scale() { + + if( ( false == m_rotationscalevalid ) || ( vAngle != m_rotationscaleangles ) || ( m_scale != m_rotationscalescale ) ) { + // same order as the per-instance render path + glm::mat4 transform( 1.0f ); + if( vAngle.y != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.y ), glm::vec3( 0.f, 1.f, 0.f ) ); } + if( vAngle.x != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.x ), glm::vec3( 1.f, 0.f, 0.f ) ); } + if( vAngle.z != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.z ), glm::vec3( 0.f, 0.f, 1.f ) ); } + if( m_scale != glm::vec3( 1.0f ) ) { transform = glm::scale( transform, m_scale ); } + m_rotationscale = transform; + m_rotationscaleangles = vAngle; + m_rotationscalescale = m_scale; + m_rotationscalevalid = true; + } + return m_rotationscale; +} + int TAnimModel::Flags() { // informacja dla TGround, czy ma być w Render, RenderAlpha, czy RenderMixed int i = pModel ? pModel->Flags() : 0; // pobranie flag całego modelu diff --git a/model/AnimModel.h b/model/AnimModel.h index 435c1dba..3c4b9f11 100644 --- a/model/AnimModel.h +++ b/model/AnimModel.h @@ -172,6 +172,8 @@ public: public: // methods void RaPrepare(); // ustawienie animacji egzemplarza na wzorcu + // rotation and scale of the instance as a matrix: rotate y * rotate x * rotate z * scale + glm::mat4 const &rotation_scale(); void RaAnimate( unsigned int const Framestamp ); // przeliczenie animacji egzemplarza // radius() subclass details, calculates node's bounding radius @@ -190,6 +192,12 @@ public: TModel3d *pModel { nullptr }; glm::vec3 vAngle; // bazowe obroty egzemplarza względem osi glm::vec3 m_scale { 1.0f, 1.0f, 1.0f }; // per-axis scale (see Scale() accessors above) + // rotation and scale part of the instance transform, cached for the instanced render path. + // rebuilt by rotation_scale() whenever the angles or the scale differ from the ones it was built from + glm::mat4 m_rotationscale { 1.0f }; + glm::vec3 m_rotationscaleangles { 0.0f, 0.0f, 0.0f }; + glm::vec3 m_rotationscalescale { 1.0f, 1.0f, 1.0f }; + bool m_rotationscalevalid { false }; material_data m_materialdata; std::string asText; // tekst dla wyświetlacza znakowego diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 352d9632..f96ddffa 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -3026,13 +3026,33 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorRoot ); + while( false == m_instance_lodpending.empty() ) { + auto const *submodel = m_instance_lodpending.back(); + m_instance_lodpending.pop_back(); + if( submodel == nullptr ) { continue; } + if( submodel->fSquareMinDist > 0.f ) { m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); } + if( submodel->fSquareMaxDist < std::numeric_limits::max() ) { m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); } + m_instance_lodpending.push_back( submodel->Child ); + m_instance_lodpending.push_back( submodel->Next ); + } + std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ); + m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() ); + auto const bandcount { m_instance_lodbounds.size() + 1 }; + + // 1. Visibility / distance cull. Collect surviving instances with their precomputed + // camera-space root modelview matrices and lod bands. m_instance_survivors and the other + // scratch vectors are persistent members reused across every Render_Instanced() call: + // clear() drops the contents but keeps the allocated capacity, so after the first few + // frames this stops calling malloc/free entirely. m_instance_survivors.clear(); m_instance_survivors.reserve( Instances.size() ); @@ -3043,8 +3063,6 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector::max(); - float farthest_distancesquared = 0.f; material_data const *batch_material { nullptr }; for( auto *Instance : Instances ) { @@ -3084,64 +3102,39 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorMaterial(); } Instance->m_framestamp = m_framestamp; - closest_distancesquared = std::min( closest_distancesquared, static_cast(distancesquared) ); - farthest_distancesquared = std::max( farthest_distancesquared, static_cast(distancesquared) ); // Build the camera-relative root modelview for this instance: // mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) * scale(m_scale) - // The scale is folded in here so the GPU-instanced path produces visually - // identical output to the regular per-instance path: each instance gets - // its own (translate × rotate × scale) baked into instance_modelview[i], - // which the shader applies via effective_modelview = instance_mv * model_local. - glm::dvec3 const offset = Instance->location() - m_renderpass.pass_camera.position(); - glm::mat4 mv = view_matrix; - mv = glm::translate( mv, glm::vec3( offset ) ); - auto const &angle = Instance->vAngle; - if( angle.y != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.y ), glm::vec3( 0.f, 1.f, 0.f ) ); } - if( angle.x != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.x ), glm::vec3( 1.f, 0.f, 0.f ) ); } - if( angle.z != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.z ), glm::vec3( 0.f, 0.f, 1.f ) ); } - auto const &scale = Instance->Scale(); - if( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ) { - mv = glm::scale( mv, scale ); - } - m_instance_survivors.emplace_back( static_cast( distancesquared ), mv ); + // The rotation and scale part is cached by the instance, the translation goes into its last + // column. The scale is folded in so the GPU-instanced path produces visually identical output + // to the regular per-instance path; the shader applies it via effective_modelview = instance_mv * model_local. + glm::mat4 root { Instance->rotation_scale() }; + root[ 3 ] = glm::vec4( glm::vec3( Instance->location() - m_renderpass.pass_camera.position() ), 1.f ); + + auto const distance { static_cast( distancesquared ) }; + m_instance_survivors.push_back( { + view_matrix * root, + distance, + static_cast( std::upper_bound( m_instance_lodbounds.begin(), m_instance_lodbounds.end(), distance ) - m_instance_lodbounds.begin() ) } ); } if( m_instance_survivors.empty() ) { return; } - // LOD: a submodel is drawn only while fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist) - // range, so the distinct range bounds split distance into bands with constant LOD. The bucket - // collects instances from every visible cell, so a single fSquareDist for all of them would draw - // far instances at the LOD of the nearest one. When some bound falls inside the distance span of - // the survivors they're sorted near-to-far and drawn one band at a time, like Render_Sleepers(). - m_instance_lodbounds.clear(); - m_instance_lodpending.clear(); - m_instance_lodpending.push_back( Model->Root ); - while( false == m_instance_lodpending.empty() ) { - auto const *submodel = m_instance_lodpending.back(); - m_instance_lodpending.pop_back(); - if( submodel == nullptr ) { continue; } - if( ( submodel->fSquareMinDist > closest_distancesquared ) && ( submodel->fSquareMinDist <= farthest_distancesquared ) ) { - m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); - } - if( ( submodel->fSquareMaxDist > closest_distancesquared ) && ( submodel->fSquareMaxDist <= farthest_distancesquared ) ) { - m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); - } - m_instance_lodpending.push_back( submodel->Child ); - m_instance_lodpending.push_back( submodel->Next ); - } - if( false == m_instance_lodbounds.empty() ) { - std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ); - m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() ); - std::sort( m_instance_survivors.begin(), m_instance_survivors.end(), - []( std::pair const &Left, std::pair const &Right ) { - return Left.first < Right.first; } ); - } - // contiguous copy of the (sorted) modelview matrices, for the UBO upload - m_instance_modelviews.clear(); - m_instance_modelviews.reserve( m_instance_survivors.size() ); + // group the modelviews by lod band (counting sort) into a contiguous array, for the UBO upload. + // every distance in a band selects the same lod; the nearest one represents the band + m_instance_bandstarts.assign( bandcount + 1, 0 ); + m_instance_banddistances.assign( bandcount, std::numeric_limits::max() ); for( auto const &survivor : m_instance_survivors ) { - m_instance_modelviews.emplace_back( survivor.second ); + ++m_instance_bandstarts[ survivor.band + 1 ]; + m_instance_banddistances[ survivor.band ] = std::min( m_instance_banddistances[ survivor.band ], survivor.distancesquared ); + } + for( std::size_t band = 1; band <= bandcount; ++band ) { + m_instance_bandstarts[ band ] += m_instance_bandstarts[ band - 1 ]; + } + m_instance_bandcursors.assign( m_instance_bandstarts.begin(), m_instance_bandstarts.end() - 1 ); + m_instance_modelviews.resize( m_instance_survivors.size() ); + for( auto const &survivor : m_instance_survivors ) { + m_instance_modelviews[ m_instance_bandcursors[ survivor.band ]++ ] = survivor.modelview; } auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 ); @@ -3150,21 +3143,10 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector::max() : *upperbound ); - std::size_t band_end = band_start; - while( ( band_end < total ) && ( m_instance_survivors[ band_end ].first < band_limit ) ) { - ++band_end; - } - // every distance in the band selects the same lod; the nearest one represents the band - float const band_distancesquared = ( m_instance_lodbounds.empty() ? closest_distancesquared : m_instance_survivors[ band_start ].first ); + for( std::size_t band = 0; band < bandcount; ++band ) { - std::size_t offset_idx = band_start; + std::size_t offset_idx = m_instance_bandstarts[ band ]; + std::size_t const band_end = m_instance_bandstarts[ band + 1 ]; while( offset_idx < band_end ) { std::size_t const this_batch = std::min( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); @@ -3183,7 +3165,7 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorRoot->fSquareDist = band_distancesquared; // shared global, used by submodel LOD + Model->Root->fSquareDist = m_instance_banddistances[ band ]; // shared global, used by submodel LOD Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); Model->Root->pRoot = Model; @@ -3203,11 +3185,10 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector( total ); - m_renderpass.draw_stats.models += static_cast( total ); + m_renderpass.draw_stats.instances += static_cast( m_instance_survivors.size() ); + m_renderpass.draw_stats.models += static_cast( m_instance_survivors.size() ); } // Renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) using the diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 8e5c0b14..2f86117f 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -428,9 +428,18 @@ class opengl33_renderer : public gfx_renderer { std::vector m_instance_modelviews; // further persistent scratch buffers for Render_Instanced(): culled instances paired with their // squared distance, and the lod bounds of the drawn model which split them into lod bands - std::vector> m_instance_survivors; + struct instance_survivor { + glm::mat4 modelview; + float distancesquared; + std::uint32_t band; + }; + std::vector m_instance_survivors; std::vector m_instance_lodbounds; std::vector m_instance_lodpending; + // per lod band: first index in m_instance_modelviews (with end sentinel), fill cursor, nearest distance + std::vector m_instance_bandstarts; + std::vector m_instance_bandcursors; + std::vector m_instance_banddistances; gl::scene_ubs scene_ubs; gl::model_ubs model_ubs; gl::light_ubs light_ubs; From d670c4b116fe1436b30da734d4b419ad7f339cfb Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 1 Oct 2026 12:57:33 +0200 Subject: [PATCH 4/4] perf(instancing): skip models without shadow casters in shadow passes --- rendering/opengl33renderer.cpp | 40 ++++++++++++++++++++++++++-------- rendering/opengl33renderer.h | 3 ++- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index f96ddffa..aba5212a 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -3032,6 +3032,12 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorMaterial() != nullptr ? Instances.front()->Material()->replacable_skins : nullptr }; + bool castsshadow { m_renderpass.draw_mode != rendermode::shadows }; m_instance_lodbounds.clear(); m_instance_lodpending.clear(); m_instance_lodpending.push_back( Model->Root ); @@ -3041,9 +3047,14 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorfSquareMinDist > 0.f ) { m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); } if( submodel->fSquareMaxDist < std::numeric_limits::max() ) { m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); } + if( ( false == castsshadow ) && ( submodel->eType < TP_ROTATOR ) ) { + auto const material { submodel->m_material >= 0 ? submodel->m_material : ( skins != nullptr ? skins[ -submodel->m_material ] : null_handle ) }; + castsshadow = ( Material( material ).shadow_rank <= Global.gfx_shadow_rank_cutoff ); + } m_instance_lodpending.push_back( submodel->Child ); m_instance_lodpending.push_back( submodel->Next ); } + if( false == castsshadow ) { return; } std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ); m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() ); auto const bandcount { m_instance_lodbounds.size() + 1 }; @@ -3064,6 +3075,7 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorlocation() - m_renderpass.pass_camera.position() ), 1.f ); auto const distance { static_cast( distancesquared ) }; + farthest_distancesquared = std::max( farthest_distancesquared, distance ); m_instance_survivors.push_back( { view_matrix * root, distance, @@ -3120,16 +3133,25 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector::max() ); - for( auto const &survivor : m_instance_survivors ) { - ++m_instance_bandstarts[ survivor.band + 1 ]; + for( auto &survivor : m_instance_survivors ) { m_instance_banddistances[ survivor.band ] = std::min( m_instance_banddistances[ survivor.band ], survivor.distancesquared ); + auto const slot { std::min( distanceslots - 1, static_cast( std::sqrt( survivor.distancesquared ) * slotscale ) ) }; + survivor.band = static_cast( survivor.band * distanceslots + slot ); // from now on it's the sort key + ++m_instance_bandstarts[ survivor.band + 1 ]; } - for( std::size_t band = 1; band <= bandcount; ++band ) { - m_instance_bandstarts[ band ] += m_instance_bandstarts[ band - 1 ]; + for( std::size_t key = 1; key <= keycount; ++key ) { + m_instance_bandstarts[ key ] += m_instance_bandstarts[ key - 1 ]; } m_instance_bandcursors.assign( m_instance_bandstarts.begin(), m_instance_bandstarts.end() - 1 ); m_instance_modelviews.resize( m_instance_survivors.size() ); @@ -3145,8 +3167,8 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 2f86117f..43cd24b0 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -436,7 +436,8 @@ class opengl33_renderer : public gfx_renderer { std::vector m_instance_survivors; std::vector m_instance_lodbounds; std::vector m_instance_lodpending; - // per lod band: first index in m_instance_modelviews (with end sentinel), fill cursor, nearest distance + // per sort key (lod band and distance slot): first index in m_instance_modelviews (with end sentinel) + // and fill cursor; per lod band: nearest distance std::vector m_instance_bandstarts; std::vector m_instance_bandcursors; std::vector m_instance_banddistances;