Files
RobustToolbox/Robust.Server/GameStates/PvsSystem.DataStorage.cs
2024-03-17 06:57:13 +11:00

385 lines
13 KiB
C#

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<EntityUid, PvsData>, though it can be less
// memory efficient.
internal sealed partial class PvsSystem
{
// This is used for asserts.
private HashSet<PvsIndex> _assignedEnts = new();
/// <summary>
/// Recently returned indexes from deleted entities. These get moved to <see cref="_pendingReturns"/> before
/// moving back into the free list.
/// </summary>
private List<PvsIndex> _incomingReturns = new();
/// <summary>
/// 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.
/// </summary>
private List<PvsIndex> _pendingReturns = new();
/// <summary>
/// Tick at which the <see cref="_pendingReturns"/> were last processed.
/// </summary>
private GameTick _lastReturn = GameTick.Zero;
/// <summary>
/// Memory region to store <see cref="PvsMetadata"/> instances and the free list.
/// </summary>
/// <remarks>
/// Unused elements form a linked list out of <see cref="PvsMetadataFreeLink"/> elements.
/// </remarks>
private ResizableMemoryRegion<PvsMetadata> _metadataMemory = default!;
/// <summary>
/// The head of the PVS data free list. This is the first element that will be used if a new one is needed.
/// </summary>
/// <remarks>
/// If the value is <see cref="PvsIndex.Invalid"/>,
/// there are no more free elements and the next allocation must expand the memory.
/// </remarks>
private PvsIndex _dataFreeListHead;
private WaitHandle? _deletionTask;
/// <summary>
/// Expand the size of <see cref="_metadataMemory"/> (and all session data stores) one iteration.
/// </summary>
/// <remarks>
/// This ensures that we have at least one free list slot.
/// </remarks>
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<PvsMetadataFreeLink>()[initial..];
InitializeFreeList(newSlots, initial, ref _dataFreeListHead);
}
/// <summary>
/// Initialize <see cref="_metadataMemory"/> and the free list.
/// </summary>
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<PvsMetadata>(maxCount, initialCount);
ResetDataMemory();
}
/// <summary>
/// Initialize a section of the free list.
/// </summary>
/// <param name="memory">The section of the free list to initialize.</param>
/// <param name="baseOffset">What offset in the total PVS data this section starts at.</param>
/// <param name="head">The current head storage of the free list to update.</param>
private static void InitializeFreeList(Span<PvsMetadataFreeLink> 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);
}
/// <summary>
/// Clear all PVS data. After this function is called,
/// <see cref="ResetDataMemory"/> must be called if the system isn't being shut down.
/// </summary>
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();
}
/// <summary>
/// Re-initialize the memory in <see cref="_metadataMemory"/> after it was fully cleared on reset.
/// </summary>
private void ResetDataMemory()
{
_dataFreeListHead = PvsIndex.Invalid;
InitializeFreeList(_metadataMemory.GetSpan<PvsMetadataFreeLink>(), 0, ref _dataFreeListHead);
}
/// <summary>
/// Shrink <see cref="_metadataMemory"/> (and all sessions) back down to initial entity size after clear.
/// </summary>
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);
}
}
}
/// <summary>
/// This method shuffles the entity free list. This is used to avoid accidental / unrealistic cache locality
/// in benchmarks.
/// </summary>
internal void ShufflePointers(int seed)
{
throw new NotImplementedException();
/*List<IntPtr> 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);
}*/
}
/// <summary>
/// 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.
/// </summary>
private void ClearPlayerPvsData(PvsSession session)
{
session.DataMemory.Clear();
}
/// <summary>
/// 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.
/// </summary>
[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;
}
}
/// <summary>
/// Get the NetEntity associated with a given <see cref="PvsIndex"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private NetEntity IndexToNetEntity(PvsIndex index)
{
DebugTools.Assert(_assignedEnts.Contains(index));
return _metadataMemory.GetRef(index.Index).NetEntity;
}
/// <summary>
/// Create a new <see cref="ResizableMemoryRegion{T}"/> suitable for assigning to a new <see cref="PvsSession"/>.
/// </summary>
private ResizableMemoryRegion<PvsData> CreateSessionDataMemory()
{
return new ResizableMemoryRegion<PvsData>(_metadataMemory.MaxSize, _metadataMemory.CurrentSize);
}
private static void FreeSessionDataMemory(PvsSession session)
{
session.DataMemory.Dispose();
}
private void OnEntityAdded(Entity<MetaDataComponent> entity)
{
DebugTools.Assert(entity.Comp.PvsData.Index == default);
AssignEntityPointer(entity.Comp);
}
/// <summary>
/// Retrieve a free entity index and assign it to an entity.
/// </summary>
private void AssignEntityPointer(MetaDataComponent meta)
{
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<PvsMetadata, PvsMetadataFreeLink>(ref metadata);
_dataFreeListHead = freeLink.NextFree;
// TODO: re-introduce this assert.
// DebugTools.AssertEqual(((PvsMetadata*) ptr)->NetEntity, NetEntity.Invalid);
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
}
/// <summary>
/// Return an entity's index in the data array back to the free list of available indices.
/// </summary>
private void OnEntityDeleted(Entity<MetaDataComponent> entity)
{
var ptr = entity.Comp.PvsData;
entity.Comp.PvsData = default;
if (ptr == default)
return;
_incomingReturns.Add(ptr);
}
/// <summary>
/// Immediately return all data indexes back to the pool after flushing all entities.
/// </summary>
private void AfterEntityFlush()
{
DebugTools.Assert(EntityManager.EntityCount == 0);
ClearPvsData();
ShrinkDataMemory();
ResetDataMemory();
}
/// <summary>
/// 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.
/// </summary>
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<PvsMetadataFreeLink>(index.Index);
freeLink.NextFree = _dataFreeListHead;
_dataFreeListHead = index;
}
private record struct PvsDeletionsJob(PvsSystem _pvs) : IParallelRobustJob
{
public int BatchSize => 8;
private PvsSystem _pvs = _pvs;
public List<PvsIndex> ToClear = new();
public int Count => ToClear.Count;
public void Execute(int index)
{
_pvs.ClearEntityPvsData(ToClear[index]);
}
}
}