Compare commits
2 Commits
mod/sync-u
...
c1b8e53138
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1b8e53138
|
||
|
|
0fda9031fd
|
@@ -90,6 +90,32 @@ int32_t pngSeekWithHandle(PNGFILE* pFile, int32_t pos) {
|
|||||||
constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead
|
constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead
|
||||||
constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom
|
constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom
|
||||||
|
|
||||||
|
// PNGdec keeps TWO scanlines in its internal ucPixels buffer (current + previous)
|
||||||
|
// and each scanline includes a leading filter byte.
|
||||||
|
// Required storage is therefore approximately: 2 * (pitch + 1) + alignment slack.
|
||||||
|
// If PNG_MAX_BUFFERED_PIXELS is smaller than this requirement for a given image,
|
||||||
|
// PNGdec can overrun its internal buffer before our draw callback executes.
|
||||||
|
int bytesPerPixelFromType(int pixelType) {
|
||||||
|
switch (pixelType) {
|
||||||
|
case PNG_PIXEL_TRUECOLOR:
|
||||||
|
return 3;
|
||||||
|
case PNG_PIXEL_GRAY_ALPHA:
|
||||||
|
return 2;
|
||||||
|
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||||
|
return 4;
|
||||||
|
case PNG_PIXEL_GRAYSCALE:
|
||||||
|
case PNG_PIXEL_INDEXED:
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int requiredPngInternalBufferBytes(int srcWidth, int pixelType) {
|
||||||
|
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
|
||||||
|
int pitch = srcWidth * bytesPerPixelFromType(pixelType);
|
||||||
|
return ((pitch + 1) * 2) + 32;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert entire source line to grayscale with alpha blending to white background.
|
// Convert entire source line to grayscale with alpha blending to white background.
|
||||||
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||||
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||||
@@ -304,6 +330,18 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
|||||||
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
|
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
|
||||||
ctx.scale, png->getBpp());
|
ctx.scale, png->getBpp());
|
||||||
|
|
||||||
|
const int pixelType = png->getPixelType();
|
||||||
|
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType);
|
||||||
|
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
|
||||||
|
LOG_ERR("PNG",
|
||||||
|
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
||||||
|
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS);
|
||||||
|
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (png->getBpp() != 8) {
|
if (png->getBpp() != 8) {
|
||||||
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -824,7 +824,8 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fontIt->second.getGlyph(' ', style)->advanceX;
|
const EpdGlyph* spaceGlyph = fontIt->second.getGlyph(' ', style);
|
||||||
|
return spaceGlyph ? spaceGlyph->advanceX : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
||||||
@@ -838,7 +839,9 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, const EpdFo
|
|||||||
int width = 0;
|
int width = 0;
|
||||||
const auto& font = fontIt->second;
|
const auto& font = fontIt->second;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||||
width += font.getGlyph(cp, style)->advanceX;
|
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||||
|
if (!glyph) glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
||||||
|
if (glyph) width += glyph->advanceX;
|
||||||
}
|
}
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
default_envs = default
|
default_envs = default
|
||||||
|
|
||||||
[crosspoint]
|
[crosspoint]
|
||||||
version = 1.0.0
|
version = 1.1.1-rc
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
platform = espressif32 @ 6.12.0
|
platform = espressif32 @ 6.12.0
|
||||||
@@ -31,9 +31,9 @@ build_flags =
|
|||||||
-std=gnu++2a
|
-std=gnu++2a
|
||||||
# Enable UTF-8 long file names in SdFat
|
# Enable UTF-8 long file names in SdFat
|
||||||
-DUSE_UTF8_LONG_NAMES=1
|
-DUSE_UTF8_LONG_NAMES=1
|
||||||
# Increase PNG scanline buffer to support up to 800px wide images
|
# Increase PNG scanline buffer to support up to 2048px wide images
|
||||||
# Default is (320*4+1)*2=2562, we need more for larger images
|
# Default is (320*4+1)*2=2562, we need more for larger images
|
||||||
-DPNG_MAX_BUFFERED_PIXELS=6402
|
-DPNG_MAX_BUFFERED_PIXELS=16416
|
||||||
|
|
||||||
build_unflags =
|
build_unflags =
|
||||||
-std=gnu++11
|
-std=gnu++11
|
||||||
|
|||||||
@@ -21,6 +21,11 @@
|
|||||||
#include "util/BookSettings.h"
|
#include "util/BookSettings.h"
|
||||||
#include "util/StringUtils.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 {
|
namespace {
|
||||||
|
|
||||||
// Number of source pixels along the image edge to average for the dominant color
|
// 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 ---
|
// --- Edge average cache ---
|
||||||
// Caches the computed edge averages alongside the cover BMP so we don't rescan on every sleep.
|
// Caches the computed edge averages alongside the cover BMP so we don't rescan on every sleep.
|
||||||
constexpr uint8_t EDGE_CACHE_VERSION = 2;
|
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);
|
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 levelA = isSolid ? snapToEinkLevel(data.avgA) / 85 : 0;
|
||||||
const uint8_t levelB = isSolid ? snapToEinkLevel(data.avgB) / 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) {
|
if (data.letterboxA > 0) {
|
||||||
for (int y = 0; y < data.letterboxA; y++)
|
for (int y = 0; y < data.letterboxA; y++)
|
||||||
for (int x = 0; x < renderer.getScreenWidth(); x++) {
|
for (int x = 0; x < renderer.getScreenWidth(); x++) {
|
||||||
uint8_t lv;
|
const uint8_t lv = isSolid ? levelA : quantizeBayerDither(data.avgA, x, y);
|
||||||
if (isSolid)
|
|
||||||
lv = levelA;
|
|
||||||
else if (hashA)
|
|
||||||
lv = hashBlockDither(data.avgA, x, y);
|
|
||||||
else
|
|
||||||
lv = quantizeBayerDither(data.avgA, x, y);
|
|
||||||
renderer.drawPixelGray(x, y, lv);
|
renderer.drawPixelGray(x, y, lv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,13 +267,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin
|
|||||||
const int start = renderer.getScreenHeight() - data.letterboxB;
|
const int start = renderer.getScreenHeight() - data.letterboxB;
|
||||||
for (int y = start; y < renderer.getScreenHeight(); y++)
|
for (int y = start; y < renderer.getScreenHeight(); y++)
|
||||||
for (int x = 0; x < renderer.getScreenWidth(); x++) {
|
for (int x = 0; x < renderer.getScreenWidth(); x++) {
|
||||||
uint8_t lv;
|
const uint8_t lv = isSolid ? levelB : quantizeBayerDither(data.avgB, x, y);
|
||||||
if (isSolid)
|
|
||||||
lv = levelB;
|
|
||||||
else if (hashB)
|
|
||||||
lv = hashBlockDither(data.avgB, x, y);
|
|
||||||
else
|
|
||||||
lv = quantizeBayerDither(data.avgB, x, y);
|
|
||||||
renderer.drawPixelGray(x, y, lv);
|
renderer.drawPixelGray(x, y, lv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,13 +275,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin
|
|||||||
if (data.letterboxA > 0) {
|
if (data.letterboxA > 0) {
|
||||||
for (int x = 0; x < data.letterboxA; x++)
|
for (int x = 0; x < data.letterboxA; x++)
|
||||||
for (int y = 0; y < renderer.getScreenHeight(); y++) {
|
for (int y = 0; y < renderer.getScreenHeight(); y++) {
|
||||||
uint8_t lv;
|
const uint8_t lv = isSolid ? levelA : quantizeBayerDither(data.avgA, x, y);
|
||||||
if (isSolid)
|
|
||||||
lv = levelA;
|
|
||||||
else if (hashA)
|
|
||||||
lv = hashBlockDither(data.avgA, x, y);
|
|
||||||
else
|
|
||||||
lv = quantizeBayerDither(data.avgA, x, y);
|
|
||||||
renderer.drawPixelGray(x, y, lv);
|
renderer.drawPixelGray(x, y, lv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,13 +283,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin
|
|||||||
const int start = renderer.getScreenWidth() - data.letterboxB;
|
const int start = renderer.getScreenWidth() - data.letterboxB;
|
||||||
for (int x = start; x < renderer.getScreenWidth(); x++)
|
for (int x = start; x < renderer.getScreenWidth(); x++)
|
||||||
for (int y = 0; y < renderer.getScreenHeight(); y++) {
|
for (int y = 0; y < renderer.getScreenHeight(); y++) {
|
||||||
uint8_t lv;
|
const uint8_t lv = isSolid ? levelB : quantizeBayerDither(data.avgB, x, y);
|
||||||
if (isSolid)
|
|
||||||
lv = levelB;
|
|
||||||
else if (hashB)
|
|
||||||
lv = hashBlockDither(data.avgB, x, y);
|
|
||||||
else
|
|
||||||
lv = quantizeBayerDither(data.avgB, x, y);
|
|
||||||
renderer.drawPixelGray(x, y, lv);
|
renderer.drawPixelGray(x, y, lv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -543,35 +480,47 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
|
|||||||
const bool hasGreyscale = bitmap.hasGreyscale() &&
|
const bool hasGreyscale = bitmap.hasGreyscale() &&
|
||||||
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER;
|
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER;
|
||||||
|
|
||||||
// Draw letterbox fill (BW pass)
|
const bool isInverted =
|
||||||
if (fillData.valid) {
|
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE;
|
||||||
drawLetterboxFill(renderer, fillData, fillMode);
|
|
||||||
|
#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) {
|
if (hasGreyscale) {
|
||||||
bitmap.rewindToData();
|
bitmap.rewindToData();
|
||||||
renderer.clearScreen(0x00);
|
renderer.clearScreen(0x00);
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||||
if (fillData.valid) {
|
if (fillData.valid) drawLetterboxFill(renderer, fillData, fillMode);
|
||||||
drawLetterboxFill(renderer, fillData, fillMode);
|
|
||||||
}
|
|
||||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
||||||
renderer.copyGrayscaleLsbBuffers();
|
renderer.copyGrayscaleLsbBuffers();
|
||||||
|
|
||||||
bitmap.rewindToData();
|
bitmap.rewindToData();
|
||||||
renderer.clearScreen(0x00);
|
renderer.clearScreen(0x00);
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||||
if (fillData.valid) {
|
if (fillData.valid) drawLetterboxFill(renderer, fillData, fillMode);
|
||||||
drawLetterboxFill(renderer, fillData, fillMode);
|
|
||||||
}
|
|
||||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
||||||
renderer.copyGrayscaleMsbBuffers();
|
renderer.copyGrayscaleMsbBuffers();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user