diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index c72b3fca..05ae5234 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -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(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 // 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 @@ -1468,6 +1497,7 @@ 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); + lastRenderCompleteMs = millis(); // Fragmentation tracker: free vs largest block after every page. A falling // maxAlloc/free ratio across pages points at whichever allocation pattern the // preceding lines show (mini rebuilds, kern reloads, BLE churn). diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 3fff2364..dfeeb40a 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -46,6 +46,13 @@ class EpubReaderActivity final : public Activity { // to the chapter/book title the moment the connection completes (and // returns on disconnect) instead of waiting for the next page turn. 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) std::vector cachedBookmarks; // Tracks whether this book is currently removed from Recent Books by the