From 9a53c9e01d5e3d01b288cbfa9412c1f464a4e244 Mon Sep 17 00:00:00 2001 From: raffmont Date: Fri, 12 Jun 2026 23:26:47 +0200 Subject: [PATCH 1/3] Add 24x24 sprite support --- README.md | 1 + components/prg32/include/prg32.h | 3 +- components/prg32/include/prg32_abi_hash.h | 4 +- components/prg32/include/prg32_abi_index.h | 3 +- components/prg32/prg32_abi_exports.c | 1 + components/prg32/prg32_abi_table.c | 1 + components/prg32/prg32_sprite.c | 14 +- docs/abi.md | 4 +- docs/api.md | 2 +- docs/assets.md | 4 + docs/cartridge-format.md | 2 + docs/examples.md | 2 +- docs/framework_manual.md | 9 +- docs/qemu.md | 1 + docs/tutorial.md | 1 + docs/tutorial_c_game.md | 4 + docs/tutorial_graphic_game.md | 2 + examples/games/README.md | 1 + examples/games/frogger/README.md | 24 ++++ examples/games/frogger/ascii/game.S | 116 +++++++++++++++++ examples/games/frogger/c/game.c | 144 +++++++++++++++++++++ examples/games/frogger/graphics/game.S | 129 ++++++++++++++++++ main/prg32_config.h | 3 + tools/prg32_abi.json | 9 +- tools/prg32_abi_generated.py | 5 +- 25 files changed, 469 insertions(+), 20 deletions(-) create mode 100644 examples/games/frogger/README.md create mode 100644 examples/games/frogger/ascii/game.S create mode 100644 examples/games/frogger/c/game.c create mode 100644 examples/games/frogger/graphics/game.S diff --git a/README.md b/README.md index 271d2be..0e8f6a0 100644 --- a/README.md +++ b/README.md @@ -525,6 +525,7 @@ C versions of: - `platformer` - `raycaster` - `wing_commander` +- `frogger` See [examples/games/README.md](examples/games/README.md) for step-by-step instructions to run each game embedded in firmware or as an uploadable diff --git a/components/prg32/include/prg32.h b/components/prg32/include/prg32.h index 83d96ac..11aa4cb 100644 --- a/components/prg32/include/prg32.h +++ b/components/prg32/include/prg32.h @@ -84,7 +84,7 @@ extern "C" { #define PRG32_CART_MAGIC "PRG2" #define PRG32_CART_ABI_MAJOR 1 -#define PRG32_CART_ABI_MINOR 0 +#define PRG32_CART_ABI_MINOR 1 #define PRG32_CART_FLAG_AUDIO_BLOCK (1u << 0) #define PRG32_CART_FLAG_MULTIPLAYER (1u << 1) #define PRG32_CART_FLAG_ABI_TABLE (1u << 2) @@ -428,6 +428,7 @@ void prg32_platform_camera_follow(const prg32_platform_actor_t *actor, int prg32_sprite_hitbox(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh); void prg32_sprite_draw_8x8(int x, int y, const uint8_t *bits, uint16_t fg, uint16_t bg); void prg32_sprite_draw_16x16(int x, int y, const uint16_t *rgb565); +void prg32_sprite_draw_24x24(int x, int y, const uint16_t *rgb565); uint32_t prg32_sprite_anim_frame(uint32_t now_ms, uint32_t frame_count, uint32_t frame_ms); diff --git a/components/prg32/include/prg32_abi_hash.h b/components/prg32/include/prg32_abi_hash.h index acd5864..0ac198f 100644 --- a/components/prg32/include/prg32_abi_hash.h +++ b/components/prg32/include/prg32_abi_hash.h @@ -3,5 +3,5 @@ /* Generated by tools/prg32_abi_gen.py; do not edit manually. */ #define PRG32_ABI_MAJOR 1u -#define PRG32_ABI_MINOR 0u -#define PRG32_ABI_HASH 0xb9cadd82u +#define PRG32_ABI_MINOR 1u +#define PRG32_ABI_HASH 0xafee0856u diff --git a/components/prg32/include/prg32_abi_index.h b/components/prg32/include/prg32_abi_index.h index f278b40..c151672 100644 --- a/components/prg32/include/prg32_abi_index.h +++ b/components/prg32/include/prg32_abi_index.h @@ -116,5 +116,6 @@ enum { PRG32_ABI_FN_PRG32_SPRITE_ANIM_UPDATE = 110, PRG32_ABI_FN_PRG32_SPRITE_ANIM_DRAW = 111, PRG32_ABI_FN_PRG32_SCORE_SUBMIT = 112, - PRG32_ABI_FN_COUNT = 113 + PRG32_ABI_FN_PRG32_SPRITE_DRAW_24X24 = 113, + PRG32_ABI_FN_COUNT = 114 }; diff --git a/components/prg32/prg32_abi_exports.c b/components/prg32/prg32_abi_exports.c index 989f59a..24491e3 100644 --- a/components/prg32/prg32_abi_exports.c +++ b/components/prg32/prg32_abi_exports.c @@ -142,6 +142,7 @@ static const prg32_any_fn_t g_prg32_cart_abi_exports[] = { (prg32_any_fn_t)prg32_sprite_hitbox, (prg32_any_fn_t)prg32_sprite_draw_8x8, (prg32_any_fn_t)prg32_sprite_draw_16x16, + (prg32_any_fn_t)prg32_sprite_draw_24x24, (prg32_any_fn_t)prg32_sprite_anim_frame, (prg32_any_fn_t)prg32_sprite_draw_frame, (prg32_any_fn_t)prg32_sprite_anim_init, diff --git a/components/prg32/prg32_abi_table.c b/components/prg32/prg32_abi_table.c index a878371..8cad10c 100644 --- a/components/prg32/prg32_abi_table.c +++ b/components/prg32/prg32_abi_table.c @@ -129,5 +129,6 @@ const prg32_abi_table_t prg32_abi_table = { [PRG32_ABI_FN_PRG32_SPRITE_ANIM_UPDATE] = (const void *)prg32_sprite_anim_update, [PRG32_ABI_FN_PRG32_SPRITE_ANIM_DRAW] = (const void *)prg32_sprite_anim_draw, [PRG32_ABI_FN_PRG32_SCORE_SUBMIT] = (const void *)prg32_score_submit, + [PRG32_ABI_FN_PRG32_SPRITE_DRAW_24X24] = (const void *)prg32_sprite_draw_24x24, }, }; diff --git a/components/prg32/prg32_sprite.c b/components/prg32/prg32_sprite.c index e993b87..b38f000 100644 --- a/components/prg32/prg32_sprite.c +++ b/components/prg32/prg32_sprite.c @@ -46,14 +46,14 @@ void prg32_sprite_draw_16x16(int x, int y, const uint16_t *rgb565) { if (!rgb565) { return; } - for (int row = 0; row < 16; ++row) { - for (int col = 0; col < 16; ++col) { - uint16_t color = rgb565[row * 16 + col]; - if (color != PRG32_COLOR_WHITE) { - prg32_gfx_pixel(x + col, y + row, color); - } - } + prg32_sprite_draw_frame(x, y, 16, 16, rgb565, 0, PRG32_COLOR_WHITE); +} + +void prg32_sprite_draw_24x24(int x, int y, const uint16_t *rgb565) { + if (!rgb565) { + return; } + prg32_sprite_draw_frame(x, y, 24, 24, rgb565, 0, PRG32_COLOR_WHITE); } uint32_t prg32_sprite_anim_frame(uint32_t now_ms, diff --git a/docs/abi.md b/docs/abi.md index 5ebd24d..2165b2a 100644 --- a/docs/abi.md +++ b/docs/abi.md @@ -46,12 +46,14 @@ keyboard, tilemap, platformer, and sprites. ## Cartridge Package ABI -The executable cartridge ABI remains `PRG2` major `1`, minor `0`. Header v2 +The executable cartridge ABI remains `PRG2` major `1`, minor `1`. Header v2 extends the original header via `header_size` with `abi_hash`, `required_features`, `optional_features`, relocation placeholders, and `import_model`. `import_model=abi-table` marks a portable cartridge; `import_model=legacy-absolute` marks the older firmware-specific path. +ABI minor `1` adds `prg32_sprite_draw_24x24` as an append-only sprite helper. + Store-ready cartridges append a backward-compatible `PRG32META` trailer after the payload. The trailer gives host tools and setup-mode clients standard blocks for `META`, `ICON`, `SCRN`, `SIGN`, and `COLO`. diff --git a/docs/api.md b/docs/api.md index 257d963..eb5019a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -118,7 +118,7 @@ Typical response fields: "firmware_version": "1.0.0", "cart_magic": "PRG32CART", "cart_abi_major": 1, - "cart_abi_minor": 0, + "cart_abi_minor": 1, "cart_abi_hash": 3117075842, "cart_abi_features": 511, "cart_load_addr": 1107296256, diff --git a/docs/assets.md b/docs/assets.md index 2e8dbfe..77ffd72 100644 --- a/docs/assets.md +++ b/docs/assets.md @@ -16,6 +16,10 @@ python3 tools/prg32_image_convert.py player.png \ --out build/player_sprite.c ``` +For a 24x24 multicolor sprite, use `--width 24 --height 24` and draw the +generated RGB565 array with `prg32_sprite_draw_24x24(x, y, sprite)`. +White pixels (`0xffff`) are transparent in the fixed-size sprite helpers. + Convert an animated GIF to assembly frames: ```bash diff --git a/docs/cartridge-format.md b/docs/cartridge-format.md index 7a03965..06b5de2 100644 --- a/docs/cartridge-format.md +++ b/docs/cartridge-format.md @@ -19,6 +19,8 @@ The `.prg32` header starts with magic `PRG2` and stores: - code payload CRC32 - cartridge name +Current executable cartridge ABI version: major `1`, minor `1`. + `PRG32_CART_FLAG_AUDIO_BLOCK` marks a cartridge that has a trailing AUDIO block. `PRG32_CART_FLAG_MULTIPLAYER` marks a cartridge that intentionally uses the multiplayer service. The game still calls `prg32_multiplayer_join()` at runtime diff --git a/docs/examples.md b/docs/examples.md index f83984c..cb8a1fa 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -31,7 +31,7 @@ examples/games// Use these for Computing Architecture and C Programming classes. -The platformer, raycaster, and wing commander C examples are the fuller +The platformer, raycaster, wing commander, and frogger C examples are the fuller playable companions to the DeviceDemo cartridge pages. The assembly versions remain compact so students can trace registers, stack frames, and ABI calls without losing the main idea. diff --git a/docs/framework_manual.md b/docs/framework_manual.md index a823ea1..bdebd03 100644 --- a/docs/framework_manual.md +++ b/docs/framework_manual.md @@ -450,13 +450,16 @@ Useful calls: - `prg32_sprite_draw_8x8(x, y, bits, fg, bg)`: draw a monochrome sprite. - `prg32_sprite_draw_16x16(x, y, rgb565)`: draw a 16x16 RGB565 sprite. +- `prg32_sprite_draw_24x24(x, y, rgb565)`: draw a 24x24 RGB565 sprite. - `prg32_sprite_hitbox(...)`: test two axis-aligned rectangles. - `prg32_sprite_anim_frame(now_ms, frame_count, frame_ms)`: compute a frame. - `prg32_sprite_draw_frame(...)`: draw one frame from a sprite sheet. -`prg32_sprite_draw_frame` accepts width, height, a pointer to contiguous RGB565 -frames, the frame index, and a transparent color. This keeps animated sprites -usable from assembly without requiring a C object. +The 16x16 and 24x24 helpers treat `PRG32_COLOR_WHITE` as transparent. For other +sizes or another transparency key, `prg32_sprite_draw_frame` accepts width, +height, a pointer to contiguous RGB565 frames, the frame index, and a +transparent color. This keeps animated sprites usable from assembly without +requiring a C object. ## Audio diff --git a/docs/qemu.md b/docs/qemu.md index 09ee37f..45f0dc2 100644 --- a/docs/qemu.md +++ b/docs/qemu.md @@ -19,6 +19,7 @@ The assembly ABI does not change. A game still calls: - `prg32_gfx_text8` - `prg32_gfx_present` - `prg32_playfield_draw_dual` +- `prg32_sprite_draw_24x24` - `prg32_sprite_draw_frame` Only the display backend changes. diff --git a/docs/tutorial.md b/docs/tutorial.md index 7607963..4e5e5c9 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -180,6 +180,7 @@ assembly lessons to: renderer on RISC-V. - `examples/games/wing_commander/c/game.c` for a playable dual-playfield cockpit with starfield, enemies, shield, and score. +- `examples/games/frogger/c/game.c` for 24x24 multicolor sprites and hitboxes. Suggested order: diff --git a/docs/tutorial_c_game.md b/docs/tutorial_c_game.md index c652073..f393965 100644 --- a/docs/tutorial_c_game.md +++ b/docs/tutorial_c_game.md @@ -184,6 +184,10 @@ Use `examples/games/wing_commander/c/game.c` when the course reaches layered rendering. It keeps the starfield and cockpit in separate playfields, then adds enemies, laser input, score, and shield state in C. +Use `examples/games/frogger/c/game.c` when the course reaches multicolor sprite +assets. It draws a 24x24 RGB565 player sprite and uses simple rectangle +hitboxes for traffic collision. + ## Break and Fix Exercise Break it: diff --git a/docs/tutorial_graphic_game.md b/docs/tutorial_graphic_game.md index 051d9d1..09a1479 100644 --- a/docs/tutorial_graphic_game.md +++ b/docs/tutorial_graphic_game.md @@ -19,6 +19,7 @@ on the physical ILI9341 display and on the QEMU virtual RGB screen. - `prg32_gfx_text8(x, y, text, fg, bg)` - `prg32_gfx_present()` - `prg32_sprite_hitbox(ax, ay, aw, ah, bx, by, bw, bh)` +- `prg32_sprite_draw_24x24(x, y, rgb565)` - `prg32_sprite_anim_frame(now_ms, frame_count, frame_ms)` - `prg32_sprite_draw_frame(x, y, w, h, frames, frame, transparent)` - `prg32_playfield_scroll(layer, x, y)` @@ -207,6 +208,7 @@ After the focused demos, try the fuller game examples: - `examples/games/raycaster/c/game.c` for the playable fixed-point raycaster. - `examples/games/wing_commander/graphics/game.S` for a dual-playfield cockpit with a scrolling starfield and fixed foreground dashboard. +- `examples/games/frogger/c/game.c` for 24x24 multicolor sprites and hitboxes. ## Break and Fix Exercise diff --git a/examples/games/README.md b/examples/games/README.md index a2a8d98..7332d30 100644 --- a/examples/games/README.md +++ b/examples/games/README.md @@ -27,6 +27,7 @@ examples/games// | `platformer` | `platformer_ascii` | `platformer_graphics` | `platformer_c` | | `raycaster` | `raycaster_ascii` | `raycaster_graphics` | `raycaster_c` | | `wing_commander` | `wing_commander_ascii` | `wing_commander_graphics` | `wing_commander_c` | +| `frogger` | `frogger_ascii` | `frogger_graphics` | `frogger_c` | Use the prefix to find the three exported symbols: diff --git a/examples/games/frogger/README.md b/examples/games/frogger/README.md new file mode 100644 index 0000000..a4b86cb --- /dev/null +++ b/examples/games/frogger/README.md @@ -0,0 +1,24 @@ +# Frogger Inspired Crossing + +This example uses 24x24 multicolor RGB565 sprites in a simple road-crossing +game. It is inspired by the arcade crossing pattern, but all names, graphics, +and rules are original classroom material. + +## Controls + +- Left and right: move one lane cell. +- Up and down: move between lanes. +- A: restart after a crash or a successful crossing. + +## Files + +- `ascii/game.S`: compact register-and-grid version for early assembly labs. +- `graphics/game.S`: assembly version that draws the 24x24 sprite helper. +- `c/game.c`: fuller sprite game with moving cars, score, and restart state. + +## Learning Goals + +- Store a 24x24 RGB565 sprite as `24 * 24` halfwords. +- Draw a multicolor sprite with `prg32_sprite_draw_24x24`. +- Use `prg32_sprite_hitbox` for rectangle collision. +- Keep movement snapped to visible lanes so game state is easy to inspect. diff --git a/examples/games/frogger/ascii/game.S b/examples/games/frogger/ascii/game.S new file mode 100644 index 0000000..d5bf5d0 --- /dev/null +++ b/examples/games/frogger/ascii/game.S @@ -0,0 +1,116 @@ +/* + * FROGGER ASCII for PRG32. + * + * A tiny grid-crossing exercise. It keeps the state readable for register + * tracing before students move to the sprite versions. + */ +.option norelax + +.section .data +frogger_ascii_x: .word 19 +frogger_ascii_y: .word 10 +frogger_ascii_car: .word 0 +frogger_ascii_title: .asciz "FROGGER ASCII" +frogger_ascii_win: .asciz "SAFE" + +.section .text +.global frogger_ascii_init +.global frogger_ascii_update +.global frogger_ascii_draw + +frogger_ascii_init: + la t0, frogger_ascii_x + li t1, 19 + sw t1, 0(t0) + la t0, frogger_ascii_y + li t1, 10 + sw t1, 0(t0) + la t0, frogger_ascii_car + sw zero, 0(t0) + ret + +frogger_ascii_update: + addi sp, sp, -16 + sw ra, 12(sp) + + call prg32_input_read + mv t2, a0 + + la t0, frogger_ascii_x + lw t1, 0(t0) + andi t3, t2, 1 + beqz t3, .Lfrogger_ascii_no_left + addi t1, t1, -1 +.Lfrogger_ascii_no_left: + andi t3, t2, 2 + beqz t3, .Lfrogger_ascii_no_right + addi t1, t1, 1 +.Lfrogger_ascii_no_right: + bltz t1, .Lfrogger_ascii_x_low + li t3, 39 + bgt t1, t3, .Lfrogger_ascii_x_high + j .Lfrogger_ascii_store_x +.Lfrogger_ascii_x_low: + li t1, 0 + j .Lfrogger_ascii_store_x +.Lfrogger_ascii_x_high: + li t1, 39 +.Lfrogger_ascii_store_x: + sw t1, 0(t0) + + la t0, frogger_ascii_y + lw t1, 0(t0) + andi t3, t2, 4 + beqz t3, .Lfrogger_ascii_no_up + addi t1, t1, -1 +.Lfrogger_ascii_no_up: + andi t3, t2, 8 + beqz t3, .Lfrogger_ascii_no_down + addi t1, t1, 1 +.Lfrogger_ascii_no_down: + bltz t1, .Lfrogger_ascii_y_low + li t3, 23 + bgt t1, t3, .Lfrogger_ascii_y_high + j .Lfrogger_ascii_store_y +.Lfrogger_ascii_y_low: + li t1, 0 + j .Lfrogger_ascii_store_y +.Lfrogger_ascii_y_high: + li t1, 23 +.Lfrogger_ascii_store_y: + sw t1, 0(t0) + + la t0, frogger_ascii_car + lw t1, 0(t0) + addi t1, t1, 1 + li t2, 40 + blt t1, t2, .Lfrogger_ascii_car_ok + li t1, 0 +.Lfrogger_ascii_car_ok: + sw t1, 0(t0) + + lw ra, 12(sp) + addi sp, sp, 16 + ret + +frogger_ascii_draw: + addi sp, sp, -16 + sw ra, 12(sp) + + call prg32_console_clear + la a0, frogger_ascii_title + call prg32_console_write + li a0, 10 + call prg32_console_putc + + la t0, frogger_ascii_y + lw t1, 0(t0) + bnez t1, .Lfrogger_ascii_no_win + la a0, frogger_ascii_win + call prg32_console_write + li a0, 10 + call prg32_console_putc +.Lfrogger_ascii_no_win: + lw ra, 12(sp) + addi sp, sp, 16 + ret diff --git a/examples/games/frogger/c/game.c b/examples/games/frogger/c/game.c new file mode 100644 index 0000000..0be308e --- /dev/null +++ b/examples/games/frogger/c/game.c @@ -0,0 +1,144 @@ +#include "prg32.h" +#include + +#define FROG_W 24 +#define FROG_H 24 +#define CELL 24 +#define START_X 144 +#define START_Y 168 + +typedef struct { + int x; + int y; + int speed; + uint16_t color; +} car_t; + +static int frog_x; +static int frog_y; +static uint32_t last_input; +static uint32_t score; +static int state; +static car_t cars[4]; + +static const uint16_t frog_sprite[FROG_W * FROG_H] = { + 0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff, + 0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff, + 0xffff,0x07e0,0x07e0,0x07e0,0x0000,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x0000,0x07e0,0x07e0,0x07e0,0x07e0,0xffff, + 0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff, + 0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff, + 0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0xffe0,0xffe0,0x07e0,0x07e0,0x07e0,0x07e0,0xffe0,0xffe0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffe0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffe0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff, + 0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff, + 0xffff,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0xffff, + 0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff, + 0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff, + 0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff, + 0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff, +}; + +static void reset_frog(void) { + frog_x = START_X; + frog_y = START_Y; +} + +void frogger_c_init(void) { + reset_frog(); + score = 0; + state = 0; + last_input = 0; + cars[0] = (car_t){-40, 60, 2, PRG32_COLOR_RED}; + cars[1] = (car_t){270, 84, -3, PRG32_COLOR_YELLOW}; + cars[2] = (car_t){40, 108, 4, PRG32_COLOR_BLUE}; + cars[3] = (car_t){210, 132, -2, 0xf81f}; +} + +void frogger_c_update(void) { + uint32_t input = prg32_input_read(); + uint32_t pressed = input & ~last_input; + last_input = input; + + if (state != 0) { + if (pressed & PRG32_BTN_A) { + state = 0; + reset_frog(); + } + return; + } + + if ((pressed & PRG32_BTN_LEFT) && frog_x > 0) { + frog_x -= CELL; + } + if ((pressed & PRG32_BTN_RIGHT) && frog_x < PRG32_GAME_W - FROG_W) { + frog_x += CELL; + } + if ((pressed & PRG32_BTN_UP) && frog_y > 0) { + frog_y -= CELL; + } + if ((pressed & PRG32_BTN_DOWN) && frog_y < PRG32_GAME_H - FROG_H) { + frog_y += CELL; + } + + for (size_t i = 0; i < sizeof(cars) / sizeof(cars[0]); ++i) { + cars[i].x += cars[i].speed; + if (cars[i].speed > 0 && cars[i].x > PRG32_GAME_W) { + cars[i].x = -56; + } + if (cars[i].speed < 0 && cars[i].x < -56) { + cars[i].x = PRG32_GAME_W; + } + if (prg32_sprite_hitbox(frog_x + 4, frog_y + 4, 16, 16, + cars[i].x, cars[i].y, 48, 18)) { + state = -1; + prg32_audio_beep(120, 80); + } + } + + if (frog_y <= 24) { + state = 1; + score++; + prg32_audio_beep(880, 80); + } +} + +void frogger_c_draw(void) { + char text[24]; + + prg32_gfx_clear(0x03ef); + prg32_gfx_rect(0, 0, PRG32_GAME_W, 32, 0x05e0); + prg32_gfx_rect(0, 56, PRG32_GAME_W, 100, 0x4208); + prg32_gfx_rect(0, 176, PRG32_GAME_W, 24, 0x07e0); + + for (int y = 80; y <= 128; y += 24) { + for (int x = 0; x < PRG32_GAME_W; x += 32) { + prg32_gfx_rect(x, y, 16, 2, PRG32_COLOR_WHITE); + } + } + + for (size_t i = 0; i < sizeof(cars) / sizeof(cars[0]); ++i) { + prg32_gfx_rect(cars[i].x, cars[i].y, 48, 18, cars[i].color); + prg32_gfx_rect(cars[i].x + 6, cars[i].y + 3, 12, 4, PRG32_COLOR_WHITE); + prg32_gfx_rect(cars[i].x + 30, cars[i].y + 3, 12, 4, PRG32_COLOR_WHITE); + } + + prg32_sprite_draw_24x24(frog_x, frog_y, frog_sprite); + + snprintf(text, sizeof(text), "SCORE %lu", (unsigned long)score); + prg32_gfx_text8(8, 8, text, PRG32_COLOR_WHITE, 0x05e0); + if (state < 0) { + prg32_gfx_text8(96, 92, "CRASH - A", PRG32_COLOR_WHITE, PRG32_COLOR_RED); + } else if (state > 0) { + prg32_gfx_text8(100, 92, "SAFE - A", PRG32_COLOR_BLACK, PRG32_COLOR_YELLOW); + } +} diff --git a/examples/games/frogger/graphics/game.S b/examples/games/frogger/graphics/game.S new file mode 100644 index 0000000..3e9b503 --- /dev/null +++ b/examples/games/frogger/graphics/game.S @@ -0,0 +1,129 @@ +/* + * FROGGER GRAPHICS for PRG32. + * + * Minimal assembly demo for the 24x24 multicolor sprite helper. + */ +.option norelax + +.section .data +frogger_graphics_x: .word 148 +frogger_graphics_y: .word 168 +frogger_graphics_car_x: .word 0 +frogger_graphics_title: .asciz "24X24 SPRITE" + +.section .rodata +.balign 2 +frogger_graphics_sprite: + .rept 3 + .half 0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff + .endr + .rept 4 + .half 0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff + .endr + .rept 4 + .half 0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff + .endr + .rept 4 + .half 0xffff,0x07e0,0x07e0,0x07e0,0x0000,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x0000,0x07e0,0x07e0,0x07e0,0xffff + .endr + .rept 4 + .half 0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0x07e0,0xffff,0xffff + .endr + .rept 5 + .half 0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x07e0,0x07e0,0xffff,0xffff,0xffff,0xffff,0xffff + .endr + +.section .text +.global frogger_graphics_init +.global frogger_graphics_update +.global frogger_graphics_draw + +frogger_graphics_init: + la t0, frogger_graphics_x + li t1, 148 + sw t1, 0(t0) + la t0, frogger_graphics_y + li t1, 168 + sw t1, 0(t0) + la t0, frogger_graphics_car_x + sw zero, 0(t0) + ret + +frogger_graphics_update: + addi sp, sp, -16 + sw ra, 12(sp) + + call prg32_input_read + mv t2, a0 + la t0, frogger_graphics_x + lw t1, 0(t0) + andi t3, t2, 1 + beqz t3, .Lfrogger_graphics_no_left + addi t1, t1, -24 +.Lfrogger_graphics_no_left: + andi t3, t2, 2 + beqz t3, .Lfrogger_graphics_no_right + addi t1, t1, 24 +.Lfrogger_graphics_no_right: + sw t1, 0(t0) + + la t0, frogger_graphics_y + lw t1, 0(t0) + andi t3, t2, 4 + beqz t3, .Lfrogger_graphics_no_up + addi t1, t1, -24 +.Lfrogger_graphics_no_up: + andi t3, t2, 8 + beqz t3, .Lfrogger_graphics_no_down + addi t1, t1, 24 +.Lfrogger_graphics_no_down: + sw t1, 0(t0) + + la t0, frogger_graphics_car_x + lw t1, 0(t0) + addi t1, t1, 4 + li t2, 320 + blt t1, t2, .Lfrogger_graphics_car_ok + li t1, -48 +.Lfrogger_graphics_car_ok: + sw t1, 0(t0) + + lw ra, 12(sp) + addi sp, sp, 16 + ret + +frogger_graphics_draw: + addi sp, sp, -16 + sw ra, 12(sp) + + li a0, 0x03ef + call prg32_gfx_clear + li a0, 0 + li a1, 56 + li a2, 320 + li a3, 88 + li a4, 0x4208 + call prg32_gfx_rect + la t0, frogger_graphics_car_x + lw a0, 0(t0) + li a1, 96 + li a2, 48 + li a3, 18 + li a4, 0xf800 + call prg32_gfx_rect + la t0, frogger_graphics_x + lw a0, 0(t0) + la t0, frogger_graphics_y + lw a1, 0(t0) + la a2, frogger_graphics_sprite + call prg32_sprite_draw_24x24 + li a0, 8 + li a1, 8 + la a2, frogger_graphics_title + li a3, 0xffff + li a4, 0x03ef + call prg32_gfx_text8 + + lw ra, 12(sp) + addi sp, sp, 16 + ret diff --git a/main/prg32_config.h b/main/prg32_config.h index 5b79f71..2ca516f 100644 --- a/main/prg32_config.h +++ b/main/prg32_config.h @@ -31,6 +31,9 @@ #define PRG32_GAME_WING_ASCII 25 #define PRG32_GAME_WING_GRAPHICS 26 #define PRG32_GAME_WING_C 27 +#define PRG32_GAME_FROGGER_ASCII 28 +#define PRG32_GAME_FROGGER_GRAPHICS 29 +#define PRG32_GAME_FROGGER_C 30 /* Runtime console mode. */ #define PRG32_MODE_UART_ONLY 0 diff --git a/tools/prg32_abi.json b/tools/prg32_abi.json index c7a4a3e..11d6541 100644 --- a/tools/prg32_abi.json +++ b/tools/prg32_abi.json @@ -1,7 +1,7 @@ { "name": "prg32-cart-abi", "major": 1, - "minor": 0, + "minor": 1, "functions": [ { "name": "prg32_ticks_ms", @@ -793,6 +793,13 @@ "args": [], "feature": "core", "notes": "PRG32 cartridge ABI entry point." + }, + { + "name": "prg32_sprite_draw_24x24", + "return": "void", + "args": [], + "feature": "sprites", + "notes": "PRG32 cartridge ABI entry point." } ] } diff --git a/tools/prg32_abi_generated.py b/tools/prg32_abi_generated.py index f0a3f18..a798ab6 100644 --- a/tools/prg32_abi_generated.py +++ b/tools/prg32_abi_generated.py @@ -1,8 +1,8 @@ # Generated by tools/prg32_abi_gen.py; do not edit manually. ABI_MAJOR = 1 -ABI_MINOR = 0 -ABI_HASH = 0xb9cadd82 +ABI_MINOR = 1 +ABI_HASH = 0xafee0856 IMPORT_NAMES = [ 'prg32_ticks_ms', 'prg32_input_read', @@ -117,6 +117,7 @@ 'prg32_sprite_anim_update', 'prg32_sprite_anim_draw', 'prg32_score_submit', + 'prg32_sprite_draw_24x24', ] FEATURE_BITS = { From 0b3b843e36a120fae9598e1a766fa121723f941f Mon Sep 17 00:00:00 2001 From: raffmont Date: Sat, 13 Jun 2026 22:08:00 +0200 Subject: [PATCH 2/3] Update cartridge and sprite docs --- components/prg32/include/prg32.h | 2 +- components/prg32/prg32_abi_exports.c | 2 +- docs/api.md | 4 ++-- docs/assets.md | 11 +++++++---- docs/cartridges.md | 2 +- docs/framework_manual.md | 9 +++++++-- docs/tutorial.md | 5 ++++- docs/tutorial_c_game.md | 4 +++- docs/tutorial_graphic_game.md | 5 ++++- examples/games/README.md | 1 + examples/games/frogger/graphics/game.S | 20 ++++++++++++++++++++ main/prg32_config.h | 2 +- tools/prg32_game.py | 2 +- 13 files changed, 53 insertions(+), 16 deletions(-) diff --git a/components/prg32/include/prg32.h b/components/prg32/include/prg32.h index 11aa4cb..ad5cd87 100644 --- a/components/prg32/include/prg32.h +++ b/components/prg32/include/prg32.h @@ -103,7 +103,7 @@ extern "C" { #define PRG32_CART_ARCH_ESP32C6 "esp32c6" #define PRG32_CART_ARCH_QEMU "qemu" #define PRG32_CART_LOAD_ADDR 0x40800000u -#define PRG32_CART_MAX_SIZE (32u * 1024u) +#define PRG32_CART_MAX_SIZE (64u * 1024u) #ifndef CONFIG_PRG32_CART_RAM_KIB #define CONFIG_PRG32_CART_RAM_KIB 32 #endif diff --git a/components/prg32/prg32_abi_exports.c b/components/prg32/prg32_abi_exports.c index 24491e3..a3ac733 100644 --- a/components/prg32/prg32_abi_exports.c +++ b/components/prg32/prg32_abi_exports.c @@ -142,13 +142,13 @@ static const prg32_any_fn_t g_prg32_cart_abi_exports[] = { (prg32_any_fn_t)prg32_sprite_hitbox, (prg32_any_fn_t)prg32_sprite_draw_8x8, (prg32_any_fn_t)prg32_sprite_draw_16x16, - (prg32_any_fn_t)prg32_sprite_draw_24x24, (prg32_any_fn_t)prg32_sprite_anim_frame, (prg32_any_fn_t)prg32_sprite_draw_frame, (prg32_any_fn_t)prg32_sprite_anim_init, (prg32_any_fn_t)prg32_sprite_anim_update, (prg32_any_fn_t)prg32_sprite_anim_draw, (prg32_any_fn_t)prg32_score_submit, + (prg32_any_fn_t)prg32_sprite_draw_24x24, }; void prg32_abi_exports_keep(void) { diff --git a/docs/api.md b/docs/api.md index eb5019a..ebc4af0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -122,7 +122,7 @@ Typical response fields: "cart_abi_hash": 3117075842, "cart_abi_features": 511, "cart_load_addr": 1107296256, - "cart_max_size": 32768, + "cart_max_size": 65536, "cart_ram_size": 65536, "cart_loaded": true, "qemu": false, @@ -258,7 +258,7 @@ Success response: Expected behavior: - upload is accepted only when `PRG32_GAME_UPLOAD_ENABLE` is enabled; -- the request body must fit in the 32 KiB cartridge package limit; +- the request body must fit in the 64 KiB cartridge package limit; - invalid cartridge images return `400` with the cartridge validation error; - disabled upload support returns `403`. diff --git a/docs/assets.md b/docs/assets.md index 77ffd72..f0407f1 100644 --- a/docs/assets.md +++ b/docs/assets.md @@ -16,9 +16,12 @@ python3 tools/prg32_image_convert.py player.png \ --out build/player_sprite.c ``` -For a 24x24 multicolor sprite, use `--width 24 --height 24` and draw the -generated RGB565 array with `prg32_sprite_draw_24x24(x, y, sprite)`. -White pixels (`0xffff`) are transparent in the fixed-size sprite helpers. +For a 24x24 multicolor sprite, use `--width 24 --height 24`. The generated +array must contain `24 * 24` RGB565 halfwords and can be drawn with +`prg32_sprite_draw_24x24(x, y, sprite)`. White pixels (`0xffff`, +`PRG32_COLOR_WHITE`) are transparent in the fixed-size 16x16 and 24x24 sprite +helpers, which lets simple classroom assets keep a visible background without a +separate alpha mask. Convert an animated GIF to assembly frames: @@ -74,7 +77,7 @@ python3 -m pip install pillow mido ``` Generated arrays can be included in firmware examples or packaged into -uploadable cartridges when they fit inside the 32 KiB cartridge package limit. +uploadable cartridges when they fit inside the 64 KiB cartridge package limit. ## Cartridge AUDIO Blocks diff --git a/docs/cartridges.md b/docs/cartridges.md index 0d50a9d..66c68a0 100644 --- a/docs/cartridges.md +++ b/docs/cartridges.md @@ -427,7 +427,7 @@ This is intentionally a classroom loader, not a general dynamic linker. - Cartridges are linked for one PRG32 firmware build. - If the firmware is rebuilt, rebuild the cartridges. -- Cartridge package size is 32 KiB. +- Cartridge package size is 64 KiB. - Cartridge RAM is selected by `CONFIG_PRG32_CART_RAM_PROFILE`: physical classroom builds default to 32 KiB, while QEMU and extended builds use 64 KiB unless a custom profile is selected. diff --git a/docs/framework_manual.md b/docs/framework_manual.md index bdebd03..9ef428d 100644 --- a/docs/framework_manual.md +++ b/docs/framework_manual.md @@ -231,7 +231,7 @@ Important constants: - `PRG32_CART_META_MAGIC`: optional metadata trailer magic, `PRG32META`. - `PRG32_CART_META_ABI`: metadata JSON ABI, `prg32-metadata-1.0`. - `PRG32_CART_COLOPHON_ABI`: colophon JSON ABI, `prg32-colophon-1.0`. -- `PRG32_CART_MAX_SIZE`: maximum `.prg32` package size, currently 32 KiB. +- `PRG32_CART_MAX_SIZE`: maximum `.prg32` package size, currently 64 KiB. - `PRG32_CART_RAM_SIZE`: statically placed executable cartridge RAM window, configured by `CONFIG_PRG32_CART_RAM_PROFILE`. Physical ESP32-C6 builds default to the 32 KiB classroom profile to leave more SRAM to the resident @@ -450,7 +450,8 @@ Useful calls: - `prg32_sprite_draw_8x8(x, y, bits, fg, bg)`: draw a monochrome sprite. - `prg32_sprite_draw_16x16(x, y, rgb565)`: draw a 16x16 RGB565 sprite. -- `prg32_sprite_draw_24x24(x, y, rgb565)`: draw a 24x24 RGB565 sprite. +- `prg32_sprite_draw_24x24(x, y, rgb565)`: draw a 24x24 RGB565 sprite from + `24 * 24` contiguous halfwords. - `prg32_sprite_hitbox(...)`: test two axis-aligned rectangles. - `prg32_sprite_anim_frame(now_ms, frame_count, frame_ms)`: compute a frame. - `prg32_sprite_draw_frame(...)`: draw one frame from a sprite sheet. @@ -461,6 +462,10 @@ height, a pointer to contiguous RGB565 frames, the frame index, and a transparent color. This keeps animated sprites usable from assembly without requiring a C object. +See `examples/games/frogger/graphics/game.S` for the assembly call sequence and +`examples/games/frogger/c/game.c` for a fuller game that pairs the 24x24 sprite +with `prg32_sprite_hitbox`. + ## Audio PRG32 has two audio layers. diff --git a/docs/tutorial.md b/docs/tutorial.md index 4e5e5c9..f01e242 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -180,7 +180,10 @@ assembly lessons to: renderer on RISC-V. - `examples/games/wing_commander/c/game.c` for a playable dual-playfield cockpit with starfield, enemies, shield, and score. -- `examples/games/frogger/c/game.c` for 24x24 multicolor sprites and hitboxes. +- `examples/games/frogger/graphics/game.S` for the 24x24 multicolor sprite + helper from assembly. +- `examples/games/frogger/c/game.c` for the same sprite pattern with hitboxes + and restart state. Suggested order: diff --git a/docs/tutorial_c_game.md b/docs/tutorial_c_game.md index f393965..7735201 100644 --- a/docs/tutorial_c_game.md +++ b/docs/tutorial_c_game.md @@ -186,7 +186,9 @@ enemies, laser input, score, and shield state in C. Use `examples/games/frogger/c/game.c` when the course reaches multicolor sprite assets. It draws a 24x24 RGB565 player sprite and uses simple rectangle -hitboxes for traffic collision. +hitboxes for traffic collision. The matching +`examples/games/frogger/graphics/game.S` file shows the same +`prg32_sprite_draw_24x24` call from RISC-V assembly. ## Break and Fix Exercise diff --git a/docs/tutorial_graphic_game.md b/docs/tutorial_graphic_game.md index 09a1479..c98c9f5 100644 --- a/docs/tutorial_graphic_game.md +++ b/docs/tutorial_graphic_game.md @@ -208,7 +208,10 @@ After the focused demos, try the fuller game examples: - `examples/games/raycaster/c/game.c` for the playable fixed-point raycaster. - `examples/games/wing_commander/graphics/game.S` for a dual-playfield cockpit with a scrolling starfield and fixed foreground dashboard. -- `examples/games/frogger/c/game.c` for 24x24 multicolor sprites and hitboxes. +- `examples/games/frogger/graphics/game.S` for the assembly 24x24 multicolor + sprite helper. +- `examples/games/frogger/c/game.c` for the same 24x24 sprite pattern with + hitboxes and restart state. ## Break and Fix Exercise diff --git a/examples/games/README.md b/examples/games/README.md index 7332d30..c831494 100644 --- a/examples/games/README.md +++ b/examples/games/README.md @@ -68,6 +68,7 @@ C versions are best for: - playing the fuller versions of the DeviceDemo cartridge ideas, especially the platformer tile-engine course, the fixed-point raycaster, and the dual-playfield space cockpit +- studying 24x24 multicolor sprite assets and hitboxes with `frogger_c` The same source can be used in two ways: diff --git a/examples/games/frogger/graphics/game.S b/examples/games/frogger/graphics/game.S index 3e9b503..05eccb9 100644 --- a/examples/games/frogger/graphics/game.S +++ b/examples/games/frogger/graphics/game.S @@ -65,6 +65,16 @@ frogger_graphics_update: beqz t3, .Lfrogger_graphics_no_right addi t1, t1, 24 .Lfrogger_graphics_no_right: + blt t1, zero, .Lfrogger_graphics_min_x + li t3, 296 + bgt t1, t3, .Lfrogger_graphics_max_x + j .Lfrogger_graphics_store_x +.Lfrogger_graphics_min_x: + li t1, 0 + j .Lfrogger_graphics_store_x +.Lfrogger_graphics_max_x: + li t1, 296 +.Lfrogger_graphics_store_x: sw t1, 0(t0) la t0, frogger_graphics_y @@ -77,6 +87,16 @@ frogger_graphics_update: beqz t3, .Lfrogger_graphics_no_down addi t1, t1, 24 .Lfrogger_graphics_no_down: + blt t1, zero, .Lfrogger_graphics_min_y + li t3, 176 + bgt t1, t3, .Lfrogger_graphics_max_y + j .Lfrogger_graphics_store_y +.Lfrogger_graphics_min_y: + li t1, 0 + j .Lfrogger_graphics_store_y +.Lfrogger_graphics_max_y: + li t1, 176 +.Lfrogger_graphics_store_y: sw t1, 0(t0) la t0, frogger_graphics_car_x diff --git a/main/prg32_config.h b/main/prg32_config.h index 2ca516f..733c61d 100644 --- a/main/prg32_config.h +++ b/main/prg32_config.h @@ -83,7 +83,7 @@ #define PRG32_PIN_LCD_RST 0 #define PRG32_PIN_LCD_BL 5 -#define PRG32_LCD_SPI_CLOCK_HZ 40000000 +#define PRG32_LCD_SPI_CLOCK_HZ 32000000 #define PRG32_LCD_BACKLIGHT_ACTIVE_LEVEL 1 #define PRG32_LCD_BOOT_TEST_MS 0 #define PRG32_LCD_SOFT_SPI 0 diff --git a/tools/prg32_game.py b/tools/prg32_game.py index b4b0e85..af19cf2 100755 --- a/tools/prg32_game.py +++ b/tools/prg32_game.py @@ -57,7 +57,7 @@ DEFAULT_PARTITION_TABLE = ROOT / "partitions_prg32.csv" DEFAULT_CART_SLOT = "cart0" FALLBACK_CART_RAM_SIZE = 64 * 1024 -FALLBACK_CART_MAX_SIZE = 32 * 1024 +FALLBACK_CART_MAX_SIZE = 64 * 1024 FALLBACK_CART_LOAD_ADDR = 0x40800000 STORE_DISCOVERY_ABI = "prg32-store-discovery-1.0" STORE_METADATA_ABI = "prg32-metadata-1.0" From efe2767445d2cfc9a1f462b4fe49bf970f43fa84 Mon Sep 17 00:00:00 2001 From: raffmont Date: Sat, 13 Jun 2026 22:46:45 +0200 Subject: [PATCH 3/3] Add cartridge scoreboard helpers --- components/prg32/include/prg32.h | 7 + components/prg32/include/prg32_abi_hash.h | 4 +- components/prg32/include/prg32_abi_index.h | 9 +- components/prg32/prg32_abi_exports.c | 7 + components/prg32/prg32_abi_table.c | 7 + components/prg32/prg32_http_scores.c | 219 ++++++++++++++++++--- docs/abi.md | 7 + docs/api.md | 6 + docs/score_api.md | 34 +++- docs/tutorial.md | 8 + tools/prg32_abi.json | 51 ++++- tools/prg32_abi_generated.py | 11 +- 12 files changed, 330 insertions(+), 40 deletions(-) diff --git a/components/prg32/include/prg32.h b/components/prg32/include/prg32.h index ad5cd87..bc27c05 100644 --- a/components/prg32/include/prg32.h +++ b/components/prg32/include/prg32.h @@ -274,7 +274,14 @@ const char *prg32_wifi_current_ssid(void); int prg32_wifi_setup_requested(void); int prg32_wifi_setup_run(void); void prg32_scores_api_start(void); +int prg32_score_player_get(char *out_player, size_t max_len); +int prg32_score_player_set(const char *player); +int prg32_score_player_prompt(void); int prg32_score_submit(const char *game, const char *player, uint32_t score); +int prg32_score_submit_current_player(const char *game, uint32_t score); +int prg32_score_count(const char *game); +int prg32_score_get(const char *game, int index, prg32_score_t *out_score); +int prg32_scoreboard_show(const char *game, const char *title); int prg32_score_submit_remote(const char *base_url, const char *game, const char *player, diff --git a/components/prg32/include/prg32_abi_hash.h b/components/prg32/include/prg32_abi_hash.h index 0ac198f..153a0e9 100644 --- a/components/prg32/include/prg32_abi_hash.h +++ b/components/prg32/include/prg32_abi_hash.h @@ -3,5 +3,5 @@ /* Generated by tools/prg32_abi_gen.py; do not edit manually. */ #define PRG32_ABI_MAJOR 1u -#define PRG32_ABI_MINOR 1u -#define PRG32_ABI_HASH 0xafee0856u +#define PRG32_ABI_MINOR 2u +#define PRG32_ABI_HASH 0x23fced32u diff --git a/components/prg32/include/prg32_abi_index.h b/components/prg32/include/prg32_abi_index.h index c151672..b0cfcb5 100644 --- a/components/prg32/include/prg32_abi_index.h +++ b/components/prg32/include/prg32_abi_index.h @@ -117,5 +117,12 @@ enum { PRG32_ABI_FN_PRG32_SPRITE_ANIM_DRAW = 111, PRG32_ABI_FN_PRG32_SCORE_SUBMIT = 112, PRG32_ABI_FN_PRG32_SPRITE_DRAW_24X24 = 113, - PRG32_ABI_FN_COUNT = 114 + PRG32_ABI_FN_PRG32_SCORE_PLAYER_GET = 114, + PRG32_ABI_FN_PRG32_SCORE_PLAYER_SET = 115, + PRG32_ABI_FN_PRG32_SCORE_PLAYER_PROMPT = 116, + PRG32_ABI_FN_PRG32_SCORE_SUBMIT_CURRENT_PLAYER = 117, + PRG32_ABI_FN_PRG32_SCORE_COUNT = 118, + PRG32_ABI_FN_PRG32_SCORE_GET = 119, + PRG32_ABI_FN_PRG32_SCOREBOARD_SHOW = 120, + PRG32_ABI_FN_COUNT = 121 }; diff --git a/components/prg32/prg32_abi_exports.c b/components/prg32/prg32_abi_exports.c index a3ac733..0a16885 100644 --- a/components/prg32/prg32_abi_exports.c +++ b/components/prg32/prg32_abi_exports.c @@ -149,6 +149,13 @@ static const prg32_any_fn_t g_prg32_cart_abi_exports[] = { (prg32_any_fn_t)prg32_sprite_anim_draw, (prg32_any_fn_t)prg32_score_submit, (prg32_any_fn_t)prg32_sprite_draw_24x24, + (prg32_any_fn_t)prg32_score_player_get, + (prg32_any_fn_t)prg32_score_player_set, + (prg32_any_fn_t)prg32_score_player_prompt, + (prg32_any_fn_t)prg32_score_submit_current_player, + (prg32_any_fn_t)prg32_score_count, + (prg32_any_fn_t)prg32_score_get, + (prg32_any_fn_t)prg32_scoreboard_show, }; void prg32_abi_exports_keep(void) { diff --git a/components/prg32/prg32_abi_table.c b/components/prg32/prg32_abi_table.c index 8cad10c..74711e2 100644 --- a/components/prg32/prg32_abi_table.c +++ b/components/prg32/prg32_abi_table.c @@ -130,5 +130,12 @@ const prg32_abi_table_t prg32_abi_table = { [PRG32_ABI_FN_PRG32_SPRITE_ANIM_DRAW] = (const void *)prg32_sprite_anim_draw, [PRG32_ABI_FN_PRG32_SCORE_SUBMIT] = (const void *)prg32_score_submit, [PRG32_ABI_FN_PRG32_SPRITE_DRAW_24X24] = (const void *)prg32_sprite_draw_24x24, + [PRG32_ABI_FN_PRG32_SCORE_PLAYER_GET] = (const void *)prg32_score_player_get, + [PRG32_ABI_FN_PRG32_SCORE_PLAYER_SET] = (const void *)prg32_score_player_set, + [PRG32_ABI_FN_PRG32_SCORE_PLAYER_PROMPT] = (const void *)prg32_score_player_prompt, + [PRG32_ABI_FN_PRG32_SCORE_SUBMIT_CURRENT_PLAYER] = (const void *)prg32_score_submit_current_player, + [PRG32_ABI_FN_PRG32_SCORE_COUNT] = (const void *)prg32_score_count, + [PRG32_ABI_FN_PRG32_SCORE_GET] = (const void *)prg32_score_get, + [PRG32_ABI_FN_PRG32_SCOREBOARD_SHOW] = (const void *)prg32_scoreboard_show, }, }; diff --git a/components/prg32/prg32_http_scores.c b/components/prg32/prg32_http_scores.c index a874593..3cc169a 100644 --- a/components/prg32/prg32_http_scores.c +++ b/components/prg32/prg32_http_scores.c @@ -1,17 +1,36 @@ #include "prg32.h" #include "prg32_config.h" -#if PRG32_WIFI_ENABLE +#include +#include +#if PRG32_WIFI_ENABLE #include "cJSON.h" -#include "esp_err.h" #include "esp_http_client.h" #include "esp_http_server.h" -#include -#include +#endif + +#if __has_include("esp_err.h") +#include "esp_err.h" +#else +#define ESP_OK 0 +#define ESP_FAIL -1 +#define ESP_ERR_INVALID_ARG -2 +#endif + +#if __has_include("freertos/FreeRTOS.h") +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#else +#define pdMS_TO_TICKS(ms) (ms) +static void vTaskDelay(int ticks) { + (void)ticks; +} +#endif static prg32_score_t scores[PRG32_SCORE_MAX]; static int score_count; +static char current_player[sizeof(scores[0].player)] = "PLAYER"; static void copy_cstr(char *dst, size_t dst_size, const char *src) { if (!dst || dst_size == 0) { @@ -24,6 +43,168 @@ static void copy_cstr(char *dst, size_t dst_size, const char *src) { dst[dst_size - 1] = '\0'; } +static int score_matches_game(const prg32_score_t *record, const char *game) { + return record && (!game || !game[0] || strcmp(record->game, game) == 0); +} + +static int score_visible_index(const char *game, int visible_index) { + int seen = 0; + for (int i = 0; i < score_count; ++i) { + if (!score_matches_game(&scores[i], game)) { + continue; + } + if (seen == visible_index) { + return i; + } + seen++; + } + return -1; +} + +int prg32_score_player_get(char *out_player, size_t max_len) { + if (!out_player || max_len == 0) { + return ESP_ERR_INVALID_ARG; + } + copy_cstr(out_player, max_len, current_player); + return ESP_OK; +} + +int prg32_score_player_set(const char *player) { + if (!player || !player[0]) { + return ESP_ERR_INVALID_ARG; + } + copy_cstr(current_player, sizeof(current_player), player); + return ESP_OK; +} + +int prg32_score_player_prompt(void) { + char player[sizeof(current_player)]; + copy_cstr(player, sizeof(player), current_player); + int len = prg32_text_input(player, sizeof(player), "PLAYER NAME"); + if (len < 0) { + return len; + } + if (player[0]) { + prg32_score_player_set(player); + } + return (int)strlen(current_player); +} + +int prg32_score_submit(const char *game, const char *player, uint32_t score) { + if (!game || !game[0] || !player || !player[0]) { + return ESP_ERR_INVALID_ARG; + } + if (score_count >= PRG32_SCORE_MAX) { + score_count = PRG32_SCORE_MAX - 1; + } + memmove(&scores[1], &scores[0], sizeof(scores[0]) * score_count); + copy_cstr(scores[0].game, sizeof(scores[0].game), game); + copy_cstr(scores[0].player, sizeof(scores[0].player), player); + scores[0].score = score; + if (score_count < PRG32_SCORE_MAX) { + score_count++; + } + return ESP_OK; +} + +int prg32_score_submit_current_player(const char *game, uint32_t score) { + return prg32_score_submit(game, current_player, score); +} + +int prg32_score_count(const char *game) { + int count = 0; + for (int i = 0; i < score_count; ++i) { + if (score_matches_game(&scores[i], game)) { + count++; + } + } + return count; +} + +int prg32_score_get(const char *game, int index, prg32_score_t *out_score) { + if (!out_score || index < 0) { + return ESP_ERR_INVALID_ARG; + } + int raw = score_visible_index(game, index); + if (raw < 0) { + return ESP_FAIL; + } + *out_score = scores[raw]; + return ESP_OK; +} + +static int top_score_index(const char *game, const int *used, int used_count) { + int best = -1; + for (int i = 0; i < score_count; ++i) { + if (!score_matches_game(&scores[i], game)) { + continue; + } + int already_used = 0; + for (int j = 0; j < used_count; ++j) { + if (used[j] == i) { + already_used = 1; + break; + } + } + if (already_used) { + continue; + } + if (best < 0 || + scores[i].score > scores[best].score || + (scores[i].score == scores[best].score && i < best)) { + best = i; + } + } + return best; +} + +int prg32_scoreboard_show(const char *game, const char *title) { + int used[PRG32_SCORE_MAX]; + + prg32_input_wait_released(PRG32_BTN_A | PRG32_BTN_B | PRG32_BTN_SELECT); + while (1) { + uint32_t input = prg32_input_read_menu(); + if (input & (PRG32_BTN_A | PRG32_BTN_B | PRG32_BTN_SELECT)) { + prg32_input_wait_released(PRG32_BTN_A | PRG32_BTN_B | PRG32_BTN_SELECT); + return 0; + } + + prg32_gfx_clear(PRG32_COLOR_BLACK); + prg32_gfx_text8(8, 8, title ? title : "SCOREBOARD", PRG32_COLOR_WHITE, 0); + prg32_gfx_text8(8, + 24, + game && game[0] ? game : "ALL GAMES", + PRG32_COLOR_CYAN, + 0); + + int used_count = 0; + for (int row = 0; row < 8; ++row) { + int index = top_score_index(game, used, used_count); + if (index < 0) { + if (row == 0) { + prg32_gfx_text8(8, 64, "NO SCORES YET", PRG32_COLOR_YELLOW, 0); + } + break; + } + used[used_count++] = index; + char line[48]; + snprintf(line, + sizeof(line), + "%2d %-16s %lu", + row + 1, + scores[index].player, + (unsigned long)scores[index].score); + prg32_gfx_text8(8, 48 + row * 18, line, PRG32_COLOR_WHITE, 0); + } + + prg32_gfx_text8(8, 224, "A / B / SELECT BACK", PRG32_COLOR_CYAN, 0); + prg32_gfx_present(); + vTaskDelay(pdMS_TO_TICKS(80)); + } +} + +#if PRG32_WIFI_ENABLE + static int copy_json_string(char *dst, size_t dst_size, const char *src) { if (!dst || dst_size == 0 || !src) { return ESP_ERR_INVALID_ARG; @@ -54,23 +235,6 @@ static int copy_json_string(char *dst, size_t dst_size, const char *src) { return ESP_OK; } -int prg32_score_submit(const char *game, const char *player, uint32_t score) { - if (!game || !player) { - return ESP_ERR_INVALID_ARG; - } - if (score_count >= PRG32_SCORE_MAX) { - score_count = PRG32_SCORE_MAX - 1; - } - memmove(&scores[1], &scores[0], sizeof(scores[0]) * score_count); - copy_cstr(scores[0].game, sizeof(scores[0].game), game); - copy_cstr(scores[0].player, sizeof(scores[0].player), player); - scores[0].score = score; - if (score_count < PRG32_SCORE_MAX) { - score_count++; - } - return ESP_OK; -} - int prg32_score_submit_remote(const char *base_url, const char *game, const char *player, @@ -106,7 +270,7 @@ int prg32_score_submit_remote(const char *base_url, esp_http_client_config_t cfg = { .url = url, .method = HTTP_METHOD_POST, - .timeout_ms = 3000 + .timeout_ms = 3000, }; esp_http_client_handle_t client = esp_http_client_init(&cfg); if (!client) { @@ -226,12 +390,12 @@ void prg32_http_register_score_handlers(httpd_handle_t server) { httpd_uri_t gs = { .uri = "/api/scores", .method = HTTP_GET, - .handler = get_scores + .handler = get_scores, }; httpd_uri_t ps = { .uri = "/api/scores", .method = HTTP_POST, - .handler = post_score + .handler = post_score, }; ESP_ERROR_CHECK(httpd_register_uri_handler(server, &gs)); ESP_ERROR_CHECK(httpd_register_uri_handler(server, &ps)); @@ -242,13 +406,6 @@ void prg32_http_register_score_handlers(httpd_handle_t server) { #else -int prg32_score_submit(const char *game, const char *player, uint32_t score) { - (void)game; - (void)player; - (void)score; - return 0; -} - int prg32_score_submit_remote(const char *base_url, const char *game, const char *player, diff --git a/docs/abi.md b/docs/abi.md index 2165b2a..3a927a3 100644 --- a/docs/abi.md +++ b/docs/abi.md @@ -231,6 +231,13 @@ Setup screens and cartridge programs use the same button bitmasks: | `prg32_cart_default_slot` | return the saved default cartridge slot, or `-1` | | `prg32_cart_set_default_slot` | save a default cartridge slot, or clear with `-1` | | `prg32_cart_select_default` | load the saved default cartridge | +| `prg32_score_player_get` | copy the current scoreboard player name | +| `prg32_score_player_set` | set the current scoreboard player name | +| `prg32_score_player_prompt` | show the on-screen player-name entry UI | +| `prg32_score_submit_current_player` | submit a score for the current player | +| `prg32_score_count` | count local scoreboard records, optionally by game | +| `prg32_score_get` | copy one local scoreboard record | +| `prg32_scoreboard_show` | show the built-in local scoreboard screen | | `prg32_performance_test_run` | run the unattended multi-screen setup benchmark | | `prg32_performance_has_results` | return nonzero when onboard benchmark results are available | | `prg32_performance_summary` | copy the latest benchmark summary into a caller-provided struct | diff --git a/docs/api.md b/docs/api.md index ebc4af0..f16a2c9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -360,6 +360,10 @@ GET /api/scores Returns the board-local in-RAM scoreboard. +Games can also access the same in-RAM records directly through +`prg32_score_count` and `prg32_score_get`, or show the built-in on-device +scoreboard with `prg32_scoreboard_show`. + Example: ```bash @@ -408,6 +412,8 @@ Success response: Expected behavior: - scores are stored in RAM by the board-local API; +- each score record associates a short game identifier, player name, and + numeric score; - rebooting the board clears board-local scores; - use the external ScoreServer for persistent classroom leaderboards. diff --git a/docs/score_api.md b/docs/score_api.md index 575f1f7..4de80df 100644 --- a/docs/score_api.md +++ b/docs/score_api.md @@ -5,6 +5,10 @@ standalone [ScoreServer](https://github.com/riscv-prg32/ScoreServer). This is useful for competitions and for teaching the boundary between assembly, C, and network services. +Games can use the score feature without Wi-Fi. The local firmware keeps an +in-RAM scoreboard, includes an on-screen player-name prompt, and provides a +simple scoreboard screen that cartridges can call directly. + ## Enable Wi-Fi Edit `main/prg32_config.h`: @@ -50,7 +54,7 @@ Response: ## Assembly usage -Assembly code should normally call a C helper at the end of a game: +Assembly code can submit an explicit player name at the end of a game: ```asm la a0, game_name # const char *game @@ -61,6 +65,29 @@ call prg32_score_submit This is a clean ABI example: arguments in `a0`, `a1`, `a2`, return value in `a0`. +For a cartridge that lets the runtime remember the current player name, call +the prompt once from C or assembly glue before play starts: + +```c +prg32_score_player_prompt(); +``` + +Then submit the score with the remembered player: + +```c +prg32_score_submit_current_player("breakout", score); +``` + +To show the local scoreboard from a game: + +```c +prg32_scoreboard_show("breakout", "BREAKOUT SCORES"); +``` + +The lower-level helpers `prg32_score_player_get`, +`prg32_score_player_set`, `prg32_score_count`, and `prg32_score_get` are also +available to cartridges that want to draw their own scoreboard UI. + ## Remote classroom server Clone and run the standalone Flask + SQLite server: @@ -93,8 +120,9 @@ prg32_score_submit_remote("http://192.168.1.20:5000", ## Current implementation -The board-local implementation stores a small in-RAM scoreboard. The standalone -ScoreServer persists records in SQLite. +The board-local implementation stores a small in-RAM scoreboard. It is cleared +when the firmware restarts. The standalone ScoreServer and Cartridge Store +persist records in SQLite. The same board HTTP server also hosts the cartridge upload API when `PRG32_GAME_UPLOAD_ENABLE` is enabled. See `docs/cartridges.md`. diff --git a/docs/tutorial.md b/docs/tutorial.md index f01e242..bcba7a3 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -127,6 +127,14 @@ li a2, 1200 call prg32_score_submit ``` +Games that want the player to type a name can use the firmware UI from C glue: + +```c +prg32_score_player_prompt(); +prg32_score_submit_current_player("pong", score); +prg32_scoreboard_show("pong", "PONG SCORES"); +``` + For a classroom server, run [ScoreServer](https://github.com/riscv-prg32/ScoreServer) and call `prg32_score_submit_remote` from C glue code or a wrapper. diff --git a/tools/prg32_abi.json b/tools/prg32_abi.json index 11d6541..a8a2179 100644 --- a/tools/prg32_abi.json +++ b/tools/prg32_abi.json @@ -1,7 +1,7 @@ { "name": "prg32-cart-abi", "major": 1, - "minor": 1, + "minor": 2, "functions": [ { "name": "prg32_ticks_ms", @@ -800,6 +800,55 @@ "args": [], "feature": "sprites", "notes": "PRG32 cartridge ABI entry point." + }, + { + "name": "prg32_score_player_get", + "return": "void", + "args": [], + "feature": "core", + "notes": "PRG32 cartridge ABI entry point." + }, + { + "name": "prg32_score_player_set", + "return": "void", + "args": [], + "feature": "core", + "notes": "PRG32 cartridge ABI entry point." + }, + { + "name": "prg32_score_player_prompt", + "return": "void", + "args": [], + "feature": "core", + "notes": "PRG32 cartridge ABI entry point." + }, + { + "name": "prg32_score_submit_current_player", + "return": "void", + "args": [], + "feature": "core", + "notes": "PRG32 cartridge ABI entry point." + }, + { + "name": "prg32_score_count", + "return": "void", + "args": [], + "feature": "core", + "notes": "PRG32 cartridge ABI entry point." + }, + { + "name": "prg32_score_get", + "return": "void", + "args": [], + "feature": "core", + "notes": "PRG32 cartridge ABI entry point." + }, + { + "name": "prg32_scoreboard_show", + "return": "void", + "args": [], + "feature": "core", + "notes": "PRG32 cartridge ABI entry point." } ] } diff --git a/tools/prg32_abi_generated.py b/tools/prg32_abi_generated.py index a798ab6..f01e37c 100644 --- a/tools/prg32_abi_generated.py +++ b/tools/prg32_abi_generated.py @@ -1,8 +1,8 @@ # Generated by tools/prg32_abi_gen.py; do not edit manually. ABI_MAJOR = 1 -ABI_MINOR = 1 -ABI_HASH = 0xafee0856 +ABI_MINOR = 2 +ABI_HASH = 0x23fced32 IMPORT_NAMES = [ 'prg32_ticks_ms', 'prg32_input_read', @@ -118,6 +118,13 @@ 'prg32_sprite_anim_draw', 'prg32_score_submit', 'prg32_sprite_draw_24x24', + 'prg32_score_player_get', + 'prg32_score_player_set', + 'prg32_score_player_prompt', + 'prg32_score_submit_current_player', + 'prg32_score_count', + 'prg32_score_get', + 'prg32_scoreboard_show', ] FEATURE_BITS = {