Sponsored By

A C++ Platform Class for Cross-Platform Double-Buffered Graphics

Learn how to write 32-bit graphics applications that compile and run without changes on Macintosh System 6.0.7 and 32-bit Windows (including Win32s and Windows 95).

June 1, 1997

21 Min Read
Game Developer logo in a gray background | Game Developer

Author: by Jon Blossom

What if someone told you that you could release a game simultaneously on Windows, Macintosh, and DOS with no more effort than it takes you to release a DOS game now? Of course, you'd kick the person off your staff, find a loophole in his or her contract, or refuse the project proposal. There are no freebies in software development.

But what if someone said you could release your product on Macintosh and Windows with very little extra work and showed you how to do it?

This article takes the first step in that direction by defining a simple double-buffering architecture that will leverage your existing three-dimensional rendering, animation, sprite composition, and other custom 8-bit graphics code onto Windows and Macintosh systems. Using the small C++ class implemented here, you will be able to write 32-bit graphics applications that compile and run without changes on Macintosh System 6.0.7 and 32-bit Windows (including Win32s and Windows 95).

Double-Buffered Graphics

Double-buffered graphics provide the core graphics technology that power almost all high-performance desktop multimedia and entertainment software. From simple card games to immersive three-dimensional environments, such graphics use offscreen memory to hide image composition from the user and provide smooth transitions between frames of animation. Most games today--and probably most games created in the near future--will rely on copying or page-flipping 256- color graphics from offscreen memory to display memory.

Full-screen 8-bit double buffers such as these are chunks of RAM managed in such a way that a one-to- one mapping exists between pixels on the screen and bytes in the buffer. That's all double buffering is, so why not abstract it in a platform-independent interface?

All it takes to create an image in the offscreen buffer is a definition of the screen-to-offscreen mapping and a method to describe the image in the offscreen buffer to the screen. For all the platforms game developers generally deal with, this requires at most four pieces of information:

* pBits, a pointer to the offscreen buffer byte that maps to the screen point (0, 0).

* Stride, the number of bytes between the buffer byte that maps to the screen point (x, y) and the buffer byte representing (x, y+1).

* Width, the width in pixels of the rectangle represented by the offscreen buffer.

* Height, the height in pixels of the rectangle represented by the offscreen buffer.

Given these four pieces of information, you can implement any graphics algorithm to render three-dimensional texture-mapped environments, animate complex action sequences, or present tic-tac-toe at 30 frames per second. To make those graphics routines work on any platform, all you need is that magical interface that provides these four elemental pieces of information and a way to display completed images on the screen.

Listing 1 shows the public aspects of this dream interface in a simple C++ declaration of a class called COffscreenBuffer. All operations on the class except GetBits are declared const because they do not allow change to the buffered image or to the size of the buffer. I've added the Lock and Unlock methods, which I'll explain.

Listing 1. Public Functions

class COffscreenBuffer
{
public:
// Basic information access
char unsigned *GetBits(void);
long GetStride(void) const;
int GetWidth(void) const;
int GetHeight(void) const;

// Displaying the buffer
void SwapBuffer(void) const;

// Pixel access control
void Lock(void) const;
void Unlock(void) const;
};

Before digging into various implementations of COffscreenBuffer, let's take a look at how we can use such an interface. Say I have a pointer called pBuffer to a COffscreenBuffer object, and I want to set a single pixel to a given color index. From the definition of pBits and Stride, I can deduce that for any point (x, y) in the buffer such that x >= 0 and y >= 0 and x < Height, the memory location pBits + Stride * y + x holds the corresponding byte of offscreen memory.

Using this definition, I can construct a SetPixel function using the following lines of code:

char unsigned *pPixel = pBuffer- >GetBits() + pBuffer->GetStride() * y + x; *pPixel = Color;

Of course, this doesn't include any validation or clipping tests, and it's also not very useful in the inner loop of an optimized rendering engine. But you get the idea.

Another simple graphics function would be to fill the buffer with a solid color. Using the four atomic offscreen data, I could perform this buffer clear with a few simple instructions:

char unsigned *pBits = pBuffer->GetBits();
for
(int y = 0; y GetHeight(); ++y, pBits += pBuffer->GetStride()) memset(pBits, Color, pBuffer->GetWidth());

For every line in the buffer, this fills as many bytes with the specified color as there are horizontal pixels, then skips down to the beginning of the next line. If I know that the buffer occupies contiguous memory, that any additional bytes included as padding are ignored, and that Stride is positive, I can make a small optimization and write the buffer clear like this:

long BufferSize = pBuffer-> GetHeight() * pBuffer->GetStride();
memset(pBuffer->GetBits(), Color, BufferSize);

This would, however, be a bad idea, as Stride has been defined as a signed long value and will often be negative on Windows machines.

A third simple example would be to draw a 45- degree line from the point (x, y) to the point (x+n, y+n), where n is positive. No problem:

char unsigned *pPixel = pBuffer->GetBits() + pBuffer-> GetStride() * y + x;

// Adding Stride+1 moves the pointer from (x,y) to (x+1,y+1)
for (int i=0; iGetStride() + 1)
*pPixel = Color;

With some extrapolation on the part of the reader, these simple examples show that the COffscreenBuffer interface shown in Listing 1 provides all the essential elements for a complete graphics system. Further, none use any code outside the accepted ANSI C++, supported by any compiler worth its salt.

In other words, as long as your target platform can transfer an 8-bit packed pixel image from memory to the screen and supports an ANSI C++ compiler, you can write a COffscreenBuffer implementation and support the drawing functions above without changes.

Making It Macintosh

So let's get down to the business of implementing COffscreenBuffer on the two platforms most likely to be the multimedia systems of the future: Windows and the Macintosh. On both, I'll show you how to exploit system-supported double buffering to do everything you ever wanted--or at least start you along that path.

Color 32-bit QuickDraw introduced a new architecture for offscreen drawing support on the Macintosh called a GWorld, which became a reliable part of the operating system in version 6.0.7. This extension allows the use of QuickDraw functions to draw into structured offscreen memory, and it enables the Macintosh version of COffscreenBuffer declared in Listing 2 and implemented in Listing 3.

Listing 2. Macintosh Declaration

#include // For GWorld stuff

class COffscreenBuffer
{
public:
// Basic information access
char unsigned *GetBits(void) { return pBits; };
long GetStride(void) const { return Stride; };
int GetWidth(void) const { return Width; };
int GetHeight(void) const { return Height; };

// Displaying the buffer
void SwapBuffer(void) const;

// Pixel access control
void Lock(void) const;
void Unlock(void) const;

// Constructor and Destructor
COffscreenBuffer(void);
~COffscreenBuffer(void);

private:
// Common implementation data
char unsigned *pBits;
long Stride;
int Height;
int Width;

// Macintosh implementation data
GWorldPtr OffscreenGWorld;
char StoredMMUMode;
};


Listing 3. Macintosh Implementation

COffscreenBuffer::COffscreenBuffer(void)
{
// Use the current GDevice and GrafPort to make a GWorld
CGrafPtr CurrentPort;
GDHandle CurrentDevice;
GetGWorld(&CurrentPort, &CurrentDevice);

// Get the color table from the current port
PixMapHandle CurrentPixMap = CurrentPort->portPixMap;
HLock((Handle)CurrentPixMap);
CTabHandle ColorTable = (*CurrentPixMap)->pmTable;

// Create a new GWorld with this information
NewGWorld(&OffscreenGWorld, 8, &CurrentPort->portRect, ColorTable,
CurrentDevice, noNewDevice);

// Store data that doesnOt change
Width = CurrentPort->portRect.right - CurrentPort->portRect.left;
Height = CurrentPort->portRect.bottom - CurrentPort->portRect.top;

// Release the current PixMap
HUnlock((Handle)CurrentPixMap);
}

