Breakout: support partial buffer rendering

This commit is contained in:
Thomas Farstrike
2026-03-11 13:17:50 +01:00
parent 983d54f271
commit c673414889
2 changed files with 215 additions and 161 deletions
+135 -73
View File
@@ -1,5 +1,6 @@
// Include the header file to get access to the MicroPython API
#include "py/dynruntime.h"
#include <stdbool.h>
// Provide a local memset for xtensawin native modules (libc isn't linked).
void *memset(void *s, int c, size_t n) {
@@ -15,6 +16,8 @@ uint16_t *g_framebuffer;
size_t g_framebuffer_len;
size_t g_framebuffer_width;
size_t g_framebuffer_height;
size_t g_render_y_offset;
size_t g_render_height;
int g_paddle_x;
int g_paddle_width;
@@ -62,7 +65,11 @@ static void draw_pixel(int x, int y, uint16_t color) {
if ((size_t)x >= g_framebuffer_width || (size_t)y >= g_framebuffer_height) {
return;
}
const size_t idx = (size_t)y * g_framebuffer_width + (size_t)x;
if ((size_t)y < g_render_y_offset || (size_t)y >= (g_render_y_offset + g_render_height)) {
return;
}
const size_t local_y = (size_t)y - g_render_y_offset;
const size_t idx = local_y * g_framebuffer_width + (size_t)x;
const size_t max_pixels = framebuffer_max_pixels();
if (idx >= max_pixels) {
return;
@@ -105,6 +112,8 @@ static mp_obj_t init(mp_obj_t framebuffer_obj, mp_obj_t width_obj, mp_obj_t heig
g_framebuffer_len = bufinfo.len;
g_framebuffer_width = (size_t)mp_obj_get_int(width_obj);
g_framebuffer_height = (size_t)mp_obj_get_int(height_obj);
g_render_y_offset = 0;
g_render_height = g_framebuffer_height;
g_paddle_width = (int)g_framebuffer_width / 5;
g_paddle_height = 4;
@@ -121,8 +130,8 @@ static mp_obj_t init(mp_obj_t framebuffer_obj, mp_obj_t width_obj, mp_obj_t heig
}
static MP_DEFINE_CONST_FUN_OBJ_3(init_obj, init);
// render(): draw a simple Breakout-style frame and advance the game simulation.
static mp_obj_t render(void) {
// render([y_offset, rows, advance]): draw a Breakout frame slice and optionally advance simulation.
static mp_obj_t render(size_t n_args, const mp_obj_t *args) {
if (g_framebuffer == NULL || g_framebuffer_width == 0 || g_framebuffer_height == 0) {
return mp_const_none;
}
@@ -130,43 +139,135 @@ static mp_obj_t render(void) {
const size_t width = g_framebuffer_width;
const size_t height = g_framebuffer_height;
size_t render_y_offset = 0;
size_t render_rows = height;
bool advance = true;
if (n_args >= 1) {
int y_offset_arg = mp_obj_get_int(args[0]);
if (y_offset_arg > 0) {
render_y_offset = (size_t)y_offset_arg;
}
}
if (n_args >= 2) {
int rows_arg = mp_obj_get_int(args[1]);
if (rows_arg > 0) {
render_rows = (size_t)rows_arg;
}
}
if (n_args >= 3) {
advance = mp_obj_is_true(args[2]);
} else {
advance = (render_y_offset == 0);
}
if (render_y_offset >= height) {
return mp_const_none;
}
const size_t max_rows_by_buf = (width > 0) ? (framebuffer_max_pixels() / width) : 0;
const size_t max_rows_by_height = height - render_y_offset;
if (render_rows > max_rows_by_height) {
render_rows = max_rows_by_height;
}
if (max_rows_by_buf > 0 && render_rows > max_rows_by_buf) {
render_rows = max_rows_by_buf;
}
if (render_rows == 0) {
return mp_const_none;
}
g_render_y_offset = render_y_offset;
g_render_height = render_rows;
//mp_printf(&mp_plat_print, "breakout.c render y=%lu rows=%lu advance=%d\n", (unsigned long)render_y_offset, (unsigned long)render_rows, (int)advance);
// Clear to black.
const size_t fill_pixels = framebuffer_max_pixels();
const size_t fill_pixels = width * render_rows;
for (size_t i = 0; i < fill_pixels; i++) { g_framebuffer[i] = 0x0000; } // RGB565 black
g_fps_frames++;
const uint32_t now_ms = ticks_ms();
const uint32_t elapsed_ms = now_ms - g_fps_last_ms;
if (elapsed_ms >= 1000) {
const uint32_t fps = (g_fps_frames * 1000) / elapsed_ms;
mp_printf(&mp_plat_print, "breakout.c fps: %lu\n", (unsigned long)fps);
g_fps_last_ms = now_ms;
g_fps_frames = 0;
}
const int paddle_y = (int)height - g_paddle_height - 4;
uint32_t tick_delta_ms = now_ms - g_last_tick_ms;
g_last_tick_ms = now_ms;
if (tick_delta_ms > 50) {
tick_delta_ms = 50;
}
const float dt = (float)tick_delta_ms / 1000.0f;
if (advance) {
g_fps_frames++;
const uint32_t now_ms = ticks_ms();
const uint32_t elapsed_ms = now_ms - g_fps_last_ms;
if (elapsed_ms >= 1000) {
const uint32_t fps = (g_fps_frames * 1000) / elapsed_ms;
mp_printf(&mp_plat_print, "breakout.c fps: %lu\n", (unsigned long)fps);
g_fps_last_ms = now_ms;
g_fps_frames = 0;
}
// Update ball position.
g_ball_x += g_ball_vx * dt;
g_ball_y += g_ball_vy * dt;
uint32_t tick_delta_ms = now_ms - g_last_tick_ms;
g_last_tick_ms = now_ms;
if (tick_delta_ms > 50) {
tick_delta_ms = 50;
}
const float dt = (float)tick_delta_ms / 1000.0f;
// Wall collisions.
if (g_ball_x <= 0.0f) {
g_ball_x = 0.0f;
g_ball_vx = 120.0f;
} else if (g_ball_x >= (float)width - 1.0f) {
g_ball_x = (float)width - 1.0f;
g_ball_vx = -120.0f;
}
// Update ball position.
g_ball_x += g_ball_vx * dt;
g_ball_y += g_ball_vy * dt;
if (g_ball_y <= 0.0f) {
g_ball_y = 0.0f;
g_ball_vy = 120.0f;
// Wall collisions.
if (g_ball_x <= 0.0f) {
g_ball_x = 0.0f;
g_ball_vx = 120.0f;
} else if (g_ball_x >= (float)width - 1.0f) {
g_ball_x = (float)width - 1.0f;
g_ball_vx = -120.0f;
}
if (g_ball_y <= 0.0f) {
g_ball_y = 0.0f;
g_ball_vy = 120.0f;
}
// Brick collision.
const int brick_gap = 2;
const int brick_rows = BRICK_ROWS;
const int brick_cols = BRICK_COLS;
const int brick_height = 6;
const int brick_area_width = (int)width - (brick_gap * (brick_cols + 1));
const int brick_width = (brick_area_width > 0) ? (brick_area_width / brick_cols) : 0;
const int brick_offset_y = 8;
if (brick_width > 0 && g_ball_y <= (float)(brick_offset_y + brick_rows * (brick_height + brick_gap))) {
for (int row = 0; row < brick_rows; row++) {
for (int col = 0; col < brick_cols; col++) {
if (!g_bricks[row][col]) {
continue;
}
const int bx = brick_gap + col * (brick_width + brick_gap);
const int by = brick_offset_y + row * (brick_height + brick_gap);
if (g_ball_x >= (float)bx && g_ball_x < (float)(bx + brick_width) && g_ball_y >= (float)by && g_ball_y < (float)(by + brick_height)) {
g_bricks[row][col] = 0;
g_ball_vy = -g_ball_vy;
row = brick_rows;
break;
}
}
}
}
// Paddle collision.
if (g_ball_y >= (float)(paddle_y - 1) && g_ball_y <= (float)(paddle_y + g_paddle_height)) {
if (g_ball_x >= (float)g_paddle_x && g_ball_x <= (float)(g_paddle_x + g_paddle_width)) {
g_ball_y = (float)(paddle_y - 1);
g_ball_vy = -120.0f;
const int paddle_center = g_paddle_x + g_paddle_width / 2;
if (g_ball_x < (float)paddle_center) {
g_ball_vx = -120.0f;
} else if (g_ball_x > (float)paddle_center) {
g_ball_vx = 120.0f;
}
}
}
// Ball fell below paddle: reset.
if (g_ball_y >= (float)((int)height - 1)) {
reset_ball();
}
}
// Brick layout.
@@ -178,45 +279,6 @@ static mp_obj_t render(void) {
const int brick_width = (brick_area_width > 0) ? (brick_area_width / brick_cols) : 0;
const int brick_offset_y = 8;
// Brick collision.
if (brick_width > 0 && g_ball_y <= (float)(brick_offset_y + brick_rows * (brick_height + brick_gap))) {
for (int row = 0; row < brick_rows; row++) {
for (int col = 0; col < brick_cols; col++) {
if (!g_bricks[row][col]) {
continue;
}
const int bx = brick_gap + col * (brick_width + brick_gap);
const int by = brick_offset_y + row * (brick_height + brick_gap);
if (g_ball_x >= (float)bx && g_ball_x < (float)(bx + brick_width) && g_ball_y >= (float)by && g_ball_y < (float)(by + brick_height)) {
g_bricks[row][col] = 0;
g_ball_vy = -g_ball_vy;
row = brick_rows;
break;
}
}
}
}
// Paddle collision.
const int paddle_y = (int)height - g_paddle_height - 4;
if (g_ball_y >= (float)(paddle_y - 1) && g_ball_y <= (float)(paddle_y + g_paddle_height)) {
if (g_ball_x >= (float)g_paddle_x && g_ball_x <= (float)(g_paddle_x + g_paddle_width)) {
g_ball_y = (float)(paddle_y - 1);
g_ball_vy = -120.0f;
const int paddle_center = g_paddle_x + g_paddle_width / 2;
if (g_ball_x < (float)paddle_center) {
g_ball_vx = -120.0f;
} else if (g_ball_x > (float)paddle_center) {
g_ball_vx = 120.0f;
}
}
}
// Ball fell below paddle: reset.
if (g_ball_y >= (float)((int)height - 1)) {
reset_ball();
}
// Draw bricks.
if (brick_width > 0) {
for (int row = 0; row < brick_rows; row++) {
@@ -237,7 +299,7 @@ static mp_obj_t render(void) {
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_0(render_obj, render);
static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(render_obj, 0, 3, render);
// move_paddle(delta): move the paddle horizontally by delta.
static mp_obj_t move_paddle(mp_obj_t delta_obj) {
@@ -1,21 +1,5 @@
# On the 320x170 T-Display-S3:
# This gets just 7.5 FPS on actual ESP32S3 hardware
# Probably because the double buffer copies.
# With a direct buffer, it's still only 10 FPS. (and flickering buttons on black screen)
# direct framebuffer + without self.canvas.invalidate() and self.canvas.center(), it's still only 13.5 FPS (and black screen)
# AHA! with a send_to_display() it is running at 21.5 FPS but with heavy flicker
# adding a wait for the render of 10ms gives a non-flicker 17.5 FPS
# not waiting for the render but adding a callback brings the FPS to 25.5 !
# on the emulator, it gets around 8 FPS (LVGL) and 3.5 FPS (breakout.c)
# at 33ms, it's 4.5 and 2.5 FPS (mpong)
# On the 320x230 (instead of 240 because memory limitation) it gets 17 FPS (LVGL) or 12 FPS (breakout.c)
import lvgl as lv
import time
import mpos.ui
from mpos import Activity, DisplayMetrics, InputManager
@@ -40,6 +24,12 @@ class Breakout(Activity):
old_callback = None
render_next = True
flush_ready = False
chunk_in_progress = False
chunk_waiting = False
chunk_rows_per = 0
chunk_total = 0
chunk_index = 0
# Widgets:
screen = None
@@ -58,18 +48,6 @@ class Breakout(Activity):
self.paddle_move_step = round(self.hor_res/16)
self.ver_res = d.get_vertical_resolution()
'''
self.canvas = lv.canvas(self.screen)
self.canvas.set_size(self.hor_res, self.ver_res)
#self.buffer = bytearray(self.hor_res * self.ver_res * 2)
#self.canvas.set_buffer(self.buffer, self.hor_res, self.ver_res, lv.COLOR_FORMAT.NATIVE)
#self.canvas.set_buffer(mpos.ui.main_display._frame_buffer1, self.hor_res, self.ver_res, lv.COLOR_FORMAT.NATIVE)
#self.canvas.add_flag(lv.obj.FLAG.CLICKABLE)
#self.canvas.add_event_cb(self.touch_cb, lv.EVENT.ALL, None)
self.layer = lv.layer_t()
self.canvas.init_layer(self.layer)
'''
self.leftbutton = lv.button(self.screen)
self.leftbutton.align(lv.ALIGN.BOTTOM_LEFT, 0, 0)
leftlabel = lv.label(self.leftbutton)
@@ -94,20 +72,7 @@ class Breakout(Activity):
def onResume(self, screen):
lv.log_register_print_cb(self.log_callback)
#mpong.init(self.buffer, self.hor_res, self.ver_res)
#self.old_callback = mpos.ui.main_display._data_bus.
#self.refresh_timer = lv.timer_create(self.run_mpong, 5, None) # max 1000ms/60fps = 16ms/frame
#self.refresh_timer = lv.timer_create(self.run_mpong, 16, None) # max 1000ms/60fps = 16ms/frame
#self.refresh_timer = lv.timer_create(self.run_mpong, 33, None) # max 1000ms/30fps = 33ms/frame
#mpos.ui.task_handler.add_event_cb(self.run_mpong, mpos.ui.task_handler.TASK_HANDLER_STARTED)
#mpos.ui.task_handler.add_event_cb(self.run_mpong, mpos.ui.task_handler.TASK_HANDLER_FINISHED) # just 18 FPS
#mpos.ui.main_display.delete_refr_timer() # how to enable after? also it doesnt help
#lv.timer_create(self.startit, 1000, None).set_repeat_count(1)
# 10ms is fine on real hardware but needs > 1000ms on emulator
lv.timer_create(self.startit, 5000, None).set_repeat_count(1) # this needs to be delayed, otherwise the whole thing hangs
#lv.async_call(self.startit, None)
lv.timer_create(self.startit, 4000, None).set_repeat_count(1) # this needs to be delayed, otherwise the whole thing hangs
def onPause(self, screen):
if self.refresh_timer:
@@ -117,19 +82,13 @@ class Breakout(Activity):
def startit(self, arg1=None):
print("starting it!")
breakout.init(mpos.ui.main_display._frame_buffer1, self.hor_res, min(self.ver_res, 230))
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)
self.refresh_timer = lv.timer_create(self.drawframe, 20, None) # max 1000ms/50fps = 20ms/frame
#self.refresh_timer = lv.timer_create(self.run_mpong, 33, None).set_repeat_count(1) # max 1000ms/60fps = 16ms/frame
#lv.async_call(self.run_mpong, None)
self.refresh_timer = lv.timer_create(self.drawframe, 16, None) # max 1000ms/60fps ~= 16ms/frame
def flush_ready_cb(self, arg1=None, arg2=None):
#print("cbb")
mpos.ui.main_display._disp_drv.flush_ready() # with this, it hangs, and without it, the device crashes
#print("cba")
#self.refresh_timer = lv.timer_create(self.run_mpong, 33, None).set_repeat_count(1) # max 1000ms/60fps = 16ms/frame
#lv.async_call(self.run_mpong, None)
self.render_next = True
self.flush_ready = True
def move_left(self):
breakout.move_paddle(-self.paddle_move_step)
@@ -155,67 +114,100 @@ class Breakout(Activity):
return
focused = focusgroup.get_focused()
if focused:
#print(f"got focus button: {focused}")
#label = focused.get_child(0)
#print(f"got label for button: {label.get_text()}")
#focused.remove_state(lv.STATE.FOCUSED) # this doesn't seem to work to remove focus
#print("checking which button is focused")
if focused == self.rightbutton:
#print("next is focused")
focusgroup.focus_prev()
elif focused == self.leftbutton:
#print("prev is focused")
focusgroup.focus_next()
else:
print("focus isn't on next or previous, leaving it...")
def send_to_display(self):
# full-screen area
x1, y1 = 0, 0
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
y2 = mpos.ui.main_display.get_vertical_resolution() - 1
y2 = y2 + mpos.ui.main_display._offset_y
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)
data_view = mpos.ui.main_display._frame_buffer1
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,
True,
tx_last,
)
def drawframe(self, arg1=None, arg2=None):
if self.render_next == False:
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()
return
if self.chunk_in_progress or not self.render_next:
return
self.render_next = False
breakout.render()
#self.play_button.set_style_opa(lv.OPA.TRANSP, lv.PART.MAIN) # works to force refresh on desktop but not esp32
#self.screen.invalidate()
#lv.refr_now(None)
#self.canvas.invalidate() # force redraw
#self.canvas.center()
#self.canvas.refre
#self.screen.invalidate()
#self.screen.center()
#mpong.render()
'''
import lvgl as lv
area = lv.area_t()
area.x1 = 0
area.y1 = 0
area.x2 = 170
area.y2 = 170
import mpos.ui
mpos.ui.main_display._flush_cb(None, area, mpos.ui.main_display._frame_buffer1) # color_p should be pointer, not memoryview
'''
self.send_to_display()
#time.sleep_ms(10) # give it time to flush, otherwise there's heavy flicker. 5ms is fine!
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)
def touch_cb(self, event):
event_code = event.get_code()