diff --git a/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs b/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs index 00211b7..a394ae5 100644 --- a/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs +++ b/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs @@ -35,6 +35,13 @@ internal sealed class FakeMesher : IChunkMesher => new float[6]; // one vertex worth } +// Mesher that returns an empty mesh, simulating a pure-air chunk. +internal sealed class EmptyMesher : IChunkMesher +{ + public float[] Build(VoxelChunk chunk, BlockRegistry registry, Func? neighborOpaqueOutside = null) + => Array.Empty(); +} + public class ChunkStreamerTests { private static (ChunkStreamer streamer, World.World world, List created) CreateStreamer(Configuration cfg) @@ -352,6 +359,38 @@ public class ChunkStreamerTests Assert.Equal(1, generateCalls); } + [Fact] + public void InstallResult_EmptyMesh_SkipsRendererCreationButMarksReady() + { + 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 EmptyMesher(); + var executor = new SynchronousJobExecutor(); + int rendererFactoryCalls = 0; + Func factory = m => + { + rendererFactoryCalls++; + return new FakeRenderer(m); + }; + Func generate = _ => new VoxelChunk(); + + var streamer = new ChunkStreamer(cfg, world, generate, mesher, registry, executor, factory); + var pos = new Vector3i(0, 0, 0); + streamer.Tick(pos); + + Assert.True(streamer.IsReady(pos)); + Assert.Equal(0, rendererFactoryCalls); + Assert.Equal(0, streamer.RendererCount); + } + [Fact] public void Tick_VerticalPriorityWeight_PrefersHorizontalAtSameDistance() { diff --git a/World/Streaming/ChunkStreamer.cs b/World/Streaming/ChunkStreamer.cs index c031d6d..81bfea9 100644 --- a/World/Streaming/ChunkStreamer.cs +++ b/World/Streaming/ChunkStreamer.cs @@ -210,8 +210,15 @@ public sealed class ChunkStreamer : IDisposable if (this._renderers.TryGetValue(result.Pos, out IChunkRenderer? old)) { old.Dispose(); + this._renderers.Remove(result.Pos); + } + // Skip renderer allocation for empty meshes (pure-air chunks above + // terrain, or chunks whose every face culls). The chunk still counts + // as ready — DrawAll's iteration over _renderers naturally no-ops. + if (result.Mesh.Length > 0) + { + this._renderers[result.Pos] = this._rendererFactory(result.Mesh); } - this._renderers[result.Pos] = this._rendererFactory(result.Mesh); this._ready.Add(result.Pos); this._dirty.Remove(result.Pos);