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:
MDietermann
2026-06-01 20:43:31 +02:00
parent 94930faccb
commit 9c28bf152f
2 changed files with 51 additions and 1 deletions

View File

@@ -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()