views:

115

answers:

2

[important new information available below at bottom of this entry]

I have what I believe is a very standard game loop using GDI+. It works reasonably well (about 25 fps for a complex game, 40 fps for a simple game) on Vista and XP. When I run it on Windows 7 (with a significantly faster CPU, and more memory), it slows down so much the game is unusable (I get anywhere from 0 fps to 4 fps). I have included below what I think are the relevant parts of the code. As I say, I believe this is the simplest kind of (memory-bitmap-based) game loop using GDI+. You can see below two attempts I made to speed things up. First, I was afraid that if InvalidateRect() was being called much more frequently than WM_PAINT messages were being sent, the system was taking this as a clue that my program was bad/slow and was withholding my time slices. So I added the paintIsPending flag to make sure I didn't invalidate more than once per paint. This gave no improvement. Second, I added the code in the OPTIONAL SECTION below, thinking that maybe if I triggered the WM_PAINT message myself instead of waiting for it to be sent things would be better. Again, no improvement.

It seems crazy to me that a simple GDI+ game loop like this would die on Windows 7. I know there are some differences in how Windows 7 handles 2D graphics acceleration, but again this code seems so basic it's hard to believe that it would be non-functional. Also, I know I can switch to DirectX, and I may do that, but currently there is a fair amount invested in the code base represented by the DrawGameStuff( graphics ) call below, and I'd rather not rewrite it if possible.

Thanks for any help.

#define CLIENT_WIDTH 320
#define CLIENT_HEIGHT 480

Graphics *graphics;
HDC memoryDC;
HBITMAP memoryBitmap;
bool paintIsPending = false;

void InitializeEngine( HDC screenDC )
{
    memoryDC = CreateCompatibleDC( screenDC );
    memoryBitmap = CreateCompatibleBitmap( screenDC, CLIENT_WIDTH, CLIENT_HEIGHT );
    SelectObject( memoryDC, memoryBitmap );
    graphics = new Graphics( memoryDC );

    ...
}

BOOL InitInstance( HINSTANCE hInstance, int nCmdShow )
{
    ...
    InitializeEngine( GetWindowDC( hWnd ) );
    ...
    myTimer = SetTimer( hWnd, timerID, 1000 / 60, NULL );
    ...
}

void DrawScreen( HDC hdc )
{
    graphics->Clear( Color( 255, 200, 200, 255 ) );

    DrawGameStuff( graphics );

    BitBlt( hdc, 0, 0, CLIENT_WIDTH, CLIENT_HEIGHT, memoryDC, 0, 0, SRCCOPY );
}

LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
{
    ...
    case WM_TIMER:
        if ( !paintIsPending )
        {
            paintIsPending = true;
            InvalidateRect( hWnd, NULL, false );
            /////// START OPTIONAL SECTION
            UpdateWindow( hWnd );
            ValidateRect( hWnd, NULL );
            /////// END OPTIONAL SECTION
        }
        break;
    case WM_PAINT:
        hdc = BeginPaint( hWnd, &ps );
        DrawScreen( hdc );
        EndPaint( hWnd, &ps );
        paintIsPending = false;
        break;
    ...
}

Aha! I now have more and very relevant information, based on a clue from Chris Becke. I thought for sure it was the BitBlt() that was slow and not the graphics->Clear(), but lo and behold when I commented out the graphics->Clear() I suddenly get 40 FPS on Windows 7. So then I changed the graphics->Clear() into

// graphics->Clear( Color( 255, 200, 200, 255 ) );
SolidBrush brush( Color( 255, 200, 200, 255 ) );
graphics->FillRectangle( &brush, 0, 0, CLIENT_WIDTH, CLIENT_HEIGHT );

and lo and behold it still ran at 40 FPS. I don't know why the FillRectangle() call is faster than the Clear() call.

So then I started adding my game drawing code back in, and I immediately found another call that kills it: to draw the game contents I draw the sprites into the memoryDC using

graphics->DrawImage( myImageThatCameFromAPngFile, destx, desty, srcx, srcy,
    width, height, UnitPixel );

