Optimise storage a quadrillion times (#37638)

* Optimise storage a quadrillion times

* How sweaty can we get

* Add fast angle checks

* Fix chunk indices

* Optimise the refresh method

Helps on client a lot as the clientside is suboptimal atm.

* Better name

* wawawewa

* Add single-angle path

* Okay FINE rider
This commit is contained in:
metalgearsloth
2025-05-21 16:16:26 +10:00
committed by GitHub
parent e100324815
commit a393033c64
4 changed files with 323 additions and 54 deletions

View File

@@ -63,6 +63,8 @@ public sealed class StorageSystem : SharedStorageSystem
component.SavedLocations[loc.Key] = new(loc.Value);
}
UpdateOccupied((uid, component));
var uiDirty = !component.StoredItems.SequenceEqual(_oldStoredItems);
if (uiDirty && UI.TryGetOpenUi<StorageBoundUserInterface>(uid, StorageComponent.StorageUiKey.Key, out var storageBui))

View File

@@ -5,6 +5,7 @@ using Content.Shared.Examine;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Storage;
using JetBrains.Annotations;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -206,15 +207,21 @@ public abstract class SharedItemSystem : EntitySystem
public IReadOnlyList<Box2i> GetAdjustedItemShape(Entity<ItemComponent?> entity, Angle rotation, Vector2i position)
{
if (!Resolve(entity, ref entity.Comp))
return new Box2i[] { };
return [];
var adjustedShapes = new List<Box2i>();
GetAdjustedItemShape(adjustedShapes, entity, rotation, position);
return adjustedShapes;
}
public void GetAdjustedItemShape(List<Box2i> adjustedShapes, Entity<ItemComponent?> entity, Angle rotation, Vector2i position)
{
var shapes = GetItemShape(entity);
var boundingShape = shapes.GetBoundingBox();
var boundingCenter = ((Box2) boundingShape).Center;
var matty = Matrix3Helpers.CreateTransform(boundingCenter, rotation);
var drift = boundingShape.BottomLeft - matty.TransformBox(boundingShape).BottomLeft;
var adjustedShapes = new List<Box2i>();
foreach (var shape in shapes)
{
var transformed = matty.TransformBox(shape).Translated(drift);
@@ -223,8 +230,6 @@ public abstract class SharedItemSystem : EntitySystem
adjustedShapes.Add(translated);
}
return adjustedShapes;
}
/// <summary>

View File

