From e95404e9b7ffb00d225cb61737304a9d3911876e Mon Sep 17 00:00:00 2001
From: Sebastian Krzyszkowiak <dos@dosowisko.net>
Date: Sat, 16 Sep 2023 03:10:40 +0200
Subject: [PATCH] py,st3m: Only redraw overlays when something changes

Let each overlay report whether it needs to redraw itself
and what region of the screen it's interested in.

In order to not introduce unneccessary allocations, a single
mutable region object gets passed into each overlay.
---
 python_payload/st3m/ui/elements/overlays.py | 237 ++++++++++++++------
 1 file changed, 169 insertions(+), 68 deletions(-)

diff --git a/python_payload/st3m/ui/elements/overlays.py b/python_payload/st3m/ui/elements/overlays.py
index d582e8736d..5ab91ab769 100644
--- a/python_payload/st3m/ui/elements/overlays.py
+++ b/python_payload/st3m/ui/elements/overlays.py
@@ -38,19 +38,78 @@ _all_kinds = [
     OverlayKind.Toast,
 ]
 
-_clip_x0 = 120
-_clip_x1 = 120
-_clip_y0 = 0
-_clip_y1 = 0
+
+class Region:
+    x0: int
+    y0: int
+    x1: int
+    y1: int
+
+    def __init__(self, *args):
+        if not args:
+            self.clear()
+        else:
+            self.set(*args)
+
+    def set(self, x0, y0, x1, y1):
+        self.x0 = x0
+        self.y0 = y0
+        self.x1 = x1
+        self.y1 = y1
+
+    def add(self, x0, y0, x1, y1):
+        self.x0 = min(self.x0, x0)
+        self.y0 = min(self.y0, y0)
+        self.x1 = max(self.x1, x1)
+        self.y1 = max(self.y1, y1)
+
+    def add_region(self, rect):
+        self.add(rect.x0, rect.y0, rect.x1, rect.y1)
+
+    def clear(self):
+        self.x0 = self.y0 = self.x1 = self.y1 = -120
+
+    def fill(self, ctx):
+        ctx.rectangle(self.x0, self.y0, self.x1 - self.x0, self.y1 - self.y0)
+        ctx.fill()
+
+    def is_empty(self):
+        return self.x1 == self.x0 or self.y1 == self.y0
+
+    def set_clip(self):
+        sys_display.overlay_clip(
+            self.x0 + 120, self.y0 + 120, self.x1 + 120, self.y1 + 120
+        )
+
+    def copy(self, rect):
+        self.x0 = rect.x0
+        self.x1 = rect.x1
+        self.y0 = rect.y0
+        self.y1 = rect.y1
+
+    def __eq__(self, rect):
+        return (
+            self.x0 == rect.x0
+            and self.x1 == rect.x1
+            and self.y0 == rect.y0
+            and self.y1 == rect.y1
+        )
+
+    def __repr__(self):
+        return f"({self.x0} x {self.y0} - {self.x1} x {self.y1})"
 
 
 class Overlay(Responder):
     """
-    An Overlay is a Responder with some kind of OverlayKind attached.g
+    An Overlay is a Responder with some kind of OverlayKind attached.
     """
 
     kind: OverlayKind
 
