Fix ChunkStreamer.EnsureLoadedAround leaving pre-warmed chunks stuck

EnsureLoadedAround skipped Get() for any active chunk already in
_inflight. That worked for the MonoGame TacticalRenderer, which calls
Get() during its own draw loop and incidentally drains pre-warm tasks.
But subscribers to OnChunkLoaded (e.g. the Godot port) saw no event
when a previously-pre-warmed chunk transitioned into the active set on
a later frame — the chunk stayed in _inflight forever, presenting as
permanently-uncached gaps in the rendered world.

Fix: drop the !_inflight.ContainsKey(cc) guard. Get() already handles
all three paths (cache hit, inflight drain, fresh generate), so passing
every active chunk through Get() guarantees OnChunkLoaded fires once
per chunk regardless of how it was scheduled.

Same flavour of bug as M1's MoistureGen FastNoiseLite race —
cross-process / event-driven consumers exercise paths the in-process
pull-based test fixtures never hit. 708/708 tests still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Christopher Wiebe
2026-05-01 20:07:06 -07:00
parent a23cf8bd97
commit f57ea0b70c
+11 -4
View File
@@ -127,12 +127,19 @@ public sealed class ChunkStreamer
active.Add(new ChunkCoord(cx, cy)); active.Add(new ChunkCoord(cx, cy));
} }
// Synchronously generate any missing active chunks (the player needs // Synchronously make every active chunk live in the cache. Get()
// them this frame). Pre-warm the next ring on the threadpool. // handles all three paths: hit cache, drain a pre-warmed inflight
// task, or generate fresh. The previous version skipped chunks in
// _inflight, which left them stuck there indefinitely once they
// entered the active set on a later frame — visible to renderers
// that subscribe to OnChunkLoaded (e.g. the Godot port) as
// permanently-uncached gaps. The MonoGame TacticalRenderer dodged
// this by calling Get() inside its draw loop; M4 of the port made
// that drain unnecessary by fixing it here.
foreach (var cc in active) foreach (var cc in active)
{ {
if (!_cache.ContainsKey(cc) && !_inflight.ContainsKey(cc)) if (!_cache.ContainsKey(cc))
_ = Get(cc); // synchronous generate + cache _ = Get(cc); // hits cache, drains inflight, or generates
} }
for (int cy = centre.Y - chunkRadius - 1; cy <= centre.Y + chunkRadius + 1; cy++) for (int cy = centre.Y - chunkRadius - 1; cy <= centre.Y + chunkRadius + 1; cy++)