using AcDream.Core.World; namespace AcDream.App.Streaming; [Flags] public enum LandblockRetirementStage : ushort { None = 0, MeshReferences = 1 << 0, StaticScripts = 1 << 1, Classification = 1 << 2, EntityLighting = 1 << 3, EntityTranslucency = 1 << 4, PluginProjection = 1 << 5, Terrain = 1 << 6, Physics = 1 << 7, CellVisibility = 1 << 8, BuildingRegistry = 1 << 9, EnvironmentCells = 1 << 10, LegacyPresentation = 1 << 11, } internal enum LandblockRetirementOperationResult : byte { NoWork, Progressed, Failed, } /// /// One retryable landblock-retirement ledger. Successful owner operations are /// committed once; a later attempt resumes at the exact failed owner/entity. /// public sealed class LandblockRetirementTicket { private readonly Dictionary _entityCursors = new(); private readonly Dictionary _failures = new(); private Exception? _callbackFailure; private Exception? _lastReportedFailure; internal LandblockRetirementTicket( GpuLandblockRetirement state, LandblockRetirementStage requiredStages) { State = state; RequiredStages = requiredStages; } public GpuLandblockRetirement State { get; } public uint LandblockId => State.LandblockId; public LandblockRetirementKind Kind => State.Kind; public IReadOnlyList Entities => State.Entities; public LandblockRetirementStage RequiredStages { get; } public LandblockRetirementStage CompletedStages { get; private set; } public bool IsComplete => (CompletedStages & RequiredStages) == RequiredStages && _callbackFailure is null; public bool RunOnce(LandblockRetirementStage stage, Action operation) { ValidateSingleStage(stage); ArgumentNullException.ThrowIfNull(operation); if ((CompletedStages & stage) != 0) return true; try { operation(); CompletedStages |= stage; _failures.Remove(stage); return true; } catch (Exception error) { _failures[stage] = error; return false; } } 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 predicate, Action operation) { ValidateSingleStage(stage); ArgumentNullException.ThrowIfNull(predicate); ArgumentNullException.ThrowIfNull(operation); if ((CompletedStages & stage) != 0) return true; _entityCursors.TryGetValue(stage, out int cursor); while (cursor < Entities.Count) { WorldEntity entity = Entities[cursor]; if (predicate(entity)) { try { operation(entity); } catch (Exception error) { _entityCursors[stage] = cursor; _failures[stage] = error; return false; } } cursor++; _entityCursors[stage] = cursor; } CompletedStages |= stage; _entityCursors.Remove(stage); _failures.Remove(stage); return true; } /// /// 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. /// internal LandblockRetirementOperationResult RunEntityStep( LandblockRetirementStage stage, Func predicate, Action 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) => _callbackFailure = error; internal bool TryTakeNewFailure(out Exception? error) { error = _callbackFailure ?? _failures.Values.FirstOrDefault(); if (error is null || ReferenceEquals(error, _lastReportedFailure)) return false; _lastReportedFailure = error; return true; } private static void ValidateSingleStage(LandblockRetirementStage stage) { ushort value = (ushort)stage; if (value == 0 || (value & (value - 1)) != 0) throw new ArgumentOutOfRangeException( nameof(stage), stage, "A retirement operation must own exactly one stage bit."); } } /// /// Pipeline-lifetime owner for landblock retirement. World-state detachment /// commits first; renderer/cache/script cleanup advances afterward from a /// 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. /// public sealed class LandblockRetirementCoordinator { private enum BudgetedAdvanceResult : byte { Progressed, Yielded, Failed, } private enum RequestKind : byte { BeginFull, BeginNearLayer, Advance, } private readonly record struct Request(RequestKind Kind, uint LandblockId); private const LandblockRetirementStage CoreStages = LandblockRetirementStage.MeshReferences | LandblockRetirementStage.StaticScripts | LandblockRetirementStage.Classification; public const LandblockRetirementStage ProductionPresentationStages = LandblockRetirementStage.EntityLighting | LandblockRetirementStage.EntityTranslucency | LandblockRetirementStage.PluginProjection | LandblockRetirementStage.Terrain | LandblockRetirementStage.Physics | LandblockRetirementStage.CellVisibility | LandblockRetirementStage.BuildingRegistry | LandblockRetirementStage.EnvironmentCells; private readonly GpuWorldState _state; private readonly Action _advancePresentation; private readonly Func< LandblockRetirementTicket, LandblockRetirementOperationResult>? _advancePresentationStep; private readonly Func _requiredPresentationStages; private readonly Action? _onDetached; private readonly Dictionary> _pending = new(); private readonly LinkedList _pendingOrder = new(); private readonly Dictionary< LandblockRetirementTicket, LinkedListNode> _pendingNodes = new(ReferenceEqualityComparer.Instance); private readonly List _completedIds = new(); private readonly Queue _requests = new(); private readonly HashSet _seenDuringDrain = new(); private bool _drainingRequests; public LandblockRetirementCoordinator( GpuWorldState state, Action advancePresentation, Func? requiredPresentationStages = null) { ArgumentNullException.ThrowIfNull(state); ArgumentNullException.ThrowIfNull(advancePresentation); _state = state; _advancePresentation = advancePresentation; _advancePresentationStep = null; _onDetached = null; _requiredPresentationStages = requiredPresentationStages ?? (kind => kind == LandblockRetirementKind.Full ? ProductionPresentationStages : ProductionPresentationStages & ~LandblockRetirementStage.Terrain); } private LandblockRetirementCoordinator( GpuWorldState state, Func advancePresentationStep, Action advancePresentation, Func requiredPresentationStages, Action? onDetached) { _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)); _onDetached = onDetached; } public int PendingCount { get; private set; } public bool IsPending(uint landblockId) => _pending.ContainsKey(Canonicalize(landblockId)); internal bool MatchesState(GpuWorldState state) => ReferenceEquals(_state, state); internal bool UsesBudgetedSteps => _advancePresentationStep is not null; /// /// 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. /// internal Exception? AdoptDetachedFull( IReadOnlyList retirements) { ArgumentNullException.ThrowIfNull(retirements); var incomingIds = new HashSet(); 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? 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? failures = null; for (int index = 0; index < retirements.Count; index++) { GpuLandblockRetirement retirement = retirements[index]; uint canonical = Canonicalize(retirement.LandblockId); _pending.TryGetValue( canonical, out List? existing); var ticket = new LandblockRetirementTicket( retirement, required); if (existing is null) { existing = new List(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))); public void BeginNearLayer(uint landblockId) => EnqueueRequest( new Request(RequestKind.BeginNearLayer, Canonicalize(landblockId))); public void Advance() => EnqueueRequest( new Request(RequestKind.Advance, 0u)); internal static LandblockRetirementCoordinator CreateBudgeted( GpuWorldState state, Func advancePresentationStep, Action advancePresentation, Action? onDetached = null) { return new LandblockRetirementCoordinator( state, advancePresentationStep, advancePresentation, kind => kind == LandblockRetirementKind.Full ? ProductionPresentationStages : ProductionPresentationStages & ~LandblockRetirementStage.Terrain, onDetached); } /// /// Advances stable FIFO retirement work under the caller's one frame meter. /// Every reservation maps to exactly one entity cursor or owner stage. /// public void Advance(StreamingWorkMeter meter) { ArgumentNullException.ThrowIfNull(meter); if (_advancePresentationStep is null) { Advance(); return; } while (_pendingOrder.First is { } head) { LandblockRetirementTicket ticket = head.Value; if (ticket.IsComplete) { RemoveCompletedTicket(ticket); continue; } if (AdvanceBudgetedTicket(ticket, meter) != BudgetedAdvanceResult.Progressed) return; if (ticket.IsComplete) RemoveCompletedTicket(ticket); } } /// /// 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. /// 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? 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) { return; } if (ticket.IsComplete) RemoveCompletedTicket(ticket); } } private void AdvanceCore() { if (_advancePresentationStep is not null) { AdvanceBudgetedEagerAttempt(); return; } if (_pending.Count == 0) return; _completedIds.Clear(); foreach ((uint id, List tickets) in _pending) { for (int i = tickets.Count - 1; i >= 0; i--) { LandblockRetirementTicket ticket = tickets[i]; AdvanceTicket(ticket); if (!ticket.IsComplete) continue; tickets.RemoveAt(i); PendingCount--; } if (tickets.Count == 0) _completedIds.Add(id); } for (int i = 0; i < _completedIds.Count; i++) _pending.Remove(_completedIds[i]); } internal static LandblockRetirementCoordinator CreateLegacy( GpuWorldState state, Action? removeTerrain, Action? demoteNearLayer) { LandblockRetirementStage Required(LandblockRetirementKind kind) => kind switch { LandblockRetirementKind.Full when removeTerrain is not null => LandblockRetirementStage.LegacyPresentation, LandblockRetirementKind.NearLayer when demoteNearLayer is not null => LandblockRetirementStage.LegacyPresentation, _ => LandblockRetirementStage.None, }; void AdvanceLegacy(LandblockRetirementTicket ticket) { Action? callback = ticket.Kind == LandblockRetirementKind.Full ? removeTerrain : demoteNearLayer; if (callback is not null) { ticket.RunOnce( LandblockRetirementStage.LegacyPresentation, () => callback(ticket.LandblockId)); } } return new LandblockRetirementCoordinator(state, AdvanceLegacy, Required); } private void BeginCore(uint landblockId, LandblockRetirementKind kind) { uint canonical = Canonicalize(landblockId); if (_pending.TryGetValue(canonical, out List? existing)) { for (int i = 0; i < existing.Count; i++) { if (existing[i].Kind == kind) { LandblockRetirementTicket existingTicket = existing[i]; if (_advancePresentationStep is not null) return; AdvanceTicket(existingTicket); if (existingTicket.IsComplete) { existing.RemoveAt(i); PendingCount--; if (existing.Count == 0) _pending.Remove(canonical); } 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) { existing = new List(1); _pending.Add(canonical, existing); } existing.Add(ticket); PendingCount++; if (_advancePresentationStep is not null) EnqueueBudgetedTicket(ticket); _onDetached?.Invoke(stateRetirement); if (_advancePresentationStep is null) { 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 // 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) { ticket.BeginAttempt(); ticket.RunOnce( LandblockRetirementStage.MeshReferences, () => _state.ReleaseLandblockMeshReferences(ticket.LandblockId)); ticket.RunForEachEntity( LandblockRetirementStage.StaticScripts, static entity => entity.ServerGuid == 0, _state.StopStaticEntityScript); ticket.RunOnce( LandblockRetirementStage.Classification, () => _state.InvalidateLandblockClassification(ticket.LandblockId)); try { _advancePresentation(ticket); } catch (Exception error) { ticket.RecordCallbackFailure(error); } if (ticket.TryTakeNewFailure(out Exception? failure)) { Console.WriteLine( $"streaming: retirement for 0x{ticket.LandblockId:X8} " + $"({ticket.Kind}) remains pending: {failure}"); } } private void AdvanceBudgetedEagerAttempt() { if (_pendingOrder.First is not { } head) return; LandblockRetirementTicket ticket = head.Value; 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 BudgetedAdvanceResult AdvanceBudgetedTicket( LandblockRetirementTicket ticket, StreamingWorkMeter meter) { 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 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 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? 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? 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; 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? 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()).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); } } }