Porting Arduboy2 library to SAMD

@MLXXXp I’ve had a look in the docs but I don’t think there’s enough there to help me over this hump. In both Arduboy2.cpp and Sprites.cpp there is assembly language. In Arduboy.cpp for the Z version you used a workaround in fillScreen() but to me there’s no obvious equivalent of the Sprite

  asm volatile(
    "push r28\n" // save Y
    "push r29\n"
    "movw r28, %[buffer_ofs]\n" // Y = buffer_ofs_2
    "adiw r28, 63\n" // buffer_ofs_2 = buffer_ofs + 128
    "adiw r28, 63\n"
    "adiw r28, 2\n"
    "loop_y:\n"
    "loop_x:\n"
    // load bitmap and mask data
    "lpm %A[bitmap_data], Z+\n"
    "lpm %A[mask_data], Z+\n"

    // shift mask and buffer data
    "tst %[yOffset]\n"
    "breq skip_shifting\n"
    "mul %A[bitmap_data], %[mul_amt]\n"
    "movw %[bitmap_data], r0\n"
    "mul %A[mask_data], %[mul_amt]\n"
    "movw %[mask_data], r0\n"

    // SECOND PAGE
    // if yOffset != 0 && sRow < 7
    "cpi %[sRow], 7\n"
    "brge end_second_page\n"
    // then
    "ld %[data], Y\n"
    "com %B[mask_data]\n" // invert high byte of mask
    "and %[data], %B[mask_data]\n"
    "or %[data], %B[bitmap_data]\n"
    // update buffer, increment
    "st Y+, %[data]\n"

    "end_second_page:\n"
    "skip_shifting:\n"

    // FIRST PAGE
    // if sRow >= 0
    "tst %[sRow]\n"
    "brmi skip_first_page\n"
    "ld %[data], %a[buffer_ofs]\n"
    // then
    "com %A[mask_data]\n"
    "and %[data], %A[mask_data]\n"
    "or %[data], %A[bitmap_data]\n"
    // update buffer, increment
    "st %a[buffer_ofs]+, %[data]\n"
    "jmp end_first_page\n"

    "skip_first_page:\n"
    // since no ST Z+ when skipped we need to do this manually
    "adiw %[buffer_ofs], 1\n"

    "end_first_page:\n"

    // "x_loop_next:\n"
    "dec %[xi]\n"
    "brne loop_x\n"

    // increment y
    "next_loop_y:\n"
    "dec %[yi]\n"
    "breq finished\n"
    "mov %[xi], %[x_count]\n" // reset x counter
    // sRow++;
    "inc %[sRow]\n"
    "clr __zero_reg__\n"
    // sprite_ofs += (w - rendered_width) * 2;
    "add %A[sprite_ofs], %A[sprite_ofs_jump]\n"
    "adc %B[sprite_ofs], __zero_reg__\n"
    // buffer_ofs += WIDTH - rendered_width;
    "add %A[buffer_ofs], %A[buffer_ofs_jump]\n"
    "adc %B[buffer_ofs], __zero_reg__\n"
    // buffer_ofs_page_2 += WIDTH - rendered_width;
    "add r28, %A[buffer_ofs_jump]\n"
    "adc r29, __zero_reg__\n"

    "rjmp loop_y\n"
    "finished:\n"
    // put the Y register back in place
    "pop r29\n"
    "pop r28\n"
    "clr __zero_reg__\n" // just in case
    : [xi] "+&a" (xi),
    [yi] "+&a" (loop_h),
    [sRow] "+&a" (sRow), // CPI requires an upper register (r16-r23)
    [data] "=&l" (data),
    [mask_data] "=&l" (mask_data),
    [bitmap_data] "=&l" (bitmap_data)
    :
    [screen_width] "M" (WIDTH),
    [x_count] "l" (rendered_width), // lower register
    [sprite_ofs] "z" (bofs),
    [buffer_ofs] "x" (Arduboy2Base::sBuffer+ofs),
    [buffer_ofs_jump] "a" (WIDTH-rendered_width), // upper reg (r16-r23)
    [sprite_ofs_jump] "a" ((w-rendered_width)*2), // upper reg (r16-r23)

    // [sprite_ofs_jump] "r" (0),
    [yOffset] "l" (yOffset), // lower register
    [mul_amt] "l" (mul_amt) // lower register
    // NOTE: We also clobber r28 and r29 (y) but sometimes the compiler
    // won't allow us, so in order to make this work we don't tell it
    // that we clobber them. Instead, we push/pop to preserve them.
    // Then we need to guarantee that the the compiler doesn't put one of
    // our own variables into r28/r29.
    // We do that by specifying all the inputs and outputs use either
    // lower registers (l) or simple (r16-r23) upper registers (a).
    : // pushes/clobbers/pops r28 and r29 (y)
  ); 

