A Sight for Sore Eyes
by Chris "Kiwidog" Hargrove
Download this week's code files: cotc7src.zip (78k)
Be sure to read the license agreement before using the code.
"What the public wants is the image of passion, not passion itself" - Roland Barthes
Hello again! Well, after all this time, we're finally ready to start diving into some graphics code. That's right, it's time for code with results you can see! :) I know some of you all may think I took way too long to get to this topic, but if you look at the overall theme of this whole column, hopefully you can understand why. Foundation and good architecture always come first.
We'll obviously be working with graphics in one form or another throughout the remainder of the project, but the pure graphics subsystem (or "video subsystem" as I often call it for 2D graphics) is actually not that large at all. I'll still be breaking it up over two articles though, since I'll be using DirectDraw code (which tends to read pretty thick... we're moving away from the "one article one topic" paradigm here). This first article will be focused purely on getting devices and display modes with some initial frame buffer access, and the second will wrap things up with some better surface management and integration with the resource manager for bitmap loading etc. If this last sentence here just went over your head, don't worry, I'll do my best to explain things somewhat :)
The Graphics Rundown
If you're a game programmer or would-be game programmer reading this series, you've probably already mucked with graphics code at least a little bit. Whether it be good old DOS VGA programming or actually working with DirectDraw, graphics seems to be one of the first targets of any game programming enthusiast. It's the route to making stuff that "looks cool", which is a lot of its charm. So if you've already done any graphics code (even in DOS), you're probably very familiar with what a frame buffer is, what a palette entry is, etc.
If you haven't, well, you should know by now that I can't teach everything in this column; there just isn't room for it. But here's the short-short-short overview of what you should know before reading the rest of this article: A "frame buffer" is a buffer of memory that represents your screen's drawing area, which you write to in order to generate the image you want. Frame buffers are generally organized linearly, visually starting at the top left of the screen and reading like a book (left to right, top to bottom). Each pixel may be 8 bits, 16 bits, 24 bits, 32 bits or whatever, depending on the color depth of the video mode you're using. Often you'll actually have two or more frame buffers being used, one called the "primary" or "front" buffer, which is currently being displayed, and the other(s) called "back" buffer(s), which are actually written to (either directly or copied from off screen). Swapping the front buffer's contents with a back buffer is called "page flipping", which is often done in graphics code to reduce screen flicker and cover up the fact that direct writes to the "primary" buffer (whose memory is generally onboard your video card) are slow. When you take an off screen buffer and actually copy its contents into a display buffer, it's called "blitting", and this term is often used even when display memory is not involved but a similar result is required (copying one rectangle's contents to another, etc).
There, that's the quick overview. Not much information, but hopefully it'll be enough to get you through all this stuff. Again, I expect that most programming-types who are reading this are probably already familiar with the terminology and the basics of how 2D graphics operations work, but I don't want to assume too much so, well, there you go.
So, ready to take the plunge? :)
You Just Can't Get To HEL Fast Enough
I'm warning you now, DirectDraw code can be rather nasty to read and even nastier to write. The initialization is a large process, and you have to contend with several enumeration functions, device choices between things like a HAL (hardware abstraction layer), HEL (hardware emulation layer), a boatload of code to deal with DirectDraw "surfaces", and on and on. So don't expect a walk in the park, without getting mugged at least. The code for today's article may be only 500 lines or so, but it's not a pretty 500 lines.
But, gotta bite the bullet sometime, right? :) Okay, I've thrown on two more files today, vid_win.cpp and vid_win.h. We'll have a couple vid_main files added on next time as well, but those will be saved until the DirectDraw side is mostly out of the way (and we'll need to do some more surface management code next time before that's the case). So keep in mind that all sample code you see in sys_main which uses vid_win is purely temporary; vid_win's interface will disappear from public eyes once the next article rolls around.
I'll go over the code layout here, but for DirectDraw function call specifics you'll want to have your DirectX SDK documentation handy (it's Microsoft's job to document their interfaces, not mine :)
[Look at vid_win.h]
Our interface to DirectDraw is pretty small, and although it won't be completed until the next article, most of it's already here. Aside from the "usual three" (init, shutdown, and frame) functions, all we need for the moment are a function to set a video mode, functions to get some current information like width, height, pitch, and frame buffer pointer, and finally some palette functions. For the moment I'm using palettes and assuming a 256 color (8-bit) video mode, since that's what this game will likely end up using.
For those of you who aren't familiar with screen "pitch", it's the logical width between scanlines, which is often the same as the actual screen width but sometimes more. When dealing with DirectDraw, you need to use pitch instead of width when calculating drawing offsets (look at the sample code in sys_main for an example).
Anyway, so it's not a very big interface. But as usual, there's more to it on the inside...
[Look at vid_main.cpp]
One of the first things you'll notice are two structures for "devices" and "modes". DirectDraw allows you to hook your video output into one of possibly many video devices, not just your primary display (you may have an auxiliary video card, or some headset thingy, or whatever), so we'll use a pool of these device structures to hold whatever devices we find in the system. The same goes for modes; each device may support a large number of video modes (screen resolutions). Both the device and mode lists will be filled in by two DirectDraw "enumeration" functions that we deal with in VIDW_Init. Once we have these filled in, we won't have to make DirectDraw fail on a call just to find out a video mode isn't there. Later on we'll also use the device and mode lists to let users choose their screen output device and resolution within the game.
Down in the implementation data section, we have the first of our DirectDraw internal stuff, followed by those device and mode pools I just mentioned, and then some current status variables for width, height, etc (accessible from the outside via interface calls). There's also the 768 bytes (256*3) set aside for our internal copy of the palette, so when using 8-bit output we can change video modes without losing color information.
Now down a bit lower in the implementation function area is where much of the work is being done. There are functions here which initialize and shut down a device, and which initialize and shut down a mode. If you haven't mucked with DirectDraw much or at all and want to see an example step-by-step process for getting a video mode up and running, walk through the device and mode initialization functions, because that's where it is. I'd suggest having your SDK documentation handy when walking through those functions.
Following those four functions are the two enumeration "callback" functions used by DirectDraw to enumerate devices and modes. Down in the interface section, VIDW_Init uses these to build up our device and mode lists as mentioned before (actually, that's all VIDW_Init does right now).
Everything else should be moderately straightforward, assuming that graphics code is not completely new to you and you aren't afraid of DirectDraw. If either of those two aren't the case, you might want to check out some other books or documentation online (including the DirectX SDK) before trying to make sense of this code. I've tried to keep the code clean and relatively commented, but I can't teach you everything (like I said, I'm not writing a book here). If it takes you a bit of time to work through this code, don't sweat it... after all it's two weeks until the next article, right? :)
We'll finish up the DirectDraw layer and its higher-level "vid_main" companion next time around, where we add in some more comprehensive surface support to deal with onboard sprites, etc.
Until next time,
- Chris"Kiwidog" Hargrove is a programmer at 3D Realms Entertainment working on Duke Nukem Forever.
Code on the Cob is © 1998 Chris Hargrove.
Reprinted with permission.