using System; using System.Collections.Generic; using AcDream.App.Rendering.Wb; using AcDream.Core.Terrain; using AcDream.Core.World; namespace AcDream.App.Streaming; /// /// Called once per frame from GameWindow.OnUpdate. Owns the /// and uses delegates into /// so tests can inject fakes. All work /// happens on the render thread; the streamer itself is background. /// /// /// Threading: not thread-safe. All calls must happen on the render thread. /// /// public sealed class StreamingController : IStreamingFrameBackend { private sealed class OriginRecenterRetirement { public bool RadiiConverged; public bool GenerationAdvanced; public bool PendingLoadsCleared; public bool DeferredApplyCleared; public bool RegionCleared; public List? ResidentIds; public int RetirementCursor; public bool PreparationCommitted; public (int X, int Y, bool IsSealedDungeon)? Destination; public bool DestinationConfigured; public bool DestinationLoadEnqueued; } private readonly Action _enqueueLoad; private readonly Action _enqueueUnload; private readonly Func> _drainCompletions; private readonly Action? _clearPendingLoads; private readonly LandblockPresentationPipeline _presentation; private readonly GpuWorldState _state; private StreamingRegion? _region; private RadiiReconfiguration? _pendingRadiiReconfiguration; private bool _advancingRadiiReconfiguration; private (int NearRadius, int FarRadius)? _deferredRadiiRequest; private OriginRecenterRetirement? _originRecenterRetirement; private bool _advancingOriginRecenter; // Hard streaming boundaries advance this token. Worker completions carry // the generation captured at enqueue time, so an old overlapping load or // unload cannot mutate the replacement window after a portal recenter. private ulong _generation; // True while streaming is collapsed to the single dungeon landblock the // player stands in (the dungeon gate, #133 FPS). AC dungeons have NO // adjacent landblocks — neighbors are unrelated ocean-grid dungeons that // are never visible, so we stop loading the 25×25 window entirely. private bool _collapsed; // The dungeon landblock id we collapsed onto. Once collapsed we key the // gate on this STABLE landblock, not the per-frame insideDungeon signal: // CurrCell can momentarily resolve to null/outdoor mid-frame, and gating // expand on that flicker thrashes collapse↔expand (reload storms + a light // leak). We only expand when the observer actually moves to a different // landblock (teleport/portal out). private uint _collapsedCenter; /// /// Near-tier radius (LBs from observer that load full detail: terrain + /// scenery + entities). Runtime changes go through /// . /// public int NearRadius { get; private set; } /// /// Far-tier radius (LBs from observer that load terrain only). /// public int FarRadius { get; private set; } /// /// Cap on completions drained per call. The cap is /// the GPU upload budget for one frame: terrain mesh + per-entity GfxObj /// sub-mesh uploads + texture uploads for one landblock take a few ms; /// applying 25 of them in a single frame produces a memory spike /// (observed: out-of-memory crash on the 5×5 first-frame load). /// /// /// 4 is the original async-streamer value; it spreads a 5×5 first-frame /// load over ~7 frames (~116ms at 60fps), which is below the human /// perception threshold. Spawn races that previously dropped entities /// while landblocks were in flight are now handled by /// 's pending-spawn list, so spreading /// completions doesn't lose any data. /// /// public int MaxCompletionsPerFrame { get; set; } = 4; /// /// #138: the teleport destination landblock id. When non-zero, /// applies this landblock's /// or /// completion immediately /// — even if it sits past the /// position in the outbox — so the player can materialise at the /// destination without waiting for every earlier-queued landblock to /// drain first. Non-priority completions drained past it are buffered /// in and applied over subsequent frames, /// so no completions are lost and there is no GPU spike. /// /// Set by GameWindow when a teleport starts; reset to 0 /// once the destination landblock has been applied (or when the /// teleport destination changes). /// public uint PriorityLandblockId { get; set; } /// /// 2026-06-22: the radius (in landblocks, Chebyshev) around /// that eager-applies /// ahead of the per-frame budget. 0 (default) = only the single center landblock — the /// original priority behaviour. A teleport sets this to the near ring so the player's /// IMMEDIATE SURROUNDINGS (terrain + collision + scenery) are resident on arrival, not /// just the one landblock they stand on. Without it, only the destination landblock /// applies immediately and everything around it drains at , /// so the player arrives to a near-empty world (the "Fort Tethana only one landblock /// loaded" symptom) and their cell-walk can't root into neighbour cells yet (the /// transient walk-through-walls). The far ring still drains at the budget. The eager /// apply runs while the portal viewport replaces the world, so the GPU spike is hidden. /// public int PriorityRadius { get; set; } // [FRAME-DIAG] (read by GameWindow's ACDREAM_WB_DIAG rollup): the standing // deferred-LOAD backlog. A non-zero value during/after a teleport is the // GPU-upload tail draining at MaxCompletionsPerFrame. Removable probe surface. public int DeferredApplyBacklog => _deferredApply.Count; public int PendingRetirementCount => _presentation.PendingRetirementCount; internal bool IsCollapsedToDungeon => _collapsed; /// /// True once every in-bounds landblock in the requested Chebyshev ring has /// crossed the render-thread publication barrier. Worker completion and /// world-state registration are not sufficient: all static GfxObj and /// EnvCell shell meshes must have completed their render-thread upload. /// Portal-space exit uses this alongside physics residency so the world /// cannot be revealed while its render slots are still absent. /// public bool IsRenderNeighborhoodResident(uint cellOrLandblockId, int radius) { if (radius < 0) throw new ArgumentOutOfRangeException(nameof(radius)); // LandDefs::InboundValidCellId validates both map axes and the low-word // class (outdoor cell, EnvCell, or canonical landblock sentinel). if (!AcDream.Core.Physics.LandDefs.InboundValidCellId(cellOrLandblockId)) return false; int cx = (int)((cellOrLandblockId >> 24) & 0xFFu); int cy = (int)((cellOrLandblockId >> 16) & 0xFFu); for (int dx = -radius; dx <= radius; dx++) for (int dy = -radius; dy <= radius; dy++) { int nx = cx + dx; int ny = cy + dy; // Match PhysicsEngine.IsNeighborhoodTerrainResident: the outer // 0xFF coordinate has no loadable neighbour beyond it. if (nx < 0 || nx > 254 || ny < 0 || ny > 254) continue; uint canonical = ((uint)nx << 24) | ((uint)ny << 16) | 0xFFFFu; if (!_state.IsNearTier(canonical) || !_state.IsRenderReady(canonical)) return false; } return true; } // [FRAME-DIAG]: full-window presentation retirements include explicit // reloads and shared-origin recenter barriers. Both retire and later // re-upload the complete resident window. public int FullWindowRetirementCount { get; private set; } public int LastFullWindowRetirementLandblockCount { get; private set; } // Completions that were drained past a priority item get buffered here // so they still apply over subsequent frames without loss. private readonly List _deferredApply = new(); /// /// Internal compatibility seam for hermetic controller policy tests. Live /// composition cannot supply presentation callbacks; it must use the /// concrete pipeline constructor below. /// internal StreamingController( Action enqueueLoad, Action enqueueUnload, Func> drainCompletions, Action applyTerrain, GpuWorldState state, int nearRadius, int farRadius, Action? removeTerrain = null, Action? demoteNearLayer = null, Action? clearPendingLoads = null, Action? onLandblockLoaded = null, Action? ensureEnvCellMeshes = null, LandblockRetirementCoordinator? retirementCoordinator = null) : this( enqueueLoad, enqueueUnload, drainCompletions, state, nearRadius, farRadius, new LandblockPresentationPipeline( applyTerrain, state, onLandblockLoaded, ensureEnvCellMeshes, retirementCoordinator, removeTerrain, demoteNearLayer), clearPendingLoads) { } /// /// Concrete presentation path. Its pipeline is the sole presentation and /// retirement owner, so no legacy publication delegate can be supplied or /// silently ignored. /// public StreamingController( Action enqueueLoad, Action enqueueUnload, Func> drainCompletions, GpuWorldState state, int nearRadius, int farRadius, LandblockPresentationPipeline presentationPipeline, Action? clearPendingLoads = null) { _enqueueLoad = enqueueLoad; _enqueueUnload = enqueueUnload; _drainCompletions = drainCompletions; _clearPendingLoads = clearPendingLoads; _state = state; _presentation = presentationPipeline ?? throw new ArgumentNullException(nameof(presentationPipeline)); if (!_presentation.MatchesState(_state)) { throw new ArgumentException( "The presentation pipeline must own the controller's world state.", nameof(presentationPipeline)); } NearRadius = nearRadius; FarRadius = farRadius; } /// /// Reconciles an active outdoor streaming window to new quality radii /// without replacing the worker/controller or blindly bootstrapping data /// already published in . Pending jobs from the /// old window are invalidated by generation; resident blocks are retained, /// promoted, demoted, loaded, or unloaded from their actual current tier. /// A sealed dungeon remains radius-zero until its normal exit edge while /// remembering the new outdoor radii for that exit. /// public void ReconfigureRadii(int nearRadius, int farRadius) { if (nearRadius < 0) throw new ArgumentOutOfRangeException(nameof(nearRadius)); if (farRadius < nearRadius) throw new ArgumentOutOfRangeException( nameof(farRadius), "Far radius must be greater than or equal to near radius."); // A radius transaction may not admit work after an origin-recenter // snapshot has been captured. Retain only the latest requested radii; // the first Tick after origin commit applies them to the new frame. if (_originRecenterRetirement is not null) { _deferredRadiiRequest = (nearRadius, farRadius); return; } // Queue callbacks are injected seams and can synchronously reenter the // controller. Last-request-wins deferral keeps the active mutation // cursor stable; the request is applied after the current transaction // converges instead of recursively replaying its active step. if (_advancingRadiiReconfiguration) { _deferredRadiiRequest = (nearRadius, farRadius); return; } // A prior callback may have failed after this controller accepted the // request. Resume that exact transaction before considering another // target; replacing it would orphan already-admitted generation work. if (_pendingRadiiReconfiguration is not null) AdvanceRadiiReconfiguration(); // This explicit call is newer than any request deferred by a prior // failed attempt and therefore supersedes it. _deferredRadiiRequest = null; if (nearRadius == NearRadius && farRadius == FarRadius) return; // A staged old-window publication must either finish or fail before // the desired-tier snapshot below is computed. Otherwise a landblock // that becomes spatially resident during convergence is absent from // the reconfiguration mutation ledger. ConvergePendingPublications(); if (_collapsed || _region is null) { NearRadius = nearRadius; FarRadius = farRadius; return; } var rebuilt = new StreamingRegion( _region.CenterX, _region.CenterY, nearRadius, farRadius); TwoTierDiff bootstrap = rebuilt.ComputeFirstTickDiff(); var desiredNear = new HashSet(bootstrap.ToLoadNear); var desiredFar = new HashSet(bootstrap.ToLoadFar); var mutations = new List(); uint[] loaded = [.. _state.LoadedLandblockIds]; for (int i = 0; i < loaded.Length; i++) { uint id = loaded[i]; if (desiredNear.Contains(id)) { if (!_state.IsNearTier(id)) mutations.Add(new RadiiMutation( () => EnqueueLoad(id, LandblockStreamJobKind.PromoteToNear))); } else if (desiredFar.Contains(id)) { if (_state.IsNearTier(id)) mutations.Add(new RadiiMutation(() => DemoteLandblock(id))); } else { mutations.Add(new RadiiMutation(() => EnqueueUnload(id))); } } foreach (uint id in desiredNear) if (!_state.IsLoaded(id)) mutations.Add(new RadiiMutation( () => EnqueueLoad(id, LandblockStreamJobKind.LoadNear))); foreach (uint id in desiredFar) if (!_state.IsLoaded(id)) mutations.Add(new RadiiMutation( () => EnqueueLoad(id, LandblockStreamJobKind.LoadFar))); _pendingRadiiReconfiguration = new RadiiReconfiguration( nearRadius, farRadius, rebuilt, mutations); AdvanceRadiiReconfiguration(); } /// /// Compatibility constructor for deterministic tests and callers that do /// not own an asynchronous worker. Production must use the generation-aware /// overload above so hard-recenter results can be rejected by incarnation. /// internal StreamingController( Action enqueueLoad, Action enqueueUnload, Func> drainCompletions, Action applyTerrain, GpuWorldState state, int nearRadius, int farRadius, Action? removeTerrain = null, Action? demoteNearLayer = null, Action? clearPendingLoads = null, Action? onLandblockLoaded = null, Action? ensureEnvCellMeshes = null, LandblockRetirementCoordinator? retirementCoordinator = null) : this( (id, kind, _) => enqueueLoad(id, kind), (id, _) => enqueueUnload(id), drainCompletions, applyTerrain, state, nearRadius, farRadius, removeTerrain, demoteNearLayer, clearPendingLoads, onLandblockLoaded, ensureEnvCellMeshes, retirementCoordinator) { } private void EnqueueLoad(uint id, LandblockStreamJobKind kind) => _enqueueLoad(id, kind, _generation); private void EnqueueUnload(uint id) => _enqueueUnload(id, _generation); private void DemoteLandblock(uint id) { uint canonical = (id & 0xFFFF0000u) | 0xFFFFu; _presentation.BeginNearLayerRetirement(canonical); } private void AdvanceGeneration() { ConvergePendingPublications(); _generation = unchecked(_generation + 1); } private void ConvergePendingPublications() { IReadOnlyList pending = _presentation.GetPendingPublicationResults(); if (pending.Count == 0) return; try { for (int i = 0; i < pending.Count; i++) _presentation.ResumePublication(pending[i]); } finally { // A completed staged result can still be present in the deferred // FIFO that originally retained it. Remove only those exact // objects; unrelated accepted completions keep their order. _deferredApply.RemoveAll(result => { for (int i = 0; i < pending.Count; i++) { if (ReferenceEquals(result, pending[i])) return !_presentation.HasPendingPublication(result); } return false; }); } } /// /// Advance one frame. / /// are landblock coordinates (0..255) of the current viewer — the camera /// in offline mode, the server-sent player position in live. /// /// Two-tier model (Phase A.5 T13): /// /// → enqueue LoadFar (terrain only, no entities) /// → enqueue LoadNear (terrain + entities) /// → enqueue PromoteToNear (entity layer for already-loaded terrain) /// → drop entities on render thread immediately (terrain stays) /// → enqueue full unload /// /// public void Tick(int observerCx, int observerCy, bool insideDungeon = false) { if (_originRecenterRetirement is not null) { _presentation.AdvanceRetirements(); return; } if (_pendingRadiiReconfiguration is not null) AdvanceRadiiReconfiguration(); else if (_deferredRadiiRequest is { } deferred) { _deferredRadiiRequest = null; ReconfigureRadii(deferred.NearRadius, deferred.FarRadius); } // Presentation-owner failures never roll back spatial state. Resume // unfinished owner ledgers before accepting replacement publications. _presentation.AdvanceRetirements(); // Complete an admitted publication before this frame can mutate its // desired tier or spatial residence. A later recenter may then demote // or retire the fully known owner set through the normal ledger. ConvergePendingPublications(); uint centerId = StreamingRegion.EncodeLandblockId(observerCx, observerCy); if (_collapsed) { // Hysteresis. Cases: // - Still in the SAME dungeon landblock → hold (sweep stragglers). // - In a DIFFERENT dungeon cell (multi-landblock dungeon / new dungeon) // → re-collapse onto it. // - CurrCell flickered null but the player hasn't gone anywhere: the // observer landblock reverts to the position-derived value, which for a // dungeon is only ever the ADJACENT off-by-one landblock (negative cell- // local Y). Hold — never expand on an adjacent flicker. // - Genuinely left to a DISTANT landblock (portal/teleport out, always far // from the ocean-grid dungeon block) → expand. if (insideDungeon && centerId != _collapsedCenter) EnterDungeonCollapse(observerCx, observerCy, centerId); else if (!insideDungeon && ChebyshevLandblocks(centerId, _collapsedCenter) > 1) ExitDungeonExpand(observerCx, observerCy); else SweepCollapsed(); } else if (insideDungeon) { EnterDungeonCollapse(observerCx, observerCy, centerId); } else { NormalTick(observerCx, observerCy); } DrainAndApply(); } private void AdvanceRadiiReconfiguration() { if (_advancingRadiiReconfiguration) return; RadiiReconfiguration pending = _pendingRadiiReconfiguration ?? throw new InvalidOperationException("No radii reconfiguration is pending."); var failures = new List(); _advancingRadiiReconfiguration = true; try { if (!pending.GenerationAdvanced) { AdvanceGeneration(); pending.GenerationAdvanced = true; } if (!pending.PendingLoadsCleared) { try { _clearPendingLoads?.Invoke(); pending.PendingLoadsCleared = true; } catch (Exception error) { if (error is StreamingMutationException { MutationCommitted: true }) pending.PendingLoadsCleared = true; failures.Add(error); } } // Do not admit new-generation work until cancellation of the old // inbox is durable. Otherwise a successful retry of ClearPendingLoads // would also erase mutations already marked complete by this ledger. for (int i = 0; pending.PendingLoadsCleared && i < pending.Mutations.Count; i++) { RadiiMutation mutation = pending.Mutations[i]; if (mutation.Completed) continue; try { mutation.Apply(); mutation.Completed = true; } catch (Exception error) { if (error is StreamingMutationException { MutationCommitted: true }) mutation.Completed = true; failures.Add(error); } } bool converged = pending.PendingLoadsCleared && pending.Mutations.All(static mutation => mutation.Completed); if (converged) { // Publish the new logical window only after every queue/retirement // admission is durable. A later Tick can now trust Resident as the // complete desired set and will never strand a hole. pending.Region.MarkResidentFromBootstrap(); _region = pending.Region; NearRadius = pending.NearRadius; FarRadius = pending.FarRadius; _deferredApply.Clear(); _pendingRadiiReconfiguration = null; } } finally { _advancingRadiiReconfiguration = false; } if (failures.Count != 0) { throw new AggregateException( "Streaming quality reconfiguration did not complete cleanly.", failures); } if (_pendingRadiiReconfiguration is null && _deferredRadiiRequest is { } deferred) { _deferredRadiiRequest = null; ReconfigureRadii(deferred.NearRadius, deferred.FarRadius); } } private sealed class RadiiReconfiguration { public RadiiReconfiguration( int nearRadius, int farRadius, StreamingRegion region, List mutations) { NearRadius = nearRadius; FarRadius = farRadius; Region = region; Mutations = mutations; } public int NearRadius { get; } public int FarRadius { get; } public StreamingRegion Region { get; } public List Mutations { get; } public bool GenerationAdvanced { get; set; } public bool PendingLoadsCleared { get; set; } } private sealed class RadiiMutation { public RadiiMutation(Action apply) => Apply = apply; public Action Apply { get; } public bool Completed { get; set; } } /// /// #135: collapse to a single dungeon landblock IMMEDIATELY, before the first /// has a chance to bootstrap the full 25×25 window. Called /// from the login / teleport spawn path the instant the streaming center is /// recentered onto a SEALED dungeon landblock. /// /// The per-frame insideDungeon gate keys on the physics /// CurrCell, which is only set once the player is PLACED — and placement /// waits for the dungeon landblock to hydrate. So for the whole hydration window /// (tens of seconds for a ~200-cell dungeon) the gate reads false and /// would enqueue the ~24 unrelated ocean-grid neighbor /// dungeons (+ ~19k entities each); the collapse then only mops them up after /// placement. That mop-up is the 10→high FPS ramp users see at a dungeon login. /// /// Pre-collapsing means the EXPENSIVE dungeon-neighbour window is never /// enqueued. On teleport nothing is enqueued at all (this fires before the next /// Tick recenters). On login a brief Holtburg outdoor window may be enqueued by the /// frame-1 NormalTick (before the player's spawn arrives) and is immediately /// cancelled by _clearPendingLoads here — cheap outdoor terrain, not the /// ocean-grid dungeons, and a handful of already-dequeued loads get swept next /// frame. Idempotent: a no-op when already collapsed onto this same landblock, so a /// re-sent spawn or a same-frame double call costs nothing. Render-thread only, /// same as . /// public void InitializeKnownLoginCenter(int cx, int cy, bool isSealedDungeon) { // StreamingReadinessGate keeps the worker stopped until the player's // real spawn supplies this center. There is therefore no guessed-center // window to force-reload here. A sealed dungeon is the one special case: // pin radius zero before the first Tick so no ocean-grid neighbours are // ever enqueued. if (isSealedDungeon) PreCollapseToDungeon(cx, cy); } /// /// Pins streaming to a sealed dungeon before the first normal Tick. /// public void PreCollapseToDungeon(int cx, int cy) { uint centerId = StreamingRegion.EncodeLandblockId(cx, cy); if (_collapsed && _collapsedCenter == centerId) return; EnterDungeonCollapse(cx, cy, centerId); } /// /// Outdoor / building-interior streaming — the original two-tier model. /// private void NormalTick(int observerCx, int observerCy) { if (_region is null) { _region = new StreamingRegion(observerCx, observerCy, NearRadius, FarRadius); var bootstrap = _region.ComputeFirstTickDiff(); foreach (var id in bootstrap.ToLoadNear) EnqueueLoad(id, 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); 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); } } /// /// Dungeon-entry edge: cancel the in-flight window load, unload every /// resident neighbor, and pin streaming to the player's single dungeon /// landblock. Retail-faithful — AC dungeons have no adjacent landblocks /// (ACE LandblockManager.GetAdjacentIDs returns empty for a dungeon); /// the 25×25 window was pulling in ~129 unrelated ocean-grid dungeons and /// their thousands of emitters (#133 FPS). Unloading them also tears down /// their lights, shrinking the static-light set toward retail's ≤40. /// private void EnterDungeonCollapse(int cx, int cy, uint centerId) { bool logTransition = !_collapsed || _collapsedCenter != centerId; if (logTransition) Console.WriteLine($"streaming: dungeon collapse -> 0x{centerId:X8}"); AdvanceGeneration(); _collapsed = true; _collapsedCenter = centerId; _clearPendingLoads?.Invoke(); _deferredApply.Clear(); _region = null; foreach (var id in _state.LoadedLandblockIds) if (id != centerId) EnqueueUnload(id); // Pin a radius-0 region so RecenterTo never re-expands while inside, // and so the post-exit rebuild starts from a clean, consistent state. _region = new StreamingRegion(cx, cy, 0, 0); _region.MarkResidentFromBootstrap(); // The dungeon landblock itself must be (or become) loaded. If a prior // ClearPendingLoads cancelled its queued load, re-enqueue it. if (!_state.IsLoaded(centerId)) EnqueueLoad(centerId, LandblockStreamJobKind.LoadNear); else if (!_state.IsNearTier(centerId)) EnqueueLoad(centerId, LandblockStreamJobKind.PromoteToNear); } /// /// While collapsed, unload any landblock that finished loading after the /// collapse edge — a Load the worker had already dequeued before the /// control job took /// effect. At steady state only the dungeon landblock is resident, so this /// is a no-op. /// private void SweepCollapsed() { // Always preserve the true dungeon landblock (_collapsedCenter), never the // per-frame observer landblock — a CurrCell flicker must not unload the dungeon. foreach (var id in _state.LoadedLandblockIds) if (id != _collapsedCenter) EnqueueUnload(id); } /// Chebyshev distance in landblock cells between two landblock ids. private static int ChebyshevLandblocks(uint a, uint b) { int ax = (int)((a >> 24) & 0xFFu), ay = (int)((a >> 16) & 0xFFu); int bx = (int)((b >> 24) & 0xFFu), by = (int)((b >> 16) & 0xFFu); return Math.Max(Math.Abs(ax - bx), Math.Abs(ay - by)); } /// /// True when is the priority center or within /// landblocks of it (Chebyshev) — i.e. inside the teleport /// near ring that eager-applies. With the default radius 0 /// this reduces to an exact match on (the original /// single-landblock priority behaviour). /// private bool IsWithinPriorityRing(uint id) => PriorityLandblockId != 0u && ChebyshevLandblocks(id, PriorityLandblockId) <= PriorityRadius; /// /// Dungeon-exit edge (portal to outdoors / teleport): rebuild the full /// two-tier window at the new center and unload anything resident from the /// collapsed state that falls outside it. /// private void ExitDungeonExpand(int observerCx, int observerCy) { Console.WriteLine( $"streaming: dungeon EXIT-expand -> ({observerCx},{observerCy}) " + $"(was collapsed on 0x{_collapsedCenter:X8})"); AdvanceGeneration(); _collapsed = false; var rebuilt = new StreamingRegion(observerCx, observerCy, NearRadius, FarRadius); foreach (var id in _state.LoadedLandblockIds) 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); foreach (var id in boot.ToLoadFar) if (!_state.IsLoaded(id)) EnqueueLoad(id, LandblockStreamJobKind.LoadFar); rebuilt.MarkResidentFromBootstrap(); _region = rebuilt; } /// /// Starts a reload of the current streaming window. Logical residency is /// detached immediately; render, physics, and static-lighting owners then /// converge through the retained presentation-retirement ledger before /// bootstraps the window again. /// Shared-origin teleports use instead so /// every old-window owner retires while the old coordinate frame is active. /// public void ForceReloadWindow() { BeginFullWindowRetirement(); } /// /// Starts the old-window half of a shared-origin recenter. All accepted /// worker work is invalidated and every resident landblock is detached /// through the retryable presentation ledger while the old coordinate /// frame is still active. remains blocked until /// succeeds. /// internal void BeginOriginRecenter() { _originRecenterRetirement ??= new OriginRecenterRetirement(); } /// /// Advances every retained old-window teardown and reports whether the /// composition root may safely change the shared world origin. /// internal bool IsOriginRecenterRetirementComplete() { if (_originRecenterRetirement is null) throw new InvalidOperationException( "No streaming-origin recenter transaction is pending."); if (!TryAdvanceOriginRecenterPreparation()) return false; _presentation.AdvanceRetirements(); return _presentation.PendingRetirementCount == 0; } /// /// 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. /// internal bool TryCommitOriginRecenter( int destinationX, int destinationY, bool isSealedDungeon) { if (_advancingOriginRecenter) return false; _advancingOriginRecenter = true; try { return TryCommitOriginRecenterCore( destinationX, destinationY, isSealedDungeon); } finally { _advancingOriginRecenter = false; } } private bool TryCommitOriginRecenterCore( int destinationX, int destinationY, bool isSealedDungeon) { OriginRecenterRetirement transaction = _originRecenterRetirement ?? throw new InvalidOperationException( "No streaming-origin recenter transaction is pending."); 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) { throw new InvalidOperationException( "A recenter transaction cannot commit two different destinations."); } transaction.Destination ??= destination; if (!transaction.DestinationConfigured) { _collapsed = isSealedDungeon; _collapsedCenter = isSealedDungeon ? StreamingRegion.EncodeLandblockId(destinationX, destinationY) : 0u; if (isSealedDungeon) { _region = new StreamingRegion( destinationX, destinationY, nearRadius: 0, farRadius: 0); _region.MarkResidentFromBootstrap(); } else { _region = null; } transaction.DestinationConfigured = true; } if (isSealedDungeon && !transaction.DestinationLoadEnqueued) { try { EnqueueLoad( StreamingRegion.EncodeLandblockId(destinationX, destinationY), LandblockStreamJobKind.LoadNear); transaction.DestinationLoadEnqueued = true; } catch (StreamingMutationException error) when (error.MutationCommitted) { transaction.DestinationLoadEnqueued = true; Console.WriteLine( $"streaming: committed dungeon recenter enqueue reported failure: {error}"); return false; } catch (Exception error) { Console.WriteLine( $"streaming: dungeon recenter enqueue will resume: {error}"); return false; } } _originRecenterRetirement = null; return true; } /// /// Releases a fully retired origin transaction at a session boundary /// without bootstrapping a destination from the ending session. /// internal bool TryCancelOriginRecenter() { if (_originRecenterRetirement is not { PreparationCommitted: true }) return false; if (_presentation.PendingRetirementCount != 0) return false; _collapsed = false; _collapsedCenter = 0u; _region = null; _originRecenterRetirement = null; return true; } private bool TryAdvanceOriginRecenterPreparation() { OriginRecenterRetirement transaction = _originRecenterRetirement ?? throw new InvalidOperationException( "No streaming-origin recenter transaction is pending."); if (transaction.PreparationCommitted) return true; if (_advancingOriginRecenter) return false; _advancingOriginRecenter = true; try { if (!transaction.RadiiConverged) { if (_pendingRadiiReconfiguration is not null) AdvanceRadiiReconfiguration(); if (_pendingRadiiReconfiguration is not null) return false; transaction.RadiiConverged = true; } if (!transaction.GenerationAdvanced) { AdvanceGeneration(); transaction.GenerationAdvanced = true; } if (!transaction.PendingLoadsCleared) { try { _clearPendingLoads?.Invoke(); transaction.PendingLoadsCleared = true; } catch (StreamingMutationException error) when (error.MutationCommitted) { transaction.PendingLoadsCleared = true; Console.WriteLine( $"streaming: committed pending-load clear reported failure: {error}"); return false; } } if (!transaction.DeferredApplyCleared) { _deferredApply.Clear(); transaction.DeferredApplyCleared = true; } if (!transaction.RegionCleared) { _collapsed = false; _region = null; transaction.RegionCleared = true; } if (transaction.ResidentIds is null) { transaction.ResidentIds = new List(_state.LoadedLandblockIds); FullWindowRetirementCount++; LastFullWindowRetirementLandblockCount = transaction.ResidentIds.Count; } while (transaction.RetirementCursor < transaction.ResidentIds.Count) { uint id = transaction.ResidentIds[transaction.RetirementCursor]; try { _presentation.BeginFullRetirement(id); transaction.RetirementCursor++; } 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++; Console.WriteLine( $"streaming: origin-recenter retirement for 0x{id:X8} " + $"will resume: {error}"); return false; } } transaction.PreparationCommitted = true; return true; } catch (Exception error) { Console.WriteLine( $"streaming: origin-recenter preparation will resume: {error}"); return false; } finally { _advancingOriginRecenter = false; } } private void BeginFullWindowRetirement() { AdvanceGeneration(); _collapsed = false; _clearPendingLoads?.Invoke(); _deferredApply.Clear(); // Commit the old region boundary before any presentation owner is // advanced. New same-id publications are fenced by the presentation // pipeline's retryable retirement ledger. _region = null; // Snapshot — RemoveLandblock mutates the loaded set we're iterating. var ids = new List(_state.LoadedLandblockIds); FullWindowRetirementCount++; // [FRAME-DIAG] churn counter LastFullWindowRetirementLandblockCount = ids.Count; // upcoming re-upload volume foreach (var id in ids) _presentation.BeginFullRetirement(id); } /// /// Apply streamed completions for this frame. LOADS (terrain mesh GPU uploads) are the /// expensive part, so they are metered at to avoid a /// GPU-upload spike; the overflow buffers in and drains over /// subsequent frames. UNLOADS are cheap (they free GPU buffers — no upload) and are applied /// IMMEDIATELY, never throttled: a teleport produces a whole window of unloads (~600), and /// metering them at the load rate left the previous location's terrain resident for seconds /// (rendering at its old world position as "floating terrain at the horizon"), and rapid /// hops accumulated them faster than they cleared — a runaway resident count (951 observed /// vs a 625 window) that also dragged FPS. Loads inside the teleport near ring /// (, applied during portal travel) likewise bypass the budget so the /// player materialises in a loaded world. /// private void DrainAndApply() { // --- Step 1: drain the outbox in bounded chunks. Apply unloads + priority near-ring // loads immediately; defer every other (budget-metered) load. Draining the whole // outbox each frame (bounded by MaxDrainIterations) is what lets unloads flush // promptly regardless of the load backlog — the throttle is on GPU UPLOADS, not on // freeing them. The drain cap must NOT be gated behind the per-frame load budget // (the prior version returned once the budget hit 0, stranding the outbox). const int MaxDrainIterations = 64; // cap at 64 * MaxCompletionsPerFrame drained/frame int iter = 0; while (iter++ < MaxDrainIterations) { var chunk = _drainCompletions(MaxCompletionsPerFrame); if (chunk.Count == 0) break; for (int chunkIndex = 0; chunkIndex < chunk.Count; chunkIndex++) { LandblockStreamResult result = chunk[chunkIndex]; // Reject obsolete hard-window work before it can occupy the // metered deferred queue. ApplyResult repeats this invariant // for direct callers and future drain paths. if (IsStaleGeneration(result) && !_presentation.HasPendingPublication(result)) continue; if (IsPublicationBlockedByRetirement(result)) _deferredApply.Add(result); else if (result is LandblockStreamResult.Unloaded || IsWithinPriorityRing(ResultLandblockId(result))) { try { ApplyResult(result); // free (unload) or behind-the-fade near ring } catch { // The concrete presentation pipeline retains the exact // unfinished stage. Preserve it and every untouched // completion from this already-drained chunk in // original FIFO order. Internal compatibility tests may // use a callback pipeline that cannot retain a receipt; // its failed result is consumed, but the untouched // suffix must still survive. if (_presentation.HasPendingPublication(result)) _deferredApply.Add(result); for (int suffix = chunkIndex + 1; suffix < chunk.Count; suffix++) _deferredApply.Add(chunk[suffix]); throw; } } else _deferredApply.Add(result); // a GPU-upload load — meter it in step 2 } } // --- Step 2: apply the deferred LOAD backlog at the per-frame budget (FIFO, so // earlier-queued landblocks win). Caps GPU upload per frame so a big diff doesn't // spike. _deferredApply now only ever holds loads — unloads were applied in step 1. int budget = MaxCompletionsPerFrame; int applied = 0; int read = 0; int write = 0; try { while (applied < budget && read < _deferredApply.Count) { LandblockStreamResult result = _deferredApply[read]; if (IsPublicationBlockedByRetirement(result)) { if (write != read) _deferredApply[write] = result; write++; read++; continue; } // Advance read only after the apply commits. If it throws, the // finally block removes prior committed entries but retains this // exact result and the untouched tail for retry. try { ApplyResult(result); read++; applied++; } catch { // A concrete suffix-stage failure keeps its pipeline // receipt and must remain at this exact FIFO position. The // internal compatibility callback cannot report its exact // committed prefix and therefore removes its receipt; // consume only that failed result so a later frame cannot // replay it automatically. if (!_presentation.HasPendingPublication(result)) read++; throw; } } } finally { // One linear tail shift replaces budget-many RemoveAt shifts. The // retained blocked prefix and the untouched FIFO tail keep their // original relative order. if (read > write) _deferredApply.RemoveRange(write, read - write); } } /// /// Apply a single with the full side- /// effects: terrain upload, GPU state, and the re-hydration callback. /// Extracted from the inline switch in the original DrainAndApply /// so both the priority-hunt path and the normal drain path share it. /// private void ApplyResult(LandblockStreamResult result) { if (_presentation.HasPendingPublication(result)) { _presentation.ResumePublication(result); ReconcileCompletedPendingPublication(result.LandblockId); return; } if (IsStaleGeneration(result)) { // A worker can finish one old load/unload after a hard recenter. // Landblock id membership cannot distinguish overlapping windows; // generation is the logical streaming incarnation boundary. return; } if (result is LandblockStreamResult.Unloaded && _region?.TryGetDesiredTier(result.LandblockId, out _) == true) { // Normal recenter does not advance the hard-window generation. A // rapid away->back can therefore re-own an id while its same- // generation unload is already in the worker outbox. Current // region ownership wins; the old unload must not tear it down. return; } if (result is LandblockStreamResult.Loaded or LandblockStreamResult.Promoted) { if (_region is null || !_region.TryGetDesiredTier(result.LandblockId, out var desiredTier)) { // A hard recenter/collapse can leave one worker job already in // flight. Its completion belongs to the old region and must not // resurrect a landblock the new StreamingRegion never owns. return; } bool isNearCompletion = result is LandblockStreamResult.Promoted or LandblockStreamResult.Loaded { Tier: LandblockStreamTier.Near }; if (isNearCompletion && _state.IsNearTier(result.LandblockId) && !_presentation.HasPendingPublication(result)) { // Normal recenter can enqueue a second high-priority Near job // while an earlier one is already in flight. The streamer // supersedes Far/Unload work but intentionally does not dedupe // high-priority jobs. Publication is idempotent at this owner: // never append statics, replay defaults, or reapply the complete // Near transaction to an already-Near landblock. return; } if (desiredTier == LandblockStreamTier.Far && isNearCompletion) { // Recenter can demote a Near job before its first completion. // StreamingRegion already considers that landblock Far, so it // deliberately emits no second LoadFar job. If no terrain is // resident yet, publish the completed heightmap/mesh as a Far // result while discarding its entity and EnvCell layers. This // is the same payload a fresh LoadFar would have produced and // closes the in-flight Near -> desired Far lifecycle without a // duplicate worker read or a permanent terrain hole. if (!_state.IsLoaded(result.LandblockId)) { switch (result) { case LandblockStreamResult.Loaded loaded: _presentation.PublishAsFar(loaded, loaded.Build, loaded.MeshData); break; case LandblockStreamResult.Promoted promoted: _presentation.PublishAsFar(promoted, promoted.Build, promoted.MeshData); break; } } return; } } switch (result) { case LandblockStreamResult.Loaded loaded: if (loaded.Tier == LandblockStreamTier.Far && _state.IsNearTier(loaded.LandblockId)) { // This Far completion was queued before a newer Near load // or promotion. Applying it would erase the entity/cell // layer that now owns the landblock. break; } _presentation.PublishLoaded(loaded); break; case LandblockStreamResult.Promoted promoted: // PromoteToNear carries a complete build and mesh because the // streamer deliberately lets it supersede a queued LoadFar. If // that Far job never started, publish this as the real Near // landblock; if the base is already resident, merge only the // Near layer so existing live projections retain identity. _presentation.PublishPromoted( promoted, mergeIntoExistingLandblock: _state.IsLoaded(promoted.LandblockId)); break; case LandblockStreamResult.Unloaded unloaded: _presentation.BeginFullRetirement(unloaded.LandblockId); break; case LandblockStreamResult.Failed failed: Console.WriteLine( $"streaming: load failed for 0x{failed.LandblockId:X8}: {failed.Error}"); break; case LandblockStreamResult.WorkerCrashed crashed: Console.WriteLine( $"streaming: worker CRASHED: {crashed.Error}"); break; } } private void ReconcileCompletedPendingPublication(uint landblockId) { if (_region is null || !_region.TryGetDesiredTier(landblockId, out LandblockStreamTier desiredTier)) { if (_state.IsLoaded(landblockId)) _presentation.BeginFullRetirement(landblockId); return; } if (desiredTier == LandblockStreamTier.Far && _state.IsNearTier(landblockId)) { _presentation.BeginNearLayerRetirement(landblockId); } } private bool IsStaleGeneration(LandblockStreamResult result) => result is not LandblockStreamResult.WorkerCrashed && result.Generation != _generation; private bool IsPublicationBlockedByRetirement(LandblockStreamResult result) => result is LandblockStreamResult.Loaded or LandblockStreamResult.Promoted && _presentation.IsRetirementPending(result.LandblockId); /// /// Returns the landblock id associated with . /// For this is 0 by /// convention (not tied to a specific landblock). /// private static uint ResultLandblockId(LandblockStreamResult result) => result.LandblockId; }