Possible simpler alternative to screen mode settings

I was thinking that it might be easier and simpler to have multiple Display sub-classes, one for each supported screen mode.

Some advantages to this method:

  • Easier to maintain as there would be a core display class and each type of display mode would be self contained in its own class file.
  • Easier to use since you wouldn’t need anything in My_settings.h for display instead you would create the display you need at run time.
  • Ability to be expanded with custom screen modes that could later be incorporated into the main library.
  • Possible to switch modes mid-game.

This method might also be able to be applied to other areas of the library as well. For example instead of #define PROJ_ENABLE_SOUND you would simply create a variable to a base Audio class that has a function to initialize the sound.

2 Likes

May I draw your attention to the prototype library I was working on eons ago but have only recently released publicly:

If you head into the graphics folder I have some preliminary examples of some graphics mode classes, for example:

Though I’ve recently been thinking that rather than putting the buffer in the class,
it might be better to just provide functions that manipulate a buffer passed in by the user,
then people could use the drawing functions to do things like dynamically composing images/sprites/textures.


I can’t find it at the moment but I’ve also previously discussed the idea of making an actual Pokitto class that’s a template class which accepts a settings class as its template parameter,
e.g.

struct PokittoSettings : Pokitto::DefaultPokittoSettings
{
	using GraphicsMode = Pokitto::Graphics::LowRes16ColourMode;
};

using PokittoObject = Pokitto::PokittoObject<MyPokittoSettings>;

PokittoObject pokitto;

After which PokittoObject::GraphicsMode would be a type alias for Pokitto::LowRes16ColourMode,
and if we went down the object route then pokitto.graphics would be an instance of that class through which people could use the LowRes16ColourMode functions via that object.

But something like a tile mode might not work so well with that approach unless the PokittoObject is just a simple container that groups the various subsystems,
in which case it might be simpler for people to just do something like:

using MyGameGraphics = Pokitto::Graphics::LowRes16ColourMode;
using MyGameAudio = Pokitto::Audio::SomeSpecificAudioMode;

// If the classes need to be instantiated and aren't just containers for static functions:
MyGameGraphics graphics;
MyGameAudio audio;

While I’m at it, I’d like to draw people’s attention to the RGB888 and RGB565 classes.

Whatever we end up doing for the actual graphics mode,
I think these would be incredibly useful because they allow people to manipulate colours at compile time,
which means you don’t have to manually convert RGB888 colours to RGB565 because the compiler will do it for you at compile time.

Which makes palettes a lot easier to write, and more importantly a lot easier to read - no more playing “guess what the original colour was before it was converted to RGB565”:

// Constexpr means this is all evaluated at compile time
// no runtime cost on startup like with older methods!
constexpr RGB565 pico8[16]
{
	// 0 - Black
	RGB565::fromRGB(0, 0, 0),

	// 1 - Dark Blue
	RGB565::fromRGB(29, 43, 83),

	// 2 - Dark Purple
	RGB565::fromRGB(126, 37, 83),

	// 3 - Dark Green
	RGB565::fromRGB(0, 135, 81),

	// 4 - Brown
	RGB565::fromRGB(171, 82, 54),

	// 5 - Dark Grey
	RGB565::fromRGB(95, 87, 79),

	// 6 - Light Grey
	RGB565::fromRGB(194, 195, 199),

	// 7 - White
	RGB565::fromRGB(255, 241, 232),

	// 8 - Red
	RGB565::fromRGB(255, 0, 77),
	
	// 9 - Orange
	RGB565::fromRGB(255, 163, 0),

	// A - Yellow
	RGB565::fromRGB(255, 236, 39),

	// B - Green
	RGB565::fromRGB(0, 228, 54),

	// C - Blue
	RGB565::fromRGB(41, 173, 255),

	// D - Indigo
	RGB565::fromRGB(131, 118, 156),

	// E - Pink
	RGB565::fromRGB(255, 119, 168),

	// F - Peach
	RGB565::fromRGB(255, 204, 170),
};

(Example from here.)

1 Like

I’d just like to point out that, in practice, this is hard to do.
In Java, each screen mode is a separate class and the user allocates the necessary one. We only ever use one mode, with Direct mode being the exception.
The problem is that a framebuffer takes up so much memory, it’s very likely it won’t be able to initialize a different mode. Even if the previous one has already been deleted, due to fragmentation.
The only safe way would be to allocate the largest buffer you could need and just use that with different modes… but doing so may defeat the point of changing modes at all.

These two things can be done with the current setup as well. The bulk of TAS mode is in its own cpp/asm files. I was going to do the same for the other modes but that pull-request was big enough already.

1 Like

This is the idea that came to mind with what @Pharap was referring to

