using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using Robust.Shared; using Robust.Shared.GameObjects; using Robust.Shared.Threading; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Robust.Server.GameStates; // This partial class handles the PvsData memory. This array stores information about when each entity was last sent to // each player. This is somewhat faster than using a per-player Dictionary, though it can be less // memory efficient. internal sealed partial class PvsSystem { // This is used for asserts. private HashSet _assignedEnts = new(); /// /// Recently returned indexes from deleted entities. These get moved to before /// moving back into the free list. /// private List _incomingReturns = new(); /// /// Recently returned pointers from deleted entities. These will get returned to the free list /// after a minimum amount of time has passed, to ensure that processing late game-state ack messages doesn't /// write data to deleted entities. /// private List _pendingReturns = new(); /// /// Tick at which the were last processed. /// private GameTick _lastReturn = GameTick.Zero; /// /// Memory region to store instances and the free list. /// /// /// Unused elements form a linked list out of elements. /// private ResizableMemoryRegion _metadataMemory = default!; /// /// The head of the PVS data free list. This is the first element that will be used if a new one is needed. /// /// /// If the value is , /// there are no more free elements and the next allocation must expand the memory. /// private PvsIndex _dataFreeListHead; private WaitHandle? _deletionTask; /// /// Expand the size of (and all session data stores) one iteration. /// /// /// This ensures that we have at least one free list slot. /// private void ExpandEntityCapacity() { var initial = _metadataMemory.CurrentSize; var entityGrowth = _configManager.GetCVar(CVars.NetPvsEntityGrowth); var newSize = initial + (entityGrowth <= 0 ? initial : entityGrowth); newSize = Math.Min(newSize, _metadataMemory.MaxSize); if (newSize == initial) throw new InvalidOperationException("Out of PVS entity capacity! Increase net.pvs_entity_max!"); Log.Debug($"Growing PvsData memory from {initial} -> {newSize} entities"); _metadataMemory.Expand(newSize); foreach (var playerSession in PlayerData.Values) { playerSession.DataMemory.Expand(newSize); } var newSlots = _metadataMemory.GetSpan()[initial..]; InitializeFreeList(newSlots, initial, ref _dataFreeListHead); } /// /// Initialize and the free list. /// private void InitializePvsArray() { var initialCount = _configManager.GetCVar(CVars.NetPvsEntityInitial); var maxCount = _configManager.GetCVar(CVars.NetPvsEntityMax); if (initialCount <= 0 || maxCount <= 0) throw new InvalidOperationException("net.pvs_entity_initial and net.pvs_entity_max must be positive"); _metadataMemory = new ResizableMemoryRegion(maxCount, initialCount); ResetDataMemory(); } /// /// Initialize a section of the free list. /// /// The section of the free list to initialize. /// What offset in the total PVS data this section starts at. /// The current head storage of the free list to update. private static void InitializeFreeList(Span memory, int baseOffset, ref PvsIndex head) { for (var i = 0; i < memory.Length; i++) { memory[i].NextFree = new PvsIndex(baseOffset + i + 1); } memory[^1].NextFree = head; head = new PvsIndex(baseOffset); } /// /// Clear all PVS data. After this function is called, /// must be called if the system isn't being shut down. /// private void ClearPvsData() { _leaveTask?.WaitOne(); _leaveTask = null; _deletionTask?.WaitOne(); _deletionTask = null; _incomingReturns.Clear(); _pendingReturns.Clear(); _deletionJob.ToClear.Clear(); _assignedEnts.Clear(); // Remove all pointers stored in any player's PVS send-histories. Required to avoid accidentally writing to // invalid bits of memory while processing late game-state acks. This also forces all players to receive a full // game state, in lieu of sending the required PVS leave messages. foreach (var session in PlayerData.Values) { session.DataMemory.Clear(); ForceFullState(session); } _metadataMemory.Clear(); } /// /// Re-initialize the memory in after it was fully cleared on reset. /// private void ResetDataMemory() { _dataFreeListHead = PvsIndex.Invalid; InitializeFreeList(_metadataMemory.GetSpan(), 0, ref _dataFreeListHead); } /// /// Shrink (and all sessions) back down to initial entity size after clear. /// private void ShrinkDataMemory() { DebugTools.Assert(EntityManager.EntityCount == 0); var initialCount = _configManager.GetCVar(CVars.NetPvsEntityInitial); if (initialCount != _metadataMemory.CurrentSize) { Log.Debug($"Shrinking PVS data from {_metadataMemory.CurrentSize} -> {initialCount} entities"); _metadataMemory.Shrink(initialCount); foreach (var player in PlayerData.Values) { player.DataMemory.Shrink(initialCount); } } } /// /// This method shuffles the entity free list. This is used to avoid accidental / unrealistic cache locality /// in benchmarks. /// internal void ShufflePointers(int seed) { throw new NotImplementedException(); /*List ptrs = new(_pointerPool); _pointerPool.Clear(); var rng = new Random(seed); var n = ptrs.Count; while (n > 0) { var k = rng.Next(n); _pointerPool.Push(ptrs[k]); ptrs[k] = ptrs[^1]; ptrs.RemoveAt(--n); }*/ } /// /// Clear all of this sessions' PvsData for all entities. This effectively means that PVS will act as if the player /// had never been sent information about any entity. Used when returning the player's index offset to the pool. /// private void ClearPlayerPvsData(PvsSession session) { session.DataMemory.Clear(); } /// /// Clear all of this entity' PvsData entries. This effectively means that PVS will act as if no player /// had never been sent information about this entity. Used when returning the entity's index back to the free list. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ClearEntityPvsData(PvsIndex index) { foreach (var playerData in PlayerData.Values) { ref var entry = ref playerData.DataMemory.GetRef(index.Index); entry = default; } } /// /// Get the NetEntity associated with a given . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private NetEntity IndexToNetEntity(PvsIndex index) { DebugTools.Assert(_assignedEnts.Contains(index)); return _metadataMemory.GetRef(index.Index).NetEntity; } /// /// Create a new suitable for assigning to a new . /// private ResizableMemoryRegion CreateSessionDataMemory() { return new ResizableMemoryRegion(_metadataMemory.MaxSize, _metadataMemory.CurrentSize); } private static void FreeSessionDataMemory(PvsSession session) { session.DataMemory.Dispose(); } private void OnEntityAdded(Entity entity) { AssignEntityPointer(entity.Comp); } /// /// Retrieve a free entity index and assign it to an entity. /// private void AssignEntityPointer(MetaDataComponent meta) { DebugTools.Assert(meta.PvsData == PvsIndex.Invalid); if (_dataFreeListHead == PvsIndex.Invalid) { ExpandEntityCapacity(); DebugTools.Assert(_dataFreeListHead != PvsIndex.Invalid); } var index = _dataFreeListHead; DebugTools.Assert(_assignedEnts.Add(index)); ref var metadata = ref _metadataMemory.GetRef(index.Index); ref var freeLink = ref Unsafe.As(ref metadata); _dataFreeListHead = freeLink.NextFree; DebugTools.AssertNotEqual(meta.NetEntity, NetEntity.Invalid); meta.PvsData = index; metadata.NetEntity = meta.NetEntity; metadata.LastModifiedTick = meta.LastModifiedTick; metadata.VisMask = meta.VisibilityMask; metadata.LifeStage = meta.EntityLifeStage; #if DEBUG metadata.Marker = uint.MaxValue; #endif } /// /// Return an entity's index in the data array back to the free list of available indices. /// private void OnEntityDeleted(Entity entity) { var ptr = entity.Comp.PvsData; entity.Comp.PvsData = PvsIndex.Invalid; if (ptr == PvsIndex.Invalid) return; _incomingReturns.Add(ptr); } /// /// Immediately return all data indexes back to the pool after flushing all entities. /// private void AfterEntityFlush() { if (EntityManager.EntityCount > 0) throw new Exception("Cannot reset PVS data without first deleting all entities."); ClearPvsData(); ShrinkDataMemory(); ResetDataMemory(); } /// /// This update method periodically returns entity indices back to the pool, once we are sure no old /// game state acks will use indices to that entity. /// private void ProcessDeletions() { var curTick = _gameTiming.CurTick; if (curTick < _lastReturn + (uint)ForceAckThreshold + 1) return; if (curTick < _lastReturn) throw new InvalidOperationException($"Time travel is not supported"); _leaveTask?.WaitOne(); _leaveTask = null; _deletionTask?.WaitOne(); _deletionTask = null; _lastReturn = curTick; foreach (var index in CollectionsMarshal.AsSpan(_deletionJob.ToClear)) { ReturnEntity(index); } _deletionJob.ToClear.Clear(); // Cycle lists. (_deletionJob.ToClear, _pendingReturns, _incomingReturns) = (_pendingReturns, _incomingReturns, _deletionJob.ToClear); if (_deletionJob.ToClear.Count == 0) return; #if DEBUG foreach (var index in CollectionsMarshal.AsSpan(_deletionJob.ToClear)) { DebugTools.Assert(_assignedEnts.Remove(index)); } #endif if (_deletionJob.ToClear.Count > 16) { _deletionTask = _parallelManager.Process(_deletionJob, _deletionJob.Count); return; } foreach (var index in CollectionsMarshal.AsSpan(_deletionJob.ToClear)) { ClearEntityPvsData(index); } } private void ReturnEntity(PvsIndex index) { DebugTools.Assert(!_assignedEnts.Contains(index)); ref var freeLink = ref _metadataMemory.GetRef(index.Index); freeLink.NextFree = _dataFreeListHead; _dataFreeListHead = index; } private record struct PvsDeletionsJob(PvsSystem _pvs) : IParallelRobustJob { public int BatchSize => 8; private PvsSystem _pvs = _pvs; public List ToClear = new(); public int Count => ToClear.Count; public void Execute(int index) { _pvs.ClearEntityPvsData(ToClear[index]); } } }