fix octree corruption bug because of node vector reallocation

This commit is contained in:
2026-02-23 13:33:32 +01:00
parent 30b02c13ed
commit 861fb34d39
7 changed files with 78 additions and 61 deletions

View File

@ -4,8 +4,8 @@
#include <raylib.h> #include <raylib.h>
#define PRINT_TIMINGS #define PRINT_TIMINGS
// #define WEB // Disables multithreading
#define BARNES_HUT // Use octree BH instead of uniform grid #define BARNES_HUT // Use octree BH instead of uniform grid
// #define WEB // Disables multithreading
// Window // Window
constexpr int INITIAL_WIDTH = 800; constexpr int INITIAL_WIDTH = 800;
@ -31,7 +31,7 @@ constexpr float ROT_SPEED = 1.0;
// Physics Engine // Physics Engine
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 TIMESTEP = 1.0 / 60; // Do 60 physics updates per second constexpr float TIMESTEP = 1.0 / 90; // Do 90 physics updates per second
constexpr float MASS = 1.0; // Mass spring system constexpr float MASS = 1.0; // Mass spring system
constexpr float SPRING_CONSTANT = 5.0; // Mass spring system constexpr float SPRING_CONSTANT = 5.0; // Mass spring system
constexpr float DAMPENING_CONSTANT = 1.0; // Mass spring system constexpr float DAMPENING_CONSTANT = 1.0; // Mass spring system

View File

@ -20,7 +20,8 @@ public:
: mass_center(Vector3Zero()), mass_total(0.0), : mass_center(Vector3Zero()), mass_total(0.0),
children(-1, -1, -1, -1, -1, -1, -1, -1), mass_id(-1), leaf(true) {} children(-1, -1, -1, -1, -1, -1, -1, -1), mass_id(-1), leaf(true) {}
~OctreeNode() {} public:
auto ChildCount() const -> int;
}; };
class Octree { class Octree {
@ -35,8 +36,6 @@ public:
Octree(Octree &&move) = delete; Octree(Octree &&move) = delete;
Octree &operator=(Octree &&move) = delete; Octree &operator=(Octree &&move) = delete;
~Octree() {}
public: public:
auto CreateNode(const Vector3 &box_min, const Vector3 &box_max) -> int; auto CreateNode(const Vector3 &box_min, const Vector3 &box_max) -> int;
@ -47,7 +46,9 @@ public:
auto Insert(int node_idx, int mass_id, const Vector3 &pos, float mass) auto Insert(int node_idx, int mass_id, const Vector3 &pos, float mass)
-> void; -> void;
auto CalculateForce(int node_idx, const Vector3 &pos) -> Vector3; auto CalculateForce(int node_idx, const Vector3 &pos) const -> Vector3;
auto Print() const -> void;
}; };
#endif #endif

View File

@ -106,8 +106,6 @@ public:
MassSpringSystem(MassSpringSystem &move) = delete; MassSpringSystem(MassSpringSystem &move) = delete;
MassSpringSystem &operator=(MassSpringSystem &&move) = delete; MassSpringSystem &operator=(MassSpringSystem &&move) = delete;
~MassSpringSystem() {};
private: private:
#ifdef BARNES_HUT #ifdef BARNES_HUT
auto BuildOctree() -> void; auto BuildOctree() -> void;

View File

