Revert "ifcviewer: GPU LOD0/LOD1 selection in compute cull (step 3c)"

This reverts commit 77cac3ec170b622db6977829f66b62603266a047.
This commit is contained in:
Dion Moult
2026-04-17 18:34:01 +10:00
parent 3c5c8e44cb
commit 643a2e1c1f
3 changed files with 101 additions and 167 deletions
+8 -12
View File
@@ -795,18 +795,14 @@ single giant model / <18 cores CPU BVH trv Phase 3E GPU cull (plann
- [x] Event-driven rendering (zero idle CPU/GPU, cull skipped on still frames)
- [~] **Phase 3E — GPU-side compute-shader culling** (in progress)
- [x] 3a: `IFC_GPU_CULL=1` drives rendering via compute cull (frustum +
contribution). Perf regressed — submits one sub-draw per mesh
even when `instanceCount=0` (CP overhead from empty commands).
- [x] 3b: fwd/rev reflection bucketing — compact shader routes by
reflected flag into CCW and CW MDI buckets.
- [x] 3c: LOD0/LOD1 selection — compact shader computes per-instance
pixel radius and routes to LOD1 bucket when below threshold.
Per-mesh `has_lod1` flags SSBO. 4 buckets per mesh (fwd/rev ×
LOD0/LOD1), 4M commands total, 2 MDIs per model.
contribution, single bucket per mesh). Correctness matches CPU
path; perf regressed — we submit one sub-draw per mesh even
when `instanceCount=0`. Fix is MDI compaction via
`glMultiDrawElementsIndirectCount`, deferred to 3a-followup so
we don't pull a GL 4.6 entrypoint loader into this commit.
- [ ] 3a-followup: compact non-empty commands, use count-buffer MDI
- [ ] 3b: fwd/rev reflection bucketing on GPU
- [ ] 3c: LOD0/LOD1 selection on GPU
- [ ] 3d: HiZ with same-frame depth pre-pass
- [ ] MDI compaction — compact non-empty commands into contiguous
buffer, use `glMultiDrawElementsIndirectCount` (GL 4.6 /
`ARB_indirect_parameters`). Deferred until all feature buckets
land so we can introduce GL 4.6 loading once, cleanly.
- [ ] Vulkan/MoltenVK backend for macOS
- [ ] Embedded Python scripting console
+87 -146
View File
@@ -280,19 +280,17 @@ layout(local_size_x = 64) in;
// Each instance contributes two vec4 entries: (min.xyz, mesh_id_as_float),
// (max.xyz, flags_as_float). mesh_id is packed via floatBitsToUint;
// flags bit 0 = reflected (winding-bucket selector).
layout(std430, binding = 0) readonly buffer AabbBuf { vec4 entries[]; };
layout(std430, binding = 1) coherent buffer IndirectBuf { uint ind[]; };
layout(std430, binding = 2) writeonly buffer VisibleBuf { uint visible[]; };
layout(std430, binding = 3) readonly buffer MeshBaseBuf { uint mesh_base[]; };
layout(std430, binding = 4) readonly buffer MeshFlagsBuf { uint mesh_flags[]; };
layout(std430, binding = 0) readonly buffer AabbBuf { vec4 entries[]; };
layout(std430, binding = 1) coherent buffer IndirectBuf { uint ind[]; };
layout(std430, binding = 2) writeonly buffer VisibleBuf { uint visible[]; };
layout(std430, binding = 3) readonly buffer MeshBaseBuf { uint mesh_base[]; };
uniform vec4 u_planes[6];
uniform uint u_count; // num instances
uniform uint u_M; // unique meshes per model
uniform uint u_count; // num instances
uniform uint u_fwd_mesh_count; // M; reflected bucket is mesh_id + M
uniform vec3 u_camera_eye;
uniform float u_focal_px;
uniform float u_min_pixel_radius;
uniform float u_lod1_px_threshold;
bool frustum(vec3 mn, vec3 mx) {
for (int i = 0; i < 6; ++i) {
@@ -305,14 +303,16 @@ bool frustum(vec3 mn, vec3 mx) {
return true;
}
float pixelRadius(vec3 mn, vec3 mx) {
bool contribution(vec3 mn, vec3 mx) {
if (u_min_pixel_radius <= 0.0) return true;
// Camera inside the AABB -> always keep (matches CPU path).
if (all(greaterThanEqual(u_camera_eye, mn)) &&
all(lessThanEqual (u_camera_eye, mx))) return 1e30;
all(lessThanEqual (u_camera_eye, mx))) return true;
vec3 ctr = 0.5 * (mx + mn);
vec3 ext = 0.5 * (mx - mn);
float radius = length(ext);
float dist = distance(ctr, u_camera_eye);
return u_focal_px * radius / max(dist, 0.001);
return u_focal_px * radius >= u_min_pixel_radius * dist;
}
void main() {
@@ -322,24 +322,13 @@ void main() {
vec4 hi = entries[gid * 2u + 1u];
vec3 mn = lo.xyz;
vec3 mx = hi.xyz;
if (!frustum(mn, mx)) return;
float px_rad = pixelRadius(mn, mx);
if (px_rad < u_min_pixel_radius) return;
if (!frustum(mn, mx)) return;
if (!contribution(mn, mx)) return;
uint mesh_id = floatBitsToUint(lo.w);
uint flags = floatBitsToUint(hi.w);
bool reflected = (flags & 1u) != 0u;
bool want_lod1 = (mesh_flags[mesh_id] & 1u) != 0u
&& u_lod1_px_threshold > 0.0
&& px_rad < u_lod1_px_threshold;
// Bucket layout: [0..M) fwd_lod0, [M..2M) fwd_lod1,
// [2M..3M) rev_lod0, [3M..4M) rev_lod1.
uint bucket = mesh_id;
if (want_lod1) bucket += u_M;
if (reflected) bucket += 2u * u_M;
uint local = atomicAdd(ind[bucket * 5u + 1u], 1u);
uint bucket = ((flags & 1u) != 0u) ? (mesh_id + u_fwd_mesh_count)
: mesh_id;
uint local = atomicAdd(ind[bucket * 5u + 1u], 1u);
visible[mesh_base[bucket] + local] = gid;
}
)";
@@ -522,10 +511,9 @@ ViewportWindow::~ViewportWindow() {
if (m.visible_ssbo) gl_->glDeleteBuffers(1, &m.visible_ssbo);
if (m.indirect_buffer) gl_->glDeleteBuffers(1, &m.indirect_buffer);
if (m.aabb_ssbo) gl_->glDeleteBuffers(1, &m.aabb_ssbo);
if (m.gpu_indirect_buffer) gl_->glDeleteBuffers(1, &m.gpu_indirect_buffer);
if (m.gpu_visible_ssbo) gl_->glDeleteBuffers(1, &m.gpu_visible_ssbo);
if (m.gpu_mesh_base_ssbo) gl_->glDeleteBuffers(1, &m.gpu_mesh_base_ssbo);
if (m.gpu_mesh_flags_ssbo) gl_->glDeleteBuffers(1, &m.gpu_mesh_flags_ssbo);
if (m.gpu_indirect_buffer) gl_->glDeleteBuffers(1, &m.gpu_indirect_buffer);
if (m.gpu_visible_ssbo) gl_->glDeleteBuffers(1, &m.gpu_visible_ssbo);
if (m.gpu_mesh_base_ssbo) gl_->glDeleteBuffers(1, &m.gpu_mesh_base_ssbo);
}
if (axis_vao_) gl_->glDeleteVertexArrays(1, &axis_vao_);
if (axis_vbo_) gl_->glDeleteBuffers(1, &axis_vbo_);
@@ -930,13 +918,12 @@ void ViewportWindow::uploadInstanceAabbs(ModelGpuData& m) {
void ViewportWindow::uploadGpuCullStaticBuffers(ModelGpuData& m) {
const uint32_t M = static_cast<uint32_t>(m.meshes.size());
m.gpu_mesh_command_count = 4u * M;
m.gpu_mesh_command_count = 2u * M;
m.gpu_forward_command_count = M;
// Count fwd / rev instances per mesh. LOD is dynamic (depends on
// camera distance), so each LOD bucket reserves worst-case capacity
// = the full fwd or rev count for that mesh. Total visible slots =
// 2 × total_instances (each instance only fills one bucket per frame).
// Count fwd / rev instances per mesh so each bucket gets a tight
// per-mesh slot range. (Sum of fwd + rev = total_instances, so the
// visible buffer is no bigger than the single-bucket version.)
std::vector<uint32_t> fwd_n(M, 0), rev_n(M, 0);
for (size_t i = 0; i < m.instances.size(); ++i) {
const uint32_t mid = m.instances[i].mesh_id;
@@ -946,52 +933,39 @@ void ViewportWindow::uploadGpuCullStaticBuffers(ModelGpuData& m) {
(reflected ? rev_n[mid] : fwd_n[mid]) += 1u;
}
// Per-mesh flags SSBO: bit 0 = has_lod1. Read by the compact shader
// to decide whether LOD1 routing is possible for a given mesh_id.
std::vector<uint32_t> mesh_flags(M, 0);
for (uint32_t i = 0; i < M; ++i) {
if (m.meshes[i].lod1_index_count > 0) mesh_flags[i] |= 1u;
}
// Build 4M commands and 4M mesh_base entries.
// [0..M) fwd_lod0 [M..2M) fwd_lod1
// [2M..3M) rev_lod0 [3M..4M) rev_lod1
// Each LOD0 command uses mesh.index_count / ebo_byte_offset;
// each LOD1 command uses mesh.lod1_index_count / lod1_ebo_byte_offset
// (count=0 if mesh has no LOD1 → MDI skips automatically).
std::vector<uint32_t> mesh_base(4u * M, 0);
std::vector<DrawElementsIndirectCommand> indir(4u * M);
auto fill_bucket = [&](uint32_t bucket_offset, bool use_lod1,
const std::vector<uint32_t>& capacity,
uint32_t& running) {
for (uint32_t i = 0; i < M; ++i) {
const MeshInfo& mesh = m.meshes[i];
const uint32_t slot = bucket_offset + i;
mesh_base[slot] = running;
DrawElementsIndirectCommand& cmd = indir[slot];
cmd.count = use_lod1 ? mesh.lod1_index_count : mesh.index_count;
cmd.instanceCount = 0;
cmd.firstIndex = use_lod1
? (mesh.lod1_ebo_byte_offset / sizeof(uint32_t))
: (mesh.ebo_byte_offset / sizeof(uint32_t));
cmd.baseVertex = mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES;
cmd.baseInstance = running;
running += capacity[i];
}
};
// Prefix sums. mesh_base[0..M) for fwd, mesh_base[M..2M) for rev.
// Same layout for the indirect commands. baseInstance of each
// command points at its visible[] slot so the vertex shader's
// gl_BaseInstanceARB + gl_InstanceID indexes directly into it.
std::vector<uint32_t> mesh_base(2u * M, 0);
std::vector<DrawElementsIndirectCommand> indir(2u * M);
uint32_t running = 0;
fill_bucket(0, false, fwd_n, running); // fwd_lod0
fill_bucket(M, true, fwd_n, running); // fwd_lod1
fill_bucket(2u * M, false, rev_n, running); // rev_lod0
fill_bucket(3u * M, true, rev_n, running); // rev_lod1
const uint32_t total_slots = running; // = 2 × total_instances
for (uint32_t i = 0; i < M; ++i) {
const MeshInfo& mesh = m.meshes[i];
mesh_base[i] = running;
DrawElementsIndirectCommand& cmd = indir[i];
cmd.count = mesh.index_count;
cmd.instanceCount = 0;
cmd.firstIndex = mesh.ebo_byte_offset / sizeof(uint32_t);
cmd.baseVertex = mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES;
cmd.baseInstance = running;
running += fwd_n[i];
}
for (uint32_t i = 0; i < M; ++i) {
const MeshInfo& mesh = m.meshes[i];
mesh_base[M + i] = running;
DrawElementsIndirectCommand& cmd = indir[M + i];
cmd.count = mesh.index_count;
cmd.instanceCount = 0;
cmd.firstIndex = mesh.ebo_byte_offset / sizeof(uint32_t);
cmd.baseVertex = mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES;
cmd.baseInstance = running;
running += rev_n[i];
}
const uint32_t total_instances = running;
// --- GPU buffer uploads ---
// Indirect buffer — 4M commands.
const size_t ind_bytes = std::max<size_t>(4u * M * sizeof(DrawElementsIndirectCommand),
// Indirect buffer — 2M commands (fwd bucket then rev bucket).
const size_t ind_bytes = std::max<size_t>(2u * M * sizeof(DrawElementsIndirectCommand),
sizeof(DrawElementsIndirectCommand));
if (m.gpu_indirect_buffer && m.gpu_indirect_capacity < ind_bytes) {
gl_->glDeleteBuffers(1, &m.gpu_indirect_buffer);
@@ -1006,11 +980,11 @@ void ViewportWindow::uploadGpuCullStaticBuffers(ModelGpuData& m) {
}
if (M > 0) {
gl_->glNamedBufferSubData(m.gpu_indirect_buffer, 0,
4u * M * sizeof(DrawElementsIndirectCommand), indir.data());
2u * M * sizeof(DrawElementsIndirectCommand), indir.data());
}
// Visible list — worst-case 2 × total_instances.
const size_t vis_bytes = std::max<size_t>(total_slots * sizeof(uint32_t),
// Visible list — exact: fwd + rev per-mesh counts sum to total_instances.
const size_t vis_bytes = std::max<size_t>(total_instances * sizeof(uint32_t),
sizeof(uint32_t));
if (m.gpu_visible_ssbo && m.gpu_visible_capacity < vis_bytes) {
gl_->glDeleteBuffers(1, &m.gpu_visible_ssbo);
@@ -1024,8 +998,8 @@ void ViewportWindow::uploadGpuCullStaticBuffers(ModelGpuData& m) {
m.gpu_visible_capacity = vis_bytes;
}
// Mesh-base SSBO — 4M entries.
const size_t mb_bytes = std::max<size_t>(4u * M * sizeof(uint32_t), sizeof(uint32_t));
// Mesh-base SSBO — 2M entries (one per bucket).
const size_t mb_bytes = std::max<size_t>(2u * M * sizeof(uint32_t), sizeof(uint32_t));
if (m.gpu_mesh_base_ssbo && m.gpu_mesh_base_capacity < mb_bytes) {
gl_->glDeleteBuffers(1, &m.gpu_mesh_base_ssbo);
m.gpu_mesh_base_ssbo = 0;
@@ -1039,25 +1013,7 @@ void ViewportWindow::uploadGpuCullStaticBuffers(ModelGpuData& m) {
}
if (M > 0) {
gl_->glNamedBufferSubData(m.gpu_mesh_base_ssbo, 0,
4u * M * sizeof(uint32_t), mesh_base.data());
}
// Mesh-flags SSBO — M entries; bit 0 = has_lod1.
const size_t mf_bytes = std::max<size_t>(M * sizeof(uint32_t), sizeof(uint32_t));
if (m.gpu_mesh_flags_ssbo && m.gpu_mesh_flags_capacity < mf_bytes) {
gl_->glDeleteBuffers(1, &m.gpu_mesh_flags_ssbo);
m.gpu_mesh_flags_ssbo = 0;
m.gpu_mesh_flags_capacity = 0;
}
if (!m.gpu_mesh_flags_ssbo) {
gl_->glCreateBuffers(1, &m.gpu_mesh_flags_ssbo);
gl_->glNamedBufferStorage(m.gpu_mesh_flags_ssbo, mf_bytes, nullptr,
GL_DYNAMIC_STORAGE_BIT);
m.gpu_mesh_flags_capacity = mf_bytes;
}
if (M > 0) {
gl_->glNamedBufferSubData(m.gpu_mesh_flags_ssbo, 0,
M * sizeof(uint32_t), mesh_flags.data());
2u * M * sizeof(uint32_t), mesh_base.data());
}
}
@@ -1135,10 +1091,9 @@ void ViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) {
if (existing->second.visible_ssbo) gl_->glDeleteBuffers(1, &existing->second.visible_ssbo);
if (existing->second.indirect_buffer) gl_->glDeleteBuffers(1, &existing->second.indirect_buffer);
if (existing->second.aabb_ssbo) gl_->glDeleteBuffers(1, &existing->second.aabb_ssbo);
if (existing->second.gpu_indirect_buffer) gl_->glDeleteBuffers(1, &existing->second.gpu_indirect_buffer);
if (existing->second.gpu_visible_ssbo) gl_->glDeleteBuffers(1, &existing->second.gpu_visible_ssbo);
if (existing->second.gpu_mesh_base_ssbo) gl_->glDeleteBuffers(1, &existing->second.gpu_mesh_base_ssbo);
if (existing->second.gpu_mesh_flags_ssbo) gl_->glDeleteBuffers(1, &existing->second.gpu_mesh_flags_ssbo);
if (existing->second.gpu_indirect_buffer) gl_->glDeleteBuffers(1, &existing->second.gpu_indirect_buffer);
if (existing->second.gpu_visible_ssbo) gl_->glDeleteBuffers(1, &existing->second.gpu_visible_ssbo);
if (existing->second.gpu_mesh_base_ssbo) gl_->glDeleteBuffers(1, &existing->second.gpu_mesh_base_ssbo);
models_gpu_.erase(existing);
}
@@ -1284,10 +1239,9 @@ void ViewportWindow::resetScene() {
if (m.visible_ssbo) gl_->glDeleteBuffers(1, &m.visible_ssbo);
if (m.indirect_buffer) gl_->glDeleteBuffers(1, &m.indirect_buffer);
if (m.aabb_ssbo) gl_->glDeleteBuffers(1, &m.aabb_ssbo);
if (m.gpu_indirect_buffer) gl_->glDeleteBuffers(1, &m.gpu_indirect_buffer);
if (m.gpu_visible_ssbo) gl_->glDeleteBuffers(1, &m.gpu_visible_ssbo);
if (m.gpu_mesh_base_ssbo) gl_->glDeleteBuffers(1, &m.gpu_mesh_base_ssbo);
if (m.gpu_mesh_flags_ssbo) gl_->glDeleteBuffers(1, &m.gpu_mesh_flags_ssbo);
if (m.gpu_indirect_buffer) gl_->glDeleteBuffers(1, &m.gpu_indirect_buffer);
if (m.gpu_visible_ssbo) gl_->glDeleteBuffers(1, &m.gpu_visible_ssbo);
if (m.gpu_mesh_base_ssbo) gl_->glDeleteBuffers(1, &m.gpu_mesh_base_ssbo);
}
models_gpu_.clear();
selected_object_id_ = 0;
@@ -1326,10 +1280,9 @@ void ViewportWindow::removeModel(uint32_t model_id) {
if (it->second.visible_ssbo) gl_->glDeleteBuffers(1, &it->second.visible_ssbo);
if (it->second.indirect_buffer) gl_->glDeleteBuffers(1, &it->second.indirect_buffer);
if (it->second.aabb_ssbo) gl_->glDeleteBuffers(1, &it->second.aabb_ssbo);
if (it->second.gpu_indirect_buffer) gl_->glDeleteBuffers(1, &it->second.gpu_indirect_buffer);
if (it->second.gpu_visible_ssbo) gl_->glDeleteBuffers(1, &it->second.gpu_visible_ssbo);
if (it->second.gpu_mesh_base_ssbo) gl_->glDeleteBuffers(1, &it->second.gpu_mesh_base_ssbo);
if (it->second.gpu_mesh_flags_ssbo) gl_->glDeleteBuffers(1, &it->second.gpu_mesh_flags_ssbo);
if (it->second.gpu_indirect_buffer) gl_->glDeleteBuffers(1, &it->second.gpu_indirect_buffer);
if (it->second.gpu_visible_ssbo) gl_->glDeleteBuffers(1, &it->second.gpu_visible_ssbo);
if (it->second.gpu_mesh_base_ssbo) gl_->glDeleteBuffers(1, &it->second.gpu_mesh_base_ssbo);
models_gpu_.erase(it);
have_cached_cull_ = false;
requestUpdate();
@@ -2009,13 +1962,10 @@ void ViewportWindow::render() {
// Phase 3E: the GPU-cull path. When IFC_GPU_CULL=1 we dispatch two
// tiny compute shaders per model (reset + compact), then let the draw
// loop below issue MDI from gpu_indirect_buffer. 4M commands per model:
// fwd_lod0, fwd_lod1, rev_lod0, rev_lod1. Two MDIs: CCW for [0..2M),
// CW for [2M..4M). HiZ still CPU-only.
static const float gpu_lod1_px_threshold = []{
const char* e = std::getenv("IFC_LOD1_PX");
return (e && *e) ? static_cast<float>(std::atof(e)) : 30.0f;
}();
// loop below issue MDI from gpu_indirect_buffer. Commands are laid
// out as two buckets of M entries each — fwd (CCW) then rev (CW) —
// so reflected instances render with correct winding. LOD and HiZ
// still live only on the CPU path.
if (gpu_cull_enabled && cull_this_frame && cull_compact_program_) {
QElapsedTimer t; t.start();
float planes_flat[24];
@@ -2029,11 +1979,11 @@ void ViewportWindow::render() {
for (auto& [mid, m] : models_gpu_) {
if (m.hidden || !m.aabb_ssbo || m.instances.empty()) continue;
if (!m.gpu_indirect_buffer || !m.gpu_visible_ssbo ||
!m.gpu_mesh_base_ssbo || !m.gpu_mesh_flags_ssbo) continue;
!m.gpu_mesh_base_ssbo) continue;
const uint32_t n = static_cast<uint32_t>(m.instances.size());
total_in += n;
// Reset — zero instanceCount on all 4M commands.
// Reset — zero instanceCount on all M commands.
gl_->glUseProgram(cull_reset_program_);
gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m.gpu_indirect_buffer);
gl_->glUniform1ui(gl_->glGetUniformLocation(cull_reset_program_, "u_mesh_count"),
@@ -2041,17 +1991,16 @@ void ViewportWindow::render() {
gl_->glDispatchCompute((m.gpu_mesh_command_count + 63u) / 64u, 1, 1);
gl_->glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
// Compact — frustum + contribution cull, LOD select, scatter.
// Compact — test + scatter.
gl_->glUseProgram(cull_compact_program_);
gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m.aabb_ssbo);
gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m.gpu_indirect_buffer);
gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m.gpu_visible_ssbo);
gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, m.gpu_mesh_base_ssbo);
gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, m.gpu_mesh_flags_ssbo);
gl_->glUniform4fv(gl_->glGetUniformLocation(cull_compact_program_, "u_planes"),
6, planes_flat);
gl_->glUniform1ui(gl_->glGetUniformLocation(cull_compact_program_, "u_count"), n);
gl_->glUniform1ui(gl_->glGetUniformLocation(cull_compact_program_, "u_M"),
gl_->glUniform1ui(gl_->glGetUniformLocation(cull_compact_program_, "u_fwd_mesh_count"),
m.gpu_forward_command_count);
gl_->glUniform3f (gl_->glGetUniformLocation(cull_compact_program_, "u_camera_eye"),
camera_eye_.x(), camera_eye_.y(), camera_eye_.z());
@@ -2059,8 +2008,6 @@ void ViewportWindow::render() {
focal_px);
gl_->glUniform1f (gl_->glGetUniformLocation(cull_compact_program_, "u_min_pixel_radius"),
min_pixel_radius);
gl_->glUniform1f (gl_->glGetUniformLocation(cull_compact_program_, "u_lod1_px_threshold"),
gpu_lod1_px_threshold);
gl_->glDispatchCompute((n + 63u) / 64u, 1, 1);
}
gl_->glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
@@ -2089,9 +2036,10 @@ void ViewportWindow::render() {
if (m.hidden || !m.ssbo || m.ssbo_instance_count == 0) continue;
if (gpu_cull_enabled) {
// GPU path: compact shader routed survivors into 4 buckets
// (fwd_lod0, fwd_lod1, rev_lod0, rev_lod1), each with M
// commands. CCW MDI for [0..2M), CW MDI for [2M..4M).
// GPU path: compact shader routed survivors into fwd/rev
// buckets (commands [0..M) and [M..2M)). Two MDIs: CCW then
// CW. LOD and HiZ still CPU-only; reflected winding is now
// correct.
if (!m.gpu_indirect_buffer || !m.gpu_visible_ssbo ||
m.gpu_mesh_command_count == 0) continue;
@@ -2101,9 +2049,8 @@ void ViewportWindow::render() {
gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m.mesh_info_ssbo);
gl_->glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m.gpu_indirect_buffer);
const uint32_t M = m.gpu_forward_command_count;
uint32_t fwd = 2u * M; // fwd_lod0 + fwd_lod1
uint32_t rev = 2u * M; // rev_lod0 + rev_lod1
uint32_t fwd = m.gpu_forward_command_count;
uint32_t rev = m.gpu_mesh_command_count - fwd;
if (max_subdraws < m.gpu_mesh_command_count) {
const uint32_t total = m.gpu_mesh_command_count;
fwd = static_cast<uint32_t>((uint64_t)fwd * max_subdraws / total);
@@ -2120,7 +2067,7 @@ void ViewportWindow::render() {
gl_->glFrontFace(GL_CW);
gl_->glMultiDrawElementsIndirect(
GL_TRIANGLES, GL_UNSIGNED_INT,
reinterpret_cast<const void*>(2u * M * sizeof(DrawElementsIndirectCommand)),
reinterpret_cast<const void*>(m.gpu_forward_command_count * sizeof(DrawElementsIndirectCommand)),
static_cast<GLsizei>(rev), 0);
++gl_draw_calls_;
gl_->glFrontFace(GL_CCW);
@@ -2236,21 +2183,15 @@ void ViewportWindow::render() {
gl_->glGetNamedBufferSubData(mm.gpu_indirect_buffer, 0,
mm.gpu_mesh_command_count * sizeof(DrawElementsIndirectCommand),
readback.data());
// Buckets: [0..M) fwd_lod0, [M..2M) fwd_lod1,
// [2M..3M) rev_lod0, [3M..4M) rev_lod1.
// Commands [0..M) are fwd, [M..2M) are rev for the same
// mesh — index meshes[] modulo forward_command_count.
const uint32_t M = mm.gpu_forward_command_count;
for (uint32_t i = 0; i < mm.gpu_mesh_command_count; ++i) {
const uint32_t ic = readback[i].instanceCount;
if (ic == 0) continue;
const uint32_t mesh_i = (M > 0) ? (i % M) : 0;
const bool is_lod1 = M > 0
&& ((i / M) == 1 || (i / M) == 3);
const uint32_t idx_count = is_lod1
? mm.meshes[mesh_i].lod1_index_count
: mm.meshes[mesh_i].index_count;
gpu_surv += ic;
gpu_obj += ic;
gpu_tri += ic * (idx_count / 3u);
gpu_tri += ic * (mm.meshes[mesh_i].index_count / 3u);
}
}
gpu_cull_last_survivors_ = gpu_surv;
+6 -9
View File
@@ -108,21 +108,18 @@ struct ModelGpuData {
// the instanceCount field of gpu_indirect_buffer is rewritten by the
// cull shader (zeroed by the reset shader, atomically incremented as
// survivors are appended into gpu_visible_ssbo at mesh_base[i] + local).
// Layout per model — 4 buckets of M commands each:
// [0..M) fwd_lod0 (non-reflected, LOD0, CCW winding)
// [M..2M) fwd_lod1 (non-reflected, LOD1, CCW winding)
// [2M..3M) rev_lod0 (reflected, LOD0, CW winding)
// [3M..4M) rev_lod1 (reflected, LOD1, CW winding)
// gpu_mesh_command_count = 4M; gpu_forward_command_count = M.
// Two MDIs: CCW for [0..2M), CW for [2M..4M).
// Layout per model:
// commands[0..M) fwd bucket (non-reflected, CCW winding)
// commands[M..2M) rev bucket (reflected, CW winding)
// gpu_mesh_command_count = 2M; gpu_forward_command_count = M.
// Each bucket gets its own mesh_base[] slot and its own visible[]
// range, sized to the exact per-mesh count of fwd / rev instances.
GLuint gpu_indirect_buffer = 0;
size_t gpu_indirect_capacity = 0;
GLuint gpu_visible_ssbo = 0;
size_t gpu_visible_capacity = 0;
GLuint gpu_mesh_base_ssbo = 0;
size_t gpu_mesh_base_capacity = 0;
GLuint gpu_mesh_flags_ssbo = 0;
size_t gpu_mesh_flags_capacity = 0;
uint32_t gpu_mesh_command_count = 0;
uint32_t gpu_forward_command_count = 0;