feat(streaming): load full column bedrock->surface to fix terrain holes
This commit is contained in:
@@ -5,17 +5,6 @@ using OpenTK.Mathematics;
|
|||||||
public sealed class Configuration
|
public sealed class Configuration
|
||||||
{
|
{
|
||||||
public int RenderDistanceHorizontal { get; set; } = 8;
|
public int RenderDistanceHorizontal { get; set; } = 8;
|
||||||
// Player-slab half-height in chunks. NOTE: repurposed by the visibility
|
|
||||||
// rework — this is no longer a global vertical load-box. It is how many
|
|
||||||
// chunks load above and below the player's chunk across the whole
|
|
||||||
// horizontal region, so caves at the player's depth render to the full
|
|
||||||
// horizontal distance.
|
|
||||||
public int RenderDistanceVertical { get; set; } = 2;
|
|
||||||
|
|
||||||
// Chunks loaded below the lowest surface chunk of each column. Gives the
|
|
||||||
// surface chunk a solid neighbor below (so its buried bottom faces cull)
|
|
||||||
// and covers cave mouths just under the surface.
|
|
||||||
public int SurfaceShellSkirtBelow { get; set; } = 1;
|
|
||||||
|
|
||||||
// Distance fog begins at this fraction of the fog-end distance (fog end is
|
// Distance fog begins at this fraction of the fog-end distance (fog end is
|
||||||
// derived from RenderDistanceHorizontal). 0.7 → fog starts at 70% of the edge.
|
// derived from RenderDistanceHorizontal). 0.7 → fog starts at 70% of the edge.
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ public sealed class Game : GameWindow
|
|||||||
{
|
{
|
||||||
base.OnLoad();
|
base.OnLoad();
|
||||||
|
|
||||||
Log.Information("Game starting with seed={Seed} renderDistanceH={H} renderDistanceV={V}",
|
Log.Information("Game starting with seed={Seed} renderDistanceH={H}",
|
||||||
this._cfg.Seed, this._cfg.RenderDistanceHorizontal, this._cfg.RenderDistanceVertical);
|
this._cfg.Seed, this._cfg.RenderDistanceHorizontal);
|
||||||
|
|
||||||
GL.ClearColor(this._cfg.SkyColor.X, this._cfg.SkyColor.Y, this._cfg.SkyColor.Z, 1.0f);
|
GL.ClearColor(this._cfg.SkyColor.X, this._cfg.SkyColor.Y, this._cfg.SkyColor.Z, 1.0f);
|
||||||
GL.Enable(EnableCap.DepthTest);
|
GL.Enable(EnableCap.DepthTest);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ public class ConfigurationTests
|
|||||||
{
|
{
|
||||||
var c = new Configuration();
|
var c = new Configuration();
|
||||||
Assert.True(c.RenderDistanceHorizontal >= 1);
|
Assert.True(c.RenderDistanceHorizontal >= 1);
|
||||||
Assert.True(c.RenderDistanceVertical >= 1);
|
|
||||||
Assert.True(c.SpawnReadyRadiusHorizontal >= 0);
|
Assert.True(c.SpawnReadyRadiusHorizontal >= 0);
|
||||||
Assert.True(c.MaxGenerateJobsInFlight >= 1);
|
Assert.True(c.MaxGenerateJobsInFlight >= 1);
|
||||||
Assert.True(c.MaxMeshUploadsPerFrame >= 1);
|
Assert.True(c.MaxMeshUploadsPerFrame >= 1);
|
||||||
@@ -32,7 +31,6 @@ public class ConfigurationTests
|
|||||||
|
|
||||||
var c2 = new Configuration
|
var c2 = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceVertical = 2,
|
|
||||||
SpawnReadyRadiusHorizontal = 0,
|
SpawnReadyRadiusHorizontal = 0,
|
||||||
MaxGenerateJobsInFlight = 2,
|
MaxGenerateJobsInFlight = 2,
|
||||||
MaxMeshUploadsPerFrame = 1,
|
MaxMeshUploadsPerFrame = 1,
|
||||||
@@ -43,7 +41,6 @@ public class ConfigurationTests
|
|||||||
WindowWidth = 800,
|
WindowWidth = 800,
|
||||||
WindowHeight = 600,
|
WindowHeight = 600,
|
||||||
};
|
};
|
||||||
Assert.Equal(2, c2.RenderDistanceVertical);
|
|
||||||
Assert.Equal(0, c2.SpawnReadyRadiusHorizontal);
|
Assert.Equal(0, c2.SpawnReadyRadiusHorizontal);
|
||||||
Assert.Equal(2, c2.MaxGenerateJobsInFlight);
|
Assert.Equal(2, c2.MaxGenerateJobsInFlight);
|
||||||
Assert.Equal(1, c2.MaxMeshUploadsPerFrame);
|
Assert.Equal(1, c2.MaxMeshUploadsPerFrame);
|
||||||
@@ -80,7 +77,6 @@ public class ConfigurationTests
|
|||||||
{
|
{
|
||||||
var c = new Configuration();
|
var c = new Configuration();
|
||||||
|
|
||||||
Assert.Equal(1, c.SurfaceShellSkirtBelow);
|
|
||||||
Assert.Equal(0.7f, c.FogStartFraction);
|
Assert.Equal(0.7f, c.FogStartFraction);
|
||||||
Assert.Equal(new OpenTK.Mathematics.Vector3(0.45f, 0.65f, 0.95f), c.SkyColor);
|
Assert.Equal(new OpenTK.Mathematics.Vector3(0.45f, 0.65f, 0.95f), c.SkyColor);
|
||||||
Assert.Equal(256, c.MinCaveY);
|
Assert.Equal(256, c.MinCaveY);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using VoxelEngine.World.Blocks;
|
|||||||
using VoxelEngine.World.Chunk;
|
using VoxelEngine.World.Chunk;
|
||||||
using VoxelEngine.World.Persistence;
|
using VoxelEngine.World.Persistence;
|
||||||
using VoxelEngine.World.Streaming;
|
using VoxelEngine.World.Streaming;
|
||||||
|
using VoxelEngine.World.WorldGen;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using VoxelChunk = VoxelEngine.World.Chunk.Chunk;
|
using VoxelChunk = VoxelEngine.World.Chunk.Chunk;
|
||||||
|
|
||||||
@@ -72,7 +73,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 1,
|
RenderDistanceHorizontal = 1,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
@@ -97,7 +97,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 1,
|
RenderDistanceHorizontal = 1,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
@@ -130,7 +129,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 1,
|
RenderDistanceHorizontal = 1,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 2, // cap drains per tick
|
MaxMeshUploadsPerFrame = 2, // cap drains per tick
|
||||||
};
|
};
|
||||||
@@ -151,19 +149,21 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 0,
|
RenderDistanceHorizontal = 0,
|
||||||
RenderDistanceVertical = 2,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
var (streamer, world, created) = CreateStreamer(cfg);
|
// Surface far above the world ceiling -> top chunk clamps to
|
||||||
|
// WorldHeightChunks-1 (31). Full column loads cy 0..31 and nothing above.
|
||||||
|
var (streamer, world, _) = CreateStreamer(cfg, surfaceHeightAt: (_, _) => 100_000);
|
||||||
|
|
||||||
// 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));
|
streamer.Tick(new Vector3i(0, 0, 0));
|
||||||
Assert.Equal(3, created.Count);
|
|
||||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 0, 0)));
|
for (int cy = 0; cy < WorldConstants.WorldHeightChunks; cy++)
|
||||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 1, 0)));
|
{
|
||||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 2, 0)));
|
var pos = new Vector3i(0, cy, 0);
|
||||||
|
Assert.NotNull(world.GetChunk(pos));
|
||||||
|
Assert.True(streamer.IsReady(pos));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -172,7 +172,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 0,
|
RenderDistanceHorizontal = 0,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
@@ -201,7 +200,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 0,
|
RenderDistanceHorizontal = 0,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
@@ -224,7 +222,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 0,
|
RenderDistanceHorizontal = 0,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
@@ -251,7 +248,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 1,
|
RenderDistanceHorizontal = 1,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
SpawnReadyRadiusHorizontal = 1,
|
SpawnReadyRadiusHorizontal = 1,
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 1, // throttle to expose progression
|
MaxMeshUploadsPerFrame = 1, // throttle to expose progression
|
||||||
@@ -271,7 +267,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 0,
|
RenderDistanceHorizontal = 0,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
@@ -296,7 +291,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 1,
|
RenderDistanceHorizontal = 1,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
@@ -318,7 +312,7 @@ public class ChunkStreamerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void MarkDirty_OnUntrackedChunk_IsNoOp()
|
public void MarkDirty_OnUntrackedChunk_IsNoOp()
|
||||||
{
|
{
|
||||||
var cfg = new Configuration { RenderDistanceHorizontal = 0, RenderDistanceVertical = 0 };
|
var cfg = new Configuration { RenderDistanceHorizontal = 0 };
|
||||||
var (streamer, _, created) = CreateStreamer(cfg);
|
var (streamer, _, created) = CreateStreamer(cfg);
|
||||||
|
|
||||||
streamer.MarkDirty(new Vector3i(99, 0, 99));
|
streamer.MarkDirty(new Vector3i(99, 0, 99));
|
||||||
@@ -334,7 +328,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 0,
|
RenderDistanceHorizontal = 0,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
@@ -368,7 +361,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 0,
|
RenderDistanceHorizontal = 0,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
@@ -400,20 +392,20 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 1,
|
RenderDistanceHorizontal = 1,
|
||||||
RenderDistanceVertical = 1,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 5, // player's chunk + 4 horizontal cardinals
|
MaxMeshUploadsPerFrame = 5, // player's chunk + 4 horizontal cardinals
|
||||||
VerticalPriorityWeight = 4,
|
VerticalPriorityWeight = 4,
|
||||||
};
|
};
|
||||||
var (streamer, world, _) = CreateStreamer(cfg);
|
// Surface at worldY 48 -> top chunk 3, so a full column is cy 0..3 and
|
||||||
|
// the player at Y=2 has in-column vertical neighbours at Y=1 and Y=3.
|
||||||
|
var (streamer, world, _) = CreateStreamer(cfg, surfaceHeightAt: (_, _) => 48);
|
||||||
|
|
||||||
// Player chunk at Y=2 so vertical neighbors Y=1 and Y=3 are valid.
|
|
||||||
var playerPos = new Vector3i(0, 2, 0);
|
var playerPos = new Vector3i(0, 2, 0);
|
||||||
streamer.Tick(playerPos);
|
streamer.Tick(playerPos);
|
||||||
|
|
||||||
// Drain budget = 5 = priority 0 (player) + 4 priority-1 (horizontal
|
// Drain budget = 5 = priority 0 (player) + 4 priority-1 (horizontal
|
||||||
// cardinals). With VerticalPriorityWeight=4, vertical cardinals
|
// cardinals). With VerticalPriorityWeight=4, vertical cardinals
|
||||||
// (Y=1, Y=3) have priority 4 — they should NOT be among the first 5.
|
// (Y=1, Y=3) have priority 4 -- they must NOT be among the first 5.
|
||||||
Assert.NotNull(world.GetChunk(playerPos));
|
Assert.NotNull(world.GetChunk(playerPos));
|
||||||
Assert.NotNull(world.GetChunk(new Vector3i( 1, 2, 0)));
|
Assert.NotNull(world.GetChunk(new Vector3i( 1, 2, 0)));
|
||||||
Assert.NotNull(world.GetChunk(new Vector3i(-1, 2, 0)));
|
Assert.NotNull(world.GetChunk(new Vector3i(-1, 2, 0)));
|
||||||
@@ -424,27 +416,55 @@ public class ChunkStreamerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Tick_LoadsSurfaceShell_AndExcludesAirAboveSurface()
|
public void Tick_LoadsFullColumn_FromBedrockToSurface()
|
||||||
{
|
{
|
||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 0, // single column at the player
|
RenderDistanceHorizontal = 0, // single column at the player
|
||||||
RenderDistanceVertical = 0, // slab is just the player's chunk
|
|
||||||
SurfaceShellSkirtBelow = 1,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
// Surface at worldY 40 → chunk Y 2. Shell = [2-1, 2] = {1, 2}. Slab at
|
// Surface at worldY 40 -> top chunk floor(40/16) = 2. Full column loads
|
||||||
// player Y=0 = {0}. Union = {0, 1, 2}. Chunk Y 3+ is air above surface.
|
// cy 0,1,2; chunk Y 3+ is air above the surface and must NOT load.
|
||||||
var (streamer, world, _) = CreateStreamer(cfg, surfaceHeightAt: (_, _) => 40);
|
var (streamer, world, _) = CreateStreamer(cfg, surfaceHeightAt: (_, _) => 40);
|
||||||
|
|
||||||
streamer.Tick(new Vector3i(0, 0, 0));
|
streamer.Tick(new Vector3i(0, 0, 0));
|
||||||
|
|
||||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 0, 0))); // slab
|
Assert.NotNull(world.GetChunk(new Vector3i(0, 0, 0))); // bedrock
|
||||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 1, 0))); // shell skirt
|
Assert.NotNull(world.GetChunk(new Vector3i(0, 1, 0))); // mid
|
||||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 2, 0))); // shell surface
|
Assert.NotNull(world.GetChunk(new Vector3i(0, 2, 0))); // surface
|
||||||
Assert.Null(world.GetChunk(new Vector3i(0, 3, 0))); // air above surface — not loaded
|
Assert.Null(world.GetChunk(new Vector3i(0, 3, 0))); // air above surface
|
||||||
Assert.Null(world.GetChunk(new Vector3i(0, 5, 0))); // air above surface — not loaded
|
Assert.Null(world.GetChunk(new Vector3i(0, 5, 0))); // air above surface
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Tick_LoadsEditedSectionAboveSurface()
|
||||||
|
{
|
||||||
|
var cfg = new Configuration
|
||||||
|
{
|
||||||
|
RenderDistanceHorizontal = 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();
|
||||||
|
|
||||||
|
// Surface at worldY 8 -> top chunk 0. A player edit sits in chunk Y 5,
|
||||||
|
// far above the surface; that edited section must still load.
|
||||||
|
var edited = new Vector3i(0, 5, 0);
|
||||||
|
store.RecordEdit(edited, 0, registry.Stone.Id);
|
||||||
|
|
||||||
|
var streamer = new ChunkStreamer(cfg, world, generate, (_, _) => 8, mesher, registry, executor, factory);
|
||||||
|
streamer.Tick(new Vector3i(0, 0, 0));
|
||||||
|
|
||||||
|
Assert.NotNull(world.GetChunk(new Vector3i(0, 0, 0))); // surface column
|
||||||
|
Assert.NotNull(world.GetChunk(edited)); // edited air section above surface
|
||||||
|
Assert.Null(world.GetChunk(new Vector3i(0, 3, 0))); // unedited air -- not loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -453,7 +473,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 1,
|
RenderDistanceHorizontal = 1,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
SpawnReadyRadiusHorizontal = 1,
|
SpawnReadyRadiusHorizontal = 1,
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
@@ -476,7 +495,6 @@ public class ChunkStreamerTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 1,
|
RenderDistanceHorizontal = 1,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ public class StreamingIntegrationTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 1,
|
RenderDistanceHorizontal = 1,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
@@ -58,7 +57,6 @@ public class StreamingIntegrationTests
|
|||||||
var cfg = new Configuration
|
var cfg = new Configuration
|
||||||
{
|
{
|
||||||
RenderDistanceHorizontal = 0,
|
RenderDistanceHorizontal = 0,
|
||||||
RenderDistanceVertical = 0,
|
|
||||||
MaxGenerateJobsInFlight = 100,
|
MaxGenerateJobsInFlight = 100,
|
||||||
MaxMeshUploadsPerFrame = 100,
|
MaxMeshUploadsPerFrame = 100,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
private readonly IWorld _world;
|
private readonly IWorld _world;
|
||||||
private readonly Func<Vector3i, VoxelChunk> _generate;
|
private readonly Func<Vector3i, VoxelChunk> _generate;
|
||||||
private readonly Func<int, int, int> _surfaceHeightAt;
|
private readonly Func<int, int, int> _surfaceHeightAt;
|
||||||
// Per chunk-column (cx,cz) surface (min,max) height range, memoized so each
|
// Per chunk-column (cx,cz) surface-TOP height, memoized so each column's
|
||||||
// column's samples are taken once and reused across ticks. Pruned to the
|
// samples are taken once and reused across ticks. Pruned to the horizontal
|
||||||
// horizontal window each recompute so it stays bounded.
|
// window each recompute so it stays bounded. Full-column loading only needs
|
||||||
private readonly Dictionary<(int cx, int cz), (int min, int max)> _surfaceRangeCache = new();
|
// the top; the bottom is always bedrock (cy 0).
|
||||||
|
private readonly Dictionary<(int cx, int cz), int> _surfaceTopCache = new();
|
||||||
private readonly IChunkMesher _mesher;
|
private readonly IChunkMesher _mesher;
|
||||||
private readonly BlockRegistry _registry;
|
private readonly BlockRegistry _registry;
|
||||||
private readonly IJobExecutor _executor;
|
private readonly IJobExecutor _executor;
|
||||||
@@ -156,42 +157,43 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
this.PruneSurfaceCache(p);
|
this.PruneSurfaceCache(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop cached surface ranges for columns outside the current horizontal
|
// Drop cached surface tops for columns outside the current horizontal
|
||||||
// window so the cache stays bounded as the player explores.
|
// window so the cache stays bounded as the player explores.
|
||||||
private void PruneSurfaceCache(Vector3i p)
|
private void PruneSurfaceCache(Vector3i p)
|
||||||
{
|
{
|
||||||
int keep = this._cfg.RenderDistanceHorizontal + 1;
|
int keep = this._cfg.RenderDistanceHorizontal + 1;
|
||||||
List<(int cx, int cz)>? stale = null;
|
List<(int cx, int cz)>? stale = null;
|
||||||
foreach (var key in this._surfaceRangeCache.Keys)
|
foreach (var key in this._surfaceTopCache.Keys)
|
||||||
{
|
{
|
||||||
if (Math.Abs(key.cx - p.X) > keep || Math.Abs(key.cz - p.Z) > keep)
|
if (Math.Abs(key.cx - p.X) > keep || Math.Abs(key.cz - p.Z) > keep)
|
||||||
(stale ??= new()).Add(key);
|
(stale ??= new()).Add(key);
|
||||||
}
|
}
|
||||||
if (stale != null)
|
if (stale != null)
|
||||||
foreach (var key in stale) this._surfaceRangeCache.Remove(key);
|
foreach (var key in stale) this._surfaceTopCache.Remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fills `into` with the chunk positions to load for a viewer at chunk
|
// Fills `into` with the chunk positions to load for a viewer at chunk
|
||||||
// `center`, over a square horizontal region of Chebyshev radius `radiusH`.
|
// `center`, over a square horizontal region of Chebyshev radius `radiusH`.
|
||||||
// Per column the loaded Y-range is the union of the surface shell and the
|
// Per column the full vertical stack from bedrock (cy 0) up to the
|
||||||
// player slab, clamped to the world's vertical bounds.
|
// surface-top chunk is loaded, plus any section above the surface holding
|
||||||
|
// player edits (e.g. a built tower).
|
||||||
private void CollectTargets(Vector3i center, int radiusH, HashSet<Vector3i> into)
|
private void CollectTargets(Vector3i center, int radiusH, HashSet<Vector3i> into)
|
||||||
{
|
{
|
||||||
int v = this._cfg.RenderDistanceVertical;
|
|
||||||
int skirt = this._cfg.SurfaceShellSkirtBelow;
|
|
||||||
|
|
||||||
for (int dx = -radiusH; dx <= radiusH; dx++)
|
for (int dx = -radiusH; dx <= radiusH; dx++)
|
||||||
for (int dz = -radiusH; dz <= radiusH; dz++)
|
for (int dz = -radiusH; dz <= radiusH; dz++)
|
||||||
{
|
{
|
||||||
int cx = center.X + dx;
|
int cx = center.X + dx;
|
||||||
int cz = center.Z + dz;
|
int cz = center.Z + dz;
|
||||||
|
|
||||||
(int minH, int maxH) = this.SurfaceHeightRange(cx, cz);
|
int surfaceTop = ChunkMath.FloorDiv(this.SurfaceTopHeight(cx, cz), VoxelChunk.Size);
|
||||||
int shellBot = ChunkMath.FloorDiv(minH, VoxelChunk.Size) - skirt;
|
ChunkStreamer.AddColumn(into, cx, cz, 0, surfaceTop);
|
||||||
int shellTop = ChunkMath.FloorDiv(maxH, VoxelChunk.Size);
|
|
||||||
ChunkStreamer.AddColumn(into, cx, cz, shellBot, shellTop);
|
|
||||||
|
|
||||||
ChunkStreamer.AddColumn(into, cx, cz, center.Y - v, center.Y + v);
|
int clampedTop = Math.Min(WorldConstants.WorldHeightChunks - 1, surfaceTop);
|
||||||
|
for (int cy = clampedTop + 1; cy < WorldConstants.WorldHeightChunks; cy++)
|
||||||
|
{
|
||||||
|
var pos = new Vector3i(cx, cy, cz);
|
||||||
|
if (this._world.ModificationStore.HasDiff(pos)) into.Add(pos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,11 +205,13 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
into.Add(new Vector3i(cx, cy, cz));
|
into.Add(new Vector3i(cx, cy, cz));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Min/max terrain surface height across a chunk column's 16×16 footprint,
|
// Max terrain surface height across a chunk column's 16x16 footprint,
|
||||||
// sampled at the four corners and the centre (catches slope spread cheaply).
|
// sampled at the four corners and the centre (cheap bound on the column's
|
||||||
private (int min, int max) SurfaceHeightRange(int cx, int cz)
|
// top). Cached per column. The bottom of the loaded stack is always bedrock,
|
||||||
|
// so only the top is needed.
|
||||||
|
private int SurfaceTopHeight(int cx, int cz)
|
||||||
{
|
{
|
||||||
if (this._surfaceRangeCache.TryGetValue((cx, cz), out (int min, int max) cached))
|
if (this._surfaceTopCache.TryGetValue((cx, cz), out int cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
int x0 = cx * VoxelChunk.Size;
|
int x0 = cx * VoxelChunk.Size;
|
||||||
@@ -217,18 +221,13 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
int xm = x0 + VoxelChunk.Size / 2;
|
int xm = x0 + VoxelChunk.Size / 2;
|
||||||
int zm = z0 + VoxelChunk.Size / 2;
|
int zm = z0 + VoxelChunk.Size / 2;
|
||||||
|
|
||||||
int h0 = this._surfaceHeightAt(x0, z0);
|
int max = Math.Max(
|
||||||
int h1 = this._surfaceHeightAt(x1, z0);
|
Math.Max(this._surfaceHeightAt(x0, z0), this._surfaceHeightAt(x1, z0)),
|
||||||
int h2 = this._surfaceHeightAt(x0, z1);
|
Math.Max(Math.Max(this._surfaceHeightAt(x0, z1), this._surfaceHeightAt(x1, z1)),
|
||||||
int h3 = this._surfaceHeightAt(x1, z1);
|
this._surfaceHeightAt(xm, zm)));
|
||||||
int h4 = this._surfaceHeightAt(xm, zm);
|
|
||||||
|
|
||||||
int min = Math.Min(Math.Min(h0, h1), Math.Min(Math.Min(h2, h3), h4));
|
this._surfaceTopCache[(cx, cz)] = max;
|
||||||
int max = Math.Max(Math.Max(h0, h1), Math.Max(Math.Max(h2, h3), h4));
|
return max;
|
||||||
|
|
||||||
(int min, int max) range = (min, max);
|
|
||||||
this._surfaceRangeCache[(cx, cz)] = range;
|
|
||||||
return range;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrainCompleted()
|
private void DrainCompleted()
|
||||||
|
|||||||
Reference in New Issue
Block a user