From fcc4ea097af4ad371cc049bd99abb10ae7d40707 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:05:47 -0500 Subject: [PATCH] display_driver: present DIRECT frames without waiting inside the flush On a framebuffer that can split its present (FBDisplay.show_nowait and wait_shown, over DotClockFramebuffer.present/wait), the last area starts the present and LVGL's flush_wait_cb finishes it just before LVGL paints again. The LVGL pass no longer blocks up to a scanout frame per present, and the loop's slow-pass yield no longer counts that wait as work. Panels without the split keep presenting with show(). --- lib/display_driver.py | 38 ++++++++++++++- lib/displaydev/fbdisplay.py | 38 +++++++++++++++ tests/test_lvgl_display_driver.py | 78 +++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) diff --git a/lib/display_driver.py b/lib/display_driver.py index 1eec0458..f99496dc 100644 --- a/lib/display_driver.py +++ b/lib/display_driver.py @@ -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, @@ -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 @@ -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: @@ -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 @@ -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 diff --git a/lib/displaydev/fbdisplay.py b/lib/displaydev/fbdisplay.py index 3e57d139..4720f593 100644 --- a/lib/displaydev/fbdisplay.py +++ b/lib/displaydev/fbdisplay.py @@ -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. diff --git a/tests/test_lvgl_display_driver.py b/tests/test_lvgl_display_driver.py index bcb7d64f..d4cab747 100644 --- a/tests/test_lvgl_display_driver.py +++ b/tests/test_lvgl_display_driver.py @@ -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."""