diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 1af627a98..c23a81644 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -1153,6 +1153,7 @@ namespace Barotrauma EventManager?.EndRound(); StatusEffect.StopAll(); AfflictionPrefab.ClearAllEffects(); + PhysicsBodyQueue.Clear(); IsRunning = false; #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 5f03a6163..797ec14e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -13,6 +13,12 @@ namespace Barotrauma.Items.Components private Holdable holdable; private float deattachTimer; + + /// + /// Flag to prevent multiple queued creation requests. + /// Uses volatile to ensure visibility across threads. + /// + private volatile bool triggerBodyCreationQueued; [Serialize(1.0f, IsPropertySaveable.No, description: "How long it takes to deattach the item from the level walls (in seconds).")] public float DeattachDuration @@ -109,9 +115,22 @@ namespace Barotrauma.Items.Components } else { - if (trigger == null) + if (trigger == null && !triggerBodyCreationQueued) { - CreateTriggerBody(); + // Queue the physics body creation to be processed on the main thread. + // This is necessary because physics body creation is not thread-safe + // and Update() may be called from a parallel loop. + triggerBodyCreationQueued = true; + PhysicsBodyQueue.EnqueueCreation(() => + { + // Double-check that trigger hasn't been created yet + // (in case this was called multiple times before queue processing) + if (trigger == null && !item.Removed) + { + CreateTriggerBody(); + } + triggerBodyCreationQueued = false; + }); } if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index b2ff80ee6..c839d488c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -73,6 +73,11 @@ namespace Barotrauma.Items.Components public PhysicsBody PhysicsBody { get; private set; } + /// + /// Flag to prevent multiple queued refresh requests. + /// + private volatile bool physicsBodyRefreshQueued; + private float radius; [Editable, Serialize(0.0f, IsPropertySaveable.Yes)] public float Radius @@ -83,7 +88,7 @@ namespace Barotrauma.Items.Components { if (radius == value) { return; } radius = value; - if (PhysicsBody != null) { RefreshPhysicsBodySize(); } + if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); } } } @@ -97,7 +102,7 @@ namespace Barotrauma.Items.Components { if (width == value) { return; } width = value; - if (PhysicsBody != null) { RefreshPhysicsBodySize(); } + if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); } } } @@ -111,10 +116,28 @@ namespace Barotrauma.Items.Components { if (height == value) { return; } height = value; - if (PhysicsBody != null) { RefreshPhysicsBodySize(); } + if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); } } } + /// + /// Queue the physics body refresh to be executed on the main thread. + /// This is necessary because physics body operations are not thread-safe. + /// + private void QueuePhysicsBodyRefresh() + { + if (physicsBodyRefreshQueued) { return; } + physicsBodyRefreshQueued = true; + PhysicsBodyQueue.EnqueueCreation(() => + { + if (!item.Removed) + { + RefreshPhysicsBodySize(); + } + physicsBodyRefreshQueued = false; + }); + } + private float currentRadius, currentWidth, currentHeight; private Vector2 bodyOffset; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index a06585116..351083203 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -307,6 +307,12 @@ namespace Barotrauma.MapCreatures.Behavior public readonly List Branches = new List(); private BallastFloraBranch? root; private readonly List bodies = new List(); + + /// + /// Branches that need physics bodies created on the main thread. + /// + private readonly List pendingBodyCreations = new List(); + private readonly object pendingBodyCreationsLock = new object(); private bool isDead; @@ -347,7 +353,8 @@ namespace Barotrauma.MapCreatures.Behavior } } UpdateConnections(branch); - CreateBody(branch); + // OnMapLoaded runs on the main thread, so we can create bodies immediately + CreateBody(branch, immediate: true); } } @@ -998,10 +1005,52 @@ namespace Barotrauma.MapCreatures.Behavior } /// - /// Create a body for a branch which works as the hitbox for flamer + /// Queue a physics body creation for a branch. + /// The actual body will be created on the main thread to ensure thread safety. /// - /// - private void CreateBody(BallastFloraBranch branch) + /// The branch to create a body for + /// If true, create the body immediately (only safe when called from main thread) + private void CreateBody(BallastFloraBranch branch, bool immediate = false) + { + if (immediate) + { + CreateBodyImmediate(branch); + return; + } + + lock (pendingBodyCreationsLock) + { + pendingBodyCreations.Add(branch); + } + PhysicsBodyQueue.EnqueueCreation(() => ProcessPendingBodyCreations()); + } + + /// + /// Process all pending body creations on the main thread. + /// This ensures Farseer Physics operations are thread-safe. + /// + private void ProcessPendingBodyCreations() + { + List branchesToProcess; + lock (pendingBodyCreationsLock) + { + if (pendingBodyCreations.Count == 0) { return; } + branchesToProcess = new List(pendingBodyCreations); + pendingBodyCreations.Clear(); + } + + foreach (var branch in branchesToProcess) + { + if (branch.Removed) { continue; } + CreateBodyImmediate(branch); + } + } + + /// + /// Actually create the physics body for a branch. + /// Must be called on the main thread. + /// + private void CreateBodyImmediate(BallastFloraBranch branch) { Rectangle rect = branch.Rect; Vector2 pos = Parent.Position + Offset + branch.Position; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 8c839934b..572990b3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -686,6 +686,10 @@ namespace Barotrauma } ); + // Process any physics body creation operations queued during Hull/Structure updates. + // BallastFlora growth (from Hull.Update) may queue physics body creations. + PhysicsBodyQueue.ProcessPendingCreations(); + #if CLIENT // Hull Cheats need to be executed after Hull update Hull.UpdateCheats(deltaTime, cam); @@ -727,6 +731,10 @@ namespace Barotrauma throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e); } + // Process any physics body creation operations that were queued during the parallel update. + // This must be done on the main thread because Farseer Physics is not thread-safe. + PhysicsBodyQueue.ProcessPendingCreations(); + UpdateAllProjSpecific(scaledDeltaTime); Spawner?.Update(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs new file mode 100644 index 000000000..64e20e51f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + /// + /// Thread-safe queue for deferring physics body creation operations to the main thread. + /// This is necessary because Farseer Physics' DynamicTree is not thread-safe, + /// and physics bodies cannot be safely created during parallel item updates. + /// + static class PhysicsBodyQueue + { + private static readonly object _lock = new object(); + private static readonly Queue _pendingCreations = new Queue(); + + /// + /// Enqueues a physics body creation action to be executed on the main thread. + /// This method is thread-safe and can be called from parallel update loops. + /// + /// The action that creates the physics body + public static void EnqueueCreation(Action createAction) + { + if (createAction == null) { return; } + lock (_lock) + { + _pendingCreations.Enqueue(createAction); + } + } + + /// + /// Gets the number of pending physics body creation operations. + /// + public static int PendingCount + { + get + { + lock (_lock) + { + return _pendingCreations.Count; + } + } + } + + /// + /// Processes all pending physics body creation operations. + /// Must be called on the main thread, outside of any parallel loops. + /// + public static void ProcessPendingCreations() + { + while (true) + { + Action action; + lock (_lock) + { + if (_pendingCreations.Count == 0) { break; } + action = _pendingCreations.Dequeue(); + } + try + { + action?.Invoke(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Error processing deferred physics body creation: {e.Message}", e); + } + } + } + + /// + /// Clears all pending physics body creation operations. + /// Should be called when ending a round or cleaning up. + /// + public static void Clear() + { + lock (_lock) + { + _pendingCreations.Clear(); + } + } + } +} +