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
38 changes: 37 additions & 1 deletion lib/display_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,14 @@ class DisplayDriver:
period, so rendering and presenting share one cadence.
"""

# DIRECT on a panel that can split its present (``show_nowait`` and
# ``wait_shown``): the last area starts the present and LVGL waits for it
# through its flush_wait_cb only when it is about to paint the
# framebuffers again, instead of blocking inside every flush.
_async_present = False
_present_pending = False # a present started and not yet waited for
_started_present = False # this flush started one (skip flush_ready)

def __init__(
self,
display_drv,
Expand Down Expand Up @@ -1568,6 +1576,17 @@ def __init__(
)
else:
self.lv_display.set_buffers(buf1, buf2, int(nbytes), lv.DISPLAY_RENDER_MODE.DIRECT)
if (
self._blocking
and callable(getattr(display_drv, "show_nowait", None))
and callable(getattr(display_drv, "wait_shown", None))
and hasattr(self.lv_display, "set_flush_wait_cb")
):
try:
self.lv_display.set_flush_wait_cb(self._flush_wait_cb)
self._async_present = True
except Exception:
self._async_present = False
else:
self._draw_buf1 = lv.draw_buf_create(
display_drv.width, display_drv.height // 10, color_format, 0
Expand Down Expand Up @@ -1650,6 +1669,7 @@ def _present(self):
def _flush_cb_direct(self, disp_drv, area, color_p):
"""DIRECT: LVGL already painted the panel FB; present on last area."""
done = False
self._started_present = False
try:
done = self._present_direct(area)
finally:
Expand All @@ -1659,9 +1679,21 @@ def _flush_cb_direct(self, disp_drv, area, color_p):
# interpreter is stuck inside the timer callback, and neither the
# REPL nor MicroPython's USB, which runs from the scheduler,
# answers again until a hard reset (micropython-pydevices#48).
if self._blocking or not done:
# A present that is still on its way is finished by
# _flush_wait_cb, which LVGL calls before it paints again.
if (self._blocking or not done) and not (done and self._started_present):
self.lv_display.flush_ready()

def _flush_wait_cb(self, disp_drv):
"""LVGL is about to paint: finish the present the last flush started."""
if not self._present_pending:
return
self._present_pending = False
try:
self.display_drv.wait_shown()
except Exception:
pass

def _present_direct(self, area):
"""The DIRECT flush's work. True once it has finished normally."""
panel = self.display_drv
Expand Down Expand Up @@ -1695,6 +1727,10 @@ def _present_direct(self, area):
frame_done = getattr(panel, "frame_done", None)
if frame_done is not None:
frame_done()
elif self._async_present:
pending = bool(panel.show_nowait())
self._present_pending = pending
self._started_present = pending
else:
panel.show()
self.presents += 1
Expand Down
38 changes: 38 additions & 0 deletions lib/displaydev/fbdisplay.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,44 @@ def show(self, _timer=None) -> None:
return
raw.refresh()

def show_nowait(self):
"""Start presenting the frame and return without waiting for it.

On a double-buffered native framebuffer that can split its present
(MicroPython ``DotClockFramebuffer`` with ``present()``/``wait()``),
this promotes the back buffer and returns at once; call
:meth:`wait_shown` before painting the framebuffers again. Elsewhere
it is :meth:`show`.

Returns:
bool: ``True`` when a present is pending and :meth:`wait_shown`
must follow, ``False`` when the frame is already out.
"""
raw = self._raw_buffer
present = getattr(raw, "present", None)
if (
self._display is None
and present is not None
and getattr(raw, "wait", None) is not None
and not getattr(raw, "auto_refresh", False)
):
present()
frame_done = getattr(self, "frame_done", None)
if frame_done is not None:
frame_done() # one frame for measure_fps, as show() would be
return True
self.show()
return False

def wait_shown(self):
"""Return once the frame :meth:`show_nowait` started is on screen.

Does nothing when no present is pending.
"""
wait = getattr(self._raw_buffer, "wait", None)
if wait is not None:
wait()

def flush_rect(self, x, y, w, h):
"""Make a directly-painted native framebuffer region visible.

Expand Down
78 changes: 78 additions & 0 deletions tests/test_lvgl_display_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,84 @@ def test_a_panel_without_frame_done_still_presents(self):
self.assertEqual(3, drv.presents)


class _AsyncPanel(_Panel):
"""A panel whose present can be split: show_nowait() then wait_shown()."""

def __init__(self, pending=True):
super().__init__(synced=False)
self.pending = pending
self.started = 0
self.waits = 0
self.log = []

def show_nowait(self):
self.started += 1
self.log.append("present")
return self.pending

def wait_shown(self):
self.waits += 1
self.log.append("wait")


class TestAsyncPresent(unittest.TestCase):
"""The last area starts the present; LVGL's flush_wait_cb finishes it."""

def _drive(self, panel, frames=3, areas=4):
cls = _display_driver_class()
drv = cls.__new__(cls)
drv.display_drv = panel
drv.lv_display = _LvDisplay()
drv._blocking = True
drv._async_present = True
drv.presents = 0
for _ in range(frames):
# What LVGL's wait_for_flushing does before it paints again.
drv._flush_wait_cb(None)
for i in range(areas):
drv.lv_display.last = i == areas - 1
drv._flush_cb_direct(None, _Area(0, i * 10, 99, i * 10 + 9), None)
return drv

def test_the_last_area_is_left_pending_until_lvgl_paints_again(self):
panel = _AsyncPanel()
drv = self._drive(panel)
self.assertEqual(3, panel.started)
self.assertEqual(0, panel.shows)
self.assertEqual(3, drv.presents)
# Three areas a frame report ready at once; the fourth waits.
self.assertEqual(9, drv.lv_display.ready)
# Each present is waited for before the next frame paints.
self.assertEqual(["present", "wait"] * 2 + ["present"], panel.log)
self.assertTrue(drv._present_pending)

def test_a_present_that_finished_at_once_reports_ready(self):
"""A panel without a split present: show_nowait() showed it."""
panel = _AsyncPanel(pending=False)
drv = self._drive(panel)
self.assertEqual(12, drv.lv_display.ready)
self.assertEqual(0, panel.waits)

def test_a_failed_present_still_reports_ready(self):
panel = _AsyncPanel()

def boom():
raise SystemExit()

panel.show_nowait = boom
cls = _display_driver_class()
drv = cls.__new__(cls)
drv.display_drv = panel
drv.lv_display = _LvDisplay()
drv.lv_display.last = True
drv._blocking = True
drv._async_present = True
drv.presents = 0
with self.assertRaises(SystemExit):
drv._flush_cb_direct(None, _Area(0, 0, 9, 9), None)
self.assertEqual(1, drv.lv_display.ready)


class _Raises:
"""A panel whose present raises what a stray soft reset or Ctrl-C raises."""

Expand Down
Loading