ArduboyG, grayscale library

I’m looking for feedback on a library I’ve been working on. It incorporates the grayscale methods @dxb and I have been iterating on for the past week in this thread. I don’t have a lot of experience designing APIs/libraries for general use.

ArduGray is a library in single-header form that provides classes ArduGray and ArduGrayBase, which derive from Arduboy2 and Arduboy2Base to provide grayscale support. As a single-header library, you define the ARDUGRAY_IMPLEMENTATION macro in one compilation unit before including ArduGray.h. The additional options are:

    ARDUGRAY_MODE
        Plane mode. Allowed values are:
        - ARDUGRAY_L4_CONTRAST
            4 levels in 2 frames using contrast.
        - ARDUGRAY_L4_TRIPLANE
            4 levels in 3 frames. Visible strobing from decreased image rate.
        - ARDUGRAY_L3
            3 levels in 2 frames. Best image quality.

    ARDUGRAY_SYNC    
        Frame sync method. Allowed values are:
        - ARDUGRAY_PARK_ROW
            Sacrifice the bottom row as the parking row. Improves image
            stability and refresh rate, but usable framebuffer height is 63.
        - ARDUGRAY_THREE_PHASE
            Loop around an additional 8 rows to cover the park row. Reduces
            both refresh rate due to extra rows drive and image stability due
            to tighter timing windows, but allows a framebuffer height of 64.
                        
    ARDUGRAY_HZ
        Target display refresh rate. Usually best left at the default value.
                    
    ARDUGRAY_UPDATE_EVERY_N
        Determines how many image cycles between game logic updates, as
        indicated by Ardugray::needsUpdate.

I’d like it to also support the additional sync mode that @dxb discovered (new value for ARDUGRAY_SYNC config) as it would enable faster refresh than ARDUGRAY_THREE_PHASE while supporting 64 rows.

Game Loop

The basic game loop in ArduGray:

void loop()
{
    if(!a.nextFrame())
        return;
    if(a.needsUpdate())
        update();
    render();
}

The nextFrame method performs display updates behind the scenes and signals when to render the next image plane.

When using grayscale, though, you probably don’t want to change the game state in between planes of the same image, so an auxiliary method needsUpdate is provided to know when to handle input and update any state.

Inside render, you can use the currentPlane method to know which image plane you are supposed to be drawing. You can also use any of the Arduboy2 drawing or printing methods. Instead of BLACK, WHITE, and INVERT, there is BLACK, DARK_GRAY, LIGHT_GRAY, and WHITE; INVERT is no longer supported.

Full Example

#define ARDUGRAY_IMPLEMENTATION
#include "ArduGray.h"

ArduGray a;

int16_t x, y;

void update()
{
    // handle input and update game state
    if(a.pressed(UP_BUTTON))    --y;
    if(a.pressed(DOWN_BUTTON))  ++y;
    if(a.pressed(LEFT_BUTTON))  --x;
    if(a.pressed(RIGHT_BUTTON)) ++x;
}

void render()
{    
    a.setCursor(20, 28);
    a.setTextColor(WHITE);
    a.print(F("Hello "));
    a.setTextColor(DARK_GRAY);
    a.print(F("ArduGray!"));
    
    a.fillRect(x +  0, y, 5, 15, WHITE);
    a.fillRect(x +  5, y, 5, 15, LIGHT_GRAY);
    a.fillRect(x + 10, y, 5, 15, DARK_GRAY);
}

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

void loop()
{
    if(!a.nextFrame())
        return;
    if(a.needsUpdate())
        update();
    render();
}
3 Likes

Functionally it looks good. It’s nice to see that it inherits from Arduboy2/Arduboy2Base to inherit their functionality, and provides a similar API.


My biggest complaint is the heavy use of macros, which should really be avoided in modern C++.

Templates are often the better option because they’re more flexible, allow the user more control over their usage, can avoid polluting the global namespace, and operate at the language level instead of the preprocessor level.

E.g. instead of:

#define ARDUGRAY_IMPLEMENTATION 0

#include "ArduGray.h"

You could have:

#include "ArduGrey.h"

ArduGrey<GreyMode::L4Contrast> ardugrey;

(Where ArduGrey is a template class, and GreyMode is an enum class.)

