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:
parent
e18df84437
commit
420e5eea70
73 changed files with 2939 additions and 1715 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue