I knew right away that I wanted to make some architectural improvements before getting too far into this year’s project. For example, I wanted to be able to read geometry definitions from a file, rather than having them hardcoded into the application. I also wanted to make the organization of the code better encapsulated. My brain likes to think about software in an object-oriented way, even if I’m using a language like plain old C. All that class stuff is just syntactic sugar anyway, right?
I figured this would be a quick task that wouldn’t cause any trouble. My first test ended with a screenful of garbage followed by a Guru Meditation error. The gray and green lines suggest that not only was I not drawing graphics in the right location, I wasn’t drawing them in the right bitplane. Somehow I’d scribbled into memory far away from where I’d intended to, and the results were fatal.
This is an important reminder of a – perhaps the – key point of developing Amiga software. The Amiga is a multitasking computer running many user and system processes in a single shared memory space with absolutely no protection between processes. This combination is very efficient and very powerful, but it’s also extremely brittle. It’s trivially easy for one runaway program to trash the entire system, like mine had just done.
That particular error turned out to be a straightforward type mismatch in a call to fscanf(), which completely screwed up my vertex data. Since there’s no memory protection, and since the line drawing function didn’t check for out-of-bounds coordinates, it happily scribbled pixels across a bunch of neighboring memory regions (including the bitmaps for the other bitplanes of the image).
Once things were working again, I made the mistake of looking for trouble. I said the Amiga OS was brittle, but in spite of that – or actually, because of that – a lot of Amiga software is very robust. Of course you don’t want an application crash to hang the system, but there’s even more to it than that. Since all Amiga programs share the same memory space, memory leaks are a critical problem – that leaked memory is effectively gone until the system reboots. Good Amiga programs are very careful about letting go of their resources.
My program? Not so good. I was losing 1104 bytes from the system each time I ran the program. Now in all honesty I could have just ignored that. Considering the features I want to add, I’ll probably be crashing the machine way too frequently for 1104 bytes a pop to become a bottleneck. But darn it, this was a matter of pride.
Over the course of several pretty frustrating hours, I worked the memory leak down to 656 bytes and then to 368. Eventually I got it down to 224, and there I was stuck for a while.
Most of the leaked bytes came from an obvious source. Shortly after the end of the 2014 Winter Warmup, I rewrote the screen setup code so that it used the Amiga’s graphics.library directly, instead of the higher-level intuition.library. This gave me a noticeable boost to the framerate, and was totally worth it. However, I got distracted by other things and never really cleaned up the code. So the setup and teardown were organized differently, and it was easy to miss things.
Honestly, the graphics.library API is kind of a mess – when Commodore released Workbench 2.04, they added a lot of options for more flexibly controlling the Amiga’s video hardware. To do that, they added some new data structures. So for each of the standard OS structures that define a screen, like View and ViewPort, there are “associated” structs like ViewExtra and ViewPortExtra. In what may have been a stab at planning for the future, those “Extra” structs (but not anything else) are allocated and deallocated with a special set of function calls (GfxNew() and GfxFree()) instead of using malloc() or the OS-defined AllocMem().
By the way, when you’re allocating things using a bunch of different function calls – malloc(), AllocMem(), GfxNew(), etc., it’s important that your allocators and deallocators match. Don’t FreeMem() memory that was malloc()ed, or vice versa, and definitely don’t use FreeVec() when you mean FreeMem() or even GfxFree().
I eventually made sure that every View, ViewPort, Extra object, ColorMap, RastPort, BitMap, etc., was deallocated using the right function when the program terminated, but it still wasn’t enough.
The last couple of glitches I found were the sort of things an experienced programmer could look at for an hour before suddently saying, “Oh, duh!” and instantly fixing it. For example, the system FreeMem call takes the size of memory to free as an argument. In a case like that it’s really easy to type “FreeMem (sizeof (view))” (where view is a “struct View*”) instead of “FreeMem (sizeof (struct View))”. And of course, both lines make perfect sense to the compiler. Problems like that make me miss C++11 and RAII.
Eventually I found the last problem: I was creating the same ColorMap structure in two different places but only deallocating it once. With that out of the way, I did a test and found that the system available memory was exactly the same before and after running the graphics program. Hurray, at last! With the basics out of the way, maybe we can do something fun now.