From dca3aa660f6d889d2b6ff4c41fb9441f85d911bf Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Tue, 11 Aug 2026 14:29:39 +0000 Subject: [PATCH 1/4] shared-module/busdisplay: merge overlapping dirty rectangles Merge dirty rectangles whose bounding box is smaller than the two areas summed, so shared pixels are not sent twice. displayio does not merge them today, so it sends the overlap once per rectangle. Merging never sends more pixels than before. Fixes #10687 --- shared-module/busdisplay/BusDisplay.c | 60 +++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/shared-module/busdisplay/BusDisplay.c b/shared-module/busdisplay/BusDisplay.c index 01e0f7a7896..23ce331753a 100644 --- a/shared-module/busdisplay/BusDisplay.c +++ b/shared-module/busdisplay/BusDisplay.c @@ -34,6 +34,10 @@ #define DELAY 0x80 +// Max dirty rectangles held while merging overlapping ones before a refresh. More than this (rare) +// falls back to refreshing the raw list unmerged. +#define MAX_MERGE_AREAS 16 + void common_hal_busdisplay_busdisplay_construct(busdisplay_busdisplay_obj_t *self, mp_obj_t bus, uint16_t width, uint16_t height, int16_t colstart, int16_t rowstart, uint16_t rotation, uint16_t color_depth, bool grayscale, bool pixels_in_byte_share_row, @@ -327,9 +331,59 @@ static void _refresh_display(busdisplay_busdisplay_obj_t *self) { } const displayio_area_t *current_area = _get_refresh_areas(self); - while (current_area != NULL) { - _refresh_area(self, current_area); - current_area = current_area->next; + + // Merge overlapping dirty rectangles so shared pixels are computed and sent once. displayio does + // not merge them, so overlapping/nested areas get their common pixels recomputed and retransmitted + // per rectangle (and drawn as separate, visibly sequential passes). A changing text label is the + // common case: it dirties the whole label area AND each glyph, so the glyph rectangles sit inside + // the label rectangle and nearly double the work. Copy the clipped areas into a scratch array and + // greedily fuse any pair whose bounding-box area is smaller than the two areas summed - i.e. they + // overlap enough that one rectangle sends fewer pixels than two. Distant / non-overlapping + // rectangles are left separate (their bounding box would be larger). The result is never more + // pixels than the unmerged list. If there are more areas than the scratch holds, fall back to + // refreshing the raw list unmerged. + displayio_area_t merged[MAX_MERGE_AREAS]; + size_t count = 0; + bool overflow = false; + for (const displayio_area_t *a = current_area; a != NULL; a = a->next) { + displayio_area_t clipped; + if (!displayio_display_core_clip_area(&self->core, a, &clipped)) { + continue; + } + if (count >= MAX_MERGE_AREAS) { + overflow = true; + break; + } + merged[count] = clipped; + count++; + } + if (overflow) { + while (current_area != NULL) { + _refresh_area(self, current_area); + current_area = current_area->next; + } + displayio_display_core_finish_refresh(&self->core); + return; + } + bool changed = true; + while (changed) { + changed = false; + for (size_t i = 0; i < count && !changed; i++) { + for (size_t j = i + 1; j < count; j++) { + displayio_area_t u; + displayio_area_union(&merged[i], &merged[j], &u); + if (displayio_area_size(&u) < displayio_area_size(&merged[i]) + displayio_area_size(&merged[j])) { + merged[i] = u; // fuse j into i + merged[j] = merged[count - 1]; // swap-remove j + count--; + changed = true; + break; + } + } + } + } + for (size_t i = 0; i < count; i++) { + _refresh_area(self, &merged[i]); } displayio_display_core_finish_refresh(&self->core); } From e9c6fb8a0aa1634a4e4f22d9b39812349afde74d Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Fri, 14 Aug 2026 05:34:18 +0000 Subject: [PATCH 2/4] displayio: share the dirty-rectangle merge across display types Move the overlap merge from busdisplay into shared code: displayio_area_array_merge_overlapping() in area.c does the greedy fuse, displayio_display_core_merge_refresh_areas() clips and collects the list (raw-list fallback past DISPLAYIO_MAX_MERGE_AREAS), and busdisplay, framebufferio and epaperdisplay all use it. --- shared-module/busdisplay/BusDisplay.c | 60 +++---------------- shared-module/displayio/area.c | 25 ++++++++ shared-module/displayio/area.h | 2 + shared-module/displayio/display_core.c | 27 +++++++++ shared-module/displayio/display_core.h | 5 ++ shared-module/epaperdisplay/EPaperDisplay.c | 17 +++++- .../framebufferio/FramebufferDisplay.c | 17 +++++- 7 files changed, 96 insertions(+), 57 deletions(-) diff --git a/shared-module/busdisplay/BusDisplay.c b/shared-module/busdisplay/BusDisplay.c index 23ce331753a..db730e6fd9f 100644 --- a/shared-module/busdisplay/BusDisplay.c +++ b/shared-module/busdisplay/BusDisplay.c @@ -34,10 +34,6 @@ #define DELAY 0x80 -// Max dirty rectangles held while merging overlapping ones before a refresh. More than this (rare) -// falls back to refreshing the raw list unmerged. -#define MAX_MERGE_AREAS 16 - void common_hal_busdisplay_busdisplay_construct(busdisplay_busdisplay_obj_t *self, mp_obj_t bus, uint16_t width, uint16_t height, int16_t colstart, int16_t rowstart, uint16_t rotation, uint16_t color_depth, bool grayscale, bool pixels_in_byte_share_row, @@ -332,58 +328,20 @@ static void _refresh_display(busdisplay_busdisplay_obj_t *self) { const displayio_area_t *current_area = _get_refresh_areas(self); - // Merge overlapping dirty rectangles so shared pixels are computed and sent once. displayio does - // not merge them, so overlapping/nested areas get their common pixels recomputed and retransmitted - // per rectangle (and drawn as separate, visibly sequential passes). A changing text label is the - // common case: it dirties the whole label area AND each glyph, so the glyph rectangles sit inside - // the label rectangle and nearly double the work. Copy the clipped areas into a scratch array and - // greedily fuse any pair whose bounding-box area is smaller than the two areas summed - i.e. they - // overlap enough that one rectangle sends fewer pixels than two. Distant / non-overlapping - // rectangles are left separate (their bounding box would be larger). The result is never more - // pixels than the unmerged list. If there are more areas than the scratch holds, fall back to - // refreshing the raw list unmerged. - displayio_area_t merged[MAX_MERGE_AREAS]; - size_t count = 0; - bool overflow = false; - for (const displayio_area_t *a = current_area; a != NULL; a = a->next) { - displayio_area_t clipped; - if (!displayio_display_core_clip_area(&self->core, a, &clipped)) { - continue; - } - if (count >= MAX_MERGE_AREAS) { - overflow = true; - break; + // Merge overlapping dirty rectangles so shared pixels are computed and sent once + // (see displayio_display_core_merge_refresh_areas). + displayio_area_t merged[DISPLAYIO_MAX_MERGE_AREAS]; + size_t merged_count; + if (displayio_display_core_merge_refresh_areas(&self->core, current_area, merged, + DISPLAYIO_MAX_MERGE_AREAS, &merged_count)) { + for (size_t i = 0; i < merged_count; i++) { + _refresh_area(self, &merged[i]); } - merged[count] = clipped; - count++; - } - if (overflow) { + } else { while (current_area != NULL) { _refresh_area(self, current_area); current_area = current_area->next; } - displayio_display_core_finish_refresh(&self->core); - return; - } - bool changed = true; - while (changed) { - changed = false; - for (size_t i = 0; i < count && !changed; i++) { - for (size_t j = i + 1; j < count; j++) { - displayio_area_t u; - displayio_area_union(&merged[i], &merged[j], &u); - if (displayio_area_size(&u) < displayio_area_size(&merged[i]) + displayio_area_size(&merged[j])) { - merged[i] = u; // fuse j into i - merged[j] = merged[count - 1]; // swap-remove j - count--; - changed = true; - break; - } - } - } - } - for (size_t i = 0; i < count; i++) { - _refresh_area(self, &merged[i]); } displayio_display_core_finish_refresh(&self->core); } diff --git a/shared-module/displayio/area.c b/shared-module/displayio/area.c index 6a79e2aeeea..fb2e1e5712c 100644 --- a/shared-module/displayio/area.c +++ b/shared-module/displayio/area.c @@ -104,6 +104,31 @@ uint32_t displayio_area_size(const displayio_area_t *area) { return displayio_area_width(area) * displayio_area_height(area); } +// Greedily fuse any pair of areas whose bounding box is smaller than the two areas summed - +// i.e. they overlap enough that one rectangle covers fewer pixels than two. Distant and +// merely touching rectangles are left separate (their bounding box would not be smaller), so +// the result never covers more pixels than the input. Runs in place; returns the new count. +size_t displayio_area_array_merge_overlapping(displayio_area_t *areas, size_t count) { + bool changed = true; + while (changed) { + changed = false; + for (size_t i = 0; i < count && !changed; i++) { + for (size_t j = i + 1; j < count; j++) { + displayio_area_t u; + displayio_area_union(&areas[i], &areas[j], &u); + if (displayio_area_size(&u) < displayio_area_size(&areas[i]) + displayio_area_size(&areas[j])) { + areas[i] = u; // fuse j into i + areas[j] = areas[count - 1]; // swap-remove j + count--; + changed = true; + break; + } + } + } + } + return count; +} + bool displayio_area_equal(const displayio_area_t *a, const displayio_area_t *b) { return a->x1 == b->x1 && a->y1 == b->y1 && diff --git a/shared-module/displayio/area.h b/shared-module/displayio/area.h index eed54613a5a..93e427035ad 100644 --- a/shared-module/displayio/area.h +++ b/shared-module/displayio/area.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include @@ -50,6 +51,7 @@ bool displayio_area_compute_overlap(const displayio_area_t *a, uint16_t displayio_area_width(const displayio_area_t *area); uint16_t displayio_area_height(const displayio_area_t *area); uint32_t displayio_area_size(const displayio_area_t *area); +size_t displayio_area_array_merge_overlapping(displayio_area_t *areas, size_t count); bool displayio_area_equal(const displayio_area_t *a, const displayio_area_t *b); void displayio_area_transform_within(bool mirror_x, bool mirror_y, bool transpose_xy, const displayio_area_t *original, diff --git a/shared-module/displayio/display_core.c b/shared-module/displayio/display_core.c index dfcddde7796..70cdb0b3607 100644 --- a/shared-module/displayio/display_core.c +++ b/shared-module/displayio/display_core.c @@ -203,3 +203,30 @@ bool displayio_display_core_clip_area(displayio_display_core_t *self, const disp } return true; } + +// Clip the collected refresh areas and merge the overlapping ones so shared pixels are +// computed and sent once. displayio does not merge areas as it collects them, so +// overlapping/nested ones get their common pixels reprocessed per rectangle (and drawn as +// separate, visibly sequential passes). A changing text label is the common case: it +// dirties the whole label area AND each glyph, so the glyph rectangles sit inside the +// label rectangle and nearly double the work. Only pairs whose bounding box is smaller +// than the two areas summed are fused, so the result never covers more pixels than the +// unmerged list. Returns false when there are more areas than `max_areas` (rare) - the +// caller should then refresh the raw list unmerged. +bool displayio_display_core_merge_refresh_areas(displayio_display_core_t *self, + const displayio_area_t *areas, displayio_area_t *merged, size_t max_areas, size_t *count_out) { + size_t count = 0; + for (const displayio_area_t *a = areas; a != NULL; a = a->next) { + displayio_area_t clipped; + if (!displayio_display_core_clip_area(self, a, &clipped)) { + continue; + } + if (count >= max_areas) { + return false; + } + merged[count] = clipped; + count++; + } + *count_out = displayio_area_array_merge_overlapping(merged, count); + return true; +} diff --git a/shared-module/displayio/display_core.h b/shared-module/displayio/display_core.h index f3fe2023636..fd93c617657 100644 --- a/shared-module/displayio/display_core.h +++ b/shared-module/displayio/display_core.h @@ -51,3 +51,8 @@ void displayio_display_core_collect_ptrs(displayio_display_core_t *self); bool displayio_display_core_fill_area(displayio_display_core_t *self, displayio_area_t *area, uint32_t *mask, uint32_t *buffer); bool displayio_display_core_clip_area(displayio_display_core_t *self, const displayio_area_t *area, displayio_area_t *clipped); + +// Scratch size for displayio_display_core_merge_refresh_areas(). +#define DISPLAYIO_MAX_MERGE_AREAS 16 +bool displayio_display_core_merge_refresh_areas(displayio_display_core_t *self, + const displayio_area_t *areas, displayio_area_t *merged, size_t max_areas, size_t *count_out); diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index d34be9d5c7c..327df929c19 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -458,9 +458,20 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ } epaperdisplay_epaperdisplay_start_refresh(self); - while (current_area != NULL) { - epaperdisplay_epaperdisplay_refresh_area(self, current_area); - current_area = current_area->next; + // Merge overlapping dirty rectangles so shared pixels are only written once + // (see displayio_display_core_merge_refresh_areas). + displayio_area_t merged[DISPLAYIO_MAX_MERGE_AREAS]; + size_t merged_count; + if (displayio_display_core_merge_refresh_areas(&self->core, current_area, merged, + DISPLAYIO_MAX_MERGE_AREAS, &merged_count)) { + for (size_t i = 0; i < merged_count; i++) { + epaperdisplay_epaperdisplay_refresh_area(self, &merged[i]); + } + } else { + while (current_area != NULL) { + epaperdisplay_epaperdisplay_refresh_area(self, current_area); + current_area = current_area->next; + } } epaperdisplay_epaperdisplay_finish_refresh(self); return true; diff --git a/shared-module/framebufferio/FramebufferDisplay.c b/shared-module/framebufferio/FramebufferDisplay.c index 8116f4b0347..691fb310730 100644 --- a/shared-module/framebufferio/FramebufferDisplay.c +++ b/shared-module/framebufferio/FramebufferDisplay.c @@ -228,9 +228,20 @@ static void _refresh_display(framebufferio_framebufferdisplay_obj_t *self) { uint8_t dirty_row_bitmask[(row_count + 7) / 8]; memset(dirty_row_bitmask, 0, sizeof(dirty_row_bitmask)); self->framebuffer_protocol->get_bufinfo(self->framebuffer, &self->bufinfo); - while (current_area != NULL) { - _refresh_area(self, current_area, dirty_row_bitmask); - current_area = current_area->next; + // Merge overlapping dirty rectangles so shared pixels are only rendered once + // (see displayio_display_core_merge_refresh_areas). + displayio_area_t merged[DISPLAYIO_MAX_MERGE_AREAS]; + size_t merged_count; + if (displayio_display_core_merge_refresh_areas(&self->core, current_area, merged, + DISPLAYIO_MAX_MERGE_AREAS, &merged_count)) { + for (size_t i = 0; i < merged_count; i++) { + _refresh_area(self, &merged[i], dirty_row_bitmask); + } + } else { + while (current_area != NULL) { + _refresh_area(self, current_area, dirty_row_bitmask); + current_area = current_area->next; + } } self->framebuffer_protocol->swapbuffers(self->framebuffer, dirty_row_bitmask); } From 42f4ba116fe5d9f0a7a69faaed40a146121ad846 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Sat, 15 Aug 2026 17:50:31 +0000 Subject: [PATCH 3/4] displayio: drop contained dirty rectangles in Group Reduce the refresh list in displayio_group_get_refresh_areas: unlink every area fully contained in another one, so covered pixels are composited and transmitted once instead of once per rectangle. The recursive list builder becomes a static _impl and the public name wraps it, so the reduction runs once on the root group's list and displays need no changes. Only the .next links are rewritten; the item-owned coordinates are never touched. Measured on an ST7789 BusDisplay changing 2-digit scaled text labels: scale 20 (the reported case) 1.75 -> 3.45 fps, scale 8 10.5 -> 19.8 fps; two stacked full-screen bitmaps 1.05 -> 1.75 fps. --- shared-module/busdisplay/BusDisplay.c | 18 +---- shared-module/displayio/Group.c | 69 ++++++++++++++++++- shared-module/displayio/area.c | 25 ------- shared-module/displayio/area.h | 2 - shared-module/displayio/display_core.c | 27 -------- shared-module/displayio/display_core.h | 5 -- shared-module/epaperdisplay/EPaperDisplay.c | 17 +---- .../framebufferio/FramebufferDisplay.c | 17 +---- 8 files changed, 76 insertions(+), 104 deletions(-) diff --git a/shared-module/busdisplay/BusDisplay.c b/shared-module/busdisplay/BusDisplay.c index db730e6fd9f..01e0f7a7896 100644 --- a/shared-module/busdisplay/BusDisplay.c +++ b/shared-module/busdisplay/BusDisplay.c @@ -327,21 +327,9 @@ static void _refresh_display(busdisplay_busdisplay_obj_t *self) { } const displayio_area_t *current_area = _get_refresh_areas(self); - - // Merge overlapping dirty rectangles so shared pixels are computed and sent once - // (see displayio_display_core_merge_refresh_areas). - displayio_area_t merged[DISPLAYIO_MAX_MERGE_AREAS]; - size_t merged_count; - if (displayio_display_core_merge_refresh_areas(&self->core, current_area, merged, - DISPLAYIO_MAX_MERGE_AREAS, &merged_count)) { - for (size_t i = 0; i < merged_count; i++) { - _refresh_area(self, &merged[i]); - } - } else { - while (current_area != NULL) { - _refresh_area(self, current_area); - current_area = current_area->next; - } + while (current_area != NULL) { + _refresh_area(self, current_area); + current_area = current_area->next; } displayio_display_core_finish_refresh(&self->core); } diff --git a/shared-module/displayio/Group.c b/shared-module/displayio/Group.c index a2bf685d0f7..fb0ebc18d3d 100644 --- a/shared-module/displayio/Group.c +++ b/shared-module/displayio/Group.c @@ -435,7 +435,7 @@ void displayio_group_finish_refresh(displayio_group_t *self) { } } -displayio_area_t *displayio_group_get_refresh_areas(displayio_group_t *self, displayio_area_t *tail) { +static displayio_area_t *group_get_refresh_areas_impl(displayio_group_t *self, displayio_area_t *tail) { if (self->item_removed) { self->dirty_area.next = tail; tail = &self->dirty_area; @@ -462,10 +462,75 @@ displayio_area_t *displayio_group_get_refresh_areas(displayio_group_t *self, dis layer = mp_obj_cast_to_native_base( self->members->items[i], &displayio_group_type); if (layer != MP_OBJ_NULL) { - tail = displayio_group_get_refresh_areas(layer, tail); + tail = group_get_refresh_areas_impl(layer, tail); continue; } } return tail; } + +// Merge the dirty-rect list by containment: unlink every area that is fully contained +// in another area in the list (a changing text label is the common case - the group +// dirties the whole label region via item_removed plus one area per glyph, and the +// glyph areas all sit inside the label region). Dropping a covered area is lossless: +// the same pixels are refreshed by the survivor, in fewer transfers. +// +// The nodes are OWNED BY THE ITEMS (TileGrid reuses dirty_area as its tile-space dirty +// accumulator between frames, vectorio keeps its current_area, ...), so their +// coordinates must never be modified here. The ONLY mutation is the .next relink, +// which is safe because every provider unconditionally reassigns .next at link time on +// every get_refresh_areas call and never reads it back. +// +// Nodes at and past `tail` belong to the caller, so the sweep uses `tail` as its end +// sentinel and never looks past it (every in-tree caller passes tail == NULL). +// +// One pass suffices: coordinates never change and unlinking preserves the relative +// order of the remaining nodes, so every pair of surviving nodes is examined. Equal +// areas satisfy the containment test in both directions; testing b-inside-a FIRST +// keeps the earlier node and drops the later one - the two tests must stay an else-if +// chain or equal areas would drop BOTH nodes and lose the area entirely. +static displayio_area_t *group_relink_refresh_areas(displayio_area_t *head, + const displayio_area_t *tail) { + displayio_area_t *a_prev = NULL; + displayio_area_t *a = head; + while (a != tail) { + // The casts from the const .next field are safe: every node before `tail` is a + // mutable item-owned struct; only the field's declared type is pointer-to-const. + displayio_area_t *b_prev = a; + displayio_area_t *b = (displayio_area_t *)a->next; + bool a_dropped = false; + while (b != tail) { + if (b->x1 >= a->x1 && b->y1 >= a->y1 && b->x2 <= a->x2 && b->y2 <= a->y2) { + // b is contained in a (equal areas land here): unlink b. + b_prev->next = b->next; + b = (displayio_area_t *)b_prev->next; + } else if (a->x1 >= b->x1 && a->y1 >= b->y1 && a->x2 <= b->x2 && a->y2 <= b->y2) { + // a is strictly contained in the later b: unlink a and move on. + if (a_prev == NULL) { + head = (displayio_area_t *)a->next; + } else { + a_prev->next = a->next; + } + a_dropped = true; + break; + } else { + b_prev = b; + b = (displayio_area_t *)b->next; + } + } + if (a_dropped) { + a = (a_prev == NULL) ? head : (displayio_area_t *)a_prev->next; + } else { + a_prev = a; + a = (displayio_area_t *)a->next; + } + } + return head; +} + +// Public entry point: displays call this once on their root group, so the whole tree's +// list is merged in one place and no display type needs its own copy of the logic. +displayio_area_t *displayio_group_get_refresh_areas(displayio_group_t *self, displayio_area_t *tail) { + return group_relink_refresh_areas(group_get_refresh_areas_impl(self, tail), tail); +} diff --git a/shared-module/displayio/area.c b/shared-module/displayio/area.c index fb2e1e5712c..6a79e2aeeea 100644 --- a/shared-module/displayio/area.c +++ b/shared-module/displayio/area.c @@ -104,31 +104,6 @@ uint32_t displayio_area_size(const displayio_area_t *area) { return displayio_area_width(area) * displayio_area_height(area); } -// Greedily fuse any pair of areas whose bounding box is smaller than the two areas summed - -// i.e. they overlap enough that one rectangle covers fewer pixels than two. Distant and -// merely touching rectangles are left separate (their bounding box would not be smaller), so -// the result never covers more pixels than the input. Runs in place; returns the new count. -size_t displayio_area_array_merge_overlapping(displayio_area_t *areas, size_t count) { - bool changed = true; - while (changed) { - changed = false; - for (size_t i = 0; i < count && !changed; i++) { - for (size_t j = i + 1; j < count; j++) { - displayio_area_t u; - displayio_area_union(&areas[i], &areas[j], &u); - if (displayio_area_size(&u) < displayio_area_size(&areas[i]) + displayio_area_size(&areas[j])) { - areas[i] = u; // fuse j into i - areas[j] = areas[count - 1]; // swap-remove j - count--; - changed = true; - break; - } - } - } - } - return count; -} - bool displayio_area_equal(const displayio_area_t *a, const displayio_area_t *b) { return a->x1 == b->x1 && a->y1 == b->y1 && diff --git a/shared-module/displayio/area.h b/shared-module/displayio/area.h index 93e427035ad..eed54613a5a 100644 --- a/shared-module/displayio/area.h +++ b/shared-module/displayio/area.h @@ -6,7 +6,6 @@ #pragma once -#include #include #include @@ -51,7 +50,6 @@ bool displayio_area_compute_overlap(const displayio_area_t *a, uint16_t displayio_area_width(const displayio_area_t *area); uint16_t displayio_area_height(const displayio_area_t *area); uint32_t displayio_area_size(const displayio_area_t *area); -size_t displayio_area_array_merge_overlapping(displayio_area_t *areas, size_t count); bool displayio_area_equal(const displayio_area_t *a, const displayio_area_t *b); void displayio_area_transform_within(bool mirror_x, bool mirror_y, bool transpose_xy, const displayio_area_t *original, diff --git a/shared-module/displayio/display_core.c b/shared-module/displayio/display_core.c index 70cdb0b3607..dfcddde7796 100644 --- a/shared-module/displayio/display_core.c +++ b/shared-module/displayio/display_core.c @@ -203,30 +203,3 @@ bool displayio_display_core_clip_area(displayio_display_core_t *self, const disp } return true; } - -// Clip the collected refresh areas and merge the overlapping ones so shared pixels are -// computed and sent once. displayio does not merge areas as it collects them, so -// overlapping/nested ones get their common pixels reprocessed per rectangle (and drawn as -// separate, visibly sequential passes). A changing text label is the common case: it -// dirties the whole label area AND each glyph, so the glyph rectangles sit inside the -// label rectangle and nearly double the work. Only pairs whose bounding box is smaller -// than the two areas summed are fused, so the result never covers more pixels than the -// unmerged list. Returns false when there are more areas than `max_areas` (rare) - the -// caller should then refresh the raw list unmerged. -bool displayio_display_core_merge_refresh_areas(displayio_display_core_t *self, - const displayio_area_t *areas, displayio_area_t *merged, size_t max_areas, size_t *count_out) { - size_t count = 0; - for (const displayio_area_t *a = areas; a != NULL; a = a->next) { - displayio_area_t clipped; - if (!displayio_display_core_clip_area(self, a, &clipped)) { - continue; - } - if (count >= max_areas) { - return false; - } - merged[count] = clipped; - count++; - } - *count_out = displayio_area_array_merge_overlapping(merged, count); - return true; -} diff --git a/shared-module/displayio/display_core.h b/shared-module/displayio/display_core.h index fd93c617657..f3fe2023636 100644 --- a/shared-module/displayio/display_core.h +++ b/shared-module/displayio/display_core.h @@ -51,8 +51,3 @@ void displayio_display_core_collect_ptrs(displayio_display_core_t *self); bool displayio_display_core_fill_area(displayio_display_core_t *self, displayio_area_t *area, uint32_t *mask, uint32_t *buffer); bool displayio_display_core_clip_area(displayio_display_core_t *self, const displayio_area_t *area, displayio_area_t *clipped); - -// Scratch size for displayio_display_core_merge_refresh_areas(). -#define DISPLAYIO_MAX_MERGE_AREAS 16 -bool displayio_display_core_merge_refresh_areas(displayio_display_core_t *self, - const displayio_area_t *areas, displayio_area_t *merged, size_t max_areas, size_t *count_out); diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 327df929c19..d34be9d5c7c 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -458,20 +458,9 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ } epaperdisplay_epaperdisplay_start_refresh(self); - // Merge overlapping dirty rectangles so shared pixels are only written once - // (see displayio_display_core_merge_refresh_areas). - displayio_area_t merged[DISPLAYIO_MAX_MERGE_AREAS]; - size_t merged_count; - if (displayio_display_core_merge_refresh_areas(&self->core, current_area, merged, - DISPLAYIO_MAX_MERGE_AREAS, &merged_count)) { - for (size_t i = 0; i < merged_count; i++) { - epaperdisplay_epaperdisplay_refresh_area(self, &merged[i]); - } - } else { - while (current_area != NULL) { - epaperdisplay_epaperdisplay_refresh_area(self, current_area); - current_area = current_area->next; - } + while (current_area != NULL) { + epaperdisplay_epaperdisplay_refresh_area(self, current_area); + current_area = current_area->next; } epaperdisplay_epaperdisplay_finish_refresh(self); return true; diff --git a/shared-module/framebufferio/FramebufferDisplay.c b/shared-module/framebufferio/FramebufferDisplay.c index 691fb310730..8116f4b0347 100644 --- a/shared-module/framebufferio/FramebufferDisplay.c +++ b/shared-module/framebufferio/FramebufferDisplay.c @@ -228,20 +228,9 @@ static void _refresh_display(framebufferio_framebufferdisplay_obj_t *self) { uint8_t dirty_row_bitmask[(row_count + 7) / 8]; memset(dirty_row_bitmask, 0, sizeof(dirty_row_bitmask)); self->framebuffer_protocol->get_bufinfo(self->framebuffer, &self->bufinfo); - // Merge overlapping dirty rectangles so shared pixels are only rendered once - // (see displayio_display_core_merge_refresh_areas). - displayio_area_t merged[DISPLAYIO_MAX_MERGE_AREAS]; - size_t merged_count; - if (displayio_display_core_merge_refresh_areas(&self->core, current_area, merged, - DISPLAYIO_MAX_MERGE_AREAS, &merged_count)) { - for (size_t i = 0; i < merged_count; i++) { - _refresh_area(self, &merged[i], dirty_row_bitmask); - } - } else { - while (current_area != NULL) { - _refresh_area(self, current_area, dirty_row_bitmask); - current_area = current_area->next; - } + while (current_area != NULL) { + _refresh_area(self, current_area, dirty_row_bitmask); + current_area = current_area->next; } self->framebuffer_protocol->swapbuffers(self->framebuffer, dirty_row_bitmask); } From 740672cec7b5f6637867534fc11be7139f18369c Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Mon, 17 Aug 2026 20:42:16 +0000 Subject: [PATCH 4/4] displayio: name the area filter for what it does Review: filter_out_redundant_areas (no group_ prefix on a static), and the public wrapper reads better with the built list in a named variable. --- shared-module/displayio/Group.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/shared-module/displayio/Group.c b/shared-module/displayio/Group.c index fb0ebc18d3d..84ebffdab6d 100644 --- a/shared-module/displayio/Group.c +++ b/shared-module/displayio/Group.c @@ -470,7 +470,7 @@ static displayio_area_t *group_get_refresh_areas_impl(displayio_group_t *self, d return tail; } -// Merge the dirty-rect list by containment: unlink every area that is fully contained +// Filter the dirty-rect list by containment: unlink every area that is fully contained // in another area in the list (a changing text label is the common case - the group // dirties the whole label region via item_removed plus one area per glyph, and the // glyph areas all sit inside the label region). Dropping a covered area is lossless: @@ -490,7 +490,7 @@ static displayio_area_t *group_get_refresh_areas_impl(displayio_group_t *self, d // areas satisfy the containment test in both directions; testing b-inside-a FIRST // keeps the earlier node and drops the later one - the two tests must stay an else-if // chain or equal areas would drop BOTH nodes and lose the area entirely. -static displayio_area_t *group_relink_refresh_areas(displayio_area_t *head, +static displayio_area_t *filter_out_redundant_areas(displayio_area_t *head, const displayio_area_t *tail) { displayio_area_t *a_prev = NULL; displayio_area_t *a = head; @@ -530,7 +530,8 @@ static displayio_area_t *group_relink_refresh_areas(displayio_area_t *head, } // Public entry point: displays call this once on their root group, so the whole tree's -// list is merged in one place and no display type needs its own copy of the logic. +// list is filtered in one place and no display type needs its own copy of the logic. displayio_area_t *displayio_group_get_refresh_areas(displayio_group_t *self, displayio_area_t *tail) { - return group_relink_refresh_areas(group_get_refresh_areas_impl(self, tail), tail); + displayio_area_t *areas = group_get_refresh_areas_impl(self, tail); + return filter_out_redundant_areas(areas, tail); }