From d7c144646f17025a14a12736aa01823c43f01335 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 12 Jul 2026 10:32:29 -0700 Subject: [PATCH] rawinput: deal with high poll rate devices, use 3x3 for default for rb touch emu (#796) ## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change Two changes: 1. Handle all HID events in `WM_INPUT` and not just the first one. This only really affects 4000Hz / 8000Hz HID devices. 2. Go back to `3x3` as the default for rb IR touch emulation and remove `1x1` as an option as it does not perform as well as I expected. ## Testing --- src/spice2x/games/rb/rb.cpp | 4 +- src/spice2x/games/rb/rb.h | 1 - src/spice2x/games/rb/touch.cpp | 37 +++-- src/spice2x/games/rb/touch.h | 6 + src/spice2x/launcher/launcher.cpp | 7 +- src/spice2x/launcher/options.cpp | 14 +- src/spice2x/rawinput/rawinput.cpp | 267 ++++++++++++++++-------------- 7 files changed, 176 insertions(+), 160 deletions(-) diff --git a/src/spice2x/games/rb/rb.cpp b/src/spice2x/games/rb/rb.cpp index 68f4434..2f9f67b 100644 --- a/src/spice2x/games/rb/rb.cpp +++ b/src/spice2x/games/rb/rb.cpp @@ -41,7 +41,7 @@ void games::rb::RBGame::attach() { Game::attach(); if (1000 < games::rb::TOUCH_POLL_RATE) { - log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000"); + log_fatal("rb", "-rbtouchhz is set too high; cannot exceed 1000"); } // init stuff @@ -60,7 +60,7 @@ void games::rb::RBGame::attach() { log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE); SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE); } else { - log_info("rb", "not changing touch poll rate (120Hz by default)"); + log_info("rb", "not changing touch poll rate (~125Hz by default)"); } } diff --git a/src/spice2x/games/rb/rb.h b/src/spice2x/games/rb/rb.h index 9066499..679af2b 100644 --- a/src/spice2x/games/rb/rb.h +++ b/src/spice2x/games/rb/rb.h @@ -7,7 +7,6 @@ namespace games::rb { extern uint16_t TOUCH_SCALING; - extern uint8_t TOUCH_SIZE; extern uint16_t TOUCH_POLL_RATE; class RBGame : public games::Game { diff --git a/src/spice2x/games/rb/touch.cpp b/src/spice2x/games/rb/touch.cpp index f14526c..f2d967a 100644 --- a/src/spice2x/games/rb/touch.cpp +++ b/src/spice2x/games/rb/touch.cpp @@ -14,7 +14,6 @@ static std::string WINDOW_TITLE = "REFLEC BEAT"; namespace games::rb { uint16_t TOUCH_SCALING = 1000; - uint8_t TOUCH_SIZE = 1; } games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) { @@ -50,6 +49,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) { if (wnd != nullptr) { + // cache the game window so read() can size against it regardless of focus + this->game_hwnd = wnd; + // reset window process to make the game not crash SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast(DefWindowProc)); @@ -106,8 +108,9 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO return 0; } - // get window - HWND window = GetForegroundWindow(); + // size against the game window cached in open() (not GetForegroundWindow), so + // touch stays correctly scaled and keeps working when the game loses focus + HWND window = this->game_hwnd; if (window == nullptr) { return 0; } @@ -166,20 +169,20 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0); const auto point_y = (int) (y - window_height / 76); - // center - grid_insert(data, point_x, point_y); - - // surrounding points - if (games::rb::TOUCH_SIZE == 3) { - grid_insert(data, point_x - offset_x, point_y); // west - grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest - grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest - grid_insert(data, point_x + offset_x, point_y); // east - grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast - grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast - grid_insert(data, point_x, point_y - offset_y); // north - grid_insert(data, point_x, point_y + offset_y); // south - } + // model a finger as a 3x3 block of IR sensors around the touch point + // this gives better accuracy (than just 1x1) since the logic below + // can toggle anywhere from 1x1 to 2x2, and the game engine calculates + // the center point, which means by inserting up to 8 extra blocks + // we are emulating a sub-"pixel" resolution + grid_insert(data, point_x, point_y); // center + grid_insert(data, point_x - offset_x, point_y); // west + grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest + grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest + grid_insert(data, point_x + offset_x, point_y); // east + grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast + grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast + grid_insert(data, point_x, point_y - offset_y); // north + grid_insert(data, point_x, point_y + offset_y); // south } // copy data to buffer diff --git a/src/spice2x/games/rb/touch.h b/src/spice2x/games/rb/touch.h index 83ef49a..e7463f4 100644 --- a/src/spice2x/games/rb/touch.h +++ b/src/spice2x/games/rb/touch.h @@ -12,6 +12,12 @@ namespace games::rb { int window_width = 1080; int window_height = 1920; + // game window resolved in open(); read() sizes against this instead of the + // foreground window so touch stays correctly scaled and keeps working when + // the game is not the foreground window (rawinput delivers touch regardless + // of focus) + HWND game_hwnd = nullptr; + // logging bool log_fps = false; uint64_t log_time = 0; diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 2cadb04..380474b 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -1323,12 +1323,7 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::spice2x_RBTouchScale].is_active()) { games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32(); } - if (options[launcher::Options::RBTouchSize].is_active()) { - const auto text = options[launcher::Options::RBTouchSize].value_text(); - if (text == "3") { - games::rb::TOUCH_SIZE = 3; - } - } + // -rbtouchsize is deprecated and ignored; touch emulation always uses the 3x3 model if (options[launcher::Options::RBTouchPollRate].is_active()) { games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32(); } diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 654cd15..823cf30 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -2716,14 +2716,15 @@ static const std::vector OPTION_DEFINITIONS = { }, { // RBTouchSize - .title = "RB Touch Emulation Size", + .title = "RB Touch Emulation Size (DEPRECATED - no longer has any effect)", .name = "rbtouchsize", - .desc = "Size of the touch area; how many IR sensors a single finger activates. Default: 1 (1x1).", + .desc = "This option is deprecated and no longer has any effect. " + "Reflec Beat touch emulation always uses the 3x3 sensor model.", .type = OptionType::Enum, + .hidden = true, .game_name = "Reflec Beat", .category = "Game Options", .elements = { - {"1", "1x1"}, {"3", "3x3"}, }, }, @@ -2731,9 +2732,10 @@ static const std::vector OPTION_DEFINITIONS = { // RBTouchPollRate .title = "RB Touch Emulation Poll Hz", .name = "rbtouchhz", - .desc = "By default, the game polls for touch at 120Hz. " - "This option overrides that rate; enter a number betwen 1 and 1000.\n\n" - "It should be noted that higher poll does not necessarily improve accuracy or performance.", + .desc = "By default, the game polls for touch at ~125Hz. " + "This option overrides that rate; enter a number between 1 and 1000.\n\n" + "Higher rates reduce input latency (the game sees a fresher touch position) " + "but do NOT improve spatial accuracy, and very high rates just waste CPU.", .type = OptionType::Integer, .setting_name = "250", .game_name = "Reflec Beat", diff --git a/src/spice2x/rawinput/rawinput.cpp b/src/spice2x/rawinput/rawinput.cpp index 8ece729..1eef49f 100644 --- a/src/spice2x/rawinput/rawinput.cpp +++ b/src/spice2x/rawinput/rawinput.cpp @@ -1764,162 +1764,173 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc( // get HID data auto &data_hid = data->data.hid; - // parse reports - for (const auto &pair : device.hidInfo->button_usage_pages) { - const auto usage_page = pair.first.first; - const auto link_collection = pair.first.second; - const auto button_count = pair.second; + // a single WM_INPUT may carry more than one HID report from the same + // device: bRawData holds dwCount reports of dwSizeHid bytes each (the + // buffer size is dwSizeHid * dwCount). parse every report instead of + // only the first one. + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawhid + const DWORD hid_report_count = data_hid.dwCount > 0 ? data_hid.dwCount : 1; + for (DWORD hid_report_index = 0; hid_report_index < hid_report_count; hid_report_index++) { + auto *report_data = reinterpret_cast(data_hid.bRawData) + + (size_t) hid_report_index * data_hid.dwSizeHid; - ULONG usages_length = button_count; - std::vector usages(static_cast(usages_length)); - if (HidP_GetUsages( - HidP_Input, - usage_page, - link_collection, - usages.data(), - &usages_length, - reinterpret_cast(device.hidInfo->preparsed_data.get()), - reinterpret_cast(data_hid.bRawData), - data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS) { + // parse reports + for (const auto &pair : device.hidInfo->button_usage_pages) { + const auto usage_page = pair.first.first; + const auto link_collection = pair.first.second; + const auto button_count = pair.second; - // log_warning( + ULONG usages_length = button_count; + std::vector usages(static_cast(usages_length)); + if (HidP_GetUsages( + HidP_Input, + usage_page, + link_collection, + usages.data(), + &usages_length, + reinterpret_cast(device.hidInfo->preparsed_data.get()), + reinterpret_cast(report_data), + data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS) { + + // log_warning( + // "rawinput", + // "failed to get usages for device {}, usage page {:x} and link collection {:x}", + // device.desc, + // usage_page, link_collection); + continue; + } + + // log_info( // "rawinput", - // "failed to get usages for device {}, usage page {:x} and link collection {:x}", + // "processing HID input for device {}, usage page {:x} and link collection {:x} with {} buttons, got {} reports", // device.desc, - // usage_page, link_collection); - continue; - } + // usage_page, link_collection, button_count, usages_length); - // log_info( - // "rawinput", - // "processing HID input for device {}, usage page {:x} and link collection {:x} with {} buttons, got {} reports", - // device.desc, - // usage_page, link_collection, button_count, usages_length); + // buttons + for (size_t cap_num = 0; cap_num < device.hidInfo->button_caps_list.size(); cap_num++) { + auto &button_caps = device.hidInfo->button_caps_list[cap_num]; + auto &button_states = device.hidInfo->button_states[cap_num]; + auto &button_down = device.hidInfo->button_down[cap_num]; + auto &button_up = device.hidInfo->button_up[cap_num]; - // buttons - for (size_t cap_num = 0; cap_num < device.hidInfo->button_caps_list.size(); cap_num++) { - auto &button_caps = device.hidInfo->button_caps_list[cap_num]; - auto &button_states = device.hidInfo->button_states[cap_num]; - auto &button_down = device.hidInfo->button_down[cap_num]; - auto &button_up = device.hidInfo->button_up[cap_num]; - - // is this the right usage page and link collection? - if (button_caps.UsagePage != usage_page || button_caps.LinkCollection != link_collection) { - continue; - } - - // get button count - int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1; - if (button_count <= 0) { - continue; - } - - // update buttons - std::vector new_states(button_count); - for (ULONG usage_num = 0; usage_num < usages_length; usage_num++) { - if (usages[usage_num] < button_caps.Range.UsageMin || - usages[usage_num] > button_caps.Range.UsageMax) { + // is this the right usage page and link collection? + if (button_caps.UsagePage != usage_page || button_caps.LinkCollection != link_collection) { continue; } - USAGE usage = usages[usage_num] - button_caps.Range.UsageMin; - - // guard against some buggy device sending an event for a usage below `UsageMin` - if (usage < button_count) { - new_states[usage] = true; + // get button count + int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1; + if (button_count <= 0) { + continue; } - } - for (int button_num = 0; button_num < button_count; button_num++) { - if (!new_states[button_num] && button_states[button_num]) { - device.updated = true; - button_states[button_num] = new_states[button_num]; - button_down[button_num] = input_time; - } else if (new_states[button_num] && !button_states[button_num]) { - device.updated = true; - button_states[button_num] = new_states[button_num]; - button_up[button_num] = input_time; + + // update buttons + std::vector new_states(button_count); + for (ULONG usage_num = 0; usage_num < usages_length; usage_num++) { + if (usages[usage_num] < button_caps.Range.UsageMin || + usages[usage_num] > button_caps.Range.UsageMax) { + continue; + } + + USAGE usage = usages[usage_num] - button_caps.Range.UsageMin; + + // guard against some buggy device sending an event for a usage below `UsageMin` + if (usage < button_count) { + new_states[usage] = true; + } + } + for (int button_num = 0; button_num < button_count; button_num++) { + if (!new_states[button_num] && button_states[button_num]) { + device.updated = true; + button_states[button_num] = new_states[button_num]; + button_down[button_num] = input_time; + } else if (new_states[button_num] && !button_states[button_num]) { + device.updated = true; + button_states[button_num] = new_states[button_num]; + button_up[button_num] = input_time; + } } } } - } - // analogs - for (auto cap_num = 0; cap_num < device.hidInfo->caps.NumberInputValueCaps; cap_num++) { - auto &value_caps = device.hidInfo->value_caps_list[cap_num]; + // analogs + for (auto cap_num = 0; cap_num < device.hidInfo->caps.NumberInputValueCaps; cap_num++) { + auto &value_caps = device.hidInfo->value_caps_list[cap_num]; - // get value - LONG value_raw = 0; - if (HidP_GetUsageValue( - HidP_Input, - value_caps.UsagePage, - value_caps.LinkCollection, - value_caps.Range.UsageMin, - reinterpret_cast(&value_raw), - reinterpret_cast(device.hidInfo->preparsed_data.get()), - reinterpret_cast(data_hid.bRawData), - data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS) - { - continue; - } + // get value + LONG value_raw = 0; + if (HidP_GetUsageValue( + HidP_Input, + value_caps.UsagePage, + value_caps.LinkCollection, + value_caps.Range.UsageMin, + reinterpret_cast(&value_raw), + reinterpret_cast(device.hidInfo->preparsed_data.get()), + reinterpret_cast(report_data), + data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS) + { + continue; + } - // get min and max - LONG value_min = value_caps.LogicalMin; - LONG value_max = value_caps.LogicalMax; + // get min and max + LONG value_min = value_caps.LogicalMin; + LONG value_max = value_caps.LogicalMax; - float value; - // 0x1 == generic desktop, 0x39 == hat switch - if (value_caps.UsagePage == 0x1 && value_caps.Range.UsageMin == 0x39) { - if (value_min <= value_raw && value_raw <= value_max) { - // scale to float; minimum valid value is UP, and increases in clockwise order - value = (float) (value_raw - value_min) / (float) (value_max - value_min); + float value; + // 0x1 == generic desktop, 0x39 == hat switch + if (value_caps.UsagePage == 0x1 && value_caps.Range.UsageMin == 0x39) { + if (value_min <= value_raw && value_raw <= value_max) { + // scale to float; minimum valid value is UP, and increases in clockwise order + value = (float) (value_raw - value_min) / (float) (value_max - value_min); + } else { + // hat switches report an out-of-bounds value to indicate a neutral position, so it + // needs special handling; here, we will use a negative value to indicate neutral + value = -1.f; + } } else { - // hat switches report an out-of-bounds value to indicate a neutral position, so it - // needs special handling; here, we will use a negative value to indicate neutral - value = -1.f; - } - } else { - // fix sign bits for signed values - if (value_caps.LogicalMin < 0 && - 0 < value_caps.BitSize && value_caps.BitSize < 32) { + // fix sign bits for signed values + if (value_caps.LogicalMin < 0 && + 0 < value_caps.BitSize && value_caps.BitSize < 32) { - ULONG raw = static_cast(value_raw) & ((1u << value_caps.BitSize) - 1u); - const ULONG sign_bit = 1u << (value_caps.BitSize - 1); - value_raw = static_cast((raw ^ sign_bit) - sign_bit); + ULONG raw = static_cast(value_raw) & ((1u << value_caps.BitSize) - 1u); + const ULONG sign_bit = 1u << (value_caps.BitSize - 1); + value_raw = static_cast((raw ^ sign_bit) - sign_bit); + } + + // automatic calibration + if (value_raw < value_min) { + value_caps.LogicalMin = value_raw; + value_min = value_raw; + } + if (value_raw > value_max) { + value_caps.LogicalMax = value_raw; + value_max = value_raw; + } + + // scale to float + value = (float) (value_raw - value_min) / (float) (value_max - value_min); } - // automatic calibration - if (value_raw < value_min) { - value_caps.LogicalMin = value_raw; - value_min = value_raw; - } - if (value_raw > value_max) { - value_caps.LogicalMax = value_raw; - value_max = value_raw; + // store value + auto &cur_state = device.hidInfo->value_states[cap_num]; + if (cur_state != value) { + device.updated = true; + cur_state = value; } - // scale to float - value = (float) (value_raw - value_min) / (float) (value_max - value_min); + // store raw value + auto &cur_raw_state = device.hidInfo->value_states_raw[cap_num]; + if (cur_raw_state != value_raw) { + device.updated = true; + cur_raw_state = value_raw; + } } - // store value - auto &cur_state = device.hidInfo->value_states[cap_num]; - if (cur_state != value) { - device.updated = true; - cur_state = value; - } - - // store raw value - auto &cur_raw_state = device.hidInfo->value_states_raw[cap_num]; - if (cur_raw_state != value_raw) { - device.updated = true; - cur_raw_state = value_raw; - } + // touch screen + rawinput::touch::update_input(&device); } - // touch screen - rawinput::touch::update_input(&device); - break; } default: