fix: Replace full-image cache buffer with streaming band buffer to reduce memory usage (#2230)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include "ImageBlock.h"
|
||||
|
||||
#include <FontCacheManager.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
@@ -55,10 +56,22 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
|
||||
LOG_DBG("IMG", "Loading from cache: %s (%dx%d)", cachePath.c_str(), cachedWidth, cachedHeight);
|
||||
|
||||
// Read and render row by row to minimize memory usage
|
||||
// Read several rows per SD access. A full-page image is re-rendered on every
|
||||
// grayscale strip pass (~14x per page), and a one-row-per-read loop here means
|
||||
// cachedHeight (~728) tiny reads through the storage mutex + SdFat each time —
|
||||
// the dominant cost of displaying an image page. Batching rows into a ~4KB
|
||||
// buffer cuts that to ~20 reads per pass without holding the whole image.
|
||||
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
uint8_t* rowBuffer = (uint8_t*)malloc(bytesPerRow);
|
||||
if (!rowBuffer) {
|
||||
int rowsPerRead = 4096 / bytesPerRow;
|
||||
if (rowsPerRead < 1) rowsPerRead = 1;
|
||||
if (rowsPerRead > cachedHeight) rowsPerRead = cachedHeight;
|
||||
uint8_t* readBuffer = (uint8_t*)malloc((size_t)rowsPerRead * bytesPerRow);
|
||||
if (!readBuffer) {
|
||||
// Fall back to a single-row buffer under memory pressure.
|
||||
rowsPerRead = 1;
|
||||
readBuffer = (uint8_t*)malloc(bytesPerRow);
|
||||
}
|
||||
if (!readBuffer) {
|
||||
LOG_ERR("IMG", "Failed to allocate row buffer");
|
||||
return false;
|
||||
}
|
||||
@@ -66,16 +79,31 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
DirectPixelWriter pw;
|
||||
pw.init(renderer);
|
||||
|
||||
int rowsInBuffer = 0;
|
||||
int bufferRow = 0;
|
||||
for (int row = 0; row < cachedHeight; row++) {
|
||||
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
|
||||
LOG_ERR("IMG", "Cache read error at row %d", row);
|
||||
free(rowBuffer);
|
||||
return false;
|
||||
if (bufferRow >= rowsInBuffer) {
|
||||
const int toRead = (cachedHeight - row < rowsPerRead) ? (cachedHeight - row) : rowsPerRead;
|
||||
const size_t bytes = (size_t)toRead * bytesPerRow;
|
||||
if (cacheFile.read(readBuffer, bytes) != static_cast<int>(bytes)) {
|
||||
LOG_ERR("IMG", "Cache read error at row %d", row);
|
||||
free(readBuffer);
|
||||
return false;
|
||||
}
|
||||
rowsInBuffer = toRead;
|
||||
bufferRow = 0;
|
||||
}
|
||||
|
||||
const uint8_t* rowBuffer = readBuffer + (size_t)bufferRow * bytesPerRow;
|
||||
bufferRow++;
|
||||
|
||||
const int destY = y + row;
|
||||
pw.beginRow(destY);
|
||||
for (int col = 0; col < cachedWidth; col++) {
|
||||
// On a grayscale strip pass only a narrow column window of the image is in
|
||||
// the active band; skip the rest instead of unpacking+clipping every pixel.
|
||||
int colStart, colEnd;
|
||||
pw.bandColRange(x, cachedWidth, colStart, colEnd);
|
||||
for (int col = colStart; col < colEnd; col++) {
|
||||
const int byteIdx = col >> 2; // col / 4
|
||||
const int bitShift = 6 - (col & 3) * 2; // MSB first within byte
|
||||
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||
@@ -84,7 +112,7 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
}
|
||||
}
|
||||
|
||||
free(rowBuffer);
|
||||
free(readBuffer);
|
||||
LOG_DBG("IMG", "Cache render complete");
|
||||
return true;
|
||||
}
|
||||
@@ -92,6 +120,14 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
} // namespace
|
||||
|
||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
// The font-prewarm scan pass only accumulates glyphs; an image contributes
|
||||
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode
|
||||
// suppression, so it would otherwise do a full (discarded) cache render every
|
||||
// page view. Skip it here. The image still draws in the real BW/grayscale
|
||||
// passes; on first view this just moves the one-time decode to the BW pass.
|
||||
FontCacheManager* fcm = renderer.getFontCacheManager();
|
||||
if (fcm && fcm->isScanning()) return;
|
||||
|
||||
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
|
||||
Reference in New Issue
Block a user