From 39c0b58f3fc793ef72ca57c6e28b65bce76087a2 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Tue, 24 Feb 2026 01:02:53 +0100 Subject: [PATCH] implement threaded physics (decoupled from rendering thread) - not yet integrated --- CMakeLists.txt | 2 +- include/physics.hpp | 64 ++++++++++++++++++++++++++++++++++++++++++--- src/main.cpp | 10 +++---- src/physics.cpp | 51 ++++++++++++++++++++++++++++++++---- 4 files changed, 113 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 294e404..b239add 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25) project(MassSprings) -set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD 26) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) find_package(raylib REQUIRED) diff --git a/include/physics.hpp b/include/physics.hpp index c5f4149..2193507 100644 --- a/include/physics.hpp +++ b/include/physics.hpp @@ -1,9 +1,14 @@ #ifndef __PHYSICS_HPP_ #define __PHYSICS_HPP_ +#include +#include +#include #include #include +#include #include +#include #include #include "config.hpp" @@ -105,14 +110,13 @@ private: #endif public: - auto AddMass(float mass, bool fixed, const State &state) -> void; + auto AddMass(const State &state) -> void; auto GetMass(const State &state) -> Mass &; auto GetMass(const State &state) const -> const Mass &; - auto AddSpring(const State &massA, const State &massB, float spring_constant, - float dampening_constant, float rest_length) -> void; + auto AddSpring(const State &massA, const State &massB) -> void; auto Clear() -> void; @@ -129,4 +133,58 @@ public: #endif }; +class ThreadedPhysics { + struct AddMass { + State s; + }; + struct AddSpring { + State a; + State b; + }; + struct ClearGraph {}; + + using Command = std::variant; + + struct PhysicsState { + std::mutex command_mtx; + std::queue pending_commands; + + std::mutex pos_mtx; + std::vector masses; // Read by renderer + std::unordered_map state_masses; // Read by renderer + std::vector springs; // Read by renderer + std::unordered_map, int> + state_springs; // Read by renderer + + std::atomic running{true}; + }; + +private: + PhysicsState state; + std::thread physics; + +public: + ThreadedPhysics() : physics(PhysicsThread, std::ref(state)) {} + + ThreadedPhysics(const ThreadedPhysics ©) = delete; + ThreadedPhysics &operator=(const ThreadedPhysics ©) = delete; + ThreadedPhysics(ThreadedPhysics &&move) = delete; + ThreadedPhysics &operator=(ThreadedPhysics &&move) = delete; + + ~ThreadedPhysics() { + state.running = false; + physics.join(); + } + +private: + static auto PhysicsThread(PhysicsState &state) -> void; + +public: +}; + +// https://en.cppreference.com/w/cpp/utility/variant/visit +template struct overloads : Ts... { + using Ts::operator()...; +}; + #endif diff --git a/src/main.cpp b/src/main.cpp index 9c4d200..ae311af 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,7 @@ +#include +#include +#include + #include "config.hpp" #include "input.hpp" #include "physics.hpp" @@ -5,10 +9,6 @@ #include "state.hpp" #include "tracy.hpp" -#include -#include -#include - // TODO: Klotski state file loading // - File should contain a single state per line, multiple lines possible // - If a file is loaded, the presets should be replaced with the states @@ -18,7 +18,7 @@ // TODO: Graph interaction // - Click states to display them in the board // - Find shortest path to any winning state and mark it in the graph -// - Also mark the next move along the path on the board +// - Also mark the next move along the path on the board auto main(int argc, char *argv[]) -> int { // if (argc < 2) { diff --git a/src/physics.cpp b/src/physics.cpp index 90f3a7b..bb2235f 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -69,8 +70,7 @@ auto Spring::CalculateSpringForce(Mass &_mass_a, Mass &_mass_b) const -> void { _mass_b.force = Vector3Add(_mass_b.force, force_b); } -auto MassSpringSystem::AddMass(float mass, bool fixed, const State &state) - -> void { +auto MassSpringSystem::AddMass(const State &state) -> void { if (!state_masses.contains(state)) { masses.emplace_back(Vector3Zero()); std::size_t idx = masses.size() - 1; @@ -86,9 +86,7 @@ auto MassSpringSystem::GetMass(const State &state) const -> const Mass & { return masses.at(state_masses.at(state)); } -auto MassSpringSystem::AddSpring(const State &state_a, const State &state_b, - float spring_constant, - float dampening_constant, float rest_length) +auto MassSpringSystem::AddSpring(const State &state_a, const State &state_b) -> void { std::pair key = std::make_pair(state_a, state_b); if (!state_springs.contains(key)) { @@ -340,3 +338,46 @@ auto MassSpringSystem::InvalidateGrid() -> void { last_springs_count = 0; } #endif + +auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state) + -> void { + MassSpringSystem mass_springs; + + const auto visitor = overloads{ + [&](const AddMass &am) { mass_springs.AddMass(am.s); }, + [&](const AddSpring &as) { mass_springs.AddSpring(as.a, as.b); }, + [&](const ClearGraph &cg) { mass_springs.Clear(); }, + }; + + while (state.running) { + // Handle queued commands + { + std::lock_guard lock(state.command_mtx); + while (!state.pending_commands.empty()) { + Command &cmd = state.pending_commands.front(); + cmd.visit(visitor); + state.pending_commands.pop(); + } + } + + if (mass_springs.masses.empty()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + + // Physics update + mass_springs.ClearForces(); + mass_springs.CalculateSpringForces(); + mass_springs.CalculateRepulsionForces(); + mass_springs.VerletUpdate(TIMESTEP * SIM_SPEED); + + // Publish the positions for the renderer (copy) + { + std::lock_guard lock(state.pos_mtx); + state.masses = mass_springs.masses; + state.state_masses = mass_springs.state_masses; + state.springs = mass_springs.springs; + state.state_springs = mass_springs.state_springs; + } + } +}