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:
parent
ac45cb1bd7
commit
b8f6317fe1
15 changed files with 1261 additions and 336 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue