Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions lib/displaydev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
40 changes: 39 additions & 1 deletion tests/test_displaydev_fps.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@ 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
self._requires_byteswap = False
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)
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
Loading