+    def needs_redraw(rect: Region) -> bool:
+        rect.add(-120, -120, 120, 120)
+        return True
+
 
 class Compositor(Responder):
     """
@@ -67,7 +126,8 @@ class Compositor(Responder):
             OverlayKind.Toast: True,
         }
         self._last_fps_string = ""
-        self._frame_skip = 0
+        self._clip_rect = Region()
+        self._last_clip = Region()
 
     def _enabled_overlays(self) -> List[Responder]:
         res: List[Responder] = []
@@ -87,32 +147,31 @@ class Compositor(Responder):
             or settings.onoff_show_fps.value
         ):
             return
-        for overlay in self._enabled_overlays():
-            overlay.think(ins, delta_ms)
+        overlays = self._enabled_overlays()
+        for i in range(len(overlays)):
+            overlays[i].think(ins, delta_ms)
 
     def draw(self, ctx: Context) -> None:
-        global _clip_x0, _clip_y0, _clip_x1, _clip_y1
         self.main.draw(ctx)
         if (sys_display.get_mode() & sys_display.osd) == 0:
             return
+        overlays = self._enabled_overlays()
         if settings.onoff_show_fps.value:
             fps_string = "{0:.1f}".format(sys_display.fps())
-            if fps_string != self._last_fps_string and self._frame_skip <= 0:
+            if fps_string != self._last_fps_string:
                 octx = sys_display.ctx(sys_display.osd)
                 self._last_fps_string = fps_string
-                _clip_x0 = 0
-                _clip_y1 = 0
-                _clip_x1 = 240
-                _clip_y1 = 24
+                self._clip_rect.set(-120, -120, 120, -96)
+                self._last_clip.add_region(self._clip_rect)
+                if self._last_clip != self._clip_rect:
+                    octx.save()
+                    octx.compositing_mode = octx.CLEAR
+                    self._last_clip.fill(octx)
+                    octx.restore()
                 octx.save()
                 octx.rgba(0, 0, 0, 0.5)
                 octx.compositing_mode = octx.COPY
-                octx.rectangle(
-                    _clip_x0 - 120,
-                    _clip_y0 - 120,
-                    _clip_x1 - _clip_x0 + 1,
-                    _clip_y1 - _clip_y0 + 2,
-                ).fill()
+                self._clip_rect.fill(octx)
                 octx.restore()
                 octx.gray(1)
                 octx.font_size = 18
@@ -120,34 +179,32 @@ class Compositor(Responder):
                 octx.move_to(0, -103)
                 octx.text_align = octx.CENTER
                 octx.text(fps_string)
-                self._frame_skip = 5
                 sys_display.update(octx)
-                sys_display.overlay_clip(_clip_x0, _clip_y0, _clip_x1, _clip_y1)
-            else:
-                self._frame_skip -= 1
+                self._clip_rect.set_clip()
+                self._last_clip.copy(self._clip_rect)
         else:
-            if self._frame_skip <= 0:
-                overlays = self._enabled_overlays()
-                _clip_x0 = 80
-                _clip_y0 = 0
-                _clip_x1 = 160
-                _clip_y1 = 0
-
-                octx = sys_display.ctx(sys_display.osd)
-                octx.save()
-                octx.compositing_mode = octx.CLEAR
-                if len(overlays) == 1:
-                    octx.rectangle(-50, -120, 100, 150).fill()
-                else:
-                    octx.rectangle(-120, -120, 240, 240).fill()
-                octx.restore()
-                for overlay in overlays:
-                    overlay.draw(octx)
-                sys_display.update(octx)
-                sys_display.overlay_clip(_clip_x0, _clip_y0, _clip_x1, _clip_y1)
-                self._frame_skip = 2
-            else:
-                self._frame_skip -= 1
+            self._clip_rect.clear()
+            redraw = False
+            for i in range(len(overlays)):
+                redraw = overlays[i].needs_redraw(self._clip_rect) or redraw
+            if self._clip_rect.is_empty():
+                self._clip_rect.set_clip()
+                return
+            if self._last_clip != self._clip_rect:
+                redraw = True
+            if not redraw:
+                return
+            octx = sys_display.ctx(sys_display.osd)
+            octx.save()
+            octx.compositing_mode = octx.CLEAR
+            self._last_clip.add_region(self._clip_rect)
+            self._last_clip.fill(octx)
+            octx.restore()
+            for i in range(len(overlays)):
+                overlays[i].draw(octx)
+            sys_display.update(octx)
+            self._clip_rect.set_clip()
+            self._last_clip.copy(self._clip_rect)
 
     def add_overlay(self, ov: Overlay) -> None:
         """
@@ -235,6 +292,10 @@ class OverlayDebug(Overlay):
         ctx.gray(1).move_to(0, 86).text(self.text())
         ctx.restore()
 
+    def needs_redraw(self, rect: Region) -> bool:
+        rect.add(-120, 80, 120, 92)
+        return True
+
 
 class OverlayCaptouch(Overlay):
     kind = OverlayKind.Touch
@@ -273,16 +334,15 @@ class OverlayCaptouch(Overlay):
             dot.think(ins, delta_ms)
 
     def draw(self, ctx: Context) -> None:
-        global _clip_y1, _clip_x0, _clip_y0, _clip_y1
         ctx.rgb(1, 0, 1)
         for dot in self.dots:
             ctx.save()
             dot.draw(ctx)
             ctx.restore()
-        _clip_x0 = 0
-        _clip_x1 = 240
-        _clip_y0 = 0
-        _clip_y1 = 240
+
+    def needs_redraw(self, rect: Region) -> bool:
+        rect.add(0, 0, 240, 240)
+        return True
 
 
 class OverlayVolume(Overlay):
@@ -336,15 +396,13 @@ class OverlayVolume(Overlay):
 
         if self._showing is None:
             return
-        self._showing -= delta_ms / 2
+        self._showing -= delta_ms
         if self._showing < 0:
             self._showing = None
 
     def draw(self, ctx: Context) -> None:
-        global _clip_y1
         if self._showing is None:
             return
-        _clip_y1 = max(_clip_y1, 161)
 
         opacity = self._showing / 200
         opacity = min(opacity, 0.8)
@@ -393,6 +451,11 @@ class OverlayVolume(Overlay):
         ctx.round_rectangle(-30, 20, width, 10, 3)
         ctx.fill()
 
+    def needs_redraw(self, rect: Region) -> bool:
+        if self._showing:
+            rect.add(-40, -40, 40, 40)
+        return self._showing
+
 
 class Icon(Responder):
     """
@@ -403,11 +466,15 @@ class Icon(Responder):
     """
 
     WIDTH: int = 25
+    _changed: bool = True
 
     @abstractmethod
     def visible(self) -> bool:
         ...
 
+    def changed(self) -> bool:
+        return self._changed
+
 
 class USBIcon(Icon):
     """
@@ -418,8 +485,15 @@ class USBIcon(Icon):
 
     WIDTH: int = 30
 
+    def __init__(self):
+        self._visible = sys_kernel.usb_connected()
+
     def visible(self) -> bool:
-        return sys_kernel.usb_connected()
+        visible = sys_kernel.usb_connected()
+        if self._visible != visible:
+            self._changed = True
+            self._visible = visible
+        return self._visible
 
     def draw(self, ctx: Context) -> None:
         ctx.gray(1.0)
@@ -435,6 +509,12 @@ class USBIcon(Icon):
     def think(self, ins: InputState, delta_ms: int) -> None:
         pass
 
+    def changed(self) -> bool:
+        if self._changed:
+            self._changed = False
+            return True
+        return False
+
 
 class WifiIcon(Icon):
     WIDTH: int = 30
@@ -442,6 +522,7 @@ class WifiIcon(Icon):
     def __init__(self) -> None:
         super().__init__()
         self._rssi: float = -120
+        self._changed = True
 
     def visible(self) -> bool:
         return st3m.wifi.enabled()
@@ -461,8 +542,13 @@ class WifiIcon(Icon):
         ctx.gray(1.0 if r > -95 else 0.2)
         ctx.arc(0, 65, 40, a, b, 0).stroke()
 
+        self._changed = False
+
     def think(self, ins: InputState, delta_ms: int) -> None:
-        self._rssi = st3m.wifi.rssi()
+        rssi = st3m.wifi.rssi()
+        if rssi != self._rssi:
+            self._rssi = rssi
+            self._changed = True
 
 
 class BatteryIcon(Icon):
@@ -472,6 +558,7 @@ class BatteryIcon(Icon):
         super().__init__()
         self._percent = 100.0
         self._charging = False
+        self._changed = False
 
     def visible(self) -> bool:
         return True
@@ -506,9 +593,17 @@ class BatteryIcon(Icon):
             ctx.line_to(40, 35 - 10)
             ctx.stroke()
 
+        self._changed = False
+
     def think(self, ins: InputState, delta_ms: int) -> None:
-        self._percent = approximate_battery_percentage(power.battery_voltage)
-        self._charging = power.battery_charging
+        percent = approximate_battery_percentage(power.battery_voltage)
+        charging = power.battery_charging
+        if percent != self._percent:
+            self._percent = percent
+            self._changed = True
+        if charging != self._charging:
+            self._charging = charging
+            self._changed = True
 
 
 class IconTray(Overlay):
@@ -528,22 +623,28 @@ class IconTray(Overlay):
 
     def think(self, ins: InputState, delta_ms: int) -> None:
         self.visible = [i for i in self.icons if i.visible()]
-        for v in self.visible:
-            v.think(ins, delta_ms)
+        for i in range(len(self.visible)):
+            self.visible[i].think(ins, delta_ms)
 
     def draw(self, ctx: Context) -> None:
-        global _clip_y1
-        if len(self.visible) < 1:
+        if not self.visible:
             return
-        _clip_y1 = max(_clip_y1, 32)
         width = 0
-        for icon in self.visible:
-            width += icon.WIDTH
+        for i in range(len(self.visible)):
+            width += self.visible[i].WIDTH
         x0 = width / -2 + self.visible[0].WIDTH / 2
-        for i, v in enumerate(self.visible):
+        for i in range(len(self.visible)):
             ctx.save()
             ctx.translate(x0, -100)
             ctx.scale(0.13, 0.13)
-            v.draw(ctx)
+            self.visible[i].draw(ctx)
             ctx.restore()
-            x0 = x0 + v.WIDTH
+            x0 = x0 + self.visible[i].WIDTH
+
+    def needs_redraw(self, rect: Region) -> bool:
+        if self.visible:
+            rect.add(-40, -120, 40, -88)
+            for i in range(len(self.visible)):
+                if self.visible[i].changed():
+                    return True
+        return False
-- 
GitLab