And those calls are very slow also. As an experiment, I pre-drew from my PNG file into a second memoryDC that is compatible with the screenDC. Then, to draw my sprite I draw from this secondary memoryDC into my primary memoryDC. So instead of the DrawImage call above I have:

BitBlt( memoryDC, destx, desty, width, height, secondaryMemoryDC,
    srcx, srcy, SRCCOPY );

And lo and behold the whole game runs at 40 FPS on Windows 7 when I do that.

However, it's not a real solution because in pre-rendering to that secondary memory DC I lose the transparency information from the PNG file, so my sprites are all opaque and ugly now.

So it seems like my problem is an incompatibility between the memoryDC (created to be compatible with the screenDC) and the PNG source file. I don't understand why this incompatibility exists (or at least, why it slows things down so much) only on Windows 7. Is there a way of saving the PNG file to be compatible with the screen from the start? Or of re-rendering it internally at the outset to get a new PNG file that is compatible with the screen? Hmmm....

Okay so I was able to get my PNG file rendering properly by rendering it to an 32bpp HBITMAP as follows:

    HDC hdc = CreateCompatibleDC( GetWindowDC( hWnd ) );
    Bitmap *bitmap = new Bitmap( image->GetWidth(),
        image->GetHeight(), PixelFormat32bppARGB );
    HBITMAP hbitmap;
    bitmap->GetHBITMAP( Color( 0, 0, 0, 0 ), &hbitmap );
    SelectObject( hdc, hbitmap );

    Graphics *g = new Graphics( hdc );
    g->DrawImage( pngImage, 0, 0, 0, 0,
        pngImage->GetWidth(), pngImage->GetHeight(), UnitPixel );

and then rendering it with AlphaBlend():

    _BLENDFUNCTION bf;
    bf.BlendOp = AC_SRC_OVER;
    bf.BlendFlags = 0;
    bf.SourceConstantAlpha = 255;
    bf.AlphaFormat = AC_SRC_ALPHA;
    AlphaBlend( memoryDC, destx, desty, width, height, hdc,
            destx, desty, width, height, bf );

So now I have my game running quickly on Windows 7.

But I still don't understand why I had to go through all this for Windows 7. Why is the default drawing from my PNG image using DrawImage() so slow on Windows 7?

+1  A: 

I don't know if this is why your current code is slow, but a more efficient double-buffering solution would be to only do the BitBlt in the WM_PAINT handler, and call DrawGameStuff in the WM_TIMER handler. This way the only thing that you're doing during a paint is copying the back buffer to the screen, and the actual drawing logic can happen at a different time.

JSBangs
Thanks, but the speed is the same even if I comment out the DrawGameStuff() call altogether. Your point is good in general, but doesn't seem to be related to the trouble I'm having.
M Katz
graphics->Clear is slow then? Except for the fact that I use str8 GDI, not GDI+ I have noticed NO problems with very similar code on Windows 7. If you are using VS not-express, there is a built in profiler that should be able to tell you where the time is being spent - once youve worked the mojo magic to make it work.
Chris Becke
A: 

Could the default interpolation mode be different between 7 and Vista/XP? That could explain your DrawImage speed issues, at least. Try setting that specifically to a lower quality value to see if that impacts the speed of DrawImage.

AlphaBlend and BitBlt are almost always going to be faster - dramatically faster - than GDI+'s DrawImage. I suspect a large part of that is because they dont do the interpolation (ie, the quality stretching/shrinking of an image) that GDI+ does.

GrandmasterB
GB, thanks for the suggestion, but I've already converted my code to DirectX, and now it runs fast everywhere. But, about interpolation mode, do you know if applies even when the image is not being resized at all (which is the case for me)?
M Katz
GDI+ is slower even in that case, just not as slow. I never fully understood why. I suspect it has something to do with the coordinate system - that DrawImage() is not just a wrapper around BitBlt, but is its own device independent (not pixel based) drawing routine. DirectX was probably a good choice for a game. Now you wont be limited at all.
GrandmasterB