Or, if you want to hide the template, a simple type alias in the library (using ArduGreyL4Contrast = ArduGrey<GreyMode::L4Contrast>;) lets the user write:

#include "ArduGrey.h"

ArduGreyL4Contrast ardugrey;

Bear in mind that because you have to do define ARDUGRAY_IMPLEMENTATION before including ArduGray.h, there’s no ARDUGRAY_MODE_L4_CONTRAST defined to make it obvious which mode is being selected.

This also means that the values of ARDUGRAY_MODE_L4_CONTRAST etc. can never be changed without breaking existing code, because the user has to use their numerical values instead of predefined names. (Though admittedly you could fix that by providing a second header that defines just those values.)

In fact, if you weren’t already using templates in your code to select the plane value, your library wouldn’t even respond to redefined macros…

Several years back I attempted to #undef ARDUBOY_10 and #define AB_DEVKIT before #include <Arduboy2.h>, and it didn’t do what I was expecting it to.

I then found out that Arduino precompiles libraries, so the library is already compiled by the time the #include <Arduboy2.h> is reached, and it uses the original macro definitions, not the redefinitions.

Defining a template within the libary prevents the library from being precompiled. (Which is probably because it can’t be specified in an object file in the way a class or function can, though that’s just an educated guess, I’ve never found anything to confirm that.)

I can provide some examples of how to go about reducing the reliance on macros and making the code more typesafe by using scoped enumerations (enum classes) if you’re interested at all.

E.g. it’s possible to change ARDUGRAY_SEND_CMDS from being a macro to being a template function such as template<uint8_t ... commands> void SendCommands(), which has several advantages.


That aside:

  • It would be nice to provide versions that use ‘grey’ instead of ‘gray’ to help those of us who use Commonwealth/non-US spellings, particularly since unlike a lot of other languages C++ provides all the necessary tools to do so (e.g. using ArduGrey = ArduGray;, void startGrey() { startGray(); }), #define GREY GRAY).
  • You probably shouldn’t mark your free functions as static, because that results in each translation unit getting a separate copy. That’s fine for really tiny functions that are going to be inlined and are smaller than a function call, but anything larger is just going to produce more code, which is more important on Arduboy than it would be on e.g. desktop.
  • It would be handy to have a version of the drawBitmap functions that reads the width and the height as the first and second byte of the bitmap, so people could use bitmaps that are in the ‘sprites’ format. (I proposed the same thing for the Arduboy2 library a while back. Which reminds me, I should probably do something about it some time…)
  • Your declaration of display is not quite right. It should return void, not bool. Not that it’s actually going to be called or anything, I just thought I’d point it out.
1 Like

This is great feedback, thank you! Your points make sense. I think it should be possible to get rid of most macros except for the SYNC option because it affects the code in the ISR.

Wow, oops! :stuck_out_tongue:

1 Like

ARDUGRAY_SYNC will have the same problem. However…

At first I couldn’t think of a good way of doing it, but after giving it a bit of thought I realised that actually you can get rid of ARDUGRAY_SYNC, provided you’re willing to replace it with another macro…

If you were to define a function macro called ARDUGREY_ISR that accepted the sync value as an argument then instead of requiring the user to write:

#define ARDUGRAY_SYNC 1

#include "ArduGrey.h"

You could have them do:

#include "ArduGrey.h"

ARDUGREY_ISR(ArduGreySync::ThreePhase);

As for implementation, you could define a template function in ardugrey_detail and then call it from ARDUGREY_ISR:

namespace ardugray_detail
{
	// ...

	template<ArduGreySync sync>
	void isr_implementation()
	{
		if(sync == ArduGreySync::ThreePhase)
		{
			if(++current_phase >= 4) current_phase = 1;
			
			if(current_phase == 1)
				OCR3A = (timer_counter >> 4) + 1; // phase 2 delay: 4 lines
			else if(current_phase == 2)
				OCR3A = timer_counter;            // phase 3 delay: 64 lines
			else if(current_phase == 3)
				OCR3A = (timer_counter >> 4) + 1; // phase 1 delay: 4 lines
		}
		else if(sync == ArduGreySync::ParkRow)
		{
			OCR3A = timer_counter;
		}

		needs_display = true;
	}
	
	// ...
}

