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
|
|
@ -13,6 +13,17 @@ public sealed record GpuLandblockRetirement(
|
|||
LandblockRetirementKind Kind,
|
||||
IReadOnlyList<WorldEntity> Entities);
|
||||
|
||||
/// <summary>
|
||||
/// Exact result of the atomic spatial-generation swap used before a shared
|
||||
/// world-origin recenter. Spatial membership is already unreachable when this
|
||||
/// value returns; the retained per-landblock receipts let presentation owners
|
||||
/// retire their resources later under the ordinary frame budget.
|
||||
/// </summary>
|
||||
internal sealed record GpuWorldRecenterRetirement(
|
||||
IReadOnlyList<GpuLandblockRetirement> Landblocks,
|
||||
int SpatialOperationCount,
|
||||
Exception? ObserverFailure);
|
||||
|
||||
public enum LandblockRetirementKind
|
||||
{
|
||||
Full,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -240,6 +240,11 @@ public sealed class LandblockPresentationPipeline
|
|||
public void AdvanceRetirements(StreamingWorkMeter meter) =>
|
||||
_retirements.Advance(meter);
|
||||
|
||||
internal void AdvancePriorityRetirement(
|
||||
uint landblockId,
|
||||
StreamingWorkMeter meter) =>
|
||||
_retirements.AdvancePriority(landblockId, meter);
|
||||
|
||||
public void BeginFullRetirement(uint landblockId)
|
||||
{
|
||||
_retirements.BeginFull(landblockId);
|
||||
|
|
@ -260,6 +265,29 @@ public sealed class LandblockPresentationPipeline
|
|||
internal void EnqueueNearLayerRetirement(uint landblockId) =>
|
||||
_retirements.BeginNearLayer(landblockId);
|
||||
|
||||
/// <summary>
|
||||
/// Withdraws the complete old spatial generation in one transaction, then
|
||||
/// transfers its exact receipts to the ordinary budgeted retirement FIFO.
|
||||
/// </summary>
|
||||
internal GpuWorldRecenterRetirement DetachAllForOriginRecenter()
|
||||
{
|
||||
GpuWorldRecenterRetirement detached =
|
||||
_state.DetachAllForOriginRecenter();
|
||||
Exception? adoptionFailure =
|
||||
_retirements.AdoptDetachedFull(detached.Landblocks);
|
||||
Exception? failure = (detached.ObserverFailure, adoptionFailure) switch
|
||||
{
|
||||
(null, null) => null,
|
||||
({ } observer, null) => observer,
|
||||
(null, { } adoption) => adoption,
|
||||
({ } observer, { } adoption) => new AggregateException(
|
||||
"Origin-recenter spatial observers and retirement adoption failed.",
|
||||
observer,
|
||||
adoption),
|
||||
};
|
||||
return detached with { ObserverFailure = failure };
|
||||
}
|
||||
|
||||
public void PublishLoaded(LandblockStreamResult.Loaded loaded)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(loaded);
|
||||
|
|
|
|||
|
|
@ -59,16 +59,15 @@ public sealed class LandblockPresentationRetirementOwner
|
|||
public void Advance(LandblockRetirementTicket ticket)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ticket);
|
||||
bool staticOnly = ticket.Kind == LandblockRetirementKind.NearLayer;
|
||||
|
||||
ticket.RunForEachEntity(
|
||||
LandblockRetirementStage.EntityLighting,
|
||||
entity => !staticOnly || entity.ServerGuid == 0,
|
||||
RemoveLighting);
|
||||
static entity => entity.ServerGuid == 0,
|
||||
_staticPresentation.RemoveLighting);
|
||||
ticket.RunForEachEntity(
|
||||
LandblockRetirementStage.EntityTranslucency,
|
||||
entity => !staticOnly || entity.ServerGuid == 0,
|
||||
RemoveTranslucency);
|
||||
static entity => entity.ServerGuid == 0,
|
||||
_staticPresentation.RemoveTranslucency);
|
||||
ticket.RunForEachEntity(
|
||||
LandblockRetirementStage.PluginProjection,
|
||||
static entity => entity.ServerGuid == 0,
|
||||
|
|
@ -105,19 +104,18 @@ public sealed class LandblockPresentationRetirementOwner
|
|||
LandblockRetirementTicket ticket)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ticket);
|
||||
bool staticOnly = ticket.Kind == LandblockRetirementKind.NearLayer;
|
||||
return ticket.NextIncompleteStage switch
|
||||
{
|
||||
LandblockRetirementStage.EntityLighting =>
|
||||
ticket.RunEntityStep(
|
||||
LandblockRetirementStage.EntityLighting,
|
||||
entity => !staticOnly || entity.ServerGuid == 0,
|
||||
RemoveLighting),
|
||||
static entity => entity.ServerGuid == 0,
|
||||
_staticPresentation.RemoveLighting),
|
||||
LandblockRetirementStage.EntityTranslucency =>
|
||||
ticket.RunEntityStep(
|
||||
LandblockRetirementStage.EntityTranslucency,
|
||||
entity => !staticOnly || entity.ServerGuid == 0,
|
||||
RemoveTranslucency),
|
||||
static entity => entity.ServerGuid == 0,
|
||||
_staticPresentation.RemoveTranslucency),
|
||||
LandblockRetirementStage.PluginProjection =>
|
||||
ticket.RunEntityStep(
|
||||
LandblockRetirementStage.PluginProjection,
|
||||
|
|
@ -152,20 +150,4 @@ public sealed class LandblockPresentationRetirementOwner
|
|||
_ => LandblockRetirementOperationResult.NoWork,
|
||||
};
|
||||
}
|
||||
|
||||
private void RemoveLighting(WorldEntity entity)
|
||||
{
|
||||
if (entity.ServerGuid == 0)
|
||||
_staticPresentation.RemoveLighting(entity);
|
||||
else
|
||||
_lighting.UnregisterOwner(entity.Id);
|
||||
}
|
||||
|
||||
private void RemoveTranslucency(WorldEntity entity)
|
||||
{
|
||||
if (entity.ServerGuid == 0)
|
||||
_staticPresentation.RemoveTranslucency(entity);
|
||||
else
|
||||
_translucency.ClearEntity(entity.Id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -506,6 +506,12 @@ internal sealed class LocalPlayerTeleportController
|
|||
return;
|
||||
break;
|
||||
case TeleportAnimEvent.PlayExitSound:
|
||||
// gmSmartBoxUI::UseTime @ 0x004D6E30 releases destination
|
||||
// cell blocking at the exact portal/world viewport swap.
|
||||
// LoginComplete remains one WorldFadeIn second later.
|
||||
_worldReveal.RevealWorldViewport();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
_presentation.ExitTunnel();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ internal sealed class StreamingCompletionQueue
|
|||
public int Count => _count;
|
||||
public long RetainedCpuBytes => _retainedCpuBytes;
|
||||
|
||||
public bool HasPriority(StreamingCompletionPriority priority) =>
|
||||
_queues[(int)priority].Count != 0;
|
||||
|
||||
public void Enqueue(StreamingQueuedCompletion completion)
|
||||
{
|
||||
_queues[(int)completion.Priority].Enqueue(completion);
|
||||
|
|
@ -69,10 +72,15 @@ internal sealed class StreamingCompletionQueue
|
|||
|
||||
public bool TryPeekNext(
|
||||
Func<LandblockStreamResult, bool> isBlocked,
|
||||
out StreamingQueuedCompletion? completion)
|
||||
out StreamingQueuedCompletion? completion,
|
||||
StreamingCompletionPriority maximumPriority =
|
||||
StreamingCompletionPriority.Far)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(isBlocked);
|
||||
for (int priority = 0; priority < _queues.Length; priority++)
|
||||
int lastPriority = Math.Min(
|
||||
(int)maximumPriority,
|
||||
_queues.Length - 1);
|
||||
for (int priority = 0; priority <= lastPriority; priority++)
|
||||
{
|
||||
Queue<StreamingQueuedCompletion> queue = _queues[priority];
|
||||
if (queue.Count == 0)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ using AcDream.App.World;
|
|||
namespace AcDream.App.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// Coordinates streaming-origin lifetime boundaries. Presentation must retire
|
||||
/// the complete old window first; a teleport then changes
|
||||
/// Coordinates streaming-origin lifetime boundaries. Spatial ownership must
|
||||
/// detach the complete old window first; a teleport then changes
|
||||
/// <see cref="LiveWorldOriginState"/> before destination streaming resumes,
|
||||
/// while a session boundary releases the controller without recentering. The
|
||||
/// transaction remains pending across frames when an owner teardown needs retry.
|
||||
/// while deferred resource receipts continue under the shared frame budget.
|
||||
/// A session boundary releases the controller without recentering.
|
||||
/// </summary>
|
||||
internal sealed class StreamingOriginRecenterCoordinator : IStreamingOriginConvergence
|
||||
{
|
||||
|
|
@ -72,9 +72,9 @@ internal sealed class StreamingOriginRecenterCoordinator : IStreamingOriginConve
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Observes retained presentation teardown and commits the new origin once
|
||||
/// the streaming frame owner has converged it. Returns true only after the
|
||||
/// destination streaming gate has been released.
|
||||
/// Observes old-window spatial detachment and commits the new origin once
|
||||
/// every detached owner has an exact retirement receipt. Returns true only
|
||||
/// after the destination streaming gate has been released.
|
||||
/// </summary>
|
||||
public bool Advance()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ public sealed record StreamingWorkBudgetOptions(
|
|||
MaxUpdateMilliseconds: 2.0,
|
||||
MaxCompletionAdmissions: 64,
|
||||
MaxAdoptedCpuBytes: 8 * MiB,
|
||||
MaxEntityOperations: 256,
|
||||
// Entity cursors are intentionally very small operations (often one
|
||||
// dictionary/index write). The elapsed-time ceiling remains the
|
||||
// authoritative CPU guard; 256 left more than 90% of that budget
|
||||
// unused and stretched destination publication past retail's
|
||||
// five-second wait-notice edge.
|
||||
MaxEntityOperations: 4_096,
|
||||
MaxGpuUploadBytes: 8 * MiB,
|
||||
MaxGlRetireOperations: 64,
|
||||
DestinationReserveFraction: 0.75f);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,16 @@ internal interface IWorldRevealStreamingScheduler
|
|||
void EndDestinationReservation(long revealGeneration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generation-scoped renderer-resource profile used only while the normal
|
||||
/// world viewport is withheld for a login or portal destination.
|
||||
/// </summary>
|
||||
internal interface IWorldRevealRenderResourceScheduler
|
||||
{
|
||||
void BeginDestinationReveal(long revealGeneration);
|
||||
void EndDestinationReveal(long revealGeneration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns one login/portal reveal lifetime across readiness and lifecycle
|
||||
/// diagnostics. This keeps the destination barrier and its observations on a
|
||||
|
|
@ -27,7 +37,9 @@ internal sealed class WorldRevealCoordinator
|
|||
private readonly WorldRevealLifecycleTelemetry _lifecycle;
|
||||
private readonly WorldGenerationQuiescence? _quiescence;
|
||||
private readonly IWorldRevealStreamingScheduler? _streaming;
|
||||
private readonly IWorldRevealRenderResourceScheduler? _renderResources;
|
||||
private long _activeGeneration;
|
||||
private bool _worldViewportReleased;
|
||||
|
||||
public WorldRevealCoordinator(
|
||||
Func<uint, int, bool> isRenderNeighborhoodReady,
|
||||
|
|
@ -39,7 +51,8 @@ internal sealed class WorldRevealCoordinator
|
|||
Func<uint, bool> isSpawnClaimUnhydratable,
|
||||
Action<string>? log = null,
|
||||
WorldGenerationQuiescence? quiescence = null,
|
||||
IWorldRevealStreamingScheduler? streaming = null)
|
||||
IWorldRevealStreamingScheduler? streaming = null,
|
||||
IWorldRevealRenderResourceScheduler? renderResources = null)
|
||||
{
|
||||
_readiness = new WorldRevealReadinessBarrier(
|
||||
isRenderNeighborhoodReady,
|
||||
|
|
@ -52,6 +65,7 @@ internal sealed class WorldRevealCoordinator
|
|||
_lifecycle = new WorldRevealLifecycleTelemetry(log);
|
||||
_quiescence = quiescence;
|
||||
_streaming = streaming;
|
||||
_renderResources = renderResources;
|
||||
}
|
||||
|
||||
public WorldRevealLifecycleSnapshot Snapshot => _lifecycle.Snapshot;
|
||||
|
|
@ -66,11 +80,13 @@ internal sealed class WorldRevealCoordinator
|
|||
_readiness.Begin();
|
||||
long generation = _lifecycle.Begin(kind);
|
||||
_activeGeneration = generation;
|
||||
_worldViewportReleased = false;
|
||||
_quiescence?.Begin(generation);
|
||||
_streaming?.BeginDestinationReservation(
|
||||
generation,
|
||||
destinationCell,
|
||||
WorldRevealReadinessBarrier.RequiredRenderRadius(destinationCell));
|
||||
_renderResources?.BeginDestinationReveal(generation);
|
||||
return generation;
|
||||
}
|
||||
|
||||
|
|
@ -99,26 +115,43 @@ internal sealed class WorldRevealCoordinator
|
|||
public bool ObserveWait(TimeSpan elapsed) =>
|
||||
_lifecycle.ObserveWait(elapsed);
|
||||
|
||||
/// <summary>
|
||||
/// Reopens the destination world at retail's TunnelFadeOut ->
|
||||
/// WorldFadeIn viewport swap without completing the one-second
|
||||
/// WorldFadeIn/LoginComplete tail.
|
||||
/// </summary>
|
||||
public void RevealWorldViewport()
|
||||
{
|
||||
long generation = _activeGeneration;
|
||||
if (generation == 0 || _worldViewportReleased)
|
||||
return;
|
||||
|
||||
_streaming?.EndDestinationReservation(generation);
|
||||
_renderResources?.EndDestinationReveal(generation);
|
||||
_quiescence?.End(generation);
|
||||
_worldViewportReleased = true;
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
_lifecycle.Complete();
|
||||
EndQuiescence();
|
||||
EndLifetime();
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
_lifecycle.Cancel();
|
||||
EndQuiescence();
|
||||
EndLifetime();
|
||||
}
|
||||
|
||||
private void EndQuiescence()
|
||||
private void EndLifetime()
|
||||
{
|
||||
long generation = _activeGeneration;
|
||||
if (generation == 0)
|
||||
return;
|
||||
|
||||
RevealWorldViewport();
|
||||
_activeGeneration = 0;
|
||||
_streaming?.EndDestinationReservation(generation);
|
||||
_quiescence?.End(generation);
|
||||
_worldViewportReleased = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue