Skip to content

multithreaded collision fixes#7311

Open
Goober5000 wants to merge 7 commits intoscp-fs2open:masterfrom
Goober5000:claude/multithreaded_collision_fixes
Open

multithreaded collision fixes#7311
Goober5000 wants to merge 7 commits intoscp-fs2open:masterfrom
Goober5000:claude/multithreaded_collision_fixes

Conversation

@Goober5000
Copy link
Copy Markdown
Contributor

@Goober5000 Goober5000 commented Mar 22, 2026

Three commits, three four fixes to the multithreaded collision code. These correspond to items 1, 2, 3, and 5 from Claude's audit report.

  1. 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.

  1. 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.

  1. 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 and passes it via the override,
completely avoiding writes to shared polymodel_instance state.

Also added @BMagnu 's fix for bug 4:

  1. Ensure threads have spun down before continuing non-threaded execution

Goober5000 and others added 3 commits March 21, 2026 21:50
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>
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>
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>
@Goober5000 Goober5000 requested a review from BMagnu March 22, 2026 03:37
@Goober5000 Goober5000 added the fix A fix for bugs, not-a-bugs, and/or regressions. label Mar 22, 2026
BMagnu and others added 2 commits March 24, 2026 21:39
Copy link
Copy Markdown
Member

@BMagnu BMagnu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small changes are needed


return { postproc, !static_cast<bool>(valid_hit_occurred), collision_data} ;
collision_data.should_detonate = should_detonate;
return { postproc || should_detonate || should_update_danger_weapon, !static_cast<bool>(valid_hit_occurred), collision_data} ;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of being seperate bools, both should_detonate and should_update_danger_weapon should just update postproc when they are determined. That makes it more intuitive, as postproc is literally meant for any need of postprocessing this collision, as well as properly updating it for the return on line 509 which also needs the should_update_danger_weapon flag at least

Comment on lines +1285 to +1289
// Optional per-submodel collision_checked override array (indexed by submodel number).
// When non-null, model_collide uses this instead of pmi->submodel[].collision_checked,
// allowing callers to control which submodels are skipped without writing to shared state.
// This is essential for thread-safe ship-ship collision detection.
const char *collision_checked_override = nullptr;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should not be optional.
This same safety issue will come back for all other forms when we parallelize them. In fact, even with the current changes, the issue would persist for ship ship collisions.
And, tbh, data used for a single collision should really not persist in the pmi data at all.

There's a case to be made for this to just store the SCP_vector directly here, being automatically initialized in model_collide if a pmi is present, and locking the irrelevant submodels with MC_SUBMODEL set.

Comment on lines 535 to 536
if (ship_objp == Player_obj)
nprintf(("Jim", "Frame %i: Weapon %d set to detonate, dist = %7.3f.\n", Framecount, OBJ_INDEX(weapon_objp), dist));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For thread-safety of prints this should also move to the resolve pass

if (*next_hit > 0)
// if hit occurs outside of this frame, do not do damage
return { false, false, {std::nullopt, -1, false, -1, -1, ZERO_VECTOR} }; //No hit, but continue checking
return { false, false, {std::nullopt, -1, false, -1, -1, ZERO_VECTOR, false, false} }; //No hit, but continue checking
Copy link
Copy Markdown
Member

@BMagnu BMagnu Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also needs to respect should_update_danger_weapon, both in the argument of the pass-out data, as well as in the postproc.

@BMagnu BMagnu added this to the Release 25.0.1 milestone Mar 29, 2026
@wookieejedi
Copy link
Copy Markdown
Member

Looks like this is the last 25.0.1 tagged PR awaiting revision. I know you are super busy, @Goober5000, just wanted to check if you might have an estimate for when this might be ready or maybe it's hard to tell?

@Goober5000
Copy link
Copy Markdown
Contributor Author

Looks like this is the last 25.0.1 tagged PR awaiting revision. I know you are super busy, @Goober5000, just wanted to check if you might have an estimate for when this might be ready or maybe it's hard to tell?

I'll work on this now

Goober5000 and others added 2 commits April 4, 2026 16:19
- 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>
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>
@Goober5000 Goober5000 marked this pull request as ready for review April 4, 2026 21:00
@Goober5000
Copy link
Copy Markdown
Contributor Author

Ok, the PR has been updated pursuant to the feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix A fix for bugs, not-a-bugs, and/or regressions.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants