diff --git a/src/graphics/niche/Drivers/EInk/ED047TC1.cpp b/src/graphics/niche/Drivers/EInk/ED047TC1.cpp index b64e541c01..c2ed4a4522 100644 --- a/src/graphics/niche/Drivers/EInk/ED047TC1.cpp +++ b/src/graphics/niche/Drivers/EInk/ED047TC1.cpp @@ -210,6 +210,7 @@ class SafeFastEPD : public FASTEPD if (!grayPulsePlanes) return false; memset(grayPulsePlanes, 0, planeBytes); + memset(grayActiveRows, 0, sizeof(grayActiveRows)); if (!baselineValid) memset(_state.pCurrent, 0xFF, 960U * 540U / 2U); changed = 0; @@ -227,6 +228,10 @@ class SafeFastEPD : public FASTEPD const uint8_t *next = image + y * (width / 2U); memcpy(oldRow, current, rowBytes); memcpy(newRow, next, rowBytes); +#if !INKHUD_BOOKREADER_PERF_LOG + if (baselineValid && memcmp(oldRow, newRow, rowBytes) == 0) + continue; +#endif uint8_t *mask = grayPulsePlanes + (top + y) * 240U; uint8_t pendingMask = 0; bool rowChanged = false; @@ -261,8 +266,10 @@ class SafeFastEPD : public FASTEPD pendingMask = 0; } } - if (rowChanged) + if (rowChanged) { + grayActiveRows[top + y] = true; memcpy(current, newRow, rowBytes); + } } #if INKHUD_BOOKREADER_PERF_LOG for (unsigned packed = 0; packed < 256U; ++packed) { @@ -331,6 +338,7 @@ class SafeFastEPD : public FASTEPD uint8_t *grayPulsePlanes = nullptr; uint8_t grayPairLookup[8][256] = {}; bool grayLookupReady = false; + bool grayActiveRows[540] = {}; #endif bool dmaFault = false; @@ -390,12 +398,17 @@ class SafeFastEPD : public FASTEPD for (unsigned row = 0; row < 540; ++row) { // Alternate buffers keep the active DMA row untouched. uint8_t *out = _state.dma_buf + offset; - if (grayLookup) { +#if INKHUD_BOOKREADER_GRAYSCALE + if (grayLookup && grayActiveRows[row]) { const uint8_t *source = _state.pCurrent + row * 480U; const uint8_t *mask = plane + row * 240U; for (unsigned x = 0; x < 240; ++x) out[x] = mask[x] ? ((grayLookup[source[x * 2U]] << 4U) | grayLookup[source[x * 2U + 1U]]) & mask[x] : 0U; - } else if (plane) + } else if (grayLookup) + memset(out, 0, 240); + else +#endif + if (plane) memcpy(out, plane + row * 240U, 240); else memset(out, 0, 240); @@ -463,14 +476,71 @@ void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_ if (failed) LOG_WARN("ED047TC1 initial fullUpdate failed rc=%d", fullRc); LOG_INFO("ED047TC1 panel profile: clock=20MHz partialPasses=4 fullPasses=5"); + TaskHandle_t task = nullptr; + // Keep panel timing off the input/rendering core on dual-core boards. + const BaseType_t refreshCore = portNUM_PROCESSORS > 1 ? (xPortGetCoreID() ^ 1) : tskNO_AFFINITY; + if (xTaskCreatePinnedToCore(runUpdates, "eink-refresh", 8192, this, 2, &task, refreshCore) == pdPASS) { + updateTask = task; + LOG_INFO("ED047TC1 background refresh enabled core=%d", static_cast(refreshCore)); + } else + LOG_WARN("ED047TC1 refresh task unavailable; using blocking updates"); +} + +void ED047TC1::runUpdates(void *context) +{ + auto *display = static_cast(context); + for (;;) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + display->updateResult = display->driveUpdate(); + display->updateComplete.store(true, std::memory_order_release); + concurrency::mainDelay.interrupt(); + } +} + +void ED047TC1::queueUpdate(UpdateTypes type, bool grayscale, bool leavingGrayscale) +{ + pendingType = !grayscale && !monochromeBaselineValid ? FULL : type; + pendingGrayscale = grayscale; + pendingLeavingGrayscale = leavingGrayscale; + monochromeBaselineValid = false; +#if INKHUD_BOOKREADER_GRAYSCALE + grayscaleBaselineValid = false; +#endif + updateStartedMs = Time::getMillis(); + failed = false; + updateComplete.store(false, std::memory_order_relaxed); + beginPolling(5, 0); + if (updateTask) + xTaskNotifyGive(static_cast(updateTask)); + else { + updateResult = driveUpdate(); + updateComplete.store(true, std::memory_order_release); + } +} + +bool ED047TC1::isUpdateDone() +{ + if (!updateComplete.load(std::memory_order_acquire)) + return false; + monochromeBaselineValid = !pendingGrayscale && updateResult == BBEP_SUCCESS; +#if INKHUD_BOOKREADER_GRAYSCALE + grayscaleBaselineValid = pendingGrayscale && updateResult == BBEP_SUCCESS; +#endif + if (static_cast(epaper)->hasDmaFault()) + panelReady = false; + failed = updateResult != BBEP_SUCCESS; + LOG_DEBUG("[EInkPerf] refresh complete gray=%u type=%u ms=%lu rc=%d", pendingGrayscale, static_cast(pendingType), + static_cast(Time::getMillis() - updateStartedMs), updateResult); + return !failed; } void ED047TC1::update(uint8_t *imageData, UpdateTypes type) { - if (!panelReady || !imageData) + if (!panelReady || !imageData || busy() || !updateComplete.load(std::memory_order_acquire)) return; + bool leavingGrayscale = false; #if INKHUD_BOOKREADER_GRAYSCALE - const bool leavingGrayscale = epaper->getMode() == BB_MODE_4BPP; + leavingGrayscale = epaper->getMode() == BB_MODE_4BPP; grayscaleBaselineValid = false; #endif @@ -503,39 +573,46 @@ void ED047TC1::update(uint8_t *imageData, UpdateTypes type) memcpy(dstRow, srcRow, srcRowBytes); } - int rc; - if (type == FULL || !monochromeBaselineValid) { + if (type != FULL && monochromeBaselineValid && memcmp(cur, epaper->previousBuffer(), dstRowBytes * dstTotalRows) == 0) { + failed = false; + return; + } + queueUpdate(type, false, leavingGrayscale); +} + +int ED047TC1::driveUpdate() +{ #if INKHUD_BOOKREADER_GRAYSCALE - rc = epaper->fullUpdate(leavingGrayscale ? CLEAR_WHITE : CLEAR_SLOW, false); -#else - rc = epaper->fullUpdate(CLEAR_SLOW, false); + if (pendingGrayscale) { + const int rc = static_cast(epaper)->updatePanelGray(); + LOG_DEBUG("[EInkPerf] grayscale drive ms=%lu rc=%d", static_cast(Time::getMillis() - updateStartedMs), rc); + return rc; + } #endif - } else { + if (pendingType == FULL) + return epaper->fullUpdate(pendingLeavingGrayscale ? CLEAR_WHITE : CLEAR_SLOW, false); + else { const uint32_t startMs = millis(); uint32_t blackToWhite = 0, whiteToBlack = 0; + const uint8_t *cur = epaper->currentBuffer(); const uint8_t *previous = epaper->previousBuffer(); - for (size_t i = 0; i < dstRowBytes * dstTotalRows; ++i) { + for (size_t i = 0; i < 960U * 540U / 8U; ++i) { blackToWhite += __builtin_popcount(static_cast(cur[i] & ~previous[i])); whiteToBlack += __builtin_popcount(static_cast(~cur[i] & previous[i])); } auto *safeEpaper = static_cast(epaper); - rc = safeEpaper->partialUpdateWithWhiteFinish(blackToWhite); - if (safeEpaper->hasDmaFault()) - panelReady = false; + const int rc = safeEpaper->partialUpdateWithWhiteFinish(blackToWhite); LOG_DEBUG("[EInkPerf] FAST blackToWhite=%lu whiteToBlack=%lu whiteExtraPasses=%u ms=%lu rc=%d", static_cast(blackToWhite), static_cast(whiteToBlack), blackToWhite ? SafeFastEPD::whiteFinishPasses : 0U, static_cast(millis() - startMs), rc); + return rc; } - monochromeBaselineValid = rc == BBEP_SUCCESS; - failed = !monochromeBaselineValid; - if (failed) - LOG_WARN("ED047TC1 monochrome update failed rc=%d", rc); } #if INKHUD_BOOKREADER_GRAYSCALE void ED047TC1::updateGrayscale(uint8_t *imageData) { - if (!panelReady || !imageData) + if (!panelReady || !imageData || busy() || !updateComplete.load(std::memory_order_acquire)) return; #if INKHUD_BOOKREADER_PERF_LOG const uint32_t startMs = millis(); @@ -572,19 +649,8 @@ void ED047TC1::updateGrayscale(uint8_t *imageData) LOG_INFO("[BookReaderPerf] gray panel BEGIN clear=none waveform=panel-native shades=16 baseline=%u " "changed=%lu prepareMs=%lu", !establishBaseline, static_cast(changedPixels), static_cast(millis() - startMs)); - const uint32_t driveStartMs = millis(); #endif - const int rc = safeEpaper->updatePanelGray(); - if (safeEpaper->hasDmaFault()) - panelReady = false; -#if INKHUD_BOOKREADER_PERF_LOG - LOG_INFO("[BookReaderPerf] gray panel END rc=%d driveMs=%lu totalMs=%lu", rc, - static_cast(millis() - driveStartMs), static_cast(millis() - startMs)); -#endif - failed = rc != BBEP_SUCCESS; - grayscaleBaselineValid = !failed; - if (failed) - LOG_WARN("ED047TC1 grayscale update failed rc=%d", rc); + queueUpdate(FAST, true); } #endif diff --git a/src/graphics/niche/Drivers/EInk/ED047TC1.h b/src/graphics/niche/Drivers/EInk/ED047TC1.h index 70494c5c11..39a8ea6707 100644 --- a/src/graphics/niche/Drivers/EInk/ED047TC1.h +++ b/src/graphics/niche/Drivers/EInk/ED047TC1.h @@ -31,6 +31,7 @@ #include "configuration.h" #include "./EInk.h" +#include // Forward declare to avoid pulling FastEPD into all translation units class FASTEPD; @@ -83,12 +84,22 @@ class ED047TC1 : public EInk #endif protected: - bool isUpdateDone() override { return true; } // FastEPD updates are blocking + bool isUpdateDone() override; private: FASTEPD *epaper = nullptr; bool panelReady = false; bool monochromeBaselineValid = false; + void *updateTask = nullptr; + std::atomic updateComplete{true}; + UpdateTypes pendingType = FAST; + bool pendingGrayscale = false; + bool pendingLeavingGrayscale = false; + int updateResult = 0; + uint32_t updateStartedMs = 0; + static void runUpdates(void *context); + void queueUpdate(UpdateTypes type, bool grayscale, bool leavingGrayscale = false); + int driveUpdate(); #if INKHUD_BOOKREADER_GRAYSCALE bool grayscaleBaselineValid = false; #endif diff --git a/src/graphics/niche/InkHUD/Applets/User/BookReader/BookReaderApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/BookReader/BookReaderApplet.cpp index 9fd86274ed..42f6eda82b 100644 --- a/src/graphics/niche/InkHUD/Applets/User/BookReader/BookReaderApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/BookReader/BookReaderApplet.cpp @@ -1021,6 +1021,10 @@ void copyMonoBitmapInto(const std::vector &src, uint16_t srcWidth, uint return; } } +#if INKHUD_BOOKREADER_GRAYSCALE + InkHUD::copyReaderGrayBitmap(src.data(), InkHUD::readerBitmapStride(srcWidth), srcWidth, srcHeight, dst.data(), + InkHUD::readerBitmapStride(dstWidth), dstWidth, dstHeight, dstLeft, dstTop); +#else for (uint16_t sy = 0; sy < srcHeight; sy++) { const int32_t dy = static_cast(dstTop) + sy; if (dy < 0 || dy >= dstHeight) { @@ -1031,16 +1035,12 @@ void copyMonoBitmapInto(const std::vector &src, uint16_t srcWidth, uint if (dx < 0 || dx >= dstWidth) { continue; } -#if INKHUD_BOOKREADER_GRAYSCALE - InkHUD::setReaderGray(dst.data(), InkHUD::readerBitmapStride(dstWidth), dx, dy, - InkHUD::readerGrayAt(src.data(), InkHUD::readerBitmapStride(srcWidth), sx, sy)); -#else if (monoBitmapBlackAt(src, srcWidth, sx, sy)) { setMonoBitmapBlack(dst, dstWidth, static_cast(dx), static_cast(dy), true); } -#endif } } +#endif } size_t renderedImageCacheBytes() @@ -4476,7 +4476,7 @@ int32_t InkHUD::BookReaderApplet::runOnce() return concurrency::OSThread::disable(); } if (imageInteractionPending) { - if (readerInputPending() || !Throttle::hasElapsed(lastReaderInputMs, 150U)) + if (readerInputPending() || !Throttle::hasElapsed(lastReaderInputMs, 25U)) return 25; imageInteractionPending = false; BR_PERF_LOG("image interaction settled zoom=%u pan=%d,%d", imageZoomPermille, imagePanX, imagePanY); @@ -6409,7 +6409,7 @@ void InkHUD::BookReaderApplet::queueImageInteractionUpdate(bool panOnly) imageInteractionPending = true; lastReaderInputMs = Time::skipZero(Time::getMillis()); concurrency::OSThread::enabled = true; - setIntervalFromNow(150); + setIntervalFromNow(25); } int32_t InkHUD::BookReaderApplet::estimateImageLineSpan(const std::string &imagePath, uint16_t contentWidth, @@ -6464,6 +6464,7 @@ bool InkHUD::BookReaderApplet::drawInlineImage(const std::string &imagePath, int BR_LOAD_SCOPE("image-load"); [[maybe_unused]] uint32_t probeMs = 0; [[maybe_unused]] uint32_t decodeMs = 0; + uint32_t scaleMs = 0; if (imagePath.empty() || maxWidth == 0U || maxHeight == 0U) { return false; } @@ -6478,7 +6479,8 @@ bool InkHUD::BookReaderApplet::drawInlineImage(const std::string &imagePath, int const std::string cacheKey = imageCacheKey(imagePath, maxWidth, maxHeight, zoomPermille, inlineRaster); if (!preloadOnly && useRenderedCache && !monoBitmap && drawRenderedImageCache(this, cacheKey, left, top, maxWidth, maxHeight, zoomPermille, panX, panY, clipBottom)) { - BR_PERF_LOG("image rendered-cache HIT path=%s", imagePath.c_str()); + LOG_DEBUG("[BookReaderPerf] cached image zoom=%u ms=%lu", zoomPermille, + static_cast(millis() - tImageStartMs)); noteBookReaderLoadActivity(true); return true; } @@ -6790,8 +6792,10 @@ bool InkHUD::BookReaderApplet::drawInlineImage(const std::string &imagePath, int gDecodedImage.ready = true; if (!preloadOnly) { BR_LOAD_SCOPE("image-source-scale"); + const uint32_t scaleStartMs = millis(); rendered = scaleSourceBitmap(gDecodedImage.bitmap, gDecodedImage.width, gDecodedImage.height, drawWidth, drawHeight, *monoBitmap); + scaleMs = millis() - scaleStartMs; } } if (!rendered) { @@ -6819,6 +6823,12 @@ bool InkHUD::BookReaderApplet::drawInlineImage(const std::string &imagePath, int static_cast(drawHeight), static_cast(cache ? cache->bitmap.size() : 0U)); drawRenderedImageCache(this, cacheKey, left, top, maxWidth, maxHeight, requestedZoom, panX, panY, clipBottom); } + if (!preloadOnly) + LOG_DEBUG( + "[BookReaderPerf] image zoom=%u sourceHit=%u src=%ux%u dst=%ux%u probe=%lums decode=%lums scale=%lums total=%lums", + requestedZoom, reuseSource, gDecodedImage.width, gDecodedImage.height, drawWidth, drawHeight, + static_cast(probeMs), static_cast(decodeMs), static_cast(scaleMs), + static_cast(millis() - tImageStartMs)); noteBookReaderLoadActivity(true); return rendered; #endif diff --git a/src/graphics/niche/InkHUD/Applets/User/BookReader/SourceBitmap.h b/src/graphics/niche/InkHUD/Applets/User/BookReader/SourceBitmap.h index 6d2809fa73..c150e8bf0f 100644 --- a/src/graphics/niche/InkHUD/Applets/User/BookReader/SourceBitmap.h +++ b/src/graphics/niche/InkHUD/Applets/User/BookReader/SourceBitmap.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #if INKHUD_BOOKREADER_GRAYSCALE && defined(ARCH_ESP32) && defined(__GNUC__) @@ -70,6 +71,41 @@ inline void setReaderGray(uint8_t *data, size_t stride, uint16_t x, uint16_t y, const unsigned shift = (x & 1U) ? 0U : 4U; byte = (byte & ~(15U << shift)) | ((ink & 15U) << shift); } + +inline void copyReaderGrayBitmap(const uint8_t *source, size_t sourceStride, uint16_t sourceWidth, uint16_t sourceHeight, + uint8_t *destination, size_t destinationStride, uint16_t destinationWidth, + uint16_t destinationHeight, int16_t left, int16_t top) +{ + const int32_t firstX = std::max(0, -static_cast(left)); + const int32_t firstY = std::max(0, -static_cast(top)); + const int32_t endX = std::min(sourceWidth, static_cast(destinationWidth) - left); + const int32_t endY = std::min(sourceHeight, static_cast(destinationHeight) - top); + if (firstX >= endX || firstY >= endY) + return; + for (int32_t y = firstY; y < endY; ++y) { + const uint8_t *row = source + y * sourceStride; + uint8_t *out = destination + (top + y) * destinationStride + (left + firstX) / 2U; + int32_t x = firstX; + if ((left + x) & 1U) { + *out = (*out & 0xF0U) | readerGrayAt(row, sourceStride, x, 0); + ++out; + ++x; + } + const int32_t pairs = (endX - x) / 2; + const uint8_t *in = row + x / 2U; + if (!(x & 1U)) { + std::memcpy(out, in, pairs); + out += pairs; + } else { + // Shift adjacent bytes when source and destination nibble alignment differs. + for (int32_t i = 0; i < pairs; ++i) + *out++ = static_cast((in[i] << 4U) | (in[i + 1] >> 4U)); + } + x += pairs * 2; + if (x < endX) + *out = (*out & 0x0FU) | (readerGrayAt(row, sourceStride, x, 0) << 4U); + } +} #endif inline bool rasterizeInlineRgb565Block(const uint16_t *pixels, int x, int y, int width, int height, int pixelStride, uint16_t sw, @@ -197,8 +233,9 @@ inline bool scaleSourceBitmap(const std::vector &source, uint16_t sw, u return true; } const size_t stride = readerBitmapStride(dw); - destination.assign(stride * dh, 0); #if INKHUD_BOOKREADER_GRAYSCALE + // Every output byte is assigned below, including an odd-width row's padding nibble. + destination.resize(stride * dh); std::vector columns(static_cast(dw) + 1U); for (uint32_t x = 0; x <= dw; ++x) columns[x] = x * sw / dw; @@ -315,6 +352,7 @@ inline bool scaleSourceBitmap(const std::vector &source, uint16_t sw, u } return true; #else + destination.assign(stride * dh, 0); if (dw < sw || dh < sh) { // Preserve source-pixel coverage when reducing fine lines and screentones. constexpr uint8_t bayer[] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5}; diff --git a/src/graphics/niche/InkHUD/Renderer.cpp b/src/graphics/niche/InkHUD/Renderer.cpp index c062e21859..31a719eca7 100644 --- a/src/graphics/niche/InkHUD/Renderer.cpp +++ b/src/graphics/niche/InkHUD/Renderer.cpp @@ -537,7 +537,7 @@ int32_t InkHUD::Renderer::runOnce() #else if (requested) #endif - return 250UL; + return 25UL; else return OSThread::disable(); } @@ -734,7 +734,7 @@ void InkHUD::Renderer::render(bool async) maintainRenderActivity(); if (keepAwakeUntilUpdateComplete) { OSThread::enabled = true; - OSThread::setIntervalFromNow(250UL); + OSThread::setIntervalFromNow(25UL); } #endif }