feat(streaming): enforce typed completion queues

Replace the flat deferred list, priority scan, unload bypass, and count-only execution cap with exact destination/control/unload/Near/Far FIFOs behind one typed frame meter. Price worker results before adoption, retain exact retry identity, reject stale generations without payload retention, and publish queue pressure through lifecycle diagnostics.

Tests: dotnet build AcDream.slnx -c Release --no-restore; dotnet test AcDream.slnx -c Release --no-restore (8138 passed, 5 skipped)
This commit is contained in:
Erik 2026-07-24 17:48:24 +02:00
parent ac45cb1bd7
commit b8f6317fe1
15 changed files with 1261 additions and 336 deletions

View file

@ -19,17 +19,12 @@ namespace AcDream.App.Streaming;
/// </summary>
public sealed class StreamingController : IStreamingFrameBackend
{
private readonly record struct DeferredCompletion(
LandblockStreamResult Result,
StreamingWorkCost Cost,
long EnqueuedTimestamp);
private sealed class OriginRecenterRetirement
{
public bool RadiiConverged;
public bool GenerationAdvanced;
public bool PendingLoadsCleared;
public bool DeferredApplyCleared;
public bool CompletionQueueCleared;
public bool RegionCleared;
public List<uint>? ResidentIds;
public int RetirementCursor;
@ -41,13 +36,18 @@ public sealed class StreamingController : IStreamingFrameBackend
private readonly Action<uint, LandblockStreamJobKind, ulong> _enqueueLoad;
private readonly Action<uint, ulong> _enqueueUnload;
private readonly Func<int, IReadOnlyList<LandblockStreamResult>> _drainCompletions;
private readonly ILandblockCompletionSource _completionSource;
private readonly Action? _clearPendingLoads;
private readonly LandblockPresentationPipeline _presentation;
private readonly StreamingWorkBudget _workBudget;
private readonly Func<LandblockStreamResult, bool>
_isPublicationBlockedByRetirement;
private readonly StreamingWorkBudgetOptions _configuredWorkBudgetOptions;
private StreamingWorkBudget _workBudget;
private StreamingWorkMeter? _activeWorkMeter;
private StreamingWorkMeterSnapshot _lastWorkMeter;
private long _deferredAdoptedCpuBytes;
private readonly StreamingCompletionQueue _completionQueue = new();
private long _nextCompletionSequence;
private int _legacyCompletionProfile = 4;
private readonly GpuWorldState _state;
private StreamingRegion? _region;
@ -88,34 +88,28 @@ public sealed class StreamingController : IStreamingFrameBackend
public int FarRadius { get; private set; }
/// <summary>
/// Cap on completions drained per <see cref="Tick"/> 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).
///
/// <para>
/// 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
/// <see cref="GpuWorldState"/>'s pending-spawn list, so spreading
/// completions doesn't lose any data.
/// </para>
/// Compatibility quality selector for callers that still express the
/// streaming profile as a completion count. Four preserves the configured
/// High profile; other positive values scale every typed time/count/byte
/// dimension together. It is not an independent execution throttle.
/// </summary>
public int MaxCompletionsPerFrame { get; set; } = 4;
public int MaxCompletionsPerFrame
{
get => _legacyCompletionProfile;
set
{
StreamingWorkBudgetOptions profile =
_configuredWorkBudgetOptions
.ScaleForLegacyCompletionCount(value);
_legacyCompletionProfile = value;
_workBudget = profile.ToBudget();
}
}
/// <summary>
/// #138: the teleport destination landblock id. When non-zero,
/// <see cref="DrainAndApply"/> applies this landblock's
/// <see cref="LandblockStreamResult.Loaded"/> or
/// <see cref="LandblockStreamResult.Promoted"/> completion immediately
/// — even if it sits past the <see cref="MaxCompletionsPerFrame"/>
/// 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 <see cref="_deferredApply"/> and applied over subsequent frames,
/// so no completions are lost and there is no GPU spike.
/// #138: the teleport destination landblock id. Loaded or promoted results
/// inside its priority ring enter the destination FIFO, ahead of ordinary
/// Near/Far work, but still consume the same typed frame budget.
///
/// <para>Set by <c>GameWindow</c> when a teleport starts; reset to 0
/// once the destination landblock has been applied (or when the
@ -124,32 +118,39 @@ public sealed class StreamingController : IStreamingFrameBackend
public uint PriorityLandblockId { get; set; }
/// <summary>
/// 2026-06-22: the radius (in landblocks, Chebyshev) around
/// <see cref="PriorityLandblockId"/> that <see cref="DrainAndApply"/> 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 <see cref="MaxCompletionsPerFrame"/>,
/// 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.
/// Radius in landblocks (Chebyshev distance) around
/// <see cref="PriorityLandblockId"/> classified as destination work.
/// Zero means only the center. This changes queue priority, never budget
/// enforcement or atomic publication semantics.
/// </summary>
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;
// accepted completion backlog. A non-zero value during/after a teleport is
// the typed-budget publication tail. Removable compatibility probe surface.
public int DeferredApplyBacklog => _completionQueue.Count;
public int PendingRetirementCount => _presentation.PendingRetirementCount;
public StreamingWorkDiagnostics WorkDiagnostics => new(
_lastWorkMeter,
_deferredApply.Count,
_deferredAdoptedCpuBytes,
OldestDeferredAgeMilliseconds(),
_presentation.PendingPublicationCount,
_presentation.PendingRetirementCount);
public StreamingWorkDiagnostics WorkDiagnostics
{
get
{
StreamingCompletionQueueSnapshot queued =
_completionQueue.CaptureSnapshot();
return new StreamingWorkDiagnostics(
_lastWorkMeter,
queued.Count,
queued.RetainedCpuBytes,
queued.OldestAgeMilliseconds,
_presentation.PendingPublicationCount,
_presentation.PendingRetirementCount,
_completionSource.BacklogCount,
queued.Destination,
queued.Control,
queued.Unload,
queued.Near,
queued.Far);
}
}
internal bool IsCollapsedToDungeon => _collapsed;
/// <summary>
@ -195,9 +196,6 @@ public sealed class StreamingController : IStreamingFrameBackend
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<DeferredCompletion> _deferredApply = new();
/// <summary>
/// Internal compatibility seam for hermetic controller policy tests. Live
/// composition cannot supply presentation callbacks; it must use the
@ -216,7 +214,8 @@ public sealed class StreamingController : IStreamingFrameBackend
Action? clearPendingLoads = null,
Action<uint>? onLandblockLoaded = null,
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
LandblockRetirementCoordinator? retirementCoordinator = null)
LandblockRetirementCoordinator? retirementCoordinator = null,
StreamingWorkBudgetOptions? workBudgetOptions = null)
: this(
enqueueLoad,
enqueueUnload,
@ -232,7 +231,8 @@ public sealed class StreamingController : IStreamingFrameBackend
retirementCoordinator,
removeTerrain,
demoteNearLayer),
clearPendingLoads)
clearPendingLoads,
workBudgetOptions)
{
}
@ -241,7 +241,7 @@ public sealed class StreamingController : IStreamingFrameBackend
/// retirement owner, so no legacy publication delegate can be supplied or
/// silently ignored.
/// </summary>
public StreamingController(
internal StreamingController(
Action<uint, LandblockStreamJobKind, ulong> enqueueLoad,
Action<uint, ulong> enqueueUnload,
Func<int, IReadOnlyList<LandblockStreamResult>> drainCompletions,
@ -251,16 +251,47 @@ public sealed class StreamingController : IStreamingFrameBackend
LandblockPresentationPipeline presentationPipeline,
Action? clearPendingLoads = null,
StreamingWorkBudgetOptions? workBudgetOptions = null)
: this(
enqueueLoad,
enqueueUnload,
new DelegateLandblockCompletionSource(drainCompletions),
state,
nearRadius,
farRadius,
presentationPipeline,
clearPendingLoads,
workBudgetOptions)
{
}
/// <summary>
/// Allocation-free production completion path. Peek/read is single-consumer
/// and lets the scheduler price a result before adopting it.
/// </summary>
public StreamingController(
Action<uint, LandblockStreamJobKind, ulong> enqueueLoad,
Action<uint, ulong> enqueueUnload,
ILandblockCompletionSource completionSource,
GpuWorldState state,
int nearRadius,
int farRadius,
LandblockPresentationPipeline presentationPipeline,
Action? clearPendingLoads = null,
StreamingWorkBudgetOptions? workBudgetOptions = null)
{
_enqueueLoad = enqueueLoad;
_enqueueUnload = enqueueUnload;
_drainCompletions = drainCompletions;
_completionSource = completionSource
?? throw new ArgumentNullException(nameof(completionSource));
_clearPendingLoads = clearPendingLoads;
_state = state;
_presentation = presentationPipeline
?? throw new ArgumentNullException(nameof(presentationPipeline));
_workBudget = (workBudgetOptions ?? StreamingWorkBudgetOptions.Default)
.ToBudget();
_isPublicationBlockedByRetirement =
IsPublicationBlockedByRetirement;
_configuredWorkBudgetOptions =
workBudgetOptions ?? StreamingWorkBudgetOptions.Default;
_workBudget = _configuredWorkBudgetOptions.ToBudget();
if (!_presentation.MatchesState(_state))
{
throw new ArgumentException(
@ -400,7 +431,8 @@ public sealed class StreamingController : IStreamingFrameBackend
Action? clearPendingLoads = null,
Action<uint>? onLandblockLoaded = null,
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
LandblockRetirementCoordinator? retirementCoordinator = null)
LandblockRetirementCoordinator? retirementCoordinator = null,
StreamingWorkBudgetOptions? workBudgetOptions = null)
: this(
(id, kind, _) => enqueueLoad(id, kind),
(id, _) => enqueueUnload(id),
@ -414,7 +446,8 @@ public sealed class StreamingController : IStreamingFrameBackend
clearPendingLoads,
onLandblockLoaded,
ensureEnvCellMeshes,
retirementCoordinator)
retirementCoordinator,
workBudgetOptions)
{
}
@ -429,48 +462,47 @@ public sealed class StreamingController : IStreamingFrameBackend
_presentation.BeginNearLayerRetirement(canonical);
}
private void AdvanceGeneration()
private bool AdvanceGeneration()
{
ConvergePendingPublications();
if (!ConvergePendingPublications())
return false;
_generation = unchecked(_generation + 1);
return true;
}
private void ConvergePendingPublications()
private bool ConvergePendingPublications()
{
IReadOnlyList<LandblockStreamResult> pending =
_presentation.GetPendingPublicationResults();
if (pending.Count == 0)
return;
return true;
try
{
bool progressed = false;
for (int i = 0; i < pending.Count; i++)
{
LandblockStreamResult result = pending[i];
ObserveResultOperation(
if (!TryObserveResultOperation(
result,
"publication-retry",
() => _presentation.ResumePublication(result));
() => _presentation.ResumePublication(result),
ensureProgress: !progressed))
{
return false;
}
progressed = true;
}
return true;
}
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(entry =>
{
for (int i = 0; i < pending.Count; i++)
{
if (ReferenceEquals(entry.Result, pending[i]))
{
return !_presentation.HasPendingPublication(
entry.Result);
}
}
return false;
});
RecalculateDeferredAdoptedBytes();
_completionQueue.RemoveResults(
pending,
result => !_presentation.HasPendingPublication(result));
}
}
@ -518,7 +550,8 @@ public sealed class StreamingController : IStreamingFrameBackend
// 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();
if (!ConvergePendingPublications())
return;
uint centerId = StreamingRegion.EncodeLandblockId(observerCx, observerCy);
@ -574,7 +607,8 @@ public sealed class StreamingController : IStreamingFrameBackend
if (!pending.GenerationAdvanced)
{
AdvanceGeneration();
if (!AdvanceGeneration())
return;
pending.GenerationAdvanced = true;
}
@ -625,7 +659,7 @@ public sealed class StreamingController : IStreamingFrameBackend
_region = pending.Region;
NearRadius = pending.NearRadius;
FarRadius = pending.FarRadius;
ClearDeferred();
_completionQueue.Clear();
_pendingRadiiReconfiguration = null;
}
}
@ -762,11 +796,12 @@ public sealed class StreamingController : IStreamingFrameBackend
bool logTransition = !_collapsed || _collapsedCenter != centerId;
if (logTransition)
Console.WriteLine($"streaming: dungeon collapse -> 0x{centerId:X8}");
AdvanceGeneration();
if (!AdvanceGeneration())
return;
_collapsed = true;
_collapsedCenter = centerId;
_clearPendingLoads?.Invoke();
ClearDeferred();
_completionQueue.Clear();
_region = null;
foreach (var id in _state.LoadedLandblockIds)
@ -811,7 +846,7 @@ public sealed class StreamingController : IStreamingFrameBackend
/// <summary>
/// True when <paramref name="id"/> is the priority center or within
/// <see cref="PriorityRadius"/> landblocks of it (Chebyshev) — i.e. inside the teleport
/// near ring that <see cref="DrainAndApply"/> eager-applies. With the default radius 0
/// near ring that <see cref="DrainAndApply"/> prioritizes. With the default radius 0
/// this reduces to an exact match on <see cref="PriorityLandblockId"/> (the original
/// single-landblock priority behaviour).
/// </summary>
@ -829,7 +864,8 @@ public sealed class StreamingController : IStreamingFrameBackend
Console.WriteLine(
$"streaming: dungeon EXIT-expand -> ({observerCx},{observerCy}) " +
$"(was collapsed on 0x{_collapsedCenter:X8})");
AdvanceGeneration();
if (!AdvanceGeneration())
return;
_collapsed = false;
var rebuilt = new StreamingRegion(observerCx, observerCy, NearRadius, FarRadius);
@ -1027,7 +1063,8 @@ public sealed class StreamingController : IStreamingFrameBackend
}
if (!transaction.GenerationAdvanced)
{
AdvanceGeneration();
if (!AdvanceGeneration())
return false;
transaction.GenerationAdvanced = true;
}
if (!transaction.PendingLoadsCleared)
@ -1045,10 +1082,10 @@ public sealed class StreamingController : IStreamingFrameBackend
return false;
}
}
if (!transaction.DeferredApplyCleared)
if (!transaction.CompletionQueueCleared)
{
ClearDeferred();
transaction.DeferredApplyCleared = true;
_completionQueue.Clear();
transaction.CompletionQueueCleared = true;
}
if (!transaction.RegionCleared)
{
@ -1103,10 +1140,11 @@ public sealed class StreamingController : IStreamingFrameBackend
private void BeginFullWindowRetirement()
{
AdvanceGeneration();
if (!AdvanceGeneration())
return;
_collapsed = false;
_clearPendingLoads?.Invoke();
ClearDeferred();
_completionQueue.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.
@ -1120,164 +1158,171 @@ public sealed class StreamingController : IStreamingFrameBackend
}
/// <summary>
/// Apply streamed completions for this frame. LOADS (terrain mesh GPU uploads) are the
/// expensive part, so they are metered at <see cref="MaxCompletionsPerFrame"/> to avoid a
/// GPU-upload spike; the overflow buffers in <see cref="_deferredApply"/> 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
/// (<see cref="PriorityRadius"/>, applied during portal travel) likewise bypass the budget so the
/// player materialises in a loaded world.
/// Adopts immutable worker results and advances explicit priority FIFOs
/// under the one typed frame meter. Destination and unload work win queue
/// order but no class bypasses time, bytes, entities, uploads, or retire
/// operation limits.
/// </summary>
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))
Defer(result);
else if (result is LandblockStreamResult.Unloaded
|| IsWithinPriorityRing(ResultLandblockId(result)))
{
try
{
ApplyResultObserved(
result,
"priority-or-unload");
}
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))
Defer(result);
for (int suffix = chunkIndex + 1; suffix < chunk.Count; suffix++)
Defer(chunk[suffix]);
throw;
}
}
else
Defer(result); // a GPU-upload load — meter it in step 2
}
}
StreamingWorkMeter meter = _activeWorkMeter
?? throw new InvalidOperationException(
"Completion scheduling requires an active frame meter.");
// --- 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
AdmitCompletions(meter);
bool executed = false;
while (_completionQueue.TryPeekNext(
_isPublicationBlockedByRetirement,
out StreamingQueuedCompletion? completion))
{
while (applied < budget && read < _deferredApply.Count)
{
DeferredCompletion entry = _deferredApply[read];
LandblockStreamResult result = entry.Result;
if (IsPublicationBlockedByRetirement(result))
{
if (write != read)
_deferredApply[write] = entry;
write++;
read++;
continue;
}
StreamingQueuedCompletion work = completion
?? throw new InvalidOperationException(
"The completion queue returned a null head.");
StreamingWorkCost executionCost =
IsStaleGeneration(work.Result)
? default
: work.ExecutionCost;
StreamingWorkAdmission admission = meter.TryReserve(
executionCost,
$"execute-{work.Priority}",
ensureProgress: !executed);
if (admission == StreamingWorkAdmission.Yielded)
break;
// 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
{
ApplyResultObserved(
result,
"deferred-publication",
entry.Cost);
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;
}
try
{
ApplyResult(work.Result);
_completionQueue.RemoveHead(work);
meter.Complete();
executed = true;
}
catch
{
// Concrete publication failures retain their exact owner
// receipt and queue head. A compatibility callback without a
// receipt cannot safely replay, so consume only that result.
if (!_presentation.HasPendingPublication(work.Result))
_completionQueue.RemoveHead(work);
meter.Fail();
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)
RemoveDeferredRange(write, read - write);
}
}
private void ApplyResultObserved(
LandblockStreamResult result,
string stage,
StreamingWorkCost? knownCost = null) =>
ObserveResultOperation(
result,
stage,
() => ApplyResult(result),
knownCost);
private void AdmitCompletions(StreamingWorkMeter meter)
{
while (_completionSource.TryPeek(out LandblockStreamResult? peeked))
{
LandblockStreamResult result = peeked
?? throw new InvalidOperationException(
"The completion source returned a null peek.");
bool stale = IsStaleGeneration(result)
&& !_presentation.HasPendingPublication(result);
LandblockStreamCostEstimate estimate =
LandblockStreamResultCost.Estimate(result);
StreamingWorkCost admissionCost = stale
? new StreamingWorkCost(CompletionAdmissions: 1)
: new StreamingWorkCost(
CompletionAdmissions:
estimate.Work.CompletionAdmissions,
AdoptedCpuBytes:
estimate.Work.AdoptedCpuBytes);
StreamingWorkAdmission admission = meter.TryReserve(
admissionCost,
"completion-admission");
if (admission == StreamingWorkAdmission.Yielded)
return;
private void ObserveResultOperation(
try
{
if (!_completionSource.TryRead(
out LandblockStreamResult? consumed)
|| !ReferenceEquals(result, consumed))
{
throw new InvalidOperationException(
"The single-consumer completion source changed between peek and read.");
}
if (!stale)
{
_completionQueue.Enqueue(new StreamingQueuedCompletion(
result,
estimate,
ClassifyCompletion(result),
result.Generation,
checked(_nextCompletionSequence++),
Stopwatch.GetTimestamp()));
}
meter.Complete();
}
catch
{
meter.Fail();
throw;
}
}
}
private StreamingCompletionPriority ClassifyCompletion(
LandblockStreamResult result)
{
if (result is LandblockStreamResult.Loaded
or LandblockStreamResult.Promoted
&& IsWithinPriorityRing(result.LandblockId))
{
return StreamingCompletionPriority.Destination;
}
return result switch
{
LandblockStreamResult.Failed
or LandblockStreamResult.WorkerCrashed =>
StreamingCompletionPriority.Control,
LandblockStreamResult.Unloaded =>
StreamingCompletionPriority.Unload,
LandblockStreamResult.Promoted
or LandblockStreamResult.Loaded
{
Tier: LandblockStreamTier.Near,
} =>
StreamingCompletionPriority.Near,
_ => StreamingCompletionPriority.Far,
};
}
private bool TryObserveResultOperation(
LandblockStreamResult result,
string stage,
Action operation,
StreamingWorkCost? knownCost = null)
StreamingWorkCost? knownCost = null,
bool ensureProgress = false)
{
StreamingWorkMeter? meter = _activeWorkMeter;
if (meter is null)
{
operation();
return;
return true;
}
StreamingWorkCost cost = knownCost
StreamingWorkCost aggregate = knownCost
?? LandblockStreamResultCost.Estimate(result).Work;
StreamingWorkAdmission admission = meter.TryReserve(cost, stage);
StreamingWorkCost cost = new(
EntityOperations: aggregate.EntityOperations,
GpuUploadBytes: aggregate.GpuUploadBytes,
GlRetireOperations:
result is LandblockStreamResult.Unloaded ? 1 : 0);
StreamingWorkAdmission admission = meter.TryReserve(
cost,
stage,
ensureProgress);
if (admission == StreamingWorkAdmission.Yielded)
meter.ForceReserveObserved(cost, stage);
return false;
try
{
operation();
meter.Complete();
return true;
}
catch
{
@ -1286,69 +1331,10 @@ public sealed class StreamingController : IStreamingFrameBackend
}
}
private void Defer(LandblockStreamResult result)
{
StreamingWorkCost cost = LandblockStreamResultCost.Estimate(result).Work;
_deferredApply.Add(new DeferredCompletion(
result,
cost,
Stopwatch.GetTimestamp()));
_deferredAdoptedCpuBytes = SaturatingAdd(
_deferredAdoptedCpuBytes,
cost.AdoptedCpuBytes);
}
private void ClearDeferred()
{
_deferredApply.Clear();
_deferredAdoptedCpuBytes = 0;
}
private void RemoveDeferredRange(int index, int count)
{
for (int i = index; i < index + count; i++)
{
_deferredAdoptedCpuBytes = Math.Max(
0,
_deferredAdoptedCpuBytes
- _deferredApply[i].Cost.AdoptedCpuBytes);
}
_deferredApply.RemoveRange(index, count);
}
private void RecalculateDeferredAdoptedBytes()
{
long total = 0;
for (int i = 0; i < _deferredApply.Count; i++)
{
total = SaturatingAdd(
total,
_deferredApply[i].Cost.AdoptedCpuBytes);
}
_deferredAdoptedCpuBytes = total;
}
private double OldestDeferredAgeMilliseconds()
{
if (_deferredApply.Count == 0)
return 0;
long oldest = _deferredApply[0].EnqueuedTimestamp;
for (int i = 1; i < _deferredApply.Count; i++)
oldest = Math.Min(oldest, _deferredApply[i].EnqueuedTimestamp);
return Math.Max(0L, Stopwatch.GetTimestamp() - oldest)
* 1000.0
/ Stopwatch.Frequency;
}
private static long SaturatingAdd(long left, long right) =>
left > long.MaxValue - right ? long.MaxValue : left + right;
/// <summary>
/// Apply a single <see cref="LandblockStreamResult"/> with the full side-
/// effects: terrain upload, GPU state, and the re-hydration callback.
/// Extracted from the inline switch in the original <c>DrainAndApply</c>
/// so both the priority-hunt path and the normal drain path share it.
/// All priority queues route through this one publication path.
/// </summary>
private void ApplyResult(LandblockStreamResult result)
{