The docs say Sprites and SpritesB do the same role but with different optimisations (speed, code size, respectively). Does this mean I can put a preprocessor macro condition in front the assembly above such as

#ifdef ARDUBOY_SAMD // from 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); // convert variables to work in Sprites
#else

and then #endif after the assembly?

Why doesn’t Arduboy grow to the next stage yet? Why does it seem like nothing is happening?

Real Talk Contained Within

Kind of the plan with the Arduboy FX, to sell the modchip to people who already have an Arduboy, or rework the remaining inventory to include all the games. Had a plan to give a way 1,000 dev kits of the FX setup co-branded with hackster.io but Seeed totally screwed me and quoted me like $27 each to produce them, despite only being about $4 BOM. I didn’t have time, or wasn’t smart enough in advance to look for another manufacturer.

Also, the first kickstarter was a complete fluke, the market for these kinds of things is not that big. I think I’d struggle to even sell through what I’ve got even with the kickstarter.

Keep in mind the special edition only sold about 700 units on the campaign.

Without some kind of distribution model or financial partner to subsidize it going into bigger markets (i.e. investment to make enough units to really bring the price down to impulse purchase territory) , the idea for Arduboy as a finished good hardware is not a viable business model.

Kit form is still an attractive option and I’ve got some ideas for the emulator too… But frankly the only reason Arduboy made it farther than anyone else doing this kind of stuff (pokitto, gamebuino, etc) is that it had the advantage of going viral a year before the origonal campaign and generated nearly a billion impressions, and got day 0 coverage on huffington post and gizmodo. Those days are over my friends.

The long term vision for Arduboy is to both fully democratize the platform as much as possible and also one last hurrah at getting it into education markets. But also to be totally 100% transparent it doesn’t even make enough money for me to pay myself a living wage any more. The only reason the company didn’t go bankrupt 2 years ago is because I’ve been living at my parents house, or girlfriends house. #realtalk #business

I’m actually looking for a new job myself so that I can continue to do Arduboy without relying on it as a primary source of income.

I guess this is sort of “too much info” for this topic, but it’s a little heartbreaking every time someone brings this sort of thing up.

I’m actually considering making a “arduboy is dead, long live arduboy” video and pushing it out, but I’m trying to resolve another issue critical to the business first. I also want to finish the Arduboy FX mod chip because I see that being the best way to give back to the developers is let customers have the easiest access possible to the games they made.

Woof :dog:

EDIT: After writing this I realized I should also give equal credit the reason Arduboy has existed at all or so long is really to you, and all of the developers and everyone continuously believing in the platform and developing for it. But nothing lasts forever.

5 Likes

Have a look in my Pokitto port, I replaced almost all of the assembly from the original:

For Sprites.cpp you can just substitute the code from SpritesB.cpp.

1 Like

Hey, I didn’t mean to do that - sorry. We all know the “what ifs” outweigh the “got dones” by an immeasurable amount.

Although it won’t pay your wages forever, I think you’ve achieved something very few have - a conceptual “standard”. Yes, the SW and HW may well be fragmented but there’s a core concept of simple monochrome handheld games that seems to have coalesced here rather than anywhere else. I hope you are able to make enough to keep this community website going for many years to come.

I also applaud the move into education. I think your idea for a $5 Arduboy is great. I want my children to have stuff like Arduboy to learn and play with and I want other people’s children to as well.

If I could make a suggestion with no intention of making you feel bad: given your outlook as captured in that message above, I think what I have said before applies. Accepting the usual caveats about design by committee, I think you could afford to crowdsource some of your hardware engineering work from the community a bit more than perhaps you have done in the past. Maybe that will help make space to transition to whatever is next.

Long live Arduboy!

1 Like

Will do, thank you!

1 Like

Theoretically you could even just scrap Sprites.cpp and just make Sprites.h consist of:

#ifndef Sprites_h
#define Sprites_h

#include "SpritesB.h"
using Sprites = SpritesB;

#endif

I’m not sure how fast SAMD51 is, but I’m assuming it would be fast enough to match the pace that the assembly in Sprites.cpp managed on Arduboy.

Maybe something more original would be better in case someone else attempts to build something SAMD based?

It depends what your goal is though, whether you’re just trying to make something that works for your purposes or whether you’re trying to make something that could work for multiple SAMD boards/configurations.

Edit:
From the way you’ve structured the code so far it looks as if you’re trying to make sure the code still compiles for Arduboy.
Is that the case?

I’ll hopefully be able to make some contributions at least.
I said I would back when this project was last discussed and I’ll stick to my work.

I still haven’t quite got round to finishing my Pokitto port of Arduboy2 for various reasons,
but I did spend a long time sifting through all the Arduboy2 functions so I know the API in quite good detail.

As I said a while back, I think your first priority should be making a list of all the functions you’ll need to reimplement so you can decide what to focus on and keep track of how far you have left to go.


Edit:
Glad to see you remembered my tip about the // TODO: comments. They’ll come in handy.

1 Like

My hope is that changes could be merged to provide a single library at some point.

Yeah too bad. I also had it refunded. I haven’t made up my mind yet if I’m gonna buy the SparkFun one or Adafruits itsybitsy M4 Express, or hack the Adafruit PyBadge LC

Can I ask you, as a connoisseur of hardware, what screen would you pair with the itsybitsy m4? I’ve used one to develop for work and they’re nice.

Not fully settled yet if it will be 128x128 OLED with 4-bit greys or 160x128 Color OLED.

2 Likes

That was my plan when I modified the Arduboy library. I wanted you to be able to select the board type in the IDE and then be able to compile any sketch, written for the original Arduboy, for the new hardware, without even having to change the includes.

For places where there ends up too much #if interleaving, it could be broken up into separate library source files that are conditionally included.

1 Like

I had gathered as much, but the reason I find it notable in this case is because it raises the point of whether it’s worth the trouble - i.e. is it likely that @SimonMerrett’s SAMD port/adaptation will be merged with the original?

When I started my Pokitto port I started under the assumption that my changes wouldn’t make it back into the original, and thus the Pokitto port would exist as a standalone version.

I was about to suggest something along these lines.
I’ve had to do something similar in the past to account for compiler-specific attributes.
(Having standardised attribute syntax has made this much easier in recent years.)

One obvious candidate for separation is the pin definitions.
Having something along the lines of:

#if defined(AB_DEVKIT)
#include "Pins_Devkit.h"
#elif defined(ARDUBOY_10)
#include "Pins_Arduboy.h"
#elif defined(ARDUBOY_SAMD51)
#include "Pins_SAMD51.h"
#endif

Would save a lot of space in ArduboyCore.h.


But if something like this is the goal, then it’s also a chance to reevaluate how much of the API is platform dependent and question whether it’s worth maybe introducing some new functions and/or macros to account for platform specific behaviours and thus make it easier to implement new platforms.

E.g. should TXLED0 be wrapped in some kind of TXLedOff() macro/function which is to be implemented by each platform?

1 Like

That would be the plan, from my point of view. But it would only be merged back in if some kind of official (or at least standardised and popular) Arduboy successor, based on this arcitecture, came to light. Developing with that in mind is a good goal, though.

Those kind of optimizations could be ongoing.

2 Likes

Good. I’m happy for more experienced people to be the architects (for a grand title) and make librarian decisions. I’m just happy when I scroll down to the next compiler error and after half an hour manage to work out that Arduboy2Beep.cpp needs to #include Arduboy2.h if I’m going to splatter it with preprocessor macros.

1 Like

I think I you need a crash course in the C++ build process and what the words ‘translation unit’ mean. :P

1 Like

I think the 4 bit grayscale and the 128x128 display is best. There is also a 128x128 color oled too, and actually that one might even be cheapest, as it’s most available I think.

And all said and done, if people have a something that works and we want to try and do a campaign, I’m game, I’ll do my best to promote it but the goal of the campaign would have to be like $100k or that ballpark. From rough estimations.

That of course changes if it’s only the PCB. That’s easy. It’s the plastic parts that are tough.

2 Likes

