From eeb9c0ec52db1948adf45f36cfbfe532f9188a4b Mon Sep 17 00:00:00 2001 From: shadow578 <52449218+shadow578@users.noreply.github.com> Date: Sat, 13 Apr 2024 20:17:16 +0200 Subject: [PATCH 1/6] add support for games on DWIN_MARLINUI --- Marlin/Configuration_adv.h | 12 +- Marlin/src/lcd/dogm/game.cpp | 52 +++++++++ Marlin/src/lcd/dogm/game.h | 11 ++ Marlin/src/lcd/e3v2/marlinui/game.cpp | 108 +++++++++++++++++ Marlin/src/lcd/e3v2/marlinui/game.h | 72 ++++++++++++ .../src/lcd/e3v2/marlinui/lcdprint_dwin.cpp | 4 +- Marlin/src/lcd/menu/game/brickout.cpp | 42 +++---- Marlin/src/lcd/menu/game/game.cpp | 16 +-- Marlin/src/lcd/menu/game/game.h | 3 +- Marlin/src/lcd/menu/game/invaders.cpp | 40 ++++--- Marlin/src/lcd/menu/game/snake.cpp | 35 +++--- Marlin/src/lcd/menu/game/types.h | 109 ++++++++++++++++++ 12 files changed, 432 insertions(+), 72 deletions(-) create mode 100644 Marlin/src/lcd/dogm/game.cpp create mode 100644 Marlin/src/lcd/dogm/game.h create mode 100644 Marlin/src/lcd/e3v2/marlinui/game.cpp create mode 100644 Marlin/src/lcd/e3v2/marlinui/game.h diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h index f0066338c177..81e8c4058e45 100644 --- a/Marlin/Configuration_adv.h +++ b/Marlin/Configuration_adv.h @@ -2015,17 +2015,17 @@ //#define STATUS_HEAT_PERCENT // Show heating in a progress bar //#define STATUS_HEAT_POWER // Show heater output power as a vertical bar - // Frivolous Game Options - //#define MARLIN_BRICKOUT - //#define MARLIN_INVADERS - //#define MARLIN_SNAKE - //#define GAMES_EASTER_EGG // Add extra blank lines above the "Games" sub-menu - #endif // HAS_MARLINUI_U8GLIB #if HAS_MARLINUI_U8GLIB || IS_DWIN_MARLINUI #define MENU_HOLLOW_FRAME // Enable to save many cycles by drawing a hollow frame on Menu Screens //#define OVERLAY_GFX_REVERSE // Swap the CW/CCW indicators in the graphics overlay + + // Frivolous Game Options + //#define MARLIN_BRICKOUT + //#define MARLIN_INVADERS + //#define MARLIN_SNAKE + //#define GAMES_EASTER_EGG // Add extra blank lines above the "Games" sub-menu #endif // diff --git a/Marlin/src/lcd/dogm/game.cpp b/Marlin/src/lcd/dogm/game.cpp new file mode 100644 index 000000000000..a9443a85251a --- /dev/null +++ b/Marlin/src/lcd/dogm/game.cpp @@ -0,0 +1,52 @@ +#include "../../inc/MarlinConfigPre.h" + +#if HAS_MARLINUI_U8GLIB && HAS_GAMES + +#include "../menu/game/types.h" // includes dogm/game.h + +void MarlinGame::frame_start() {} + +void MarlinGame::frame_end() {} + +void MarlinGame::set_color(const uint8_t color) { + u8g.setColorIndex(color); +} + +void MarlinGame::draw_hline(const game_dim_t x, const game_dim_t y, const game_dim_t w) { + u8g.drawHLine(x, y, w); +} + +void MarlinGame::draw_vline(const game_dim_t x, const game_dim_t y, const game_dim_t h) { + u8g.drawVLine(x, y, h); +} + +void MarlinGame::draw_frame(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { + u8g.drawFrame(x, y, w, h); +} + +void MarlinGame::draw_box(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { + u8g.drawBox(x, y, w, h); +} + +void MarlinGame::draw_pixel(const game_dim_t x, const game_dim_t y) { + u8g.drawPixel(x, y); +} + +void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_dim_t bytes_per_row, const game_dim_t rows, const pgm_bitmap_t bitmap) { + u8g.drawBitmapP(x, y, bytes_per_row, rows, bitmap); +} + +int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, const char* str) { + lcd_moveto(x, y); + return lcd_put_u8str_P(str); +} + +int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, FSTR_P const fstr) { + lcd_moveto(x, y); + return lcd_put_u8str(fstr); +} + +void MarlinGame::draw_int(const game_dim_t x, const game_dim_t y, const int value) { + lcd_put_int(x, y, value); +} +#endif // HAS_MARLINUI_U8GLIB && HAS_GAMES diff --git a/Marlin/src/lcd/dogm/game.h b/Marlin/src/lcd/dogm/game.h new file mode 100644 index 000000000000..6e16e8e0579c --- /dev/null +++ b/Marlin/src/lcd/dogm/game.h @@ -0,0 +1,11 @@ +#pragma once +#include "marlinui_DOGM.h" +#include "../lcdprint.h" + +typedef uint8_t game_dim_t; +typedef const u8g_pgm_uint8_t* pgm_bitmap_t; + +constexpr game_dim_t GAME_WIDTH = LCD_PIXEL_WIDTH; +constexpr game_dim_t GAME_HEIGHT = LCD_PIXEL_HEIGHT; +constexpr game_dim_t GAME_FONT_WIDTH = MENU_FONT_WIDTH; +constexpr game_dim_t GAME_FONT_ASCENT = MENU_FONT_ASCENT; diff --git a/Marlin/src/lcd/e3v2/marlinui/game.cpp b/Marlin/src/lcd/e3v2/marlinui/game.cpp new file mode 100644 index 000000000000..2a67d2a072a5 --- /dev/null +++ b/Marlin/src/lcd/e3v2/marlinui/game.cpp @@ -0,0 +1,108 @@ +#include "../../../inc/MarlinConfigPre.h" + +#if IS_DWIN_MARLINUI && HAS_GAMES + +#include "../../menu/game/types.h" // includes e3v2/marlinui/game.h +#include "../../lcdprint.h" +#include "lcdprint_dwin.h" +#include "marlinui_dwin.h" + +void MarlinGame::frame_start() { + // clear the screen before each frame + //dwinFrameClear(RGB(0, 0, 0)); + + // filling the play area should be faster than clearing the whole screen + const uint16_t fg = dwin_font.fg; + dwin_font.fg = dwin_game::color_to_dwin(0); + draw_box(0, 0, GAME_WIDTH, GAME_HEIGHT); + dwin_font.fg = fg; +} + +void MarlinGame::frame_end() {} + +void MarlinGame::set_color(const uint8_t color) { + dwin_font.fg = dwin_game::color_to_dwin(color); +} + +void MarlinGame::draw_hline(const game_dim_t x, const game_dim_t y, const game_dim_t w) { + // draw lines as boxes, since DWIN lines are always 1px wide but we want to scale them + draw_box(x, y, w, 1); +} + +void MarlinGame::draw_vline(const game_dim_t x, const game_dim_t y, const game_dim_t h) { + // draw lines as boxes, since DWIN lines are always 1px wide but we want to scale them + draw_box(x, y, 1, h); +} + +void MarlinGame::draw_frame(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { + dwinDrawBox( + 0, // mode = frame + dwin_font.fg, // color + dwin_game::game_to_screen(x) + dwin_game::x_offset, + dwin_game::game_to_screen(y) + dwin_game::y_offset, + dwin_game::game_to_screen(w), + dwin_game::game_to_screen(h) + ); +} + +void MarlinGame::draw_box(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { + dwinDrawBox( + 1, // mode = fill + dwin_font.fg, // color + dwin_game::game_to_screen(x) + dwin_game::x_offset, + dwin_game::game_to_screen(y) + dwin_game::y_offset, + dwin_game::game_to_screen(w), + dwin_game::game_to_screen(h) + ); +} + +void MarlinGame::draw_pixel(const game_dim_t x, const game_dim_t y) { + // draw pixels as boxes, since DWIN pixels are always 1px wide but we want to scale them + draw_box(x, y, 1, 1); +} + +void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_dim_t bytes_per_row, const game_dim_t rows, const pgm_bitmap_t bitmap) { + // DWIN theorethically supports bitmaps since kernel 2.1, but most screens don't support it + // (either because they use an older kernel version, or because they just (badly) emulate the DWIN protocol). + // So instead, we draw the bitmap as a series of pixels, effectively emulating the draw call. + // This will totally suck for performance, but it's the best we can do. + for (game_dim_t row = 0; row < rows; row++) { + for (game_dim_t col = 0; col < bytes_per_row; col++) { + const uint8_t byte = bitmap[(row * bytes_per_row) + col]; + for (uint8_t bit = 0; bit < 8; bit++) { + // assume that the screen area is cleared before drawing + if (byte & (1 << bit)) { + draw_pixel(x + (col * 8) + (7 - bit + 1), y + row); + } + } + } + } +} + +int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, const char* str) { + lcd_moveto_xy( + dwin_game::game_to_screen(x) + dwin_game::x_offset, + dwin_game::game_to_screen(y) + dwin_game::y_offset + ); + + return lcd_put_u8str_max_P( + str, + PIXEL_LEN_NOLIMIT + ); +} + +int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, FSTR_P const str) { + return draw_string(x, y, FTOP(str)); +} + +void MarlinGame::draw_int(const game_dim_t x, const game_dim_t y, const int value) { + COUNT_DRAW_CALL(0); + + lcd_moveto_xy( + dwin_game::game_to_screen(x) + dwin_game::x_offset, + dwin_game::game_to_screen(y) + dwin_game::y_offset + ); + + lcd_put_int(value); +} +#endif // IS_DWIN_MARLINUI && HAS_GAMES diff --git a/Marlin/src/lcd/e3v2/marlinui/game.h b/Marlin/src/lcd/e3v2/marlinui/game.h new file mode 100644 index 000000000000..7cfcbcfea0ea --- /dev/null +++ b/Marlin/src/lcd/e3v2/marlinui/game.h @@ -0,0 +1,72 @@ +#pragma once +#include +#include "../marlinui/marlinui_dwin.h" + +typedef uint8_t game_dim_t; +typedef uint16_t screen_dim_t; +typedef const uint8_t* pgm_bitmap_t; + +namespace dwin_game { + /** + * @brief Target the renderer at 128x64 pixels to match UG8 screens + */ + constexpr screen_dim_t TARGET_WIDTH = 128; + constexpr screen_dim_t TARGET_HEIGHT = 64; + + constexpr int calculate_scale() + { + // use whichever is smaller: the width or height scaling factor + float scaling_factor = _MIN( + static_cast(DWIN_WIDTH) / static_cast(TARGET_WIDTH), + static_cast(DWIN_HEIGHT) / static_cast(TARGET_HEIGHT) + ); + + // round DOWN to closest integer + return static_cast(scaling_factor); + } + + /** + * @brief Game render scale. + */ + constexpr int scale = calculate_scale(); + + /** + * @brief scale a game dimension to screen dimensions + */ + constexpr game_dim_t screen_to_game(const screen_dim_t x) { + return x / scale; + } + + /** + * @brief scale a screen dimension to game dimensions + */ + constexpr screen_dim_t game_to_screen(const game_dim_t x) { + return x * scale; + } + + /** + * @brief Offset of the game window on the screen. Applied after scaling. + */ + constexpr screen_dim_t x_offset = (DWIN_WIDTH - game_to_screen(TARGET_WIDTH)) / 2; + constexpr screen_dim_t y_offset = (DWIN_HEIGHT - game_to_screen(TARGET_HEIGHT)) / 2; + + static_assert(game_to_screen(TARGET_WIDTH) + (x_offset * 2) <= DWIN_WIDTH, "DWIN game renderer failed to auto-scale, is too wide"); + static_assert(game_to_screen(TARGET_HEIGHT) + (y_offset * 2) <= DWIN_HEIGHT, "DWIN game renderer failed to auto-scale, is too high"); + + /** + * @brief convert a color (from set_color) to dwin + */ + constexpr uint16_t color_to_dwin(const uint8_t color) { + return color == 0 ? RGB(0, 0, 0) : RGB(255, 255, 255); + } +} // namespace dwin_game + +constexpr game_dim_t GAME_WIDTH = dwin_game::screen_to_game(DWIN_WIDTH - (dwin_game::x_offset * 2)); +constexpr game_dim_t GAME_HEIGHT = dwin_game::screen_to_game(DWIN_HEIGHT - (dwin_game::y_offset * 2)); +constexpr game_dim_t GAME_FONT_WIDTH = dwin_game::screen_to_game(MENU_FONT_WIDTH); +constexpr game_dim_t GAME_FONT_ASCENT = dwin_game::screen_to_game(MENU_FONT_ASCENT); + +// not needed on DWIN +#define PAGE_OVER(ya) true +#define PAGE_UNDER(yb) true +#define PAGE_CONTAINS(ya, yb) true diff --git a/Marlin/src/lcd/e3v2/marlinui/lcdprint_dwin.cpp b/Marlin/src/lcd/e3v2/marlinui/lcdprint_dwin.cpp index f689a6ff698d..9fa75a700bc6 100644 --- a/Marlin/src/lcd/e3v2/marlinui/lcdprint_dwin.cpp +++ b/Marlin/src/lcd/e3v2/marlinui/lcdprint_dwin.cpp @@ -52,7 +52,9 @@ void lcd_moveto(const lcd_uint_t col, const lcd_uint_t row) { inline void lcd_advance_cursor(const uint8_t len=1) { cursor.x += len * dwin_font.width; } void lcd_put_int(const int i) { - // TODO: Draw an int at the cursor position, advance the cursor + char buf[12]; // 10 digits + sign + null + itoa(i, buf, 10); + lcd_put_u8str_max(buf, PIXEL_LEN_NOLIMIT); } int lcd_put_dwin_string() { diff --git a/Marlin/src/lcd/menu/game/brickout.cpp b/Marlin/src/lcd/menu/game/brickout.cpp index 078cbbcceee2..72ab3c041889 100644 --- a/Marlin/src/lcd/menu/game/brickout.cpp +++ b/Marlin/src/lcd/menu/game/brickout.cpp @@ -27,14 +27,14 @@ #include "game.h" #define BRICK_H 5 -#define BRICK_TOP MENU_FONT_ASCENT +#define BRICK_TOP GAME_FONT_ASCENT #define PADDLE_H 2 #define PADDLE_VEL 3 -#define PADDLE_W ((LCD_PIXEL_WIDTH) / 8) -#define PADDLE_Y (LCD_PIXEL_HEIGHT - 1 - PADDLE_H) +#define PADDLE_W ((GAME_WIDTH) / 8) +#define PADDLE_Y (GAME_HEIGHT - 1 - PADDLE_H) -#define BRICK_W ((LCD_PIXEL_WIDTH) / (BRICK_COLS)) +#define BRICK_W ((GAME_WIDTH) / (BRICK_COLS)) #define BRICK_BOT (BRICK_TOP + BRICK_H * BRICK_ROWS - 1) #define BRICK_COL(X) ((X) / (BRICK_W)) @@ -53,7 +53,7 @@ void reset_ball() { bdat.ballv = FTOF(1.3f); bdat.ballh = -FTOF(1.25f); uint8_t bx = bdat.paddle_x + (PADDLE_W) / 2 + ball_dist; - if (bx >= LCD_PIXEL_WIDTH - 10) { bx -= ball_dist * 2; bdat.ballh = -bdat.ballh; } + if (bx >= GAME_WIDTH - 10) { bx -= ball_dist * 2; bdat.ballh = -bdat.ballh; } bdat.ballx = BTOF(bx); bdat.hit_dir = -1; } @@ -61,7 +61,7 @@ void reset_ball() { void BrickoutGame::game_screen() { if (game_frame()) { // Run logic twice for finer resolution // Update Paddle Position - bdat.paddle_x = constrain(int8_t(ui.encoderPosition), 0, (LCD_PIXEL_WIDTH - (PADDLE_W)) / (PADDLE_VEL)); + bdat.paddle_x = constrain(int8_t(ui.encoderPosition), 0, (GAME_WIDTH - (PADDLE_W)) / (PADDLE_VEL)); ui.encoderPosition = bdat.paddle_x; bdat.paddle_x *= (PADDLE_VEL); @@ -70,7 +70,7 @@ void BrickoutGame::game_screen() { // Provisionally update the ball position const fixed_t newx = bdat.ballx + bdat.ballh, newy = bdat.bally + bdat.ballv; // current next position - if (!WITHIN(newx, 0, BTOF(LCD_PIXEL_WIDTH - 1))) { // out in x? + if (!WITHIN(newx, 0, BTOF(GAME_WIDTH - 1))) { // out in x? bdat.ballh = -bdat.ballh; _BUZZ(5, 220); // bounce x } if (newy < 0) { // out in y? @@ -78,7 +78,7 @@ void BrickoutGame::game_screen() { bdat.hit_dir = 1; } // Did the ball go below the bottom? - else if (newy > BTOF(LCD_PIXEL_HEIGHT)) { + else if (newy > BTOF(GAME_HEIGHT)) { _BUZZ(500, 75); if (--bdat.balls_left) reset_ball(); else game_state = 0; break; // done @@ -134,7 +134,8 @@ void BrickoutGame::game_screen() { } while (false); } - u8g.setColorIndex(1); + frame_start(); + set_color(1); // Draw bricks if (PAGE_CONTAINS(BRICK_TOP, BRICK_BOT)) { @@ -146,7 +147,7 @@ void BrickoutGame::game_screen() { const uint8_t xx = x * BRICK_W; for (uint8_t v = 0; v < BRICK_H - 1; ++v) if (PAGE_CONTAINS(yy + v, yy + v)) - u8g.drawHLine(xx, yy + v, BRICK_W - 1); + draw_hline(xx, yy + v, BRICK_W - 1); } } } @@ -155,11 +156,11 @@ void BrickoutGame::game_screen() { // Draw paddle if (PAGE_CONTAINS(PADDLE_Y-1, PADDLE_Y)) { - u8g.drawHLine(bdat.paddle_x, PADDLE_Y, PADDLE_W); + draw_hline(bdat.paddle_x, PADDLE_Y, PADDLE_W); #if PADDLE_H > 1 - u8g.drawHLine(bdat.paddle_x, PADDLE_Y-1, PADDLE_W); + draw_hline(bdat.paddle_x, PADDLE_Y-1, PADDLE_W); #if PADDLE_H > 2 - u8g.drawHLine(bdat.paddle_x, PADDLE_Y-2, PADDLE_W); + draw_hline(bdat.paddle_x, PADDLE_Y-2, PADDLE_W); #endif #endif } @@ -168,29 +169,30 @@ void BrickoutGame::game_screen() { if (game_state) { const uint8_t by = FTOB(bdat.bally); if (PAGE_CONTAINS(by, by+1)) - u8g.drawFrame(FTOB(bdat.ballx), by, 2, 2); + draw_frame(FTOB(bdat.ballx), by, 2, 2); } // Or draw GAME OVER else draw_game_over(); - if (PAGE_UNDER(MENU_FONT_ASCENT)) { + if (PAGE_UNDER(GAME_FONT_ASCENT)) { // Score Digits - //const uint8_t sx = (LCD_PIXEL_WIDTH - (score >= 10 ? score >= 100 ? score >= 1000 ? 4 : 3 : 2 : 1) * MENU_FONT_WIDTH) / 2; + //const uint8_t sx = (GAME_WIDTH - (score >= 10 ? score >= 100 ? score >= 1000 ? 4 : 3 : 2 : 1) * GAME_FONT_WIDTH) / 2; constexpr uint8_t sx = 0; - lcd_put_int(sx, MENU_FONT_ASCENT - 1, score); + draw_int(sx, GAME_FONT_ASCENT - 1, score); // Balls Left - lcd_moveto(LCD_PIXEL_WIDTH - MENU_FONT_WIDTH * 3, MENU_FONT_ASCENT - 1); PGM_P const ohs = PSTR("ooo\0\0"); - lcd_put_u8str_P(ohs + 3 - bdat.balls_left); + draw_string(GAME_WIDTH - GAME_FONT_WIDTH * 3, GAME_FONT_ASCENT - 1, ohs + 3 - bdat.balls_left); } + frame_end(); + // A click always exits this game if (ui.use_click()) exit_game(); } -#define SCREEN_M ((LCD_PIXEL_WIDTH) / 2) +#define SCREEN_M ((GAME_WIDTH) / 2) void BrickoutGame::enter_game() { init_game(2, game_screen); // 2 = reset bricks on paddle hit diff --git a/Marlin/src/lcd/menu/game/game.cpp b/Marlin/src/lcd/menu/game/game.cpp index d465b00388cf..ee2a3e6397d6 100644 --- a/Marlin/src/lcd/menu/game/game.cpp +++ b/Marlin/src/lcd/menu/game/game.cpp @@ -40,15 +40,15 @@ bool MarlinGame::game_frame() { } void MarlinGame::draw_game_over() { - constexpr int8_t gowide = (MENU_FONT_WIDTH) * 9, - gohigh = MENU_FONT_ASCENT - 3, - lx = (LCD_PIXEL_WIDTH - gowide) / 2, - ly = (LCD_PIXEL_HEIGHT + gohigh) / 2; + constexpr int8_t gowide = (GAME_FONT_WIDTH) * 9, + gohigh = GAME_FONT_ASCENT - 3, + lx = (GAME_WIDTH - gowide) / 2, + ly = (GAME_HEIGHT + gohigh) / 2; if (PAGE_CONTAINS(ly - gohigh - 1, ly + 1)) { - u8g.setColorIndex(0); - u8g.drawBox(lx - 1, ly - gohigh - 1, gowide + 2, gohigh + 2); - u8g.setColorIndex(1); - if (ui.get_blink()) lcd_put_u8str(lx, ly, F("GAME OVER")); + set_color(0); + draw_box(lx - 1, ly - gohigh - 1, gowide + 2, gohigh + 2); + set_color(1); + if (ui.get_blink()) draw_string(lx, ly, F("GAME OVER")); } } diff --git a/Marlin/src/lcd/menu/game/game.h b/Marlin/src/lcd/menu/game/game.h index ba123cb98bfe..2e9d55b1a5fc 100644 --- a/Marlin/src/lcd/menu/game/game.h +++ b/Marlin/src/lcd/menu/game/game.h @@ -22,9 +22,8 @@ #pragma once #include "../../../inc/MarlinConfigPre.h" -#include "../../dogm/marlinui_DOGM.h" -#include "../../lcdprint.h" #include "../../marlinui.h" +#include "types.h" //#define MUTE_GAMES diff --git a/Marlin/src/lcd/menu/game/invaders.cpp b/Marlin/src/lcd/menu/game/invaders.cpp index 588523854f94..54307bed73be 100644 --- a/Marlin/src/lcd/menu/game/invaders.cpp +++ b/Marlin/src/lcd/menu/game/invaders.cpp @@ -29,11 +29,11 @@ #define CANNON_W 11 #define CANNON_H 8 #define CANNON_VEL 4 -#define CANNON_Y (LCD_PIXEL_HEIGHT - 1 - CANNON_H) +#define CANNON_Y (GAME_HEIGHT - 1 - CANNON_H) #define INVADER_VEL 3 -#define INVADER_TOP MENU_FONT_ASCENT +#define INVADER_TOP GAME_FONT_ASCENT #define INVADERS_WIDE ((INVADER_COL_W) * (INVADER_COLS)) #define INVADERS_HIGH ((INVADER_ROW_H) * (INVADER_ROWS)) @@ -175,7 +175,7 @@ inline void update_invader_data() { } idat.leftmost = 0; for (uint8_t i = 0; i < INVADER_COLS; ++i) { if (TEST(inv_mask, i)) break; idat.leftmost -= INVADER_COL_W; } - idat.rightmost = LCD_PIXEL_WIDTH - (INVADERS_WIDE); + idat.rightmost = GAME_WIDTH - (INVADERS_WIDE); for (uint8_t i = INVADER_COLS; i--;) { if (TEST(inv_mask, i)) break; idat.rightmost += INVADER_COL_W; } if (idat.count == 2) idat.dir = idat.dir > 0 ? INVADER_VEL + 1 : -(INVADER_VEL + 1); } @@ -195,7 +195,7 @@ inline void reset_invaders() { inline void spawn_ufo() { idat.ufov = random(0, 2) ? 1 : -1; - idat.ufox = idat.ufov > 0 ? -(UFO_W) : LCD_PIXEL_WIDTH - 1; + idat.ufox = idat.ufov > 0 ? -(UFO_W) : GAME_WIDTH - 1; } inline void reset_player() { @@ -205,7 +205,7 @@ inline void reset_player() { inline void fire_cannon() { idat.laser.x = idat.cannon_x + CANNON_W / 2; - idat.laser.y = LCD_PIXEL_HEIGHT - CANNON_H - (LASER_H); + idat.laser.y = GAME_HEIGHT - CANNON_H - (LASER_H); idat.laser.v = -(LASER_H); } @@ -235,7 +235,7 @@ void InvadersGame::game_screen() { if (ui.first_page) { // Update Cannon Position - int16_t ep = constrain(int16_t(ui.encoderPosition), 0, (LCD_PIXEL_WIDTH - (CANNON_W)) / (CANNON_VEL)); + int16_t ep = constrain(int16_t(ui.encoderPosition), 0, (GAME_WIDTH - (CANNON_W)) / (CANNON_VEL)); ui.encoderPosition = ep; ep *= (CANNON_VEL); @@ -246,7 +246,7 @@ void InvadersGame::game_screen() { if (game_state) do { // Move the UFO, if any - if (idat.ufov) { idat.ufox += idat.ufov; if (!WITHIN(idat.ufox, -(UFO_W), LCD_PIXEL_WIDTH - 1)) idat.ufov = 0; } + if (idat.ufov) { idat.ufox += idat.ufov; if (!WITHIN(idat.ufox, -(UFO_W), GAME_WIDTH - 1)) idat.ufov = 0; } if (game_state > 1) { if (--game_state == 2) { reset_invaders(); } else if (game_state == 100) { game_state = 1; } break; } @@ -326,7 +326,7 @@ void InvadersGame::game_screen() { if (b->v) { // Update alien bullet position b->y += b->v; - if (b->y >= LCD_PIXEL_HEIGHT) + if (b->y >= GAME_HEIGHT) b->v = 0; // Offscreen else if (b->y >= CANNON_Y && WITHIN(b->x, idat.cannon_x, idat.cannon_x + CANNON_W - 1)) kill_cannon(game_state, 120); // Hit the cannon @@ -365,7 +365,8 @@ void InvadersGame::game_screen() { if (!idat.quit_count) exit_game(); - u8g.setColorIndex(1); + frame_start(); + set_color(1); // Draw invaders if (PAGE_CONTAINS(idat.pos.y, idat.pos.y + idat.botmost * (INVADER_ROW_H) - 2 - 1)) { @@ -376,7 +377,7 @@ void InvadersGame::game_screen() { int8_t xx = idat.pos.x; for (uint8_t x = 0; x < INVADER_COLS; ++x) { if (TEST(idat.bugs[y], x)) - u8g.drawBitmapP(xx, yy, 2, INVADER_H, invader[type][idat.game_blink]); + draw_bitmap(xx, yy, 2, INVADER_H, invader[type][idat.game_blink]); xx += INVADER_COL_W; } } @@ -386,43 +387,44 @@ void InvadersGame::game_screen() { // Draw UFO if (idat.ufov && PAGE_UNDER(UFO_H + 2)) - u8g.drawBitmapP(idat.ufox, 2, 2, UFO_H, ufo); + draw_bitmap(idat.ufox, 2, 2, UFO_H, ufo); // Draw cannon if (game_state && PAGE_CONTAINS(CANNON_Y, CANNON_Y + CANNON_H - 1) && (game_state < 2 || (game_state & 0x02))) - u8g.drawBitmapP(idat.cannon_x, CANNON_Y, 2, CANNON_H, cannon); + draw_bitmap(idat.cannon_x, CANNON_Y, 2, CANNON_H, cannon); // Draw laser if (idat.laser.v && PAGE_CONTAINS(idat.laser.y, idat.laser.y + LASER_H - 1)) - u8g.drawVLine(idat.laser.x, idat.laser.y, LASER_H); + draw_vline(idat.laser.x, idat.laser.y, LASER_H); // Draw invader bullets for (uint8_t i = 0; i < COUNT(idat.bullet); ++i) { if (idat.bullet[i].v && PAGE_CONTAINS(idat.bullet[i].y - (SHOT_H - 1), idat.bullet[i].y)) - u8g.drawVLine(idat.bullet[i].x, idat.bullet[i].y - (SHOT_H - 1), SHOT_H); + draw_vline(idat.bullet[i].x, idat.bullet[i].y - (SHOT_H - 1), SHOT_H); } // Draw explosion if (idat.explod.v && PAGE_CONTAINS(idat.explod.y, idat.explod.y + 7 - 1)) { - u8g.drawBitmapP(idat.explod.x, idat.explod.y, 2, 7, explosion); + draw_bitmap(idat.explod.x, idat.explod.y, 2, 7, explosion); --idat.explod.v; } // Blink GAME OVER when game is over if (!game_state) draw_game_over(); - if (PAGE_UNDER(MENU_FONT_ASCENT - 1)) { + if (PAGE_UNDER(GAME_FONT_ASCENT - 1)) { // Draw Score - //const uint8_t sx = (LCD_PIXEL_WIDTH - (score >= 10 ? score >= 100 ? score >= 1000 ? 4 : 3 : 2 : 1) * MENU_FONT_WIDTH) / 2; + //const uint8_t sx = (GAME_WIDTH - (score >= 10 ? score >= 100 ? score >= 1000 ? 4 : 3 : 2 : 1) * GAME_FONT_WIDTH) / 2; constexpr uint8_t sx = 0; - lcd_put_int(sx, MENU_FONT_ASCENT - 1, score); + draw_int(sx, GAME_FONT_ASCENT - 1, score); // Draw lives if (idat.cannons_left) for (uint8_t i = 1; i <= idat.cannons_left; ++i) - u8g.drawBitmapP(LCD_PIXEL_WIDTH - i * (LIFE_W), 6 - (LIFE_H), 1, LIFE_H, life); + draw_bitmap(GAME_WIDTH - i * (LIFE_W), 6 - (LIFE_H), 1, LIFE_H, life); } + frame_end(); } void InvadersGame::enter_game() { diff --git a/Marlin/src/lcd/menu/game/snake.cpp b/Marlin/src/lcd/menu/game/snake.cpp index 2a78c089cfbe..a000983f48ce 100644 --- a/Marlin/src/lcd/menu/game/snake.cpp +++ b/Marlin/src/lcd/menu/game/snake.cpp @@ -28,13 +28,13 @@ #define SNAKE_BOX 4 -#define HEADER_H (MENU_FONT_ASCENT - 2) +#define HEADER_H (GAME_FONT_ASCENT - 2) #define SNAKE_WH (SNAKE_BOX + 1) #define IDEAL_L 2 -#define IDEAL_R (LCD_PIXEL_WIDTH - 1 - 2) +#define IDEAL_R (GAME_WIDTH - 1 - 2) #define IDEAL_T (HEADER_H + 2) -#define IDEAL_B (LCD_PIXEL_HEIGHT - 1 - 2) +#define IDEAL_B (GAME_HEIGHT - 1 - 2) #define IDEAL_W (IDEAL_R - (IDEAL_L) + 1) #define IDEAL_H (IDEAL_B - (IDEAL_T) + 1) @@ -43,9 +43,9 @@ #define BOARD_W ((SNAKE_WH) * (GAME_W) + 1) #define BOARD_H ((SNAKE_WH) * (GAME_H) + 1) -#define BOARD_L ((LCD_PIXEL_WIDTH - (BOARD_W) + 1) / 2) +#define BOARD_L ((GAME_WIDTH - (BOARD_W) + 1) / 2) #define BOARD_R (BOARD_L + BOARD_W - 1) -#define BOARD_T (((LCD_PIXEL_HEIGHT + IDEAL_T) - (BOARD_H)) / 2) +#define BOARD_T (((GAME_HEIGHT + IDEAL_T) - (BOARD_H)) / 2) #define BOARD_B (BOARD_T + BOARD_H - 1) #define GAMEX(X) (BOARD_L + ((X) * (SNAKE_WH))) @@ -228,13 +228,14 @@ void SnakeGame::game_screen() { } while(0); - u8g.setColorIndex(1); + frame_start(); + set_color(1); // Draw Score - if (PAGE_UNDER(HEADER_H)) lcd_put_int(0, HEADER_H - 1, score); + if (PAGE_UNDER(HEADER_H)) draw_int(0, HEADER_H - 1, score); // DRAW THE PLAYFIELD BORDER - u8g.drawFrame(BOARD_L - 2, BOARD_T - 2, BOARD_R - BOARD_L + 4, BOARD_B - BOARD_T + 4); + draw_frame(BOARD_L - 2, BOARD_T - 2, BOARD_R - BOARD_L + 4, BOARD_B - BOARD_T + 4); // Draw the snake (tail) #if SNAKE_WH < 2 @@ -245,11 +246,11 @@ void SnakeGame::game_screen() { if (p.x == q.x) { const int8_t y1 = GAMEY(_MIN(p.y, q.y)), y2 = GAMEY(_MAX(p.y, q.y)); if (PAGE_CONTAINS(y1, y2)) - u8g.drawVLine(GAMEX(p.x), y1, y2 - y1 + 1); + draw_vline(GAMEX(p.x), y1, y2 - y1 + 1); } else if (PAGE_CONTAINS(GAMEY(p.y), GAMEY(p.y))) { const int8_t x1 = GAMEX(_MIN(p.x, q.x)), x2 = GAMEX(_MAX(p.x, q.x)); - u8g.drawHLine(x1, GAMEY(p.y), x2 - x1 + 1); + draw_hline(x1, GAMEY(p.y), x2 - x1 + 1); } } @@ -261,13 +262,13 @@ void SnakeGame::game_screen() { if (p.x == q.x) { const int8_t y1 = GAMEY(_MIN(p.y, q.y)), y2 = GAMEY(_MAX(p.y, q.y)); if (PAGE_CONTAINS(y1, y2 + 1)) - u8g.drawFrame(GAMEX(p.x), y1, 2, y2 - y1 + 1 + 1); + draw_frame(GAMEX(p.x), y1, 2, y2 - y1 + 1 + 1); } else { const int8_t py = GAMEY(p.y); if (PAGE_CONTAINS(py, py + 1)) { const int8_t x1 = GAMEX(_MIN(p.x, q.x)), x2 = GAMEX(_MAX(p.x, q.x)); - u8g.drawFrame(x1, py, x2 - x1 + 1 + 1, 2); + draw_frame(x1, py, x2 - x1 + 1 + 1, 2); } } } @@ -283,7 +284,7 @@ void SnakeGame::game_screen() { for (int8_t i = y1; i <= y2; ++i) { const int8_t y = GAMEY(i); if (PAGE_CONTAINS(y, y + SNAKE_SIZ - 1)) - u8g.drawBox(GAMEX(p.x), y, SNAKE_SIZ, SNAKE_SIZ); + draw_box(GAMEX(p.x), y, SNAKE_SIZ, SNAKE_SIZ); } } } @@ -292,7 +293,7 @@ void SnakeGame::game_screen() { if (PAGE_CONTAINS(py, py + SNAKE_SIZ - 1)) { const int8_t x1 = _MIN(p.x, q.x), x2 = _MAX(p.x, q.x); for (int8_t i = x1; i <= x2; ++i) - u8g.drawBox(GAMEX(i), py, SNAKE_SIZ, SNAKE_SIZ); + draw_box(GAMEX(i), py, SNAKE_SIZ, SNAKE_SIZ); } } } @@ -303,10 +304,12 @@ void SnakeGame::game_screen() { const int8_t fy = GAMEY(sdat.foody); if (PAGE_CONTAINS(fy, fy + FOOD_WH - 1)) { const int8_t fx = GAMEX(sdat.foodx); - u8g.drawFrame(fx, fy, FOOD_WH, FOOD_WH); - if (FOOD_WH == 5) u8g.drawPixel(fx + 2, fy + 2); + draw_frame(fx, fy, FOOD_WH, FOOD_WH); + if (FOOD_WH == 5) draw_pixel(fx + 2, fy + 2); } + frame_end(); + // Draw GAME OVER if (!game_state) draw_game_over(); diff --git a/Marlin/src/lcd/menu/game/types.h b/Marlin/src/lcd/menu/game/types.h index 6e0a2051d74e..49e07760db65 100644 --- a/Marlin/src/lcd/menu/game/types.h +++ b/Marlin/src/lcd/menu/game/types.h @@ -22,6 +22,14 @@ #pragma once #include +#include "../../../inc/MarlinConfigPre.h" +#include "../../marlinui.h" + +#if HAS_MARLINUI_U8GLIB + #include "../../dogm/game.h" +#elif IS_DWIN_MARLINUI + #include "../../e3v2/marlinui/game.h" +#endif typedef struct { int8_t x, y; } pos_t; @@ -41,6 +49,107 @@ class MarlinGame { static bool game_frame(); static void draw_game_over(); static void exit_game(); + public: static void init_game(const uint8_t init_state, const screenFunc_t screen); + + // + // Render API, based on U8GLib + // draw functions are implemented by the screen-specific renderer + // +protected: + /** + * @brief Called before any draw calls in the current frame. + */ + static void frame_start(); + + /** + * @brief Called after all draw calls in the current frame. + */ + static void frame_end(); + + /** + * @brief Set the color for subsequent draw calls. + * @param color The color to use for subsequent draw calls. + * @see https://github.com/olikraus/u8glib/wiki/userreference#setcolorindex + */ + static void set_color(const uint8_t color); + + /** + * @brief Draw a horizontal line. + * @param x The x-coordinate of the start of the line. + * @param y The y-coordinate of the line. + * @param l The length of the line. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawhline + */ + static void draw_hline(const game_dim_t x, const game_dim_t y, const game_dim_t l); + + /** + * @brief Draw a vertical line. + * @param x The x-coordinate of the line. + * @param y The y-coordinate of the start of the line. + * @param l The length of the line. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawvline + */ + static void draw_vline(const game_dim_t x, const game_dim_t y, const game_dim_t l); + + /** + * @brief Draw a outlined rectangle (frame). + * @param x The x-coordinate of the top-left corner of the frame. + * @param y The y-coordinate of the top-left corner of the frame. + * @param w The width of the frame. + * @param h The height of the frame. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawframe + */ + static void draw_frame(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h); + + /** + * @brief Draw a filled rectangle (box). + * @param x The x-coordinate of the top-left corner of the box. + * @param y The y-coordinate of the top-left corner of the box. + * @param w The width of the box. + * @param h The height of the box. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawbox + */ + static void draw_box(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h); + + /** + * @brief Draw a pixel. + * @param x The x-coordinate of the pixel. + * @param y The y-coordinate of the pixel. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawpixel + */ + static void draw_pixel(const game_dim_t x, const game_dim_t y); + + /** + * @brief Draw a bitmap. + * @param x The x-coordinate of the top-left corner of the bitmap. + * @param y The y-coordinate of the top-left corner of the bitmap. + * @param bytes_per_row The number of bytes per row in the bitmap (Width = bytes_per_row * 8). + * @param rows The number of rows in the bitmap (= Height). + * @param bitmap The bitmap to draw. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawbitmap + */ + static void draw_bitmap(const game_dim_t x, const game_dim_t y, const game_dim_t bytes_per_row, const game_dim_t rows, const pgm_bitmap_t bitmap); + + /** + * @brief Draw a string. + * @param x The x-coordinate of the string. + * @param y The y-coordinate of the string. + * @param str The string to draw. + * @see lcd_moveto + lcd_put_u8str + * @note The font size is available using the GAME_FONT_WIDTH and GAME_FONT_ASCENT constants. + */ + static int draw_string(const game_dim_t x, const game_dim_t y, const char *str); + static int draw_string(const game_dim_t x, const game_dim_t y, FSTR_P const str); + + /** + * @brief Draw an integer. + * @param x The x-coordinate of the integer. + * @param y The y-coordinate of the integer. + * @param value The integer to draw. + * @see lcd_put_int + * @note The font size is available using the GAME_FONT_WIDTH and GAME_FONT_ASCENT constants. + */ + static void draw_int(const game_dim_t x, const game_dim_t y, const int value); }; From a0e20609e816c4cba8bc82bdc9c3caf139e4d8f4 Mon Sep 17 00:00:00 2001 From: Chris <52449218+shadow578@users.noreply.github.com> Date: Wed, 17 Apr 2024 07:46:38 +0000 Subject: [PATCH 2/6] update games to include colors --- Marlin/src/lcd/dogm/game.cpp | 13 ++++++-- Marlin/src/lcd/e3v2/marlinui/game.cpp | 37 ++++++++++++++++++++--- Marlin/src/lcd/e3v2/marlinui/game.h | 7 ----- Marlin/src/lcd/menu/game/brickout.cpp | 13 ++++++-- Marlin/src/lcd/menu/game/game.cpp | 4 +-- Marlin/src/lcd/menu/game/invaders.cpp | 43 ++++++++++++++++++++++----- Marlin/src/lcd/menu/game/snake.cpp | 22 +++++++------- Marlin/src/lcd/menu/game/types.h | 27 +++++++++++++++-- 8 files changed, 129 insertions(+), 37 deletions(-) diff --git a/Marlin/src/lcd/dogm/game.cpp b/Marlin/src/lcd/dogm/game.cpp index a9443a85251a..73c98aec52f4 100644 --- a/Marlin/src/lcd/dogm/game.cpp +++ b/Marlin/src/lcd/dogm/game.cpp @@ -8,8 +8,17 @@ void MarlinGame::frame_start() {} void MarlinGame::frame_end() {} -void MarlinGame::set_color(const uint8_t color) { - u8g.setColorIndex(color); +void MarlinGame::set_color(const color color) { + switch(color) + { + case color::BLACK: + u8g.setColorIndex(0); + break; + case color::WHITE: + default: + u8g.setColorIndex(1); + break; + } } void MarlinGame::draw_hline(const game_dim_t x, const game_dim_t y, const game_dim_t w) { diff --git a/Marlin/src/lcd/e3v2/marlinui/game.cpp b/Marlin/src/lcd/e3v2/marlinui/game.cpp index 2a67d2a072a5..2d149c9ae340 100644 --- a/Marlin/src/lcd/e3v2/marlinui/game.cpp +++ b/Marlin/src/lcd/e3v2/marlinui/game.cpp @@ -9,19 +9,48 @@ void MarlinGame::frame_start() { // clear the screen before each frame - //dwinFrameClear(RGB(0, 0, 0)); + //dwinFrameClear(CLEAR_COLOR); // filling the play area should be faster than clearing the whole screen const uint16_t fg = dwin_font.fg; - dwin_font.fg = dwin_game::color_to_dwin(0); + dwin_font.fg = COLOR_BG_BLACK; draw_box(0, 0, GAME_WIDTH, GAME_HEIGHT); dwin_font.fg = fg; } void MarlinGame::frame_end() {} -void MarlinGame::set_color(const uint8_t color) { - dwin_font.fg = dwin_game::color_to_dwin(color); +void MarlinGame::set_color(const color color) { + switch(color) + { + case color::BLACK: + dwin_font.fg = COLOR_BG_BLACK; + break; + case color::WHITE: + default: + dwin_font.fg = COLOR_WHITE; + break; + + // https://rgbcolorpicker.com/565/table + case color::RED: + dwin_font.fg = RGB(0x1F, 0x00, 0x00); + break; + case color::GREEN: + dwin_font.fg = RGB(0x00, 0x3F, 0x00); + break; + case color::BLUE: + dwin_font.fg = RGB(0x00, 0x00, 0x1F); + break; + case color::YELLOW: + dwin_font.fg = RGB(0x1F, 0x3F, 0x00); + break; + case color::CYAN: + dwin_font.fg = RGB(0x00, 0x3F, 0x1F); + break; + case color::MAGENTA: + dwin_font.fg = RGB(0x1F, 0x00, 0x1F); + break; + } } void MarlinGame::draw_hline(const game_dim_t x, const game_dim_t y, const game_dim_t w) { diff --git a/Marlin/src/lcd/e3v2/marlinui/game.h b/Marlin/src/lcd/e3v2/marlinui/game.h index 7cfcbcfea0ea..ddd402e6fc82 100644 --- a/Marlin/src/lcd/e3v2/marlinui/game.h +++ b/Marlin/src/lcd/e3v2/marlinui/game.h @@ -52,13 +52,6 @@ namespace dwin_game { static_assert(game_to_screen(TARGET_WIDTH) + (x_offset * 2) <= DWIN_WIDTH, "DWIN game renderer failed to auto-scale, is too wide"); static_assert(game_to_screen(TARGET_HEIGHT) + (y_offset * 2) <= DWIN_HEIGHT, "DWIN game renderer failed to auto-scale, is too high"); - - /** - * @brief convert a color (from set_color) to dwin - */ - constexpr uint16_t color_to_dwin(const uint8_t color) { - return color == 0 ? RGB(0, 0, 0) : RGB(255, 255, 255); - } } // namespace dwin_game constexpr game_dim_t GAME_WIDTH = dwin_game::screen_to_game(DWIN_WIDTH - (dwin_game::x_offset * 2)); diff --git a/Marlin/src/lcd/menu/game/brickout.cpp b/Marlin/src/lcd/menu/game/brickout.cpp index 72ab3c041889..b0d0f1109787 100644 --- a/Marlin/src/lcd/menu/game/brickout.cpp +++ b/Marlin/src/lcd/menu/game/brickout.cpp @@ -135,14 +135,20 @@ void BrickoutGame::game_screen() { } frame_start(); - set_color(1); - // Draw bricks + // Draw bricks, cycling through colors for each brick + const color brick_colors[] = { color::RED, color::CYAN, color::GREEN, color::YELLOW, color::MAGENTA, color::BLUE }; + int color_index = 0; if (PAGE_CONTAINS(BRICK_TOP, BRICK_BOT)) { for (uint8_t y = 0; y < BRICK_ROWS; ++y) { const uint8_t yy = y * BRICK_H + BRICK_TOP; if (PAGE_CONTAINS(yy, yy + BRICK_H - 1)) { for (uint8_t x = 0; x < BRICK_COLS; ++x) { + // cycle through colors, even if the brick is gone + // otherwise, bricks would change color if their neighbor is hit + set_color(brick_colors[color_index++ % COUNT(brick_colors)]); + + // draw brick if it's still there if (TEST(bdat.bricks[y], x)) { const uint8_t xx = x * BRICK_W; for (uint8_t v = 0; v < BRICK_H - 1; ++v) @@ -154,6 +160,9 @@ void BrickoutGame::game_screen() { } } + // everything else is white + set_color(color::WHITE); + // Draw paddle if (PAGE_CONTAINS(PADDLE_Y-1, PADDLE_Y)) { draw_hline(bdat.paddle_x, PADDLE_Y, PADDLE_W); diff --git a/Marlin/src/lcd/menu/game/game.cpp b/Marlin/src/lcd/menu/game/game.cpp index ee2a3e6397d6..933e2e8e66e3 100644 --- a/Marlin/src/lcd/menu/game/game.cpp +++ b/Marlin/src/lcd/menu/game/game.cpp @@ -45,9 +45,9 @@ void MarlinGame::draw_game_over() { lx = (GAME_WIDTH - gowide) / 2, ly = (GAME_HEIGHT + gohigh) / 2; if (PAGE_CONTAINS(ly - gohigh - 1, ly + 1)) { - set_color(0); + set_color(color::BLACK); draw_box(lx - 1, ly - gohigh - 1, gowide + 2, gohigh + 2); - set_color(1); + set_color(color::WHITE); if (ui.get_blink()) draw_string(lx, ly, F("GAME OVER")); } } diff --git a/Marlin/src/lcd/menu/game/invaders.cpp b/Marlin/src/lcd/menu/game/invaders.cpp index 54307bed73be..1f884ddb31e6 100644 --- a/Marlin/src/lcd/menu/game/invaders.cpp +++ b/Marlin/src/lcd/menu/game/invaders.cpp @@ -48,6 +48,16 @@ #define INVADER_RIGHT ((INVADER_COLS) * (INVADER_COL_W)) + +#define INVADER_COLOR { MarlinGame::color::GREEN, MarlinGame::color::CYAN, MarlinGame::color::YELLOW } +#define CANNON_COLOR MarlinGame::color::WHITE +#define LASER_COLOR MarlinGame::color::WHITE // shot by player +#define BULLET_COLOR LASER_COLOR // shot by invader +#define LIFE_COLOR CANNON_COLOR +#define UFO_COLOR MarlinGame::color::MAGENTA +#define EXPLOSION_COLOR MarlinGame::color::RED + + // 11x8 const unsigned char invader[3][2][16] PROGMEM = { { { B00000110,B00000000, @@ -366,7 +376,6 @@ void InvadersGame::game_screen() { if (!idat.quit_count) exit_game(); frame_start(); - set_color(1); // Draw invaders if (PAGE_CONTAINS(idat.pos.y, idat.pos.y + idat.botmost * (INVADER_ROW_H) - 2 - 1)) { @@ -376,8 +385,11 @@ void InvadersGame::game_screen() { if (PAGE_CONTAINS(yy, yy + INVADER_H - 1)) { int8_t xx = idat.pos.x; for (uint8_t x = 0; x < INVADER_COLS; ++x) { - if (TEST(idat.bugs[y], x)) + if (TEST(idat.bugs[y], x)) { + constexpr color type_color[] = INVADER_COLOR; + set_color(type_color[type]); draw_bitmap(xx, yy, 2, INVADER_H, invader[type][idat.game_blink]); + } xx += INVADER_COL_W; } } @@ -386,31 +398,44 @@ void InvadersGame::game_screen() { } // Draw UFO - if (idat.ufov && PAGE_UNDER(UFO_H + 2)) + if (idat.ufov && PAGE_UNDER(UFO_H + 2)) { + set_color(UFO_COLOR); draw_bitmap(idat.ufox, 2, 2, UFO_H, ufo); + } // Draw cannon - if (game_state && PAGE_CONTAINS(CANNON_Y, CANNON_Y + CANNON_H - 1) && (game_state < 2 || (game_state & 0x02))) + if (game_state && PAGE_CONTAINS(CANNON_Y, CANNON_Y + CANNON_H - 1) && (game_state < 2 || (game_state & 0x02))) { + set_color(CANNON_COLOR); draw_bitmap(idat.cannon_x, CANNON_Y, 2, CANNON_H, cannon); + } // Draw laser - if (idat.laser.v && PAGE_CONTAINS(idat.laser.y, idat.laser.y + LASER_H - 1)) + if (idat.laser.v && PAGE_CONTAINS(idat.laser.y, idat.laser.y + LASER_H - 1)) { + set_color(LASER_COLOR); draw_vline(idat.laser.x, idat.laser.y, LASER_H); + } // Draw invader bullets for (uint8_t i = 0; i < COUNT(idat.bullet); ++i) { - if (idat.bullet[i].v && PAGE_CONTAINS(idat.bullet[i].y - (SHOT_H - 1), idat.bullet[i].y)) + if (idat.bullet[i].v && PAGE_CONTAINS(idat.bullet[i].y - (SHOT_H - 1), idat.bullet[i].y)) { + set_color(BULLET_COLOR); draw_vline(idat.bullet[i].x, idat.bullet[i].y - (SHOT_H - 1), SHOT_H); + } } // Draw explosion if (idat.explod.v && PAGE_CONTAINS(idat.explod.y, idat.explod.y + 7 - 1)) { + set_color(EXPLOSION_COLOR); draw_bitmap(idat.explod.x, idat.explod.y, 2, 7, explosion); --idat.explod.v; } + set_color(color::WHITE); + // Blink GAME OVER when game is over - if (!game_state) draw_game_over(); + if (!game_state) { + draw_game_over(); + } if (PAGE_UNDER(GAME_FONT_ASCENT - 1)) { // Draw Score @@ -420,8 +445,10 @@ void InvadersGame::game_screen() { // Draw lives if (idat.cannons_left) - for (uint8_t i = 1; i <= idat.cannons_left; ++i) + for (uint8_t i = 1; i <= idat.cannons_left; ++i) { + set_color(LIFE_COLOR); draw_bitmap(GAME_WIDTH - i * (LIFE_W), 6 - (LIFE_H), 1, LIFE_H, life); + } } frame_end(); diff --git a/Marlin/src/lcd/menu/game/snake.cpp b/Marlin/src/lcd/menu/game/snake.cpp index a000983f48ce..1dfbda829de0 100644 --- a/Marlin/src/lcd/menu/game/snake.cpp +++ b/Marlin/src/lcd/menu/game/snake.cpp @@ -229,15 +229,9 @@ void SnakeGame::game_screen() { } while(0); frame_start(); - set_color(1); - // Draw Score - if (PAGE_UNDER(HEADER_H)) draw_int(0, HEADER_H - 1, score); - - // DRAW THE PLAYFIELD BORDER - draw_frame(BOARD_L - 2, BOARD_T - 2, BOARD_R - BOARD_L + 4, BOARD_B - BOARD_T + 4); - - // Draw the snake (tail) + // Draw the snake (tail) in green + set_color(color::GREEN); #if SNAKE_WH < 2 // At this scale just draw a line @@ -300,7 +294,8 @@ void SnakeGame::game_screen() { #endif - // Draw food + // Draw food in red + set_color(color::RED); const int8_t fy = GAMEY(sdat.foody); if (PAGE_CONTAINS(fy, fy + FOOD_WH - 1)) { const int8_t fx = GAMEX(sdat.foodx); @@ -308,13 +303,20 @@ void SnakeGame::game_screen() { if (FOOD_WH == 5) draw_pixel(fx + 2, fy + 2); } - frame_end(); + // Draw the playfield border + set_color(color::WHITE); + draw_frame(BOARD_L - 2, BOARD_T - 2, BOARD_R - BOARD_L + 4, BOARD_B - BOARD_T + 4); + + // Draw Score + if (PAGE_UNDER(HEADER_H)) draw_int(0, HEADER_H - 1, score); // Draw GAME OVER if (!game_state) draw_game_over(); // A click always exits this game if (ui.use_click()) exit_game(); + + frame_end(); } void SnakeGame::enter_game() { diff --git a/Marlin/src/lcd/menu/game/types.h b/Marlin/src/lcd/menu/game/types.h index 49e07760db65..f9947847dd5f 100644 --- a/Marlin/src/lcd/menu/game/types.h +++ b/Marlin/src/lcd/menu/game/types.h @@ -57,6 +57,30 @@ class MarlinGame { // Render API, based on U8GLib // draw functions are implemented by the screen-specific renderer // +public: + /** + * @brief The colors available for drawing games. + * @note If a screen doesn't support (a) color, it shall fall back to using WHITE. + */ + enum class color { + /** + * @brief Black color. This is guaranteed to be the clear color on all screens. + */ + BLACK, + + /** + * @brief White color. Guranteed to be white on all screens. + */ + WHITE, + + RED, + GREEN, + BLUE, + YELLOW, + CYAN, + MAGENTA, + }; + protected: /** * @brief Called before any draw calls in the current frame. @@ -71,9 +95,8 @@ class MarlinGame { /** * @brief Set the color for subsequent draw calls. * @param color The color to use for subsequent draw calls. - * @see https://github.com/olikraus/u8glib/wiki/userreference#setcolorindex */ - static void set_color(const uint8_t color); + static void set_color(const color color); /** * @brief Draw a horizontal line. From 50fbb626201fe4c65be1d550e9304df079a0a491 Mon Sep 17 00:00:00 2001 From: Chris <52449218+shadow578@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:38:22 +0000 Subject: [PATCH 3/6] add dwinDrawPointMap to draw bitmaps using points on DWIN screens --- Marlin/src/lcd/e3v2/common/dwin_api.cpp | 66 +++++++++++++++++++++++++ Marlin/src/lcd/e3v2/common/dwin_api.h | 20 ++++++++ Marlin/src/lcd/e3v2/marlinui/game.cpp | 44 ++++++++++++----- 3 files changed, 119 insertions(+), 11 deletions(-) diff --git a/Marlin/src/lcd/e3v2/common/dwin_api.cpp b/Marlin/src/lcd/e3v2/common/dwin_api.cpp index 1688b2423053..04beafe68ab7 100644 --- a/Marlin/src/lcd/e3v2/common/dwin_api.cpp +++ b/Marlin/src/lcd/e3v2/common/dwin_api.cpp @@ -169,6 +169,72 @@ void dwinFrameClear(const uint16_t color) { dwinWord(i, y); dwinSend(i); } + + // Draw a map of multiple points using minimal amount of point drawing commands + // color: point color + // point_width: point width 0x01-0x0F + // point_height: point height 0x01-0x0F + // x,y: upper left point + // map_columns: columns in theh point map. each column is a byte in the map and contains 8 points + // map_rows: rows in the point map + // map: point bitmap. 2D array of points, 1 bit per point + // Note: somewhat similar to U8G's drawBitmap() function, see https://github.com/olikraus/u8glib/wiki/userreference#drawbitmap + void dwinDrawPointMap( + const uint16_t color, + const uint8_t point_width, + const uint8_t point_height, + const uint16_t x, + const uint16_t y, + const uint16_t map_columns, + const uint16_t map_rows, + const uint8_t *map_data) { + // how many bytes can we write to the send buffer? + // one byte is used for F_HONE, so we can write up to len(dwinSendBuf) - 1 bytes. + constexpr size_t send_buffer_size = (COUNT(dwinSendBuf) - 1); + // at how many bytes should we flush the send buffer? + // one byte is used (hidden) for F_HONE, and we need 4 bytes when appending a point. + // so we should flush the send buffer when we have less than 5 bytes left. + constexpr size_t flush_send_buffer_at = (COUNT(dwinSendBuf) - 1 - 4); + + // how long is the header of each draw command? + // 1B CMD, 2B COLOR, 1B WIDTH, 1B HEIGHT + constexpr size_t command_header_size = 5; + + // draw the point map + size_t i = 0; + for (uint16_t row = 0; row < map_rows; row++) { + for (uint16_t col = 0; col < map_columns; col++) { + const uint8_t map_byte = map_data[(row * map_columns) + col]; + for (uint8_t bit = 0; bit < 8; bit++) { + // draw the bit of the byte if it's set + if (TEST(map_byte, bit)) { + // flush the send buffer and prepare next draw if either + // a) the buffer reached the 'should flush' state, or + // b) this is the first point to draw + if (i >= flush_send_buffer_at || i == 0) + { + // dispatch the current draw command + if (i > command_header_size) dwinSend(i); + + // prepare the next draw command + i = 0; + dwinByte(i, 0x02); // cmd: draw point(s) + dwinWord(i, color); + dwinByte(i, point_width); + dwinByte(i, point_height); + } + + // append point coordinates to draw command + dwinWord(i, x + (point_width * ((8 * col) + (7 - bit)))); // x + dwinWord(i, y + (point_height * (row))); // y + } + } + } + } + + // dispatch final draw command if the buffer contains any points + if (i > command_header_size) dwinSend(i); + } #endif // Draw a line diff --git a/Marlin/src/lcd/e3v2/common/dwin_api.h b/Marlin/src/lcd/e3v2/common/dwin_api.h index 48785150328f..64145b69c4c1 100644 --- a/Marlin/src/lcd/e3v2/common/dwin_api.h +++ b/Marlin/src/lcd/e3v2/common/dwin_api.h @@ -164,6 +164,26 @@ inline void dwinDrawBox(uint8_t mode, uint16_t color, uint16_t xStart, uint16_t void dwinDrawPoint(uint16_t color, uint8_t width, uint8_t height, uint16_t x, uint16_t y); #endif +// Draw a map of multiple points using minimal amount of point drawing commands +// color: point color +// point_width: point width 0x01-0x0F +// point_height: point height 0x01-0x0F +// x,y: upper left point +// map_columns: columns in theh point map. each column is a byte in the map and contains 8 points +// map_rows: rows in the point map +// map: point bitmap. 2D array of points, 1 bit per point +#if DISABLED(TJC_DISPLAY) + void dwinDrawPointMap( + const uint16_t color, + const uint8_t point_width, + const uint8_t point_height, + const uint16_t x, + const uint16_t y, + const uint16_t map_columns, + const uint16_t map_rows, + const uint8_t *map_data); +#endif + // Move a screen area // mode: 0, circle shift; 1, translation // dir: 0=left, 1=right, 2=up, 3=down diff --git a/Marlin/src/lcd/e3v2/marlinui/game.cpp b/Marlin/src/lcd/e3v2/marlinui/game.cpp index 2d149c9ae340..b0e382c9a55b 100644 --- a/Marlin/src/lcd/e3v2/marlinui/game.cpp +++ b/Marlin/src/lcd/e3v2/marlinui/game.cpp @@ -86,26 +86,48 @@ void MarlinGame::draw_box(const game_dim_t x, const game_dim_t y, const game_dim } void MarlinGame::draw_pixel(const game_dim_t x, const game_dim_t y) { - // draw pixels as boxes, since DWIN pixels are always 1px wide but we want to scale them + // draw pixels using boxes. + // while DWIN protocol supports drawing points with different sizes, the + // 0x02 'draw point' command is way slower per pixel than 0x05 'fill rectangle' + // (0.4 us vs 0.14 us per pixel) draw_box(x, y, 1, 1); } void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_dim_t bytes_per_row, const game_dim_t rows, const pgm_bitmap_t bitmap) { // DWIN theorethically supports bitmaps since kernel 2.1, but most screens don't support it // (either because they use an older kernel version, or because they just (badly) emulate the DWIN protocol). - // So instead, we draw the bitmap as a series of pixels, effectively emulating the draw call. - // This will totally suck for performance, but it's the best we can do. - for (game_dim_t row = 0; row < rows; row++) { - for (game_dim_t col = 0; col < bytes_per_row; col++) { - const uint8_t byte = bitmap[(row * bytes_per_row) + col]; - for (uint8_t bit = 0; bit < 8; bit++) { - // assume that the screen area is cleared before drawing - if (byte & (1 << bit)) { - draw_pixel(x + (col * 8) + (7 - bit + 1), y + row); + // So instead, we have to fall back to drawing points manually. + + #if DISABLED(TJC_DISPLAY) + // DWIN T5UI actually supports drawing multiple points in one go using the 0x02 'draw point' command, ever since kernel 1.2. + // So we use that to draw the bitmap as a series of points, which should be faster than drawing rectangles using draw_pixel. + // This will be somewhat slow, but way faster than drawing rectangles one by one. + dwinDrawPointMap( + dwin_font.fg, // color + dwin_game::game_to_screen(1), // point size + dwin_game::game_to_screen(1), + dwin_game::game_to_screen(x) + dwin_game::x_offset, // x / y + dwin_game::game_to_screen(y) + dwin_game::y_offset, + bytes_per_row, // bitmap dimensions + rows, + bitmap // U8G bitmap format is compatible to DrawPointMap format + ); + #else + // TJC displays don't seem to support the 0x02 'draw point' command, so instead we have to draw the bitmap + // as a series of rectangles using draw_pixel. + // This will absolutely suck for performance, but it's the best we can do on these screens. + for (game_dim_t row = 0; row < rows; row++) { + for (game_dim_t col = 0; col < bytes_per_row; col++) { + const uint8_t byte = bitmap[(row * bytes_per_row) + col]; + for (uint8_t bit = 0; bit < 8; bit++) { + // assume that the screen area is cleared before drawing + if (byte & (1 << bit)) { + draw_pixel(x + (col * 8) + (7 - bit + 1), y + row); + } } } } - } + #endif } int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, const char* str) { From 13bc3ccf7fd77f5beabbe8e3efd664884b99a2b2 Mon Sep 17 00:00:00 2001 From: shadow578 <52449218+shadow578@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:15:29 +0200 Subject: [PATCH 4/6] add performance counters to DWIN game renderer --- Marlin/src/lcd/e3v2/marlinui/game.cpp | 70 ++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/Marlin/src/lcd/e3v2/marlinui/game.cpp b/Marlin/src/lcd/e3v2/marlinui/game.cpp index b0e382c9a55b..3a3730042878 100644 --- a/Marlin/src/lcd/e3v2/marlinui/game.cpp +++ b/Marlin/src/lcd/e3v2/marlinui/game.cpp @@ -2,11 +2,23 @@ #if IS_DWIN_MARLINUI && HAS_GAMES +#define PERFORMANCE_COUNTERS 1 + #include "../../menu/game/types.h" // includes e3v2/marlinui/game.h #include "../../lcdprint.h" #include "lcdprint_dwin.h" #include "marlinui_dwin.h" +#if PERFORMANCE_COUNTERS == 1 + static uint32_t draw_call_cnt = 0; // total number of draw calls in the current frame + static millis_t frame_draw_millis = 0, // time spent drawing the frame + frame_wait_millis = 0; // time spent waiting for the next frame + + #define DRAW_CALL_INC(subcall_cnt) draw_call_cnt++ +#else + #define DRAW_CALL_INC(subcall_cnt) +#endif + void MarlinGame::frame_start() { // clear the screen before each frame //dwinFrameClear(CLEAR_COLOR); @@ -16,9 +28,50 @@ void MarlinGame::frame_start() { dwin_font.fg = COLOR_BG_BLACK; draw_box(0, 0, GAME_WIDTH, GAME_HEIGHT); dwin_font.fg = fg; + + dwin_font.index = DWIN_FONT_MENU; + + // reset performance counters + #if PERFORMANCE_COUNTERS == 1 + draw_call_cnt = 0; + frame_draw_millis = millis(); + frame_wait_millis = frame_draw_millis - frame_wait_millis; + #endif } -void MarlinGame::frame_end() {} +void MarlinGame::frame_end() { + #if PERFORMANCE_COUNTERS == 1 + const millis_t frame_wait = frame_wait_millis; + frame_wait_millis = millis(); + frame_draw_millis = frame_wait_millis - frame_draw_millis; + + // calculate frames per deci-seconds (100 milliseconds) + const uint32_t fpds = 100 / (frame_draw_millis + frame_wait); + + // format the performance counters as a string + char perf_str[64]; + sprintf_P( + perf_str, + PSTR("d%04lu w%04lu c%04lu f%02lu "), + frame_draw_millis, + frame_wait, + draw_call_cnt, + fpds + ); + + // draw the performance counters at the (physical) origin of the screen + const uint16_t fg = dwin_font.fg; + const bool solid = dwin_font.solid; + dwin_font.fg = RGB(0x1F, 0x3F, 0x00); + dwin_font.solid = true; + + lcd_moveto_xy(0, 0); + lcd_put_u8str(perf_str); + + dwin_font.fg = fg; + dwin_font.solid = solid; + #endif +} void MarlinGame::set_color(const color color) { switch(color) @@ -56,11 +109,15 @@ void MarlinGame::set_color(const color color) { void MarlinGame::draw_hline(const game_dim_t x, const game_dim_t y, const game_dim_t w) { // draw lines as boxes, since DWIN lines are always 1px wide but we want to scale them draw_box(x, y, w, 1); + + DRAW_CALL_INC(1); } void MarlinGame::draw_vline(const game_dim_t x, const game_dim_t y, const game_dim_t h) { // draw lines as boxes, since DWIN lines are always 1px wide but we want to scale them draw_box(x, y, 1, h); + + DRAW_CALL_INC(1); } void MarlinGame::draw_frame(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { @@ -72,6 +129,8 @@ void MarlinGame::draw_frame(const game_dim_t x, const game_dim_t y, const game_d dwin_game::game_to_screen(w), dwin_game::game_to_screen(h) ); + + DRAW_CALL_INC(0); } void MarlinGame::draw_box(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { @@ -83,6 +142,8 @@ void MarlinGame::draw_box(const game_dim_t x, const game_dim_t y, const game_dim dwin_game::game_to_screen(w), dwin_game::game_to_screen(h) ); + + DRAW_CALL_INC(0); } void MarlinGame::draw_pixel(const game_dim_t x, const game_dim_t y) { @@ -91,6 +152,8 @@ void MarlinGame::draw_pixel(const game_dim_t x, const game_dim_t y) { // 0x02 'draw point' command is way slower per pixel than 0x05 'fill rectangle' // (0.4 us vs 0.14 us per pixel) draw_box(x, y, 1, 1); + + DRAW_CALL_INC(1); } void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_dim_t bytes_per_row, const game_dim_t rows, const pgm_bitmap_t bitmap) { @@ -112,6 +175,8 @@ void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_ rows, bitmap // U8G bitmap format is compatible to DrawPointMap format ); + + DRAW_CALL_INC(0); #else // TJC displays don't seem to support the 0x02 'draw point' command, so instead we have to draw the bitmap // as a series of rectangles using draw_pixel. @@ -123,6 +188,7 @@ void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_ // assume that the screen area is cleared before drawing if (byte & (1 << bit)) { draw_pixel(x + (col * 8) + (7 - bit + 1), y + row); + DRAW_CALL_INC(1); } } } @@ -131,6 +197,8 @@ void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_ } int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, const char* str) { + DRAW_CALL_INC(0); + lcd_moveto_xy( dwin_game::game_to_screen(x) + dwin_game::x_offset, dwin_game::game_to_screen(y) + dwin_game::y_offset From 7e46c9fd22429b6559c26201e6c09d653c2b2ed1 Mon Sep 17 00:00:00 2001 From: shadow578 <52449218+shadow578@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:28:33 +0200 Subject: [PATCH 5/6] draw brickout bricks using draw_box instead of draw_line reduces the number of draw calls required considerably --- Marlin/src/lcd/menu/game/brickout.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Marlin/src/lcd/menu/game/brickout.cpp b/Marlin/src/lcd/menu/game/brickout.cpp index b0d0f1109787..83c74d82f449 100644 --- a/Marlin/src/lcd/menu/game/brickout.cpp +++ b/Marlin/src/lcd/menu/game/brickout.cpp @@ -151,9 +151,11 @@ void BrickoutGame::game_screen() { // draw brick if it's still there if (TEST(bdat.bricks[y], x)) { const uint8_t xx = x * BRICK_W; - for (uint8_t v = 0; v < BRICK_H - 1; ++v) - if (PAGE_CONTAINS(yy + v, yy + v)) - draw_hline(xx, yy + v, BRICK_W - 1); + draw_box(xx, yy, BRICK_W - 1, BRICK_H - 1); + + //for (uint8_t v = 0; v < BRICK_H - 1; ++v) + // if (PAGE_CONTAINS(yy + v, yy + v)) + // draw_hline(xx, yy + v, BRICK_W - 1); } } } From 1d31b3f38f0db90f2c4c26f95b10f15a9495d25e Mon Sep 17 00:00:00 2001 From: Chris <52449218+shadow578@users.noreply.github.com> Date: Mon, 29 Apr 2024 08:17:21 +0000 Subject: [PATCH 6/6] minor cleanup of game renderer code --- Marlin/src/lcd/e3v2/common/dwin_api.cpp | 29 ++++----- Marlin/src/lcd/e3v2/marlinui/game.cpp | 86 ++++++++++++------------- Marlin/src/lcd/e3v2/marlinui/game.h | 10 +-- Marlin/src/lcd/menu/game/brickout.cpp | 12 ++-- Marlin/src/lcd/menu/game/invaders.cpp | 22 +++---- Marlin/src/lcd/menu/game/types.h | 21 +++--- 6 files changed, 85 insertions(+), 95 deletions(-) diff --git a/Marlin/src/lcd/e3v2/common/dwin_api.cpp b/Marlin/src/lcd/e3v2/common/dwin_api.cpp index 04beafe68ab7..a064505807a3 100644 --- a/Marlin/src/lcd/e3v2/common/dwin_api.cpp +++ b/Marlin/src/lcd/e3v2/common/dwin_api.cpp @@ -188,35 +188,32 @@ void dwinFrameClear(const uint16_t color) { const uint16_t map_columns, const uint16_t map_rows, const uint8_t *map_data) { - // how many bytes can we write to the send buffer? - // one byte is used for F_HONE, so we can write up to len(dwinSendBuf) - 1 bytes. - constexpr size_t send_buffer_size = (COUNT(dwinSendBuf) - 1); - // at how many bytes should we flush the send buffer? - // one byte is used (hidden) for F_HONE, and we need 4 bytes when appending a point. - // so we should flush the send buffer when we have less than 5 bytes left. + // At how many bytes should we flush the send buffer? + // One byte is used (hidden) for F_HONE, and we need 4 bytes when appending a point. + // So we should flush the send buffer when we have less than 5 bytes left. constexpr size_t flush_send_buffer_at = (COUNT(dwinSendBuf) - 1 - 4); - // how long is the header of each draw command? + // How long is the header of each draw command? // 1B CMD, 2B COLOR, 1B WIDTH, 1B HEIGHT constexpr size_t command_header_size = 5; - // draw the point map + // Draw the point map size_t i = 0; for (uint16_t row = 0; row < map_rows; row++) { for (uint16_t col = 0; col < map_columns; col++) { const uint8_t map_byte = map_data[(row * map_columns) + col]; for (uint8_t bit = 0; bit < 8; bit++) { - // draw the bit of the byte if it's set + // Draw the bit of the byte if it's set if (TEST(map_byte, bit)) { - // flush the send buffer and prepare next draw if either - // a) the buffer reached the 'should flush' state, or - // b) this is the first point to draw + // Flush the send buffer and prepare next draw if either + // a) The buffer reached the 'should flush' state, or + // b) This is the first point to draw if (i >= flush_send_buffer_at || i == 0) { - // dispatch the current draw command + // Dispatch the current draw command if (i > command_header_size) dwinSend(i); - // prepare the next draw command + // Prepare the next draw command i = 0; dwinByte(i, 0x02); // cmd: draw point(s) dwinWord(i, color); @@ -224,7 +221,7 @@ void dwinFrameClear(const uint16_t color) { dwinByte(i, point_height); } - // append point coordinates to draw command + // Append point coordinates to draw command dwinWord(i, x + (point_width * ((8 * col) + (7 - bit)))); // x dwinWord(i, y + (point_height * (row))); // y } @@ -232,7 +229,7 @@ void dwinFrameClear(const uint16_t color) { } } - // dispatch final draw command if the buffer contains any points + // Dispatch final draw command if the buffer contains any points if (i > command_header_size) dwinSend(i); } #endif diff --git a/Marlin/src/lcd/e3v2/marlinui/game.cpp b/Marlin/src/lcd/e3v2/marlinui/game.cpp index 3a3730042878..1c2acf4792a5 100644 --- a/Marlin/src/lcd/e3v2/marlinui/game.cpp +++ b/Marlin/src/lcd/e3v2/marlinui/game.cpp @@ -2,67 +2,68 @@ #if IS_DWIN_MARLINUI && HAS_GAMES -#define PERFORMANCE_COUNTERS 1 +// Show performance counters on the screen (frame timing and draw call count) +#define PERFORMANCE_COUNTERS 0 + +// Compound calls are calls that are made up of multiple subcalls (e.g. draw_hline, which is made up of a draw_box call) +#define INCLUDE_COMPOUNT_CALLS 0 #include "../../menu/game/types.h" // includes e3v2/marlinui/game.h #include "../../lcdprint.h" #include "lcdprint_dwin.h" #include "marlinui_dwin.h" -#if PERFORMANCE_COUNTERS == 1 - static uint32_t draw_call_cnt = 0; // total number of draw calls in the current frame - static millis_t frame_draw_millis = 0, // time spent drawing the frame - frame_wait_millis = 0; // time spent waiting for the next frame +#if ENABLED(PERFORMANCE_COUNTERS) + static uint32_t draw_call_cnt = 0; // Total number of draw calls in the current frame + static millis_t frame_draw_millis = 0, // Time spent drawing the frame + frame_wait_millis = 0; // Time spent waiting for the next frame - #define DRAW_CALL_INC(subcall_cnt) draw_call_cnt++ + #define COUNT_DRAW_CALL(compound_call_count) TERN(INCLUDE_COMPOUNT_CALLS, draw_call_cnt++, draw_call_cnt = draw_call_cnt + 1 - compound_call_count) #else - #define DRAW_CALL_INC(subcall_cnt) + #define COUNT_DRAW_CALL(compound_call_count) #endif void MarlinGame::frame_start() { - // clear the screen before each frame + // Clear the screen before each frame //dwinFrameClear(CLEAR_COLOR); - // filling the play area should be faster than clearing the whole screen + // Filling the play area is faster than clearing the whole screen const uint16_t fg = dwin_font.fg; dwin_font.fg = COLOR_BG_BLACK; draw_box(0, 0, GAME_WIDTH, GAME_HEIGHT); dwin_font.fg = fg; + // Ensure the correct font is selected dwin_font.index = DWIN_FONT_MENU; - // reset performance counters - #if PERFORMANCE_COUNTERS == 1 + // Reset the performance counters + #if ENABLED(PERFORMANCE_COUNTERS) draw_call_cnt = 0; frame_draw_millis = millis(); - frame_wait_millis = frame_draw_millis - frame_wait_millis; + frame_wait_millis = frame_draw_millis - frame_wait_millis; #endif } void MarlinGame::frame_end() { - #if PERFORMANCE_COUNTERS == 1 + #if ENABLED(PERFORMANCE_COUNTERS) const millis_t frame_wait = frame_wait_millis; frame_wait_millis = millis(); frame_draw_millis = frame_wait_millis - frame_draw_millis; - // calculate frames per deci-seconds (100 milliseconds) - const uint32_t fpds = 100 / (frame_draw_millis + frame_wait); - - // format the performance counters as a string + // Format the performance counters as a string char perf_str[64]; sprintf_P( perf_str, - PSTR("d%04lu w%04lu c%04lu f%02lu "), + PSTR("d%04lu w%04lu c%04lu "), frame_draw_millis, frame_wait, - draw_call_cnt, - fpds + draw_call_cnt ); - // draw the performance counters at the (physical) origin of the screen + // Draw the performance counters at the (physical) origin of the screen const uint16_t fg = dwin_font.fg; const bool solid = dwin_font.solid; - dwin_font.fg = RGB(0x1F, 0x3F, 0x00); + set_color(color::YELLOW); dwin_font.solid = true; lcd_moveto_xy(0, 0); @@ -107,17 +108,17 @@ void MarlinGame::set_color(const color color) { } void MarlinGame::draw_hline(const game_dim_t x, const game_dim_t y, const game_dim_t w) { - // draw lines as boxes, since DWIN lines are always 1px wide but we want to scale them + // Draw lines as boxes, since DWIN lines are always 1px wide but we want to scale them draw_box(x, y, w, 1); - DRAW_CALL_INC(1); + COUNT_DRAW_CALL(1); } void MarlinGame::draw_vline(const game_dim_t x, const game_dim_t y, const game_dim_t h) { - // draw lines as boxes, since DWIN lines are always 1px wide but we want to scale them + // Draw lines as boxes, since DWIN lines are always 1px wide but we want to scale them draw_box(x, y, 1, h); - DRAW_CALL_INC(1); + COUNT_DRAW_CALL(1); } void MarlinGame::draw_frame(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { @@ -130,7 +131,7 @@ void MarlinGame::draw_frame(const game_dim_t x, const game_dim_t y, const game_d dwin_game::game_to_screen(h) ); - DRAW_CALL_INC(0); + COUNT_DRAW_CALL(0); } void MarlinGame::draw_box(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { @@ -143,17 +144,17 @@ void MarlinGame::draw_box(const game_dim_t x, const game_dim_t y, const game_dim dwin_game::game_to_screen(h) ); - DRAW_CALL_INC(0); + COUNT_DRAW_CALL(0); } void MarlinGame::draw_pixel(const game_dim_t x, const game_dim_t y) { - // draw pixels using boxes. - // while DWIN protocol supports drawing points with different sizes, the - // 0x02 'draw point' command is way slower per pixel than 0x05 'fill rectangle' + // Draw pixels using boxes. + // While DWIN protocol supports drawing points with different sizes, the + // 0x02 'draw point' command is slower per pixel than 0x05 'fill rectangle' // (0.4 us vs 0.14 us per pixel) draw_box(x, y, 1, 1); - DRAW_CALL_INC(1); + COUNT_DRAW_CALL(1); } void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_dim_t bytes_per_row, const game_dim_t rows, const pgm_bitmap_t bitmap) { @@ -163,20 +164,19 @@ void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_ #if DISABLED(TJC_DISPLAY) // DWIN T5UI actually supports drawing multiple points in one go using the 0x02 'draw point' command, ever since kernel 1.2. - // So we use that to draw the bitmap as a series of points, which should be faster than drawing rectangles using draw_pixel. - // This will be somewhat slow, but way faster than drawing rectangles one by one. + // So we use that to draw the bitmap as a series of points, which is faster than drawing rectangles using draw_pixel. dwinDrawPointMap( - dwin_font.fg, // color - dwin_game::game_to_screen(1), // point size + dwin_font.fg, + dwin_game::game_to_screen(1), dwin_game::game_to_screen(1), - dwin_game::game_to_screen(x) + dwin_game::x_offset, // x / y + dwin_game::game_to_screen(x) + dwin_game::x_offset, dwin_game::game_to_screen(y) + dwin_game::y_offset, - bytes_per_row, // bitmap dimensions + bytes_per_row, rows, - bitmap // U8G bitmap format is compatible to DrawPointMap format + bitmap ); - DRAW_CALL_INC(0); + COUNT_DRAW_CALL(0); #else // TJC displays don't seem to support the 0x02 'draw point' command, so instead we have to draw the bitmap // as a series of rectangles using draw_pixel. @@ -185,10 +185,10 @@ void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_ for (game_dim_t col = 0; col < bytes_per_row; col++) { const uint8_t byte = bitmap[(row * bytes_per_row) + col]; for (uint8_t bit = 0; bit < 8; bit++) { - // assume that the screen area is cleared before drawing + // Assuming that the drawing area was cleared before drawing if (byte & (1 << bit)) { draw_pixel(x + (col * 8) + (7 - bit + 1), y + row); - DRAW_CALL_INC(1); + COUNT_DRAW_CALL(1); } } } @@ -197,7 +197,7 @@ void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_ } int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, const char* str) { - DRAW_CALL_INC(0); + COUNT_DRAW_CALL(0); lcd_moveto_xy( dwin_game::game_to_screen(x) + dwin_game::x_offset, diff --git a/Marlin/src/lcd/e3v2/marlinui/game.h b/Marlin/src/lcd/e3v2/marlinui/game.h index ddd402e6fc82..de988f9a4908 100644 --- a/Marlin/src/lcd/e3v2/marlinui/game.h +++ b/Marlin/src/lcd/e3v2/marlinui/game.h @@ -15,13 +15,13 @@ namespace dwin_game { constexpr int calculate_scale() { - // use whichever is smaller: the width or height scaling factor + // Use whichever is smaller: the width or height scaling factor float scaling_factor = _MIN( static_cast(DWIN_WIDTH) / static_cast(TARGET_WIDTH), static_cast(DWIN_HEIGHT) / static_cast(TARGET_HEIGHT) ); - // round DOWN to closest integer + // Round DOWN to closest integer return static_cast(scaling_factor); } @@ -31,14 +31,14 @@ namespace dwin_game { constexpr int scale = calculate_scale(); /** - * @brief scale a game dimension to screen dimensions + * @brief Scale a game dimension to screen dimensions */ constexpr game_dim_t screen_to_game(const screen_dim_t x) { return x / scale; } /** - * @brief scale a screen dimension to game dimensions + * @brief Scale a screen dimension to game dimensions */ constexpr screen_dim_t game_to_screen(const game_dim_t x) { return x * scale; @@ -59,7 +59,7 @@ constexpr game_dim_t GAME_HEIGHT = dwin_game::screen_to_game(DWIN_HEIGHT - (dwin constexpr game_dim_t GAME_FONT_WIDTH = dwin_game::screen_to_game(MENU_FONT_WIDTH); constexpr game_dim_t GAME_FONT_ASCENT = dwin_game::screen_to_game(MENU_FONT_ASCENT); -// not needed on DWIN +// DWIN screens don't page, so these macros are always true #define PAGE_OVER(ya) true #define PAGE_UNDER(yb) true #define PAGE_CONTAINS(ya, yb) true diff --git a/Marlin/src/lcd/menu/game/brickout.cpp b/Marlin/src/lcd/menu/game/brickout.cpp index 83c74d82f449..f4f9837ba445 100644 --- a/Marlin/src/lcd/menu/game/brickout.cpp +++ b/Marlin/src/lcd/menu/game/brickout.cpp @@ -144,25 +144,21 @@ void BrickoutGame::game_screen() { const uint8_t yy = y * BRICK_H + BRICK_TOP; if (PAGE_CONTAINS(yy, yy + BRICK_H - 1)) { for (uint8_t x = 0; x < BRICK_COLS; ++x) { - // cycle through colors, even if the brick is gone - // otherwise, bricks would change color if their neighbor is hit + // Cycle through colors, even if the brick is gone. + // Otherwise, bricks would change color if their neighbor is hit set_color(brick_colors[color_index++ % COUNT(brick_colors)]); - // draw brick if it's still there + // Draw brick if it's still there if (TEST(bdat.bricks[y], x)) { const uint8_t xx = x * BRICK_W; draw_box(xx, yy, BRICK_W - 1, BRICK_H - 1); - - //for (uint8_t v = 0; v < BRICK_H - 1; ++v) - // if (PAGE_CONTAINS(yy + v, yy + v)) - // draw_hline(xx, yy + v, BRICK_W - 1); } } } } } - // everything else is white + // Everything else is white set_color(color::WHITE); // Draw paddle diff --git a/Marlin/src/lcd/menu/game/invaders.cpp b/Marlin/src/lcd/menu/game/invaders.cpp index 1f884ddb31e6..a77137fa614e 100644 --- a/Marlin/src/lcd/menu/game/invaders.cpp +++ b/Marlin/src/lcd/menu/game/invaders.cpp @@ -51,8 +51,8 @@ #define INVADER_COLOR { MarlinGame::color::GREEN, MarlinGame::color::CYAN, MarlinGame::color::YELLOW } #define CANNON_COLOR MarlinGame::color::WHITE -#define LASER_COLOR MarlinGame::color::WHITE // shot by player -#define BULLET_COLOR LASER_COLOR // shot by invader +#define LASER_COLOR MarlinGame::color::WHITE // Shot by player +#define BULLET_COLOR LASER_COLOR // Shot by invader #define LIFE_COLOR CANNON_COLOR #define UFO_COLOR MarlinGame::color::MAGENTA #define EXPLOSION_COLOR MarlinGame::color::RED @@ -386,8 +386,8 @@ void InvadersGame::game_screen() { int8_t xx = idat.pos.x; for (uint8_t x = 0; x < INVADER_COLS; ++x) { if (TEST(idat.bugs[y], x)) { - constexpr color type_color[] = INVADER_COLOR; - set_color(type_color[type]); + constexpr color invader_color[] = INVADER_COLOR; + set_color(invader_color[type]); draw_bitmap(xx, yy, 2, INVADER_H, invader[type][idat.game_blink]); } xx += INVADER_COL_W; @@ -433,22 +433,20 @@ void InvadersGame::game_screen() { set_color(color::WHITE); // Blink GAME OVER when game is over - if (!game_state) { - draw_game_over(); - } + if (!game_state) draw_game_over(); if (PAGE_UNDER(GAME_FONT_ASCENT - 1)) { - // Draw Score - //const uint8_t sx = (GAME_WIDTH - (score >= 10 ? score >= 100 ? score >= 1000 ? 4 : 3 : 2 : 1) * GAME_FONT_WIDTH) / 2; - constexpr uint8_t sx = 0; - draw_int(sx, GAME_FONT_ASCENT - 1, score); - // Draw lives if (idat.cannons_left) for (uint8_t i = 1; i <= idat.cannons_left; ++i) { set_color(LIFE_COLOR); draw_bitmap(GAME_WIDTH - i * (LIFE_W), 6 - (LIFE_H), 1, LIFE_H, life); } + + // Draw Score + //const uint8_t sx = (GAME_WIDTH - (score >= 10 ? score >= 100 ? score >= 1000 ? 4 : 3 : 2 : 1) * GAME_FONT_WIDTH) / 2; + constexpr uint8_t sx = 0; + draw_int(sx, GAME_FONT_ASCENT - 1, score); } frame_end(); diff --git a/Marlin/src/lcd/menu/game/types.h b/Marlin/src/lcd/menu/game/types.h index f9947847dd5f..9c6c4776e4d4 100644 --- a/Marlin/src/lcd/menu/game/types.h +++ b/Marlin/src/lcd/menu/game/types.h @@ -54,25 +54,18 @@ class MarlinGame { static void init_game(const uint8_t init_state, const screenFunc_t screen); // - // Render API, based on U8GLib - // draw functions are implemented by the screen-specific renderer + // Render API, based on U8GLib. + // draw_* functions are implemented by the screen-specific renderer // public: /** * @brief The colors available for drawing games. - * @note If a screen doesn't support (a) color, it shall fall back to using WHITE. + * @note If a screen doesn't support (a) color, it is expected to map to the closest + * available color OR white if the closest available color is (near) black. */ enum class color { - /** - * @brief Black color. This is guaranteed to be the clear color on all screens. - */ BLACK, - - /** - * @brief White color. Guranteed to be white on all screens. - */ WHITE, - RED, GREEN, BLUE, @@ -162,6 +155,9 @@ class MarlinGame { * @param str The string to draw. * @see lcd_moveto + lcd_put_u8str * @note The font size is available using the GAME_FONT_WIDTH and GAME_FONT_ASCENT constants. + * + * @note On the DWIN renderer, strings may flush the screen, which may cause flickering. + * Consider drawing strings after all other elements have been drawn. */ static int draw_string(const game_dim_t x, const game_dim_t y, const char *str); static int draw_string(const game_dim_t x, const game_dim_t y, FSTR_P const str); @@ -173,6 +169,9 @@ class MarlinGame { * @param value The integer to draw. * @see lcd_put_int * @note The font size is available using the GAME_FONT_WIDTH and GAME_FONT_ASCENT constants. + * + * @note On the DWIN renderer, strings may flush the screen, which may cause flickering. + * Consider drawing strings after all other elements have been drawn. */ static void draw_int(const game_dim_t x, const game_dim_t y, const int value); };