feat: ChunkStreamer load path with target-set, dispatch, and diff application
This commit is contained in:
173
VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs
Normal file
173
VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using OpenTK.Mathematics;
|
||||
using VoxelEngine.Core;
|
||||
using VoxelEngine.World;
|
||||
using VoxelEngine.World.Blocks;
|
||||
using VoxelEngine.World.Chunk;
|
||||
using VoxelEngine.World.Persistence;
|
||||
using VoxelEngine.World.Streaming;
|
||||
using Xunit;
|
||||
using VoxelChunk = VoxelEngine.World.Chunk.Chunk;
|
||||
|
||||
namespace VoxelEngine.Tests.Streaming;
|
||||
|
||||
// Synchronous executor: runs the submitted action on the calling thread.
|
||||
internal sealed class SynchronousJobExecutor : IJobExecutor
|
||||
{
|
||||
public void Submit(Action work) => work();
|
||||
}
|
||||
|
||||
// Fake renderer: never touches GL. Just records lifecycle.
|
||||
internal sealed class FakeRenderer : IChunkRenderer
|
||||
{
|
||||
public bool Disposed { get; private set; }
|
||||
public int DrawCount { get; private set; }
|
||||
public float[] Mesh { get; }
|
||||
|
||||
public FakeRenderer(float[] mesh) { this.Mesh = mesh; }
|
||||
public void Draw() => this.DrawCount++;
|
||||
public void Dispose() => this.Disposed = true;
|
||||
}
|
||||
|
||||
// Minimal mesher returning a fixed-length non-empty mesh.
|
||||
internal sealed class FakeMesher : IChunkMesher
|
||||
{
|
||||
public float[] Build(VoxelChunk chunk, BlockRegistry registry, Func<int, int, int, bool>? neighborOpaqueOutside = null)
|
||||
=> new float[6]; // one vertex worth
|
||||
}
|
||||
|
||||
public class ChunkStreamerTests
|
||||
{
|
||||
private static (ChunkStreamer streamer, World.World world, List<FakeRenderer> created) CreateStreamer(Configuration cfg)
|
||||
{
|
||||
var registry = new BlockRegistry();
|
||||
var store = new InMemoryChunkModificationStore();
|
||||
var world = new World.World(registry, store);
|
||||
var mesher = new FakeMesher();
|
||||
var executor = new SynchronousJobExecutor();
|
||||
var created = new List<FakeRenderer>();
|
||||
Func<float[], IChunkRenderer> factory = m =>
|
||||
{
|
||||
var r = new FakeRenderer(m);
|
||||
created.Add(r);
|
||||
return r;
|
||||
};
|
||||
Func<Vector3i, VoxelChunk> generate = _ => new VoxelChunk();
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, mesher, registry, executor, factory);
|
||||
return (streamer, world, created);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_FirstCall_LoadsAllChunksInLoadBox()
|
||||
{
|
||||
var cfg = new Configuration
|
||||
{
|
||||
RenderDistanceHorizontal = 1,
|
||||
RenderDistanceVertical = 0,
|
||||
MaxGenerateJobsInFlight = 100,
|
||||
MaxMeshUploadsPerFrame = 100,
|
||||
};
|
||||
var (streamer, world, created) = CreateStreamer(cfg);
|
||||
|
||||
streamer.Tick(new Vector3i(0, 0, 0));
|
||||
|
||||
// Load box H=1, V=0 → 3 × 1 × 3 = 9 chunks
|
||||
Assert.Equal(9, created.Count);
|
||||
for (int x = -1; x <= 1; x++)
|
||||
for (int z = -1; z <= 1; z++)
|
||||
{
|
||||
var pos = new Vector3i(x, 0, z);
|
||||
Assert.NotNull(world.GetChunk(pos));
|
||||
Assert.True(streamer.IsReady(pos));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_PlayerMoves_LoadsNewChunksOnly()
|
||||
{
|
||||
var cfg = new Configuration
|
||||
{
|
||||
RenderDistanceHorizontal = 1,
|
||||
RenderDistanceVertical = 0,
|
||||
MaxGenerateJobsInFlight = 100,
|
||||
MaxMeshUploadsPerFrame = 100,
|
||||
};
|
||||
var (streamer, world, created) = CreateStreamer(cfg);
|
||||
|
||||
streamer.Tick(new Vector3i(0, 0, 0)); // loads 9
|
||||
int firstWave = created.Count;
|
||||
streamer.Tick(new Vector3i(1, 0, 0)); // loads only the 3 new column to +x
|
||||
|
||||
Assert.Equal(firstWave + 3, created.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_RespectsMaxMeshUploadsPerFrame()
|
||||
{
|
||||
var cfg = new Configuration
|
||||
{
|
||||
RenderDistanceHorizontal = 1,
|
||||
RenderDistanceVertical = 0,
|
||||
MaxGenerateJobsInFlight = 100,
|
||||
MaxMeshUploadsPerFrame = 2, // cap drains per tick
|
||||
};
|
||||
var (streamer, _, created) = CreateStreamer(cfg);
|
||||
|
||||
// With sync executor every submitted job completes immediately and
|
||||
// posts into _completed. The cap throttles how many drain per Tick().
|
||||
streamer.Tick(new Vector3i(0, 0, 0)); // dispatch fills the queue; drain ≤ 2
|
||||
Assert.Equal(2, created.Count);
|
||||
|
||||
streamer.Tick(new Vector3i(0, 0, 0)); // same player chunk, drains 2 more
|
||||
Assert.Equal(4, created.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_ClampsYToWorldBounds()
|
||||
{
|
||||
var cfg = new Configuration
|
||||
{
|
||||
RenderDistanceHorizontal = 0,
|
||||
RenderDistanceVertical = 2,
|
||||
MaxGenerateJobsInFlight = 100,
|
||||
MaxMeshUploadsPerFrame = 100,
|
||||
};
|
||||
var (streamer, world, created) = CreateStreamer(cfg);
|
||||
|
||||
// Player at y=0 (chunk Y = 0). V=2 wants Y ∈ [-2, 2], but valid Y is [0, 8).
|
||||
// So only Y ∈ {0, 1, 2} qualifies → 1 × 3 × 1 = 3 chunks.
|
||||
streamer.Tick(new Vector3i(0, 0, 0));
|
||||
Assert.Equal(3, created.Count);
|
||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 0, 0)));
|
||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 1, 0)));
|
||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 2, 0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_AppliesModificationDiffOnLoad()
|
||||
{
|
||||
var cfg = new Configuration
|
||||
{
|
||||
RenderDistanceHorizontal = 0,
|
||||
RenderDistanceVertical = 0,
|
||||
MaxGenerateJobsInFlight = 100,
|
||||
MaxMeshUploadsPerFrame = 100,
|
||||
};
|
||||
var registry = new BlockRegistry();
|
||||
var store = new InMemoryChunkModificationStore();
|
||||
var world = new World.World(registry, store);
|
||||
var mesher = new FakeMesher();
|
||||
var executor = new SynchronousJobExecutor();
|
||||
Func<float[], IChunkRenderer> factory = m => new FakeRenderer(m);
|
||||
Func<Vector3i, VoxelChunk> generate = _ => new VoxelChunk(); // all air
|
||||
|
||||
// Pre-record an edit before the chunk loads.
|
||||
var pos = new Vector3i(0, 0, 0);
|
||||
int localIndex = (5 * VoxelChunk.Size + 6) * VoxelChunk.Size + 7;
|
||||
store.RecordEdit(pos, localIndex, registry.Stone.Id);
|
||||
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, mesher, registry, executor, factory);
|
||||
streamer.Tick(pos);
|
||||
|
||||
Assert.Equal(registry.Stone.Id, world.GetBlock(5, 6, 7).Id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user