perf: font-compression improvements (#1056)
## Purpose This PR includes some preparatory changes that are needed for an upcoming performant CJK font feature. The changes have no impact on render time and heap allocation for latin text. **Despite this, I think these changes stand on their own as a better font compression/decompression implementation.** ## Summary - Font decompressor rewrite: Replaced the 4-slot LRU group cache with a two-tier system — a page buffer (glyphs prewarmed before rendering begins) and a hot-group fallback (last decompressed group retained for non-prewarmed glyphs). - Byte-aligned compressed bitmap format: Glyph bitmaps within compressed groups are now stored row-padded rather than tightly packed before DEFLATE compression, improving compression ratios by making identical pixel rows produce identical byte patterns. Glyphs are compacted back to packed format on demand at render time. Reduces flash size by 155 KB. - Page prewarm system: Added `Page::collectText` and `Page::getDominantStyle` to extract per-style glyph requirements before rendering, and `GfxRenderer::prewarmFontCache` to pre-decompress only the groups needed for the dominant style — eliminating mid-render decompression for the common case. - UTF-8 robustness fixes: `utf8NextCodepoint` now validates continuation bytes and returns a replacement glyph on malformed input; `ChapterHtmlSlimParser` correctly preserves incomplete multi-byte sequences across word-buffer flush boundaries rather than splitting them. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_ Architecture and design was done by me, refined a bit by Claude. Code mostly by Claude, but not entirely.
This commit is contained in:
committed by
GitHub
parent
b467ea7973
commit
f1e9dc7f30
@@ -2,11 +2,13 @@
|
||||
|
||||
#include <Epub/Page.h>
|
||||
#include <Epub/blocks/TextBlock.h>
|
||||
#include <FontCacheManager.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <esp_system.h>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
@@ -632,7 +634,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
const auto start = millis();
|
||||
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
||||
renderer.clearFontCache();
|
||||
}
|
||||
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
|
||||
|
||||
@@ -662,11 +663,30 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
|
||||
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
||||
const int orientedMarginRight, const int orientedMarginBottom,
|
||||
const int orientedMarginLeft) {
|
||||
const auto t0 = millis();
|
||||
auto* fcm = renderer.getFontCacheManager();
|
||||
fcm->resetStats();
|
||||
|
||||
// Font prewarm: scan pass accumulates text, then prewarm, then real render
|
||||
const uint32_t heapBefore = esp_get_free_heap_size();
|
||||
auto scope = fcm->createPrewarmScope();
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); // scan pass
|
||||
scope.endScanAndPrewarm();
|
||||
const uint32_t heapAfter = esp_get_free_heap_size();
|
||||
fcm->logStats("prewarm");
|
||||
const auto tPrewarm = millis();
|
||||
|
||||
LOG_DBG("ERS", "Heap: before=%lu after=%lu delta=%ld", heapBefore, heapAfter,
|
||||
(int32_t)heapAfter - (int32_t)heapBefore);
|
||||
|
||||
// Force special handling for pages with images when anti-aliasing is on
|
||||
bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing;
|
||||
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||
renderStatusBar();
|
||||
fcm->logStats("bw_render");
|
||||
const auto tBwRender = millis();
|
||||
|
||||
if (imagePageWithAA) {
|
||||
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
|
||||
// HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust.
|
||||
@@ -689,9 +709,11 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
} else {
|
||||
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||
}
|
||||
const auto tDisplay = millis();
|
||||
|
||||
// Save bw buffer to reset buffer state after grayscale data sync
|
||||
renderer.storeBwBuffer();
|
||||
const auto tBwStore = millis();
|
||||
|
||||
// grayscale rendering
|
||||
// TODO: Only do this if font supports it
|
||||
@@ -700,20 +722,42 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
const auto tGrayLsb = millis();
|
||||
|
||||
// Render and copy to MSB buffer
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
const auto tGrayMsb = millis();
|
||||
|
||||
// display grayscale part
|
||||
renderer.displayGrayBuffer();
|
||||
const auto tGrayDisplay = millis();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
}
|
||||
fcm->logStats("gray");
|
||||
|
||||
// restore the bw data
|
||||
renderer.restoreBwBuffer();
|
||||
// restore the bw data
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
|
||||
const auto tEnd = millis();
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums "
|
||||
"gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore,
|
||||
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
|
||||
} else {
|
||||
// restore the bw data
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
|
||||
const auto tEnd = millis();
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tBwRestore - tBwStore,
|
||||
tEnd - t0);
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderActivity::renderStatusBar() const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "TxtReaderActivity.h"
|
||||
|
||||
#include <FontCacheManager.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
@@ -320,7 +321,6 @@ void TxtReaderActivity::render(RenderLock&&) {
|
||||
|
||||
renderer.clearScreen();
|
||||
renderPage();
|
||||
renderer.clearFontCache();
|
||||
|
||||
// Save progress
|
||||
saveProgress();
|
||||
@@ -365,7 +365,13 @@ void TxtReaderActivity::renderPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// First pass: BW rendering
|
||||
// Font prewarm: scan pass accumulates text, then prewarm, then real render
|
||||
auto* fcm = renderer.getFontCacheManager();
|
||||
auto scope = fcm->createPrewarmScope();
|
||||
renderLines(); // scan pass — text accumulated, no drawing
|
||||
scope.endScanAndPrewarm();
|
||||
|
||||
// BW rendering
|
||||
renderLines();
|
||||
renderStatusBar();
|
||||
|
||||
@@ -374,6 +380,7 @@ void TxtReaderActivity::renderPage() {
|
||||
if (SETTINGS.textAntiAliasing) {
|
||||
ReaderUtils::renderAntiAliased(renderer, [&renderLines]() { renderLines(); });
|
||||
}
|
||||
// scope destructor clears font cache via FontCacheManager
|
||||
}
|
||||
|
||||
void TxtReaderActivity::renderStatusBar() const {
|
||||
|
||||
Reference in New Issue
Block a user