mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-21 18:19:48 +00:00
perf: cache recent decoded map tiles
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()"):
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user