From da2c7e3d35e870771d51f1683fc5af57b9a77858 Mon Sep 17 00:00:00 2001 From: Delio Brignoli Date: Wed, 14 Mar 2018 10:24:53 -0400 Subject: [PATCH] Add SpritesB class as alternative to Sprites The SpritesB class has functions identical to those in Sprites. When used in place of Sprites it will usually reduce code size (at the expense of slower execution speed). Code size reduction is accomplished by factoring all draw modes into the same loop. Modifications made to the Sprites class to create this class made by @dxxb (Delio Brignoli). Creation of the SpritesB class and documentation by @MLXXXp (Scott Allen). Also, bootLogoSpritesBSelfMasked() and bootLogoSpritesBOverwrite() alternative boot logo functions added by @MLXXXp. --- README.md | 15 +++- keywords.txt | 3 + src/Arduboy2.cpp | 24 +++++- src/Arduboy2.h | 33 ++++++++- src/Sprites.cpp | 3 +- src/Sprites.h | 35 ++++++--- src/SpritesB.cpp | 176 ++++++++++++++++++++++++++++++++++++++++++++ src/SpritesB.h | 116 +++++++++++++++++++++++++++++ src/SpritesCommon.h | 18 +++++ 9 files changed, 406 insertions(+), 17 deletions(-) create mode 100644 src/SpritesB.cpp create mode 100644 src/SpritesB.h create mode 100644 src/SpritesCommon.h diff --git a/README.md b/README.md index 264fabf..4ed3d5d 100644 --- a/README.md +++ b/README.md @@ -217,10 +217,23 @@ This saves whatever code *blank()*, *systemButtons()*, *bootLogo()* and *waitNoB There are a few functions provided that are roughly equivalent to the standard functions used by *begin()* but which use less code space. -- *bootLogoCompressed()*, *bootLogoSpritesSelfMasked()* and *bootLogoSpritesOverwrite()* will do the same as *bootLogo()* but will use *drawCompressed()*, or *Sprites* class *drawSelfMasked()* or *drawOverwrite()*, functions respectively, instead of *drawBitmask()*, to render the logo. If the sketch uses one of these functions, then using the boot logo function that also uses it may reduce code size. It's best to try each of them to see which one produces the smallest size. +- *bootLogoCompressed()*, *bootLogoSpritesSelfMasked()*, *bootLogoSpritesOverwrite()*, *bootLogoSpritesBSelfMasked()* and *bootLogoSpritesBOverwrite()* will do the same as *bootLogo()* but will use *drawCompressed()*, or *Sprites* / *SpritesB* class *drawSelfMasked()* or *drawOverwrite()* functions respectively, instead of *drawBitmask()*, to render the logo. If the sketch uses one of these functions, then using the boot logo function that also uses it may reduce code size. It's best to try each of them to see which one produces the smallest size. - *bootLogoText()* can be used in place *bootLogo()* in the case where the sketch uses text functions. It renders the logo as text instead of as a bitmap (so doesn't look as good). - *safeMode()* can be used in place of *flashlight()* for cases where it's needed to allow uploading a new sketch when the bootloader "magic key" problem is an issue. It only lights the red RGB LED, so you don't get the bright light that is the primary purpose of *flashlight()*. +#### Use the SpritesB class instead of Sprites + +The *SpritesB* class has functions identical to the *Sprites* class. The difference is that *SpritesB* is optimized for small code size rather than execution speed. If you want to use the sprites functions, and the slower speed of *SpritesB* doesn't affect your sketch, you may be able to use it to gain some code space. + +Even if the speed is acceptable when using *SpritesB*, you should still try using *Sprites*. In some cases *Sprites* will produce less code than *SpritesB*, notably when only one of the functions is used. + +You can easily switch between using *Sprites* or *SpritesB* by using one or the other to create an object instance: + +```cpp +Sprites sprites; // Use this to optimize for execution speed +SpritesB sprites; // Use this to (likely) optimize for code size +``` + #### Eliminate the USB stack code **Warning:** Although this will free up a fair amount of code and some RAM space, without an active USB interface uploader programs will be unable to automatically force a reset to invoke the bootloader. This means the user will have to manually initiate a reset in order to upload a new sketch. This can be an inconvenience or even frustrating for a user, due to the fact that timing the sequence can sometimes be tricky. Therefore, using this technique should be considered as a last resort. If it is used, the sketch documentation should state clearly what will be involved to upload a new sketch. diff --git a/keywords.txt b/keywords.txt index 65912f0..42465c6 100644 --- a/keywords.txt +++ b/keywords.txt @@ -13,6 +13,7 @@ BeepPin2 KEYWORD1 Point KEYWORD1 Rect KEYWORD1 Sprites KEYWORD1 +SpritesB KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) @@ -25,6 +26,8 @@ boot KEYWORD2 bootLogo KEYWORD2 bootLogoCompressed KEYWORD2 bootLogoShell KEYWORD2 +bootLogoSpritesBOverwrite KEYWORD2 +bootLogoSpritesBSelfMasked KEYWORD2 bootLogoSpritesOverwrite KEYWORD2 bootLogoSpritesSelfMasked KEYWORD2 bootLogoText KEYWORD2 diff --git a/src/Arduboy2.cpp b/src/Arduboy2.cpp index 14a56b8..cf8613b 100644 --- a/src/Arduboy2.cpp +++ b/src/Arduboy2.cpp @@ -41,11 +41,13 @@ void Arduboy2Base::begin() audio.begin(); bootLogo(); - // alternative logo functions. Work the same a bootLogo() but may reduce + // alternative logo functions. Work the same as bootLogo() but may reduce // memory size if the sketch uses the same bitmap drawing function // bootLogoCompressed(); // bootLogoSpritesSelfMasked(); // bootLogoSpritesOverwrite(); +// bootLogoSpritesBSelfMasked(); +// bootLogoSpritesBOverwrite(); waitNoButtons(); // wait for all buttons to be released } @@ -135,6 +137,26 @@ void Arduboy2Base::drawLogoSpritesOverwrite(int16_t y) Sprites::drawOverwrite(20, y, arduboy_logo_sprite, 0); } +void Arduboy2Base::bootLogoSpritesBSelfMasked() +{ + bootLogoShell(drawLogoSpritesBSelfMasked); +} + +void Arduboy2Base::drawLogoSpritesBSelfMasked(int16_t y) +{ + SpritesB::drawSelfMasked(20, y, arduboy_logo_sprite, 0); +} + +void Arduboy2Base::bootLogoSpritesBOverwrite() +{ + bootLogoShell(drawLogoSpritesBOverwrite); +} + +void Arduboy2Base::drawLogoSpritesBOverwrite(int16_t y) +{ + SpritesB::drawOverwrite(20, y, arduboy_logo_sprite, 0); +} + // bootLogoText() should be kept in sync with bootLogoShell() // if changes are made to one, equivalent changes should be made to the other void Arduboy2Base::bootLogoShell(void (*drawLogo)(int16_t)) diff --git a/src/Arduboy2.h b/src/Arduboy2.h index cb7624a..37f63fa 100644 --- a/src/Arduboy2.h +++ b/src/Arduboy2.h @@ -12,6 +12,7 @@ #include "Arduboy2Core.h" #include "Arduboy2Beep.h" #include "Sprites.h" +#include "SpritesB.h" #include #include @@ -277,8 +278,7 @@ class Arduboy2Base : public Arduboy2Core void bootLogoCompressed(); /** \brief - * Display the boot logo sequence using the `Sprites` class - * `drawSelfMasked()` function. + * Display the boot logo sequence using `Sprites::drawSelfMasked()`. * * \details * This function can be called by a sketch after `boot()` as an alternative to @@ -290,8 +290,7 @@ class Arduboy2Base : public Arduboy2Core void bootLogoSpritesSelfMasked(); /** \brief - * Display the boot logo sequence using the `Sprites` class - * `drawOverwrite()` function. + * Display the boot logo sequence using `Sprites::drawOverwrite()`. * * \details * This function can be called by a sketch after `boot()` as an alternative to @@ -302,6 +301,30 @@ class Arduboy2Base : public Arduboy2Core */ void bootLogoSpritesOverwrite(); + /** \brief + * Display the boot logo sequence using `SpritesB::drawSelfMasked()`. + * + * \details + * This function can be called by a sketch after `boot()` as an alternative to + * `bootLogo()`. This may reduce code size if the sketch itself uses + * `SpritesB` class functions. + * + * \see bootLogo() begin() boot() SpritesB + */ + void bootLogoSpritesBSelfMasked(); + + /** \brief + * Display the boot logo sequence using `SpritesB::drawOverwrite()`. + * + * \details + * This function can be called by a sketch after `boot()` as an alternative to + * `bootLogo()`. This may reduce code size if the sketch itself uses + * `SpritesB` class functions. + * + * \see bootLogo() begin() boot() SpritesB + */ + void bootLogoSpritesBOverwrite(); + /** \brief * Display the boot logo sequence using the provided function * @@ -1163,6 +1186,8 @@ class Arduboy2Base : public Arduboy2Core static void drawLogoCompressed(int16_t y); static void drawLogoSpritesSelfMasked(int16_t y); static void drawLogoSpritesOverwrite(int16_t y); + static void drawLogoSpritesBSelfMasked(int16_t y); + static void drawLogoSpritesBOverwrite(int16_t y); // For button handling uint8_t currentButtonState; diff --git a/src/Sprites.cpp b/src/Sprites.cpp index 459e7f9..8b81ce3 100644 --- a/src/Sprites.cpp +++ b/src/Sprites.cpp @@ -81,13 +81,12 @@ void Sprites::drawBitmap(int16_t x, int16_t y, // xOffset technically doesn't need to be 16 bit but the math operations // are measurably faster if it is uint16_t xOffset, ofs; - int8_t yOffset = abs(y) % 8; + int8_t yOffset = y & 7; int8_t sRow = y / 8; uint8_t loop_h, start_h, rendered_width; if (y < 0 && yOffset > 0) { sRow--; - yOffset = 8 - yOffset; } // if the left side of the render is offscreen skip those loops diff --git a/src/Sprites.h b/src/Sprites.h index 109a4ac..e0ba046 100644 --- a/src/Sprites.h +++ b/src/Sprites.h @@ -8,14 +8,7 @@ #define Sprites_h #include "Arduboy2.h" - -#define SPRITE_MASKED 1 -#define SPRITE_UNMASKED 2 -#define SPRITE_OVERWRITE 2 -#define SPRITE_PLUS_MASK 3 -#define SPRITE_IS_MASK 250 -#define SPRITE_IS_MASK_ERASE 251 -#define SPRITE_AUTO_MODE 255 +#include "SpritesCommon.h" /** \brief * A class for drawing animated sprites from image and mask bitmaps. @@ -49,11 +42,35 @@ * * Data for each frame after the first one immediately follows the previous * frame. Frame numbers start at 0. - + * * \note + * \parblock + * A separate `SpritesB` class is available as an alternative to this class. + * The only difference is that the `SpritesB` class is optimized for small + * code size rather than for execution speed. One or the other can be used + * depending on whether size or speed is more important. + * + * Even if the speed is acceptable when using `SpritesB`, you should still try + * using `Sprites`. In some cases `Sprites` will produce less code than + * `SpritesB`, notably when only one of the functions is used. + * + * You can easily switch between using the `Sprites` class or the `SpritesB` + * class by using one or the other to create an object instance: + * + * \code{.cpp} + * Sprites sprites; // Use this to optimize for execution speed + * SpritesB sprites; // Use this to (likely) optimize for code size + * \endcode + * \endparblock + * + * \note + * \parblock * In the example patterns given in each Sprites function description, * a # character represents a bit set to 1 and * a - character represents a bit set to 0. + * \endparblock + * + * \see SpritesB */ class Sprites { diff --git a/src/SpritesB.cpp b/src/SpritesB.cpp new file mode 100644 index 0000000..27924ae --- /dev/null +++ b/src/SpritesB.cpp @@ -0,0 +1,176 @@ +/** + * @file SpritesB.cpp + * \brief + * A class for drawing animated sprites from image and mask bitmaps. + * Optimized for small code size. + */ + +#include "SpritesB.h" + +void SpritesB::drawExternalMask(int16_t x, int16_t y, const uint8_t *bitmap, + const uint8_t *mask, uint8_t frame, uint8_t mask_frame) +{ + draw(x, y, bitmap, frame, mask, mask_frame, SPRITE_MASKED); +} + +void SpritesB::drawOverwrite(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame) +{ + draw(x, y, bitmap, frame, NULL, 0, SPRITE_OVERWRITE); +} + +void SpritesB::drawErase(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame) +{ + draw(x, y, bitmap, frame, NULL, 0, SPRITE_IS_MASK_ERASE); +} + +void SpritesB::drawSelfMasked(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame) +{ + draw(x, y, bitmap, frame, NULL, 0, SPRITE_IS_MASK); +} + +void SpritesB::drawPlusMask(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame) +{ + draw(x, y, bitmap, frame, NULL, 0, SPRITE_PLUS_MASK); +} + + +//common functions +void SpritesB::draw(int16_t x, int16_t y, + const uint8_t *bitmap, uint8_t frame, + const uint8_t *mask, uint8_t sprite_frame, + uint8_t drawMode) +{ + unsigned int frame_offset; + + if (bitmap == NULL) + return; + + uint8_t width = pgm_read_byte(bitmap); + uint8_t height = pgm_read_byte(++bitmap); + bitmap++; + if (frame > 0 || sprite_frame > 0) { + frame_offset = (width * ( height / 8 + ( height % 8 == 0 ? 0 : 1))); + // sprite plus mask uses twice as much space for each frame + if (drawMode == SPRITE_PLUS_MASK) { + frame_offset *= 2; + } else if (mask != NULL) { + mask += sprite_frame * frame_offset; + } + bitmap += frame * frame_offset; + } + + // if we're detecting the draw mode then base it on whether a mask + // was passed as a separate object + if (drawMode == SPRITE_AUTO_MODE) { + drawMode = mask == NULL ? SPRITE_UNMASKED : SPRITE_MASKED; + } + + drawBitmap(x, y, bitmap, mask, width, height, drawMode); +} + +void SpritesB::drawBitmap(int16_t x, int16_t y, + const uint8_t *bitmap, const uint8_t *mask, + uint8_t w, uint8_t h, uint8_t draw_mode) +{ + // no need to draw at all of we're offscreen + if (x + w <= 0 || x > WIDTH - 1 || y + h <= 0 || y > HEIGHT - 1) + return; + + if (bitmap == NULL) + return; + + // xOffset technically doesn't need to be 16 bit but the math operations + // are measurably faster if it is + uint16_t xOffset, ofs; + int8_t yOffset = y & 7; + int8_t sRow = y / 8; + uint8_t loop_h, start_h, rendered_width; + + if (y < 0 && yOffset > 0) { + sRow--; + } + + // if the left side of the render is offscreen skip those loops + if (x < 0) { + xOffset = abs(x); + } else { + xOffset = 0; + } + + // if the right side of the render is offscreen skip those loops + if (x + w > WIDTH - 1) { + rendered_width = ((WIDTH - x) - xOffset); + } else { + rendered_width = (w - xOffset); + } + + // if the top side of the render is offscreen skip those loops + if (sRow < -1) { + start_h = abs(sRow) - 1; + } else { + start_h = 0; + } + + loop_h = h / 8 + (h % 8 > 0 ? 1 : 0); // divide, then round up + + // if (sRow + loop_h - 1 > (HEIGHT/8)-1) + if (sRow + loop_h > (HEIGHT / 8)) { + loop_h = (HEIGHT / 8) - sRow; + } + + // prepare variables for loops later so we can compare with 0 + // instead of comparing two variables + loop_h -= start_h; + + sRow += start_h; + ofs = (sRow * WIDTH) + x + xOffset; + + uint8_t mul_amt = 1 << yOffset; + uint16_t mask_data; + uint16_t bitmap_data; + + const uint8_t ofs_step = draw_mode == SPRITE_PLUS_MASK ? 2 : 1; + const uint8_t ofs_stride = (w - rendered_width)*ofs_step; + const uint16_t initial_bofs = ((start_h * w) + xOffset)*ofs_step; + + const uint8_t *bofs = bitmap + initial_bofs; + const uint8_t *mask_ofs = !mask ? bitmap : mask; + mask_ofs += initial_bofs + ofs_step - 1; + + for (uint8_t a = 0; a < loop_h; a++) { + for (uint8_t iCol = 0; iCol < rendered_width; iCol++) { + uint8_t data; + + bitmap_data = pgm_read_byte(bofs) * mul_amt; + mask_data = ~bitmap_data; + + if (draw_mode == SPRITE_UNMASKED) { + mask_data = ~(0xFF * mul_amt); + } else if (draw_mode == SPRITE_IS_MASK_ERASE) { + bitmap_data = 0; + } else { + mask_data = ~(pgm_read_byte(mask_ofs) * mul_amt); + } + + if (sRow >= 0) { + data = Arduboy2Base::sBuffer[ofs]; + data &= (uint8_t)(mask_data); + data |= (uint8_t)(bitmap_data); + Arduboy2Base::sBuffer[ofs] = data; + } + if (yOffset != 0 && sRow < 7) { + data = Arduboy2Base::sBuffer[ofs + WIDTH]; + data &= (*((unsigned char *) (&mask_data) + 1)); + data |= (*((unsigned char *) (&bitmap_data) + 1)); + Arduboy2Base::sBuffer[ofs + WIDTH] = data; + } + ofs++; + mask_ofs += ofs_step; + bofs += ofs_step; + } + sRow++; + bofs += ofs_stride; + mask_ofs += ofs_stride; + ofs += WIDTH - rendered_width; + } +} diff --git a/src/SpritesB.h b/src/SpritesB.h new file mode 100644 index 0000000..29a5370 --- /dev/null +++ b/src/SpritesB.h @@ -0,0 +1,116 @@ +/** + * @file SpritesB.h + * \brief + * A class for drawing animated sprites from image and mask bitmaps. + * Optimized for small code size. + */ + +#ifndef SpritesB_h +#define SpritesB_h + +#include "Arduboy2.h" +#include "SpritesCommon.h" + +/** \brief + * A class for drawing animated sprites from image and mask bitmaps. + * Optimized for small code size. + * + * \details + * The functions in this class are identical to the `Sprites` class. The only + * difference is that the functions in this class are optimized for smaller + * code size rather than execution speed. + * + * See the `Sprites` class documentation for details on the use of the + * functions in this class. + * + * Even if the speed is acceptable when using `SpritesB`, you should still try + * using `Sprites`. In some cases `Sprites` will produce less code than + * `SpritesB`, notably when only one of the functions is used. + * + * You can easily switch between using the `Sprites` class or the `SpritesB` + * class by using one or the other to create an object instance: + * + * \code{.cpp} + * Sprites sprites; // Use this to optimize for execution speed + * SpritesB sprites; // Use this to (likely) optimize for code size + * \endcode + * + * \see Sprites + */ +class SpritesB +{ + public: + /** \brief + * Draw a sprite using a separate image and mask array. + * + * \param x,y The coordinates of the top left pixel location. + * \param bitmap A pointer to the array containing the image frames. + * \param mask A pointer to the array containing the mask frames. + * \param frame The frame number of the image to draw. + * \param mask_frame The frame number for the mask to use (can be different + * from the image frame number). + * + * \see Sprites::drawExternalMask() + */ + static void drawExternalMask(int16_t x, int16_t y, const uint8_t *bitmap, + const uint8_t *mask, uint8_t frame, uint8_t mask_frame); + + /** \brief + * Draw a sprite using an array containing both image and mask values. + * + * \param x,y The coordinates of the top left pixel location. + * \param bitmap A pointer to the array containing the image/mask frames. + * \param frame The frame number of the image to draw. + * + * \see Sprites::drawPlusMask() + */ + static void drawPlusMask(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame); + + /** \brief + * Draw a sprite by replacing the existing content completely. + * + * \param x,y The coordinates of the top left pixel location. + * \param bitmap A pointer to the array containing the image frames. + * \param frame The frame number of the image to draw. + * + * \see Sprites::drawOverwrite() + */ + static void drawOverwrite(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame); + + /** \brief + * "Erase" a sprite. + * + * \param x,y The coordinates of the top left pixel location. + * \param bitmap A pointer to the array containing the image frames. + * \param frame The frame number of the image to erase. + * + * \see Sprites::drawErase() + */ + static void drawErase(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame); + + /** \brief + * Draw a sprite using only the bits set to 1. + * + * \param x,y The coordinates of the top left pixel location. + * \param bitmap A pointer to the array containing the image frames. + * \param frame The frame number of the image to draw. + * + * \see Sprites::drawSelfMasked() + */ + static void drawSelfMasked(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame); + + // Master function. Needs to be abstracted into separate function for + // every render type. + // (Not officially part of the API) + static void draw(int16_t x, int16_t y, + const uint8_t *bitmap, uint8_t frame, + const uint8_t *mask, uint8_t sprite_frame, + uint8_t drawMode); + + // (Not officially part of the API) + static void drawBitmap(int16_t x, int16_t y, + const uint8_t *bitmap, const uint8_t *mask, + uint8_t w, uint8_t h, uint8_t draw_mode); +}; + +#endif diff --git a/src/SpritesCommon.h b/src/SpritesCommon.h new file mode 100644 index 0000000..ffa7938 --- /dev/null +++ b/src/SpritesCommon.h @@ -0,0 +1,18 @@ +/** + * @file SpritesCommon.h + * \brief + * Common header file for sprite functions + */ + +#ifndef SpritesCommon_h +#define SpritesCommon_h + +#define SPRITE_MASKED 1 +#define SPRITE_UNMASKED 2 +#define SPRITE_OVERWRITE 2 +#define SPRITE_PLUS_MASK 3 +#define SPRITE_IS_MASK 250 +#define SPRITE_IS_MASK_ERASE 251 +#define SPRITE_AUTO_MODE 255 + +#endif