Converting MS-DOS-based Games to Windows 95: A Report from the Trenches

August 15, 1996

Zachary Simpson, Titanic Entertainment

Table of Contents

Overview

Events

Drawing

Sound

Miscellaneous

Appendix A. Event Interface

Appendix B. Screen Interface

Appendix C. Sound Interface

Overview

This is not a FAQ, it's a ZAQ--a list of questions I asked when I was trying to convert a game from MS-DOS® to Windows®. What follows is a guide that is far from complete but may offer answers to a few of the basic theory and coding questions that don't seem to be found elsewhere.

The best source of information about Windows is the MSDN Library. If you're not an MSDN member, stop whatever you're doing and sign up now. If, like me, you're too cheap to pay the member fee, find a friend who subscribes and get an old copy of the quarterly CD: everything you might want--with the exception of DirectX 2--will be there.

Events

Is it just me, or is the Windows event system just really stupid?

No, its not you; the Windows event system is a mess, and its inefficiencies become painfully apparent when you're writing a game. Remember that the system was never really designed to do what you want. Whereas a game typically polls the input devices once per frame, a normal Windows program is entirely event driven. That is, a "good" Windows program doesn't do anything until the user performs an action. This, of course, doesn't map well to games: a game is typically doing something, such as rendering or physics, whether or not the user is providing input.

One snag with implementing a polled system under the Windows WndProc() system is that keyboard events are reported in two separate, partially overlapping, messages. Furthermore, the definitions for the key constants are not interchangeable between ASCII characters and key codes. For example, the code for F1 is 0x70, which is the same as the ASCII value of "p." Thus, a single switch statement cannot contain both F1 and "p." Appendix A, "Event Interface," demonstrates code that converts all of the values into a homogenous set. Note that this is source from three separate files with functionality deliberately separated to avoid unnecessary header file inclusion.

What do I do with all this "message pump" stuff?

Windows 3.1 was designed to be cooperatively multitasked (as opposed to preemptively multitasked). In a cooperative system, each application is responsible for relinquishing control back to the operating system before another application gets any CPU. For example, suppose an application goes into an infinite loop. Under Windows 3.1, because the application would never relinquish control back to the OS, the machine would lock up. Under Windows 95, however, the OS would task-switch away from the process containing the infinite loop and other applications would continue to execute. Applications under Windows 3.1 had to have a standard place to return control back to the OS. In fact, this was done behind the scenes in the nonobvious GetMessage() function, which is, as you would expect, called from the main loop. Actually, in many typical Windows programs, it is the main loop.


int CALLBACK WinMain(HANDLE _hInst, HANDLE hPrevInst, LPSTR nCmdParam, int nCmdShow)
{
   // Window setup
   . . .

   // The message pump is also the main loop
   MSG msg;
   while (GetMessage (&msg, hWnd, 0, 0)) {
      TranslateMessage (&msg);
      DispatchMessage (&msg);
   }
}

The While loop is referred to here as the message pump. It causes Windows to check the pending queue of events for events that are applicable to this application. Here's the weird part: this function returns only when there are messages pending; in other words, this is a blocking call. The return state of GetMessage() is not true if message pending but rather true if message is something other than WM_QUIT. So you can see that this function is completely tailored to a traditional Windows application. Clearly, a game would not want to block in the main loop waiting for the rest of the OS. In Windows 3.1, a game would write a main loop as follows to avoid the block:


int CALLBACK WinMain(HANDLE _hInst, HANDLE hPrevInst, LPSTR nCmdParam, int nCmdShow) {
   . . .
   while (1) {
      // Poll events without blocking
      MSG msg;
      while (PeekMessage (&msg, hWnd, 0, 0, PM_NOYIELD|PM_REMOVE)) {
         if (msg.message == WM_QUIT) {
            return msg.wParam;
         }
         TranslateMessage (&msg);
         DispatchMessage (&msg);
      }
      // Update the world
      // Render the world
   }
}

In this case, PeekMessage() is used instead of GetMessage(). PeekMessage() returns what you might expect: true if and only if message pending. The PM_NOYIELD tells the OS not to block and the PM_REMOVE tells it to toss the message once you've extracted it.

At the risk of repeating myself, note that this is an infinite loop. An infinite loop under Windows 3.1 would cause all other applications to stop; preemptive multitasking under Windows 95 would allow other applications to execute. Despite this, I still use the nonblocking calls under Windows 95 just so that my application will at least put up a fight.

I program some games to pause when the user switches to another application. It doesn't seem fair to let the bad guys kill the user while he's reading mail. The code looks something like this:


int foreground;
// This variable tells us if the game is in focused application or not

int CALLBACK WinMain(HANDLE _hInst, HANDLE hPrevInst, LPSTR nCmdParam, int nCmdShow) {
   . . .
   while (1) {
      // Poll events without blocking
      MSG msg;
      while (PeekMessage (&msg, hWnd, 0, 0, (foreground?PM_NOYIELD:0)|PM_REMOVE)) {
         // Block if not foreground

         if (msg.message == WM_QUIT) {
            return msg.wParam;
         }
         TranslateMessage (&msg);
         DispatchMessage (&msg);
      }
      // Update the world
      // Render the world
   }
}

long CALLBACK WndProc (HWND hWnd, UINT message, UINT wParam, long lParam) {
   switch(message) {
   case WM_ACTIVATEAPP:
      // This message is sent when the application is
      // either entering the foreground or leaving it.
      foreground = wParam;
      InvalidateRect (hwnd, NULL, TRUE);
      break;

   case WM_PAINT:
      if (foreground) {
         setRepaintAll();
      }
      else {
         // Paint "Game Paused" in the middle of the screen
         PAINTSTRUCT ps;
         RECT rect;
         HDC hdc = BeginPaint (hWnd, &ps) ;
         GetClientRect (hwnd, &rect);
         DrawText (hdc, "Game Paused", -1, &rect,
            DT_SINGLELINE | DT_CENTER | DT_VCENTER);
         EndPaint (hWnd, &ps);
         return 0;
      }
   . . . 
}

The meaning of the WM_PAINT message is explained in more detail below.

What are TranslateMessage and DispatchMessage doing?

Two more lines in the simple message pump need explanation--TranslateMessage() and DispatchMessage().

The TranslateMessage() call looks for certain combinations of events and translates them into Windows commands. For example, if you have a menu bar with, say, f as a hot key, then pressing alt+f should activate the menu and send a menu command message (as opposed to a keyboard message). Generally speaking, you should keep the TranslateMesssage() call in the message pump and avoid any alt+ combinations in your application.

Finally, DispatchMessage() takes the message and calls the WndProc() you registered for your window. Note that this is not the only time WndProc() will be called; occasionally, the OS will call it directly.

If the message pump is not running, you will not receive all events. Occasionally, you may want to call the message pump from some piece of code other than the main loop. For example, sometimes I write a little piece of debugging code that stops and waits for a key so that I can examine some state. In this case, I have a function called waitForKey(), which has a message pump and checks for a key press:


void waitForKey() {
   while (1) {
      MSG msg;
      while (GetMessage (&msg, hWnd, 0, 0)) {
         TranslateMessage (&msg);
         DispatchMessage (&msg);
      }
      if (getEvent(false).isValid()) {
      // Uses my eventinterface system described above
      return;
      }
   }
}

One last piece of advice: profile your input code. Some of the calls I wrote in the previous sample are surprisingly slow. You may need to hack the hell out of this, but the code will at least give you a basic understanding of what's going on.

All I want is the damn time of day in milliseconds. Is this too much to ask?

Yes, it's tragic: no single call does everything you want. If you want the actual time of day, you have to use GetSystemTime(). Although this function returns a field which is milliseconds, it actually has a resolution of about 50ms. You can also use a multimedia function (as if only "multimedia" programs need an accurate clock) called timeGetTime() which does indeed return an accurate time in milliseconds, but its value is the time since the system booted instead of the time of day. If you need accurate time for the purposes of profiling, you can use QueryPerformanceCounter() which gives extremely accurate timings in a completely arbitrary units that must be converted to meaningful units with QueryPerformanceFrequency().


void main() {
   __int64 start, end, freq;

   QueryPerformanceCounter((LARGE_INTEGER*)&start);
   Sleep (1000);
   QueryPerformanceCounter((LARGE_INTEGER*)&end);

   QueryPerformanceFrequency((LARGE_INTEGER*)&freq);

   double ms = (double)(end - start) * 1000.0 / (double)freq;
   printf("%f\n",ms);
   while(1);
}

Drawing

What is the difference between "WinG," "CreateDIBSection," and DirectDraw?

There is a lot of confusion over the difference between WinG and CreateDIBSection(). The latter was actually created first for Win32 (Windows NT and and Windows 95). WinG was a reverse port of this 32-bit code back to 16-bit Windows 3.1 and was sometimes referred to as "CreateDIBSection for 3.1." OK, so what is a DIB section and why do you care?

Under the original Windows specification, there was really no good way to create a simple frame buffer without a lot of overhead. For example, in the old way of doing it you had to create a bitmap in the application address space and then copy this bitmap first into the device driver's address space, where it was then often color converted pixel by pixel as it was copied across the bus into the video card's memory. Needless to say, that sucked. CreateDIBSection() solved both of these problems. First of all, it creates a frame buffer in shared device-driver/application memory avoiding the extraneous copy. Second, you can create it in the bit-depth that you want (typically 8bpp indexed) and create an "identity palette" that instructs the system-memory to video-memory blt() to avoid any per-pixel color translation. Unfortunately, writing this code is still very cumbersome, but it does indeed work well enough that you can actually play a game in a window if you are only mildly masochistic.

DirectDraw was a latter addition that got rid of a lot of the complexity of creating a frame-buffer with CreateDIBSection() and furthermore gives the game programmer access to a lot of the fun hardware that exists on all modern video cards such as super high-speed video-memory to video-memory blts and even esoteric features such as hardware scale and rotate. While most of these features were supported deep inside of the device-driver layer of window's GDI (graphic device interface, the API used for drawing spreadsheets, word processors, and other "normal" Windows applications), they were not accessible either directly nor though DOS. Thus, understanding how to take advantage of this hardware can significantly improve the performance of your games over a DOS implementation. For example, using a fill blt and page flipping roughly doubled the frame rate of Wing Command III™ on a 486 at 640x480 when we ported the DOS version to Windows.

The practical details of all of this are beyond the scope of this paper. The best place to start is Animation Techniques in Win32 by Nigel Thompson or WinG Programmer's Reference by Chris Hecker (both available on the MSDN Library). The Game SDK (DirectX API) has a good programmers' reference but is short on theoretical explanation.

Here are the basics. Think of your computer and video card as two separate, high-performance computers that communicate via a nineteenth-century telegraph line. Obviously, the problem is the very slow connection, and thus you want to minimize the traffic across this line, or data bus. Now, the best form of compression would be to say something like, "Hey video card, go fill the rect from 0,0 to 639, 479 with black." Then, the video card would obey this command at super high speed since it fills the frame buffer with the computer and memory on its side of the bus. This would certainly be a hell of a lot better than touching each pixel independently across the bus. In fact, video cards can do a lot of little tricks like this as long as all the information necessary is on the remote side of the bus. For example, you can say: "Hey, copy (i.e. blt) the data in this rect of video memory to this other place in video memory." Again, nothing will go across the bus except the command. Not only that, but in many cases, the computer on the other side (the video chip) will actually do this operation asynchronously--that is, the CPU doesn't have to wait for the operation to complete! Other features, some of which are implemented on all cards, others of which aren't, are: line drawing, pattern filling, scaling, rotating, bltting with a transparent color, alpha blending, and page flipping.

So what do you do with this? The answer, of course, depends on the game. Here are a couple of ideas:

Space Combat. Suppose you're writing a space action game; let's call it "Wing Demander III." In DOS, I would allocate a 640x480 frame buffer in system memory. Once per frame, I would:

  1. Clear frame buffer with black.
  2. Render the stars and ships into the frame buffer.
  3. Copy the cockpit sprite and HUD displays on top of this.
  4. Wait for vertical blank.
  5. Copy the whole damn 640x480 frame buffer across the bus (painfully slow).

Now, the silly thing is that for the vast majority of frames, significantly over half of the completed frame is either black or cockpit. Why should we have to copy this across the bus? With DirectDraw, we allocate two surfaces which page flipped on command. Then, once per frame we:

  1. Tell the video card to fill-blt the back surface with black (practically instantaneous and also usually asynchronous).
  2. Rendered the stars and ships across the bus into the back buffer.
  3. Used the hardware blter to copy the cockpit from another location in video memory (we store it there when we first load). Again, this is lightning fast and asynchronous.
  4. Request page flip. The video card automatically flips at the next vertical blank without us having to wait for it.

As you can imagine, this is significantly faster. The only time that it isn't faster is in cases where there is significant overdraw (multiple polygons get drawn on top of each other).

[WIN2700B  1234 bytes ]

Figure 1. The previous frame (gray) has been scrolled up and to the left, revealing the two "sliver rects" in white at the bottom and right.

Top-down or side scrollers. Now, let's suppose you want to write a top-down scroller, let's call it "Sim-Convenience Store." In games like this, the camera is in a fixed position and the user scrolls around rapidly (or not so rapidly, depending on how good the programmers are). Under DOS, the render loop might look like:

  1. If the screen has scrolled, shift the back buffer accordingly with a series of memcpy()s. Dirty the "sliver rects" that are revealed after a scroll. See Figure 1.
  2. Traverse each of the dirty rects (including the sliver rects); draw everything that could possibly intersect each rect, all the while clipping the bitmaps to this dirty rect.
  3. Wait for vertical blank.
  4. Copy the whole 640x480 frame buffer across the bus.

Again, using the hardware blter to expedite the shift would allow you to avoid both the series of memcpy()s as well as the 300K bus copy.

  1. If the screen has scrolled, tell the back buffer DirectDraw Surface to blt from itself to itself with the rects offset. This replaces the series of memcpy()s from above. Dirty the sliver rects.
  2. Traverse the dirty rects, and draw everything as before, but now across the bus. But, instead of drawing directly into the back buffer, utilize all remaining video memory as a cache.
  3. Check to see if each shape is already in the cache, if not, toss out the least recently used shapes and copy the shape from system memory to the video memory cache.
  4. For each shape, use DirectDraw to blt from the video memory cache to the video-memory back buffer.
  5. Request page flip. The video card automatically flips at the next vertical blank without us having to wait for it.

In this case, we can accelerate the renderer even more than we were able to under "Wing Demander III" because we can utilize extra video memory as a cache and avoid a lot of extra bus copies as well as shape decodes. Also, the expensive buffer shift and full frame across the bus copy have been eliminated. There is no case when this will be slower than the DOS equivalent.

First-person Dungeon Crawls. For the "Dooms" of the world, there's not much that the video card can do to help, other than maybe cache the unchanging window frame or weapons overlays and allow page flipping. Since every pixel is typically changing, there's no win from using the hardware blters. You might consider caching the animations of sprite objects in video memory, and with some video cards you might even be able to use the hardware blter to scale them, but generally this isn't going to add up to much savings.

What is frame pitch?

[WIN2700C  1611 bytes ]

Figure 2. Surface width vs. surface pitch

The blters on some video cards can only copy to and from memory in certain widths, which is often not the same as the width of the surface. In other words, there is often extra unused memory when you allocate a surface. See Figure 2. To compensate, DirectDraw has a rectangular memory manager which will automatically use the wasted space if you allocate a surface which can fit. For example, we could fit a 384 pixel wide surface in the wasted (gray) space in . For the most part, you don't need to worry about the pitch. However, one time when it is very important is when you are copying data directly into a surface. In this case, you must first lock the surface and then be sure to advance your destination pointers by the pitch of the surface, not the width. See copyFromBuffer in Appendix B, "Screen Interface."

Do I care about CreateDIBSection now that there's DirectDraw?

Y es, you care. It is very difficult to debug using DirectDraw. For example, if you hit a breakpoint, you're screwed: you can't get back to the debugger to see what's going on, especially if you're page flipping The only way to keep your sanity is to implement your game so that it can run in either a window or DirectDraw and then do all of your development in a window and switch only into full screen when you are not expecting to hit a breakpoint or crash.

Setting up the abstraction between a DirectDraw surface and a DIB section is a little intimidating. Appendix B, "Screen Interface," contains the listing for an interface which abstracts a DirectDraw surface and a DIB section. This is intended for demonstration purposes only.

How do I set up my window? What do I do with the WM_PAINT message?

I usually make a very simple window that has nothing more than a title bar, menu, and a 640x480 client rect. My main loop updates the world which causes rects to get dirtied, and then the renderer fills in each of the dirty rects. In response to a WM_PAINT message, I simply mark the entire frame dirty. This may seem a bit extreme, but really WM_PAINT messages don't occur very often when you are the topmost window. For example, they occur most often when you drag one window on top of your game and then pull it away again. You could try to get fancy and dirty only the part that is exposed, but its really not worth the hassle. Note that earlier I suggested pausing the game when the application wasn't the in the foreground. Now, you can see why I put the call to setRepaintAll() into the if (foreground) statement.

Here's some prototype code for making a simple window.


#include "windows.h"
#include "resource.h"

char *appName = "Proto";

HINSTANCE hInstance;
HWND hWnd;

long CALLBACK WndProc (HWND hWnd, UINT message, UINT wParam, long lParam);

int CALLBACK WinMain(HANDLE _hInst, HANDLE hPrevInst, LPSTR nCmdParam, int nCmdShow) {
   hInstance = _hInst;

   // Setup the description of the window
   WNDCLASS  wc;
   wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
   // The window should repaint on h or v resize 
   // and the device context never changes
   wc.lpfnWndProc = (WNDPROC)WndProc;
   // Point to the WndProc
   wc.cbClsExtra = 0;
   wc.cbWndExtra = 0;
   // There is no extra data.
   wc.hInstance = hInstance;
   // Owner of the class
   wc.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE(IDI_ICON1));
   // IDI_ICON1 is an icon I made in the resource editor
   wc.hCursor = LoadCursor(NULL, IDC_ARROW);
   // IDC_ARROW is a stock cursor
   wc.hbrBackground = NULL;
   // This is the color that the window will automatically get
   // filled with before we receive a WM_PAINT message.
   // As we will repaint the whole screen anyway, there's no
   // point wasting time filling the screen with a color we'll
   // just overwrite.
   wc.lpszMenuName  = MAKEINTRESOURCE(IDR_MENU1);
   // IDR_MENU1 is a menu I created in the resource editor
   wc.lpszClassName = appName;
   // Name to register this class as

   // Register the window class
   if (!RegisterClass(&wc)) {
      return FALSE;
   }

   // Create the window (You may want to look at CreateWindowEX for
   // more sophisticated Windows attributes)
   hWnd = CreateWindow (
      appName,                      // See RegisterClass() call.
      appName,                      // Text for window title bar.
      WS_OVERLAPPEDWINDOW,          // Window style.
      CW_USEDEFAULT, CW_USEDEFAULT, // Use default positioning
      640, 480,                     // Client width and height
      NULL,                         // Overlapped Windows have no parent.
      NULL,                         // Use the class menu registered above.
      hInstance,                    // This instance owns this window.
      NULL                          // We don't use any data in our WM_CREATE
   );

   ShowWindow (hWnd, SW_SHOWDEFAULT);
   // make the window visible
   UpdateWindow(hWnd);
   // Sends WM_PAINT message and makes it go.

   while (1) {
      // Poll events without blocking
      MSG msg;
      while (PeekMessage (&msg, hWnd, 0, 0, PM_NOYIELD|PM_REMOVE)) {
         if (msg.message == WM_QUIT) {
            return msg.wParam;
         }
         TranslateMessage (&msg);
         DispatchMessage (&msg);
      }

      updateWorld();
      // Update the world causes some rects to get dirtied

      renderWorld(); 
      // Render each of the dirted rects. A WM_PAINT message may have
      // dirtied the whole screen.
   }
}

