perf(streaming): cache per-column surface heights to fix move lag spikes
RecomputeTargetSet sampled TerrainSampler.HeightAt (a 169-point biome blend kernel) 5x per column over the whole render distance on the main thread every chunk crossing (~1,445 calls = ~250k biome lookups), causing multi-frame hitches while moving. Memoize each chunk column's (min,max) surface range, reused across ticks and pruned to the horizontal window, so each column is sampled once. Exact (no terrain change); spike is amortized to a thin edge strip per crossing and zero when stationary.
This commit is contained in:
@@ -18,6 +18,10 @@ public sealed class ChunkStreamer : IDisposable
|
||||
private readonly IWorld _world;
|
||||
private readonly Func<Vector3i, VoxelChunk> _generate;
|
||||
private readonly Func<int, int, int> _surfaceHeightAt;
|
||||
// Per chunk-column (cx,cz) surface (min,max) height range, memoized so each
|
||||
// column's samples are taken once and reused across ticks. Pruned to the
|
||||
// horizontal window each recompute so it stays bounded.
|
||||
private readonly Dictionary<(int cx, int cz), (int min, int max)> _surfaceRangeCache = new();
|
||||
private readonly IChunkMesher _mesher;
|
||||
private readonly BlockRegistry _registry;
|
||||
private readonly IJobExecutor _executor;
|
||||
@@ -148,6 +152,23 @@ public sealed class ChunkStreamer : IDisposable
|
||||
this._tracked.Remove(pos);
|
||||
this._pendingUnload.Enqueue(pos);
|
||||
}
|
||||
|
||||
this.PruneSurfaceCache(p);
|
||||
}
|
||||
|
||||
// Drop cached surface ranges for columns outside the current horizontal
|
||||
// window so the cache stays bounded as the player explores.
|
||||
private void PruneSurfaceCache(Vector3i p)
|
||||
{
|
||||
int keep = this._cfg.RenderDistanceHorizontal + 1;
|
||||
List<(int cx, int cz)>? stale = null;
|
||||
foreach (var key in this._surfaceRangeCache.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._surfaceRangeCache.Remove(key);
|
||||
}
|
||||
|
||||
// Fills `into` with the chunk positions to load for a viewer at chunk
|
||||
@@ -186,6 +207,9 @@ public sealed class ChunkStreamer : IDisposable
|
||||
// sampled at the four corners and the centre (catches slope spread cheaply).
|
||||
private (int min, int max) SurfaceHeightRange(int cx, int cz)
|
||||
{
|
||||
if (this._surfaceRangeCache.TryGetValue((cx, cz), out (int min, int max) cached))
|
||||
return cached;
|
||||
|
||||
int x0 = cx * VoxelChunk.Size;
|
||||
int z0 = cz * VoxelChunk.Size;
|
||||
int x1 = x0 + VoxelChunk.Size - 1;
|
||||
@@ -201,7 +225,10 @@ public sealed class ChunkStreamer : IDisposable
|
||||
|
||||
int min = Math.Min(Math.Min(h0, h1), Math.Min(Math.Min(h2, h3), h4));
|
||||
int max = Math.Max(Math.Max(h0, h1), Math.Max(Math.Max(h2, h3), h4));
|
||||
return (min, max);
|
||||
|
||||
(int min, int max) range = (min, max);
|
||||
this._surfaceRangeCache[(cx, cz)] = range;
|
||||
return range;
|
||||
}
|
||||
|
||||
private void DrainCompleted()
|
||||
|
||||
Reference in New Issue
Block a user