Alex
Alex

Reputation: 47

WinApi: Displaying HBITMAP on the screen

I created a simple render function as a test. I call this function with FPS 60. I'm testing 2 pictures in total. Each time the function is called, I draw one picture. With each new call, the picture changes. The idea is that due to the high frequency of the function call, I should get two flashing pictures on the screen.

The problem is that one picture is displayed for a long time on the screen, because of this, the second picture is not rendered. After some time, the second picture starts to be displayed on the screen for a long time, and the first one stops being drawn because of this.

The point is that each picture should be rendered 30 times on the screen in 1 second. But I don't get that flashing

I created two HDC because in the future I will need to be able to display many images at a time.

void Game::render()
{
    static HBITMAP hBitMap1 = (HBITMAP)LoadImage(NULL, L"..\\images\\player01.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
    static HBITMAP hBitMap2 = (HBITMAP)LoadImage(NULL, L"..\\images\\player02.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);

    HDC memDC = CreateCompatibleDC(dc);
    HDC memDC1 = CreateCompatibleDC(dc);
    HBITMAP memBM = CreateCompatibleBitmap(dc, windowRect.right, windowRect.bottom);

    SelectObject(memDC, memBM);

    static int image_changing = 0;
    int x, y;

    HBITMAP hBitMap;
    if (image_changing < 1) {
        hBitMap = hBitMap1;
        x = 200;
        y = 200;
        image_changing++;
    } else {
        hBitMap = hBitMap2;
        x = 200;
        y = 50;
        image_changing = 0;
    }

    HBITMAP hbit = (HBITMAP)SelectObject(memDC1, hBitMap);
    BitBlt(memDC, x, y, IMAGE_SIZE_X, IMAGE_SIZE_Y, memDC1, 0, 0, SRCCOPY);
    SelectObject(memDC1, hbit);

    BitBlt(dc, 0, 0, windowRect.right, windowRect.bottom, memDC, 0, 0, SRCCOPY);

    DeleteDC(memDC1);
    DeleteDC(memDC);
    DeleteObject(memBM);
}

loop for updating render

void Game::run()
{
    // Initialize previous time to current time
    DWORD prevTime = timeGetTime();

    // Initialize frame counter and FPS variables
    int frameCount = 0;

    // Initialize timer variables
    DWORD timer = timeGetTime();
    int elapsedTime = 0;

    MSG msg = { };
    while (true)
    {

        if (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE))
        {
            // Exit
            if (msg.message == WM_QUIT)
                break;
            
            // Add Bullets
            if (msg.message == WM_LBUTTONDOWN)
                if (player->getPlayerCanFire()) {
                    addBullet(LOWORD(msg.lParam), HIWORD(msg.lParam));
                    player->setPlayerCanFire(FALSE);
                    player->setPlayerFireElapsedTime(0);
                }
            
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            // Get current time
            DWORD currentTime = timeGetTime();

            // Calculate difference between current time and previous time
            DWORD deltaTime = currentTime - prevTime;

            // If difference is less than desired time interval, sleep for the difference
            if (deltaTime < FRAME_INTERVAL) {
                Sleep(FRAME_INTERVAL - deltaTime);
            }

            prevTime = currentTime;

            // convert to seconds
            update(deltaTime / 1000.0);

            render();

            // Increment frame counter
            frameCount++;

            // Check if timer interval has elapsed
            elapsedTime = currentTime - timer;
            if (elapsedTime >= 1000) {
                // Calculate FPS
                fps = frameCount * 1000 / elapsedTime;

                // Reset timer and frame counter
                timer = currentTime;
                frameCount = 0;
            }
        }
    }
}

Upvotes: 1

Views: 89

Answers (1)

Adrian McCarthy
Adrian McCarthy

Reputation: 48022

Note that Sleep is not a precise function. The clock rate may be quite coarse. If you ask it to sleep less than a full clock period, it might not sleep at all. If you ask it to sleep more than a clock period, the thread might sleep almost an additional clock period. Also note that once the time has elapsed and the thread is again ready to run, when it's actually run again is up to the scheduler.

My solution for game loops on Windows is to use Sleep only when I have many milliseconds to wait, and then I ask it to Sleep for slightly less than the full time. After that, I do a busy wait for the next frame time. Busy waits are generally bad, but it is a game loop, the spin time is limited, and it's better than changing the clock period for all processes.

Is your computed frame rate really 60 fps? Given the imprecision of sleeping and an unknown amount of time in update, I'd be surprised.

In the rendering code, there's an inefficiency in that you're blitting the bitmap twice: first from memDC1 to memDC, and then from memDC to dc. You seemed to try to justify that in the comments, but I couldn't quite follow your reasoning. Even if there's a good reason to do that in the long run, you might want to eliminate the extra step while debugging the current problem.

The clean-up of the GDI objects has a problem. Deleting memDC while memBM is still selected into it is technically an error. This may or may not be a factor in the problem you're seeing. Sometimes GDI seems to be more forgiving of these kinds of problems, but sometimes it can cause weird behavior.

If I recall correctly, the desktop window manager uses heuristics to figure out when GDI is "finished" with a frame so that it can update the texture for that window. For event-driven painting, this usually works fine. For rapid blitting, it's possible it's just not syncing up at the right moments. This could be because of the sequence problem in the clean-up or maybe the GDI commands are being batched. If fixing the sequence problem doesn't solve the problem, try calling GdiFlush at the end of your render call.

Upvotes: 0

Related Questions