From: Melissa LeBlanc-Williams Date: Fri, 22 Sep 2023 21:25:07 +0000 (-0700) Subject: Fewer bugs, more code, shape done X-Git-Tag: 1.0.0^2~19 X-Git-Url: https://git.ayoreis.com/hackapet/Adafruit_Blinka_Displayio.git/commitdiff_plain/8dc304ff4f82d37e493b9500e17fb13799dc200f Fewer bugs, more code, shape done --- diff --git a/displayio/__init__.py b/displayio/__init__.py index 74facd9..fad9122 100644 --- a/displayio/__init__.py +++ b/displayio/__init__.py @@ -43,8 +43,9 @@ display_buses = [] def _background(): """Main thread function to loop through all displays and update them""" - for display in displays: - display.background() + while True: + for display in displays: + display.background() def release_displays() -> None: diff --git a/displayio/_area.py b/displayio/_area.py index 5b96e93..f7272dd 100644 --- a/displayio/_area.py +++ b/displayio/_area.py @@ -38,19 +38,22 @@ class Area: def __str__(self): return f"Area TL({self.x1},{self.y1}) BR({self.x2},{self.y2})" - def _copy_into(self, dst) -> None: + def copy_into(self, dst) -> None: + """Copy the area into another area.""" dst.x1 = self.x1 dst.y1 = self.y1 dst.x2 = self.x2 dst.y2 = self.y2 - def _scale(self, scale: int) -> None: + def scale(self, scale: int) -> None: + """Scale the area by scale.""" self.x1 *= scale self.y1 *= scale self.x2 *= scale self.y2 *= scale - def _shift(self, dx: int, dy: int) -> None: + def shift(self, dx: int, dy: int) -> None: + """Shift the area by dx and dy.""" self.x1 += dx self.y1 += dy self.x2 += dx @@ -70,21 +73,24 @@ class Area: return overlap.y1 < overlap.y2 - def _empty(self): + def empty(self): + """Return True if the area is empty.""" return (self.x1 == self.x2) or (self.y1 == self.y2) - def _canon(self): + def canon(self): + """Make sure the area is in canonical form.""" if self.x1 > self.x2: self.x1, self.x2 = self.x2, self.x1 if self.y1 > self.y2: self.y1, self.y2 = self.y2, self.y1 - def _union(self, other, union): - if self._empty(): - self._copy_into(union) + def union(self, other, union): + """Combine this area along with another into union""" + if self.empty(): + self.copy_into(union) return - if other._empty(): # pylint: disable=protected-access - other._copy_into(union) # pylint: disable=protected-access + if other.empty(): + other.copy_into(union) return union.x1 = min(self.x1, other.x1) diff --git a/displayio/_bitmap.py b/displayio/_bitmap.py index c61de47..03dd75b 100644 --- a/displayio/_bitmap.py +++ b/displayio/_bitmap.py @@ -21,6 +21,7 @@ from __future__ import annotations from typing import Union, Tuple from PIL import Image from ._structs import RectangleStruct +from ._area import Area __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" @@ -76,6 +77,9 @@ class Bitmap: if x > self._image.width or y > self._image.height: raise ValueError(f"Index {index} is out of range") + return self._get_pixel(x, y) + + def _get_pixel(self, x: int, y: int) -> int: return self._image.getpixel((x, y)) def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None: @@ -129,8 +133,8 @@ class Bitmap: y2: int, skip_index: int, ) -> None: - # pylint: disable=unnecessary-pass, invalid-name """Inserts the source_bitmap region defined by rectangular boundaries""" + # pylint: disable=invalid-name if x2 is None: x2 = source_bitmap.width if y2 is None: @@ -172,9 +176,28 @@ class Bitmap: break def dirty(self, x1: int = 0, y1: int = 0, x2: int = -1, y2: int = -1) -> None: - # pylint: disable=unnecessary-pass, invalid-name """Inform displayio of bitmap updates done via the buffer protocol.""" - pass + # pylint: disable=invalid-name + if x2 == -1: + x2 = self._bmp_width + if y2 == -1: + y2 = self._bmp_height + area = Area(x1, y1, x2, y2) + area.canon() + area.union(self._dirty_area, area) + bitmap_area = Area(0, 0, self._bmp_width, self._bmp_height) + area.compute_overlap(bitmap_area, self._dirty_area) + + def _finish_refresh(self): + if self._read_only: + return + self._dirty_area.x1 = 0 + self._dirty_area.x2 = 0 + + def _get_refresh_areas(self, areas: list[Area]) -> None: + if self._dirty_area.x1 == self._dirty_area.x2 or self._read_only: + return + areas.append(self._dirty_area) @property def width(self) -> int: diff --git a/displayio/_colorconverter.py b/displayio/_colorconverter.py index db57b6a..f8d8deb 100644 --- a/displayio/_colorconverter.py +++ b/displayio/_colorconverter.py @@ -45,6 +45,7 @@ class ColorConverter: self._cached_colorspace = None self._cached_input_pixel = None self._cached_output_color = None + self._needs_refresh = False @staticmethod def _dither_noise_1(noise): @@ -84,7 +85,7 @@ class ColorConverter: red8 = color_rgb888 >> 16 grn8 = (color_rgb888 >> 8) & 0xFF blu8 = color_rgb888 & 0xFF - return (red8 * 19 + grn8 * 182 + blu8 + 54) / 255 + return (red8 * 19 + grn8 * 182 + blu8 + 54) // 255 @staticmethod def _compute_chroma(color_rgb888: int): @@ -104,11 +105,11 @@ class ColorConverter: return 0 hue = 0 if max_color == red8: - hue = (((grn8 - blu8) * 40) / chroma) % 240 + hue = (((grn8 - blu8) * 40) // chroma) % 240 elif max_color == grn8: - hue = (((blu8 - red8) + (2 * chroma)) * 40) / chroma + hue = (((blu8 - red8) + (2 * chroma)) * 40) // chroma elif max_color == blu8: - hue = (((red8 - grn8) + (4 * chroma)) * 40) / chroma + hue = (((red8 - grn8) + (4 * chroma)) * 40) // chroma if hue < 0: hue += 240 diff --git a/displayio/_display.py b/displayio/_display.py index 8e33803..cdf9f78 100644 --- a/displayio/_display.py +++ b/displayio/_display.py @@ -191,8 +191,9 @@ class Display: allocate_display, ) - allocate_display(cls) - return super().__new__(cls) + display_instance = super().__new__(cls) + allocate_display(display_instance) + return display_instance def _initialize(self, init_sequence): i = 0 @@ -290,12 +291,12 @@ class Display: return True def _refresh_display(self): - # pylint: disable=protected-access if not self._core.start_refresh(): return False # TODO: Likely move this to _refresh_area() # Go through groups and and add each to buffer + """ if self._core.current_group is not None: buffer = Image.new("RGBA", (self._core.width, self._core.height)) # Recursively have everything draw to the image @@ -304,7 +305,7 @@ class Display: ) # pylint: disable=protected-access # save image to buffer (or probably refresh buffer so we can compare) self._buffer.paste(buffer) - + """ areas_to_refresh = self._get_refresh_areas() for area in areas_to_refresh: @@ -317,9 +318,12 @@ class Display: def _get_refresh_areas(self) -> list[Area]: """Get a list of areas to be refreshed""" areas = [] - if self._core.current_group is not None: - # Eventually calculate dirty rectangles here - areas.append(Area(0, 0, self._core.width, self._core.height)) + if self._core.full_refresh: + areas.append(self._core.area) + elif self._core.current_group is not None: + self._core.current_group._get_refresh_areas( # pylint: disable=protected-access + areas + ) return areas def background(self): diff --git a/displayio/_displaycore.py b/displayio/_displaycore.py index 3126b87..d2f298a 100644 --- a/displayio/_displaycore.py +++ b/displayio/_displaycore.py @@ -258,21 +258,6 @@ class _DisplayCore: ) return False - """ - def _clip(self, rectangle): - if self._core.rotation in (90, 270): - width, height = self._core.height, self._core.width - else: - width, height = self._core.width, self._core.height - - rectangle.x1 = max(rectangle.x1, 0) - rectangle.y1 = max(rectangle.y1, 0) - rectangle.x2 = min(rectangle.x2, width) - rectangle.y2 = min(rectangle.y2, height) - - return rectangle - """ - def clip_area(self, area: Area, clipped: Area) -> bool: """Shrink the area to the region shared by the two areas""" @@ -308,11 +293,11 @@ class _DisplayCore: if self.colorspace.depth < 8: pixels_per_byte = 8 // self.colorspace.depth if self.colorspace.pixels_in_byte_share_row: - region_x1 /= pixels_per_byte * self.colorspace.bytes_per_cell - region_x2 /= pixels_per_byte * self.colorspace.bytes_per_cell + region_x1 //= pixels_per_byte * self.colorspace.bytes_per_cell + region_x2 //= pixels_per_byte * self.colorspace.bytes_per_cell else: - region_y1 /= pixels_per_byte * self.colorspace.bytes_per_cell - region_y2 /= pixels_per_byte * self.colorspace.bytes_per_cell + region_y1 //= pixels_per_byte * self.colorspace.bytes_per_cell + region_y2 //= pixels_per_byte * self.colorspace.bytes_per_cell region_x2 -= 1 region_y2 -= 1 @@ -330,12 +315,6 @@ class _DisplayCore: ) else: data_type = DISPLAY_COMMAND - """ - self._core.send( - DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data - ) - self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data) - """ if self.ram_width < 0x100: # Single Byte Bounds data = struct.pack(">BB", region_x1, region_x2) @@ -426,6 +405,7 @@ class _DisplayCore: """ Send the data to the current bus """ + print(data_type, chip_select, data) self._send(data_type, chip_select, data) def begin_transaction(self) -> None: diff --git a/displayio/_group.py b/displayio/_group.py index a23ae4e..903d721 100644 --- a/displayio/_group.py +++ b/displayio/_group.py @@ -52,6 +52,7 @@ class Group: self._layers = [] self._supported_types = (TileGrid, Group) self._in_group = False + self._item_removed = False self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False) self._set_scale(scale) # Set the scale via the setter @@ -166,6 +167,12 @@ class Group: if isinstance(layer, (Group, TileGrid)): layer._finish_refresh() # pylint: disable=protected-access + def _get_refresh_areas(self, areas: list[Area]) -> None: + for layer in self._layers: + if isinstance(layer, (Group, TileGrid)): + if not layer.hidden: + layer._get_refresh_areas(areas) # pylint: disable=protected-access + @property def hidden(self) -> bool: """True when the Group and all of it's layers are not visible. When False, the diff --git a/displayio/_helpers.py b/displayio/_helpers.py index cbff9f4..39e1c11 100644 --- a/displayio/_helpers.py +++ b/displayio/_helpers.py @@ -20,10 +20,12 @@ displayio for Blinka __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + def clamp(value, min_value, max_value): """Clamp a value between a minimum and maximum value""" return max(min(max_value, value), min_value) + def bswap16(value): """Swap the bytes in a 16 bit value""" - return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8 \ No newline at end of file + return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8 diff --git a/displayio/_i2cdisplay.py b/displayio/_i2cdisplay.py index b5de381..67022f5 100644 --- a/displayio/_i2cdisplay.py +++ b/displayio/_i2cdisplay.py @@ -58,8 +58,9 @@ class I2CDisplay: allocate_display_bus, ) - allocate_display_bus(cls) - return super().__new__(cls) + display_bus_instance = super().__new__(cls) + allocate_display_bus(display_bus_instance) + return display_bus_instance def _release(self): self.reset() diff --git a/displayio/_palette.py b/displayio/_palette.py index 2424db1..af72dcb 100644 --- a/displayio/_palette.py +++ b/displayio/_palette.py @@ -139,6 +139,9 @@ class Palette: """Returns True if the palette index is transparent. Returns False if opaque.""" return self._colors[palette_index].transparent + def _finish_refresh(self): + pass + @property def dither(self) -> bool: """When true the palette dithers the output by adding diff --git a/displayio/_shape.py b/displayio/_shape.py index 946919d..81d15a0 100644 --- a/displayio/_shape.py +++ b/displayio/_shape.py @@ -18,7 +18,10 @@ displayio for Blinka """ +import struct from ._bitmap import Bitmap +from ._area import Area +from ._helpers import clamp __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" @@ -33,14 +36,95 @@ class Shape(Bitmap): def __init__( self, width: int, height: int, *, mirror_x: bool = False, mirror_y: bool = False ): - # pylint: disable=unused-argument """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the column boundaries of the shape on each row. Each row’s boundary defaults to the full row. """ + self._mirror_x = mirror_x + self._mirror_y = mirror_y + self._width = width + self._height = height + if self._mirror_x: + width //= 2 + width += self._width % 2 + self._half_width = width + if self._mirror_y: + height //= 2 + height += self._height % 2 + self._half_height = height + self._data = bytearray(height * struct.calcsize("HH")) + for i in range(height): + self._data[2 * i] = 0 + self._data[2 * i + 1] = width + + self._dirty_area = Area(0, 0, width, height) super().__init__(width, height, 2) def set_boundary(self, y: int, start_x: int, end_x: int) -> None: - # pylint: disable=unnecessary-pass """Loads pre-packed data into the given row.""" - pass + max_y = self._height - 1 + if self._mirror_y: + max_y = self._half_height - 1 + y = clamp(y, 0, max_y) + max_x = self._width - 1 + if self._mirror_x: + max_x = self._half_width - 1 + start_x = clamp(start_x, 0, max_x) + end_x = clamp(end_x, 0, max_x) + + # find x-boundaries for updating based on current data and start_x, end_x, and mirror_x + lower_x = min(start_x, self._data[2 * y]) + + if self._mirror_x: + upper_x = ( + self._width - lower_x + 1 + ) # dirty rectangles are treated with max value exclusive + else: + upper_x = max( + end_x, self._data[2 * y + 1] + ) # dirty rectangles are treated with max value exclusive + + # find y-boundaries based on y and mirror_y + lower_y = y + + if self._mirror_y: + upper_y = ( + self._height - lower_y + 1 + ) # dirty rectangles are treated with max value exclusive + else: + upper_y = y + 1 # dirty rectangles are treated with max value exclusive + + self._data[2 * y] = start_x # update the data array with the new boundaries + self._data[2 * y + 1] = end_x + + if self._dirty_area.x1 == self._dirty_area.x2: # dirty region is empty + self._dirty_area.x1 = lower_x + self._dirty_area.x2 = upper_x + self._dirty_area.y1 = lower_y + self._dirty_area.y2 = upper_y + else: + self._dirty_area.x1 = min(lower_x, self._dirty_area.x1) + self._dirty_area.x2 = max(upper_x, self._dirty_area.x2) + self._dirty_area.y1 = min(lower_y, self._dirty_area.y1) + self._dirty_area.y2 = max(upper_y, self._dirty_area.y2) + + def _get_pixel(self, x: int, y: int) -> int: + if x >= self._width or x < 0 or y >= self._height or y < 0: + return 0 + if self._mirror_x and x >= self._half_width: + x = self._width - x - 1 + if self._mirror_y and y >= self._half_height: + y = self._height - y - 1 + start_x = self._data[2 * y] + end_x = self._data[2 * y + 1] + if x < start_x or x >= end_x: + return 0 + return 1 + + def _finish_refresh(self): + self._dirty_area.x1 = 0 + self._dirty_area.x2 = 0 + + def _get_refresh_areas(self, areas: list[Area]) -> None: + if self._dirty_area.x1 != self._dirty_area.x2: + areas.append(self._dirty_area) diff --git a/displayio/_tilegrid.py b/displayio/_tilegrid.py index 0cfc6d3..72e57c0 100644 --- a/displayio/_tilegrid.py +++ b/displayio/_tilegrid.py @@ -73,6 +73,8 @@ class TileGrid: if isinstance(self._pixel_shader, ColorConverter): self._pixel_shader._rgba = True # pylint: disable=protected-access self._hidden_tilegrid = False + self._hidden_by_parent = False + self._rendered_hidden = False self._x = x self._y = y self._width_in_tiles = width @@ -98,16 +100,21 @@ class TileGrid: raise ValueError("Default Tile is out of range") self._pixel_width = width * tile_width self._pixel_height = height * tile_height - self._tiles = (self._width_in_tiles * self._height_in_tiles) * [default_tile] + self._tiles = bytearray( + (self._width_in_tiles * self._height_in_tiles) * [default_tile] + ) self._in_group = False self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False) self._current_area = Area(0, 0, self._pixel_width, self._pixel_height) + self._dirty_area = Area(0, 0, 0, 0) + self._previous_area = Area(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF) self._moved = False + self._full_change = True + self._partial_change = True self._bitmap_width_in_tiles = bitmap_width // tile_width self._tiles_in_bitmap = self._bitmap_width_in_tiles * ( bitmap_height // tile_height ) - self.inline_tiles = False # We have plenty of memory def _update_transform(self, absolute_transform): """Update the parent transform and child transforms""" @@ -121,6 +128,7 @@ class TileGrid: width = self._pixel_height else: width = self._pixel_width + if self._absolute_transform.transpose_xy: self._current_area.y1 = ( self._absolute_transform.y + self._absolute_transform.dy * self._x @@ -153,6 +161,7 @@ class TileGrid: height = self._pixel_width else: height = self._pixel_height + if self._absolute_transform.transpose_xy: self._current_area.x1 = ( self._absolute_transform.x + self._absolute_transform.dx * self._y @@ -210,7 +219,7 @@ class TileGrid: # If no tiles are present we have no impact tiles = self._tiles - if self._hidden_tilegrid: + if self._hidden_tilegrid or self._hidden_by_parent: return False overlap = Area() @@ -230,10 +239,10 @@ class TileGrid: start = 0 if (self._absolute_transform.dx < 0) != flip_x: - start += (area.width() - 1) * x_stride + start += (area.x2 - area.x1 - 1) * x_stride x_stride *= -1 if (self._absolute_transform.dy < 0) != flip_y: - start += (area.height() - 1) * y_stride + start += (area.y2 - area.y1 - 1) * y_stride y_stride *= -1 full_coverage = area == overlap @@ -298,7 +307,11 @@ class TileGrid: input_pixel.tile // self._bitmap_width_in_tiles ) * self._tile_height + local_y % self._tile_height - input_pixel.pixel = self.bitmap[input_pixel.tile_x, input_pixel.tile_y] + input_pixel.pixel = ( + self._bitmap._get_pixel( # pylint: disable=protected-access + input_pixel.tile_x, input_pixel.tile_y + ) + ) output_pixel.opaque = True if self._pixel_shader is None: @@ -352,7 +365,108 @@ class TileGrid: return full_coverage def _finish_refresh(self): - pass + first_draw = self._previous_area.x1 == self._previous_area.x2 + hidden = self._hidden_tilegrid or self._hidden_by_parent + if not first_draw and hidden: + self._previous_area.x2 = self._previous_area.x1 + elif self._moved or first_draw: + self._current_area.copy_into(self._previous_area) + + self._moved = False + self._full_change = False + self._partial_change = False + if isinstance(self._pixel_shader, (Palette, ColorConverter)): + self._pixel_shader._finish_refresh() # pylint: disable=protected-access + if isinstance(self._bitmap, (Bitmap, Shape)): + self._bitmap._finish_refresh() # pylint: disable=protected-access + + def _get_refresh_areas(self, areas: list[Area]) -> None: + # pylint: disable=invalid-name, too-many-branches, too-many-statements + first_draw = self._previous_area.x1 == self._previous_area.x2 + hidden = self._hidden_tilegrid or self._hidden_by_parent + + # Check hidden first because it trumps all other changes + if hidden: + self._rendered_hidden = True + if not first_draw: + areas.append(self._previous_area) + return + if self._moved and not first_draw: + self._previous_area.union(self._current_area, self._dirty_area) + if self._dirty_area.size() < 2 * self._pixel_width * self._pixel_height: + areas.append(self._dirty_area) + return + areas.append(self._current_area) + areas.append(self._previous_area) + return + + tail = areas[-1] + # If we have an in-memory bitmap, then check it for modifications + if isinstance(self._bitmap, Bitmap): + self._bitmap._get_refresh_areas(areas) # pylint: disable=protected-access + if tail != areas[-1]: + # Special case a TileGrid that shows a full bitmap and use its + # dirty area. Copy it to ours so we can transform it. + if self._tiles_in_bitmap == 1: + areas[-1].copy_into(self._dirty_area) + self._partial_change = True + else: + self._full_change = True + elif isinstance(self._bitmap, Shape): + self._bitmap._get_refresh_areas(areas) # pylint: disable=protected-access + if areas[-1] != tail: + areas[-1].copy_into(self._dirty_area) + self._partial_change = True + + self._full_change = self._full_change or ( + isinstance(self._pixel_shader, (Palette, ColorConverter)) + and self._pixel_shader._needs_refresh # pylint: disable=protected-access + ) + if self._full_change or first_draw: + areas.append(self._current_area) + return + + if self._partial_change: + x = self._x + y = self._y + if self._absolute_transform.transpose_xy: + x, y = y, x + x1 = self._dirty_area.x1 + x2 = self._dirty_area.x2 + if self._flip_x: + x1 = self._pixel_width - x1 + x2 = self._pixel_width - x2 + y1 = self._dirty_area.y1 + y2 = self._dirty_area.y2 + if self._flip_y: + y1 = self._pixel_height - y1 + y2 = self._pixel_height - y2 + if self._transpose_xy != self._absolute_transform.transpose_xy: + x1, y1 = y1, x1 + x2, y2 = y2, x2 + self._dirty_area.x1 = ( + self._absolute_transform.x + self._absolute_transform.dx * (x + x1) + ) + self._dirty_area.y1 = ( + self._absolute_transform.y + self._absolute_transform.dy * (y + y1) + ) + self._dirty_area.x2 = ( + self._absolute_transform.x + self._absolute_transform.dx * (x + x2) + ) + self._dirty_area.y2 = ( + self._absolute_transform.y + self._absolute_transform.dy * (y + y2) + ) + if self._dirty_area.y2 < self._dirty_area.y1: + self._dirty_area.y1, self._dirty_area.y2 = ( + self._dirty_area.y2, + self._dirty_area.y1, + ) + if self._dirty_area.x2 < self._dirty_area.x1: + self._dirty_area.x1, self._dirty_area.x2 = ( + self._dirty_area.x2, + self._dirty_area.x1, + ) + areas.append(self._dirty_area) @property def hidden(self) -> bool: