2 Commits

Author SHA1 Message Date
592c4b6cc0 massive state space solver improvement: supercomp took ~10s, now ~40ms
achieved by imposing a 15 block limit on each board and changing the
internal representation from std::string to 4x uint64_t
2026-03-02 05:26:18 +01:00
c7361fe47e fix board goal rendering bug if no target block exists or board has no win condition 2026-03-01 00:30:06 +01:00
41 changed files with 1928 additions and 5271 deletions

5
.gitignore vendored
View File

@ -6,8 +6,3 @@ cmake-build-release
/.gdb_history /.gdb_history
/valgrind.log /valgrind.log
.idea .idea
/perf.data
/perf.data.old
/clusters.puzzle
/benchs.json
/benchs.old.json

View File

@ -1,67 +1,54 @@
cmake_minimum_required(VERSION 3.28) cmake_minimum_required(VERSION 3.25)
project(MassSprings) project(MassSprings)
set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD 26)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Disable boost warning because our cmake/boost are recent enough
if(POLICY CMP0167) if(POLICY CMP0167)
cmake_policy(SET CMP0167 NEW) cmake_policy(SET CMP0167 NEW)
endif() endif()
option(DISABLE_THREADPOOL "Disable additional physics threads" OFF)
option(DISABLE_BACKWARD "Disable backward stacktrace printer" OFF) option(DISABLE_BACKWARD "Disable backward stacktrace printer" OFF)
option(DISABLE_TRACY "Disable the Tracy profiler client" OFF) option(DISABLE_TRACY "Disable the Tracy profiler client" ON)
option(DISABLE_TESTS "Disable building tests" OFF) option(DISABLE_TESTS "Disable building and running tests" ON)
option(DISABLE_BENCH "Disable building benchmarks" OFF)
# Headers + Sources (excluding main.cpp) # Headers + Sources
set(SOURCES set(SOURCES
src/backward.cpp src/backward.cpp
src/bits.cpp
src/cpu_layout_engine.cpp
src/cpu_spring_system.cpp
src/graph_distances.cpp src/graph_distances.cpp
src/input_handler.cpp src/input_handler.cpp
src/load_save.cpp src/mass_spring_system.cpp
src/octree.cpp src/octree.cpp
src/orbit_camera.cpp src/orbit_camera.cpp
src/puzzle.cpp
src/renderer.cpp src/renderer.cpp
src/state_manager.cpp src/state_manager.cpp
src/threaded_physics.cpp
src/user_interface.cpp src/user_interface.cpp
src/puzzle.cpp
) )
# Libraries # Libraries
include(FetchContent)
find_package(raylib REQUIRED) find_package(raylib REQUIRED)
find_package(GLEW REQUIRED) find_package(Boost REQUIRED)
find_package(libmorton REQUIRED) set(LIBS raylib Boost::headers)
find_package(Boost COMPONENTS program_options REQUIRED)
set(LIBS raylib GLEW::GLEW Boost::headers Boost::program_options)
set(FLAGS "") set(FLAGS "")
if(WIN32) if(WIN32)
list(APPEND LIBS opengl32 gdi32 winmm) list(APPEND LIBS opengl32 gdi32 winmm)
endif() endif()
if(NOT DISABLE_THREADPOOL) include(FetchContent)
list(APPEND FLAGS THREADPOOL)
endif()
if(NOT DISABLE_BACKWARD) if(NOT DISABLE_BACKWARD)
find_package(Backward REQUIRED) find_package(Backward REQUIRED)
list(APPEND LIBS Backward::Backward) list(APPEND LIBS Backward::Backward)
list(APPEND FLAGS BACKWARD) list(APPEND FLAGS BACKWARD)
message("-- BACKWARD: Enabled")
endif() endif()
if(NOT DISABLE_TRACY) if(NOT DISABLE_TRACY)
FetchContent_Declare(tracy FetchContent_Declare(tracy
GIT_REPOSITORY https://github.com/wolfpld/tracy.git GIT_REPOSITORY https://github.com/wolfpld/tracy.git
GIT_TAG v0.13.1 GIT_TAG v0.11.1
GIT_SHALLOW TRUE GIT_SHALLOW TRUE
GIT_PROGRESS TRUE GIT_PROGRESS TRUE
) )
@ -71,15 +58,12 @@ if(NOT DISABLE_TRACY)
list(APPEND LIBS TracyClient) list(APPEND LIBS TracyClient)
list(APPEND FLAGS TRACY) list(APPEND FLAGS TRACY)
message("-- TRACY: Enabled")
endif() endif()
# Set this after fetching tracy to hide tracy's warnings. # Set this after fetching tracy to hide tracy's warnings
# We set -Wno-alloc-size-larger-than because it prevents BS::thread_pool from building with current gcc set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wfloat-equal -Wundef -Wshadow -Wpointer-arith -Wcast-align -Wno-unused-parameter -Wunreachable-code")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wfloat-equal -Wundef -Wshadow -Wpointer-arith -Wcast-align -Wno-unused-parameter -Wunreachable-code -Wno-alloc-size-larger-than")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb -O0") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb -O0")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -ggdb -O3 -ffast-math -march=native") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -ggdb -Ofast -march=native")
message("-- CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}") message("-- CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}")
message("-- CMAKE_C_FLAGS_DEBUG: ${CMAKE_C_FLAGS_DEBUG}") message("-- CMAKE_C_FLAGS_DEBUG: ${CMAKE_C_FLAGS_DEBUG}")
@ -94,54 +78,36 @@ target_include_directories(masssprings PRIVATE include)
target_link_libraries(masssprings PRIVATE ${LIBS}) target_link_libraries(masssprings PRIVATE ${LIBS})
target_compile_definitions(masssprings PRIVATE ${FLAGS}) target_compile_definitions(masssprings PRIVATE ${FLAGS})
# Testing # Testing sources
if(NOT DISABLE_TESTS AND NOT WIN32) if(NOT DISABLE_TESTS AND NOT WIN32)
enable_testing() enable_testing()
FetchContent_Declare(Catch2 FetchContent_Declare(Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.13.0 GIT_TAG v3.13.0
) )
FetchContent_MakeAvailable(Catch2) FetchContent_MakeAvailable(Catch2)
set(TEST_SOURCES set(TEST_SOURCES
test/bits.cpp test/bits.cpp
test/bitmap.cpp
test/bitmap_find_first_empty.cpp
# test/puzzle.cpp
) )
add_executable(tests ${TEST_SOURCES} ${SOURCES}) add_executable(tests ${TEST_SOURCES} ${SOURCES})
target_include_directories(tests PRIVATE include) target_include_directories(tests PRIVATE include)
target_link_libraries(tests Catch2::Catch2WithMain raylib GLEW::GLEW) target_link_libraries(tests Catch2::Catch2WithMain raylib)
include(Catch) include(Catch)
catch_discover_tests(tests) catch_discover_tests(tests)
message("-- TESTS: Enabled")
endif()
# Benchmarking
if(NOT DISABLE_BENCH AND NOT WIN32)
find_package(benchmark REQUIRED)
set(BENCH_SOURCES
benchmark/state_space.cpp
)
add_executable(benchmarks ${BENCH_SOURCES} ${SOURCES})
target_include_directories(benchmarks PRIVATE include)
target_link_libraries(benchmarks benchmark raylib GLEW::GLEW)
message("-- BENCHMARKS: Enabled")
endif() endif()
# LTO # LTO
#if(NOT WIN32)
include(CheckIPOSupported) include(CheckIPOSupported)
check_ipo_supported(RESULT supported OUTPUT error) check_ipo_supported(RESULT supported OUTPUT error)
if(supported) if(supported)
message("-- IPO/LTO: Enabled") message(STATUS "IPO / LTO enabled")
set_property(TARGET masssprings PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) set_property(TARGET masssprings PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
else() else()
message("-- IPO/LTO: Disabled") message(STATUS "IPO / LTO not supported")
endif() endif()
#endif()

View File

@ -1,212 +0,0 @@
// ReSharper disable CppTooWideScope
// ReSharper disable CppDFAUnreadVariable
#include "puzzle.hpp"
#include <random>
#include <unordered_set>
#include <benchmark/benchmark.h>
#include <boost/unordered/unordered_flat_map.hpp>
static std::vector<std::string> puzzles = {
// 0: RushHour 1
"S:[6x6] G:[4,2] M:[R] B:[{3x1 _ _ _ _ 1x3} {_ _ _ _ _ _} {_ _ 1x2 2X1 _ _} {_ _ _ 1x2 2x1 _} {1x2 _ 1x2 _ 2x1 _} {_ _ _ 3x1 _ _}]",
// 1: RushHour 2
"S:[6x6] G:[4,2] M:[R] B:[{1x2 3x1 _ _ 1x2 1x3} {_ 3x1 _ _ _ _} {2X1 _ 1x2 1x2 1x2 _} {2x1 _ _ _ _ _} {_ _ _ 1x2 2x1 _} {_ _ _ _ 2x1 _}]",
// 2: RushHour 3
"S:[6x6] G:[4,2] M:[R] B:[{3x1 _ _ 1x2 _ _} {1x2 2x1 _ _ _ 1x2} {_ 2X1 _ 1x2 1x2 _} {2x1 _ 1x2 _ _ 1x2} {_ _ _ 2x1 _ _} {_ 2x1 _ 2x1 _ _}]",
// 3: RushHour 4
"S:[6x6] G:[4,2] M:[R] B:[{1x3 2x1 _ _ 1x2 _} {_ 1x2 1x2 _ _ 1x3} {_ _ _ 2X1 _ _} {3x1 _ _ 1x2 _ _} {_ _ 1x2 _ 2x1 _} {2x1 _ _ 2x1 _ _}]",
// 4: RushHour + Walls 1
"S:[6x6] G:[4,2] M:[R] B:[{1x2 2x1 _ 1*1 _ _} {_ _ _ 1x2 2x1 _} {1x2 2X1 _ _ _ _} {_ _ 1x2 2x1 _ 1x3} {2x1 _ _ _ _ _} {2x1 _ 3x1 _ _ _}]",
// 5: RushHour + Walls 2
"S:[6x6] G:[4,2] M:[R] B:[{2x1 _ _ 1x2 1x2 1*1} {3x1 _ _ _ _ _} {1x2 2X1 _ 1x2 _ _} {_ _ 1x2 _ 2x1 _} {_ _ _ 2x1 _ 1x2} {_ _ 2x1 _ 1*1 _}]",
// 6: Dad's Puzzler
"S:[4x5] G:[0,3] M:[F] B:[{2X2 _ 2x1 _} {_ _ 2x1 _} {1x1 1x1 _ _} {1x2 1x2 2x1 _} {_ _ 2x1 _}]",
// 7: Nine Blocks
"S:[4x5] G:[0,3] M:[F] B:[{1x2 1x2 _ _} {_ _ 2x1 _} {1x2 1x2 2x1 _} {_ _ 2X2 _} {1x1 1x1 _ _}]",
// 8: Quzzle
"S:[4x5] G:[2,0] M:[F] B:[{2X2 _ 2x1 _} {_ _ 1x2 1x2} {_ _ _ _} {1x2 2x1 _ 1x1} {_ 2x1 _ 1x1}]",
// 9: Thin Klotski
"S:[4x5] G:[1,4] M:[F] B:[{1x2 _ 2X1 _} {_ 2x2 _ 1x1} {_ _ _ 1x1} {2x2 _ 1x1 1x1} {_ _ 1x1 1x1}]",
// 10: Fat Klotski
"S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ 1x1} {1x1 _ _ 1x2} {1x1 2x2 _ _} {1x1 _ _ _} {1x1 1x1 2x1 _}]",
// 11: Klotski
"S:[4x5] G:[1,3] M:[F] B:[{1x2 2X2 _ 1x2} {_ _ _ _} {1x2 2x1 _ 1x2} {_ 1x1 1x1 _} {1x1 _ _ 1x1}]",
// 12: Century
"S:[4x5] G:[1,3] M:[F] B:[{1x1 2X2 _ 1x1} {1x2 _ _ 1x2} {_ 1x2 _ _} {1x1 _ _ 1x1} {2x1 _ 2x1 _}]",
// 13: Super Century
"S:[4x5] G:[1,3] M:[F] B:[{1x2 1x1 1x1 1x1} {_ 1x2 2X2 _} {1x2 _ _ _} {_ 2x1 _ 1x1} {_ 2x1 _ _}]",
// 14: Supercompo
"S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}]",
};
template <u8 N>
struct uint_hasher
{
int64_t nums;
auto operator()(const std::array<u64, N>& ints) const noexcept -> size_t
{
size_t h = 0;
for (size_t i = 0; i < N; ++i) {
puzzle::hash_combine(h, ints[i]);
}
return h;
}
};
template <u8 N>
static auto unordered_set_uint64(benchmark::State& state) -> void
{
std::random_device random_device;
std::mt19937 generator(random_device());
std::uniform_int_distribution<u64> distribution(
std::numeric_limits<u64>::min(),
std::numeric_limits<u64>::max()
);
std::unordered_set<std::array<u64, N>, uint_hasher<N>> set;
std::array<u64, N> ints;
for (size_t i = 0; i < N; ++i) {
ints[i] = distribution(generator);
}
for (auto _ : state) {
for (size_t i = 0; i < 100000; ++i) {
set.emplace(ints);
}
benchmark::DoNotOptimize(set);
}
}
template <u8 N>
static auto unordered_flat_set_uint64(benchmark::State& state) -> void
{
std::random_device random_device;
std::mt19937 generator(random_device());
std::uniform_int_distribution<u64> distribution(
std::numeric_limits<u64>::min(),
std::numeric_limits<u64>::max()
);
boost::unordered_flat_set<std::array<u64, N>, uint_hasher<N>> set;
std::array<u64, N> ints;
for (size_t i = 0; i < N; ++i) {
ints[i] = distribution(generator);
}
for (auto _ : state) {
for (size_t i = 0; i < 100000; ++i) {
set.emplace(ints);
}
benchmark::DoNotOptimize(set);
}
}
static auto unordered_flat_set_block_hasher(benchmark::State& state) -> void
{
blockset set;
const block b = block(2, 3, 1, 2, true, false);
for (auto _ : state) {
for (size_t i = 0; i < 100000; ++i) {
set.emplace(b);
}
benchmark::DoNotOptimize(set);
}
}
static auto unordered_flat_set_block_hasher2(benchmark::State& state) -> void
{
blockset2 set;
const block b = block(2, 3, 1, 2, true, false);
for (auto _ : state) {
for (size_t i = 0; i < 100000; ++i) {
set.emplace(b);
}
benchmark::DoNotOptimize(set);
}
}
static auto unordered_flat_set_puzzle_hasher(benchmark::State& state) -> void
{
puzzleset set;
const puzzle p = puzzle(puzzles[0]);
for (auto _ : state) {
for (size_t i = 0; i < 100000; ++i) {
set.emplace(p);
}
benchmark::DoNotOptimize(set);
}
}
static auto explore_state_space(benchmark::State& state) -> void
{
const puzzle p = puzzle(puzzles[state.range(0)]);
for (auto _ : state) {
auto space = p.explore_state_space();
benchmark::DoNotOptimize(space);
}
}
static auto explore_rush_hour_puzzle_space(benchmark::State& state) -> void
{
constexpr u8 max_blocks = 5;
constexpr u8 board_width = 4;
constexpr u8 board_height = 5;
constexpr u8 goal_x = board_width - 1;
constexpr u8 goal_y = 2;
constexpr bool restricted = true;
const blockset2 permitted_blocks = {
block(0, 0, 2, 1, false, false),
block(0, 0, 3, 1, false, false),
block(0, 0, 1, 2, false, false),
block(0, 0, 1, 3, false, false)
};
const block target_block = block(0, 0, 2, 1, true, false);
constexpr std::tuple<u8, u8, u8, u8> target_block_pos_range = {
0,
goal_y,
goal_x,
goal_y
};
const puzzle p = puzzle(board_width, board_height, goal_x, goal_y, restricted, true);
for (auto _ : state) {
puzzleset result = p.explore_puzzle_space(
permitted_blocks,
target_block,
target_block_pos_range,
max_blocks,
0,
std::nullopt);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(unordered_set_uint64<4>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_set_uint64<8>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_set_uint64<16>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_uint64<4>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_uint64<8>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_uint64<16>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_block_hasher)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_block_hasher2)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_puzzle_hasher)->Unit(benchmark::kMicrosecond);
BENCHMARK(explore_state_space)->DenseRange(0, puzzles.size() - 1)->Unit(benchmark::kMicrosecond);
BENCHMARK(explore_rush_hour_puzzle_space)->Unit(benchmark::kSecond);
BENCHMARK_MAIN();

6
flake.lock generated
View File

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1773201692, "lastModified": 1770843696,
"narHash": "sha256-NXrKzNMniu4Oam2kAFvqJ3GB2kAvlAFIriTAheaY8hw=", "narHash": "sha256-LovWTGDwXhkfCOmbgLVA10bvsi/P8eDDpRudgk68HA8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b6067cc0127d4db9c26c79e4de0513e58d0c40c9", "rev": "2343bbb58f99267223bc2aac4fc9ea301a155a16",
"type": "github" "type": "github"
}, },
"original": { "original": {

440
flake.nix
View File

@ -14,128 +14,26 @@ rec {
# Create a shell (and possibly package) for each possible system, not only x86_64-linux # Create a shell (and possibly package) for each possible system, not only x86_64-linux
flake-utils.lib.eachDefaultSystem ( flake-utils.lib.eachDefaultSystem (
system: let system: let
# =========================================================================================
# Define pkgs/stdenvs
# =========================================================================================
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
config.allowUnfree = true; config.allowUnfree = true;
overlays = []; overlays = [];
}; };
inherit (pkgs) lib stdenv;
clangPkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [];
# Use this to change the compiler:
# - GCC: pkgs.stdenv
# - Clang: pkgs.clangStdenv
# NixOS packages are built using GCC by default. Using clang requires a full rebuild/redownload.
config.replaceStdenv = {pkgs}: pkgs.clangStdenv;
};
windowsPkgs = import nixpkgs { windowsPkgs = import nixpkgs {
inherit system; inherit system;
config.allowUnfree = true;
overlays = [];
# Use this to cross compile to a different system
crossSystem = { crossSystem = {
config = "x86_64-w64-mingw32"; config = "x86_64-w64-mingw32";
}; };
config.allowUnfree = true;
}; };
inherit (pkgs) lib stdenv;
# =========================================================================================
# Define shell environment
# =========================================================================================
# Setup the shell when entering the "nix develop" environment (bash script).
shellHook = let
mkCmakeScript = type: let
typeLower = lib.toLower type;
in
pkgs.writers.writeFish "cmake-${typeLower}.fish" ''
cd $FLAKE_PROJECT_ROOT
echo "Removing build directory ./cmake-build-${typeLower}/"
rm -rf ./cmake-build-${typeLower}
echo "Creating build directory"
mkdir cmake-build-${typeLower}
cd cmake-build-${typeLower}
echo "Running cmake"
cmake -G "Ninja" \
-DCMAKE_BUILD_TYPE="${type}" \
..
echo "Linking compile_commands.json"
cd ..
ln -sf ./cmake-build-${typeLower}/compile_commands.json ./compile_commands.json
'';
cmakeDebug = mkCmakeScript "Debug";
cmakeRelease = mkCmakeScript "Release";
mkBuildScript = type: let
typeLower = lib.toLower type;
in
pkgs.writers.writeFish "cmake-build.fish" ''
cd $FLAKE_PROJECT_ROOT/cmake-build-${typeLower}
echo "Running cmake"
NIX_ENFORCE_NO_NATIVE=0 cmake --build . -j$(nproc)
'';
buildDebug = mkBuildScript "Debug";
buildRelease = mkBuildScript "Release";
# Use this to specify commands that should be ran after entering fish shell
initProjectShell = pkgs.writers.writeFish "init-shell.fish" ''
echo "Entering \"${description}\" environment..."
# Determine the project root, used e.g. in cmake scripts
set -g -x FLAKE_PROJECT_ROOT (git rev-parse --show-toplevel)
# C/C++:
abbr -a cmake-debug "${cmakeDebug}"
abbr -a cmake-release "${cmakeRelease}"
abbr -a build-debug "${buildDebug}"
abbr -a build-release "${buildRelease}"
abbr -a debug-clean "${cmakeDebug} && ${buildDebug} && ./cmake-build-debug/masssprings"
abbr -a release-clean "${cmakeRelease} && ${buildRelease} && ./cmake-build-release/masssprings"
abbr -a debug "${buildDebug} && ./cmake-build-debug/masssprings"
abbr -a release "${buildRelease} && ./cmake-build-release/masssprings"
abbr -a run "${buildRelease} && ./cmake-build-release/masssprings"
abbr -a runclusters "${buildRelease} && ./cmake-build-release/masssprings --output=clusters.puzzle --space=rh --moves=10 --blocks=4"
abbr -a runtests "${buildDebug} && ./cmake-build-debug/tests"
abbr -a runbenchs "mv -f benchs.json benchs.old.json; ${buildRelease} && sudo cpupower frequency-set --governor performance && ./cmake-build-release/benchmarks --benchmark_out=benchs.json --benchmark_out_format=console; sudo cpupower frequency-set --governor powersave"
abbr -a rungdb "${buildDebug} && gdb --tui ./cmake-build-debug/masssprings"
abbr -a runvalgrind "${buildDebug} && valgrind --leak-check=full --show-reachable=no --show-leak-kinds=definite,indirect,possible --track-origins=no --suppressions=valgrind.supp --log-file=valgrind.log ./cmake-build-debug/masssprings && cat valgrind.log"
abbr -a runperf "${buildRelease} && perf record -g ./cmake-build-release/masssprings && hotspot ./perf.data"
abbr -a runperf-graph "${buildRelease} && perf record -g ./cmake-build-release/benchmarks --benchmark_filter='explore_state_space' && hotspot ./perf.data"
abbr -a runperf-space "${buildRelease} && perf record -g ./cmake-build-release/benchmarks --benchmark_filter='explore_rush_hour_puzzle_space' && hotspot ./perf.data"
abbr -a runtracy "tracy -a 127.0.0.1 &; ${buildRelease} && sudo -E ./cmake-build-release/masssprings"
abbr -a runclion "clion ./CMakeLists.txt 2>/dev/null 1>&2 & disown;"
'';
in
builtins.concatStringsSep "\n" [
# Launch into pure fish shell
''
exec "$(type -p fish)" -C "source ${initProjectShell}"
''
];
# =========================================================================================== # ===========================================================================================
# Define custom dependencies # Define custom dependencies
# =========================================================================================== # ===========================================================================================
raygui = stdenv.mkDerivation rec { raygui = stdenv.mkDerivation (finalAttrs: {
pname = "raygui"; pname = "raygui";
version = "4.0-unstable-2026-02-24"; version = "4.0-unstable-2026-02-24";
@ -163,13 +61,13 @@ rec {
Name: raygui Name: raygui
Description: Simple and easy-to-use immediate-mode gui library Description: Simple and easy-to-use immediate-mode gui library
URL: https://github.com/raysan5/raygui URL: https://github.com/raysan5/raygui
Version: ${version} Version: ${finalAttrs.version}
Cflags: -I"{includedir}" Cflags: -I"{includedir}"
EOF EOF
runHook postInstall runHook postInstall
''; '';
}; });
thread-pool = stdenv.mkDerivation { thread-pool = stdenv.mkDerivation {
pname = "thread-pool"; pname = "thread-pool";
@ -185,69 +83,8 @@ rec {
# Header-only library # Header-only library
dontBuild = true; dontBuild = true;
installPhase = '' installPhase = ''
runHook preInstall
mkdir -p $out mkdir -p $out
cp -rv ./include $out/include mv ./include $out/include
runHook postInstall
'';
};
# We can use the pkgs.stdenv for Linux+Windows because it's a header only library.
# The build is required to create the pkg-config/cmake configuration files.
libmorton = stdenv.mkDerivation {
pname = "libmorton";
version = "0.2.12-unstable-2023-05-24";
src = pkgs.fetchFromGitHub {
owner = "Forceflow";
repo = "libmorton";
rev = "7923faa88d7e564020b2d5d408bf8c186ecbe363";
hash = "sha256-5LHiWu2GIuDmfM2gXGbRsFasE7AmVCSRphNdFElbbjk=";
};
nativeBuildInputs = with pkgs; [cmake];
cmakeFlags = [
"-DBUILD_TESTING=OFF"
"-DCMAKE_INSTALL_INCLUDEDIR=include"
"-DCMAKE_INSTALL_DATADIR=share"
];
};
glew-windows = windowsPkgs.stdenv.mkDerivation rec {
pname = "glew-windows";
version = "2.2.0";
src = pkgs.fetchurl {
url = "https://github.com/nigels-com/glew/releases/download/glew-${version}/glew-${version}.tgz";
hash = "sha256-1PyCiTz7ABCVeNChojN/uMozWzzsz5e5flzH8I5DU+E=";
};
nativeBuildInputs = with pkgs; [
cmake
ninja
pkg-config
];
preConfigure = ''
cd build/cmake
'';
cmakeFlags = [
"-DBUILD_UTILS=OFF"
"-DGLEW_OSMESA=OFF"
"-DBUILD_SHARED_LIBS=ON"
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
];
installPhase = ''
runHook preInstall
cmake --install . --prefix "$out"
runHook postInstall
''; '';
}; };
@ -261,20 +98,29 @@ rec {
# - Those which are needed on $PATH during the build, for example cmake and pkg-config # - Those which are needed on $PATH during the build, for example cmake and pkg-config
# - Setup hooks, for example makeWrapper # - Setup hooks, for example makeWrapper
# - Interpreters needed by patchShebangs for build scripts (with the --build flag), which can be the case for e.g. perl # - Interpreters needed by patchShebangs for build scripts (with the --build flag), which can be the case for e.g. perl
# NOTE: Do not add compiler here, they are provided by the stdenv
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
# Languages: # Languages:
# binutils binutils
gcc
# C/C++: # C/C++:
cmake
ninja
gdb gdb
valgrind valgrind
kdePackages.kcachegrind # gnumake
cmake
ninja
# cling
# pkg-config
# clang-tools
# compdb
# pprof
# gprof2dot
perf perf
hotspot hotspot
# heaptrack kdePackages.kcachegrind
gdbgui
massif-visualizer
heaptrack
# renderdoc # renderdoc
]; ];
@ -285,23 +131,18 @@ rec {
# C/C++: # C/C++:
raylib raylib
raygui raygui
glew
thread-pool thread-pool
libmorton
boost boost
# Debugging/Testing/Profiling # Debugging
tracy_0_13 tracy-wayland
backward-cpp backward-cpp
libbfd libbfd
catch2_3 catch2_3
gbenchmark
]; ];
# =========================================================================================== # ===========================================================================================
# Define buildable + installable packages # Define buildable + installable packages
# =========================================================================================== # ===========================================================================================
package = stdenv.mkDerivation rec { package = stdenv.mkDerivation rec {
inherit buildInputs; inherit buildInputs;
pname = "masssprings"; pname = "masssprings";
@ -311,17 +152,12 @@ rec {
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
gcc gcc
cmake cmake
# Fix the working directory so the auxiliary files are always available
makeWrapper
]; ];
cmakeFlags = [ cmakeFlags = [
"-DDISABLE_THREADPOOL=Off"
"-DDISABLE_TRACY=On" "-DDISABLE_TRACY=On"
"-DDISABLE_BACKWARD=On" "-DDISABLE_BACKWARD=On"
"-DDISABLE_TESTS=On" "-DDISABLE_TESTS=On"
"-DDISABLE_BENCH=On"
]; ];
hardeningDisable = ["all"]; hardeningDisable = ["all"];
@ -331,29 +167,11 @@ rec {
''; '';
installPhase = '' installPhase = ''
runHook preInstall
mkdir -p $out/lib
cp ./${pname} $out/lib/
cp -rv $src/default.puzzle $out/lib/
cp -rv $src/fonts $out/lib/fonts
cp -rv $src/shader $out/lib/shader
# The wrapper enters the correct working dir, so fonts/shaders/presets are available
mkdir -p $out/bin mkdir -p $out/bin
makeWrapper $out/lib/${pname} $out/bin/${pname} --chdir "$out/lib" cp ./${pname} $out/bin/
cp $src/default.puzzle $out/bin/
# Generate a .desktop file cp -r $src/fonts $out/bin/fonts
mkdir -p $out/share/applications cp -r $src/shader $out/bin/shader
cat <<INI > $out/share/applications/${pname}.desktop
[Desktop Entry]
Terminal=true
Name=PuzzleSpaces
Exec=$out/bin/${pname} %f
Type=Application
INI
runHook postInstall
''; '';
}; };
@ -371,39 +189,22 @@ rec {
buildInputs = with windowsPkgs; [ buildInputs = with windowsPkgs; [
raylib raylib
raygui raygui
glew-windows
thread-pool thread-pool
libmorton boost
# Disable stacktrace since that's platform dependant and won't cross compile to windows
# https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/libraries/boost/generic.nix#L43
(boost.override {
enableShared = false;
extraB2Args = [
"--without-stacktrace"
];
})
]; ];
cmakeFlags = [ cmakeFlags = [
"-DCMAKE_SYSTEM_NAME=Windows" "-DCMAKE_SYSTEM_NAME=Windows"
"-DDISABLE_THREADPOOL=Off"
"-DDISABLE_TRACY=On" "-DDISABLE_TRACY=On"
"-DDISABLE_BACKWARD=On" "-DDISABLE_BACKWARD=On"
"-DDISABLE_TESTS=On"
"-DDISABLE_BENCH=On"
]; ];
installPhase = '' installPhase = ''
runHook preInstall
mkdir -p $out/bin mkdir -p $out/bin
cp -rv ./${pname}.exe $out/bin/ cp ./${pname}.exe $out/bin/
cp -rv $src/default.puzzle $out/bin/ cp $src/default.puzzle $out/bin/
cp -rv $src/fonts $out/bin/fonts cp -r $src/fonts $out/bin/fonts
cp -rv $src/shader $out/bin/shader cp -r $src/shader $out/bin/shader
runHook postInstall
''; '';
}; };
in rec { in rec {
@ -417,55 +218,156 @@ rec {
# Provide environment for "nix develop" # Provide environment for "nix develop"
devShells = { devShells = {
default = pkgs.mkShell { default = pkgs.mkShell {
inherit nativeBuildInputs buildInputs shellHook; inherit nativeBuildInputs buildInputs;
name = description; name = description;
# ========================================================================================= # =========================================================================================
# Define environment variables # Define environment variables
# ========================================================================================= # =========================================================================================
# Custom dynamic libraries:
# LD_LIBRARY_PATH = builtins.concatStringsSep ":" [
# # Rust Bevy GUI app:
# # "${pkgs.xorg.libX11}/lib"
# # "${pkgs.xorg.libXcursor}/lib"
# # "${pkgs.xorg.libXrandr}/lib"
# # "${pkgs.xorg.libXi}/lib"
# # "${pkgs.libGL}/lib"
#
# # JavaFX app:
# # "${pkgs.libGL}/lib"
# # "${pkgs.gtk3}/lib"
# # "${pkgs.glib.out}/lib"
# # "${pkgs.xorg.libXtst}/lib"
# ];
# Dynamic libraries from buildinputs: # Dynamic libraries from buildinputs:
LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath buildInputs; LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath buildInputs;
# =========================================================================================
# Define shell environment
# =========================================================================================
# Setup the shell when entering the "nix develop" environment (bash script).
shellHook = let
mkCmakeScript = type: let
typeLower = lib.toLower type;
in
pkgs.writers.writeFish "cmake-${typeLower}.fish" ''
cd $FLAKE_PROJECT_ROOT
# set -g -x CC ${pkgs.clang}/bin/clang
# set -g -x CXX ${pkgs.clang}/bin/clang++
echo "Removing build directory ./cmake-build-${typeLower}/"
rm -rf ./cmake-build-${typeLower}
echo "Creating build directory"
mkdir cmake-build-${typeLower}
cd cmake-build-${typeLower}
echo "Running cmake"
cmake -G "Ninja" \
-DCMAKE_BUILD_TYPE="${type}" \
..
echo "Linking compile_commands.json"
cd ..
ln -sf ./cmake-build-${typeLower}/compile_commands.json ./compile_commands.json
'';
cmakeDebug = mkCmakeScript "Debug";
cmakeRelease = mkCmakeScript "Release";
mkBuildScript = type: let
typeLower = lib.toLower type;
in
pkgs.writers.writeFish "cmake-build.fish" ''
cd $FLAKE_PROJECT_ROOT/cmake-build-${typeLower}
echo "Running cmake"
NIX_ENFORCE_NO_NATIVE=0 cmake --build . -j$(nproc)
'';
buildDebug = mkBuildScript "Debug";
buildRelease = mkBuildScript "Release";
# Use this to specify commands that should be ran after entering fish shell
initProjectShell = pkgs.writers.writeFish "init-shell.fish" ''
echo "Entering \"${description}\" environment..."
# Determine the project root, used e.g. in cmake scripts
set -g -x FLAKE_PROJECT_ROOT (git rev-parse --show-toplevel)
# C/C++:
abbr -a cmake-debug "${cmakeDebug}"
abbr -a cmake-release "${cmakeRelease}"
abbr -a build-debug "${buildDebug}"
abbr -a build-release "${buildRelease}"
abbr -a debug "${buildDebug} && ./cmake-build-debug/masssprings"
abbr -a release "${buildRelease} && ./cmake-build-release/masssprings"
abbr -a debug-clean "${cmakeDebug} && ${buildDebug} && ./cmake-build-debug/masssprings"
abbr -a release-clean "${cmakeRelease} && ${buildRelease} && ./cmake-build-release/masssprings"
abbr -a rungdb "${buildDebug} && gdb --tui ./cmake-build-debug/masssprings"
abbr -a runperf "${buildRelease} && perf record -g ./cmake-build-release/masssprings && hotspot ./perf.data"
abbr -a runtracy "tracy -a 127.0.0.1 &; ${buildRelease} && sudo -E ./cmake-build-release/masssprings"
abbr -a runvalgrind "${buildDebug} && valgrind --leak-check=full --show-reachable=no --show-leak-kinds=definite,indirect,possible --track-origins=no --suppressions=valgrind.supp --log-file=valgrind.log ./cmake-build-debug/masssprings && cat valgrind.log"
abbr -a runtests "${buildDebug} && ./cmake-build-debug/tests"
abbr -a runclion "clion ./CMakeLists.txt 2>/dev/null 1>&2 & disown;"
'';
in
builtins.concatStringsSep "\n" [
# Launch into pure fish shell
''
exec "$(type -p fish)" -C "source ${initProjectShell} && abbr -a menu '${pkgs.bat}/bin/bat "${initProjectShell}"'"
''
];
}; };
# Provide environment with clang stdenv for "nix develop .#clang" # TODO: Can't get renderdoc in FHS to work
# TODO: Broken. Clang can't find stdlib headers or library headers (raylib, backward, ...).
# Does the clangStdenv not automatically collect the include paths?
clang =
pkgs.mkShell.override {
stdenv = pkgs.clangStdenv;
} {
inherit shellHook;
name = description;
nativeBuildInputs = with pkgs; [ # FHS environment for renderdoc. Access with "nix develop .#renderdoc".
cmake # https://ryantm.github.io/nixpkgs/builders/special/fhs-environments
ninja # renderdoc =
]; # (pkgs.buildFHSEnv {
# name = "renderdoc-env";
buildInputs = with pkgs; [ #
# C/C++: # targetPkgs = pkgs:
raylib # with pkgs; [
raygui # # RenderDoc
glew # renderdoc
thread-pool #
libmorton # # Build tools
boost # gcc
# cmake
# Debugging/Testing/Profiling #
backward-cpp # # Raylib
libbfd # raylib
catch2_3 # libGL
gbenchmark # mesa
]; #
# # X11
# ========================================================================================= # libx11
# Define environment variables # libxcursor
# ========================================================================================= # libxrandr
# libxinerama
# Dynamic libraries from buildinputs: # libxi
LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath buildInputs; # libxext
}; # libxfixes
#
# # Wayland
# wayland
# wayland-protocols
# libxkbcommon
# ];
#
# runScript = "fish";
#
# profile = ''
# '';
# }).env;
}; };
} }
); );

View File

@ -1,69 +0,0 @@
#ifndef BITS_HPP_
#define BITS_HPP_
#include "util.hpp"
#include <concepts>
template <class T>
requires std::unsigned_integral<T>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto create_mask(const u8 first, const u8 last) -> T
{
// If the mask width is equal the type width return all 1s instead of shifting
// as shifting by type-width is undefined behavior.
if (static_cast<size_t>(last - first + 1) >= sizeof(T) * 8) {
return ~T{0};
}
// Example: first=4, last=7, 7-4+1=4
// 1 << 4 = 0b00010000
// 32 - 1 = 0b00001111
// 31 << 4 = 0b11110000
// Subtracting 1 generates a consecutive mask.
return ((T{1} << (last - first + 1)) - 1) << first;
}
template <class T>
requires std::unsigned_integral<T>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto clear_bits(T& bits, const u8 first, const u8 last) -> void
{
const T mask = create_mask<T>(first, last);
bits = bits & ~mask;
}
template <class T, class U>
requires std::unsigned_integral<T> && std::unsigned_integral<U>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto set_bits(T& bits, const u8 first, const u8 last, const U value) -> void
{
const T mask = create_mask<T>(first, last);
// Example: first=4, last=6, value=0b1110, bits = 0b 01111110
// mask = 0b 01110000
// bits & ~mask = 0b 00001110
// value << 4 = 0b 11100000
// (value << 4) & mask = 0b 01100000
// (bits & ~mask) | (value << 4) & mask = 0b 01101110
// Insert position: ^^^
// First clear the bits, then | with the value positioned at the insertion point.
// The value may be larger than [first, last], extra bits are ignored.
bits = (bits & ~mask) | ((static_cast<T>(value) << first) & mask);
}
template <class T>
requires std::unsigned_integral<T>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto get_bits(const T bits, const u8 first, const u8 last) -> T
{
const T mask = create_mask<T>(first, last);
// We can >> without sign extension because T is unsigned_integral
return (bits & mask) >> first;
}
auto print_bitmap(u64 bitmap, u8 w, u8 h, const std::string& title) -> void;
#endif

View File

@ -3,32 +3,12 @@
#include <raylib.h> #include <raylib.h>
// Calculate the octree parallel to the layout calculation. #define THREADPOOL // Enable physics threadpool
// Layout uses the octree from last frame.
#define ASYNC_OCTREE
// Gets set by CMake // Gets set by CMake
// #define THREADPOOL // Enable physics threadpool
// #define BACKWARD // Enable pretty stack traces // #define BACKWARD // Enable pretty stack traces
// #define TRACY // Enable tracy profiling support // #define TRACY // Enable tracy profiling support
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
#if defined(_WIN32)
#define NOGDI // All GDI defines and routines
#define NOUSER // All USER defines and routines
#endif
#define BS_THREAD_POOL_NATIVE_EXTENSIONS
// ReSharper disable once CppUnusedIncludeDirective
#include <BS_thread_pool.hpp>
using threadpool = std::optional<BS::thread_pool<>* const>;
#if defined(_WIN32) // raylib uses these names as function parameters
#undef near
#undef far
#endif
// Window // Window
constexpr int INITIAL_WIDTH = 600; constexpr int INITIAL_WIDTH = 600;
constexpr int INITIAL_HEIGHT = 600; constexpr int INITIAL_HEIGHT = 600;
@ -47,14 +27,12 @@ constexpr int FONT_SIZE = 26;
// Camera Controls // Camera Controls
constexpr float CAMERA_FOV = 90.0; constexpr float CAMERA_FOV = 90.0;
constexpr float FOV_SPEED = 1.0; constexpr float FOV_SPEED = 1.0;
constexpr float FOV_MULTIPLIER = 4.0;
constexpr float MIN_FOV = 10.0; constexpr float MIN_FOV = 10.0;
constexpr float MAX_PERSP_FOV = 120.0; constexpr float MAX_FOV = 180.0;
constexpr float MAX_ORTHO_FOV = 540.0; constexpr float CAMERA_DISTANCE = 20.0;
constexpr float CAMERA_DISTANCE = 150.0; constexpr float ZOOM_SPEED = 2.5;
constexpr float MIN_CAMERA_DISTANCE = 2.0; constexpr float MIN_CAMERA_DISTANCE = 2.0;
constexpr float MAX_CAMERA_DISTANCE = 2000.0; constexpr float MAX_CAMERA_DISTANCE = 2000.0;
constexpr float ZOOM_SPEED = 2.5;
constexpr float ZOOM_MULTIPLIER = 4.0; constexpr float ZOOM_MULTIPLIER = 4.0;
constexpr float PAN_SPEED = 2.0; constexpr float PAN_SPEED = 2.0;
constexpr float PAN_MULTIPLIER = 10.0; constexpr float PAN_MULTIPLIER = 10.0;
@ -66,27 +44,24 @@ constexpr float TARGET_UPS = 90; // How often to update physics
constexpr float TIMESTEP = 1.0 / TARGET_UPS; // Update interval in seconds 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 SIM_SPEED = 4.0; // How large each update should be
constexpr float MASS = 1.0; // Mass spring system constexpr float MASS = 1.0; // Mass spring system
constexpr float SPRING_K = 4.0; // Mass spring system constexpr float SPRING_CONSTANT = 5.0; // Mass spring system
constexpr float DAMPENING_K = 1.5; // Mass spring system constexpr float DAMPENING_CONSTANT = 1.0; // Mass spring system
constexpr float REST_LENGTH = 3.0; // Mass spring system constexpr float REST_LENGTH = 3.0; // Mass spring system
constexpr float VERLET_DAMPENING = 0.1; // [0, 1] constexpr float VERLET_DAMPENING = 0.05; // [0, 1]
constexpr float BH_FORCE = 2.5; // Barnes-Hut [1.0, 3.0] constexpr float BH_FORCE = 2.5; // Barnes-Hut [1.0, 3.0]
constexpr float THETA = 1.0; // Barnes-Hut [0.5, 1.0] constexpr float THETA = 0.8; // Barnes-Hut [0.5, 1.0]
constexpr float SOFTENING = 0.05; // Barnes-Hut [0.01, 1.0] constexpr float SOFTENING = 0.01; // Barnes-Hut [0.01, 1.0]
// Graph Drawing // Graph Drawing
static const Color EDGE_COLOR = Fade(BLUE, 0.3); constexpr Color EDGE_COLOR = DARKBLUE;
constexpr int DRAW_EDGES_LIMIT = 5'000'000; constexpr float VERTEX_SIZE = 0.5;
constexpr float VERTEX_SIZE = 0.75; static const Color VERTEX_COLOR = Fade(BLUE, 0.5);
constexpr int DRAW_VERTICES_LIMIT = 1'000'000; constexpr Color VERTEX_VISITED_COLOR = DARKBLUE;
static const Color VERTEX_COLOR = Fade(BLUE, 0.8);
constexpr Color VERTEX_VISITED_COLOR = ORANGE;
constexpr Color VERTEX_START_COLOR = ORANGE;
constexpr Color VERTEX_CURRENT_COLOR = ORANGE;
constexpr Color VERTEX_PATH_COLOR = GREEN; constexpr Color VERTEX_PATH_COLOR = GREEN;
constexpr Color VERTEX_TARGET_COLOR = GREEN; constexpr Color VERTEX_TARGET_COLOR = RED;
static const Color VERTEX_CLOSEST_COLOR = Fade(PINK, 1.0); constexpr Color VERTEX_START_COLOR = ORANGE;
static const Color VERTEX_FARTHEST_COLOR = Fade(DARKBLUE, 0.8); constexpr Color VERTEX_CURRENT_COLOR = PURPLE;
constexpr int DRAW_VERTICES_LIMIT = 1000000;
// Klotski Drawing // Klotski Drawing
constexpr int BOARD_PADDING = 10; constexpr int BOARD_PADDING = 10;
@ -97,8 +72,4 @@ constexpr Color BLOCK_COLOR = DARKBLUE;
constexpr Color TARGET_BLOCK_COLOR = RED; constexpr Color TARGET_BLOCK_COLOR = RED;
constexpr Color WALL_COLOR = BLACK; constexpr Color WALL_COLOR = BLACK;
// Threadpool
static constexpr int SMALL_TASK_BLOCK_SIZE = 256; // Weirdly larger blocks decrease performance...
static constexpr int LARGE_TASK_BLOCK_SIZE = 256;
#endif #endif

View File

@ -1,47 +0,0 @@
#ifndef MASS_SPRING_SYSTEM_HPP_
#define MASS_SPRING_SYSTEM_HPP_
#include "octree.hpp"
#include "config.hpp"
#include <optional>
#include <raylib.h>
using spring = std::pair<size_t, size_t>;
class cpu_spring_system
{
public:
octree tree;
// This is the main ownership of all the states/masses/springs.
std::vector<Vector3> positions;
std::vector<Vector3> previous_positions; // for verlet integration
std::vector<Vector3> velocities;
std::vector<Vector3> forces;
std::vector<spring> springs;
public:
cpu_spring_system() {}
NO_COPY_NO_MOVE(cpu_spring_system);
public:
auto clear() -> void;
auto add_mass() -> void;
auto add_spring(size_t a, size_t b) -> void;
auto clear_forces() -> void;
auto calculate_spring_force(size_t s) -> void;
auto calculate_spring_forces(threadpool thread_pool = std::nullopt) -> void;
auto calculate_repulsion_forces(threadpool thread_pool = std::nullopt) -> void;
auto integrate_velocity(size_t m, float dt) -> void;
auto integrate_position(size_t m, float dt) -> void;
auto verlet_update(size_t m, float dt) -> void;
auto update(float dt, threadpool thread_pool = std::nullopt) -> void;
auto center_masses(threadpool thread_pool = std::nullopt) -> void;
};
#endif

View File

@ -1,8 +1,7 @@
#ifndef DISTANCE_HPP_ #ifndef DISTANCE_HPP_
#define DISTANCE_HPP_ #define DISTANCE_HPP_
#include "cpu_spring_system.hpp" #include <cstddef>
#include <vector> #include <vector>
class graph_distances class graph_distances
@ -16,11 +15,10 @@ public:
auto clear() -> void; auto clear() -> void;
[[nodiscard]] auto empty() const -> bool; [[nodiscard]] auto empty() const -> bool;
auto calculate_distances(size_t node_count, auto calculate_distances(size_t node_count, const std::vector<std::pair<size_t, size_t>>& edges,
const std::vector<spring>& edges,
const std::vector<size_t>& targets) -> void; const std::vector<size_t>& targets) -> void;
[[nodiscard]] auto get_shortest_path(size_t source) const -> std::vector<size_t>; [[nodiscard]] auto get_shortest_path(size_t source) const -> std::vector<size_t>;
}; };
#endif #endif

View File

@ -1,5 +1,5 @@
#ifndef INPUT_HANDLER_HPP_ #ifndef INPUT_HPP_
#define INPUT_HANDLER_HPP_ #define INPUT_HPP_
#include "orbit_camera.hpp" #include "orbit_camera.hpp"
#include "state_manager.hpp" #include "state_manager.hpp"
@ -22,7 +22,8 @@ struct show_yes_no_message
std::function<void()> on_yes; std::function<void()> on_yes;
}; };
struct show_save_preset_window {}; struct show_save_preset_window
{};
using ui_command = std::variant<show_ok_message, show_yes_no_message, show_save_preset_window>; using ui_command = std::variant<show_ok_message, show_yes_no_message, show_save_preset_window>;
@ -74,11 +75,10 @@ public:
bool mark_path = false; bool mark_path = false;
bool mark_solutions = false; bool mark_solutions = false;
bool connect_solutions = false; bool connect_solutions = false;
bool color_by_distance = false;
// Camera // Camera
bool camera_lock = true; bool camera_lock = true;
bool camera_mass_center_lock = true; bool camera_mass_center_lock = false;
bool camera_panning = false; bool camera_panning = false;
bool camera_rotating = false; bool camera_rotating = false;
@ -86,17 +86,16 @@ public:
Vector2 mouse = Vector2Zero(); Vector2 mouse = Vector2Zero();
Vector2 last_mouse = Vector2Zero(); Vector2 last_mouse = Vector2Zero();
// State selection from graph
size_t collision_mass = -1;
public: public:
input_handler(state_manager& _state, orbit_camera& _camera) input_handler(state_manager& _state, orbit_camera& _camera) : state(_state), camera(_camera)
: state(_state), camera(_camera)
{ {
init_handlers(); init_handlers();
} }
NO_COPY_NO_MOVE(input_handler); input_handler(const input_handler& copy) = delete;
auto operator=(const input_handler& copy) -> input_handler& = delete;
input_handler(input_handler&& move) = delete;
auto operator=(input_handler&& move) -> input_handler& = delete;
private: private:
auto init_handlers() -> void; auto init_handlers() -> void;
@ -123,7 +122,6 @@ public:
auto add_block() -> void; auto add_block() -> void;
auto remove_block() -> void; auto remove_block() -> void;
auto place_goal() const -> void; auto place_goal() const -> void;
auto select_state() const -> void;
// Key actions // Key actions
auto toggle_camera_lock() -> void; auto toggle_camera_lock() -> void;
@ -141,7 +139,6 @@ public:
auto clear_graph() -> void; auto clear_graph() -> void;
auto toggle_mark_solutions() -> void; auto toggle_mark_solutions() -> void;
auto toggle_connect_solutions() -> void; auto toggle_connect_solutions() -> void;
auto toggle_color_by_distance() -> void;
auto toggle_mark_path() -> void; auto toggle_mark_path() -> void;
auto goto_optimal_next_state() const -> void; auto goto_optimal_next_state() const -> void;
auto goto_most_distant_state() const -> void; auto goto_most_distant_state() const -> void;
@ -161,14 +158,18 @@ public:
// General // General
auto register_generic_handler(const std::function<void(input_handler&)>& handler) -> void; auto register_generic_handler(const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_pressed_handler(MouseButton button, const std::function<void(input_handler&)>& handler) -> void; auto register_mouse_pressed_handler(MouseButton button,
const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_released_handler(MouseButton button, auto register_mouse_released_handler(MouseButton button,
const std::function<void(input_handler&)>& handler) -> void; const std::function<void(input_handler&)>& handler)
-> void;
auto register_key_pressed_handler(KeyboardKey key, const std::function<void(input_handler&)>& handler) -> void; auto register_key_pressed_handler(KeyboardKey key,
const std::function<void(input_handler&)>& handler) -> void;
auto register_key_released_handler(KeyboardKey key, const std::function<void(input_handler&)>& handler) -> void; auto register_key_released_handler(KeyboardKey key,
const std::function<void(input_handler&)>& handler) -> void;
auto handle_input() -> void; auto handle_input() -> void;
}; };

View File

@ -1,15 +0,0 @@
#ifndef LOAD_SAVE_HPP_
#define LOAD_SAVE_HPP_
#include "puzzle.hpp"
#include <string>
auto parse_preset_file(const std::string& preset_file) -> std::pair<std::vector<puzzle>, std::vector<std::string>>;
auto append_preset_file(const std::string& preset_file, const std::string& preset_name, const puzzle& p) -> bool;
auto append_preset_file_quiet(const std::string& preset_file,
const std::string& preset_name,
const puzzle& p,
bool validate) -> bool;
#endif

View File

@ -0,0 +1,114 @@
#ifndef MASS_SPRING_SYSTEM_HPP_
#define MASS_SPRING_SYSTEM_HPP_
#include "octree.hpp"
#include "util.hpp"
#include "config.hpp"
#include <raylib.h>
#include <raymath.h>
#ifdef THREADPOOL
#if defined(_WIN32)
#define NOGDI // All GDI defines and routines
#define NOUSER // All USER defines and routines
#endif
#define BS_THREAD_POOL_NATIVE_EXTENSIONS
#include <BS_thread_pool.hpp>
#if defined(_WIN32) // raylib uses these names as function parameters
#undef near
#undef far
#endif
#endif
class mass_spring_system
{
public:
class mass
{
public:
Vector3 position = Vector3Zero();
Vector3 previous_position = Vector3Zero(); // for verlet integration
Vector3 velocity = Vector3Zero();
Vector3 force = Vector3Zero();
public:
mass() = delete;
explicit mass(const Vector3 _position)
: position(_position), previous_position(_position) {}
public:
auto clear_force() -> void;
auto calculate_velocity(float delta_time) -> void;
auto calculate_position(float delta_time) -> void;
auto verlet_update(float delta_time) -> void;
};
class spring
{
public:
size_t a;
size_t b;
public:
spring(const size_t _a, const size_t _b)
: a(_a), b(_b) {}
public:
static auto calculate_spring_force(mass& _a, mass& _b) -> void;
};
private:
#ifdef THREADPOOL
BS::thread_pool<> threads;
#endif
public:
octree tree;
// This is the main ownership of all the states/masses/springs.
std::vector<mass> masses;
std::vector<spring> springs;
public:
mass_spring_system()
#ifdef THREADPOOL
: threads(std::thread::hardware_concurrency() - 1, set_thread_name)
#endif
{
infoln("Using Barnes-Hut + Octree repulsion force calculation.");
#ifdef THREADPOOL
infoln("Thread-pool: {} threads.", threads.get_thread_count());
#else
infoln("Thread-pool: Disabled.");
#endif
}
mass_spring_system(const mass_spring_system& copy) = delete;
auto operator=(const mass_spring_system& copy) -> mass_spring_system& = delete;
mass_spring_system(mass_spring_system& move) = delete;
auto operator=(mass_spring_system&& move) -> mass_spring_system& = delete;
private:
#ifdef THREADPOOL
static auto set_thread_name(size_t idx) -> void;
#endif
auto build_octree() -> void;
public:
auto clear() -> void;
auto add_mass() -> void;
auto add_spring(size_t a, size_t b) -> void;
auto clear_forces() -> void;
auto calculate_spring_forces() -> void;
auto calculate_repulsion_forces() -> void;
auto verlet_update(float delta_time) -> void;
auto center_masses() -> void;
};
#endif

View File

