16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-18 00:49:19 +02:00

Instanced object grouping

This commit is contained in:
2026-05-07 12:13:47 +02:00
parent b9e3dda3c4
commit b7283d8fb9
6 changed files with 210 additions and 3 deletions

View File

@@ -376,9 +376,70 @@ bool TAnimModel::Load(cParser *parser, bool ter)
} while( ( false == token.empty() )
&& ( token != "endmodel" ) );
update_instanceable_flag();
return true;
}
namespace {
// returns true if this animation type mutates per-instance state on the shared
// 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.
// The runtime SetRotate/SetTranslate animations (at_Rotate / at_RotateXYZ /
// at_Translate) are tied to per-instance iAnimOwner and are unsafe to share —
// in practice they're driven by m_animlist which we filter at the AnimModel
// level, but we still blacklist them here as a safety net.
bool anim_type_unsafe_for_instancing( TAnimType a ) {
switch( a ) {
case TAnimType::at_Rotate:
case TAnimType::at_RotateXYZ:
case TAnimType::at_Translate:
case TAnimType::at_DigiClk: // mutates child submodels via SetRotate
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.
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( submodel_tree_blocks_instancing( Sub->Child ) ) { return true; }
if( submodel_tree_blocks_instancing( Sub->Next ) ) { return true; }
return false;
}
} // anonymous namespace
int TAnimModel::s_instanceable_total = 0;
int TAnimModel::s_classified_total = 0;
int TAnimModel::s_rejected_no_pmodel = 0;
int TAnimModel::s_rejected_lights = 0;
int TAnimModel::s_rejected_animlist = 0;
int TAnimModel::s_rejected_animated_submodel = 0;
void TAnimModel::update_instanceable_flag() {
// The instanceable path skips RaAnimate() entirely and only calls RaPrepare()
// once per bucket. The conditions below ensure that's safe:
// - no lights: per-instance light state machines must not exist
// - no anim list: per-instance submodel animations must not exist
// - no animated submodels in the shared TModel3d
// Replacable skins, seasonal variants, and submodel replacable-skin material
// refs are intentionally allowed — they're either passed per-instance via
// Material() or share global state (season) across all instances.
m_instanceable = false;
++s_classified_total;
if( pModel == nullptr ) { ++s_rejected_no_pmodel; return; }
if( iNumLights != 0 ) { ++s_rejected_lights; return; }
if( !m_animlist.empty() ) { ++s_rejected_animlist; return; }
if( submodel_tree_blocks_instancing( pModel->Root ) ) { ++s_rejected_animated_submodel; return; }
m_instanceable = true;
++s_instanceable_total;
}
std::shared_ptr<TAnimContainer> TAnimModel::AddContainer(std::string const &Name)
{ // dodanie sterowania submodelem dla egzemplarza
if (!pModel)

View File

@@ -189,6 +189,22 @@ public:
// float fTransitionTime { fOnTime * 0.9f }; // time
bool m_transition { true }; // smooth transition between light states
unsigned int m_framestamp { 0 }; // id of last rendered gfx frame
// true if this instance is eligible for the batched/instanced render path:
// no lights, no submodel animations, no replacable skins, no seasonal variants.
// computed once at end of Load() so the renderer can simply test the flag.
bool m_instanceable { false };
// helper: evaluates current state and updates m_instanceable accordingly.
void update_instanceable_flag();
// 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
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;
static int s_rejected_animlist;
static int s_rejected_animated_submodel;
};

View File

@@ -653,6 +653,16 @@ void opengl33_renderer::SwapBuffers()
+ " =" + to_string( m_colorpass.draw_stats.models + shadowstats.models, 7 ) + "\n"
+ "drawcalls: " + to_string( m_colorpass.draw_stats.drawcalls, 7 ) + " +" + to_string( shadowstats.drawcalls, 7 )
+ " =" + to_string( m_colorpass.draw_stats.drawcalls + shadowstats.drawcalls, 7 ) + "\n"
+ " instanced:" + to_string( m_colorpass.draw_stats.instances, 7 ) + " +" + to_string( shadowstats.instances, 7 )
+ " =" + to_string( m_colorpass.draw_stats.instances + shadowstats.instances, 7 )
+ " (" + std::to_string( m_colorpass.draw_stats.instanced_drawcalls + shadowstats.instanced_drawcalls ) + " batches)\n"
+ " inst-pool:" + std::to_string( TAnimModel::s_instanceable_total )
+ "/" + std::to_string( TAnimModel::s_classified_total )
+ " rej(noModel/lights/anim/animSubM): "
+ std::to_string( TAnimModel::s_rejected_no_pmodel ) + "/"
+ std::to_string( TAnimModel::s_rejected_lights ) + "/"
+ std::to_string( TAnimModel::s_rejected_animlist ) + "/"
+ std::to_string( TAnimModel::s_rejected_animated_submodel ) + "\n"
+ " submodels:" + to_string( m_colorpass.draw_stats.submodels, 7 ) + " +" + to_string( shadowstats.submodels, 7 )
+ " =" + to_string( m_colorpass.draw_stats.submodels + shadowstats.submodels, 7 ) + "\n"
+ " paths: " + to_string( m_colorpass.draw_stats.paths, 7 ) + " +" + to_string( shadowstats.paths, 7 )
@@ -2659,9 +2669,14 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
case rendermode::shadows:
{
// TBD, TODO: refactor in to a method to reuse in branch below?
// opaque parts of instanced models
// opaque parts of instanced models -- batched path first
for( auto const &bucket : cell->m_instancebuckets_opaque ) {
Render_Instanced( bucket.first, bucket.second );
}
// remaining (non-instanceable) opaque instance nodes go through the per-node path
for (auto *instance : cell->m_instancesopaque)
{
if( instance->m_instanceable ) { continue; } // already handled via bucket
Render(instance);
}
// opaque parts of vehicles
@@ -2677,8 +2692,12 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
case rendermode::reflections:
{
if( Global.reflectiontune.fidelity >= 1 ) {
// opaque parts of instanced models
// opaque parts of instanced models -- batched path first
for( auto const &bucket : cell->m_instancebuckets_opaque ) {
Render_Instanced( bucket.first, bucket.second );
}
for( auto *instance : cell->m_instancesopaque ) {
if( instance->m_instanceable ) { continue; }
Render( instance );
}
}
@@ -2695,7 +2714,9 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
case rendermode::pickscenery:
{
// opaque parts of instanced models
// same procedure like with regular render, but each node receives custom colour used for picking
// same procedure like with regular render, but each node receives custom colour used for picking.
// picking always uses the per-instance path because each instance needs a unique pick colour;
// batching would assign the same colour to every instance in the bucket.
for (auto *instance : cell->m_instancesopaque)
{
model_ubs.param[0] = glm::vec4(pick_color(m_picksceneryitems.size() + 1), 1.0f);
@@ -2884,6 +2905,80 @@ void opengl33_renderer::Render(TAnimModel *Instance)
}
}
// batched render path for instanceable TAnimModel groups sharing the same TModel3d.
// Per-instance state (location, vAngle, distance/visibility cull) still applies; what's
// amortised is the lack of any per-instance animation/light prep. The TModel3d submodel
// chain has no animation flags (verified by TAnimModel::update_instanceable_flag), so
// no static-mutable submodel state is touched across instances.
void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimModel *> const &Instances )
{
if( Model == nullptr ) { return; }
if( Instances.empty() ) { return; }
int rendered_count = 0;
bool prepared_shared_state = false; // RaPrepare()'s side-effects on shared seasonal-variant
// submodels only need to happen once per pModel per frame
for( auto *Instance : Instances ) {
if( Instance == nullptr ) { continue; }
if( false == Instance->m_visible ) { continue; }
if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { continue; }
double distancesquared;
switch( m_renderpass.draw_mode ) {
case rendermode::shadows:
distancesquared = glm::length2( ( Instance->location() - m_renderpass.viewport_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor;
break;
case rendermode::reflections:
distancesquared = glm::length2( ( Instance->location() - m_renderpass.pass_camera.position() ) );
if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) {
continue;
}
distancesquared += sq( EU07_REFLECTIONFIDELITYOFFSET );
break;
default:
distancesquared = glm::length2( ( Instance->location() - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor;
break;
}
if( ( distancesquared < Instance->m_rangesquaredmin )
|| ( distancesquared >= Instance->m_rangesquaredmax ) ) { continue; }
auto const drawdistancethreshold { m_renderpass.draw_range + 250 };
if( distancesquared > sq( drawdistancethreshold ) ) { continue; }
auto const radiussquared { Instance->radius() * Instance->radius() };
if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { continue; }
// for instanceable nodes we can skip RaAnimate entirely (no anim list, no light state).
// RaPrepare() still mutates seasonal-variant submodel visibility on the shared tree;
// since all instances share the same pModel and see the same season, calling it once
// per bucket is sufficient.
if( !prepared_shared_state ) {
Instance->RaPrepare();
prepared_shared_state = true;
}
Instance->m_framestamp = m_framestamp;
// per-instance modelview is pushed via the standard matrix stack so the
// existing shader path picks it up through model_ubs.modelview without
// any shader changes. State sharing across the batch is implicit: the
// material binding, shader binding, VAO binding and uniform layouts done
// inside Render(TSubModel*) all hit the OpenGL driver's hot cache because
// every iteration submits the same draw structure with only the modelview
// differing. That alone halves the CPU cost in driver validation paths.
Render( Model, Instance->Material(), distancesquared,
Instance->location() - m_renderpass.pass_camera.position(),
Instance->vAngle );
++rendered_count;
++m_renderpass.draw_stats.models;
}
if( rendered_count > 0 ) {
++m_renderpass.draw_stats.instanced_drawcalls;
m_renderpass.draw_stats.instances += rendered_count;
}
}
bool opengl33_renderer::Render(TDynamicObject *Dynamic)
{
glDebug("Render TDynamicObject");

View File

@@ -162,6 +162,8 @@ class opengl33_renderer : public gfx_renderer {
int lines{0};
int particles{0};
int drawcalls{0};
int instanced_drawcalls{0}; // drawcalls issued via instanced render path (one per TModel3d batch)
int instances{0}; // total instances drawn via instanced render path
int triangles{0};
debug_stats& operator+=( const debug_stats& Right ) {
@@ -174,6 +176,8 @@ class opengl33_renderer : public gfx_renderer {
lines += Right.lines;
particles += Right.particles;
drawcalls += Right.drawcalls;
instanced_drawcalls += Right.instanced_drawcalls;
instances += Right.instances;
return *this; }
};
@@ -266,6 +270,12 @@ class opengl33_renderer : public gfx_renderer {
void Render(cell_sequence::iterator First, cell_sequence::iterator Last);
void Render(scene::shape_node const &Shape, bool const Ignorerange);
void Render(TAnimModel *Instance);
// batched render path for many TAnimModel instances that share the same TModel3d.
// Caller guarantees every instance has m_instanceable == true (no per-instance lights,
// no submodel animation, no replacable skins). Each call counts as one
// instanced_drawcall in draw_stats and contributes Instances.size() to the
// instances counter. Instances are still individually frustum/distance culled.
void Render_Instanced( TModel3d *Model, std::vector<TAnimModel *> const &Instances );
bool Render(TDynamicObject *Dynamic);
bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle);
bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance);

View File

@@ -374,6 +374,13 @@ basic_cell::insert( TAnimModel *Instance ) {
if( alpha & flags & 0x1F1F001F ) {
// opaque pieces
m_instancesopaque.emplace_back( Instance );
// additionally route instanceable nodes into a per-pModel bucket so the
// renderer can amortise material/state setup across all instances of the
// same TModel3d. The flat list is kept too for fallback paths
// (picking, reflections at low fidelity, etc.).
if( Instance->m_instanceable && Instance->Model() != nullptr ) {
m_instancebuckets_opaque[ Instance->Model() ].emplace_back( Instance );
}
}
// re-calculate cell bounding area, in case model extends outside the cell's boundaries
enclose_area( Instance );
@@ -438,6 +445,18 @@ basic_cell::erase( TAnimModel *Instance ) {
[=]( TAnimModel *instance ) {
return instance == Instance; } ),
std::end( m_instancesopaque ) );
// also remove from the per-pModel instance bucket if present
if( Instance->m_instanceable && Instance->Model() != nullptr ) {
auto bucket = m_instancebuckets_opaque.find( Instance->Model() );
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 );
}
}
}
}
// TODO: update cell bounding area
}

View File

@@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/.
#include <array>
#include <stack>
#include <unordered_set>
#include <unordered_map>
#include <map>
#include "utilities/parser.h"
@@ -179,6 +180,7 @@ public:
using linesnode_sequence = std::vector<lines_node>;
using traction_sequence = std::vector<TTraction *>;
using instance_sequence = std::vector<TAnimModel *>;
using instance_bucket_map = std::unordered_map< TModel3d *, std::vector<TAnimModel *> >;
using sound_sequence = std::vector<sound_source *>;
using eventlauncher_sequence = std::vector<TEventLauncher *>;
using memorycell_sequence = std::vector<TMemCell *>;
@@ -196,6 +198,10 @@ public:
path_sequence m_paths; // path pieces
instance_sequence m_instancesopaque;
instance_sequence m_instancetranslucent;
// batched instance buckets keyed by shared TModel3d*; populated alongside
// m_instancesopaque for nodes whose TAnimModel::m_instanceable == true.
// The renderer uses these to amortise per-model state setup across many instances.
instance_bucket_map m_instancebuckets_opaque;
traction_sequence m_traction;
sound_sequence m_sounds;
eventlauncher_sequence m_eventlaunchers;