diff --git a/lib/EpdFont/SdCardFont.cpp b/lib/EpdFont/SdCardFont.cpp index 143e6c10..55c3d695 100644 --- a/lib/EpdFont/SdCardFont.cpp +++ b/lib/EpdFont/SdCardFont.cpp @@ -272,7 +272,7 @@ static uint8_t miniLookupKernClass(const EpdKernClassEntry* entries, uint16_t co // the mini versions together in applyKernLigaturePointers, so a codepoint not // on this page simply returns class 0 (no kerning), which was the pre-existing // behavior for any codepoint outside the kern classes. -bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount) { +bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount, HalFile& file) { freeStyleMiniKern(s); if (!s.kernLeftClasses || !s.kernRightClasses || s.header.kernLeftEntryCount == 0 || s.header.kernRightEntryCount == 0) { @@ -368,56 +368,123 @@ bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, ui } } - // Step 6: read the full matrix's rows for each used left class, keep only - // columns for used right classes. One SD seek + one read per used left class; - // a row is kernRightClassCount bytes (~200 for Literata). - FsFile file; - if (!Storage.openFileForRead("SDCF", filePath_, file)) { - LOG_ERR("SDCF", "Failed to open .cpfont for mini kern: %s", filePath_); - freeStyleMiniKern(s); - return false; + // Step 6: read the full matrix's rows for each used left class, keeping only + // columns for used right classes. + // + // Strategy: read the matrix in fixed-size chunks (KERN_CHUNK_BYTES), sweeping + // forward through the file. Each chunk covers a contiguous run of full rows; + // we extract the needed columns inline as each chunk is read. This costs at + // most ceil(fullMatrixBytes / KERN_CHUNK_BYTES) seeks + reads regardless of + // how many left classes are used, and caps the heap spike at KERN_CHUNK_BYTES + // instead of the full matrix size (~28 KB for Literata). + // + // Fallback: if even the chunk buffer can't be allocated, fall back to one + // seek + one row read per used left class (original behavior). + static constexpr uint32_t KERN_CHUNK_BYTES = 4096; + const uint32_t rowBytes = s.header.kernRightClassCount; + + // Build a sorted-by-oldL mapping so we sweep the file forward. + // Each entry: (oldL, newL) — oldL is the file row index (1-based). + // We reuse the usedLeft and usedRight scratch slots (slots 0 and 1), which + // are fully consumed after step 1 and not read again. newToOldLeft/Right + // (slots 4 and 5) must stay intact — they are still read during column + // extraction in the inner loop below. + uint8_t* sortedOldL = base + 0 * 256; // reuse usedLeft slot + uint8_t* sortedNewL = base + 1 * 256; // reuse usedRight slot + { + uint8_t k = 0; + // Sweep oldL in ascending order — newToOldLeft maps newL→oldL, so we + // scan all 255 possible oldL values and emit matches in order. O(255×numLeft). + for (int oldL = 1; oldL < 256; oldL++) { + for (uint8_t newL = 1; newL <= numLeft; newL++) { + if (newToOldLeft[newL] == static_cast(oldL)) { + sortedOldL[k] = static_cast(oldL); + sortedNewL[k] = newL; + k++; + break; + } + } + } + // k == numLeft at this point } - std::unique_ptr rowBuf(new (std::nothrow) int8_t[s.header.kernRightClassCount]); - if (!rowBuf) { - LOG_ERR("SDCF", "Failed to allocate row buffer (%u bytes)", s.header.kernRightClassCount); - file.close(); - freeStyleMiniKern(s); - return false; - } + std::unique_ptr chunkBuf(new (std::nothrow) int8_t[KERN_CHUNK_BYTES]); + if (chunkBuf) { + // Chunked sweep: one seek per chunk boundary, rows processed in oldL order. + uint32_t chunkStart = UINT32_MAX; // file offset of first byte in chunkBuf + uint32_t chunkEnd = 0; // one-past-last valid byte in chunkBuf + uint32_t chunkSeeks = 0; - for (uint8_t newL = 1; newL <= numLeft; newL++) { - const uint8_t oldL = newToOldLeft[newL]; - const uint32_t rowFileOff = s.kernMatrixFileOffset + (oldL - 1u) * s.header.kernRightClassCount; - if (!file.seekSet(rowFileOff)) { - LOG_ERR("SDCF", "Failed to seek to kern row %u", oldL); - file.close(); + for (uint8_t k = 0; k < numLeft; k++) { + const uint8_t oldL = sortedOldL[k]; + const uint8_t newL = sortedNewL[k]; + const uint32_t rowFileOff = s.kernMatrixFileOffset + (oldL - 1u) * rowBytes; + const uint32_t rowFileEnd = rowFileOff + rowBytes; + + // If this row is not fully inside the current chunk, load the next chunk. + if (rowFileOff < chunkStart || rowFileEnd > chunkEnd) { + chunkStart = rowFileOff; + uint32_t toRead = KERN_CHUNK_BYTES; + const uint32_t matrixEnd = + s.kernMatrixFileOffset + static_cast(s.header.kernLeftClassCount) * rowBytes; + if (chunkStart + toRead > matrixEnd) toRead = matrixEnd - chunkStart; + if (!file.seekSet(chunkStart)) { + LOG_ERR("SDCF", "Failed to seek to kern chunk at %u", chunkStart); + freeStyleMiniKern(s); + return false; + } + if (file.read(reinterpret_cast(chunkBuf.get()), toRead) != static_cast(toRead)) { + LOG_ERR("SDCF", "Failed to read kern chunk (%u bytes)", toRead); + freeStyleMiniKern(s); + return false; + } + chunkEnd = chunkStart + toRead; + chunkSeeks++; + } + + const int8_t* srcRow = chunkBuf.get() + (rowFileOff - chunkStart); + int8_t* miniRow = s.miniKernMatrix + (newL - 1u) * numRight; + for (uint8_t newR = 1; newR <= numRight; newR++) { + miniRow[newR - 1] = srcRow[newToOldRight[newR] - 1u]; + } + } + LOG_DBG("SDCF", "Built mini kern (chunked %uB): %u×%u=%u bytes, %u seeks (full was %u×%u=%u bytes)", + KERN_CHUNK_BYTES, numLeft, numRight, matrixBytes, chunkSeeks, s.header.kernLeftClassCount, + s.header.kernRightClassCount, static_cast(s.header.kernLeftClassCount) * rowBytes); + } else { + // Fallback: one seek + one row read per used left class. + LOG_DBG("SDCF", "Built mini kern (per-row fallback): %u rows", numLeft); + std::unique_ptr rowBuf(new (std::nothrow) int8_t[rowBytes]); + if (!rowBuf) { + LOG_ERR("SDCF", "Failed to allocate row buffer (%u bytes)", rowBytes); freeStyleMiniKern(s); return false; } - if (file.read(reinterpret_cast(rowBuf.get()), s.header.kernRightClassCount) != - static_cast(s.header.kernRightClassCount)) { - LOG_ERR("SDCF", "Failed to read kern row %u", oldL); - file.close(); - freeStyleMiniKern(s); - return false; - } - int8_t* miniRow = s.miniKernMatrix + (newL - 1u) * numRight; - for (uint8_t newR = 1; newR <= numRight; newR++) { - miniRow[newR - 1] = rowBuf[newToOldRight[newR] - 1u]; + for (uint8_t k = 0; k < numLeft; k++) { + const uint8_t oldL = sortedOldL[k]; + const uint8_t newL = sortedNewL[k]; + const uint32_t rowFileOff = s.kernMatrixFileOffset + (oldL - 1u) * rowBytes; + if (!file.seekSet(rowFileOff)) { + LOG_ERR("SDCF", "Failed to seek to kern row %u", oldL); + freeStyleMiniKern(s); + return false; + } + if (file.read(reinterpret_cast(rowBuf.get()), rowBytes) != static_cast(rowBytes)) { + LOG_ERR("SDCF", "Failed to read kern row %u", oldL); + freeStyleMiniKern(s); + return false; + } + int8_t* miniRow = s.miniKernMatrix + (newL - 1u) * numRight; + for (uint8_t newR = 1; newR <= numRight; newR++) { + miniRow[newR - 1] = rowBuf[newToOldRight[newR] - 1u]; + } } } - file.close(); - s.miniKernLeftEntryCount = lIdx; s.miniKernRightEntryCount = rIdx; s.miniKernLeftClassCount = numLeft; s.miniKernRightClassCount = numRight; - - LOG_DBG("SDCF", "Built mini kern: %u×%u matrix (%u bytes, full was %u×%u = %u bytes)", numLeft, numRight, matrixBytes, - s.header.kernLeftClassCount, s.header.kernRightClassCount, - static_cast(s.header.kernLeftClassCount) * s.header.kernRightClassCount); return true; } @@ -1164,7 +1231,6 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 } uint32_t sdTime = millis() - sdStart; - if (file) file.close(); delete[] mappings; // Kern/ligature wiring strategy: @@ -1183,7 +1249,17 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 bool kernLigOk = false; if (!metadataOnly) { if (loadStyleKernLigatureData(s)) { - kernLigOk = buildMiniKernMatrix(s, codepoints, cpCount); + // Reuse the file handle from the bitmap pass to avoid a redundant + // Storage.openFileForRead() inside buildMiniKernMatrix (fix A). + if (!file) { + if (!Storage.openFileForRead("SDCF", filePath_, file)) { + LOG_ERR("SDCF", "Failed to open .cpfont for kern matrix (style %u)", styleIdx); + } else { + kernLigOk = buildMiniKernMatrix(s, codepoints, cpCount, file); + } + } else { + kernLigOk = buildMiniKernMatrix(s, codepoints, cpCount, file); + } } } else if (loadKernLigatureData) { loadStyleKernLigatureData(s, /*ligatureOnly=*/true); @@ -1191,6 +1267,8 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 // ligatures are still resident on stubData (set in loadStyleKernLigatureData). } + if (file) file.close(); + // Populate miniData and swap memset(&s.miniData, 0, sizeof(s.miniData)); s.miniData.bitmap = s.miniBitmap; diff --git a/lib/EpdFont/SdCardFont.h b/lib/EpdFont/SdCardFont.h index 6776ce34..68a93535 100644 --- a/lib/EpdFont/SdCardFont.h +++ b/lib/EpdFont/SdCardFont.h @@ -6,6 +6,8 @@ #include "EpdFont.h" #include "EpdFontData.h" +class HalFile; + class SdCardFont { public: static constexpr uint16_t MAX_PAGE_GLYPHS = 512; @@ -206,7 +208,7 @@ class SdCardFont { void freeStyleKernLigatureData(PerStyle& s); void freeStyleMiniKern(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, HalFile& file); void applyKernLigaturePointers(const PerStyle& s, EpdFontData& data) const; void applyGlyphMissCallback(uint8_t styleIdx); int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;