@ -1,15 +1,10 @@
#ifndef OCTREE_HPP_ #ifndef OCTREE_HPP_
#define OCTREE_HPP_ #define OCTREE_HPP_
#include "util.hpp"
#include "config.hpp"
#include <array> #include <array>
#include <vector>
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <libmorton/morton.h> #include <vector>
class octree class octree
{ {
@ -18,193 +13,40 @@ class octree
public: public:
Vector3 mass_center = Vector3Zero(); Vector3 mass_center = Vector3Zero();
float mass_total = 0.0; float mass_total = 0.0;
u8 depth = 0; Vector3 box_min = Vector3Zero(); // area start
float size = 0.0f; // Because our octree cells are cubic we don't need to store the bounds Vector3 box_max = Vector3Zero(); // area end
std::array<int, 8> children = {-1, -1, -1, -1, -1, -1, -1, -1}; std::array<int, 8> children = {-1, -1, -1, -1, -1, -1, -1, -1};
int mass_id = -1; int mass_id = -1;
bool leaf = true; bool leaf = true;
public:
[[nodiscard]] auto child_count() const -> int;
}; };
private: public:
// 21 * 3 = 63, fits in u64 for combined x/y/z morton-code static constexpr int MAX_DEPTH = 20;
static constexpr int MAX_DEPTH = 21;
std::vector<node> nodes; std::vector<node> nodes;
// This approach is actually slower than the array of nodes
// beacuse we access all the attributes in the same function
// std::vector<Vector3> mass_centers;
// std::vector<float> mass_totals;
// std::vector<Vector3> box_mins;
// std::vector<Vector3> box_maxs;
// std::vector<std::array<int, 8>> childrens;
// std::vector<int> mass_ids;
// std::vector<u8> leafs; // bitpacked std::vector<bool> is a lot slower
public: public:
octree() = default; octree() = default;
// Required for async octree octree(const octree& copy) = delete;
// NO_COPY_NO_MOVE(octree); auto operator=(const octree& copy) -> octree& = delete;
octree(octree&& move) = delete;
private: auto operator=(octree&& move) -> octree& = delete;
[[nodiscard]] INLINE static inline auto get_octant(const Vector3& box_min,
const Vector3& box_max,
const Vector3& pos) -> int;
[[nodiscard]] INLINE static inline auto get_child_bounds(const Vector3& box_min,
const Vector3& box_max,
int octant) -> std::pair<Vector3, Vector3>;
// Map a floating point coordinate to a discrete integer (so its morton-code can be computed)
// The "bits" parameter determines the discrete axis resolution
[[nodiscard]] INLINE static inline auto quantize_axis(float coordinate,
float box_min,
float box_max,
int bits) -> u32;
[[nodiscard]] INLINE static inline auto pos_to_morton(const Vector3& p,
const Vector3& root_min,
const Vector3& root_max) -> u64;
[[nodiscard]] INLINE static inline auto jitter_pos(Vector3 p,
u32 seed,
const Vector3& root_min,
const Vector3& root_max,
float root_extent) -> Vector3;
// Use this to obtain an ancestor node of a leaf node (on any level).
// Because the morton codes (interleaved coordinates) encode the octree path, we can take
// the morten code of any leaf and only take the 3*n first interleaved bits to find the
// leaf ancestor on level n.
// Leaf Code: [101 110 100 001] -> Ancestors (from leaf to root):
// - [101 110 100]
// - [101 110]
// - [101] (root)
[[nodiscard]] INLINE static inline auto path_to_ancestor(u64 leaf_code, int leaf_depth, int depth) -> u64;
// Use this to obtain the octant a leaf node is contained in (on any level).
// The 3 interleaved bits in the morten code encode the octant [0, 7].
// Leaf Code: [101 110 100 001] -> Octants:
// - [100] (Level 2)
// - [110] (Level 1)
// - [101] (Level 0)
[[nodiscard]] INLINE static inline auto octant_at_level(u64 leaf_code, int level, int leaf_depth) -> int;
public: public:
auto clear() -> void; auto create_empty_leaf(const Vector3& box_min, const Vector3& box_max) -> int;
auto reserve(size_t count) -> void;
[[nodiscard]] auto empty() const -> bool;
[[nodiscard]] auto root() const -> const node&;
// Morton/linear octree implementation [[nodiscard]] auto get_octant(int node_idx, const Vector3& pos) const -> int;
static auto build_octree_morton(octree& t,
const std::vector<Vector3>& positions, [[nodiscard]] auto get_child_bounds(int node_idx, int octant) const
const std::optional<BS::thread_pool<>*>& thread_pool) -> void; -> std::pair<Vector3, Vector3>;
[[nodiscard]] auto calculate_force_morton(int node_idx, const Vector3& pos, int self_id) const -> Vector3;
auto insert(int node_idx, int mass_id, const Vector3& pos, float mass, int depth) -> void;
[[nodiscard]] auto calculate_force(int node_idx, const Vector3& pos) const -> Vector3;
}; };
INLINE inline auto octree::get_octant(const Vector3& box_min, const Vector3& box_max, const Vector3& pos) -> int #endif
{
auto [cx, cy, cz] = (box_min + box_max) / 2.0f;
// The octant is encoded as a 3-bit integer "zyx". The node area is split
// along all 3 axes, if a position is right of an axis, this bit is set to 1.
// If a position is right of the x-axis and y-axis and left of the z-axis, the
// encoded octant is "011".
return (pos.x >= cx) | ((pos.y >= cy) << 1) | ((pos.z >= cz) << 2);
}
INLINE inline auto octree::get_child_bounds(const Vector3& box_min,
const Vector3& box_max,
const int octant) -> std::pair<Vector3, Vector3>
{
auto [cx, cy, cz] = (box_min + box_max) / 2.0f;
Vector3 min = Vector3Zero();
Vector3 max = Vector3Zero();
// If (octant & 1), the octant is to the right of the node region's x-axis
// (see GetOctant). This means the left bound is the x-axis and the right
// bound the node's region max.
min.x = octant & 1 ? cx : box_min.x;
max.x = octant & 1 ? box_max.x : cx;
min.y = octant & 2 ? cy : box_min.y;
max.y = octant & 2 ? box_max.y : cy;
min.z = octant & 4 ? cz : box_min.z;
max.z = octant & 4 ? box_max.z : cz;
return std::make_pair(min, max);
}
INLINE inline auto octree::quantize_axis(const float coordinate,
const float box_min,
const float box_max,
const int bits) -> u32
{
const float extent = box_max - box_min;
if (extent <= 0.0f) {
return 0;
}
float normalized = (coordinate - box_min) / extent; // normalize to [0,1]
normalized = std::max(0.0f, std::min(normalized, std::nextafter(1.0f, 0.0f))); // avoid exactly 1.0
// bits up to 21 => (1u << bits) safe in 32-bit
const u32 grid_max = (1u << bits) - 1u;
return static_cast<u32>(normalized * static_cast<float>(grid_max));
}
INLINE inline auto octree::pos_to_morton(const Vector3& p, const Vector3& root_min, const Vector3& root_max) -> u64
{
const u32 x = quantize_axis(p.x, root_min.x, root_max.x, MAX_DEPTH);
const u32 y = quantize_axis(p.y, root_min.y, root_max.y, MAX_DEPTH);
const u32 z = quantize_axis(p.z, root_min.z, root_max.z, MAX_DEPTH);
return libmorton::morton3D_64_encode(x, y, z);
}
INLINE inline auto octree::jitter_pos(Vector3 p,
const u32 seed,
const Vector3& root_min,
const Vector3& root_max,
const float root_extent) -> Vector3
{
// Use a hash to calculate a deterministic jitter: The same position should always get the same jitter.
// We want this to get stable physics, particles at the same position shouldn't get different jitters
// across frames...
u32 h = (seed ^ 61u) ^ (seed >> 16);
h *= 9u;
h = h ^ (h >> 4);
h *= 0x27d4eb2du;
h = h ^ (h >> 15);
// finest cell size at depth L
const float finest_cell = root_extent / static_cast<float>(1u << MAX_DEPTH);
const float s = finest_cell * 1e-4f; // small pp
p.x += (h & 1u) ? +s : -s;
p.y += (h & 2u) ? +s : -s;
p.z += (h & 4u) ? +s : -s;
// clamp back into bounds just in case
p.x = std::max(root_min.x, std::min(p.x, root_max.x));
p.y = std::max(root_min.y, std::min(p.y, root_max.y));
p.z = std::max(root_min.z, std::min(p.z, root_max.z));
return p;
}
INLINE inline auto octree::path_to_ancestor(const u64 leaf_code, const int leaf_depth, const int depth) -> u64
{
// keep top 3*depth bits; drop the rest
const int drop = 3 * (leaf_depth - depth);
return (drop > 0) ? (leaf_code >> drop) : leaf_code;
}
INLINE inline auto octree::octant_at_level(const u64 leaf_code, const int level, const int leaf_depth) -> int
{
// level 1 => child of root => topmost 3 bits
const int shift = 3 * (leaf_depth - level);
return static_cast<int>((leaf_code >> shift) & 0x7ull);
}
#endif

View File

@ -24,7 +24,8 @@ public:
auto pan(Vector2 last_mouse, Vector2 mouse) -> void; auto pan(Vector2 last_mouse, Vector2 mouse) -> void;
auto update(const Vector3& current_target, const Vector3& mass_center, bool lock, bool mass_center_lock) -> void; auto update(const Vector3& current_target, const Vector3& mass_center, bool lock,
bool mass_center_lock) -> void;
}; };
#endif #endif

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
#ifndef RENDERER_HPP_ #ifndef RENDERER_HPP_
#define RENDERER_HPP_ #define RENDERER_HPP_
#include "config.hpp"
#include "orbit_camera.hpp" #include "orbit_camera.hpp"
#include "config.hpp"
#include "input_handler.hpp" #include "input_handler.hpp"
#include "state_manager.hpp" #include "state_manager.hpp"
#include "user_interface.hpp" #include "user_interface.hpp"
@ -14,73 +14,55 @@ class renderer
{ {
private: private:
const state_manager& state; const state_manager& state;
input_handler& input; const input_handler& input;
user_interface& gui; user_interface& gui;
const orbit_camera& camera; const orbit_camera& camera;
RenderTexture graph_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); RenderTexture render_target =
LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
// TODO: Those should be moved to the user_interface.h // TODO: Those should be moved to the user_interface.h
RenderTexture klotski_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); RenderTexture klotski_target =
LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
RenderTexture menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT); RenderTexture menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT);
// Edges // Batching
unsigned int edge_vao_id = 0;
unsigned int edge_vbo_id = 0;
std::vector<Vector3> edge_vertices;
Shader edge_shader = LoadShader("shader/edge_vertex.glsl", "shader/edge_fragment.glsl");
int edge_color_loc = -1;
std::vector<std::pair<Vector3, Vector3>> connections; std::vector<std::pair<Vector3, Vector3>> connections;
// Vertex instancing // Instancing
static constexpr int INSTANCE_COLOR_ATTR = 5; static constexpr int INSTANCE_COLOR_ATTR = 5;
std::vector<Matrix> transforms; std::vector<Matrix> transforms;
std::vector<Color> colors; std::vector<Color> colors;
Material vertex_mat = LoadMaterialDefault(); Material vertex_mat = LoadMaterialDefault();
Mesh cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE); Mesh cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE);
Shader instancing_shader = LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl"); Shader instancing_shader =
LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl");
unsigned int color_vbo_id = 0; unsigned int color_vbo_id = 0;
public: public:
// TODO: I am allocating HUGE vertex buffers instead of resizing dynamically... renderer(const orbit_camera& _camera, const state_manager& _state, const input_handler& _input,
// Edges: 5'000'000 * 2 * 12 Byte ~= 115 MB user_interface& _gui)
// Verts: 1'000'000 * 16 Byte ~= 15 MB
// This is also allocated on the CPU by the vectors
renderer(const orbit_camera& _camera, const state_manager& _state, input_handler& _input, user_interface& _gui)
: state(_state), input(_input), gui(_gui), camera(_camera) : state(_state), input(_input), gui(_gui), camera(_camera)
{ {
// Edges
edge_shader.locs[SHADER_LOC_VERTEX_POSITION] = GetShaderLocationAttrib(edge_shader, "vertexPosition");
edge_shader.locs[SHADER_LOC_MATRIX_MVP] = GetShaderLocation(edge_shader, "mvp");
edge_shader.locs[SHADER_LOC_COLOR_DIFFUSE] = GetShaderLocation(edge_shader, "colDiffuse");
edge_color_loc = GetShaderLocation(edge_shader, "colDiffuse");
edge_vertices.reserve(DRAW_EDGES_LIMIT * 2);
edge_vao_id = rlLoadVertexArray();
edge_vbo_id = rlLoadVertexBuffer(nullptr, DRAW_EDGES_LIMIT * 2 * sizeof(Vector3), true);
rlEnableVertexArray(edge_vao_id);
rlEnableVertexBuffer(edge_vbo_id);
rlSetVertexAttribute(0, 3, RL_FLOAT, false, sizeof(Vector3), 0);
rlEnableVertexAttribute(0);
rlDisableVertexBuffer();
rlDisableVertexArray();
// Vertex instancing
instancing_shader.locs[SHADER_LOC_MATRIX_MVP] = GetShaderLocation(instancing_shader, "mvp"); instancing_shader.locs[SHADER_LOC_MATRIX_MVP] = GetShaderLocation(instancing_shader, "mvp");
instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] = GetShaderLocationAttrib( instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] =
instancing_shader, GetShaderLocationAttrib(instancing_shader, "instanceTransform");
"instanceTransform"); instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] =
instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] = GetShaderLocation(instancing_shader, "viewPos"); GetShaderLocation(instancing_shader, "viewPos");
// infoln("LOC vertexPosition: {}",
// rlGetLocationAttrib(instancing_shader.id, "vertexPosition"));
// infoln("LOC instanceTransform: {}",
// rlGetLocationAttrib(instancing_shader.id, "instanceTransform"));
// infoln("LOC instanceColor: {}", rlGetLocationAttrib(instancing_shader.id, "instanceColor"));
// vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR;
vertex_mat.shader = instancing_shader; vertex_mat.shader = instancing_shader;
transforms.reserve(DRAW_VERTICES_LIMIT); transforms.reserve(DRAW_VERTICES_LIMIT);
colors.reserve(DRAW_VERTICES_LIMIT); colors.reserve(DRAW_VERTICES_LIMIT);
color_vbo_id = rlLoadVertexBuffer(nullptr, DRAW_VERTICES_LIMIT * sizeof(Color), true); color_vbo_id = rlLoadVertexBuffer(colors.data(), DRAW_VERTICES_LIMIT * sizeof(Color), true);
rlEnableVertexArray(cube_instance.vaoId); rlEnableVertexArray(cube_instance.vaoId);
rlEnableVertexBuffer(color_vbo_id); rlEnableVertexBuffer(color_vbo_id);
@ -92,27 +74,23 @@ public:
rlDisableVertexArray(); rlDisableVertexArray();
} }
NO_COPY_NO_MOVE(renderer); renderer(const renderer& copy) = delete;
auto operator=(const renderer& copy) -> renderer& = delete;
renderer(renderer&& move) = delete;
auto operator=(renderer&& move) -> renderer& = delete;
~renderer() ~renderer()
{ {
UnloadRenderTexture(graph_target); UnloadRenderTexture(render_target);
UnloadRenderTexture(klotski_target); UnloadRenderTexture(klotski_target);
UnloadRenderTexture(menu_target); UnloadRenderTexture(menu_target);
// Edges
rlUnloadVertexArray(edge_vao_id);
rlUnloadVertexBuffer(edge_vbo_id);
UnloadShader(edge_shader);
// Instancing // Instancing
UnloadMaterial(vertex_mat); UnloadMaterial(vertex_mat);
UnloadMesh(cube_instance); UnloadMesh(cube_instance);
// I think the shader already gets unloaded with the material? // I think the shader already gets unloaded with the material?
// UnloadShader(instancing_shader); // UnloadShader(instancing_shader);
rlUnloadVertexBuffer(color_vbo_id);
} }
private: private:
@ -124,7 +102,8 @@ private:
auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void; auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void;
public: public:
auto render(const std::vector<Vector3>& masses, int fps, int ups, size_t mass_count, size_t spring_count) -> void; auto render(const std::vector<Vector3>& masses, int fps, int ups, size_t mass_count,
size_t spring_count) -> void;
}; };
#endif #endif

View File

