feat: implement multithreading for collision resolution and ball updates

This commit is contained in:
Florian Sylvain
2025-05-11 06:35:47 +02:00
parent d1c4f36963
commit c58a2cead2
8 changed files with 182 additions and 29 deletions
+4 -1
View File
@@ -125,6 +125,9 @@ void Ball::handleWallCollision(const sf::Vector2f& windowSize) {
} }
void Ball::resolveCollision(Ball& other) { void Ball::resolveCollision(Ball& other) {
std::lock_guard<std::mutex> lockA(m_mutex);
std::lock_guard<std::mutex> lockB(other.m_mutex);
sf::Vector2f posA = getPosition(); sf::Vector2f posA = getPosition();
sf::Vector2f posB = other.getPosition(); sf::Vector2f posB = other.getPosition();
sf::Vector2f delta = posB - posA; 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 vA_n = vA.x * normal.x + vA.y * normal.y;
float vB_n = vB.x * normal.x + vB.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 vA_n_new = vB_n * restitution;
float vB_n_new = vA_n * restitution; float vB_n_new = vA_n * restitution;
+3
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <SFML/Graphics/CircleShape.hpp> #include <SFML/Graphics/CircleShape.hpp>
#include <mutex>
#include "PhysicalObject.hpp" #include "PhysicalObject.hpp"
@@ -15,6 +16,8 @@ class Ball : public PhysicalObject {
void handleWallCollision(const sf::Vector2f& windowSize); void handleWallCollision(const sf::Vector2f& windowSize);
void updateColor(); void updateColor();
mutable std::mutex m_mutex;
public: public:
Ball(float radius, const sf::Vector2f& pos, const sf::Vector2f& vel, Ball(float radius, const sf::Vector2f& pos, const sf::Vector2f& vel,
const sf::Color& color); const sf::Color& color);
+2 -2
View File
@@ -8,6 +8,6 @@ constexpr float RESTITUTION = 0.8f;
constexpr float FRICTION = 0.9f; constexpr float FRICTION = 0.9f;
constexpr float IMPULSE = 2000.f; constexpr float IMPULSE = 2000.f;
constexpr float REST_PIXEL_VELOCITY = 2.0f; constexpr float REST_PIXEL_VELOCITY = 2.0f;
constexpr float BALL_RADIUS = 15.f; constexpr float BALL_RADIUS = 6.f;
constexpr unsigned BALL_QUANTITY = 500; constexpr unsigned BALL_QUANTITY = 1000;
} // namespace Constants } // namespace Constants
+6 -4
View File
@@ -9,8 +9,9 @@ DebugOverlay::DebugOverlay(const std::string& fontPath)
m_text.setPosition(sf::Vector2f(5.f, 5.f)); m_text.setPosition(sf::Vector2f(5.f, 5.f));
} }
void DebugOverlay::update(int drawCalls, float timescale, void DebugOverlay::update(int drawCalls, float timeScale,
const sf::RenderWindow& window) { sf::RenderWindow& window, size_t threadCount,
size_t ballsPerThread) {
float elapsed = m_fpsClock.restart().asSeconds(); float elapsed = m_fpsClock.restart().asSeconds();
if (elapsed > 0.f) m_fps = static_cast<int>(1.f / elapsed); if (elapsed > 0.f) m_fps = static_cast<int>(1.f / elapsed);
@@ -21,8 +22,9 @@ void DebugOverlay::update(int drawCalls, float timescale,
oss << "Framerate: " << m_fps << " FPS\n"; oss << "Framerate: " << m_fps << " FPS\n";
oss << "Frametime: " << (elapsed * 1000.f) << " ms\n"; oss << "Frametime: " << (elapsed * 1000.f) << " ms\n";
oss << "Mouse: " << mousePos.x << ", " << mousePos.y << "\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()); m_text.setString(oss.str());
} }
+2 -1
View File
@@ -9,7 +9,8 @@ class DebugOverlay {
public: public:
DebugOverlay(const std::string& fontPath); 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); void draw(sf::RenderWindow& window);
private: private:
+70 -13
View File
@@ -3,6 +3,7 @@
#include <SFML/Window/Event.hpp> #include <SFML/Window/Event.hpp>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <thread>
#include <tuple> #include <tuple>
#include <unordered_map> #include <unordered_map>
@@ -17,7 +18,10 @@ Game::Game()
std::bind(&Game::processMousePressed, this, std::placeholders::_1)), std::bind(&Game::processMousePressed, this, std::placeholders::_1)),
m_debugLines(sf::PrimitiveType::Lines), m_debugLines(sf::PrimitiveType::Lines),
m_drawCallCount(0), 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}), m_window.create(sf::VideoMode({Constants::WIDTH, Constants::HEIGHT}),
"SFML Playground"); "SFML Playground");
m_window.setVerticalSyncEnabled(true); m_window.setVerticalSyncEnabled(true);
@@ -78,46 +82,92 @@ Game::Grid Game::buildSpatialGrid() {
return grid; return grid;
} }
void Game::resolveSpatialCollisions(const Grid &grid) { void Game::resolveSpatialCollisionsParallel(const Grid &grid) {
static const Cell forwardNeighbors[] = { static const Cell forwardNeighbors[] = {
{0, 0}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}}; {0, 0}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}};
std::vector<std::pair<Ball *, Ball *>> collisionPairs;
std::mutex collisionsMutex;
for (const auto &[cell, cellBalls] : grid) { for (const auto &[cell, cellBalls] : grid) {
for (const auto &offset : forwardNeighbors) { for (const auto &offset : forwardNeighbors) {
Cell neighborCell = {cell.first + offset.first, Cell neighborCell = {cell.first + offset.first,
cell.second + offset.second}; cell.second + offset.second};
auto neighborIt = grid.find(neighborCell); auto neighborIt = grid.find(neighborCell);
if (neighborIt == grid.end()) continue; if (neighborIt == grid.end()) continue;
if (neighborCell == cell) { if (neighborCell == cell) {
for (size_t i = 0; i < cellBalls.size(); ++i) { for (size_t i = 0; i < cellBalls.size(); ++i) {
for (size_t j = i + 1; j < cellBalls.size(); ++j) { for (size_t j = i + 1; j < cellBalls.size(); ++j) {
cellBalls[i]->resolveCollision(*cellBalls[j]); std::lock_guard<std::mutex> lock(collisionsMutex);
collisionPairs.emplace_back(cellBalls[i], cellBalls[j]);
} }
} }
} else { } else {
for (Ball *ballA : cellBalls) { for (Ball *ballA : cellBalls) {
for (Ball *ballB : neighborIt->second) { for (Ball *ballB : neighborIt->second) {
ballA->resolveCollision(*ballB); std::lock_guard<std::mutex> lock(collisionsMutex);
collisionPairs.emplace_back(ballA, ballB);
} }
} }
} }
} }
} }
const size_t chunkSize = std::max(
size_t(1), collisionPairs.size() / std::thread::hardware_concurrency());
std::vector<std::future<void>> 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() { void Game::update() {
float dt = m_clock.restart().asSeconds() * m_timeScale; float dt = m_clock.restart().asSeconds() * m_timeScale;
if (dt > 0.1f) dt = 0.1f; if (dt > 0.1f) dt = 0.1f;
for (auto &object : m_objects) { updateBallsParallel(dt);
if (auto *ball = dynamic_cast<Ball *>(object.get())) {
ball->update(dt, m_windowSize);
} else {
object->update(dt, m_windowSize);
}
}
auto grid = buildSpatialGrid(); 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<std::future<void>> 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<Ball *>(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() { void Game::render() {
@@ -128,8 +178,12 @@ void Game::render() {
m_batchRenderer.clear(); m_batchRenderer.clear();
std::vector<const Ball *> balls;
balls.reserve(m_objects.size());
for (const auto &object : m_objects) { for (const auto &object : m_objects) {
if (auto *ball = dynamic_cast<Ball *>(object.get())) { if (auto *ball = dynamic_cast<Ball *>(object.get())) {
balls.push_back(ball);
m_batchRenderer.addBall(*ball); m_batchRenderer.addBall(*ball);
} else { } else {
object->draw(m_window); object->draw(m_window);
@@ -148,7 +202,10 @@ void Game::render() {
if (m_toggleDebug) { if (m_toggleDebug) {
DebugDraw::drawBatchedLines(m_window, m_debugLines); 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); m_debugOverlay.draw(m_window);
} }
+12 -8
View File
@@ -15,6 +15,7 @@
#include "DebugOverlay.hpp" #include "DebugOverlay.hpp"
#include "InputManager.hpp" #include "InputManager.hpp"
#include "PhysicalObject.hpp" #include "PhysicalObject.hpp"
#include "ThreadPool.hpp"
class Ball; class Ball;
class PhysicalObject; class PhysicalObject;
@@ -32,12 +33,7 @@ class Game {
DebugOverlay m_debugOverlay; DebugOverlay m_debugOverlay;
bool m_toggleDebug = true; bool m_toggleDebug = true;
BatchRenderer m_batchRenderer; BatchRenderer m_batchRenderer;
ThreadPool m_threadPool;
void processKeyPressed(const sf::Event::KeyPressed& keyPressed);
void processMousePressed(const sf::Event::MouseButtonPressed& mousePressed);
void handleMouseClick(const sf::Vector2i& mousePos);
void update();
void render();
struct CellHash { struct CellHash {
std::size_t operator()(const std::pair<int, int>& k) const { std::size_t operator()(const std::pair<int, int>& k) const {
@@ -45,13 +41,21 @@ class Game {
static_cast<std::size_t>(k.second) * 19349663; static_cast<std::size_t>(k.second) * 19349663;
} }
}; };
using Cell = std::pair<int, int>; using Cell = std::pair<int, int>;
using Grid = std::unordered_map<Cell, std::vector<Ball*>, CellHash>; using Grid = std::unordered_map<Cell, std::vector<Ball*>, 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(); Grid buildSpatialGrid();
void resolveSpatialCollisions(const Grid& grid);
public: public:
Game(); Game();
void run(); void run();
size_t getThreadCount() const { return m_threadPool.getThreadCount(); }
}; };
+83
View File
@@ -0,0 +1,83 @@
#pragma once
#include <condition_variable>
#include <functional>
#include <future>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
class ThreadPool {
public:
explicit ThreadPool(size_t numThreads);
~ThreadPool();
size_t getThreadCount() const { return workers.size(); }
template <class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::invoke_result<F, Args...>::type>;
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> 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<void()> task;
{
std::unique_lock<std::mutex> 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<std::mutex> lock(queueMutex);
stop = true;
}
condition.notify_all();
for (auto& worker : workers) worker.join();
}
template <class F, class... Args>
inline auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::invoke_result<F, Args...>::type> {
using return_type = typename std::invoke_result<F, Args...>::type;
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queueMutex);
if (stop) throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task]() { (*task)(); });
}
condition.notify_one();
return res;
}