From 4d3a401aae3bbb28a1a25d0bb2fb3809fadd6dba Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Fri, 16 Oct 2026 01:08:44 +0200 Subject: [PATCH] Cull and assign LOD to instances in a compute pass --- CMakeLists.txt | 1 + gl/glsl_common.cpp | 9 + gl/ubo.h | 39 +++ rendering/frustum.h | 4 + rendering/opengl46/opengl46instancecull.cpp | 312 ++++++++++++++++++++ rendering/opengl46/opengl46instancecull.h | 144 +++++++++ rendering/opengl46/opengl46renderer.cpp | 67 ++++- rendering/opengl46/opengl46renderer.h | 10 +- rendering/openglcamera.h | 3 + scene/scene.h | 11 +- shaders/instance_cull.comp | 151 ++++++++++ shaders/instance_cull_offsets.comp | 53 ++++ utilities/Globals.cpp | 6 + utilities/Globals.h | 1 + 14 files changed, 805 insertions(+), 6 deletions(-) create mode 100644 rendering/opengl46/opengl46instancecull.cpp create mode 100644 rendering/opengl46/opengl46instancecull.h create mode 100644 shaders/instance_cull.comp create mode 100644 shaders/instance_cull_offsets.comp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f58ced9..546ae3b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -338,6 +338,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46exposure.cpp" "rendering/opengl46/opengl46ao.cpp" "rendering/opengl46/opengl46locallights.cpp" + "rendering/opengl46/opengl46instancecull.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" "rendering/opengl46/opengl46terrain.cpp" diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 27d1f540..4cffe0d1 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -15,6 +15,15 @@ void gl::glsl_common_setup() "#define MAX_CASCADES " + std::to_string(MAX_CASCADES) + "U\n" + "#define MAX_PARAMS " + std::to_string(MAX_PARAMS) + "U\n" + "#define INSTANCE_SSBO_BINDING " + std::to_string(INSTANCE_SSBO_BINDING) + "\n" + + "#define INSTANCE_RECORD_SSBO_BINDING " + std::to_string(INSTANCE_RECORD_SSBO_BINDING) + "\n" + + "#define INSTANCE_JOB_SSBO_BINDING " + std::to_string(INSTANCE_JOB_SSBO_BINDING) + "\n" + + "#define INSTANCE_GROUP_SSBO_BINDING " + std::to_string(INSTANCE_GROUP_SSBO_BINDING) + "\n" + + "#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_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_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" + "#define CLUSTER_GRID_Y " + std::to_string(CLUSTER_GRID_Y) + "\n" + diff --git a/gl/ubo.h b/gl/ubo.h index 18d7105a..200e10cd 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -374,5 +374,44 @@ namespace gl static_assert(sizeof(atmosphere_result) == 16 * (SKY_SH_COEFFICIENTS + 4), "bad size of atmosphere_result"); + // culling of model instances in compute passes (instance_cull.comp, instance_cull_offsets.comp). binding points of + // the shader storage blocks, apart from the ones bound for the shading so the passes don't disturb them + const GLuint INSTANCE_RECORD_SSBO_BINDING = 9; // scene::basic_cell::instance_record of the queued buckets + const GLuint INSTANCE_JOB_SSBO_BINDING = 10; // instance_cull_job, one per queued bucket + const GLuint INSTANCE_GROUP_SSBO_BINDING = 11; // instance_cull_group, one per model and skins + 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_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. + // 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; + + // layout std430: a bucket of a scene cell queued in the pass (GLSL: instance_cull_job) + struct instance_cull_job + { + packed_vec3 origin; // origin of the records of the bucket, relative to the camera of the pass + uint32_t first; // first record of the bucket in the record buffer + packed_vec3 distanceorigin; // the same origin, relative to the camera the distance is measured from + uint32_t count; // records of the bucket + uint32_t group; + uint32_t candidatestart; // records of the jobs before this one + uint32_t pad[2]; + }; + + static_assert(sizeof(instance_cull_job) == 48, "bad size of instance_cull_job"); + + // layout std430: instances of one model with one set of skins, the unit drawn together (GLSL: instance_cull_group) + struct instance_cull_group + { + uint32_t lodfirst; // first bound in the lod buffer + uint32_t lodcount; // bounds; the group has lodcount + 1 bands + uint32_t entryfirst; // first entry of the counters + uint32_t pad; + }; + + static_assert(sizeof(instance_cull_group) == 16, "bad size of instance_cull_group"); + #pragma pack(pop) } diff --git a/rendering/frustum.h b/rendering/frustum.h index b12a7c58..9a706aa5 100644 --- a/rendering/frustum.h +++ b/rendering/frustum.h @@ -67,6 +67,10 @@ public: cube_inside( float3 const &Center, float const Size ) const { return cube_inside( Center.x, Center.y, Center.z, Size ); } bool cube_inside( float const X, float const Y, float const Z, float const Size ) const; + // A, B, C and D of the planes, in world coordinates + inline + auto const & + planes() const { return m_frustum; } private: // types: diff --git a/rendering/opengl46/opengl46instancecull.cpp b/rendering/opengl46/opengl46instancecull.cpp new file mode 100644 index 00000000..0f30ac0e --- /dev/null +++ b/rendering/opengl46/opengl46instancecull.cpp @@ -0,0 +1,312 @@ +/* +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/opengl46instancecull.h" + +#include "model/Model3d.h" +#include "rendering/frustum.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +namespace +{ + +constexpr GLsizeiptr RECORD_SIZE{sizeof(scene::basic_cell::instance_record)}; + +// grows a buffer to hold at least Size bytes, at least doubling it. a new buffer drops the contents and the binding +void reserve(gl::ssbo &Buffer, GLsizeiptr const Size) +{ + if (Size > Buffer.size()) + Buffer.resize(std::max(Size, 2 * Buffer.size())); +} + +} // namespace + +void opengl46_instance_cull::init() +{ + auto const make_compute = [](std::string const &Name) { + gl::shader shader(Name); + return std::make_unique(std::vector>{shader}); + }; + m_cull_program = make_compute("instance_cull.comp"); + m_offsets_program = make_compute("instance_cull_offsets.comp"); + + auto const readbacksize{static_cast((gl::INSTANCE_COUNTER_HEADER + READBACK_ENTRIES) * sizeof(std::uint32_t))}; + m_records = std::make_unique(65536 * RECORD_SIZE, gl::INSTANCE_RECORD_SSBO_BINDING); + m_jobs = std::make_unique(4096 * sizeof(gl::instance_cull_job), gl::INSTANCE_JOB_SSBO_BINDING); + m_groups = std::make_unique(1024 * sizeof(gl::instance_cull_group), gl::INSTANCE_GROUP_SSBO_BINDING); + m_lods = std::make_unique(4096 * sizeof(float), gl::INSTANCE_LOD_SSBO_BINDING); + // the readback copies a fixed size from it + m_counters = std::make_unique(readbacksize, gl::INSTANCE_COUNTER_SSBO_BINDING); + m_output = std::make_unique(65536 * sizeof(glm::mat4), gl::INSTANCE_OUTPUT_SSBO_BINDING); + m_readback = std::make_unique(readbacksize); + m_readbackdata.resize(gl::INSTANCE_COUNTER_HEADER + READBACK_ENTRIES); +} + +void opengl46_instance_cull::begin_pass(pass_setup const &Setup) +{ + collect(); + + m_inpass = true; + m_dispatched = false; + m_setup = Setup; + m_buckets.clear(); + m_joblist.clear(); + m_grouplist.clear(); + m_lodlist.clear(); + m_groupmap.clear(); + m_cpucounts.clear(); + m_candidates = 0; + m_entries = 0; +} + +void opengl46_instance_cull::add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket) +{ + if (!m_inpass || Bucket.records.empty() || Key.pModel == nullptr) + return; + + auto lookup{m_groupmap.find(Key)}; + if (lookup == m_groupmap.end()) + { + // lod bounds of the model, the same set Render_Instanced() splits the distance into bands with + auto const lodfirst{static_cast(m_lodlist.size())}; + m_pending.clear(); + m_pending.push_back(Key.pModel->Root); + while (!m_pending.empty()) + { + auto const *submodel{m_pending.back()}; + m_pending.pop_back(); + if (submodel == nullptr) + continue; + if (submodel->fSquareMinDist > 0.f) + m_lodlist.emplace_back(submodel->fSquareMinDist); + if (submodel->fSquareMaxDist < std::numeric_limits::max()) + m_lodlist.emplace_back(submodel->fSquareMaxDist); + m_pending.push_back(submodel->Child); + m_pending.push_back(submodel->Next); + } + std::sort(m_lodlist.begin() + lodfirst, m_lodlist.end()); + m_lodlist.erase(std::unique(m_lodlist.begin() + lodfirst, m_lodlist.end()), m_lodlist.end()); + auto const lodcount{static_cast(m_lodlist.size()) - lodfirst}; + + group_data const group{static_cast(m_grouplist.size()), m_entries, lodcount + 1}; + m_grouplist.push_back({lodfirst, lodcount, m_entries, 0}); + m_entries += group.entrycount; + lookup = m_groupmap.emplace(Key, group).first; + } + + place(Bucket); + + gl::instance_cull_job job{}; + job.origin = packed_vec3{Bucket.recordorigin - m_setup.camera}; + job.first = Bucket.gpufirst; + job.distanceorigin = packed_vec3{Bucket.recordorigin - m_setup.distancecamera}; + job.count = static_cast(Bucket.records.size()); + job.group = lookup->second.index; + job.candidatestart = m_candidates; + m_joblist.push_back(job); + m_buckets.push_back(&Bucket); + m_candidates += job.count; +} + +void opengl46_instance_cull::place(scene::basic_cell::instance_bucket const &Bucket) +{ + auto const count{static_cast(Bucket.records.size())}; + auto const current{Bucket.gpugeneration == m_recordgeneration}; + if (current && !Bucket.gpustale) + return; + if (current && count <= Bucket.gpucapacity) + { + m_records->update(Bucket.records.data(), static_cast(Bucket.gpufirst) * RECORD_SIZE, static_cast(count) * RECORD_SIZE); + Bucket.gpustale = false; + return; + } + + // a new place, with some room for instances the editor adds + auto const allocate = [this](scene::basic_cell::instance_bucket const &Target) { + auto const records{static_cast(Target.records.size())}; + Target.gpufirst = m_recordfill; + Target.gpucapacity = records + records / 4; + Target.gpugeneration = m_recordgeneration; + Target.gpustale = false; + m_recordfill += Target.gpucapacity; + m_records->update(Target.records.data(), static_cast(Target.gpufirst) * RECORD_SIZE, static_cast(records) * RECORD_SIZE); + }; + + auto const capacity{static_cast(m_records->size() / RECORD_SIZE)}; + if (m_recordfill + count + count / 4 > capacity) + { + // the buffer is started over, and only the buckets of this pass get places again. the places left behind by + // buckets which were made anew or are out of sight are dropped this way + GLsizeiptr live{count + count / 4}; + for (auto const *bucket : m_buckets) + live += bucket->records.size() + bucket->records.size() / 4; + if (2 * live * RECORD_SIZE > m_records->size()) + m_records->resize(std::max(2 * live * RECORD_SIZE, 2 * m_records->size())); + ++m_recordgeneration; + m_recordfill = 0; + for (std::size_t job = 0; job < m_buckets.size(); ++job) + { + allocate(*m_buckets[job]); + m_joblist[job].first = m_buckets[job]->gpufirst; + } + } + allocate(Bucket); +} + +void opengl46_instance_cull::dispatch(glm::mat4 const &View) +{ + if (!m_inpass || m_joblist.empty()) + return; + + auto const countersize{static_cast((gl::INSTANCE_COUNTER_HEADER + 2 * m_entries) * sizeof(std::uint32_t))}; + reserve(*m_jobs, static_cast(m_joblist.size() * sizeof(gl::instance_cull_job))); + reserve(*m_groups, static_cast(m_grouplist.size() * sizeof(gl::instance_cull_group))); + reserve(*m_lods, static_cast(std::max(m_lodlist.size(), 1) * sizeof(float))); + reserve(*m_counters, countersize); + reserve(*m_output, static_cast(m_candidates) * static_cast(sizeof(glm::mat4))); + + m_jobs->update(m_joblist.data(), 0, static_cast(m_joblist.size() * sizeof(gl::instance_cull_job))); + m_groups->update(m_grouplist.data(), 0, static_cast(m_grouplist.size() * sizeof(gl::instance_cull_group))); + if (!m_lodlist.empty()) + m_lods->update(m_lodlist.data(), 0, static_cast(m_lodlist.size() * sizeof(float))); + // counts and cursors start at zero + glClearNamedBufferSubData(*m_counters, GL_R32UI, 0, countersize, GL_RED_INTEGER, GL_UNSIGNED_INT, nullptr); + std::uint32_t const header[gl::INSTANCE_COUNTER_HEADER]{m_setup.frame, 0, m_entries, 0}; + m_counters->update(header, 0, sizeof(header)); + + m_records->bind_base(); + m_jobs->bind_base(); + m_groups->bind_base(); + m_lods->bind_base(); + m_counters->bind_base(); + m_output->bind_base(); + + // planes moved to the camera of the pass, in double precision: the cpu tests world coordinates + auto const &frustumplanes{m_setup.frustum->planes()}; + glm::vec4 planes[6]; + for (int plane = 0; plane < 6; ++plane) + { + auto const *source{frustumplanes[plane]}; + planes[plane] = glm::vec4{source[0], source[1], source[2], + static_cast(source[0] * m_setup.camera.x + source[1] * m_setup.camera.y + source[2] * m_setup.camera.z + source[3])}; + } + glProgramUniformMatrix4fv(*m_cull_program, 0, 1, GL_FALSE, glm::value_ptr(View)); + glProgramUniform4fv(*m_cull_program, 1, 6, glm::value_ptr(planes[0])); + 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); + + // 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}; + auto const groups_x{std::min(groups, 65535)}; + auto const groups_y{(groups + groups_x - 1) / groups_x}; + + m_timer.begin(); + glProgramUniform4ui(*m_cull_program, 9, static_cast(m_joblist.size()), m_candidates, m_entries, 0); + m_cull_program->dispatch(groups_x, groups_y); + gl::memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT); + m_offsets_program->dispatch(1); + gl::memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT); + glProgramUniform4ui(*m_cull_program, 9, static_cast(m_joblist.size()), m_candidates, m_entries, 1); + m_cull_program->dispatch(groups_x, groups_y); + // 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(); + + m_readback->push(*m_counters); + m_cpucounts.assign(m_entries, 0); + m_dispatched = true; +} + +void opengl46_instance_cull::cpu_result(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts) +{ + if (!m_dispatched) + return; + auto const lookup{m_groupmap.find(Key)}; + if (lookup == m_groupmap.end()) + return; + auto const &group{lookup->second}; + for (std::size_t band = 0; band < std::min(Bandcounts.size(), group.entrycount); ++band) + m_cpucounts[group.entryfirst + band] += Bandcounts[band]; +} + +void opengl46_instance_cull::end_pass() +{ + if (m_dispatched) + { + std::uint64_t total{0}; + for (auto const count : m_cpucounts) + total += count; + m_cpupasses.push_back({m_setup.frame, m_cpucounts, total}); + // the gpu is never this far behind + while (m_cpupasses.size() > 8) + m_cpupasses.pop_front(); + } + m_inpass = false; + m_dispatched = false; +} + +void opengl46_instance_cull::collect() +{ + if (!m_readback || !m_readback->pop(m_readbackdata.data())) + 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.compared = std::min({result.entries, READBACK_ENTRIES, static_cast(cpu.counts.size())}); + for (std::uint32_t entry = 0; entry < result.compared; ++entry) + { + auto const gpu{m_readbackdata[gl::INSTANCE_COUNTER_HEADER + entry]}; + auto const difference{gpu > cpu.counts[entry] ? gpu - cpu.counts[entry] : cpu.counts[entry] - gpu}; + if (difference > 0) + { + ++result.mismatched; + result.largestdifference = std::max(result.largestdifference, difference); + } + } + m_cpupasses.pop_front(); + + m_last = result; + ++m_comparedframes; + if ((result.mismatched > 0) || (result.gputotal != result.cputotal) || (result.entries != cpu.counts.size())) + ++m_mismatchedframes; +} + +std::string opengl46_instance_cull::info() +{ + if (!Global.gfx_gpucull) + return {}; + 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, " + + std::to_string(m_last.mismatched) + " differ (by up to " + std::to_string(m_last.largestdifference) + "); frames differing " + std::to_string(m_mismatchedframes) + " of " + + std::to_string(m_comparedframes); +} + +std::string opengl46_instance_cull::info_times() +{ + std::string text; + if (!Global.gfx_gpucull) + return text; + if (auto const time = m_timer.result()) + text += " instance cull: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; + return text; +} diff --git a/rendering/opengl46/opengl46instancecull.h b/rendering/opengl46/opengl46instancecull.h new file mode 100644 index 00000000..cd0c88c7 --- /dev/null +++ b/rendering/opengl46/opengl46instancecull.h @@ -0,0 +1,144 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "gl/shader.h" +#include "gl/ssbo.h" +#include "gl/readback.h" +#include "gl/query.h" +#include "gl/ubo.h" +#include "scene/scene.h" + +class cFrustum; + +// 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(): +// - the records of the instances (scene::basic_cell::instance_record) are kept in one buffer, each bucket of a scene cell +// in its own place, and sent again only when the bucket made them anew +// - every bucket queued in the pass becomes a job; the buckets of one model and one set of skins make a group, whose +// 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) +class opengl46_instance_cull +{ + public: + // what a pass needs for the tests, the way Render_Instanced() takes it + struct pass_setup + { + unsigned int frame{0}; + glm::dvec3 camera{0.0}; // camera of the pass: frustum and placement + glm::dvec3 distancecamera{0.0}; // the distance is measured from it + cFrustum const *frustum{nullptr}; // of the pass camera, in world coordinates + float distancescale{1.f}; // of the squared distance + float distanceoffset{0.f}; // added to the scaled squared distance past the range test + float distancerange{0.f}; // squared range of the scaled distance before the offset + float drawrange{0.f}; // squared draw range + float zoom{1.f}; + }; + + opengl46_instance_cull() = default; + opengl46_instance_cull(const opengl46_instance_cull &) = delete; + opengl46_instance_cull &operator=(const opengl46_instance_cull &) = delete; + + // creates the programs and buffers. throws gl::shader_exception + void init(); + // starts collecting the buckets of a pass + void begin_pass(pass_setup const &Setup); + bool in_pass() const + { + 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); + // counts per lod band of the instances Render_Instanced() drew for a model and its skins in this pass + void cpu_result(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts); + // ends the pass: its cpu counts are kept for the comparison + void end_pass(); + // lines for the debug panel + std::string info(); + std::string info_times(); + + private: + struct group_data + { + std::uint32_t index; + std::uint32_t entryfirst; + std::uint32_t entrycount; + }; + // counts of the cpu of a pass, waiting for those of the gpu + struct cpu_pass + { + unsigned int frame; + std::vector counts; + std::uint64_t total; + }; + struct comparison + { + unsigned int frame{0}; + std::uint64_t gputotal{0}; + std::uint64_t cputotal{0}; + std::uint32_t entries{0}; + std::uint32_t compared{0}; // entries compared, at most READBACK_ENTRIES + std::uint32_t mismatched{0}; + std::uint32_t largestdifference{0}; + }; + + // finds a place for the records of a bucket in the record buffer and sends them + void place(scene::basic_cell::instance_bucket const &Bucket); + // takes the newest counts of the gpu which arrived and compares them with the ones of the cpu + void collect(); + + // entries read back for the comparison; with more of them only the totals and these are compared + static constexpr std::uint32_t READBACK_ENTRIES{16384}; + + std::unique_ptr m_cull_program; + std::unique_ptr m_offsets_program; + std::unique_ptr m_records; + std::unique_ptr m_jobs; + std::unique_ptr m_groups; + std::unique_ptr m_lods; + std::unique_ptr m_counters; + std::unique_ptr m_output; + std::unique_ptr m_readback; + gl::gpu_timer m_timer; + + // record buffer: records placed so far, the generation which tells current places from the ones of a replaced buffer + std::uint32_t m_recordfill{0}; + unsigned int m_recordgeneration{1}; + + bool m_inpass{false}; + pass_setup m_setup; + std::vector m_buckets; // of the jobs, in their order + std::vector m_joblist; + std::vector m_grouplist; + std::vector m_lodlist; + std::unordered_map m_groupmap; + std::vector m_pending; // walk of the submodel tree + std::uint32_t m_candidates{0}; + std::uint32_t m_entries{0}; + bool m_dispatched{false}; + + std::vector m_cpucounts; // of the current pass, by entry + std::deque m_cpupasses; + std::vector m_readbackdata; + comparison m_last; + std::uint64_t m_comparedframes{0}; + std::uint64_t m_mismatchedframes{0}; +}; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index d007d22d..dd0027b2 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -188,6 +188,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) try { m_sky.init(); + m_instancecull.init(); m_exposure.init(); m_ao.init(); } @@ -721,6 +722,7 @@ void opengl46_renderer::SwapBuffers() m_debugtimestext += m_sky.info_times(); m_debugtimestext += m_exposure.info_times(); m_debugtimestext += m_ao.info_times(); + m_debugtimestext += m_instancecull.info_times(); m_debugtimestext += "uilayer: " + to_string( Timer::subsystem.gfx_gui.average(), 2 ) + " ms\n"; if( DebugModeFlag ) m_debugtimestext += m_textures.info(); @@ -762,7 +764,8 @@ void opengl46_renderer::SwapBuffers() + "\nlights: " + to_string( static_cast( m_lights_lit ), 7 ) + " (" + std::to_string( m_lights_dropped ) + " over capacity), " + ( Global.gfx_lights_clustered ? "clustered" : "first " + std::to_string( std::max( Global.DynamicLightCount, 0 ) ) ) - + "\n" + m_locallights.info(); + + "\n" + m_locallights.info() + + m_instancecull.info(); if( auto const cullstats = ( m_viewports.empty() || !( Global.gfx_lights_clustered || Global.gfx_lights_clusterheatmap ) ? std::nullopt : m_viewports.front()->clusters->stats() ) ) { m_debugstatstext += "\ncluster lists: " + std::to_string( cullstats->index_count ) @@ -823,6 +826,15 @@ void opengl46_renderer::draw_debug_ui() } ImGui::End(); + if (ImGui::Begin("Instance culling")) + { + ImGui::Checkbox("cull instances on the gpu too, compare with the cpu (gfx.gpucull)", &Global.gfx_gpucull); + auto const info{m_instancecull.info()}; + if (!info.empty()) + ImGui::TextUnformatted(info.c_str() + 1); + } + ImGui::End(); + ImGui::SetNextWindowSize(ImVec2S(400, 400)); if (ImGui::Begin("Pickbuffer") && m_pick_tex) { ImGui::Image((ImTextureID)(intptr_t)(m_pick_tex->id), ImGui::GetContentRegionAvail(), ImVec2(0, 1.0), ImVec2(1.0, 0)); @@ -2548,6 +2560,30 @@ 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 + m_instancecullpass = false; + if( ( Global.gfx_gpucull ) + && ( m_renderpass.draw_mode == rendermode::color ) + && ( m_current_viewport != nullptr ) + && ( m_current_viewport->main ) + && ( m_instancecullframe != m_framestamp ) ) { + m_instancecullframe = m_framestamp; + m_instancecullpass = true; + // the same choices as in Render_Instanced() + auto const reflections { m_renderpass.draw_mode == rendermode::reflections }; + opengl46_instance_cull::pass_setup setup; + setup.frame = m_framestamp; + setup.camera = m_renderpass.pass_camera.position(); + setup.distancecamera = ( m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera.position() : m_renderpass.pass_camera.position() ); + setup.frustum = &m_renderpass.pass_camera.frustum(); + setup.distancescale = ( reflections ? 1.f : static_cast( 1.0 / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) * static_cast( Global.fDistanceFactor ) ) ) ); + setup.distanceoffset = ( reflections ? static_cast( sq( EU07_REFLECTIONFIDELITYOFFSET ) ) : 0.f ); + setup.distancerange = ( reflections ? static_cast( sq( Global.reflectiontune.range_instances ) ) : std::numeric_limits::max() ); + setup.drawrange = static_cast( sq( m_renderpass.draw_range + 250 ) ); + setup.zoom = Global.ZoomFactor; + m_instancecull.begin_pass( setup ); + } // build a list of region sections to render glm::vec3 const cameraposition{m_renderpass.pass_camera.position()}; auto const camerax = static_cast(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); @@ -2982,11 +3018,22 @@ void opengl46_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // still happens inside Render_Instanced(), so correctness is unchanged -- only // 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 ) ); + } for( auto &bucket : m_frame_instance_buckets ) { if( bucket.second.empty() ) { continue; } - Render_Instanced( bucket.first.pModel, bucket.second ); + Render_Instanced( bucket.first.pModel, bucket.second, ( m_instancecullpass ? &m_instance_bandcounts : nullptr ) ); + if( m_instancecullpass ) { + m_instancecull.cpu_result( bucket.first, m_instance_bandcounts ); + } bucket.second.clear(); } + if( m_instancecullpass ) { + m_instancecull.end_pass(); + m_instancecullpass = false; + } } void opengl46_renderer::Draw_Geometry(std::vector::iterator begin, std::vector::iterator end) @@ -3221,6 +3268,10 @@ void opengl46_renderer::Queue_Instances( scene::basic_cell const *Cell ) || ( instancebucket.records.size() != instancebucket.instances.size() ) ) { Update_Instance_Records( bucket.first.pModel, instancebucket, Cell->m_area.center ); } + if( m_instancecullpass ) { + // the gpu tests every instance of the bucket, the way Render_Instanced() does + m_instancecull.add_bucket( bucket.first, instancebucket ); + } auto const &records { instancebucket.records }; std::vector *dest { nullptr }; // looked up when there's something to put in it for( std::size_t idx = 0; idx < records.size(); ++idx ) { @@ -3270,7 +3321,7 @@ void opengl46_renderer::Update_Instance_Records( TModel3d const *Model, scene::b for( auto *instance : Bucket.instances ) { scene::basic_cell::instance_record record; if( instance != nullptr ) { - record.position = glm::vec3{ instance->location() - Origin }; + record.position = packed_vec3{ instance->location() - Origin }; record.radius = instance->radius(); record.rangesquaredmin = tofloat( instance->m_rangesquaredmin ); record.rangesquaredmax = tofloat( instance->m_rangesquaredmax ); @@ -3286,6 +3337,7 @@ void opengl46_renderer::Update_Instance_Records( TModel3d const *Model, scene::b Bucket.records.emplace_back( record ); } Bucket.recordsversion = m_instancerecordsversion; + Bucket.gpustale = true; } // places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them @@ -3315,8 +3367,9 @@ std::uint32_t opengl46_renderer::Upload_Instance_Modelviews( glm::mat4 const *Mo // instance_modelview[i] is the camera-space root transform of an instance (precomputed below) // and model_ubs.modelview is the submodel-local chain (accumulated by the matrix stack starting // from identity, NOT the camera/instance frame). -void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector const &Instances ) +void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector const &Instances, std::vector *Bandcounts ) { + if( Bandcounts != nullptr ) { Bandcounts->clear(); } if( Model == nullptr ) { return; } if( Instances.empty() ) { return; } @@ -3353,6 +3406,7 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vectorassign( bandcount, 0 ); } // 1. Visibility / distance cull, by the records of the instances. Collect surviving instances with // their precomputed camera-space root modelview matrices and lod bands. m_instance_survivors and the @@ -3456,6 +3510,11 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector( m_instance_bandstarts[ ( band + 1 ) * distanceslots ] - m_instance_bandstarts[ band * distanceslots ] ); + } + } // all bands go to the instance buffer at once, each band is drawn from its own base instance auto const baseinstance { Upload_Instance_Modelviews( m_instance_modelviews.data(), m_instance_modelviews.size() ) }; diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 9ebce345..d2838c95 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -36,6 +36,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46exposure.h" #include "rendering/opengl46/opengl46ao.h" #include "rendering/opengl46/opengl46locallights.h" +#include "rendering/opengl46/opengl46instancecull.h" // bare-bones render controller, in lack of anything better yet class opengl46_renderer : public gfx_renderer { @@ -316,7 +317,8 @@ class opengl46_renderer : public gfx_renderer { // ones still are. Instances are individually frustum/distance culled by their records, the // TAnimModel itself is read only for the first instance drawn. Each lod band drawn counts as // one instanced_drawcall in draw_stats; the instances drawn are added to the instances counter. - void Render_Instanced( TModel3d *Model, std::vector const &Instances ); + // Bandcounts, when given, gets the number of instances drawn in each lod band. + void Render_Instanced( TModel3d *Model, std::vector const &Instances, std::vector *Bandcounts = nullptr ); // adds the bucketed instances of a scene cell to the frame-level buckets drawn by Render_Instanced(), // less the ones their records show to be out of sight in the current render pass void Queue_Instances( scene::basic_cell const *Cell ); @@ -473,6 +475,12 @@ class opengl46_renderer : public gfx_renderer { // and whenever scene::node_state_version does, as instances are shown, hidden or drawn on their own 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 + opengl46_instance_cull m_instancecull; + bool m_instancecullpass { false }; // the current pass feeds it + unsigned int m_instancecullframe { ~0u }; // frame it last ran in + std::vector m_instance_bandcounts; // reported by Render_Instanced() for the comparison renderpass_config m_colorpass; // parametrs of most recent color pass std::array m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages std::vector m_pickcontrolsitems; diff --git a/rendering/openglcamera.h b/rendering/openglcamera.h index 4c8671cf..d1d2075a 100644 --- a/rendering/openglcamera.h +++ b/rendering/openglcamera.h @@ -53,6 +53,9 @@ public: inline std::vector & frustum_points() { return m_frustumpoints; } + inline + cFrustum const & + frustum() const { return m_frustum; } // transforms provided set of clip space points to world space template void diff --git a/scene/scene.h b/scene/scene.h index 5b599d8a..edf5707f 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -242,7 +242,7 @@ public: // pass rejects most of them. the renderer makes the copies and goes by them, and reads the instance itself only // for the first one of a batch drawn. the layout follows std430, for the copies to be handed to the gpu as they are struct instance_record { - glm::vec3 position { 0.f }; // location of the instance, relative to the origin of the records of its bucket + packed_vec3 position { 0.f }; // location of the instance, relative to the origin of the records of its bucket float radius { 0.f }; float rangesquaredmin { 0.f }; // visibility range of the instance float rangesquaredmax { 0.f }; @@ -251,6 +251,9 @@ public: glm::vec4 rotationscale[ 3 ] {}; // columns of rotation and scale of the instance, w unused }; static_assert( sizeof( instance_record ) == 80, "instance_record has to keep its std430 layout" ); + static_assert( offsetof( instance_record, radius ) == 12, "instance_record has to keep its std430 layout" ); + static_assert( offsetof( instance_record, flags ) == 24, "instance_record has to keep its std430 layout" ); + static_assert( offsetof( instance_record, rotationscale ) == 32, "instance_record has to keep its std430 layout" ); enum instance_record_flags : std::uint32_t { instance_visible = 1 << 0, instance_instanceable = 1 << 1, // false once an event gave the instance animations or a skin of its own @@ -262,6 +265,12 @@ public: mutable glm::dvec3 recordorigin { 0.0 }; // the records hold locations relative to it mutable float lodrangesquared { 0.f }; // past it no submodel of the model of the bucket is drawn mutable unsigned int recordsversion { 0 }; + // place of the records in the record buffer of the gpu culling: first record, room, and the generation of + // the buffer it is good for. gpustale: the records changed since they were sent + mutable std::uint32_t gpufirst { 0 }; + mutable std::uint32_t gpucapacity { 0 }; + mutable unsigned int gpugeneration { 0 }; + mutable bool gpustale { true }; }; using instance_bucketdata_map = std::unordered_map< instance_bucket_key, instance_bucket, instance_bucket_key_hash >; using sound_sequence = std::vector; diff --git a/shaders/instance_cull.comp b/shaders/instance_cull.comp new file mode 100644 index 00000000..e0bfd728 --- /dev/null +++ b/shaders/instance_cull.comp @@ -0,0 +1,151 @@ +// 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 + +#include + +layout(local_size_x = INSTANCE_CULL_GROUP_SIZE) in; + +// scene::basic_cell::instance_record +struct instance_record +{ + vec3 position; // relative to the origin of the bucket + float radius; + float rangesquaredmin; + float rangesquaredmax; + uint flags; + uint pad; + vec4 rotationscale[3]; +}; + +struct instance_cull_job +{ + vec3 origin; + uint first; + vec3 distanceorigin; + uint count; + uint group; + uint candidatestart; + uint pad0; + uint pad1; +}; + +struct instance_cull_group +{ + uint lodfirst; + uint lodcount; + uint entryfirst; + uint pad; +}; + +layout(std430, binding = INSTANCE_RECORD_SSBO_BINDING) readonly buffer instance_record_ssbo +{ + instance_record records[]; +}; +layout(std430, binding = INSTANCE_JOB_SSBO_BINDING) readonly buffer instance_job_ssbo +{ + instance_cull_job jobs[]; +}; +layout(std430, binding = INSTANCE_GROUP_SSBO_BINDING) readonly buffer instance_group_ssbo +{ + instance_cull_group groups[]; +}; +layout(std430, binding = INSTANCE_LOD_SSBO_BINDING) readonly buffer instance_lod_ssbo +{ + float lodbounds[]; +}; +layout(std430, binding = INSTANCE_COUNTER_SSBO_BINDING) buffer instance_counter_ssbo +{ + uint counters[]; +}; +layout(std430, binding = INSTANCE_OUTPUT_SSBO_BINDING) writeonly buffer instance_output_ssbo +{ + mat4 modelviews[]; +}; + +// rotation of the view of the pass: the instances are placed relative to its camera +layout(location = 0) uniform mat4 view; +// planes of the frustum of the pass, relative to its camera +layout(location = 1) uniform vec4 planes[6]; +// x: scale of the squared distance, y: added to it past the range test, z: range of the scaled distance before the +// addition (reflections), w: squared draw range +layout(location = 7) uniform vec4 distance_params; +// x: zoom factor +layout(location = 8) uniform vec4 size_params; +// x: jobs, y: records in all jobs, z: entries, w: stage +layout(location = 9) uniform uvec4 counts; + +const uint INSTANCE_VISIBLE = 1u; +const uint INSTANCE_INSTANCEABLE = 2u; +// smallest part of the view an instance has to take +const float MIN_SCREEN_SIZE = 0.003; + +// the job which holds the given record: the last one starting at it or before it +uint find_job(uint candidate) +{ + uint low = 0u; + uint high = counts.x - 1u; + while (low < high) + { + uint middle = (low + high + 1u) / 2u; + if (jobs[middle].candidatestart <= candidate) + low = middle; + else + high = middle - 1u; + } + return low; +} + +void main() +{ + // the dispatch can be two dimensional when the records don't fit in the work group count limit of one + uint candidate = (gl_WorkGroupID.y * gl_NumWorkGroups.x + gl_WorkGroupID.x) * INSTANCE_CULL_GROUP_SIZE + gl_LocalInvocationIndex; + if (candidate >= counts.y) + return; + + instance_cull_job job = jobs[find_job(candidate)]; + instance_record record = records[job.first + (candidate - job.candidatestart)]; + // instances hidden, or drawn on their own after getting animations or skins from events + if ((record.flags & (INSTANCE_VISIBLE | INSTANCE_INSTANCEABLE)) != (INSTANCE_VISIBLE | INSTANCE_INSTANCEABLE)) + return; + + vec3 location = job.origin + record.position; + for (int plane = 0; plane < 6; ++plane) + { + if (dot(planes[plane].xyz, location) + planes[plane].w <= -record.radius) + return; + } + + vec3 offset = job.distanceorigin + record.position; + float distancesquared = dot(offset, offset) * distance_params.x; + if (distancesquared > distance_params.z) + return; + distancesquared += distance_params.y; + if ((distancesquared < record.rangesquaredmin) || (distancesquared >= record.rangesquaredmax)) + return; + if (distancesquared > distance_params.w) + return; + if (record.radius * record.radius * size_params.x / distancesquared < MIN_SCREEN_SIZE * MIN_SCREEN_SIZE) + return; + + // band: the number of lod bounds the distance reached, like upper_bound over the sorted bounds + instance_cull_group group = groups[job.group]; + uint band = 0u; + for (uint bound = 0u; bound < group.lodcount; ++bound) + { + if (lodbounds[group.lodfirst + bound] <= distancesquared) + band = bound + 1u; + } + uint entry = group.entryfirst + band; + + if (counts.w == 0u) + { + atomicAdd(counters[INSTANCE_COUNTER_HEADER + entry], 1u); + } + else + { + uint slot = atomicAdd(counters[INSTANCE_COUNTER_HEADER + counts.z + entry], 1u); + modelviews[slot] = view * mat4(record.rotationscale[0], record.rotationscale[1], record.rotationscale[2], vec4(location, 1.0)); + } +} diff --git a/shaders/instance_cull_offsets.comp b/shaders/instance_cull_offsets.comp new file mode 100644 index 00000000..a171dd19 --- /dev/null +++ b/shaders/instance_cull_offsets.comp @@ -0,0 +1,53 @@ +// turns the counts of instances of the entries, written by stage 0 of instance_cull.comp, into the first slots of the +// entries in the output (cursors, which stage 1 advances as it writes), and puts the total in the header. +// a single work group: each invocation sums a run of the entries, then the sums are scanned in shared memory + +#include + +layout(local_size_x = INSTANCE_OFFSETS_GROUP_SIZE) in; + +layout(std430, binding = INSTANCE_COUNTER_SSBO_BINDING) buffer instance_counter_ssbo +{ + uint counters[]; +}; + +// x: entries +layout(location = 0) uniform uvec4 counts; + +shared uint sums[INSTANCE_OFFSETS_GROUP_SIZE]; + +void main() +{ + uint entries = counts.x; + uint invocation = gl_LocalInvocationIndex; + uint run = (entries + INSTANCE_OFFSETS_GROUP_SIZE - 1u) / INSTANCE_OFFSETS_GROUP_SIZE; + uint begin = min(invocation * run, entries); + uint end = min(begin + run, entries); + + uint sum = 0u; + for (uint entry = begin; entry < end; ++entry) + sum += counters[INSTANCE_COUNTER_HEADER + entry]; + sums[invocation] = sum; + memoryBarrierShared(); + barrier(); + + // inclusive scan of the sums + for (uint step = 1u; step < INSTANCE_OFFSETS_GROUP_SIZE; step *= 2u) + { + uint added = (invocation >= step ? sums[invocation - step] : 0u); + memoryBarrierShared(); + barrier(); + sums[invocation] += added; + memoryBarrierShared(); + barrier(); + } + + uint offset = sums[invocation] - sum; + for (uint entry = begin; entry < end; ++entry) + { + counters[INSTANCE_COUNTER_HEADER + entries + entry] = offset; + offset += counters[INSTANCE_COUNTER_HEADER + entry]; + } + if (invocation == INSTANCE_OFFSETS_GROUP_SIZE - 1u) + counters[1] = sums[invocation]; +} diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index d1738b37..46c06466 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1426,6 +1426,11 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser.getTokens(1); Parser >> gfx_lights_clustered; } + else if (Token == "gfx.gpucull") + { + Parser.getTokens(1); + Parser >> gfx_gpucull; + } else if (Token == "gfx.lights.clusterheatmap") { Parser.getTokens(1); @@ -1830,6 +1835,7 @@ 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.postfx.motionblur.enabled", gfx_postfx_motionblur_enabled ); export_as_text( Output, "gfx.postfx.motionblur.shutter", gfx_postfx_motionblur_shutter ); // TODO: export gfx_postfx_motionblur_format diff --git a/utilities/Globals.h b/utilities/Globals.h index a8e68e74..56a63522 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -285,6 +285,7 @@ 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) bool gfx_postfx_motionblur_enabled = true; float gfx_postfx_motionblur_shutter = 0.01f; GLenum gfx_postfx_motionblur_format = GL_RG16F;