Introduction
I am currently trying to make on an old dream come true: Writing a demo for the Commodore Amiga in C (using AmigaOS APIs instead of direct hardware access). First challenge was handling the blitter to put bitmaps on screen and make them move around. This is now working quite well. Therefore, I moved over to a second effect: Bitmap rotation in realtime. This article sums up the results.
Source Code
An example project which demonstrates my approach is available here on Github. It is build with gcc and vasm. To make things easier, I created a docker container for compilation.
Boiler Plate Code
Hiding the Mouse
We do not want to see the mouse cursor while our object is rotating. My first approach was to disable sprites DMA via OFF_SPRITE macro but this is not very OS friendly. Therefore, I am using another approach:
UWORD *emptyPointer;
struct Screen *my_wbscreen_ptr;
emptyPointer = AllocVec(22 * sizeof(UWORD), MEMF_CHIP | MEMF_CLEAR);
my_wbscreen_ptr = LockPubScreen("Workbench");
SetPointer(my_wbscreen_ptr->FirstWindow, emptyPointer, 8, 8, -6, 0);
UnlockPubScreen(NULL, my_wbscreen_ptr);
First, we allocate memory filled with 0 which will be used as an invisible, empty mouse cursor. Afterwards, we get a pointer to the workbench Screen object. Finally, we assign the empty cursor to this screen. It is important to revert these changes when terminating the application via ClearPointer.
Draw Video Buffer on Screen
The first challenge is the creation of a video buffer which will later contain the rotating bitmap. In more decent AmigaOS versions, the API AllocBitMap is available which gets width, height, color depth and flags as parameters. On success, it returns a BitMap buffer which is the foundation of AmigaOS graphics library when displaying or manipulating objects. The flag BMF_CLEAR can be used to specify that the allocated raster should be filled with color 0. Finally, BitMap buffers can be freed again via FreeBitMap.
To show the buffer on screen, I am using the Screen datatype which is AmigaOS display abstraction layer. It can be created via OpenScreenTagList. Besides resolution and color depth, it also gets the previously created BitMap as a parameter. Finally, we call ScreenToFront to show the screen and its video buffer on the Amigas display.
If you take a look at the source code, you will notice that Screen and BitMap are created twice. The reason for this is double buffering. So, while my rotation algorithm is drawing into one buffer, the second is put to front and vice versa.
Painting a Square
The video buffer is ready. We now have to think about on how to create an object which can be drawn and rotated. One option is to use Deluxe Paint, save the result as IFF file and load it from floppy or hard disk. Since the bitmap is just needed in a proof of concept implementation, I have chosen a simpler solution.
struct BitMap *rectBitmap = NULL;
struct RastPort rastPort = {0};
InitRastPort(&rastPort);
rastPort.BitMap = rectBitmap;
SetAPen(&rastPort, 1);
RectFill(&rastPort, RECT_X, RECT_Y,
RECT_X + RECT_WIDTH,
RECT_Y + RECT_HEIGHT);
Drawing and font information is stored in a RastPort structure. It is initialized and linked with a BitMap. Finally we select color 1 (depending on the color depth, between 2 and 256 colors are available in a color table) and draw a rectangle into the BitMap. This rectangle will be rotated and displayed on screen later on.
Chunky Planar Conversion
The previously allocated BitMaps are in planar format. In comparison, on modern hardware you usually work with chunky buffers. The following example for a PAL screen with a 4 bit color depth demonstrates the approach:
#define WIDTH 320
#define HEIGHT 256
BYTE chunkyBuffer[WIDTH * HEIGHT];
// write color 5 to pixel (10,15)
chunkyBuffer[ 10 + 15*WIDTH ] = 5;
The chunky buffer in the example above is a byte array. Each element represents a pixel. Since we have a 4 bit color depth, each pixel value is between 0 and 15. The next example demonstrates the same video buffer in chunky format:
#define WIDTH 320
#define HEIGHT 256
BOOL planarBuffer1[WIDTH * HEIGHT];
BOOL planarBuffer2[WIDTH * HEIGHT];
BOOL planarBuffer3[WIDTH * HEIGHT];
BOOL planarBuffer4[WIDTH * HEIGHT];
// write color 5 to pixel (10,15)
planarBuffer1[ 10 + 15*WIDTH ] = 1;
planarBuffer2[ 10 + 15*WIDTH ] = 0;
planarBuffer3[ 10 + 15*WIDTH ] = 1;
planarBuffer4[ 10 + 15*WIDTH ] = 0;
In comparison to the chunky format, planar buffers are a set of bit arrays. Each array represents one layer of color depth. This leads to following advantages / disadvantages:
4-Bit PAL Chunky Buffer | 4-Bit PAL Planar Buffer | |
Read/Write operations per Pixel | 1 | 4 |
Size in Bytes | 256 | 128 |
As mentioned above, the Amiga uses BitMaps in planar format which consume less memory when color depth is below 8. This makes perfect sense, because RAM was very expensive when the Amiga was manufactured. The disadvantage is: When accessing a certain pixel, several read or write operations are necessary. Furthermore, you have to work with bit masking since direct bit access in memory is not possible. To sum it up: Planar video buffers are memory friendly but bad for performance. Chunky buffers consume more memory but are easier to access.
My Amiga has a 128MB RAM expansion and a 40Mhz 68030 CPU. Therefore, I can afford a few more bytes for chunky video buffers and CPU cycles for conversion. Many converter implementations already exist. I integrated a converter from Morten Eriksen written in assembler. It provides two functions:
- PlanarToChunkyAsm(): Gets a planar BitMap and converts its content into a byte array.
- ChunkyToPlanarAsm(): Gets a chunky byte array and write its content into a planar BitMap.
AmigaOS can perform the conversion too. I added the flag NATIVE_CONVERTER which disables ChunkyToPlanarAsm() and uses instead:
- WritePixelArray8(): Gets a chunky byte buffer and writes the conversion result in a BitMap encapsulated in a RastPort.
This way, I can play around with different implementations and enable/disable them at compile time.
Rotation Algorithm
This is where the fun begins 😉 Since the whole blog article has become rather huge, I decided to split it up. So, stay tuned for part two.