Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 150 additions & 29 deletions lib/EpdFont/SdCardFont.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <HalStorage.h>
#include <Logging.h>
#include <Memory.h>
#include <Utf8.h>

#include <algorithm>
Expand Down Expand Up @@ -131,6 +132,7 @@ void SdCardFont::freeAll() {
freeStyleAll(styles_[i]);
}
styleCount_ = 0;
cacheOwner_ = CacheOwner::None;
contentHash_ = 0;
loaded_ = false;
}
Expand Down Expand Up @@ -657,11 +659,47 @@ int32_t SdCardFont::findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint)

// --- Prewarm ---

void SdCardFont::appendLigatureOutputs(uint8_t styleIdx, uint32_t* codepoints, uint32_t& cpCount, uint32_t maxCount) {
if (styleIdx >= MAX_STYLES || !styles_[styleIdx].present || cpCount == 0 || cpCount >= maxCount) return;

auto& s = styles_[styleIdx];
loadStyleKernLigatureData(s);
if (!s.ligaturePairs) return;

for (uint8_t li = 0; li < s.header.ligaturePairCount && cpCount < maxCount; li++) {
const uint32_t leftCp = s.ligaturePairs[li].pair >> 16;
const uint32_t rightCp = s.ligaturePairs[li].pair & 0xFFFF;
const uint32_t outCp = s.ligaturePairs[li].ligatureCp;

bool hasLeft = false;
bool hasRight = false;
for (uint32_t i = 0; i < cpCount; i++) {
if (codepoints[i] == leftCp) hasLeft = true;
if (codepoints[i] == rightCp) hasRight = true;
if (hasLeft && hasRight) break;
}
if (!hasLeft || !hasRight) continue;

bool hasOut = false;
for (uint32_t i = 0; i < cpCount; i++) {
if (codepoints[i] == outCp) {
hasOut = true;
break;
}
}
if (!hasOut) codepoints[cpCount++] = outCp;
}
}

int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOnly) {
if (!loaded_) return -1;
if (!utf8Text) return 0;
styleMask = resolveStyleMask(styleMask);
if (styleMask == 0) return 0;

if (cacheOwner_ == CacheOwner::Ui) clearCache();
cacheOwner_ = CacheOwner::ReaderPage;

unsigned long startMs = millis();

// Step 1: Extract unique codepoints from UTF-8 text (shared across all styles).
Expand Down Expand Up @@ -715,35 +753,7 @@ int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOn
if (!metadataOnly) {
for (uint8_t si = 0; si < MAX_STYLES; si++) {
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
auto& s = styles_[si];

loadStyleKernLigatureData(s);
if (s.ligaturePairs && s.header.ligaturePairCount > 0) {
for (uint8_t li = 0; li < s.header.ligaturePairCount && cpCount < MAX_PAGE_GLYPHS; li++) {
uint32_t leftCp = s.ligaturePairs[li].pair >> 16;
uint32_t rightCp = s.ligaturePairs[li].pair & 0xFFFF;
uint32_t outCp = s.ligaturePairs[li].ligatureCp;

bool hasLeft = false, hasRight = false;
for (uint32_t i = 0; i < cpCount; i++) {
if (codepoints[i] == leftCp) hasLeft = true;
if (codepoints[i] == rightCp) hasRight = true;
if (hasLeft && hasRight) break;
}
if (!hasLeft || !hasRight) continue;

bool hasOut = false;
for (uint32_t i = 0; i < cpCount; i++) {
if (codepoints[i] == outCp) {
hasOut = true;
break;
}
}
if (!hasOut) {
codepoints[cpCount++] = outCp;
}
}
}
appendLigatureOutputs(si, codepoints.get(), cpCount, MAX_PAGE_GLYPHS);
}
}

Expand All @@ -761,6 +771,107 @@ int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOn
return totalMissed;
}

int SdCardFont::prewarmUi(const char* utf8Text, uint8_t styleMask) {
if (!loaded_) return -1;
if (!utf8Text) return 0;
styleMask = resolveStyleMask(styleMask);
if (styleMask == 0) return 0;

// Cache mutation is not internally synchronized; callers must serialize UI
// measurement and rendering with RenderLock.
if (cacheOwner_ == CacheOwner::Ui) {
bool allCached = true;
for (uint8_t si = 0; si < MAX_STYLES && allCached; si++) {
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
const auto& s = styles_[si];

const char* cursor = utf8Text;
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&cursor)))) {
cp = s.epdFont.applyLigatures(cp, cursor);
if (!s.epdFont.hasCodepoint(cp)) {
allCached = false;
break;
}
}
}
if (allCached) return 0;
}

if (cacheOwner_ != CacheOwner::Ui) {
clearCache();
cacheOwner_ = CacheOwner::Ui;
}

auto codepoints = makeUniqueNoThrow<uint32_t[]>(MAX_UI_GLYPHS);
if (!codepoints) {
LOG_ERR("SDCF", "Failed to allocate UI codepoint buffer (%u bytes)", MAX_UI_GLYPHS * 4);
return -1;
}
uint32_t cpCount = 0;

// Put the current string first. If the bounded cache fills, the visible UI
// gets priority over glyphs retained from an older screen.
collectUniqueCodepoints(utf8Text, codepoints.get(), cpCount, MAX_UI_GLYPHS);

bool hasReplacement = false;
for (uint32_t i = 0; i < cpCount; i++) {
if (codepoints[i] == REPLACEMENT_GLYPH) {
hasReplacement = true;
break;
}
}
if (!hasReplacement && cpCount < MAX_UI_GLYPHS) codepoints[cpCount++] = REPLACEMENT_GLYPH;

// Retain already cached glyphs while there is room. The next rebuild keeps
// the union, so a repeated UI string does not touch the SD card again.
for (uint8_t si = 0; si < MAX_STYLES && cpCount < MAX_UI_GLYPHS; si++) {
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
const auto& s = styles_[si];
for (uint32_t ii = 0; ii < s.miniData.intervalCount && cpCount < MAX_UI_GLYPHS; ii++) {
const auto& interval = s.miniData.intervals[ii];
for (uint32_t cp = interval.first;; cp++) {
bool found = false;
for (uint32_t i = 0; i < cpCount; i++) {
if (codepoints[i] == cp) {
found = true;
break;
}
}
if (!found) codepoints[cpCount++] = cp;
if (cp == interval.last || cpCount >= MAX_UI_GLYPHS) break;
}
}
}

for (uint8_t si = 0; si < MAX_STYLES; si++) {
if (styleMask & (1 << si)) appendLigatureOutputs(si, codepoints.get(), cpCount, MAX_UI_GLYPHS);
}
std::sort(codepoints.get(), codepoints.get() + cpCount);

bool needsRebuild = false;
for (uint8_t si = 0; si < MAX_STYLES && !needsRebuild; si++) {
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
const auto& s = styles_[si];
for (uint32_t i = 0; i < cpCount; i++) {
if (findGlobalGlyphIndex(s, codepoints[i]) >= 0 && !s.epdFont.hasCodepoint(codepoints[i])) {
needsRebuild = true;
break;
}
}
}
if (!needsRebuild) return 0;

unsigned long startMs = millis();
int totalMissed = 0;
for (uint8_t si = 0; si < MAX_STYLES; si++) {
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
totalMissed += prewarmStyle(si, codepoints.get(), cpCount, false);
}
stats_.prewarmTotalMs = millis() - startMs;
return totalMissed;
}

int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly) {
auto& s = styles_[styleIdx];

Expand Down Expand Up @@ -996,6 +1107,16 @@ void SdCardFont::clearCache() {
freeStyleMiniData(styles_[i]);
applyGlyphMissCallback(i);
}
cacheOwner_ = CacheOwner::None;
}

// Activity transitions deliberately clear all active bitmap caches,
// regardless of cacheOwner_.
void SdCardFont::clearUiCache() { clearCache(); }

void SdCardFont::clearReaderCache() {
clearCache();
clearPersistentCache();
}

