Skip to content

shared-module/busdisplay: merge overlapping dirty rectangles - #11189

Open
lynt-smitka wants to merge 2 commits into
adafruit:mainfrom
MakerClassCZ:fix/10687-busdisplay-merge-dirty-rects
Open

shared-module/busdisplay: merge overlapping dirty rectangles#11189
lynt-smitka wants to merge 2 commits into
adafruit:mainfrom
MakerClassCZ:fix/10687-busdisplay-merge-dirty-rects

Conversation

@lynt-smitka

Copy link
Copy Markdown

Addresses #10687: when dirty rectangles overlap, displayio resends the shared pixels once per rectangle (and draws them as separate, visible passes). A changing text label is the common trigger.

@tannewt proposed two options in the issue - a quick heuristic (sum the dirty areas; if they exceed the screen, do a full refresh) or the more complex rectangle merge. This PR implements merge. The heuristic only helps when the summed dirty area exceeds the whole screen; it does nothing when the change covers only part of the screen - the common case, e.g. a label that updates but doesn't fill the display. Merge helps there too, and when the change does fill the screen it matches the heuristic (the union is then the whole screen anyway).

Measured on a PicoPad (ST7789 SPI)

scene baseline [fps] heuristic (sum>screen) [fps] merge [fps]
large sub-screen text (scale 8) 10.45 10.07 (never triggers) 19.60
giant text, screen-filling (scale 20, reported case) 1.75 3.30 3.30
parallax (full-screen bg + band) 1.64 2.42 2.42

What it does

Before each refresh, the clipped dirty areas are copied into a small scratch array and any pair whose bounding box is smaller than the two summed is fused; non-overlapping rectangles stay separate. The motivating case is a changing text label: it dirties a whole-label rectangle plus a nested per-glyph rectangle per character, sending those pixels twice - merge collapses them into one.

  • Never sends more pixels than the unmerged code - no regression.
  • Greedy, bounded by MAX_MERGE_AREAS (16); above that, refreshes the raw list unmerged.
  • Near-zero for a single moving sprite (nothing to merge).
  • +304 B flash, ~192 B stack, no heap.

Fixes #10687

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 adafruit#10687

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about having Group do this instead? It'll have more context and work for all different display types.

@lynt-smitka

Copy link
Copy Markdown
Author

What do you think about having Group do this instead? It'll have more context and work for all different display types.

I agree, that will be more versatile. I'll see where it would be best to place it (shared-module/displayio/area.c?) and try to move it there.

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.
@lynt-smitka

Copy link
Copy Markdown
Author

Moved the merge into shared displayio code (area.c + display_core), used by busdisplay, framebufferio and epaperdisplay.

Group could still win a different case - combining a moved group's areas at the source (they touch rather than overlap, so list-level merging can't). But that's a bigger change. Should I try to implement it in this PR or make a new one?

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do it in group to prevent duplication. The comment has a link to where I'd do it.

Comment on lines +331 to +334
// 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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be repeated because the areas are const by this time. Instead do it here:

displayio_area_t *displayio_group_get_refresh_areas(displayio_group_t *self, displayio_area_t *tail) {
if (self->item_removed) {
self->dirty_area.next = tail;
tail = &self->dirty_area;
}
for (int32_t i = self->members->len - 1; i >= 0; i--) {
mp_obj_t layer;
#if CIRCUITPY_VECTORIO
const vectorio_draw_protocol_t *draw_protocol = mp_proto_get(MP_QSTR_protocol_draw, self->members->items[i]);
if (draw_protocol != NULL) {
layer = draw_protocol->draw_get_protocol_self(self->members->items[i]);
tail = draw_protocol->draw_protocol_impl->draw_get_refresh_areas(layer, tail);
continue;
}
#endif
layer = mp_obj_cast_to_native_base(
self->members->items[i], &displayio_tilegrid_type);
if (layer != MP_OBJ_NULL) {
if (!displayio_tilegrid_get_rendered_hidden(layer)) {
tail = displayio_tilegrid_get_refresh_areas(layer, tail);
}
continue;
}
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);
continue;
}
}
return tail;
}
There is always a root group that will do the top level. That way displays don't need to worry about it at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detect major dirty rectangle overlap and refresh everything instead

2 participants