From 49355fe32b2104e353b245e8ceb1d7cef76608dd Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 14:42:17 +0800 Subject: [PATCH] Unstable Add thread-safe queue for deferred physics body creation Introduces PhysicsBodyQueue to safely defer physics body creation to the main thread, addressing thread-safety issues with Farseer Physics during parallel updates. Updates LevelResource, TriggerComponent, BallastFloraBehavior, and MapEntity to use the queue for all physics body creation and refresh operations, ensuring they are processed outside of parallel loops. Also adds cleanup of the queue at round end. --- .../SharedSource/GameSession/GameSession.cs | 1 + .../Components/Holdable/LevelResource.cs | 23 +++++- .../Items/Components/TriggerComponent.cs | 29 ++++++- .../Map/Creatures/BallastFloraBehavior.cs | 57 ++++++++++++- .../SharedSource/Map/MapEntity.cs | 8 ++ .../SharedSource/Physics/PhysicsBodyQueue.cs | 82 +++++++++++++++++++ 6 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs 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(); + } + } + } +} +