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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue