wip: integrating threaded decoupled physics

Current Issues:
- HUGE memory leak
- HUGE amount of needles copying
- FillGraph() does thousands of lock_guards instead of one
- Can no longer rely on new states appearing immediately - have to check
each access
- Physics run as fast as possible, no constant sim speed`
- Irregular long freezes
This commit is contained in:
2026-02-24 02:00:15 +01:00
parent 39c0b58f3f
commit 550970f8a1
7 changed files with 140 additions and 87 deletions

View File

@ -7,6 +7,7 @@
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <thread> #include <thread>
#include <tracy/Tracy.hpp>
#include <unordered_map> #include <unordered_map>
#include <variant> #include <variant>
#include <vector> #include <vector>
@ -146,10 +147,10 @@ class ThreadedPhysics {
using Command = std::variant<AddMass, AddSpring, ClearGraph>; using Command = std::variant<AddMass, AddSpring, ClearGraph>;
struct PhysicsState { struct PhysicsState {
std::mutex command_mtx; TracyLockable(std::mutex, command_mtx);
std::queue<Command> pending_commands; std::queue<Command> pending_commands;
std::mutex pos_mtx; TracyLockable(std::mutex, pos_mtx);
std::vector<Mass> masses; // Read by renderer std::vector<Mass> masses; // Read by renderer
std::unordered_map<State, int> state_masses; // Read by renderer std::unordered_map<State, int> state_masses; // Read by renderer
std::vector<Spring> springs; // Read by renderer std::vector<Spring> springs; // Read by renderer
@ -160,9 +161,11 @@ class ThreadedPhysics {
}; };
private: private:
PhysicsState state;
std::thread physics; std::thread physics;
public:
PhysicsState state;
public: public:
ThreadedPhysics() : physics(PhysicsThread, std::ref(state)) {} ThreadedPhysics() : physics(PhysicsThread, std::ref(state)) {}
@ -180,6 +183,11 @@ private:
static auto PhysicsThread(PhysicsState &state) -> void; static auto PhysicsThread(PhysicsState &state) -> void;
public: public:
auto AddMassCmd(const State &_state) -> void;
auto AddSpringCmd(const State &a, const State &b) -> void;
auto ClearCmd() -> void;
}; };
// https://en.cppreference.com/w/cpp/utility/variant/visit // https://en.cppreference.com/w/cpp/utility/variant/visit

View File

@ -61,25 +61,29 @@ public:
} }
private: private:
auto AllocateGraphInstancing(const MassSpringSystem &mass_springs) -> void; auto AllocateGraphInstancing(const std::vector<Mass> &masses) -> void;
auto auto ReallocateGraphInstancingIfNecessary(const std::vector<Mass> &masses)
ReallocateGraphInstancingIfNecessary(const MassSpringSystem &mass_springs)
-> void; -> void;
public: public:
auto UpdateTextureSizes() -> void; auto UpdateTextureSizes() -> void;
auto DrawMassSprings(const MassSpringSystem &mass_springs, auto DrawMassSprings(
const State &current_state, const State &starting_state, const std::vector<Mass> &masses,
const std::unordered_set<State> &winning_states, const std::unordered_map<State, int> &state_masses,
const std::unordered_set<State> &visited_states) -> void; const std::vector<Spring> &springs,
const std::unordered_map<std::pair<State, State>, int> state_springs,
const State &current_state, const State &starting_state,
const std::unordered_set<State> &winning_states,
const std::unordered_set<State> &visited_states) -> void;
auto DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, auto DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x,
int sel_y, int block_add_x, int block_add_y, int sel_y, int block_add_x, int block_add_y,
const WinCondition win_condition) -> void; const WinCondition win_condition) -> void;
auto DrawMenu(const MassSpringSystem &mass_springs, int current_preset, auto DrawMenu(const std::vector<Mass> &masses,
const std::vector<Spring> &springs, int current_preset,
const State &current_state, const State &current_state,
const std::unordered_set<State> &winning_states) -> void; const std::unordered_set<State> &winning_states) -> void;

View File

@ -1,7 +1,6 @@
#ifndef __STATE_HPP_ #ifndef __STATE_HPP_
#define __STATE_HPP_ #define __STATE_HPP_
#include "config.hpp"
#include "physics.hpp" #include "physics.hpp"
#include "presets.hpp" #include "presets.hpp"
#include "puzzle.hpp" #include "puzzle.hpp"
@ -10,7 +9,7 @@
class StateManager { class StateManager {
public: public:
MassSpringSystem &mass_springs; ThreadedPhysics &physics;
int current_preset; int current_preset;
State starting_state; State starting_state;
@ -23,12 +22,12 @@ public:
std::unordered_set<State> visited_states; std::unordered_set<State> visited_states;
public: public:
StateManager(MassSpringSystem &_mass_springs) StateManager(ThreadedPhysics &_physics)
: mass_springs(_mass_springs), current_preset(0), : physics(_physics), current_preset(0),
starting_state(generators[current_preset]()), starting_state(generators[current_preset]()),
current_state(starting_state), previous_state(starting_state), current_state(starting_state), previous_state(starting_state),
edited(false) { edited(false) {
mass_springs.AddMass(MASS, false, current_state); physics.AddMassCmd(current_state);
} }
StateManager(const StateManager &copy) = delete; StateManager(const StateManager &copy) = delete;

View File

@ -1,3 +1,4 @@
#include <mutex>
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <tracy/Tracy.hpp> #include <tracy/Tracy.hpp>
@ -37,20 +38,27 @@ auto main(int argc, char *argv[]) -> int {
// Game setup // Game setup
OrbitCamera3D camera; OrbitCamera3D camera;
Renderer renderer(camera); Renderer renderer(camera);
MassSpringSystem mass_springs; ThreadedPhysics physics;
StateManager state(mass_springs); StateManager state(physics);
InputHandler input(state, renderer); InputHandler input(state, renderer);
std::vector<Mass> masses; // Read from physics
std::unordered_map<State, int> state_masses; // Read from physics
std::vector<Spring> springs; // Read from physics
std::unordered_map<std::pair<State, State>, int>
state_springs; // Read from physics
// Game loop // Game loop
double timestep_accumulator = 0.0; // double timestep_accumulator = 0.0;
while (!WindowShouldClose()) { while (!WindowShouldClose()) {
timestep_accumulator += GetFrameTime(); // timestep_accumulator += GetFrameTime();
// Input update // Input update
state.previous_state = state.current_state; state.previous_state = state.current_state;
input.HandleInput(); input.HandleInput();
state.UpdateGraph(); // Add state added after user input state.UpdateGraph(); // Add state added after user input
/*
// Physics update // Physics update
if (timestep_accumulator > TIMESTEP) { if (timestep_accumulator > TIMESTEP) {
// Do not try to catch up if we're falling behind. Frametimes would get // Do not try to catch up if we're falling behind. Frametimes would get
@ -63,21 +71,43 @@ auto main(int argc, char *argv[]) -> int {
timestep_accumulator -= TIMESTEP; timestep_accumulator -= TIMESTEP;
} }
*/
// Read positions from physics thread
// TODO: We're reading a HUGE amount of SHIT, because the state
// representations (std::string, ~100 Bytes) are copied ~3 times per
// spring: state_masses<State, int> and state_springs<<State, State>,
// int>
// TODO: Don't download each frame if nothing changed, check for update
// first
{
std::lock_guard<LockableBase(std::mutex)> lock(physics.state.pos_mtx);
masses = physics.state.masses;
state_masses = physics.state.state_masses;
springs = physics.state.springs;
state_springs = physics.state.state_springs;
}
// Update the camera after the physics, so target lock is smooth // Update the camera after the physics, so target lock is smooth
camera.Update(mass_springs.GetMass(state.current_state).position); if (state_masses.contains(state.current_state)) {
const Mass &current_mass =
masses.at(state_masses.at(state.current_state));
camera.Update(current_mass.position);
}
// Rendering // Rendering
renderer.UpdateTextureSizes(); renderer.UpdateTextureSizes();
renderer.DrawMassSprings(mass_springs, state.current_state, if (masses.size() > 0 && state_masses.contains(state.current_state)) {
state.starting_state, state.winning_states, renderer.DrawMassSprings(masses, state_masses, springs, state_springs,
state.visited_states); state.current_state, state.starting_state,
state.winning_states, state.visited_states);
}
renderer.DrawKlotski(state.current_state, input.hov_x, input.hov_y, renderer.DrawKlotski(state.current_state, input.hov_x, input.hov_y,
input.sel_x, input.sel_y, input.block_add_x, input.sel_x, input.sel_y, input.block_add_x,
input.block_add_y, state.CurrentWinCondition()); input.block_add_y, state.CurrentWinCondition());
renderer.DrawMenu(mass_springs, state.current_preset, state.current_state, renderer.DrawMenu(masses, springs, state.current_preset,
state.winning_states); state.current_state, state.winning_states);
renderer.DrawTextures(); renderer.DrawTextures();
} }

View File

@ -344,15 +344,15 @@ auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state)
MassSpringSystem mass_springs; MassSpringSystem mass_springs;
const auto visitor = overloads{ const auto visitor = overloads{
[&](const AddMass &am) { mass_springs.AddMass(am.s); }, [&](const struct AddMass &am) { mass_springs.AddMass(am.s); },
[&](const AddSpring &as) { mass_springs.AddSpring(as.a, as.b); }, [&](const struct AddSpring &as) { mass_springs.AddSpring(as.a, as.b); },
[&](const ClearGraph &cg) { mass_springs.Clear(); }, [&](const struct ClearGraph &cg) { mass_springs.Clear(); },
}; };
while (state.running) { while (state.running) {
// Handle queued commands // Handle queued commands
{ {
std::lock_guard<std::mutex> lock(state.command_mtx); std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
while (!state.pending_commands.empty()) { while (!state.pending_commands.empty()) {
Command &cmd = state.pending_commands.front(); Command &cmd = state.pending_commands.front();
cmd.visit(visitor); cmd.visit(visitor);
@ -373,7 +373,7 @@ auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state)
// Publish the positions for the renderer (copy) // Publish the positions for the renderer (copy)
{ {
std::lock_guard<std::mutex> lock(state.pos_mtx); std::lock_guard<LockableBase(std::mutex)> lock(state.pos_mtx);
state.masses = mass_springs.masses; state.masses = mass_springs.masses;
state.state_masses = mass_springs.state_masses; state.state_masses = mass_springs.state_masses;
state.springs = mass_springs.springs; state.springs = mass_springs.springs;
@ -381,3 +381,24 @@ auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state)
} }
} }
} }
auto ThreadedPhysics::AddMassCmd(const State &_state) -> void {
{
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
state.pending_commands.push(AddMass{_state});
}
}
auto ThreadedPhysics::AddSpringCmd(const State &a, const State &b) -> void {
{
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
state.pending_commands.push(AddSpring{a, b});
}
}
auto ThreadedPhysics::ClearCmd() -> void {
{
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
state.pending_commands.push(ClearGraph{});
}
}

View File

@ -33,7 +33,7 @@ auto Renderer::UpdateTextureSizes() -> void {
menu_target = LoadRenderTexture(width * 2, MENU_HEIGHT); menu_target = LoadRenderTexture(width * 2, MENU_HEIGHT);
} }
auto Renderer::AllocateGraphInstancing(const MassSpringSystem &mass_springs) auto Renderer::AllocateGraphInstancing(const std::vector<Mass> &masses)
-> void { -> void {
cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE); cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE);
@ -48,38 +48,40 @@ auto Renderer::AllocateGraphInstancing(const MassSpringSystem &mass_springs)
vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR; vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR;
vertex_mat.shader = instancing_shader; vertex_mat.shader = instancing_shader;
transforms = (Matrix *)MemAlloc(mass_springs.masses.size() * sizeof(Matrix)); transforms = (Matrix *)MemAlloc(masses.size() * sizeof(Matrix));
transforms_size = mass_springs.masses.size(); transforms_size = masses.size();
} }
auto Renderer::ReallocateGraphInstancingIfNecessary( auto Renderer::ReallocateGraphInstancingIfNecessary(
const MassSpringSystem &mass_springs) -> void { const std::vector<Mass> &masses) -> void {
if (transforms_size != mass_springs.masses.size()) { if (transforms_size != masses.size()) {
transforms = (Matrix *)MemRealloc(transforms, mass_springs.masses.size() * transforms =
sizeof(Matrix)); (Matrix *)MemRealloc(transforms, masses.size() * sizeof(Matrix));
transforms_size = mass_springs.masses.size(); transforms_size = masses.size();
} }
} }
auto Renderer::DrawMassSprings(const MassSpringSystem &mass_springs, auto Renderer::DrawMassSprings(
const State &current_state, const std::vector<Mass> &masses,
const State &starting_state, const std::unordered_map<State, int> &state_masses,
const std::unordered_set<State> &winning_states, const std::vector<Spring> &springs,
const std::unordered_set<State> &visited_states) const std::unordered_map<std::pair<State, State>, int> state_springs,
-> void { const State &current_state, const State &starting_state,
const std::unordered_set<State> &winning_states,
const std::unordered_set<State> &visited_states) -> void {
ZoneScoped; ZoneScoped;
// Prepare cube instancing // Prepare cube instancing
{ {
ZoneNamedN(prepare_masses, "PrepareMasses", true); ZoneNamedN(prepare_masses, "PrepareMasses", true);
if (mass_springs.masses.size() < DRAW_VERTICES_LIMIT) { if (masses.size() < DRAW_VERTICES_LIMIT) {
if (transforms == nullptr) { if (transforms == nullptr) {
AllocateGraphInstancing(mass_springs); AllocateGraphInstancing(masses);
} }
ReallocateGraphInstancingIfNecessary(mass_springs); ReallocateGraphInstancingIfNecessary(masses);
int i = 0; int i = 0;
for (const auto &mass : mass_springs.masses) { for (const auto &mass : masses) {
transforms[i] = transforms[i] =
MatrixTranslate(mass.position.x, mass.position.y, mass.position.z); MatrixTranslate(mass.position.x, mass.position.y, mass.position.z);
++i; ++i;
@ -96,10 +98,10 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &mass_springs,
{ {
ZoneNamedN(draw_springs, "DrawSprings", true); ZoneNamedN(draw_springs, "DrawSprings", true);
rlBegin(RL_LINES); rlBegin(RL_LINES);
for (const auto &spring : mass_springs.springs) { for (const auto &spring : springs) {
// We have to do a lookup of the actual mass object, which is slow :( // We have to do a lookup of the actual mass object, which is slow :(
const Mass &a = mass_springs.masses.at(spring.mass_a); const Mass &a = masses.at(spring.mass_a);
const Mass &b = mass_springs.masses.at(spring.mass_b); const Mass &b = masses.at(spring.mass_b);
rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a); rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a);
rlVertex3f(a.position.x, a.position.y, a.position.z); rlVertex3f(a.position.x, a.position.y, a.position.z);
rlVertex3f(b.position.x, b.position.y, b.position.z); rlVertex3f(b.position.x, b.position.y, b.position.z);
@ -110,20 +112,19 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &mass_springs,
// Draw masses (instanced) // Draw masses (instanced)
{ {
ZoneNamedN(draw_masses, "DrawMasses", true); ZoneNamedN(draw_masses, "DrawMasses", true);
if (mass_springs.masses.size() < DRAW_VERTICES_LIMIT) { if (masses.size() < DRAW_VERTICES_LIMIT) {
// NOTE: I don't know if drawing all this inside a shader would make it // NOTE: I don't know if drawing all this inside a shader would make it
// much faster... The amount of data sent to the GPU would be // much faster... The amount of data sent to the GPU would be
// reduced (just positions instead of matrices), but is this // reduced (just positions instead of matrices), but is this
// noticable for < 100000 cubes? // noticable for < 100000 cubes?
DrawMeshInstanced(cube_instance, vertex_mat, transforms, DrawMeshInstanced(cube_instance, vertex_mat, transforms, masses.size());
mass_springs.masses.size());
} }
} }
// Mark winning states // Mark winning states
if (mark_solutions || connect_solutions) { if (mark_solutions || connect_solutions) {
for (const auto &state : winning_states) { for (const auto &state : winning_states) {
const Mass &winning_mass = mass_springs.GetMass(state); const Mass &winning_mass = masses.at(state_masses.at(state));
if (mark_solutions) { if (mark_solutions) {
DrawCube(winning_mass.position, 2 * VERTEX_SIZE, 2 * VERTEX_SIZE, DrawCube(winning_mass.position, 2 * VERTEX_SIZE, 2 * VERTEX_SIZE,
2 * VERTEX_SIZE, BLUE); 2 * VERTEX_SIZE, BLUE);
@ -131,26 +132,26 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &mass_springs,
if (connect_solutions) { if (connect_solutions) {
DrawLine3D(winning_mass.position, DrawLine3D(winning_mass.position,
mass_springs.GetMass(current_state).position, PURPLE); masses.at(state_masses.at(current_state)).position, PURPLE);
} }
} }
} }
// Mark visited states // Mark visited states
for (const auto &state : visited_states) { for (const auto &state : visited_states) {
const Mass &visited_mass = mass_springs.GetMass(state); const Mass &visited_mass = masses.at(state_masses.at(state));
DrawCube(visited_mass.position, VERTEX_SIZE * 1.5, VERTEX_SIZE * 1.5, DrawCube(visited_mass.position, VERTEX_SIZE * 1.5, VERTEX_SIZE * 1.5,
VERTEX_SIZE * 1.5, PURPLE); VERTEX_SIZE * 1.5, PURPLE);
} }
// Mark starting state // Mark starting state
const Mass &starting_mass = mass_springs.GetMass(starting_state); const Mass &starting_mass = masses.at(state_masses.at(starting_state));
DrawCube(starting_mass.position, VERTEX_SIZE * 2, VERTEX_SIZE * 2, DrawCube(starting_mass.position, VERTEX_SIZE * 2, VERTEX_SIZE * 2,
VERTEX_SIZE * 2, ORANGE); VERTEX_SIZE * 2, ORANGE);
// Mark current state // Mark current state
const Mass &current_mass = mass_springs.GetMass(current_state); const Mass &current_mass = masses.at(state_masses.at(current_state));
DrawCube(current_mass.position, VERTEX_SIZE * 2, VERTEX_SIZE * 2, DrawCube(current_mass.position, VERTEX_SIZE * 2, VERTEX_SIZE * 2,
VERTEX_SIZE * 2, RED); VERTEX_SIZE * 2, RED);
@ -250,8 +251,9 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x,
EndTextureMode(); EndTextureMode();
} }
auto Renderer::DrawMenu(const MassSpringSystem &mass_springs, auto Renderer::DrawMenu(const std::vector<Mass> &masses,
int current_preset, const State &current_state, const std::vector<Spring> &springs, int current_preset,
const State &current_state,
const std::unordered_set<State> &winning_states) const std::unordered_set<State> &winning_states)
-> void { -> void {
ZoneScoped; ZoneScoped;
@ -277,8 +279,7 @@ auto Renderer::DrawMenu(const MassSpringSystem &mass_springs,
draw_btn(0, 0, draw_btn(0, 0,
std::format("States: {}, Transitions: {}, Winning: {}", std::format("States: {}, Transitions: {}, Winning: {}",
mass_springs.masses.size(), mass_springs.springs.size(), masses.size(), springs.size(), winning_states.size()),
winning_states.size()),
DARKGREEN); DARKGREEN);
draw_btn( draw_btn(
0, 1, 0, 1,

View File

@ -1,8 +1,8 @@
#include "state.hpp" #include "state.hpp"
#include "config.hpp"
#include "presets.hpp" #include "presets.hpp"
#include "tracy.hpp" #include "tracy.hpp"
#include <mutex>
#include <raymath.h> #include <raymath.h>
auto StateManager::LoadPreset(int preset) -> void { auto StateManager::LoadPreset(int preset) -> void {
@ -38,30 +38,18 @@ auto StateManager::FillGraph() -> void {
std::pair<std::unordered_set<State>, std::vector<std::pair<State, State>>> std::pair<std::unordered_set<State>, std::vector<std::pair<State, State>>>
closure = current_state.Closure(); closure = current_state.Closure();
for (const auto &state : closure.first) { for (const auto &state : closure.first) {
mass_springs.AddMass(MASS, false, state); physics.AddMassCmd(state);
} }
for (const auto &[from, to] : closure.second) { for (const auto &[from, to] : closure.second) {
mass_springs.AddSpring(from, to, SPRING_CONSTANT, DAMPENING_CONSTANT, physics.AddSpringCmd(from, to);
REST_LENGTH);
} }
std::cout << "Inserted " << mass_springs.masses.size() << " masses and "
<< mass_springs.springs.size() << " springs." << std::endl;
FindWinningStates(); FindWinningStates();
std::cout << "Consuming "
<< sizeof(decltype(*mass_springs.masses.begin())) *
mass_springs.masses.size()
<< " Bytes for masses." << std::endl;
std::cout << "Consuming "
<< sizeof(decltype(*mass_springs.springs.begin())) *
mass_springs.springs.size()
<< " Bytes for springs." << std::endl;
} }
auto StateManager::UpdateGraph() -> void { auto StateManager::UpdateGraph() -> void {
if (previous_state != current_state) { if (previous_state != current_state) {
mass_springs.AddMass(MASS, false, current_state); physics.AddMassCmd(current_state);
mass_springs.AddSpring(current_state, previous_state, SPRING_CONSTANT, physics.AddSpringCmd(current_state, previous_state);
DAMPENING_CONSTANT, REST_LENGTH);
if (win_conditions[current_preset](current_state)) { if (win_conditions[current_preset](current_state)) {
winning_states.insert(current_state); winning_states.insert(current_state);
} }
@ -72,8 +60,8 @@ auto StateManager::UpdateGraph() -> void {
auto StateManager::ClearGraph() -> void { auto StateManager::ClearGraph() -> void {
winning_states.clear(); winning_states.clear();
visited_states.clear(); visited_states.clear();
mass_springs.Clear(); physics.ClearCmd();
mass_springs.AddMass(MASS, false, current_state); physics.AddMassCmd(current_state);
// The previous_state is no longer in the graph // The previous_state is no longer in the graph
previous_state = current_state; previous_state = current_state;
@ -84,14 +72,16 @@ auto StateManager::ClearGraph() -> void {
auto StateManager::FindWinningStates() -> void { auto StateManager::FindWinningStates() -> void {
winning_states.clear(); winning_states.clear();
for (const auto &[state, mass] : mass_springs.state_masses) { std::unordered_map<State, int> state_masses;
{
std::lock_guard<LockableBase(std::mutex)> lock(physics.state.pos_mtx);
state_masses = physics.state.state_masses;
}
for (const auto &[state, mass] : state_masses) {
if (win_conditions[current_preset](state)) { if (win_conditions[current_preset](state)) {
winning_states.insert(state); winning_states.insert(state);
} }
} }
std::cout << "Found " << winning_states.size() << " winning states."
<< std::endl;
} }
auto StateManager::CurrentGenerator() -> StateGenerator { auto StateManager::CurrentGenerator() -> StateGenerator {