Files
2026-03-11 18:14:42 +01:00

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
}