mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-14 03:14:23 +00:00
ifcviewer: benchmark CLI, settle recull fix, and Phase 3G documentation
Add --camera tx,ty,tz,dist,yaw,pitch and --benchmark N CLI args for reproducible performance measurement. The benchmark orbits the camera (0.5°/frame yaw) for N frames after a 5-frame warmup, prints avg/median/p1/p99 frame times, then exits. Press C during interactive use to print the current camera as a --camera argument. Fix settle recull to fire after ANY camera motion (not just when IFC_MIN_PX_MOTION is set), ensuring HiZ artifacts from motion frames are always cleared when the camera stops. Document Phase 3G (motion-adaptive culling + HiZ during motion) in README with benchmark results from 1.06M-instance scene: - Baseline: 16.3 fps - IFC_MIN_PX_MOTION=10: 26.5 fps (1.6x) - IFC_HIZ_MOTION=1: 46.6 fps (2.9x) - Both combined: 51.0 fps (3.1x) - + GPU_CULL: 52.0 fps (3.2x, negligible gain) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -190,6 +190,7 @@ void MainWindow::connectStreamer(GeometryStreamer* streamer) {
|
||||
void MainWindow::startNextLoad() {
|
||||
if (load_queue_.empty()) {
|
||||
loading_model_id_ = 0;
|
||||
applyPendingBenchmark();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -554,3 +555,34 @@ void MainWindow::populateProperties(uint32_t object_id) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::setPendingCamera(const QString& params) {
|
||||
pending_camera_ = params;
|
||||
}
|
||||
|
||||
void MainWindow::setPendingBenchmark(int frames) {
|
||||
pending_benchmark_ = frames;
|
||||
}
|
||||
|
||||
void MainWindow::applyPendingBenchmark() {
|
||||
if (pending_camera_.isEmpty() && pending_benchmark_ <= 0) return;
|
||||
|
||||
if (!pending_camera_.isEmpty()) {
|
||||
QStringList parts = pending_camera_.split(',');
|
||||
if (parts.size() == 6) {
|
||||
viewport_->setCamera(
|
||||
parts[0].toFloat(), parts[1].toFloat(), parts[2].toFloat(),
|
||||
parts[3].toFloat(), parts[4].toFloat(), parts[5].toFloat());
|
||||
qDebug("Camera set: %s", qPrintable(pending_camera_));
|
||||
} else {
|
||||
qWarning("--camera expects 6 comma-separated values: tx,ty,tz,dist,yaw,pitch");
|
||||
}
|
||||
pending_camera_.clear();
|
||||
}
|
||||
|
||||
if (pending_benchmark_ > 0) {
|
||||
qDebug("Starting benchmark: %d frames", pending_benchmark_);
|
||||
viewport_->setBenchmarkFrames(pending_benchmark_);
|
||||
pending_benchmark_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ public:
|
||||
~MainWindow();
|
||||
|
||||
void addFiles(const QStringList& paths);
|
||||
void setPendingCamera(const QString& params);
|
||||
void setPendingBenchmark(int frames);
|
||||
|
||||
private slots:
|
||||
void onFileOpen();
|
||||
@@ -109,6 +111,11 @@ private:
|
||||
static uint64_t scopedKey(uint32_t model_id, int ifc_id) {
|
||||
return (static_cast<uint64_t>(model_id) << 32) | static_cast<uint32_t>(ifc_id);
|
||||
}
|
||||
|
||||
QString pending_camera_;
|
||||
int pending_benchmark_ = 0;
|
||||
|
||||
void applyPendingBenchmark();
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
||||
+97
-24
@@ -694,31 +694,33 @@ thousands and the frame time drops accordingly.
|
||||
|
||||
##### Known caveats
|
||||
|
||||
- **Disabled while the camera moves.** The pyramid is aligned to the
|
||||
VP matrix of the frame that produced it. On a moving camera the
|
||||
stored VP no longer matches the current one, and reusing it would
|
||||
pop objects in and out as the stale depth falsely claims they're
|
||||
occluded. The cull now compares `hiz_vp_ == current_vp` and drops
|
||||
HiZ rejection entirely when they differ, so HiZ only contributes on
|
||||
still frames. The honest cost: orbiting — the exact motion where
|
||||
the frame rate tends to dip — gets no HiZ help. A proper fix needs
|
||||
a same-frame depth pre-pass (draw cheap depth, build HiZ from *that*
|
||||
frame's VP, then issue the colour pass against it); deferred to the
|
||||
GPU-compute cull rewrite in Phase 3E where we're touching this code
|
||||
anyway. We also tried a 3-deep PBO ring for async readback (2-frame
|
||||
stale) which produced visible flicker on fast orbits — reverted.
|
||||
- **Optional during camera motion (`IFC_HIZ_MOTION=1`).** The pyramid
|
||||
is aligned to the previous frame's VP. On a moving camera the stale
|
||||
depth can falsely occlude objects, particularly thin geometry (pipes,
|
||||
railings) at oblique angles. By default HiZ is disabled during motion
|
||||
(`hiz_vp_ == current_vp` check). Setting `IFC_HIZ_MOTION=1` forces
|
||||
HiZ on during motion — benchmarks show this is the single biggest
|
||||
perf lever (2.9× speedup), and the artifacts are transient and minor
|
||||
during active orbiting. When the camera stops, a settle recull fires
|
||||
with `hiz_vp_valid_ = false`, disabling HiZ for that one frame and
|
||||
re-culling the full scene. This guarantees the stationary view is
|
||||
artifact-free. See Phase 3G for benchmark data.
|
||||
- **Conservative occlusion test.** The original "max over coarse mip"
|
||||
test was too aggressive for BIM scenes where the entire depth range
|
||||
compresses into 0.99–1.00. Replaced with "all fine-mip texels must
|
||||
agree" — sample at mip 1, reject only if every texel has depth less
|
||||
than the AABB's nearest point, early-out on the first non-occluding
|
||||
texel. Queries covering >64 texels skip HiZ entirely. Eliminates
|
||||
most false occlusions at the cost of fewer true rejections.
|
||||
- **Depth blit replaced with shader downsample.** The original
|
||||
`glBlitFramebuffer` for scaling the resolved depth to HiZ size
|
||||
produced `GL_INVALID_VALUE` on some drivers. Replaced with a
|
||||
fullscreen-triangle shader writing `gl_FragDepth`. The resolve
|
||||
texture uses `GL_DEPTH24_STENCIL8` to match Qt's default FBO format
|
||||
(which uses D24S8 even when only depth is requested).
|
||||
- **Readback syncs the GPU.** `glGetTextureImage` is blocking.
|
||||
Measured cost is well under a millisecond at 256×128; not a
|
||||
bottleneck on the machines tested. Phase 3D's compute-shader cull
|
||||
removes it entirely.
|
||||
- **Doesn't move the needle on overview shots.** Those scenes are
|
||||
CPU-bound on the cull traversal itself, not GPU-bound on drawing,
|
||||
so cutting the drawn-triangle count in half is invisible in the
|
||||
frame time. `hiz_rej` still rises modestly on overviews (the frustum
|
||||
hull contains everything behind visible walls) but saved GPU work
|
||||
is masked by CPU cost. HiZ pays off on interior views, where the
|
||||
GPU *was* the bottleneck. If a project never leaves overview,
|
||||
`IFC_NO_HIZ=1` shaves the ~1 ms of HiZ cost.
|
||||
bottleneck on the machines tested.
|
||||
- **Transparent geometry would need special handling**, but the
|
||||
current renderer doesn't have any, so no-op for now.
|
||||
|
||||
@@ -961,6 +963,74 @@ doors, windows, pipe fittings) share geometry across placements.
|
||||
ratio (~80 k vs ~120 k), confirming per-draw overhead as the
|
||||
dominant cost.
|
||||
|
||||
#### 3G. Motion-adaptive culling + HiZ during motion — ✅ done
|
||||
|
||||
The bottleneck during camera orbit is the sheer number of visible
|
||||
objects and sub_draws. Two complementary strategies address this:
|
||||
|
||||
##### Motion-adaptive contribution culling (`IFC_MIN_PX_MOTION`)
|
||||
|
||||
During camera motion, use a larger pixel-radius threshold to hide
|
||||
small objects that contribute little at interactive rates. When the
|
||||
camera stops, a settle recull restores the base threshold and full
|
||||
detail within one frame. No visual artifacts — objects below the
|
||||
motion threshold are genuinely tiny on screen.
|
||||
|
||||
##### HiZ during motion (`IFC_HIZ_MOTION=1`)
|
||||
|
||||
Force the one-frame-stale HiZ pyramid to remain active during camera
|
||||
motion. The stale depth causes minor false occlusions on thin
|
||||
geometry at oblique angles, but these are transient during active
|
||||
orbit. When the camera stops, the settle recull invalidates the HiZ
|
||||
pyramid (`hiz_vp_valid_ = false`) and re-culls without HiZ,
|
||||
guaranteeing the stationary view is artifact-free.
|
||||
|
||||
##### Benchmark results
|
||||
|
||||
Benchmarked on 1.06 M-instance / 111-model scene, 200-frame orbit
|
||||
(103° arc, 0.5°/frame), GTX 1650:
|
||||
|
||||
| Configuration | avg ms | fps | speedup | obj | sub_draws | hiz_rej |
|
||||
|----------------------------------|--------|------|---------|-------|-----------|---------|
|
||||
| Baseline (no opts) | 61.25 | 16.3 | 1.0× | 254k | 155k | 0 |
|
||||
| MIN_PX_MOTION=10 | 37.67 | 26.5 | 1.6× | 70k | 56k | 0 |
|
||||
| HIZ_MOTION=1 | 21.44 | 46.6 | 2.9× | 33k | 17.5k | 28k |
|
||||
| HIZ_MOTION=1 + MIN_PX_MOTION=10 | 19.62 | 51.0 | 3.1× | 11.4k | 8.7k | 11.5k |
|
||||
| GPU_CULL + HIZ + MIN_PX | 19.22 | 52.0 | 3.2× | 11.3k | 8.6k | 59k |
|
||||
|
||||
##### Conclusions
|
||||
|
||||
1. **HiZ during motion is the biggest single lever** — 2.9× alone.
|
||||
Artifacts are minor and transient during orbit; the stationary view
|
||||
is guaranteed correct by the settle recull.
|
||||
|
||||
2. **Motion pixel culling is clean and effective** — 1.6× with zero
|
||||
artifacts.
|
||||
|
||||
3. **Combining both gives diminishing returns** — 3.1× vs 2.9× (HiZ
|
||||
alone) or 1.6× (MIN_PX alone). They compete over the same objects.
|
||||
|
||||
4. **GPU cull adds nothing** on top of these — 52.0 vs 51.0 fps. The
|
||||
CPU BVH path handles the reduced visible set in ~2 ms.
|
||||
|
||||
5. **The ~19 ms floor is GPU rendering**, not culling. At 8.6k
|
||||
sub_draws the bottleneck shifts to draw dispatch + triangle
|
||||
rasterization. Further improvement requires reducing sub_draws
|
||||
(static batching) or moving to a more efficient draw model.
|
||||
|
||||
##### Benchmark CLI
|
||||
|
||||
Press **C** during interactive use to print the current camera as a
|
||||
`--camera` argument. Then benchmark reproducibly:
|
||||
|
||||
```bash
|
||||
./IfcViewer --camera tx,ty,tz,dist,yaw,pitch --benchmark 200 files...
|
||||
```
|
||||
|
||||
The benchmark orbits the camera (0.5°/frame yaw), measures N frames
|
||||
after a 5-frame warmup, prints avg/median/p1/p99 frame times, then
|
||||
exits. Env vars control the test configuration.
|
||||
|
||||
### Planned follow-ups (post-Phase-3)
|
||||
|
||||
- **Mesh shaders / meshlets.** Ceiling-raising, but overkill until the
|
||||
@@ -979,6 +1049,7 @@ Scene size Bottleneck Fix
|
||||
multi-million + occluders redundant rasterisation Phase 3C HiZ (done, CPU readback)
|
||||
many models, serial cull single-thread BVH trv Phase 3D parallel cull (done)
|
||||
single giant model / <18 cores CPU BVH trv Phase 3E GPU cull (hybrid, done)
|
||||
orbit fps on 1M+ scenes too many vis objects Phase 3G motion culling + HiZ (done, 3.1×)
|
||||
90k+ unique visible meshes per-draw GPU overhead Phase 3F static batching (next)
|
||||
```
|
||||
|
||||
@@ -996,7 +1067,7 @@ single giant model / <18 cores CPU BVH trv Phase 3E GPU cull (hybri
|
||||
- [x] Reflection-aware two-pass draw for mirrored placements
|
||||
- [x] Backface culling (user-toggleable, default on)
|
||||
- [x] `reorient-shells` enabled in iterator
|
||||
- [x] Perf diagnostic env vars (`IFC_SKIP_MDI`, `IFC_MAX_SUBDRAWS`, `IFC_MIN_PX`, `IFC_LOD1_PX`, `IFC_NO_HIZ`, `IFC_HIZ_SIZE`, `IFC_CULL_THREADS`)
|
||||
- [x] Perf diagnostic env vars (`IFC_SKIP_MDI`, `IFC_MAX_SUBDRAWS`, `IFC_MIN_PX`, `IFC_LOD1_PX`, `IFC_NO_HIZ`, `IFC_HIZ_SIZE`, `IFC_CULL_THREADS`, `IFC_MIN_PX_MOTION`, `IFC_HIZ_MOTION`, `IFC_GPU_CULL`, `IFC_SUBDRAW_DIAG`)
|
||||
- [x] Phase 3A — screen-space contribution culling
|
||||
- [x] Phase 3B — distance / contribution LOD (meshoptimizer `simplifySloppy`)
|
||||
- [x] Phase 3C — Hierarchical-Z occlusion culling (v1, CPU-side readback)
|
||||
@@ -1004,6 +1075,8 @@ single giant model / <18 cores CPU BVH trv Phase 3E GPU cull (hybri
|
||||
- [x] Quantized VBO (16 B/vert, sidecar v6)
|
||||
- [x] Event-driven rendering (zero idle CPU/GPU, cull skipped on still frames)
|
||||
- [x] Phase 3E — GPU compute-shader culling (hybrid: GPU frustum+contribution, async readback, CPU HiZ+LOD+emit)
|
||||
- [x] Phase 3G — Motion-adaptive culling + HiZ during motion (3.1× orbit speedup on 1M-instance scene)
|
||||
- [x] Benchmark CLI (`--camera`, `--benchmark`, press C to capture camera)
|
||||
- [ ] **Phase 3F — Static batching of single-instance meshes** (next; reduces 90k+ sub_draws to hundreds)
|
||||
- [ ] Vulkan/MoltenVK backend for macOS
|
||||
- [ ] Embedded Python scripting console
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
#include "AppSettings.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QCoreApplication>
|
||||
#include <QtMath>
|
||||
#include <QtOpenGL/QOpenGLVersionFunctionsFactory>
|
||||
|
||||
@@ -1203,6 +1205,44 @@ void ViewportWindow::setSelectedObjectId(uint32_t id) {
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::setCamera(float tx, float ty, float tz,
|
||||
float dist, float yaw, float pitch) {
|
||||
camera_target_ = QVector3D(tx, ty, tz);
|
||||
camera_distance_ = dist;
|
||||
camera_yaw_ = yaw;
|
||||
camera_pitch_ = pitch;
|
||||
have_cached_cull_ = false;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::setBenchmarkFrames(int n) {
|
||||
benchmark_total_ = n;
|
||||
benchmark_count_ = 0;
|
||||
benchmark_warmup_ = 5;
|
||||
benchmark_yaw_start_ = camera_yaw_;
|
||||
benchmark_frame_times_.clear();
|
||||
benchmark_frame_times_.reserve(n);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
QString ViewportWindow::cameraString() const {
|
||||
return QString("%1,%2,%3,%4,%5,%6")
|
||||
.arg(camera_target_.x(), 0, 'f', 4)
|
||||
.arg(camera_target_.y(), 0, 'f', 4)
|
||||
.arg(camera_target_.z(), 0, 'f', 4)
|
||||
.arg(camera_distance_, 0, 'f', 4)
|
||||
.arg(camera_yaw_, 0, 'f', 2)
|
||||
.arg(camera_pitch_, 0, 'f', 2);
|
||||
}
|
||||
|
||||
void ViewportWindow::keyPressEvent(QKeyEvent* event) {
|
||||
if (event->key() == Qt::Key_C && !(event->modifiers() & Qt::ControlModifier)) {
|
||||
qDebug("--camera %s", qPrintable(cameraString()));
|
||||
return;
|
||||
}
|
||||
QWindow::keyPressEvent(event);
|
||||
}
|
||||
|
||||
// --- HiZ occlusion culling (Phase 3C) -----------------------------------
|
||||
|
||||
// Baseline HiZ resolution. 256x128 is enough to cull big occluders
|
||||
@@ -1983,24 +2023,23 @@ void ViewportWindow::render() {
|
||||
&& last_cull_view_ == view_matrix_
|
||||
&& last_cull_proj_ == proj_matrix_;
|
||||
const bool camera_moving = !camera_unchanged;
|
||||
// Force a re-cull on the first still frame after motion so we
|
||||
// restore the base (tighter) contribution threshold.
|
||||
const bool needs_settle_recull = !camera_moving
|
||||
&& last_cull_was_motion_
|
||||
const bool use_motion_threshold = camera_moving
|
||||
&& motion_min_pixel_radius > base_min_pixel_radius;
|
||||
// Force a re-cull on the first still frame after motion so we
|
||||
// restore the base contribution threshold and clear stale HiZ.
|
||||
const bool needs_settle_recull = !camera_moving
|
||||
&& last_cull_was_motion_;
|
||||
const bool cull_this_frame = camera_moving || needs_settle_recull;
|
||||
// Invalidate HiZ on the settle frame: the pyramid was built from the
|
||||
// motion frame's sparse depth (aggressive threshold hid objects whose
|
||||
// depth would normally populate the pyramid), causing false occlusion.
|
||||
if (needs_settle_recull)
|
||||
hiz_vp_valid_ = false;
|
||||
const bool use_motion_threshold = camera_moving
|
||||
&& motion_min_pixel_radius > base_min_pixel_radius;
|
||||
const float min_pixel_radius = use_motion_threshold
|
||||
? motion_min_pixel_radius : base_min_pixel_radius;
|
||||
if (cull_this_frame) {
|
||||
hiz_reject_count_.store(0, std::memory_order_relaxed);
|
||||
last_cull_was_motion_ = use_motion_threshold;
|
||||
last_cull_was_motion_ = camera_moving;
|
||||
} else {
|
||||
++cull_skipped_frames_;
|
||||
}
|
||||
@@ -2353,6 +2392,42 @@ void ViewportWindow::render() {
|
||||
// Reported fps = "if I rendered continuously, this is the rate I'd hit",
|
||||
// which is what profiling actually wants.
|
||||
const float frame_cost_s = frame_cost_clock.nsecsElapsed() * 1e-9f;
|
||||
|
||||
if (benchmark_total_ > 0) {
|
||||
camera_yaw_ += benchmark_yaw_speed_;
|
||||
have_cached_cull_ = false;
|
||||
|
||||
if (benchmark_warmup_ > 0) {
|
||||
--benchmark_warmup_;
|
||||
} else {
|
||||
benchmark_frame_times_.push_back(frame_cost_s * 1000.0f);
|
||||
++benchmark_count_;
|
||||
}
|
||||
if (benchmark_count_ >= benchmark_total_) {
|
||||
std::sort(benchmark_frame_times_.begin(), benchmark_frame_times_.end());
|
||||
float sum = 0.0f;
|
||||
for (float t : benchmark_frame_times_) sum += t;
|
||||
float avg = sum / benchmark_frame_times_.size();
|
||||
float median = benchmark_frame_times_[benchmark_frame_times_.size() / 2];
|
||||
float p1 = benchmark_frame_times_[(size_t)(benchmark_frame_times_.size() * 0.01f)];
|
||||
float p99 = benchmark_frame_times_[(size_t)(benchmark_frame_times_.size() * 0.99f)];
|
||||
float total_arc = benchmark_yaw_speed_ * (benchmark_total_ + 5);
|
||||
qDebug("\n=== BENCHMARK (%d frames, orbit %.0f° at %.1f°/frame) ===",
|
||||
benchmark_total_, total_arc, benchmark_yaw_speed_);
|
||||
qDebug(" avg: %.2f ms (%.1f fps)", avg, 1000.0f / avg);
|
||||
qDebug(" median: %.2f ms (%.1f fps)", median, 1000.0f / median);
|
||||
qDebug(" p1: %.2f ms p99: %.2f ms", p1, p99);
|
||||
qDebug(" last frame: obj %u tri %u sub_draws %u hiz_rej %u",
|
||||
visible_objects_, visible_triangles_,
|
||||
indirect_sub_draws_,
|
||||
hiz_reject_count_.load());
|
||||
qDebug("=== END BENCHMARK ===\n");
|
||||
QCoreApplication::quit();
|
||||
return;
|
||||
}
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
accumulated_time_ += frame_cost_s;
|
||||
frame_count_++;
|
||||
if (accumulated_time_ >= 1.0f) {
|
||||
|
||||
@@ -174,6 +174,10 @@ public:
|
||||
void setSelectedObjectId(uint32_t id);
|
||||
uint32_t pickObjectAt(int x, int y);
|
||||
|
||||
void setCamera(float tx, float ty, float tz, float dist, float yaw, float pitch);
|
||||
void setBenchmarkFrames(int n);
|
||||
QString cameraString() const;
|
||||
|
||||
struct FrameStats {
|
||||
float fps;
|
||||
float frame_time_ms;
|
||||
@@ -194,6 +198,7 @@ signals:
|
||||
protected:
|
||||
void exposeEvent(QExposeEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
bool event(QEvent* event) override;
|
||||
|
||||
private:
|
||||
@@ -381,6 +386,14 @@ private:
|
||||
// When the camera stops, re-cull once at the base threshold.
|
||||
bool last_cull_was_motion_ = false;
|
||||
|
||||
// Benchmark mode: render N frames, collect stats, then exit.
|
||||
int benchmark_total_ = 0;
|
||||
int benchmark_count_ = 0;
|
||||
int benchmark_warmup_ = 5;
|
||||
float benchmark_yaw_start_ = 0.0f;
|
||||
float benchmark_yaw_speed_ = 0.5f; // degrees per frame
|
||||
std::vector<float> benchmark_frame_times_;
|
||||
|
||||
// Per-frame stats
|
||||
uint32_t visible_triangles_ = 0;
|
||||
uint32_t visible_objects_ = 0;
|
||||
|
||||
@@ -41,6 +41,10 @@ int main(int argc, char* argv[]) {
|
||||
parser.setApplicationDescription("IfcOpenShell IFC Viewer");
|
||||
parser.addHelpOption();
|
||||
parser.addPositionalArgument("files", "IFC file(s) to open", "[files...]");
|
||||
parser.addOption({{"c", "camera"},
|
||||
"Set camera: tx,ty,tz,dist,yaw,pitch", "params"});
|
||||
parser.addOption({{"b", "benchmark"},
|
||||
"Run N frames then print stats and exit", "frames"});
|
||||
parser.process(app);
|
||||
|
||||
MainWindow window;
|
||||
@@ -51,5 +55,12 @@ int main(int argc, char* argv[]) {
|
||||
window.addFiles(args);
|
||||
}
|
||||
|
||||
if (parser.isSet("camera")) {
|
||||
window.setPendingCamera(parser.value("camera"));
|
||||
}
|
||||
if (parser.isSet("benchmark")) {
|
||||
window.setPendingBenchmark(parser.value("benchmark").toInt());
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user