feat(A.5 T11): activate LandblockStreamer worker thread
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>
This commit is contained in:
parent
0cf86bb126
commit
00bb030c9f
2 changed files with 102 additions and 69 deletions
|
|
@ -19,9 +19,13 @@ public class LandblockStreamerTests
|
|||
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);
|
||||
loadLandblock: id => id == 0xA9B4FFFEu ? stubLandblock : null,
|
||||
buildMeshOrNull: (_, _) => stubMesh);
|
||||
|
||||
streamer.Start();
|
||||
streamer.EnqueueLoad(0xA9B4FFFEu);
|
||||
|
|
@ -104,37 +108,46 @@ public class LandblockStreamerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ExecutesLoaderSynchronously_OnCallingThread()
|
||||
public async Task Load_ExecutesLoaderOnWorkerThread()
|
||||
{
|
||||
// Streamer was made synchronous after Phase A.1 visual verification
|
||||
// exposed concurrent dat reads as the cause of "ball of spikes"
|
||||
// terrain corruption — DatReaderWriter's DatCollection isn't
|
||||
// thread-safe and locking around every dat read on every render-
|
||||
// thread code path was too invasive. Until Phase A.3 introduces a
|
||||
// thread-safe dat wrapper, the load delegate runs on the calling
|
||||
// thread and the result is in the outbox by the time EnqueueLoad
|
||||
// returns. This test pins that contract.
|
||||
// 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;
|
||||
});
|
||||
using var streamer = new LandblockStreamer(
|
||||
loadLandblock: id =>
|
||||
{
|
||||
loaderThreadId = System.Environment.CurrentManagedThreadId;
|
||||
return stubLandblock;
|
||||
},
|
||||
buildMeshOrNull: (_, _) => stubMesh);
|
||||
|
||||
streamer.Start();
|
||||
streamer.EnqueueLoad(0x77770FFEu);
|
||||
|
||||
// Result is already in the outbox — no spinning needed.
|
||||
var drained = streamer.DrainCompletions(LandblockStreamer.DefaultDrainBatchSize);
|
||||
// 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.Single(drained);
|
||||
Assert.IsType<LandblockStreamResult.Loaded>(drained[0]);
|
||||
Assert.Equal(testThreadId, loaderThreadId);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue