mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
feat(ui-lvgl): live map tiles kept apart, under a size limit
Tiles fetched while browsing online go to /sdcard/maps-live with an index of arrival order; past the limit (Settings > Storage, 16 MB-1 GB, default 64 MB) the oldest are deleted a few per loop. Storage shows their count and size and can delete them all. Downloaded maps are never touched. Roadmap: live tiles done, second batch of user items added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -212,6 +212,20 @@ static void saveHistKeep(int idx) {
|
||||
p.putChar("hkeep", (int8_t)idx);
|
||||
p.end();
|
||||
}
|
||||
// Settings > Storage > Live map tiles: index into mapview::LIVE_CAP_MB, -1 = default.
|
||||
static int loadLiveCap() {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", true)) return -1;
|
||||
int v = p.getChar("ltcap", -1);
|
||||
p.end();
|
||||
return v;
|
||||
}
|
||||
static void saveLiveCap(int idx) {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", false)) return;
|
||||
p.putChar("ltcap", (int8_t)idx);
|
||||
p.end();
|
||||
}
|
||||
// Filesystem size and space in use (Settings > Storage), plus the card's own
|
||||
// size -- a card whose FAT partition is small (e.g. written by a Raspberry Pi
|
||||
// imager) shows both. The first free-space count on a big card takes a moment.
|
||||
@@ -508,6 +522,9 @@ static void saveAccent(int idx) { s_accent = idx; }
|
||||
static int s_hist_keep = -1;
|
||||
static int loadHistKeep() { return s_hist_keep; }
|
||||
static void saveHistKeep(int idx) { s_hist_keep = idx; }
|
||||
static int s_live_cap = -1;
|
||||
static int loadLiveCap() { return s_live_cap; }
|
||||
static void saveLiveCap(int idx) { s_live_cap = idx; }
|
||||
// The browser has no card: a nominal 32 GB, used = what the files add up to
|
||||
// (the storage screen counts them anyway; it passes that in).
|
||||
static bool sdInfo(uint64_t& total, uint64_t& used, uint64_t& card) { total = card = 32ULL << 30; used = 0; return true; }
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
#include <math.h>
|
||||
#include "map/TileProvider.h"
|
||||
#include "map/TileCache.h"
|
||||
#include "map/LiveCache.h"
|
||||
#include "map/TileDownloader.h"
|
||||
|
||||
namespace mapview {
|
||||
|
||||
static RasterTileProvider s_raster("/sdcard/maps");
|
||||
static RasterTileProvider s_raster("/sdcard/maps", LIVE_ROOT);
|
||||
static TileProvider* s_provider = &s_raster; // the one place to swap in a vector renderer
|
||||
static TileCache& s_cache = *new (psramBuf<TileCache>(1)) TileCache(); // decoded tiles, in PSRAM
|
||||
static TileDownloader s_dl("/sdcard/maps");
|
||||
@@ -787,6 +788,7 @@ void UITask::setLiveTiles(bool on) {
|
||||
void UITask::mapDownloadTick() {
|
||||
mapview::TileDownloader& dl = mapview::s_dl;
|
||||
dl.loop();
|
||||
mapview::s_live_cache.service(6); // trims live tiles past the Storage limit
|
||||
if (dl.liveOn()) {
|
||||
int z, x, y;
|
||||
bool got = false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Settings > Storage: how full the SD card and the internal flash are and
|
||||
// what takes the card's space (maps, message history, GPX trails, the rest),
|
||||
// how many messages each conversation keeps (HistoryStore.h) and deleting
|
||||
// the history.
|
||||
// the history, and how much space live map tiles may take (map/LiveCache.h).
|
||||
//
|
||||
// The card's files are counted in the background while the screen is open
|
||||
// (a map can be tens of thousands of tiles), a few milliseconds per loop.
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
namespace storeview {
|
||||
|
||||
enum Cat : uint8_t { C_MAPS, C_MSGS, C_TRAILS, C_OTHER, C_COUNT };
|
||||
static const char* const CAT_NAME[C_COUNT] = { "Maps", "Messages", "Trails (GPX)", "Other" };
|
||||
enum Cat : uint8_t { C_MAPS, C_LIVE, C_MSGS, C_TRAILS, C_OTHER, C_COUNT };
|
||||
static const char* const CAT_NAME[C_COUNT] = { "Maps", "Live map tiles", "Messages", "Trails (GPX)", "Other" };
|
||||
|
||||
static uint64_t s_bytes[C_COUNT];
|
||||
static uint32_t s_files = 0;
|
||||
@@ -34,6 +34,9 @@ static lv_obj_t* s_cat_val[C_COUNT];
|
||||
static lv_obj_t* s_status = nullptr;
|
||||
static lv_obj_t* s_clear_lbl = nullptr;
|
||||
static uint32_t s_clear_armed_ms = 0;
|
||||
static lv_obj_t* s_live_lbl = nullptr; // "Delete live map tiles" row label
|
||||
static lv_obj_t* s_live_info = nullptr; // count / size from the index
|
||||
static uint32_t s_live_armed_ms = 0;
|
||||
static uint32_t s_shown_ms = 0;
|
||||
|
||||
static void fmtBytes(char* out, size_t n, uint64_t b) {
|
||||
@@ -65,6 +68,7 @@ static void startWalk() {
|
||||
// What a top-level folder counts as.
|
||||
static uint8_t topCat(const char* name) {
|
||||
if (!strcmp(name, "maps")) return C_MAPS;
|
||||
if (!strcmp(name, "maps-live")) return C_LIVE;
|
||||
if (!strcmp(name, "meshcore")) return C_MSGS;
|
||||
if (!strcmp(name, "trails")) return C_TRAILS;
|
||||
return C_OTHER;
|
||||
@@ -193,6 +197,10 @@ static void onHistKeep(lv_event_t* e) {
|
||||
s_ui->storageKeep((int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onHistClear(lv_event_t* e) { (void)e; s_ui->storageClearHistory(); }
|
||||
static void onLiveCap(lv_event_t* e) {
|
||||
s_ui->storageLiveCap((int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onLiveClear(lv_event_t* e) { (void)e; s_ui->storageClearLive(); }
|
||||
|
||||
void UITask::showStorage() {
|
||||
_screen = SCR_STORAGE;
|
||||
@@ -202,7 +210,7 @@ void UITask::showStorage() {
|
||||
void UITask::buildStorage() {
|
||||
using namespace storeview;
|
||||
lv_obj_t* body = newScreen("Storage", true);
|
||||
s_sd_bar = s_sd_lbl = s_status = s_clear_lbl = nullptr;
|
||||
s_sd_bar = s_sd_lbl = s_status = s_clear_lbl = s_live_lbl = s_live_info = nullptr;
|
||||
memset(s_cat_val, 0, sizeof(s_cat_val));
|
||||
s_clear_armed_ms = 0;
|
||||
|
||||
@@ -245,6 +253,21 @@ void UITask::buildStorage() {
|
||||
lv_obj_t* row = listRow(body, LV_SYMBOL_TRASH " Delete message history", "Every chat, on the card and in memory",
|
||||
onHistClear, NULL);
|
||||
s_clear_lbl = lv_obj_get_child(row, 0);
|
||||
|
||||
sectionTitle(body, "LIVE MAP TILES");
|
||||
int cap_idx = mapview::LIVE_CAP_DEFAULT;
|
||||
for (int i = 0; i < mapview::LIVE_CAP_COUNT; i++)
|
||||
if (((uint64_t)mapview::LIVE_CAP_MB[i] << 20) == mapview::s_live_cache.limit()) cap_idx = i;
|
||||
lv_obj_t* cd = dropdownRow(body, "Keep up to", mapview::LIVE_CAP_OPTS, cap_idx);
|
||||
lv_obj_remove_event_cb(cd, onKeyboardAlphabet);
|
||||
lv_obj_add_event_cb(cd, onLiveCap, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
s_live_info = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(s_live_info, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(s_live_info, LV_PCT(100));
|
||||
lv_obj_t* lrow = listRow(body, LV_SYMBOL_TRASH " Delete live map tiles", "Downloaded maps stay",
|
||||
onLiveClear, NULL);
|
||||
s_live_lbl = lv_obj_get_child(lrow, 0);
|
||||
s_live_armed_ms = 0;
|
||||
s_shown_ms = 0;
|
||||
}
|
||||
|
||||
@@ -274,6 +297,18 @@ void UITask::pollStorage() {
|
||||
if (used == 0) for (int c = 0; c < C_COUNT; c++) used += s_bytes[c]; // the sim counts files
|
||||
showUsage(s_sd_bar, s_sd_lbl, s_sd_total, used);
|
||||
}
|
||||
if (s_live_info) {
|
||||
char b[16], t[96];
|
||||
fmtBytes(b, sizeof(b), mapview::s_live_cache.bytes());
|
||||
if (mapview::s_live_cache.clearing()) snprintf(t, sizeof(t), "Deleting... %lu left", (unsigned long)mapview::s_live_cache.count());
|
||||
else snprintf(t, sizeof(t), "Fetched while browsing online: %lu tiles, %s. Past the limit, the oldest go first.",
|
||||
(unsigned long)mapview::s_live_cache.count(), b);
|
||||
lv_label_set_text(s_live_info, t);
|
||||
}
|
||||
if (s_live_armed_ms && millis() - s_live_armed_ms > 4000) {
|
||||
s_live_armed_ms = 0;
|
||||
if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Delete live map tiles");
|
||||
}
|
||||
if (s_clear_armed_ms && millis() - s_clear_armed_ms > 4000) { // the confirm tap timed out
|
||||
s_clear_armed_ms = 0;
|
||||
if (s_clear_lbl) lv_label_set_text(s_clear_lbl, LV_SYMBOL_TRASH " Delete message history");
|
||||
@@ -304,3 +339,23 @@ void UITask::storageClearHistory() {
|
||||
showToast("Message history deleted");
|
||||
startWalk();
|
||||
}
|
||||
|
||||
void UITask::storageLiveCap(int idx) {
|
||||
if (idx < 0 || idx >= mapview::LIVE_CAP_COUNT) return;
|
||||
lvport::saveLiveCap(idx);
|
||||
mapview::s_live_cache.setLimit((uint64_t)mapview::LIVE_CAP_MB[idx] << 20); // trimmed in the background
|
||||
showToast("Saved");
|
||||
}
|
||||
|
||||
void UITask::storageClearLive() {
|
||||
using namespace storeview;
|
||||
if (!s_live_armed_ms) { // first tap: ask
|
||||
s_live_armed_ms = millis();
|
||||
if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Tap again to delete");
|
||||
return;
|
||||
}
|
||||
s_live_armed_ms = 0;
|
||||
mapview::s_live_cache.clearAll(); // a few files per loop (mapDownloadTick)
|
||||
if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Delete live map tiles");
|
||||
showToast("Deleting live map tiles");
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ template <class T> static T* psramBuf(size_t n) {
|
||||
#include "Anim.h"
|
||||
#include "LvglPort.h"
|
||||
#include "HistoryStore.h"
|
||||
#include "map/LiveCache.h"
|
||||
static histstore::SdArchive s_archive; // message history on the SD card
|
||||
namespace storeview { static void stopWalk(); } // StorageScreen.h
|
||||
#include "../ui-core/KeyboardData.h"
|
||||
@@ -216,6 +217,8 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs
|
||||
{ // history kept on the SD card: back into the ring, then every new entry to it
|
||||
int hk = lvport::loadHistKeep();
|
||||
s_archive.setKeep(histstore::KEEP[hk >= 0 && hk < histstore::KEEP_COUNT ? hk : histstore::KEEP_DEFAULT]);
|
||||
int lc = lvport::loadLiveCap();
|
||||
mapview::s_live_cache.setLimit((uint64_t)mapview::LIVE_CAP_MB[lc >= 0 && lc < mapview::LIVE_CAP_COUNT ? lc : mapview::LIVE_CAP_DEFAULT] << 20);
|
||||
s_archive.restore(_core->history);
|
||||
_core->history.setArchive(&s_archive);
|
||||
}
|
||||
|
||||
@@ -224,6 +224,8 @@ public:
|
||||
void pollStorage();
|
||||
void storageKeep(int idx);
|
||||
void storageClearHistory();
|
||||
void storageLiveCap(int idx);
|
||||
void storageClearLive();
|
||||
bool locked() const;
|
||||
void pinKey(const char* key); // the lock screen's keypad
|
||||
void pinSetupPopup(); // Settings > Display & power > Screen PIN
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#pragma once
|
||||
// Live map tiles (fetched while browsing online) live apart from downloaded
|
||||
// maps, under LIVE_ROOT/{z}/{x}/{y}.png, and only up to a size limit
|
||||
// (Settings > Storage): an index remembers the order they came in, and the
|
||||
// oldest are deleted once the tiles add up to more than the limit. Maps
|
||||
// downloaded on purpose (TileDownloader jobs, the PC tool) are never touched.
|
||||
//
|
||||
// Index: LIVE_ROOT/index.bin, a header and a ring of RING records. Eviction
|
||||
// runs a few files per loop (service()), so lowering the limit or clearing
|
||||
// thousands of tiles never blocks the UI.
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp only (after LvglPort.h).
|
||||
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
namespace mapview {
|
||||
|
||||
static const char* const LIVE_ROOT = "/sdcard/maps-live";
|
||||
|
||||
// Settings > Storage > Live map tiles: "Keep up to".
|
||||
static const uint32_t LIVE_CAP_MB[] = { 16, 64, 256, 1024 };
|
||||
static const int LIVE_CAP_COUNT = sizeof(LIVE_CAP_MB) / sizeof(LIVE_CAP_MB[0]);
|
||||
static const int LIVE_CAP_DEFAULT = 1;
|
||||
static const char* const LIVE_CAP_OPTS = "16 MB\n64 MB\n256 MB\n1 GB";
|
||||
|
||||
class LiveCache {
|
||||
public:
|
||||
static const uint32_t RING = 16384; // tiles; ~400 MB of typical tiles, 256 KB of index
|
||||
|
||||
void setLimit(uint64_t bytes) { _limit = bytes; }
|
||||
uint64_t limit() const { return _limit; }
|
||||
uint64_t bytes() { load(); return _h.bytes; }
|
||||
uint32_t count() { load(); return _h.count; }
|
||||
bool clearing() const { return _clearing; }
|
||||
|
||||
// A tile was written to LIVE_ROOT: remember it (eviction follows in service()).
|
||||
void add(int z, int x, int y, uint32_t size) {
|
||||
load();
|
||||
if (_h.count >= RING) evictOne(); // the ring is full: make room right away
|
||||
Rec r = { (int32_t)z, x, y, size };
|
||||
uint32_t slot = (_h.head + _h.count) % RING;
|
||||
_h.count++;
|
||||
_h.bytes += size;
|
||||
writeRec(slot, r);
|
||||
writeHdr();
|
||||
}
|
||||
|
||||
// Delete every live tile (a few per service() call).
|
||||
void clearAll() { load(); _clearing = true; }
|
||||
|
||||
// Evicts while over the limit (or clearing), for up to `budget_ms`.
|
||||
void service(uint32_t budget_ms) {
|
||||
if (!_loaded && !_clearing) return; // nothing written or asked for yet
|
||||
load();
|
||||
uint32_t t0 = millis();
|
||||
bool changed = false;
|
||||
while ((_clearing ? _h.count > 0 : _h.bytes > _limit) && millis() - t0 < budget_ms) {
|
||||
evictOne(false);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) writeHdr();
|
||||
if (_clearing && _h.count == 0) { _clearing = false; _h.bytes = 0; writeHdr(); }
|
||||
}
|
||||
|
||||
static void tilePath(char* out, size_t n, int z, int x, int y) {
|
||||
snprintf(out, n, "%s/%d/%d/%d.png", LIVE_ROOT, z, x, y);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Hdr { uint32_t magic, ring, head, count; uint64_t bytes; };
|
||||
struct Rec { int32_t z, x, y; uint32_t size; };
|
||||
static const uint32_t MAGIC = 0x3143544C; // "LTC1"
|
||||
|
||||
Hdr _h = { MAGIC, RING, 0, 0, 0 };
|
||||
bool _loaded = false, _clearing = false;
|
||||
uint64_t _limit = 64ULL << 20;
|
||||
|
||||
static void indexPath(char* out, size_t n) { snprintf(out, n, "%s/index.bin", LIVE_ROOT); }
|
||||
|
||||
void load() {
|
||||
if (_loaded) return;
|
||||
_loaded = true;
|
||||
char p[128];
|
||||
indexPath(p, sizeof(p));
|
||||
FILE* f = fopen(p, "rb");
|
||||
Hdr h;
|
||||
if (f && fread(&h, sizeof(h), 1, f) == 1 && h.magic == MAGIC && h.ring == RING && h.count <= RING && h.head < RING) _h = h;
|
||||
else _h = { MAGIC, RING, 0, 0, 0 };
|
||||
if (f) fclose(f);
|
||||
}
|
||||
|
||||
FILE* openIndex() {
|
||||
char p[128];
|
||||
indexPath(p, sizeof(p));
|
||||
FILE* f = fopen(p, "r+b");
|
||||
if (!f) {
|
||||
mkdir(LIVE_ROOT, 0777);
|
||||
f = fopen(p, "w+b");
|
||||
}
|
||||
return f;
|
||||
}
|
||||
void writeHdr() {
|
||||
if (FILE* f = openIndex()) { fwrite(&_h, sizeof(_h), 1, f); fclose(f); }
|
||||
}
|
||||
void writeRec(uint32_t slot, const Rec& r) {
|
||||
if (FILE* f = openIndex()) {
|
||||
if (fseek(f, (long)(sizeof(Hdr) + slot * sizeof(Rec)), SEEK_SET) == 0) fwrite(&r, sizeof(r), 1, f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
bool readRec(uint32_t slot, Rec& r) {
|
||||
char p[128];
|
||||
indexPath(p, sizeof(p));
|
||||
FILE* f = fopen(p, "rb");
|
||||
if (!f) return false;
|
||||
bool ok = fseek(f, (long)(sizeof(Hdr) + slot * sizeof(Rec)), SEEK_SET) == 0 && fread(&r, sizeof(r), 1, f) == 1;
|
||||
fclose(f);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void evictOne(bool save = true) {
|
||||
if (_h.count == 0) return;
|
||||
Rec r;
|
||||
if (readRec(_h.head, r)) {
|
||||
char path[128];
|
||||
tilePath(path, sizeof(path), r.z, r.x, r.y);
|
||||
remove(path);
|
||||
_h.bytes = _h.bytes > r.size ? _h.bytes - r.size : 0;
|
||||
}
|
||||
_h.head = (_h.head + 1) % RING;
|
||||
_h.count--;
|
||||
if (save) writeHdr();
|
||||
}
|
||||
};
|
||||
|
||||
static LiveCache s_live_cache;
|
||||
|
||||
} // namespace mapview
|
||||
@@ -20,7 +20,8 @@
|
||||
// Live tiles: while the map is open, single tiles it is missing are fetched
|
||||
// the same way (one at a time, between the job's steps when there is no job),
|
||||
// the WiFi connected on the first miss and dropped by the caller (liveEnd)
|
||||
// once the map has been closed for a while.
|
||||
// once the map has been closed for a while. They go to LIVE_ROOT, not
|
||||
// <root>, and only up to the Storage limit (LiveCache.h).
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp only (after LvglPort.h).
|
||||
|
||||
@@ -317,7 +318,10 @@ private:
|
||||
_lv_fetching = false;
|
||||
size_t len = 0;
|
||||
const uint8_t* data = r > 0 ? lvport::fetchData(len) : nullptr;
|
||||
if (r > 0 && looksLikeImage(data, len) && !isNoTilePicture(data, len) && writeTile(_lv_cur.z, _lv_cur.x, _lv_cur.y, data, len)) {
|
||||
char lpath[64];
|
||||
LiveCache::tilePath(lpath, sizeof(lpath), _lv_cur.z, _lv_cur.x, _lv_cur.y);
|
||||
if (r > 0 && looksLikeImage(data, len) && !isNoTilePicture(data, len) && writeFile(lpath, data, len)) {
|
||||
s_live_cache.add(_lv_cur.z, _lv_cur.x, _lv_cur.y, (uint32_t)len); // kept apart, within the Storage limit
|
||||
_lv_consec_fail = 0;
|
||||
if (_lv_done_n < LV_DONE) _lv_done[_lv_done_n++] = _lv_cur;
|
||||
} else {
|
||||
@@ -476,8 +480,12 @@ private:
|
||||
}
|
||||
|
||||
bool writeTile(int z, int x, int y, const uint8_t* d, size_t n) {
|
||||
char path[96], tmp[100];
|
||||
char path[96];
|
||||
tilePath(path, sizeof(path), z, x, y);
|
||||
return writeFile(path, d, n);
|
||||
}
|
||||
static bool writeFile(const char* path, const uint8_t* d, size_t n) {
|
||||
char tmp[100];
|
||||
makeParents(path);
|
||||
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
||||
FILE* f = fopen(tmp, "wb");
|
||||
|
||||
@@ -47,7 +47,9 @@ public:
|
||||
// which copies onto a FAT card far faster than millions of loose files.
|
||||
class RasterTileProvider : public TileProvider {
|
||||
public:
|
||||
explicit RasterTileProvider(const char* root) : _root(root) {}
|
||||
// `live`: a second folder in the same layout (live tiles, LiveCache.h), read
|
||||
// when <root> has no tile.
|
||||
explicit RasterTileProvider(const char* root, const char* live = nullptr) : _root(root), _live(live) {}
|
||||
|
||||
// Also reads <root>/attribution.txt (written by tools/maps/fetch_tiles.py and
|
||||
// the on-device downloader): the tile set's credit line, shown on the map.
|
||||
@@ -68,8 +70,9 @@ public:
|
||||
|
||||
bool renderTile(int z, int x, int y, uint16_t* out) override {
|
||||
uint32_t len = 0;
|
||||
uint8_t* png = readLoose(z, x, y, len);
|
||||
uint8_t* png = readLoose(_root, z, x, y, len);
|
||||
if (!png) png = readPacked(z, x, y, len);
|
||||
if (!png && _live) png = readLoose(_live, z, x, y, len);
|
||||
if (!png) return false;
|
||||
|
||||
unsigned char* res = nullptr;
|
||||
@@ -94,6 +97,7 @@ public:
|
||||
|
||||
private:
|
||||
const char* _root;
|
||||
const char* _live;
|
||||
char _attr[96] = "";
|
||||
|
||||
static uint8_t* readRange(FILE* f, long off, uint32_t len) {
|
||||
@@ -104,9 +108,9 @@ private:
|
||||
return buf;
|
||||
}
|
||||
|
||||
uint8_t* readLoose(int z, int x, int y, uint32_t& len) {
|
||||
uint8_t* readLoose(const char* root, int z, int x, int y, uint32_t& len) {
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "%s/%d/%d/%d.png", _root, z, x, y);
|
||||
snprintf(path, sizeof(path), "%s/%d/%d/%d.png", root, z, x, y);
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return nullptr;
|
||||
fseek(f, 0, SEEK_END);
|
||||
|
||||
Reference in New Issue
Block a user