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;
|
uint8_t* fb;
|
||||||
GfxRenderer::RenderMode mode;
|
GfxRenderer::RenderMode mode;
|
||||||
uint16_t displayWidthBytes; // Runtime framebuffer stride (X4: 100, X3: 99)
|
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:
|
// Orientation is collapsed into a linear transform:
|
||||||
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
|
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
|
||||||
@@ -28,7 +34,9 @@ struct DirectPixelWriter {
|
|||||||
int rowPhyXBase, rowPhyYBase;
|
int rowPhyXBase, rowPhyYBase;
|
||||||
|
|
||||||
void init(GfxRenderer& renderer) {
|
void init(GfxRenderer& renderer) {
|
||||||
fb = renderer.getFrameBuffer();
|
fb = renderer.getWriteTarget();
|
||||||
|
originY = renderer.getWriteOriginY();
|
||||||
|
clipRows = renderer.getWriteRows();
|
||||||
mode = renderer.getRenderMode();
|
mode = renderer.getRenderMode();
|
||||||
displayWidthBytes = renderer.getDisplayWidthBytes();
|
displayWidthBytes = renderer.getDisplayWidthBytes();
|
||||||
|
|
||||||
@@ -120,7 +128,12 @@ struct DirectPixelWriter {
|
|||||||
const int phyX = rowPhyXBase + logicalX * phyXStepX;
|
const int phyX = rowPhyXBase + logicalX * phyXStepX;
|
||||||
const int phyY = rowPhyYBase + logicalX * phyYStepX;
|
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));
|
const uint8_t bitMask = 1 << (7 - (phyX & 7));
|
||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
|
|||||||
+138
-26
@@ -8,6 +8,8 @@
|
|||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#include "FontCacheManager.h"
|
#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=false → Portrait (phyY counts down, phyBitPos counts up).
|
||||||
// inverted=true → PortraitInverted (phyY counts up, phyBitPos counts down).
|
// inverted=true → PortraitInverted (phyY counts up, phyBitPos counts down).
|
||||||
// Both template params are compile-time constants; all ternaries fold away.
|
// 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>
|
template <uint8_t drawMask, bool inverted>
|
||||||
static void renderGlyphFast2BitPortrait(uint8_t* const frameBuffer, const uint8_t* const bitmap, const int glyphWidth,
|
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 int glyphHeight, const int screenXBase, const int screenYBase,
|
||||||
const bool writeState, const int displayWidth, const int displayHeight,
|
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++) {
|
for (int glyphX = 0; glyphX < glyphWidth; glyphX++) {
|
||||||
const int phyY = inverted ? (screenXBase + glyphX) : (displayHeight - 1 - (screenXBase + glyphX));
|
const int phyY = inverted ? (screenXBase + glyphX) : (displayHeight - 1 - (screenXBase + glyphX));
|
||||||
if (phyY < 0 || phyY >= displayHeight) continue;
|
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) {
|
for (int glyphY = 0; glyphY < glyphHeight; glyphY += 8) {
|
||||||
const int count = std::min(8, glyphHeight - glyphY);
|
const int count = std::min(8, glyphHeight - glyphY);
|
||||||
const uint8_t mask = build2BitColMask<drawMask>(bitmap, glyphWidth, glyphX, glyphY, count, inverted);
|
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,
|
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 int glyphHeight, const int screenXBase, const int screenYBase,
|
||||||
const bool pixelState, const GfxRenderer::Orientation orientation,
|
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.
|
// 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.
|
// 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.
|
// 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;
|
const bool writeState = (drawMask == 0x0E) ? pixelState : false;
|
||||||
|
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case GfxRenderer::LandscapeCounterClockwise: {
|
case GfxRenderer::LandscapeCounterClockwise: {
|
||||||
for (int glyphY = 0; glyphY < glyphHeight; glyphY++) {
|
for (int glyphY = 0; glyphY < glyphHeight; glyphY++) {
|
||||||
const int phyY = screenYBase + glyphY;
|
const int phyY = screenYBase + glyphY;
|
||||||
if (phyY < 0 || phyY >= displayHeight) continue;
|
const int rowY = phyY - fbOriginY;
|
||||||
uint8_t* const row = frameBuffer + phyY * widthBytes;
|
if (static_cast<unsigned>(rowY) >= static_cast<unsigned>(fbRows)) continue;
|
||||||
|
uint8_t* const row = frameBuffer + rowY * widthBytes;
|
||||||
const int rowStartPixel = glyphY * glyphWidth;
|
const int rowStartPixel = glyphY * glyphWidth;
|
||||||
for (int glyphX = 0; glyphX < glyphWidth; glyphX += 8) {
|
for (int glyphX = 0; glyphX < glyphWidth; glyphX += 8) {
|
||||||
const int count = std::min(8, glyphWidth - glyphX);
|
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.
|
// within each row, which is more cache-friendly than the chunk-outer alternative.
|
||||||
for (int glyphY = 0; glyphY < glyphHeight; glyphY++) {
|
for (int glyphY = 0; glyphY < glyphHeight; glyphY++) {
|
||||||
const int phyY = displayHeight - 1 - (screenYBase + glyphY);
|
const int phyY = displayHeight - 1 - (screenYBase + glyphY);
|
||||||
if (phyY < 0 || phyY >= displayHeight) continue;
|
const int rowY = phyY - fbOriginY;
|
||||||
uint8_t* const row = frameBuffer + phyY * widthBytes;
|
if (static_cast<unsigned>(rowY) >= static_cast<unsigned>(fbRows)) continue;
|
||||||
|
uint8_t* const row = frameBuffer + rowY * widthBytes;
|
||||||
const int rowStartPixel = glyphY * glyphWidth;
|
const int rowStartPixel = glyphY * glyphWidth;
|
||||||
for (int chunkEnd = glyphWidth - 1; chunkEnd >= 0; chunkEnd -= 8) {
|
for (int chunkEnd = glyphWidth - 1; chunkEnd >= 0; chunkEnd -= 8) {
|
||||||
const int chunkStart = std::max(0, chunkEnd - 7);
|
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:
|
case GfxRenderer::Portrait:
|
||||||
renderGlyphFast2BitPortrait<drawMask, false>(frameBuffer, bitmap, glyphWidth, glyphHeight, screenXBase,
|
renderGlyphFast2BitPortrait<drawMask, false>(frameBuffer, bitmap, glyphWidth, glyphHeight, screenXBase,
|
||||||
screenYBase, writeState, displayWidth, displayHeight, widthBytes);
|
screenYBase, writeState, displayWidth, displayHeight, widthBytes,
|
||||||
|
fbOriginY, fbRows);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GfxRenderer::PortraitInverted:
|
case GfxRenderer::PortraitInverted:
|
||||||
renderGlyphFast2BitPortrait<drawMask, true>(frameBuffer, bitmap, glyphWidth, glyphHeight, screenXBase,
|
renderGlyphFast2BitPortrait<drawMask, true>(frameBuffer, bitmap, glyphWidth, glyphHeight, screenXBase,
|
||||||
screenYBase, writeState, displayWidth, displayHeight, widthBytes);
|
screenYBase, writeState, displayWidth, displayHeight, widthBytes,
|
||||||
|
fbOriginY, fbRows);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,6 +745,23 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
|||||||
const int left = glyph->left;
|
const int left = glyph->left;
|
||||||
const int top = glyph->top;
|
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);
|
const uint8_t* bitmap = renderer.getGlyphBitmap(fontData, glyph);
|
||||||
|
|
||||||
if (bitmap != nullptr) {
|
if (bitmap != nullptr) {
|
||||||
@@ -752,26 +787,32 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
|||||||
|
|
||||||
if constexpr (rotation == TextRotation::None) {
|
if constexpr (rotation == TextRotation::None) {
|
||||||
// Fast path for normal text orientation. Handles all device orientations via renderGlyphFast2Bit.
|
// 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) {
|
switch (drawMask) {
|
||||||
case 0x0E: // BW
|
case 0x0E: // BW
|
||||||
renderGlyphFast2Bit<0x0E>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
renderGlyphFast2Bit<0x0E>(fb, bitmap, width, height, innerBase, outerBase, pixelState,
|
||||||
pixelState, renderer.getOrientation(), renderer.getDisplayWidth(),
|
renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes());
|
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes(), fbOriginY, fbRows);
|
||||||
break;
|
break;
|
||||||
case 0x06: // raw {1,2}
|
case 0x06: // raw {1,2}
|
||||||
renderGlyphFast2Bit<0x06>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
renderGlyphFast2Bit<0x06>(fb, bitmap, width, height, innerBase, outerBase, pixelState,
|
||||||
pixelState, renderer.getOrientation(), renderer.getDisplayWidth(),
|
renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes());
|
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes(), fbOriginY, fbRows);
|
||||||
break;
|
break;
|
||||||
case 0x04: // raw {2}
|
case 0x04: // raw {2}
|
||||||
renderGlyphFast2Bit<0x04>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
renderGlyphFast2Bit<0x04>(fb, bitmap, width, height, innerBase, outerBase, pixelState,
|
||||||
pixelState, renderer.getOrientation(), renderer.getDisplayWidth(),
|
renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes());
|
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes(), fbOriginY, fbRows);
|
||||||
break;
|
break;
|
||||||
case 0x02: // raw {1}
|
case 0x02: // raw {1}
|
||||||
renderGlyphFast2Bit<0x02>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
renderGlyphFast2Bit<0x02>(fb, bitmap, width, height, innerBase, outerBase, pixelState,
|
||||||
pixelState, renderer.getOrientation(), renderer.getDisplayWidth(),
|
renderer.getOrientation(), renderer.getDisplayWidth(),
|
||||||
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes());
|
renderer.getDisplayHeight(), renderer.getDisplayWidthBytes(), fbOriginY, fbRows);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -808,8 +849,12 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fast path: 1-bit BW mode, non-rotated text — byte-level framebuffer writes, no drawPixel() per pixel.
|
// 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 constexpr (rotation == TextRotation::None) {
|
||||||
if (renderMode == GfxRenderer::BW) {
|
if (renderMode == GfxRenderer::BW && !renderer.isStripActive()) {
|
||||||
renderGlyphFastBW(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase, pixelState,
|
renderGlyphFastBW(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase, pixelState,
|
||||||
renderer.getOrientation(), renderer.getDisplayWidth(), renderer.getDisplayHeight(),
|
renderer.getOrientation(), renderer.getDisplayWidth(), renderer.getDisplayHeight(),
|
||||||
renderer.getDisplayWidthBytes());
|
renderer.getDisplayWidthBytes());
|
||||||
@@ -921,14 +966,26 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
|||||||
return;
|
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
|
// 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
|
const uint8_t bitPosition = 7 - (phyX % 8); // MSB first
|
||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
target[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
||||||
} else {
|
} 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);
|
const int cX1 = std::min(phyX_end, (int)getDisplayWidth() - 1);
|
||||||
if (cX0 > cX1 || phyY < 0 || phyY >= (int)getDisplayHeight()) return;
|
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 startByte = cX0 >> 3;
|
||||||
const int endByte = cX1 >> 3;
|
const int endByte = cX1 >> 3;
|
||||||
const int leftBits = cX0 & 7; // first bit index within startByte
|
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 {
|
void GfxRenderer::clearScreen(const uint8_t color) const {
|
||||||
start_ms = millis();
|
start_ms = millis();
|
||||||
start_ms_valid = true;
|
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);
|
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 {
|
void GfxRenderer::invertScreen() const {
|
||||||
for (uint32_t i = 0; i < frameBufferSize; i++) {
|
for (uint32_t i = 0; i < frameBufferSize; i++) {
|
||||||
frameBuffer[i] = ~frameBuffer[i];
|
frameBuffer[i] = ~frameBuffer[i];
|
||||||
|
|||||||
@@ -72,6 +72,19 @@ class GfxRenderer {
|
|||||||
mutable FontCacheManager* fontCacheManager_ = nullptr;
|
mutable FontCacheManager* fontCacheManager_ = nullptr;
|
||||||
mutable std::atomic<unsigned int> refreshOverride = REFRESH_OVERRIDE_NONE;
|
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,
|
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
|
||||||
EpdFontFamily::Style style) const;
|
EpdFontFamily::Style style) const;
|
||||||
void freeBwBufferChunks();
|
void freeBwBufferChunks();
|
||||||
@@ -209,6 +222,45 @@ class GfxRenderer {
|
|||||||
void copyGrayscaleLsbBuffers() const;
|
void copyGrayscaleLsbBuffers() const;
|
||||||
void copyGrayscaleMsbBuffers() const;
|
void copyGrayscaleMsbBuffers() const;
|
||||||
void displayGrayBuffer() 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 storeBwBuffer(); // Returns true if buffer was stored successfully
|
||||||
bool storeBwBufferRect(int x, int y, int width, int height); // Store only rows intersecting logical rect
|
bool storeBwBufferRect(int x, int y, int width, int height); // Store only rows intersecting logical rect
|
||||||
void restoreBwBuffer(); // Restore and free the stored buffer
|
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_HIDE_BATTERY: "Hide Battery %"
|
||||||
STR_EXTRA_SPACING: "Extra Paragraph Spacing"
|
STR_EXTRA_SPACING: "Extra Paragraph Spacing"
|
||||||
STR_TEXT_AA: "Text Anti-Aliasing"
|
STR_TEXT_AA: "Text Anti-Aliasing"
|
||||||
|
STR_FAST_AA: "Fast AA (X3 only)"
|
||||||
STR_TEXT_DARKNESS: "Text Darkness"
|
STR_TEXT_DARKNESS: "Text Darkness"
|
||||||
STR_EXTRA_DARK: "Extra Dark"
|
STR_EXTRA_DARK: "Extra Dark"
|
||||||
STR_MAX_DARK: "Maximum"
|
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::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::getDisplayWidth() const { return einkDisplay.getDisplayWidth(); }
|
||||||
|
|
||||||
uint16_t HalDisplay::getDisplayHeight() const { return einkDisplay.getDisplayHeight(); }
|
uint16_t HalDisplay::getDisplayHeight() const { return einkDisplay.getDisplayHeight(); }
|
||||||
|
|||||||
@@ -55,6 +55,18 @@ class HalDisplay {
|
|||||||
|
|
||||||
void displayGrayBuffer(bool turnOffScreen = false);
|
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
|
// Runtime geometry passthrough
|
||||||
uint16_t getDisplayWidth() const;
|
uint16_t getDisplayWidth() const;
|
||||||
uint16_t getDisplayHeight() const;
|
uint16_t getDisplayHeight() const;
|
||||||
|
|||||||
+1
-1
Submodule open-x4-sdk updated: 15050cb2a8...5f4a46ea3a
@@ -223,6 +223,11 @@ class CrossPointSettings {
|
|||||||
// Text rendering settings
|
// Text rendering settings
|
||||||
uint8_t extraParagraphSpacing = 1;
|
uint8_t extraParagraphSpacing = 1;
|
||||||
uint8_t textAntiAliasing = 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
|
// Text darkness (0 = normal, 1 = dark, 2 = extra dark). Default 1 preserves
|
||||||
// historical AA rendering (both grayscale shades drawn in the MSB pass).
|
// historical AA rendering (both grayscale shades drawn in the MSB pass).
|
||||||
uint8_t textDarkness = DARKNESS_DARK;
|
uint8_t textDarkness = DARKNESS_DARK;
|
||||||
|
|||||||
@@ -111,6 +111,14 @@ inline const std::vector<SettingInfo> list = {
|
|||||||
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
|
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
|
||||||
StrId::STR_CAT_READER)
|
StrId::STR_CAT_READER)
|
||||||
.withSubmenu(StrId::STR_MENU_READER_FONT),
|
.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,
|
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_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, "textDarkness",
|
||||||
StrId::STR_CAT_READER)
|
StrId::STR_CAT_READER)
|
||||||
|
|||||||
@@ -104,6 +104,70 @@ void logReaderMemSnapshot(const char* stage) {
|
|||||||
inline void logReaderMemSnapshot(const char*) {}
|
inline void logReaderMemSnapshot(const char*) {}
|
||||||
#endif
|
#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
|
// 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
|
// pre-render writes), in which case the next saveProgress() will overwrite progress.bin with the
|
||||||
// real value before the user can leave the reader.
|
// 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();
|
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.
|
// 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.
|
// Placeholder-only pages don't deposit grayscale data that needs settling.
|
||||||
if (page->hasImages() && !page->allImagesArePlaceholders(effectiveForceLoad) &&
|
if (page->hasImages() && !page->allImagesArePlaceholders(effectiveForceLoad) &&
|
||||||
@@ -2077,69 +2098,138 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
pendingHalfRefreshAfterImagePage = true;
|
pendingHalfRefreshAfterImagePage = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// grayscale rendering
|
// Tiled grayscale: try the strip path first when AA is on and the controller
|
||||||
// TODO: Only do this if font supports it
|
// supports it. It allocates an ~8 KB scratch instead of saving a partial BW
|
||||||
if (aaEnabledForThisRender && bwBufferStored) {
|
// frame, leaving the live BW framebuffer intact (no storeBwBuffer needed).
|
||||||
logReaderMemSnapshot("gray_lsb_begin");
|
// Falls through to the legacy snapshot path if strip is unsupported or the
|
||||||
renderer.clearScreen(0x00);
|
// scratch can't be allocated.
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
bool grayscaleDone = false;
|
||||||
page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
uint32_t tiledGrayMs = 0;
|
||||||
renderer.copyGrayscaleLsbBuffers();
|
if (aaEnabledForThisRender) {
|
||||||
const auto tGrayLsb = millis();
|
logReaderMemSnapshot("tiled_gray_begin");
|
||||||
logReaderMemSnapshot("gray_lsb_end");
|
const auto tTiledBegin = millis();
|
||||||
|
grayscaleDone = runTiledGrayscalePass(renderer, *page, getEffectiveReaderFontId(), orientedMarginLeft, contentTop,
|
||||||
// Render and copy to MSB buffer
|
SETTINGS.fastAntiAliasing);
|
||||||
logReaderMemSnapshot("gray_msb_begin");
|
if (grayscaleDone) {
|
||||||
renderer.clearScreen(0x00);
|
tiledGrayMs = millis() - tTiledBegin;
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
fcm->logStats("tiled_gray");
|
||||||
page->renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
logReaderMemSnapshot("tiled_gray_end");
|
||||||
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");
|
|
||||||
|
|
||||||
|
if (grayscaleDone) {
|
||||||
const auto tEnd = millis();
|
const auto tEnd = millis();
|
||||||
lastRenderStats.usedGrayscale = true;
|
lastRenderStats.usedGrayscale = true;
|
||||||
lastRenderStats.phases = {tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender,
|
lastRenderStats.phases = {tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, 0, tiledGrayMs, 0, 0, 0,
|
||||||
tBwStore - tDisplay, tGrayLsb - tBwStore, tGrayMsb - tGrayLsb,
|
tEnd - t0};
|
||||||
tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0};
|
LOG_DBG("ERS", "Page render (tiled): prewarm=%lums bw_render=%lums display=%lums tiled_gray=%lums total=%lums",
|
||||||
LOG_DBG("ERS",
|
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tiledGrayMs, tEnd - t0);
|
||||||
"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 {
|
} else {
|
||||||
uint32_t bwRestoreMs = 0;
|
// Legacy fallback: save (partial) BW frame, render LSB+MSB planes into the
|
||||||
if (bwBufferStored) {
|
// 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
|
// restore the bw data
|
||||||
logReaderMemSnapshot("bw_restore_begin");
|
logReaderMemSnapshot("bw_restore_begin");
|
||||||
renderer.restoreBwBuffer();
|
renderer.restoreBwBuffer();
|
||||||
const auto tBwRestore = millis();
|
const auto tBwRestore = millis();
|
||||||
logReaderMemSnapshot("bw_restore_end");
|
logReaderMemSnapshot("bw_restore_end");
|
||||||
bwRestoreMs = tBwRestore - tBwStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto tEnd = millis();
|
const auto tEnd = millis();
|
||||||
lastRenderStats.usedGrayscale = false;
|
lastRenderStats.usedGrayscale = true;
|
||||||
lastRenderStats.phases = {
|
lastRenderStats.phases = {tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender,
|
||||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, 0, 0, 0, bwRestoreMs,
|
tBwStore - tDisplay, tGrayLsb - tBwStore, tGrayMsb - tGrayLsb,
|
||||||
tEnd - t0};
|
tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0};
|
||||||
LOG_DBG("ERS",
|
LOG_DBG("ERS",
|
||||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums",
|
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums "
|
||||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, bwRestoreMs, tEnd - t0);
|
"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()) {
|
if (const auto* cacheManager = renderer.getFontCacheManager()) {
|
||||||
@@ -2195,24 +2285,22 @@ void EpubReaderActivity::displayPreRenderedPage(const Page& page, const int orie
|
|||||||
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
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;
|
const bool aaConfigured = SETTINGS.textAntiAliasing && !antiAliasingSuspendedLowMemory;
|
||||||
if (aaConfigured) {
|
if (aaConfigured) {
|
||||||
|
if (runTiledGrayscalePass(renderer, page, getEffectiveReaderFontId(), orientedMarginLeft, contentTop,
|
||||||
|
SETTINGS.fastAntiAliasing)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const uint32_t freeHeap = esp_get_free_heap_size();
|
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);
|
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) {
|
if (freeHeap >= BW_SNAPSHOT_MIN_FREE_HEAP_BYTES && contigHeap >= BW_SNAPSHOT_MIN_CONTIG_HEAP_BYTES) {
|
||||||
const int contentLeft = orientedMarginLeft;
|
// Full-framebuffer snapshot — see the matching site above for the rationale (issue #256).
|
||||||
const int contentRight = std::max(contentLeft, renderer.getScreenWidth() - orientedMarginRight);
|
if (renderer.storeBwBuffer()) {
|
||||||
const int contentBottom = std::max(contentTop, renderer.getScreenHeight() - orientedMarginBottom);
|
renderer.setFastGrayscaleLut(SETTINGS.fastAntiAliasing);
|
||||||
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)) {
|
|
||||||
renderer.clearScreen(0x00);
|
renderer.clearScreen(0x00);
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||||
page.renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
page.renderTextOnly(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
||||||
|
|||||||
Reference in New Issue
Block a user