Improve pre warm caching
This commit is contained in:
+315
-51
@@ -50,6 +50,8 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
|||||||
s.miniBitmap = nullptr;
|
s.miniBitmap = nullptr;
|
||||||
s.miniIntervalCount = 0;
|
s.miniIntervalCount = 0;
|
||||||
s.miniGlyphCount = 0;
|
s.miniGlyphCount = 0;
|
||||||
|
s.miniMode = PerStyle::MiniMode::NONE;
|
||||||
|
s.reportedMissCount = 0;
|
||||||
freeStyleMiniKern(s);
|
freeStyleMiniKern(s);
|
||||||
memset(&s.miniData, 0, sizeof(s.miniData));
|
memset(&s.miniData, 0, sizeof(s.miniData));
|
||||||
s.epdFont.data = &s.stubData;
|
s.epdFont.data = &s.stubData;
|
||||||
@@ -667,30 +669,183 @@ int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOn
|
|||||||
return totalMissed;
|
return totalMissed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true iff every codepoint in `codepoints[0..cpCount)` is already covered
|
||||||
|
// by the style's current miniIntervals. O(cpCount * log intervalCount).
|
||||||
|
bool SdCardFont::allCpsCovered(const PerStyle& s, const uint32_t* codepoints, uint32_t cpCount) {
|
||||||
|
if (s.miniIntervalCount == 0) return false;
|
||||||
|
for (uint32_t i = 0; i < cpCount; i++) {
|
||||||
|
const uint32_t cp = codepoints[i];
|
||||||
|
// Binary search miniIntervals for the range containing cp.
|
||||||
|
int lo = 0, hi = static_cast<int>(s.miniIntervalCount) - 1;
|
||||||
|
bool found = false;
|
||||||
|
while (lo <= hi) {
|
||||||
|
int mid = (lo + hi) / 2;
|
||||||
|
const auto& iv = s.miniIntervals[mid];
|
||||||
|
if (cp < iv.first) {
|
||||||
|
hi = mid - 1;
|
||||||
|
} else if (cp > iv.last) {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly) {
|
int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly) {
|
||||||
auto& s = styles_[styleIdx];
|
auto& s = styles_[styleIdx];
|
||||||
|
|
||||||
|
// ---- Fast-path coverage check ----
|
||||||
|
// If the existing cache already covers all requested cps in a compatible mode,
|
||||||
|
// there's nothing to do. This is the dominant case during pagination after the
|
||||||
|
// first paragraph has populated the metadata cache.
|
||||||
|
if (s.miniMode != PerStyle::MiniMode::NONE && allCpsCovered(s, codepoints, cpCount)) {
|
||||||
|
// For metadata-only calls, METADATA or FULL cache both satisfy layout queries.
|
||||||
|
// For full (bitmap) calls, only FULL satisfies — METADATA lacks bitmap data.
|
||||||
|
if (metadataOnly || s.miniMode == PerStyle::MiniMode::FULL) {
|
||||||
|
// Already wired into miniData; nothing else to do.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Decide merge vs rebuild ----
|
||||||
|
// Merge mode: extend the existing METADATA cache with the new cps without
|
||||||
|
// re-reading already-loaded glyphs. Only safe when:
|
||||||
|
// - request is metadata-only (no bitmap data needed)
|
||||||
|
// - existing mode is METADATA
|
||||||
|
// - merged total stays within MAX_PAGE_GLYPHS
|
||||||
|
// In all other cases we fall back to today's full-rebuild behavior (which is
|
||||||
|
// also what happens for full bitmap prewarms — those are page-scoped).
|
||||||
|
const bool tryMerge =
|
||||||
|
metadataOnly && s.miniMode == PerStyle::MiniMode::METADATA && s.miniGlyphs != nullptr && s.miniGlyphCount > 0;
|
||||||
|
|
||||||
// Map codepoints to global glyph indices for this style
|
// Map codepoints to global glyph indices for this style
|
||||||
struct CpGlyphMapping {
|
struct CpGlyphMapping {
|
||||||
uint32_t codepoint;
|
uint32_t codepoint;
|
||||||
int32_t globalIndex;
|
int32_t globalIndex;
|
||||||
};
|
};
|
||||||
CpGlyphMapping* mappings = new (std::nothrow) CpGlyphMapping[cpCount];
|
|
||||||
|
// Worst-case allocation: existing + new (for the merge path) or just new.
|
||||||
|
const uint32_t mappingCapacity = tryMerge ? (s.miniGlyphCount + cpCount) : cpCount;
|
||||||
|
CpGlyphMapping* mappings = new (std::nothrow) CpGlyphMapping[mappingCapacity];
|
||||||
if (!mappings) {
|
if (!mappings) {
|
||||||
LOG_ERR("SDCF", "Failed to allocate mapping array for style %u", styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mapping array for style %u", styleIdx);
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t validCount = 0;
|
uint32_t validCount = 0;
|
||||||
|
|
||||||
|
// For merge: seed mappings with already-loaded cps + their global indices,
|
||||||
|
// recovered by walking miniIntervals and re-resolving via fullIntervals.
|
||||||
|
// (We don't store globalIndex per mini-glyph; recomputing it is a binary
|
||||||
|
// search per loaded cp, cheap relative to SD I/O.)
|
||||||
|
if (tryMerge) {
|
||||||
|
for (uint32_t iv = 0; iv < s.miniIntervalCount; iv++) {
|
||||||
|
const auto& interval = s.miniIntervals[iv];
|
||||||
|
for (uint32_t cp = interval.first; cp <= interval.last; cp++) {
|
||||||
|
int32_t gIdx = findGlobalGlyphIndex(s, cp);
|
||||||
|
if (gIdx < 0) continue; // shouldn't happen; defensive
|
||||||
|
mappings[validCount].codepoint = cp;
|
||||||
|
mappings[validCount].globalIndex = gIdx;
|
||||||
|
validCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark where the existing-glyph block ends; new cps follow.
|
||||||
|
const uint32_t existingCount = validCount;
|
||||||
|
|
||||||
|
// Add new requested cps that aren't already in the existing block.
|
||||||
for (uint32_t i = 0; i < cpCount; i++) {
|
for (uint32_t i = 0; i < cpCount; i++) {
|
||||||
int32_t idx = findGlobalGlyphIndex(s, codepoints[i]);
|
const uint32_t cp = codepoints[i];
|
||||||
|
|
||||||
|
// Skip cps already in the existing block (only relevant in merge mode).
|
||||||
|
if (existingCount > 0) {
|
||||||
|
bool dup = false;
|
||||||
|
// Existing cps are sorted; binary search.
|
||||||
|
int lo = 0, hi = static_cast<int>(existingCount) - 1;
|
||||||
|
while (lo <= hi) {
|
||||||
|
int mid = (lo + hi) / 2;
|
||||||
|
if (mappings[mid].codepoint < cp) {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else if (mappings[mid].codepoint > cp) {
|
||||||
|
hi = mid - 1;
|
||||||
|
} else {
|
||||||
|
dup = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dup) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t idx = findGlobalGlyphIndex(s, cp);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
mappings[validCount].codepoint = codepoints[i];
|
if (validCount >= MAX_PAGE_GLYPHS) {
|
||||||
|
LOG_DBG("SDCF", "Cumulative cap (%u glyphs) reached for style %u; dropping merge cache", MAX_PAGE_GLYPHS,
|
||||||
|
styleIdx);
|
||||||
|
// Soft cap: discard the accumulated cache and fall back to rebuilding
|
||||||
|
// with just the new request set. Caller's text will be covered;
|
||||||
|
// already-loaded but no-longer-requested cps are lost.
|
||||||
|
delete[] mappings;
|
||||||
|
freeStyleMiniData(s);
|
||||||
|
// Recurse with rebuild semantics by clearing tryMerge state and trying
|
||||||
|
// again — implemented as inline reset below.
|
||||||
|
return prewarmStyle(styleIdx, codepoints, cpCount, metadataOnly);
|
||||||
|
}
|
||||||
|
mappings[validCount].codepoint = cp;
|
||||||
mappings[validCount].globalIndex = idx;
|
mappings[validCount].globalIndex = idx;
|
||||||
validCount++;
|
validCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int missed = static_cast<int>(cpCount - validCount);
|
// Sort the full mappings array by codepoint (existing block is already sorted,
|
||||||
|
// so std::sort on a partially sorted array is fast in practice; insertion-sort
|
||||||
|
// would be optimal but std::sort is fine for our sizes).
|
||||||
|
if (validCount > 0) {
|
||||||
|
std::sort(mappings, mappings + validCount,
|
||||||
|
[](const CpGlyphMapping& a, const CpGlyphMapping& b) { return a.codepoint < b.codepoint; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count cps that are *newly* missing this accumulation cycle (i.e. not present
|
||||||
|
// in mappings[] AND not previously reported). Already-reported misses are
|
||||||
|
// suppressed so the same 4 special chars don't spam the log every paragraph.
|
||||||
|
// `mappings[]` is sorted by codepoint after the std::sort above, enabling
|
||||||
|
// O(log validCount) lookup per requested cp.
|
||||||
|
int missed = 0;
|
||||||
|
for (uint32_t i = 0; i < cpCount; i++) {
|
||||||
|
const uint32_t cp = codepoints[i];
|
||||||
|
int lo = 0, hi = static_cast<int>(validCount) - 1;
|
||||||
|
bool found = false;
|
||||||
|
while (lo <= hi) {
|
||||||
|
int mid = (lo + hi) / 2;
|
||||||
|
if (mappings[mid].codepoint < cp) {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else if (mappings[mid].codepoint > cp) {
|
||||||
|
hi = mid - 1;
|
||||||
|
} else {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) continue;
|
||||||
|
|
||||||
|
// cp wasn't resolved. Check if we've already reported it this cycle.
|
||||||
|
bool alreadyReported = false;
|
||||||
|
for (uint8_t r = 0; r < s.reportedMissCount; r++) {
|
||||||
|
if (s.reportedMisses[r] == cp) {
|
||||||
|
alreadyReported = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alreadyReported) continue;
|
||||||
|
|
||||||
|
if (s.reportedMissCount < PerStyle::MAX_REPORTED_MISSES) {
|
||||||
|
s.reportedMisses[s.reportedMissCount++] = cp;
|
||||||
|
}
|
||||||
|
missed++;
|
||||||
|
}
|
||||||
|
|
||||||
if (validCount == 0) {
|
if (validCount == 0) {
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
@@ -699,13 +854,29 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
return missed;
|
return missed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build mini intervals from sorted codepoints
|
// Stash the old miniGlyphs so we can copy already-loaded entries by codepoint
|
||||||
|
// before freeing them. (For merge path; nullptr when not merging.)
|
||||||
|
EpdGlyph* oldGlyphs = tryMerge ? s.miniGlyphs : nullptr;
|
||||||
|
EpdUnicodeInterval* oldIntervals = tryMerge ? s.miniIntervals : nullptr;
|
||||||
|
uint32_t oldIntervalCount = tryMerge ? s.miniIntervalCount : 0;
|
||||||
|
// Detach so freeStyleMiniData doesn't delete them yet.
|
||||||
|
if (tryMerge) {
|
||||||
|
s.miniIntervals = nullptr;
|
||||||
|
s.miniGlyphs = nullptr;
|
||||||
|
s.miniIntervalCount = 0;
|
||||||
|
s.miniGlyphCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// freeStyleMiniData wipes everything, including miniBitmap (which is nullptr
|
||||||
|
// in metadata mode anyway). Safe in either path.
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
|
|
||||||
uint32_t intervalCapacity = validCount;
|
uint32_t intervalCapacity = validCount;
|
||||||
s.miniIntervals = new (std::nothrow) EpdUnicodeInterval[intervalCapacity];
|
s.miniIntervals = new (std::nothrow) EpdUnicodeInterval[intervalCapacity];
|
||||||
if (!s.miniIntervals) {
|
if (!s.miniIntervals) {
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
|
||||||
|
delete[] oldGlyphs;
|
||||||
|
delete[] oldIntervals;
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
}
|
}
|
||||||
@@ -727,65 +898,138 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
s.miniGlyphs = new (std::nothrow) EpdGlyph[s.miniGlyphCount];
|
s.miniGlyphs = new (std::nothrow) EpdGlyph[s.miniGlyphCount];
|
||||||
if (!s.miniGlyphs) {
|
if (!s.miniGlyphs) {
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
|
||||||
|
delete[] oldGlyphs;
|
||||||
|
delete[] oldIntervals;
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build sorted read order for sequential I/O
|
// Build a tracking array of which mappings still need SD I/O. For the merge
|
||||||
uint32_t* readOrder = new (std::nothrow) uint32_t[validCount];
|
// path, copy already-loaded glyph metadata from oldGlyphs first; remaining
|
||||||
if (!readOrder) {
|
// entries get read from SD below.
|
||||||
LOG_ERR("SDCF", "Failed to allocate read order for style %u", styleIdx);
|
bool* needsRead = new (std::nothrow) bool[validCount];
|
||||||
|
if (!needsRead) {
|
||||||
|
LOG_ERR("SDCF", "Failed to allocate needsRead array for style %u", styleIdx);
|
||||||
|
delete[] oldGlyphs;
|
||||||
|
delete[] oldIntervals;
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
}
|
}
|
||||||
for (uint32_t i = 0; i < validCount; i++) readOrder[i] = i;
|
for (uint32_t i = 0; i < validCount; i++) needsRead[i] = true;
|
||||||
std::sort(readOrder, readOrder + validCount,
|
|
||||||
[&](uint32_t a, uint32_t b) { return mappings[a].globalIndex < mappings[b].globalIndex; });
|
|
||||||
|
|
||||||
FsFile file;
|
if (tryMerge && oldGlyphs && oldIntervals) {
|
||||||
if (!Storage.openFileForRead("SDCF", filePath_, file)) {
|
// For each cp in the new merged set, look it up in the old miniIntervals to
|
||||||
LOG_ERR("SDCF", "Failed to reopen .cpfont for prewarm (style %u)", styleIdx);
|
// see if we already have its EpdGlyph. If yes, copy it over and skip the read.
|
||||||
delete[] readOrder;
|
for (uint32_t i = 0; i < validCount; i++) {
|
||||||
delete[] mappings;
|
const uint32_t cp = mappings[i].codepoint;
|
||||||
freeStyleMiniData(s);
|
// Binary search oldIntervals for cp.
|
||||||
return static_cast<int>(cpCount);
|
int lo = 0, hi = static_cast<int>(oldIntervalCount) - 1;
|
||||||
}
|
while (lo <= hi) {
|
||||||
|
int mid = (lo + hi) / 2;
|
||||||
unsigned long sdStart = millis();
|
const auto& iv = oldIntervals[mid];
|
||||||
uint32_t seekCount = 0;
|
if (cp < iv.first) {
|
||||||
|
hi = mid - 1;
|
||||||
// Read glyph metadata. lastReadIndex tracks sequential reads to skip redundant
|
} else if (cp > iv.last) {
|
||||||
// seeks; INT32_MIN guarantees the first iteration always seeks to the correct
|
lo = mid + 1;
|
||||||
// offset (otherwise when gIdx == 0, the "gIdx != lastReadIndex + 1" check would
|
} else {
|
||||||
// be false and we'd read from the file's current position — the header — which
|
s.miniGlyphs[i] = oldGlyphs[iv.offset + (cp - iv.first)];
|
||||||
// decodes to a garbage EpdGlyph with a massive advanceX, inflating any word
|
needsRead[i] = false;
|
||||||
// containing that codepoint beyond page width).
|
break;
|
||||||
int32_t lastReadIndex = INT32_MIN;
|
}
|
||||||
for (uint32_t i = 0; i < validCount; i++) {
|
}
|
||||||
uint32_t mapIdx = readOrder[i];
|
|
||||||
int32_t gIdx = mappings[mapIdx].globalIndex;
|
|
||||||
|
|
||||||
uint32_t fileOff = s.glyphsFileOffset + static_cast<uint32_t>(gIdx) * sizeof(EpdGlyph);
|
|
||||||
if (gIdx != lastReadIndex + 1) {
|
|
||||||
file.seekSet(fileOff);
|
|
||||||
seekCount++;
|
|
||||||
}
|
}
|
||||||
if (file.read(reinterpret_cast<uint8_t*>(&s.miniGlyphs[mapIdx]), sizeof(EpdGlyph)) != sizeof(EpdGlyph)) {
|
}
|
||||||
LOG_ERR("SDCF", "Prewarm: short glyph read (style %u, glyph %d)", styleIdx, gIdx);
|
|
||||||
file.close();
|
delete[] oldGlyphs;
|
||||||
delete[] readOrder;
|
delete[] oldIntervals;
|
||||||
|
|
||||||
|
// Count how many SD reads are still needed, and build a sorted read order.
|
||||||
|
uint32_t toReadCount = 0;
|
||||||
|
for (uint32_t i = 0; i < validCount; i++) {
|
||||||
|
if (needsRead[i]) toReadCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t* readOrder = nullptr;
|
||||||
|
if (toReadCount > 0) {
|
||||||
|
readOrder = new (std::nothrow) uint32_t[toReadCount];
|
||||||
|
if (!readOrder) {
|
||||||
|
LOG_ERR("SDCF", "Failed to allocate read order for style %u", styleIdx);
|
||||||
|
delete[] needsRead;
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
}
|
}
|
||||||
lastReadIndex = gIdx;
|
uint32_t k = 0;
|
||||||
|
for (uint32_t i = 0; i < validCount; i++) {
|
||||||
|
if (needsRead[i]) readOrder[k++] = i;
|
||||||
|
}
|
||||||
|
std::sort(readOrder, readOrder + toReadCount,
|
||||||
|
[&](uint32_t a, uint32_t b) { return mappings[a].globalIndex < mappings[b].globalIndex; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned long sdStart = millis();
|
||||||
|
uint32_t seekCount = 0;
|
||||||
|
FsFile file;
|
||||||
|
|
||||||
|
if (toReadCount > 0) {
|
||||||
|
if (!Storage.openFileForRead("SDCF", filePath_, file)) {
|
||||||
|
LOG_ERR("SDCF", "Failed to reopen .cpfont for prewarm (style %u)", styleIdx);
|
||||||
|
delete[] readOrder;
|
||||||
|
delete[] needsRead;
|
||||||
|
delete[] mappings;
|
||||||
|
freeStyleMiniData(s);
|
||||||
|
return static_cast<int>(cpCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read glyph metadata. lastReadIndex tracks sequential reads to skip redundant
|
||||||
|
// seeks; INT32_MIN guarantees the first iteration always seeks to the correct
|
||||||
|
// offset (otherwise when gIdx == 0, the "gIdx != lastReadIndex + 1" check would
|
||||||
|
// be false and we'd read from the file's current position — the header — which
|
||||||
|
// decodes to a garbage EpdGlyph with a massive advanceX, inflating any word
|
||||||
|
// containing that codepoint beyond page width).
|
||||||
|
int32_t lastReadIndex = INT32_MIN;
|
||||||
|
for (uint32_t i = 0; i < toReadCount; i++) {
|
||||||
|
uint32_t mapIdx = readOrder[i];
|
||||||
|
int32_t gIdx = mappings[mapIdx].globalIndex;
|
||||||
|
|
||||||
|
uint32_t fileOff = s.glyphsFileOffset + static_cast<uint32_t>(gIdx) * sizeof(EpdGlyph);
|
||||||
|
if (gIdx != lastReadIndex + 1) {
|
||||||
|
file.seekSet(fileOff);
|
||||||
|
seekCount++;
|
||||||
|
}
|
||||||
|
if (file.read(reinterpret_cast<uint8_t*>(&s.miniGlyphs[mapIdx]), sizeof(EpdGlyph)) != sizeof(EpdGlyph)) {
|
||||||
|
LOG_ERR("SDCF", "Prewarm: short glyph read (style %u, glyph %d)", styleIdx, gIdx);
|
||||||
|
file.close();
|
||||||
|
delete[] readOrder;
|
||||||
|
delete[] needsRead;
|
||||||
|
delete[] mappings;
|
||||||
|
freeStyleMiniData(s);
|
||||||
|
return static_cast<int>(cpCount);
|
||||||
|
}
|
||||||
|
lastReadIndex = gIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete[] needsRead;
|
||||||
|
delete[] readOrder;
|
||||||
|
readOrder = nullptr;
|
||||||
|
|
||||||
uint32_t totalBitmapSize = 0;
|
uint32_t totalBitmapSize = 0;
|
||||||
|
|
||||||
if (!metadataOnly) {
|
if (!metadataOnly) {
|
||||||
|
// Full render prewarm always reads bitmap data for every glyph in the
|
||||||
|
// mini set. The metadata-pass file open above only covered cps that
|
||||||
|
// needed metadata reads — open the file now if it isn't already.
|
||||||
|
if (!file) {
|
||||||
|
if (!Storage.openFileForRead("SDCF", filePath_, file)) {
|
||||||
|
LOG_ERR("SDCF", "Failed to reopen .cpfont for bitmap prewarm (style %u)", styleIdx);
|
||||||
|
delete[] mappings;
|
||||||
|
freeStyleMiniData(s);
|
||||||
|
return static_cast<int>(cpCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Compute total bitmap size
|
// Compute total bitmap size
|
||||||
for (uint32_t i = 0; i < validCount; i++) {
|
for (uint32_t i = 0; i < validCount; i++) {
|
||||||
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
||||||
@@ -795,20 +1039,29 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
if (!s.miniBitmap) {
|
if (!s.miniBitmap) {
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
||||||
file.close();
|
file.close();
|
||||||
delete[] readOrder;
|
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read bitmap data sorted by file offset
|
// Allocate a fresh readOrder covering all validCount glyphs, sorted by
|
||||||
std::sort(readOrder, readOrder + validCount,
|
// bitmap file offset for sequential I/O.
|
||||||
|
uint32_t* bitmapOrder = new (std::nothrow) uint32_t[validCount];
|
||||||
|
if (!bitmapOrder) {
|
||||||
|
LOG_ERR("SDCF", "Failed to allocate bitmap read order for style %u", styleIdx);
|
||||||
|
file.close();
|
||||||
|
delete[] mappings;
|
||||||
|
freeStyleMiniData(s);
|
||||||
|
return static_cast<int>(cpCount);
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < validCount; i++) bitmapOrder[i] = i;
|
||||||
|
std::sort(bitmapOrder, bitmapOrder + validCount,
|
||||||
[&](uint32_t a, uint32_t b) { return s.miniGlyphs[a].dataOffset < s.miniGlyphs[b].dataOffset; });
|
[&](uint32_t a, uint32_t b) { return s.miniGlyphs[a].dataOffset < s.miniGlyphs[b].dataOffset; });
|
||||||
|
|
||||||
uint32_t miniBitmapOffset = 0;
|
uint32_t miniBitmapOffset = 0;
|
||||||
uint32_t lastBitmapEnd = UINT32_MAX;
|
uint32_t lastBitmapEnd = UINT32_MAX;
|
||||||
for (uint32_t i = 0; i < validCount; i++) {
|
for (uint32_t i = 0; i < validCount; i++) {
|
||||||
uint32_t mapIdx = readOrder[i];
|
uint32_t mapIdx = bitmapOrder[i];
|
||||||
EpdGlyph& glyph = s.miniGlyphs[mapIdx];
|
EpdGlyph& glyph = s.miniGlyphs[mapIdx];
|
||||||
|
|
||||||
if (glyph.dataLength == 0) {
|
if (glyph.dataLength == 0) {
|
||||||
@@ -824,7 +1077,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
if (file.read(s.miniBitmap + miniBitmapOffset, glyph.dataLength) != static_cast<int>(glyph.dataLength)) {
|
if (file.read(s.miniBitmap + miniBitmapOffset, glyph.dataLength) != static_cast<int>(glyph.dataLength)) {
|
||||||
LOG_ERR("SDCF", "Prewarm: short bitmap read (style %u)", styleIdx);
|
LOG_ERR("SDCF", "Prewarm: short bitmap read (style %u)", styleIdx);
|
||||||
file.close();
|
file.close();
|
||||||
delete[] readOrder;
|
delete[] bitmapOrder;
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
@@ -834,11 +1087,11 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
glyph.dataOffset = miniBitmapOffset;
|
glyph.dataOffset = miniBitmapOffset;
|
||||||
miniBitmapOffset += glyph.dataLength;
|
miniBitmapOffset += glyph.dataLength;
|
||||||
}
|
}
|
||||||
|
delete[] bitmapOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t sdTime = millis() - sdStart;
|
uint32_t sdTime = millis() - sdStart;
|
||||||
file.close();
|
if (file) file.close();
|
||||||
delete[] readOrder;
|
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
|
|
||||||
// Full render prewarm: load the persistent kern classes + ligatures (one-time
|
// Full render prewarm: load the persistent kern classes + ligatures (one-time
|
||||||
@@ -870,6 +1123,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
s.miniData.glyphMissCtx = &overflowCtx_[styleIdx];
|
s.miniData.glyphMissCtx = &overflowCtx_[styleIdx];
|
||||||
|
|
||||||
s.epdFont.data = &s.miniData;
|
s.epdFont.data = &s.miniData;
|
||||||
|
s.miniMode = metadataOnly ? PerStyle::MiniMode::METADATA : PerStyle::MiniMode::FULL;
|
||||||
|
|
||||||
// Accumulate stats
|
// Accumulate stats
|
||||||
stats_.sdReadTimeMs += sdTime;
|
stats_.sdReadTimeMs += sdTime;
|
||||||
@@ -891,6 +1145,16 @@ void SdCardFont::clearCache() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdCardFont::clearAccumulation() {
|
||||||
|
// Same as clearCache() but skips the overflow ring buffer (per-glyph on-demand
|
||||||
|
// loads are independent of the cumulative metadata cache).
|
||||||
|
for (uint8_t i = 0; i < MAX_STYLES; i++) {
|
||||||
|
if (!styles_[i].present) continue;
|
||||||
|
freeStyleMiniData(styles_[i]);
|
||||||
|
applyGlyphMissCallback(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Stats ---
|
// --- Stats ---
|
||||||
|
|
||||||
void SdCardFont::logStats(const char* label) {
|
void SdCardFont::logStats(const char* label) {
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ class SdCardFont {
|
|||||||
// Free mini data for all styles, restore stub EpdFontData.
|
// Free mini data for all styles, restore stub EpdFontData.
|
||||||
void clearCache();
|
void clearCache();
|
||||||
|
|
||||||
|
// Soft cache reset: drop the cumulative metadata-only prewarm cache built
|
||||||
|
// up by repeated layout-time ensureSdCardFontReady() calls. Bitmap-mode
|
||||||
|
// (FULL) caches are also dropped. Call this between sections to bound the
|
||||||
|
// cumulative cp set growth across pagination.
|
||||||
|
void clearAccumulation();
|
||||||
|
|
||||||
// Returns pointer to the managed EpdFont for a given style.
|
// Returns pointer to the managed EpdFont for a given style.
|
||||||
// Returns nullptr if the style is not present.
|
// Returns nullptr if the style is not present.
|
||||||
EpdFont* getEpdFont(uint8_t style = 0);
|
EpdFont* getEpdFont(uint8_t style = 0);
|
||||||
@@ -117,6 +123,25 @@ class SdCardFont {
|
|||||||
uint32_t miniIntervalCount = 0;
|
uint32_t miniIntervalCount = 0;
|
||||||
uint32_t miniGlyphCount = 0;
|
uint32_t miniGlyphCount = 0;
|
||||||
|
|
||||||
|
// Cache mode for the current mini buffers:
|
||||||
|
// NONE = empty / freshly cleared, no glyphs loaded
|
||||||
|
// METADATA = miniGlyphs has glyph metrics only (no bitmap data); built
|
||||||
|
// up incrementally across paragraph-level ensureSdCardFontReady
|
||||||
|
// calls during pagination. Safe to merge new cps into.
|
||||||
|
// FULL = miniGlyphs + miniBitmap loaded, page-scoped (built once per
|
||||||
|
// page render). Layout-only prewarm calls are no-ops in this
|
||||||
|
// mode (overflow handler covers any uncovered cp at draw time).
|
||||||
|
enum class MiniMode : uint8_t { NONE, METADATA, FULL };
|
||||||
|
MiniMode miniMode = MiniMode::NONE;
|
||||||
|
|
||||||
|
// Codepoints already reported as missing during the current accumulation
|
||||||
|
// cycle. Cleared by clearAccumulation() / freeStyleAll(). Bounded by
|
||||||
|
// MAX_REPORTED_MISSES; once full, further misses go unreported (the cps
|
||||||
|
// still cleanly fall back to the replacement glyph in EpdFont::getGlyph).
|
||||||
|
static constexpr uint8_t MAX_REPORTED_MISSES = 32;
|
||||||
|
uint32_t reportedMisses[MAX_REPORTED_MISSES] = {};
|
||||||
|
uint8_t reportedMissCount = 0;
|
||||||
|
|
||||||
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
|
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
|
||||||
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
|
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
|
||||||
// used on the current page to renumbered class IDs (1..miniKern*ClassCount).
|
// used on the current page to renumbered class IDs (1..miniKern*ClassCount).
|
||||||
@@ -166,6 +191,7 @@ class SdCardFont {
|
|||||||
bool loaded_ = false;
|
bool loaded_ = false;
|
||||||
|
|
||||||
// Per-style helpers
|
// Per-style helpers
|
||||||
|
static bool allCpsCovered(const PerStyle& s, const uint32_t* codepoints, uint32_t cpCount);
|
||||||
void freeStyleMiniData(PerStyle& s);
|
void freeStyleMiniData(PerStyle& s);
|
||||||
void freeStyleAll(PerStyle& s);
|
void freeStyleAll(PerStyle& s);
|
||||||
void freeStyleKernLigatureData(PerStyle& s);
|
void freeStyleKernLigatureData(PerStyle& s);
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::clearSdCardFontAccumulation() const {
|
||||||
|
for (auto& [id, font] : sdCardFonts_) {
|
||||||
|
font->clearAccumulation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GfxRenderer::begin() {
|
void GfxRenderer::begin() {
|
||||||
frameBuffer = display.getFrameBuffer();
|
frameBuffer = display.getFrameBuffer();
|
||||||
if (!frameBuffer) {
|
if (!frameBuffer) {
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ class GfxRenderer {
|
|||||||
// (which holds a const GfxRenderer&) before measuring word widths. Safe to call on
|
// (which holds a const GfxRenderer&) before measuring word widths. Safe to call on
|
||||||
// non-SD fonts (no-op).
|
// non-SD fonts (no-op).
|
||||||
void ensureSdCardFontReady(int fontId, const char* utf8Text) const;
|
void ensureSdCardFontReady(int fontId, const char* utf8Text) const;
|
||||||
|
// Clear the cumulative SD card font metadata cache built up by repeated
|
||||||
|
// ensureSdCardFontReady() calls. Call between sections to bound the cumulative
|
||||||
|
// codepoint set growth across pagination. Safe to call when no SD font is active.
|
||||||
|
void clearSdCardFontAccumulation() const;
|
||||||
|
|
||||||
// Orientation control (affects logical width/height and coordinate transforms)
|
// Orientation control (affects logical width/height and coordinate transforms)
|
||||||
void setOrientation(const Orientation o) { orientation.store(static_cast<int>(o), std::memory_order_relaxed); }
|
void setOrientation(const Orientation o) { orientation.store(static_cast<int>(o), std::memory_order_relaxed); }
|
||||||
|
|||||||
@@ -1223,6 +1223,10 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
|||||||
GUI.fillPopupProgress(renderer, popupRect, progress);
|
GUI.fillPopupProgress(renderer, popupRect, progress);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reset cumulative SD font metadata cache so this section starts fresh.
|
||||||
|
// Pagination will rebuild only the cps it actually encounters, bounded
|
||||||
|
// by MAX_PAGE_GLYPHS per style.
|
||||||
|
renderer.clearSdCardFontAccumulation();
|
||||||
if (!section->createSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(),
|
if (!section->createSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering,
|
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering,
|
||||||
@@ -1385,6 +1389,8 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpineIndex);
|
LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpineIndex);
|
||||||
|
// Reset cumulative SD font metadata cache for the new section.
|
||||||
|
renderer.clearSdCardFontAccumulation();
|
||||||
if (!nextSection.createSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(),
|
if (!nextSection.createSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
|
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user