Skip to content

Commit 99a2fd4

Browse files
authored
Sync display_driver.py from lvgl-bindings 45be184 (#24)
lvgl-bindings#19: after a slow LVGL pass, the frame gate holds for at most max_yield_ms (default 100; event_loop(max_yield_ms=...) or on the instance). Passes up to 100 ms hold exactly as before, so lvgl-bindings#15's case is unchanged; a long pass -- typically application work run from LVGL timers -- no longer leaves the thread idle for as long again. It also brings d5f3d34 (lvgl-bindings#15), which this repo's pin predated: after a slow pass the gate holds for as long as the pass took, so a slow frame lowers the frame rate instead of taking the application's thread. Made with scripts/sync_from_lvgl_bindings.sh --ref 45be1848ed73057891aff7ccd477ce563a56939a.
1 parent 28e8aa7 commit 99a2fd4

2 files changed

Lines changed: 54 additions & 10 deletions

File tree

‎LVGL_BINDINGS_COMMIT‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
aa6c6bc449aed6936b644ab45b11e453945a18ee
1+
45be1848ed73057891aff7ccd477ce563a56939a

‎display_driver.py‎

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,9 @@ class event_loop:
563563

564564
_current_instance = None
565565

566+
# The most the gate holds LVGL back after a slow pass (see _arm_gate).
567+
max_yield_ms = 100
568+
566569
def __init__(
567570
self,
568571
freq=None,
@@ -571,6 +574,7 @@ def __init__(
571574
asynchronous=False,
572575
exception_sink=None,
573576
period_ms=None,
577+
max_yield_ms=None,
574578
):
575579
"""Create and register the LVGL event loop.
576580
@@ -583,6 +587,9 @@ def __init__(
583587
exception_sink: Callable receiving exceptions from task handling;
584588
defaults to :meth:`default_exception_sink`.
585589
period_ms: Explicit tick period in milliseconds (overrides ``freq``).
590+
max_yield_ms: Most the gate stays shut after a slow pass, leaving
591+
the thread to the application (default 100; see
592+
:meth:`_arm_gate`).
586593
587594
Raises:
588595
RuntimeError: Another loop is already running or async mode is
@@ -602,6 +609,8 @@ def __init__(
602609
self.delay = max(1, 1000 // int(freq))
603610
else:
604611
self.delay = LVGL_PERIOD_MS
612+
if max_yield_ms is not None:
613+
self.max_yield_ms = int(max_yield_ms)
605614

606615
self.refresh_cb = refresh_cb
607616
self.exception_sink = exception_sink if exception_sink else self.default_exception_sink
@@ -708,7 +717,7 @@ def _gate_allows(self):
708717
# Positive diff means _next_ok_ms is still in the future.
709718
return ticks_diff(self._next_ok_ms, ticks_ms()) <= 0
710719

711-
def _arm_gate(self):
720+
def _arm_gate(self, work_ms=0):
712721
"""Open the next slot one period after the last one, not after the work.
713722
714723
Pacing from *completion* silently halved the tick rate: the next timer
@@ -717,10 +726,31 @@ def _arm_gate(self):
717726
second tick was rejected no matter how fast the work was (measured 50/s
718727
on a 10 ms timer, ESP32-P4).
719728
720-
Advancing from the previous slot keeps the cadence for fast frames. The
721-
backlog protection the old comment was after is still there: if a slow
722-
flush overran its slot, resynchronise to now instead of letting the
723-
queued ticks fire back-to-back to catch up.
729+
Advancing from the previous slot keeps the cadence for fast frames.
730+
731+
The overrun branch is what decides how much of the thread the
732+
application gets, because on MicroPython the tick arrives through
733+
``micropython.schedule`` and so runs between the application's own
734+
bytecodes. Resynchronising to *now* opened the gate at the instant the
735+
slow pass ended, leaving only the one tick period before the next one
736+
started: a 67.8 ms repaint against a 10 ms tick took ~87 % of the
737+
thread indefinitely, and a 300-iteration Python loop took 20 340 ms
738+
(lvgl-bindings#15). So after an overrun the gate stays shut for as
739+
long as the pass itself took -- a slow frame halves the frame rate
740+
instead of taking the thread. ``now + delay`` is not enough: the
741+
timer's own cadence already delivers the next tick a period later,
742+
which is exactly the sliver the application was already getting.
743+
744+
Fast frames never reach this branch, so their cadence is untouched.
745+
746+
The hold is capped at ``max_yield_ms``. A pass much longer than a
747+
repaint is almost always the application's own work, run from LVGL
748+
timers (list rows built, results drained), and there is no other
749+
application code waiting for the thread: an uncapped hold only left
750+
it idle for as long again, doubling every UI stall. On an
751+
ESP32-S3-Touch-LCD-7 a 440 ms pass was followed by ~400 ms of idle,
752+
on every screen change. The capped hold still covers the ~60 ms
753+
repaints lvgl-bindings#15 was about.
724754
"""
725755
if ticks_ms is None or ticks_add is None or ticks_diff is None:
726756
return
@@ -730,9 +760,20 @@ def _arm_gate(self):
730760
return
731761
nxt = ticks_add(self._next_ok_ms, self.delay)
732762
if ticks_diff(nxt, now) < 0:
733-
nxt = now
763+
hold = min(work_ms, self.max_yield_ms)
764+
nxt = ticks_add(now, self.delay if hold < self.delay else hold)
734765
self._next_ok_ms = nxt
735766

767+
def _run_and_arm(self, run):
768+
"""Run one pass and arm the gate with what that pass cost."""
769+
if ticks_ms is None or ticks_diff is None:
770+
run()
771+
self._arm_gate()
772+
return
773+
start = ticks_ms()
774+
run()
775+
self._arm_gate(ticks_diff(ticks_ms(), start))
776+
736777
def timer_cb(self, t):
737778
"""Shared-timer callback: advance LVGL time and run/signal task handling.
738779
@@ -761,23 +802,26 @@ def timer_cb(self, t):
761802
self.refresh_event.set()
762803
self._arm_gate()
763804
else:
764-
self.task_handler()
765-
self._arm_gate()
805+
self._run_and_arm(self.task_handler)
766806

767807
async def async_refresh(self):
768808
"""Asyncio task body: wait for refresh signals and run ``lv.task_handler``."""
769809
while True:
770810
await self.refresh_event.wait()
771811
if _LV_NESTING is None or _LV_NESTING.value == 0:
772812
self.refresh_event.clear()
813+
start = ticks_ms() if ticks_ms is not None else None
773814
try:
774815
lv.task_handler()
775816
except Exception as e:
776817
if self.exception_sink:
777818
self.exception_sink(e)
778819
if self.refresh_cb:
779820
self.refresh_cb()
780-
self._arm_gate()
821+
if start is None:
822+
self._arm_gate()
823+
else:
824+
self._arm_gate(ticks_diff(ticks_ms(), start))
781825

782826
def default_exception_sink(self, e):
783827
"""Print ``e`` with traceback to stderr (default :attr:`exception_sink`)."""

0 commit comments

Comments
 (0)