Reputation: 63816
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
Reputation: 19
I faced this issue in the past in a win32 app and again with WPF.
The solution i came up with is this:
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
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
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:
WM_MOUSEMOVE
save the cursor position in a member variable and set a flag meaning that the mouse has moved.OnIdle()
handler that checks the mouse moved flag. If not moved, do nothing. If moved, then do the expensive calculation.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