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:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue