Skip to content

Commit 57eec4d

Browse files
Goober5000claudeBMagnu
authored
multithreaded collision fixes (scp-fs2open#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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * Convert to CV and spinup counter * Update comment --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: BMagnu <6238428+BMagnu@users.noreply.github.com>
1 parent affd625 commit 57eec4d

10 files changed

Lines changed: 142 additions & 59 deletions

File tree

code/asteroid/asteroid.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,20 +1315,18 @@ int asteroid_check_collision(object *pasteroid, object *other_obj, vec3d *hitpos
13151315
model_get_moving_submodel_list(submodel_vector, heavy_obj);
13161316

13171317
// turn off all moving submodels, collide against only 1 at a time.
1318-
// turn off collision detection for all moving submodels
1318+
mc.collision_checked.assign(pm->n_models, 0);
13191319
for (auto submodel: submodel_vector) {
1320-
pmi->submodel[submodel].collision_checked = true;
1320+
mc.collision_checked[submodel] = true;
13211321
}
13221322

13231323
// Only check single submodel now, since children of moving submodels are handled as moving as well
13241324
mc.flags = orig_flags | MC_SUBMODEL;
13251325

13261326
// check each submodel in turn
13271327
for (auto submodel: submodel_vector) {
1328-
auto smi = &pmi->submodel[submodel];
1329-
13301328
// turn on just one submodel for collision test
1331-
smi->collision_checked = false;
1329+
mc.collision_checked[submodel] = false;
13321330

13331331
// find the start and end positions of the sphere in submodel RF
13341332
model_instance_global_to_local_point(&p0, &light_obj->last_pos, pm, pmi, submodel, &heavy_obj->last_orient, &heavy_obj->last_pos, true);
@@ -1360,9 +1358,12 @@ int asteroid_check_collision(object *pasteroid, object *other_obj, vec3d *hitpos
13601358
}
13611359
}
13621360
// Don't look at this submodel again
1363-
smi->collision_checked = true;
1361+
mc.collision_checked[submodel] = true;
13641362
}
13651363

1364+
// Clear collision_checked before base model pass so it auto-inits fresh
1365+
mc.collision_checked.clear();
1366+
13661367
}
13671368

13681369
// Now complete base model collision checks that do not take into account rotating submodels.

code/debris/debris.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,9 +1030,9 @@ int debris_check_collision(object *pdebris, object *other_obj, vec3d *hitpos, co
10301030
model_get_moving_submodel_list(submodel_vector, heavy_obj);
10311031

10321032
// turn off all moving submodels, collide against only 1 at a time.
1033-
// turn off collision detection for all moving submodels
1033+
mc.collision_checked.assign(pm->n_models, 0);
10341034
for (auto submodel : submodel_vector) {
1035-
pmi->submodel[submodel].collision_checked = true;
1035+
mc.collision_checked[submodel] = true;
10361036
}
10371037

10381038
// Only check single submodel now, since children of moving submodels are handled as moving as well
@@ -1044,10 +1044,8 @@ int debris_check_collision(object *pdebris, object *other_obj, vec3d *hitpos, co
10441044

10451045
// check each submodel in turn
10461046
for (auto submodel: submodel_vector) {
1047-
auto smi = &pmi->submodel[submodel];
1048-
10491047
// turn on just one submodel for collision test
1050-
smi->collision_checked = false;
1048+
mc.collision_checked[submodel] = false;
10511049

10521050
// find the start and end positions of the sphere in submodel RF
10531051
model_instance_global_to_local_point(&p0, &light_obj->last_pos, pm, pmi, submodel, &heavy_obj->last_orient, &heavy_obj->last_pos, true);
@@ -1081,8 +1079,11 @@ int debris_check_collision(object *pdebris, object *other_obj, vec3d *hitpos, co
10811079
}
10821080

10831081
// Don't look at this submodel again
1084-
smi->collision_checked = true;
1082+
mc.collision_checked[submodel] = true;
10851083
}
1084+
1085+
// Clear collision_checked before base model pass so it auto-inits fresh
1086+
mc.collision_checked.clear();
10861087
}
10871088

10881089
// Now complete base model collision checks that do not take into account rotating submodels.

code/model/model.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ struct submodel_instance
137137
TIMESTAMP stepped_translation_started;
138138

139139
bool blown_off = false; // If set, this subobject is blown off
140-
bool collision_checked = false;
141140

142141
// These fields are the true standard reference for submodel rotation. They should seldom be read directly
143142
// and should almost never be written directly. In most cases, coders should prefer cur_angle and prev_angle.
@@ -1282,6 +1281,12 @@ typedef struct mc_info {
12821281
float radius = 0; // If MC_CHECK_THICK is set, checks a sphere moving with the radius.
12831282
int lod = 0; // Which detail level of the submodel to check instead
12841283

1284+
// Per-submodel collision_checked flags (indexed by submodel number).
1285+
// When non-empty, model_collide uses this to determine which submodels to skip.
1286+
// Auto-initialized from pmi in model_collide when empty; callers may pre-populate
1287+
// to control which submodels are checked (e.g., rotating submodel collision).
1288+
SCP_vector<char> collision_checked;
1289+
12851290
// Return values
12861291
int num_hits = 0; // How many collisions were found
12871292
float hit_dist = 0.0f; // The distance from p0 to hitpoint

code/model/modelcollide.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1098,7 +1098,7 @@ void mc_check_subobj( int mn )
10981098
vm_vec_add2(&instance_offset, &csmi->canonical_offset);
10991099

11001100
blown_off = csmi->blown_off;
1101-
collision_checked = csmi->collision_checked;
1101+
collision_checked = !Mc->collision_checked.empty() && Mc->collision_checked[i];
11021102
}
11031103

11041104
// Don't check it or its children if it is destroyed
@@ -1150,6 +1150,10 @@ int model_collide(mc_info *mc_info_obj)
11501150
Mc_pmi = NULL;
11511151
}
11521152

