acdream/tests/AcDream.Core.Tests/Streaming/StreamingControllerTests.cs
Erik b8d80fe282 feat(A.5 T13): StreamingController two-tier Tick
Replaces the single-radius Tick with a two-tier model that consumes
StreamingRegion's TwoTierDiff (5-list) and routes to the appropriate
JobKind:

- ToLoadFar    -> _enqueueLoad(id, LoadFar)
- ToLoadNear   -> _enqueueLoad(id, LoadNear)
- ToPromote    -> _enqueueLoad(id, PromoteToNear)
- ToDemote     -> _state.RemoveEntitiesFromLandblock(id) on render thread
- ToUnload     -> _enqueueUnload(id)

Drain switch handles Loaded (terrain + entity layer), Promoted (entity
layer only -- terrain already loaded), Unloaded, Failed, WorkerCrashed.

Constructor signature: nearRadius/farRadius separate ints. Old single-
radius ctor removed; existing single-radius tests updated to pass
nearRadius=farRadius for backward-compat coverage.

GameWindow's enqueueLoad lambda updated from (id =>...) to (id, kind) =>
to match new Action<uint, LandblockStreamJobKind> signature; radius: arg
renamed to nearRadius:/farRadius: (both set to _streamingRadius until T16
wires the full two-tier env-var parsing).

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

113 lines
4 KiB
C#
Raw Permalink 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, LandblockStreamJobKind _) => 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,
nearRadius: 2,
farRadius: 2);
// Center at (50, 50); no landblocks loaded yet.
controller.Tick(observerCx: 50, observerCy: 50);
// 5×5 window = 25 loads enqueued (nearRadius==farRadius so all go to ToLoadNear), 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, nearRadius: 2, farRadius: 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, nearRadius: 2, farRadius: 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>());
// A.5 T10-T12 follow-up: use a real empty mesh instance instead of
// default! so any future test that flows MeshData through the apply
// callback gets a non-null reference to inspect rather than an NRE.
var stubMesh = new AcDream.Core.Terrain.LandblockMeshData(
System.Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
System.Array.Empty<uint>());
fake.Pending.Enqueue(new LandblockStreamResult.Loaded(0x32320FFEu, LandblockStreamTier.Near, lb, stubMesh));
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, nearRadius: 2, farRadius: 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));
}
}