17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 08:39:19 +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

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