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>
#define PRINT_TIMINGS
// #define WEB // Disables multithreading
#define BARNES_HUT // Use octree BH instead of uniform grid
// #define WEB // Disables multithreading
// Window
constexpr int INITIAL_WIDTH = 800;
@ -31,7 +31,7 @@ constexpr float ROT_SPEED = 1.0;
// Physics Engine
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 SPRING_CONSTANT = 5.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),
children(-1, -1, -1, -1, -1, -1, -1, -1), mass_id(-1), leaf(true) {}
~OctreeNode() {}
public:
auto ChildCount() const -> int;
};
class Octree {
@ -35,8 +36,6 @@ public:
Octree(Octree &&move) = delete;
Octree &operator=(Octree &&move) = delete;
~Octree() {}
public:
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)
-> 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

View File

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

View File

@ -23,6 +23,9 @@
// - Click states to display them in the board
// - Find shortest path to any winning state and mark it in the graph
// - Also mark the next move along the path on the board
// TODO: Smooth camera (on target change)
// TODO: Mark the starting state
// TODO: Mark the visited states
auto main(int argc, char *argv[]) -> int {
// 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> render_time_accumulator =
std::chrono::duration<double, std::milli>(0);
int loop_count = 0;
long loop_count = 0;
#endif
float timestep_accumulator = 0.0;
int update_accumulator = 0;
double timestep_accumulator = 0.0;
long update_accumulator = 0;
while (!WindowShouldClose()) {
timestep_accumulator += GetFrameTime();
@ -123,8 +126,9 @@ auto main(int argc, char *argv[]) -> int {
<< render_time_accumulator / loop_count << "." << std::endl;
std::cout << " - Physics updates avg: "
<< static_cast<float>(update_accumulator) / loop_count
<< "x per frame (" << timestep_accumulator << "s remaining)."
<< std::endl;
<< "x per frame ("
<< static_cast<int>(timestep_accumulator / TIMESTEP)
<< "x behind)." << std::endl;
last_print_time = GetTime();
physics_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 "config.hpp"
#include "util.hpp"
#include <iostream>
#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 {
OctreeNode node;
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.z + node.box_max.z) / 2.0);
Vector3 min;
Vector3 max;
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
@ -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)
-> 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
node.mass_id = mass_id;
node.mass_center = pos;
node.mass_total = mass;
nodes[node_idx].mass_id = mass_id;
nodes[node_idx].mass_center = pos;
nodes[node_idx].mass_total = mass;
return;
}
if (node.leaf) {
if (nodes[node_idx].leaf) {
// The leaf is occupied, we need to subdivide
int existing_id = node.mass_id;
Vector3 existing_pos = node.mass_center;
float existing_mass = node.mass_total;
node.mass_id = -1;
node.leaf = false;
int existing_id = nodes[node_idx].mass_id;
Vector3 existing_pos = nodes[node_idx].mass_center;
float existing_mass = nodes[node_idx].mass_total;
nodes[node_idx].mass_id = -1;
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);
if (node.children[oct] == -1) {
if (nodes[node_idx].children[oct] == -1) {
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
@ -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);
// Update the center of mass
node = nodes[node_idx];
float new_mass = node.mass_total + mass;
node.mass_center.x =
(node.mass_center.x * node.mass_total + pos.x) / new_mass;
node.mass_center.y =
(node.mass_center.y * node.mass_total + pos.y) / new_mass;
node.mass_center.z =
(node.mass_center.z * node.mass_total + pos.z) / new_mass;
node.mass_total = new_mass;
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;
nodes[node_idx].mass_center.y =
(nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) /
new_mass;
nodes[node_idx].mass_center.z =
(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) {
return Vector3Zero();
}
OctreeNode &node = nodes[node_idx];
const OctreeNode &node = nodes[node_idx];
if (node.mass_total == 0.0f) {
return Vector3Zero();
}
@ -146,3 +163,11 @@ auto Octree::CalculateForce(int node_idx, const Vector3 &pos) -> Vector3 {
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 "config.hpp"
#include "util.hpp"
#include <algorithm>
#include <cfloat>
@ -249,15 +250,9 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void {
solve_octree(i);
}
#else
threads.detach_blocks(
0, mass_pointers.size(),
[&](int start, int end) {
for (int i = start; i < end; ++i) {
solve_octree(i);
}
},
256);
threads.wait();
BS::multi_future<void> loop_future =
threads.submit_loop(0, mass_pointers.size(), solve_octree, 256);
loop_future.wait();
#endif
#else
@ -333,15 +328,9 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void {
calculate_grid(i);
}
#else
threads.detach_blocks(
0, mass_pointers.size(),
[&](int start, int end) {
for (int i = start; i < end; ++i) {
solve_grid(i);
}
},
512);
threads.wait();
BS::multi_future<void> loop_future =
threads.submit_loop(0, mass_pointers.size(), solve_grid, 512);
loop_future.wait();
#endif
#endif

View File

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