Moving To Windows Part 5

Pheon

Another bug! Another crash! Another BSOD! WTF is going on! These are all common questions I often ask when developing stuff. Most of these things can be worked out and solved quite easily with the VC debugger, an invaluable tool to your collection. Which leads us to this tute, Windowed DirectDraw. In practice windowed DirectDraw is essential to debugging and the time spent implementing this will save you 10000x of hours and $$$ in hair replacement treatment.

What's Different?

So what's different about windowed mode? Well firstly we can't page flip, secondly it's windowed so we have to obey the local laws (Win32) and thirdly we don't have to worry about video mode support. The page flipping problem is solved by creating two DirectDraw surfaces independently unlike fullscreen where we implicitly create two surfaces. First we create a front buffer, which is in fact a DDraw surface for the entire desktop, so if we were feeling nasty we could just piss all over the users desktop.

Before we create our Front buffer we need to create a DDraw object and set the cooperative levels, similar to the full screen mode. The exception here is we must set the cooperative level to DDSCL_NORMAL which indicates we don't want exclusive access to the desktop or wish to change the screen mode and just be a normal Win32 app.

        // create DDraw object
        hr = DirectDrawCreate( NULL, &m_DDraw, NULL );
        if (FAILED(hr)) return 1;

        // set cooperative level
        hr = m_DDraw->SetCooperativeLevel( g_hWnd, DDSCL_NORMAL );
        if (FAILED(hr)) goto error;

Now we create our front buffer, seeing as this is the desktop we only specify DDSCAPS_VIDEOMEMORY and DDSCAPS_PRIMARYSURFACE and leave the DDSCAPS_COMPLEX, DDSCAPS_FLIP out as its only one surface. I.e. we can't page flip 1 surface, and a single surface is not complex.

        // create front and back buffers
        memset( &ddsd, 0, sizeof(ddsd) );
        ddsd.dwSize = sizeof(ddsd);
        ddsd.dwFlags = DDSD_CAPS;
        ddsd.ddsCaps.dwCaps = DDSCAPS_VIDEOMEMORY | DDSCAPS_PRIMARYSURFACE;
        hr = m_DDraw->CreateSurface( &ddsd, &m_FrontBuffer, NULL );
        if (FAILED(hr)) goto error;

As before we need a back buffer, we need to specificy its dimensions using DDSD_WIDTH and DDSD_HEIGHT in the dwFlags and then fill in the dwWidth and dwHeight fields. Also we have to specify DDSCAPS_VIDEOMEMORY and DDSCAPS_OFFSCREENPLAIN because our back buffer needs to be video memory (VIDEOMEMORY) and it can't be shown (OFFSCREENPLAIN).

        // create back buffer
        memset( &ddsd, 0, sizeof(ddsd) );
        ddsd.dwSize = sizeof(ddsd);
        ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
        ddsd.dwWidth = Width;
        ddsd.dwHeight = Height;
        ddsd.ddsCaps.dwCaps = DDSCAPS_VIDEOMEMORY | DDSCAPS_OFFSCREENPLAIN;
        hr = m_DDraw->CreateSurface( &ddsd, &m_BackBuffer, NULL );
        if (FAILED(hr)) goto error;

Convert to Codeisum

We must also setup some color space conversion just like the previous tutes but in addition we must save the current desktop Bpp. We need this in the Flip() routine so we know the byte size of each pixel.

        // get info on the front buffer pixel format
        memset( &PixelFormat, 0, sizeof(PixelFormat) );
        PixelFormat.dwSize = sizeof(PixelFormat);
        hr = m_FrontBuffer->GetPixelFormat( &PixelFormat );
        if (FAILED(hr)) goto error;

        // save the bpp of desktop
        m_DesktopBpp = PixelFormat.dwRGBBitCount;

        // setup color space shifts/masks
        m_RedMask = PixelFormat.dwRBitMask;
          .
          .
          .

Clip my what?

Windowed DDraw requires a little more elegance i.e. we can't write crap to the entire screen as it will corrupt other applications windows. To avoid this we create a Clipper object that as the name suggest clips writes to a DDraw surface. Clipper objects are pretty cool, you only need the windows HWND and the rest is done by itself. We start by creating one.

        // create clipper
        hr = m_DDraw->CreateClipper( 0, &m_Clipper, NULL);
        if (FAILED(hr)) goto error;

Pretty easy, we now associate our HWND to it. So it can get the dimensions of our window to calculate the clip planes.

        // attach clipper to window
        hr = m_Clipper->SetHWnd( 0, g_hWnd );
        if (FAILED(hr)) goto error;

And finally attach it to our Frontbuffer, we only want to draw into our window.

        // attach clipper to front buffer
        hr = m_FrontBuffer->SetClipper( m_Clipper );
        if (FAILED(hr)) goto error;

So now we are a well behaved Win32 application.

The rest of the setup and killing code is pretty much identical, with the addition of releasing Clipper objects. Also we've removed all the screen enumeration stuff as its not needed.

Flip-Flop Phenomenon

The flipping routine is very similar but does have some oddities. Firstly we must check if we are the currently active window, if we aren't then we must not write to the frame buffer. As a window may be on top of it thus we will be in some other applications desktop real estate. There is most probably a better way to do this but I check the currently active windows HWND with our app, if they are different i.e. some other window is active then we aren't active.

        // if were not the active app
        if (g_hWnd != GetForegroundWindow() ) return;

Next the desktop could be set to any bpp, 16, 24, or 32, and our full screen conversion routine won't always work, as it uses unsigned short * pointers. What we do is convert the *Dest pointer to unsigned char * and use some nasty looking Case: statements in the heart of our conversion routine. If anyone knows a better way please email me (I think you can use some weird arse C++ extensions for this). None the less we just cast it depending on the screen depth and increment Dest appropriately.

        // ugly data cast
        if (m_DesktopBpp == 16)
        {
                *((unsigned short *)Dest) = r+g+b;
        Dest += 2;
        }
        else if (m_DesktopBpp == 24)
        {
                *((unsigned int *)Dest) = r+g+b;
                Dest+=3;
        }
        else if (m_DesktopBpp == 32)
        {
                *((unsigned int *)Dest) = r+g+b;
                Dest += 4;
        }

And finally we must replace the flip with a Blt, this has two advantages. One, the destination window area might not be the same as our 320x240 VPage, i.e. it needs to be scaled, and two, it can be accelerated by hardware blitters! Cool eh? To do this we first grab the desktop coordinates of the window.

        // Get the window dimensions
        GetWindowRect(g_hWnd, &rect);

Then do the actual Blt(). Its prototype is

        HRESULT Blt(    LPRECT lpDestRect,
                        LPDIRECTDRAWSURFACE lpDDSrcSurface,
                                LPRECT lpSrcRect,
                                DWORD dwFlags,
                                LPDDBLTFX lpDDBltFx
                   );

We ignore most of these fields, DestRect we get from the GetWindowRect() call contains where the window is. The source DDraw surface is the m_BackBuffer, we want to copy the entire source surface so we set lpSrcRect to NULL indicating all of it. Our flags are DDBLT_WAIT as we want to wait until the Blt() has been *PROCESSED*, this means it usually gets put into some FIFO but is dependant on its hardware implementation. It does NOT mean wait until the Blt() has been completed! If we don't specify this and we are doing several Blt()s then we might get the DDERR_WASSTILLDRAWING message. Its meaning is a little misleading but it says I can't process this Blt() now because all my love slots are full or some other hardware reasons. Just remember pretty much ALL DirectX BLAH_WAIT flags mean wait until it's been PROCESSED not completed, a misconception many people seem to make.

Ok, so here's the call.

       // an emulated flip
       hr = m_FrontBuffer->Blt( &rect, m_BackBuffer, NULL, DDBLT_WAIT, NULL);
       if (FAILED(hr)) OutputDebugString( "Frontbuffer->Blit FAILED!\n" );

Remember: *PROCESSED*.

Geez that was quick

Well that's about it, we must create a header file with the new interfaces and add it to the VVideo list, this is all pretty trivial so I won't blab on about it and just paste it.

   /*A spiffy new header file*/

   /*************************************************************************
   *
   *       Title:  WinDDraw.h
   *       Desc:   Windowed DDraw Hedaer file
   *
   *       Note:
   *
   *************************************************************************/
   #ifndef WINDDRAW_H
   #define WINDDRAW_H

   /************************************************************************/
   // Includes
   /************************************************************************/

   #include "VVideo.h"

   /************************************************************************/
   // Global Data
   /************************************************************************/

   /************************************************************************/
   // Global Functions
   /************************************************************************/

   int             WinDDraw_Open( void );
   void            WinDDraw_Close( void );
   void            WinDDraw_Flip( void );
   Color32_t       *WinDDraw_GetAddress( void );

   #endif //WINDDRAW_H

And our new VVideo driver list:

           // the list
           #define NUMBER_DRIVERS 3
           static Driver_t         m_DriverList[ NUMBER_DRIVERS ] =
           {
                   {       "GDI",
                           GDI_Open,
                           GDI_Close,
                           GDI_GetAddress,
                           GDI_Flip,
                   },
                   {
                           "DirectDraw",
                           DDraw_Open,
                           DDraw_Close,
                           DDraw_GetAddress,
                           DDraw_Flip,
                   },
                   {
                           "WindowedDirectDraw",
                           WinDDraw_Open,
                           WinDDraw_Close,
                           WinDDraw_GetAddress,
                           WinDDraw_Flip,
                   }
           };

Revolving doors

And we're done *cheer*! Just don't forget about VVConfig.cfg and change the driver number. So I hope you guys can see DirectDraw really ain't too hard and can be faster than its VESA equivalent via the use of hardware blitters, DMA transfers that are all transparent to us. This is a blessing and a curse like you must setup the right conditions for these things to work, which is hardware dependant and not documented... ahh the joy of DirectX optimization.

Good night

Well that about wraps up this 2D Frame buffer what's a ma thing. Hopefully I've covered all the basics and you now have no reason to hide away in DOS and VESA. Send me some greetz in your next production or write me some mail and until then code your life away.

Thanks to all dudes who read and gave me some feedback, Ravian, Pluse, MZ, gyr, zoon, Adok for slotting it into Hugi, the #coders mob and the Ratbag crew.

Pheon/Aaron Foo . pheons@hotmail.com