Skip to content

fix(rage-input-five): fix mouse lag with high polling rate mice - #4302

Open
xalva98 wants to merge 3 commits into
citizenfx:masterfrom
xalva98:fix/high-polling-rate-mouse
Open

xalva98 wants to merge 3 commits into
citizenfx:masterfrom
xalva98:fix/high-polling-rate-mouse

Conversation

@xalva98

@xalva98 xalva98 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Goal of this PR

Fix mouse lag with high polling rate mice. At 1000 Hz the mouse is fine; at 4000/8000 Hz camera
and aim movement lag, and the frame rate drops sharply while the mouse moves.

How is this PR achieving the goal

The game reads raw mouse input one WM_INPUT message at a time with GetRawInputData, and pumps
messages until the queue is empty. At 8000 Hz a new report arrives every 125 us. Every pump
iteration (PeekMessage, DispatchMessage, GetRawInputData, DefWindowProc) takes the window
manager lock, which other processes reacting to the same mouse contend for. An ETW trace showed
the game's main thread blocking on that lock (WrResource) ~1400 times per 0.5 s while moving
the mouse, so an iteration takes longer than 125 us. The queue then never runs empty, and the
frame is stuck in the pump.

grcWindowProcedure now handles raw input in batches:

  1. On the first WM_INPUT, read the rest of the pending raw input with GetRawInputBuffer, which
    takes it off the queue.
  2. Merge consecutive relative mouse moves from the same device into one event (deltas summed).
    Button, wheel, absolute, keyboard and HID input is not merged and keeps its order.
  3. Pass the game one WM_INPUT per resulting event. An IAT hook on the game's GetRawInputData
    answers those calls from our copy; any other call goes to the real function.
  4. Once a batch has been handed to the game, an IAT hook on the game's PeekMessageW leaves raw
    input and mouse messages that arrive later in the same pump pass for the next frame. Other
    messages are still processed right away.

When WM_INPUT is not passed to the game (NUI cursor, console), the pending input is dropped in one
go, as each of those messages would have been dropped anyway.

The game still gets every report: the movement it reads adds up to exactly the raw input, and the
newest report it sees each frame is at most one pump pass old, so 8000 Hz keeps its latency
advantage over 1000 Hz.

This PR applies to the following area(s)

FiveM

Successfully tested on

Game builds: 3258 (FiveM)
Platforms: Windows

Tested in game with an 8000 Hz mouse, moving it hard. Measured with temporary counters, while
moving the mouse:

  • before: message pump 15-620 ms per frame, 2-47 fps
  • after: message pump 0.2-0.8 ms per frame, 150-280 fps, up to ~7400 mouse reports/s read

In both cases the summed movement the game read matched the raw input exactly.

Checklist

  • Code compiles and has been tested successfully.
  • Code is properly formatted and follows the existing style.
  • Commit message is clear and follows the project conventions.
  • This PR introduces no new compilation warnings.

Fixes #1082

Read pending raw input in one go with GetRawInputBuffer and hand the game
one merged WM_INPUT per message pump pass, so 4000/8000 Hz mice no longer
keep the pump busy for most of the frame.

Signed-off-by: alva98 <59375624+xalva98@users.noreply.github.com>
@github-actions github-actions Bot added triage Needs a preliminary assessment to determine the urgency and required action invalid Requires changes before it's considered valid and can be (re)triaged and removed triage Needs a preliminary assessment to determine the urgency and required action labels Oct 24, 2026
@xalva98

xalva98 commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author
fivem-mouse-lag.mp4

small video to demonstrate the issue; moving your mouse ingame with 8000hz input drops your fps in half, and on minimap from 300 to 20-30 sometimes

@OneEarJoe5

Copy link
Copy Markdown

Built this locally and can confirm it fixes it here too. 8000 Hz camera movement went from 42-54 fps to 360-420 (7800X3D, RX 9070 XT, Logitech Pro X Superstrike).

There was still a gap to 1000 Hz, which turned out to be the legacy mouse messages Windows generates for every report. I opened a small PR on your branch that sets RIDEV_NOLEGACY while the game owns the mouse. That gets 8000 Hz to ~490 fps vs ~530 at 1000 Hz: xalva98#1

…the mouse (#1)

With 4000/8000 Hz mice Windows generates WM_MOUSEMOVE/WM_SETCURSOR/WM_NCHITTEST
for every raw report, which made PeekMessage cost ~0.5 ms per frame even with
batched raw input reads. Register the mouse with RIDEV_NOLEGACY while the game
window is active and neither the console nor a NUI frame needs the cursor
(including SetNuiFocusKeepInput). Configurable via in_mouseNoLegacy.

Also reset the raw input pass flag every frame so a pump pass that ends early
can't hold back input in the next frame.

Signed-off-by: OneEarJoe5 <212275319+OneEarJoe5@users.noreply.github.com>
@OneEarJoe5

Copy link
Copy Markdown

The NOLEGACY part broke the "Windows Input" mouse method (no camera movement on a fresh start). Fixed in a follow-up on the branch, which also gets Windows Input to ~480 fps at 8000 Hz instead of 40-60: xalva98#2

* fix(rage-input-five): fix the "Windows Input" mouse method

NOLEGACY broke the "Windows Input" mouse method, the camera didn't react
until you changed the mouse method in the settings once. That mode gets its
moves and clicks from the legacy messages, so NOLEGACY is only used for raw
input now.

While testing that I noticed "Windows Input" still drops to 40-60 fps with
an 8000 Hz mouse, because the game handles one WM_MOUSEMOVE per report. For
the non-raw methods the pump pass now also ends after the first
WM_MOUSEMOVE. Buttons and the wheel are not held back anymore, only WM_INPUT
and WM_MOUSEMOVE.

Also removes the raw mouse registration the game leaves behind when it
starts with "Windows Input". With it every report came in twice, which made
the sensitivity feel off until you switched the method once.

Signed-off-by: OneEarJoe5 <212275319+OneEarJoe5@users.noreply.github.com>

* fix(rage-input-five): fix dragging the pause map with "Windows Input"

With "Windows Input" you couldn't drag or click on the pause menu map. The
capture count reset that runs every frame while the game cursor is visible
makes the game drop the held button in that mode.

Skip the reset for "Windows Input", the cursor stays centered there anyway.

Signed-off-by: OneEarJoe5 <212275319+OneEarJoe5@users.noreply.github.com>

---------

Signed-off-by: OneEarJoe5 <212275319+OneEarJoe5@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

invalid Requires changes before it's considered valid and can be (re)triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Call to windows API causing FPS issues

2 participants