17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-06 12:48:35 +01:00

Cull instances by compact records and draw them from a shader storage buffer

This commit is contained in:
2026-10-16 00:37:54 +02:00
parent fd98d4eea5
commit 9277c28805
13 changed files with 269 additions and 228 deletions

View File

@@ -14,7 +14,7 @@ void gl::glsl_common_setup()
"#define MAX_LIGHTS " + std::to_string(MAX_LIGHTS) + "U\n" + "#define MAX_LIGHTS " + std::to_string(MAX_LIGHTS) + "U\n" +
"#define MAX_CASCADES " + std::to_string(MAX_CASCADES) + "U\n" + "#define MAX_CASCADES " + std::to_string(MAX_CASCADES) + "U\n" +
"#define MAX_PARAMS " + std::to_string(MAX_PARAMS) + "U\n" + "#define MAX_PARAMS " + std::to_string(MAX_PARAMS) + "U\n" +
"#define MAX_INSTANCES_PER_BATCH " + std::to_string(MAX_INSTANCES_PER_BATCH) + "U\n" + "#define INSTANCE_SSBO_BINDING " + std::to_string(INSTANCE_SSBO_BINDING) + "\n" +
"#define LIGHT_SSBO_BINDING " + std::to_string(LIGHT_SSBO_BINDING) + "\n" + "#define LIGHT_SSBO_BINDING " + std::to_string(LIGHT_SSBO_BINDING) + "\n" +
"#define CLUSTER_GRID_X " + std::to_string(CLUSTER_GRID_X) + "\n" + "#define CLUSTER_GRID_X " + std::to_string(CLUSTER_GRID_X) + "\n" +
"#define CLUSTER_GRID_Y " + std::to_string(CLUSTER_GRID_Y) + "\n" + "#define CLUSTER_GRID_Y " + std::to_string(CLUSTER_GRID_Y) + "\n" +
@@ -184,24 +184,21 @@ void gl::glsl_common_setup()
vec4 wiper_timer_return; vec4 wiper_timer_return;
}; };
// Per-instance modelview matrices for GPU-instanced draws. #ifdef STAGE_VERTEX
// The UBO itself is declared in every stage so the binding stays valid, // Per-instance modelview matrices for GPU-instanced draws, element 0 is identity.
// but the helper functions reference gl_InstanceID which is vertex-only, // Draws which aren't instanced have gl_BaseInstance == 0 and gl_InstanceID == 0,
// so they are guarded behind STAGE_VERTEX. // so effective_modelview() reduces to model_ubo.modelview. For instanced draws
layout (std140) uniform instance_ubo // the C++ side uploads camera-space root transforms of the instances and draws
// them from base instance onwards, with model_ubo.modelview set to the submodel-local
// chain (computed from identity, NOT from the camera/instance), so the product
// gives the correct final transform per instance per submodel.
layout(std430, binding = INSTANCE_SSBO_BINDING) readonly buffer instance_ssbo
{ {
mat4 instance_modelview[MAX_INSTANCES_PER_BATCH]; mat4 instance_modelview[];
}; };
#ifdef STAGE_VERTEX
// For non-instanced draws gl_InstanceID == 0 and slot 0 is permanently set
// to identity, so effective_modelview() reduces to model_ubo.modelview.
// For instanced draws the C++ side uploads camera-space root transforms for
// instances 0..N-1 and sets model_ubo.modelview to the submodel-local chain
// (computed from identity, NOT from the camera/instance), so the product
// gives the correct final transform per instance per submodel.
mat4 effective_modelview() { mat4 effective_modelview() {
return instance_modelview[gl_InstanceID] * modelview; return instance_modelview[gl_BaseInstance + gl_InstanceID] * modelview;
} }
mat3 effective_modelviewnormal() { mat3 effective_modelviewnormal() {
// instance_modelview can include per-instance scale (uniform or per-axis), // instance_modelview can include per-instance scale (uniform or per-axis),
@@ -209,7 +206,7 @@ void gl::glsl_common_setup()
// case. Compute the correct normal matrix via transpose(inverse(...)). // case. Compute the correct normal matrix via transpose(inverse(...)).
// For pure rotation+translation this still produces the rotation matrix // For pure rotation+translation this still produces the rotation matrix
// (one extra mat3 inverse per vertex — modern GPUs handle it cheaply). // (one extra mat3 inverse per vertex — modern GPUs handle it cheaply).
mat3 instance_mv3 = mat3(instance_modelview[gl_InstanceID]); mat3 instance_mv3 = mat3(instance_modelview[gl_BaseInstance + gl_InstanceID]);
mat3 instance_normal = transpose(inverse(instance_mv3)); mat3 instance_normal = transpose(inverse(instance_mv3));
return instance_normal * modelviewnormal; return instance_normal * modelviewnormal;
} }

View File

@@ -281,9 +281,6 @@ void gl::program::init()
if ((index = glGetUniformBlockIndex(*this, "model_ubo")) != GL_INVALID_INDEX) if ((index = glGetUniformBlockIndex(*this, "model_ubo")) != GL_INVALID_INDEX)
glUniformBlockBinding(*this, index, 1); glUniformBlockBinding(*this, index, 1);
if ((index = glGetUniformBlockIndex(*this, "instance_ubo")) != GL_INVALID_INDEX)
glUniformBlockBinding(*this, index, 3);
} }
gl::program::program() gl::program::program()

View File

@@ -104,17 +104,6 @@ namespace gl
static_assert(sizeof(model_ubs) == 208 + 16 * MAX_PARAMS, "bad size of ubs"); static_assert(sizeof(model_ubs) == 208 + 16 * MAX_PARAMS, "bad size of ubs");
// maximum number of instances per single GPU-instanced draw call.
// 256 mat4 = 16 KiB, the guaranteed minimum UBO size. Bigger batches must split.
const size_t MAX_INSTANCES_PER_BATCH = 256;
struct instance_ubs
{
glm::mat4 instance_modelview[MAX_INSTANCES_PER_BATCH];
};
static_assert(sizeof(instance_ubs) == 64 * MAX_INSTANCES_PER_BATCH, "bad size of instance_ubs");
// layout std430 (shader storage buffers) // layout std430 (shader storage buffers)
// structs must match with GLSL, offsets checked below // structs must match with GLSL, offsets checked below
@@ -295,6 +284,10 @@ namespace gl
const GLuint ATMOSPHERE_RESULT_SSBO_BINDING = 7; const GLuint ATMOSPHERE_RESULT_SSBO_BINDING = 7;
const GLuint ATMOSPHERE_UBO_BINDING = 4; const GLuint ATMOSPHERE_UBO_BINDING = 4;
// binding point of the per-instance modelview matrices of instanced draws (glsl: instance_ssbo, mat4 per instance).
// element 0 is identity, the matrix of every draw which isn't instanced
const GLuint INSTANCE_SSBO_BINDING = 8;
// number of coefficients of the spherical harmonics of order 2 (bands 0..2) // number of coefficients of the spherical harmonics of order 2 (bands 0..2)
const size_t SKY_SH_COEFFICIENTS = 9; const size_t SKY_SH_COEFFICIENTS = 9;

View File

@@ -552,6 +552,7 @@ std::shared_ptr<TAnimContainer> TAnimModel::AddContainer(std::string const &Name
// containers are added by events after the instance was loaded and bucketed; // 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 // the instanced path skips RaAnimate(), so this instance has to be drawn on its own
m_instanceable = false; m_instanceable = false;
++scene::node_state_version;
return tmp; return tmp;
} }
return nullptr; return nullptr;
@@ -781,6 +782,7 @@ 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 // the instance stays in the bucket of its old skin set, where it would get the textures of the batch
m_instanceable = false; m_instanceable = false;
++scene::node_state_version;
} }
void TAnimModel::AnimUpdate(double dt) void TAnimModel::AnimUpdate(double dt)

View File

@@ -327,9 +327,9 @@ geometry_bank::draw( gfx::geometry_handle const &Geometry, gfx::stream_units con
// draws geometry stored in specified chunk InstanceCount times via instanced draw call // draws geometry stored in specified chunk InstanceCount times via instanced draw call
std::size_t std::size_t
geometry_bank::draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) { geometry_bank::draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams ) {
if( InstanceCount == 0 ) { return 0; } if( InstanceCount == 0 ) { return 0; }
return draw_instanced_( Geometry, Units, InstanceCount, Streams ); return draw_instanced_( Geometry, Units, InstanceCount, BaseInstance, Streams );
} }
// draws geometry stored in supplied sequence of chunks of the bank // draws geometry stored in supplied sequence of chunks of the bank
@@ -432,7 +432,7 @@ geometrybank_manager::draw( gfx::geometry_handle const &Geometry, unsigned int c
// draws geometry stored in specified chunk InstanceCount times via instanced draw call // draws geometry stored in specified chunk InstanceCount times via instanced draw call
void void
geometrybank_manager::draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, unsigned int const Streams ) { geometrybank_manager::draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams ) {
if( Geometry == null_handle ) { return; } if( Geometry == null_handle ) { return; }
if( InstanceCount == 0 ) { return; } if( InstanceCount == 0 ) { return; }
@@ -440,7 +440,7 @@ geometrybank_manager::draw_instanced( gfx::geometry_handle const &Geometry, std:
auto &bankrecord = bank( Geometry ); auto &bankrecord = bank( Geometry );
bankrecord.second = m_garbagecollector.timestamp(); bankrecord.second = m_garbagecollector.timestamp();
m_primitivecount += bankrecord.first->draw_instanced( Geometry, m_units, InstanceCount, Streams ); m_primitivecount += bankrecord.first->draw_instanced( Geometry, m_units, InstanceCount, BaseInstance, Streams );
} }
// draws geometry stored in supplied sequence of chunks // draws geometry stored in supplied sequence of chunks

View File

@@ -119,8 +119,8 @@ public:
auto append( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry ) -> bool; auto append( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry ) -> bool;
// draws geometry stored in specified chunk // draws geometry stored in specified chunk
auto draw( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t; auto draw( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t;
// draws geometry stored in specified chunk N times via glDrawElementsInstanced* // draws geometry stored in specified chunk N times via glDrawElementsInstanced*, as instances BaseInstance and onwards
auto draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams = basic_streams ) -> std::size_t; auto draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams = basic_streams ) -> std::size_t;
// draws geometry stored in supplied sequence of chunks of the bank, in their order. the bank is free to combine them into fewer draw calls // draws geometry stored in supplied sequence of chunks of the bank, in their order. the bank is free to combine them into fewer draw calls
auto draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t; auto draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t;
// draws geometry stored in supplied list of chunks // draws geometry stored in supplied list of chunks
@@ -185,7 +185,7 @@ private:
// draw() subclass details // draw() subclass details
virtual auto draw_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t = 0; virtual auto draw_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t = 0;
// draw_instanced() subclass details. Default implementation falls back to N regular draws. // draw_instanced() subclass details. Default implementation falls back to N regular draws.
virtual auto draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) -> std::size_t { virtual auto draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams ) -> std::size_t {
std::size_t count { 0 }; std::size_t count { 0 };
for( std::size_t i = 0; i < InstanceCount; ++i ) { count += draw_( Geometry, Units, Streams ); } for( std::size_t i = 0; i < InstanceCount; ++i ) { count += draw_( Geometry, Units, Streams ); }
return count; } return count; }
@@ -223,8 +223,8 @@ public:
// draws geometry stored in specified chunk // draws geometry stored in specified chunk
void draw( gfx::geometry_handle const &Geometry, unsigned int const Streams = basic_streams ); void draw( gfx::geometry_handle const &Geometry, unsigned int const Streams = basic_streams );
// draws geometry stored in specified chunk InstanceCount times via GPU instancing. // draws geometry stored in specified chunk InstanceCount times via GPU instancing.
// The shader reads per-instance modelview matrices from instance_ubo[gl_InstanceID]. // The shader reads per-instance modelview matrices from instance_ssbo[gl_BaseInstance + gl_InstanceID].
void draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, unsigned int const Streams = basic_streams ); void draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams = basic_streams );
// draws geometry stored in supplied sequence of chunks, in their order. neighbouring chunks of the same bank are passed to it together // draws geometry stored in supplied sequence of chunks, in their order. neighbouring chunks of the same bank are passed to it together
void draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, unsigned int const Streams = basic_streams ); void draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, unsigned int const Streams = basic_streams );
template <typename Iterator_> template <typename Iterator_>

View File

