refactor(app): key live projections by runtime identity

Move materialized live-object sidecars and presentation worksets to exact RuntimeEntityKey ownership. Runtime remains the only GUID/incarnation/local-ID authority while hydration, animation, effects, lights, equipped children, renderer resources, visibility, liveness, and teardown resolve exact projection identities. Preserve synchronous callbacks, local-ID allocation order, and current rendering behavior.
This commit is contained in:
Erik 2026-07-25 21:50:58 +02:00
parent e18df84437
commit 420e5eea70
73 changed files with 2939 additions and 1715 deletions

View file

@ -150,15 +150,14 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
// This index mirrors only loaded live projections and changes at the same
// transaction boundary as _projectionLocations.
private readonly Dictionary<uint, HashSet<WorldEntity>> _loadedLiveByLandblock = new();
// A server GUID normally has exactly one spatial projection. Keep that
// allocation-free common case directly in the primary map; the secondary
// list exists only for the rare overlap during GUID reuse/re-entrant
// lifetime transitions.
private readonly Dictionary<uint, WorldEntity> _primaryProjectionByGuid = new();
private readonly Dictionary<uint, List<WorldEntity>> _additionalProjectionsByGuid = new();
// Runtime-issued local IDs stay unique for the complete materialized
// lifetime, including retryable teardown. Spatial storage therefore never
// groups graphical owners by reusable server GUID.
private readonly Dictionary<uint, WorldEntity> _liveProjectionByLocalId = new();
private readonly Dictionary<uint, int> _visibleLiveProjectionCounts = new();
private readonly Dictionary<uint, bool> _visibilityBeforeMutation = new();
private readonly Queue<(uint ServerGuid, bool Visible)> _visibilityTransitions = new();
private readonly Queue<(uint LocalEntityId, bool Visible)> _visibilityTransitions = new();
private readonly List<WorldEntity> _guidRemovalScratch = new();
private bool _dispatchingVisibilityTransitions;
private int _mutationDepth;
private long _visibilityCommitCount;
@ -180,18 +179,18 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
_loaded.ContainsKey(landblockId)
&& (_wbSpawnAdapter?.IsLandblockRenderReady(landblockId) ?? true);
/// <summary>
/// True when at least one projection for the server object belongs to a
/// loaded spatial bucket. This deliberately ignores the world-generation
/// True when the exact Runtime-issued local projection belongs to a loaded
/// spatial bucket. This deliberately ignores the world-generation
/// presentation gate: destination objects must acquire their render
/// resources while portal/login reveal keeps normal world consumers closed.
/// </summary>
public bool IsLiveEntityProjectionResident(uint serverGuid) =>
serverGuid != 0
&& _visibleLiveProjectionCounts.ContainsKey(serverGuid);
public bool IsLiveEntityProjectionResident(uint localEntityId) =>
localEntityId != 0
&& _visibleLiveProjectionCounts.ContainsKey(localEntityId);
public bool IsLiveEntityVisible(uint serverGuid) =>
public bool IsLiveEntityVisible(uint localEntityId) =>
_availability.IsWorldAvailable
&& IsLiveEntityProjectionResident(serverGuid);
&& IsLiveEntityProjectionResident(localEntityId);
/// <summary>
/// Try to grab the loaded record for a landblock — useful for callers
@ -583,11 +582,11 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
if (--_mutationDepth != 0)
return;
foreach ((uint guid, bool wasVisible) in _visibilityBeforeMutation)
foreach ((uint localEntityId, bool wasVisible) in _visibilityBeforeMutation)
{
bool isVisible = _visibleLiveProjectionCounts.ContainsKey(guid);
bool isVisible = _visibleLiveProjectionCounts.ContainsKey(localEntityId);
if (wasVisible != isVisible)
_visibilityTransitions.Enqueue((guid, isVisible));
_visibilityTransitions.Enqueue((localEntityId, isVisible));
}
_visibilityBeforeMutation.Clear();
_visibilityCommitCount++;
@ -604,26 +603,27 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
private void AdjustVisibleLiveProjection(WorldEntity entity, int delta)
{
uint guid = entity.ServerGuid;
if (guid == 0 || delta == 0)
uint localEntityId = entity.Id;
if (entity.ServerGuid == 0 || localEntityId == 0 || delta == 0)
return;
_visibleLiveProjectionCounts.TryGetValue(guid, out int priorCount);
if (!_visibilityBeforeMutation.ContainsKey(guid))
_visibilityBeforeMutation.Add(guid, priorCount > 0);
_visibleLiveProjectionCounts.TryGetValue(localEntityId, out int priorCount);
if (!_visibilityBeforeMutation.ContainsKey(localEntityId))
_visibilityBeforeMutation.Add(localEntityId, priorCount > 0);
int nextCount = checked(priorCount + delta);
if (nextCount < 0)
throw new InvalidOperationException(
$"Live projection visibility count underflow for 0x{guid:X8}.");
$"Live projection visibility count underflow for local " +
$"0x{localEntityId:X8} / server 0x{entity.ServerGuid:X8}.");
if (nextCount == 0)
{
_visibleLiveProjectionCounts.Remove(guid);
_visibleLiveProjectionCounts.Remove(localEntityId);
}
else
{
_visibleLiveProjectionCounts[guid] = nextCount;
_visibleLiveProjectionCounts[localEntityId] = nextCount;
}
}
@ -695,16 +695,11 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
if (!isNew)
return;
uint guid = entity.ServerGuid;
if (_primaryProjectionByGuid.TryAdd(guid, entity))
return;
if (!_additionalProjectionsByGuid.TryGetValue(guid, out List<WorldEntity>? projections))
if (!_liveProjectionByLocalId.TryAdd(entity.Id, entity))
{
projections = new List<WorldEntity>(1);
_additionalProjectionsByGuid.Add(guid, projections);
throw new InvalidOperationException(
$"Runtime local projection id 0x{entity.Id:X8} is already spatially owned.");
}
projections.Add(entity);
}
private void RemoveProjectionLocation(WorldEntity entity)
@ -715,51 +710,18 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
if (location.IsLoaded)
RemoveLoadedLiveIndex(location.LandblockId, entity);
uint guid = entity.ServerGuid;
if (!_primaryProjectionByGuid.TryGetValue(guid, out WorldEntity? primary))
throw new InvalidOperationException(
$"Live projection 0x{entity.Id:X8} had no server-guid index entry.");
if (ReferenceEquals(primary, entity))
if (!_liveProjectionByLocalId.Remove(
entity.Id,
out WorldEntity? indexed))
{
if (_additionalProjectionsByGuid.TryGetValue(guid, out List<WorldEntity>? projections)
&& projections.Count > 0)
{
int lastIndex = projections.Count - 1;
_primaryProjectionByGuid[guid] = projections[lastIndex];
projections.RemoveAt(lastIndex);
if (projections.Count == 0)
_additionalProjectionsByGuid.Remove(guid);
}
else
{
_primaryProjectionByGuid.Remove(guid);
}
return;
}
if (!_additionalProjectionsByGuid.TryGetValue(guid, out List<WorldEntity>? additional))
throw new InvalidOperationException(
$"Live projection 0x{entity.Id:X8} was absent from its server-guid index.");
int index = -1;
for (int i = 0; i < additional.Count; i++)
$"Live projection 0x{entity.Id:X8} had no exact local-id index entry.");
}
if (!ReferenceEquals(indexed, entity))
{
if (!ReferenceEquals(additional[i], entity))
continue;
index = i;
break;
}
if (index < 0)
throw new InvalidOperationException(
$"Live projection 0x{entity.Id:X8} was absent from its server-guid index.");
int lastAdditionalIndex = additional.Count - 1;
if (index != lastAdditionalIndex)
additional[index] = additional[lastAdditionalIndex];
additional.RemoveAt(lastAdditionalIndex);
if (additional.Count == 0)
_additionalProjectionsByGuid.Remove(guid);
$"Runtime local projection id 0x{entity.Id:X8} resolved a different owner.");
}
}
private void AddLoadedLiveIndex(uint landblockId, WorldEntity entity)
@ -1397,10 +1359,13 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
MutationBatch mutation = BeginMutationBatch();
try
{
foreach ((uint guid, int count) in _visibleLiveProjectionCounts)
foreach ((uint localEntityId, int count) in _visibleLiveProjectionCounts)
{
if (count > 0 && !_visibilityBeforeMutation.ContainsKey(guid))
_visibilityBeforeMutation.Add(guid, true);
if (count > 0
&& !_visibilityBeforeMutation.ContainsKey(localEntityId))
{
_visibilityBeforeMutation.Add(localEntityId, true);
}
}
_visibleLiveProjectionCounts.Clear();
@ -1410,8 +1375,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
_flatEntityIndices.Clear();
_projectionLocations.Clear();
_loadedLiveByLandblock.Clear();
_primaryProjectionByGuid.Clear();
_additionalProjectionsByGuid.Clear();
_liveProjectionByLocalId.Clear();
_loaded.Clear();
_renderTraversalLandblockSlots.Clear();
@ -1525,12 +1489,18 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
using MutationBatch mutation = BeginMutationBatch();
// Canonical live projections are indexed by server GUID and entity
// identity, so delete/withdraw never scans every resident landblock.
while (_primaryProjectionByGuid.TryGetValue(serverGuid, out WorldEntity? projection))
// GUID-only removal is reserved for session cleanup paths that do not
// have an exact projection owner. Ordinary lifecycle operations use
// the exact-entity overload below.
_guidRemovalScratch.Clear();
foreach (WorldEntity projection in _projectionLocations.Keys)
{
RemoveEntityFromAllBuckets(projection);
if (projection.ServerGuid == serverGuid)
_guidRemovalScratch.Add(projection);
}
for (int i = 0; i < _guidRemovalScratch.Count; i++)
RemoveEntityFromAllBuckets(_guidRemovalScratch[i]);
_guidRemovalScratch.Clear();
// A persistent projection may have been rescued by RemoveLandblock
// and not yet drained by the next GameWindow frame. Rebucketing or
@ -1582,8 +1552,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
_visibilityBeforeMutation.Clear();
_projectionLocations.Clear();
_loadedLiveByLandblock.Clear();
_primaryProjectionByGuid.Clear();
_additionalProjectionsByGuid.Clear();
_liveProjectionByLocalId.Clear();
}
/// <summary>
@ -1905,7 +1874,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
try
{
((Action<uint, bool>)subscribers[i])(
transition.ServerGuid,
transition.LocalEntityId,
transition.Visible);
}
catch (Exception error)