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

@ -242,6 +242,13 @@ public sealed class LandblockRetirementTicket
/// </summary>
public sealed class LandblockRetirementCoordinator
{
private enum BudgetedAdvanceResult : byte
{
Progressed,
Yielded,
Failed,
}
private enum RequestKind : byte
{
BeginFull,
@ -275,7 +282,11 @@ public sealed class LandblockRetirementCoordinator
_requiredPresentationStages;
private readonly Action<GpuLandblockRetirement>? _onDetached;
private readonly Dictionary<uint, List<LandblockRetirementTicket>> _pending = new();
private readonly Queue<LandblockRetirementTicket> _pendingOrder = new();
private readonly LinkedList<LandblockRetirementTicket> _pendingOrder = new();
private readonly Dictionary<
LandblockRetirementTicket,
LinkedListNode<LandblockRetirementTicket>> _pendingNodes =
new(ReferenceEqualityComparer.Instance);
private readonly List<uint> _completedIds = new();
private readonly Queue<Request> _requests = new();
private readonly HashSet<Request> _seenDuringDrain = new();
@ -326,6 +337,104 @@ public sealed class LandblockRetirementCoordinator
ReferenceEquals(_state, state);
internal bool UsesBudgetedSteps => _advancePresentationStep is not null;
/// <summary>
/// Adopts exact receipts produced by the atomic origin-recenter spatial
/// swap. Spatial detachment has already committed; this method only
/// establishes the same retryable presentation ledgers as BeginFull.
/// </summary>
internal Exception? AdoptDetachedFull(
IReadOnlyList<GpuLandblockRetirement> retirements)
{
ArgumentNullException.ThrowIfNull(retirements);
var incomingIds = new HashSet<uint>();
for (int index = 0; index < retirements.Count; index++)
{
GpuLandblockRetirement retirement = retirements[index];
if (retirement.Kind != LandblockRetirementKind.Full)
{
throw new ArgumentException(
"Origin-recenter adoption accepts only full retirements.",
nameof(retirements));
}
uint canonical = Canonicalize(retirement.LandblockId);
if (!incomingIds.Add(canonical))
{
throw new ArgumentException(
$"Origin-recenter receipts contain duplicate landblock " +
$"0x{canonical:X8}.",
nameof(retirements));
}
if (_pending.TryGetValue(
canonical,
out List<LandblockRetirementTicket>? existing)
&& existing.Any(
static ticket =>
ticket.Kind == LandblockRetirementKind.Full))
{
throw new InvalidOperationException(
$"Landblock 0x{canonical:X8} already has a full " +
"retirement receipt.");
}
}
LandblockRetirementStage required =
CoreStages
| _requiredPresentationStages(LandblockRetirementKind.Full);
List<Exception>? failures = null;
for (int index = 0; index < retirements.Count; index++)
{
GpuLandblockRetirement retirement = retirements[index];
uint canonical = Canonicalize(retirement.LandblockId);
_pending.TryGetValue(
canonical,
out List<LandblockRetirementTicket>? existing);
var ticket = new LandblockRetirementTicket(
retirement,
required);
if (existing is null)
{
existing = new List<LandblockRetirementTicket>(1);
_pending.Add(canonical, existing);
}
existing.Add(ticket);
PendingCount++;
if (_advancePresentationStep is not null)
EnqueueBudgetedTicket(ticket);
try
{
_onDetached?.Invoke(retirement);
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
if (_advancePresentationStep is null)
{
AdvanceTicket(ticket);
if (ticket.IsComplete)
{
existing.Remove(ticket);
PendingCount--;
if (existing.Count == 0)
_pending.Remove(canonical);
}
}
}
return failures switch
{
null => null,
{ Count: 1 } => failures[0],
_ => new AggregateException(
"One or more detached landblock observers failed.",
failures),
};
}
public void BeginFull(uint landblockId) => EnqueueRequest(
new Request(RequestKind.BeginFull, Canonicalize(landblockId)));
@ -365,40 +474,56 @@ public sealed class LandblockRetirementCoordinator
return;
}
while (_pendingOrder.TryPeek(out LandblockRetirementTicket? ticket))
while (_pendingOrder.First is { } head)
{
LandblockRetirementTicket ticket = head.Value;
if (ticket.IsComplete)
{
RemoveCompletedTicket(ticket);
continue;
}
LandblockRetirementStage stage = ticket.NextIncompleteStage;
if (stage == LandblockRetirementStage.None)
throw new InvalidOperationException(
"An incomplete retirement ticket has no unfinished stage.");
StreamingWorkCost cost = new(
EntityOperations: 1,
GlRetireOperations:
stage is LandblockRetirementStage.MeshReferences
or LandblockRetirementStage.Terrain
? 1
: 0);
StreamingWorkAdmission admission = meter.TryReserve(
cost,
$"retire-{stage}-0x{ticket.LandblockId:X8}");
if (admission == StreamingWorkAdmission.Yielded)
if (AdvanceBudgetedTicket(ticket, meter)
!= BudgetedAdvanceResult.Progressed)
return;
if (ticket.IsComplete)
RemoveCompletedTicket(ticket);
}
}
LandblockRetirementOperationResult result = AdvanceTicketOne(ticket);
if (result == LandblockRetirementOperationResult.Failed)
/// <summary>
/// Advances only retirement receipts for one canonical landblock. This is
/// the dependency lane used by a destination replacement: unrelated
/// detached-world cleanup retains stable FIFO order, while the exact old
/// owner that fences the destination may complete out of order under the
/// same frame meter.
/// </summary>
internal void AdvancePriority(uint landblockId, StreamingWorkMeter meter)
{
ArgumentNullException.ThrowIfNull(meter);
if (_advancePresentationStep is null)
{
Advance();
return;
}
uint canonical = Canonicalize(landblockId);
while (_pending.TryGetValue(
canonical,
out List<LandblockRetirementTicket>? tickets))
{
if (tickets.Count == 0)
throw new InvalidOperationException(
"A pending retirement owner has no tickets.");
LandblockRetirementTicket ticket = tickets[0];
if (!ticket.IsComplete
&& AdvanceBudgetedTicket(ticket, meter)
!= BudgetedAdvanceResult.Progressed)
{
meter.Fail();
return;
}
meter.Complete();
if (ticket.IsComplete)
RemoveCompletedTicket(ticket);
}
@ -535,7 +660,7 @@ public sealed class LandblockRetirementCoordinator
existing.Add(ticket);
PendingCount++;
if (_advancePresentationStep is not null)
_pendingOrder.Enqueue(ticket);
EnqueueBudgetedTicket(ticket);
_onDetached?.Invoke(stateRetirement);
@ -592,8 +717,9 @@ public sealed class LandblockRetirementCoordinator
private void AdvanceBudgetedEagerAttempt()
{
if (!_pendingOrder.TryPeek(out LandblockRetirementTicket? ticket))
if (_pendingOrder.First is not { } head)
return;
LandblockRetirementTicket ticket = head.Value;
if (ticket.IsComplete)
{
@ -657,15 +783,63 @@ public sealed class LandblockRetirementCoordinator
return result;
}
private void RemoveCompletedTicket(LandblockRetirementTicket ticket)
private BudgetedAdvanceResult AdvanceBudgetedTicket(
LandblockRetirementTicket ticket,
StreamingWorkMeter meter)
{
if (!_pendingOrder.TryPeek(out LandblockRetirementTicket? head)
|| !ReferenceEquals(head, ticket))
LandblockRetirementStage stage = ticket.NextIncompleteStage;
if (stage == LandblockRetirementStage.None)
{
throw new InvalidOperationException(
"Retirement FIFO completion did not match its active head.");
"An incomplete retirement ticket has no unfinished stage.");
}
_pendingOrder.Dequeue();
StreamingWorkCost cost = new(
EntityOperations: 1,
GlRetireOperations:
stage is LandblockRetirementStage.MeshReferences
or LandblockRetirementStage.Terrain
? 1
: 0);
StreamingWorkAdmission admission = meter.TryReserve(
cost,
$"retire-{stage}-0x{ticket.LandblockId:X8}");
if (admission == StreamingWorkAdmission.Yielded)
return BudgetedAdvanceResult.Yielded;
LandblockRetirementOperationResult result = AdvanceTicketOne(ticket);
if (result == LandblockRetirementOperationResult.Failed)
{
meter.Fail();
return BudgetedAdvanceResult.Failed;
}
meter.Complete();
return BudgetedAdvanceResult.Progressed;
}
private void EnqueueBudgetedTicket(LandblockRetirementTicket ticket)
{
LinkedListNode<LandblockRetirementTicket> node =
_pendingOrder.AddLast(ticket);
if (!_pendingNodes.TryAdd(ticket, node))
{
_pendingOrder.Remove(node);
throw new InvalidOperationException(
"A retirement ticket cannot enter the budgeted FIFO twice.");
}
}
private void RemoveCompletedTicket(LandblockRetirementTicket ticket)
{
if (!_pendingNodes.Remove(
ticket,
out LinkedListNode<LandblockRetirementTicket>? node))
{
throw new InvalidOperationException(
"Completed retirement ticket is missing from its budgeted order.");
}
_pendingOrder.Remove(node);
uint id = Canonicalize(ticket.LandblockId);
if (!_pending.TryGetValue(id, out List<LandblockRetirementTicket>? tickets)