From a14c8e762dee82908fa4588f06928b208d270d2f Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 18 May 2026 19:41:02 -0700 Subject: [PATCH] perf: shrink HomeActivity cover cache from 48KB framebuffer to 16KB region (#2035) On-device repro showed the cover snapshot pinning ~52KB of contiguous heap (cloning the full 48KB framebuffer with malloc overhead). MaxAlloc on Home was 61KB; nothing was leaving headroom for HTTPS, which needs 30-50KB contiguous for the mbedTLS handshake. Add region-aware framebuffer helpers to GfxRenderer that translate a logical rect through rotateCoordinates and copy only the byte range that contains the rotated rect. HomeActivity records the tile rect it passes to drawRecentBookCover and caches only that subregion. Measured on device (X3, Portrait): Idle on Home | Free 102K -> 139K | MaxAlloc 61K -> 115K Mid-EPUB-read | Free 81K -> 134K | MaxAlloc 70K -> 115K Cover cache | ~52K -> ~16K (per allocation) Works in all four orientations because the bounds helper samples the four logical corners through the existing rotation, so the cached byte range always covers the pixels the theme could have drawn into. Savings will vary with theme, but should be significant across all. --- lib/GfxRenderer/GfxRenderer.cpp | 83 ++++++++++++++++++++++++++++ lib/GfxRenderer/GfxRenderer.h | 12 ++++ src/activities/home/HomeActivity.cpp | 48 ++++++++-------- src/activities/home/HomeActivity.h | 8 +++ 4 files changed, 128 insertions(+), 23 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 993b2cb7..57a8a937 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -1094,6 +1094,89 @@ int GfxRenderer::getScreenHeight() const { return panelWidth; } +// Translate a logical rect through rotateCoordinates and take the bounding +// box of its four corners on the physical panel. Output coords are inclusive +// and clamped. Returns false if the rect ends up fully off-panel. +static bool logicalRectToPhysicalBounds(GfxRenderer::Orientation orientation, int lx, int ly, int lw, int lh, + uint16_t panelWidth, uint16_t panelHeight, int* outX0, int* outY0, int* outX1, + int* outY1) { + if (lw <= 0 || lh <= 0) return false; + int minX = INT32_MAX; + int minY = INT32_MAX; + int maxX = INT32_MIN; + int maxY = INT32_MIN; + const int corners[4][2] = {{lx, ly}, {lx + lw - 1, ly}, {lx, ly + lh - 1}, {lx + lw - 1, ly + lh - 1}}; + for (auto& c : corners) { + int phyX; + int phyY; + rotateCoordinates(orientation, c[0], c[1], &phyX, &phyY, panelWidth, panelHeight); + if (phyX < minX) minX = phyX; + if (phyY < minY) minY = phyY; + if (phyX > maxX) maxX = phyX; + if (phyY > maxY) maxY = phyY; + } + if (minX < 0) minX = 0; + if (minY < 0) minY = 0; + if (maxX >= panelWidth) maxX = panelWidth - 1; + if (maxY >= panelHeight) maxY = panelHeight - 1; + if (minX > maxX || minY > maxY) return false; + *outX0 = minX; + *outY0 = minY; + *outX1 = maxX; + *outY1 = maxY; + return true; +} + +size_t GfxRenderer::getRegionByteSize(int lx, int ly, int lw, int lh) const { + int x0, y0, x1, y1; + if (!logicalRectToPhysicalBounds(orientation, lx, ly, lw, lh, panelWidth, panelHeight, &x0, &y0, &x1, &y1)) { + return 0; + } + // x bounds are in pixels; widen to byte boundaries on either side so per-row + // memcpy stays byte-aligned even when the logical rect doesn't. + const int byteX0 = x0 / 8; + const int byteX1 = x1 / 8; + const int bytesPerRow = byteX1 - byteX0 + 1; + const int rowCount = y1 - y0 + 1; + return static_cast(bytesPerRow) * static_cast(rowCount); +} + +bool GfxRenderer::copyRegionToBuffer(int lx, int ly, int lw, int lh, uint8_t* buf, size_t bufSize) const { + int x0, y0, x1, y1; + if (!logicalRectToPhysicalBounds(orientation, lx, ly, lw, lh, panelWidth, panelHeight, &x0, &y0, &x1, &y1)) { + return false; + } + const int byteX0 = x0 / 8; + const int byteX1 = x1 / 8; + const int bytesPerRow = byteX1 - byteX0 + 1; + const int rowCount = y1 - y0 + 1; + const size_t needed = static_cast(bytesPerRow) * static_cast(rowCount); + if (bufSize < needed || !frameBuffer || !buf) return false; + for (int row = 0; row < rowCount; row++) { + const uint8_t* src = frameBuffer + (y0 + row) * panelWidthBytes + byteX0; + memcpy(buf + row * bytesPerRow, src, bytesPerRow); + } + return true; +} + +bool GfxRenderer::copyBufferToRegion(int lx, int ly, int lw, int lh, const uint8_t* buf, size_t bufSize) const { + int x0, y0, x1, y1; + if (!logicalRectToPhysicalBounds(orientation, lx, ly, lw, lh, panelWidth, panelHeight, &x0, &y0, &x1, &y1)) { + return false; + } + const int byteX0 = x0 / 8; + const int byteX1 = x1 / 8; + const int bytesPerRow = byteX1 - byteX0 + 1; + const int rowCount = y1 - y0 + 1; + const size_t needed = static_cast(bytesPerRow) * static_cast(rowCount); + if (bufSize < needed || !frameBuffer || !buf) return false; + for (int row = 0; row < rowCount; row++) { + uint8_t* dst = frameBuffer + (y0 + row) * panelWidthBytes + byteX0; + memcpy(dst, buf + row * bytesPerRow, bytesPerRow); + } + return true; +} + int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style style) const { // Advance table fast-path for SD card fonts during layout auto sdIt = sdCardFonts_.find(fontId); diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 21a9cad9..a97b2b9f 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -185,4 +185,16 @@ class GfxRenderer { uint16_t getDisplayWidth() const { return panelWidth; } uint16_t getDisplayHeight() const { return panelHeight; } uint16_t getDisplayWidthBytes() const { return panelWidthBytes; } + + // Region cache: take a logical (orientation-aware) rect, hit the framebuffer + // bytes that the rect can have touched, and pump them in or out of a caller- + // supplied buffer. Used by HomeActivity to snapshot just the cover tile + // (~16 KB in Portrait) instead of cloning the entire 48 KB framebuffer. + // + // getRegionByteSize: required buffer length for the rect at current orientation. + // copyRegionToBuffer / copyBufferToRegion: false if `bufSize` is smaller than that. + size_t getRegionByteSize(int logicalX, int logicalY, int logicalW, int logicalH) const; + bool copyRegionToBuffer(int logicalX, int logicalY, int logicalW, int logicalH, uint8_t* buf, size_t bufSize) const; + bool copyBufferToRegion(int logicalX, int logicalY, int logicalW, int logicalH, const uint8_t* buf, + size_t bufSize) const; }; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 76457491..7f643994 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -130,37 +130,30 @@ void HomeActivity::onExit() { } bool HomeActivity::storeCoverBuffer() { - uint8_t* frameBuffer = renderer.getFrameBuffer(); - if (!frameBuffer) { - return false; - } - - // Free any existing buffer first + // render() must have already set the cover rect; without it we'd be back to + // cloning the whole framebuffer. + if (coverRectW <= 0 || coverRectH <= 0) return false; freeCoverBuffer(); - - const size_t bufferSize = renderer.getBufferSize(); - coverBuffer = static_cast(malloc(bufferSize)); + const size_t needed = renderer.getRegionByteSize(coverRectX, coverRectY, coverRectW, coverRectH); + if (needed == 0) return false; + coverBuffer = static_cast(malloc(needed)); if (!coverBuffer) { + LOG_ERR("HOME", "OOM: cover buffer (%u bytes)", (unsigned)needed); + return false; + } + coverBufferSize = needed; + if (!renderer.copyRegionToBuffer(coverRectX, coverRectY, coverRectW, coverRectH, coverBuffer, coverBufferSize)) { + free(coverBuffer); + coverBuffer = nullptr; + coverBufferSize = 0; return false; } - - memcpy(coverBuffer, frameBuffer, bufferSize); return true; } bool HomeActivity::restoreCoverBuffer() { - if (!coverBuffer) { - return false; - } - - uint8_t* frameBuffer = renderer.getFrameBuffer(); - if (!frameBuffer) { - return false; - } - - const size_t bufferSize = renderer.getBufferSize(); - memcpy(frameBuffer, coverBuffer, bufferSize); - return true; + if (!coverBuffer || coverRectW <= 0 || coverRectH <= 0) return false; + return renderer.copyBufferToRegion(coverRectX, coverRectY, coverRectW, coverRectH, coverBuffer, coverBufferSize); } void HomeActivity::freeCoverBuffer() { @@ -168,6 +161,7 @@ void HomeActivity::freeCoverBuffer() { free(coverBuffer); coverBuffer = nullptr; } + coverBufferSize = 0; coverBufferStored = false; } @@ -221,6 +215,14 @@ void HomeActivity::render(RenderLock&&) { GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, metrics.homeContinueReadingInMenu && !recentBooks.empty() ? recentBooks[0].title.c_str() : nullptr); + // Record the tile rect so storeCoverBuffer (called from the theme) knows + // which sub-region of the framebuffer to snapshot. ~16 KB in Portrait + // instead of the 48 KB full framebuffer the previous bind captured. + coverRectX = 0; + coverRectY = metrics.homeTopPadding; + coverRectW = pageWidth; + coverRectH = metrics.homeCoverTileHeight; + GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight}, recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, std::bind(&HomeActivity::storeCoverBuffer, this)); diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index e3bfd4db..b1bd494d 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -19,6 +19,14 @@ class HomeActivity final : public Activity { bool coverRendered = false; // Track if cover has been rendered once bool coverBufferStored = false; // Track if cover buffer is stored uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image + size_t coverBufferSize = 0; // Bytes allocated to coverBuffer + // Logical rect last passed to drawRecentBookCover. The cover snapshot only + // needs to cover this region, not the entire framebuffer, so we cache the + // tile instead of all 48 KB. Set in render() before the call. + int coverRectX = 0; + int coverRectY = 0; + int coverRectW = 0; + int coverRectH = 0; std::vector recentBooks; void onSelectBook(const std::string& path); void onFileBrowserOpen();