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:
parent
2c848d4167
commit
823936ec31
57 changed files with 2551 additions and 324 deletions
|
|
@ -33,10 +33,8 @@ public sealed class StreamingController
|
|||
public bool PendingLoadsCleared;
|
||||
public bool CompletionQueueCleared;
|
||||
public bool RegionCleared;
|
||||
public List<uint>? ResidentIds;
|
||||
public int RetirementCursor;
|
||||
public bool SpatialGenerationDetached;
|
||||
public bool PreparationCommitted;
|
||||
public IEnumerator<uint>? ResidentEnumerator;
|
||||
public (int X, int Y, bool IsSealedDungeon)? Destination;
|
||||
public bool DestinationConfigured;
|
||||
public bool DestinationLoadEnqueued;
|
||||
|
|
@ -75,7 +73,6 @@ public sealed class StreamingController
|
|||
private string? _maximumWorkFrameStage;
|
||||
private double _maximumWorkOperationMilliseconds;
|
||||
private string? _maximumWorkOperationStage;
|
||||
|
||||
private readonly GpuWorldState _state;
|
||||
private StreamingRegion? _region;
|
||||
private RadiiReconfiguration? _pendingRadiiReconfiguration;
|
||||
|
|
@ -538,7 +535,8 @@ public sealed class StreamingController
|
|||
return true;
|
||||
}
|
||||
|
||||
private bool ConvergePendingPublications()
|
||||
private bool ConvergePendingPublications(
|
||||
bool preferDestination = false)
|
||||
{
|
||||
IReadOnlyList<LandblockStreamResult> pending =
|
||||
_presentation.GetPendingPublicationResults();
|
||||
|
|
@ -554,22 +552,48 @@ public sealed class StreamingController
|
|||
try
|
||||
{
|
||||
bool progressed = false;
|
||||
for (int i = 0; i < pending.Count; i++)
|
||||
bool destinationPending = false;
|
||||
if (preferDestination)
|
||||
{
|
||||
LandblockStreamResult result = pending[i];
|
||||
using StreamingWorkMeter.LaneScope lane =
|
||||
_activeWorkMeter.EnterLane(
|
||||
IsDestinationWork(result.LandblockId)
|
||||
? StreamingWorkLane.Destination
|
||||
: StreamingWorkLane.NonDestination);
|
||||
LandblockPublicationAdvance advance =
|
||||
_presentation.ResumePublication(
|
||||
result,
|
||||
_activeWorkMeter,
|
||||
ensureProgress: !progressed);
|
||||
progressed |= advance.Progressed;
|
||||
if (!advance.Completed)
|
||||
return false;
|
||||
for (int i = 0; i < pending.Count; i++)
|
||||
{
|
||||
if (!IsDestinationWork(pending[i].LandblockId))
|
||||
continue;
|
||||
destinationPending = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A partially prepared background landblock must not serialize the
|
||||
// reveal-critical destination behind dictionary insertion order.
|
||||
// Each publication owns independent receipts, so destination
|
||||
// transactions can safely resume first without replaying or
|
||||
// discarding the non-destination cursor.
|
||||
for (int destinationPass = 1; destinationPass >= 0; destinationPass--)
|
||||
{
|
||||
bool requireDestination = destinationPass != 0;
|
||||
for (int i = 0; i < pending.Count; i++)
|
||||
{
|
||||
LandblockStreamResult result = pending[i];
|
||||
bool isDestination = IsDestinationWork(result.LandblockId);
|
||||
if (destinationPending && !isDestination)
|
||||
continue;
|
||||
if (isDestination != requireDestination)
|
||||
continue;
|
||||
|
||||
using StreamingWorkMeter.LaneScope lane =
|
||||
_activeWorkMeter.EnterLane(
|
||||
isDestination
|
||||
? StreamingWorkLane.Destination
|
||||
: StreamingWorkLane.NonDestination);
|
||||
LandblockPublicationAdvance advance =
|
||||
_presentation.ResumePublication(
|
||||
result,
|
||||
_activeWorkMeter,
|
||||
ensureProgress: !progressed);
|
||||
progressed |= advance.Progressed;
|
||||
if (!advance.Completed)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -615,6 +639,7 @@ public sealed class StreamingController
|
|||
bool retirementWasPending =
|
||||
_presentation.PendingRetirementCount != 0;
|
||||
TryAdvanceFullWindowRetirement(meter);
|
||||
AdvanceDestinationRetirementDependency(meter);
|
||||
if (_presentation.UsesBudgetedRetirementSteps
|
||||
|| retirementWasPending)
|
||||
{
|
||||
|
|
@ -636,6 +661,7 @@ public sealed class StreamingController
|
|||
bool retirementWasPending =
|
||||
_presentation.PendingRetirementCount != 0;
|
||||
TryAdvanceOriginRecenterPreparation(meter);
|
||||
AdvanceDestinationRetirementDependency(meter);
|
||||
if (_presentation.UsesBudgetedRetirementSteps
|
||||
|| retirementWasPending)
|
||||
{
|
||||
|
|
@ -657,8 +683,15 @@ public sealed class StreamingController
|
|||
// or retire the fully known owner set through the normal ledger.
|
||||
bool retirementWasPendingAtFrameStart =
|
||||
_presentation.PendingRetirementCount != 0;
|
||||
AdvanceDestinationRetirementDependency(meter);
|
||||
_presentation.AdvanceRetirements(meter);
|
||||
if (!ConvergePendingPublications())
|
||||
bool destinationPublicationIncomplete =
|
||||
_destinationReservation is not null
|
||||
&& !IsRenderNeighborhoodResident(
|
||||
DestinationLandblockId,
|
||||
DestinationRadius);
|
||||
if (!ConvergePendingPublications(
|
||||
preferDestination: destinationPublicationIncomplete))
|
||||
return;
|
||||
|
||||
uint centerId = StreamingRegion.EncodeLandblockId(observerCx, observerCy);
|
||||
|
|
@ -691,7 +724,8 @@ public sealed class StreamingController
|
|||
NormalTick(observerCx, observerCy);
|
||||
}
|
||||
|
||||
DrainAndApply();
|
||||
DrainAndApply(
|
||||
preferDestination: destinationPublicationIncomplete);
|
||||
// Retirement is cleanup after immediate spatial withdrawal. Spend
|
||||
// remaining capacity only after visible/destination publication
|
||||
// work has had access to this frame's shared meter.
|
||||
|
|
@ -709,6 +743,22 @@ public sealed class StreamingController
|
|||
}
|
||||
}
|
||||
|
||||
private void AdvanceDestinationRetirementDependency(
|
||||
StreamingWorkMeter meter)
|
||||
{
|
||||
if (_destinationReservation is null
|
||||
|| !_presentation.IsRetirementPending(DestinationLandblockId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using StreamingWorkMeter.LaneScope lane =
|
||||
meter.EnterLane(StreamingWorkLane.Destination);
|
||||
_presentation.AdvancePriorityRetirement(
|
||||
DestinationLandblockId,
|
||||
meter);
|
||||
}
|
||||
|
||||
private void ObserveWorkLifetime(StreamingWorkMeterSnapshot snapshot)
|
||||
{
|
||||
_lifetimeWorkOverruns = SaturatingAdd(
|
||||
|
|
@ -912,21 +962,53 @@ public sealed class StreamingController
|
|||
{
|
||||
_region = new StreamingRegion(observerCx, observerCy, NearRadius, FarRadius);
|
||||
var bootstrap = _region.ComputeFirstTickDiff();
|
||||
foreach (var id in bootstrap.ToLoadNear) EnqueueLoad(id, LandblockStreamJobKind.LoadNear);
|
||||
EnqueueLoadsByRevealPriority(
|
||||
bootstrap.ToLoadNear,
|
||||
LandblockStreamJobKind.LoadNear);
|
||||
foreach (var id in bootstrap.ToLoadFar) EnqueueLoad(id, LandblockStreamJobKind.LoadFar);
|
||||
_region.MarkResidentFromBootstrap();
|
||||
}
|
||||
else if (_region.CenterX != observerCx || _region.CenterY != observerCy)
|
||||
{
|
||||
var diff = _region.RecenterTo(observerCx, observerCy);
|
||||
foreach (var id in diff.ToPromote) EnqueueLoad(id, LandblockStreamJobKind.PromoteToNear);
|
||||
foreach (var id in diff.ToLoadNear) EnqueueLoad(id, LandblockStreamJobKind.LoadNear);
|
||||
EnqueueLoadsByRevealPriority(
|
||||
diff.ToPromote,
|
||||
LandblockStreamJobKind.PromoteToNear);
|
||||
EnqueueLoadsByRevealPriority(
|
||||
diff.ToLoadNear,
|
||||
LandblockStreamJobKind.LoadNear);
|
||||
foreach (var id in diff.ToLoadFar) EnqueueLoad(id, LandblockStreamJobKind.LoadFar);
|
||||
foreach (var id in diff.ToDemote) DemoteLandblock(id);
|
||||
foreach (var id in diff.ToUnload) EnqueueUnload(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Places the canonical reveal neighborhood at the front of the worker's
|
||||
/// existing Near FIFO. Completion priority alone is too late: a
|
||||
/// non-destination build can otherwise begin a multi-frame publication
|
||||
/// before the destination result has even reached the render thread.
|
||||
/// </summary>
|
||||
private void EnqueueLoadsByRevealPriority(
|
||||
IReadOnlyList<uint> landblockIds,
|
||||
LandblockStreamJobKind kind,
|
||||
bool skipLoaded = false)
|
||||
{
|
||||
for (int destinationPass = 1; destinationPass >= 0; destinationPass--)
|
||||
{
|
||||
bool requireDestination = destinationPass != 0;
|
||||
for (int i = 0; i < landblockIds.Count; i++)
|
||||
{
|
||||
uint id = landblockIds[i];
|
||||
if ((!skipLoaded || !_state.IsLoaded(id))
|
||||
&& IsDestinationWork(id) == requireDestination)
|
||||
{
|
||||
EnqueueLoad(id, kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dungeon-entry edge: cancel the in-flight window load, unload every
|
||||
/// resident neighbor, and pin streaming to the player's single dungeon
|
||||
|
|
@ -1016,8 +1098,10 @@ public sealed class StreamingController
|
|||
if (!rebuilt.Resident.Contains(id)) EnqueueUnload(id);
|
||||
|
||||
var boot = rebuilt.ComputeFirstTickDiff();
|
||||
foreach (var id in boot.ToLoadNear)
|
||||
if (!_state.IsLoaded(id)) EnqueueLoad(id, LandblockStreamJobKind.LoadNear);
|
||||
EnqueueLoadsByRevealPriority(
|
||||
boot.ToLoadNear,
|
||||
LandblockStreamJobKind.LoadNear,
|
||||
skipLoaded: true);
|
||||
foreach (var id in boot.ToLoadFar)
|
||||
if (!_state.IsLoaded(id)) EnqueueLoad(id, LandblockStreamJobKind.LoadFar);
|
||||
rebuilt.MarkResidentFromBootstrap();
|
||||
|
|
@ -1053,8 +1137,10 @@ public sealed class StreamingController
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances every retained old-window teardown and reports whether the
|
||||
/// composition root may safely change the shared world origin.
|
||||
/// Reports whether every old resident has been detached and captured by
|
||||
/// an exact retirement receipt. Deferred receipt cleanup may continue
|
||||
/// after the shared origin changes; same-landblock publication remains
|
||||
/// fenced by <see cref="IsPublicationBlockedByRetirement"/>.
|
||||
/// </summary>
|
||||
internal bool IsOriginRecenterRetirementComplete()
|
||||
{
|
||||
|
|
@ -1062,14 +1148,14 @@ public sealed class StreamingController
|
|||
throw new InvalidOperationException(
|
||||
"No streaming-origin recenter transaction is pending.");
|
||||
|
||||
return _originRecenterRetirement.PreparationCommitted
|
||||
&& _presentation.PendingRetirementCount == 0;
|
||||
return _originRecenterRetirement.PreparationCommitted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the streaming bootstrap gate after the composition root has
|
||||
/// committed the new shared origin. No old-window presentation owner may
|
||||
/// still be pending at this edge.
|
||||
/// committed the new shared origin. Old detached receipts continue under
|
||||
/// the shared frame meter and fence only a replacement publication with
|
||||
/// the same canonical landblock key.
|
||||
/// </summary>
|
||||
internal bool TryCommitOriginRecenter(
|
||||
int destinationX,
|
||||
|
|
@ -1104,10 +1190,6 @@ public sealed class StreamingController
|
|||
if (!transaction.PreparationCommitted)
|
||||
throw new InvalidOperationException(
|
||||
"Streaming-origin retirement preparation has not completed.");
|
||||
if (_presentation.PendingRetirementCount != 0)
|
||||
throw new InvalidOperationException(
|
||||
"The streaming origin cannot change while old-window presentation retirement is pending.");
|
||||
|
||||
var destination = (destinationX, destinationY, isSealedDungeon);
|
||||
if (transaction.Destination is { } retained && retained != destination)
|
||||
{
|
||||
|
|
@ -1174,8 +1256,6 @@ public sealed class StreamingController
|
|||
{
|
||||
if (_originRecenterRetirement is not { PreparationCommitted: true })
|
||||
return false;
|
||||
if (_presentation.PendingRetirementCount != 0)
|
||||
return false;
|
||||
|
||||
_collapsed = false;
|
||||
_collapsedCenter = 0u;
|
||||
|
|
@ -1293,35 +1373,33 @@ public sealed class StreamingController
|
|||
}
|
||||
transaction.RegionCleared = true;
|
||||
}
|
||||
if (transaction.ResidentIds is null)
|
||||
{
|
||||
transaction.ResidentIds = [];
|
||||
transaction.ResidentEnumerator =
|
||||
_state.LoadedLandblockIds.GetEnumerator();
|
||||
}
|
||||
while (transaction.ResidentEnumerator is { } residentEnumerator)
|
||||
if (!transaction.SpatialGenerationDetached)
|
||||
{
|
||||
StreamingWorkAdmission admission = meter.TryReserve(
|
||||
new StreamingWorkCost(EntityOperations: 1),
|
||||
"recenter-capture-resident-id");
|
||||
new StreamingWorkCost(
|
||||
EntityOperations:
|
||||
_state.OriginRecenterSpatialOperationCount),
|
||||
"recenter-detach-spatial-generation",
|
||||
ensureProgress: true);
|
||||
if (admission == StreamingWorkAdmission.Yielded)
|
||||
return false;
|
||||
|
||||
bool moved;
|
||||
try
|
||||
{
|
||||
moved = residentEnumerator.MoveNext();
|
||||
if (moved)
|
||||
transaction.ResidentIds.Add(residentEnumerator.Current);
|
||||
else
|
||||
{
|
||||
residentEnumerator.Dispose();
|
||||
transaction.ResidentEnumerator = null;
|
||||
FullWindowRetirementCount++;
|
||||
LastFullWindowRetirementLandblockCount =
|
||||
transaction.ResidentIds.Count;
|
||||
}
|
||||
GpuWorldRecenterRetirement detached =
|
||||
_presentation.DetachAllForOriginRecenter();
|
||||
transaction.SpatialGenerationDetached = true;
|
||||
FullWindowRetirementCount++;
|
||||
LastFullWindowRetirementLandblockCount =
|
||||
detached.Landblocks.Count;
|
||||
meter.Complete();
|
||||
if (detached.ObserverFailure is not null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"streaming: committed origin-recenter spatial " +
|
||||
$"generation reported failure: {detached.ObserverFailure}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -1330,46 +1408,6 @@ public sealed class StreamingController
|
|||
}
|
||||
}
|
||||
|
||||
List<uint> residentIds = transaction.ResidentIds
|
||||
?? throw new InvalidOperationException(
|
||||
"Origin-recenter resident capture did not commit.");
|
||||
while (transaction.RetirementCursor < residentIds.Count)
|
||||
{
|
||||
uint id = residentIds[transaction.RetirementCursor];
|
||||
int entityCount = _state.TryGetLandblock(
|
||||
id,
|
||||
out LoadedLandblock? loaded)
|
||||
? loaded!.Entities.Count
|
||||
: 0;
|
||||
StreamingWorkAdmission admission = meter.TryReserve(
|
||||
new StreamingWorkCost(
|
||||
EntityOperations: Math.Max(1, entityCount)),
|
||||
$"recenter-detach-0x{id:X8}");
|
||||
if (admission == StreamingWorkAdmission.Yielded)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
_presentation.EnqueueFullRetirement(id);
|
||||
transaction.RetirementCursor++;
|
||||
meter.Complete();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
// A delivered visibility-observer failure may surface after
|
||||
// detachment has committed. Advance that exact cursor only
|
||||
// when world state proves the old resident is unreachable;
|
||||
// otherwise retain it for the next frame.
|
||||
if (!_state.IsLoaded(id))
|
||||
transaction.RetirementCursor++;
|
||||
meter.Fail();
|
||||
Console.WriteLine(
|
||||
$"streaming: origin-recenter retirement for 0x{id:X8} " +
|
||||
$"will resume: {error}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
transaction.PreparationCommitted = true;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1567,17 +1605,24 @@ public sealed class StreamingController
|
|||
/// order but no class bypasses time, bytes, entities, uploads, or retire
|
||||
/// operation limits.
|
||||
/// </summary>
|
||||
private void DrainAndApply()
|
||||
private void DrainAndApply(bool preferDestination = false)
|
||||
{
|
||||
StreamingWorkMeter meter = _activeWorkMeter
|
||||
?? throw new InvalidOperationException(
|
||||
"Completion scheduling requires an active frame meter.");
|
||||
|
||||
AdmitCompletions(meter);
|
||||
bool destinationQueued =
|
||||
preferDestination
|
||||
&& _completionQueue.HasPriority(
|
||||
StreamingCompletionPriority.Destination);
|
||||
bool executed = false;
|
||||
while (_completionQueue.TryPeekNext(
|
||||
_isPublicationBlockedByRetirement,
|
||||
out StreamingQueuedCompletion? completion))
|
||||
out StreamingQueuedCompletion? completion,
|
||||
destinationQueued
|
||||
? StreamingCompletionPriority.Unload
|
||||
: StreamingCompletionPriority.Far))
|
||||
{
|
||||
StreamingQueuedCompletion work = completion
|
||||
?? throw new InvalidOperationException(
|
||||
|
|
@ -1650,8 +1695,13 @@ public sealed class StreamingController
|
|||
: ClassifyCompletion(result);
|
||||
LandblockStreamCostEstimate estimate =
|
||||
LandblockStreamResultCost.Estimate(result);
|
||||
// A stale result belongs to a generation that was already
|
||||
// cancelled. Reading it releases worker-outbox ownership; it does
|
||||
// not admit payload into the current world. Charge only elapsed
|
||||
// time so a large completed old window cannot consume the
|
||||
// destination generation's completion quota for many frames.
|
||||
StreamingWorkCost admissionCost = stale
|
||||
? new StreamingWorkCost(CompletionAdmissions: 1)
|
||||
? default
|
||||
: new StreamingWorkCost(
|
||||
CompletionAdmissions:
|
||||
estimate.Work.CompletionAdmissions,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue