diff --git a/lib/displaydev/__init__.py b/lib/displaydev/__init__.py index 6103b1a7..0b3888d4 100644 --- a/lib/displaydev/__init__.py +++ b/lib/displaydev/__init__.py @@ -454,6 +454,7 @@ class _FpsMeter: def __init__(self, now, diff): self.now = now self.diff = diff + self.in_flush = False # inside a wrapped flush_rect/blit_rect self.reset() def reset(self): @@ -504,7 +505,7 @@ def frame(self, t, dt): self._advance(t) def flush(self, t, dt, pixels): - """A ``flush_rect`` ended at ``t`` after ``dt`` µs: part of a frame.""" + """A ``flush_rect`` or ``blit_rect`` ended at ``t`` after ``dt`` µs: part of a frame.""" self.frame_us += dt self.win_present += dt self.win_flushes += 1 @@ -987,12 +988,14 @@ def bfa_area(self): def measure_fps(self, on=True): """Count frames and time presents (``on=True``), or stop (``on=False``). - Switched on, the instance's ``show`` and ``flush_rect`` are wrapped - with timers; switched off they are the class's own methods again. A - frame is one ``show()``, or one :meth:`frame_done` from a GUI that - presents with ``flush_rect`` (LVGL on a direct-framebuffer panel). - ``flush_rect`` calls are counted as ``flushes`` and ``pixels``, not - frames. ``PYDEVICES_FPS=1`` switches it on when the driver starts. + Switched on, the instance's ``show``, ``flush_rect`` and ``blit_rect`` + are wrapped with timers; switched off they are the class's own + methods again. A frame is one ``show()``, or one :meth:`frame_done` + from a GUI that presents with ``flush_rect`` (LVGL on a + direct-framebuffer panel). ``flush_rect`` and ``blit_rect`` calls + (a partial-render panel presents through ``blit_rect``) are counted + as ``flushes`` and ``pixels``, not frames; one that calls the other + is counted once. ``PYDEVICES_FPS=1`` switches it on when the driver starts. Returns: bool: Whether it is measuring now. @@ -1004,6 +1007,7 @@ def measure_fps(self, on=True): self._fps_wrapped = [] self._fps_wrap("show") self._fps_wrap("flush_rect") + self._fps_wrap("blit_rect") elif self._fps is not None: self.fps_print(0) for name, wrapper, prev in self._fps_wrapped: @@ -1031,10 +1035,28 @@ def _fps_wrap(self, name): if name == "flush_rect": def wrapper(x, y, w, h, *args, **kwargs): + if meter.in_flush: + return orig(x, y, w, h, *args, **kwargs) + meter.in_flush = True t = now() try: return orig(x, y, w, h, *args, **kwargs) finally: + meter.in_flush = False + t1 = now() + meter.flush(t1, diff(t1, t), w * h) + + elif name == "blit_rect": + + def wrapper(buf, x, y, w, h, *args, **kwargs): + if meter.in_flush: + return orig(buf, x, y, w, h, *args, **kwargs) + meter.in_flush = True + t = now() + try: + return orig(buf, x, y, w, h, *args, **kwargs) + finally: + meter.in_flush = False t1 = now() meter.flush(t1, diff(t1, t), w * h) diff --git a/tests/test_displaydev_fps.py b/tests/test_displaydev_fps.py index 5769766c..4bdc9163 100644 --- a/tests/test_displaydev_fps.py +++ b/tests/test_displaydev_fps.py @@ -35,7 +35,7 @@ def diff(a, b): class Panel(DisplayDriver): - def __init__(self, clock=None, show_us=0, flush_us=0): + def __init__(self, clock=None, show_us=0, flush_us=0, blit_us=0): self._width = 8 self._height = 4 self._rotation = 0 @@ -43,6 +43,8 @@ def __init__(self, clock=None, show_us=0, flush_us=0): self.clock = clock self.show_us = show_us self.flush_us = flush_us + self.blit_us = blit_us + self.blit_calls_flush = False self.shows = 0 self.flushed = [] super().__init__(quiet=True) @@ -54,6 +56,10 @@ def fill_rect(self, x, y, w, h, c): return (x, y, w, h) def blit_rect(self, buf, x, y, w, h): + if self.clock is not None: + self.clock.t += self.blit_us + if self.blit_calls_flush: + self.flush_rect(x, y, w, h) return (x, y, w, h) def show(self, _timer=None): @@ -196,6 +202,38 @@ def test_flush_rect_counts_flushes_and_pixels_not_frames(self): self.assertAlmostEqual(1.0, s["present_ms_max"]) self.assertEqual(6, len(d.flushed)) + def test_blit_rect_counts_flushes_and_pixels(self): + # A PARTIAL panel presents through blit_rect, not flush_rect. + d = Panel(self.clock, blit_us=500) + d.measure_fps(True) + for _ in range(3): + self.assertEqual((0, 0, 8, 2), d.blit_rect(bytearray(32), 0, 0, 8, 2)) + d.blit_rect(bytearray(16), 0, 2, 4, 2) + d.frame_done() + self.clock.t += 10000 + s = d.fps() + self.assertEqual(3, s["frames"]) + self.assertEqual(6, s["flushes"]) + self.assertEqual(3 * (16 + 8), s["pixels"]) + self.assertAlmostEqual(1.0, s["present_ms"]) + + def test_a_blit_that_flushes_is_counted_once(self): + d = Panel(self.clock, flush_us=500) + d.blit_calls_flush = True + d.measure_fps(True) + d.blit_rect(bytearray(32), 0, 0, 8, 2) + s = d.fps() + self.assertEqual(1, len(d.flushed)) + self.assertEqual(1, s["flushes"]) + self.assertEqual(16, s["pixels"]) + + def test_switching_off_restores_blit_rect(self): + d = Panel() + d.measure_fps(True) + self.assertIn("blit_rect", d.__dict__) + d.measure_fps(False) + self.assertNotIn("blit_rect", d.__dict__) + def test_flushes_alone_are_not_frames(self): d = Panel(self.clock, flush_us=100) d.measure_fps(True)