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.
This commit is contained in:
Eero
2025-12-28 15:10:06 +08:00
parent 49355fe32b
commit f485583621
5 changed files with 261 additions and 46 deletions

View File

@@ -486,7 +486,10 @@ namespace Barotrauma.Items.Components
} }
else else
{ {
item.body.ResetDynamics(); // Calculate target position
Vector2 targetPos;
Submarine forceSubmarine = picker.Submarine;
Limb heldHand, arm; Limb heldHand, arm;
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) if (picker.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
{ {
@@ -504,17 +507,42 @@ namespace Barotrauma.Items.Components
Vector2 diff = new Vector2( Vector2 diff = new Vector2(
(heldHand.SimPosition.X - arm.SimPosition.X) / 2f, (heldHand.SimPosition.X - arm.SimPosition.X) / 2f,
(heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f); (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, //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 //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 //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, // -> 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 // or it will end up in a weird state and seemingly disappear
item.SetTransform(heldHand.SimPosition + diff, 0.0f, forceSubmarine: picker.Submarine); item.SetTransform(targetPos, 0.0f, forceSubmarine: forceSubmarine);
}
else
{
item.SetTransform(picker.SimPosition, 0.0f, forceSubmarine: picker.Submarine);
} }
} }
} }

View File

@@ -3669,20 +3669,45 @@ namespace Barotrauma
if (body != null) if (body != null)
{ {
IsActive = true; IsActive = true;
body.Enabled = true;
body.PhysEnabled = true; // Physics body operations must be deferred if we're in a parallel update context,
body.ResetDynamics(); // because Farseer Physics is not thread-safe.
if (dropper != null) 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( if (capturedBody.Removed || Removed) { return; }
"Failed to drop the item \"" + Name + "\" (body has been removed" capturedBody.Enabled = true;
+ (Removed ? ", item has been removed)" : ")")); capturedBody.PhysEnabled = true;
} capturedBody.ResetDynamics();
else if (setTransform) 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) 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.RemoveContained(this);
Container = null; Container = null;

View File

@@ -660,7 +660,15 @@ namespace Barotrauma
{ {
Parallel.ForEach(hullList, parallelOptions, hull => Parallel.ForEach(hullList, parallelOptions, hull =>
{ {
hull.Update(deltaTime, cam); PhysicsBodyQueue.IsInParallelContext = true;
try
{
hull.Update(deltaTime, cam);
}
finally
{
PhysicsBodyQueue.IsInParallelContext = false;
}
}); });
}, },
// Structure parallel update // Structure parallel update
@@ -668,7 +676,15 @@ namespace Barotrauma
{ {
Parallel.ForEach(structureList, parallelOptions, structure => 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) // Gap reset (must be done before update)
@@ -686,9 +702,9 @@ namespace Barotrauma
} }
); );
// Process any physics body creation operations queued during Hull/Structure updates. // Process any physics operations queued during Hull/Structure updates.
// BallastFlora growth (from Hull.Update) may queue physics body creations. // BallastFlora growth (from Hull.Update) may queue physics body creations/transforms.
PhysicsBodyQueue.ProcessPendingCreations(); PhysicsBodyQueue.ProcessPendingOperations();
#if CLIENT #if CLIENT
// Hull Cheats need to be executed after Hull update // 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(); var shuffledGaps = gapList.OrderBy(g => Rand.Int(int.MaxValue)).ToList();
Parallel.ForEach(gapList, parallelOptions, gap => 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 #if CLIENT
sw.Stop(); sw.Stop();
@@ -718,8 +745,16 @@ namespace Barotrauma
{ {
Parallel.ForEach(itemList, parallelOptions, item => Parallel.ForEach(itemList, parallelOptions, item =>
{ {
lastUpdatedItem = item; PhysicsBodyQueue.IsInParallelContext = true;
item.Update(scaledDeltaTime, cam); try
{
lastUpdatedItem = item;
item.Update(scaledDeltaTime, cam);
}
finally
{
PhysicsBodyQueue.IsInParallelContext = false;
}
}); });
} }
catch (InvalidOperationException e) catch (InvalidOperationException e)
@@ -731,9 +766,9 @@ namespace Barotrauma
throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e); 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. // This must be done on the main thread because Farseer Physics is not thread-safe.
PhysicsBodyQueue.ProcessPendingCreations(); PhysicsBodyQueue.ProcessPendingOperations();
UpdateAllProjSpecific(scaledDeltaTime); UpdateAllProjSpecific(scaledDeltaTime);
Spawner?.Update(); Spawner?.Update();

View File

