diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 429554361..405b0c9b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -486,7 +486,10 @@ namespace Barotrauma.Items.Components } else { - item.body.ResetDynamics(); + // Calculate target position + Vector2 targetPos; + Submarine forceSubmarine = picker.Submarine; + Limb heldHand, arm; if (picker.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) { @@ -504,17 +507,42 @@ namespace Barotrauma.Items.Components Vector2 diff = new Vector2( (heldHand.SimPosition.X - arm.SimPosition.X) / 2f, (heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f); + targetPos = heldHand.SimPosition + diff; + } + else + { + targetPos = picker.SimPosition; + } + // Defer physics operations if in parallel context + if (PhysicsBodyQueue.IsInParallelContext) + { + var capturedBody = item.body; + var capturedItem = item; + var capturedTargetPos = targetPos; + var capturedForceSubmarine = forceSubmarine; + + PhysicsBodyQueue.Enqueue(() => + { + if (capturedBody.Removed || capturedItem.Removed) { return; } + capturedBody.ResetDynamics(); + //we have forced the item to be in the same sub as the dropper above, + //and are placing it to the position of the hands in "local" coordinates + //which may be outside the sub if the character is e.g. standing half-way through the airlock + // -> let's use the forceSubmarine argument ensure the item is still considered to be in the sub's coordinate space, + // or it will end up in a weird state and seemingly disappear + capturedItem.SetTransform(capturedTargetPos, 0.0f, forceSubmarine: capturedForceSubmarine); + }); + } + else + { + item.body.ResetDynamics(); //we have forced the item to be in the same sub as the dropper above, //and are placing it to the position of the hands in "local" coordinates //which may be outside the sub if the character is e.g. standing half-way through the airlock // -> let's use the forceSubmarine argument ensure the item is still considered to be in the sub's coordinate space, // or it will end up in a weird state and seemingly disappear - item.SetTransform(heldHand.SimPosition + diff, 0.0f, forceSubmarine: picker.Submarine); - } - else - { - item.SetTransform(picker.SimPosition, 0.0f, forceSubmarine: picker.Submarine); + item.SetTransform(targetPos, 0.0f, forceSubmarine: forceSubmarine); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index c2576bb48..2ce5e3d2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -3669,20 +3669,45 @@ namespace Barotrauma if (body != null) { IsActive = true; - body.Enabled = true; - body.PhysEnabled = true; - body.ResetDynamics(); - if (dropper != null) + + // Physics body operations must be deferred if we're in a parallel update context, + // because Farseer Physics is not thread-safe. + if (PhysicsBodyQueue.IsInParallelContext) { - if (body.Removed) + // Capture the values we need for the deferred operation + var capturedBody = body; + var capturedDropperSimPos = dropper?.SimPosition ?? Microsoft.Xna.Framework.Vector2.Zero; + var capturedSetTransform = setTransform && dropper != null; + + PhysicsBodyQueue.Enqueue(() => { - DebugConsole.ThrowError( - "Failed to drop the item \"" + Name + "\" (body has been removed" - + (Removed ? ", item has been removed)" : ")")); - } - else if (setTransform) + if (capturedBody.Removed || Removed) { return; } + capturedBody.Enabled = true; + capturedBody.PhysEnabled = true; + capturedBody.ResetDynamics(); + if (capturedSetTransform) + { + capturedBody.SetTransformIgnoreContacts(capturedDropperSimPos, 0.0f); + } + }); + } + else + { + body.Enabled = true; + body.PhysEnabled = true; + body.ResetDynamics(); + if (dropper != null) { - body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f); + if (body.Removed) + { + DebugConsole.ThrowError( + "Failed to drop the item \"" + Name + "\" (body has been removed" + + (Removed ? ", item has been removed)" : ")")); + } + else if (setTransform) + { + body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f); + } } } } @@ -3693,7 +3718,22 @@ namespace Barotrauma { if (setTransform) { - SetTransform(Container.SimPosition, 0.0f); + // Defer SetTransform if in parallel context + if (PhysicsBodyQueue.IsInParallelContext) + { + var capturedContainerSimPos = Container.SimPosition; + PhysicsBodyQueue.Enqueue(() => + { + if (!Removed) + { + SetTransform(capturedContainerSimPos, 0.0f); + } + }); + } + else + { + SetTransform(Container.SimPosition, 0.0f); + } } Container.RemoveContained(this); Container = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 572990b3b..39c77c116 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -660,7 +660,15 @@ namespace Barotrauma { Parallel.ForEach(hullList, parallelOptions, hull => { - hull.Update(deltaTime, cam); + PhysicsBodyQueue.IsInParallelContext = true; + try + { + hull.Update(deltaTime, cam); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } }); }, // Structure parallel update @@ -668,7 +676,15 @@ namespace Barotrauma { Parallel.ForEach(structureList, parallelOptions, structure => { - structure.Update(deltaTime, cam); + PhysicsBodyQueue.IsInParallelContext = true; + try + { + structure.Update(deltaTime, cam); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } }); }, // Gap reset (must be done before update) @@ -686,9 +702,9 @@ 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(); + // Process any physics operations queued during Hull/Structure updates. + // BallastFlora growth (from Hull.Update) may queue physics body creations/transforms. + PhysicsBodyQueue.ProcessPendingOperations(); #if CLIENT // Hull Cheats need to be executed after Hull update @@ -699,8 +715,19 @@ namespace Barotrauma var shuffledGaps = gapList.OrderBy(g => Rand.Int(int.MaxValue)).ToList(); Parallel.ForEach(gapList, parallelOptions, gap => { - gap.Update(deltaTime, cam); + PhysicsBodyQueue.IsInParallelContext = true; + try + { + gap.Update(deltaTime, cam); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } }); + + // Process any physics operations queued during Gap updates. + PhysicsBodyQueue.ProcessPendingOperations(); #if CLIENT sw.Stop(); @@ -718,8 +745,16 @@ namespace Barotrauma { Parallel.ForEach(itemList, parallelOptions, item => { - lastUpdatedItem = item; - item.Update(scaledDeltaTime, cam); + PhysicsBodyQueue.IsInParallelContext = true; + try + { + lastUpdatedItem = item; + item.Update(scaledDeltaTime, cam); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } }); } catch (InvalidOperationException e) @@ -731,9 +766,9 @@ 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. + // Process any physics 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(); + PhysicsBodyQueue.ProcessPendingOperations(); UpdateAllProjSpecific(scaledDeltaTime); Spawner?.Update(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs index 64e20e51f..a8aec47ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs @@ -1,17 +1,65 @@ using System; using System.Collections.Generic; +using System.Threading; namespace Barotrauma { /// - /// Thread-safe queue for deferring physics body creation operations to the main thread. + /// Thread-safe queue for deferring physics 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. + /// and physics operations cannot be safely performed during parallel updates. + /// + /// Supported operations include: + /// - Physics body creation + /// - Physics body transform updates (SetTransform, SetTransformIgnoreContacts) + /// - Any other operation that modifies the Farseer physics world /// +/// +/// ├─> PhysicsBodyQueue.IsInParallelContext = true (ThreadStatic) +/// ├─> Item.Update() +/// │ └─> StatusEffect.Apply() +/// │ └─> Character.Kill() +/// │ └─> Item.Drop() +/// │ └─> Check if IsInParallelContext == true +/// │ └─> PhysicsBodyQueue.Enqueue(Physics operation) +/// ├──> PhysicsBodyQueue.IsInParallelContext = false +/// └──> PhysicsBodyQueue.ProcessPendingOperations() ← Main thread executes +/// └─> body.SetTransformIgnoreContacts() static class PhysicsBodyQueue { private static readonly object _lock = new object(); - private static readonly Queue _pendingCreations = new Queue(); + private static readonly Queue _pendingOperations = new Queue(); + + /// + /// Thread-local flag indicating whether the current thread is in a parallel physics update context. + /// When true, physics operations should be deferred using this queue instead of executing immediately. + /// + [ThreadStatic] + private static bool _isInParallelContext; + + /// + /// Gets or sets whether the current thread is in a parallel update context. + /// When true, physics operations should be queued instead of executed immediately. + /// + public static bool IsInParallelContext + { + get => _isInParallelContext; + set => _isInParallelContext = value; + } + + /// + /// Enqueues a physics operation to be executed on the main thread. + /// This method is thread-safe and can be called from parallel update loops. + /// + /// The physics operation to defer + public static void Enqueue(Action operation) + { + if (operation == null) { return; } + lock (_lock) + { + _pendingOperations.Enqueue(operation); + } + } /// /// Enqueues a physics body creation action to be executed on the main thread. @@ -20,15 +68,31 @@ namespace Barotrauma /// The action that creates the physics body public static void EnqueueCreation(Action createAction) { - if (createAction == null) { return; } - lock (_lock) + Enqueue(createAction); + } + + /// + /// Executes a physics operation, either immediately or deferred depending on context. + /// If called from a parallel context, the operation will be queued for later execution. + /// If called from the main thread (outside parallel loops), the operation executes immediately. + /// + /// The physics operation to execute + public static void ExecuteOrDefer(Action operation) + { + if (operation == null) { return; } + + if (_isInParallelContext) { - _pendingCreations.Enqueue(createAction); + Enqueue(operation); + } + else + { + operation(); } } /// - /// Gets the number of pending physics body creation operations. + /// Gets the number of pending physics operations. /// public static int PendingCount { @@ -36,24 +100,24 @@ namespace Barotrauma { lock (_lock) { - return _pendingCreations.Count; + return _pendingOperations.Count; } } } /// - /// Processes all pending physics body creation operations. + /// Processes all pending physics operations. /// Must be called on the main thread, outside of any parallel loops. /// - public static void ProcessPendingCreations() + public static void ProcessPendingOperations() { while (true) { Action action; lock (_lock) { - if (_pendingCreations.Count == 0) { break; } - action = _pendingCreations.Dequeue(); + if (_pendingOperations.Count == 0) { break; } + action = _pendingOperations.Dequeue(); } try { @@ -61,20 +125,29 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error processing deferred physics body creation: {e.Message}", e); + DebugConsole.ThrowError($"Error processing deferred physics operation: {e.Message}", e); } } } /// - /// Clears all pending physics body creation operations. + /// Legacy method for backwards compatibility. + /// Calls ProcessPendingOperations(). + /// + public static void ProcessPendingCreations() + { + ProcessPendingOperations(); + } + + /// + /// Clears all pending physics operations. /// Should be called when ending a round or cleaning up. /// public static void Clear() { lock (_lock) { - _pendingCreations.Clear(); + _pendingOperations.Clear(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 11b3ce9ad..577324da2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -178,9 +178,23 @@ namespace Barotrauma Parallel.Invoke(parallelOptions, () => GameMain.ParticleManager.Update((float)deltaTime), - () => { if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); } + () => + { + PhysicsBodyQueue.IsInParallelContext = true; + try + { + if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } + } ); + // Process any physics operations queued during Level update + PhysicsBodyQueue.ProcessPendingOperations(); + sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:Particles+Level", sw.ElapsedTicks); @@ -246,9 +260,34 @@ namespace Barotrauma #elif SERVER Parallel.Invoke(parallelOptions, - () => { if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, Camera.Instance); }, - () => Character.UpdateAll((float)deltaTime, Camera.Instance) + () => + { + PhysicsBodyQueue.IsInParallelContext = true; + try + { + if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, Camera.Instance); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } + }, + () => + { + PhysicsBodyQueue.IsInParallelContext = true; + try + { + Character.UpdateAll((float)deltaTime, Camera.Instance); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } + } ); + + // Process any physics operations queued during parallel updates + PhysicsBodyQueue.ProcessPendingOperations(); #endif var submarines = Submarine.Loaded.ToList();