กก |
Direct Draw
As usual, you can download the complete source code of the application that is used in this example.
Direct Draw App
If you want fast animation (games!), you have to use DirectX. Try filling the screen by drawing each pixel separately using GDI. Compare it with DirectX, which gives you practically direct access to the screen buffer. There's no competition!
The bad news is that DirectX involves dealing with COM interfaces. The good news is that you can encapsulate them nicely inside C++ classes. Let me show you first how to use these classes to write a simple application that displays an animated, rotating, bitmap. We'll use Windows timer to drive the animation. We'll have to:
- Initialize the program, including the DirectX subsystem
- Respond to timer messages
- Respond to window resizing (we'll use the window mode, rather than full screen)
- Respond to paint messages
Let's start with the WM_PAINT message. This message is sent to us when Windows needs to repaint our window. We have to stash somewhere a complete animation frame, so that we can quickly blit it to screen in response to WM_PAINT. The storage for this frame will be called the back surface.
Upon receiving the timer message, we have to paint a new frame on the back surface. Once we're done, we'll invalidate our window, so that WM_PAINT message will be sent to us.
Upon resizing the window, we have to create a new back surface, corresponding to the new window size. Then we'll draw on it and invalidate the window.
The initialization involves creating the timer, creating the World (that's where we keep our bitmap and drive the animation), creating the View, and setting the first timer countdown.
void Controller::Create (CREATESTRUCT * create)
{
_timer.Create (_h, 1);
_world = new World (create->hInstance, IDB_RS);
_view.Create (_h, _world);
_timer.Set (10);
}
|
Here's our response to WM_SIZE. We let the View know about the new size (it will create a new back surface), we call it's Update method which draws the animation frame, and we invalidate the window.
bool Controller::Size (int width, int height)
{
_view.Size (_h, width, height);
_view.Update ();
// force repaint
_h.Invalidate (false);
return true;
}
|
In response to WM_PAINT, we let the View do the painting (blit the back surface to the screen). We also have to tell Windows that the painting is done--validate the uncovered area. This is done simply by constructing the PaintCanvas object.
bool Controller::Paint (HDC hdc)
{
_view.Paint (_h);
// validate painted area
PaintCanvas canvas (_h);
return true;
}
|
In response to WM_TIMER, we kill the timer, tell the World to move one frame ahead, let the View repaint the frame on the back surface, invalidate the window (so that Paint is called) and set the timeout again.
bool Controller::Timer (int id, TIMERPROC * proc)
{
_timer.Kill ();
_world->Step ();
_view.Update ();
// force repaint
_h.Invalidate (false);
_timer.Set (10);
return true;
}
|
There is one more thing. DirectX doesn't like when a screen saver kicks in, so we have to preempt it.
bool Controller::SysCommand (int cmd)
{
// disable screen savers
if (cmd == SC_SCREENSAVE || cmd == SC_MONITORPOWER)
return true;
return false;
}
|
By the way, this is how SysCommand is called from the window procedure:
case WM_SYSCOMMAND:
if (pCtrl->SysCommand (wParam & 0xfff0))
return 0;
break;
|
Let's now have a closer look at View, which is the class dealing with output to screen. Notice two important data members, the Direct::Draw object and the Direct::Surface object.
class View
{
public:
View ();
void Create (HWND h, World * world);
void Size (HWND h, int width, int height);
void Update ();
void Paint (HWND h);
private:
Direct::Draw _draw;
Direct::Surface _backSurface;
World * _world;
int _dx;
int _dy;
};
|
I have encapsulated all DirectX classes inside the namespace Direct. This trick enforces a very nice naming convention, namely, you have to prefix all DirectX classes with the prefix Direct::.
The Direct::Draw object's duty is to initialize the DirectX subsystem. It does it in it's constructor, so any time View is constructed (once per program instantiation), DirectX is ready to use. It's destructor, by the way, frees the resources used by DirectX.
Direct::Surface _backSurface will be used by View to draw the animation upon it.
The initialization of DirectX also involves setting up the collaboration level with Windows. Here, we are telling it to work nicely with other windows, rather then taking over in the full-screen mode.
void View::Create (HWND h, World * world)
{
_world = world;
// Set coopration with Windows
_draw.SetCoopNormal (h);
}
|
Back surface is created (and re-created) in response to WM_SIZE message. Surfaces are implemented in such a way that regular assignment operation does the right thing. It deallocates the previous surface and initializes a new one. Notice that the constructor of an OffScreenSurface takes the Direct::Draw object and the dimensions of the surface in pixels.
void View::Size (HWND h, int width, int height)
{
_dx = width;
_dy = height;
if (_dx == 0 || _dy == 0)
return;
_backSurface = Direct::OffScreenSurface (_draw, _dx, _dy);
}
|
Painting is slightly tricky. We have to get access to the primary drawing surface, which is the whole screen buffer. But since we are not using the full-screen mode, we want to draw only inside the rectangular area of our window. That's why we create a Direct::Clipper, to clip anything that would fall outside of that area. The clipper figures out the correct area using the handle to the window that we are passing to it.
Next, we create the primary surface and pass it the clipper. Now we can blit the image directly from our back surface to the screen. Again, since the primary surface represents the whole screen, we have to offset our blit by specifying the target rectangle. The auxillary class Direct::WinRect retrieves the coordinates of that rectangle.
void View::Paint (HWND h)
{
if (_dx == 0 || _dy == 0)
return;
// Clip to window
Direct::Clipper clipper (_draw);
clipper.SetHWnd (h);
// Screen surface
Direct::PrimarySurface surf (_draw);
surf.SetClipper (clipper);
Direct::WinRect rect (h);
// Blit from back surface to screen
surf.BltFrom (_backSurface, &rect);
}
|
Finally, the actual drawing is done using direct access to the back surface buffer. But first, we flood the back surface with white background. Then we construct the Direct::SurfaceBuf. This is the object that let's us set pixels directly into the buffer's memory (depending on implementation, it might be your video card memory or a chunk of your system memory). The details of our drawing algorithm are inessential here. Suffice it to say that the work of setting the pixel is done in the SetPixel method.
void View::Update ()
{
if (_dx == 0 || _dy == 0)
return;
try
{
// white background
_backSurface.Fill (RGB (255, 255, 255));
{
// Get direct access to back surface
Direct::SurfaceBuf buf (_backSurface);
// draw bitmap within a centered square
int side = 100;
if (_dx < side)
side = _dx;
if (_dy < side)
side = _dy;
int xOff = (_dx - side) / 2;
int yOff = (_dy - side) / 2;
assert (xOff >= 0);
assert (yOff >= 0);
double dxInv = 1.0 / side;
double dyInv = 1.0 / side;
for (int i = 0; i < side; ++i)
{
for (int j = 0; j < side; ++j)
{
double u = dxInv * i;
double v = dyInv * j;
COLORREF color = _world->GetTexel (u, v);
// draw pixel directly
buf.SetPixel (xOff + i, yOff + j, color);
}
}
}
// Paint will blit the image to screen
}
catch (char const * msg)
{
::MessageBox (0, msg, "Viewer error",
MB_OK | MB_ICONERROR);
throw;
}
catch (...)
{
::MessageBox (0, "Unknown error",
"Viewer error",
MB_OK | MB_ICONERROR);
throw;
}
}
|
Besides being able to set individual pixels quickly, DirectX also provides ways to efficiently blit bitmaps or even use GDI functions to write to a surface.
Implementation
As mentioned before, all DirectDraw objects are enclosed in the Direct namespace. The fundamental object, Direct::Draw, is responsible for the initialization and the release of the DirectDraw subsystem. It can also be used to set up the cooperation level with Windows. It's easy to add more methods and options to this class, as the need arises.
namespace Direct
{
// Direct Draw object
class Draw
{
public:
Draw ();
~Draw ()
{
_pDraw->Release ();
}
void SetCoopNormal (HWND h)
{
HRESULT res = _pDraw->SetCooperativeLevel(h,
DDSCL_NORMAL);
if(res != DD_OK)
throw "Cannot set normal cooperative level";
}
IDirectDraw * operator-> () { return _pDraw; }
private:
IDirectDraw * _pDraw;
};
}
// implementation file
using namespace Direct;
Draw::Draw ()
{
HRESULT res = ::DirectDrawCreate (0, &_pDraw, 0);
if(res != DD_OK)
throw "Cannot create Direct Draw object";
}
|
DirectDraw is based on COM interfaces, so it makes sense to encapsulate the "interface magic" in a handy template. The Direct::IFace class takes care of reference counting, pointer dereferencing and assignment (see the assignment of surfaces above).
template<class I>
class IFace
{
public:
IFace (IFace<I> & i)
{
i->AddRef ();
_i = i._i;
}
~IFace ()
{
if (_i)
_i->Release ();
}
void operator = (IFace<I> & i)
{
if (i._i)
i._i->AddRef ();
if (_i)
_i->Release ();
_i = i._i;
}
I * operator-> () { return _i; }
operator I * () { return _i; }
protected:
IFace () : _i (0) {}
protected:
I * _i;
};
|
A drawing surface is an example of an interface. First we define a generic surface, then we'll specialize it to primary and off-screen surfaces.
class Surface: public IFace<IDirectDrawSurface>
{
friend class SurfaceBuf;
protected:
// Locking and unlocking the whole surface
void Lock (SurfaceDesc & desc)
{
assert (_i != 0);
HRESULT res;
do
res = _i->Lock (0, &desc, 0, 0);
while (res == DDERR_WASSTILLDRAWING);
if(res != DD_OK)
throw "Cannot lock surface";
}
void Unlock ()
{
assert (_i != 0);
_i->Unlock (0);
}
public:
Surface () {}
void GetDescription (SurfaceDesc & desc)
{
HRESULT res = _i->GetSurfaceDesc (&desc);
if(res != DD_OK)
throw "Cannot get surface description";
}
void SetClipper (Clipper & clipper)
{
assert (_i != 0);
HRESULT res = _i->SetClipper (clipper);
if(res != DD_OK)
throw "Cannot set clipper";
}
void BltFrom (Surface & src, RECT * dstRect = 0, RECT * srcRect = 0)
{
assert (_i != 0);
HRESULT res = _i->Blt (dstRect, src._i, srcRect, 0, 0);
if(res != DD_OK)
throw "Cannot perform a blt";
}
void Fill (COLORREF color);
};
|
When you create a surface, it has to be one of the derived ones. The primary surface lets you draw directly on your screen, the off-screen surface lets you prepare a picture off-screen in a format that can be very quickly transferred to the screen.
class PrimarySurface: public Surface
{
public:
PrimarySurface () {}
PrimarySurface (Draw & draw) { Init (draw); }
void Init (Draw & draw)
{
SurfaceDesc desc;
desc.SetCapabilities (DDSCAPS_PRIMARYSURFACE);
HRESULT res = draw->CreateSurface (&desc, &_i, 0);
if(res != DD_OK)
throw "Cannot create primary surface";
}
};
class OffScreenSurface: public Surface
{
public:
OffScreenSurface () {}
OffScreenSurface (Draw & draw, int width, int height)
{ Init (draw, width, height); }
void Init (Draw & draw, int width, int height)
{
SurfaceDesc desc;
desc.SetCapabilities (DDSCAPS_OFFSCREENPLAIN);
desc.SetDimensions (width, height);
HRESULT res = draw->CreateSurface (&desc, &_i, 0);
if(res != DD_OK)
throw "Cannot create off-screen surface";
}
};
|
In order to directly access pixels in a surface, you have to lock it. The class, SurfaceBuf, encapsulates locking and unlocking in its constructor and destructor, so that you don't have to worry about it. It also provides direct access to the buffer through its SetPixel method. Notice the low-level address calculations and color formatting.
class SurfaceBuf
{
public:
SurfaceBuf (Surface & surface)
: _surface (surface)
{
SurfaceDesc desc;
surface.Lock (desc);
_pitch = desc.Pitch ();
_buf = static_cast<unsigned char *> (desc.Buffer ());
_format.Init (desc);
int bpp = _format.BitsPp ();
if (bpp != 16 && bpp != 24 && bpp != 32)
{
surface.Unlock ();
throw "Only high color and true color supported";
}
}
~SurfaceBuf ()
{
_surface.Unlock ();
}
void SetPixel (int x, int y, COLORREF color)
{
switch (_format.BitsPp ())
{
case 16:
{
int offset = y * _pitch + x * 2;
unsigned short * p = reinterpret_cast<unsigned short *> (
_buf + offset);
*p &= _format.Mask ();
*p |= static_cast<unsigned short> (
_format.ColorValue16 (color));
}
break;
case 24:
{
int offset = y * _pitch + x * 3;
unsigned long * p = reinterpret_cast<unsigned long *> (
_buf + offset);
*p &= _format.Mask ();
*p |= _format.ColorValue24 (color);
}
break;
case 32:
{
int offset = y * _pitch + x * 4;
unsigned long * p = reinterpret_cast<unsigned long *> (
_buf + offset);
*p &= _format.Mask ();
*p |= _format.ColorValue32 (color);
}
break;
}
}
private:
Surface & _surface;
unsigned char * _buf;
int _pitch;
PixelFormat _format;
};
|
There is a separate class to deal with the formatting of color values. As you can see, depending on the bits-per-pixel setting of the video card, different color encodings are used. The surface descriptor contains bit masks that are used in this encoding. These masks vary not only between bpp settings, but also between video cards. The most complex is the encoding of the color for the 6-bit setting. In 32-bit mode the card can actually support more colors that can be packed into the standard Windows COLORREF. Here, we're not making use of it, but it would be an interesting area to experiment.
class PixelFormat
{
public:
PixelFormat () {}
PixelFormat (SurfaceDesc & desc)
{
Init (desc);
}
void Init (SurfaceDesc & desc);
int BitsPp () const { return _bpp; }
unsigned long Mask () const { return _mask; }
unsigned long ColorValue16 (COLORREF color)
{
return (
(GetRValue (color) << _redShift) & (_redMask << 16)
| (GetGValue (color) << _greenShift) & (_greenMask << 16)
| (GetBValue (color) << _blueShift) & (_blueMask << 16))
>> 16;
}
unsigned long ColorValue24 (COLORREF color)
{
return
(GetRValue (color) << 16) & _redMask
| (GetGValue (color) << 8) & _greenMask
| GetBValue (color) & _blueMask;
}
unsigned long ColorValue32 (COLORREF color)
{
return
(GetRValue (color) << 16) & _redMask
| (GetGValue (color) << 8) & _greenMask
| GetBValue (color) & _blueMask;
}
unsigned long ColorValue (COLORREF color)
{
switch (_bpp)
{
case 16:
return ColorValue16 (color);
case 24:
return ColorValue24 (color);
case 32:
return ColorValue32 (color);
default:
throw "PixelFormat: only 16, 24 and 32 bits supported";
}
}
private:
int _bpp; // bits per pixel 4, 8, 16, 24, or 32
unsigned long _redMask;
unsigned long _greenMask;
unsigned long _blueMask;
unsigned _redShift;
unsigned _greenShift;
unsigned _blueShift;
unsigned long _mask;
};
unsigned HiBitShift (unsigned val)
{
unsigned i = 0;
while (val != 0)
{
val >>= 1;
++i;
}
return i;
}
void PixelFormat::Init (SurfaceDesc & desc)
{
DDPIXELFORMAT & format = desc.PixelFormat ();
if (format.dwFlags != DDPF_RGB)
throw "Direct Draw: Non-RGB formats not supported";
_bpp = format.dwRGBBitCount;
_redMask = format.dwRBitMask;
_greenMask = format.dwGBitMask;
_blueMask = format.dwBBitMask;
_mask = ~(_redMask | _greenMask | _blueMask);
switch (_bpp)
{
case 16:
_redShift = HiBitShift (_redMask) + 8;
_greenShift = HiBitShift (_greenMask) + 8;
_blueShift = HiBitShift (_blueMask) + 8;
break;
case 24:
break;
case 32:
break;
default:
throw "Only 16, 24 and 32 bit graphics supported";
}
}
|
Notice that this tutorial only scratches the surface of DirectX. There are several versions of DirectDraw. There is Direct3D for three-dimensional graphics (although it seems like Open GL might be a better platform). Sound can be dealt with using DirectSound and input devices have their DirectInput. All these subsystems can be encapsulated using similar techniques.
|