Keith Holman and Zach Barth are developers at the game studio Zachtronics. If you like obscurely technical topics like writing games for 25-year-old computers, you’ll probably like our “puzzle games for programmers,” including SpaceChem, Infinifactory, TIS-100, and SHENZHEN I/O!
Getting Started
Can two programmers who are accustomed to making games for modern computers with gigabytes of RAM and high-color HD displays port one of their games to MS-DOS? Neither of us had any experience developing on such old hardware, but since working within artificially limited systems is something of a Zachtronics game design specialty, we felt compelled to try!
The project began when Zach created a mockup of SHENZHEN SOLITAIRE, the solitaire minigame from our game SHENZHEN I/O (also available as a standalone game), as it might look running on a 256-color VGA display:
It looks just like you would find on a PC in the early 90’s! From there it was simply a matter of writing the code. Right?
The Development Environment
First we had to figure out how to write anything that would run on an ancient DOS computer. Our target hardware was an appropriately vintage IBM-compatible PC from Zach’s collection:
Intel 80386SX CPU at 20 MHz
5 MB of RAM
VGA card
3½ inch floppy disk drive
Serial mouse
MS-DOS version 6.22
The only reasonable choice of programming language for a machine of this age was C. We weren’t going to write the entire game in x86 assembly language! After considering various choices of tools, we settled on Borland C++ 3.1, published in 1992. Running on DOSBox, Borland C++ provided a convenient and accurate emulation of the target machine.
Graphics
Computers with VGA graphics had a couple different drawing modes. Mode 13h was a popular and easy option: 320 x 200 pixel resolution with 256 colors. A more difficult choice was to use the unofficial “Mode X”, which had a higher resolution of 320 x 240 pixels. There are many tutorials on how to use Mode 13h on the internet, and it’s straightforward to program: the 320x200 pixels are represented by a 64,000 byte array, with each byte representing one pixel. Mode X is more obscure and difficult to use, but has certain advantages: Its resolution is 320 by 240, which is one of the few VGA resolutions with square pixels. It also supports drawing with 256 colors chosen from a palette, the most colors you can use in any VGA mode. Because we wanted better graphics and because we enjoy making things hard for ourselves, we decided to use Mode X.
So now we have a graphics mode with lots of pixels and colors (for a computer of that time, anyway). What are we going to draw? The most important categories were:
Full-screen backgrounds (desktop wallpaper, the green card-playing table)
Cards (including card icons and card numbers in three suit colors)
Text (for various buttons and labels)
We already had high-resolution, high-color versions of all these assets from the original version of SHENZHEN SOLITAIRE, but they needed to be converted to a much lower resolution and to use no more than 256 colors in total. There was no clever trick for this conversion process, just a few hours of work in Photoshop manually redrawing cards, symbols, and interface elements and scaling down the resolutions and color palettes of the backgrounds.
Here’s where the major downside of Mode X became relevant. Representing 320 x 240 pixels requires 76,800 bytes. VGA cards have 256 kilobytes of video memory, but divided into four “planes” of 64 kB each, which you can only access one at a time*. This works fine for Mode 13h which only requires 64,000 bytes, but Mode X has to split its video data across multiple planes.
*It’s a little more complicated than this: in some VGA modes, including Mode 13h, you can access multiple planes at once, but doing so comes with other downsides. Mode X limits you to one plane at a time.
At this point the graphics code started to get complicated, so we consulted the the Graphics Programming Black Book by Michael Abrash, the ultimate reference for all things VGA. As the Black Book explains, Mode X divides its pixel data across all four planes; each plane stores a quarter of the pixels, in an interleaved scheme. Plane 0 stores pixels 0, 4, 8, 12, etc. Plane 1 stores pixels 1, 5, 9, 13, and so on.
This is a classic situation in game programming: We know what output we need to produce, and the choice of how to structure the input data (in this case, the images) will have an enormous effect on the complexity and performance of the rendering code. Thankfully we didn’t have to figure it all out ourselves; Abrash has plenty of useful advice on efficiently drawing in Mode X. Since the entire screen is divided into the four planes, and it’s relatively slow to switch between planes, the most convenient (and fastest!) option is to break each image into four blocks of data so that each plane’s worth of data is in one contiguous chunk. This makes the code extremely simple, and also just about as fast as possible. Here’s the code that draws a full-screen (320 x 240) image into VGA memory:
// Code sample: Drawing an image to VGA memory
void far *vgaMemory = MAKE_FAR_POINTER(0xA000, 0x0000);
short bytesPerPlane = (320 / 4) * 240;
for (plane = 0; plane < 4; plane++)
{
SetCurrentVgaPlane(plane);
_fmemcpy(vgaMemory, imageData, bytesPerPlane);
imageData += bytesPerPlane;
}
This code snippet also demonstrates some of the quirks you have to deal with when writing a 16-bit DOS program. With 16-bit pointers, you can only directly address 64 kB of memory. (These are “near” pointers.) However, most DOS computers have much more than 64 kB of memory, and the addresses that correspond to VGA memory take up 64 kB already! In assembler, you deal with this using segment registers; in C, you typically use “far pointers”, which are a 32-bit pointer type that lets you address up to 1 megabyte. (Life becomes much easier once 32 bit processors show up, because then you can represent memory addresses up to 4 gigabytes without worrying about segment registers.)
Fortunately, 64 kB is quite a lot of memory, and nearly the entire game fits within that limit. There are only two parts of the code that require the use of far pointers. The first I’ve already mentioned: VGA memory is mapped to an entire 64 kB range at address 0xA0000. The second chunk was the image data. After converting all our art to low resolution and one byte-per-pixel, we had a total of about 250 kB of image data stored in one big file. Being more than 64 kB, this also required a far pointer. It also marked the one and only instance in the game’s code where we used dynamic memory allocation…
Memory Management
A major source of bugs and complexity in many C programs arises from managing dynamically allocated memory. In Shenzhen Solitaire, we kept things simple: we knew exactly how many cards we needed to keep track of, so we simply allocated the memory for them up front. There were no malloc() calls that could fail, and no free() calls for us to forget. (The same strategy applies to state throughout the rest of the game.) Here’s what the solitaire engine’s state looks like:
// Code sample: Declaring state for the solitaire engine
struct CardOrCell
{
byte Suit;
byte Value;
byte CellType;
struct CardOrCell *Parent;
struct CardOrCell *Child;
};
CardOrCell Cards[NUM_CARDS];
CardOrCell FreeCells[NUM_FREE_CELLS];
CardOrCell FoundationCells[NUM_FOUNDATION_CELLS];
CardOrCell TableauCells[NUM_TABLEAU_CELLS];
CardOrCell FlowerCell;
CardOrCell *DraggedCard;
As mentioned previously, the one place where we couldn’t use this strategy was for image data, because that was much larger than 64 kB. Instead, we stored all the images in one big file (with the image data divided into four planes, as detailed above) and load it in at startup while the loading bar is displayed:
// Code sample: Loading image data from file
// In the actual source code, this is more complicated and has
// more robust error handling.
FILE *f = fopen("SHENZHEN.IMG", "rb"); assert(f);
long size = GetSizeOfFile(f);
ImageData = farmalloc(size); assert(ImageData);
long loaded = 0;
byte huge *dest = ImageData;
while (loaded < size)
{