perf(streaming): quiesce retired generations and budget teardown
Publish the retail blocking-for-cells edge before deferred recenter work, freeze old-world presentation/simulation/audio, and advance full-window retirement from exact metered entity and owner cursors. This removes synchronous portal teardown without allowing retained owners to remain observable.
This commit is contained in:
parent
b8f6317fe1
commit
bb16f74fd4
38 changed files with 1691 additions and 170 deletions
|
|
@ -20,6 +20,13 @@ public enum LandblockRetirementStage : ushort
|
|||
LegacyPresentation = 1 << 11,
|
||||
}
|
||||
|
||||
internal enum LandblockRetirementOperationResult : byte
|
||||
{
|
||||
NoWork,
|
||||
Progressed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One retryable landblock-retirement ledger. Successful owner operations are
|
||||
/// committed once; a later attempt resumes at the exact failed owner/entity.
|
||||
|
|
@ -70,6 +77,29 @@ public sealed class LandblockRetirementTicket
|
|||
}
|
||||
}
|
||||
|
||||
internal LandblockRetirementOperationResult RunOnceStep(
|
||||
LandblockRetirementStage stage,
|
||||
Action operation)
|
||||
{
|
||||
ValidateSingleStage(stage);
|
||||
ArgumentNullException.ThrowIfNull(operation);
|
||||
if ((CompletedStages & stage) != 0)
|
||||
return LandblockRetirementOperationResult.NoWork;
|
||||
|
||||
try
|
||||
{
|
||||
operation();
|
||||
CompletedStages |= stage;
|
||||
_failures.Remove(stage);
|
||||
return LandblockRetirementOperationResult.Progressed;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
_failures[stage] = error;
|
||||
return LandblockRetirementOperationResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RunForEachEntity(
|
||||
LandblockRetirementStage stage,
|
||||
Func<WorldEntity, bool> predicate,
|
||||
|
|
@ -109,6 +139,74 @@ public sealed class LandblockRetirementTicket
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances exactly one entity cursor, including predicate misses. This
|
||||
/// makes both owner work and the scan needed to find it visible to the
|
||||
/// frame budget.
|
||||
/// </summary>
|
||||
internal LandblockRetirementOperationResult RunEntityStep(
|
||||
LandblockRetirementStage stage,
|
||||
Func<WorldEntity, bool> predicate,
|
||||
Action<WorldEntity> operation)
|
||||
{
|
||||
ValidateSingleStage(stage);
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
ArgumentNullException.ThrowIfNull(operation);
|
||||
if ((CompletedStages & stage) != 0)
|
||||
return LandblockRetirementOperationResult.NoWork;
|
||||
|
||||
_entityCursors.TryGetValue(stage, out int cursor);
|
||||
if (cursor >= Entities.Count)
|
||||
{
|
||||
CompletedStages |= stage;
|
||||
_entityCursors.Remove(stage);
|
||||
_failures.Remove(stage);
|
||||
return LandblockRetirementOperationResult.Progressed;
|
||||
}
|
||||
|
||||
WorldEntity entity = Entities[cursor];
|
||||
if (predicate(entity))
|
||||
{
|
||||
try
|
||||
{
|
||||
operation(entity);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
_entityCursors[stage] = cursor;
|
||||
_failures[stage] = error;
|
||||
return LandblockRetirementOperationResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
cursor++;
|
||||
if (cursor == Entities.Count)
|
||||
{
|
||||
CompletedStages |= stage;
|
||||
_entityCursors.Remove(stage);
|
||||
_failures.Remove(stage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_entityCursors[stage] = cursor;
|
||||
}
|
||||
|
||||
return LandblockRetirementOperationResult.Progressed;
|
||||
}
|
||||
|
||||
internal LandblockRetirementStage NextIncompleteStage
|
||||
{
|
||||
get
|
||||
{
|
||||
ushort remaining = (ushort)(RequiredStages & ~CompletedStages);
|
||||
if (remaining == 0)
|
||||
return LandblockRetirementStage.None;
|
||||
|
||||
ushort lowest = (ushort)(remaining & (ushort)-(short)remaining);
|
||||
return (LandblockRetirementStage)lowest;
|
||||
}
|
||||
}
|
||||
|
||||
internal void BeginAttempt() => _callbackFailure = null;
|
||||
|
||||
internal void RecordCallbackFailure(Exception error) =>
|
||||
|
|
@ -170,9 +268,13 @@ public sealed class LandblockRetirementCoordinator
|
|||
|
||||
private readonly GpuWorldState _state;
|
||||
private readonly Action<LandblockRetirementTicket> _advancePresentation;
|
||||
private readonly Func<
|
||||
LandblockRetirementTicket,
|
||||
LandblockRetirementOperationResult>? _advancePresentationStep;
|
||||
private readonly Func<LandblockRetirementKind, LandblockRetirementStage>
|
||||
_requiredPresentationStages;
|
||||
private readonly Dictionary<uint, List<LandblockRetirementTicket>> _pending = new();
|
||||
private readonly Queue<LandblockRetirementTicket> _pendingOrder = new();
|
||||
private readonly List<uint> _completedIds = new();
|
||||
private readonly Queue<Request> _requests = new();
|
||||
private readonly HashSet<Request> _seenDuringDrain = new();
|
||||
|
|
@ -187,12 +289,30 @@ public sealed class LandblockRetirementCoordinator
|
|||
ArgumentNullException.ThrowIfNull(advancePresentation);
|
||||
_state = state;
|
||||
_advancePresentation = advancePresentation;
|
||||
_advancePresentationStep = null;
|
||||
_requiredPresentationStages = requiredPresentationStages
|
||||
?? (kind => kind == LandblockRetirementKind.Full
|
||||
? ProductionPresentationStages
|
||||
: ProductionPresentationStages & ~LandblockRetirementStage.Terrain);
|
||||
}
|
||||
|
||||
private LandblockRetirementCoordinator(
|
||||
GpuWorldState state,
|
||||
Func<LandblockRetirementTicket, LandblockRetirementOperationResult>
|
||||
advancePresentationStep,
|
||||
Action<LandblockRetirementTicket> advancePresentation,
|
||||
Func<LandblockRetirementKind, LandblockRetirementStage>
|
||||
requiredPresentationStages)
|
||||
{
|
||||
_state = state ?? throw new ArgumentNullException(nameof(state));
|
||||
_advancePresentationStep = advancePresentationStep
|
||||
?? throw new ArgumentNullException(nameof(advancePresentationStep));
|
||||
_advancePresentation = advancePresentation
|
||||
?? throw new ArgumentNullException(nameof(advancePresentation));
|
||||
_requiredPresentationStages = requiredPresentationStages
|
||||
?? throw new ArgumentNullException(nameof(requiredPresentationStages));
|
||||
}
|
||||
|
||||
public int PendingCount { get; private set; }
|
||||
|
||||
public bool IsPending(uint landblockId) =>
|
||||
|
|
@ -200,6 +320,7 @@ public sealed class LandblockRetirementCoordinator
|
|||
|
||||
internal bool MatchesState(GpuWorldState state) =>
|
||||
ReferenceEquals(_state, state);
|
||||
internal bool UsesBudgetedSteps => _advancePresentationStep is not null;
|
||||
|
||||
public void BeginFull(uint landblockId) => EnqueueRequest(
|
||||
new Request(RequestKind.BeginFull, Canonicalize(landblockId)));
|
||||
|
|
@ -210,8 +331,81 @@ public sealed class LandblockRetirementCoordinator
|
|||
public void Advance() => EnqueueRequest(
|
||||
new Request(RequestKind.Advance, 0u));
|
||||
|
||||
internal static LandblockRetirementCoordinator CreateBudgeted(
|
||||
GpuWorldState state,
|
||||
Func<LandblockRetirementTicket, LandblockRetirementOperationResult>
|
||||
advancePresentationStep,
|
||||
Action<LandblockRetirementTicket> advancePresentation)
|
||||
{
|
||||
return new LandblockRetirementCoordinator(
|
||||
state,
|
||||
advancePresentationStep,
|
||||
advancePresentation,
|
||||
kind => kind == LandblockRetirementKind.Full
|
||||
? ProductionPresentationStages
|
||||
: ProductionPresentationStages & ~LandblockRetirementStage.Terrain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances stable FIFO retirement work under the caller's one frame meter.
|
||||
/// Every reservation maps to exactly one entity cursor or owner stage.
|
||||
/// </summary>
|
||||
public void Advance(StreamingWorkMeter meter)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(meter);
|
||||
if (_advancePresentationStep is null)
|
||||
{
|
||||
Advance();
|
||||
return;
|
||||
}
|
||||
|
||||
while (_pendingOrder.TryPeek(out LandblockRetirementTicket? ticket))
|
||||
{
|
||||
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)
|
||||
return;
|
||||
|
||||
LandblockRetirementOperationResult result = AdvanceTicketOne(ticket);
|
||||
if (result == LandblockRetirementOperationResult.Failed)
|
||||
{
|
||||
meter.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
meter.Complete();
|
||||
if (ticket.IsComplete)
|
||||
RemoveCompletedTicket(ticket);
|
||||
}
|
||||
}
|
||||
|
||||
private void AdvanceCore()
|
||||
{
|
||||
if (_advancePresentationStep is not null)
|
||||
{
|
||||
AdvanceBudgetedEagerAttempt();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pending.Count == 0)
|
||||
return;
|
||||
|
||||
|
|
@ -278,6 +472,9 @@ public sealed class LandblockRetirementCoordinator
|
|||
if (existing[i].Kind == kind)
|
||||
{
|
||||
LandblockRetirementTicket existingTicket = existing[i];
|
||||
if (_advancePresentationStep is not null)
|
||||
return;
|
||||
|
||||
AdvanceTicket(existingTicket);
|
||||
if (existingTicket.IsComplete)
|
||||
{
|
||||
|
|
@ -331,14 +528,19 @@ public sealed class LandblockRetirementCoordinator
|
|||
}
|
||||
existing.Add(ticket);
|
||||
PendingCount++;
|
||||
if (_advancePresentationStep is not null)
|
||||
_pendingOrder.Enqueue(ticket);
|
||||
|
||||
AdvanceTicket(ticket);
|
||||
if (ticket.IsComplete)
|
||||
if (_advancePresentationStep is null)
|
||||
{
|
||||
existing.Remove(ticket);
|
||||
PendingCount--;
|
||||
if (existing.Count == 0)
|
||||
_pending.Remove(canonical);
|
||||
AdvanceTicket(ticket);
|
||||
if (ticket.IsComplete)
|
||||
{
|
||||
existing.Remove(ticket);
|
||||
PendingCount--;
|
||||
if (existing.Count == 0)
|
||||
_pending.Remove(canonical);
|
||||
}
|
||||
}
|
||||
// Visibility is a delivered-by-attempt broadcast: every subscriber is
|
||||
// invoked and failures are aggregated, but failed subscribers are not
|
||||
|
|
@ -380,6 +582,96 @@ public sealed class LandblockRetirementCoordinator
|
|||
}
|
||||
}
|
||||
|
||||
private void AdvanceBudgetedEagerAttempt()
|
||||
{
|
||||
if (!_pendingOrder.TryPeek(out LandblockRetirementTicket? ticket))
|
||||
return;
|
||||
|
||||
if (ticket.IsComplete)
|
||||
{
|
||||
RemoveCompletedTicket(ticket);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compatibility/debug callers explicitly asking for an unmetered
|
||||
// advance retain the former one-attempt behavior: every independent
|
||||
// stage is attempted once, while successful stages/entities remain
|
||||
// committed in the same exact ticket. Production calls the metered
|
||||
// overload and never enters this path.
|
||||
AdvanceTicket(ticket);
|
||||
if (ticket.IsComplete)
|
||||
RemoveCompletedTicket(ticket);
|
||||
}
|
||||
|
||||
private LandblockRetirementOperationResult AdvanceTicketOne(
|
||||
LandblockRetirementTicket ticket)
|
||||
{
|
||||
ticket.BeginAttempt();
|
||||
LandblockRetirementStage stage = ticket.NextIncompleteStage;
|
||||
LandblockRetirementOperationResult result;
|
||||
try
|
||||
{
|
||||
result = stage switch
|
||||
{
|
||||
LandblockRetirementStage.MeshReferences =>
|
||||
ticket.RunOnceStep(
|
||||
stage,
|
||||
() => _state.ReleaseLandblockMeshReferences(
|
||||
ticket.LandblockId)),
|
||||
LandblockRetirementStage.StaticScripts =>
|
||||
ticket.RunEntityStep(
|
||||
stage,
|
||||
static entity => entity.ServerGuid == 0,
|
||||
_state.StopStaticEntityScript),
|
||||
LandblockRetirementStage.Classification =>
|
||||
ticket.RunOnceStep(
|
||||
stage,
|
||||
() => _state.InvalidateLandblockClassification(
|
||||
ticket.LandblockId)),
|
||||
LandblockRetirementStage.None =>
|
||||
LandblockRetirementOperationResult.NoWork,
|
||||
_ => _advancePresentationStep!(ticket),
|
||||
};
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
ticket.RecordCallbackFailure(error);
|
||||
result = LandblockRetirementOperationResult.Failed;
|
||||
}
|
||||
|
||||
if (ticket.TryTakeNewFailure(out Exception? failure))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"streaming: retirement for 0x{ticket.LandblockId:X8} " +
|
||||
$"({ticket.Kind}) remains pending: {failure}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void RemoveCompletedTicket(LandblockRetirementTicket ticket)
|
||||
{
|
||||
if (!_pendingOrder.TryPeek(out LandblockRetirementTicket? head)
|
||||
|| !ReferenceEquals(head, ticket))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Retirement FIFO completion did not match its active head.");
|
||||
}
|
||||
_pendingOrder.Dequeue();
|
||||
|
||||
uint id = Canonicalize(ticket.LandblockId);
|
||||
if (!_pending.TryGetValue(id, out List<LandblockRetirementTicket>? tickets)
|
||||
|| !tickets.Remove(ticket))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Completed retirement ticket is missing from its owner ledger.");
|
||||
}
|
||||
|
||||
PendingCount--;
|
||||
if (tickets.Count == 0)
|
||||
_pending.Remove(id);
|
||||
}
|
||||
|
||||
private static uint Canonicalize(uint id) =>
|
||||
(id & 0xFFFF0000u) | 0xFFFFu;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue