Mr. Boy
Mr. Boy

Reputation: 63816

Moving the mouse blocks WM_TIMER and WM_PAINT

In an application I'm working on, in some cases the application runs pretty slowly and in these cases I've discovered that my moving the mouse, timer/paint messages are not being handled. If I move the mouse in a slow circle, I can indefinitely prevent the window being re-painted!

I found that this is expected behaviour:

With the exception of the WM_PAINT message, the WM_TIMER message, and the WM_QUIT message, the system always posts messages at the end of a message queue. This ensures that a window receives its input messages in the proper first in, first out (FIFO) sequence. The WM_PAINT message, the WM_TIMER message, and the WM_QUIT message, however, are kept in the queue and are forwarded to the window procedure only when the queue contains no other messages. In addition, multiple WM_PAINT messages for the same window are combined into a single WM_PAINT message, consolidating all invalid parts of the client area into a single area. Combining WM_PAINT messages reduces the number of times a window must redraw the contents of its client area.

However, what can I do about this? Sometimes in direct response to a mouse-move, I need to re-paint as quickly as possible.

I catch messages through a method like this on my CWnd-derived class:

virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);

Upvotes: 5

Views: 2883

Answers (3)

I faced this issue in the past in a win32 app and again with WPF.

The solution i came up with is this:

  • Paint / Invalidate requests are only stored (only set a needs_painting flag), never attended immediately
  • There is a timer in your graphics window that ticks every few MS (depending on desired max frame rate, for 30fps will be 33ms, 16ms for 60fps) and basically does nothing (it is to force frequent messages into the msg queue)
  • On the WndProc (window message processing method) first you check if paint is needed, then you paint. To check if paint is required i have this conditions: if there was a paint request (paint pending flag is true) and amount of ms since last paint is same or more than 1000/(your desired frame rate).
  • As well its a good practice to set some extra room for other things to happen, so its good practice to have Adaptative fps: decrease the FPS if time to paint is bigger than the fps: (paint time + 3-4 ms) < (1000 / desired fps)

This will have you app having quite consistent frame rates even if mouse moves like crazy.

For WM_TIMER you can do something similar (but im guessing that with the above your issue will be solved).

Edit:

I'm going to add some code to help clarify/understand the mechanism. Its from the last implementation i did of the mechanism in c# (wpf .net 8).

    private readonly D3DImage myD3DImage = new();
    private nint myColorSurf; // Direct3D color surface pointer
    private Document myDoc;

    private bool myRenderNeeded = false;
    private Stopwatch myLastRenderStartStopwatch = Stopwatch.StartNew();
    private Stopwatch myLastRenderEndStopwatch = Stopwatch.StartNew();

    private double myCurrFPS2ms = 1000.0 / 45.0;

    private Timer myRenderTimer = new Timer(OnRenderTimer, null, 0, myCurrFPS2ms);

    public void RequestRender()
    {
        myRenderNeeded = true;
        TryRender();
    }

    private void OnRendering(object? sender, EventArgs e)
    {
        myRenderNeeded = true;
        TryRender();
    }

    private void TryRender()
    {
        if (!myRenderNeeded)
            return;

        if (myLastRenderStartStopwatch?.Elapsed.TotalMilliseconds > myCurrFPS2ms && myLastRenderEndStopwatch?.Elapsed.TotalMilliseconds > 4.0) // 45 FPS
        {
            myLastRenderStartStopwatch.Restart();
            Stopwatch renderSW = Stopwatch.StartNew();
            Render();
            renderSW.Stop();
            if(renderSW.Elapsed.TotalMilliseconds > 2.0 * myCurrFPS2ms)
                myCurrFPS2ms = 1000.0 / 30.0;
            else if(renderSW.Elapsed.TotalMilliseconds < 0.5 * myCurrFPS2ms)
                myCurrFPS2ms = 1000.0 / 45.0;
            myLastRenderEndStopwatch.Restart();
            myRenderNeeded = false;
        }
    }

    private void Render()
    {
        if (!HasFailed
          && myD3DImage.IsFrontBufferAvailable
          && myColorSurf != nint.Zero
          && myD3DImage.PixelWidth != 0 && myD3DImage.PixelHeight != 0)
        {
            myD3DImage.Lock();
            {
                myDoc.GetOCDocument()?.GetView().RedrawView();
                myD3DImage.AddDirtyRect(new Int32Rect(0, 0, myD3DImage.PixelWidth, myD3DImage.PixelHeight));
            }
            myD3DImage.Unlock();
        }
    }

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        TryRender();

        return IntPtr.Zero;
    }

    private void OnRenderTimer(object? state)
    {
        try
        {
            Dispatcher?.Invoke(() => TryRender());
        }
        catch (Exception)
        {
        }
    }

    // NOTE: Stop the timer when window is going to close

