diff --git a/c_mpos/breakout/breakout.c b/c_mpos/breakout/breakout.c index 2337ea0f..aba0d17b 100644 --- a/c_mpos/breakout/breakout.c +++ b/c_mpos/breakout/breakout.c @@ -1,5 +1,6 @@ // Include the header file to get access to the MicroPython API #include "py/dynruntime.h" +#include // 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) { diff --git a/internal_filesystem/apps/com.micropythonos.breakout/assets/breakout.py b/internal_filesystem/apps/com.micropythonos.breakout/assets/breakout.py index f546c4f1..ab979cf0 100644 --- a/internal_filesystem/apps/com.micropythonos.breakout/assets/breakout.py +++ b/internal_filesystem/apps/com.micropythonos.breakout/assets/breakout.py @@ -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()