diff --git a/lib/EpdFont/FontDecompressor.cpp b/lib/EpdFont/FontDecompressor.cpp index 1b347673..8dbf2ba9 100644 --- a/lib/EpdFont/FontDecompressor.cpp +++ b/lib/EpdFont/FontDecompressor.cpp @@ -147,6 +147,19 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep break; // Found the right slot but glyph wasn't in it; don't check other slots } + // Check fallback LRU cache + for (uint8_t i = 0; i < FALLBACK_CACHE_SLOTS; i++) { + if (_fallbackCache[i].fontData == fontData && _fallbackCache[i].glyphIndex == glyphIndex) { + _fallbackCache[i].lastUsedTick = ++_fallbackTick; + stats.cacheHits++; + stats.fallbackCacheHits++; + stats.getBitmapTimeUs += micros() - tStart; + return _fallbackCache[i].buffer; + } + } + + stats.fallbackCacheMisses++; + // Fallback: glyph wasn't in the page buffer — decompress its group transiently. // This is the rare path (prewarm should cover all glyphs on a normal page). uint16_t groupIndex = getGroupIndex(fontData, glyphIndex); @@ -181,11 +194,25 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep } uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex); - compactSingleGlyph(&groupBuf[alignedOff], _hotGlyphBuf, glyph->width, glyph->height); + + uint8_t lruIndex = 0; + uint32_t oldestTick = UINT32_MAX; + for (uint8_t i = 0; i < FALLBACK_CACHE_SLOTS; i++) { + if (_fallbackCache[i].lastUsedTick < oldestTick) { + oldestTick = _fallbackCache[i].lastUsedTick; + lruIndex = i; + } + } + + compactSingleGlyph(&groupBuf[alignedOff], _fallbackCache[lruIndex].buffer, glyph->width, glyph->height); free(groupBuf); + _fallbackCache[lruIndex].fontData = fontData; + _fallbackCache[lruIndex].glyphIndex = glyphIndex; + _fallbackCache[lruIndex].lastUsedTick = ++_fallbackTick; + stats.getBitmapTimeUs += micros() - tStart; - return _hotGlyphBuf; + return _fallbackCache[lruIndex].buffer; } // --- Prewarm: pre-decompress glyph bitmaps for a page of text --- @@ -537,5 +564,12 @@ void FontDecompressor::logStats(const char* label) { LOG_DBG("FDC", "[%s] getBitmap: %lu calls, %luus total, %luus/call avg", label, stats.getBitmapCalls, stats.getBitmapTimeUs, stats.getBitmapTimeUs / stats.getBitmapCalls); } + + uint32_t lruTotal = stats.fallbackCacheHits + stats.fallbackCacheMisses; + if (lruTotal > 0) { + LOG_DBG("FDC", "[%s] LRU Fallback: hits=%lu misses=%lu (%.1f%%)", label, stats.fallbackCacheHits, + stats.fallbackCacheMisses, 100.0f * stats.fallbackCacheHits / lruTotal); + } + resetStats(); } diff --git a/lib/EpdFont/FontDecompressor.h b/lib/EpdFont/FontDecompressor.h index 8f574dba..3eb8147b 100644 --- a/lib/EpdFont/FontDecompressor.h +++ b/lib/EpdFont/FontDecompressor.h @@ -41,6 +41,10 @@ class FontDecompressor { uint32_t peakTempBytes = 0; // largest temp buffer in prewarm or getBitmap miss uint32_t getBitmapTimeUs = 0; // cumulative getBitmap time (micros) uint32_t getBitmapCalls = 0; // number of getBitmap calls + + // LRU specific stats + uint32_t fallbackCacheHits = 0; + uint32_t fallbackCacheMisses = 0; }; void logStats(const char* label = "FDC"); void resetStats(); @@ -68,10 +72,18 @@ class FontDecompressor { uint8_t pageSlotCount = 0; static constexpr uint16_t HOT_GLYPH_BUF_SIZE = 512; // largest packed single glyph + static constexpr uint8_t FALLBACK_CACHE_SLOTS = 1; - // Scratch buffer for compacting a single glyph after a getBitmap() miss. - // Valid until the next getBitmap() call. - uint8_t _hotGlyphBuf[HOT_GLYPH_BUF_SIZE] = {}; + struct FallbackSlot { + const EpdFontData* fontData; + uint32_t glyphIndex; + uint32_t lastUsedTick; + uint8_t buffer[HOT_GLYPH_BUF_SIZE]; + }; + + // LRU cache for fallback glyph decompresion + FallbackSlot _fallbackCache[FALLBACK_CACHE_SLOTS] = {}; + uint32_t _fallbackTick = 0; void freePageBuffer(); uint16_t getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex);