From 36fa53122a4792b34a4004853dd5a82203c3e295 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 12 Apr 2026 19:01:43 +1000 Subject: [PATCH] Add profiling for VRAM, FPS ratios, and instancing analysis Per-second frame log reports fps/ms, visible/total object & triangle ratios, VRAM breakdown (VBO+EBO), model count, and pending uploads. Upload-complete log includes per-model VBO/EBO MB and scene total VRAM. Streamer runs an instancing analysis keyed on geom.id(): total shapes, unique representations, dedup ratio, theoretical VBO/EBO/SSBO sizes if instanced, potential savings, and top-5 most-duplicated representations. Used to validate whether GPU instancing is worth the architectural rewrite for a given dataset. Co-Authored-By: Claude Opus 4.6 --- src/ifcviewer/GeometryStreamer.cpp | 103 +++++++++++++++++++++++++++++ src/ifcviewer/ViewportWindow.cpp | 41 ++++++++++-- 2 files changed, 139 insertions(+), 5 deletions(-) diff --git a/src/ifcviewer/GeometryStreamer.cpp b/src/ifcviewer/GeometryStreamer.cpp index 7235bced9f..54b37df70c 100644 --- a/src/ifcviewer/GeometryStreamer.cpp +++ b/src/ifcviewer/GeometryStreamer.cpp @@ -27,6 +27,9 @@ #include #include +#include +#include + GeometryStreamer::GeometryStreamer(QObject* parent) : QObject(parent) { @@ -126,6 +129,20 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { int last_progress = 0; + // Instancing analysis: count shapes grouped by representation id. + struct GeomStat { + uint32_t count = 0; + size_t vertex_count = 0; + size_t index_count = 0; + std::string example_type; + }; + std::unordered_map geom_stats; + uint32_t total_shapes = 0; + size_t total_vertices = 0; + size_t total_indices = 0; + QElapsedTimer stream_timer; + stream_timer.start(); + do { if (cancel_requested_.load()) break; @@ -147,6 +164,24 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { info.type = tri_elem->type(); info.parent_id = tri_elem->parent_id(); + // Instancing stats: key by representation id, count unique vs repeated. + const auto& geom = tri_elem->geometry(); + const std::string& geom_id = geom.id(); + size_t nv = geom.verts().size() / 3; + size_t ni = geom.faces().size(); + if (!geom_id.empty()) { + auto& gs = geom_stats[geom_id]; + gs.count++; + if (gs.count == 1) { + gs.vertex_count = nv; + gs.index_count = ni; + gs.example_type = info.type; + } + } + total_shapes++; + total_vertices += nv; + total_indices += ni; + { std::lock_guard lock(elements_mutex_); pending_elements_.push_back(std::move(info)); @@ -168,6 +203,74 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { progress_ = 100; emit progressChanged(100); + + // === Instancing report === + { + size_t unique_geoms = geom_stats.size(); + size_t unique_vertices = 0; + size_t unique_indices = 0; + size_t repeated_shapes = 0; // total shapes that share a repr with another + for (const auto& [gid, gs] : geom_stats) { + unique_vertices += gs.vertex_count; + unique_indices += gs.index_count; + if (gs.count > 1) repeated_shapes += gs.count; + } + + // Bytes assuming current layout (32 B/vertex, 4 B/index). + size_t baked_vbo_bytes = total_vertices * 32; + size_t baked_ebo_bytes = total_indices * 4; + size_t instanced_vbo_bytes = unique_vertices * 32; + size_t instanced_ebo_bytes = unique_indices * 4; + // Per-instance data: 64 B transform + 8 B (object_id + color). + size_t per_instance_bytes = 72; + size_t instance_ssbo_bytes = total_shapes * per_instance_bytes; + + double dedup_ratio = unique_geoms > 0 + ? static_cast(total_shapes) / static_cast(unique_geoms) + : 1.0; + + qDebug("=== Instancing analysis: %s ===", path.c_str()); + qDebug(" Stream time: %.2f s", stream_timer.elapsed() / 1000.0); + qDebug(" Total shapes: %u", total_shapes); + qDebug(" Unique geometries: %zu (dedup ratio %.2fx)", + unique_geoms, dedup_ratio); + qDebug(" Repeated shapes: %zu (%.1f%% of total)", + repeated_shapes, + total_shapes > 0 ? 100.0 * repeated_shapes / total_shapes : 0.0); + qDebug(" Baked geometry: VBO %.1f MB + EBO %.1f MB = %.1f MB", + baked_vbo_bytes / (1024.0*1024.0), + baked_ebo_bytes / (1024.0*1024.0), + (baked_vbo_bytes + baked_ebo_bytes) / (1024.0*1024.0)); + qDebug(" If instanced: VBO %.1f MB + EBO %.1f MB + SSBO %.1f MB = %.1f MB", + instanced_vbo_bytes / (1024.0*1024.0), + instanced_ebo_bytes / (1024.0*1024.0), + instance_ssbo_bytes / (1024.0*1024.0), + (instanced_vbo_bytes + instanced_ebo_bytes + instance_ssbo_bytes) + / (1024.0*1024.0)); + size_t baked_total = baked_vbo_bytes + baked_ebo_bytes; + size_t inst_total = instanced_vbo_bytes + instanced_ebo_bytes + instance_ssbo_bytes; + if (inst_total > 0 && baked_total > inst_total) { + qDebug(" Potential savings: %.1f MB (%.1f%%)", + (baked_total - inst_total) / (1024.0*1024.0), + 100.0 * (baked_total - inst_total) / baked_total); + } else { + qDebug(" Potential savings: none (instance overhead exceeds dedup win)"); + } + + // Top-5 most duplicated representations. + std::vector> sorted(geom_stats.begin(), geom_stats.end()); + std::partial_sort(sorted.begin(), + sorted.begin() + std::min(5, sorted.size()), + sorted.end(), + [](const auto& a, const auto& b) { return a.second.count > b.second.count; }); + qDebug(" Top duplicated representations:"); + for (size_t i = 0; i < std::min(5, sorted.size()); ++i) { + const auto& [gid, gs] = sorted[i]; + qDebug(" [%zu] count=%u verts=%zu type=%s repr_id=%s", + i + 1, gs.count, gs.vertex_count, + gs.example_type.c_str(), gid.c_str()); + } + } } static MaterialInfo materialFromStyle(const ifcopenshell::geometry::taxonomy::style::ptr& style) { diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index ae50f6dc44..c872f799b6 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -592,7 +592,19 @@ void ViewportWindow::processPendingUploads() { model_bvhs_[pu.model_id] = std::move(pu.bvh_set); } - qDebug("Progressive upload complete: model %u", pu.model_id); + size_t total_vbo = 0, total_ebo = 0; + for (const auto& [mid, mg] : models_gpu_) { + total_vbo += mg.vbo_capacity; + total_ebo += mg.ebo_capacity; + } + qDebug("Progressive upload complete: model %u (this: vbo %.1f MB + ebo %.1f MB, " + "%u objects, %u triangles) scene total vram %.1f MB", + pu.model_id, + mgpu.vbo_capacity / (1024.0 * 1024.0), + mgpu.ebo_capacity / (1024.0 * 1024.0), + static_cast(mgpu.draw_info.size()), + mgpu.total_triangles, + (total_vbo + total_ebo) / (1024.0 * 1024.0)); pending_uploads_.pop_front(); } @@ -953,12 +965,17 @@ void ViewportWindow::render() { accumulated_time_ = 0.0f; uint32_t total_obj = 0, total_tri = 0, vis_obj = 0; + size_t total_vram = 0, total_vbo = 0, total_ebo = 0; + size_t num_models = 0, num_hidden = 0; for (const auto& [mid, m] : models_gpu_) { - if (!m.hidden) { - total_obj += static_cast(m.draw_info.size()); - total_tri += m.total_triangles; - } + num_models++; + if (m.hidden) { num_hidden++; continue; } + total_obj += static_cast(m.draw_info.size()); + total_tri += m.total_triangles; + total_vbo += m.vbo_capacity; + total_ebo += m.ebo_capacity; } + total_vram = total_vbo + total_ebo; for (const auto& cmd : frame_draw_cmds_) { vis_obj += static_cast(cmd.counts.size()); } @@ -971,6 +988,20 @@ void ViewportWindow::render() { 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_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", + last_fps_, 1000.0f / last_fps_, + vis_obj, 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, + frame_draw_cmds_.size(), + pending_uploads_.size()); } }