17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-01 05:42:44 +01:00

Cull lights into clusters in a compute pass

This commit is contained in:
2026-10-15 15:21:21 +02:00
parent f85cf5fe52
commit 6aa6e505e8
8 changed files with 368 additions and 48 deletions

View File

@@ -1,6 +1,7 @@
#include "stdafx.h"
#include "cluster_grid.h"
#include <algorithm>
#include <cstring>
gl::cluster_grid::cluster_grid()
@@ -17,6 +18,17 @@ gl::cluster_grid::cluster_grid()
m_params_buffer.update(m_params);
}
gl::cluster_grid::~cluster_grid()
{
for (auto &slot : m_readback)
{
// fences first, they belong to the copies
slot.copied.reset();
if (slot.buffer != 0)
glDeleteBuffers(1, &slot.buffer);
}
}
void gl::cluster_grid::bind()
{
m_aabbs.bind_base();
@@ -64,3 +76,53 @@ void gl::cluster_grid::build(program &build_program)
memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT);
m_boxes_stale = false;
}
void gl::cluster_grid::cull(program &cull_program)
{
collect_stats();
bind();
// the counters are the allocator of the index list and the count of dropped lights: every pass starts them anew
m_counters.update(cluster_counters{});
cull_program.dispatch((CLUSTER_COUNT + CLUSTER_CULL_GROUP_SIZE - 1) / CLUSTER_CULL_GROUP_SIZE);
// the clusters are read by the shading, and the counters by the copy below and then by the next pass writing them
memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
// a copy of the counters for the cpu to read when the gpu gets there; if it is far behind, this frame is not reported
if (m_readback_used < READBACK_SLOTS)
{
auto &slot{m_readback[(m_readback_first + m_readback_used) % READBACK_SLOTS]};
if (slot.buffer == 0)
{
glCreateBuffers(1, &slot.buffer);
glNamedBufferStorage(slot.buffer, sizeof(cluster_counters), nullptr, GL_CLIENT_STORAGE_BIT);
}
glCopyNamedBufferSubData(m_counters, slot.buffer, 0, 0, sizeof(cluster_counters));
slot.copied = std::make_unique<fence>();
++m_readback_used;
}
}
void gl::cluster_grid::collect_stats()
{
// oldest first, as long as the gpu has finished the copies
while (m_readback_used > 0)
{
auto &slot{m_readback[m_readback_first]};
if (!slot.copied->is_signalled())
break;
cluster_counters counters{};
glGetNamedBufferSubData(slot.buffer, 0, sizeof(counters), &counters);
slot.copied.reset();
cull_stats stats{};
stats.index_count = counters.index_count;
stats.overflow_count = counters.overflow_count;
stats.overflow_peak = std::max(counters.overflow_count, m_stats ? m_stats->overflow_peak : 0U);
m_stats = stats;
m_readback_first = (m_readback_first + 1) % READBACK_SLOTS;
--m_readback_used;
}
}

View File

@@ -1,7 +1,12 @@
#pragma once
#include <array>
#include <memory>
#include <optional>
#include <glm/glm.hpp>
#include "fence.h"
#include "ssbo.h"
#include "ubo.h"
#include "shader.h"
@@ -19,7 +24,21 @@ namespace gl
class cluster_grid
{
public:
// what the culling pass reports about a frame
struct cull_stats
{
// entries of the shared index list in use
uint32_t index_count = 0;
// (cluster, light) pairs dropped because the cluster was full
uint32_t overflow_count = 0;
// highest overflow_count seen since the grid was created
uint32_t overflow_peak = 0;
};
cluster_grid();
~cluster_grid();
cluster_grid(const cluster_grid &) = delete;
cluster_grid &operator=(const cluster_grid &) = delete;
void bind();
@@ -29,6 +48,11 @@ namespace gl
// recomputes the boxes of the clusters in view space with the program made from cluster_build.comp.
// binds the grid, and leaves the boxes ready to be read by the compute passes which follow
void build(program &build_program);
// assigns the lights of light_ssbo to the clusters with the program made from cluster_cull.comp: zeroes the
// counters, fills the (offset, count) of every cluster and the index list, and leaves them ready to be
// read by the shading and the debug view which follow. the boxes have to be current (update(), build()) and the
// grid valid. also queues the counters for reading back, see stats()
void cull(program &cull_program);
// false if the last update found no grid possible for the pass (orthographic projection)
bool valid() const
@@ -39,6 +63,12 @@ namespace gl
{
return m_params;
}
// counters of the most recent culling pass whose result has reached the cpu, which takes a few frames
// (read back without waiting for the gpu); empty until there is one
std::optional<cull_stats> stats() const
{
return m_stats;
}
private:
// cluster_aabb per cluster
@@ -53,5 +83,19 @@ namespace gl
cluster_ubs m_params{};
bool m_boxes_stale = true;
// counters copied for reading back: a queue of buffers, each with the fence of the copy into it
static constexpr size_t READBACK_SLOTS = 4;
struct readback_slot
{
GLuint buffer = 0;
std::unique_ptr<fence> copied;
};
void collect_stats();
std::array<readback_slot, READBACK_SLOTS> m_readback;
// oldest slot in use and the number of slots in use
size_t m_readback_first = 0;
size_t m_readback_used = 0;
std::optional<cull_stats> m_stats;
};
} // namespace gl

