Files
57eec4d015 multithreaded collision fixes (#7311)
* rename misleading variable check_again to never_check_again

The first element of collision_result means "never check again" per the
typedef comment, but was destructured as check_again (opposite meaning).
Rename to never_check_again to match the actual semantics and prevent
future confusion.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix data races in ship-weapon collision threading

Two thread-safety bugs in ship_weapon_check_collision, which runs on
worker threads:

1. update_danger_weapon() wrote to Ai_info[] (danger_weapon_objnum,
   danger_weapon_signature) without synchronization. Multiple weapons
   targeting the same ship would race on the same Ai_info entry.

2. Homing missile detonation logic wrote wp->lifeleft and
   wp->weapon_flags directly on the Weapons[] global array.

Fix: defer both writes to the main-thread post-processing phase via
new flags in ship_weapon_collision_data. Also convert the collision
data from a 6-element tuple to a named struct for readability.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix data race in ship-ship collision submodel checking

ship_ship_check_collision() toggled pmi->submodel[].collision_checked
flags on the heavy ship's polymodel_instance to selectively enable/
disable submodel collision checks. When two collision pairs shared the
same heavy ship, worker threads would race on the same pmi entries.

Fix: add a collision_checked_override pointer to mc_info that lets
callers provide a thread-local array of collision_checked values.
model_collide() uses the override when set, falling back to the
pmi->submodel[] field otherwise. ship_ship_check_collision() now
builds a local SCP_vector<char> and passes it via the override,
completely avoiding writes to shared polymodel_instance state.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* Ensure threads have spun down before continuing non-threaded execution

* address review feedback for ship-weapon collision threading

- postproc flag now accumulates incrementally instead of being OR'd
  at the final return, ensuring all return paths respect it
- early return for predictive collisions now preserves the
  should_update_danger_weapon flag in the collision data
- moved nprintf detonation debug print to the main-thread process
  pass for thread safety

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* move collision_checked from pmi into mc_info for thread safety

collision_checked flags were transient per-collision-pass state that
didn't belong in the shared polymodel_instance. Now mc_info owns an
SCP_vector<char> that model_collide auto-initializes from pmi when
empty. All collision callers (ship-ship, asteroid, debris, prop)
updated to manipulate mc.collision_checked instead of pmi->submodel[].

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* Convert to CV and spinup counter

* Update comment

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: BMagnu <[email protected]>
2026-04-07 11:24:32 +09:00

243 lines
7.2 KiB
C++

#include "threading.h"
#include "cmdline/cmdline.h"
#include "object/objcollide.h"
#include "globalincs/pstypes.h"
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <thread>
#ifdef WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <sys/sysctl.h>
#endif
namespace threading {
static size_t num_threads = 1;
static std::condition_variable wait_for_task;
static std::mutex wait_for_task_mutex;
static bool wait_for_task_condition;
static std::condition_variable wait_for_spindown_tasks;
static std::mutex wait_for_spindown_task_mutex;
static size_t wait_for_spindown_tasks_counter;
static std::condition_variable wait_for_spinup_tasks;
static std::mutex wait_for_spinup_task_mutex;
static size_t wait_for_spinup_tasks_counter;
static std::atomic<WorkerThreadTask> worker_task;
static SCP_vector<std::thread> worker_threads;
//Internal Functions
static void mp_worker_thread_main(size_t threadIdx) {
while(true) {
{
std::scoped_lock lock {wait_for_spindown_task_mutex};
++wait_for_spindown_tasks_counter;
wait_for_spindown_tasks.notify_all();
}
//We're waiting for a new task, so spindown was successful
{
std::unique_lock<std::mutex> lk(wait_for_task_mutex);
wait_for_task.wait(lk, []() { return wait_for_task_condition; });
}
//Notify that we passed the wait and can now start processing. This is necessary, as slow thread wakeups in very low workloads could cause the wait_for_task_condition to be false, never waking up the thread, locking on spindown wait
{
std::scoped_lock lock {wait_for_spinup_task_mutex};
++wait_for_spinup_tasks_counter;
wait_for_spinup_tasks.notify_all();
}
switch (worker_task.load(std::memory_order_acquire)) {
case WorkerThreadTask::EXIT:
//We're done and will quit, so ensure we report this.
return;
case WorkerThreadTask::COLLISION:
collide_mp_worker_thread(threadIdx);
break;
default:
UNREACHABLE("Invalid threaded worker task!");
}
}
}
static size_t get_number_of_physical_cores_fallback() {
unsigned int hardware_threads = std::thread::hardware_concurrency();
if (hardware_threads > 0) {
return hardware_threads;
}
else {
Warning(LOCATION, "Could not autodetect available number of threads! Disabling multithreading...");
return 1;
}
}
//We don't want to rely on std::thread::hardware_concurrency() unless we have to, as it reports threads, not physical cores, and FSO doesn't gain much from hyperthreaded threads at the moment.
#ifdef WIN32
static size_t get_number_of_physical_cores() {
auto glpi = (BOOL (WINAPI *)(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION, PDWORD)) GetProcAddress(
GetModuleHandle(TEXT("kernel32")),
"GetLogicalProcessorInformation");
if (glpi == nullptr)
return get_number_of_physical_cores_fallback();
DWORD length = 0;
glpi(nullptr, &length);
SCP_vector<SYSTEM_LOGICAL_PROCESSOR_INFORMATION> infoBuffer(length / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION));
DWORD error = glpi(infoBuffer.data(), &length);
if (error == 0)
return get_number_of_physical_cores_fallback();
size_t num_cores = 0;
for (const auto& info : infoBuffer) {
if (info.Relationship == RelationProcessorCore && info.ProcessorMask != 0)
num_cores++;
}
if (num_cores < 1) {
//invalid results, try fallback
return get_number_of_physical_cores_fallback();
}
else {
return num_cores;
}
}
#elif defined __APPLE__
static size_t get_number_of_physical_cores() {
int rval = 0;
int num = 0;
size_t numSize = sizeof(num);
// apple silicon (performance cores only)
rval = sysctlbyname("hw.perflevel0.physicalcpu", &num, &numSize, nullptr, 0);
// intel
if (rval != 0) {
rval = sysctlbyname("hw.physicalcpu", &num, &numSize, nullptr, 0);
}
if (rval == 0 && num > 0) {
return num;
} else {
// invalid results, try fallback
return get_number_of_physical_cores_fallback();
}
}
#elif defined SCP_UNIX
static size_t get_number_of_physical_cores() {
try {
std::ifstream cpuinfo("/proc/cpuinfo");
SCP_string line;
while (std::getline(cpuinfo, line)) {
//Looking for a cpu cores property is fine assuming a user has only one physical CPU socket. If they have multiple CPU's, this'll underreport the core count, but that should be very rare in typical configurations
if (line.find("cpu cores") != SCP_string::npos){
size_t numberpos = line.find(": ");
if (numberpos == SCP_string::npos)
return get_number_of_physical_cores_fallback();
int num_cores = std::stoi(line.substr(numberpos + 2));
if (num_cores < 1) {
//invalid results, try fallback
return get_number_of_physical_cores_fallback();
}
else {
return num_cores;
}
}
}
return get_number_of_physical_cores_fallback();
}
catch (const std::exception&) {
return get_number_of_physical_cores_fallback();
}
}
#else
#define get_number_of_physical_cores() get_number_of_physical_cores_fallback()
#endif
//External Functions
void spin_up_threaded_task(WorkerThreadTask task) {
{
std::scoped_lock lock {wait_for_spindown_task_mutex};
wait_for_spindown_tasks_counter = 0;
//No notify here cause we only ever lock, never unlock the wait here.
}
worker_task.store(task);
{
std::scoped_lock lock {wait_for_task_mutex};
wait_for_task_condition = true;
wait_for_task.notify_all();
}
}
void spin_down_threaded_task() {
//Make sure all threads are actually running before stopping the pool again
{
std::unique_lock<std::mutex> lk(wait_for_spinup_task_mutex);
wait_for_spinup_tasks.wait(lk, []() { return wait_for_spinup_tasks_counter >= num_threads; });
wait_for_spinup_tasks_counter = 0;
}
{
std::scoped_lock lock {wait_for_task_mutex};
wait_for_task_condition = false;
}
}
void spin_down_wait_complete() {
//Technically, spindowns should only occur when the actual code is confirmed to be complete. So we shouldn't be waiting for long here
{
std::unique_lock<std::mutex> lk(wait_for_spindown_task_mutex);
wait_for_spindown_tasks.wait(lk, []() { return wait_for_spindown_tasks_counter >= num_threads; });
};
}
void init_task_pool() {
if (Cmdline_multithreading == 0) {
//At least given the current collision-detection threading, 8 cores (if available) seems like a sweetspot, with more cores adding too much overhead.
//This could be improved in the future.
//This could also be made task-dependant, if stuff like parallelized loading benefits from more cores.
num_threads = std::min(get_number_of_physical_cores() - 1, static_cast<size_t>(7));
}
else {
num_threads = Cmdline_multithreading - 1;
}
if (!is_threading())
return;
mprintf(("Spinning up threadpool with %d threads...\n", static_cast<int>(num_threads)));
for (size_t i = 0; i < num_threads; i++) {
worker_threads.emplace_back([i](){ mp_worker_thread_main(i); });
}
}
void shut_down_task_pool() {
spin_up_threaded_task(WorkerThreadTask::EXIT);
//Technically we could await spin_down_wait_complete here, but since we're returning and joining the threads here, there is no need
for(auto& thread : worker_threads) {
thread.join();
}
}
bool is_threading() {
return num_threads > 0;
}
size_t get_num_workers() {
return worker_threads.size();
}
}