Merge pull request #255 from jpirnay/reorder-image-handling
refactor: Reorder image handling to not compete for memory
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user