From 913b7e5f8b98823cacc06abdec6c0ca3aca52180 Mon Sep 17 00:00:00 2001 From: spfenwick Date: Sat, 2 May 2026 15:54:40 +1200 Subject: [PATCH] Avoid OOM during prewarm by loading only ligature data, not kern pairs This is particularly important when using SD fonts with many kern pairs --- lib/EpdFont/SdCardFont.cpp | 63 +++++++++++++++++++++++--------------- lib/EpdFont/SdCardFont.h | 5 +-- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/lib/EpdFont/SdCardFont.cpp b/lib/EpdFont/SdCardFont.cpp index b493cf1c..ea88e8c3 100644 --- a/lib/EpdFont/SdCardFont.cpp +++ b/lib/EpdFont/SdCardFont.cpp @@ -66,11 +66,12 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) { void SdCardFont::freeStyleKernLigatureData(PerStyle& s) { delete[] s.kernLeftClasses; s.kernLeftClasses = nullptr; + s.kernClassesLoaded = false; delete[] s.kernRightClasses; s.kernRightClasses = nullptr; delete[] s.ligaturePairs; s.ligaturePairs = nullptr; - s.kernLigLoaded = false; + s.ligLoaded = false; } void SdCardFont::freeStyleMiniKern(PerStyle& s) { @@ -134,14 +135,17 @@ void SdCardFont::applyKernLigaturePointers(const PerStyle& s, EpdFontData& data) data.ligaturePairCount = s.header.ligaturePairCount; } -bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) { - if (s.kernLigLoaded) return true; - bool hasKern = s.header.kernLeftEntryCount > 0; - bool hasLig = s.header.ligaturePairCount > 0; - if (!hasKern && !hasLig) { - s.kernLigLoaded = true; - return true; - } +bool SdCardFont::loadStyleKernLigatureData(PerStyle& s, bool ligatureOnly) { + // During metadata-only (layout) prewarms, skip the kern class tables: the kern + // matrix is never built at layout time so getKerning() returns 0 regardless. + // Skipping them saves ~4KB per style (~17KB total for 4 styles), preventing OOM + // on low-heap devices when long paragraphs try to grow their word vector. + const bool wantKern = !ligatureOnly && s.header.kernLeftEntryCount > 0; + const bool wantLig = s.header.ligaturePairCount > 0; + + const bool kernDone = !wantKern || s.kernClassesLoaded; + const bool ligDone = !wantLig || s.ligLoaded; + if (kernDone && ligDone) return true; FsFile file; if (!Storage.openFileForRead("SDCF", filePath_, file)) { @@ -149,7 +153,7 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) { return false; } - if (hasKern) { + if (wantKern && !s.kernClassesLoaded) { // Load only the small class-lookup tables (~3KB each). The full matrix // (~36KB contiguous for Literata) is built per-page from SD in // buildMiniKernMatrix(). @@ -179,9 +183,10 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) { file.close(); return false; } + s.kernClassesLoaded = true; } - if (hasLig) { + if (wantLig && !s.ligLoaded) { s.ligaturePairs = new (std::nothrow) EpdLigaturePair[s.header.ligaturePairCount]; if (!s.ligaturePairs) { LOG_ERR("SDCF", "Failed to allocate ligature pairs"); @@ -202,19 +207,21 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) { file.close(); return false; } + s.ligLoaded = true; + + // Make ligatures visible to the stub (used when no mini data built yet). + // Kern stays nullptr on the stub — it is only wired in miniData via + // applyKernLigaturePointers() after buildMiniKernMatrix() runs. + s.stubData.ligaturePairs = s.ligaturePairs; + s.stubData.ligaturePairCount = s.header.ligaturePairCount; } file.close(); - s.kernLigLoaded = true; - - // Make ligatures visible to the stub (used when no mini data built yet). - // Kern stays nullptr on the stub — it is only wired in miniData via - // applyKernLigaturePointers() after buildMiniKernMatrix() runs. - s.stubData.ligaturePairs = s.ligaturePairs; - s.stubData.ligaturePairCount = s.header.ligaturePairCount; - - LOG_DBG("SDCF", "Kern classes + lig loaded: kernL=%u, kernR=%u, ligs=%u", s.header.kernLeftEntryCount, - s.header.kernRightEntryCount, s.header.ligaturePairCount); + LOG_DBG("SDCF", "Kern/lig loaded: kernL=%u kernR=%u ligs=%u ligOnly=%d", + s.kernClassesLoaded ? s.header.kernLeftEntryCount : 0u, + s.kernClassesLoaded ? s.header.kernRightEntryCount : 0u, + s.ligLoaded ? s.header.ligaturePairCount : 0u, + ligatureOnly); return true; } @@ -653,7 +660,7 @@ int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOn if (!(styleMask & (1 << si)) || !styles_[si].present) continue; auto& s = styles_[si]; - loadStyleKernLigatureData(s); + loadStyleKernLigatureData(s, /*ligatureOnly=*/true); 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; @@ -1143,7 +1150,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 kernLigOk = buildMiniKernMatrix(s, codepoints, cpCount); } } else if (loadKernLigatureData) { - loadStyleKernLigatureData(s); + loadStyleKernLigatureData(s, /*ligatureOnly=*/true); // Don't set kernLigOk → mini kern matrix stays null on miniData, but // ligatures are still resident on stubData (set in loadStyleKernLigatureData). } @@ -1161,7 +1168,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 if (kernLigOk) { // Full prewarm: wire mini kern matrix + class tables + ligatures. applyKernLigaturePointers(s, s.miniData); - } else if (loadKernLigatureData && s.kernLigLoaded) { + } else if (loadKernLigatureData && s.ligLoaded) { // Layout-only prewarm: wire ligatures so applyLigatures() works (e.g. "fi" // measures correctly). Skip the kern matrix — getKerning() returns 0 // cleanly when kernMatrix is null. Per-pair kern is applied at render time. @@ -1173,6 +1180,8 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 s.epdFont.data = &s.miniData; s.miniMode = metadataOnly ? PerStyle::MiniMode::METADATA : PerStyle::MiniMode::FULL; + LOG_DBG("SDCF", "prewarmStyle %u: mode→%s glyphs=%u bitmap=%p", + styleIdx, metadataOnly ? "METADATA" : "FULL", validCount, s.miniBitmap); // Accumulate stats stats_.sdReadTimeMs += sdTime; @@ -1236,6 +1245,10 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) { const auto& s = self->styles_[styleIdx]; if (!s.fullIntervals) return nullptr; + // Diagnostic: log first miss per codepoint+style to show why it bypassed prewarm + LOG_DBG("SDCF", "onGlyphMiss: U+%04X style %u miniMode=%u miniIntervals=%u bitmap=%p", + codepoint, styleIdx, (uint8_t)s.miniMode, s.miniIntervalCount, s.miniBitmap); + // Check overflow cache first (matching both codepoint and style) for (uint32_t i = 0; i < self->overflowCount_; i++) { if (self->overflow_[i].codepoint == codepoint && self->overflow_[i].styleIdx == styleIdx) { @@ -1309,6 +1322,8 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) { // All reads succeeded — commit to slot (evict old entry if at capacity) if (wasAtCapacity) { + LOG_DBG("SDCF", "Overflow: evicting U+%04X style %u from slot %u", + self->overflow_[slot].codepoint, self->overflow_[slot].styleIdx, slot); delete[] self->overflow_[slot].bitmap; } self->overflow_[slot].glyph = tempGlyph; diff --git a/lib/EpdFont/SdCardFont.h b/lib/EpdFont/SdCardFont.h index c7671a0f..597cbf06 100644 --- a/lib/EpdFont/SdCardFont.h +++ b/lib/EpdFont/SdCardFont.h @@ -118,7 +118,8 @@ class SdCardFont { EpdKernClassEntry* kernLeftClasses = nullptr; EpdKernClassEntry* kernRightClasses = nullptr; EpdLigaturePair* ligaturePairs = nullptr; - bool kernLigLoaded = false; + bool ligLoaded = false; ///< ligaturePairs resident + bool kernClassesLoaded = false; ///< kernLeft/RightClasses resident (skipped during metadata-only prewarm) // Stub EpdFontData returned when not prewarmed EpdFontData stubData{}; @@ -204,7 +205,7 @@ class SdCardFont { void freeStyleAll(PerStyle& s); void freeStyleKernLigatureData(PerStyle& s); void freeStyleMiniKern(PerStyle& s); - bool loadStyleKernLigatureData(PerStyle& s); + bool loadStyleKernLigatureData(PerStyle& s, bool ligatureOnly = false); bool buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount); void applyKernLigaturePointers(const PerStyle& s, EpdFontData& data) const; void applyGlyphMissCallback(uint8_t styleIdx);