17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-18 00:59:18 +02:00

Let the instances drawn from the GPU hide one another, in two phases

This commit is contained in:
2026-10-17 21:50:23 +02:00
parent 918c056b83
commit 5a5c67aaaf
14 changed files with 367 additions and 129 deletions

View File

@@ -268,13 +268,20 @@ views go as before.
|---|---|---|
| `gfx.gpucull` | `no` | `no`: on the processor, as before. `yes`: on the graphics card, and the models are drawn from its result. `compare`: on the processor, which draws, and on the graphics card too, for the results to be compared (debugging) |
| `gfx.gpucull.occlusion yes/no` | `yes` | with `gfx.gpucull yes`: models hidden whole behind what was drawn before them (the cab, the terrain, buildings and other models drawn on their own, vehicles) are left out |
| `gfx.gpucull.occlusion.instances yes/no` | `yes` | with `gfx.gpucull.occlusion yes`: the models drawn this way hide one another too (see below) |
| `gfx.depthprepass yes/no` | `yes` | with `gfx.gpucull yes`: the depth of the models is drawn first, so the colour is worked out only for what stays in view (dense foliage) |
| `gfx.depthprepass.scene yes/no` | `no` | the depth of the whole opaque scene of the main view (terrain, tracks, buildings, models, vehicles, the cab) is drawn before its colour, see below |
The occlusion test does not take models drawn this way as covering each other: a tree hidden behind another tree is still
drawn (its colour is not worked out with the depth prepass on). Models whose material is not one of the usual ones (parallax,
water and the like) are left out of the depth prepass and drawn as before. All three can be changed while the simulator
runs, in the **Instance culling** debug window.
With `gfx.gpucull.occlusion.instances yes` the test goes in two phases: the models seen in the previous frame are drawn
first (their depth, or their colour without the depth prepass), the depth they leave is added to what the others are tested
against, and only then are the others tested and drawn; the graphics card also notes which models it saw, for the next frame.
So a tree hidden behind other trees, or behind a hill of models, is left out as well. Whatever is left out is hidden behind
something drawn, so the picture is the same either way; a model coming into view is drawn in the frame it appears in. It
costs the depth pyramid a second time and the processor one more pass over the draws of the models (hundreds of draw calls,
not the scene). With `no` the models do not cover each other: a tree hidden behind another tree is still drawn (its colour
is not worked out with the depth prepass on). Models whose material is not one of the usual ones (parallax, water and the
like) are left out of the depth prepass and drawn as before; with the depth prepass on, they don't hide the others either.
All of these can be changed while the simulator runs, in the **Instance culling** debug window.
With `gfx.depthprepass.scene yes` the scene is gone through twice: first only its depth is drawn, then its colour, worked
out once per pixel for the surface which stays in view. That costs the processor a second walk through the scene (fewer
@@ -351,11 +358,13 @@ editor goes through the first `dynamiclights` headlights, the reflections throug
of it, which can be edited and applied with a time of change, and the layers of clouds, the fog, the precipitation and
the wind, which change at once. These changes are of this computer only. Shift+F12 shows the GPU time of `clouds:` (the
main view), `clouds panorama:` (a band of the panorama), `cloud shadows:` and `fog volume:`.
- **Instance culling** window: the mode of `gfx.gpucull` and the two switches above. With `gfx.gpucull yes` Shift+F12 shows
`gpu cull:` (models drawn and hidden by the occlusion test, as counted on the graphics card a few frames before, the levels
of detail, the draw calls with those of the depth prepass, and the processor time of the whole step) and the timings
`instance cull:`, `occlusion depth:` (the depth pyramid of the test) and `depth prepass:`. With `compare` it shows how many
models both counted and how many levels of detail differ.
- **Instance culling** window: the mode of `gfx.gpucull` and the switches above. With `gfx.gpucull yes` Shift+F12 shows
`gpu cull:` (models drawn, in two phases how many of them first, and hidden by the occlusion test, as counted on the
graphics card a few frames before, the levels of detail, the draw calls with those of the depth prepass, and the processor
time of the whole step) and the timings `instance cull:`, `occlusion depth:` (the depth pyramid of the test) and
`depth prepass:`; in two phases each of them adds in brackets the time of the second phase, of the second pyramid and of
the depth of the models drawn first. With `compare` it shows how many models both counted and how many levels of detail
differ.
- With `gfx.depthprepass.scene yes` Shift+F12 shows the `scene depth:` row (draws of the depth pass and its processor time)
and the timing `scene depth:` (its GPU time).
- With `gfx.contactshadows` on Shift+F12 shows the `contact shadows:` row (the level, the length and reach of the rays, or

View File

@@ -469,16 +469,17 @@ namespace gl
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_COUNTER_SSBO_BINDING = 13; // header, then count[entries], cursor[entries] and first[entries]
const GLuint INSTANCE_OUTPUT_SSBO_BINDING = 14; // modelview matrices of the instances which passed, by entry
const GLuint INSTANCE_COMMAND_SSBO_BINDING = 15; // indirect draw commands of the entries (instance_cull_commands.comp)
const GLuint INSTANCE_CULL_GROUP_SIZE = 64;
const GLuint INSTANCE_OFFSETS_GROUP_SIZE = 1024;
const GLuint INSTANCE_COMMAND_GROUP_SIZE = 64;
// uints of the header of the counter buffer: frame number, survivors in total, entries, instances left out as hidden
// behind the depth already drawn (occlusion test).
// uints of the header of the counter buffer: frame number, survivors of the tests but the occlusion one in total,
// entries, instances left out as hidden behind the depth already drawn (occlusion test), instances drawn first in two
// phases (seen in the previous frame), 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;
const GLuint INSTANCE_COUNTER_HEADER = 8;
// work group of the passes which make the depth pyramid of the occlusion test (hiz_build.comp), 8x8
const GLuint HIZ_GROUP_SIZE = 8;
@@ -510,7 +511,8 @@ namespace gl
// layout std430: an indirect draw of the submodel of a group in one of its lod bands. the first five fields are the
// command of glDrawElementsIndirect (count, instance count, first index, base vertex, base instance), or with indexed 0
// the one of glDrawArraysIndirect (count, instance count, first vertex, base instance). the cpu fills in what it knows
// of the geometry, instance_cull_commands.comp the instances, from the counts of the entry
// of the geometry, instance_cull_commands.comp the instances, from the counts of the entry. the command buffer holds the
// commands of a pass three times over (instances drawn first, all, drawn second), see opengl46_instance_cull
struct instance_draw_command
{
uint32_t count;

View File

@@ -70,14 +70,15 @@ void opengl46_hiz::allocate(int const Width, int const Height)
glTextureParameteri(m_pyramid, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
void opengl46_hiz::build(GLuint const Source, int const Width, int const Height)
void opengl46_hiz::build(GLuint const Source, int const Width, int const Height, bool const Second)
{
if ((Width <= 0) || (Height <= 0))
return;
if (m_sourcesize != glm::ivec2{Width, Height})
allocate(Width, Height);
m_timer.begin();
auto &timer{Second ? m_secondtimer : m_timer};
timer.begin();
// resolve of the multisampled depth; the scissor test would limit the blit
auto const scissor{glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE};
@@ -107,13 +108,19 @@ void opengl46_hiz::build(GLuint const Source, int const Width, int const Height)
glBindTextureUnit(0, 0);
opengl_texture::reset_unit_cache();
m_timer.end();
timer.end();
}
std::string opengl46_hiz::info_times()
std::string opengl46_hiz::info_times(bool const Second)
{
std::string text;
if (auto const time = m_timer.result())
text += " occlusion depth: " + to_string(static_cast<double>(*time) / 1000000.0, 3) + " ms\n";
{
text += " occlusion depth: " + to_string(static_cast<double>(*time) / 1000000.0, 3) + " ms";
auto const second{m_secondtimer.result()};
if (Second && second)
text += " (+" + to_string(static_cast<double>(*second) / 1000000.0, 3) + " ms with the instances drawn first)";
text += "\n";
}
return text;
}

View File

@@ -19,7 +19,8 @@ http://mozilla.org/MPL/2.0/.
// 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
// depth of the 2x2 texels below it; the first level is half the size of the view.
// in two phases the pyramid is made a second time in the pass, with the instances drawn first in it
class opengl46_hiz
{
public:
@@ -31,8 +32,9 @@ class opengl46_hiz
// 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);
// leaves the framebuffer bindings alone, texture unit 0 and image unit 0 are used (the unit cache is reset).
// Second: the second pyramid of the pass, timed on its own
void build(GLuint const Source, int const Width, int const Height, bool const Second = false);
// the pyramid, its first level size and level count; valid after build()
GLuint texture() const
{
@@ -50,7 +52,8 @@ class opengl46_hiz
{
return m_sourcesize;
}
std::string info_times();
// Second: with the time of the second pyramid
std::string info_times(bool const Second);
private:
// (re)creates the textures for a view of the given size
@@ -65,4 +68,5 @@ class opengl46_hiz
glm::ivec2 m_size{0};
int m_levels{0};
gl::gpu_timer m_timer;
gl::gpu_timer m_secondtimer;
};

View File

@@ -210,13 +210,14 @@ void opengl46_instance_cull::place(scene::basic_cell::instance_bucket const &Buc
allocate(Bucket);
}
void opengl46_instance_cull::dispatch(glm::mat4 const &View, occlusion_setup const *Occlusion)
void opengl46_instance_cull::dispatch(glm::mat4 const &View, occlusion_setup const *Occlusion, bool const Twophase)
{
m_occlusion = false;
m_twophase = false;
if (!m_inpass || m_joblist.empty())
return;
auto const countersize{static_cast<GLsizeiptr>((gl::INSTANCE_COUNTER_HEADER + 2 * m_entries) * sizeof(std::uint32_t))};
auto const countersize{static_cast<GLsizeiptr>((gl::INSTANCE_COUNTER_HEADER + 3 * m_entries) * sizeof(std::uint32_t))};
reserve(*m_jobs, static_cast<GLsizeiptr>(m_joblist.size() * sizeof(gl::instance_cull_job)));
reserve(*m_groups, static_cast<GLsizeiptr>(m_grouplist.size() * sizeof(gl::instance_cull_group)));
reserve(*m_lods, static_cast<GLsizeiptr>(std::max<std::size_t>(m_lodlist.size(), 1) * sizeof(float)));
@@ -229,15 +230,10 @@ void opengl46_instance_cull::dispatch(glm::mat4 const &View, occlusion_setup con
m_lods->update(m_lodlist.data(), 0, static_cast<GLsizeiptr>(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};
std::uint32_t const header[gl::INSTANCE_COUNTER_HEADER]{m_setup.frame, 0, m_entries, 0, 0, 0, 0, 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();
bind_buffers();
// planes moved to the camera of the pass, in double precision: the cpu tests world coordinates
auto const &frustumplanes{m_setup.frustum->planes()};
@@ -266,6 +262,8 @@ void opengl46_instance_cull::dispatch(glm::mat4 const &View, occlusion_setup con
{
glProgramUniform4i(*m_cull_program, 14, 0, 0, 0, 0);
}
m_twophase = (m_occlusion && Twophase);
glProgramUniform4ui(*m_cull_program, 16, (m_twophase ? 1u : 0u), 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};
@@ -289,19 +287,62 @@ void opengl46_instance_cull::dispatch(glm::mat4 const &View, occlusion_setup con
opengl_texture::reset_unit_cache();
}
m_readback->push(*m_counters);
m_cpucounts.assign(m_entries, 0);
m_dispatched = true;
}
void opengl46_instance_cull::bind_buffers()
{
m_records->bind_base();
m_jobs->bind_base();
m_groups->bind_base();
m_lods->bind_base();
m_counters->bind_base();
m_output->bind_base();
}
void opengl46_instance_cull::dispatch_second(occlusion_setup const &Occlusion)
{
if (!m_dispatched || !m_twophase)
return;
// the draws of the first phase bound buffers of their own, and other passes share the binding of the records
bind_buffers();
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);
auto const groups{(m_candidates + gl::INSTANCE_CULL_GROUP_SIZE - 1) / gl::INSTANCE_CULL_GROUP_SIZE};
auto const groups_x{std::min<GLuint>(groups, 65535)};
auto const groups_y{(groups + groups_x - 1) / groups_x};
m_secondtimer.begin();
glProgramUniform4ui(*m_cull_program, 9, static_cast<GLuint>(m_joblist.size()), m_candidates, m_entries, 2);
m_cull_program->dispatch(groups_x, groups_y);
gl::memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT);
if (m_commandcount > 0)
{
m_commands->bind_base();
glProgramUniform4ui(*m_commands_program, 0, m_commandcount, m_entries, 1, 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, the counts to the copy for the cpu
gl::memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT | GL_COMMAND_BARRIER_BIT);
m_secondtimer.end();
glBindTextureUnit(0, 0);
opengl_texture::reset_unit_cache();
}
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;
// the first copy is sent, the others are made from it on the gpu
auto const size{static_cast<GLsizeiptr>(Commands.size() * sizeof(gl::instance_draw_command))};
reserve(*m_commands, size);
reserve(*m_commands, 3 * size);
m_commands->update(Commands.data(), 0, size);
m_counters->bind_base();
m_commands->bind_base();
@@ -340,6 +381,9 @@ void opengl46_instance_cull::cpu_result(scene::basic_cell::instance_bucket_key c
void opengl46_instance_cull::end_pass()
{
// the counts of both phases, for the panel and the comparison
if (m_dispatched)
m_readback->push(*m_counters);
// the cpu counts are there to compare with only when the cpu drew
if (m_dispatched && !m_setup.drawing)
{
@@ -363,9 +407,10 @@ void opengl46_instance_cull::collect()
auto const frame{m_readbackdata[0]};
comparison result;
result.frame = frame;
result.gputotal = m_readbackdata[1];
result.entries = m_readbackdata[2];
result.occluded = m_readbackdata[3];
result.gputotal = m_readbackdata[1] - std::min(m_readbackdata[1], result.occluded);
result.entries = m_readbackdata[2];
result.first = m_readbackdata[4];
while (!m_cpupasses.empty() && m_cpupasses.front().frame != frame)
m_cpupasses.pop_front();
if (m_cpupasses.empty())
@@ -404,8 +449,10 @@ std::string opengl46_instance_cull::info()
{
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 (+" +
return "\ngpu cull: " + std::to_string(m_last.gputotal) + " instances drawn" +
(m_twophase ? " (" + std::to_string(m_last.first) + " of them first, seen in the last frame)" : std::string{}) + ", " + std::to_string(m_last.occluded) +
" hidden by occlusion" + (m_occlusion ? (m_twophase ? " (also by one another)" : "") : " (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)
@@ -421,6 +468,12 @@ std::string opengl46_instance_cull::info_times()
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";
{
text += " instance cull: " + to_string(static_cast<double>(*time) / 1000000.0, 3) + " ms";
auto const second{m_secondtimer.result()};
if (m_twophase && second)
text += " (+" + to_string(static_cast<double>(*second) / 1000000.0, 3) + " ms second phase)";
text += "\n";
}
return text;
}

View File

@@ -37,7 +37,10 @@ class TAnimModel;
// 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.
// (opengl46_hiz) can leave out more instances. in two phases the instances seen in the previous frame are culled and drawn
// first, then the pyramid is made again with them in it (dispatch_second()) and the others are tested against it, so the
// instances hide one another too. the command buffer holds the commands three times over: the instances drawn first, all
// of them, and the ones drawn second, each copy command_count() commands long (draw_command_offset())
// 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
@@ -98,8 +101,18 @@ class opengl46_instance_cull
// 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);
// to test the instances against, or null. Twophase: with the occlusion test, only the instances seen in the previous
// frame pass for now, dispatch_second() takes the others
void dispatch(glm::mat4 const &View, occlusion_setup const *Occlusion = nullptr, bool const Twophase = false);
// the second phase, after the instances which passed dispatch() were drawn and the pyramid was made again with them:
// the others are tested against it, then the commands of all instances and of the ones of this phase are filled in.
// needs fill_commands() before it
void dispatch_second(occlusion_setup const &Occlusion);
// the last dispatch() works in two phases
bool two_phase() const
{
return m_twophase;
}
// 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
{
@@ -109,9 +122,22 @@ class opengl46_instance_cull
{
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()
// sends the indirect draw commands made for the entries of the pass and fills in their instances (of the instances drawn
// first, in two phases). after this the commands are in the buffer bound by bind_draw(), the modelview matrices of the
// instances in the output
void fill_commands(std::vector<gl::instance_draw_command> const &Commands);
// copies of the commands in the command buffer
enum class command_set : std::uint32_t
{
first = 0, // instances which passed dispatch(): in one phase all of them
all = 1, // after dispatch_second()
second = 2 // the ones which passed dispatch_second()
};
// offset of command Index of a copy in the command buffer
std::intptr_t draw_command_offset(command_set const Set, std::size_t const Index) const
{
return static_cast<std::intptr_t>((static_cast<std::size_t>(Set) * m_commandcount + Index) * sizeof(gl::instance_draw_command));
}
// 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
@@ -164,10 +190,13 @@ class opengl46_instance_cull
std::uint32_t mismatched{0};
std::uint32_t largestdifference{0};
std::uint32_t occluded{0}; // left out by the occlusion test
std::uint32_t first{0}; // drawn first, seen in the previous frame (two phases)
};
// 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);
// binds the buffers of the culling passes
void bind_buffers();
// takes the newest counts of the gpu which arrived and compares them with the ones of the cpu
void collect();
@@ -186,6 +215,7 @@ class opengl46_instance_cull
std::unique_ptr<gl::ssbo> m_commands;
std::unique_ptr<gl::readback> m_readback;
gl::gpu_timer m_timer;
gl::gpu_timer m_secondtimer;
// 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};
@@ -216,4 +246,5 @@ class opengl46_instance_cull
std::uint32_t m_commandcount{0};
double m_cputime{0.0};
bool m_occlusion{false}; // the last dispatch tested occlusion
bool m_twophase{false}; // in two phases
};

View File

@@ -749,9 +749,15 @@ void opengl46_renderer::SwapBuffers()
m_debugtimestext += " contact shadows depth: " + to_string( static_cast<double>( *contacttime ) / 1000000.0, 3 ) + " ms\n";
}
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";
auto const twophase { m_instancecull.two_phase() };
m_debugtimestext += m_hiz.info_times( twophase );
if( auto const prepasstime = m_prepasstimer.result() ) {
m_debugtimestext += " depth prepass: " + to_string( static_cast<double>( *prepasstime ) / 1000000.0, 3 ) + " ms";
auto const firsttime { m_prepassfirsttimer.result() };
if( twophase && firsttime )
m_debugtimestext += " (+" + to_string( static_cast<double>( *firsttime ) / 1000000.0, 3 ) + " ms of the instances drawn first)";
m_debugtimestext += "\n";
}
}
m_debugtimestext += m_localshadows.info_times();
m_debugtimestext += "uilayer: " + to_string( Timer::subsystem.gfx_gui.average(), 2 ) + " ms\n";
@@ -894,6 +900,8 @@ void opengl46_renderer::draw_debug_ui()
if (Global.gfx_gpucull == 1)
{
ImGui::Checkbox("leave out instances hidden behind the depth drawn before them (gfx.gpucull.occlusion)", &Global.gfx_gpucull_occlusion);
if (Global.gfx_gpucull_occlusion)
ImGui::Checkbox("draw the instances seen in the last frame first, to hide the others (gfx.gpucull.occlusion.instances)", &Global.gfx_gpucull_occlusion_instances);
ImGui::Checkbox("draw the depth of the instances before their colour (gfx.depthprepass)", &Global.gfx_depthprepass);
}
ImGui::Checkbox("draw the depth of the whole scene before its colour (gfx.depthprepass.scene)", &Global.gfx_depthprepass_scene);
@@ -3181,7 +3189,7 @@ void opengl46_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
if( false == m_indirectdraws.empty() ) {
auto const cpustart { std::chrono::steady_clock::now() };
m_instancecull.bind_draw();
Render_Instances_Indirect_Colour();
Render_Instances_Indirect_Colour( m_indirectcolourset );
::glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0 );
Bind_Instance_Stream();
m_instancecull.draw_result(
@@ -3810,7 +3818,12 @@ void opengl46_renderer::Render_Instances_Indirect()
occlusion.screen = glm::vec2{ m_hiz.source_size() };
occlusion.projection = OpenGLMatrices.data( GL_PROJECTION );
}
m_instancecull.dispatch( OpenGLMatrices.data( GL_MODELVIEW ), ( useocclusion ? &occlusion : nullptr ) );
// in two phases the instances seen in the previous frame are drawn first, and the others are tested against a pyramid
// made again with their depth in it (not in wireframe, which doesn't draw surfaces)
m_instancecull.dispatch(
OpenGLMatrices.data( GL_MODELVIEW ),
( useocclusion ? &occlusion : nullptr ),
( Global.gfx_gpucull_occlusion_instances && ( false == Global.bWireFrame ) ) );
// the draws, recorded while the gpu culls
m_indirectdraws.clear();
@@ -3842,29 +3855,43 @@ void opengl46_renderer::Render_Instances_Indirect()
m_indirectrecording = false;
m_instancecull.fill_commands( m_indirectcommands );
using command_set = opengl46_instance_cull::command_set;
auto const twophase { m_instancecull.two_phase() };
auto const draws { false == m_indirectdraws.empty() };
// the depth comes first in the depth pass of the whole scene, which has its state set and leaves the colour to the colour
// pass (m_indirectcolourpending), and with the depth prepass of the instances: 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
auto const depthfirst { m_scenedepthpass || ( Global.gfx_depthprepass && ( false == Global.bWireFrame ) ) };
std::uint32_t prepassdraws { 0 };
if( false == m_indirectdraws.empty() ) {
m_instancecull.bind_draw();
if( m_scenedepthpass ) {
// the depth pass of the whole scene, which has its state set: only the depth here, the colour pass draws the
// colour of the same draws (m_indirectcolourpending)
prepassdraws = Render_Instances_Indirect_Depth();
}
else {
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 );
prepassdraws = Render_Instances_Indirect_Depth();
::glDisable( GL_POLYGON_OFFSET_FILL );
::glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE );
m_prepasstimer.end();
std::uint32_t colourdraws { 0 };
auto colourset { command_set::first };
if( twophase ) {
if( draws ) {
// the instances seen in the previous frame, then the pyramid again, with them in it
m_instancecull.bind_draw();
if( depthfirst ) {
prepassdraws += Render_Instances_Indirect_Prepass( command_set::first, m_prepassfirsttimer );
}
Render_Instances_Indirect_Colour();
else {
Render_Instances_Indirect_Colour( command_set::first );
colourdraws += static_cast<std::uint32_t>( m_indirectdraws.size() );
}
m_hiz.build( *viewport->msaa_fb, viewport->width, viewport->height, true );
occlusion.pyramid = m_hiz.texture();
}
// the others are tested against it; with no draws the instances are only marked for the next frame
m_instancecull.dispatch_second( occlusion );
colourset = ( depthfirst ? command_set::all : command_set::second );
}
if( draws ) {
m_instancecull.bind_draw();
if( depthfirst ) {
prepassdraws += Render_Instances_Indirect_Prepass( ( twophase ? command_set::second : command_set::first ), m_prepasstimer );
}
if( false == m_scenedepthpass ) {
Render_Instances_Indirect_Colour( colourset );
colourdraws += static_cast<std::uint32_t>( m_indirectdraws.size() );
}
::glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0 );
Bind_Instance_Stream();
@@ -3872,25 +3899,42 @@ void opengl46_renderer::Render_Instances_Indirect()
if( m_scenedepthpass ) {
// the colour pass takes the draws from here, and counts them
m_indirectcolourpending = true;
m_indirectcolourset = colourset;
m_renderpass.draw_stats.drawcalls += static_cast<int>( prepassdraws );
colourdraws = static_cast<std::uint32_t>( m_indirectdraws.size() );
}
else {
m_renderpass.draw_stats.instanced_drawcalls += static_cast<int>( m_indirectdraws.size() );
m_renderpass.draw_stats.instanced_drawcalls += static_cast<int>( colourdraws );
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() ),
colourdraws,
prepassdraws,
std::chrono::duration<double, std::milli>( std::chrono::steady_clock::now() - cpustart ).count() );
}
std::uint32_t opengl46_renderer::Render_Instances_Indirect_Depth()
std::uint32_t opengl46_renderer::Render_Instances_Indirect_Prepass( opengl46_instance_cull::command_set const Set, gl::gpu_timer &Timer )
{
if( m_scenedepthpass ) {
return Render_Instances_Indirect_Depth( Set );
}
Timer.begin();
setup_drawing( false );
::glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );
::glEnable( GL_POLYGON_OFFSET_FILL );
::glPolygonOffset( -1.f, -1.f );
auto const draws { Render_Instances_Indirect_Depth( Set ) };
::glDisable( GL_POLYGON_OFFSET_FILL );
::glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE );
Timer.end();
return draws;
}
std::uint32_t opengl46_renderer::Render_Instances_Indirect_Depth( opengl46_instance_cull::command_set const Set )
{
auto const commandoffset = []( std::size_t const Index ) {
return static_cast<std::intptr_t>( Index * sizeof( gl::instance_draw_command ) ); };
std::uint32_t draws { 0 };
// the draws without the alpha test first, then the ones with it, one program each
for( auto prepassclass = 1; prepassclass <= 2; ++prepassclass ) {
@@ -3917,24 +3961,22 @@ std::uint32_t opengl46_renderer::Render_Instances_Indirect_Depth()
}
model_ubs = draw.ubs;
model_ubo->update( model_ubs );
m_geometry.draw_indirect( draw.geometry, commandoffset( index ) );
m_geometry.draw_indirect( draw.geometry, m_instancecull.draw_command_offset( Set, index ) );
++draws;
}
}
return draws;
}
void opengl46_renderer::Render_Instances_Indirect_Colour()
void opengl46_renderer::Render_Instances_Indirect_Colour( opengl46_instance_cull::command_set const Set )
{
auto const commandoffset = []( std::size_t const Index ) {
return static_cast<std::intptr_t>( Index * sizeof( gl::instance_draw_command ) ); };
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 ) );
m_geometry.draw_indirect( draw.geometry, m_instancecull.draw_command_offset( Set, index ) );
if( draw.blending ) { setup_drawing( false ); }
}
model_ubs.emission = 0.0f;

View File

@@ -351,12 +351,17 @@ class opengl46_renderer : public gfx_renderer {
// 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). inside the depth pass of the whole scene only their depth is drawn, their
// colour waits for the colour pass (Render_Instances_Indirect_Colour())
// colour waits for the colour pass (Render_Instances_Indirect_Colour()). in two phases
// (Global.gfx_gpucull_occlusion_instances) the instances seen in the previous frame are drawn first (their depth, or
// their colour without the prepass), the pyramid is made again and the other instances are culled against it
void Render_Instances_Indirect();
// the recorded draws of the instances: their depth with the programs of the depth prepass, with the state of the prepass
// already set (returns the number of draws), and their colour. the command buffer and the instances are bound by the caller
std::uint32_t Render_Instances_Indirect_Depth();
void Render_Instances_Indirect_Colour();
// the recorded draws of the instances, with the commands of the given set: their depth with the programs of the depth
// prepass, with the state of the prepass already set (returns the number of draws), the same with the state set for it
// and the time measured outside the depth pass of the whole scene, and their colour. the command buffer and the
// instances are bound by the caller
std::uint32_t Render_Instances_Indirect_Depth( opengl46_instance_cull::command_set const Set );
std::uint32_t Render_Instances_Indirect_Prepass( opengl46_instance_cull::command_set const Set, gl::gpu_timer &Timer );
void Render_Instances_Indirect_Colour( opengl46_instance_cull::command_set const Set );
// 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 );
// depth of the whole opaque scene of the colour pass, before its colour (Global.gfx_depthprepass_scene): the colour pass
@@ -552,11 +557,13 @@ class opengl46_renderer : public gfx_renderer {
std::unique_ptr<gl::program> m_prepass_alpha_shader;
std::unordered_map<gl::program const *, int> m_prepassclasses;
gl::gpu_timer m_prepasstimer;
gl::gpu_timer m_prepassfirsttimer; // of the instances drawn first, in two phases
// depth pass of the whole scene (Render_Scene_Depth())
bool m_scenedepthpass { false }; // inside it: Bind_Material() and draw() work for the depth
bool m_scenedepthskip { true }; // the material bound last isn't drawn in it
bool m_scenedepthdrawn { false }; // it was drawn for the colour pass under way, which skips what was done in it already
bool m_indirectcolourpending { false }; // the instances were culled in it, the colour pass draws them from the recorded draws
opengl46_instance_cull::command_set m_indirectcolourset { opengl46_instance_cull::command_set::first }; // with these commands
debug_stats m_scenedepthstats; // of its last run
double m_scenedepthcputime { 0.0 }; // ms
gl::gpu_timer m_scenedepthtimer;

View File

@@ -247,7 +247,7 @@ public:
float rangesquaredmin { 0.f }; // visibility range of the instance
float rangesquaredmax { 0.f };
std::uint32_t flags { 0 }; // instance_record_flags
std::uint32_t pad { 0 };
std::uint32_t pad { 0 }; // the copy of the gpu culling keeps there whether the instance was seen in the last frame
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" );

View File

@@ -1,9 +1,15 @@
// 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.
// stage 0 counts the instances which pass the tests, 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 the modelview matrices of the instances drawn first
// there, in no particular order, and stage 2 (two phases) those of the instances drawn second after them.
// with the occlusion test on, an instance whose bounding sphere lies whole behind the depth already drawn in the pass
// (the farthest depth of the pyramid over the part of the view it takes, see hiz_build.comp) is left out as well
// (the farthest depth of the pyramid over the part of the view it takes, see hiz_build.comp) is left out as well.
// in two phases (opengl46_instance_cull::dispatch_second()) stage 1 takes only the instances seen in the previous frame,
// which are drawn, the pyramid is made again with them in it, and stage 2 tests the others against it; it also keeps for
// the next frame which instances it found in view. an instance seen before but hidden behind the depth of the first
// pyramid is hidden behind the second one as well (it only holds nearer depth), so each instance is drawn at most once,
// and everything left out is hidden behind something drawn
#include <common>
@@ -17,7 +23,7 @@ struct instance_record
float rangesquaredmin;
float rangesquaredmax;
uint flags;
uint pad;
uint visible; // the instance was seen in the previous frame (written by stage 2, 0 when the cpu sends the record)
vec4 rotationscale[3];
};
@@ -41,7 +47,7 @@ struct instance_cull_group
uint pad;
};
layout(std430, binding = INSTANCE_RECORD_SSBO_BINDING) readonly buffer instance_record_ssbo
layout(std430, binding = INSTANCE_RECORD_SSBO_BINDING) buffer instance_record_ssbo
{
instance_record records[];
};
@@ -82,6 +88,8 @@ layout(location = 10) uniform mat4 hiz_projection;
layout(location = 14) uniform ivec4 hiz_params;
// xy: size of the depth the pyramid was made from, pixels
layout(location = 15) uniform vec4 hiz_screen;
// x: two phases, stage 1 takes only the instances seen in the previous frame
layout(location = 16) uniform uvec4 phases;
// depth pyramid, farthest (smallest, reversed) depth per texel
layout(binding = 0) uniform sampler2D hiz;
@@ -90,6 +98,9 @@ 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;
// header of the counters, see gl::INSTANCE_COUNTER_HEADER
const uint COUNTER_OCCLUDED = 3u;
const uint COUNTER_FIRST = 4u;
// the job which holds the given record: the last one starting at it or before it
uint find_job(uint candidate)
@@ -151,43 +162,33 @@ bool occluded(vec3 center, float radius)
return nearest < farthest;
}
void main()
// the tests of the cpu (Render_Instanced()), all but the occlusion one: the entry of the instance, or false when it's left out.
// Location: relative to the camera of the pass
bool passes(instance_cull_job job, instance_record record, out vec3 location, out uint entry)
{
// 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)];
location = job.origin + record.position;
entry = 0u;
// 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;
return false;
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;
return false;
}
vec3 offset = job.distanceorigin + record.position;
float distancesquared = dot(offset, offset) * distance_params.x;
if (distancesquared > distance_params.z)
return;
return false;
distancesquared += distance_params.y;
if ((distancesquared < record.rangesquaredmin) || (distancesquared >= record.rangesquaredmax))
return;
return false;
if (distancesquared > distance_params.w)
return;
return false;
if (record.radius * record.radius * size_params.x / distancesquared < MIN_SCREEN_SIZE * MIN_SCREEN_SIZE)
return;
if ((hiz_params.x != 0) && occluded((view * vec4(location, 1.0)).xyz, record.radius))
{
if (counts.w == 0u)
atomicAdd(counters[3], 1u);
return;
}
return false;
// band: the number of lod bounds the distance reached, like upper_bound over the sorted bounds
instance_cull_group group = groups[job.group];
@@ -197,15 +198,65 @@ void main()
if (lodbounds[group.lodfirst + bound] <= distancesquared)
band = bound + 1u;
}
uint entry = group.entryfirst + band;
entry = group.entryfirst + band;
return true;
}
void write(uint entry, instance_record record, vec3 location)
{
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));
}
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)];
uint index = job.first + (candidate - job.candidatestart);
instance_record record = records[index];
vec3 location;
uint entry;
bool passed = passes(job, record, location, entry);
if (counts.w == 0u)
{
atomicAdd(counters[INSTANCE_COUNTER_HEADER + entry], 1u);
// room in the output for every instance which passes, whatever the occlusion test says
if (passed)
atomicAdd(counters[INSTANCE_COUNTER_HEADER + entry], 1u);
return;
}
else
bool seen = (record.visible != 0u);
if (counts.w == 1u)
{
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));
// the ones seen in the previous frame first, the others wait for stage 2
if ((false == passed) || ((phases.x != 0u) && (false == seen)))
return;
if ((hiz_params.x != 0) && occluded((view * vec4(location, 1.0)).xyz, record.radius))
{
atomicAdd(counters[COUNTER_OCCLUDED], 1u);
return;
}
write(entry, record, location);
if (phases.x != 0u)
atomicAdd(counters[COUNTER_FIRST], 1u);
return;
}
// stage 2, against the pyramid with the instances drawn first in it
bool inview = passed && (false == occluded((view * vec4(location, 1.0)).xyz, record.radius));
if (passed && (false == seen))
{
if (inview)
write(entry, record, location);
else
atomicAdd(counters[COUNTER_OCCLUDED], 1u);
}
uint visible = (inview ? 1u : 0u);
if (visible != record.visible)
records[index].visible = visible;
}

View File

@@ -1,6 +1,9 @@
// fills in the instances of the indirect draw commands of the entries, after stage 1 of instance_cull.comp wrote the
// modelview matrices of the instances which passed. one invocation per command: the instance count is the count of its
// entry, the base instance the first slot of the entry in the output (its cursor, advanced by stage 1, less the count)
// fills in the instances of the indirect draw commands of the entries, after instance_cull.comp wrote the modelview
// matrices of the instances drawn. one invocation per command. the commands are kept three times over: the first copy
// draws the instances of stage 1, the second all of them, the third those of stage 2 (two phases), each from its first
// slot in the output on. stage 0 (after stage 1): the instance count of the first copy is how far stage 1 advanced the
// cursor of its entry, the base instance the first slot of the entry. stage 1 (after stage 2): the other two copies take
// the command of the first one, the second with the instances of both stages, the third with the ones past the first copy
#include <common>
@@ -18,9 +21,18 @@ layout(std430, binding = INSTANCE_COMMAND_SSBO_BINDING) buffer instance_command_
uint commands[];
};
// x: commands, y: entries
// x: commands (of one copy), y: entries, z: stage
layout(location = 0) uniform uvec4 counts;
void set_instances(uint field, uint count, uint base)
{
commands[field + 1u] = count;
if (commands[field + 6u] != 0u)
commands[field + 4u] = base;
else
commands[field + 3u] = base;
}
void main()
{
uint command = gl_GlobalInvocationID.x;
@@ -29,11 +41,22 @@ void main()
uint field = command * 8u;
uint entry = commands[field + 5u];
uint count = counters[INSTANCE_COUNTER_HEADER + entry];
uint base = counters[INSTANCE_COUNTER_HEADER + counts.y + entry] - count;
commands[field + 1u] = count;
if (commands[field + 6u] != 0u)
commands[field + 4u] = base;
else
commands[field + 3u] = base;
uint first = counters[INSTANCE_COUNTER_HEADER + 2u * counts.y + entry];
uint count = counters[INSTANCE_COUNTER_HEADER + counts.y + entry] - first;
if (counts.z == 0u)
{
set_instances(field, count, first);
return;
}
uint drawnfirst = commands[field + 1u];
uint all = (counts.x + command) * 8u;
uint second = (2u * counts.x + command) * 8u;
for (uint value = 0u; value < 8u; ++value)
{
commands[all + value] = commands[field + value];
commands[second + value] = commands[field + value];
}
set_instances(all, count, first);
set_instances(second, count - drawnfirst, first + drawnfirst);
}

