Simple, Small and Lean Tutorial on DirectDraw

Submissive

Well... After reading the last Hugi-Mag I realized that there are a lot of beginner tutorials. I don't want to bash anyone, but no one in the world needs a mode-x line tutorial anymore...

I decided it would be a nice idea to write something. I've been asked 'bout a tutorial how to do DirectDraw. That would be interesting for beginners who don't want to fool around with Vesa and for the old farts who still do Vesa-demos and are too lazy to change to windows.

If you're going to use DDraw you have to ask yourself what you want. DDraw can do a lot of things for you. You do accelerated blitting and rect-filling and lots of other niffy stuff, but do you really need that? I think at first it would be cool to have a code that replaces the Vesa-engine and hides all the windows-details from you. This won't be a complete DDraw wrapper like PTC. It's just a video-mode startup.

Creating a Window

Every DDraw program needs a window. Because we don't see the window anymore as soon as we started up DDraw we don't have to worry much about it. It's sufficient to create a silly fullscreen window which only does what's absolutely needed.

At first we have to create (register) a window-class. Window-Classes describe the look and feel of a window, and tell windows where to send the messages to. You don't have to worry much about that code.

    WNDCLASS wc;
    memset (&wc, 0, sizeof (wc));
    wc.style         = CS_BYTEALIGNCLIENT;
    wc.lpfnWndProc   = WindowProc;
    wc.hInstance     = hInstance;
    wc.hbrBackground = (HBRUSH) GetStockObject (BLACK_BRUSH);
    wc.lpszClassName = "HugiSucks";
    RegisterClass( &wc );

wc.lpfnWndProc:
It's a pointer to a Message-Function which I'll describe in detail later. It's the place where the window will send its messages to.

wc.hInstance:
That is the instance handle of our program. You will get the instance handle at the main routine below. It's damn important that you use this magic instance number.

wc.lpszClassName:
This name identifies the window. You should use a cryptic name. If another program already registered a window under the same name you're in trouble.

We will now code the WindowProc thing. It's the function which will receive all the thousand window-messages. Because this window should not do anything the WindowProc is kinda short.

All WindowProcs look like this. They receive messages and parameters and use a big switch-statement to handle some of them. As you see we only handle three of them.

    long CALLBACK WindowProc( HWND hWnd, UINT message,
                              WPARAM wParam, LPARAM lParam )
    {
        switch (message)
        {
            case WM_ACTIVATE:
              if (wParam== WA_ACTIVE)   ShowCursor (0);
              if (wParam== WA_INACTIVE) ShowCursor (1);
            break;

            case WM_PAINT:
            {
              if (!dd)
                InitDirectDraw (hWnd, 640, 480, 8)
            } break;

            case WM_DESTROY:
            case WM_KEYDOWN:
                PostQuitMessage( 0 );
            break;
        }
        return DefWindowProc( hWnd, message, wParam, lParam );
    }

The WM_ACTIVATE message just tells us that our window has changed its activated flag activated. This is a perfect spot to deactivate the ugly window-cursor. WM_DESTROY and WM_KEYDOWN are used to force a program-exit. At the moment don't worry about the WM_PAINT handler. I'll explain that one later.

Now we create one of these fine windows. Keep in mind to use the same name as the one of our just registered window-class. You also don't have to worry about the style and styleex values unless you want to work with multiple windows and do gui-stuff. Finally we check if the window is initialized and show it.

    HWND WindowHandle =
       CreateWindowEx( WS_EX_TOPMOST,                 // styleex
                       "HugiSucks",                   // classname
                       "",                            // caption (title)
                       WS_POPUP,                      // style
                       0,                             // left
                       0,                             // top
                       GetSystemMetrics(SM_CXSCREEN), // right
                       GetSystemMetrics(SM_CYSCREEN), // bottom
                       0,                             // parent window (none)
                       0,                             // menu (none)
                       hInstance,                     // instance handle
                       0 );                           // useless thing

    if (!WindowHandle) exit (1);

    ShowWindow( WindowHandle, SW_SHOW);

That's basically all we need to do. Let's test it. Here comes the main-routine of our Windows program. In Windows-programs main is not called main anymore but WinMain (sucks, eh?). You have to cut'n'paste the above code in the main-function to make it run (or be lazy and compile the example code). There is not much magic in this code. The only interesting thing is the message-loop at the end of the function. Here we ask Windows for messages, and pass them to our window.

 int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst,
                        LPSTR lpCmdLine, int nCmdShow)
     {
         // do the following stuff here:
         // 1. Register window class
         // 2. Create a window
         // 3. Show that damn window.

         MSG message;
         while ( GetMessage( &message, 0, 0, 0 ) )
         {
             TranslateMessage( &message );
             DispatchMessage( &message );
         }

         CloseDirectDraw();
         return 0;
     }

When you start this program you'll see nothing but a black screen (to be exact it's our fullscreen window without a caption). That's exactly what we need to fire up DirectDraw.

Starting up DirectDraw

Now we do the minimal DirectDraw startup. Don't wonder that I've used goto's in this code. All that error-handling sucks. Goto's are really cool for this.

We need a handful of static variables.

    static IDirectDraw          *dd=0;
    static IDirectDraw2         *dd2=0;
    static IDirectDrawSurface   *ddSurface=0;

And some code:

    int InitDirectDraw (HWND Window, int width, int height, int bits)
    {
        dd = dd2 = ddSurface = 0;

        if (DirectDrawCreate (NULL, &dd, 0)!= DD_OK) goto error;

        if (dd->SetCooperativeLevel
                (Window, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN |
                 DDSCL_ALLOWREBOOT)!=DD_OK) goto error;

        if (dd->QueryInterface
                (IID_IDirectDraw2, (void **) &dd2)!=DD_OK) goto error;

        if (dd2->SetDisplayMode (width, height, bits, 0, 0)!=DD_OK)
            goto error;

        DDSURFACEDESC Surface;
        memset (&Surface, 0, sizeof (Surface));
        Surface.dwSize            = sizeof( Surface );
        Surface.dwFlags           = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
        Surface.dwBackBufferCount = 1;
        Surface.ddsCaps.dwCaps    = DDSCAPS_PRIMARYSURFACE |
                                    DDSCAPS_FLIP |
                                    DDSCAPS_COMPLEX;

        if (dd2->CreateSurface(&Surface, &ddSurface, 0)!=DD_OK) goto error;
        return 1;

    error:
        CloseDirectDraw();
        return 0;
    }

I won't describe each and every call here... Just the outline:

You create a DirectDraw device. That's something like a control-class for your graphic-card. Then you set the cooperative level to something I would call "not very cooperative". This allows you to change the videomode and go into fullscreen. When this is done you have to take care about what you do. If your program crashes from now on, you have to reboot.

The QueryInterface stuff tells DirectX that we want to work with a newer Version of DirectDraw. (For us, DDraw 2 is okay. The stupid idiots from Microsoft forgot to include a "set video mode" call into DDraw 1). Then we set the mode (if it's supported...).

Now we need a surface. A Surface in DDraw slang is a chunk of video memory which we can work with. Surfaces can be textures, zbuffers, offscreen memory and lots of other stuff we don't need at the moment.

The surface we are going to create is the primary surface. This is the chunk of videomemory you can actually see. Because it's nice to have double- buffering support we tell DDraw that we also want one backbuffer.

Shutting down DirectDraw

Not much to say about that. It just frees all resources allocated by the init-code. You don't have to worry about changing video-mode again. This will be done automaticially. Don't forget to call this function when you exit your program.

    void CloseDirectDraw (void)
    {
        if (ddSurface)  ddSurface->Release();
        if (dd2)        dd2->Release();
        if (dd)         dd->Release();
    }

Accessing Video-Memory and Page-Flipping

All right. I guess you want to draw a some pixels now. Let's see what it takes to get a pointer to the video-memory. We are now going to access the surface. Therefore we must now check if no stupid idiot switched the task or trashed our surface:

    if (ddSurface->IsLost()!=DD_OK)
       ddSurface->Restore();

Rembember that we initialized the surface for doublebuffering? We will not draw our pixels into the visible screen area, but into the hidden one. DDraw will give us a pointer to this surface with the following line:

    DDSCAPS caps;
    caps.dwCaps = DDSCAPS_BACKBUFFER;
    IDirectDrawSurface * backbuffer;
    ddSurface-> GetAttachedSurface(&caps, &backbuffer);

Once again we're going to access it, so a check is necessary:

    if (backbuffer->IsLost()!=DD_OK)
       backbuffer->Restore();

And finally we get our pointer:

    DDSURFACEDESC sd;
    memset (&sd, 0, sizeof (DDSURFACEDESC));
    sd.dwSize = sizeof (sd);
    backbuffer->Lock (0, &sd, DDLOCK_SURFACEMEMORYPTR  | DDLOCK_WAIT ,0);

The pointer to the video data can now be accessed at sd.lpSurface. One common bug is to assume that the number of pixels per row is exactly the width of your surface. That's true in most cases, but it's a better style to check out the pitch-field of the ds structure. A dummy-code that copies a picture into the surface should look like this:

    // assumes a 8 bits per pixel mode..
    char  *source = (char *) cool_picture_of_you;
    char  *dest   = (char *) sd.lpSurface;

    for (register int y=0; y<height; y++)
    {
      memcpy (dest, source, width);
      dest   += sd.lPitch;
      source += width;
    }

When you're finished writing to the video-memory you have to unlock the memory by calling the unlock function:

    backbuffer->Unlock (sd.lpSurface);

Now we do the doublebuffering using the surface->flip function. This function will in most cases sync with the video-refresh rate. Smooth animation is possible!

    ddSurface->Flip (0, DDFLIP_WAIT );

How to structure your program

Where is the best place to initialize DirectDraw? The answer is quite simple. To initialize DDraw we need a window... and the window must also be visible. This is always the case when Windows tells us that we have to draw the window. Remember the WM_PAINT-Handler in the WindowProc? This is the spot where we call the initialize function. Deinitialisation is done at the end of the program... Quite simple, eh?

Now we come up with a problem which is native to Windows coding. We don't have any point where we can start rendering images. Since Windows-programs are event-driven the program spends all its time waiting for messages (and it does this very well. If there is no message the main-loop will simply stall). Fortunately Windows can do multi-threading. And we're now starting up a thread that doesn't have to care about window-messages and other useless stuff. It's something like the good old main you used all the time before.

    DWORD WINAPI mymain (void * argument)
    {
        // do whatever you like here..

        // close the window..
        SendMessage (MainWindow, WM_CLOSE, 0, 0);
    }

It's important that you send the WM_CLOSE message, otherwise the program will loop forever. Starting and killing threads is easy. I always do it with these two nice calls:

    CreateThread (0, 0, mymain, 0, 0, &ThreadId);
    TerminateThread ((HANDLE) ThreadId, 0);

It's best to create the thread directly after the DirectDraw initialization.

Now there is one final gotcha in the code. The old dos-farts among you might already know what's coming up: process syncronization. We have two threads working on the same computer, using the same memory, at almost the same time (this is not true for one CPU computers, but we should always think like it is). It can happen that both threads want to access the same memory at the same time. The only variables that are important here are the DirectDraw Surface things. Our second thread will access them. And if we are unlucky we are just writing to them while the other thread calls CloseDirectDraw().

That would suck, and it takes little to prevent this. We will use critical sections to make sure that access to certain variables is only possible for one thread at a time. Windows has a nice support for critical sections. At the very start of your program you initialize it:

    static CRITICAL_SECTION cr;
    InitializeCriticalSection (&cr);

Now each time you access a surface, ddraw device or something which is shared between the two threads you enclose it in a critical-section pair like this:

    EnterCriticalSection (&cr);

    // access to ddSurface

    LeaveCriticalSection (&cr);

What it does is simple: When you call EnterCriticalSection and another thread is in the critical section your thread will be suspended. The other thread will be restarted, and as soon as he leaves the critical section your first thread will continue to run. That's a little bit weird to understand, but it works... If you're unsure just take a look at the example code.

Last words

Play with the source. I haven't told you how to setup the palette, but you should be able to dig out that stuff yourself. When coding Windows things always put the #define WIN32_LEAN_AND_MEAN before your Windows-include. It prevents the preprocessor from including all the Windows-headers for rare used thinks like OLE, RPC and video-compression.

Oh... you have to link with ddraw.lib and dxguid.lib.

No greetings here... greetings in tutorials suck... I save my words for the next demo/intro I'll do...

Example code

 #define WIN32_LEAN_AND_MEAN
 #include <windows.h>
 #include <ddraw.h>
 #include <stdlib.h>

 static IDirectDraw          *dd        = 0;
 static IDirectDraw2         *dd2       = 0;
 static IDirectDrawSurface   *ddSurface = 0;
 static DWORD                 ThreadId  = 0;
 static HWND                  WindowHandle;
 static CRITICAL_SECTION      cr;


 DWORD WINAPI mymain (void * argument);


 void CloseDirectDraw (void)
 {
         EnterCriticalSection (&cr);
         if (ddSurface)  ddSurface->Release();
         if (dd2)        dd2->Release();
         if (dd)         dd->Release();
         LeaveCriticalSection (&cr);
 }


 int InitDirectDraw (HWND Window, int width, int height, int bits)
 {
         EnterCriticalSection (&cr);

         if (DirectDrawCreate (NULL, &dd, 0)!= DD_OK) goto error;

         if (dd->SetCooperativeLevel
                 (Window, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN |
                  DDSCL_ALLOWREBOOT)!=DD_OK) goto error;

         if (dd->QueryInterface
                 (IID_IDirectDraw2, (void **) &dd2)!=DD_OK) goto error;

         if (dd2->SetDisplayMode (width, height, bits, 0, 0)!=DD_OK)
             goto error;

         DDSURFACEDESC Surface;
         memset (&Surface, 0, sizeof (Surface));
         Surface.dwSize            = sizeof( Surface );
         Surface.dwFlags           = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
         Surface.dwBackBufferCount = 1;
         Surface.ddsCaps.dwCaps    = DDSCAPS_PRIMARYSURFACE |
                                     DDSCAPS_FLIP |
                                     DDSCAPS_COMPLEX;

         if (dd2->CreateSurface(&Surface, &ddSurface, 0)!=DD_OK) goto error;
         LeaveCriticalSection (&cr);
         return 1;

     error:
         LeaveCriticalSection (&cr);
         CloseDirectDraw();
         return 0;
 }



 long CALLBACK WindowProc( HWND hWnd, UINT message,
                           WPARAM wParam, LPARAM lParam )
 {
     switch (message)
     {
         case WM_ACTIVATE:
           if (wParam== WA_ACTIVE)   ShowCursor (0);
           if (wParam== WA_INACTIVE) ShowCursor (1);
         break;

         case WM_PAINT:
           if (!dd)
           {
               InitDirectDraw (hWnd, 320, 240, 8);
               CreateThread (0, 0, mymain, 0, 0, &ThreadId);
           }
         break;

         case WM_DESTROY:
         case WM_KEYDOWN:
             PostQuitMessage( 0 );
         break;
     }
     return DefWindowProc( hWnd, message, wParam, lParam );
 }




 int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst,
                    LPSTR lpCmdLine, int nCmdShow)
 {
     InitializeCriticalSection (&cr);

     HINSTANCE instance = hInstance;

     WNDCLASS wc;
     memset (&wc, 0, sizeof (wc));
     wc.style         = CS_BYTEALIGNCLIENT;
     wc.lpfnWndProc   = WindowProc;
     wc.hInstance     = instance;
     wc.hbrBackground = (HBRUSH) GetStockObject (BLACK_BRUSH);
     wc.lpszClassName = "HugiSucks";
     RegisterClass( &wc );

     WindowHandle =
        CreateWindowEx( WS_EX_TOPMOST,                 // styleex
                        "HugiSucks",                   // classname
                        "",                            // caption (title)
                        WS_POPUP,                      // style
                        0,                             // left
                        0,                             // top
                        GetSystemMetrics(SM_CXSCREEN), // right
                        GetSystemMetrics(SM_CYSCREEN), // bottom
                        0,                             // parent window (none)
                        0,                             // menu (none)
                        instance,                      // instance handle
                        0 );                           // useless thing


     if (!WindowHandle) exit (1);
     ShowWindow( WindowHandle, SW_SHOW);


     MSG message;
     while ( GetMessage( &message, 0, 0, 0 ) )
     {
       TranslateMessage( &message );
       DispatchMessage( &message );
     }

     if (ThreadId)
       TerminateThread ((HANDLE) ThreadId, 0);

     CloseDirectDraw();
     UnregisterClass("HugiSucks", hInstance);
     return 0;
 }



 DWORD WINAPI mymain (void * argument)
 {
     char * temp = new char[320*240];

     for (int frames=0; frames<3200; frames++)
     {
             // draw an ugly pattern...
             int i=0;
             for (int y=0; y<240; y++)
             for (int x=0; x<320; x++)
             {
                 temp[i++] =((x+frames)^y);
             }

             // and show it..
             EnterCriticalSection (&cr);
             if (ddSurface->IsLost()!=DD_OK)
                     ddSurface->Restore();

             DDSCAPS caps;
             caps.dwCaps = DDSCAPS_BACKBUFFER;
             IDirectDrawSurface * backbuffer;
             ddSurface-> GetAttachedSurface(&caps, &backbuffer);

             if (backbuffer->IsLost()!=DD_OK)
                backbuffer->Restore();

             DDSURFACEDESC sd;
             memset (&sd, 0, sizeof (DDSURFACEDESC));
             sd.dwSize = sizeof (sd);
             backbuffer->Lock (0, &sd, DDLOCK_SURFACEMEMORYPTR
                | DDLOCK_WAIT ,0);

             char  *source = (char *) test;
             char  *dest   = (char *) sd.lpSurface;

             for (register int y=0; y<240; y++)
             {
                       memcpy (dest, source, 320);
                       dest   += sd.lPitch;
                       source += 320;
             }

             backbuffer->Unlock (sd.lpSurface);
             ddSurface->Flip (0, DDFLIP_WAIT );
             LeaveCriticalSection (&cr);

     }
     SendMessage (WindowHandle, WM_CLOSE, 0, 0);
     return 0;
 }

- Submissive/Cubic