Skip to content
Snippets Groups Projects
Select Git revision
  • 18f17883bd787e088e60edc93075d551d1d2b9fd
  • main default protected
  • phhw
  • captouch-threshold
  • t
  • dos
  • test2
  • test
  • slewtest
  • simtest
  • view-think
  • vm-pending
  • media-buf
  • scope
  • passthrough
  • wave
  • vsync
  • dos-main-patch-50543
  • json-error
  • rahix/big-flow3r
  • pippin/media_framework
  • v1.3.0
  • v1.2.0
  • v1.2.0+rc1
  • v1.1.1
  • v1.1.0
  • v1.1.0+rc1
  • v1.0.0
  • v1.0.0+rc6
  • v1.0.0+rc5
  • v1.0.0+rc4
  • v1.0.0+rc3
  • v1.0.0+rc2
  • v1.0.0+rc1
34 results

reactor.py

Blame
  • Forked from flow3r / flow3r firmware
    Source project has a limited visibility.
    • dos's avatar
      18f17883
      py,st3m: Reactor: Don't make draw calls unless the drawlist is submittable · 18f17883
      dos authored and pippin's avatar pippin committed
      In the current graphics pipeline, only a single drawlist can be submitted
      to the rasterizer's queue at a time. This means that at a given time,
      there are two "pending" frames: the one currently rasterized, and the one
      placed in the queue. Once the queued frame's rasterization starts, a new
      frame can be queued. This means that to achieve full pipeline utilization,
      a new frame can be submitted anytime between the moment when the queue
      becomes empty and when the rasterization of the new frame finishes.
      
      Previously, Reactor would eagerly make the draw calls as soon as a drawlist
      became free, setting the drawlist content in stone. Then, at the end of each
      think cycle, it would check whether the queue became free and submit the frame
      if that's the case. Since draw calls are lightweight, it usually wasn't the
      case right away, meaning that the already prepared drawlist would have to
      wait for (at least) the next think cycle until being submitted - for no actual
      benefit, as another frame would still be rasterized at that point of time.
      
      Reduce latency and only make the draw calls once a drawlist becomes submittable.
      The worst case latency regression is a fraction of draw call duration (which
      usually is very short anyway), while best case improvement is frame rendering
      time plus think cycle time (which can be very long for heavy frames).
      18f17883
      History
      py,st3m: Reactor: Don't make draw calls unless the drawlist is submittable
      dos authored and pippin's avatar pippin committed
      In the current graphics pipeline, only a single drawlist can be submitted
      to the rasterizer's queue at a time. This means that at a given time,
      there are two "pending" frames: the one currently rasterized, and the one
      placed in the queue. Once the queued frame's rasterization starts, a new
      frame can be queued. This means that to achieve full pipeline utilization,
      a new frame can be submitted anytime between the moment when the queue
      becomes empty and when the rasterization of the new frame finishes.
      
      Previously, Reactor would eagerly make the draw calls as soon as a drawlist
      became free, setting the drawlist content in stone. Then, at the end of each
      think cycle, it would check whether the queue became free and submit the frame
      if that's the case. Since draw calls are lightweight, it usually wasn't the
      case right away, meaning that the already prepared drawlist would have to
      wait for (at least) the next think cycle until being submitted - for no actual
      benefit, as another frame would still be rasterized at that point of time.
      
      Reduce latency and only make the draw calls once a drawlist becomes submittable.
      The worst case latency regression is a fraction of draw call duration (which
      usually is very short anyway), while best case improvement is frame rendering
      time plus think cycle time (which can be very long for heavy frames).
    reactor.py 4.94 KiB
    from st3m.goose import ABCBase, abstractmethod, List, Optional
    from st3m.input import InputState
    from st3m.profiling import ftop
    from st3m.utils import RingBuf
    from ctx import Context
    
    import time
    import sys_display
    import sys_kernel
    import captouch
    
    
    class Responder(ABCBase):
        """
        Responder is an interface from the Reactor to any running Micropython code
        that wishes to access input state or draw to the screen.
    
        A Responder can be a system menu, an application, a graphical widget, etc.
    
        The Reactor will call think and draw methods at a somewhat-constant pace in
        order to maintain a smooth system-wide update rate and framerate.
        """
    
        @abstractmethod
        def think(self, ins: InputState, delta_ms: int) -> None:
            """
            think() will be called when the Responder should process the InputState
            and perform internal logic. delta_ms will be set to the number of
            milliseconds elapsed since the last reactor think loop.
    
            The code must not sleep or block during this callback, as that will
            impact the system tickrate and framerate.
            """
            pass
    
        @abstractmethod
        def draw(self, ctx: Context) -> None:
            """
            draw() will be called when the Responder should draw, ie. generate a drawlist by performing calls on the given ctx object.
    
            Depending on what calls the Responder, the ctx might either represent
            the surface of the entire screen, or some composited-subview (eg. an
            application screen that is currently being transitioned out by sliding
            left). Unless specified otherwise by the compositing stack, the screen
            coordinates are +/- 120 in both X and Y (positive numbers towards up and
            right), with 0,0 being the middle of the screen.
    
            The Reactor will then rasterize and blit the result.
    
            The code must not sleep or block during this callback, as that will
            impact the system tickrate and framerate.
            """
            pass
    
    
    class ReactorStats:
        SIZE = 20
    
        def __init__(self) -> None:
            self.run_times = RingBuf(self.SIZE)
            self.render_times = RingBuf(self.SIZE)
    
        def record_run_time(self, ms: int) -> None:
            self.run_times.append(ms)
    
        def record_render_time(self, ms: int) -> None:
            self.render_times.append(ms)
    
    
    class Reactor:
        """
        The Reactor is the main Micropython scheduler of the st3m system and any
        running payloads.
    
        It will attempt to run a top Responder with a fixed tickrate a framerate
        that saturates the display rasterization/blitting pipeline.
        """
    
        __slots__ = (
            "_top",
            "_tickrate_ms",
            "_last_tick",
            "_ctx",
            "_ts",
            "_last_ctx_get",
            "stats",
        )
    
        def __init__(self) -> None:
            self._top: Optional[Responder] = None
            self._tickrate_ms: int = 20
            self._ts: int = 0
            self._last_tick: Optional[int] = None
            self._last_ctx_get: Optional[int] = None
            self._ctx: Optional[Context] = None
            self.stats = ReactorStats()
    
        def set_top(self, top: Responder) -> None:
            """
            Set top Responder. It will be called by the reactor in a loop once run()
            is called.
    
            This can be also called after the reactor is started.
            """
            self._top = top
    
        def run(self) -> None:
            """
            Run the reactor forever, processing the top Responder in a loop.
            """
            while True:
                self._run_once()
    
        def _run_once(self) -> None:
            start = time.ticks_ms()
            deadline = start + self._tickrate_ms
    
            self._run_top(start)
    
            end = time.ticks_ms()
            elapsed = end - start
            self.stats.record_run_time(elapsed)
            wait = deadline - end
            if wait > 0:
                sys_kernel.freertos_sleep(wait)
    
        def _run_top(self, start: int) -> None:
            # Skip if we have no top Responder.
            if self._top is None:
                return
    
            # Calculate delta (default to tickrate if running first iteration).
            delta = self._tickrate_ms
            if self._last_tick is not None:
                delta = start - self._last_tick
            self._last_tick = start
    
            self._ts += delta
    
            # temp band aid against input dropping, will be cleaned up in
            # upcoming input api refactor
            captouch.refresh_events()
    
            hr = InputState.gather()
    
            # Think!
            self._top.think(hr, delta)
    
            # Draw!
            if sys_display.pipe_available() and not sys_display.pipe_full():
                ctx = sys_display.ctx(0)
                if ctx is not None:
                    if self._last_ctx_get is not None:
                        diff = start - self._last_ctx_get
                        self.stats.record_render_time(diff)
                    self._last_ctx_get = start
    
                    ctx.save()
                    self._top.draw(ctx)
                    ctx.restore()
    
                    sys_display.update(ctx)
    
            # Share!
            if ftop.run(delta):
                print(ftop.report)