From 20d8bc6320aedc44d5108dbd9d9ed79b565663f3 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Sun, 8 Feb 2026 23:47:25 +0300 Subject: [PATCH] analogs working (and various cleanups) --- .../configure_hotkeys_controller.cpp | 6 + .../configuration/configure_input.cpp | 3 +- src/citra_qt/hotkey_monitor.cpp | 4 + src/citra_qt/hotkeys.cpp | 31 ++-- src/citra_qt/hotkeys.h | 9 +- .../controller_sequence_dialog.cpp | 5 +- src/input_common/sdl/sdl_impl.cpp | 149 ++++++++++++------ 7 files changed, 143 insertions(+), 64 deletions(-) diff --git a/src/citra_qt/configuration/configure_hotkeys_controller.cpp b/src/citra_qt/configuration/configure_hotkeys_controller.cpp index 6f45c24c0..f6ee15497 100644 --- a/src/citra_qt/configuration/configure_hotkeys_controller.cpp +++ b/src/citra_qt/configuration/configure_hotkeys_controller.cpp @@ -95,6 +95,7 @@ void ConfigureControllerHotkeys::ApplyConfiguration(HotkeyRegistry& registry) { if (action_name != action->text()) continue; hotkey.controller_keyseq = controller_keyseq->text(); + registry.UpdateControllerHotkey(action_name, hotkey); } } } @@ -147,6 +148,8 @@ QString ConfigureControllerHotkeys::CleanSequence(QString controller_keyseq) { output = QString::fromStdString("Hat " + p1.Get("hat", "") + " " + p1.Get("direction", "")); } else if (p1.Has("button")) { output = QString::fromStdString("Button " + p1.Get("button", "")); + } else if (p1.Has("axis")) { + output += QString::fromStdString("Axis " + p1.Get("axis", "") + p1.Get("direction", "")); } if (keys.length() > 1) { @@ -157,6 +160,9 @@ QString ConfigureControllerHotkeys::CleanSequence(QString controller_keyseq) { QString::fromStdString("Hat " + p1.Get("hat", "") + " " + p1.Get("direction", "")); } else if (p1.Has("button")) { output += QString::fromStdString("Button " + p1.Get("button", "")); + } else if (p1.Has("axis")) { + output += + QString::fromStdString("Axis " + p1.Get("axis", "") + p1.Get("direction", "")); } } return output; diff --git a/src/citra_qt/configuration/configure_input.cpp b/src/citra_qt/configuration/configure_input.cpp index 6cc2972e4..2cc319968 100644 --- a/src/citra_qt/configuration/configure_input.cpp +++ b/src/citra_qt/configuration/configure_input.cpp @@ -407,8 +407,7 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) Common::ParamPackage params; for (auto& poller : device_pollers) { params = poller->GetNextInput(); - // skip button downs and only process button ups to maintain former behavior - if (params.Has("engine") && !params.Has("down")) { + if (params.Has("engine")) { SetPollingResult(params, false); return; } diff --git a/src/citra_qt/hotkey_monitor.cpp b/src/citra_qt/hotkey_monitor.cpp index 454f1f3fb..f876358c4 100644 --- a/src/citra_qt/hotkey_monitor.cpp +++ b/src/citra_qt/hotkey_monitor.cpp @@ -45,6 +45,10 @@ void ControllerHotkeyMonitor::checkAllButtons() { if (it.hk->button_device2) { // two buttons, need both pressed and one *just now* pressed bool currentStatus2 = it.hk->button_device2->GetStatus(); + if (currentStatus) + std::cerr << "button one pressed" << std::endl; + if (currentStatus2) + std::cerr << "button two pressed" << std::endl; trigger = currentStatus && currentStatus2 && (!it.lastStatus || !it.lastStatus2); it.lastStatus = currentStatus; it.lastStatus2 = currentStatus2; diff --git a/src/citra_qt/hotkeys.cpp b/src/citra_qt/hotkeys.cpp index 6cce80e11..c8b8de3c6 100644 --- a/src/citra_qt/hotkeys.cpp +++ b/src/citra_qt/hotkeys.cpp @@ -24,7 +24,22 @@ void HotkeyRegistry::SaveHotkeys() { } } } - +void HotkeyRegistry::UpdateControllerHotkey(QString name, Hotkey& hk) { + if (hk.controller_keyseq.isEmpty()) { + buttonMonitor.removeButton(name); + } else { + QStringList paramList = hk.controller_keyseq.split(QStringLiteral("||")); + if (paramList.length() > 0) { + hk.button_device = + Input::CreateDevice(paramList.value(0).toStdString()); + if (paramList.length() > 1) { + hk.button_device2 = + Input::CreateDevice(paramList.value(1).toStdString()); + } + buttonMonitor.addButton(name, &hk); + } + } +} void HotkeyRegistry::LoadHotkeys() { // Make sure NOT to use a reference here because it would become invalid once we call // beginGroup() @@ -36,18 +51,8 @@ void HotkeyRegistry::LoadHotkeys() { hk.context = static_cast(shortcut.shortcut.context); hk.controller_keyseq = shortcut.shortcut.controller_keyseq; } - if (!hk.controller_keyseq.isEmpty()) { - QStringList paramList = hk.controller_keyseq.split(QStringLiteral("||")); - if (paramList.length() > 0) { - hk.button_device = - Input::CreateDevice(paramList.at(0).toStdString()); - if (paramList.length() > 1) { - hk.button_device2 = - Input::CreateDevice(paramList.at(1).toStdString()); - } - buttonMonitor.addButton(shortcut.name, &hk); - } - } + UpdateControllerHotkey(shortcut.name, hk); + for (auto const& [_, hotkey_shortcut] : hk.shortcuts) { if (hotkey_shortcut) { hotkey_shortcut->disconnect(); diff --git a/src/citra_qt/hotkeys.h b/src/citra_qt/hotkeys.h index ba465a34f..6f444be0b 100644 --- a/src/citra_qt/hotkeys.h +++ b/src/citra_qt/hotkeys.h @@ -52,6 +52,11 @@ public: */ void SaveHotkeys(); + /** + * Updates the button devices for a hotkey based on the controller_keyseq value + */ + void UpdateControllerHotkey(QString name, Hotkey& hk); + /** * Returns a QShortcut object whose activated() signal can be connected to other QObjects' * slots. @@ -59,8 +64,8 @@ public: * @param group General group this hotkey belongs to (e.g. "Main Window", "Debugger"). * @param action Name of the action (e.g. "Start Emulation", "Load Image"). * @param widget Parent widget of the returned QShortcut. - * @warning If multiple QWidgets' call this function for the same action, the returned QShortcut - * will be the same. Thus, you shouldn't rely on the caller really being the + * @warning If multiple QWidgets' call this function for the same action, the returned + * QShortcut will be the same. Thus, you shouldn't rely on the caller really being the * QShortcut's parent. */ QShortcut* GetHotkey(const QString& group, const QString& action, QObject* widget); diff --git a/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp b/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp index d070599d6..247c7d790 100644 --- a/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp +++ b/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp @@ -27,7 +27,7 @@ ControllerSequenceDialog::ControllerSequenceDialog(QWidget* parent) connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); - LaunchPollers(); // Fixed: added semicolon + LaunchPollers(); } ControllerSequenceDialog::~ControllerSequenceDialog() = default; @@ -55,8 +55,7 @@ void ControllerSequenceDialog::LaunchPollers() { Common::ParamPackage params; for (auto& poller : device_pollers) { params = poller->GetNextInput(); - if (params.Has("engine") && - (params.Has("hat") || params.Has("button"))) { // for now, no analog inputs + if (params.Has("engine")) { std::cerr << "controller hotkey event detected: " + params.Serialize() << std::endl; if (params.Has("down")) { downCount++; diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp index 729beef23..3150cfccd 100644 --- a/src/input_common/sdl/sdl_impl.cpp +++ b/src/input_common/sdl/sdl_impl.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -187,6 +188,7 @@ public: state.buttons[button] = value; } + // TODO: remove if all goes well // no longer used - creating state breaks hotkey detection, so // poll sdl directly below bool GetButton(int button) const { @@ -207,11 +209,18 @@ public: state.axes[axis] = value; } + // TODO: remove if all goes well float GetAxis(int axis) const { std::lock_guard lock{mutex}; return state.axes.at(axis) / 32767.0f; } + float GetAxisDirect(int axis) const { + if (!sdl_joystick) + return 0.0; + return SDL_JoystickGetAxis(sdl_joystick.get(), axis) / 32767.0f; + } + std::tuple GetAnalog(int axis_x, int axis_y) const { float x = GetAxis(axis_x); float y = GetAxis(axis_y); @@ -235,6 +244,7 @@ public: } // no longer used, poll directly instead + // TODO: remove if all goes well bool GetHatDirection(int hat, Uint8 direction) const { std::lock_guard lock{mutex}; return (state.hats.at(hat) & direction) != 0; @@ -661,7 +671,7 @@ public: trigger_if_greater(trigger_if_greater_) {} bool GetStatus() const override { - float axis_value = joystick->GetAxis(axis); + float axis_value = joystick->GetAxisDirect(axis); if (trigger_if_greater) return axis_value > threshold; return axis_value < threshold; @@ -968,6 +978,7 @@ public: void Start() override { state.event_queue.Clear(); + state.polling = true; } @@ -985,56 +996,92 @@ public: Common::ParamPackage GetNextInput() override { SDL_Event event; - bool down = false; while (state.event_queue.Pop(event)) { + auto axis = event.jaxis.axis; + auto id = event.jaxis.which; + auto value = event.jaxis.value; switch (event.type) { case SDL_JOYAXISMOTION: - if (!axis_memory.count(event.jaxis.which) || - !axis_memory[event.jaxis.which].count(event.jaxis.axis)) { - axis_memory[event.jaxis.which][event.jaxis.axis] = event.jaxis.value; - axis_event_count[event.jaxis.which][event.jaxis.axis] = 1; - if (IsAxisAtPole(event.jaxis.value)) { - down = true; - } else { - break; + std::cerr << "axis" << event.jaxis.axis << " down event, has value " + << event.jaxis.value << " and timestamp " << event.jaxis.timestamp + << std::endl; + // if a button has been pressed down within 50ms of this axis movement, + // assume they are actually the same thing and skip this axis + if (buttonDownTimestamp && ((event.jaxis.timestamp >= buttonDownTimestamp && + event.jaxis.timestamp - buttonDownTimestamp <= 50) || + (event.jaxis.timestamp < buttonDownTimestamp && + buttonDownTimestamp - event.jaxis.timestamp <= 50))) { + axis_skip[id][axis] = true; + std::cerr << "skipping axis " << axis + << " because a button happened simultaneously"; + break; + } + + // skipping this axis + if (axis_skip[id][axis]) + break; + if (!axis_memory.count(id) || !axis_memory[id].count(axis)) { + // starting a new movement. + axisStartTimestamps[id][axis] = event.jaxis.timestamp; + axis_event_count[id][axis] = 1; + if (IsAxisAtExtreme(value)) { + // a single event with a value right at the extreme. + // Assume this is a digital "axis" and send the down + // signal with center set to 0. + event.jaxis.value = std::copysign(32767, value); + axis_center_value[id][axis] = 0; + return SDLEventToButtonParamPackage(state, event, true); } + // otherwise, this is our first event, identify the center + if (value < -28000) + axis_center_value[id][axis] = -32768; + else if (value > 28000) + axis_center_value[id][axis] = 32767; + else + axis_center_value[id][axis] = 0; + + axis_memory[id][axis] = axis_center_value[id][axis]; + break; } else { - axis_event_count[event.jaxis.which][event.jaxis.axis]++; - // The joystick and axis exist in our map if we take this branch, so no checks - // needed - if (std::abs( - (event.jaxis.value - axis_memory[event.jaxis.which][event.jaxis.axis]) / - 32767.0) < 0.5) { - break; - } else { - if (axis_event_count[event.jaxis.which][event.jaxis.axis] == 2 && - IsAxisAtPole(event.jaxis.value) && - IsAxisAtPole(axis_memory[event.jaxis.which][event.jaxis.axis])) { - // If we have exactly two events and both are near a pole, this is - // likely a digital input masquerading as an analog axis; Instead of - // trying to look at the direction the axis travelled, assume the first - // event was press and the second was release; This should handle most - // digital axes while deferring to the direction of travel for analog - // axes - event.jaxis.value = static_cast(std::copysign( - 32767, axis_memory[event.jaxis.which][event.jaxis.axis])); - } else { - // There are more than two events, so this is likely a true analog axis, - // check the direction it travelled - event.jaxis.value = static_cast(std::copysign( - 32767, event.jaxis.value - - axis_memory[event.jaxis.which][event.jaxis.axis])); - } - axis_memory.clear(); - axis_event_count.clear(); + axis_event_count[id][axis]++; + // only two events, second one at center, means this is a digital release + if (axis_event_count[id][axis] == 2 && IsAxisAtCenter(value, id, axis) && + IsAxisAtExtreme(axis_memory[id][axis])) { + // send the up signal for this digital axis, and clear. + axis_event_count[id][axis] = 0; + axis_memory[id][axis] = 0; + return SDLEventToButtonParamPackage(state, event, false); + } + if (IsAxisAtCenter(value, id, axis) && + IsAxisPastThreshold(axis_memory[id][axis], id, axis)) { + // returned to center, send the up signal + event.jaxis.value = static_cast(std::copysign( + 32767, axis_memory[id][axis] - axis_center_value[id][axis])); + axis_memory[id][axis] = 0; + axis_event_count[id][axis] = 0; + std::cerr << "sending up signal for axis " << axis; + return SDLEventToButtonParamPackage(state, event, false); + } else if (IsAxisAtCenter(axis_memory[id][axis], id, axis) && + IsAxisPastThreshold(event.jaxis.value, id, axis)) { + event.jaxis.value = static_cast( + std::copysign(32767, event.jaxis.value - axis_center_value[id][axis])); + // make this the start of the new event + axis_memory[id][axis] = event.jaxis.value; + std::cerr << "sending down signal for axis " << axis; + return SDLEventToButtonParamPackage(state, event, true); } } - return SDLEventToButtonParamPackage(state, event, down); break; case SDL_JOYBUTTONDOWN: + std::cerr << "button " << event.jbutton.button << " down event, has axis " + << event.jaxis.axis << " and timestamp " << event.jbutton.timestamp + << std::endl; + buttonDownTimestamp = event.jbutton.timestamp; return SDLEventToButtonParamPackage(state, event, true); break; case SDL_JOYBUTTONUP: + std::cerr << "button " << event.jbutton.button << " up event, timestamp " + << event.jbutton.timestamp; return SDLEventToButtonParamPackage(state, event, false); break; case SDL_JOYHATMOTION: @@ -1047,14 +1094,28 @@ public: } private: - // Determine whether an axis value is close to an extreme or center - // Some controllers have a digital D-Pad as a pair of analog sticks, with 3 possible values per - // axis, which is why the center must be considered a pole - bool IsAxisAtPole(int16_t value) { - return std::abs(value) >= 32767 || std::abs(value) < 327; + bool IsAxisAtCenter(int16_t value, SDL_JoystickID id, uint8_t axis) { + return std::abs(value - axis_center_value[id][axis]) < 367; } + + bool IsAxisPastThreshold(int16_t value, SDL_JoystickID id, uint8_t axis) { + return std::abs(value - axis_center_value[id][axis]) > 32767 / 2; + } + + bool IsAxisAtExtreme(int16_t value) { + return std::abs(value) > 32766; + } + + bool IsValueNear(int16_t value1, int16_t value2) {} + /** Holds the first received value for the axis. Used to + * identify situations where "released" is -32768 (some triggers) + */ + std::unordered_map> axis_center_value; std::unordered_map> axis_memory; std::unordered_map> axis_event_count; + std::unordered_map> axis_skip; + int buttonDownTimestamp = 0; + std::unordered_map> axisStartTimestamps; }; class SDLAnalogPoller final : public SDLPoller {