COffscreenBuffer::~COffscreenBuffer(void)
{
// Free the allocated GWorld
if (OffscreenGWorld)
DisposeGWorld(OffscreenGWorld);
}

void COffscreenBuffer::Lock(void) const
{
PixMapHandle OffscreenPixMap = GetGWorldPixMap(OffscreenGWorld);
if (OffscreenPixMap)
{
// Lock the PixMap memory and pull some info off the PixMap structure
LockPixels(OffscreenPixMap);
Stride = (*OffscreenPixMap)->rowBytes & 0x3FFF;
pBits = (char unsigned *)GetPixBaseAddr(OffscreenPixMap);

// Make sure the MMU is in true 32-bit access mode
StoredMMUMode = true32b;
SwapMMUMode(&StoredMMUMode);
}
}

void COffscreenBuffer::Unlock(void) const
{
PixMapHandle OffscreenPixMap = GetGWorldPixMap(OffscreenGWorld);
if (OffscreenPixMap)
{
// Unlock the PixMap memory and reset Stride and pBits
UnlockPixels(OffscreenPixMap);
Stride = 0;
pBits = 0;

// Restore the previous MMU mode
SwapMMUMode(&StoredMMUMode);
}
}

void COffscreenBuffer::SwapBuffer(void) const
{
// Copy all bits from the offscreen GWorld to the active GrafPort
// Note: The offscreen GWorld should be locked!
CopyBits(&((GrafPort)OffscreenGWorld)->portBits,&((GrafPort)thePort)->portBits,
&OffscreenGWorld->portRect, &thePort->portRect,srcCopy, thePort->visRgn);
}


Constructing a buffer from a GWorld requires a single call to the handy function called NewGWorld. This API requires a rectangle describing the dimensions of the desired buffer and a color table, both of which the COffscreenBuffer constructor swipes from the active window. The dimensions come directly off the CGrafPort structure, and the color table comes from the associated PixMap. For good measure, I've chosen to lock down every handle ever used here, though you may not always have to do so.

The constructor's task finishes with the call to NewGWorld, at which point the calling application becomes the proud parent of a COffscreenBuffer object. So far, so good. However, gaining access to the bits of that GWorld proves to be a trifle difficult because the operating system has allocated the buffer in moveable memory. Enter Lock and Unlock, those traditional commands that prevent data from moving in a linear address space.

Before our application can touch the bits of the offscreen buffer, the Lock method has to guarantee that the bits won't move during pixel access. Apple provides the GetGWorldPixMap and LockPixels functions to handle this, and GetPixBaseAddr provides the magic pBits pointer when all the locking is done. Apple provides the Stride value in the rowBytes field of the PixMap structure, but the system tacks on the two high bits to make things difficult. A simple mask of 0x3FFF pulls them off the top.

In addition to locking the image in memory and masking off the high bit of the scanline offset, I've heard off and on that it's also a good idea to make sure the memory management unit is in true 32-bit access mode. SwapMMUMode handles this, storing the current mode for restoration by the Unlock method.

The Unlock method mirrors the locking function, allowing the operating system to move the offscreen buffer around when the application doesn't need it. The aptly named system call UnlockPixels handles this task, after which I reset the cached pBits value to zero to avoid being bitten by an attempt to access the bits when the buffer is unlocked.

In debug versions, I also add an integer LockCount member variable incremented by Lock and decremented by Unlock, and I assert it is zero when the COffscreenBuffer destructor is called.

The Macintosh requires the Lock...Unlock pair, and other platforms may require it as well. All implementations of COffscreenBuffer must include both methods, whether they do anything or not, and all functions written for COffscreenBuffer must operate between a Lock...Unlock pair to be completely portable. This includes the previous examples and calls to SwapBuffer.