long CALLBACK WndProc (HWND hWnd, UINT message, UINT wParam, long lParam) {
   int id  = LOWORD(wParam);
   int event = HIWORD(wParam);

   switch (message) {
   case WM_COMMAND:
      switch (id) {
      case ID_QUIT: // This ID is from the resource file menu
         DestroyWindow (hWnd);
         return (0);
      }
   break;

   case WM_DESTROY:
      // The window is being shutdown
      PostQuitMessage(0);
      return 0;

   case WM_PAINT:
      setRedrawAll();
      return 0;
   }

   return DefWindowProc(hWnd, message, wParam, lParam);
}

Has the person who implemented palettes been executed yet?

I'll refrain from identifying the individual at Microsoft who implemented palettes. The legal department understandably keeps this is big secret lest a horde of irate programmers come and lynch this person.

Yes, the palette system is a giant piece of busted crap. Here's but one beautiful sample: the logical palette uses a structure PALETTEENTRY which is defined as CHAR R, G, B, flags, whereas DIBs use RGBQUAD, which is CHAR B, G, R flags--and, they're even defined in the same header file!

In Appendix B, "Screen Interface," you can examine the setPalette() function. Don't, I repeat don't, try to understand this code!

Weird Cursor Jumpiness?

The cursor functionality in DirectDraw has problems. For example, on some machines, you will get lots of flicker as you blt of page flip. With other video cards you won't see this. This problem comes up with some drivers only and will, one hopes, get worked out in the next few versions of DirectDraw. The cursor also becomes jumpy when bltting or flipping. This is due to a GDI lock deep in DirectDraw. The Microsoft folks say this will be removed by DirectX 3. In the meantime, there's a good little hack that seems to help a lot. Call GetCursorPos(), SetCursorPos() at least once per game loop, more often if you need to. For some reason this seems to alleviate the problem. I'm not sure how this affects speed consequences, nor am I sure if it fixes the problem on all drivers.


POINT point;
GetCursorPos (&point);
SetCursorPos (point.x, point.y);n

Sound

Does WaveMix Suck?

Yes. It is very slow and high latency. Use DirectSound instead.

What is DirectSound, and do I still need a sound system like AIL?

DirectSound is a very simple low-latency mixer API to replace the pathetically slow WaveMix. Although it is a little cumbersome to set up, it is conceptually simple and it works very well with practically no overhead. Here's the basic idea:

Instantiate a DirectSound object. From the object, allocate sound buffers. There are two kinds of sound buffers--primary and secondary. You need only one primary buffer, which serves as the master digital source from which DMA copies to the sound card. Allocate secondary buffers for your sound effects and then tell each of them to Play(). This will cause the secondary buffer to mix into the primary buffer. For digital music that you might be spooling off of a CD, set the looping flag on the secondary buffer and then write into the buffer just behind the read head. Under DOS, you would typically hook the timer interrupt and use this to feed to buffer to avoid skipping. Under Windows, you should create another thread. Although the Windows interface is a little tricky, it is much easier to program: you can debug it and it is generally more robust when something goes wrong. For a good introduction to multithreading, see chapter one in Advanced Windows NT by Jeffrey Richter (available on the MSDN Library).

DirectSound is for digital sound only. If you want to use MIDI, use a higher level sound system such as John Miles's AIL. John was actually involved in the implementation of DirectSound, so, as you might expect, AIL plugs nicely into DirectSound and gives you a single API for many different platforms such as the Mac and Playstation. See Appendix C, "Sound Interface," for more information.

Miscellanous

WINDOWS.H seems to take forever to compile. Is something w rong here?

No, you're not imagining this. WINDOWS.H with all of its include files is something like 15,000 lines long. Is it any wonder that it takes a long time to compile? Here's a big hint: Avoid, at all cost, including WINDOWS.H. I have a single MAIN.CPP that includes it to set up my Windows and screen interfaces, into which I shove tons of code. That way I avoid compiling WINDOWS.H more than once. Make your own free functions that encapsulate Windows functionality and use your own typedefs even if you have to duplicate Windows structures with your own types to accomplish this. Of course, this makes porting easier too, so its a doubly good idea.

[WIN2700D  13600 bytes ]

Figure 3. Optimal settings for precomplied headers

There's one other trick. MSVC++ 2.0 sets the precomplied headers default incorrectly (or at least suboptimally). Set the project settings as demonstrated in . Then be sure that any C file that is going to include WINDOWS.H includes it first. This causes the precomplied header system to only precompile WINDOWS.H but it ensures that it will always do it without duplication. Although you lose on not precompiling your own code (you could put it before as long as you did the same in all modules), it is still a huge win to ensure that WINDOWS.H will never be compiled again. (This problem may be solved in versions past DirectX 2; I haven't tried it.)

What are the crazy error messages returned by DirectX?

When DirectX returns an error, you'll typically be looking at it in the debugger, which will happily give you a huge negative number printed in decimal. Convert this to hex. Ignore the top word. Convert the bottom word back to decimal. (In other words, just inspect the value with &0xFFFF). Look for that number in the appropriate D*.H file such as DDRAW.H. Have fun.

Why does Zack have such a bad attitude?

Five years of writing DOS games has made me generally mad at the world. Thank you for caring.

Appendix A. Event Interface


// This code is extracted from the "WndProc" that would typically be in the same
// module as WinMain.  It uses the "eventinterface" defined below

long CALLBACK WndProc (HWND hWnd, UINT message, UINT wParam, long lParam) {
   switch(message) {
   case WM_LBUTTONDOWN:
   case WM_RBUTTONDOWN:
   case WM_LBUTTONUP:
   case WM_RBUTTONUP: {
      int upDown = (message==WM_LBUTTONUP||message==WM_RBUTTONUP) ? 
         IS_UP_MASK : 0;
      int shift  = (GetKeyState(VK_SHIFT)   & 0x80000000) ? IS_SHIFT_MASK : 0;
      int ctrl   = (GetKeyState(VK_CONTROL) & 0x80000000) ? IS_CTRL_MASK : 0;
      int key    = (message==WM_LBUTTONUP||message==WM_LBUTTONDOWN) ?
         KC_MOUSE_LEFT : KC_MOUSE_RIGHT;
      pushEvent(key|upDown|shift|ctrl, LOWORD(lParam), HIWORD(lParam));
      break;
   }

   case WM_KEYUP:
      case WM_KEYDOWN: {
         int upDown = message==WM_KEYUP ? IS_UP_MASK : 0;
         int shift = (GetKeyState(VK_SHIFT)   & 0x80000000) ? IS_SHIFT_MASK : 0;
         int ctrl  = (GetKeyState(VK_CONTROL) & 0x80000000) ? IS_CTRL_MASK : 0;

         POINT mousePos;
         GetCursorPos(&mousePos);
         RECT rect;
         GetWindowRect (hWnd, &rect);
         mousePos.x -= clientOffsetX + rect.left;
         mousePos.y -= clientOffsetY + rect.top;

         unsigned char stateInfo[256];
         GetKeyboardState (stateInfo);
         int key = 0;
         int a = ToAscii (wParam, (lParam>>16)&0x00FF, stateInfo, (WORD *)&key, 0);
         if (a==1) {
            pushEvent(key|upDown|shift|ctrl, mousePos.x, mousePos.y);
         }
         else {
            pushEvent(((int)wParam<<16)|upDown|shift|ctrl, mousePos.x, mousePos.y);
         }
         break;
      }
   }
   return DefWindowProc(hWnd, message, wParam, lParam);
}

//------------------------------------------------------------------------------------
#ifndef EVENTINTERFACE_H
#define EVENTINTERFACE_H

// Note the complete absence of headers here!

///////////////////////////////////////////////////////////////////////
// Key defs.  Note that ascii values are just fine too and
// will not conflict with any of these values as these are
// all Windows KC_ codes shifted up 16.  There are some ascii
// values here that also have KC_ codes (like esc) the KC_ and the
// ascii value can be used interchangeably
///////////////////////////////////////////////////////////////////////

