mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-09-16 18:02:36 +00:00
[verified] feat: prefer selected offline map packs
This commit is contained in:
@@ -129,6 +129,9 @@ public:
|
||||
bool takeResult(MapTileDownloadResult& result);
|
||||
|
||||
bool isBusy() const { return active_ || queue_count_ != 0U; }
|
||||
bool willStartTransportOnNextPump() const {
|
||||
return active_ && stage_ == Stage::SELECTED;
|
||||
}
|
||||
std::size_t queuedCount() const { return queue_count_; }
|
||||
std::size_t resultCount() const { return result_count_; }
|
||||
std::uint32_t droppedResultCount() const { return dropped_results_; }
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "MapScreen.h"
|
||||
#include "MapTileLookupPolicy.h"
|
||||
#include "MapTileStreamReader.h"
|
||||
#include "Hardware/TDeck/MapTileCa.h"
|
||||
|
||||
#ifdef ARDUINO
|
||||
@@ -39,6 +41,77 @@ Hardware::TDeck::MapTileDownloadConfig makeDownloadConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
class PackReadStream final : public Pyxis::MapTileReadStream {
|
||||
public:
|
||||
explicit PackReadStream(Hardware::TDeck::MapTilePack& pack) : pack_(pack) {}
|
||||
Pyxis::MapTileStreamResult begin(const Hardware::TDeck::TileKey& key,
|
||||
std::uint32_t& size) override {
|
||||
const Hardware::TDeck::MapTilePackResult result = pack_.beginGet(key, size);
|
||||
if (result == Hardware::TDeck::MapTilePackResult::OK) return Pyxis::MapTileStreamResult::OK;
|
||||
if (result == Hardware::TDeck::MapTilePackResult::UNCOVERED ||
|
||||
result == Hardware::TDeck::MapTilePackResult::TILE_MISSING ||
|
||||
result == Hardware::TDeck::MapTilePackResult::NOT_INITIALIZED) {
|
||||
return Pyxis::MapTileStreamResult::MISS;
|
||||
}
|
||||
if (result == Hardware::TDeck::MapTilePackResult::STORAGE_UNAVAILABLE) {
|
||||
return Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE;
|
||||
}
|
||||
return Pyxis::MapTileStreamResult::IO_ERROR;
|
||||
}
|
||||
Pyxis::MapTileStreamResult read(std::uint8_t* output, std::size_t capacity,
|
||||
std::size_t& count) override {
|
||||
const Hardware::TDeck::MapTilePackResult result =
|
||||
pack_.readGetChunk(output, capacity, count);
|
||||
if (result == Hardware::TDeck::MapTilePackResult::OK) return Pyxis::MapTileStreamResult::OK;
|
||||
if (result == Hardware::TDeck::MapTilePackResult::STORAGE_UNAVAILABLE) {
|
||||
return Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE;
|
||||
}
|
||||
return Pyxis::MapTileStreamResult::IO_ERROR;
|
||||
}
|
||||
void end() override { pack_.endGet(); }
|
||||
private:
|
||||
Hardware::TDeck::MapTilePack& pack_;
|
||||
};
|
||||
|
||||
class LiveReadStream final : public Pyxis::MapTileReadStream {
|
||||
public:
|
||||
explicit LiveReadStream(Hardware::TDeck::MapTileStore& store) : store_(store) {}
|
||||
Pyxis::MapTileStreamResult begin(const Hardware::TDeck::TileKey& key,
|
||||
std::uint32_t& size) override {
|
||||
const Hardware::TDeck::TileStoreResult result = store_.beginGet(key, size);
|
||||
if (result == Hardware::TDeck::TileStoreResult::OK) return Pyxis::MapTileStreamResult::OK;
|
||||
if (result == Hardware::TDeck::TileStoreResult::MISS) return Pyxis::MapTileStreamResult::MISS;
|
||||
if (result == Hardware::TDeck::TileStoreResult::STORAGE_UNAVAILABLE ||
|
||||
result == Hardware::TDeck::TileStoreResult::NOT_INITIALIZED) {
|
||||
return Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE;
|
||||
}
|
||||
return Pyxis::MapTileStreamResult::IO_ERROR;
|
||||
}
|
||||
Pyxis::MapTileStreamResult read(std::uint8_t* output, std::size_t capacity,
|
||||
std::size_t& count) override {
|
||||
const Hardware::TDeck::TileStoreResult result =
|
||||
store_.readGetChunk(output, capacity, count);
|
||||
if (result == Hardware::TDeck::TileStoreResult::OK) return Pyxis::MapTileStreamResult::OK;
|
||||
if (result == Hardware::TDeck::TileStoreResult::STORAGE_UNAVAILABLE) {
|
||||
return Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE;
|
||||
}
|
||||
return Pyxis::MapTileStreamResult::IO_ERROR;
|
||||
}
|
||||
void end() override { store_.endGet(); }
|
||||
private:
|
||||
Hardware::TDeck::MapTileStore& store_;
|
||||
};
|
||||
|
||||
class AtomicStopSource final : public Pyxis::MapTileStopSource {
|
||||
public:
|
||||
explicit AtomicStopSource(const std::atomic<bool>& stop) : stop_(stop) {}
|
||||
bool stopRequested() const override {
|
||||
return stop_.load(std::memory_order_acquire);
|
||||
}
|
||||
private:
|
||||
const std::atomic<bool>& stop_;
|
||||
};
|
||||
|
||||
lv_obj_t* createToolbarButton(lv_obj_t* parent, const char* text,
|
||||
lv_event_cb_t callback, void* context,
|
||||
lv_coord_t width) {
|
||||
@@ -73,22 +146,24 @@ MapScreen::MapScreen(lv_obj_t* parent)
|
||||
presenter_(), storage_(),
|
||||
store_config_{STORE_ENTRY_CAPACITY, STORE_BYTE_QUOTA,
|
||||
MAX_COMPRESSED_TILE_BYTES},
|
||||
store_(storage_, store_config_), download_store_(store_),
|
||||
store_(storage_, store_config_), pack_(storage_), download_store_(store_),
|
||||
download_transport_(), download_clock_(), download_policy_(),
|
||||
download_config_(makeDownloadConfig()),
|
||||
downloader_(download_store_, download_transport_, download_clock_,
|
||||
download_policy_, download_config_),
|
||||
downloads_enabled_(false), screen_visible_(false), transport_close_epoch_(0U),
|
||||
downloads_enabled_(false), screen_visible_(false), pack_refresh_epoch_(0U),
|
||||
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),
|
||||
state_mutex_(nullptr), transport_start_mutex_(nullptr), worker_task_(nullptr),
|
||||
worker_exited_(true), worker_started_(false), store_initialized_(false),
|
||||
requests_released_(false),
|
||||
has_location_fix_(false), center_initialized_(false), current_location_{}, dragging_(false),
|
||||
last_drag_point_{0, 0}, back_callback_() {
|
||||
LVGL_LOCK();
|
||||
state_mutex_ = xSemaphoreCreateMutex();
|
||||
transport_start_mutex_ = xSemaphoreCreateMutex();
|
||||
// Reserve the mandatory SD/PNG staging buffer before optional decoded
|
||||
// cache entries. A partial cache must never prevent the worker starting.
|
||||
compressed_staging_ = static_cast<std::uint8_t*>(heap_caps_malloc(
|
||||
@@ -257,6 +332,8 @@ MapScreen::~MapScreen() {
|
||||
compressed_staging_ = nullptr;
|
||||
if (state_mutex_) vSemaphoreDelete(state_mutex_);
|
||||
state_mutex_ = nullptr;
|
||||
if (transport_start_mutex_) vSemaphoreDelete(transport_start_mutex_);
|
||||
transport_start_mutex_ = nullptr;
|
||||
}
|
||||
|
||||
bool MapScreen::lockState(TickType_t ticks) {
|
||||
@@ -267,9 +344,26 @@ void MapScreen::unlockState() {
|
||||
xSemaphoreGive(state_mutex_);
|
||||
}
|
||||
|
||||
void MapScreen::synchronizeTransportStart() {
|
||||
if (transport_start_mutex_ &&
|
||||
xSemaphoreTake(transport_start_mutex_, portMAX_DELAY) == pdTRUE) {
|
||||
xSemaphoreGive(transport_start_mutex_);
|
||||
}
|
||||
}
|
||||
|
||||
void MapScreen::setDownloadEnabled(bool enabled) {
|
||||
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_);
|
||||
if (!enabled) synchronizeTransportStart();
|
||||
}
|
||||
|
||||
bool MapScreen::startWorker() {
|
||||
if (worker_started_) return true;
|
||||
if (!state_mutex_ || !compressed_staging_) return false;
|
||||
if (!state_mutex_ || !transport_start_mutex_ || !compressed_staging_) return false;
|
||||
stop_requested_.store(false, std::memory_order_release);
|
||||
worker_exited_.store(false, std::memory_order_release);
|
||||
const BaseType_t created = xTaskCreatePinnedToCore(
|
||||
@@ -310,14 +404,15 @@ 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_pack_refresh_epoch =
|
||||
pack_refresh_epoch_.load(std::memory_order_acquire);
|
||||
(void)pack_.initialize();
|
||||
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) {
|
||||
@@ -325,13 +420,31 @@ void MapScreen::workerLoop() {
|
||||
download_transport_.disconnectIdle();
|
||||
handled_close_epoch = close_epoch;
|
||||
}
|
||||
const bool should_retain_download_transport = downloads_enabled &&
|
||||
const bool screen_visible =
|
||||
screen_visible_.load(std::memory_order_acquire);
|
||||
const std::uint32_t pack_refresh_epoch =
|
||||
pack_refresh_epoch_.load(std::memory_order_acquire);
|
||||
if (pack_refresh_epoch != handled_pack_refresh_epoch) {
|
||||
const bool had_selection = pack_.hasSelection();
|
||||
char previous_pack_id[Pyxis::MapPackManifest::PACK_ID_CAPACITY] = {};
|
||||
if (had_selection) {
|
||||
std::memcpy(previous_pack_id, pack_.metadata().pack_id,
|
||||
sizeof(previous_pack_id));
|
||||
}
|
||||
(void)pack_.initialize();
|
||||
const bool has_selection = pack_.hasSelection();
|
||||
if (had_selection != has_selection ||
|
||||
(had_selection && has_selection &&
|
||||
std::strcmp(previous_pack_id, pack_.metadata().pack_id) != 0)) {
|
||||
decoded_tile_cache_.clear();
|
||||
}
|
||||
handled_pack_refresh_epoch = pack_refresh_epoch;
|
||||
}
|
||||
|
||||
Pyxis::MapTileRequest request{};
|
||||
bool have_request = false;
|
||||
if (lockState(pdMS_TO_TICKS(20))) {
|
||||
if (requests_released_ && should_retain_download_transport) {
|
||||
if (requests_released_ && screen_visible) {
|
||||
have_request = presenter_.takeRequest(request);
|
||||
}
|
||||
unlockState();
|
||||
@@ -360,9 +473,14 @@ void MapScreen::workerLoop() {
|
||||
Pyxis::MapTileLoadResult MapScreen::loadTile(
|
||||
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 bool decode_failed = decodeFailedFor(request);
|
||||
if (!Pyxis::MapTileLookupPolicy::shouldStartOnline(
|
||||
cached, downloads_enabled_.load(std::memory_order_acquire),
|
||||
screen_visible_.load(std::memory_order_acquire), decode_failed,
|
||||
transport_epoch,
|
||||
transport_close_epoch_.load(std::memory_order_acquire))) {
|
||||
return decode_failed ? Pyxis::MapTileLoadResult::DOWNLOAD_FAILED : cached;
|
||||
}
|
||||
const Pyxis::MapTileLoadResult downloaded =
|
||||
downloadTile(request, transport_epoch);
|
||||
if (downloaded != Pyxis::MapTileLoadResult::READY) return downloaded;
|
||||
@@ -433,7 +551,25 @@ Pyxis::MapTileLoadResult MapScreen::downloadTile(
|
||||
(void)downloader_.cancelGeneration(request.frame_epoch);
|
||||
download_transport_.disconnectIdle();
|
||||
}
|
||||
(void)downloader_.pump();
|
||||
if (downloader_.willStartTransportOnNextPump()) {
|
||||
if (xSemaphoreTake(transport_start_mutex_, portMAX_DELAY) == pdTRUE) {
|
||||
const bool start_allowed =
|
||||
!stop_requested_.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;
|
||||
if (start_allowed) {
|
||||
(void)downloader_.pump();
|
||||
} else {
|
||||
downloader_.setEnabled(false);
|
||||
download_transport_.disconnectIdle();
|
||||
}
|
||||
xSemaphoreGive(transport_start_mutex_);
|
||||
}
|
||||
} else {
|
||||
(void)downloader_.pump();
|
||||
}
|
||||
if (downloader_.isBusy()) vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
|
||||
@@ -464,44 +600,49 @@ Pyxis::MapTileLoadResult MapScreen::readTile(
|
||||
TILE_PIXEL_COUNT)) {
|
||||
return Pyxis::MapTileLoadResult::READY;
|
||||
}
|
||||
if (!store_initialized_) {
|
||||
|
||||
struct LocalReadContext {
|
||||
MapScreen* screen;
|
||||
const Pyxis::MapTileRequest* request;
|
||||
} context = {this, &request};
|
||||
return Pyxis::MapTileLookupPolicy::readLocal(
|
||||
&context,
|
||||
[](void* opaque, Pyxis::MapTileLookupPolicy::LocalSource source) ->
|
||||
Pyxis::MapTileLoadResult {
|
||||
LocalReadContext* local = static_cast<LocalReadContext*>(opaque);
|
||||
const CompressedTileSource mapped =
|
||||
source == Pyxis::MapTileLookupPolicy::LocalSource::PACK
|
||||
? CompressedTileSource::PACK
|
||||
: CompressedTileSource::LIVE_STORE;
|
||||
return local->screen->readCompressedTile(*local->request, mapped);
|
||||
});
|
||||
}
|
||||
|
||||
Pyxis::MapTileLoadResult MapScreen::readCompressedTile(
|
||||
const Pyxis::MapTileRequest& request, CompressedTileSource source) {
|
||||
if (source == CompressedTileSource::LIVE_STORE && !store_initialized_) {
|
||||
return Pyxis::MapTileLoadResult::STORAGE_UNAVAILABLE;
|
||||
}
|
||||
std::uint32_t size = 0U;
|
||||
Hardware::TDeck::TileStoreResult result =
|
||||
store_.beginGet(request.key, size);
|
||||
if (result == Hardware::TDeck::TileStoreResult::MISS) {
|
||||
PackReadStream pack_stream(pack_);
|
||||
LiveReadStream live_stream(store_);
|
||||
Pyxis::MapTileReadStream& stream = source == CompressedTileSource::PACK
|
||||
? static_cast<Pyxis::MapTileReadStream&>(pack_stream)
|
||||
: static_cast<Pyxis::MapTileReadStream&>(live_stream);
|
||||
AtomicStopSource stop(stop_requested_);
|
||||
std::size_t total = 0U;
|
||||
const Pyxis::MapTileStreamResult read = Pyxis::MapTileStreamReader::readExact(
|
||||
stream, stop, request.key, compressed_staging_, MAX_COMPRESSED_TILE_BYTES,
|
||||
READ_CHUNK_BYTES, total);
|
||||
if (read == Pyxis::MapTileStreamResult::MISS) {
|
||||
return Pyxis::MapTileLoadResult::MISS;
|
||||
}
|
||||
if (result == Hardware::TDeck::TileStoreResult::STORAGE_UNAVAILABLE ||
|
||||
result == Hardware::TDeck::TileStoreResult::NOT_INITIALIZED) {
|
||||
if (read == Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE) {
|
||||
return Pyxis::MapTileLoadResult::STORAGE_UNAVAILABLE;
|
||||
}
|
||||
if (result != Hardware::TDeck::TileStoreResult::OK) {
|
||||
return Pyxis::MapTileLoadResult::IO_ERROR;
|
||||
}
|
||||
if (size > MAX_COMPRESSED_TILE_BYTES) {
|
||||
store_.endGet();
|
||||
if (read == Pyxis::MapTileStreamResult::TOO_LARGE) {
|
||||
return Pyxis::MapTileLoadResult::TOO_LARGE;
|
||||
}
|
||||
std::size_t total = 0U;
|
||||
while (total < size &&
|
||||
!stop_requested_.load(std::memory_order_acquire)) {
|
||||
const std::size_t capacity =
|
||||
(size - total) < READ_CHUNK_BYTES ? (size - total) : READ_CHUNK_BYTES;
|
||||
std::size_t count = 0U;
|
||||
result = store_.readGetChunk(compressed_staging_ + total,
|
||||
capacity, count);
|
||||
if (result != Hardware::TDeck::TileStoreResult::OK || count == 0U) {
|
||||
store_.endGet();
|
||||
return result == Hardware::TDeck::TileStoreResult::STORAGE_UNAVAILABLE
|
||||
? Pyxis::MapTileLoadResult::STORAGE_UNAVAILABLE
|
||||
: Pyxis::MapTileLoadResult::IO_ERROR;
|
||||
}
|
||||
total += count;
|
||||
}
|
||||
store_.endGet();
|
||||
if (total != size || stop_requested_.load(std::memory_order_acquire)) {
|
||||
if (read != Pyxis::MapTileStreamResult::OK) {
|
||||
return Pyxis::MapTileLoadResult::IO_ERROR;
|
||||
}
|
||||
|
||||
@@ -517,11 +658,15 @@ Pyxis::MapTileLoadResult MapScreen::readTile(
|
||||
&width, &height, &decode_state, compressed_staging_, total);
|
||||
if (decode_error != 0U || width != 256U || height != 256U) {
|
||||
lodepng_state_cleanup(&decode_state);
|
||||
const Hardware::TDeck::TileStoreResult removed = store_.removeTile(request.key);
|
||||
return (removed == Hardware::TDeck::TileStoreResult::OK ||
|
||||
removed == Hardware::TDeck::TileStoreResult::MISS)
|
||||
? Pyxis::MapTileLoadResult::INVALID_PNG
|
||||
: Pyxis::MapTileLoadResult::IO_ERROR;
|
||||
if (source == CompressedTileSource::LIVE_STORE) {
|
||||
const Hardware::TDeck::TileStoreResult removed =
|
||||
store_.removeTile(request.key);
|
||||
return (removed == Hardware::TDeck::TileStoreResult::OK ||
|
||||
removed == Hardware::TDeck::TileStoreResult::MISS)
|
||||
? Pyxis::MapTileLoadResult::INVALID_PNG
|
||||
: Pyxis::MapTileLoadResult::IO_ERROR;
|
||||
}
|
||||
return Pyxis::MapTileLoadResult::INVALID_PNG;
|
||||
}
|
||||
decode_state.info_raw.colortype = LCT_RGB;
|
||||
decode_state.info_raw.bitdepth = 8U;
|
||||
@@ -530,11 +675,15 @@ Pyxis::MapTileLoadResult MapScreen::readTile(
|
||||
lodepng_state_cleanup(&decode_state);
|
||||
if (decode_error != 0U || rgb == nullptr || width != 256U || height != 256U) {
|
||||
if (rgb) lv_mem_free(rgb);
|
||||
const Hardware::TDeck::TileStoreResult removed = store_.removeTile(request.key);
|
||||
return (removed == Hardware::TDeck::TileStoreResult::OK ||
|
||||
removed == Hardware::TDeck::TileStoreResult::MISS)
|
||||
? Pyxis::MapTileLoadResult::INVALID_PNG
|
||||
: Pyxis::MapTileLoadResult::IO_ERROR;
|
||||
if (source == CompressedTileSource::LIVE_STORE) {
|
||||
const Hardware::TDeck::TileStoreResult removed =
|
||||
store_.removeTile(request.key);
|
||||
return (removed == Hardware::TDeck::TileStoreResult::OK ||
|
||||
removed == Hardware::TDeck::TileStoreResult::MISS)
|
||||
? Pyxis::MapTileLoadResult::INVALID_PNG
|
||||
: Pyxis::MapTileLoadResult::IO_ERROR;
|
||||
}
|
||||
return Pyxis::MapTileLoadResult::INVALID_PNG;
|
||||
}
|
||||
if (request.slot_index >= TILE_COUNT || !tile_pixels_[request.slot_index]) {
|
||||
lv_mem_free(rgb);
|
||||
@@ -688,6 +837,7 @@ bool MapScreen::applyOneCompletion() {
|
||||
}
|
||||
|
||||
void MapScreen::show() {
|
||||
pack_refresh_epoch_.fetch_add(1U, std::memory_order_acq_rel);
|
||||
screen_visible_.store(true, std::memory_order_release);
|
||||
if (worker_task_) xTaskNotifyGive(worker_task_);
|
||||
if (lockState(pdMS_TO_TICKS(100))) {
|
||||
@@ -712,6 +862,7 @@ void MapScreen::hide() {
|
||||
transport_close_epoch_.fetch_add(1U, std::memory_order_acq_rel);
|
||||
}
|
||||
if (worker_task_) xTaskNotifyGive(worker_task_);
|
||||
synchronizeTransportStart();
|
||||
if (lockState(pdMS_TO_TICKS(100))) {
|
||||
presenter_.hide();
|
||||
unlockState();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "Hardware/TDeck/MapTileStore.h"
|
||||
#include "Hardware/TDeck/MapTileStoreSD.h"
|
||||
#include "Hardware/TDeck/MapTilePack.h"
|
||||
#include "Hardware/TDeck/MapTileDownloader.h"
|
||||
#include "Hardware/TDeck/MapTileHttpArduino.h"
|
||||
|
||||
@@ -49,14 +50,7 @@ public:
|
||||
// These methods never call LVGL and are invoked before LVGL_LOCK.
|
||||
void serviceIo();
|
||||
void updateModel(const Pyxis::MapView::Request& request);
|
||||
void setDownloadEnabled(bool enabled) {
|
||||
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_);
|
||||
}
|
||||
void setDownloadEnabled(bool enabled);
|
||||
|
||||
// These methods only mutate the pre-created object pool and are invoked
|
||||
// while UIManager owns LVGL_LOCK.
|
||||
@@ -88,6 +82,7 @@ private:
|
||||
Hardware::TDeck::MapTileStoreSD storage_;
|
||||
Hardware::TDeck::TileStoreConfig store_config_;
|
||||
Hardware::TDeck::MapTileStore store_;
|
||||
Hardware::TDeck::MapTilePack pack_;
|
||||
Hardware::TDeck::MapTileStoreDownloadAdapter download_store_;
|
||||
Hardware::TDeck::MapTileHttpArduino download_transport_;
|
||||
Hardware::TDeck::MapTileMillisClock download_clock_;
|
||||
@@ -96,12 +91,14 @@ private:
|
||||
Hardware::TDeck::MapTileDownloader downloader_;
|
||||
std::atomic<bool> downloads_enabled_;
|
||||
std::atomic<bool> screen_visible_;
|
||||
std::atomic<std::uint32_t> pack_refresh_epoch_;
|
||||
std::atomic<std::uint32_t> 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];
|
||||
std::uint8_t* compressed_staging_;
|
||||
SemaphoreHandle_t state_mutex_;
|
||||
SemaphoreHandle_t transport_start_mutex_;
|
||||
TaskHandle_t worker_task_;
|
||||
std::atomic<bool> stop_requested_;
|
||||
std::atomic<bool> worker_exited_;
|
||||
@@ -116,10 +113,16 @@ private:
|
||||
BackCallback back_callback_;
|
||||
|
||||
static void workerEntry(void* context);
|
||||
enum class CompressedTileSource : std::uint8_t {
|
||||
PACK = 0,
|
||||
LIVE_STORE
|
||||
};
|
||||
void workerLoop();
|
||||
Pyxis::MapTileLoadResult loadTile(const Pyxis::MapTileRequest& request,
|
||||
std::uint32_t transport_epoch);
|
||||
Pyxis::MapTileLoadResult readTile(const Pyxis::MapTileRequest& request);
|
||||
Pyxis::MapTileLoadResult readCompressedTile(
|
||||
const Pyxis::MapTileRequest& request, CompressedTileSource source);
|
||||
Pyxis::MapTileLoadResult downloadTile(const Pyxis::MapTileRequest& request,
|
||||
std::uint32_t transport_epoch);
|
||||
bool decodeFailedFor(const Pyxis::MapTileRequest& request) const;
|
||||
@@ -128,6 +131,7 @@ private:
|
||||
void stopWorker();
|
||||
bool lockState(TickType_t ticks = portMAX_DELAY);
|
||||
void unlockState();
|
||||
void synchronizeTransportStart();
|
||||
void setPlaceholder(std::size_t index);
|
||||
void setStatusFor(Pyxis::MapTileLoadResult result);
|
||||
void pan(double dx, double dy);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2026 Pyxis contributors
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef UI_LXMF_MAP_TILE_LOOKUP_POLICY_H
|
||||
#define UI_LXMF_MAP_TILE_LOOKUP_POLICY_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "MapScreenPresenter.h"
|
||||
|
||||
namespace Pyxis {
|
||||
|
||||
/** Portable policy for pack -> live cache -> optional network orchestration. */
|
||||
class MapTileLookupPolicy {
|
||||
public:
|
||||
enum class LocalSource : std::uint8_t { PACK = 0, LIVE_STORE };
|
||||
typedef MapTileLoadResult (*ReadLocalSource)(void*, LocalSource);
|
||||
|
||||
static MapTileLoadResult readLocal(void* context, ReadLocalSource read) {
|
||||
if (read == NULL) return MapTileLoadResult::IO_ERROR;
|
||||
const MapTileLoadResult pack = read(context, LocalSource::PACK);
|
||||
if (pack == MapTileLoadResult::READY) return pack;
|
||||
const MapTileLoadResult live = read(context, LocalSource::LIVE_STORE);
|
||||
return resolveLocal(pack, live);
|
||||
}
|
||||
|
||||
static MapTileLoadResult resolveLocal(MapTileLoadResult pack,
|
||||
MapTileLoadResult live) {
|
||||
if (pack == MapTileLoadResult::READY) return pack;
|
||||
if (live != MapTileLoadResult::MISS) return live;
|
||||
if (pack == MapTileLoadResult::IO_ERROR ||
|
||||
pack == MapTileLoadResult::STORAGE_UNAVAILABLE) return pack;
|
||||
return MapTileLoadResult::MISS;
|
||||
}
|
||||
|
||||
static bool shouldStartOnline(MapTileLoadResult local,
|
||||
bool enabled,
|
||||
bool visible,
|
||||
bool decode_failed,
|
||||
std::uint32_t request_epoch,
|
||||
std::uint32_t current_epoch) {
|
||||
return enabled && visible && !decode_failed &&
|
||||
request_epoch == current_epoch &&
|
||||
(local == MapTileLoadResult::MISS ||
|
||||
local == MapTileLoadResult::INVALID_PNG);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Pyxis
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2026 Pyxis contributors
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef UI_LXMF_MAP_TILE_STREAM_READER_H
|
||||
#define UI_LXMF_MAP_TILE_STREAM_READER_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "Hardware/TDeck/MapTileStore.h"
|
||||
|
||||
namespace Pyxis {
|
||||
|
||||
enum class MapTileStreamResult : std::uint8_t {
|
||||
OK = 0,
|
||||
MISS,
|
||||
STORAGE_UNAVAILABLE,
|
||||
TOO_LARGE,
|
||||
IO_ERROR
|
||||
};
|
||||
|
||||
class MapTileReadStream {
|
||||
public:
|
||||
virtual ~MapTileReadStream() {}
|
||||
virtual MapTileStreamResult begin(const Hardware::TDeck::TileKey& key,
|
||||
std::uint32_t& size) = 0;
|
||||
virtual MapTileStreamResult read(std::uint8_t* output,
|
||||
std::size_t capacity,
|
||||
std::size_t& count) = 0;
|
||||
virtual void end() = 0;
|
||||
};
|
||||
|
||||
class MapTileStopSource {
|
||||
public:
|
||||
virtual ~MapTileStopSource() {}
|
||||
virtual bool stopRequested() const = 0;
|
||||
};
|
||||
|
||||
class MapTileStreamReader {
|
||||
public:
|
||||
static MapTileStreamResult readExact(
|
||||
MapTileReadStream& stream,
|
||||
MapTileStopSource& stop,
|
||||
const Hardware::TDeck::TileKey& key,
|
||||
std::uint8_t* output,
|
||||
std::size_t maximum_size,
|
||||
std::size_t chunk_size,
|
||||
std::size_t& total) {
|
||||
total = 0U;
|
||||
if (output == NULL || maximum_size == 0U || chunk_size == 0U) {
|
||||
return MapTileStreamResult::IO_ERROR;
|
||||
}
|
||||
std::uint32_t declared = 0U;
|
||||
const MapTileStreamResult begun = stream.begin(key, declared);
|
||||
if (begun != MapTileStreamResult::OK) return begun;
|
||||
if (declared > maximum_size) {
|
||||
stream.end();
|
||||
return MapTileStreamResult::TOO_LARGE;
|
||||
}
|
||||
while (total < declared) {
|
||||
if (stop.stopRequested()) {
|
||||
stream.end();
|
||||
return MapTileStreamResult::IO_ERROR;
|
||||
}
|
||||
const std::size_t remaining = static_cast<std::size_t>(declared) - total;
|
||||
const std::size_t capacity = remaining < chunk_size ? remaining : chunk_size;
|
||||
std::size_t count = 0U;
|
||||
const MapTileStreamResult read = stream.read(output + total, capacity, count);
|
||||
if (read != MapTileStreamResult::OK || count == 0U || count > capacity) {
|
||||
stream.end();
|
||||
return read == MapTileStreamResult::STORAGE_UNAVAILABLE
|
||||
? read : MapTileStreamResult::IO_ERROR;
|
||||
}
|
||||
total += count;
|
||||
}
|
||||
stream.end();
|
||||
if (stop.stopRequested()) return MapTileStreamResult::IO_ERROR;
|
||||
return MapTileStreamResult::OK;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Pyxis
|
||||
|
||||
#endif
|
||||
@@ -69,6 +69,94 @@ def test_worker_predecodes_and_render_path_has_no_io():
|
||||
assert "MAX_COMPLETIONS_PER_TICK = 1" in text(UI / "MapScreen.h")
|
||||
|
||||
|
||||
def test_selected_pack_is_worker_owned_read_only_and_precedes_live_cache():
|
||||
source = text(UI / "MapScreen.cpp")
|
||||
header = text(UI / "MapScreen.h")
|
||||
assert '#include "Hardware/TDeck/MapTilePack.h"' in header
|
||||
assert "Hardware::TDeck::MapTilePack pack_;" in header
|
||||
constructor = source[source.index("MapScreen::MapScreen"):
|
||||
source.index("MapScreen::~MapScreen")]
|
||||
assert "pack_(storage_)" in constructor
|
||||
|
||||
worker = function_body(source, "void MapScreen::workerLoop()")
|
||||
assert worker.index("store_.initialize()") < worker.index("pack_.initialize()")
|
||||
|
||||
read_tile = function_body(source, "Pyxis::MapTileLoadResult MapScreen::readTile(")
|
||||
assert read_tile.index("decoded_tile_cache_.get") < read_tile.index("PACK")
|
||||
assert read_tile.index("PACK") < read_tile.index("LIVE_STORE")
|
||||
|
||||
pack_read = function_body(
|
||||
source, "Pyxis::MapTileLoadResult MapScreen::readCompressedTile(")
|
||||
assert ("source == CompressedTileSource::LIVE_STORE && !store_initialized_"
|
||||
in pack_read)
|
||||
assert "MapTileStreamReader::readExact" in pack_read
|
||||
assert "PackReadStream pack_stream(pack_)" in pack_read
|
||||
assert "LiveReadStream live_stream(store_)" in pack_read
|
||||
pack_adapter = source[source.index("class PackReadStream"):
|
||||
source.index("class LiveReadStream")]
|
||||
live_adapter = source[source.index("class LiveReadStream"):
|
||||
source.index("class AtomicStopSource")]
|
||||
assert "pack_.beginGet" in pack_adapter
|
||||
assert "pack_.readGetChunk" in pack_adapter
|
||||
assert "pack_.endGet" in pack_adapter
|
||||
assert "remove" not in pack_adapter
|
||||
assert "store_.beginGet" in live_adapter
|
||||
assert "store_.readGetChunk" in live_adapter
|
||||
assert "store_.endGet" in live_adapter
|
||||
remove_token = "store_.removeTile(request.key)"
|
||||
remove_offsets = []
|
||||
cursor = 0
|
||||
while True:
|
||||
offset = pack_read.find(remove_token, cursor)
|
||||
if offset < 0:
|
||||
break
|
||||
remove_offsets.append(offset)
|
||||
cursor = offset + len(remove_token)
|
||||
assert len(remove_offsets) == 2
|
||||
for offset in remove_offsets:
|
||||
remove_block = pack_read[max(0, offset - 220):offset + 80]
|
||||
assert "source == CompressedTileSource::LIVE_STORE" in remove_block
|
||||
|
||||
# Covered-missing, uncovered, and corrupt immutable-pack tiles all continue
|
||||
# to the mutable live cache before optional online acquisition.
|
||||
assert "MapTilePackResult::UNCOVERED" in pack_adapter
|
||||
assert "MapTilePackResult::TILE_MISSING" in pack_adapter
|
||||
assert "MapTileLookupPolicy::readLocal" in read_tile
|
||||
assert "static MapTileLoadResult resolveLocal" in text(UI / "MapTileLookupPolicy.h")
|
||||
assert "MapTileLookupPolicy::shouldStartOnline" in source
|
||||
|
||||
# Every activation publishes a durable pack refresh edge. The worker owns
|
||||
# reinitialization and clears decoded tiles only when selection identity
|
||||
# actually changes; failed replacement remains transactional in MapTilePack.
|
||||
show = function_body(source, "void MapScreen::show()")
|
||||
assert "pack_refresh_epoch_.fetch_add" in show
|
||||
assert "std::atomic<std::uint32_t> pack_refresh_epoch_;" in header
|
||||
assert "pack_refresh_epoch != handled_pack_refresh_epoch" in worker
|
||||
initial_epoch = worker.index("handled_pack_refresh_epoch")
|
||||
initial_pack_init = worker.index("pack_.initialize()")
|
||||
assert initial_epoch < initial_pack_init
|
||||
assert worker.index("pack_.initialize()", worker.index("pack_refresh_epoch !=")) < worker.index("presenter_.takeRequest")
|
||||
assert "decoded_tile_cache_.clear()" in worker
|
||||
assert "std::strcmp(previous_pack_id, pack_.metadata().pack_id)" in worker
|
||||
|
||||
download = function_body(source, "Pyxis::MapTileLoadResult MapScreen::downloadTile(")
|
||||
assert "willStartTransportOnNextPump()" in download
|
||||
assert "xSemaphoreTake(transport_start_mutex_, portMAX_DELAY)" in download
|
||||
guarded_start = download[download.index("willStartTransportOnNextPump()"):
|
||||
download.index("xSemaphoreGive(transport_start_mutex_)")]
|
||||
assert "downloads_enabled_.load" in guarded_start
|
||||
assert "screen_visible_.load" in guarded_start
|
||||
assert "transport_close_epoch_.load" in guarded_start
|
||||
assert "downloader_.pump()" in guarded_start
|
||||
disable = function_body(source, "void MapScreen::setDownloadEnabled(")
|
||||
hide = function_body(source, "void MapScreen::hide()")
|
||||
assert "synchronizeTransportStart()" in disable
|
||||
assert "synchronizeTransportStart()" in hide
|
||||
|
||||
library = text(ROOT / "lib/tdeck_ui/library.json")
|
||||
assert '"+<Hardware/TDeck/*.cpp>"' in library
|
||||
|
||||
|
||||
def test_sd_adapter_never_mounts_or_formats():
|
||||
source = text(HW / "MapTileStoreSD.cpp")
|
||||
for forbidden in ("SD.begin", "SD.format", "LittleFS"):
|
||||
|
||||
@@ -86,10 +86,13 @@ def test_downloader_is_explicitly_opt_in_and_wired_only_for_visible_misses():
|
||||
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 "requests_released_ && screen_visible" in worker
|
||||
take_request = worker[worker.index("presenter_.takeRequest") - 160:
|
||||
worker.index("presenter_.takeRequest") + 80]
|
||||
assert "downloads_enabled" not in take_request
|
||||
assert "should_retain_download_transport" not in take_request
|
||||
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
|
||||
@@ -113,7 +116,9 @@ def test_recent_decoded_tiles_use_a_fixed_psram_lru_before_sd_decode():
|
||||
cache = (ROOT / "lib/tdeck_ui/UI/LXMF/DecodedTileCache.h").read_text()
|
||||
assert "CAPACITY = 12U" in cache
|
||||
assert "decoded_tile_cache_.get" in screen
|
||||
assert screen.index("decoded_tile_cache_.get") < screen.index("store_.beginGet")
|
||||
read_tile = screen[screen.index("Pyxis::MapTileLoadResult MapScreen::readTile("):
|
||||
screen.index("Pyxis::MapTileLoadResult MapScreen::readCompressedTile(")]
|
||||
assert read_tile.index("decoded_tile_cache_.get") < read_tile.index("MapTileLookupPolicy::readLocal")
|
||||
assert "decoded_tile_cache_.put" in screen
|
||||
assert "MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT" in screen
|
||||
assert "decoded_cache_pixels_[Pyxis::DecodedTileCache::CAPACITY]" in header
|
||||
|
||||
@@ -136,6 +136,10 @@ void testSuccessExactChunksAndPublicContract() { beginTest(); FakeStore s; s.max
|
||||
CHECK(r.code==MapTileResultCode::SUCCESS && r.bytes==8192U && same(r.key,key()) && r.generation==11U);
|
||||
CHECK(s.chunks.size()==2U && s.chunks[0]==4096U && s.chunks[1]==4096U); CHECK(t.url=="https://tile.openstreetmap.org/3/1/2.png");
|
||||
CHECK(t.agent=="Pyxis/1.2.3 (+https://github.com/torlando-tech/pyxis)"); CHECK(t.ca=="TEST CA"); CHECK(t.connect_timeout==321U && t.read_timeout==654U); CHECK(t.starts==1 && t.closes==1); }
|
||||
void testTransportStartGateStage() { beginTest(); FakeStore s; FakeTransport t; t.body=bytes(1U); FakeClock c; MapTileDownloader d(s,t,c,enabled(),config());
|
||||
CHECK(d.enqueue(key(),1U)==MapTileEnqueueResult::ACCEPTED); CHECK(!d.willStartTransportOnNextPump());
|
||||
CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(d.willStartTransportOnNextPump()); CHECK(t.starts==0);
|
||||
CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(!d.willStartTransportOnNextPump()); CHECK(t.starts==1); runUntilIdle(d,c); CHECK(take(d).code==MapTileResultCode::SUCCESS); }
|
||||
void testStatusAndContentTypeFailures() { beginTest(); const char* bad[]={"text/plain","image/pngx","image/ png",NULL};
|
||||
for(int i=0;i<4;++i){ FakeStore s; FakeTransport t; t.type=bad[i]; FakeClock c; MapTileDownloader d(s,t,c,enabled(),config()); CHECK(d.enqueue(key(),1U)==MapTileEnqueueResult::ACCEPTED); runUntilIdle(d,c); CHECK(take(d).code==MapTileResultCode::CONTENT_TYPE_ERROR); CHECK(s.begins==0); }
|
||||
FakeStore s; FakeTransport t; t.status=204; FakeClock c; MapTileDownloader d(s,t,c,enabled(),config()); CHECK(d.enqueue(key(),1U)==MapTileEnqueueResult::ACCEPTED); runUntilIdle(d,c); CHECK(take(d).code==MapTileResultCode::HTTP_STATUS_ERROR); CHECK(s.begins==0);
|
||||
@@ -228,4 +232,4 @@ void testSdDisappearanceAndMailboxBound() { beginTest(); FakeStore s; FakeTransp
|
||||
void testStress() { beginTest(); FakeStore s; FakeTransport t; FakeClock c; MapTileDownloader d(s,t,c,enabled(),config());
|
||||
for(std::uint32_t i=0;i<100000U;++i){ TileKey k=key(i&3U); const std::uint32_t g=i&7U; MapTileEnqueueResult r=d.enqueue(k,g); CHECK(r==MapTileEnqueueResult::ACCEPTED||r==MapTileEnqueueResult::DUPLICATE||r==MapTileEnqueueResult::QUEUE_FULL); if((i&3U)==0U)d.cancelGeneration(g); MapTileDownloadResult ignored; while(d.takeResult(ignored)){} } CHECK(d.queuedCount()<=MapTileDownloader::QUEUE_CAPACITY); }
|
||||
}
|
||||
int main(){ testDisabledByDefault(); testCanonicalUrlAndBounds(); testDedupeAndQueueFullNoEviction(); testSuccessExactChunksAndPublicContract(); testStatusAndContentTypeFailures(); testLengthOverUnderAndChunkOverCap(); testTransportAndStoreFailuresAbort(); testTransportStartFailureHardClosesAndRetriesOnce(); testTransportStartRetryIsBounded(); testTransportStartTimeoutIsNotRetried(); testTransportRetryHonorsOverallDeadline(); testSecondTransportFailureHonorsOverallDeadline(); testTransportRetryCanBeCanceledBetweenAttempts(); testTransportRetryBudgetResetsForNextRequest(); testCancellationAtStagesAndGenerationIsolation(); testTimeoutRollbackAndSaturation(); testDestructorAbortsOwnedResources(); testRuntimeDisableCancelsAllWork(); testSdDisappearanceAndMailboxBound(); testStress(); std::cout<<"map tile downloader: "<<tests_run<<" tests passed\n"; }
|
||||
int main(){ testDisabledByDefault(); testCanonicalUrlAndBounds(); testDedupeAndQueueFullNoEviction(); testSuccessExactChunksAndPublicContract(); testTransportStartGateStage(); testStatusAndContentTypeFailures(); testLengthOverUnderAndChunkOverCap(); testTransportAndStoreFailuresAbort(); testTransportStartFailureHardClosesAndRetriesOnce(); testTransportStartRetryIsBounded(); testTransportStartTimeoutIsNotRetried(); testTransportRetryHonorsOverallDeadline(); testSecondTransportFailureHonorsOverallDeadline(); testTransportRetryCanBeCanceledBetweenAttempts(); testTransportRetryBudgetResetsForNextRequest(); testCancellationAtStagesAndGenerationIsolation(); testTimeoutRollbackAndSaturation(); testDestructorAbortsOwnedResources(); testRuntimeDisableCancelsAllWork(); testSdDisappearanceAndMailboxBound(); testStress(); std::cout<<"map tile downloader: "<<tests_run<<" tests passed\n"; }
|
||||
|
||||
@@ -29,4 +29,4 @@ def test_bounded_map_tile_downloader(tmp_path: Path, sanitize: bool) -> None:
|
||||
env["UBSAN_OPTIONS"] = "halt_on_error=1:print_stacktrace=1"
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60, env=env)
|
||||
assert ran.returncode == 0, ran.stdout + ran.stderr
|
||||
assert ran.stdout == "map tile downloader: 20 tests passed\n"
|
||||
assert ran.stdout == "map tile downloader: 21 tests passed\n"
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "UI/LXMF/MapTileLookupPolicy.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
using Pyxis::MapTileLoadResult;
|
||||
using Pyxis::MapTileLookupPolicy;
|
||||
|
||||
namespace {
|
||||
std::size_t tests_run = 0U;
|
||||
void fail(const char* expression, int line) {
|
||||
std::cerr << "line " << line << ": " << expression << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
#define CHECK(expression) do { if (!(expression)) fail(#expression, __LINE__); } while (false)
|
||||
void beginTest() { ++tests_run; }
|
||||
|
||||
void testLocalPrecedenceTable() {
|
||||
beginTest();
|
||||
CHECK(MapTileLookupPolicy::resolveLocal(MapTileLoadResult::READY,
|
||||
MapTileLoadResult::IO_ERROR) ==
|
||||
MapTileLoadResult::READY);
|
||||
const MapTileLoadResult fallthrough[] = {
|
||||
MapTileLoadResult::MISS, MapTileLoadResult::INVALID_PNG,
|
||||
MapTileLoadResult::TOO_LARGE
|
||||
};
|
||||
for (std::size_t index = 0U; index < sizeof(fallthrough) / sizeof(fallthrough[0]); ++index) {
|
||||
CHECK(MapTileLookupPolicy::resolveLocal(fallthrough[index],
|
||||
MapTileLoadResult::READY) ==
|
||||
MapTileLoadResult::READY);
|
||||
CHECK(MapTileLookupPolicy::resolveLocal(fallthrough[index],
|
||||
MapTileLoadResult::MISS) ==
|
||||
MapTileLoadResult::MISS);
|
||||
}
|
||||
CHECK(MapTileLookupPolicy::resolveLocal(MapTileLoadResult::IO_ERROR,
|
||||
MapTileLoadResult::MISS) ==
|
||||
MapTileLoadResult::IO_ERROR);
|
||||
CHECK(MapTileLookupPolicy::resolveLocal(MapTileLoadResult::STORAGE_UNAVAILABLE,
|
||||
MapTileLoadResult::MISS) ==
|
||||
MapTileLoadResult::STORAGE_UNAVAILABLE);
|
||||
CHECK(MapTileLookupPolicy::resolveLocal(MapTileLoadResult::IO_ERROR,
|
||||
MapTileLoadResult::READY) ==
|
||||
MapTileLoadResult::READY);
|
||||
CHECK(MapTileLookupPolicy::resolveLocal(MapTileLoadResult::MISS,
|
||||
MapTileLoadResult::INVALID_PNG) ==
|
||||
MapTileLoadResult::INVALID_PNG);
|
||||
}
|
||||
|
||||
void testOnlineGate() {
|
||||
beginTest();
|
||||
CHECK(MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::MISS,
|
||||
true, true, false, 7U, 7U));
|
||||
CHECK(MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::INVALID_PNG,
|
||||
true, true, false, 7U, 7U));
|
||||
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::READY,
|
||||
true, true, false, 7U, 7U));
|
||||
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::MISS,
|
||||
false, true, false, 7U, 7U));
|
||||
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::MISS,
|
||||
true, false, false, 7U, 7U));
|
||||
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::MISS,
|
||||
true, true, true, 7U, 7U));
|
||||
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::MISS,
|
||||
true, true, false, 7U, 8U));
|
||||
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::IO_ERROR,
|
||||
true, true, false, 7U, 7U));
|
||||
}
|
||||
|
||||
struct FakeLocalSources {
|
||||
MapTileLoadResult pack;
|
||||
MapTileLoadResult live;
|
||||
int calls[2];
|
||||
std::size_t count;
|
||||
};
|
||||
|
||||
MapTileLoadResult readFake(void* opaque, MapTileLookupPolicy::LocalSource source) {
|
||||
FakeLocalSources* fake = static_cast<FakeLocalSources*>(opaque);
|
||||
fake->calls[fake->count++] = source == MapTileLookupPolicy::LocalSource::PACK ? 0 : 1;
|
||||
return source == MapTileLookupPolicy::LocalSource::PACK ? fake->pack : fake->live;
|
||||
}
|
||||
|
||||
void testExecutableSourceOrderAndFallback() {
|
||||
beginTest();
|
||||
FakeLocalSources ready = {MapTileLoadResult::READY, MapTileLoadResult::IO_ERROR, {0, 0}, 0U};
|
||||
CHECK(MapTileLookupPolicy::readLocal(&ready, readFake) == MapTileLoadResult::READY);
|
||||
CHECK(ready.count == 1U && ready.calls[0] == 0);
|
||||
const MapTileLoadResult fallthrough[] = {
|
||||
MapTileLoadResult::MISS, MapTileLoadResult::INVALID_PNG,
|
||||
MapTileLoadResult::TOO_LARGE, MapTileLoadResult::IO_ERROR
|
||||
};
|
||||
for (std::size_t index = 0U; index < sizeof(fallthrough) / sizeof(fallthrough[0]); ++index) {
|
||||
FakeLocalSources fake = {fallthrough[index], MapTileLoadResult::READY, {0, 0}, 0U};
|
||||
CHECK(MapTileLookupPolicy::readLocal(&fake, readFake) == MapTileLoadResult::READY);
|
||||
CHECK(fake.count == 2U && fake.calls[0] == 0 && fake.calls[1] == 1);
|
||||
}
|
||||
CHECK(MapTileLookupPolicy::readLocal(NULL, NULL) == MapTileLoadResult::IO_ERROR);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
testLocalPrecedenceTable();
|
||||
testOnlineGate();
|
||||
testExecutableSourceOrderAndFallback();
|
||||
std::cout << "map tile lookup policy: " << tests_run << " tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from native_test import find_cxx
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE = ROOT / "tests/native/test_map_tile_lookup_policy.cpp"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sanitize", [False, True], ids=["strict-cxx11", "asan-ubsan"])
|
||||
def test_map_tile_lookup_policy(tmp_path: Path, sanitize: bool) -> None:
|
||||
binary = tmp_path / "test_map_tile_lookup_policy"
|
||||
command = [
|
||||
find_cxx(), "-std=c++11", "-Wall", "-Wextra", "-Werror", "-pedantic",
|
||||
"-Wconversion", "-Wsign-conversion", f"-I{ROOT / 'lib/tdeck_ui'}",
|
||||
str(SOURCE), "-o", str(binary),
|
||||
]
|
||||
if sanitize:
|
||||
command[1:1] = ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
|
||||
compiled = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||
assert compiled.returncode == 0, compiled.stdout + compiled.stderr
|
||||
env = os.environ.copy()
|
||||
if sanitize:
|
||||
env["ASAN_OPTIONS"] = "detect_leaks=1:halt_on_error=1"
|
||||
env["UBSAN_OPTIONS"] = "halt_on_error=1:print_stacktrace=1"
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60, env=env)
|
||||
assert ran.returncode == 0, ran.stdout + ran.stderr
|
||||
assert ran.stdout == "map tile lookup policy: 3 tests passed\n"
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "UI/LXMF/MapTileStreamReader.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
using Hardware::TDeck::TileKey;
|
||||
using Pyxis::MapTileReadStream;
|
||||
using Pyxis::MapTileStopSource;
|
||||
using Pyxis::MapTileStreamReader;
|
||||
using Pyxis::MapTileStreamResult;
|
||||
|
||||
namespace {
|
||||
std::size_t tests_run = 0U;
|
||||
void fail(const char* expression, int line) { std::cerr << "line " << line << ": " << expression << '\n'; std::exit(1); }
|
||||
#define CHECK(expression) do { if (!(expression)) fail(#expression, __LINE__); } while (false)
|
||||
void beginTest() { ++tests_run; }
|
||||
|
||||
class Stop final : public MapTileStopSource {
|
||||
public:
|
||||
Stop() : requested(false) {}
|
||||
bool stopRequested() const override { return requested; }
|
||||
bool requested;
|
||||
};
|
||||
|
||||
class Stream final : public MapTileReadStream {
|
||||
public:
|
||||
Stream() : begin_result(MapTileStreamResult::OK), read_result(MapTileStreamResult::OK),
|
||||
declared(3U), position(0U), begin_calls(0U), read_calls(0U), end_calls(0U),
|
||||
zero_progress(false), oversized_count(false), stop(NULL) {
|
||||
bytes[0] = 1U; bytes[1] = 2U; bytes[2] = 3U;
|
||||
}
|
||||
MapTileStreamResult begin(const TileKey&, std::uint32_t& size) override {
|
||||
++begin_calls; size = declared; return begin_result;
|
||||
}
|
||||
MapTileStreamResult read(std::uint8_t* output, std::size_t capacity,
|
||||
std::size_t& count) override {
|
||||
++read_calls;
|
||||
if (read_result != MapTileStreamResult::OK) { count = 0U; return read_result; }
|
||||
if (zero_progress) { count = 0U; return MapTileStreamResult::OK; }
|
||||
if (oversized_count) { count = capacity + 1U; return MapTileStreamResult::OK; }
|
||||
const std::size_t remaining = declared - position;
|
||||
count = remaining < capacity ? remaining : capacity;
|
||||
if (count != 0U) std::memcpy(output, bytes + position, count);
|
||||
position += count;
|
||||
if (stop != NULL) stop->requested = true;
|
||||
return MapTileStreamResult::OK;
|
||||
}
|
||||
void end() override { ++end_calls; }
|
||||
MapTileStreamResult begin_result;
|
||||
MapTileStreamResult read_result;
|
||||
std::uint32_t declared;
|
||||
std::size_t position;
|
||||
std::size_t begin_calls;
|
||||
std::size_t read_calls;
|
||||
std::size_t end_calls;
|
||||
bool zero_progress;
|
||||
bool oversized_count;
|
||||
Stop* stop;
|
||||
std::uint8_t bytes[8];
|
||||
};
|
||||
|
||||
void testSuccessAndMiss() {
|
||||
beginTest(); Stop stop; Stream stream; std::uint8_t output[8] = {}; std::size_t total = 99U;
|
||||
CHECK(MapTileStreamReader::readExact(stream, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::OK);
|
||||
CHECK(total == 3U && output[0] == 1U && output[2] == 3U); CHECK(stream.end_calls == 1U);
|
||||
Stream miss; miss.begin_result = MapTileStreamResult::MISS; total = 99U;
|
||||
CHECK(MapTileStreamReader::readExact(miss, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::MISS);
|
||||
CHECK(total == 0U && miss.end_calls == 0U);
|
||||
}
|
||||
|
||||
void testEveryOpenedFailureCloses() {
|
||||
beginTest(); std::uint8_t output[4] = {}; std::size_t total = 0U; Stop stop;
|
||||
Stream large; large.declared = 5U;
|
||||
CHECK(MapTileStreamReader::readExact(large, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::TOO_LARGE); CHECK(large.end_calls == 1U);
|
||||
Stream error; error.read_result = MapTileStreamResult::IO_ERROR;
|
||||
CHECK(MapTileStreamReader::readExact(error, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::IO_ERROR); CHECK(error.end_calls == 1U);
|
||||
Stream unavailable; unavailable.read_result = MapTileStreamResult::STORAGE_UNAVAILABLE;
|
||||
CHECK(MapTileStreamReader::readExact(unavailable, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::STORAGE_UNAVAILABLE); CHECK(unavailable.end_calls == 1U);
|
||||
Stream zero; zero.zero_progress = true;
|
||||
CHECK(MapTileStreamReader::readExact(zero, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::IO_ERROR); CHECK(zero.end_calls == 1U);
|
||||
Stream excessive; excessive.oversized_count = true;
|
||||
CHECK(MapTileStreamReader::readExact(excessive, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::IO_ERROR); CHECK(excessive.end_calls == 1U);
|
||||
}
|
||||
|
||||
void testStopMidReadCloses() {
|
||||
beginTest(); Stop stop; Stream stream; stream.stop = &stop; std::uint8_t output[8] = {}; std::size_t total = 0U;
|
||||
CHECK(MapTileStreamReader::readExact(stream, stop, TileKey{1U,0U,0U}, output, sizeof(output), 1U, total) == MapTileStreamResult::IO_ERROR);
|
||||
CHECK(stream.read_calls == 1U && stream.end_calls == 1U);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
testSuccessAndMiss(); testEveryOpenedFailureCloses(); testStopMidReadCloses();
|
||||
std::cout << "map tile stream reader: " << tests_run << " tests passed\n";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from native_test import find_cxx
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE = ROOT / "tests/native/test_map_tile_stream_reader.cpp"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sanitize", [False, True], ids=["strict-cxx11", "asan-ubsan"])
|
||||
def test_map_tile_stream_reader(tmp_path: Path, sanitize: bool) -> None:
|
||||
binary = tmp_path / "test_map_tile_stream_reader"
|
||||
command = [find_cxx(), "-std=c++11", "-Wall", "-Wextra", "-Werror", "-pedantic",
|
||||
"-Wconversion", "-Wsign-conversion", f"-I{ROOT / 'lib/tdeck_ui'}",
|
||||
str(SOURCE), "-o", str(binary)]
|
||||
if sanitize:
|
||||
command[1:1] = ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
|
||||
compiled = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||
assert compiled.returncode == 0, compiled.stdout + compiled.stderr
|
||||
env = os.environ.copy()
|
||||
if sanitize:
|
||||
env["ASAN_OPTIONS"] = "detect_leaks=1:halt_on_error=1"
|
||||
env["UBSAN_OPTIONS"] = "halt_on_error=1:print_stacktrace=1"
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60, env=env)
|
||||
assert ran.returncode == 0, ran.stdout + ran.stderr
|
||||
assert ran.stdout == "map tile stream reader: 3 tests passed\n"
|
||||
Reference in New Issue
Block a user