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) {
|
||||
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(rowBuffer);
|
||||
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();
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <HalDisplay.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
// Direct framebuffer writer that eliminates per-pixel overhead from the image
|
||||
// rendering hot path. Pre-computes orientation transform as linear coefficients
|
||||
// and caches render-mode state so the inner loop is: one multiply, one add,
|
||||
@@ -99,6 +101,46 @@ struct DirectPixelWriter {
|
||||
rowPhyYBase = phyYBase + logicalY * phyYStepY;
|
||||
}
|
||||
|
||||
// For the current row (set via beginRow), narrow [colStart, colEnd) to the
|
||||
// columns whose pixels fall inside the active strip band. writePixel() would
|
||||
// clip the rest anyway, but on a strip pass that is most of a full-page image
|
||||
// (only ~one strip-height worth of columns survive in portrait); skipping them
|
||||
// here avoids the per-pixel unpack+transform entirely. For full-frame passes
|
||||
// (clipRows == panel height) the range is unchanged. xBase is the logical X of
|
||||
// column 0; the band test mirrors writePixel(): 0 <= phyY - originY < clipRows.
|
||||
inline void bandColRange(int xBase, int width, int& colStart, int& colEnd) const {
|
||||
// init() only ever sets phyYStepX to 0, +1, or -1; the +1/-1 solve below
|
||||
// relies on that.
|
||||
assert(phyYStepX == 0 || phyYStepX == 1 || phyYStepX == -1);
|
||||
colStart = 0;
|
||||
colEnd = width;
|
||||
if (phyYStepX == 0) {
|
||||
// phyY is constant across the row: the whole row is in-band or out.
|
||||
const int sy = rowPhyYBase - originY;
|
||||
if (static_cast<unsigned>(sy) >= static_cast<unsigned>(clipRows)) colEnd = 0;
|
||||
return;
|
||||
}
|
||||
// phyY = rowPhyYBase + logicalX * phyYStepX (phyYStepX is +1 or -1).
|
||||
// Solve originY <= phyY <= originY + clipRows - 1 for logicalX.
|
||||
const int loY = originY;
|
||||
const int hiY = originY + clipRows - 1;
|
||||
int xLo, xHi;
|
||||
if (phyYStepX > 0) {
|
||||
xLo = loY - rowPhyYBase;
|
||||
xHi = hiY - rowPhyYBase;
|
||||
} else {
|
||||
xLo = rowPhyYBase - hiY;
|
||||
xHi = rowPhyYBase - loY;
|
||||
}
|
||||
const int cs = xLo - xBase;
|
||||
const int ce = xHi - xBase + 1; // exclusive
|
||||
if (cs > colStart) colStart = cs;
|
||||
if (ce < colEnd) colEnd = ce;
|
||||
if (colStart < 0) colStart = 0;
|
||||
if (colEnd > width) colEnd = width;
|
||||
if (colStart > colEnd) colStart = colEnd;
|
||||
}
|
||||
|
||||
// Write a single 2-bit dithered pixel value to the framebuffer.
|
||||
// Must be called after beginRow() for the current row.
|
||||
// No bounds checking — caller guarantees coordinates are valid.
|
||||
@@ -147,27 +189,42 @@ struct DirectPixelWriter {
|
||||
// Direct cache writer that eliminates per-pixel overhead from PixelCache::setPixel().
|
||||
// Pre-computes row pointer so the inner loop is just byte index + bit manipulation.
|
||||
//
|
||||
// Caller guarantees coordinates are within cache bounds.
|
||||
// The cache buffer is a small streaming band (e.g. 16 rows), not the full image,
|
||||
// so a band-relative row/column that lands outside it would corrupt adjacent
|
||||
// heap. This writer therefore bounds-checks every access: beginRow() invalidates
|
||||
// the row when it falls outside the band, and writePixel() drops out-of-range
|
||||
// columns. This path only runs during the single decode that populates the
|
||||
// cache, never on the screen render hot path, so the checks are cheap.
|
||||
struct DirectCacheWriter {
|
||||
uint8_t* buffer;
|
||||
int bytesPerRow;
|
||||
int bandRows;
|
||||
int originX;
|
||||
uint8_t* rowPtr; // Pre-computed for current row
|
||||
uint8_t* rowPtr; // Pre-computed for current row; nullptr if row is out of band
|
||||
|
||||
void init(uint8_t* cacheBuffer, int cacheBytesPerRow, int cacheOriginX) {
|
||||
void init(uint8_t* cacheBuffer, int cacheBytesPerRow, int cacheBandRows, int cacheOriginX) {
|
||||
buffer = cacheBuffer;
|
||||
bytesPerRow = cacheBytesPerRow;
|
||||
bandRows = cacheBandRows;
|
||||
originX = cacheOriginX;
|
||||
rowPtr = nullptr;
|
||||
}
|
||||
|
||||
// Call once per row before the column loop.
|
||||
inline void beginRow(int screenY, int cacheOriginY) { rowPtr = buffer + (screenY - cacheOriginY) * bytesPerRow; }
|
||||
// Call once per row before the column loop. Drops rows outside the band.
|
||||
inline void beginRow(int screenY, int cacheOriginY) {
|
||||
const int localRow = screenY - cacheOriginY;
|
||||
rowPtr = (static_cast<unsigned>(localRow) < static_cast<unsigned>(bandRows))
|
||||
? buffer + (size_t)localRow * bytesPerRow
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
// Write a 2-bit pixel value. No bounds checking.
|
||||
// Write a 2-bit pixel value. Drops the write if the row is out of band or the
|
||||
// column is out of range.
|
||||
inline void writePixel(int screenX, uint8_t value) const {
|
||||
if (!rowPtr) return;
|
||||
const int localX = screenX - originX;
|
||||
const int byteIdx = localX >> 2; // localX / 4
|
||||
if (static_cast<unsigned>(byteIdx) >= static_cast<unsigned>(bytesPerRow)) return;
|
||||
const int bitShift = 6 - (localX & 3) * 2; // MSB first: pixel 0 at bits 6-7
|
||||
rowPtr[byteIdx] = (rowPtr[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
if (stride <= 0 || blockH <= 0 || validW <= 0) return 1;
|
||||
|
||||
const bool useDithering = ctx->config->useDithering;
|
||||
const bool caching = ctx->caching;
|
||||
bool caching = ctx->caching;
|
||||
const int32_t fineScaleFPX = ctx->fineScaleFPX;
|
||||
const int32_t invScaleFPX = ctx->invScaleFPX;
|
||||
const int32_t fineScaleFPY = ctx->fineScaleFPY;
|
||||
@@ -169,9 +169,21 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
DirectPixelWriter pw;
|
||||
pw.init(renderer);
|
||||
|
||||
// The cache streams to disk one MCU-row band at a time. Flushing rows below
|
||||
// this block (raster order guarantees they are final) repositions the band;
|
||||
// cacheOriginY then maps screen rows to the band-local buffer rows. If a flush
|
||||
// write fails, stop caching for the rest of this decode (and let finalize drop
|
||||
// the partial file) rather than writing past the band buffer.
|
||||
DirectCacheWriter cw;
|
||||
int cacheOriginY = 0;
|
||||
if (caching) {
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.originX);
|
||||
if (!ctx->cache.advanceTo(dstYStart)) {
|
||||
caching = false;
|
||||
ctx->caching = false;
|
||||
} else {
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||
cacheOriginY = ctx->config->y + ctx->cache.bandStart;
|
||||
}
|
||||
}
|
||||
|
||||
// === 1:1 fast path: no scaling math ===
|
||||
@@ -179,7 +191,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||
const int outY = cfgY + dstY;
|
||||
pw.beginRow(outY);
|
||||
if (caching) cw.beginRow(outY, ctx->config->y);
|
||||
if (caching) cw.beginRow(outY, cacheOriginY);
|
||||
const uint8_t* row = &pixels[(dstY - blockY) * stride];
|
||||
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
|
||||
const int outX = cfgX + dstX;
|
||||
@@ -213,7 +225,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||
const int outY = cfgY + dstY;
|
||||
pw.beginRow(outY);
|
||||
if (caching) cw.beginRow(outY, ctx->config->y);
|
||||
if (caching) cw.beginRow(outY, cacheOriginY);
|
||||
const int32_t srcFyFP = dstY * invScaleFPY;
|
||||
const int32_t fy = srcFyFP & FP_MASK;
|
||||
const int32_t fyInv = FP_ONE - fy;
|
||||
@@ -310,7 +322,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||
const int outY = cfgY + dstY;
|
||||
pw.beginRow(outY);
|
||||
if (caching) cw.beginRow(outY, ctx->config->y);
|
||||
if (caching) cw.beginRow(outY, cacheOriginY);
|
||||
const int32_t srcFyFP = dstY * invScaleFPY;
|
||||
int ly = (srcFyFP >> FP_SHIFT) - blockY;
|
||||
if (ly < 0) ly = 0;
|
||||
@@ -469,11 +481,14 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
|
||||
jpeg->setUserPointer(&ctx);
|
||||
|
||||
// Allocate cache buffer using final output dimensions
|
||||
// Start streaming the pixel cache to disk. The band only needs to hold the
|
||||
// tallest single decode block: a JPEGDEC MCU cell is at most 16 scaled-source
|
||||
// rows tall, which our fine scale maps to this many output rows.
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
if (ctx.caching) {
|
||||
if (!ctx.cache.allocate(destWidth, destHeight, config.x, config.y)) {
|
||||
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
|
||||
const int maxBlockDstRows = (int)(((int64_t)16 * ctx.fineScaleFPY) >> FP_SHIFT) + 2;
|
||||
if (!ctx.cache.begin(config.cachePath, destWidth, destHeight, config.x, config.y, maxBlockDstRows)) {
|
||||
LOG_ERR("JPG", "Failed to start cache stream, continuing without caching");
|
||||
ctx.caching = false;
|
||||
}
|
||||
}
|
||||
@@ -484,14 +499,16 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
|
||||
if (rc != 1) {
|
||||
LOG_ERR("JPG", "Decode failed (rc=%d, lastError=%d)", rc, jpeg->getLastError());
|
||||
if (ctx.caching) ctx.cache.abort();
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("JPG", "JPEG decoding complete - render time: %lu ms", decodeTime);
|
||||
|
||||
// Write cache file if caching was enabled
|
||||
// Finalize the streamed cache file. Note: a flush failure mid-decode clears
|
||||
// ctx.caching (the partial file is dropped), so re-read the flag here.
|
||||
if (ctx.caching) {
|
||||
ctx.cache.writeToFile(config.cachePath);
|
||||
ctx.cache.finalize();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -4,76 +4,181 @@
|
||||
#include <Logging.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
// Cache buffer for storing 2-bit pixels (4 levels) during decode.
|
||||
// Packs 4 pixels per byte, MSB first.
|
||||
// Streaming cache writer for 2-bit pixels (4 levels). Packs 4 pixels per byte,
|
||||
// MSB first.
|
||||
//
|
||||
// The .pxc file is written incrementally in small row bands rather than holding
|
||||
// the whole decoded image in one heap buffer. A full-page image (e.g. 482x728)
|
||||
// needs ~88KB packed, which will not fit alongside the ~20KB JPEG decoder on a
|
||||
// fragmented 380KB heap (free heap is routinely ~55KB on an image page). When
|
||||
// the cache cannot be written, every render pass re-decodes the JPEG from
|
||||
// scratch; an anti-aliased image page renders ~14 times (BW + AA restore + two
|
||||
// grayscale planes x ~6 strips), so a 2s decode becomes a ~30s freeze / watchdog
|
||||
// reset. Streaming keeps the working set to a single MCU-row band, so caching
|
||||
// succeeds and the image is decoded exactly once.
|
||||
//
|
||||
// Correctness relies on JPEGDEC delivering blocks in raster MCU order (outer
|
||||
// loop over y, inner over x: see jpeg.inl DecodeJPEG). Consecutive MCU rows map
|
||||
// to contiguous, non-overlapping destination row ranges, so once a block whose
|
||||
// top row is Y arrives, every output row < Y is final and is flushed to disk.
|
||||
struct PixelCache {
|
||||
uint8_t* buffer;
|
||||
uint8_t* buffer; // band buffer: (bandRows + 1) rows; last row kept zeroed
|
||||
uint8_t* zeroRow; // points at the spare zeroed row, for gap/clip fill
|
||||
int width;
|
||||
int height;
|
||||
int bytesPerRow;
|
||||
int originX; // config.x - to convert screen coords to cache coords
|
||||
int originY; // config.y
|
||||
int bandRows; // rows held in the band buffer
|
||||
int bandStart; // image-local row index of band buffer row 0
|
||||
int flushedRows; // image-local rows already written to file
|
||||
HalFile file;
|
||||
std::string cachePathStr;
|
||||
bool ok;
|
||||
|
||||
PixelCache() : buffer(nullptr), width(0), height(0), bytesPerRow(0), originX(0), originY(0) {}
|
||||
PixelCache()
|
||||
: buffer(nullptr),
|
||||
zeroRow(nullptr),
|
||||
width(0),
|
||||
height(0),
|
||||
bytesPerRow(0),
|
||||
originX(0),
|
||||
originY(0),
|
||||
bandRows(0),
|
||||
bandStart(0),
|
||||
flushedRows(0),
|
||||
ok(false) {}
|
||||
PixelCache(const PixelCache&) = delete;
|
||||
PixelCache& operator=(const PixelCache&) = delete;
|
||||
|
||||
static constexpr size_t MAX_CACHE_BYTES = 256 * 1024; // 256KB limit for embedded targets
|
||||
static constexpr int MIN_BAND_ROWS = 16;
|
||||
static constexpr size_t MAX_BAND_BYTES = 24 * 1024; // band working-set ceiling
|
||||
|
||||
bool allocate(int w, int h, int ox, int oy) {
|
||||
// Open the cache file, write the header, and allocate a band buffer big enough
|
||||
// to hold the tallest single decode block (maxBlockDstRows output rows).
|
||||
bool begin(const std::string& cachePath, int w, int h, int ox, int oy, int maxBlockDstRows) {
|
||||
width = w;
|
||||
height = h;
|
||||
originX = ox;
|
||||
originY = oy;
|
||||
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
size_t bufferSize = (size_t)bytesPerRow * h;
|
||||
if (bufferSize > MAX_CACHE_BYTES) {
|
||||
LOG_ERR("IMG", "Cache buffer too large: %d bytes for %dx%d (limit %d)", bufferSize, w, h, MAX_CACHE_BYTES);
|
||||
bandStart = 0;
|
||||
flushedRows = 0;
|
||||
ok = false;
|
||||
|
||||
int wantRows = maxBlockDstRows + 2;
|
||||
if (wantRows < MIN_BAND_ROWS) wantRows = MIN_BAND_ROWS;
|
||||
if (wantRows > h) wantRows = h;
|
||||
|
||||
size_t maxRowsByMem = MAX_BAND_BYTES / (size_t)bytesPerRow;
|
||||
if (maxRowsByMem < 1) maxRowsByMem = 1;
|
||||
if ((size_t)wantRows > maxRowsByMem) wantRows = (int)maxRowsByMem;
|
||||
|
||||
// A single decode block must fit inside the band, otherwise streaming would
|
||||
// drop rows. This only fails for pathological upscales that could not be
|
||||
// cached at all; fall back to the no-cache path.
|
||||
if (wantRows < maxBlockDstRows) {
|
||||
LOG_ERR("IMG", "Cache band too small (%d < %d rows) for %dx%d", wantRows, maxBlockDstRows, w, h);
|
||||
return false;
|
||||
}
|
||||
buffer = (uint8_t*)malloc(bufferSize);
|
||||
if (buffer) {
|
||||
memset(buffer, 0, bufferSize);
|
||||
LOG_DBG("IMG", "Allocated cache buffer: %d bytes for %dx%d", bufferSize, w, h);
|
||||
}
|
||||
return buffer != nullptr;
|
||||
bandRows = wantRows;
|
||||
|
||||
const size_t bufSize = (size_t)(bandRows + 1) * bytesPerRow; // +1 spare zero row
|
||||
buffer = (uint8_t*)malloc(bufSize);
|
||||
if (!buffer) {
|
||||
LOG_ERR("IMG", "OOM cache band: %u bytes", (unsigned)bufSize);
|
||||
return false;
|
||||
}
|
||||
memset(buffer, 0, bufSize);
|
||||
zeroRow = buffer + (size_t)bandRows * bytesPerRow;
|
||||
|
||||
void setPixel(int screenX, int screenY, uint8_t value) {
|
||||
if (!buffer) return;
|
||||
int localX = screenX - originX;
|
||||
int localY = screenY - originY;
|
||||
if (localX < 0 || localX >= width || localY < 0 || localY >= height) return;
|
||||
|
||||
int byteIdx = localY * bytesPerRow + localX / 4;
|
||||
int bitShift = 6 - (localX % 4) * 2; // MSB first: pixel 0 at bits 6-7
|
||||
buffer[byteIdx] = (buffer[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||
}
|
||||
|
||||
bool writeToFile(const std::string& cachePath) {
|
||||
if (!buffer) return false;
|
||||
|
||||
HalFile cacheFile;
|
||||
if (!Storage.openFileForWrite("IMG", cachePath, cacheFile)) {
|
||||
if (!Storage.openFileForWrite("IMG", cachePath, file)) {
|
||||
LOG_ERR("IMG", "Failed to open cache file for writing: %s", cachePath.c_str());
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
return false;
|
||||
}
|
||||
cachePathStr = cachePath;
|
||||
|
||||
uint16_t w16 = (uint16_t)w;
|
||||
uint16_t h16 = (uint16_t)h;
|
||||
if (file.write(&w16, 2) != 2 || file.write(&h16, 2) != 2) {
|
||||
LOG_ERR("IMG", "Failed to write cache header: %s", cachePath.c_str());
|
||||
abort();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t w = width;
|
||||
uint16_t h = height;
|
||||
cacheFile.write(&w, 2);
|
||||
cacheFile.write(&h, 2);
|
||||
cacheFile.write(buffer, bytesPerRow * height);
|
||||
cacheFile.close();
|
||||
|
||||
LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePath.c_str(), width, height, 4 + bytesPerRow * height);
|
||||
LOG_DBG("IMG", "Cache stream started: %s (%dx%d, band %d rows)", cachePath.c_str(), w, h, bandRows);
|
||||
ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flush every output row below newTopRow (they are final in raster order) and
|
||||
// reposition the band to start at newTopRow. Returns false if a write failed,
|
||||
// in which case the caller must stop caching for the rest of the decode.
|
||||
bool advanceTo(int newTopRow) {
|
||||
if (!ok) return false;
|
||||
if (newTopRow <= bandStart) return true;
|
||||
if (newTopRow > height) newTopRow = height;
|
||||
|
||||
for (int r = bandStart; r < newTopRow; ++r) {
|
||||
const int idx = r - bandStart;
|
||||
const uint8_t* rowPtr = (idx < bandRows) ? (buffer + (size_t)idx * bytesPerRow) : zeroRow;
|
||||
if (file.write(rowPtr, (size_t)bytesPerRow) != (size_t)bytesPerRow) {
|
||||
LOG_ERR("IMG", "Cache write error at row %d", r);
|
||||
ok = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
flushedRows = newTopRow;
|
||||
bandStart = newTopRow;
|
||||
memset(buffer, 0, (size_t)bandRows * bytesPerRow); // fresh band (gaps stay black)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flush the final band and zero-fill any rows never covered (image clipped by
|
||||
// the screen), then close the file.
|
||||
bool finalize() {
|
||||
if (!ok) {
|
||||
abort();
|
||||
return false;
|
||||
}
|
||||
for (int r = flushedRows; r < height; ++r) {
|
||||
const int idx = r - bandStart;
|
||||
const uint8_t* rowPtr = (idx >= 0 && idx < bandRows) ? (buffer + (size_t)idx * bytesPerRow) : zeroRow;
|
||||
if (file.write(rowPtr, (size_t)bytesPerRow) != (size_t)bytesPerRow) {
|
||||
LOG_ERR("IMG", "Cache write error at row %d", r);
|
||||
abort();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePathStr.c_str(), width, height,
|
||||
4 + bytesPerRow * height);
|
||||
ok = false; // file handed off; nothing left to clean up
|
||||
return true;
|
||||
}
|
||||
|
||||
// Drop a partial/failed cache so a later decode re-creates it cleanly.
|
||||
void abort() {
|
||||
if (file.isOpen()) file.close();
|
||||
if (!cachePathStr.empty()) {
|
||||
Storage.remove(cachePathStr.c_str());
|
||||
}
|
||||
ok = false;
|
||||
}
|
||||
|
||||
~PixelCache() {
|
||||
if (file.isOpen()) {
|
||||
// The file is still open, so neither finalize() nor abort() ran, or a
|
||||
// mid-stream write failed (advanceTo() cleared ok but left the file open).
|
||||
// Drop the partial cache so we leave no corrupt file behind.
|
||||
abort();
|
||||
}
|
||||
if (buffer) {
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
|
||||
@@ -201,10 +201,19 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
pw.init(*ctx->renderer);
|
||||
pw.beginRow(outY);
|
||||
|
||||
// The cache streams to disk one row at a time. Flushing rows below this one
|
||||
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
||||
// A flush failure stops caching for the rest of the decode so we never write
|
||||
// past the band buffer; finalize() then drops the partial file.
|
||||
DirectCacheWriter cw;
|
||||
if (caching) {
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.originX);
|
||||
cw.beginRow(outY, ctx->config->y);
|
||||
if (!ctx->cache.advanceTo(dstY)) {
|
||||
caching = false;
|
||||
ctx->caching = false;
|
||||
} else {
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||
cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
|
||||
}
|
||||
}
|
||||
|
||||
int srcX = 0;
|
||||
@@ -348,19 +357,16 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate cache buffer using SCALED dimensions.
|
||||
// PNG decode is fast enough (~135ms for 400x600) that caching provides minimal benefit
|
||||
// for larger images, while the cache buffer competes with the 44KB PNG decoder for heap.
|
||||
// Skip caching when the buffer would exceed the framebuffer size (48KB).
|
||||
static constexpr size_t PNG_MAX_CACHE_BYTES = 48000;
|
||||
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to
|
||||
// bottom and we emit at most one (downscaled) output row per callback, so the
|
||||
// band only needs a single row. Streaming keeps the working set tiny, so
|
||||
// unlike the old full-image buffer it neither competes with the ~44KB decoder
|
||||
// nor forces larger images to skip caching - which previously meant a full
|
||||
// re-decode on every one of an image page's ~14 render passes.
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
if (ctx.caching) {
|
||||
size_t cacheSize = (size_t)((ctx.dstWidth + 3) / 4) * ctx.dstHeight;
|
||||
if (cacheSize > PNG_MAX_CACHE_BYTES) {
|
||||
LOG_DBG("PNG", "Skipping cache: %zu bytes exceeds PNG limit (%zu)", cacheSize, PNG_MAX_CACHE_BYTES);
|
||||
ctx.caching = false;
|
||||
} else if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
|
||||
LOG_ERR("PNG", "Failed to allocate cache buffer, continuing without caching");
|
||||
if (!ctx.cache.begin(config.cachePath, ctx.dstWidth, ctx.dstHeight, config.x, config.y, 1)) {
|
||||
LOG_ERR("PNG", "Failed to start cache stream, continuing without caching");
|
||||
ctx.caching = false;
|
||||
}
|
||||
}
|
||||
@@ -374,14 +380,15 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
|
||||
if (rc != PNG_SUCCESS) {
|
||||
LOG_ERR("PNG", "Decode failed: %d", rc);
|
||||
if (ctx.caching) ctx.cache.abort();
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime);
|
||||
|
||||
// Write cache file if caching was enabled and buffer was allocated
|
||||
// Finalize the streamed cache (caching may have been cleared on a flush error).
|
||||
if (ctx.caching) {
|
||||
ctx.cache.writeToFile(config.cachePath);
|
||||
ctx.cache.finalize();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user