Merge pull request #257 from jpirnay/refactor-stripedbuffer
feat: tiled grayscale rendering to drop the storeBwBuffer peak (#2106 by jermydk)
This commit is contained in:
@@ -16,6 +16,12 @@ struct DirectPixelWriter {
|
||||
uint8_t* fb;
|
||||
GfxRenderer::RenderMode mode;
|
||||
uint16_t displayWidthBytes; // Runtime framebuffer stride (X4: 100, X3: 99)
|
||||
// Active write target: for tiled grayscale, fb is the band scratch, originY is
|
||||
// the band's top physical row, and clipRows is the band height. Off-band
|
||||
// pixels are dropped. With no strip active these collapse to the full frame
|
||||
// (originY 0, clipRows panelHeight) so the clip doubles as a bounds guard.
|
||||
int originY;
|
||||
int clipRows;
|
||||
|
||||
// Orientation is collapsed into a linear transform:
|
||||
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
|
||||
@@ -28,7 +34,9 @@ struct DirectPixelWriter {
|
||||
int rowPhyXBase, rowPhyYBase;
|
||||
|
||||
void init(GfxRenderer& renderer) {
|
||||
fb = renderer.getFrameBuffer();
|
||||
fb = renderer.getWriteTarget();
|
||||
originY = renderer.getWriteOriginY();
|
||||
clipRows = renderer.getWriteRows();
|
||||
mode = renderer.getRenderMode();
|
||||
displayWidthBytes = renderer.getDisplayWidthBytes();
|
||||
|
||||
@@ -120,7 +128,12 @@ struct DirectPixelWriter {
|
||||
const int phyX = rowPhyXBase + logicalX * phyXStepX;
|
||||
const int phyY = rowPhyYBase + logicalX * phyYStepX;
|
||||
|
||||
const uint16_t byteIndex = phyY * displayWidthBytes + (phyX >> 3);
|
||||
// Band-local row. The unsigned compare drops both off-band pixels (strip
|
||||
// mode) and any out-of-frame row (full-frame mode) in one branch.
|
||||
const int sy = phyY - originY;
|
||||
if (static_cast<unsigned>(sy) >= static_cast<unsigned>(clipRows)) return;
|
||||
|
||||
const uint16_t byteIndex = static_cast<uint16_t>(sy * displayWidthBytes + (phyX >> 3));
|
||||
const uint8_t bitMask = 1 << (7 - (phyX & 7));
|
||||
|
||||
if (state) {
|
||||
|
||||
+138
-26
@@ -8,6 +8,8 @@
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
#include "FontCacheManager.h"
|
||||
|
||||
@@ -611,15 +613,21 @@ static inline uint8_t build2BitColMask(const uint8_t* const bitmap, const int gl
|
||||
// inverted=false → Portrait (phyY counts down, phyBitPos counts up).
|
||||
// inverted=true → PortraitInverted (phyY counts up, phyBitPos counts down).
|
||||
// Both template params are compile-time constants; all ternaries fold away.
|
||||
// `frameBuffer` may be a strip scratch covering only rows [fbOriginY, fbOriginY+fbRows);
|
||||
// the writer subtracts fbOriginY when indexing and drops rows outside the band.
|
||||
// In non-strip mode the caller passes fbOriginY=0, fbRows=displayHeight, so the
|
||||
// translation is a no-op and the existing absolute-row indexing is preserved.
|
||||
template <uint8_t drawMask, bool inverted>
|
||||
static void renderGlyphFast2BitPortrait(uint8_t* const frameBuffer, const uint8_t* const bitmap, const int glyphWidth,
|
||||
const int glyphHeight, const int screenXBase, const int screenYBase,
|
||||
const bool writeState, const int displayWidth, const int displayHeight,
|
||||
const int widthBytes) {
|
||||
const int widthBytes, const int fbOriginY, const int fbRows) {
|
||||
for (int glyphX = 0; glyphX < glyphWidth; glyphX++) {
|
||||
const int phyY = inverted ? (screenXBase + glyphX) : (displayHeight - 1 - (screenXBase + glyphX));
|
||||
if (phyY < 0 || phyY >= displayHeight) continue;
|
||||
uint8_t* const row = frameBuffer + phyY * widthBytes;
|
||||
const int rowY = phyY - fbOriginY;
|
||||
if (static_cast<unsigned>(rowY) >= static_cast<unsigned>(fbRows)) continue;
|
||||
uint8_t* const row = frameBuffer + rowY * widthBytes;
|
||||
for (int glyphY = 0; glyphY < glyphHeight; glyphY += 8) {
|
||||
const int count = std::min(8, glyphHeight - glyphY);
|
||||
const uint8_t mask = build2BitColMask<drawMask>(bitmap, glyphWidth, glyphX, glyphY, count, inverted);
|
||||
@@ -635,18 +643,25 @@ template <uint8_t drawMask>
|
||||
static void renderGlyphFast2Bit(uint8_t* const frameBuffer, const uint8_t* const bitmap, const int glyphWidth,
|
||||
const int glyphHeight, const int screenXBase, const int screenYBase,
|
||||
const bool pixelState, const GfxRenderer::Orientation orientation,
|
||||
const int displayWidth, const int displayHeight, const int widthBytes) {
|
||||
const int displayWidth, const int displayHeight, const int widthBytes,
|
||||
const int fbOriginY, const int fbRows) {
|
||||
// Non-rotated text fast path for 2-bit glyphs. Writes compact masks directly to framebuffer rows.
|
||||
// TextRotation::Rotated90CW keeps the legacy per-pixel fallback path for safety and readability.
|
||||
// BW (drawMask 0x0E) honors the caller's pixelState; grayscale passes always clear the bit.
|
||||
//
|
||||
// Tiled grayscale: `frameBuffer` may be a strip scratch with origin fbOriginY
|
||||
// and fbRows; we subtract the origin when indexing and clip rows outside the
|
||||
// band. The unsigned compare drops both off-band rows (strip mode) and any
|
||||
// out-of-frame row (full-frame mode) in one branch.
|
||||
const bool writeState = (drawMask == 0x0E) ? pixelState : false;
|
||||
|
||||
switch (orientation) {
|
||||
case GfxRenderer::LandscapeCounterClockwise: {
|
||||
for (int glyphY = 0; glyphY < glyphHeight; glyphY++) {
|
||||
const int phyY = screenYBase + glyphY;
|
||||
if (phyY < 0 || phyY >= displayHeight) continue;
|
||||
uint8_t* const row = frameBuffer + phyY * widthBytes;
|
||||
const int rowY = phyY - fbOriginY;
|
||||
if (static_cast<unsigned>(rowY) >= static_cast<unsigned>(fbRows)) continue;
|
||||
uint8_t* const row = frameBuffer + rowY * widthBytes;
|
||||
const int rowStartPixel = glyphY * glyphWidth;
|
||||
for (int glyphX = 0; glyphX < glyphWidth; glyphX += 8) {
|
||||
const int count = std::min(8, glyphWidth - glyphX);
|
||||
@@ -673,8 +688,9 @@ static void renderGlyphFast2Bit(uint8_t* const frameBuffer, const uint8_t* const
|
||||
// within each row, which is more cache-friendly than the chunk-outer alternative.
|
||||
for (int glyphY = 0; glyphY < glyphHeight; glyphY++) {
|
||||
const int phyY = displayHeight - 1 - (screenYBase + glyphY);
|
||||
if (phyY < 0 || phyY >= displayHeight) continue;
|
||||
uint8_t* const row = frameBuffer + phyY * widthBytes;
|
||||
const int rowY = phyY - fbOriginY;
|
||||
if (static_cast<unsigned>(rowY) >= static_cast<unsigned>(fbRows)) continue;
|
||||
uint8_t* const row = frameBuffer + rowY * widthBytes;
|
||||
const int rowStartPixel = glyphY * glyphWidth;
|
||||
for (int chunkEnd = glyphWidth - 1; chunkEnd >= 0; chunkEnd -= 8) {
|
||||
const int chunkStart = std::max(0, chunkEnd - 7);
|
||||
@@ -698,12 +714,14 @@ static void renderGlyphFast2Bit(uint8_t* const frameBuffer, const uint8_t* const
|
||||
|
||||
case GfxRenderer::Portrait:
|
||||
renderGlyphFast2BitPortrait<drawMask, false>(frameBuffer, bitmap, glyphWidth, glyphHeight, screenXBase,
|
||||
screenYBase, writeState, displayWidth, displayHeight, widthBytes);
|
||||
screenYBase, writeState, displayWidth, displayHeight, widthBytes,
|
||||
fbOriginY, fbRows);
|
||||
break;
|
||||
|
||||
case GfxRenderer::PortraitInverted:
|
||||
renderGlyphFast2BitPortrait<drawMask, true>(frameBuffer, bitmap, glyphWidth, glyphHeight, screenXBase,
|
||||
screenYBase, writeState, displayWidth, displayHeight, widthBytes);
|
||||
screenYBase, writeState, displayWidth, displayHeight, widthBytes,
|
||||
fbOriginY, fbRows);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -727,6 +745,23 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
||||
const int left = glyph->left;
|
||||
const int top = glyph->top;
|
||||
|
||||
// Tiled-grayscale band culling: if this glyph's physical y-extent is entirely
|
||||
// outside the active strip, skip it before the expensive bitmap decode. This
|
||||
// is what makes per-band re-rendering cheap. No-op outside strip mode.
|
||||
if constexpr (rotation == TextRotation::Rotated90CW) {
|
||||
const int ob = cursorX + fontData->ascender - top;
|
||||
const int ib = cursorY - left;
|
||||
if (!renderer.glyphIntersectsStrip(ob, ib - (width - 1), ob + height - 1, ib)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const int gx0 = cursorX + left;
|
||||
const int gy0 = cursorY - top;
|
||||
if (!renderer.glyphIntersectsStrip(gx0, gy0, gx0 + width - 1, gy0 + height - 1)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t* bitmap = renderer.getGlyphBitmap(fontData, glyph);
|
||||
|
||||
if (bitmap != nullptr) {
|
||||
@@ -752,26 +787,32 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
||||
|
||||
if constexpr (rotation == TextRotation::None) {
|
||||
// Fast path for normal text orientation. Handles all device orientations via renderGlyphFast2Bit.
|
||||
// Strip-aware: getWriteTarget() returns the band scratch when a strip is active, otherwise
|
||||
// the live framebuffer; the (fbOriginY, fbRows) pair tells the writer how to translate phyY
|
||||
// and clip rows outside the band.
|
||||
uint8_t* const fb = renderer.getWriteTarget();
|
||||
const int fbOriginY = renderer.getWriteOriginY();
|
||||
const int fbRows = renderer.getWriteRows();
|
||||
switch (drawMask) {
|
||||
case 0x0E: // BW
|
||||
renderGlyphFast2Bit<0x0E>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
||||
pixelState, renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes());
|
||||
renderGlyphFast2Bit<0x0E>(fb, bitmap, width, height, innerBase, outerBase, pixelState,
|
||||
renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes(), fbOriginY, fbRows);
|
||||
break;
|
||||
case 0x06: // raw {1,2}
|
||||
renderGlyphFast2Bit<0x06>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
||||
pixelState, renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes());
|
||||
renderGlyphFast2Bit<0x06>(fb, bitmap, width, height, innerBase, outerBase, pixelState,
|
||||
renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes(), fbOriginY, fbRows);
|
||||
break;
|
||||
case 0x04: // raw {2}
|
||||
renderGlyphFast2Bit<0x04>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
||||
pixelState, renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes());
|
||||
renderGlyphFast2Bit<0x04>(fb, bitmap, width, height, innerBase, outerBase, pixelState,
|
||||
renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes(), fbOriginY, fbRows);
|
||||
break;
|
||||
case 0x02: // raw {1}
|
||||
renderGlyphFast2Bit<0x02>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
||||
pixelState, renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes());
|
||||
renderGlyphFast2Bit<0x02>(fb, bitmap, width, height, innerBase, outerBase, pixelState,
|
||||
renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes(), fbOriginY, fbRows);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
@@ -808,8 +849,12 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
||||
}
|
||||
} else {
|
||||
// Fast path: 1-bit BW mode, non-rotated text — byte-level framebuffer writes, no drawPixel() per pixel.
|
||||
// renderGlyphFastBW is NOT strip-aware (no fbOriginY/fbRows in its signature) and would
|
||||
// mis-index into the strip scratch as if it were the full framebuffer. Today no caller
|
||||
// activates a strip in BW mode, but route to the per-pixel fallback (drawPixel is
|
||||
// strip-aware) if that ever changes so we never hand a strip buffer to the fast helper.
|
||||
if constexpr (rotation == TextRotation::None) {
|
||||
if (renderMode == GfxRenderer::BW) {
|
||||
if (renderMode == GfxRenderer::BW && !renderer.isStripActive()) {
|
||||
renderGlyphFastBW(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase, pixelState,
|
||||
renderer.getOrientation(), renderer.getDisplayWidth(), renderer.getDisplayHeight(),
|
||||
renderer.getDisplayWidthBytes());
|
||||
@@ -921,14 +966,26 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tiled grayscale: redirect writes to the strip scratch and clip to the
|
||||
// current band. Single predictable branch on the hot per-pixel path.
|
||||
uint8_t* target = frameBuffer;
|
||||
uint32_t rowY = static_cast<uint32_t>(phyY);
|
||||
if (stripActive_) {
|
||||
if (phyY < stripY0_ || phyY >= stripY0_ + stripRows_) {
|
||||
return; // pixel outside the band currently being rendered
|
||||
}
|
||||
target = stripBuf_;
|
||||
rowY = static_cast<uint32_t>(phyY - stripY0_);
|
||||
}
|
||||
|
||||
// Calculate byte position and bit position
|
||||
const uint32_t byteIndex = static_cast<uint32_t>(phyY) * getDisplayWidthBytes() + (phyX / 8);
|
||||
const uint32_t byteIndex = rowY * getDisplayWidthBytes() + (phyX / 8);
|
||||
const uint8_t bitPosition = 7 - (phyX % 8); // MSB first
|
||||
|
||||
if (state) {
|
||||
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
||||
target[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
||||
} else {
|
||||
frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit
|
||||
target[byteIndex] |= 1 << bitPosition; // Set bit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1237,7 +1294,17 @@ void GfxRenderer::fillPhysicalHSpanByte(const int phyY, const int phyX_start, co
|
||||
const int cX1 = std::min(phyX_end, (int)getDisplayWidth() - 1);
|
||||
if (cX0 > cX1 || phyY < 0 || phyY >= (int)getDisplayHeight()) return;
|
||||
|
||||
uint8_t* const row = frameBuffer + phyY * getDisplayWidthBytes();
|
||||
// Tiled grayscale: redirect to the strip scratch and drop rows outside the
|
||||
// active band. Off-band rows return cheaply before any bit-fiddling.
|
||||
uint8_t* target = frameBuffer;
|
||||
int rowY = phyY;
|
||||
if (stripActive_) {
|
||||
if (phyY < stripY0_ || phyY >= stripY0_ + stripRows_) return;
|
||||
target = stripBuf_;
|
||||
rowY = phyY - stripY0_;
|
||||
}
|
||||
|
||||
uint8_t* const row = target + rowY * getDisplayWidthBytes();
|
||||
const int startByte = cX0 >> 3;
|
||||
const int endByte = cX1 >> 3;
|
||||
const int leftBits = cX0 & 7; // first bit index within startByte
|
||||
@@ -1862,9 +1929,54 @@ static bool start_ms_valid = false;
|
||||
void GfxRenderer::clearScreen(const uint8_t color) const {
|
||||
start_ms = millis();
|
||||
start_ms_valid = true;
|
||||
if (stripActive_) {
|
||||
// Clear only the active band's scratch, not the shared framebuffer.
|
||||
memset(stripBuf_, color, static_cast<size_t>(panelWidthBytes) * stripRows_);
|
||||
return;
|
||||
}
|
||||
display.clearScreen(color);
|
||||
}
|
||||
|
||||
void GfxRenderer::beginStripTarget(uint8_t* scratch, int stripY0, int stripRows) const {
|
||||
// Band is caller-guaranteed in-bounds (the reader's grayscale loop computes
|
||||
// it); assert catches future misuse in debug before it mis-renders.
|
||||
assert(scratch != nullptr && stripRows > 0 && stripY0 >= 0 && stripY0 <= static_cast<int>(panelHeight) - stripRows);
|
||||
stripBuf_ = scratch;
|
||||
stripY0_ = stripY0;
|
||||
stripRows_ = stripRows;
|
||||
stripActive_ = true;
|
||||
}
|
||||
|
||||
void GfxRenderer::endStripTarget() const {
|
||||
stripActive_ = false;
|
||||
stripBuf_ = nullptr;
|
||||
stripY0_ = 0;
|
||||
stripRows_ = 0;
|
||||
}
|
||||
|
||||
bool GfxRenderer::glyphIntersectsStrip(int x0, int y0, int x1, int y1) const {
|
||||
if (!stripActive_) {
|
||||
return true;
|
||||
}
|
||||
// Rotate the two opposite bbox corners to physical coords. For 90-degree
|
||||
// orientations the physical bbox stays axis-aligned, so min/max of the two
|
||||
// rotated corners' Y bounds the glyph's physical y-extent.
|
||||
int ax, ay, bx, by;
|
||||
rotateCoordinates(getOrientation(), x0, y0, &ax, &ay, panelWidth, panelHeight);
|
||||
rotateCoordinates(getOrientation(), x1, y1, &bx, &by, panelWidth, panelHeight);
|
||||
const int minY = ay < by ? ay : by;
|
||||
const int maxY = ay > by ? ay : by;
|
||||
return !(maxY < stripY0_ || minY >= stripY0_ + stripRows_);
|
||||
}
|
||||
|
||||
void GfxRenderer::writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* scratch, int yStart, int numRows) const {
|
||||
// Guard the uint16_t casts below: a negative would wrap to a huge length.
|
||||
assert(yStart >= 0 && numRows > 0 && yStart <= static_cast<int>(panelHeight) - numRows);
|
||||
display.writeGrayscalePlaneStrip(lsbPlane, scratch, static_cast<uint16_t>(yStart), static_cast<uint16_t>(numRows));
|
||||
}
|
||||
|
||||
bool GfxRenderer::supportsStripGrayscale() const { return display.supportsStripGrayscale(); }
|
||||
|
||||
void GfxRenderer::invertScreen() const {
|
||||
for (uint32_t i = 0; i < frameBufferSize; i++) {
|
||||
frameBuffer[i] = ~frameBuffer[i];
|
||||
|
||||
@@ -72,6 +72,19 @@ class GfxRenderer {
|
||||
mutable FontCacheManager* fontCacheManager_ = nullptr;
|
||||
mutable std::atomic<unsigned int> refreshOverride = REFRESH_OVERRIDE_NONE;
|
||||
|
||||
// Tiled grayscale strip target. When active, drawPixel(), clearScreen(),
|
||||
// fillPhysicalHSpanByte() and renderGlyphFast2Bit() write into a caller-owned
|
||||
// scratch holding one horizontal band of physical rows
|
||||
// [_stripY0, _stripY0 + _stripRows) (panelWidthBytes wide) instead of the
|
||||
// shared framebuffer; pixels outside the band are clipped. Lets grayscale
|
||||
// planes render band-by-band straight to the controller without destroying
|
||||
// the BW framebuffer (no storeBwBuffer). Mutable because the render path is
|
||||
// const. See beginStripTarget()/endStripTarget().
|
||||
mutable uint8_t* stripBuf_ = nullptr;
|
||||
mutable int stripY0_ = 0;
|
||||
mutable int stripRows_ = 0;
|
||||
mutable bool stripActive_ = false;
|
||||
|
||||
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
|
||||
EpdFontFamily::Style style) const;
|
||||
void freeBwBufferChunks();
|
||||
@@ -209,6 +222,45 @@ class GfxRenderer {
|
||||
void copyGrayscaleLsbBuffers() const;
|
||||
void copyGrayscaleMsbBuffers() const;
|
||||
void displayGrayBuffer() const;
|
||||
|
||||
// Tiled grayscale (X4 + X3): stream one band of a plane straight to
|
||||
// controller RAM from `scratch` (panelWidthBytes * numRows, physical rows
|
||||
// [yStart, yStart+numRows)), bypassing the framebuffer.
|
||||
// supportsStripGrayscale() gates use.
|
||||
void writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* scratch, int yStart, int numRows) const;
|
||||
bool supportsStripGrayscale() const;
|
||||
|
||||
// X3-only: trade AA visual fidelity for ~2.2 s faster page-flip wall clock.
|
||||
// No effect on X4 (its single grayscale LUT already runs at ~500 ms).
|
||||
void setFastGrayscaleLut(bool fast) const { display.setFastGrayscaleLut(fast); }
|
||||
bool getFastGrayscaleLut() const { return display.getFastGrayscaleLut(); }
|
||||
|
||||
// Tiled grayscale strip target. While active, drawPixel(), clearScreen(),
|
||||
// fillPhysicalHSpanByte() and renderGlyphFast2Bit() operate on `scratch`
|
||||
// (panelWidthBytes * stripRows bytes, holding physical rows
|
||||
// [stripY0, stripY0 + stripRows)) instead of the framebuffer; pixels whose
|
||||
// physical row falls outside the band are clipped. The clip is applied after
|
||||
// the orientation rotate, so it is orientation-agnostic. Used to render
|
||||
// grayscale planes band-by-band without a full second buffer.
|
||||
void beginStripTarget(uint8_t* scratch, int stripY0, int stripRows) const;
|
||||
void endStripTarget() const;
|
||||
|
||||
// Active pixel-write target for raw writers that bypass drawPixel for speed.
|
||||
// When a strip target is active these return the band scratch plus its
|
||||
// physical-row origin and extent; otherwise the full framebuffer ([0,
|
||||
// panelHeight)). Writers subtract the origin and clip to the extent, so they
|
||||
// honor tiled-grayscale banding without per-pixel method calls.
|
||||
uint8_t* getWriteTarget() const { return stripActive_ ? stripBuf_ : frameBuffer; }
|
||||
int getWriteOriginY() const { return stripActive_ ? stripY0_ : 0; }
|
||||
int getWriteRows() const { return stripActive_ ? stripRows_ : static_cast<int>(panelHeight); }
|
||||
bool isStripActive() const { return stripActive_; }
|
||||
|
||||
// Band culling. Takes a glyph bounding box in logical screen coords and
|
||||
// returns false only when a strip is active AND the box's physical y-extent
|
||||
// lies entirely outside the active band, letting callers skip expensive
|
||||
// bitmap decode. Returns true when no strip is active.
|
||||
bool glyphIntersectsStrip(int x0, int y0, int x1, int y1) const;
|
||||
|
||||
bool storeBwBuffer(); // Returns true if buffer was stored successfully
|
||||
bool storeBwBufferRect(int x, int y, int width, int height); // Store only rows intersecting logical rect
|
||||
void restoreBwBuffer(); // Restore and free the stored buffer
|
||||
|
||||
@@ -97,6 +97,7 @@ STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode"
|
||||
STR_HIDE_BATTERY: "Hide Battery %"
|
||||
STR_EXTRA_SPACING: "Extra Paragraph Spacing"
|
||||
STR_TEXT_AA: "Text Anti-Aliasing"
|
||||
STR_FAST_AA: "Fast AA (X3 only)"
|
||||
STR_TEXT_DARKNESS: "Text Darkness"
|
||||
STR_EXTRA_DARK: "Extra Dark"
|
||||
STR_MAX_DARK: "Maximum"
|
||||
|
||||
@@ -97,6 +97,17 @@ void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay.
|
||||
|
||||
void HalDisplay::displayGrayBuffer(bool turnOffScreen) { einkDisplay.displayGrayBuffer(turnOffScreen); }
|
||||
|
||||
void HalDisplay::writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* rows, uint16_t yStart, uint16_t numRows) {
|
||||
einkDisplay.writeGrayscalePlaneStrip(lsbPlane ? EInkDisplay::GRAY_PLANE_LSB : EInkDisplay::GRAY_PLANE_MSB, rows,
|
||||
yStart, numRows);
|
||||
}
|
||||
|
||||
bool HalDisplay::supportsStripGrayscale() const { return einkDisplay.supportsStripGrayscale(); }
|
||||
|
||||
void HalDisplay::setFastGrayscaleLut(bool fast) { einkDisplay.setFastGrayscaleLut(fast); }
|
||||
|
||||
bool HalDisplay::getFastGrayscaleLut() const { return einkDisplay.getFastGrayscaleLut(); }
|
||||
|
||||
uint16_t HalDisplay::getDisplayWidth() const { return einkDisplay.getDisplayWidth(); }
|
||||
|
||||
uint16_t HalDisplay::getDisplayHeight() const { return einkDisplay.getDisplayHeight(); }
|
||||
|
||||
@@ -55,6 +55,18 @@ class HalDisplay {
|
||||
|
||||
void displayGrayBuffer(bool turnOffScreen = false);
|
||||
|
||||
// Tiled grayscale: stream one band of a plane (lsbPlane selects LSB/MSB RAM)
|
||||
// straight to the controller; supportsStripGrayscale() gates the path. See
|
||||
// EInkDisplay::writeGrayscalePlaneStrip.
|
||||
void writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* rows, uint16_t yStart, uint16_t numRows);
|
||||
bool supportsStripGrayscale() const;
|
||||
|
||||
// X3-only knob: pick between the OEM 53-frame grayscale LUT (default, slow
|
||||
// and accurate) and the 7-frame community LUT (fast, slightly darker
|
||||
// mid-tones). No effect on X4. See EInkDisplay::setFastGrayscaleLut.
|
||||
void setFastGrayscaleLut(bool fast);
|
||||
bool getFastGrayscaleLut() const;
|
||||
|
||||
// Runtime geometry passthrough
|
||||
uint16_t getDisplayWidth() const;
|
||||
uint16_t getDisplayHeight() const;
|
||||
|
||||
+1
-1
Submodule open-x4-sdk updated: 15050cb2a8...5f4a46ea3a
@@ -223,6 +223,11 @@ class CrossPointSettings {
|
||||
// Text rendering settings
|
||||
uint8_t extraParagraphSpacing = 1;
|
||||
uint8_t textAntiAliasing = 1;
|
||||
// X3-only: when on, the AA refresh uses the 7-frame community grayscale LUT
|
||||
// (~130 ms panel time) instead of the OEM 53-frame LUT (~2.4 s). Mid-tones
|
||||
// run slightly darker than X4. Matches what papyrix-reader has shipped since
|
||||
// 2025-11. Default off preserves OEM-fidelity grays. No effect on X4.
|
||||
uint8_t fastAntiAliasing = 0;
|
||||
// Text darkness (0 = normal, 1 = dark, 2 = extra dark). Default 1 preserves
|
||||
// historical AA rendering (both grayscale shades drawn in the MSB pass).
|
||||
uint8_t textDarkness = DARKNESS_DARK;
|
||||
|
||||
@@ -111,6 +111,14 @@ inline const std::vector<SettingInfo> list = {
|
||||
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
|
||||
StrId::STR_CAT_READER)
|
||||
.withSubmenu(StrId::STR_MENU_READER_FONT),
|
||||
// X3-only fast AA LUT toggle. Swaps the 53-frame OEM grayscale waveform
|
||||
// (~2.4 s panel time, X4-accurate grays) for the 7-frame community LUT
|
||||
// (~130 ms, mid-tones slightly darker). See open-x4-sdk
|
||||
// EInkDisplay::setFastGrayscaleLut for trade-offs.
|
||||
SettingInfo::Toggle(StrId::STR_FAST_AA, &CrossPointSettings::fastAntiAliasing, "fastAntiAliasing",
|
||||
StrId::STR_CAT_READER)
|
||||
.withSubmenu(StrId::STR_MENU_READER_FONT)
|
||||
.withDeviceTarget(SettingDeviceTarget::X3),
|
||||
SettingInfo::Enum(StrId::STR_TEXT_DARKNESS, &CrossPointSettings::textDarkness,
|
||||
{StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, "textDarkness",
|
||||
StrId::STR_CAT_READER)
|
||||
|
||||
@@ -104,6 +104,70 @@ void logReaderMemSnapshot(const char* stage) {
|
||||
inline void logReaderMemSnapshot(const char*) {}
|
||||
#endif
|
||||
|
||||
// Tiled grayscale: render each plane band-by-band into a small scratch and
|
||||
// stream straight to the controller, leaving the BW framebuffer intact so no
|
||||
// storeBwBuffer / restoreBwBuffer is needed. Controller RAM is re-synced from
|
||||
// the live framebuffer afterward via cleanupGrayscaleWithFrameBuffer().
|
||||
//
|
||||
// Returns true when the strip path ran end-to-end (controller now holds the AA
|
||||
// planes and the live BW frame is clean). Returns false when the controller
|
||||
// doesn't support strip grayscale OR the scratch allocation fails — caller
|
||||
// should fall back to the legacy storeBwBufferRect path.
|
||||
//
|
||||
// The page is re-rendered ceil(panelHeight/STRIP_ROWS) times per plane, but
|
||||
// renderCharImpl culls out-of-band glyphs before bitmap decode so the cost
|
||||
// stays close to one render. Only renderTextOnly() is called here, matching the
|
||||
// legacy AA pass — images and HRs do not participate in grayscale.
|
||||
bool runTiledGrayscalePass(GfxRenderer& renderer, const Page& page, int fontId, int marginLeft, int contentTop,
|
||||
bool fastAA) {
|
||||
if (!renderer.supportsStripGrayscale()) return false;
|
||||
|
||||
// Push the SETTINGS toggle into the SDK before the AA refresh. No-op on X4;
|
||||
// on X3 picks between OEM _gc (slow/accurate) and community _grayscale
|
||||
// (fast/darker mid-tones). Re-applied per render so a settings change takes
|
||||
// effect on the next page flip without rebooting.
|
||||
renderer.setFastGrayscaleLut(fastAA);
|
||||
|
||||
// Strip height trades scratch size for the number of re-renders. Each render
|
||||
// pays layout + glyph-cull overhead even when bitmap decode is skipped, so
|
||||
// fewer/bigger bands win as long as the scratch fits. 240 rows × ~100 bytes
|
||||
// ≈ ~24 KB — still well below the legacy partial-snapshot footprint while
|
||||
// cutting X3 (480 px) to 2 bands/plane and X4 (800 px) to 4 bands/plane.
|
||||
constexpr int STRIP_ROWS = 240;
|
||||
const int gh = renderer.getDisplayHeight();
|
||||
const int gwBytes = renderer.getDisplayWidthBytes();
|
||||
|
||||
auto scratch = std::unique_ptr<uint8_t[]>(new (std::nothrow) uint8_t[static_cast<size_t>(gwBytes) * STRIP_ROWS]);
|
||||
if (!scratch) {
|
||||
LOG_INF("ERS", "Tiled grayscale: scratch alloc failed (%d bytes); falling back to legacy path",
|
||||
gwBytes * STRIP_ROWS);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto renderPlane = [&](GfxRenderer::RenderMode mode, bool lsbPlane) {
|
||||
renderer.setRenderMode(mode);
|
||||
for (int y = 0; y < gh; y += STRIP_ROWS) {
|
||||
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
|
||||
renderer.beginStripTarget(scratch.get(), y, rows);
|
||||
renderer.clearScreen(0x00);
|
||||
page.renderTextOnly(renderer, fontId, marginLeft, contentTop);
|
||||
renderer.endStripTarget();
|
||||
renderer.writeGrayscalePlaneStrip(lsbPlane, scratch.get(), y, rows);
|
||||
}
|
||||
};
|
||||
|
||||
renderPlane(GfxRenderer::GRAYSCALE_LSB, true);
|
||||
renderPlane(GfxRenderer::GRAYSCALE_MSB, false);
|
||||
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
renderer.displayGrayBuffer();
|
||||
|
||||
// BW framebuffer is intact; re-sync controller RAM for the next differential
|
||||
// page turn directly from it.
|
||||
renderer.cleanupGrayscaleWithFrameBuffer();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Computes the [0..100] EPUB progress percent. Returns 0 when pageCount is unknown (sync/bookmark
|
||||
// pre-render writes), in which case the next saveProgress() will overwrite progress.bin with the
|
||||
// real value before the user can leave the reader.
|
||||
@@ -2027,49 +2091,6 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
}
|
||||
const auto tDisplay = millis();
|
||||
|
||||
// Save bw buffer to reset buffer state after grayscale data sync.
|
||||
// Pre-check heap to avoid entering the chunk-retry path when success is unlikely.
|
||||
const uint32_t bwStoreFreeHeap = esp_get_free_heap_size();
|
||||
const uint32_t bwStoreContigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
const bool shouldAttemptBwSnapshot =
|
||||
bwStoreFreeHeap >= BW_SNAPSHOT_MIN_FREE_HEAP_BYTES && bwStoreContigHeap >= BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES;
|
||||
|
||||
logReaderMemSnapshot("bw_store_begin");
|
||||
bool bwBufferStored = false;
|
||||
if (shouldAttemptBwSnapshot) {
|
||||
// Snapshot must cover every BW pixel drawn outside the grayscale text region —
|
||||
// status bar (in top/bottom margins), truncated-section hint, edge progress bars.
|
||||
// The AA pass clears the whole BW buffer to 0x00, renders text-only, and pushes
|
||||
// grayscale; rows outside the restored band end up zero, causing a BW/grayscale
|
||||
// mismatch in the status bar area on the next page turn — visible as ghosting
|
||||
// at the bottom in Landscape CCW (issue #256).
|
||||
//
|
||||
// The X argument is only meaningful in portrait (where panel rows map to logical
|
||||
// X); in landscape, panel rows map to logical Y and the X range doesn't affect
|
||||
// the saved row band. So spanning the full logical Y always saves the full panel
|
||||
// rows in landscape (≈ full framebuffer), while in portrait we keep the
|
||||
// optimization by clipping to the content X band — the status bar still fits
|
||||
// since it's anchored to the same content margins.
|
||||
const int contentLeft = orientedMarginLeft;
|
||||
const int contentRight = std::max(contentLeft, renderer.getScreenWidth() - orientedMarginRight);
|
||||
bwBufferStored = renderer.storeBwBufferRect(contentLeft, 0, contentRight - contentLeft, renderer.getScreenHeight());
|
||||
} else {
|
||||
LOG_INF("ERS", "Skipping BW snapshot precheck (free=%lu contig=%lu, need free>=%lu contig>=%lu)", bwStoreFreeHeap,
|
||||
bwStoreContigHeap, static_cast<uint32_t>(BW_SNAPSHOT_MIN_FREE_HEAP_BYTES),
|
||||
static_cast<uint32_t>(BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES));
|
||||
}
|
||||
const auto tBwStore = millis();
|
||||
logReaderMemSnapshot("bw_store_end");
|
||||
if (!bwBufferStored) {
|
||||
if (aaEnabledForThisRender && !antiAliasingSuspendedLowMemory) {
|
||||
antiAliasingSuspendedLowMemory = true;
|
||||
const uint32_t freeHeapNow = esp_get_free_heap_size();
|
||||
const uint32_t contigHeapNow = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
LOG_INF("ERS", "Suspending text anti-aliasing due to low heap (free=%lu contig=%lu)", freeHeapNow, contigHeapNow);
|
||||
}
|
||||
LOG_INF("ERS", "Skipping grayscale/BW-restore for this page (insufficient heap for BW snapshot)");
|
||||
}
|
||||
|
||||
// Only schedule the half-refresh if at least one real image was decoded on this page.
|
||||
// Placeholder-only pages don't deposit grayscale data that needs settling.
|
||||
if (page->hasImages() && !page->allImagesArePlaceholders(effectiveForceLoad) &&
|
||||
@@ -2077,69 +2098,138 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
pendingHalfRefreshAfterImagePage = true;
|
||||
}
|
||||
|
||||
// grayscale rendering
|
||||
// TODO: Only do this if font supports it
|
||||
if (aaEnabledForThisRender && bwBufferStored) {
|
||||
logReaderMemSnapshot("gray_lsb_begin");
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
const auto tGrayLsb = millis();
|
||||
logReaderMemSnapshot("gray_lsb_end");
|
||||
|
||||
// Render and copy to MSB buffer
|
||||
logReaderMemSnapshot("gray_msb_begin");
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
const auto tGrayMsb = millis();
|
||||
logReaderMemSnapshot("gray_msb_end");
|
||||
|
||||
// display grayscale part
|
||||
logReaderMemSnapshot("gray_display_begin");
|
||||
renderer.displayGrayBuffer();
|
||||
const auto tGrayDisplay = millis();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
fcm->logStats("gray");
|
||||
logReaderMemSnapshot("gray_display_end");
|
||||
|
||||
// restore the bw data
|
||||
logReaderMemSnapshot("bw_restore_begin");
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
logReaderMemSnapshot("bw_restore_end");
|
||||
// Tiled grayscale: try the strip path first when AA is on and the controller
|
||||
// supports it. It allocates an ~8 KB scratch instead of saving a partial BW
|
||||
// frame, leaving the live BW framebuffer intact (no storeBwBuffer needed).
|
||||
// Falls through to the legacy snapshot path if strip is unsupported or the
|
||||
// scratch can't be allocated.
|
||||
bool grayscaleDone = false;
|
||||
uint32_t tiledGrayMs = 0;
|
||||
if (aaEnabledForThisRender) {
|
||||
logReaderMemSnapshot("tiled_gray_begin");
|
||||
const auto tTiledBegin = millis();
|
||||
grayscaleDone = runTiledGrayscalePass(renderer, *page, getEffectiveReaderFontId(), orientedMarginLeft, contentTop,
|
||||
SETTINGS.fastAntiAliasing);
|
||||
if (grayscaleDone) {
|
||||
tiledGrayMs = millis() - tTiledBegin;
|
||||
fcm->logStats("tiled_gray");
|
||||
logReaderMemSnapshot("tiled_gray_end");
|
||||
}
|
||||
}
|
||||
|
||||
if (grayscaleDone) {
|
||||
const auto tEnd = millis();
|
||||
lastRenderStats.usedGrayscale = true;
|
||||
lastRenderStats.phases = {tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender,
|
||||
tBwStore - tDisplay, tGrayLsb - tBwStore, tGrayMsb - tGrayLsb,
|
||||
tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0};
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums "
|
||||
"gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore,
|
||||
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
|
||||
lastRenderStats.phases = {tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, 0, tiledGrayMs, 0, 0, 0,
|
||||
tEnd - t0};
|
||||
LOG_DBG("ERS", "Page render (tiled): prewarm=%lums bw_render=%lums display=%lums tiled_gray=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tiledGrayMs, tEnd - t0);
|
||||
} else {
|
||||
uint32_t bwRestoreMs = 0;
|
||||
if (bwBufferStored) {
|
||||
// Legacy fallback: save (partial) BW frame, render LSB+MSB planes into the
|
||||
// live framebuffer, display, then restore. Pre-check heap to avoid entering
|
||||
// the chunk-retry path when success is unlikely. Skip the snapshot entirely
|
||||
// when AA is off — there is nothing to restore from since no grayscale pass
|
||||
// will run.
|
||||
const uint32_t bwStoreFreeHeap = esp_get_free_heap_size();
|
||||
const uint32_t bwStoreContigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
const bool shouldAttemptBwSnapshot = aaEnabledForThisRender && bwStoreFreeHeap >= BW_SNAPSHOT_MIN_FREE_HEAP_BYTES &&
|
||||
bwStoreContigHeap >= BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES;
|
||||
|
||||
logReaderMemSnapshot("bw_store_begin");
|
||||
bool bwBufferStored = false;
|
||||
if (shouldAttemptBwSnapshot) {
|
||||
// Snapshot the full framebuffer. The AA pass calls clearScreen(0x00) which
|
||||
// touches every panel row, and restoreBwBuffer() ends with
|
||||
// cleanupGrayscaleBuffers(frameBuffer) that syncs the controller's belief
|
||||
// from the framebuffer — so any row not in the snapshot ends up zero in the
|
||||
// FB and the controller drifts out of sync with what's physically on the
|
||||
// panel, producing ghosting on the next page turn (issue #256).
|
||||
bwBufferStored = renderer.storeBwBuffer();
|
||||
} else if (aaEnabledForThisRender) {
|
||||
LOG_INF("ERS", "Skipping BW snapshot precheck (free=%lu contig=%lu, need free>=%lu contig>=%lu)", bwStoreFreeHeap,
|
||||
bwStoreContigHeap, static_cast<uint32_t>(BW_SNAPSHOT_MIN_FREE_HEAP_BYTES),
|
||||
static_cast<uint32_t>(BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES));
|
||||
}
|
||||
const auto tBwStore = millis();
|
||||
logReaderMemSnapshot("bw_store_end");
|
||||
if (aaEnabledForThisRender && !bwBufferStored) {
|
||||
if (!antiAliasingSuspendedLowMemory) {
|
||||
antiAliasingSuspendedLowMemory = true;
|
||||
const uint32_t freeHeapNow = esp_get_free_heap_size();
|
||||
const uint32_t contigHeapNow = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
LOG_INF("ERS", "Suspending text anti-aliasing due to low heap (free=%lu contig=%lu)", freeHeapNow,
|
||||
contigHeapNow);
|
||||
}
|
||||
LOG_INF("ERS", "Skipping grayscale/BW-restore for this page (insufficient heap for BW snapshot)");
|
||||
}
|
||||
|
||||
// grayscale rendering
|
||||
// TODO: Only do this if font supports it
|
||||
if (aaEnabledForThisRender && bwBufferStored) {
|
||||
// Push fast-AA toggle into the SDK before the AA refresh (X3 only; no-op
|
||||
// on X4). Mirrors what runTiledGrayscalePass() does.
|
||||
renderer.setFastGrayscaleLut(SETTINGS.fastAntiAliasing);
|
||||
logReaderMemSnapshot("gray_lsb_begin");
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
const auto tGrayLsb = millis();
|
||||
logReaderMemSnapshot("gray_lsb_end");
|
||||
|
||||
// Render and copy to MSB buffer
|
||||
logReaderMemSnapshot("gray_msb_begin");
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
const auto tGrayMsb = millis();
|
||||
logReaderMemSnapshot("gray_msb_end");
|
||||
|
||||
// display grayscale part
|
||||
logReaderMemSnapshot("gray_display_begin");
|
||||
renderer.displayGrayBuffer();
|
||||
const auto tGrayDisplay = millis();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
fcm->logStats("gray");
|
||||
logReaderMemSnapshot("gray_display_end");
|
||||
|
||||
// restore the bw data
|
||||
logReaderMemSnapshot("bw_restore_begin");
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
logReaderMemSnapshot("bw_restore_end");
|
||||
bwRestoreMs = tBwRestore - tBwStore;
|
||||
}
|
||||
|
||||
const auto tEnd = millis();
|
||||
lastRenderStats.usedGrayscale = false;
|
||||
lastRenderStats.phases = {
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, 0, 0, 0, bwRestoreMs,
|
||||
tEnd - t0};
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, bwRestoreMs, tEnd - t0);
|
||||
const auto tEnd = millis();
|
||||
lastRenderStats.usedGrayscale = true;
|
||||
lastRenderStats.phases = {tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender,
|
||||
tBwStore - tDisplay, tGrayLsb - tBwStore, tGrayMsb - tGrayLsb,
|
||||
tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0};
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums "
|
||||
"gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore,
|
||||
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
|
||||
} else {
|
||||
uint32_t bwRestoreMs = 0;
|
||||
if (bwBufferStored) {
|
||||
// restore the bw data
|
||||
logReaderMemSnapshot("bw_restore_begin");
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
logReaderMemSnapshot("bw_restore_end");
|
||||
bwRestoreMs = tBwRestore - tBwStore;
|
||||
}
|
||||
|
||||
const auto tEnd = millis();
|
||||
lastRenderStats.usedGrayscale = false;
|
||||
lastRenderStats.phases = {
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, 0, 0, 0, bwRestoreMs,
|
||||
tEnd - t0};
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, bwRestoreMs, tEnd - t0);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto* cacheManager = renderer.getFontCacheManager()) {
|
||||
@@ -2195,24 +2285,22 @@ void EpubReaderActivity::displayPreRenderedPage(const Page& page, const int orie
|
||||
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||
}
|
||||
|
||||
// Grayscale AA pass (same as normal render — BW snapshot, gray LSB+MSB, restore).
|
||||
// Grayscale AA pass. Prefer the tiled strip path (no BW snapshot needed);
|
||||
// fall back to the legacy storeBwBufferRect path on controllers that don't
|
||||
// support strip grayscale or when the strip scratch can't be allocated.
|
||||
const bool aaConfigured = SETTINGS.textAntiAliasing && !antiAliasingSuspendedLowMemory;
|
||||
if (aaConfigured) {
|
||||
if (runTiledGrayscalePass(renderer, page, getEffectiveReaderFontId(), orientedMarginLeft, contentTop,
|
||||
SETTINGS.fastAntiAliasing)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t freeHeap = esp_get_free_heap_size();
|
||||
const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
if (freeHeap >= BW_SNAPSHOT_MIN_FREE_HEAP_BYTES && contigHeap >= BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES) {
|
||||
const int contentLeft = orientedMarginLeft;
|
||||
const int contentRight = std::max(contentLeft, renderer.getScreenWidth() - orientedMarginRight);
|
||||
const int contentBottom = std::max(contentTop, renderer.getScreenHeight() - orientedMarginBottom);
|
||||
int bandTop = 0;
|
||||
int bandBottom = std::max(0, contentBottom - contentTop);
|
||||
if (!computePageDynamicYBand(page, renderer, getEffectiveReaderFontId(), bandBottom, &bandTop, &bandBottom)) {
|
||||
bandTop = 0;
|
||||
bandBottom = std::max(0, contentBottom - contentTop);
|
||||
}
|
||||
const int snapshotTop = contentTop + bandTop;
|
||||
const int snapshotHeight = std::max(0, bandBottom - bandTop);
|
||||
if (renderer.storeBwBufferRect(contentLeft, snapshotTop, contentRight - contentLeft, snapshotHeight)) {
|
||||
// Full-framebuffer snapshot — see the matching site above for the rationale (issue #256).
|
||||
if (renderer.storeBwBuffer()) {
|
||||
renderer.setFastGrayscaleLut(SETTINGS.fastAntiAliasing);
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
page.renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||
|
||||
Reference in New Issue
Block a user