1153+
if (Mc_pmi && Mc->collision_checked.empty()) {
1154+
Mc->collision_checked.resize(Mc_pm->n_models, 0);
1155+
}
1156+
11531157
// DA 11/19/98 - disable this check for rotating submodels
11541158
// Don't do check if for very small movement
11551159
// if (Mc_mag < 0.01f) {

code/object/collideshipship.cpp

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,9 @@ int ship_ship_check_collision(collision_info_struct *ship_ship_hit_info)
260260
model_get_moving_submodel_list(submodel_vector, heavy_obj);
261261

262262
// turn off all moving submodels, collide against only 1 at a time.
263-
// turn off collision detection for all moving submodels
263+
mc.collision_checked.assign(pm->n_models, 0);
264264
for (auto submodel : submodel_vector) {
265-
pmi->submodel[submodel].collision_checked = true;
265+
mc.collision_checked[submodel] = true;
266266
}
267267

268268
// Only check single submodel now, since children of moving submodels are handled as moving as well
@@ -274,14 +274,12 @@ int ship_ship_check_collision(collision_info_struct *ship_ship_hit_info)
274274

275275
// check each submodel in turn
276276
for (auto submodel : submodel_vector) {
277-
auto smi = &pmi->submodel[submodel];
278-
279277
// turn on just one submodel for collision test
280-
smi->collision_checked = false;
278+
mc.collision_checked[submodel] = false;
281279

282-
if (smi->blown_off)
280+
if (pmi->submodel[submodel].blown_off)
283281
{
284-
smi->collision_checked = true;
282+
mc.collision_checked[submodel] = true;
285283
continue;
286284
}
287285

@@ -315,8 +313,13 @@ int ship_ship_check_collision(collision_info_struct *ship_ship_hit_info)
315313
model_instance_local_to_global_point(&ship_ship_hit_info->light_collision_cm_pos, &int_light_pos, pm, pmi, mc.hit_submodel, &heavy_obj->orient, &zero);
316314
}
317315
}
316+
317+
// re-disable this submodel before enabling the next one
318+
mc.collision_checked[submodel] = true;
318319
}
319320

321+
// Clear collision_checked before subsequent model_collide calls so it auto-inits fresh
322+
mc.collision_checked.clear();
320323
}
321324

322325
// Now complete base model collision checks that do not take into account rotating submodels.

code/object/collideshipweapon.cpp

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@
2929
#include "ship/shiphit.h"
3030
#include "weapon/weapon.h"
3131

