From 78b4a656354dbabf2270866b201a0a2d57f54218 Mon Sep 17 00:00:00 2001 From: todd-herbert Date: Thu, 22 Feb 2024 08:00:56 +1300 Subject: [PATCH] E-Ink: additional conditions for "Dynamic Partial" mode (#3256) Co-authored-by: Ben Meadors --- src/graphics/EInkDisplay2.cpp | 74 ++++++++++++++++++++++++++++++++--- src/graphics/EInkDisplay2.h | 17 +++++++- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 10653e25a..66e7ffd40 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -362,6 +362,12 @@ void EInkDisplay::lowPriority() isHighPriority = false; } +// Full-refresh is explicitly requested for next one update - no skipping please +void EInkDisplay::demandFullRefresh() +{ + demandingFull = true; +} + // configure display for partial-refresh void EInkDisplay::configForPartialRefresh() { @@ -384,6 +390,49 @@ void EInkDisplay::configForFullRefresh() #endif } +#ifdef EINK_PARTIAL_ERASURE_LIMIT +// Count black pixels in an image. Used for "erasure tracking" +int32_t EInkDisplay::countBlackPixels() +{ + int32_t blackCount = 0; // Signed, to avoid underflow when comparing + for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) { + for (uint8_t i = 0; i < 7; i++) { + // Check if each bit is black or white + blackCount += (buffer[b] >> i) & 1; + } + } + return blackCount; +} + +// Evaluate the (rough) amount of black->white pixel change since last full refresh +bool EInkDisplay::tooManyErasures() +{ + // Ideally, we would compare the new and old buffers, to count *actual* white-to-black pixel changes + // but that would require substantially more "code tampering" + + // Get the black pixel stats for this image + int32_t blackCount = countBlackPixels(); + int32_t blackDifference = blackCount - prevBlackCount; + + // Update the running total of "erasures" - black pixels which have become white, since last full-refresh + if (blackDifference < 0) + erasedSinceFull -= blackDifference; + + // Store black pixel count for next time + prevBlackCount = blackCount; + + // Log the running total - help devs setup new boards + LOG_DEBUG("Dynamic Partial: erasedSinceFull=%hu, EINK_PARTIAL_ERASURE_LIMIT=%hu\n", erasedSinceFull, + EINK_PARTIAL_ERASURE_LIMIT); + + // Check if too many pixels have been erased + if (erasedSinceFull > EINK_PARTIAL_ERASURE_LIMIT) + return true; // Too many + else + return false; // Still okay +} +#endif // ifdef EINK_PARTIAL_BRIGHTEN_LIMIT_PX + bool EInkDisplay::newImageMatchesOld() { uint32_t newImageHash = 0; @@ -416,16 +465,20 @@ bool EInkDisplay::determineRefreshMode() missedHighPriorityUpdate = false; } - // Abort: if too soon for a new frame - if (isHighPriority && partialRefreshCount > 0 && sinceLast < highPriorityLimitMsec) { - LOG_DEBUG("Update skipped: exceeded EINK_HIGHPRIORITY_LIMIT_SECONDS\n"); + // Abort: if too soon for a new frame (unless demanding full) + if (!demandingFull && isHighPriority && partialRefreshCount > 0 && sinceLast < highPriorityLimitMsec) { + LOG_DEBUG("Dynamic Partial: update skipped. Exceeded EINK_HIGHPRIORITY_LIMIT_SECONDS\n"); missedHighPriorityUpdate = true; return false; } - if (!isHighPriority && sinceLast < lowPriorityLimitMsec) { + if (!demandingFull && !isHighPriority && !demandingFull && sinceLast < lowPriorityLimitMsec) { return false; } + // If demanded full refresh: give it to them + if (demandingFull) + needsFull = true; + // Check if old image (partial) should be redrawn (as full), for image quality if (partialRefreshCount > 0 && !isHighPriority) needsFull = true; @@ -434,7 +487,14 @@ bool EInkDisplay::determineRefreshMode() if (partialRefreshCount >= partialRefreshLimit) needsFull = true; +#ifdef EINK_PARTIAL_ERASURE_LIMIT + // Some displays struggle with erasing black pixels to white, during partial refresh + if (tooManyErasures()) + needsFull = true; +#endif + // If image matches + // (Block must run, even if full already selected, to store hash for next time) if (newImageMatchesOld()) { // If low priority: limit rate // otherwise, every loop() will run the hash method @@ -453,9 +513,11 @@ bool EInkDisplay::determineRefreshMode() if (partialRefreshCount > 0) configForFullRefresh(); - LOG_DEBUG("Conditions met for full-refresh\n"); + LOG_DEBUG("Dynamic Partial: conditions met for full-refresh\n"); partialRefreshCount = 0; needsFull = false; + demandingFull = false; + erasedSinceFull = 0; // Reset the count for EINK_PARTIAL_ERASURE_LIMIT - tracks ghosting buildup } // If options allow a partial refresh @@ -463,7 +525,7 @@ bool EInkDisplay::determineRefreshMode() if (partialRefreshCount == 0) configForPartialRefresh(); - LOG_DEBUG("Conditions met for partial-refresh\n"); + LOG_DEBUG("Dynamic Partial: conditions met for partial-refresh\n"); partialRefreshCount++; } diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 91261c865..aeaddee2d 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -64,8 +64,10 @@ class EInkDisplay : public OLEDDisplay // Use full refresh if EITHER: // * lowPriority() was set + // * demandFullRefresh() was called - (single shot) // * too many partial updates in a row: protect display - (EINK_PARTIAL_REPEAT_LIMIT) // * no recent updates, and last update was partial: redraw for image quality (EINK_LOWPRIORITY_LIMIT_SECONDS) + // * (optional) too many "erasures" since full-refresh (black pixels cleared to white) // Rate limit if: // * lowPriority() - (EINK_LOWPRIORITY_LIMIT_SECONDS) @@ -74,6 +76,7 @@ class EInkDisplay : public OLEDDisplay // Skip update entirely if ALL criteria met: // * new image matches old image // * lowPriority() + // * no call to demandFullRefresh() // * not redrawing for image quality // * not refreshing for display health @@ -89,24 +92,33 @@ class EInkDisplay : public OLEDDisplay #define EINK_LOWPRIORITY_LIMIT_SECONDS 30 #define EINK_HIGHPRIORITY_LIMIT_SECONDS 1 #define EINK_PARTIAL_REPEAT_LIMIT 5 + #define EINK_PARTIAL_ERASURE_LIMIT 300 // optional */ public: - void highPriority(); // Suggest partial refresh - void lowPriority(); // Suggest full refresh + void highPriority(); // Suggest partial refresh + void lowPriority(); // Suggest full refresh + void demandFullRefresh(); // For next update: explicitly request full refresh protected: void configForPartialRefresh(); // Display specific code to select partial refresh mode void configForFullRefresh(); // Display specific code to return to full refresh mode bool newImageMatchesOld(); // Is the new update actually different to the last image? bool determineRefreshMode(); // Called immediately before data written to display - choose refresh mode, or abort update +#ifdef EINK_PARTIAL_ERASURE_LIMIT + int32_t countBlackPixels(); // Calculate the number of black pixels in the new image + bool tooManyErasures(); // Has too much "ghosting" (black pixels erased to white) accumulated since last full-refresh? +#endif bool isHighPriority = true; // Does the method calling update believe that this is urgent? bool needsFull = false; // Is a full refresh forced? (display health) + bool demandingFull = false; // Was full refresh specifically requested? (splash screens, etc) bool missedHighPriorityUpdate = false; // Was a high priority update skipped for rate-limiting? uint16_t partialRefreshCount = 0; // How many partials have occurred since last full refresh? uint32_t lastUpdateMsec = 0; // When did the last update occur? uint32_t prevImageHash = 0; // Used to check if update will change screen image (skippable or not) + int32_t prevBlackCount = 0; // How many black pixels were in the previous image + uint32_t erasedSinceFull = 0; // How many black pixels have been set back to white since last full-refresh? (roughly) // Set in variant.h const uint32_t lowPriorityLimitMsec = (uint32_t)1000 * EINK_LOWPRIORITY_LIMIT_SECONDS; // Max rate for partial refreshes @@ -117,5 +129,6 @@ class EInkDisplay : public OLEDDisplay // Tolerate calls to these methods anywhere, just to be safe void highPriority() {} void lowPriority() {} + void demandFullRefresh() {} #endif };