Merge pull request #255 from jpirnay/reorder-image-handling

refactor: Reorder image handling to not compete for memory
This commit is contained in:
jpirnay
2026-05-22 14:18:06 +02:00
committed by GitHub
6 changed files with 60 additions and 9 deletions
+15
View File
@@ -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<const PageImage&>(*element).getImageBlock();
if (ib.wouldShowPlaceholder(forceLoadLargeImages)) continue;
if (ib.hasPixelCache()) continue;
static_cast<PageImage&>(*element).renderWithForceLoad(renderer, xOffset, yOffset, forceLoadLargeImages);
}
}
bool Page::hasPlaceholderImages(const bool forceLoadLargeImages) const {
for (const auto& el : elements) {
if (el->getTag() == TAG_PageImage) {
+6
View File
@@ -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;
+5
View File
@@ -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;
+4
View File
@@ -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; }
@@ -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.
+18 -3
View File
@@ -1969,22 +1969,37 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> 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<<N means fragmentation.
LOG_DBG("ERS", "Heap: before=%lu (contig=%lu) after=%lu (contig=%lu) delta=%ld", heapBefore, contigBefore, heapAfter,
contigAfter, (int32_t)heapAfter - (int32_t)heapBefore);
logReaderMemSnapshot("prewarm_end");
const bool aaConfigured = SETTINGS.textAntiAliasing;