Avoid OOM during prewarm by loading only ligature data, not kern pairs
This is particularly important when using SD fonts with many kern pairs
This commit is contained in:
+39
-24
@@ -66,11 +66,12 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
|||||||
void SdCardFont::freeStyleKernLigatureData(PerStyle& s) {
|
void SdCardFont::freeStyleKernLigatureData(PerStyle& s) {
|
||||||
delete[] s.kernLeftClasses;
|
delete[] s.kernLeftClasses;
|
||||||
s.kernLeftClasses = nullptr;
|
s.kernLeftClasses = nullptr;
|
||||||
|
s.kernClassesLoaded = false;
|
||||||
delete[] s.kernRightClasses;
|
delete[] s.kernRightClasses;
|
||||||
s.kernRightClasses = nullptr;
|
s.kernRightClasses = nullptr;
|
||||||
delete[] s.ligaturePairs;
|
delete[] s.ligaturePairs;
|
||||||
s.ligaturePairs = nullptr;
|
s.ligaturePairs = nullptr;
|
||||||
s.kernLigLoaded = false;
|
s.ligLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdCardFont::freeStyleMiniKern(PerStyle& s) {
|
void SdCardFont::freeStyleMiniKern(PerStyle& s) {
|
||||||
@@ -134,14 +135,17 @@ void SdCardFont::applyKernLigaturePointers(const PerStyle& s, EpdFontData& data)
|
|||||||
data.ligaturePairCount = s.header.ligaturePairCount;
|
data.ligaturePairCount = s.header.ligaturePairCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) {
|
bool SdCardFont::loadStyleKernLigatureData(PerStyle& s, bool ligatureOnly) {
|
||||||
if (s.kernLigLoaded) return true;
|
// During metadata-only (layout) prewarms, skip the kern class tables: the kern
|
||||||
bool hasKern = s.header.kernLeftEntryCount > 0;
|
// matrix is never built at layout time so getKerning() returns 0 regardless.
|
||||||
bool hasLig = s.header.ligaturePairCount > 0;
|
// Skipping them saves ~4KB per style (~17KB total for 4 styles), preventing OOM
|
||||||
if (!hasKern && !hasLig) {
|
// on low-heap devices when long paragraphs try to grow their word vector.
|
||||||
s.kernLigLoaded = true;
|
const bool wantKern = !ligatureOnly && s.header.kernLeftEntryCount > 0;
|
||||||
return true;
|
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;
|
FsFile file;
|
||||||
if (!Storage.openFileForRead("SDCF", filePath_, file)) {
|
if (!Storage.openFileForRead("SDCF", filePath_, file)) {
|
||||||
@@ -149,7 +153,7 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasKern) {
|
if (wantKern && !s.kernClassesLoaded) {
|
||||||
// Load only the small class-lookup tables (~3KB each). The full matrix
|
// Load only the small class-lookup tables (~3KB each). The full matrix
|
||||||
// (~36KB contiguous for Literata) is built per-page from SD in
|
// (~36KB contiguous for Literata) is built per-page from SD in
|
||||||
// buildMiniKernMatrix().
|
// buildMiniKernMatrix().
|
||||||
@@ -179,9 +183,10 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) {
|
|||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
s.kernClassesLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasLig) {
|
if (wantLig && !s.ligLoaded) {
|
||||||
s.ligaturePairs = new (std::nothrow) EpdLigaturePair[s.header.ligaturePairCount];
|
s.ligaturePairs = new (std::nothrow) EpdLigaturePair[s.header.ligaturePairCount];
|
||||||
if (!s.ligaturePairs) {
|
if (!s.ligaturePairs) {
|
||||||
LOG_ERR("SDCF", "Failed to allocate ligature pairs");
|
LOG_ERR("SDCF", "Failed to allocate ligature pairs");
|
||||||
@@ -202,19 +207,21 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) {
|
|||||||
file.close();
|
file.close();
|
||||||
return false;
|
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();
|
file.close();
|
||||||
s.kernLigLoaded = true;
|
LOG_DBG("SDCF", "Kern/lig loaded: kernL=%u kernR=%u ligs=%u ligOnly=%d",
|
||||||
|
s.kernClassesLoaded ? s.header.kernLeftEntryCount : 0u,
|
||||||
// Make ligatures visible to the stub (used when no mini data built yet).
|
s.kernClassesLoaded ? s.header.kernRightEntryCount : 0u,
|
||||||
// Kern stays nullptr on the stub — it is only wired in miniData via
|
s.ligLoaded ? s.header.ligaturePairCount : 0u,
|
||||||
// applyKernLigaturePointers() after buildMiniKernMatrix() runs.
|
ligatureOnly);
|
||||||
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);
|
|
||||||
return true;
|
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;
|
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
|
||||||
auto& s = styles_[si];
|
auto& s = styles_[si];
|
||||||
|
|
||||||
loadStyleKernLigatureData(s);
|
loadStyleKernLigatureData(s, /*ligatureOnly=*/true);
|
||||||
if (s.ligaturePairs && s.header.ligaturePairCount > 0) {
|
if (s.ligaturePairs && s.header.ligaturePairCount > 0) {
|
||||||
for (uint8_t li = 0; li < s.header.ligaturePairCount && cpCount < MAX_PAGE_GLYPHS; li++) {
|
for (uint8_t li = 0; li < s.header.ligaturePairCount && cpCount < MAX_PAGE_GLYPHS; li++) {
|
||||||
uint32_t leftCp = s.ligaturePairs[li].pair >> 16;
|
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);
|
kernLigOk = buildMiniKernMatrix(s, codepoints, cpCount);
|
||||||
}
|
}
|
||||||
} else if (loadKernLigatureData) {
|
} else if (loadKernLigatureData) {
|
||||||
loadStyleKernLigatureData(s);
|
loadStyleKernLigatureData(s, /*ligatureOnly=*/true);
|
||||||
// Don't set kernLigOk → mini kern matrix stays null on miniData, but
|
// Don't set kernLigOk → mini kern matrix stays null on miniData, but
|
||||||
// ligatures are still resident on stubData (set in loadStyleKernLigatureData).
|
// 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) {
|
if (kernLigOk) {
|
||||||
// Full prewarm: wire mini kern matrix + class tables + ligatures.
|
// Full prewarm: wire mini kern matrix + class tables + ligatures.
|
||||||
applyKernLigaturePointers(s, s.miniData);
|
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"
|
// Layout-only prewarm: wire ligatures so applyLigatures() works (e.g. "fi"
|
||||||
// measures correctly). Skip the kern matrix — getKerning() returns 0
|
// measures correctly). Skip the kern matrix — getKerning() returns 0
|
||||||
// cleanly when kernMatrix is null. Per-pair kern is applied at render time.
|
// 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.epdFont.data = &s.miniData;
|
||||||
s.miniMode = metadataOnly ? PerStyle::MiniMode::METADATA : PerStyle::MiniMode::FULL;
|
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
|
// Accumulate stats
|
||||||
stats_.sdReadTimeMs += sdTime;
|
stats_.sdReadTimeMs += sdTime;
|
||||||
@@ -1236,6 +1245,10 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
|||||||
const auto& s = self->styles_[styleIdx];
|
const auto& s = self->styles_[styleIdx];
|
||||||
if (!s.fullIntervals) return nullptr;
|
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)
|
// Check overflow cache first (matching both codepoint and style)
|
||||||
for (uint32_t i = 0; i < self->overflowCount_; i++) {
|
for (uint32_t i = 0; i < self->overflowCount_; i++) {
|
||||||
if (self->overflow_[i].codepoint == codepoint && self->overflow_[i].styleIdx == styleIdx) {
|
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)
|
// All reads succeeded — commit to slot (evict old entry if at capacity)
|
||||||
if (wasAtCapacity) {
|
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;
|
delete[] self->overflow_[slot].bitmap;
|
||||||
}
|
}
|
||||||
self->overflow_[slot].glyph = tempGlyph;
|
self->overflow_[slot].glyph = tempGlyph;
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ class SdCardFont {
|
|||||||
EpdKernClassEntry* kernLeftClasses = nullptr;
|
EpdKernClassEntry* kernLeftClasses = nullptr;
|
||||||
EpdKernClassEntry* kernRightClasses = nullptr;
|
EpdKernClassEntry* kernRightClasses = nullptr;
|
||||||
EpdLigaturePair* ligaturePairs = 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
|
// Stub EpdFontData returned when not prewarmed
|
||||||
EpdFontData stubData{};
|
EpdFontData stubData{};
|
||||||
@@ -204,7 +205,7 @@ class SdCardFont {
|
|||||||
void freeStyleAll(PerStyle& s);
|
void freeStyleAll(PerStyle& s);
|
||||||
void freeStyleKernLigatureData(PerStyle& s);
|
void freeStyleKernLigatureData(PerStyle& s);
|
||||||
void freeStyleMiniKern(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);
|
bool buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount);
|
||||||
void applyKernLigaturePointers(const PerStyle& s, EpdFontData& data) const;
|
void applyKernLigaturePointers(const PerStyle& s, EpdFontData& data) const;
|
||||||
void applyGlyphMissCallback(uint8_t styleIdx);
|
void applyGlyphMissCallback(uint8_t styleIdx);
|
||||||
|
|||||||
Reference in New Issue
Block a user