Watch the newest gameplay demo of Magicore Anomala: Link
The technology behind Magicore Anomala requires the background to be 8 colors or less, although any Amiga fan will know that the system is capable of a rather incredible color range. As with many classic systems, Amiga's hardware limitations are extremely fascinating and follow rules that can be cleverly bent.
Why 8 colors?
Amiga has a palette of 32 colors, but those colors aren't fixed. The programmer can take any RGB value and assign it to a color in the palette. Magicore reserves 16 colors for sprites (colors 16-31), so realistically, we have 16 colors to draw the screen with (colors 0-15).
Amiga uses bitplanes, which means we don't have one single screen space—we define multiple screens, each 1 bit per pixel. That means each pixel can be either "on" or "off", like classic Macintosh systems.
The difference is that with multiple bitplanes, Amiga can take several 1-bit screens and stack them together to create a multicolored display. My background is 3 bitplanes, which means I'm stacking three 1-bit screens to give myself 3 bits per pixel. 3 bits can represent 000-111
or 0-7, hence 8 colors.
For example, here is a diagram of an 8-color background that has been split into three individual bitplanes. Any given pixel can be on or off in each bitplane, for a total of 8 combinations.
The illustrator doesn't need to worry about this. They can simply produce an 8-color PNG image—it's converted to bitplanes during the build process.
Bitplanes make Magicore possible
Bitplanes are the entire reason I conceptualized Magicore. I asked myself: Is Amiga fast enough to completely clear and redraw one bitplane every single frame?
The answer is yes—very yes. In fact, I can clear and draw up to 200 unique bullets every frame, and maintain a full 60fps. With that kind of throughput, I was set on making a bullet hell game.
But the limitation at play is that the attacks layer is just one bitplane, which means each pixel only gets 1 bit (on or off).
When stacking the attacks bitplane on top of the 3 background bitplanes, interesting things happen. The background alone, 3 bitplanes, means each pixel can be assigned a color in the range 000-111
(0-7). If we're drawing another bitplane on top of that—in this case, the attacks—the pixel is transformed to the color range 1000-1111
(we're adding an extra bit to the front). Now, any pixel where there's no bullet is color 0000-0111
(0-7), and a pixel containing a bullet is color 1000-1111
(8-15).
So, despite the attacks layer being just 1 bit per pixel, the way it gets stacked on the background means it takes up 8 colors in our palette.
That's the full distribution of the color palette: 0-7 for the background, 8-15 for the attacks, and 16-31 for the sprites.
8 palette slots != 8 colors
What happens if we change the color palette in the middle of the frame?
Like most classic systems, Amiga doesn't build a complete frame buffer—it draws the screen line by line, taking about 16.67ms (or 20ms for PAL) to draw the entire frame. That means we can edit hardware registers, like the color palette, while the frame is still being drawn.
Amiga has a special chip, the Copper (coprocessor), to handle this. The Copper takes a unique instruction set whose entire purpose is to wait for the desired line on the screen, then adjust hardware registers with near-perfect timing.
In fact, the Copper is so good at its job that if I really wanted, I could give it instructions to change all 8 background colors on every single line of the screen. And it does this in parallel with the CPU!
Logistics hurdles
So, using the Copper, we can change the color palette while different regions of the background are being displayed, creating an even more colorful image. That's great, but how the heck do we produce backgrounds to comply with such a specific workaround?
The first rule is that a background can't have more than 8 colors per horizontal line. Realistically, the Copper isn't precise enough for pixel-perfect color swaps, so we're only changing the palette in between lines.
That means we're looking at a process like this:
- Illustrate a compliant background image
- Note which lines introduce new colors
- Figure out which palette indices to replace (i.e. which colors are no longer being used) with the new colors
- Carefully produce an 8-color variant of the background, where each pixel is represented by the decided palette index
- Write the Copper instructions to set the palette indices to the correct color values at the correct parts of the screen
Without some kind of automation, this workflow quickly becomes unreasonable. The color regions need careful planning, and any edits to the source image could require also editing (or redoing) those accompanying assets.
Automatic color regions
Magicore's asset data, like graphics and audio, is built using a Python framework I wrote called Simple Binary Builder (see this post). Thanks to this, I can leverage Pillow, an extremely powerful image manipulation library.
Using Pillow, here is the new pipeline that background images are put through as part of my data compiler:
- The background is split into 180 individual lines. Each line is quantized so that its palette contains only the colors used in that line.
- Traversing line by line, we figure out what the "color regions" are, based on which lines have new colors introduced.
- The lines are grouped into regions of 8-color palettes.
- From one region to the next, we rearrange the palettes to match as many colors as possible between regions. (Pillow can use
remap_palette()
to reorder a palette, and rewrite the image contents to match the new palette indices.) - Now, we can go through each region's palette and determine which color indices need to be updated per region. This is written to a color region data table that's separate from the image.
- All the regions are recombined into a single image, using the palette of the topmost region as the starting palette.
Below is an example of a background image that has been processed down to 8 colors, and then after its color regions are applied (with lines showing separation between regions).
See how in the top image, the grassy hill is the same color as the sky? That's because when the image processor needed a new color region for the hill, it noticed that the sky color is no longer being used. So, the hill was assigned that same palette index to be recycled into a new color.
Now, back to the header image:
The background here is a free image asset I found online (illustrated by ansimuz) to use as a stress test. In the final game, an artist will be illustrating each background with color regions in mind, but I wanted to see how far I could push the color regions.
It works even better than I expected—there's so much detail preserved in the moon, mountains, clouds, and trees. Including all the color regions, the background has a total of 23 colors!
As a reminder, the background uses palette indices 0-7. The cyan boing ball is on the attacks layer, so it's using palette indices 8-15. By copying colors 0-7 into colors 8-15 and boosting the green and blue values, we get this cool additive blending effect.
Finally, the character standing on top of the ball is a sprite, using colors 24-31.
Building the copperlist
Once the game engine loads the background and its color region data, it needs to assemble a copperlist (set of Copper instructions) to change colors on the correct lines of the screen.
The copperlist itself is pretty simple, looking something like this after it's built:
dc.w $9c07,$fffe ; hill
dc.w COLOR07,$0ce8
dc.w $b607,$fffe ; platforms
dc.w COLOR06,$0db8
dc.w $be07,$fffe ; hill base
dc.w COLOR06,$0bd8
dc.w $d507,$fffe ; ground
dc.w COLOR07,$08b7
dc.w $01fe,$0000 ; ground 2
dc.w COLOR05,$0344
dc.w $01fe,$0000 ; ground 3
dc.w COLOR04,$0486
For each region, the first line specifies which line to wait until (e.g. $9c
or 156
for the hill). Then, the second line sets the appropriate color.
The ground requires 3 colors to be changed on the same line, so the latter two "wait" instructions are no-ops. Each color definition needs to remain evenly-spaced, because the colors in this copperlist aren't static—they're modified by an external loop that writes to the copperlist like an array.
Why do the colors need to be modified? Primarily, to handle screen fades.
It's easy to forget, but screen fades aren't very simple on old hardware—they typically require running calculations on every color in the palette.
This means there has to be some middle layer that can transform the colors before applying them to the copperlist. Magicore's game engine already has a lot of good functions and data structures for this purpose (see this post), so it was straightforward to set them up for use on this copperlist as well.
Did you also notice the screen-shake effect in the above video? That's interesting, and a little less straightforward. If the background is moving up and down, that means the lines that require color changes are shifting around all over the place.
So, I have to account for that as well. Whenever there's a vertical scroll, the copperlist is updated to offset all of those line definitions by the scroll amount.
Side note: The textbox that slides in at the end of that video? That also uses the copperlist to edit all 32 colors, as well as the screen pointer itself, to render the portrait and text region. That's right, the "video RAM" can be anywhere you want in memory, and even changed mid-frame with a simple pointer update. So, the textbox isn't drawn "on top" of the main screen—it has its own dedicated memory space which is composited to the top of the display using the Copper. Isn't that incredible for 1985 hardware?
The heart of Amiga
I consider Magicore to be an honest game—that is, I'm not using crazy tricks and demoscene effects to make it possible. The engine is built with consistency, flexibility, and good-practice principles.
In fact, given the function of bitplanes and the Copper, the Amiga was practically designed with these kinds of effects in mind. The way bitplanes enable "stackable" graphics, and the way the Copper allows for adjusting the screen mid-frame—that's the vision under which Amiga was designed. It's the reason Magicore is an Amiga game not just in code, but at its heart.