From 4aecdbbb25525630157ee7ef3bb3ee5e69120ff1 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 4 Dec 2023 11:57:49 +0100 Subject: [PATCH] input: new saturation setting for full analog axes. Gamepad settings UI Saturation reduces or increases the range of analog axes. Issue #675 Axes values are re-scaled with dead zone to allow small values. New Gamepad Settings dialog box to set up dead zone, saturation and rumble/haptic power. --- core/input/gamepad_device.cpp | 54 +++++++++++++++++------ core/input/gamepad_device.h | 10 +++++ core/input/mapping.cpp | 3 ++ core/input/mapping.h | 2 + core/rend/gui.cpp | 81 +++++++++++++++++++++++++++-------- 5 files changed, 119 insertions(+), 31 deletions(-) diff --git a/core/input/gamepad_device.cpp b/core/input/gamepad_device.cpp index 175437b77..8fdbc1b7e 100644 --- a/core/input/gamepad_device.cpp +++ b/core/input/gamepad_device.cpp @@ -179,6 +179,26 @@ bool GamepadDevice::gamepad_btn_input(u32 code, bool pressed) return rc; } +static DreamcastKey getOppositeAxis(DreamcastKey key) +{ + switch (key) + { + case DC_AXIS_RIGHT: return DC_AXIS_LEFT; + case DC_AXIS_LEFT: return DC_AXIS_RIGHT; + case DC_AXIS_UP: return DC_AXIS_DOWN; + case DC_AXIS_DOWN: return DC_AXIS_UP; + case DC_AXIS2_RIGHT: return DC_AXIS2_LEFT; + case DC_AXIS2_LEFT: return DC_AXIS2_RIGHT; + case DC_AXIS2_UP: return DC_AXIS2_DOWN; + case DC_AXIS2_DOWN: return DC_AXIS2_UP; + case DC_AXIS3_RIGHT: return DC_AXIS3_LEFT; + case DC_AXIS3_LEFT: return DC_AXIS3_RIGHT; + case DC_AXIS3_UP: return DC_AXIS3_DOWN; + case DC_AXIS3_DOWN: return DC_AXIS3_UP; + default: return key; + } +} + // // value must be >= -32768 and <= 32767 for full axes // and 0 to 32767 for half axes/triggers @@ -216,7 +236,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value) { //printf("AXIS %d Mapped to %d -> %d\n", key, value, v); s16 *this_axis; - s16 *other_axis; + int otherAxisValue; int axisDirection = -1; switch (key) { @@ -225,7 +245,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value) [[fallthrough]]; case DC_AXIS_LEFT: this_axis = &joyx[port]; - other_axis = &joyy[port]; + otherAxisValue = lastAxisValue[port][DC_AXIS_UP]; break; case DC_AXIS_DOWN: @@ -233,7 +253,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value) [[fallthrough]]; case DC_AXIS_UP: this_axis = &joyy[port]; - other_axis = &joyx[port]; + otherAxisValue = lastAxisValue[port][DC_AXIS_LEFT]; break; case DC_AXIS2_RIGHT: @@ -241,7 +261,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value) [[fallthrough]]; case DC_AXIS2_LEFT: this_axis = &joyrx[port]; - other_axis = &joyry[port]; + otherAxisValue = lastAxisValue[port][DC_AXIS2_UP]; break; case DC_AXIS2_DOWN: @@ -249,7 +269,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value) [[fallthrough]]; case DC_AXIS2_UP: this_axis = &joyry[port]; - other_axis = &joyrx[port]; + otherAxisValue = lastAxisValue[port][DC_AXIS2_LEFT]; break; case DC_AXIS3_RIGHT: @@ -257,7 +277,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value) [[fallthrough]]; case DC_AXIS3_LEFT: this_axis = &joy3x[port]; - other_axis = &joy3y[port]; + otherAxisValue = lastAxisValue[port][DC_AXIS3_UP]; break; case DC_AXIS3_DOWN: @@ -265,17 +285,18 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value) [[fallthrough]]; case DC_AXIS3_UP: this_axis = &joy3y[port]; - other_axis = &joy3x[port]; + otherAxisValue = lastAxisValue[port][DC_AXIS3_LEFT]; break; default: return false; } - // Lightgun with left analog stick int& lastValue = lastAxisValue[port][key]; - if (lastValue != v) + int& lastOpValue = lastAxisValue[port][getOppositeAxis(key)]; + if (lastValue != v || lastOpValue != v) { - lastValue = v; + lastValue = lastOpValue = v; + // Lightgun with left analog stick if (key == DC_AXIS_RIGHT || key == DC_AXIS_LEFT) mo_x_abs[port] = (std::abs(v) * axisDirection + 32768) * 639 / 65535; else if (key == DC_AXIS_UP || key == DC_AXIS_DOWN) @@ -283,14 +304,19 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value) } // Radial dead zone // FIXME compute both axes at the same time - v = std::min(32767, std::abs(v)); - if ((float)(v * v + *other_axis * *other_axis) < input_mapper->dead_zone * input_mapper->dead_zone * 32768.f * 32768.f) + const float nv = std::abs(v) / 32768.f; + const float r2 = nv * nv + otherAxisValue * otherAxisValue / 32768.f / 32768.f; + if (r2 < input_mapper->dead_zone * input_mapper->dead_zone || r2 == 0.f) { *this_axis = 0; - *other_axis = 0; } else - *this_axis = v * axisDirection; + { + float pdz = nv * input_mapper->dead_zone / std::sqrt(r2); + // there's a dead angular zone at 45° with saturation > 1 (both axes are saturated) + v = std::round((nv - pdz) / (1 - pdz) * 32768.f * input_mapper->saturation); + *this_axis = std::clamp(v * axisDirection, -32768, 32767); + } } else if (key != EMU_BTN_NONE && key <= DC_BTN_BITMAPPED_LAST) // Map triggers to digital buttons { diff --git a/core/input/gamepad_device.h b/core/input/gamepad_device.h index 9719561a7..4941f1c7f 100644 --- a/core/input/gamepad_device.h +++ b/core/input/gamepad_device.h @@ -80,6 +80,16 @@ public: save_mapping(); } } + float get_saturation() const { return input_mapper->saturation; } + void set_saturation(float saturation) + { + if (saturation != input_mapper->saturation) + { + input_mapper->saturation = saturation; + input_mapper->set_dirty(); + save_mapping(); + } + } static void Register(const std::shared_ptr& gamepad); diff --git a/core/input/mapping.cpp b/core/input/mapping.cpp index 0f4df76e9..5689eeffe 100644 --- a/core/input/mapping.cpp +++ b/core/input/mapping.cpp @@ -196,6 +196,8 @@ void InputMapping::load(FILE* fp) dz = std::min(dz, 100); dz = std::max(dz, 0); this->dead_zone = (float)dz / 100.f; + int sat = std::clamp(mf.get_int("emulator", "saturation", 100), 50, 200); + this->saturation = (float)sat / 100.f; this->rumblePower = mf.get_int("emulator", "rumble_power", this->rumblePower); version = mf.get_int("emulator", "version", 1); @@ -428,6 +430,7 @@ bool InputMapping::save(const std::string& name) mf.set("emulator", "mapping_name", this->name); mf.set_int("emulator", "dead_zone", (int)std::round(this->dead_zone * 100.f)); + mf.set_int("emulator", "saturation", (int)std::round(this->saturation * 100.f)); mf.set_int("emulator", "rumble_power", this->rumblePower); mf.set_int("emulator", "version", 3); diff --git a/core/input/mapping.h b/core/input/mapping.h index 9e3d2530e..4e8925005 100644 --- a/core/input/mapping.h +++ b/core/input/mapping.h @@ -35,6 +35,7 @@ public: InputMapping(const InputMapping& other) { name = other.name; dead_zone = other.dead_zone; + saturation = other.saturation; for (int port = 0; port < 4; port++) { buttons[port] = other.buttons[port]; @@ -44,6 +45,7 @@ public: std::string name; float dead_zone = 0.1f; + float saturation = 1.0f; int rumblePower = 100; int version = 3; diff --git a/core/rend/gui.cpp b/core/rend/gui.cpp index 04ecd8f0d..bf8b827ff 100644 --- a/core/rend/gui.cpp +++ b/core/rend/gui.cpp @@ -1257,6 +1257,61 @@ static void controller_mapping_popup(const std::shared_ptr& gamep ImGui::PopStyleVar(); } +static void gamepadSettingsPopup(const std::shared_ptr& gamepad) +{ + centerNextWindow(); + ImGui::SetNextWindowSize(min(ImGui::GetIO().DisplaySize, ScaledVec2(450.f, 300.f))); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); + if (ImGui::BeginPopupModal("Gamepad Settings", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + if (ImGui::Button("Done", ScaledVec2(100, 30))) + { + ImGui::CloseCurrentPopup(); + gamepad->save_mapping(map_system); + ImGui::EndPopup(); + ImGui::PopStyleVar(); + return; + } + ImGui::NewLine(); + if (gamepad->is_virtual_gamepad()) + { + header("Haptic"); + OptionSlider("Power", config::VirtualGamepadVibration, 0, 60, "Haptic feedback power"); + } + else if (gamepad->is_rumble_enabled()) + { + header("Rumble"); + int power = gamepad->get_rumble_power(); + ImGui::SetNextItemWidth(300 * settings.display.uiScale); + if (ImGui::SliderInt("Power", &power, 0, 100, "%d%%")) + gamepad->set_rumble_power(power); + ImGui::SameLine(); + ShowHelpMarker("Rumble power"); + } + if (gamepad->has_analog_stick()) + { + header("Thumbsticks"); + int deadzone = std::round(gamepad->get_dead_zone() * 100.f); + ImGui::SetNextItemWidth(300 * settings.display.uiScale); + if (ImGui::SliderInt("Dead zone", &deadzone, 0, 100, "%d%%")) + gamepad->set_dead_zone(deadzone / 100.f); + ImGui::SameLine(); + ShowHelpMarker("Minimum deflection to register as input"); + int saturation = std::round(gamepad->get_saturation() * 100.f); + ImGui::SetNextItemWidth(300 * settings.display.uiScale); + if (ImGui::SliderInt("Saturation", &saturation, 50, 200, "%d%%")) + gamepad->set_saturation(saturation / 100.f); + ImGui::SameLine(); + ShowHelpMarker("Value sent to the game at 100% thumbstick deflection. " + "Values greater than 100% will saturate before full deflection of the thumbstick."); + } + + ImGui::EndPopup(); + } + ImGui::PopStyleVar(); +} + void error_popup() { if (!error_msg_shown && !error_msg.empty()) @@ -1690,31 +1745,23 @@ static void gui_display_settings() #ifdef __ANDROID__ if (gamepad->is_virtual_gamepad()) { - if (ImGui::Button("Edit")) + if (ImGui::Button("Edit Layout")) { vjoy_start_editing(); gui_setState(GuiState::VJoyEdit); } - ImGui::SameLine(); - OptionSlider("Haptic", config::VirtualGamepadVibration, 0, 60); } - else #endif - if (gamepad->is_rumble_enabled()) + if (gamepad->is_rumble_enabled() || gamepad->has_analog_stick() +#ifdef __ANDROID__ + || gamepad->is_virtual_gamepad() +#endif + ) { ImGui::SameLine(0, 16 * settings.display.uiScale); - int power = gamepad->get_rumble_power(); - ImGui::SetNextItemWidth(150 * settings.display.uiScale); - if (ImGui::SliderInt("Rumble", &power, 0, 100, "%d%%")) - gamepad->set_rumble_power(power); - } - if (gamepad->has_analog_stick()) - { - ImGui::SameLine(0, 16 * settings.display.uiScale); - int deadzone = std::round(gamepad->get_dead_zone() * 100.f); - ImGui::SetNextItemWidth(150 * settings.display.uiScale); - if (ImGui::SliderInt("Dead zone", &deadzone, 0, 100, "%d%%")) - gamepad->set_dead_zone(deadzone / 100.f); + if (ImGui::Button("Settings")) + ImGui::OpenPopup("Gamepad Settings"); + gamepadSettingsPopup(gamepad); } ImGui::NextColumn(); ImGui::PopID();