From f6e59aab72db77dbd65740e633a2ae6330dfea05 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Wed, 24 Jun 2026 07:53:19 -0400 Subject: [PATCH] feat: Add displayGrayscaleBase method for differential refresh (#2334) Introduces a new display method that prepares the framebuffer as a base frame for grayscale overlays. On X3 panels, this uses the OEM differential base waveform (AA-pre-BW) without forcing a resync. Other panels fall back to normal display with configurable refresh mode. Dependent upon matching SDK commit to work --- lib/GfxRenderer/GfxRenderer.cpp | 24 +++++++++++++++++++++ lib/GfxRenderer/GfxRenderer.h | 10 +++++++++ lib/hal/HalDisplay.cpp | 10 +++++++++ lib/hal/HalDisplay.h | 13 +++++++++++ open-x4-sdk | 2 +- src/activities/boot_sleep/SleepActivity.cpp | 10 ++++++++- src/activities/reader/XtcReaderActivity.cpp | 14 +++++++++++- 7 files changed, 80 insertions(+), 3 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index fd89bbc6..ddd5f1e4 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -1587,6 +1587,30 @@ size_t GfxRenderer::getBufferSize() const { return frameBufferSize; } // unused // void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); } +void GfxRenderer::displayGrayscaleBase(HalDisplay::RefreshMode fallback) const { + display.displayGrayscaleBase(fallback, fadingFix); +} + +void GfxRenderer::preconditionGrayscale() const { display.preconditionGrayscale(); } + +void GfxRenderer::preconditionGrayscale(int x, int y, int w, int h) const { + if (w <= 0 || h <= 0) return; + // Rotate the logical rect's opposite corners to physical panel coords; the + // physical bbox stays axis-aligned for all four orientations. + int ax, ay, bx, by; + rotateCoordinates(orientation, x, y, &ax, &ay, panelWidth, panelHeight); + rotateCoordinates(orientation, x + w - 1, y + h - 1, &bx, &by, panelWidth, panelHeight); + int x0 = ax < bx ? ax : bx, x1 = ax > bx ? ax : bx; + int y0 = ay < by ? ay : by, y1 = ay > by ? ay : by; + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 >= panelWidth) x1 = panelWidth - 1; + if (y1 >= panelHeight) y1 = panelHeight - 1; + if (x1 < x0 || y1 < y0) return; + display.preconditionGrayscale(static_cast(x0), static_cast(y0), + static_cast(x1 - x0 + 1), static_cast(y1 - y0 + 1)); +} + void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); } void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 924e5c0b..471c0a8f 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -218,6 +218,16 @@ class GfxRenderer { // Grayscale functions void setRenderMode(const RenderMode mode) { this->renderMode = mode; } RenderMode getRenderMode() const { return renderMode; } + // Grayscale preconditioning settle pass (no-op on X4). The rect overload + // takes the gray region in LOGICAL screen coordinates and rotates it to the + // panel; the no-arg overload settles the full frame. Call after the BW base + // frame is displayed and before the grayscale planes are written. + void preconditionGrayscale() const; + void preconditionGrayscale(int x, int y, int w, int h) const; + // Display the framebuffer as the base frame for a grayscale overlay that + // follows (X3: OEM differential base waveform; others: plain display with + // `fallback`). + void displayGrayscaleBase(HalDisplay::RefreshMode fallback = HalDisplay::HALF_REFRESH) const; void copyGrayscaleLsbBuffers() const; void copyGrayscaleMsbBuffers() const; void displayGrayBuffer() const; diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index 11f08758..e3a5b644 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -81,6 +81,16 @@ void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* m einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer); } +void HalDisplay::displayGrayscaleBase(RefreshMode fallback, bool turnOffScreen) { + einkDisplay.displayGrayscaleBase(convertRefreshMode(fallback), turnOffScreen); +} + +void HalDisplay::preconditionGrayscale() { einkDisplay.preconditionGrayscale(); } + +void HalDisplay::preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + einkDisplay.preconditionGrayscale(x, y, w, h); +} + void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); } void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); } diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h index 628296cb..9b35254a 100644 --- a/lib/hal/HalDisplay.h +++ b/lib/hal/HalDisplay.h @@ -47,6 +47,19 @@ class HalDisplay { // Access to frame buffer uint8_t* getFrameBuffer() const; + // X3 grayscale preconditioning (OEM "AA-pre-BW(mid)" settle pass), windowed + // to the gray region in physical panel coordinates (no-arg = full frame). + // Call after the BW base frame is displayed and before the grayscale planes + // are written; no-op on X4. See EInkDisplay::preconditionGrayscale. + void preconditionGrayscale(); + void preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + + // Display the framebuffer as the base frame for a grayscale overlay that + // follows. X3 uses the OEM differential base waveform ("AA-pre-BW(mid)"); + // other panels display normally with `fallback` mode (previous behavior). + // Deliberately does NOT force the X3 resync that displayBuffer(HALF) does. + void displayGrayscaleBase(RefreshMode fallback = HALF_REFRESH, bool turnOffScreen = false); + void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer); void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer); void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer); diff --git a/open-x4-sdk b/open-x4-sdk index 26648d64..198ad267 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit 26648d643a1c883ab2f71e1869d05fe2a0c9d498 +Subproject commit 198ad267219c25c8ab84418b806c66f1fb5216a3 diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 86fc6ab2..fb6c188b 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -218,7 +218,15 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { renderer.invertScreen(); } - renderer.displayBuffer(HalDisplay::HALF_REFRESH); + if (hasGreyscale) { + // OEM grayscale pipeline base: on X3 this displays the frame with the + // dedicated "AA-pre-BW(mid)" differential waveform, leaving every pixel + // in the calibrated state the gray nudge refresh expects; on X4 it is a + // plain HALF refresh (previous behavior). + renderer.displayGrayscaleBase(HalDisplay::HALF_REFRESH); + } else { + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + } if (hasGreyscale) { bitmap.rewindToData(); diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index c2306aa8..d4af3e9b 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -294,7 +294,19 @@ void XtcReaderActivity::renderPage() { } } - ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh); + if (pagesUntilFullRefresh <= 1) { + // Periodic ghost cleanup: scrub via the normal path, then run the + // settle flavor of the grayscale base pass (DTM planes are equal after + // the display sync, so only the gentle reinforcement cells fire). + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + renderer.preconditionGrayscale(); + pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); + } else { + // OEM grayscale pipeline base: differential "AA-pre-BW(mid)" update as + // the page turn on X3; plain FAST refresh on X4 (previous behavior). + renderer.displayGrayscaleBase(HalDisplay::FAST_REFRESH); + pagesUntilFullRefresh--; + } // Pass 2: LSB buffer - mark DARK gray only (XTH value 1) // In LUT: 0 bit = apply gray effect, 1 bit = untouched