Files
lamatrix/renderloop.py

174 lines
5.1 KiB
Python
Executable File

# The game looop
import time
import gc
from math import ceil
if not hasattr(time, 'sleep_ms') or not hasattr(time, 'ticks_ms'):
# Emulate https://docs.pycom.io/firmwareapi/micropython/utime.html
time.ticks_ms = lambda: int(time.time() * 1000)
time.sleep_ms = lambda x: time.sleep(x/1000.0)
class RenderLoop:
def __init__(self, display, config=None):
self.display = display
self.debug = False
self.fps = display.fps
self.t_next_frame = None
self.prev_frame = 0
self.frame = 1
self.t_init = time.ticks_ms()
self.scenes = []
self.scene_index = 0
self.scene_switch_effect = 0
self.scene_switch_countdown = self.fps * 40
self.display.clear()
if not config:
return
if 'debug' in config:
self.debug = config['debug']
if 'sceneTimeout' in config:
self.scene_switch_countdown = self.fps * config['sceneTimeout']
def add_scene(self, scene):
"""
Add new scene to the render loop.
Called by main.py.
"""
self.scenes.append(scene)
def next_frame(self, button_state=0):
"""
Display next frame, possibly after a delay to ensure we meet the FPS target
Called by main.py.
"""
scene = self.scenes[self.scene_index]
# Process input
if button_state:
# Let the scene handle input
handled_bit = scene.input(button_state)
button_state &= ~handled_bit
# Use long-pressed buttons to handle intensity changes
if button_state & 0x22:
clear = 0
for s in self.scenes:
if hasattr(s, 'set_intensity'):
i = s.set_intensity()
if button_state & 0x02:
i -= 2
clear = 0x02
elif button_state & 0x20:
i += 2
clear = 0x20
i = (i + 32) % 32
s.set_intensity(i)
button_state &= ~clear
if self.debug:
print('RenderLoop: updated intensity to {} on scenes, remaining state: {}'.format(i, button_state))
# Calculate how much we need to wait before rendering the next frame
t_now = time.ticks_ms() - self.t_init
if not self.t_next_frame:
self.t_next_frame = t_now
delay = self.t_next_frame - t_now
if delay >= 0:
# Wait until we can display next frame
time.sleep_ms(delay)
else:
# Resynchronize
num_dropped_frames = ceil(-delay*self.fps/1000)
if self.debug:
print('RenderLoop: FPS {} too high, should\'ve rendered frame {} at {}ms but was {}ms late and dropped {} frames'.format(self.fps, self.frame, self.t_next_frame, -delay, num_dropped_frames))
self.frame += num_dropped_frames
self.t_next_frame += ceil(1000*num_dropped_frames/self.fps)
if self.debug:
print('RenderLoop: Updated frame counters to frame {} with current next at {}'.format(self.frame, self.t_next_frame))
# Let the scene render its frame
t = time.ticks_ms()
loop_again = scene.render(self.frame, self.frame - self.prev_frame - 1, self.fps)
t = time.ticks_ms() - t
if t > 1000/self.fps and self.debug:
print('RenderLoop: WARN: Spent {}ms rendering'.format(t))
# Consider switching scenes and update frame counters
self.scene_switch_countdown -= 1
scene_increment = 1
if not loop_again:
self.scene_switch_countdown = 0
elif button_state:
self.scene_switch_countdown = 0
if button_state & 0x1:
scene_increment = -1
if not self.scene_switch_countdown:
self.reset_scene_switch_counter()
# Transition to next scene
self.next_scene(scene_increment, button_state)
# Account for time wasted above
t_new = time.ticks_ms() - self.t_init
t_diff = t_new - t_now
frames_wasted = ceil(t_diff*self.fps/1000.0)
if self.debug:
print('RenderLoop: setup: scene switch took {}ms, original t {}ms, new t {}ms, spent {} frames'.format(t_diff, t_now,t_new, self.fps*t_diff/1000.0))
self.frame += int(frames_wasted)
self.t_next_frame += int(1000.0 * frames_wasted / self.fps)
self.prev_frame = self.frame
self.frame += 1
self.t_next_frame += int(1000/self.fps)
def reset_scene_switch_counter(self):
"""
Reset counter used to automatically switch scenes.
The counter is decreased in .next_frame()
"""
self.scene_switch_countdown = 45 * self.fps
def next_scene(self, increment=1, button_state=0):
"""
Transition to a new scene and re-initialize the scene
"""
if len(self.scenes) < 2:
return
print('RenderLoop: next_scene: transitioning scene')
# Fade out current scene
t0 = time.ticks_ms()
if button_state & 0x01:
self.display.hscroll(-4)
button_state &= ~0x01
elif button_state & 0x10:
self.display.hscroll(4)
button_state &= ~0x10
else:
effect = self.scene_switch_effect
self.scene_switch_effect = (effect + 1) % 4
if effect == 0:
self.display.vscroll()
elif effect == 1:
self.display.hscroll()
elif effect == 2:
self.display.fade()
else:
self.display.dissolve()
t2 = time.ticks_ms()
t1 = t2 - t0
gc.collect()
t3 = time.ticks_ms()
t2 = t3 - t1
num_scenes = len(self.scenes)
i = self.scene_index = (num_scenes + self.scene_index + increment) % num_scenes
# (Re-)initialize scene
self.scenes[i].reset()
t4 = time.ticks_ms()
t3 = t4 - t3
if self.debug:
print('RenderLoop: next_scene: selected {}, effect {}ms, gc {}ms, scene reset {}ms, total {}ms'.format(self.scenes[i].__class__.__name__, t1, t2, t3, t4-t0))
return button_state