winapieventsdesign-patterns

Combining Windows' GetMessage Event Loop with a UI Animation Loop for Low CPU Usage


I am developing an app on Windows using the WinAPI. This app has a UI built using my own framework. Some views of this UI are animated and need to be refreshed over time.

Prior to implementing animated views, I was simply doing the following:

while (GetMessage(&msg, NULL, 0, 0)) {
    // if (msg.message == WM_QUIT) {
    //     break;
    // }
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

I’ve been using GetMessage instead of PeekMessage primarily for CPU resource savings. Redrawing of the UI and its elements is done in the WndProc function. When I receive, say, a WM_MOUSEMOVE event, I update the UI and trigger a redraw (all GPU-based).

Now, with animated views, I need something like the following (pseudo-code):

for (;;) {
    for (animation in app->timeline) {
        animation->tick(current_time, ...);
    }
}

In this naive version, I have to iterate over all animations in the timeline to ensure they run, even if the timeline contains no active animations. This keeps the main thread running continuously, even when there’s no animation, which is not ideal. Consequently, I’d need to switch to using PeekMessage:

while (!done) {
    for (animation in app->timeline) {
        animation->tick(current_time, ...);
    }

    if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
        if (msg.message == WM_QUIT) {
            done = true;
        } else {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
}

If I want to keep using GetMessage, what’s a good way to handle UI animations?

I’m curious how others have approached this in the past and whether there are alternatives to the two methods I’m considering.

Edit:

A special kudos to the person who replied to that post with a lengthy, detailed explanation. I thought I’d provide an update on the approach I’ve chosen, which is similar to the Chromium approach.

The idea is to move the infinite loop into a DoRunLoop function (the function name isn't particularly important). Essentially, it’s a for (;;) loop. Inside this loop, we do three main things:

It’s a bit intricate, but it seems logical, flexible, and reasonably robust.


Solution

  • Generally, with a traditional WinAPI application, it's best to keep all of the UI on a single thread. Performing the animation in a separate thread would be tricky and prone to bugs.

    The approaches I've used are (1) a timer, (2) custom messages, (3) PeekMessage. The first two work well with class Petzold-era, GDI-based UIs that draw only in response to WM_PAINT messages. The third works with that style, as well, but it's also suited to OpenGL or DirectX GUIs.

    Timer

    When an animation begins, set a timer and perform the animation update on each WM_TIMER message. In a classic GUI, this usually means moving a window or invalidating a window (in which the actual screen update happens in response to a WM_PAINT). When the animation completes, kill the timer.

    By default, timers have a coarse resolution (like 10 to 16 ms), so your frame rate won't be very high. The timer interval you set is aspirational. The actual interval may be longer or even a tad shorter than what you requested. It may or may not be super consistent.

    It's not uncommon for applications to request 1 ms timer resolution. If that's done only for the duration of short, occasional animations, that can be fine. But it's a system-wide setting, and, unfortunately, it's common for to applications to leave it at the higher resolution when no longer need it. This increases power consumption, which is especially bad for people running on battery power. Also, Microsoft has been making changes in Windows to improve power consumption that could make that approach less reliable.

    Custom Messages

    You can post a custom message to your window when an animation is to begin. The handler for the message would update for the next frame of animation and (in the case of a classic UI) invalidate the window. The invalidation will (eventually) cause a WM_PAINT message to be dispatched at which point the window procedure will actually draw the first frame.

    The trick is to have the WM_PAINT handler check whether another animation frame is needed. If so, it reposts the custom message to continue the animation. (To avoid infinite recursion, it must post, not send.) If the animation is complete, it just returns.

    I've tried doing this using only window invalidation without a custom message, but WM_PAINT messages are synthetic and invalidation has subtleties so I've never gotten that to work reliably without posting a custom animation message.

    PeekMessage

    Video games typically use a game loop, which runs continuously rather than waiting for an event with something like GetMessage. In Windows, you can check for events using PeekMessage inside a game loop. Game loops sometimes run as fast as they can, but others strive for a particular frame rate and will throttle themselves if they exceed it.

    This also works for productivity apps that have occasional animations, but you don't want to generate a bunch of frames per second when nothing is happening. Inside the loop, you can test whether there's an animation in process. If there is, then you use PeekMessage and keep going. But when there are no animations running, you instead call GetMessage, which will block until there's a message to dispatch.

    I mostly prefer this approach nowadays.