diff --git a/CMakeLists.txt b/CMakeLists.txt index 10c5f8d..4359765 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,24 @@ 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') +find_package(OpenMP REQUIRED) +target_link_libraries(sim PRIVATE OpenMP::OpenMP_CXX) + +# 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) - 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) +# 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(CMAKE_CXX_FLAGS_RELEASE "-O3 -Xpreprocessor -fopenmp") +set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT sim) diff --git a/src/main.cpp b/src/main.cpp index 9f309bf..89dad81 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,7 +2,11 @@ #include #include #include -#include +#include +#include +#include +#include +#include #include @@ -11,140 +15,200 @@ #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("/Library/Fonts/JetBrainsMonoNL-Regular.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; -} \ No newline at end of file + sf::Color p_color = sf::Color::Cyan; + const int screen_size = 1380; + sf::RenderWindow window(sf::VideoMode({screen_size, screen_size}), "Simulation"); + 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 frameTimer; + sf::Clock phaseClock; + sf::Clock uiTimer; + + // Font loading with fallbacks + sf::Font font; + bool canDrawText = false; + { + 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" + }; + + for (const auto& path : fontCandidates) { + if (font.openFromFile(path)) { + std::cout << "Loaded font: " << path << "\n"; + canDrawText = true; + break; + } + } + + 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); + + 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; + } + } + + // Build initial tree/forces + fmm::FmmTree tree(sources, 60, 10); + // fmm::BhTree tree(sources, 1, 2.5); + tree.buildTree(); + + bool drawBoxes = false; + + // 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 overlay(font, "", 20); + overlay.setFillColor(sf::Color::White); + overlay.setPosition({10.f, 8.f}); + + // 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()) { + if (keyPressed->scancode == sf::Keyboard::Scancode::Escape) window.close(); + } + } + + window.clear(sf::Color::Black); + + // 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; + } + + // 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(); + 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); + } + + window.draw(particle_va); + + 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(overlay); + } + + window.display(); + tot_frames++; + } + + return 0; +}