# Letterbox Fill: Replace Dithering with Edge Replication ("Extend Edges") **Date:** 2026-02-13 ## Task Description After implementing Bayer ordered dithering for the "Blended" letterbox fill mode (replacing earlier noise dithering), the user reported that the problematic cover (*The World in a Grain* by Vince Beiser) looked even worse. Investigation revealed that **any dithering technique** is fundamentally incompatible with the e-ink multi-pass rendering pipeline for large uniform fill areas: - The BW HALF_REFRESH pass creates a visible dark/white pattern in the letterbox - The subsequent greyscale correction can't cleanly handle mixed-level patterns in large uniform areas - This causes crosstalk artifacts that can affect adjacent cover rendering - Uniform fills (all same level, like Solid mode) work fine because all pixels go through identical correction The user's key insight: the display already renders the cover's grayscale correctly (including Atkinson-dithered mixed levels). Instead of re-dithering an averaged color, replicate the cover's actual boundary pixels into the letterbox -- letting the display render them the same way it renders the cover itself. ## Changes Made ### `src/CrossPointSettings.h` - Renamed `LETTERBOX_BLENDED` (value 2) → `LETTERBOX_EXTENDED` - Updated comment to reflect "None / Solid / Extend Edges" ### `src/SettingsList.h` - Changed UI label from `"Blended"` → `"Extend Edges"` ### `src/activities/boot_sleep/SleepActivity.cpp` - **Removed**: `BAYER_4X4` matrix, `quantizeBayerDither()` function (all Bayer dithering code) - **Added**: `getPackedPixel()`, `setPackedPixel()` helpers for packed 2-bit array access - **Rewrote** `LetterboxFillData` struct: - Added `edgeA`/`edgeB` (dynamically allocated packed 2-bit arrays) for per-pixel edge data - Added `edgePixelCount`, `scale` for coordinate mapping - Added `freeEdgeData()` cleanup method - **Renamed** `computeEdgeAverages()` → `computeEdgeData()`: - New `captureEdgePixels` parameter controls whether to allocate and fill edge arrays - For horizontal letterboxing: captures first/last visible BMP rows - For vertical letterboxing: captures leftmost/rightmost pixel per visible row - Still computes averages for SOLID mode in the same pass - **Rewrote** `drawLetterboxFill()`: - SOLID mode: unchanged (uniform fill with snapped level) - EXTENDED mode: maps screen coordinates → BMP coordinates via scale factor, looks up stored 2-bit pixel values from the cover's boundary row/column - **Updated** `renderBitmapSleepScreen()`: new log messages, `freeEdgeData()` call at end ## Backward Compatibility - Enum value 2 is unchanged (was `LETTERBOX_BLENDED`, now `LETTERBOX_EXTENDED`) - Serialized settings files continue to work without migration ## Follow-up Items - Test "Extend Edges" mode with both covers (Power Broker and World in a Grain) - Test vertical letterboxing (left/right) if applicable covers are available - Verify SOLID mode still works as expected