Handmade Hero»Forums»Game
Gaurav Gautam
98 posts
Day4: Why is `StretchDIBits` done twice and what triggers `WM_PAINT` ?

On day4 we have the animated weird gradient. To do this we have a backbuffer to which we write the pixels of the gradient using the RenderWeirdGradient function. And we put those pixels onto the screen using the StretchDIBits function by giving it a HDC.

However, this StretchDIBits function is called twice (through Win32UpdateWindow) for each iteration of the main loop we run. Once in the loop itself:

            while(Running)
            {
                // -----snip-----
                HDC DeviceContext = GetDC(Window);
                // ----snip------
                Win32UpdateWindow(DeviceContext, &ClientRect, 0, 0, WindowWidth, WindowHeight);
                // ----snip-----
            }

and once through the handler for WM_PAINT:

        case WM_PAINT:
        {
            HDC DeviceContext = BeginPaint(Window, &Paint);
            //-----snip----
            Win32UpdateWindow(DeviceContext, &ClientRect, X, Y, Width, Height);
            //-----snip----
        } break;

Since, it is getting called in the WM_PAINT I tried removing it from the mainloop. And this led to nothing getting painted on the screen because no WM_PAINT messages were coming unless I moved the window around.

So, then I tried removing the handler for WM_PAINT. And this got me the gradient back. So, it appears that just calling the StretchDIBits is somehow making windows send the WM_PAINT messsage and the only thing we need to do inside the WM_PAINT handler is call BeginPaint and EndPaint (I tried this and it works) or just leave it to the default handler.

So are we calling the StretchDIBits function in the mainloop just to trigger the WM_PAINT?

I also tried to trigger WM_PAINT by other means:

Method: 1

        UpdateWindow(Window);
        /*
        HDC DeviceContext = GetDC(Window);
        RECT ClientRect;
        GetClientRect(Window, &ClientRect);
        int WindowWidth = ClientRect.right - ClientRect.left;
        int WindowHeight = ClientRect.bottom - ClientRect.top;
        Win32UpdateWindow(DeviceContext, &ClientRect);
        ReleaseDC(Window, DeviceContext);
        */

Method: 2

        /*
        HDC DeviceContext = GetDC(Window);
        RECT ClientRect;
        GetClientRect(Window, &ClientRect);
        int WindowWidth = ClientRect.right - ClientRect.left;
        int WindowHeight = ClientRect.bottom - ClientRect.top;
        Win32UpdateWindow(DeviceContext, &ClientRect);
        ReleaseDC(Window, DeviceContext);
        */
        RECT ClientRect;
        GetClientRect(Window, &ClientRect);
        InvalidateRect(
            Window,
            &ClientRect,
            false
            );

I found that UpdateWindow does not do the job while InvalidateRect does it with the last parameter set to either true or false. So based on this, I feel we are doing a wasteful copy of the buffer into windows's memory? And why does the UpdateWindow not work? The docs say that UpdateWindow will send WM_PAINT to the window directly but my breakpoint on WM_PAINT never trips even though UpdateWindow is tripping with each iteration.

Simon Anciaux
1337 posts
Day4: Why is `StretchDIBits` done twice and what triggers `WM_PAINT` ?
Edited by Simon Anciaux on

The UpdateWindow function updates the client area of the specified window by sending a WM_PAINT message to the window if the window's update region is not empty. UpdateWindow on MSDN

If you do not want the application to wait until the application's message queue has no other messages, use the UpdateWindow function to force the WM_PAINT message to be sent immediately. If there is any invalid part of the client area, UpdateWindow sends the WM_PAINT message for the specified window directly to the window procedure. Invalidating the Client Area on MSDN

UpdateWindow will call the window procedure with a WM_PAINT message without going through GetMessage or PeekMessage, ONLY if there is an update region (you called InvalidateRect or InvalidateRng).

Since UpdateWindow doesn't go through the message queue, you need to have the drawing code in the WM_PAINT message if you want to use that function. I'm not very familiar with WM_PAINT but maybe there are other way to get those messages without going through the message queue which would be a reason to keep the drawing code there. In application that use D3D, OpenGL or other graphics API, I don't think you ever need to handle WM_PAINT messages.

There might be more information here https://learn.microsoft.com/en-us/windows/win32/gdi/painting-and-drawing

Mārtiņš Možeiko
2559 posts / 2 projects
Day4: Why is `StretchDIBits` done twice and what triggers `WM_PAINT` ?

The main reason to have drawing in WM_PAINT is because that is only code executed when you are moving window or when you are resizing it. During these operations your main loop does not run as message processing loop enters separate modal loop.

If you don't care about redrawing in such case, then there's no need to use WM_PAINT and just having drawing in main loop is enough.

But if you want to handle such drawing, then having draw code in WM_PAINT and main loop is fine to have. It does not mean it will be always called twice. Simply in normal game run the main loop will do the drawing. And only when window is moved/resized/obscured then WM_PAINT draw kicks-in. You can issue InvalidateRect to force WM_PAINT from main code. But meh.. it does not really matter.

Gaurav Gautam
98 posts
Day4: Why is `StretchDIBits` done twice and what triggers `WM_PAINT` ?
Replying to mrmixer (#26969)

I see, that explains why I wasn't getting the WM_PAINT message when I was calling just UpdateWindow

Gaurav Gautam
98 posts
Day4: Why is `StretchDIBits` done twice and what triggers `WM_PAINT` ?
Replying to mmozeiko (#26970)

Hmm I see yeah I don't know why I would want to paint when its moving...