From 74764aed2b26ae86787bb9b6f217d111793d0e89 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Sun, 24 May 2026 20:22:16 +0200 Subject: [PATCH] perf(ui): O(n) FullscreenMsgView wrap via getCodepointWidth virtual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wrap loop used to memcpy a growing substring and call getTextWidth on it for every appended codepoint — O(n²) UTF-8 decoding with Lemon font, ~5-20 ms on a full e-ink redraw of a long message. Add DisplayDriver::getCodepointWidth(cp) (default: build a UTF-8 buffer and forward to getTextWidth) plus DisplayDriver::decodeCodepoint() as the shared UTF-8 walker. Override getCodepointWidth in GxEPDDisplay (direct lemonXAdvance for Lemon size 1) and SH1106Display (lemonXAdvance for Lemon, fixed 6*sz for built-in font). FullscreenMsgView now walks codepoints once and sums widths incrementally. Co-Authored-By: Claude Sonnet 4.6 --- .../ui-new/FullscreenMsgView.h | 36 ++++++++-------- src/helpers/ui/DisplayDriver.h | 43 ++++++++++++++++++- src/helpers/ui/GxEPDDisplay.h | 4 ++ src/helpers/ui/SH1106Display.h | 4 ++ 4 files changed, 68 insertions(+), 19 deletions(-) diff --git a/examples/companion_radio/ui-new/FullscreenMsgView.h b/examples/companion_radio/ui-new/FullscreenMsgView.h index f83b6359..0d81ebdc 100644 --- a/examples/companion_radio/ui-new/FullscreenMsgView.h +++ b/examples/companion_radio/ui-new/FullscreenMsgView.h @@ -16,33 +16,33 @@ struct FullscreenMsgView { void begin() { scroll = 0; active = true; } // Pixel-accurate word-wrap: breaks at last space that fits within max_px, - // hard-breaks long words. Works for both fixed- and variable-width fonts. + // hard-breaks long words. Walks the string codepoint-by-codepoint, summing + // per-codepoint widths via DisplayDriver::getCodepointWidth — O(n) instead + // of O(n²) substring re-measurement. Works for both fixed- and + // variable-width fonts. static int wrapLines(DisplayDriver& display, const char* text, int max_px, char out[][FS_CHARS_MAX], int max_lines) { int count = 0; - const char* seg = text; + const uint8_t* seg = (const uint8_t*)text; while (*seg && count < max_lines) { - const char* p = seg; - const char* last_sp = nullptr; - const char* last_fit = seg; + const uint8_t* p = seg; + const uint8_t* last_sp = nullptr; // pointer to space byte (cut here, drop the space) + const uint8_t* last_fit = seg; // farthest point that still fits + int width = 0; while (*p && (p - seg) < FS_CHARS_MAX - 1) { - uint8_t b0 = (uint8_t)*p; - int cb = (b0 < 0x80) ? 1 : (b0 < 0xE0) ? 2 : (b0 < 0xF0) ? 3 : 4; - if ((p - seg) + cb >= FS_CHARS_MAX) break; - - char buf[FS_CHARS_MAX]; - int n = (int)(p - seg) + cb; - memcpy(buf, seg, n); buf[n] = '\0'; - if (display.getTextWidth(buf) > max_px && p > seg) break; - - if (*p == ' ') last_sp = p; - p += cb; + const uint8_t* cp_start = p; + uint32_t cp = DisplayDriver::decodeCodepoint(p); + if ((p - seg) >= FS_CHARS_MAX) { p = cp_start; break; } + uint16_t cw = display.getCodepointWidth(cp); + if (width + cw > max_px && cp_start > seg) { p = cp_start; break; } + if (cp == ' ') last_sp = cp_start; + width += cw; last_fit = p; } - const char* end; - const char* next_seg; + const uint8_t* end; + const uint8_t* next_seg; if (!*p) { end = p; next_seg = p; } else if (last_sp && last_sp > seg) { diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 081a9b12..3f1089ec 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -160,7 +160,48 @@ public: virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) { translateUTF8Static(dest, src, dest_size); } - + + // Advance a UTF-8 pointer by one codepoint, returning the decoded value. + // Invalid sequences return 0xFFFD and consume trailing continuation bytes. + static uint32_t decodeCodepoint(const uint8_t*& p) { + uint8_t c = *p++; + if (c < 0x80) return c; + if ((c & 0xE0) == 0xC0) { + uint32_t cp = c & 0x1F; + if (*p) cp = (cp << 6) | (*p++ & 0x3F); + return cp; + } + if ((c & 0xF0) == 0xE0) { + uint32_t cp = c & 0x0F; + if (*p) cp = (cp << 6) | (*p++ & 0x3F); + if (*p) cp = (cp << 6) | (*p++ & 0x3F); + return cp; + } + if ((c & 0xF8) == 0xF0) { + uint32_t cp = c & 0x07; + if (*p) cp = (cp << 6) | (*p++ & 0x3F); + if (*p) cp = (cp << 6) | (*p++ & 0x3F); + if (*p) cp = (cp << 6) | (*p++ & 0x3F); + return cp; + } + while (*p && (*p & 0xC0) == 0x80) p++; + return 0xFFFD; + } + + // Width of a single codepoint in pixels. Default: fall back to getTextWidth + // on a one-codepoint UTF-8 string. Drivers can override for O(1) lookup. + virtual uint16_t getCodepointWidth(uint32_t cp) { + char buf[5]; + int n = 0; + if (cp < 0x80) { buf[n++] = (char)cp; } + else if (cp < 0x800) { buf[n++] = 0xC0 | (cp >> 6); buf[n++] = 0x80 | (cp & 0x3F); } + else if (cp < 0x10000) { buf[n++] = 0xE0 | (cp >> 12); buf[n++] = 0x80 | ((cp >> 6) & 0x3F); buf[n++] = 0x80 | (cp & 0x3F); } + else { buf[n++] = 0xF0 | (cp >> 18); buf[n++] = 0x80 | ((cp >> 12) & 0x3F); buf[n++] = 0x80 | ((cp >> 6) & 0x3F); buf[n++] = 0x80 | (cp & 0x3F); } + buf[n] = '\0'; + return getTextWidth(buf); + } + + // draw text with ellipsis if it exceeds max_width virtual void drawTextEllipsized(int x, int y, int max_width, const char* str) { char temp_str[256]; // reasonable buffer size diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index d36967fd..18e185ca 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -114,6 +114,10 @@ public: void drawRect(int x, int y, int w, int h) override; void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; uint16_t getTextWidth(const char* str) override; + uint16_t getCodepointWidth(uint32_t cp) override { + if (_use_lemon && _text_sz == 1) return lemonXAdvance(cp, scale()); + return DisplayDriver::getCodepointWidth(cp); + } void setDisplayRotation(uint8_t rot) override; void setFullRefreshInterval(uint8_t n) override { _full_refresh_interval = n; _partial_count = 0; } void endFrame() override; diff --git a/src/helpers/ui/SH1106Display.h b/src/helpers/ui/SH1106Display.h index ebc6553b..d7016a72 100644 --- a/src/helpers/ui/SH1106Display.h +++ b/src/helpers/ui/SH1106Display.h @@ -48,6 +48,10 @@ public: void drawRect(int x, int y, int w, int h) override; void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override; uint16_t getTextWidth(const char *str) override; + uint16_t getCodepointWidth(uint32_t cp) override { + if (_use_lemon) return lemonXAdvance(cp); + return 6 * _text_sz; // built-in 5x7 font: 6 px advance per glyph + } int getCharWidth() const override { return (_use_lemon ? 5 : 6) * _text_sz; } int getLineHeight() const override { return (_use_lemon ? 9 : 8) * _text_sz; } void setLemonFont(bool enabled) override { _use_lemon = enabled; _vw_dirty = true; }