Phase A.1 reverted to synchronous mode due to DatCollection thread- safety; T10 documented the lock that makes concurrent reads safe. T11 activates the dedicated worker thread and switches enqueue methods to non-blocking Channel.Writer.TryWrite. EnqueueLoad now takes LandblockStreamJobKind (default: LoadNear from all callers, matching previous full-load semantics). T13/T16 will route by kind per TwoTierDiff. Constructor gains optional buildMeshOrNull param (defaults to null- returning stub); T12 wires the real LandblockMesh.Build factory. GameWindow construction site updated: Action<uint> enqueueLoad delegate now wraps a lambda (method group won't bind to Action<uint> when the method has an optional second param). LandblockStreamerTests updated: the synchronous-thread-pinning test replaced by Load_ExecutesLoaderOnWorkerThread which asserts the loader runs on a different thread; Load_FollowedByDrain now supplies a stubMesh so the worker can produce Loaded (not Failed) results. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
153 lines
5.5 KiB
C#
153 lines
5.5 KiB
C#
using System.Threading.Tasks;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Streaming;
|
|
|
|
public class LandblockStreamerTests
|
|
{
|
|
private const int SpinTimeoutMs = 2000;
|
|
private const int SpinStepMs = 10;
|
|
private const int SpinMaxIterations = SpinTimeoutMs / SpinStepMs;
|
|
|
|
[Fact]
|
|
public async Task Load_FollowedByDrain_ReturnsLoadedRecord()
|
|
{
|
|
var stubLandblock = new LoadedLandblock(
|
|
0xA9B4FFFEu,
|
|
new LandBlock(),
|
|
System.Array.Empty<WorldEntity>());
|
|
var stubMesh = new AcDream.Core.Terrain.LandblockMeshData(
|
|
System.Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
|
|
System.Array.Empty<uint>());
|
|
|
|
using var streamer = new LandblockStreamer(
|
|
loadLandblock: id => id == 0xA9B4FFFEu ? stubLandblock : null,
|
|
buildMeshOrNull: (_, _) => stubMesh);
|
|
|
|
streamer.Start();
|
|
streamer.EnqueueLoad(0xA9B4FFFEu);
|
|
|
|
// Spin until the worker produces a completion, with a 2s timeout.
|
|
LandblockStreamResult? result = null;
|
|
for (int i = 0; i < SpinMaxIterations && result is null; i++)
|
|
{
|
|
var drained = streamer.DrainCompletions(maxBatchSize: LandblockStreamer.DefaultDrainBatchSize);
|
|
if (drained.Count > 0) result = drained[0];
|
|
else await Task.Delay(SpinStepMs);
|
|
}
|
|
|
|
Assert.NotNull(result);
|
|
var loaded = Assert.IsType<LandblockStreamResult.Loaded>(result);
|
|
Assert.Equal(0xA9B4FFFEu, loaded.LandblockId);
|
|
Assert.Same(stubLandblock, loaded.Landblock);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_WhenLoaderReturnsNull_ReportsFailed()
|
|
{
|
|
using var streamer = new LandblockStreamer(
|
|
loadLandblock: _ => null);
|
|
|
|
streamer.Start();
|
|
streamer.EnqueueLoad(0x12340000u);
|
|
|
|
LandblockStreamResult? result = null;
|
|
for (int i = 0; i < SpinMaxIterations && result is null; i++)
|
|
{
|
|
var drained = streamer.DrainCompletions(LandblockStreamer.DefaultDrainBatchSize);
|
|
if (drained.Count > 0) result = drained[0];
|
|
else await Task.Delay(SpinStepMs);
|
|
}
|
|
|
|
Assert.NotNull(result);
|
|
Assert.IsType<LandblockStreamResult.Failed>(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_WhenLoaderThrows_ReportsFailedWithMessage()
|
|
{
|
|
using var streamer = new LandblockStreamer(
|
|
loadLandblock: _ => throw new System.InvalidOperationException("boom"));
|
|
|
|
streamer.Start();
|
|
streamer.EnqueueLoad(0x55550000u);
|
|
|
|
LandblockStreamResult? result = null;
|
|
for (int i = 0; i < SpinMaxIterations && result is null; i++)
|
|
{
|
|
var drained = streamer.DrainCompletions(LandblockStreamer.DefaultDrainBatchSize);
|
|
if (drained.Count > 0) result = drained[0];
|
|
else await Task.Delay(SpinStepMs);
|
|
}
|
|
|
|
var failed = Assert.IsType<LandblockStreamResult.Failed>(result);
|
|
Assert.Contains("boom", failed.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unload_ProducesUnloadedResult()
|
|
{
|
|
using var streamer = new LandblockStreamer(loadLandblock: _ => null);
|
|
|
|
streamer.Start();
|
|
streamer.EnqueueUnload(0xABCD0000u);
|
|
|
|
LandblockStreamResult? result = null;
|
|
for (int i = 0; i < SpinMaxIterations && result is null; i++)
|
|
{
|
|
var drained = streamer.DrainCompletions(LandblockStreamer.DefaultDrainBatchSize);
|
|
if (drained.Count > 0) result = drained[0];
|
|
else await Task.Delay(SpinStepMs);
|
|
}
|
|
|
|
var unloaded = Assert.IsType<LandblockStreamResult.Unloaded>(result);
|
|
Assert.Equal(0xABCD0000u, unloaded.LandblockId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_ExecutesLoaderOnWorkerThread()
|
|
{
|
|
// Phase A.5 T11: the load delegate now runs on the dedicated worker
|
|
// thread (not the calling/render thread). This test verifies the
|
|
// async hand-off: EnqueueLoad returns immediately and the result
|
|
// appears in the outbox only after the worker processes the inbox.
|
|
int testThreadId = System.Environment.CurrentManagedThreadId;
|
|
int? loaderThreadId = null;
|
|
var stubLandblock = new LoadedLandblock(
|
|
0x77770FFEu,
|
|
new LandBlock(),
|
|
System.Array.Empty<WorldEntity>());
|
|
var stubMesh = new AcDream.Core.Terrain.LandblockMeshData(
|
|
System.Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
|
|
System.Array.Empty<uint>());
|
|
|
|
using var streamer = new LandblockStreamer(
|
|
loadLandblock: id =>
|
|
{
|
|
loaderThreadId = System.Environment.CurrentManagedThreadId;
|
|
return stubLandblock;
|
|
},
|
|
buildMeshOrNull: (_, _) => stubMesh);
|
|
|
|
streamer.Start();
|
|
streamer.EnqueueLoad(0x77770FFEu);
|
|
|
|
// Spin until the worker produces a completion.
|
|
LandblockStreamResult? result = null;
|
|
for (int i = 0; i < SpinMaxIterations && result is null; i++)
|
|
{
|
|
var drained = streamer.DrainCompletions(LandblockStreamer.DefaultDrainBatchSize);
|
|
if (drained.Count > 0) result = drained[0];
|
|
else await Task.Delay(SpinStepMs);
|
|
}
|
|
|
|
Assert.NotNull(result);
|
|
Assert.IsType<LandblockStreamResult.Loaded>(result);
|
|
// The loader MUST have run on a different thread than the test thread.
|
|
Assert.NotNull(loaderThreadId);
|
|
Assert.NotEqual(testThreadId, loaderThreadId.Value);
|
|
}
|
|
}
|