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

@ -43,7 +43,7 @@ namespace AcDream.App.Streaming;
/// methods are thread-safe.
/// </remarks>
/// </summary>
public sealed class LandblockStreamer : IDisposable
public sealed class LandblockStreamer : IDisposable, ILandblockCompletionSource
{
/// <summary>
/// Default drain batch size. Tuned to cap GPU upload work the render
@ -61,6 +61,7 @@ public sealed class LandblockStreamer : IDisposable
private readonly object _inboxGate = new();
private Thread? _worker;
private Exception? _workerFailure;
private int _completionBacklog;
private int _disposed;
private readonly object _disposeGate = new();
private bool _disposeCompleted;
@ -276,11 +277,37 @@ public sealed class LandblockStreamer : IDisposable
public IReadOnlyList<LandblockStreamResult> DrainCompletions(int maxBatchSize = DefaultDrainBatchSize)
{
var batch = new List<LandblockStreamResult>(maxBatchSize);
while (batch.Count < maxBatchSize && _outbox.Reader.TryRead(out var result))
while (batch.Count < maxBatchSize && TryRead(out var result))
{
if (result is null)
throw new InvalidOperationException(
"The completion channel returned a null result.");
batch.Add(result);
}
return batch;
}
public int BacklogCount => Math.Max(
0,
System.Threading.Volatile.Read(ref _completionBacklog));
public bool TryPeek(out LandblockStreamResult? result) =>
_outbox.Reader.TryPeek(out result);
public bool TryRead(out LandblockStreamResult? result)
{
if (!_outbox.Reader.TryRead(out result))
return false;
System.Threading.Interlocked.Decrement(ref _completionBacklog);
return true;
}
private void PublishResult(LandblockStreamResult result)
{
if (_outbox.Writer.TryWrite(result))
System.Threading.Interlocked.Increment(ref _completionBacklog);
}
private void WorkerLoop()
{
var highPriority = new Queue<LandblockStreamJob>();
@ -337,7 +364,7 @@ public sealed class LandblockStreamer : IDisposable
_workerFailure = ex;
_inbox.Writer.TryComplete(ex);
}
_outbox.Writer.TryWrite(new LandblockStreamResult.WorkerCrashed(ex.ToString()));
PublishResult(new LandblockStreamResult.WorkerCrashed(ex.ToString()));
}
finally
{
@ -426,13 +453,13 @@ public sealed class LandblockStreamer : IDisposable
var build = _loadLandblock(load.Request);
if (build is null)
{
_outbox.Writer.TryWrite(new LandblockStreamResult.Failed(
PublishResult(new LandblockStreamResult.Failed(
load.LandblockId, "LandblockLoader.Load returned null", load.Generation));
break;
}
if (build.Origin != load.Origin)
{
_outbox.Writer.TryWrite(new LandblockStreamResult.Failed(
PublishResult(new LandblockStreamResult.Failed(
load.LandblockId,
$"Landblock build origin {build.Origin} did not match request origin {load.Origin}",
load.Generation));
@ -444,18 +471,18 @@ public sealed class LandblockStreamer : IDisposable
var promotedMesh = _buildMeshOrNull(load.LandblockId, lb);
if (promotedMesh is null)
{
_outbox.Writer.TryWrite(new LandblockStreamResult.Failed(
PublishResult(new LandblockStreamResult.Failed(
load.LandblockId, "buildMeshOrNull returned null", load.Generation));
break;
}
_outbox.Writer.TryWrite(new LandblockStreamResult.Promoted(
PublishResult(new LandblockStreamResult.Promoted(
load.LandblockId, build, promotedMesh, load.Generation));
break;
}
var mesh = _buildMeshOrNull(load.LandblockId, lb);
if (mesh is null)
{
_outbox.Writer.TryWrite(new LandblockStreamResult.Failed(
PublishResult(new LandblockStreamResult.Failed(
load.LandblockId, "buildMeshOrNull returned null", load.Generation));
break;
}
@ -485,18 +512,18 @@ public sealed class LandblockStreamer : IDisposable
PhysicsDatBundle.Empty);
build = new LandblockBuild(lb, Origin: build.Origin);
}
_outbox.Writer.TryWrite(new LandblockStreamResult.Loaded(
PublishResult(new LandblockStreamResult.Loaded(
load.LandblockId, tier, build, mesh, load.Generation));
}
catch (Exception ex)
{
_outbox.Writer.TryWrite(new LandblockStreamResult.Failed(
PublishResult(new LandblockStreamResult.Failed(
load.LandblockId, ex.ToString(), load.Generation));
}
break;
case LandblockStreamJob.Unload unload:
_outbox.Writer.TryWrite(new LandblockStreamResult.Unloaded(
PublishResult(new LandblockStreamResult.Unloaded(
unload.LandblockId,
unload.Generation));
break;