diff --git a/src/ifcviewer/README.md b/src/ifcviewer/README.md index be7d7a1368..81c43b324c 100644 --- a/src/ifcviewer/README.md +++ b/src/ifcviewer/README.md @@ -795,14 +795,18 @@ 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, 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 + 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. - [ ] 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 diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index f5fe67ecdd..5688a8e810 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -280,17 +280,19 @@ 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 = 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[]; }; uniform vec4 u_planes[6]; -uniform uint u_count; // num instances -uniform uint u_fwd_mesh_count; // M; reflected bucket is mesh_id + M +uniform uint u_count; // num instances +uniform uint u_M; // unique meshes per model 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) { @@ -303,16 +305,14 @@ bool frustum(vec3 mn, vec3 mx) { return true; } -bool contribution(vec3 mn, vec3 mx) { - if (u_min_pixel_radius <= 0.0) return true; - // Camera inside the AABB -> always keep (matches CPU path). +float pixelRadius(vec3 mn, vec3 mx) { if (all(greaterThanEqual(u_camera_eye, mn)) && - all(lessThanEqual (u_camera_eye, mx))) return true; + all(lessThanEqual (u_camera_eye, mx))) return 1e30; 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 >= u_min_pixel_radius * dist; + return u_focal_px * radius / max(dist, 0.001); } void main() { @@ -322,13 +322,24 @@ void main() { vec4 hi = entries[gid * 2u + 1u]; vec3 mn = lo.xyz; vec3 mx = hi.xyz; - if (!frustum(mn, mx)) return; - if (!contribution(mn, mx)) return; + if (!frustum(mn, mx)) return; + float px_rad = pixelRadius(mn, mx); + if (px_rad < u_min_pixel_radius) return; + uint mesh_id = floatBitsToUint(lo.w); uint flags = floatBitsToUint(hi.w); - uint bucket = ((flags & 1u) != 0u) ? (mesh_id + u_fwd_mesh_count) - : mesh_id; - uint local = atomicAdd(ind[bucket * 5u + 1u], 1u); + 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); visible[mesh_base[bucket] + local] = gid; } )"; @@ -511,9 +522,10 @@ 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_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 (axis_vao_) gl_->glDeleteVertexArrays(1, &axis_vao_); if (axis_vbo_) gl_->glDeleteBuffers(1, &axis_vbo_); @@ -918,12 +930,13 @@ void ViewportWindow::uploadInstanceAabbs(ModelGpuData& m) { void ViewportWindow::uploadGpuCullStaticBuffers(ModelGpuData& m) { const uint32_t M = static_cast(m.meshes.size()); - m.gpu_mesh_command_count = 2u * M; + m.gpu_mesh_command_count = 4u * M; m.gpu_forward_command_count = M; - // 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.) + // 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). std::vector 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; @@ -933,39 +946,52 @@ void ViewportWindow::uploadGpuCullStaticBuffers(ModelGpuData& m) { (reflected ? rev_n[mid] : fwd_n[mid]) += 1u; } - // 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 mesh_base(2u * M, 0); - std::vector indir(2u * M); - uint32_t running = 0; + // 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 mesh_flags(M, 0); 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]; + if (m.meshes[i].lod1_index_count > 0) mesh_flags[i] |= 1u; } - 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; - // Indirect buffer — 2M commands (fwd bucket then rev bucket). - const size_t ind_bytes = std::max(2u * M * sizeof(DrawElementsIndirectCommand), + // 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 mesh_base(4u * M, 0); + std::vector indir(4u * M); + + auto fill_bucket = [&](uint32_t bucket_offset, bool use_lod1, + const std::vector& 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]; + } + }; + + 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 + + // --- GPU buffer uploads --- + + // Indirect buffer — 4M commands. + const size_t ind_bytes = std::max(4u * M * sizeof(DrawElementsIndirectCommand), sizeof(DrawElementsIndirectCommand)); if (m.gpu_indirect_buffer && m.gpu_indirect_capacity < ind_bytes) { gl_->glDeleteBuffers(1, &m.gpu_indirect_buffer); @@ -980,11 +1006,11 @@ void ViewportWindow::uploadGpuCullStaticBuffers(ModelGpuData& m) { } if (M > 0) { gl_->glNamedBufferSubData(m.gpu_indirect_buffer, 0, - 2u * M * sizeof(DrawElementsIndirectCommand), indir.data()); + 4u * M * sizeof(DrawElementsIndirectCommand), indir.data()); } - // Visible list — exact: fwd + rev per-mesh counts sum to total_instances. - const size_t vis_bytes = std::max(total_instances * sizeof(uint32_t), + // Visible list — worst-case 2 × total_instances. + const size_t vis_bytes = std::max(total_slots * sizeof(uint32_t), sizeof(uint32_t)); if (m.gpu_visible_ssbo && m.gpu_visible_capacity < vis_bytes) { gl_->glDeleteBuffers(1, &m.gpu_visible_ssbo); @@ -998,8 +1024,8 @@ void ViewportWindow::uploadGpuCullStaticBuffers(ModelGpuData& m) { m.gpu_visible_capacity = vis_bytes; } - // Mesh-base SSBO — 2M entries (one per bucket). - const size_t mb_bytes = std::max(2u * M * sizeof(uint32_t), sizeof(uint32_t)); + // Mesh-base SSBO — 4M entries. + const size_t mb_bytes = std::max(4u * 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; @@ -1013,7 +1039,25 @@ void ViewportWindow::uploadGpuCullStaticBuffers(ModelGpuData& m) { } if (M > 0) { gl_->glNamedBufferSubData(m.gpu_mesh_base_ssbo, 0, - 2u * M * sizeof(uint32_t), mesh_base.data()); + 4u * M * sizeof(uint32_t), mesh_base.data()); + } + + // Mesh-flags SSBO — M entries; bit 0 = has_lod1. + const size_t mf_bytes = std::max(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()); } } @@ -1091,9 +1135,10 @@ 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_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); models_gpu_.erase(existing); } @@ -1239,9 +1284,10 @@ 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_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); } models_gpu_.clear(); selected_object_id_ = 0; @@ -1280,9 +1326,10 @@ 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_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); models_gpu_.erase(it); have_cached_cull_ = false; requestUpdate(); @@ -1962,10 +2009,13 @@ 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. 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. + // 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(std::atof(e)) : 30.0f; + }(); if (gpu_cull_enabled && cull_this_frame && cull_compact_program_) { QElapsedTimer t; t.start(); float planes_flat[24]; @@ -1979,11 +2029,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) continue; + !m.gpu_mesh_base_ssbo || !m.gpu_mesh_flags_ssbo) continue; const uint32_t n = static_cast(m.instances.size()); total_in += n; - // Reset — zero instanceCount on all M commands. + // Reset — zero instanceCount on all 4M 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"), @@ -1991,16 +2041,17 @@ void ViewportWindow::render() { gl_->glDispatchCompute((m.gpu_mesh_command_count + 63u) / 64u, 1, 1); gl_->glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - // Compact — test + scatter. + // Compact — frustum + contribution cull, LOD select, 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_fwd_mesh_count"), + gl_->glUniform1ui(gl_->glGetUniformLocation(cull_compact_program_, "u_M"), m.gpu_forward_command_count); gl_->glUniform3f (gl_->glGetUniformLocation(cull_compact_program_, "u_camera_eye"), camera_eye_.x(), camera_eye_.y(), camera_eye_.z()); @@ -2008,6 +2059,8 @@ 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); @@ -2036,10 +2089,9 @@ 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 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. + // 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). if (!m.gpu_indirect_buffer || !m.gpu_visible_ssbo || m.gpu_mesh_command_count == 0) continue; @@ -2049,8 +2101,9 @@ void ViewportWindow::render() { gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m.mesh_info_ssbo); gl_->glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m.gpu_indirect_buffer); - uint32_t fwd = m.gpu_forward_command_count; - uint32_t rev = m.gpu_mesh_command_count - fwd; + 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 if (max_subdraws < m.gpu_mesh_command_count) { const uint32_t total = m.gpu_mesh_command_count; fwd = static_cast((uint64_t)fwd * max_subdraws / total); @@ -2067,7 +2120,7 @@ void ViewportWindow::render() { gl_->glFrontFace(GL_CW); gl_->glMultiDrawElementsIndirect( GL_TRIANGLES, GL_UNSIGNED_INT, - reinterpret_cast(m.gpu_forward_command_count * sizeof(DrawElementsIndirectCommand)), + reinterpret_cast(2u * M * sizeof(DrawElementsIndirectCommand)), static_cast(rev), 0); ++gl_draw_calls_; gl_->glFrontFace(GL_CCW); @@ -2183,15 +2236,21 @@ void ViewportWindow::render() { gl_->glGetNamedBufferSubData(mm.gpu_indirect_buffer, 0, mm.gpu_mesh_command_count * sizeof(DrawElementsIndirectCommand), readback.data()); - // Commands [0..M) are fwd, [M..2M) are rev for the same - // mesh — index meshes[] modulo forward_command_count. + // Buckets: [0..M) fwd_lod0, [M..2M) fwd_lod1, + // [2M..3M) rev_lod0, [3M..4M) rev_lod1. 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 * (mm.meshes[mesh_i].index_count / 3u); + gpu_tri += ic * (idx_count / 3u); } } gpu_cull_last_survivors_ = gpu_surv; diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 22a206971e..12338596b9 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -108,18 +108,21 @@ 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: - // 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. + // 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). 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;