From 67da8139b3591b891695083c571cda5330399658 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Wed, 17 Dec 2025 01:39:22 +1100 Subject: [PATCH] Use 6x8kB chunks instead of 1x48kB chunk for secondary display buffer (#36) ## Summary - When allocating the `bwBuffer` required to restore the RED RAM in the EPD, we were previously allocating the whole frame buffer in one contiguous memory chunk (48kB) - Depending on the state of memory fragmentation at the time of this call, it may not be possible to allocate all that memory - Instead, we now allocate 6 blocks of 8kB instead of the full 48kB, this should mean the display updates are more resilient to different memory conditions ## Additional Context --- lib/GfxRenderer/GfxRenderer.cpp | 75 ++++++++++++++++++++++++++++----- lib/GfxRenderer/GfxRenderer.h | 8 +++- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index dc7c781..ac36668 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -180,33 +180,88 @@ void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsb void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); } +void GfxRenderer::freeBwBufferChunks() { + for (auto& bwBufferChunk : bwBufferChunks) { + if (bwBufferChunk) { + free(bwBufferChunk); + bwBufferChunk = nullptr; + } + } +} + /** * This should be called before grayscale buffers are populated. * A `restoreBwBuffer` call should always follow the grayscale render if this method was called. + * Uses chunked allocation to avoid needing 48KB of contiguous memory. */ void GfxRenderer::storeBwBuffer() { - if (bwBuffer) { - Serial.printf("[%lu] [GFX] !! BW buffer already stored - this is likely a bug, freeing it\n", millis()); - free(bwBuffer); + const uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + + // Allocate and copy each chunk + for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) { + // Check if any chunks are already allocated + if (bwBufferChunks[i]) { + Serial.printf("[%lu] [GFX] !! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk\n", + millis(), i); + free(bwBufferChunks[i]); + bwBufferChunks[i] = nullptr; + } + + const size_t offset = i * BW_BUFFER_CHUNK_SIZE; + bwBufferChunks[i] = static_cast(malloc(BW_BUFFER_CHUNK_SIZE)); + + if (!bwBufferChunks[i]) { + Serial.printf("[%lu] [GFX] !! Failed to allocate BW buffer chunk %zu (%zu bytes)\n", millis(), i, + BW_BUFFER_CHUNK_SIZE); + // Free previously allocated chunks + freeBwBufferChunks(); + return; + } + + memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE); } - bwBuffer = static_cast(malloc(EInkDisplay::BUFFER_SIZE)); - memcpy(bwBuffer, einkDisplay.getFrameBuffer(), EInkDisplay::BUFFER_SIZE); + Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS, + BW_BUFFER_CHUNK_SIZE); } /** * This can only be called if `storeBwBuffer` was called prior to the grayscale render. * It should be called to restore the BW buffer state after grayscale rendering is complete. + * Uses chunked restoration to match chunked storage. */ void GfxRenderer::restoreBwBuffer() { - if (!bwBuffer) { - Serial.printf("[%lu] [GFX] !! BW buffer not stored - this is likely a bug\n", millis()); + // Check if any all chunks are allocated + bool missingChunks = false; + for (const auto& bwBufferChunk : bwBufferChunks) { + if (!bwBufferChunk) { + missingChunks = true; + break; + } + } + + if (missingChunks) { + freeBwBufferChunks(); return; } - einkDisplay.cleanupGrayscaleBuffers(bwBuffer); - free(bwBuffer); - bwBuffer = nullptr; + uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) { + // Check if chunk is missing + if (!bwBufferChunks[i]) { + Serial.printf("[%lu] [GFX] !! BW buffer chunks not stored - this is likely a bug\n", millis()); + freeBwBufferChunks(); + return; + } + + const size_t offset = i * BW_BUFFER_CHUNK_SIZE; + memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE); + } + + einkDisplay.cleanupGrayscaleBuffers(frameBuffer); + + freeBwBufferChunks(); + Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis()); } void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y, diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index cb8b588..f07fcfb 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -10,12 +10,18 @@ class GfxRenderer { enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; private: + static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory + static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE; + static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE, + "BW buffer chunking does not line up with display buffer size"); + EInkDisplay& einkDisplay; RenderMode renderMode; - uint8_t* bwBuffer = nullptr; + uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; std::map fontMap; void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, EpdFontStyle style) const; + void freeBwBufferChunks(); public: explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW) {}