17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 03:59:19 +02:00

fix(instancing): keep batches correct after load-time classification

This commit is contained in:
2026-10-01 11:51:41 +02:00
parent f05b58b0e9
commit 20f536f05e
5 changed files with 158 additions and 71 deletions

View File

@@ -478,6 +478,9 @@ std::shared_ptr<TAnimContainer> TAnimModel::AddContainer(std::string const &Name
auto tmp = std::make_shared<TAnimContainer>(); auto tmp = std::make_shared<TAnimContainer>();
tmp->Init(tsb); tmp->Init(tsb);
m_animlist.push_back(tmp); 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 tmp;
} }
return nullptr; return nullptr;
@@ -688,6 +691,8 @@ std::optional<std::tuple<float, float, std::optional<glm::vec3>> > TAnimModel::L
void TAnimModel::SkinSet( int const Index, material_handle const Material ) { void TAnimModel::SkinSet( int const Index, material_handle const Material ) {
m_materialdata.replacable_skins[ std::clamp( Index, 1, 4 ) ] = 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) void TAnimModel::AnimUpdate(double dt)

View File

@@ -215,13 +215,34 @@ opengl33_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry,
GLsizei const inst_count = static_cast<GLsizei>(InstanceCount); GLsizei const inst_count = static_cast<GLsizei>(InstanceCount);
if( chunkrecord.index_count > 0 ) { if( chunkrecord.index_count > 0 ) {
m_vao->bind(); // GLES 3.0 context doesn't provide the base vertex variant, same fallbacks as in draw_()
::glDrawElementsInstancedBaseVertex( if( glDrawElementsInstancedBaseVertex ) {
chunk.type, m_vao->bind();
chunkrecord.index_count, GL_UNSIGNED_INT, ::glDrawElementsInstancedBaseVertex(
reinterpret_cast<void const *>( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), chunk.type,
inst_count, chunkrecord.index_count, GL_UNSIGNED_INT,
chunkrecord.vertex_offset ); reinterpret_cast<void const *>( 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<void const *>( 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<void const *>( chunkrecord.index_offset * sizeof( gfx::basic_index ) ),
inst_count );
}
} }
else { else {
m_vao->bind(); m_vao->bind();

View File

@@ -2382,8 +2382,11 @@ void opengl33_renderer::Render(scene::basic_region *Region)
m_sectionqueue.clear(); m_sectionqueue.clear();
m_cellqueue.clear(); m_cellqueue.clear();
// discard last pass's accumulated instance buckets before this pass starts // discard last pass's accumulated instance buckets before this pass starts.
m_frame_instance_buckets.clear(); // 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 // build a list of region sections to render
glm::vec3 const cameraposition{m_renderpass.pass_camera.position()}; glm::vec3 const cameraposition{m_renderpass.pass_camera.position()};
auto const camerax = static_cast<int>(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); auto const camerax = static_cast<int>(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 // still happens inside Render_Instanced(), so correctness is unchanged -- only
// the number of calls and glBufferSubData round-trips drops sharply. // 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 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 ); Render_Instanced( bucket.first.pModel, bucket.second );
bucket.second.clear();
} }
m_frame_instance_buckets.clear();
} }
void opengl33_renderer::Draw_Geometry(std::vector<gfx::geometrybank_handle>::iterator begin, std::vector<gfx::geometrybank_handle>::iterator end) void opengl33_renderer::Draw_Geometry(std::vector<gfx::geometrybank_handle>::iterator begin, std::vector<gfx::geometrybank_handle>::iterator end)
@@ -3024,13 +3028,13 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
// 1. Visibility / distance cull. Build parallel arrays of surviving // 1. Visibility / distance cull. Build parallel arrays of surviving
// instances and their precomputed camera-space root modelview matrices. // instances and their precomputed camera-space root modelview matrices.
// m_instance_modelviews is a persistent member reused across every // m_instance_survivors (and m_instance_modelviews below) are persistent members
// Render_Instanced() call: clear() drops the contents but keeps the // reused across every Render_Instanced() call: clear() drops the contents but keeps the
// allocated capacity, so after the first few frames this stops calling // allocated capacity, so after the first few frames this stops calling
// malloc/free entirely (the reserve() below becomes a no-op once the // malloc/free entirely (the reserve() below becomes a no-op once the
// buffer has grown to the largest batch encountered). // buffer has grown to the largest batch encountered).
m_instance_modelviews.clear(); m_instance_survivors.clear();
m_instance_modelviews.reserve( Instances.size() ); m_instance_survivors.reserve( Instances.size() );
// Pull the current pass camera/view transform once. We use the current GL // Pull the current pass camera/view transform once. We use the current GL
// modelview matrix as the view matrix because at the point Render_Instanced // modelview matrix as the view matrix because at the point Render_Instanced
@@ -3040,10 +3044,14 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
bool prepared_shared_state = false; // RaPrepare() runs once per bucket bool prepared_shared_state = false; // RaPrepare() runs once per bucket
float closest_distancesquared = std::numeric_limits<float>::max(); float closest_distancesquared = std::numeric_limits<float>::max();
float farthest_distancesquared = 0.f;
material_data const *batch_material { nullptr }; material_data const *batch_material { nullptr };
for( auto *Instance : Instances ) { for( auto *Instance : Instances ) {
if( Instance == nullptr ) { continue; } 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 == Instance->m_visible ) { continue; }
if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { continue; } if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { continue; }
@@ -3077,6 +3085,7 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
} }
Instance->m_framestamp = m_framestamp; Instance->m_framestamp = m_framestamp;
closest_distancesquared = std::min<float>( closest_distancesquared, static_cast<float>(distancesquared) ); closest_distancesquared = std::min<float>( closest_distancesquared, static_cast<float>(distancesquared) );
farthest_distancesquared = std::max<float>( farthest_distancesquared, static_cast<float>(distancesquared) );
// Build the camera-relative root modelview for this instance: // Build the camera-relative root modelview for this instance:
// mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) * scale(m_scale) // 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<TAnimMode
if( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ) { if( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ) {
mv = glm::scale( mv, scale ); mv = glm::scale( mv, scale );
} }
m_instance_modelviews.emplace_back( mv ); m_instance_survivors.emplace_back( static_cast<float>( 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<float, glm::mat4> const &Left, std::pair<float, glm::mat4> 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 // 2. Walk the submodel tree once per sub-batch. The submodel-local matrix
// stack starts at identity; the per-instance camera transform comes from // stack starts at identity; the per-instance camera transform comes from
// instance_modelview[gl_InstanceID] in the shader. // instance_modelview[gl_InstanceID] in the shader.
std::size_t const total = m_instance_modelviews.size(); std::size_t const total = m_instance_modelviews.size();
std::size_t offset_idx = 0; std::size_t band_start = 0;
while( offset_idx < total ) { while( band_start < total ) {
std::size_t const this_batch = std::min<std::size_t>( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); // 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)
// 2a. Upload N modelviews to instance_ubo[0..N-1]. auto const upperbound = std::upper_bound( m_instance_lodbounds.begin(), m_instance_lodbounds.end(), m_instance_survivors[ band_start ].first );
instance_ubo->update( float const band_limit = ( upperbound == m_instance_lodbounds.end() ? std::numeric_limits<float>::max() : *upperbound );
reinterpret_cast<uint8_t const *>( m_instance_modelviews.data() + offset_idx ), std::size_t band_end = band_start;
0, while( ( band_end < total ) && ( m_instance_survivors[ band_end ].first < band_limit ) ) {
static_cast<int>( this_batch * sizeof( glm::mat4 ) ) ); ++band_end;
// 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<uint8_t const *>( &identity ), 0, sizeof( identity ) );
} }
// 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; std::size_t offset_idx = band_start;
++m_renderpass.draw_stats.instanced_drawcalls; while( offset_idx < band_end ) {
std::size_t const this_batch = std::min<std::size_t>( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH );
// 2a. Upload N modelviews to instance_ubo[0..N-1].
instance_ubo->update(
reinterpret_cast<uint8_t const *>( m_instance_modelviews.data() + offset_idx ),
0,
static_cast<int>( 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<uint8_t const *>( &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<int>( total ); m_renderpass.draw_stats.instances += static_cast<int>( total );

View File

@@ -376,7 +376,7 @@ class opengl33_renderer : public gfx_renderer {
// cell's buckets are merged here keyed by (TModel3d*, skins), so that // cell's buckets are merged here keyed by (TModel3d*, skins), so that
// Render_Instanced() runs once per unique model across the whole pass instead // 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 // 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; scene::basic_cell::instance_bucket_map m_frame_instance_buckets;
renderpass_config m_colorpass; // parametrs of most recent color pass renderpass_config m_colorpass; // parametrs of most recent color pass
std::array<renderpass_config, 3> m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages std::array<renderpass_config, 3> 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 // reused across calls -- clear() retains capacity, so once it has grown to
// the largest batch seen, steady-state frames perform no allocation here. // the largest batch seen, steady-state frames perform no allocation here.
std::vector<glm::mat4> m_instance_modelviews; std::vector<glm::mat4> 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<std::pair<float, glm::mat4>> m_instance_survivors;
std::vector<float> m_instance_lodbounds;
std::vector<TSubModel const *> m_instance_lodpending;
gl::scene_ubs scene_ubs; gl::scene_ubs scene_ubs;
gl::model_ubs model_ubs; gl::model_ubs model_ubs;
gl::light_ubs light_ubs; gl::light_ubs light_ubs;

View File

@@ -442,22 +442,18 @@ basic_cell::erase( TAnimModel *Instance ) {
[=]( TAnimModel *instance ) { [=]( TAnimModel *instance ) {
return instance == Instance; } ), return instance == Instance; } ),
std::end( m_instancesopaque ) ); std::end( m_instancesopaque ) );
// also remove from the per-(pModel, skins) instance bucket if present // also remove from the per-(pModel, skins) instance bucket if present.
if( Instance->m_instanceable && Instance->Model() != nullptr ) { // NOTE: searched by pointer rather than by key: the instance may have lost its instanceable
instance_bucket_key key; // status or changed its skins since it was inserted, which would leave a dangling pointer behind
key.pModel = Instance->Model(); for( auto bucket = m_instancebuckets_opaque.begin(); bucket != m_instancebuckets_opaque.end(); ) {
auto const *mat = Instance->Material(); bucket->second.erase(
if( mat != nullptr ) { std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ),
for( int i = 0; i < 5; ++i ) { key.skins[i] = mat->replacable_skins[i]; } std::end( bucket->second ) );
if( bucket->second.empty() ) {
bucket = m_instancebuckets_opaque.erase( bucket );
} }
auto bucket = m_instancebuckets_opaque.find( key ); else {
if( bucket != m_instancebuckets_opaque.end() ) { ++bucket;
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 );
}
} }
} }
} }