@@ -42,13 +42,14 @@ using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared.Rounding;
using Robust.Shared.Collections;
using Robust.Shared.Map.Enumerators;
namespace Content.Shared.Storage.EntitySystems;
public abstract class SharedStorageSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] protected readonly IRobustRandom Random = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
@@ -115,6 +116,10 @@ public abstract class SharedStorageSystem : EntitySystem
protected readonly List<string> CantFillReasons = [];
// Caching for various checks
private readonly Dictionary<Vector2i, ulong> _ignored = new();
private List<Box2i> _itemShape = new();
/// <inheritdoc />
public override void Initialize()
{
@@ -183,6 +188,8 @@ public abstract class SharedStorageSystem : EntitySystem
return;
}
UpdateOccupied((container.Owner, storage));
if (!ItemFitsInGridLocation((itemEnt.Owner, itemEnt.Comp), (container.Owner, storage), loc))
{
ContainerSystem.Remove(itemEnt.Owner, container, force: true);
@@ -237,6 +244,7 @@ public abstract class SharedStorageSystem : EntitySystem
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
// TODO: This should update all entities in storage as well.
if (args.ByType.ContainsKey(typeof(ItemSizePrototype))
|| (args.Removed?.ContainsKey(typeof(ItemSizePrototype)) ?? false))
{
@@ -266,6 +274,9 @@ public abstract class SharedStorageSystem : EntitySystem
{
storageComp.Container = ContainerSystem.EnsureContainer<Container>(uid, StorageComponent.ContainerId);
UpdateAppearance((uid, storageComp, null));
// Make sure the initial starting grid is okay.
UpdateOccupied((uid, storageComp));
}
/// <summary>
@@ -341,7 +352,7 @@ public abstract class SharedStorageSystem : EntitySystem
/// <summary>
/// Tries to get the storage location of an item.
/// </summary>
public bool TryGetStorageLocation(Entity<ItemComponent?> itemEnt, [NotNullWhen(true)] out BaseContainer? container, out StorageComponent? storage, out ItemStorageLocation loc)
public bool TryGetStorageLocation(Entity<ItemComponent?> itemEnt, [NotNullWhen(true)] out BaseContainer? container, [NotNullWhen(true)] out StorageComponent? storage, out ItemStorageLocation loc)
{
loc = default;
storage = null;
@@ -862,7 +873,7 @@ public abstract class SharedStorageSystem : EntitySystem
}
entity.Comp.StoredItems[args.Entity] = location.Value;
Dirty(entity, entity.Comp);
AddOccupiedEntity(entity, args.Entity, location.Value);
}
UpdateAppearance((entity, entity.Comp, null));
@@ -878,7 +889,11 @@ public abstract class SharedStorageSystem : EntitySystem
if (args.Container.ID != StorageComponent.ContainerId)
return;
entity.Comp.StoredItems.Remove(args.Entity);
if (entity.Comp.StoredItems.Remove(args.Entity, out var loc))
{
RemoveOccupiedEntity(entity, args.Entity, loc);
}
Dirty(entity, entity.Comp);
UpdateAppearance((entity, entity.Comp, null));
@@ -1071,7 +1086,7 @@ public abstract class SharedStorageSystem : EntitySystem
return false;
uid.Comp.StoredItems[insertEnt] = location;
Dirty(uid, uid.Comp);
AddOccupiedEntity((uid.Owner, uid.Comp), insertEnt, location);
if (Insert(uid,
insertEnt,
@@ -1085,6 +1100,7 @@ public abstract class SharedStorageSystem : EntitySystem
return true;
}
RemoveOccupiedEntity((uid.Owner, uid.Comp), insertEnt, location);
uid.Comp.StoredItems.Remove(insertEnt);
return false;
}
@@ -1247,9 +1263,14 @@ public abstract class SharedStorageSystem : EntitySystem
if (!ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation))
return false;
storageEnt.Comp.StoredItems[itemEnt] = location;
if (storageEnt.Comp.StoredItems.Remove(itemEnt, out var existing))
{
RemoveOccupiedEntity((storageEnt.Owner, storageEnt.Comp), itemEnt, existing);
}
storageEnt.Comp.StoredItems.Add(itemEnt, location);
AddOccupiedEntity((storageEnt.Owner, storageEnt.Comp), itemEnt, location);
UpdateUI(storageEnt);
Dirty(storageEnt, storageEnt.Comp);
return true;
}
@@ -1294,17 +1315,102 @@ public abstract class SharedStorageSystem : EntitySystem
}
}
for (var y = storageBounding.Bottom; y <= storageBounding.Top; y++)
// Ignore the item's existing location for fitting purposes.
_ignored.Clear();
if (storageEnt.Comp.StoredItems.TryGetValue(itemEnt.Owner, out var existing))
{
for (var x = storageBounding.Left; x <= storageBounding.Right; x++)
AddOccupied(itemEnt, existing, _ignored);
}
// This uses a faster path than the typical codepaths
// as we can cache a bunch more data and re-use it to avoid a bunch of component overhead.
// So if we have an item that occupies 0,0 we can assume that the tile itself we're checking
// is always in its shapes regardless of angle. This matches virtually every item in the game and
// means we can skip getting the item's rotated shape at all if the tile is occupied.
// This mostly makes heavy checks (e.g. area insert) much, much faster.
var fastPath = false;
var itemShape = ItemSystem.GetItemShape(itemEnt);
var fastAngles = itemShape.Count == 1;
foreach (var shape in itemShape)
{
if (shape.Contains(Vector2i.Zero))
{
for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f)
fastPath = true;
break;
}
}
var chunkEnumerator = new ChunkIndicesEnumerator(storageBounding, StorageComponent.ChunkSize);
var angles = new ValueList<Angle>();
if (!fastAngles)
{
angles.Clear();
for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f)
{
angles.Add(angle);
}
}
else
{
var shape = itemShape[0];
// At least 1 check for a square.
angles.Add(startAngle);
// If it's a rectangle make it 2.
if (shape.Width != shape.Height)
{
// Idk if there's a preferred facing but + or - 90 pick one.
angles.Add(startAngle + Angle.FromDegrees(90));
}
}
while (chunkEnumerator.MoveNext(out var storageChunk))
{
var storageChunkOrigin = storageChunk.Value * StorageComponent.ChunkSize;
var left = Math.Max(storageChunkOrigin.X, storageBounding.Left);
var bottom = Math.Max(storageChunkOrigin.Y, storageBounding.Bottom);
var top = Math.Min(storageChunkOrigin.Y + StorageComponent.ChunkSize - 1, storageBounding.Top);
var right = Math.Min(storageChunkOrigin.X + StorageComponent.ChunkSize - 1, storageBounding.Right);
// No data so assume empty.
if (!storageEnt.Comp.OccupiedGrid.TryGetValue(storageChunkOrigin, out var occupied))
continue;
// This has a lot of redundant tile checks but with the fast path it shouldn't matter for average ss14
// use cases.
for (var y = bottom; y <= top; y++)
{
for (var x = left; x <= right; x++)
{
var location = new ItemStorageLocation(angle, (x, y));
if (ItemFitsInGridLocation(itemEnt, storageEnt, location))
foreach (var angle in angles)
{
storageLocation = location;
return true;
var position = new Vector2i(x, y);
// This bit of code is how area inserts go from tanking frames to being negligible.
if (fastPath)
{
var flag = SharedMapSystem.ToBitmask(position, StorageComponent.ChunkSize);
// Occupied so skip.
if ((occupied & flag) == flag)
continue;
}
_itemShape.Clear();
ItemSystem.GetAdjustedItemShape(_itemShape, itemEnt, angle, position);
if (ItemFitsInGridLocation(storageEnt.Comp.OccupiedGrid, _itemShape, _ignored))
{
storageLocation = new ItemStorageLocation(angle, position);
return true;
}
}
}
}
@@ -1395,6 +1501,59 @@ public abstract class SharedStorageSystem : EntitySystem
return ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation);
}
private bool ItemFitsInGridLocation(
Dictionary<Vector2i, ulong> occupied,
IReadOnlyList<Box2i> itemShape,
Dictionary<Vector2i, ulong> ignored)
{
// We pre-cache the occupied / ignored tiles upfront and then can just check each tile 1-by-1.
// We do it by chunk so we can avoid dictionary overhead.
foreach (var box in itemShape)
{
var chunkEnumerator = new ChunkIndicesEnumerator(box, StorageComponent.ChunkSize);
while (chunkEnumerator.MoveNext(out var chunk))
{
var chunkOrigin = chunk.Value * StorageComponent.ChunkSize;
// Box may not necessarily be in 1 chunk so clamp it.
var left = Math.Max(chunkOrigin.X, box.Left);
var bottom = Math.Max(chunkOrigin.Y, box.Bottom);
var right = Math.Min(chunkOrigin.X + StorageComponent.ChunkSize - 1, box.Right);
var top = Math.Min(chunkOrigin.Y + StorageComponent.ChunkSize - 1, box.Top);
// Assume it's occupied if no data.
if (!occupied.TryGetValue(chunkOrigin, out var occupiedMask))
{
return false;
}
var ignoredMask = ignored.GetValueOrDefault(chunkOrigin);
for (var x = left; x <= right; x++)
{
for (var y = bottom; y <= top; y++)
{
var index = new Vector2i(x, y);
var chunkRelative = SharedMapSystem.GetChunkRelative(index, StorageComponent.ChunkSize);
var flag = SharedMapSystem.ToBitmask(chunkRelative, StorageComponent.ChunkSize);
// Ignore it
if ((ignoredMask & flag) == flag)
continue;
if ((occupiedMask & flag) == flag)
{
return false;
}
}
}
}
}
return true;
}
/// <summary>
/// Checks if an item fits into a specific spot on a storage grid.
/// </summary>
@@ -1412,62 +1571,157 @@ public abstract class SharedStorageSystem : EntitySystem
return false;
var itemShape = ItemSystem.GetAdjustedItemShape(itemEnt, rotation, position);
// Ignore the item's existing location for fitting purposes.
_ignored.Clear();
foreach (var box in itemShape)
if (storageEnt.Comp.StoredItems.TryGetValue(itemEnt.Owner, out var existing))
{
for (var offsetY = box.Bottom; offsetY <= box.Top; offsetY++)
{
for (var offsetX = box.Left; offsetX <= box.Right; offsetX++)
{
var pos = (offsetX, offsetY);
AddOccupied(itemEnt, existing, _ignored);
}
if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos))
return false;
}
}
return ItemFitsInGridLocation(storageEnt.Comp.OccupiedGrid, itemShape, _ignored);
}
/// <summary>
/// Checks if a space on a grid is valid and not occupied by any other pieces.
/// </summary>
public bool IsGridSpaceEmpty(Entity<StorageComponent?> storageEnt, Vector2i location, Dictionary<Vector2i, ulong>? ignored = null)
{
if (!Resolve(storageEnt, ref storageEnt.Comp))
return false;
var chunkOrigin = SharedMapSystem.GetChunkIndices(location, StorageComponent.ChunkSize) * StorageComponent.ChunkSize;
// No entry so assume it's occupied.
if (!storageEnt.Comp.OccupiedGrid.TryGetValue(chunkOrigin, out var occupiedMask))
return false;
var chunkRelative = SharedMapSystem.GetChunkRelative(location, StorageComponent.ChunkSize);
var occupiedIndex = SharedMapSystem.ToBitmask(chunkRelative);
if (ignored?.TryGetValue(chunkOrigin, out var ignoredMask) == true && (ignoredMask & occupiedIndex) == occupiedIndex)
{
return true;
}
if ((occupiedMask & occupiedIndex) != 0x0)
{
return false;
}
return true;
}
/// <summary>
/// Checks if a space on a grid is valid and not occupied by any other pieces.
/// Updates the occupied grid mask for the entity.
/// </summary>
public bool IsGridSpaceEmpty(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, Vector2i location)
protected void UpdateOccupied(Entity<StorageComponent> ent)
{
if (!Resolve(storageEnt, ref storageEnt.Comp))
return false;
ent.Comp.OccupiedGrid.Clear();
RemoveOccupied(ent.Comp.Grid, ent.Comp.OccupiedGrid);
var validGrid = false;
foreach (var grid in storageEnt.Comp.Grid)
Dirty(ent);
foreach (var (stent, storedItem) in ent.Comp.StoredItems)
{
if (grid.Contains(location))
{
validGrid = true;
break;
}
}
if (!validGrid)
return false;
foreach (var (ent, storedItem) in storageEnt.Comp.StoredItems)
{
if (ent == itemEnt.Owner)
if (!_itemQuery.TryGetComponent(stent, out var itemComp))
continue;
if (!_itemQuery.TryGetComponent(ent, out var itemComp))
continue;
AddOccupiedEntity(ent, (stent, itemComp), storedItem);
}
}
var adjustedShape = ItemSystem.GetAdjustedItemShape((ent, itemComp), storedItem);
foreach (var box in adjustedShape)
private void AddOccupiedEntity(Entity<StorageComponent> storageEnt, Entity<ItemComponent?> itemEnt, ItemStorageLocation location)
{
AddOccupied(itemEnt, location, storageEnt.Comp.OccupiedGrid);
Dirty(storageEnt);
}
private void AddOccupied(Entity<ItemComponent?> itemEnt, ItemStorageLocation location, Dictionary<Vector2i, ulong> occupied)
{
var adjustedShape = ItemSystem.GetAdjustedItemShape((itemEnt.Owner, itemEnt.Comp), location);
AddOccupied(adjustedShape, occupied);
}
private void RemoveOccupied(IReadOnlyList<Box2i> adjustedShape, Dictionary<Vector2i, ulong> occupied)
{
foreach (var box in adjustedShape)
{
var chunks = new ChunkIndicesEnumerator(box, StorageComponent.ChunkSize);
while (chunks.MoveNext(out var chunk))
{
if (box.Contains(location))
return false;
var chunkOrigin = chunk.Value * StorageComponent.ChunkSize;
var left = Math.Max(box.Left, chunkOrigin.X);
var bottom = Math.Max(box.Bottom, chunkOrigin.Y);
var right = Math.Min(box.Right, chunkOrigin.X + StorageComponent.ChunkSize - 1);
var top = Math.Min(box.Top, chunkOrigin.Y + StorageComponent.ChunkSize - 1);
var existing = occupied.GetValueOrDefault(chunkOrigin, ulong.MaxValue);
// Unmark all of the tiles that we actually have.
for (var x = left; x <= right; x++)
{
for (var y = bottom; y <= top; y++)
{
var index = new Vector2i(x, y);
var chunkRelative = SharedMapSystem.GetChunkRelative(index, StorageComponent.ChunkSize);
var flag = SharedMapSystem.ToBitmask(chunkRelative, StorageComponent.ChunkSize);
existing &= ~flag;
}
}
// My kingdom for collections.marshal
occupied[chunkOrigin] = existing;
}
}
}
return true;
private void AddOccupied(IReadOnlyList<Box2i> adjustedShape, Dictionary<Vector2i, ulong> occupied)
{
foreach (var box in adjustedShape)
{
// Reduce dictionary access from every tile to just once per chunk.
// Makes this more complicated but dictionaries are slow af.
// This is how we get savings over IsGridSpaceEmpty.
var chunkEnumerator = new ChunkIndicesEnumerator(box, StorageComponent.ChunkSize);
while (chunkEnumerator.MoveNext(out var chunk))
{
var chunkOrigin = chunk.Value * StorageComponent.ChunkSize;
var existing = occupied.GetOrNew(chunkOrigin);
// Box may not necessarily be in 1 chunk so clamp it.
var left = Math.Max(chunkOrigin.X, box.Left);
var bottom = Math.Max(chunkOrigin.Y, box.Bottom);
var right = Math.Min(chunkOrigin.X + StorageComponent.ChunkSize - 1, box.Right);
var top = Math.Min(chunkOrigin.Y + StorageComponent.ChunkSize - 1, box.Top);
for (var x = left; x <= right; x++)
{
for (var y = bottom; y <= top; y++)
{
var index = new Vector2i(x, y);
var chunkRelative = SharedMapSystem.GetChunkRelative(index, StorageComponent.ChunkSize);
var flag = SharedMapSystem.ToBitmask(chunkRelative, StorageComponent.ChunkSize);
existing |= flag;
}
}
occupied[chunkOrigin] = existing;
}
}
}
private void RemoveOccupiedEntity(Entity<StorageComponent> storageEnt, Entity<ItemComponent?> itemEnt, ItemStorageLocation location)
{
var adjustedShape = ItemSystem.GetAdjustedItemShape((itemEnt.Owner, itemEnt.Comp), location);
RemoveOccupied(adjustedShape, storageEnt.Comp.OccupiedGrid);
Dirty(storageEnt);
}
/// <summary>

View File

@@ -19,6 +19,14 @@ namespace Content.Shared.Storage
{
public static string ContainerId = "storagebase";
public const byte ChunkSize = 8;
// No datafield because we can just derive it from stored items.
/// <summary>
/// Bitmask of occupied tiles
/// </summary>
public Dictionary<Vector2i, ulong> OccupiedGrid = new();
[ViewVariables]
public Container Container = default!;