188 lines
6.6 KiB
C++
188 lines
6.6 KiB
C++
#pragma once
|
|
|
|
#include <HalStorage.h>
|
|
#include <Logging.h>
|
|
#include <stdint.h>
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
// 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; // 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),
|
|
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 int MIN_BAND_ROWS = 16;
|
|
static constexpr size_t MAX_BAND_BYTES = 24 * 1024; // band working-set ceiling
|
|
|
|
// 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
|
|
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;
|
|
}
|
|
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;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
};
|