From 263c1e92b2e61001e26eed499336143c1d7d619d Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Wed, 17 Dec 2025 00:17:49 +1100 Subject: [PATCH] Use single buffer mode for EInkDisplay (#34) ## Summary * Frees up 48kB of statically allocated RAM in exchange for 48kB just when grayscale rendering is needed ## Additional Context * Upstream changes: https://github.com/open-x4-epaper/community-sdk/pull/7 --- lib/GfxRenderer/GfxRenderer.cpp | 51 +++++++++++++++++++++++++++----- lib/GfxRenderer/GfxRenderer.h | 7 ++++- open-x4-sdk | 2 +- platformio.ini | 1 + src/screens/EpubReaderScreen.cpp | 27 ++++++++++------- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 8199bd6..dc7c781 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -132,13 +132,19 @@ void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) cons einkDisplay.displayBuffer(refreshMode); } -// TODO: Support partial window update -// void GfxRenderer::flushArea(const int x, const int y, const int width, const int height) const { -// const int rotatedX = y; -// const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; -// -// einkDisplay.displayBuffer(EInkDisplay::FAST_REFRESH, rotatedX, rotatedY, height, width); -// } +void GfxRenderer::displayWindow(const int x, const int y, const int width, const int height) const { + // Rotate coordinates from portrait (480x800) to landscape (800x480) + // Rotation: 90 degrees clockwise + // Portrait coordinates: (x, y) with dimensions (width, height) + // Landscape coordinates: (rotatedX, rotatedY) with dimensions (rotatedWidth, rotatedHeight) + + const int rotatedX = y; + const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x - width + 1; + const int rotatedWidth = height; + const int rotatedHeight = width; + + einkDisplay.displayWindow(rotatedX, rotatedY, rotatedWidth, rotatedHeight); +} // Note: Internal driver treats screen in command orientation, this library treats in portrait orientation int GfxRenderer::getScreenWidth() { return EInkDisplay::DISPLAY_HEIGHT; } @@ -164,7 +170,7 @@ int GfxRenderer::getLineHeight(const int fontId) const { uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } -void GfxRenderer::swapBuffers() const { einkDisplay.swapBuffers(); } +size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; } void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); } @@ -174,6 +180,35 @@ void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsb void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); } +/** + * This should be called before grayscale buffers are populated. + * A `restoreBwBuffer` call should always follow the grayscale render if this method was called. + */ +void GfxRenderer::storeBwBuffer() { + if (bwBuffer) { + Serial.printf("[%lu] [GFX] !! BW buffer already stored - this is likely a bug, freeing it\n", millis()); + free(bwBuffer); + } + + bwBuffer = static_cast(malloc(EInkDisplay::BUFFER_SIZE)); + memcpy(bwBuffer, einkDisplay.getFrameBuffer(), EInkDisplay::BUFFER_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. + */ +void GfxRenderer::restoreBwBuffer() { + if (!bwBuffer) { + Serial.printf("[%lu] [GFX] !! BW buffer not stored - this is likely a bug\n", millis()); + return; + } + + einkDisplay.cleanupGrayscaleBuffers(bwBuffer); + free(bwBuffer); + bwBuffer = nullptr; +} + void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y, const bool pixelState, const EpdFontStyle style) const { const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 720bf70..cb8b588 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -12,6 +12,7 @@ class GfxRenderer { private: EInkDisplay& einkDisplay; RenderMode renderMode; + uint8_t* bwBuffer = nullptr; std::map fontMap; void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, EpdFontStyle style) const; @@ -27,6 +28,8 @@ class GfxRenderer { static int getScreenWidth(); static int getScreenHeight(); void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const; + // EXPERIMENTAL: Windowed update - display only a rectangular region (portrait coordinates) + void displayWindow(int x, int y, int width, int height) const; void invertScreen() const; void clearScreen(uint8_t color = 0xFF) const; @@ -49,9 +52,11 @@ class GfxRenderer { void copyGrayscaleLsbBuffers() const; void copyGrayscaleMsbBuffers() const; void displayGrayBuffer() const; + void storeBwBuffer(); + void restoreBwBuffer(); // Low level functions uint8_t* getFrameBuffer() const; - void swapBuffers() const; + static size_t getBufferSize(); void grayscaleRevert() const; }; diff --git a/open-x4-sdk b/open-x4-sdk index 4d0dcd5..98a5aa1 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit 4d0dcd5ff87fcd86eb2966a123e85b03284a03db +Subproject commit 98a5aa1f8969ccd317c9b45bf0fa84b6c82e167f diff --git a/platformio.ini b/platformio.ini index 0f044c0..eb839e2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,6 +20,7 @@ build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1 + -DEINK_DISPLAY_SINGLE_BUFFER_MODE=1 # https://libexpat.github.io/doc/api/latest/#XML_GE -DXML_GE=0 -DXML_CONTEXT_BYTES=1024 diff --git a/src/screens/EpubReaderScreen.cpp b/src/screens/EpubReaderScreen.cpp index 39fd74f..6b4855e 100644 --- a/src/screens/EpubReaderScreen.cpp +++ b/src/screens/EpubReaderScreen.cpp @@ -31,7 +31,6 @@ void EpubReaderScreen::onEnter() { epub->setupCacheDir(); - // TODO: Move this to a state object if (SD.exists((epub->getCachePath() + "/progress.bin").c_str())) { File f = SD.open((epub->getCachePath() + "/progress.bin").c_str()); uint8_t data[4]; @@ -210,20 +209,20 @@ void EpubReaderScreen::renderScreen() { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); { + renderer.grayscaleRevert(); + const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing..."); constexpr int margin = 20; - const int x = (GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2; - constexpr int y = 50; - const int w = textWidth + margin * 2; - const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2; - renderer.grayscaleRevert(); - uint8_t* fb1 = renderer.getFrameBuffer(); - renderer.swapBuffers(); - memcpy(fb1, renderer.getFrameBuffer(), EInkDisplay::BUFFER_SIZE); - renderer.fillRect(x, y, w, h, 0); + // Round all coordinates to 8 pixel boundaries + const int x = ((GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2 + 7) / 8 * 8; + constexpr int y = 56; + const int w = (textWidth + margin * 2 + 7) / 8 * 8; + const int h = (renderer.getLineHeight(READER_FONT_ID) + margin * 2 + 7) / 8 * 8; + renderer.fillRect(x, y, w, h, false); renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing..."); renderer.drawRect(x + 5, y + 5, w - 10, h - 10); - renderer.displayBuffer(); + // EXPERIMENTAL: Still suffers from ghosting + renderer.displayWindow(x, y, w, h); pagesUntilFullRefresh = 0; } @@ -297,6 +296,9 @@ void EpubReaderScreen::renderContents(std::unique_ptr page) { pagesUntilFullRefresh--; } + // Save bw buffer to reset buffer state after grayscale data sync + renderer.storeBwBuffer(); + // grayscale rendering // TODO: Only do this if font supports it { @@ -315,6 +317,9 @@ void EpubReaderScreen::renderContents(std::unique_ptr page) { renderer.displayGrayBuffer(); renderer.setRenderMode(GfxRenderer::BW); } + + // restore the bw data + renderer.restoreBwBuffer(); } void EpubReaderScreen::renderStatusBar() const {