@ -2,17 +2,17 @@
#define STATE_MANAGER_HPP_ #define STATE_MANAGER_HPP_
#include "graph_distances.hpp" #include "graph_distances.hpp"
#include "load_save.hpp" #include "threaded_physics.hpp"
#include "cpu_layout_engine.hpp"
#include "puzzle.hpp" #include "puzzle.hpp"
#include <stack>
#include <boost/unordered/unordered_flat_map.hpp> #include <boost/unordered/unordered_flat_map.hpp>
#include <boost/unordered/unordered_flat_set.hpp> #include <boost/unordered/unordered_flat_set.hpp>
class state_manager class state_manager
{ {
private: private:
cpu_layout_engine& physics; threaded_physics& physics;
std::string preset_file; std::string preset_file;
size_t current_preset = 0; size_t current_preset = 0;
@ -22,9 +22,9 @@ private:
// State storage (store states twice for bidirectional lookup). // State storage (store states twice for bidirectional lookup).
// Everything else should only store indices to state_pool. // Everything else should only store indices to state_pool.
std::vector<puzzle> state_pool; // Indices are equal to mass_springs mass indices std::vector<puzzle> state_pool; // Indices are equal to mass_springs mass indices
puzzlemap<size_t> state_indices; // Maps states to indices boost::unordered_flat_map<puzzle, size_t, puzzle_hasher> state_indices; // Maps states to indices
std::vector<spring> links; // Indices are equal to mass_springs springs indices std::vector<std::pair<size_t, size_t>> links; // Indices are equal to mass_springs springs indices
graph_distances node_target_distances; // Buffered and reused if the graph doesn't change graph_distances node_target_distances; // Buffered and reused if the graph doesn't change
boost::unordered_flat_set<size_t> winning_indices; // Indices of all states where the board is solved boost::unordered_flat_set<size_t> winning_indices; // Indices of all states where the board is solved
@ -42,13 +42,17 @@ private:
bool edited = false; bool edited = false;
public: public:
state_manager(cpu_layout_engine& _physics, const std::string& _preset_file) state_manager(threaded_physics& _physics, const std::string& _preset_file)
: physics(_physics), preset_file(_preset_file) : physics(_physics)
{ {
reload_preset_file(); parse_preset_file(_preset_file);
load_preset(0);
} }
NO_COPY_NO_MOVE(state_manager); state_manager(const state_manager& copy) = delete;
auto operator=(const state_manager& copy) -> state_manager& = delete;
state_manager(state_manager&& move) = delete;
auto operator=(state_manager&& move) -> state_manager& = delete;
private: private:
/** /**
@ -80,7 +84,8 @@ private:
* @param states List of states to insert * @param states List of states to insert
* @param _links List of links to insert * @param _links List of links to insert
*/ */
auto synced_insert_statespace(const std::vector<puzzle>& states, const std::vector<spring>& _links) -> void; auto synced_insert_statespace(const std::vector<puzzle>& states,
const std::vector<std::pair<size_t, size_t>>& _links) -> void;
/** /**
* Clears all states and links (and related) from the state_manager and the physics system. * Clears all states and links (and related) from the state_manager and the physics system.
@ -90,13 +95,15 @@ private:
public: public:
// Presets // Presets
auto save_current_to_preset_file(const std::string& preset_comment) -> void;
auto reload_preset_file() -> void; auto parse_preset_file(const std::string& _preset_file) -> bool;
auto append_preset_file(const std::string& preset_name) -> bool;
auto load_preset(size_t preset) -> void; auto load_preset(size_t preset) -> void;
auto load_previous_preset() -> void; auto load_previous_preset() -> void;
auto load_next_preset() -> void; auto load_next_preset() -> void;
// Update current_state // Update current_state
auto update_current_state(const puzzle& p) -> void; auto update_current_state(const puzzle& p) -> void;
auto edit_starting_state(const puzzle& p) -> void; auto edit_starting_state(const puzzle& p) -> void;
auto goto_starting_state() -> void; auto goto_starting_state() -> void;
@ -106,6 +113,7 @@ public:
auto goto_closest_target_state() -> void; auto goto_closest_target_state() -> void;
// Update graph // Update graph
auto populate_graph() -> void; auto populate_graph() -> void;
auto clear_graph_and_add_current(const puzzle& p) -> void; auto clear_graph_and_add_current(const puzzle& p) -> void;
auto clear_graph_and_add_current() -> void; auto clear_graph_and_add_current() -> void;
@ -114,6 +122,7 @@ public:
auto populate_winning_path() -> void; auto populate_winning_path() -> void;
// Index mapping // Index mapping
[[nodiscard]] auto get_index(const puzzle& state) const -> size_t; [[nodiscard]] auto get_index(const puzzle& state) const -> size_t;
[[nodiscard]] auto get_current_index() const -> size_t; [[nodiscard]] auto get_current_index() const -> size_t;
[[nodiscard]] auto get_starting_index() const -> size_t; [[nodiscard]] auto get_starting_index() const -> size_t;
@ -126,7 +135,7 @@ public:
[[nodiscard]] auto get_target_count() const -> size_t; [[nodiscard]] auto get_target_count() const -> size_t;
[[nodiscard]] auto get_link_count() const -> size_t; [[nodiscard]] auto get_link_count() const -> size_t;
[[nodiscard]] auto get_path_length() const -> size_t; [[nodiscard]] auto get_path_length() const -> size_t;
[[nodiscard]] auto get_links() const -> const std::vector<spring>&; [[nodiscard]] auto get_links() const -> const std::vector<std::pair<size_t, size_t>>&;
[[nodiscard]] auto get_winning_indices() const -> const boost::unordered_flat_set<size_t>&; [[nodiscard]] auto get_winning_indices() const -> const boost::unordered_flat_set<size_t>&;
[[nodiscard]] auto get_visit_counts() const -> const boost::unordered_flat_map<size_t, int>&; [[nodiscard]] auto get_visit_counts() const -> const boost::unordered_flat_map<size_t, int>&;
[[nodiscard]] auto get_winning_path() const -> const std::vector<size_t>&; [[nodiscard]] auto get_winning_path() const -> const std::vector<size_t>&;
@ -137,7 +146,6 @@ public:
[[nodiscard]] auto get_current_preset_comment() const -> const std::string&; [[nodiscard]] auto get_current_preset_comment() const -> const std::string&;
[[nodiscard]] auto has_history() const -> bool; [[nodiscard]] auto has_history() const -> bool;
[[nodiscard]] auto has_distances() const -> bool; [[nodiscard]] auto has_distances() const -> bool;
[[nodiscard]] auto get_distances() const -> std::vector<int>;
[[nodiscard]] auto get_total_moves() const -> size_t; [[nodiscard]] auto get_total_moves() const -> size_t;
[[nodiscard]] auto was_edited() const -> bool; [[nodiscard]] auto was_edited() const -> bool;
}; };

View File

@ -1,10 +1,6 @@
#ifndef PHYSICS_HPP_ #ifndef PHYSICS_HPP_
#define PHYSICS_HPP_ #define PHYSICS_HPP_
#include "config.hpp"
#include "cpu_spring_system.hpp"
#include "util.hpp"
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
#include <mutex> #include <mutex>
@ -15,10 +11,13 @@
#include <variant> #include <variant>
#include <vector> #include <vector>
class cpu_layout_engine #ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
class threaded_physics
{ {
struct add_mass struct add_mass {};
{};
struct add_spring struct add_spring
{ {
@ -26,25 +25,24 @@ class cpu_layout_engine
size_t b; size_t b;
}; };
struct clear_graph struct clear_graph {};
{};
using command = std::variant<add_mass, add_spring, clear_graph>; using command = std::variant<add_mass, add_spring, clear_graph>;
struct physics_state struct physics_state
{ {
#ifdef TRACY #ifdef TRACY
TracyLockable(std::mutex, command_mtx); TracyLockable(std::mutex, command_mtx);
#else #else
std::mutex command_mtx; std::mutex command_mtx;
#endif #endif
std::queue<command> pending_commands; std::queue<command> pending_commands;
#ifdef TRACY #ifdef TRACY
TracyLockable(std::mutex, data_mtx); TracyLockable(std::mutex, data_mtx);
#else #else
std::mutex data_mtx; std::mutex data_mtx;
#endif #endif
std::condition_variable_any data_ready_cnd; std::condition_variable_any data_ready_cnd;
std::condition_variable_any data_consumed_cnd; std::condition_variable_any data_consumed_cnd;
Vector3 mass_center = Vector3Zero(); Vector3 mass_center = Vector3Zero();
@ -59,21 +57,21 @@ class cpu_layout_engine
}; };
private: private:
threadpool thread_pool;
std::thread physics; std::thread physics;
public: public:
physics_state state; physics_state state;
public: public:
explicit cpu_layout_engine( threaded_physics()
const threadpool _thread_pool = std::nullopt) : physics(physics_thread, std::ref(state)) {}
: thread_pool(_thread_pool), physics(physics_thread, std::ref(state), std::ref(thread_pool))
{}
NO_COPY_NO_MOVE(cpu_layout_engine); threaded_physics(const threaded_physics& copy) = delete;
auto operator=(const threaded_physics& copy) -> threaded_physics& = delete;
threaded_physics(threaded_physics&& move) = delete;
auto operator=(threaded_physics&& move) -> threaded_physics& = delete;
~cpu_layout_engine() ~threaded_physics()
{ {
state.running = false; state.running = false;
state.data_ready_cnd.notify_all(); state.data_ready_cnd.notify_all();
@ -82,19 +80,16 @@ public:
} }
private: private:
#ifdef ASYNC_OCTREE static auto physics_thread(physics_state& state) -> void;
static auto set_octree_pool_thread_name(size_t idx) -> void;
#endif
static auto physics_thread(physics_state& state,
threadpool thread_pool) -> void;
public: public:
auto clear_cmd() -> void;
auto add_mass_cmd() -> void; auto add_mass_cmd() -> void;
auto add_spring_cmd(size_t a, size_t b) -> void; auto add_spring_cmd(size_t a, size_t b) -> void;
auto add_mass_springs_cmd(size_t num_masses,
const std::vector<spring>& springs) -> void; auto clear_cmd() -> void;
auto add_mass_springs_cmd(size_t num_masses, const std::vector<std::pair<size_t, size_t>>& springs) -> void;
}; };
#endif #endif

View File

@ -1,5 +1,5 @@
#ifndef USER_INTERFACE_HPP_ #ifndef GUI_HPP_
#define USER_INTERFACE_HPP_ #define GUI_HPP_
#include "orbit_camera.hpp" #include "orbit_camera.hpp"
#include "config.hpp" #include "config.hpp"
@ -22,17 +22,15 @@ class user_interface
const int padding; const int padding;
public: public:
grid(const int _x, grid(const int _x, const int _y, const int _width, const int _height, const int _columns,
const int _y, const int _rows, const int _padding)
const int _width, : x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows),
const int _height, padding(_padding)
const int _columns, {}
const int _rows,
const int _padding)
: x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows), padding(_padding) {}
public: public:
auto update_bounds(int _x, int _y, int _width, int _height, int _columns, int _rows) -> void; auto update_bounds(int _x, int _y, int _width, int _height, int _columns, int _rows)
-> void;
auto update_bounds(int _x, int _y, int _width, int _height) -> void; auto update_bounds(int _x, int _y, int _width, int _height) -> void;
auto update_bounds(int _x, int _y) -> void; auto update_bounds(int _x, int _y) -> void;
@ -40,7 +38,8 @@ class user_interface
[[nodiscard]] auto bounds(int _x, int _y, int _width, int _height) const -> Rectangle; [[nodiscard]] auto bounds(int _x, int _y, int _width, int _height) const -> Rectangle;
[[nodiscard]] auto square_bounds() const -> Rectangle; [[nodiscard]] auto square_bounds() const -> Rectangle;
[[nodiscard]] auto square_bounds(int _x, int _y, int _width, int _height) const -> Rectangle; [[nodiscard]] auto square_bounds(int _x, int _y, int _width, int _height) const
-> Rectangle;
}; };
struct style struct style
@ -88,17 +87,14 @@ private:
grid menu_grid = grid(0, 0, GetScreenWidth(), MENU_HEIGHT, MENU_COLS, MENU_ROWS, MENU_PAD); grid menu_grid = grid(0, 0, GetScreenWidth(), MENU_HEIGHT, MENU_COLS, MENU_ROWS, MENU_PAD);
grid board_grid = grid(0, grid board_grid =
MENU_HEIGHT, grid(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT,
GetScreenWidth() / 2, state.get_current_state().get_width(), state.get_current_state().get_height(), BOARD_PADDING);
GetScreenHeight() - MENU_HEIGHT,
state.get_current_state().get_width(),
state.get_current_state().get_height(),
BOARD_PADDING);
grid graph_overlay_grid = grid(GetScreenWidth() / 2, MENU_HEIGHT, 200, 100, 1, 4, MENU_PAD); grid graph_overlay_grid = grid(GetScreenWidth() / 2, MENU_HEIGHT, 200, 100, 1, 4, MENU_PAD);
grid debug_overlay_grid = grid(GetScreenWidth() / 2, GetScreenHeight() - 75, 200, 75, 1, 3, MENU_PAD); grid debug_overlay_grid =
grid(GetScreenWidth() / 2, GetScreenHeight() - 75, 200, 75, 1, 3, MENU_PAD);
// Windows // Windows
@ -108,7 +104,7 @@ private:
bool ok_message = false; bool ok_message = false;
bool yes_no_message = false; bool yes_no_message = false;
bool save_window = false; bool save_window = false;
std::array<char, 256> preset_comment = {}; std::array<char, 256> preset_name = {};
bool help_window = false; bool help_window = false;
public: public:
@ -118,7 +114,10 @@ public:
init(); init();
} }
NO_COPY_NO_MOVE(user_interface); user_interface(const user_interface& copy) = delete;
auto operator=(const user_interface& copy) -> user_interface& = delete;
user_interface(user_interface&& move) = delete;
auto operator=(user_interface&& move) -> user_interface& = delete;
private: private:
static auto init() -> void; static auto init() -> void;
@ -134,68 +133,32 @@ private:
[[nodiscard]] static auto popup_bounds() -> Rectangle; [[nodiscard]] static auto popup_bounds() -> Rectangle;
auto draw_button(Rectangle bounds, auto draw_button(Rectangle bounds, const std::string& label, Color color, bool enabled = true,
const std::string& label,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int; int font_size = FONT_SIZE) const -> int;
auto draw_menu_button(int x, auto draw_menu_button(int x, int y, int width, int height, const std::string& label,
int y, Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int;
int width,
int height,
const std::string& label,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_toggle_slider(Rectangle bounds, auto draw_toggle_slider(Rectangle bounds, const std::string& off_label,
const std::string& off_label, const std::string& on_label, int* active, Color color,
const std::string& on_label, bool enabled = true, int font_size = FONT_SIZE) const -> int;
int* active,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_menu_toggle_slider(int x, auto draw_menu_toggle_slider(int x, int y, int width, int height, const std::string& off_label,
int y, const std::string& on_label, int* active, Color color,
int width, bool enabled = true, int font_size = FONT_SIZE) const -> int;
int height,
const std::string& off_label,
const std::string& on_label,
int* active,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_spinner(Rectangle bounds, auto draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max,
const std::string& label, Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int;
int* value,
int min,
int max,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_menu_spinner(int x, auto draw_menu_spinner(int x, int y, int width, int height, const std::string& label,
int y, int* value, int min, int max, Color color, bool enabled = true,
int width,
int height,
const std::string& label,
int* value,
int min,
int max,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int; int font_size = FONT_SIZE) const -> int;
auto draw_label(Rectangle bounds, auto draw_label(Rectangle bounds, const std::string& text, Color color, bool enabled = true,
const std::string& text,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int; int font_size = FONT_SIZE) const -> int;
auto draw_board_block(int x, int y, int width, int height, Color color, bool enabled = true) const -> bool; auto draw_board_block(int x, int y, int width, int height, Color color,
bool enabled = true) const -> bool;
[[nodiscard]] auto window_open() const -> bool; [[nodiscard]] auto window_open() const -> bool;

View File

@ -1,34 +1,67 @@
#ifndef UTIL_HPP_ #ifndef UTIL_HPP_
#define UTIL_HPP_ #define UTIL_HPP_
#include <vector>
#include <iostream> #include <iostream>
#include <raylib.h> #include <raylib.h>
#define INLINE __attribute__((always_inline)) // Bit shifting + masking
#define PACKED __attribute__((packed))
#define STARTTIME const auto start = std::chrono::high_resolution_clock::now() template <class T>
#define ENDTIME(msg, cast, unit) const auto end = std::chrono::high_resolution_clock::now(); \ requires std::unsigned_integral<T>
infoln("{}. Took {}{}.", msg, std::chrono::duration_cast<cast>(end - start).count(), unit) auto create_mask(const uint8_t first, const uint8_t last) -> T
{
// If the mask width is equal the type width return all 1s instead of shifting
// as shifting by type-width is undefined behavior.
if (static_cast<size_t>(last - first + 1) >= sizeof(T) * 8) {
return ~T{0};
}
#define COMMENT if (false) // Example: first=4, last=7, 7-4+1=4
// 1 << 4 = 0b00010000
// 32 - 1 = 0b00001111
// 31 << 4 = 0b11110000
// Subtracting 1 generates a consecutive mask.
return ((T{1} << (last - first + 1)) - 1) << first;
}
#define NO_COPY_NO_MOVE(typename) \ template <class T>
typename(const typename& copy) = delete; \ requires std::unsigned_integral<T>
auto operator=(const typename& copy) -> typename& = delete; \ auto clear_bits(T& bits, const uint8_t first, const uint8_t last) -> void
typename(typename&& move) = delete; \ {
auto operator=(typename&& move) -> typename& = delete; const T mask = create_mask<T>(first, last);
using u8 = uint8_t; bits = bits & ~mask;
using u16 = uint16_t; }
using u32 = uint32_t;
using u64 = uint64_t;
using i8 = int8_t; template <class T, class U>
using i16 = int16_t; requires std::unsigned_integral<T> && std::unsigned_integral<U>
using i32 = int32_t; auto set_bits(T& bits, const uint8_t first, const uint8_t last, const U value) -> void
using i64 = int64_t; {
const T mask = create_mask<T>(first, last);
// Example: first=4, last=6, value=0b1110, bits = 0b 01111110
// mask = 0b 01110000
// bits & ~mask = 0b 00001110
// value << 4 = 0b 11100000
// (value << 4) & mask = 0b 01100000
// (bits & ~mask) | (value << 4) & mask = 0b 01101110
// Insert position: ^^^
// First clear the bits, then | with the value positioned at the insertion point.
// The value may be larger than [first, last], extra bits are ignored.
bits = (bits & ~mask) | ((static_cast<T>(value) << first) & mask);
}
template <class T>
requires std::unsigned_integral<T>
auto get_bits(const T bits, const uint8_t first, const uint8_t last) -> T
{
const T mask = create_mask<T>(first, last);
// We can >> without sign extension because T is unsigned_integral
return (bits & mask) >> first;
}
// std::variant visitor
// https://en.cppreference.com/w/cpp/utility/variant/visit // https://en.cppreference.com/w/cpp/utility/variant/visit
template <class... Ts> template <class... Ts>
@ -37,21 +70,9 @@ struct overloads : Ts...
using Ts::operator()...; using Ts::operator()...;
}; };
inline auto binom(const int n, const int k) -> int
{
std::vector<int> solutions(k);
solutions[0] = n - k + 1;
for (int i = 1; i < k; ++i) {
solutions[i] = solutions[i - 1] * (n - k + 1 + i) / (i + 1);
}
return solutions[k - 1];
}
// Enums // Enums
enum dir : u8 enum direction
{ {
nor = 1 << 0, nor = 1 << 0,
eas = 1 << 1, eas = 1 << 1,
@ -59,9 +80,7 @@ enum dir : u8
wes = 1 << 3, wes = 1 << 3,
}; };
// Ansi enum ctrl
enum class ctrl : u8
{ {
reset = 0, reset = 0,
bold_bright = 1, bold_bright = 1,
@ -72,40 +91,30 @@ enum class ctrl : u8
inverse_off = 27 inverse_off = 27
}; };
enum class fg : u8 enum fg
{ {
black = 30, fg_black = 30,
red = 31, fg_red = 31,
green = 32, fg_green = 32,
yellow = 33, fg_yellow = 33,
blue = 34, fg_blue = 34,
magenta = 35, fg_magenta = 35,
cyan = 36, fg_cyan = 36,
white = 37 fg_white = 37
}; };
enum class bg : u8 enum bg
{ {
black = 40, bg_black = 40,
red = 41, bg_red = 41,
green = 42, bg_green = 42,
yellow = 43, bg_yellow = 43,
blue = 44, bg_blue = 44,
magenta = 45, bg_magenta = 45,
cyan = 46, bg_cyan = 46,
white = 47 bg_white = 47
}; };
inline auto ansi_bold_fg(const fg color) -> std::string
{
return std::format("\033[{};{}m", static_cast<int>(ctrl::bold_bright), static_cast<int>(color));
}
inline auto ansi_reset() -> std::string
{
return std::format("\033[{}m", static_cast<int>(ctrl::reset));
}
// Output // Output
inline auto operator<<(std::ostream& os, const Vector2& v) -> std::ostream& inline auto operator<<(std::ostream& os, const Vector2& v) -> std::ostream&
@ -120,37 +129,36 @@ inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream&
return os; return os;
} }
// std::println doesn't work with mingw inline auto ansi_bold_fg(const fg color) -> std::string
template <typename... Args>
auto traceln(std::format_string<Args...> fmt, Args&&... args) -> void
{ {
std::cout << std::format("[{}TRACE{}]: ", ansi_bold_fg(fg::cyan), ansi_reset()) << std::format( return std::format("\033[1;{}m", static_cast<int>(color));
fmt,
std::forward<Args>(args)...) << std::endl;
} }
inline auto ansi_reset() -> std::string
{
return "\033[0m";
}
// std::println doesn't work with mingw
template <typename... Args> template <typename... Args>
auto infoln(std::format_string<Args...> fmt, Args&&... args) -> void auto infoln(std::format_string<Args...> fmt, Args&&... args) -> void
{ {
std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg::blue), ansi_reset()) << std::format( std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg_blue), ansi_reset()) << std::format(
fmt, fmt, std::forward<Args>(args)...) << std::endl;
std::forward<Args>(args)...) << std::endl;
} }
template <typename... Args> template <typename... Args>
auto warnln(std::format_string<Args...> fmt, Args&&... args) -> void auto warnln(std::format_string<Args...> fmt, Args&&... args) -> void
{ {
std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg::yellow), ansi_reset()) << std::format( std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg_yellow), ansi_reset()) << std::format(
fmt, fmt, std::forward<Args>(args)...) << std::endl;
std::forward<Args>(args)...) << std::endl;
} }
template <typename... Args> template <typename... Args>
auto errln(std::format_string<Args...> fmt, Args&&... args) -> void auto errln(std::format_string<Args...> fmt, Args&&... args) -> void
{ {
std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg::red), ansi_reset()) << std::format( std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg_red), ansi_reset()) << std::format(
fmt, fmt, std::forward<Args>(args)...) << std::endl;
std::forward<Args>(args)...) << std::endl;
} }
#endif #endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 KiB

After

Width:  |  Height:  |  Size: 326 KiB

View File

@ -1,9 +0,0 @@
#version 330
uniform vec4 colDiffuse;
out vec4 finalColor;
void main()
{
finalColor = colDiffuse;
}

View File

@ -1,9 +0,0 @@
#version 330
in vec3 vertexPosition;
uniform mat4 mvp;
void main()
{
gl_Position = mvp * vec4(vertexPosition, 1.0);
}

View File

@ -1,15 +0,0 @@
#include "bits.hpp"
auto print_bitmap(const u64 bitmap, const u8 w, const u8 h, const std::string& title) -> void {
traceln("{}:", title);
traceln("{}", std::string(2 * w - 1, '='));
for (size_t y = 0; y < h; ++y) {
std::cout << " ";
for (size_t x = 0; x < w; ++x) {
std::cout << static_cast<int>(get_bits(bitmap, y * w + x, y * w + x)) << " ";
}
std::cout << "\n";
}
std::cout << std::flush;
traceln("{}", std::string(2 * w - 1, '='));
}

View File

@ -1,209 +0,0 @@
#include "cpu_spring_system.hpp"
#include "config.hpp"
#include <cfloat>
#include <cstring>
auto cpu_spring_system::clear() -> void
{
positions.clear();
previous_positions.clear();
velocities.clear();
forces.clear();
springs.clear();
tree.clear();
}
auto cpu_spring_system::add_mass() -> void
{
// Adding all positions to (0, 0, 0) breaks the octree
// Done when adding springs
// Vector3 position{
// static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100,
// 100)), static_cast<float>(GetRandomValue(-100, 100))
// };
// position = Vector3Scale(Vector3Normalize(position), REST_LENGTH * 2.0);
positions.emplace_back(Vector3Zero());
previous_positions.emplace_back(Vector3Zero());
velocities.emplace_back(Vector3Zero());
forces.emplace_back(Vector3Zero());
}
auto cpu_spring_system::add_spring(size_t a, size_t b) -> void
{
// Update masses to be located along a random walk when adding the springs
const Vector3& mass_a = positions[a];
const Vector3& mass_b = positions[b];
Vector3 offset{
static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100))
};
// By spawning the masses close together, we "explode" them naturally, so they cluster faster (also looks cool)
offset = Vector3Normalize(offset) * REST_LENGTH * 0.1;
// If the offset moves the mass closer to the current center of mass, flip it
if (!tree.empty()) {
const Vector3 mass_center_direction = Vector3Subtract(positions[a], tree.root().mass_center);
const float mass_center_distance = Vector3Length(mass_center_direction);
if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) {
offset = Vector3Negate(offset);
}
}
positions[b] = mass_a + offset;
previous_positions[b] = mass_b;
// infoln("Adding spring: ({}, {}, {})->({}, {}, {})", mass_a.position.x, mass_a.position.y,
// mass_a.position.z,
// mass_b.position.x, mass_b.position.y, mass_b.position.z);
springs.emplace_back(a, b);
}
auto cpu_spring_system::clear_forces() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
memset(forces.data(), 0, forces.size() * sizeof(Vector3));
}
auto cpu_spring_system::calculate_spring_force(const size_t s) -> void
{
const spring _s = springs[s];
const Vector3 a_pos = positions[_s.first];
const Vector3 b_pos = positions[_s.second];
const Vector3 a_vel = velocities[_s.first];
const Vector3 b_vel = velocities[_s.second];
const Vector3 delta_pos = a_pos - b_pos;
const Vector3 delta_vel = a_vel - b_vel;
const float sq_len = Vector3DotProduct(delta_pos, delta_pos);
const float inv_len = 1.0f / sqrt(sq_len);
const float len = sq_len * inv_len;
const float hooke = SPRING_K * (len - REST_LENGTH);
const float dampening = DAMPENING_K * Vector3DotProduct(delta_vel, delta_pos) * inv_len;
const Vector3 a_force = Vector3Scale(delta_pos, -(hooke + dampening) * inv_len);
const Vector3 b_force = a_force * -1.0f;
forces[_s.first] += a_force;
forces[_s.second] += b_force;
}
auto cpu_spring_system::calculate_spring_forces(const threadpool thread_pool) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
const auto solve_spring_force = [&](const int i)
{
calculate_spring_force(i);
};
if (thread_pool) {
(*thread_pool)->submit_loop(0, springs.size(), solve_spring_force, SMALL_TASK_BLOCK_SIZE).wait();
} else {
for (size_t i = 0; i < springs.size(); ++i) {
solve_spring_force(i);
}
}
}
auto cpu_spring_system::calculate_repulsion_forces(const threadpool thread_pool) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
const auto solve_octree = [&](const int i)
{
const Vector3 force = tree.calculate_force_morton(0, positions[i], i);
forces[i] += force;
};
// Calculate forces using Barnes-Hut
if (thread_pool) {
(*thread_pool)->submit_loop(0, positions.size(), solve_octree, LARGE_TASK_BLOCK_SIZE).wait();
} else {
for (size_t i = 0; i < positions.size(); ++i) {
solve_octree(i);
}
}
}
auto cpu_spring_system::integrate_velocity(const size_t m, const float dt) -> void
{
const Vector3 acc = forces[m] / MASS;
velocities[m] += acc * dt;
}
auto cpu_spring_system::integrate_position(const size_t m, const float dt) -> void
{
previous_positions[m] = positions[m];
positions[m] += velocities[m] * dt;
}
auto cpu_spring_system::verlet_update(const size_t m, const float dt) -> void
{
const Vector3 acc = (forces[m] / MASS) * dt * dt;
const Vector3 pos = positions[m];
Vector3 delta_pos = pos - previous_positions[m];
delta_pos *= 1.0 - VERLET_DAMPENING; // Minimal dampening
positions[m] += delta_pos + acc;
previous_positions[m] = pos;
}
auto cpu_spring_system::update(const float dt, const threadpool thread_pool) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
const auto update = [&](const int i)
{
verlet_update(i, dt);
};
if (thread_pool) {
(*thread_pool)->submit_loop(0, positions.size(), update, SMALL_TASK_BLOCK_SIZE).wait();
} else {
for (size_t i = 0; i < positions.size(); ++i) {
update(i);
}
}
}
auto cpu_spring_system::center_masses(const threadpool thread_pool) -> void
{
Vector3 mean = Vector3Zero();
for (const Vector3& pos : positions) {
mean += pos;
}
mean /= static_cast<float>(positions.size());
const auto center_mass = [&](const int i)
{
positions[i] -= mean;
};
if (thread_pool) {
(*thread_pool)->submit_loop(0, positions.size(), center_mass, SMALL_TASK_BLOCK_SIZE).wait();
} else {
for (size_t i = 0; i < positions.size(); ++i) {
center_mass(i);
}
}
}

View File

@ -2,6 +2,10 @@
#include <queue> #include <queue>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto graph_distances::clear() -> void auto graph_distances::clear() -> void
{ {
distances.clear(); distances.clear();
@ -15,7 +19,7 @@ auto graph_distances::empty() const -> bool
} }
auto graph_distances::calculate_distances(const size_t node_count, auto graph_distances::calculate_distances(const size_t node_count,
const std::vector<spring>& edges, const std::vector<std::pair<size_t, size_t>>& edges,
const std::vector<size_t>& targets) -> void const std::vector<size_t>& targets) -> void
{ {
// Build a list of adjacent nodes to speed up BFS // Build a list of adjacent nodes to speed up BFS

View File

@ -18,7 +18,6 @@ auto input_handler::init_handlers() -> void
register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::add_block); register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::add_block);
register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::start_add_block); register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::start_add_block);
register_mouse_pressed_handler(MOUSE_BUTTON_MIDDLE, &input_handler::place_goal); register_mouse_pressed_handler(MOUSE_BUTTON_MIDDLE, &input_handler::place_goal);
register_mouse_pressed_handler(MOUSE_BUTTON_MIDDLE, &input_handler::select_state);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::camera_start_rotate); register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::camera_start_rotate);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::remove_block); register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::remove_block);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::clear_add_block); register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::clear_add_block);
@ -43,7 +42,6 @@ auto input_handler::init_handlers() -> void
register_key_pressed_handler(KEY_C, &input_handler::clear_graph); register_key_pressed_handler(KEY_C, &input_handler::clear_graph);
register_key_pressed_handler(KEY_I, &input_handler::toggle_mark_solutions); register_key_pressed_handler(KEY_I, &input_handler::toggle_mark_solutions);
register_key_pressed_handler(KEY_O, &input_handler::toggle_connect_solutions); register_key_pressed_handler(KEY_O, &input_handler::toggle_connect_solutions);
register_key_pressed_handler(KEY_Z, &input_handler::toggle_color_by_distance);
register_key_pressed_handler(KEY_TAB, &input_handler::toggle_editing); register_key_pressed_handler(KEY_TAB, &input_handler::toggle_editing);
register_key_pressed_handler(KEY_F, &input_handler::toggle_restricted_movement); register_key_pressed_handler(KEY_F, &input_handler::toggle_restricted_movement);
@ -144,17 +142,12 @@ auto input_handler::camera_zoom() const -> void
auto input_handler::camera_fov() const -> void auto input_handler::camera_fov() const -> void
{ {
if (!mouse_in_graph_pane() || !IsKeyDown(KEY_LEFT_CONTROL)) { if (!mouse_in_graph_pane() || !IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_LEFT_SHIFT)) {
return; return;
} }
const float wheel = GetMouseWheelMove(); const float wheel = GetMouseWheelMove();
camera.fov -= wheel * FOV_SPEED;
if (IsKeyDown(KEY_LEFT_SHIFT)) {
camera.fov -= wheel * FOV_SPEED * FOV_MULTIPLIER;
} else {
camera.fov -= wheel * FOV_SPEED;
}
} }
auto input_handler::select_block() -> void auto input_handler::select_block() -> void
@ -208,7 +201,7 @@ auto input_handler::add_block() -> void
has_block_add_xy = false; has_block_add_xy = false;
} else if (current.covers(block_add_x, block_add_y, block_add_width, block_add_height)) { } else if (current.covers(block_add_x, block_add_y, block_add_width, block_add_height)) {
const std::optional<puzzle>& next = current.try_add_block( const std::optional<puzzle>& next = current.try_add_block(
block(block_add_x, block_add_y, block_add_width, block_add_height, false)); puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false));
if (next) { if (next) {
sel_x = block_add_x; sel_x = block_add_x;
@ -224,7 +217,7 @@ auto input_handler::add_block() -> void
auto input_handler::remove_block() -> void auto input_handler::remove_block() -> void
{ {
const puzzle& current = state.get_current_state(); const puzzle& current = state.get_current_state();
const std::optional<block>& b = current.try_get_block(hov_x, hov_y); const std::optional<puzzle::block>& b = current.try_get_block(hov_x, hov_y);
if (!editing || has_block_add_xy || !b) { if (!editing || has_block_add_xy || !b) {
return; return;
} }
@ -246,7 +239,7 @@ auto input_handler::remove_block() -> void
auto input_handler::place_goal() const -> void auto input_handler::place_goal() const -> void
{ {
const puzzle& current = state.get_current_state(); const puzzle& current = state.get_current_state();
if (!editing || !mouse_in_board_pane() || !current.covers(hov_x, hov_y)) { if (!editing || !current.covers(hov_x, hov_y)) {
return; return;
} }
@ -258,16 +251,6 @@ auto input_handler::place_goal() const -> void
state.edit_starting_state(*next); state.edit_starting_state(*next);
} }
auto input_handler::select_state() const -> void
{
if (!mouse_in_graph_pane() || collision_mass == static_cast<size_t>(-1)) {
return;
}
const puzzle& selected = state.get_state(collision_mass);
state.update_current_state(selected);
}
auto input_handler::toggle_camera_lock() -> void auto input_handler::toggle_camera_lock() -> void
{ {
if (!camera_lock) { if (!camera_lock) {
@ -295,7 +278,7 @@ auto input_handler::toggle_camera_projection() const -> void
auto input_handler::move_block_nor() -> void auto input_handler::move_block_nor() -> void
{ {
const puzzle& current = state.get_current_state(); const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_move_block_at(sel_x, sel_y, nor); const std::optional<puzzle>& next = current.try_move_block_at_fast(sel_x, sel_y, nor);
if (!next) { if (!next) {
return; return;
} }
@ -307,7 +290,7 @@ auto input_handler::move_block_nor() -> void
auto input_handler::move_block_wes() -> void auto input_handler::move_block_wes() -> void
{ {
const puzzle& current = state.get_current_state(); const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_move_block_at(sel_x, sel_y, wes); const std::optional<puzzle>& next = current.try_move_block_at_fast(sel_x, sel_y, wes);
if (!next) { if (!next) {
return; return;
} }
@ -319,7 +302,7 @@ auto input_handler::move_block_wes() -> void
auto input_handler::move_block_sou() -> void auto input_handler::move_block_sou() -> void
{ {
const puzzle& current = state.get_current_state(); const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_move_block_at(sel_x, sel_y, sou); const std::optional<puzzle>& next = current.try_move_block_at_fast(sel_x, sel_y, sou);
if (!next) { if (!next) {
return; return;
} }
@ -331,7 +314,7 @@ auto input_handler::move_block_sou() -> void
auto input_handler::move_block_eas() -> void auto input_handler::move_block_eas() -> void
{ {
const puzzle& current = state.get_current_state(); const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_move_block_at(sel_x, sel_y, eas); const std::optional<puzzle>& next = current.try_move_block_at_fast(sel_x, sel_y, eas);
if (!next) { if (!next) {
return; return;
} }
@ -424,11 +407,6 @@ auto input_handler::toggle_connect_solutions() -> void
connect_solutions = !connect_solutions; connect_solutions = !connect_solutions;
} }
auto input_handler::toggle_color_by_distance() -> void
{
color_by_distance = !color_by_distance;
}
auto input_handler::toggle_mark_path() -> void auto input_handler::toggle_mark_path() -> void
{ {
mark_path = !mark_path; mark_path = !mark_path;

View File

@ -1,82 +0,0 @@
#include "load_save.hpp"
#include <fstream>
auto parse_preset_file(const std::string& preset_file) -> std::pair<std::vector<puzzle>, std::vector<std::string>>
{
std::fstream file(preset_file, std::ios::in);
if (!file) {
infoln("Preset file \"{}\" couldn't be opened.", preset_file);
return {};
}
std::string line;
std::vector<std::string> comment_lines;
std::vector<std::string> preset_lines;
while (std::getline(file, line)) {
if (line.starts_with("S")) {
preset_lines.push_back(line);
} else if (line.starts_with("#")) {
comment_lines.push_back(line);
}
}
if (preset_lines.empty() || comment_lines.size() != preset_lines.size()) {
infoln("Preset file \"{}\" couldn't be opened.", preset_file);
return {};
}
std::vector<puzzle> preset_states;
for (const auto& preset : preset_lines) {
// Each char is a bit
const puzzle& p = puzzle(preset);
if (const std::optional<std::string>& reason = p.try_get_invalid_reason()) {
infoln("Preset file \"{}\" contained invalid presets: {}", preset_file, *reason);
return {};
}
preset_states.emplace_back(p);
}
infoln("Loaded {} presets from \"{}\".", preset_lines.size(), preset_file);
return {preset_states, comment_lines};
}
auto append_preset_file(const std::string& preset_file, const std::string& preset_name, const puzzle& p) -> bool
{
infoln(R"(Saving preset "{}" to "{}")", preset_name, preset_file);
if (p.try_get_invalid_reason()) {
return false;
}
std::fstream file(preset_file, std::ios_base::app | std::ios_base::out);
if (!file) {
infoln("Preset file \"{}\" couldn't be opened.", preset_file);
return false;
}
file << "\n# " << preset_name << "\n" << p.string_repr() << std::flush;
return true;
}
auto append_preset_file_quiet(const std::string& preset_file,
const std::string& preset_name,
const puzzle& p,
const bool validate) -> bool
{
if (validate && p.try_get_invalid_reason()) {
return false;
}
std::fstream file(preset_file, std::ios_base::app | std::ios_base::out);
if (!file) {
return false;
}
file << "\n# " << preset_name << "\n" << p.string_repr() << std::flush;
return true;
}

View File

@ -1,64 +1,78 @@
#include <chrono>
#include <mutex>
#include <raylib.h>
#include "config.hpp" #include "config.hpp"
#include "input_handler.hpp" #include "input_handler.hpp"
#include "cpu_layout_engine.hpp" #include "mass_spring_system.hpp"
#include "threaded_physics.hpp"
#include "renderer.hpp" #include "renderer.hpp"
#include "state_manager.hpp" #include "state_manager.hpp"
#include "user_interface.hpp" #include "user_interface.hpp"
#include <chrono> #ifdef TRACY
#include <mutex> #include <tracy/Tracy.hpp>
#include <GL/glew.h>
#include <raylib.h>
#include <filesystem>
#if not defined(_WIN32)
#include <boost/program_options.hpp>
namespace po = boost::program_options;
#endif #endif
// Threadpool setup // TODO: Add some popups (my split between input.cpp/gui.cpp makes this ugly)
#ifdef THREADPOOL // - Clear graph: Notify that this will clear the visited states and move
auto set_pool_thread_name(size_t idx) -> void // history
// - Reset state: Notify that this will reset the move count
// TODO: Reduce memory usage
// - The memory model of the puzzle board is terrible (bitboards?)
// TODO: Improve solver
// - Move discovery is terrible
// - Instead of trying each direction for each block, determine the
// possible moves more efficiently (requires a different memory model)
// - Implement state discovery/enumeration
// - Find all possible initial board states (single one for each
// possible statespace). Currently wer're just finding all states
// given the initial state
// - Would allow to generate random puzzles with a certain move count
// TODO: Move selection accordingly when undoing moves (need to diff two states
// and get the moved blocks)
// TODO: Click states in the graph to display them in the board
// NOTE: Tracy uses a huge amount of memory. For longer testing disable Tracy.
// For profiling explore_state_space
auto main2(int argc, char* argv[]) -> int
{ {
BS::this_thread::set_os_thread_name(std::format("worker-{}", idx)); const puzzle p = puzzle(
"S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}]");
for (int i = 0; i < 50; ++i) {
auto space = p.explore_state_space();
}
return 0;
} }
BS::thread_pool<> threads(std::thread::hardware_concurrency() - 2, set_pool_thread_name); auto main(int argc, char* argv[]) -> int
constexpr threadpool thread_pool = &threads;
#else
constexpr threadpool thread_pool = std::nullopt;
#endif
// Argparse defaults
std::string preset_file = "default.puzzle";
std::string output_file = "clusters.puzzle";
int max_blocks = 5;
int min_moves = 10;
// Puzzle space setup
int board_width;
int board_height;
int goal_x;
int goal_y;
bool restricted;
blockset2 permitted_blocks;
block target_block;
std::tuple<u8, u8, u8, u8> target_block_pos_range;
// TODO: Export cluster to graphviz
// TODO: Fix naming:
// - Target: The block that has to leave the board to win
// - Goal: The opening in the board for the target
// - Puzzle (not board or state): A puzzle configuration (width, height, goal_x, goal_y, restricted, goal)
// - Block: A puzzle block (x, y, width, height, target, immovable)
// - Puzzle State: A specific puzzle state (width, height, goal_x, goal_y, restricted, goal, blocks)
// - Cluster: A graph of puzzle states connected by moves, generated from a specific Puzzle State
// - Puzzle Space: A number of Clusters generated from a generic Puzzle
// TODO: Add state space generation time to debug overlay
// TODO: Move selection accordingly when undoing moves (need to diff two states and get the moved blocks)
auto ui_mode() -> int
{ {
std::string preset_file;
if (argc != 2) {
preset_file = "default.puzzle";
} else {
preset_file = argv[1];
}
#ifdef BACKWARD
infoln("Backward stack-traces enabled.");
#else
infoln("Backward stack-traces disabled.");
#endif
#ifdef TRACY
infoln("Tracy adapter enabled.");
#else
infoln("Tracy adapter disabled.");
#endif
// RayLib window setup // RayLib window setup
SetTraceLogLevel(LOG_ERROR); SetTraceLogLevel(LOG_ERROR);
SetConfigFlags(FLAG_VSYNC_HINT); SetConfigFlags(FLAG_VSYNC_HINT);
@ -67,15 +81,8 @@ auto ui_mode() -> int
SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN); SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN);
InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings"); InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings");
// GLEW setup
glewExperimental = GL_TRUE;
const GLenum glew_err = glewInit();
if (glew_err != GLEW_OK) {
TraceLog(LOG_FATAL, "Failed to initialize GLEW: %s", glewGetErrorString(glew_err));
}
// Game setup // Game setup
cpu_layout_engine physics(thread_pool); threaded_physics physics;
state_manager state(physics, preset_file); state_manager state(physics, preset_file);
orbit_camera camera; orbit_camera camera;
input_handler input(state, camera); input_handler input(state, camera);
@ -143,8 +150,8 @@ auto ui_mode() -> int
// Update the camera after the physics, so target lock is smooth // Update the camera after the physics, so target lock is smooth
size_t current_index = state.get_current_index(); size_t current_index = state.get_current_index();
if (masses.size() > current_index) { if (masses.size() > current_index) {
const Vector3& current_mass = masses[current_index]; const mass_spring_system::mass& current_mass = mass_spring_system::mass(masses.at(current_index));
camera.update(current_mass, mass_center, input.camera_lock, input.camera_mass_center_lock); camera.update(current_mass.position, mass_center, input.camera_lock, input.camera_mass_center_lock);
} }
// Rendering // Rendering
@ -159,248 +166,11 @@ auto ui_mode() -> int
++loop_iterations; ++loop_iterations;
#ifdef TRACY #ifdef TRACY
FrameMark; FrameMark; FrameMarkEnd("MainThread");
FrameMarkEnd("MainThread");
#endif #endif
} }
CloseWindow(); CloseWindow();
return 0; return 0;
}
auto rush_hour_puzzle_space() -> void
{
board_width = 6;
board_height = 6;
goal_x = 4;
goal_y = 2;
restricted = true;
permitted_blocks = {
block(0, 0, 2, 1, false, false),
block(0, 0, 3, 1, false, false),
block(0, 0, 1, 2, false, false),
block(0, 0, 1, 3, false, false)
};
target_block = block(0, 0, 2, 1, true, false);
target_block_pos_range = {0, goal_y, board_width - target_block.get_width(), goal_y};
}
auto klotski_puzzle_space() -> void
{
board_width = 4;
board_height = 5;
goal_x = 1;
goal_y = 3;
restricted = false;
permitted_blocks = {
block(0, 0, 1, 1, false, false),
block(0, 0, 1, 2, false, false),
block(0, 0, 2, 1, false, false),
};
target_block = block(0, 0, 2, 2, true, false);
target_block_pos_range = {
0,
0,
board_width - target_block.get_width(),
board_height - target_block.get_height(),
};
}
auto puzzle_space() -> int
{
// We don't only pick max_blocks out of n (with duplicates), but also 1 out of n, 2, 3, ... max_blocks-1 out of n
int upper_set_count = 0;
for (int i = 1; i <= max_blocks; ++i) {
upper_set_count += binom(permitted_blocks.size() + i - 1, i);
}
infoln("Exploring puzzle space:");
infoln("- Size: {}x{}", board_width, board_height);
infoln("- Goal: {},{}", goal_x, goal_y);
infoln("- Restricted: {}", restricted);
infoln("- Max Blocks: {}", max_blocks);
infoln("- Min Moves: {}", min_moves);
infoln("- Target: {}x{}", target_block.get_width(), target_block.get_height());
infoln("- Max Sets: {}", upper_set_count);
infoln("- Permitted block sizes:");
std::cout << " ";
for (const block b : permitted_blocks) {
std::cout << std::format(" {}x{},", b.get_width(), b.get_height());
}
std::cout << std::endl;
const puzzle p = puzzle(board_width, board_height, goal_x, goal_y, restricted, true);
STARTTIME;
const puzzleset result = p.explore_puzzle_space(
permitted_blocks,
target_block,
target_block_pos_range,
max_blocks,
min_moves,
thread_pool);
ENDTIME(std::format("Found {} different clusters", result.size()), std::chrono::seconds, "s");
// TODO: The exported clusters are the numerically smallest state of the cluster.
// Not the state with the longest path.
infoln("Sorting clusters...");
std::vector<puzzle> result_sorted{result.begin(), result.end()};
std::ranges::sort(result_sorted, std::ranges::greater{});
infoln("Saving clusters...");
size_t i = 0;
size_t success = 0;
std::filesystem::remove(output_file);
for (const puzzle& _p : result_sorted) {
if (append_preset_file_quiet(output_file, std::format("Cluster {}", i), _p, true)) {
++success;
}
++i;
}
if (success != result_sorted.size()) {
warnln("Saved {} of {} clusters", success, result_sorted.size());
} else {
infoln("Saved {} of {} clusters", success, result_sorted.size());
}
return 0;
}
enum class runmode
{
USER_INTERFACE,
RUSH_HOUR_PUZZLE_SPACE,
KLOTSKI_PUZZLE_SPACE,
EXIT,
};
auto argparse(const int argc, char* argv[]) -> runmode
{
#if not defined(_WIN32)
po::options_description desc("Allowed options");
desc.add_options() //
("help", "produce help message") //
("presets", po::value<std::string>()->default_value(preset_file), "load presets from file") //
("output", po::value<std::string>()->default_value(output_file), "output file for generated clusters") //
("space", po::value<std::string>()->value_name("rh|klotski"), "generate puzzle space with ruleset") //
// ("w", po::value<int>()->default_value(board_width)->value_name("[3, 8]"), "board width") //
// ("h", po::value<int>()->default_value(board_height)->value_name("[3, 8"), "board height") //
// ("gx", po::value<int>()->default_value(goal_x)->value_name("[0, w-1]"), "board goal horizontal position") //
// ("gy", po::value<int>()->default_value(goal_y)->value_name("[0, h-1]"), "board goal vertical position") //
// ("free", "allow free block movement") //
("blocks",
po::value<int>()->default_value(max_blocks)->value_name("[1, 15]"),
"block limit for puzzle space generation") //
("moves",
po::value<int>()->default_value(min_moves),
"only save puzzles with at least this many required moves") //
;
po::positional_options_description positional;
positional.add("presets", -1);
po::variables_map vm;
po::store(po::command_line_parser(argc, argv).options(desc).positional(positional).run(), vm);
po::notify(vm);
if (vm.contains("help")) {
std::cout << desc << std::endl;
return runmode::EXIT;
}
if (vm.contains("output")) {
output_file = vm["output"].as<std::string>();
}
// if (vm.contains("w")) {
// board_width = vm["w"].as<int>();
// board_width = std::max(static_cast<int>(puzzle::MIN_WIDTH),
// std::min(board_width, static_cast<int>(puzzle::MAX_WIDTH)));
// }
// if (vm.contains("h")) {
// board_height = vm["h"].as<int>();
// board_height = std::max(static_cast<int>(puzzle::MIN_HEIGHT),
// std::min(board_height, static_cast<int>(puzzle::MAX_HEIGHT)));
// }
// if (vm.contains("gx")) {
// goal_x = vm["gx"].as<int>();
// goal_x = std::max(0, std::min(goal_x, static_cast<int>(puzzle::MAX_WIDTH) - 1));
// }
// if (vm.contains("gy")) {
// goal_y = vm["gy"].as<int>();
// goal_y = std::max(0, std::min(goal_y, static_cast<int>(puzzle::MAX_HEIGHT) - 1));
// }
// if (vm.contains("free")) {
// restricted = false;
// }
if (vm.contains("blocks")) {
max_blocks = vm["blocks"].as<int>();
max_blocks = std::max(1, std::min(max_blocks, static_cast<int>(puzzle::MAX_BLOCKS)));
}
if (vm.contains("moves")) {
min_moves = vm["moves"].as<int>();
min_moves = std::max(0, min_moves);
}
if (vm.contains("space")) {
const std::string ruleset = vm["space"].as<std::string>();
if (ruleset == "rh") {
return runmode::RUSH_HOUR_PUZZLE_SPACE;
}
if (ruleset == "klotski") {
return runmode::KLOTSKI_PUZZLE_SPACE;
}
}
if (vm.contains("presets")) {
preset_file = vm["presets"].as<std::string>();
}
#endif
return runmode::USER_INTERFACE;
}
auto main(const int argc, char* argv[]) -> int
{
#ifdef BACKWARD
infoln("Backward stack-traces enabled.");
#else
infoln("Backward stack-traces disabled.");
#endif
#ifdef TRACY
infoln("Tracy adapter enabled.");
#else
infoln("Tracy adapter disabled.");
#endif
infoln("Using background thread for physics.");
infoln("Using linear octree + Barnes-Hut for graph layout.");
#ifdef ASYNC_OCTREE
infoln("Using asynchronous octree build.");
#else
infoln("Using synchronous octree build.");
#endif
#ifdef THREADPOOL
infoln("Additional thread-pool enabled ({} threads).", threads.get_thread_count());
#else
infoln("Additional thread-pool disabled.");
#endif
switch (argparse(argc, argv)) {
case runmode::USER_INTERFACE:
return ui_mode();
case runmode::RUSH_HOUR_PUZZLE_SPACE:
rush_hour_puzzle_space();
return puzzle_space();
case runmode::KLOTSKI_PUZZLE_SPACE:
klotski_puzzle_space();
return puzzle_space();
case runmode::EXIT:
return 0;
};
return 1;
} }

231
src/mass_spring_system.cpp Normal file
View File

@ -0,0 +1,231 @@
#include "mass_spring_system.hpp"
#include "config.hpp"
#include <cfloat>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto mass_spring_system::mass::clear_force() -> void
{
force = Vector3Zero();
}
auto mass_spring_system::mass::calculate_velocity(const float delta_time) -> void
{
const Vector3 acceleration = Vector3Scale(force, 1.0 / MASS);
const Vector3 temp = Vector3Scale(acceleration, delta_time);
velocity = Vector3Add(velocity, temp);
}
auto mass_spring_system::mass::calculate_position(const float delta_time) -> void
{
previous_position = position;
const Vector3 temp = Vector3Scale(velocity, delta_time);
position = Vector3Add(position, temp);
}
auto mass_spring_system::mass::verlet_update(const float delta_time) -> void
{
const Vector3 acceleration = Vector3Scale(force, 1.0 / MASS);
const Vector3 temp_position = position;
Vector3 displacement = Vector3Subtract(position, previous_position);
const Vector3 accel_term = Vector3Scale(acceleration, delta_time * delta_time);
// Minimal dampening
displacement = Vector3Scale(displacement, 1.0 - VERLET_DAMPENING);
position = Vector3Add(Vector3Add(position, displacement), accel_term);
previous_position = temp_position;
}
auto mass_spring_system::spring::calculate_spring_force(mass& _a, mass& _b) -> void
{
// TODO: Use a bungee force here instead of springs, since we already have global repulsion?
const Vector3 delta_position = Vector3Subtract(_a.position, _b.position);
const float current_length = Vector3Length(delta_position);
const Vector3 delta_velocity = Vector3Subtract(_a.velocity, _b.velocity);
const float hooke = SPRING_CONSTANT * (current_length - REST_LENGTH);
const float dampening = DAMPENING_CONSTANT * Vector3DotProduct(delta_velocity, delta_position) / current_length;
const Vector3 force_a = Vector3Scale(delta_position, -(hooke + dampening) / current_length);
const Vector3 force_b = Vector3Scale(force_a, -1.0);
_a.force = Vector3Add(_a.force, force_a);
_b.force = Vector3Add(_b.force, force_b);
}
auto mass_spring_system::clear() -> void
{
masses.clear();
springs.clear();
tree.nodes.clear();
}
auto mass_spring_system::add_mass() -> void
{
// Adding all positions to (0, 0, 0) breaks the octree
// Done when adding springs
// Vector3 position{
// static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100,
// 100)), static_cast<float>(GetRandomValue(-100, 100))
// };
// position = Vector3Scale(Vector3Normalize(position), REST_LENGTH * 2.0);
masses.emplace_back(Vector3Zero());
}
auto mass_spring_system::add_spring(size_t a, size_t b) -> void
{
// Update masses to be located along a random walk when adding the springs
const mass& mass_a = masses.at(a);
mass& mass_b = masses.at(b);
Vector3 offset{
static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100))
};
offset = Vector3Normalize(offset) * REST_LENGTH;
// If the offset moves the mass closer to the current center of mass, flip it
if (!tree.nodes.empty()) {
const Vector3 mass_center_direction = Vector3Subtract(mass_a.position, tree.nodes.at(0).mass_center);
const float mass_center_distance = Vector3Length(mass_center_direction);
if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) {
offset = Vector3Negate(offset);
}
}
mass_b.position = mass_a.position + offset;
mass_b.previous_position = mass_b.position;
// infoln("Adding spring: ({}, {}, {})->({}, {}, {})", mass_a.position.x, mass_a.position.y,
// mass_a.position.z,
// mass_b.position.x, mass_b.position.y, mass_b.position.z);
springs.emplace_back(a, b);
}
auto mass_spring_system::clear_forces() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
for (auto& m : masses) {
m.clear_force();
}
}
auto mass_spring_system::calculate_spring_forces() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
for (const auto s : springs) {
mass& a = masses.at(s.a);
mass& b = masses.at(s.b);
spring::calculate_spring_force(a, b);
}
}
#ifdef THREADPOOL
auto mass_spring_system::set_thread_name(size_t idx) -> void
{
BS::this_thread::set_os_thread_name(std::format("bh-worker-{}", idx));
}
#endif
auto mass_spring_system::build_octree() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
tree.nodes.clear();
tree.nodes.reserve(masses.size() * 2);
// Compute bounding box around all masses
Vector3 min{FLT_MAX, FLT_MAX, FLT_MAX};
Vector3 max{-FLT_MAX, -FLT_MAX, -FLT_MAX};
for (const auto& m : masses) {
min.x = std::min(min.x, m.position.x);
max.x = std::max(max.x, m.position.x);
min.y = std::min(min.y, m.position.y);
max.y = std::max(max.y, m.position.y);
min.z = std::min(min.z, m.position.z);
max.z = std::max(max.z, m.position.z);
}
// Pad the bounding box
constexpr float pad = 1.0;
min = Vector3Subtract(min, Vector3Scale(Vector3One(), pad));
max = Vector3Add(max, Vector3Scale(Vector3One(), pad));
// Make it cubic (so subdivisions are balanced)
const float max_extent = std::max({max.x - min.x, max.y - min.y, max.z - min.z});
max = Vector3Add(min, Vector3Scale(Vector3One(), max_extent));
// Root node spans the entire area
const int root = tree.create_empty_leaf(min, max);
for (size_t i = 0; i < masses.size(); ++i) {
tree.insert(root, static_cast<int>(i), masses[i].position, MASS, 0);
}
}
auto mass_spring_system::calculate_repulsion_forces() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
build_octree();
auto solve_octree = [&](const int i)
{
const Vector3 force = tree.calculate_force(0, masses[i].position);
masses[i].force = Vector3Add(masses[i].force, force);
};
// Calculate forces using Barnes-Hut
#ifdef THREADPOOL
const BS::multi_future<void> loop_future = threads.submit_loop(0, masses.size(), solve_octree, 256);
loop_future.wait();
#else
for (size_t i = 0; i < masses.size(); ++i) {
solve_octree(i);
}
#endif
}
auto mass_spring_system::verlet_update(const float delta_time) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
for (auto& m : masses) {
m.verlet_update(delta_time);
}
}
auto mass_spring_system::center_masses() -> void
{
Vector3 mean = Vector3Zero();
for (const auto& m : masses) {
mean += m.position;
}
mean /= static_cast<float>(masses.size());
for (auto& m : masses) {
m.position -= mean;
}
}

View File

@ -1,335 +1,193 @@
#include "octree.hpp" #include "octree.hpp"
#include "config.hpp" #include "config.hpp"
#include "util.hpp"
#include <cfloat>
#include <raymath.h> #include <raymath.h>
auto octree::clear() -> void #ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto octree::node::child_count() const -> int
{ {
nodes.clear(); int child_count = 0;
for (const int child : children) {
if (child != -1) {
++child_count;
}
}
return child_count;
} }
auto octree::reserve(const size_t count) -> void auto octree::create_empty_leaf(const Vector3& box_min, const Vector3& box_max) -> int
{ {
nodes.reserve(count); node n;
n.box_min = box_min;
n.box_max = box_max;
nodes.emplace_back(n);
return static_cast<int>(nodes.size() - 1);
} }
auto octree::empty() const -> bool auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int
{ {
return nodes.empty(); const node& n = nodes[node_idx];
auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f,
(n.box_min.z + n.box_max.z) / 2.0f);
// The octant is encoded as a 3-bit integer "zyx". The node area is split
// along all 3 axes, if a position is right of an axis, this bit is set to 1.
// If a position is right of the x-axis and y-axis and left of the z-axis, the
// encoded octant is "011".
int octant = 0;
if (pos.x >= cx) {
octant |= 1;
}
if (pos.y >= cy) {
octant |= 2;
}
if (pos.z >= cz) {
octant |= 4;
}
return octant;
} }
auto octree::root() const -> const node& auto octree::get_child_bounds(const int node_idx, const int octant) const -> std::pair<Vector3, Vector3>
{ {
return nodes[0]; const node& n = nodes[node_idx];
auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f,
(n.box_min.z + n.box_max.z) / 2.0f);
Vector3 min = Vector3Zero();
Vector3 max = Vector3Zero();
// If (octant & 1), the octant is to the right of the node region's x-axis
// (see GetOctant). This means the left bound is the x-axis and the right
// bound the node's region max.
min.x = octant & 1 ? cx : n.box_min.x;
max.x = octant & 1 ? n.box_max.x : cx;
min.y = octant & 2 ? cy : n.box_min.y;
max.y = octant & 2 ? n.box_max.y : cy;
min.z = octant & 4 ? cz : n.box_min.z;
max.z = octant & 4 ? n.box_max.z : cz;
return std::make_pair(min, max);
} }
// Replaced the 50 line recursive octree insertion with this morton bitch to gain 5 UPS, FML auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, const float mass,
auto octree::build_octree_morton(octree& t, const int depth) -> void
const std::vector<Vector3>& positions,
const std::optional<BS::thread_pool<>*>& thread_pool) -> void
{ {
#ifdef TRACY // infoln("Inserting position ({}, {}, {}) into octree at node {} (depth {})", pos.x, pos.y,
ZoneScoped; // pos.z, node_idx, depth);
#endif if (depth > MAX_DEPTH) {
throw std::runtime_error(std::format("MAX_DEPTH! node={} box_min=({},{},{}) box_max=({},{},{}) pos=({},{},{})",
node_idx, nodes[node_idx].box_min.x, nodes[node_idx].box_min.y,
nodes[node_idx].box_min.z, nodes[node_idx].box_max.x,
nodes[node_idx].box_max.y, nodes[node_idx].box_max.z, pos.x, pos.y,
pos.z));
}
t.clear(); // NOTE: Do not store a nodes[node_idx] reference as the nodes vector might reallocate during
if (positions.empty()) { // this function
// We can place the particle in the empty leaf
if (nodes[node_idx].leaf && nodes[node_idx].mass_id == -1) {
nodes[node_idx].mass_id = mass_id;
nodes[node_idx].mass_center = pos;
nodes[node_idx].mass_total = mass;
return; return;
} }
// Compute bounding box around all masses // The leaf is occupied, we need to subdivide
Vector3 root_min{FLT_MAX, FLT_MAX, FLT_MAX}; if (nodes[node_idx].leaf) {
Vector3 root_max{-FLT_MAX, -FLT_MAX, -FLT_MAX}; const int existing_id = nodes[node_idx].mass_id;
for (const auto& [x, y, z] : positions) { const Vector3 existing_pos = nodes[node_idx].mass_center;
root_min.x = std::min(root_min.x, x); const float existing_mass = nodes[node_idx].mass_total;
root_max.x = std::max(root_max.x, x);
root_min.y = std::min(root_min.y, y);
root_max.y = std::max(root_max.y, y);
root_min.z = std::min(root_min.z, z);
root_max.z = std::max(root_max.z, z);
}
constexpr float pad = 1.0f; // If positions are identical we jitter the particles
root_min = Vector3Subtract(root_min, Vector3Scale(Vector3One(), pad)); const Vector3 diff = Vector3Subtract(pos, existing_pos);
root_max = Vector3Add(root_max, Vector3Scale(Vector3One(), pad)); if (diff == Vector3Zero()) {
// warnln("Trying to insert an identical partical into octree (jittering position)");
const float max_extent = std::max({root_max.x - root_min.x, root_max.y - root_min.y, root_max.z - root_min.z}); Vector3 jittered = pos;
root_max = Vector3Add(root_min, Vector3Scale(Vector3One(), max_extent)); jittered.x += 0.001;
jittered.y += 0.001;
insert(node_idx, mass_id, jittered, mass, depth);
return;
const float root_extent = root_max.x - root_min.x; // cubic // Could also merge them, but that leads to the octree having less leafs than we have
// masses nodes[node_idx].mass_total += mass; return;
// Container for building the particle list before sorting by morton code
struct sort_node
{
u64 code;
u32 id;
Vector3 pos;
};
// Calculate morton code for each node
std::vector<sort_node> sort_container;
sort_container.resize(positions.size());
const auto calculate_morton = [&](const u32 i)
{
sort_container[i] = {pos_to_morton(positions[i], root_min, root_max), i, positions[i]};
};
if (thread_pool) {
(*thread_pool)->submit_loop(0, positions.size(), calculate_morton, SMALL_TASK_BLOCK_SIZE).wait();
} else {
for (u32 i = 0; i < positions.size(); ++i) {
calculate_morton(i);
}
}
// Sort the list by morton codes. Because positions close to each other have similar morten codes,
// this provides us with "spatial locality" in the datastructure.
auto sort_by_code = [&]()
{
std::ranges::sort(sort_container,
[](const sort_node& a, const sort_node& b)
{
if (a.code != b.code) {
return a.code < b.code;
}
return a.id < b.id;
});
};
sort_by_code();
// Resolve duplicates by jittering the later one deterministically and re-encoding.
for (int seed = 0; seed < 8; ++seed) {
bool had_dupes = false;
for (size_t i = 1; i < sort_container.size(); ++i) {
// Because elements are spatially ordered after sorting, we can scan for duplicates in O(n)
if (sort_container[i].code == sort_container[i - 1].code) {
had_dupes = true;
sort_container[i].pos = jitter_pos(sort_container[i].pos,
sort_container[i].id + seed * 0x9e3779b9u,
root_min,
root_max,
root_extent);
sort_container[i].code = pos_to_morton(sort_container[i].pos, root_min, root_max);
}
} }
if (!had_dupes) { // Convert the leaf to an internal node
break; nodes[node_idx].mass_id = -1;
nodes[node_idx].leaf = false;
nodes[node_idx].mass_total = 0.0;
nodes[node_idx].mass_center = Vector3Zero();
// Re-insert the existing mass into a new empty leaf (see above)
const int oct = get_octant(node_idx, existing_pos);
if (nodes[node_idx].children[oct] == -1) {
const auto& [min, max] = get_child_bounds(node_idx, oct);
const int child_idx = create_empty_leaf(min, max);
nodes[node_idx].children[oct] = child_idx;
} }
sort_by_code(); insert(nodes[node_idx].children[oct], existing_id, existing_pos, existing_mass, depth + 1);
} }
// Sanity check // Insert the new mass
for (size_t i = 1; i < sort_container.size(); ++i) { const int oct = get_octant(node_idx, pos);
if (sort_container[i].code == sort_container[i - 1].code) { if (nodes[node_idx].children[oct] == -1) {
throw std::runtime_error("Duplicates remain after jittering"); const auto& [min, max] = get_child_bounds(node_idx, oct);
} const int child_idx = create_empty_leaf(min, max);
nodes[node_idx].children[oct] = child_idx;
} }
insert(nodes[node_idx].children[oct], mass_id, pos, mass, depth + 1);
std::vector<std::vector<node>> tree_levels; // Update the center of mass
tree_levels.assign(MAX_DEPTH + 1, {}); const float new_mass = nodes[node_idx].mass_total + mass;
nodes[node_idx].mass_center.x = (nodes[node_idx].mass_center.x * nodes[node_idx].mass_total + pos.x) / new_mass;
// Leaves at MAX_DEPTH: 1 particle per leaf in morton order (close particles close together) nodes[node_idx].mass_center.y = (nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) / new_mass;
auto& leafs = tree_levels[MAX_DEPTH]; nodes[node_idx].mass_center.z = (nodes[node_idx].mass_center.z * nodes[node_idx].mass_total + pos.z) / new_mass;
leafs.reserve(sort_container.size()); nodes[node_idx].mass_total = new_mass;
const float leaf_size = root_extent / static_cast<float>(1u << MAX_DEPTH);
for (const auto& [code, id, pos] : sort_container) {
node leaf;
leaf.leaf = true;
leaf.mass_id = static_cast<int>(id);
leaf.depth = MAX_DEPTH;
leaf.size = leaf_size;
leaf.mass_total = MASS;
leaf.mass_center = pos; // force uses mass_center instead of jittered position
leaf.children.fill(-1);
leafs.push_back(leaf);
}
// We now have to group the particles (currently we have only sorted the leaves),
// but upwards subdivisions have to be created.
// For grouping, store a nodes local index in its level.
struct leaf
{
u64 leaf_code;
int depth;
int level_index;
};
std::vector<leaf> leaves;
leaves.reserve(tree_levels[MAX_DEPTH].size());
for (int i = 0; i < static_cast<int>(tree_levels[MAX_DEPTH].size()); ++i) {
leaves.emplace_back(sort_container[static_cast<size_t>(i)].code, MAX_DEPTH, i);
}
// Build internal levels from MAX_DEPTH-1 to 0
for (int current_depth = MAX_DEPTH - 1; current_depth >= 0; --current_depth) {
auto& current_level = tree_levels[current_depth];
current_level.clear();
std::vector<leaf> next_refs;
next_refs.reserve(leaves.size());
const float parent_size = root_extent / static_cast<float>(1u << current_depth);
size_t i = 0;
while (i < leaves.size()) {
const u64 key = path_to_ancestor(leaves[i].leaf_code, MAX_DEPTH, current_depth);
size_t j = i + 1;
while (j < leaves.size() && path_to_ancestor(leaves[j].leaf_code, MAX_DEPTH, current_depth) == key) {
++j;
}
const size_t group_size = j - i;
// Unary compression: just carry the child ref upward unchanged.
if (group_size == 1) {
next_refs.push_back(leaves[i]);
i = j;
continue;
}
node parent;
parent.leaf = false;
parent.mass_id = -1;
parent.depth = current_depth;
parent.size = parent_size;
parent.children.fill(-1);
float mass_total = 0.0f;
Vector3 mass_center_acc = Vector3Zero();
for (size_t k = i; k < j; ++k) {
const int child_depth = leaves[k].depth;
const int child_local = leaves[k].level_index;
// Read the child from its actual stored level.
const node& child = tree_levels[child_depth][child_local];
// Which octant of this parent does it belong to?
// Octant comes from the NEXT level after current_depth,
// but the child might skip levels due to compression.
// We must use the child's first level under the parent (current_depth+1).
const int oct = octant_at_level(leaves[k].leaf_code, current_depth + 1, MAX_DEPTH);
// Store global child reference: we only have an int slot, so we need a single index space.
parent.children[oct] = (child_depth << 24) | (child_local & 0x00FFFFFF);
mass_total += child.mass_total;
mass_center_acc = Vector3Add(mass_center_acc, Vector3Scale(child.mass_center, child.mass_total));
}
parent.mass_total = mass_total;
parent.mass_center = (mass_total > 0.0f) ? Vector3Scale(mass_center_acc, 1.0f / mass_total) : Vector3Zero();
const int parent_local = static_cast<int>(current_level.size());
current_level.push_back(parent);
next_refs.push_back({leaves[i].leaf_code, current_depth, parent_local});
i = j;
}
leaves.swap(next_refs);
}
// Flatten levels and fix child indices from local->global.
// All depth 0 nodes come first, then depth 1, last depth MAX_DEPTH.
std::vector<int> offsets(tree_levels.size(), 0);
int total = 0;
for (int d = 0; d <= MAX_DEPTH; ++d) {
offsets[d] = total;
total += static_cast<int>(tree_levels[d].size());
}
t.nodes.clear();
t.nodes.reserve(total);
for (int d = 0; d <= MAX_DEPTH; ++d) {
t.nodes.insert(t.nodes.end(), tree_levels[d].begin(), tree_levels[d].end());
}
// Fix child indices: convert local index in levels[d+1] to global index in t.nodes
for (int d = 0; d <= MAX_DEPTH; ++d) {
const int base = offsets[d];
for (int i2 = 0; i2 < static_cast<int>(tree_levels[d].size()); ++i2) {
node& n = t.nodes[base + i2];
if (!n.leaf) {
for (int c = 0; c < 8; ++c) {
int packed = n.children[c];
if (packed >= 0) {
const int child_depth = (packed >> 24) & 0xFF;
const int child_local = packed & 0x00FFFFFF;
n.children[c] = offsets[child_depth] + child_local;
}
}
}
}
}
// const size_t _leaves = tree_levels[MAX_DEPTH].size();
// const size_t _total = t.nodes.size();
// const size_t _internal = _total - _leaves;
// traceln("Morton octree nodes: {}, leaves: {}, ratio: {:.3} children per internal node.",
// _total,
// _leaves,
// static_cast<float>(_total - 1) / _internal);
} }
auto octree::calculate_force_morton(const int node_idx, const Vector3& pos, const int self_id) const -> Vector3 auto octree::calculate_force(const int node_idx, const Vector3& pos) const -> Vector3
{ {
if (node_idx < 0) { if (node_idx < 0) {
return Vector3Zero(); return Vector3Zero();
} }
// Force accumulator const node& n = nodes[node_idx];
float fx = 0.0f; if (std::abs(n.mass_total) <= 0.001f) {
float fy = 0.0f; return Vector3Zero();
float fz = 0.0f; }
std::vector<int> stack; const Vector3 diff = Vector3Subtract(pos, n.mass_center);
stack.reserve(512); float dist_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
stack.push_back(node_idx);
constexpr float theta2 = THETA * THETA; // Softening
dist_sq += SOFTENING;
while (!stack.empty()) { // Barnes-Hut
const int idx = stack.back(); const float size = n.box_max.x - n.box_min.x;
stack.pop_back(); if (n.leaf || size * size / dist_sq < THETA * THETA) {
const float dist = std::sqrt(dist_sq);
const float force_mag = BH_FORCE * n.mass_total / dist_sq;
const node& n = nodes[idx]; return Vector3Scale(diff, force_mag / dist);
}
// No self-force for single-particle leafs // Collect child forces
if (n.leaf && n.mass_id == self_id) { Vector3 force = Vector3Zero();
continue; for (const int child : n.children) {
} if (child >= 0) {
const Vector3 child_force = calculate_force(child, pos);
const float dx = pos.x - n.mass_center.x; force = Vector3Add(force, child_force);
const float dy = pos.y - n.mass_center.y;
const float dz = pos.z - n.mass_center.z;
const float dist_sq = dx * dx + dy * dy + dz * dz + SOFTENING;
// BarnesHut criterion
if (n.leaf || ((n.size * n.size) / dist_sq) < theta2) {
const float inv_dist = 1.0f / std::sqrt(dist_sq);
const float force_mag = (BH_FORCE * n.mass_total) / dist_sq; // ~ 1/r^2
const float s = force_mag * inv_dist; // scale by 1/r to get vector
fx += dx * s;
fy += dy * s;
fz += dz * s;
continue;
}
for (int c = 0; c < 8; ++c) {
const int child = n.children[c];
if (child >= 0) {
stack.push_back(child);
}
} }
} }
return Vector3{fx, fy, fz}; return force;
} }

View File

@ -4,6 +4,10 @@
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto orbit_camera::rotate(const Vector2 last_mouse, const Vector2 mouse) -> void auto orbit_camera::rotate(const Vector2 last_mouse, const Vector2 mouse) -> void
{ {
const auto [dx, dy] = Vector2Subtract(mouse, last_mouse); const auto [dx, dy] = Vector2Subtract(mouse, last_mouse);
@ -31,27 +35,24 @@ auto orbit_camera::pan(const Vector2 last_mouse, const Vector2 mouse) -> void
const Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up)); const Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up));
const Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward)); const Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward));
const Vector3 offset = Vector3Add(Vector3Scale(right, -dx * speed), Vector3Scale(up, dy * speed)); const Vector3 offset =
Vector3Add(Vector3Scale(right, -dx * speed), Vector3Scale(up, dy * speed));
target = Vector3Add(target, offset); target = Vector3Add(target, offset);
} }
auto orbit_camera::update(const Vector3& current_target, auto orbit_camera::update(const Vector3& current_target, const Vector3& mass_center,
const Vector3& mass_center, const bool lock, const bool mass_center_lock) -> void
const bool lock,
const bool mass_center_lock) -> void
{ {
if (lock) { if (lock) {
if (mass_center_lock) { if (mass_center_lock) {
target = Vector3MoveTowards(target, target = Vector3MoveTowards(target, mass_center,
mass_center, CAMERA_SMOOTH_SPEED * GetFrameTime() *
CAMERA_SMOOTH_SPEED * GetFrameTime() * Vector3Length( Vector3Length(Vector3Subtract(target, mass_center)));
Vector3Subtract(target, mass_center)));
} else { } else {
target = Vector3MoveTowards(target, target = Vector3MoveTowards(target, current_target,
current_target, CAMERA_SMOOTH_SPEED * GetFrameTime() *
CAMERA_SMOOTH_SPEED * GetFrameTime() * Vector3Length( Vector3Length(Vector3Subtract(target, current_target)));
Vector3Subtract(target, current_target)));
} }
} }
@ -66,14 +67,10 @@ auto orbit_camera::update(const Vector3& current_target,
const float y = sin(angle_y) * actual_distance; const float y = sin(angle_y) * actual_distance;
const float z = cos(angle_y) * cos(angle_x) * actual_distance; const float z = cos(angle_y) * cos(angle_x) * actual_distance;
if (projection == CAMERA_ORTHOGRAPHIC) { fov = Clamp(fov, MIN_FOV, MAX_FOV);
fov = Clamp(fov, MIN_FOV, MAX_ORTHO_FOV);
} else {
fov = Clamp(fov, MIN_FOV, MAX_PERSP_FOV);
}
camera.position = Vector3Add(target, Vector3(x, y, z)); camera.position = Vector3Add(target, Vector3(x, y, z));
camera.target = target; camera.target = target;
camera.fovy = fov; camera.fovy = fov;
camera.projection = projection; camera.projection = projection;
} }

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,10 @@
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <rlgl.h> #include <rlgl.h>
#include <GL/glew.h>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto renderer::update_texture_sizes() -> void auto renderer::update_texture_sizes() -> void
{ {
@ -12,142 +15,70 @@ auto renderer::update_texture_sizes() -> void
return; return;
} }
UnloadRenderTexture(graph_target); UnloadRenderTexture(render_target);
UnloadRenderTexture(klotski_target); UnloadRenderTexture(klotski_target);
UnloadRenderTexture(menu_target); UnloadRenderTexture(menu_target);
const int width = GetScreenWidth() / 2; const int width = GetScreenWidth() / 2;
const int height = GetScreenHeight() - MENU_HEIGHT; const int height = GetScreenHeight() - MENU_HEIGHT;
graph_target = LoadRenderTexture(width, height); render_target = LoadRenderTexture(width, height);
klotski_target = LoadRenderTexture(width, height); klotski_target = LoadRenderTexture(width, height);
menu_target = LoadRenderTexture(width * 2, MENU_HEIGHT); menu_target = LoadRenderTexture(width * 2, MENU_HEIGHT);
} }
auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
{ {
#ifdef TRACY #ifdef TRACY
ZoneScoped; ZoneScoped;
#endif #endif
if (masses.size() != state.get_state_count()) { if (masses.size() != state.get_state_count()) {
// Because the physics run in a different thread, it might need time to catch up // Because the physics run in a different thread, it might need time to catch up
return; return;
} }
// Prepare edge buffer // Prepare connection batching
{ {
#ifdef TRACY #ifdef TRACY
ZoneNamedN(prepare_edge_buffers, "PrepareEdgeBuffers", true); ZoneNamedN(prepare_masses, "PrepareConnectionsBatching", true);
#endif #endif
edge_vertices.clear();
for (const auto& [from, to] : state.get_links()) {
edge_vertices.push_back(masses[from]);
edge_vertices.push_back(masses[to]);
}
rlUpdateVertexBuffer(edge_vbo_id, edge_vertices.data(), edge_vertices.size() * sizeof(Vector3), 0);
}
// Prepare connection drawing
{
#ifdef TRACY
ZoneNamedN(prepare_connections, "PrepareConnectionsDrawing", true);
#endif
connections.clear(); connections.clear();
connections.reserve(state.get_target_count()); connections.reserve(state.get_target_count());
if (input.connect_solutions) { if (input.connect_solutions) {
for (const size_t& _state : state.get_winning_indices()) { for (const size_t& _state : state.get_winning_indices()) {
const Vector3& current_mass = masses[state.get_current_index()]; const Vector3& current_mass = masses.at(state.get_current_index());
const Vector3& winning_mass = masses[_state]; const Vector3& winning_mass = masses.at(_state);
connections.emplace_back(current_mass, winning_mass); connections.emplace_back(current_mass, winning_mass);
DrawLine3D(current_mass, winning_mass, Fade(TARGET_BLOCK_COLOR, 0.5));
} }
} }
} }
// Prepare cube instancing // Prepare cube instancing
{ {
#ifdef TRACY #ifdef TRACY
ZoneNamedN(prepare_masses, "PrepareMassInstancing", true); ZoneNamedN(prepare_masses, "PrepareMassInstancing", true);
#endif #endif
if (masses.size() < DRAW_VERTICES_LIMIT) { if (masses.size() < DRAW_VERTICES_LIMIT) {
// Don't have to reserve, capacity is already set to DRAW_VERTICES_LIMIT in constructor // Don't have to reserve, capacity is already set to DRAW_VERTICES_LIMIT in constructor
transforms.clear(); transforms.clear();
colors.clear(); colors.clear();
// Collisions
// TODO: This would benefit greatly from a spatial data structure.
// Would it be worth to copy the octree from the physics thread?
input.collision_mass = -1;
if (input.mouse_in_graph_pane() && IsKeyDown(KEY_Q)) {
#ifdef TRACY
ZoneNamedN(mass_collisions, "MassCollisions", true);
#endif
const Ray ray = GetScreenToWorldRayEx(
GetMousePosition() - Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT),
camera.camera,
graph_target.texture.width,
graph_target.texture.height);
// Ray collision hit info
size_t mass = 0;
for (const auto& [x, y, z] : masses) {
const RayCollision collision = GetRayCollisionBox(ray,
BoundingBox{
{
x - VERTEX_SIZE / 2.0f,
y - VERTEX_SIZE / 2.0f,
z - VERTEX_SIZE / 2.0f
},
{
x + VERTEX_SIZE / 2.0f,
y + VERTEX_SIZE / 2.0f,
z + VERTEX_SIZE / 2.0f
}
});
if (collision.hit) {
input.collision_mass = mass;
break;
}
++mass;
}
}
// Find max distance to interpolate colors in the given [0, max] range
int max_distance = 0;
for (const int distance : state.get_distances()) {
if (distance > max_distance) {
max_distance = distance;
}
}
const auto lerp_color = [&](const Color from, const Color to, const int distance)
{
const float weight = 1.0 - static_cast<float>(distance) / max_distance;
Color result;
result.r = static_cast<u8>((1 - weight) * from.r + weight * to.r);
result.g = static_cast<u8>((1 - weight) * from.g + weight * to.g);
result.b = static_cast<u8>((1 - weight) * from.b + weight * to.b);
result.a = static_cast<u8>((1 - weight) * from.a + weight * to.a);
return result;
};
const std::vector<int>& distances = state.get_distances();
size_t mass = 0; size_t mass = 0;
for (const auto& [x, y, z] : masses) { for (const auto& [x, y, z] : masses) {
transforms.emplace_back(MatrixTranslate(x, y, z)); transforms.emplace_back(MatrixTranslate(x, y, z));
// Normal vertex // Normal vertex
Color c = VERTEX_COLOR; Color c = VERTEX_COLOR;
if ((input.mark_solutions || input.mark_path) && state.get_winning_indices().contains(mass)) { if ((input.mark_solutions || input.mark_path) &&
state.get_winning_indices().contains(mass)) {
// Winning vertex // Winning vertex
c = VERTEX_TARGET_COLOR; c = VERTEX_TARGET_COLOR;
} else if ((input.mark_solutions || input.mark_path) && state.get_path_indices().contains(mass)) { } else if ((input.mark_solutions || input.mark_path) &&
state.get_path_indices().contains(mass)) {
// Path vertex // Path vertex
c = VERTEX_PATH_COLOR; c = VERTEX_PATH_COLOR;
} else if (mass == state.get_starting_index()) { } else if (mass == state.get_starting_index()) {
@ -156,14 +87,9 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
} else if (state.get_visit_counts().at(mass) > 0) { } else if (state.get_visit_counts().at(mass) > 0) {
// Visited vertex // Visited vertex
c = VERTEX_VISITED_COLOR; c = VERTEX_VISITED_COLOR;
} else if (input.color_by_distance && distances.size() == masses.size()) {
c = lerp_color(VERTEX_FARTHEST_COLOR, VERTEX_CLOSEST_COLOR, static_cast<float>(distances[mass]));
} }
if (mass == input.collision_mass) {
c = RED;
}
// Current vertex is drawn as individual cube to increase its size // Current vertex is drawn as individual cube to increase its size
colors.emplace_back(c); colors.emplace_back(c);
++mass; ++mass;
} }
@ -172,71 +98,42 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
rlUpdateVertexBuffer(color_vbo_id, colors.data(), colors.size() * sizeof(Color), 0); rlUpdateVertexBuffer(color_vbo_id, colors.data(), colors.size() * sizeof(Color), 0);
} }
BeginTextureMode(graph_target); BeginTextureMode(render_target);
ClearBackground(RAYWHITE); ClearBackground(RAYWHITE);
BeginMode3D(camera.camera); BeginMode3D(camera.camera);
rlDrawRenderBatchActive(); // Draw springs (batched)
// Draw edges
{ {
#ifdef TRACY #ifdef TRACY
ZoneNamedN(draw_springs, "DrawSprings", true); ZoneNamedN(draw_springs, "DrawSprings", true);
#endif #endif
rlEnableShader(edge_shader.id); rlBegin(RL_LINES);
for (const auto& [from, to] : state.get_links()) {
Matrix modelview = rlGetMatrixModelview(); if (masses.size() > from && masses.size() > to) {
Matrix projection = rlGetMatrixProjection(); const auto& [ax, ay, az] = masses.at(from);
Matrix mvp = MatrixMultiply(modelview, projection); const auto& [bx, by, bz] = masses.at(to);
rlSetUniformMatrix(edge_shader.locs[SHADER_LOC_MATRIX_MVP], mvp); rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a);
rlVertex3f(ax, ay, az);
const std::array<float, 4> edge_color = { rlVertex3f(bx, by, bz);
EDGE_COLOR.r / 255.0f, }
EDGE_COLOR.g / 255.0f, }
EDGE_COLOR.b / 255.0f, rlEnd();
EDGE_COLOR.a / 255.0f
};
rlSetUniform(edge_color_loc, edge_color.data(), SHADER_UNIFORM_VEC4, 1);
glBindVertexArray(edge_vao_id);
glDrawArrays(GL_LINES, 0, edge_vertices.size());
glBindVertexArray(0);
rlDisableShader();
// This draws triangles:
// rlEnableVertexArray(edge_vao_id);
// rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a);
// rlDrawVertexArray(0, edge_vertices.size());
// rlDisableVertexArray();
// This is fucking slow:
// rlBegin(RL_LINES);
// for (const auto& [from, to] : state.get_links()) {
// if (masses.size() > from && masses.size() > to) {
// const auto& [ax, ay, az] = masses[from];
// const auto& [bx, by, bz] = masses[to];
// rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a);
// rlVertex3f(ax, ay, az);
// rlVertex3f(bx, by, bz);
// }
// }
// rlEnd();
} }
// Draw masses (instanced) // Draw masses (instanced)
{ {
#ifdef TRACY #ifdef TRACY
ZoneNamedN(draw_masses, "DrawMasses", true); ZoneNamedN(draw_masses, "DrawMasses", true);
#endif #endif
if (masses.size() < DRAW_VERTICES_LIMIT) { if (masses.size() < DRAW_VERTICES_LIMIT) {
// NOTE: I don't know if drawing all this inside a shader would make it // NOTE: I don't know if drawing all this inside a shader would make it
// much faster... The amount of data sent to the GPU would be // much faster... The amount of data sent to the GPU would be
// reduced (just positions instead of matrices), but is this // reduced (just positions instead of matrices), but is this
// noticable for < 100000 cubes? // noticable for < 100000 cubes?
DrawMeshInstanced(cube_instance, vertex_mat, transforms.data(), masses.size()); // NOLINT(*-narrowing-conversions) DrawMeshInstanced(cube_instance, vertex_mat, transforms.data(),
masses.size()); // NOLINT(*-narrowing-conversions)
} }
} }
@ -255,8 +152,9 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
// Mark current state // Mark current state
const size_t current_index = state.get_current_index(); const size_t current_index = state.get_current_index();
if (masses.size() > current_index) { if (masses.size() > current_index) {
const Vector3& current_mass = masses[current_index]; const Vector3& current_mass = masses.at(current_index);
DrawCube(current_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_CURRENT_COLOR); DrawCube(current_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2,
VERTEX_CURRENT_COLOR);
} }
EndMode3D(); EndMode3D();
@ -265,9 +163,9 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
auto renderer::draw_klotski() const -> void auto renderer::draw_klotski() const -> void
{ {
#ifdef TRACY #ifdef TRACY
ZoneScoped; ZoneScoped;
#endif #endif
BeginTextureMode(klotski_target); BeginTextureMode(klotski_target);
ClearBackground(RAYWHITE); ClearBackground(RAYWHITE);
@ -279,9 +177,9 @@ auto renderer::draw_klotski() const -> void
auto renderer::draw_menu() const -> void auto renderer::draw_menu() const -> void
{ {
#ifdef TRACY #ifdef TRACY
ZoneScoped; ZoneScoped;
#endif #endif
BeginTextureMode(menu_target); BeginTextureMode(menu_target);
ClearBackground(RAYWHITE); ClearBackground(RAYWHITE);
@ -291,48 +189,37 @@ auto renderer::draw_menu() const -> void
EndTextureMode(); EndTextureMode();
} }
auto renderer::draw_textures(const int fps, auto renderer::draw_textures(const int fps, const int ups, const size_t mass_count,
const int ups,
const size_t mass_count,
const size_t spring_count) const -> void const size_t spring_count) const -> void
{ {
BeginDrawing(); BeginDrawing();
DrawTextureRec(menu_target.texture, DrawTextureRec(menu_target.texture,
Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height), Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height),
Vector2(0, 0), Vector2(0, 0), WHITE);
WHITE);
DrawTextureRec(klotski_target.texture, DrawTextureRec(klotski_target.texture,
Rectangle(0, 0, klotski_target.texture.width, -klotski_target.texture.height), Rectangle(0, 0, klotski_target.texture.width, -klotski_target.texture.height),
Vector2(0, MENU_HEIGHT), Vector2(0, MENU_HEIGHT), WHITE);
WHITE); DrawTextureRec(render_target.texture,
DrawTextureRec(graph_target.texture, Rectangle(0, 0, render_target.texture.width, -render_target.texture.height),
Rectangle(0, 0, graph_target.texture.width, -graph_target.texture.height), Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT), WHITE);
Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT),
WHITE);
// Draw borders // Draw borders
DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0f, BLACK); DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0f, BLACK);
DrawRectangleLinesEx(Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), DrawRectangleLinesEx(
1.0f, Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), 1.0f,
BLACK); BLACK);
DrawRectangleLinesEx(Rectangle(GetScreenWidth() / 2.0f, DrawRectangleLinesEx(Rectangle(GetScreenWidth() / 2.0f, MENU_HEIGHT, GetScreenWidth() / 2.0f,
MENU_HEIGHT,
GetScreenWidth() / 2.0f,
GetScreenHeight() - MENU_HEIGHT), GetScreenHeight() - MENU_HEIGHT),
1.0f, 1.0f, BLACK);
BLACK);
gui.draw(fps, ups, mass_count, spring_count); gui.draw(fps, ups, mass_count, spring_count);
EndDrawing(); EndDrawing();
} }
auto renderer::render(const std::vector<Vector3>& masses, auto renderer::render(const std::vector<Vector3>& masses, const int fps, const int ups,
const int fps, const size_t mass_count, const size_t spring_count) -> void
const int ups,
const size_t mass_count,
const size_t spring_count) -> void
{ {
update_texture_sizes(); update_texture_sizes();
@ -340,4 +227,4 @@ auto renderer::render(const std::vector<Vector3>& masses,
draw_klotski(); draw_klotski();
draw_menu(); draw_menu();
draw_textures(fps, ups, mass_count, spring_count); draw_textures(fps, ups, mass_count, spring_count);
} }

View File

@ -2,10 +2,17 @@
#include "graph_distances.hpp" #include "graph_distances.hpp"
#include "util.hpp" #include "util.hpp"
#include <fstream>
#include <ios>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto state_manager::synced_try_insert_state(const puzzle& state) -> size_t auto state_manager::synced_try_insert_state(const puzzle& state) -> size_t
{ {
if (state_indices.contains(state)) { if (state_indices.contains(state)) {
return state_indices[state]; return state_indices.at(state);
} }
const size_t index = state_pool.size(); const size_t index = state_pool.size();
@ -28,7 +35,7 @@ auto state_manager::synced_insert_link(size_t first_index, size_t second_index)
} }
auto state_manager::synced_insert_statespace(const std::vector<puzzle>& states, auto state_manager::synced_insert_statespace(const std::vector<puzzle>& states,
const std::vector<spring>& _links) -> void const std::vector<std::pair<size_t, size_t>>& _links) -> void
{ {
if (!state_pool.empty() || !state_indices.empty() || !links.empty()) { if (!state_pool.empty() || !state_indices.empty() || !links.empty()) {
warnln("Inserting statespace but collections haven't been cleared"); warnln("Inserting statespace but collections haven't been cleared");
@ -70,27 +77,78 @@ auto state_manager::synced_clear_statespace() -> void
physics.clear_cmd(); physics.clear_cmd();
} }
auto state_manager::save_current_to_preset_file(const std::string& preset_comment) -> void auto state_manager::parse_preset_file(const std::string& _preset_file) -> bool
{ {
if (append_preset_file(preset_file, preset_comment, get_current_state())) { preset_file = _preset_file;
current_preset = preset_states.size();
reload_preset_file(); std::ifstream file(preset_file);
if (!file) {
infoln("Preset file \"{}\" couldn't be loaded.", preset_file);
return false;
} }
std::string line;
std::vector<std::string> comment_lines;
std::vector<std::string> preset_lines;
while (std::getline(file, line)) {
if (line.starts_with("S")) {
preset_lines.push_back(line);
} else if (line.starts_with("#")) {
comment_lines.push_back(line);
}
}
if (preset_lines.empty() || comment_lines.size() != preset_lines.size()) {
infoln("Preset file \"{}\" couldn't be loaded.", preset_file);
return false;
}
preset_states.clear();
for (const auto& preset : preset_lines) {
// Each char is a bit
const puzzle& p = puzzle(preset);
if (const std::optional<std::string>& reason = p.try_get_invalid_reason()) {
preset_states = {puzzle(4, 5, 0, 0, true, false)};
infoln("Preset file \"{}\" contained invalid presets: {}", preset_file, *reason);
return false;
}
preset_states.emplace_back(p);
}
preset_comments = comment_lines;
infoln("Loaded {} presets from \"{}\".", preset_lines.size(), preset_file);
return true;
} }
auto state_manager::reload_preset_file() -> void auto state_manager::append_preset_file(const std::string& preset_name) -> bool
{ {
const auto [presets, comments] = parse_preset_file(preset_file); infoln(R"(Saving preset "{}" to "{}")", preset_name, preset_file);
if (!presets.empty()) {
preset_states = presets; if (get_current_state().try_get_invalid_reason()) {
preset_comments = comments; return false;
} }
load_preset(current_preset);
std::ofstream file(preset_file, std::ios_base::app | std::ios_base::out);
if (!file) {
infoln("Preset file \"{}\" couldn't be loaded.", preset_file);
return false;
}
file << "\n# " << preset_name << "\n" << get_current_state().string_repr() << std::flush;
infoln("Refreshing presets...");
if (parse_preset_file(preset_file)) {
load_preset(preset_states.size() - 1);
}
return true;
} }
auto state_manager::load_preset(const size_t preset) -> void auto state_manager::load_preset(const size_t preset) -> void
{ {
clear_graph_and_add_current(preset_states[preset]); clear_graph_and_add_current(preset_states.at(preset));
current_preset = preset; current_preset = preset;
edited = false; edited = false;
} }
@ -212,8 +270,8 @@ auto state_manager::goto_most_distant_state() -> void
int max_distance = 0; int max_distance = 0;
size_t max_distance_index = 0; size_t max_distance_index = 0;
for (size_t i = 0; i < node_target_distances.distances.size(); ++i) { for (size_t i = 0; i < node_target_distances.distances.size(); ++i) {
if (node_target_distances.distances[i] > max_distance) { if (node_target_distances.distances.at(i) > max_distance) {
max_distance = node_target_distances.distances[i]; max_distance = node_target_distances.distances.at(i);
max_distance_index = i; max_distance_index = i;
} }
} }
@ -227,7 +285,7 @@ auto state_manager::goto_closest_target_state() -> void
return; return;
} }
update_current_state(get_state(node_target_distances.nearest_targets[current_state_index])); update_current_state(get_state(node_target_distances.nearest_targets.at(current_state_index)));
} }
auto state_manager::populate_graph() -> void auto state_manager::populate_graph() -> void
@ -240,23 +298,17 @@ auto state_manager::populate_graph() -> void
const puzzle s = get_starting_state(); const puzzle s = get_starting_state();
const puzzle p = get_current_state(); const puzzle p = get_current_state();
// Clear the graph first so we don't add duplicates somehow // Clear the graph first so we don't add duplicates somehow
synced_clear_statespace(); synced_clear_statespace();
// Explore the entire statespace starting from the current state // Explore the entire statespace starting from the current state
const std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
const auto& [states, _links] = s.explore_state_space(); const auto& [states, _links] = s.explore_state_space();
synced_insert_statespace(states, _links); synced_insert_statespace(states, _links);
const std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); current_state_index = state_indices.at(p);
infoln("Explored puzzle. Took {}ms.", std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count());
infoln("State space has size {} with {} transitions.", state_pool.size(), links.size());
current_state_index = state_indices[p];
previous_state_index = current_state_index; previous_state_index = current_state_index;
starting_state_index = state_indices[s]; starting_state_index = state_indices.at(s);
// Search for cool stuff // Search for cool stuff
populate_winning_indices(); populate_winning_indices();
@ -341,7 +393,7 @@ auto state_manager::get_starting_index() const -> size_t
auto state_manager::get_state(const size_t index) const -> const puzzle& auto state_manager::get_state(const size_t index) const -> const puzzle&
{ {
return state_pool[index]; return state_pool.at(index);
} }
auto state_manager::get_current_state() const -> const puzzle& auto state_manager::get_current_state() const -> const puzzle&
@ -374,7 +426,7 @@ auto state_manager::get_path_length() const -> size_t
return winning_path.size(); return winning_path.size();
} }
auto state_manager::get_links() const -> const std::vector<spring>& auto state_manager::get_links() const -> const std::vector<std::pair<size_t, size_t>>&
{ {
return links; return links;
} }
@ -416,7 +468,7 @@ auto state_manager::get_preset_count() const -> size_t
auto state_manager::get_current_preset_comment() const -> const std::string& auto state_manager::get_current_preset_comment() const -> const std::string&
{ {
return preset_comments[current_preset]; return preset_comments.at(current_preset);
} }
auto state_manager::has_history() const -> bool auto state_manager::has_history() const -> bool
@ -429,11 +481,6 @@ auto state_manager::has_distances() const -> bool
return !node_target_distances.empty(); return !node_target_distances.empty();
} }
auto state_manager::get_distances() const -> std::vector<int>
{
return node_target_distances.distances;
}
auto state_manager::get_total_moves() const -> size_t auto state_manager::get_total_moves() const -> size_t
{ {
return total_moves; return total_moves;

View File

@ -1,7 +1,6 @@
#include "cpu_layout_engine.hpp" #include "threaded_physics.hpp"
#include "config.hpp" #include "config.hpp"
#include "cpu_spring_system.hpp" #include "mass_spring_system.hpp"
#include "util.hpp"
#include <chrono> #include <chrono>
#include <raylib.h> #include <raylib.h>
@ -9,29 +8,20 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#ifdef ASYNC_OCTREE #ifdef TRACY
auto cpu_layout_engine::set_octree_pool_thread_name(size_t idx) -> void #include <tracy/Tracy.hpp>
{
BS::this_thread::set_os_thread_name(std::format("octree-{}", idx));
// traceln("Using thread \"{}\"", BS::this_thread::get_os_thread_name().value_or("INVALID NAME"));
}
#endif #endif
auto cpu_layout_engine::physics_thread(physics_state& state, const threadpool thread_pool) -> void auto threaded_physics::physics_thread(physics_state& state) -> void
{ {
cpu_spring_system mass_springs; #ifdef THREADPOOL
#ifdef ASYNC_OCTREE
BS::this_thread::set_os_thread_name("physics"); BS::this_thread::set_os_thread_name("physics");
BS::thread_pool<> octree_thread(1, set_octree_pool_thread_name);
std::future<void> octree_future;
octree tree_buffer;
size_t last_mass_count = 0;
#endif #endif
mass_spring_system mass_springs;
const auto visitor = overloads{ const auto visitor = overloads{
[&](const struct add_mass&) [&](const struct add_mass& am)
{ {
mass_springs.add_mass(); mass_springs.add_mass();
}, },
@ -39,7 +29,7 @@ auto cpu_layout_engine::physics_thread(physics_state& state, const threadpool th
{ {
mass_springs.add_spring(as.a, as.b); mass_springs.add_spring(as.a, as.b);
}, },
[&](const struct clear_graph&) [&](const struct clear_graph& cg)
{ {
mass_springs.clear(); mass_springs.clear();
}, },
@ -76,58 +66,26 @@ auto cpu_layout_engine::physics_thread(physics_state& state, const threadpool th
} }
} }
if (mass_springs.positions.empty()) { if (mass_springs.masses.empty()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue; continue;
} }
// Physics update // Physics update
if (physics_accumulator.count() > TIMESTEP) { if (physics_accumulator.count() > TIMESTEP) {
#ifdef ASYNC_OCTREE
// Snapshot the positions so mass_springs is not mutating the vector while the octree is building
std::vector<Vector3> positions = mass_springs.positions;
// Start building the octree for the next physics update.
// Move the snapshot into the closure so it doesn't get captured by reference (don't use [&])
octree_future = octree_thread.submit_task([&tree_buffer, &thread_pool, positions = std::move(positions)]()
{
octree::build_octree_morton(tree_buffer, positions, thread_pool);
});
// Rebuild the tree synchronously if we changed the number of masses to not use
// an empty tree from the last frame in the frame where the graph was generated
if (last_mass_count != mass_springs.positions.size()) {
traceln("Rebuilding octree synchronously because graph size changed");
octree::build_octree_morton(mass_springs.tree, mass_springs.positions, thread_pool);
last_mass_count = mass_springs.positions.size();
}
#else
octree::build_octree_morton(mass_springs.tree, mass_springs.positions, thread_pool);
#endif
mass_springs.clear_forces(); mass_springs.clear_forces();
mass_springs.calculate_spring_forces(thread_pool); mass_springs.calculate_spring_forces();
mass_springs.calculate_repulsion_forces(thread_pool); mass_springs.calculate_repulsion_forces();
mass_springs.update(TIMESTEP * SIM_SPEED, thread_pool); mass_springs.verlet_update(TIMESTEP * SIM_SPEED);
// This is only helpful if we're drawing a grid at (0, 0, 0). Otherwise, it's just // This is only helpful if we're drawing a grid at (0, 0, 0). Otherwise, it's just
// expensive and yields no benefit since we can lock the camera to the center of mass // expensive and yields no benefit since we can lock the camera to the center of mass
// cheaply. // cheaply. mass_springs.center_masses();
// mass_springs.center_masses(thread_pool);
++loop_iterations; ++loop_iterations;
physics_accumulator -= std::chrono::duration<double>(TIMESTEP); physics_accumulator -= std::chrono::duration<double>(TIMESTEP);
} }
#ifdef ASYNC_OCTREE
// Wait for the octree to be built
if (octree_future.valid()) {
octree_future.wait();
octree_future = std::future<void>{};
std::swap(mass_springs.tree, tree_buffer);
}
#endif
// Publish the positions for the renderer (copy) // Publish the positions for the renderer (copy)
#ifdef TRACY #ifdef TRACY
FrameMarkStart("PhysicsThreadProduceLock"); FrameMarkStart("PhysicsThreadProduceLock");
@ -138,11 +96,10 @@ auto cpu_layout_engine::physics_thread(physics_state& state, const threadpool th
#else #else
std::unique_lock<std::mutex> lock(state.data_mtx); std::unique_lock<std::mutex> lock(state.data_mtx);
#endif #endif
state.data_consumed_cnd.wait(lock, state.data_consumed_cnd.wait(lock, [&]
[&] {
{ return state.data_consumed || !state.running.load();
return state.data_consumed || !state.running.load(); });
});
if (!state.running.load()) { if (!state.running.load()) {
// Running turned false while we were waiting for the condition // Running turned false while we were waiting for the condition
break; break;
@ -154,19 +111,19 @@ auto cpu_layout_engine::physics_thread(physics_state& state, const threadpool th
loop_iterations = 0; loop_iterations = 0;
ups_accumulator = std::chrono::duration<double>(0); ups_accumulator = std::chrono::duration<double>(0);
} }
if (mass_springs.tree.empty()) { if (mass_springs.tree.nodes.empty()) {
state.mass_center = Vector3Zero(); state.mass_center = Vector3Zero();
} else { } else {
state.mass_center = mass_springs.tree.root().mass_center; state.mass_center = mass_springs.tree.nodes.at(0).mass_center;
} }
state.masses.clear(); state.masses.clear();
state.masses.reserve(mass_springs.positions.size()); state.masses.reserve(mass_springs.masses.size());
for (const Vector3& pos : mass_springs.positions) { for (const auto& mass : mass_springs.masses) {
state.masses.emplace_back(pos); state.masses.emplace_back(mass.position);
} }
state.mass_count = mass_springs.positions.size(); state.mass_count = mass_springs.masses.size();
state.spring_count = mass_springs.springs.size(); state.spring_count = mass_springs.springs.size();
state.data_ready = true; state.data_ready = true;
@ -174,27 +131,15 @@ auto cpu_layout_engine::physics_thread(physics_state& state, const threadpool th
} }
// Notify the rendering thread that new data is available // Notify the rendering thread that new data is available
state.data_ready_cnd.notify_all(); state.data_ready_cnd.notify_all();
#ifdef TRACY #ifdef TRACY
FrameMarkEnd("PhysicsThreadProduceLock"); FrameMarkEnd("PhysicsThreadProduceLock");
FrameMarkEnd("PhysicsThread"); FrameMarkEnd("PhysicsThread");
#endif #endif
} }
} }
auto cpu_layout_engine::clear_cmd() -> void auto threaded_physics::add_mass_cmd() -> void
{
{
#ifdef TRACY
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
#else
std::lock_guard<std::mutex> lock(state.command_mtx);
#endif
state.pending_commands.emplace(clear_graph{});
}
}
auto cpu_layout_engine::add_mass_cmd() -> void
{ {
{ {
#ifdef TRACY #ifdef TRACY
@ -206,7 +151,7 @@ auto cpu_layout_engine::add_mass_cmd() -> void
} }
} }
auto cpu_layout_engine::add_spring_cmd(const size_t a, const size_t b) -> void auto threaded_physics::add_spring_cmd(const size_t a, const size_t b) -> void
{ {
{ {
#ifdef TRACY #ifdef TRACY
@ -218,7 +163,20 @@ auto cpu_layout_engine::add_spring_cmd(const size_t a, const size_t b) -> void
} }
} }
auto cpu_layout_engine::add_mass_springs_cmd(const size_t num_masses, const std::vector<spring>& springs) -> void auto threaded_physics::clear_cmd() -> void
{
{
#ifdef TRACY
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
#else
std::lock_guard<std::mutex> lock(state.command_mtx);
#endif
state.pending_commands.emplace(clear_graph{});
}
}
auto threaded_physics::add_mass_springs_cmd(const size_t num_masses,
const std::vector<std::pair<size_t, size_t>>& springs) -> void
{ {
{ {
#ifdef TRACY #ifdef TRACY

View File

@ -7,12 +7,12 @@
#define RAYGUI_IMPLEMENTATION #define RAYGUI_IMPLEMENTATION
#include <raygui.h> #include <raygui.h>
auto user_interface::grid::update_bounds(const int _x, #ifdef TRACY
const int _y, #include <tracy/Tracy.hpp>
const int _width, #endif
const int _height,
const int _columns, auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height,
const int _rows) -> void const int _columns, const int _rows) -> void
{ {
x = _x; x = _x;
y = _y; y = _y;
@ -55,9 +55,7 @@ auto user_interface::grid::bounds(const int _x, const int _y, const int _width,
const int cell_width = (width - padding) / columns; const int cell_width = (width - padding) / columns;
const int cell_height = (height - padding) / rows; const int cell_height = (height - padding) / rows;
return Rectangle(x + _x * cell_width + padding, return Rectangle(x + _x * cell_width + padding, y + _y * cell_height + padding, _width * cell_width - padding,
y + _y * cell_height + padding,
_width * cell_width - padding,
_height * cell_height - padding); _height * cell_height - padding);
} }
@ -71,9 +69,7 @@ auto user_interface::grid::square_bounds() const -> Rectangle
return bounds; return bounds;
} }
auto user_interface::grid::square_bounds(const int _x, auto user_interface::grid::square_bounds(const int _x, const int _y, const int _width,
const int _y,
const int _width,
const int _height) const -> Rectangle const int _height) const -> Rectangle
{ {
// Assumes each cell is square, so either width or height are not completely // Assumes each cell is square, so either width or height are not completely
@ -92,10 +88,8 @@ auto user_interface::grid::square_bounds(const int _x,
const int x_offset = (width - grid_width) / 2; const int x_offset = (width - grid_width) / 2;
const int y_offset = (height - grid_height) / 2; const int y_offset = (height - grid_height) / 2;
return Rectangle(x_offset + _x * (cell_size + padding) + padding, return Rectangle(x_offset + _x * (cell_size + padding) + padding, y_offset + _y * (cell_size + padding) + padding,
y_offset + _y * (cell_size + padding) + padding, _width * cell_size + padding * (_width - 1), _height * cell_size + padding * (_height - 1));
_width * cell_size + padding * (_width - 1),
_height * cell_size + padding * (_height - 1));
} }
auto user_interface::init() -> void auto user_interface::init() -> void
@ -156,26 +150,16 @@ auto user_interface::get_default_style() -> default_style
// access... // access...
return { return {
{ {
GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL), GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL), GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL),
GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL), GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL), GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED),
GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL), GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED), GuiGetStyle(DEFAULT, TEXT_COLOR_FOCUSED),
GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED), GuiGetStyle(DEFAULT, BORDER_COLOR_PRESSED), GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED),
GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED), GuiGetStyle(DEFAULT, TEXT_COLOR_PRESSED), GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED),
GuiGetStyle(DEFAULT, TEXT_COLOR_FOCUSED), GuiGetStyle(DEFAULT, BASE_COLOR_DISABLED), GuiGetStyle(DEFAULT, TEXT_COLOR_DISABLED)
GuiGetStyle(DEFAULT, BORDER_COLOR_PRESSED),
GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED),
GuiGetStyle(DEFAULT, TEXT_COLOR_PRESSED),
GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED),
GuiGetStyle(DEFAULT, BASE_COLOR_DISABLED),
GuiGetStyle(DEFAULT, TEXT_COLOR_DISABLED)
}, },
GuiGetStyle(DEFAULT, BACKGROUND_COLOR), GuiGetStyle(DEFAULT, BACKGROUND_COLOR), GuiGetStyle(DEFAULT, LINE_COLOR), GuiGetStyle(DEFAULT, TEXT_SIZE),
GuiGetStyle(DEFAULT, LINE_COLOR), GuiGetStyle(DEFAULT, TEXT_SPACING), GuiGetStyle(DEFAULT, TEXT_LINE_SPACING),
GuiGetStyle(DEFAULT, TEXT_SIZE), GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL), GuiGetStyle(DEFAULT, TEXT_WRAP_MODE)
GuiGetStyle(DEFAULT, TEXT_SPACING),
GuiGetStyle(DEFAULT, TEXT_LINE_SPACING),
GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL),
GuiGetStyle(DEFAULT, TEXT_WRAP_MODE)
}; };
} }
@ -211,21 +195,14 @@ auto user_interface::get_component_style(const int component) -> component_style
{ {
return { return {
{ {
GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL),
GuiGetStyle(component, BASE_COLOR_NORMAL), GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED),
GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED),
GuiGetStyle(component, BORDER_COLOR_FOCUSED), GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED),
GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED),
GuiGetStyle(component, TEXT_COLOR_FOCUSED), GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED)
GuiGetStyle(component, BORDER_COLOR_PRESSED),
GuiGetStyle(component, BASE_COLOR_PRESSED),
GuiGetStyle(component, TEXT_COLOR_PRESSED),
GuiGetStyle(component, BORDER_COLOR_DISABLED),
GuiGetStyle(component, BASE_COLOR_DISABLED),
GuiGetStyle(component, TEXT_COLOR_DISABLED)
}, },
GuiGetStyle(component, BORDER_WIDTH), GuiGetStyle(component, BORDER_WIDTH), GuiGetStyle(component, TEXT_PADDING),
GuiGetStyle(component, TEXT_PADDING),
GuiGetStyle(component, TEXT_ALIGNMENT) GuiGetStyle(component, TEXT_ALIGNMENT)
}; };
} }
@ -256,16 +233,11 @@ auto user_interface::set_component_style(const int component, const component_st
auto user_interface::popup_bounds() -> Rectangle auto user_interface::popup_bounds() -> Rectangle
{ {
return Rectangle(static_cast<float>(GetScreenWidth()) / 2.0f - POPUP_WIDTH / 2.0f, return Rectangle(static_cast<float>(GetScreenWidth()) / 2.0f - POPUP_WIDTH / 2.0f,
static_cast<float>(GetScreenHeight()) / 2.0f - POPUP_HEIGHT / 2.0f, static_cast<float>(GetScreenHeight()) / 2.0f - POPUP_HEIGHT / 2.0f, POPUP_WIDTH, POPUP_HEIGHT);
POPUP_WIDTH,
POPUP_HEIGHT);
} }
auto user_interface::draw_button(const Rectangle bounds, auto user_interface::draw_button(const Rectangle bounds, const std::string& label, const Color color,
const std::string& label, const bool enabled, const int font_size) const -> int
const Color color,
const bool enabled,
const int font_size) const -> int
{ {
// Save original styling // Save original styling
const default_style original_default = get_default_style(); const default_style original_default = get_default_style();
@ -295,25 +267,16 @@ auto user_interface::draw_button(const Rectangle bounds,
return pressed; return pressed;
} }
auto user_interface::draw_menu_button(const int x, auto user_interface::draw_menu_button(const int x, const int y, const int width, const int height,
const int y, const std::string& label, const Color color, const bool enabled,
const int width,
const int height,
const std::string& label,
const Color color,
const bool enabled,
const int font_size) const -> int const int font_size) const -> int
{ {
const Rectangle bounds = menu_grid.bounds(x, y, width, height); const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_button(bounds, label, color, enabled, font_size); return draw_button(bounds, label, color, enabled, font_size);
} }
auto user_interface::draw_toggle_slider(const Rectangle bounds, auto user_interface::draw_toggle_slider(const Rectangle bounds, const std::string& off_label,
const std::string& off_label, const std::string& on_label, int* active, Color color, bool enabled,
const std::string& on_label,
int* active,
Color color,
bool enabled,
int font_size) const -> int int font_size) const -> int
{ {
// Save original styling // Save original styling
@ -349,29 +312,16 @@ auto user_interface::draw_toggle_slider(const Rectangle bounds,
return pressed; return pressed;
} }
auto user_interface::draw_menu_toggle_slider(const int x, auto user_interface::draw_menu_toggle_slider(const int x, const int y, const int width, const int height,
const int y, const std::string& off_label, const std::string& on_label, int* active,
const int width, const Color color, const bool enabled, const int font_size) const -> int
const int height,
const std::string& off_label,
const std::string& on_label,
int* active,
const Color color,
const bool enabled,
const int font_size) const -> int
{ {
const Rectangle bounds = menu_grid.bounds(x, y, width, height); const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_toggle_slider(bounds, off_label, on_label, active, color, enabled, font_size); return draw_toggle_slider(bounds, off_label, on_label, active, color, enabled, font_size);
} }
auto user_interface::draw_spinner(Rectangle bounds, auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max, Color color,
const std::string& label, bool enabled, int font_size) const -> int
int* value,
int min,
int max,
Color color,
bool enabled,
int font_size) const -> int
{ {
// Save original styling // Save original styling
const default_style original_default = get_default_style(); const default_style original_default = get_default_style();
@ -406,26 +356,15 @@ auto user_interface::draw_spinner(Rectangle bounds,
return pressed; return pressed;
} }
auto user_interface::draw_menu_spinner(const int x, auto user_interface::draw_menu_spinner(const int x, const int y, const int width, const int height,
const int y, const std::string& label, int* value, const int min, const int max,
const int width, const Color color, const bool enabled, const int font_size) const -> int
const int height,
const std::string& label,
int* value,
const int min,
const int max,
const Color color,
const bool enabled,
const int font_size) const -> int
{ {
const Rectangle bounds = menu_grid.bounds(x, y, width, height); const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_spinner(bounds, label, value, min, max, color, enabled, font_size); return draw_spinner(bounds, label, value, min, max, color, enabled, font_size);
} }
auto user_interface::draw_label(const Rectangle bounds, auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color, const bool enabled,
const std::string& text,
const Color color,
const bool enabled,
const int font_size) const -> int const int font_size) const -> int
{ {
// Save original styling // Save original styling
@ -456,11 +395,7 @@ auto user_interface::draw_label(const Rectangle bounds,
return pressed; return pressed;
} }
auto user_interface::draw_board_block(const int x, auto user_interface::draw_board_block(const int x, const int y, const int width, const int height, const Color color,
const int y,
const int width,
const int height,
const Color color,
const bool enabled) const -> bool const bool enabled) const -> bool
{ {
component_style s = get_component_style(BUTTON); component_style s = get_component_style(BUTTON);
@ -469,7 +404,7 @@ auto user_interface::draw_board_block(const int x,
const Rectangle bounds = board_grid.square_bounds(x, y, width, height); const Rectangle bounds = board_grid.square_bounds(x, y, width, height);
const bool focused = CheckCollisionPointRec(input.mouse - Vector2(0, MENU_HEIGHT), bounds); const bool focused = CheckCollisionPointRec(input.mouse - Vector2(0, MENU_HEIGHT), bounds);
const bool pressed = block(x, y, width, height, false).covers(input.sel_x, input.sel_y); const bool pressed = puzzle::block(x, y, width, height, false).covers(input.sel_x, input.sel_y);
// Background to make faded colors work // Background to make faded colors work
DrawRectangleRec(bounds, RAYWHITE); DrawRectangleRec(bounds, RAYWHITE);
@ -507,15 +442,7 @@ auto user_interface::window_open() const -> bool
auto user_interface::draw_menu_header(const Color color) const -> void auto user_interface::draw_menu_header(const Color color) const -> void
{ {
int preset = static_cast<int>(state.get_current_preset()); int preset = static_cast<int>(state.get_current_preset());
draw_menu_spinner(0, draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1, static_cast<int>(state.get_preset_count()), color,
0,
1,
1,
"Preset: ",
&preset,
-1,
static_cast<int>(state.get_preset_count()),
color,
!input.editing); !input.editing);
if (preset > static_cast<int>(state.get_current_preset())) { if (preset > static_cast<int>(state.get_current_preset())) {
input.load_next_preset(); input.load_next_preset();
@ -523,17 +450,11 @@ auto user_interface::draw_menu_header(const Color color) const -> void
input.load_previous_preset(); input.load_previous_preset();
} }
draw_menu_button(1, draw_menu_button(1, 0, 1, 1, std::format("{}: {}/{} Blocks",
0, state.was_edited()
1, ? "Modified"
1, : std::format("\"{}\"", state.get_current_preset_comment().substr(2)),
std::format("{}: {}/{} Blocks", state.get_current_state().block_count(), puzzle::MAX_BLOCKS), color);
state.was_edited()
? "Modified"
: std::format("\"{}\"", state.get_current_preset_comment().substr(2)),
state.get_current_state().block_count(),
puzzle::MAX_BLOCKS),
color);
int editing = input.editing; int editing = input.editing;
draw_menu_toggle_slider(2, 0, 1, 1, "Puzzle Mode (Tab)", "Edit Mode (Tab)", &editing, color); draw_menu_toggle_slider(2, 0, 1, 1, "Puzzle Mode (Tab)", "Edit Mode (Tab)", &editing, color);
@ -544,22 +465,13 @@ auto user_interface::draw_menu_header(const Color color) const -> void
auto user_interface::draw_graph_info(const Color color) const -> void auto user_interface::draw_graph_info(const Color color) const -> void
{ {
draw_menu_button(0, draw_menu_button(0, 1, 1, 1, std::format("Found {} States ({} Winning)", state.get_state_count(),
1, state.get_target_count()), color);
1,
1,
std::format("Found {} States ({} Winning)", state.get_state_count(), state.get_target_count()),
color);
draw_menu_button(1, 1, 1, 1, std::format("Found {} Transitions", state.get_link_count()), color); draw_menu_button(1, 1, 1, 1, std::format("Found {} Transitions", state.get_link_count()), color);
draw_menu_button(2, draw_menu_button(2, 1, 1, 1, std::format("{} Moves to Nearest Solution",
1, state.get_path_length() > 0 ? state.get_path_length() - 1 : 0), color);
1,
1,
std::format("{} Moves to Nearest Solution",
state.get_path_length() > 0 ? state.get_path_length() - 1 : 0),
color);
} }
auto user_interface::draw_graph_controls(const Color color) const -> void auto user_interface::draw_graph_controls(const Color color) const -> void
@ -596,14 +508,7 @@ auto user_interface::draw_camera_controls(const Color color) const -> void
} }
int lock_camera_mass_center = input.camera_mass_center_lock; int lock_camera_mass_center = input.camera_mass_center_lock;
draw_menu_toggle_slider(1, draw_menu_toggle_slider(1, 3, 1, 1, "Current Block (U)", "Graph Center (U)", &lock_camera_mass_center, color,
3,
1,
1,
"Current Block (U)",
"Graph Center (U)",
&lock_camera_mass_center,
color,
input.camera_lock); input.camera_lock);
if (lock_camera_mass_center != input.camera_mass_center_lock) { if (lock_camera_mass_center != input.camera_mass_center_lock) {
input.toggle_camera_mass_center_lock(); input.toggle_camera_mass_center_lock();
@ -636,12 +541,8 @@ auto user_interface::draw_puzzle_controls(const Color color) const -> void
}; };
const int visits = state.get_current_visits(); const int visits = state.get_current_visits();
draw_menu_button(0, draw_menu_button(0, 4, 1, 1, std::format("{} Moves ({}{} Time at this State)", state.get_total_moves(), visits,
4, nth(visits)), color);
1,
1,
std::format("{} Moves ({}{} Time at this State)", state.get_total_moves(), visits, nth(visits)),
color);
if (draw_menu_button(1, 4, 1, 1, "Make Optimal Move (Space)", color, state.has_distances())) { if (draw_menu_button(1, 4, 1, 1, "Make Optimal Move (Space)", color, state.has_distances())) {
input.goto_optimal_next_state(); input.goto_optimal_next_state();
@ -659,12 +560,7 @@ auto user_interface::draw_puzzle_controls(const Color color) const -> void
input.goto_most_distant_state(); input.goto_most_distant_state();
} }
if (draw_menu_button(2, if (draw_menu_button(2, 5, 1, 1, "Go to Starting State (R)", color,
5,
1,
1,
"Go to Starting State (R)",
color,
state.get_current_index() != state.get_starting_index())) { state.get_current_index() != state.get_starting_index())) {
input.goto_starting_state(); input.goto_starting_state();
} }
@ -742,19 +638,14 @@ auto user_interface::draw_save_preset_popup() -> void
} }
// Returns the pressed button index // Returns the pressed button index
const int button = GuiTextInputBox(popup_bounds(), const int button = GuiTextInputBox(popup_bounds(), "Save as Preset", "Enter Preset Name", "Ok;Cancel",
"Save as Preset", preset_name.data(), 255, nullptr);
"Enter Preset Name",
"Ok;Cancel",
preset_comment.data(),
255,
nullptr);
if (button == 1) { if (button == 1) {
state.save_current_to_preset_file(preset_comment.data()); state.append_preset_file(preset_name.data());
} }
if (button == 0 || button == 1 || button == 2) { if (button == 0 || button == 1 || button == 2) {
save_window = false; save_window = false;
TextCopy(preset_comment.data(), "\0"); TextCopy(preset_name.data(), "\0");
} }
} }
@ -811,11 +702,7 @@ auto user_interface::draw_puzzle_board() -> void
{ {
const puzzle& current = state.get_current_state(); const puzzle& current = state.get_current_state();
board_grid.update_bounds(0, board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, current.get_width(),
MENU_HEIGHT,
GetScreenWidth() / 2,
GetScreenHeight() - MENU_HEIGHT,
current.get_width(),
current.get_height()); current.get_height());
// Draw outer border // Draw outer border
@ -823,22 +710,18 @@ auto user_interface::draw_puzzle_board() -> void
DrawRectangleRec(bounds, current.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED); DrawRectangleRec(bounds, current.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED);
// Draw inner borders // Draw inner borders
DrawRectangle(bounds.x + BOARD_PADDING, DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING, bounds.width - 2 * BOARD_PADDING,
bounds.y + BOARD_PADDING,
bounds.width - 2 * BOARD_PADDING,
bounds.height - 2 * BOARD_PADDING, bounds.height - 2 * BOARD_PADDING,
current.get_restricted() ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE); current.get_restricted() ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE);
// Draw target opening // Draw target opening
// TODO: Only draw single direction (in corner) if restricted (use target block principal // TODO: Only draw single direction (in corner) if restricted (use target block principal
// direction) // direction)
const std::optional<block> target_block = current.try_get_target_block(); const std::optional<puzzle::block> target_block = current.try_get_target_block();
const int target_x = current.get_goal_x(); const int target_x = current.get_goal_x();
const int target_y = current.get_goal_y(); const int target_y = current.get_goal_y();
if (current.get_goal() && target_block) { if (current.get_goal() && target_block) {
auto [x, y, width, height] = board_grid.square_bounds(target_x, auto [x, y, width, height] = board_grid.square_bounds(target_x, target_y, target_block->get_width(),
target_y,
target_block->get_width(),
target_block->get_height()); target_block->get_height());
const Color opening_color = Fade(current.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3); const Color opening_color = Fade(current.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3);
@ -885,7 +768,7 @@ auto user_interface::draw_puzzle_board() -> void
} }
// Draw blocks // Draw blocks
for (const block b : current.block_view()) { for (const puzzle::block b : current.block_view()) {
Color c = BLOCK_COLOR; Color c = BLOCK_COLOR;
if (b.get_target()) { if (b.get_target()) {
c = TARGET_BLOCK_COLOR; c = TARGET_BLOCK_COLOR;
@ -903,22 +786,16 @@ auto user_interface::draw_puzzle_board() -> void
if (current.covers(input.block_add_x, input.block_add_y) && input.hov_x >= input.block_add_x && input.hov_y >= if (current.covers(input.block_add_x, input.block_add_y) && input.hov_x >= input.block_add_x && input.hov_y >=
input.block_add_y) { input.block_add_y) {
bool collides = false; bool collides = false;
for (const block b : current.block_view()) { for (const puzzle::block b : current.block_view()) {
if (b.collides(block(input.block_add_x, if (b.collides(puzzle::block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1,
input.block_add_y, input.hov_y - input.block_add_y + 1, false))) {
input.hov_x - input.block_add_x + 1,
input.hov_y - input.block_add_y + 1,
false))) {
collides = true; collides = true;
break; break;
} }
} }
if (!collides) { if (!collides) {
draw_board_block(input.block_add_x, draw_board_block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1,
input.block_add_y, input.hov_y - input.block_add_y + 1, PURPLE);
input.hov_x - input.block_add_x + 1,
input.hov_y - input.block_add_y + 1,
PURPLE);
} }
} }
} }
@ -930,8 +807,7 @@ auto user_interface::draw_puzzle_board() -> void
// Draw goal boundaries when editing // Draw goal boundaries when editing
if (input.editing && current.get_goal() && target_block) { if (input.editing && current.get_goal() && target_block) {
DrawRectangleLinesEx( DrawRectangleLinesEx(
board_grid.square_bounds(target_x, target_y, target_block->get_width(), target_block->get_height()), board_grid.square_bounds(target_x, target_y, target_block->get_width(), target_block->get_height()), 2.0,
2.0,
TARGET_BLOCK_COLOR); TARGET_BLOCK_COLOR);
} }
} }

View File

@ -1,74 +0,0 @@
// ReSharper disable CppLocalVariableMayBeConst
#include "puzzle.hpp"
#include <random>
#include <catch2/catch_test_macros.hpp>
TEST_CASE("bitmap_is_full all bits set", "[puzzle][board]")
{
puzzle p1(5, 5);
puzzle p2(3, 4);
puzzle p3(5, 4);
puzzle p4(3, 7);
u64 bitmap = -1;
REQUIRE(p1.bitmap_is_full(bitmap));
REQUIRE(p2.bitmap_is_full(bitmap));
REQUIRE(p3.bitmap_is_full(bitmap));
REQUIRE(p4.bitmap_is_full(bitmap));
}
TEST_CASE("bitmap_is_full no bits set", "[puzzle][board]")
{
puzzle p1(5, 5);
puzzle p2(3, 4);
puzzle p3(5, 4);
puzzle p4(3, 7);
u64 bitmap = 0;
REQUIRE_FALSE(p1.bitmap_is_full(bitmap));
REQUIRE_FALSE(p2.bitmap_is_full(bitmap));
REQUIRE_FALSE(p3.bitmap_is_full(bitmap));
REQUIRE_FALSE(p4.bitmap_is_full(bitmap));
}
TEST_CASE("bitmap_is_full necessary bits set", "[puzzle][board]")
{
puzzle p1(5, 5);
puzzle p2(3, 4);
puzzle p3(5, 4);
puzzle p4(3, 7);
u64 bitmap1 = (1ull << 25) - 1; // 5 * 5
u64 bitmap2 = (1ull << 12) - 1; // 3 * 4
u64 bitmap3 = (1ull << 20) - 1; // 5 * 4
u64 bitmap4 = (1ull << 21) - 1; // 3 * 7
REQUIRE(p1.bitmap_is_full(bitmap1));
REQUIRE(p2.bitmap_is_full(bitmap2));
REQUIRE(p3.bitmap_is_full(bitmap3));
REQUIRE(p4.bitmap_is_full(bitmap4));
}
TEST_CASE("bitmap_is_full necessary bits not set", "[puzzle][board]")
{
puzzle p1(5, 5);
puzzle p2(3, 4);
puzzle p3(5, 4);
puzzle p4(3, 7);
u64 bitmap1 = (1ull << 25) - 1; // 5 * 5
u64 bitmap2 = (1ull << 12) - 1; // 3 * 4
u64 bitmap3 = (1ull << 20) - 1; // 5 * 4
u64 bitmap4 = (1ull << 21) - 1; // 3 * 7
bitmap1 &= ~(1ull << 12);
bitmap2 &= ~(1ull << 6);
bitmap3 &= ~(1ull << 8);
bitmap4 &= ~(1ull << 18);
REQUIRE_FALSE(p1.bitmap_is_full(bitmap1));
REQUIRE_FALSE(p2.bitmap_is_full(bitmap2));
REQUIRE_FALSE(p3.bitmap_is_full(bitmap3));
REQUIRE_FALSE(p4.bitmap_is_full(bitmap4));
}

View File

@ -1,266 +0,0 @@
// ReSharper disable CppLocalVariableMayBeConst
#include "puzzle.hpp"
#include <random>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
static auto board_mask(const int w, const int h) -> u64
{
const int cells = w * h;
if (cells == 64) {
return ~0ULL;
}
return (1ULL << cells) - 1ULL;
}
TEST_CASE("Empty board returns (0,0)", "[puzzle][board]")
{
puzzle p(5, 5);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(0ULL, x, y));
REQUIRE(x == 0);
REQUIRE(y == 0);
}
TEST_CASE("Full board detection respects width*height only", "[puzzle][board]")
{
auto [w, h] = GENERATE(std::tuple{3, 3}, std::tuple{4, 4}, std::tuple{5, 6}, std::tuple{8, 8});
puzzle p(w, h);
u64 mask = board_mask(w, h);
int x = -1, y = -1;
REQUIRE_FALSE(p.bitmap_find_first_empty(mask, x, y));
// Bits outside board should not affect fullness
REQUIRE_FALSE(p.bitmap_find_first_empty(mask | (~mask), x, y));
}
TEST_CASE("Single empty cell at various positions", "[puzzle][board]")
{
auto [w, h] = GENERATE(std::tuple{3, 3}, std::tuple{4, 4}, std::tuple{5, 5}, std::tuple{8, 8});
puzzle p(w, h);
int cells = w * h;
auto empty_index = GENERATE_COPY(values<int>({ 0, cells / 2, cells - 1}));
u64 bitmap = board_mask(w, h);
bitmap &= ~(1ULL << empty_index);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
REQUIRE(x == empty_index % w);
REQUIRE(y == empty_index / w);
}
TEST_CASE("Bits outside board are ignored", "[puzzle][board]")
{
puzzle p(3, 3); // 9 cells
u64 mask = board_mask(3, 3);
// Board is full
u64 bitmap = mask;
// Set extra bits outside 9 cells
bitmap |= (1ULL << 20);
bitmap |= (1ULL << 63);
int x = -1, y = -1;
REQUIRE_FALSE(p.bitmap_find_first_empty(bitmap, x, y));
}
TEST_CASE("First empty found in forward search branch", "[puzzle][branch]")
{
puzzle p(4, 4); // 16 cells
// Only MSB (within board) set
u64 bitmap = (1ULL << 15);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
REQUIRE(x == 0);
REQUIRE(y == 0);
}
TEST_CASE("Backward search branch finds gap before MSB cluster", "[puzzle][branch]")
{
puzzle p(4, 4); // 16 cells
// Set bits 15,14,13 but leave 12 empty
u64 bitmap = (1ULL << 15) | (1ULL << 14) | (1ULL << 13);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
REQUIRE(x == 0);
REQUIRE(y == 0);
}
TEST_CASE("Rectangular board coordinate mapping", "[puzzle][rect]")
{
puzzle p(5, 3); // 15 cells
int empty_index = 11;
u64 bitmap = board_mask(5, 3);
bitmap &= ~(1ULL << empty_index);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
REQUIRE(x == empty_index % 5);
REQUIRE(y == empty_index / 5);
}
TEST_CASE("Non-64-sized board near limit", "[puzzle][limit]")
{
puzzle p(7, 8); // 56 cells
u64 mask = board_mask(7, 8);
// Full board should return false
int x = -1, y = -1;
REQUIRE_FALSE(p.bitmap_find_first_empty(mask, x, y));
// Clear highest valid cell
int empty_index = 55;
mask &= ~(1ULL << empty_index);
REQUIRE(p.bitmap_find_first_empty(mask, x, y));
REQUIRE(x == empty_index % 7);
REQUIRE(y == empty_index / 7);
}
// --- Oracle: find first zero bit inside board ---
static auto oracle_find_first_empty(u64 bitmap, int w, int h, int& x, int& y) -> bool
{
int cells = w * h;
for (int i = 0; i < cells; ++i) {
if ((bitmap & (1ULL << i)) == 0) {
x = i % w;
y = i / w;
return true;
}
}
return false;
}
TEST_CASE("Oracle validation across board sizes 3x3 to 8x8", "[puzzle][oracle]")
{
auto [w, h] = GENERATE(std::tuple{3, 3}, std::tuple{4, 4}, std::tuple{5, 5}, std::tuple{6, 6}, std::tuple{7, 7},
std::tuple{8, 8}, std::tuple{3, 5}, std::tuple{5, 3}, std::tuple{7, 8}, std::tuple{8, 7});
puzzle p(w, h);
u64 mask = board_mask(w, h);
std::mt19937_64 rng(12345);
std::uniform_int_distribution<u64> dist(0, UINT64_MAX);
for (int iteration = 0; iteration < 200; ++iteration) {
u64 bitmap = dist(rng);
int ox = -1, oy = -1;
bool oracle_result = oracle_find_first_empty(bitmap, w, h, ox, oy);
int x = -1, y = -1;
bool result = p.bitmap_find_first_empty(bitmap, x, y);
REQUIRE(result == oracle_result);
if (result) {
REQUIRE(x == ox);
REQUIRE(y == oy);
}
}
}
TEST_CASE("Bits set outside board only behaves as empty board", "[puzzle][outside]")
{
puzzle p(3, 3); // 9 cells
u64 bitmap = (1ULL << 40) | (1ULL << 63);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
REQUIRE(x == 0);
REQUIRE(y == 0);
}
TEST_CASE("Last valid cell empty stresses upper bound", "[puzzle][boundary]")
{
auto [w, h] = GENERATE(std::tuple{4, 4}, std::tuple{5, 6}, std::tuple{7, 8}, std::tuple{8, 8});
puzzle p(w, h);
int cells = w * h;
u64 bitmap = board_mask(w, h);
// Clear last valid bit
bitmap &= ~(1ULL << (cells - 1));
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
REQUIRE(x == (cells - 1) % w);
REQUIRE(y == (cells - 1) / w);
}
TEST_CASE("Board sizes between 33 and 63 cells", "[puzzle][midrange]")
{
auto [w, h] = GENERATE(std::tuple{6, 6}, // 36
std::tuple{7, 6}, // 42
std::tuple{7, 7}, // 49
std::tuple{8, 7}, // 56
std::tuple{7, 8} // 56
);
puzzle p(w, h);
int cells = w * h;
for (int index : {31, 32, cells - 2}) {
if (index >= cells) continue;
u64 bitmap = board_mask(w, h);
bitmap &= ~(1ULL << index);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
REQUIRE(x == index % w);
REQUIRE(y == index / w);
}
}
TEST_CASE("Multiple holes choose lowest index", "[puzzle][multiple]")
{
puzzle p(5, 5);
u64 bitmap = board_mask(5, 5);
// Clear several positions
bitmap &= ~(1ULL << 3);
bitmap &= ~(1ULL << 7);
bitmap &= ~(1ULL << 12);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
// Oracle expectation: index 3
REQUIRE(x == 3 % 5);
REQUIRE(y == 3 / 5);
}

View File

@ -2,7 +2,7 @@
#include <catch2/catch_template_test_macros.hpp> #include <catch2/catch_template_test_macros.hpp>
#include <cstdint> #include <cstdint>
#include "bits.hpp" #include "util.hpp"
// ============================================================================= // =============================================================================
// Catch2 // Catch2
@ -31,8 +31,8 @@
// 4. TEMPLATE_TEST_CASE(name, tags, Type1, Type2, ...) // 4. TEMPLATE_TEST_CASE(name, tags, Type1, Type2, ...)
// A parameterised test that is instantiated once for each type listed. // A parameterised test that is instantiated once for each type listed.
// Inside the test body, the alias `TestType` refers to the current type. // Inside the test body, the alias `TestType` refers to the current type.
// This avoids duplicating identical logic for u8, u16, u32, // This avoids duplicating identical logic for uint8_t, uint16_t, uint32_t,
// and u64. Catch2 automatically appends the type name to the test name // and uint64_t. Catch2 automatically appends the type name to the test name
// in the output so you can see which instantiation failed. // in the output so you can see which instantiation failed.
// //
// 5. Tags (e.g. "[create_mask]", "[round-trip]") // 5. Tags (e.g. "[create_mask]", "[round-trip]")
@ -49,7 +49,7 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("create_mask produces correct masks", "[create_mask]", TEMPLATE_TEST_CASE("create_mask produces correct masks", "[create_mask]",
u8, u16, u32, u64) uint8_t, uint16_t, uint32_t, uint64_t)
{ {
SECTION("single bit mask at bit 0") { SECTION("single bit mask at bit 0") {
auto m = create_mask<TestType>(0, 0); auto m = create_mask<TestType>(0, 0);
@ -72,16 +72,16 @@ TEMPLATE_TEST_CASE("create_mask produces correct masks", "[create_mask]",
} }
SECTION("full-width mask returns all ones") { SECTION("full-width mask returns all ones") {
constexpr u8 last = sizeof(TestType) * 8 - 1; constexpr uint8_t last = sizeof(TestType) * 8 - 1;
auto m = create_mask<TestType>(0, last); auto m = create_mask<TestType>(0, last);
REQUIRE(m == static_cast<TestType>(~TestType{0})); REQUIRE(m == static_cast<TestType>(~TestType{0}));
} }
} }
TEST_CASE("create_mask 32-bit specific cases", "[create_mask]") { TEST_CASE("create_mask 32-bit specific cases", "[create_mask]") {
REQUIRE(create_mask<u32>(0, 15) == 0x0000FFFF); REQUIRE(create_mask<uint32_t>(0, 15) == 0x0000FFFF);
REQUIRE(create_mask<u32>(0, 31) == 0xFFFFFFFF); REQUIRE(create_mask<uint32_t>(0, 31) == 0xFFFFFFFF);
REQUIRE(create_mask<u32>(16, 31) == 0xFFFF0000); REQUIRE(create_mask<uint32_t>(16, 31) == 0xFFFF0000);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -89,11 +89,11 @@ TEST_CASE("create_mask 32-bit specific cases", "[create_mask]") {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("clear_bits zeroes the specified range", "[clear_bits]", TEMPLATE_TEST_CASE("clear_bits zeroes the specified range", "[clear_bits]",
u8, u16, u32, u64) uint8_t, uint16_t, uint32_t, uint64_t)
{ {
SECTION("clear all bits") { SECTION("clear all bits") {
TestType val = static_cast<TestType>(~TestType{0}); TestType val = static_cast<TestType>(~TestType{0});
constexpr u8 last = sizeof(TestType) * 8 - 1; constexpr uint8_t last = sizeof(TestType) * 8 - 1;
clear_bits(val, 0, last); clear_bits(val, 0, last);
REQUIRE(val == TestType{0}); REQUIRE(val == TestType{0});
} }
@ -128,55 +128,55 @@ TEMPLATE_TEST_CASE("clear_bits zeroes the specified range", "[clear_bits]",
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("set_bits writes value into the specified range", "[set_bits]", TEMPLATE_TEST_CASE("set_bits writes value into the specified range", "[set_bits]",
u8, u16, u32, u64) uint8_t, uint16_t, uint32_t, uint64_t)
{ {
SECTION("set lower nibble on zero") { SECTION("set lower nibble on zero") {
TestType val = TestType{0}; TestType val = TestType{0};
set_bits(val, u8{0}, u8{3}, static_cast<TestType>(0xA)); set_bits(val, uint8_t{0}, uint8_t{3}, static_cast<TestType>(0xA));
REQUIRE(val == static_cast<TestType>(0x0A)); REQUIRE(val == static_cast<TestType>(0x0A));
} }
SECTION("set upper nibble on zero") { SECTION("set upper nibble on zero") {
TestType val = TestType{0}; TestType val = TestType{0};
set_bits(val, u8{4}, u8{7}, static_cast<TestType>(0xB)); set_bits(val, uint8_t{4}, uint8_t{7}, static_cast<TestType>(0xB));
REQUIRE(val == static_cast<TestType>(0xB0)); REQUIRE(val == static_cast<TestType>(0xB0));
} }
SECTION("set_bits replaces existing bits") { SECTION("set_bits replaces existing bits") {
TestType val = static_cast<TestType>(0xFF); TestType val = static_cast<TestType>(0xFF);
set_bits(val, u8{0}, u8{3}, static_cast<TestType>(0x5)); set_bits(val, uint8_t{0}, uint8_t{3}, static_cast<TestType>(0x5));
REQUIRE(val == static_cast<TestType>(0xF5)); REQUIRE(val == static_cast<TestType>(0xF5));
} }
SECTION("set single bit to 1") { SECTION("set single bit to 1") {
TestType val = TestType{0}; TestType val = TestType{0};
set_bits(val, u8{3}, u8{3}, static_cast<TestType>(1)); set_bits(val, uint8_t{3}, uint8_t{3}, static_cast<TestType>(1));
REQUIRE(val == static_cast<TestType>(0x08)); REQUIRE(val == static_cast<TestType>(0x08));
} }
SECTION("set single bit to 0") { SECTION("set single bit to 0") {
TestType val = static_cast<TestType>(0xFF); TestType val = static_cast<TestType>(0xFF);
set_bits(val, u8{3}, u8{3}, static_cast<TestType>(0)); set_bits(val, uint8_t{3}, uint8_t{3}, static_cast<TestType>(0));
REQUIRE(val == static_cast<TestType>(0xF7)); REQUIRE(val == static_cast<TestType>(0xF7));
} }
SECTION("setting value 0 clears the range") { SECTION("setting value 0 clears the range") {
TestType val = static_cast<TestType>(0xFF); TestType val = static_cast<TestType>(0xFF);
set_bits(val, u8{0}, u8{7}, static_cast<TestType>(0)); set_bits(val, uint8_t{0}, uint8_t{7}, static_cast<TestType>(0));
REQUIRE(val == TestType{0}); REQUIRE(val == TestType{0});
} }
} }
TEST_CASE("set_bits with different value type (U != T)", "[set_bits]") { TEST_CASE("set_bits with different value type (U != T)", "[set_bits]") {
u32 val = 0; uint32_t val = 0;
constexpr u8 small_val = 0x3F; constexpr uint8_t small_val = 0x3F;
set_bits(val, u8{8}, u8{13}, small_val); set_bits(val, uint8_t{8}, uint8_t{13}, small_val);
REQUIRE(val == (u32{0x3F} << 8)); REQUIRE(val == (uint32_t{0x3F} << 8));
} }
TEST_CASE("set_bits preserves surrounding bits in 32-bit", "[set_bits]") { TEST_CASE("set_bits preserves surrounding bits in 32-bit", "[set_bits]") {
u32 val = 0xDEADBEEF; uint32_t val = 0xDEADBEEF;
set_bits(val, u8{8}, u8{15}, u32{0x42}); set_bits(val, uint8_t{8}, uint8_t{15}, uint32_t{0x42});
REQUIRE(val == 0xDEAD42EF); REQUIRE(val == 0xDEAD42EF);
} }
@ -185,53 +185,53 @@ TEST_CASE("set_bits preserves surrounding bits in 32-bit", "[set_bits]") {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("get_bits extracts the specified range", "[get_bits]", TEMPLATE_TEST_CASE("get_bits extracts the specified range", "[get_bits]",
u8, u16, u32, u64) uint8_t, uint16_t, uint32_t, uint64_t)
{ {
SECTION("get lower nibble") { SECTION("get lower nibble") {
TestType val = static_cast<TestType>(0xAB); TestType val = static_cast<TestType>(0xAB);
auto result = get_bits(val, u8{0}, u8{3}); auto result = get_bits(val, uint8_t{0}, uint8_t{3});
REQUIRE(result == TestType{0xB}); REQUIRE(result == TestType{0xB});
} }
SECTION("get upper nibble") { SECTION("get upper nibble") {
TestType val = static_cast<TestType>(0xAB); TestType val = static_cast<TestType>(0xAB);
auto result = get_bits(val, u8{4}, u8{7}); auto result = get_bits(val, uint8_t{4}, uint8_t{7});
REQUIRE(result == TestType{0xA}); REQUIRE(result == TestType{0xA});
} }
SECTION("get single bit that is set") { SECTION("get single bit that is set") {
TestType val = static_cast<TestType>(0x08); TestType val = static_cast<TestType>(0x08);
auto result = get_bits(val, u8{3}, u8{3}); auto result = get_bits(val, uint8_t{3}, uint8_t{3});
REQUIRE(result == TestType{1}); REQUIRE(result == TestType{1});
} }
SECTION("get single bit that is clear") { SECTION("get single bit that is clear") {
TestType val = static_cast<TestType>(0xF7); TestType val = static_cast<TestType>(0xF7);
auto result = get_bits(val, u8{3}, u8{3}); auto result = get_bits(val, uint8_t{3}, uint8_t{3});
REQUIRE(result == TestType{0}); REQUIRE(result == TestType{0});
} }
SECTION("get all bits") { SECTION("get all bits") {
TestType val = static_cast<TestType>(~TestType{0}); TestType val = static_cast<TestType>(~TestType{0});
constexpr u8 last = sizeof(TestType) * 8 - 1; constexpr uint8_t last = sizeof(TestType) * 8 - 1;
auto result = get_bits(val, u8{0}, last); auto result = get_bits(val, uint8_t{0}, last);
REQUIRE(result == val); REQUIRE(result == val);
} }
SECTION("get from zero returns zero") { SECTION("get from zero returns zero") {
TestType val = TestType{0}; TestType val = TestType{0};
auto result = get_bits(val, u8{0}, u8{7}); auto result = get_bits(val, uint8_t{0}, uint8_t{7});
REQUIRE(result == TestType{0}); REQUIRE(result == TestType{0});
} }
} }
TEST_CASE("get_bits 32-bit specific extractions", "[get_bits]") { TEST_CASE("get_bits 32-bit specific extractions", "[get_bits]") {
constexpr u32 val = 0xDEADBEEF; constexpr uint32_t val = 0xDEADBEEF;
REQUIRE(get_bits(val, u8{0}, u8{7}) == 0xEF); REQUIRE(get_bits(val, uint8_t{0}, uint8_t{7}) == 0xEF);
REQUIRE(get_bits(val, u8{8}, u8{15}) == 0xBE); REQUIRE(get_bits(val, uint8_t{8}, uint8_t{15}) == 0xBE);
REQUIRE(get_bits(val, u8{16}, u8{23}) == 0xAD); REQUIRE(get_bits(val, uint8_t{16}, uint8_t{23}) == 0xAD);
REQUIRE(get_bits(val, u8{24}, u8{31}) == 0xDE); REQUIRE(get_bits(val, uint8_t{24}, uint8_t{31}) == 0xDE);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -239,29 +239,29 @@ TEST_CASE("get_bits 32-bit specific extractions", "[get_bits]") {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_CASE("set_bits then get_bits round-trips correctly", "[round-trip]") { TEST_CASE("set_bits then get_bits round-trips correctly", "[round-trip]") {
u32 reg = 0; uint32_t reg = 0;
set_bits(reg, u8{4}, u8{11}, u32{0xAB}); set_bits(reg, uint8_t{4}, uint8_t{11}, uint32_t{0xAB});
REQUIRE(get_bits(reg, u8{4}, u8{11}) == 0xAB); REQUIRE(get_bits(reg, uint8_t{4}, uint8_t{11}) == 0xAB);
REQUIRE(get_bits(reg, u8{0}, u8{3}) == 0x0); REQUIRE(get_bits(reg, uint8_t{0}, uint8_t{3}) == 0x0);
REQUIRE(get_bits(reg, u8{12}, u8{31}) == 0x0); REQUIRE(get_bits(reg, uint8_t{12}, uint8_t{31}) == 0x0);
} }
TEST_CASE("multiple set_bits on different ranges", "[round-trip]") { TEST_CASE("multiple set_bits on different ranges", "[round-trip]") {
u32 reg = 0; uint32_t reg = 0;
set_bits(reg, u8{0}, u8{7}, u32{0x01}); set_bits(reg, uint8_t{0}, uint8_t{7}, uint32_t{0x01});
set_bits(reg, u8{8}, u8{15}, u32{0x02}); set_bits(reg, uint8_t{8}, uint8_t{15}, uint32_t{0x02});
set_bits(reg, u8{16}, u8{23}, u32{0x03}); set_bits(reg, uint8_t{16}, uint8_t{23}, uint32_t{0x03});
set_bits(reg, u8{24}, u8{31}, u32{0x04}); set_bits(reg, uint8_t{24}, uint8_t{31}, uint32_t{0x04});
REQUIRE(reg == 0x04030201); REQUIRE(reg == 0x04030201);
} }
TEST_CASE("64-bit round-trip", "[round-trip]") { TEST_CASE("64-bit round-trip", "[round-trip]") {
u64 reg = 0; uint64_t reg = 0;
set_bits(reg, u8{32}, u8{63}, u64{0xCAFEBABE}); set_bits(reg, uint8_t{32}, uint8_t{63}, uint64_t{0xCAFEBABE});
REQUIRE(get_bits(reg, u8{32}, u8{63}) == u64{0xCAFEBABE}); REQUIRE(get_bits(reg, uint8_t{32}, uint8_t{63}) == uint64_t{0xCAFEBABE});
REQUIRE(get_bits(reg, u8{0}, u8{31}) == u64{0}); REQUIRE(get_bits(reg, uint8_t{0}, uint8_t{31}) == uint64_t{0});
} }

File diff suppressed because it is too large Load Diff