ArduboyG, grayscale library

I didn’t notice your changes until after I’d written most of my ISR comment (when the page 404ed when refreshed, because the file name had changed).

I’ll read the latest changes when I get chance, but that probably won’t be until Sunday now.

Calling it ArduboyG is a sensible compromise.

I’m not sure if you know, but you can actually provide bitwise operators for enum class. Though whether you think it’s worth the effort probably depends on how much you care about type safety. (I care a lot - Haskell has really spoilt me.)

Whether it actually does I couldn’t say for definite, but it’s certainly possible. It’s easier to do on Arduino because the whole program is being put into a single self-contained executable that doesn’t reference any external sources, which allows the compiler to presume that it’s impossible for any external source to modify any of the variables (or cause any similar side effects).

For future reference: constexpr automatically confers internal linkage, and namespace scope variables automatically have static storage duration, so static is redundant on namespace scope constexpr variables. (Note: ‘namespace scope’ includes the global namespace.)

It doesn’t really matter that it’s redundant, but knowing that might save you some typing next time.

1 Like

Just had a play with this library … great work!

I might look to convert one of the PPOT games - probably Turtle Bridge as it has grey throughout it already - to see how it performs. I am a curious to understand if the need to render each graphic multiple times will cause performance problems.

In your sample code, you use:

if(a.needsUpdate()) update();

What is this?

It appears to be counting frames, but your sample also uses if(!a.nextFrame()) return; as well.

    static bool needsUpdate()
    {
        if(update_counter >= update_every_n)
        {
            update_counter = 0;
            ++BASE::frameCount; // to allow everyXFrames
            return true;
        }
        return false;
    }
3 Likes

Cool, I didn’t know this!

A potentially questionable design choice… my thought was that in grayscale there’s a difference between pushing an image to the display (which happens 2 or 3 times per grayscale frame) and the complete grayscale “frame”. I guessed that a user wouldn’t want to update game state in between drawing the planes of the same image. So nextFrame indicates when to render the next plane of the current image, while needsUpdate indicates when to handle game update logic, which only happens after the final plane of an image. (Just occurred to me – should it be called nextUpdate?)

:slight_smile:

Sorry, I found that explanation a little confusing.

Let’s assume I have a single three-coloured image. I set the frame rate to 60fps.

Has the nextFrame logic changed? ie., if I use the classic if(!a.nextFrame()) return; will my loop code execute every 1/60th of a second as it used to? This is the ‘traditional’ frame behaviour.

Within that frame I need to render my images. In my testing, I created an image with three parts and saved them separately as IMG1, IMG2 and IMG3.

Can’t I just rendered them sequentially as shown below in a single frame as shown in the code below? Likewise, can’t I handle my game updates in that same frame?

This sort of suggests that I the rendering process happens over multiple ‘traditional’ frames. And …

… and this would make sense as you only want to do the update work after all planes are completed.
But is this what you mean?



#define ABG_IMPLEMENTATION
#include "ArduboyG.h"

ArduboyG a;

const uint8_t PROGMEM IMG1[] = {
0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 
};

const uint8_t PROGMEM IMG2[] = {
0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 
};

const uint8_t PROGMEM IMG3[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 
};

void setup() {
    a.begin();
    a.startGray();
}

void loop() {

    if (!a.nextFrame()) return;

    a.drawBitmap(22, 2, IMG1, 16, 12, DARK_GRAY);
    a.drawBitmap(22, 2, IMG2, 16, 12, LIGHT_GRAY);
    a.drawBitmap(22, 2, IMG3, 16, 12, WHITE);

}

Can I use the music libraries with this library? Or is the interrupt a problem?

1 Like

I think the best way to explain it would be to break down the expected order of operations.

Also, to differentiate between ‘frame’ and ‘plane’ - if I understand correctly, a greyscale ‘frame’ is finished when 2 to 3 1-bit frames have been drawn, and this is effectively a form of bitplaning, right?

In which case, I would presume nextFrame returns true 2-3 times before needsUpdate/nextUpdate does, because nextFrame would return false until all the necessary ‘planes’ have been drawn, right?

Or am I guessing incorrectly?

(I wasn’t completely following the earlier discussion, I’m just guessing what the sequence of events might be based on the contents of loop and the fact you’re still using a 1KB frame buffer.)


If that’s the case, I would presume that the ‘frame rate’ is now the ‘plane rate’?
(Which is what I think @filmote is trying to discern.)

In which case, perhaps a better arrangement would be:

void loop()
{
	if(!ardugrey.nextFrame())
		return;
		
	updateGameLogic();
		
	while(!ardugrey.allPlanesDrawn())
		drawNextPlane();
		
	ardugrey.display();
}

Or something similar.
(Which I think is the sort of thing @filmote is expecting?)


I’m reasonably confident that it would clash with ArduboyTones due to both using the same ISR:

(Unfortunately I don’t have the other sound libraries bookmarked so you’ll have to check those when you have a minute.)

It might be possible to change the sound libraries to use timer 4, but I think that would prevent it working in the emulator.

I’m not sure about the specifics of the Arduboy’s timer interrupts though, so I don’t know if they run at different frequencies or have any other limitations.

If timer 4 isn’t an option, it might be possible to get timer 3 to handle both, but that might be pushing it a bit - interrupts are supposed to be fast. (I can’t help but think that this is exactly the sort of scenario where being able to overrule the default ISR would be incredibly useful. I didn’t actually have a use-case for that before - to have one suddenly appear is a nice coincidence.)

If ISR shenanigans aren’t an option, modifying the sound library so you can feed it new sound data as part of the main loop might work, but that’s probably going to take a bit more work.

Sorry for the delay in responding – hopefully this is a better explanation.

Assuming L4_Triplane mode and:

a.setUpdateEveryN(2);
a.setRefreshHz(135);

Order of operations (the list items below happen at a frequency of 135 Hz and are each triggered by nextFrame() returning true):

  1. Push buffer to display (containing plane 2 from previous “frame”)
    UpdateneedsUpdate() returns true here
    Render plane 0
  2. Push buffer to display
    Render plane 1
  3. Push buffer to display
    Render plane 2
  4. Push buffer to display (containing plane 2 from step 3)
    Render plane 0
  5. Push buffer to display
    Render plane 1
  6. Push buffer to display
    Render plane 2

When it’s time for the next plane, the call to nextFrame() sneakily does the display push before returning true, so there’s no need for calls to display. That is, the library doesn’t want you to do this:

  1. Update
    Render plane 0
    Push buffer to display
  2. Render plane 1
    Push buffer to display
  3. etc.

The reason it’s not ordered this way is to keep the display updates at the same cadence: if display updates happened immediately after rendering, the time each plane spent on the display would vary slightly depending on how long the rendering to the buffer took. More importantly, you could miss timing windows: imagine plane 0 took really long to render and plane 1 did not, so that there wasn’t enough time for the display to finish driving the pixels for plane 0 before receiving plane 1.

Revisiting the game loop:

void loop()
{
    if(!a.nextFrame())
        return;
    // nextFrame() returned true, so the previously rendered plane
    // was just pushed to the display
    if(a.needsUpdate())
        // given setUpdateEveryN(N), we are about to render the Nth plane 0
        update();
    render();
}

Perhaps a way to improve the clarity of the API would be to rename nextFrame to nextPlane and needsUpdate to nextFrame – i.e., as @Pharap suggested, consistently use the terms plane and frame to refer to a single bitplane and the image composed of all bitplanes, respectively.

Maybe its worth writing a simple example for the library that demonstrates this logic?

Here is an example of manually choosing what to render for each plane (adapted from your earlier comment):

#define ABG_IMPLEMENTATION
#include "ArduboyG.h"

ArduboyG_Config<ABG_Mode::L4_Triplane> a;

const uint8_t PROGMEM IMG1[] = {
0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 
};

const uint8_t PROGMEM IMG2[] = {
0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 
};

const uint8_t PROGMEM IMG3[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 
};

void setup() {
    a.begin();
    a.startGray();
}

void loop() {

    if (!a.nextFrame()) return;

    uint8_t plane = a.currentPlane();
    if(plane == 0) a.drawBitmap(22, 2, IMG1, 16, 12, WHITE);
    if(plane == 1) a.drawBitmap(22, 2, IMG2, 16, 12, WHITE);
    if(plane == 2) a.drawBitmap(22, 2, IMG3, 16, 12, WHITE);

}

There is also the short example on Github that demonstrates using the overridden Arduboy2Base drawing methods.

These methods call the base class methods internally, but adjust the color (to WHITE or BLACK) according to the current plane, the requested gray shade, and the configured mode. For example, in mode L3, if you were to call

a.fillRect(x, y, w, h, GRAY);

this method internally calls

Arduboy2Base::fillRect(x, y, w, h, WHITE);

if the current plane is 0, or

Arduboy2Base::fillRect(x, y, w, h, BLACK);

if the current plane is 1. The plane-color mapping is

Plane                               0  1  2
===========================================

ABG_Mode::L4_Contrast   BLACK       .  .
ABG_Mode::L4_Contrast   DARK_GRAY   X  .
ABG_Mode::L4_Contrast   LIGHT_GRAY  .  X
ABG_Mode::L4_Contrast   WHITE       X  X

ABG_Mode::L4_Triplane   BLACK       .  .  .
ABG_Mode::L4_Triplane   DARK_GRAY   X  .  .
ABG_Mode::L4_Triplane   LIGHT_GRAY  X  X  .
ABG_Mode::L4_Triplane   WHITE       X  X  X

ABG_Mode::L3            BLACK       .  .
ABG_Mode::L3            GRAY        X  .
ABG_Mode::L3            WHITE       X  X

where ‘X’ means white and ‘.’ means black.

EDIT:

I think ArduboyG could be made configurable to use timer 4. ATMlib uses timer 4 so it’d be good to have the library support either.

EDIT 2: Updated. The library now supports using either timer 3 or timer 4. Define one of:

  • ABG_TIMER3 (default if none defined)
  • ABG_TIMER4

before including ArduboyG.h. There is also an additional sync option: AGB_SYNC_SLOW_DRIVE which is the method of slowing down the row drive time and disabling the charge pump for the park row by @dxb. It enables full speed refresh while retaining use of all 64 rows, at the expense of the occasional slight glitch at the park row.

3 Likes

Excellent!

Thanks for your hard work! @brow1067

Sensitive is now the grayscalest game on Arduboy :smiley:

2 Likes

Updated. Now includes optimized versions of drawOverwrite, drawPlusMask, and drawExternalMask for grayscale sprites. The demo includes a simple example demonstrating a smooth scrolling tilemap background (153 8x8 tiles drawn) with text and two sprites in the foreground, demonstrating both styles of masking.

Currently there is no support for L4_Triplane for the sprite methods.

The sprite format is similar to that used by the Sprites and SpritesB classes, but each sprite frame has two planes. For drawPlusMask, the interleaving is: plane 0, plane 1, mask.

Interestingly, as implemented, drawPlusMask does not offer a performance boost over drawExternalMask as they both stream image and mask data in the exact same number of cycles.

drawPlusMask selects one of the first two interleaved bytes to load based on the current plane (10 cycles):

sbrc %[plane], 0
adiw %[image], 1
lpm %A[image_data], %a[image]+
sbrs %[plane], 0
adiw %[image], 1
lpm %A[mask_data], %a[image]+

drawExternalMask swaps the image and mask pointers back and forth (10 cycles):

lpm %A[image_data], %a[image]+
movw %[image_temp], %[image]
movw %[image], %[mask]
lpm %A[mask_data], %a[image]+
movw %[mask], %[image]
movw %[image], %[image_temp]
1 Like

I’m struggling to get 4 shades, is that possible?

It should be possible. The default mode is a 2-plane mode that adjusts contrast every other plane to achieve 4 shades. I took a look at your code and see that you are rendering the title screen in 3 planes. This is probably producing flickering as every other image has different contrast applied to its planes. The library does have a 4-level triplane mode, but since the rest of your game uses 3 shades, you can use the 2-plane, 3-level mode and just render a 3-plane title anyway as you are. You can configure the library by changing:

// ArduboyG a;
ArduboyG_Config<ABG_Mode::L3> a;
1 Like

I’ll give that a try. I did notice that the brightness of the mid grey value in the game seems to vary a bit, but it seems stable. One time I switch on and it’s quite light, other time a bit darker. Would this be something to do with battery level related timing inconsistencies in the screen?

That could also be related to the contrast switching. Depending on where in the 2-plane frame cycle you are at when you leave the title screen, the game’s gray shades will either use the higher or lower contrast plane of the L4_Contrast mode. Changing the mode to L3 could also fix that as it doesn’t mess with the contrast at all.

I wonder if the default configuration should use L3 to begin with.

1 Like

Worked on sensitive, but having trouble with this…

Ah – the sprites methods in ArduboyG don’t support triplane rendering at the moment.

To make your code work you could make this change:

// a.drawOverwrite(0, 0, background, 0);
Sprites::drawOverwrite(0, 0, background, frameNumber);

Perfect! (more text)

Will it be possible to render directly from the SD card with this library? It would be excellent to be able to stream some hi-res, 4 colour images from the SD card but I suspect that the use of the screen memory will be an issue. @Mr.Blinky and @brow1067 is it possible?