Refactor MapEntity.UpdateAll for improved parallelism

Rewrites MapEntity.UpdateAll to use more granular parallelism, reducing contention and improving performance by parallelizing hull, structure, and gap resets, while keeping order-dependent and non-thread-safe updates sequential. Updates GameScreen to pass ParallelOptions to UpdateAll.
This commit is contained in:
eero
2025-12-22 17:48:14 +08:00
parent bab02fca8a
commit b146fa326d
2 changed files with 86 additions and 65 deletions
@@ -638,53 +638,80 @@ namespace Barotrauma
} }
/// <summary> /// <summary>
/// Call Update() on every object in Entity.list /// Call Update() on every object in Entity.list - 完全并行版本
/// </summary> /// </summary>
public static void UpdateAll(float deltaTime, Camera cam) public static void UpdateAll(float deltaTime, Camera cam , ParallelOptions parallelOptions)
{ {
mapEntityUpdateTick++; mapEntityUpdateTick++;
#if CLIENT #if CLIENT
var sw = new System.Diagnostics.Stopwatch(); var sw = new System.Diagnostics.Stopwatch();
sw.Start(); sw.Start();
#endif #endif
Task StructuralTask = Task.Factory.StartNew(() =>
{ bool shouldUpdateMapEntities = mapEntityUpdateTick % MapEntityUpdateInterval == 0;
if (mapEntityUpdateTick % MapEntityUpdateInterval == 0) bool shouldUpdatePower = mapEntityUpdateTick % PoweredUpdateInterval == 0;
// Buffer lists to avoid repeated allocations
var hullList = shouldUpdateMapEntities ? Hull.HullList.ToList() : null;
var structureList = shouldUpdateMapEntities ? Structure.WallList.ToList() : null;
var gapList = Gap.GapList.ToList();
var itemList = shouldUpdateMapEntities ? Item.ItemList.ToList() : null;
// First phase: parallel updates that have no order dependencies
Parallel.Invoke(parallelOptions,
// Hull parallel update
() =>
{ {
foreach (Hull hull in Hull.HullList) if (shouldUpdateMapEntities && hullList != null)
{ {
hull.Update(deltaTime * MapEntityUpdateInterval, cam); Parallel.ForEach(hullList, parallelOptions, hull =>
{
hull.Update(deltaTime * MapEntityUpdateInterval, cam);
});
} }
},
// Structure parallel update
() =>
{
if (shouldUpdateMapEntities && structureList != null)
{
Parallel.ForEach(structureList, parallelOptions, structure =>
{
structure.Update(deltaTime * MapEntityUpdateInterval, cam);
});
}
},
// Gap reset (must be done before update)
() =>
{
Parallel.ForEach(gapList, parallelOptions, gap =>
{
gap.ResetWaterFlowThisFrame();
});
},
// Powered components update
() =>
{
if (shouldUpdatePower)
{
Powered.UpdatePower(deltaTime * PoweredUpdateInterval);
}
}
);
#if CLIENT #if CLIENT
// Hull Cheats need to be executed after Hull update
if (shouldUpdateMapEntities)
{
Hull.UpdateCheats(deltaTime * MapEntityUpdateInterval, cam); Hull.UpdateCheats(deltaTime * MapEntityUpdateInterval, cam);
}
#endif #endif
foreach (Structure structure in Structure.WallList) // Gap update (has order dependencies, keep random order but execute sequentially)
{ var shuffledGaps = gapList.OrderBy(g => Rand.Int(int.MaxValue)).ToList();
structure.Update(deltaTime * MapEntityUpdateInterval, cam); foreach (Gap gap in shuffledGaps)
}
}
});
Task GapTask = Task.Factory.StartNew(() =>
{ {
foreach (Gap gap in Gap.GapList) gap.Update(deltaTime, cam);
{
gap.ResetWaterFlowThisFrame();
}
//update gaps in random order, because otherwise in rooms with multiple gaps
//the water/air will always tend to flow through the first gap in the list,
//which may lead to weird behavior like water draining down only through
//one gap in a room even if there are several
foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue)))
{
gap.Update(deltaTime, cam);
}
});
if (mapEntityUpdateTick % PoweredUpdateInterval == 0)
{
Powered.UpdatePower(deltaTime * PoweredUpdateInterval);
} }
#if CLIENT #if CLIENT
@@ -692,45 +719,39 @@ namespace Barotrauma
GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Misc", sw.ElapsedTicks); GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Misc", sw.ElapsedTicks);
sw.Restart(); sw.Restart();
#endif #endif
//TODO: Divide ItemList into different parts to update
Task ItemTask = Task.Factory.StartNew(() =>
{
Item.UpdatePendingConditionUpdates(deltaTime);
if (mapEntityUpdateTick % MapEntityUpdateInterval == 0)
{
Item lastUpdatedItem = null;
try // Item update (Item.Update() is not thread-safe and must be executed on the main thread)
{ if (shouldUpdateMapEntities && itemList != null)
foreach (Item item in Item.ItemList)
{
lastUpdatedItem = item;
item.Update(deltaTime * MapEntityUpdateInterval, cam);
}
}
catch (InvalidOperationException e)
{
GameAnalyticsManager.AddErrorEventOnce(
"MapEntity.UpdateAll:ItemUpdateInvalidOperation",
GameAnalyticsManager.ErrorSeverity.Critical,
$"Error while updating item {lastUpdatedItem?.Name ?? "null"}: {e.Message}");
throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e);
}
}
});
Task.WaitAll(ItemTask, GapTask, StructuralTask);
if (mapEntityUpdateTick % MapEntityUpdateInterval == 0)
{ {
UpdateAllProjSpecific(deltaTime * MapEntityUpdateInterval); Item.UpdatePendingConditionUpdates(deltaTime);
float scaledDeltaTime = deltaTime * MapEntityUpdateInterval;
Item lastUpdatedItem = null;
try
{
foreach (Item item in itemList)
{
lastUpdatedItem = item;
item.Update(scaledDeltaTime, cam);
}
}
catch (InvalidOperationException e)
{
GameAnalyticsManager.AddErrorEventOnce(
"MapEntity.UpdateAll:ItemUpdateInvalidOperation",
GameAnalyticsManager.ErrorSeverity.Critical,
$"Error while updating item {lastUpdatedItem?.Name ?? "null"}: {e.Message}");
throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e);
}
UpdateAllProjSpecific(scaledDeltaTime);
Spawner?.Update(); Spawner?.Update();
} }
#if CLIENT #if CLIENT
sw.Stop(); sw.Stop();
GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Items", sw.ElapsedTicks); GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Items", sw.ElapsedTicks);
sw.Restart();
#endif #endif
} }
@@ -273,11 +273,11 @@ namespace Barotrauma
}); });
#if CLIENT #if CLIENT
MapEntity.UpdateAll((float)deltaTime, cam); MapEntity.UpdateAll((float)deltaTime, cam, parallelOptions);
#elif SERVER #elif SERVER
// TODO: Move both UpdateAll() method to this file // TODO: Move both UpdateAll() method to this file
MapEntity.UpdateAll((float)deltaTime, Camera.Instance); MapEntity.UpdateAll((float)deltaTime, Camera.Instance, parallelOptions);
StatusEffect.UpdateAll((float)deltaTime); StatusEffect.UpdateAll((float)deltaTime);
#endif #endif