Select Git revision
Forked from
flow3r / flow3r firmware
Source project has a limited visibility.
-
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).
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)