4 Commits

Author SHA1 Message Date
cottongin
ad282cadfe fix: Align double FAST_REFRESH image rendering with upstream PR #957
Reorder refresh branches so image+AA pages always use the double
FAST_REFRESH technique instead of occasionally falling through to
HALF_REFRESH when the refresh counter expires. Image pages no longer
count toward the full refresh cadence. Remove experimental Method B
toggle (USE_IMAGE_DOUBLE_FAST_REFRESH / displayWindow).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 14:30:10 -05:00
cottongin
c8ba4fe973 fix: Port upstream CSS-aware image sizing (PR #1002)
Parse CSS height/width into CssStyle for images and use aspect-ratio-
preserving logic when CSS dimensions are set. Falls back to viewport-fit
scaling when no CSS dimensions are present. Includes divide-by-zero
guards and viewport clamping with aspect ratio rescaling.

- Add imageHeight field to CssStyle/CssPropertyFlags
- Parse CSS height declarations into imageHeight
- Add imageHeight + width to cache serialization (bump cache v2->v3)
- Replace viewport-fit-only image scaling with CSS-aware sizing

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 14:21:31 -05:00
cottongin
c1b8e53138 fix: Port upstream 1.1.0-rc fixes (glyph null-safety, PNGdec wide image buffer)
Cherry-pick two bug fixes from upstream PR #992:

- fix(GfxRenderer): Null-safety in getSpaceWidth/getTextAdvanceX to
  prevent Load access fault when bold/italic font variants lack certain
  glyphs (upstream 3e2c518)
- fix(PNGdec): Increase PNG_MAX_BUFFERED_PIXELS to 16416 for 2048px
  wide images and add pre-decode buffer overflow guard (upstream b8e743e)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 13:20:30 -05:00
cottongin
0fda9031fd 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 <cursoragent@cursor.com>
2026-02-19 11:33:45 -05:00
8 changed files with 172 additions and 148 deletions

View File

@@ -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 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.
// 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.
@@ -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,
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) {
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
}

View File