View File

@@ -22,6 +22,7 @@ void gl::glsl_common_setup()
"#define CLUSTER_COUNT " + std::to_string(CLUSTER_COUNT) + "\n" +
"#define MAX_LIGHTS_PER_CLUSTER " + std::to_string(MAX_LIGHTS_PER_CLUSTER) + "\n" +
"#define CLUSTER_BUILD_GROUP_SIZE " + std::to_string(CLUSTER_BUILD_GROUP_SIZE) + "\n" +
"#define CLUSTER_CULL_GROUP_SIZE " + std::to_string(CLUSTER_CULL_GROUP_SIZE) + "\n" +
"#define CLUSTER_UBO_BINDING " + std::to_string(CLUSTER_UBO_BINDING) + "\n" +
"#define CLUSTER_AABB_SSBO_BINDING " + std::to_string(CLUSTER_AABB_SSBO_BINDING) + "\n" +
"#define CLUSTER_RANGE_SSBO_BINDING " + std::to_string(CLUSTER_RANGE_SSBO_BINDING) + "\n" +
@@ -59,8 +60,11 @@ void gl::glsl_common_setup()
layout(std430, binding = LIGHT_SSBO_BINDING) readonly buffer light_ssbo
{
vec3 ambient;
// lights stored in lights[], what the cluster culling goes through
uint lights_total;
vec3 fog_color;
// lights the shading loop goes through
uint lights_count;
light_s lights[];

View File

@@ -156,22 +156,26 @@ namespace gl
static_assert(offsetof(light_element, headlight_projection) == 64, "bad layout of light_element");
static_assert(offsetof(light_element, headlight_weights) == 128, "bad layout of light_element");
// capacity of light_data.lights; slot 0 is the sun
const size_t MAX_LIGHTS = 8;
// capacity of light_data.lights; slot 0 is the sun. a buffer size, not a limit of the shaders: they take the
// number of lights from the header
const size_t MAX_LIGHTS = 256;
// contents of light_ssbo: global lighting header followed by the light array
struct light_data
{
packed_vec3 ambient;
UBS_PAD(4);
// number of lights stored in lights[] (the sun included): what the light clusters are culled from
uint32_t lights_total;
packed_vec3 fog_color;
// number of lights (the sun included) the shading loop over lights[] goes through
uint32_t lights_count;
light_element lights[MAX_LIGHTS];
};
static_assert(offsetof(light_data, ambient) == 0, "bad layout of light_data");
static_assert(offsetof(light_data, lights_total) == 12, "bad layout of light_data");
static_assert(offsetof(light_data, fog_color) == 16, "bad layout of light_data");
static_assert(offsetof(light_data, lights_count) == 28, "bad layout of light_data");
static_assert(offsetof(light_data, lights) == 32, "bad layout of light_data");
@@ -186,8 +190,9 @@ namespace gl
const GLuint CLUSTER_COUNT = CLUSTER_GRID_X * CLUSTER_GRID_Y * CLUSTER_GRID_Z;
// capacity of the light list of a single cluster
const GLuint MAX_LIGHTS_PER_CLUSTER = 64;
// work group size of cluster_build.comp, which handles one cluster per invocation
// work group size of cluster_build.comp and cluster_cull.comp, which handle one cluster per invocation
const GLuint CLUSTER_BUILD_GROUP_SIZE = 64;
const GLuint CLUSTER_CULL_GROUP_SIZE = 64;
// binding points of the cluster buffers: layout(binding = ...) in glsl_common.
// the uniform block numbers go together with scene (0), model (1) and instance (3) blocks
@@ -247,8 +252,9 @@ namespace gl
static_assert(offsetof(cluster_range, offset) == 0, "bad layout of cluster_range");
static_assert(offsetof(cluster_range, count) == 4, "bad layout of cluster_range");
// layout std430: cluster_counter_ssbo. index_count is the allocator of the shared index list, overflow_count
// the number of lights dropped from clusters which were full
// layout std430: cluster_counter_ssbo, zeroed before every culling pass. index_count is the allocator of the shared
// index list, overflow_count the number of (cluster, light) pairs dropped because the cluster already held
// MAX_LIGHTS_PER_CLUSTER lights
struct cluster_counters
{
uint32_t index_count;