@ -23,6 +23,9 @@
// - Click states to display them in the board // - Click states to display them in the board
// - Find shortest path to any winning state and mark it in the graph // - Find shortest path to any winning state and mark it in the graph
// - Also mark the next move along the path on the board // - Also mark the next move along the path on the board
// TODO: Smooth camera (on target change)
// TODO: Mark the starting state
// TODO: Mark the visited states
auto main(int argc, char *argv[]) -> int { auto main(int argc, char *argv[]) -> int {
// if (argc < 2) { // if (argc < 2) {
@ -52,10 +55,10 @@ auto main(int argc, char *argv[]) -> int {
std::chrono::duration<double, std::milli>(0); std::chrono::duration<double, std::milli>(0);
std::chrono::duration<double, std::milli> render_time_accumulator = std::chrono::duration<double, std::milli> render_time_accumulator =
std::chrono::duration<double, std::milli>(0); std::chrono::duration<double, std::milli>(0);
int loop_count = 0; long loop_count = 0;
#endif #endif
float timestep_accumulator = 0.0; double timestep_accumulator = 0.0;
int update_accumulator = 0; long update_accumulator = 0;
while (!WindowShouldClose()) { while (!WindowShouldClose()) {
timestep_accumulator += GetFrameTime(); timestep_accumulator += GetFrameTime();
@ -123,8 +126,9 @@ auto main(int argc, char *argv[]) -> int {
<< render_time_accumulator / loop_count << "." << std::endl; << render_time_accumulator / loop_count << "." << std::endl;
std::cout << " - Physics updates avg: " std::cout << " - Physics updates avg: "
<< static_cast<float>(update_accumulator) / loop_count << static_cast<float>(update_accumulator) / loop_count
<< "x per frame (" << timestep_accumulator << "s remaining)." << "x per frame ("
<< std::endl; << static_cast<int>(timestep_accumulator / TIMESTEP)
<< "x behind)." << std::endl;
last_print_time = GetTime(); last_print_time = GetTime();
physics_time_accumulator = std::chrono::duration<double, std::milli>(0); physics_time_accumulator = std::chrono::duration<double, std::milli>(0);
render_time_accumulator = std::chrono::duration<double, std::milli>(0); render_time_accumulator = std::chrono::duration<double, std::milli>(0);

View File

@ -1,8 +1,20 @@
#include "octree.hpp" #include "octree.hpp"
#include "config.hpp" #include "config.hpp"
#include "util.hpp"
#include <iostream>
#include <raymath.h> #include <raymath.h>
auto OctreeNode::ChildCount() const -> int {
int child_count = 0;
for (int child : children) {
if (child != -1) {
++child_count;
}
}
return child_count;
}
auto Octree::CreateNode(const Vector3 &box_min, const Vector3 &box_max) -> int { auto Octree::CreateNode(const Vector3 &box_min, const Vector3 &box_max) -> int {
OctreeNode node; OctreeNode node;
node.box_min = box_min; node.box_min = box_min;
@ -43,8 +55,8 @@ auto Octree::GetChildBounds(int node_idx, int octant)
(node.box_min.y + node.box_max.y) / 2.0, (node.box_min.y + node.box_max.y) / 2.0,
(node.box_min.z + node.box_max.z) / 2.0); (node.box_min.z + node.box_max.z) / 2.0);
Vector3 min; Vector3 min = Vector3Zero();
Vector3 max; Vector3 max = Vector3Zero();
// If (octant & 1), the octant is to the right of the node region's x-axis // 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 // (see GetOctant). This means the left bound is the x-axis and the right
@ -61,31 +73,34 @@ auto Octree::GetChildBounds(int node_idx, int octant)
auto Octree::Insert(int node_idx, int mass_id, const Vector3 &pos, float mass) auto Octree::Insert(int node_idx, int mass_id, const Vector3 &pos, float mass)
-> void { -> void {
OctreeNode &node = nodes[node_idx]; // NOTE: Do not store a nodes[node_idx] reference beforehand as the nodes
// vector might reallocate during this function
if (node.leaf && node.mass_id == -1) { if (nodes[node_idx].leaf && nodes[node_idx].mass_id == -1) {
// We can place the particle in the empty leaf // We can place the particle in the empty leaf
node.mass_id = mass_id; nodes[node_idx].mass_id = mass_id;
node.mass_center = pos; nodes[node_idx].mass_center = pos;
node.mass_total = mass; nodes[node_idx].mass_total = mass;
return; return;
} }
if (node.leaf) { if (nodes[node_idx].leaf) {
// The leaf is occupied, we need to subdivide // The leaf is occupied, we need to subdivide
int existing_id = node.mass_id; int existing_id = nodes[node_idx].mass_id;
Vector3 existing_pos = node.mass_center; Vector3 existing_pos = nodes[node_idx].mass_center;
float existing_mass = node.mass_total; float existing_mass = nodes[node_idx].mass_total;
node.mass_id = -1; nodes[node_idx].mass_id = -1;
node.leaf = false; nodes[node_idx].leaf = false;
nodes[node_idx].mass_total = 0.0;
// Re-add the existing mass into a new empty leaf (see above) // Re-insert the existing mass into a new empty leaf (see above)
int oct = GetOctant(node_idx, existing_pos); int oct = GetOctant(node_idx, existing_pos);
if (node.children[oct] == -1) { if (nodes[node_idx].children[oct] == -1) {
auto [min, max] = GetChildBounds(node_idx, oct); auto [min, max] = GetChildBounds(node_idx, oct);
node.children[oct] = CreateNode(min, max); nodes[node_idx].children[oct] = CreateNode(min, max);
} }
Insert(node.children[oct], existing_id, existing_pos, existing_mass); Insert(nodes[node_idx].children[oct], existing_id, existing_pos,
existing_mass);
} }
// Insert the new mass // Insert the new mass
@ -97,23 +112,25 @@ auto Octree::Insert(int node_idx, int mass_id, const Vector3 &pos, float mass)
Insert(nodes[node_idx].children[oct], mass_id, pos, mass); Insert(nodes[node_idx].children[oct], mass_id, pos, mass);
// Update the center of mass // Update the center of mass
node = nodes[node_idx]; float new_mass = nodes[node_idx].mass_total + mass;
float new_mass = node.mass_total + mass; nodes[node_idx].mass_center.x =
node.mass_center.x = (nodes[node_idx].mass_center.x * nodes[node_idx].mass_total + pos.x) /
(node.mass_center.x * node.mass_total + pos.x) / new_mass; new_mass;
node.mass_center.y = nodes[node_idx].mass_center.y =
(node.mass_center.y * node.mass_total + pos.y) / new_mass; (nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) /
node.mass_center.z = new_mass;
(node.mass_center.z * node.mass_total + pos.z) / new_mass; nodes[node_idx].mass_center.z =
node.mass_total = new_mass; (nodes[node_idx].mass_center.z * nodes[node_idx].mass_total + pos.z) /
new_mass;
nodes[node_idx].mass_total = new_mass;
} }
auto Octree::CalculateForce(int node_idx, const Vector3 &pos) -> Vector3 { auto Octree::CalculateForce(int node_idx, const Vector3 &pos) const -> Vector3 {
if (node_idx < 0) { if (node_idx < 0) {
return Vector3Zero(); return Vector3Zero();
} }
OctreeNode &node = nodes[node_idx]; const OctreeNode &node = nodes[node_idx];
if (node.mass_total == 0.0f) { if (node.mass_total == 0.0f) {
return Vector3Zero(); return Vector3Zero();
} }
@ -146,3 +163,11 @@ auto Octree::CalculateForce(int node_idx, const Vector3 &pos) -> Vector3 {
return force; return force;
} }
auto Octree::Print() const -> void {
std::cout << "Octree Start ===========================" << std::endl;
for (const auto &node : nodes) {
std::cout << "Center: " << node.mass_center << ", Mass: " << node.mass_total
<< ", Direct Children: " << node.ChildCount() << std::endl;
}
}

View File

@ -1,5 +1,6 @@
#include "physics.hpp" #include "physics.hpp"
#include "config.hpp" #include "config.hpp"
#include "util.hpp"
#include <algorithm> #include <algorithm>
#include <cfloat> #include <cfloat>
@ -249,15 +250,9 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void {
solve_octree(i); solve_octree(i);
} }
#else #else
threads.detach_blocks( BS::multi_future<void> loop_future =
0, mass_pointers.size(), threads.submit_loop(0, mass_pointers.size(), solve_octree, 256);
[&](int start, int end) { loop_future.wait();
for (int i = start; i < end; ++i) {
solve_octree(i);
}
},
256);
threads.wait();
#endif #endif
#else #else
@ -333,15 +328,9 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void {
calculate_grid(i); calculate_grid(i);
} }
#else #else
threads.detach_blocks( BS::multi_future<void> loop_future =
0, mass_pointers.size(), threads.submit_loop(0, mass_pointers.size(), solve_grid, 512);
[&](int start, int end) { loop_future.wait();
for (int i = start; i < end; ++i) {
solve_grid(i);
}
},
512);
threads.wait();
#endif #endif
#endif #endif

View File

@ -16,8 +16,8 @@ auto StateManager::ResetState() -> void {
current_state = generators[current_preset](); current_state = generators[current_preset]();
previous_state = current_state; previous_state = current_state;
if (edited) { if (edited) {
// We also need to clear the graph, in case the state has been edited. // We also need to clear the graph in case the state has been edited
// Then the graph would contain states that are impossible to reach. // because the graph could contain states that are impossible to reach now.
ClearGraph(); ClearGraph();
edited = false; edited = false;
} }