From 790e77c818377422d91e3289b75823a2f07b1665 Mon Sep 17 00:00:00 2001 From: Alexander Matthes Date: Sat, 15 Aug 2026 19:46:20 +0200 Subject: [PATCH 1/4] feat: Add FreeInkDisplay functions to work with absolute grayscale luts This is needed for using the absolute factory lut of the SS1677 on X4 without device specific information in crosspoint reader. Renamed supportsFactoryGrayscale to supportsAbsoluteGrayscale to make the difference more to the default differential approach more clear Co-authored-by: zgredex <112968378+zgredex@users.noreply.github.com> --- .../FreeInkDisplay/include/FreeInkDisplay.h | 3 +++ .../FreeInkDisplay/src/FreeInkDisplay.cpp | 17 +++++++++++++++++ .../FreeInkDisplay/src/driver/PanelDriver.h | 9 +++++++++ .../FreeInkDisplay/src/driver/Ssd1677Driver.cpp | 5 +++++ .../FreeInkDisplay/src/driver/Ssd1677Driver.h | 2 ++ 5 files changed, 36 insertions(+) diff --git a/libs/display/FreeInkDisplay/include/FreeInkDisplay.h b/libs/display/FreeInkDisplay/include/FreeInkDisplay.h index 20388339..85c14486 100644 --- a/libs/display/FreeInkDisplay/include/FreeInkDisplay.h +++ b/libs/display/FreeInkDisplay/include/FreeInkDisplay.h @@ -131,6 +131,8 @@ class FreeInkDisplay { bool supportsBusyGrayscaleStaging() const; void prepareGrayscaleTarget(); bool supportsStripGrayscale() const; + // True only when the runtime-selected panel driver accepts absolute LUT plane encoding. + bool supportsAbsoluteGrayscale() const; // True when displayGrayscaleBase() defers the base activation so the gray // planes join it in one waveform (Paper Mono) - see PanelDriver. bool combinesGrayscaleBase() const; @@ -255,6 +257,7 @@ class FreeInkDisplay { // EXPERIMENTAL: Windowed update - display only a rectangular region void displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool turnOffScreen = false); void displayGrayBuffer(bool turnOffScreen = false, const unsigned char* lut = nullptr, bool factoryMode = false); + void displayAbsoluteGrayBuffer(bool turnOffScreen = false); void displayGrayCalibration(uint16_t customX, uint16_t customY, uint16_t customW, uint16_t customH); void refreshDisplay(RefreshMode mode = FAST_REFRESH, bool turnOffScreen = false); diff --git a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp index 5970fd28..5465e251 100644 --- a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp +++ b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp @@ -796,6 +796,19 @@ void FreeInkDisplay::displayGrayBuffer(bool turnOffScreen, const unsigned char* _driver->displayGray(_bus, frameBuffer, turnOffScreen, lut, factoryMode); } +void FreeInkDisplay::displayAbsoluteGrayBuffer(bool turnOffScreen) { +#if defined(SSD1677_PROBE_DEBUG) && SSD1677_PROBE_DEBUG + Serial.printf("[EPD] displayAbsoluteGrayBuffer\n"); +#endif + // Inverted mode deliberately renders a crisp BW page. Writing normal + // grayscale planes afterward would partially undo the output inversion. + if (_inverted) return; + syncPendingAsync(); + _shadowValid = false; + _redRamSynced = false; // grayscale leaves RED holding a gray plane, not the BW baseline + _driver->displayGrayAbsolute(_bus, frameBuffer, turnOffScreen); +} + void FreeInkDisplay::displayGrayCalibration(uint16_t customX, uint16_t customY, uint16_t customW, uint16_t customH) { if (_inverted) return; syncPendingAsync(); @@ -871,6 +884,10 @@ bool FreeInkDisplay::supportsStripGrayscale() const { return !_inverted && _driver && _driver->supportsStripGrayscale(); } +bool FreeInkDisplay::supportsAbsoluteGrayscale() const { + return !_inverted && _driver && _driver->supportsAbsoluteGrayscale(); +} + bool FreeInkDisplay::combinesGrayscaleBase() const { return _driver && _driver->combinesGrayscaleBase(); } void FreeInkDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { diff --git a/libs/display/FreeInkDisplay/src/driver/PanelDriver.h b/libs/display/FreeInkDisplay/src/driver/PanelDriver.h index 1318b971..986ae3c3 100644 --- a/libs/display/FreeInkDisplay/src/driver/PanelDriver.h +++ b/libs/display/FreeInkDisplay/src/driver/PanelDriver.h @@ -111,6 +111,11 @@ class PanelDriver { // grayscale pass. Drivers with a dedicated grayscale-base waveform must // override this even when their normal display path supports deferral. virtual bool supportsAsyncGrayscaleBase() const { return false; } + + // True when this controller accepts absolute selector planes via displayGrayAbsolute() + // This is a runtime capability because one firmware image may include several drivers + // and driver configurations and select the controller during boot. + virtual bool supportsAbsoluteGrayscale() const { return false; } // True when displayGrayscaleBase() DEFERS the base activation so the gray // planes join it in a single waveform (Paper Mono). Hosts should then route the // grayscale base through displayGrayscaleBase() instead of display(): a @@ -163,6 +168,10 @@ class PanelDriver { (void)factoryMode; display(bus, fb, nullptr, RefreshMode::Fast, turnOff); } + virtual void displayGrayAbsolute(EpdBus& bus, const uint8_t* fb, bool turnOff) + { + displayGray(bus, fb, turnOff, nullptr, false); + } // Diagnostic four-gray comparison. The full frame is first rendered with // the controller's flashing OTP waveform; `custom*` is then rebuilt with the // driver's non-flashing grayscale path. Default drivers keep the OTP frame. diff --git a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp index 6041d623..188805ef 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp @@ -599,6 +599,11 @@ void Ssd1677Driver::displayGray(EpdBus& bus, const uint8_t* fb, bool turnOff, co setCustomLut(bus, false, nullptr); } +void Ssd1677Driver::displayGrayAbsolute(EpdBus& bus, const uint8_t* fb, bool turnOff) +{ + displayGray(bus, fb, turnOff, lut_factory_quality, true); +} + void Ssd1677Driver::cleanupGrayscaleBuffers(EpdBus& bus, const uint8_t* bw) { if (!bw) return; setRamArea(bus, 0, 0, _w, _h); diff --git a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h index 8f49284b..398c2214 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h +++ b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h @@ -86,11 +86,13 @@ class Ssd1677Driver : public PanelDriver { bool supportsStripGrayscale() const override { return true; } bool supportsAsyncGrayscaleBase() const override { return true; } + bool supportsAbsoluteGrayscale() const override { return true; } void copyGrayscaleLsb(EpdBus& bus, const uint8_t* lsb) override; void copyGrayscaleMsb(EpdBus& bus, const uint8_t* msb) override; void writeGrayscalePlaneStrip(EpdBus& bus, GrayPlane plane, const uint8_t* rows, uint16_t yStart, uint16_t numRows) override; void displayGray(EpdBus& bus, const uint8_t* fb, bool turnOff, const unsigned char* lut, bool factoryMode) override; + void displayGrayAbsolute(EpdBus& bus, const uint8_t* fb, bool turnOff) override; void cleanupGrayscaleBuffers(EpdBus& bus, const uint8_t* bw) override; // No grayscaleRevert override. Grayscale exits via cleanupGrayscaleBuffers From 545e9fe984ecc25ca2acece76b3e199825615daf Mon Sep 17 00:00:00 2001 From: Alexander Matthes Date: Sun, 16 Aug 2026 20:05:26 +0200 Subject: [PATCH 2/4] fix: enable absolute grayscale lut support only for X4 with SSD1677, not for sticky Co-authored-by: zgredex <112968378+zgredex@users.noreply.github.com> --- libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp | 2 ++ libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp index 188805ef..d2424887 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp @@ -65,6 +65,8 @@ const Ssd1677Config& ssd1677DefaultConfig() { 0xC0, // borderWaveformGray: written explicitly with the external AA LUT (vendor // reference stage 2); same value the B/W paths leave in the register, so // the wire state is unchanged — just no longer relying on carry-over + false, // grayPowerUpFirst + true, // enableAbsoluteLut }; return cfg; } diff --git a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h index 398c2214..a9c42e6a 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h +++ b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h @@ -53,6 +53,7 @@ struct Ssd1677Config { // collapsing toward B/W). The X4 keeps the panel powered between fast // refreshes, so it never needs this and keeps stock behavior. bool grayPowerUpFirst = false; + bool enableAbsoluteLut = false; }; // Standard config (Xteink X4 / GDEQ0426T82). Panel mounting (mirror/180°) is NOT @@ -86,7 +87,7 @@ class Ssd1677Driver : public PanelDriver { bool supportsStripGrayscale() const override { return true; } bool supportsAsyncGrayscaleBase() const override { return true; } - bool supportsAbsoluteGrayscale() const override { return true; } + bool supportsAbsoluteGrayscale() const override { return _cfg.enableAbsoluteLut; } void copyGrayscaleLsb(EpdBus& bus, const uint8_t* lsb) override; void copyGrayscaleMsb(EpdBus& bus, const uint8_t* msb) override; void writeGrayscalePlaneStrip(EpdBus& bus, GrayPlane plane, const uint8_t* rows, uint16_t yStart, From d8db8fad459ebd79528e150c34def6d091a3dcb2 Mon Sep 17 00:00:00 2001 From: Alexander Matthes Date: Wed, 2 Sep 2026 15:31:01 +0200 Subject: [PATCH 3/4] feat: Activate absolute lut rendering for sticky Co-authored-by: zgredex <112968378+zgredex@users.noreply.github.com> --- libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp index d2424887..8533dc59 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp @@ -97,6 +97,7 @@ static const Ssd1677Config& ssd1677StickyConfig() { // black under the grayscale LUT (black frame on every AA/cover refresh) true, // grayPowerUpFirst: vendor sequences power down after every refresh, so // settle the rails before the short gray LUT phases (see Ssd1677Config) + true, // enableAbsoluteLut }; return cfg; } From 511664ead067e686723ab84b87dd2ea710759e13 Mon Sep 17 00:00:00 2001 From: Alexander Matthes Date: Wed, 9 Sep 2026 14:19:28 +0200 Subject: [PATCH 4/4] fix: Avoid rare incomplete absolute lut rendering Co-authored-by: zgredex <112968378+zgredex@users.noreply.github.com> --- libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp index 8533dc59..78f51362 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp @@ -586,11 +586,13 @@ void Ssd1677Driver::displayGray(EpdBus& bus, const uint8_t* fb, bool turnOff, co // ignore RED RAM and break 4-level grayscale. bus.cmd(CMD_DISPLAY_UPDATE_CTRL1); bus.data(CTRL1_NORMAL); + // Keep rails on after the factory gray paint. Running ANALOG_OFF/CLOCK_OFF in + // the same activation can disturb settled gray particles and reintroduce smear. bus.cmd(CMD_DISPLAY_UPDATE_CTRL2); - bus.data(0xC7); // CLOCK_ON|ANALOG_ON|DISPLAY_START|ANALOG_OFF|CLOCK_OFF + bus.data(0xCC); // CLOCK_ON|ANALOG_ON|MODE_SELECT|DISPLAY_START bus.cmd(CMD_MASTER_ACTIVATION); - bus.waitBusy("factory_gray"); - _isScreenOn = false; // 0xC7 always powers down after the update + bus.waitRefreshComplete("factory_gray"); + _isScreenOn = true; } else { // Settled rails before the gray waveform (no-op where the panel is already // on, i.e. the X4's fast path). refresh() then runs the 0xCC external-LUT