Fix performance issues when moving the mouse with high polling rate on Windows
Co-authored-by: Pāvels Nadtočajevs <7645683+bruvzg@users.noreply.github.com> Co-authored-by: Jon Tamayo <volsungn@gmail.com> (cherry picked from commit 8caf6fd456ccdcc2cbfc3c8f2e601597911a1959)
This commit is contained in:
1 parent
dcabf2e4db
commit
364304d372
3 files changed
+266
-91
No files matched your search
@@ -81,6 +81,7 @@
|
||||
#include <shlwapi.h>
|
||||
#include <shobjidl.h>
|
||||
#include <wbemcli.h>
|
||||
#include <windns.h> // For QWORD.
|
||||
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||
@@ -287,6 +288,37 @@ void DisplayServerWindows::_set_mouse_mode_impl(DisplayServerEnums::MouseMode p_
|
||||
SetCapture(wd.hWnd);
|
||||
|
||||
_register_raw_input_devices(window_id);
|
||||
|
||||
const BitField<WinKeyModifierMask> &mods = _get_mods();
|
||||
|
||||
Ref<InputEventMouseMotion> mm;
|
||||
mm.instantiate();
|
||||
|
||||
mm->set_window_id(window_id);
|
||||
mm->set_pressure(windows[window_id].last_pressure);
|
||||
mm->set_ctrl_pressed(mods.has_flag(WinKeyModifierMask::CTRL));
|
||||
mm->set_shift_pressed(mods.has_flag(WinKeyModifierMask::SHIFT));
|
||||
mm->set_alt_pressed(mods.has_flag(WinKeyModifierMask::ALT));
|
||||
mm->set_meta_pressed(mods.has_flag(WinKeyModifierMask::META));
|
||||
mm->set_button_mask(mouse_get_button_state());
|
||||
|
||||
mm->set_position(center);
|
||||
mm->set_global_position(center);
|
||||
mm->set_relative(Vector2(0, 0));
|
||||
mm->set_relative_screen_position(Vector2(0, 0));
|
||||
mm->set_velocity(Vector2(0, 0));
|
||||
mm->set_screen_velocity(Vector2(0, 0));
|
||||
|
||||
mm->set_global_position(center);
|
||||
mm->set_velocity(Vector2(0, 0));
|
||||
mm->set_screen_velocity(Vector2(0, 0));
|
||||
|
||||
if (windows[window_id].window_focused || window_get_active_popup() == window_id) {
|
||||
Input::get_singleton()->parse_input_event(mm);
|
||||
}
|
||||
|
||||
old_x = center.x;
|
||||
old_y = center.y;
|
||||
}
|
||||
} else {
|
||||
// Mouse is free to move around (not captured or confined).
|
||||
@@ -4221,6 +4253,174 @@ String DisplayServerWindows::keyboard_get_layout_name(int p_index) const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
Vector2 DisplayServerWindows::_get_raw_mouse_motion(const RAWINPUT &p_raw, DisplayServerEnums::WindowID p_window_id) {
|
||||
if (p_raw.data.mouse.usFlags == MOUSE_MOVE_RELATIVE) {
|
||||
return Vector2(p_raw.data.mouse.lLastX, p_raw.data.mouse.lLastY);
|
||||
} else if (p_raw.data.mouse.usFlags == MOUSE_MOVE_ABSOLUTE) {
|
||||
int n_screen_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
int n_screen_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
int n_screen_left = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||
int n_screen_top = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
|
||||
Vector2 abs_pos = Vector2(
|
||||
(double(p_raw.data.mouse.lLastX) - 65536.0 / (n_screen_width)) * n_screen_width / 65536.0 + n_screen_left,
|
||||
(double(p_raw.data.mouse.lLastY) - 65536.0 / (n_screen_height)) * n_screen_height / 65536.0 + n_screen_top);
|
||||
|
||||
POINT coords; // Client coords.
|
||||
coords.x = abs_pos.x;
|
||||
coords.y = abs_pos.y;
|
||||
|
||||
ScreenToClient(windows[p_window_id].hWnd, &coords);
|
||||
|
||||
Vector2 relative(coords.x - old_x, coords.y - old_y);
|
||||
old_x = coords.x;
|
||||
old_y = coords.y;
|
||||
return relative;
|
||||
}
|
||||
return Vector2();
|
||||
}
|
||||
|
||||
void DisplayServerWindows::_process_raw_mouse_motion(const Vector2 &p_relative, bool p_left_button_down, DisplayServerEnums::WindowID p_window_id) {
|
||||
if (p_relative == Vector2()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Ref<InputEventMouseMotion> mm;
|
||||
mm.instantiate();
|
||||
const BitField<WinKeyModifierMask> &mods = _get_mods();
|
||||
|
||||
mm->set_window_id(p_window_id);
|
||||
mm->set_ctrl_pressed(mods.has_flag(WinKeyModifierMask::CTRL));
|
||||
mm->set_shift_pressed(mods.has_flag(WinKeyModifierMask::SHIFT));
|
||||
mm->set_alt_pressed(mods.has_flag(WinKeyModifierMask::ALT));
|
||||
mm->set_meta_pressed(mods.has_flag(WinKeyModifierMask::META));
|
||||
|
||||
mm->set_pressure(p_left_button_down ? 1.0f : 0.0f);
|
||||
mm->set_button_mask(mouse_get_button_state());
|
||||
|
||||
Point2i c(windows[p_window_id].width / 2, windows[p_window_id].height / 2);
|
||||
|
||||
// Centering just so it works as before.
|
||||
POINT pos = { (int)c.x, (int)c.y };
|
||||
ClientToScreen(windows[p_window_id].hWnd, &pos);
|
||||
SetCursorPos(pos.x, pos.y);
|
||||
|
||||
mm->set_position(c);
|
||||
mm->set_global_position(c);
|
||||
mm->set_velocity(Vector2(0, 0));
|
||||
mm->set_screen_velocity(Vector2(0, 0));
|
||||
mm->set_relative(p_relative);
|
||||
mm->set_relative_screen_position(p_relative);
|
||||
|
||||
if (windows[p_window_id].window_focused || windows[p_window_id].is_popup) {
|
||||
Input::get_singleton()->parse_input_event(mm);
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayServerWindows::_process_raw_input_event(const RAWINPUT &p_raw, DisplayServerEnums::WindowID p_window_id) {
|
||||
if (p_raw.header.dwType == RIM_TYPEKEYBOARD) {
|
||||
if (p_raw.data.keyboard.VKey == VK_SHIFT) {
|
||||
// If multiple Shifts are held down at the same time,
|
||||
// Windows natively only sends a KEYUP for the last one to be released.
|
||||
// Handle all Shift "key up" events here for consistency.
|
||||
if (p_raw.data.keyboard.Flags & RI_KEY_BREAK) {
|
||||
ERR_FAIL_COND(key_event_pos >= KEY_EVENT_BUFFER_SIZE);
|
||||
const BitField<WinKeyModifierMask> &mods = _get_mods();
|
||||
|
||||
KeyEvent ke;
|
||||
ke.shift = false;
|
||||
ke.altgr = mods.has_flag(WinKeyModifierMask::ALT_GR);
|
||||
ke.alt = mods.has_flag(WinKeyModifierMask::ALT);
|
||||
ke.control = mods.has_flag(WinKeyModifierMask::CTRL);
|
||||
ke.meta = mods.has_flag(WinKeyModifierMask::META);
|
||||
ke.uMsg = WM_KEYUP;
|
||||
ke.window_id = p_window_id;
|
||||
|
||||
ke.wParam = VK_SHIFT;
|
||||
// data.keyboard.MakeCode -> 0x2A - left shift, 0x36 - right shift.
|
||||
// Bit 30 -> key was previously down, bit 31 -> key is being released.
|
||||
ke.lParam = p_raw.data.keyboard.MakeCode << 16 | 1 << 30 | 1 << 31;
|
||||
key_event_buffer[key_event_pos++] = ke;
|
||||
}
|
||||
}
|
||||
} else if (mouse_mode == DisplayServerEnums::MOUSE_MODE_CAPTURED && p_raw.header.dwType == RIM_TYPEMOUSE) {
|
||||
_process_raw_mouse_motion(
|
||||
_get_raw_mouse_motion(p_raw, p_window_id),
|
||||
p_raw.data.mouse.ulButtons & RI_MOUSE_LEFT_BUTTON_DOWN,
|
||||
p_window_id);
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayServerWindows::process_raw_input() {
|
||||
if (!use_raw_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the same window the mouse is captured for in _set_mouse_mode_impl(),
|
||||
// which is not necessarily the main window.
|
||||
DisplayServerEnums::WindowID window_id = _get_focused_window_or_popup();
|
||||
if (!windows.has(window_id)) {
|
||||
window_id = DisplayServerEnums::MAIN_WINDOW_ID;
|
||||
}
|
||||
|
||||
// Normal 8 kHz frames stay precise; only a larger-sized tail that originates
|
||||
// from a stall is coalesced.
|
||||
constexpr UINT MAX_RAW_MOUSE_EVENTS_PER_FRAME = 512;
|
||||
UINT raw_mouse_events = 0;
|
||||
Vector2 coalesced_raw_mouse_motion;
|
||||
bool coalesced_raw_mouse_left_button_down = false;
|
||||
const bool coalesce_all_raw_mouse_motion = Input::get_singleton()->is_using_accumulated_input();
|
||||
auto flush_coalesced_raw_mouse_motion = [&]() {
|
||||
_process_raw_mouse_motion(coalesced_raw_mouse_motion, coalesced_raw_mouse_left_button_down, window_id);
|
||||
coalesced_raw_mouse_motion = Vector2();
|
||||
coalesced_raw_mouse_left_button_down = false;
|
||||
};
|
||||
|
||||
// Read until the raw queue is empty. A small pass cap can never catch up
|
||||
// after a long stall reaches Windows' 10,000-message queue limit, but a
|
||||
// stall-sized captured mouse tail is coalesced below before it reaches the
|
||||
// rest of the engine.
|
||||
while (true) {
|
||||
UINT n_buffer = 0;
|
||||
// With a null buffer, this reports the byte size of the first pending
|
||||
// message (the minimum required buffer), not a message count.
|
||||
if (GetRawInputBuffer(nullptr, &n_buffer, sizeof(RAWINPUTHEADER)) != 0 || n_buffer == 0) {
|
||||
flush_coalesced_raw_mouse_motion();
|
||||
return;
|
||||
}
|
||||
|
||||
UINT dw_size = n_buffer * sizeof(RAWINPUT);
|
||||
LPBYTE lpb = new BYTE[dw_size];
|
||||
|
||||
// NOTE: Use dw_size, not n_buffer.
|
||||
UINT n_read = GetRawInputBuffer((PRAWINPUT)lpb, &dw_size, sizeof(RAWINPUTHEADER));
|
||||
if (n_read == (UINT)-1 || n_read == 0) {
|
||||
delete[] lpb;
|
||||
flush_coalesced_raw_mouse_motion();
|
||||
return;
|
||||
}
|
||||
|
||||
PRAWINPUT raw = (PRAWINPUT)lpb;
|
||||
for (UINT i = 0; i < n_read; ++i) {
|
||||
if (mouse_mode == DisplayServerEnums::MOUSE_MODE_CAPTURED &&
|
||||
raw->header.dwType == RIM_TYPEMOUSE &&
|
||||
(coalesce_all_raw_mouse_motion || raw_mouse_events >= MAX_RAW_MOUSE_EVENTS_PER_FRAME)) {
|
||||
coalesced_raw_mouse_motion += _get_raw_mouse_motion(*raw, window_id);
|
||||
coalesced_raw_mouse_left_button_down = coalesced_raw_mouse_left_button_down ||
|
||||
(raw->data.mouse.ulButtons & RI_MOUSE_LEFT_BUTTON_DOWN);
|
||||
} else {
|
||||
_process_raw_input_event(*raw, window_id);
|
||||
}
|
||||
if (raw->header.dwType == RIM_TYPEMOUSE) {
|
||||
raw_mouse_events++;
|
||||
}
|
||||
// Move to next RAWINPUT in buffer.
|
||||
raw = NEXTRAWINPUTBLOCK(raw);
|
||||
}
|
||||
delete[] lpb;
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayServerWindows::process_events() {
|
||||
ERR_FAIL_COND(!Thread::is_main_thread());
|
||||
|
||||
@@ -4233,8 +4433,51 @@ void DisplayServerWindows::process_events() {
|
||||
}
|
||||
|
||||
_THREAD_SAFE_LOCK_
|
||||
|
||||
process_raw_input();
|
||||
|
||||
// The pump throttles only what the hardware can flood, and drains the rest.
|
||||
// See <https://ph3at.github.io/posts/Windows-Input/> for more information.
|
||||
//
|
||||
// - WM_INPUT is never dispatched from here: pulling raw input through
|
||||
// PeekMessage() one message at a time is what collapses the pump at high
|
||||
// polling rates (2,000 Hz and above). It stays in the queue for the next
|
||||
// buffered read in process_raw_input().
|
||||
// - WM_MOUSEMOVE and WM_NCMOUSEMOVE are synthesized on demand from the
|
||||
// hardware input stream, so while the mouse is moving, the queue never
|
||||
// reads empty and an unbounded pump spins until the mouse stops. They are
|
||||
// dispatched at most once per frame each. No motion is lost: they carry
|
||||
// the latest (coalesced) cursor position, and the relative motion derived
|
||||
// from successive positions preserves the total delta. Captured mouse
|
||||
// mode gets its sub-frame motion from raw input.
|
||||
// - Everything else (keys, characters, mouse buttons, wheel, system
|
||||
// messages) is discrete and low-rate, and is drained fully. Capping these
|
||||
// would split the WM_CHAR posted by TranslateMessage() from its
|
||||
// WM_KEYDOWN across frames, which breaks the pairing logic in
|
||||
// _process_key_events() and makes keys fire twice.
|
||||
MSG msg = {};
|
||||
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
auto peek_discrete = [&] {
|
||||
BOOL ret = PeekMessageW(&msg, nullptr, 0, WM_NCMOUSEMOVE - 1, PM_REMOVE);
|
||||
if (!ret) {
|
||||
ret = PeekMessageW(&msg, nullptr, WM_NCMOUSEMOVE + 1, WM_INPUT - 1, PM_REMOVE);
|
||||
}
|
||||
if (!ret) {
|
||||
ret = PeekMessageW(&msg, nullptr, WM_INPUT + 1, WM_MOUSEMOVE - 1, PM_REMOVE);
|
||||
}
|
||||
if (!ret) {
|
||||
ret = PeekMessageW(&msg, nullptr, WM_MOUSEMOVE + 1, std::numeric_limits<UINT>::max(), PM_REMOVE);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
while (peek_discrete()) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
if (PeekMessageW(&msg, nullptr, WM_MOUSEMOVE, WM_MOUSEMOVE, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
if (PeekMessageW(&msg, nullptr, WM_NCMOUSEMOVE, WM_NCMOUSEMOVE, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
@@ -5629,99 +5872,20 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
|
||||
break;
|
||||
}
|
||||
|
||||
// The bulk of raw input is drained in batch by process_raw_input()
|
||||
// before the message pump runs. This only sees WM_INPUT messages the
|
||||
// pump picks up afterwards (it peeks the full message range whenever
|
||||
// the app is not focused, and raw input can also arrive while the pump
|
||||
// itself is running). Without this fallback, DefWindowProc() would
|
||||
// free their data unread.
|
||||
UINT dwSize;
|
||||
if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, nullptr, &dwSize, sizeof(RAWINPUTHEADER)) != 0 || dwSize == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, nullptr, &dwSize, sizeof(RAWINPUTHEADER));
|
||||
LPBYTE lpb = new BYTE[dwSize];
|
||||
if (lpb == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, lpb, &dwSize, sizeof(RAWINPUTHEADER)) != dwSize) {
|
||||
OutputDebugString(TEXT("GetRawInputData does not return correct size !\n"));
|
||||
}
|
||||
|
||||
RAWINPUT *raw = (RAWINPUT *)lpb;
|
||||
|
||||
const BitField<WinKeyModifierMask> &mods = _get_mods();
|
||||
if (raw->header.dwType == RIM_TYPEKEYBOARD) {
|
||||
if (raw->data.keyboard.VKey == VK_SHIFT) {
|
||||
// If multiple Shifts are held down at the same time,
|
||||
// Windows natively only sends a KEYUP for the last one to be released.
|
||||
// Handle all shift key up here for consistency
|
||||
if (raw->data.keyboard.Flags & RI_KEY_BREAK) {
|
||||
ERR_BREAK(key_event_pos >= KEY_EVENT_BUFFER_SIZE);
|
||||
|
||||
KeyEvent ke;
|
||||
ke.shift = false;
|
||||
ke.altgr = mods.has_flag(WinKeyModifierMask::ALT_GR);
|
||||
ke.alt = mods.has_flag(WinKeyModifierMask::ALT);
|
||||
ke.control = mods.has_flag(WinKeyModifierMask::CTRL);
|
||||
ke.meta = mods.has_flag(WinKeyModifierMask::META);
|
||||
ke.uMsg = WM_KEYUP;
|
||||
ke.window_id = window_id;
|
||||
|
||||
ke.wParam = VK_SHIFT;
|
||||
// data.keyboard.MakeCode -> 0x2A - left shift, 0x36 - right shift.
|
||||
// Bit 30 -> key was previously down, bit 31 -> key is being released.
|
||||
ke.lParam = raw->data.keyboard.MakeCode << 16 | 1 << 30 | 1 << 31;
|
||||
key_event_buffer[key_event_pos++] = ke;
|
||||
}
|
||||
}
|
||||
} else if (mouse_mode == DisplayServerEnums::MOUSE_MODE_CAPTURED && raw->header.dwType == RIM_TYPEMOUSE) {
|
||||
Ref<InputEventMouseMotion> mm;
|
||||
mm.instantiate();
|
||||
|
||||
mm->set_window_id(window_id);
|
||||
mm->set_ctrl_pressed(mods.has_flag(WinKeyModifierMask::CTRL));
|
||||
mm->set_shift_pressed(mods.has_flag(WinKeyModifierMask::SHIFT));
|
||||
mm->set_alt_pressed(mods.has_flag(WinKeyModifierMask::ALT));
|
||||
mm->set_meta_pressed(mods.has_flag(WinKeyModifierMask::META));
|
||||
|
||||
mm->set_pressure((raw->data.mouse.ulButtons & RI_MOUSE_LEFT_BUTTON_DOWN) ? 1.0f : 0.0f);
|
||||
|
||||
mm->set_button_mask(mouse_get_button_state());
|
||||
|
||||
Point2i c(windows[window_id].width / 2, windows[window_id].height / 2);
|
||||
|
||||
// Centering just so it works as before.
|
||||
POINT pos = { (int)c.x, (int)c.y };
|
||||
ClientToScreen(windows[window_id].hWnd, &pos);
|
||||
SetCursorPos(pos.x, pos.y);
|
||||
|
||||
mm->set_position(c);
|
||||
mm->set_global_position(c);
|
||||
mm->set_velocity(Vector2(0, 0));
|
||||
mm->set_screen_velocity(Vector2(0, 0));
|
||||
|
||||
if (raw->data.mouse.usFlags == MOUSE_MOVE_RELATIVE) {
|
||||
mm->set_relative(Vector2(raw->data.mouse.lLastX, raw->data.mouse.lLastY));
|
||||
|
||||
} else if (raw->data.mouse.usFlags == MOUSE_MOVE_ABSOLUTE) {
|
||||
int nScreenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
int nScreenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
int nScreenLeft = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||
int nScreenTop = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
|
||||
Vector2 abs_pos(
|
||||
(double(raw->data.mouse.lLastX) - 65536.0 / (nScreenWidth)) * nScreenWidth / 65536.0 + nScreenLeft,
|
||||
(double(raw->data.mouse.lLastY) - 65536.0 / (nScreenHeight)) * nScreenHeight / 65536.0 + nScreenTop);
|
||||
|
||||
POINT coords; // Client coords.
|
||||
coords.x = abs_pos.x;
|
||||
coords.y = abs_pos.y;
|
||||
|
||||
ScreenToClient(hWnd, &coords);
|
||||
|
||||
mm->set_relative(Vector2(coords.x - old_x, coords.y - old_y));
|
||||
old_x = coords.x;
|
||||
old_y = coords.y;
|
||||
}
|
||||
mm->set_relative_screen_position(mm->get_relative());
|
||||
|
||||
if ((windows[window_id].window_focused || windows[window_id].is_popup) && mm->get_relative() != Vector2()) {
|
||||
Input::get_singleton()->parse_input_event(mm);
|
||||
}
|
||||
if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, lpb, &dwSize, sizeof(RAWINPUTHEADER)) == dwSize) {
|
||||
_process_raw_input_event(*(RAWINPUT *)lpb, window_id);
|
||||
}
|
||||
delete[] lpb;
|
||||
} break;
|
||||
|
||||
@@ -551,6 +551,10 @@ class DisplayServerWindows : public DisplayServer {
|
||||
HWND _find_window_from_process_id(ProcessID p_pid, HWND p_current_hwnd);
|
||||
|
||||
void initialize_tts() const;
|
||||
void process_raw_input();
|
||||
Vector2 _get_raw_mouse_motion(const RAWINPUT &p_raw, DisplayServerEnums::WindowID p_window_id);
|
||||
void _process_raw_mouse_motion(const Vector2 &p_relative, bool p_left_button_down, DisplayServerEnums::WindowID p_window_id);
|
||||
void _process_raw_input_event(const RAWINPUT &p_raw, DisplayServerEnums::WindowID p_window_id);
|
||||
|
||||
struct ScreenHdrData {
|
||||
bool hdr_supported = false;
|
||||
|
||||
@@ -68,6 +68,13 @@ char *wc_to_utf8(const wchar_t *wc) {
|
||||
int widechar_main(int argc, wchar_t **argv) {
|
||||
godot_init_profiler();
|
||||
|
||||
// Prevent Windows from replacing the window with a "ghost" when the main
|
||||
// thread briefly stalls (e.g. during editor startup or focus transitions).
|
||||
// The ghost window intercepts click-backs, so the editor never receives
|
||||
// WM_ACTIVATEAPP and gets permanently stuck at the unfocused throttle.
|
||||
// Must be called before any window is created.
|
||||
DisableProcessWindowsGhosting();
|
||||
|
||||
OS_Windows os(nullptr);
|
||||
|
||||
setlocale(LC_CTYPE, "");
|
||||
|
||||
Reference in new issue
Block a user