From 819196b3ce248ded4b39537508d0dd8a614b8618 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 27 May 2026 14:14:40 +1000 Subject: [PATCH] wgpu backend: --benchmark N parity with the GL minimal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 11 of the wgpu port. WgpuViewportWindow gains setBenchmarkFrames(N); the minimal driver wires it to a --benchmark N flag. Renders N frames after a 5-frame warmup, yaw-sweeping the camera at 0.5°/frame, captures per-frame wall time with QElapsedTimer (cull + encode + present), and prints avg/median/p1/p99 + last-frame stats in the same line format as IfcViewerMinimal so a script can diff them line for line. Per-frame stats (visible_objects, visible_triangles, sub_draws) are now summed in render() from m.mesh_draws. hiz_rej reports 0 until stage 7 adds HiZ occlusion. Verified on basic.ifc (3 instances): wgpu 11.68 ms avg vs GL 11.75 ms avg — same scene, same camera sweep, same window size. Noise-level delta as expected on a tiny scene; the interesting comparison is on real BIM corpora once you bake them to v13 sidecars. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-wgpu-minimal/main.cpp | 5 ++ src/ifcviewer-wgpu/WgpuViewportWindow.cpp | 79 +++++++++++++++++++++++ src/ifcviewer-wgpu/WgpuViewportWindow.h | 23 +++++++ 3 files changed, 107 insertions(+) diff --git a/src/ifcviewer-wgpu-minimal/main.cpp b/src/ifcviewer-wgpu-minimal/main.cpp index 46988dd4a9..2f4a4f2d6b 100644 --- a/src/ifcviewer-wgpu-minimal/main.cpp +++ b/src/ifcviewer-wgpu-minimal/main.cpp @@ -43,6 +43,8 @@ int main(int argc, char* argv[]) { "[files...]"); parser.addOption({{"s", "screenshot"}, "Render one frame, save to PATH as PNG, exit.", "path"}); + parser.addOption({{"b", "benchmark"}, + "Render N frames (yaw-sweeping the camera), print stats, exit.", "frames"}); parser.process(app); auto* viewport = new WgpuViewportWindow; @@ -67,6 +69,9 @@ int main(int argc, char* argv[]) { viewport->captureNextFrameToPng(parser.value("screenshot"), /*quit_after=*/true); } + if (parser.isSet("benchmark")) { + viewport->setBenchmarkFrames(parser.value("benchmark").toInt()); + } return app.exec(); } diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index 8f003a217a..9ef57afa45 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -765,6 +766,15 @@ static bool aabbInFrustum(const float mn[3], const float mx[3], return true; } +void WgpuViewportWindow::setBenchmarkFrames(int frames) { + bench_total_ = std::max(0, frames); + bench_count_ = 0; + bench_yaw_start_ = camera_yaw_deg_; + bench_frame_ms_.clear(); + bench_frame_ms_.reserve(size_t(bench_total_)); + if (isExposed() && bench_total_ > 0) requestUpdate(); +} + void WgpuViewportWindow::cullModelCpu(WgpuModelGpuData& m, const float planes[6][4]) { if (m.instances.empty() || m.meshes.empty() || !m.visible_buffer) { for (auto& d : m.mesh_draws) d.instance_count = 0; @@ -813,6 +823,11 @@ void WgpuViewportWindow::cullModelCpu(WgpuModelGpuData& m, const float planes[6] } void WgpuViewportWindow::render() { + // Time the whole render() body (cull + encode + present) for the + // benchmark stats. Started before any wgpu work so cull is included. + QElapsedTimer frame_timer; + if (bench_total_ > 0) frame_timer.start(); + WGPUSurfaceTexture surf_tex = {}; wgpuSurfaceGetCurrentTexture(surface_, &surf_tex); @@ -844,6 +859,9 @@ void WgpuViewportWindow::render() { // cull writes its results directly into each model's visible_buffer via // wgpuQueueWriteBuffer — these writes are sequenced before the draw // commands we encode next. + last_visible_objects_ = 0; + last_visible_triangles_ = 0; + last_sub_draws_ = 0; { const QVector3D target(camera_target_[0], camera_target_[1], camera_target_[2]); const QVector3D eye = orbitEye(camera_target_, camera_distance_, @@ -859,6 +877,12 @@ void WgpuViewportWindow::render() { for (auto& [mid, m] : models_gpu_) { if (m.hidden) continue; cullModelCpu(m, planes); + for (const auto& d : m.mesh_draws) { + if (d.instance_count == 0 || d.index_count == 0) continue; + last_visible_objects_ += d.instance_count; + last_visible_triangles_ += (d.index_count / 3u) * d.instance_count; + last_sub_draws_ += 1; + } } } @@ -1036,6 +1060,61 @@ void WgpuViewportWindow::render() { wgpuSurfacePresent(surface_); wgpuTextureRelease(surf_tex.texture); + + // ---- Benchmark integration + auto-quit ------------------------------- + if (bench_total_ > 0) { + const float ms = float(frame_timer.nsecsElapsed()) / 1e6f; + + // Warm-up frames are dropped from the sample. The yaw advance starts + // immediately so the warmup frames already exercise different views. + if (bench_count_ >= bench_warmup_) { + bench_frame_ms_.push_back(ms); + } + camera_yaw_deg_ = bench_yaw_start_ + + bench_yaw_speed_ * float(bench_count_ + 1); + ++bench_count_; + + if (bench_count_ >= bench_warmup_ + bench_total_) { + // Final frame — assemble stats and emit. Format mirrors the GL + // minimal so output is line-diffable across backends. + std::vector times = bench_frame_ms_; + std::sort(times.begin(), times.end()); + auto pct = [×](double p) -> float { + if (times.empty()) return 0.0f; + const size_t idx = std::min(times.size() - 1, + size_t(p * double(times.size() - 1))); + return times[idx]; + }; + float sum = 0.0f; + for (float f : times) sum += f; + const float avg = times.empty() ? 0.0f : sum / float(times.size()); + const float median = pct(0.5); + const float p1 = pct(0.01); + const float p99 = pct(0.99); + + const float total_sweep = bench_yaw_speed_ * float(bench_total_); + qInfo().noquote().nospace() + << "\n=== BENCHMARK (" << bench_total_ << " frames, orbit " + << total_sweep << "° at " << bench_yaw_speed_ << "°/frame) ==="; + qInfo().noquote().nospace() + << " avg: " << avg << " ms (" << (avg > 0 ? 1000.0f/avg : 0.0f) << " fps)"; + qInfo().noquote().nospace() + << " median: " << median << " ms (" << (median > 0 ? 1000.0f/median : 0.0f) << " fps)"; + qInfo().noquote().nospace() + << " p1: " << p1 << " ms p99: " << p99 << " ms"; + qInfo().noquote().nospace() + << " last frame: obj " << last_visible_objects_ + << " tri " << last_visible_triangles_ + << " sub_draws " << last_sub_draws_ + << " hiz_rej 0"; // HiZ lands in stage 7 + qInfo().noquote() << "=== END BENCHMARK ===\n"; + + bench_total_ = 0; + QCoreApplication::quit(); + } else { + requestUpdate(); + } + } } // ----------------------------------------------------------------------------- diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.h b/src/ifcviewer-wgpu/WgpuViewportWindow.h index 488b9d64ff..a01734f59a 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.h +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.h @@ -83,6 +83,12 @@ public: // parity testing against the GL backend. void captureNextFrameToPng(const QString& path, bool quit_after = true); + // Benchmark mode: render N timed frames (after a small warmup), yaw- + // sweeping the camera at 0.5°/frame, then print a stats block on + // stderr and QCoreApplication::quit(). Mirrors the GL minimal's + // --benchmark output format so a script can diff them line for line. + void setBenchmarkFrames(int frames); + protected: void exposeEvent(QExposeEvent* event) override; void resizeEvent(QResizeEvent* event) override; @@ -176,6 +182,23 @@ private: // safe to dedicate to orbit for now. Qt::MouseButton nav_active_button_ = Qt::NoButton; QPoint nav_last_pos_; + + // Benchmark mode. setBenchmarkFrames(N) arms it; render() integrates the + // yaw, captures per-frame ms after warmup, and prints + quits when the + // target frame count is hit. + int bench_total_ = 0; + int bench_count_ = 0; + int bench_warmup_ = 5; + float bench_yaw_start_ = 0.0f; + float bench_yaw_speed_ = 0.5f; // degrees per frame + std::vector bench_frame_ms_; + + // Per-frame stat snapshot from the last cull. Sum of m.mesh_draws across + // visible models. Exposed via the benchmark summary; will grow into a + // proper FrameStats signal when stage 11's host integration arrives. + uint32_t last_visible_objects_ = 0; + uint32_t last_visible_triangles_ = 0; + uint32_t last_sub_draws_ = 0; }; #endif // WGPUVIEWPORTWINDOW_H