Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
$<$<CONFIG:Release>:-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)
340 changes: 202 additions & 138 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
#include <vector>
#include <random>
#include <iostream>
#include <fstream>
#include <optional>
#include <string>
#include <sstream>
#include <iomanip>
#include <algorithm>

#include <omp.h>

Expand All @@ -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<double> cluster(1.0 * screen_size / 2, 1.0 * screen_size / 10);
std::uniform_real_distribution<double> uniform(100.0, screen_size * 1.0 - 100.0);

std::vector<fmm::Source> 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<sf::Event::Closed>()) window.close();

else if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>()) {
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<float>(boxes[i].first.real());
float cy = static_cast<float>(boxes[i].first.imag());
float len = static_cast<float>(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<float>(sources[i].position.real());
float cy = static_cast<float>(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");
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<std::string> 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<double> cluster(1.0 * screen_size / 2, 1.0 * screen_size / 10);
std::uniform_real_distribution<double> uniform(100.0, screen_size * 1.0 - 100.0);

std::vector<fmm::Source> 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<sf::Event::Closed>()) window.close();
else if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>()) {
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<int>(sources.size()); i++) {
sources[i].velocity += 0.5 * tree.forces[i] * dt;
sources[i].position += sources[i].velocity * dt;
}
tIntegrateMs = static_cast<float>(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<float>(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<int>(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<float>(sources[i].position.real()),
static_cast<float>(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<int>(i) * 8;

float cx = static_cast<float>(boxes[i].first.real());
float cy = static_cast<float>(boxes[i].first.imag());
float len = static_cast<float>(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<float>(phaseClock.getElapsedTime().asMicroseconds()) / 1000.0f;
tFrameMs = static_cast<float>(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;
}