// ascii defs, you can use '\b', '\t', etc also.
#define KC_BACKSPACE      (0x08)
#define KC_TAB            (0x09)
#define KC_ENTER          (0x0D)
#define KC_ESCAPE         (0x1B)
#define KC_SPACE          (0x20)

// These don't have ascii equivalents.  Each of these
// values is derived from winuser.h and shifted up.
#define KC_MOUSE_LEFT     (0x01<<16)
#define KC_MOUSE_RIGHT    (0x02<<16)
#define KC_SHIFT          (0x10<<16)
#define KC_CONTROL        (0x11<<16)
#define KC_CAPSLOCK       (0x14<<16)
#define KC_PGUP           (0x21<<16)
#define KC_PGDN           (0x22<<16)
#define KC_END            (0x23<<16)
#define KC_HOME           (0x24<<16)
#define KC_LEFT           (0x25<<16)
#define KC_RIGHT          (0x27<<16)
#define KC_UP             (0x26<<16)
#define KC_DOWN           (0x28<<16)
#define KC_INSERT         (0x2D<<16)
#define KC_DELETE         (0x2E<<16)
#define KC_F1             (0x70<<16)
#define KC_F2             (0x71<<16)
#define KC_F3             (0x72<<16)
#define KC_F4             (0x73<<16)
#define KC_F5             (0x74<<16)
#define KC_F6             (0x75<<16)
#define KC_F7             (0x76<<16)
#define KC_F8             (0x77<<16)
#define KC_F9             (0x78<<16)
#define KC_F10            (0x79<<16)
#define KC_F11            (0x7A<<16)
#define KC_F12            (0x7B<<16)

// This is a massive simplification of the Windows event interface.
// Each keyboard and mouse event is shoved into an array which 
// uniquely but homogenously identifies both mouse and keyboard.
// Furthermore, the mouse position at the time of the event
// is recorded with all events as well as the shift and control status
// The "key" in the keycode is an int that identifies:
// 1. if (bit 31 == 1) then it is an up event
// 2. if (bit 30 == 1) then the key is toggled (useful for numlock, caps, etc.)
// 3. if (bit 29 == 1) then shift was down at the time
// 4. if (bit 28 == 1) then control was down at the time
// 5. if (bits 16-27 are 0) then key is ascii in lower word
// 6. if lower word is 0 then then bits 16-27 contain a key code (defined below)
// When you getEvents you can choose to ignore all of the up events
// by passing a flag to the getEvent function.

struct ZEvent {
   #define IS_UP_MASK      (0x80000000)
   #define IS_TOGGLED_MASK (0x40000000)
   #define IS_SHIFT_MASK   (0x20000000)
   #define IS_CTRL_MASK    (0x10000000)

   int key;
   int mouseX;
   int mouseY;

   ZEvent() { key = mouseX = mouseY = 0; }
   ZEvent (int _k, int _x, int _y) { key = _k; mouseX = _x; mouseY = _y; }
   int isValid()   { return key != 0; }
   int isUp()      { return key&IS_UP_MASK; }
   int isToggled() { return key&IS_TOGGLED_MASK; }
   int isDown()    { return !(key&IS_UP_MASK); }
   int isShift()   { return key&IS_SHIFT_MASK; }
   int isCtrl()    { return key&IS_CTRL_MASK; }
   int getKey()    { return key & ~(IS_UP_MASK|IS_SHIFT_MASK|IS_CTRL_MASK); }
   int isMouse()   { return key==KC_MOUSE_LEFT || key==KC_MOUSE_RIGHT; }
};

ZEvent getState(int key=0xFFFF);
// This is used to get the async key state.  You can also use it
// to get the mouse x and y since these are polled at the
// same time.

ZEvent getEvent(int filterOutUpEvents = 1);
// This gives you the next queued event.  It stores the mouse
// x and y for every even (not just mouse events)
// You can choose to filter the up events by passing true

void pushEvent(int key, int mouseX, int mouseY);
// This is called by the Windows modules to stuff the queue,
// It is expected that the key argument already has the appropriate
// bits set for up/down, shift, and ctrl.  Also, the VK should already
// be shiffted by 16 bits

extern int keyToDirection(int key);
#endif

//-------------------------------------------------------------------------
// eventinterface.cpp

#define KEY_STACK_SIZE 40
ZEvent keyStack[KEY_STACK_SIZE];
int readHead = 0;
int writeHead = 0;

extern HWND hWnd;

ZEvent getState(int key) {
   key &= ~(IS_UP_MASK|IS_SHIFT_MASK|IS_CTRL_MASK);
   int a = key>>16;
   ZEvent e;
   int q  = GetKeyState(a);
   e.key  = (q & 0x80000000) ? 0 : IS_UP_MASK;
   e.key |= (q & 0x00000001) ? IS_TOGGLED_MASK : 0;

   POINT mousePos;
   GetCursorPos(&mousePos);
   RECT rect;
   GetWindowRect (hWnd, &rect);  // *** this could be moved global

   if (mousePos.x < rect.left || mousePos.x > rect.right ||
      mousePos.y < rect.top || mousePos.y > rect.bottom) {
      e.mouseX = -1;
      e.mouseY = -1;
   }
   else {
      e.mouseX = mousePos.x - (clientOffsetX + rect.left);
      e.mouseY = mousePos.y - (clientOffsetY + rect.top);
   }

   return e;
}

ZEvent getEvent(int filterOutUpEvents) {
   if (readHead != writeHead) {
      ZEvent e = keyStack[readHead];
      readHead = (readHead+1) % KEY_STACK_SIZE;
      if (!(filterOutUpEvents && e.isUp())) {
         return e;
      }
   }
   return ZEvent(0,0,0);
}

void pushEvent(int key, int mouseX, int mouseY) {
   keyStack[writeHead].key = key;
   keyStack[writeHead].mouseX = mouseX;
   keyStack[writeHead].mouseY = mouseY;
   writeHead = (writeHead+1) % KEY_STACK_SIZE;
   if (writeHead == readHead) {
      writeHead = (writeHead+KEY_STACK_SIZE-1) % KEY_STACK_SIZE;
   }
}

Appendix B. Screen Interface

This code is extracted from a larger system and will not compile due to external references to BaseFile, VFX, etc. This is intended for demonstration purposes only.

The main point of this code is to create a free-function interface that provides one interface for five different modes. The first three--Windowed, 2 page flipping, and 2 page bltting--are probably the most common. The last two modes--4 page bltting and 4 page flipping--are special modes that really only make sense in the context of the system I removed this from, but they do demonstrate the use of more than two video pages.

The DSurface class is a wrapper around DirectDraw's surface and palettes classes. This is certainly not the most efficient way to implement any of this, on the other hand, it keeps it pretty clear.

The "Simple" modes mean that the secondary surface is blted to the primary surface as opposed to flipping. This mode will tend to cause tearing, but is easier to understand and sometimes works when flipping doesn't.

The four page modes implemented here are designed to allow me to have static bitmap that doesn't move while I'm scrolling the foreground around. This is a little more complicated. The logic goes like this:

  1. Fill the dirty rects on the "map surface" (the foreground in my case) to 255 which will be the transparent color.
  2. Shift (i.e. scroll) the map surface if necessary.
  3. Lock and draw dirty rects to the map surface.
  4. Set the blter to transparent mode (doesn't work on all video cards, unfortunately).
  5. Blt the background bitmap to the hidden surface.
  6. Transparent blt the map surface on to the hidden surface.
  7. Blt or flip depending on the mode.

Anyway, this is a lot of code. I hope it helps.


/////////////////////////////////////////////////////////////////////////////////
// ScreenInterface.h
/////////////////////////////////////////////////////////////////////////////////

#ifndef SCREENINTERFACE_H
#define SCREENINTERFACE_H

#include "rect.h"


// These are duplicates of the VFX WINDOW and PANE structures.
// We can't include the actual definitions in vfx.h due to header conflicts
struct VFXWindow {
 char *buffer;
 int x_max;
 int y_max;
 void *stencil;
 void *shadow;
};

struct VFXPane {
 VFXWindow *window;
 int x0;
 int y0;
 int x1;
 int y1;
};

struct RGBF {
 // This is a duplicate of the windows RGBQUAD structure
 // duplicated here so that we don't have to include windows.h
 char b, g, r, f;
};


// Screen Interface
//------------------------------------------------------------------------

// Hacks for old
//#define FULLSCREEN_MODE 1
//#define WINDOW_MODE 2

extern int screenMode;
 #define SM_NO_MODE (0)
  // No mode. Startup condition

 #define SM_DIBSECTION_WINDOW (1)
  // A DIB section is used as the primary back buffer.

 #define SM_TWO_PAGE_FLIPPING (2)
  // There are only two buffers, and they are page flipping.
  // The back buffer is copied from the primary surface

 #define SM_FOUR_PAGE_BACKGROUND_COMPOSITE_FLIPPING (3)
  // There are two extra surfaces. One contains the backdrop
  // and the other holds the foreground map. The two images are
  // blt on to the backsurface before the page flip

 #define SM_TWO_PAGE_SIMPLE (4)
  // There are only two buffers, and they are not page flipping.
  // The slamRect slams from the back surface to the front surface

 #define SM_FOUR_PAGE_BACKGROUND_COMPOSITE_SIMPLE (5)
  // There are two extra surfaces. One contains the backdrop 
  // and the other holds the foreground map. The two images are
  // blt on to the backsurface before the bltting the back surface
  // to the primary

inline int isADDrawMode(int mode) {
 return mode>SM_DIBSECTION_WINDOW;
}

extern int         backBufferPitch;
extern int         backBufferWidth;
extern int         backBufferHeight;
extern char       *backBuffer;
extern VFXWindow   vfxWindow;
extern VFXPane     vfxPane;

extern int windowWidth;
extern int windowHeight;
 // Size of the client area of the window

extern int clientOffsetX;
extern int clientOffsetY;
 // These are the offset relative to the screen of the client area 

extern int maxWinXDim;
extern int maxWinYDim;
 // This is the size of the window given the client area of 640 x 480

void initScreen();
void setScreenMode (int mode);

void lockSurface();
void unlockSurface();
void setClipRect (Rect clip);
Rect getClipRect();

void flip();
void slam();
void slamRect(Rect rect);
void shiftBackBuffer (int pixelXOff, int pixelYOff);

void pset (int x, int y, int color);
void drawRect (Rect rect, int color);
void fillRect (Rect rect, int color);
void line (int x0, int y0, int x1, int y1, int color);
void circle (int xc, int yc, int radius, int color);

void setPalette (int start, int count, RGBF *colors, int realize);
void loadPalette (char *filename, int blankFirst);
int findNearestColor (int r, int g, int b);

// Implemented in VFX.CPP
void VFXDraw (void *shapePtr, int frame, int x, int y);
void VFXTransformDraw (void *shapePtr, int frame, int x, int y, int rot, int xScale, int yScale);
 // rot is from 0 to 3600. scale is in 16.16. example 1:1 = 0x10000. 0.5:1 = 0x8000

enum {
 FT_BASIC=0,
 FT_BASIC_ITALIC,
 FT_DEBUG,
 FT_CRAZY,
 FT_NUM_FONTS
};

void initFonts();
void drawText (char *s, int x, int y, int font, int color);
void getTextDim (char *s, int &w, int &h, int font);

#endif


/////////////////////////////////////////////////////////////////////////////////
// ScreenInterface.cpp
/////////////////////////////////////////////////////////////////////////////////

#include "windows.h"
#include "ddraw.h"
#include "common.h"
#include "basefile.h"

// DSurface class
//------------------------------------------------------------------------------

class EX_DDRAW_FAIL { };

struct DSurface {
 // This class is meant to encapsolate the ferw basic kinds of
 // surfaces that I use often.  It is not meant to cover to whole range
 // of functionality of ddraw nor act as some over bloated MFC for ddraw.
 #define DEFAULT_BLT_FLAGS (DDBLT_WAIT|DDBLT_ASYNC)

 static LPDIRECTDRAW ddraw;
 static LPDIRECTDRAWPALETTE pal;
 static void goFullscreen();
 static void leaveFullscreen();

 HDC dc;
 LPDIRECTDRAWSURFACE surface;
 DDSURFACEDESC desc;
 char *ptr;

 DSurface (LPDIRECTDRAWSURFACE _surface);
 DSurface(int type, int _w=640, int _h=480);
  #define DT_FLIPPING_PRIMARY (1)
  #define DT_SIMPLE_PRIMARY (2)
  #define DT_SIMPLE_BACK (3)
 ~DSurface();

 int w() { return desc.dwWidth; }
 int h() { return desc.dwHeight; }
 int p() { return desc.lPitch; }
  // Remember, these values aren't valid unless the surface is locked!

 int isLocked() { return ptr!=NULL; }
 char *lock();
 void unlock();

 LPDIRECTDRAWSURFACE getBackSurface();
  // Primary comlex flipping surfaces are strange in that
  // they actually contain the pointer to the back surface.
  // This function gets the pointer to the back surface
  // which you can use to new a new DSurface.

 void setTransparent(int color=255);
 void bltFrom(
int x, int y, DSurface *src, Rect srcRect, int ddbltflags=DEFAULT_BLT_FLAGS
);
 void bltFromBuffer(int x, int y, char *srcBuffer, int srcPitch, Rect srcRect);
 void fillBlt(Rect rect, int color, int flags=0);
  // Use DDBLT_WAIT to block until blt done
 void flip(int flags=0);
  // Use DDFILIP_WAIT to block until flip done
 HDC getDC() {
  HRESULT result;
  do {
   result = surface->GetDC(&dc);
  } while (result == DDERR_WASSTILLDRAWING);
  if (result != DD_OK) {
   trace (TL_TRACE, "error on getdc %d\n",result);
  }
  return dc;
 }
 void releaseDC() {
  HRESULT result;
  result = surface->ReleaseDC(dc);
  if (result != DD_OK) {
   trace (TL_TRACE, "error on release dc %d\n",result);
  }
 }
};

// Screen Interface variables
//------------------------------------------------------------------------------

// Externed from main
extern HDC  mainDC;
extern HWND hWnd;

// This the mode that the screen interface is running under
int screenMode = 0;

// This is the state of the window before entering ddraw
RECT oldWindowPos = { 0, 0, 0, 0 }; 
HMENU oldWindowMenu = NULL;

// Interface Variables. (public)
int         backBufferPitch  = 0;
int         backBufferWidth  = 0;
int         backBufferHeight = 0;
char       *backBuffer       = NULL;
VFXWindow   vfxWindow;
VFXPane     vfxPane;

// Size of the client area of the window    
int windowWidth = 0;   
int windowHeight = 0;

// These are the offset relative to the screen of the client area 
int clientOffsetX = 0;
int clientOffsetY = 0;

int maxWinXDim = -1;
int maxWinYDim = -1;

// Interface Variables. (private)
static int         surfaceLocked = 0;
static HDC         backDC = NULL;
static HBITMAP     backBufferHandle = NULL;
static BITMAPINFO *backBufferHeader = NULL;
static char       *dibBackBuffer    = NULL;
static DSurface   *priSurf = NULL;
static DSurface   *secSurf = NULL;
static DSurface   *mapSurf = NULL;
static DSurface   *cldSurf = NULL;

// Palette related and color variables
int colorBlack;
int colorRed;
int colorGreen;
int colorBlue;
int colorMagenta;
int colorYellow;
int colorOrange;
int colorWhite;
int colorGray;

// Private palette variables
static PALETTEENTRY *logPal = NULL;
static LOGPALETTE   *logPalHeader = NULL;
static HPALETTE      hPalette = NULL;
static HPALETTE      hOldPalette = NULL;
static RGBF          scratchPalette[256];


// Static Members of DSurface
//------------------------------------------------------------------------------

LPDIRECTDRAW DSurface::ddraw = NULL;
LPDIRECTDRAWPALETTE DSurface::pal = NULL;

void DSurface::goFullscreen() {
 if (ddraw) {
  return;
 }

 ddraw = NULL;

 HRESULT result = DirectDrawCreate(NULL, &ddraw, NULL);
 if (result != DD_OK) {
  throw EX_DDRAW_FAIL();
 }

 // Set exclusive mode
 result = ddraw->SetCooperativeLevel (
  hWnd, 
  DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN | DDSCL_ALLOWREBOOT
 );
 if (result != DD_OK) {
  throw EX_DDRAW_FAIL();
 }

 // Set up for 640x480x8bpp mode
 result = ddraw->SetDisplayMode(640, 480, 8);
 if (result != DD_OK) {
  throw EX_DDRAW_FAIL();
 }
}

void DSurface::leaveFullscreen() {
 if (!ddraw) {
  return;
 }
 // *** Do I have to release the surface and palettes?
 if (pal) {
  pal->Release();
  pal = NULL;
 }

 ddraw->Release();
 ddraw = NULL;
}

// Regular Methods of DSurface
//------------------------------------------------------------------------------

DSurface::DSurface (LPDIRECTDRAWSURFACE _surface) {
 // This constructor is used to create a "Dsurface" from
 // the hard-to-reach back surface of a primary DIRECTDRAWSURFACE
 surface = _surface;
 surface->GetSurfaceDesc(&desc);
 desc.dwSize = sizeof(desc);
 ptr = NULL;
}

DSurface::DSurface(int type, int _w, int _h) {
 surface = NULL;
 ptr = NULL;

 HRESULT result;
 memset (&desc, 0, sizeof(desc));
 desc.dwSize = sizeof(desc);

 if (type == DT_FLIPPING_PRIMARY) {
  desc.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
  desc.ddsCaps.dwCaps = DDSCAPS_COMPLEX | DDSCAPS_FLIP | DDSCAPS_PRIMARYSURFACE;
  desc.dwBackBufferCount = 1;

  result = ddraw->CreatePalette (
   DDPCAPS_8BIT | DDPCAPS_INITIALIZE | DDPCAPS_ALLOW256,
   logPal, &pal, NULL
  );
  if (result != DD_OK) {
   throw EX_DDRAW_FAIL();
  }
 }
 else if (type == DT_SIMPLE_PRIMARY) {
  desc.dwFlags = DDSD_CAPS;
  desc.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;

  result = ddraw->CreatePalette (
   DDPCAPS_8BIT | DDPCAPS_INITIALIZE | DDPCAPS_ALLOW256,
   logPal, &pal, NULL
  );
  if (result != DD_OK) {
   throw EX_DDRAW_FAIL();
  }
 }
 else if (type == DT_SIMPLE_BACK) {
  desc.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
  desc.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_VIDEOMEMORY;
  desc.dwWidth  = _w;
  desc.dwHeight = _h;
 }

 result = ddraw->CreateSurface(&desc, &surface, NULL);
 if (result != DD_OK) {
  throw EX_DDRAW_FAIL();
 }

 // Associate default palette object with primary buffer
 if (
  type == DT_FLIPPING_PRIMARY ||
  type == DT_SIMPLE_PRIMARY
 ) {
  result = surface->SetPalette(pal);
  if (result != DD_OK) {
   throw EX_DDRAW_FAIL();
  }
 }
}

DSurface::~DSurface() {
 if (!(desc.ddsCaps.dwCaps & DDSCAPS_BACKBUFFER)) {
  surface->Release();
 }
}

char *DSurface::lock() {
 HRESULT result;
 if (surface->IsLost() == DDERR_SURFACELOST) {
  surface->Restore();
 }
 result = surface->Lock(NULL, &desc, DDLOCK_WAIT, NULL);
 if (result != DD_OK) {
  trace (TL_TRACE, "lock failed.\n");
  return NULL;
 }
 ptr = (char *)desc.lpSurface;
 return ptr;
}

void DSurface::unlock() {
 HRESULT result;
 if (surface->IsLost() == DDERR_SURFACELOST) {
  surface->Restore();
 }
 result = surface->Unlock(NULL);
 if (result != DD_OK) {
  trace (TL_TRACE, "unlock failed.\n");
 }
}

void DSurface::setTransparent(int color) {
 if (surface->IsLost() == DDERR_SURFACELOST) {
  surface->Restore();
 }
 DDCOLORKEY colorKey;
 colorKey.dwColorSpaceLowValue = color;
 colorKey.dwColorSpaceHighValue = color;
 HRESULT result = surface->SetColorKey (DDCKEY_SRCBLT, &colorKey);
 if (result != DD_OK) {
  trace (TL_TRACE, "error on setcolorkey %d\n",result);
 }
}

void DSurface::bltFrom(int x, int y, DSurface *src, Rect srcRect, int ddbltflags) {
 if (surface->IsLost() == DDERR_SURFACELOST) {
  surface->Restore();
 }
 Rect dstRect (x, y, x+srcRect.w()-1, y+srcRect.h()-1);
  // Note the lack of -1 here is due to the fact that DirectDraw seems
  // to use rects in a l, t, w, h fashion.
 HRESULT result = surface->Blt(
  (RECT *)&dstRect, 
src->surface, 
(RECT *)&srcRect, 
DDBLT_WAIT|ddbltflags, 
NULL
);
 if (result != DD_OK) {
  trace (TL_TRACE, "blt from %d (%d %d %d %d) %p \n",
   result,
   srcRect.l,srcRect.t,srcRect.r,srcRect.b,
   src->surface
  );
 }
}

void DSurface::bltFromBuffer(
 int x, int y, char *srcBuffer, int srcPitch, Rect srcRect
) {
 if (surface->IsLost() == DDERR_SURFACELOST) {
  surface->Restore();
 }
 Rect dstRect = srcRect;

 char *dst = lock();

 dstRect.moveTo (x, y);
 dstRect.clipTo (Rect (0, 0, w()-1, h()-1));
 int dstWidth = dstRect.w();
 int dstPitch = p();
 dst += dstPitch * dstRect.t + dstRect.l;
 char *src = srcBuffer + srcPitch * (
  srcRect.h() - dstRect.h()) + (srcRect.w() - dstRect.w()
 );

 int numRows = dstRect.h();
 for (int i=0; iIsLost() == DDERR_SURFACELOST) {
  surface->Restore();
 }
 rect.r++; 
 rect.b++; 
  // Inc here due to the fact that DirectDraw seems
  // to use rects in a l, t, w, h fashion.

 DDBLTFX bltfx;
 bltfx.dwSize = sizeof(bltfx);
 bltfx.dwFillColor = color;

 HRESULT result;
 result = surface->Blt(
  (RECT *)&rect, NULL, NULL, 
  DDBLT_WAIT|DDBLT_COLORFILL|DDBLT_ASYNC|flags, &bltfx
 );
 if (result != DD_OK) {
  trace (TL_TRACE, "error on fillblt %d\n",result);
 }
}

LPDIRECTDRAWSURFACE DSurface::getBackSurface() {
 LPDIRECTDRAWSURFACE attachedSurface;
 DDSCAPS caps;
 caps.dwCaps = DDSCAPS_BACKBUFFER;
 HRESULT result = surface->GetAttachedSurface (&caps, &attachedSurface);
 if (result != DD_OK) {
  trace (TL_TRACE, "error on getbacksurf %d\n",result);
 }
 return attachedSurface;
}

void DSurface::flip(int flags) {
 if (surface->IsLost() == DDERR_SURFACELOST) {
  surface->Restore();
 }
 HRESULT result = surface->Flip (NULL, DDFLIP_WAIT|flags);
 if (result != DD_OK) {
  trace (TL_TRACE, "error on flip %d\n",result);
 }
}

// Setup Related Screen Interface Code
//-----------------------------------------------------------------------------

void initScreen() {
 backBufferPitch  = 0;
 backBufferWidth  = 640;
 backBufferHeight = 480;
 
 backBufferHeader = (BITMAPINFO *)malloc (
  sizeof(BITMAPINFOHEADER)+sizeof(RGBQUAD)*256
 );
 backBufferHeader->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
 backBufferHeader->bmiHeader.biWidth = backBufferWidth;
 backBufferHeader->bmiHeader.biHeight = -backBufferHeight;
  // Height is negative to create bitmap right-side up!
  // Oh, the joys of Windows programming.
 backBufferHeader->bmiHeader.biPlanes = 1;
 backBufferHeader->bmiHeader.biBitCount = 8;
 backBufferHeader->bmiHeader.biCompression = 0;
 backBufferHeader->bmiHeader.biSizeImage = 0;
 backBufferHeader->bmiHeader.biXPelsPerMeter = 0;
 backBufferHeader->bmiHeader.biYPelsPerMeter = 0;
 backBufferHeader->bmiHeader.biClrUsed = 256;
 backBufferHeader->bmiHeader.biClrImportant = 256;

 backBufferHandle = CreateDIBSection (
  mainDC,
  backBufferHeader,
  DIB_RGB_COLORS,
  (void **)&dibBackBuffer,
  NULL, 0
 );
 memset(dibBackBuffer, 0, backBufferWidth * backBufferHeight);

 backBuffer = dibBackBuffer;

 vfxWindow.buffer  = backBuffer;
 vfxWindow.x_max   = backBufferPitch-1;
 vfxWindow.y_max   = backBufferHeight-1;
 vfxWindow.stencil = NULL;
 vfxWindow.shadow  = NULL;

 vfxPane.window = &vfxWindow;
 vfxPane.x0 = vfxPane.y0 = vfxPane.x1 = vfxPane.y1 = 0;

 // Create the logical palette 
 logPalHeader = (LOGPALETTE *)malloc(sizeof(PALETTEENTRY)*256 + sizeof(WORD)*2);
 logPalHeader->palVersion = 0x300;
 logPalHeader->palNumEntries = 256;
 logPal = &logPalHeader->palPalEntry[0];

 oldWindowMenu = GetMenu (hWnd);
 screenMode = SM_NO_MODE;

 backDC = CreateCompatibleDC (mainDC);
 SelectObject (backDC, backBufferHandle);
}

void setScreenMode (int mode) {
 if (mode == screenMode) {
  return;
 }

 // Clean up from the current mode
 if (isADDrawMode(screenMode)) {
  if (priSurf) delete priSurf; priSurf = NULL;
  if (secSurf) delete secSurf; secSurf = NULL;
  if (mapSurf) delete mapSurf; mapSurf = NULL;
  if (cldSurf) delete cldSurf; cldSurf = NULL;
  DSurface::leaveFullscreen();
 }

 // Start the new mode
 if (mode == SM_DIBSECTION_WINDOW) {
  ShowWindow (hWnd, SW_HIDE);
  SetWindowLong (hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
  SetMenu (hWnd, oldWindowMenu);

  SetWindowPos (hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE);
  ShowWindow (hWnd, SW_MAXIMIZE);
  MoveWindow (
   hWnd, 
   oldWindowPos.left, oldWindowPos.top, 
   maxWinXDim, maxWinYDim, TRUE
  );
  Rect rect (0, 0, 640, 480);
  FillRect (mainDC, (RECT *)&rect, (HBRUSH)(COLOR_INACTIVEBORDER+1));
  UpdateWindow (hWnd);

  clientOffsetX = GetSystemMetrics(SM_CXFRAME);
  clientOffsetY = GetSystemMetrics(SM_CYCAPTION) + 
   GetSystemMetrics(SM_CYFRAME) + 
   ((oldWindowMenu==NULL)?0:GetSystemMetrics(SM_CYMENU))
  ;

  backBuffer = dibBackBuffer;
  backBufferPitch = backBufferWidth;
 }

 if (isADDrawMode(mode)) {
  // Set up the window to fullscreen
  GetWindowRect (hWnd, &oldWindowPos);
  ShowWindow (hWnd, SW_HIDE);
  oldWindowMenu = GetMenu (hWnd);
  SetMenu (hWnd, NULL);
  SetWindowLong (hWnd, GWL_STYLE, WS_POPUP);

  MoveWindow (hWnd, 0, 0, 640, 480, TRUE);
  ShowWindow (hWnd, SW_SHOW);

  clientOffsetX = 0;
  clientOffsetY = 0;

  DSurface::goFullscreen();

  backBuffer = NULL;
  backBufferPitch = 0;
 }

 switch (mode) {
  case SM_TWO_PAGE_FLIPPING:
   priSurf = new DSurface (DT_FLIPPING_PRIMARY);
   secSurf = new DSurface (priSurf->getBackSurface());
   break;

  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_SIMPLE:
  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_FLIPPING: {
   if (mode == SM_FOUR_PAGE_BACKGROUND_COMPOSITE_SIMPLE) {
    priSurf = new DSurface (DT_SIMPLE_PRIMARY);
    secSurf = new DSurface (DT_SIMPLE_BACK);
   }
   else {
    priSurf = new DSurface (DT_FLIPPING_PRIMARY);
    secSurf = new DSurface (priSurf->getBackSurface());
   }
   mapSurf = new DSurface (DT_SIMPLE_BACK);
   cldSurf = new DSurface (DT_SIMPLE_BACK);

   // Set the map surface as transparent to color 255
   mapSurf->setTransparent();

   // Load a non-scrollling backgound bitmap
   int w, h;
   extern char *loadGif (char *filename, int &w, int &h);
   char *background = loadGif ("background.gif", w, h);
   assert (background && w==640 && h==480);

   cldSurf->bltFromBuffer (0, 0, background, w, Rect (0, 0, w-1, h-1));
   delete background;
   break;
  }

  case SM_TWO_PAGE_SIMPLE:
   priSurf = new DSurface (DT_SIMPLE_PRIMARY);
   secSurf = new DSurface (DT_SIMPLE_BACK);
   break;
 }

 screenMode = mode;
 setPalette (0, 0, NULL, true);
}

void lockSurface() {
 if (surfaceLocked++ != 0) {
  return;
 }

 switch (screenMode) {
  case SM_TWO_PAGE_SIMPLE:
  case SM_TWO_PAGE_FLIPPING:
   backBuffer = secSurf->lock();
   backBufferPitch = secSurf->p();
   break;

  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_SIMPLE:
  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_FLIPPING:
   backBuffer = mapSurf->lock();
   backBufferPitch = mapSurf->p();
   break;

  case SM_DIBSECTION_WINDOW:
   backBuffer = dibBackBuffer;
   backBufferPitch = backBufferWidth;
   break;
 }

 vfxWindow.buffer = backBuffer;
 vfxWindow.x_max = backBufferPitch - 1;
}

void unlockSurface() {
 if (--surfaceLocked != 0) {
  return;
 }

 switch (screenMode) {
  case SM_TWO_PAGE_FLIPPING:
  case SM_TWO_PAGE_SIMPLE:
   secSurf->unlock();
   backBuffer = NULL;
   backBufferPitch = 0;
   break;

  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_SIMPLE:
  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_FLIPPING:
   mapSurf->unlock();
   backBuffer = NULL;
   backBufferPitch = 0;
   break;
 }

 vfxWindow.buffer = backBuffer;
}

void setClipRect (Rect clip) {
 vfxPane.x0 = clip.l;
 vfxPane.y0 = clip.t;
 vfxPane.x1 = clip.r;
 vfxPane.y1 = clip.b;
}

Rect getClipRect () {
 return Rect (
  vfxPane.x0, vfxPane.y0,
  vfxPane.x1, vfxPane.y1
 );
}


// Surface Related Code
//-----------------------------------------------------------------------------

void flip() {
 switch (screenMode) {
  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_FLIPPING:
   secSurf->bltFrom (0, 0, cldSurf, Rect (0, 0, 639, 479));
   secSurf->bltFrom (
    0, 0, mapSurf, Rect (0, 0, 639, 479), DEFAULT_BLT_FLAGS|DDBLT_KEYSRC
   );
   priSurf->flip(DDFLIP_WAIT);
   break;

  case SM_TWO_PAGE_FLIPPING:
   priSurf->flip(DDFLIP_WAIT);
   break;
 }
}

void slam() {
 slamRect (Rect (0, 0, backBufferWidth-1, backBufferHeight-1));
}

void slamRect(Rect rect) {
 switch (screenMode) {
  case SM_TWO_PAGE_SIMPLE:
   priSurf->bltFrom (rect.l, rect.t, secSurf, rect);
   // *** Should I "wait" flag here (default)?
   break;

  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_SIMPLE:
   // First copy over the proper section of the background
   secSurf->bltFrom (rect.l, rect.t, cldSurf, rect);
   secSurf->bltFrom (
    rect.l, rect.t, mapSurf, rect, DEFAULT_BLT_FLAGS|DDBLT_KEYSRC
   );
   priSurf->bltFrom (rect.l, rect.t, secSurf, rect);
   break;

  case SM_DIBSECTION_WINDOW:
   StretchDIBits (
    mainDC,
    rect.l, rect.t, rect.r-rect.l+1, rect.b-rect.t+1,
    rect.l, backBufferHeight-rect.b-1, rect.r-rect.l+1, rect.b-rect.t+1,
    backBuffer,
    backBufferHeader,
    DIB_RGB_COLORS,
    SRCCOPY
   );
   break;
 }
}

void shiftBackBuffer (int pixelXOff, int pixelYOff) {
 if (abs(pixelXOff)>=backBufferWidth || abs(pixelYOff)>=backBufferHeight) {
  return;
 }

 if (screenMode == SM_DIBSECTION_WINDOW) {
  char *src  = (char *)backBuffer;
  char *dst = (char *)backBuffer;
  int rowLen, numRows;

  if (pixelXOff <= 0) {
   dst -= pixelXOff;
   rowLen = backBufferPitch + pixelXOff;
  }
  else {
   src += pixelXOff;
   rowLen = backBufferPitch - pixelXOff;
  }

  if(pixelYOff <= 0) {
   numRows = backBufferHeight + pixelYOff;
   dst += (backBufferHeight-1) * backBufferPitch;
   src += (backBufferHeight+pixelYOff-1) * backBufferPitch;
   for(int i=0; ibltFrom (
     pixelXOff<0?-pixelXOff:0, pixelYOff<0?-pixelYOff:0, 
     secSurf, srcRect
    );
    break;

   case SM_TWO_PAGE_FLIPPING:
    secSurf->bltFrom (
     pixelXOff<0?-pixelXOff:0, pixelYOff<0?-pixelYOff:0, 
     priSurf, srcRect
    );
    break;

   case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_SIMPLE:
   case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_FLIPPING:
    mapSurf->bltFrom (
     pixelXOff<0?-pixelXOff:0, pixelYOff<0?-pixelYOff:0, 
     mapSurf, srcRect
    );
    break;
  }
 }
}

// Draw Related Code
//-----------------------------------------------------------------------------

inline void clipFillRect(Rect &rect) {
 rect.l = max (rect.l, vfxPane.x0);
 rect.t = max (rect.t, vfxPane.y0);
 rect.r = min (rect.r, vfxPane.x1);
 rect.b = min (rect.b, vfxPane.y1);
}

void fillRect (Rect rect, int color) {
 clipFillRect (rect); 
 if (rect.w()<0 || rect.h()<0) {
  return;
 }

 unlockSurface();

 switch (screenMode) {
  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_SIMPLE:
  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_FLIPPING:
   mapSurf->fillBlt (rect, color);
   break;

  case SM_TWO_PAGE_SIMPLE:
  case SM_TWO_PAGE_FLIPPING:
   secSurf->fillBlt (rect, color);
   break;

  case SM_DIBSECTION_WINDOW:
   int numRows = rect.b - rect.t + 1;
   int rectWidth = rect.r - rect.l + 1;
   char *dst = (char *)backBuffer + rect.t*backBufferPitch + rect.l;
   for (int i=0; ipalPalEntry[0];

 if (backBufferHeader && colors) {
  memcpy (&backBufferHeader->bmiColors[start], colors, count*sizeof(RGBQUAD));
 }
 if (!colors) {
  colors = (RGBF *)&backBufferHeader->bmiColors[start];
 }
 RGBF         *p = colors;
 PALETTEENTRY *q = &logPal[start];
 for (int i=start; ipeRed   = p->r;
  q->peGreen = p->g;
  q->peBlue  = p->b;
  q->peFlags = PC_NOCOLLAPSE;
 }

 extern void initRiftPal(PALETTEENTRY *pal);
 initRiftPal(logPal);
 if (realize) {
  if (count == 0) {
   start = 0;
   count = 256;
  }
  if (isADDrawMode(screenMode)) {
   HRESULT result = DSurface::pal->SetEntries(
    DDSETPAL_IMMEDIATE, start, count, &logPal[start]
   );
   if (result != DD_OK) {
    throw EX_DDRAW_FAIL();
   }
  }
  else {
   if (hPalette) {
    SelectPalette (mainDC, hOldPalette, FALSE);
    DeleteObject (hPalette);
   }
   hPalette = CreatePalette (logPalHeader);
   hOldPalette = SelectPalette (mainDC, hPalette, FALSE);
   RealizePalette (mainDC);
  }
 }
}

int findNearestColor (int r, int g, int b) {
 float bestDist = 10000000.0f;
 int best = 0;
 for (int i=0; i<256; i++) {
  int rd = logPal[i].peRed   - r;
  int gd = logPal[i].peGreen - g;
  int bd = logPal[i].peBlue  - b;
  float dist = rd*rd + gd*gd + bd*bd;
  if (dist < bestDist) {
   bestDist = dist;
   best = i;
  }
 }
 return best;
}

void loadPalette (char *filename, int blankFirst) {
 BaseFile palFile (filename);
 if (palFile.size() == 776) {
  palFile.seek (8);
  for (int i=0; i<256; i++) {
    palFile.read (&scratchPalette[i].r, 1);
    palFile.read (&scratchPalette[i].g, 1);
    palFile.read (&scratchPalette[i].b, 1);
  }
  // This this is a .col file. Skip the first 8 bytes and read triplets
 }
 else if (palFile.size() == 1024) {
  // This is quads, probably froma .bmp
  palFile.read (scratchPalette, 256*4);
 }
 else {
  assert (0);
 }
 if (blankFirst) {
  lockSurface();
  fillRect (Rect (0, 0, 639, 479), 0);
  unlockSurface();
  slam();
 }
 setPalette (0, 256, scratchPalette, TRUE);

 colorBlack   = findNearestColor (0, 0, 0);
 colorRed     = findNearestColor (255, 0, 0);
 colorGreen   = findNearestColor (0, 255, 0);
 colorBlue    = findNearestColor (0, 0, 255);
 colorMagenta = findNearestColor (255, 0, 255);
 colorYellow  = findNearestColor (255, 255, 0);
 colorOrange  = findNearestColor (240, 61, 0);
  colorWhite   = findNearestColor (255, 255, 255);
 colorGray    = findNearestColor (128, 128, 128);
}

// Font Related Code
//-----------------------------------------------------------------------------

HFONT fonts [FT_NUM_FONTS];
int fontHeights [FT_NUM_FONTS];
HPEN pen;

void initFonts() {
 memset (fonts, 0, sizeof(HFONT) * FT_NUM_FONTS);

 fontHeights[F_BASIC] = 16;
 fonts[FT_BASIC] = CreateFont (
  16, 0, 0, 0, 0, 0, 0, 0, DEFAULT_CHARSET, 0, 0, 0, 0, 
  "Bookman Old Style Bold"
 );

 fontHeights[FT_BASIC_ITALIC] = 16;
 fonts[FT_BASIC_ITALIC] = CreateFont (
  16, 0, 0, 0, 0, 10, 0, 0, DEFAULT_CHARSET, 0, 0, 0, 0, 
  "Bookman Old Style Bold"
 );

 fontHeights[FT_DEBUG] = 14;
 fonts[FT_DEBUG] = CreateFont (
  14, 0, 0, 0, 0, 0, 0, 0, DEFAULT_CHARSET, 0, 0, 0, 0, 
  "Courier New"
 );

 fonts[FT_CRAZY] = NULL;

 pen = CreatePen (PS_SOLID, 0, 0);
}

void getTextDim (char *s, int &w, int &h, int font) {
 SIZE size;
 SelectObject (backDC, fonts[font]);
 GetTextExtentPoint32(backDC,s,strlen(s),&size); 
 w = size.cx;
 h = fontHeights[font];
}

void drawText (char *s, int x, int y, int font, int color) {
 assert (0 <= font && font < FT_NUM_FONTS);
 assert (fonts[font]);

 HDC dc = NULL;
 RECT *clipRect = (RECT *)&getClipRect();

 DSurface *surf = NULL;
 unlockSurface();

 switch (screenMode) {
  case SM_TWO_PAGE_SIMPLE:
  case SM_TWO_PAGE_FLIPPING:
   surf = secSurf;
   dc = secSurf->getDC();
   break;

  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_SIMPLE:
  case SM_FOUR_PAGE_BACKGROUND_COMPOSITE_FLIPPING:
   surf = mapSurf;
   dc = mapSurf->getDC();
   break;

  case SM_DIBSECTION_WINDOW:
   dc = backDC;
   break;
 };

 assert (dc);

 SetBkMode (dc, TRANSPARENT);
 SetTextAlign (dc, TA_LEFT|TA_TOP|TA_NOUPDATECP);
    SetTextColor(dc, PALETTEINDEX(color));
 SelectObject (dc, fonts[font]);
 ExtTextOut (dc, x, y, ETO_CLIPPED, clipRect, s, strlen(s), NULL);

 if (surf) {
  surf->releaseDC();
 }

 lockSurface();
}

Appendix C. Sound Interface


// This is a very simplistic sound interface for demonstration only.
// Note that, for example, I do not properly delete the sound objects
// after I'm done with them

#include "windows.h"
#include "dsound.h"
#include "stdio.h"
#include "io.h"

#define SKIP_HEADER 75  // *** Cheesey skip .wav header hack

#define false (0)
#define true (1)

int noSound = 1;
int soundInitialized = false;
LPDIRECTSOUND directSound = NULL;
LPDIRECTSOUNDBUFFER primaryBuffer = NULL;
WAVEFORMATEX theWaveFormat;
extern HWND hWnd = NULL;

int initSound() {
 if (soundInitialized) {
  return false;
 }

 DSBUFFERDESC bufferDesc;

 soundInitialized = false;

 if (SUCCEEDED(DirectSoundCreate(NULL, &directSound, NULL))) {
  if (SUCCEEDED(directSound->SetCooperativeLevel(hWnd, DSSCL_PRIORITY))) {
   // Set up wave format structure for primary buffer
   // Configure for 22 kHz, 8-bit mono
   memset(&theWaveFormat, 0, sizeof(WAVEFORMATEX));
   theWaveFormat.wFormatTag       = WAVE_FORMAT_PCM;
   theWaveFormat.nChannels        = 1;
   theWaveFormat.nSamplesPerSec   = 22050;
   theWaveFormat.nBlockAlign      = 1;
   theWaveFormat.wBitsPerSample   = 8;
   theWaveFormat.nAvgBytesPerSec = 
    theWaveFormat.nSamplesPerSec* theWaveFormat.nBlockAlign;

   // Set up DSBUFFERDESC structure
   // lpwxFormat must be NULL for primary buffers at setup time
   memset(&bufferDesc, 0, sizeof(DSBUFFERDESC));
   bufferDesc.dwSize            = sizeof(DSBUFFERDESC);
   bufferDesc.dwFlags           = DSBCAPS_PRIMARYBUFFER;
   bufferDesc.dwBufferBytes     = 0;
   bufferDesc.lpwfxFormat       = NULL;

   // Try to create primary sound buffer
   if (SUCCEEDED (
    directSound->CreateSoundBuffer(&bufferDesc, &primaryBuffer, NULL)
   )) {
    // Set primary buffer to desired output format
    if (SUCCEEDED(primaryBuffer->SetFormat(&theWaveFormat))) {
     soundInitialized = true;
    }
   }
  }
 }

 // Log error on failure
 if (!soundInitialized) {
  if (directSound) {
   directSound->Release();
   directSound = NULL;
  }
  noSound = 1;
 }

 return soundInitialized;
}

void shutdownSound() {
 if (!soundInitialized) {
  return;
 }

 soundInitialized = FALSE;

 if (directSound) {
  directSound->Release();
  directSound = NULL;
 }
}

int playWav (char *filename) {
 // This is a totally stupid implemenation for demonstration purposes only.
 // This will allocate a sound buffer every time you call it!

 FILE *f = fopen (filename, "rb");
 if (!f) {
  return false;
 }
 unsigned long size = _filelength(fileno(f))-SKIP_HEADER;

 // Set up DSBUFFERDESC structure
 // lpwxFormat must be NULL for primary buffers at setup time
 DSBUFFERDESC bufferDesc;
 memset(&bufferDesc, 0, sizeof(DSBUFFERDESC));
 bufferDesc.dwSize            =  sizeof(DSBUFFERDESC);
 bufferDesc.dwFlags           =  0;
 bufferDesc.dwBufferBytes     =  size;
 bufferDesc.lpwfxFormat       = &theWaveFormat;

 // Try to create secondary sound buffer
 LPDIRECTSOUNDBUFFER buffer;
 if (!(SUCCEEDED (directSound->CreateSoundBuffer(&bufferDesc, &buffer, NULL)))) {
  return false;
 }

 // Lock buffer
 char *destPtr = NULL;
 unsigned long actualSize;
 void *dummy1;
 unsigned long dummy2;
 if (!SUCCEEDED (
  buffer->Lock (0, size, &destPtr, &actualSize, &dummy1, &dummy2, 0)
 )) {
  return false;
 }
 else if (actualSize != size) {
  return false;
 }

 // Read the data into the secondary buffer
 fseek (f, SKIP_HEADER, SEEK_SET);
 fread (destPtr, size, 1, f);
 buffer->Unlock (destPtr, actualSize, dummy1, dummy2);
 buffer->Play (0, 0, 0);
 return true;
}

Zack Simpson is chief executive nerd and cofounder of Titanic Entertainment and spends much of his time reading dry MSDN articles. Before that, he was director of technology at Origin Systems.

Discuss this article in the forums


Date this article was posted to GameDev.net: 8/7/1999
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
General

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!