From 010f94f25d19addeaf85a0dfbf45bae6400f481a Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 19 May 2026 10:11:57 +0200 Subject: [PATCH 1/3] Fix icon position --- src/components/themes/lyra/LyraCarouselTheme.cpp | 3 ++- src/components/themes/lyra/LyraTheme.cpp | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/themes/lyra/LyraCarouselTheme.cpp b/src/components/themes/lyra/LyraCarouselTheme.cpp index ba7bfe93..603f7afd 100644 --- a/src/components/themes/lyra/LyraCarouselTheme.cpp +++ b/src/components/themes/lyra/LyraCarouselTheme.cpp @@ -586,14 +586,15 @@ void LyraCarouselTheme::drawList(const GfxRenderer& renderer, Rect rect, int ite int textX = rect.x + LyraCarouselMetrics::values.contentSidePadding + hPad; int textWidth = contentWidth - LyraCarouselMetrics::values.contentSidePadding * 2 - hPad * 2; int iconSize = 0; + int iconY = 0; if (rowIcon != nullptr) { iconSize = (rowSubtitle != nullptr) ? mainMenuIconSz : listIconSz; + iconY = (rowHeight - iconSize) / 2; textX += iconSize + hPad; textWidth -= iconSize + hPad; } const auto pageStartIndex = selectedIndex / pageItems * pageItems; - const int iconY = (rowSubtitle != nullptr) ? 16 : 10; for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) { const int itemY = rect.y + (i % pageItems) * rowHeight; const bool sel = (i == selectedIndex); diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index c233137d..99b6c3ff 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -364,16 +364,17 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int textX = rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection; int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2; - int iconSize; + int iconSize = 0; + int iconY = 0; if (rowIcon != nullptr) { iconSize = (rowSubtitle != nullptr) ? mainMenuIconSize : listIconSize; + iconY = (rowHeight - iconSize) / 2; textX += iconSize + hPaddingInSelection; textWidth -= iconSize + hPaddingInSelection; } // Draw all items const auto pageStartIndex = selectedIndex / pageItems * pageItems; - int iconY = (rowSubtitle != nullptr) ? 16 : 10; for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) { const int itemY = rect.y + (i % pageItems) * rowHeight; int rowTextWidth = textWidth; From 60079923d910f402a975e9f68087c2fe31a6dd0d Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 19 May 2026 10:45:12 +0200 Subject: [PATCH 2/3] 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); From 85d2a105b26232f9cff9a2733f0dabc8f7c4b7d1 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 19 May 2026 11:15:46 +0200 Subject: [PATCH 3/3] cppcheck --- src/activities/reader/EpubReaderActivity.cpp | 6 ++++-- src/activities/reader/EpubReaderActivity.h | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index c9c092c7..3e76a2c3 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -2136,7 +2136,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or } } -void EpubReaderActivity::renderPageContentOnly(Page& page, const int orientedMarginTop, const int orientedMarginRight, +void EpubReaderActivity::renderPageContentOnly(const Page& page, const int orientedMarginTop, + const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { auto* fcm = renderer.getFontCacheManager(); fcm->resetStats(); @@ -2153,7 +2154,8 @@ void EpubReaderActivity::renderPageContentOnly(Page& page, const int orientedMar // Status bar intentionally omitted — superimposed at display time with live values. } -void EpubReaderActivity::displayPreRenderedPage(Page& page, const int orientedMarginTop, const int orientedMarginRight, +void EpubReaderActivity::displayPreRenderedPage(const 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); diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index e2f4273d..e788df88 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -249,12 +249,12 @@ class EpubReaderActivity final : public Activity { // 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, + void renderPageContentOnly(const 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, + void displayPreRenderedPage(const Page& page, int orientedMarginTop, int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft); void renderStatusBar() const; void silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight);