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
This commit is contained in:
bicarus
2026-07-12 10:32:29 -07:00
committed by GitHub
parent 27077aa21f
commit d7c144646f
7 changed files with 176 additions and 160 deletions
+2 -2
View File
@@ -41,7 +41,7 @@ void games::rb::RBGame::attach() {
Game::attach(); Game::attach();
if (1000 < games::rb::TOUCH_POLL_RATE) { 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 // 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); 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); SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
} else { } else {
log_info("rb", "not changing touch poll rate (120Hz by default)"); log_info("rb", "not changing touch poll rate (~125Hz by default)");
} }
} }
-1
View File
@@ -7,7 +7,6 @@
namespace games::rb { namespace games::rb {
extern uint16_t TOUCH_SCALING; extern uint16_t TOUCH_SCALING;
extern uint8_t TOUCH_SIZE;
extern uint16_t TOUCH_POLL_RATE; extern uint16_t TOUCH_POLL_RATE;
class RBGame : public games::Game { class RBGame : public games::Game {
+12 -9
View File
@@ -14,7 +14,6 @@ static std::string WINDOW_TITLE = "REFLEC BEAT";
namespace games::rb { namespace games::rb {
uint16_t TOUCH_SCALING = 1000; uint16_t TOUCH_SCALING = 1000;
uint8_t TOUCH_SIZE = 1;
} }
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) { games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
@@ -50,6 +49,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
if (wnd != nullptr) { 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 // reset window process to make the game not crash
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc)); SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
@@ -106,8 +108,9 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
return 0; return 0;
} }
// get window // size against the game window cached in open() (not GetForegroundWindow), so
HWND window = GetForegroundWindow(); // touch stays correctly scaled and keeps working when the game loses focus
HWND window = this->game_hwnd;
if (window == nullptr) { if (window == nullptr) {
return 0; return 0;
} }
@@ -166,11 +169,12 @@ 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_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
const auto point_y = (int) (y - window_height / 76); const auto point_y = (int) (y - window_height / 76);
// center // model a finger as a 3x3 block of IR sensors around the touch point
grid_insert(data, point_x, point_y); // this gives better accuracy (than just 1x1) since the logic below
// can toggle anywhere from 1x1 to 2x2, and the game engine calculates
// surrounding points // the center point, which means by inserting up to 8 extra blocks
if (games::rb::TOUCH_SIZE == 3) { // 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); // west
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest 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 + offset_y); // southwest
@@ -180,7 +184,6 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
grid_insert(data, point_x, point_y - offset_y); // north grid_insert(data, point_x, point_y - offset_y); // north
grid_insert(data, point_x, point_y + offset_y); // south grid_insert(data, point_x, point_y + offset_y); // south
} }
}
// copy data to buffer // copy data to buffer
memcpy(lpBuffer, data, 20); memcpy(lpBuffer, data, 20);
+6
View File
@@ -12,6 +12,12 @@ namespace games::rb {
int window_width = 1080; int window_width = 1080;
int window_height = 1920; 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 // logging
bool log_fps = false; bool log_fps = false;
uint64_t log_time = 0; uint64_t log_time = 0;
+1 -6
View File
@@ -1323,12 +1323,7 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) { if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32(); games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
} }
if (options[launcher::Options::RBTouchSize].is_active()) { // -rbtouchsize is deprecated and ignored; touch emulation always uses the 3x3 model
const auto text = options[launcher::Options::RBTouchSize].value_text();
if (text == "3") {
games::rb::TOUCH_SIZE = 3;
}
}
if (options[launcher::Options::RBTouchPollRate].is_active()) { if (options[launcher::Options::RBTouchPollRate].is_active()) {
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32(); games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
} }
+8 -6
View File
@@ -2716,14 +2716,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// RBTouchSize // RBTouchSize
.title = "RB Touch Emulation Size", .title = "RB Touch Emulation Size (DEPRECATED - no longer has any effect)",
.name = "rbtouchsize", .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, .type = OptionType::Enum,
.hidden = true,
.game_name = "Reflec Beat", .game_name = "Reflec Beat",
.category = "Game Options", .category = "Game Options",
.elements = { .elements = {
{"1", "1x1"},
{"3", "3x3"}, {"3", "3x3"},
}, },
}, },
@@ -2731,9 +2732,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// RBTouchPollRate // RBTouchPollRate
.title = "RB Touch Emulation Poll Hz", .title = "RB Touch Emulation Poll Hz",
.name = "rbtouchhz", .name = "rbtouchhz",
.desc = "By default, the game polls for touch at 120Hz. " .desc = "By default, the game polls for touch at ~125Hz. "
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n" "This option overrides that rate; enter a number between 1 and 1000.\n\n"
"It should be noted that higher poll does not necessarily improve accuracy or performance.", "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, .type = OptionType::Integer,
.setting_name = "250", .setting_name = "250",
.game_name = "Reflec Beat", .game_name = "Reflec Beat",
+13 -2
View File
@@ -1764,6 +1764,16 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
// get HID data // get HID data
auto &data_hid = data->data.hid; auto &data_hid = data->data.hid;
// 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<BYTE *>(data_hid.bRawData)
+ (size_t) hid_report_index * data_hid.dwSizeHid;
// parse reports // parse reports
for (const auto &pair : device.hidInfo->button_usage_pages) { for (const auto &pair : device.hidInfo->button_usage_pages) {
const auto usage_page = pair.first.first; const auto usage_page = pair.first.first;
@@ -1779,7 +1789,7 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
usages.data(), usages.data(),
&usages_length, &usages_length,
reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()), reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()),
reinterpret_cast<PCHAR>(data_hid.bRawData), reinterpret_cast<PCHAR>(report_data),
data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS) { data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
// log_warning( // log_warning(
@@ -1856,7 +1866,7 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
value_caps.Range.UsageMin, value_caps.Range.UsageMin,
reinterpret_cast<ULONG *>(&value_raw), reinterpret_cast<ULONG *>(&value_raw),
reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()), reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()),
reinterpret_cast<CHAR *>(data_hid.bRawData), reinterpret_cast<CHAR *>(report_data),
data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS) data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS)
{ {
continue; continue;
@@ -1919,6 +1929,7 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
// touch screen // touch screen
rawinput::touch::update_input(&device); rawinput::touch::update_input(&device);
}
break; break;
} }