Replace RemoveAt(0)-in-a-loop drain idiom with RemoveRange(0, i) in both Step-1 deferred drain and the post-found deferred drain inside DrainAndApply, making each an O(N) single shift instead of O(N²) on the render-thread hot path. Add PriorityNeverArrives_noThrow_noLoss_noDoubleApply test: sets a priority id that never appears in the outbox, ticks several times, asserts no throw, no loss of the non-priority completions, and no double-apply (applied count == completions enqueued). Comment above the priority-hunt explains the failure mode: completions relocate to _deferredApply while the hunt is active and drain at per-frame budget until the caller clears PriorityLandblockId. Restyle test 2 to use named constructor arguments and break the compressed lambda onto readable lines (matches test 1 style). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
149 lines
6.1 KiB
C#
149 lines
6.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.Core.Terrain;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Streaming;
|
|
|
|
public class StreamingControllerPriorityApplyTests
|
|
{
|
|
private static LandblockStreamResult.Loaded LoadedOf(uint canonicalId)
|
|
=> new(canonicalId, LandblockStreamTier.Near,
|
|
new LoadedLandblock(canonicalId, new LandBlock(), Array.Empty<WorldEntity>()),
|
|
new LandblockMeshData(Array.Empty<TerrainVertex>(), Array.Empty<uint>()));
|
|
|
|
[Fact]
|
|
public void PriorityLandblock_isApplied_evenWhenBeyondPerFrameCap()
|
|
{
|
|
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
|
var outbox = new Queue<LandblockStreamResult>(new LandblockStreamResult[]
|
|
{
|
|
LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 0)),
|
|
LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 1)),
|
|
LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 2)),
|
|
LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 3)),
|
|
LoadedOf(priority),
|
|
});
|
|
|
|
var applied = new List<uint>();
|
|
var state = new GpuWorldState();
|
|
var ctrl = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max =>
|
|
{
|
|
var batch = new List<LandblockStreamResult>();
|
|
while (batch.Count < max && outbox.Count > 0) batch.Add(outbox.Dequeue());
|
|
return batch;
|
|
},
|
|
applyTerrain: (lb, _) => applied.Add(lb.LandblockId),
|
|
state: state, nearRadius: 4, farRadius: 12)
|
|
{ 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
|
|
}
|
|
|
|
[Fact]
|
|
public void Deferred_nonPriority_completions_applyOnLaterFrames_withoutLoss()
|
|
{
|
|
// After the priority is found, the non-priority items drained past it must
|
|
// still be applied (over subsequent ticks), never dropped.
|
|
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
|
var outbox = new Queue<LandblockStreamResult>(new LandblockStreamResult[]
|
|
{
|
|
LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 0)),
|
|
LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 1)),
|
|
LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 2)),
|
|
LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 3)),
|
|
LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 4)),
|
|
LoadedOf(priority),
|
|
});
|
|
var applied = new List<uint>();
|
|
var state = new GpuWorldState();
|
|
var ctrl = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max =>
|
|
{
|
|
var batch = new List<LandblockStreamResult>();
|
|
while (batch.Count < max && outbox.Count > 0) batch.Add(outbox.Dequeue());
|
|
return batch;
|
|
},
|
|
applyTerrain: (lb, _) => applied.Add(lb.LandblockId),
|
|
state: state,
|
|
nearRadius: 4,
|
|
farRadius: 12)
|
|
{ MaxCompletionsPerFrame = 4 };
|
|
ctrl.PriorityLandblockId = priority;
|
|
|
|
ctrl.Tick(169, 180); // priority + some others
|
|
ctrl.PriorityLandblockId = 0u;
|
|
ctrl.Tick(169, 180); // drains the deferred remainder
|
|
ctrl.Tick(169, 180);
|
|
|
|
Assert.Contains(priority, applied);
|
|
Assert.Equal(6, applied.Count); // all six applied, none lost
|
|
}
|
|
|
|
[Fact]
|
|
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).
|
|
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
|
uint otherId0 = StreamingRegion.EncodeLandblockIdForTest(1, 0);
|
|
uint otherId1 = StreamingRegion.EncodeLandblockIdForTest(1, 1);
|
|
uint otherId2 = StreamingRegion.EncodeLandblockIdForTest(1, 2);
|
|
|
|
var outbox = new Queue<LandblockStreamResult>(new LandblockStreamResult[]
|
|
{
|
|
LoadedOf(otherId0),
|
|
LoadedOf(otherId1),
|
|
LoadedOf(otherId2),
|
|
});
|
|
|
|
var applied = new List<uint>();
|
|
var state = new GpuWorldState();
|
|
var ctrl = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max =>
|
|
{
|
|
var batch = new List<LandblockStreamResult>();
|
|
while (batch.Count < max && outbox.Count > 0) batch.Add(outbox.Dequeue());
|
|
return batch;
|
|
},
|
|
applyTerrain: (lb, _) => applied.Add(lb.LandblockId),
|
|
state: state,
|
|
nearRadius: 4,
|
|
farRadius: 12)
|
|
{ MaxCompletionsPerFrame = 4 };
|
|
|
|
// Set a priority id that will NEVER appear in the outbox.
|
|
ctrl.PriorityLandblockId = priority;
|
|
|
|
// Tick several times — should not throw and every non-priority completion
|
|
// must still be applied without duplication.
|
|
ctrl.Tick(169, 180);
|
|
ctrl.Tick(169, 180);
|
|
ctrl.Tick(169, 180);
|
|
|
|
// (a) no throw — covered by reaching here without exception
|
|
// (b) every non-priority completion is applied (no loss)
|
|
Assert.Contains(otherId0, applied);
|
|
Assert.Contains(otherId1, applied);
|
|
Assert.Contains(otherId2, applied);
|
|
// (c) no double-apply: applied count matches completions enqueued
|
|
Assert.Equal(3, applied.Count);
|
|
}
|
|
}
|