From 494304903fd4eaeb31ffffffa32c5f93fdbea9f0 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sun, 18 Oct 2026 00:47:05 +0200 Subject: [PATCH] Draw the instances from the GPU with multi draws, the model block of each draw read by its number --- ci_shadervalidator/main.cpp | 19 +- docs/opengl46-renderer.md | 17 +- gl/glsl_common.cpp | 45 +++ gl/shader.cpp | 7 +- gl/shader.h | 5 +- gl/ubo.h | 4 + rendering/geometrybank.cpp | 19 + rendering/geometrybank.h | 8 + rendering/opengl46/opengl46geometrybank.cpp | 22 ++ rendering/opengl46/opengl46geometrybank.h | 3 + rendering/opengl46/opengl46instancecull.cpp | 8 +- rendering/opengl46/opengl46instancecull.h | 11 +- rendering/opengl46/opengl46renderer.cpp | 408 +++++++++++++++++--- rendering/opengl46/opengl46renderer.h | 81 +++- shaders/depthprepass.vert | 3 + shaders/vertex.vert | 4 + utilities/Globals.cpp | 6 + utilities/Globals.h | 3 + 18 files changed, 609 insertions(+), 64 deletions(-) diff --git a/ci_shadervalidator/main.cpp b/ci_shadervalidator/main.cpp index 37c70cd0..196b275e 100644 --- a/ci_shadervalidator/main.cpp +++ b/ci_shadervalidator/main.cpp @@ -11,13 +11,13 @@ _global Global; -bool check_shader(std::string filename) +bool check_shader(std::string filename, std::string const &defines = {}) { gl::shader shader_processor; shader_processor.name = filename; gl::glsl_common_setup(); - std::pair source = shader_processor.process_source(filename, "../shaders/"); + std::pair source = shader_processor.process_source(filename, "../shaders/", defines); std::string stage; if (source.first == GL_VERTEX_SHADER) @@ -70,6 +70,21 @@ bool check_shader_allopts(std::string filename) if (!check_shader(filename)) suc = false; + // the variant of the vertex and fragment stages the multi draws of the instances use (model blocks by gl_DrawID) + auto const ext{std::filesystem::path(filename).extension()}; + if (ext == ".vert" || ext == ".frag") + { + std::cout << "done, model draws..." << std::flush; + Global.gfx_shadowmap_enabled = true; + Global.gfx_envmap_enabled = true; + Global.gfx_postfx_motionblur_enabled = true; + Global.gfx_skippipeline = false; + Global.gfx_extraeffects = true; + Global.gfx_shadergamma = true; + if (!check_shader(filename, "#define MODEL_DRAWS 1\n")) + suc = false; + } + std::cout << "done" << std::flush; return suc; diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index c00c9110..b573e982 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -273,6 +273,7 @@ views go as before. | `gfx.gpucull.occlusion yes/no` | `yes` | with `gfx.gpucull yes`: models hidden whole behind what was drawn before them (the cab, the terrain, buildings and other models drawn on their own, vehicles) are left out | | `gfx.gpucull.occlusion.instances yes/no` | `yes` | with `gfx.gpucull.occlusion yes`: the models drawn this way hide one another too (see below) | | `gfx.depthprepass yes/no` | `yes` | with `gfx.gpucull yes`: the depth of the models is drawn first, so the colour is worked out only for what stays in view (dense foliage) | +| `gfx.gpucull.multidraw yes/no` | `yes` | with `gfx.gpucull yes`: parts of models with the same material (and texture variant) in the same model are drawn with one call (multi draw), see below | | `gfx.depthprepass.scene yes/no` | `no` | the depth of the whole opaque scene of the main view (terrain, tracks, buildings, models, vehicles, the cab) is drawn before its colour, see below | With `gfx.gpucull.occlusion.instances yes` the test goes in two phases: the models seen in the previous frame are drawn @@ -286,6 +287,16 @@ is not worked out with the depth prepass on). Models whose material is not one o like) are left out of the depth prepass and drawn as before; with the depth prepass on, they don't hide the others either. All of these can be changed while the simulator runs, in the **Instance culling** debug window. +With `gfx.gpucull.multidraw yes` the draws of the models drawn from the GPU are put in an order in which the parts with the +same material, texture variant and model (the model keeps its geometry in a buffer of its own) follow one another, and each +such run goes in one call (`glMultiDrawElementsIndirect`); the data of every part (its position in the model, colours, +parameters) is read by the shaders from a buffer, by the number of the part in the call. The parts of one model at one level +of detail keep their order (parts of a model lying on one another are drawn as before), only the parts of different copies +and levels of detail are brought together: a trunk seen in two levels of detail, the six sides of a block in the same +material. The depth prepass joins the parts of a model with the same alpha-tested texture, whatever their material. The +picture is the same either way; `no` draws them one by one, as before. Materials whose shader can't be built in this form +(a message in the log) are drawn one by one. + With `gfx.depthprepass.scene yes` the scene is gone through twice: first only its depth is drawn, then its colour, worked out once per pixel for the surface which stays in view. That costs the processor a second walk through the scene (fewer draws than the colour pass: what the depth programs can't draw, like parallax materials and faded parts, is left to the @@ -363,8 +374,8 @@ editor goes through the first `dynamiclights` headlights, the reflections throug main view), `clouds panorama:` (a band of the panorama), `cloud shadows:` and `fog volume:`. - **Instance culling** window: the mode of `gfx.gpucull` and the switches above. With `gfx.gpucull yes` Shift+F12 shows `gpu cull:` (models drawn, in two phases how many of them first, and hidden by the occlusion test, as counted on the - graphics card a few frames before, the levels of detail, the draw calls with those of the depth prepass, and the processor - time of the whole step) and the timings `instance cull:`, `occlusion depth:` (the depth pyramid of the test) and + graphics card a few frames before, the levels of detail, the draws with those of the depth prepass and the calls which + made them — fewer than the draws with `gfx.gpucull.multidraw yes` — and the processor time of the whole step) and the timings `instance cull:`, `occlusion depth:` (the depth pyramid of the test) and `depth prepass:`; in two phases each of them adds in brackets the time of the second phase, of the second pyramid and of the depth of the models drawn first. With `compare` it shows how many models both counted and how many levels of detail differ. @@ -378,6 +389,8 @@ editor goes through the first `dynamiclights` headlights, the reflections throug `model blocks:` row: how many blocks of model data the frame wrote, with `gfx.bufferstreaming yes` also how much of the buffer of the frame they took, how many times a frame had to wait for the graphics card to finish with a buffer before writing to it (`waits`, should stay at 0) and how many times a buffer had to grow (`grown`, a few times after the start). + With `gfx.gpucull yes` and `gfx.gpucull.multidraw yes` the `multi draw blocks:` row shows the same for the data of the + parts of models drawn with multi draws (one block per draw, written once a frame). - `gfx.gldebug yes` turns on the OpenGL debug output (`KHR_debug`); messages go to the log. ## Building diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index e7a27328..2df97c0b 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -22,6 +22,7 @@ void gl::glsl_common_setup() "#define INSTANCE_COUNTER_SSBO_BINDING " + std::to_string(INSTANCE_COUNTER_SSBO_BINDING) + "\n" + "#define INSTANCE_OUTPUT_SSBO_BINDING " + std::to_string(INSTANCE_OUTPUT_SSBO_BINDING) + "\n" + "#define INSTANCE_COMMAND_SSBO_BINDING " + std::to_string(INSTANCE_COMMAND_SSBO_BINDING) + "\n" + + "#define MODEL_DRAW_SSBO_BINDING " + std::to_string(MODEL_DRAW_SSBO_BINDING) + "\n" + "#define INSTANCE_CULL_GROUP_SIZE " + std::to_string(INSTANCE_CULL_GROUP_SIZE) + "\n" + "#define INSTANCE_OFFSETS_GROUP_SIZE " + std::to_string(INSTANCE_OFFSETS_GROUP_SIZE) + "\n" + "#define INSTANCE_COMMAND_GROUP_SIZE " + std::to_string(INSTANCE_COMMAND_GROUP_SIZE) + "\n" + @@ -187,6 +188,49 @@ void gl::glsl_common_setup() }; #endif + #ifdef MODEL_DRAWS + // programs of the multi draws of the instances drawn from the gpu culling: the model block of every draw comes from a + // storage buffer, picked by the number of the draw in the multi draw. the fragment stage gets the number from the vertex + // stage (which writes f_model_draw), and the names of the uniform block below stand for the fields of the draw + struct model_draw_s + { + mat4 modelview; + mat3 modelviewnormal; + vec4 param[MAX_PARAMS]; + + mat4 future; + float opacity; + float emission; + float fog_density; + float alpha_mult; + float shadow_tone; + }; + + layout(std430, binding = MODEL_DRAW_SSBO_BINDING) readonly buffer model_draw_ssbo + { + model_draw_s model_draws[]; + }; + + #if defined(STAGE_VERTEX) + flat out uint f_model_draw; + #define MODEL_DRAW gl_DrawID + #elif defined(STAGE_FRAGMENT) + flat in uint f_model_draw; + #define MODEL_DRAW f_model_draw + #else + #define MODEL_DRAW 0 + #endif + + #define modelview model_draws[MODEL_DRAW].modelview + #define modelviewnormal model_draws[MODEL_DRAW].modelviewnormal + #define param model_draws[MODEL_DRAW].param + #define future model_draws[MODEL_DRAW].future + #define opacity model_draws[MODEL_DRAW].opacity + #define emission model_draws[MODEL_DRAW].emission + #define fog_density model_draws[MODEL_DRAW].fog_density + #define alpha_mult model_draws[MODEL_DRAW].alpha_mult + #define shadow_tone model_draws[MODEL_DRAW].shadow_tone + #else layout (std140) uniform model_ubo { mat4 modelview; @@ -200,6 +244,7 @@ void gl::glsl_common_setup() float alpha_mult; float shadow_tone; }; + #endif layout (std140) uniform scene_ubo { diff --git a/gl/shader.cpp b/gl/shader.cpp index 935f9598..6be32459 100644 --- a/gl/shader.cpp +++ b/gl/shader.cpp @@ -74,7 +74,7 @@ std::unordered_map gl::shader::defaultp { "glossiness", defaultparam_e::glossiness } }; -std::pair gl::shader::process_source(const std::string &filename, const std::string &basedir) +std::pair gl::shader::process_source(const std::string &filename, const std::string &basedir, const std::string &defines) { std::string str; @@ -101,6 +101,7 @@ std::pair gl::shader::process_source(const std::string &fil str += "vec4 FBOUT(vec4 x) { return " + (Global.gfx_shadergamma ? std::string("vec4(pow(x.rgb, vec3(1.0 / 2.2)), x.a)") : std::string("x")) + "; }\n"; // whether the colour written to the window is encoded by the shader (FBOUT) or left to the sRGB framebuffer (dither.glsl) str += std::string("#define OUTPUT_GAMMA ") + (Global.gfx_shadergamma ? "1" : "0") + "\n"; + str += defines; str += read_file(basedir + filename); @@ -231,10 +232,10 @@ void gl::shader::log_error(const std::string &str) const ErrorLog("bad shader: " + name + ": " + str, logtype::shader); } -gl::shader::shader(const std::string &filename) : name(filename) +gl::shader::shader(const std::string &filename, const std::string &defines) : name(filename) { - auto [shadertype, shadersource] = process_source(filename, "shaders/"); + auto [shadertype, shadersource] = process_source(filename, "shaders/", defines); const GLchar *cstr = shadersource.c_str(); diff --git a/gl/shader.h b/gl/shader.h index c92f7b28..a3b4d628 100644 --- a/gl/shader.h +++ b/gl/shader.h @@ -21,7 +21,8 @@ namespace gl #ifdef SHADERVALIDATOR_STANDALONE shader() = default; #endif - explicit shader(const std::string &filename); + // Defines: lines put before the source (after the version), e.g. "#define MODEL_DRAWS 1\n" for a variant of the shader + explicit shader(const std::string &filename, const std::string &defines = {}); ~shader(); enum class components_e @@ -67,7 +68,7 @@ namespace gl #ifndef SHADERVALIDATOR_STANDALONE private: #endif - std::pair process_source(const std::string &filename, const std::string &basedir); + std::pair process_source(const std::string &filename, const std::string &basedir, const std::string &defines = {}); void expand_includes(std::string &str, const std::string &basedir); void parse_texture_entries(std::string &str); diff --git a/gl/ubo.h b/gl/ubo.h index 6e950fb7..e33a353b 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -472,6 +472,10 @@ namespace gl const GLuint INSTANCE_COUNTER_SSBO_BINDING = 13; // header, then count[entries], cursor[entries] and first[entries] const GLuint INSTANCE_OUTPUT_SSBO_BINDING = 14; // modelview matrices of the instances which passed, by entry const GLuint INSTANCE_COMMAND_SSBO_BINDING = 15; // indirect draw commands of the entries (instance_cull_commands.comp) + // the model blocks of the draws of a multi draw of the instances drawn from the gpu culling, one gl::model_ubs per draw + // (std430, the same layout), read by gl_DrawID in programs built with MODEL_DRAWS. the point is the one of the jobs of the + // instance culling, which binds its buffers before every use; the draws bind theirs before every multi draw + const GLuint MODEL_DRAW_SSBO_BINDING = 10; const GLuint INSTANCE_CULL_GROUP_SIZE = 64; const GLuint INSTANCE_OFFSETS_GROUP_SIZE = 1024; const GLuint INSTANCE_COMMAND_GROUP_SIZE = 64; diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index 4b35bf09..551c2b5a 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -353,6 +353,13 @@ geometry_bank::draw_indirect( gfx::geometry_handle const &Geometry, std::intptr_ draw_indirect_( Geometry, Offset, Streams ); } +// draws chunks of the bank with Count commands from Offset in the bound draw indirect buffer, in one call +bool +geometry_bank::draw_indirect_multi( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, std::uint32_t const Count, std::uint32_t const Stride ) { + // template method implementation + return draw_indirect_multi_( Geometry, Offset, Count, Stride ); +} + // frees subclass-specific resources associated with the bank, typically called when the bank wasn't in use for a period of time void geometry_bank::release() { @@ -515,6 +522,18 @@ geometrybank_manager::draw_indirect( gfx::geometry_handle const &Geometry, std:: bankrecord.first->draw_indirect( Geometry, Offset, Streams ); } +// draws chunks of the bank of the specified one with Count commands from Offset in the bound draw indirect buffer, in one call +bool +geometrybank_manager::draw_indirect_multi( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, std::uint32_t const Count, std::uint32_t const Stride ) { + + if( Geometry == null_handle ) { return false; } + + auto &bankrecord = bank( Geometry ); + + bankrecord.second = m_garbagecollector.timestamp(); + return bankrecord.first->draw_indirect_multi( Geometry, Offset, Count, Stride ); +} + // provides direct access to index data of specfied chunk gfx::index_array const & geometrybank_manager::indices( gfx::geometry_handle const &Geometry ) const { diff --git a/rendering/geometrybank.h b/rendering/geometrybank.h index 9ee21019..e245486f 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -138,6 +138,10 @@ public: // draws geometry stored in specified chunk with the command at Offset (bytes) in the bound draw indirect buffer, // a glDrawElementsIndirect one for an indexed chunk, glDrawArraysIndirect otherwise void draw_indirect( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, unsigned int const Streams = basic_streams ); + // draws chunks of the bank with Count commands from Offset (bytes) in the bound draw indirect buffer, Stride bytes apart, + // with a single glMultiDrawElementsIndirect (or glMultiDrawArraysIndirect). the chunks have to be of the type of the + // specified one and indexed alike. returns: false if the bank can't, nothing is drawn then + auto draw_indirect_multi( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, std::uint32_t const Count, std::uint32_t const Stride ) -> bool; // draws geometry stored in supplied list of chunks template auto draw( Iterator_ First, Iterator_ Last, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) ->std::size_t { @@ -214,6 +218,7 @@ private: // indirect_source() and draw_indirect() subclass details. by default the bank can't draw indirectly virtual auto indirect_source_( gfx::geometry_handle const & /*Geometry*/, gfx::indirect_source & /*Source*/ ) -> bool { return false; } virtual void draw_indirect_( gfx::geometry_handle const & /*Geometry*/, std::intptr_t const /*Offset*/, unsigned int const /*Streams*/ ) {} + virtual auto draw_indirect_multi_( gfx::geometry_handle const & /*Geometry*/, std::intptr_t const /*Offset*/, std::uint32_t const /*Count*/, std::uint32_t const /*Stride*/ ) -> bool { return false; } // resource release subclass details virtual void release_() = 0; }; @@ -251,6 +256,9 @@ public: auto indirect_source( gfx::geometry_handle const &Geometry, gfx::indirect_source &Source ) -> bool; // draws geometry stored in specified chunk with the command at Offset (bytes) in the bound draw indirect buffer void draw_indirect( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, unsigned int const Streams = basic_streams ); + // draws chunks of the bank of the specified one, of its type and indexed alike, with Count commands from Offset in the bound + // draw indirect buffer, Stride bytes apart, in one call. returns: false if the bank can't, nothing is drawn then + auto draw_indirect_multi( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, std::uint32_t const Count, std::uint32_t const Stride ) -> bool; template void draw( Iterator_ First, Iterator_ Last, unsigned int const Streams = basic_streams ) { while( First != Last ) { diff --git a/rendering/opengl46/opengl46geometrybank.cpp b/rendering/opengl46/opengl46geometrybank.cpp index c75bb0dc..a709d829 100644 --- a/rendering/opengl46/opengl46geometrybank.cpp +++ b/rendering/opengl46/opengl46geometrybank.cpp @@ -274,6 +274,28 @@ opengl46_vaogeometrybank::draw_indirect_( gfx::geometry_handle const &Geometry, } } +// draw_indirect_multi() subclass details — the commands come from the bound draw indirect buffer, the chunk tells the type +// and whether they're indexed +bool +opengl46_vaogeometrybank::draw_indirect_multi_( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, std::uint32_t const Count, std::uint32_t const Stride ) +{ + setup_buffer(); + + // the counts of the chunks are in the commands, which may be for chunks other than this one + if( ( false == m_vao.has_value() ) || ( Count == 0 ) ) { return true; } + + auto const &chunkrecord = m_chunkrecords.at(Geometry.chunk - 1); + auto const &chunk = gfx::geometry_bank::chunk( Geometry ); + m_vao->bind(); + if( chunkrecord.index_count > 0 ) { + ::glMultiDrawElementsIndirect( chunk.type, GL_UNSIGNED_INT, reinterpret_cast( Offset ), static_cast( Count ), static_cast( Stride ) ); + } + else { + ::glMultiDrawArraysIndirect( chunk.type, reinterpret_cast( Offset ), static_cast( Count ), static_cast( Stride ) ); + } + return true; +} + // draw_batch() subclass details — neighbouring non-indexed chunks of the same type are sent with a single GL call. // The chunks are drawn in the supplied order, same as with a draw_() call for each of them. std::size_t diff --git a/rendering/opengl46/opengl46geometrybank.h b/rendering/opengl46/opengl46geometrybank.h index 2e11fc66..32f78f82 100644 --- a/rendering/opengl46/opengl46geometrybank.h +++ b/rendering/opengl46/opengl46geometrybank.h @@ -65,6 +65,9 @@ private: // draw_indirect() subclass details — issues glDrawElementsIndirect or glDrawArraysIndirect void draw_indirect_( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, unsigned int const Streams ) override; + // draw_indirect_multi() subclass details — issues glMultiDrawElementsIndirect or glMultiDrawArraysIndirect + auto + draw_indirect_multi_( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, std::uint32_t const Count, std::uint32_t const Stride ) -> bool override; // release() subclass details void release_() override; diff --git a/rendering/opengl46/opengl46instancecull.cpp b/rendering/opengl46/opengl46instancecull.cpp index 87033d31..8edb106a 100644 --- a/rendering/opengl46/opengl46instancecull.cpp +++ b/rendering/opengl46/opengl46instancecull.cpp @@ -360,10 +360,12 @@ void opengl46_instance_cull::bind_draw() glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::INSTANCE_SSBO_BINDING, 1, &output); } -void opengl46_instance_cull::draw_result(std::uint32_t const Draws, std::uint32_t const Prepassdraws, double const Cputime) +void opengl46_instance_cull::draw_result(std::uint32_t const Draws, std::uint32_t const Prepassdraws, std::uint32_t const Calls, std::uint32_t const Prepasscalls, double const Cputime) { m_draws = Draws; m_prepassdraws = Prepassdraws; + m_calls = Calls; + m_prepasscalls = Prepasscalls; m_cputime = Cputime; } @@ -452,8 +454,8 @@ std::string opengl46_instance_cull::info() return "\ngpu cull: " + std::to_string(m_last.gputotal) + " instances drawn" + (m_twophase ? " (" + std::to_string(m_last.first) + " of them first, seen in the last frame)" : std::string{}) + ", " + std::to_string(m_last.occluded) + " hidden by occlusion" + (m_occlusion ? (m_twophase ? " (also by one another)" : "") : " (test off)") + ", " + std::to_string(m_last.entries) + " bands; " + - std::to_string(m_draws) + " indirect draws (+" + - std::to_string(m_prepassdraws) + " depth prepass), cpu " + to_string(m_cputime, 3) + " ms"; + std::to_string(m_draws) + " indirect draws in " + std::to_string(m_calls) + " calls (+" + std::to_string(m_prepassdraws) + " depth prepass in " + + std::to_string(m_prepasscalls) + "), cpu " + to_string(m_cputime, 3) + " ms"; } if (m_comparedframes == 0) return "\ngpu cull: no result yet"; diff --git a/rendering/opengl46/opengl46instancecull.h b/rendering/opengl46/opengl46instancecull.h index f51a7489..95e6fdf9 100644 --- a/rendering/opengl46/opengl46instancecull.h +++ b/rendering/opengl46/opengl46instancecull.h @@ -140,13 +140,18 @@ class opengl46_instance_cull } // binds the commands to GL_DRAW_INDIRECT_BUFFER and the output in place of the instance buffer of the frame void bind_draw(); - // counts of the last result which came back from the gpu, with the draws of the pass, for the debug panel - void draw_result(std::uint32_t const Draws, std::uint32_t const Prepassdraws, double const Cputime); + // counts of the last result which came back from the gpu, with the draws of the pass and the gl calls which made them (fewer + // with multi draws), for the debug panel + void draw_result(std::uint32_t const Draws, std::uint32_t const Prepassdraws, std::uint32_t const Calls, std::uint32_t const Prepasscalls, double const Cputime); // what draw_result() was given last std::uint32_t prepass_draws() const { return m_prepassdraws; } + std::uint32_t prepass_calls() const + { + return m_prepasscalls; + } double cpu_time() const { return m_cputime; @@ -243,6 +248,8 @@ class opengl46_instance_cull // draws of the last pass drawn from the gpu std::uint32_t m_draws{0}; std::uint32_t m_prepassdraws{0}; + std::uint32_t m_calls{0}; + std::uint32_t m_prepasscalls{0}; std::uint32_t m_commandcount{0}; double m_cputime{0.0}; bool m_occlusion{false}; // the last dispatch tested occlusion diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 30fd7481..a67c7794 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -191,6 +191,33 @@ bool opengl46_renderer::Init(GLFWwindow *Window) static_cast(16384 * sizeof(glm::mat4)), static_cast(sizeof(glm::mat4)), Global.gfx_bufferstreaming, &identity, static_cast(sizeof(identity))); } WriteLog(std::string("Model data streaming: ") + (model_ubo->streamed() ? "persistently mapped buffers" : "glBufferSubData")); + // multi draws of the instances drawn from the gpu culling (Render_Instances_Indirect()): the model blocks of their draws in a + // storage buffer bound at whole blocks, and the variants of the programs which read them there. without them the draws are + // made one by one, as before + try + { + GLint alignment{256}; + glGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &alignment); + if ((alignment <= 0) || (sizeof(gl::model_ubs) % static_cast(alignment) != 0)) + throw gl::shader_exception("storage buffer offset alignment of " + std::to_string(alignment) + " bytes"); + std::string const defines{"#define MODEL_DRAWS 1\n"}; + m_vertex_multidraw_shader = std::make_unique("vertex.vert", defines); + gl::shader prepassvertex("depthprepass.vert", defines); + gl::shader prepassfragment("depthprepass.frag", defines); + gl::shader prepassalphafragment("depthprepass_alpha.frag", defines); + m_prepass_multidraw_shader = std::make_unique(std::vector>{prepassvertex, prepassfragment}); + m_prepass_alpha_multidraw_shader = std::make_unique(std::vector>{prepassvertex, prepassalphafragment}); + m_drawblockstream = std::make_unique( + static_cast(4096 * sizeof(gl::model_ubs)), static_cast(sizeof(gl::model_ubs)), Global.gfx_bufferstreaming); + } + catch (gl::shader_exception const &e) + { + ErrorLog("multi draws of the instances not available: " + std::string(e.what())); + m_vertex_multidraw_shader.reset(); + m_prepass_multidraw_shader.reset(); + m_prepass_alpha_multidraw_shader.reset(); + m_drawblockstream.reset(); + } try { @@ -661,6 +688,8 @@ bool opengl46_renderer::Render() m_instancestream->next_frame(); Bind_Instance_Stream(); model_ubo->next_frame(); + if (m_drawblockstream) + m_drawblockstream->next_frame(); m_renderpass.draw_mode = rendermode::none; // force setup anew m_renderpass.draw_stats = debug_stats(); @@ -797,6 +826,11 @@ void opengl46_renderer::SwapBuffers() + std::to_string( model_ubo->stream()->waits() + m_instancestream->waits() ) + ", grown " + std::to_string( model_ubo->stream()->grows() + m_instancestream->grows() ) + ")\n" : ", glBufferSubData\n" ) + + ( ( m_drawblockstream && ( Global.gfx_gpucull == 1 ) && Global.gfx_gpucull_multidraw ) ? + " multi draw blocks: " + std::to_string( m_drawblockstream->used() / static_cast( sizeof( gl::model_ubs ) ) ) + ", " + + std::to_string( m_drawblockstream->size() / 1024 ) + " KiB (waits " + std::to_string( m_drawblockstream->waits() ) + ", grown " + + std::to_string( m_drawblockstream->grows() ) + ")\n" : + std::string{} ) + " inst-pool:" + std::to_string( TAnimModel::s_instanceable_total ) + "/" + std::to_string( TAnimModel::s_classified_total ) + " rej(noModel/lights/anim/animSubM): " @@ -914,6 +948,10 @@ void opengl46_renderer::draw_debug_ui() if (Global.gfx_gpucull_occlusion) ImGui::Checkbox("draw the instances seen in the last frame first, to hide the others (gfx.gpucull.occlusion.instances)", &Global.gfx_gpucull_occlusion_instances); ImGui::Checkbox("draw the depth of the instances before their colour (gfx.depthprepass)", &Global.gfx_depthprepass); + if (m_drawblockstream) + ImGui::Checkbox("draw the instances of the same material and model with one multi draw (gfx.gpucull.multidraw)", &Global.gfx_gpucull_multidraw); + else + ImGui::TextUnformatted("(multi draws not available, see the log)"); } ImGui::Checkbox("draw the depth of the whole scene before its colour (gfx.depthprepass.scene)", &Global.gfx_depthprepass_scene); if (Global.gfx_contactshadows > 0) @@ -2474,6 +2512,9 @@ std::shared_ptr opengl46_renderer::Fetch_Shader(const std::string & gl::shader fragment("mat_" + name + ".frag"); gl::program *program = new gl::program({fragment, *m_vertex_shader.get()}); m_shaders.insert({name, std::shared_ptr(program)}); + // the variant for the multi draws of the instances, made now rather than when the instances first come into view + if ((Global.gfx_gpucull == 1) && Global.gfx_gpucull_multidraw) + Multidraw_Program(program); } return m_shaders[name]; @@ -2648,7 +2689,14 @@ void opengl46_renderer::Bind_Material( material_handle const Material, TSubModel if (lastdiff) glBindTextures(0, lastdiff, &opengl_texture::units[0]); - material.shader->bind(); + auto *program{material.shader.get()}; + if (m_bindmultidraw) + { + // the replay of the instances drawn from the gpu culling with multi draws (Render_Instances_Indirect_Colour()) + if (auto *multidraw{Multidraw_Program(program)}) + program = multidraw; + } + program->bind(); } else if (Material != m_invalid_material) Bind_Material(m_invalid_material); @@ -3224,18 +3272,21 @@ void opengl46_renderer::Render(cell_sequence::iterator First, cell_sequence::ite if( m_indirectcolourpending ) { // the instances culled and recorded by the depth pass of the whole scene, the buckets weren't queued for the cpu m_indirectcolourpending = false; + indirect_counts colourcounts; if( false == m_indirectdraws.empty() ) { auto const cpustart { std::chrono::steady_clock::now() }; m_instancecull.bind_draw(); - Render_Instances_Indirect_Colour( m_indirectcolourset ); + colourcounts = Render_Instances_Indirect_Colour( m_indirectcolourset ); ::glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0 ); Bind_Instance_Stream(); m_instancecull.draw_result( - static_cast( m_indirectdraws.size() ), + colourcounts.draws, m_instancecull.prepass_draws(), + colourcounts.calls, + m_instancecull.prepass_calls(), m_instancecull.cpu_time() + std::chrono::duration( std::chrono::steady_clock::now() - cpustart ).count() ); } - m_renderpass.draw_stats.instanced_drawcalls += static_cast( m_indirectdraws.size() ); + m_renderpass.draw_stats.instanced_drawcalls += static_cast( colourcounts.calls ); m_renderpass.draw_stats.instances += static_cast( m_instancecull.last_count() ); m_renderpass.draw_stats.models += static_cast( m_instancecull.last_count() ); } @@ -3892,6 +3943,14 @@ void opengl46_renderer::Render_Instances_Indirect() } m_indirectrecording = false; + // with multi draws the draws with the same state follow one another, and their model blocks are in a buffer the programs + // of the multi draws read them from. the order goes into the commands, so it comes first + m_indirectmultidraw = ( Global.gfx_gpucull_multidraw && ( m_drawblockstream != nullptr ) ); + if( m_indirectmultidraw && ( false == m_indirectdraws.empty() ) ) { + Order_Indirect_Draws(); + Upload_Indirect_Blocks(); + } + m_instancecull.fill_commands( m_indirectcommands ); using command_set = opengl46_instance_cull::command_set; auto const twophase { m_instancecull.two_phase() }; @@ -3901,19 +3960,18 @@ void opengl46_renderer::Render_Instances_Indirect() // view. the depth goes a little farther than the surface, so the colour pass, which tests and writes its depth as usual, // is sure to pass where it's the same auto const depthfirst { m_scenedepthpass || ( Global.gfx_depthprepass && ( false == Global.bWireFrame ) ) }; - std::uint32_t prepassdraws { 0 }; - std::uint32_t colourdraws { 0 }; + indirect_counts prepasscounts; + indirect_counts colourcounts; auto colourset { command_set::first }; if( twophase ) { if( draws ) { // the instances seen in the previous frame, then the pyramid again, with them in it m_instancecull.bind_draw(); if( depthfirst ) { - prepassdraws += Render_Instances_Indirect_Prepass( command_set::first, m_prepassfirsttimer ); + prepasscounts += Render_Instances_Indirect_Prepass( command_set::first, m_prepassfirsttimer ); } else { - Render_Instances_Indirect_Colour( command_set::first ); - colourdraws += static_cast( m_indirectdraws.size() ); + colourcounts += Render_Instances_Indirect_Colour( command_set::first ); } m_hiz.build( *viewport->msaa_fb, viewport->width, viewport->height, true ); occlusion.pyramid = m_hiz.texture(); @@ -3925,11 +3983,10 @@ void opengl46_renderer::Render_Instances_Indirect() if( draws ) { m_instancecull.bind_draw(); if( depthfirst ) { - prepassdraws += Render_Instances_Indirect_Prepass( ( twophase ? command_set::second : command_set::first ), m_prepasstimer ); + prepasscounts += Render_Instances_Indirect_Prepass( ( twophase ? command_set::second : command_set::first ), m_prepasstimer ); } if( false == m_scenedepthpass ) { - Render_Instances_Indirect_Colour( colourset ); - colourdraws += static_cast( m_indirectdraws.size() ); + colourcounts += Render_Instances_Indirect_Colour( colourset ); } ::glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0 ); Bind_Instance_Stream(); @@ -3938,23 +3995,25 @@ void opengl46_renderer::Render_Instances_Indirect() // the colour pass takes the draws from here, and counts them m_indirectcolourpending = true; m_indirectcolourset = colourset; - m_renderpass.draw_stats.drawcalls += static_cast( prepassdraws ); - colourdraws = static_cast( m_indirectdraws.size() ); + m_renderpass.draw_stats.drawcalls += static_cast( prepasscounts.calls ); + colourcounts.draws = static_cast( m_indirectdraws.size() ); } else { - m_renderpass.draw_stats.instanced_drawcalls += static_cast( colourdraws ); - m_renderpass.draw_stats.drawcalls += static_cast( prepassdraws ); + m_renderpass.draw_stats.instanced_drawcalls += static_cast( colourcounts.calls ); + m_renderpass.draw_stats.drawcalls += static_cast( prepasscounts.calls ); // the count of the frame the gpu reported last m_renderpass.draw_stats.instances += static_cast( m_instancecull.last_count() ); m_renderpass.draw_stats.models += static_cast( m_instancecull.last_count() ); } m_instancecull.draw_result( - colourdraws, - prepassdraws, + colourcounts.draws, + prepasscounts.draws, + colourcounts.calls, + prepasscounts.calls, std::chrono::duration( std::chrono::steady_clock::now() - cpustart ).count() ); } -std::uint32_t opengl46_renderer::Render_Instances_Indirect_Prepass( opengl46_instance_cull::command_set const Set, gl::gpu_timer &Timer ) +opengl46_renderer::indirect_counts opengl46_renderer::Render_Instances_Indirect_Prepass( opengl46_instance_cull::command_set const Set, gl::gpu_timer &Timer ) { if( m_scenedepthpass ) { return Render_Instances_Indirect_Depth( Set ); @@ -3964,34 +4023,80 @@ std::uint32_t opengl46_renderer::Render_Instances_Indirect_Prepass( opengl46_ins ::glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE ); ::glEnable( GL_POLYGON_OFFSET_FILL ); ::glPolygonOffset( -1.f, -1.f ); - auto const draws { Render_Instances_Indirect_Depth( Set ) }; + auto const counts { Render_Instances_Indirect_Depth( Set ) }; ::glDisable( GL_POLYGON_OFFSET_FILL ); ::glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); Timer.end(); - return draws; + return counts; } -std::uint32_t opengl46_renderer::Render_Instances_Indirect_Depth( opengl46_instance_cull::command_set const Set ) +opengl46_renderer::indirect_counts opengl46_renderer::Render_Instances_Indirect_Depth( opengl46_instance_cull::command_set const Set ) { - std::uint32_t draws { 0 }; + indirect_counts counts; + auto const count { m_indirectdraws.size() }; + // whether the prepass of the class draws the draw, and with which diffuse texture (class 2) + auto const prepass_texture = [ this ]( std::size_t const Index, int const Prepassclass, texture_handle &Texture ) -> bool { + auto const &draw { m_indirectdraws[ Index ] }; + if( ( draw.blending ) || ( draw.ubs.opacity < 0.f ) ) { return false; } + if( ( draw.type != GL_TRIANGLES ) && ( draw.type != GL_TRIANGLE_STRIP ) && ( draw.type != GL_TRIANGLE_FAN ) ) { return false; } + if( Prepass_Class( draw.material ) != Prepassclass ) { return false; } + Texture = null_handle; + if( Prepassclass == 2 ) { + // the diffuse texture of the colour pass, the same choice as Bind_Material() makes + auto const &material { m_materials.material( draw.material ) }; + auto const &textures { + material.texture_variants.empty() ? + material.textures : + material.GetTextures( TSubModel::VariantId( draw.submodel ) ) }; + if( textures[ 0 ] == null_handle ) { return false; } + Texture = textures[ 0 ]; + } + return true; + }; + auto lastdrawn { count }; // the draws without the alpha test first, then the ones with it, one program each for( auto prepassclass = 1; prepassclass <= 2; ++prepassclass ) { auto &program { prepassclass == 1 ? *m_prepass_shader : *m_prepass_alpha_shader }; + auto *multidrawprogram { + m_indirectmultidraw ? + ( prepassclass == 1 ? m_prepass_multidraw_shader.get() : m_prepass_alpha_multidraw_shader.get() ) : + nullptr }; auto bound { false }; - for( std::size_t index = 0; index < m_indirectdraws.size(); ++index ) { - auto const &draw { m_indirectdraws[ index ] }; - if( ( draw.blending ) || ( draw.ubs.opacity < 0.f ) ) { continue; } - if( ( draw.type != GL_TRIANGLES ) && ( draw.type != GL_TRIANGLE_STRIP ) && ( draw.type != GL_TRIANGLE_FAN ) ) { continue; } - if( Prepass_Class( draw.material ) != prepassclass ) { continue; } + for( std::size_t index = 0; index < count; ) { + texture_handle texture; + if( false == prepass_texture( index, prepassclass, texture ) ) { + ++index; + continue; + } if( prepassclass == 2 ) { - // the diffuse texture of the colour pass, the same choice as Bind_Material() makes - auto const &material { m_materials.material( draw.material ) }; - auto const &textures { - material.texture_variants.empty() ? - material.textures : - material.GetTextures( TSubModel::VariantId( draw.submodel ) ) }; - if( textures[ 0 ] == null_handle ) { continue; } - m_textures.bind( 0, textures[ 0 ] ); + m_textures.bind( 0, texture ); + } + auto const &draw { m_indirectdraws[ index ] }; + if( multidrawprogram != nullptr ) { + // the draws which follow in the same bank and of the same primitive, with the same texture for the alpha test. + // the material doesn't matter to the depth + auto last { index + 1 }; + texture_handle othertexture; + while( ( last < count ) + && ( m_indirectdraws[ last ].geometry.bank == draw.geometry.bank ) + && ( m_indirectdraws[ last ].type == draw.type ) + && ( m_indirectcommands[ last ].indexed == m_indirectcommands[ index ].indexed ) + && ( prepass_texture( last, prepassclass, othertexture ) ) + && ( othertexture == texture ) ) { + ++last; + } + if( false == bound ) { + multidrawprogram->bind(); + bound = true; + } + auto const drawcount { static_cast( last - index ) }; + Bind_Indirect_Blocks( index, drawcount ); + m_geometry.draw_indirect_multi( draw.geometry, m_instancecull.draw_command_offset( Set, index ), drawcount, sizeof( gl::instance_draw_command ) ); + counts.draws += drawcount; + ++counts.calls; + lastdrawn = last - 1; + index = last; + continue; } if( false == bound ) { program.bind(); @@ -4000,24 +4105,239 @@ std::uint32_t opengl46_renderer::Render_Instances_Indirect_Depth( opengl46_insta model_ubs = draw.ubs; model_ubo->update( model_ubs ); m_geometry.draw_indirect( draw.geometry, m_instancecull.draw_command_offset( Set, index ) ); - ++draws; + ++counts.draws; + ++counts.calls; + ++index; } } - return draws; + if( lastdrawn < count ) { + // the model block the draws made one by one leave behind + model_ubs = m_indirectdraws[ lastdrawn ].ubs; + model_ubo->update( model_ubs ); + } + return counts; } -void opengl46_renderer::Render_Instances_Indirect_Colour( opengl46_instance_cull::command_set const Set ) +opengl46_renderer::indirect_counts opengl46_renderer::Render_Instances_Indirect_Colour( opengl46_instance_cull::command_set const Set ) { - for( std::size_t index = 0; index < m_indirectdraws.size(); ++index ) { + indirect_counts counts; + auto const count { m_indirectdraws.size() }; + for( std::size_t index = 0; index < count; ) { auto const &draw { m_indirectdraws[ index ] }; + auto *multidrawprogram { m_indirectmultidraw ? Multidraw_Program( Indirect_Program( draw.material ) ) : nullptr }; if( draw.blending ) { setup_drawing( true ); } - Bind_Material( draw.material, draw.submodel ); - model_ubs = draw.ubs; - model_ubo->update( model_ubs ); - m_geometry.draw_indirect( draw.geometry, m_instancecull.draw_command_offset( Set, index ) ); + if( multidrawprogram != nullptr ) { + // the draws which follow with the same state, in one call + auto last { index + 1 }; + while( ( last < count ) && Indirect_Mergeable( index, last ) ) { + ++last; + } + m_bindmultidraw = true; + Bind_Material( draw.material, draw.submodel ); + m_bindmultidraw = false; + auto const drawcount { static_cast( last - index ) }; + Bind_Indirect_Blocks( index, drawcount ); + m_geometry.draw_indirect_multi( draw.geometry, m_instancecull.draw_command_offset( Set, index ), drawcount, sizeof( gl::instance_draw_command ) ); + counts.draws += drawcount; + index = last; + } + else { + Bind_Material( draw.material, draw.submodel ); + model_ubs = draw.ubs; + model_ubo->update( model_ubs ); + m_geometry.draw_indirect( draw.geometry, m_instancecull.draw_command_offset( Set, index ) ); + ++counts.draws; + ++index; + } + ++counts.calls; if( draw.blending ) { setup_drawing( false ); } } + if( m_indirectmultidraw && ( count > 0 ) ) { + // the model block the draws made one by one leave behind + model_ubs = m_indirectdraws.back().ubs; + model_ubo->update( model_ubs ); + } model_ubs.emission = 0.0f; + return counts; +} + +// the order of the recorded draws for multi draws. draws go in one multi draw when they follow one another with the same +// state (material and its texture variant, geometry bank, primitive, blending; Indirect_Mergeable()). the draws of one entry, +// the walk of one submodel tree in one lod band, keep their order: submodels of a model may lie on one another and rely on +// the later one being drawn over the earlier one. the draws of different entries are drawn for different instances, which +// the gpu draws in no particular order anyway, so they're free to move. the entries are queues of draws; the draws at the +// heads of the queues which have the state taken next all go, each queue as far as its draws keep that state, then the +// queues wait for the state of their next draw. the states go in the order in which the queues first reach them +void opengl46_renderer::Order_Indirect_Draws() +{ + auto const count { m_indirectdraws.size() }; + if( count < 2 ) { return; } + + // a number for each state the draws have, by sorting the states (the buffers are kept between frames) + m_indirectstatekeys.clear(); + for( std::size_t index = 0; index < count; ++index ) { + auto const &draw { m_indirectdraws[ index ] }; + auto variant { 0u }; + if( draw.material != null_handle ) { + auto const &material { m_materials.material( draw.material ) }; + if( false == material.texture_variants.empty() ) { + variant = TSubModel::VariantId( draw.submodel ) % static_cast( material.texture_variants.size() ); + } + } + m_indirectstatekeys.push_back( { draw.material, variant, draw.geometry.bank, draw.type, m_indirectcommands[ index ].indexed, draw.blending, static_cast( index ) } ); + } + std::sort( + std::begin( m_indirectstatekeys ), std::end( m_indirectstatekeys ), + []( indirect_state_key const &Left, indirect_state_key const &Right ) { + return Left.tie() < Right.tie(); } ); + m_indirectdrawstates.resize( count ); + std::uint32_t statecount { 0 }; + for( std::size_t index = 0; index < count; ++index ) { + if( ( index > 0 ) && ( m_indirectstatekeys[ index ].tie() != m_indirectstatekeys[ index - 1 ].tie() ) ) { + ++statecount; + } + m_indirectdrawstates[ m_indirectstatekeys[ index ].draw ] = statecount; + } + ++statecount; + if( statecount == count ) { + // no two draws share a state, nothing would go together + return; + } + + auto constexpr none { std::numeric_limits::max() }; + m_indirectstates.assign( statecount, indirect_state { none, none } ); + m_indirectstateorder.clear(); + m_indirectqueues.clear(); + // the queue waits for the state of its next draw, behind the queues waiting for it already + auto const enqueue = [ this, none ]( std::uint32_t const Queue ) { + auto &queue { m_indirectqueues[ Queue ] }; + auto &state { m_indirectstates[ m_indirectdrawstates[ queue.next ] ] }; + queue.link = none; + if( state.head == none ) { + state.head = Queue; + m_indirectstateorder.push_back( m_indirectdrawstates[ queue.next ] ); + } + else { + m_indirectqueues[ state.tail ].link = Queue; + } + state.tail = Queue; + }; + for( std::size_t index = 0; index < count; ) { + auto end { index + 1 }; + while( ( end < count ) && ( m_indirectcommands[ end ].entry == m_indirectcommands[ index ].entry ) ) { ++end; } + m_indirectqueues.push_back( { static_cast( index ), static_cast( end ), none } ); + index = end; + } + for( std::uint32_t queue = 0; queue < m_indirectqueues.size(); ++queue ) { + enqueue( queue ); + } + m_indirectorder.clear(); + for( std::size_t taken = 0; taken < m_indirectstateorder.size(); ++taken ) { + auto const stateindex { m_indirectstateorder[ taken ] }; + auto queueindex { m_indirectstates[ stateindex ].head }; + m_indirectstates[ stateindex ] = { none, none }; + while( queueindex != none ) { + auto &queue { m_indirectqueues[ queueindex ] }; + auto const nextqueue { queue.link }; + while( ( queue.next < queue.end ) && ( m_indirectdrawstates[ queue.next ] == stateindex ) ) { + m_indirectorder.push_back( queue.next ); + ++queue.next; + } + if( queue.next < queue.end ) { + enqueue( queueindex ); + } + queueindex = nextqueue; + } + } + + m_indirectdrawsordered.clear(); + m_indirectcommandsordered.clear(); + for( auto const index : m_indirectorder ) { + m_indirectdrawsordered.push_back( m_indirectdraws[ index ] ); + m_indirectcommandsordered.push_back( m_indirectcommands[ index ] ); + } + m_indirectdraws.swap( m_indirectdrawsordered ); + m_indirectcommands.swap( m_indirectcommandsordered ); +} + +void opengl46_renderer::Upload_Indirect_Blocks() +{ + m_indirectblocks.clear(); + for( auto const &draw : m_indirectdraws ) { + m_indirectblocks.push_back( draw.ubs ); + } + m_indirectblockoffset = m_drawblockstream->push( m_indirectblocks.data(), static_cast( m_indirectblocks.size() * sizeof( gl::model_ubs ) ) ); + m_indirectblockbuffer = m_drawblockstream->buffer(); + m_indirectblockgeneration = m_drawblockstream->generation(); +} + +void opengl46_renderer::Bind_Indirect_Blocks( std::size_t const First, std::size_t const Count ) +{ + if( m_drawblockstream->generation() != m_indirectblockgeneration ) { + // the buffer the blocks went to was replaced since (it shouldn't, the draws are recorded once a frame) + Upload_Indirect_Blocks(); + } + GLuint const buffer { m_indirectblockbuffer }; + GLintptr const offset { m_indirectblockoffset + static_cast( First * sizeof( gl::model_ubs ) ) }; + GLsizeiptr const size { static_cast( Count * sizeof( gl::model_ubs ) ) }; + // the plural form leaves the general GL_SHADER_STORAGE_BUFFER binding alone, as gl::ssbo does + ::glBindBuffersRange( GL_SHADER_STORAGE_BUFFER, gl::MODEL_DRAW_SSBO_BINDING, 1, &buffer, &offset, &size ); +} + +bool opengl46_renderer::Indirect_Mergeable( std::size_t const First, std::size_t const Other ) const +{ + auto const &first { m_indirectdraws[ First ] }; + auto const &other { m_indirectdraws[ Other ] }; + if( ( first.material != other.material ) + || ( first.blending != other.blending ) + || ( first.type != other.type ) + || ( first.geometry.bank != other.geometry.bank ) + || ( m_indirectcommands[ First ].indexed != m_indirectcommands[ Other ].indexed ) ) { + return false; + } + if( first.material == null_handle ) { return true; } + // the texture set Bind_Material() binds + auto const &material { m_materials.material( first.material ) }; + if( material.texture_variants.empty() ) { return true; } + return ( &material.GetTextures( TSubModel::VariantId( first.submodel ) ) == &material.GetTextures( TSubModel::VariantId( other.submodel ) ) ); +} + +gl::program const *opengl46_renderer::Indirect_Program( material_handle Material ) const +{ + // the same choice as Bind_Material() makes: what can't be drawn is drawn with the material of failures, if that one can + if( Material == null_handle ) { Material = m_invalid_material; } + if( Material == null_handle ) { return nullptr; } + auto const *material { &m_materials.material( Material ) }; + if( false == material->is_good ) { + if( Material == m_invalid_material ) { return nullptr; } + material = &m_materials.material( m_invalid_material ); + if( false == material->is_good ) { return nullptr; } + } + return material->shader.get(); +} + +gl::program *opengl46_renderer::Multidraw_Program( gl::program const *Program ) +{ + if( ( Program == nullptr ) || ( m_vertex_multidraw_shader == nullptr ) || ( m_drawblockstream == nullptr ) ) { return nullptr; } + auto const lookup { m_multidrawprograms.find( Program ) }; + if( lookup != m_multidrawprograms.end() ) { return lookup->second.get(); } + + // the fragment shader of the program by its name, mat_.frag (Fetch_Shader()) + std::unique_ptr variant; + for( auto const &shader : m_shaders ) { + if( shader.second.get() != Program ) { continue; } + try { + gl::shader fragment( "mat_" + shader.first + ".frag", "#define MODEL_DRAWS 1\n" ); + variant = std::make_unique( std::vector>{ fragment, *m_vertex_multidraw_shader } ); + } + catch( gl::shader_exception const &e ) { + ErrorLog( "material " + shader.first + " drawn without multi draws: " + std::string( e.what() ) ); + } + break; + } + auto *result { variant.get() }; + m_multidrawprograms.emplace( Program, std::move( variant ) ); + return result; } // depth of the whole opaque scene of the colour pass, before its colour. the same walk as the colour pass makes (the cab drawn diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index f6044244..78d0c9af 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -355,13 +355,37 @@ class opengl46_renderer : public gfx_renderer { // (Global.gfx_gpucull_occlusion_instances) the instances seen in the previous frame are drawn first (their depth, or // their colour without the prepass), the pyramid is made again and the other instances are culled against it void Render_Instances_Indirect(); + // draws of the instances made, and the gl calls which made them + struct indirect_counts { + std::uint32_t draws { 0 }; + std::uint32_t calls { 0 }; + indirect_counts &operator+=( indirect_counts const &Other ) { + draws += Other.draws; + calls += Other.calls; + return *this; } + }; // the recorded draws of the instances, with the commands of the given set: their depth with the programs of the depth - // prepass, with the state of the prepass already set (returns the number of draws), the same with the state set for it - // and the time measured outside the depth pass of the whole scene, and their colour. the command buffer and the - // instances are bound by the caller - std::uint32_t Render_Instances_Indirect_Depth( opengl46_instance_cull::command_set const Set ); - std::uint32_t Render_Instances_Indirect_Prepass( opengl46_instance_cull::command_set const Set, gl::gpu_timer &Timer ); - void Render_Instances_Indirect_Colour( opengl46_instance_cull::command_set const Set ); + // prepass, with the state of the prepass already set, the same with the state set for it and the time measured outside + // the depth pass of the whole scene, and their colour. the command buffer and the instances are bound by the caller. + // with multi draws (m_indirectmultidraw) the draws which follow one another with the same state go in one call + indirect_counts Render_Instances_Indirect_Depth( opengl46_instance_cull::command_set const Set ); + indirect_counts Render_Instances_Indirect_Prepass( opengl46_instance_cull::command_set const Set, gl::gpu_timer &Timer ); + indirect_counts Render_Instances_Indirect_Colour( opengl46_instance_cull::command_set const Set ); + // multi draws of the recorded draws (Global.gfx_gpucull_multidraw): puts the draws in an order in which the ones with the + // same state follow one another, the draws of one entry kept in the order of the walk, then places their model blocks in + // the buffer of the frame (m_drawblockstream), in the same order + void Order_Indirect_Draws(); + void Upload_Indirect_Blocks(); + // binds the model blocks of Count recorded draws from First for the programs of the multi draws (MODEL_DRAW_SSBO_BINDING) + void Bind_Indirect_Blocks( std::size_t const First, std::size_t const Count ); + // whether two recorded draws can go in one multi draw of the colour pass: the same material and texture variant, geometry + // bank, primitive and blending + bool Indirect_Mergeable( std::size_t const First, std::size_t const Other ) const; + // the program Bind_Material() binds for the material, nullptr when it binds none + gl::program const *Indirect_Program( material_handle Material ) const; + // the variant of a material program for multi draws (built with MODEL_DRAWS), made the first time it's asked for. + // nullptr if there's none (a program not fetched by Fetch_Shader(), or one which didn't compile) + gl::program *Multidraw_Program( gl::program const *Program ); // how the depth prepass treats the material: 0 left out, 1 drawn without alpha test, 2 with the alpha test of the diffuse texture int Prepass_Class( material_handle const Material ); // depth of the whole opaque scene of the colour pass, before its colour (Global.gfx_depthprepass_scene): the colour pass @@ -548,6 +572,51 @@ class opengl46_renderer : public gfx_renderer { }; std::vector m_indirectdraws; std::vector m_indirectcommands; // in the order of m_indirectdraws + // multi draws of the recorded draws: whether they were recorded for them (ordered, their model blocks uploaded), and where + // the blocks are + bool m_indirectmultidraw { false }; + GLuint m_indirectblockbuffer { 0 }; + GLintptr m_indirectblockoffset { 0 }; + std::uint64_t m_indirectblockgeneration { 0 }; + // working space of Order_Indirect_Draws() and Upload_Indirect_Blocks(), kept between frames + struct indirect_queue { + std::uint32_t next; // draw + std::uint32_t end; + std::uint32_t link; // next queue waiting for the same state + }; + struct indirect_state { + std::uint32_t head; + std::uint32_t tail; + }; + // what a multi draw has in common, and the draw which has it + struct indirect_state_key { + material_handle material; + std::uint32_t variant; // of the textures of the material + std::uint32_t bank; + unsigned int type; + std::uint32_t indexed; + bool blending; + std::uint32_t draw; + auto tie() const { + return std::tie( material, variant, bank, type, indexed, blending ); } + }; + std::vector m_indirectstatekeys; + std::vector m_indirectdrawstates; + std::vector m_indirectqueues; + std::vector m_indirectstates; + std::vector m_indirectstateorder; + std::vector m_indirectorder; + std::vector m_indirectdrawsordered; + std::vector m_indirectcommandsordered; + std::vector m_indirectblocks; + // the model blocks of the multi draws, one buffer for each of the last frames like model_ubo (nullptr: no multi draws) + std::unique_ptr m_drawblockstream; + // programs of the multi draws: the vertex shader of the materials, variants of the material programs, of the depth prepass + std::unique_ptr m_vertex_multidraw_shader; + std::unordered_map> m_multidrawprograms; + std::unique_ptr m_prepass_multidraw_shader; + std::unique_ptr m_prepass_alpha_multidraw_shader; + bool m_bindmultidraw { false }; // Bind_Material() binds the variant of the material program for multi draws bool m_indirectrecording { false }; // draw() and Bind_Material() record instead of drawing std::uint32_t m_indirectentry { 0 }; // entry (group and lod band) recorded material_handle m_indirectmaterial { null_handle }; // what the last Bind_Material() bound while recording diff --git a/shaders/depthprepass.vert b/shaders/depthprepass.vert index e3975217..e6a7ed6c 100644 --- a/shaders/depthprepass.vert +++ b/shaders/depthprepass.vert @@ -17,4 +17,7 @@ void main() vec4 position = mv * vec4(v_vert, 1.0); gl_Position = projection * position; f_coord = v_coord; +#ifdef MODEL_DRAWS + f_model_draw = uint(gl_DrawID); +#endif } diff --git a/shaders/vertex.vert b/shaders/vertex.vert index 7d5c8bfc..b8c631e3 100644 --- a/shaders/vertex.vert +++ b/shaders/vertex.vert @@ -48,4 +48,8 @@ void main() // TangentViewPos = TBN * vec3(0.0, 0.0, 0.0); TangentFragPos = TBN * f_pos.xyz; UserData = v_userdata; +#ifdef MODEL_DRAWS + // which model block of the multi draw the fragments read (glsl_common) + f_model_draw = uint(gl_DrawID); +#endif } diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index b092363e..b73630a5 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1449,6 +1449,11 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser.getTokens(1); Parser >> gfx_depthprepass; } + else if (Token == "gfx.gpucull.multidraw") + { + Parser.getTokens(1); + Parser >> gfx_gpucull_multidraw; + } else if (Token == "gfx.depthprepass.scene") { Parser.getTokens(1); @@ -1944,6 +1949,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.gpucull.occlusion", gfx_gpucull_occlusion ); export_as_text( Output, "gfx.gpucull.occlusion.instances", gfx_gpucull_occlusion_instances ); export_as_text( Output, "gfx.depthprepass", gfx_depthprepass ); + export_as_text( Output, "gfx.gpucull.multidraw", gfx_gpucull_multidraw ); export_as_text( Output, "gfx.depthprepass.scene", gfx_depthprepass_scene ); export_as_text( Output, "gfx.bufferstreaming", gfx_bufferstreaming ); export_as_text( Output, "gfx.postfx.motionblur.enabled", gfx_postfx_motionblur_enabled ); diff --git a/utilities/Globals.h b/utilities/Globals.h index d2c5c73b..c55d00cd 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -291,6 +291,9 @@ struct global_settings { bool gfx_gpucull_occlusion = true; // with gfx_gpucull 1: instances hidden behind the depth drawn before them are left out bool gfx_gpucull_occlusion_instances = true; // with gfx_gpucull_occlusion: in two phases, so that instances hide one another too bool gfx_depthprepass = true; // with gfx_gpucull 1: the depth of the instances is drawn before their colour + // with gfx_gpucull 1: the draws of the instances which follow one another with the same material and geometry bank are + // made with one multi draw, their model blocks read from a storage buffer by the number of the draw + bool gfx_gpucull_multidraw = true; // (opengl 4.6) the depth of the whole opaque scene of the main view is drawn before its colour, the instances included bool gfx_depthprepass_scene = false; // (opengl 4.6) the model block of every draw and the matrices of instances are written to persistently mapped buffers,