From 17fafba38bf3703564490182220acd89e4c88bf9 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 18 Oct 2026 18:10:36 -0700 Subject: [PATCH] displayio: draw RGB565 Bitmaps a row at a time A 16-bit Bitmap shown through an RGB565 ColorConverter without dithering reaches a 16-bit display unchanged apart from byte order, so it can take the existing row fast path instead of converting each pixel. Runs of 32 pixels not yet drawn are copied as a block when no transparent color is set. A full-screen 800x480 Bitmap on the ESP32-S31-Korvo-1 refreshes in about 35 ms instead of 255 ms. Co-Authored-By: Claude Opus 5.5 (1M context) --- shared-module/displayio/ColorConverter.c | 2 - shared-module/displayio/ColorConverter.h | 3 + shared-module/displayio/TileGrid.c | 138 ++++++++++++++++------- 3 files changed, 102 insertions(+), 41 deletions(-) diff --git a/shared-module/displayio/ColorConverter.c b/shared-module/displayio/ColorConverter.c index d4e64ee286e..2ee5f6cf0f9 100644 --- a/shared-module/displayio/ColorConverter.c +++ b/shared-module/displayio/ColorConverter.c @@ -9,8 +9,6 @@ #include "py/misc.h" #include "py/runtime.h" -#define NO_TRANSPARENT_COLOR (0x1000000) - uint32_t displayio_colorconverter_dither_noise_1(uint32_t n) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; diff --git a/shared-module/displayio/ColorConverter.h b/shared-module/displayio/ColorConverter.h index 304004d566a..8cab1f4b2b4 100644 --- a/shared-module/displayio/ColorConverter.h +++ b/shared-module/displayio/ColorConverter.h @@ -12,6 +12,9 @@ #include "py/obj.h" #include "shared-module/displayio/Palette.h" +// Stored in transparent_color when no color is transparent. It is outside the 24 bit color range. +#define NO_TRANSPARENT_COLOR (0x1000000) + typedef struct displayio_colorconverter { mp_obj_base_t base; bool dither; diff --git a/shared-module/displayio/TileGrid.c b/shared-module/displayio/TileGrid.c index 88eb0f694fa..c70c9327a5e 100644 --- a/shared-module/displayio/TileGrid.c +++ b/shared-module/displayio/TileGrid.c @@ -425,17 +425,25 @@ void common_hal_displayio_tilegrid_set_top_left(displayio_tilegrid_t *self, uint self->full_change = true; } +// Any transparent color above this value can never match a 16 bit pixel. +#define MAX_16BIT_PIXEL_VALUE (0xffff) + // One row of the common case: an 8 or 16 bpp Bitmap or a mapped 8 bpp OnDiskBitmap, a Palette -// without dithering, a 16 bpp display, no scaling, no x flip and no transpose. Same result as the -// per-pixel loop in displayio_tilegrid_fill_area, without the per-pixel calls. Returns false if a -// pixel was transparent. +// without dithering, a 16 bpp display, no scaling, no x flip and no transpose. Also a 16 bpp +// Bitmap with an RGB565 ColorConverter without dithering, whose pixels reach a 16 bpp display +// unchanged apart from byte order. Same result as the per-pixel loop in +// displayio_tilegrid_fill_area, without the per-pixel calls. Returns false if a pixel was +// transparent. static bool _fill_row_fast(displayio_tilegrid_t *self, const void *tiles, displayio_bitmap_t *bitmap, displayio_ondiskbitmap_t *odb, displayio_palette_t *palette, + displayio_colorconverter_t *converter, const _displayio_colorspace_t *colorspace, uint32_t *mask, uint16_t *buffer, int32_t offset, uint16_t x, uint16_t y, int16_t count, int16_t x_in_tile, uint16_t x_tile_index, uint16_t row_tile_location, int16_t y_in_tile) { bool covered = true; uint16_t tile_width = self->tile_width; + bool swap = converter != NULL && + (converter->input_colorspace == DISPLAYIO_COLORSPACE_RGB565_SWAPPED) != colorspace->reverse_bytes_in_word; while (count > 0) { uint16_t tile_location = row_tile_location + x_tile_index; // uint8_t like displayio_input_pixel_t.tile, which the per-pixel loop goes through. @@ -466,36 +474,78 @@ static bool _fill_row_fast(displayio_tilegrid_t *self, const void *tiles, row = bitmap->data + by * bitmap->stride; } bool row_inside = by >= 0 && by < bitmap_height; - for (int16_t i = 0; i < run; i++, offset++, bx++, x++) { - uint32_t bit = 1u << (offset & 31); - if (mask[offset >> 5] & bit) { - continue; - } - uint32_t index = 0; // get_pixel returns 0 outside the bitmap - if (row_inside && bx >= 0 && bx < bitmap_width) { - index = (bits_per_value == 8) ? - ((const uint8_t *)row)[bx] : ((const uint16_t *)row)[bx]; - } - if (index >= palette->color_count || palette->colors[index].transparent) { - covered = false; - continue; + if (converter != NULL) { + // A separate loop, so the Palette loop below stays as fast as before. With no + // transparent color, copy 32 pixels at a time where none of them is set yet. + bool blocks = converter->transparent_color > MAX_16BIT_PIXEL_VALUE && + row_inside && bx >= 0 && bx + run <= bitmap_width; + for (int16_t i = 0; i < run; i++, offset++, bx++) { + while (blocks && (offset & 31) == 0 && run - i >= 32 && mask[offset >> 5] == 0) { + const uint16_t *in = (const uint16_t *)row + bx; + uint16_t *out = buffer + offset; + if (swap) { + for (int k = 0; k < 32; k++) { + out[k] = __builtin_bswap16(in[k]); + } + } else { + memcpy(out, in, 32 * sizeof(uint16_t)); + } + mask[offset >> 5] = 0xffffffff; + i += 32; + offset += 32; + bx += 32; + } + if (i >= run) { + break; + } + uint32_t bit = 1u << (offset & 31); + if (mask[offset >> 5] & bit) { + continue; + } + uint32_t pixel = 0; // get_pixel returns 0 outside the bitmap + if (row_inside && bx >= 0 && bx < bitmap_width) { + pixel = ((const uint16_t *)row)[bx]; + } + if (pixel == converter->transparent_color) { + covered = false; + continue; + } + mask[offset >> 5] |= bit; + buffer[offset] = swap ? __builtin_bswap16(pixel) : pixel; } - const _displayio_color_t *color = &palette->colors[index]; - uint16_t pixel; - if (color->cached_colorspace == colorspace && - color->cached_colorspace_grayscale_bit == colorspace->grayscale_bit && - color->cached_colorspace_grayscale == colorspace->grayscale) { - pixel = color->cached_color; - } else { - displayio_input_pixel_t input_pixel = { - .pixel = index, .x = x, .y = y, .tile = tile, .tile_x = bx, .tile_y = by, - }; - displayio_output_pixel_t output_pixel = { .pixel = 0, .opaque = true }; - displayio_palette_get_color(palette, colorspace, &input_pixel, &output_pixel); - pixel = output_pixel.pixel; + x += run; + } else { + for (int16_t i = 0; i < run; i++, offset++, bx++, x++) { + uint32_t bit = 1u << (offset & 31); + if (mask[offset >> 5] & bit) { + continue; + } + uint32_t index = 0; // get_pixel returns 0 outside the bitmap + if (row_inside && bx >= 0 && bx < bitmap_width) { + index = (bits_per_value == 8) ? + ((const uint8_t *)row)[bx] : ((const uint16_t *)row)[bx]; + } + if (index >= palette->color_count || palette->colors[index].transparent) { + covered = false; + continue; + } + const _displayio_color_t *color = &palette->colors[index]; + uint16_t pixel; + if (color->cached_colorspace == colorspace && + color->cached_colorspace_grayscale_bit == colorspace->grayscale_bit && + color->cached_colorspace_grayscale == colorspace->grayscale) { + pixel = color->cached_color; + } else { + displayio_input_pixel_t input_pixel = { + .pixel = index, .x = x, .y = y, .tile = tile, .tile_x = bx, .tile_y = by, + }; + displayio_output_pixel_t output_pixel = { .pixel = 0, .opaque = true }; + displayio_palette_get_color(palette, colorspace, &input_pixel, &output_pixel); + pixel = output_pixel.pixel; + } + mask[offset >> 5] |= bit; + buffer[offset] = pixel; } - mask[offset >> 5] |= bit; - buffer[offset] = pixel; } count -= run; x_in_tile = 0; @@ -617,15 +667,25 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self, displayio_bitmap_t *row_bitmap = NULL; displayio_ondiskbitmap_t *row_odb = NULL; displayio_palette_t *row_palette = NULL; + displayio_colorconverter_t *row_converter = NULL; if (scale == 1 && x_stride == 1 && colorspace->depth == 16 && self->transpose_xy == self->absolute_transform->transpose_xy && - mp_obj_is_type(self->bitmap, &displayio_bitmap_type) && - mp_obj_is_type(self->pixel_shader, &displayio_palette_type)) { + mp_obj_is_type(self->bitmap, &displayio_bitmap_type)) { displayio_bitmap_t *bitmap = self->bitmap; - displayio_palette_t *palette = self->pixel_shader; - if ((bitmap->bits_per_value == 8 || bitmap->bits_per_value == 16) && !palette->dither) { - row_bitmap = bitmap; - row_palette = palette; + if (mp_obj_is_type(self->pixel_shader, &displayio_palette_type)) { + displayio_palette_t *palette = self->pixel_shader; + if ((bitmap->bits_per_value == 8 || bitmap->bits_per_value == 16) && !palette->dither) { + row_bitmap = bitmap; + row_palette = palette; + } + } else if (mp_obj_is_type(self->pixel_shader, &displayio_colorconverter_type)) { + displayio_colorconverter_t *converter = self->pixel_shader; + if (bitmap->bits_per_value == 16 && !converter->dither && + (converter->input_colorspace == DISPLAYIO_COLORSPACE_RGB565 || + converter->input_colorspace == DISPLAYIO_COLORSPACE_RGB565_SWAPPED)) { + row_bitmap = bitmap; + row_converter = converter; + } } } #if CIRCUITPY_STORAGE_MAP_FILE @@ -655,8 +715,8 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self, int16_t tile_base_y = 0; if (row_bitmap != NULL || row_odb != NULL) { - if (!_fill_row_fast(self, tiles, row_bitmap, row_odb, row_palette, colorspace, mask, - (uint16_t *)buffer, row_start + x_shift, start_x, input_pixel.y, + if (!_fill_row_fast(self, tiles, row_bitmap, row_odb, row_palette, row_converter, + colorspace, mask, (uint16_t *)buffer, row_start + x_shift, start_x, input_pixel.y, end_x - start_x, x_in_tile, x_tile_index, row_tile_location, y_in_tile)) { full_coverage = false; }