NewShow: A C++ DIVE Class Sample

From EDM2
Jump to: navigation, search

by Larry Kyrala

For some time now we've heard that C++ is good for us, that we should use it, that it will make our lives easier...and yet it's still hard to find C++ code that does anything fast, small, and cool! So I set out to take a piece of code that was cool and try to implement it in C++, without slowing it down, and without incurring a lot of overhead. All I needed was IBM VisualAge C++ Version 3.0, some friendly pessimism ('It can't be done!'), and I was set to go! This article will show what I found out trying to do such a thing ('gasp!') and pave the way, I hope, for other object-oriented games and multimedia programmers.

I figured OS/2's Direct Interface Video Extensions (DIVE) would be a good place to start, partly because performance issues in DIVE would be highly visible, but mostly because DIVE is cool, and I'd have to tackle it before moving on to other pieces of game code. The other nice thing about DIVE is that an API already exists, which means that I didn't have to spend a lot of time in the abstraction phase of Object-Oriented Design (OOD) - most of it was done for me.

Where Do We Start?

The first part of creating a class is figuring out what the class is going to be used for and what functions it needs to support. This is done through a gradual process of abstraction.

In our case, I started with the current DIVE sample (SHOW.EXE) and looked at only the code that was absolutely necessary to get the image to the screen. (SHOW.EXE is in TOOLKIT\SAMPLES\MM\DIVE when you install the Developer's Toolkit for OS/2 Warp.) Here's a list of the DIVE API's that I singled out:

  • DiveAllocImageBuffer
  • DiveBeginImageBufferAccess
  • DiveBlitImage
  • DiveClose
  • DiveEndImageBufferAccess
  • DiveFreeImageBuffer
  • DiveOpen
  • DiveSetupBlitter

These will become the basic member functions of the Dive class. In order to implement these members, we need to decide which parameters we'll keep and which we won't. At first glance it might seem easiest to simply duplicate the DIVE API parameters and returns, but after taking a closer look, it becomes obvious that simple duplication results in more, not less work.

For instance, notice that all these APIs use the parameter HDIVE to refer to the DIVE instance created - something that we can safely remove from our member function parameters because the Dive class object should know its own handle on construction, a detail that can be internalized. The other thing I look for is any parameters that can use defaults, because I hate typing extra 0L parameters. In C++ if an argument isn't specified, its default value is used (if no default is provided, an error results). As it turns out, there are several places where we can use defaults in these basic API wrappers. Here's what we have so far:

class Dive {
public:
ULONG allocImageBuffer( FOURCC fccColorSpace,
ULONG ulWidth,
ULONG ulHeight,
PBYTE pbImageBuffer = 0,
ULONG ulLineSizeBytes = 0 );

PBYTE beginImageBufferAccess( ULONG ulBufferNumber );

void blitImage( ULONG ulSrcBufNumber,
ULONG ulDstBufNumber = DIVE_BUFFER_SCREEN );  

void close();
void endImageBufferAccess( ULONG ulBufferNumber );
void freeImageBuffer( ULONG ulBufferNumber );
void open( BOOL fNonScreenInstance = FALSE );
void setupBlitter( PSETUP_BLITTER pSetupBlitter = 0 );
private:
HDIVE hDiveInst;
};

Notice that we can reorder any parameters the way we want.

Optimization

OK, now let's think about optimization a little bit. We don't want to penalize the user of our class by adding any additional overhead to the existing DIVE APIs. Fortunately, C++ gives us a way to do this via the inline keyword.

Declaring all of our basic members to be inline functions means that the compiler will treat our functions kind of like a macro. Every time we use an inline member, the code for that function will be expanded into that location, meaning that we will avoid the overhead of a function call, and that our member function will execute as quickly as if we had written the C DIVE API call. Because we use the blitImage() member quite a bit, I'll use that as an example implementation:

inline void Dive::blitImage( ULONG ulScrBufNumber,
ULONG ulDstBufNumber )
{
ULONG rc = DiveBlitImage( hDiveInst,
ulScrBufNumber,
ulDstBufNumber);
}

Now that we have all of our basic members implemented (see the sample code listing in \SOURCE\DEVNEWS\VOL9\NEWSHOW on disc 1 of your DevCon CD- ROMs for how I did it), let's use a few. When I create or delete an instance of the Dive class, I expect it to open or close automatically. This calls for a constructor and a destructor using the previously defined inline members open() and close():

Dive::Dive()
{
open();
memset( &setBlit, '\0', sizeof(SETUP_BLITTER) );
}

Dive::-Dive()
{
close();
} 

Point of No Return...

At this point, we could set up a simple image and blit it to the screen, but there's still the matter of loading images into our Dive instance... I don't want to think about that too much, so I'm going to start making assumptions. Why assume? Because generalized performance is an oxymoron. In other words, where you go from here depends on what you want to do.

I wanted to be able to load an image simply by passing its file name in as a string, so I add a higher level member function:

int loadImage( char* );

I call this a higher level member function because it uses the basic members we previously implemented and builds additional function on top of them.

This member function uses the multimedia input/output (MMIO) APIs provided in OS/2 Multimedia. This allows LoadImage() to read any bitmap format (provided that it has a registered I/O procedure in the system) and translate it to a standard OS/2 .BMP 8-bit color format in our DIVE allocated buffer. (For more information on MMIO and I/O procedures, read the Multimedia Application Programming Guide and the Multimedia Programming Reference, which are in the Developer's Toolkit.) I like this, because it means I can load .TGAs and .TIFs as well as .BMPs (not to mention any other supported image formats in the future). For the present, my loadImage() assumes bitmaps are in 8-bit color (or LUT8), but this could be changed or enhanced without too much trouble.

Note that I set the fccSrcColorFormat, ulSrcWidth, and ulSrcHeight fields of the SETUP_BLITTER data member as soon as I read them. This carries the assumption that if multiple bitmaps are loaded, they are all the same size. This code could be changed to deal with different size bitmaps.

The second thing I wanted to do was simplify the setupBlitter procedure. I wanted to simply specify a target window and set it as the destination for a blit. So I added another higher level member function:

void setupDestination( IWindow* );

Notice that I use an IWindow* instead of an HWND. Although it wouldn't be too hard to implement setupDestination( HWND ), it was easier to use IBM's Open Class Library implementation of IWindow. However, it can be added in later as an overloaded member function anyway, so we can have the best of both worlds.

This member function sets the rest of the required SETUP_BLITTER fields, such as ulDstWidth and ulDstHeight, based off of the position, visible region, and size of the IWindow passed.

Where to Now?

We could stop here and make a simple picture viewer, but a picture viewer doesn't really show you how fast you can blit to the screen. So let's go ahead and implement a simple animation sample (the functional equivalent of SHOW.EXE, but implemented in C++).

I chose to break my design into four parts: a Dive class (as above), a DiveWindow class (uses the Dive class to display in a window), a DiveHandler class (to handle DIVE WM_VRN* events), and a StandardWindow class (a frame window to hold the DiveWindow as a client).

DiveWindow

In order to handle animation blitting as quickly as possible, we need a blitter thread and a way to handle the WM_VRNENABLED/WM_VRNDISABLED messages generated by DIVE. The blitter thread is simple:

void DiveWindow::paintMe()
{
int i = 0;

while( true )
{
if ( pleaseBlit )
{
myDive.blitImage( myImage[i++] );
if ( i > 15 )
i = 0;
}
ICurrentThread::current().sleep( 0 );
}
}

DiveHandler

The DIVE handler is a little trickier. The handler takes advantage of virtual functions to create two members: DiveEnabled() and DiveDisabled(). In the DiveHandler class these simply return false to indicate that the message has not been processed. But, because these are virtual functions, we can implement them in any class derived from DiveHandler, such as DiveWindow. So we can write the following:

inline Boolean DiveWindow::DiveDisabled( IEvent &event )
{
pleaseBlit = false;
myDive.setupBlitter();
 

return true;
}

inline Boolean DiveWindow::DiveEnabled( IEvent &event )
{
if (!pleaseBlit)
{
myDive.setupDestination( this );
pleaseBlit = true;
}
return true;
}

Now we know what to do when we enable or disable DIVE, but how do we know when to do this? DiveHandler has another virtual function named dispatchHandlerEvent() for just that purpose:

Boolean IDiveHandler::dispatchHandlerEvent( IEvent &event )
{
switch (event.eventId())
{
case WM_VRNDISABLED:
return( DiveDisabled( event ) );
break;

case WM_VRNENABLED:
return( DiveEnabled( event ) );
break;

default:
break;
}
return(false);
}

Apart from some front-end work, that's about it (see the code listing for entire sample on disc 1 of your accompanying DevCon CD-ROMs in SOURCE\DEVNEWS\VOL9\NEWSHOW).

Summary

I've shown you how to implement a simple Dive class, and a simple DiveWindow that uses it. What's really neat about this sample is that it's just as fast as the SHOW.EXE sample written in C, at least as far as the critical code path (the blit thread) is concerned, which is all we're really worried about anyway. I could also say that it is significantly easier to read and augment, but that should be qualified by adding that you know something of C++ and the IBM Open Class Library.

You may ask, 'Why duplicate the function of SHOW.EXE instead of exploring vast new frontiers?' My answer is that before we implement those new structures, we need a benchmark to get our bearings. My hope is that this sample will spark your own ideas for implementation and that a little friendly competition will bring the best to light.

Let the games begin!

Reprint Courtesy of International Business Machines Corporation, © International Business Machines Corporation