From 6bc1357d6a180f62e4bcbe349db5420c3a263a13 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:17:54 -0700 Subject: [PATCH] ddr: add support for analog axis-only pads and handle left+right / up+down input (#659) ## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change Some crappy DDR soft mats (either USB-based or console pads with crappy adapters that lack a dedicated "DDR pad mode") use X and Y analog axis for arrows, which means they cannot cleanly report Up+Down or Left+Right input. According to some ancient Stepmania code, a lot of these report a middle value (value between center and right, for example, for left+right). Borrow this hack from Stepmania and implement it in spice as analog input for DDR. Also, for rawinput/xinput automatic bind scenario, update the logic so that face buttons are always detected before analog axis / dpads for consistency. This would help with binding any DDR pads that simultaneously input face buttons / dpad / analog axis, so that we can prefer face buttons when detected. ## Testing I don't have any soft mats that have this, but it was tested with an xbox controller. --- src/spice2x/acio/mdxf/mdxf.cpp | 22 ++- src/spice2x/games/ddr/ddr.cpp | 46 ++++++ src/spice2x/games/ddr/ddr.h | 3 + src/spice2x/games/ddr/io.cpp | 43 ++++++ src/spice2x/games/ddr/io.h | 13 ++ src/spice2x/games/ddr/p3io/p3io.cpp | 50 ++++++- src/spice2x/games/io.cpp | 3 + src/spice2x/overlay/windows/config.cpp | 190 +++++++++++++------------ src/spice2x/rawinput/xinput.cpp | 105 ++++++++++---- src/spice2x/rawinput/xinput.h | 4 +- 10 files changed, 351 insertions(+), 128 deletions(-) diff --git a/src/spice2x/acio/mdxf/mdxf.cpp b/src/spice2x/acio/mdxf/mdxf.cpp index ca3f835..67648fc 100644 --- a/src/spice2x/acio/mdxf/mdxf.cpp +++ b/src/spice2x/acio/mdxf/mdxf.cpp @@ -2,6 +2,7 @@ #include "mdxf_poll.h" #include "avs/game.h" +#include "games/ddr/ddr.h" #include "games/ddr/io.h" #include "launcher/launcher.h" #include "rawinput/rawinput.h" @@ -357,14 +358,17 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP // decide on button map const size_t *button_map = nullptr; + int player = 0; switch (node) { case 17: case 25: button_map = &buttons_p1[0]; + player = 1; break; case 18: case 26: button_map = &buttons_p2[0]; + player = 2; break; } @@ -374,19 +378,27 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP if (source == EXTERNAL_POLL) { // get buttons auto &buttons = games::ddr::get_buttons(); + + // get analogs + bool analog_left = false; + bool analog_right = false; + games::ddr::get_analog_x_axis(player, analog_left, analog_right); + bool analog_up = false; + bool analog_down = false; + games::ddr::get_analog_y_axis(player, analog_up, analog_down); + uint8_t up_down = 0; uint8_t left_right = 0; - - if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0]))) { + if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0])) || analog_up) { up_down |= 0xF0; } - if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1]))) { + if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1])) || analog_down) { up_down |= 0x0F; } - if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2]))) { + if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2])) || analog_left) { left_right |= 0xF0; } - if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3]))) { + if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3])) || analog_right) { left_right |= 0x0F; } current_state = (uint16_t(up_down) << 8) | left_right; diff --git a/src/spice2x/games/ddr/ddr.cpp b/src/spice2x/games/ddr/ddr.cpp index ddfa8c5..685556b 100644 --- a/src/spice2x/games/ddr/ddr.cpp +++ b/src/spice2x/games/ddr/ddr.cpp @@ -291,4 +291,50 @@ namespace games::ddr { // dispose device hook devicehook_dispose(); } + + static void get_analog_xy_axis(Analog &analog, bool &less, bool &more) { + if (!analog.isSet()) { + return; + } + const auto value = GameAPI::Analogs::getState(RI_MGR, analog); + + // stepmania linux source: + // https://github.com/stepmania/stepmania/blob/d55acb1ba26f1c5b5e3048d6d6c0bd116625216f/src/arch/InputHandler/InputHandler_Linux_Event.cpp#L410 + + // for example, value cap with range [0, 255]: + // 127 = 0.498, neutral + // 128 = 0.502, both + // apparently some adapters report values like above; this obviously makes a lot of + // assumptions about bit width of the HID value cap and how the adapter reports + // neutral/both values, but this is good enough for parity with stepmania and its many forks + if (0.5001f < value && value < 0.75f) { + less |= true; + more |= true; + + } else if (value <= 0.25f) { + less |= true; + } else if (value >= 0.75f) { + more |= true; + } + } + + void get_analog_x_axis(int player, bool &left, bool &right) { + if (player != 1 && player != 2) { + log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player); + return; + } + + auto &analog = get_analogs().at(player == 1 ? Analogs::P1_LEFT_RIGHT : Analogs::P2_LEFT_RIGHT); + get_analog_xy_axis(analog, left, right); + } + + void get_analog_y_axis(int player, bool &up, bool &down) { + if (player != 1 && player != 2) { + log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player); + return; + } + + auto &analog = get_analogs().at(player == 1 ? Analogs::P1_UP_DOWN : Analogs::P2_UP_DOWN); + get_analog_xy_axis(analog, up, down); + } } diff --git a/src/spice2x/games/ddr/ddr.h b/src/spice2x/games/ddr/ddr.h index 7e685f4..cd905bb 100644 --- a/src/spice2x/games/ddr/ddr.h +++ b/src/spice2x/games/ddr/ddr.h @@ -23,4 +23,7 @@ namespace games::ddr { private: void register_codecs(); }; + + void get_analog_x_axis(int player, bool &left, bool &right); + void get_analog_y_axis(int player, bool &up, bool &down); } diff --git a/src/spice2x/games/ddr/io.cpp b/src/spice2x/games/ddr/io.cpp index e63cc45..4450497 100644 --- a/src/spice2x/games/ddr/io.cpp +++ b/src/spice2x/games/ddr/io.cpp @@ -35,6 +35,49 @@ std::vector