@@ -295,6 +295,9 @@ void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& sty
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft =
1;
}
} else if (propNameBuf == "height") {
style.imageHeight = interpretLength(propValueBuf);
style.defined.imageHeight = 1;
} else if (propNameBuf == "width") {
style.width = interpretLength(propValueBuf);
style.defined.width = 1;
@@ -565,7 +568,7 @@ CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return par
// Cache serialization
// Cache format version - increment when format changes
constexpr uint8_t CSS_CACHE_VERSION = 2;
constexpr uint8_t CSS_CACHE_VERSION = 3;
constexpr char rulesCache[] = "/css_rules.cache";
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
@@ -616,6 +619,8 @@ bool CssParser::saveToCache() const {
writeLength(style.paddingBottom);
writeLength(style.paddingLeft);
writeLength(style.paddingRight);
writeLength(style.imageHeight);
writeLength(style.width);
// Write defined flags as uint16_t
uint16_t definedBits = 0;
@@ -632,6 +637,8 @@ bool CssParser::saveToCache() const {
if (style.defined.paddingBottom) definedBits |= 1 << 10;
if (style.defined.paddingLeft) definedBits |= 1 << 11;
if (style.defined.paddingRight) definedBits |= 1 << 12;
if (style.defined.width) definedBits |= 1 << 13;
if (style.defined.imageHeight) definedBits |= 1 << 14;
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
}
@@ -733,7 +740,8 @@ bool CssParser::loadFromCache() {
if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) ||
!readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) ||
!readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight)) {
!readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight) ||
!readLength(style.imageHeight) || !readLength(style.width)) {
rulesBySelector_.clear();
file.close();
return false;
@@ -759,6 +767,8 @@ bool CssParser::loadFromCache() {
style.defined.paddingBottom = (definedBits & 1 << 10) != 0;
style.defined.paddingLeft = (definedBits & 1 << 11) != 0;
style.defined.paddingRight = (definedBits & 1 << 12) != 0;
style.defined.width = (definedBits & 1 << 13) != 0;
style.defined.imageHeight = (definedBits & 1 << 14) != 0;
rulesBySelector_[selector] = style;
}

View File

@@ -70,6 +70,7 @@ struct CssPropertyFlags {
uint16_t paddingLeft : 1;
uint16_t paddingRight : 1;
uint16_t width : 1;
uint16_t imageHeight : 1;
CssPropertyFlags()
: textAlign(0),
@@ -85,18 +86,20 @@ struct CssPropertyFlags {
paddingBottom(0),
paddingLeft(0),
paddingRight(0),
width(0) {}
width(0),
imageHeight(0) {}
[[nodiscard]] bool anySet() const {
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || width;
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || width ||
imageHeight;
}
void clearAll() {
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
marginTop = marginBottom = marginLeft = marginRight = 0;
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
width = 0;
width = imageHeight = 0;
}
};
@@ -118,7 +121,8 @@ struct CssStyle {
CssLength paddingBottom; // Padding after
CssLength paddingLeft; // Padding left
CssLength paddingRight; // Padding right
CssLength width; // Element width (used for table columns/cells)
CssLength width; // Element width (used for table columns/cells and image sizing)
CssLength imageHeight; // Height for img (e.g. 2em) -- width derived from aspect ratio when only height set
CssPropertyFlags defined; // Tracks which properties were explicitly set
@@ -181,6 +185,10 @@ struct CssStyle {
width = base.width;
defined.width = 1;
}
if (base.hasImageHeight()) {
imageHeight = base.imageHeight;
defined.imageHeight = 1;
}
}
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
@@ -197,6 +205,7 @@ struct CssStyle {
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
[[nodiscard]] bool hasWidth() const { return defined.width; }
[[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; }
void reset() {
textAlign = CssTextAlign::Left;
@@ -207,6 +216,7 @@ struct CssStyle {
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
width = CssLength{};
imageHeight = CssLength{};
defined.clearAll();
}
};

View File

@@ -418,18 +418,58 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (decoder->getDimensions(cachedImagePath, dims)) {
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
// Scale to fit viewport while maintaining aspect ratio
int maxWidth = self->viewportWidth;
int maxHeight = self->viewportHeight;
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f;
float scale = (scaleX < scaleY) ? scaleX : scaleY;
if (scale > 1.0f) scale = 1.0f;
int displayWidth = 0;
int displayHeight = 0;
const float emSize =
static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression;
CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{};
if (!styleAttr.empty()) {
imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
}
const bool hasCssHeight = imgStyle.hasImageHeight();
const bool hasCssWidth = imgStyle.hasWidth();
int displayWidth = (int)(dims.width * scale);
int displayHeight = (int)(dims.height * scale);
if (hasCssHeight && dims.width > 0 && dims.height > 0) {
displayHeight = static_cast<int>(
imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f);
if (displayHeight < 1) displayHeight = 1;
displayWidth =
static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f);
if (displayWidth > self->viewportWidth) {
displayWidth = self->viewportWidth;
displayHeight =
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
if (displayHeight < 1) displayHeight = 1;
}
if (displayWidth < 1) displayWidth = 1;
LOG_DBG("EHP", "Display size from CSS height: %dx%d", displayWidth, displayHeight);
} else if (hasCssWidth && !hasCssHeight && dims.width > 0 && dims.height > 0) {
displayWidth = static_cast<int>(
imgStyle.width.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f);
if (displayWidth > self->viewportWidth) displayWidth = self->viewportWidth;
if (displayWidth < 1) displayWidth = 1;
displayHeight =
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
if (displayHeight > self->viewportHeight) {
displayHeight = self->viewportHeight;
displayWidth =
static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f);
if (displayWidth < 1) displayWidth = 1;
}
if (displayHeight < 1) displayHeight = 1;
LOG_DBG("EHP", "Display size from CSS width: %dx%d", displayWidth, displayHeight);
} else {
int maxWidth = self->viewportWidth;
int maxHeight = self->viewportHeight;
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f;
float scale = (scaleX < scaleY) ? scaleX : scaleY;
if (scale > 1.0f) scale = 1.0f;
LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale);
displayWidth = (int)(dims.width * scale);
displayHeight = (int)(dims.height * scale);
LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale);
}
// Create page for image - only break if image won't fit remaining space
if (self->currentPage && !self->currentPage->elements.empty() &&

View File

@@ -824,7 +824,8 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
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 {
@@ -838,7 +839,9 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, const EpdFo
int width = 0;
const auto& font = fontIt->second;
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;
}

View File

@@ -2,7 +2,7 @@
default_envs = default
[crosspoint]
version = 1.0.0
version = 1.1.1-rc
[base]
platform = espressif32 @ 6.12.0
@@ -31,9 +31,9 @@ build_flags =
-std=gnu++2a
# Enable UTF-8 long file names in SdFat
-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
-DPNG_MAX_BUFFERED_PIXELS=6402
-DPNG_MAX_BUFFERED_PIXELS=16416
build_unflags =
-std=gnu++11

View File

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

View File

@@ -22,11 +22,6 @@
#include "util/BookmarkStore.h"
#include "util/Dictionary.h"
// Image refresh optimization strategy:
// 0 = Use double FAST_REFRESH technique (default, feels snappier)
// 1 = Use displayWindow() for partial refresh (experimental)
#define USE_IMAGE_DOUBLE_FAST_REFRESH 0
namespace {
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
constexpr unsigned long skipChapterMs = 700;
@@ -1022,13 +1017,8 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) {
// Determine if this page needs special image handling
bool pageHasImages = page->hasImages();
bool useAntiAliasing = SETTINGS.textAntiAliasing;
// Force half refresh for pages with images when anti-aliasing is on,
// as grayscale tones require half refresh to display correctly
bool forceFullRefresh = pageHasImages && useAntiAliasing;
// Force special handling for pages with images when anti-aliasing is on
bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing;
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
@@ -1048,42 +1038,26 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
}
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
// Check if half-refresh is needed (either entering Reader or pages counter reached)
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else if (forceFullRefresh) {
// OPTIMIZATION: For image pages with anti-aliasing, use fast double-refresh technique
// to reduce perceived lag. Only when pagesUntilFullRefresh > 1 (screen already clean).
if (imagePageWithAA) {
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
// HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust.
// Instead, blank only the image area and do two fast refreshes.
int imgX, imgY, imgW, imgH;
if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) {
int screenX = imgX + orientedMarginLeft;
int screenY = imgY + orientedMarginTop;
LOG_DBG("ERS", "Image page: fast double-refresh (page bbox: %d,%d %dx%d, screen: %d,%d %dx%d)", imgX, imgY, imgW,
imgH, screenX, screenY, imgW, imgH);
renderer.fillRect(imgX + orientedMarginLeft, imgY + orientedMarginTop, imgW, imgH, false);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
#if USE_IMAGE_DOUBLE_FAST_REFRESH == 0
// Method A: Fill blank area + two FAST_REFRESH operations
renderer.fillRect(screenX, screenY, imgW, imgH, false);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
#else
// Method B (experimental): Use displayWindow() for partial refresh
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayWindow(screenX, screenY, imgW, imgH, HalDisplay::FAST_REFRESH);
#endif
} else {
LOG_DBG("ERS", "Image page but no bbox, using standard half refresh");
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
pagesUntilFullRefresh--;
// Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence
} else if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else {
// Normal page without images, or images without anti-aliasing
renderer.displayBuffer();
pagesUntilFullRefresh--;
}