Amen. The enclosure is critical for a “consumer” product, nigh on irrelevant for tinkerers/hackers and can be absolutely minimal for educational users (makers/players). The aliexpress knockoff kits are good examples of what could be perfectly acceptable to many people. There are also options for small lifespan moulds using aluminium or silicone that I’ve had very reasonable quotes for injection moulded parts in the past based on. I’m sure there are options in this space if people want to bring something to life at scales of 10+.

Thanks for your support, I realise that you have significant business pressures at the moment and hope they ease off soon.

Excellent - I was looking to see if I had put my Arduboy2.h #includes and #ifdefs in the wrong order but your helpful PM has cleared that up for me. Please point out other specific issues you see me creating, although I may struggle to digest all the terms in the links you sent me!

Could we add in the #ifdef ARDUBOY_SAMD to Sprites.h and use the alias conditionally? On my phone atm but will post code of what I mean in a while.

1 Like

The most important parts are to understand the preprocessing phase and realise that every .cpp file is a separate translation unit that only sees what it includes.

With those two things in mind it should be a lot easier to understand what order you need to define things in.

(I’d like to point out that actually #defines aren’t the best way of implementing most of these things,
but that’s what Arduboy2’s already using so that’s the approach we’re stuck with.)

#ifndef Sprites_h
#define Sprites_h

#include "Arduboy2Core.h"

#if defined(ARDUBOY_SAMD)
#include "SpritesB.h"

using Sprites = SpritesB;
#else
// Regular definition of Sprites
#endif

#endif

Although it might be better to do:

#ifndef Sprites_h
#define Sprites_h

#include "Arduboy2Core.h"

#if !defined(/*Some AVR specific macro*/)
#include "SpritesB.h"

using Sprites = SpritesB;
#else
// Regular definition of Sprites (using AVR assembly)
#endif

#endif

Strictly speaking SpritesB is actually the ‘base case’,
Sprites is the special platform-specific version,
so it makes sense to make the alias the default implementation,
and make the more detailed definition the ‘AVR only’ implementation.

1 Like

I’m trying to do the “better” version. A bit stuck. BTW, Sprites.h doesn’t #include "Arduboy2Core.h", in the version I have it #include "Arduboy2.h".

Here’s where I have got to in Sprites.h:

#ifndef Sprites_h
#define Sprites_h

#include "Arduboy2.h"
#include "SpritesCommon.h"

#if !defined(ARDUINO_ARCH_AVR)
	#include "SpritesB.h"

using Sprites = SpritesB;
#else

What confuses me is

because the assembly is in Sprites.cpp. Or do we just put the function declarations next in Sprites.h and the double #endif right at the bottom, like

/**
 * @file Sprites.h
 * \brief
 * A class for drawing animated sprites from image and mask bitmaps.
 */

#ifndef Sprites_h
#define Sprites_h

#include "Arduboy2.h"
#include "SpritesCommon.h"

// TODO: this section is experimental to address the issue of assembly being used in Sprites.cpp
#if !defined(ARDUINO_ARCH_AVR)
#include "SpritesB.h"

using Sprites = SpritesB;
#else
//end of experimental section - need to remove the additional #endif at the end of the file below if removing the experimental secion.

/** \brief
 * A class for drawing animated sprites from image and mask bitmaps.
 *
 * \details
 * The functions in this class will draw to the screen buffer an image
 * contained in an array located in program memory. A mask can also be
 * specified or implied, which dictates how existing pixels in the buffer,
 * within the image boundaries, will be affected.
 *
 * A sprite or mask array contains one or more "frames". Each frame is intended
 * to show whatever the sprite represents in a different position, such as the
 * various poses for a running or jumping character. By specifying a different
 * frame each time the sprite is drawn, it can be animated.
 *
 * Each image array begins with values for the width and height of the sprite,
 * in pixels. The width can be any value. The height must be a multiple of
 * 8 pixels, but with proper masking, a sprite of any height can be created.
 *
 * For a separate mask array, as is used with `drawExternalMask()`, the width
 * and height are not included but must contain data of the same dimensions
 * as the corresponding image array.
 *
 * Following the width and height values for an image array, or from the
 * beginning of a separate mask array, the array contains the image and/or
 * mask data for each frame. Each byte represents a vertical column of 8 pixels
 * with the least significant bit (bit 0) at the top. The bytes are drawn as
 * 8 pixel high rows from left to right, top to bottom. When the end of a row
 * is reached, as specified by the width value, the next byte in the array will
 * be the start of the next row.
 *
 * 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
{
  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).
     *
     * \details
     * An array containing the image frames, and another array containing
     * corresponding mask frames, are used to draw a sprite.
     *
     * For the mask array, the width and height are not included but must
     * contain data of the same dimensions as the corresponding image array.
     *
     * Bits set to 1 in the mask indicate that the pixel will be set to the
     * value of the corresponding image bit. Bits set to 0 in the mask will be
     * left unchanged.
     *
     *     image  mask   before  after  (# = 1, - = 0)
     *
     *     -----  -###-  -----   -----
     *     --#--  #####  -----   --#--
     *     ##-##  ##-##  -----   ##-##
     *     --#--  #####  -----   --#--
     *     -----  -###-  -----   -----
     *
     *     image  mask   before  after
     *
     *     -----  -###-  #####   #---#
     *     --#--  #####  #####   --#--
     *     ##-##  #####  #####   ##-##
     *     --#--  #####  #####   --#--
     *     -----  -###-  #####   #---#
     */
    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.
     *
     * \details
     * An array containing combined image and mask data is used to draw a
     * sprite. Bytes are given in pairs with the first byte representing the
     * image pixels and the second byte specifying the corresponding mask.
     * The width given in the array still specifies the image width, so each
     * row of image and mask bytes will be twice the width value.
     *
     * Bits set to 1 in the mask indicate that the pixel will be set to the
     * value of the corresponding image bit. Bits set to 0 in the mask will be
     * left unchanged.
     *
     *     image  mask   before  after  (# = 1, - = 0)
     *
     *     -----  -###-  -----   -----
     *     --#--  #####  -----   --#--
     *     ##-##  ##-##  -----   ##-##
     *     --#--  #####  -----   --#--
     *     -----  -###-  -----   -----
     *
     *     image  mask   before  after
     *
     *     -----  -###-  #####   #---#
     *     --#--  #####  #####   --#--
     *     ##-##  #####  #####   ##-##
     *     --#--  #####  #####   --#--
     *     -----  -###-  #####   #---#
     */
    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.
     *
     * \details
     * A sprite is drawn by overwriting the pixels in the buffer with the data
     * from the specified frame in the array. No masking is done. A bit set
     * to 1 in the frame will set the pixel to 1 in the buffer, and a 0 in the
     * array will set a 0 in the buffer.
     *
     *     image  before  after  (# = 1, - = 0)
     *
     *     -----  -----   -----
     *     --#--  -----   --#--
     *     ##-##  -----   ##-##
     *     --#--  -----   --#--
     *     -----  -----   -----
     *
     *     image  before  after
     *
     *     -----  #####   -----
     *     --#--  #####   --#--
     *     ##-##  #####   ##-##
     *     --#--  #####   --#--
     *     -----  #####   -----
     */
    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.
     *
     * \details
     * The data from the specified frame in the array is used to erase a
     * sprite. To "erase" a sprite, bits set to 1 in the frame will set the
     * corresponding pixel in the buffer to 0. Frame bits set to 0 will remain
     * unchanged in the buffer.
     *
     *     image  before  after  (# = 1, - = 0)
     *
     *     -----  -----   -----
     *     --#--  -----   -----
     *     ##-##  -----   -----
     *     --#--  -----   -----
     *     -----  -----   -----
     *
     *     image  before  after
     *
     *     -----  #####   #####
     *     --#--  #####   ##-##
     *     ##-##  #####   --#--
     *     --#--  #####   ##-##
     *     -----  #####   #####
     */
    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.
     *
     * \details
     * Bits set to 1 in the frame will be used to draw the sprite by setting
     * the corresponding pixel in the buffer to 1. Bits set to 0 in the frame
     * will remain unchanged in the buffer.
     *
     *     image  before  after  (# = 1, - = 0)
     *
     *     -----  -----   -----
     *     --#--  -----   --#--
     *     ##-##  -----   ##-##
     *     --#--  -----   --#--
     *     -----  -----   -----
     *
     *     image  before  after
     *
     *     -----  #####   #####  (no change because all pixels were
     *     --#--  #####   #####  already white)
     *     ##-##  #####   #####
     *     --#--  #####   #####
     *     -----  #####   #####
     */
    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 // experimental section
#endif