diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 15bcf10e..d72e7566 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -215,6 +215,21 @@ void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, co } } +void Page::warmImageCaches(GfxRenderer& renderer, const int xOffset, const int yOffset, + const bool forceLoadLargeImages) const { + // Only do the costly decode pass when there's at least one image that would + // actually require a PNG/JPG decoder allocation. Cached and placeholder paths + // do not need the contiguous heap headroom, so skipping the iteration entirely + // saves the no-op overhead on text-only pages (the common case). + for (auto& element : elements) { + if (element->getTag() != TAG_PageImage) continue; + const auto& ib = static_cast(*element).getImageBlock(); + if (ib.wouldShowPlaceholder(forceLoadLargeImages)) continue; + if (ib.hasPixelCache()) continue; + static_cast(*element).renderWithForceLoad(renderer, xOffset, yOffset, forceLoadLargeImages); + } +} + bool Page::hasPlaceholderImages(const bool forceLoadLargeImages) const { for (const auto& el : elements) { if (el->getTag() == TAG_PageImage) { diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 54985193..822adb0a 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -131,6 +131,12 @@ class Page { void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool forceLoadLargeImages = true) const; void renderTextOnly(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; + // Decode any missing .pxc pixel caches for images on this page. Called before the + // BW render so the large (~60 KB contiguous) PNG decoder allocation runs while heap + // contig is at its peak — before font prewarm and BW backup chunks fragment it. + // Writes pixels to the framebuffer as a side effect (decoder requirement); callers + // must clearScreen() afterward if the framebuffer needs to be clean. + void warmImageCaches(GfxRenderer& renderer, int xOffset, int yOffset, bool forceLoadLargeImages) const; bool hasPlaceholderImages(bool forceLoadLargeImages) const; bool allImagesArePlaceholders(bool forceLoadLargeImages) const; bool serialize(FsFile& file) const; diff --git a/lib/Epub/Epub/blocks/ImageBlock.cpp b/lib/Epub/Epub/blocks/ImageBlock.cpp index 17e37da8..cba35dbe 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.cpp +++ b/lib/Epub/Epub/blocks/ImageBlock.cpp @@ -116,6 +116,11 @@ bool ImageBlock::isLargeImage() const { return largeImageCached == 1; } +bool ImageBlock::hasPixelCache() const { + const ImageDitherMode ditherMode = imageDitherModeFromSetting(SETTINGS.imageDithering); + return Storage.exists(getCachePath(imagePath, ditherMode).c_str()); +} + bool ImageBlock::wouldShowPlaceholder(bool forceLoad) const { if (forceLoad) return false; if (!isLargeImage()) return false; diff --git a/lib/Epub/Epub/blocks/ImageBlock.h b/lib/Epub/Epub/blocks/ImageBlock.h index 7145adc0..7cf93213 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.h +++ b/lib/Epub/Epub/blocks/ImageBlock.h @@ -31,6 +31,10 @@ class ImageBlock final : public Block { // False when: forceLoad is true, image is not large, or pixel cache already exists. bool wouldShowPlaceholder(bool forceLoad) const; + // True when the .pxc pixel cache file exists for this image at the current + // dither setting. Used by warm-cache paths to skip already-cached images. + bool hasPixelCache() const; + BlockType getType() override { return IMAGE_BLOCK; } bool isEmpty() override { return false; } diff --git a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp index b949a33b..440bfd67 100644 --- a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp @@ -171,12 +171,18 @@ int32_t pngSeekWithHandle(PNGFILE* pFile, int32_t pos) { return f->seek(pos); } -// The PNG decoder (PNGdec) is ~42 KB due to internal zlib decompression buffers. -// We heap-allocate it on demand rather than using a static instance, so this memory -// is only consumed while actually decoding/querying PNG images. This is critical on -// the ESP32-C3 where total RAM is ~320 KB. -constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead -constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom +// The PNG decoder (PNGdec) is large due to internal zlib decompression buffers +// (~40 KB ucZLIB + 16 KB ucPixels at PNG_MAX_BUFFERED_PIXELS=16416 + smaller +// fields ≈ 60 KB). We heap-allocate it on demand rather than using a static +// instance, so this memory is only consumed while actually decoding/querying +// PNG images. This is critical on the ESP32-C3 where total RAM is ~320 KB. +// Use sizeof(PNG) so the precheck stays accurate if PNG_MAX_BUFFERED_PIXELS +// or other PNGdec buffers are resized. +constexpr size_t PNG_DECODER_APPROX_SIZE = sizeof(PNG); +// Headroom covers heap fragmentation: free heap is the *sum* of all free +// blocks but `new` needs a single contiguous block. 32 KB headroom on a +// ~60 KB allocation has historically been enough on this device. +constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 32 * 1024; // PNGdec keeps TWO scanlines in its internal ucPixels buffer (current + previous) // and each scanline includes a leading filter byte. diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 3866691a..38c0528e 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -1969,22 +1969,37 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or logReaderMemSnapshot("render_start"); auto* fcm = renderer.getFontCacheManager(); fcm->resetStats(); - logReaderMemSnapshot("prewarm_begin"); const int viewportHeight = std::max(0, renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom); const int contentTop = orientedMarginTop + getImageOnlyPageYOffset(*page, viewportHeight); + // Warm any missing image pixel caches BEFORE font prewarm and BW backup chunks + // reduce heap contig below the ~60 KB the PNG/JPG decoder needs. The decode + // writes pixels into the framebuffer as a side effect, so we reclear before + // the real BW render begins. Skips when no decode is needed (all images cached + // or the page is text-only). Mirrors the effectiveForceLoad rule used by the + // BW render below so placeholder logic is identical. + const bool warmForceLoad = forceLoadLargeImages || !SETTINGS.largeImagePlaceholder; + page->warmImageCaches(renderer, orientedMarginLeft, contentTop, warmForceLoad); + renderer.clearScreen(); + + logReaderMemSnapshot("prewarm_begin"); + // Font prewarm: scan pass accumulates text, then prewarm, then real render const uint32_t heapBefore = esp_get_free_heap_size(); + const uint32_t contigBefore = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); auto scope = fcm->createPrewarmScope(); page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop); // scan pass scope.endScanAndPrewarm(); const uint32_t heapAfter = esp_get_free_heap_size(); + const uint32_t contigAfter = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); 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); + // contig= reports the largest contiguous block, which is what large allocations + // (e.g. 60 KB PNG decoder) actually need. free=N with contig<