perf: cache recent decoded map tiles

This commit is contained in:
torlando-agent[bot]
2026-08-07 01:37:13 +00:00
parent c46b23e860
commit 7ab8df8922
7 changed files with 350 additions and 1 deletions
+114
View File
@@ -0,0 +1,114 @@
// Copyright (c) 2026 Pyxis contributors
// SPDX-License-Identifier: MIT
#include "DecodedTileCache.h"
#include <cstring>
namespace Pyxis {
DecodedTileCache::DecodedTileCache(std::size_t pixel_count)
: pixel_count_(pixel_count), entries_{} {}
bool DecodedTileCache::sameKey(const Hardware::TDeck::TileKey& left,
const Hardware::TDeck::TileKey& right) {
return left.zoom == right.zoom && left.x == right.x && left.y == right.y;
}
bool DecodedTileCache::attach(std::size_t index, std::uint16_t* pixels) {
if (index >= CAPACITY || pixels == NULL) return false;
entries_[index].pixels = pixels;
entries_[index].valid = false;
entries_[index].rank = 0U;
return true;
}
int DecodedTileCache::find(const Hardware::TDeck::TileKey& key) const {
for (std::size_t index = 0U; index < CAPACITY; ++index) {
if (entries_[index].pixels != NULL && entries_[index].valid &&
sameKey(entries_[index].key, key)) {
return static_cast<int>(index);
}
}
return -1;
}
int DecodedTileCache::selectTarget() const {
int oldest = -1;
std::uint8_t oldest_rank = 0U;
for (std::size_t index = 0U; index < CAPACITY; ++index) {
if (entries_[index].pixels == NULL) continue;
if (!entries_[index].valid) return static_cast<int>(index);
if (oldest < 0 || entries_[index].rank > oldest_rank) {
oldest = static_cast<int>(index);
oldest_rank = entries_[index].rank;
}
}
return oldest;
}
void DecodedTileCache::touch(std::size_t index) {
const std::uint8_t previous = entries_[index].valid
? entries_[index].rank
: static_cast<std::uint8_t>(CAPACITY);
for (std::size_t other = 0U; other < CAPACITY; ++other) {
if (other == index || !entries_[other].valid ||
entries_[other].pixels == NULL) continue;
if (entries_[other].rank < previous) ++entries_[other].rank;
}
entries_[index].rank = 0U;
}
bool DecodedTileCache::get(const Hardware::TDeck::TileKey& key,
std::uint16_t* output,
std::size_t pixel_count) {
if (output == NULL || pixel_count != pixel_count_) return false;
const int found = find(key);
if (found < 0) return false;
const std::size_t index = static_cast<std::size_t>(found);
std::memcpy(output, entries_[index].pixels,
pixel_count_ * sizeof(std::uint16_t));
touch(index);
return true;
}
bool DecodedTileCache::put(const Hardware::TDeck::TileKey& key,
const std::uint16_t* input,
std::size_t pixel_count) {
if (input == NULL || pixel_count != pixel_count_) return false;
int target = find(key);
if (target < 0) target = selectTarget();
if (target < 0) return false;
const std::size_t index = static_cast<std::size_t>(target);
std::memcpy(entries_[index].pixels, input,
pixel_count_ * sizeof(std::uint16_t));
entries_[index].key = key;
touch(index);
entries_[index].valid = true;
return true;
}
void DecodedTileCache::clear() {
for (std::size_t index = 0U; index < CAPACITY; ++index) {
entries_[index].valid = false;
entries_[index].rank = 0U;
}
}
std::size_t DecodedTileCache::validCount() const {
std::size_t count = 0U;
for (std::size_t index = 0U; index < CAPACITY; ++index) {
if (entries_[index].valid && entries_[index].pixels != NULL) ++count;
}
return count;
}
std::size_t DecodedTileCache::attachedCount() const {
std::size_t count = 0U;
for (std::size_t index = 0U; index < CAPACITY; ++index) {
if (entries_[index].pixels != NULL) ++count;
}
return count;
}
} // namespace Pyxis
+51
View File
@@ -0,0 +1,51 @@
// Copyright (c) 2026 Pyxis contributors
// SPDX-License-Identifier: MIT
#ifndef UI_LXMF_DECODED_TILE_CACHE_H
#define UI_LXMF_DECODED_TILE_CACHE_H
#include <cstddef>
#include <cstdint>
#include "Hardware/TDeck/MapTileStore.h"
namespace Pyxis {
/** Fixed-capacity LRU over caller-owned decoded RGB565 buffers. */
class DecodedTileCache {
public:
static constexpr std::size_t CAPACITY = 12U;
explicit DecodedTileCache(std::size_t pixel_count);
bool attach(std::size_t index, std::uint16_t* pixels);
bool get(const Hardware::TDeck::TileKey& key,
std::uint16_t* output, std::size_t pixel_count);
bool put(const Hardware::TDeck::TileKey& key,
const std::uint16_t* input, std::size_t pixel_count);
void clear();
std::size_t validCount() const;
std::size_t attachedCount() const;
private:
struct Entry {
Hardware::TDeck::TileKey key;
std::uint16_t* pixels;
std::uint8_t rank;
bool valid;
};
std::size_t pixel_count_;
Entry entries_[CAPACITY];
static bool sameKey(const Hardware::TDeck::TileKey& left,
const Hardware::TDeck::TileKey& right);
int find(const Hardware::TDeck::TileKey& key) const;
int selectTarget() const;
void touch(std::size_t index);
};
} // namespace Pyxis
#endif
+24 -1
View File
@@ -68,7 +68,8 @@ MapScreen::MapScreen(lv_obj_t* parent)
status_label_(nullptr), attribution_label_(nullptr), zoom_label_(nullptr),
zoom_out_button_(nullptr), zoom_in_button_(nullptr),
recenter_button_(nullptr), pan_buttons_{}, tile_images_{},
tile_descriptors_{}, tile_pixels_{}, approximation_halos_{}, markers_{}, marker_labels_{},
tile_descriptors_{}, tile_pixels_{}, decoded_tile_cache_(TILE_PIXEL_COUNT),
decoded_cache_pixels_{}, approximation_halos_{}, markers_{}, marker_labels_{},
presenter_(), storage_(),
store_config_{STORE_ENTRY_CAPACITY, STORE_BYTE_QUOTA,
MAX_COMPRESSED_TILE_BYTES},
@@ -160,6 +161,14 @@ MapScreen::MapScreen(lv_obj_t* parent)
tile_images_[index] = lv_img_create(viewport_);
lv_obj_add_flag(tile_images_[index], LV_OBJ_FLAG_HIDDEN);
}
for (std::size_t index = 0U; index < Pyxis::DecodedTileCache::CAPACITY; ++index) {
decoded_cache_pixels_[index] = static_cast<std::uint16_t*>(heap_caps_malloc(
TILE_PIXEL_COUNT * sizeof(std::uint16_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
if (decoded_cache_pixels_[index]) {
(void)decoded_tile_cache_.attach(index, decoded_cache_pixels_[index]);
}
}
for (std::size_t index = 0; index < MARKER_COUNT; ++index) {
approximation_halos_[index] = lv_obj_create(viewport_);
@@ -232,6 +241,10 @@ MapScreen::~MapScreen() {
if (tile_pixels_[index]) heap_caps_free(tile_pixels_[index]);
tile_pixels_[index] = nullptr;
}
for (std::size_t index = 0U; index < Pyxis::DecodedTileCache::CAPACITY; ++index) {
if (decoded_cache_pixels_[index]) heap_caps_free(decoded_cache_pixels_[index]);
decoded_cache_pixels_[index] = nullptr;
}
if (compressed_staging_) heap_caps_free(compressed_staging_);
compressed_staging_ = nullptr;
if (state_mutex_) vSemaphoreDelete(state_mutex_);
@@ -416,6 +429,13 @@ Pyxis::MapTileLoadResult MapScreen::downloadTile(
Pyxis::MapTileLoadResult MapScreen::readTile(
const Pyxis::MapTileRequest& request) {
if (request.slot_index < TILE_COUNT && tile_pixels_[request.slot_index] &&
decoded_tile_cache_.get(
request.key,
reinterpret_cast<std::uint16_t*>(tile_pixels_[request.slot_index]),
TILE_PIXEL_COUNT)) {
return Pyxis::MapTileLoadResult::READY;
}
if (!store_initialized_) {
return Pyxis::MapTileLoadResult::STORAGE_UNAVAILABLE;
}
@@ -499,6 +519,9 @@ Pyxis::MapTileLoadResult MapScreen::readTile(
rgb[source + 2U]);
}
lv_mem_free(rgb);
(void)decoded_tile_cache_.put(
request.key, reinterpret_cast<const std::uint16_t*>(pixels),
TILE_PIXEL_COUNT);
return Pyxis::MapTileLoadResult::READY;
}
+3
View File
@@ -5,6 +5,7 @@
#define UI_LXMF_MAP_SCREEN_H
#include "MapScreenPresenter.h"
#include "DecodedTileCache.h"
#ifdef ARDUINO
@@ -72,6 +73,8 @@ private:
lv_obj_t* tile_images_[TILE_COUNT];
lv_img_dsc_t tile_descriptors_[TILE_COUNT];
lv_color_t* tile_pixels_[TILE_COUNT];
Pyxis::DecodedTileCache decoded_tile_cache_;
std::uint16_t* decoded_cache_pixels_[Pyxis::DecodedTileCache::CAPACITY];
lv_obj_t* approximation_halos_[MARKER_COUNT];
lv_obj_t* markers_[MARKER_COUNT];
lv_obj_t* marker_labels_[MARKER_COUNT];
@@ -86,6 +86,19 @@ def test_downloader_is_explicitly_opt_in_and_wired_only_for_visible_misses():
assert '"Offline"' not in status
def test_recent_decoded_tiles_use_a_fixed_psram_lru_before_sd_decode():
screen = MAP_SCREEN.read_text()
header = (ROOT / "lib/tdeck_ui/UI/LXMF/MapScreen.h").read_text()
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")
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
assert "heap_caps_free(decoded_cache_pixels_[index])" in screen
def test_settings_save_defers_persistence_and_application_outside_lvgl():
settings = SETTINGS.read_text()
capture = settings[settings.index("void SettingsScreen::save_settings()"):
+104
View File
@@ -0,0 +1,104 @@
#include <cstddef>
#include <cstdint>
#include <iostream>
#include "UI/LXMF/DecodedTileCache.h"
namespace {
int passed = 0;
int failures = 0;
#define CHECK(expr) do { if (expr) { ++passed; } else { ++failures; std::cerr << "FAIL line " << __LINE__ << ": " #expr << '\n'; } } while (false)
Hardware::TDeck::TileKey key(std::uint8_t zoom, std::uint32_t x, std::uint32_t y) {
Hardware::TDeck::TileKey value{};
value.zoom = zoom;
value.x = x;
value.y = y;
return value;
}
void missDoesNotMutateOutput() {
Pyxis::DecodedTileCache cache(4U);
std::uint16_t storage[4] = {};
std::uint16_t output[4] = {9U, 9U, 9U, 9U};
CHECK(cache.attach(0U, storage));
CHECK(!cache.get(key(2U, 1U, 1U), output, 4U));
CHECK(output[0] == 9U && output[3] == 9U);
CHECK(cache.validCount() == 0U);
}
void putAndGetCopiesPixels() {
Pyxis::DecodedTileCache cache(4U);
std::uint16_t storage[4] = {};
const std::uint16_t input[4] = {1U, 2U, 3U, 4U};
std::uint16_t output[4] = {};
CHECK(cache.attach(0U, storage));
CHECK(cache.put(key(3U, 2U, 4U), input, 4U));
CHECK(cache.get(key(3U, 2U, 4U), output, 4U));
CHECK(output[0] == 1U && output[1] == 2U &&
output[2] == 3U && output[3] == 4U);
CHECK(cache.validCount() == 1U);
}
void leastRecentlyUsedAttachedEntryIsEvicted() {
Pyxis::DecodedTileCache cache(2U);
std::uint16_t buffers[3][2] = {};
const std::uint16_t a[2] = {10U, 11U};
const std::uint16_t b[2] = {20U, 21U};
const std::uint16_t c[2] = {30U, 31U};
std::uint16_t output[2] = {};
for (std::size_t index = 0U; index < 3U; ++index) {
CHECK(cache.attach(index, buffers[index]));
}
CHECK(cache.put(key(4U, 1U, 1U), a, 2U));
CHECK(cache.put(key(4U, 2U, 2U), b, 2U));
CHECK(cache.put(key(4U, 3U, 3U), c, 2U));
CHECK(cache.get(key(4U, 1U, 1U), output, 2U));
CHECK(cache.put(key(4U, 4U, 4U), a, 2U));
CHECK(cache.get(key(4U, 1U, 1U), output, 2U));
CHECK(!cache.get(key(4U, 2U, 2U), output, 2U));
CHECK(cache.get(key(4U, 3U, 3U), output, 2U));
CHECK(cache.get(key(4U, 4U, 4U), output, 2U));
CHECK(cache.validCount() == 3U);
}
void duplicatePutRefreshesAndReplacesPixels() {
Pyxis::DecodedTileCache cache(2U);
std::uint16_t buffers[2][2] = {};
const Hardware::TDeck::TileKey a_key = key(5U, 6U, 7U);
const std::uint16_t first[2] = {1U, 2U};
const std::uint16_t second[2] = {8U, 9U};
std::uint16_t output[2] = {};
CHECK(cache.attach(0U, buffers[0]));
CHECK(cache.attach(1U, buffers[1]));
CHECK(cache.put(a_key, first, 2U));
CHECK(cache.put(a_key, second, 2U));
CHECK(cache.validCount() == 1U);
CHECK(cache.get(a_key, output, 2U));
CHECK(output[0] == 8U && output[1] == 9U);
}
void invalidArgumentsFailClosed() {
Pyxis::DecodedTileCache cache(2U);
std::uint16_t buffer[2] = {};
const std::uint16_t input[2] = {1U, 2U};
CHECK(!cache.attach(Pyxis::DecodedTileCache::CAPACITY, buffer));
CHECK(!cache.attach(0U, nullptr));
CHECK(cache.attach(0U, buffer));
CHECK(!cache.put(key(1U, 0U, 0U), input, 1U));
CHECK(!cache.put(key(1U, 0U, 0U), nullptr, 2U));
CHECK(!cache.get(key(1U, 0U, 0U), buffer, 1U));
CHECK(cache.validCount() == 0U);
}
}
int main() {
missDoesNotMutateOutput();
putAndGetCopiesPixels();
leastRecentlyUsedAttachedEntryIsEvicted();
duplicatePutRefreshesAndReplacesPixels();
invalidArgumentsFailClosed();
std::cout << "decoded tile cache: " << passed << " passed, "
<< failures << " failed\n";
return failures == 0 ? 0 : 1;
}
+41
View File
@@ -0,0 +1,41 @@
"""Compile the fixed decoded-tile LRU as strict C++11 under ASan/UBSan."""
from __future__ import annotations
import os
from pathlib import Path
import subprocess
from native_test import find_cxx
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
def test_decoded_tile_cache_cpp11_sanitized(tmp_path):
binary = tmp_path / "test_decoded_tile_cache"
command = [
find_cxx(),
"-std=c++11",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
"-fsanitize=address,undefined",
"-fno-omit-frame-pointer",
f"-I{ROOT / 'lib/tdeck_ui'}",
str(HERE / "test_decoded_tile_cache.cpp"),
str(ROOT / "lib/tdeck_ui/UI/LXMF/DecodedTileCache.cpp"),
"-o",
str(binary),
]
compiled = subprocess.run(command, capture_output=True, text=True, timeout=60)
assert compiled.returncode == 0, compiled.stdout + compiled.stderr
env = os.environ.copy()
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 "decoded tile cache:" in ran.stdout
assert "0 failed" in ran.stdout