diff --git a/Robust.Benchmarks/Physics/BroadphaseDetachBenchmark.cs b/Robust.Benchmarks/Physics/BroadphaseDetachBenchmark.cs new file mode 100644 index 00000000000..3fa87c514e7 --- /dev/null +++ b/Robust.Benchmarks/Physics/BroadphaseDetachBenchmark.cs @@ -0,0 +1,78 @@ +using BenchmarkDotNet.Attributes; +using JetBrains.Annotations; +using System.Numerics; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Maths; +using Robust.UnitTesting.Server; + +namespace Robust.Benchmarks.Physics; + +[Virtual, MemoryDiagnoser] +public class BroadphaseDetachBenchmark +{ + private ISimulation _simulation = default!; + private IEntityManager _entManager = default!; + private EntityLookupSystem _lookup = default!; + private SharedMapSystem _map = default!; + + private EntityUid _root; + private TransformComponent _rootXform = default!; + private EntityUid[] _children = default!; + + [UsedImplicitly] + [Params(0, 20, 50, 100)] + public int Children; + + [GlobalSetup] + public void GlobalSetup() + { + _simulation = RobustServerSimulation.NewSimulation().InitializeInstance(); + _entManager = _simulation.Resolve(); + var systems = _entManager.EntitySysManager; + _lookup = systems.GetEntitySystem(); + _map = systems.GetEntitySystem(); + + var (mapUid, mapId) = _simulation.CreateMap(); + var grid = _map.CreateGridEntity(mapId); + _map.SetTile(grid, Vector2i.Zero, new Tile(1)); + + _root = _entManager.SpawnEntity(null, new EntityCoordinates(grid.Owner, new Vector2(0.5f, 0.5f))); + _rootXform = _entManager.GetComponent(_root); + _children = new EntityUid[Children]; + + var parent = _root; + for (var i = 0; i < Children; i++) + { + var child = _entManager.SpawnEntity(null, new EntityCoordinates(parent, new Vector2(0.001f, 0f))); + _children[i] = child; + parent = child; + } + } + + [IterationSetup] + public void IterationSetup() + { + _lookup.FindAndAddToEntityTree(_root, true, _rootXform); + } + + [Benchmark(Baseline = true)] + public void RemoveEveryEntityRecursive() + { + for (var i = _children.Length - 1; i >= 0; i--) + { + var child = _children[i]; + _lookup.RemoveFromEntityTree(child, _entManager.GetComponent(child)); + } + + _lookup.RemoveFromEntityTree(_root, _rootXform); + } + + [Benchmark] + public void RemoveRootOnly() + { + _lookup.RemoveFromEntityTree(_root, _rootXform); + } +} diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs index 9ef5161534e..76a3a2ac0d7 100644 --- a/Robust.Client/GameStates/ClientGameStateManager.cs +++ b/Robust.Client/GameStates/ClientGameStateManager.cs @@ -59,6 +59,8 @@ public sealed partial class ClientGameStateManager : IClientGameStateManager private readonly HashSet _sorted = new(); private readonly List _created = new(); private readonly List _detached = new(); + private readonly HashSet _detachBatch = new(); + private readonly List<(NetEntity NetEntity, Entity Entity)> _detachEntities = new(); private readonly record struct StateData( EntityUid Uid, @@ -1301,25 +1303,29 @@ private void Detach(GameTick maxTick, ContainerSystem containerSys, EntityLookupSystem lookupSys) { + _detachBatch.Clear(); + _detachEntities.Clear(); + foreach (var netEntity in entities) { - if (!_entities.TryGetEntityData(netEntity, out var ent, out var meta)) + if (!TryGetDetachEntity(netEntity, maxTick, out var ent)) continue; - if (meta.LastStateApplied > maxTick) - { - // Server sent a new state for this entity sometime after the detach message was sent. The - // detach message probably just arrived late or was initially dropped. - continue; - } + _detachBatch.Add(ent.Owner); + _detachEntities.Add((netEntity, ent)); + } - if ((meta.Flags & (MetaDataFlags.Detached | MetaDataFlags.Undetachable)) != 0) - continue; + var broadphaseRoots = 0; + + foreach (var (netEntity, ent) in _detachEntities) + { + var uid = ent.Owner; + var metadata = ent.Comp; if (lastStateApplied.HasValue) - meta.LastStateApplied = lastStateApplied.Value; + metadata.LastStateApplied = lastStateApplied.Value; - var xform = xforms.GetComponent(ent.Value); + var xform = xforms.GetComponent(uid); // TODO PVS DETACH // Why is this if block here again? If a null-space entity gets sent to a player via some PVS override, @@ -1327,30 +1333,35 @@ private void Detach(GameTick maxTick, // I.e., modifying the metadata flag & pausing the entity should probably happen outside of this block. if (xform.ParentUid.IsValid()) { - lookupSys.RemoveFromEntityTree(ent.Value, xform); + if (!HasDetachingParent(xform, xforms)) + { + lookupSys.RemoveFromEntityTree(uid, xform); + broadphaseRoots++; + } + xform.Broadphase = BroadphaseData.Invalid; // In some cursed scenarios an entity inside of a container can leave PVS without the container itself leaving PVS. // In those situations, we need to add the entity back to the list of expected entities after detaching. BaseContainer? container = null; - if ((meta.Flags & MetaDataFlags.InContainer) != 0 && + if ((metadata.Flags & MetaDataFlags.InContainer) != 0 && metas.TryGetComponent(xform.ParentUid, out var containerMeta) && (containerMeta.Flags & MetaDataFlags.Detached) == 0 && - containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container)) + containerSys.TryGetContainingContainer(xform.ParentUid, uid, out container)) { - containerSys.Remove((ent.Value, xform, meta), container, false, true); + containerSys.Remove((uid, xform, metadata), container, false, true); } - meta._flags |= MetaDataFlags.Detached; - xformSys.DetachEntity(ent.Value, xform); - DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0); + metadata._flags |= MetaDataFlags.Detached; + xformSys.DetachEntity(uid, xform); + DebugTools.Assert((metadata.Flags & MetaDataFlags.InContainer) == 0); // We mark the entity as paused, without raising a pause-event. // The entity gets un-paused when the metadata's comp-state is reapplied (which also does not raise // an un-pause event). The assumption is that game logic that has to handle the pausing should be // getting networked anyway. And if its some client-side timer on a networked entity, the timer // shouldn't actually be getting paused just because the entity has left the players view. - meta.PauseTime = TimeSpan.Zero; + metadata.PauseTime = TimeSpan.Zero; if (container != null) containerSys.AddExpectedEntity(netEntity, container); @@ -1358,6 +1369,50 @@ private void Detach(GameTick maxTick, _detached.Add(netEntity); } + + _prof.WriteValue("Broadphase roots", ProfData.Int32(broadphaseRoots)); + _prof.WriteValue("Batch", ProfData.Int32(_detachBatch.Count)); + _detachBatch.Clear(); + _detachEntities.Clear(); + } + + private bool TryGetDetachEntity( + NetEntity netEntity, + GameTick maxTick, + out Entity ent) + { + ent = default; + + if (!_entities.TryGetEntityData(netEntity, out var uid, out var meta)) + return false; + + if (meta.LastStateApplied > maxTick) + { + // Server sent a new state for this entity sometime after the detach message was sent. The + // detach message probably just arrived late or was initially dropped. + return false; + } + + if ((meta.Flags & (MetaDataFlags.Detached | MetaDataFlags.Undetachable)) != 0) + return false; + + ent = (uid.Value, meta); + return true; + } + + private bool HasDetachingParent(TransformComponent xform, EntityQuery xforms) + { + var parent = xform.ParentUid; + + while (parent.IsValid()) + { + if (_detachBatch.Contains(parent)) + return true; + + parent = xforms.GetComponent(parent).ParentUid; + } + + return false; } private void HandleEntityState(in StateData data, IEventBus bus, GameTick toTick) diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs index 4e3ef777c0f..b41bb303edf 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs @@ -242,7 +242,7 @@ private void InitializeChild( if (!_broadQuery.TryGetComponent(xform.Broadphase.Value.Uid, out var oldBroadphase)) { - DebugTools.Assert("Encountered deleted broadphase."); + AssertMissingBroadphaseExpected(xform.Broadphase.Value.Uid); if (_fixturesQuery.TryGetComponent(child, out var fixtures)) { foreach (var fixture in fixtures.Fixtures.Values) @@ -567,7 +567,7 @@ private void UpdateParent(EntityUid uid, TransformComponent xform) if (!_broadQuery.TryGetComponent(xform.Broadphase.Value.Uid, out oldBroadphase)) { - DebugTools.Assert("Encountered deleted broadphase."); + AssertMissingBroadphaseExpected(xform.Broadphase.Value.Uid); // broadphase was probably deleted. if (_fixturesQuery.TryGetComponent(uid, out var fixtures)) @@ -794,6 +794,16 @@ private void RemoveFromEntityTree( // parented to one from another. DebugTools.Assert(_netMan.IsClient); broadUid = old.Uid; + + if (!_broadQuery.TryGetComponent(broadUid, out var currentBroadphase)) + { + AssertMissingBroadphaseExpected(broadUid); + ClearFixtureProxies(uid); + xform.Broadphase = null; + return; + } + + broadphase = currentBroadphase; } if (old.CanCollide) @@ -828,17 +838,9 @@ public bool TryGetCurrentBroadphase(TransformComponent xform, [NotNullWhen(true) if (!_broadQuery.TryGetComponent(old.Uid, out broadphase)) { // broadphase was probably deleted - DebugTools.Assert("Encountered deleted broadphase."); - - if (_fixturesQuery.TryGetComponent(xform.Owner, out FixturesComponent? fixtures)) - { - foreach (var fixture in fixtures.Fixtures.Values) - { - fixture.ProxyCount = 0; - fixture.Proxies = Array.Empty(); - } - } + AssertMissingBroadphaseExpected(old.Uid); + ClearFixtureProxies(xform.Owner); xform.Broadphase = null; return false; } @@ -846,6 +848,26 @@ public bool TryGetCurrentBroadphase(TransformComponent xform, [NotNullWhen(true) return true; } + private void AssertMissingBroadphaseExpected(EntityUid broadphaseUid) + { + if (TerminatingOrDeleted(broadphaseUid)) + return; + + DebugTools.Assert("Encountered deleted broadphase."); + } + + private void ClearFixtureProxies(EntityUid uid) + { + if (!_fixturesQuery.TryGetComponent(uid, out FixturesComponent? fixtures)) + return; + + foreach (var fixture in fixtures.Fixtures.Values) + { + fixture.ProxyCount = 0; + fixture.Proxies = Array.Empty(); + } + } + public BroadphaseComponent? GetCurrentBroadphase(TransformComponent xform) { TryGetCurrentBroadphase(xform, out var broadphase);