Some X3 optimisations for band striping

This commit is contained in:
jpirnay
2026-05-24 17:47:03 +02:00
parent a704222e8e
commit 09ec0908cc
4 changed files with 127 additions and 28 deletions
+60 -6
View File
@@ -1945,6 +1945,32 @@ void GfxRenderer::beginStripTarget(uint8_t* scratch, int stripY0, int stripRows)
stripY0_ = stripY0; stripY0_ = stripY0;
stripRows_ = stripRows; stripRows_ = stripRows;
stripActive_ = true; stripActive_ = true;
// Latch the orientation→phyY linear coefficients used by glyphIntersectsStrip()
// so the cull is one multiply-add per bbox corner instead of a switch.
// Derived from rotateCoordinates() with only the y-output retained.
switch (getOrientation()) {
case Portrait:
stripPhyYStepX_ = -1;
stripPhyYStepY_ = 0;
stripPhyYBase_ = panelHeight - 1;
break;
case LandscapeClockwise:
stripPhyYStepX_ = 0;
stripPhyYStepY_ = -1;
stripPhyYBase_ = panelHeight - 1;
break;
case PortraitInverted:
stripPhyYStepX_ = 1;
stripPhyYStepY_ = 0;
stripPhyYBase_ = 0;
break;
case LandscapeCounterClockwise:
stripPhyYStepX_ = 0;
stripPhyYStepY_ = 1;
stripPhyYBase_ = 0;
break;
}
} }
void GfxRenderer::endStripTarget() const { void GfxRenderer::endStripTarget() const {
@@ -1954,16 +1980,44 @@ void GfxRenderer::endStripTarget() const {
stripRows_ = 0; stripRows_ = 0;
} }
bool GfxRenderer::acquireStripScratch() {
if (stripScratch_) return true;
if (panelWidthBytes == 0 || panelHeight == 0) {
LOG_ERR("GFX", "acquireStripScratch called before begin()");
return false;
}
int rows = STRIP_SCRATCH_TARGET_BYTES / panelWidthBytes;
if (rows < 1) rows = 1;
if (rows > static_cast<int>(panelHeight)) rows = panelHeight;
const size_t bytes = static_cast<size_t>(panelWidthBytes) * rows;
stripScratch_ = static_cast<uint8_t*>(heap_caps_malloc(bytes, MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT));
if (!stripScratch_) {
LOG_INF("GFX", "Strip scratch alloc failed (%zu bytes)", bytes);
return false;
}
stripScratchRows_ = rows;
return true;
}
void GfxRenderer::releaseStripScratch() {
if (!stripScratch_) return;
heap_caps_free(stripScratch_);
stripScratch_ = nullptr;
stripScratchRows_ = 0;
}
bool GfxRenderer::glyphIntersectsStrip(int x0, int y0, int x1, int y1) const { bool GfxRenderer::glyphIntersectsStrip(int x0, int y0, int x1, int y1) const {
if (!stripActive_) { if (!stripActive_) {
return true; return true;
} }
// Rotate the two opposite bbox corners to physical coords. For 90-degree // Use the precomputed (stepX, stepY, base) latched in beginStripTarget() so
// orientations the physical bbox stays axis-aligned, so min/max of the two // each call is two multiply-adds + a range check, no rotateCoordinates
// rotated corners' Y bounds the glyph's physical y-extent. // switch. The four 90-degree orientations all reduce to "phyY depends on
int ax, ay, bx, by; // exactly one of (x, y)" — exactly one of stepX/stepY is non-zero — so phyY
rotateCoordinates(getOrientation(), x0, y0, &ax, &ay, panelWidth, panelHeight); // is monotonic across the bbox and the two opposite-corner phyY values
rotateCoordinates(getOrientation(), x1, y1, &bx, &by, panelWidth, panelHeight); // bracket the full physical y-extent.
const int ay = stripPhyYStepX_ * x0 + stripPhyYStepY_ * y0 + stripPhyYBase_;
const int by = stripPhyYStepX_ * x1 + stripPhyYStepY_ * y1 + stripPhyYBase_;
const int minY = ay < by ? ay : by; const int minY = ay < by ? ay : by;
const int maxY = ay > by ? ay : by; const int maxY = ay > by ? ay : by;
return !(maxY < stripY0_ || minY >= stripY0_ + stripRows_); return !(maxY < stripY0_ || minY >= stripY0_ + stripRows_);
+40 -1
View File
@@ -85,6 +85,26 @@ class GfxRenderer {
mutable int stripRows_ = 0; mutable int stripRows_ = 0;
mutable bool stripActive_ = false; mutable bool stripActive_ = false;
// Precomputed orientation→physicalY linear coefficients for the band-cull
// fast path. Latched once in beginStripTarget() and used by every
// glyphIntersectsStrip() call to avoid the 4-case rotateCoordinates switch
// per glyph. phyY = stripPhyYStepX_ * x + stripPhyYStepY_ * y + stripPhyYBase_,
// with steps in {-1, 0, 1}. Orientation can't change mid-pass; the strip
// session is the natural latch point.
mutable int8_t stripPhyYStepX_ = 0;
mutable int8_t stripPhyYStepY_ = 0;
mutable int stripPhyYBase_ = 0;
// Session-owned strip scratch. acquireStripScratch() allocates once (sized
// STRIP_SCRATCH_TARGET_BYTES, rounded to a whole number of rows of
// panelWidthBytes) and the buffer persists until releaseStripScratch().
// Allocating per page turn fragments the tight ESP32-C3 heap badly enough
// to cause AA to suspend after a few pages; hold one buffer for the reader
// session instead. The strip height we pick at acquire time is exposed via
// getStripScratchRows() so the caller plans its band loop around it.
uint8_t* stripScratch_ = nullptr;
int stripScratchRows_ = 0;
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();
@@ -108,7 +128,10 @@ class GfxRenderer {
orientation(static_cast<int>(Portrait)), orientation(static_cast<int>(Portrait)),
fadingFix(false), fadingFix(false),
textDarkness(1) {} textDarkness(1) {}
~GfxRenderer() { freeBwBufferChunks(); } ~GfxRenderer() {
freeBwBufferChunks();
releaseStripScratch();
}
static constexpr int VIEWABLE_MARGIN_TOP = 9; static constexpr int VIEWABLE_MARGIN_TOP = 9;
static constexpr int VIEWABLE_MARGIN_RIGHT = 3; static constexpr int VIEWABLE_MARGIN_RIGHT = 3;
@@ -245,6 +268,22 @@ class GfxRenderer {
void beginStripTarget(uint8_t* scratch, int stripY0, int stripRows) const; void beginStripTarget(uint8_t* scratch, int stripY0, int stripRows) const;
void endStripTarget() const; void endStripTarget() const;
// Session-owned strip scratch lifecycle. Reader activities call acquire on
// onEnter() and release on onExit(); the buffer is then reused across all
// page-turn AA passes for that session. Acquire is idempotent and returns
// true on success or when the buffer is already held. Sizing uses the
// current panel geometry, so begin() must have run first.
bool acquireStripScratch();
void releaseStripScratch();
uint8_t* getStripScratch() const { return stripScratch_; }
int getStripScratchRows() const { return stripScratchRows_; }
// Target byte budget for the session-owned strip scratch. ~24 KB lands at
// 240 rows on both X4 (100 B/row, panel 480) → 2 bands/plane and X3
// (99 B/row, panel 528) → 3 bands/plane. acquire clamps to panelHeight so
// a smaller panel never over-allocates.
static constexpr int STRIP_SCRATCH_TARGET_BYTES = 24000;
// Active pixel-write target for raw writers that bypass drawPixel for speed. // 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 // When a strip target is active these return the band scratch plus its
// physical-row origin and extent; otherwise the full framebuffer ([0, // physical-row origin and extent; otherwise the full framebuffer ([0,
+26 -20
View File
@@ -129,10 +129,11 @@ inline void logReaderMemSnapshot(const char*) {}
// //
// Returns true when the strip path ran end-to-end (controller now holds the AA // 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 // planes and the live BW frame is clean). Returns false when the controller
// doesn't support strip grayscale OR the scratch allocation fails — caller // doesn't support strip grayscale OR the session strip scratch isn't held
// should fall back to the legacy storeBwBufferRect path. // (acquireStripScratch in onEnter failed, e.g. heap was already too tight at
// reader open). Caller should fall back to the legacy storeBwBufferRect path.
// //
// The page is re-rendered ceil(panelHeight/STRIP_ROWS) times per plane, but // The page is re-rendered ceil(panelHeight/stripRows) times per plane, but
// renderCharImpl culls out-of-band glyphs before bitmap decode so the cost // renderCharImpl culls out-of-band glyphs before bitmap decode so the cost
// stays close to one render. Only renderTextOnly() is called here, matching the // stays close to one render. Only renderTextOnly() is called here, matching the
// legacy AA pass — images and HRs do not participate in grayscale. // legacy AA pass — images and HRs do not participate in grayscale.
@@ -146,31 +147,27 @@ bool runTiledGrayscalePass(GfxRenderer& renderer, const Page& page, int fontId,
// effect on the next page flip without rebooting. // effect on the next page flip without rebooting.
renderer.setFastGrayscaleLut(fastAA); renderer.setFastGrayscaleLut(fastAA);
// Strip height trades scratch size for the number of re-renders. Each render // Strip scratch is owned by GfxRenderer for the reader session
// pays layout + glyph-cull overhead even when bitmap decode is skipped, so // (acquireStripScratch in onEnter, releaseStripScratch in onExit). Allocating
// fewer/bigger bands win as long as the scratch fits. 240 rows × ~100 bytes // per page turn fragmented the ESP32-C3 heap badly enough to flip AA into the
// ≈ ~24 KB — still well below the legacy partial-snapshot footprint while // "suspended low memory" state after a few pages, so this path now refuses
// cutting X3 (480 px) to 2 bands/plane and X4 (800 px) to 4 bands/plane. // and falls back to the legacy snapshot if no session scratch is held.
constexpr int STRIP_ROWS = 240; uint8_t* const scratch = renderer.getStripScratch();
const int gh = renderer.getDisplayHeight(); const int stripRows = renderer.getStripScratchRows();
const int gwBytes = renderer.getDisplayWidthBytes(); if (!scratch || stripRows <= 0) {
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; return false;
} }
const int gh = renderer.getDisplayHeight();
auto renderPlane = [&](GfxRenderer::RenderMode mode, bool lsbPlane) { auto renderPlane = [&](GfxRenderer::RenderMode mode, bool lsbPlane) {
renderer.setRenderMode(mode); renderer.setRenderMode(mode);
for (int y = 0; y < gh; y += STRIP_ROWS) { for (int y = 0; y < gh; y += stripRows) {
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS; const int rows = (gh - y < stripRows) ? (gh - y) : stripRows;
renderer.beginStripTarget(scratch.get(), y, rows); renderer.beginStripTarget(scratch, y, rows);
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
page.renderTextOnly(renderer, fontId, marginLeft, contentTop); page.renderTextOnly(renderer, fontId, marginLeft, contentTop);
renderer.endStripTarget(); renderer.endStripTarget();
renderer.writeGrayscalePlaneStrip(lsbPlane, scratch.get(), y, rows); renderer.writeGrayscalePlaneStrip(lsbPlane, scratch, y, rows);
} }
}; };
@@ -288,6 +285,14 @@ void EpubReaderActivity::onEnter() {
} }
logReaderMemSnapshot("onEnter_after_orientation"); logReaderMemSnapshot("onEnter_after_orientation");
// Allocate the strip scratch once per reader session so tiled grayscale
// (runTiledGrayscalePass) doesn't have to malloc ~24 KB on every page turn.
// Failure is non-fatal: the AA pass falls back to the legacy snapshot path.
if (!renderer.acquireStripScratch()) {
LOG_INF("ERS", "Strip scratch unavailable; tiled grayscale will fall back to legacy snapshot");
}
logReaderMemSnapshot("onEnter_after_strip_scratch");
epub->setupCacheDir(); epub->setupCacheDir();
logReaderMemSnapshot("onEnter_after_setupCacheDir"); logReaderMemSnapshot("onEnter_after_setupCacheDir");
@@ -402,6 +407,7 @@ void EpubReaderActivity::onExit() {
epub.reset(); epub.reset();
currentPageFootnotes.clear(); currentPageFootnotes.clear();
currentPageFootnotes.shrink_to_fit(); currentPageFootnotes.shrink_to_fit();
renderer.releaseStripScratch();
logReaderMemSnapshot("onExit_after_release"); logReaderMemSnapshot("onExit_after_release");
} }