feat: ChunkStreamer unload path and SpawnReady property

This commit is contained in:
2026-06-01 11:32:05 +02:00
parent c1f0928344
commit 7d06706400
2 changed files with 147 additions and 11 deletions

View File

@@ -26,12 +26,17 @@ public sealed class ChunkStreamer : IDisposable
private readonly HashSet<Vector3i> _ready = new();
private readonly PriorityQueue<Vector3i, int> _pendingLoad = new();
private readonly ConcurrentQueue<ChunkJobResult> _completed = new();
private readonly Queue<Vector3i> _pendingUnload = new();
private Vector3i _lastPlayerChunk = new(int.MinValue, int.MinValue, int.MinValue);
private int _inFlight;
private bool _spawnReady;
private bool _spawnAnchorSet;
private Vector3i _spawnAnchor;
public int RendererCount => this._renderers.Count;
public bool IsReady(Vector3i pos) => this._ready.Contains(pos);
public bool SpawnReady => this._spawnReady;
public ChunkStreamer(
Configuration cfg,
@@ -59,14 +64,16 @@ public sealed class ChunkStreamer : IDisposable
this._lastPlayerChunk = playerChunkPos;
}
// Dispatch BEFORE drain: with a synchronous executor (tests) work
// completes before Submit() returns, so we want to pick up those
// results in this same tick. With the production threadpool executor,
// jobs don't finish synchronously — Drain harvests whatever previous
// ticks' workers completed in the meantime. Either way, this order is
// correct.
// Unloads first to free in-flight slots, then dispatch BEFORE drain:
// with a synchronous executor (tests), Submit() runs work to completion
// before returning, so a single tick must drain its own dispatches.
// With the production threadpool executor, jobs don't finish in this
// tick — Drain harvests results submitted on previous ticks. Either way
// this ordering is correct.
this.ProcessUnloads();
this.DispatchJobs();
this.DrainCompleted();
this.UpdateSpawnReady();
}
public void DrawAll(Action<Vector3i, IChunkRenderer> drawCallback)
@@ -82,22 +89,46 @@ public sealed class ChunkStreamer : IDisposable
private void RecomputeTargetSet(Vector3i p)
{
if (!this._spawnAnchorSet)
{
this._spawnAnchor = p;
this._spawnAnchorSet = true;
}
int h = this._cfg.RenderDistanceHorizontal;
int v = this._cfg.RenderDistanceVertical;
var target = new HashSet<Vector3i>();
for (int dx = -h; dx <= h; dx++)
for (int dy = -v; dy <= v; dy++)
for (int dz = -h; dz <= h; dz++)
{
int cy = p.Y + dy;
if (cy < 0 || cy >= WorldConstants.WorldHeightChunks) continue;
target.Add(new Vector3i(p.X + dx, cy, p.Z + dz));
}
var pos = new Vector3i(p.X + dx, cy, p.Z + dz);
if (this._tracked.Contains(pos)) continue;
foreach (var pos in target)
{
if (this._tracked.Add(pos))
{
int priority = (pos.X - p.X) * (pos.X - p.X)
+ (pos.Y - p.Y) * (pos.Y - p.Y)
+ (pos.Z - p.Z) * (pos.Z - p.Z);
this._pendingLoad.Enqueue(pos, priority);
}
}
this._tracked.Add(pos);
int priority = dx * dx + dy * dy + dz * dz;
this._pendingLoad.Enqueue(pos, priority);
// Anything currently tracked but no longer in target → unload.
var toRemove = new List<Vector3i>();
foreach (var tracked in this._tracked)
{
if (!target.Contains(tracked)) toRemove.Add(tracked);
}
foreach (var pos in toRemove)
{
this._tracked.Remove(pos);
this._pendingUnload.Enqueue(pos);
}
}
@@ -112,6 +143,41 @@ public sealed class ChunkStreamer : IDisposable
}
}
private void ProcessUnloads()
{
while (this._pendingUnload.TryDequeue(out Vector3i pos))
{
if (this._renderers.TryGetValue(pos, out IChunkRenderer? renderer))
{
renderer.Dispose();
this._renderers.Remove(pos);
}
this._world.RemoveChunk(pos);
this._ready.Remove(pos);
Log.Debug("Unloaded {Pos}", pos);
}
}
private void UpdateSpawnReady()
{
if (this._spawnReady || !this._spawnAnchorSet) return;
int hs = this._cfg.SpawnReadyRadiusHorizontal;
int v = this._cfg.RenderDistanceVertical;
Vector3i a = this._spawnAnchor;
for (int dx = -hs; dx <= hs; dx++)
for (int dy = -v; dy <= v; dy++)
for (int dz = -hs; dz <= hs; dz++)
{
int cy = a.Y + dy;
if (cy < 0 || cy >= WorldConstants.WorldHeightChunks) continue;
if (!this._ready.Contains(new Vector3i(a.X + dx, cy, a.Z + dz))) return;
}
this._spawnReady = true;
Log.Information("Spawn area ready at anchor {Anchor}", a);
}
private void InstallResult(ChunkJobResult result)
{
if (result.Error != null || result.Mesh == null || result.Chunk == null)