From 370e992ef19c038c03dbde69251826d6b9d5d598 Mon Sep 17 00:00:00 2001 From: Lars Bergmann Date: Mon, 17 Aug 2026 19:57:01 +0200 Subject: [PATCH 1/3] Prelimininary linux fixes --- CMakeLists.txt | 15 ++++++--------- src/main.cpp | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 10c5f8d..c4b7340 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,12 +2,10 @@ cmake_minimum_required(VERSION 3.28) project(CMakeSFMLProject LANGUAGES CXX) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) - set(CMAKE_CXX_STANDARD 20) add_executable(sim src/main.cpp) -# 1. Fetch SFML normally (Without OpenMP polluting it) include(FetchContent) FetchContent_Declare(SFML GIT_REPOSITORY https://github.com/SFML/SFML.git @@ -18,19 +16,18 @@ FetchContent_Declare(SFML ) FetchContent_MakeAvailable(SFML) -# 2. Link SFML to your executable target_compile_features(sim PRIVATE cxx_std_20) target_link_libraries(sim PRIVATE sfml-graphics sfml-window sfml-system) -# 3. Target-Specific OpenMP for Mac (Applies ONLY to 'sim') +# Cross-platform OpenMP (Linux/macOS/Windows where available) +find_package(OpenMP REQUIRED) +target_link_libraries(sim PRIVATE OpenMP::OpenMP_CXX) + +# Optional: keep Apple-specific fallback only if you truly need it if(APPLE) target_include_directories(sim PRIVATE /opt/homebrew/opt/libomp/include) target_link_directories(sim PRIVATE /opt/homebrew/opt/libomp/lib) - target_compile_options(sim PRIVATE -Xpreprocessor -fopenmp) - target_link_options(sim PRIVATE -Xpreprocessor -fopenmp) # Ensure linker knows about threads - target_link_libraries(sim PRIVATE omp) endif() set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT main) - -set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Xpreprocessor -fopenmp") +set(CMAKE_CXX_FLAGS_RELEASE "-O3") diff --git a/src/main.cpp b/src/main.cpp index 9f309bf..1c7d543 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,7 +20,7 @@ int main() { const double dt = 0.001; sf::Clock fpstimer; - const sf::Font font("/Library/Fonts/JetBrainsMonoNL-Regular.ttf"); + const sf::Font font("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"); std::mt19937 gen(42); std::normal_distribution cluster(1.0 * screen_size / 2, 1.0 * screen_size / 10); @@ -147,4 +147,4 @@ int main() { // if (tot_frames == frame_limit) break; } return 0; -} \ No newline at end of file +} From 070a202ddc06f16465ac8602d1e02f1bf9732ad4 Mon Sep 17 00:00:00 2001 From: Lars Bergmann Date: Mon, 17 Aug 2026 20:17:03 +0200 Subject: [PATCH 2/3] Try further improve main --- CMakeLists.txt | 14 ++- src/main.cpp | 322 ++++++++++++++++++++++++++++--------------------- 2 files changed, 196 insertions(+), 140 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c4b7340..4359765 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,15 +19,21 @@ FetchContent_MakeAvailable(SFML) target_compile_features(sim PRIVATE cxx_std_20) target_link_libraries(sim PRIVATE sfml-graphics sfml-window sfml-system) -# Cross-platform OpenMP (Linux/macOS/Windows where available) find_package(OpenMP REQUIRED) target_link_libraries(sim PRIVATE OpenMP::OpenMP_CXX) -# Optional: keep Apple-specific fallback only if you truly need it +# Keep only if needed on Apple if(APPLE) target_include_directories(sim PRIVATE /opt/homebrew/opt/libomp/include) target_link_directories(sim PRIVATE /opt/homebrew/opt/libomp/lib) endif() -set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT main) -set(CMAKE_CXX_FLAGS_RELEASE "-O3") +# Better than overwriting global CMAKE_CXX_FLAGS_RELEASE +target_compile_options(sim PRIVATE + $<$:-O3 -DNDEBUG> +) + +# Optional: enable IPO/LTO in Release +set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) + +set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT sim) diff --git a/src/main.cpp b/src/main.cpp index 1c7d543..77a579d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include @@ -11,140 +13,188 @@ #include "diagnostics.hpp" int main() { - sf::Color p_color = sf::Color::Cyan; - const int screen_size = 1380; - sf::RenderWindow window(sf::VideoMode({screen_size, screen_size}) , "Simulation"); - const int frame_rate = 60; - window.setFramerateLimit(frame_rate); - - const double dt = 0.001; - - sf::Clock fpstimer; - const sf::Font font("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"); - - std::mt19937 gen(42); - std::normal_distribution cluster(1.0 * screen_size / 2, 1.0 * screen_size / 10); - std::uniform_real_distribution uniform(100.0, screen_size * 1.0 - 100.0); - - std::vector sources; - Complex center{1.0 * screen_size / 2, 1.0 * screen_size / 2}; - for (size_t i = 0; i < 40000; i++) { - double x = cluster(gen), y = cluster(gen); - while (x < 100 or x > screen_size - 100) x = cluster(gen); - while (y < 100 or y > screen_size - 100) y = cluster(gen); - sources.emplace_back(x, y, 10.0); - } - for (size_t i = 0; i < 10000; i++) - sources.emplace_back(uniform(gen), uniform(gen), 1.0); - - const double orbital_speed = 200.0; - for (auto &s : sources) { - Complex disp = s.position - center; - double r = std::abs(disp); - if (r > 1.0) { - Complex tangent{-disp.imag(), disp.real()}; - tangent /= r; - s.velocity = tangent * orbital_speed; - } - } - - fmm::FmmTree tree(sources, 60, 10); - // fmm::BhTree tree(sources, 1, 2.5); - tree.buildTree(); - - bool drawBoxes = false; - - int frame_limit = 1800, tot_frames = 0; - float tot_time = 0, max_time = 0, min_time = 100; - float times[frame_limit]; - - double error_l2[frame_limit]; - double error_abs[frame_limit]; - double avg_error_l2 = 0, avg_error_abs = 0, max_error = 0; - - while (window.isOpen()) { - while (const std::optional event = window.pollEvent()) { - if (event->is()) window.close(); - - else if (const auto* keyPressed = event->getIf()) { - if (keyPressed->scancode == sf::Keyboard::Scancode::Escape) window.close(); - } - } - fpstimer.restart(); - window.clear(sf::Color::Black); - - #pragma omp parallel for - for (size_t i = 0; i < sources.size(); i++) { - sources[i].velocity += 0.5 * tree.forces[i] * dt; - sources[i].position += sources[i].velocity * dt; - } - - // forces = fmm::computeExactForces(sources); - tree.buildTree(); - // fmm::ErrorData error = fmm::evaluateSimulationError(sources, tree.forces, 1000); - - if (drawBoxes) { - auto boxes = tree.getBoxGeometries(); - sf::VertexArray box_va(sf::PrimitiveType::Lines, boxes.size() * 8); - sf::Color box_color(66, 191, 245); - - #pragma omp parallel for schedule(static) - for (size_t i = 0; i < (int)boxes.size(); i++) { - int id_b = i * 8; - - float cx = static_cast(boxes[i].first.real()); - float cy = static_cast(boxes[i].first.imag()); - float len = static_cast(boxes[i].second / 2.0); - - sf::Vector2f tl(cx - len, cy - len); - sf::Vector2f tr(cx + len, cy - len); - sf::Vector2f br(cx + len, cy + len); - sf::Vector2f bl(cx - len, cy + len); - - box_va[id_b ] = sf::Vertex{tl, box_color}; - box_va[id_b + 1] = sf::Vertex{tr, box_color}; - - box_va[id_b + 2] = sf::Vertex{tr, box_color}; - box_va[id_b + 3] = sf::Vertex{br, box_color}; - - box_va[id_b + 4] = sf::Vertex{br, box_color}; - box_va[id_b + 5] = sf::Vertex{bl, box_color}; - - box_va[id_b + 6] = sf::Vertex{bl, box_color}; - box_va[id_b + 7] = sf::Vertex{tl, box_color}; - } - window.draw(box_va); - } - - #pragma omp parallel for - for (size_t i = 0; i < sources.size(); i++) - sources[i].velocity += 0.5 * tree.forces[i] * dt; - - sf::VertexArray particle_va(sf::PrimitiveType::Points, sources.size()); - - #pragma omp parallel for schedule(static) - for (int i = 0; i < (int)sources.size(); ++i) { - float cx = static_cast(sources[i].position.real()); - float cy = static_cast(sources[i].position.imag()); - - particle_va[i] = sf::Vertex{sf::Vector2f(cx, cy), p_color}; - } - - window.draw(particle_va); - - float ms = 1.0 * fpstimer.getElapsedTime().asMicroseconds() / 1000; - sf::Text number(font, std::to_string(ms) + "ms", 24); - number.setFillColor(sf::Color::White); - window.draw(number); - window.display(); - - // min_time = std::min(min_time, ms); - // max_time = std::max(max_time, ms); - // tot_time += ms; - // times[tot_frames] = ms; - - tot_frames++; - // if (tot_frames == frame_limit) break; - } - return 0; + sf::Color p_color = sf::Color::Cyan; + const int screen_size = 1380; + sf::RenderWindow window(sf::VideoMode({screen_size, screen_size}), "Simulation"); + const int frame_rate = 60; + window.setFramerateLimit(frame_rate); + + const double dt = 0.001; + + sf::Clock fpstimer; + sf::Clock uiTimer; + + // Cross-platform-ish font fallback chain + sf::Font font; + { + const std::vector fontCandidates = { + "assets/fonts/JetBrainsMonoNL-Regular.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + "/usr/share/fonts/truetype/liberation2/LiberationMono-Regular.ttf", + "/Library/Fonts/JetBrainsMonoNL-Regular.ttf" + }; + + bool loaded = false; + for (const auto& path : fontCandidates) { + if (font.openFromFile(path)) { + loaded = true; + std::cout << "Loaded font: " << path << "\n"; + break; + } + } + + if (!loaded) { + std::cerr << "Warning: no font loaded. FPS text will be hidden.\n"; + } + } + + std::mt19937 gen(42); + std::normal_distribution cluster(1.0 * screen_size / 2, 1.0 * screen_size / 10); + std::uniform_real_distribution uniform(100.0, screen_size * 1.0 - 100.0); + + std::vector sources; + sources.reserve(50000); + + Complex center{1.0 * screen_size / 2, 1.0 * screen_size / 2}; + for (size_t i = 0; i < 40000; i++) { + double x = cluster(gen), y = cluster(gen); + while (x < 100 || x > screen_size - 100) x = cluster(gen); + while (y < 100 || y > screen_size - 100) y = cluster(gen); + sources.emplace_back(x, y, 10.0); + } + for (size_t i = 0; i < 10000; i++) + sources.emplace_back(uniform(gen), uniform(gen), 1.0); + + const double orbital_speed = 200.0; + for (auto& s : sources) { + Complex disp = s.position - center; + double r = std::abs(disp); + if (r > 1.0) { + Complex tangent{-disp.imag(), disp.real()}; + tangent /= r; + s.velocity = tangent * orbital_speed; + } + } + + fmm::FmmTree tree(sources, 60, 10); + // fmm::BhTree tree(sources, 1, 2.5); + tree.buildTree(); + + bool drawBoxes = false; + + int frame_limit = 1800, tot_frames = 0; + float tot_time = 0, max_time = 0, min_time = 100; + float times[1800]{}; + + double error_l2[1800]{}; + double error_abs[1800]{}; + double avg_error_l2 = 0, avg_error_abs = 0, max_error = 0; + + // Reuse render buffers (avoid per-frame allocations) + sf::VertexArray particle_va(sf::PrimitiveType::Points, sources.size()); + for (size_t i = 0; i < sources.size(); ++i) { + particle_va[i].color = p_color; + } + + sf::Text number(font, "0.0ms", 24); + number.setFillColor(sf::Color::White); + bool canDrawText = font.getInfo().family != ""; + + std::string msText = "0.0ms"; + + while (window.isOpen()) { + while (const std::optional event = window.pollEvent()) { + if (event->is()) window.close(); + else if (const auto* keyPressed = event->getIf()) { + if (keyPressed->scancode == sf::Keyboard::Scancode::Escape) window.close(); + } + } + + fpstimer.restart(); + window.clear(sf::Color::Black); + + // Velocity Verlet half-step + #pragma omp parallel for schedule(static) + for (int i = 0; i < static_cast(sources.size()); i++) { + sources[i].velocity += 0.5 * tree.forces[i] * dt; + sources[i].position += sources[i].velocity * dt; + } + + // Recompute forces at new positions + tree.buildTree(); + // fmm::ErrorData error = fmm::evaluateSimulationError(sources, tree.forces, 1000); + + if (drawBoxes) { + auto boxes = tree.getBoxGeometries(); + sf::VertexArray box_va(sf::PrimitiveType::Lines, boxes.size() * 8); + sf::Color box_color(66, 191, 245); + + for (size_t i = 0; i < boxes.size(); i++) { + int id_b = static_cast(i) * 8; + + float cx = static_cast(boxes[i].first.real()); + float cy = static_cast(boxes[i].first.imag()); + float len = static_cast(boxes[i].second / 2.0); + + sf::Vector2f tl(cx - len, cy - len); + sf::Vector2f tr(cx + len, cy - len); + sf::Vector2f br(cx + len, cy + len); + sf::Vector2f bl(cx - len, cy + len); + + box_va[id_b ] = sf::Vertex{tl, box_color}; + box_va[id_b + 1] = sf::Vertex{tr, box_color}; + + box_va[id_b + 2] = sf::Vertex{tr, box_color}; + box_va[id_b + 3] = sf::Vertex{br, box_color}; + + box_va[id_b + 4] = sf::Vertex{br, box_color}; + box_va[id_b + 5] = sf::Vertex{bl, box_color}; + + box_va[id_b + 6] = sf::Vertex{bl, box_color}; + box_va[id_b + 7] = sf::Vertex{tl, box_color}; + } + window.draw(box_va); + } + + // Velocity Verlet second half-step + #pragma omp parallel for schedule(static) + for (int i = 0; i < static_cast(sources.size()); i++) + sources[i].velocity += 0.5 * tree.forces[i] * dt; + + // Update particle vertices (single-thread often smoother for this memory pattern) + for (size_t i = 0; i < sources.size(); ++i) { + float cx = static_cast(sources[i].position.real()); + float cy = static_cast(sources[i].position.imag()); + particle_va[i].position = sf::Vector2f(cx, cy); + } + + window.draw(particle_va); + + float ms = static_cast(fpstimer.getElapsedTime().asMicroseconds()) / 1000.0f; + + // Update displayed timing text at ~10Hz instead of every frame + if (uiTimer.getElapsedTime().asMilliseconds() >= 100) { + msText = std::to_string(ms).substr(0, 5) + "ms"; + if (canDrawText) number.setString(msText); + uiTimer.restart(); + } + + if (canDrawText) { + window.draw(number); + } + + window.display(); + + // min_time = std::min(min_time, ms); + // max_time = std::max(max_time, ms); + // tot_time += ms; + // times[tot_frames] = ms; + + tot_frames++; + if (tot_frames >= frame_limit) { + // break; // keep disabled unless you want fixed-length benchmark runs + } + } + + return 0; } From 913a8cb65737b718d960cac30a71c449745719cd Mon Sep 17 00:00:00 2001 From: Lars Bergmann Date: Mon, 17 Aug 2026 20:23:43 +0200 Subject: [PATCH 3/3] Handle for build tree steps, more debug output --- src/main.cpp | 130 ++++++++++++++++++++++++++++----------------------- 1 file changed, 72 insertions(+), 58 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 77a579d..89dad81 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,9 +2,11 @@ #include #include #include -#include #include #include +#include +#include +#include #include @@ -16,16 +18,19 @@ int main() { sf::Color p_color = sf::Color::Cyan; const int screen_size = 1380; sf::RenderWindow window(sf::VideoMode({screen_size, screen_size}), "Simulation"); - const int frame_rate = 60; - window.setFramerateLimit(frame_rate); + window.setFramerateLimit(60); + // Simulation settings const double dt = 0.001; + const int BUILD_EVERY_N_FRAMES = 4; // 1 = every frame, 2 = every second frame, etc. - sf::Clock fpstimer; + sf::Clock frameTimer; + sf::Clock phaseClock; sf::Clock uiTimer; - // Cross-platform-ish font fallback chain + // Font loading with fallbacks sf::Font font; + bool canDrawText = false; { const std::vector fontCandidates = { "assets/fonts/JetBrainsMonoNL-Regular.ttf", @@ -34,20 +39,20 @@ int main() { "/Library/Fonts/JetBrainsMonoNL-Regular.ttf" }; - bool loaded = false; for (const auto& path : fontCandidates) { if (font.openFromFile(path)) { - loaded = true; std::cout << "Loaded font: " << path << "\n"; + canDrawText = true; break; } } - if (!loaded) { - std::cerr << "Warning: no font loaded. FPS text will be hidden.\n"; + if (!canDrawText) { + std::cerr << "Warning: no font loaded. Overlay text disabled.\n"; } } + // Initial particle setup std::mt19937 gen(42); std::normal_distribution cluster(1.0 * screen_size / 2, 1.0 * screen_size / 10); std::uniform_real_distribution uniform(100.0, screen_size * 1.0 - 100.0); @@ -76,33 +81,33 @@ int main() { } } + // Build initial tree/forces fmm::FmmTree tree(sources, 60, 10); // fmm::BhTree tree(sources, 1, 2.5); tree.buildTree(); bool drawBoxes = false; - int frame_limit = 1800, tot_frames = 0; - float tot_time = 0, max_time = 0, min_time = 100; - float times[1800]{}; - - double error_l2[1800]{}; - double error_abs[1800]{}; - double avg_error_l2 = 0, avg_error_abs = 0, max_error = 0; - - // Reuse render buffers (avoid per-frame allocations) + // Reused draw buffers sf::VertexArray particle_va(sf::PrimitiveType::Points, sources.size()); for (size_t i = 0; i < sources.size(); ++i) { particle_va[i].color = p_color; } - sf::Text number(font, "0.0ms", 24); - number.setFillColor(sf::Color::White); - bool canDrawText = font.getInfo().family != ""; + sf::Text overlay(font, "", 20); + overlay.setFillColor(sf::Color::White); + overlay.setPosition({10.f, 8.f}); - std::string msText = "0.0ms"; + // Timing stats + int tot_frames = 0; + float tIntegrateMs = 0.f, tBuildMs = 0.f, tRenderMs = 0.f, tFrameMs = 0.f; + float emaBuildMs = 0.f; + float maxBuildMs = 0.f; + constexpr float emaAlpha = 0.10f; while (window.isOpen()) { + frameTimer.restart(); + while (const std::optional event = window.pollEvent()) { if (event->is()) window.close(); else if (const auto* keyPressed = event->getIf()) { @@ -110,19 +115,44 @@ int main() { } } - fpstimer.restart(); window.clear(sf::Color::Black); - // Velocity Verlet half-step + // Phase 1: integrate (half-kick + drift) + phaseClock.restart(); #pragma omp parallel for schedule(static) for (int i = 0; i < static_cast(sources.size()); i++) { sources[i].velocity += 0.5 * tree.forces[i] * dt; sources[i].position += sources[i].velocity * dt; } + tIntegrateMs = static_cast(phaseClock.getElapsedTime().asMicroseconds()) / 1000.0f; + + // Phase 2: rebuild tree (optionally decimated) + phaseClock.restart(); + if (BUILD_EVERY_N_FRAMES <= 1 || (tot_frames % BUILD_EVERY_N_FRAMES) == 0) { + tree.buildTree(); + } + tBuildMs = static_cast(phaseClock.getElapsedTime().asMicroseconds()) / 1000.0f; + + // Build-time stats + if (tot_frames == 0) emaBuildMs = tBuildMs; + else emaBuildMs = (1.0f - emaAlpha) * emaBuildMs + emaAlpha * tBuildMs; + maxBuildMs = std::max(maxBuildMs, tBuildMs); + + // Phase 3: second half-kick + draw prep + draw + phaseClock.restart(); + + #pragma omp parallel for schedule(static) + for (int i = 0; i < static_cast(sources.size()); i++) { + sources[i].velocity += 0.5 * tree.forces[i] * dt; + } - // Recompute forces at new positions - tree.buildTree(); - // fmm::ErrorData error = fmm::evaluateSimulationError(sources, tree.forces, 1000); + // Updating vertex buffer is often smoother single-threaded + for (size_t i = 0; i < sources.size(); ++i) { + particle_va[i].position = sf::Vector2f( + static_cast(sources[i].position.real()), + static_cast(sources[i].position.imag()) + ); + } if (drawBoxes) { auto boxes = tree.getBoxGeometries(); @@ -143,57 +173,41 @@ int main() { box_va[id_b ] = sf::Vertex{tl, box_color}; box_va[id_b + 1] = sf::Vertex{tr, box_color}; - box_va[id_b + 2] = sf::Vertex{tr, box_color}; box_va[id_b + 3] = sf::Vertex{br, box_color}; - box_va[id_b + 4] = sf::Vertex{br, box_color}; box_va[id_b + 5] = sf::Vertex{bl, box_color}; - box_va[id_b + 6] = sf::Vertex{bl, box_color}; box_va[id_b + 7] = sf::Vertex{tl, box_color}; } window.draw(box_va); } - // Velocity Verlet second half-step - #pragma omp parallel for schedule(static) - for (int i = 0; i < static_cast(sources.size()); i++) - sources[i].velocity += 0.5 * tree.forces[i] * dt; - - // Update particle vertices (single-thread often smoother for this memory pattern) - for (size_t i = 0; i < sources.size(); ++i) { - float cx = static_cast(sources[i].position.real()); - float cy = static_cast(sources[i].position.imag()); - particle_va[i].position = sf::Vector2f(cx, cy); - } - window.draw(particle_va); - float ms = static_cast(fpstimer.getElapsedTime().asMicroseconds()) / 1000.0f; - - // Update displayed timing text at ~10Hz instead of every frame - if (uiTimer.getElapsedTime().asMilliseconds() >= 100) { - msText = std::to_string(ms).substr(0, 5) + "ms"; - if (canDrawText) number.setString(msText); + tRenderMs = static_cast(phaseClock.getElapsedTime().asMicroseconds()) / 1000.0f; + tFrameMs = static_cast(frameTimer.getElapsedTime().asMicroseconds()) / 1000.0f; + + // Update overlay at ~10 Hz + if (canDrawText && uiTimer.getElapsedTime().asMilliseconds() >= 100) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(2) + << "frame: " << tFrameMs << " ms\n" + << "integrate: " << tIntegrateMs << " ms\n" + << "buildTree: " << tBuildMs << " ms" + << " (ema " << emaBuildMs << ", max " << maxBuildMs << ")\n" + << "render: " << tRenderMs << " ms\n" + << "build every N: " << BUILD_EVERY_N_FRAMES; + overlay.setString(oss.str()); uiTimer.restart(); } if (canDrawText) { - window.draw(number); + window.draw(overlay); } window.display(); - - // min_time = std::min(min_time, ms); - // max_time = std::max(max_time, ms); - // tot_time += ms; - // times[tot_frames] = ms; - tot_frames++; - if (tot_frames >= frame_limit) { - // break; // keep disabled unless you want fixed-length benchmark runs - } } return 0;