diff --git a/CMakeLists.txt b/CMakeLists.txt index 294e404..be16d57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,11 @@ 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) +find_package(Backward REQUIRED) # Need to enable/disable this based on a variable for nix build if(USE_TRACY) @@ -43,14 +44,15 @@ add_executable(masssprings src/state.cpp src/input.cpp src/tracy.cpp + src/backward.cpp ) target_include_directories(masssprings PUBLIC ${RAYLIB_CPP_INCLUDE_DIR}) if(USE_TRACY) - target_link_libraries(masssprings PUBLIC raylib TracyClient) + target_link_libraries(masssprings PUBLIC raylib Backward::Backward TracyClient) else() - target_link_libraries(masssprings PUBLIC raylib) + target_link_libraries(masssprings PUBLIC raylib Backward::Backward) endif() include(CheckIPOSupported) diff --git a/flake.nix b/flake.nix index 694ed91..e0c15be 100644 --- a/flake.nix +++ b/flake.nix @@ -128,7 +128,7 @@ rec { # - Interpreters needed by patchShebangs for build scripts (with the --build flag), which can be the case for e.g. perl nativeBuildInputs = with pkgs; [ # Languages: - # bintools + binutils gcc # C/C++: @@ -159,6 +159,8 @@ rec { # octree # this one doesn't store center of mass per node - which I need :( tracy-wayland thread-pool + backward-cpp + libbfd # llvmPackages.openmp # not required for compilation but for clangd to find the headers # raylib-cpp # tinyobjloader @@ -283,7 +285,7 @@ rec { abbr -a debug "${buildDebug} && ./cmake-build-debug/masssprings" abbr -a release "${buildRelease} && ./cmake-build-release/masssprings" abbr -a rungdb "${buildDebug} && gdb --tui ./cmake-build-debug/masssprings" - abbr -a runtracy "tracy -a 127.0.0.1" + abbr -a runtracy "tracy -a 127.0.0.1 &; ${buildRelease} && sudo -E ./cmake-build-release/masssprings" ''; in builtins.concatStringsSep "\n" [ diff --git a/include/config.hpp b/include/config.hpp index 7440163..72b658f 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -3,7 +3,6 @@ #include -#define BARNES_HUT // Use octree BH instead of uniform grid // #define WEB // Disables multithreading // Window @@ -30,19 +29,17 @@ constexpr float ROT_SPEED = 1.0; constexpr float CAMERA_SMOOTH_SPEED = 15.0; // Physics Engine -constexpr float SIM_SPEED = 4.0; // How large each update should be -constexpr float TIMESTEP = 1.0 / 90; // Do 90 physics updates per second -constexpr float MASS = 1.0; // Mass spring system -constexpr float SPRING_CONSTANT = 5.0; // Mass spring system -constexpr float DAMPENING_CONSTANT = 1.0; // Mass spring system -constexpr float REST_LENGTH = 2.0; // Mass spring system -constexpr float VERLET_DAMPENING = 0.05; // [0, 1] -constexpr float BH_FORCE = 2.0; // BH: [1.0, 3.0] -constexpr float THETA = 1.0; // Barnes-Hut [0.5, 1.0] -constexpr float SOFTENING = 0.01; // Barnes-Hut [0.01, 1.0] -constexpr float GRID_FORCE = 0.02; // Grid: [0.0, ~0.05] -constexpr float REPULSION_RANGE = 5.0 * REST_LENGTH; // Grid -constexpr int REPULSION_GRID_REFRESH = 5; // Grid rebuild freq +constexpr float TARGET_UPS = 90; // How often to update physics +constexpr float TIMESTEP = 1.0 / TARGET_UPS; // Update interval in seconds +constexpr float SIM_SPEED = 4.0; // How large each update should be +constexpr float MASS = 1.0; // Mass spring system +constexpr float SPRING_CONSTANT = 5.0; // Mass spring system +constexpr float DAMPENING_CONSTANT = 1.0; // Mass spring system +constexpr float REST_LENGTH = 2.0; // Mass spring system +constexpr float VERLET_DAMPENING = 0.05; // [0, 1] +constexpr float BH_FORCE = 2.0; // Barnes-Hut [1.0, 3.0] +constexpr float THETA = 0.9; // Barnes-Hut [0.5, 1.0] +constexpr float SOFTENING = 0.01; // Barnes-Hut [0.01, 1.0] // Graph Drawing constexpr float VERTEX_SIZE = 0.5; diff --git a/include/input.hpp b/include/input.hpp index 69e0bc5..8ef34c0 100644 --- a/include/input.hpp +++ b/include/input.hpp @@ -1,13 +1,11 @@ #ifndef __INPUT_HPP_ #define __INPUT_HPP_ -#include "renderer.hpp" #include "state.hpp" class InputHandler { public: StateManager &state; - Renderer &renderer; int hov_x; int hov_y; @@ -18,10 +16,14 @@ public: int block_add_x; int block_add_y; + bool mark_solutions; + bool connect_solutions; + public: - InputHandler(StateManager &_state, Renderer &_renderer) - : state(_state), renderer(_renderer), hov_x(-1), hov_y(-1), sel_x(0), - sel_y(0), has_block_add_xy(false), block_add_x(-1), block_add_y(-1) {} + InputHandler(StateManager &_state) + : state(_state), hov_x(-1), hov_y(-1), sel_x(0), sel_y(0), + has_block_add_xy(false), block_add_x(-1), block_add_y(-1), + mark_solutions(false), connect_solutions(false) {} InputHandler(const InputHandler ©) = delete; InputHandler &operator=(const InputHandler ©) = delete; diff --git a/include/physics.hpp b/include/physics.hpp index c5f4149..2dd8b36 100644 --- a/include/physics.hpp +++ b/include/physics.hpp @@ -1,19 +1,22 @@ #ifndef __PHYSICS_HPP_ #define __PHYSICS_HPP_ +#include +#include +#include +#include +#include #include #include -#include +#include +#include +#include #include -#include "config.hpp" -#include "puzzle.hpp" - -#ifdef BARNES_HUT #include "octree.hpp" -#endif #ifndef WEB +#define BS_THREAD_POOL_NATIVE_EXTENSIONS #include #endif @@ -41,29 +44,19 @@ public: class Spring { public: - int mass_a; - int mass_b; + std::size_t a; + std::size_t b; 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: - auto CalculateSpringForce(Mass &_mass_a, Mass &_mass_b) const -> void; + auto CalculateSpringForce(Mass &_a, Mass &_b) const -> void; }; class MassSpringSystem { private: -#ifdef BARNES_HUT - // Barnes-Hut Octree octree; -#else - // Uniform grid - std::vector mass_indices; - std::vector cell_ids; - int last_build; - int last_masses_count; - int last_springs_count; -#endif #ifndef WEB BS::thread_pool threads; @@ -72,19 +65,13 @@ private: public: // This is the main ownership of all the states/masses/springs. std::vector masses; - std::unordered_map state_masses; std::vector springs; - std::unordered_map, int> state_springs; public: - MassSpringSystem() { -#ifndef BARNES_HUT - last_build = REPULSION_GRID_REFRESH; - std::cout << "Using uniform grid repulsion force calculation." << std::endl; -#else + MassSpringSystem() + : threads(std::thread::hardware_concurrency() - 1, SetThreadName) { std::cout << "Using Barnes-Hut + octree repulsion force calculation." << std::endl; -#endif #ifndef WEB std::cout << "Thread-Pool: " << threads.get_thread_count() << " threads." @@ -98,21 +85,14 @@ public: MassSpringSystem &operator=(MassSpringSystem &&move) = delete; private: -#ifdef BARNES_HUT + static auto SetThreadName(std::size_t idx) -> void; + auto BuildOctree() -> void; -#else - auto BuildUniformGrid() -> void; -#endif public: - auto AddMass(float mass, bool fixed, const State &state) -> void; + auto AddMass() -> 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(int a, int b) -> void; auto Clear() -> void; @@ -123,10 +103,74 @@ public: auto CalculateRepulsionForces() -> void; auto VerletUpdate(float delta_time) -> void; +}; -#ifndef BARNES_HUT - auto InvalidateGrid() -> void; -#endif +class ThreadedPhysics { + struct AddMass {}; + struct AddSpring { + std::size_t a; + std::size_t b; + }; + struct ClearGraph {}; + + using Command = std::variant; + + struct PhysicsState { + TracyLockable(std::mutex, command_mtx); + std::queue pending_commands; + + TracyLockable(std::mutex, data_mtx); + std::condition_variable_any data_ready_cnd; + std::condition_variable_any data_consumed_cnd; + unsigned int ups = 0; + bool data_ready = false; + bool data_consumed = true; + std::vector masses; // Read by renderer + std::vector> + springs; // Read by renderer + + std::atomic running{true}; + }; + +private: + std::thread physics; + +public: + PhysicsState state; + +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; + state.data_ready_cnd.notify_all(); + state.data_consumed_cnd.notify_all(); + physics.join(); + } + +private: + static auto PhysicsThread(PhysicsState &state) -> void; + +public: + auto AddMassCmd() -> void; + + auto AddSpringCmd(std::size_t a, std::size_t b) -> void; + + auto ClearCmd() -> void; + + auto AddMassSpringsCmd( + std::size_t num_masses, + const std::vector> &springs) -> void; +}; + +// https://en.cppreference.com/w/cpp/utility/variant/visit +template struct overloads : Ts... { + using Ts::operator()...; }; #endif diff --git a/include/puzzle.hpp b/include/puzzle.hpp index a197487..ea854aa 100644 --- a/include/puzzle.hpp +++ b/include/puzzle.hpp @@ -7,7 +7,6 @@ #include #include #include -#include #include enum Direction { @@ -219,8 +218,9 @@ public: auto GetNextStates() const -> std::vector; - auto Closure() const -> std::pair, - std::vector>>; + auto Closure() const + -> std::pair, + std::vector>>; }; // Provide hash functions so we can use State and as hash-set diff --git a/include/renderer.hpp b/include/renderer.hpp index 2dfbcaa..64a67dc 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -3,15 +3,17 @@ #include #include -#include #include "camera.hpp" #include "config.hpp" -#include "physics.hpp" -#include "puzzle.hpp" +#include "input.hpp" +#include "state.hpp" class Renderer { private: + const StateManager &state; + const InputHandler &input; + const OrbitCamera3D &camera; RenderTexture render_target; RenderTexture klotski_target; @@ -25,13 +27,10 @@ private: Shader instancing_shader; public: - bool mark_solutions; - bool connect_solutions; - -public: - Renderer(const OrbitCamera3D &_camera) - : camera(_camera), transforms_size(0), transforms(nullptr), - mark_solutions(false), connect_solutions(false) { + Renderer(const OrbitCamera3D &_camera, const StateManager &_state, + const InputHandler &_input) + : state(_state), input(_input), camera(_camera), transforms_size(0), + transforms(nullptr) { render_target = LoadRenderTexture(GetScreenWidth() / 2.0, GetScreenHeight() - MENU_HEIGHT); klotski_target = LoadRenderTexture(GetScreenWidth() / 2.0, @@ -61,29 +60,24 @@ public: } private: - auto AllocateGraphInstancing(const MassSpringSystem &mass_springs) -> void; + auto AllocateGraphInstancing(std::size_t size) -> void; - auto - ReallocateGraphInstancingIfNecessary(const MassSpringSystem &mass_springs) - -> void; + auto ReallocateGraphInstancingIfNecessary(std::size_t size) -> void; public: auto UpdateTextureSizes() -> void; - auto DrawMassSprings(const MassSpringSystem &mass_springs, - const State ¤t_state, const State &starting_state, - const std::unordered_set &winning_states, - const std::unordered_set &visited_states) -> void; + auto DrawMassSprings( + const std::vector &masses, + const std::vector> &springs) -> void; - 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, - const WinCondition win_condition) -> void; + auto DrawKlotski() -> void; - auto DrawMenu(const MassSpringSystem &mass_springs, int current_preset, - const State ¤t_state, - const std::unordered_set &winning_states) -> void; + auto DrawMenu(const std::vector &masses, + const std::vector> &springs) + -> void; - auto DrawTextures() -> void; + auto DrawTextures(float ups) -> void; }; #endif diff --git a/include/state.hpp b/include/state.hpp index dfd3684..83ae6f3 100644 --- a/include/state.hpp +++ b/include/state.hpp @@ -1,16 +1,21 @@ #ifndef __STATE_HPP_ #define __STATE_HPP_ -#include "config.hpp" #include "physics.hpp" #include "presets.hpp" #include "puzzle.hpp" #include +#include +#include class StateManager { public: - MassSpringSystem &mass_springs; + ThreadedPhysics &physics; + + std::unordered_map states; + std::unordered_set winning_states; + std::unordered_set visited_states; int current_preset; State starting_state; @@ -19,16 +24,13 @@ public: bool edited = false; - std::unordered_set winning_states; - std::unordered_set visited_states; - public: - StateManager(MassSpringSystem &_mass_springs) - : mass_springs(_mass_springs), current_preset(0), + StateManager(ThreadedPhysics &_physics) + : physics(_physics), current_preset(0), starting_state(generators[current_preset]()), current_state(starting_state), previous_state(starting_state), edited(false) { - mass_springs.AddMass(MASS, false, current_state); + ClearGraph(); } StateManager(const StateManager ©) = delete; @@ -55,9 +57,11 @@ public: auto FindWinningStates() -> void; - auto CurrentGenerator() -> StateGenerator; + auto CurrentGenerator() const -> StateGenerator; - auto CurrentWinCondition() -> WinCondition; + auto CurrentWinCondition() const -> WinCondition; + + auto CurrentMassIndex() const -> std::size_t; }; #endif diff --git a/src/backward.cpp b/src/backward.cpp new file mode 100644 index 0000000..110441c --- /dev/null +++ b/src/backward.cpp @@ -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 diff --git a/src/input.cpp b/src/input.cpp index 78d1800..76fdd32 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -133,9 +133,9 @@ auto InputHandler::HandleKeys() -> void { } else if (IsKeyPressed(KEY_C)) { state.ClearGraph(); } else if (IsKeyPressed(KEY_I)) { - renderer.mark_solutions = !renderer.mark_solutions; + mark_solutions = !mark_solutions; } else if (IsKeyPressed(KEY_O)) { - renderer.connect_solutions = !renderer.connect_solutions; + connect_solutions = !connect_solutions; } else if (IsKeyPressed(KEY_F)) { state.current_state.ToggleRestricted(); state.ClearGraph(); diff --git a/src/main.cpp b/src/main.cpp index 9c4d200..c98f9d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,8 @@ +#include +#include +#include +#include + #include "config.hpp" #include "input.hpp" #include "physics.hpp" @@ -5,10 +10,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 +19,9 @@ // 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 +// TODO: Do I have a huge memory leak or is the memory just not reclaimed from +// the C++ runtime? auto main(int argc, char *argv[]) -> int { // if (argc < 2) { @@ -35,50 +38,61 @@ auto main(int argc, char *argv[]) -> int { InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings"); // Game setup + ThreadedPhysics physics; + StateManager state(physics); + InputHandler input(state); OrbitCamera3D camera; - Renderer renderer(camera); - MassSpringSystem mass_springs; - StateManager state(mass_springs); - InputHandler input(state, renderer); + Renderer renderer(camera, state, input); + + unsigned int ups; + std::vector masses; // Read from physics + std::vector> springs; // Read from physics // Game loop - double timestep_accumulator = 0.0; while (!WindowShouldClose()) { - timestep_accumulator += GetFrameTime(); + FrameMarkStart("MainThread"); // Input update state.previous_state = state.current_state; input.HandleInput(); 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); + // Read positions from physics thread + FrameMarkStart("MainThreadConsumeLock"); + { + std::unique_lock lock(physics.state.data_mtx); - timestep_accumulator -= TIMESTEP; + ups = physics.state.ups; + + // Only copy data if any has been produced + if (physics.state.data_ready) { + masses = physics.state.masses; + springs = physics.state.springs; + + physics.state.data_ready = false; + physics.state.data_consumed = true; + + lock.unlock(); + // Notify the physics thread that data has been consumed + physics.state.data_consumed_cnd.notify_all(); + } } + FrameMarkEnd("MainThreadConsumeLock"); // Update the camera after the physics, so target lock is smooth - camera.Update(mass_springs.GetMass(state.current_state).position); + std::size_t current_index = state.CurrentMassIndex(); + if (masses.size() > current_index) { + const Mass ¤t_mass = masses.at(current_index); + camera.Update(current_mass.position); + } // Rendering renderer.UpdateTextureSizes(); - renderer.DrawMassSprings(mass_springs, state.current_state, - state.starting_state, 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(mass_springs, state.current_preset, state.current_state, - state.winning_states); - renderer.DrawTextures(); + renderer.DrawMassSprings(masses, springs); + renderer.DrawKlotski(); + renderer.DrawMenu(masses, springs); + renderer.DrawTextures(ups); + FrameMarkEnd("MainThread"); } CloseWindow(); diff --git a/src/physics.cpp b/src/physics.cpp index 90f3a7b..fae51fd 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -2,20 +2,19 @@ #include "config.hpp" #include "tracy.hpp" +#include #include #include +#include #include +#include +#include #include #include #include -#include #include #include -#ifndef BARNES_HUT -#include -#endif - auto Mass::ClearForce() -> void { force = Vector3Zero(); } auto Mass::CalculateVelocity(const float delta_time) -> void { @@ -69,58 +68,29 @@ 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 { - if (!state_masses.contains(state)) { - masses.emplace_back(Vector3Zero()); - std::size_t idx = masses.size() - 1; - state_masses.insert(std::make_pair(state, idx)); +auto MassSpringSystem::AddMass() -> void { masses.emplace_back(Vector3Zero()); } + +auto MassSpringSystem::AddSpring(int a, int b) -> void { + Mass &mass_a = masses.at(a); + Mass &mass_b = masses.at(b); + + Vector3 position = mass_a.position; + Vector3 offset = Vector3(static_cast(GetRandomValue(-100, 100)), + static_cast(GetRandomValue(-100, 100)), + static_cast(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 & { - 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, - float spring_constant, - float dampening_constant, float rest_length) - -> void { - std::pair 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(GetRandomValue(-100, 100)), - static_cast(GetRandomValue(-100, 100)), - static_cast(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)); - } + springs.emplace_back(a, b); } auto MassSpringSystem::Clear() -> void { masses.clear(); - state_masses.clear(); springs.clear(); - state_springs.clear(); -#ifndef BARNES_HUT - InvalidateGrid(); -#endif + octree.nodes.clear(); } auto MassSpringSystem::ClearForces() -> void { @@ -135,13 +105,16 @@ auto MassSpringSystem::CalculateSpringForces() -> void { ZoneScoped; for (const auto spring : springs) { - Mass &a = masses.at(spring.mass_a); - Mass &b = masses.at(spring.mass_b); + Mass &a = masses.at(spring.a); + Mass &b = masses.at(spring.b); spring.CalculateSpringForce(a, b); } } -#ifdef BARNES_HUT +auto MassSpringSystem::SetThreadName(std::size_t idx) -> void { + BS::this_thread::set_os_thread_name(std::format("bh-worker-{}", idx)); +} + auto MassSpringSystem::BuildOctree() -> void { ZoneScoped; @@ -177,52 +150,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 { ZoneScoped; -#ifdef BARNES_HUT BuildOctree(); auto solve_octree = [&](int i) { @@ -240,86 +170,6 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void { threads.submit_loop(0, masses.size(), solve_octree, 256); loop_future.wait(); #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 loop_future = - threads.submit_loop(0, mass_pointers.size(), solve_grid, 512); - loop_future.wait(); -#endif - -#endif } auto MassSpringSystem::VerletUpdate(float delta_time) -> void { @@ -330,13 +180,131 @@ 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; +auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state) + -> void { + BS::this_thread::set_os_thread_name("physics"); + + MassSpringSystem mass_springs; + + const auto visitor = overloads{ + [&](const struct AddMass &am) { mass_springs.AddMass(); }, + [&](const struct AddSpring &as) { mass_springs.AddSpring(as.a, as.b); }, + [&](const struct ClearGraph &cg) { mass_springs.Clear(); }, + }; + + std::chrono::time_point last = std::chrono::high_resolution_clock::now(); + std::chrono::duration accumulator(0); + std::chrono::duration update_accumulator(0); + unsigned int updates = 0; + + while (state.running.load()) { + FrameMarkStart("PhysicsThread"); + + // Time tracking + std::chrono::time_point now = std::chrono::high_resolution_clock::now(); + std::chrono::duration deltatime = now - last; + accumulator += deltatime; + update_accumulator += deltatime; + last = now; + + // 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 + if (accumulator.count() > TIMESTEP) { + mass_springs.ClearForces(); + mass_springs.CalculateSpringForces(); + mass_springs.CalculateRepulsionForces(); + mass_springs.VerletUpdate(TIMESTEP * SIM_SPEED); + + ++updates; + accumulator -= std::chrono::duration(TIMESTEP); + } + + // Publish the positions for the renderer (copy) + FrameMarkStart("PhysicsThreadProduceLock"); + { + std::unique_lock lock(state.data_mtx); + 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; + } + + if (update_accumulator.count() > 1.0) { + // Update each second + state.ups = updates; + updates = 0; + update_accumulator = std::chrono::duration(0); + } + + state.masses.clear(); + state.masses.reserve(mass_springs.masses.size()); + for (const auto &mass : mass_springs.masses) { + 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_consumed = false; + } + // Notify the rendering thread that new data is available + state.data_ready_cnd.notify_all(); + FrameMarkEnd("PhysicsThreadProduceLock"); + + FrameMarkEnd("PhysicsThread"); + } +} + +auto ThreadedPhysics::AddMassCmd() -> void { + { + std::lock_guard lock(state.command_mtx); + state.pending_commands.push(AddMass{}); + } +} + +auto ThreadedPhysics::AddSpringCmd(std::size_t a, std::size_t b) -> void { + { + std::lock_guard lock(state.command_mtx); + state.pending_commands.push(AddSpring{a, b}); + } +} + +auto ThreadedPhysics::ClearCmd() -> void { + { + std::lock_guard lock(state.command_mtx); + state.pending_commands.push(ClearGraph{}); + } +} + +auto ThreadedPhysics::AddMassSpringsCmd( + std::size_t num_masses, + const std::vector> &springs) -> void { + { + std::lock_guard lock(state.command_mtx); + for (std::size_t i = 0; i < num_masses; ++i) { + state.pending_commands.push(AddMass{}); + } + for (const auto &[from, to] : springs) { + state.pending_commands.push(AddSpring{from, to}); + } + } } -#endif diff --git a/src/puzzle.cpp b/src/puzzle.cpp index d8719a0..84b3be1 100644 --- a/src/puzzle.cpp +++ b/src/puzzle.cpp @@ -1,6 +1,8 @@ #include "puzzle.hpp" #include "tracy.hpp" +#include + auto Block::Hash() const -> int { std::string s = std::format("{},{},{},{}", x, y, width, height); return std::hash{}(s); @@ -262,11 +264,16 @@ auto State::GetNextStates() const -> std::vector { return new_states; } -auto State::Closure() const -> std::pair, - std::vector>> { - std::unordered_set states; - std::vector> links; +auto State::Closure() const + -> std::pair, + std::vector>> { + std::vector states; + std::vector> links; + // Helper to construct the links vector + std::unordered_map state_indices; + + // Buffer for all states we want to call GetNextStates() on std::unordered_set remaining_states; remaining_states.insert(*this); @@ -274,13 +281,18 @@ auto State::Closure() const -> std::pair, const State current = *remaining_states.begin(); remaining_states.erase(current); - std::vector new_states = current.GetNextStates(); - for (const State &s : new_states) { - if (!states.contains(s)) { + if (!state_indices.contains(current)) { + state_indices.emplace(current, states.size()); + states.push_back(current); + } + + for (const State &s : current.GetNextStates()) { + if (!state_indices.contains(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); diff --git a/src/renderer.cpp b/src/renderer.cpp index b21155f..b66b1d0 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -1,6 +1,5 @@ #include "renderer.hpp" #include "config.hpp" -#include "physics.hpp" #include "puzzle.hpp" #include "tracy.hpp" @@ -10,7 +9,6 @@ #include #include #include -#include #ifdef BATCHING #include @@ -33,8 +31,7 @@ auto Renderer::UpdateTextureSizes() -> void { menu_target = LoadRenderTexture(width * 2, MENU_HEIGHT); } -auto Renderer::AllocateGraphInstancing(const MassSpringSystem &mass_springs) - -> void { +auto Renderer::AllocateGraphInstancing(std::size_t size) -> void { cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE); instancing_shader = LoadShader("shader/instancing_vertex.glsl", @@ -48,40 +45,34 @@ auto Renderer::AllocateGraphInstancing(const MassSpringSystem &mass_springs) vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR; vertex_mat.shader = instancing_shader; - transforms = (Matrix *)MemAlloc(mass_springs.masses.size() * sizeof(Matrix)); - transforms_size = mass_springs.masses.size(); + transforms = (Matrix *)MemAlloc(size * sizeof(Matrix)); + transforms_size = size; } -auto Renderer::ReallocateGraphInstancingIfNecessary( - const MassSpringSystem &mass_springs) -> void { - if (transforms_size != mass_springs.masses.size()) { - transforms = (Matrix *)MemRealloc(transforms, mass_springs.masses.size() * - sizeof(Matrix)); - transforms_size = mass_springs.masses.size(); +auto Renderer::ReallocateGraphInstancingIfNecessary(std::size_t size) -> void { + if (transforms_size != size) { + transforms = (Matrix *)MemRealloc(transforms, size * sizeof(Matrix)); + transforms_size = size; } } -auto Renderer::DrawMassSprings(const MassSpringSystem &mass_springs, - const State ¤t_state, - const State &starting_state, - const std::unordered_set &winning_states, - const std::unordered_set &visited_states) - -> void { +auto Renderer::DrawMassSprings( + const std::vector &masses, + const std::vector> &springs) -> void { ZoneScoped; // Prepare cube instancing { ZoneNamedN(prepare_masses, "PrepareMasses", true); - if (mass_springs.masses.size() < DRAW_VERTICES_LIMIT) { + if (masses.size() < DRAW_VERTICES_LIMIT) { if (transforms == nullptr) { - AllocateGraphInstancing(mass_springs); + AllocateGraphInstancing(masses.size()); } - ReallocateGraphInstancingIfNecessary(mass_springs); + ReallocateGraphInstancingIfNecessary(masses.size()); int i = 0; - for (const auto &mass : mass_springs.masses) { - transforms[i] = - MatrixTranslate(mass.position.x, mass.position.y, mass.position.z); + for (const Vector3 &mass : masses) { + transforms[i] = MatrixTranslate(mass.x, mass.y, mass.z); ++i; } } @@ -96,13 +87,14 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &mass_springs, { ZoneNamedN(draw_springs, "DrawSprings", true); rlBegin(RL_LINES); - for (const auto &spring : mass_springs.springs) { - // 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 &b = mass_springs.masses.at(spring.mass_b); - rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a); - rlVertex3f(a.position.x, a.position.y, a.position.z); - rlVertex3f(b.position.x, b.position.y, b.position.z); + for (const auto &[from, to] : springs) { + if (masses.size() > from && masses.size() > to) { + const Vector3 &a = masses.at(from); + const Vector3 &b = masses.at(to); + rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a); + rlVertex3f(a.x, a.y, a.z); + rlVertex3f(b.x, b.y, b.z); + } } rlEnd(); } @@ -110,49 +102,63 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &mass_springs, // Draw masses (instanced) { 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 // much faster... The amount of data sent to the GPU would be // reduced (just positions instead of matrices), but is this // noticable for < 100000 cubes? - DrawMeshInstanced(cube_instance, vertex_mat, transforms, - mass_springs.masses.size()); + DrawMeshInstanced(cube_instance, vertex_mat, transforms, masses.size()); } } // Mark winning states - if (mark_solutions || connect_solutions) { - for (const auto &state : winning_states) { - const Mass &winning_mass = mass_springs.GetMass(state); - if (mark_solutions) { - DrawCube(winning_mass.position, 2 * VERTEX_SIZE, 2 * VERTEX_SIZE, - 2 * VERTEX_SIZE, BLUE); - } + if (input.mark_solutions || input.connect_solutions) { + for (const State &_state : state.winning_states) { - if (connect_solutions) { - DrawLine3D(winning_mass.position, - mass_springs.GetMass(current_state).position, PURPLE); + std::size_t winning_index = state.states.at(_state); + if (masses.size() > winning_index) { + + 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 ¤t_mass = masses.at(current_index); + DrawLine3D(winning_mass, current_mass, PURPLE); + } } } } // Mark visited states - for (const auto &state : visited_states) { - const Mass &visited_mass = mass_springs.GetMass(state); + for (const State &_state : state.visited_states) { + std::size_t visited_index = state.states.at(_state); - DrawCube(visited_mass.position, VERTEX_SIZE * 1.5, VERTEX_SIZE * 1.5, - VERTEX_SIZE * 1.5, PURPLE); + if (masses.size() > visited_index) { + 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 - const Mass &starting_mass = mass_springs.GetMass(starting_state); - DrawCube(starting_mass.position, VERTEX_SIZE * 2, VERTEX_SIZE * 2, - VERTEX_SIZE * 2, ORANGE); + std::size_t starting_index = state.states.at(state.starting_state); + if (masses.size() > starting_index) { + const Vector3 &starting_mass = masses.at(starting_index); + DrawCube(starting_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2, + ORANGE); + } // Mark current state - const Mass ¤t_mass = mass_springs.GetMass(current_state); - DrawCube(current_mass.position, VERTEX_SIZE * 2, VERTEX_SIZE * 2, - VERTEX_SIZE * 2, RED); + std::size_t current_index = state.states.at(state.current_state); + if (masses.size() > current_index) { + const Vector3 ¤t_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, // REPULSION_RANGE, BLACK); @@ -164,9 +170,7 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &mass_springs, EndTextureMode(); } -auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, - int sel_y, int block_add_x, int block_add_y, - const WinCondition win_condition) -> void { +auto Renderer::DrawKlotski() -> void { ZoneScoped; BeginTextureMode(klotski_target); @@ -175,22 +179,26 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, // Draw Board const int board_width = GetScreenWidth() / 2 - 2 * BOARD_PADDING; const int board_height = GetScreenHeight() - MENU_HEIGHT - 2 * BOARD_PADDING; - int block_size = - std::min(board_width / state.width, board_height / state.height) - - 2 * BLOCK_PADDING; - int x_offset = - (board_width - (block_size + 2 * BLOCK_PADDING) * state.width) / 2.0; - int y_offset = - (board_height - (block_size + 2 * BLOCK_PADDING) * state.height) / 2.0; + int block_size = std::min(board_width / state.current_state.width, + board_height / state.current_state.height) - + 2 * BLOCK_PADDING; + int x_offset = (board_width - (block_size + 2 * BLOCK_PADDING) * + state.current_state.width) / + 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, RAYWHITE); - DrawRectangle( - x_offset, y_offset, board_width - 2 * x_offset + 2 * BOARD_PADDING, - board_height - 2 * y_offset + 2 * BOARD_PADDING, - win_condition(state) ? GREEN : (state.restricted ? DARKGRAY : LIGHTGRAY)); - for (int x = 0; x < state.width; ++x) { - for (int y = 0; y < state.height; ++y) { + DrawRectangle(x_offset, y_offset, + board_width - 2 * x_offset + 2 * BOARD_PADDING, + board_height - 2 * y_offset + 2 * BOARD_PADDING, + state.CurrentWinCondition()(state.current_state) + ? GREEN + : (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 + BLOCK_PADDING + x * block_size, y_offset + BOARD_PADDING + y * BLOCK_PADDING * 2 + @@ -200,13 +208,13 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, } // Draw Blocks - for (Block block : state) { + for (Block block : state.current_state) { Color c = BLOCK_COLOR; - if (block.Covers(sel_x, sel_y)) { + if (block.Covers(input.sel_x, input.sel_y)) { c = HL_BLOCK_COLOR; } if (block.target) { - if (block.Covers(sel_x, sel_y)) { + if (block.Covers(input.sel_x, input.sel_y)) { c = HL_TARGET_BLOCK_COLOR; } else { c = TARGET_BLOCK_COLOR; @@ -222,7 +230,7 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, 2 * BLOCK_PADDING, c); - if (block.Covers(hov_x, hov_y)) { + if (block.Covers(input.hov_x, input.hov_y)) { DrawRectangleLinesEx( Rectangle(x_offset + BOARD_PADDING + block.x * BLOCK_PADDING * 2 + BLOCK_PADDING + block.x * block_size, @@ -237,12 +245,13 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, } // Draw editing starting position - if (block_add_x >= 0 && block_add_y >= 0) { - DrawCircle(x_offset + BOARD_PADDING + block_add_x * BLOCK_PADDING * 2 + - BLOCK_PADDING + block_add_x * block_size + block_size / 2, - y_offset + BOARD_PADDING + block_add_y * BLOCK_PADDING * 2 + - BLOCK_PADDING + block_add_y * block_size + block_size / 2, - block_size / 10.0, Fade(BLACK, 0.5)); + if (input.block_add_x >= 0 && input.block_add_y >= 0) { + DrawCircle( + x_offset + BOARD_PADDING + input.block_add_x * BLOCK_PADDING * 2 + + BLOCK_PADDING + input.block_add_x * block_size + block_size / 2, + y_offset + BOARD_PADDING + input.block_add_y * BLOCK_PADDING * 2 + + 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, @@ -250,10 +259,9 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, EndTextureMode(); } -auto Renderer::DrawMenu(const MassSpringSystem &mass_springs, - int current_preset, const State ¤t_state, - const std::unordered_set &winning_states) - -> void { +auto Renderer::DrawMenu( + const std::vector &masses, + const std::vector> &springs) -> void { ZoneScoped; BeginTextureMode(menu_target); @@ -277,8 +285,8 @@ auto Renderer::DrawMenu(const MassSpringSystem &mass_springs, draw_btn(0, 0, std::format("States: {}, Transitions: {}, Winning: {}", - mass_springs.masses.size(), mass_springs.springs.size(), - winning_states.size()), + masses.size(), springs.size(), + state.winning_states.size()), DARKGREEN); draw_btn( 0, 1, @@ -294,21 +302,21 @@ auto Renderer::DrawMenu(const MassSpringSystem &mass_springs, draw_btn(1, 2, std::format("Print Board State to Console (P)"), DARKBLUE); draw_btn(2, 0, - std::format("Preset (M/N): {}, {} (F)", current_preset, - current_state.restricted ? "Restricted" : "Free"), + std::format("Preset (M/N): {}, {} (F)", state.current_preset, + state.current_state.restricted ? "Restricted" : "Free"), DARKPURPLE); draw_btn(2, 1, std::format("Populate Graph (G), Clear Graph (C)"), DARKPURPLE); draw_btn(2, 2, - std::format("Mark (I): {} / Connect (O): {}", mark_solutions, - connect_solutions), + std::format("Mark (I): {} / Connect (O): {}", input.mark_solutions, + input.connect_solutions), DARKPURPLE); DrawLine(0, MENU_HEIGHT - 1, GetScreenWidth(), MENU_HEIGHT - 1, BLACK); EndTextureMode(); } -auto Renderer::DrawTextures() -> void { +auto Renderer::DrawTextures(float ups) -> void { BeginDrawing(); DrawTextureRec(menu_target.texture, Rectangle(0, 0, menu_target.texture.width, @@ -322,7 +330,9 @@ auto Renderer::DrawTextures() -> void { Rectangle(0, 0, render_target.texture.width, -1 * render_target.texture.height), Vector2(GetScreenWidth() / 2.0, MENU_HEIGHT), WHITE); + DrawFPS(GetScreenWidth() / 2 + 10, MENU_HEIGHT + 10); + DrawText(TextFormat("%.0f UPS", ups), GetScreenWidth() / 2 + 120, + MENU_HEIGHT + 10, 20, ORANGE); EndDrawing(); - FrameMark; } diff --git a/src/state.cpp b/src/state.cpp index 4ca7007..d1e12ec 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -1,20 +1,18 @@ #include "state.hpp" -#include "config.hpp" #include "presets.hpp" #include "tracy.hpp" #include auto StateManager::LoadPreset(int preset) -> void { - current_state = generators[preset](); - previous_state = current_state; - ClearGraph(); current_preset = preset; + current_state = CurrentGenerator()(); + ClearGraph(); edited = false; } auto StateManager::ResetState() -> void { - current_state = generators[current_preset](); + current_state = CurrentGenerator()(); previous_state = current_state; if (edited) { // We also need to clear the graph in case the state has been edited @@ -35,69 +33,67 @@ auto StateManager::NextPreset() -> void { auto StateManager::FillGraph() -> void { ClearGraph(); - std::pair, std::vector>> + std::pair, + std::vector>> closure = current_state.Closure(); - for (const auto &state : closure.first) { - mass_springs.AddMass(MASS, false, state); + + physics.ClearCmd(); + physics.AddMassSpringsCmd(closure.first.size(), closure.second); + for (const State &state : closure.first) { + states.insert(std::make_pair(state, states.size())); } - for (const auto &[from, to] : closure.second) { - mass_springs.AddSpring(from, to, SPRING_CONSTANT, DAMPENING_CONSTANT, - REST_LENGTH); - } - std::cout << "Inserted " << mass_springs.masses.size() << " masses and " - << mass_springs.springs.size() << " springs." << std::endl; 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 { - if (previous_state != current_state) { - mass_springs.AddMass(MASS, false, current_state); - mass_springs.AddSpring(current_state, previous_state, SPRING_CONSTANT, - DAMPENING_CONSTANT, REST_LENGTH); - if (win_conditions[current_preset](current_state)) { - winning_states.insert(current_state); - } - visited_states.insert(current_state); + if (previous_state == current_state) { + return; + } + + if (!states.contains(current_state)) { + states.insert(std::make_pair(current_state, states.size())); + physics.AddMassCmd(); + physics.AddSpringCmd(states.at(current_state), states.at(previous_state)); + } + + visited_states.insert(current_state); + if (win_conditions[current_preset](current_state)) { + winning_states.insert(current_state); } } auto StateManager::ClearGraph() -> void { + states.clear(); winning_states.clear(); visited_states.clear(); - mass_springs.Clear(); - mass_springs.AddMass(MASS, false, current_state); + physics.ClearCmd(); - // The previous_state is no longer in the graph + 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; - - // The starting state is no longer in the graph starting_state = current_state; } auto StateManager::FindWinningStates() -> void { winning_states.clear(); - for (const auto &[state, mass] : mass_springs.state_masses) { - if (win_conditions[current_preset](state)) { + for (const auto &[state, mass] : states) { + if (CurrentWinCondition()(state)) { winning_states.insert(state); } } - - std::cout << "Found " << winning_states.size() << " winning states." - << std::endl; } -auto StateManager::CurrentGenerator() -> StateGenerator { +auto StateManager::CurrentGenerator() const -> StateGenerator { return generators[current_preset]; } -auto StateManager::CurrentWinCondition() -> WinCondition { +auto StateManager::CurrentWinCondition() const -> WinCondition { return win_conditions[current_preset]; } + +auto StateManager::CurrentMassIndex() const -> std::size_t { + return states.at(current_state); +}