Last time, I left you after an introduction to the Amiga. This time, let’s get right down to the technical stuff. The first thing we have to do, is to set up our graphics screen.
More custom hardware: the copper
I have already introduced you to the blitter, which we will be using later. However, first I must introduce another bit of custom hardware, because we will be needing it to set up our screen. That is the co–processor in the Amiga, affectionately know as the ‘copper’. It is an extremely simple processor, which has only 3 instructions: WAIT, MOVE and SKIP. Nevertheless it is an important tool in the Amiga. The key characteristic of the copper is that it is synchronized with the raster position (the position of the electron beam in an old CRT display). The copper restarts at every frame.
There are counters for the horizontal and vertical pixel positions, and the WAIT-instruction can be used to wait for a certain position on screen. Then the MOVE instruction can write new values to the registers of the custom chips. And finally the SKIP-instruction can be used to skip the next instruction if the current raster position is equal to or greater than the one specified in the SKIP instruction. This can be used to implement conditional jumps. A program for the copper is known as a ‘copper list’.
In fact, you may have heard of an effect known as ‘copper bars’ (okay, so this demo takes the concept a lot further than just the classic horizontal bars):
This effect is named after the Amiga’s copper, since it is a very simple effect if you use the copper: you just change the colours of the palette at every scanline (and then take it from there, changing the colours at horizontal positions as well, drawing all sorts of patterns, go crazy with it, and you get the Copper Master demo). This has no effect on the image above the current position, since it has already been drawn by the monitor. For the copper it is really simple: just program a WAIT for the end of a scanline, then a MOVE to update the palette. On other machines it may be far more difficult to get the timing right. The effect has been done on other machines, but calling them ‘copper bars’ on machines without a copper is a bit strange. The more generic name for the effect is ‘raster bars’.
Setting up a screen
Setting up a screen on the Amiga is quite difficult. On the PC, you have some BIOS routines that you can use to initialize a video mode (good ole int 10h). With things like the unchained mode, mode X and double-buffering, you are modifying some of the videochip’s registers directly, but you don’t have to set everything up from scratch by yourself. On the Amiga you do, however. You have to tell the Amiga how many bitplanes you want, where you have them stored in memory, whether you want hi-res mode or not, interlace or not, and you even have to set up all kinds of timings that determine exactly where your visible screen starts and ends, both horizontally and vertically (so you have a lot of control over the actual resolution of the screen, allowing for smaller viewports, or ‘overscan’, where your image covers the entire screen, no borders).
Another difference with the PC is that some of these registers have to be re-initialized every frame. So you don’t really set a videomode as such. On the other hand, you can also re-set these registers during a frame, effectively combining multiple videomodes on the same screen. This is where the copper comes in: you create a copperlist that sets up the videomode you want. The copper restarts for every frame, so now the videomode is taken care of automatically.
The Amiga doesn’t have videomemory in the traditional sense. This is what sets it apart from other systems, especially from PC graphics cards. As I explained in part 1, setting up double buffering on EGA and VGA is quite tricky. You need to have the second buffer in videomemory, otherwise you cannot do a page flip. And in the case of VGA this was only possible in unchained mode, not in regular mode 13h. CGA only had 16k of memory, so it could not do page flipping at all.
With the Amiga, there is one big pool of memory, which both the custom chips and the CPU can access. This is called ‘chipmem’. A standard Amiga generally came with 512KB or 1MB of chipmem. There was plenty of room to store your graphics buffers there. You just allocated your own bitplanes anywhere in chipmem and then passed the pointers to the custom chips in your copperlist. Each bitplane had a separate pointer. The bitplanes themselves are in the same ‘classic’ planar format as EGA, which I covered in part 1.
I won’t go into detail on the exact registers to set, and the formulas required to work out some of the timings. They are explained in great detail in the Amiga Hardware Reference Manual and the Amiga System Programmers Guide that I mentioned in part 2.
Drawing a line
Right, the first step in rendering 3D graphics on the Amiga is to make a line drawing routine. There is a Bresenham line drawing mode built into the blitter. All it takes is to set the right parameters to the right registers, and it will draw the line for you. The symmetry of lines is exploited: you have to determine which octant your line goes through. This results in a 3-bit value that tells the blitter how to step along the line. Other than that, you specify some delta values and the starting point, and that is more or less it. Again, these values are explained in detail in the aforementioned documentation. So there is not that much to it, is there? At any rate, I just followed the steps and had some lines on screen reasonably quickly.
Filling a polygon
Rendering polygons as wireframe is nice, but what we are really after are filled polygons ofcourse. As I already mentioned previously, the blitter can perform an area fill (flood-fill) operation. You mark the outlines of a shape, and then have the blitter fill it. The blitter can work inside a rectangle of any size. So you determine the bounding box of your polygon, and let the blitter work within that box only. While that sounds simple in theory, there is a catch…
The blitter can only fill from right to left, and only between two set pixels. Each pixel it encounters will toggle the fill mode. Therefore it is important to have exactly 2 pixels on a scanline, or else you will get filling errors. There is a special line drawing mode where the blitter draws only one pixel per scanline, which you need to use for blitter fills. Not many people will get it right the first time, I suppose. I most certainly did not. I ended up with something like this:
As you can see, in some places a pixel on the left edge is missing, so the blitter will continue filling until it reaches the end of the bounding box. In other places, there is a pixel missing on the right edge, so it never starts filling until it reaches the left edge. Another problem is that my bounding box did not fit properly. It has to be aligned to word-size. My box was a word short on the left side, missing up to 16 pixels.
So, there isn’t much you can change about the fill routine, aside from the bounding box. But that is a simple problem. The other issues stem from the line drawing routine. While trying to solve these problem, I came across an online version of the Amiga Hardware Reference Manual. It contained a passage on line drawing that was missing from the older version that I was using. The first problem I noticed was that a formula that I used from the Amiga System Programmers Guide was actually off by 1 pixel: the length of the line that is stored in the bltsize register is 1 pixel too short. I did notice that the endpoints looked a bit sloppy at times while rendering in wireframe mode, but I did not pay too much attention to it. With the modified length, the wireframes now touched nicely at the endpoints.
Are we there yet? No we aren’t. A problem with most rasterization algorithms, including Bresenham, is that they have slight rounding errors. As a result, they are not fully symmetric. If you try to render a line from point A to B, and then render the same line again from B to A, the resulting line might be *slightly* different. A good way to visualize this is to render the line in xor-mode: the first time a pixel is rendered, it is turned on, the second time it is rendered, it is turned off. Any pixels that occur in only one of the two lines will remain visible.
Normally you would always rasterize a polygon in the same direction, generally from top to bottom. This ensures that an edge will have the same errors, regardless of whether it’s on the left side or the right side of a polygon. So neighbouring polygons will fit together nicely. In the Amiga’s case, we are going to take the same approach: we sort the points of each line on their y-coordinates, so they are always rendered top-down.
Now we have reduced our problem areas to the endpoints of the lines: when two endpoints meet at a sharp angle, this results in an endpoint of only one pixel on a scanline. There is no way to fill this properly. There are probably many different ways to solve this, but I will just mention two possibilities, which I find simple yet elegant. The first is the aforementioned xor-mode of line drawing: when the first endpoint is drawn, the pixel is turned on. The second endpoint turns it back off again. This way the filler will just ignore the scanline altogether, and fill correctly (perhaps you want to plot the endpoints separately so that there still is a pixel on that scanline, but for performance reasons this is generally not done on Amiga).
The second possibility stems from an interesting hardware-quirk in the Amiga. Namely, there are two pointer registers used for the linedrawing mode. They need to be set to the same address, according to the Hardware Reference Manual. However, as people have discovered, one of the pointers (bltdpt) is only used to draw the first pixel of the line. Every other pixel is rendered via the other pointer (bltcpt). This ‘feature’ can be exploited in our case: by pointing the bltdpt register to somewhere else than our drawing buffer, the first pixel of the line will magically ‘disappear’, and solve some of our endpoint problems.
Another small issue is that of choosing the right fill mode. The blitter supports both an ‘inclusive’ and an ‘exclusive’ fill. In inclusive mode, both the left and right edges are included. In exclusive mode, the right edge is included (the starting edge), but the left edge is not. This is the mode we want to use when rendering polygons: When two polygons are side-by-side, sharing the same edge, the edge is rendered only once, on the leftmost polygon (where the edge is on the right side). Otherwise the edges would overlap, and this could cause colour errors when side-by-side polygons are rendered to different bitplanes.
After I tweaked the linedrawing well enough (I apply all three methods: the sorting, the xor-linedrawing and the bltdpt exploit to remove the first point), my routine was now filling the polygon without excess pixels leaking out:
(There is still some visible flickering. This is because I am not using double-buffering yet. On the other hand, it has the nice side-effect that you can see how the edges of the polygon are marked by the linedrawer first, after which the filler is applied)
Drawing the polygons to the screen
Okay, so now we can rasterize the shape of a polygon into a bitplane. Are we there yet? Well no. This is only a single bitplane. If we want to be able to use all possible colours, then we need to be able to render into multiple bitplanes as well. Unfortunately the blitter cannot render to multiple bitplanes at a time, like we’ve seen earlier with EGA and VGA. So what do we do then? Do we just repeat the drawing operation for all bitplanes? Well, that would work, but that would result in the overhead of setting up and drawing the lines and doing the fill operation for every bitplane. A simpler, more efficient approach is to rasterize the polygon once, then use the blitter to copy it to the other bitplanes.
But what if we want to render more than one polygon? As we’ve already seen above, the blitter fill will only work correctly when there are exactly two pixels on a scanline. So if you want to render into an area where there are already other pixels, you will run into problems with the filler again.
Use the scratchpad, Luke!
Again there is a solution to this problem, and it is not even that difficult. The blitter can render anywhere in chipmem, so it is easy to just set up a temporary clean buffer, a ‘scratchpad’, and render the polygon into that. Then you copy it to the actual screen area using a masked blit operation. This is also affectionately known as a ‘cookie cut’. It is basically the same sort of operation that you would use with 2D bitmaps, where you only write pixels when they are set in the source image, and leave the target pixels untouched otherwise (a logical OR operation), a simple form of transparency. This will combine the polygons properly on screen.
In fact, perhaps this would be a good time to explain the blitter in a bit more detail. The blitter has 3 inputs and 1 output. These are all handled by DMA, so it can run completely independently from the CPU, once it is set up. The 3 inputs can be combined via logical operations, so-called min-terms. The result is then written to the output channel. In the case of a masked blit, you would basically perform an operation like this:
output = (mask AND bitmap) OR (NOT mask AND output)
So you would require 3 inputs here: the mask, the bitmap, and the output itself (so the pixels can be written back unmodified). The xor-linedrawing mode I mentioned earlier is done in a similar way, just a different min-term. The blitter can perform these operations in rectangular windows in memory, overcoming the problem that the width of the bitmaps is not the same as the width of the screen area. It processes data one scanline at a time, then adds a ‘modulo’ value to each of the pointers.
The blitter addresses the memory with word granularity. Therefore it also has a shift-value which can be applied to some of the inputs. This shifts the input data by 0-15 bits, so that you can still move data around with the bit-accurate precision that is required for pixel-addressing in graphics (at least, with planar addressing as used by the Amiga).
Instead of cleaning the buffer after every polygon, you could use a large scratchpad buffer. You ‘allocate’ enough room for the bounding box of your polygon and rasterize it, then mask-blit it to the screen-area. Then you advance the pointer of your buffer by the size of the bounding box, giving you a clean work area again for the next polygon. Repeat until you have rendered all polygons (or until the buffer runs out), and then you can clear it with a single large clear operation, again using the blitter.
Now that your polygons can be rendered to screen, we need to make them update without flickering. On Amiga, double-buffering is not that difficult: you can make as many screen buffers as you like in chipmem. So just create two sets of bitplanes. As mentioned before, these bitplanes will be set up by a copperlist at every frame. A simple way to implement double-buffering is to just overwrite the pointers in your copperlist everytime you what to swap the front- and backbuffer. You just have to be careful not to overwrite the copperlist while it is running. Updating the copperlist when the vertical blank interval is in progress should be safe.
Once we’ve done all that, things should come out more or less like this:
Well, that covers the Amiga-specifics as far as polygon rendering is concerned, I suppose. Obviously there are other things you need to do before you can actually render 3D objects to the screen, but these are the same on all platforms: z-sorting, backface-culling, polygon clipping, transform and perspective project, perhaps some lighting… But I will leave that for another day.