fix(streaming): clear loading screen once the spawn surface band is ready
UpdateSpawnReady required every chunk in the spawn columns down to bedrock. With full-column loading, those deep chunks carry the lowest load priority in the whole render volume (dy^2 * VerticalPriorityWeight), so they stream in last -- the loading screen waited for nearly the entire render distance to generate (minutes on a tall mountain spawn, appearing as a hang). Gate readiness on just the near-surface band (surfaceTop-2 .. surfaceTop) of the spawn area instead. Those are the highest-priority chunks, so spawn-ready now flips in ~350ms. Deep chunks are occluded at spawn and stream in afterwards. Extracts a SurfaceTopChunk helper shared with CollectTargets.
This commit is contained in:
@@ -14,6 +14,10 @@ public sealed class ChunkStreamer : IDisposable
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<ChunkStreamer>();
|
||||
|
||||
// How many chunks below the surface the spawn area must load before the
|
||||
// loading screen clears (just enough to stand on and see; the rest streams in).
|
||||
private const int SpawnReadyDepthChunks = 2;
|
||||
|
||||
private readonly Configuration _cfg;
|
||||
private readonly IWorld _world;
|
||||
private readonly Func<Vector3i, VoxelChunk> _generate;
|
||||
@@ -101,7 +105,7 @@ public sealed class ChunkStreamer : IDisposable
|
||||
public int DrawVisible(Func<Vector3i, bool> isChunkVisible, Action<Vector3i, IChunkRenderer> draw)
|
||||
{
|
||||
int drawn = 0;
|
||||
foreach (var kvp in this._renderers)
|
||||
foreach (KeyValuePair<Vector3i, IChunkRenderer> kvp in this._renderers)
|
||||
{
|
||||
if (!isChunkVisible(kvp.Key)) continue;
|
||||
draw(kvp.Key, kvp.Value);
|
||||
@@ -112,7 +116,7 @@ public sealed class ChunkStreamer : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var r in this._renderers.Values) r.Dispose();
|
||||
foreach (IChunkRenderer r in this._renderers.Values) r.Dispose();
|
||||
this._renderers.Clear();
|
||||
}
|
||||
|
||||
@@ -124,10 +128,10 @@ public sealed class ChunkStreamer : IDisposable
|
||||
this._spawnAnchorSet = true;
|
||||
}
|
||||
|
||||
var target = new HashSet<Vector3i>();
|
||||
HashSet<Vector3i> target = new();
|
||||
this.CollectTargets(p, this._cfg.RenderDistanceHorizontal, target);
|
||||
|
||||
foreach (var pos in target)
|
||||
foreach (Vector3i pos in target)
|
||||
{
|
||||
if (this._failed.Contains(pos)) continue;
|
||||
if (this._tracked.Add(pos))
|
||||
@@ -143,12 +147,12 @@ public sealed class ChunkStreamer : IDisposable
|
||||
}
|
||||
|
||||
// Anything currently tracked but no longer in target → unload.
|
||||
var toRemove = new List<Vector3i>();
|
||||
foreach (var tracked in this._tracked)
|
||||
List<Vector3i> toRemove = new();
|
||||
foreach (Vector3i tracked in this._tracked)
|
||||
{
|
||||
if (!target.Contains(tracked)) toRemove.Add(tracked);
|
||||
}
|
||||
foreach (var pos in toRemove)
|
||||
foreach (Vector3i pos in toRemove)
|
||||
{
|
||||
this._tracked.Remove(pos);
|
||||
this._pendingUnload.Enqueue(pos);
|
||||
@@ -163,13 +167,13 @@ public sealed class ChunkStreamer : IDisposable
|
||||
{
|
||||
int keep = this._cfg.RenderDistanceHorizontal + 1;
|
||||
List<(int cx, int cz)>? stale = null;
|
||||
foreach (var key in this._surfaceTopCache.Keys)
|
||||
foreach ((int cx, int cz) key in this._surfaceTopCache.Keys)
|
||||
{
|
||||
if (Math.Abs(key.cx - p.X) > keep || Math.Abs(key.cz - p.Z) > keep)
|
||||
(stale ??= new()).Add(key);
|
||||
}
|
||||
if (stale != null)
|
||||
foreach (var key in stale) this._surfaceTopCache.Remove(key);
|
||||
foreach ((int cx, int cz) key in stale) this._surfaceTopCache.Remove(key);
|
||||
}
|
||||
|
||||
// Fills `into` with the chunk positions to load for a viewer at chunk
|
||||
@@ -186,19 +190,14 @@ public sealed class ChunkStreamer : IDisposable
|
||||
int cx = center.X + dx;
|
||||
int cz = center.Z + dz;
|
||||
|
||||
// Clamp the column top up to at least the sea-level chunk: where the
|
||||
// terrain surface is below sea level, WorldGenerator fills opaque water
|
||||
// from the surface up to SeaLevel, so the column must load through the
|
||||
// waterline or the submerged water surface renders as see-through holes.
|
||||
int surfaceTop = Math.Max(
|
||||
ChunkMath.FloorDiv(this.SurfaceTopHeight(cx, cz), VoxelChunk.Size),
|
||||
ChunkMath.FloorDiv(WorldConstants.SeaLevel, VoxelChunk.Size));
|
||||
ChunkStreamer.AddColumn(into, cx, cz, 0, surfaceTop);
|
||||
int top = this.SurfaceTopChunk(cx, cz);
|
||||
AddColumn(into, cx, cz, 0, top);
|
||||
|
||||
int clampedTop = Math.Min(WorldConstants.WorldHeightChunks - 1, surfaceTop);
|
||||
for (int cy = clampedTop + 1; cy < WorldConstants.WorldHeightChunks; cy++)
|
||||
// Player-built structures can rise above the surface; load any edited
|
||||
// section above the column top so they remain visible.
|
||||
for (int cy = top + 1; cy < WorldConstants.WorldHeightChunks; cy++)
|
||||
{
|
||||
var pos = new Vector3i(cx, cy, cz);
|
||||
Vector3i pos = new(cx, cy, cz);
|
||||
if (this._world.ModificationStore.HasDiff(pos)) into.Add(pos);
|
||||
}
|
||||
}
|
||||
@@ -212,6 +211,16 @@ public sealed class ChunkStreamer : IDisposable
|
||||
into.Add(new Vector3i(cx, cy, cz));
|
||||
}
|
||||
|
||||
// Chunk Y of a column's loaded top: the terrain surface chunk, raised to the
|
||||
// sea-level (waterline) chunk for submerged columns, and clamped to the world
|
||||
// ceiling. The bottom of a loaded column is always bedrock (cy 0).
|
||||
private int SurfaceTopChunk(int cx, int cz)
|
||||
=> Math.Min(
|
||||
WorldConstants.WorldHeightChunks - 1,
|
||||
Math.Max(
|
||||
ChunkMath.FloorDiv(this.SurfaceTopHeight(cx, cz), VoxelChunk.Size),
|
||||
ChunkMath.FloorDiv(WorldConstants.SeaLevel, VoxelChunk.Size)));
|
||||
|
||||
// Max terrain surface height across a chunk column's 16x16 footprint,
|
||||
// sampled at the four corners and the centre (cheap bound on the column's
|
||||
// top). Cached per column. The bottom of the loaded stack is always bedrock,
|
||||
@@ -264,17 +273,30 @@ public sealed class ChunkStreamer : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// The loading screen waits on this. It only needs the VISIBLE surface around
|
||||
// spawn to be ready — gating on the full bedrock column would block on the
|
||||
// deepest chunks, which carry the lowest load priority in the whole render
|
||||
// volume and stream in last (minutes for tall mountain spawn columns). Those
|
||||
// deep chunks are occluded at spawn and arrive shortly after.
|
||||
private void UpdateSpawnReady()
|
||||
{
|
||||
if (this._spawnReady || !this._spawnAnchorSet) return;
|
||||
|
||||
var spawnTargets = new HashSet<Vector3i>();
|
||||
this.CollectTargets(this._spawnAnchor, this._cfg.SpawnReadyRadiusHorizontal, spawnTargets);
|
||||
|
||||
foreach (var pos in spawnTargets)
|
||||
int hs = this._cfg.SpawnReadyRadiusHorizontal;
|
||||
Vector3i a = this._spawnAnchor;
|
||||
for (int dx = -hs; dx <= hs; dx++)
|
||||
for (int dz = -hs; dz <= hs; dz++)
|
||||
{
|
||||
if (this._failed.Contains(pos)) continue;
|
||||
if (!this._ready.Contains(pos)) return;
|
||||
int cx = a.X + dx;
|
||||
int cz = a.Z + dz;
|
||||
int top = this.SurfaceTopChunk(cx, cz);
|
||||
int bottom = Math.Max(0, top - SpawnReadyDepthChunks);
|
||||
for (int cy = bottom; cy <= top; cy++)
|
||||
{
|
||||
Vector3i pos = new(cx, cy, cz);
|
||||
if (this._failed.Contains(pos)) continue;
|
||||
if (!this._ready.Contains(pos)) return;
|
||||
}
|
||||
}
|
||||
|
||||
this._spawnReady = true;
|
||||
@@ -330,9 +352,9 @@ public sealed class ChunkStreamer : IDisposable
|
||||
new(0, -1, 0), new(0, 1, 0),
|
||||
new(0, 0, -1), new(0, 0, 1),
|
||||
};
|
||||
foreach (var off in offsets)
|
||||
foreach (Vector3i off in offsets)
|
||||
{
|
||||
var neighbor = pos + off;
|
||||
Vector3i neighbor = pos + off;
|
||||
if (this._ready.Contains(neighbor)) this._dirty.Add(neighbor);
|
||||
}
|
||||
}
|
||||
@@ -360,13 +382,13 @@ public sealed class ChunkStreamer : IDisposable
|
||||
if (chunk == null) return; // got unloaded between MarkDirty and now
|
||||
|
||||
NeighborSnapshots neighbors = this.SnapshotNeighbors(pos);
|
||||
VoxelChunk snapshotChunk = ChunkStreamer.CopyChunk(chunk); // worker reads from copy
|
||||
VoxelChunk snapshotChunk = CopyChunk(chunk); // worker reads from copy
|
||||
|
||||
this._inFlight++;
|
||||
|
||||
IChunkMesher mesher = this._mesher;
|
||||
BlockRegistry registry = this._registry;
|
||||
var completed = this._completed;
|
||||
ConcurrentQueue<ChunkJobResult> completed = this._completed;
|
||||
|
||||
this._executor.Submit(() =>
|
||||
{
|
||||
@@ -385,7 +407,7 @@ public sealed class ChunkStreamer : IDisposable
|
||||
|
||||
private static VoxelChunk CopyChunk(VoxelChunk source)
|
||||
{
|
||||
var copy = new VoxelChunk();
|
||||
VoxelChunk copy = new();
|
||||
for (int x = 0; x < VoxelChunk.Size; x++)
|
||||
for (int y = 0; y < VoxelChunk.Size; y++)
|
||||
for (int z = 0; z < VoxelChunk.Size; z++)
|
||||
@@ -400,13 +422,13 @@ public sealed class ChunkStreamer : IDisposable
|
||||
NeighborSnapshots neighbors = this.SnapshotNeighbors(pos);
|
||||
this._world.ModificationStore.TryGetDiff(pos, out IReadOnlyDictionary<int, ushort>? diff);
|
||||
|
||||
var job = new ChunkJob(pos, neighbors, diff);
|
||||
ChunkJob job = new(pos, neighbors, diff);
|
||||
this._inFlight++;
|
||||
|
||||
IChunkMesher mesher = this._mesher;
|
||||
BlockRegistry registry = this._registry;
|
||||
var generate = this._generate;
|
||||
var completed = this._completed;
|
||||
Func<Vector3i, VoxelChunk> generate = this._generate;
|
||||
ConcurrentQueue<ChunkJobResult> completed = this._completed;
|
||||
|
||||
this._executor.Submit(() =>
|
||||
{
|
||||
@@ -415,7 +437,7 @@ public sealed class ChunkStreamer : IDisposable
|
||||
VoxelChunk chunk = generate(job.Pos);
|
||||
if (job.Diff != null)
|
||||
{
|
||||
foreach (var (idx, id) in job.Diff)
|
||||
foreach ((int idx, ushort id) in job.Diff)
|
||||
{
|
||||
int lx = idx / (VoxelChunk.Size * VoxelChunk.Size);
|
||||
int ly = (idx / VoxelChunk.Size) % VoxelChunk.Size;
|
||||
@@ -442,7 +464,7 @@ public sealed class ChunkStreamer : IDisposable
|
||||
{
|
||||
VoxelChunk? c = this._world.GetChunk(p);
|
||||
if (c == null) return null;
|
||||
var copy = new ushort[VoxelChunk.Size * VoxelChunk.Size * VoxelChunk.Size];
|
||||
ushort[] copy = new ushort[VoxelChunk.Size * VoxelChunk.Size * VoxelChunk.Size];
|
||||
for (int x = 0; x < VoxelChunk.Size; x++)
|
||||
for (int y = 0; y < VoxelChunk.Size; y++)
|
||||
for (int z = 0; z < VoxelChunk.Size; z++)
|
||||
|
||||
Reference in New Issue
Block a user