fix(map): gate both backend swaps on tile-worker quiescence

mapNoteStorageChanged() guarded only the adopt direction. The teardown
direction repointed s_tile_fs away from &SD with no check at all, even
though that is the side that strands an open SD File: the worker holds
one for the whole of a download (up to the 12 s cap) and dereferences
the global backend for every filesystem operation of the request.
Every current caller happens to gate, but the format-failure path checks
only tileFetchPendingLoad() and the format-success path checks nothing,
so the invariant was one caller away from being violated.

The adopt branch's early return was also too broad. It skipped the dedup
clear, freeMapTiles() and the re-render that are the whole point of the
call, so swapping cards while a fetch was in flight left the previous
card's tiles on screen with the dedup ring suppressing the re-fetch until
sdHealthTick's retry landed.

Compute may_swap once, apply it to both branches, and always fall through
to the invalidation.

Signed-off-by: Pixel Perfect <me@pixp.cc>
This commit is contained in:
Pixel Perfect committed 2026-08-05 17:56:03 -07:00
1 parent 3ecb6b7677
commit 0288ebb993
1 file changed
+11 -6
+11 -6
View File
@@ -25845,7 +25845,16 @@ static void freeMapTiles() {
// * re-renders immediately when the map is the visible tab
static void mapNoteStorageChanged() {
#if CAP_SD || defined(TLORA_PAGER)
if (!s_sd_mounted && s_tile_fs == &SD) {
// A fetch snapshots its paths but still dereferences the global backend for
// every filesystem operation of the request, so the pointer may only move
// while the queue and the worker are both idle. That governs BOTH directions:
// dropping a lost card strands an open SD File exactly as surely as adopting
// a new one does. When the swap has to wait, sdHealthTick retries it at the
// first idle tick — but the invalidation below still has to run now, or a
// swapped card keeps showing the previous card's tiles with the dedup ring
// suppressing the re-fetch until that retry lands.
const bool may_swap = (tileFetchPendingLoad() == 0);
if (may_swap && !s_sd_mounted && s_tile_fs == &SD) {
if (s_tile_fs_default && s_tile_fs_default != &SD) {
// SD tile mode lost its card, but the dedicated LittleFS cache is still
// mounted. Keep maps online instead of disabling that healthy fallback.
@@ -25860,11 +25869,7 @@ static void mapNoteStorageChanged() {
s_tile_root_default[0] = '\0';
s_tiles_fs_ready = false;
}
} else if (s_sd_mounted && (s_tiles_from_sd || !s_tiles_fs_ready)) {
// A fetch snapshots paths but still dereferences the global backend for
// each filesystem operation. Do not repoint it mid-request; sdHealthTick
// retries this transition as soon as the queue and worker are idle.
if (s_tile_fs != &SD && tileFetchPendingLoad() > 0) return;
} else if (may_swap && s_sd_mounted && (s_tiles_from_sd || !s_tiles_fs_ready)) {
// Reinserted while SD mode is selected, or no internal backend exists.
// Preserve an existing LittleFS default so toggling SD mode off remains a
// valid fallback after any number of remove/reinsert cycles.