From 77fd288d079fd19f00cb9cda165c4633fa796c54 Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:37:24 -0400 Subject: [PATCH 1/2] Show the measured controller polling rate Appends the assigned controller's polling rate to the Assigned: line in Controller settings, and suggests hidusbf for a wired DualSense under 1000 Hz. SDL's reported rate is hard-coded by device type in SDL_hidapi_ps5.c (250 Hz over USB), so it reads 250 Hz even when hidusbf has the controller at 1000 Hz. The rate is measured instead from the device-clocked sensor_timestamp on accelerometer events, which SDL posts once per HID report. The accelerometer is only enabled while the Controller settings menu is open, and only switched off again if it was enabled here, since Wii Remote motion shares it. --- runtime/src/settings_overlay.cpp | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index 616d141d8..216c097f6 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #if defined(_WIN32) @@ -737,6 +738,52 @@ void DrawRumbleSettings() { } } +struct PollRate { + uint64_t startNs = 0; + uint32_t samples = 0; + int hz = 0; + bool ownsSensor = false; +}; +std::unordered_map g_pollRates; +bool g_pollRateWanted = false; +bool g_pollRateActive = false; + +// Accel reports arrive once per HID report; only disable sensors enabled here, as Wii Remote motion shares them. +void UpdatePollRateSensors() { + if (g_pollRateWanted == g_pollRateActive) return; + g_pollRateActive = g_pollRateWanted; + if (g_pollRateActive) { + for (uint32_t index = 0; index < PADCount(); ++index) { + SDL_Gamepad* pad = PADGetSDLGamepadForIndex(index); + if (pad && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL) && + !SDL_GamepadSensorEnabled(pad, SDL_SENSOR_ACCEL) && + SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_ACCEL, true)) { + g_pollRates[SDL_GetGamepadID(pad)].ownsSensor = true; + } + } + return; + } + for (const auto& [id, rate] : g_pollRates) { + if (SDL_Gamepad* pad = SDL_GetGamepadFromID(id); pad && rate.ownsSensor) { + SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_ACCEL, false); + } + } + g_pollRates.clear(); +} + +std::string PollRateLabel(uint32_t port) { + const s32 index = PADGetIndexForPort(port); + SDL_Gamepad* pad = index >= 0 ? PADGetSDLGamepadForIndex(static_cast(index)) : nullptr; + const auto it = pad ? g_pollRates.find(SDL_GetGamepadID(pad)) : g_pollRates.end(); + if (it == g_pollRates.end() || it->second.hz <= 0) return {}; + std::string label = std::to_string(it->second.hz) + " Hz"; + if (it->second.hz < 900 && SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5 && + SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRED) { + label += " - hidusbf can raise this to 1000 Hz"; + } + return label; +} + void DrawControllerSettings() { for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) { const std::string label = "Port " + std::to_string(port + 1); @@ -754,6 +801,11 @@ void DrawControllerSettings() { ImGui::Separator(); const char* currentName = PADGetName(selectedGamePort); ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None"); + g_pollRateWanted = true; + if (const std::string rate = PollRateLabel(selectedGamePort); !rate.empty()) { + ImGui::SameLine(); + ImGui::TextDisabled("%s", rate.c_str()); + } if (ImGui::MenuItem("Unassign controller")) { PADClearPort(selectedGamePort); g_configuredControllerIndices.fill(std::numeric_limits::min()); @@ -1420,6 +1472,19 @@ void HandleEvents(const AuroraEvent* events) noexcept { if (IsMouseActivity(ev->sdl)) { g_lastMouseActivity = Clock::now(); } + if (g_pollRateActive && ev->sdl.type == SDL_EVENT_GAMEPAD_SENSOR_UPDATE && + ev->sdl.gsensor.sensor == SDL_SENSOR_ACCEL) { + PollRate& rate = g_pollRates[ev->sdl.gsensor.which]; + const uint64_t ns = ev->sdl.gsensor.sensor_timestamp; + if (rate.samples++ == 0 || ns <= rate.startNs) { + rate.startNs = ns; + rate.samples = 1; + } else if (ns - rate.startNs >= 250'000'000) { + rate.hz = static_cast(std::lround((rate.samples - 1) * 1e9 / static_cast(ns - rate.startNs))); + rate.startNs = ns; + rate.samples = 1; + } + } } } @@ -1453,6 +1518,7 @@ void Draw() noexcept { // its "communications interrupted" prompt without polling pads). Same guest // thread as PADRead, so no concurrent access to the scanner's state. WiiRemoteInput::Poll(); + g_pollRateWanted = false; ApplyConfiguredMappings(); PersistDisplayModeIfChanged(); UpdateCursorAutoHide(); @@ -1464,6 +1530,7 @@ void Draw() noexcept { DrawExitPrompt(); controller_mapping_wizard::Draw(); ApplyInputBlockState(); + UpdatePollRateSensors(); DrawStartupScreen(); } From a247a84756e7ea9da95b5dca4b29be1e13ecbbfc Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:37:33 -0400 Subject: [PATCH 2/2] Rescan controllers while measuring, and leave Wii Remotes alone The sensor scan now runs every frame the Controller settings menu is open, so a controller connected or reconnected while it is open is picked up, and an enable that failed is retried on the next frame. Wii Remotes are no longer enabled here. WiiRemoteInput caches the instance it enabled, so if this code had enabled a remote's accelerometer first and then disabled it on close, the Wii path would never re-enable it. Its rate is still shown when WiiRemoteInput has already turned the sensor on. --- runtime/src/settings_overlay.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index 216c097f6..ac9f098b8 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -748,21 +748,23 @@ std::unordered_map g_pollRates; bool g_pollRateWanted = false; bool g_pollRateActive = false; -// Accel reports arrive once per HID report; only disable sensors enabled here, as Wii Remote motion shares them. +// Accel reports arrive once per HID report; Wii Remotes are left to WiiRemoteInput, which owns their sensors. void UpdatePollRateSensors() { - if (g_pollRateWanted == g_pollRateActive) return; - g_pollRateActive = g_pollRateWanted; - if (g_pollRateActive) { + if (g_pollRateWanted) { + g_pollRateActive = true; for (uint32_t index = 0; index < PADCount(); ++index) { SDL_Gamepad* pad = PADGetSDLGamepadForIndex(index); if (pad && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL) && !SDL_GamepadSensorEnabled(pad, SDL_SENSOR_ACCEL) && + WiiRemoteInput::KindForName(SDL_GetGamepadName(pad)) == WiiRemoteInput::Kind::NotWii && SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_ACCEL, true)) { g_pollRates[SDL_GetGamepadID(pad)].ownsSensor = true; } } return; } + if (!g_pollRateActive) return; + g_pollRateActive = false; for (const auto& [id, rate] : g_pollRates) { if (SDL_Gamepad* pad = SDL_GetGamepadFromID(id); pad && rate.ownsSensor) { SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_ACCEL, false);