Upvotes: -1

Bruce Dawson
Bruce Dawson

Reputation: 3502

The correct solution in almost all cases is that when you update your application state in response to WM_MOUSEMOVE in such a way that repainting is required you should call UpdateWindow(). That is it. You shouldn't use PeekMessage(), GetCursorPos, OnIdle(), etc.

If you call UpdateWindow() then a WM_PAINT message will be forced and your window will be updated, the user will see a reaction to their mouse movement, and your application will feel responsive. This is good. This is, in fact, ideal.

If you don't call UpdateWindow() then another mouse message may appear before a WM_PAINT message gets delivered. Most mice can deliver WM_MOUSEMOVE messages at 128/s, touch-screens seem to run at 200/s, and gaming mice are probably even faster. So, if the user is moving the mouse quickly then WM_PAINT messages may never get a chance to be delivered if not forced. This leaves you in a situation where you are processing 100+ WM_MOUSEMOVE messages per second, but never painting the window. This means you have an internal framerate of 100 fps, and an visible framerate of 0 fps. Broken.

So, once again, the answer is to call UpdateWindow() after an WM_MOUSEMOVE processing that requires painting. You internal framerate may be lower (since now you do more work per message) but your visible framerate will now match your internal framerate, and it is your visible framerate that matters.

The extra WM_MOUSEMOVE messages will be quietly discarded, as described at http://blogs.msdn.com/b/oldnewthing/archive/2003/10/01/55108.aspx

I have made this fix to many applications and the results have always been spectacular. This one-line fix frequently makes applications seem 10x more responsive.

Upvotes: 2

rodrigo
rodrigo

Reputation: 98496

The WM_PAINT and WM_TIMER, as you read in the docs, are different from other messages.

The practical difference is that they are lowest priority messages. That is, they will not be processed if there are any other messages in the queue.

That said, you are moving the mouse, so a lot of messages are being posted, but the frequency of these messages is usually quite low (a few dozens a second) so your program should be mostly idle, But it is not, probably because some of these messages is taking much more time than expected and it is choking the queue. You just have to detect which one and why.

Some of the mouse-related messages, out of my mind:

  • WM_MOUSEMOVE
  • WM_NCMOUSEMOVE
  • WM_SETCURSOR
  • WM_NCHITTEST

And from a search in the web:

  • WM_MOUSEOVER
  • WM_MOUSELEAVE
  • WM_NCMOUSEOVER
  • WM_NCMOUSELEAVE

Anyway, if you want to repaint immediately, without waiting for a WM_PAINT you should call UpdateWindow(). This function forces an immediate processing of WM_PAINT (if there is anything invalidated, of course) and blocks until it finishes, bypassing the low priority issue of that message.

UPDATE: According to your situation from your comments I think that your best solution may be something along these lines:

  1. In WM_MOUSEMOVE save the cursor position in a member variable and set a flag meaning that the mouse has moved.
  2. Implement an OnIdle() handler that checks the mouse moved flag. If not moved, do nothing. If moved, then do the expensive calculation.
  3. You may try it with or without calling UpdateWindow() from OnIdle() and see which one is better.

Yes, it will still feel choppy in slow computers, but since OnIdle() has even lower priority than WM_TIMER and WM_PAINT, these messages will not be relegated indefinitely. And more importantly, you will not be queuing multiple calls to the expensive function.

Upvotes: 4

Related Questions