diff --git a/src/Ball.cpp b/src/Ball.cpp index c49fd53..67ca554 100644 --- a/src/Ball.cpp +++ b/src/Ball.cpp @@ -125,6 +125,9 @@ void Ball::handleWallCollision(const sf::Vector2f& windowSize) { } void Ball::resolveCollision(Ball& other) { + std::lock_guard lockA(m_mutex); + std::lock_guard lockB(other.m_mutex); + sf::Vector2f posA = getPosition(); sf::Vector2f posB = other.getPosition(); sf::Vector2f delta = posB - posA; @@ -143,7 +146,7 @@ void Ball::resolveCollision(Ball& other) { float vA_n = vA.x * normal.x + vA.y * normal.y; float vB_n = vB.x * normal.x + vB.y * normal.y; - float restitution = Constants::RESTITUTION; + float restitution = 0.95f; float vA_n_new = vB_n * restitution; float vB_n_new = vA_n * restitution; diff --git a/src/Ball.hpp b/src/Ball.hpp index c0857e6..634b83e 100644 --- a/src/Ball.hpp +++ b/src/Ball.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include "PhysicalObject.hpp" @@ -15,6 +16,8 @@ class Ball : public PhysicalObject { void handleWallCollision(const sf::Vector2f& windowSize); void updateColor(); + mutable std::mutex m_mutex; + public: Ball(float radius, const sf::Vector2f& pos, const sf::Vector2f& vel, const sf::Color& color); diff --git a/src/Constants.hpp b/src/Constants.hpp index 2af559f..b8b82a9 100644 --- a/src/Constants.hpp +++ b/src/Constants.hpp @@ -8,6 +8,6 @@ constexpr float RESTITUTION = 0.8f; constexpr float FRICTION = 0.9f; constexpr float IMPULSE = 2000.f; constexpr float REST_PIXEL_VELOCITY = 2.0f; -constexpr float BALL_RADIUS = 15.f; -constexpr unsigned BALL_QUANTITY = 500; +constexpr float BALL_RADIUS = 6.f; +constexpr unsigned BALL_QUANTITY = 1000; } // namespace Constants diff --git a/src/DebugOverlay.cpp b/src/DebugOverlay.cpp index f51c00f..0f63d54 100644 --- a/src/DebugOverlay.cpp +++ b/src/DebugOverlay.cpp @@ -9,8 +9,9 @@ DebugOverlay::DebugOverlay(const std::string& fontPath) m_text.setPosition(sf::Vector2f(5.f, 5.f)); } -void DebugOverlay::update(int drawCalls, float timescale, - const sf::RenderWindow& window) { +void DebugOverlay::update(int drawCalls, float timeScale, + sf::RenderWindow& window, size_t threadCount, + size_t ballsPerThread) { float elapsed = m_fpsClock.restart().asSeconds(); if (elapsed > 0.f) m_fps = static_cast(1.f / elapsed); @@ -21,8 +22,9 @@ void DebugOverlay::update(int drawCalls, float timescale, oss << "Framerate: " << m_fps << " FPS\n"; oss << "Frametime: " << (elapsed * 1000.f) << " ms\n"; oss << "Mouse: " << mousePos.x << ", " << mousePos.y << "\n"; - oss << "Time scale: " << timescale << "\n"; - + oss << "Time scale: " << timeScale << "\n"; + oss << "\nThreads: " << threadCount; + oss << "\nBalls per thread: " << ballsPerThread; m_text.setString(oss.str()); } diff --git a/src/DebugOverlay.hpp b/src/DebugOverlay.hpp index 302bd3a..ea5f24d 100644 --- a/src/DebugOverlay.hpp +++ b/src/DebugOverlay.hpp @@ -9,7 +9,8 @@ class DebugOverlay { public: DebugOverlay(const std::string& fontPath); - void update(int drawCalls, float timescale, const sf::RenderWindow& window); + void update(int drawCalls, float timeScale, sf::RenderWindow& window, + size_t threadCount, size_t ballsPerThread); void draw(sf::RenderWindow& window); private: diff --git a/src/Game.cpp b/src/Game.cpp index a73bed8..e9afde1 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -17,7 +18,10 @@ Game::Game() std::bind(&Game::processMousePressed, this, std::placeholders::_1)), m_debugLines(sf::PrimitiveType::Lines), m_drawCallCount(0), - m_debugOverlay("assets/consolas.ttf") { + m_debugOverlay("assets/consolas.ttf"), + m_threadPool(std::max(1u, std::thread::hardware_concurrency() > 2 + ? std::thread::hardware_concurrency() - 2 + : 1u)) { m_window.create(sf::VideoMode({Constants::WIDTH, Constants::HEIGHT}), "SFML Playground"); m_window.setVerticalSyncEnabled(true); @@ -78,46 +82,92 @@ Game::Grid Game::buildSpatialGrid() { return grid; } -void Game::resolveSpatialCollisions(const Grid &grid) { +void Game::resolveSpatialCollisionsParallel(const Grid &grid) { static const Cell forwardNeighbors[] = { {0, 0}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}}; + + std::vector> collisionPairs; + std::mutex collisionsMutex; + for (const auto &[cell, cellBalls] : grid) { for (const auto &offset : forwardNeighbors) { Cell neighborCell = {cell.first + offset.first, cell.second + offset.second}; auto neighborIt = grid.find(neighborCell); if (neighborIt == grid.end()) continue; + if (neighborCell == cell) { for (size_t i = 0; i < cellBalls.size(); ++i) { for (size_t j = i + 1; j < cellBalls.size(); ++j) { - cellBalls[i]->resolveCollision(*cellBalls[j]); + std::lock_guard lock(collisionsMutex); + collisionPairs.emplace_back(cellBalls[i], cellBalls[j]); } } } else { for (Ball *ballA : cellBalls) { for (Ball *ballB : neighborIt->second) { - ballA->resolveCollision(*ballB); + std::lock_guard lock(collisionsMutex); + collisionPairs.emplace_back(ballA, ballB); } } } } } + + const size_t chunkSize = std::max( + size_t(1), collisionPairs.size() / std::thread::hardware_concurrency()); + std::vector> futures; + + for (size_t i = 0; i < collisionPairs.size(); i += chunkSize) { + size_t end = std::min(i + chunkSize, collisionPairs.size()); + + futures.push_back(m_threadPool.enqueue([&collisionPairs, i, end]() { + for (size_t j = i; j < end; ++j) { + auto &[ballA, ballB] = collisionPairs[j]; + ballA->resolveCollision(*ballB); + } + })); + } + + for (auto &future : futures) { + future.get(); + } } void Game::update() { float dt = m_clock.restart().asSeconds() * m_timeScale; if (dt > 0.1f) dt = 0.1f; - for (auto &object : m_objects) { - if (auto *ball = dynamic_cast(object.get())) { - ball->update(dt, m_windowSize); - } else { - object->update(dt, m_windowSize); - } - } + updateBallsParallel(dt); auto grid = buildSpatialGrid(); - resolveSpatialCollisions(grid); + + resolveSpatialCollisionsParallel(grid); +} + +void Game::updateBallsParallel(float dt) { + const size_t chunkSize = std::max( + size_t(1), m_objects.size() / std::thread::hardware_concurrency()); + + std::vector> futures; + + for (size_t i = 0; i < m_objects.size(); i += chunkSize) { + size_t end = std::min(i + chunkSize, m_objects.size()); + + futures.push_back(m_threadPool.enqueue([&, i, end, dt]() { + for (size_t j = i; j < end; ++j) { + if (auto *ball = dynamic_cast(m_objects[j].get())) { + ball->update(dt, m_windowSize); + } else { + m_objects[j]->update(dt, m_windowSize); + } + } + })); + } + + for (auto &future : futures) { + future.get(); + } } void Game::render() { @@ -128,8 +178,12 @@ void Game::render() { m_batchRenderer.clear(); + std::vector balls; + balls.reserve(m_objects.size()); + for (const auto &object : m_objects) { if (auto *ball = dynamic_cast(object.get())) { + balls.push_back(ball); m_batchRenderer.addBall(*ball); } else { object->draw(m_window); @@ -148,7 +202,10 @@ void Game::render() { if (m_toggleDebug) { DebugDraw::drawBatchedLines(m_window, m_debugLines); - m_debugOverlay.update(m_drawCallCount + 2, m_timeScale, m_window); + size_t threadCount = m_threadPool.getThreadCount(); + size_t ballsPerThread = m_objects.size() / threadCount; + m_debugOverlay.update(m_drawCallCount + 2, m_timeScale, m_window, + threadCount, ballsPerThread); m_debugOverlay.draw(m_window); } diff --git a/src/Game.hpp b/src/Game.hpp index f65dd7e..8bbf05e 100644 --- a/src/Game.hpp +++ b/src/Game.hpp @@ -15,6 +15,7 @@ #include "DebugOverlay.hpp" #include "InputManager.hpp" #include "PhysicalObject.hpp" +#include "ThreadPool.hpp" class Ball; class PhysicalObject; @@ -32,12 +33,7 @@ class Game { DebugOverlay m_debugOverlay; bool m_toggleDebug = true; BatchRenderer m_batchRenderer; - - void processKeyPressed(const sf::Event::KeyPressed& keyPressed); - void processMousePressed(const sf::Event::MouseButtonPressed& mousePressed); - void handleMouseClick(const sf::Vector2i& mousePos); - void update(); - void render(); + ThreadPool m_threadPool; struct CellHash { std::size_t operator()(const std::pair& k) const { @@ -45,13 +41,21 @@ class Game { static_cast(k.second) * 19349663; } }; - using Cell = std::pair; using Grid = std::unordered_map, CellHash>; + + void processKeyPressed(const sf::Event::KeyPressed& keyPressed); + void processMousePressed(const sf::Event::MouseButtonPressed& mousePressed); + void handleMouseClick(const sf::Vector2i& mousePos); + void update(); + void render(); + void updateBallsParallel(float dt); + void resolveSpatialCollisionsParallel(const Grid& grid); + Grid buildSpatialGrid(); - void resolveSpatialCollisions(const Grid& grid); public: Game(); void run(); + size_t getThreadCount() const { return m_threadPool.getThreadCount(); } }; diff --git a/src/ThreadPool.hpp b/src/ThreadPool.hpp new file mode 100644 index 0000000..d1db76c --- /dev/null +++ b/src/ThreadPool.hpp @@ -0,0 +1,83 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +class ThreadPool { + public: + explicit ThreadPool(size_t numThreads); + ~ThreadPool(); + size_t getThreadCount() const { return workers.size(); } + + template + auto enqueue(F&& f, Args&&... args) + -> std::future::type>; + + private: + std::vector workers; + + std::queue> tasks; + + std::mutex queueMutex; + std::condition_variable condition; + bool stop; +}; + +inline ThreadPool::ThreadPool(size_t numThreads) : stop(false) { + for (size_t i = 0; i < numThreads; ++i) { + workers.emplace_back([this] { + while (true) { + std::function task; + + { + std::unique_lock lock(this->queueMutex); + this->condition.wait(lock, [this] { + return this->stop || !this->tasks.empty(); + }); + + if (this->stop && this->tasks.empty()) return; + + task = std::move(this->tasks.front()); + this->tasks.pop(); + } + + task(); + } + }); + } +} + +inline ThreadPool::~ThreadPool() { + { + std::unique_lock lock(queueMutex); + stop = true; + } + condition.notify_all(); + + for (auto& worker : workers) worker.join(); +} + +template +inline auto ThreadPool::enqueue(F&& f, Args&&... args) + -> std::future::type> { + using return_type = typename std::invoke_result::type; + + auto task = std::make_shared>( + std::bind(std::forward(f), std::forward(args)...)); + + std::future res = task->get_future(); + + { + std::unique_lock lock(queueMutex); + if (stop) throw std::runtime_error("enqueue on stopped ThreadPool"); + + tasks.emplace([task]() { (*task)(); }); + } + + condition.notify_one(); + return res; +} \ No newline at end of file