#define ARDUGREY_ISR(sync) \
	ISR(TIMER3_COMPA_vect) \
	{ \
		ardugray_detail::isr_implementation<sync>(); \
	}

If you don’t trust the compiler to eliminate the dead code or inline the isr_implementation function then you could use template specialisation and [[gnu::always_inline]]*, but personally I think this would be enough.

(* Though contrary to its name, it doesn’t always inline, according to the docs. :P)

In summary, there’s three benefits to this approach:

  • The user can use named values instead of having that awkward stray 1 in their code.
  • You could allow timer_counter or hz values to be passed in as arguments to the macro, which would then be passed as template parameters to the template, more or less ensuring that they’d be treated as constants.
  • The user can choose to forgo using the library-provided ISR in favour of their own implementation. For the day-to-day users that’s not a big gain, but for the ‘power users’ and experimenters that might come in handy.

Of course, there are downsides:

  • The user is responsible for remembering to use ARDUGREY_ISR in their .ino file.
  • The user is responsible for making sure the sync value they provide is compatible with what they’re using elsewhere.

Though I hasten to point out that this is possible:

constexpr ArduGreySync projectSyncValue = ArduGreySync::ThreePhase;

ArduGrey<projectSyncValue> ardugrey;

ARDUGREY_ISR(projectSyncValue);
1 Like

EDIT: Wrote this while your response came – reading that now

EDIT 2:
The SYNC macro approach I ended up with solves the issues I think, with the exception of the “power user” case of being able to supply their own ISR implementation. I’ll have to think about it a bit more…

Updated

I opted for a name change to avoid ‘gray’ being in the filename, identify the library as relevant specifically to Arduboy instead of just Arduino, and increase the similarity to the name ‘Arduboy2’.

The header file is now ArduboyG.h and the classes are ArduboyG and ArduboyGBase for the default configurations. These are aliases to templated types:

using ArduboyGBase = ArduboyGBase_Config<>;
using ArduboyG     = ArduboyG_Config<>;

The configuration is:

enum class ABG_Mode : uint8_t
{
    L4_Contrast,
    L4_Triplane,
    L3,
    Default = L4_Contrast,
};

struct ABG_Flags
{
    enum
    {
        None = 0,
        OptimizeFillRect = (1 << 0),
        Default = OptimizeFillRect,
    };
};

template<
    ABG_Mode MODE = ABG_Mode::Default,
    uint8_t FLAGS = ABG_Flags::Default
>
struct ArduboyG_Config;

(The OptimizeFillRect flag results in speed-optimized replacements for fillRect and drawFastVLine.)

Update interval and frame refresh speed are no longer configured via macros, nor are they compile time constants (though it looks like the compiler can optimize them out if their values never change at runtime). They can be adjusted at runtime with setUpdateEveryN and setRefreshHz.

The one remaining macro configuration option is to control the frame sync method. You define one of

  • ABG_SYNC_THREE_PHASE (default if none specified)
  • ABG_SYNC_PARK_ROW

before including ArduboyG.h.

There are now startGrey aliases to startGray in both classes and color aliases are similar.

#undef BLACK
#undef WHITE
static constexpr uint8_t BLACK      = 0;
static constexpr uint8_t DARK_GRAY  = 1;
static constexpr uint8_t DARK_GREY  = 1;
static constexpr uint8_t GRAY       = 1;
static constexpr uint8_t GREY       = 1;
static constexpr uint8_t LIGHT_GRAY = 2;
static constexpr uint8_t LIGHT_GREY = 2;
static constexpr uint8_t WHITE      = 3;

Sprites

I am curious about how to handle grayscale sprites. Since the display planes need to be rendered so fast, it’d be nice to support the Sprites/SpritesB drawing methods. Most of them transfer fairly simply except for drawSpritePlusMask, the most desirable. I think the assembly implementation would need a rewrite to handle three pixels interleaved together, two sprite planes and a mask.

Supporting sprites on L4_Triplane will also present a challenge. Depending on which display plane you are rendering, you may need to read both sprite planes:

  • Display plane 0: sprite plane 0
  • Display plane 1: bitwise OR of sprite planes 0 and 1
  • Display plane 2: bitwise AND of sprite planes 0 and 1
1 Like

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?