diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index d25ebaa6fa..a689c14514 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -150,6 +150,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) { @@ -240,14 +257,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(phyY); + if (_stripActive) { + if (phyY < _stripY0 || phyY >= _stripY0 + _stripRows) { + return; // pixel outside the band currently being rendered + } + target = _stripBuf; + rowY = static_cast(phyY - _stripY0); + } + // Calculate byte position and bit position - const uint32_t byteIndex = static_cast(phyY) * panelWidthBytes + (phyX / 8); + const uint32_t byteIndex = rowY * panelWidthBytes + (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 } } @@ -966,9 +995,47 @@ static uint32_t start_ms = 0; void GfxRenderer::clearScreen(const uint8_t color) const { start_ms = halPlatform.millis(); + if (_stripActive) { + // Clear only the active band's scratch, not the shared framebuffer. + memset(_stripBuf, color, static_cast(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 or wraps + // the downstream uint16_t cast in writeGrayscalePlaneStrip. + assert(scratch != nullptr && stripRows > 0 && stripY0 >= 0 && stripY0 <= static_cast(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(orientation, x0, y0, &ax, &ay, panelWidth, panelHeight); + rotateCoordinates(orientation, 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::invertScreen() const { for (uint32_t i = 0; i < frameBufferSize; i++) { frameBuffer[i] = ~frameBuffer[i]; @@ -1370,6 +1437,14 @@ void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuff void GfxRenderer::displayGrayBuffer() const { display.displayGrayBuffer(fadingFix); } +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(panelHeight) - numRows); + display.writeGrayscalePlaneStrip(lsbPlane, scratch, static_cast(yStart), static_cast(numRows)); +} + +bool GfxRenderer::supportsStripGrayscale() const { return display.supportsStripGrayscale(); } + void GfxRenderer::freeBwBufferChunks() { for (auto& bwBufferChunk : bwBufferChunks) { if (bwBufferChunk) { diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index a97b2b9f85..78f1092296 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -54,6 +54,18 @@ class GfxRenderer { // as before, concentrated in a single pointer instead of four fields. mutable FontCacheManager* fontCacheManager_ = nullptr; + // Tiled grayscale strip target. When active, drawPixel()/clearScreen() + // operate on a caller-owned scratch holding one horizontal band of physical + // rows [_stripY0, _stripY0 + _stripRows) (panelWidthBytes wide) instead of + // the shared framebuffer, clipping pixels outside the band. 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(); @@ -114,6 +126,31 @@ class GfxRenderer { void clearScreen(uint8_t color = 0xFF) const; void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const; + // Tiled grayscale strip target. While active, drawPixel() and clearScreen() + // 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; + + // Band culling for tiled grayscale. 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 an expensive bitmap decode. Returns true when no strip is active. + // Corners are rotated to physical, so it is orientation-aware. + bool glyphIntersectsStrip(int x0, int y0, int x1, int y1) const; + + // Active pixel-write target for raw writers (DirectPixelWriter) 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 : panelHeight; } + // Drawing void drawPixel(int x, int y, bool state = true) const; void drawLine(int x1, int y1, int x2, int y2, bool state = true) const; @@ -172,6 +209,12 @@ class GfxRenderer { void copyGrayscaleLsbBuffers() const; void copyGrayscaleMsbBuffers() const; void displayGrayBuffer() const; + + // Tiled grayscale (X4): 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; bool storeBwBuffer(); // Returns true if buffer was stored successfully void restoreBwBuffer(); // Restore and free the stored buffer void cleanupGrayscaleWithFrameBuffer() const; diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 0a3c7948fb..f96c4c7a4b 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -103,6 +103,7 @@ STR_INVERTED: "Inverted" STR_LANDSCAPE_CCW: "Landscape CCW" STR_PREV_NEXT: "Prev/Next" STR_NEXT_PREV: "Next/Prev" +STR_DISABLED: "Disabled" STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SANS: "Noto Sans" STR_COURIER_PRIME: "Courier Prime" diff --git a/lib/ImageDecoder/DirectPixelWriter.h b/lib/ImageDecoder/DirectPixelWriter.h index bc66c2f78e..25cb57558e 100644 --- a/lib/ImageDecoder/DirectPixelWriter.h +++ b/lib/ImageDecoder/DirectPixelWriter.h @@ -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(sy) >= static_cast(clipRows)) return; + + const uint16_t byteIndex = static_cast(sy * displayWidthBytes + (phyX >> 3)); const uint8_t bitMask = 1 << (7 - (phyX & 7)); if (state) { diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index ec0e9c9deb..1226d38e73 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -89,6 +89,13 @@ 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(); } + uint16_t HalDisplay::getDisplayWidth() const { return einkDisplay.getDisplayWidth(); } uint16_t HalDisplay::getDisplayHeight() const { return einkDisplay.getDisplayHeight(); } diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h index 238aaa8f95..d4e950be00 100644 --- a/lib/hal/HalDisplay.h +++ b/lib/hal/HalDisplay.h @@ -55,6 +55,12 @@ 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; + // Runtime geometry passthrough uint16_t getDisplayWidth() const; uint16_t getDisplayHeight() const; diff --git a/open-x4-sdk b/open-x4-sdk index f086948686..11b77feaae 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit f086948686fd7e71aa546d0b7cdb5a89da4a5b46 +Subproject commit 11b77feaae46e45eedafdbe511cd3c3750c581d6 diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 7b37faa0b8..e5f2843ccd 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -73,9 +73,8 @@ class CrossPointSettings { }; // Side button layout options - // Default: Previous, Next - // Swapped: Next, Previous - enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTON_LAYOUT_COUNT }; + // Default: Up = Previous, Down = Next + enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTONS_DISABLED = 2, SIDE_BUTTON_LAYOUT_COUNT }; // Font family options (built-in fonts only; SD card fonts use sdFontFamilyName) enum FONT_FAMILY { NOTOSERIF = 0, NOTOSANS = 1, COURIERPRIME = 2, FONT_FAMILY_COUNT }; diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 8d6dccfaeb..5a85d8662e 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -2,24 +2,8 @@ #include "CrossPointSettings.h" -namespace { -using ButtonIndex = uint8_t; - -struct SideLayoutMap { - ButtonIndex pageBack; - ButtonIndex pageForward; -}; - -// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT. -constexpr SideLayoutMap kSideLayouts[] = { - {HalGPIO::BTN_UP, HalGPIO::BTN_DOWN}, - {HalGPIO::BTN_DOWN, HalGPIO::BTN_UP}, -}; -} // namespace - bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { - const auto sideLayout = static_cast(SETTINGS.sideButtonLayout); - const auto& side = kSideLayouts[sideLayout]; + const auto sideLayout = SETTINGS.sideButtonLayout; switch (button) { case Button::Back: @@ -45,10 +29,26 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint return (gpio.*fn)(HalGPIO::BTN_POWER); case Button::PageBack: // Reader page navigation uses side buttons and can be swapped via settings. - return (gpio.*fn)(side.pageBack); + switch (sideLayout) { + case CrossPointSettings::PREV_NEXT: + return (gpio.*fn)(HalGPIO::BTN_UP); + case CrossPointSettings::NEXT_PREV: + return (gpio.*fn)(HalGPIO::BTN_DOWN); + case CrossPointSettings::SIDE_BUTTONS_DISABLED: + default: + return false; + } case Button::PageForward: // Reader page navigation uses side buttons and can be swapped via settings. - return (gpio.*fn)(side.pageForward); + switch (sideLayout) { + case CrossPointSettings::PREV_NEXT: + return (gpio.*fn)(HalGPIO::BTN_DOWN); + case CrossPointSettings::NEXT_PREV: + return (gpio.*fn)(HalGPIO::BTN_UP); + case CrossPointSettings::SIDE_BUTTONS_DISABLED: + default: + return false; + } } return false; diff --git a/src/SettingsList.h b/src/SettingsList.h index 9f73f9e521..0f73efb832 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -164,7 +164,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist "imageRendering", StrId::STR_CAT_READER), // --- Controls --- SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout, - {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS), + {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV, StrId::STR_DISABLED}, "sideButtonLayout", + StrId::STR_CAT_CONTROLS), SettingInfo::Toggle(StrId::STR_FRONT_BTN_FOLLOW_ORIENTATION, &CrossPointSettings::frontButtonFollowOrientation, "frontButtonFollowOrientation", StrId::STR_CAT_CONTROLS), SettingInfo::Enum(StrId::STR_LONG_PRESS_BEHAVIOR, &CrossPointSettings::longPressButtonBehavior, diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index e9f452f36d..4bc0acdd44 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -811,49 +812,105 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or } const auto tDisplay = halPlatform.millis(); - // Save bw buffer to reset buffer state after grayscale data sync - renderer.storeBwBuffer(); - const auto tBwStore = halPlatform.millis(); - - // grayscale rendering - // TODO: Only do this if font supports it - if (SETTINGS.textAntiAliasing) { - renderer.clearScreen(0x00); - renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); - renderer.copyGrayscaleLsbBuffers(); - const auto tGrayLsb = halPlatform.millis(); - - // Render and copy to MSB buffer - renderer.clearScreen(0x00); - renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); - renderer.copyGrayscaleMsbBuffers(); - const auto tGrayMsb = halPlatform.millis(); - - // display grayscale part - renderer.displayGrayBuffer(); - const auto tGrayDisplay = halPlatform.millis(); - renderer.setRenderMode(GfxRenderer::BW); - // restore the bw data - renderer.restoreBwBuffer(); - const auto tBwRestore = halPlatform.millis(); - - const auto tEnd = halPlatform.millis(); - LOG_DBG("ERS", - "Page render: prewarm=%ums bw_render=%ums display=%ums bw_store=%ums " - "gray_lsb=%ums gray_msb=%ums gray_display=%ums bw_restore=%ums total=%ums", - tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore, - tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0); + // 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 + // full-frame storeBwBuffer is needed; controller RAM is re-synced from the + // live framebuffer afterward. The page is re-rendered ceil(H/STRIP_ROWS) times + // per plane, but renderCharImpl culls out-of-band glyphs before decode so the + // cost stays close to one render. Both text (drawPixel) and images + // (DirectPixelWriter) honor the active strip target. + if (SETTINGS.textAntiAliasing && renderer.supportsStripGrayscale()) { + constexpr int STRIP_ROWS = 80; + const int gh = renderer.getDisplayHeight(); + const int gwBytes = renderer.getDisplayWidthBytes(); + + auto scratch = makeUniqueNoThrow(static_cast(gwBytes) * STRIP_ROWS); + if (!scratch) { + LOG_ERR("ERS", "OOM: grayscale strip scratch (%d bytes); skipping AA this page", gwBytes * STRIP_ROWS); + } else { + // Bands may be streamed in any order: X4 windows each via setRamArea, X3 + // via PTL. + renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); + 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->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderer.endStripTarget(); + renderer.writeGrayscalePlaneStrip(true, scratch.get(), y, rows); + } + const auto tGrayLsb = halPlatform.millis(); + + // MSB plane. + renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); + 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->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderer.endStripTarget(); + renderer.writeGrayscalePlaneStrip(false, scratch.get(), y, rows); + } + const auto tGrayMsb = halPlatform.millis(); + + renderer.setRenderMode(GfxRenderer::BW); + renderer.displayGrayBuffer(); + const auto tGrayDisplay = halPlatform.millis(); + + // BW framebuffer is intact; re-sync controller RAM for the next + // differential page turn directly from it. + renderer.cleanupGrayscaleWithFrameBuffer(); + const auto tCleanup = halPlatform.millis(); + + const auto tEnd = halPlatform.millis(); + LOG_DBG("ERS", + "Page render (tiled): prewarm=%ums bw_render=%ums display=%ums gray_lsb=%ums " + "gray_msb=%ums gray_display=%ums cleanup=%ums total=%ums", + tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayLsb - tDisplay, tGrayMsb - tGrayLsb, + tGrayDisplay - tGrayMsb, tCleanup - tGrayDisplay, tEnd - t0); + } } else { - // restore the bw data - renderer.restoreBwBuffer(); - const auto tBwRestore = halPlatform.millis(); - - const auto tEnd = halPlatform.millis(); - LOG_DBG("ERS", "Page render: prewarm=%ums bw_render=%ums display=%ums bw_store=%ums bw_restore=%ums total=%ums", - tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tBwRestore - tBwStore, - tEnd - t0); + // Fallback path for a controller without strip support. grayscale rendering + // TODO: Only do this if font supports it + if (SETTINGS.textAntiAliasing) { + // Save the BW frame before the grayscale passes overwrite it, restore + // after. Only needed when grayscale actually renders. + renderer.storeBwBuffer(); + const auto tBwStore = halPlatform.millis(); + + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderer.copyGrayscaleLsbBuffers(); + const auto tGrayLsb = halPlatform.millis(); + + // Render and copy to MSB buffer + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderer.copyGrayscaleMsbBuffers(); + const auto tGrayMsb = halPlatform.millis(); + + // display grayscale part + renderer.displayGrayBuffer(); + const auto tGrayDisplay = halPlatform.millis(); + renderer.setRenderMode(GfxRenderer::BW); + renderer.restoreBwBuffer(); + const auto tBwRestore = halPlatform.millis(); + + const auto tEnd = halPlatform.millis(); + LOG_DBG("ERS", + "Page render: prewarm=%ums bw_render=%ums display=%ums bw_store=%ums " + "gray_lsb=%ums gray_msb=%ums gray_display=%ums bw_restore=%ums total=%ums", + tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore, + tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0); + } else { + // No anti-aliasing: BW frame already displayed above, no grayscale to + // render, so no save/restore. + const auto tEnd = halPlatform.millis(); + LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums total=%lums", tPrewarm - t0, + tBwRender - tPrewarm, tDisplay - tBwRender, tEnd - t0); + } } }