From 031dceeb48687bbeb41f4ba6f99c05b04798b916 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 17 Nov 2022 19:01:29 +1100 Subject: [PATCH] Parallel physics broadphase (#3483) --- .../Physics/Systems/SharedBroadphaseSystem.cs | 132 +++++++++++------- .../Server/RobustServerSimulation.cs | 2 + Robust.UnitTesting/TestingParallelManager.cs | 18 +++ 3 files changed, 98 insertions(+), 54 deletions(-) create mode 100644 Robust.UnitTesting/TestingParallelManager.cs diff --git a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs index d8b52238c..0b87067c3 100644 --- a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs +++ b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs @@ -1,6 +1,9 @@ using System; +using System.Buffers; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.ObjectPool; +using Robust.Shared.Collections; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -10,6 +13,7 @@ using Robust.Shared.Maths; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Dynamics.Contacts; +using Robust.Shared.Threading; using Robust.Shared.Utility; using SharpZstd.Interop; @@ -18,14 +22,13 @@ namespace Robust.Shared.Physics.Systems public abstract class SharedBroadphaseSystem : EntitySystem { [Dependency] private readonly IMapManagerInternal _mapManager = default!; + [Dependency] private readonly IParallelManager _parallel = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!; private ISawmill _logger = default!; - private const int MinimumBroadphaseCapacity = 256; - /* * Okay so Box2D has its own "MoveProxy" stuff so you can easily find new contacts when required. * Our problem is that we have nested broadphases (rather than being on separate maps) which makes this @@ -40,10 +43,10 @@ namespace Robust.Shared.Physics.Systems /// private float _broadphaseExpand; - private readonly ObjectPool> _proxyPool = - new DefaultObjectPool>(new SetPolicy(), 4096); + private const int PairBufferParallel = 8; - private readonly Dictionary> _pairBuffer = new(64); + private ObjectPool> _bufferPool = + new DefaultObjectPool>(new ListPolicy(), 2048); public override void Initialize() { @@ -168,34 +171,69 @@ namespace Robust.Shared.Physics.Systems // Handle grids first as they're not stored on map broadphase at all. HandleGridCollisions(mapId, contactManager, movedGrids, physicsQuery, xformQuery); - DebugTools.Assert(moveBuffer.Count > 0 || _pairBuffer.Count == 0); + // EZ + if (moveBuffer.Count == 0) + return; - foreach (var (proxy, worldAABB) in moveBuffer) + var count = moveBuffer.Count; + var contactBuffer = ArrayPool>.Shared.Rent(count); + var pMoveBuffer = ArrayPool<(FixtureProxy Proxy, Box2 AABB)>.Shared.Rent(count); + + var idx = 0; + + foreach (var (proxy, aabb) in moveBuffer) { - var proxyBody = proxy.Fixture.Body; - DebugTools.Assert(!proxyBody.Deleted); - - var state = (this, proxy, worldAABB, _pairBuffer, xformQuery, broadphaseQuery); - - // Get every broadphase we may be intersecting. - _mapManager.FindGridsIntersectingApprox(mapId, worldAABB.Enlarged(_broadphaseExpand), ref state, - static (IMapGrid grid, ref ( - SharedBroadphaseSystem system, - FixtureProxy proxy, - Box2 worldAABB, - Dictionary> pairBuffer, - EntityQuery xformQuery, - EntityQuery broadphaseQuery) tuple) => - { - tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, grid.GridEntityId, tuple.pairBuffer, tuple.xformQuery, tuple.broadphaseQuery); - return true; - }); - - FindPairs(proxy, worldAABB, _mapManager.GetMapEntityId(mapId), _pairBuffer, xformQuery, broadphaseQuery); + contactBuffer[idx] = _bufferPool.Get(); + pMoveBuffer[idx++] = (proxy, aabb); } - foreach (var (proxyA, proxies) in _pairBuffer) + var options = new ParallelOptions() { + MaxDegreeOfParallelism = _parallel.ParallelProcessCount, + }; + + var batches = (int)MathF.Ceiling((float) count / PairBufferParallel); + + Parallel.For(0, batches, options, i => + { + var start = i * PairBufferParallel; + var end = Math.Min(start + PairBufferParallel, count); + + for (var j = start; j < end; j++) + { + var (proxy, worldAABB) = pMoveBuffer[j]; + var buffer = contactBuffer[j]; + + var proxyBody = proxy.Fixture.Body; + DebugTools.Assert(!proxyBody.Deleted); + + var state = (this, proxy, worldAABB, buffer, xformQuery, broadphaseQuery); + + // Get every broadphase we may be intersecting. + _mapManager.FindGridsIntersectingApprox(mapId, worldAABB.Enlarged(_broadphaseExpand), ref state, + static (IMapGrid grid, ref ( + SharedBroadphaseSystem system, + FixtureProxy proxy, + Box2 worldAABB, + List pairBuffer, + EntityQuery xformQuery, + EntityQuery broadphaseQuery) tuple) => + { + ref var buffer = ref tuple.pairBuffer; + tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, grid.GridEntityId, buffer, tuple.xformQuery, tuple.broadphaseQuery); + return true; + }); + + // Struct ref moment, I have no idea what's fastest. + buffer = state.buffer; + FindPairs(proxy, worldAABB, _mapManager.GetMapEntityId(mapId), buffer, xformQuery, broadphaseQuery); + } + }); + + for (var i = 0; i < count; i++) + { + var proxyA = pMoveBuffer[i].Proxy; + var proxies = contactBuffer[i]; var proxyABody = proxyA.Fixture.Body; foreach (var other in proxies) @@ -216,12 +254,13 @@ namespace Robust.Shared.Physics.Systems } } - foreach (var (_, proxies) in _pairBuffer) + for (var i = 0; i < count; i++) { - _proxyPool.Return(proxies); + _bufferPool.Return(contactBuffer[i]); } - _pairBuffer.Clear(); + ArrayPool>.Shared.Return(contactBuffer); + ArrayPool<(FixtureProxy Proxy, Box2 AABB)>.Shared.Return(pMoveBuffer); moveBuffer.Clear(); _mapManager.ClearMovedGrids(mapId); } @@ -311,7 +350,7 @@ namespace Robust.Shared.Physics.Systems FixtureProxy proxy, Box2 worldAABB, EntityUid broadphase, - Dictionary> pairBuffer, + List pairBuffer, EntityQuery xformQuery, EntityQuery broadphaseQuery) { @@ -346,27 +385,21 @@ namespace Robust.Shared.Physics.Systems } var broadphaseComp = broadphaseQuery.GetComponent(broadphase); + var state = (pairBuffer, proxy); - if (!pairBuffer.TryGetValue(proxy, out var proxyPairs)) - { - proxyPairs = _proxyPool.Get(); - pairBuffer[proxy] = proxyPairs; - } - - var state = (proxyPairs, pairBuffer, proxy); - - QueryBroadphase(broadphaseComp.DynamicTree, ref state, aabb); + QueryBroadphase(broadphaseComp.DynamicTree, state, aabb); if ((proxy.Fixture.Body.BodyType & BodyType.Static) != 0x0) return; - QueryBroadphase(broadphaseComp.StaticTree, ref state, aabb); + QueryBroadphase(broadphaseComp.StaticTree, state, aabb); + pairBuffer = state.pairBuffer; } - private void QueryBroadphase(IBroadPhase broadPhase, ref (HashSet, Dictionary>, FixtureProxy) state, Box2 aabb) + private void QueryBroadphase(IBroadPhase broadPhase, (List, FixtureProxy) state, Box2 aabb) { broadPhase.QueryAabb(ref state, static ( - ref (HashSet proxyPairs, Dictionary> pairBuffer, FixtureProxy proxy) tuple, + ref (List pairBuffer, FixtureProxy proxy) tuple, in FixtureProxy other) => { DebugTools.Assert(other.Fixture.Body.CanCollide); @@ -379,16 +412,7 @@ namespace Robust.Shared.Physics.Systems return true; } - // Don't add duplicates. - // Look it disgusts me but we can't do it Box2D's way because we're getting pairs - // with different broadphases so can't use Proxy sorting to skip duplicates. - if (tuple.proxyPairs.Contains(other) || - tuple.pairBuffer.TryGetValue(other, out var otherPairs) && otherPairs.Contains(tuple.proxy)) - { - return true; - } - - tuple.proxyPairs.Add(other); + tuple.pairBuffer.Add(other); return true; }, aabb, true); } diff --git a/Robust.UnitTesting/Server/RobustServerSimulation.cs b/Robust.UnitTesting/Server/RobustServerSimulation.cs index 8b47d969c..841f67659 100644 --- a/Robust.UnitTesting/Server/RobustServerSimulation.cs +++ b/Robust.UnitTesting/Server/RobustServerSimulation.cs @@ -36,6 +36,7 @@ using Robust.Shared.Reflection; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Threading; using Robust.Shared.Timing; namespace Robust.UnitTesting.Server @@ -221,6 +222,7 @@ namespace Robust.UnitTesting.Server container.Register(); container.Register(); container.Register(); + container.Register(); // Needed for grid fixture debugging. container.Register(); diff --git a/Robust.UnitTesting/TestingParallelManager.cs b/Robust.UnitTesting/TestingParallelManager.cs new file mode 100644 index 000000000..3f724406c --- /dev/null +++ b/Robust.UnitTesting/TestingParallelManager.cs @@ -0,0 +1,18 @@ +using System; +using Robust.Shared.Threading; + +namespace Robust.UnitTesting; + +/// +/// Only allows 1 parallel process for testing purposes. +/// +public sealed class TestingParallelManager : IParallelManager +{ + public event Action? ParallelCountChanged; + public int ParallelProcessCount => 1; + public void AddAndInvokeParallelCountChanged(Action changed) + { + // Gottem + return; + } +}