acdream/tests/AcDream.Core.Tests/Streaming/StreamingControllerPriorityApplyTests.cs
Erik f918b3ea2c refactor(teleport A): O(N) deferred drain + never-arrives test (review fixes)
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>
2026-06-22 13:03:20 +02:00

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);
}
}