diff --git a/lib/tdeck_ui/UI/LXMF/MapScreen.cpp b/lib/tdeck_ui/UI/LXMF/MapScreen.cpp index 9d660fd2..b44b0cb7 100644 --- a/lib/tdeck_ui/UI/LXMF/MapScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/MapScreen.cpp @@ -78,7 +78,8 @@ MapScreen::MapScreen(lv_obj_t* parent) download_config_(makeDownloadConfig()), downloader_(download_store_, download_transport_, download_clock_, download_policy_, download_config_), - downloads_enabled_(false), download_failed_frame_epoch_(0U), + downloads_enabled_(false), screen_visible_(false), transport_close_epoch_(0U), + download_failed_frame_epoch_(0U), decode_failed_keys_{}, decode_failed_generations_{}, compressed_staging_(nullptr), state_mutex_(nullptr), worker_task_(nullptr), stop_requested_(false), @@ -309,11 +310,28 @@ void MapScreen::workerEntry(void* context) { void MapScreen::workerLoop() { Hardware::TDeck::TileStoreResult initialized = store_.initialize(); store_initialized_ = initialized == Hardware::TDeck::TileStoreResult::OK; + std::uint32_t handled_close_epoch = + transport_close_epoch_.load(std::memory_order_acquire); while (!stop_requested_.load(std::memory_order_acquire)) { + // The worker exclusively owns HTTP/TLS teardown. Keep a successful + // session across frame/zoom boundaries only while the map is visible + // and online acquisition remains explicitly enabled. + const bool downloads_enabled = + downloads_enabled_.load(std::memory_order_acquire); + const std::uint32_t close_epoch = + transport_close_epoch_.load(std::memory_order_acquire); + if (close_epoch != handled_close_epoch) { + downloader_.setEnabled(false); + download_transport_.disconnectIdle(); + handled_close_epoch = close_epoch; + } + const bool should_retain_download_transport = downloads_enabled && + screen_visible_.load(std::memory_order_acquire); + Pyxis::MapTileRequest request{}; bool have_request = false; if (lockState(pdMS_TO_TICKS(20))) { - if (requests_released_) { + if (requests_released_ && should_retain_download_transport) { have_request = presenter_.takeRequest(request); } unlockState(); @@ -328,26 +346,25 @@ void MapScreen::workerLoop() { completion.slot_token = request.slot_token; completion.slot_index = request.slot_index; completion.key = request.key; - completion.result = loadTile(request); - bool frame_drained = false; + completion.result = loadTile(request, handled_close_epoch); if (lockState(portMAX_DELAY)) { (void)presenter_.publishCompletion(completion); - frame_drained = presenter_.requestCount() == 0U; unlockState(); } - if (frame_drained) download_transport_.disconnectIdle(); } + download_transport_.disconnectIdle(); worker_exited_.store(true, std::memory_order_release); vTaskDelete(nullptr); } Pyxis::MapTileLoadResult MapScreen::loadTile( - const Pyxis::MapTileRequest& request) { + const Pyxis::MapTileRequest& request, std::uint32_t transport_epoch) { const Pyxis::MapTileLoadResult cached = readTile(request); if (cached != Pyxis::MapTileLoadResult::MISS && cached != Pyxis::MapTileLoadResult::INVALID_PNG) return cached; if (decodeFailedFor(request)) return Pyxis::MapTileLoadResult::DOWNLOAD_FAILED; - const Pyxis::MapTileLoadResult downloaded = downloadTile(request); + const Pyxis::MapTileLoadResult downloaded = + downloadTile(request, transport_epoch); if (downloaded != Pyxis::MapTileLoadResult::READY) return downloaded; const Pyxis::MapTileLoadResult decoded = readTile(request); if (decoded == Pyxis::MapTileLoadResult::INVALID_PNG) { @@ -375,10 +392,15 @@ void MapScreen::markDecodeFailed(const Pyxis::MapTileRequest& request) { } Pyxis::MapTileLoadResult MapScreen::downloadTile( - const Pyxis::MapTileRequest& request) { + const Pyxis::MapTileRequest& request, std::uint32_t transport_epoch) { const bool enabled = downloads_enabled_.load(std::memory_order_acquire); downloader_.setEnabled(enabled); - if (!enabled) return Pyxis::MapTileLoadResult::MISS; + if (!enabled || !screen_visible_.load(std::memory_order_acquire) || + transport_close_epoch_.load(std::memory_order_acquire) != transport_epoch) { + downloader_.setEnabled(false); + download_transport_.disconnectIdle(); + return Pyxis::MapTileLoadResult::MISS; + } if (download_failed_frame_epoch_ == request.frame_epoch) { return Pyxis::MapTileLoadResult::DOWNLOAD_FAILED; } @@ -393,8 +415,12 @@ Pyxis::MapTileLoadResult MapScreen::downloadTile( while (downloader_.isBusy()) { if (stop_requested_.load(std::memory_order_acquire) || - !downloads_enabled_.load(std::memory_order_acquire)) { + !downloads_enabled_.load(std::memory_order_acquire) || + !screen_visible_.load(std::memory_order_acquire) || + transport_close_epoch_.load(std::memory_order_acquire) != + transport_epoch) { downloader_.setEnabled(false); + download_transport_.disconnectIdle(); return Pyxis::MapTileLoadResult::DOWNLOAD_FAILED; } bool stale = false; @@ -662,6 +688,8 @@ bool MapScreen::applyOneCompletion() { } void MapScreen::show() { + screen_visible_.store(true, std::memory_order_release); + if (worker_task_) xTaskNotifyGive(worker_task_); if (lockState(pdMS_TO_TICKS(100))) { presenter_.show(); unlockState(); @@ -680,6 +708,10 @@ void MapScreen::show() { } void MapScreen::hide() { + if (screen_visible_.exchange(false, std::memory_order_acq_rel)) { + transport_close_epoch_.fetch_add(1U, std::memory_order_acq_rel); + } + if (worker_task_) xTaskNotifyGive(worker_task_); if (lockState(pdMS_TO_TICKS(100))) { presenter_.hide(); unlockState(); diff --git a/lib/tdeck_ui/UI/LXMF/MapScreen.h b/lib/tdeck_ui/UI/LXMF/MapScreen.h index a9cb436e..54d609b9 100644 --- a/lib/tdeck_ui/UI/LXMF/MapScreen.h +++ b/lib/tdeck_ui/UI/LXMF/MapScreen.h @@ -50,7 +50,12 @@ public: void serviceIo(); void updateModel(const Pyxis::MapView::Request& request); void setDownloadEnabled(bool enabled) { - downloads_enabled_.store(enabled, std::memory_order_release); + const bool was_enabled = + downloads_enabled_.exchange(enabled, std::memory_order_acq_rel); + if (was_enabled && !enabled) { + transport_close_epoch_.fetch_add(1U, std::memory_order_acq_rel); + } + if (worker_task_) xTaskNotifyGive(worker_task_); } // These methods only mutate the pre-created object pool and are invoked @@ -90,6 +95,8 @@ private: Hardware::TDeck::MapTileDownloadConfig download_config_; Hardware::TDeck::MapTileDownloader downloader_; std::atomic downloads_enabled_; + std::atomic screen_visible_; + std::atomic transport_close_epoch_; std::uint32_t download_failed_frame_epoch_; Hardware::TDeck::TileKey decode_failed_keys_[TILE_COUNT]; std::uint32_t decode_failed_generations_[TILE_COUNT]; @@ -110,9 +117,11 @@ private: static void workerEntry(void* context); void workerLoop(); - Pyxis::MapTileLoadResult loadTile(const Pyxis::MapTileRequest& request); + Pyxis::MapTileLoadResult loadTile(const Pyxis::MapTileRequest& request, + std::uint32_t transport_epoch); Pyxis::MapTileLoadResult readTile(const Pyxis::MapTileRequest& request); - Pyxis::MapTileLoadResult downloadTile(const Pyxis::MapTileRequest& request); + Pyxis::MapTileLoadResult downloadTile(const Pyxis::MapTileRequest& request, + std::uint32_t transport_epoch); bool decodeFailedFor(const Pyxis::MapTileRequest& request) const; void markDecodeFailed(const Pyxis::MapTileRequest& request); bool startWorker(); diff --git a/tests/build_scripts/test_map_tile_downloader_contract.py b/tests/build_scripts/test_map_tile_downloader_contract.py index 7f6a70bc..c4a57005 100644 --- a/tests/build_scripts/test_map_tile_downloader_contract.py +++ b/tests/build_scripts/test_map_tile_downloader_contract.py @@ -79,12 +79,25 @@ def test_default_endpoint_uses_current_chain_with_known_fallback_available(): def test_downloader_is_explicitly_opt_in_and_wired_only_for_visible_misses(): screen = MAP_SCREEN.read_text() settings = SETTINGS.read_text() - assert "downloadTile(request)" in screen + assert "downloadTile(request, transport_epoch)" in screen assert "downloader_.enqueue(request.key, request.frame_epoch)" in screen assert "presenter_.frameEpoch() != request.frame_epoch" in screen assert "download_failed_frame_epoch_ == request.frame_epoch" in screen - assert "presenter_.requestCount() == 0U" in screen - assert "download_transport_.disconnectIdle()" in screen + worker = screen[screen.index("void MapScreen::workerLoop()"): + screen.index("Pyxis::MapTileLoadResult MapScreen::loadTile")] + assert "frame_drained" not in worker + assert "retain_download_transport" in worker + assert "screen_visible_.load(std::memory_order_acquire)" in worker + assert "transport_close_epoch_.load(std::memory_order_acquire)" in worker + assert "requests_released_ && should_retain_download_transport" in worker + assert "download_transport_.disconnectIdle()" in worker + assert "screen_visible_.store(true, std::memory_order_release)" in screen + assert "screen_visible_.exchange(false, std::memory_order_acq_rel)" in screen + assert "transport_close_epoch_.fetch_add(1U, std::memory_order_acq_rel)" in screen + download = screen[screen.index("Pyxis::MapTileLoadResult MapScreen::downloadTile"): + screen.index("Pyxis::MapTileLoadResult MapScreen::readTile")] + assert "!screen_visible_.load(std::memory_order_acquire)" in download + assert "transport_close_epoch_.load(std::memory_order_acquire) !=" in download assert 'KEY_MAP_DOWNLOAD = "map_dl"' in settings assert "prefs.getBool(KEY_MAP_DOWNLOAD, false)" in settings assert "Download map tiles:" in settings