// --- Advance table ---
Expand Down
15 changes: 15 additions & 0 deletions lib/EpdFont/SdCardFont.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
class SdCardFont {
public:
static constexpr uint16_t MAX_PAGE_GLYPHS = 512;
static constexpr uint16_t MAX_UI_GLYPHS = 256;
static constexpr uint8_t MAX_STYLES = 4;

SdCardFont() = default;
Expand All @@ -44,6 +45,10 @@ class SdCardFont {
// Returns number of glyphs that couldn't be loaded (0 on full success).
int prewarm(const char* utf8Text, uint8_t styleMask = 0x0F, bool metadataOnly = false);

// Batch-load UI glyphs and keep the bounded bitmap cache until the next
// reader/UI transition or SD-font invalidation.
int prewarmUi(const char* utf8Text, uint8_t styleMask = 0x0F);

// Build a compact advance-only table for layout measurement.
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
// batch-reads advanceX from SD, stores in a sorted per-style table.
Expand All @@ -63,6 +68,12 @@ class SdCardFont {
// previously fetched metrics.
void clearCache();

// Clear the active UI bitmap cache while retaining font metadata and advances.
void clearUiCache();

// Clear reader bitmap and persistent advance caches.
void clearReaderCache();

// Drop the persistent advance cache. Call when unloading the SD font or
// when font/size/family/glyph-table state changes.
void clearPersistentCache();
Expand Down Expand Up @@ -191,8 +202,11 @@ class SdCardFont {
bool present = false;
};

enum class CacheOwner : uint8_t { None, ReaderPage, Ui };

PerStyle styles_[MAX_STYLES] = {};
uint8_t styleCount_ = 0;
CacheOwner cacheOwner_ = CacheOwner::None;

char filePath_[128] = {};

Expand Down Expand Up @@ -244,6 +258,7 @@ class SdCardFont {
void freeStyleMiniKern(PerStyle& s);
bool loadStyleKernLigatureData(PerStyle& s);
bool buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount);
void appendLigatureOutputs(uint8_t styleIdx, uint32_t* codepoints, uint32_t& cpCount, uint32_t maxCount);
void applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const;
void applyGlyphMissCallback(uint8_t styleIdx);
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
Expand Down
26 changes: 26 additions & 0 deletions lib/GfxRenderer/FontCacheManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@ void FontCacheManager::clearCache() {
}
}

void FontCacheManager::clearUiCache() {
if (fontDecompressor_) fontDecompressor_->clearCache();
for (auto& [id, font] : sdCardFonts_) {
font->clearUiCache();
}
}

void FontCacheManager::clearReaderCache() {
if (fontDecompressor_) fontDecompressor_->clearCache();
for (auto& [id, font] : sdCardFonts_) {
font->clearReaderCache();
}
}

void FontCacheManager::prewarmUi(int fontId, const char* utf8Text, EpdFontFamily::Style style) {
auto it = sdCardFonts_.find(fontId);
if (it == sdCardFonts_.end()) return;

const uint8_t styleMask = static_cast<uint8_t>(1u << (static_cast<uint8_t>(style) & 0x03));
const int missed = it->second->prewarmUi(utf8Text, styleMask);
if (missed > 0) {
LOG_DBG("FCM", "prewarmUi: %d glyph(s) not found (fontId=%d, style=%u)", missed, fontId,
static_cast<uint8_t>(style) & 0x03);
}
}

void FontCacheManager::prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask) {
// SD card font prewarm path: prewarm all requested styles in one call
auto it = sdCardFonts_.find(fontId);
Expand Down
3 changes: 3 additions & 0 deletions lib/GfxRenderer/FontCacheManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ class FontCacheManager {
void setFontDecompressor(FontDecompressor* d);

void clearCache();
void clearUiCache();
void clearReaderCache();
void prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F);
void prewarmUi(int fontId, const char* utf8Text, EpdFontFamily::Style style);
void logStats(const char* label = "render");
void resetStats();

Expand Down
30 changes: 30 additions & 0 deletions lib/GfxRenderer/GfxRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,30 @@ void GfxRenderer::begin() {

bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); }

void GfxRenderer::clearUiFontCache() const {
if (fontCacheManager_) {
fontCacheManager_->clearUiCache();
return;
}
for (const auto& [id, font] : sdCardFonts_) {
font->clearUiCache();
}
}

void GfxRenderer::clearReaderFontCache() const {
if (fontCacheManager_) {
fontCacheManager_->clearReaderCache();
return;
}
for (const auto& [id, font] : sdCardFonts_) {
font->clearReaderCache();
}
}

void GfxRenderer::prewarmUiText(const int fontId, const char* text, const EpdFontFamily::Style style) const {
resolveTextFontId(fontId, text, style);
}

void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
auto result = fontMap.insert({fontId, font});
if (!result.second) {
Expand Down Expand Up @@ -120,6 +144,12 @@ int GfxRenderer::resolveTextFontId(const int fontId, const char* text, const Epd
// Only redirect for CJK the primary font cannot draw. Latin/symbol strings
// the built-in UI fonts already cover are left untouched.
if (utf8IsCjkBreakable(cp) && !primary.hasCodepoint(cp, style)) {
if (fontCacheManager_) {
fontCacheManager_->prewarmUi(fallbackFontId, text, style);
} else {
const auto sdIt = sdCardFonts_.find(fallbackFontId);
if (sdIt != sdCardFonts_.end()) sdIt->second->prewarmUi(text, 1u << (static_cast<uint8_t>(style) & 0x03));
}
return fallbackFontId;
}
}
Expand Down
5 changes: 5 additions & 0 deletions lib/GfxRenderer/GfxRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ class GfxRenderer {
}
void setFontCacheManager(FontCacheManager* m) { fontCacheManager_ = m; }
FontCacheManager* getFontCacheManager() const { return fontCacheManager_; }
void clearUiFontCache() const;
void clearReaderFontCache() const;
// Trigger one batch prewarm for a UI string. The normal resolver keeps
// Latin-only strings on the built-in font and redirects CJK strings to SD.
void prewarmUiText(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
bool isFontCacheScanning() const;
const std::map<int, EpdFontFamily>& getFontMap() const { return fontMap; }
void registerSdCardFont(int fontId, SdCardFont* font) { sdCardFonts_[fontId] = font; }
Expand Down
Loading
Loading