SwapBuffer provides the memory-to-screen transfer for the application when it finishes its graphics processing and wants to display the image. SwapBuffer will call out to CopyBits to do the job, copying the entire offscreen image into the current CGrafPort, using the visRgn as a mask. The operating system will handle all the work of clipping to the visible window area.

The COffscreenBuffer destructor is the easiest of all to implement on the Macintosh. It's just a call to DisposeGWorld, which ditches the GWorld for good.

To use the COffscreenBuffer class properly on the Macintosh, be sure to turn on the pmExplicit and pmAnimated flags of all entries in the palette of the target window to ensure that the color indices used in the offscreen buffer will properly match the colors on the screen, yielding highest copying speeds and proper color matching.

Windows

Like GWorlds on the Macintosh, WinG is the obvious candidate for implementing a double-buffering architecture for Windows. WinG lets us create a buffer, access its bits, and copy it to the screen quickly. The Windows declaration of COffscreenBuffer appears in Listing 4. The following implementation appears in Listing 5.

Listing 4. Windows Declaration

#include // For WinG stuff

// Note that this is supposed to be for Win32, so there are no FAR types.
// However, it could be adapted easily for 16-bit Windows.

class COffscreenBuffer
{
public:
// Basic information access
char unsigned *GetBits(void) { return pBits; };
long GetStride(void) const { return Stride; };
int GetWidth(void) const { return Width; };
int GetHeight(void) const { return Height; };

// Displaying the buffer
void SwapBuffer(void) const;

// Pixel access control - these are no-ops in Windows
void Lock(void) const {};
void Unlock(void) const {};

// Constructor and Destructor
COffscreenBuffer(void);
~COffscreenBuffer(void);

private:
// Common implementation data
char unsigned *pBits;
long Stride;
int Height;
int Width;

// Windows implementation data
HDC OffscreenDC;
HBITMAP OffscreenBitmap;
HBITMAP OriginalMonoBitmap;
};

 

Listing 5. Windows Implementation

// This is here to keep it off the stack during the constructor call
struct {
BITMAPINFOHEADER Header;
RGBQUAD ColorTable[256];
} BufferInfo;

COffscreenBuffer::COffscreenBuffer(void)
{
HWND ActiveWindow = GetActiveWindow();

// Make the buffer the same size as the active window
RECT ClientRect;
GetClientRect(ActiveWindow, &ClientRect);
Width = ClientRect.right - ClientRect.left;
Height = ClientRect.bottom - ClientRect.top;
Stride = (Width + 3) & (~3);

// Set up the header for an optimal WinGBitmap
if (WinGRecommendDIBFormat((LPBITMAPINFO)&BufferInfo))
{
// Preserve sign on biHeight for appropriate orientation
BufferInfo.Header.biWidth = Width;
BufferInfo.Header.biHeight *= Height;

// Grab the color entries from the current palette
HDC hdcScreen = GetDC(ActiveWindow);
if (hdcScreen)
{
PALETTEENTRY Palette[256];
GetSystemPaletteEntries(hdcScreen, 0, 256, Palette);
ReleaseDC(ActiveWindow, hdcScreen);

// Convert the palette entries into RGBQUADs for the color table
for (int i=0; i<256; ++i)
{
BufferInfo.ColorTable[i].rgbRed = Palette[i].peRed;
BufferInfo.ColorTable[i].rgbGreen = Palette[i].peGreen;
BufferInfo.ColorTable[i].rgbBlue = Palette[i].peBlue;
BufferInfo.ColorTable[i].rgbReserved = 0;
}
}

// Create the offscreen DC
OffscreenDC = WinGCreateDC();
if (OffscreenDC)
{
// Create the offscreen bitmap
OffscreenBitmap = WinGCreateBitmap(OffscreenDC,
(LPBITMAPINFO)&BufferInfo, (void * *)&pBits);

if (OffscreenBitmap)
{
// Adjust pBits and Stride for bottom-up DIBs
if (BufferInfo.Header.biHeight > 0)
{
pBits = pBits + (Height - 1) * Stride;
Stride = -Stride;
}

// Prepare the WinGDC/WinGBitmap
OriginalMonoBitmap = (HBITMAP)SelectObject(OffscreenDC,OffscreenBitmap);
}
else
{
// Clean up in case of error
DeleteDC(OffscreenDC);
OffscreenDC = 0;
}
}
}
}