32-
//mc, notify_ai_shield_down, shield_collision, quadrant_num, shield_tri_hit, shield_hitpoint
33-
using ship_weapon_collision_data = std::tuple<std::optional<mc_info>, int, bool, int, int, vec3d>;
32+
struct ship_weapon_collision_data {
33+
std::optional<mc_info> mc;
34+
int notify_ai_shield_down;
35+
bool shield_collision;
36+
int quadrant_num;
37+
int shield_tri_hit;
38+
vec3d shield_hitpos;
39+
bool should_update_danger_weapon; // deferred to main thread for thread safety
40+
bool should_detonate; // deferred to main thread for thread safety
41+
};
3442

3543
extern int Game_skill_level;
3644
extern float ai_endangered_time(const object *ship_objp, const object *weapon_objp);
@@ -206,14 +214,14 @@ static std::tuple<bool, bool, ship_weapon_collision_data> ship_weapon_check_coll
206214
Assert( shipp->objnum == OBJ_INDEX(ship_objp));
207215

208216
// Make ships that are warping in not get collision detection done
209-
if ( shipp->is_arriving() ) return {false, true, {std::nullopt, -1, false, -1, -1, ZERO_VECTOR}};
217+
if ( shipp->is_arriving() ) return {false, true, {std::nullopt, -1, false, -1, -1, ZERO_VECTOR, false, false}};
210218

211219
// Return information for AI to detect incoming fire.
212220
// Could perhaps be done elsewhere at lower cost --MK, 11/7/97
213221
float dist = vm_vec_dist_quick(&ship_objp->pos, &weapon_objp->pos);
214-
if (dist < weapon_objp->phys_info.speed) {
215-
update_danger_weapon(ship_objp, weapon_objp);
216-
}
222+
bool should_update_danger_weapon = (dist < weapon_objp->phys_info.speed);
223+
bool should_detonate = false;
224+
bool postproc = should_update_danger_weapon;
217225

218226
int valid_hit_occurred = 0; // If this is set, then hitpos is set
219227
int quadrant_num = -1;
@@ -503,12 +511,13 @@ static std::tuple<bool, bool, ship_weapon_collision_data> ship_weapon_check_coll
503511
*next_hit = (int) (1000.0f * (mc->hit_dist*(flFrametime + time_limit) - flFrametime) );
504512
if (*next_hit > 0)
505513
// if hit occurs outside of this frame, do not do damage
506-
return { false, false, {std::nullopt, -1, false, -1, -1, ZERO_VECTOR} }; //No hit, but continue checking
514+
return { postproc, false, {std::nullopt, -1, false, -1, -1, ZERO_VECTOR, should_update_danger_weapon, false} }; //No hit, but continue checking
507515
}
508516

509-
bool postproc = valid_hit_occurred || notify_ai_shield_down >= 0;
517+
postproc = postproc || valid_hit_occurred || notify_ai_shield_down >= 0;
510518
ship_weapon_collision_data collision_data {
511-
valid_hit_occurred ? std::optional(*mc) : std::nullopt, notify_ai_shield_down, postproc, quadrant_num, shield_tri_hit, shield_hitpos
519+
valid_hit_occurred ? std::optional(*mc) : std::nullopt, notify_ai_shield_down, postproc, quadrant_num, shield_tri_hit, shield_hitpos,
520+
should_update_danger_weapon, false
512521
};
513522

514523
// when the $Fixed Missile Detonation: flag is active, skip this whole block, as it's redundant to a similar check in weapon_home()
@@ -521,17 +530,16 @@ static std::tuple<bool, bool, ship_weapon_collision_data> ship_weapon_check_coll
521530
if (vm_vec_dot(&vec_to_ship, &weapon_objp->orient.vec.fvec) < 0.0f) {
522531
// check if we're colliding against "invisible" ship
523532
if (!(shipp->flags[Ship::Ship_Flags::Dont_collide_invis])) {
524-
wp->lifeleft = 0.001f;
525-
wp->weapon_flags.set(Weapon::Weapon_Flags::Begun_detonation);
526-
527-
if (ship_objp == Player_obj)
528-
nprintf(("Jim", "Frame %i: Weapon %d set to detonate, dist = %7.3f.\n", Framecount, OBJ_INDEX(weapon_objp), dist));
533+
// Detonation writes are deferred to the main thread for thread safety
534+
should_detonate = true;
535+
postproc = true;
529536
valid_hit_occurred = 1; //No hit, continue checking
530537
}
531538
}
532539
}
533540
}
534541

542+
collision_data.should_detonate = should_detonate;
535543
return { postproc, !static_cast<bool>(valid_hit_occurred), collision_data} ;
536544
}
537545