View File

@@ -1,5 +1,6 @@
// 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.
// entries in the output (cursors, which stages 1 and 2 advance as they write, and the same first slots kept for
// instance_cull_commands.comp), 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 <common>
@@ -46,6 +47,7 @@ void main()
for (uint entry = begin; entry < end; ++entry)
{
counters[INSTANCE_COUNTER_HEADER + entries + entry] = offset;
counters[INSTANCE_COUNTER_HEADER + 2u * entries + entry] = offset;
offset += counters[INSTANCE_COUNTER_HEADER + entry];
}
if (invocation == INSTANCE_OFFSETS_GROUP_SIZE - 1u)

View File

@@ -1439,6 +1439,11 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token
Parser.getTokens(1);
Parser >> gfx_gpucull_occlusion;
}
else if (Token == "gfx.gpucull.occlusion.instances")
{
Parser.getTokens(1);
Parser >> gfx_gpucull_occlusion_instances;
}
else if (Token == "gfx.depthprepass")
{
Parser.getTokens(1);
@@ -1934,6 +1939,7 @@ global_settings::export_as_text( std::ostream &Output ) const {
export_as_text( Output, "gfx.lights.clusterheatmap", gfx_lights_clusterheatmap );
export_as_text( Output, "gfx.gpucull", std::string{ gfx_gpucull == 1 ? "yes" : gfx_gpucull == 2 ? "compare" : "no" } );
export_as_text( Output, "gfx.gpucull.occlusion", gfx_gpucull_occlusion );
export_as_text( Output, "gfx.gpucull.occlusion.instances", gfx_gpucull_occlusion_instances );
export_as_text( Output, "gfx.depthprepass", gfx_depthprepass );
export_as_text( Output, "gfx.depthprepass.scene", gfx_depthprepass_scene );
export_as_text( Output, "gfx.bufferstreaming", gfx_bufferstreaming );

View File

@@ -289,6 +289,7 @@ struct global_settings {
// drawn from with indirect draws, 2 on the cpu, which draws, and in a compute pass too, for the results to be compared
int gfx_gpucull = 0;
bool gfx_gpucull_occlusion = true; // with gfx_gpucull 1: instances hidden behind the depth drawn before them are left out
bool gfx_gpucull_occlusion_instances = true; // with gfx_gpucull_occlusion: in two phases, so that instances hide one another too
bool gfx_depthprepass = true; // with gfx_gpucull 1: the depth of the instances is drawn before their colour
// (opengl 4.6) the depth of the whole opaque scene of the main view is drawn before its colour, the instances included
bool gfx_depthprepass_scene = false;