COffscreenBuffer::~COffscreenBuffer(void)
{
// Delete the offscreen bitmap, selecting back in the original bitmap
if (OffscreenDC && OffscreenBitmap)
{
SelectObject(OffscreenDC, OriginalMonoBitmap);
DeleteObject(OffscreenBitmap);
}

// Delete the offscreen device context
if (OffscreenDC)
DeleteDC(OffscreenDC);
}

void COffscreenBuffer::SwapBuffer(void) const
{
// Use the DC of the active window
// NOTE: YouOll lose the 1:1 palette mapping if the Window isnOt CS_OWNDC
HWND ActiveWindow = GetActiveWindow();
if (ActiveWindow)
{
HDC ActiveDC = GetDC(ActiveWindow);
if (ActiveDC)
{
// Perform the blt!
if (ActiveDC)
{
WinGBitBlt(ActiveDC, 0, 0, Width, Height, OffscreenDC, 0, 0);
ReleaseDC(ActiveWindow, ActiveDC);
}
}
}
}


WinG allocates an offscreen buffer for us when we use a WinGCreateDC/WinGCreateBitmap call pair. The buffer memory allocated this way will always map to the same address. It will never move in memory as far as our applications can see, so the unneeded extra limbs Lock and Unlock can be compiled out by declaring them as empty inline functions. Only the constructor, destructor, and swap functions remain.

WinG supports both top-down and bottom-up buffer orientations, as discussed in every piece of WinG literature to date, so the first step in constructing an offscreen buffer is to determine the orientation that will make memory-to-screen blts fastest. WinG provides this information through the WinGRecommendDIBFormat function, called by the constructor before creating the offscreen buffer.

Once it has the optimal Device Independent Bitmap (DIB) orientation, the constructor fills in the biWidth and biHeight fields of the BITMAPINFOHEADER structure containing the optimal format, preserving the sign of biHeight. A color table stolen from the current system palette completes the information necessary to create a WinGBitmap and an accompanying WinGDC, which the COffscreenBuffer constructor does for us. Selecting a WinGBitmap into a new WinGDC pops out a stock monochrome bitmap that the destructor will need later, so I store it in the COffscreenBuffer object until then.

The Width and Height of the buffer came from the foreground window, and WinGRecommendDIBFormat returns the pBits for the buffer. Only the Stride remains, and it's easily calculated because we know the WinG buffer is actually a standard Windows DIB. Every scanline of a DIB begins on a 4-byte boundary, and since our offscreen buffers are one byte per pixel, Stride is just the DWORD aligned Width:

// Align to the highest 4-byte boundary
Stride = (Width + 3) & (~3);

If WinG recommends a top-down DIB, that's all the calculation needed to set up the buffer. pBits points to the top of the buffer, coinciding with (0,0), and Stride indicates a positive step through memory.

For bottom-up DIBs, however, things must be switched around. As it stands, pBits would point to the last scanline in the buffer, and Stride would be the offset from (x, y) to (x, y-1). If you've used WinG before, you'll know that flipping these values around to point in the correct direction requires only two lines:

// Point to first scanline (end of buffer)
pBits = pBits + (Height - 1) * Stride;

// Orient Stride from bottom to top
Stride = -Stride;

With that, the constructor has finished its work, and the calling application is the happy owner of a WinG-based offscreen buffer, wrapped in a COffscreenBuffer object. Your application can draw into this buffer however you choose, calling SwapBuffer when you're ready to display an image.

