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();