@@ -171,9 +171,9 @@ opengl46_vaogeometrybank::draw_( gfx::geometry_handle const &Geometry, gfx::stre
} }
// draw_instanced() subclass details — single GL instanced draw for InstanceCount instances. // draw_instanced() subclass details — single GL instanced draw for InstanceCount instances.
// The vertex shader reads per-instance modelview from instance_ubo[gl_InstanceID]. // The vertex shader reads per-instance modelview from instance_ssbo[gl_BaseInstance + gl_InstanceID].
std::size_t std::size_t
opengl46_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) opengl46_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams )
{ {
setup_buffer(); setup_buffer();
@@ -199,16 +199,17 @@ opengl46_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry,
if( chunkrecord.index_count > 0 ) { if( chunkrecord.index_count > 0 ) {
m_vao->bind(); m_vao->bind();
::glDrawElementsInstancedBaseVertex( ::glDrawElementsInstancedBaseVertexBaseInstance(
chunk.type, chunk.type,
chunkrecord.index_count, GL_UNSIGNED_INT, chunkrecord.index_count, GL_UNSIGNED_INT,
reinterpret_cast<void const *>( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), reinterpret_cast<void const *>( chunkrecord.index_offset * sizeof( gfx::basic_index ) ),
inst_count, inst_count,
chunkrecord.vertex_offset ); chunkrecord.vertex_offset,
BaseInstance );
} }
else { else {
m_vao->bind(); m_vao->bind();
::glDrawArraysInstanced( chunk.type, chunkrecord.vertex_offset, chunkrecord.vertex_count, inst_count ); ::glDrawArraysInstancedBaseInstance( chunk.type, chunkrecord.vertex_offset, chunkrecord.vertex_count, inst_count, BaseInstance );
} }
auto const vertexcount { ( chunkrecord.index_count > 0 ? chunkrecord.index_count : chunkrecord.vertex_count ) }; auto const vertexcount { ( chunkrecord.index_count > 0 ? chunkrecord.index_count : chunkrecord.vertex_count ) };

View File

@@ -53,9 +53,9 @@ private:
// draw() subclass details // draw() subclass details
auto auto
draw_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t override; draw_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t override;
// draw_instanced() subclass details — issues glDrawElementsInstancedBaseVertex // draw_instanced() subclass details — issues glDrawElementsInstancedBaseVertexBaseInstance
auto auto
draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) -> std::size_t override; draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams ) -> std::size_t override;
// draw_batch() subclass details — issues glMultiDrawArrays for runs of non-indexed chunks of the same type // draw_batch() subclass details — issues glMultiDrawArrays for runs of non-indexed chunks of the same type
auto auto
draw_batch_( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t override; draw_batch_( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t override;

View File

@@ -179,7 +179,11 @@ bool opengl46_renderer::Init(GLFWwindow *Window)
model_ubo = std::make_unique<gl::ubo>(sizeof(gl::model_ubs), 1, GL_STREAM_DRAW); model_ubo = std::make_unique<gl::ubo>(sizeof(gl::model_ubs), 1, GL_STREAM_DRAW);
light_ssbo = std::make_unique<gl::ssbo>(sizeof(gl::light_data), gl::LIGHT_SSBO_BINDING); light_ssbo = std::make_unique<gl::ssbo>(sizeof(gl::light_data), gl::LIGHT_SSBO_BINDING);
light_ssbo->bind_base(); light_ssbo->bind_base();
instance_ubo = std::make_unique<gl::ubo>(sizeof(gl::instance_ubs), 3, GL_STREAM_DRAW); for (auto &buffer : m_instancebuffers)
{
// grows when a frame needs more
buffer = std::make_unique<gl::ssbo>(static_cast<GLsizeiptr>(16384 * sizeof(glm::mat4)), gl::INSTANCE_SSBO_BINDING);
}
try try
{ {
@@ -203,13 +207,15 @@ bool opengl46_renderer::Init(GLFWwindow *Window)
model_ubo->update(model_ubs); model_ubo->update(model_ubs);
scene_ubo->update(scene_ubs); scene_ubo->update(scene_ubs);
// initialize instance_ubo slot 0 to identity. This is the matrix sampled by // element 0 of the instance buffers is identity. This is the matrix sampled by
// gl_InstanceID==0 in non-instanced draws, so existing rendering paths // non-instanced draws, so existing rendering paths
// continue to multiply effective_modelview = identity * modelview = modelview. // continue to multiply effective_modelview = identity * modelview = modelview.
for (auto &buffer : m_instancebuffers)
{ {
glm::mat4 identity( 1.0f ); glm::mat4 const identity(1.0f);
instance_ubo->update( reinterpret_cast<uint8_t const*>(&identity), 0, sizeof(identity) ); buffer->update(identity);
} }
m_instancebuffers[m_instancebufferindex]->bind_base();
int samples = 1 << Global.iMultisampling; int samples = 1 << Global.iMultisampling;
if (samples > 1) if (samples > 1)
@@ -630,10 +636,16 @@ bool opengl46_renderer::Render()
opengl_texture::reset_unit_cache(); opengl_texture::reset_unit_cache();
// textures of new heightmap terrain layers go to the arrays the terrain shader reads them from // textures of new heightmap terrain layers go to the arrays the terrain shader reads them from
m_terrainmaterials.update(); m_terrainmaterials.update();
if( EditorModeFlag ) { if( ( EditorModeFlag ) || ( m_nodestateversion != scene::node_state_version ) ) {
// the editor moves and resizes model instances in place, which leaves the copies of their bounds out of date // the editor moves and resizes model instances in place, events show and hide them and make them drawn on
++m_instanceboundsversion; // their own: the instance records are out of date
++m_instancerecordsversion;
m_nodestateversion = scene::node_state_version;
} }
// instanced draws of the frame place their matrices in the next of the instance buffers, the one used the longest ago
m_instancebufferindex = ( m_instancebufferindex + 1 ) % m_instancebuffers.size();
m_instancebufferfill = 1;
m_instancebuffers[ m_instancebufferindex ]->bind_base();
m_renderpass.draw_mode = rendermode::none; // force setup anew m_renderpass.draw_mode = rendermode::none; // force setup anew
m_renderpass.draw_stats = debug_stats(); m_renderpass.draw_stats = debug_stats();
@@ -729,6 +741,8 @@ void opengl46_renderer::SwapBuffers()
+ " instanced:" + to_string( m_colorpass.draw_stats.instances, 7 ) + " +" + to_string( shadowstats.instances, 7 ) + " instanced:" + to_string( m_colorpass.draw_stats.instances, 7 ) + " +" + to_string( shadowstats.instances, 7 )
+ " =" + to_string( m_colorpass.draw_stats.instances + 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" + " (" + std::to_string( m_colorpass.draw_stats.instanced_drawcalls + shadowstats.instanced_drawcalls ) + " batches)\n"
+ " instance buffer: " + std::to_string( m_instancebufferfill - 1 ) + " matrices, "
+ std::to_string( m_instancebuffers[ m_instancebufferindex ]->size() / 1024 ) + " KiB\n"
+ " inst-pool:" + std::to_string( TAnimModel::s_instanceable_total ) + " inst-pool:" + std::to_string( TAnimModel::s_instanceable_total )
+ "/" + std::to_string( TAnimModel::s_classified_total ) + "/" + std::to_string( TAnimModel::s_classified_total )
+ " rej(noModel/lights/anim/animSubM): " + " rej(noModel/lights/anim/animSubM): "
@@ -2999,7 +3013,7 @@ void opengl46_renderer::draw(const gfx::geometry_handle &handle)
if( m_current_instance_count > 0 ) { if( m_current_instance_count > 0 ) {
// inside Render_Instanced(): one GL instanced draw replaces what would // inside Render_Instanced(): one GL instanced draw replaces what would
// otherwise be N regular draws (one per instance) at this submodel // otherwise be N regular draws (one per instance) at this submodel
m_geometry.draw_instanced( handle, m_current_instance_count ); m_geometry.draw_instanced( handle, m_current_instance_count, m_current_instance_base );
} }
else { else {
m_geometry.draw( handle ); m_geometry.draw( handle );
@@ -3182,13 +3196,13 @@ void opengl46_renderer::Render(TAnimModel *Instance)
} }
// adds the bucketed instances of a scene cell to the frame-level buckets, for Render_Instanced() to draw. // adds the bucketed instances of a scene cell to the frame-level buckets, for Render_Instanced() to draw.
// Render_Instanced() culls each instance it's given. the tests it makes are repeated here on the copies of instance // Render_Instanced() culls each instance it's given by its record. the tests it makes are repeated here, with some slack
// bounds kept by the buckets, with some slack for the single precision of the copies, and the instances which fail are // for the single precision of the records, and the instances which fail are left out of the queue
// left out: the instances themselves are fetched only when there's a chance for them to be drawn
void opengl46_renderer::Queue_Instances( scene::basic_cell const *Cell ) void opengl46_renderer::Queue_Instances( scene::basic_cell const *Cell )
{ {
// the editor moves and resizes the instances in place, the copies can't be relied on while it's active // the editor moves and resizes the instances in place. the records are made anew for each frame then,
auto const usebounds { false == EditorModeFlag }; // but aren't relied on to leave instances out of the queue
auto const userecords { false == EditorModeFlag };
auto const reflections { m_renderpass.draw_mode == rendermode::reflections }; auto const reflections { m_renderpass.draw_mode == rendermode::reflections };
// same choices as in Render_Instanced(): shadows measure the distance from the real camera, reflections // same choices as in Render_Instanced(): shadows measure the distance from the real camera, reflections
// ignore zoom and distance factor, test the plain distance against their own range and add the fidelity offset // ignore zoom and distance factor, test the plain distance against their own range and add the fidelity offset
@@ -3198,42 +3212,39 @@ void opengl46_renderer::Queue_Instances( scene::basic_cell const *Cell )
auto const distanceoffset { reflections ? sq( static_cast<double>( EU07_REFLECTIONFIDELITYOFFSET ) ) : 0.0 }; auto const distanceoffset { reflections ? sq( static_cast<double>( EU07_REFLECTIONFIDELITYOFFSET ) ) : 0.0 };
auto const slack { 1.001 }; auto const slack { 1.001 };
auto const distancelimit { slack * ( reflections ? sq( static_cast<double>( Global.reflectiontune.range_instances ) ) : sq( static_cast<double>( m_renderpass.draw_range ) + 250.0 ) ) }; 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 // up close the rounding of the records is too large a part of the distance for the slack to cover it
auto const distancetested { sq( 64.0 ) }; auto const distancetested { sq( 64.0 ) };
for( auto const &bucket : Cell->m_instancebuckets_opaque ) { for( auto const &bucket : Cell->m_instancebuckets_opaque ) {
auto const &instances { bucket.second.instances }; auto const &instancebucket { bucket.second };
if( false == usebounds ) { if( ( instancebucket.recordsversion != m_instancerecordsversion )
auto &dest { m_frame_instance_buckets[ bucket.first ] }; || ( instancebucket.records.size() != instancebucket.instances.size() ) ) {
dest.insert( dest.end(), instances.begin(), instances.end() ); Update_Instance_Records( bucket.first.pModel, instancebucket, Cell->m_area.center );
continue;
} }
if( ( bucket.second.boundsversion != m_instanceboundsversion ) auto const &records { instancebucket.records };
|| ( bucket.second.bounds.size() != instances.size() ) ) { std::vector<queued_instance> *dest { nullptr }; // looked up when there's something to put in it
Update_Instance_Bounds( bucket.first.pModel, bucket.second ); for( std::size_t idx = 0; idx < records.size(); ++idx ) {
} if( userecords ) {
auto const &bounds { bucket.second.bounds }; auto const &record { records[ idx ] };
std::vector<TAnimModel *> *dest { nullptr }; // looked up when there's something to put in it auto const center { instancebucket.recordorigin + glm::dvec3{ record.position } };
for( std::size_t idx = 0; idx < bounds.size(); ++idx ) { auto const distancesquared { glm::length2( center - cameraposition ) };
auto const &instancebounds { bounds[ idx ] }; if( distancesquared > distancetested ) {
auto const center { glm::dvec3{ instancebounds.center } }; auto const scaleddistancesquared { distancesquared * distancescale };
auto const distancesquared { glm::length2( center - cameraposition ) }; if( scaleddistancesquared > distancelimit ) { continue; }
if( distancesquared > distancetested ) { if( scaleddistancesquared + distanceoffset >= slack * std::min( instancebucket.lodrangesquared, record.rangesquaredmax ) ) { continue; }
auto const scaleddistancesquared { distancesquared * distancescale }; }
if( scaleddistancesquared > distancelimit ) { continue; } if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ center, record.radius } ) ) { continue; }
if( scaleddistancesquared + distanceoffset >= slack * instancebounds.rangesquaredmax ) { continue; }
} }
if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ center, instancebounds.radius } ) ) { continue; }
if( dest == nullptr ) { if( dest == nullptr ) {
dest = &m_frame_instance_buckets[ bucket.first ]; dest = &m_frame_instance_buckets[ bucket.first ];
} }
dest->emplace_back( instances[ idx ] ); dest->push_back( { &instancebucket, idx } );
} }
} }
} }
// makes the copies of the bounds of the instances held by a bucket of a scene cell // makes the records of the instances held by a bucket of a scene cell, with locations relative to Origin
void opengl46_renderer::Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ) void opengl46_renderer::Update_Instance_Records( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket, glm::dvec3 const &Origin )
{ {
// a submodel is drawn only closer than its fSquareMaxDist, see Render( TSubModel * ). past the largest of these // a submodel is drawn only closer than its fSquareMaxDist, see Render( TSubModel * ). past the largest of these
// there's nothing of the model left to draw, which makes it another limit of the range of its instances // there's nothing of the model left to draw, which makes it another limit of the range of its instances
@@ -3248,31 +3259,63 @@ void opengl46_renderer::Update_Instance_Bounds( TModel3d const *Model, scene::ba
m_instance_lodpending.push_back( submodel->Child ); m_instance_lodpending.push_back( submodel->Child );
m_instance_lodpending.push_back( submodel->Next ); m_instance_lodpending.push_back( submodel->Next );
} }
Bucket.lodrangesquared = lodrangesquared;
Bucket.recordorigin = Origin;
Bucket.bounds.clear(); auto const tofloat = []( double const Value ) {
Bucket.bounds.reserve( Bucket.instances.size() ); return static_cast<float>( std::min( Value, static_cast<double>( std::numeric_limits<float>::max() ) ) ); };
for( auto const *instance : Bucket.instances ) {
scene::basic_cell::instance_bounds instancebounds; Bucket.records.clear();
Bucket.records.reserve( Bucket.instances.size() );
for( auto *instance : Bucket.instances ) {
scene::basic_cell::instance_record record;
if( instance != nullptr ) { if( instance != nullptr ) {
instancebounds.center = glm::vec3{ instance->m_area.center }; record.position = glm::vec3{ instance->location() - Origin };
instancebounds.radius = instance->m_area.radius; record.radius = instance->radius();
instancebounds.rangesquaredmax = std::min( lodrangesquared, static_cast<float>( std::min( instance->m_rangesquaredmax, static_cast<double>( std::numeric_limits<float>::max() ) ) ) ); record.rangesquaredmin = tofloat( instance->m_rangesquaredmin );
record.rangesquaredmax = tofloat( instance->m_rangesquaredmax );
record.flags =
( instance->visible() ? scene::basic_cell::instance_visible : 0u )
| ( instance->m_instanceable ? scene::basic_cell::instance_instanceable : 0u );
auto const &rotationscale { instance->rotation_scale() };
for( int column = 0; column < 3; ++column ) {
record.rotationscale[ column ] = rotationscale[ column ];
}
} }
Bucket.bounds.emplace_back( instancebounds ); // an empty slot keeps its record without flags, and isn't drawn
Bucket.records.emplace_back( record );
} }
Bucket.boundsversion = m_instanceboundsversion; Bucket.recordsversion = m_instancerecordsversion;
}
// places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them
std::uint32_t opengl46_renderer::Upload_Instance_Modelviews( glm::mat4 const *Modelviews, std::size_t const Count )
{
auto &buffer { m_instancebuffers[ m_instancebufferindex ] };
auto const needed { static_cast<GLsizeiptr>( ( m_instancebufferfill + Count ) * sizeof( glm::mat4 ) ) };
if( needed > buffer->size() ) {
// the draws issued with the replaced buffer keep it until the gpu is done with them. the new one starts with identity
buffer->resize( std::max( needed, 2 * buffer->size() ) );
glm::mat4 const identity( 1.0f );
buffer->update( identity );
buffer->bind_base();
m_instancebufferfill = 1;
}
auto const base { static_cast<std::uint32_t>( m_instancebufferfill ) };
buffer->update( Modelviews, static_cast<GLintptr>( base * sizeof( glm::mat4 ) ), static_cast<GLsizeiptr>( Count * sizeof( glm::mat4 ) ) );
m_instancebufferfill += Count;
return base;
} }
// True GPU-instanced render path for a group of TAnimModel instances sharing the same // True GPU-instanced render path for a group of TAnimModel instances sharing the same
// TModel3d. The submodel tree is walked ONCE per batch; at every submodel that draws // TModel3d. The submodel tree is walked ONCE per lod band; at every submodel that draws
// geometry we issue a single glDrawElementsInstancedBaseVertex(N) covering all visible // geometry we issue a single glDrawElementsInstancedBaseVertexBaseInstance(N) covering all
// instances. The vertex shader multiplies effective_modelview = instance_modelview[gl_InstanceID] // visible instances of the band. The vertex shader multiplies effective_modelview =
// * model_ubs.modelview, where instance_modelview[i] is the camera-space root transform // instance_modelview[gl_BaseInstance + gl_InstanceID] * model_ubs.modelview, where
// of instance i (precomputed below) and model_ubs.modelview is the submodel-local chain // instance_modelview[i] is the camera-space root transform of an instance (precomputed below)
// (accumulated by the matrix stack starting from identity, NOT the camera/instance frame). // and model_ubs.modelview is the submodel-local chain (accumulated by the matrix stack starting
// // from identity, NOT the camera/instance frame).
// Batch size is capped at MAX_INSTANCES_PER_BATCH (256). Larger groups are split. void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector<queued_instance> const &Instances )
void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimModel *> const &Instances )
{ {
if( Model == nullptr ) { return; } if( Model == nullptr ) { return; }
if( Instances.empty() ) { return; } if( Instances.empty() ) { return; }
@@ -3287,7 +3330,8 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
// whose materials are all excluded by the shadow rank cutoff (e.g. grass) would otherwise go through // whose materials are all excluded by the shadow rank cutoff (e.g. grass) would otherwise go through
// the whole per-instance culling and the batched draws in every shadow pass, only for every submodel // the whole per-instance culling and the batched draws in every shadow pass, only for every submodel
// to be skipped in Render(TSubModel*) // to be skipped in Render(TSubModel*)
auto const *skins { Instances.front() != nullptr && Instances.front()->Material() != nullptr ? Instances.front()->Material()->replacable_skins : nullptr }; auto const *frontinstance { Instances.front().bucket->instances[ Instances.front().index ] };
auto const *skins { frontinstance != nullptr && frontinstance->Material() != nullptr ? frontinstance->Material()->replacable_skins : nullptr };
bool castsshadow { m_renderpass.draw_mode != rendermode::shadows }; bool castsshadow { m_renderpass.draw_mode != rendermode::shadows };
m_instance_lodbounds.clear(); m_instance_lodbounds.clear();
m_instance_lodpending.clear(); m_instance_lodpending.clear();
@@ -3310,9 +3354,9 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), 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 }; auto const bandcount { m_instance_lodbounds.size() + 1 };
// 1. Visibility / distance cull. Collect surviving instances with their precomputed // 1. Visibility / distance cull, by the records of the instances. Collect surviving instances with
// camera-space root modelview matrices and lod bands. m_instance_survivors and the other // their precomputed camera-space root modelview matrices and lod bands. m_instance_survivors and the
// scratch vectors are persistent members reused across every Render_Instanced() call: // 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 // clear() drops the contents but keeps the allocated capacity, so after the first few
// frames this stops calling malloc/free entirely. // frames this stops calling malloc/free entirely.
m_instance_survivors.clear(); m_instance_survivors.clear();
@@ -3324,55 +3368,58 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
// per-instance transform has been pushed yet. // per-instance transform has been pushed yet.
glm::mat4 const view_matrix = OpenGLMatrices.data( GL_MODELVIEW ); glm::mat4 const view_matrix = OpenGLMatrices.data( GL_MODELVIEW );
bool prepared_shared_state = false; // RaPrepare() runs once per bucket TAnimModel *batch_instance { nullptr }; // the first instance drawn; its RaPrepare() and material serve the whole bucket
material_data const *batch_material { nullptr }; material_data const *batch_material { nullptr };
float farthest_distancesquared { 0.f }; float farthest_distancesquared { 0.f };
for( auto *Instance : Instances ) { for( auto const &queued : Instances ) {
if( Instance == nullptr ) { continue; } auto const &record { queued.bucket->records[ queued.index ] };
// instances which lost their instanceable status after being bucketed (animation or skin // instances which lost their instanceable status after being bucketed (animation or skin
// assigned by an event) are drawn by the per-node path // assigned by an event) are drawn by the per-node path
if( false == Instance->m_instanceable ) { continue; } if( ( record.flags & scene::basic_cell::instance_instanceable ) == 0 ) { continue; }
if( false == Instance->m_visible ) { continue; } if( ( record.flags & scene::basic_cell::instance_visible ) == 0 ) { continue; }
if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { continue; } glm::dvec3 const location { queued.bucket->recordorigin + glm::dvec3{ record.position } };
if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ location, record.radius } ) ) { continue; }
double distancesquared; double distancesquared;
switch( m_renderpass.draw_mode ) { switch( m_renderpass.draw_mode ) {
case rendermode::shadows: case rendermode::shadows:
distancesquared = glm::length2( ( Instance->location() - m_renderpass.viewport_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor; distancesquared = glm::length2( ( location - m_renderpass.viewport_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor;
break; break;
case rendermode::reflections: case rendermode::reflections:
distancesquared = glm::length2( ( Instance->location() - m_renderpass.pass_camera.position() ) ); distancesquared = glm::length2( ( location - m_renderpass.pass_camera.position() ) );
if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) { continue; } if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) { continue; }
distancesquared += sq( EU07_REFLECTIONFIDELITYOFFSET ); distancesquared += sq( EU07_REFLECTIONFIDELITYOFFSET );
break; break;
default: default:
distancesquared = glm::length2( ( Instance->location() - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor; distancesquared = glm::length2( ( location - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor;
break; break;
} }
if( ( distancesquared < Instance->m_rangesquaredmin ) if( ( distancesquared < record.rangesquaredmin )
|| ( distancesquared >= Instance->m_rangesquaredmax ) ) { continue; } || ( distancesquared >= record.rangesquaredmax ) ) { continue; }
auto const drawdistancethreshold { m_renderpass.draw_range + 250 }; auto const drawdistancethreshold { m_renderpass.draw_range + 250 };
if( distancesquared > sq( drawdistancethreshold ) ) { continue; } if( distancesquared > sq( drawdistancethreshold ) ) { continue; }
auto const radiussquared { Instance->radius() * Instance->radius() }; auto const radiussquared { record.radius * record.radius };
if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { continue; } if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { continue; }
if( !prepared_shared_state ) { if( batch_instance == nullptr ) {
Instance->RaPrepare(); batch_instance = queued.bucket->instances[ queued.index ];
prepared_shared_state = true; batch_instance->RaPrepare();
batch_material = Instance->Material(); batch_material = batch_instance->Material();
} }
Instance->m_framestamp = m_framestamp;
// 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)
// The rotation and scale part is cached by the instance, the translation goes into its last // The rotation and scale part comes from the record, the translation goes into its last
// column. The scale is folded in so the GPU-instanced path produces visually identical output // 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. // to the regular per-instance path; the shader applies it via effective_modelview = instance_mv * model_local.
glm::mat4 root { Instance->rotation_scale() }; glm::mat4 const root {
root[ 3 ] = glm::vec4( glm::vec3( Instance->location() - m_renderpass.pass_camera.position() ), 1.f ); record.rotationscale[ 0 ],
record.rotationscale[ 1 ],
record.rotationscale[ 2 ],
glm::vec4( glm::vec3( location - m_renderpass.pass_camera.position() ), 1.f ) };
auto const distance { static_cast<float>( distancesquared ) }; auto const distance { static_cast<float>( distancesquared ) };
farthest_distancesquared = std::max( farthest_distancesquared, distance ); farthest_distancesquared = std::max( farthest_distancesquared, distance );
@@ -3384,7 +3431,7 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
if( m_instance_survivors.empty() ) { return; } if( m_instance_survivors.empty() ) { return; }
// group the modelviews by lod band into a contiguous array, for the UBO upload. every distance in // group the modelviews by lod band into a contiguous array, for the upload. every distance in
// a band selects the same lod; the nearest one represents the band. // a band selects the same lod; the nearest one represents the band.
// within a band the instances are additionally ordered roughly near-to-far, in distance slots. dense // within a band the instances are additionally ordered roughly near-to-far, in distance slots. dense
// alpha tested foliage then gets the most out of the early depth test, as the nearest instances fill // alpha tested foliage then gets the most out of the early depth test, as the nearest instances fill
@@ -3409,55 +3456,43 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
for( auto const &survivor : m_instance_survivors ) { for( auto const &survivor : m_instance_survivors ) {
m_instance_modelviews[ m_instance_bandcursors[ survivor.band ]++ ] = survivor.modelview; m_instance_modelviews[ m_instance_bandcursors[ survivor.band ]++ ] = survivor.modelview;
} }
// all bands go to the instance buffer at once, each band is drawn from its own base instance
auto const baseinstance { Upload_Instance_Modelviews( m_instance_modelviews.data(), m_instance_modelviews.size() ) };
auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 ); auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 );
alpha ^= 0x0F0F000F; alpha ^= 0x0F0F000F;
// 2. Walk the submodel tree once per sub-batch. The submodel-local matrix // 2. Walk the submodel tree once per lod band. 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_BaseInstance + gl_InstanceID] in the shader.
for( std::size_t band = 0; band < bandcount; ++band ) { for( std::size_t band = 0; band < bandcount; ++band ) {
std::size_t offset_idx = m_instance_bandstarts[ band * distanceslots ]; std::size_t const band_start = m_instance_bandstarts[ band * distanceslots ];
std::size_t const band_end = m_instance_bandstarts[ ( band + 1 ) * distanceslots ]; std::size_t const band_end = m_instance_bandstarts[ ( band + 1 ) * distanceslots ];
while( offset_idx < band_end ) { if( band_start == band_end ) { continue; }
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]. // 2a. Push identity onto the matrix stack so submodel transforms
instance_ubo->update( // accumulate from identity, not from camera/instance space.
reinterpret_cast<uint8_t const *>( m_instance_modelviews.data() + offset_idx ), ::glPushMatrix();
0, ::glLoadIdentity();
static_cast<int>( this_batch * sizeof( glm::mat4 ) ) );
// 2b. Push identity onto the matrix stack so submodel transforms // 2b. Configure the existing TModel3d/TSubModel render path. Setting
// accumulate from identity, not from camera/instance space. // m_current_instance_count routes draw(handle) calls to draw_instanced.
::glPushMatrix(); m_current_instance_count = band_end - band_start;
::glLoadIdentity(); m_current_instance_base = baseinstance + static_cast<std::uint32_t>( band_start );
// 2c. Configure the existing TModel3d/TSubModel render path. Setting Model->Root->fSquareDist = m_instance_banddistances[ band ]; // shared global, used by submodel LOD
// m_current_instance_count routes draw(handle) calls to draw_instanced. Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha );
m_current_instance_count = this_batch; Model->Root->pRoot = Model;
Model->Root->fSquareDist = m_instance_banddistances[ band ]; // shared global, used by submodel LOD Render( Model->Root );
Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha );
Model->Root->pRoot = Model;
Render( Model->Root ); m_current_instance_count = 0;
m_current_instance_base = 0;
m_current_instance_count = 0; ::glPopMatrix();
::glPopMatrix(); ++m_renderpass.draw_stats.instanced_drawcalls;
offset_idx += this_batch;
++m_renderpass.draw_stats.instanced_drawcalls;
}
}
// 3. Restore instance_modelview[0] to identity so subsequent
// non-instanced draws continue to compute identity * modelview.
// done once the last batch is out, as everything drawn up to that point is instanced
{
glm::mat4 const identity( 1.0f );
instance_ubo->update( reinterpret_cast<uint8_t const *>( &identity ), 0, sizeof( identity ) );
} }
m_renderpass.draw_stats.instances += static_cast<int>( m_instance_survivors.size() ); m_renderpass.draw_stats.instances += static_cast<int>( m_instance_survivors.size() );
@@ -3467,8 +3502,8 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
// Renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) using the // Renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) using the
// existing GPU-instanced submodel pipeline. The track owns a vector of pre-baked // existing GPU-instanced submodel pipeline. The track owns a vector of pre-baked
// local-space matrices; we compose each with `view * translate(track_origin - camera)` // local-space matrices; we compose each with `view * translate(track_origin - camera)`
// to get a camera-space modelview, then issue batched glDrawElementsInstancedBaseVertex // to get a camera-space modelview, then issue glDrawElementsInstancedBaseVertexBaseInstance
// calls -- one batch per MAX_INSTANCES_PER_BATCH sleepers. // calls -- one per LOD band of the sleepers.
// //
// Skipped entirely when: // Skipped entirely when:
// - Global.SleeperDistance == 0 (sleeper rendering globally disabled) // - Global.SleeperDistance == 0 (sleeper rendering globally disabled)
@@ -3665,7 +3700,7 @@ void opengl46_renderer::Render_Sleepers( TTrack *Track )
[]( std::pair<float, glm::mat4> const &Left, std::pair<float, glm::mat4> const &Right ) { []( std::pair<float, glm::mat4> const &Left, std::pair<float, glm::mat4> const &Right ) {
return Left.first < Right.first; } ); return Left.first < Right.first; } );
// contiguous copy of the sorted modelview matrices, for the UBO upload. // contiguous copy of the sorted modelview matrices, for the upload.
// Per-sleeper distances stay available as survivors[i].first, in lockstep. // Per-sleeper distances stay available as survivors[i].first, in lockstep.
std::vector<glm::mat4> instance_modelviews; std::vector<glm::mat4> instance_modelviews;
instance_modelviews.reserve( survivors.size() ); instance_modelviews.reserve( survivors.size() );
@@ -3705,12 +3740,12 @@ void opengl46_renderer::Render_Sleepers( TTrack *Track )
} }
// Phase 3 -- draw. Walk the sorted survivors one LOD band at a time. Each // Phase 3 -- draw. Walk the sorted survivors one LOD band at a time. Each
// band is submitted as one or more instanced draws (split only when it // band is submitted as one instanced draw from its own base instance; every
// exceeds MAX_INSTANCES_PER_BATCH); every draw in the band uses an in-band // draw uses an in-band fSquareDist, so each sleeper renders at the LOD its own
// fSquareDist, so each sleeper renders at the LOD its own distance selects // distance selects while instancing stays fully in effect.
// while instancing stays fully in effect.
auto *Model = Track->m_sleeper_model; auto *Model = Track->m_sleeper_model;
std::size_t const total = instance_modelviews.size(); std::size_t const total = instance_modelviews.size();
auto const baseinstance { Upload_Instance_Modelviews( instance_modelviews.data(), total ) };
std::size_t band_start = 0; std::size_t band_start = 0;
while( band_start < total ) { while( band_start < total ) {
// the band ends at the first sleeper distance that reaches the next LOD // the band ends at the first sleeper distance that reaches the next LOD
@@ -3728,42 +3763,27 @@ void opengl46_renderer::Render_Sleepers( TTrack *Track )
// sleeper is used as the representative fSquareDist. // sleeper is used as the representative fSquareDist.
float const band_distancesquared = survivors[ band_start ].first; float const band_distancesquared = survivors[ band_start ].first;
std::size_t offset_idx = band_start; ::glPushMatrix();
while( offset_idx < band_end ) { ::glLoadIdentity();
std::size_t const this_batch = std::min<std::size_t>( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH );
instance_ubo->update( m_current_instance_count = band_end - band_start;
reinterpret_cast<uint8_t const *>( instance_modelviews.data() + offset_idx ), m_current_instance_base = baseinstance + static_cast<std::uint32_t>( band_start );
0,
static_cast<int>( this_batch * sizeof( glm::mat4 ) ) );
::glPushMatrix(); Model->Root->fSquareDist = band_distancesquared;
::glLoadIdentity(); auto alpha = ( has_skin ? sleeper_material.textures_alpha : 0x30300030 );
alpha ^= 0x0F0F000F;
Model->Root->ReplacableSet( ( has_skin ? sleeper_material.replacable_skins : nullptr ), alpha );
Model->Root->pRoot = Model;
m_current_instance_count = this_batch; Render( Model->Root );
Model->Root->fSquareDist = band_distancesquared; m_current_instance_count = 0;
auto alpha = ( has_skin ? sleeper_material.textures_alpha : 0x30300030 ); m_current_instance_base = 0;
alpha ^= 0x0F0F000F;
Model->Root->ReplacableSet( ( has_skin ? sleeper_material.replacable_skins : nullptr ), alpha );
Model->Root->pRoot = Model;
Render( Model->Root ); ::glPopMatrix();
m_current_instance_count = 0; ++m_renderpass.draw_stats.instanced_drawcalls;
::glPopMatrix();
// restore instance_modelview[0] to identity so subsequent non-instanced draws
// continue to compute identity * modelview (mirroring Render_Instanced).
{
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; band_start = band_end;
} }

View File

@@ -305,17 +305,25 @@ class opengl46_renderer : public gfx_renderer {
void Render(cell_sequence::iterator First, cell_sequence::iterator Last); void Render(cell_sequence::iterator First, cell_sequence::iterator Last);
void Render(scene::shape_node const &Shape, bool const Ignorerange); void Render(scene::shape_node const &Shape, bool const Ignorerange);
void Render(TAnimModel *Instance); void Render(TAnimModel *Instance);
// an instance of a bucket of a scene cell, queued to be drawn by Render_Instanced()
struct queued_instance {
scene::basic_cell::instance_bucket const *bucket;
std::size_t index; // of the instance and its record in the bucket
};
// batched render path for many TAnimModel instances that share the same TModel3d. // batched render path for many TAnimModel instances that share the same TModel3d.
// Caller guarantees every instance has m_instanceable == true (no per-instance lights, // Caller guarantees every instance was instanceable when bucketed (no per-instance lights,
// no submodel animation, no replacable skins). Each call counts as one // no submodel animation, same replacable skins); the records of the instances tell which
// instanced_drawcall in draw_stats and contributes Instances.size() to the // ones still are. Instances are individually frustum/distance culled by their records, the
// instances counter. Instances are still individually frustum/distance culled. // TAnimModel itself is read only for the first instance drawn. Each lod band drawn counts as
void Render_Instanced( TModel3d *Model, std::vector<TAnimModel *> const &Instances ); // one instanced_drawcall in draw_stats; the instances drawn are added to the instances counter.
void Render_Instanced( TModel3d *Model, std::vector<queued_instance> const &Instances );
// adds the bucketed instances of a scene cell to the frame-level buckets drawn by Render_Instanced(), // 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 // less the ones their records show to be out of sight in the current render pass
void Queue_Instances( scene::basic_cell const *Cell ); void Queue_Instances( scene::basic_cell const *Cell );
// makes the copies of the bounds of the instances held by a bucket of a scene cell // makes the records of the instances held by a bucket of a scene cell, with locations relative to Origin
void Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ); void Update_Instance_Records( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket, glm::dvec3 const &Origin );
// places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them
std::uint32_t Upload_Instance_Modelviews( glm::mat4 const *Modelviews, std::size_t const Count );
bool Render(TDynamicObject *Dynamic); 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, glm::dvec3 const &Position, glm::vec3 const &Angle);
bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance); bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance);
@@ -455,14 +463,16 @@ class opengl46_renderer : public gfx_renderer {
// 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; the vectors are emptied (not freed) 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; std::unordered_map<scene::basic_cell::instance_bucket_key, std::vector<queued_instance>, scene::basic_cell::instance_bucket_key_hash> m_frame_instance_buckets;
// textures used by vehicle models, for the release of far textures // textures used by vehicle models, for the release of far textures
std::unordered_map<TModel3d const *, std::vector<texture_handle>> m_modeltextures; std::unordered_map<TModel3d const *, std::vector<texture_handle>> m_modeltextures;
// geometry of neighbouring tracks of a cell which share the material, gathered to be drawn together. reused by each Render(path_sequence) // geometry of neighbouring tracks of a cell which share the material, gathered to be drawn together. reused by each Render(path_sequence)
std::vector<gfx::geometrybank_handle> m_pathbatch; std::vector<gfx::geometrybank_handle> m_pathbatch;
// copies of instance bounds kept by the buckets of scene cells are good as long as they carry this number. // instance records kept by the buckets of scene cells are good as long as they carry this number.
// it changes with each frame drawn with the scenery editor active, as the editor moves and resizes instances in place // it changes with each frame drawn with the scenery editor active, as the editor moves and resizes instances in place,
unsigned int m_instanceboundsversion { 1 }; // and whenever scene::node_state_version does, as instances are shown, hidden or drawn on their own
unsigned int m_instancerecordsversion { 1 };
unsigned int m_nodestateversion { 0 }; // scene::node_state_version which m_instancerecordsversion was last checked against
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
std::vector<TSubModel const *> m_pickcontrolsitems; std::vector<TSubModel const *> m_pickcontrolsitems;
@@ -506,18 +516,20 @@ class opengl46_renderer : public gfx_renderer {
std::unique_ptr<gl::ubo> scene_ubo; std::unique_ptr<gl::ubo> scene_ubo;
std::unique_ptr<gl::ubo> model_ubo; std::unique_ptr<gl::ubo> model_ubo;
std::unique_ptr<gl::ssbo> light_ssbo; std::unique_ptr<gl::ssbo> light_ssbo;
// per-instance modelview UBO for GPU-instanced draws. // per-instance modelview matrices of GPU-instanced draws, one buffer for each of the last frames, used in turn so
// Slot 0 is initialized once to identity and never overwritten by non-instanced // a frame doesn't write to the buffer the gpu may still read for an earlier one. element 0 of each is identity
// rendering, so existing draws (where gl_InstanceID==0) compute identity*modelview // and never overwritten: draws which aren't instanced (gl_BaseInstance == gl_InstanceID == 0) compute
// in the vertex shader and behave exactly as before. Render_Instanced uploads // identity*modelview in the vertex shader. Render_Instanced and Render_Sleepers append the camera-space root
// per-instance camera-space root matrices to slots 0..N-1, calls glDrawElementsInstanced*, // matrices of their instances past the ones placed earlier in the frame, and draw them from that base instance
// then restores slot 0 to identity for subsequent non-instanced draws. std::array<std::unique_ptr<gl::ssbo>, 3> m_instancebuffers;
std::unique_ptr<gl::ubo> instance_ubo; std::size_t m_instancebufferindex { 0 }; // the buffer of the current frame
std::size_t m_instancebufferfill { 1 }; // matrices placed in it so far, identity included
// when > 0, the renderer is inside a Render_Instanced() call. The draw(handle) // when > 0, the renderer is inside a Render_Instanced() call. The draw(handle)
// method routes to glDrawElementsInstanced* with this many instances rather // method routes to glDrawElementsInstanced* with this many instances from
// than a single regular draw. The vertex shader reads per-instance modelview // m_current_instance_base onwards rather than a single regular draw. The vertex shader reads
// from instance_ubo[gl_InstanceID]. Reset to 0 by Render_Instanced() on exit. // per-instance modelview from instance_ssbo[gl_BaseInstance + gl_InstanceID]. Reset to 0 on exit.
std::size_t m_current_instance_count { 0 }; std::size_t m_current_instance_count { 0 };
std::uint32_t m_current_instance_base { 0 };
// persistent scratch buffer for Render_Instanced(): holds the per-instance // persistent scratch buffer for Render_Instanced(): holds the per-instance
// camera-space root modelview matrices for the batch currently being built. // camera-space root modelview matrices for the batch currently being built.
// Kept as a member rather than a function-local so its heap allocation is // Kept as a member rather than a function-local so its heap allocation is

View File

@@ -381,7 +381,7 @@ basic_cell::insert( TAnimModel *Instance ) {
} }
auto &bucket { m_instancebuckets_opaque[ key ] }; auto &bucket { m_instancebuckets_opaque[ key ] };
bucket.instances.emplace_back( Instance ); bucket.instances.emplace_back( Instance );
bucket.bounds.clear(); bucket.records.clear();
} }
} }
// re-calculate cell bounding area, in case model extends outside the cell's boundaries // re-calculate cell bounding area, in case model extends outside the cell's boundaries
@@ -484,7 +484,7 @@ basic_cell::erase( TAnimModel *Instance ) {
instances.erase( instances.erase(
std::remove( std::begin( instances ), std::end( instances ), Instance ), std::remove( std::begin( instances ), std::end( instances ), Instance ),
std::end( instances ) ); std::end( instances ) );
bucket->second.bounds.clear(); bucket->second.records.clear();
if( instances.empty() ) { if( instances.empty() ) {
bucket = m_instancebuckets_opaque.erase( bucket ); bucket = m_instancebuckets_opaque.erase( bucket );
} }

View File

@@ -237,19 +237,31 @@ public:
} }
}; };
using instance_bucket_map = std::unordered_map< instance_bucket_key, std::vector<TAnimModel *>, instance_bucket_key_hash >; using instance_bucket_map = std::unordered_map< instance_bucket_key, std::vector<TAnimModel *>, instance_bucket_key_hash >;
// what it takes to tell that an instance is out of sight, copied from the instance. // what it takes to cull and place an instance, copied from the instance.
// a cell can hold thousands of instances, each of them large and placed wherever the memory was free, and a render // a cell can hold thousands of instances, each of them large and placed wherever the memory was free, and a render
// pass rejects most of them. the renderer makes the copies and goes by them, to fetch only the instances it may draw // pass rejects most of them. the renderer makes the copies and goes by them, and reads the instance itself only
struct instance_bounds { // for the first one of a batch drawn. the layout follows std430, for the copies to be handed to the gpu as they are
glm::vec3 center { 0.f }; // location of the instance, in the precision the frustum test takes it struct instance_record {
glm::vec3 position { 0.f }; // location of the instance, relative to the origin of the records of its bucket
float radius { 0.f }; float radius { 0.f };
float rangesquaredmax { 0.f }; // past it the instance isn't drawn: by its own range, or the lod stages of its model float rangesquaredmin { 0.f }; // visibility range of the instance
float rangesquaredmax { 0.f };
std::uint32_t flags { 0 }; // instance_record_flags
std::uint32_t pad { 0 };
glm::vec4 rotationscale[ 3 ] {}; // columns of rotation and scale of the instance, w unused
};
static_assert( sizeof( instance_record ) == 80, "instance_record has to keep its std430 layout" );
enum instance_record_flags : std::uint32_t {
instance_visible = 1 << 0,
instance_instanceable = 1 << 1, // false once an event gave the instance animations or a skin of its own
}; };
struct instance_bucket { struct instance_bucket {
std::vector<TAnimModel *> instances; std::vector<TAnimModel *> instances;
// caches owned by the renderer; emptied whenever the content of the bucket changes // 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 std::vector<instance_record> records; // copies for the instances, in the same order
mutable unsigned int boundsversion { 0 }; mutable glm::dvec3 recordorigin { 0.0 }; // the records hold locations relative to it
mutable float lodrangesquared { 0.f }; // past it no submodel of the model of the bucket is drawn
mutable unsigned int recordsversion { 0 };
}; };
using instance_bucketdata_map = std::unordered_map< instance_bucket_key, instance_bucket, instance_bucket_key_hash >; using instance_bucketdata_map = std::unordered_map< instance_bucket_key, instance_bucket, instance_bucket_key_hash >;
using sound_sequence = std::vector<sound_source *>; using sound_sequence = std::vector<sound_source *>;

View File

@@ -345,6 +345,10 @@ private:
// changes whenever a node is shown or hidden, or a model instance stops being drawn along with other instances of its
// model. copies of these states (instance records of the renderer) are made anew once it changes
inline unsigned int node_state_version { 1 };
// base interface for nodes which can be actvated in scenario editor // base interface for nodes which can be actvated in scenario editor
class basic_node { class basic_node {
@@ -460,6 +464,9 @@ basic_node::location() {
inline inline
void void
basic_node::visible( bool const Visible ) { basic_node::visible( bool const Visible ) {
if( Visible != m_visible ) {
++node_state_version;
}
m_visible = Visible; m_visible = Visible;
} }