I wrote previously about RESET pitfalls when writing code for the new and upcoming 8-bit console called the Game Tank. Today I thought we'd look at the pitfalls of trying to do some basics with the Game Tank's Blitter.
This one will be a bit more technical, but less assembly-involved, so buckle up.
The Blitter
The graphics system of the Game Tank is rather unique to me. The Blitter is made up of logic gates -- not driven by a GPU -- and it draws to the screen at the rate of 1 pixel per CPU cycle. It has 2 framebuffers, each having a resolution of 128x128. You can theoretically draw over 3 entire layers' worth of graphics per frame.
The Palette
Before we get into the pitfalls, let's talk about the palette. A color is selected with a single byte. The byte is in the form of hhhsslll. That is, from left to right, the first three bits describe the hue, the next two bits describe the saturation, and the last three bits describe the luminosity.
To put it more simply, look at the above palette image, and then consider the following:
- Hue: which row from the top within the bank
- Saturation: which bank of 8x8 colors, from the left
- Luminosity: which column from the left within the bank
Let's say you want the most upper-right color, which is a bright yellow. With a 0-based index, this is row 0, bank 3, and column 7. Encoding this in binary would be row 000, bank 11, and column 111, or %00011111. In hexadecimal, this is color $1f.
That would look like this in the emulator if you filled the screen (although you may not want to do that for a game -- more on this later):
Pitfalls
Now let's look at the some pitfalls that you may encounter working with the Blitter for the first time.
Inverted Colors for Color Fill
Let's say you enable Color Fill mode by writing both the DMA_COLORFILL_ENABLE and DMA_ENABLE bits to the Video/Blitter Flag Register at $2007.
You choose color $1f like in the above example, but your square looks very dark, like this:
Let's look at some assembly code. First let's assume we have definitions like this:
DMA_ENABLE = %00000001
DMA_COLORFILL_ENABLE = %00001000
BLITTER_FLAGS = $2007
DMA_VX = $4000
DMA_VY = $4001
DMA_GX = $4002
DMA_GY = $4003
DMA_WIDTH = $4004
DMA_HEIGHT = $4005
DMA_START = $4006
DMA_COLOR = $4007
Now let's look at the actual drawing code:
lda #(DMA_COLORFILL_ENABLE | DMA_ENABLE)
sta BLITTER_FLAGS
jsr DrawSquare
...
.proc DrawSquare
lda #$00
sta DMA_VX
lda #$00
sta DMA_VY
lda #$40
sta DMA_WIDTH
lda #$40
sta DMA_HEIGHT
lda #$1f
sta DMA_COLOR
lda #$01
sta DMA_START
rts
.endproc
The code is correct except in one minor point. In the above screenshot of the wiki page on the Blitter Registers, it notes perhaps a bit obscurely, "Value is inverted." Further down on the page, it explains:
For solid color fills, the value written to the COLOR register should be inverted from the expected color number. So to write a color with value 213 set the register to 42.
This means all the bits need to be completely flipped. You can achieve this in ca65 by prefixing a constant number with the ~ character. Although this generates a warning that -1 is out of the range, you can manually select the low byte with the < character. In short:
lda #<~$1f
sta DMA_COLOR ; This achieves the actual color of 1f
Remember, this inversion is not needed for sprites! This only applies to Color Fill Mode.
Using Transparency for Black
Let's say this time, you actually do want a black rectangle. You have 8 different blacks to choose from in the palette, and you choose color 0. You remember to invert it, so you write color $ff to the Blitter, and... nothing. The snow of the randomly generated video memory hasn't changed.
This can be difficult to solve. The actual answer is that color $00 ($ff inverted) doesn't draw black: it is the transparency color. Using this color in Color Fill Mode while DMA_OPAQUE is disabled does nothing more than make the Blitter spin in circles while transparency is enabled.
In order to draw black, you have two options:
- You can set DMA_OPAQUE in the Blitter Flags Register to disable transparency altogether.
- You can use color $20 ($df inverted).
Option #2 is the one I'd personally recommend for simple rectangles.
Using Width or Height 128
Since the video dimensions of each framebuffer is 128x128, it might make sense to you that you can draw a width and a height of 128 each to fill the screen. Unfortunately, this isn't the case. Look again at the WIDTH and HEIGHT register descriptions. Bit 7controls flipping for horizontal for WIDTH and vertical for HEIGHT. If both high bits are set, then the Blitter tries to draw to the left and up respectively.
Specifically, if you attempt to fill the screen from (0, 0) with a width and height of 128 each, you'll end up drawing... nothing. It can't go backwards from (0, 0) in either width or height, so you will be looking at snow again.
Unintentionally Writing to Column 127
Because the 128x128 display ratio of 1:1 isn't the television aspect ratio, the Game Tank's display has vertical bars on either side of its display output. You are able to change the color of these bars for each row by writing to column 127.
There are actually two pitfalls here.
- Unintentionally changing the border color of a scanline by letting a draw write into Column 127.
- Destabilizing the TV's picture by changing the border color.
For #1, if you draw a square that touches the edge of the right border (ie. column 127), you end up changing both borders on all scanlines affected. This will quite possibly look like graphics corruption to players.
For #2, televisions are rather funny with how they interpret signals. For example, see how some televisions interpreted the NES color $0d. There's a whole world of timing and colors and signals that go beyond the scope of this article.
Clyde Shaffer, the Game Tank's inventor, said it like this in November of 2025:
The border color has a slight influence on how TVs interpret the rest of the scanline. It varies between televisions, and isn't simulated in the emulator yet. [...] I set it to black by default, but if you use a border effect that sets it to a different solid color the worst that should happen would be slightly dimming the rest of the screen during the effect.
It's probably best to set the border color to black. Nevertheless, if you want to add border effects, then it would be best to try any border effects in the emulator and then on real hardware before you package it in a final game -- and even then, some TVs may still be more sensitive than the ones you've tested. You may want to add a way for the player to disable any border changes to your game (such as in the options menu).
Unintentionally Interrupting a Blit Mid-Draw
The Blitter must be allowed to complete the draw before you begin writing to it again if you wish the draw to occur as you directed it. If your blits are very small -- a handful of pixels in size, let's say -- then your overhead of setting up the next blit might be slow enough to let the Blitter finish.
Thankfully we have a better option than manually counting cycles and pixels: setting DMA_IRQ in the Blitter Flags Register will have the Blitter raise an IRQ when a draw operation is completed. You can maintain a queue of blits, start the first one, and then use the IRQ handler to work its way one by one in the queue.
The above screenshot is from the emulator's snapshot of VRAM. It shows the 1st framebuffer on the top and the 2nd one on the bottom. I had attemped to set both framebuffers to black, but something very clearly went wrong. The last blit in my 1st set of queued draws was partially corrupted. The last blit in my 2nd set of queued draws didn't seem to happen at all.
As it turned out, the flag I was using in my code to determine if my drawing queue was empty was being cleared prematurely. I was detecting if the current blit that was being processed was the last draw operation. This was incorrect logic. I should have been checking if the blit that was just completed was truly the last one.
Conclusion
The Game Tank's Blitter is a very interesting and powerful piece of hardware that will likely be the main defining aspect to the Game Tank, at least for its early era, but the fact we're dealing with low level hardware means that we must be very precise in what we're doing.







Top comments (0)