Moving To Windows Part 4
The Grim Reaper, DirectX, DirectDraw all conger up dark and evil emotions of the devil itself, MS... but hopefully we can get though this tute and you will realize DirectDraw really isn't very hard and perhaps MS ain't too bad ...
DirectDraw
The number one rule is ALWAYS CHECK THE FUCKING RETURN CODE! Let me say that again ALWAYS CHECK THE FUCKING RETURN CODE no matter how trivial the call may be!.. This is something to swear by. You will find if you don't and some error occurs, the problem generally will not be where it crashed, but somewhere else buried deep within your code. This can sometimes be quite tricky and hard to track down, esp. when dealing with fullscreen exclusive mode... no pansy pretty GUI debugger here.
So how do we check it? Well you could just do if (hr != DD_OK) and this would be fine as 99% of success codes return DD_OK but there are those that return something other than DD_OK to indicate success and because of this we use the FAILED() macro. The FAILED() macro is a generic HRESULT test, HRESULTs are part of Win32 and FAILED() just tests if the result is < 0, SUCCESS() test for >= 0 but the moral of the story is don't test for DD_OK OR D3D_OK, use the FAILED() or SUCCESS() macros and ALWAYS check the return code.
DDraw Picture Frames
DirectDraw, Direct3D, DirectSound, DirectArse... all use a similar interface structure, they have the base/root object which you create everything else off of. For DirectDraw we have a DIRECTDRAW structure/object which is created using the DirectDrawCreate() function. The prototype is
HRESULT WINAPI DirectDrawCreate
(GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter);
If you can take the Hungarian notation, we have the first parameter lpGUID, this is the 'device/screen/object/driver' GUID (Global Unique IDentifier) which is used to specifically select the correct object. In general we want the full desktop display which is identified by essential not specifying a GUID, we pass NULL. The second parameter is the address of a pointer (get use to these) of where we want the DIRECTDRAW object to be placed, and the final parameter must be NULL (see the DirectX docs for that). So to create a DirectDraw object we simply do the following:
// create DDraw object
hr = DirectDrawCreate( NULL, &m_DDraw, NULL );
if (FAILED(hr)) return 1;
And wola, m_DDraw has our brand spanking new DIRECTDRAW object. Now that we have it what do we do with it? Well, we could go nuts, set the video mode and then some where down the track on some machine with a video card that ceased production in the 18th century it will fail and crash. This according to Murphy's law will be 99% of your target platform and your demo will just crash and die and thus 'sux'. What do we do about this? We have a look at what video modes are supported, which is exactly what the EnumDisplayModes() function does.
Enum my What?
EnumDisplayModes() uses a user defined callback function which we will use to save each and every video mode that is supported. The prototype for this function is
HRESULT EnumDisplayModes(DWORD dwFlags, LPDDSURFACEDESC lpDDSurfaceDesc,
LPVOID lpContext,
LPDDENUMMODESCALLBACK2 lpEnumModesCallback);
We ignore most of the parameters as we want every single mode shown to us, lpDDSurfaceDesc is used to filter modes which we don't want to do so we set it to NULL. lpContext is used to pass a variable to our lpEnumModesCallback and seeing as we don't want to send any specific data to the callback we just set lpContect to NULL as well. We use DDEDM_STANDARDVGAMODES in the flags as we also want low resolution modes which were primarily interested in and of course we specify a callback function.
// search though all available modes to find 320x240
m_NumberModes = 0;
hr = m_DDraw->EnumDisplayModes( DDEDM_STANDARDVGAMODES, NULL, NULL,
DDraw_DisplayCallback);
if (FAILED(hr)) goto error;
Our callback DDraw_DisplayCallback() fills out our static mode info structure and increments m_NumberModes each time it gets called. The mode list structure goes like this.
// list of available modes
static struct
{
int Width;
int Height;
int Bpp;
} m_ModeList[ MAX_MODES ];
static int m_NumberModes;
And our callback does the following.
/*************************************************************************
*
* Function: DDraw_DisplayCallback()
*
* Desc: Called for each Display mode enumerated in
* DDraw_Init()
*
* Notes:
*
*************************************************************************/
static HRESULT WINAPI DDraw_DisplayCallback( LPDDSURFACEDESC
lpDDSurfaceDesc, LPVOID lpContext )
{
char buf[256];
// copy mode info into our structure
m_ModeList[ m_NumberModes ].Width = lpDDSurfaceDesc->dwWidth;
m_ModeList[ m_NumberModes ].Height = lpDDSurfaceDesc->dwHeight;
m_ModeList[ m_NumberModes ].Bpp =
lpDDSurfaceDesc->ddpfPixelFormat.dwRGBBitCount;
m_NumberModes++;
// show the modes
sprintf( buf, "(%02i) - %03i x %03i x %i\n",
m_NumberModes-1,
m_ModeList[m_NumberModes-1 ].Width,
m_ModeList[ m_NumberModes-1 ].Height,
m_ModeList[ m_NumberModes-1 ].Bpp );
OutputDebugString( buf );
// next mode
return DDENUMRET_OK;
}
We add the width, height and bpp of each mode in our static structure and then write some debugging info. The sprintf() and OutputDebugString() functions are to display text in VC's Debug window, it currently just lists all the modes we found which can be useful if your having trouble with a specific mode, i.e. it doesn't exist. Also the return value is very important: we have DDENUMRET_OK which allows the next mode to be enumerated, if we specified DDENUMRET_CANCEL then it will not enumerate any more modes and will return from the EnumDisplayMode() function call.
Next we check for the mode we want in our filled out mode structure, for us it's 320x240x16.
// search for our desired mode
Width = 320;
Height = 240;
Bpp = 16;
ModeFound = 0;
for (i=0; i < m_NumberModes; i++)
{
if ( (m_ModeList[i].Width == Width) &&
(m_ModeList[i].Height == Height) &&
(m_ModeList[i].Bpp == Bpp) )
{
ModeFound = 1;
}
}
// was one found?
if (!ModeFound) goto error;
Cooperate with what?
To set a fullscreen mode and take the entire desktop for yourself you must tell Win32, this is done with SetCooperativeLevel(). We use the DDSCL_EXCLUSIVE flag, as we want exclusive access and no one else is to touch it, and DSCL_FULLSCREEN to say we want exclusive access to the entire desktop, the FULLSCREEN. Now all that's left is to set the display mode, and we use SetDisplayMode().
// so the mode exists, set the cooperative level so we can go fullscreen
hr = m_DDraw->SetCooperativeLevel( g_hWnd, D
DSCL_EXCLUSIVE | DDSCL_FULLSCREEN );
if (FAILED(hr)) goto error;
// change the display mode
hr = m_DDraw->SetDisplayMode( Width, Height, Bpp);
if (FAILED(hr)) goto error;
Back to front
And the final thing we need is the actual front and back buffer surfaces, the real video memory, the hard stuff. We create this using the CreateSurface() function. Its prototype is
HRESULT CreateSurface( LPDDSURFACEDESC lpDDSurfaceDesc,
LPDIRECTDRAWSURFACE FAR *lplpDDSurface,
IUnknown FAR *pUnkOuter );
lpDDSurfaceDesc is the surfaces description structure, as always we zero its memory with memset() then, Number 2 Rule. ALWAYS SET dwSize OF THE STRUCTURE!
The number of times I've been caught by this is disgraceful, it often returns DDERR_INVALIDPARAMS so if you get this error remember this rule.
// create front and back buffers
memset( &ddsd, 0, sizeof(ddsd) );
ddsd.dwSize = sizeof(ddsd);
Next we fill out the fields we are interested in.
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.dwBackBufferCount = 1;
ddsd.ddsCaps.dwCaps = DDSCAPS_FLIP | DDSCAPS_COMPLEX |
DDSCAPS_VIDEOMEMORY | DDSCAPS_PRIMARYSURFACE;
dwFlags, DDSD_CAPS means the ddsd.ddsCaps structure has valid data, and DDSD_BACKBUFFERCOUNT means ddsd.dwBackBufferCount has valid data. We want one back buffer as this call only creates our front buffer. The ddsCaps requires a bit more explaining.
DDSCAPS_FLIP - means we want to be able to flip between
surfaces aka page flipping.
DDSCAPS_COMPLEX - means it's a 'complex' surface, i.e. it
is more than 1 surface. We are creating
a Front buffer with 1 back buffer which
is 'attached' to it thus complex.
DDSCAPS_VIDEOMEMORY - means we want this surface to be in
video memory.
DDSCAPS_PRIMARYSURFACE - means it's the surface we see, visible on
the screen.
So when we combine all this data we call CreateSurface() and prey and hope it works.
hr = m_DDraw->CreateSurface( &ddsd, &m_FrontBuffer, NULL );
if (FAILED(hr)) goto error;
Trapdoor Buffer
So now have our Front buffer surface created but a another problem exists, we don't actually do anything to the Front buffer. Everything is done to the backbuffer and thus we need to gain access to it, we use GetAttachedSurface() to do this. Since we created the Front buffer with one Back buffer, the back buffer surface already exists, we just need to grab it. The prototype is
HRESULT GetAttachedSurface( LPDDSCAPS lpDDSCaps,
LPDIRECTDRAWSURFACE FAR *lplpDDAttachedSurface);
The DDSCAPS structure doesn't have a dwSize member but it doesn't hurt to zero its memory. We must specify what kind of attached surface we want to get. The answer is a DDSCAPS_BACKBUFFER so we set this flag and call the function. Note we use m_FrontBufer as the object not m_DDraw, this is because m_FrontBuffer is actually a fully defined object and has many methods just look in the DirectX help and see DirectX is very much an OOP based API.
// get back buffer
memset( &ddcs, 0, sizeof(ddcs) );
ddcs.dwCaps = DDSCAPS_BACKBUFFER;
hr = m_FrontBuffer->GetAttachedSurface( &ddcs, &m_BackBuffer);
if (FAILED(hr)) goto error;
Pixel Panties
By now we have almost everything we need, a Front buffer, Back buffer, but what's the pixel format of the Backbuffer? We could assume it's 565 as most 16bpp pixel formats are but what about the others? Those strange 555 or 666... We need to determine the format and write a general purpose conversion routine from our nice 888 format to whatever the back buffer is in. We use GetPixelFormat() to get the format, its prototype:
HRESULT GetPixelFormat( LPDDPIXELFORMAT lpDDPixelFormat );
The DDPIXELFORMAT structure is pretty strange, it covers any and every possible type of color/pixel/bizarre space. Since we are getting a back buffers pixel format it's safe to assume its of the RGB type, this can be checked by ensuring the DDPF_RGB flag is present in the dwFlags field. What we are really interested in is the RGB bit masks, we get this from dwRBitMask, dwGBitMask, dwBBitMask. We use these to determine the shift left, right and final bit mask in our general conversion routine. It goes like this.
// get info on the back buffer pixel format
memset( &PixelFormat, 0, sizeof(PixelFormat) );
PixelFormat.dwSize = sizeof(PixelFormat);
hr = m_FrontBuffer->GetPixelFormat( &PixelFormat );
if (FAILED(hr)) goto error;
And deterring the left, right, mask for the Red channel is
// setup color space shifts/masks
m_RedMask = PixelFormat.dwRBitMask;
m_RedRShift = 8 - (GetHighestBit( PixelFormat.dwRBitMask ) -
GetLowestBit(PixelFormat.dwRBitMask ));
m_RedLShift = GetLowestBit( PixelFormat.dwRBitMask );
Where GetHighestBit() returns the highest set bit in the mask (out of 32) and GetLowestBit() returns the lowest set bit. So for our conversion we need to do the following:
If we have a 565 format,
m_RedMask = 0xF800; (11111 000000 00000 in binary)
m_RedRShift = 3;
m_RedLShift = 11;
RRRRRRRR - our 8bit VPage data
shift to the right m_RedRShift(3 bits) to remove any lower bits,
xxxRRRRR - 5Bit
shift left m_RedLShift(11 bits) to where the back buffers desired pixel format is,
RRRRR xxxxxx xxxxx
apply mask as it might contain bits in the lower part,
RRRRR 000000 00000
and we're done for Red, and the same applies for Blue and Green. At the end of it we mash them together with an add to compose the final color. E.g. where
r = RRRRR00000000000;
g = 00000GGGGGG00000;
b = 00000000000BBBBB;
Color = r + b + g;
So why don't we just do 'Shift = RightShift - LeftShit'? Well because some things need to be moved to the left and others to the right, take Red which gets shifted left 3 bits from its original 8 and Blue which gets shifted right 3 bits, and seeing as shifts don't take well to negative numbers we perform both. The mask also just ensures we don't overflow into any other fields.
Flip that burger
We're almost done now, but how do we see all this DDraw magic? Well we've got to convert our VPage to the back buffer, and naturally we do this in our Flip() function. In a similar way to Tute3's GDI conversion but we will be using a more general conversion. For starters how on earth do we write to the back buffer? DirectX uses the Lock/Unlock semantics, which allows us direct access to memory between Lock() and Unlock() calls. We firstly Lock() the Back buffer, splatter our pixels all over it, Unlock() it then page flip the backbuffer to the front buffer with Flip(), easy.
The Lock prototype is
HRESULT Lock(LPRECT lpDestRect, LPDDSURFACEDESC lpDDSurfaceDesc,
DWORD dwFlags, HANDLE hEvent)
Seeing as we want access to the entire back buffer we set lpDestRect to NULL, meaning everything. We have to zero memory and set the size of lpDDSurfaceDesc and nothing else in this structure as Lock() fills this out. We set dwFlags to DDLOCK_SURFACEMEMORYPTR as we want an address to the memory and DDLOCK_WAIT so DDraw will wait until it can gain access. The main reason it might not have access is due to other operations on the surface like a Blting call, or some D3D calls.
// lock the surface
memset( &ddsd, 0, sizeof(ddsd) );
ddsd.dwSize = sizeof(ddsd);
hr = m_BackBuffer->Lock( NULL, &ddsd,
DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL);
Now we have the surface locked, we must fill it quickly and efficiently, you should not have long periods between Lock/Unlock. We simply go though the entire VPage and convert each pixel and then write it to the back buffer. The address of the top left hand corner of the backbuffer is written in ddsd.lpSurface, and do not assume it will remain the same for every Lock() call. Always use Lock()/Unlock() when writing to the backbuffer, if you try to be a smartie pants and Lock()/Unlock() once and save the lpSurface address then just write to it you will shafted, i.e. DO NOT DO THIS! Also always call Unlock() immediately after you finish with the data. If you call Flip() without Unlocking the back buffer it will basically tell you to fuck off as will most of the other DDraw calls. One thing you must remember is the lPitch variable, some video cards the pitch - the number of bytes between successive scanlines is not equal to the width of the page times the number of bytes per pixel. So always add ddsd.lPitch to move from scanline to scanline.
// setup pointers
Scanline = (unsigned char *)ddsd.lpSurface;
Src = m_VPage;
// for all scanlines
for (y=0; y < 240; y++)
{
// for all pixels in the scan
Dest = (unsigned short *)Scanline;
for (x=0; x < 320; x++)
{
// conver 888 to whatever format the display is in
r = (((Src->r >> m_RedRShift) << m_RedLShift) & m_RedMask);
g = (((Src->g >> m_GreenRShift) << m_GreenLShift) & m_GreenMask);
b = (((Src->b >> m_BlueRShift) << m_BlueLShift) & m_BlueMask);
Src++;
// write to back buffer
*Dest = r + g + b;
Dest++;
}
// NOTE: lPith might NOT be equal to 320*2.
Scanline += ddsd.lPitch;
}
hr = m_BackBuffer->Unlock(NULL);
if (FAILED(hr)) OutputDebugString("BackBuffer->Unlock() Failed!");
And finally we do some magical page flipping with a simple call:
// page flip it
hr = m_FrontBuffer->Flip(NULL, DDFLIP_WAIT);
if (FAILED(hr)) OutputDebugString("FrontBuffer->Flip() Failed!");
So to summarize we just described the Flip() routine which copies and converts our VPage to the back buffer then shows it to the world, its completed form goes like this:
/*************************************************************************
*
* Function: DDraw_Flip()
*
* Desc: Shows the VPage to the world
*
* Notes:
*
*************************************************************************/
void DDraw_Flip( void )
{
HRESULT hr;
DDSURFACEDESC ddsd;
unsigned char *Scanline;
unsigned short *Dest;
Color32_t *Src;
int x, y;
unsigned r, g, b;
// lock the surface
memset( &ddsd, 0, sizeof(ddsd) );
ddsd.dwSize = sizeof(ddsd);
hr = m_BackBuffer->Lock( NULL, &ddsd,
DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT,
NULL);
if (!FAILED(hr))
{
// setup pointers
Scanline = (unsigned char *)ddsd.lpSurface;
Src = m_VPage;
// for all scanlines
for (y=0; y < 240; y++)
{
// for all pixels in the scan
Dest = (unsigned short *)Scanline;
for (x=0; x < 320; x++)
{
// convert 888 to whatever format the
// display is in
r = (((Src->r >> m_RedRShift)
<< m_RedLShift) & m_RedMask);
g = (((Src->g >> m_GreenRShift)
<< m_GreenLShift) & m_GreenMask);
b = (((Src->b >> m_BlueRShift)
<< m_BlueLShift) & m_BlueMask);
Src++;
// write to back buffer
*Dest = r + g + b;
Dest++;
}
// NOTE: lPith might NOT be equal to 320*2.
Scanline += ddsd.lPitch;
}
hr = m_BackBuffer->Unlock(NULL);
if (FAILED(hr))
OutputDebugString("BackBuffer->Unlock() Failed!");
}
// page flip it
hr = m_FrontBuffer->Flip(NULL, DDFLIP_WAIT);
if (FAILED(hr)) OutputDebugString("FrontBuffer->Flip() Failed!");
}
Error unable to barf - Explosion imminent
And now the joyful task of error handling and clean up. You probably saw above during initialization if(FAILED(hr)) goto error; so wtf is error? Surprisingly enough it's our error handling code. We 'release' or free DDraw references here in back to front order using the IBlah->Release() method. We'll start from the last thing created, our Front and back buffers.
We release the back buffer first:
if (m_BackBuffer)
{
m_BackBuffer->Release();
m_BackBuffer = NULL;
}
Note the if(m_BackBuffer) check and NULL assuagement, this is essential as an error might occur half way during init and thus some interfaces will not exist! And setting it to NULL at the beginning of the program we won't try free this non existent interface. It is also good to set it to NULL upon release, this will say It doesn't exist and if subsequent calls to the error handling are received the released interface will not be released again.
Now the Front buffer:
if (m_FrontBuffer)
{
m_FrontBuffer->Release();
m_FrontBuffer = NULL;
}
Our DirectDraw object:
if (m_DDraw)
{
m_DDraw->SetCooperativeLevel( g_hWnd, DDSCL_NORMAL );
m_DDraw->RestoreDisplayModes();
m_DDraw->Release();
m_DDraw = NULL;
}
We must tell Win32 we are no longer responsible for the desktop display via SetCooperativeLevel()... we are a normal application. And the user will always appreciate his/her desktop in their specified settings, 320x240 desktops aren't all that useful.
And finally the Virtual page memory, thus just prevents any excessive memory leaks:
if (m_VPage)
{
free( m_VPage );
m_VPage = NULL;
}
Cleanup after your dog
The cleanup code is identical to the above error handling code, its frees and releases all interfaces we assign and hopefully leaves the user with a working machine.
Misc. - Things that should never be talked about
There are a number of 'nice' ways to debug and aid you in the quest for DirectDraw supremacy, the main one is DirectX Debug runtimes. These spew unbelievable amounts of crap into your VC debugging window most of which is exactly that... crap. But on occasion when 'strange' things are happening or crashes occur install the debug runtimes and they might tell you what your doing wrong. You do this with the DirectX SDK Install program there's an option of Retail or Debug libs, you then go to the Win32 Control Panel and a DirectX Icon should be there. Go to the DirectDraw tab and play with the Debug level slider. Note that increasing the debug level makes your app incredibly slow.
Another cool toy to play with is SoftICE, it's a kernel mode debugger and can display many error msg's that you might not be able to see, it works irrelevant of the display mode (although some cards fuck this up) and has a really nifty call stack feature. It's not for the pansy VB GUI loving people as it's purely text based but once you've got the hang of it you'll never look back.
' '
Phew we're done, now the Fullscreen DDraw driver is done, simply edit the VVConfig.cfg file and set the driver to 1 and wola! Coke me up and fuck me dead, that lame tunnel effect is engulfing your entire screen! With no changes to the tunnel code or WinMain or anything! It just works as if god intended it! And although it still looks lame it looks fuck loads less lame in full screen! This is the beauty of our Virtual Video frame work, we can plug any driver in and all the code that uses it will just work, it's bliss.
Next we go to Windowed DDraw, to make debugging easier and reduce the coolness factor of any effect. As usual feel free to send bugs/comments/flames to my dogey hotmail account.