From ea11d2f7d32d0dbcedaea9dc139981d0f894b69a Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 13 Feb 2026 11:12:27 -0500 Subject: [PATCH] refactor: Revert letterbox fill to Dithered/Solid/None with edge caching Simplify letterbox fill modes back to Dithered (default), Solid, and None. Remove the Extend Edges mode and all per-pixel edge replication code. Restore Bayer ordered dithering for the Dithered fill mode. Re-introduce edge average caching so cover edge computations persist across sleep cycles, stored as a small binary file alongside the cover BMP. Co-authored-by: Cursor --- src/CrossPointSettings.cpp | 5 +- src/CrossPointSettings.h | 12 +- src/SettingsList.h | 4 +- src/activities/boot_sleep/SleepActivity.cpp | 371 ++++++++------------ src/activities/boot_sleep/SleepActivity.h | 1 + 5 files changed, 149 insertions(+), 244 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 1ecf6234..df1fb9c7 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -22,7 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 32; +constexpr uint8_t SETTINGS_COUNT = 31; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; // Validate front button mapping to ensure each hardware button is unique. @@ -119,7 +119,6 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, fadingFix); serialization::writePod(outputFile, embeddedStyle); serialization::writePod(outputFile, sleepScreenLetterboxFill); - serialization::writePod(outputFile, sleepScreenGradientDir); // New fields added at end for backward compatibility outputFile.close(); @@ -227,7 +226,7 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, sleepScreenLetterboxFill, SLEEP_SCREEN_LETTERBOX_FILL_COUNT); if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, sleepScreenGradientDir, SLEEP_SCREEN_GRADIENT_DIR_COUNT); + { uint8_t _ignore; serialization::readPod(inputFile, _ignore); } // legacy: sleepScreenGradientDir if (++settingsRead >= fileSettingsCount) break; // New fields added at end for backward compatibility } while (false); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 317c19d5..a5dabdf2 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -32,13 +32,11 @@ class CrossPointSettings { SLEEP_SCREEN_COVER_FILTER_COUNT }; enum SLEEP_SCREEN_LETTERBOX_FILL { - LETTERBOX_NONE = 0, + LETTERBOX_DITHERED = 0, LETTERBOX_SOLID = 1, - LETTERBOX_BLENDED = 2, - LETTERBOX_GRADIENT = 3, + LETTERBOX_NONE = 2, SLEEP_SCREEN_LETTERBOX_FILL_COUNT }; - enum SLEEP_SCREEN_GRADIENT_DIR { GRADIENT_TO_WHITE = 0, GRADIENT_TO_BLACK = 1, SLEEP_SCREEN_GRADIENT_DIR_COUNT }; // Status bar display type enum enum STATUS_BAR_MODE { @@ -133,10 +131,8 @@ class CrossPointSettings { uint8_t sleepScreenCoverMode = FIT; // Sleep screen cover filter uint8_t sleepScreenCoverFilter = NO_FILTER; - // Sleep screen letterbox fill mode (None / Solid / Blended / Gradient) - uint8_t sleepScreenLetterboxFill = LETTERBOX_GRADIENT; - // Sleep screen gradient direction (towards white or black) - uint8_t sleepScreenGradientDir = GRADIENT_TO_WHITE; + // Sleep screen letterbox fill mode (Dithered / Solid / None) + uint8_t sleepScreenLetterboxFill = LETTERBOX_DITHERED; // Status bar settings uint8_t statusBar = FULL; // Text rendering settings diff --git a/src/SettingsList.h b/src/SettingsList.h index fbf0f5f8..7c4db46d 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -19,9 +19,7 @@ inline std::vector getSettingsList() { SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter, {"None", "Contrast", "Inverted"}, "sleepScreenCoverFilter", "Display"), SettingInfo::Enum("Letterbox Fill", &CrossPointSettings::sleepScreenLetterboxFill, - {"None", "Solid", "Blended", "Gradient"}, "sleepScreenLetterboxFill", "Display"), - SettingInfo::Enum("Gradient Direction", &CrossPointSettings::sleepScreenGradientDir, {"To White", "To Black"}, - "sleepScreenGradientDir", "Display"), + {"Dithered", "Solid", "None"}, "sleepScreenLetterboxFill", "Display"), SettingInfo::Enum( "Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full w/ Percentage", "Full w/ Book Bar", "Book Bar Only", "Full w/ Chapter Bar"}, diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 879c5972..6f41338c 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -1,6 +1,5 @@ #include "SleepActivity.h" -#include #include #include #include @@ -9,6 +8,7 @@ #include #include +#include #include "CrossPointSettings.h" #include "CrossPointState.h" @@ -19,36 +19,62 @@ namespace { -// Number of source pixels along the image edge to average for the gradient color +// Number of source pixels along the image edge to average for the dominant color constexpr int EDGE_SAMPLE_DEPTH = 20; // Map a 2-bit quantized pixel value to an 8-bit grayscale value constexpr uint8_t val2bitToGray(uint8_t val2bit) { return val2bit * 85; } -// Edge gradient data produced by sampleBitmapEdges and consumed by drawLetterboxGradients. -// edgeA is the "first" edge (top or left), edgeB is the "second" edge (bottom or right). -struct LetterboxGradientData { - uint8_t* edgeA = nullptr; - uint8_t* edgeB = nullptr; - int edgeCount = 0; +// Letterbox fill data: one average gray value per edge (top/bottom or left/right). +struct LetterboxFillData { + uint8_t avgA = 128; // average gray of edge A (top or left) + uint8_t avgB = 128; // average gray of edge B (bottom or right) int letterboxA = 0; // pixel size of the first letterbox area (top or left) int letterboxB = 0; // pixel size of the second letterbox area (bottom or right) bool horizontal = false; // true = top/bottom letterbox, false = left/right - - void free() { - ::free(edgeA); - ::free(edgeB); - edgeA = nullptr; - edgeB = nullptr; - } + bool valid = false; }; -// Binary cache version for edge data files -constexpr uint8_t EDGE_CACHE_VERSION = 1; +// Snap an 8-bit gray value to the nearest of the 4 e-ink levels: 0, 85, 170, 255. +uint8_t snapToEinkLevel(uint8_t gray) { + // Thresholds at midpoints: 42, 127, 212 + if (gray < 43) return 0; + if (gray < 128) return 85; + if (gray < 213) return 170; + return 255; +} -// Load cached edge data from a binary file. Returns true if the cache was valid and loaded successfully. -// Validates cache version and screen dimensions to detect stale data. -bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, LetterboxGradientData& data) { +// 4x4 Bayer ordered dithering matrix, values 0-255. +// Produces a structured halftone pattern for 4-level quantization. +// clang-format off +constexpr uint8_t BAYER_4X4[4][4] = { + { 0, 128, 32, 160}, + {192, 64, 224, 96}, + { 48, 176, 16, 144}, + {240, 112, 208, 80} +}; +// clang-format on + +// Ordered (Bayer) dithering for 4-level e-ink display. +// Maps an 8-bit gray value to a 2-bit level (0-3) using the Bayer matrix +// to produce a structured, repeating halftone pattern. +uint8_t quantizeBayerDither(int gray, int x, int y) { + const int threshold = BAYER_4X4[y & 3][x & 3]; + const int scaled = gray * 3; + if (scaled < 255) { + return (scaled + threshold >= 255) ? 1 : 0; + } else if (scaled < 510) { + return ((scaled - 255) + threshold >= 255) ? 2 : 1; + } else { + return ((scaled - 510) + threshold >= 255) ? 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; + +bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, LetterboxFillData& data) { FsFile file; if (!Storage.openFileForRead("SLP", path, file)) return false; @@ -71,9 +97,8 @@ bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, L serialization::readPod(file, horizontal); data.horizontal = (horizontal != 0); - uint16_t edgeCount; - serialization::readPod(file, edgeCount); - data.edgeCount = edgeCount; + serialization::readPod(file, data.avgA); + serialization::readPod(file, data.avgB); int16_t lbA, lbB; serialization::readPod(file, lbA); @@ -81,34 +106,15 @@ bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, L data.letterboxA = lbA; data.letterboxB = lbB; - if (edgeCount == 0 || edgeCount > 2048) { - file.close(); - return false; - } - - data.edgeA = static_cast(malloc(edgeCount)); - data.edgeB = static_cast(malloc(edgeCount)); - if (!data.edgeA || !data.edgeB) { - data.free(); - file.close(); - return false; - } - - if (file.read(data.edgeA, edgeCount) != static_cast(edgeCount) || - file.read(data.edgeB, edgeCount) != static_cast(edgeCount)) { - data.free(); - file.close(); - return false; - } - file.close(); - Serial.printf("[%lu] [SLP] Loaded edge cache from %s (%d edges)\n", millis(), path.c_str(), edgeCount); + data.valid = true; + Serial.printf("[%lu] [SLP] Loaded edge cache from %s (avgA=%d, avgB=%d)\n", millis(), path.c_str(), data.avgA, + data.avgB); return true; } -// Save edge data to a binary cache file for reuse on subsequent sleep screens. -bool saveEdgeCache(const std::string& path, int screenWidth, int screenHeight, const LetterboxGradientData& data) { - if (!data.edgeA || !data.edgeB || data.edgeCount <= 0) return false; +bool saveEdgeCache(const std::string& path, int screenWidth, int screenHeight, const LetterboxFillData& data) { + if (!data.valid) return false; FsFile file; if (!Storage.openFileForWrite("SLP", path, file)) return false; @@ -117,23 +123,22 @@ bool saveEdgeCache(const std::string& path, int screenWidth, int screenHeight, c serialization::writePod(file, static_cast(screenWidth)); serialization::writePod(file, static_cast(screenHeight)); serialization::writePod(file, static_cast(data.horizontal ? 1 : 0)); - serialization::writePod(file, static_cast(data.edgeCount)); + serialization::writePod(file, data.avgA); + serialization::writePod(file, data.avgB); serialization::writePod(file, static_cast(data.letterboxA)); serialization::writePod(file, static_cast(data.letterboxB)); - file.write(data.edgeA, data.edgeCount); - file.write(data.edgeB, data.edgeCount); file.close(); - Serial.printf("[%lu] [SLP] Saved edge cache to %s (%d edges)\n", millis(), path.c_str(), data.edgeCount); + Serial.printf("[%lu] [SLP] Saved edge cache to %s\n", millis(), path.c_str()); return true; } -// Read the bitmap once to sample the first/last EDGE_SAMPLE_DEPTH rows or columns. -// Returns edge color arrays in source pixel resolution. Caller must call data.free() when done. +// Read the bitmap once to compute a single average gray value for the top/bottom or left/right edges. +// Only computes running sums -- no per-pixel arrays, no malloc beyond row buffers. // After sampling the bitmap is rewound via rewindToData(). -LetterboxGradientData sampleBitmapEdges(const Bitmap& bitmap, int imgX, int imgY, int pageWidth, int pageHeight, - float scale, float cropX, float cropY) { - LetterboxGradientData data; +LetterboxFillData computeEdgeAverages(const Bitmap& bitmap, int imgX, int imgY, int pageWidth, int pageHeight, + float scale, float cropX, float cropY) { + LetterboxFillData data; const int cropPixX = static_cast(std::floor(bitmap.getWidth() * cropX / 2.0f)); const int cropPixY = static_cast(std::floor(bitmap.getHeight() * cropY / 2.0f)); @@ -146,35 +151,22 @@ LetterboxGradientData sampleBitmapEdges(const Bitmap& bitmap, int imgX, int imgY auto* outputRow = static_cast(malloc(outputRowSize)); auto* rowBytes = static_cast(malloc(bitmap.getRowBytes())); if (!outputRow || !rowBytes) { - ::free(outputRow); - ::free(rowBytes); + free(outputRow); + free(rowBytes); return data; } if (imgY > 0) { - // Top/bottom letterboxing -- sample per-column averages of first/last N rows + // Top/bottom letterboxing -- compute overall average of first/last EDGE_SAMPLE_DEPTH rows data.horizontal = true; - data.edgeCount = visibleWidth; const int scaledHeight = static_cast(std::round(static_cast(visibleHeight) * scale)); data.letterboxA = imgY; data.letterboxB = pageHeight - imgY - scaledHeight; if (data.letterboxB < 0) data.letterboxB = 0; const int sampleRows = std::min(EDGE_SAMPLE_DEPTH, visibleHeight); - - auto* accumTop = static_cast(calloc(visibleWidth, sizeof(uint32_t))); - auto* accumBot = static_cast(calloc(visibleWidth, sizeof(uint32_t))); - data.edgeA = static_cast(malloc(visibleWidth)); - data.edgeB = static_cast(malloc(visibleWidth)); - - if (!accumTop || !accumBot || !data.edgeA || !data.edgeB) { - ::free(accumTop); - ::free(accumBot); - data.free(); - ::free(outputRow); - ::free(rowBytes); - return data; - } + uint64_t sumTop = 0, sumBot = 0; + int countTop = 0, countBot = 0; for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) { if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) break; @@ -187,188 +179,106 @@ LetterboxGradientData sampleBitmapEdges(const Bitmap& bitmap, int imgX, int imgY if (!inTop && !inBot) continue; for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) { - const int outX = bmpX - cropPixX; const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3; const uint8_t gray = val2bitToGray(val); - if (inTop) accumTop[outX] += gray; - if (inBot) accumBot[outX] += gray; + if (inTop) { + sumTop += gray; + countTop++; + } + if (inBot) { + sumBot += gray; + countBot++; + } } } - for (int i = 0; i < visibleWidth; i++) { - data.edgeA[i] = static_cast(accumTop[i] / sampleRows); - data.edgeB[i] = static_cast(accumBot[i] / sampleRows); - } - ::free(accumTop); - ::free(accumBot); + data.avgA = countTop > 0 ? static_cast(sumTop / countTop) : 128; + data.avgB = countBot > 0 ? static_cast(sumBot / countBot) : 128; + data.valid = true; } else if (imgX > 0) { - // Left/right letterboxing -- sample per-row averages of first/last N columns + // Left/right letterboxing -- compute overall average of first/last EDGE_SAMPLE_DEPTH columns data.horizontal = false; - data.edgeCount = visibleHeight; const int scaledWidth = static_cast(std::round(static_cast(visibleWidth) * scale)); data.letterboxA = imgX; data.letterboxB = pageWidth - imgX - scaledWidth; if (data.letterboxB < 0) data.letterboxB = 0; const int sampleCols = std::min(EDGE_SAMPLE_DEPTH, visibleWidth); - - auto* accumLeft = static_cast(calloc(visibleHeight, sizeof(uint32_t))); - auto* accumRight = static_cast(calloc(visibleHeight, sizeof(uint32_t))); - data.edgeA = static_cast(malloc(visibleHeight)); - data.edgeB = static_cast(malloc(visibleHeight)); - - if (!accumLeft || !accumRight || !data.edgeA || !data.edgeB) { - ::free(accumLeft); - ::free(accumRight); - data.free(); - ::free(outputRow); - ::free(rowBytes); - return data; - } + uint64_t sumLeft = 0, sumRight = 0; + int countLeft = 0, countRight = 0; for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) { if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) break; const int logicalY = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY; if (logicalY < cropPixY || logicalY >= bitmap.getHeight() - cropPixY) continue; - const int outY = logicalY - cropPixY; - // Sample left edge columns for (int bmpX = cropPixX; bmpX < cropPixX + sampleCols; bmpX++) { const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3; - accumLeft[outY] += val2bitToGray(val); + sumLeft += val2bitToGray(val); + countLeft++; } - // Sample right edge columns for (int bmpX = bitmap.getWidth() - cropPixX - sampleCols; bmpX < bitmap.getWidth() - cropPixX; bmpX++) { const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3; - accumRight[outY] += val2bitToGray(val); + sumRight += val2bitToGray(val); + countRight++; } } - for (int i = 0; i < visibleHeight; i++) { - data.edgeA[i] = static_cast(accumLeft[i] / sampleCols); - data.edgeB[i] = static_cast(accumRight[i] / sampleCols); - } - ::free(accumLeft); - ::free(accumRight); + data.avgA = countLeft > 0 ? static_cast(sumLeft / countLeft) : 128; + data.avgB = countRight > 0 ? static_cast(sumRight / countRight) : 128; + data.valid = true; } - ::free(outputRow); - ::free(rowBytes); + free(outputRow); + free(rowBytes); bitmap.rewindToData(); return data; } -// Draw dithered fills in the letterbox areas using the sampled edge colors. -// fillMode selects the fill algorithm: SOLID (single dominant shade), BLENDED (per-pixel edge color), -// or GRADIENT (per-pixel edge color interpolated toward targetColor). -// targetColor is the color the gradient fades toward (255=white, 0=black); only used in GRADIENT mode. +// Draw letterbox fill in the areas around the cover image. +// DITHERED: fills with the edge average using Bayer ordered dithering to approximate the color. +// SOLID: snaps edge average to nearest e-ink level (0/85/170/255) for a clean uniform fill. // Must be called once per render pass (BW, GRAYSCALE_LSB, GRAYSCALE_MSB). -void drawLetterboxFill(GfxRenderer& renderer, const LetterboxGradientData& data, float scale, uint8_t fillMode, - int targetColor) { - if (!data.edgeA || !data.edgeB || data.edgeCount <= 0) return; +void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uint8_t fillMode) { + if (!data.valid) return; const bool isSolid = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_SOLID); - const bool isGradient = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_GRADIENT); - // For SOLID mode, compute the dominant (average) shade for each edge once - uint8_t solidColorA = 0, solidColorB = 0; - if (isSolid) { - uint32_t sumA = 0, sumB = 0; - for (int i = 0; i < data.edgeCount; i++) { - sumA += data.edgeA[i]; - sumB += data.edgeB[i]; - } - solidColorA = static_cast(sumA / data.edgeCount); - solidColorB = static_cast(sumB / data.edgeCount); - } - - // Helper: compute gray value for a pixel given the edge color and interpolation factor t (0..1) - // GRADIENT interpolates from edgeColor toward targetColor; SOLID and BLENDED return edgeColor directly. - auto computeGray = [&](int edgeColor, float t) -> int { - if (isGradient) return edgeColor + static_cast(static_cast(targetColor - edgeColor) * t); - return edgeColor; - }; + // For SOLID: snap to nearest e-ink level then convert to 2-bit + // For DITHERED: use the raw average, Bayer dithering produces the 2-bit level per pixel + const uint8_t colorA = isSolid ? snapToEinkLevel(data.avgA) : data.avgA; + const uint8_t colorB = isSolid ? snapToEinkLevel(data.avgB) : data.avgB; + const uint8_t solidA = colorA / 85; // only used for SOLID + const uint8_t solidB = colorB / 85; if (data.horizontal) { // Top letterbox if (data.letterboxA > 0) { - const int imgTopY = data.letterboxA; - for (int screenY = 0; screenY < imgTopY; screenY++) { - const float t = static_cast(imgTopY - screenY) / static_cast(imgTopY); - for (int screenX = 0; screenX < renderer.getScreenWidth(); screenX++) { - int edgeColor; - if (isSolid) { - edgeColor = solidColorA; - } else { - int srcCol = static_cast(screenX / scale); - srcCol = std::max(0, std::min(srcCol, data.edgeCount - 1)); - edgeColor = data.edgeA[srcCol]; - } - const int gray = computeGray(edgeColor, t); - renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY)); - } - } + for (int y = 0; y < data.letterboxA; y++) + for (int x = 0; x < renderer.getScreenWidth(); x++) + renderer.drawPixelGray(x, y, isSolid ? solidA : quantizeBayerDither(colorA, x, y)); } - // Bottom letterbox if (data.letterboxB > 0) { - const int imgBottomY = renderer.getScreenHeight() - data.letterboxB; - for (int screenY = imgBottomY; screenY < renderer.getScreenHeight(); screenY++) { - const float t = static_cast(screenY - imgBottomY + 1) / static_cast(data.letterboxB); - for (int screenX = 0; screenX < renderer.getScreenWidth(); screenX++) { - int edgeColor; - if (isSolid) { - edgeColor = solidColorB; - } else { - int srcCol = static_cast(screenX / scale); - srcCol = std::max(0, std::min(srcCol, data.edgeCount - 1)); - edgeColor = data.edgeB[srcCol]; - } - const int gray = computeGray(edgeColor, t); - renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY)); - } - } + const int start = renderer.getScreenHeight() - data.letterboxB; + for (int y = start; y < renderer.getScreenHeight(); y++) + for (int x = 0; x < renderer.getScreenWidth(); x++) + renderer.drawPixelGray(x, y, isSolid ? solidB : quantizeBayerDither(colorB, x, y)); } } else { // Left letterbox if (data.letterboxA > 0) { - const int imgLeftX = data.letterboxA; - for (int screenX = 0; screenX < imgLeftX; screenX++) { - const float t = static_cast(imgLeftX - screenX) / static_cast(imgLeftX); - for (int screenY = 0; screenY < renderer.getScreenHeight(); screenY++) { - int edgeColor; - if (isSolid) { - edgeColor = solidColorA; - } else { - int srcRow = static_cast(screenY / scale); - srcRow = std::max(0, std::min(srcRow, data.edgeCount - 1)); - edgeColor = data.edgeA[srcRow]; - } - const int gray = computeGray(edgeColor, t); - renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY)); - } - } + for (int x = 0; x < data.letterboxA; x++) + for (int y = 0; y < renderer.getScreenHeight(); y++) + renderer.drawPixelGray(x, y, isSolid ? solidA : quantizeBayerDither(colorA, x, y)); } - // Right letterbox if (data.letterboxB > 0) { - const int imgRightX = renderer.getScreenWidth() - data.letterboxB; - for (int screenX = imgRightX; screenX < renderer.getScreenWidth(); screenX++) { - const float t = static_cast(screenX - imgRightX + 1) / static_cast(data.letterboxB); - for (int screenY = 0; screenY < renderer.getScreenHeight(); screenY++) { - int edgeColor; - if (isSolid) { - edgeColor = solidColorB; - } else { - int srcRow = static_cast(screenY / scale); - srcRow = std::max(0, std::min(srcRow, data.edgeCount - 1)); - edgeColor = data.edgeB[srcRow]; - } - const int gray = computeGray(edgeColor, t); - renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY)); - } - } + const int start = renderer.getScreenWidth() - data.letterboxB; + for (int x = start; x < renderer.getScreenWidth(); x++) + for (int y = 0; y < renderer.getScreenHeight(); y++) + renderer.drawPixelGray(x, y, isSolid ? solidB : quantizeBayerDither(colorB, x, y)); } } } @@ -530,28 +440,31 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str // Determine letterbox fill settings const uint8_t fillMode = SETTINGS.sleepScreenLetterboxFill; const bool wantFill = (fillMode != CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_NONE); - const int targetColor = - (SETTINGS.sleepScreenGradientDir == CrossPointSettings::SLEEP_SCREEN_GRADIENT_DIR::GRADIENT_TO_BLACK) ? 0 : 255; - static const char* fillModeNames[] = {"none", "solid", "blended", "gradient"}; - const char* fillModeName = (fillMode < 4) ? fillModeNames[fillMode] : "unknown"; + static const char* fillModeNames[] = {"dithered", "solid", "none"}; + const char* fillModeName = (fillMode < 3) ? fillModeNames[fillMode] : "unknown"; - // Load cached edge data or sample from bitmap (first pass over bitmap, then rewind) - LetterboxGradientData gradientData; + // Compute edge averages if letterbox fill is requested (try cache first) + LetterboxFillData fillData; const bool hasLetterbox = (x > 0 || y > 0); if (hasLetterbox && wantFill) { bool cacheLoaded = false; if (!edgeCachePath.empty()) { - cacheLoaded = loadEdgeCache(edgeCachePath, pageWidth, pageHeight, gradientData); + cacheLoaded = loadEdgeCache(edgeCachePath, pageWidth, pageHeight, fillData); } if (!cacheLoaded) { - Serial.printf("[%lu] [SLP] Letterbox detected (x=%d, y=%d), sampling edges for %s fill\n", millis(), x, y, - fillModeName); - gradientData = sampleBitmapEdges(bitmap, x, y, pageWidth, pageHeight, scale, cropX, cropY); - if (!edgeCachePath.empty() && gradientData.edgeA) { - saveEdgeCache(edgeCachePath, pageWidth, pageHeight, gradientData); + Serial.printf("[%lu] [SLP] Letterbox detected (x=%d, y=%d), computing edge averages for %s fill\n", millis(), x, + y, fillModeName); + fillData = computeEdgeAverages(bitmap, x, y, pageWidth, pageHeight, scale, cropX, cropY); + if (fillData.valid && !edgeCachePath.empty()) { + saveEdgeCache(edgeCachePath, pageWidth, pageHeight, fillData); } } + if (fillData.valid) { + Serial.printf("[%lu] [SLP] Letterbox fill: %s, horizontal=%d, avgA=%d, avgB=%d, letterboxA=%d, letterboxB=%d\n", + millis(), fillModeName, fillData.horizontal, fillData.avgA, fillData.avgB, fillData.letterboxA, + fillData.letterboxB); + } } renderer.clearScreen(); @@ -560,8 +473,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER; // Draw letterbox fill (BW pass) - if (gradientData.edgeA) { - drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor); + if (fillData.valid) { + drawLetterboxFill(renderer, fillData, fillMode); } renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); @@ -576,8 +489,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - if (gradientData.edgeA) { - drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor); + if (fillData.valid) { + drawLetterboxFill(renderer, fillData, fillMode); } renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); renderer.copyGrayscaleLsbBuffers(); @@ -585,8 +498,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - if (gradientData.edgeA) { - drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor); + if (fillData.valid) { + drawLetterboxFill(renderer, fillData, fillMode); } renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); renderer.copyGrayscaleMsbBuffers(); @@ -594,8 +507,6 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str renderer.displayGrayBuffer(); renderer.setRenderMode(GfxRenderer::BW); } - - gradientData.free(); } void SleepActivity::renderCoverSleepScreen() const { @@ -665,17 +576,17 @@ void SleepActivity::renderCoverSleepScreen() const { return (this->*renderNoCoverSleepScreen)(); } - // Derive edge cache path from cover BMP path (e.g. cover.bmp -> cover_edges.bin) - std::string edgeCachePath; - if (coverBmpPath.size() > 4) { - edgeCachePath = coverBmpPath.substr(0, coverBmpPath.size() - 4) + "_edges.bin"; - } - FsFile file; if (Storage.openFileForRead("SLP", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { Serial.printf("[%lu] [SLP] Rendering sleep cover: %s\n", millis(), coverBmpPath.c_str()); + // Derive edge cache path from cover BMP path (e.g. cover.bmp -> cover_edges.bin) + std::string edgeCachePath; + const auto dotPos = coverBmpPath.rfind(".bmp"); + if (dotPos != std::string::npos) { + edgeCachePath = coverBmpPath.substr(0, dotPos) + "_edges.bin"; + } renderBitmapSleepScreen(bitmap, edgeCachePath); return; } diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 7c703e2a..a39ac448 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -1,4 +1,5 @@ #pragma once + #include #include "../Activity.h"