From 21dbe2174501a3cafd2e1298a47b2efbdb709a86 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 29 Jun 2026 21:50:09 -0500 Subject: [PATCH] Faster TFTColor processing (#10814) --- src/graphics/TFTColorRegions.cpp | 14 +++++++++++++ src/graphics/TFTColorRegions.h | 23 +++++++++++++++++++++ src/graphics/TFTDisplay.cpp | 35 +++++++++++++++++++++++++------- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/graphics/TFTColorRegions.cpp b/src/graphics/TFTColorRegions.cpp index 877835ea6..54d3e3265 100644 --- a/src/graphics/TFTColorRegions.cpp +++ b/src/graphics/TFTColorRegions.cpp @@ -792,6 +792,20 @@ void clearTFTColorRegions() colorRegionCount = 0; } +// Per-row culling fast path (see TFTColorRegions.h / resolveTFTColorPixelRow()). +uint8_t tftColorRowRegions[MAX_TFT_COLOR_REGIONS]; +uint8_t tftColorRowCount = 0; + +void beginTFTColorRow(int16_t y) +{ + tftColorRowCount = 0; + for (uint8_t i = 0; i < colorRegionCount; i++) { + const TFTColorRegion &r = colorRegions[i]; + if (y >= r.y && y < r.y + r.height) + tftColorRowRegions[tftColorRowCount++] = i; + } +} + uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor) { for (int i = static_cast(colorRegionCount) - 1; i >= 0; i--) { diff --git a/src/graphics/TFTColorRegions.h b/src/graphics/TFTColorRegions.h index 4dc113627..d0517ad4e 100644 --- a/src/graphics/TFTColorRegions.h +++ b/src/graphics/TFTColorRegions.h @@ -67,6 +67,29 @@ uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t default // Resolve effective region-mapped OFF color at a coordinate in native-endian RGB565. uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor); +// -- Per-row fast path for the hot pixel loops in TFTDisplay::display() -------- +// resolveTFTColorPixel() is O(regions) per pixel; for a full 800x480 repaint that +// dominates redraw time. Regions are vertically localized, so cull to the regions +// overlapping the current row once per row, then resolve each pixel against only +// those (inlined here, so no per-pixel cross-TU call). +extern uint8_t tftColorRowRegions[MAX_TFT_COLOR_REGIONS]; // indices into colorRegions[], ascending +extern uint8_t tftColorRowCount; + +// Build tftColorRowRegions for row y. Call once per row before resolveTFTColorPixelRow(). +void beginTFTColorRow(int16_t y); + +// Resolve one pixel against the current row's active regions (set by beginTFTColorRow()). +// Highest-index region wins, matching resolveTFTColorPixel()'s precedence. +inline uint16_t resolveTFTColorPixelRow(int16_t x, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor) +{ + for (int j = static_cast(tftColorRowCount) - 1; j >= 0; j--) { + const TFTColorRegion &r = colorRegions[tftColorRowRegions[j]]; + if (x >= r.x && x < r.x + r.width) + return isset ? r.onColorBe : r.offColorBe; + } + return isset ? defaultOnColor : defaultOffColor; +} + // -- Theme engine ------------------------------------------------------ // Each theme has four fields that work together: // diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 4ef707efe..e6fd16e66 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1270,13 +1270,30 @@ void TFTDisplay::display(bool fromBlank) y_byteMask = (1 << (y & 7)); uint16_t *chunkRow = repaintChunkBuffer + (row * displayWidth); + + // Step 1: fill the whole row with the default colors. No per-pixel + // region scan, so background pixels (the bulk of the screen) are O(1). for (x = 0; x < displayWidth; x++) { isset = (buffer[x + y_byteIndex] & y_byteMask) != 0; - if (hasColorRegions) { - chunkRow[x] = graphics::resolveTFTColorPixel(static_cast(x), static_cast(y), isset, - colorTftWhite, colorTftBlack); - } else { - chunkRow[x] = isset ? colorTftWhite : colorTftBlack; + chunkRow[x] = isset ? colorTftWhite : colorTftBlack; + } + + // Step 2: overprint each region overlapping this row, applied in + // ascending index order so the highest-index region wins (matches + // resolveTFTColorPixel precedence). Only region-covered pixels are + // re-touched, so total cost is ~screen + sum of region spans. + if (hasColorRegions) { + graphics::beginTFTColorRow(static_cast(y)); + for (uint8_t k = 0; k < graphics::tftColorRowCount; k++) { + const graphics::TFTColorRegion &r = graphics::colorRegions[graphics::tftColorRowRegions[k]]; + int32_t xs = r.x > 0 ? r.x : 0; + int32_t xe = r.x + r.width; + if (xe > (int32_t)displayWidth) + xe = (int32_t)displayWidth; + for (int32_t xx = xs; xx < xe; xx++) { + isset = (buffer[xx + y_byteIndex] & y_byteMask) != 0; + chunkRow[xx] = isset ? r.onColorBe : r.offColorBe; + } } } } @@ -1363,12 +1380,16 @@ void TFTDisplay::display(bool fromBlank) } // Step 3: Copy only the changed span into the pixel line buffer. +#if GRAPHICS_TFT_COLORING_ENABLED + if (hasColorRegions) + graphics::beginTFTColorRow(static_cast(y)); +#endif for (x = x_FirstPixelUpdate; x <= x_LastPixelUpdate; x++) { isset = buffer[x + y_byteIndex] & y_byteMask; #if GRAPHICS_TFT_COLORING_ENABLED if (hasColorRegions) { - linePixelBuffer[x] = graphics::resolveTFTColorPixel(static_cast(x), static_cast(y), isset, - colorTftWhite, colorTftBlack); + linePixelBuffer[x] = + graphics::resolveTFTColorPixelRow(static_cast(x), isset, colorTftWhite, colorTftBlack); } else { linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack; }