fix(rendering): bound portal resource lifetime

Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-18 21:35:16 +02:00
parent 3971997689
commit 749e8ceeb1
225 changed files with 29107 additions and 3914 deletions

View file

@ -136,6 +136,11 @@ public sealed class LiveEntityRecord
internal ulong ProjectionMutationVersion { get; set; }
public bool WorldSpawnPublished { get; internal set; }
public LiveEntityProjectionKind ProjectionKind { get; internal set; }
internal bool RuntimeComponentsTeardownCompleted { get; set; }
internal LiveEntityTeardownPlan? RuntimeComponentTeardownPlan { get; set; }
internal bool SpatialProjectionTeardownCompleted { get; set; }
internal bool TeardownInProgress { get; set; }
internal bool DeleteAcceptedForTeardown { get; set; }
internal bool TryDequeueStateTransition(out RetailPhysicsStateTransition transition) =>
_pendingStateTransitions.TryDequeue(out transition);
@ -217,12 +222,27 @@ public sealed class LiveEntityRuntime
private readonly Action<LiveEntityRecord>? _tearDownRuntimeComponents;
private readonly InboundPhysicsStateController _inbound = new();
private readonly Dictionary<uint, LiveEntityRecord> _recordsByGuid = new();
// Records leave the active gameplay map before arbitrary teardown callbacks
// so a newer GUID generation can be accepted re-entrantly. They remain
// canonical teardown tombstones until every resource owner confirms
// unregister; failures are therefore retryable by Delete or session Clear.
private readonly Dictionary<(uint Guid, ushort Generation), LiveEntityRecord>
_teardownRecords = new();
private readonly Dictionary<uint, WorldEntity> _materializedWorldEntitiesByGuid = new();
private readonly Dictionary<uint, WorldEntity> _visibleWorldEntitiesByGuid = new();
private readonly Dictionary<uint, uint> _guidByLocalId = new();
private readonly Dictionary<uint, ILiveEntityAnimationRuntime> _animationsByLocalId = new();
private readonly Dictionary<uint, ILiveEntityRemoteMotionRuntime> _remoteMotionByGuid = new();
// Logical components survive retail's 25-second leave-visibility lifetime.
// Frame loops must not scan those retained owners. These component-specific
// indexes contain only records with a loaded, non-cellless spatial
// projection. Hidden and Frozen remain members: their state controls what
// each retail update path advances after the O(active) lookup.
private readonly Dictionary<uint, ILiveEntityAnimationRuntime> _spatialAnimationsByLocalId = new();
private readonly Dictionary<uint, ILiveEntityRemoteMotionRuntime> _spatialRemoteMotionByGuid = new();
private readonly Dictionary<uint, ILiveEntityProjectileRuntime> _spatialProjectilesByGuid = new();
private bool _isClearing;
private bool _sessionClearPendingFinalization;
private bool _isRegisteringResources;
private int _logicalTeardownDepth;
private ulong _sessionLifetimeVersion;
@ -248,6 +268,7 @@ public sealed class LiveEntityRuntime
}
public int Count => _recordsByGuid.Count;
public int PendingTeardownCount => _teardownRecords.Count;
public int MaterializedCount => _guidByLocalId.Count;
public IReadOnlyCollection<LiveEntityRecord> Records => _recordsByGuid.Values;
/// <summary>
@ -270,6 +291,12 @@ public sealed class LiveEntityRuntime
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _inbound.Snapshots;
public IReadOnlyDictionary<uint, ILiveEntityAnimationRuntime> AnimationRuntimes => _animationsByLocalId;
public IReadOnlyDictionary<uint, ILiveEntityRemoteMotionRuntime> RemoteMotionRuntimes => _remoteMotionByGuid;
public IReadOnlyDictionary<uint, ILiveEntityAnimationRuntime> SpatialAnimationRuntimes =>
_spatialAnimationsByLocalId;
public IReadOnlyDictionary<uint, ILiveEntityRemoteMotionRuntime> SpatialRemoteMotionRuntimes =>
_spatialRemoteMotionByGuid;
internal IReadOnlyDictionary<uint, ILiveEntityProjectileRuntime> SpatialProjectileRuntimes =>
_spatialProjectilesByGuid;
public ParentAttachmentState ParentAttachments { get; } = new();
/// <summary>
@ -280,10 +307,10 @@ public sealed class LiveEntityRuntime
public LiveEntityRegistrationResult RegisterLiveEntity(WorldSession.EntitySpawn incoming)
{
if (_isClearing || _isRegisteringResources)
if (_isClearing || _sessionClearPendingFinalization || _isRegisteringResources)
{
throw new InvalidOperationException(
_isClearing
_isClearing || _sessionClearPendingFinalization
? "A live entity cannot register while the session lifetime is clearing."
: "A live entity cannot register from inside atomic resource registration.");
}
@ -300,9 +327,24 @@ public sealed class LiveEntityRuntime
{
retained.Snapshot = result.Snapshot;
retained.RefreshDerivedState();
RefreshSpatialRuntimeIndexes(retained);
return new LiveEntityRegistrationResult(result, retained, false, false);
}
// A failed materialization rollback retains this exact incarnation
// as a teardown tombstone. Do not create a second active record
// with the same (GUID, INSTANCE_TS) while its partial resources are
// still converging; a later retransmit can recover after teardown.
if (_teardownRecords.ContainsKey(
(incoming.Guid, result.Snapshot.InstanceSequence)))
{
return new LiveEntityRegistrationResult(
result,
null,
false,
false);
}
// Defensive repair for a caller that cleared only the logical map.
// Normal session teardown clears both owners together.
var recovered = new LiveEntityRecord(result.Snapshot);
@ -317,12 +359,14 @@ public sealed class LiveEntityRuntime
Exception? tearDownFailure = null;
if (old is not null)
{
RetainTeardownRecord(old);
_logicalTeardownDepth++;
try
{
try
{
TearDownRecord(old);
ReleaseTeardownRecord(old);
}
catch (Exception error)
{
@ -379,7 +423,10 @@ public sealed class LiveEntityRuntime
LiveEntityProjectionKind projectionKind = LiveEntityProjectionKind.World)
{
ArgumentNullException.ThrowIfNull(factory);
if (_isClearing || _logicalTeardownDepth != 0 || _isRegisteringResources)
if (_isClearing
|| _sessionClearPendingFinalization
|| _logicalTeardownDepth != 0
|| _isRegisteringResources)
{
throw new InvalidOperationException(
"A live entity cannot materialize inside an active logical-lifetime transition.");
@ -404,34 +451,47 @@ public sealed class LiveEntityRuntime
_isRegisteringResources = true;
try
{
// Registration may span several App owners. Until the whole
// edge returns, Unregister remains a required rollback
// obligation; a failed rollback retains this identity as a
// teardown tombstone instead of making partial owners
// unreachable.
record.ResourcesRegistered = true;
try
{
_resources.Register(entity);
record.ResourcesRegistered = true;
}
catch (Exception registrationError)
{
// Registration is an atomic logical-lifetime boundary.
// Give composite owners a symmetric rollback opportunity,
// then remove identity even if cleanup itself fails.
Exception? rollbackError = null;
try
{
_resources.Unregister(entity);
record.ResourcesRegistered = false;
}
catch (Exception rollbackError)
catch (Exception error)
{
throw new AggregateException(
"Live entity resource registration and rollback both failed.",
registrationError,
rollbackError);
rollbackError = error;
}
finally
if (rollbackError is null)
{
_guidByLocalId.Remove(localId);
record.WorldEntity = null;
record.ResourcesRegistered = false;
throw;
}
throw;
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? active)
&& ReferenceEquals(active, record))
{
_recordsByGuid.Remove(serverGuid);
}
RetainTeardownRecord(record);
throw new AggregateException(
"Live entity resource registration and rollback both failed; " +
"the partial owner was retained for teardown retry.",
registrationError,
rollbackError);
}
}
finally
@ -510,6 +570,7 @@ public sealed class LiveEntityRuntime
record.CanonicalLandblockId = spatialCellOrLandblockId == 0
? 0u
: (spatialCellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
RefreshSpatialRuntimeIndexes(record);
Exception? runtimeNotificationFailure = null;
if (!wasProjected || wasVisible != visible)
{
@ -570,6 +631,7 @@ public sealed class LiveEntityRuntime
_visibleWorldEntitiesByGuid.Remove(serverGuid);
record.IsSpatiallyProjected = false;
record.IsSpatiallyVisible = false;
RefreshSpatialRuntimeIndexes(record);
Exception? runtimeNotificationFailure = null;
if (spatialEdgeWasDeferred)
{
@ -616,30 +678,53 @@ public sealed class LiveEntityRuntime
throw new InvalidOperationException(
"A live entity cannot unregister from inside atomic resource registration.");
}
if (!_inbound.TryDelete(delete, isLocalPlayer))
return false;
AdvanceLifetimeMutation(delete.Guid);
var teardownKey = (delete.Guid, delete.InstanceSequence);
_teardownRecords.TryGetValue(teardownKey, out LiveEntityRecord? record);
bool retryingAcceptedDelete = record is not null
&& (record.DeleteAcceptedForTeardown
|| _isClearing
|| _sessionClearPendingFinalization);
if (!retryingAcceptedDelete)
{
if (!_inbound.TryDelete(delete, isLocalPlayer))
return false;
AdvanceLifetimeMutation(delete.Guid);
ParentAttachments.DeleteGeneration(delete.Guid, delete.InstanceSequence);
ParentAttachments.DeleteGeneration(delete.Guid, delete.InstanceSequence);
// Remove the accepted incarnation from canonical identity before
// invoking arbitrary callbacks. A callback may synchronously create a
// newer generation with the same server GUID; materialization waits
// until this teardown completes, and cleanup remains scoped to this
// captured record.
_recordsByGuid.Remove(delete.Guid, out LiveEntityRecord? record);
// End active gameplay identity before arbitrary callbacks so a
// newer generation can be accepted re-entrantly, but retain the
// exact incarnation as a teardown tombstone until unregister has
// succeeded. A throwing resource callback can then be retried by
// the same accepted Delete instead of orphaning its owner.
LiveEntityRecord? retainedRegistrationFailure = record;
_recordsByGuid.Remove(delete.Guid, out LiveEntityRecord? activeRecord);
if (activeRecord is not null)
{
record = activeRecord;
RetainTeardownRecord(record);
}
else
{
record = retainedRegistrationFailure;
}
if (record is not null)
record.DeleteAcceptedForTeardown = true;
}
List<Exception>? failures = null;
_logicalTeardownDepth++;
try
{
try
if (!retryingAcceptedDelete)
{
beforeTeardown?.Invoke();
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
try
{
beforeTeardown?.Invoke();
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
}
}
if (record is not null)
@ -647,6 +732,7 @@ public sealed class LiveEntityRuntime
try
{
TearDownRecord(record);
ReleaseTeardownRecord(record);
}
catch (Exception error)
{
@ -662,8 +748,10 @@ public sealed class LiveEntityRuntime
try
{
// Persistence is GUID-scoped. End it only when the accepted
// delete left no replacement incarnation behind.
if (!_recordsByGuid.ContainsKey(delete.Guid))
// delete left no replacement incarnation or unfinished teardown
// behind.
if (!_recordsByGuid.ContainsKey(delete.Guid)
&& !HasPendingTeardown(delete.Guid))
{
_spatial.ForgetLiveEntity(delete.Guid);
}
@ -739,6 +827,7 @@ public sealed class LiveEntityRuntime
_animationsByLocalId.Remove(entity.Id);
record.AnimationRuntime = runtime;
_animationsByLocalId[entity.Id] = runtime;
RefreshSpatialRuntimeIndexes(record);
}
public bool TryGetAnimationRuntime(uint localEntityId, out ILiveEntityAnimationRuntime runtime) =>
@ -752,6 +841,7 @@ public sealed class LiveEntityRuntime
if (record.LocalEntityId is { } localId)
_animationsByLocalId.Remove(localId);
record.AnimationRuntime = null;
RefreshSpatialRuntimeIndexes(record);
return true;
}
@ -803,6 +893,7 @@ public sealed class LiveEntityRuntime
});
}
_remoteMotionByGuid[serverGuid] = runtime;
RefreshSpatialRuntimeIndexes(record);
}
public bool TryGetRemoteMotionRuntime(uint serverGuid, out ILiveEntityRemoteMotionRuntime runtime) =>
@ -815,6 +906,7 @@ public sealed class LiveEntityRuntime
return false;
_remoteMotionByGuid.Remove(serverGuid);
record.RemoteMotionRuntime = null;
RefreshSpatialRuntimeIndexes(record);
return true;
}
@ -838,6 +930,7 @@ public sealed class LiveEntityRuntime
record.ProjectileRuntime = runtime;
record.PhysicsBody = candidateBody;
candidateBody.State = record.FinalPhysicsState;
RefreshSpatialRuntimeIndexes(record);
}
public bool TryGetProjectileRuntime(
@ -862,6 +955,7 @@ public sealed class LiveEntityRuntime
return false;
record.ProjectileRuntime = null;
RefreshSpatialRuntimeIndexes(record);
return true;
}
@ -1049,6 +1143,67 @@ public sealed class LiveEntityRuntime
&& record.FullCellId != 0
&& (record.FinalPhysicsState & PhysicsStateFlags.Frozen) == 0;
/// <summary>
/// Copies the currently spatial remote-motion owners into caller-owned
/// reusable storage. The record reference is the incarnation token: a
/// delete and GUID reuse cannot make an older snapshot address the newer
/// object.
/// </summary>
internal void CopySpatialRemoteMotionRecordsTo(List<LiveEntityRecord> destination)
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach ((uint serverGuid, ILiveEntityRemoteMotionRuntime runtime) in _spatialRemoteMotionByGuid)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& ReferenceEquals(record.RemoteMotionRuntime, runtime)
&& HasSpatialRuntimeProjection(record))
{
destination.Add(record);
}
}
}
internal bool IsCurrentSpatialRemoteMotion(
LiveEntityRecord record,
ILiveEntityRemoteMotionRuntime runtime) =>
IsCurrentRecord(record)
&& ReferenceEquals(record.RemoteMotionRuntime, runtime)
&& _spatialRemoteMotionByGuid.TryGetValue(record.ServerGuid, out var indexed)
&& ReferenceEquals(indexed, runtime)
&& HasSpatialRuntimeProjection(record);
/// <summary>
/// Copies only projectile owners that currently have a loaded world-cell
/// projection. Pending projectiles retain their logical runtime but impose
/// no per-frame scan cost.
/// </summary>
internal void CopySpatialProjectileRecordsTo(List<LiveEntityRecord> destination)
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach ((uint serverGuid, ILiveEntityProjectileRuntime runtime) in _spatialProjectilesByGuid)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& ReferenceEquals(record.ProjectileRuntime, runtime)
&& HasSpatialRuntimeProjection(record))
{
destination.Add(record);
}
}
}
internal bool IsCurrentSpatialProjectile(
LiveEntityRecord record,
ILiveEntityProjectileRuntime runtime) =>
IsCurrentRecord(record)
&& ReferenceEquals(record.ProjectileRuntime, runtime)
&& _spatialProjectilesByGuid.TryGetValue(record.ServerGuid, out var indexed)
&& ReferenceEquals(indexed, runtime)
&& HasSpatialRuntimeProjection(record);
public bool IsHidden(uint serverGuid) =>
_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
@ -1081,6 +1236,7 @@ public sealed class LiveEntityRuntime
}
_isClearing = true;
_sessionClearPendingFinalization = true;
_sessionLifetimeVersion++;
_lifetimeMutationVersionByGuid.Clear();
List<Exception>? failures = null;
@ -1088,17 +1244,28 @@ public sealed class LiveEntityRuntime
{
foreach (LiveEntityRecord record in _recordsByGuid.Values.ToArray())
{
// Remove canonical identity first. Registration is rejected
// during this terminal clear, so callbacks cannot leak an
// owner that is absent from the teardown snapshot.
if (!_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
|| !ReferenceEquals(current, record))
continue;
_recordsByGuid.Remove(record.ServerGuid);
RetainTeardownRecord(record);
}
ParentAttachments.Clear();
_inbound.Clear();
foreach (LiveEntityRecord record in _teardownRecords.Values.ToArray())
{
// A re-entrant Clear can observe the tombstone currently being
// unregistered by its caller. Leave it queued; the outer
// teardown's ReleaseTeardownRecord finalizes the session once
// it unwinds successfully.
if (record.TeardownInProgress)
continue;
try
{
TearDownRecord(record);
ReleaseTeardownRecord(record);
}
catch (Exception error)
{
@ -1106,15 +1273,7 @@ public sealed class LiveEntityRuntime
}
}
_recordsByGuid.Clear();
_materializedWorldEntitiesByGuid.Clear();
_visibleWorldEntitiesByGuid.Clear();
_guidByLocalId.Clear();
_animationsByLocalId.Clear();
_remoteMotionByGuid.Clear();
ParentAttachments.Clear();
_inbound.Clear();
_spatial.ClearLiveEntityLifetimeState();
CompleteSessionClearIfConverged();
}
finally
{
@ -1125,6 +1284,57 @@ public sealed class LiveEntityRuntime
throw new AggregateException("One or more live entities failed session teardown.", failures);
}
/// <summary>
/// Drains teardown tombstones whose first unregister attempt failed. This
/// runs on the update thread after the callback stack that deferred the
/// removal has unwound; successful steps are not replayed.
/// </summary>
public int RetryPendingTeardowns()
{
if (_teardownRecords.Count == 0
|| _isClearing
|| _isRegisteringResources
|| _logicalTeardownDepth != 0)
return 0;
int completed = 0;
List<Exception>? failures = null;
foreach (LiveEntityRecord record in _teardownRecords.Values.ToArray())
{
if (record.TeardownInProgress)
continue;
_logicalTeardownDepth++;
try
{
try
{
TearDownRecord(record);
ReleaseTeardownRecord(record);
completed++;
if (!_recordsByGuid.ContainsKey(record.ServerGuid)
&& !HasPendingTeardown(record.ServerGuid)
&& !_inbound.TryGetSnapshot(record.ServerGuid, out _))
{
_spatial.ForgetLiveEntity(record.ServerGuid);
}
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
}
}
finally
{
_logicalTeardownDepth--;
}
}
if (failures is not null)
throw new AggregateException("One or more pending live-entity teardowns failed again.", failures);
return completed;
}
private void RefreshRecord(
uint guid,
WorldSession.EntitySpawn accepted,
@ -1185,6 +1395,84 @@ public sealed class LiveEntityRuntime
return;
record.FullCellId = 0;
record.CanonicalLandblockId = 0;
RefreshSpatialRuntimeIndexes(record);
}
private bool IsCurrentRecord(LiveEntityRecord record) =>
_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, record);
private static bool HasSpatialRuntimeProjection(LiveEntityRecord record) =>
record.WorldEntity is not null
&& record.IsSpatiallyProjected
&& record.IsSpatiallyVisible
&& record.FullCellId != 0;
private void RefreshSpatialRuntimeIndexes(LiveEntityRecord record)
{
bool current = IsCurrentRecord(record);
bool spatial = current && HasSpatialRuntimeProjection(record);
if (record.WorldEntity is { } entity)
{
if (spatial && record.AnimationRuntime is { } animation)
{
_spatialAnimationsByLocalId[entity.Id] = animation;
}
else if (current
|| (record.AnimationRuntime is { } retainedAnimation
&& _spatialAnimationsByLocalId.TryGetValue(entity.Id, out var indexedAnimation)
&& ReferenceEquals(indexedAnimation, retainedAnimation)))
{
_spatialAnimationsByLocalId.Remove(entity.Id);
}
}
if (spatial && record.RemoteMotionRuntime is { } remote)
{
_spatialRemoteMotionByGuid[record.ServerGuid] = remote;
}
else if (current
|| (record.RemoteMotionRuntime is { } retainedRemote
&& _spatialRemoteMotionByGuid.TryGetValue(record.ServerGuid, out var indexedRemote)
&& ReferenceEquals(indexedRemote, retainedRemote)))
{
_spatialRemoteMotionByGuid.Remove(record.ServerGuid);
}
if (spatial && record.ProjectileRuntime is { } projectile)
{
_spatialProjectilesByGuid[record.ServerGuid] = projectile;
}
else if (current
|| (record.ProjectileRuntime is { } retainedProjectile
&& _spatialProjectilesByGuid.TryGetValue(record.ServerGuid, out var indexedProjectile)
&& ReferenceEquals(indexedProjectile, retainedProjectile)))
{
_spatialProjectilesByGuid.Remove(record.ServerGuid);
}
}
private void RemoveSpatialRuntimeIndexes(LiveEntityRecord record)
{
if (record.WorldEntity is { } entity
&& _spatialAnimationsByLocalId.TryGetValue(entity.Id, out var indexedAnimation)
&& ReferenceEquals(indexedAnimation, record.AnimationRuntime))
{
_spatialAnimationsByLocalId.Remove(entity.Id);
}
if (_spatialRemoteMotionByGuid.TryGetValue(record.ServerGuid, out var indexedRemote)
&& ReferenceEquals(indexedRemote, record.RemoteMotionRuntime))
{
_spatialRemoteMotionByGuid.Remove(record.ServerGuid);
}
if (_spatialProjectilesByGuid.TryGetValue(record.ServerGuid, out var indexedProjectile)
&& ReferenceEquals(indexedProjectile, record.ProjectileRuntime))
{
_spatialProjectilesByGuid.Remove(record.ServerGuid);
}
}
private uint ReserveLocalEntityId()
@ -1219,6 +1507,7 @@ public sealed class LiveEntityRuntime
bool wasVisible = record.IsSpatiallyVisible;
record.IsSpatiallyVisible = visible;
RefreshSpatialRuntimeIndexes(record);
RefreshPresentation(record);
if (_rebucketingGuid != serverGuid && wasVisible != visible)
PublishProjectionVisibilityChanged(record, visible);
@ -1267,67 +1556,170 @@ public sealed class LiveEntityRuntime
_visibleWorldEntitiesByGuid.Remove(record.ServerGuid);
}
private static (uint Guid, ushort Generation) TeardownKey(LiveEntityRecord record) =>
(record.ServerGuid, record.Generation);
private void RetainTeardownRecord(LiveEntityRecord record)
{
var key = TeardownKey(record);
if (_teardownRecords.TryGetValue(key, out LiveEntityRecord? retained)
&& !ReferenceEquals(retained, record))
{
throw new InvalidOperationException(
$"Live entity teardown tombstone collision for 0x{record.ServerGuid:X8} generation {record.Generation}.");
}
_teardownRecords[key] = record;
if (record.WorldEntity is { } entity
&& _visibleWorldEntitiesByGuid.TryGetValue(
record.ServerGuid,
out WorldEntity? visible)
&& ReferenceEquals(visible, entity))
{
_visibleWorldEntitiesByGuid.Remove(record.ServerGuid);
}
}
private void ReleaseTeardownRecord(LiveEntityRecord record)
{
var key = TeardownKey(record);
if (_teardownRecords.TryGetValue(key, out LiveEntityRecord? retained)
&& ReferenceEquals(retained, record))
{
_teardownRecords.Remove(key);
}
CompleteSessionClearIfConverged();
}
private bool HasPendingTeardown(uint serverGuid)
{
foreach ((uint Guid, ushort Generation) key in _teardownRecords.Keys)
{
if (key.Guid == serverGuid)
return true;
}
return false;
}
private void CompleteSessionClearIfConverged()
{
if (!_sessionClearPendingFinalization
|| _recordsByGuid.Count != 0
|| _teardownRecords.Count != 0)
{
return;
}
_materializedWorldEntitiesByGuid.Clear();
_visibleWorldEntitiesByGuid.Clear();
_guidByLocalId.Clear();
_animationsByLocalId.Clear();
_remoteMotionByGuid.Clear();
_spatialAnimationsByLocalId.Clear();
_spatialRemoteMotionByGuid.Clear();
_spatialProjectilesByGuid.Clear();
ParentAttachments.Clear();
_inbound.Clear();
_spatial.ClearLiveEntityLifetimeState();
_sessionClearPendingFinalization = false;
}
private void TearDownRecord(LiveEntityRecord record)
{
if (record.TeardownInProgress)
throw new InvalidOperationException(
$"Live entity 0x{record.ServerGuid:X8} teardown is already in progress.");
record.TeardownInProgress = true;
List<Exception>? failures = null;
void RunCleanup(Action cleanup)
bool TryCleanup(Action cleanup)
{
try
{
cleanup();
return true;
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
return false;
}
}
if (_tearDownRuntimeComponents is not null)
RunCleanup(() => _tearDownRuntimeComponents(record));
if (record.WorldEntity is { } entity)
try
{
RunCleanup(() => _spatial.RemoveLiveEntityProjection(entity));
if (record.ResourcesRegistered)
RunCleanup(() => _resources.Unregister(entity));
_guidByLocalId.Remove(entity.Id);
if (_materializedWorldEntitiesByGuid.TryGetValue(
record.ServerGuid,
out WorldEntity? materialized)
&& ReferenceEquals(materialized, entity))
{
_materializedWorldEntitiesByGuid.Remove(record.ServerGuid);
}
if (_visibleWorldEntitiesByGuid.TryGetValue(
record.ServerGuid,
out WorldEntity? visible)
&& ReferenceEquals(visible, entity))
{
_visibleWorldEntitiesByGuid.Remove(record.ServerGuid);
}
_animationsByLocalId.Remove(entity.Id);
}
// Remove frame-workset membership before arbitrary callbacks. The
// logical component dictionaries and WorldEntity remain intact as
// a retryable tombstone until every external cleanup acknowledges
// success.
RemoveSpatialRuntimeIndexes(record);
if (_remoteMotionByGuid.TryGetValue(record.ServerGuid, out var remote)
&& ReferenceEquals(remote, record.RemoteMotionRuntime))
if (!record.RuntimeComponentsTeardownCompleted)
{
record.RuntimeComponentsTeardownCompleted =
_tearDownRuntimeComponents is null
|| TryCleanup(() => _tearDownRuntimeComponents(record));
}
if (record.WorldEntity is { } entity)
{
if (!record.SpatialProjectionTeardownCompleted)
{
record.SpatialProjectionTeardownCompleted =
TryCleanup(() => _spatial.RemoveLiveEntityProjection(entity));
}
if (record.ResourcesRegistered
&& TryCleanup(() => _resources.Unregister(entity)))
{
record.ResourcesRegistered = false;
}
}
if (failures is not null)
{
throw new AggregateException(
$"Live entity 0x{record.ServerGuid:X8} teardown failed.",
failures);
}
if (record.WorldEntity is { } finalizedEntity)
{
_guidByLocalId.Remove(finalizedEntity.Id);
if (_materializedWorldEntitiesByGuid.TryGetValue(
record.ServerGuid,
out WorldEntity? materialized)
&& ReferenceEquals(materialized, finalizedEntity))
{
_materializedWorldEntitiesByGuid.Remove(record.ServerGuid);
}
if (_visibleWorldEntitiesByGuid.TryGetValue(
record.ServerGuid,
out WorldEntity? visible)
&& ReferenceEquals(visible, finalizedEntity))
{
_visibleWorldEntitiesByGuid.Remove(record.ServerGuid);
}
_animationsByLocalId.Remove(finalizedEntity.Id);
}
if (_remoteMotionByGuid.TryGetValue(record.ServerGuid, out var remote)
&& ReferenceEquals(remote, record.RemoteMotionRuntime))
{
_remoteMotionByGuid.Remove(record.ServerGuid);
}
record.AnimationRuntime = null;
record.RemoteMotionRuntime = null;
record.RequiresRemotePlacementRuntime = false;
record.PhysicsBody = null;
record.ProjectileRuntime = null;
record.EffectProfile = null;
record.IsSpatiallyProjected = false;
record.IsSpatiallyVisible = false;
record.WorldSpawnPublished = false;
record.WorldEntity = null;
}
finally
{
_remoteMotionByGuid.Remove(record.ServerGuid);
record.TeardownInProgress = false;
}
record.AnimationRuntime = null;
record.RemoteMotionRuntime = null;
record.RequiresRemotePlacementRuntime = false;
record.PhysicsBody = null;
record.ProjectileRuntime = null;
record.EffectProfile = null;
record.ResourcesRegistered = false;
record.IsSpatiallyProjected = false;
record.IsSpatiallyVisible = false;
record.WorldSpawnPublished = false;
record.WorldEntity = null;
if (failures is not null)
throw new AggregateException(
$"Live entity 0x{record.ServerGuid:X8} teardown failed.",
failures);
}
}