FS#6629 - VIdeo scroll with mouse right click

Attached to Project: OpenTTD
Opened by Franzerfaust (Franzerfaust) - Friday, 20 October 2017, 10:52 GMT
Last edited by Michael Lutz (michi_cc) - Saturday, 09 December 2017, 19:22 GMT
Type Bug
Category Interface
Status Closed
Assigned To No-one
Operating System Windows
Severity High
Priority Normal
Reported Version 1.7.1
Due in Version Undecided
Due Date Undecided
Percent Complete 100%
Votes 0
Private No


after the installation on Fall Creator Update for Windows 10 Pro 64bit the quick video scroll with mouse right click doesn't work.
The map "trembles" trying to move in other direction but it doesn't move.
Workaround: set movement when arrows touch the borders in setting menu, not the best but usefull.
This task depends upon

Closed by  Michael Lutz (michi_cc)
Saturday, 09 December 2017, 19:22 GMT
Reason for closing:  Fixed
Additional comments about closing:  In r27935.
Comment by Coragem (ST2) - Sunday, 22 October 2017, 23:30 GMT
won't add much, only a couple of links, probably known, of talks of people with same problem since W10 Fall Creator Update: (couple workarounds that, probably, will break other stuff) (removed from the frontpage - don't ask me why :S)
Comment by frosch (frosch) - Tuesday, 24 October 2017, 14:43 GMT
Apparently W10 has a (new) method to retrieve relative mouse movement:

Well, at least it seems to be the first time in windows history that official docs talk about relative movement.
Comment by frosch (frosch) - Tuesday, 07 November 2017, 23:06 GMT
This post
contains useful logs what is happening.
* 1703: When the mouse is moved, one or multiple movement commands are received. Then a distinct movement event for the SetCursorPos is received.
* 1709: There is no distinct event for SetCursorPos. At some "random" position in the event queue the positions refer to the new mouse position, but they already include new relative movement.

Possible solutions:
* Try other methods instead of SetCursorPos. Internet suggests stuff like
Input.type = INPUT_MOUSE;
Input.mi.dx = nX;
Input.mi.dy = nY;
Input.mi.time = GetTickCount();
Input.mi.dwExtraInfo = GetMessageExtraInfo();
SendInput(1, &Input, sizeof(INPUT));
* Try to detect the execution of SetCursorPos from a sudden jump in the movement direction.
** This would probably involve some strategy to not move the mouse back immediately, but only after it traveled a few 100 pixels, or when the right mouse button was released. Then the SetCursorPos could be detected via a sudden jump of several 100 pixels, while normal movement would only involve few pixels.
** Probably complicated, so the first method should be tried first.

Anyway, I do not have access to any W10 machine, so I can't try myself.
Comment by Lrd (Lrd) - Thursday, 09 November 2017, 21:28 GMT
Thank you for looking into my logs, frosch! I think I better understand what's going on now.

The MSDN specification of the WM_MOUSEMOVE message [0] is very spartan: "Posted to a window when the cursor moves." In particular, it does not make any promises about _when_ a window gets a WM_MOUSEMOVE, so in particular it does not promise that a SetCursorPos will be followed by a WM_MOUSEMOVE for that particular position. This just happened to be the case on previous versions of Windows most of the time. Raymond Chen explains [1] that WM_MOUSEMOVE in fact is generated on-demand exactly when GetMessage or PeekMessage (which OpenTTD uses) is called and the mouse has moved since the last call. This supports that we should not be making assumptions about getting WM_MOUSEMOVEs for particular coordinates.

For testing, I added debug code to the WM_MOUSEMOVE handling in OpenTTD that also prints the coordinates that we "missed" (before calling CursorVars::UpdateCursorPosition), using GetMouseMovePointsEx [2] as described in [3]. The resulting logs for Windows 10 1703 and 1709 are attached. The "missing points" line is ordered oldest-to-newest; the last entry should always be equal to the coordinate of the current WM_MOUSEMOVE message. I am not fully sure what happened in the cases where the "missing points" list is empty, but I think we can ignore those for the moment.

