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
|
|
@ -1004,7 +1004,15 @@ public sealed class LandblockPresentationPipelineTests
|
|||
state,
|
||||
nearRadius: 0,
|
||||
farRadius: 2,
|
||||
removeTerrain: id => calls.Add($"unload:{id:X8}"));
|
||||
removeTerrain: id => calls.Add($"unload:{id:X8}"),
|
||||
workBudgetOptions: new StreamingWorkBudgetOptions(
|
||||
MaxUpdateMilliseconds: 100,
|
||||
MaxCompletionAdmissions: 3,
|
||||
MaxAdoptedCpuBytes: 1_000_000,
|
||||
MaxEntityOperations: 1_000,
|
||||
MaxGpuUploadBytes: 1_000_000,
|
||||
MaxGlRetireOperations: 1_000,
|
||||
DestinationReserveFraction: 0.75f));
|
||||
controller.PriorityLandblockId = priorityId;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x70, 0x70));
|
||||
|
|
@ -1021,8 +1029,8 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.Equal(
|
||||
[
|
||||
$"publish:{priorityId:X8}",
|
||||
$"publish:{laterId:X8}",
|
||||
$"unload:{unloadId:X8}",
|
||||
$"publish:{laterId:X8}",
|
||||
],
|
||||
calls);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
using System.Diagnostics;
|
||||
using AcDream.App.Streaming;
|
||||
|
||||
namespace AcDream.App.Tests.Streaming;
|
||||
|
||||
public sealed class StreamingCompletionQueueTests
|
||||
{
|
||||
[Fact]
|
||||
public void PrioritySelectionIsStableFifoAndSkipsOnlyBlockedHead()
|
||||
{
|
||||
var queue = new StreamingCompletionQueue();
|
||||
StreamingQueuedCompletion farFirst = Completion(
|
||||
new LandblockStreamResult.Failed(1, "far-first"),
|
||||
StreamingCompletionPriority.Far,
|
||||
sequence: 1);
|
||||
StreamingQueuedCompletion nearBlocked = Completion(
|
||||
new LandblockStreamResult.Failed(2, "near-blocked"),
|
||||
StreamingCompletionPriority.Near,
|
||||
sequence: 2);
|
||||
StreamingQueuedCompletion nearTail = Completion(
|
||||
new LandblockStreamResult.Failed(3, "near-tail"),
|
||||
StreamingCompletionPriority.Near,
|
||||
sequence: 3);
|
||||
StreamingQueuedCompletion destinationFirst = Completion(
|
||||
new LandblockStreamResult.Failed(4, "destination-first"),
|
||||
StreamingCompletionPriority.Destination,
|
||||
sequence: 4);
|
||||
StreamingQueuedCompletion destinationTail = Completion(
|
||||
new LandblockStreamResult.Failed(5, "destination-tail"),
|
||||
StreamingCompletionPriority.Destination,
|
||||
sequence: 5);
|
||||
|
||||
queue.Enqueue(farFirst);
|
||||
queue.Enqueue(nearBlocked);
|
||||
queue.Enqueue(nearTail);
|
||||
queue.Enqueue(destinationFirst);
|
||||
queue.Enqueue(destinationTail);
|
||||
|
||||
Assert.True(queue.TryPeekNext(
|
||||
static _ => false,
|
||||
out StreamingQueuedCompletion? selected));
|
||||
Assert.Equal(destinationFirst.Sequence, selected?.Sequence);
|
||||
Assert.Same(destinationFirst.Result, selected?.Result);
|
||||
queue.RemoveHead(destinationFirst);
|
||||
|
||||
Assert.True(queue.TryPeekNext(
|
||||
result => ReferenceEquals(result, destinationTail.Result)
|
||||
|| ReferenceEquals(result, nearBlocked.Result),
|
||||
out selected));
|
||||
Assert.Equal(farFirst.Sequence, selected?.Sequence);
|
||||
Assert.Same(farFirst.Result, selected?.Result);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
queue.RemoveHead(nearTail));
|
||||
|
||||
Assert.True(queue.TryPeekNext(
|
||||
static _ => false,
|
||||
out selected));
|
||||
Assert.Equal(destinationTail.Sequence, selected?.Sequence);
|
||||
Assert.Same(destinationTail.Result, selected?.Result);
|
||||
queue.RemoveHead(destinationTail);
|
||||
Assert.True(queue.TryPeekNext(
|
||||
static _ => false,
|
||||
out selected));
|
||||
Assert.Equal(nearBlocked.Sequence, selected?.Sequence);
|
||||
Assert.Same(nearBlocked.Result, selected?.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetainedBytesAndPriorityFactsTrackExactQueueLifetime()
|
||||
{
|
||||
var queue = new StreamingCompletionQueue();
|
||||
StreamingQueuedCompletion destination = Completion(
|
||||
new LandblockStreamResult.Failed(10, "destination"),
|
||||
StreamingCompletionPriority.Destination,
|
||||
sequence: 1,
|
||||
adoptedBytes: 12);
|
||||
StreamingQueuedCompletion unload = Completion(
|
||||
new LandblockStreamResult.Unloaded(11),
|
||||
StreamingCompletionPriority.Unload,
|
||||
sequence: 2,
|
||||
adoptedBytes: 20);
|
||||
StreamingQueuedCompletion near = Completion(
|
||||
new LandblockStreamResult.Failed(12, "near"),
|
||||
StreamingCompletionPriority.Near,
|
||||
sequence: 3,
|
||||
adoptedBytes: 30);
|
||||
|
||||
queue.Enqueue(destination);
|
||||
queue.Enqueue(unload);
|
||||
queue.Enqueue(near);
|
||||
|
||||
StreamingCompletionQueueSnapshot snapshot = queue.CaptureSnapshot();
|
||||
Assert.Equal(3, snapshot.Count);
|
||||
Assert.Equal(62, snapshot.RetainedCpuBytes);
|
||||
Assert.Equal(1, snapshot.Destination);
|
||||
Assert.Equal(1, snapshot.Unload);
|
||||
Assert.Equal(1, snapshot.Near);
|
||||
Assert.True(snapshot.OldestAgeMilliseconds >= 0);
|
||||
|
||||
queue.RemoveHead(destination);
|
||||
snapshot = queue.CaptureSnapshot();
|
||||
Assert.Equal(2, snapshot.Count);
|
||||
Assert.Equal(50, snapshot.RetainedCpuBytes);
|
||||
|
||||
queue.Clear();
|
||||
snapshot = queue.CaptureSnapshot();
|
||||
Assert.Equal(0, snapshot.Count);
|
||||
Assert.Equal(0, snapshot.RetainedCpuBytes);
|
||||
Assert.Equal(0, snapshot.OldestAgeMilliseconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactReferenceRemovalDoesNotConflateEqualRecords()
|
||||
{
|
||||
var queue = new StreamingCompletionQueue();
|
||||
var firstResult =
|
||||
new LandblockStreamResult.Failed(20, "same", Generation: 7);
|
||||
var equalButDistinctResult =
|
||||
new LandblockStreamResult.Failed(20, "same", Generation: 7);
|
||||
StreamingQueuedCompletion first = Completion(
|
||||
firstResult,
|
||||
StreamingCompletionPriority.Control,
|
||||
sequence: 1,
|
||||
adoptedBytes: 11);
|
||||
StreamingQueuedCompletion second = Completion(
|
||||
equalButDistinctResult,
|
||||
StreamingCompletionPriority.Control,
|
||||
sequence: 2,
|
||||
adoptedBytes: 13);
|
||||
queue.Enqueue(first);
|
||||
queue.Enqueue(second);
|
||||
|
||||
int removed = queue.RemoveResults(
|
||||
[equalButDistinctResult],
|
||||
static _ => true);
|
||||
|
||||
Assert.Equal(1, removed);
|
||||
Assert.Equal(1, queue.Count);
|
||||
Assert.Equal(11, queue.RetainedCpuBytes);
|
||||
Assert.True(queue.TryPeekNext(
|
||||
static _ => false,
|
||||
out StreamingQueuedCompletion? selected));
|
||||
Assert.Equal(first.Sequence, selected?.Sequence);
|
||||
Assert.Same(first.Result, selected?.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CallbackAppendNeverInvalidatesActiveHead()
|
||||
{
|
||||
var queue = new StreamingCompletionQueue();
|
||||
StreamingQueuedCompletion active = Completion(
|
||||
new LandblockStreamResult.Failed(30, "active"),
|
||||
StreamingCompletionPriority.Near,
|
||||
sequence: 1);
|
||||
StreamingQueuedCompletion appended = Completion(
|
||||
new LandblockStreamResult.Failed(31, "appended"),
|
||||
StreamingCompletionPriority.Near,
|
||||
sequence: 2);
|
||||
queue.Enqueue(active);
|
||||
|
||||
Assert.True(queue.TryPeekNext(
|
||||
static _ => false,
|
||||
out StreamingQueuedCompletion? selected));
|
||||
Assert.Equal(active.Sequence, selected?.Sequence);
|
||||
Assert.Same(active.Result, selected?.Result);
|
||||
|
||||
queue.Enqueue(appended);
|
||||
queue.RemoveHead(active);
|
||||
|
||||
Assert.True(queue.TryPeekNext(
|
||||
static _ => false,
|
||||
out selected));
|
||||
Assert.Equal(appended.Sequence, selected?.Sequence);
|
||||
Assert.Same(appended.Result, selected?.Result);
|
||||
}
|
||||
|
||||
private static StreamingQueuedCompletion Completion(
|
||||
LandblockStreamResult result,
|
||||
StreamingCompletionPriority priority,
|
||||
long sequence,
|
||||
long adoptedBytes = 0)
|
||||
{
|
||||
var estimate = new LandblockStreamCostEstimate(
|
||||
Work: new StreamingWorkCost(
|
||||
CompletionAdmissions: 1,
|
||||
AdoptedCpuBytes: adoptedBytes),
|
||||
TerrainPayloadBytes: 0,
|
||||
Entities: 0,
|
||||
MeshReferences: 0,
|
||||
VisibilityCells: 0,
|
||||
EnvCellShells: 0,
|
||||
PortalConnections: 0,
|
||||
PortalPolygonVertices: 0,
|
||||
PhysicsEnvCells: 0,
|
||||
PhysicsEnvironments: 0,
|
||||
PhysicsSetups: 0,
|
||||
PhysicsGfxObjects: 0);
|
||||
return new StreamingQueuedCompletion(
|
||||
result,
|
||||
estimate,
|
||||
priority,
|
||||
result.Generation,
|
||||
sequence,
|
||||
Stopwatch.GetTimestamp());
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,40 @@ public sealed class StreamingWorkBudgetTests
|
|||
snapshot.LastLimit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureProgressCanCrossTheLimitOnlyOncePerFrame()
|
||||
{
|
||||
var meter = new StreamingWorkMeter(
|
||||
Budget(cpuBytes: 1),
|
||||
static () => 0,
|
||||
timestampFrequency: 1_000);
|
||||
|
||||
Assert.Equal(
|
||||
StreamingWorkAdmission.Admitted,
|
||||
meter.TryReserve(
|
||||
new StreamingWorkCost(CompletionAdmissions: 1),
|
||||
"admission"));
|
||||
meter.Complete();
|
||||
Assert.Equal(
|
||||
StreamingWorkAdmission.Admitted,
|
||||
meter.TryReserve(
|
||||
new StreamingWorkCost(AdoptedCpuBytes: 1),
|
||||
"first-execution",
|
||||
ensureProgress: true));
|
||||
meter.Complete();
|
||||
|
||||
Assert.Equal(
|
||||
StreamingWorkAdmission.Yielded,
|
||||
meter.TryReserve(
|
||||
new StreamingWorkCost(AdoptedCpuBytes: 1),
|
||||
"second-execution",
|
||||
ensureProgress: true));
|
||||
Assert.Equal(1, meter.Snapshot.YieldCount);
|
||||
Assert.Equal(
|
||||
StreamingWorkLimit.AdoptedCpuBytes,
|
||||
meter.Snapshot.LastLimit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeterUsesInjectedMonotonicClockAndRecordsOverrunAndFailure()
|
||||
{
|
||||
|
|
@ -129,6 +163,64 @@ public sealed class StreamingWorkBudgetTests
|
|||
timestampFrequency: 1_000));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2, 1.0, 2, 50, 4, 50, 2)]
|
||||
[InlineData(4, 2.0, 4, 100, 8, 100, 4)]
|
||||
[InlineData(6, 3.0, 6, 150, 12, 150, 6)]
|
||||
public void LegacyCompletionSelectorScalesTheWholeProfile(
|
||||
int count,
|
||||
double milliseconds,
|
||||
int admissions,
|
||||
long cpuBytes,
|
||||
int entityOperations,
|
||||
long gpuBytes,
|
||||
int retireOperations)
|
||||
{
|
||||
var options = new StreamingWorkBudgetOptions(
|
||||
MaxUpdateMilliseconds: 2,
|
||||
MaxCompletionAdmissions: 4,
|
||||
MaxAdoptedCpuBytes: 100,
|
||||
MaxEntityOperations: 8,
|
||||
MaxGpuUploadBytes: 100,
|
||||
MaxGlRetireOperations: 4,
|
||||
DestinationReserveFraction: 0.75f);
|
||||
|
||||
StreamingWorkBudgetOptions scaled =
|
||||
options.ScaleForLegacyCompletionCount(count);
|
||||
|
||||
Assert.Equal(milliseconds, scaled.MaxUpdateMilliseconds);
|
||||
Assert.Equal(admissions, scaled.MaxCompletionAdmissions);
|
||||
Assert.Equal(cpuBytes, scaled.MaxAdoptedCpuBytes);
|
||||
Assert.Equal(entityOperations, scaled.MaxEntityOperations);
|
||||
Assert.Equal(gpuBytes, scaled.MaxGpuUploadBytes);
|
||||
Assert.Equal(retireOperations, scaled.MaxGlRetireOperations);
|
||||
Assert.Equal(0.75f, scaled.DestinationReserveFraction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyCompletionSelectorRejectsZeroAndSaturatesHugeProfiles()
|
||||
{
|
||||
var options = new StreamingWorkBudgetOptions(
|
||||
MaxUpdateMilliseconds: 2,
|
||||
MaxCompletionAdmissions: int.MaxValue,
|
||||
MaxAdoptedCpuBytes: long.MaxValue,
|
||||
MaxEntityOperations: int.MaxValue,
|
||||
MaxGpuUploadBytes: long.MaxValue,
|
||||
MaxGlRetireOperations: int.MaxValue,
|
||||
DestinationReserveFraction: 0.75f);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
options.ScaleForLegacyCompletionCount(0));
|
||||
|
||||
StreamingWorkBudgetOptions scaled =
|
||||
options.ScaleForLegacyCompletionCount(int.MaxValue);
|
||||
Assert.Equal(int.MaxValue, scaled.MaxCompletionAdmissions);
|
||||
Assert.Equal(long.MaxValue, scaled.MaxAdoptedCpuBytes);
|
||||
Assert.Equal(int.MaxValue, scaled.MaxEntityOperations);
|
||||
Assert.Equal(long.MaxValue, scaled.MaxGpuUploadBytes);
|
||||
Assert.Equal(int.MaxValue, scaled.MaxGlRetireOperations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompletionCostChargesExactArraysAndDeterministicLogicalEntries()
|
||||
{
|
||||
|
|
@ -239,10 +331,15 @@ public sealed class StreamingWorkBudgetTests
|
|||
applyTerrain: static (_, _) => { },
|
||||
state: new GpuWorldState(),
|
||||
nearRadius: 1,
|
||||
farRadius: 1)
|
||||
{
|
||||
MaxCompletionsPerFrame = 1,
|
||||
};
|
||||
farRadius: 1,
|
||||
workBudgetOptions: new StreamingWorkBudgetOptions(
|
||||
MaxUpdateMilliseconds: 100,
|
||||
MaxCompletionAdmissions: 2,
|
||||
MaxAdoptedCpuBytes: 1_000,
|
||||
MaxEntityOperations: 100,
|
||||
MaxGpuUploadBytes: 44,
|
||||
MaxGlRetireOperations: 100,
|
||||
DestinationReserveFraction: 0.75f));
|
||||
|
||||
controller.Tick(32, 32);
|
||||
|
||||
|
|
@ -250,9 +347,10 @@ public sealed class StreamingWorkBudgetTests
|
|||
Assert.Equal(1, first.DeferredCompletions);
|
||||
Assert.Equal(44, first.DeferredAdoptedCpuBytes);
|
||||
Assert.True(first.OldestDeferredAgeMilliseconds >= 0);
|
||||
Assert.Equal(1, first.LastFrame.Operations);
|
||||
Assert.Equal(1, first.LastFrame.CompletedOperations);
|
||||
Assert.Equal(44, first.LastFrame.Used.AdoptedCpuBytes);
|
||||
Assert.Equal(3, first.LastFrame.Operations);
|
||||
Assert.Equal(3, first.LastFrame.CompletedOperations);
|
||||
Assert.Equal(1, first.LastFrame.YieldCount);
|
||||
Assert.Equal(88, first.LastFrame.Used.AdoptedCpuBytes);
|
||||
|
||||
controller.Tick(32, 32);
|
||||
|
||||
|
|
@ -263,6 +361,155 @@ public sealed class StreamingWorkBudgetTests
|
|||
Assert.Equal(1, second.LastFrame.CompletedOperations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ControllerBoundsProductionAdmissionByCountBeforeAdoption()
|
||||
{
|
||||
uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32);
|
||||
var source = new QueueCompletionSource(
|
||||
Loaded(center),
|
||||
Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 33)),
|
||||
Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 34)));
|
||||
int applied = 0;
|
||||
StreamingController controller = Controller(
|
||||
source,
|
||||
() => applied++,
|
||||
WorkOptions(admissions: 2, cpuBytes: 1_000));
|
||||
|
||||
controller.Tick(32, 32);
|
||||
|
||||
Assert.Equal(2, applied);
|
||||
Assert.Equal(1, source.BacklogCount);
|
||||
Assert.Equal(1, controller.WorkDiagnostics.WorkerCompletionBacklog);
|
||||
Assert.Equal(2, controller.WorkDiagnostics.LastFrame.Used.CompletionAdmissions);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ControllerBoundsProductionAdmissionByRetainedCpuBytes()
|
||||
{
|
||||
uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32);
|
||||
var source = new QueueCompletionSource(
|
||||
Loaded(center),
|
||||
Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 33)),
|
||||
Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 34)));
|
||||
int applied = 0;
|
||||
StreamingController controller = Controller(
|
||||
source,
|
||||
() => applied++,
|
||||
WorkOptions(admissions: 64, cpuBytes: 44));
|
||||
|
||||
controller.Tick(32, 32);
|
||||
|
||||
Assert.Equal(1, applied);
|
||||
Assert.Equal(2, source.BacklogCount);
|
||||
Assert.Equal(44, controller.WorkDiagnostics.LastFrame.Used.AdoptedCpuBytes);
|
||||
Assert.Equal(
|
||||
StreamingWorkLimit.AdoptedCpuBytes,
|
||||
controller.WorkDiagnostics.LastFrame.LastLimit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DestinationAndUnloadPriorityNeverBypassExecutionBudgets()
|
||||
{
|
||||
uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32);
|
||||
uint ordinary = StreamingRegion.EncodeLandblockIdForTest(33, 33);
|
||||
var source = new QueueCompletionSource(
|
||||
Loaded(ordinary),
|
||||
Loaded(center),
|
||||
new LandblockStreamResult.Unloaded(0x1111FFFFu),
|
||||
new LandblockStreamResult.Unloaded(0x2222FFFFu));
|
||||
var applied = new List<uint>();
|
||||
StreamingController controller = Controller(
|
||||
source,
|
||||
id => applied.Add(id),
|
||||
WorkOptions(
|
||||
admissions: 4,
|
||||
cpuBytes: 1_000,
|
||||
gpuBytes: 44,
|
||||
retireOperations: 1));
|
||||
controller.PriorityLandblockId = center;
|
||||
|
||||
controller.Tick(32, 32);
|
||||
|
||||
Assert.Equal([center], applied);
|
||||
Assert.Equal(2, controller.WorkDiagnostics.DeferredCompletions);
|
||||
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.YieldCount);
|
||||
Assert.Equal(44, controller.WorkDiagnostics.LastFrame.Used.GpuUploadBytes);
|
||||
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||
|
||||
controller.Tick(32, 32);
|
||||
|
||||
Assert.Equal([center, ordinary], applied);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions);
|
||||
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaleGenerationResultsConsumeAdmissionButRetainNoPayload()
|
||||
{
|
||||
uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32);
|
||||
var source = new QueueCompletionSource(
|
||||
Loaded(center, generation: 1),
|
||||
Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 33), generation: 1),
|
||||
Loaded(center));
|
||||
int applied = 0;
|
||||
StreamingController controller = Controller(
|
||||
source,
|
||||
() => applied++,
|
||||
WorkOptions(admissions: 2, cpuBytes: 44));
|
||||
|
||||
controller.Tick(32, 32);
|
||||
|
||||
Assert.Equal(0, applied);
|
||||
Assert.Equal(1, source.BacklogCount);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.DeferredAdoptedCpuBytes);
|
||||
Assert.Equal(2, controller.WorkDiagnostics.LastFrame.Used.CompletionAdmissions);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.AdoptedCpuBytes);
|
||||
|
||||
controller.Tick(32, 32);
|
||||
Assert.Equal(1, applied);
|
||||
}
|
||||
|
||||
private static StreamingController Controller(
|
||||
ILandblockCompletionSource source,
|
||||
Action applied,
|
||||
StreamingWorkBudgetOptions options) =>
|
||||
Controller(source, _ => applied(), options);
|
||||
|
||||
private static StreamingController Controller(
|
||||
ILandblockCompletionSource source,
|
||||
Action<uint> applied,
|
||||
StreamingWorkBudgetOptions options)
|
||||
{
|
||||
var state = new GpuWorldState();
|
||||
var presentation = new LandblockPresentationPipeline(
|
||||
(build, _) => applied(build.Landblock.LandblockId),
|
||||
state);
|
||||
return new StreamingController(
|
||||
enqueueLoad: static (_, _, _) => { },
|
||||
enqueueUnload: static (_, _) => { },
|
||||
completionSource: source,
|
||||
state,
|
||||
nearRadius: 1,
|
||||
farRadius: 1,
|
||||
presentationPipeline: presentation,
|
||||
workBudgetOptions: options);
|
||||
}
|
||||
|
||||
private static StreamingWorkBudgetOptions WorkOptions(
|
||||
int admissions,
|
||||
long cpuBytes,
|
||||
long gpuBytes = 1_000,
|
||||
int retireOperations = 100) => new(
|
||||
MaxUpdateMilliseconds: 100,
|
||||
MaxCompletionAdmissions: admissions,
|
||||
MaxAdoptedCpuBytes: cpuBytes,
|
||||
MaxEntityOperations: 100,
|
||||
MaxGpuUploadBytes: gpuBytes,
|
||||
MaxGlRetireOperations: retireOperations,
|
||||
DestinationReserveFraction: 0.75f);
|
||||
|
||||
private static WorldEntity Entity(uint id, int meshRefs)
|
||||
{
|
||||
var refs = new MeshRef[meshRefs];
|
||||
|
|
@ -278,7 +525,9 @@ public sealed class StreamingWorkBudgetTests
|
|||
};
|
||||
}
|
||||
|
||||
private static LandblockStreamResult.Loaded Loaded(uint landblockId) => new(
|
||||
private static LandblockStreamResult.Loaded Loaded(
|
||||
uint landblockId,
|
||||
ulong generation = 0) => new(
|
||||
landblockId,
|
||||
LandblockStreamTier.Near,
|
||||
new LoadedLandblock(
|
||||
|
|
@ -287,5 +536,39 @@ public sealed class StreamingWorkBudgetTests
|
|||
Array.Empty<WorldEntity>()),
|
||||
new LandblockMeshData(
|
||||
new TerrainVertex[1],
|
||||
new uint[1]));
|
||||
new uint[1]),
|
||||
generation);
|
||||
|
||||
private sealed class QueueCompletionSource(
|
||||
params LandblockStreamResult[] results)
|
||||
: ILandblockCompletionSource
|
||||
{
|
||||
private readonly Queue<LandblockStreamResult> _results = new(results);
|
||||
|
||||
public int BacklogCount => _results.Count;
|
||||
|
||||
public bool TryPeek(out LandblockStreamResult? result)
|
||||
{
|
||||
if (_results.TryPeek(out LandblockStreamResult? peeked))
|
||||
{
|
||||
result = peeked;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryRead(out LandblockStreamResult? result)
|
||||
{
|
||||
if (_results.TryDequeue(out LandblockStreamResult? consumed))
|
||||
{
|
||||
result = consumed;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -263,6 +263,34 @@ public class LandblockStreamerTests
|
|||
Assert.Equal(43ul, unloaded.Generation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompletionSourcePeekPreservesExactResultAndBacklog()
|
||||
{
|
||||
using var streamer = new LandblockStreamer(loadLandblock: _ => null);
|
||||
streamer.Start();
|
||||
streamer.EnqueueUnload(0xABCE0000u, generation: 44);
|
||||
|
||||
for (int i = 0;
|
||||
i < SpinMaxIterations && streamer.BacklogCount == 0;
|
||||
i++)
|
||||
{
|
||||
await Task.Delay(SpinStepMs);
|
||||
}
|
||||
|
||||
Assert.Equal(1, streamer.BacklogCount);
|
||||
Assert.True(streamer.TryPeek(out LandblockStreamResult? firstPeek));
|
||||
Assert.NotNull(firstPeek);
|
||||
Assert.Equal(1, streamer.BacklogCount);
|
||||
Assert.True(streamer.TryPeek(out LandblockStreamResult? secondPeek));
|
||||
Assert.Same(firstPeek, secondPeek);
|
||||
Assert.Equal(1, streamer.BacklogCount);
|
||||
|
||||
Assert.True(streamer.TryRead(out LandblockStreamResult? consumed));
|
||||
Assert.Same(firstPeek, consumed);
|
||||
Assert.Equal(0, streamer.BacklogCount);
|
||||
Assert.False(streamer.TryRead(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_ExecutesLoaderOnWorkerThread()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public class StreamingControllerPriorityApplyTests
|
|||
generation);
|
||||
|
||||
[Fact]
|
||||
public void PriorityLandblock_isApplied_evenWhenBeyondPerFrameCap()
|
||||
public void PriorityLandblock_WinsExecutionOrderAfterBoundedAdmission()
|
||||
{
|
||||
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
||||
var outbox = new Queue<LandblockStreamResult>(new LandblockStreamResult[]
|
||||
|
|
@ -42,14 +42,18 @@ public class StreamingControllerPriorityApplyTests
|
|||
return batch;
|
||||
},
|
||||
applyTerrain: (lb, _) => applied.Add(lb.LandblockId),
|
||||
state: state, nearRadius: 4, farRadius: 12)
|
||||
state: state,
|
||||
nearRadius: 4,
|
||||
farRadius: 12,
|
||||
workBudgetOptions: GenerousBudget())
|
||||
{ MaxCompletionsPerFrame = 4 };
|
||||
|
||||
ctrl.PriorityLandblockId = priority;
|
||||
ctrl.Tick(169, 180);
|
||||
|
||||
Assert.Contains(priority, applied); // priority applied THIS tick
|
||||
Assert.True(applied.Count <= 5); // did not blindly flush the whole outbox
|
||||
Assert.NotEmpty(applied);
|
||||
Assert.Equal(priority, applied[0]);
|
||||
Assert.True(applied.Count <= 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -98,10 +102,8 @@ public class StreamingControllerPriorityApplyTests
|
|||
public void PriorityNeverArrives_noThrow_noLoss_noDoubleApply()
|
||||
{
|
||||
// While a PriorityLandblockId is set but the matching completion never
|
||||
// arrives (e.g. the load failed), the hunt moves outbox completions into
|
||||
// _deferredApply and they drain at the per-frame budget — same backpressure
|
||||
// as the normal throttle, just relocated — until the caller clears
|
||||
// PriorityLandblockId (the TAS MaxContinue safety net does this on timeout).
|
||||
// arrives (e.g. the load failed), ordinary results still enter the typed
|
||||
// Near/Far queues and advance under the same budget without loss.
|
||||
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
||||
uint otherId0 = StreamingRegion.EncodeLandblockIdForTest(169, 181);
|
||||
uint otherId1 = StreamingRegion.EncodeLandblockIdForTest(169, 182);
|
||||
|
|
@ -149,6 +151,15 @@ public class StreamingControllerPriorityApplyTests
|
|||
Assert.Equal(3, applied.Count);
|
||||
}
|
||||
|
||||
private static StreamingWorkBudgetOptions GenerousBudget() => new(
|
||||
MaxUpdateMilliseconds: 100,
|
||||
MaxCompletionAdmissions: 64,
|
||||
MaxAdoptedCpuBytes: 16 * StreamingWorkBudgetOptions.MiB,
|
||||
MaxEntityOperations: 10_000,
|
||||
MaxGpuUploadBytes: 16 * StreamingWorkBudgetOptions.MiB,
|
||||
MaxGlRetireOperations: 10_000,
|
||||
DestinationReserveFraction: 0.75f);
|
||||
|
||||
[Fact]
|
||||
public void DeferredCompaction_ApplyFailureRetainsExactResultWithoutReplayingCommittedPrefix()
|
||||
{
|
||||
|
|
@ -223,7 +234,15 @@ public class StreamingControllerPriorityApplyTests
|
|||
applyTerrain: (build, _) => applied.Add(build.LandblockId),
|
||||
state: state,
|
||||
nearRadius: 4,
|
||||
farRadius: 12)
|
||||
farRadius: 12,
|
||||
workBudgetOptions: new StreamingWorkBudgetOptions(
|
||||
MaxUpdateMilliseconds: 100,
|
||||
MaxCompletionAdmissions: 4,
|
||||
MaxAdoptedCpuBytes: 1_000_000,
|
||||
MaxEntityOperations: 1_000,
|
||||
MaxGpuUploadBytes: 1_000_000,
|
||||
MaxGlRetireOperations: 1_000,
|
||||
DestinationReserveFraction: 0.75f))
|
||||
{ MaxCompletionsPerFrame = 4, PriorityLandblockId = priority };
|
||||
|
||||
ctrl.Tick(169, 180);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue