wip: implement a smaller data model to reduce copying from physics to main thread

This commit is contained in:
2026-02-24 17:05:24 +01:00
parent 1347abad34
commit c4222c783c
13 changed files with 312 additions and 470 deletions

View File

@ -3,7 +3,6 @@
#include <raylib.h> #include <raylib.h>
#define BARNES_HUT // Use octree BH instead of uniform grid
// #define WEB // Disables multithreading // #define WEB // Disables multithreading
// Window // Window
@ -30,7 +29,7 @@ constexpr float ROT_SPEED = 1.0;
constexpr float CAMERA_SMOOTH_SPEED = 15.0; constexpr float CAMERA_SMOOTH_SPEED = 15.0;
// Physics Engine // Physics Engine
constexpr float SIM_SPEED = 0.2; // How large each update should be constexpr float SIM_SPEED = 1.0; // How large each update should be
constexpr float TIMESTEP = 1.0 / 90; // Do 90 physics updates per second constexpr float TIMESTEP = 1.0 / 90; // Do 90 physics updates per second
constexpr float MASS = 1.0; // Mass spring system constexpr float MASS = 1.0; // Mass spring system
constexpr float SPRING_CONSTANT = 5.0; // Mass spring system constexpr float SPRING_CONSTANT = 5.0; // Mass spring system

View File

