17
0
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:
2026-10-16 12:54:52 +02:00
parent 2f0a06b98e
commit 466cb5339e
23 changed files with 1002 additions and 32 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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" +

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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 ) {

View File

@@ -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

View File

@@ -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;

View 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;
}

View 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;
};

View File

@@ -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";

View File

@@ -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
};

View File

@@ -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)`

View File

@@ -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;

View 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
View 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;
}

View 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
View 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));
}

View File

@@ -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];

View 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;
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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;