refactor(streaming): own presentation retirement in pipeline
Move full and near-tier presentation retirement out of GameWindow and into a concrete pipeline-owned stage mapper. Harden the retry ledger against reentrancy and committed visibility observer failures, enforce a single state/resource ownership graph, and split concrete versus legacy controller construction so callbacks cannot be silently ignored. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
dc39772bf9
commit
ea7ffbc186
10 changed files with 860 additions and 140 deletions
|
|
@ -136,14 +136,23 @@ public sealed class LandblockRetirementTicket
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Game-window-lifetime owner for landblock retirement. World-state detachment
|
||||
/// Pipeline-lifetime owner for landblock retirement. World-state detachment
|
||||
/// commits first; renderer/cache/script cleanup advances afterward from a
|
||||
/// per-owner ledger. The coordinator is intentionally shared by replacement
|
||||
/// <see cref="StreamingController"/> instances so a quality-setting rebuild
|
||||
/// cannot abandon an unfinished retirement.
|
||||
/// per-owner ledger. Reentrant begin/advance requests are serialized so an
|
||||
/// owner callback cannot recursively replay its active stage or mutate the
|
||||
/// pending-ticket map while it is being enumerated.
|
||||
/// </summary>
|
||||
public sealed class LandblockRetirementCoordinator
|
||||
{
|
||||
private enum RequestKind : byte
|
||||
{
|
||||
BeginFull,
|
||||
BeginNearLayer,
|
||||
Advance,
|
||||
}
|
||||
|
||||
private readonly record struct Request(RequestKind Kind, uint LandblockId);
|
||||
|
||||
private const LandblockRetirementStage CoreStages =
|
||||
LandblockRetirementStage.MeshReferences
|
||||
| LandblockRetirementStage.StaticScripts
|
||||
|
|
@ -165,6 +174,9 @@ public sealed class LandblockRetirementCoordinator
|
|||
_requiredPresentationStages;
|
||||
private readonly Dictionary<uint, List<LandblockRetirementTicket>> _pending = new();
|
||||
private readonly List<uint> _completedIds = new();
|
||||
private readonly Queue<Request> _requests = new();
|
||||
private readonly HashSet<Request> _seenDuringDrain = new();
|
||||
private bool _drainingRequests;
|
||||
|
||||
public LandblockRetirementCoordinator(
|
||||
GpuWorldState state,
|
||||
|
|
@ -186,13 +198,19 @@ public sealed class LandblockRetirementCoordinator
|
|||
public bool IsPending(uint landblockId) =>
|
||||
_pending.ContainsKey(Canonicalize(landblockId));
|
||||
|
||||
public void BeginFull(uint landblockId) =>
|
||||
Begin(landblockId, LandblockRetirementKind.Full);
|
||||
internal bool MatchesState(GpuWorldState state) =>
|
||||
ReferenceEquals(_state, state);
|
||||
|
||||
public void BeginNearLayer(uint landblockId) =>
|
||||
Begin(landblockId, LandblockRetirementKind.NearLayer);
|
||||
public void BeginFull(uint landblockId) => EnqueueRequest(
|
||||
new Request(RequestKind.BeginFull, Canonicalize(landblockId)));
|
||||
|
||||
public void Advance()
|
||||
public void BeginNearLayer(uint landblockId) => EnqueueRequest(
|
||||
new Request(RequestKind.BeginNearLayer, Canonicalize(landblockId)));
|
||||
|
||||
public void Advance() => EnqueueRequest(
|
||||
new Request(RequestKind.Advance, 0u));
|
||||
|
||||
private void AdvanceCore()
|
||||
{
|
||||
if (_pending.Count == 0)
|
||||
return;
|
||||
|
|
@ -250,7 +268,7 @@ public sealed class LandblockRetirementCoordinator
|
|||
return new LandblockRetirementCoordinator(state, AdvanceLegacy, Required);
|
||||
}
|
||||
|
||||
private void Begin(uint landblockId, LandblockRetirementKind kind)
|
||||
private void BeginCore(uint landblockId, LandblockRetirementKind kind)
|
||||
{
|
||||
uint canonical = Canonicalize(landblockId);
|
||||
if (_pending.TryGetValue(canonical, out List<LandblockRetirementTicket>? existing))
|
||||
|
|
@ -259,20 +277,52 @@ public sealed class LandblockRetirementCoordinator
|
|||
{
|
||||
if (existing[i].Kind == kind)
|
||||
{
|
||||
AdvanceTicket(existing[i]);
|
||||
LandblockRetirementTicket existingTicket = existing[i];
|
||||
AdvanceTicket(existingTicket);
|
||||
if (existingTicket.IsComplete)
|
||||
{
|
||||
existing.RemoveAt(i);
|
||||
PendingCount--;
|
||||
if (existing.Count == 0)
|
||||
_pending.Remove(canonical);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GpuLandblockRetirement? stateRetirement = kind == LandblockRetirementKind.Full
|
||||
? _state.DetachLandblock(canonical)
|
||||
: _state.DetachNearLayer(canonical);
|
||||
if (stateRetirement is null)
|
||||
return;
|
||||
|
||||
LandblockRetirementStage required =
|
||||
CoreStages | _requiredPresentationStages(kind);
|
||||
GpuLandblockRetirement? stateRetirement = null;
|
||||
Exception? detachmentObserverFailure = null;
|
||||
GpuWorldState.MutationBatch mutation = _state.BeginMutationBatch();
|
||||
try
|
||||
{
|
||||
// The detach methods create a nested batch. Holding this outer
|
||||
// transaction lets us retain their returned teardown context
|
||||
// before visibility observers run at the outer commit boundary.
|
||||
stateRetirement = kind == LandblockRetirementKind.Full
|
||||
? _state.DetachLandblock(canonical)
|
||||
: _state.DetachNearLayer(canonical);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
mutation.Dispose();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
detachmentObserverFailure = error;
|
||||
}
|
||||
}
|
||||
if (stateRetirement is null)
|
||||
{
|
||||
if (detachmentObserverFailure is not null)
|
||||
throw detachmentObserverFailure;
|
||||
return;
|
||||
}
|
||||
|
||||
var ticket = new LandblockRetirementTicket(stateRetirement, required);
|
||||
if (existing is null)
|
||||
{
|
||||
|
|
@ -290,6 +340,13 @@ public sealed class LandblockRetirementCoordinator
|
|||
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
|
||||
// retained as replayable work. Complete the presentation teardown
|
||||
// from the preserved detach receipt, then surface that committed-edge
|
||||
// failure to the caller without inventing a one-frame pending fence.
|
||||
if (detachmentObserverFailure is not null)
|
||||
throw detachmentObserverFailure;
|
||||
}
|
||||
|
||||
private void AdvanceTicket(LandblockRetirementTicket ticket)
|
||||
|
|
@ -325,4 +382,70 @@ public sealed class LandblockRetirementCoordinator
|
|||
|
||||
private static uint Canonicalize(uint id) =>
|
||||
(id & 0xFFFF0000u) | 0xFFFFu;
|
||||
|
||||
private void EnqueueRequest(Request request)
|
||||
{
|
||||
if (_drainingRequests)
|
||||
{
|
||||
if (_seenDuringDrain.Add(request))
|
||||
_requests.Enqueue(request);
|
||||
return;
|
||||
}
|
||||
|
||||
_drainingRequests = true;
|
||||
_seenDuringDrain.Clear();
|
||||
_requests.Clear();
|
||||
_seenDuringDrain.Add(request);
|
||||
_requests.Enqueue(request);
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
while (_requests.TryDequeue(out Request pending))
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (pending.Kind)
|
||||
{
|
||||
case RequestKind.BeginFull:
|
||||
BeginCore(
|
||||
pending.LandblockId,
|
||||
LandblockRetirementKind.Full);
|
||||
break;
|
||||
case RequestKind.BeginNearLayer:
|
||||
BeginCore(
|
||||
pending.LandblockId,
|
||||
LandblockRetirementKind.NearLayer);
|
||||
break;
|
||||
case RequestKind.Advance:
|
||||
AdvanceCore();
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unknown retirement request {pending.Kind}.");
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= new List<Exception>()).Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_requests.Clear();
|
||||
_seenDuringDrain.Clear();
|
||||
_drainingRequests = false;
|
||||
}
|
||||
|
||||
if (failures is { Count: 1 })
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo
|
||||
.Capture(failures[0])
|
||||
.Throw();
|
||||
if (failures is { Count: > 1 })
|
||||
{
|
||||
throw new AggregateException(
|
||||
"One or more serialized landblock retirement requests failed.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue