From af954fc72e991e3935946aea1cbc2c82c31aa7eb Mon Sep 17 00:00:00 2001 From: Seb James Date: Wed, 7 Jan 2026 13:53:51 +0000 Subject: [PATCH 1/4] Simple frames-per-second profiling utility class --- mplot/CMakeLists.txt | 3 ++ mplot/fps/CMakeLists.txt | 1 + mplot/fps/profiler.h | 112 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 mplot/fps/CMakeLists.txt create mode 100644 mplot/fps/profiler.h diff --git a/mplot/CMakeLists.txt b/mplot/CMakeLists.txt index 830f9412..701f78b7 100644 --- a/mplot/CMakeLists.txt +++ b/mplot/CMakeLists.txt @@ -125,6 +125,9 @@ add_subdirectory(wx) # Compound ray glue code add_subdirectory(compoundray) +# FPS profiling +add_subdirectory(fps) + # Install the EXPORT so that mathplot has its own .cmake file and find_package(mathplot) should work install(FILES mathplot-config.cmake DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/cmake/mathplot) #install(EXPORT mathplot DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/cmake/mathplot) diff --git a/mplot/fps/CMakeLists.txt b/mplot/fps/CMakeLists.txt new file mode 100644 index 00000000..694e8732 --- /dev/null +++ b/mplot/fps/CMakeLists.txt @@ -0,0 +1 @@ +install(FILES profiler.h DESTINATION ${CMAKE_INSTALL_PREFIX}/include/mplot/fps) diff --git a/mplot/fps/profiler.h b/mplot/fps/profiler.h new file mode 100644 index 00000000..d53ae8cc --- /dev/null +++ b/mplot/fps/profiler.h @@ -0,0 +1,112 @@ +/* + * A profiler that computes FPS and manages an std::string that can be used on a graphical info + * screen. + * + * Author: Seb James + * Date: 2024-2025 + */ +#pragma once + +#include +#include +#include +#include +#include + +namespace mplot::fps +{ + using namespace std::chrono; + using sc = std::chrono::steady_clock; + + struct profiler + { + sc::time_point t0 = sc::now(); + sc::time_point t1 = sc::now(); + + std::deque fps; + double fps_mean = 0.0; // a running-mean of fps + unsigned int fps_mean_over_n_samples_last = 0; + + // Current FPS text + std::string fps_txt; + + // Call at the start of the loop that you're timing + void at_begin (int csampl) + { + sc::duration t_d = this->t1 - this->t0; + unsigned int fps_mean_over_n_samples = best_n_samples (csampl); + if (fps_mean_over_n_samples != fps_mean_over_n_samples_last) { + // Reset counters + this->fps.clear(); + this->fps_mean = 0.0; + this->fps_mean_over_n_samples_last = fps_mean_over_n_samples; + } + double fps_mean_period = 1.0 / fps_mean_over_n_samples; + double fps_now = 0.0; + double usecs = static_cast(duration_cast(t_d).count()); + if (usecs > 0.0) { fps_now = 1000000.0 / usecs; } + this->fps.push_back (fps_now * fps_mean_period); + this->fps_mean += this->fps.back(); + if (this->fps.size() > fps_mean_over_n_samples) { + this->fps_mean -= this->fps.front(); + this->fps.pop_front(); + } + std::stringstream ss; + // Stream into ss ready for display + ss << csampl << " samples " << static_cast(std::round(this->fps_mean)) << " FPS"; + this->fps_txt = ss.str(); + + this->t0 = sc::now(); + } + + // Call at the end of the loop that you're timing + void at_end () { this->t1 = sc::now(); } + + // For a given samples per omm, return a sensible number of loops over which to + // average fps, so that fps takes around 1 sec to stabilize. + static constexpr unsigned int best_n_samples (int samples_per_omm) + { + unsigned int best_n = 0; + switch (samples_per_omm) { + case 1: + case 2: + { + best_n = 1024; // about a seconds worth + break; + } + case 4: + case 8: + case 16: + case 32: + case 64: + { + best_n = 512; + break; + } + case 128: + case 256: + { + best_n = 256; + break; + } + case 512: + { + best_n = 128; + break; + } + case 1024: + case 2048: + { + best_n = 64; + break; + } + default: + { + best_n = 32; + } + } + return best_n; + } + }; + +} // namespace From db5c8c44521d46e46172f79dd493c854be3822ad Mon Sep 17 00:00:00 2001 From: Seb James Date: Thu, 8 Jan 2026 14:29:18 +0000 Subject: [PATCH 2/4] Faster to just poll() --- examples/fps.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/fps.cpp b/examples/fps.cpp index 689878f8..d8a934a4 100644 --- a/examples/fps.cpp +++ b/examples/fps.cpp @@ -74,7 +74,7 @@ int main() all_dur += sc::now() - t00; t00 = sc::now(); - v.waitevents (0.00001); + v.poll(); if (k > 8.0f) { k = 1.0f; } t0 = sc::now(); @@ -109,6 +109,7 @@ int main() update_dur = sc::duration{0}; all_dur = sc::duration{0}; fcount = 0; + v.waitevents (0.00001); } v.render(); From dbb675b807ddd5aa4ebe282b035b19e7c42d389e Mon Sep 17 00:00:00 2001 From: Seb James Date: Thu, 8 Jan 2026 14:30:05 +0000 Subject: [PATCH 3/4] Removes some extraneous and app specific code --- mplot/fps/profiler.h | 51 ++------------------------------------------ 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/mplot/fps/profiler.h b/mplot/fps/profiler.h index d53ae8cc..f0ca6e43 100644 --- a/mplot/fps/profiler.h +++ b/mplot/fps/profiler.h @@ -31,10 +31,9 @@ namespace mplot::fps std::string fps_txt; // Call at the start of the loop that you're timing - void at_begin (int csampl) + void at_begin (unsigned int fps_mean_over_n_samples) { sc::duration t_d = this->t1 - this->t0; - unsigned int fps_mean_over_n_samples = best_n_samples (csampl); if (fps_mean_over_n_samples != fps_mean_over_n_samples_last) { // Reset counters this->fps.clear(); @@ -53,7 +52,7 @@ namespace mplot::fps } std::stringstream ss; // Stream into ss ready for display - ss << csampl << " samples " << static_cast(std::round(this->fps_mean)) << " FPS"; + ss << static_cast(std::round(this->fps_mean)) << " FPS"; this->fps_txt = ss.str(); this->t0 = sc::now(); @@ -61,52 +60,6 @@ namespace mplot::fps // Call at the end of the loop that you're timing void at_end () { this->t1 = sc::now(); } - - // For a given samples per omm, return a sensible number of loops over which to - // average fps, so that fps takes around 1 sec to stabilize. - static constexpr unsigned int best_n_samples (int samples_per_omm) - { - unsigned int best_n = 0; - switch (samples_per_omm) { - case 1: - case 2: - { - best_n = 1024; // about a seconds worth - break; - } - case 4: - case 8: - case 16: - case 32: - case 64: - { - best_n = 512; - break; - } - case 128: - case 256: - { - best_n = 256; - break; - } - case 512: - { - best_n = 128; - break; - } - case 1024: - case 2048: - { - best_n = 64; - break; - } - default: - { - best_n = 32; - } - } - return best_n; - } }; } // namespace From 0d2f8e00c60b91d60b7647be841bcf64379f6ebe Mon Sep 17 00:00:00 2001 From: Seb James Date: Thu, 8 Jan 2026 14:30:21 +0000 Subject: [PATCH 4/4] Adds an example use of mplot::fps::profiler --- examples/CMakeLists.txt | 7 +++ examples/fps_with_profiler.cpp | 95 ++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 examples/fps_with_profiler.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4e2d9cbe..2e147b34 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -65,6 +65,13 @@ if(ARMADILLO_FOUND) target_link_libraries(fps OpenGL::GL glfw Freetype::Freetype) endif() + add_executable(fps_with_profiler fps_with_profiler.cpp) + if(OpenMP_CXX_FOUND) + target_link_libraries(fps_with_profiler OpenMP::OpenMP_CXX OpenGL::GL glfw Freetype::Freetype) + else() + target_link_libraries(fps_with_profiler OpenGL::GL glfw Freetype::Freetype) + endif() + add_executable(cartgrid cartgrid.cpp) target_link_libraries(cartgrid OpenGL::GL glfw Freetype::Freetype) diff --git a/examples/fps_with_profiler.cpp b/examples/fps_with_profiler.cpp new file mode 100644 index 00000000..bb01b14d --- /dev/null +++ b/examples/fps_with_profiler.cpp @@ -0,0 +1,95 @@ +/* + * An example mplot::Visual scene, containing a HexGrid. + * + * Using mplot::fps::profiler for the FPS profile + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include + +int main() +{ + mplot::Visual v(1600, 1000, "mplot::Visual"); + v.fov = 15.0f; + v.zFar = 200.0f; + v.lightingEffects(); + mplot::VisualTextModel<>* fps_tm; + v.addLabel ("0 FPS", {0.13f, -0.23f, 0.0f}, fps_tm); // With fps_tm can update the VisualTextModel with fps_tm->setupText("new text") + + // Create a hexgrid to show in the scene + constexpr float hex_to_hex = 0.02f; + + sm::hexgrid hg(hex_to_hex, 15.0f, 0.0f); + hg.setEllipticalBoundary (4.0f, 4.0f); + std::cout << "Number of hexes in grid:" << hg.num() << std::endl; + std::stringstream sss; + sss << "Surface evaluated at " << hg.num() << " coordinates"; + v.addLabel (sss.str(), {0.0f, 0.0f, 0.0f}); + + // Make some dummy data (a radially symmetric Bessel fn) to make an interesting surface + std::vector data(hg.num(), 0.0f); + std::vector r(hg.num(), 0.0f); + float k = 1.0f; + for (unsigned int hi = 0; hi < hg.num(); ++hi) { + // x/y: hg.d_x[hi] hg.d_y[hi] + r[hi] = std::sqrt (hg.d_x[hi] * hg.d_x[hi] + hg.d_y[hi] * hg.d_y[hi]); + data[hi] = std::sin (k * r[hi]) / k * r[hi]; + } + + // Add a HexGridVisual to display the HexGrid within the mplot::Visual scene + sm::vec offset = { 0.0f, -0.05f, 0.0f }; + auto hgv = std::make_unique>(&hg, offset); + v.bindmodel (hgv); + hgv->setScalarData (&data); + hgv->hexVisMode = mplot::HexVisMode::Triangles; + hgv->finalize(); + auto hgvp = v.addVisualModel (hgv); + + unsigned int fcount = 0u; + + // Our profiler object + mplot::fps::profiler fps_profiler; + + while (v.readyToFinish() == false) { + + v.poll(); + + fps_profiler.at_begin (1000); + + if (k > 8.0f) { k = 1.0f; } + +#pragma omp parallel for shared(r,k,data) + for (unsigned int hi = 0; hi < hg.num(); ++hi) { + data[hi] = std::sin (k * r[hi]) / k * r[hi]; + } + if (v.validVisualModel (hgvp) != nullptr) { // Test hgvp is still valid + hgvp->updateData (&data); + } + k += 0.02f; + + if (fcount == 500) { // Update FPS text + fps_tm->setupText (fps_profiler.fps_txt); + fcount = 0; + v.waitevents (0.00001); + } + + v.render(); + fcount++; + + fps_profiler.at_end(); + } + + return 0; +}