From f57ea0b70cd317ddeaddeec5a90ce4623a46c91c Mon Sep 17 00:00:00 2001 From: Christopher Wiebe Date: Fri, 1 May 2026 20:07:06 -0700 Subject: [PATCH] Fix ChunkStreamer.EnsureLoadedAround leaving pre-warmed chunks stuck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Theriapolis.Core/Tactical/ChunkStreamer.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Theriapolis.Core/Tactical/ChunkStreamer.cs b/Theriapolis.Core/Tactical/ChunkStreamer.cs index 97347fc..3df98c7 100644 --- a/Theriapolis.Core/Tactical/ChunkStreamer.cs +++ b/Theriapolis.Core/Tactical/ChunkStreamer.cs @@ -127,12 +127,19 @@ public sealed class ChunkStreamer active.Add(new ChunkCoord(cx, cy)); } - // Synchronously generate any missing active chunks (the player needs - // them this frame). Pre-warm the next ring on the threadpool. + // Synchronously make every active chunk live in the cache. Get() + // 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) { - if (!_cache.ContainsKey(cc) && !_inflight.ContainsKey(cc)) - _ = Get(cc); // synchronous generate + cache + if (!_cache.ContainsKey(cc)) + _ = Get(cc); // hits cache, drains inflight, or generates } for (int cy = centre.Y - chunkRadius - 1; cy <= centre.Y + chunkRadius + 1; cy++)