@@ -543,15 +551,26 @@ static void ship_weapon_process_collision(obj_pair* pair, const ship_weapon_coll
543551
const weapon_info* wip = &Weapon_info[wp->weapon_info_index];
544552
const ship_info* sip = &Ship_info[shipp->ship_info_index];
545553

546-
const auto& [mc_opt, notify_ai_shield_down, shield_collision, quadrant_num, shield_tri_hit, shield_hitpos] = collision_data;
547-
bool valid_hit_occurred = mc_opt.has_value();
548-
auto mc = valid_hit_occurred ? &(*mc_opt) : nullptr;
554+
bool valid_hit_occurred = collision_data.mc.has_value();
555+
auto mc = valid_hit_occurred ? &(*collision_data.mc) : nullptr;
556+
557+
// Perform deferred writes that are unsafe to do from worker threads
558+
if (collision_data.should_update_danger_weapon)
559+
update_danger_weapon(ship_objp, weapon_objp);
560+
561+
if (collision_data.should_detonate) {
562+
wp->lifeleft = 0.001f;
563+
wp->weapon_flags.set(Weapon::Weapon_Flags::Begun_detonation);
564+
565+
if (ship_objp == Player_obj)
566+
nprintf(("Jim", "Frame %i: Weapon %d set to detonate, dist = %7.3f.\n", Framecount, OBJ_INDEX(weapon_objp), vm_vec_dist_quick(&ship_objp->pos, &weapon_objp->pos)));
567+
}
549568

550-
if (notify_ai_shield_down >= 0)
551-
Ai_info[Ships[ship_objp->instance].ai_index].danger_shield_quadrant = notify_ai_shield_down;
569+
if (collision_data.notify_ai_shield_down >= 0)
570+
Ai_info[Ships[ship_objp->instance].ai_index].danger_shield_quadrant = collision_data.notify_ai_shield_down;
552571

553-
if (shield_tri_hit >= 0)
554-
add_shield_point(OBJ_INDEX(ship_objp), shield_tri_hit, &shield_hitpos, wip->shield_impact_effect_radius);
572+
if (collision_data.shield_tri_hit >= 0)
573+
add_shield_point(OBJ_INDEX(ship_objp), collision_data.shield_tri_hit, &collision_data.shield_hitpos, wip->shield_impact_effect_radius);
555574

556575
if ( valid_hit_occurred )
557576
{
@@ -583,12 +602,12 @@ static void ship_weapon_process_collision(obj_pair* pair, const ship_weapon_coll
583602
}
584603

585604
if(!ship_override && !weapon_override) {
586-
if (shield_collision && quadrant_num >= 0) {
605+
if (collision_data.shield_collision && collision_data.quadrant_num >= 0) {
587606
if ((sip->shield_impact_explosion_anim.isValid()) && (wip->shield_impact_explosion_radius > 0)) {
588607
shield_impact_explosion(mc->hit_point, mc->hit_normal, ship_objp, weapon_objp, wip->shield_impact_explosion_radius, sip->shield_impact_explosion_anim);
589608
}
590609
}
591-
ship_weapon_do_hit_stuff(ship_objp, weapon_objp, &mc->hit_point_world, &mc->hit_point, quadrant_num, mc->hit_submodel, &mc->hit_normal);
610+
ship_weapon_do_hit_stuff(ship_objp, weapon_objp, &mc->hit_point_world, &mc->hit_point, collision_data.quadrant_num, mc->hit_submodel, &mc->hit_normal);
592611
}
593612

594613
if (scripting::hooks::OnWeaponCollision->isActive() && !(weapon_override && !ship_override)) {
@@ -682,7 +701,7 @@ static std::tuple<bool, bool, ship_weapon_collision_data> prop_weapon_check_coll
682701
bool postproc = true;
683702
bool recheck = false;
684703
// most of this data is irrevelant for props but let's match it to make it easy
685-
ship_weapon_collision_data cd{mc, -1, postproc, -1, -1, ZERO_VECTOR};
704+
ship_weapon_collision_data cd{mc, -1, postproc, -1, -1, ZERO_VECTOR, false, false};
686705
return {postproc, recheck, cd};
687706
}
688707
}
@@ -692,7 +711,7 @@ static std::tuple<bool, bool, ship_weapon_collision_data> prop_weapon_check_coll
692711
bool postproc = (valid_hit_occurred != 0);
693712
bool recheck = (valid_hit_occurred == 0);
694713
// most of this data is irrevelant for props but let's match it to make it easy
695-
ship_weapon_collision_data cd{(valid_hit_occurred ? mc : mc_info{}), -1, postproc, -1, -1, ZERO_VECTOR};
714+
ship_weapon_collision_data cd{(valid_hit_occurred ? mc : mc_info{}), -1, postproc, -1, -1, ZERO_VECTOR, false, false};
696715

697716
return{postproc, recheck, cd};
698717
}
@@ -702,11 +721,10 @@ static void prop_weapon_process_collision(obj_pair* pair, const ship_weapon_coll
702721
object* prop_objp = pair->a;
703722
object* weapon_objp = pair->b;
704723

705-
auto mc_opt = std::get<0>(cd);
706-
if (!mc_opt) {
724+
if (!cd.mc) {
707725
return;
708726
}
709-
const mc_info& mc = *mc_opt;
727+
const mc_info& mc = *cd.mc;
710728

711729
weapon* wp = &Weapons[weapon_objp->instance];
712730
wp->collisionInfo = new mc_info(mc);

code/object/objcollide.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,7 @@ void spin_up_mp_collision() {
762762
void spin_down_mp_collision() {
763763
threading::spin_down_threaded_task();
764764
collision_processing_done.store(true);
765+
threading::spin_down_wait_complete();
765766
}
766767

767768
void queue_mp_collision(uint ctype, const obj_pair& colliding) {
@@ -1210,11 +1211,11 @@ void collide_mp_worker_thread(size_t threadIdx) {
12101211
UNREACHABLE("Got non MP-compatible collision type!");
12111212
}
12121213

1213-
auto&& [check_again, collision_data_maybe, collision_fnc] = check_collision(&collision_check.objs);
1214+
auto&& [never_check_again, collision_data_maybe, collision_fnc] = check_collision(&collision_check.objs);
12141215

12151216
{
12161217
std::scoped_lock lock{thread.result_mutex};
1217-
thread.queue_results->emplace_back(collision_thread_data::collision_queue_result{collision_check.objs, check_again, collision_data_maybe, collision_fnc});
1218+
thread.queue_results->emplace_back(collision_thread_data::collision_queue_result{collision_check.objs, never_check_again, collision_data_maybe, collision_fnc});
12181219
}
12191220
thread.result_length.fetch_add(1, std::memory_order_release);
12201221
thread.queue_length.fetch_sub(1, std::memory_order_release);

code/prop/prop.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -934,9 +934,9 @@ int prop_check_collision(object* prop_obj, object* other_obj, vec3d* hitpos, col
934934
model_get_moving_submodel_list(submodel_vector, heavy_obj);
935935

936936
// turn off all moving submodels, collide against only 1 at a time.
937-
// turn off collision detection for all moving submodels
937+
mc.collision_checked.assign(pm->n_models, 0);
938938
for (auto submodel : submodel_vector) {
939-
pmi->submodel[submodel].collision_checked = true;
939+
mc.collision_checked[submodel] = true;
940940
}
941941

942942
// Only check single submodel now, since children of moving submodels are handled as moving as well
@@ -948,10 +948,8 @@ int prop_check_collision(object* prop_obj, object* other_obj, vec3d* hitpos, col
948948

949949
// check each submodel in turn
950950
for (auto submodel : submodel_vector) {
951-
auto smi = &pmi->submodel[submodel];
952-
953951
// turn on just one submodel for collision test
954-
smi->collision_checked = false;
952+
mc.collision_checked[submodel] = false;
955953

956954
// find the start and end positions of the sphere in submodel RF
957955
model_instance_global_to_local_point(&p0, &light_obj->last_pos, pm, pmi, submodel, &heavy_obj->last_orient, &heavy_obj->last_pos, true);
@@ -984,8 +982,11 @@ int prop_check_collision(object* prop_obj, object* other_obj, vec3d* hitpos, col
984982
}
985983

986984
// Don't look at this submodel again
987-
smi->collision_checked = true;
985+
mc.collision_checked[submodel] = true;
988986
}
987+
988+
// Clear collision_checked before base model pass so it auto-inits fresh
989+
mc.collision_checked.clear();
989990
}
990991

991992
// Now complete base model collision checks that do not take into account rotating submodels.

0 commit comments

Comments
 (0)