mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* 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]>
243 lines
7.2 KiB
C++
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();
|
|
}
|
|
}
|