From 466cb5339e2ad8bfb22f0a48b5089e1fa126fc7f Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Fri, 16 Oct 2026 12:54:52 +0200 Subject: [PATCH] Draw instances from the GPU culling with indirect draws, occlusion culling and a depth prepass --- CMakeLists.txt | 1 + docs/opengl46-renderer.md | 24 +++ gl/glsl_common.cpp | 3 + gl/ubo.h | 25 ++- rendering/geometrybank.cpp | 38 ++++ rendering/geometrybank.h | 22 ++ rendering/opengl46/opengl46geometrybank.cpp | 52 +++++ rendering/opengl46/opengl46geometrybank.h | 6 + rendering/opengl46/opengl46hiz.cpp | 119 +++++++++++ rendering/opengl46/opengl46hiz.h | 68 +++++++ rendering/opengl46/opengl46instancecull.cpp | 140 +++++++++++-- rendering/opengl46/opengl46instancecull.h | 78 ++++++- rendering/opengl46/opengl46renderer.cpp | 213 +++++++++++++++++++- rendering/opengl46/opengl46renderer.h | 34 +++- shaders/depthprepass.frag | 7 + shaders/depthprepass.vert | 20 ++ shaders/depthprepass_alpha.frag | 16 ++ shaders/hiz_build.comp | 39 ++++ shaders/instance_cull.comp | 62 +++++- shaders/instance_cull_commands.comp | 39 ++++ shaders/vertex.vert | 3 + utilities/Globals.cpp | 19 +- utilities/Globals.h | 6 +- 23 files changed, 1002 insertions(+), 32 deletions(-) create mode 100644 rendering/opengl46/opengl46hiz.cpp create mode 100644 rendering/opengl46/opengl46hiz.h create mode 100644 shaders/depthprepass.frag create mode 100644 shaders/depthprepass.vert create mode 100644 shaders/depthprepass_alpha.frag create mode 100644 shaders/hiz_build.comp create mode 100644 shaders/instance_cull_commands.comp diff --git a/CMakeLists.txt b/CMakeLists.txt index 25439c85..d23c5c9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -340,6 +340,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46locallights.cpp" "rendering/opengl46/opengl46localshadows.cpp" "rendering/opengl46/opengl46instancecull.cpp" + "rendering/opengl46/opengl46hiz.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" "rendering/opengl46/opengl46terrain.cpp" diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index b55290a9..82e71994 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -164,6 +164,25 @@ Lamps of vehicles and lit compartments cast no shadows. The memory taken is 2 ma | `gfx.lights.shadows.lamps.range` | `100` | distance from the camera within which scenery lamps cast shadows, metres | | `gfx.lights.shadows.vehicles.range` | `50` | distance from the camera within which vehicles (and the occupied cab) cast the shadows of scenery lamps, metres; 0: only the scenery does | +### Models drawn from the GPU + +Many models of the scenery (trees, bushes, poles, fences: models without lights and animations) are drawn in batches, one +batch per model and level of detail. Which of them are drawn, and at which level of detail, can be worked out on the +processor (as before) or on the graphics card. On the graphics card the processor no longer goes through the models one by +one, so with dense vegetation it is much less loaded. Only the main view is drawn this way; shadows, reflections and other +views go as before. + +| Entry | Default | Meaning | +|---|---|---| +| `gfx.gpucull` | `no` | `no`: on the processor, as before. `yes`: on the graphics card, and the models are drawn from its result. `compare`: on the processor, which draws, and on the graphics card too, for the results to be compared (debugging) | +| `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.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) | + +The occlusion test does not take models drawn this way as covering each other: a tree hidden behind another tree is still +drawn (its colour is not worked out with the depth prepass on). Models whose material is not one of the usual ones (parallax, +water and the like) are left out of the depth prepass and drawn as before. All three can be changed while the simulator +runs, in the **Instance culling** debug window. + ### Changed option `dynamiclights` (0..7) used to limit the number of headlight sets that light the surroundings. It is still @@ -210,6 +229,11 @@ editor goes through the first `dynamiclights` headlights, the reflections throug Shift+F12 shows the GPU time of `atmosphere:` (look-up tables, only when the sun moved or the weather changed), `envmap ggx:` (prefiltering, with each update of the environment map), `exposure:` and `bloom:`. The same window holds the ambient occlusion mode and its settings; Shift+F12 shows its time as `ambient occlusion:` (gtao only). +- **Instance culling** window: the mode of `gfx.gpucull` and the two switches above. With `gfx.gpucull yes` Shift+F12 shows + `gpu cull:` (models drawn 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 `depth prepass:`. With `compare` it shows how many + models both counted and how many levels of detail differ. - `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 1eb8f162..d13db0e6 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -21,8 +21,11 @@ void gl::glsl_common_setup() "#define INSTANCE_LOD_SSBO_BINDING " + std::to_string(INSTANCE_LOD_SSBO_BINDING) + "\n" + "#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 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" + + "#define HIZ_GROUP_SIZE " + std::to_string(HIZ_GROUP_SIZE) + "\n" + "#define INSTANCE_COUNTER_HEADER " + std::to_string(INSTANCE_COUNTER_HEADER) + "\n" + "#define LIGHT_SSBO_BINDING " + std::to_string(LIGHT_SSBO_BINDING) + "\n" + "#define CLUSTER_GRID_X " + std::to_string(CLUSTER_GRID_X) + "\n" + diff --git a/gl/ubo.h b/gl/ubo.h index ef1b6839..af0871fe 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -391,11 +391,16 @@ namespace gl const GLuint INSTANCE_LOD_SSBO_BINDING = 12; // sorted lod bounds (squared distances) of the groups const GLuint INSTANCE_COUNTER_SSBO_BINDING = 13; // header, then count[entries] and cursor[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) const GLuint INSTANCE_CULL_GROUP_SIZE = 64; const GLuint INSTANCE_OFFSETS_GROUP_SIZE = 1024; - // uints of the header of the counter buffer: frame number, survivors in total, entries, unused. + const GLuint INSTANCE_COMMAND_GROUP_SIZE = 64; + // uints of the header of the counter buffer: frame number, survivors in total, entries, instances left out as hidden + // behind the depth already drawn (occlusion test). // an entry is a lod band of a group: survivors of group g in band b are counted at entry entryfirst(g) + b const GLuint INSTANCE_COUNTER_HEADER = 4; + // work group of the passes which make the depth pyramid of the occlusion test (hiz_build.comp), 8x8 + const GLuint HIZ_GROUP_SIZE = 8; // layout std430: a bucket of a scene cell queued in the pass (GLSL: instance_cull_job) struct instance_cull_job @@ -422,5 +427,23 @@ namespace gl static_assert(sizeof(instance_cull_group) == 16, "bad size of instance_cull_group"); + // layout std430: an indirect draw of the submodel of a group in one of its lod bands. the first five fields are the + // command of glDrawElementsIndirect (count, instance count, first index, base vertex, base instance), or with indexed 0 + // the one of glDrawArraysIndirect (count, instance count, first vertex, base instance). the cpu fills in what it knows + // of the geometry, instance_cull_commands.comp the instances, from the counts of the entry + struct instance_draw_command + { + uint32_t count; + uint32_t instancecount; + uint32_t first; + uint32_t basevertex; // the base instance of an array draw + uint32_t baseinstance; + uint32_t entry; + uint32_t indexed; + uint32_t pad; + }; + + static_assert(sizeof(instance_draw_command) == 32, "bad size of instance_draw_command"); + #pragma pack(pop) } diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index ef2d8f4e..ae59716d 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -339,6 +339,20 @@ geometry_bank::draw_batch( gfx::geometry_handle const *First, gfx::geometry_hand return draw_batch_( First, Last, Units, Streams ); } +// makes the data of specified chunk ready for drawing and tells where it lies +bool +geometry_bank::indirect_source( gfx::geometry_handle const &Geometry, gfx::indirect_source &Source ) { + // template method implementation + return indirect_source_( Geometry, Source ); +} + +// draws geometry stored in specified chunk with the command at Offset in the bound draw indirect buffer +void +geometry_bank::draw_indirect( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, unsigned int const Streams ) { + // template method implementation + draw_indirect_( Geometry, Offset, Streams ); +} + // 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() { @@ -469,6 +483,30 @@ geometrybank_manager::draw_batch( gfx::geometry_handle const *First, gfx::geomet } } +// makes the data of specified chunk ready for an indirect draw and tells where it lies +bool +geometrybank_manager::indirect_source( gfx::geometry_handle const &Geometry, gfx::indirect_source &Source ) { + + if( Geometry == null_handle ) { return false; } + + auto &bankrecord = bank( Geometry ); + + bankrecord.second = m_garbagecollector.timestamp(); + return bankrecord.first->indirect_source( Geometry, Source ); +} + +// draws geometry stored in specified chunk with the command at Offset in the bound draw indirect buffer +void +geometrybank_manager::draw_indirect( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, unsigned int const Streams ) { + + if( Geometry == null_handle ) { return; } + + auto &bankrecord = bank( Geometry ); + + bankrecord.second = m_garbagecollector.timestamp(); + bankrecord.first->draw_indirect( Geometry, Offset, Streams ); +} + // 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 2bc51dfa..29c62449 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -61,6 +61,16 @@ struct stream_units { using basic_index = std::uint32_t; +// where a chunk lies in the buffers of its bank, for an indirect draw command made on the cpu: count of indices (or of +// vertices), first index (or vertex), base vertex, whether the draw is indexed, and the primitive type +struct indirect_source { + std::uint32_t count { 0 }; + std::uint32_t first { 0 }; + std::int32_t basevertex { 0 }; + bool indexed { false }; + unsigned int type { 0 }; +}; + using vertex_array = std::vector; using userdata_array = std::vector; using index_array = std::vector; @@ -123,6 +133,11 @@ public: 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 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; + // makes the data of specified chunk ready for drawing and tells where it lies. returns: false if there's nothing to draw + 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, + // 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 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 { @@ -194,6 +209,9 @@ private: std::size_t count { 0 }; for( ; First != Last; ++First ) { count += draw_( *First, Units, Streams ); } return count; } + // 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*/ ) {} // resource release subclass details virtual void release_() = 0; }; @@ -227,6 +245,10 @@ public: 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 void draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, unsigned int const Streams = basic_streams ); + // makes the data of specified chunk ready for an indirect draw and tells where it lies. returns: false if there's nothing to draw + 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 ); 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 00950b71..c75bb0dc 100644 --- a/rendering/opengl46/opengl46geometrybank.cpp +++ b/rendering/opengl46/opengl46geometrybank.cpp @@ -222,6 +222,58 @@ opengl46_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry, return prims * InstanceCount; } +// indirect_source() subclass details — the chunk is uploaded first if needed, as by a draw, so that an indirect draw +// command made from what this tells finds it in place +bool +opengl46_vaogeometrybank::indirect_source_( gfx::geometry_handle const &Geometry, gfx::indirect_source &Source ) +{ + setup_buffer(); + + auto &chunkrecord = m_chunkrecords.at(Geometry.chunk - 1); + if( chunkrecord.vertex_count == 0 ) { return false; } + + auto const &chunk = gfx::geometry_bank::chunk( Geometry ); + if( !chunkrecord.is_good ) { + m_vao->bind(); + if( chunkrecord.index_count > 0 ) { + m_indexbuffer->upload( gl::buffer::ELEMENT_ARRAY_BUFFER, chunk.indices.data(), chunkrecord.index_offset * sizeof( gfx::basic_index ), chunkrecord.index_count * sizeof( gfx::basic_index ) ); + } + m_vertexbuffer->upload( gl::buffer::ARRAY_BUFFER, chunk.vertices.data(), chunkrecord.vertex_offset * sizeof( gfx::basic_vertex ), chunkrecord.vertex_count * sizeof( gfx::basic_vertex ) ); + if( chunkrecord.has_userdata ) { + m_vertexbuffer->upload( gl::buffer::ARRAY_BUFFER, chunk.userdata.data(), + static_cast(m_vertex_count * sizeof(gfx::basic_vertex) + chunkrecord.vertex_offset * sizeof(gfx::vertex_userdata)), + static_cast(chunkrecord.vertex_count * sizeof(gfx::vertex_userdata)) ); + } + chunkrecord.is_good = true; + } + + Source.indexed = ( chunkrecord.index_count > 0 ); + Source.count = static_cast( Source.indexed ? chunkrecord.index_count : chunkrecord.vertex_count ); + Source.first = static_cast( Source.indexed ? chunkrecord.index_offset : chunkrecord.vertex_offset ); + Source.basevertex = static_cast( Source.indexed ? chunkrecord.vertex_offset : 0 ); + Source.type = chunk.type; + return true; +} + +// draw_indirect() subclass details — the command comes from the bound draw indirect buffer +void +opengl46_vaogeometrybank::draw_indirect_( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, unsigned int const /*Streams*/ ) +{ + setup_buffer(); + + auto const &chunkrecord = m_chunkrecords.at(Geometry.chunk - 1); + if( chunkrecord.vertex_count == 0 ) { return; } + + auto const &chunk = gfx::geometry_bank::chunk( Geometry ); + m_vao->bind(); + if( chunkrecord.index_count > 0 ) { + ::glDrawElementsIndirect( chunk.type, GL_UNSIGNED_INT, reinterpret_cast( Offset ) ); + } + else { + ::glDrawArraysIndirect( chunk.type, reinterpret_cast( Offset ) ); + } +} + // 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 957c9d12..2e11fc66 100644 --- a/rendering/opengl46/opengl46geometrybank.h +++ b/rendering/opengl46/opengl46geometrybank.h @@ -59,6 +59,12 @@ private: // draw_batch() subclass details — issues glMultiDrawArrays for runs of non-indexed chunks of the same type 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; + // indirect_source() subclass details — uploads the chunk if needed, tells its place in the buffers + auto + indirect_source_( gfx::geometry_handle const &Geometry, gfx::indirect_source &Source ) -> bool override; + // 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; // release() subclass details void release_() override; diff --git a/rendering/opengl46/opengl46hiz.cpp b/rendering/opengl46/opengl46hiz.cpp new file mode 100644 index 00000000..51a8c5c8 --- /dev/null +++ b/rendering/opengl46/opengl46hiz.cpp @@ -0,0 +1,119 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/opengl46/opengl46hiz.h" + +#include "gl/ubo.h" +#include "model/Texture.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +opengl46_hiz::~opengl46_hiz() +{ + release(); +} + +void opengl46_hiz::init() +{ + gl::shader shader("hiz_build.comp"); + m_program = std::make_unique(std::vector>{shader}); +} + +void opengl46_hiz::release() +{ + if (m_depthfb != 0) + glDeleteFramebuffers(1, &m_depthfb); + if (m_depth != 0) + glDeleteTextures(1, &m_depth); + if (m_pyramid != 0) + glDeleteTextures(1, &m_pyramid); + m_depthfb = 0; + m_depth = 0; + m_pyramid = 0; + m_sourcesize = glm::ivec2{0}; + m_size = glm::ivec2{0}; + m_levels = 0; +} + +void opengl46_hiz::allocate(int const Width, int const Height) +{ + release(); + m_sourcesize = glm::ivec2{Width, Height}; + + // the blit of depth needs the format of the source + glCreateTextures(GL_TEXTURE_2D, 1, &m_depth); + glTextureStorage2D(m_depth, 1, Global.gfx_format_depth, Width, Height); + glTextureParameteri(m_depth, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(m_depth, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTextureParameteri(m_depth, GL_TEXTURE_COMPARE_MODE, GL_NONE); + glCreateFramebuffers(1, &m_depthfb); + glNamedFramebufferTexture(m_depthfb, GL_DEPTH_ATTACHMENT, m_depth, 0); + glNamedFramebufferDrawBuffer(m_depthfb, GL_NONE); + glNamedFramebufferReadBuffer(m_depthfb, GL_NONE); + + m_size = glm::ivec2{(Width + 1) / 2, (Height + 1) / 2}; + m_levels = 1; + while ((std::max(m_size.x, m_size.y) >> m_levels) > 0) + ++m_levels; + glCreateTextures(GL_TEXTURE_2D, 1, &m_pyramid); + glTextureStorage2D(m_pyramid, m_levels, GL_R32F, m_size.x, m_size.y); + glTextureParameteri(m_pyramid, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + glTextureParameteri(m_pyramid, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTextureParameteri(m_pyramid, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTextureParameteri(m_pyramid, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +void opengl46_hiz::build(GLuint const Source, int const Width, int const Height) +{ + if ((Width <= 0) || (Height <= 0)) + return; + if (m_sourcesize != glm::ivec2{Width, Height}) + allocate(Width, Height); + + m_timer.begin(); + + // resolve of the multisampled depth; the scissor test would limit the blit + auto const scissor{glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE}; + if (scissor) + glDisable(GL_SCISSOR_TEST); + glBlitNamedFramebuffer(Source, m_depthfb, 0, 0, Width, Height, 0, 0, Width, Height, GL_DEPTH_BUFFER_BIT, GL_NEAREST); + if (scissor) + glEnable(GL_SCISSOR_TEST); + + auto source{m_depth}; + auto sourcelevel{0}; + auto sourcesize{m_sourcesize}; + for (int level = 0; level < m_levels; ++level) + { + auto const targetsize{glm::max(m_size >> level, glm::ivec2{1})}; + glBindTextureUnit(0, source); + glBindImageTexture(0, m_pyramid, level, GL_FALSE, 0, GL_WRITE_ONLY, GL_R32F); + glProgramUniform4i(*m_program, 0, sourcesize.x, sourcesize.y, targetsize.x, targetsize.y); + glProgramUniform4i(*m_program, 1, sourcelevel, 0, 0, 0); + m_program->dispatch((targetsize.x + gl::HIZ_GROUP_SIZE - 1) / gl::HIZ_GROUP_SIZE, (targetsize.y + gl::HIZ_GROUP_SIZE - 1) / gl::HIZ_GROUP_SIZE); + glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + source = m_pyramid; + sourcelevel = level; + sourcesize = targetsize; + } + glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R32F); + glBindTextureUnit(0, 0); + opengl_texture::reset_unit_cache(); + + m_timer.end(); +} + +std::string opengl46_hiz::info_times() +{ + std::string text; + if (auto const time = m_timer.result()) + text += " occlusion depth: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; + return text; +} diff --git a/rendering/opengl46/opengl46hiz.h b/rendering/opengl46/opengl46hiz.h new file mode 100644 index 00000000..7e50e27a --- /dev/null +++ b/rendering/opengl46/opengl46hiz.h @@ -0,0 +1,68 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include + +#include "gl/shader.h" +#include "gl/query.h" + +// depth pyramid of the occlusion test of the instance culling (instance_cull.comp). made in the middle of the colour +// pass of the main view, from the depth drawn up to then (cab, terrain, shapes, tracks, vehicles and the models drawn on +// their own), so it needs no reprojection: the instances are tested against what is already in the image. +// the multisampled depth is resolved by a blit, then each level of the pyramid (hiz_build.comp) keeps the farthest +// depth of the 2x2 texels below it; the first level is half the size of the view +class opengl46_hiz +{ + public: + opengl46_hiz() = default; + ~opengl46_hiz(); + opengl46_hiz(const opengl46_hiz &) = delete; + opengl46_hiz &operator=(const opengl46_hiz &) = delete; + + // creates the program. throws gl::shader_exception + void init(); + // makes the pyramid from the depth attachment of Source, a framebuffer of Width x Height pixels. + // leaves the framebuffer bindings alone, texture unit 0 and image unit 0 are used (the unit cache is reset) + void build(GLuint const Source, int const Width, int const Height); + // the pyramid, its first level size and level count; valid after build() + GLuint texture() const + { + return m_pyramid; + } + glm::ivec2 size() const + { + return m_size; + } + int levels() const + { + return m_levels; + } + glm::ivec2 source_size() const + { + return m_sourcesize; + } + std::string info_times(); + + private: + // (re)creates the textures for a view of the given size + void allocate(int const Width, int const Height); + void release(); + + std::unique_ptr m_program; + GLuint m_depth{0}; // resolved depth, the format of the scene depth + GLuint m_depthfb{0}; + GLuint m_pyramid{0}; // r32f, m_levels levels + glm::ivec2 m_sourcesize{0}; + glm::ivec2 m_size{0}; + int m_levels{0}; + gl::gpu_timer m_timer; +}; diff --git a/rendering/opengl46/opengl46instancecull.cpp b/rendering/opengl46/opengl46instancecull.cpp index 0f30ac0e..2df29940 100644 --- a/rendering/opengl46/opengl46instancecull.cpp +++ b/rendering/opengl46/opengl46instancecull.cpp @@ -10,7 +10,9 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/opengl46/opengl46instancecull.h" +#include "model/AnimModel.h" #include "model/Model3d.h" +#include "model/Texture.h" #include "rendering/frustum.h" #include "utilities/Globals.h" #include "utilities/utilities.h" @@ -37,6 +39,7 @@ void opengl46_instance_cull::init() }; m_cull_program = make_compute("instance_cull.comp"); m_offsets_program = make_compute("instance_cull_offsets.comp"); + m_commands_program = make_compute("instance_cull_commands.comp"); auto const readbacksize{static_cast((gl::INSTANCE_COUNTER_HEADER + READBACK_ENTRIES) * sizeof(std::uint32_t))}; m_records = std::make_unique(65536 * RECORD_SIZE, gl::INSTANCE_RECORD_SSBO_BINDING); @@ -46,6 +49,7 @@ void opengl46_instance_cull::init() // the readback copies a fixed size from it m_counters = std::make_unique(readbacksize, gl::INSTANCE_COUNTER_SSBO_BINDING); m_output = std::make_unique(65536 * sizeof(glm::mat4), gl::INSTANCE_OUTPUT_SSBO_BINDING); + m_commands = std::make_unique(4096 * sizeof(gl::instance_draw_command), gl::INSTANCE_COMMAND_SSBO_BINDING); m_readback = std::make_unique(readbacksize); m_readbackdata.resize(gl::INSTANCE_COUNTER_HEADER + READBACK_ENTRIES); } @@ -60,6 +64,7 @@ void opengl46_instance_cull::begin_pass(pass_setup const &Setup) m_buckets.clear(); m_joblist.clear(); m_grouplist.clear(); + m_drawgroups.clear(); m_lodlist.clear(); m_groupmap.clear(); m_cpucounts.clear(); @@ -67,11 +72,19 @@ void opengl46_instance_cull::begin_pass(pass_setup const &Setup) m_entries = 0; } -void opengl46_instance_cull::add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket) +void opengl46_instance_cull::add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area) { if (!m_inpass || Bucket.records.empty() || Key.pModel == nullptr) return; + // range of the squared distances the instances of the bucket can be at, scaled as in the culling, with some slack for + // its single precision: the draws are made only for the lod bands within it + auto const centerdistance{glm::length(Area.center - m_setup.distancecamera)}; + auto const nearest{std::max(0.0, centerdistance - Area.radius)}; + auto const farthest{centerdistance + Area.radius}; + auto const distancemin{static_cast(0.999 * (nearest * nearest * m_setup.distancescale + m_setup.distanceoffset))}; + auto const distancemax{static_cast(1.001 * (farthest * farthest * m_setup.distancescale + m_setup.distanceoffset))}; + auto lookup{m_groupmap.find(Key)}; if (lookup == m_groupmap.end()) { @@ -96,10 +109,46 @@ void opengl46_instance_cull::add_bucket(scene::basic_cell::instance_bucket_key c m_lodlist.erase(std::unique(m_lodlist.begin() + lodfirst, m_lodlist.end()), m_lodlist.end()); auto const lodcount{static_cast(m_lodlist.size()) - lodfirst}; - group_data const group{static_cast(m_grouplist.size()), m_entries, lodcount + 1}; + group_data const group{static_cast(m_grouplist.size()), m_entries, lodcount + 1, distancemin, distancemax}; m_grouplist.push_back({lodfirst, lodcount, m_entries, 0}); m_entries += group.entrycount; lookup = m_groupmap.emplace(Key, group).first; + if (m_setup.drawing) + { + // any instance of the group serves; it's only prepared and asked for its material + TAnimModel *instance{nullptr}; + for (auto *candidate : Bucket.instances) + { + if (candidate != nullptr) + { + instance = candidate; + break; + } + } + m_drawgroups.push_back({&lookup->first, instance, lodfirst, lodcount, group.entryfirst, distancemin, distancemax}); + } + } + else + { + lookup->second.distancemin = std::min(lookup->second.distancemin, distancemin); + lookup->second.distancemax = std::max(lookup->second.distancemax, distancemax); + if (m_setup.drawing) + { + auto &drawgroup{m_drawgroups[lookup->second.index]}; + drawgroup.distancemin = lookup->second.distancemin; + drawgroup.distancemax = lookup->second.distancemax; + if (drawgroup.instance == nullptr) + { + for (auto *candidate : Bucket.instances) + { + if (candidate != nullptr) + { + drawgroup.instance = candidate; + break; + } + } + } + } } place(Bucket); @@ -161,8 +210,9 @@ void opengl46_instance_cull::place(scene::basic_cell::instance_bucket const &Buc allocate(Bucket); } -void opengl46_instance_cull::dispatch(glm::mat4 const &View) +void opengl46_instance_cull::dispatch(glm::mat4 const &View, occlusion_setup const *Occlusion) { + m_occlusion = false; if (!m_inpass || m_joblist.empty()) return; @@ -203,6 +253,19 @@ void opengl46_instance_cull::dispatch(glm::mat4 const &View) glProgramUniform4f(*m_cull_program, 7, m_setup.distancescale, m_setup.distanceoffset, m_setup.distancerange, m_setup.drawrange); glProgramUniform4f(*m_cull_program, 8, m_setup.zoom, 0.f, 0.f, 0.f); glProgramUniform4ui(*m_offsets_program, 0, m_entries, 0, 0, 0); + // the occlusion test only when the instances are drawn from the result, the cpu doesn't make it + m_occlusion = (m_setup.drawing && (Occlusion != nullptr) && (Occlusion->pyramid != 0)); + if (m_occlusion) + { + glProgramUniformMatrix4fv(*m_cull_program, 10, 1, GL_FALSE, glm::value_ptr(Occlusion->projection)); + glProgramUniform4i(*m_cull_program, 14, 1, Occlusion->levels, Occlusion->size.x, Occlusion->size.y); + glProgramUniform4f(*m_cull_program, 15, Occlusion->screen.x, Occlusion->screen.y, 0.f, 0.f); + glBindTextureUnit(0, Occlusion->pyramid); + } + else + { + glProgramUniform4i(*m_cull_program, 14, 0, 0, 0, 0); + } // one invocation per record; past the count limit of one dimension the groups go into rows auto const groups{(m_candidates + gl::INSTANCE_CULL_GROUP_SIZE - 1) / gl::INSTANCE_CULL_GROUP_SIZE}; @@ -220,12 +283,49 @@ void opengl46_instance_cull::dispatch(glm::mat4 const &View) // the output and the counts go to draws, indirect draws and the copy for the cpu gl::memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT | GL_COMMAND_BARRIER_BIT); m_timer.end(); + if (m_occlusion) + { + glBindTextureUnit(0, 0); + opengl_texture::reset_unit_cache(); + } m_readback->push(*m_counters); m_cpucounts.assign(m_entries, 0); m_dispatched = true; } +void opengl46_instance_cull::fill_commands(std::vector const &Commands) +{ + m_commandcount = static_cast(Commands.size()); + if (!m_dispatched || Commands.empty()) + return; + + auto const size{static_cast(Commands.size() * sizeof(gl::instance_draw_command))}; + reserve(*m_commands, size); + m_commands->update(Commands.data(), 0, size); + m_counters->bind_base(); + m_commands->bind_base(); + glProgramUniform4ui(*m_commands_program, 0, m_commandcount, m_entries, 0, 0); + m_commands_program->dispatch((m_commandcount + gl::INSTANCE_COMMAND_GROUP_SIZE - 1) / gl::INSTANCE_COMMAND_GROUP_SIZE); + // the commands go to indirect draws, the matrices to the vertex shaders + gl::memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_COMMAND_BARRIER_BIT); +} + +void opengl46_instance_cull::bind_draw() +{ + glBindBuffer(GL_DRAW_INDIRECT_BUFFER, *m_commands); + // the way gl::ssbo binds, leaving the general binding point alone + GLuint const output{*m_output}; + 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) +{ + m_draws = Draws; + m_prepassdraws = Prepassdraws; + m_cputime = Cputime; +} + void opengl46_instance_cull::cpu_result(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts) { if (!m_dispatched) @@ -240,7 +340,8 @@ void opengl46_instance_cull::cpu_result(scene::basic_cell::instance_bucket_key c void opengl46_instance_cull::end_pass() { - if (m_dispatched) + // the cpu counts are there to compare with only when the cpu drew + if (m_dispatched && !m_setup.drawing) { std::uint64_t total{0}; for (auto const count : m_cpucounts) @@ -260,17 +361,22 @@ void opengl46_instance_cull::collect() return; auto const frame{m_readbackdata[0]}; - while (!m_cpupasses.empty() && m_cpupasses.front().frame != frame) - m_cpupasses.pop_front(); - if (m_cpupasses.empty()) - return; - - auto const &cpu{m_cpupasses.front()}; comparison result; result.frame = frame; result.gputotal = m_readbackdata[1]; - result.cputotal = cpu.total; result.entries = m_readbackdata[2]; + result.occluded = m_readbackdata[3]; + while (!m_cpupasses.empty() && m_cpupasses.front().frame != frame) + m_cpupasses.pop_front(); + if (m_cpupasses.empty()) + { + // drawn from the gpu: nothing to compare with + m_last = result; + return; + } + + auto const &cpu{m_cpupasses.front()}; + result.cputotal = cpu.total; result.compared = std::min({result.entries, READBACK_ENTRIES, static_cast(cpu.counts.size())}); for (std::uint32_t entry = 0; entry < result.compared; ++entry) { @@ -292,8 +398,16 @@ void opengl46_instance_cull::collect() std::string opengl46_instance_cull::info() { - if (!Global.gfx_gpucull) + if (Global.gfx_gpucull == 0) return {}; + if (Global.gfx_gpucull == 1) + { + if (m_last.frame == 0) + return "\ngpu cull: no result yet"; + return "\ngpu cull: " + std::to_string(m_last.gputotal) + " instances drawn, " + std::to_string(m_last.occluded) + " hidden by occlusion" + + (m_occlusion ? "" : " (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"; + } if (m_comparedframes == 0) return "\ngpu cull: no result yet"; return "\ngpu cull: " + std::to_string(m_last.gputotal) + " instances (cpu " + std::to_string(m_last.cputotal) + "), " + std::to_string(m_last.entries) + " bands, " + @@ -304,7 +418,7 @@ std::string opengl46_instance_cull::info() std::string opengl46_instance_cull::info_times() { std::string text; - if (!Global.gfx_gpucull) + if (Global.gfx_gpucull == 0) return text; if (auto const time = m_timer.result()) text += " instance cull: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; diff --git a/rendering/opengl46/opengl46instancecull.h b/rendering/opengl46/opengl46instancecull.h index cd0c88c7..ecc79a5d 100644 --- a/rendering/opengl46/opengl46instancecull.h +++ b/rendering/opengl46/opengl46instancecull.h @@ -23,6 +23,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/scene.h" class cFrustum; +class TAnimModel; // culling of the model instances of a render pass in compute passes (instance_cull.comp, instance_cull_offsets.comp), // the gpu side of opengl46_renderer::Queue_Instances() and Render_Instanced(): @@ -32,8 +33,13 @@ class cFrustum; // lod bands are its entries // - the passes count the instances which pass per entry, turn the counts into places in the output and write the // modelview matrices of the instances there -// so far the result is only compared with the one of the cpu, which still draws: the counts per entry are read back a few -// frames later and set against the counts Render_Instanced() reported for the same frame (Global.gfx_gpucull) +// with Global.gfx_gpucull 1 the instances are drawn from the result: the renderer turns each lod band of a group into a +// list of indirect draw commands (one per submodel drawn in the band), whose instances a fourth pass fills in from the +// counts (instance_cull_commands.comp), and draws them with the output bound as the instance buffer +// (opengl46_renderer::Render_Instances_Indirect()). an occlusion test against a depth pyramid of what the pass drew before +// (opengl46_hiz) can leave out more instances. +// with Global.gfx_gpucull 2 the cpu still draws, and the result is only compared with its own: the counts per entry are +// read back a few frames later and set against the counts Render_Instanced() reported for the same frame class opengl46_instance_cull { public: @@ -49,6 +55,27 @@ class opengl46_instance_cull float distancerange{0.f}; // squared range of the scaled distance before the offset float drawrange{0.f}; // squared draw range float zoom{1.f}; + bool drawing{false}; // the instances are drawn from the result (Global.gfx_gpucull 1), not compared + }; + // occlusion test of a pass + struct occlusion_setup + { + GLuint pyramid{0}; // texture of the depth pyramid (opengl46_hiz) + glm::ivec2 size{0}; // of its first level + int levels{0}; + glm::vec2 screen{0.f}; // size of the depth it was made from, pixels + glm::mat4 projection{1.f}; // of the pass + }; + // a group of instances (model and skins) of the pass, for the renderer to make the draw commands of + struct draw_group + { + scene::basic_cell::instance_bucket_key const *key; + TAnimModel *instance; // one of the group, prepares the model and gives the material + std::uint32_t lodfirst; // in lod_bounds() + std::uint32_t lodcount; + std::uint32_t entryfirst; + float distancemin; // squared distances (scaled as the culling does) the instances of the group can be at + float distancemax; }; opengl46_instance_cull() = default; @@ -63,10 +90,37 @@ class opengl46_instance_cull { return m_inpass; } - // adds the records of a bucket (made already) to the pass, sending them to the gpu when they changed - void add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket); - // runs the culling passes for the buckets added. View: rotation of the view of the pass - void dispatch(glm::mat4 const &View); + bool drawing() const + { + return m_inpass && m_setup.drawing; + } + // adds the records of a bucket (made already) to the pass, sending them to the gpu when they changed. + // Area: bounds of the cell holding the bucket, which hold the locations of all its instances + void add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area); + // runs the culling passes for the buckets added. View: rotation of the view of the pass. Occlusion: the depth pyramid + // to test the instances against, or null + void dispatch(glm::mat4 const &View, occlusion_setup const *Occlusion = nullptr); + // groups of the pass and the lod bounds they refer to, valid from dispatch() to end_pass() + std::vector const &draw_groups() const + { + return m_drawgroups; + } + std::vector const &lod_bounds() const + { + return m_lodlist; + } + // sends the indirect draw commands made for the entries of the pass and fills in their instances. after this the + // commands are in the buffer bound by bind_draw(), the modelview matrices of the instances in output() + void fill_commands(std::vector const &Commands); + // 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); + // instances which passed in the last result which came back from the gpu, a few frames old + std::uint32_t last_count() const + { + return static_cast(m_last.gputotal); + } // counts per lod band of the instances Render_Instanced() drew for a model and its skins in this pass void cpu_result(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts); // ends the pass: its cpu counts are kept for the comparison @@ -81,6 +135,8 @@ class opengl46_instance_cull std::uint32_t index; std::uint32_t entryfirst; std::uint32_t entrycount; + float distancemin; + float distancemax; }; // counts of the cpu of a pass, waiting for those of the gpu struct cpu_pass @@ -98,6 +154,7 @@ class opengl46_instance_cull std::uint32_t compared{0}; // entries compared, at most READBACK_ENTRIES std::uint32_t mismatched{0}; std::uint32_t largestdifference{0}; + std::uint32_t occluded{0}; // left out by the occlusion test }; // finds a place for the records of a bucket in the record buffer and sends them @@ -110,12 +167,14 @@ class opengl46_instance_cull std::unique_ptr m_cull_program; std::unique_ptr m_offsets_program; + std::unique_ptr m_commands_program; std::unique_ptr m_records; std::unique_ptr m_jobs; std::unique_ptr m_groups; std::unique_ptr m_lods; std::unique_ptr m_counters; std::unique_ptr m_output; + std::unique_ptr m_commands; std::unique_ptr m_readback; gl::gpu_timer m_timer; @@ -128,6 +187,7 @@ class opengl46_instance_cull std::vector m_buckets; // of the jobs, in their order std::vector m_joblist; std::vector m_grouplist; + std::vector m_drawgroups; std::vector m_lodlist; std::unordered_map m_groupmap; std::vector m_pending; // walk of the submodel tree @@ -141,4 +201,10 @@ class opengl46_instance_cull comparison m_last; std::uint64_t m_comparedframes{0}; std::uint64_t m_mismatchedframes{0}; + // 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_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 0606eaa5..f940d452 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -167,6 +167,8 @@ bool opengl46_renderer::Init(GLFWwindow *Window) m_cluster_cull_shader = std::make_unique(std::vector>{cluster_cull}); } m_cluster_heatmap_shader = make_shader("quad.vert", "cluster_heatmap.frag"); + m_prepass_shader = make_shader("depthprepass.vert", "depthprepass.frag"); + m_prepass_alpha_shader = make_shader("depthprepass.vert", "depthprepass_alpha.frag"); m_invalid_material = Fetch_Material("invalid"); } catch (gl::shader_exception const &e) @@ -189,6 +191,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) { m_sky.init(); m_instancecull.init(); + m_hiz.init(); m_exposure.init(); m_ao.init(); m_localshadows.init(); @@ -728,6 +731,11 @@ void opengl46_renderer::SwapBuffers() m_debugtimestext += m_exposure.info_times(); m_debugtimestext += m_ao.info_times(); m_debugtimestext += m_instancecull.info_times(); + if( Global.gfx_gpucull == 1 ) { + m_debugtimestext += m_hiz.info_times(); + if( auto const prepasstime = m_prepasstimer.result() ) + m_debugtimestext += " depth prepass: " + to_string( static_cast( *prepasstime ) / 1000000.0, 3 ) + " ms\n"; + } m_debugtimestext += m_localshadows.info_times(); m_debugtimestext += "uilayer: " + to_string( Timer::subsystem.gfx_gui.average(), 2 ) + " ms\n"; if( DebugModeFlag ) @@ -837,7 +845,13 @@ void opengl46_renderer::draw_debug_ui() if (ImGui::Begin("Instance culling")) { - ImGui::Checkbox("cull instances on the gpu too, compare with the cpu (gfx.gpucull)", &Global.gfx_gpucull); + char const *modes[]{"cpu", "gpu, drawn from its result", "cpu, compared with the gpu"}; + ImGui::Combo("instance culling (gfx.gpucull)", &Global.gfx_gpucull, modes, IM_ARRAYSIZE(modes)); + if (Global.gfx_gpucull == 1) + { + ImGui::Checkbox("leave out instances hidden behind the depth drawn before them (gfx.gpucull.occlusion)", &Global.gfx_gpucull_occlusion); + ImGui::Checkbox("draw the depth of the instances before their colour (gfx.depthprepass)", &Global.gfx_depthprepass); + } auto const info{m_instancecull.info()}; if (!info.empty()) ImGui::TextUnformatted(info.c_str() + 1); @@ -2454,6 +2468,14 @@ void opengl46_renderer::Bind_Material( material_handle const Material, TSubModel else model_ubs.alpha_mult = 1.0f; + if (m_indirectrecording) + { + // the walk of Render_Instances_Indirect() only notes what would be bound, it's bound when the draw is made + m_indirectmaterial = Material; + m_indirectsubmodel = sm; + return; + } + // materials which declare texture variants provide a different texture set per model instance, // picked from the seed of the rendered instance and the variant group of the submodel auto const &materialtextures{ @@ -2586,10 +2608,10 @@ void opengl46_renderer::Render(scene::basic_region *Region) for( auto &bucket : m_frame_instance_buckets ) { bucket.second.clear(); } - // the gpu culling runs along the first color pass of the main viewport of a frame, for its result to be compared - // with the one of the cpu + // the gpu culling runs along the first color pass of the main viewport of a frame. its result is drawn (Global.gfx_gpucull 1), + // or compared with the one of the cpu, which draws (2) m_instancecullpass = false; - if( ( Global.gfx_gpucull ) + if( ( Global.gfx_gpucull != 0 ) && ( m_renderpass.draw_mode == rendermode::color ) && ( m_current_viewport != nullptr ) && ( m_current_viewport->main ) @@ -2608,6 +2630,7 @@ void opengl46_renderer::Render(scene::basic_region *Region) setup.distancerange = ( reflections ? static_cast( sq( Global.reflectiontune.range_instances ) ) : std::numeric_limits::max() ); setup.drawrange = static_cast( sq( m_renderpass.draw_range + 250 ) ); setup.zoom = Global.ZoomFactor; + setup.drawing = ( Global.gfx_gpucull == 1 ); m_instancecull.begin_pass( setup ); } // build a list of region sections to render @@ -3049,8 +3072,14 @@ void opengl46_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // the number of calls and glBufferSubData round-trips drops sharply. // (For pickscenery/pickcontrols modes the map stays empty, so this is a no-op.) if( m_instancecullpass ) { - // the view of the pass is in place here, the one Render_Instanced() takes - m_instancecull.dispatch( OpenGLMatrices.data( GL_MODELVIEW ) ); + if( m_instancecull.drawing() ) { + // the buckets of the pass weren't queued for the cpu, which leaves the loop below empty + Render_Instances_Indirect(); + } + else { + // the view of the pass is in place here, the one Render_Instanced() takes + m_instancecull.dispatch( OpenGLMatrices.data( GL_MODELVIEW ) ); + } } for( auto &bucket : m_frame_instance_buckets ) { if( bucket.second.empty() ) { continue; } @@ -3085,6 +3114,15 @@ void opengl46_renderer::Draw_Geometry(const gfx::geometrybank_handle &handle) void opengl46_renderer::draw(const gfx::geometry_handle &handle) { model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + if( m_indirectrecording ) { + // the walk of Render_Instances_Indirect(): the draw is made later, from the gpu culling, with this state + gfx::indirect_source source; + if( m_geometry.indirect_source( handle, source ) ) { + m_indirectcommands.push_back( { source.count, 0, source.first, static_cast( source.basevertex ), 0, m_indirectentry, ( source.indexed ? 1u : 0u ), 0 } ); + m_indirectdraws.push_back( { model_ubs, m_indirectmaterial, m_indirectsubmodel, handle, source.type, m_blendingenabled } ); + } + return; + } model_ubo->update(model_ubs); if( m_current_instance_count > 0 ) { @@ -3300,7 +3338,9 @@ void opengl46_renderer::Queue_Instances( scene::basic_cell const *Cell ) } if( m_instancecullpass ) { // the gpu tests every instance of the bucket, the way Render_Instanced() does - m_instancecull.add_bucket( bucket.first, instancebucket ); + m_instancecull.add_bucket( bucket.first, instancebucket, Cell->m_area ); + // and when the instances are drawn from its result, the cpu has nothing more to do with them + if( m_instancecull.drawing() ) { continue; } } auto const &records { instancebucket.records }; std::vector *dest { nullptr }; // looked up when there's something to put in it @@ -3588,6 +3628,165 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector( m_instance_survivors.size() ); } +// how the depth prepass treats the material. the colour program of the material decides: the ones which test the alpha of the +// diffuse texture against the opacity (mat_default.frag and the like) get the same test, the ones which never discard a +// fragment get none, and any other (parallax, water, clouds, unknown) is left out and writes its depth in the colour pass +int opengl46_renderer::Prepass_Class( material_handle const Material ) +{ + if( Material == null_handle ) { return 0; } + auto const &material { m_materials.material( Material ) }; + if( ( false == material.is_good ) || ( material.shader == nullptr ) ) { return 0; } + auto const lookup { m_prepassclasses.find( material.shader.get() ) }; + if( lookup != m_prepassclasses.end() ) { return lookup->second; } + + // names the programs are fetched by: mat_.frag + static std::unordered_set const alphatested { + "default", "default_1", "default_2", "default_detail", "default_specgloss", + "detail_normalmap", "detail_normalmap_specgloss", "normalmap", "normalmap_specgloss", + "reflmap", "reflmap_specgloss", "shadowlessnormalmap", "shadowlessnormalmap_specgloss", + "sunlessnormalmap", "sunlessnormalmap_specgloss" }; + static std::unordered_set const solid { "colored", "default_0" }; + auto prepassclass { 0 }; + for( auto const &shader : m_shaders ) { + if( shader.second.get() != material.shader.get() ) { continue; } + prepassclass = ( alphatested.count( shader.first ) > 0 ? 2 : solid.count( shader.first ) > 0 ? 1 : 0 ); + break; + } + m_prepassclasses.emplace( material.shader.get(), prepassclass ); + return prepassclass; +} + +// draws the instances queued in the pass from the result of the gpu culling. +// the submodel trees are walked the way Render_Instanced() walks them, once per lod band of each group, but the walk only +// records the draws (draw() and Bind_Material() note what they would do, see m_indirectrecording): the instances of a band +// are known only on the gpu, so each draw becomes an indirect draw command whose instances the gpu fills in. +// bands no instance of the group can fall in, by the distance of the cells which hold them, are left out +void opengl46_renderer::Render_Instances_Indirect() +{ + auto const cpustart { std::chrono::steady_clock::now() }; + auto *viewport { m_current_viewport }; + + // occlusion test against the depth drawn so far in the pass: cab, terrain, shapes, tracks, vehicles, other models + opengl46_instance_cull::occlusion_setup occlusion; + auto const useocclusion { + Global.gfx_gpucull_occlusion + && ( false == Global.gfx_skippipeline ) + && ( viewport != nullptr ) + && ( viewport->msaa_fb != nullptr ) }; + if( useocclusion ) { + m_hiz.build( *viewport->msaa_fb, viewport->width, viewport->height ); + occlusion.pyramid = m_hiz.texture(); + occlusion.size = m_hiz.size(); + occlusion.levels = m_hiz.levels(); + occlusion.screen = glm::vec2{ m_hiz.source_size() }; + occlusion.projection = OpenGLMatrices.data( GL_PROJECTION ); + } + m_instancecull.dispatch( OpenGLMatrices.data( GL_MODELVIEW ), ( useocclusion ? &occlusion : nullptr ) ); + + // the draws, recorded while the gpu culls + m_indirectdraws.clear(); + m_indirectcommands.clear(); + auto const &lodbounds { m_instancecull.lod_bounds() }; + m_indirectrecording = true; + for( auto const &group : m_instancecull.draw_groups() ) { + auto *model { group.key->pModel }; + if( ( model == nullptr ) || ( model->Root == nullptr ) || ( group.instance == nullptr ) ) { continue; } + group.instance->RaPrepare(); + auto const *material { group.instance->Material() }; + auto alpha { material != nullptr ? material->textures_alpha : 0x30300030 }; + alpha ^= 0x0F0F000F; + for( std::uint32_t band = 0; band <= group.lodcount; ++band ) { + // a band holds the squared distances from its lower bound up to the next one; any of them selects the same lod + auto const bandmin { band == 0 ? 0.f : lodbounds[ group.lodfirst + band - 1 ] }; + auto const bandmax { band == group.lodcount ? std::numeric_limits::max() : lodbounds[ group.lodfirst + band ] }; + if( ( bandmin > group.distancemax ) || ( bandmax <= group.distancemin ) ) { continue; } + m_indirectentry = group.entryfirst + band; + ::glPushMatrix(); + ::glLoadIdentity(); + model->Root->fSquareDist = bandmin; + model->Root->ReplacableSet( ( material != nullptr ? material->replacable_skins : nullptr ), alpha ); + model->Root->pRoot = model; + Render( model->Root ); + ::glPopMatrix(); + } + } + m_indirectrecording = false; + + m_instancecull.fill_commands( m_indirectcommands ); + std::uint32_t prepassdraws { 0 }; + if( false == m_indirectdraws.empty() ) { + m_instancecull.bind_draw(); + auto const commandoffset = []( std::size_t const Index ) { + return static_cast( Index * sizeof( gl::instance_draw_command ) ); }; + + if( Global.gfx_depthprepass && ( false == Global.bWireFrame ) ) { + // the depth first: the colour pass shades only what stays in 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 + m_prepasstimer.begin(); + setup_drawing( false ); + ::glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE ); + ::glEnable( GL_POLYGON_OFFSET_FILL ); + ::glPolygonOffset( -1.f, -1.f ); + // 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 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; } + 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 ] ); + } + if( false == bound ) { + program.bind(); + bound = true; + } + model_ubs = draw.ubs; + model_ubo->update( model_ubs ); + m_geometry.draw_indirect( draw.geometry, commandoffset( index ) ); + ++prepassdraws; + } + } + ::glDisable( GL_POLYGON_OFFSET_FILL ); + ::glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); + m_prepasstimer.end(); + } + + for( std::size_t index = 0; index < m_indirectdraws.size(); ++index ) { + auto const &draw { m_indirectdraws[ index ] }; + 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, commandoffset( index ) ); + if( draw.blending ) { setup_drawing( false ); } + } + model_ubs.emission = 0.0f; + + ::glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0 ); + m_instancebuffers[ m_instancebufferindex ]->bind_base(); + } + + m_renderpass.draw_stats.instanced_drawcalls += static_cast( m_indirectdraws.size() ); + m_renderpass.draw_stats.drawcalls += static_cast( prepassdraws ); + // 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( + static_cast( m_indirectdraws.size() ), + prepassdraws, + std::chrono::duration( std::chrono::steady_clock::now() - cpustart ).count() ); +} + // 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 // local-space matrices; we compose each with `view * translate(track_origin - camera)` diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 4c22980a..52ca215b 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -38,6 +38,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46locallights.h" #include "rendering/opengl46/opengl46localshadows.h" #include "rendering/opengl46/opengl46instancecull.h" +#include "rendering/opengl46/opengl46hiz.h" // bare-bones render controller, in lack of anything better yet class opengl46_renderer : public gfx_renderer { @@ -333,6 +334,13 @@ class opengl46_renderer : public gfx_renderer { 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 ); + // draws the instances queued in the pass from the result of the gpu culling (Global.gfx_gpucull 1): makes the depth + // pyramid of the occlusion test, runs the culling, walks the submodel trees of the groups once per lod band to record + // the draws (m_indirectdraws, m_indirectcommands), has their instances filled in, then draws them with indirect draws, + // their depth first (Global.gfx_depthprepass) + void Render_Instances_Indirect(); + // 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 ); 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); @@ -483,11 +491,35 @@ class opengl46_renderer : public gfx_renderer { unsigned int m_instancerecordsversion { 1 }; unsigned int m_nodestateversion { 0 }; // scene::node_state_version which m_instancerecordsversion was last checked against // culling of instances in compute passes, run along the first color pass of the main viewport of a frame - // when Global.gfx_gpucull is set; for now its result is only compared with the one of the cpu + // when Global.gfx_gpucull is set: the instances of the pass are drawn from its result (1), or the result is compared + // with the one of the cpu (2) opengl46_instance_cull m_instancecull; bool m_instancecullpass { false }; // the current pass feeds it unsigned int m_instancecullframe { ~0u }; // frame it last ran in std::vector m_instance_bandcounts; // reported by Render_Instanced() for the comparison + // depth pyramid for the occlusion test of the gpu culling + opengl46_hiz m_hiz; + // a draw recorded from the walk of the submodel tree of a group in one lod band (Render_Instances_Indirect()), to be + // replayed once the instances of its command are known: the state the walk left for it and what it bound + struct indirect_draw { + gl::model_ubs ubs; + material_handle material; + TSubModel const *submodel; + gfx::geometry_handle geometry; + unsigned int type; // primitive + bool blending; // drawn with the transparency hack of submodels faded out + }; + std::vector m_indirectdraws; + std::vector m_indirectcommands; // in the order of m_indirectdraws + 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 + TSubModel const *m_indirectsubmodel { nullptr }; + // depth prepass: programs and the class of the material programs (see Prepass_Class()) + std::unique_ptr m_prepass_shader; + std::unique_ptr m_prepass_alpha_shader; + std::unordered_map m_prepassclasses; + gl::gpu_timer m_prepasstimer; renderpass_config m_colorpass; // parametrs of most recent color pass std::array m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages std::vector m_pickcontrolsitems; diff --git a/shaders/depthprepass.frag b/shaders/depthprepass.frag new file mode 100644 index 00000000..a5ce35d0 --- /dev/null +++ b/shaders/depthprepass.frag @@ -0,0 +1,7 @@ +// depth prepass of materials which never discard a fragment (no alpha test) + +in vec2 f_coord; + +void main() +{ +} diff --git a/shaders/depthprepass.vert b/shaders/depthprepass.vert new file mode 100644 index 00000000..e3975217 --- /dev/null +++ b/shaders/depthprepass.vert @@ -0,0 +1,20 @@ +// depth prepass of the instances drawn from the gpu culling (opengl46_renderer::Render_Instances_Indirect()). +// the position is worked out the way vertex.vert of the materials does it, and declared invariant in both, so the colour +// pass which follows gets the same depth for the same surface + +layout(location = 0) in vec3 v_vert; +layout(location = 2) in vec2 v_coord; + +#include + +invariant gl_Position; + +out vec2 f_coord; + +void main() +{ + mat4 mv = effective_modelview(); + vec4 position = mv * vec4(v_vert, 1.0); + gl_Position = projection * position; + f_coord = v_coord; +} diff --git a/shaders/depthprepass_alpha.frag b/shaders/depthprepass_alpha.frag new file mode 100644 index 00000000..cdab4c39 --- /dev/null +++ b/shaders/depthprepass_alpha.frag @@ -0,0 +1,16 @@ +// depth prepass of materials whose colour shader tests the alpha of the diffuse texture against the opacity of the +// material (mat_default.frag and the like). the test here is stricter by one step of 8-bit alpha: a fragment which writes +// depth here is never discarded by the colour pass, which would leave a hole with nothing drawn behind it + +in vec2 f_coord; + +#include + +#texture (diffuse, 0, sRGB_A) +uniform sampler2D diffuse; + +void main() +{ + if (texture(diffuse, f_coord).a < opacity + 1.0 / 255.0) + discard; +} diff --git a/shaders/hiz_build.comp b/shaders/hiz_build.comp new file mode 100644 index 00000000..e8af8901 --- /dev/null +++ b/shaders/hiz_build.comp @@ -0,0 +1,39 @@ +// one level of the depth pyramid of the occlusion test of the instances (opengl46_hiz): each texel holds the farthest +// depth of the texels of the level below it which it covers. the depth is reversed (1 near, 0 far), so the farthest is +// the smallest. the first level is made from the resolved depth of the scene, the others from the level below. +// a level halves the one below, rounding down; when that one has an odd size, the last row and column of the new level +// take its last row and column as well, so that nothing is left out + +#include + +layout(local_size_x = HIZ_GROUP_SIZE, local_size_y = HIZ_GROUP_SIZE) in; + +layout(binding = 0) uniform sampler2D source; +layout(r32f, binding = 0) writeonly uniform image2D target; + +// xy: size of the source level, zw: size of the target level +layout(location = 0) uniform ivec4 sizes; +// x: level of the source read +layout(location = 1) uniform ivec4 params; + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(texel, sizes.zw))) + return; + + ivec2 first = min(2 * texel, sizes.xy - 1); + ivec2 last = min(2 * texel + 1, sizes.xy - 1); + if (texel.x == sizes.z - 1) + last.x = sizes.x - 1; + if (texel.y == sizes.w - 1) + last.y = sizes.y - 1; + + float farthest = 1.0; + for (int y = first.y; y <= last.y; ++y) + { + for (int x = first.x; x <= last.x; ++x) + farthest = min(farthest, texelFetch(source, ivec2(x, y), params.x).r); + } + imageStore(target, texel, vec4(farthest)); +} diff --git a/shaders/instance_cull.comp b/shaders/instance_cull.comp index e0bfd728..6c925e79 100644 --- a/shaders/instance_cull.comp +++ b/shaders/instance_cull.comp @@ -1,7 +1,9 @@ // culls the model instances of the buckets queued in a render pass and assigns them their lod bands, with the tests // opengl46_renderer::Render_Instanced() makes on the cpu. one invocation per record of a queued bucket. // stage 0 counts the instances which pass, per entry (lod band of a group); after instance_cull_offsets.comp turned the -// counts into the first slots of the entries, stage 1 writes their modelview matrices there, in no particular order +// counts into the first slots of the entries, stage 1 writes their modelview matrices there, in no particular order. +// with the occlusion test on, an instance whose bounding sphere lies whole behind the depth already drawn in the pass +// (the farthest depth of the pyramid over the part of the view it takes, see hiz_build.comp) is left out as well #include @@ -75,6 +77,14 @@ layout(location = 7) uniform vec4 distance_params; layout(location = 8) uniform vec4 size_params; // x: jobs, y: records in all jobs, z: entries, w: stage layout(location = 9) uniform uvec4 counts; +// occlusion test: projection of the pass, x: on, y: levels of the pyramid, zw: size of its first level +layout(location = 10) uniform mat4 hiz_projection; +layout(location = 14) uniform ivec4 hiz_params; +// xy: size of the depth the pyramid was made from, pixels +layout(location = 15) uniform vec4 hiz_screen; + +// depth pyramid, farthest (smallest, reversed) depth per texel +layout(binding = 0) uniform sampler2D hiz; const uint INSTANCE_VISIBLE = 1u; const uint INSTANCE_INSTANCEABLE = 2u; @@ -97,6 +107,50 @@ uint find_job(uint candidate) return low; } +// whether a sphere (view space) lies whole behind the depth in the pyramid over the rectangle of the view it takes +bool occluded(vec3 center, float radius) +{ + vec2 low = vec2(1.0); + vec2 high = vec2(-1.0); + float nearest = 0.0; + for (int corner = 0; corner < 8; ++corner) + { + vec3 offset = vec3(((corner & 1) != 0) ? radius : -radius, ((corner & 2) != 0) ? radius : -radius, ((corner & 4) != 0) ? radius : -radius); + vec4 clip = hiz_projection * vec4(center + offset, 1.0); + // reaches the plane of the camera: no rectangle to test + if (clip.w <= 1e-3) + return false; + vec3 ndc = clip.xyz / clip.w; + low = min(low, ndc.xy); + high = max(high, ndc.xy); + nearest = max(nearest, ndc.z); + } + if (any(greaterThan(low, vec2(1.0))) || any(lessThan(high, vec2(-1.0))) || (nearest >= 1.0)) + return false; + + // pixels of the rectangle, then texels of the first level: each covers 2x2 pixels + ivec2 size = hiz_params.zw; + ivec2 pixelfirst = ivec2(clamp((low * 0.5 + 0.5) * hiz_screen.xy, vec2(0.0), hiz_screen.xy - 1.0)); + ivec2 pixellast = ivec2(clamp((high * 0.5 + 0.5) * hiz_screen.xy, vec2(0.0), hiz_screen.xy - 1.0)); + ivec2 first = min(pixelfirst / 2, size - 1); + ivec2 last = min(pixellast / 2, size - 1); + // the level where the rectangle takes at most 2x2 texels + int level = 0; + while ((level < hiz_params.y - 1) && any(greaterThan((last >> level) - (first >> level), ivec2(1)))) + ++level; + ivec2 levelsize = max(size >> level, ivec2(1)); + first = min(first >> level, levelsize - 1); + last = min(last >> level, levelsize - 1); + + float farthest = 1.0; + for (int y = first.y; y <= last.y; ++y) + { + for (int x = first.x; x <= last.x; ++x) + farthest = min(farthest, texelFetch(hiz, ivec2(x, y), level).r); + } + return nearest < farthest; +} + void main() { // the dispatch can be two dimensional when the records don't fit in the work group count limit of one @@ -128,6 +182,12 @@ void main() return; if (record.radius * record.radius * size_params.x / distancesquared < MIN_SCREEN_SIZE * MIN_SCREEN_SIZE) return; + if ((hiz_params.x != 0) && occluded((view * vec4(location, 1.0)).xyz, record.radius)) + { + if (counts.w == 0u) + atomicAdd(counters[3], 1u); + return; + } // band: the number of lod bounds the distance reached, like upper_bound over the sorted bounds instance_cull_group group = groups[job.group]; diff --git a/shaders/instance_cull_commands.comp b/shaders/instance_cull_commands.comp new file mode 100644 index 00000000..5c3204ad --- /dev/null +++ b/shaders/instance_cull_commands.comp @@ -0,0 +1,39 @@ +// fills in the instances of the indirect draw commands of the entries, after stage 1 of instance_cull.comp wrote the +// modelview matrices of the instances which passed. one invocation per command: the instance count is the count of its +// entry, the base instance the first slot of the entry in the output (its cursor, advanced by stage 1, less the count) + +#include + +layout(local_size_x = INSTANCE_COMMAND_GROUP_SIZE) in; + +layout(std430, binding = INSTANCE_COUNTER_SSBO_BINDING) readonly buffer instance_counter_ssbo +{ + uint counters[]; +}; + +// gl::instance_draw_command, eight uints each: count, instance count, first, base vertex (base instance of an array +// draw), base instance, entry, indexed, unused +layout(std430, binding = INSTANCE_COMMAND_SSBO_BINDING) buffer instance_command_ssbo +{ + uint commands[]; +}; + +// x: commands, y: entries +layout(location = 0) uniform uvec4 counts; + +void main() +{ + uint command = gl_GlobalInvocationID.x; + if (command >= counts.x) + return; + + uint field = command * 8u; + uint entry = commands[field + 5u]; + uint count = counters[INSTANCE_COUNTER_HEADER + entry]; + uint base = counters[INSTANCE_COUNTER_HEADER + counts.y + entry] - count; + commands[field + 1u] = count; + if (commands[field + 6u] != 0u) + commands[field + 4u] = base; + else + commands[field + 3u] = base; +} diff --git a/shaders/vertex.vert b/shaders/vertex.vert index fdf70dc3..7d5c8bfc 100644 --- a/shaders/vertex.vert +++ b/shaders/vertex.vert @@ -6,6 +6,9 @@ layout(location = 4) in vec4 v_userdata; #include +// the depth prepass (depthprepass.vert) works the position out the same way, and has to get the same depth +invariant gl_Position; + out vec3 f_normal; flat out vec3 f_normal_raw; out vec2 f_coord; diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 1ba3da8d..40028f27 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1428,8 +1428,21 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token } else if (Token == "gfx.gpucull") { + // yes: culled and drawn from the gpu, compare: culled on both, drawn by the cpu, anything else: cpu Parser.getTokens(1); - Parser >> gfx_gpucull; + std::string mode; + Parser >> mode; + gfx_gpucull = (mode == "yes" || mode == "true" || mode == "1") ? 1 : (mode == "compare" || mode == "2") ? 2 : 0; + } + else if (Token == "gfx.gpucull.occlusion") + { + Parser.getTokens(1); + Parser >> gfx_gpucull_occlusion; + } + else if (Token == "gfx.depthprepass") + { + Parser.getTokens(1); + Parser >> gfx_depthprepass; } else if (Token == "gfx.lights.clusterheatmap") { @@ -1855,7 +1868,9 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.envmap.enabled", gfx_envmap_enabled ); export_as_text( Output, "gfx.lights.clustered", gfx_lights_clustered ); export_as_text( Output, "gfx.lights.clusterheatmap", gfx_lights_clusterheatmap ); - export_as_text( Output, "gfx.gpucull", gfx_gpucull ); + export_as_text( Output, "gfx.gpucull", std::string{ gfx_gpucull == 1 ? "yes" : gfx_gpucull == 2 ? "compare" : "no" } ); + export_as_text( Output, "gfx.gpucull.occlusion", gfx_gpucull_occlusion ); + export_as_text( Output, "gfx.depthprepass", gfx_depthprepass ); export_as_text( Output, "gfx.postfx.motionblur.enabled", gfx_postfx_motionblur_enabled ); export_as_text( Output, "gfx.postfx.motionblur.shutter", gfx_postfx_motionblur_shutter ); // TODO: export gfx_postfx_motionblur_format diff --git a/utilities/Globals.h b/utilities/Globals.h index 5d9e5ec2..6ec01bf0 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -285,7 +285,11 @@ struct global_settings { bool gfx_envmap_enabled = true; bool gfx_lights_clustered = true; // headlights from the light lists of the clusters (no limit of lit headlights) or the first dynamiclights of them bool gfx_lights_clusterheatmap = false; // debug view of the light clusters - bool gfx_gpucull = false; // culls model instances of the main view in a compute pass too, and compares the result with the cpu (opengl 4.6) + // culling of the model instances of the main view (opengl 4.6): 0 on the cpu, 1 in a compute pass, which the instances are + // drawn from with indirect draws, 2 on the cpu, which draws, and in a compute pass too, for the results to be compared + int gfx_gpucull = 0; + bool gfx_gpucull_occlusion = true; // with gfx_gpucull 1: instances hidden behind the depth drawn before them are left out + bool gfx_depthprepass = true; // with gfx_gpucull 1: the depth of the instances is drawn before their colour bool gfx_postfx_motionblur_enabled = true; float gfx_postfx_motionblur_shutter = 0.01f; GLenum gfx_postfx_motionblur_format = GL_RG16F;