acdream/tests/AcDream.Core.Tests/Streaming/StreamingControllerTests.cs
Erik 0405947bac feat(A.5 T12): inject mesh-build dependency into LandblockStreamer
Replaces the T7-temporary default! MeshData placeholder. Streamer
now takes Func<uint, LoadedLandblock?, LandblockMeshData?> at
construction; the worker calls it after _loadLandblock succeeds and
passes the pre-built mesh into LandblockStreamResult.Loaded.

GameWindow's buildMeshOrNull factory takes the already-loaded
LoadedLandblock (lb.Heightmap is the LandBlock dat object), so no
additional dat read is needed — _heightTable and _blendCtx are
read-only after init, _surfaceCache is ConcurrentDictionary (T9).
Zero dat lock needed inside the mesh-build closure.

StreamingController._applyTerrain delegate signature widened to
Action<LoadedLandblock, LandblockMeshData> so the pre-built mesh
flows render-thread-side via the Loaded result. ApplyLoadedTerrainLocked
now accepts meshData and calls _terrain.AddLandblock directly, skipping
the per-frame LandblockMesh.Build that previously ran on the render
thread (~5ms per LB at radius=12 first traversal).

StreamingControllerTests updated: all four applyTerrain lambdas
adapted to the two-arg Action signature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 07:35:45 +02:00

106 lines
3.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using AcDream.App.Streaming;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using Xunit;
namespace AcDream.Core.Tests.Streaming;
public class StreamingControllerTests
{
private sealed class FakeStreamer
{
public List<uint> Loads { get; } = new();
public List<uint> Unloads { get; } = new();
public Queue<LandblockStreamResult> Pending { get; } = new();
public void EnqueueLoad(uint id) => Loads.Add(id);
public void EnqueueUnload(uint id) => Unloads.Add(id);
public IReadOnlyList<LandblockStreamResult> DrainCompletions(int max)
{
var batch = new List<LandblockStreamResult>();
while (batch.Count < max && Pending.Count > 0)
batch.Add(Pending.Dequeue());
return batch;
}
}
[Fact]
public void FirstTick_EnqueuesWholeVisibleWindow()
{
var state = new GpuWorldState();
var fake = new FakeStreamer();
var controller = new StreamingController(
enqueueLoad: fake.EnqueueLoad,
enqueueUnload: fake.EnqueueUnload,
drainCompletions: fake.DrainCompletions,
applyTerrain: (_, _) => { },
state: state,
radius: 2);
// Center at (50, 50); no landblocks loaded yet.
controller.Tick(observerCx: 50, observerCy: 50);
// 5×5 window = 25 loads enqueued, 0 unloads.
Assert.Equal(25, fake.Loads.Count);
Assert.Empty(fake.Unloads);
}
[Fact]
public void SecondTick_SamePosition_EnqueuesNothing()
{
var state = new GpuWorldState();
var fake = new FakeStreamer();
var controller = new StreamingController(
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
(_, _) => { }, state, radius: 2);
controller.Tick(50, 50);
fake.Loads.Clear();
controller.Tick(50, 50);
Assert.Empty(fake.Loads);
Assert.Empty(fake.Unloads);
}
[Fact]
public void DrainingLoadedResult_AddsToState()
{
var state = new GpuWorldState();
var fake = new FakeStreamer();
var applied = new List<LoadedLandblock>();
var controller = new StreamingController(
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
(lb, _) => applied.Add(lb), state, radius: 2);
// Note: LoadedLandblock's actual fields are LandblockId, Heightmap,
// Entities (positional record). Adjust if the first positional arg
// name differs.
var lb = new LoadedLandblock(0x32320FFEu, new LandBlock(), System.Array.Empty<WorldEntity>());
fake.Pending.Enqueue(new LandblockStreamResult.Loaded(0x32320FFEu, LandblockStreamTier.Near, lb, MeshData: default!));
controller.Tick(50, 50);
Assert.Single(applied);
Assert.True(state.IsLoaded(0x32320FFEu));
}
[Fact]
public void DrainingUnloadedResult_RemovesFromState()
{
var state = new GpuWorldState();
var fake = new FakeStreamer();
var controller = new StreamingController(
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
(_, _) => { }, state, radius: 2);
var lb = new LoadedLandblock(0x32320FFEu, new LandBlock(), System.Array.Empty<WorldEntity>());
state.AddLandblock(lb);
fake.Pending.Enqueue(new LandblockStreamResult.Unloaded(0x32320FFEu));
controller.Tick(50, 50);
Assert.False(state.IsLoaded(0x32320FFEu));
}
}