From f458228a24b10229c335737938863353c5ed5229 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Fri, 10 Apr 2026 10:49:39 +0200 Subject: [PATCH 1/3] use atomic access to state fields --- lib/GfxRenderer/GfxRenderer.cpp | 50 ++++++++++++++++++--------------- lib/GfxRenderer/GfxRenderer.h | 35 ++++++++++++++--------- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index a99d0338..62230a5d 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -797,7 +797,7 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { int phyY = 0; // Note: this call should be inlined for better performance - rotateCoordinates(orientation, x, y, &phyX, &phyY, panelWidth, panelHeight); + rotateCoordinates(getOrientation(), x, y, &phyX, &phyY, panelWidth, panelHeight); // Bounds checking against runtime panel dimensions if (phyX < 0 || phyX >= panelWidth || phyY < 0 || phyY >= panelHeight) { @@ -870,7 +870,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop); const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left, combiningGlyph->width); - renderCharImpl(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style); + renderCharImpl(*this, getRenderMode(), font, cp, combiningX, yPos - raiseBy, black, style); continue; } @@ -902,7 +902,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha lastBaseAdvanceFP = glyph->advanceX; prevAdvanceFP = lastBaseAdvanceFP; - renderCharImpl(*this, renderMode, font, cp, lastBaseX, yPos, black, style); + renderCharImpl(*this, getRenderMode(), font, cp, lastBaseX, yPos, black, style); prevCp = cp; } } @@ -914,7 +914,7 @@ void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) con std::swap(y1, y2); } // In Portrait/PortraitInverted a logical vertical line maps to a physical horizontal span. - switch (orientation) { + switch (getOrientation()) { case Portrait: fillPhysicalHSpan(HalDisplay::DISPLAY_HEIGHT - 1 - x1, y1, y2, state); return; @@ -930,7 +930,7 @@ void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) con std::swap(x1, x2); } // In Landscape a logical horizontal line maps to a physical horizontal span. - switch (orientation) { + switch (getOrientation()) { case LandscapeCounterClockwise: fillPhysicalHSpan(y1, x1, x2, state); return; @@ -1144,7 +1144,7 @@ void GfxRenderer::fillRect(const int x, const int y, const int width, const int // For each orientation, one logical dimension maps to a constant physical row, allowing the // perpendicular dimension to be written as a byte-level span — eliminating per-pixel overhead. - switch (orientation) { + switch (getOrientation()) { case Portrait: // Logical column x → physical row (479-x); logical y range → physical x span for (int lx = x; lx < x + width; lx++) { @@ -1212,7 +1212,7 @@ void GfxRenderer::fillRectDither(const int x, const int y, const int width, cons // Byte patterns (phyY even / phyY odd): // Portrait / PortraitInverted: 0xAA / 0x55 // LandscapeCW / LandscapeCCW: 0x55 / 0xAA - switch (orientation) { + switch (getOrientation()) { case Portrait: for (int lx = x; lx < x + width; lx++) { const int phyY = HalDisplay::DISPLAY_HEIGHT - 1 - lx; @@ -1251,7 +1251,7 @@ void GfxRenderer::fillRectDither(const int x, const int y, const int width, cons // PortraitInverted: 0xAA / 0xFF (skip) // LandscapeCCW: 0x55 / 0xFF (skip) // LandscapeCW: 0xFF (skip) / 0xAA - switch (orientation) { + switch (getOrientation()) { case Portrait: for (int lx = x; lx < x + width; lx++) { const int phyY = HalDisplay::DISPLAY_HEIGHT - 1 - lx; @@ -1401,9 +1401,9 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const { int rotatedX = 0; int rotatedY = 0; - rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY, panelWidth, panelHeight); + rotateCoordinates(getOrientation(), x, y, &rotatedX, &rotatedY, panelWidth, panelHeight); // Rotate origin corner - switch (orientation) { + switch (getOrientation()) { case Portrait: rotatedY = rotatedY - height; break; @@ -1519,11 +1519,12 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3; - if (renderMode == BW && val < 3) { + const auto currentRenderMode = getRenderMode(); + if (currentRenderMode == BW && val < 3) { drawPixel(screenX, screenY); - } else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { + } else if (currentRenderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { drawPixel(screenX, screenY, false); - } else if (renderMode == GRAYSCALE_LSB && val == 1) { + } else if (currentRenderMode == GRAYSCALE_LSB && val == 1) { drawPixel(screenX, screenY, false); } } @@ -1685,16 +1686,19 @@ void GfxRenderer::invertScreen() const { } void GfxRenderer::setNextDisplayRefreshMode(const HalDisplay::RefreshMode refreshMode) const { - useNextRefreshOverride = true; - nextRefreshOverride = refreshMode; + useNextRefreshOverride.store(true, std::memory_order_relaxed); + nextRefreshOverride.store(static_cast(refreshMode), std::memory_order_relaxed); } void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const { - const auto effectiveMode = useNextRefreshOverride ? nextRefreshOverride : refreshMode; - useNextRefreshOverride = false; + const auto effectiveMode = + useNextRefreshOverride.load(std::memory_order_relaxed) + ? static_cast(nextRefreshOverride.load(std::memory_order_relaxed)) + : refreshMode; + useNextRefreshOverride.store(false, std::memory_order_relaxed); auto elapsed = millis() - start_ms; LOG_DBG("GFX", "Time = %lu ms from clearScreen to displayBuffer", elapsed); - display.displayBuffer(effectiveMode, fadingFix); + display.displayBuffer(effectiveMode, fadingFix.load(std::memory_order_relaxed)); } std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, @@ -1783,7 +1787,7 @@ std::vector GfxRenderer::wrappedText(const int fontId, const char* // Note: Internal driver treats screen in command orientation; this library exposes a logical orientation int GfxRenderer::getScreenWidth() const { - switch (orientation) { + switch (getOrientation()) { case Portrait: case PortraitInverted: // 480px wide in portrait logical coordinates @@ -1797,7 +1801,7 @@ int GfxRenderer::getScreenWidth() const { } int GfxRenderer::getScreenHeight() const { - switch (orientation) { + switch (getOrientation()) { case Portrait: case PortraitInverted: // 800px tall in portrait logical coordinates @@ -1943,7 +1947,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y const int combiningX = x - raiseBy; const int combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth, combiningGlyph->left, combiningGlyph->width); - renderCharImpl(*this, renderMode, font, cp, combiningX, combiningY, black, style); + renderCharImpl(*this, getRenderMode(), font, cp, combiningX, combiningY, black, style); continue; } @@ -1974,7 +1978,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y lastBaseAdvanceFP = glyph->advanceX; prevAdvanceFP = lastBaseAdvanceFP; - renderCharImpl(*this, renderMode, font, cp, x, lastBaseY, black, style); + renderCharImpl(*this, getRenderMode(), font, cp, x, lastBaseY, black, style); prevCp = cp; } } @@ -2083,7 +2087,7 @@ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const { } void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const { - switch (orientation) { + switch (getOrientation()) { case Portrait: *outTop = VIEWABLE_MARGIN_TOP; *outRight = VIEWABLE_MARGIN_RIGHT; diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index afb97f7d..50bc8d53 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -5,6 +5,7 @@ class FontCacheManager; +#include #include #include #include @@ -32,10 +33,11 @@ class GfxRenderer { static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory HalDisplay& display; - RenderMode renderMode; - Orientation orientation; - bool fadingFix; + std::atomic renderMode; + std::atomic orientation; + std::atomic fadingFix; // Text darkness for 2-bit grayscale glyph rendering. + std::atomic textDarkness; // 0 = Normal — true 4-level AA (raw=1 → light gray, raw=2 → dark gray) // 1 = Dark — historical default; raw=2 collapses to black // 2 = Extra Dark — both AA shades go black in the grayscale plane @@ -46,7 +48,6 @@ class GfxRenderer { // 1-bit fonts and the BW pass are unchanged. Default is 1 to preserve historical // rendering. See drawMaskFor2BitMode() in GfxRenderer.cpp for the per-level // pixel breakdown and a worked example glyph. - uint8_t textDarkness = 1; uint8_t* frameBuffer = nullptr; uint16_t panelWidth = HalDisplay::DISPLAY_WIDTH; uint16_t panelHeight = HalDisplay::DISPLAY_HEIGHT; @@ -59,8 +60,8 @@ class GfxRenderer { // recording to the (non-const) FontCacheManager. Same pragmatic compromise // as before, concentrated in a single pointer instead of four fields. mutable FontCacheManager* fontCacheManager_ = nullptr; - mutable bool useNextRefreshOverride = false; - mutable HalDisplay::RefreshMode nextRefreshOverride = HalDisplay::FAST_REFRESH; + mutable std::atomic useNextRefreshOverride = false; + mutable std::atomic nextRefreshOverride = static_cast(HalDisplay::FAST_REFRESH); void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState, EpdFontFamily::Style style) const; @@ -80,7 +81,11 @@ class GfxRenderer { public: explicit GfxRenderer(HalDisplay& halDisplay) - : display(halDisplay), renderMode(BW), orientation(Portrait), fadingFix(false) {} + : display(halDisplay), + renderMode(static_cast(BW)), + orientation(static_cast(Portrait)), + fadingFix(false), + textDarkness(1) {} ~GfxRenderer() { freeBwBufferChunks(); } static constexpr int VIEWABLE_MARGIN_TOP = 9; @@ -96,11 +101,11 @@ class GfxRenderer { const std::map& getFontMap() const { return fontMap; } // Orientation control (affects logical width/height and coordinate transforms) - void setOrientation(const Orientation o) { orientation = o; } - Orientation getOrientation() const { return orientation; } + void setOrientation(const Orientation o) { orientation.store(static_cast(o), std::memory_order_relaxed); } + Orientation getOrientation() const { return static_cast(orientation.load(std::memory_order_relaxed)); } // Fading fix control - void setFadingFix(const bool enabled) { fadingFix = enabled; } + void setFadingFix(const bool enabled) { fadingFix.store(enabled, std::memory_order_relaxed); } // Screen ops int getScreenWidth() const; @@ -165,16 +170,18 @@ class GfxRenderer { int getTextHeight(int fontId) const; // Grayscale functions - void setRenderMode(const RenderMode mode) { this->renderMode = mode; } - RenderMode getRenderMode() const { return renderMode; } + void setRenderMode(const RenderMode mode) { + this->renderMode.store(static_cast(mode), std::memory_order_relaxed); + } + RenderMode getRenderMode() const { return static_cast(renderMode.load(std::memory_order_relaxed)); } // Text darkness control: // 0 = Normal, 1 = Dark, 2 = Extra Dark, 3 = Maximum. // Only affects anti-aliased pixels in 2-bit (grayscale) glyph rendering; // 1-bit fonts and the BW pass are unchanged. See drawMaskFor2BitMode() in // GfxRenderer.cpp for the per-level pixel breakdown and a worked example. - void setTextDarkness(const uint8_t d) { textDarkness = d; } - uint8_t getTextDarkness() const { return textDarkness; } + void setTextDarkness(const uint8_t d) { textDarkness.store(d, std::memory_order_relaxed); } + uint8_t getTextDarkness() const { return static_cast(textDarkness.load(std::memory_order_relaxed)); } void copyGrayscaleLsbBuffers() const; void copyGrayscaleMsbBuffers() const; void displayGrayBuffer() const; From bcb0634c87ae526e6dae58ea8c89afdd27994890 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Fri, 10 Apr 2026 12:43:55 +0200 Subject: [PATCH 2/3] Review comments --- lib/GfxRenderer/GfxRenderer.cpp | 42 +++++++++++++++++++++++---------- lib/GfxRenderer/GfxRenderer.h | 4 ++-- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 62230a5d..39445608 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -1399,11 +1399,12 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con } void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const { + const auto currentOrientation = getOrientation(); int rotatedX = 0; int rotatedY = 0; - rotateCoordinates(getOrientation(), x, y, &rotatedX, &rotatedY, panelWidth, panelHeight); + rotateCoordinates(currentOrientation, x, y, &rotatedX, &rotatedY, panelWidth, panelHeight); // Rotate origin corner - switch (getOrientation()) { + switch (currentOrientation) { case Portrait: rotatedY = rotatedY - height; break; @@ -1476,6 +1477,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con return; } + const auto renderModeSnapshot = getRenderMode(); for (int bmpY = 0; bmpY < (bitmap.getHeight() - cropPixY); bmpY++) { // The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative). // Screen's (0, 0) is the top-left corner. @@ -1519,12 +1521,11 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3; - const auto currentRenderMode = getRenderMode(); - if (currentRenderMode == BW && val < 3) { + if (renderModeSnapshot == BW && val < 3) { drawPixel(screenX, screenY); - } else if (currentRenderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { + } else if (renderModeSnapshot == GRAYSCALE_MSB && (val == 1 || val == 2)) { drawPixel(screenX, screenY, false); - } else if (currentRenderMode == GRAYSCALE_LSB && val == 1) { + } else if (renderModeSnapshot == GRAYSCALE_LSB && val == 1) { drawPixel(screenX, screenY, false); } } @@ -1685,17 +1686,32 @@ void GfxRenderer::invertScreen() const { } } +static constexpr unsigned int encodeRefreshMode(const HalDisplay::RefreshMode mode) { + return static_cast(mode) + 1u; +} + +static constexpr HalDisplay::RefreshMode decodeRefreshMode(const unsigned int value) { + return static_cast(value - 1u); +} + void GfxRenderer::setNextDisplayRefreshMode(const HalDisplay::RefreshMode refreshMode) const { - useNextRefreshOverride.store(true, std::memory_order_relaxed); - nextRefreshOverride.store(static_cast(refreshMode), std::memory_order_relaxed); + refreshOverride.store(encodeRefreshMode(refreshMode), std::memory_order_release); } void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const { - const auto effectiveMode = - useNextRefreshOverride.load(std::memory_order_relaxed) - ? static_cast(nextRefreshOverride.load(std::memory_order_relaxed)) - : refreshMode; - useNextRefreshOverride.store(false, std::memory_order_relaxed); + auto effectiveMode = refreshMode; + unsigned int overrideValue = refreshOverride.load(std::memory_order_acquire); + if (overrideValue != REFRESH_OVERRIDE_NONE) { + unsigned int expected = overrideValue; + if (refreshOverride.compare_exchange_strong(expected, REFRESH_OVERRIDE_NONE, std::memory_order_acq_rel, + std::memory_order_acquire)) { + effectiveMode = decodeRefreshMode(overrideValue); + } else if (expected != REFRESH_OVERRIDE_NONE) { + effectiveMode = decodeRefreshMode(expected); + refreshOverride.store(REFRESH_OVERRIDE_NONE, std::memory_order_release); + } + } + auto elapsed = millis() - start_ms; LOG_DBG("GFX", "Time = %lu ms from clearScreen to displayBuffer", elapsed); display.displayBuffer(effectiveMode, fadingFix.load(std::memory_order_relaxed)); diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 50bc8d53..03831847 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -31,6 +31,7 @@ class GfxRenderer { private: static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory + static constexpr unsigned int REFRESH_OVERRIDE_NONE = 0; HalDisplay& display; std::atomic renderMode; @@ -60,8 +61,7 @@ class GfxRenderer { // recording to the (non-const) FontCacheManager. Same pragmatic compromise // as before, concentrated in a single pointer instead of four fields. mutable FontCacheManager* fontCacheManager_ = nullptr; - mutable std::atomic useNextRefreshOverride = false; - mutable std::atomic nextRefreshOverride = static_cast(HalDisplay::FAST_REFRESH); + mutable std::atomic refreshOverride = REFRESH_OVERRIDE_NONE; void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState, EpdFontFamily::Style style) const; From 2e1074cac3fe71b53f86b4368783d6467e32889f Mon Sep 17 00:00:00 2001 From: jpirnay Date: Fri, 10 Apr 2026 12:48:22 +0200 Subject: [PATCH 3/3] One more --- lib/GfxRenderer/GfxRenderer.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 39445608..2066cf31 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -860,6 +860,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha return; } const auto& font = fontIt->second; + const auto renderModeSnapshot = getRenderMode(); uint32_t cp; uint32_t prevCp = 0; @@ -870,7 +871,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop); const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left, combiningGlyph->width); - renderCharImpl(*this, getRenderMode(), font, cp, combiningX, yPos - raiseBy, black, style); + renderCharImpl(*this, renderModeSnapshot, font, cp, combiningX, yPos - raiseBy, black, style); continue; } @@ -902,7 +903,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha lastBaseAdvanceFP = glyph->advanceX; prevAdvanceFP = lastBaseAdvanceFP; - renderCharImpl(*this, getRenderMode(), font, cp, lastBaseX, yPos, black, style); + renderCharImpl(*this, renderModeSnapshot, font, cp, lastBaseX, yPos, black, style); prevCp = cp; } }