Add idle-time glyph prewarming for next page
Preload glyphs for the next page during idle time to reduce page-turn latency. After a 400ms debounce period, scan the next page in FCM scan mode (no pixels drawn) to cache missing glyphs from SD card, avoiding ~100ms of SD reads during the actual page turn. Prewarm is deferred when rendering is active, heap is low, or during rapid page-flipping.
This commit is contained in:
@@ -356,6 +356,35 @@ void EpubReaderActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Idle glyph prewarm for the likely next page (currentPage + 1). The scan
|
||||||
|
// pass draws nothing (FCM scan mode suppresses pixels), so the displayed
|
||||||
|
// framebuffer is untouched; endScanAndPrewarm loads only glyphs not already
|
||||||
|
// cached. Debounced past rapid page-flipping, one attempt per position, and
|
||||||
|
// deferred while a render/build owns the CPU or the heap is at the render
|
||||||
|
// floor. Cross-chapter prewarm is deliberately out of scope (next spine's
|
||||||
|
// section isn't loaded).
|
||||||
|
constexpr unsigned long IDLE_PREWARM_DEBOUNCE_MS = 400;
|
||||||
|
if (section && !section->isBuilding() && !RenderLock::peek() && renderer.hasFrameBuffer() &&
|
||||||
|
lastRenderCompleteMs != 0 && millis() - lastRenderCompleteMs > IDLE_PREWARM_DEBOUNCE_MS &&
|
||||||
|
ESP.getFreeHeap() > RENDER_MIN_FREE_HEAP &&
|
||||||
|
(idlePrewarmSpine != currentSpineIndex || idlePrewarmPage != section->currentPage)) {
|
||||||
|
idlePrewarmSpine = currentSpineIndex;
|
||||||
|
idlePrewarmPage = section->currentPage;
|
||||||
|
const int nextPage = section->currentPage + 1;
|
||||||
|
if (nextPage < static_cast<int>(section->pageCount)) {
|
||||||
|
RenderLock lock; // the page table must not change under the scan
|
||||||
|
if (const auto p = section->loadPage(nextPage)) {
|
||||||
|
if (auto* fcm = renderer.getFontCacheManager()) {
|
||||||
|
const auto t0 = millis();
|
||||||
|
auto scope = fcm->createPrewarmScope();
|
||||||
|
p->render(renderer, SETTINGS.getReaderFontId(), 0, 0); // scan only, no pixels
|
||||||
|
scope.endScanAndPrewarm();
|
||||||
|
LOG_DBG("ERS", "Idle prewarm: page %d in %lums", nextPage, millis() - t0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lazily resume a partial's extension build once the reader nears its watermark. Far from
|
// Lazily resume a partial's extension build once the reader nears its watermark. Far from
|
||||||
// it the rebuild is all cost (whole-chapter re-layout from page 0) and no benefit this
|
// it the rebuild is all cost (whole-chapter re-layout from page 0) and no benefit this
|
||||||
// session, so reopening a partial deliberately does NOT start it (see the deferral in
|
// session, so reopening a partial deliberately does NOT start it (see the deferral in
|
||||||
@@ -1468,6 +1497,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
|||||||
const auto start = millis();
|
const auto start = millis();
|
||||||
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
||||||
|
lastRenderCompleteMs = millis();
|
||||||
// Fragmentation tracker: free vs largest block after every page. A falling
|
// Fragmentation tracker: free vs largest block after every page. A falling
|
||||||
// maxAlloc/free ratio across pages points at whichever allocation pattern the
|
// maxAlloc/free ratio across pages points at whichever allocation pattern the
|
||||||
// preceding lines show (mini rebuilds, kern reloads, BLE churn).
|
// preceding lines show (mini rebuilds, kern reloads, BLE churn).
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ class EpubReaderActivity final : public Activity {
|
|||||||
// to the chapter/book title the moment the connection completes (and
|
// to the chapter/book title the moment the connection completes (and
|
||||||
// returns on disconnect) instead of waiting for the next page turn.
|
// returns on disconnect) instead of waiting for the next page turn.
|
||||||
bool statusBarBleConnected = false;
|
bool statusBarBleConnected = false;
|
||||||
|
// Idle-time glyph prewarm: after a page settles, scan the LIKELY next page
|
||||||
|
// (scan mode draws nothing) and load its missing glyphs from SD during idle,
|
||||||
|
// so the next turn's in-render prewarm is a cache hit instead of ~100 ms of
|
||||||
|
// SD reads on the page-turn critical path. One attempt per position.
|
||||||
|
int idlePrewarmSpine = -1;
|
||||||
|
int idlePrewarmPage = -1;
|
||||||
|
unsigned long lastRenderCompleteMs = 0;
|
||||||
bool bookmarkRemoved = false; // true when last toggle removed (controls popup text)
|
bool bookmarkRemoved = false; // true when last toggle removed (controls popup text)
|
||||||
std::vector<BookmarkEntry> cachedBookmarks;
|
std::vector<BookmarkEntry> cachedBookmarks;
|
||||||
// Tracks whether this book is currently removed from Recent Books by the
|
// Tracks whether this book is currently removed from Recent Books by the
|
||||||
|
|||||||
Reference in New Issue
Block a user