@@ -1,17 +1,65 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
namespace Barotrauma namespace Barotrauma
{ {
/// <summary> /// <summary>
/// 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, /// 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
/// </summary> /// </summary>
/// <start>
/// ├─> 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 static class PhysicsBodyQueue
{ {
private static readonly object _lock = new object(); private static readonly object _lock = new object();
private static readonly Queue<Action> _pendingCreations = new Queue<Action>(); private static readonly Queue<Action> _pendingOperations = new Queue<Action>();
/// <summary>
/// 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.
/// </summary>
[ThreadStatic]
private static bool _isInParallelContext;
/// <summary>
/// Gets or sets whether the current thread is in a parallel update context.
/// When true, physics operations should be queued instead of executed immediately.
/// </summary>
public static bool IsInParallelContext
{
get => _isInParallelContext;
set => _isInParallelContext = value;
}
/// <summary>
/// Enqueues a physics operation to be executed on the main thread.
/// This method is thread-safe and can be called from parallel update loops.
/// </summary>
/// <param name="operation">The physics operation to defer</param>
public static void Enqueue(Action operation)
{
if (operation == null) { return; }
lock (_lock)
{
_pendingOperations.Enqueue(operation);
}
}
/// <summary> /// <summary>
/// Enqueues a physics body creation action to be executed on the main thread. /// Enqueues a physics body creation action to be executed on the main thread.
@@ -20,15 +68,31 @@ namespace Barotrauma
/// <param name="createAction">The action that creates the physics body</param> /// <param name="createAction">The action that creates the physics body</param>
public static void EnqueueCreation(Action createAction) public static void EnqueueCreation(Action createAction)
{ {
if (createAction == null) { return; } Enqueue(createAction);
lock (_lock) }
/// <summary>
/// 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.
/// </summary>
/// <param name="operation">The physics operation to execute</param>
public static void ExecuteOrDefer(Action operation)
{
if (operation == null) { return; }
if (_isInParallelContext)
{ {
_pendingCreations.Enqueue(createAction); Enqueue(operation);
}
else
{
operation();
} }
} }
/// <summary> /// <summary>
/// Gets the number of pending physics body creation operations. /// Gets the number of pending physics operations.
/// </summary> /// </summary>
public static int PendingCount public static int PendingCount
{ {
@@ -36,24 +100,24 @@ namespace Barotrauma
{ {
lock (_lock) lock (_lock)
{ {
return _pendingCreations.Count; return _pendingOperations.Count;
} }
} }
} }
/// <summary> /// <summary>
/// Processes all pending physics body creation operations. /// Processes all pending physics operations.
/// Must be called on the main thread, outside of any parallel loops. /// Must be called on the main thread, outside of any parallel loops.
/// </summary> /// </summary>
public static void ProcessPendingCreations() public static void ProcessPendingOperations()
{ {
while (true) while (true)
{ {
Action action; Action action;
lock (_lock) lock (_lock)
{ {
if (_pendingCreations.Count == 0) { break; } if (_pendingOperations.Count == 0) { break; }
action = _pendingCreations.Dequeue(); action = _pendingOperations.Dequeue();
} }
try try
{ {
@@ -61,20 +125,29 @@ namespace Barotrauma
} }
catch (Exception e) catch (Exception e)
{ {
DebugConsole.ThrowError($"Error processing deferred physics body creation: {e.Message}", e); DebugConsole.ThrowError($"Error processing deferred physics operation: {e.Message}", e);
} }
} }
} }
/// <summary> /// <summary>
/// Clears all pending physics body creation operations. /// Legacy method for backwards compatibility.
/// Calls ProcessPendingOperations().
/// </summary>
public static void ProcessPendingCreations()
{
ProcessPendingOperations();
}
/// <summary>
/// Clears all pending physics operations.
/// Should be called when ending a round or cleaning up. /// Should be called when ending a round or cleaning up.
/// </summary> /// </summary>
public static void Clear() public static void Clear()
{ {
lock (_lock) lock (_lock)
{ {
_pendingCreations.Clear(); _pendingOperations.Clear();
} }
} }
} }

View File

@@ -178,9 +178,23 @@ namespace Barotrauma
Parallel.Invoke(parallelOptions, Parallel.Invoke(parallelOptions,
() => GameMain.ParticleManager.Update((float)deltaTime), () => 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(); sw.Stop();
GameMain.PerformanceCounter.AddElapsedTicks("Update:Particles+Level", sw.ElapsedTicks); GameMain.PerformanceCounter.AddElapsedTicks("Update:Particles+Level", sw.ElapsedTicks);
@@ -246,9 +260,34 @@ namespace Barotrauma
#elif SERVER #elif SERVER
Parallel.Invoke(parallelOptions, 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 #endif
var submarines = Submarine.Loaded.ToList(); var submarines = Submarine.Loaded.ToList();