2026-03-11 13:49:05 +01:00
|
|
|
# Breakout app UI/driver glue. This app renders into a framebuffer that may be
|
|
|
|
|
# smaller than the full display (partial framebuffer). The draw loop is more
|
|
|
|
|
# complex because it slices the screen into chunks, renders each slice in C,
|
|
|
|
|
# and flushes them sequentially using a flush-ready IRQ callback. A scheduled
|
|
|
|
|
# (non-IRQ) handler advances chunks so it can work on larger-than-320x230
|
|
|
|
|
# displays without requiring a full-size framebuffer.
|
2026-03-09 12:39:19 +01:00
|
|
|
import lvgl as lv
|
2026-03-10 19:00:01 +01:00
|
|
|
import mpos.ui
|
2026-03-09 12:39:19 +01:00
|
|
|
from mpos import Activity, DisplayMetrics, InputManager
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
if sys.platform == "esp32":
|
2026-03-11 11:57:42 +01:00
|
|
|
import breakout_xtensawin as breakout
|
2026-03-09 12:39:19 +01:00
|
|
|
else:
|
2026-03-11 11:57:42 +01:00
|
|
|
import breakout_x64 as breakout
|
2026-03-09 12:39:19 +01:00
|
|
|
|
2026-03-09 23:14:25 +01:00
|
|
|
class Breakout(Activity):
|
2026-03-09 12:39:19 +01:00
|
|
|
|
|
|
|
|
hor_res = 0
|
|
|
|
|
ver_res = 0
|
2026-03-09 18:56:06 +01:00
|
|
|
paddle_move_step = None
|
2026-03-09 12:39:19 +01:00
|
|
|
layer = None
|
|
|
|
|
buffer = None
|
2026-03-09 19:09:34 +01:00
|
|
|
touch_active = False
|
|
|
|
|
touch_last_x = None
|
2026-03-09 21:08:36 +01:00
|
|
|
last_fps = 0
|
|
|
|
|
average_fps = 0
|
2026-03-09 12:39:19 +01:00
|
|
|
|
2026-03-10 22:27:30 +01:00
|
|
|
old_callback = None
|
|
|
|
|
|
|
|
|
|
render_next = True
|
2026-03-11 13:17:50 +01:00
|
|
|
flush_ready = False
|
|
|
|
|
chunk_in_progress = False
|
|
|
|
|
chunk_waiting = False
|
|
|
|
|
chunk_rows_per = 0
|
|
|
|
|
chunk_total = 0
|
|
|
|
|
chunk_index = 0
|
2026-03-11 13:24:48 +01:00
|
|
|
|
2026-03-09 12:39:19 +01:00
|
|
|
# Widgets:
|
|
|
|
|
screen = None
|
|
|
|
|
canvas = None
|
2026-03-09 18:56:06 +01:00
|
|
|
leftbutton = None
|
2026-03-09 21:08:36 +01:00
|
|
|
playbutton = None
|
2026-03-09 18:56:06 +01:00
|
|
|
rightbutton = None
|
2026-03-09 12:39:19 +01:00
|
|
|
|
|
|
|
|
def onCreate(self):
|
|
|
|
|
self.screen = lv.obj()
|
2026-03-09 23:14:25 +01:00
|
|
|
self.screen.add_flag(lv.obj.FLAG.CLICKABLE)
|
|
|
|
|
self.screen.add_event_cb(self.touch_cb, lv.EVENT.ALL, None)
|
|
|
|
|
|
2026-03-09 12:39:19 +01:00
|
|
|
d = lv.display_get_default()
|
|
|
|
|
self.hor_res = d.get_horizontal_resolution()
|
2026-03-11 13:49:05 +01:00
|
|
|
self.paddle_move_step = round(self.hor_res/10)
|
2026-03-09 12:39:19 +01:00
|
|
|
self.ver_res = d.get_vertical_resolution()
|
2026-03-09 23:20:12 +01:00
|
|
|
|
2026-03-09 18:56:06 +01:00
|
|
|
self.leftbutton = lv.button(self.screen)
|
|
|
|
|
self.leftbutton.align(lv.ALIGN.BOTTOM_LEFT, 0, 0)
|
|
|
|
|
leftlabel = lv.label(self.leftbutton)
|
|
|
|
|
leftlabel.set_text("<")
|
2026-03-09 21:08:36 +01:00
|
|
|
self.leftbutton.add_event_cb(lambda e: self.move_left_unfocus(),lv.EVENT.FOCUSED,None)
|
2026-03-09 18:56:06 +01:00
|
|
|
self.leftbutton.add_event_cb(lambda e: self.move_left(),lv.EVENT.CLICKED,None)
|
|
|
|
|
|
2026-03-09 21:08:36 +01:00
|
|
|
# Invisible button, just for defocusing the left and right buttons:
|
|
|
|
|
self.play_button = lv.button(self.screen)
|
2026-03-10 19:00:01 +01:00
|
|
|
self.play_button.align(lv.ALIGN.BOTTOM_MID,0,0)
|
|
|
|
|
self.play_button.set_size(1,1)
|
2026-03-09 21:08:36 +01:00
|
|
|
self.play_button.set_style_opa(lv.OPA.TRANSP, lv.PART.MAIN)
|
|
|
|
|
|
2026-03-09 18:56:06 +01:00
|
|
|
self.rightbutton = lv.button(self.screen)
|
|
|
|
|
self.rightbutton.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0)
|
|
|
|
|
rightlabel = lv.label(self.rightbutton)
|
|
|
|
|
rightlabel.set_text(">")
|
2026-03-09 21:08:36 +01:00
|
|
|
self.rightbutton.add_event_cb(lambda e: self.move_right_unfocus(),lv.EVENT.FOCUSED,None)
|
2026-03-09 18:56:06 +01:00
|
|
|
self.rightbutton.add_event_cb(lambda e: self.move_right(),lv.EVENT.CLICKED,None)
|
|
|
|
|
|
2026-03-09 12:39:19 +01:00
|
|
|
self.setContentView(self.screen)
|
|
|
|
|
|
|
|
|
|
def onResume(self, screen):
|
2026-03-09 21:08:36 +01:00
|
|
|
lv.log_register_print_cb(self.log_callback)
|
2026-03-11 14:06:49 +01:00
|
|
|
breakout.init(mpos.ui.main_display._frame_buffer1, self.hor_res, self.ver_res)
|
|
|
|
|
mpos.ui.main_display._data_bus.register_callback(self.flush_ready_cb)
|
|
|
|
|
mpos.ui.task_handler.add_event_cb(self.drawframe, mpos.ui.task_handler.TASK_HANDLER_FINISHED)
|
2026-03-10 19:00:01 +01:00
|
|
|
|
2026-03-09 12:39:19 +01:00
|
|
|
def onPause(self, screen):
|
2026-03-09 21:08:36 +01:00
|
|
|
lv.log_register_print_cb(None)
|
2026-03-10 22:27:30 +01:00
|
|
|
mpos.ui.main_display._data_bus.register_callback(mpos.ui.main_display._flush_ready_cb)
|
|
|
|
|
|
2026-03-09 18:56:06 +01:00
|
|
|
def move_left(self):
|
2026-03-11 11:57:42 +01:00
|
|
|
breakout.move_paddle(-self.paddle_move_step)
|
2026-03-09 18:56:06 +01:00
|
|
|
|
|
|
|
|
def move_right(self):
|
2026-03-11 11:57:42 +01:00
|
|
|
breakout.move_paddle(self.paddle_move_step)
|
2026-03-09 18:56:06 +01:00
|
|
|
|
2026-03-09 21:08:36 +01:00
|
|
|
def move_left_unfocus(self):
|
|
|
|
|
self.unfocus()
|
2026-03-11 11:57:42 +01:00
|
|
|
breakout.move_paddle(-self.paddle_move_step)
|
2026-03-09 21:08:36 +01:00
|
|
|
|
|
|
|
|
def move_right_unfocus(self):
|
|
|
|
|
self.unfocus()
|
2026-03-11 11:57:42 +01:00
|
|
|
breakout.move_paddle(self.paddle_move_step)
|
2026-03-09 21:08:36 +01:00
|
|
|
|
2026-03-09 21:25:49 +01:00
|
|
|
# This only works with the PREV/pageup and NEXT/pagedown buttons,
|
|
|
|
|
# because the focus_direction handling of the arrow keys uses a trick to move focus (focus_next)
|
|
|
|
|
# which conflicts with the focus_next below...
|
2026-03-09 21:08:36 +01:00
|
|
|
def unfocus(self):
|
|
|
|
|
focusgroup = lv.group_get_default()
|
|
|
|
|
if not focusgroup:
|
|
|
|
|
print("WARNING: imageview.py could not get default focus group")
|
|
|
|
|
return
|
|
|
|
|
focused = focusgroup.get_focused()
|
|
|
|
|
if focused:
|
|
|
|
|
if focused == self.rightbutton:
|
|
|
|
|
focusgroup.focus_prev()
|
|
|
|
|
elif focused == self.leftbutton:
|
|
|
|
|
focusgroup.focus_next()
|
|
|
|
|
else:
|
|
|
|
|
print("focus isn't on next or previous, leaving it...")
|
|
|
|
|
|
2026-03-11 13:49:05 +01:00
|
|
|
def flush_ready_cb(self, arg1=None, arg2=None):
|
|
|
|
|
# This is called in IRQ (interrupt) context so it can't allocate memory
|
2026-03-11 14:00:32 +01:00
|
|
|
# So no printf, no calling drawframe() directly, just setting variables or scheduling a function.
|
2026-03-11 13:49:05 +01:00
|
|
|
mpos.ui.main_display._disp_drv.flush_ready()
|
|
|
|
|
self.flush_ready = True
|
2026-03-10 19:00:01 +01:00
|
|
|
|
2026-03-11 11:57:42 +01:00
|
|
|
def drawframe(self, arg1=None, arg2=None):
|
2026-03-11 13:17:50 +01:00
|
|
|
if self.chunk_waiting:
|
|
|
|
|
if self.flush_ready:
|
|
|
|
|
self.flush_ready = False
|
|
|
|
|
self.chunk_waiting = False
|
|
|
|
|
self.chunk_index += 1
|
|
|
|
|
if self.chunk_index >= self.chunk_total:
|
|
|
|
|
self.chunk_in_progress = False
|
|
|
|
|
self.render_next = True
|
|
|
|
|
else:
|
|
|
|
|
self._render_and_send_chunk()
|
2026-03-10 22:27:30 +01:00
|
|
|
return
|
2026-03-11 13:17:50 +01:00
|
|
|
|
|
|
|
|
if self.chunk_in_progress or not self.render_next:
|
|
|
|
|
return
|
|
|
|
|
|
2026-03-10 22:27:30 +01:00
|
|
|
self.render_next = False
|
2026-03-11 13:17:50 +01:00
|
|
|
|
|
|
|
|
buffer_len = len(mpos.ui.main_display._frame_buffer1)
|
|
|
|
|
bytes_per_row = self.hor_res * 2
|
|
|
|
|
if bytes_per_row <= 0:
|
|
|
|
|
self.render_next = True
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
rows_per_chunk = buffer_len // bytes_per_row
|
|
|
|
|
if rows_per_chunk <= 0:
|
|
|
|
|
self.render_next = True
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if rows_per_chunk >= self.ver_res:
|
|
|
|
|
self.chunk_rows_per = self.ver_res
|
|
|
|
|
self.chunk_index = 0
|
|
|
|
|
self.chunk_total = 1
|
|
|
|
|
else:
|
|
|
|
|
self.chunk_rows_per = rows_per_chunk
|
|
|
|
|
self.chunk_index = 0
|
|
|
|
|
self.chunk_total = (self.ver_res + rows_per_chunk - 1) // rows_per_chunk
|
|
|
|
|
|
|
|
|
|
self.chunk_in_progress = True
|
|
|
|
|
self.chunk_waiting = False
|
|
|
|
|
self.flush_ready = False
|
|
|
|
|
self._render_and_send_chunk()
|
|
|
|
|
|
|
|
|
|
def _render_and_send_chunk(self):
|
|
|
|
|
if not self.chunk_in_progress:
|
|
|
|
|
return
|
|
|
|
|
if self.chunk_waiting:
|
|
|
|
|
return
|
|
|
|
|
if self.chunk_index >= self.chunk_total:
|
|
|
|
|
self.chunk_in_progress = False
|
|
|
|
|
self.render_next = True
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
y_offset = self.chunk_index * self.chunk_rows_per
|
|
|
|
|
rows = min(self.chunk_rows_per, self.ver_res - y_offset)
|
|
|
|
|
advance = (self.chunk_index == 0)
|
|
|
|
|
is_last = (self.chunk_index + 1) == self.chunk_total
|
|
|
|
|
|
|
|
|
|
self.chunk_waiting = True
|
|
|
|
|
breakout.render(y_offset, rows, advance)
|
|
|
|
|
self.send_to_display(y_offset, rows, is_last)
|
2026-03-09 12:39:19 +01:00
|
|
|
|
2026-03-11 14:00:32 +01:00
|
|
|
def send_to_display(self, y_offset=0, rows=None, is_last=True):
|
|
|
|
|
x1 = 0
|
|
|
|
|
x2 = mpos.ui.main_display.get_horizontal_resolution() - 1
|
|
|
|
|
x2 = x2 + mpos.ui.main_display._offset_x
|
|
|
|
|
x1 = x1 + mpos.ui.main_display._offset_x
|
2026-03-11 13:49:05 +01:00
|
|
|
|
2026-03-11 14:00:32 +01:00
|
|
|
if rows is None:
|
|
|
|
|
rows = mpos.ui.main_display.get_vertical_resolution()
|
|
|
|
|
y1 = y_offset
|
|
|
|
|
y2 = y_offset + rows - 1
|
|
|
|
|
y1 = y1 + mpos.ui.main_display._offset_y
|
|
|
|
|
y2 = y2 + mpos.ui.main_display._offset_y
|
|
|
|
|
|
|
|
|
|
cmd = mpos.ui.main_display._set_memory_location(x1, y1, x2, y2)
|
|
|
|
|
bytes_needed = rows * mpos.ui.main_display.get_horizontal_resolution() * 2
|
|
|
|
|
data_view = memoryview(mpos.ui.main_display._frame_buffer1)[:bytes_needed]
|
|
|
|
|
|
|
|
|
|
tx_last = True
|
|
|
|
|
mpos.ui.main_display._data_bus.tx_color(
|
|
|
|
|
cmd,
|
|
|
|
|
data_view,
|
|
|
|
|
x1, y1, x2, y2,
|
|
|
|
|
mpos.ui.main_display._rotation,
|
|
|
|
|
tx_last,
|
|
|
|
|
)
|
2026-03-11 13:49:05 +01:00
|
|
|
|
2026-03-09 12:39:19 +01:00
|
|
|
def touch_cb(self, event):
|
2026-03-09 19:09:34 +01:00
|
|
|
event_code = event.get_code()
|
|
|
|
|
if event_code == lv.EVENT.PRESSED:
|
|
|
|
|
x, y = InputManager.pointer_xy()
|
|
|
|
|
self.touch_active = True
|
|
|
|
|
self.touch_last_x = x
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if event_code == lv.EVENT.PRESSING:
|
|
|
|
|
if not self.touch_active:
|
2026-03-09 12:39:19 +01:00
|
|
|
x, y = InputManager.pointer_xy()
|
2026-03-09 19:09:34 +01:00
|
|
|
self.touch_active = True
|
|
|
|
|
self.touch_last_x = x
|
2026-03-09 12:39:19 +01:00
|
|
|
return
|
2026-03-09 19:09:34 +01:00
|
|
|
x, y = InputManager.pointer_xy()
|
|
|
|
|
if self.touch_last_x is not None:
|
|
|
|
|
delta = x - self.touch_last_x
|
|
|
|
|
if delta:
|
2026-03-11 14:06:49 +01:00
|
|
|
breakout.move_paddle(round(delta*1.3)) # amplify movement to avoid sides
|
2026-03-09 19:09:34 +01:00
|
|
|
self.touch_last_x = x
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if event_code == lv.EVENT.RELEASED:
|
|
|
|
|
self.touch_active = False
|
|
|
|
|
self.touch_last_x = None
|
|
|
|
|
return
|
2026-03-09 21:08:36 +01:00
|
|
|
|
|
|
|
|
average_samples = 20
|
|
|
|
|
fps_buffer = [0.0] * average_samples
|
|
|
|
|
fps_index = 0
|
|
|
|
|
fps_sum = 0.0
|
|
|
|
|
fps_count = 0 # Number of valid samples (0 to average_samples)
|
|
|
|
|
def moving_average(self, value):
|
|
|
|
|
if self.fps_count == self.average_samples:
|
|
|
|
|
self.fps_sum -= self.fps_buffer[self.fps_index]
|
|
|
|
|
else:
|
|
|
|
|
self.fps_count += 1
|
|
|
|
|
self.fps_sum += value
|
|
|
|
|
self.fps_buffer[self.fps_index] = value
|
|
|
|
|
self.fps_index = (self.fps_index + 1) % self.average_samples
|
|
|
|
|
return self.fps_sum / self.fps_count
|
|
|
|
|
|
|
|
|
|
# Custom log callback to capture FPS
|
|
|
|
|
def log_callback(self, level, log_str):
|
|
|
|
|
log_str = log_str.decode() if isinstance(log_str, bytes) else log_str
|
|
|
|
|
if "sysmon:" in log_str and "FPS" in log_str:
|
|
|
|
|
try:
|
|
|
|
|
fps_part = log_str.split("FPS")[0].split("sysmon:")[1].strip()
|
|
|
|
|
self.last_fps = int(fps_part)
|
|
|
|
|
self.average_fps = self.moving_average(self.last_fps)
|
|
|
|
|
print(f"Current FPS: {self.last_fps} - Average FPS: {self.average_fps}")
|
|
|
|
|
except (IndexError, ValueError):
|
|
|
|
|
pass
|