From 7d9642a5b1e48affe0c48abc268e21af718be215 Mon Sep 17 00:00:00 2001 From: eero Date: Mon, 22 Dec 2025 00:16:25 +0800 Subject: [PATCH] Enable and improve threaded physics execution Activated the RUN_PHYSICS_IN_SEPARATE_THREAD directive and refactored the physics threading logic. Added cancellation support, improved error handling, and used synchronization primitives to coordinate physics updates. These changes aim to improve performance and stability when running physics in a separate thread. --- .../SharedSource/Screens/GameScreen.cs | 97 ++++++++++++++++--- 1 file changed, 82 insertions(+), 15 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 169bb03e4..9775a0f5a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -1,4 +1,4 @@ -//#define RUN_PHYSICS_IN_SEPARATE_THREAD +#define RUN_PHYSICS_IN_SEPARATE_THREAD using Microsoft.Xna.Framework; using System.Threading; @@ -6,10 +6,10 @@ using FarseerPhysics.Dynamics; using FarseerPhysics; using System.Threading.Tasks; using System.Linq; +using System; #if DEBUG && CLIENT -using System; using Barotrauma.Sounds; using Microsoft.Xna.Framework.Input; #endif @@ -18,9 +18,15 @@ namespace Barotrauma { partial class GameScreen : Screen { - private object updateLock = new object(); + private readonly object updateLock = new object(); private double physicsTime; +#if RUN_PHYSICS_IN_SEPARATE_THREAD + private CancellationTokenSource physicsCancellation; + private readonly AutoResetEvent physicsEvent = new AutoResetEvent(false); + private const int PHYSICS_WAIT_TIMEOUT_MS = 2; +#endif + #if CLIENT private readonly Camera cam; @@ -71,12 +77,13 @@ namespace Barotrauma MapEntity.ClearHighlightedEntities(); - Task PhysicsTask = Task.Factory.StartNew(() => ExecutePhysics()); #if RUN_PHYSICS_IN_SEPARATE_THREAD + physicsCancellation = new CancellationTokenSource(); var physicsThread = new Thread(ExecutePhysics) { Name = "Physics thread", - IsBackground = true + IsBackground = true, + Priority = ThreadPriority.AboveNormal }; physicsThread.Start(); #endif @@ -85,6 +92,12 @@ namespace Barotrauma public override void Deselect() { base.Deselect(); + +#if RUN_PHYSICS_IN_SEPARATE_THREAD + physicsCancellation?.Cancel(); + physicsEvent?.Set(); +#endif + #if CLIENT var config = GameSettings.CurrentConfig; config.CrewMenuOpen = CrewManager.PreferCrewMenuOpen; @@ -101,6 +114,7 @@ namespace Barotrauma #endif #endif } + /// /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. @@ -111,9 +125,11 @@ namespace Barotrauma #warning For now CL side performence counter is partly useless bucz multiple changes on such things. Need time to take care of it #if RUN_PHYSICS_IN_SEPARATE_THREAD - physicsTime += deltaTime; lock (updateLock) - { + { + physicsTime += deltaTime; + } + physicsEvent?.Set(); #endif @@ -334,27 +350,78 @@ namespace Barotrauma GameMain.PerformanceCounter.AddElapsedTicks("Update:Physics", sw.ElapsedTicks); #endif UpdateProjSpecific(deltaTime); - -#if RUN_PHYSICS_IN_SEPARATE_THREAD - } -#endif } partial void UpdateProjSpecific(double deltaTime); private void ExecutePhysics() { - while (true) +#if RUN_PHYSICS_IN_SEPARATE_THREAD + var token = physicsCancellation.Token; + + try { - while (physicsTime >= Timing.Step) + while (!token.IsCancellationRequested) { + bool hasWork = false; + lock (updateLock) { - GameMain.World.Step((float)Timing.Step); - physicsTime -= Timing.Step; + while (physicsTime >= Timing.Step) + { + try + { + GameMain.World.Step((float)Timing.Step); + physicsTime -= Timing.Step; + hasWork = true; + } + catch (WorldLockedException e) + { + string errorMsg = $"Physics thread WorldLockedException: {e.Message}\n{e.StackTrace}"; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce( + "GameScreen.ExecutePhysics:WorldLockedException" + e.Message, + GameAnalyticsManager.ErrorSeverity.Error, + errorMsg); + break; + } + catch (Exception e) + { + string errorMsg = $"Physics step error: {e.Message}\n{e.StackTrace}"; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce( + "GameScreen.ExecutePhysics:PhysicsStepError" + e.GetType().Name, + GameAnalyticsManager.ErrorSeverity.Error, + errorMsg); + break; + } + } + } + + if (!hasWork) + { + physicsEvent.WaitOne(PHYSICS_WAIT_TIMEOUT_MS); } } } + catch (ThreadAbortException) + { + DebugConsole.Log("Physics thread aborted."); + } + catch (Exception e) + { + string errorMsg = $"Fatal error in physics thread: {e.Message}\n{e.StackTrace}"; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce( + "GameScreen.ExecutePhysics:FatalError", + GameAnalyticsManager.ErrorSeverity.Critical, + errorMsg); + } + finally + { + DebugConsole.Log("Physics thread terminated."); + } +#endif } } }