Applay striped buffer approach to save memory
This commit is contained in:
@@ -160,6 +160,63 @@ bool computePageDynamicYBand(const Page& page, const GfxRenderer& renderer, cons
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tiled grayscale: render each plane band-by-band into a small scratch and
|
||||
// stream straight to the controller, leaving the BW framebuffer intact so no
|
||||
// storeBwBuffer / restoreBwBuffer is needed. Controller RAM is re-synced from
|
||||
// the live framebuffer afterward via cleanupGrayscaleWithFrameBuffer().
|
||||
//
|
||||
// Returns true when the strip path ran end-to-end (controller now holds the AA
|
||||
// planes and the live BW frame is clean). Returns false when the controller
|
||||
// doesn't support strip grayscale OR the scratch allocation fails — caller
|
||||
// should fall back to the legacy storeBwBufferRect path.
|
||||
//
|
||||
// The page is re-rendered ceil(panelHeight/STRIP_ROWS) times per plane, but
|
||||
// renderCharImpl culls out-of-band glyphs before bitmap decode so the cost
|
||||
// stays close to one render. Only renderTextOnly() is called here, matching the
|
||||
// legacy AA pass — images and HRs do not participate in grayscale.
|
||||
bool runTiledGrayscalePass(GfxRenderer& renderer, Page& page, int fontId, int marginLeft, int contentTop) {
|
||||
if (!renderer.supportsStripGrayscale()) return false;
|
||||
|
||||
// Strip height trades scratch size for the number of re-renders. Each render
|
||||
// pays layout + glyph-cull overhead even when bitmap decode is skipped, so
|
||||
// fewer/bigger bands win as long as the scratch fits. 240 rows × ~100 bytes
|
||||
// ≈ ~24 KB — still well below the legacy partial-snapshot footprint while
|
||||
// cutting X3 (480 px) to 2 bands/plane and X4 (800 px) to 4 bands/plane.
|
||||
constexpr int STRIP_ROWS = 240;
|
||||
const int gh = renderer.getDisplayHeight();
|
||||
const int gwBytes = renderer.getDisplayWidthBytes();
|
||||
|
||||
auto scratch = std::unique_ptr<uint8_t[]>(new (std::nothrow) uint8_t[static_cast<size_t>(gwBytes) * STRIP_ROWS]);
|
||||
if (!scratch) {
|
||||
LOG_INF("ERS", "Tiled grayscale: scratch alloc failed (%d bytes); falling back to legacy path",
|
||||
gwBytes * STRIP_ROWS);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto renderPlane = [&](GfxRenderer::RenderMode mode, bool lsbPlane) {
|
||||
renderer.setRenderMode(mode);
|
||||
for (int y = 0; y < gh; y += STRIP_ROWS) {
|
||||
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
|
||||
renderer.beginStripTarget(scratch.get(), y, rows);
|
||||
renderer.clearScreen(0x00);
|
||||
page.renderTextOnly(renderer, fontId, marginLeft, contentTop);
|
||||
renderer.endStripTarget();
|
||||
renderer.writeGrayscalePlaneStrip(lsbPlane, scratch.get(), y, rows);
|
||||
}
|
||||
};
|
||||
|
||||
renderPlane(GfxRenderer::GRAYSCALE_LSB, true);
|
||||
renderPlane(GfxRenderer::GRAYSCALE_MSB, false);
|
||||
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
renderer.displayGrayBuffer();
|
||||
|
||||
// BW framebuffer is intact; re-sync controller RAM for the next differential
|
||||
// page turn directly from it.
|
||||
renderer.cleanupGrayscaleWithFrameBuffer();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Computes the [0..100] EPUB progress percent. Returns 0 when pageCount is unknown (sync/bookmark
|
||||
// pre-render writes), in which case the next saveProgress() will overwrite progress.bin with the
|
||||
// real value before the user can leave the reader.
|
||||
@@ -2083,47 +2140,6 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
}
|
||||
const auto tDisplay = millis();
|
||||
|
||||
// Save bw buffer to reset buffer state after grayscale data sync.
|
||||
// Pre-check heap to avoid entering the chunk-retry path when success is unlikely.
|
||||
const uint32_t bwStoreFreeHeap = esp_get_free_heap_size();
|
||||
const uint32_t bwStoreContigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
const bool shouldAttemptBwSnapshot =
|
||||
bwStoreFreeHeap >= BW_SNAPSHOT_MIN_FREE_HEAP_BYTES && bwStoreContigHeap >= BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES;
|
||||
|
||||
logReaderMemSnapshot("bw_store_begin");
|
||||
bool bwBufferStored = false;
|
||||
if (shouldAttemptBwSnapshot) {
|
||||
const int contentLeft = orientedMarginLeft;
|
||||
const int contentRight = std::max(contentLeft, renderer.getScreenWidth() - orientedMarginRight);
|
||||
const int contentBottom = std::max(contentTop, renderer.getScreenHeight() - orientedMarginBottom);
|
||||
|
||||
int bandTop = 0;
|
||||
int bandBottom = std::max(0, contentBottom - contentTop);
|
||||
if (!computePageDynamicYBand(*page, renderer, getEffectiveReaderFontId(), bandBottom, &bandTop, &bandBottom)) {
|
||||
bandTop = 0;
|
||||
bandBottom = std::max(0, contentBottom - contentTop);
|
||||
}
|
||||
|
||||
const int snapshotTop = contentTop + bandTop;
|
||||
const int snapshotHeight = std::max(0, bandBottom - bandTop);
|
||||
bwBufferStored = renderer.storeBwBufferRect(contentLeft, snapshotTop, contentRight - contentLeft, snapshotHeight);
|
||||
} else {
|
||||
LOG_INF("ERS", "Skipping BW snapshot precheck (free=%lu contig=%lu, need free>=%lu contig>=%lu)", bwStoreFreeHeap,
|
||||
bwStoreContigHeap, static_cast<uint32_t>(BW_SNAPSHOT_MIN_FREE_HEAP_BYTES),
|
||||
static_cast<uint32_t>(BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES));
|
||||
}
|
||||
const auto tBwStore = millis();
|
||||
logReaderMemSnapshot("bw_store_end");
|
||||
if (!bwBufferStored) {
|
||||
if (aaEnabledForThisRender && !antiAliasingSuspendedLowMemory) {
|
||||
antiAliasingSuspendedLowMemory = true;
|
||||
const uint32_t freeHeapNow = esp_get_free_heap_size();
|
||||
const uint32_t contigHeapNow = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
LOG_INF("ERS", "Suspending text anti-aliasing due to low heap (free=%lu contig=%lu)", freeHeapNow, contigHeapNow);
|
||||
}
|
||||
LOG_INF("ERS", "Skipping grayscale/BW-restore for this page (insufficient heap for BW snapshot)");
|
||||
}
|
||||
|
||||
// Only schedule the half-refresh if at least one real image was decoded on this page.
|
||||
// Placeholder-only pages don't deposit grayscale data that needs settling.
|
||||
if (page->hasImages() && !page->allImagesArePlaceholders(effectiveForceLoad) &&
|
||||
@@ -2131,69 +2147,139 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
pendingHalfRefreshAfterImagePage = true;
|
||||
}
|
||||
|
||||
// grayscale rendering
|
||||
// TODO: Only do this if font supports it
|
||||
if (aaEnabledForThisRender && bwBufferStored) {
|
||||
logReaderMemSnapshot("gray_lsb_begin");
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
const auto tGrayLsb = millis();
|
||||
logReaderMemSnapshot("gray_lsb_end");
|
||||
|
||||
// Render and copy to MSB buffer
|
||||
logReaderMemSnapshot("gray_msb_begin");
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
const auto tGrayMsb = millis();
|
||||
logReaderMemSnapshot("gray_msb_end");
|
||||
|
||||
// display grayscale part
|
||||
logReaderMemSnapshot("gray_display_begin");
|
||||
renderer.displayGrayBuffer();
|
||||
const auto tGrayDisplay = millis();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
fcm->logStats("gray");
|
||||
logReaderMemSnapshot("gray_display_end");
|
||||
|
||||
// restore the bw data
|
||||
logReaderMemSnapshot("bw_restore_begin");
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
logReaderMemSnapshot("bw_restore_end");
|
||||
// Tiled grayscale: try the strip path first when AA is on and the controller
|
||||
// supports it. It allocates an ~8 KB scratch instead of saving a partial BW
|
||||
// frame, leaving the live BW framebuffer intact (no storeBwBuffer needed).
|
||||
// Falls through to the legacy snapshot path if strip is unsupported or the
|
||||
// scratch can't be allocated.
|
||||
bool grayscaleDone = false;
|
||||
uint32_t tiledGrayMs = 0;
|
||||
if (aaEnabledForThisRender) {
|
||||
logReaderMemSnapshot("tiled_gray_begin");
|
||||
const auto tTiledBegin = millis();
|
||||
grayscaleDone = runTiledGrayscalePass(renderer, *page, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
if (grayscaleDone) {
|
||||
tiledGrayMs = millis() - tTiledBegin;
|
||||
fcm->logStats("tiled_gray");
|
||||
logReaderMemSnapshot("tiled_gray_end");
|
||||
}
|
||||
}
|
||||
|
||||
if (grayscaleDone) {
|
||||
const auto tEnd = millis();
|
||||
lastRenderStats.usedGrayscale = true;
|
||||
lastRenderStats.phases = {tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender,
|
||||
tBwStore - tDisplay, tGrayLsb - tBwStore, tGrayMsb - tGrayLsb,
|
||||
tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0};
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums "
|
||||
"gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore,
|
||||
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
|
||||
lastRenderStats.phases = {tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, 0, tiledGrayMs, 0, 0, 0,
|
||||
tEnd - t0};
|
||||
LOG_DBG("ERS", "Page render (tiled): prewarm=%lums bw_render=%lums display=%lums tiled_gray=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tiledGrayMs, tEnd - t0);
|
||||
} else {
|
||||
uint32_t bwRestoreMs = 0;
|
||||
if (bwBufferStored) {
|
||||
// Legacy fallback: save (partial) BW frame, render LSB+MSB planes into the
|
||||
// live framebuffer, display, then restore. Pre-check heap to avoid entering
|
||||
// the chunk-retry path when success is unlikely.
|
||||
const uint32_t bwStoreFreeHeap = esp_get_free_heap_size();
|
||||
const uint32_t bwStoreContigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
const bool shouldAttemptBwSnapshot =
|
||||
bwStoreFreeHeap >= BW_SNAPSHOT_MIN_FREE_HEAP_BYTES && bwStoreContigHeap >= BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES;
|
||||
|
||||
logReaderMemSnapshot("bw_store_begin");
|
||||
bool bwBufferStored = false;
|
||||
if (shouldAttemptBwSnapshot) {
|
||||
const int contentLeft = orientedMarginLeft;
|
||||
const int contentRight = std::max(contentLeft, renderer.getScreenWidth() - orientedMarginRight);
|
||||
const int contentBottom = std::max(contentTop, renderer.getScreenHeight() - orientedMarginBottom);
|
||||
|
||||
int bandTop = 0;
|
||||
int bandBottom = std::max(0, contentBottom - contentTop);
|
||||
if (!computePageDynamicYBand(*page, renderer, getEffectiveReaderFontId(), bandBottom, &bandTop, &bandBottom)) {
|
||||
bandTop = 0;
|
||||
bandBottom = std::max(0, contentBottom - contentTop);
|
||||
}
|
||||
|
||||
const int snapshotTop = contentTop + bandTop;
|
||||
const int snapshotHeight = std::max(0, bandBottom - bandTop);
|
||||
bwBufferStored = renderer.storeBwBufferRect(contentLeft, snapshotTop, contentRight - contentLeft, snapshotHeight);
|
||||
} else {
|
||||
LOG_INF("ERS", "Skipping BW snapshot precheck (free=%lu contig=%lu, need free>=%lu contig>=%lu)", bwStoreFreeHeap,
|
||||
bwStoreContigHeap, static_cast<uint32_t>(BW_SNAPSHOT_MIN_FREE_HEAP_BYTES),
|
||||
static_cast<uint32_t>(BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES));
|
||||
}
|
||||
const auto tBwStore = millis();
|
||||
logReaderMemSnapshot("bw_store_end");
|
||||
if (!bwBufferStored) {
|
||||
if (aaEnabledForThisRender && !antiAliasingSuspendedLowMemory) {
|
||||
antiAliasingSuspendedLowMemory = true;
|
||||
const uint32_t freeHeapNow = esp_get_free_heap_size();
|
||||
const uint32_t contigHeapNow = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
LOG_INF("ERS", "Suspending text anti-aliasing due to low heap (free=%lu contig=%lu)", freeHeapNow,
|
||||
contigHeapNow);
|
||||
}
|
||||
LOG_INF("ERS", "Skipping grayscale/BW-restore for this page (insufficient heap for BW snapshot)");
|
||||
}
|
||||
|
||||
// grayscale rendering
|
||||
// TODO: Only do this if font supports it
|
||||
if (aaEnabledForThisRender && bwBufferStored) {
|
||||
logReaderMemSnapshot("gray_lsb_begin");
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
const auto tGrayLsb = millis();
|
||||
logReaderMemSnapshot("gray_lsb_end");
|
||||
|
||||
// Render and copy to MSB buffer
|
||||
logReaderMemSnapshot("gray_msb_begin");
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
const auto tGrayMsb = millis();
|
||||
logReaderMemSnapshot("gray_msb_end");
|
||||
|
||||
// display grayscale part
|
||||
logReaderMemSnapshot("gray_display_begin");
|
||||
renderer.displayGrayBuffer();
|
||||
const auto tGrayDisplay = millis();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
fcm->logStats("gray");
|
||||
logReaderMemSnapshot("gray_display_end");
|
||||
|
||||
// restore the bw data
|
||||
logReaderMemSnapshot("bw_restore_begin");
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
logReaderMemSnapshot("bw_restore_end");
|
||||
bwRestoreMs = tBwRestore - tBwStore;
|
||||
}
|
||||
|
||||
const auto tEnd = millis();
|
||||
lastRenderStats.usedGrayscale = false;
|
||||
lastRenderStats.phases = {
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, 0, 0, 0, bwRestoreMs,
|
||||
tEnd - t0};
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, bwRestoreMs, tEnd - t0);
|
||||
const auto tEnd = millis();
|
||||
lastRenderStats.usedGrayscale = true;
|
||||
lastRenderStats.phases = {tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender,
|
||||
tBwStore - tDisplay, tGrayLsb - tBwStore, tGrayMsb - tGrayLsb,
|
||||
tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0};
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums "
|
||||
"gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore,
|
||||
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
|
||||
} else {
|
||||
uint32_t bwRestoreMs = 0;
|
||||
if (bwBufferStored) {
|
||||
// restore the bw data
|
||||
logReaderMemSnapshot("bw_restore_begin");
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
logReaderMemSnapshot("bw_restore_end");
|
||||
bwRestoreMs = tBwRestore - tBwStore;
|
||||
}
|
||||
|
||||
const auto tEnd = millis();
|
||||
lastRenderStats.usedGrayscale = false;
|
||||
lastRenderStats.phases = {
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, 0, 0, 0, bwRestoreMs,
|
||||
tEnd - t0};
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, bwRestoreMs, tEnd - t0);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto* cacheManager = renderer.getFontCacheManager()) {
|
||||
@@ -2249,9 +2335,16 @@ void EpubReaderActivity::displayPreRenderedPage(const Page& page, const int orie
|
||||
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||
}
|
||||
|
||||
// Grayscale AA pass (same as normal render — BW snapshot, gray LSB+MSB, restore).
|
||||
// Grayscale AA pass. Prefer the tiled strip path (no BW snapshot needed);
|
||||
// fall back to the legacy storeBwBufferRect path on controllers that don't
|
||||
// support strip grayscale or when the strip scratch can't be allocated.
|
||||
const bool aaConfigured = SETTINGS.textAntiAliasing && !antiAliasingSuspendedLowMemory;
|
||||
if (aaConfigured) {
|
||||
Page& pageRef = const_cast<Page&>(page);
|
||||
if (runTiledGrayscalePass(renderer, pageRef, getEffectiveReaderFontId(), orientedMarginLeft, contentTop)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t freeHeap = esp_get_free_heap_size();
|
||||
const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
if (freeHeap >= BW_SNAPSHOT_MIN_FREE_HEAP_BYTES && contigHeap >= BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES) {
|
||||
|
||||
Reference in New Issue
Block a user