I think what we see in those logs is:
- On 1703, a SetCursorPos works (it resets the cursor position) _and_ leads to an immediate new WM_MOUSEMOVE message (maybe it is actively inserted into the message queue and not generated on-demand as described by Raymond?). We still miss some movements that occurred during our processing of the last WM_MOUSEMOVE message up to the call to SetCursorPos.
- On 1709, SetCursorPos works (it resets the cursor position) but does _not_ lead to a new WM_MOUSEMOVE message (maybe the cursor movement due to SetCursorPos is now treated like any other cursor movement?). We thus miss the SetCursorPos movement when other movements happen between SetCursorPos and the next PeekMessage.

One caveat: We don't see missed movements during WM_MOUSEMOVE processing on the 1709 machine, but it is also significantly faster than my 1703 one.

My summary: The current handling of right-click-and-drag in OpenTTD is based on wrong assumptions/implementation-specific behaviour observed in previous versions of Windows, but the implementation has changed in 1709 (while still being "according to spec", whatever little spec there is). I see two alternatives for OpenTTD:
(a) Accept that using the WM_MOUSEMOVE coordinates is only an approximation of what the user actually does with their mouse, and observe (from my logs) that every WM_MOUSEMOVE is in fact relative to the last SetCursorPos (there is no queue of mouse events in Windows according to Raymond!). Set the queued_warp parameter to UpdateCursorPosition back to false. (However, the queued_warp = true had been introduced for some reason as per the forum discussion; we'd need to be pretty sure that that reason does not apply / is not as relevant as fixing 1709.)
(b) Attempt to more precisely reproduce the cursor movements: On getting a WM_MOUSEMOVE, use not only its coordinates, but also "replay" the "missed points" obtained via GetMouseMovePointsEx. Accept that this will still "fail" if we get very far behind because GetMouseMovePointsEx only returns a maximum of 64 points.

Disclaimer: I haven't really worked on OpenTTD before and done very little "raw" Windows API programming, so someone else (frosch?) should look over this and check whether I indeed draw "correct" conclusions. I'd be happy to try and implement alternative (b) if this turns out to be the desired solution. I personally feel it is a lot of code to fix a perceived problem, and (a) is actually acceptable, more elegant, and does match the "spec" of SetCursorPos/WM_MOUSEMOVE when taking Raymond's explanation into account.

Comment by frosch (frosch) - Sunday, 12 November 2017, 19:24 GMT
Thanks for discovering those blogs.

To my experience there is some queueing of mouse events.
In earlier versions of OpenTTD the code assumed that SetCursorPos would be executed immediately and affects the next mouse event.
However, the observation then was this:
* You are scrolling.
* Suddenly a lag spike occurs and it takes half a second before OTTD processes the next event.
* During the lag spike you actually moved the mouse quite far, so OTTD gets mouse positions far from the starting position.
* (assuming there is queueing) you get multiple mouse events quite far from the starting position. The longer the lag spike, the more far away is the mouse, and the more events are queued.
* Result: When the game lags, scrolling jumps huge distance (relative movement distances get squared), and the player gets disoriented.

Anyway, for the problem at hand, I am considering this algorithm:
* Do not call SetCursorPos after every movement, but only when the mouse reaches the window border.
** Essentially divide the window in a 3x3 grid:
** Probably the ratio of row/col height/width should be something like 1/4 1/2 1/4
** Only call SetCursorPos when the mouse has left the middle area (5), and then call SetCursorPos to move it to the opposite edge of (5) compared to where it left.
* As a result, every SetCursorPos moves the mouse huge distances, never small distances.
* This is used in the mouse event stream to detect at which position the SetCursorPos happened. Normal relative movement is by the user. Huge jumps are from SetCursorPos.
* Special case: Exiting scroll mode, calls SetCursorPos for the original start position.