From 48276eb00a1b579e7214cd6b5ca9e1ac54521530 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Mon, 10 Jul 2023 17:15:59 +1000 Subject: [PATCH] Optimise marker spawning (#17922) --- Content.Server/Parallax/BiomeSystem.cs | 222 +++++++++++------- .../Parallax/Biomes/BiomeComponent.cs | 6 + .../Parallax/Biomes/SharedBiomeSystem.cs | 20 +- 3 files changed, 158 insertions(+), 90 deletions(-) diff --git a/Content.Server/Parallax/BiomeSystem.cs b/Content.Server/Parallax/BiomeSystem.cs index 6915ed5a11..9d38df8d5c 100644 --- a/Content.Server/Parallax/BiomeSystem.cs +++ b/Content.Server/Parallax/BiomeSystem.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Threading.Tasks; using Content.Server.Decals; using Content.Server.Ghost.Roles.Components; using Content.Server.Shuttles.Events; @@ -6,6 +7,7 @@ using Content.Shared.Decals; using Content.Shared.Parallax.Biomes; using Content.Shared.Parallax.Biomes.Layers; using Content.Shared.Parallax.Biomes.Markers; +using Microsoft.Extensions.ObjectPool; using Robust.Server.Player; using Robust.Shared; using Robust.Shared.Collections; @@ -17,6 +19,8 @@ using Robust.Shared.Noise; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Threading; using Robust.Shared.Utility; namespace Content.Server.Parallax; @@ -26,9 +30,11 @@ public sealed partial class BiomeSystem : SharedBiomeSystem [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IConsoleHost _console = default!; [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IParallelManager _parallel = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly ISerializationManager _serManager = default!; [Dependency] private readonly DecalSystem _decals = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; @@ -36,6 +42,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem private const float DefaultLoadRange = 16f; private float _loadRange = DefaultLoadRange; + private ObjectPool> _tilePool = + new DefaultObjectPool>(new SetPolicy(), 256); + /// /// Load area for chunks containing tiles, decals etc. /// @@ -265,7 +274,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem while (biomes.MoveNext(out var biome)) { - _activeChunks.Add(biome, new HashSet()); + _activeChunks.Add(biome, _tilePool.Get()); _markerChunks.GetOrNew(biome); } @@ -321,6 +330,12 @@ public sealed partial class BiomeSystem : SharedBiomeSystem } _handledEntities.Clear(); + + foreach (var tiles in _activeChunks.Values) + { + _tilePool.Return(tiles); + } + _activeChunks.Clear(); _markerChunks.Clear(); } @@ -364,100 +379,53 @@ public sealed partial class BiomeSystem : SharedBiomeSystem { var markers = _markerChunks[component]; var loadedMarkers = component.LoadedMarkers; - var spawnSet = new HashSet(); - var spawns = new List(); - var frontier = new ValueList(); foreach (var (layer, chunks) in markers) { - foreach (var chunk in chunks) + Parallel.ForEach(chunks, new ParallelOptions() { MaxDegreeOfParallelism = _parallel.ParallelProcessCount }, chunk => { if (loadedMarkers.TryGetValue(layer, out var mobChunks) && mobChunks.Contains(chunk)) - continue; + return; + + var noiseCopy = new FastNoiseLite(); + _serManager.CopyTo(component.Noise, ref noiseCopy, notNullableOverride: true); + var spawnSet = _tilePool.Get(); + var frontier = new ValueList(32); + + // Make a temporary version and copy back in later. + var pending = new Dictionary>>(); - spawns.Clear(); - spawnSet.Clear(); var layerProto = _proto.Index(layer); var buffer = layerProto.Radius / 2f; - mobChunks ??= new HashSet(); - mobChunks.Add(chunk); - loadedMarkers[layer] = mobChunks; var rand = new Random(noise.GetSeed() + chunk.X * 8 + chunk.Y + layerProto.GetHashCode()); // We treat a null entity mask as requiring nothing else on the tile var lower = (int) Math.Floor(buffer); var upper = (int) Math.Ceiling(layerProto.Size - buffer); - // TODO: Okay this is inefficient as FUCK - // I think the ideal is pick a random tile then BFS outwards from it probably ig - // It will bias edge tiles significantly more but will make the CPU cry less. - for (var x = lower; x <= upper; x++) - { - for (var y = lower; y <= upper; y++) - { - var index = new Vector2i(x + chunk.X, y + chunk.Y); - TryGetEntity(index, component.Layers, component.Noise, grid, out var proto); - - if (proto != layerProto.EntityMask) - { - continue; - } - - spawns.Add(index); - spawnSet.Add(index); - } - } - - // Load NOW // TODO: Need poisson but crashes whenever I use moony's due to inputs or smth idk - var count = (int) ((layerProto.Size - buffer) * (layerProto.Size - buffer) / (layerProto.Radius * layerProto.Radius)); + var count = (int) ((layerProto.Size - buffer) * (layerProto.Size - buffer) / + (layerProto.Radius * layerProto.Radius)); count = Math.Min(count, layerProto.MaxCount); + // Pick a random tile then BFS outwards from it + // It will bias edge tiles significantly more but will make the CPU cry less. for (var i = 0; i < count; i++) { - if (spawns.Count == 0) - break; - - var index = rand.Next(spawns.Count); - var point = spawns[index]; - spawns.RemoveSwap(index); - - // Point was potentially used in BFS search below but we hadn't updated the list yet. - if (!spawnSet.Remove(point)) - { - i--; - continue; - } - - // BFS search - frontier.Add(point); var groupCount = layerProto.GroupCount; + var startNodeX = rand.Next(lower, upper + 1); + var startNodeY = rand.Next(lower, upper + 1); + var startNode = new Vector2i(startNodeX, startNodeY); + frontier.Clear(); + frontier.Add(startNode); - while (frontier.Count > 0 && groupCount > 0) + while (groupCount > 0 && frontier.Count > 0) { - var frontierIndex = _random.Next(frontier.Count); + var frontierIndex = rand.Next(frontier.Count); var node = frontier[frontierIndex]; frontier.RemoveSwap(frontierIndex); - var enumerator = grid.GetAnchoredEntitiesEnumerator(node); - - if (enumerator.MoveNext(out _)) - continue; - - // Need to ensure the tile under it has loaded for anchoring. - if (TryGetBiomeTile(node, component.Layers, component.Noise, grid, out var tile)) - { - grid.SetTile(node, tile.Value); - } - - // If it is a ghost role then purge it - // TODO: This is *kind* of a bandaid but natural mobs spawns needs a lot more work. - // Ideally we'd just have ghost role and non-ghost role variants for some stuff. - var uid = EntityManager.CreateEntityUninitialized(layerProto.Prototype, new EntityCoordinates(gridUid, node)); - RemComp(uid); - RemComp(uid); - EntityManager.InitializeAndStartEntity(uid); - groupCount--; + // Add neighbors regardless. for (var x = -1; x <= 1; x++) { for (var y = -1; y <= 1; y++) @@ -467,25 +435,85 @@ public sealed partial class BiomeSystem : SharedBiomeSystem var neighbor = new Vector2i(x + node.X, y + node.Y); - if (!spawnSet.Contains(neighbor)) + // Check if it's inbounds. + if (neighbor.X < lower || + neighbor.Y < lower || + neighbor.X > upper || + neighbor.Y > upper) + { continue; + } frontier.Add(neighbor); - // Rather than doing some uggo remove check on the list we'll defer it until later - spawnSet.Remove(neighbor); } } + + var actualNode = node + chunk; + + if (!spawnSet.Add(actualNode)) + continue; + + // Check if it's a valid spawn, if so then use it. + var enumerator = grid.GetAnchoredEntitiesEnumerator(actualNode); + + if (enumerator.MoveNext(out _)) + continue; + + // Check if mask matches. + TryGetEntity(actualNode, component.Layers, noiseCopy, grid, out var proto); + + if (proto != layerProto.EntityMask) + { + continue; + } + + var chunkOrigin = SharedMapSystem.GetChunkIndices(actualNode, ChunkSize) * ChunkSize; + + if (!pending.TryGetValue(chunkOrigin, out var pendingMarkers)) + { + pendingMarkers = new Dictionary>(); + pending[chunkOrigin] = pendingMarkers; + } + + if (!pendingMarkers.TryGetValue(layer, out var layerMarkers)) + { + layerMarkers = new List(); + pendingMarkers[layer] = layerMarkers; + } + + // Log.Info($"Added node at {actualNode} for chunk {chunkOrigin}"); + layerMarkers.Add(actualNode); + groupCount--; + } + } + + lock (component.PendingMarkers) + { + if (!loadedMarkers.TryGetValue(layer, out var lockMobChunks)) + { + lockMobChunks = new HashSet(); + loadedMarkers[layer] = lockMobChunks; + } + + lockMobChunks.Add(chunk); + + foreach (var (chunkOrigin, layers) in pending) + { + if (!component.PendingMarkers.TryGetValue(chunkOrigin, out var lockMarkers)) + { + lockMarkers = new Dictionary>(); + component.PendingMarkers[chunkOrigin] = lockMarkers; + } + + foreach (var (lockLayer, nodes) in layers) + { + lockMarkers[lockLayer] = nodes; + } } - // Add the unused nodes back in - foreach (var node in frontier) - { - spawnSet.Add(node); - } - - frontier.Clear(); + _tilePool.Return(spawnSet); } - } + }); } var active = _activeChunks[component]; @@ -515,7 +543,36 @@ public sealed partial class BiomeSystem : SharedBiomeSystem EntityQuery xformQuery) { component.ModifiedTiles.TryGetValue(chunk, out var modified); - modified ??= new HashSet(); + modified ??= _tilePool.Get(); + + // Load any pending marker tiles first. + if (component.PendingMarkers.TryGetValue(chunk, out var layers)) + { + foreach (var (layer, nodes) in layers) + { + var layerProto = _proto.Index(layer); + + foreach (var node in nodes) + { + // Need to ensure the tile under it has loaded for anchoring. + if (TryGetBiomeTile(node, component.Layers, component.Noise, grid, out var tile)) + { + grid.SetTile(node, tile.Value); + } + + // If it is a ghost role then purge it + // TODO: This is *kind* of a bandaid but natural mobs spawns needs a lot more work. + // Ideally we'd just have ghost role and non-ghost role variants for some stuff. + var uid = EntityManager.CreateEntityUninitialized(layerProto.Prototype, grid.GridTileToLocal(node)); + RemComp(uid); + RemComp(uid); + EntityManager.InitializeAndStartEntity(uid); + modified.Add(node); + } + } + + component.PendingMarkers.Remove(chunk); + } // Set tiles first for (var x = 0; x < ChunkSize; x++) @@ -606,6 +663,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem if (modified.Count == 0) { + _tilePool.Return(modified); component.ModifiedTiles.Remove(chunk); } else diff --git a/Content.Shared/Parallax/Biomes/BiomeComponent.cs b/Content.Shared/Parallax/Biomes/BiomeComponent.cs index b8c233d407..19b0e85744 100644 --- a/Content.Shared/Parallax/Biomes/BiomeComponent.cs +++ b/Content.Shared/Parallax/Biomes/BiomeComponent.cs @@ -58,6 +58,12 @@ public sealed partial class BiomeComponent : Component #region Markers + /// + /// Work out entire marker tiles in advance but only load the entities when in range. + /// + [DataField("pendingMarkers")] + public Dictionary>> PendingMarkers = new(); + /// /// Track what markers we've loaded already to avoid double-loading. /// diff --git a/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs b/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs index 4780883b11..e22f009760 100644 --- a/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs +++ b/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs @@ -28,20 +28,22 @@ public abstract class SharedBiomeSystem : EntitySystem component.Noise.SetSeed(component.Seed); } - protected T Pick(List collection, float value) + private T Pick(List collection, float value) { - DebugTools.Assert(value is >= 0f and <= 1f); + // Listen I don't need this exact and I'm too lazy to finetune just for random ent picking. + value %= 1f; + value = Math.Clamp(value, 0f, 1f); if (collection.Count == 1) return collection[0]; - value *= collection.Count; + var randValue = value * collection.Count; foreach (var item in collection) { - value -= 1f; + randValue -= 1f; - if (value <= 0f) + if (randValue <= 0f) { return item; } @@ -50,9 +52,10 @@ public abstract class SharedBiomeSystem : EntitySystem throw new ArgumentOutOfRangeException(); } - protected int Pick(int count, float value) + private int Pick(int count, float value) { - DebugTools.Assert(value is >= 0f and <= 1f); + value %= 1f; + value = Math.Clamp(value, 0f, 1f); if (count == 1) return 0; @@ -234,7 +237,8 @@ public abstract class SharedBiomeSystem : EntitySystem return false; } - entity = Pick(biomeLayer.Entities, (noise.GetNoise(indices.X, indices.Y, i) + 1f) / 2f); + var noiseValue = noise.GetNoise(indices.X, indices.Y, i); + entity = Pick(biomeLayer.Entities, (noiseValue + 1f) / 2f); noise.SetSeed(oldSeed); return true; }