Fix cross-object skeleton mutex deadlock in CCF_Skeleton::BuildState - #576
Merged
themrdemonized merged 2 commits intoJun 21, 2026
Conversation
gwalls
marked this pull request as ready for review
June 18, 2026 13:52
6 tasks
Owner
|
That try lock guard looks useful, please move that to where xrCriticalSectionGuard is placed, call it xrCriticalSectionTryGuard |
Author
Okay, I moved it over to Thanks! |
themrdemonized
approved these changes
Jun 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On the multithreaded bone-calc path, the game can hit a silent freeze (full hang, no crash, nothing in the log) in scenes with several co-located animated objects - e.g. a stacked NPC squad, a crowded chokepoint, or NPCs throwing bolts. It's intermittent and timing-dependent.
Root cause: cross-object AB-BA deadlock
UCalc_Mutexis one critical section per skeleton instance. Two threads end up each holding their object's lock and blocking on the other's:CKinematics::CalculateBonestakes the object's ownUCalc_Mutex(SkeletonRigid.cpp:77) and holds it across the whole body — including the IKUpdate_Callbackat the end.CCF_Skeleton::BuildState, which took a blocking guard on the neighbour'sUCalc_Mutex.So with the main render thread computing bones for object X and the
CalculateBonesThreadworker computing object Y at the same time:X.UCalc_Mutex(CalculateBones)Y.UCalc_Mutex(BuildState)Y.UCalc_Mutex(CalculateBones)X.UCalc_Mutex(BuildState)Confirmed with a debugger capture during a live freeze — both threads parked identically entering the second skeleton's lock:
Fix
Make
BuildState's neighbour-skeleton lock non-blocking (TryEnter): if the object is mid-recalc on another thread, skip the lock and read its one-frame-stale bone transforms instead of blocking. This removes the only cross-object blocking acquisition, so the cycle can't form — while keeping parallel bone calculation fully intact.Worst case is a foot-IK ray-pick seeing one neighbour's bones a frame stale (no crash — the bone array is fixed-size; a torn matrix read self-corrects next frame). This is effectively the long-standing pre-lock behaviour, only in the rare contended case.
Alternatives considered
LL_GetTransform_safed— doesn't help; it's still a blocking acquire of the neighbour's lock, so the deadlock persists. The fix has to be non-blocking.Testing
Reproduced reliably on the current
-mtbuild by rushing 2–3 unprotected squads through a shapeless anomaly field (especially acidic anomalies); also reproduced via the bolt path. With this change the freeze no longer reproduces, and foot IK looks correct (no visible popping).I'm testing with my own fork of @Priler 's Anomaly NDA.