The Windows SwapBuffer implementation grabs the Device Context from the active window and uses a straightforward WinGBitBlt call to copy the image from the offscreen WinGDC to the topmost window on the screen. Nothing could be easier.

The call to GetDC returns a virgin device context that will not reflect selections you may have made to previous DCs from the window. It contains no palette information. If you have gone through the effort to create an identity palette for the window (or are using the WinG halftone palette), your work will be in vain unless you register the Window with the CS_OWNDC style, which preserves device context settings over GetDC...ReleaseDC call pairs.

When it comes time to destroy the COffscreenBuffer, only three steps need to be taken: selection of the original monochrome bitmap back into the WinGDC, destruction of the WinGBitmap, and destruction of the WinGDC.

Extending the Buffer Class

The COffscreenBuffer class I've developed here provides only the most basic elements of a double buffering system, barely enough to be useful. Many other features could make it a very useful tool in cross-platform game development, but I have left them out for the sake of brevity.

One important thing missing from this COffscreenBuffer class is the explicit connection of an offscreen buffer to a window. As implemented here, COffscreenBuffer uses whatever window happens to be active at the time a method is invoked. When your application isn't in the foreground, this can be a messy thing!

It's easy to store a platform-specific window identifier in the COffscreenBuffer structure, and you can hang a pointer back to the buffer object on the window, too. Under Win32, try using Set/GetWindowLong and GWL_USER to store the pointer. On the Macintosh, Set/GetWRefCon performs a nearly identical task.

Of course, attaching a buffer to a resizable window means you'll have to do something smart when the window changes size. Matching the buffer dimensions to the new dimensions of the window wouldn't be a bad idea.... You'll need to look at UpdateGWorld on the Macintosh, and you'll most likely have to create a new WinGBitmap under Windows.

Many applications don't want an API as clumsy as SwapBuffer. You want to optimize your screen accesses by writing only the areas that haven't changed. A SwapRect method would do the trick very nicely. Implement it on both platforms, and remember to make the rectangle description platform independent!

And what about colors? Both constructors use the current palette to initialize the offscreen color table, but wouldn't it be nice to enable color animation? Just be sure to maintain that 1:1 offscreen mapping for speed.

Wrapping it Up

To compile the code presented in this article, I used Microsoft Visual C++ 2.0 on Windows NT version 3.5 and Symantec C++ 7.0 on Macintosh System 7.0. I ran the applications created on Windows 3.11 with Win32s, Windows NT v. 3.5, and Macintosh System 7.0. The graphics code implemented on top of the COffscreenBuffer API did not change.

If multiplatform graphics programming can be this easy, the excuses for writing DOS-only games begin to look silly. Take, for example, that it took only a weekend (with no sleep) to create a graphics-only version of Doom for Windows using WinG because of the structured design chosen by the programmers at id Software. Most of the third-party three-dimensional rendering systems on the market today have at least three versions--DOS, Windows, and Macintosh. Why shouldn't you?

The Microsoft machine has finally started cranking up and facing the problems of providing real game support in Windows, much of it promised for Windows 95. More and more Macintosh games are appearing on the market, and maybe one day someone will actually come up with a decent Macintosh joystick. DOS continues to score with game programmers, but users hate the configuration problems.

Using a simple system like the COffscreenBuffer class introduced here will enable all of your existing graphics routines on all three platforms and may help you reach more users. With the advent of Windows 95 and its promised support for sound mixing and joystick input, and with the multimedia capabilities already provided on Mac and Windows, your reasons for sticking exclusively to DOS begin to look shortsighted. Why not take the cross-platform plunge?

Jon Blossom is the coauthor of the WinG graphics library for Windows and is the author of Gossamer, a free three-dimensional polygon engine for the Macintosh. He currently works for Maxis and can be reached at [email protected] or through Game Developer magazine.

Read more about:

Features
Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like