From 60079923d910f402a975e9f68087c2fe31a6dd0d Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 19 May 2026 10:45:12 +0200 Subject: [PATCH] Prerender the next page --- src/activities/reader/EpubReaderActivity.cpp | 156 ++++++++++++++++++- src/activities/reader/EpubReaderActivity.h | 24 +++ 2 files changed, 178 insertions(+), 2 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 3f790fdb..c9c092c7 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -1516,12 +1516,25 @@ bool EpubReaderActivity::stepPageState(const bool isForwardTurn) { } void EpubReaderActivity::pageTurn(bool isForwardTurn) { + if (isForwardTurn && section && preRenderedPage.ready && preRenderedPage.spineIndex == currentSpineIndex && + preRenderedPage.pageIndex == section->currentPage + 1) { + // Fast path: the frame buffer already holds the next page content. Advance state here on the + // loop task, then hand off to render() via usePreRenderedBuffer — all display work (status + // bar, flush, AA pass) stays on the render task where it belongs. No RenderLock acquired here. + section->currentPage = preRenderedPage.pageIndex; + preRenderedPage.ready = false; + usePreRenderedBuffer = true; + sessionPagesAdvanced++; + lastPageTurnTime = millis(); + requestUpdate(); + return; + } + if (!stepPageState(isForwardTurn)) { return; } - // Track real progress within this session so auto-push-on-close can ignore brief - // book inspections. Counts both directions — the user is engaging with the book either way. sessionPagesAdvanced++; + preRenderedPage.ready = false; requestUpdate(); } @@ -1624,6 +1637,67 @@ void EpubReaderActivity::render(RenderLock&& lock) { lastRenderStats.largestFreeBlockBefore = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); showTruncatedSectionHintThisRender = false; + // Capture and clear all pre-render flags before any state checks. + // - isPreRenderPass: render next page content only (no status bar, no flush). + // - isBufferDisplayPass: frame buffer already has content; just add status bar and flush. + // - anything else: normal full render; discard any stale pre-render state. + const bool isPreRenderPass = pendingPreRender; + const bool isBufferDisplayPass = usePreRenderedBuffer; + pendingPreRender = false; + usePreRenderedBuffer = false; + if (!isPreRenderPass && !isBufferDisplayPass) { + preRenderedPage.ready = false; + } + + // Fast-display pass: frame buffer holds pre-rendered content; superimpose live status bar, + // flush to display, and run the AA pass — all on the render task with no SD font re-read. + if (isBufferDisplayPass) { + if (section) { + auto p = section->loadPageFromSectionFile(); + if (p && !p->hasImages()) { + currentPageFootnotes = std::move(p->footnotes); + displayPreRenderedPage(*p, orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); + + pendingProgressSave.spineIndex = currentSpineIndex; + pendingProgressSave.page = section->currentPage; + pendingProgressSave.pageCount = section->pageCount; + pendingProgressSave.pending.store(true, std::memory_order_release); + + if (section->currentPage + 1 < section->pageCount) { + pendingPreRender = true; + requestUpdate(); + } + return; + } + // Page load failed or was an image page — fall through to full render. + } + } + + // Pre-render pass: render next page content into the frame buffer (no status bar, no flush). + if (isPreRenderPass) { + if (section && !preRenderedPage.ready) { + const int nextPage = section->currentPage + 1; + if (nextPage < section->pageCount) { + const uint32_t freeHeap = esp_get_free_heap_size(); + const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); + if (freeHeap >= SILENT_INDEX_MIN_FREE_HEAP_BYTES && contigHeap >= SILENT_INDEX_MIN_CONTIG_HEAP_BYTES) { + const int savedPage = section->currentPage; + section->currentPage = nextPage; + auto p = section->loadPageFromSectionFile(); + section->currentPage = savedPage; + if (p && !p->hasImages()) { + section->currentPage = nextPage; + renderPageContentOnly(*p, orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); + section->currentPage = savedPage; + preRenderedPage = {true, currentSpineIndex, nextPage}; + LOG_DBG("ERS", "Pre-rendered page %d/%d", nextPage, section->pageCount - 1); + } + } + } + } + return; + } + if (!section) { if (currentSpineIndex < 0 || currentSpineIndex >= spineCount) { LOG_ERR("ERS", "Render rejected invalid spine index %d (valid 0..%d)", currentSpineIndex, spineCount - 1); @@ -1774,6 +1848,13 @@ void EpubReaderActivity::render(RenderLock&& lock) { pendingScreenshot = false; ScreenshotUtil::takeScreenshot(renderer); } + + // Schedule a pre-render of the next page into the frame buffer so the next forward + // page turn can skip the render pass and go straight to displayBuffer(). + if (!preRenderedPage.ready && section->currentPage + 1 < section->pageCount) { + pendingPreRender = true; + requestUpdate(); + } } void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportWidth, const uint16_t viewportHeight) { @@ -2055,6 +2136,77 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or } } +void EpubReaderActivity::renderPageContentOnly(Page& page, const int orientedMarginTop, const int orientedMarginRight, + const int orientedMarginBottom, const int orientedMarginLeft) { + auto* fcm = renderer.getFontCacheManager(); + fcm->resetStats(); + + const int viewportHeight = std::max(0, renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom); + const int contentTop = orientedMarginTop + getImageOnlyPageYOffset(page, viewportHeight); + + auto scope = fcm->createPrewarmScope(); + page.renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop); + scope.endScanAndPrewarm(); + + renderer.clearScreen(); + page.render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop); + // Status bar intentionally omitted — superimposed at display time with live values. +} + +void EpubReaderActivity::displayPreRenderedPage(Page& page, const int orientedMarginTop, const int orientedMarginRight, + const int orientedMarginBottom, const int orientedMarginLeft) { + const int viewportHeight = std::max(0, renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom); + const int contentTop = orientedMarginTop + getImageOnlyPageYOffset(page, viewportHeight); + + renderStatusBar(); + + // Pre-rendered pages are text-only (image pages are excluded from pre-rendering), so we + // always go through the normal refresh cycle — no imagePageWithAA or forceHalfRefresh paths. + const bool forceHalfRefreshThisPage = pendingHalfRefreshAfterImagePage && SETTINGS.halfRefreshAfterImagePage; + pendingHalfRefreshAfterImagePage = false; + if (forceHalfRefreshThisPage) { + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); + } else { + ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh); + } + + // Grayscale AA pass (same as normal render — BW snapshot, gray LSB+MSB, restore). + const bool aaConfigured = SETTINGS.textAntiAliasing && !antiAliasingSuspendedLowMemory; + if (aaConfigured) { + const uint32_t freeHeap = esp_get_free_heap_size(); + const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); + if (freeHeap >= BW_SNAPSHOT_MIN_FREE_HEAP_BYTES && contigHeap >= BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES) { + const int contentLeft = orientedMarginLeft; + const int contentRight = std::max(contentLeft, renderer.getScreenWidth() - orientedMarginRight); + const int contentBottom = std::max(contentTop, renderer.getScreenHeight() - orientedMarginBottom); + int bandTop = 0; + int bandBottom = std::max(0, contentBottom - contentTop); + if (!computePageDynamicYBand(page, renderer, getEffectiveReaderFontId(), bandBottom, &bandTop, &bandBottom)) { + bandTop = 0; + bandBottom = std::max(0, contentBottom - contentTop); + } + const int snapshotTop = contentTop + bandTop; + const int snapshotHeight = std::max(0, bandBottom - bandTop); + if (renderer.storeBwBufferRect(contentLeft, snapshotTop, contentRight - contentLeft, snapshotHeight)) { + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); + page.renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop); + renderer.copyGrayscaleLsbBuffers(); + + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); + page.renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop); + renderer.copyGrayscaleMsbBuffers(); + + renderer.displayGrayBuffer(); + renderer.setRenderMode(GfxRenderer::BW); + renderer.restoreBwBuffer(); + } + } + } +} + void EpubReaderActivity::renderStatusBar() const { // Calculate progress in book const int currentPage = section->currentPage + 1; diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index d242046a..e2f4273d 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -189,6 +189,20 @@ class EpubReaderActivity final : public Activity { uint32_t maxFreeHeapAfter = 0; }; LastRenderStats lastRenderStats; + // Pre-rendered next page: frame buffer holds page content (no status bar) ready to display. + // Set by the pre-render pass in render(); consumed and cleared by the fast path in pageTurn(). + // Invalidated (ready=false) on any navigation that is not a simple forward page turn. + struct PreRenderedPage { + bool ready = false; + int spineIndex = -1; + int pageIndex = -1; + }; + PreRenderedPage preRenderedPage; + // Set by render() after a normal page render to request a pre-render of the next page. + bool pendingPreRender = false; + // Set by pageTurn() fast path to tell render() the frame buffer already holds the next page + // content and only the status bar + display flush are needed. + bool usePreRenderedBuffer = false; // Progress save is posted by render() and consumed by loop() to keep SD I/O off the render task. // render() writes spineIndex/page/pageCount then sets pending with release semantics so loop() // sees a coherent snapshot when it observes pending==true via acquire. @@ -232,6 +246,16 @@ class EpubReaderActivity final : public Activity { void renderContents(std::unique_ptr page, int orientedMarginTop, int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft); + // Renders page content into the frame buffer (prewarm + BW pass) without drawing the status bar + // or flushing to the display. Used by the pre-render pass so the status bar can be superimposed + // at display time with live values (clock, battery). + void renderPageContentOnly(Page& page, int orientedMarginTop, int orientedMarginRight, int orientedMarginBottom, + int orientedMarginLeft); + // Draws the status bar over the current frame buffer and flushes to the display. + // Handles the refresh cycle and grayscale AA pass. page must be the same page + // that was last rendered into the buffer (needed for image AA re-render). + void displayPreRenderedPage(Page& page, int orientedMarginTop, int orientedMarginRight, int orientedMarginBottom, + int orientedMarginLeft); void renderStatusBar() const; void silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight); void saveProgress(int spineIndex, int currentPage, int pageCount);