You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
368 lines
12 KiB
C
368 lines
12 KiB
C
// Breakout native module renderer. Draws into a framebuffer that may be
|
|
// smaller than the full display (partial framebuffer). Rendering is done
|
|
// per-slice using a y-offset/row count so MicroPythonOS can refresh displays
|
|
// larger than 320x230 without allocating a full-size framebuffer. This keeps
|
|
// the simulation state global while allowing sequential chunk flushes.
|
|
|
|
// 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) {
|
|
unsigned char *p = (unsigned char *)s;
|
|
while (n--) {
|
|
*p++ = (unsigned char)c;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// Global BSS (non-static) state is required for native modules.
|
|
uint16_t *g_framebuffer;
|
|
size_t g_framebuffer_len;
|
|
size_t g_framebuffer_width;
|
|
size_t g_framebuffer_height;
|
|
size_t g_framebuffer_max_pixels;
|
|
size_t g_render_y_offset;
|
|
size_t g_render_height;
|
|
|
|
int g_paddle_x;
|
|
int g_paddle_width;
|
|
int g_paddle_height;
|
|
float g_ball_x;
|
|
float g_ball_y;
|
|
float g_ball_vx;
|
|
float g_ball_vy;
|
|
uint32_t g_last_tick_ms;
|
|
|
|
uint32_t g_fps_last_ms;
|
|
uint32_t g_fps_frames;
|
|
|
|
#define BRICK_ROWS 12
|
|
#define BRICK_COLS 8
|
|
uint8_t g_bricks[BRICK_ROWS][BRICK_COLS];
|
|
|
|
static uint32_t ticks_ms(void) {
|
|
mp_obj_t time_mod = mp_import_name(MP_QSTR_time, mp_const_none, MP_OBJ_NEW_SMALL_INT(0));
|
|
mp_obj_t ticks_fun = mp_load_attr(time_mod, MP_QSTR_ticks_ms);
|
|
mp_obj_t ticks_val = mp_call_function_n_kw(ticks_fun, 0, 0, NULL);
|
|
return (uint32_t)mp_obj_get_int(ticks_val);
|
|
}
|
|
|
|
static inline int clamp_int(int value, int min_value, int max_value) {
|
|
if (value < min_value) {
|
|
return min_value;
|
|
}
|
|
if (value > max_value) {
|
|
return max_value;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
static inline size_t framebuffer_max_pixels(void) {
|
|
return g_framebuffer_max_pixels;
|
|
}
|
|
|
|
static void draw_pixel(int x, int y, uint16_t color) {
|
|
if (x < 0 || y < 0) {
|
|
return;
|
|
}
|
|
if ((size_t)x >= g_framebuffer_width || (size_t)y >= g_framebuffer_height) {
|
|
return;
|
|
}
|
|
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;
|
|
}
|
|
g_framebuffer[idx] = color;
|
|
}
|
|
|
|
static void draw_rect(int x, int y, int w, int h, uint16_t color) {
|
|
if (w <= 0 || h <= 0 || g_framebuffer == NULL) {
|
|
return;
|
|
}
|
|
|
|
const int x0 = (x < 0) ? 0 : x;
|
|
const int y0 = (y < 0) ? 0 : y;
|
|
const int x1 = x + w;
|
|
const int y1 = y + h;
|
|
|
|
const int max_x = (int)g_framebuffer_width;
|
|
const int max_y = (int)g_framebuffer_height;
|
|
|
|
int clip_x0 = x0;
|
|
int clip_y0 = y0;
|
|
int clip_x1 = (x1 > max_x) ? max_x : x1;
|
|
int clip_y1 = (y1 > max_y) ? max_y : y1;
|
|
|
|
const int slice_y0 = (int)g_render_y_offset;
|
|
const int slice_y1 = (int)(g_render_y_offset + g_render_height);
|
|
if (clip_y0 < slice_y0) {
|
|
clip_y0 = slice_y0;
|
|
}
|
|
if (clip_y1 > slice_y1) {
|
|
clip_y1 = slice_y1;
|
|
}
|
|
|
|
if (clip_x0 >= clip_x1 || clip_y0 >= clip_y1) {
|
|
return;
|
|
}
|
|
|
|
const size_t width = g_framebuffer_width;
|
|
const size_t fill_width = (size_t)(clip_x1 - clip_x0);
|
|
|
|
for (int yy = clip_y0; yy < clip_y1; yy++) {
|
|
const size_t local_y = (size_t)(yy - (int)g_render_y_offset);
|
|
uint16_t *row = g_framebuffer + local_y * width + (size_t)clip_x0;
|
|
for (size_t xx = 0; xx < fill_width; xx++) {
|
|
row[xx] = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void reset_ball(void) {
|
|
g_ball_x = (float)((int)g_framebuffer_width / 2);
|
|
g_ball_y = (float)((int)g_framebuffer_height / 2);
|
|
g_ball_vx = 120.0f;
|
|
g_ball_vy = -120.0f;
|
|
}
|
|
|
|
static void reset_bricks(void) {
|
|
for (int row = 0; row < BRICK_ROWS; row++) {
|
|
for (int col = 0; col < BRICK_COLS; col++) {
|
|
g_bricks[row][col] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// init(framebuffer, width, height): store a reference to the framebuffer and dimensions.
|
|
static mp_obj_t init(mp_obj_t framebuffer_obj, mp_obj_t width_obj, mp_obj_t height_obj) {
|
|
mp_buffer_info_t bufinfo;
|
|
mp_get_buffer_raise(framebuffer_obj, &bufinfo, MP_BUFFER_WRITE);
|
|
|
|
g_framebuffer = (uint16_t *)bufinfo.buf;
|
|
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);
|
|
const size_t max_pixels = g_framebuffer_len / sizeof(uint16_t);
|
|
const size_t total_pixels = g_framebuffer_width * g_framebuffer_height;
|
|
g_framebuffer_max_pixels = (max_pixels < total_pixels) ? max_pixels : total_pixels;
|
|
g_render_y_offset = 0;
|
|
g_render_height = g_framebuffer_height;
|
|
|
|
g_paddle_width = (int)g_framebuffer_width / 5;
|
|
g_paddle_height = 4;
|
|
g_paddle_x = ((int)g_framebuffer_width - g_paddle_width) / 2;
|
|
|
|
reset_ball();
|
|
reset_bricks();
|
|
|
|
g_fps_last_ms = ticks_ms();
|
|
g_fps_frames = 0;
|
|
g_last_tick_ms = g_fps_last_ms;
|
|
|
|
return mp_const_none;
|
|
}
|
|
static MP_DEFINE_CONST_FUN_OBJ_3(init_obj, init);
|
|
|
|
// 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;
|
|
}
|
|
|
|
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 = width * render_rows;
|
|
memset(g_framebuffer, 0, fill_pixels * sizeof(uint16_t));
|
|
|
|
const int paddle_y = (int)height - g_paddle_height - 4;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
|
|
// Update ball position.
|
|
g_ball_x += g_ball_vx * dt;
|
|
g_ball_y += g_ball_vy * dt;
|
|
|
|
// 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.
|
|
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;
|
|
|
|
// Draw bricks.
|
|
if (brick_width > 0) {
|
|
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);
|
|
draw_rect(bx, by, brick_width, brick_height, 0xF800); // RGB565 red
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw paddle and ball.
|
|
draw_rect(g_paddle_x, paddle_y, g_paddle_width, g_paddle_height, 0xFFFF); // RGB565 white
|
|
draw_pixel((int)g_ball_x, (int)g_ball_y, 0xFFFF);
|
|
|
|
return mp_const_none;
|
|
}
|
|
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) {
|
|
int delta = mp_obj_get_int(delta_obj);
|
|
//mp_printf(&mp_plat_print, "delta: %d\n", delta);
|
|
if (g_framebuffer_width > 0) {
|
|
g_paddle_x = clamp_int(g_paddle_x + delta, 0, (int)g_framebuffer_width - g_paddle_width);
|
|
}
|
|
return mp_const_none;
|
|
}
|
|
static MP_DEFINE_CONST_FUN_OBJ_1(move_paddle_obj, move_paddle);
|
|
|
|
// This is the entry point and is called when the module is imported
|
|
mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
|
|
// This must be first, it sets up the globals dict and other things
|
|
MP_DYNRUNTIME_INIT_ENTRY
|
|
|
|
// Make the function available in the module's namespace
|
|
mp_store_global(MP_QSTR_init, MP_OBJ_FROM_PTR(&init_obj));
|
|
mp_store_global(MP_QSTR_render, MP_OBJ_FROM_PTR(&render_obj));
|
|
mp_store_global(MP_QSTR_move_paddle, MP_OBJ_FROM_PTR(&move_paddle_obj));
|
|
|
|
// This must be last, it restores the globals dict
|
|
MP_DYNRUNTIME_INIT_EXIT
|
|
}
|