From f4855836210e330e99a02978e5114e9c81bedc2c Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 15:10:06 +0800 Subject: [PATCH] Unstable 0.2 Defer physics operations during parallel updates Introduces a thread-safe queue for deferring physics operations (such as body creation and transforms) to the main thread, ensuring Farseer Physics is not accessed from parallel contexts. Updates Holdable, Item, MapEntity, and GameScreen to use the new PhysicsBodyQueue for safe physics operations during parallel updates, and refactors PhysicsBodyQueue to support general deferred physics actions. --- .../Items/Components/Holdable/Holdable.cs | 40 ++++++- .../SharedSource/Items/Item.cs | 64 +++++++++-- .../SharedSource/Map/MapEntity.cs | 55 ++++++++-- .../SharedSource/Physics/PhysicsBodyQueue.cs | 103 +++++++++++++++--- .../SharedSource/Screens/GameScreen.cs | 45 +++++++- 5 files changed, 261 insertions(+), 46 deletions(-) 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();