#include "threaded_physics.hpp" #include "config.hpp" #include "mass_spring_system.hpp" #include "util.hpp" #include #include #include #include #include #ifdef ASYNC_OCTREE auto threaded_physics::set_octree_pool_thread_name(size_t idx) -> void { 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 auto threaded_physics::physics_thread(physics_state& state, const std::optional* const> thread_pool) -> void { mass_spring_system mass_springs; #ifdef ASYNC_OCTREE BS::this_thread::set_os_thread_name("physics"); BS::thread_pool<> octree_thread(1, set_octree_pool_thread_name); std::future octree_future; octree tree_buffer; size_t last_mass_count = 0; infoln("Using asynchronous octree builder."); #endif const auto visitor = overloads{ [&](const struct add_mass&) { mass_springs.add_mass(); }, [&](const struct add_spring& as) { mass_springs.add_spring(as.a, as.b); }, [&](const struct clear_graph&) { mass_springs.clear(); }, }; std::chrono::time_point last = std::chrono::high_resolution_clock::now(); std::chrono::duration physics_accumulator(0); std::chrono::duration ups_accumulator(0); int loop_iterations = 0; while (state.running.load()) { #ifdef TRACY FrameMarkStart("PhysicsThread"); #endif // Time tracking std::chrono::time_point now = std::chrono::high_resolution_clock::now(); const std::chrono::duration deltatime = now - last; physics_accumulator += deltatime; ups_accumulator += deltatime; last = now; // Handle queued commands { #ifdef TRACY std::lock_guard lock(state.command_mtx); #else std::lock_guard lock(state.command_mtx); #endif while (!state.pending_commands.empty()) { command& cmd = state.pending_commands.front(); cmd.visit(visitor); state.pending_commands.pop(); } } if (mass_springs.positions.empty()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } // Physics update 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 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, positions = std::move(positions)]() { octree::build_octree(tree_buffer, positions); }); // 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(mass_springs.tree, mass_springs.positions); last_mass_count = mass_springs.positions.size(); } #else octree::build_octree(mass_springs.tree, mass_springs.positions); #endif mass_springs.clear_forces(); mass_springs.calculate_spring_forces(thread_pool); mass_springs.calculate_repulsion_forces(thread_pool); mass_springs.update(TIMESTEP * SIM_SPEED, thread_pool); // 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 // cheaply. // mass_springs.center_masses(thread_pool); ++loop_iterations; physics_accumulator -= std::chrono::duration(TIMESTEP); } #ifdef ASYNC_OCTREE // Wait for the octree to be built if (octree_future.valid()) { octree_future.wait(); octree_future = std::future{}; std::swap(mass_springs.tree, tree_buffer); } #endif // Publish the positions for the renderer (copy) #ifdef TRACY FrameMarkStart("PhysicsThreadProduceLock"); #endif { #ifdef TRACY std::unique_lock lock(state.data_mtx); #else std::unique_lock lock(state.data_mtx); #endif state.data_consumed_cnd.wait(lock, [&] { return state.data_consumed || !state.running.load(); }); if (!state.running.load()) { // Running turned false while we were waiting for the condition break; } if (ups_accumulator.count() > 1.0) { // Update each second state.ups = loop_iterations; loop_iterations = 0; ups_accumulator = std::chrono::duration(0); } if (mass_springs.tree.nodes.empty()) { state.mass_center = Vector3Zero(); } else { state.mass_center = mass_springs.tree.nodes[0].mass_center; } state.masses.clear(); state.masses.reserve(mass_springs.positions.size()); for (const Vector3& pos : mass_springs.positions) { state.masses.emplace_back(pos); } state.mass_count = mass_springs.positions.size(); state.spring_count = mass_springs.springs.size(); state.data_ready = true; state.data_consumed = false; } // Notify the rendering thread that new data is available state.data_ready_cnd.notify_all(); #ifdef TRACY FrameMarkEnd("PhysicsThreadProduceLock"); FrameMarkEnd("PhysicsThread"); #endif } } auto threaded_physics::clear_cmd() -> void { { #ifdef TRACY std::lock_guard lock(state.command_mtx); #else std::lock_guard lock(state.command_mtx); #endif state.pending_commands.emplace(clear_graph{}); } } auto threaded_physics::add_mass_cmd() -> void { { #ifdef TRACY std::lock_guard lock(state.command_mtx); #else std::lock_guard lock(state.command_mtx); #endif state.pending_commands.emplace(add_mass{}); } } auto threaded_physics::add_spring_cmd(const size_t a, const size_t b) -> void { { #ifdef TRACY std::lock_guard lock(state.command_mtx); #else std::lock_guard lock(state.command_mtx); #endif state.pending_commands.emplace(add_spring{a, b}); } } auto threaded_physics::add_mass_springs_cmd(const size_t num_masses, const std::vector>& springs) -> void { { #ifdef TRACY std::lock_guard lock(state.command_mtx); #else std::lock_guard lock(state.command_mtx); #endif for (size_t i = 0; i < num_masses; ++i) { state.pending_commands.emplace(add_mass{}); } for (const auto& [from, to] : springs) { state.pending_commands.emplace(add_spring{from, to}); } } }