Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why is my Win32 gdi+ game unusably slow on Windows 7?

[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?

like image 750
M Katz Avatar asked Nov 15 '22 09:11

M Katz


1 Answers

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.

like image 196
JSBձոգչ Avatar answered Dec 19 '22 08:12

JSBձոգչ