@ -1,13 +1,11 @@
#ifndef __INPUT_HPP_ #ifndef __INPUT_HPP_
#define __INPUT_HPP_ #define __INPUT_HPP_
#include "renderer.hpp"
#include "state.hpp" #include "state.hpp"
class InputHandler { class InputHandler {
public: public:
StateManager &state; StateManager &state;
Renderer &renderer;
int hov_x; int hov_x;
int hov_y; int hov_y;
@ -18,10 +16,14 @@ public:
int block_add_x; int block_add_x;
int block_add_y; int block_add_y;
bool mark_solutions;
bool connect_solutions;
public: public:
InputHandler(StateManager &_state, Renderer &_renderer) InputHandler(StateManager &_state)
: state(_state), renderer(_renderer), hov_x(-1), hov_y(-1), sel_x(0), : state(_state), hov_x(-1), hov_y(-1), sel_x(0), sel_y(0),
sel_y(0), has_block_add_xy(false), block_add_x(-1), block_add_y(-1) {} has_block_add_xy(false), block_add_x(-1), block_add_y(-1),
mark_solutions(false), connect_solutions(false) {}
InputHandler(const InputHandler &copy) = delete; InputHandler(const InputHandler &copy) = delete;
InputHandler &operator=(const InputHandler &copy) = delete; InputHandler &operator=(const InputHandler &copy) = delete;

View File

@ -3,22 +3,17 @@
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
#include <cstddef>
#include <mutex> #include <mutex>
#include <queue> #include <queue>
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <thread> #include <thread>
#include <tracy/Tracy.hpp> #include <tracy/Tracy.hpp>
#include <unordered_map>
#include <variant> #include <variant>
#include <vector> #include <vector>
#include "config.hpp"
#include "puzzle.hpp"
#ifdef BARNES_HUT
#include "octree.hpp" #include "octree.hpp"
#endif
#ifndef WEB #ifndef WEB
#include <BS_thread_pool.hpp> #include <BS_thread_pool.hpp>
@ -48,29 +43,19 @@ public:
class Spring { class Spring {
public: public:
int mass_a; std::size_t a;
int mass_b; std::size_t b;
public: public:
Spring(int _mass_a, int _mass_b) : mass_a(_mass_a), mass_b(_mass_b) {} Spring(std::size_t _a, std::size_t _b) : a(_a), b(_b) {}
public: public:
auto CalculateSpringForce(Mass &_mass_a, Mass &_mass_b) const -> void; auto CalculateSpringForce(Mass &_a, Mass &_b) const -> void;
}; };
class MassSpringSystem { class MassSpringSystem {
private: private:
#ifdef BARNES_HUT
// Barnes-Hut
Octree octree; Octree octree;
#else
// Uniform grid
std::vector<int> mass_indices;
std::vector<int64_t> cell_ids;
int last_build;
int last_masses_count;
int last_springs_count;
#endif
#ifndef WEB #ifndef WEB
BS::thread_pool<BS::tp::none> threads; BS::thread_pool<BS::tp::none> threads;
@ -79,19 +64,12 @@ private:
public: public:
// This is the main ownership of all the states/masses/springs. // This is the main ownership of all the states/masses/springs.
std::vector<Mass> masses; std::vector<Mass> masses;
std::unordered_map<State, int> state_masses;
std::vector<Spring> springs; std::vector<Spring> springs;
std::unordered_map<std::pair<State, State>, int> state_springs;
public: public:
MassSpringSystem() : threads(std::thread::hardware_concurrency() - 1) { MassSpringSystem() : threads(std::thread::hardware_concurrency() - 1) {
#ifndef BARNES_HUT
last_build = REPULSION_GRID_REFRESH;
std::cout << "Using uniform grid repulsion force calculation." << std::endl;
#else
std::cout << "Using Barnes-Hut + octree repulsion force calculation." std::cout << "Using Barnes-Hut + octree repulsion force calculation."
<< std::endl; << std::endl;
#endif
#ifndef WEB #ifndef WEB
std::cout << "Thread-Pool: " << threads.get_thread_count() << " threads." std::cout << "Thread-Pool: " << threads.get_thread_count() << " threads."
@ -105,20 +83,12 @@ public:
MassSpringSystem &operator=(MassSpringSystem &&move) = delete; MassSpringSystem &operator=(MassSpringSystem &&move) = delete;
private: private:
#ifdef BARNES_HUT
auto BuildOctree() -> void; auto BuildOctree() -> void;
#else
auto BuildUniformGrid() -> void;
#endif
public: public:
auto AddMass(const State &state) -> void; auto AddMass() -> void;
auto GetMass(const State &state) -> Mass &; auto AddSpring(int a, int b) -> void;
auto GetMass(const State &state) const -> const Mass &;
auto AddSpring(const State &massA, const State &massB) -> void;
auto Clear() -> void; auto Clear() -> void;
@ -129,19 +99,13 @@ public:
auto CalculateRepulsionForces() -> void; auto CalculateRepulsionForces() -> void;
auto VerletUpdate(float delta_time) -> void; auto VerletUpdate(float delta_time) -> void;
#ifndef BARNES_HUT
auto InvalidateGrid() -> void;
#endif
}; };
class ThreadedPhysics { class ThreadedPhysics {
struct AddMass { struct AddMass {};
State s;
};
struct AddSpring { struct AddSpring {
State a; std::size_t a;
State b; std::size_t b;
}; };
struct ClearGraph {}; struct ClearGraph {};
@ -156,11 +120,9 @@ class ThreadedPhysics {
std::condition_variable_any data_consumed_cnd; std::condition_variable_any data_consumed_cnd;
bool data_ready = false; bool data_ready = false;
bool data_consumed = true; bool data_consumed = true;
std::vector<Mass> masses; // Read by renderer std::vector<Vector3> masses; // Read by renderer
std::unordered_map<State, int> state_masses; // Read by renderer std::vector<std::pair<std::size_t, std::size_t>>
std::vector<Spring> springs; // Read by renderer springs; // Read by renderer
std::unordered_map<std::pair<State, State>, int>
state_springs; // Read by renderer
std::atomic<bool> running{true}; std::atomic<bool> running{true};
}; };
@ -181,6 +143,8 @@ public:
~ThreadedPhysics() { ~ThreadedPhysics() {
state.running = false; state.running = false;
state.data_ready_cnd.notify_all();
state.data_consumed_cnd.notify_all();
physics.join(); physics.join();
} }
@ -188,15 +152,15 @@ private:
static auto PhysicsThread(PhysicsState &state) -> void; static auto PhysicsThread(PhysicsState &state) -> void;
public: public:
auto AddMassCmd(const State &_state) -> void; auto AddMassCmd() -> void;
auto AddSpringCmd(const State &a, const State &b) -> void; auto AddSpringCmd(std::size_t a, std::size_t b) -> void;
auto ClearCmd() -> void; auto ClearCmd() -> void;
auto AddMassSpringsCmd(const std::unordered_set<State> &masses, auto AddMassSpringsCmd(
const std::vector<std::pair<State, State>> &springs) std::size_t num_masses,
-> void; const std::vector<std::pair<std::size_t, std::size_t>> &springs) -> void;
}; };
// https://en.cppreference.com/w/cpp/utility/variant/visit // https://en.cppreference.com/w/cpp/utility/variant/visit

View File

@ -7,7 +7,6 @@
#include <functional> #include <functional>
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <unordered_set>
#include <vector> #include <vector>
enum Direction { enum Direction {
@ -219,8 +218,9 @@ public:
auto GetNextStates() const -> std::vector<State>; auto GetNextStates() const -> std::vector<State>;
auto Closure() const -> std::pair<std::unordered_set<State>, auto Closure() const
std::vector<std::pair<State, State>>>; -> std::pair<std::vector<State>,
std::vector<std::pair<std::size_t, std::size_t>>>;
}; };
// Provide hash functions so we can use State and <State, State> as hash-set // Provide hash functions so we can use State and <State, State> as hash-set

View File

@ -3,15 +3,17 @@
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <unordered_set>
#include "camera.hpp" #include "camera.hpp"
#include "config.hpp" #include "config.hpp"
#include "physics.hpp" #include "input.hpp"
#include "puzzle.hpp" #include "state.hpp"
class Renderer { class Renderer {
private: private:
const StateManager &state;
const InputHandler &input;
const OrbitCamera3D &camera; const OrbitCamera3D &camera;
RenderTexture render_target; RenderTexture render_target;
RenderTexture klotski_target; RenderTexture klotski_target;
@ -25,13 +27,10 @@ private:
Shader instancing_shader; Shader instancing_shader;
public: public:
bool mark_solutions; Renderer(const OrbitCamera3D &_camera, const StateManager &_state,
bool connect_solutions; const InputHandler &_input)
: state(_state), input(_input), camera(_camera), transforms_size(0),
public: transforms(nullptr) {
Renderer(const OrbitCamera3D &_camera)
: camera(_camera), transforms_size(0), transforms(nullptr),
mark_solutions(false), connect_solutions(false) {
render_target = LoadRenderTexture(GetScreenWidth() / 2.0, render_target = LoadRenderTexture(GetScreenWidth() / 2.0,
GetScreenHeight() - MENU_HEIGHT); GetScreenHeight() - MENU_HEIGHT);
klotski_target = LoadRenderTexture(GetScreenWidth() / 2.0, klotski_target = LoadRenderTexture(GetScreenWidth() / 2.0,
@ -69,22 +68,14 @@ public:
auto UpdateTextureSizes() -> void; auto UpdateTextureSizes() -> void;
auto DrawMassSprings( auto DrawMassSprings(
const std::vector<Mass> &masses, const std::vector<Vector3> &masses,
const std::unordered_map<State, int> &state_masses, const std::vector<std::pair<std::size_t, std::size_t>> &springs) -> 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() -> void;
int sel_y, int block_add_x, int block_add_y,
const WinCondition win_condition) -> void;
auto DrawMenu(const std::vector<Mass> &masses, auto DrawMenu(const std::vector<Vector3> &masses,
const std::vector<Spring> &springs, int current_preset, const std::vector<std::pair<std::size_t, std::size_t>> &springs)
const State &current_state, -> void;
const std::unordered_set<State> &winning_states) -> void;
auto DrawTextures() -> void; auto DrawTextures() -> void;
}; };

View File

@ -6,11 +6,17 @@
#include "puzzle.hpp" #include "puzzle.hpp"
#include <raymath.h> #include <raymath.h>
#include <unordered_map>
#include <unordered_set>
class StateManager { class StateManager {
public: public:
ThreadedPhysics &physics; ThreadedPhysics &physics;
std::unordered_map<State, std::size_t> states;
std::unordered_set<State> winning_states;
std::unordered_set<State> visited_states;
int current_preset; int current_preset;
State starting_state; State starting_state;
State current_state; State current_state;
@ -18,16 +24,13 @@ public:
bool edited = false; bool edited = false;
std::unordered_set<State> winning_states;
std::unordered_set<State> visited_states;
public: public:
StateManager(ThreadedPhysics &_physics) StateManager(ThreadedPhysics &_physics)
: physics(_physics), 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) {
physics.AddMassCmd(current_state); ClearGraph();
} }
StateManager(const StateManager &copy) = delete; StateManager(const StateManager &copy) = delete;
@ -54,9 +57,11 @@ public:
auto FindWinningStates() -> void; auto FindWinningStates() -> void;
auto CurrentGenerator() -> StateGenerator; auto CurrentGenerator() const -> StateGenerator;
auto CurrentWinCondition() -> WinCondition; auto CurrentWinCondition() const -> WinCondition;
auto CurrentMassIndex() const -> std::size_t;
}; };
#endif #endif

42
src/backward.cpp Normal file
View File

@ -0,0 +1,42 @@
// Pick your poison.
//
// On GNU/Linux, you have few choices to get the most out of your stack trace.
//
// By default you get:
// - object filename
// - function name
//
// In order to add:
// - source filename
// - line and column numbers
// - source code snippet (assuming the file is accessible)
// Install one of the following libraries then uncomment one of the macro (or
// better, add the detection of the lib and the macro definition in your build
// system)
// - apt-get install libdw-dev ...
// - g++/clang++ -ldw ...
// #define BACKWARD_HAS_DW 1
// - apt-get install binutils-dev ...
// - g++/clang++ -lbfd ...
// #define BACKWARD_HAS_BFD 1
// - apt-get install libdwarf-dev ...
// - g++/clang++ -ldwarf ...
// #define BACKWARD_HAS_DWARF 1
// Regardless of the library you choose to read the debug information,
// for potentially more detailed stack traces you can use libunwind
// - apt-get install libunwind-dev
// - g++/clang++ -lunwind
// #define BACKWARD_HAS_LIBUNWIND 1
#include "backward.hpp"
namespace backward {
backward::SignalHandling sh;
} // namespace backward

View File

@ -133,9 +133,9 @@ auto InputHandler::HandleKeys() -> void {
} else if (IsKeyPressed(KEY_C)) { } else if (IsKeyPressed(KEY_C)) {
state.ClearGraph(); state.ClearGraph();
} else if (IsKeyPressed(KEY_I)) { } else if (IsKeyPressed(KEY_I)) {
renderer.mark_solutions = !renderer.mark_solutions; mark_solutions = !mark_solutions;
} else if (IsKeyPressed(KEY_O)) { } else if (IsKeyPressed(KEY_O)) {
renderer.connect_solutions = !renderer.connect_solutions; connect_solutions = !connect_solutions;
} else if (IsKeyPressed(KEY_F)) { } else if (IsKeyPressed(KEY_F)) {
state.current_state.ToggleRestricted(); state.current_state.ToggleRestricted();
state.ClearGraph(); state.ClearGraph();

View File

@ -36,86 +36,57 @@ auto main(int argc, char *argv[]) -> int {
InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings"); InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings");
// Game setup // Game setup
OrbitCamera3D camera;
Renderer renderer(camera);
ThreadedPhysics physics; ThreadedPhysics physics;
StateManager state(physics); // TODO: What is this warning? StateManager state(physics);
InputHandler input(state, renderer); InputHandler input(state);
OrbitCamera3D camera;
Renderer renderer(camera, state, input);
std::vector<Mass> masses; // Read from physics std::vector<Vector3> masses; // Read from physics
std::unordered_map<State, int> state_masses; // Read from physics std::vector<std::pair<std::size_t, std::size_t>> springs; // 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;
while (!WindowShouldClose()) { while (!WindowShouldClose()) {
// timestep_accumulator += GetFrameTime(); FrameMarkStart("MainThread");
// 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
if (timestep_accumulator > TIMESTEP) {
// Do not try to catch up if we're falling behind. Frametimes would get
// larger, resulting in more catching up, resulting in even larger
// frametimes -> death spiral.
mass_springs.ClearForces();
mass_springs.CalculateSpringForces();
mass_springs.CalculateRepulsionForces();
mass_springs.VerletUpdate(TIMESTEP * SIM_SPEED);
timestep_accumulator -= TIMESTEP;
}
*/
// Read positions from physics thread // Read positions from physics thread
// TODO: We're copying 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::unique_lock<LockableBase(std::mutex)> lock(physics.state.data_mtx); std::unique_lock<LockableBase(std::mutex)> lock(physics.state.data_mtx);
physics.state.data_ready_cnd.wait( physics.state.data_ready_cnd.wait(lock, [&] {
lock, [&] { return physics.state.data_ready; }); return physics.state.data_ready || !physics.state.running.load();
});
if (!physics.state.running.load()) {
// Running turned false while we were waiting for the condition
break;
}
masses = physics.state.masses; masses = physics.state.masses;
state_masses = physics.state.state_masses;
springs = physics.state.springs; springs = physics.state.springs;
state_springs = physics.state.state_springs;
physics.state.data_ready = false; physics.state.data_ready = false;
physics.state.data_consumed = true; physics.state.data_consumed = true;
} }
physics.state.data_consumed_cnd.notify_one(); physics.state.data_consumed_cnd.notify_all();
// Update the camera after the physics, so target lock is smooth // Update the camera after the physics, so target lock is smooth
if (state_masses.contains(state.current_state)) { std::size_t current_index = state.CurrentMassIndex();
const Mass &current_mass = if (masses.size() > current_index) {
masses.at(state_masses.at(state.current_state)); const Mass &current_mass = masses.at(current_index);
camera.Update(current_mass.position); camera.Update(current_mass.position);
} }
// Rendering // Rendering
renderer.UpdateTextureSizes(); renderer.UpdateTextureSizes();
if (masses.size() > 0 && state_masses.contains(state.current_state)) { renderer.DrawMassSprings(masses, springs);
renderer.DrawMassSprings(masses, state_masses, springs, state_springs, renderer.DrawKlotski();
state.current_state, state.starting_state, renderer.DrawMenu(masses, springs);
state.winning_states, state.visited_states);
}
renderer.DrawKlotski(state.current_state, input.hov_x, input.hov_y,
input.sel_x, input.sel_y, input.block_add_x,
input.block_add_y, state.CurrentWinCondition());
renderer.DrawMenu(masses, springs, state.current_preset,
state.current_state, state.winning_states);
renderer.DrawTextures(); renderer.DrawTextures();
FrameMarkEnd("MainThread");
} }
CloseWindow(); CloseWindow();

View File

@ -9,14 +9,9 @@
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <tracy/Tracy.hpp> #include <tracy/Tracy.hpp>
#include <unordered_map>
#include <utility> #include <utility>
#include <vector> #include <vector>
#ifndef BARNES_HUT
#include <numeric>
#endif
auto Mass::ClearForce() -> void { force = Vector3Zero(); } auto Mass::ClearForce() -> void { force = Vector3Zero(); }
auto Mass::CalculateVelocity(const float delta_time) -> void { auto Mass::CalculateVelocity(const float delta_time) -> void {
@ -70,55 +65,29 @@ auto Spring::CalculateSpringForce(Mass &_mass_a, Mass &_mass_b) const -> void {
_mass_b.force = Vector3Add(_mass_b.force, force_b); _mass_b.force = Vector3Add(_mass_b.force, force_b);
} }
auto MassSpringSystem::AddMass(const State &state) -> void { auto MassSpringSystem::AddMass() -> void { masses.emplace_back(Vector3Zero()); }
if (!state_masses.contains(state)) {
masses.emplace_back(Vector3Zero()); auto MassSpringSystem::AddSpring(int a, int b) -> void {
std::size_t idx = masses.size() - 1; Mass &mass_a = masses.at(a);
state_masses.insert(std::make_pair(state, idx)); Mass &mass_b = masses.at(b);
Vector3 position = mass_a.position;
Vector3 offset = Vector3(static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)));
offset = Vector3Scale(Vector3Normalize(offset), REST_LENGTH);
if (mass_b.position == Vector3Zero()) {
mass_b.position = Vector3Add(position, offset);
} }
}
auto MassSpringSystem::GetMass(const State &state) -> Mass & { springs.emplace_back(a, b);
return masses.at(state_masses.at(state));
}
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)
-> void {
std::pair<State, State> key = std::make_pair(state_a, state_b);
if (!state_springs.contains(key)) {
int a = state_masses.at(state_a);
int b = state_masses.at(state_b);
const Mass &mass_a = masses.at(a);
Mass &mass_b = masses.at(b);
Vector3 position = mass_a.position;
Vector3 offset = Vector3(static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)));
offset = Vector3Scale(Vector3Normalize(offset), REST_LENGTH);
if (mass_b.position == Vector3Zero()) {
mass_b.position = Vector3Add(position, offset);
}
springs.emplace_back(a, b);
int idx = springs.size() - 1;
state_springs.insert(std::make_pair(key, idx));
}
} }
auto MassSpringSystem::Clear() -> void { auto MassSpringSystem::Clear() -> void {
masses.clear(); masses.clear();
state_masses.clear();
springs.clear(); springs.clear();
state_springs.clear(); octree.nodes.clear();
#ifndef BARNES_HUT
InvalidateGrid();
#endif
} }
auto MassSpringSystem::ClearForces() -> void { auto MassSpringSystem::ClearForces() -> void {
@ -133,13 +102,12 @@ auto MassSpringSystem::CalculateSpringForces() -> void {
ZoneScoped; ZoneScoped;
for (const auto spring : springs) { for (const auto spring : springs) {
Mass &a = masses.at(spring.mass_a); Mass &a = masses.at(spring.a);
Mass &b = masses.at(spring.mass_b); Mass &b = masses.at(spring.b);
spring.CalculateSpringForce(a, b); spring.CalculateSpringForce(a, b);
} }
} }
#ifdef BARNES_HUT
auto MassSpringSystem::BuildOctree() -> void { auto MassSpringSystem::BuildOctree() -> void {
ZoneScoped; ZoneScoped;
@ -175,52 +143,9 @@ auto MassSpringSystem::BuildOctree() -> void {
} }
} }
#else
auto MassSpringSystem::BuildUniformGrid() -> void {
// Use a vector of pointers to masses, because we can't parallelize the
// range-based for loop over the masses unordered_map using OpenMP.
mass_pointers.clear();
mass_pointers.reserve(masses.size());
for (auto &[state, mass] : masses) {
mass_pointers.push_back(&mass);
}
// Assign each mass a cell_id based on its position.
auto cell_id = [&](const Vector3 &position) -> int64_t {
int x = (int)std::floor(position.x / REPULSION_RANGE);
int y = (int)std::floor(position.y / REPULSION_RANGE);
int z = (int)std::floor(position.z / REPULSION_RANGE);
// Pack into a single int64 (assumes a coordinate fits in 20 bits)
return ((int64_t)(x & 0xFFFFF) << 40) | ((int64_t)(y & 0xFFFFF) << 20) |
(int64_t)(z & 0xFFFFF);
};
// Sort mass indices by cell_id to improve cache locality and allow cell
// iteration with std::lower_bound and std::upper_bound
mass_indices.clear();
mass_indices.resize(masses.size());
std::iota(mass_indices.begin(), mass_indices.end(),
0); // Fill the indices array with ascending numbers
std::sort(mass_indices.begin(), mass_indices.end(), [&](int a, int b) {
return cell_id(mass_pointers[a]->position) <
cell_id(mass_pointers[b]->position);
});
// Build cell start/end table: maps mass index to cell_id.
// All indices of a single cell are consecutive.
cell_ids.clear();
cell_ids.resize(masses.size());
for (int i = 0; i < masses.size(); ++i) {
cell_ids[i] = cell_id(mass_pointers[mass_indices[i]]->position);
}
}
#endif
auto MassSpringSystem::CalculateRepulsionForces() -> void { auto MassSpringSystem::CalculateRepulsionForces() -> void {
ZoneScoped; ZoneScoped;
#ifdef BARNES_HUT
BuildOctree(); BuildOctree();
auto solve_octree = [&](int i) { auto solve_octree = [&](int i) {
@ -238,86 +163,6 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void {
threads.submit_loop(0, masses.size(), solve_octree, 256); threads.submit_loop(0, masses.size(), solve_octree, 256);
loop_future.wait(); loop_future.wait();
#endif #endif
#else
// Refresh grid if necessary
if (last_build >= REPULSION_GRID_REFRESH ||
masses.size() != last_masses_count ||
springs.size() != last_springs_count) {
BuildUniformGrid();
last_build = 0;
last_masses_count = masses.size();
last_springs_count = springs.size();
}
last_build++;
auto solve_grid = [&](int i) {
Mass *mass = mass_pointers[mass_indices[i]];
int cell_x = (int)std::floor(mass->position.x / REPULSION_RANGE);
int cell_y = (int)std::floor(mass->position.y / REPULSION_RANGE);
int cell_z = (int)std::floor(mass->position.z / REPULSION_RANGE);
Vector3 force = Vector3Zero();
// Search all 3*3*3 neighbor cells for masses
for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
for (int dz = -1; dz <= 1; ++dz) {
int64_t neighbor_id = ((int64_t)((cell_x + dx) & 0xFFFFF) << 40) |
((int64_t)((cell_y + dy) & 0xFFFFF) << 20) |
(int64_t)((cell_z + dz) & 0xFFFFF);
// Find the first and last occurence of the neighbor_id (iterator).
// Because cell_ids is sorted, all elements of this cell are between
// those.
// If there is no cell, the iterators just won't do anything.
auto cell_start =
std::lower_bound(cell_ids.begin(), cell_ids.end(), neighbor_id);
auto cell_end =
std::upper_bound(cell_ids.begin(), cell_ids.end(), neighbor_id);
// For each mass, iterate through all the masses of neighboring cells
// to accumulate the repulsion forces.
// This is slow with O(n * m), where m is the number of masses in each
// neighboring cell.
for (auto it = cell_start; it != cell_end; ++it) {
Mass *neighbor = mass_pointers[mass_indices[it - cell_ids.begin()]];
if (neighbor == mass) {
// Skip ourselves
continue;
}
Vector3 direction =
Vector3Subtract(mass->position, neighbor->position);
float distance = Vector3Length(direction);
if (std::abs(distance) <= 0.001f || distance >= REPULSION_RANGE) {
continue;
}
force = Vector3Add(
force, Vector3Scale(Vector3Normalize(direction), GRID_FORCE));
}
}
}
}
mass->force = Vector3Add(mass->force, force);
};
// Calculate forces using uniform grid
#ifdef WEB
// Search the neighboring cells for each mass to calculate repulsion forces
for (int i = 0; i < mass_pointers.size(); ++i) {
calculate_grid(i);
}
#else
BS::multi_future<void> loop_future =
threads.submit_loop(0, mass_pointers.size(), solve_grid, 512);
loop_future.wait();
#endif
#endif
} }
auto MassSpringSystem::VerletUpdate(float delta_time) -> void { auto MassSpringSystem::VerletUpdate(float delta_time) -> void {
@ -328,28 +173,18 @@ auto MassSpringSystem::VerletUpdate(float delta_time) -> void {
} }
} }
#ifndef BARNES_HUT
auto MassSpringSystem::InvalidateGrid() -> void {
mass_pointers.clear();
mass_indices.clear();
cell_ids.clear();
last_build = REPULSION_GRID_REFRESH;
last_masses_count = 0;
last_springs_count = 0;
}
#endif
auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state) auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state)
-> void { -> void {
MassSpringSystem mass_springs; MassSpringSystem mass_springs;
const auto visitor = overloads{ const auto visitor = overloads{
[&](const struct AddMass &am) { mass_springs.AddMass(am.s); }, [&](const struct AddMass &am) { mass_springs.AddMass(); },
[&](const struct AddSpring &as) { mass_springs.AddSpring(as.a, as.b); }, [&](const struct AddSpring &as) { mass_springs.AddSpring(as.a, as.b); },
[&](const struct ClearGraph &cg) { mass_springs.Clear(); }, [&](const struct ClearGraph &cg) { mass_springs.Clear(); },
}; };
while (state.running.load()) { while (state.running.load()) {
FrameMarkStart("PhysicsThread");
// Handle queued commands // Handle queued commands
{ {
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx); std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
@ -371,39 +206,46 @@ auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state)
mass_springs.CalculateRepulsionForces(); mass_springs.CalculateRepulsionForces();
mass_springs.VerletUpdate(TIMESTEP * SIM_SPEED); mass_springs.VerletUpdate(TIMESTEP * SIM_SPEED);
// TODO: Notify the main thread of update
// TODO: Just wait here until the main thread has fetched the data?
// Then only the copying would be sequential, while the main thread
// renders this thread can already calculate the physics...
// TODO: Would std::swap help in any way?
// Publish the positions for the renderer (copy) // Publish the positions for the renderer (copy)
{ {
std::unique_lock<LockableBase(std::mutex)> lock(state.data_mtx); std::unique_lock<LockableBase(std::mutex)> lock(state.data_mtx);
state.data_consumed_cnd.wait(lock, [&] { return state.data_consumed; }); state.data_consumed_cnd.wait(
lock, [&] { return state.data_consumed || !state.running.load(); });
if (!state.running.load()) {
// Running turned false while we were waiting for the condition
break;
}
state.masses = mass_springs.masses; state.masses.clear();
state.state_masses = mass_springs.state_masses; state.masses.reserve(mass_springs.masses.size());
state.springs = mass_springs.springs; for (const auto &mass : mass_springs.masses) {
state.state_springs = mass_springs.state_springs; state.masses.emplace_back(mass.position);
}
state.springs.clear();
state.springs.reserve(mass_springs.springs.size());
for (const auto &spring : mass_springs.springs) {
state.springs.emplace_back(spring.a, spring.b);
}
state.data_ready = true; state.data_ready = true;
state.data_consumed = false; state.data_consumed = false;
} }
// Notify the rendering thread that new data is available // Notify the rendering thread that new data is available
state.data_ready_cnd.notify_one(); state.data_ready_cnd.notify_all();
FrameMarkEnd("PhysicsThread");
} }
} }
auto ThreadedPhysics::AddMassCmd(const State &_state) -> void { auto ThreadedPhysics::AddMassCmd() -> void {
{ {
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx); std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
state.pending_commands.push(AddMass{_state}); state.pending_commands.push(AddMass{});
} }
} }
auto ThreadedPhysics::AddSpringCmd(const State &a, const State &b) -> void { auto ThreadedPhysics::AddSpringCmd(std::size_t a, std::size_t b) -> void {
{ {
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx); std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
state.pending_commands.push(AddSpring{a, b}); state.pending_commands.push(AddSpring{a, b});
@ -418,12 +260,12 @@ auto ThreadedPhysics::ClearCmd() -> void {
} }
auto ThreadedPhysics::AddMassSpringsCmd( auto ThreadedPhysics::AddMassSpringsCmd(
const std::unordered_set<State> &masses, std::size_t num_masses,
const std::vector<std::pair<State, State>> &springs) -> void { const std::vector<std::pair<std::size_t, std::size_t>> &springs) -> void {
{ {
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx); std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
for (const auto &_state : masses) { for (std::size_t i = 0; i < num_masses; ++i) {
state.pending_commands.push(AddMass{_state}); state.pending_commands.push(AddMass{});
} }
for (const auto &[from, to] : springs) { for (const auto &[from, to] : springs) {
state.pending_commands.push(AddSpring{from, to}); state.pending_commands.push(AddSpring{from, to});

View File

@ -1,6 +1,8 @@
#include "puzzle.hpp" #include "puzzle.hpp"
#include "tracy.hpp" #include "tracy.hpp"
#include <unordered_set>
auto Block::Hash() const -> int { auto Block::Hash() const -> int {
std::string s = std::format("{},{},{},{}", x, y, width, height); std::string s = std::format("{},{},{},{}", x, y, width, height);
return std::hash<std::string>{}(s); return std::hash<std::string>{}(s);
@ -262,11 +264,16 @@ auto State::GetNextStates() const -> std::vector<State> {
return new_states; return new_states;
} }
auto State::Closure() const -> std::pair<std::unordered_set<State>, auto State::Closure() const
std::vector<std::pair<State, State>>> { -> std::pair<std::vector<State>,
std::unordered_set<State> states; std::vector<std::pair<std::size_t, std::size_t>>> {
std::vector<std::pair<State, State>> links; std::vector<State> states;
std::vector<std::pair<std::size_t, std::size_t>> links;
// Helper to construct the links vector
std::unordered_map<State, std::size_t> state_indices;
// Buffer for all states we want to call GetNextStates() on
std::unordered_set<State> remaining_states; std::unordered_set<State> remaining_states;
remaining_states.insert(*this); remaining_states.insert(*this);
@ -274,13 +281,18 @@ auto State::Closure() const -> std::pair<std::unordered_set<State>,
const State current = *remaining_states.begin(); const State current = *remaining_states.begin();
remaining_states.erase(current); remaining_states.erase(current);
std::vector<State> new_states = current.GetNextStates(); if (!state_indices.contains(current)) {
for (const State &s : new_states) { state_indices.emplace(current, states.size());
if (!states.contains(s)) { states.push_back(current);
}
for (const State &s : current.GetNextStates()) {
if (!state_indices.contains(s)) {
remaining_states.insert(s); remaining_states.insert(s);
states.insert(s); state_indices.emplace(s, states.size());
states.push_back(s);
} }
links.emplace_back(current.state, s.state); links.emplace_back(state_indices.at(current), state_indices.at(s));
} }
} while (remaining_states.size() > 0); } while (remaining_states.size() > 0);

View File

@ -1,6 +1,5 @@
#include "renderer.hpp" #include "renderer.hpp"
#include "config.hpp" #include "config.hpp"
#include "physics.hpp"
#include "puzzle.hpp" #include "puzzle.hpp"
#include "tracy.hpp" #include "tracy.hpp"
@ -10,7 +9,6 @@
#include <raymath.h> #include <raymath.h>
#include <rlgl.h> #include <rlgl.h>
#include <tracy/Tracy.hpp> #include <tracy/Tracy.hpp>
#include <unordered_set>
#ifdef BATCHING #ifdef BATCHING
#include <cstring> #include <cstring>
@ -59,13 +57,8 @@ auto Renderer::ReallocateGraphInstancingIfNecessary(std::size_t size) -> void {
} }
auto Renderer::DrawMassSprings( auto Renderer::DrawMassSprings(
const std::vector<Mass> &masses, const std::vector<Vector3> &masses,
const std::unordered_map<State, int> &state_masses, const std::vector<std::pair<std::size_t, std::size_t>> &springs) -> 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 {
ZoneScoped; ZoneScoped;
// Prepare cube instancing // Prepare cube instancing
@ -78,9 +71,8 @@ auto Renderer::DrawMassSprings(
ReallocateGraphInstancingIfNecessary(masses.size()); ReallocateGraphInstancingIfNecessary(masses.size());
int i = 0; int i = 0;
for (const auto &mass : masses) { for (const Vector3 &mass : masses) {
transforms[i] = transforms[i] = MatrixTranslate(mass.x, mass.y, mass.z);
MatrixTranslate(mass.position.x, mass.position.y, mass.position.z);
++i; ++i;
} }
} }
@ -95,13 +87,14 @@ auto Renderer::DrawMassSprings(
{ {
ZoneNamedN(draw_springs, "DrawSprings", true); ZoneNamedN(draw_springs, "DrawSprings", true);
rlBegin(RL_LINES); rlBegin(RL_LINES);
for (const auto &spring : springs) { for (const auto &[from, to] : springs) {
// We have to do a lookup of the actual mass object, which is slow :( if (masses.size() > from && masses.size() > to) {
const Mass &a = masses.at(spring.mass_a); const Vector3 &a = masses.at(from);
const Mass &b = masses.at(spring.mass_b); const Vector3 &b = masses.at(to);
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.x, a.y, a.z);
rlVertex3f(b.position.x, b.position.y, b.position.z); rlVertex3f(b.x, b.y, b.z);
}
} }
rlEnd(); rlEnd();
} }
@ -119,38 +112,53 @@ auto Renderer::DrawMassSprings(
} }
// Mark winning states // Mark winning states
if (mark_solutions || connect_solutions) { if (input.mark_solutions || input.connect_solutions) {
for (const auto &state : winning_states) { for (const State &_state : state.winning_states) {
const Mass &winning_mass = masses.at(state_masses.at(state));
if (mark_solutions) {
DrawCube(winning_mass.position, 2 * VERTEX_SIZE, 2 * VERTEX_SIZE,
2 * VERTEX_SIZE, BLUE);
}
if (connect_solutions) { std::size_t winning_index = state.states.at(_state);
DrawLine3D(winning_mass.position, if (masses.size() > winning_index) {
masses.at(state_masses.at(current_state)).position, PURPLE);
const Vector3 &winning_mass = masses.at(winning_index);
if (input.mark_solutions) {
DrawCube(winning_mass, 2 * VERTEX_SIZE, 2 * VERTEX_SIZE,
2 * VERTEX_SIZE, BLUE);
}
std::size_t current_index = state.CurrentMassIndex();
if (input.connect_solutions && masses.size() > current_index) {
const Vector3 &current_mass = masses.at(current_index);
DrawLine3D(winning_mass, current_mass, PURPLE);
}
} }
} }
} }
// Mark visited states // Mark visited states
for (const auto &state : visited_states) { for (const State &_state : state.visited_states) {
const Mass &visited_mass = masses.at(state_masses.at(state)); std::size_t visited_index = state.states.at(_state);
DrawCube(visited_mass.position, VERTEX_SIZE * 1.5, VERTEX_SIZE * 1.5, if (masses.size() > visited_index) {
VERTEX_SIZE * 1.5, PURPLE); const Vector3 &visited_mass = masses.at(visited_index);
DrawCube(visited_mass, VERTEX_SIZE * 1.5, VERTEX_SIZE * 1.5,
VERTEX_SIZE * 1.5, PURPLE);
}
} }
// Mark starting state // Mark starting state
const Mass &starting_mass = masses.at(state_masses.at(starting_state)); std::size_t starting_index = state.states.at(state.starting_state);
DrawCube(starting_mass.position, VERTEX_SIZE * 2, VERTEX_SIZE * 2, if (masses.size() > starting_index) {
VERTEX_SIZE * 2, ORANGE); const Vector3 &starting_mass = masses.at(starting_index);
DrawCube(starting_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2,
ORANGE);
}
// Mark current state // Mark current state
const Mass &current_mass = masses.at(state_masses.at(current_state)); std::size_t current_index = state.states.at(state.current_state);
DrawCube(current_mass.position, VERTEX_SIZE * 2, VERTEX_SIZE * 2, if (masses.size() > current_index) {
VERTEX_SIZE * 2, RED); const Vector3 &current_mass = masses.at(current_index);
DrawCube(current_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2,
RED);
}
// DrawCubeWires(current_mass.position, REPULSION_RANGE, REPULSION_RANGE, // DrawCubeWires(current_mass.position, REPULSION_RANGE, REPULSION_RANGE,
// REPULSION_RANGE, BLACK); // REPULSION_RANGE, BLACK);
@ -162,9 +170,7 @@ auto Renderer::DrawMassSprings(
EndTextureMode(); EndTextureMode();
} }
auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, auto Renderer::DrawKlotski() -> void {
int sel_y, int block_add_x, int block_add_y,
const WinCondition win_condition) -> void {
ZoneScoped; ZoneScoped;
BeginTextureMode(klotski_target); BeginTextureMode(klotski_target);
@ -173,22 +179,26 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x,
// Draw Board // Draw Board
const int board_width = GetScreenWidth() / 2 - 2 * BOARD_PADDING; const int board_width = GetScreenWidth() / 2 - 2 * BOARD_PADDING;
const int board_height = GetScreenHeight() - MENU_HEIGHT - 2 * BOARD_PADDING; const int board_height = GetScreenHeight() - MENU_HEIGHT - 2 * BOARD_PADDING;
int block_size = int block_size = std::min(board_width / state.current_state.width,
std::min(board_width / state.width, board_height / state.height) - board_height / state.current_state.height) -
2 * BLOCK_PADDING; 2 * BLOCK_PADDING;
int x_offset = int x_offset = (board_width - (block_size + 2 * BLOCK_PADDING) *
(board_width - (block_size + 2 * BLOCK_PADDING) * state.width) / 2.0; state.current_state.width) /
int y_offset = 2.0;
(board_height - (block_size + 2 * BLOCK_PADDING) * state.height) / 2.0; int y_offset = (board_height - (block_size + 2 * BLOCK_PADDING) *
state.current_state.height) /
2.0;
DrawRectangle(0, 0, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, DrawRectangle(0, 0, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT,
RAYWHITE); RAYWHITE);
DrawRectangle( DrawRectangle(x_offset, y_offset,
x_offset, y_offset, board_width - 2 * x_offset + 2 * BOARD_PADDING, board_width - 2 * x_offset + 2 * BOARD_PADDING,
board_height - 2 * y_offset + 2 * BOARD_PADDING, board_height - 2 * y_offset + 2 * BOARD_PADDING,
win_condition(state) ? GREEN : (state.restricted ? DARKGRAY : LIGHTGRAY)); state.CurrentWinCondition()(state.current_state)
for (int x = 0; x < state.width; ++x) { ? GREEN
for (int y = 0; y < state.height; ++y) { : (state.current_state.restricted ? DARKGRAY : LIGHTGRAY));
for (int x = 0; x < state.current_state.width; ++x) {
for (int y = 0; y < state.current_state.height; ++y) {
DrawRectangle(x_offset + BOARD_PADDING + x * BLOCK_PADDING * 2 + DrawRectangle(x_offset + BOARD_PADDING + x * BLOCK_PADDING * 2 +
BLOCK_PADDING + x * block_size, BLOCK_PADDING + x * block_size,
y_offset + BOARD_PADDING + y * BLOCK_PADDING * 2 + y_offset + BOARD_PADDING + y * BLOCK_PADDING * 2 +
@ -198,13 +208,13 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x,
} }
// Draw Blocks // Draw Blocks
for (Block block : state) { for (Block block : state.current_state) {
Color c = BLOCK_COLOR; Color c = BLOCK_COLOR;
if (block.Covers(sel_x, sel_y)) { if (block.Covers(input.sel_x, input.sel_y)) {
c = HL_BLOCK_COLOR; c = HL_BLOCK_COLOR;
} }
if (block.target) { if (block.target) {
if (block.Covers(sel_x, sel_y)) { if (block.Covers(input.sel_x, input.sel_y)) {
c = HL_TARGET_BLOCK_COLOR; c = HL_TARGET_BLOCK_COLOR;
} else { } else {
c = TARGET_BLOCK_COLOR; c = TARGET_BLOCK_COLOR;
@ -220,7 +230,7 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x,
2 * BLOCK_PADDING, 2 * BLOCK_PADDING,
c); c);
if (block.Covers(hov_x, hov_y)) { if (block.Covers(input.hov_x, input.hov_y)) {
DrawRectangleLinesEx( DrawRectangleLinesEx(
Rectangle(x_offset + BOARD_PADDING + block.x * BLOCK_PADDING * 2 + Rectangle(x_offset + BOARD_PADDING + block.x * BLOCK_PADDING * 2 +
BLOCK_PADDING + block.x * block_size, BLOCK_PADDING + block.x * block_size,
@ -235,12 +245,13 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x,
} }
// Draw editing starting position // Draw editing starting position
if (block_add_x >= 0 && block_add_y >= 0) { if (input.block_add_x >= 0 && input.block_add_y >= 0) {
DrawCircle(x_offset + BOARD_PADDING + block_add_x * BLOCK_PADDING * 2 + DrawCircle(
BLOCK_PADDING + block_add_x * block_size + block_size / 2, x_offset + BOARD_PADDING + input.block_add_x * BLOCK_PADDING * 2 +
y_offset + BOARD_PADDING + block_add_y * BLOCK_PADDING * 2 + BLOCK_PADDING + input.block_add_x * block_size + block_size / 2,
BLOCK_PADDING + block_add_y * block_size + block_size / 2, y_offset + BOARD_PADDING + input.block_add_y * BLOCK_PADDING * 2 +
block_size / 10.0, Fade(BLACK, 0.5)); BLOCK_PADDING + input.block_add_y * block_size + block_size / 2,
block_size / 10.0, Fade(BLACK, 0.5));
} }
DrawLine(GetScreenWidth() / 2 - 1, 0, GetScreenWidth() / 2 - 1, DrawLine(GetScreenWidth() / 2 - 1, 0, GetScreenWidth() / 2 - 1,
@ -248,11 +259,9 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x,
EndTextureMode(); EndTextureMode();
} }
auto Renderer::DrawMenu(const std::vector<Mass> &masses, auto Renderer::DrawMenu(
const std::vector<Spring> &springs, int current_preset, const std::vector<Vector3> &masses,
const State &current_state, const std::vector<std::pair<std::size_t, std::size_t>> &springs) -> void {
const std::unordered_set<State> &winning_states)
-> void {
ZoneScoped; ZoneScoped;
BeginTextureMode(menu_target); BeginTextureMode(menu_target);
@ -276,7 +285,8 @@ auto Renderer::DrawMenu(const std::vector<Mass> &masses,
draw_btn(0, 0, draw_btn(0, 0,
std::format("States: {}, Transitions: {}, Winning: {}", std::format("States: {}, Transitions: {}, Winning: {}",
masses.size(), springs.size(), winning_states.size()), masses.size(), springs.size(),
state.winning_states.size()),
DARKGREEN); DARKGREEN);
draw_btn( draw_btn(
0, 1, 0, 1,
@ -292,14 +302,14 @@ auto Renderer::DrawMenu(const std::vector<Mass> &masses,
draw_btn(1, 2, std::format("Print Board State to Console (P)"), DARKBLUE); draw_btn(1, 2, std::format("Print Board State to Console (P)"), DARKBLUE);
draw_btn(2, 0, draw_btn(2, 0,
std::format("Preset (M/N): {}, {} (F)", current_preset, std::format("Preset (M/N): {}, {} (F)", state.current_preset,
current_state.restricted ? "Restricted" : "Free"), state.current_state.restricted ? "Restricted" : "Free"),
DARKPURPLE); DARKPURPLE);
draw_btn(2, 1, std::format("Populate Graph (G), Clear Graph (C)"), draw_btn(2, 1, std::format("Populate Graph (G), Clear Graph (C)"),
DARKPURPLE); DARKPURPLE);
draw_btn(2, 2, draw_btn(2, 2,
std::format("Mark (I): {} / Connect (O): {}", mark_solutions, std::format("Mark (I): {} / Connect (O): {}", input.mark_solutions,
connect_solutions), input.connect_solutions),
DARKPURPLE); DARKPURPLE);
DrawLine(0, MENU_HEIGHT - 1, GetScreenWidth(), MENU_HEIGHT - 1, BLACK); DrawLine(0, MENU_HEIGHT - 1, GetScreenWidth(), MENU_HEIGHT - 1, BLACK);
@ -322,5 +332,4 @@ auto Renderer::DrawTextures() -> void {
Vector2(GetScreenWidth() / 2.0, MENU_HEIGHT), WHITE); Vector2(GetScreenWidth() / 2.0, MENU_HEIGHT), WHITE);
DrawFPS(GetScreenWidth() / 2 + 10, MENU_HEIGHT + 10); DrawFPS(GetScreenWidth() / 2 + 10, MENU_HEIGHT + 10);
EndDrawing(); EndDrawing();
FrameMark;
} }

View File

@ -2,19 +2,16 @@
#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 {
current_state = generators[preset]();
previous_state = current_state;
ClearGraph();
current_preset = preset; current_preset = preset;
ClearGraph();
edited = false; edited = false;
} }
auto StateManager::ResetState() -> void { auto StateManager::ResetState() -> void {
current_state = generators[current_preset](); current_state = CurrentGenerator()();
previous_state = current_state; previous_state = current_state;
if (edited) { if (edited) {
// We also need to clear the graph in case the state has been edited // We also need to clear the graph in case the state has been edited
@ -35,59 +32,67 @@ auto StateManager::NextPreset() -> void {
auto StateManager::FillGraph() -> void { auto StateManager::FillGraph() -> void {
ClearGraph(); ClearGraph();
std::pair<std::unordered_set<State>, std::vector<std::pair<State, State>>> std::pair<std::vector<State>,
std::vector<std::pair<std::size_t, std::size_t>>>
closure = current_state.Closure(); closure = current_state.Closure();
physics.AddMassSpringsCmd(closure.first, closure.second); physics.AddMassSpringsCmd(closure.first.size(), closure.second);
for (const State &state : closure.first) {
// TODO: We have only dispatched the commands, the states won't be downloaded states.insert(std::make_pair(state, states.size()));
// when calling this... Make FindWinningStates() another command? }
// Or recalculate whenever masses.size() changes? FindWinningStates();
// FindWinningStates();
} }
auto StateManager::UpdateGraph() -> void { auto StateManager::UpdateGraph() -> void {
if (previous_state != current_state) { if (previous_state == current_state) {
physics.AddMassCmd(current_state); return;
physics.AddSpringCmd(current_state, previous_state); }
if (win_conditions[current_preset](current_state)) {
winning_states.insert(current_state); if (!states.contains(current_state)) {
} physics.AddMassCmd();
visited_states.insert(current_state); physics.AddSpringCmd(states.at(current_state), states.at(previous_state));
}
states.insert(std::make_pair(current_state, states.size()));
visited_states.insert(current_state);
if (win_conditions[current_preset](current_state)) {
winning_states.insert(current_state);
} }
} }
auto StateManager::ClearGraph() -> void { auto StateManager::ClearGraph() -> void {
states.clear();
winning_states.clear(); winning_states.clear();
visited_states.clear(); visited_states.clear();
physics.ClearCmd(); physics.ClearCmd();
physics.AddMassCmd(current_state);
// The previous_state is no longer in the graph current_state = CurrentGenerator()();
states.insert(std::make_pair(current_state, states.size()));
visited_states.insert(current_state);
physics.AddMassCmd();
// These states are no longer in the graph
previous_state = current_state; previous_state = current_state;
// The starting state is no longer in the graph
starting_state = current_state; starting_state = current_state;
} }
auto StateManager::FindWinningStates() -> void { auto StateManager::FindWinningStates() -> void {
winning_states.clear(); winning_states.clear();
std::unordered_map<State, int> state_masses; for (const auto &[state, mass] : states) {
{ if (CurrentWinCondition()(state)) {
std::lock_guard<LockableBase(std::mutex)> lock(physics.state.data_mtx);
state_masses = physics.state.state_masses;
}
for (const auto &[state, mass] : state_masses) {
if (win_conditions[current_preset](state)) {
winning_states.insert(state); winning_states.insert(state);
} }
} }
} }
auto StateManager::CurrentGenerator() -> StateGenerator { auto StateManager::CurrentGenerator() const -> StateGenerator {
return generators[current_preset]; return generators[current_preset];
} }
auto StateManager::CurrentWinCondition() -> WinCondition { auto StateManager::CurrentWinCondition() const -> WinCondition {
return win_conditions[current_preset]; return win_conditions[current_preset];
} }
auto StateManager::CurrentMassIndex() const -> std::size_t {
return states.at(current_state);
}