fix(streaming): preserve portal destination ownership

Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
This commit is contained in:
Erik 2026-07-25 08:35:12 +02:00
parent 2c848d4167
commit 823936ec31
57 changed files with 2551 additions and 324 deletions

View file

@ -460,6 +460,14 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
public int PersistentGuidCount => _persistentGuids.Count;
public int PendingVisibilityTransitionCount => _visibilityTransitions.Count;
internal long VisibilityCommitCount => _visibilityCommitCount;
internal int OriginRecenterSpatialOperationCount =>
Math.Max(
1,
_loaded.Count
+ _pendingByLandblock.Count
+ _pendingRenderIdsByLandblock.Count
+ _pendingNearTierLandblocks.Count
+ _projectionLocations.Count);
/// <summary>
/// Groups spatial mutations into one visibility publication transaction.
@ -1257,6 +1265,207 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
detachedEntities);
}
/// <summary>
/// Atomically withdraws the complete old spatial generation before a
/// shared-origin change. The origin is common to every resident transform,
/// so admitting hundreds of independent landblock detaches across frames
/// only prolongs portal transit; it does not expose a useful intermediate
/// state. This operation swaps the spatial indexes as one transaction,
/// retains live logical objects, and returns exact landblock receipts so
/// renderer/physics/script destruction can remain frame-budgeted.
/// </summary>
internal GpuWorldRecenterRetirement DetachAllForOriginRecenter()
{
var ids = new List<uint>(
_loaded.Count
+ _pendingByLandblock.Count
+ _pendingRenderIdsByLandblock.Count);
var seenIds = new HashSet<uint>();
void AddId(uint id)
{
uint canonical = (id & 0xFFFF0000u) | 0xFFFFu;
if (seenIds.Add(canonical))
ids.Add(canonical);
}
// Preserve the accepted renderer traversal order for already-resident
// landblocks, then append presentation-only and pending-only owners in
// their stable insertion order.
for (int i = 0; i < _renderTraversalLandblockSlots.Count; i++)
{
uint id = _renderTraversalLandblockSlots[i];
if (id != 0u)
AddId(id);
}
foreach (uint id in _pendingByLandblock.Keys)
AddId(id);
foreach (uint id in _pendingRenderIdsByLandblock.Keys)
AddId(id);
foreach (uint id in _pendingNearTierLandblocks)
AddId(id);
foreach (uint id in _tierByLandblock.Keys)
AddId(id);
foreach (uint id in _aabbs.Keys)
AddId(id);
var retirements = new List<GpuLandblockRetirement>(ids.Count);
for (int i = 0; i < ids.Count; i++)
{
uint id = ids[i];
IReadOnlyList<WorldEntity> loaded =
_loaded.TryGetValue(id, out LoadedLandblock? landblock)
? landblock.Entities
: Array.Empty<WorldEntity>();
IReadOnlyList<WorldEntity> pending =
_pendingByLandblock.TryGetValue(id, out List<WorldEntity>? bucket)
? bucket
: Array.Empty<WorldEntity>();
IReadOnlyList<WorldEntity> detached = loaded;
if (pending.Count != 0)
{
if (loaded.Count == 0)
{
detached = pending;
}
else
{
var combined = new List<WorldEntity>(
loaded.Count + pending.Count);
var seenEntities = new HashSet<WorldEntity>(
ReferenceEqualityComparer.Instance);
for (int entityIndex = 0;
entityIndex < loaded.Count;
entityIndex++)
{
WorldEntity entity = loaded[entityIndex];
if (seenEntities.Add(entity))
combined.Add(entity);
}
for (int entityIndex = 0;
entityIndex < pending.Count;
entityIndex++)
{
WorldEntity entity = pending[entityIndex];
if (seenEntities.Add(entity))
combined.Add(entity);
}
detached = combined;
}
}
retirements.Add(new GpuLandblockRetirement(
id,
LandblockRetirementKind.Full,
detached));
}
// Capture live projections from their small dedicated index rather
// than walking every DAT-static entity in the 25x25 world window.
var retainedByLandblock =
new Dictionary<uint, List<(WorldEntity Entity, int BucketIndex)>>();
var rescued = new HashSet<WorldEntity>(
_persistentRescued,
ReferenceEqualityComparer.Instance);
foreach ((WorldEntity entity, ProjectionLocation location) in
_projectionLocations)
{
if (_persistentGuids.Contains(entity.ServerGuid))
{
if (rescued.Add(entity))
{
_persistentRescued.Add(entity);
EntityVanishProbe.Log(
$"[ent] RESCUE guid=0x{entity.ServerGuid:X8} " +
$"from=recenter lb=0x{location.LandblockId:X8}");
}
continue;
}
if (!retainedByLandblock.TryGetValue(
location.LandblockId,
out List<(WorldEntity Entity, int BucketIndex)>? retained))
{
retained = [];
retainedByLandblock.Add(location.LandblockId, retained);
}
retained.Add((entity, location.BucketIndex));
}
int spatialOperationCount = OriginRecenterSpatialOperationCount;
Exception? observerFailure = null;
MutationBatch mutation = BeginMutationBatch();
try
{
foreach ((uint guid, int count) in _visibleLiveProjectionCounts)
{
if (count > 0 && !_visibilityBeforeMutation.ContainsKey(guid))
_visibilityBeforeMutation.Add(guid, true);
}
_visibleLiveProjectionCounts.Clear();
if (_flatEntities.Count != 0)
_flatMembershipDirty = true;
_flatEntities.Clear();
_flatEntityIndices.Clear();
_projectionLocations.Clear();
_loadedLiveByLandblock.Clear();
_primaryProjectionByGuid.Clear();
_additionalProjectionsByGuid.Clear();
_loaded.Clear();
_renderTraversalLandblockSlots.Clear();
_freeRenderTraversalLandblockSlots.Clear();
_renderTraversalSlotByLandblock.Clear();
_tierByLandblock.Clear();
_aabbs.Clear();
_animatedIndexByLandblock.Clear();
_pendingByLandblock.Clear();
_pendingRenderIdsByLandblock.Clear();
_pendingNearTierLandblocks.Clear();
_landblockEntriesView.Clear();
_landblockEntriesWithoutAnimatedIndexView.Clear();
_landblockBoundsView.Clear();
InvalidateLandblockRenderViews(entries: true, bounds: true);
foreach ((uint id, List<(WorldEntity Entity, int BucketIndex)> retained) in
retainedByLandblock)
{
retained.Sort(static (left, right) =>
left.BucketIndex.CompareTo(right.BucketIndex));
var pending = new List<WorldEntity>(retained.Count);
for (int i = 0; i < retained.Count; i++)
{
WorldEntity entity = retained[i].Entity;
pending.Add(entity);
SetProjectionLocation(
entity,
id,
isLoaded: false,
i);
}
_pendingByLandblock.Add(id, pending);
}
}
finally
{
try
{
mutation.Dispose();
}
catch (Exception error)
{
observerFailure = error;
}
}
return new GpuWorldRecenterRetirement(
retirements,
spatialOperationCount,
observerFailure);
}
/// <summary>
/// Compatibility edge for direct state tests. Production streaming uses
/// <see cref="LandblockRetirementCoordinator"/> so each presentation owner