From 8c8ef5c32bb87fe92f64231ad747cd556c9c11c7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 12 Apr 2026 19:17:44 +1000 Subject: [PATCH] Leaf-batched BVH draw commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a BVH leaf passes the frustum test, emit a single glMultiDrawElements record covering the leaf's entire index range instead of one per object. Leaves are contiguous in the EBO after reorderEbo, so the range is just [first_object.index_offset, sum(index_count)]. Cuts draw calls by ~8x (BVH_MAX_LEAF_SIZE) and shifts the bottleneck from CPU/driver per-draw overhead toward GPU vertex throughput. Per-object features (selection highlight, per-vertex color, object_id picking) are unchanged — they operate on vertex attributes, not draw state. Future per-object hide/override will use SSBO lookups sampled by object_id in the fragment shader. Slight overdraw from skipping per-object frustum tests within a leaf is negligible given median-split BVH tightness and spare tri throughput. Also adds visible_objects_ counter so stats still report true object counts (not leaf counts), plus leaf_draws/model_draws breakdown in the per-second frame log. Co-Authored-By: Claude Opus 4.6 --- src/ifcviewer/ViewportWindow.cpp | 40 +++++++++++++++++++++----------- src/ifcviewer/ViewportWindow.h | 1 + 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index c872f799b6..1c6ab78625 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -838,16 +838,25 @@ void ViewportWindow::traverseBvh(const ModelBvh& mbvh, const ModelGpuData& mgpu, continue; if (node.count > 0) { + // Leaf-batched draw: after reorderEbo, a leaf's objects occupy a + // contiguous EBO range. Emit one draw command covering all of them + // instead of N per-object tests/draws. The leaf AABB test above is + // already a conservative cull; any overdraw (up to BVH_MAX_LEAF_SIZE + // objects that may be fully outside the frustum but inside the leaf + // AABB) costs far less than the per-draw CPU/driver overhead we save. + uint32_t first_oi = mbvh.object_indices[node.right_or_first]; + const auto& first_obj = mgpu.draw_info[first_oi]; + uint32_t leaf_offset = first_obj.index_offset; + uint32_t leaf_count = 0; for (uint32_t i = 0; i < node.count; ++i) { uint32_t oi = mbvh.object_indices[node.right_or_first + i]; - const auto& obj = mgpu.draw_info[oi]; - if (aabbInFrustum(obj.aabb_min, obj.aabb_max, planes)) { - cmd.counts.push_back(static_cast(obj.index_count)); - cmd.offsets.push_back(reinterpret_cast( - static_cast(obj.index_offset))); - visible_triangles_ += obj.index_count / 3; - } + leaf_count += mgpu.draw_info[oi].index_count; } + cmd.counts.push_back(static_cast(leaf_count)); + cmd.offsets.push_back(reinterpret_cast( + static_cast(leaf_offset))); + visible_triangles_ += leaf_count / 3; + visible_objects_ += node.count; } else { if (sp < 63) { stack[sp++] = node.right_or_first; @@ -860,6 +869,7 @@ void ViewportWindow::traverseBvh(const ModelBvh& mbvh, const ModelGpuData& mgpu, void ViewportWindow::buildVisibleList(const QMatrix4x4& vp) { frame_draw_cmds_.clear(); visible_triangles_ = 0; + visible_objects_ = 0; // Extract 6 frustum planes from the view-projection matrix. float planes[6][4]; @@ -912,6 +922,7 @@ void ViewportWindow::buildVisibleList(const QMatrix4x4& vp) { cmd.offsets.push_back(reinterpret_cast( static_cast(obj.index_offset))); visible_triangles_ += obj.index_count / 3; + visible_objects_++; } } } @@ -964,9 +975,10 @@ void ViewportWindow::render() { frame_count_ = 0; accumulated_time_ = 0.0f; - uint32_t total_obj = 0, total_tri = 0, vis_obj = 0; + uint32_t total_obj = 0, total_tri = 0; size_t total_vram = 0, total_vbo = 0, total_ebo = 0; size_t num_models = 0, num_hidden = 0; + size_t total_leaf_draws = 0; for (const auto& [mid, m] : models_gpu_) { num_models++; if (m.hidden) { num_hidden++; continue; } @@ -977,29 +989,31 @@ void ViewportWindow::render() { } total_vram = total_vbo + total_ebo; for (const auto& cmd : frame_draw_cmds_) { - vis_obj += static_cast(cmd.counts.size()); + total_leaf_draws += cmd.counts.size(); } FrameStats stats; stats.fps = last_fps_; stats.frame_time_ms = 1000.0f / last_fps_; stats.total_objects = total_obj; - stats.visible_objects = vis_obj; + stats.visible_objects = visible_objects_; stats.total_triangles = total_tri; stats.visible_triangles = visible_triangles_; emit frameStatsUpdated(stats); - double vis_obj_pct = total_obj > 0 ? 100.0 * vis_obj / total_obj : 0.0; + double vis_obj_pct = total_obj > 0 ? 100.0 * visible_objects_ / total_obj : 0.0; double vis_tri_pct = total_tri > 0 ? 100.0 * visible_triangles_ / total_tri : 0.0; qDebug("[frame] %.1f fps %.2f ms obj %u/%u (%.1f%%) tri %u/%u (%.1f%%) " - "vram %.1f MB (vbo %.1f + ebo %.1f) models %zu (%zu hidden) draws %zu pending_uploads %zu", + "vram %.1f MB (vbo %.1f + ebo %.1f) models %zu (%zu hidden) " + "leaf_draws %zu model_draws %zu pending_uploads %zu", last_fps_, 1000.0f / last_fps_, - vis_obj, total_obj, vis_obj_pct, + visible_objects_, total_obj, vis_obj_pct, visible_triangles_, total_tri, vis_tri_pct, total_vram / (1024.0 * 1024.0), total_vbo / (1024.0 * 1024.0), total_ebo / (1024.0 * 1024.0), num_models, num_hidden, + total_leaf_draws, frame_draw_cmds_.size(), pending_uploads_.size()); } diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 62abc48002..97925e6e2e 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -201,6 +201,7 @@ private: }; std::vector frame_draw_cmds_; uint32_t visible_triangles_ = 0; + uint32_t visible_objects_ = 0; // Camera QVector3D camera_target_{0, 0, 0};