mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 07:29:20 +02:00
Draw instances from the GPU culling with indirect draws, occlusion culling and a depth prepass
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" +
|
||||
|
||||
25
gl/ubo.h
25
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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<basic_vertex>;
|
||||
using userdata_array = std::vector<vertex_userdata>;
|
||||
using index_array = std::vector<basic_index>;
|
||||
@@ -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 <typename Iterator_>
|
||||
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 <typename Iterator_>
|
||||
void draw( Iterator_ First, Iterator_ Last, unsigned int const Streams = basic_streams ) {
|
||||
while( First != Last ) {
|
||||
|
||||
@@ -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<int>(m_vertex_count * sizeof(gfx::basic_vertex) + chunkrecord.vertex_offset * sizeof(gfx::vertex_userdata)),
|
||||
static_cast<int>(chunkrecord.vertex_count * sizeof(gfx::vertex_userdata)) );
|
||||
}
|
||||
chunkrecord.is_good = true;
|
||||
}
|
||||
|
||||
Source.indexed = ( chunkrecord.index_count > 0 );
|
||||
Source.count = static_cast<std::uint32_t>( Source.indexed ? chunkrecord.index_count : chunkrecord.vertex_count );
|
||||
Source.first = static_cast<std::uint32_t>( Source.indexed ? chunkrecord.index_offset : chunkrecord.vertex_offset );
|
||||
Source.basevertex = static_cast<std::int32_t>( 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<void const *>( Offset ) );
|
||||
}
|
||||
else {
|
||||
::glDrawArraysIndirect( chunk.type, reinterpret_cast<void const *>( 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
119
rendering/opengl46/opengl46hiz.cpp
Normal file
119
rendering/opengl46/opengl46hiz.cpp
Normal file
@@ -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<gl::program>(std::vector<std::reference_wrapper<const gl::shader>>{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<double>(*time) / 1000000.0, 3) + " ms\n";
|
||||
return text;
|
||||
}
|
||||
68
rendering/opengl46/opengl46hiz.h
Normal file
68
rendering/opengl46/opengl46hiz.h
Normal file
@@ -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 <memory>
|
||||
#include <string>
|
||||
|
||||
#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<gl::program> 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;
|
||||
};
|
||||
@@ -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<GLsizeiptr>((gl::INSTANCE_COUNTER_HEADER + READBACK_ENTRIES) * sizeof(std::uint32_t))};
|
||||
m_records = std::make_unique<gl::ssbo>(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<gl::ssbo>(readbacksize, gl::INSTANCE_COUNTER_SSBO_BINDING);
|
||||
m_output = std::make_unique<gl::ssbo>(65536 * sizeof(glm::mat4), gl::INSTANCE_OUTPUT_SSBO_BINDING);
|
||||
m_commands = std::make_unique<gl::ssbo>(4096 * sizeof(gl::instance_draw_command), gl::INSTANCE_COMMAND_SSBO_BINDING);
|
||||
m_readback = std::make_unique<gl::readback>(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<float>(0.999 * (nearest * nearest * m_setup.distancescale + m_setup.distanceoffset))};
|
||||
auto const distancemax{static_cast<float>(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<std::uint32_t>(m_lodlist.size()) - lodfirst};
|
||||
|
||||
group_data const group{static_cast<std::uint32_t>(m_grouplist.size()), m_entries, lodcount + 1};
|
||||
group_data const group{static_cast<std::uint32_t>(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<gl::instance_draw_command> const &Commands)
|
||||
{
|
||||
m_commandcount = static_cast<std::uint32_t>(Commands.size());
|
||||
if (!m_dispatched || Commands.empty())
|
||||
return;
|
||||
|
||||
auto const size{static_cast<GLsizeiptr>(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<std::uint32_t> 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<std::uint32_t>({result.entries, READBACK_ENTRIES, static_cast<std::uint32_t>(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<double>(*time) / 1000000.0, 3) + " ms\n";
|
||||
|
||||
@@ -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<draw_group> const &draw_groups() const
|
||||
{
|
||||
return m_drawgroups;
|
||||
}
|
||||
std::vector<float> 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<gl::instance_draw_command> 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<std::uint32_t>(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<std::uint32_t> 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<gl::program> m_cull_program;
|
||||
std::unique_ptr<gl::program> m_offsets_program;
|
||||
std::unique_ptr<gl::program> m_commands_program;
|
||||
std::unique_ptr<gl::ssbo> m_records;
|
||||
std::unique_ptr<gl::ssbo> m_jobs;
|
||||
std::unique_ptr<gl::ssbo> m_groups;
|
||||
std::unique_ptr<gl::ssbo> m_lods;
|
||||
std::unique_ptr<gl::ssbo> m_counters;
|
||||
std::unique_ptr<gl::ssbo> m_output;
|
||||
std::unique_ptr<gl::ssbo> m_commands;
|
||||
std::unique_ptr<gl::readback> m_readback;
|
||||
gl::gpu_timer m_timer;
|
||||
|
||||
@@ -128,6 +187,7 @@ class opengl46_instance_cull
|
||||
std::vector<scene::basic_cell::instance_bucket const *> m_buckets; // of the jobs, in their order
|
||||
std::vector<gl::instance_cull_job> m_joblist;
|
||||
std::vector<gl::instance_cull_group> m_grouplist;
|
||||
std::vector<draw_group> m_drawgroups;
|
||||
std::vector<float> m_lodlist;
|
||||
std::unordered_map<scene::basic_cell::instance_bucket_key, group_data, scene::basic_cell::instance_bucket_key_hash> m_groupmap;
|
||||
std::vector<TSubModel const *> 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
|
||||
};
|
||||
|
||||
@@ -167,6 +167,8 @@ bool opengl46_renderer::Init(GLFWwindow *Window)
|
||||
m_cluster_cull_shader = std::make_unique<gl::program>(std::vector<std::reference_wrapper<const gl::shader>>{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<double>( *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<float>( sq( Global.reflectiontune.range_instances ) ) : std::numeric_limits<float>::max() );
|
||||
setup.drawrange = static_cast<float>( 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<std::uint32_t>( 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<queued_instance> *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<queued_in
|
||||
m_renderpass.draw_stats.models += static_cast<int>( 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_<name>.frag
|
||||
static std::unordered_set<std::string> 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<std::string> 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<float>::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<std::intptr_t>( 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<int>( m_indirectdraws.size() );
|
||||
m_renderpass.draw_stats.drawcalls += static_cast<int>( prepassdraws );
|
||||
// the count of the frame the gpu reported last
|
||||
m_renderpass.draw_stats.instances += static_cast<int>( m_instancecull.last_count() );
|
||||
m_renderpass.draw_stats.models += static_cast<int>( m_instancecull.last_count() );
|
||||
m_instancecull.draw_result(
|
||||
static_cast<std::uint32_t>( m_indirectdraws.size() ),
|
||||
prepassdraws,
|
||||
std::chrono::duration<double, std::milli>( 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)`
|
||||
|
||||
@@ -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<std::uint32_t> 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<indirect_draw> m_indirectdraws;
|
||||
std::vector<gl::instance_draw_command> 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<gl::program> m_prepass_shader;
|
||||
std::unique_ptr<gl::program> m_prepass_alpha_shader;
|
||||
std::unordered_map<gl::program const *, int> m_prepassclasses;
|
||||
gl::gpu_timer m_prepasstimer;
|
||||
renderpass_config m_colorpass; // parametrs of most recent color pass
|
||||
std::array<renderpass_config, 3> m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages
|
||||
std::vector<TSubModel const *> m_pickcontrolsitems;
|
||||
|
||||
7
shaders/depthprepass.frag
Normal file
7
shaders/depthprepass.frag
Normal file
@@ -0,0 +1,7 @@
|
||||
// depth prepass of materials which never discard a fragment (no alpha test)
|
||||
|
||||
in vec2 f_coord;
|
||||
|
||||
void main()
|
||||
{
|
||||
}
|
||||
20
shaders/depthprepass.vert
Normal file
20
shaders/depthprepass.vert
Normal file
@@ -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 <common>
|
||||
|
||||
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;
|
||||
}
|
||||
16
shaders/depthprepass_alpha.frag
Normal file
16
shaders/depthprepass_alpha.frag
Normal file
@@ -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 <common>
|
||||
|
||||
#texture (diffuse, 0, sRGB_A)
|
||||
uniform sampler2D diffuse;
|
||||
|
||||
void main()
|
||||
{
|
||||
if (texture(diffuse, f_coord).a < opacity + 1.0 / 255.0)
|
||||
discard;
|
||||
}
|
||||
39
shaders/hiz_build.comp
Normal file
39
shaders/hiz_build.comp
Normal file
@@ -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 <common>
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -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 <common>
|
||||
|
||||
@@ -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];
|
||||
|
||||
39
shaders/instance_cull_commands.comp
Normal file
39
shaders/instance_cull_commands.comp
Normal file
@@ -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 <common>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -6,6 +6,9 @@ layout(location = 4) in vec4 v_userdata;
|
||||
|
||||
#include <common>
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user