NewShow: A C++ DIVE Class Sample: Difference between revisions
No edit summary |
mNo edit summary |
||
(2 intermediate revisions by the same user not shown) | |||
Line 3: | Line 3: | ||
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. | 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. | 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?== | ==Where Do We Start?== | ||
Line 9: | Line 9: | ||
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: | 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. | 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: | 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: | ||
<code> | |||
class Dive { | class Dive { | ||
public: | public: | ||
Line 43: | Line 42: | ||
HDIVE hDiveInst; | HDIVE hDiveInst; | ||
}; | }; | ||
</code> | |||
Notice that we can reorder any parameters the way we want. | Notice that we can reorder any parameters the way we want. | ||
Line 50: | Line 49: | ||
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: | 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: | ||
<code> | |||
inline void Dive::blitImage( ULONG ulScrBufNumber, | inline void Dive::blitImage( ULONG ulScrBufNumber, | ||
ULONG ulDstBufNumber ) | ULONG ulDstBufNumber ) | ||
Line 57: | Line 57: | ||
ulDstBufNumber); | ulDstBufNumber); | ||
} | } | ||
</code> | |||
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()''': | 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()''': | ||
<code> | |||
Dive::Dive() | Dive::Dive() | ||
{ | { | ||
Line 69: | Line 69: | ||
{ | { | ||
close(); | close(); | ||
} | } | ||
</code> | |||
==Point of No Return...== | ==Point of No Return...== | ||
Line 76: | Line 77: | ||
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: | 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* ); | 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. | 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. | ||
Line 85: | Line 85: | ||
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: | 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* ); | 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. | 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. | ||
Line 93: | Line 92: | ||
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++). | 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). | 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== | ==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: | 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: | ||
<code> | |||
void DiveWindow::paintMe() | void DiveWindow::paintMe() | ||
{ | { | ||
Line 113: | Line 112: | ||
} | } | ||
} | } | ||
</code> | |||
==DiveHandler== | ==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: | 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: | ||
<code> | |||
inline Boolean DiveWindow::DiveDisabled( IEvent &event ) | inline Boolean DiveWindow::DiveDisabled( IEvent &event ) | ||
{ | { | ||
Line 135: | Line 135: | ||
return true; | return true; | ||
} | } | ||
</code> | |||
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: | 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: | ||
<code> | |||
Boolean IDiveHandler::dispatchHandlerEvent( IEvent &event ) | Boolean IDiveHandler::dispatchHandlerEvent( IEvent &event ) | ||
{ | { | ||
Line 155: | Line 155: | ||
return(false); | return(false); | ||
} | } | ||
</code> | |||
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). | 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== | ==Summary== | ||
Line 165: | Line 165: | ||
Let the games begin! | Let the games begin! | ||
[[Category: | {{IBM-Reprint}} | ||
[[Category:Developer Connection News Volume 9]] |
Latest revision as of 19:54, 8 December 2019
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