And it wouldn’t actually defeat the purpose either. A good, but simple, example would be switching between hi-res 16-color and low-res 16-color. The reason for the switch is not to save on RAM, but to boost performance. The hi-res mode starts to suffer if you have a lot on screen that’s needing to be redrawn all the time, however it would be good for a menu system so as to have better details and easier to read fonts. Then you’d switch to low-res mode for the actual game. In this case each mode would be a static class that doesn’t contain an internal frame buffer.

The possibility of switching modes isn’t the main benefit though, mostly just an additional ability that some advanced users might figure out neat ways to take advantage of. Mostly this would greatly simplify the maintainability and readability of the code (one example is reading through the PokittoDisplay.cpp file and getting lost in trying to remember which #ifdef block hierarchy I’m currently in just to find the methods used for a specific mode).

This might be good to add to the master TODO list since it’s quite useful since it would also improve performance by not having to do RGB888->RGB565 each time the value is needed.

Agreed that this is far easier to read/write and would make editing small bitmaps or pallets manually a lot easier.

1 Like

While I don’t disagree with this, I’d like to note that there are other flaws with the current approach.
Mainly the reliance on macros for changing mode,
but also that the current approach makes it hard to identify which modes support which functions.

Maintaining separate classes has the benefit of giving each mode a tangible API,
which then makes it easier to identify which modes don’t support particular functions and what the API overlap is.

This is one of the ideas I had in mind when stating:

Because in that situation it would be possible to allocate the largest buffer needed and then repurpose it for different modes.

However, @FManga is right that this partly defeats the point of having separate screen modes.

When it comes to screen modes, there are three concerns:

  • RAM
    • High-res screen modes tend to chew up the most, with High-res 16-colour chewing up 19360 bytes of the Pokitto’s 36864, leaving 17504 for everything else (the library and the user’s game)
    • Low-res screen modes tend to use a lot less
    • The recently introduced ‘TAS mode’ uses up possibly the least amount of memory
  • Speed
    • Some screen modes are faster than others. I find that the ones that use fewer bits per pixel tend to be slower because of the extra shifting and masking.
  • Resolution
    • 220x176 vs 110x88. Depending on the game, 110x88 is perfectly liveable.

That’s a fair argument. I could imagine that being useful.

I completely concur with this.

It’s been a while since I last touched the library code, but that was more or less my experience,
and the main reason I’ve never even attempted to touch any of the graphics code.
(And also why when trying to design a replacement I’ve opted to rewrite functions from scratch rather than bringing across the old code.)

Perhaps I’ll propose adding them to the library at some point,
but I’ve got to get some other things off my personal to-do list first.

Theoretically these could be added without breaking backwards compatibility,
so they’re easier to include than the other features.

1 Like

Compile-time settings have their meaning – the compiler can optimize better the more things it knows in advance. For example if you know the game resolution is always going to be 110x88, the screen operations will be faster because you have constants (as opposed to variables) in your expressions which can then be partially precomputed. Also things like memory allocation are an issues as @FManga says – in general if you make something dynamic, as oppoed to static, things become more complicated. Compile-time options make things simpler, faster and save memory, and should be used whenever possible. Settings that aren’t supposed to change during runtime – such as screen or sound modes – should stay compile-time.

Also I think by going heavier on OOP makes thing more complicated, not simpler, but that’s an opinion I get a lot of hate for. Still I stand behind it very firmly.

You can avoid nested #ifdef hell easily – my non-C++ solution to this would be to have each screen mode implementation in a separate header file. Each file would implement the same functions (initScreen, setPixel, …) but differently. Then based on the SCREENMODE define include the corresponding file. This effectively achieves compile-time polymorphism (or something like that? anyway, no need for C++).

I’m not sure how this relates to the quote you chose.

Are you trying to reinforce the comment or refute something claimed within it?

If by ‘compile-time options’ you mean macros then that’s simply not true.

Combinations of templates and constexpr can achieve the same effect and should be preferred over macros.

For example, anything involving dimensions can have the dimensions passed as template parameters rather than function parameters, and thus the compiler will treat the template parameters and/or constexpr variables as compile-time constants rather than variables.

That’s more or less what this class-based solution would be doing.
And in fact, that’s what my library prototype code does.

Each screen mode class implements a similar set of functions and constants so the types can be easily substituted.
For example, all of classes for indexed screen modes have a static constexpr std::size_t displayWidth that identifies the width of the display mode - 220 for high-res, 110 for low-res.

The difference with this approach is that rather than having some kind of weird macro set up dictating the screen mode, you simply specify which class you indend to use, like so:

using MyGraphicsMode = Pokitto::Graphics::LowRes16ColourMode;

void someFunction()
{
    // Draw a diagonal line on the screen:
    MyGraphicsMode::drawLine(0, 0, MyGraphicsMode::displayWidth - 1, MyGraphicsMode::displayHeight -1, 0);
}

To change graphics mode you simply replace the type alias:

using using MyGraphicsMode = Pokitto::Graphics::HighRes16ColourMode;

Or you could even write a template function like so:

temlate<typename GraphicsMode>
void someFunction()
{
    // Draw a diagonal line on the screen:
    GraphicsMode::drawLine(0, 0, GraphicsMode::displayWidth - 1, GraphicsMode::displayHeight -1, 0);
}

And it would work for all graphics modes.

This kind of thing is used a lot in the C++ standard library.
For example std::stack and std::queue are just wrapper classes for another container,
you can ask them to use a std::vector or a std::deque depending on your performance requirements.

Dynamically changing graphics mode would indeed be more complicated,
but it’s manageable if you insist that the user provides the buffer.

The functions that support dynamically changing the screen mode don’t even have to be the same ones used if you’re only using a single screen mode for the whole game.

Compile-time polymorphism (sometimes called ‘static polymorphism’) is indeed a thing,
it’s heavily used in template programming and template meta-programming.

For example, many of the algorithms provided by <algorithm> will detect whether the provided iterator is capable of random access or not, and will choose the more appropriate implementation.

I’d like to point out that when using templates this sort of thing is something the user doesn’t have to think about because the template definitions cause the compiler to infer all the necessary information.

I’ve found this route usually means either don’t use c++ for anything and strictly use C, which can easily be included in c++ programs, or make use of the features available in c++ to better optimize not only for performance and memory but also readability and ease of use.

Again this kind of boils down to not using an OOP language if OOP is too complicated. Besides, a good example of super heavy OOP is the Qt toolkit, but the user is never really exposed to the more complex stuff (unless they want to) because Qt does a good job of hiding it away to make a simple API that has lots of complex things happening in the background that the programmer almost never needs to worry about.

I remember reading an article several years ago on the argument of why c++ isn’t a true OOP language and it argued that it makes it easier to use libraries already written in C without needing complex wrappers.

I myself was confused my heavier OOP designs when I was new to the language, but libraries can utilize it without exposing any complexity to the user. In fact utilizing it can often create an even simpler interface for the user because it obfuscates the complexities behind the curtains so to speak.

1 Like

Precisely.

The complexity has to go somewhere,
it can either be hidden away in the library or forced onto the end-user.
C++ tends to prefer the former, C tends to do the latter.
(E.g. in C++ if an API uses references the end-user never even has to touch a pointer, let alone know what one is.)

It depends what definition of OOP you use.

There’s a ‘purist’ definition that more closely resembles Java and includes things like inheritance and polymorphism as being mandatory,
and there’s a more minimal definition that just considers classes/objects to be a means of combining structs with ‘member functions’.

(Personally I prefer the minimal definition.)

I could go into more detail, but we’re not really here to debate C vs C++ or OOP vs non-OOP.

1 Like

Thinking aloud:

Option 1:
Have some kind of FormatBuffer<Width, Height> class with all relevant functions.
E.g. Colour16Buffer, with Colour16Buffer<220, 176> representing the screen and other values (e.g. Colour16Buffer<16, 16>) representing sprites.
(Representing a sprite that could be multiple sizes is doable, but there’s several approaches of varying merit.)
Then to update the screen you’d have functions like:

  • Screen::drawHighRes(const Colour16Buffer<220, 176> &, const std::array<RGB565, 16> &);
  • Screen::drawLowRes(const Colour16Buffer<110, 88> &, const std::array<RGB565, 16> &);.

Option 2:
Have various templated functions that manipulate arrays passed in as arguments.
E.g. Colour16::getPixel<16, 16>(buffer, x, y); // 16, 16 is inferred when array passed has 2 dimensions.


I’m leaning towards option 1 because it’s easier to make it safer,
and I think it makes sense to represent actual buffers as classes.

Plus the idea of having a class be represetative of the format of the data it contains is useful,
because it means if you (for example) pass a Colour16Buffer to a function then the format can effectively be detected with function overloading,
whereas with the other approach the format is represented by the function itself.

The main benefit to the second approach is that it’s easier to use the same buffer as two different formats, but naturally that’s more dangerous.

It would be possible to have two APIs:

  • Use option 1 to create the safe API for games that only use one screen mode
  • Use option 2 to create an unsafe API for people who want to attempt mixing screen modes
1 Like

In a similar way to the mixed screen mode I made? Or different?

Sort of.

You’d still need a custom function for actually outputting the buffer to the screen,
but you’d be able to use the provided functions for drawing to the buffer.
E.g.

// Use part of the buffer in 4bpp mode
Colour16::drawBitmap(buffer, bitmap);

// Use another part of the buffer in 2bpp mode
Colour4::drawBitmap(&buffer[offset], otherBitmap);

You’d probably need more parameters than that,
but that’s the general idea - same buffer, but part of it is in a different format,
reusing the provided buffer manipulation functions instead of needing custom functions.


However if you were only planning to use (for example) half the screen in one format and half the screen in another without deviating from that arrangement then ‘option 1’ would also be suitable.

What ‘option 2’ provides is flexability with how you use the buffer - i.e. you could dynamically change the format of the buffer.
But that’s precisely why it’s more dangerous,
you wouldn’t have the compile-time safeguards in place so you’d need more runtime safeguards (and then you have to decide between safety or performance - burn cycles doing safety checks vs saving cycles by being unsafe).

In what way would it be unsafe? If the frame buffer is still a fixed size there shouldn’t be any overflows or anything like that surely.

The unsafe part (or at least more error prone) is that it’s up to the programmer to ensure that the supplied dimensions and the specifies overlap are correct.

In the first case a template can infer the dimensions of the array (assuming it’s 2D),
but in the latter case it’s a pointer being passed, so templates won’t infer the size,
meaning it’s up to the programmer to make sure the dimensions are consistent.

Here’s a slightly more complete example:

// Buffer dimensions
constexpr std::size_t width = 110;
constexpr std::size_t height = 88;

/// 110x88, 8 bpp
unsigned char buffer[height][width];

/// 8 bpp format
const unsigned char bitmap[16][16] { ... };

/// 4 bpp format
const unsigned char bitmap2[16][16] { ... };

// Use the buffer in 8 bpp mode.
// Dimensions can be inferred because 'buffer' is an array.
// 'bitmap' dimensions are also inferred
Colour256::drawBitmap(buffer, bitmap, x, y);

// Use the second half of the buffer in 4 bpp mode.
// Only the dimensions of the otherBitmap can be inferred,
// the dimensions to be used for the buffer have to be calculated manually.
Colour16::drawBitmap(&buffer[(height / 2)][0], width, (height / 2), otherBitmap, x, y);

// Definition of first example is something like:
// Note that the inferred dimensions are for _the whole of the buffer_,
// so drawing is not restricted to the first half
template<std::size_t bufferWidth, std::size_t bufferHeight, std::size_t imageWidth, std::size_t imageHeight>
void drawBitmap(unsigned char (&buffer)[bufferHeight][bufferWidth], unsigned char (&image)[imageHeight][imageWidth], int x, int y)
{
	// image is drawn onto the buffer in 8 bpp mode.
}

// Definition of second example is something like:
template<std::size_t imageWidth, std::size_t imageHeight>
void drawBitmap(unsigned char * buffer, std::size_t bufferHeight, std::size_t bufferWidth, unsigned char (&image)[imageHeight][imageWidth], int x, int y)
{
	// image is drawn onto the buffer in 4 bpp mode.
}

The key point here is that the Colour256::drawBitmap(buffer, bitmap, x, y); version infers the array sizes, so it’s almost impossible to get an overflow,
but in Colour16::drawBitmap(&buffer[(height / 2)][0], width, (height / 2), otherBitmap, x, y); it’s up to the programmer to make sure that the supplied dimensions are correct,
and if the dimensions aren’t correct then there could easily be a buffer overflow by mistake.

I can think of a few ways to make it a bit less error-prone (e.g. by using std::span or span-like objects),
but at some point the programmer has to determine the size in manually.


For the record, when I say ‘unsafe’ I don’t mean “is guaranteed to shoot you in the foot”,
I mean “has the potential to corrupt memory and/or cause awkward runtime bugs”.

Something that’s ‘safe’ on the other hand is something that’s very hard to accidentally misuse and almost certainly won’t cause an error assuming the implementation is correct.

For example, here's a 'safe' version of `std::memcpy`
template<typename Type>
void objectCopy(Type & destination, cosnt Type & source)
{
	static_assert(std::is_trivially_copyable<Type>::value, "Error: attempt to use objectCopy on a non-trivially-copyable Type");
	
	std::memcpy(&destination, &source, sizeof(Type));
}

Safety features:

  • It doesn’t let the programmer provide the size of the object,
    it infers the size from the types of the objects.
  • It doesn’t let the programmer provide two different types of object.
  • It doesn’t let the programmer pass in objects that aren’t trivially copyable
    • According to std::memcpy's documentation “If the objects are potentially-overlapping or not TriviallyCopyable, the behavior of memcpy is not specified and may be undefined”, but std::memcpy itself does not stop the programmer passing an object that isn’t trivially copyable, it allows the programmer to shoot themselves in the foot. This version stops the programmer from being able to incur undefined behaviour.
    • Technically it would be better to use std::enable_if rather than static_assert, but the former has slightly more obscure syntax so I’m using static_assert to make the example simpler.