From 0fda9031fdf05757432908487ff48166db61d290 Mon Sep 17 00:00:00 2001 From: cottongin Date: Thu, 19 Feb 2026 11:33:45 -0500 Subject: [PATCH] fix: Use double FAST_REFRESH for dithered letterbox sleep covers Replace HALF_REFRESH with double FAST_REFRESH technique for the BW pass when dithered letterbox fill is active. This avoids the e-ink crosstalk and image corruption that occurred when HALF_REFRESH drove large areas of dithered gray pixels simultaneously. Revert the hash-based block dithering workaround (bayerCrossesBwBoundary, hashBlockDither) back to standard Bayer dithering for all gray ranges, since the root cause was HALF_REFRESH rather than the dithering pattern itself. Letterbox fill is now included in all three render passes (BW, LSB, MSB) so the greyscale LUT treats letterbox pixels identically to cover pixels, maintaining color-matched edges. Co-authored-by: Cursor --- src/activities/boot_sleep/SleepActivity.cpp | 127 ++++++-------------- 1 file changed, 38 insertions(+), 89 deletions(-) diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 7aaf0e62..400cb55b 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -21,6 +21,11 @@ #include "util/BookSettings.h" #include "util/StringUtils.h" +// Sleep cover refresh strategy when dithered letterbox fill is active: +// 1 = Double FAST_REFRESH (clear to white, then render content -- avoids HALF_REFRESH crosstalk) +// 0 = Standard HALF_REFRESH (original behavior) +#define USE_SLEEP_DOUBLE_FAST_REFRESH 1 + namespace { // Number of source pixels along the image edge to average for the dominant color @@ -74,37 +79,6 @@ uint8_t quantizeBayerDither(int gray, int x, int y) { } } -// Check whether a gray value would produce a dithered mix that crosses the -// level-2 / level-3 boundary. This is the ONLY boundary where some dithered -// pixels are BLACK (level ≤ 2) and others are WHITE (level 3) in the BW pass, -// creating a high-frequency checkerboard that causes e-ink display crosstalk -// and washes out adjacent content during HALF_REFRESH. -// Gray values 171-254 produce a level-2/level-3 mix via Bayer dithering. -bool bayerCrossesBwBoundary(uint8_t gray) { return gray > 170 && gray < 255; } - -// Hash-based block dithering for BW-boundary gray values (171-254). -// Each blockSize×blockSize pixel block gets a single uniform level (2 or 3), -// determined by a deterministic spatial hash. The proportion of level-3 blocks -// approximates the target gray. Unlike Bayer, the pattern is irregular -// (noise-like), making it much less visually obvious at the same block size. -// The hash is purely spatial (depends only on x, y, blockSize) so it produces -// identical levels across BW, LSB, and MSB render passes. -static constexpr int BW_DITHER_BLOCK = 2; - -uint8_t hashBlockDither(uint8_t avg, int x, int y) { - const int bx = x / BW_DITHER_BLOCK; - const int by = y / BW_DITHER_BLOCK; - // Fast mixing hash (splitmix32-inspired) - uint32_t h = (uint32_t)bx * 2654435761u ^ (uint32_t)by * 2246822519u; - h ^= h >> 16; - h *= 0x45d9f3bu; - h ^= h >> 16; - // Proportion of level-3 blocks needed to approximate the target gray - const float ratio = (avg - 170.0f) / 85.0f; - const uint32_t threshold = (uint32_t)(ratio * 4294967295.0f); - return (h < threshold) ? 3 : 2; -} - // --- Edge average cache --- // Caches the computed edge averages alongside the cover BMP so we don't rescan on every sleep. constexpr uint8_t EDGE_CACHE_VERSION = 2; @@ -278,19 +252,6 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin const bool isSolid = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_SOLID); - // For DITHERED mode with gray values in 171-254 (the level-2/level-3 BW boundary): - // Pixel-level Bayer dithering creates a regular high-frequency checkerboard in - // the BW pass that causes e-ink display crosstalk during HALF_REFRESH. - // - // Solution: HASH-BASED BLOCK DITHERING. Each 2x2 pixel block gets a single - // level (2 or 3) determined by a spatial hash, with the proportion of level-3 - // blocks tuned to approximate the target gray. The 2px minimum run avoids BW - // crosstalk, and the irregular hash pattern is much less visible than a regular - // Bayer grid at the same block size. - const bool hashA = !isSolid && bayerCrossesBwBoundary(data.avgA); - const bool hashB = !isSolid && bayerCrossesBwBoundary(data.avgB); - - // For solid mode: snap to nearest e-ink level const uint8_t levelA = isSolid ? snapToEinkLevel(data.avgA) / 85 : 0; const uint8_t levelB = isSolid ? snapToEinkLevel(data.avgB) / 85 : 0; @@ -298,13 +259,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin if (data.letterboxA > 0) { for (int y = 0; y < data.letterboxA; y++) for (int x = 0; x < renderer.getScreenWidth(); x++) { - uint8_t lv; - if (isSolid) - lv = levelA; - else if (hashA) - lv = hashBlockDither(data.avgA, x, y); - else - lv = quantizeBayerDither(data.avgA, x, y); + const uint8_t lv = isSolid ? levelA : quantizeBayerDither(data.avgA, x, y); renderer.drawPixelGray(x, y, lv); } } @@ -312,13 +267,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin const int start = renderer.getScreenHeight() - data.letterboxB; for (int y = start; y < renderer.getScreenHeight(); y++) for (int x = 0; x < renderer.getScreenWidth(); x++) { - uint8_t lv; - if (isSolid) - lv = levelB; - else if (hashB) - lv = hashBlockDither(data.avgB, x, y); - else - lv = quantizeBayerDither(data.avgB, x, y); + const uint8_t lv = isSolid ? levelB : quantizeBayerDither(data.avgB, x, y); renderer.drawPixelGray(x, y, lv); } } @@ -326,13 +275,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin if (data.letterboxA > 0) { for (int x = 0; x < data.letterboxA; x++) for (int y = 0; y < renderer.getScreenHeight(); y++) { - uint8_t lv; - if (isSolid) - lv = levelA; - else if (hashA) - lv = hashBlockDither(data.avgA, x, y); - else - lv = quantizeBayerDither(data.avgA, x, y); + const uint8_t lv = isSolid ? levelA : quantizeBayerDither(data.avgA, x, y); renderer.drawPixelGray(x, y, lv); } } @@ -340,13 +283,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin const int start = renderer.getScreenWidth() - data.letterboxB; for (int x = start; x < renderer.getScreenWidth(); x++) for (int y = 0; y < renderer.getScreenHeight(); y++) { - uint8_t lv; - if (isSolid) - lv = levelB; - else if (hashB) - lv = hashBlockDither(data.avgB, x, y); - else - lv = quantizeBayerDither(data.avgB, x, y); + const uint8_t lv = isSolid ? levelB : quantizeBayerDither(data.avgB, x, y); renderer.drawPixelGray(x, y, lv); } } @@ -543,35 +480,47 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str const bool hasGreyscale = bitmap.hasGreyscale() && SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER; - // Draw letterbox fill (BW pass) - if (fillData.valid) { - drawLetterboxFill(renderer, fillData, fillMode); + const bool isInverted = + SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE; + +#if USE_SLEEP_DOUBLE_FAST_REFRESH + const bool useDoubleFast = + fillData.valid && fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_DITHERED; +#else + const bool useDoubleFast = false; +#endif + + if (useDoubleFast) { + // Double FAST_REFRESH technique: avoids HALF_REFRESH crosstalk with dithered letterbox. + // Pass 1: clear to white baseline + renderer.clearScreen(); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + + // Pass 2: render actual content and display + if (fillData.valid) drawLetterboxFill(renderer, fillData, fillMode); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); + if (isInverted) renderer.invertScreen(); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + } else { + // Standard path: single HALF_REFRESH + if (fillData.valid) drawLetterboxFill(renderer, fillData, fillMode); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); + if (isInverted) renderer.invertScreen(); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } - renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); - - if (SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE) { - renderer.invertScreen(); - } - - renderer.displayBuffer(HalDisplay::HALF_REFRESH); - if (hasGreyscale) { bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - if (fillData.valid) { - drawLetterboxFill(renderer, fillData, fillMode); - } + if (fillData.valid) drawLetterboxFill(renderer, fillData, fillMode); renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); renderer.copyGrayscaleLsbBuffers(); bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - if (fillData.valid) { - drawLetterboxFill(renderer, fillData, fillMode); - } + if (fillData.valid) drawLetterboxFill(renderer, fillData, fillMode); renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); renderer.copyGrayscaleMsbBuffers();